+Pangolin is a self-hosted tunneled reverse proxy server with identity and context aware access control, designed to easily expose and protect applications running anywhere. Pangolin acts as a central hub and connects isolated networks — even those behind restrictive firewalls — through encrypted tunnels, enabling easy access to remote services without opening ports or requiring a VPN.
-Pangolin is a self-hosted tunneled reverse proxy server with identity and access control, designed to securely expose private resources on distributed networks. Acting as a central hub, it connects isolated networks — even those behind restrictive firewalls — through encrypted tunnels, enabling easy access to remote services without opening ports.
+## Installation
-
-
-
-
-## Key Features
-
-### Reverse Proxy Through WireGuard Tunnel
-
-- Expose private resources on your network **without opening ports** (firewall punching).
-- Secure and easy to configure private connectivity via a custom **user space WireGuard client**, [Newt](https://github.com/fosrl/newt).
-- Built-in support for any WireGuard client.
-- Automated **SSL certificates** (https) via [LetsEncrypt](https://letsencrypt.org/).
-- Support for HTTP/HTTPS and **raw TCP/UDP services**.
-- Load balancing.
-- Extend functionality with existing [Traefik](https://github.com/traefik/traefik) plugins, such as [CrowdSec](https://plugins.traefik.io/plugins/6335346ca4caa9ddeffda116/crowdsec-bouncer-traefik-plugin) and [Geoblock](https://github.com/PascalMinder/geoblock).
- - **Automatically install and configure Crowdsec via Pangolin's installer script.**
-- Attach as many sites to the central server as you wish.
-
-### Identity & Access Management
-
-- Centralized authentication system using platform SSO. **Users will only have to manage one login.**
-- **Define access control rules for IPs, IP ranges, and URL paths per resource.**
-- TOTP with backup codes for two-factor authentication.
-- Create organizations, each with multiple sites, users, and roles.
-- **Role-based access control** to manage resource access permissions.
-- Additional authentication options include:
- - Email whitelisting with **one-time passcodes.**
- - **Temporary, self-destructing share links.**
- - Resource specific pin codes.
- - Resource specific passwords.
- - Passkeys
-- External identity provider (IdP) support with OAuth2/OIDC, such as Authentik, Keycloak, Okta, and others.
- - Auto-provision users and roles from your IdP.
-
-
-
-## Use Cases
-
-### Manage Access to Internal Apps
-
-- Grant users access to your apps from anywhere using just a web browser. No client software required.
-
-### Developers and DevOps
-
-- Expose and test internal tools and dashboards like **Grafana**. Bring localhost or private IPs online for easy access.
-
-### Secure API Gateway
-
-- One application load balancer across multiple clouds and on-premises.
-
-### IoT and Edge Devices
-
-- Easily expose **IoT devices**, **edge servers**, or **Raspberry Pi** to the internet for field equipment monitoring.
-
-
+Check out the [quick install guide](https://docs.digpangolin.com) for how to install and set up Pangolin.
## Deployment Options
-### Fully Self Hosted
+| | Description |
+|-----------------|--------------|
+| **Self-Host: Community Edition** | Free, open source, and AGPL-3 compliant. |
+| **Self-Host: Enterprise Edition** | Licensed under Fossorial Commercial License. Free for personal and hobbyist use, and for businesses earning under \$100K USD annually. |
+| **Pangolin Cloud** | Fully managed service with instant setup and pay-as-you-go pricing — no infrastructure required. Or, self-host your own [remote node](https://github.com/fosrl/remote-note) and connect to our control plane. |
-Host the full application on your own server or on the cloud with a VPS. Take a look at the [documentation](https://docs.digpangolin.com/self-host/quick-install) to get started.
+## Key Features
-> Many of our users have had a great experience with [RackNerd](https://my.racknerd.com/aff.php?aff=13788). Depending on promotions, you can get a [**VPS with 1 vCPU, 1GB RAM, and ~20GB SSD for just around $12/year**](https://my.racknerd.com/aff.php?aff=13788&pid=912). That's a great deal!
+Pangolin packages everything you need for seamless application access and exposure into one cohesive platform.
-### Pangolin Cloud
+| | |
+|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|--------------------------------------------------------------------|
+| **Manage applications in one place**
Pangolin provides a unified dashboard where you can monitor, configure, and secure all of your services regardless of where they are hosted. |
|
+| **Reverse proxy across networks anywhere**
Route traffic via tunnels to any private network. Pangolin works like a reverse proxy that spans multiple networks and handles routing, load balancing, health checking, and more to the right services on the other end. |
|
+| **Enforce identity and context aware rules**
Protect your applications with identity and context aware rules such as SSO, OIDC, PIN, password, temporary share links, geolocation, IP, and more. |
|
+| **Quickly connect Pangolin sites**
Pangolin's lightweight [Newt](https://github.com/fosrl/newt) client runs in userspace and can run anywhere. Use it as a site connector to route traffic to backends across all of your environments. |
|
-Easy to use with simple [pay as you go pricing](https://digpangolin.com/pricing). [Check it out here](https://pangolin.fossorial.io/auth/signup).
+## Get Started
-- Everything you get with self hosted Pangolin, but fully managed for you.
+### Check out the docs
-### Managed & High Availability
+We encourage everyone to read the full documentation first, which is
+available at [docs.digpangolin.com](https://docs.digpangolin.com). This README provides only a very brief subset of
+the docs to illustrate some basic ideas.
-Managed control plane, your infrastructure
+### Sign up and try now
-- We manage database and control plane.
-- You self-host lightweight exit-node.
-- Traffic flows through your infra.
-- We coordinate failover between your nodes or to Cloud when things go bad.
-
-Try it out using [Pangolin Cloud](https://pangolin.fossorial.io)
-
-### Full Enterprise On-Premises
-
-[Contact us](mailto:numbat@fossorial.io) for a full distributed and enterprise deployments on your infrastructure controlled by your team.
-
-## Project Development / Roadmap
-
-We want to hear your feature requests! Add them to the [discussion board](https://github.com/orgs/fosrl/discussions/categories/feature-requests).
+For Pangolin's managed service, you will first need to create an account at
+[pangolin.fossorial.io](https://pangolin.fossorial.io). We have a generous free tier to get started.
## Licensing
-Pangolin is dual licensed under the AGPL-3 and the Fossorial Commercial license. For inquiries about commercial licensing, please contact us at [numbat@fossorial.io](mailto:numbat@fossorial.io).
+Pangolin is dual licensed under the AGPL-3 and the [Fossorial Commercial License](https://digpangolin.com/fcl.html). For inquiries about commercial licensing, please contact us at [contact@fossorial.io](mailto:contact@fossorial.io).
## Contributions
-Looking for something to contribute? Take a look at issues marked with [help wanted](https://github.com/fosrl/pangolin/issues?q=is%3Aissue%20state%3Aopen%20label%3A%22help%20wanted%22). Also take a look through the freature requests in Discussions - any are available and some are marked as a good first issue.
-
Please see [CONTRIBUTING](./CONTRIBUTING.md) in the repository for guidelines and best practices.
-
-Please post bug reports and other functional issues in the [Issues](https://github.com/fosrl/pangolin/issues) section of the repository.
-
-If you are looking to help with translations, please contribute [on Crowdin](https://crowdin.com/project/fossorial-pangolin) or open a PR with changes to the translations files found in `messages/`.
\ No newline at end of file
diff --git a/drizzle.pg.config.ts b/drizzle.pg.config.ts
index 2b10d2af..febd5f45 100644
--- a/drizzle.pg.config.ts
+++ b/drizzle.pg.config.ts
@@ -1,16 +1,9 @@
import { defineConfig } from "drizzle-kit";
import path from "path";
-import { build } from "@server/build";
-let schema;
-if (build === "oss") {
- schema = [path.join("server", "db", "pg", "schema.ts")];
-} else {
- schema = [
- path.join("server", "db", "pg", "schema.ts"),
- path.join("server", "db", "pg", "privateSchema.ts")
- ];
-}
+const schema = [
+ path.join("server", "db", "pg", "schema"),
+];
export default defineConfig({
dialect: "postgresql",
diff --git a/drizzle.sqlite.config.ts b/drizzle.sqlite.config.ts
index 25bbe7f3..4912c256 100644
--- a/drizzle.sqlite.config.ts
+++ b/drizzle.sqlite.config.ts
@@ -1,17 +1,10 @@
-import { build } from "@server/build";
import { APP_PATH } from "@server/lib/consts";
import { defineConfig } from "drizzle-kit";
import path from "path";
-let schema;
-if (build === "oss") {
- schema = [path.join("server", "db", "sqlite", "schema.ts")];
-} else {
- schema = [
- path.join("server", "db", "sqlite", "schema.ts"),
- path.join("server", "db", "sqlite", "privateSchema.ts")
- ];
-}
+const schema = [
+ path.join("server", "db", "sqlite", "schema"),
+];
export default defineConfig({
dialect: "sqlite",
diff --git a/esbuild.mjs b/esbuild.mjs
index 8086a77e..7f67fe81 100644
--- a/esbuild.mjs
+++ b/esbuild.mjs
@@ -2,8 +2,9 @@ import esbuild from "esbuild";
import yargs from "yargs";
import { hideBin } from "yargs/helpers";
import { nodeExternalsPlugin } from "esbuild-node-externals";
+import path from "path";
+import fs from "fs";
// import { glob } from "glob";
-// import path from "path";
const banner = `
// patch __dirname
@@ -18,7 +19,7 @@ const require = topLevelCreateRequire(import.meta.url);
`;
const argv = yargs(hideBin(process.argv))
- .usage("Usage: $0 -entry [string] -out [string]")
+ .usage("Usage: $0 -entry [string] -out [string] -build [string]")
.option("entry", {
alias: "e",
describe: "Entry point file",
@@ -31,6 +32,13 @@ const argv = yargs(hideBin(process.argv))
type: "string",
demandOption: true,
})
+ .option("build", {
+ alias: "b",
+ describe: "Build type (oss, saas, enterprise)",
+ type: "string",
+ choices: ["oss", "saas", "enterprise"],
+ default: "oss",
+ })
.help()
.alias("help", "h").argv;
@@ -46,6 +54,179 @@ function getPackagePaths() {
return ["package.json"];
}
+// Plugin to guard against bad imports from #private
+function privateImportGuardPlugin() {
+ return {
+ name: "private-import-guard",
+ setup(build) {
+ const violations = [];
+
+ build.onResolve({ filter: /^#private\// }, (args) => {
+ const importingFile = args.importer;
+
+ // Check if the importing file is NOT in server/private
+ const normalizedImporter = path.normalize(importingFile);
+ const isInServerPrivate = normalizedImporter.includes(path.normalize("server/private"));
+
+ if (!isInServerPrivate) {
+ const violation = {
+ file: importingFile,
+ importPath: args.path,
+ resolveDir: args.resolveDir
+ };
+ violations.push(violation);
+
+ console.log(`PRIVATE IMPORT VIOLATION:`);
+ console.log(` File: ${importingFile}`);
+ console.log(` Import: ${args.path}`);
+ console.log(` Resolve dir: ${args.resolveDir || 'N/A'}`);
+ console.log('');
+ }
+
+ // Return null to let the default resolver handle it
+ return null;
+ });
+
+ build.onEnd((result) => {
+ if (violations.length > 0) {
+ console.log(`\nSUMMARY: Found ${violations.length} private import violation(s):`);
+ violations.forEach((v, i) => {
+ console.log(` ${i + 1}. ${path.relative(process.cwd(), v.file)} imports ${v.importPath}`);
+ });
+ console.log('');
+
+ result.errors.push({
+ text: `Private import violations detected: ${violations.length} violation(s) found`,
+ location: null,
+ notes: violations.map(v => ({
+ text: `${path.relative(process.cwd(), v.file)} imports ${v.importPath}`,
+ location: null
+ }))
+ });
+ }
+ });
+ }
+ };
+}
+
+// Plugin to guard against bad imports from #private
+function dynamicImportGuardPlugin() {
+ return {
+ name: "dynamic-import-guard",
+ setup(build) {
+ const violations = [];
+
+ build.onResolve({ filter: /^#dynamic\// }, (args) => {
+ const importingFile = args.importer;
+
+ // Check if the importing file is NOT in server/private
+ const normalizedImporter = path.normalize(importingFile);
+ const isInServerPrivate = normalizedImporter.includes(path.normalize("server/private"));
+
+ if (isInServerPrivate) {
+ const violation = {
+ file: importingFile,
+ importPath: args.path,
+ resolveDir: args.resolveDir
+ };
+ violations.push(violation);
+
+ console.log(`DYNAMIC IMPORT VIOLATION:`);
+ console.log(` File: ${importingFile}`);
+ console.log(` Import: ${args.path}`);
+ console.log(` Resolve dir: ${args.resolveDir || 'N/A'}`);
+ console.log('');
+ }
+
+ // Return null to let the default resolver handle it
+ return null;
+ });
+
+ build.onEnd((result) => {
+ if (violations.length > 0) {
+ console.log(`\nSUMMARY: Found ${violations.length} dynamic import violation(s):`);
+ violations.forEach((v, i) => {
+ console.log(` ${i + 1}. ${path.relative(process.cwd(), v.file)} imports ${v.importPath}`);
+ });
+ console.log('');
+
+ result.errors.push({
+ text: `Dynamic import violations detected: ${violations.length} violation(s) found`,
+ location: null,
+ notes: violations.map(v => ({
+ text: `${path.relative(process.cwd(), v.file)} imports ${v.importPath}`,
+ location: null
+ }))
+ });
+ }
+ });
+ }
+ };
+}
+
+// Plugin to dynamically switch imports based on build type
+function dynamicImportSwitcherPlugin(buildValue) {
+ return {
+ name: "dynamic-import-switcher",
+ setup(build) {
+ const switches = [];
+
+ build.onStart(() => {
+ console.log(`Dynamic import switcher using build type: ${buildValue}`);
+ });
+
+ build.onResolve({ filter: /^#dynamic\// }, (args) => {
+ // Extract the path after #dynamic/
+ const dynamicPath = args.path.replace(/^#dynamic\//, '');
+
+ // Determine the replacement based on build type
+ let replacement;
+ if (buildValue === "oss") {
+ replacement = `#open/${dynamicPath}`;
+ } else if (buildValue === "saas" || buildValue === "enterprise") {
+ replacement = `#closed/${dynamicPath}`; // We use #closed here so that the route guards dont complain after its been changed but this is the same as #private
+ } else {
+ console.warn(`Unknown build type '${buildValue}', defaulting to #open/`);
+ replacement = `#open/${dynamicPath}`;
+ }
+
+ const switchInfo = {
+ file: args.importer,
+ originalPath: args.path,
+ replacementPath: replacement,
+ buildType: buildValue
+ };
+ switches.push(switchInfo);
+
+ console.log(`DYNAMIC IMPORT SWITCH:`);
+ console.log(` File: ${args.importer}`);
+ console.log(` Original: ${args.path}`);
+ console.log(` Switched to: ${replacement} (build: ${buildValue})`);
+ console.log('');
+
+ // Rewrite the import path and let the normal resolution continue
+ return build.resolve(replacement, {
+ importer: args.importer,
+ namespace: args.namespace,
+ resolveDir: args.resolveDir,
+ kind: args.kind
+ });
+ });
+
+ build.onEnd((result) => {
+ if (switches.length > 0) {
+ console.log(`\nDYNAMIC IMPORT SUMMARY: Switched ${switches.length} import(s) for build type '${buildValue}':`);
+ switches.forEach((s, i) => {
+ console.log(` ${i + 1}. ${path.relative(process.cwd(), s.file)}`);
+ console.log(` ${s.originalPath} → ${s.replacementPath}`);
+ });
+ console.log('');
+ }
+ });
+ }
+ };
+}
+
esbuild
.build({
entryPoints: [argv.entry],
@@ -59,6 +240,9 @@ esbuild
platform: "node",
external: ["body-parser"],
plugins: [
+ privateImportGuardPlugin(),
+ dynamicImportGuardPlugin(),
+ dynamicImportSwitcherPlugin(argv.build),
nodeExternalsPlugin({
packagePath: getPackagePaths(),
}),
@@ -66,7 +250,27 @@ esbuild
sourcemap: "inline",
target: "node22",
})
- .then(() => {
+ .then((result) => {
+ // Check if there were any errors in the build result
+ if (result.errors && result.errors.length > 0) {
+ console.error(`Build failed with ${result.errors.length} error(s):`);
+ result.errors.forEach((error, i) => {
+ console.error(`${i + 1}. ${error.text}`);
+ if (error.notes) {
+ error.notes.forEach(note => {
+ console.error(` - ${note.text}`);
+ });
+ }
+ });
+
+ // remove the output file if it was created
+ if (fs.existsSync(argv.out)) {
+ fs.unlinkSync(argv.out);
+ }
+
+ process.exit(1);
+ }
+
console.log("Build completed successfully");
})
.catch((error) => {
diff --git a/install/config/config.yml b/install/config/config.yml
index b86f7890..c4b76057 100644
--- a/install/config/config.yml
+++ b/install/config/config.yml
@@ -1,15 +1,10 @@
# To see all available options, please visit the docs:
-# https://docs.digpangolin.com/self-host/advanced/config-file
+# https://docs.digpangolin.com/
gerbil:
start_port: 51820
base_endpoint: "{{.DashboardDomain}}"
-{{if .HybridMode}}
-managed:
- id: "{{.HybridId}}"
- secret: "{{.HybridSecret}}"
-
-{{else}}
+
app:
dashboard_url: "https://{{.DashboardDomain}}"
log_level: "info"
@@ -28,6 +23,7 @@ server:
methods: ["GET", "POST", "PUT", "DELETE", "PATCH"]
allowed_headers: ["X-CSRF-Token", "Content-Type"]
credentials: false
+ {{if .EnableGeoblocking}}maxmind_db_path: "./config/GeoLite2-Country.mmdb"{{end}}
{{if .EnableEmail}}
email:
smtp_host: "{{.EmailSMTPHost}}"
@@ -40,5 +36,4 @@ flags:
require_email_verification: {{.EnableEmail}}
disable_signup_without_invite: true
disable_user_create_org: false
- allow_raw_resources: true
-{{end}}
\ No newline at end of file
+ allow_raw_resources: true
\ No newline at end of file
diff --git a/install/config/docker-compose.yml b/install/config/docker-compose.yml
index 97b30317..6ec3989c 100644
--- a/install/config/docker-compose.yml
+++ b/install/config/docker-compose.yml
@@ -6,8 +6,8 @@ services:
restart: unless-stopped
volumes:
- ./config:/app/config
- - pangolin-data:/var/certificates
- - pangolin-data:/var/dynamic
+ - pangolin-data-certificates:/var/certificates
+ - pangolin-data-dynamic:/var/dynamic
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:3001/api/v1/"]
interval: "10s"
@@ -33,7 +33,7 @@ services:
ports:
- 51820:51820/udp
- 21820:21820/udp
- - 443:{{if .HybridMode}}8443{{else}}443{{end}}
+ - 443:443
- 80:80
{{end}}
traefik:
@@ -57,8 +57,8 @@ services:
- ./config/letsencrypt:/letsencrypt # Volume to store the Let's Encrypt certificates
- ./config/traefik/logs:/var/log/traefik # Volume to store Traefik logs
# Shared volume for certificates and dynamic config in file mode
- - pangolin-data:/var/certificates:ro
- - pangolin-data:/var/dynamic:ro
+ - pangolin-data-certificates:/var/certificates:ro
+ - pangolin-data-dynamic:/var/dynamic:ro
networks:
default:
@@ -67,4 +67,5 @@ networks:
{{if .EnableIPv6}} enable_ipv6: true{{end}}
volumes:
- pangolin-data:
+ pangolin-data-dynamic:
+ pangolin-data-certificates:
diff --git a/install/config/traefik/traefik_config.yml b/install/config/traefik/traefik_config.yml
index 8bb5aa6c..a9693ce6 100644
--- a/install/config/traefik/traefik_config.yml
+++ b/install/config/traefik/traefik_config.yml
@@ -3,17 +3,12 @@ api:
dashboard: true
providers:
-{{if not .HybridMode}}
http:
endpoint: "http://pangolin:3001/api/v1/traefik-config"
pollInterval: "5s"
file:
filename: "/etc/traefik/dynamic_config.yml"
-{{else}}
- file:
- directory: "/var/dynamic"
- watch: true
-{{end}}
+
experimental:
plugins:
badger:
@@ -27,7 +22,7 @@ log:
maxBackups: 3
maxAge: 3
compress: true
-{{if not .HybridMode}}
+
certificatesResolvers:
letsencrypt:
acme:
@@ -36,22 +31,18 @@ certificatesResolvers:
email: "{{.LetsEncryptEmail}}"
storage: "/letsencrypt/acme.json"
caServer: "https://acme-v02.api.letsencrypt.org/directory"
-{{end}}
+
entryPoints:
web:
address: ":80"
websecure:
address: ":443"
-{{if .HybridMode}} proxyProtocol:
- trustedIPs:
- - 0.0.0.0/0
- - ::1/128{{end}}
transport:
respondingTimeouts:
readTimeout: "30m"
-{{if not .HybridMode}} http:
+ http:
tls:
- certResolver: "letsencrypt"{{end}}
+ certResolver: "letsencrypt"
serversTransport:
insecureSkipVerify: true
diff --git a/install/main.go b/install/main.go
index f16dc214..72ffbac0 100644
--- a/install/main.go
+++ b/install/main.go
@@ -2,7 +2,6 @@ package main
import (
"bufio"
- "bytes"
"embed"
"fmt"
"io"
@@ -48,10 +47,8 @@ type Config struct {
InstallGerbil bool
TraefikBouncerKey string
DoCrowdsecInstall bool
+ EnableGeoblocking bool
Secret string
- HybridMode bool
- HybridId string
- HybridSecret string
}
type SupportedContainer string
@@ -98,24 +95,6 @@ func main() {
fmt.Println("\n=== Generating Configuration Files ===")
- // If the secret and id are not generated then generate them
- if config.HybridMode && (config.HybridId == "" || config.HybridSecret == "") {
- // fmt.Println("Requesting hybrid credentials from cloud...")
- credentials, err := requestHybridCredentials()
- if err != nil {
- fmt.Printf("Error requesting hybrid credentials: %v\n", err)
- fmt.Println("Please obtain credentials manually from the dashboard and run the installer again.")
- os.Exit(1)
- }
- config.HybridId = credentials.RemoteExitNodeId
- config.HybridSecret = credentials.Secret
- fmt.Printf("Your managed credentials have been obtained successfully.\n")
- fmt.Printf(" ID: %s\n", config.HybridId)
- fmt.Printf(" Secret: %s\n", config.HybridSecret)
- fmt.Println("Take these to the Pangolin dashboard https://pangolin.fossorial.io to adopt your node.")
- readBool(reader, "Have you adopted your node?", true)
- }
-
if err := createConfigFiles(config); err != nil {
fmt.Printf("Error creating config files: %v\n", err)
os.Exit(1)
@@ -125,6 +104,15 @@ func main() {
fmt.Println("\nConfiguration files created successfully!")
+ // Download MaxMind database if requested
+ if config.EnableGeoblocking {
+ fmt.Println("\n=== Downloading MaxMind Database ===")
+ if err := downloadMaxMindDatabase(); err != nil {
+ fmt.Printf("Error downloading MaxMind database: %v\n", err)
+ fmt.Println("You can download it manually later if needed.")
+ }
+ }
+
fmt.Println("\n=== Starting installation ===")
if readBool(reader, "Would you like to install and start the containers?", true) {
@@ -172,9 +160,34 @@ func main() {
} else {
alreadyInstalled = true
fmt.Println("Looks like you already installed Pangolin!")
+
+ // Check if MaxMind database exists and offer to update it
+ fmt.Println("\n=== MaxMind Database Update ===")
+ if _, err := os.Stat("config/GeoLite2-Country.mmdb"); err == nil {
+ fmt.Println("MaxMind GeoLite2 Country database found.")
+ if readBool(reader, "Would you like to update the MaxMind database to the latest version?", false) {
+ if err := downloadMaxMindDatabase(); err != nil {
+ fmt.Printf("Error updating MaxMind database: %v\n", err)
+ fmt.Println("You can try updating it manually later if needed.")
+ }
+ }
+ } else {
+ fmt.Println("MaxMind GeoLite2 Country database not found.")
+ if readBool(reader, "Would you like to download the MaxMind GeoLite2 database for geoblocking functionality?", false) {
+ if err := downloadMaxMindDatabase(); err != nil {
+ fmt.Printf("Error downloading MaxMind database: %v\n", err)
+ fmt.Println("You can try downloading it manually later if needed.")
+ }
+ // Now you need to update your config file accordingly to enable geoblocking
+ fmt.Println("Please remember to update your config/config.yml file to enable geoblocking! \n")
+ // add maxmind_db_path: "./config/GeoLite2-Country.mmdb" under server
+ fmt.Println("Add the following line under the 'server' section:")
+ fmt.Println(" maxmind_db_path: \"./config/GeoLite2-Country.mmdb\"")
+ }
+ }
}
- if !checkIsCrowdsecInstalledInCompose() && !checkIsPangolinInstalledWithHybrid() {
+ if !checkIsCrowdsecInstalledInCompose() {
fmt.Println("\n=== CrowdSec Install ===")
// check if crowdsec is installed
if readBool(reader, "Would you like to install CrowdSec?", false) {
@@ -230,7 +243,7 @@ func main() {
}
}
- if !config.HybridMode && !alreadyInstalled {
+ if !alreadyInstalled {
// Setup Token Section
fmt.Println("\n=== Setup Token ===")
@@ -251,9 +264,7 @@ func main() {
fmt.Println("\nInstallation complete!")
- if !config.HybridMode && !checkIsPangolinInstalledWithHybrid() {
- fmt.Printf("\nTo complete the initial setup, please visit:\nhttps://%s/auth/initial-setup\n", config.DashboardDomain)
- }
+ fmt.Printf("\nTo complete the initial setup, please visit:\nhttps://%s/auth/initial-setup\n", config.DashboardDomain)
}
func podmanOrDocker(reader *bufio.Reader) SupportedContainer {
@@ -328,66 +339,38 @@ func collectUserInput(reader *bufio.Reader) Config {
// Basic configuration
fmt.Println("\n=== Basic Configuration ===")
- for {
- response := readString(reader, "Do you want to install Pangolin as a cloud-managed (beta) node? (yes/no)", "")
- if strings.EqualFold(response, "yes") || strings.EqualFold(response, "y") {
- config.HybridMode = true
- break
- } else if strings.EqualFold(response, "no") || strings.EqualFold(response, "n") {
- config.HybridMode = false
- break
- }
- fmt.Println("Please answer 'yes' or 'no'")
+
+ config.BaseDomain = readString(reader, "Enter your base domain (no subdomain e.g. example.com)", "")
+
+ // Set default dashboard domain after base domain is collected
+ defaultDashboardDomain := ""
+ if config.BaseDomain != "" {
+ defaultDashboardDomain = "pangolin." + config.BaseDomain
+ }
+ config.DashboardDomain = readString(reader, "Enter the domain for the Pangolin dashboard", defaultDashboardDomain)
+ config.LetsEncryptEmail = readString(reader, "Enter email for Let's Encrypt certificates", "")
+ config.InstallGerbil = readBool(reader, "Do you want to use Gerbil to allow tunneled connections", true)
+
+ // Email configuration
+ fmt.Println("\n=== Email Configuration ===")
+ config.EnableEmail = readBool(reader, "Enable email functionality (SMTP)", false)
+
+ if config.EnableEmail {
+ config.EmailSMTPHost = readString(reader, "Enter SMTP host", "")
+ config.EmailSMTPPort = readInt(reader, "Enter SMTP port (default 587)", 587)
+ config.EmailSMTPUser = readString(reader, "Enter SMTP username", "")
+ config.EmailSMTPPass = readString(reader, "Enter SMTP password", "") // Should this be readPassword?
+ config.EmailNoReply = readString(reader, "Enter no-reply email address", "")
}
- if config.HybridMode {
- alreadyHaveCreds := readBool(reader, "Do you already have credentials from the dashboard? If not, we will create them later", false)
-
- if alreadyHaveCreds {
- config.HybridId = readString(reader, "Enter your ID", "")
- config.HybridSecret = readString(reader, "Enter your secret", "")
- }
-
- // Try to get public IP as default
- publicIP := getPublicIP()
- if publicIP != "" {
- fmt.Printf("Detected public IP: %s\n", publicIP)
- }
- config.DashboardDomain = readString(reader, "The public addressable IP address for this node or a domain pointing to it", publicIP)
- config.InstallGerbil = true
- } else {
- config.BaseDomain = readString(reader, "Enter your base domain (no subdomain e.g. example.com)", "")
-
- // Set default dashboard domain after base domain is collected
- defaultDashboardDomain := ""
- if config.BaseDomain != "" {
- defaultDashboardDomain = "pangolin." + config.BaseDomain
- }
- config.DashboardDomain = readString(reader, "Enter the domain for the Pangolin dashboard", defaultDashboardDomain)
- config.LetsEncryptEmail = readString(reader, "Enter email for Let's Encrypt certificates", "")
- config.InstallGerbil = readBool(reader, "Do you want to use Gerbil to allow tunneled connections", true)
-
- // Email configuration
- fmt.Println("\n=== Email Configuration ===")
- config.EnableEmail = readBool(reader, "Enable email functionality (SMTP)", false)
-
- if config.EnableEmail {
- config.EmailSMTPHost = readString(reader, "Enter SMTP host", "")
- config.EmailSMTPPort = readInt(reader, "Enter SMTP port (default 587)", 587)
- config.EmailSMTPUser = readString(reader, "Enter SMTP username", "")
- config.EmailSMTPPass = readString(reader, "Enter SMTP password", "") // Should this be readPassword?
- config.EmailNoReply = readString(reader, "Enter no-reply email address", "")
- }
-
- // Validate required fields
- if config.BaseDomain == "" {
- fmt.Println("Error: Domain name is required")
- os.Exit(1)
- }
- if config.LetsEncryptEmail == "" {
- fmt.Println("Error: Let's Encrypt email is required")
- os.Exit(1)
- }
+ // Validate required fields
+ if config.BaseDomain == "" {
+ fmt.Println("Error: Domain name is required")
+ os.Exit(1)
+ }
+ if config.LetsEncryptEmail == "" {
+ fmt.Println("Error: Let's Encrypt email is required")
+ os.Exit(1)
}
// Advanced configuration
@@ -395,6 +378,7 @@ func collectUserInput(reader *bufio.Reader) Config {
fmt.Println("\n=== Advanced Configuration ===")
config.EnableIPv6 = readBool(reader, "Is your server IPv6 capable?", true)
+ config.EnableGeoblocking = readBool(reader, "Do you want to download the MaxMind GeoLite2 database for geoblocking functionality?", false)
if config.DashboardDomain == "" {
fmt.Println("Error: Dashboard Domain name is required")
@@ -429,11 +413,6 @@ func createConfigFiles(config Config) error {
return nil
}
- // the hybrid does not need the dynamic config
- if config.HybridMode && strings.Contains(path, "dynamic_config.yml") {
- return nil
- }
-
// skip .DS_Store
if strings.Contains(path, ".DS_Store") {
return nil
@@ -663,18 +642,30 @@ func checkPortsAvailable(port int) error {
return nil
}
-func checkIsPangolinInstalledWithHybrid() bool {
- // Check if config/config.yml exists and contains hybrid section
- if _, err := os.Stat("config/config.yml"); err != nil {
- return false
+func downloadMaxMindDatabase() error {
+ fmt.Println("Downloading MaxMind GeoLite2 Country database...")
+
+ // Download the GeoLite2 Country database
+ if err := run("curl", "-L", "-o", "GeoLite2-Country.tar.gz",
+ "https://github.com/GitSquared/node-geolite2-redist/raw/refs/heads/master/redist/GeoLite2-Country.tar.gz"); err != nil {
+ return fmt.Errorf("failed to download GeoLite2 database: %v", err)
}
-
- // Read config file to check for hybrid section
- content, err := os.ReadFile("config/config.yml")
- if err != nil {
- return false
+
+ // Extract the database
+ if err := run("tar", "-xzf", "GeoLite2-Country.tar.gz"); err != nil {
+ return fmt.Errorf("failed to extract GeoLite2 database: %v", err)
}
-
- // Check for hybrid section
- return bytes.Contains(content, []byte("managed:"))
+
+ // Find the .mmdb file and move it to the config directory
+ if err := run("bash", "-c", "mv GeoLite2-Country_*/GeoLite2-Country.mmdb config/"); err != nil {
+ return fmt.Errorf("failed to move GeoLite2 database to config directory: %v", err)
+ }
+
+ // Clean up the downloaded files
+ if err := run("rm", "-rf", "GeoLite2-Country.tar.gz", "GeoLite2-Country_*"); err != nil {
+ fmt.Printf("Warning: failed to clean up temporary files: %v\n", err)
+ }
+
+ fmt.Println("MaxMind GeoLite2 Country database downloaded successfully!")
+ return nil
}
diff --git a/install/quickStart.go b/install/quickStart.go
deleted file mode 100644
index ece8e8ff..00000000
--- a/install/quickStart.go
+++ /dev/null
@@ -1,110 +0,0 @@
-package main
-
-import (
- "bytes"
- "encoding/base64"
- "encoding/json"
- "fmt"
- "io"
- "net/http"
- "time"
-)
-
-const (
- FRONTEND_SECRET_KEY = "af4e4785-7e09-11f0-b93a-74563c4e2a7e"
- // CLOUD_API_URL = "https://pangolin.fossorial.io/api/v1/remote-exit-node/quick-start"
- CLOUD_API_URL = "https://pangolin.fossorial.io/api/v1/remote-exit-node/quick-start"
-)
-
-// HybridCredentials represents the response from the cloud API
-type HybridCredentials struct {
- RemoteExitNodeId string `json:"remoteExitNodeId"`
- Secret string `json:"secret"`
-}
-
-// APIResponse represents the full response structure from the cloud API
-type APIResponse struct {
- Data HybridCredentials `json:"data"`
-}
-
-// RequestPayload represents the request body structure
-type RequestPayload struct {
- Token string `json:"token"`
-}
-
-func generateValidationToken() string {
- timestamp := time.Now().UnixMilli()
- data := fmt.Sprintf("%s|%d", FRONTEND_SECRET_KEY, timestamp)
- obfuscated := make([]byte, len(data))
- for i, char := range []byte(data) {
- obfuscated[i] = char + 5
- }
- return base64.StdEncoding.EncodeToString(obfuscated)
-}
-
-// requestHybridCredentials makes an HTTP POST request to the cloud API
-// to get hybrid credentials (ID and secret)
-func requestHybridCredentials() (*HybridCredentials, error) {
- // Generate validation token
- token := generateValidationToken()
-
- // Create request payload
- payload := RequestPayload{
- Token: token,
- }
-
- // Marshal payload to JSON
- jsonData, err := json.Marshal(payload)
- if err != nil {
- return nil, fmt.Errorf("failed to marshal request payload: %v", err)
- }
-
- // Create HTTP request
- req, err := http.NewRequest("POST", CLOUD_API_URL, bytes.NewBuffer(jsonData))
- if err != nil {
- return nil, fmt.Errorf("failed to create HTTP request: %v", err)
- }
-
- // Set headers
- req.Header.Set("Content-Type", "application/json")
- req.Header.Set("X-CSRF-Token", "x-csrf-protection")
-
- // Create HTTP client with timeout
- client := &http.Client{
- Timeout: 30 * time.Second,
- }
-
- // Make the request
- resp, err := client.Do(req)
- if err != nil {
- return nil, fmt.Errorf("failed to make HTTP request: %v", err)
- }
- defer resp.Body.Close()
-
- // Check response status
- if resp.StatusCode != http.StatusOK {
- return nil, fmt.Errorf("API request failed with status code: %d", resp.StatusCode)
- }
-
- // Read response body for debugging
- body, err := io.ReadAll(resp.Body)
- if err != nil {
- return nil, fmt.Errorf("failed to read response body: %v", err)
- }
-
- // Print the raw JSON response for debugging
- // fmt.Printf("Raw JSON response: %s\n", string(body))
-
- // Parse response
- var apiResponse APIResponse
- if err := json.Unmarshal(body, &apiResponse); err != nil {
- return nil, fmt.Errorf("failed to decode API response: %v", err)
- }
-
- // Validate response data
- if apiResponse.Data.RemoteExitNodeId == "" || apiResponse.Data.Secret == "" {
- return nil, fmt.Errorf("invalid response: missing remoteExitNodeId or secret")
- }
-
- return &apiResponse.Data, nil
-}
diff --git a/messages/bg-BG.json b/messages/bg-BG.json
index 9cc9e9f0..74bf5d87 100644
--- a/messages/bg-BG.json
+++ b/messages/bg-BG.json
@@ -715,7 +715,6 @@
"pangolinServerAdmin": "Администратор на сървър - Панголин",
"licenseTierProfessional": "Професионален лиценз",
"licenseTierEnterprise": "Предприятие лиценз",
- "licenseTierCommercial": "Търговски лиценз",
"licensed": "Лицензиран",
"yes": "Да",
"no": "Не",
@@ -1084,7 +1083,6 @@
"navbar": "Навигационно меню",
"navbarDescription": "Главно навигационно меню за приложението",
"navbarDocsLink": "Документация",
- "commercialEdition": "Търговско издание",
"otpErrorEnable": "Не може да се активира 2FA",
"otpErrorEnableDescription": "Възникна грешка при активиране на 2FA",
"otpSetupCheckCode": "Моля, въведете 6-цифрен код",
diff --git a/messages/cs-CZ.json b/messages/cs-CZ.json
index f1106fb8..9d573022 100644
--- a/messages/cs-CZ.json
+++ b/messages/cs-CZ.json
@@ -715,7 +715,6 @@
"pangolinServerAdmin": "Správce serveru - Pangolin",
"licenseTierProfessional": "Profesionální licence",
"licenseTierEnterprise": "Podniková licence",
- "licenseTierCommercial": "Obchodní licence",
"licensed": "Licencováno",
"yes": "Ano",
"no": "Ne",
@@ -1084,7 +1083,6 @@
"navbar": "Navigation Menu",
"navbarDescription": "Hlavní navigační menu aplikace",
"navbarDocsLink": "Dokumentace",
- "commercialEdition": "Obchodní vydání",
"otpErrorEnable": "2FA nelze povolit",
"otpErrorEnableDescription": "Došlo k chybě při povolování 2FA",
"otpSetupCheckCode": "Zadejte 6místný kód",
diff --git a/messages/de-DE.json b/messages/de-DE.json
index 5ac9fc02..eaca92bf 100644
--- a/messages/de-DE.json
+++ b/messages/de-DE.json
@@ -715,7 +715,6 @@
"pangolinServerAdmin": "Server-Admin - Pangolin",
"licenseTierProfessional": "Professional Lizenz",
"licenseTierEnterprise": "Enterprise Lizenz",
- "licenseTierCommercial": "Gewerbliche Lizenz",
"licensed": "Lizenziert",
"yes": "Ja",
"no": "Nein",
@@ -1084,7 +1083,6 @@
"navbar": "Navigationsmenü",
"navbarDescription": "Hauptnavigationsmenü für die Anwendung",
"navbarDocsLink": "Dokumentation",
- "commercialEdition": "Kommerzielle Edition",
"otpErrorEnable": "2FA konnte nicht aktiviert werden",
"otpErrorEnableDescription": "Beim Aktivieren der 2FA ist ein Fehler aufgetreten",
"otpSetupCheckCode": "Bitte geben Sie einen 6-stelligen Code ein",
diff --git a/messages/en-US.json b/messages/en-US.json
index c27f067e..09b8734d 100644
--- a/messages/en-US.json
+++ b/messages/en-US.json
@@ -96,7 +96,7 @@
"siteWgDescription": "Use any WireGuard client to establish a tunnel. Manual NAT setup required.",
"siteWgDescriptionSaas": "Use any WireGuard client to establish a tunnel. Manual NAT setup required.",
"siteLocalDescription": "Local resources only. No tunneling.",
- "siteLocalDescriptionSaas": "Local resources only. No tunneling.",
+ "siteLocalDescriptionSaas": "Local resources only. No tunneling. Only available on remote nodes.",
"siteSeeAll": "See All Sites",
"siteTunnelDescription": "Determine how you want to connect to your site",
"siteNewtCredentials": "Newt Credentials",
@@ -468,7 +468,10 @@
"createdAt": "Created At",
"proxyErrorInvalidHeader": "Invalid custom Host Header value. Use domain name format, or save empty to unset custom Host Header.",
"proxyErrorTls": "Invalid TLS Server Name. Use domain name format, or save empty to remove the TLS Server Name.",
- "proxyEnableSSL": "Enable SSL (https)",
+ "proxyEnableSSL": "Enable SSL",
+ "proxyEnableSSLDescription": "Enable SSL/TLS encryption for secure HTTPS connections to your targets.",
+ "target": "Target",
+ "configureTarget": "Configure Targets",
"targetErrorFetch": "Failed to fetch targets",
"targetErrorFetchDescription": "An error occurred while fetching targets",
"siteErrorFetch": "Failed to fetch resource",
@@ -495,7 +498,7 @@
"targetTlsSettings": "Secure Connection Configuration",
"targetTlsSettingsDescription": "Configure SSL/TLS settings for your resource",
"targetTlsSettingsAdvanced": "Advanced TLS Settings",
- "targetTlsSni": "TLS Server Name (SNI)",
+ "targetTlsSni": "TLS Server Name",
"targetTlsSniDescription": "The TLS Server Name to use for SNI. Leave empty to use the default.",
"targetTlsSubmit": "Save Settings",
"targets": "Targets Configuration",
@@ -504,9 +507,21 @@
"targetStickySessionsDescription": "Keep connections on the same backend target for their entire session.",
"methodSelect": "Select method",
"targetSubmit": "Add Target",
- "targetNoOne": "No targets. Add a target using the form.",
+ "targetNoOne": "This resource doesn't have any targets. Add a target to configure where to send requests to your backend.",
"targetNoOneDescription": "Adding more than one target above will enable load balancing.",
"targetsSubmit": "Save Targets",
+ "addTarget": "Add Target",
+ "targetErrorInvalidIp": "Invalid IP address",
+ "targetErrorInvalidIpDescription": "Please enter a valid IP address or hostname",
+ "targetErrorInvalidPort": "Invalid port",
+ "targetErrorInvalidPortDescription": "Please enter a valid port number",
+ "targetErrorNoSite": "No site selected",
+ "targetErrorNoSiteDescription": "Please select a site for the target",
+ "targetCreated": "Target created",
+ "targetCreatedDescription": "Target has been created successfully",
+ "targetErrorCreate": "Failed to create target",
+ "targetErrorCreateDescription": "An error occurred while creating the target",
+ "save": "Save",
"proxyAdditional": "Additional Proxy Settings",
"proxyAdditionalDescription": "Configure how your resource handles proxy settings",
"proxyCustomHeader": "Custom Host Header",
@@ -715,7 +730,7 @@
"pangolinServerAdmin": "Server Admin - Pangolin",
"licenseTierProfessional": "Professional License",
"licenseTierEnterprise": "Enterprise License",
- "licenseTierCommercial": "Commercial License",
+ "licenseTierPersonal": "Personal License",
"licensed": "Licensed",
"yes": "Yes",
"no": "No",
@@ -750,7 +765,7 @@
"idpDisplayName": "A display name for this identity provider",
"idpAutoProvisionUsers": "Auto Provision Users",
"idpAutoProvisionUsersDescription": "When enabled, users will be automatically created in the system upon first login with the ability to map users to roles and organizations.",
- "licenseBadge": "Professional",
+ "licenseBadge": "EE",
"idpType": "Provider Type",
"idpTypeDescription": "Select the type of identity provider you want to configure",
"idpOidcConfigure": "OAuth2/OIDC Configuration",
@@ -1084,7 +1099,6 @@
"navbar": "Navigation Menu",
"navbarDescription": "Main navigation menu for the application",
"navbarDocsLink": "Documentation",
- "commercialEdition": "Commercial Edition",
"otpErrorEnable": "Unable to enable 2FA",
"otpErrorEnableDescription": "An error occurred while enabling 2FA",
"otpSetupCheckCode": "Please enter a 6-digit code",
@@ -1140,7 +1154,7 @@
"sidebarAllUsers": "All Users",
"sidebarIdentityProviders": "Identity Providers",
"sidebarLicense": "License",
- "sidebarClients": "Clients (Beta)",
+ "sidebarClients": "Clients",
"sidebarDomains": "Domains",
"enableDockerSocket": "Enable Docker Blueprint",
"enableDockerSocketDescription": "Enable Docker Socket label scraping for blueprint labels. Socket path must be provided to Newt.",
@@ -1333,7 +1347,6 @@
"twoFactorRequired": "Two-factor authentication is required to register a security key.",
"twoFactor": "Two-Factor Authentication",
"adminEnabled2FaOnYourAccount": "Your administrator has enabled two-factor authentication for {email}. Please complete the setup process to continue.",
- "continueToApplication": "Continue to Application",
"securityKeyAdd": "Add Security Key",
"securityKeyRegisterTitle": "Register New Security Key",
"securityKeyRegisterDescription": "Connect your security key and enter a name to identify it",
@@ -1411,6 +1424,7 @@
"externalProxyEnabled": "External Proxy Enabled",
"addNewTarget": "Add New Target",
"targetsList": "Targets List",
+ "advancedMode": "Advanced Mode",
"targetErrorDuplicateTargetFound": "Duplicate target found",
"healthCheckHealthy": "Healthy",
"healthCheckUnhealthy": "Unhealthy",
@@ -1543,8 +1557,8 @@
"autoLoginError": "Auto Login Error",
"autoLoginErrorNoRedirectUrl": "No redirect URL received from the identity provider.",
"autoLoginErrorGeneratingUrl": "Failed to generate authentication URL.",
- "remoteExitNodeManageRemoteExitNodes": "Manage Self-Hosted",
- "remoteExitNodeDescription": "Manage nodes to extend your network connectivity",
+ "remoteExitNodeManageRemoteExitNodes": "Remote Nodes",
+ "remoteExitNodeDescription": "Self-host one or more remote nodes to extend your network connectivity and reduce reliance on the cloud",
"remoteExitNodes": "Nodes",
"searchRemoteExitNodes": "Search nodes...",
"remoteExitNodeAdd": "Add Node",
@@ -1554,7 +1568,7 @@
"remoteExitNodeMessageConfirm": "To confirm, please type the name of the node below.",
"remoteExitNodeConfirmDelete": "Confirm Delete Node",
"remoteExitNodeDelete": "Delete Node",
- "sidebarRemoteExitNodes": "Nodes",
+ "sidebarRemoteExitNodes": "Remote Nodes",
"remoteExitNodeCreate": {
"title": "Create Node",
"description": "Create a new node to extend your network connectivity",
@@ -1724,20 +1738,160 @@
"healthCheckNotAvailable": "Local",
"rewritePath": "Rewrite Path",
"rewritePathDescription": "Optionally rewrite the path before forwarding to the target.",
+ "continueToApplication": "Continue to application",
+ "checkingInvite": "Checking Invite",
"setResourceHeaderAuth": "setResourceHeaderAuth",
"resourceHeaderAuthRemove": "Remove Header Auth",
"resourceHeaderAuthRemoveDescription": "Header authentication removed successfully.",
"resourceErrorHeaderAuthRemove": "Failed to remove Header Authentication",
"resourceErrorHeaderAuthRemoveDescription": "Could not remove header authentication for the resource.",
- "resourceHeaderAuthProtection": "Header Authentication Protection: {{status}}",
- "headerAuthRemove": "Remove",
- "headerAuthAdd": "Add",
+ "resourceHeaderAuthProtectionEnabled": "Header Authentication Enabled",
+ "resourceHeaderAuthProtectionDisabled": "Header Authentication Disabled",
+ "headerAuthRemove": "Remove Header Auth",
+ "headerAuthAdd": "Add Header Auth",
"resourceErrorHeaderAuthSetup": "Failed to set Header Authentication",
"resourceErrorHeaderAuthSetupDescription": "Could not set header authentication for the resource.",
"resourceHeaderAuthSetup": "Header Authentication set successfully",
"resourceHeaderAuthSetupDescription": "Header authentication has been successfully set.",
"resourceHeaderAuthSetupTitle": "Set Header Authentication",
- "resourceHeaderAuthSetupTitleDescription": "Set the basic auth credentials (username and password) to protect this resource with HTTP Header Authentication. Leave both fields blank to remove existing header authentication.",
+ "resourceHeaderAuthSetupTitleDescription": "Set the basic auth credentials (username and password) to protect this resource with HTTP Header Authentication. Access it using the format https://username:password@resource.example.com",
"resourceHeaderAuthSubmit": "Set Header Authentication",
- "actionSetResourceHeaderAuth": "Set Header Authentication"
-}
\ No newline at end of file
+ "actionSetResourceHeaderAuth": "Set Header Authentication",
+ "enterpriseEdition": "Enterprise Edition",
+ "unlicensed": "Unlicensed",
+ "beta": "Beta",
+ "manageClients": "Manage Clients",
+ "manageClientsDescription": "Clients are devices that can connect to your sites",
+ "licenseTableValidUntil": "Valid Until",
+ "saasLicenseKeysSettingsTitle": "Enterprise Licenses",
+ "saasLicenseKeysSettingsDescription": "Generate and manage Enterprise license keys for self-hosted Pangolin instances",
+ "sidebarEnterpriseLicenses": "Licenses",
+ "generateLicenseKey": "Generate License Key",
+ "generateLicenseKeyForm": {
+ "validation": {
+ "emailRequired": "Please enter a valid email address",
+ "useCaseTypeRequired": "Please select a use case type",
+ "firstNameRequired": "First name is required",
+ "lastNameRequired": "Last name is required",
+ "primaryUseRequired": "Please describe your primary use",
+ "jobTitleRequiredBusiness": "Job title is required for business use",
+ "industryRequiredBusiness": "Industry is required for business use",
+ "stateProvinceRegionRequired": "State/Province/Region is required",
+ "postalZipCodeRequired": "Postal/ZIP Code is required",
+ "companyNameRequiredBusiness": "Company name is required for business use",
+ "countryOfResidenceRequiredBusiness": "Country of residence is required for business use",
+ "countryRequiredPersonal": "Country is required for personal use",
+ "agreeToTermsRequired": "You must agree to the terms",
+ "complianceConfirmationRequired": "You must confirm compliance with the Fossorial Commercial License"
+ },
+ "useCaseOptions": {
+ "personal": {
+ "title": "Personal Use",
+ "description": "For individual, non-commercial use such as learning, personal projects, or experimentation."
+ },
+ "business": {
+ "title": "Business Use",
+ "description": "For use within organizations, companies, or commercial or revenue-generating activities."
+ }
+ },
+ "steps": {
+ "emailLicenseType": {
+ "title": "Email & License Type",
+ "description": "Enter your email and choose your license type"
+ },
+ "personalInformation": {
+ "title": "Personal Information",
+ "description": "Tell us about yourself"
+ },
+ "contactInformation": {
+ "title": "Contact Information",
+ "description": "Your contact details"
+ },
+ "termsGenerate": {
+ "title": "Terms & Generate",
+ "description": "Review and accept terms to generate your license"
+ }
+ },
+ "alerts": {
+ "commercialUseDisclosure": {
+ "title": "Usage Disclosure",
+ "description": "Select the license tier that accurately reflects your intended use. The Personal License permits free use of the Software for individual, non-commercial or small-scale commercial activities with annual gross revenue under $100,000 USD. Any use beyond these limits — including use within a business, organization, or other revenue-generating environment — requires a valid Enterprise License and payment of the applicable licensing fee. All users, whether Personal or Enterprise, must comply with the Fossorial Commercial License Terms."
+ },
+ "trialPeriodInformation": {
+ "title": "Trial Period Information",
+ "description": "This License Key enables Enterprise features for a 7-day evaluation period. Continued access to Paid Features beyond the evaluation period requires activation under a valid Personal or Enterprise License. For Enterprise licensing, contact sales@fossorial.io."
+ }
+ },
+ "form": {
+ "useCaseQuestion": "Are you using Pangolin for personal or business use?",
+ "firstName": "First Name",
+ "lastName": "Last Name",
+ "jobTitle": "Job Title",
+ "primaryUseQuestion": "What do you primarily plan to use Pangolin for?",
+ "industryQuestion": "What is your industry?",
+ "prospectiveUsersQuestion": "How many prospective users do you expect to have?",
+ "prospectiveSitesQuestion": "How many prospective sites (tunnels) do you expect to have?",
+ "companyName": "Company name",
+ "countryOfResidence": "Country of residence",
+ "stateProvinceRegion": "State / Province / Region",
+ "postalZipCode": "Postal / ZIP Code",
+ "companyWebsite": "Company website",
+ "companyPhoneNumber": "Company phone number",
+ "country": "Country",
+ "phoneNumberOptional": "Phone number (optional)",
+ "complianceConfirmation": "I confirm that I am in compliance with the Fossorial Commercial License and that reporting inaccurate information or misidentifying use of the product is a violation of the license."
+ },
+ "buttons": {
+ "close": "Close",
+ "previous": "Previous",
+ "next": "Next",
+ "generateLicenseKey": "Generate License Key"
+ },
+ "toasts": {
+ "success": {
+ "title": "License key generated successfully",
+ "description": "Your license key has been generated and is ready to use."
+ },
+ "error": {
+ "title": "Failed to generate license key",
+ "description": "An error occurred while generating the license key."
+ }
+ }
+ },
+ "priority": "Priority",
+ "priorityDescription": "Higher priority routes are evaluated first. Priority = 100 means automatic ordering (system decides). Use another number to enforce manual priority.",
+ "instanceName": "Instance Name",
+ "pathMatchModalTitle": "Configure Path Matching",
+ "pathMatchModalDescription": "Set up how incoming requests should be matched based on their path.",
+ "pathMatchType": "Match Type",
+ "pathMatchPrefix": "Prefix",
+ "pathMatchExact": "Exact",
+ "pathMatchRegex": "Regex",
+ "pathMatchValue": "Path Value",
+ "clear": "Clear",
+ "saveChanges": "Save Changes",
+ "pathMatchRegexPlaceholder": "^/api/.*",
+ "pathMatchDefaultPlaceholder": "/path",
+ "pathMatchPrefixHelp": "Example: /api matches /api, /api/users, etc.",
+ "pathMatchExactHelp": "Example: /api matches only /api",
+ "pathMatchRegexHelp": "Example: ^/api/.* matches /api/anything",
+ "pathRewriteModalTitle": "Configure Path Rewriting",
+ "pathRewriteModalDescription": "Transform the matched path before forwarding to the target.",
+ "pathRewriteType": "Rewrite Type",
+ "pathRewritePrefixOption": "Prefix - Replace prefix",
+ "pathRewriteExactOption": "Exact - Replace entire path",
+ "pathRewriteRegexOption": "Regex - Pattern replacement",
+ "pathRewriteStripPrefixOption": "Strip Prefix - Remove prefix",
+ "pathRewriteValue": "Rewrite Value",
+ "pathRewriteRegexPlaceholder": "/new/$1",
+ "pathRewriteDefaultPlaceholder": "/new-path",
+ "pathRewritePrefixHelp": "Replace the matched prefix with this value",
+ "pathRewriteExactHelp": "Replace the entire path with this value when the path matches exactly",
+ "pathRewriteRegexHelp": "Use capture groups like $1, $2 for replacement",
+ "pathRewriteStripPrefixHelp": "Leave empty to strip prefix or provide new prefix",
+ "pathRewritePrefix": "Prefix",
+ "pathRewriteExact": "Exact",
+ "pathRewriteRegex": "Regex",
+ "pathRewriteStrip": "Strip",
+ "pathRewriteStripLabel": "strip"
+}
diff --git a/messages/es-ES.json b/messages/es-ES.json
index a1b92f8b..2e7cf00a 100644
--- a/messages/es-ES.json
+++ b/messages/es-ES.json
@@ -715,7 +715,6 @@
"pangolinServerAdmin": "Admin Servidor - Pangolin",
"licenseTierProfessional": "Licencia profesional",
"licenseTierEnterprise": "Licencia Enterprise",
- "licenseTierCommercial": "Licencia comercial",
"licensed": "Licenciado",
"yes": "Sí",
"no": "Nu",
@@ -1084,7 +1083,6 @@
"navbar": "Menú de navegación",
"navbarDescription": "Menú de navegación principal para la aplicación",
"navbarDocsLink": "Documentación",
- "commercialEdition": "Edición Comercial",
"otpErrorEnable": "No se puede habilitar 2FA",
"otpErrorEnableDescription": "Se ha producido un error al habilitar 2FA",
"otpSetupCheckCode": "Por favor, introduzca un código de 6 dígitos",
diff --git a/messages/fr-FR.json b/messages/fr-FR.json
index 6028ab5b..4a1670f3 100644
--- a/messages/fr-FR.json
+++ b/messages/fr-FR.json
@@ -715,7 +715,6 @@
"pangolinServerAdmin": "Admin Serveur - Pangolin",
"licenseTierProfessional": "Licence Professionnelle",
"licenseTierEnterprise": "Licence Entreprise",
- "licenseTierCommercial": "Licence commerciale",
"licensed": "Sous licence",
"yes": "Oui",
"no": "Non",
@@ -1084,7 +1083,6 @@
"navbar": "Menu de navigation",
"navbarDescription": "Menu de navigation principal de l'application",
"navbarDocsLink": "Documentation",
- "commercialEdition": "Édition Commerciale",
"otpErrorEnable": "Impossible d'activer l'A2F",
"otpErrorEnableDescription": "Une erreur s'est produite lors de l'activation de l'A2F",
"otpSetupCheckCode": "Veuillez entrer un code à 6 chiffres",
diff --git a/messages/it-IT.json b/messages/it-IT.json
index ca22ba63..143da0c5 100644
--- a/messages/it-IT.json
+++ b/messages/it-IT.json
@@ -715,7 +715,6 @@
"pangolinServerAdmin": "Server Admin - Pangolina",
"licenseTierProfessional": "Licenza Professional",
"licenseTierEnterprise": "Licenza Enterprise",
- "licenseTierCommercial": "Licenza Commerciale",
"licensed": "Con Licenza",
"yes": "Sì",
"no": "No",
@@ -1084,7 +1083,6 @@
"navbar": "Menu di Navigazione",
"navbarDescription": "Menu di navigazione principale dell'applicazione",
"navbarDocsLink": "Documentazione",
- "commercialEdition": "Edizione Commerciale",
"otpErrorEnable": "Impossibile abilitare 2FA",
"otpErrorEnableDescription": "Si è verificato un errore durante l'abilitazione di 2FA",
"otpSetupCheckCode": "Inserisci un codice a 6 cifre",
diff --git a/messages/ko-KR.json b/messages/ko-KR.json
index 3d010cd5..8ace81a5 100644
--- a/messages/ko-KR.json
+++ b/messages/ko-KR.json
@@ -715,7 +715,6 @@
"pangolinServerAdmin": "서버 관리자 - 판골린",
"licenseTierProfessional": "전문 라이센스",
"licenseTierEnterprise": "기업 라이선스",
- "licenseTierCommercial": "상업용 라이선스",
"licensed": "라이센스",
"yes": "예",
"no": "아니요",
@@ -1084,7 +1083,6 @@
"navbar": "탐색 메뉴",
"navbarDescription": "애플리케이션의 주요 탐색 메뉴",
"navbarDocsLink": "문서",
- "commercialEdition": "상업용 에디션",
"otpErrorEnable": "2FA를 활성화할 수 없습니다.",
"otpErrorEnableDescription": "2FA를 활성화하는 동안 오류가 발생했습니다",
"otpSetupCheckCode": "6자리 코드를 입력하세요",
diff --git a/messages/nb-NO.json b/messages/nb-NO.json
index 84dc5266..f7f0d3ab 100644
--- a/messages/nb-NO.json
+++ b/messages/nb-NO.json
@@ -715,7 +715,6 @@
"pangolinServerAdmin": "Server Admin - Pangolin",
"licenseTierProfessional": "Profesjonell lisens",
"licenseTierEnterprise": "Bedriftslisens",
- "licenseTierCommercial": "Kommersiell lisens",
"licensed": "Lisensiert",
"yes": "Ja",
"no": "Nei",
@@ -1084,7 +1083,6 @@
"navbar": "Navigasjonsmeny",
"navbarDescription": "Hovednavigasjonsmeny for applikasjonen",
"navbarDocsLink": "Dokumentasjon",
- "commercialEdition": "Kommersiell utgave",
"otpErrorEnable": "Kunne ikke aktivere 2FA",
"otpErrorEnableDescription": "En feil oppstod under aktivering av 2FA",
"otpSetupCheckCode": "Vennligst skriv inn en 6-sifret kode",
diff --git a/messages/nl-NL.json b/messages/nl-NL.json
index fb82fbb6..34d1c811 100644
--- a/messages/nl-NL.json
+++ b/messages/nl-NL.json
@@ -715,7 +715,6 @@
"pangolinServerAdmin": "Serverbeheer - Pangolin",
"licenseTierProfessional": "Professionele licentie",
"licenseTierEnterprise": "Enterprise Licentie",
- "licenseTierCommercial": "Commerciële licentie",
"licensed": "Gelicentieerd",
"yes": "ja",
"no": "Neen",
@@ -1084,7 +1083,6 @@
"navbar": "Navigatiemenu",
"navbarDescription": "Hoofd navigatie menu voor de applicatie",
"navbarDocsLink": "Documentatie",
- "commercialEdition": "Commerciële editie",
"otpErrorEnable": "Kan 2FA niet inschakelen",
"otpErrorEnableDescription": "Er is een fout opgetreden tijdens het inschakelen van 2FA",
"otpSetupCheckCode": "Voer een 6-cijferige code in",
diff --git a/messages/pl-PL.json b/messages/pl-PL.json
index c3db35f5..08287ed9 100644
--- a/messages/pl-PL.json
+++ b/messages/pl-PL.json
@@ -715,7 +715,6 @@
"pangolinServerAdmin": "Administrator serwera - Pangolin",
"licenseTierProfessional": "Licencja Professional",
"licenseTierEnterprise": "Licencja Enterprise",
- "licenseTierCommercial": "Licencja handlowa",
"licensed": "Licencjonowany",
"yes": "Tak",
"no": "Nie",
@@ -1084,7 +1083,6 @@
"navbar": "Menu nawigacyjne",
"navbarDescription": "Główne menu nawigacyjne aplikacji",
"navbarDocsLink": "Dokumentacja",
- "commercialEdition": "Edycja komercyjna",
"otpErrorEnable": "Nie można włączyć 2FA",
"otpErrorEnableDescription": "Wystąpił błąd podczas włączania 2FA",
"otpSetupCheckCode": "Wprowadź 6-cyfrowy kod",
diff --git a/messages/pt-PT.json b/messages/pt-PT.json
index 1d61c581..5a45eedd 100644
--- a/messages/pt-PT.json
+++ b/messages/pt-PT.json
@@ -715,7 +715,6 @@
"pangolinServerAdmin": "Administrador do Servidor - Pangolin",
"licenseTierProfessional": "Licença Profissional",
"licenseTierEnterprise": "Licença Empresarial",
- "licenseTierCommercial": "Licença comercial",
"licensed": "Licenciado",
"yes": "Sim",
"no": "Não",
@@ -1084,7 +1083,6 @@
"navbar": "Menu de Navegação",
"navbarDescription": "Menu de navegação principal da aplicação",
"navbarDocsLink": "Documentação",
- "commercialEdition": "Edição Comercial",
"otpErrorEnable": "Não foi possível ativar 2FA",
"otpErrorEnableDescription": "Ocorreu um erro ao ativar 2FA",
"otpSetupCheckCode": "Por favor, insira um código de 6 dígitos",
diff --git a/messages/ru-RU.json b/messages/ru-RU.json
index 80be36dd..c41215d2 100644
--- a/messages/ru-RU.json
+++ b/messages/ru-RU.json
@@ -715,7 +715,6 @@
"pangolinServerAdmin": "Администратор сервера - Pangolin",
"licenseTierProfessional": "Профессиональная лицензия",
"licenseTierEnterprise": "Корпоративная лицензия",
- "licenseTierCommercial": "Коммерческая лицензия",
"licensed": "Лицензировано",
"yes": "Да",
"no": "Нет",
@@ -1084,7 +1083,6 @@
"navbar": "Навигационное меню",
"navbarDescription": "Главное навигационное меню приложения",
"navbarDocsLink": "Документация",
- "commercialEdition": "Коммерческая версия",
"otpErrorEnable": "Невозможно включить 2FA",
"otpErrorEnableDescription": "Произошла ошибка при включении 2FA",
"otpSetupCheckCode": "Пожалуйста, введите 6-значный код",
diff --git a/messages/tr-TR.json b/messages/tr-TR.json
index b84bef19..6296b7fe 100644
--- a/messages/tr-TR.json
+++ b/messages/tr-TR.json
@@ -715,7 +715,6 @@
"pangolinServerAdmin": "Sunucu Yöneticisi - Pangolin",
"licenseTierProfessional": "Profesyonel Lisans",
"licenseTierEnterprise": "Kurumsal Lisans",
- "licenseTierCommercial": "Ticari Lisans",
"licensed": "Lisanslı",
"yes": "Evet",
"no": "Hayır",
@@ -1084,7 +1083,6 @@
"navbar": "Navigasyon Menüsü",
"navbarDescription": "Uygulamanın ana navigasyon menüsü",
"navbarDocsLink": "Dokümantasyon",
- "commercialEdition": "Ticari Sürüm",
"otpErrorEnable": "2FA etkinleştirilemedi",
"otpErrorEnableDescription": "2FA etkinleştirilirken bir hata oluştu",
"otpSetupCheckCode": "6 haneli bir kod girin",
diff --git a/messages/zh-CN.json b/messages/zh-CN.json
index 4f1d9c14..a8f578db 100644
--- a/messages/zh-CN.json
+++ b/messages/zh-CN.json
@@ -715,7 +715,6 @@
"pangolinServerAdmin": "服务器管理员 - Pangolin",
"licenseTierProfessional": "专业许可证",
"licenseTierEnterprise": "企业许可证",
- "licenseTierCommercial": "商业许可证",
"licensed": "已授权",
"yes": "是",
"no": "否",
@@ -1084,7 +1083,6 @@
"navbar": "导航菜单",
"navbarDescription": "应用程序的主导航菜单",
"navbarDocsLink": "文件",
- "commercialEdition": "商业版",
"otpErrorEnable": "无法启用 2FA",
"otpErrorEnableDescription": "启用 2FA 时出错",
"otpSetupCheckCode": "请输入您的6位数字代码",
diff --git a/package-lock.json b/package-lock.json
index 2661a0f3..50bc095a 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -447,51 +447,51 @@
}
},
"node_modules/@aws-sdk/client-sesv2": {
- "version": "3.899.0",
- "resolved": "https://registry.npmjs.org/@aws-sdk/client-sesv2/-/client-sesv2-3.899.0.tgz",
- "integrity": "sha512-aMs3QgB9lWaKKrnx9KhIopoeXLNzI/sqdp5M56j30jlBD4vqdcCzW2OwFAAs26QzUgNKOOSY+iLZcE9DUDdIvg==",
+ "version": "3.901.0",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/client-sesv2/-/client-sesv2-3.901.0.tgz",
+ "integrity": "sha512-xCS2qZlvgbXKZbJW8XgU8OEAL7BJyVqJ5yODOQxa1TJFZ/+wEhik9XZtULjNnQqa29sJDpPltuSDG1aDG2OUxQ==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"@aws-crypto/sha256-browser": "5.2.0",
"@aws-crypto/sha256-js": "5.2.0",
- "@aws-sdk/core": "3.899.0",
- "@aws-sdk/credential-provider-node": "3.899.0",
- "@aws-sdk/middleware-host-header": "3.893.0",
- "@aws-sdk/middleware-logger": "3.893.0",
- "@aws-sdk/middleware-recursion-detection": "3.893.0",
- "@aws-sdk/middleware-user-agent": "3.899.0",
- "@aws-sdk/region-config-resolver": "3.893.0",
- "@aws-sdk/signature-v4-multi-region": "3.899.0",
- "@aws-sdk/types": "3.893.0",
- "@aws-sdk/util-endpoints": "3.895.0",
- "@aws-sdk/util-user-agent-browser": "3.893.0",
- "@aws-sdk/util-user-agent-node": "3.899.0",
- "@smithy/config-resolver": "^4.2.2",
- "@smithy/core": "^3.13.0",
- "@smithy/fetch-http-handler": "^5.2.1",
- "@smithy/hash-node": "^4.1.1",
- "@smithy/invalid-dependency": "^4.1.1",
- "@smithy/middleware-content-length": "^4.1.1",
- "@smithy/middleware-endpoint": "^4.2.5",
- "@smithy/middleware-retry": "^4.3.1",
- "@smithy/middleware-serde": "^4.1.1",
- "@smithy/middleware-stack": "^4.1.1",
- "@smithy/node-config-provider": "^4.2.2",
- "@smithy/node-http-handler": "^4.2.1",
- "@smithy/protocol-http": "^5.2.1",
- "@smithy/smithy-client": "^4.6.5",
- "@smithy/types": "^4.5.0",
- "@smithy/url-parser": "^4.1.1",
- "@smithy/util-base64": "^4.1.0",
- "@smithy/util-body-length-browser": "^4.1.0",
- "@smithy/util-body-length-node": "^4.1.0",
- "@smithy/util-defaults-mode-browser": "^4.1.5",
- "@smithy/util-defaults-mode-node": "^4.1.5",
- "@smithy/util-endpoints": "^3.1.2",
- "@smithy/util-middleware": "^4.1.1",
- "@smithy/util-retry": "^4.1.2",
- "@smithy/util-utf8": "^4.1.0",
+ "@aws-sdk/core": "3.901.0",
+ "@aws-sdk/credential-provider-node": "3.901.0",
+ "@aws-sdk/middleware-host-header": "3.901.0",
+ "@aws-sdk/middleware-logger": "3.901.0",
+ "@aws-sdk/middleware-recursion-detection": "3.901.0",
+ "@aws-sdk/middleware-user-agent": "3.901.0",
+ "@aws-sdk/region-config-resolver": "3.901.0",
+ "@aws-sdk/signature-v4-multi-region": "3.901.0",
+ "@aws-sdk/types": "3.901.0",
+ "@aws-sdk/util-endpoints": "3.901.0",
+ "@aws-sdk/util-user-agent-browser": "3.901.0",
+ "@aws-sdk/util-user-agent-node": "3.901.0",
+ "@smithy/config-resolver": "^4.3.0",
+ "@smithy/core": "^3.14.0",
+ "@smithy/fetch-http-handler": "^5.3.0",
+ "@smithy/hash-node": "^4.2.0",
+ "@smithy/invalid-dependency": "^4.2.0",
+ "@smithy/middleware-content-length": "^4.2.0",
+ "@smithy/middleware-endpoint": "^4.3.0",
+ "@smithy/middleware-retry": "^4.4.0",
+ "@smithy/middleware-serde": "^4.2.0",
+ "@smithy/middleware-stack": "^4.2.0",
+ "@smithy/node-config-provider": "^4.3.0",
+ "@smithy/node-http-handler": "^4.3.0",
+ "@smithy/protocol-http": "^5.3.0",
+ "@smithy/smithy-client": "^4.7.0",
+ "@smithy/types": "^4.6.0",
+ "@smithy/url-parser": "^4.2.0",
+ "@smithy/util-base64": "^4.2.0",
+ "@smithy/util-body-length-browser": "^4.2.0",
+ "@smithy/util-body-length-node": "^4.2.0",
+ "@smithy/util-defaults-mode-browser": "^4.2.0",
+ "@smithy/util-defaults-mode-node": "^4.2.0",
+ "@smithy/util-endpoints": "^3.2.0",
+ "@smithy/util-middleware": "^4.2.0",
+ "@smithy/util-retry": "^4.2.0",
+ "@smithy/util-utf8": "^4.2.0",
"tslib": "^2.6.2"
},
"engines": {
@@ -499,49 +499,49 @@
}
},
"node_modules/@aws-sdk/client-sesv2/node_modules/@aws-sdk/client-sso": {
- "version": "3.899.0",
- "resolved": "https://registry.npmjs.org/@aws-sdk/client-sso/-/client-sso-3.899.0.tgz",
- "integrity": "sha512-EKz/iiVDv2OC8/3ONcXG3+rhphx9Heh7KXQdsZzsAXGVn6mWtrHQLrWjgONckmK4LrD07y4+5WlJlGkMxSMA5A==",
+ "version": "3.901.0",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/client-sso/-/client-sso-3.901.0.tgz",
+ "integrity": "sha512-sGyDjjkJ7ppaE+bAKL/Q5IvVCxtoyBIzN+7+hWTS/mUxWJ9EOq9238IqmVIIK6sYNIzEf9yhobfMARasPYVTNg==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"@aws-crypto/sha256-browser": "5.2.0",
"@aws-crypto/sha256-js": "5.2.0",
- "@aws-sdk/core": "3.899.0",
- "@aws-sdk/middleware-host-header": "3.893.0",
- "@aws-sdk/middleware-logger": "3.893.0",
- "@aws-sdk/middleware-recursion-detection": "3.893.0",
- "@aws-sdk/middleware-user-agent": "3.899.0",
- "@aws-sdk/region-config-resolver": "3.893.0",
- "@aws-sdk/types": "3.893.0",
- "@aws-sdk/util-endpoints": "3.895.0",
- "@aws-sdk/util-user-agent-browser": "3.893.0",
- "@aws-sdk/util-user-agent-node": "3.899.0",
- "@smithy/config-resolver": "^4.2.2",
- "@smithy/core": "^3.13.0",
- "@smithy/fetch-http-handler": "^5.2.1",
- "@smithy/hash-node": "^4.1.1",
- "@smithy/invalid-dependency": "^4.1.1",
- "@smithy/middleware-content-length": "^4.1.1",
- "@smithy/middleware-endpoint": "^4.2.5",
- "@smithy/middleware-retry": "^4.3.1",
- "@smithy/middleware-serde": "^4.1.1",
- "@smithy/middleware-stack": "^4.1.1",
- "@smithy/node-config-provider": "^4.2.2",
- "@smithy/node-http-handler": "^4.2.1",
- "@smithy/protocol-http": "^5.2.1",
- "@smithy/smithy-client": "^4.6.5",
- "@smithy/types": "^4.5.0",
- "@smithy/url-parser": "^4.1.1",
- "@smithy/util-base64": "^4.1.0",
- "@smithy/util-body-length-browser": "^4.1.0",
- "@smithy/util-body-length-node": "^4.1.0",
- "@smithy/util-defaults-mode-browser": "^4.1.5",
- "@smithy/util-defaults-mode-node": "^4.1.5",
- "@smithy/util-endpoints": "^3.1.2",
- "@smithy/util-middleware": "^4.1.1",
- "@smithy/util-retry": "^4.1.2",
- "@smithy/util-utf8": "^4.1.0",
+ "@aws-sdk/core": "3.901.0",
+ "@aws-sdk/middleware-host-header": "3.901.0",
+ "@aws-sdk/middleware-logger": "3.901.0",
+ "@aws-sdk/middleware-recursion-detection": "3.901.0",
+ "@aws-sdk/middleware-user-agent": "3.901.0",
+ "@aws-sdk/region-config-resolver": "3.901.0",
+ "@aws-sdk/types": "3.901.0",
+ "@aws-sdk/util-endpoints": "3.901.0",
+ "@aws-sdk/util-user-agent-browser": "3.901.0",
+ "@aws-sdk/util-user-agent-node": "3.901.0",
+ "@smithy/config-resolver": "^4.3.0",
+ "@smithy/core": "^3.14.0",
+ "@smithy/fetch-http-handler": "^5.3.0",
+ "@smithy/hash-node": "^4.2.0",
+ "@smithy/invalid-dependency": "^4.2.0",
+ "@smithy/middleware-content-length": "^4.2.0",
+ "@smithy/middleware-endpoint": "^4.3.0",
+ "@smithy/middleware-retry": "^4.4.0",
+ "@smithy/middleware-serde": "^4.2.0",
+ "@smithy/middleware-stack": "^4.2.0",
+ "@smithy/node-config-provider": "^4.3.0",
+ "@smithy/node-http-handler": "^4.3.0",
+ "@smithy/protocol-http": "^5.3.0",
+ "@smithy/smithy-client": "^4.7.0",
+ "@smithy/types": "^4.6.0",
+ "@smithy/url-parser": "^4.2.0",
+ "@smithy/util-base64": "^4.2.0",
+ "@smithy/util-body-length-browser": "^4.2.0",
+ "@smithy/util-body-length-node": "^4.2.0",
+ "@smithy/util-defaults-mode-browser": "^4.2.0",
+ "@smithy/util-defaults-mode-node": "^4.2.0",
+ "@smithy/util-endpoints": "^3.2.0",
+ "@smithy/util-middleware": "^4.2.0",
+ "@smithy/util-retry": "^4.2.0",
+ "@smithy/util-utf8": "^4.2.0",
"tslib": "^2.6.2"
},
"engines": {
@@ -549,24 +549,24 @@
}
},
"node_modules/@aws-sdk/client-sesv2/node_modules/@aws-sdk/core": {
- "version": "3.899.0",
- "resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.899.0.tgz",
- "integrity": "sha512-Enp5Zw37xaRlnscyaelaUZNxVqyE3CTS8gjahFbW2bbzVtRD2itHBVgq8A3lvKiFb7Feoxa71aTe0fQ1I6AhQQ==",
+ "version": "3.901.0",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.901.0.tgz",
+ "integrity": "sha512-brKAc3y64tdhyuEf+OPIUln86bRTqkLgb9xkd6kUdIeA5+qmp/N6amItQz+RN4k4O3kqkCPYnAd3LonTKluobw==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
- "@aws-sdk/types": "3.893.0",
- "@aws-sdk/xml-builder": "3.894.0",
- "@smithy/core": "^3.13.0",
- "@smithy/node-config-provider": "^4.2.2",
- "@smithy/property-provider": "^4.1.1",
- "@smithy/protocol-http": "^5.2.1",
- "@smithy/signature-v4": "^5.2.1",
- "@smithy/smithy-client": "^4.6.5",
- "@smithy/types": "^4.5.0",
- "@smithy/util-base64": "^4.1.0",
- "@smithy/util-middleware": "^4.1.1",
- "@smithy/util-utf8": "^4.1.0",
+ "@aws-sdk/types": "3.901.0",
+ "@aws-sdk/xml-builder": "3.901.0",
+ "@smithy/core": "^3.14.0",
+ "@smithy/node-config-provider": "^4.3.0",
+ "@smithy/property-provider": "^4.2.0",
+ "@smithy/protocol-http": "^5.3.0",
+ "@smithy/signature-v4": "^5.3.0",
+ "@smithy/smithy-client": "^4.7.0",
+ "@smithy/types": "^4.6.0",
+ "@smithy/util-base64": "^4.2.0",
+ "@smithy/util-middleware": "^4.2.0",
+ "@smithy/util-utf8": "^4.2.0",
"tslib": "^2.6.2"
},
"engines": {
@@ -574,16 +574,16 @@
}
},
"node_modules/@aws-sdk/client-sesv2/node_modules/@aws-sdk/credential-provider-env": {
- "version": "3.899.0",
- "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-env/-/credential-provider-env-3.899.0.tgz",
- "integrity": "sha512-wXQ//KQ751EFhUbdfoL/e2ZDaM8l2Cff+hVwFcj32yiZyeCMhnoLRMQk2euAaUOugqPY5V5qesFbHhISbIedtw==",
+ "version": "3.901.0",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-env/-/credential-provider-env-3.901.0.tgz",
+ "integrity": "sha512-5hAdVl3tBuARh3zX5MLJ1P/d+Kr5kXtDU3xm1pxUEF4xt2XkEEpwiX5fbkNkz2rbh3BCt2gOHsAbh6b3M7n+DA==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
- "@aws-sdk/core": "3.899.0",
- "@aws-sdk/types": "3.893.0",
- "@smithy/property-provider": "^4.1.1",
- "@smithy/types": "^4.5.0",
+ "@aws-sdk/core": "3.901.0",
+ "@aws-sdk/types": "3.901.0",
+ "@smithy/property-provider": "^4.2.0",
+ "@smithy/types": "^4.6.0",
"tslib": "^2.6.2"
},
"engines": {
@@ -591,21 +591,21 @@
}
},
"node_modules/@aws-sdk/client-sesv2/node_modules/@aws-sdk/credential-provider-http": {
- "version": "3.899.0",
- "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-http/-/credential-provider-http-3.899.0.tgz",
- "integrity": "sha512-/rRHyJFdnPrupjt/1q/PxaO6O26HFsguVUJSUeMeGUWLy0W8OC3slLFDNh89CgTqnplCyt1aLFMCagRM20HjNQ==",
+ "version": "3.901.0",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-http/-/credential-provider-http-3.901.0.tgz",
+ "integrity": "sha512-Ggr7+0M6QZEsrqRkK7iyJLf4LkIAacAxHz9c4dm9hnDdU7vqrlJm6g73IxMJXWN1bIV7IxfpzB11DsRrB/oNjQ==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
- "@aws-sdk/core": "3.899.0",
- "@aws-sdk/types": "3.893.0",
- "@smithy/fetch-http-handler": "^5.2.1",
- "@smithy/node-http-handler": "^4.2.1",
- "@smithy/property-provider": "^4.1.1",
- "@smithy/protocol-http": "^5.2.1",
- "@smithy/smithy-client": "^4.6.5",
- "@smithy/types": "^4.5.0",
- "@smithy/util-stream": "^4.3.2",
+ "@aws-sdk/core": "3.901.0",
+ "@aws-sdk/types": "3.901.0",
+ "@smithy/fetch-http-handler": "^5.3.0",
+ "@smithy/node-http-handler": "^4.3.0",
+ "@smithy/property-provider": "^4.2.0",
+ "@smithy/protocol-http": "^5.3.0",
+ "@smithy/smithy-client": "^4.7.0",
+ "@smithy/types": "^4.6.0",
+ "@smithy/util-stream": "^4.4.0",
"tslib": "^2.6.2"
},
"engines": {
@@ -613,24 +613,24 @@
}
},
"node_modules/@aws-sdk/client-sesv2/node_modules/@aws-sdk/credential-provider-ini": {
- "version": "3.899.0",
- "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.899.0.tgz",
- "integrity": "sha512-B8oFNFTDV0j1yiJiqzkC2ybml+theNnmsLrTLBhJbnBLWkxEcmVGKVIMnATW9BUCBhHmEtDiogdNIzSwP8tbMw==",
+ "version": "3.901.0",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.901.0.tgz",
+ "integrity": "sha512-zxadcDS0hNJgv8n4hFYJNOXyfjaNE1vvqIiF/JzZSQpSSYXzCd+WxXef5bQh+W3giDtRUmkvP5JLbamEFjZKyw==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
- "@aws-sdk/core": "3.899.0",
- "@aws-sdk/credential-provider-env": "3.899.0",
- "@aws-sdk/credential-provider-http": "3.899.0",
- "@aws-sdk/credential-provider-process": "3.899.0",
- "@aws-sdk/credential-provider-sso": "3.899.0",
- "@aws-sdk/credential-provider-web-identity": "3.899.0",
- "@aws-sdk/nested-clients": "3.899.0",
- "@aws-sdk/types": "3.893.0",
- "@smithy/credential-provider-imds": "^4.1.2",
- "@smithy/property-provider": "^4.1.1",
- "@smithy/shared-ini-file-loader": "^4.2.0",
- "@smithy/types": "^4.5.0",
+ "@aws-sdk/core": "3.901.0",
+ "@aws-sdk/credential-provider-env": "3.901.0",
+ "@aws-sdk/credential-provider-http": "3.901.0",
+ "@aws-sdk/credential-provider-process": "3.901.0",
+ "@aws-sdk/credential-provider-sso": "3.901.0",
+ "@aws-sdk/credential-provider-web-identity": "3.901.0",
+ "@aws-sdk/nested-clients": "3.901.0",
+ "@aws-sdk/types": "3.901.0",
+ "@smithy/credential-provider-imds": "^4.2.0",
+ "@smithy/property-provider": "^4.2.0",
+ "@smithy/shared-ini-file-loader": "^4.3.0",
+ "@smithy/types": "^4.6.0",
"tslib": "^2.6.2"
},
"engines": {
@@ -638,23 +638,23 @@
}
},
"node_modules/@aws-sdk/client-sesv2/node_modules/@aws-sdk/credential-provider-node": {
- "version": "3.899.0",
- "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.899.0.tgz",
- "integrity": "sha512-nHBnZ2ZCOqTGJ2A9xpVj8iK6+WV+j0JNv3XGEkIuL4mqtGEPJlEex/0mD/hqc1VF8wZzojji2OQ3892m1mUOSA==",
+ "version": "3.901.0",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.901.0.tgz",
+ "integrity": "sha512-dPuFzMF7L1s/lQyT3wDxqLe82PyTH+5o1jdfseTEln64LJMl0ZMWaKX/C1UFNDxaTd35Cgt1bDbjjAWHMiKSFQ==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
- "@aws-sdk/credential-provider-env": "3.899.0",
- "@aws-sdk/credential-provider-http": "3.899.0",
- "@aws-sdk/credential-provider-ini": "3.899.0",
- "@aws-sdk/credential-provider-process": "3.899.0",
- "@aws-sdk/credential-provider-sso": "3.899.0",
- "@aws-sdk/credential-provider-web-identity": "3.899.0",
- "@aws-sdk/types": "3.893.0",
- "@smithy/credential-provider-imds": "^4.1.2",
- "@smithy/property-provider": "^4.1.1",
- "@smithy/shared-ini-file-loader": "^4.2.0",
- "@smithy/types": "^4.5.0",
+ "@aws-sdk/credential-provider-env": "3.901.0",
+ "@aws-sdk/credential-provider-http": "3.901.0",
+ "@aws-sdk/credential-provider-ini": "3.901.0",
+ "@aws-sdk/credential-provider-process": "3.901.0",
+ "@aws-sdk/credential-provider-sso": "3.901.0",
+ "@aws-sdk/credential-provider-web-identity": "3.901.0",
+ "@aws-sdk/types": "3.901.0",
+ "@smithy/credential-provider-imds": "^4.2.0",
+ "@smithy/property-provider": "^4.2.0",
+ "@smithy/shared-ini-file-loader": "^4.3.0",
+ "@smithy/types": "^4.6.0",
"tslib": "^2.6.2"
},
"engines": {
@@ -662,17 +662,17 @@
}
},
"node_modules/@aws-sdk/client-sesv2/node_modules/@aws-sdk/credential-provider-process": {
- "version": "3.899.0",
- "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-process/-/credential-provider-process-3.899.0.tgz",
- "integrity": "sha512-1PWSejKcJQUKBNPIqSHlEW4w8vSjmb+3kNJqCinJybjp5uP5BJgBp6QNcb8Nv30VBM0bn3ajVd76LCq4ZshQAw==",
+ "version": "3.901.0",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-process/-/credential-provider-process-3.901.0.tgz",
+ "integrity": "sha512-/IWgmgM3Cl1wTdJA5HqKMAojxLkYchh5kDuphApxKhupLu6Pu0JBOHU8A5GGeFvOycyaVwosod6zDduINZxe+A==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
- "@aws-sdk/core": "3.899.0",
- "@aws-sdk/types": "3.893.0",
- "@smithy/property-provider": "^4.1.1",
- "@smithy/shared-ini-file-loader": "^4.2.0",
- "@smithy/types": "^4.5.0",
+ "@aws-sdk/core": "3.901.0",
+ "@aws-sdk/types": "3.901.0",
+ "@smithy/property-provider": "^4.2.0",
+ "@smithy/shared-ini-file-loader": "^4.3.0",
+ "@smithy/types": "^4.6.0",
"tslib": "^2.6.2"
},
"engines": {
@@ -680,19 +680,19 @@
}
},
"node_modules/@aws-sdk/client-sesv2/node_modules/@aws-sdk/credential-provider-sso": {
- "version": "3.899.0",
- "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.899.0.tgz",
- "integrity": "sha512-URlMbo74CAhIGrhzEP2fw5F5Tt6MRUctA8aa88MomlEHCEbJDsMD3nh6qoXxwR3LyvEBFmCWOZ/1TWmAjMsSdA==",
+ "version": "3.901.0",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.901.0.tgz",
+ "integrity": "sha512-SjmqZQHmqFSET7+6xcZgtH7yEyh5q53LN87GqwYlJZ6KJ5oNw11acUNEhUOL1xTSJEvaWqwTIkS2zqrzLcM9bw==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
- "@aws-sdk/client-sso": "3.899.0",
- "@aws-sdk/core": "3.899.0",
- "@aws-sdk/token-providers": "3.899.0",
- "@aws-sdk/types": "3.893.0",
- "@smithy/property-provider": "^4.1.1",
- "@smithy/shared-ini-file-loader": "^4.2.0",
- "@smithy/types": "^4.5.0",
+ "@aws-sdk/client-sso": "3.901.0",
+ "@aws-sdk/core": "3.901.0",
+ "@aws-sdk/token-providers": "3.901.0",
+ "@aws-sdk/types": "3.901.0",
+ "@smithy/property-provider": "^4.2.0",
+ "@smithy/shared-ini-file-loader": "^4.3.0",
+ "@smithy/types": "^4.6.0",
"tslib": "^2.6.2"
},
"engines": {
@@ -700,66 +700,18 @@
}
},
"node_modules/@aws-sdk/client-sesv2/node_modules/@aws-sdk/credential-provider-web-identity": {
- "version": "3.899.0",
- "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.899.0.tgz",
- "integrity": "sha512-UEn5o5FMcbeFPRRkJI6VCrgdyR9qsLlGA7+AKCYuYADsKbvJGIIQk6A2oD82vIVvLYD3TtbTLDLsF7haF9mpbw==",
+ "version": "3.901.0",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.901.0.tgz",
+ "integrity": "sha512-NYjy/6NLxH9m01+pfpB4ql8QgAorJcu8tw69kzHwUd/ql6wUDTbC7HcXqtKlIwWjzjgj2BKL7j6SyFapgCuafA==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
- "@aws-sdk/core": "3.899.0",
- "@aws-sdk/nested-clients": "3.899.0",
- "@aws-sdk/types": "3.893.0",
- "@smithy/property-provider": "^4.1.1",
- "@smithy/shared-ini-file-loader": "^4.2.0",
- "@smithy/types": "^4.5.0",
- "tslib": "^2.6.2"
- },
- "engines": {
- "node": ">=18.0.0"
- }
- },
- "node_modules/@aws-sdk/client-sesv2/node_modules/@aws-sdk/middleware-host-header": {
- "version": "3.893.0",
- "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-host-header/-/middleware-host-header-3.893.0.tgz",
- "integrity": "sha512-qL5xYRt80ahDfj9nDYLhpCNkDinEXvjLe/Qen/Y/u12+djrR2MB4DRa6mzBCkLkdXDtf0WAoW2EZsNCfGrmOEQ==",
- "dev": true,
- "license": "Apache-2.0",
- "dependencies": {
- "@aws-sdk/types": "3.893.0",
- "@smithy/protocol-http": "^5.2.1",
- "@smithy/types": "^4.5.0",
- "tslib": "^2.6.2"
- },
- "engines": {
- "node": ">=18.0.0"
- }
- },
- "node_modules/@aws-sdk/client-sesv2/node_modules/@aws-sdk/middleware-logger": {
- "version": "3.893.0",
- "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-logger/-/middleware-logger-3.893.0.tgz",
- "integrity": "sha512-ZqzMecjju5zkBquSIfVfCORI/3Mge21nUY4nWaGQy+NUXehqCGG4W7AiVpiHGOcY2cGJa7xeEkYcr2E2U9U0AA==",
- "dev": true,
- "license": "Apache-2.0",
- "dependencies": {
- "@aws-sdk/types": "3.893.0",
- "@smithy/types": "^4.5.0",
- "tslib": "^2.6.2"
- },
- "engines": {
- "node": ">=18.0.0"
- }
- },
- "node_modules/@aws-sdk/client-sesv2/node_modules/@aws-sdk/middleware-recursion-detection": {
- "version": "3.893.0",
- "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-recursion-detection/-/middleware-recursion-detection-3.893.0.tgz",
- "integrity": "sha512-H7Zotd9zUHQAr/wr3bcWHULYhEeoQrF54artgsoUGIf/9emv6LzY89QUccKIxYd6oHKNTrTyXm9F0ZZrzXNxlg==",
- "dev": true,
- "license": "Apache-2.0",
- "dependencies": {
- "@aws-sdk/types": "3.893.0",
- "@aws/lambda-invoke-store": "^0.0.1",
- "@smithy/protocol-http": "^5.2.1",
- "@smithy/types": "^4.5.0",
+ "@aws-sdk/core": "3.901.0",
+ "@aws-sdk/nested-clients": "3.901.0",
+ "@aws-sdk/types": "3.901.0",
+ "@smithy/property-provider": "^4.2.0",
+ "@smithy/shared-ini-file-loader": "^4.3.0",
+ "@smithy/types": "^4.6.0",
"tslib": "^2.6.2"
},
"engines": {
@@ -767,25 +719,25 @@
}
},
"node_modules/@aws-sdk/client-sesv2/node_modules/@aws-sdk/middleware-sdk-s3": {
- "version": "3.899.0",
- "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-sdk-s3/-/middleware-sdk-s3-3.899.0.tgz",
- "integrity": "sha512-/3/EIRSwQ5CNOSTHx96gVGzzmTe46OxcPG5FTgM6i9ZD+K/Q3J/UPGFL5DPzct5fXiSLvD1cGQitWHStVDjOVQ==",
+ "version": "3.901.0",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-sdk-s3/-/middleware-sdk-s3-3.901.0.tgz",
+ "integrity": "sha512-prgjVC3fDT2VIlmQPiw/cLee8r4frTam9GILRUVQyDdNtshNwV3MiaSCLzzQJjKJlLgnBLNUHJCSmvUVtg+3iA==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
- "@aws-sdk/core": "3.899.0",
- "@aws-sdk/types": "3.893.0",
+ "@aws-sdk/core": "3.901.0",
+ "@aws-sdk/types": "3.901.0",
"@aws-sdk/util-arn-parser": "3.893.0",
- "@smithy/core": "^3.13.0",
- "@smithy/node-config-provider": "^4.2.2",
- "@smithy/protocol-http": "^5.2.1",
- "@smithy/signature-v4": "^5.2.1",
- "@smithy/smithy-client": "^4.6.5",
- "@smithy/types": "^4.5.0",
- "@smithy/util-config-provider": "^4.1.0",
- "@smithy/util-middleware": "^4.1.1",
- "@smithy/util-stream": "^4.3.2",
- "@smithy/util-utf8": "^4.1.0",
+ "@smithy/core": "^3.14.0",
+ "@smithy/node-config-provider": "^4.3.0",
+ "@smithy/protocol-http": "^5.3.0",
+ "@smithy/signature-v4": "^5.3.0",
+ "@smithy/smithy-client": "^4.7.0",
+ "@smithy/types": "^4.6.0",
+ "@smithy/util-config-provider": "^4.2.0",
+ "@smithy/util-middleware": "^4.2.0",
+ "@smithy/util-stream": "^4.4.0",
+ "@smithy/util-utf8": "^4.2.0",
"tslib": "^2.6.2"
},
"engines": {
@@ -793,18 +745,18 @@
}
},
"node_modules/@aws-sdk/client-sesv2/node_modules/@aws-sdk/middleware-user-agent": {
- "version": "3.899.0",
- "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-user-agent/-/middleware-user-agent-3.899.0.tgz",
- "integrity": "sha512-6EsVCC9j1VIyVyLOg+HyO3z9L+c0PEwMiHe3kuocoMf8nkfjSzJfIl6zAtgAXWgP5MKvusTP2SUbS9ezEEHZ+A==",
+ "version": "3.901.0",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-user-agent/-/middleware-user-agent-3.901.0.tgz",
+ "integrity": "sha512-Zby4F03fvD9xAgXGPywyk4bC1jCbnyubMEYChLYohD+x20ULQCf+AimF/Btn7YL+hBpzh1+RmqmvZcx+RgwgNQ==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
- "@aws-sdk/core": "3.899.0",
- "@aws-sdk/types": "3.893.0",
- "@aws-sdk/util-endpoints": "3.895.0",
- "@smithy/core": "^3.13.0",
- "@smithy/protocol-http": "^5.2.1",
- "@smithy/types": "^4.5.0",
+ "@aws-sdk/core": "3.901.0",
+ "@aws-sdk/types": "3.901.0",
+ "@aws-sdk/util-endpoints": "3.901.0",
+ "@smithy/core": "^3.14.0",
+ "@smithy/protocol-http": "^5.3.0",
+ "@smithy/types": "^4.6.0",
"tslib": "^2.6.2"
},
"engines": {
@@ -812,67 +764,49 @@
}
},
"node_modules/@aws-sdk/client-sesv2/node_modules/@aws-sdk/nested-clients": {
- "version": "3.899.0",
- "resolved": "https://registry.npmjs.org/@aws-sdk/nested-clients/-/nested-clients-3.899.0.tgz",
- "integrity": "sha512-ySXXsFO0RH28VISEqvCuPZ78VAkK45/+OCIJgPvYpcCX9CVs70XSvMPXDI46I49mudJ1s4H3IUKccYSEtA+jaw==",
+ "version": "3.901.0",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/nested-clients/-/nested-clients-3.901.0.tgz",
+ "integrity": "sha512-feAAAMsVwctk2Tms40ONybvpfJPLCmSdI+G+OTrNpizkGLNl6ik2Ng2RzxY6UqOfN8abqKP/DOUj1qYDRDG8ag==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"@aws-crypto/sha256-browser": "5.2.0",
"@aws-crypto/sha256-js": "5.2.0",
- "@aws-sdk/core": "3.899.0",
- "@aws-sdk/middleware-host-header": "3.893.0",
- "@aws-sdk/middleware-logger": "3.893.0",
- "@aws-sdk/middleware-recursion-detection": "3.893.0",
- "@aws-sdk/middleware-user-agent": "3.899.0",
- "@aws-sdk/region-config-resolver": "3.893.0",
- "@aws-sdk/types": "3.893.0",
- "@aws-sdk/util-endpoints": "3.895.0",
- "@aws-sdk/util-user-agent-browser": "3.893.0",
- "@aws-sdk/util-user-agent-node": "3.899.0",
- "@smithy/config-resolver": "^4.2.2",
- "@smithy/core": "^3.13.0",
- "@smithy/fetch-http-handler": "^5.2.1",
- "@smithy/hash-node": "^4.1.1",
- "@smithy/invalid-dependency": "^4.1.1",
- "@smithy/middleware-content-length": "^4.1.1",
- "@smithy/middleware-endpoint": "^4.2.5",
- "@smithy/middleware-retry": "^4.3.1",
- "@smithy/middleware-serde": "^4.1.1",
- "@smithy/middleware-stack": "^4.1.1",
- "@smithy/node-config-provider": "^4.2.2",
- "@smithy/node-http-handler": "^4.2.1",
- "@smithy/protocol-http": "^5.2.1",
- "@smithy/smithy-client": "^4.6.5",
- "@smithy/types": "^4.5.0",
- "@smithy/url-parser": "^4.1.1",
- "@smithy/util-base64": "^4.1.0",
- "@smithy/util-body-length-browser": "^4.1.0",
- "@smithy/util-body-length-node": "^4.1.0",
- "@smithy/util-defaults-mode-browser": "^4.1.5",
- "@smithy/util-defaults-mode-node": "^4.1.5",
- "@smithy/util-endpoints": "^3.1.2",
- "@smithy/util-middleware": "^4.1.1",
- "@smithy/util-retry": "^4.1.2",
- "@smithy/util-utf8": "^4.1.0",
- "tslib": "^2.6.2"
- },
- "engines": {
- "node": ">=18.0.0"
- }
- },
- "node_modules/@aws-sdk/client-sesv2/node_modules/@aws-sdk/region-config-resolver": {
- "version": "3.893.0",
- "resolved": "https://registry.npmjs.org/@aws-sdk/region-config-resolver/-/region-config-resolver-3.893.0.tgz",
- "integrity": "sha512-/cJvh3Zsa+Of0Zbg7vl9wp/kZtdb40yk/2+XcroAMVPO9hPvmS9r/UOm6tO7FeX4TtkRFwWaQJiTZTgSdsPY+Q==",
- "dev": true,
- "license": "Apache-2.0",
- "dependencies": {
- "@aws-sdk/types": "3.893.0",
- "@smithy/node-config-provider": "^4.2.2",
- "@smithy/types": "^4.5.0",
- "@smithy/util-config-provider": "^4.1.0",
- "@smithy/util-middleware": "^4.1.1",
+ "@aws-sdk/core": "3.901.0",
+ "@aws-sdk/middleware-host-header": "3.901.0",
+ "@aws-sdk/middleware-logger": "3.901.0",
+ "@aws-sdk/middleware-recursion-detection": "3.901.0",
+ "@aws-sdk/middleware-user-agent": "3.901.0",
+ "@aws-sdk/region-config-resolver": "3.901.0",
+ "@aws-sdk/types": "3.901.0",
+ "@aws-sdk/util-endpoints": "3.901.0",
+ "@aws-sdk/util-user-agent-browser": "3.901.0",
+ "@aws-sdk/util-user-agent-node": "3.901.0",
+ "@smithy/config-resolver": "^4.3.0",
+ "@smithy/core": "^3.14.0",
+ "@smithy/fetch-http-handler": "^5.3.0",
+ "@smithy/hash-node": "^4.2.0",
+ "@smithy/invalid-dependency": "^4.2.0",
+ "@smithy/middleware-content-length": "^4.2.0",
+ "@smithy/middleware-endpoint": "^4.3.0",
+ "@smithy/middleware-retry": "^4.4.0",
+ "@smithy/middleware-serde": "^4.2.0",
+ "@smithy/middleware-stack": "^4.2.0",
+ "@smithy/node-config-provider": "^4.3.0",
+ "@smithy/node-http-handler": "^4.3.0",
+ "@smithy/protocol-http": "^5.3.0",
+ "@smithy/smithy-client": "^4.7.0",
+ "@smithy/types": "^4.6.0",
+ "@smithy/url-parser": "^4.2.0",
+ "@smithy/util-base64": "^4.2.0",
+ "@smithy/util-body-length-browser": "^4.2.0",
+ "@smithy/util-body-length-node": "^4.2.0",
+ "@smithy/util-defaults-mode-browser": "^4.2.0",
+ "@smithy/util-defaults-mode-node": "^4.2.0",
+ "@smithy/util-endpoints": "^3.2.0",
+ "@smithy/util-middleware": "^4.2.0",
+ "@smithy/util-retry": "^4.2.0",
+ "@smithy/util-utf8": "^4.2.0",
"tslib": "^2.6.2"
},
"engines": {
@@ -880,17 +814,17 @@
}
},
"node_modules/@aws-sdk/client-sesv2/node_modules/@aws-sdk/signature-v4-multi-region": {
- "version": "3.899.0",
- "resolved": "https://registry.npmjs.org/@aws-sdk/signature-v4-multi-region/-/signature-v4-multi-region-3.899.0.tgz",
- "integrity": "sha512-wV51Jogxhd7dI4Q2Y1ASbkwTsRT3G8uwWFDCwl+WaErOQAzofKlV6nFJQlfgjMk4iEn2gFOIWqJ8fMTGShRK/A==",
+ "version": "3.901.0",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/signature-v4-multi-region/-/signature-v4-multi-region-3.901.0.tgz",
+ "integrity": "sha512-2IWxbll/pRucp1WQkHi2W5E2SVPGBvk4Is923H7gpNksbVFws18ItjMM8ZpGm44cJEoy1zR5gjhLFklatpuoOw==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
- "@aws-sdk/middleware-sdk-s3": "3.899.0",
- "@aws-sdk/types": "3.893.0",
- "@smithy/protocol-http": "^5.2.1",
- "@smithy/signature-v4": "^5.2.1",
- "@smithy/types": "^4.5.0",
+ "@aws-sdk/middleware-sdk-s3": "3.901.0",
+ "@aws-sdk/types": "3.901.0",
+ "@smithy/protocol-http": "^5.3.0",
+ "@smithy/signature-v4": "^5.3.0",
+ "@smithy/types": "^4.6.0",
"tslib": "^2.6.2"
},
"engines": {
@@ -898,49 +832,18 @@
}
},
"node_modules/@aws-sdk/client-sesv2/node_modules/@aws-sdk/token-providers": {
- "version": "3.899.0",
- "resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.899.0.tgz",
- "integrity": "sha512-Ovu1nWr8HafYa/7DaUvvPnzM/yDUGDBqaiS7rRzv++F5VwyFY37+z/mHhvRnr+PbNWo8uf22a121SNue5uwP2w==",
+ "version": "3.901.0",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.901.0.tgz",
+ "integrity": "sha512-pJEr1Ggbc/uVTDqp9IbNu9hdr0eQf3yZix3s4Nnyvmg4xmJSGAlbPC9LrNr5u3CDZoc8Z9CuLrvbP4MwYquNpQ==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
- "@aws-sdk/core": "3.899.0",
- "@aws-sdk/nested-clients": "3.899.0",
- "@aws-sdk/types": "3.893.0",
- "@smithy/property-provider": "^4.1.1",
- "@smithy/shared-ini-file-loader": "^4.2.0",
- "@smithy/types": "^4.5.0",
- "tslib": "^2.6.2"
- },
- "engines": {
- "node": ">=18.0.0"
- }
- },
- "node_modules/@aws-sdk/client-sesv2/node_modules/@aws-sdk/types": {
- "version": "3.893.0",
- "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.893.0.tgz",
- "integrity": "sha512-Aht1nn5SnA0N+Tjv0dzhAY7CQbxVtmq1bBR6xI0MhG7p2XYVh1wXuKTzrldEvQWwA3odOYunAfT9aBiKZx9qIg==",
- "dev": true,
- "license": "Apache-2.0",
- "dependencies": {
- "@smithy/types": "^4.5.0",
- "tslib": "^2.6.2"
- },
- "engines": {
- "node": ">=18.0.0"
- }
- },
- "node_modules/@aws-sdk/client-sesv2/node_modules/@aws-sdk/util-endpoints": {
- "version": "3.895.0",
- "resolved": "https://registry.npmjs.org/@aws-sdk/util-endpoints/-/util-endpoints-3.895.0.tgz",
- "integrity": "sha512-MhxBvWbwxmKknuggO2NeMwOVkHOYL98pZ+1ZRI5YwckoCL3AvISMnPJgfN60ww6AIXHGpkp+HhpFdKOe8RHSEg==",
- "dev": true,
- "license": "Apache-2.0",
- "dependencies": {
- "@aws-sdk/types": "3.893.0",
- "@smithy/types": "^4.5.0",
- "@smithy/url-parser": "^4.1.1",
- "@smithy/util-endpoints": "^3.1.2",
+ "@aws-sdk/core": "3.901.0",
+ "@aws-sdk/nested-clients": "3.901.0",
+ "@aws-sdk/types": "3.901.0",
+ "@smithy/property-provider": "^4.2.0",
+ "@smithy/shared-ini-file-loader": "^4.3.0",
+ "@smithy/types": "^4.6.0",
"tslib": "^2.6.2"
},
"engines": {
@@ -948,29 +851,29 @@
}
},
"node_modules/@aws-sdk/client-sesv2/node_modules/@aws-sdk/util-user-agent-browser": {
- "version": "3.893.0",
- "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-browser/-/util-user-agent-browser-3.893.0.tgz",
- "integrity": "sha512-PE9NtbDBW6Kgl1bG6A5fF3EPo168tnkj8TgMcT0sg4xYBWsBpq0bpJZRh+Jm5Bkwiw9IgTCLjEU7mR6xWaMB9w==",
+ "version": "3.901.0",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-browser/-/util-user-agent-browser-3.901.0.tgz",
+ "integrity": "sha512-Ntb6V/WFI21Ed4PDgL/8NSfoZQQf9xzrwNgiwvnxgAl/KvAvRBgQtqj5gHsDX8Nj2YmJuVoHfH9BGjL9VQ4WNg==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
- "@aws-sdk/types": "3.893.0",
- "@smithy/types": "^4.5.0",
+ "@aws-sdk/types": "3.901.0",
+ "@smithy/types": "^4.6.0",
"bowser": "^2.11.0",
"tslib": "^2.6.2"
}
},
"node_modules/@aws-sdk/client-sesv2/node_modules/@aws-sdk/util-user-agent-node": {
- "version": "3.899.0",
- "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-node/-/util-user-agent-node-3.899.0.tgz",
- "integrity": "sha512-CiP0UAVQWLg2+8yciUBzVLaK5Fr7jBQ7wVu+p/O2+nlCOD3E3vtL1KZ1qX/d3OVpVSVaMAdZ9nbyewGV9hvjjg==",
+ "version": "3.901.0",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-node/-/util-user-agent-node-3.901.0.tgz",
+ "integrity": "sha512-l59KQP5TY7vPVUfEURc7P5BJKuNg1RSsAKBQW7LHLECXjLqDUbo2SMLrexLBEoArSt6E8QOrIN0C8z/0Xk0jYw==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
- "@aws-sdk/middleware-user-agent": "3.899.0",
- "@aws-sdk/types": "3.893.0",
- "@smithy/node-config-provider": "^4.2.2",
- "@smithy/types": "^4.5.0",
+ "@aws-sdk/middleware-user-agent": "3.901.0",
+ "@aws-sdk/types": "3.901.0",
+ "@smithy/node-config-provider": "^4.3.0",
+ "@smithy/types": "^4.6.0",
"tslib": "^2.6.2"
},
"engines": {
@@ -985,21 +888,6 @@
}
}
},
- "node_modules/@aws-sdk/client-sesv2/node_modules/@aws-sdk/xml-builder": {
- "version": "3.894.0",
- "resolved": "https://registry.npmjs.org/@aws-sdk/xml-builder/-/xml-builder-3.894.0.tgz",
- "integrity": "sha512-E6EAMc9dT1a2DOdo4zyOf3fp5+NJ2wI+mcm7RaW1baFIWDwcb99PpvWoV7YEiK7oaBDshuOEGWKUSYXdW+JYgA==",
- "dev": true,
- "license": "Apache-2.0",
- "dependencies": {
- "@smithy/types": "^4.5.0",
- "fast-xml-parser": "5.2.5",
- "tslib": "^2.6.2"
- },
- "engines": {
- "node": ">=18.0.0"
- }
- },
"node_modules/@aws-sdk/client-sso": {
"version": "3.908.0",
"resolved": "https://registry.npmjs.org/@aws-sdk/client-sso/-/client-sso-3.908.0.tgz",
@@ -1680,6 +1568,22 @@
"node": ">=6.9.0"
}
},
+ "node_modules/@babel/generator/node_modules/@babel/parser": {
+ "version": "7.28.4",
+ "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.4.tgz",
+ "integrity": "sha512-yZbBqeM6TkpP9du/I2pUZnJsRMGGvOuIrhjzC1AwHwW+6he4mni6Bp/m8ijn0iOuZuPI2BfkCoSRunpyjnrQKg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/types": "^7.28.4"
+ },
+ "bin": {
+ "parser": "bin/babel-parser.js"
+ },
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
"node_modules/@babel/helper-compilation-targets": {
"version": "7.27.2",
"resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.27.2.tgz",
@@ -1731,6 +1635,41 @@
"node": ">=6.9.0"
}
},
+ "node_modules/@babel/helper-module-imports/node_modules/@babel/parser": {
+ "version": "7.28.4",
+ "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.4.tgz",
+ "integrity": "sha512-yZbBqeM6TkpP9du/I2pUZnJsRMGGvOuIrhjzC1AwHwW+6he4mni6Bp/m8ijn0iOuZuPI2BfkCoSRunpyjnrQKg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/types": "^7.28.4"
+ },
+ "bin": {
+ "parser": "bin/babel-parser.js"
+ },
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/@babel/helper-module-imports/node_modules/@babel/traverse": {
+ "version": "7.28.4",
+ "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.4.tgz",
+ "integrity": "sha512-YEzuboP2qvQavAcjgQNVgsvHIDv6ZpwXvcvjmyySP2DIMuByS/6ioU5G9pYrWHM6T2YDfc7xga9iNzYOs12CFQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/code-frame": "^7.27.1",
+ "@babel/generator": "^7.28.3",
+ "@babel/helper-globals": "^7.28.0",
+ "@babel/parser": "^7.28.4",
+ "@babel/template": "^7.27.2",
+ "@babel/types": "^7.28.4",
+ "debug": "^4.3.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
"node_modules/@babel/helper-module-transforms": {
"version": "7.28.3",
"resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.3.tgz",
@@ -1749,6 +1688,41 @@
"@babel/core": "^7.0.0"
}
},
+ "node_modules/@babel/helper-module-transforms/node_modules/@babel/parser": {
+ "version": "7.28.4",
+ "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.4.tgz",
+ "integrity": "sha512-yZbBqeM6TkpP9du/I2pUZnJsRMGGvOuIrhjzC1AwHwW+6he4mni6Bp/m8ijn0iOuZuPI2BfkCoSRunpyjnrQKg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/types": "^7.28.4"
+ },
+ "bin": {
+ "parser": "bin/babel-parser.js"
+ },
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/@babel/helper-module-transforms/node_modules/@babel/traverse": {
+ "version": "7.28.4",
+ "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.4.tgz",
+ "integrity": "sha512-YEzuboP2qvQavAcjgQNVgsvHIDv6ZpwXvcvjmyySP2DIMuByS/6ioU5G9pYrWHM6T2YDfc7xga9iNzYOs12CFQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/code-frame": "^7.27.1",
+ "@babel/generator": "^7.28.3",
+ "@babel/helper-globals": "^7.28.0",
+ "@babel/parser": "^7.28.4",
+ "@babel/template": "^7.27.2",
+ "@babel/types": "^7.28.4",
+ "debug": "^4.3.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
"node_modules/@babel/helper-string-parser": {
"version": "7.27.1",
"resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz",
@@ -1794,13 +1768,13 @@
}
},
"node_modules/@babel/parser": {
- "version": "7.28.4",
- "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.4.tgz",
- "integrity": "sha512-yZbBqeM6TkpP9du/I2pUZnJsRMGGvOuIrhjzC1AwHwW+6he4mni6Bp/m8ijn0iOuZuPI2BfkCoSRunpyjnrQKg==",
+ "version": "7.27.0",
+ "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.27.0.tgz",
+ "integrity": "sha512-iaepho73/2Pz7w2eMS0Q5f83+0RKI7i4xmiYeBmDzfRVbQtTOG7Ts0S4HzJVsTMGI9keU8rNfuZr8DKfSt7Yyg==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@babel/types": "^7.28.4"
+ "@babel/types": "^7.27.0"
},
"bin": {
"parser": "bin/babel-parser.js"
@@ -1824,20 +1798,36 @@
"node": ">=6.9.0"
}
},
- "node_modules/@babel/traverse": {
+ "node_modules/@babel/template/node_modules/@babel/parser": {
"version": "7.28.4",
- "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.4.tgz",
- "integrity": "sha512-YEzuboP2qvQavAcjgQNVgsvHIDv6ZpwXvcvjmyySP2DIMuByS/6ioU5G9pYrWHM6T2YDfc7xga9iNzYOs12CFQ==",
+ "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.4.tgz",
+ "integrity": "sha512-yZbBqeM6TkpP9du/I2pUZnJsRMGGvOuIrhjzC1AwHwW+6he4mni6Bp/m8ijn0iOuZuPI2BfkCoSRunpyjnrQKg==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@babel/code-frame": "^7.27.1",
- "@babel/generator": "^7.28.3",
- "@babel/helper-globals": "^7.28.0",
- "@babel/parser": "^7.28.4",
- "@babel/template": "^7.27.2",
- "@babel/types": "^7.28.4",
- "debug": "^4.3.1"
+ "@babel/types": "^7.28.4"
+ },
+ "bin": {
+ "parser": "bin/babel-parser.js"
+ },
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/@babel/traverse": {
+ "version": "7.27.0",
+ "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.27.0.tgz",
+ "integrity": "sha512-19lYZFzYVQkkHkl4Cy4WrAVcqBkgvV2YM2TU3xG6DIwO7O3ecbDPfW3yM3bjAGcqcQHi+CCtjMR3dIEHxsd6bA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/code-frame": "^7.26.2",
+ "@babel/generator": "^7.27.0",
+ "@babel/parser": "^7.27.0",
+ "@babel/template": "^7.27.0",
+ "@babel/types": "^7.27.0",
+ "debug": "^4.3.1",
+ "globals": "^11.1.0"
},
"engines": {
"node": ">=6.9.0"
@@ -1923,17 +1913,6 @@
"@noble/ciphers": "^1.0.0"
}
},
- "node_modules/@emnapi/runtime": {
- "version": "1.5.0",
- "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.5.0.tgz",
- "integrity": "sha512-97/BJ3iXHww3djw6hYIfErCZFee7qCtrneuLa20UXFCOTCfBM2cvQHjWJ2EG0s0MtdNwInarqCTz35i4wWXHsQ==",
- "dev": true,
- "license": "MIT",
- "optional": true,
- "dependencies": {
- "tslib": "^2.4.0"
- }
- },
"node_modules/@esbuild-kit/core-utils": {
"version": "3.3.2",
"resolved": "https://registry.npmjs.org/@esbuild-kit/core-utils/-/core-utils-3.3.2.tgz",
@@ -2146,6 +2125,18 @@
"url": "https://opencollective.com/eslint"
}
},
+ "node_modules/@eslint/eslintrc/node_modules/globals": {
+ "version": "14.0.0",
+ "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz",
+ "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
"node_modules/@eslint/js": {
"version": "9.37.0",
"resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.37.0.tgz",
@@ -2354,154 +2345,6 @@
"node": ">=18"
}
},
- "node_modules/@img/sharp-darwin-arm64": {
- "version": "0.34.4",
- "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.34.4.tgz",
- "integrity": "sha512-sitdlPzDVyvmINUdJle3TNHl+AG9QcwiAMsXmccqsCOMZNIdW2/7S26w0LyU8euiLVzFBL3dXPwVCq/ODnf2vA==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "license": "Apache-2.0",
- "optional": true,
- "os": [
- "darwin"
- ],
- "engines": {
- "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
- },
- "funding": {
- "url": "https://opencollective.com/libvips"
- },
- "optionalDependencies": {
- "@img/sharp-libvips-darwin-arm64": "1.2.3"
- }
- },
- "node_modules/@img/sharp-darwin-x64": {
- "version": "0.34.4",
- "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.34.4.tgz",
- "integrity": "sha512-rZheupWIoa3+SOdF/IcUe1ah4ZDpKBGWcsPX6MT0lYniH9micvIU7HQkYTfrx5Xi8u+YqwLtxC/3vl8TQN6rMg==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "Apache-2.0",
- "optional": true,
- "os": [
- "darwin"
- ],
- "engines": {
- "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
- },
- "funding": {
- "url": "https://opencollective.com/libvips"
- },
- "optionalDependencies": {
- "@img/sharp-libvips-darwin-x64": "1.2.3"
- }
- },
- "node_modules/@img/sharp-libvips-darwin-arm64": {
- "version": "1.2.3",
- "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.2.3.tgz",
- "integrity": "sha512-QzWAKo7kpHxbuHqUC28DZ9pIKpSi2ts2OJnoIGI26+HMgq92ZZ4vk8iJd4XsxN+tYfNJxzH6W62X5eTcsBymHw==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "license": "LGPL-3.0-or-later",
- "optional": true,
- "os": [
- "darwin"
- ],
- "funding": {
- "url": "https://opencollective.com/libvips"
- }
- },
- "node_modules/@img/sharp-libvips-darwin-x64": {
- "version": "1.2.3",
- "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.2.3.tgz",
- "integrity": "sha512-Ju+g2xn1E2AKO6YBhxjj+ACcsPQRHT0bhpglxcEf+3uyPY+/gL8veniKoo96335ZaPo03bdDXMv0t+BBFAbmRA==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "LGPL-3.0-or-later",
- "optional": true,
- "os": [
- "darwin"
- ],
- "funding": {
- "url": "https://opencollective.com/libvips"
- }
- },
- "node_modules/@img/sharp-libvips-linux-arm": {
- "version": "1.2.3",
- "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.2.3.tgz",
- "integrity": "sha512-x1uE93lyP6wEwGvgAIV0gP6zmaL/a0tGzJs/BIDDG0zeBhMnuUPm7ptxGhUbcGs4okDJrk4nxgrmxpib9g6HpA==",
- "cpu": [
- "arm"
- ],
- "dev": true,
- "license": "LGPL-3.0-or-later",
- "optional": true,
- "os": [
- "linux"
- ],
- "funding": {
- "url": "https://opencollective.com/libvips"
- }
- },
- "node_modules/@img/sharp-libvips-linux-arm64": {
- "version": "1.2.3",
- "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.2.3.tgz",
- "integrity": "sha512-I4RxkXU90cpufazhGPyVujYwfIm9Nk1QDEmiIsaPwdnm013F7RIceaCc87kAH+oUB1ezqEvC6ga4m7MSlqsJvQ==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "license": "LGPL-3.0-or-later",
- "optional": true,
- "os": [
- "linux"
- ],
- "funding": {
- "url": "https://opencollective.com/libvips"
- }
- },
- "node_modules/@img/sharp-libvips-linux-ppc64": {
- "version": "1.2.3",
- "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.2.3.tgz",
- "integrity": "sha512-Y2T7IsQvJLMCBM+pmPbM3bKT/yYJvVtLJGfCs4Sp95SjvnFIjynbjzsa7dY1fRJX45FTSfDksbTp6AGWudiyCg==",
- "cpu": [
- "ppc64"
- ],
- "dev": true,
- "license": "LGPL-3.0-or-later",
- "optional": true,
- "os": [
- "linux"
- ],
- "funding": {
- "url": "https://opencollective.com/libvips"
- }
- },
- "node_modules/@img/sharp-libvips-linux-s390x": {
- "version": "1.2.3",
- "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.2.3.tgz",
- "integrity": "sha512-RgWrs/gVU7f+K7P+KeHFaBAJlNkD1nIZuVXdQv6S+fNA6syCcoboNjsV2Pou7zNlVdNQoQUpQTk8SWDHUA3y/w==",
- "cpu": [
- "s390x"
- ],
- "dev": true,
- "license": "LGPL-3.0-or-later",
- "optional": true,
- "os": [
- "linux"
- ],
- "funding": {
- "url": "https://opencollective.com/libvips"
- }
- },
"node_modules/@img/sharp-libvips-linux-x64": {
"version": "1.2.3",
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.2.3.tgz",
@@ -2519,132 +2362,6 @@
"url": "https://opencollective.com/libvips"
}
},
- "node_modules/@img/sharp-libvips-linuxmusl-arm64": {
- "version": "1.2.3",
- "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.2.3.tgz",
- "integrity": "sha512-F9q83RZ8yaCwENw1GieztSfj5msz7GGykG/BA+MOUefvER69K/ubgFHNeSyUu64amHIYKGDs4sRCMzXVj8sEyw==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "license": "LGPL-3.0-or-later",
- "optional": true,
- "os": [
- "linux"
- ],
- "funding": {
- "url": "https://opencollective.com/libvips"
- }
- },
- "node_modules/@img/sharp-libvips-linuxmusl-x64": {
- "version": "1.2.3",
- "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.2.3.tgz",
- "integrity": "sha512-U5PUY5jbc45ANM6tSJpsgqmBF/VsL6LnxJmIf11kB7J5DctHgqm0SkuXzVWtIY90GnJxKnC/JT251TDnk1fu/g==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "LGPL-3.0-or-later",
- "optional": true,
- "os": [
- "linux"
- ],
- "funding": {
- "url": "https://opencollective.com/libvips"
- }
- },
- "node_modules/@img/sharp-linux-arm": {
- "version": "0.34.4",
- "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.34.4.tgz",
- "integrity": "sha512-Xyam4mlqM0KkTHYVSuc6wXRmM7LGN0P12li03jAnZ3EJWZqj83+hi8Y9UxZUbxsgsK1qOEwg7O0Bc0LjqQVtxA==",
- "cpu": [
- "arm"
- ],
- "dev": true,
- "license": "Apache-2.0",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
- },
- "funding": {
- "url": "https://opencollective.com/libvips"
- },
- "optionalDependencies": {
- "@img/sharp-libvips-linux-arm": "1.2.3"
- }
- },
- "node_modules/@img/sharp-linux-arm64": {
- "version": "0.34.4",
- "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.34.4.tgz",
- "integrity": "sha512-YXU1F/mN/Wu786tl72CyJjP/Ngl8mGHN1hST4BGl+hiW5jhCnV2uRVTNOcaYPs73NeT/H8Upm3y9582JVuZHrQ==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "license": "Apache-2.0",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
- },
- "funding": {
- "url": "https://opencollective.com/libvips"
- },
- "optionalDependencies": {
- "@img/sharp-libvips-linux-arm64": "1.2.3"
- }
- },
- "node_modules/@img/sharp-linux-ppc64": {
- "version": "0.34.4",
- "resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.34.4.tgz",
- "integrity": "sha512-F4PDtF4Cy8L8hXA2p3TO6s4aDt93v+LKmpcYFLAVdkkD3hSxZzee0rh6/+94FpAynsuMpLX5h+LRsSG3rIciUQ==",
- "cpu": [
- "ppc64"
- ],
- "dev": true,
- "license": "Apache-2.0",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
- },
- "funding": {
- "url": "https://opencollective.com/libvips"
- },
- "optionalDependencies": {
- "@img/sharp-libvips-linux-ppc64": "1.2.3"
- }
- },
- "node_modules/@img/sharp-linux-s390x": {
- "version": "0.34.4",
- "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.34.4.tgz",
- "integrity": "sha512-qVrZKE9Bsnzy+myf7lFKvng6bQzhNUAYcVORq2P7bDlvmF6u2sCmK2KyEQEBdYk+u3T01pVsPrkj943T1aJAsw==",
- "cpu": [
- "s390x"
- ],
- "dev": true,
- "license": "Apache-2.0",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
- },
- "funding": {
- "url": "https://opencollective.com/libvips"
- },
- "optionalDependencies": {
- "@img/sharp-libvips-linux-s390x": "1.2.3"
- }
- },
"node_modules/@img/sharp-linux-x64": {
"version": "0.34.4",
"resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.34.4.tgz",
@@ -2668,132 +2385,6 @@
"@img/sharp-libvips-linux-x64": "1.2.3"
}
},
- "node_modules/@img/sharp-linuxmusl-arm64": {
- "version": "0.34.4",
- "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.34.4.tgz",
- "integrity": "sha512-8hDVvW9eu4yHWnjaOOR8kHVrew1iIX+MUgwxSuH2XyYeNRtLUe4VNioSqbNkB7ZYQJj9rUTT4PyRscyk2PXFKA==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "license": "Apache-2.0",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
- },
- "funding": {
- "url": "https://opencollective.com/libvips"
- },
- "optionalDependencies": {
- "@img/sharp-libvips-linuxmusl-arm64": "1.2.3"
- }
- },
- "node_modules/@img/sharp-linuxmusl-x64": {
- "version": "0.34.4",
- "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.34.4.tgz",
- "integrity": "sha512-lU0aA5L8QTlfKjpDCEFOZsTYGn3AEiO6db8W5aQDxj0nQkVrZWmN3ZP9sYKWJdtq3PWPhUNlqehWyXpYDcI9Sg==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "Apache-2.0",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
- },
- "funding": {
- "url": "https://opencollective.com/libvips"
- },
- "optionalDependencies": {
- "@img/sharp-libvips-linuxmusl-x64": "1.2.3"
- }
- },
- "node_modules/@img/sharp-wasm32": {
- "version": "0.34.4",
- "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.34.4.tgz",
- "integrity": "sha512-33QL6ZO/qpRyG7woB/HUALz28WnTMI2W1jgX3Nu2bypqLIKx/QKMILLJzJjI+SIbvXdG9fUnmrxR7vbi1sTBeA==",
- "cpu": [
- "wasm32"
- ],
- "dev": true,
- "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT",
- "optional": true,
- "dependencies": {
- "@emnapi/runtime": "^1.5.0"
- },
- "engines": {
- "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
- },
- "funding": {
- "url": "https://opencollective.com/libvips"
- }
- },
- "node_modules/@img/sharp-win32-arm64": {
- "version": "0.34.4",
- "resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.34.4.tgz",
- "integrity": "sha512-2Q250do/5WXTwxW3zjsEuMSv5sUU4Tq9VThWKlU2EYLm4MB7ZeMwF+SFJutldYODXF6jzc6YEOC+VfX0SZQPqA==",
- "cpu": [
- "arm64"
- ],
- "dev": true,
- "license": "Apache-2.0 AND LGPL-3.0-or-later",
- "optional": true,
- "os": [
- "win32"
- ],
- "engines": {
- "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
- },
- "funding": {
- "url": "https://opencollective.com/libvips"
- }
- },
- "node_modules/@img/sharp-win32-ia32": {
- "version": "0.34.4",
- "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.34.4.tgz",
- "integrity": "sha512-3ZeLue5V82dT92CNL6rsal6I2weKw1cYu+rGKm8fOCCtJTR2gYeUfY3FqUnIJsMUPIH68oS5jmZ0NiJ508YpEw==",
- "cpu": [
- "ia32"
- ],
- "dev": true,
- "license": "Apache-2.0 AND LGPL-3.0-or-later",
- "optional": true,
- "os": [
- "win32"
- ],
- "engines": {
- "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
- },
- "funding": {
- "url": "https://opencollective.com/libvips"
- }
- },
- "node_modules/@img/sharp-win32-x64": {
- "version": "0.34.4",
- "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.34.4.tgz",
- "integrity": "sha512-xIyj4wpYs8J18sVN3mSQjwrw7fKUqRw+Z5rnHNCy5fYTxigBz81u5mOMPmFumwjcn8+ld1ppptMBCLic1nz6ig==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "Apache-2.0 AND LGPL-3.0-or-later",
- "optional": true,
- "os": [
- "win32"
- ],
- "engines": {
- "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
- },
- "funding": {
- "url": "https://opencollective.com/libvips"
- }
- },
"node_modules/@ioredis/commands": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/@ioredis/commands/-/commands-1.4.0.tgz",
@@ -2969,22 +2560,6 @@
"node": ">= 10"
}
},
- "node_modules/@next/swc-linux-x64-musl": {
- "version": "15.5.4",
- "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-15.5.4.tgz",
- "integrity": "sha512-nlQQ6nfgN0nCO/KuyEUwwOdwQIGjOs4WNMjEUtpIQJPR2NUfmGpW2wkJln1d4nJ7oUzd1g4GivH5GoEPBgfsdw==",
- "cpu": [
- "x64"
- ],
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">= 10"
- }
- },
"node_modules/@noble/ciphers": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/@noble/ciphers/-/ciphers-1.3.0.tgz",
@@ -3068,22 +2643,6 @@
"node": ">= 10"
}
},
- "node_modules/@node-rs/argon2-linux-x64-musl": {
- "version": "2.0.2",
- "resolved": "https://registry.npmjs.org/@node-rs/argon2-linux-x64-musl/-/argon2-linux-x64-musl-2.0.2.tgz",
- "integrity": "sha512-of5uPqk7oCRF/44a89YlWTEfjsftPywyTULwuFDKyD8QtVZoonrJR6ZWvfFE/6jBT68S0okAkAzzMEdBVWdxWw==",
- "cpu": [
- "x64"
- ],
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">= 10"
- }
- },
"node_modules/@node-rs/bcrypt": {
"version": "1.9.0",
"resolved": "https://registry.npmjs.org/@node-rs/bcrypt/-/bcrypt-1.9.0.tgz",
@@ -3129,22 +2688,6 @@
"node": ">= 10"
}
},
- "node_modules/@node-rs/bcrypt-linux-x64-musl": {
- "version": "1.9.0",
- "resolved": "https://registry.npmjs.org/@node-rs/bcrypt-linux-x64-musl/-/bcrypt-linux-x64-musl-1.9.0.tgz",
- "integrity": "sha512-duIiuqQ+Lew8ASSAYm6ZRqcmfBGWwsi81XLUwz86a2HR7Qv6V4yc3ZAUQovAikhjCsIqe8C11JlAZSK6+PlXYg==",
- "cpu": [
- "x64"
- ],
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">= 10"
- }
- },
"node_modules/@nodelib/fs.scandir": {
"version": "2.1.5",
"resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
@@ -3403,9 +2946,9 @@
}
},
"node_modules/@posthog/core": {
- "version": "1.2.4",
- "resolved": "https://registry.npmjs.org/@posthog/core/-/core-1.2.4.tgz",
- "integrity": "sha512-o2TkycuV98PtAkcqE8B1DJv5LBvHEDTWirK5TlkQMeF2MJg0BYliY95CeRZFILNgZJCbI3k/fhahSMRQlpXOMg==",
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/@posthog/core/-/core-1.3.0.tgz",
+ "integrity": "sha512-hxLL8kZNHH098geedcxCz8y6xojkNYbmJEW+1vFXsmPcExyCXIUUJ/34X6xa9GcprKxd0Wsx3vfJQLQX4iVPhw==",
"license": "MIT"
},
"node_modules/@radix-ui/colors": {
@@ -4802,41 +4345,6 @@
"zod": "3.24.3"
}
},
- "node_modules/@react-email/preview-server/node_modules/@babel/parser": {
- "version": "7.27.0",
- "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.27.0.tgz",
- "integrity": "sha512-iaepho73/2Pz7w2eMS0Q5f83+0RKI7i4xmiYeBmDzfRVbQtTOG7Ts0S4HzJVsTMGI9keU8rNfuZr8DKfSt7Yyg==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@babel/types": "^7.27.0"
- },
- "bin": {
- "parser": "bin/babel-parser.js"
- },
- "engines": {
- "node": ">=6.0.0"
- }
- },
- "node_modules/@react-email/preview-server/node_modules/@babel/traverse": {
- "version": "7.27.0",
- "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.27.0.tgz",
- "integrity": "sha512-19lYZFzYVQkkHkl4Cy4WrAVcqBkgvV2YM2TU3xG6DIwO7O3ecbDPfW3yM3bjAGcqcQHi+CCtjMR3dIEHxsd6bA==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@babel/code-frame": "^7.26.2",
- "@babel/generator": "^7.27.0",
- "@babel/parser": "^7.27.0",
- "@babel/template": "^7.27.0",
- "@babel/types": "^7.27.0",
- "debug": "^4.3.1",
- "globals": "^11.1.0"
- },
- "engines": {
- "node": ">=6.9.0"
- }
- },
"node_modules/@react-email/preview-server/node_modules/@next/env": {
"version": "15.5.2",
"resolved": "https://registry.npmjs.org/@next/env/-/env-15.5.2.tgz",
@@ -4861,23 +4369,6 @@
"node": ">= 10"
}
},
- "node_modules/@react-email/preview-server/node_modules/@next/swc-linux-x64-musl": {
- "version": "15.5.2",
- "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-15.5.2.tgz",
- "integrity": "sha512-/VUnh7w8RElYZ0IV83nUcP/J4KJ6LLYliiBIri3p3aW2giF+PAVgZb6mk8jbQSB3WlTai8gEmCAr7kptFa1H6g==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">= 10"
- }
- },
"node_modules/@react-email/preview-server/node_modules/@types/node": {
"version": "22.14.1",
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.14.1.tgz",
@@ -4946,16 +4437,6 @@
"node": ">= 6"
}
},
- "node_modules/@react-email/preview-server/node_modules/globals": {
- "version": "11.12.0",
- "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz",
- "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=4"
- }
- },
"node_modules/@react-email/preview-server/node_modules/jiti": {
"version": "1.21.7",
"resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.7.tgz",
@@ -5061,55 +4542,6 @@
"node": "^10 || ^12 || >=14"
}
},
- "node_modules/@react-email/preview-server/node_modules/postcss-load-config": {
- "version": "4.0.2",
- "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-4.0.2.tgz",
- "integrity": "sha512-bSVhyJGL00wMVoPUzAVAnbEoWyqRxkjv64tUl427SKnPrENtq6hJwUojroMz2VB+Q1edmi4IfrAPpami5VVgMQ==",
- "dev": true,
- "funding": [
- {
- "type": "opencollective",
- "url": "https://opencollective.com/postcss/"
- },
- {
- "type": "github",
- "url": "https://github.com/sponsors/ai"
- }
- ],
- "license": "MIT",
- "dependencies": {
- "lilconfig": "^3.0.0",
- "yaml": "^2.3.4"
- },
- "engines": {
- "node": ">= 14"
- },
- "peerDependencies": {
- "postcss": ">=8.0.9",
- "ts-node": ">=9.0.0"
- },
- "peerDependenciesMeta": {
- "postcss": {
- "optional": true
- },
- "ts-node": {
- "optional": true
- }
- }
- },
- "node_modules/@react-email/preview-server/node_modules/postcss-load-config/node_modules/lilconfig": {
- "version": "3.1.3",
- "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz",
- "integrity": "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=14"
- },
- "funding": {
- "url": "https://github.com/sponsors/antonk52"
- }
- },
"node_modules/@react-email/preview-server/node_modules/react": {
"version": "19.0.0",
"resolved": "https://registry.npmjs.org/react/-/react-19.0.0.tgz",
@@ -5202,6 +4634,55 @@
"node": ">=14.0.0"
}
},
+ "node_modules/@react-email/preview-server/node_modules/tailwindcss/node_modules/postcss-load-config": {
+ "version": "4.0.2",
+ "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-4.0.2.tgz",
+ "integrity": "sha512-bSVhyJGL00wMVoPUzAVAnbEoWyqRxkjv64tUl427SKnPrENtq6hJwUojroMz2VB+Q1edmi4IfrAPpami5VVgMQ==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/postcss/"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "lilconfig": "^3.0.0",
+ "yaml": "^2.3.4"
+ },
+ "engines": {
+ "node": ">= 14"
+ },
+ "peerDependencies": {
+ "postcss": ">=8.0.9",
+ "ts-node": ">=9.0.0"
+ },
+ "peerDependenciesMeta": {
+ "postcss": {
+ "optional": true
+ },
+ "ts-node": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@react-email/preview-server/node_modules/tailwindcss/node_modules/postcss-load-config/node_modules/lilconfig": {
+ "version": "3.1.3",
+ "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz",
+ "integrity": "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=14"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/antonk52"
+ }
+ },
"node_modules/@react-email/preview-server/node_modules/undici-types": {
"version": "6.21.0",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
@@ -5292,9 +4773,9 @@
"license": "MIT"
},
"node_modules/@rushstack/eslint-patch": {
- "version": "1.12.0",
- "resolved": "https://registry.npmjs.org/@rushstack/eslint-patch/-/eslint-patch-1.12.0.tgz",
- "integrity": "sha512-5EwMtOqvJMMa3HbmxLlF74e+3/HhwBTMcvt3nqVJgGCozO6hzIPOBlwm8mGVNR9SN2IJpxSnlxczyDjcn7qIyw==",
+ "version": "1.13.0",
+ "resolved": "https://registry.npmjs.org/@rushstack/eslint-patch/-/eslint-patch-1.13.0.tgz",
+ "integrity": "sha512-2ih5qGw5SZJ+2fLZxP6Lr6Na2NTIgPRL/7Kmyuw0uIyBQnuhQ8fi8fzUTd38eIQmqp+GYLC00cI6WgtqHxBwmw==",
"license": "MIT"
},
"node_modules/@scarf/scarf": {
@@ -5349,12 +4830,12 @@
}
},
"node_modules/@smithy/abort-controller": {
- "version": "4.2.0",
- "resolved": "https://registry.npmjs.org/@smithy/abort-controller/-/abort-controller-4.2.0.tgz",
- "integrity": "sha512-PLUYa+SUKOEZtXFURBu/CNxlsxfaFGxSBPcStL13KpVeVWIfdezWyDqkz7iDLmwnxojXD0s5KzuB5HGHvt4Aeg==",
+ "version": "4.2.2",
+ "resolved": "https://registry.npmjs.org/@smithy/abort-controller/-/abort-controller-4.2.2.tgz",
+ "integrity": "sha512-fPbcmEI+A6QiGOuumTpKSo7z+9VYr5DLN8d5/8jDJOwmt4HAKy/UGuRstCMpKbtr+FMaHH4pvFinSAbIAYCHZQ==",
"license": "Apache-2.0",
"dependencies": {
- "@smithy/types": "^4.6.0",
+ "@smithy/types": "^4.7.1",
"tslib": "^2.6.2"
},
"engines": {
@@ -5387,15 +4868,15 @@
}
},
"node_modules/@smithy/config-resolver": {
- "version": "4.3.0",
- "resolved": "https://registry.npmjs.org/@smithy/config-resolver/-/config-resolver-4.3.0.tgz",
- "integrity": "sha512-9oH+n8AVNiLPK/iK/agOsoWfrKZ3FGP3502tkksd6SRsKMYiu7AFX0YXo6YBADdsAj7C+G/aLKdsafIJHxuCkQ==",
+ "version": "4.3.2",
+ "resolved": "https://registry.npmjs.org/@smithy/config-resolver/-/config-resolver-4.3.2.tgz",
+ "integrity": "sha512-F/G+VaulIebINyfvcoXmODgIc7JU/lxWK9/iI0Divxyvd2QWB7/ZcF7JKwMssWI6/zZzlMkq/Pt6ow2AOEebPw==",
"license": "Apache-2.0",
"dependencies": {
- "@smithy/node-config-provider": "^4.3.0",
- "@smithy/types": "^4.6.0",
+ "@smithy/node-config-provider": "^4.3.2",
+ "@smithy/types": "^4.7.1",
"@smithy/util-config-provider": "^4.2.0",
- "@smithy/util-middleware": "^4.2.0",
+ "@smithy/util-middleware": "^4.2.2",
"tslib": "^2.6.2"
},
"engines": {
@@ -5403,18 +4884,18 @@
}
},
"node_modules/@smithy/core": {
- "version": "3.15.0",
- "resolved": "https://registry.npmjs.org/@smithy/core/-/core-3.15.0.tgz",
- "integrity": "sha512-VJWncXgt+ExNn0U2+Y7UywuATtRYaodGQKFo9mDyh70q+fJGedfrqi2XuKU1BhiLeXgg6RZrW7VEKfeqFhHAJA==",
+ "version": "3.16.1",
+ "resolved": "https://registry.npmjs.org/@smithy/core/-/core-3.16.1.tgz",
+ "integrity": "sha512-yRx5ag3xEQ/yGvyo80FVukS7ZkeUP49Vbzg0MjfHLkuCIgg5lFtaEJfZR178KJmjWPqLU4d0P4k7SKgF9UkOaQ==",
"license": "Apache-2.0",
"dependencies": {
- "@smithy/middleware-serde": "^4.2.0",
- "@smithy/protocol-http": "^5.3.0",
- "@smithy/types": "^4.6.0",
+ "@smithy/middleware-serde": "^4.2.2",
+ "@smithy/protocol-http": "^5.3.2",
+ "@smithy/types": "^4.7.1",
"@smithy/util-base64": "^4.3.0",
"@smithy/util-body-length-browser": "^4.2.0",
- "@smithy/util-middleware": "^4.2.0",
- "@smithy/util-stream": "^4.5.0",
+ "@smithy/util-middleware": "^4.2.2",
+ "@smithy/util-stream": "^4.5.2",
"@smithy/util-utf8": "^4.2.0",
"@smithy/uuid": "^1.1.0",
"tslib": "^2.6.2"
@@ -5424,15 +4905,15 @@
}
},
"node_modules/@smithy/credential-provider-imds": {
- "version": "4.2.0",
- "resolved": "https://registry.npmjs.org/@smithy/credential-provider-imds/-/credential-provider-imds-4.2.0.tgz",
- "integrity": "sha512-SOhFVvFH4D5HJZytb0bLKxCrSnwcqPiNlrw+S4ZXjMnsC+o9JcUQzbZOEQcA8yv9wJFNhfsUiIUKiEnYL68Big==",
+ "version": "4.2.2",
+ "resolved": "https://registry.npmjs.org/@smithy/credential-provider-imds/-/credential-provider-imds-4.2.2.tgz",
+ "integrity": "sha512-hOjFTK+4mfehDnfjNkPqHUKBKR2qmlix5gy7YzruNbTdeoBE3QkfNCPvuCK2r05VUJ02QQ9bz2G41CxhSexsMw==",
"license": "Apache-2.0",
"dependencies": {
- "@smithy/node-config-provider": "^4.3.0",
- "@smithy/property-provider": "^4.2.0",
- "@smithy/types": "^4.6.0",
- "@smithy/url-parser": "^4.2.0",
+ "@smithy/node-config-provider": "^4.3.2",
+ "@smithy/property-provider": "^4.2.2",
+ "@smithy/types": "^4.7.1",
+ "@smithy/url-parser": "^4.2.2",
"tslib": "^2.6.2"
},
"engines": {
@@ -5510,14 +4991,14 @@
}
},
"node_modules/@smithy/fetch-http-handler": {
- "version": "5.3.1",
- "resolved": "https://registry.npmjs.org/@smithy/fetch-http-handler/-/fetch-http-handler-5.3.1.tgz",
- "integrity": "sha512-3AvYYbB+Dv5EPLqnJIAgYw/9+WzeBiUYS8B+rU0pHq5NMQMvrZmevUROS4V2GAt0jEOn9viBzPLrZE+riTNd5Q==",
+ "version": "5.3.3",
+ "resolved": "https://registry.npmjs.org/@smithy/fetch-http-handler/-/fetch-http-handler-5.3.3.tgz",
+ "integrity": "sha512-cipIcM3xQ5NdIVwcRb37LaQwIxZNMEZb/ZOPmLFS9uGo9TGx2dGCyMBj9oT7ypH4TUD/kOTc/qHmwQzthrSk+g==",
"license": "Apache-2.0",
"dependencies": {
- "@smithy/protocol-http": "^5.3.0",
- "@smithy/querystring-builder": "^4.2.0",
- "@smithy/types": "^4.6.0",
+ "@smithy/protocol-http": "^5.3.2",
+ "@smithy/querystring-builder": "^4.2.2",
+ "@smithy/types": "^4.7.1",
"@smithy/util-base64": "^4.3.0",
"tslib": "^2.6.2"
},
@@ -5526,14 +5007,14 @@
}
},
"node_modules/@smithy/hash-blob-browser": {
- "version": "4.2.1",
- "resolved": "https://registry.npmjs.org/@smithy/hash-blob-browser/-/hash-blob-browser-4.2.1.tgz",
- "integrity": "sha512-Os9cg1fTXMwuqbvjemELlf+HB5oEeVyZmYsTbAtDQBmjGyibjmbeeqcaw7xOJLIHrkH/u0wAYabNcN6FRTqMRg==",
+ "version": "4.2.3",
+ "resolved": "https://registry.npmjs.org/@smithy/hash-blob-browser/-/hash-blob-browser-4.2.3.tgz",
+ "integrity": "sha512-ZYd5FYhyvD4PCnqa1vO1tzqQ1s9fWs+el8DJZUfHumvq9wl7UXoLAqof+lAqSe/mVsf2I4tclf9BbTr6NL/pjQ==",
"license": "Apache-2.0",
"dependencies": {
"@smithy/chunked-blob-reader": "^5.2.0",
"@smithy/chunked-blob-reader-native": "^4.2.1",
- "@smithy/types": "^4.6.0",
+ "@smithy/types": "^4.7.1",
"tslib": "^2.6.2"
},
"engines": {
@@ -5623,18 +5104,18 @@
}
},
"node_modules/@smithy/middleware-endpoint": {
- "version": "4.3.1",
- "resolved": "https://registry.npmjs.org/@smithy/middleware-endpoint/-/middleware-endpoint-4.3.1.tgz",
- "integrity": "sha512-JtM4SjEgImLEJVXdsbvWHYiJ9dtuKE8bqLlvkvGi96LbejDL6qnVpVxEFUximFodoQbg0Gnkyff9EKUhFhVJFw==",
+ "version": "4.3.3",
+ "resolved": "https://registry.npmjs.org/@smithy/middleware-endpoint/-/middleware-endpoint-4.3.3.tgz",
+ "integrity": "sha512-CfxQ6X9L87/3C67Po6AGWXsx8iS4w2BO8vQEZJD6hwqg2vNRC/lMa2O5wXYCG9tKotdZ0R8KG33TS7kpUnYKiw==",
"license": "Apache-2.0",
"dependencies": {
- "@smithy/core": "^3.15.0",
- "@smithy/middleware-serde": "^4.2.0",
- "@smithy/node-config-provider": "^4.3.0",
- "@smithy/shared-ini-file-loader": "^4.3.0",
- "@smithy/types": "^4.6.0",
- "@smithy/url-parser": "^4.2.0",
- "@smithy/util-middleware": "^4.2.0",
+ "@smithy/core": "^3.16.1",
+ "@smithy/middleware-serde": "^4.2.2",
+ "@smithy/node-config-provider": "^4.3.2",
+ "@smithy/shared-ini-file-loader": "^4.3.2",
+ "@smithy/types": "^4.7.1",
+ "@smithy/url-parser": "^4.2.2",
+ "@smithy/util-middleware": "^4.2.2",
"tslib": "^2.6.2"
},
"engines": {
@@ -5642,18 +5123,18 @@
}
},
"node_modules/@smithy/middleware-retry": {
- "version": "4.4.1",
- "resolved": "https://registry.npmjs.org/@smithy/middleware-retry/-/middleware-retry-4.4.1.tgz",
- "integrity": "sha512-wXxS4ex8cJJteL0PPQmWYkNi9QKDWZIpsndr0wZI2EL+pSSvA/qqxXU60gBOJoIc2YgtZSWY/PE86qhKCCKP1w==",
+ "version": "4.4.3",
+ "resolved": "https://registry.npmjs.org/@smithy/middleware-retry/-/middleware-retry-4.4.3.tgz",
+ "integrity": "sha512-EHnKGeFuzbmER4oSl/VJDxPLi+aiZUb3nk5KK8eNwHjMhI04jHlui2ZkaBzMfNmXOgymaS6zV//fyt6PSnI1ow==",
"license": "Apache-2.0",
"dependencies": {
- "@smithy/node-config-provider": "^4.3.0",
- "@smithy/protocol-http": "^5.3.0",
- "@smithy/service-error-classification": "^4.2.0",
- "@smithy/smithy-client": "^4.7.1",
- "@smithy/types": "^4.6.0",
- "@smithy/util-middleware": "^4.2.0",
- "@smithy/util-retry": "^4.2.0",
+ "@smithy/node-config-provider": "^4.3.2",
+ "@smithy/protocol-http": "^5.3.2",
+ "@smithy/service-error-classification": "^4.2.2",
+ "@smithy/smithy-client": "^4.8.1",
+ "@smithy/types": "^4.7.1",
+ "@smithy/util-middleware": "^4.2.2",
+ "@smithy/util-retry": "^4.2.2",
"@smithy/uuid": "^1.1.0",
"tslib": "^2.6.2"
},
@@ -5662,13 +5143,13 @@
}
},
"node_modules/@smithy/middleware-serde": {
- "version": "4.2.0",
- "resolved": "https://registry.npmjs.org/@smithy/middleware-serde/-/middleware-serde-4.2.0.tgz",
- "integrity": "sha512-rpTQ7D65/EAbC6VydXlxjvbifTf4IH+sADKg6JmAvhkflJO2NvDeyU9qsWUNBelJiQFcXKejUHWRSdmpJmEmiw==",
+ "version": "4.2.2",
+ "resolved": "https://registry.npmjs.org/@smithy/middleware-serde/-/middleware-serde-4.2.2.tgz",
+ "integrity": "sha512-tDMPMBCsA1GBxanShhPvQYwdiau3NmctUp+eELMhUTDua+EUrugXlaKCnTMMoEB5mbHFebdv81uJPkVP02oihA==",
"license": "Apache-2.0",
"dependencies": {
- "@smithy/protocol-http": "^5.3.0",
- "@smithy/types": "^4.6.0",
+ "@smithy/protocol-http": "^5.3.2",
+ "@smithy/types": "^4.7.1",
"tslib": "^2.6.2"
},
"engines": {
@@ -5676,12 +5157,12 @@
}
},
"node_modules/@smithy/middleware-stack": {
- "version": "4.2.0",
- "resolved": "https://registry.npmjs.org/@smithy/middleware-stack/-/middleware-stack-4.2.0.tgz",
- "integrity": "sha512-G5CJ//eqRd9OARrQu9MK1H8fNm2sMtqFh6j8/rPozhEL+Dokpvi1Og+aCixTuwDAGZUkJPk6hJT5jchbk/WCyg==",
+ "version": "4.2.2",
+ "resolved": "https://registry.npmjs.org/@smithy/middleware-stack/-/middleware-stack-4.2.2.tgz",
+ "integrity": "sha512-7rgzDyLOQouh1bC6gOXnCGSX2dqvbOclgClsFkj735xQM2CHV63Ams8odNZGJgcqnBsEz44V/pDGHU6ALEUD+w==",
"license": "Apache-2.0",
"dependencies": {
- "@smithy/types": "^4.6.0",
+ "@smithy/types": "^4.7.1",
"tslib": "^2.6.2"
},
"engines": {
@@ -5689,14 +5170,14 @@
}
},
"node_modules/@smithy/node-config-provider": {
- "version": "4.3.0",
- "resolved": "https://registry.npmjs.org/@smithy/node-config-provider/-/node-config-provider-4.3.0.tgz",
- "integrity": "sha512-5QgHNuWdT9j9GwMPPJCKxy2KDxZ3E5l4M3/5TatSZrqYVoEiqQrDfAq8I6KWZw7RZOHtVtCzEPdYz7rHZixwcA==",
+ "version": "4.3.2",
+ "resolved": "https://registry.npmjs.org/@smithy/node-config-provider/-/node-config-provider-4.3.2.tgz",
+ "integrity": "sha512-u38G0Audi2ORsL0QnzhopZ3yweMblQf8CZNbzUJ3wfTtZ7OiOwOzee0Nge/3dKeG/8lx0kt8K0kqDi6sYu0oKQ==",
"license": "Apache-2.0",
"dependencies": {
- "@smithy/property-provider": "^4.2.0",
- "@smithy/shared-ini-file-loader": "^4.3.0",
- "@smithy/types": "^4.6.0",
+ "@smithy/property-provider": "^4.2.2",
+ "@smithy/shared-ini-file-loader": "^4.3.2",
+ "@smithy/types": "^4.7.1",
"tslib": "^2.6.2"
},
"engines": {
@@ -5704,15 +5185,15 @@
}
},
"node_modules/@smithy/node-http-handler": {
- "version": "4.3.0",
- "resolved": "https://registry.npmjs.org/@smithy/node-http-handler/-/node-http-handler-4.3.0.tgz",
- "integrity": "sha512-RHZ/uWCmSNZ8cneoWEVsVwMZBKy/8123hEpm57vgGXA3Irf/Ja4v9TVshHK2ML5/IqzAZn0WhINHOP9xl+Qy6Q==",
+ "version": "4.4.1",
+ "resolved": "https://registry.npmjs.org/@smithy/node-http-handler/-/node-http-handler-4.4.1.tgz",
+ "integrity": "sha512-9gKJoL45MNyOCGTG082nmx0A6KrbLVQ+5QSSKyzRi0AzL0R81u3wC1+nPvKXgTaBdAKM73fFPdCBHpmtipQwdQ==",
"license": "Apache-2.0",
"dependencies": {
- "@smithy/abort-controller": "^4.2.0",
- "@smithy/protocol-http": "^5.3.0",
- "@smithy/querystring-builder": "^4.2.0",
- "@smithy/types": "^4.6.0",
+ "@smithy/abort-controller": "^4.2.2",
+ "@smithy/protocol-http": "^5.3.2",
+ "@smithy/querystring-builder": "^4.2.2",
+ "@smithy/types": "^4.7.1",
"tslib": "^2.6.2"
},
"engines": {
@@ -5720,12 +5201,12 @@
}
},
"node_modules/@smithy/property-provider": {
- "version": "4.2.0",
- "resolved": "https://registry.npmjs.org/@smithy/property-provider/-/property-provider-4.2.0.tgz",
- "integrity": "sha512-rV6wFre0BU6n/tx2Ztn5LdvEdNZ2FasQbPQmDOPfV9QQyDmsCkOAB0osQjotRCQg+nSKFmINhyda0D3AnjSBJw==",
+ "version": "4.2.2",
+ "resolved": "https://registry.npmjs.org/@smithy/property-provider/-/property-provider-4.2.2.tgz",
+ "integrity": "sha512-MW7MfI+qYe/Ue5RH0uEztEKB+vBlOMM+1Dz68qzTsY8fC9kanXMFPEVdiq35JTGKWt5wZAjU1R0uXYEjK2MM1g==",
"license": "Apache-2.0",
"dependencies": {
- "@smithy/types": "^4.6.0",
+ "@smithy/types": "^4.7.1",
"tslib": "^2.6.2"
},
"engines": {
@@ -5733,12 +5214,12 @@
}
},
"node_modules/@smithy/protocol-http": {
- "version": "5.3.0",
- "resolved": "https://registry.npmjs.org/@smithy/protocol-http/-/protocol-http-5.3.0.tgz",
- "integrity": "sha512-6POSYlmDnsLKb7r1D3SVm7RaYW6H1vcNcTWGWrF7s9+2noNYvUsm7E4tz5ZQ9HXPmKn6Hb67pBDRIjrT4w/d7Q==",
+ "version": "5.3.2",
+ "resolved": "https://registry.npmjs.org/@smithy/protocol-http/-/protocol-http-5.3.2.tgz",
+ "integrity": "sha512-nkKOI8xEkBXUmdxsFExomOb+wkU+Xgn0Fq2LMC7YIX5r4YPUg7PLayV/s/u3AtbyjWYlrvN7nAiDTLlqSdUjHw==",
"license": "Apache-2.0",
"dependencies": {
- "@smithy/types": "^4.6.0",
+ "@smithy/types": "^4.7.1",
"tslib": "^2.6.2"
},
"engines": {
@@ -5746,12 +5227,12 @@
}
},
"node_modules/@smithy/querystring-builder": {
- "version": "4.2.0",
- "resolved": "https://registry.npmjs.org/@smithy/querystring-builder/-/querystring-builder-4.2.0.tgz",
- "integrity": "sha512-Q4oFD0ZmI8yJkiPPeGUITZj++4HHYCW3pYBYfIobUCkYpI6mbkzmG1MAQQ3lJYYWj3iNqfzOenUZu+jqdPQ16A==",
+ "version": "4.2.2",
+ "resolved": "https://registry.npmjs.org/@smithy/querystring-builder/-/querystring-builder-4.2.2.tgz",
+ "integrity": "sha512-YgXvq89o+R/8zIoeuXYv8Ysrbwgjx+iVYu9QbseqZjMDAhIg/FRt7jis0KASYFtd/Cnsnz4/nYTJXkJDWe8wHg==",
"license": "Apache-2.0",
"dependencies": {
- "@smithy/types": "^4.6.0",
+ "@smithy/types": "^4.7.1",
"@smithy/util-uri-escape": "^4.2.0",
"tslib": "^2.6.2"
},
@@ -5760,12 +5241,12 @@
}
},
"node_modules/@smithy/querystring-parser": {
- "version": "4.2.0",
- "resolved": "https://registry.npmjs.org/@smithy/querystring-parser/-/querystring-parser-4.2.0.tgz",
- "integrity": "sha512-BjATSNNyvVbQxOOlKse0b0pSezTWGMvA87SvoFoFlkRsKXVsN3bEtjCxvsNXJXfnAzlWFPaT9DmhWy1vn0sNEA==",
+ "version": "4.2.2",
+ "resolved": "https://registry.npmjs.org/@smithy/querystring-parser/-/querystring-parser-4.2.2.tgz",
+ "integrity": "sha512-DczOD2yJy3NXcv1JvhjFC7bIb/tay6nnIRD/qrzBaju5lrkVBOwCT3Ps37tra20wy8PicZpworStK7ZcI9pCRQ==",
"license": "Apache-2.0",
"dependencies": {
- "@smithy/types": "^4.6.0",
+ "@smithy/types": "^4.7.1",
"tslib": "^2.6.2"
},
"engines": {
@@ -5773,24 +5254,24 @@
}
},
"node_modules/@smithy/service-error-classification": {
- "version": "4.2.0",
- "resolved": "https://registry.npmjs.org/@smithy/service-error-classification/-/service-error-classification-4.2.0.tgz",
- "integrity": "sha512-Ylv1ttUeKatpR0wEOMnHf1hXMktPUMObDClSWl2TpCVT4DwtJhCeighLzSLbgH3jr5pBNM0LDXT5yYxUvZ9WpA==",
+ "version": "4.2.2",
+ "resolved": "https://registry.npmjs.org/@smithy/service-error-classification/-/service-error-classification-4.2.2.tgz",
+ "integrity": "sha512-1X17cMLwe/vb4RpZbQVpJ1xQQ7fhQKggMdt3qjdV3+6QNllzvUXyS3WFnyaFWLyaGqfYHKkNONbO1fBCMQyZtQ==",
"license": "Apache-2.0",
"dependencies": {
- "@smithy/types": "^4.6.0"
+ "@smithy/types": "^4.7.1"
},
"engines": {
"node": ">=18.0.0"
}
},
"node_modules/@smithy/shared-ini-file-loader": {
- "version": "4.3.0",
- "resolved": "https://registry.npmjs.org/@smithy/shared-ini-file-loader/-/shared-ini-file-loader-4.3.0.tgz",
- "integrity": "sha512-VCUPPtNs+rKWlqqntX0CbVvWyjhmX30JCtzO+s5dlzzxrvSfRh5SY0yxnkirvc1c80vdKQttahL71a9EsdolSQ==",
+ "version": "4.3.2",
+ "resolved": "https://registry.npmjs.org/@smithy/shared-ini-file-loader/-/shared-ini-file-loader-4.3.2.tgz",
+ "integrity": "sha512-AWnLgSmOTdDXM8aZCN4Im0X07M3GGffeL9vGfea4mdKZD0cPT9yLF9SsRbEa00tHLI+KfubDrmjpaKT2pM4GdQ==",
"license": "Apache-2.0",
"dependencies": {
- "@smithy/types": "^4.6.0",
+ "@smithy/types": "^4.7.1",
"tslib": "^2.6.2"
},
"engines": {
@@ -5817,17 +5298,17 @@
}
},
"node_modules/@smithy/smithy-client": {
- "version": "4.7.1",
- "resolved": "https://registry.npmjs.org/@smithy/smithy-client/-/smithy-client-4.7.1.tgz",
- "integrity": "sha512-WXVbiyNf/WOS/RHUoFMkJ6leEVpln5ojCjNBnzoZeMsnCg3A0BRhLK3WYc4V7PmYcYPZh9IYzzAg9XcNSzYxYQ==",
+ "version": "4.8.1",
+ "resolved": "https://registry.npmjs.org/@smithy/smithy-client/-/smithy-client-4.8.1.tgz",
+ "integrity": "sha512-N5wK57pVThzLVK5NgmHxocTy5auqGDGQ+JsL5RjCTriPt8JLYgXT0Awa915zCpzc9hXHDOKqDX5g9BFdwkSfUA==",
"license": "Apache-2.0",
"dependencies": {
- "@smithy/core": "^3.15.0",
- "@smithy/middleware-endpoint": "^4.3.1",
- "@smithy/middleware-stack": "^4.2.0",
- "@smithy/protocol-http": "^5.3.0",
- "@smithy/types": "^4.6.0",
- "@smithy/util-stream": "^4.5.0",
+ "@smithy/core": "^3.16.1",
+ "@smithy/middleware-endpoint": "^4.3.3",
+ "@smithy/middleware-stack": "^4.2.2",
+ "@smithy/protocol-http": "^5.3.2",
+ "@smithy/types": "^4.7.1",
+ "@smithy/util-stream": "^4.5.2",
"tslib": "^2.6.2"
},
"engines": {
@@ -5835,9 +5316,9 @@
}
},
"node_modules/@smithy/types": {
- "version": "4.6.0",
- "resolved": "https://registry.npmjs.org/@smithy/types/-/types-4.6.0.tgz",
- "integrity": "sha512-4lI9C8NzRPOv66FaY1LL1O/0v0aLVrq/mXP/keUa9mJOApEeae43LsLd2kZRUJw91gxOQfLIrV3OvqPgWz1YsA==",
+ "version": "4.7.1",
+ "resolved": "https://registry.npmjs.org/@smithy/types/-/types-4.7.1.tgz",
+ "integrity": "sha512-WwP7vzoDyzvIFLzF5UhLQ6AsEx/PvSObzlNtJNW3lLy+BaSvTqCU628QKVvcJI/dydlAS1mSHQP7anKcxDcOxA==",
"license": "Apache-2.0",
"dependencies": {
"tslib": "^2.6.2"
@@ -5847,13 +5328,13 @@
}
},
"node_modules/@smithy/url-parser": {
- "version": "4.2.0",
- "resolved": "https://registry.npmjs.org/@smithy/url-parser/-/url-parser-4.2.0.tgz",
- "integrity": "sha512-AlBmD6Idav2ugmoAL6UtR6ItS7jU5h5RNqLMZC7QrLCoITA9NzIN3nx9GWi8g4z1pfWh2r9r96SX/jHiNwPJ9A==",
+ "version": "4.2.2",
+ "resolved": "https://registry.npmjs.org/@smithy/url-parser/-/url-parser-4.2.2.tgz",
+ "integrity": "sha512-s2EYKukaswzjiHJCss6asB1F4zjRc0E/MFyceAKzb3+wqKA2Z/+Gfhb5FP8xVVRHBAvBkregaQAydifgbnUlCw==",
"license": "Apache-2.0",
"dependencies": {
- "@smithy/querystring-parser": "^4.2.0",
- "@smithy/types": "^4.6.0",
+ "@smithy/querystring-parser": "^4.2.2",
+ "@smithy/types": "^4.7.1",
"tslib": "^2.6.2"
},
"engines": {
@@ -5924,14 +5405,14 @@
}
},
"node_modules/@smithy/util-defaults-mode-browser": {
- "version": "4.3.0",
- "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-browser/-/util-defaults-mode-browser-4.3.0.tgz",
- "integrity": "sha512-H4MAj8j8Yp19Mr7vVtGgi7noJjvjJbsKQJkvNnLlrIFduRFT5jq5Eri1k838YW7rN2g5FTnXpz5ktKVr1KVgPQ==",
+ "version": "4.3.2",
+ "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-browser/-/util-defaults-mode-browser-4.3.2.tgz",
+ "integrity": "sha512-6JvKHZ5GORYkEZ2+yJKEHp6dQQKng+P/Mu3g3CDy0fRLQgXEO8be+FLrBGGb4kB9lCW6wcQDkN7kRiGkkVAXgg==",
"license": "Apache-2.0",
"dependencies": {
- "@smithy/property-provider": "^4.2.0",
- "@smithy/smithy-client": "^4.7.1",
- "@smithy/types": "^4.6.0",
+ "@smithy/property-provider": "^4.2.2",
+ "@smithy/smithy-client": "^4.8.1",
+ "@smithy/types": "^4.7.1",
"tslib": "^2.6.2"
},
"engines": {
@@ -5939,17 +5420,17 @@
}
},
"node_modules/@smithy/util-defaults-mode-node": {
- "version": "4.2.1",
- "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-node/-/util-defaults-mode-node-4.2.1.tgz",
- "integrity": "sha512-PuDcgx7/qKEMzV1QFHJ7E4/MMeEjaA7+zS5UNcHCLPvvn59AeZQ0DSDGMpqC2xecfa/1cNGm4l8Ec/VxCuY7Ug==",
+ "version": "4.2.3",
+ "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-node/-/util-defaults-mode-node-4.2.3.tgz",
+ "integrity": "sha512-bkTGuMmKvghfCh9NayADrQcjngoF8P+XTgID5r3rm+8LphFiuM6ERqpBS95YyVaLjDetnKus9zK/bGlkQOOtNQ==",
"license": "Apache-2.0",
"dependencies": {
- "@smithy/config-resolver": "^4.3.0",
- "@smithy/credential-provider-imds": "^4.2.0",
- "@smithy/node-config-provider": "^4.3.0",
- "@smithy/property-provider": "^4.2.0",
- "@smithy/smithy-client": "^4.7.1",
- "@smithy/types": "^4.6.0",
+ "@smithy/config-resolver": "^4.3.2",
+ "@smithy/credential-provider-imds": "^4.2.2",
+ "@smithy/node-config-provider": "^4.3.2",
+ "@smithy/property-provider": "^4.2.2",
+ "@smithy/smithy-client": "^4.8.1",
+ "@smithy/types": "^4.7.1",
"tslib": "^2.6.2"
},
"engines": {
@@ -5983,12 +5464,12 @@
}
},
"node_modules/@smithy/util-middleware": {
- "version": "4.2.0",
- "resolved": "https://registry.npmjs.org/@smithy/util-middleware/-/util-middleware-4.2.0.tgz",
- "integrity": "sha512-u9OOfDa43MjagtJZ8AapJcmimP+K2Z7szXn8xbty4aza+7P1wjFmy2ewjSbhEiYQoW1unTlOAIV165weYAaowA==",
+ "version": "4.2.2",
+ "resolved": "https://registry.npmjs.org/@smithy/util-middleware/-/util-middleware-4.2.2.tgz",
+ "integrity": "sha512-wL9tZwWKy0x0qf6ffN7tX5CT03hb1e7XpjdepaKfKcPcyn5+jHAWPqivhF1Sw/T5DYi9wGcxsX8Lu07MOp2Puw==",
"license": "Apache-2.0",
"dependencies": {
- "@smithy/types": "^4.6.0",
+ "@smithy/types": "^4.7.1",
"tslib": "^2.6.2"
},
"engines": {
@@ -5996,13 +5477,13 @@
}
},
"node_modules/@smithy/util-retry": {
- "version": "4.2.0",
- "resolved": "https://registry.npmjs.org/@smithy/util-retry/-/util-retry-4.2.0.tgz",
- "integrity": "sha512-BWSiuGbwRnEE2SFfaAZEX0TqaxtvtSYPM/J73PFVm+A29Fg1HTPiYFb8TmX1DXp4hgcdyJcNQmprfd5foeORsg==",
+ "version": "4.2.2",
+ "resolved": "https://registry.npmjs.org/@smithy/util-retry/-/util-retry-4.2.2.tgz",
+ "integrity": "sha512-TlbnWAOoCuG2PgY0Hi3BGU1w2IXs3xDsD4E8WDfKRZUn2qx3wRA9mbYnmpWHPswTJCz2L+ebh+9OvD42sV4mNw==",
"license": "Apache-2.0",
"dependencies": {
- "@smithy/service-error-classification": "^4.2.0",
- "@smithy/types": "^4.6.0",
+ "@smithy/service-error-classification": "^4.2.2",
+ "@smithy/types": "^4.7.1",
"tslib": "^2.6.2"
},
"engines": {
@@ -6010,14 +5491,14 @@
}
},
"node_modules/@smithy/util-stream": {
- "version": "4.5.0",
- "resolved": "https://registry.npmjs.org/@smithy/util-stream/-/util-stream-4.5.0.tgz",
- "integrity": "sha512-0TD5M5HCGu5diEvZ/O/WquSjhJPasqv7trjoqHyWjNh/FBeBl7a0ztl9uFMOsauYtRfd8jvpzIAQhDHbx+nvZw==",
+ "version": "4.5.2",
+ "resolved": "https://registry.npmjs.org/@smithy/util-stream/-/util-stream-4.5.2.tgz",
+ "integrity": "sha512-RWYVuQVKtNbr7E0IxV8XHDId714yHPTxU6dHScd6wSMWAXboErzTG7+xqcL+K3r0Xg0cZSlfuNhl1J0rzMLSSw==",
"license": "Apache-2.0",
"dependencies": {
- "@smithy/fetch-http-handler": "^5.3.1",
- "@smithy/node-http-handler": "^4.3.0",
- "@smithy/types": "^4.6.0",
+ "@smithy/fetch-http-handler": "^5.3.3",
+ "@smithy/node-http-handler": "^4.4.1",
+ "@smithy/types": "^4.7.1",
"@smithy/util-base64": "^4.3.0",
"@smithy/util-buffer-from": "^4.2.0",
"@smithy/util-hex-encoding": "^4.2.0",
@@ -6185,23 +5666,6 @@
"node": ">= 10"
}
},
- "node_modules/@tailwindcss/oxide-linux-x64-musl": {
- "version": "4.1.14",
- "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.1.14.tgz",
- "integrity": "sha512-TNGeLiN1XS66kQhxHG/7wMeQDOoL0S33x9BgmydbrWAb9Qw0KYdd8o1ifx4HOGDWhVmJ+Ul+JQ7lyknQFilO3Q==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">= 10"
- }
- },
"node_modules/@tailwindcss/postcss": {
"version": "4.1.14",
"resolved": "https://registry.npmjs.org/@tailwindcss/postcss/-/postcss-4.1.14.tgz",
@@ -6348,9 +5812,9 @@
}
},
"node_modules/@types/express-serve-static-core": {
- "version": "5.0.7",
- "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-5.0.7.tgz",
- "integrity": "sha512-R+33OsgWw7rOhD1emjU7dzCDHucJrgJXMA5PYCzJxVil0dsyx5iBEPHqpPfiKNJQb7lZ1vxwoLR4Z87bBUpeGQ==",
+ "version": "5.1.0",
+ "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-5.1.0.tgz",
+ "integrity": "sha512-jnHMsrd0Mwa9Cf4IdOzbz543y4XJepXrbia2T4b6+spXC2We3t1y6K44D3mR8XMFSXMCf3/l7rCgddfx7UNVBA==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -6517,6 +5981,28 @@
"license": "MIT"
},
"node_modules/@types/send": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/@types/send/-/send-1.2.0.tgz",
+ "integrity": "sha512-zBF6vZJn1IaMpg3xUF25VK3gd3l8zwE0ZLRX7dsQyQi+jp4E8mMDJNGDYnYse+bQhYwWERTxVwHpi3dMOq7RKQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/node": "*"
+ }
+ },
+ "node_modules/@types/serve-static": {
+ "version": "1.15.9",
+ "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.9.tgz",
+ "integrity": "sha512-dOTIuqpWLyl3BBXU3maNQsS4A3zuuoYRNIvYSxxhebPfXg2mzWQEPne/nlJ37yOse6uGgR386uTpdsx4D0QZWA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/http-errors": "*",
+ "@types/node": "*",
+ "@types/send": "<1"
+ }
+ },
+ "node_modules/@types/serve-static/node_modules/@types/send": {
"version": "0.17.5",
"resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.5.tgz",
"integrity": "sha512-z6F2D3cOStZvuk2SaP6YrwkNO65iTZcwA2ZkSABegdkAh/lf+Aa/YQndZVfmEXT5vgAp6zv06VQ3ejSVjAny4w==",
@@ -6527,18 +6013,6 @@
"@types/node": "*"
}
},
- "node_modules/@types/serve-static": {
- "version": "1.15.8",
- "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.8.tgz",
- "integrity": "sha512-roei0UY3LhpOJvjbIP6ZZFngyLKl5dskOtDhxY5THRSpO+ZI+nzJ+m5yUMzGrp89YRa7lvknKkMYjqQFGwA7Sg==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@types/http-errors": "*",
- "@types/node": "*",
- "@types/send": "*"
- }
- },
"node_modules/@types/swagger-ui-express": {
"version": "4.1.8",
"resolved": "https://registry.npmjs.org/@types/swagger-ui-express/-/swagger-ui-express-4.1.8.tgz",
@@ -6881,19 +6355,6 @@
"linux"
]
},
- "node_modules/@unrs/resolver-binding-linux-x64-musl": {
- "version": "1.11.1",
- "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-x64-musl/-/resolver-binding-linux-x64-musl-1.11.1.tgz",
- "integrity": "sha512-rV0YSoyhK2nZ4vEswT/QwqzqQXw5I6CjoaYMOX0TqBlWhojUf8P94mvI7nuJTeaCkkds3QE4+zS8Ko+GdXuZtA==",
- "cpu": [
- "x64"
- ],
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ]
- },
"node_modules/@webassemblyjs/ast": {
"version": "1.14.1",
"resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.14.1.tgz",
@@ -7477,15 +6938,6 @@
"node": ">= 0.4"
}
},
- "node_modules/async-generator-function": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/async-generator-function/-/async-generator-function-1.0.0.tgz",
- "integrity": "sha512-+NAXNqgCrB95ya4Sr66i1CL2hqLVckAk7xwRYWdcm39/ELQ6YNn1aw5r0bdQtqNZgQpEWzc5yc/igXc7aL5SLA==",
- "license": "MIT",
- "engines": {
- "node": ">= 0.4"
- }
- },
"node_modules/asynckit": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
@@ -7611,9 +7063,9 @@
}
},
"node_modules/baseline-browser-mapping": {
- "version": "2.8.9",
- "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.8.9.tgz",
- "integrity": "sha512-hY/u2lxLrbecMEWSB0IpGzGyDyeoMFQhCvZd2jGFSE5I17Fh01sYUBPCJtkWERw7zrac9+cIghxm/ytJa2X8iA==",
+ "version": "2.8.13",
+ "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.8.13.tgz",
+ "integrity": "sha512-7s16KR8io8nIBWQyCYhmFhd+ebIzb9VKTzki+wOJXHTxTnV6+mFGH3+Jwn1zoKaY9/H9T/0BcKCZnzXljPnpSQ==",
"dev": true,
"license": "Apache-2.0",
"bin": {
@@ -7720,9 +7172,9 @@
}
},
"node_modules/browserslist": {
- "version": "4.26.2",
- "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.26.2.tgz",
- "integrity": "sha512-ECFzp6uFOSB+dcZ5BK/IBaGWssbSYBHvuMeMt3MMFyhI0Z8SqGgEkBLARgpRH3hutIgPVsALcMwbDrJqPxQ65A==",
+ "version": "4.26.3",
+ "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.26.3.tgz",
+ "integrity": "sha512-lAUU+02RFBuCKQPj/P6NgjlbCnLBMp4UtgTx7vNHd3XSIJF87s9a5rA3aH2yw3GS9DqZAUbOtZdCCiZeVRqt0w==",
"dev": true,
"funding": [
{
@@ -7740,9 +7192,9 @@
],
"license": "MIT",
"dependencies": {
- "baseline-browser-mapping": "^2.8.3",
- "caniuse-lite": "^1.0.30001741",
- "electron-to-chromium": "^1.5.218",
+ "baseline-browser-mapping": "^2.8.9",
+ "caniuse-lite": "^1.0.30001746",
+ "electron-to-chromium": "^1.5.227",
"node-releases": "^2.0.21",
"update-browserslist-db": "^1.1.3"
},
@@ -7866,9 +7318,9 @@
}
},
"node_modules/caniuse-lite": {
- "version": "1.0.30001745",
- "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001745.tgz",
- "integrity": "sha512-ywt6i8FzvdgrrrGbr1jZVObnVv6adj+0if2/omv9cmR2oiZs30zL4DIyaptKcbOrBdOIc74QTMoJvSE2QHh5UQ==",
+ "version": "1.0.30001748",
+ "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001748.tgz",
+ "integrity": "sha512-5P5UgAr0+aBmNiplks08JLw+AW/XG/SurlgZLgB1dDLfAw7EfRGxIwzPHxdSCGY/BTKDqIVyJL87cCN6s0ZR0w==",
"funding": [
{
"type": "opencollective",
@@ -8636,9 +8088,9 @@
}
},
"node_modules/detect-libc": {
- "version": "2.1.1",
- "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.1.tgz",
- "integrity": "sha512-ecqj/sy1jcK1uWrwpR67UhYrIFQ+5WlGxth34WquCbamhFA6hkkwiu37o6J5xCHdo1oixJRfVRw+ywV+Hq/0Aw==",
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz",
+ "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==",
"license": "Apache-2.0",
"engines": {
"node": ">=8"
@@ -8952,9 +8404,9 @@
"license": "MIT"
},
"node_modules/electron-to-chromium": {
- "version": "1.5.227",
- "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.227.tgz",
- "integrity": "sha512-ITxuoPfJu3lsNWUi2lBM2PaBPYgH3uqmxut5vmBxgYvyI4AlJ6P3Cai1O76mOrkJCBzq0IxWg/NtqOrpu/0gKA==",
+ "version": "1.5.233",
+ "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.233.tgz",
+ "integrity": "sha512-iUdTQSf7EFXsDdQsp8MwJz5SVk4APEFqXU/S47OtQ0YLqacSwPXdZ5vRlMX3neb07Cy2vgioNuRnWUXFwuslkg==",
"dev": true,
"license": "ISC"
},
@@ -10461,9 +9913,9 @@
}
},
"node_modules/generator-function": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/generator-function/-/generator-function-2.0.0.tgz",
- "integrity": "sha512-xPypGGincdfyl/AiSGa7GjXLkvld9V7GjZlowup9SHIJnQnHLFiLODCd/DqKOp0PBagbHJ68r1KJI9Mut7m4sA==",
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/generator-function/-/generator-function-2.0.1.tgz",
+ "integrity": "sha512-SFdFmIJi+ybC0vjlHN0ZGVGHc3lgE0DxPAT0djjVg+kjOnSqclqmj0KQ7ykTOLP6YxoqOvuAODGdcHJn+43q3g==",
"license": "MIT",
"engines": {
"node": ">= 0.4"
@@ -10501,19 +9953,16 @@
}
},
"node_modules/get-intrinsic": {
- "version": "1.3.1",
- "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.1.tgz",
- "integrity": "sha512-fk1ZVEeOX9hVZ6QzoBNEC55+Ucqg4sTVwrVuigZhuRPESVFpMyXnd3sbXvPOwp7Y9riVyANiqhEuRF0G1aVSeQ==",
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
+ "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
"license": "MIT",
"dependencies": {
- "async-function": "^1.0.0",
- "async-generator-function": "^1.0.0",
"call-bind-apply-helpers": "^1.0.2",
"es-define-property": "^1.0.1",
"es-errors": "^1.3.0",
"es-object-atoms": "^1.1.1",
"function-bind": "^1.1.2",
- "generator-function": "^2.0.0",
"get-proto": "^1.0.1",
"gopd": "^1.2.0",
"has-symbols": "^1.1.0",
@@ -10580,9 +10029,9 @@
}
},
"node_modules/get-tsconfig": {
- "version": "4.10.1",
- "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.10.1.tgz",
- "integrity": "sha512-auHyJ4AgMz7vgS8Hp3N6HXSmlMdUyhSUrfBF16w153rxtLIEOE+HGqaBppczZvnHLqQJfiHotCYpNhl0lUROFQ==",
+ "version": "4.11.0",
+ "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.11.0.tgz",
+ "integrity": "sha512-sNsqf7XKQ38IawiVGPOoAlqZo1DMrO7TU+ZcZwi7yLl7/7S0JwmoBMKz/IkUPhSoXM0Ng3vT0yB1iCe5XavDeQ==",
"license": "MIT",
"dependencies": {
"resolve-pkg-maps": "^1.0.0"
@@ -10655,15 +10104,13 @@
}
},
"node_modules/globals": {
- "version": "14.0.0",
- "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz",
- "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==",
+ "version": "11.12.0",
+ "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz",
+ "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==",
+ "dev": true,
"license": "MIT",
"engines": {
- "node": ">=18"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
+ "node": ">=4"
}
},
"node_modules/globalthis": {
@@ -11251,13 +10698,14 @@
}
},
"node_modules/is-generator-function": {
- "version": "1.1.0",
- "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.1.0.tgz",
- "integrity": "sha512-nPUB5km40q9e8UfN/Zc24eLlzdSf9OfKByBw9CIdw4H1giPMeA0OIJvbchsCu4npfI2QcMVBsGEBHKZ7wLTWmQ==",
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.1.2.tgz",
+ "integrity": "sha512-upqt1SkGkODW9tsGNG5mtXTXtECizwtS2kA161M+gJPc1xdb/Ax629af6YrTwcOeQHbewrPNlE5Dx7kzvXTizA==",
"license": "MIT",
"dependencies": {
- "call-bound": "^1.0.3",
- "get-proto": "^1.0.0",
+ "call-bound": "^1.0.4",
+ "generator-function": "^2.0.0",
+ "get-proto": "^1.0.1",
"has-tostringtag": "^1.0.2",
"safe-regex-test": "^1.1.0"
},
@@ -11586,9 +11034,9 @@
}
},
"node_modules/jiti": {
- "version": "2.6.0",
- "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.6.0.tgz",
- "integrity": "sha512-VXe6RjJkBPj0ohtqaO8vSWP3ZhAKo66fKrFNCll4BTcwljPLz03pCbaNKfzGP5MbrCYcbJ7v0nOYYwUzTEIdXQ==",
+ "version": "2.6.1",
+ "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.6.1.tgz",
+ "integrity": "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==",
"devOptional": true,
"license": "MIT",
"bin": {
@@ -11858,27 +11306,6 @@
"url": "https://opencollective.com/parcel"
}
},
- "node_modules/lightningcss-linux-x64-musl": {
- "version": "1.30.1",
- "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.30.1.tgz",
- "integrity": "sha512-rRomAK7eIkL+tHY0YPxbc5Dra2gXlI63HL+v1Pdi1a3sC+tJTcFrHX+E86sulgAXeI7rSzDYhPSeHHjqFhqfeQ==",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "MPL-2.0",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">= 12.0.0"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/parcel"
- }
- },
"node_modules/lilconfig": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-2.1.0.tgz",
@@ -12383,9 +11810,9 @@
"license": "MIT"
},
"node_modules/napi-postinstall": {
- "version": "0.3.3",
- "resolved": "https://registry.npmjs.org/napi-postinstall/-/napi-postinstall-0.3.3.tgz",
- "integrity": "sha512-uTp172LLXSxuSYHv/kou+f6KW3SMppU9ivthaVTXian9sOt3XM/zHYHpRZiLgQoxeWfYUnslNWQHF1+G71xcow==",
+ "version": "0.3.4",
+ "resolved": "https://registry.npmjs.org/napi-postinstall/-/napi-postinstall-0.3.4.tgz",
+ "integrity": "sha512-PHI5f1O0EP5xJ9gQmFGMS6IZcrVvTjpXjz7Na41gTE7eE2hK11lg04CECCYEEjdc17EV4DO+fkGEtt7TpTaTiQ==",
"license": "MIT",
"bin": {
"napi-postinstall": "lib/cli.js"
@@ -12537,9 +11964,9 @@
}
},
"node_modules/node-abi": {
- "version": "3.77.0",
- "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.77.0.tgz",
- "integrity": "sha512-DSmt0OEcLoK4i3NuscSbGjOf3bqiDEutejqENSplMSFA/gmB8mkED9G4pKWnPl7MDU4rSHebKPHeitpDfyH0cQ==",
+ "version": "3.78.0",
+ "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.78.0.tgz",
+ "integrity": "sha512-E2wEyrgX/CqvicaQYU3Ze1PFGjc4QYPGsjUrlYkqAE0WjHEZwgOsGMPMzkMse4LjJbDmaEuDX3CM036j5K2DSQ==",
"license": "MIT",
"dependencies": {
"semver": "^7.3.5"
@@ -12610,9 +12037,9 @@
}
},
"node_modules/node-releases": {
- "version": "2.0.21",
- "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.21.tgz",
- "integrity": "sha512-5b0pgg78U3hwXkCM8Z9b2FJdPZlr9Psr9V2gQPESdGHqbntyFJKFW4r5TeWGFzafGY3hzs1JC62VEQMbl1JFkw==",
+ "version": "2.0.23",
+ "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.23.tgz",
+ "integrity": "sha512-cCmFDMSm26S6tQSDpBCg/NR8NENrVPhAJSf+XbxBG4rPFaaonlEoE9wHQmun+cls499TQGSb7ZyPBRlzgKfpeg==",
"dev": true,
"license": "MIT"
},
@@ -15413,22 +14840,6 @@
"node": ">= 10"
}
},
- "node_modules/oslo/node_modules/@node-rs/argon2-linux-x64-musl": {
- "version": "1.7.0",
- "resolved": "https://registry.npmjs.org/@node-rs/argon2-linux-x64-musl/-/argon2-linux-x64-musl-1.7.0.tgz",
- "integrity": "sha512-/o1efYCYIxjfuoRYyBTi2Iy+1iFfhqHCvvVsnjNSgO1xWiWrX0Rrt/xXW5Zsl7vS2Y+yu8PL8KFWRzZhaVxfKA==",
- "cpu": [
- "x64"
- ],
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ],
- "engines": {
- "node": ">= 10"
- }
- },
"node_modules/own-keys": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/own-keys/-/own-keys-1.0.1.tgz",
@@ -15922,12 +15333,12 @@
}
},
"node_modules/posthog-node": {
- "version": "5.9.5",
- "resolved": "https://registry.npmjs.org/posthog-node/-/posthog-node-5.9.5.tgz",
- "integrity": "sha512-Rv82jMVhnxlBNf8wDbP+iAJdZrhU0aHul0LaFrQ/JGxxDiK3EkclIqr+QUwA9CulleTtXf6AIFz22tLvbVs/HA==",
+ "version": "5.10.0",
+ "resolved": "https://registry.npmjs.org/posthog-node/-/posthog-node-5.10.0.tgz",
+ "integrity": "sha512-uNN+YUuOdbDSbDMGk/Wq57o2YBEH0Unu1kEq2PuYmqFmnu+oYsKyJBrb58VNwEuYsaXVJmk4FtbD+Tl8BT69+w==",
"license": "MIT",
"dependencies": {
- "@posthog/core": "1.2.4"
+ "@posthog/core": "1.3.0"
},
"engines": {
"node": ">=20"
@@ -16992,9 +16403,9 @@
"license": "MIT"
},
"node_modules/schema-utils": {
- "version": "4.3.2",
- "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.3.2.tgz",
- "integrity": "sha512-Gn/JaSk/Mt9gYubxTtSn/QCV4em9mpAPiR1rqy/Ocu19u/G9J5WWdNoUT4SiV6mFC3y6cxyFcFwdzPM3FgxGAQ==",
+ "version": "4.3.3",
+ "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.3.3.tgz",
+ "integrity": "sha512-eflK8wEtyOE6+hsaRVPxvUKYCpRgzLqDTb8krvAsRIwOGlHoSgYLgBXoubGgLd2fT41/OUYdb48v4k4WWHQurA==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -18145,9 +17556,9 @@
}
},
"node_modules/swagger-ui-dist": {
- "version": "5.29.1",
- "resolved": "https://registry.npmjs.org/swagger-ui-dist/-/swagger-ui-dist-5.29.1.tgz",
- "integrity": "sha512-qyjpz0qgcomRr41a5Aye42o69TKwCeHM9F8htLGVeUMKekNS6qAqz9oS7CtSvgGJSppSNAYAIh7vrfrSdHj9zw==",
+ "version": "5.29.3",
+ "resolved": "https://registry.npmjs.org/swagger-ui-dist/-/swagger-ui-dist-5.29.3.tgz",
+ "integrity": "sha512-U99f/2YocRA2Mxqx3eUBRhQonWVtE5dIvMs0Zlsn4a4ip8awMq0JxXhU+Sidtna2WlZrHbK2Rro3RZvYUymRbA==",
"license": "Apache-2.0",
"dependencies": {
"@scarf/scarf": "=1.4.0"
@@ -18185,9 +17596,9 @@
"license": "MIT"
},
"node_modules/tapable": {
- "version": "2.2.3",
- "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.3.tgz",
- "integrity": "sha512-ZL6DDuAlRlLGghwcfmSn9sK3Hr6ArtyudlSAiCqQ6IfE+b+HHbydbYDIG15IfS5do+7XQQBdBiubF/cV2dnDzg==",
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.0.tgz",
+ "integrity": "sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==",
"dev": true,
"license": "MIT",
"engines": {
@@ -18950,9 +18361,9 @@
}
},
"node_modules/use-sync-external-store": {
- "version": "1.5.0",
- "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.5.0.tgz",
- "integrity": "sha512-Rb46I4cGGVBmjamjphe8L/UnvJD+uPPtTkNvX5mZgqdbavhI4EbgIWJiIHXJ8bc/i9EQGPRh4DwEURJ552Do0A==",
+ "version": "1.6.0",
+ "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz",
+ "integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==",
"license": "MIT",
"peerDependencies": {
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
@@ -19033,9 +18444,9 @@
}
},
"node_modules/webpack": {
- "version": "5.102.0",
- "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.102.0.tgz",
- "integrity": "sha512-hUtqAR3ZLVEYDEABdBioQCIqSoguHbFn1K7WlPPWSuXmx0031BD73PSE35jKyftdSh4YLDoQNgK4pqBt5Q82MA==",
+ "version": "5.102.1",
+ "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.102.1.tgz",
+ "integrity": "sha512-7h/weGm9d/ywQ6qzJ+Xy+r9n/3qgp/thalBbpOi5i223dPXKi04IBtqPN9nTd+jBc7QKfvDbaBnFipYp4sJAUQ==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -19047,7 +18458,7 @@
"@webassemblyjs/wasm-parser": "^1.14.1",
"acorn": "^8.15.0",
"acorn-import-phases": "^1.0.3",
- "browserslist": "^4.24.5",
+ "browserslist": "^4.26.3",
"chrome-trace-event": "^1.0.2",
"enhanced-resolve": "^5.17.3",
"es-module-lexer": "^1.2.1",
@@ -19059,8 +18470,8 @@
"loader-runner": "^4.2.0",
"mime-types": "^2.1.27",
"neo-async": "^2.6.2",
- "schema-utils": "^4.3.2",
- "tapable": "^2.2.3",
+ "schema-utils": "^4.3.3",
+ "tapable": "^2.3.0",
"terser-webpack-plugin": "^5.3.11",
"watchpack": "^2.4.4",
"webpack-sources": "^3.3.3"
diff --git a/package.json b/package.json
index 74f540be..bca66d3d 100644
--- a/package.json
+++ b/package.json
@@ -19,17 +19,17 @@
"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",
- "set:saas": "echo 'export const build = \"saas\" as any;' > server/build.ts",
- "set:enterprise": "echo 'export const build = \"enterprise\" as any;' > server/build.ts",
+ "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:sqlite": "echo 'export * from \"./sqlite\";' > server/db/index.ts",
"set:pg": "echo 'export * from \"./pg\";' > server/db/index.ts",
+ "next:build": "next build",
"build:sqlite": "mkdir -p dist && next build && node esbuild.mjs -e server/index.ts -o dist/server.mjs && node esbuild.mjs -e server/setup/migrationsSqlite.ts -o dist/migrations.mjs",
"build:pg": "mkdir -p dist && next build && node esbuild.mjs -e server/index.ts -o dist/server.mjs && node esbuild.mjs -e server/setup/migrationsPg.ts -o dist/migrations.mjs",
"start": "ENVIRONMENT=prod node dist/migrations.mjs && ENVIRONMENT=prod NODE_ENV=development node --enable-source-maps dist/server.mjs",
"email": "email dev --dir server/emails/templates --port 3005",
- "build:cli": "node esbuild.mjs -e cli/index.ts -o dist/cli.mjs",
- "db:sqlite:seed-exit-node": "sqlite3 config/db/db.sqlite \"INSERT INTO exitNodes (exitNodeId, name, address, endpoint, publicKey, listenPort, reachableAt, maxConnections, online, lastPing, type, region) VALUES (null, 'test', '10.0.0.1/24', 'localhost', 'MJ44MpnWGxMZURgxW/fWXDFsejhabnEFYDo60LQwK3A=', 1234, 'http://localhost:3003', 123, 1, null, 'gerbil', null);\""
+ "build:cli": "node esbuild.mjs -e cli/index.ts -o dist/cli.mjs"
},
"dependencies": {
"@asteasolutions/zod-to-openapi": "^7.3.4",
diff --git a/public/logo/word_mark_black.png b/public/logo/word_mark_black.png
index ba6fb84e..cc412165 100644
Binary files a/public/logo/word_mark_black.png and b/public/logo/word_mark_black.png differ
diff --git a/public/logo/word_mark_white.png b/public/logo/word_mark_white.png
index fb7a252d..cd02b58a 100644
Binary files a/public/logo/word_mark_white.png and b/public/logo/word_mark_white.png differ
diff --git a/server/apiServer.ts b/server/apiServer.ts
index 0b5a6305..6c490053 100644
--- a/server/apiServer.ts
+++ b/server/apiServer.ts
@@ -7,21 +7,21 @@ import {
errorHandlerMiddleware,
notFoundMiddleware
} from "@server/middlewares";
-import { corsWithLoginPageSupport } from "@server/middlewares/private/corsWithLoginPage";
-import { authenticated, unauthenticated } from "@server/routers/external";
-import { router as wsRouter, handleWSUpgrade } from "@server/routers/ws";
+import { authenticated, unauthenticated } from "#dynamic/routers/external";
+import { router as wsRouter, handleWSUpgrade } from "#dynamic/routers/ws";
import { logIncomingMiddleware } from "./middlewares/logIncoming";
import { csrfProtectionMiddleware } from "./middlewares/csrfProtection";
import helmet from "helmet";
-import { stripeWebhookHandler } from "@server/routers/private/billing/webhooks";
import { build } from "./build";
import rateLimit, { ipKeyGenerator } from "express-rate-limit";
import createHttpError from "http-errors";
import HttpCode from "./types/HttpCode";
import requestTimeoutMiddleware from "./middlewares/requestTimeout";
-import { createStore } from "@server/lib/private/rateLimitStore";
-import hybridRouter from "@server/routers/private/hybrid";
+import { createStore } from "#dynamic/lib/rateLimitStore";
import { stripDuplicateSesions } from "./middlewares/stripDuplicateSessions";
+import { corsWithLoginPageSupport } from "@server/lib/corsWithLoginPage";
+import { hybridRouter } from "#dynamic/routers/hybrid";
+import { billingWebhookHandler } from "#dynamic/routers/billing/webhooks";
const dev = config.isDev;
const externalPort = config.getRawConfig().server.external_port;
@@ -39,32 +39,30 @@ export function createApiServer() {
apiServer.post(
`${prefix}/billing/webhooks`,
express.raw({ type: "application/json" }),
- stripeWebhookHandler
+ billingWebhookHandler
);
}
const corsConfig = config.getRawConfig().server.cors;
+ const options = {
+ ...(corsConfig?.origins
+ ? { origin: corsConfig.origins }
+ : {
+ origin: (origin: any, callback: any) => {
+ callback(null, true);
+ }
+ }),
+ ...(corsConfig?.methods && { methods: corsConfig.methods }),
+ ...(corsConfig?.allowed_headers && {
+ allowedHeaders: corsConfig.allowed_headers
+ }),
+ credentials: !(corsConfig?.credentials === false)
+ };
- if (build == "oss") {
- const options = {
- ...(corsConfig?.origins
- ? { origin: corsConfig.origins }
- : {
- origin: (origin: any, callback: any) => {
- callback(null, true);
- }
- }),
- ...(corsConfig?.methods && { methods: corsConfig.methods }),
- ...(corsConfig?.allowed_headers && {
- allowedHeaders: corsConfig.allowed_headers
- }),
- credentials: !(corsConfig?.credentials === false)
- };
-
+ if (build == "oss" || !corsConfig) {
logger.debug("Using CORS options", options);
-
apiServer.use(cors(options));
- } else {
+ } else if (corsConfig) {
// Use the custom CORS middleware with loginPage support
apiServer.use(corsWithLoginPageSupport(corsConfig));
}
diff --git a/server/auth/actions.ts b/server/auth/actions.ts
index 668be0db..e48bc502 100644
--- a/server/auth/actions.ts
+++ b/server/auth/actions.ts
@@ -4,7 +4,6 @@ import { userActions, roleActions, userOrgs } from "@server/db";
import { and, eq } from "drizzle-orm";
import createHttpError from "http-errors";
import HttpCode from "@server/types/HttpCode";
-import { sendUsageNotification } from "@server/routers/org";
export enum ActionsEnum {
createOrgUser = "createOrgUser",
diff --git a/server/auth/sessions/resource.ts b/server/auth/sessions/resource.ts
index a378202e..31ab2b38 100644
--- a/server/auth/sessions/resource.ts
+++ b/server/auth/sessions/resource.ts
@@ -4,9 +4,6 @@ import { resourceSessions, ResourceSession } from "@server/db";
import { db } from "@server/db";
import { eq, and } from "drizzle-orm";
import config from "@server/lib/config";
-import axios from "axios";
-import logger from "@server/logger";
-import { tokenManager } from "@server/lib/tokenManager";
export const SESSION_COOKIE_NAME =
config.getRawConfig().server.session_cookie_name;
@@ -65,29 +62,6 @@ export async function validateResourceSessionToken(
token: string,
resourceId: number
): Promise {
- if (config.isManagedMode()) {
- try {
- const response = await axios.post(`${config.getRawConfig().managed?.endpoint}/api/v1/hybrid/resource/${resourceId}/session/validate`, {
- token: token
- }, await tokenManager.getAuthHeader());
- return response.data.data;
- } catch (error) {
- if (axios.isAxiosError(error)) {
- logger.error("Error validating resource session token in hybrid mode:", {
- message: error.message,
- code: error.code,
- status: error.response?.status,
- statusText: error.response?.statusText,
- url: error.config?.url,
- method: error.config?.method
- });
- } else {
- logger.error("Error validating resource session token in hybrid mode:", error);
- }
- return { resourceSession: null };
- }
- }
-
const sessionId = encodeHexLowerCase(
sha256(new TextEncoder().encode(token))
);
diff --git a/server/cleanup.ts b/server/cleanup.ts
new file mode 100644
index 00000000..de54ed77
--- /dev/null
+++ b/server/cleanup.ts
@@ -0,0 +1,13 @@
+import { cleanup as wsCleanup } from "@server/routers/ws";
+
+async function cleanup() {
+ await wsCleanup();
+
+ process.exit(0);
+}
+
+export async function initCleanup() {
+ // Handle process termination
+ process.on("SIGTERM", () => cleanup());
+ process.on("SIGINT", () => cleanup());
+}
\ No newline at end of file
diff --git a/server/db/pg/driver.ts b/server/db/pg/driver.ts
index 23904c7e..6dbef7e8 100644
--- a/server/db/pg/driver.ts
+++ b/server/db/pg/driver.ts
@@ -38,9 +38,9 @@ function createDb() {
const poolConfig = config.postgres.pool;
const primaryPool = new Pool({
connectionString,
- max: poolConfig.max_connections,
- idleTimeoutMillis: poolConfig.idle_timeout_ms,
- connectionTimeoutMillis: poolConfig.connection_timeout_ms,
+ max: poolConfig?.max_connections || 20,
+ idleTimeoutMillis: poolConfig?.idle_timeout_ms || 30000,
+ connectionTimeoutMillis: poolConfig?.connection_timeout_ms || 5000,
});
const replicas = [];
@@ -51,9 +51,9 @@ function createDb() {
for (const conn of replicaConnections) {
const replicaPool = new Pool({
connectionString: conn.connection_string,
- max: poolConfig.max_replica_connections,
- idleTimeoutMillis: poolConfig.idle_timeout_ms,
- connectionTimeoutMillis: poolConfig.connection_timeout_ms,
+ max: poolConfig?.max_replica_connections || 20,
+ idleTimeoutMillis: poolConfig?.idle_timeout_ms || 30000,
+ connectionTimeoutMillis: poolConfig?.connection_timeout_ms || 5000,
});
replicas.push(DrizzlePostgres(replicaPool));
}
diff --git a/server/db/pg/index.ts b/server/db/pg/index.ts
index 5cc80e86..6e2c79f5 100644
--- a/server/db/pg/index.ts
+++ b/server/db/pg/index.ts
@@ -1,3 +1,3 @@
export * from "./driver";
-export * from "./schema";
-export * from "./privateSchema";
+export * from "./schema/schema";
+export * from "./schema/privateSchema";
diff --git a/server/db/pg/privateSchema.ts b/server/db/pg/schema/privateSchema.ts
similarity index 96%
rename from server/db/pg/privateSchema.ts
rename to server/db/pg/schema/privateSchema.ts
index 8ea8f9de..67fb28ec 100644
--- a/server/db/pg/privateSchema.ts
+++ b/server/db/pg/schema/privateSchema.ts
@@ -1,16 +1,3 @@
-/*
- * 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 {
pgTable,
serial,
diff --git a/server/db/pg/schema.ts b/server/db/pg/schema/schema.ts
similarity index 99%
rename from server/db/pg/schema.ts
rename to server/db/pg/schema/schema.ts
index e94bfe36..2e307c5f 100644
--- a/server/db/pg/schema.ts
+++ b/server/db/pg/schema/schema.ts
@@ -720,4 +720,5 @@ export type OrgDomains = InferSelectModel;
export type SiteResource = InferSelectModel;
export type SetupToken = InferSelectModel;
export type HostMeta = InferSelectModel;
-export type TargetHealthCheck = InferSelectModel;
\ No newline at end of file
+export type TargetHealthCheck = InferSelectModel;
+export type IdpOidcConfig = InferSelectModel;
diff --git a/server/db/queries/verifySessionQueries.ts b/server/db/queries/verifySessionQueries.ts
index 09c465b5..8944a491 100644
--- a/server/db/queries/verifySessionQueries.ts
+++ b/server/db/queries/verifySessionQueries.ts
@@ -17,10 +17,6 @@ import {
users
} from "@server/db";
import { and, eq } from "drizzle-orm";
-import axios from "axios";
-import config from "@server/lib/config";
-import logger from "@server/logger";
-import { tokenManager } from "@server/lib/tokenManager";
export type ResourceWithAuth = {
resource: Resource | null;
@@ -40,30 +36,6 @@ export type UserSessionWithUser = {
export async function getResourceByDomain(
domain: string
): Promise {
- if (config.isManagedMode()) {
- try {
- const response = await axios.get(
- `${config.getRawConfig().managed?.endpoint}/api/v1/hybrid/resource/domain/${domain}`,
- await tokenManager.getAuthHeader()
- );
- return response.data.data;
- } catch (error) {
- if (axios.isAxiosError(error)) {
- logger.error("Error fetching config in verify session:", {
- message: error.message,
- code: error.code,
- status: error.response?.status,
- statusText: error.response?.statusText,
- url: error.config?.url,
- method: error.config?.method
- });
- } else {
- logger.error("Error fetching config in verify session:", error);
- }
- return null;
- }
- }
-
const [result] = await db
.select()
.from(resources)
@@ -100,30 +72,6 @@ export async function getResourceByDomain(
export async function getUserSessionWithUser(
userSessionId: string
): Promise {
- if (config.isManagedMode()) {
- try {
- const response = await axios.get(
- `${config.getRawConfig().managed?.endpoint}/api/v1/hybrid/session/${userSessionId}`,
- await tokenManager.getAuthHeader()
- );
- return response.data.data;
- } catch (error) {
- if (axios.isAxiosError(error)) {
- logger.error("Error fetching config in verify session:", {
- message: error.message,
- code: error.code,
- status: error.response?.status,
- statusText: error.response?.statusText,
- url: error.config?.url,
- method: error.config?.method
- });
- } else {
- logger.error("Error fetching config in verify session:", error);
- }
- return null;
- }
- }
-
const [res] = await db
.select()
.from(sessions)
@@ -144,30 +92,6 @@ export async function getUserSessionWithUser(
* Get user organization role
*/
export async function getUserOrgRole(userId: string, orgId: string) {
- if (config.isManagedMode()) {
- try {
- const response = await axios.get(
- `${config.getRawConfig().managed?.endpoint}/api/v1/hybrid/user/${userId}/org/${orgId}/role`,
- await tokenManager.getAuthHeader()
- );
- return response.data.data;
- } catch (error) {
- if (axios.isAxiosError(error)) {
- logger.error("Error fetching config in verify session:", {
- message: error.message,
- code: error.code,
- status: error.response?.status,
- statusText: error.response?.statusText,
- url: error.config?.url,
- method: error.config?.method
- });
- } else {
- logger.error("Error fetching config in verify session:", error);
- }
- return null;
- }
- }
-
const userOrgRole = await db
.select()
.from(userOrgs)
@@ -184,30 +108,6 @@ export async function getRoleResourceAccess(
resourceId: number,
roleId: number
) {
- if (config.isManagedMode()) {
- try {
- const response = await axios.get(
- `${config.getRawConfig().managed?.endpoint}/api/v1/hybrid/role/${roleId}/resource/${resourceId}/access`,
- await tokenManager.getAuthHeader()
- );
- return response.data.data;
- } catch (error) {
- if (axios.isAxiosError(error)) {
- logger.error("Error fetching config in verify session:", {
- message: error.message,
- code: error.code,
- status: error.response?.status,
- statusText: error.response?.statusText,
- url: error.config?.url,
- method: error.config?.method
- });
- } else {
- logger.error("Error fetching config in verify session:", error);
- }
- return null;
- }
- }
-
const roleResourceAccess = await db
.select()
.from(roleResources)
@@ -229,30 +129,6 @@ export async function getUserResourceAccess(
userId: string,
resourceId: number
) {
- if (config.isManagedMode()) {
- try {
- const response = await axios.get(
- `${config.getRawConfig().managed?.endpoint}/api/v1/hybrid/user/${userId}/resource/${resourceId}/access`,
- await tokenManager.getAuthHeader()
- );
- return response.data.data;
- } catch (error) {
- if (axios.isAxiosError(error)) {
- logger.error("Error fetching config in verify session:", {
- message: error.message,
- code: error.code,
- status: error.response?.status,
- statusText: error.response?.statusText,
- url: error.config?.url,
- method: error.config?.method
- });
- } else {
- logger.error("Error fetching config in verify session:", error);
- }
- return null;
- }
- }
-
const userResourceAccess = await db
.select()
.from(userResources)
@@ -273,30 +149,6 @@ export async function getUserResourceAccess(
export async function getResourceRules(
resourceId: number
): Promise {
- if (config.isManagedMode()) {
- try {
- const response = await axios.get(
- `${config.getRawConfig().managed?.endpoint}/api/v1/hybrid/resource/${resourceId}/rules`,
- await tokenManager.getAuthHeader()
- );
- return response.data.data;
- } catch (error) {
- if (axios.isAxiosError(error)) {
- logger.error("Error fetching config in verify session:", {
- message: error.message,
- code: error.code,
- status: error.response?.status,
- statusText: error.response?.statusText,
- url: error.config?.url,
- method: error.config?.method
- });
- } else {
- logger.error("Error fetching config in verify session:", error);
- }
- return [];
- }
- }
-
const rules = await db
.select()
.from(resourceRules)
@@ -311,30 +163,6 @@ export async function getResourceRules(
export async function getOrgLoginPage(
orgId: string
): Promise {
- if (config.isManagedMode()) {
- try {
- const response = await axios.get(
- `${config.getRawConfig().managed?.endpoint}/api/v1/hybrid/org/${orgId}/login-page`,
- await tokenManager.getAuthHeader()
- );
- return response.data.data;
- } catch (error) {
- if (axios.isAxiosError(error)) {
- logger.error("Error fetching config in verify session:", {
- message: error.message,
- code: error.code,
- status: error.response?.status,
- statusText: error.response?.statusText,
- url: error.config?.url,
- method: error.config?.method
- });
- } else {
- logger.error("Error fetching config in verify session:", error);
- }
- return null;
- }
- }
-
const [result] = await db
.select()
.from(loginPageOrg)
diff --git a/server/db/sqlite/driver.ts b/server/db/sqlite/driver.ts
index 6369c268..211ba8ea 100644
--- a/server/db/sqlite/driver.ts
+++ b/server/db/sqlite/driver.ts
@@ -1,6 +1,6 @@
import { drizzle as DrizzleSqlite } from "drizzle-orm/better-sqlite3";
import Database from "better-sqlite3";
-import * as schema from "./schema";
+import * as schema from "./schema/schema";
import path from "path";
import fs from "fs";
import { APP_PATH } from "@server/lib/consts";
diff --git a/server/db/sqlite/index.ts b/server/db/sqlite/index.ts
index 8c7a15e5..6e2c79f5 100644
--- a/server/db/sqlite/index.ts
+++ b/server/db/sqlite/index.ts
@@ -1,3 +1,3 @@
export * from "./driver";
-export * from "./schema";
-export * from "./privateSchema";
\ No newline at end of file
+export * from "./schema/schema";
+export * from "./schema/privateSchema";
diff --git a/server/db/sqlite/privateSchema.ts b/server/db/sqlite/schema/privateSchema.ts
similarity index 95%
rename from server/db/sqlite/privateSchema.ts
rename to server/db/sqlite/schema/privateSchema.ts
index fbe86e25..557ebfd6 100644
--- a/server/db/sqlite/privateSchema.ts
+++ b/server/db/sqlite/schema/privateSchema.ts
@@ -1,16 +1,3 @@
-/*
- * 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 {
sqliteTable,
integer,
diff --git a/server/db/sqlite/schema.ts b/server/db/sqlite/schema/schema.ts
similarity index 99%
rename from server/db/sqlite/schema.ts
rename to server/db/sqlite/schema/schema.ts
index 9d64b85e..2c19a1c7 100644
--- a/server/db/sqlite/schema.ts
+++ b/server/db/sqlite/schema/schema.ts
@@ -759,4 +759,5 @@ export type SiteResource = InferSelectModel;
export type OrgDomains = InferSelectModel;
export type SetupToken = InferSelectModel;
export type HostMeta = InferSelectModel;
-export type TargetHealthCheck = InferSelectModel;
\ No newline at end of file
+export type TargetHealthCheck = InferSelectModel;
+export type IdpOidcConfig = InferSelectModel;
diff --git a/server/emails/index.ts b/server/emails/index.ts
index 2cdef8a1..42cfa39c 100644
--- a/server/emails/index.ts
+++ b/server/emails/index.ts
@@ -6,11 +6,6 @@ import logger from "@server/logger";
import SMTPTransport from "nodemailer/lib/smtp-transport";
function createEmailClient() {
- if (config.isManagedMode()) {
- // LETS NOT WORRY ABOUT EMAILS IN HYBRID
- return;
- }
-
const emailConfig = config.getRawConfig().email;
if (!emailConfig) {
logger.warn(
diff --git a/server/emails/sendEmail.ts b/server/emails/sendEmail.ts
index 24fb4fbd..c8a0b077 100644
--- a/server/emails/sendEmail.ts
+++ b/server/emails/sendEmail.ts
@@ -2,7 +2,6 @@ import { render } from "@react-email/render";
import { ReactElement } from "react";
import emailClient from "@server/emails";
import logger from "@server/logger";
-import config from "@server/lib/config";
export async function sendEmail(
template: ReactElement,
@@ -25,7 +24,7 @@ export async function sendEmail(
const emailHtml = await render(template);
- const appName = config.getRawPrivateConfig().branding?.app_name || "Pangolin";
+ const appName = process.env.BRANDING_APP_NAME || "Pangolin"; // From the private config loading into env vars to seperate away the private config
await emailClient.sendMail({
from: {
diff --git a/server/emails/templates/PrivateNotifyUsageLimitApproaching.tsx b/server/emails/templates/NotifyUsageLimitApproaching.tsx
similarity index 86%
rename from server/emails/templates/PrivateNotifyUsageLimitApproaching.tsx
rename to server/emails/templates/NotifyUsageLimitApproaching.tsx
index c66265e5..beab0300 100644
--- a/server/emails/templates/PrivateNotifyUsageLimitApproaching.tsx
+++ b/server/emails/templates/NotifyUsageLimitApproaching.tsx
@@ -1,16 +1,3 @@
-/*
- * 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 React from "react";
import { Body, Head, Html, Preview, Tailwind } from "@react-email/components";
import { themeColors } from "./lib/theme";
diff --git a/server/emails/templates/PrivateNotifyUsageLimitReached.tsx b/server/emails/templates/NotifyUsageLimitReached.tsx
similarity index 87%
rename from server/emails/templates/PrivateNotifyUsageLimitReached.tsx
rename to server/emails/templates/NotifyUsageLimitReached.tsx
index c4eac322..783d1b0e 100644
--- a/server/emails/templates/PrivateNotifyUsageLimitReached.tsx
+++ b/server/emails/templates/NotifyUsageLimitReached.tsx
@@ -1,16 +1,3 @@
-/*
- * 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 React from "react";
import { Body, Head, Html, Preview, Tailwind } from "@react-email/components";
import { themeColors } from "./lib/theme";
diff --git a/server/hybridServer.ts b/server/hybridServer.ts
deleted file mode 100644
index 7e9ce095..00000000
--- a/server/hybridServer.ts
+++ /dev/null
@@ -1,151 +0,0 @@
-import logger from "@server/logger";
-import config from "@server/lib/config";
-import { createWebSocketClient } from "./routers/ws/client";
-import { addPeer, deletePeer } from "./routers/gerbil/peers";
-import { db, exitNodes } from "./db";
-import { TraefikConfigManager } from "./lib/traefik/TraefikConfigManager";
-import { tokenManager } from "./lib/tokenManager";
-import { APP_VERSION } from "./lib/consts";
-import axios from "axios";
-
-export async function createHybridClientServer() {
- logger.info("Starting hybrid client server...");
-
- // Start the token manager
- await tokenManager.start();
-
- const token = await tokenManager.getToken();
-
- const monitor = new TraefikConfigManager();
-
- await monitor.start();
-
- // Create client
- const client = createWebSocketClient(
- token,
- config.getRawConfig().managed!.endpoint!,
- {
- reconnectInterval: 5000,
- pingInterval: 30000,
- pingTimeout: 10000
- }
- );
-
- // Register message handlers
- client.registerHandler("remoteExitNode/peers/add", async (message) => {
- const { publicKey, allowedIps } = message.data;
-
- // TODO: we are getting the exit node twice here
- // NOTE: there should only be one gerbil registered so...
- const [exitNode] = await db.select().from(exitNodes).limit(1);
- await addPeer(exitNode.exitNodeId, {
- publicKey: publicKey,
- allowedIps: allowedIps || []
- });
- });
-
- client.registerHandler("remoteExitNode/peers/remove", async (message) => {
- const { publicKey } = message.data;
-
- // TODO: we are getting the exit node twice here
- // NOTE: there should only be one gerbil registered so...
- const [exitNode] = await db.select().from(exitNodes).limit(1);
- await deletePeer(exitNode.exitNodeId, publicKey);
- });
-
- // /update-proxy-mapping
- client.registerHandler("remoteExitNode/update-proxy-mapping", async (message) => {
- try {
- const [exitNode] = await db.select().from(exitNodes).limit(1);
- if (!exitNode) {
- logger.error("No exit node found for proxy mapping update");
- return;
- }
-
- const response = await axios.post(`${exitNode.endpoint}/update-proxy-mapping`, message.data);
- logger.info(`Successfully updated proxy mapping: ${response.status}`);
- } catch (error) {
- // pull data out of the axios error to log
- if (axios.isAxiosError(error)) {
- logger.error("Error updating proxy mapping:", {
- message: error.message,
- code: error.code,
- status: error.response?.status,
- statusText: error.response?.statusText,
- url: error.config?.url,
- method: error.config?.method
- });
- } else {
- logger.error("Error updating proxy mapping:", error);
- }
- }
- });
-
- // /update-destinations
- client.registerHandler("remoteExitNode/update-destinations", async (message) => {
- try {
- const [exitNode] = await db.select().from(exitNodes).limit(1);
- if (!exitNode) {
- logger.error("No exit node found for destinations update");
- return;
- }
-
- const response = await axios.post(`${exitNode.endpoint}/update-destinations`, message.data);
- logger.info(`Successfully updated destinations: ${response.status}`);
- } catch (error) {
- // pull data out of the axios error to log
- if (axios.isAxiosError(error)) {
- logger.error("Error updating destinations:", {
- message: error.message,
- code: error.code,
- status: error.response?.status,
- statusText: error.response?.statusText,
- url: error.config?.url,
- method: error.config?.method
- });
- } else {
- logger.error("Error updating destinations:", error);
- }
- }
- });
-
- client.registerHandler("remoteExitNode/traefik/reload", async (message) => {
- await monitor.HandleTraefikConfig();
- });
-
- // Listen to connection events
- client.on("connect", () => {
- logger.info("Connected to WebSocket server");
- client.sendMessage("remoteExitNode/register", {
- remoteExitNodeVersion: APP_VERSION
- });
- });
-
- client.on("disconnect", () => {
- logger.info("Disconnected from WebSocket server");
- });
-
- client.on("message", (message) => {
- logger.info(
- `Received message: ${message.type} ${JSON.stringify(message.data)}`
- );
- });
-
- // Connect to the server
- try {
- await client.connect();
- logger.info("Connection initiated");
- } catch (error) {
- logger.error("Failed to connect:", error);
- }
-
- // Store the ping interval stop function for cleanup if needed
- const stopPingInterval = client.sendMessageInterval(
- "remoteExitNode/ping",
- { timestamp: Date.now() / 1000 },
- 60000
- ); // send every minute
-
- // Return client and cleanup function for potential use
- return { client, stopPingInterval };
-}
diff --git a/server/index.ts b/server/index.ts
index 2497e301..a92968a6 100644
--- a/server/index.ts
+++ b/server/index.ts
@@ -5,13 +5,20 @@ import { runSetupFunctions } from "./setup";
import { createApiServer } from "./apiServer";
import { createNextServer } from "./nextServer";
import { createInternalServer } from "./internalServer";
-import { ApiKey, ApiKeyOrg, RemoteExitNode, Session, User, UserOrg } from "@server/db";
+import {
+ ApiKey,
+ ApiKeyOrg,
+ RemoteExitNode,
+ Session,
+ User,
+ UserOrg
+} from "@server/db";
import { createIntegrationApiServer } from "./integrationApiServer";
-import { createHybridClientServer } from "./hybridServer";
import config from "@server/lib/config";
import { setHostMeta } from "@server/lib/hostMeta";
import { initTelemetryClient } from "./lib/telemetry.js";
import { TraefikConfigManager } from "./lib/traefik/TraefikConfigManager.js";
+import { initCleanup } from "#dynamic/cleanup";
async function startServers() {
await setHostMeta();
@@ -25,16 +32,11 @@ async function startServers() {
const apiServer = createApiServer();
const internalServer = createInternalServer();
- let hybridClientServer;
let nextServer;
- if (config.isManagedMode()) {
- hybridClientServer = await createHybridClientServer();
- } else {
- nextServer = await createNextServer();
- if (config.getRawConfig().traefik.file_mode) {
- const monitor = new TraefikConfigManager();
- await monitor.start();
- }
+ nextServer = await createNextServer();
+ if (config.getRawConfig().traefik.file_mode) {
+ const monitor = new TraefikConfigManager();
+ await monitor.start();
}
let integrationServer;
@@ -42,12 +44,13 @@ async function startServers() {
integrationServer = createIntegrationApiServer();
}
+ await initCleanup();
+
return {
apiServer,
nextServer,
internalServer,
- integrationServer,
- hybridClientServer
+ integrationServer
};
}
diff --git a/server/integrationApiServer.ts b/server/integrationApiServer.ts
index cbbea83f..3416004c 100644
--- a/server/integrationApiServer.ts
+++ b/server/integrationApiServer.ts
@@ -7,7 +7,7 @@ import {
errorHandlerMiddleware,
notFoundMiddleware,
} from "@server/middlewares";
-import { authenticated, unauthenticated } from "@server/routers/integration";
+import { authenticated, unauthenticated } from "#dynamic/routers/integration";
import { logIncomingMiddleware } from "./middlewares/logIncoming";
import helmet from "helmet";
import swaggerUi from "swagger-ui-express";
diff --git a/server/internalServer.ts b/server/internalServer.ts
index 0ba64eec..d15e3c45 100644
--- a/server/internalServer.ts
+++ b/server/internalServer.ts
@@ -8,7 +8,7 @@ import {
errorHandlerMiddleware,
notFoundMiddleware
} from "@server/middlewares";
-import internal from "@server/routers/internal";
+import { internalRouter } from "#dynamic/routers/internal";
import { stripDuplicateSesions } from "./middlewares/stripDuplicateSessions";
const internalPort = config.getRawConfig().server.internal_port;
@@ -23,7 +23,7 @@ export function createInternalServer() {
internalServer.use(express.json());
const prefix = `/api/v1`;
- internalServer.use(prefix, internal);
+ internalServer.use(prefix, internalRouter);
internalServer.use(notFoundMiddleware);
internalServer.use(errorHandlerMiddleware);
diff --git a/server/lib/billing/createCustomer.ts b/server/lib/billing/createCustomer.ts
new file mode 100644
index 00000000..7f65bfb2
--- /dev/null
+++ b/server/lib/billing/createCustomer.ts
@@ -0,0 +1,6 @@
+export async function createCustomer(
+ orgId: string,
+ email: string | null | undefined
+): Promise {
+ return;
+}
diff --git a/server/lib/private/billing/features.ts b/server/lib/billing/features.ts
similarity index 88%
rename from server/lib/private/billing/features.ts
rename to server/lib/billing/features.ts
index 11d78bbb..b72543cc 100644
--- a/server/lib/private/billing/features.ts
+++ b/server/lib/billing/features.ts
@@ -1,16 +1,3 @@
-/*
- * 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 Stripe from "stripe";
export enum FeatureId {
diff --git a/server/lib/billing/getOrgTierData.ts b/server/lib/billing/getOrgTierData.ts
new file mode 100644
index 00000000..24664790
--- /dev/null
+++ b/server/lib/billing/getOrgTierData.ts
@@ -0,0 +1,8 @@
+export async function getOrgTierData(
+ orgId: string
+): Promise<{ tier: string | null; active: boolean }> {
+ let tier = null;
+ let active = false;
+
+ return { tier, active };
+}
diff --git a/server/lib/billing/index.ts b/server/lib/billing/index.ts
new file mode 100644
index 00000000..6c3ef792
--- /dev/null
+++ b/server/lib/billing/index.ts
@@ -0,0 +1,5 @@
+export * from "./limitSet";
+export * from "./features";
+export * from "./limitsService";
+export * from "./getOrgTierData";
+export * from "./createCustomer";
\ No newline at end of file
diff --git a/server/lib/private/billing/limitSet.ts b/server/lib/billing/limitSet.ts
similarity index 82%
rename from server/lib/private/billing/limitSet.ts
rename to server/lib/billing/limitSet.ts
index ec6107b2..153d8ae8 100644
--- a/server/lib/private/billing/limitSet.ts
+++ b/server/lib/billing/limitSet.ts
@@ -1,16 +1,3 @@
-/*
- * 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 { FeatureId } from "./features";
export type LimitSet = {
diff --git a/server/lib/private/billing/limitsService.ts b/server/lib/billing/limitsService.ts
similarity index 76%
rename from server/lib/private/billing/limitsService.ts
rename to server/lib/billing/limitsService.ts
index 168f5580..a07f70b3 100644
--- a/server/lib/private/billing/limitsService.ts
+++ b/server/lib/billing/limitsService.ts
@@ -1,16 +1,3 @@
-/*
- * 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 { db, limits } from "@server/db";
import { and, eq } from "drizzle-orm";
import { LimitSet } from "./limitSet";
diff --git a/server/lib/private/billing/tiers.ts b/server/lib/billing/tiers.ts
similarity index 69%
rename from server/lib/private/billing/tiers.ts
rename to server/lib/billing/tiers.ts
index e6322c9f..6ccf8898 100644
--- a/server/lib/private/billing/tiers.ts
+++ b/server/lib/billing/tiers.ts
@@ -1,16 +1,3 @@
-/*
- * 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.
- */
-
export enum TierId {
STANDARD = "standard",
}
diff --git a/server/lib/private/billing/usageService.ts b/server/lib/billing/usageService.ts
similarity index 84%
rename from server/lib/private/billing/usageService.ts
rename to server/lib/billing/usageService.ts
index f18542d2..0b2b095f 100644
--- a/server/lib/private/billing/usageService.ts
+++ b/server/lib/billing/usageService.ts
@@ -1,21 +1,7 @@
-/*
- * 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 { eq, sql, and } from "drizzle-orm";
import NodeCache from "node-cache";
import { v4 as uuidv4 } from "uuid";
import { PutObjectCommand } from "@aws-sdk/client-s3";
-import { s3Client } from "../s3";
import * as fs from "fs/promises";
import * as path from "path";
import {
@@ -30,10 +16,10 @@ import {
Transaction
} from "@server/db";
import { FeatureId, getFeatureMeterId } from "./features";
-import config from "@server/lib/config";
import logger from "@server/logger";
-import { sendToClient } from "@server/routers/ws";
+import { sendToClient } from "#dynamic/routers/ws";
import { build } from "@server/build";
+import { s3Client } from "@server/lib/s3";
interface StripeEvent {
identifier?: string;
@@ -45,6 +31,17 @@ interface StripeEvent {
};
}
+export function noop() {
+ if (
+ build !== "saas" ||
+ !process.env.S3_BUCKET ||
+ !process.env.LOCAL_FILE_PATH
+ ) {
+ return true;
+ }
+ return false;
+}
+
export class UsageService {
private cache: NodeCache;
private bucketName: string | undefined;
@@ -55,11 +52,13 @@ export class UsageService {
constructor() {
this.cache = new NodeCache({ stdTTL: 300 }); // 5 minute TTL
- if (build !== "saas") {
+ if (noop()) {
return;
}
- this.bucketName = config.getRawPrivateConfig().stripe?.s3Bucket;
- this.eventsDir = config.getRawPrivateConfig().stripe?.localFilePath;
+ // this.bucketName = privateConfig.getRawPrivateConfig().stripe?.s3Bucket;
+ // this.eventsDir = privateConfig.getRawPrivateConfig().stripe?.localFilePath;
+ this.bucketName = process.env.S3_BUCKET || undefined;
+ this.eventsDir = process.env.LOCAL_FILE_PATH || undefined;
// Ensure events directory exists
this.initializeEventsDirectory().then(() => {
@@ -83,7 +82,9 @@ export class UsageService {
private async initializeEventsDirectory(): Promise {
if (!this.eventsDir) {
- logger.warn("Stripe local file path is not configured, skipping events directory initialization.");
+ logger.warn(
+ "Stripe local file path is not configured, skipping events directory initialization."
+ );
return;
}
try {
@@ -95,7 +96,9 @@ export class UsageService {
private async uploadPendingEventFilesOnStartup(): Promise {
if (!this.eventsDir || !this.bucketName) {
- logger.warn("Stripe local file path or bucket name is not configured, skipping leftover event file upload.");
+ logger.warn(
+ "Stripe local file path or bucket name is not configured, skipping leftover event file upload."
+ );
return;
}
try {
@@ -118,15 +121,17 @@ export class UsageService {
ContentType: "application/json"
});
await s3Client.send(uploadCommand);
-
+
// Check if file still exists before unlinking
try {
await fs.access(filePath);
await fs.unlink(filePath);
} catch (unlinkError) {
- logger.debug(`Startup file ${file} was already deleted`);
+ logger.debug(
+ `Startup file ${file} was already deleted`
+ );
}
-
+
logger.info(
`Uploaded leftover event file ${file} to S3 with ${events.length} events`
);
@@ -136,7 +141,9 @@ export class UsageService {
await fs.access(filePath);
await fs.unlink(filePath);
} catch (unlinkError) {
- logger.debug(`Empty startup file ${file} was already deleted`);
+ logger.debug(
+ `Empty startup file ${file} was already deleted`
+ );
}
}
} catch (err) {
@@ -147,8 +154,8 @@ export class UsageService {
}
}
}
- } catch (err) {
- logger.error("Failed to scan for leftover event files:", err);
+ } catch (error) {
+ logger.error("Failed to scan for leftover event files");
}
}
@@ -158,17 +165,17 @@ export class UsageService {
value: number,
transaction: any = null
): Promise {
- if (build !== "saas") {
+ if (noop()) {
return null;
}
-
+
// Truncate value to 11 decimal places
value = this.truncateValue(value);
-
+
// Implement retry logic for deadlock handling
const maxRetries = 3;
let attempt = 0;
-
+
while (attempt <= maxRetries) {
try {
// Get subscription data for this org (with caching)
@@ -191,7 +198,12 @@ export class UsageService {
);
} else {
await db.transaction(async (trx) => {
- usage = await this.internalAddUsage(orgId, featureId, value, trx);
+ usage = await this.internalAddUsage(
+ orgId,
+ featureId,
+ value,
+ trx
+ );
});
}
@@ -201,25 +213,26 @@ export class UsageService {
return usage || null;
} catch (error: any) {
// Check if this is a deadlock error
- const isDeadlock = error?.code === '40P01' ||
- error?.cause?.code === '40P01' ||
- (error?.message && error.message.includes('deadlock'));
-
+ const isDeadlock =
+ error?.code === "40P01" ||
+ error?.cause?.code === "40P01" ||
+ (error?.message && error.message.includes("deadlock"));
+
if (isDeadlock && attempt < maxRetries) {
attempt++;
// Exponential backoff with jitter: 50-150ms, 100-300ms, 200-600ms
const baseDelay = Math.pow(2, attempt - 1) * 50;
const jitter = Math.random() * baseDelay;
const delay = baseDelay + jitter;
-
+
logger.warn(
`Deadlock detected for ${orgId}/${featureId}, retrying attempt ${attempt}/${maxRetries} after ${delay.toFixed(0)}ms`
);
-
- await new Promise(resolve => setTimeout(resolve, delay));
+
+ await new Promise((resolve) => setTimeout(resolve, delay));
continue;
}
-
+
logger.error(
`Failed to add usage for ${orgId}/${featureId} after ${attempt} attempts:`,
error
@@ -239,10 +252,10 @@ export class UsageService {
): Promise {
// Truncate value to 11 decimal places
value = this.truncateValue(value);
-
+
const usageId = `${orgId}-${featureId}`;
const meterId = getFeatureMeterId(featureId);
-
+
// Use upsert: insert if not exists, otherwise increment
const [returnUsage] = await trx
.insert(usage)
@@ -259,7 +272,8 @@ export class UsageService {
set: {
latestValue: sql`${usage.latestValue} + ${value}`
}
- }).returning();
+ })
+ .returning();
return returnUsage;
}
@@ -280,7 +294,7 @@ export class UsageService {
value?: number,
customerId?: string
): Promise {
- if (build !== "saas") {
+ if (noop()) {
return;
}
try {
@@ -351,7 +365,7 @@ export class UsageService {
.set({
latestValue: newRunningTotal,
instantaneousValue: value,
- updatedAt: Math.floor(Date.now() / 1000)
+ updatedAt: Math.floor(Date.now() / 1000)
})
.where(eq(usage.usageId, usageId));
}
@@ -366,7 +380,7 @@ export class UsageService {
meterId,
instantaneousValue: truncatedValue,
latestValue: truncatedValue,
- updatedAt: Math.floor(Date.now() / 1000)
+ updatedAt: Math.floor(Date.now() / 1000)
});
}
});
@@ -427,7 +441,7 @@ export class UsageService {
): Promise {
// Truncate value to 11 decimal places before sending to Stripe
const truncatedValue = this.truncateValue(value);
-
+
const event: StripeEvent = {
identifier: uuidv4(),
timestamp: Math.floor(new Date().getTime() / 1000),
@@ -444,7 +458,9 @@ export class UsageService {
private async writeEventToFile(event: StripeEvent): Promise {
if (!this.eventsDir || !this.bucketName) {
- logger.warn("Stripe local file path or bucket name is not configured, skipping event file write.");
+ logger.warn(
+ "Stripe local file path or bucket name is not configured, skipping event file write."
+ );
return;
}
if (!this.currentEventFile) {
@@ -493,7 +509,9 @@ export class UsageService {
private async uploadFileToS3(): Promise {
if (!this.bucketName || !this.eventsDir) {
- logger.warn("Stripe local file path or bucket name is not configured, skipping S3 upload.");
+ logger.warn(
+ "Stripe local file path or bucket name is not configured, skipping S3 upload."
+ );
return;
}
if (!this.currentEventFile) {
@@ -505,7 +523,9 @@ export class UsageService {
// Check if this file is already being uploaded
if (this.uploadingFiles.has(fileName)) {
- logger.debug(`File ${fileName} is already being uploaded, skipping`);
+ logger.debug(
+ `File ${fileName} is already being uploaded, skipping`
+ );
return;
}
@@ -517,7 +537,9 @@ export class UsageService {
try {
await fs.access(filePath);
} catch (error) {
- logger.debug(`File ${fileName} does not exist, may have been already processed`);
+ logger.debug(
+ `File ${fileName} does not exist, may have been already processed`
+ );
this.uploadingFiles.delete(fileName);
// Reset current file if it was this file
if (this.currentEventFile === fileName) {
@@ -537,7 +559,9 @@ export class UsageService {
await fs.unlink(filePath);
} catch (unlinkError) {
// File may have been already deleted
- logger.debug(`File ${fileName} was already deleted during cleanup`);
+ logger.debug(
+ `File ${fileName} was already deleted during cleanup`
+ );
}
this.currentEventFile = null;
this.uploadingFiles.delete(fileName);
@@ -560,7 +584,9 @@ export class UsageService {
await fs.unlink(filePath);
} catch (unlinkError) {
// File may have been already deleted by another process
- logger.debug(`File ${fileName} was already deleted during upload`);
+ logger.debug(
+ `File ${fileName} was already deleted during upload`
+ );
}
logger.info(
@@ -571,10 +597,7 @@ export class UsageService {
this.currentEventFile = null;
this.currentFileStartTime = 0;
} catch (error) {
- logger.error(
- `Failed to upload ${fileName} to S3:`,
- error
- );
+ logger.error(`Failed to upload ${fileName} to S3:`, error);
} finally {
// Always remove from uploading set
this.uploadingFiles.delete(fileName);
@@ -591,7 +614,7 @@ export class UsageService {
orgId: string,
featureId: FeatureId
): Promise {
- if (build !== "saas") {
+ if (noop()) {
return null;
}
@@ -610,7 +633,7 @@ export class UsageService {
`Creating new usage record for ${orgId}/${featureId}`
);
const meterId = getFeatureMeterId(featureId);
-
+
try {
const [newUsage] = await db
.insert(usage)
@@ -665,7 +688,7 @@ export class UsageService {
orgId: string,
featureId: FeatureId
): Promise {
- if (build !== "saas") {
+ if (noop()) {
return null;
}
await this.updateDaily(orgId, featureId); // Ensure daily usage is updated
@@ -685,7 +708,9 @@ export class UsageService {
*/
private async uploadOldEventFiles(): Promise {
if (!this.eventsDir || !this.bucketName) {
- logger.warn("Stripe local file path or bucket name is not configured, skipping old event file upload.");
+ logger.warn(
+ "Stripe local file path or bucket name is not configured, skipping old event file upload."
+ );
return;
}
try {
@@ -693,15 +718,17 @@ export class UsageService {
const now = Date.now();
for (const file of files) {
if (!file.endsWith(".json")) continue;
-
+
// Skip files that are already being uploaded
if (this.uploadingFiles.has(file)) {
- logger.debug(`Skipping file ${file} as it's already being uploaded`);
+ logger.debug(
+ `Skipping file ${file} as it's already being uploaded`
+ );
continue;
}
const filePath = path.join(this.eventsDir, file);
-
+
try {
// Check if file still exists before processing
try {
@@ -716,7 +743,7 @@ export class UsageService {
if (age >= 90000) {
// 1.5 minutes - Mark as being uploaded
this.uploadingFiles.add(file);
-
+
try {
const fileContent = await fs.readFile(
filePath,
@@ -732,15 +759,17 @@ export class UsageService {
ContentType: "application/json"
});
await s3Client.send(uploadCommand);
-
+
// Check if file still exists before unlinking
try {
await fs.access(filePath);
await fs.unlink(filePath);
} catch (unlinkError) {
- logger.debug(`File ${file} was already deleted during interval upload`);
+ logger.debug(
+ `File ${file} was already deleted during interval upload`
+ );
}
-
+
logger.info(
`Interval: Uploaded event file ${file} to S3 with ${events.length} events`
);
@@ -755,7 +784,9 @@ export class UsageService {
await fs.access(filePath);
await fs.unlink(filePath);
} catch (unlinkError) {
- logger.debug(`Empty file ${file} was already deleted`);
+ logger.debug(
+ `Empty file ${file} was already deleted`
+ );
}
}
} finally {
@@ -777,12 +808,17 @@ export class UsageService {
}
}
- public async checkLimitSet(orgId: string, kickSites = false, featureId?: FeatureId, usage?: Usage): Promise {
- if (build !== "saas") {
+ public async checkLimitSet(
+ orgId: string,
+ kickSites = false,
+ featureId?: FeatureId,
+ usage?: Usage
+ ): Promise {
+ if (noop()) {
return false;
}
// This method should check the current usage against the limits set for the organization
- // and kick out all of the sites on the org
+ // and kick out all of the sites on the org
let hasExceededLimits = false;
try {
@@ -817,16 +853,30 @@ export class UsageService {
if (usage) {
currentUsage = usage;
} else {
- currentUsage = await this.getUsage(orgId, limit.featureId as FeatureId);
+ currentUsage = await this.getUsage(
+ orgId,
+ limit.featureId as FeatureId
+ );
}
- const usageValue = currentUsage?.instantaneousValue || currentUsage?.latestValue || 0;
- logger.debug(`Current usage for org ${orgId} on feature ${limit.featureId}: ${usageValue}`);
- logger.debug(`Limit for org ${orgId} on feature ${limit.featureId}: ${limit.value}`);
- if (currentUsage && limit.value !== null && usageValue > limit.value) {
+ const usageValue =
+ currentUsage?.instantaneousValue ||
+ currentUsage?.latestValue ||
+ 0;
+ logger.debug(
+ `Current usage for org ${orgId} on feature ${limit.featureId}: ${usageValue}`
+ );
+ logger.debug(
+ `Limit for org ${orgId} on feature ${limit.featureId}: ${limit.value}`
+ );
+ if (
+ currentUsage &&
+ limit.value !== null &&
+ usageValue > limit.value
+ ) {
logger.debug(
`Org ${orgId} has exceeded limit for ${limit.featureId}: ` +
- `${usageValue} > ${limit.value}`
+ `${usageValue} > ${limit.value}`
);
hasExceededLimits = true;
break; // Exit early if any limit is exceeded
@@ -835,7 +885,9 @@ export class UsageService {
// If any limits are exceeded, disconnect all sites for this organization
if (hasExceededLimits && kickSites) {
- logger.warn(`Disconnecting all sites for org ${orgId} due to exceeded limits`);
+ logger.warn(
+ `Disconnecting all sites for org ${orgId} due to exceeded limits`
+ );
// Get all sites for this organization
const orgSites = await db
@@ -844,7 +896,7 @@ export class UsageService {
.where(eq(sites.orgId, orgId));
// Mark all sites as offline and send termination messages
- const siteUpdates = orgSites.map(site => site.siteId);
+ const siteUpdates = orgSites.map((site) => site.siteId);
if (siteUpdates.length > 0) {
// Send termination messages to newt sites
@@ -865,17 +917,21 @@ export class UsageService {
};
// Don't await to prevent blocking
- sendToClient(newt.newtId, payload).catch((error: any) => {
- logger.error(
- `Failed to send termination message to newt ${newt.newtId}:`,
- error
- );
- });
+ sendToClient(newt.newtId, payload).catch(
+ (error: any) => {
+ logger.error(
+ `Failed to send termination message to newt ${newt.newtId}:`,
+ error
+ );
+ }
+ );
}
}
}
- logger.info(`Disconnected ${orgSites.length} sites for org ${orgId} due to exceeded limits`);
+ logger.info(
+ `Disconnected ${orgSites.length} sites for org ${orgId} due to exceeded limits`
+ );
}
}
} catch (error) {
diff --git a/server/lib/blueprints/applyNewtDockerBlueprint.ts b/server/lib/blueprints/applyNewtDockerBlueprint.ts
index f69e4854..2afba84c 100644
--- a/server/lib/blueprints/applyNewtDockerBlueprint.ts
+++ b/server/lib/blueprints/applyNewtDockerBlueprint.ts
@@ -1,4 +1,4 @@
-import { sendToClient } from "@server/routers/ws";
+import { sendToClient } from "#dynamic/routers/ws";
import { processContainerLabels } from "./parseDockerContainers";
import { applyBlueprint } from "./applyBlueprint";
import { db, sites } from "@server/db";
diff --git a/server/lib/blueprints/proxyResources.ts b/server/lib/blueprints/proxyResources.ts
index 407c6019..a31cfb9d 100644
--- a/server/lib/blueprints/proxyResources.ts
+++ b/server/lib/blueprints/proxyResources.ts
@@ -25,7 +25,7 @@ import {
TargetData
} from "./types";
import logger from "@server/logger";
-import { createCertificate } from "@server/routers/private/certificates/createCertificate";
+import { createCertificate } from "#dynamic/routers/certificates/createCertificate";
import { pickPort } from "@server/routers/target/helpers";
import { resourcePassword } from "@server/db";
import { hashPassword } from "@server/auth/password";
diff --git a/server/lib/certificates.ts b/server/lib/certificates.ts
new file mode 100644
index 00000000..a6c51c96
--- /dev/null
+++ b/server/lib/certificates.ts
@@ -0,0 +1,13 @@
+export async function getValidCertificatesForDomains(domains: Set): Promise<
+ Array<{
+ id: number;
+ domain: string;
+ wildcard: boolean | null;
+ certFile: string | null;
+ keyFile: string | null;
+ expiresAt: number | null;
+ updatedAt?: number | null;
+ }>
+> {
+ return []; // stub
+}
\ No newline at end of file
diff --git a/server/lib/config.ts b/server/lib/config.ts
index 8b084e62..22c30338 100644
--- a/server/lib/config.ts
+++ b/server/lib/config.ts
@@ -3,19 +3,14 @@ import { __DIRNAME, APP_VERSION } from "@server/lib/consts";
import { db } from "@server/db";
import { SupporterKey, supporterKey } from "@server/db";
import { eq } from "drizzle-orm";
-import { license } from "@server/license/license";
+import { license } from "#dynamic/license/license";
import { configSchema, readConfigFile } from "./readConfigFile";
import { fromError } from "zod-validation-error";
-import {
- privateConfigSchema,
- readPrivateConfigFile
-} from "@server/lib/private/readConfigFile";
-import logger from "@server/logger";
import { build } from "@server/build";
+import logger from "@server/logger";
export class Config {
private rawConfig!: z.infer;
- private rawPrivateConfig!: z.infer;
supporterData: SupporterKey | null = null;
@@ -37,19 +32,6 @@ export class Config {
throw new Error(`Invalid configuration file: ${errors}`);
}
- const privateEnvironment = readPrivateConfigFile();
-
- const {
- data: parsedPrivateConfig,
- success: privateSuccess,
- error: privateError
- } = privateConfigSchema.safeParse(privateEnvironment);
-
- if (!privateSuccess) {
- const errors = fromError(privateError);
- throw new Error(`Invalid private configuration file: ${errors}`);
- }
-
if (
// @ts-ignore
parsedConfig.users ||
@@ -109,132 +91,29 @@ export class Config {
? "true"
: "false";
- if (parsedPrivateConfig.branding?.colors) {
- process.env.BRANDING_COLORS = JSON.stringify(
- parsedPrivateConfig.branding?.colors
- );
- }
-
- if (parsedPrivateConfig.branding?.logo?.light_path) {
- process.env.BRANDING_LOGO_LIGHT_PATH =
- parsedPrivateConfig.branding?.logo?.light_path;
- }
- if (parsedPrivateConfig.branding?.logo?.dark_path) {
- process.env.BRANDING_LOGO_DARK_PATH =
- parsedPrivateConfig.branding?.logo?.dark_path || undefined;
- }
-
- process.env.HIDE_SUPPORTER_KEY = parsedPrivateConfig.flags
- ?.hide_supporter_key
- ? "true"
- : "false";
-
- if (build != "oss") {
- if (parsedPrivateConfig.branding?.logo?.light_path) {
- process.env.BRANDING_LOGO_LIGHT_PATH =
- parsedPrivateConfig.branding?.logo?.light_path;
- }
- if (parsedPrivateConfig.branding?.logo?.dark_path) {
- process.env.BRANDING_LOGO_DARK_PATH =
- parsedPrivateConfig.branding?.logo?.dark_path || undefined;
- }
-
- process.env.BRANDING_LOGO_AUTH_WIDTH = parsedPrivateConfig.branding
- ?.logo?.auth_page?.width
- ? parsedPrivateConfig.branding?.logo?.auth_page?.width.toString()
- : undefined;
- process.env.BRANDING_LOGO_AUTH_HEIGHT = parsedPrivateConfig.branding
- ?.logo?.auth_page?.height
- ? parsedPrivateConfig.branding?.logo?.auth_page?.height.toString()
- : undefined;
-
- process.env.BRANDING_LOGO_NAVBAR_WIDTH = parsedPrivateConfig
- .branding?.logo?.navbar?.width
- ? parsedPrivateConfig.branding?.logo?.navbar?.width.toString()
- : undefined;
- process.env.BRANDING_LOGO_NAVBAR_HEIGHT = parsedPrivateConfig
- .branding?.logo?.navbar?.height
- ? parsedPrivateConfig.branding?.logo?.navbar?.height.toString()
- : undefined;
-
- process.env.BRANDING_FAVICON_PATH =
- parsedPrivateConfig.branding?.favicon_path;
-
- process.env.BRANDING_APP_NAME =
- parsedPrivateConfig.branding?.app_name || "Pangolin";
-
- if (parsedPrivateConfig.branding?.footer) {
- process.env.BRANDING_FOOTER = JSON.stringify(
- parsedPrivateConfig.branding?.footer
- );
- }
-
- process.env.LOGIN_PAGE_TITLE_TEXT =
- parsedPrivateConfig.branding?.login_page?.title_text || "";
- process.env.LOGIN_PAGE_SUBTITLE_TEXT =
- parsedPrivateConfig.branding?.login_page?.subtitle_text || "";
-
- process.env.SIGNUP_PAGE_TITLE_TEXT =
- parsedPrivateConfig.branding?.signup_page?.title_text || "";
- process.env.SIGNUP_PAGE_SUBTITLE_TEXT =
- parsedPrivateConfig.branding?.signup_page?.subtitle_text || "";
-
- process.env.RESOURCE_AUTH_PAGE_HIDE_POWERED_BY =
- parsedPrivateConfig.branding?.resource_auth_page
- ?.hide_powered_by === true
- ? "true"
- : "false";
- process.env.RESOURCE_AUTH_PAGE_SHOW_LOGO =
- parsedPrivateConfig.branding?.resource_auth_page?.show_logo ===
- true
- ? "true"
- : "false";
- process.env.RESOURCE_AUTH_PAGE_TITLE_TEXT =
- parsedPrivateConfig.branding?.resource_auth_page?.title_text ||
- "";
- process.env.RESOURCE_AUTH_PAGE_SUBTITLE_TEXT =
- parsedPrivateConfig.branding?.resource_auth_page
- ?.subtitle_text || "";
-
- if (parsedPrivateConfig.branding?.background_image_path) {
- process.env.BACKGROUND_IMAGE_PATH =
- parsedPrivateConfig.branding?.background_image_path;
- }
-
- if (parsedPrivateConfig.server.reo_client_id) {
- process.env.REO_CLIENT_ID =
- parsedPrivateConfig.server.reo_client_id;
- }
- }
-
if (parsedConfig.server.maxmind_db_path) {
process.env.MAXMIND_DB_PATH = parsedConfig.server.maxmind_db_path;
}
this.rawConfig = parsedConfig;
- this.rawPrivateConfig = parsedPrivateConfig;
}
public async initServer() {
if (!this.rawConfig) {
throw new Error("Config not loaded. Call load() first.");
}
- if (this.rawConfig.managed) {
- // LETS NOT WORRY ABOUT THE SERVER SECRET WHEN MANAGED
- return;
- }
+
license.setServerSecret(this.rawConfig.server.secret!);
await this.checkKeyStatus();
}
private async checkKeyStatus() {
- const licenseStatus = await license.check();
- if (
- !this.rawPrivateConfig.flags?.hide_supporter_key &&
- build != "oss" &&
- !licenseStatus.isHostLicensed
- ) {
+ if (build === "enterprise") {
+ await license.check();
+ }
+
+ if (build == "oss") {
this.checkSupporterKey();
}
}
@@ -243,10 +122,6 @@ export class Config {
return this.rawConfig;
}
- public getRawPrivateConfig() {
- return this.rawPrivateConfig;
- }
-
public getNoReplyEmail(): string | undefined {
return (
this.rawConfig.email?.no_reply || this.rawConfig.email?.smtp_user
@@ -280,10 +155,6 @@ export class Config {
return false;
}
- public isManagedMode() {
- return typeof this.rawConfig?.managed === "object";
- }
-
public async checkSupporterKey() {
const [key] = await db.select().from(supporterKey).limit(1);
diff --git a/server/lib/consts.ts b/server/lib/consts.ts
index 544123a9..8ad98167 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.10.4";
+export const APP_VERSION = "1.11.0";
export const __FILENAME = fileURLToPath(import.meta.url);
export const __DIRNAME = path.dirname(__FILENAME);
diff --git a/server/middlewares/private/corsWithLoginPage.ts b/server/lib/corsWithLoginPage.ts
similarity index 88%
rename from server/middlewares/private/corsWithLoginPage.ts
rename to server/lib/corsWithLoginPage.ts
index 95867fa1..43b26264 100644
--- a/server/middlewares/private/corsWithLoginPage.ts
+++ b/server/lib/corsWithLoginPage.ts
@@ -1,16 +1,3 @@
-/*
- * 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 cors, { CorsOptions } from "cors";
import config from "@server/lib/config";
diff --git a/server/lib/private/createUserAccountOrg.ts b/server/lib/createUserAccountOrg.ts
similarity index 89%
rename from server/lib/private/createUserAccountOrg.ts
rename to server/lib/createUserAccountOrg.ts
index abde5ca7..1406b935 100644
--- a/server/lib/private/createUserAccountOrg.ts
+++ b/server/lib/createUserAccountOrg.ts
@@ -1,16 +1,3 @@
-/*
- * 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 { isValidCIDR } from "@server/lib/validators";
import { getNextAvailableOrgSubnet } from "@server/lib/ip";
import {
@@ -28,9 +15,9 @@ import {
} from "@server/db";
import { eq } from "drizzle-orm";
import { defaultRoleAllowedActions } from "@server/routers/role";
-import { FeatureId, limitsService, sandboxLimitSet } from "@server/lib/private/billing";
-import { createCustomer } from "@server/routers/private/billing/createCustomer";
-import { usageService } from "@server/lib/private/billing/usageService";
+import { FeatureId, limitsService, sandboxLimitSet } from "@server/lib/billing";
+import { createCustomer } from "#dynamic/lib/billing";
+import { usageService } from "@server/lib/billing/usageService";
export async function createUserAccountOrg(
userId: string,
diff --git a/server/lib/exitNodes/exitNodes.ts b/server/lib/exitNodes/exitNodes.ts
index 8372d675..bb269710 100644
--- a/server/lib/exitNodes/exitNodes.ts
+++ b/server/lib/exitNodes/exitNodes.ts
@@ -16,7 +16,11 @@ export async function verifyExitNodeOrgAccess(
return { hasAccess: true, exitNode };
}
-export async function listExitNodes(orgId: string, filterOnline = false, noCloud = false) {
+export async function listExitNodes(
+ orgId: string,
+ filterOnline = false,
+ noCloud = false
+) {
// TODO: pick which nodes to send and ping better than just all of them that are not remote
const allExitNodes = await db
.select({
@@ -59,7 +63,16 @@ export async function checkExitNodeOrg(exitNodeId: number, orgId: string) {
return false;
}
-export async function resolveExitNodes(hostname: string, publicKey: string) {
+export async function resolveExitNodes(
+ hostname: string,
+ publicKey: string
+): Promise<
+ {
+ endpoint: string;
+ publicKey: string;
+ orgId: string;
+ }[]
+> {
// OSS version: simple implementation that returns empty array
return [];
}
diff --git a/server/lib/exitNodes/index.ts b/server/lib/exitNodes/index.ts
index dda94368..ba30ccc2 100644
--- a/server/lib/exitNodes/index.ts
+++ b/server/lib/exitNodes/index.ts
@@ -1,33 +1,4 @@
-import { build } from "@server/build";
-
-// Import both modules
-import * as exitNodesModule from "./exitNodes";
-import * as privateExitNodesModule from "./privateExitNodes";
-
-// Conditionally export exit nodes implementation based on build type
-const exitNodesImplementation = build === "oss" ? exitNodesModule : privateExitNodesModule;
-
-// Re-export all items from the selected implementation
-export const {
- verifyExitNodeOrgAccess,
- listExitNodes,
- selectBestExitNode,
- checkExitNodeOrg,
- resolveExitNodes
-} = exitNodesImplementation;
-
-// Import communications modules
-import * as exitNodeCommsModule from "./exitNodeComms";
-import * as privateExitNodeCommsModule from "./privateExitNodeComms";
-
-// Conditionally export communications implementation based on build type
-const exitNodeCommsImplementation = build === "oss" ? exitNodeCommsModule : privateExitNodeCommsModule;
-
-// Re-export communications functions from the selected implementation
-export const {
- sendToExitNode
-} = exitNodeCommsImplementation;
-
-// Re-export shared modules
+export * from "./exitNodes";
+export * from "./exitNodeComms";
export * from "./subnet";
export * from "./getCurrentExitNodeId";
\ No newline at end of file
diff --git a/server/lib/geoip.ts b/server/lib/geoip.ts
index d6252360..ac739fa3 100644
--- a/server/lib/geoip.ts
+++ b/server/lib/geoip.ts
@@ -1,8 +1,5 @@
import logger from "@server/logger";
import { maxmindLookup } from "@server/db/maxmind";
-import axios from "axios";
-import config from "./config";
-import { tokenManager } from "./tokenManager";
export async function getCountryCodeForIp(
ip: string
@@ -33,32 +30,4 @@ export async function getCountryCodeForIp(
}
return;
-}
-
-export async function remoteGetCountryCodeForIp(
- ip: string
-): Promise {
- try {
- const response = await axios.get(
- `${config.getRawConfig().managed?.endpoint}/api/v1/hybrid/geoip/${ip}`,
- await tokenManager.getAuthHeader()
- );
-
- return response.data.data.countryCode;
- } catch (error) {
- if (axios.isAxiosError(error)) {
- logger.error("Error fetching config in verify session:", {
- message: error.message,
- code: error.code,
- status: error.response?.status,
- statusText: error.response?.statusText,
- url: error.config?.url,
- method: error.config?.method
- });
- } else {
- logger.error("Error fetching config in verify session:", error);
- }
- }
-
- return;
-}
+}
\ No newline at end of file
diff --git a/server/lib/private/rateLimitStore.ts b/server/lib/private/rateLimitStore.ts
deleted file mode 100644
index 4700ba09..00000000
--- a/server/lib/private/rateLimitStore.ts
+++ /dev/null
@@ -1,25 +0,0 @@
-/*
- * 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 RedisStore from "@server/db/private/redisStore";
-import { MemoryStore, Store } from "express-rate-limit";
-
-export function createStore(): Store {
- const rateLimitStore: Store = new RedisStore({
- prefix: 'api-rate-limit', // Optional: customize Redis key prefix
- skipFailedRequests: true, // Don't count failed requests
- skipSuccessfulRequests: false, // Count successful requests
- });
-
- return rateLimitStore;
-}
diff --git a/server/lib/private/readConfigFile.ts b/server/lib/private/readConfigFile.ts
deleted file mode 100644
index ddc9ee82..00000000
--- a/server/lib/private/readConfigFile.ts
+++ /dev/null
@@ -1,192 +0,0 @@
-/*
- * 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 fs from "fs";
-import yaml from "js-yaml";
-import { privateConfigFilePath1 } from "@server/lib/consts";
-import { z } from "zod";
-import { colorsSchema } from "@server/lib/colorsSchema";
-import { build } from "@server/build";
-
-const portSchema = z.number().positive().gt(0).lte(65535);
-
-export const privateConfigSchema = z
- .object({
- app: z.object({
- region: z.string().optional().default("default"),
- base_domain: z.string().optional()
- }).optional().default({
- region: "default"
- }),
- server: z.object({
- encryption_key_path: z
- .string()
- .optional()
- .default("./config/encryption.pem")
- .pipe(z.string().min(8)),
- resend_api_key: z.string().optional(),
- reo_client_id: z.string().optional(),
- }).optional().default({
- encryption_key_path: "./config/encryption.pem"
- }),
- redis: z
- .object({
- host: z.string(),
- port: portSchema,
- password: z.string().optional(),
- db: z.number().int().nonnegative().optional().default(0),
- replicas: z
- .array(
- z.object({
- host: z.string(),
- port: portSchema,
- password: z.string().optional(),
- db: z.number().int().nonnegative().optional().default(0)
- })
- )
- .optional()
- // tls: z
- // .object({
- // reject_unauthorized: z
- // .boolean()
- // .optional()
- // .default(true)
- // })
- // .optional()
- })
- .optional(),
- gerbil: z
- .object({
- local_exit_node_reachable_at: z.string().optional().default("http://gerbil:3003")
- })
- .optional()
- .default({}),
- flags: z
- .object({
- enable_redis: z.boolean().optional(),
- hide_supporter_key: z.boolean().optional()
- })
- .optional(),
- branding: z
- .object({
- app_name: z.string().optional(),
- background_image_path: z.string().optional(),
- colors: z
- .object({
- light: colorsSchema.optional(),
- dark: colorsSchema.optional()
- })
- .optional(),
- logo: z
- .object({
- light_path: z.string().optional(),
- dark_path: z.string().optional(),
- auth_page: z
- .object({
- width: z.number().optional(),
- height: z.number().optional()
- })
- .optional(),
- navbar: z
- .object({
- width: z.number().optional(),
- height: z.number().optional()
- })
- .optional()
- })
- .optional(),
- favicon_path: z.string().optional(),
- footer: z
- .array(
- z.object({
- text: z.string(),
- href: z.string().optional()
- })
- )
- .optional(),
- login_page: z
- .object({
- subtitle_text: z.string().optional(),
- title_text: z.string().optional()
- })
- .optional(),
- signup_page: z
- .object({
- subtitle_text: z.string().optional(),
- title_text: z.string().optional()
- })
- .optional(),
- resource_auth_page: z
- .object({
- show_logo: z.boolean().optional(),
- hide_powered_by: z.boolean().optional(),
- title_text: z.string().optional(),
- subtitle_text: z.string().optional()
- })
- .optional(),
- emails: z
- .object({
- signature: z.string().optional(),
- colors: z
- .object({
- primary: z.string().optional()
- })
- .optional()
- })
- .optional()
- })
- .optional(),
- stripe: z
- .object({
- secret_key: z.string(),
- webhook_secret: z.string(),
- s3Bucket: z.string(),
- s3Region: z.string().default("us-east-1"),
- localFilePath: z.string()
- })
- .optional(),
- });
-
-export function readPrivateConfigFile() {
- if (build == "oss") {
- return {};
- }
-
- const loadConfig = (configPath: string) => {
- try {
- const yamlContent = fs.readFileSync(configPath, "utf8");
- const config = yaml.load(yamlContent);
- return config;
- } catch (error) {
- if (error instanceof Error) {
- throw new Error(
- `Error loading configuration file: ${error.message}`
- );
- }
- throw error;
- }
- };
-
- let environment: any;
- if (fs.existsSync(privateConfigFilePath1)) {
- environment = loadConfig(privateConfigFilePath1);
- }
-
- if (!environment) {
- throw new Error(
- "No private configuration file found."
- );
- }
-
- return environment;
-}
diff --git a/server/lib/private/s3.ts b/server/lib/private/s3.ts
deleted file mode 100644
index 26b1d49b..00000000
--- a/server/lib/private/s3.ts
+++ /dev/null
@@ -1,19 +0,0 @@
-/*
- * 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 { S3Client } from "@aws-sdk/client-s3";
-import config from "@server/lib/config";
-
-export const s3Client = new S3Client({
- region: config.getRawPrivateConfig().stripe?.s3Region || "us-east-1",
-});
diff --git a/server/lib/readConfigFile.ts b/server/lib/readConfigFile.ts
index ea872252..d614103a 100644
--- a/server/lib/readConfigFile.ts
+++ b/server/lib/readConfigFile.ts
@@ -12,42 +12,36 @@ const getEnvOrYaml = (envVar: string) => (valFromYaml: any) => {
export const configSchema = z
.object({
- app: z.object({
- dashboard_url: z
- .string()
- .url()
- .pipe(z.string().url())
- .transform((url) => url.toLowerCase())
- .optional(),
- log_level: z
- .enum(["debug", "info", "warn", "error"])
- .optional()
- .default("info"),
- save_logs: z.boolean().optional().default(false),
- log_failed_attempts: z.boolean().optional().default(false),
- telemetry: z
- .object({
- anonymous_usage: z.boolean().optional().default(true)
- })
- .optional()
- .default({}),
- }).optional().default({
- log_level: "info",
- save_logs: false,
- log_failed_attempts: false,
- telemetry: {
- anonymous_usage: true
- }
- }),
- managed: z
+ app: z
.object({
- name: z.string().optional(),
- id: z.string().optional(),
- secret: z.string().optional(),
- endpoint: z.string().optional().default("https://pangolin.fossorial.io"),
- redirect_endpoint: z.string().optional()
+ dashboard_url: z
+ .string()
+ .url()
+ .pipe(z.string().url())
+ .transform((url) => url.toLowerCase())
+ .optional(),
+ log_level: z
+ .enum(["debug", "info", "warn", "error"])
+ .optional()
+ .default("info"),
+ save_logs: z.boolean().optional().default(false),
+ log_failed_attempts: z.boolean().optional().default(false),
+ telemetry: z
+ .object({
+ anonymous_usage: z.boolean().optional().default(true)
+ })
+ .optional()
+ .default({})
})
- .optional(),
+ .optional()
+ .default({
+ log_level: "info",
+ save_logs: false,
+ log_failed_attempts: false,
+ telemetry: {
+ anonymous_usage: true
+ }
+ }),
domains: z
.record(
z.string(),
@@ -61,94 +55,95 @@ export const configSchema = z
})
)
.optional(),
- server: z.object({
- integration_port: portSchema
- .optional()
- .default(3003)
- .transform(stoi)
- .pipe(portSchema.optional()),
- external_port: portSchema
- .optional()
- .default(3000)
- .transform(stoi)
- .pipe(portSchema),
- internal_port: portSchema
- .optional()
- .default(3001)
- .transform(stoi)
- .pipe(portSchema),
- next_port: portSchema
- .optional()
- .default(3002)
- .transform(stoi)
- .pipe(portSchema),
- internal_hostname: z
- .string()
- .optional()
- .default("pangolin")
- .transform((url) => url.toLowerCase()),
- session_cookie_name: z
- .string()
- .optional()
- .default("p_session_token"),
- resource_access_token_param: z
- .string()
- .optional()
- .default("p_token"),
- resource_access_token_headers: z
- .object({
- id: z.string().optional().default("P-Access-Token-Id"),
- token: z.string().optional().default("P-Access-Token")
- })
- .optional()
- .default({}),
- resource_session_request_param: z
- .string()
- .optional()
- .default("resource_session_request_param"),
- dashboard_session_length_hours: z
- .number()
- .positive()
- .gt(0)
- .optional()
- .default(720),
- resource_session_length_hours: z
- .number()
- .positive()
- .gt(0)
- .optional()
- .default(720),
- cors: z
- .object({
- origins: z.array(z.string()).optional(),
- methods: z.array(z.string()).optional(),
- allowed_headers: z.array(z.string()).optional(),
- credentials: z.boolean().optional()
- })
- .optional(),
- trust_proxy: z.number().int().gte(0).optional().default(1),
- secret: z
- .string()
- .pipe(z.string().min(8))
- .optional(),
- maxmind_db_path: z.string().optional()
- }).optional().default({
- integration_port: 3003,
- external_port: 3000,
- internal_port: 3001,
- next_port: 3002,
- internal_hostname: "pangolin",
- session_cookie_name: "p_session_token",
- resource_access_token_param: "p_token",
- resource_access_token_headers: {
- id: "P-Access-Token-Id",
- token: "P-Access-Token"
- },
- resource_session_request_param: "resource_session_request_param",
- dashboard_session_length_hours: 720,
- resource_session_length_hours: 720,
- trust_proxy: 1
- }),
+ server: z
+ .object({
+ integration_port: portSchema
+ .optional()
+ .default(3003)
+ .transform(stoi)
+ .pipe(portSchema.optional()),
+ external_port: portSchema
+ .optional()
+ .default(3000)
+ .transform(stoi)
+ .pipe(portSchema),
+ internal_port: portSchema
+ .optional()
+ .default(3001)
+ .transform(stoi)
+ .pipe(portSchema),
+ next_port: portSchema
+ .optional()
+ .default(3002)
+ .transform(stoi)
+ .pipe(portSchema),
+ internal_hostname: z
+ .string()
+ .optional()
+ .default("pangolin")
+ .transform((url) => url.toLowerCase()),
+ session_cookie_name: z
+ .string()
+ .optional()
+ .default("p_session_token"),
+ resource_access_token_param: z
+ .string()
+ .optional()
+ .default("p_token"),
+ resource_access_token_headers: z
+ .object({
+ id: z.string().optional().default("P-Access-Token-Id"),
+ token: z.string().optional().default("P-Access-Token")
+ })
+ .optional()
+ .default({}),
+ resource_session_request_param: z
+ .string()
+ .optional()
+ .default("resource_session_request_param"),
+ dashboard_session_length_hours: z
+ .number()
+ .positive()
+ .gt(0)
+ .optional()
+ .default(720),
+ resource_session_length_hours: z
+ .number()
+ .positive()
+ .gt(0)
+ .optional()
+ .default(720),
+ cors: z
+ .object({
+ origins: z.array(z.string()).optional(),
+ methods: z.array(z.string()).optional(),
+ allowed_headers: z.array(z.string()).optional(),
+ credentials: z.boolean().optional()
+ })
+ .optional(),
+ trust_proxy: z.number().int().gte(0).optional().default(1),
+ secret: z.string().pipe(z.string().min(8)).optional(),
+ maxmind_db_path: z.string().optional()
+ })
+ .optional()
+ .default({
+ integration_port: 3003,
+ external_port: 3000,
+ internal_port: 3001,
+ next_port: 3002,
+ internal_hostname: "pangolin",
+ session_cookie_name: "p_session_token",
+ resource_access_token_param: "p_token",
+ resource_access_token_headers: {
+ id: "P-Access-Token-Id",
+ token: "P-Access-Token"
+ },
+ resource_session_request_param:
+ "resource_session_request_param",
+ dashboard_session_length_hours: 720,
+ resource_session_length_hours: 720,
+ trust_proxy: 1
+ }),
postgres: z
.object({
connection_string: z.string().optional(),
@@ -161,18 +156,29 @@ export const configSchema = z
.optional(),
pool: z
.object({
- max_connections: z.number().positive().optional().default(20),
- max_replica_connections: z.number().positive().optional().default(10),
- idle_timeout_ms: z.number().positive().optional().default(30000),
- connection_timeout_ms: z.number().positive().optional().default(5000)
+ max_connections: z
+ .number()
+ .positive()
+ .optional()
+ .default(20),
+ max_replica_connections: z
+ .number()
+ .positive()
+ .optional()
+ .default(10),
+ idle_timeout_ms: z
+ .number()
+ .positive()
+ .optional()
+ .default(30000),
+ connection_timeout_ms: z
+ .number()
+ .positive()
+ .optional()
+ .default(5000)
})
.optional()
- .default({
- max_connections: 20,
- max_replica_connections: 10,
- idle_timeout_ms: 30000,
- connection_timeout_ms: 5000
- })
+ .default({})
})
.optional(),
traefik: z
@@ -193,7 +199,10 @@ export const configSchema = z
.optional()
.default("/var/dynamic/router_config.yml"),
static_domains: z.array(z.string()).optional().default([]),
- site_types: z.array(z.string()).optional().default(["newt", "wireguard", "local"]),
+ site_types: z
+ .array(z.string())
+ .optional()
+ .default(["newt", "wireguard", "local"]),
allow_raw_resources: z.boolean().optional().default(true),
file_mode: z.boolean().optional().default(false)
})
@@ -320,10 +329,7 @@ export const configSchema = z
if (data.flags?.disable_config_managed_domains) {
return true;
}
- // If hybrid is defined, domains are not required
- if (data.managed) {
- return true;
- }
+
if (keys.length === 0) {
return false;
}
@@ -335,15 +341,14 @@ export const configSchema = z
)
.refine(
(data) => {
- // If hybrid is defined, server secret is not required
- if (data.managed) {
- return true;
- }
// If hybrid is not defined, server secret must be defined. If its not defined already then pull it from env
if (data.server?.secret === undefined) {
data.server.secret = process.env.SERVER_SECRET;
}
- return data.server?.secret !== undefined && data.server.secret.length > 0;
+ return (
+ data.server?.secret !== undefined &&
+ data.server.secret.length > 0
+ );
},
{
message: "Server secret must be defined"
@@ -351,12 +356,11 @@ export const configSchema = z
)
.refine(
(data) => {
- // If hybrid is defined, dashboard_url is not required
- if (data.managed) {
- return true;
- }
// If hybrid is not defined, dashboard_url must be defined
- return data.app.dashboard_url !== undefined && data.app.dashboard_url.length > 0;
+ return (
+ data.app.dashboard_url !== undefined &&
+ data.app.dashboard_url.length > 0
+ );
},
{
message: "Dashboard URL must be defined"
diff --git a/server/lib/remoteCertificates/certificates.ts b/server/lib/remoteCertificates/certificates.ts
deleted file mode 100644
index 6404ee75..00000000
--- a/server/lib/remoteCertificates/certificates.ts
+++ /dev/null
@@ -1,80 +0,0 @@
-import axios from "axios";
-import { tokenManager } from "../tokenManager";
-import logger from "@server/logger";
-import config from "../config";
-
-/**
- * Get valid certificates for the specified domains
- */
-export async function getValidCertificatesForDomainsHybrid(domains: Set): Promise<
- Array<{
- id: number;
- domain: string;
- wildcard: boolean | null;
- certFile: string | null;
- keyFile: string | null;
- expiresAt: number | null;
- updatedAt?: number | null;
- }>
-> {
- if (domains.size === 0) {
- return [];
- }
-
- const domainArray = Array.from(domains);
-
- try {
- const response = await axios.get(
- `${config.getRawConfig().managed?.endpoint}/api/v1/hybrid/certificates/domains`,
- {
- params: {
- domains: domainArray
- },
- headers: (await tokenManager.getAuthHeader()).headers
- }
- );
-
- if (response.status !== 200) {
- logger.error(
- `Failed to fetch certificates for domains: ${response.status} ${response.statusText}`,
- { responseData: response.data, domains: domainArray }
- );
- return [];
- }
-
- // logger.debug(
- // `Successfully retrieved ${response.data.data?.length || 0} certificates for ${domainArray.length} domains`
- // );
-
- return response.data.data;
- } catch (error) {
- // pull data out of the axios error to log
- if (axios.isAxiosError(error)) {
- logger.error("Error getting certificates:", {
- message: error.message,
- code: error.code,
- status: error.response?.status,
- statusText: error.response?.statusText,
- url: error.config?.url,
- method: error.config?.method
- });
- } else {
- logger.error("Error getting certificates:", error);
- }
- return [];
- }
-}
-
-export async function getValidCertificatesForDomains(domains: Set): Promise<
- Array<{
- id: number;
- domain: string;
- wildcard: boolean | null;
- certFile: string | null;
- keyFile: string | null;
- expiresAt: number | null;
- updatedAt?: number | null;
- }>
-> {
- return []; // stub
-}
\ No newline at end of file
diff --git a/server/lib/remoteCertificates/index.ts b/server/lib/remoteCertificates/index.ts
deleted file mode 100644
index fcd43d30..00000000
--- a/server/lib/remoteCertificates/index.ts
+++ /dev/null
@@ -1,14 +0,0 @@
-import { build } from "@server/build";
-
-// Import both modules
-import * as certificateModule from "./certificates";
-import * as privateCertificateModule from "./privateCertificates";
-
-// Conditionally export Remote Certificates implementation based on build type
-const remoteCertificatesImplementation = build === "oss" ? certificateModule : privateCertificateModule;
-
-// Re-export all items from the selected implementation
-export const {
- getValidCertificatesForDomains,
- getValidCertificatesForDomainsHybrid
- } = remoteCertificatesImplementation;
\ No newline at end of file
diff --git a/server/lib/remoteProxy.ts b/server/lib/remoteProxy.ts
deleted file mode 100644
index c9016071..00000000
--- a/server/lib/remoteProxy.ts
+++ /dev/null
@@ -1,73 +0,0 @@
-import { Request, Response, NextFunction } from "express";
-import { Router } from "express";
-import axios from "axios";
-import HttpCode from "@server/types/HttpCode";
-import createHttpError from "http-errors";
-import logger from "@server/logger";
-import config from "@server/lib/config";
-import { tokenManager } from "./tokenManager";
-
-/**
- * Proxy function that forwards requests to the remote cloud server
- */
-
-export const proxyToRemote = async (
- req: Request,
- res: Response,
- next: NextFunction,
- endpoint: string
-): Promise => {
- try {
- const remoteUrl = `${config.getRawConfig().managed?.endpoint?.replace(/\/$/, '')}/api/v1/${endpoint}`;
-
- logger.debug(`Proxying request to remote server: ${remoteUrl}`);
-
- // Forward the request to the remote server
- const response = await axios({
- method: req.method as any,
- url: remoteUrl,
- data: req.body,
- headers: {
- 'Content-Type': 'application/json',
- ...(await tokenManager.getAuthHeader()).headers
- },
- params: req.query,
- timeout: 30000, // 30 second timeout
- validateStatus: () => true // Don't throw on non-2xx status codes
- });
-
- logger.debug(`Proxy response: ${JSON.stringify(response.data)}`);
-
- // Forward the response status and data
- return res.status(response.status).json(response.data);
-
- } catch (error) {
- logger.error("Error proxying request to remote server:", error);
-
- if (axios.isAxiosError(error)) {
- if (error.code === 'ECONNREFUSED' || error.code === 'ENOTFOUND') {
- return next(
- createHttpError(
- HttpCode.SERVICE_UNAVAILABLE,
- "Remote server is unavailable"
- )
- );
- }
- if (error.code === 'ECONNABORTED') {
- return next(
- createHttpError(
- HttpCode.REQUEST_TIMEOUT,
- "Request to remote server timed out"
- )
- );
- }
- }
-
- return next(
- createHttpError(
- HttpCode.INTERNAL_SERVER_ERROR,
- "Error communicating with remote server"
- )
- );
- }
-};
\ No newline at end of file
diff --git a/server/lib/resend.ts b/server/lib/resend.ts
new file mode 100644
index 00000000..7dd130c8
--- /dev/null
+++ b/server/lib/resend.ts
@@ -0,0 +1,15 @@
+export enum AudienceIds {
+ General = "",
+ Subscribed = "",
+ Churned = ""
+}
+
+let resend;
+export default resend;
+
+export async function moveEmailToAudience(
+ email: string,
+ audienceId: AudienceIds
+) {
+ return;
+}
\ No newline at end of file
diff --git a/server/lib/s3.ts b/server/lib/s3.ts
new file mode 100644
index 00000000..5fc3318f
--- /dev/null
+++ b/server/lib/s3.ts
@@ -0,0 +1,5 @@
+import { S3Client } from "@aws-sdk/client-s3";
+
+export const s3Client = new S3Client({
+ region: process.env.S3_REGION || "us-east-1",
+});
diff --git a/server/lib/telemetry.ts b/server/lib/telemetry.ts
index 827f1c7e..0e0ae24e 100644
--- a/server/lib/telemetry.ts
+++ b/server/lib/telemetry.ts
@@ -9,6 +9,7 @@ import { APP_VERSION } from "./consts";
import crypto from "crypto";
import { UserType } from "@server/types/UserTypes";
import { build } from "@server/build";
+import license from "@server/license/license";
class TelemetryClient {
private client: PostHog | null = null;
@@ -176,17 +177,36 @@ class TelemetryClient {
const stats = await this.getSystemStats();
- this.client.capture({
- distinctId: hostMeta.hostMetaId,
- event: "supporter_status",
- properties: {
- valid: stats.supporterStatus.valid,
- tier: stats.supporterStatus.tier,
- github_username: stats.supporterStatus.githubUsername
- ? this.anon(stats.supporterStatus.githubUsername)
- : "None"
- }
- });
+ if (build === "enterprise") {
+ const licenseStatus = await license.check();
+ const payload = {
+ distinctId: hostMeta.hostMetaId,
+ event: "enterprise_status",
+ properties: {
+ is_host_licensed: licenseStatus.isHostLicensed,
+ is_license_valid: licenseStatus.isLicenseValid,
+ license_tier: licenseStatus.tier || "unknown"
+ }
+ };
+ logger.debug("Sending enterprise startup telemtry payload:", {
+ payload
+ });
+ // this.client.capture(payload);
+ }
+
+ if (build === "oss") {
+ this.client.capture({
+ distinctId: hostMeta.hostMetaId,
+ event: "supporter_status",
+ properties: {
+ valid: stats.supporterStatus.valid,
+ tier: stats.supporterStatus.tier,
+ github_username: stats.supporterStatus.githubUsername
+ ? this.anon(stats.supporterStatus.githubUsername)
+ : "None"
+ }
+ });
+ }
this.client.capture({
distinctId: hostMeta.hostMetaId,
diff --git a/server/lib/tokenManager.ts b/server/lib/tokenManager.ts
deleted file mode 100644
index 2e0e1118..00000000
--- a/server/lib/tokenManager.ts
+++ /dev/null
@@ -1,274 +0,0 @@
-import axios from "axios";
-import config from "@server/lib/config";
-import logger from "@server/logger";
-
-export interface TokenResponse {
- success: boolean;
- message?: string;
- data: {
- token: string;
- };
-}
-
-/**
- * Token Manager - Handles automatic token refresh for hybrid server authentication
- *
- * Usage throughout the application:
- * ```typescript
- * import { tokenManager } from "@server/lib/tokenManager";
- *
- * // Get the current valid token
- * const token = await tokenManager.getToken();
- *
- * // Force refresh if needed
- * await tokenManager.refreshToken();
- * ```
- *
- * The token manager automatically refreshes tokens every 24 hours by default
- * and is started once in the privateHybridServer.ts file.
- */
-
-export class TokenManager {
- private token: string | null = null;
- private refreshInterval: NodeJS.Timeout | null = null;
- private isRefreshing: boolean = false;
- private refreshIntervalMs: number;
- private retryInterval: NodeJS.Timeout | null = null;
- private retryIntervalMs: number;
- private tokenAvailablePromise: Promise | null = null;
- private tokenAvailableResolve: (() => void) | null = null;
-
- constructor(refreshIntervalMs: number = 24 * 60 * 60 * 1000, retryIntervalMs: number = 5000) {
- // Default to 24 hours for refresh, 5 seconds for retry
- this.refreshIntervalMs = refreshIntervalMs;
- this.retryIntervalMs = retryIntervalMs;
- this.setupTokenAvailablePromise();
- }
-
- /**
- * Set up promise that resolves when token becomes available
- */
- private setupTokenAvailablePromise(): void {
- this.tokenAvailablePromise = new Promise((resolve) => {
- this.tokenAvailableResolve = resolve;
- });
- }
-
- /**
- * Resolve the token available promise
- */
- private resolveTokenAvailable(): void {
- if (this.tokenAvailableResolve) {
- this.tokenAvailableResolve();
- this.tokenAvailableResolve = null;
- }
- }
-
- /**
- * Start the token manager - gets initial token and sets up refresh interval
- * If initial token fetch fails, keeps retrying every few seconds until successful
- */
- async start(): Promise {
- logger.info("Starting token manager...");
-
- try {
- await this.refreshToken();
- this.setupRefreshInterval();
- this.resolveTokenAvailable();
- logger.info("Token manager started successfully");
- } catch (error) {
- logger.warn(`Failed to get initial token, will retry in ${this.retryIntervalMs / 1000} seconds:`, error);
- this.setupRetryInterval();
- }
- }
-
- /**
- * Set up retry interval for initial token acquisition
- */
- private setupRetryInterval(): void {
- if (this.retryInterval) {
- clearInterval(this.retryInterval);
- }
-
- this.retryInterval = setInterval(async () => {
- try {
- logger.debug("Retrying initial token acquisition");
- await this.refreshToken();
- this.setupRefreshInterval();
- this.clearRetryInterval();
- this.resolveTokenAvailable();
- logger.info("Token manager started successfully after retry");
- } catch (error) {
- logger.debug("Token acquisition retry failed, will try again");
- }
- }, this.retryIntervalMs);
- }
-
- /**
- * Clear retry interval
- */
- private clearRetryInterval(): void {
- if (this.retryInterval) {
- clearInterval(this.retryInterval);
- this.retryInterval = null;
- }
- }
-
- /**
- * Stop the token manager and clear all intervals
- */
- stop(): void {
- if (this.refreshInterval) {
- clearInterval(this.refreshInterval);
- this.refreshInterval = null;
- }
- this.clearRetryInterval();
- logger.info("Token manager stopped");
- }
-
- /**
- * Get the current valid token
- */
-
- // TODO: WE SHOULD NOT BE GETTING A TOKEN EVERY TIME WE REQUEST IT
- async getToken(): Promise {
- // If we don't have a token yet, wait for it to become available
- if (!this.token && this.tokenAvailablePromise) {
- await this.tokenAvailablePromise;
- }
-
- if (!this.token) {
- if (this.isRefreshing) {
- // Wait for current refresh to complete
- await this.waitForRefresh();
- } else {
- throw new Error("No valid token available");
- }
- }
-
- if (!this.token) {
- throw new Error("No valid token available");
- }
-
- return this.token;
- }
-
- async getAuthHeader() {
- return {
- headers: {
- Authorization: `Bearer ${await this.getToken()}`,
- "X-CSRF-Token": "x-csrf-protection",
- }
- };
- }
-
- /**
- * Force refresh the token
- */
- async refreshToken(): Promise {
- if (this.isRefreshing) {
- await this.waitForRefresh();
- return;
- }
-
- this.isRefreshing = true;
-
- try {
- const hybridConfig = config.getRawConfig().managed;
-
- if (
- !hybridConfig?.id ||
- !hybridConfig?.secret ||
- !hybridConfig?.endpoint
- ) {
- throw new Error("Hybrid configuration is not defined");
- }
-
- const tokenEndpoint = `${hybridConfig.endpoint}/api/v1/auth/remoteExitNode/get-token`;
-
- const tokenData = {
- remoteExitNodeId: hybridConfig.id,
- secret: hybridConfig.secret
- };
-
- logger.debug("Requesting new token from server");
-
- const response = await axios.post(
- tokenEndpoint,
- tokenData,
- {
- headers: {
- "Content-Type": "application/json",
- "X-CSRF-Token": "x-csrf-protection"
- },
- timeout: 10000 // 10 second timeout
- }
- );
-
- if (!response.data.success) {
- throw new Error(
- `Failed to get token: ${response.data.message}`
- );
- }
-
- if (!response.data.data.token) {
- throw new Error("Received empty token from server");
- }
-
- this.token = response.data.data.token;
- logger.debug("Token refreshed successfully");
- } catch (error) {
- if (axios.isAxiosError(error)) {
- logger.error("Error updating proxy mapping:", {
- message: error.message,
- code: error.code,
- status: error.response?.status,
- statusText: error.response?.statusText,
- url: error.config?.url,
- method: error.config?.method
- });
- } else {
- logger.error("Error updating proxy mapping:", error);
- }
-
- throw new Error("Failed to refresh token");
- } finally {
- this.isRefreshing = false;
- }
- }
-
- /**
- * Set up automatic token refresh interval
- */
- private setupRefreshInterval(): void {
- if (this.refreshInterval) {
- clearInterval(this.refreshInterval);
- }
-
- this.refreshInterval = setInterval(async () => {
- try {
- logger.debug("Auto-refreshing token");
- await this.refreshToken();
- } catch (error) {
- logger.error("Failed to auto-refresh token:", error);
- }
- }, this.refreshIntervalMs);
- }
-
- /**
- * Wait for current refresh operation to complete
- */
- private async waitForRefresh(): Promise {
- return new Promise((resolve) => {
- const checkInterval = setInterval(() => {
- if (!this.isRefreshing) {
- clearInterval(checkInterval);
- resolve();
- }
- }, 100);
- });
- }
-}
-
-// Export a singleton instance for use throughout the application
-export const tokenManager = new TokenManager();
diff --git a/server/lib/traefik/TraefikConfigManager.ts b/server/lib/traefik/TraefikConfigManager.ts
index 51466ecf..ec4e25f4 100644
--- a/server/lib/traefik/TraefikConfigManager.ts
+++ b/server/lib/traefik/TraefikConfigManager.ts
@@ -6,14 +6,10 @@ import * as yaml from "js-yaml";
import axios from "axios";
import { db, exitNodes } from "@server/db";
import { eq } from "drizzle-orm";
-import { tokenManager } from "../tokenManager";
import { getCurrentExitNodeId } from "@server/lib/exitNodes";
-import { getTraefikConfig } from "./";
-import {
- getValidCertificatesForDomains,
- getValidCertificatesForDomainsHybrid
-} from "../remoteCertificates";
-import { sendToExitNode } from "../exitNodes";
+import { getTraefikConfig } from "#dynamic/lib/traefik";
+import { getValidCertificatesForDomains } from "#dynamic/lib/certificates";
+import { sendToExitNode } from "#dynamic/lib/exitNodes";
import { build } from "@server/build";
export class TraefikConfigManager {
@@ -313,93 +309,92 @@ export class TraefikConfigManager {
this.lastActiveDomains = new Set(domains);
}
- // Scan current local certificate state
- this.lastLocalCertificateState =
- await this.scanLocalCertificateState();
+ if (
+ process.env.USE_PANGOLIN_DNS === "true" &&
+ build != "oss"
+ ) {
+ // Scan current local certificate state
+ this.lastLocalCertificateState =
+ await this.scanLocalCertificateState();
- // Only fetch certificates if needed (domain changes, missing certs, or daily renewal check)
- let validCertificates: Array<{
- id: number;
- domain: string;
- wildcard: boolean | null;
- certFile: string | null;
- keyFile: string | null;
- expiresAt: number | null;
- updatedAt?: number | null;
- }> = [];
+ // Only fetch certificates if needed (domain changes, missing certs, or daily renewal check)
+ let validCertificates: Array<{
+ id: number;
+ domain: string;
+ wildcard: boolean | null;
+ certFile: string | null;
+ keyFile: string | null;
+ expiresAt: number | null;
+ updatedAt?: number | null;
+ }> = [];
- if (this.shouldFetchCertificates(domains)) {
- // Filter out domains that are already covered by wildcard certificates
- const domainsToFetch = new Set();
- for (const domain of domains) {
- if (
- !isDomainCoveredByWildcard(
- domain,
- this.lastLocalCertificateState
- )
- ) {
- domainsToFetch.add(domain);
- } else {
- logger.debug(
- `Domain ${domain} is covered by existing wildcard certificate, skipping fetch`
- );
- }
- }
-
- if (domainsToFetch.size > 0) {
- // Get valid certificates for domains not covered by wildcards
- if (config.isManagedMode()) {
- validCertificates =
- await getValidCertificatesForDomainsHybrid(
- domainsToFetch
+ if (this.shouldFetchCertificates(domains)) {
+ // Filter out domains that are already covered by wildcard certificates
+ const domainsToFetch = new Set();
+ for (const domain of domains) {
+ if (
+ !isDomainCoveredByWildcard(
+ domain,
+ this.lastLocalCertificateState
+ )
+ ) {
+ domainsToFetch.add(domain);
+ } else {
+ logger.debug(
+ `Domain ${domain} is covered by existing wildcard certificate, skipping fetch`
);
- } else {
+ }
+ }
+
+ if (domainsToFetch.size > 0) {
+ // Get valid certificates for domains not covered by wildcards
validCertificates =
await getValidCertificatesForDomains(
domainsToFetch
);
+ this.lastCertificateFetch = new Date();
+ this.lastKnownDomains = new Set(domains);
+
+ logger.info(
+ `Fetched ${validCertificates.length} certificates from remote (${domains.size - domainsToFetch.size} domains covered by wildcards)`
+ );
+
+ // Download and decrypt new certificates
+ await this.processValidCertificates(validCertificates);
+ } else {
+ logger.info(
+ "All domains are covered by existing wildcard certificates, no fetch needed"
+ );
+ this.lastCertificateFetch = new Date();
+ this.lastKnownDomains = new Set(domains);
}
- this.lastCertificateFetch = new Date();
- this.lastKnownDomains = new Set(domains);
- logger.info(
- `Fetched ${validCertificates.length} certificates from remote (${domains.size - domainsToFetch.size} domains covered by wildcards)`
- );
-
- // Download and decrypt new certificates
- await this.processValidCertificates(validCertificates);
+ // Always ensure all existing certificates (including wildcards) are in the config
+ await this.updateDynamicConfigFromLocalCerts(domains);
} else {
- logger.info(
- "All domains are covered by existing wildcard certificates, no fetch needed"
- );
- this.lastCertificateFetch = new Date();
- this.lastKnownDomains = new Set(domains);
+ const timeSinceLastFetch = this.lastCertificateFetch
+ ? Math.round(
+ (Date.now() -
+ this.lastCertificateFetch.getTime()) /
+ (1000 * 60)
+ )
+ : 0;
+
+ // logger.debug(
+ // `Skipping certificate fetch - no changes detected and within 24-hour window (last fetch: ${timeSinceLastFetch} minutes ago)`
+ // );
+
+ // Still need to ensure config is up to date with existing certificates
+ await this.updateDynamicConfigFromLocalCerts(domains);
}
- // Always ensure all existing certificates (including wildcards) are in the config
- await this.updateDynamicConfigFromLocalCerts(domains);
- } else {
- const timeSinceLastFetch = this.lastCertificateFetch
- ? Math.round(
- (Date.now() - this.lastCertificateFetch.getTime()) /
- (1000 * 60)
- )
- : 0;
+ // Clean up certificates for domains no longer in use
+ await this.cleanupUnusedCertificates(domains);
- // logger.debug(
- // `Skipping certificate fetch - no changes detected and within 24-hour window (last fetch: ${timeSinceLastFetch} minutes ago)`
- // );
-
- // Still need to ensure config is up to date with existing certificates
- await this.updateDynamicConfigFromLocalCerts(domains);
+ // wait 1 second for traefik to pick up the new certificates
+ await new Promise((resolve) => setTimeout(resolve, 500));
}
- // Clean up certificates for domains no longer in use
- await this.cleanupUnusedCertificates(domains);
-
- // wait 1 second for traefik to pick up the new certificates
- await new Promise((resolve) => setTimeout(resolve, 500));
-
// Write traefik config as YAML to a second dynamic config file if changed
await this.writeTraefikDynamicConfig(traefikConfig);
@@ -448,32 +443,15 @@ export class TraefikConfigManager {
} | null> {
let traefikConfig;
try {
- if (config.isManagedMode()) {
- const resp = await axios.get(
- `${config.getRawConfig().managed?.endpoint}/api/v1/hybrid/traefik-config`,
- await tokenManager.getAuthHeader()
- );
-
- if (resp.status !== 200) {
- logger.error(
- `Failed to fetch traefik config: ${resp.status} ${resp.statusText}`,
- { responseData: resp.data }
- );
- return null;
- }
-
- traefikConfig = resp.data.data;
- } else {
- const currentExitNode = await getCurrentExitNodeId();
- // logger.debug(`Fetching traefik config for exit node: ${currentExitNode}`);
- traefikConfig = await getTraefikConfig(
- // this is called by the local exit node to get its own config
- currentExitNode,
- config.getRawConfig().traefik.site_types,
- build == "oss", // filter out the namespace domains in open source
- build != "oss" // generate the login pages on the cloud and hybrid
- );
- }
+ const currentExitNode = await getCurrentExitNodeId();
+ // logger.debug(`Fetching traefik config for exit node: ${currentExitNode}`);
+ traefikConfig = await getTraefikConfig(
+ // this is called by the local exit node to get its own config
+ currentExitNode,
+ config.getRawConfig().traefik.site_types,
+ build == "oss", // filter out the namespace domains in open source
+ build != "oss" // generate the login pages on the cloud and hybrid
+ );
const domains = new Set();
@@ -718,7 +696,12 @@ export class TraefikConfigManager {
for (const cert of validCertificates) {
try {
- if (!cert.certFile || !cert.keyFile) {
+ if (
+ !cert.certFile ||
+ !cert.keyFile ||
+ cert.certFile.length === 0 ||
+ cert.keyFile.length === 0
+ ) {
logger.warn(
`Certificate for domain ${cert.domain} is missing cert or key file`
);
@@ -842,7 +825,9 @@ export class TraefikConfigManager {
const lastUpdateStr = fs
.readFileSync(lastUpdatePath, "utf8")
.trim();
- lastUpdateTime = Math.floor(new Date(lastUpdateStr).getTime() / 1000);
+ lastUpdateTime = Math.floor(
+ new Date(lastUpdateStr).getTime() / 1000
+ );
} catch {
lastUpdateTime = null;
}
diff --git a/server/lib/traefik/getTraefikConfig.ts b/server/lib/traefik/getTraefikConfig.ts
index 7e1ce562..4055c7a0 100644
--- a/server/lib/traefik/getTraefikConfig.ts
+++ b/server/lib/traefik/getTraefikConfig.ts
@@ -1,9 +1,8 @@
-import { db, exitNodes, targetHealthCheck } from "@server/db";
+import { db, targetHealthCheck } from "@server/db";
import { and, eq, inArray, or, isNull, ne, isNotNull, desc } from "drizzle-orm";
import logger from "@server/logger";
import config from "@server/lib/config";
-import { orgs, resources, sites, Target, targets } from "@server/db";
-import { build } from "@server/build";
+import { resources, sites, Target, targets } from "@server/db";
import createPathRewriteMiddleware from "./middleware";
import { sanitize, validatePathRewriteConfig } from "./utils";
@@ -105,7 +104,12 @@ export async function getTraefikConfig(
const priority = row.priority ?? 100;
// Create a unique key combining resourceId, path config, and rewrite config
- const pathKey = [targetPath, pathMatchType, rewritePath, rewritePathType]
+ const pathKey = [
+ targetPath,
+ pathMatchType,
+ rewritePath,
+ rewritePathType
+ ]
.filter(Boolean)
.join("-");
const mapKey = [resourceId, pathKey].filter(Boolean).join("-");
@@ -120,13 +124,15 @@ export async function getTraefikConfig(
);
if (!validation.isValid) {
- logger.error(`Invalid path rewrite configuration for resource ${resourceId}: ${validation.error}`);
+ logger.error(
+ `Invalid path rewrite configuration for resource ${resourceId}: ${validation.error}`
+ );
return;
}
resourcesMap.set(key, {
resourceId: row.resourceId,
- name: resourceName,
+ name: resourceName,
fullDomain: row.fullDomain,
ssl: row.ssl,
http: row.http,
@@ -158,9 +164,6 @@ export async function getTraefikConfig(
port: row.port,
internalPort: row.internalPort,
enabled: row.targetEnabled,
- rewritePath: row.rewritePath,
- rewritePathType: row.rewritePathType,
- priority: row.priority,
site: {
siteId: row.siteId,
type: row.siteType,
@@ -239,21 +242,18 @@ export async function getTraefikConfig(
preferWildcardCert = configDomain.prefer_wildcard_cert;
}
- let tls = {};
- if (build == "oss") {
- tls = {
- certResolver: certResolver,
- ...(preferWildcardCert
- ? {
- domains: [
- {
- main: wildCard
- }
- ]
- }
- : {})
- };
- }
+ const tls = {
+ certResolver: certResolver,
+ ...(preferWildcardCert
+ ? {
+ domains: [
+ {
+ main: wildCard
+ }
+ ]
+ }
+ : {})
+ };
const additionalMiddlewares =
config.getRawConfig().traefik.additional_middlewares || [];
@@ -264,11 +264,12 @@ export async function getTraefikConfig(
];
// Handle path rewriting middleware
- if (resource.rewritePath &&
- resource.path &&
+ if (
+ resource.rewritePath !== null &&
+ resource.path !== null &&
resource.pathMatchType &&
- resource.rewritePathType) {
-
+ resource.rewritePathType
+ ) {
// Create a unique middleware name
const rewriteMiddlewareName = `rewrite-r${resource.resourceId}-${key}`;
@@ -287,7 +288,10 @@ export async function getTraefikConfig(
}
// the middleware to the config
- Object.assign(config_output.http.middlewares, rewriteResult.middlewares);
+ Object.assign(
+ config_output.http.middlewares,
+ rewriteResult.middlewares
+ );
// middlewares to the router middleware chain
if (rewriteResult.chain) {
@@ -298,9 +302,13 @@ export async function getTraefikConfig(
routerMiddlewares.push(rewriteMiddlewareName);
}
- logger.debug(`Created path rewrite middleware ${rewriteMiddlewareName}: ${resource.pathMatchType}(${resource.path}) -> ${resource.rewritePathType}(${resource.rewritePath})`);
+ logger.debug(
+ `Created path rewrite middleware ${rewriteMiddlewareName}: ${resource.pathMatchType}(${resource.path}) -> ${resource.rewritePathType}(${resource.rewritePath})`
+ );
} catch (error) {
- logger.error(`Failed to create path rewrite middleware for resource ${resource.resourceId}: ${error}`);
+ logger.error(
+ `Failed to create path rewrite middleware for resource ${resource.resourceId}: ${error}`
+ );
}
}
@@ -316,7 +324,9 @@ export async function getTraefikConfig(
value: string;
}[];
} catch (e) {
- logger.warn(`Failed to parse headers for resource ${resource.resourceId}: ${e}`);
+ logger.warn(
+ `Failed to parse headers for resource ${resource.resourceId}: ${e}`
+ );
}
headersArr.forEach((header) => {
@@ -482,14 +492,14 @@ export async function getTraefikConfig(
})(),
...(resource.stickySession
? {
- sticky: {
- cookie: {
- name: "p_sticky", // TODO: make this configurable via config.yml like other cookies
- secure: resource.ssl,
- httpOnly: true
- }
- }
- }
+ sticky: {
+ cookie: {
+ name: "p_sticky", // TODO: make this configurable via config.yml like other cookies
+ secure: resource.ssl,
+ httpOnly: true
+ }
+ }
+ }
: {})
}
};
@@ -590,13 +600,13 @@ export async function getTraefikConfig(
})(),
...(resource.stickySession
? {
- sticky: {
- ipStrategy: {
- depth: 0,
- sourcePort: true
- }
- }
- }
+ sticky: {
+ ipStrategy: {
+ depth: 0,
+ sourcePort: true
+ }
+ }
+ }
: {})
}
};
diff --git a/server/lib/traefik/index.ts b/server/lib/traefik/index.ts
index 1d654510..5630028c 100644
--- a/server/lib/traefik/index.ts
+++ b/server/lib/traefik/index.ts
@@ -1,11 +1 @@
-import { build } from "@server/build";
-
-// Import both modules
-import * as traefikModule from "./getTraefikConfig";
-import * as privateTraefikModule from "./privateGetTraefikConfig";
-
-// Conditionally export Traefik configuration implementation based on build type
-const traefikImplementation = build === "oss" ? traefikModule : privateTraefikModule;
-
-// Re-export all items from the selected implementation
-export const { getTraefikConfig } = traefikImplementation;
\ No newline at end of file
+export * from "./getTraefikConfig";
\ No newline at end of file
diff --git a/server/license/license.ts b/server/license/license.ts
index aeb628df..919fdb03 100644
--- a/server/license/license.ts
+++ b/server/license/license.ts
@@ -1,26 +1,17 @@
-import { db } from "@server/db";
-import { hostMeta, licenseKey, sites } from "@server/db";
-import logger from "@server/logger";
-import NodeCache from "node-cache";
-import { validateJWT } from "./licenseJwt";
-import { count, eq } from "drizzle-orm";
-import moment from "moment";
+import { db, hostMeta, HostMeta } from "@server/db";
import { setHostMeta } from "@server/lib/hostMeta";
-import { encrypt, decrypt } from "@server/lib/crypto";
-const keyTypes = ["HOST", "SITES"] as const;
-type KeyType = (typeof keyTypes)[number];
+const keyTypes = ["host"] as const;
+export type LicenseKeyType = (typeof keyTypes)[number];
-const keyTiers = ["PROFESSIONAL", "ENTERPRISE"] as const;
-type KeyTier = (typeof keyTiers)[number];
+const keyTiers = ["personal", "enterprise"] as const;
+export type LicenseKeyTier = (typeof keyTiers)[number];
export type LicenseStatus = {
isHostLicensed: boolean; // Are there any license keys?
isLicenseValid: boolean; // Is the license key valid?
hostId: string; // Host ID
- maxSites?: number;
- usedSites?: number;
- tier?: KeyTier;
+ tier?: LicenseKeyTier;
};
export type LicenseKeyCache = {
@@ -28,451 +19,27 @@ export type LicenseKeyCache = {
licenseKeyEncrypted: string;
valid: boolean;
iat?: Date;
- type?: KeyType;
- tier?: KeyTier;
- numSites?: number;
-};
-
-type ActivateLicenseKeyAPIResponse = {
- data: {
- instanceId: string;
- };
- success: boolean;
- error: string;
- message: string;
- status: number;
-};
-
-type ValidateLicenseAPIResponse = {
- data: {
- licenseKeys: {
- [key: string]: string;
- };
- };
- success: boolean;
- error: string;
- message: string;
- status: number;
-};
-
-type TokenPayload = {
- valid: boolean;
- type: KeyType;
- tier: KeyTier;
- quantity: number;
- terminateAt: string; // ISO
- iat: number; // Issued at
+ type?: LicenseKeyType;
+ tier?: LicenseKeyTier;
+ terminateAt?: Date;
};
export class License {
- private phoneHomeInterval = 6 * 60 * 60; // 6 hours = 6 * 60 * 60 = 21600 seconds
- private validationServerUrl =
- "https://api.fossorial.io/api/v1/license/professional/validate";
- private activationServerUrl =
- "https://api.fossorial.io/api/v1/license/professional/activate";
-
- private statusCache = new NodeCache({ stdTTL: this.phoneHomeInterval });
- private licenseKeyCache = new NodeCache();
-
- private ephemeralKey!: string;
- private statusKey = "status";
private serverSecret!: string;
- private publicKey = `-----BEGIN PUBLIC KEY-----
-MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAx9RKc8cw+G8r7h/xeozF
-FNkRDggQfYO6Ae+EWHGujZ9WYAZ10spLh9F/zoLhhr3XhsjpoRXwMfgNuO5HstWf
-CYM20I0l7EUUMWEyWd4tZLd+5XQ4jY5xWOCWyFJAGQSp7flcRmxdfde+l+xg9eKl
-apbY84aVp09/GqM96hCS+CsQZrhohu/aOqYVB/eAhF01qsbmiZ7Y3WtdhTldveYt
-h4mZWGmjf8d/aEgePf/tk1gp0BUxf+Ae5yqoAqU+6aiFbjJ7q1kgxc18PWFGfE9y
-zSk+OZk887N5ThQ52154+oOUCMMR2Y3t5OH1hVZod51vuY2u5LsQXsf+87PwB91y
-LQIDAQAB
------END PUBLIC KEY-----`;
+ constructor(private hostMeta: HostMeta) {}
- constructor(private hostId: string) {
- this.ephemeralKey = Buffer.from(
- JSON.stringify({ ts: new Date().toISOString() })
- ).toString("base64");
-
- setInterval(
- async () => {
- await this.check();
- },
- 1000 * 60 * 60
- ); // 1 hour = 60 * 60 = 3600 seconds
- }
-
- public listKeys(): LicenseKeyCache[] {
- const keys = this.licenseKeyCache.keys();
- return keys.map((key) => {
- return this.licenseKeyCache.get(key)!;
- });
+ public async check(): Promise {
+ return {
+ hostId: this.hostMeta.hostMetaId,
+ isHostLicensed: false,
+ isLicenseValid: false
+ };
}
public setServerSecret(secret: string) {
this.serverSecret = secret;
}
-
- public async forceRecheck() {
- this.statusCache.flushAll();
- this.licenseKeyCache.flushAll();
-
- return await this.check();
- }
-
- public async isUnlocked(): Promise {
- const status = await this.check();
- if (status.isHostLicensed) {
- if (status.isLicenseValid) {
- return true;
- }
- }
- return false;
- }
-
- public async check(): Promise {
- // Set used sites
- const [siteCount] = await db
- .select({
- value: count()
- })
- .from(sites);
-
- const status: LicenseStatus = {
- hostId: this.hostId,
- isHostLicensed: true,
- isLicenseValid: false,
- maxSites: undefined,
- usedSites: siteCount.value
- };
-
- try {
- if (this.statusCache.has(this.statusKey)) {
- const res = this.statusCache.get("status") as LicenseStatus;
- res.usedSites = status.usedSites;
- return res;
- }
-
- // Invalidate all
- this.licenseKeyCache.flushAll();
-
- const allKeysRes = await db.select().from(licenseKey);
-
- if (allKeysRes.length === 0) {
- status.isHostLicensed = false;
- return status;
- }
-
- let foundHostKey = false;
- // Validate stored license keys
- for (const key of allKeysRes) {
- try {
- // Decrypt the license key and token
- const decryptedKey = decrypt(
- key.licenseKeyId,
- this.serverSecret
- );
- const decryptedToken = decrypt(
- key.token,
- this.serverSecret
- );
-
- const payload = validateJWT(
- decryptedToken,
- this.publicKey
- );
-
- this.licenseKeyCache.set(decryptedKey, {
- licenseKey: decryptedKey,
- licenseKeyEncrypted: key.licenseKeyId,
- valid: payload.valid,
- type: payload.type,
- tier: payload.tier,
- numSites: payload.quantity,
- iat: new Date(payload.iat * 1000)
- });
-
- if (payload.type === "HOST") {
- foundHostKey = true;
- }
- } catch (e) {
- logger.error(
- `Error validating license key: ${key.licenseKeyId}`
- );
- logger.error(e);
-
- this.licenseKeyCache.set(
- key.licenseKeyId,
- {
- licenseKey: key.licenseKeyId,
- licenseKeyEncrypted: key.licenseKeyId,
- valid: false
- }
- );
- }
- }
-
- if (!foundHostKey && allKeysRes.length) {
- logger.debug("No host license key found");
- status.isHostLicensed = false;
- }
-
- const keys = allKeysRes.map((key) => ({
- licenseKey: decrypt(key.licenseKeyId, this.serverSecret),
- instanceId: decrypt(key.instanceId, this.serverSecret)
- }));
-
- let apiResponse: ValidateLicenseAPIResponse | undefined;
- try {
- // Phone home to validate license keys
- apiResponse = await this.phoneHome(keys);
-
- if (!apiResponse?.success) {
- throw new Error(apiResponse?.error);
- }
- } catch (e) {
- logger.error("Error communicating with license server:");
- logger.error(e);
- }
-
- logger.debug("Validate response", apiResponse);
-
- // Check and update all license keys with server response
- for (const key of keys) {
- try {
- const cached = this.licenseKeyCache.get(
- key.licenseKey
- )!;
- const licenseKeyRes =
- apiResponse?.data?.licenseKeys[key.licenseKey];
-
- if (!apiResponse || !licenseKeyRes) {
- logger.debug(
- `No response from server for license key: ${key.licenseKey}`
- );
- if (cached.iat) {
- const exp = moment(cached.iat)
- .add(7, "days")
- .toDate();
- if (exp > new Date()) {
- logger.debug(
- `Using cached license key: ${key.licenseKey}, valid ${cached.valid}`
- );
- continue;
- }
- }
-
- logger.debug(
- `Can't trust license key: ${key.licenseKey}`
- );
- cached.valid = false;
- this.licenseKeyCache.set(
- key.licenseKey,
- cached
- );
- continue;
- }
-
- const payload = validateJWT(
- licenseKeyRes,
- this.publicKey
- );
- cached.valid = payload.valid;
- cached.type = payload.type;
- cached.tier = payload.tier;
- cached.numSites = payload.quantity;
- cached.iat = new Date(payload.iat * 1000);
-
- // Encrypt the updated token before storing
- const encryptedKey = encrypt(
- key.licenseKey,
- this.serverSecret
- );
- const encryptedToken = encrypt(
- licenseKeyRes,
- this.serverSecret
- );
-
- await db
- .update(licenseKey)
- .set({
- token: encryptedToken
- })
- .where(eq(licenseKey.licenseKeyId, encryptedKey));
-
- this.licenseKeyCache.set(
- key.licenseKey,
- cached
- );
- } catch (e) {
- logger.error(`Error validating license key: ${key}`);
- logger.error(e);
- }
- }
-
- // Compute host status
- for (const key of keys) {
- const cached = this.licenseKeyCache.get(
- key.licenseKey
- )!;
-
- logger.debug("Checking key", cached);
-
- if (cached.type === "HOST") {
- status.isLicenseValid = cached.valid;
- status.tier = cached.tier;
- }
-
- if (!cached.valid) {
- continue;
- }
-
- if (!status.maxSites) {
- status.maxSites = 0;
- }
-
- status.maxSites += cached.numSites || 0;
- }
- } catch (error) {
- logger.error("Error checking license status:");
- logger.error(error);
- }
-
- this.statusCache.set(this.statusKey, status);
- return status;
- }
-
- public async activateLicenseKey(key: string) {
- // Encrypt the license key before storing
- const encryptedKey = encrypt(key, this.serverSecret);
-
- const [existingKey] = await db
- .select()
- .from(licenseKey)
- .where(eq(licenseKey.licenseKeyId, encryptedKey))
- .limit(1);
-
- if (existingKey) {
- throw new Error("License key already exists");
- }
-
- let instanceId: string | undefined;
- try {
- // Call activate
- const apiResponse = await fetch(this.activationServerUrl, {
- method: "POST",
- headers: {
- "Content-Type": "application/json"
- },
- body: JSON.stringify({
- licenseKey: key,
- instanceName: this.hostId
- })
- });
-
- const data = await apiResponse.json();
-
- if (!data.success) {
- throw new Error(`${data.message || data.error}`);
- }
-
- const response = data as ActivateLicenseKeyAPIResponse;
-
- if (!response.data) {
- throw new Error("No response from server");
- }
-
- if (!response.data.instanceId) {
- throw new Error("No instance ID in response");
- }
-
- instanceId = response.data.instanceId;
- } catch (error) {
- throw Error(`Error activating license key: ${error}`);
- }
-
- // Phone home to validate license key
- const keys = [
- {
- licenseKey: key,
- instanceId: instanceId!
- }
- ];
-
- let validateResponse: ValidateLicenseAPIResponse;
- try {
- validateResponse = await this.phoneHome(keys);
-
- if (!validateResponse) {
- throw new Error("No response from server");
- }
-
- if (!validateResponse.success) {
- throw new Error(validateResponse.error);
- }
-
- // Validate the license key
- const licenseKeyRes = validateResponse.data.licenseKeys[key];
- if (!licenseKeyRes) {
- throw new Error("Invalid license key");
- }
-
- const payload = validateJWT(
- licenseKeyRes,
- this.publicKey
- );
-
- if (!payload.valid) {
- throw new Error("Invalid license key");
- }
-
- const encryptedToken = encrypt(licenseKeyRes, this.serverSecret);
- // Encrypt the instanceId before storing
- const encryptedInstanceId = encrypt(instanceId!, this.serverSecret);
-
- // Store the license key in the database
- await db.insert(licenseKey).values({
- licenseKeyId: encryptedKey,
- token: encryptedToken,
- instanceId: encryptedInstanceId
- });
- } catch (error) {
- throw Error(`Error validating license key: ${error}`);
- }
-
- // Invalidate the cache and re-compute the status
- return await this.forceRecheck();
- }
-
- private async phoneHome(
- keys: {
- licenseKey: string;
- instanceId: string;
- }[]
- ): Promise {
- // Decrypt the instanceIds before sending to the server
- const decryptedKeys = keys.map((key) => ({
- licenseKey: key.licenseKey,
- instanceId: key.instanceId
- ? decrypt(key.instanceId, this.serverSecret)
- : key.instanceId
- }));
-
- const response = await fetch(this.validationServerUrl, {
- method: "POST",
- headers: {
- "Content-Type": "application/json"
- },
- body: JSON.stringify({
- licenseKeys: decryptedKeys,
- ephemeralKey: this.ephemeralKey,
- instanceName: this.hostId
- })
- });
-
- const data = await response.json();
-
- return data as ValidateLicenseAPIResponse;
- }
}
await setHostMeta();
@@ -483,6 +50,6 @@ if (!info) {
throw new Error("Host information not found");
}
-export const license = new License(info.hostMetaId);
+export const license = new License(info);
export default license;
diff --git a/server/middlewares/index.ts b/server/middlewares/index.ts
index f211fa9e..629cafe9 100644
--- a/server/middlewares/index.ts
+++ b/server/middlewares/index.ts
@@ -21,10 +21,9 @@ export * from "./verifyIsLoggedInUser";
export * from "./verifyIsLoggedInUser";
export * from "./verifyClientAccess";
export * from "./integration";
-export * from "./verifyValidLicense";
export * from "./verifyUserHasAction";
export * from "./verifyApiKeyAccess";
export * from "./verifyDomainAccess";
export * from "./verifyClientsEnabled";
export * from "./verifyUserIsOrgOwner";
-export * from "./verifySiteResourceAccess";
\ No newline at end of file
+export * from "./verifySiteResourceAccess";
diff --git a/server/auth/sessions/privateRemoteExitNode.ts b/server/private/auth/sessions/remoteExitNode.ts
similarity index 100%
rename from server/auth/sessions/privateRemoteExitNode.ts
rename to server/private/auth/sessions/remoteExitNode.ts
diff --git a/server/private/cleanup.ts b/server/private/cleanup.ts
new file mode 100644
index 00000000..8bf5ea3d
--- /dev/null
+++ b/server/private/cleanup.ts
@@ -0,0 +1,28 @@
+/*
+ * 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 { rateLimitService } from "#private/lib/rateLimit";
+import { cleanup as wsCleanup } from "#private/routers/ws";
+
+async function cleanup() {
+ await rateLimitService.cleanup();
+ await wsCleanup();
+
+ process.exit(0);
+}
+
+export async function initCleanup() {
+ // Handle process termination
+ process.on("SIGTERM", () => cleanup());
+ process.on("SIGINT", () => cleanup());
+}
\ No newline at end of file
diff --git a/server/routers/private/billing/createCustomer.ts b/server/private/lib/billing/createCustomer.ts
similarity index 96%
rename from server/routers/private/billing/createCustomer.ts
rename to server/private/lib/billing/createCustomer.ts
index d1c08a0e..52c72c53 100644
--- a/server/routers/private/billing/createCustomer.ts
+++ b/server/private/lib/billing/createCustomer.ts
@@ -13,7 +13,7 @@
import { customers, db } from "@server/db";
import { eq } from "drizzle-orm";
-import stripe from "@server/lib/private/stripe";
+import stripe from "#private/lib/stripe";
import { build } from "@server/build";
export async function createCustomer(
diff --git a/server/private/lib/billing/getOrgTierData.ts b/server/private/lib/billing/getOrgTierData.ts
new file mode 100644
index 00000000..fbfb5cb0
--- /dev/null
+++ b/server/private/lib/billing/getOrgTierData.ts
@@ -0,0 +1,46 @@
+/*
+ * 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 { getTierPriceSet } from "@server/lib/billing/tiers";
+import { getOrgSubscriptionData } from "#private/routers/billing/getOrgSubscription";
+import { build } from "@server/build";
+
+export async function getOrgTierData(
+ orgId: string
+): Promise<{ tier: string | null; active: boolean }> {
+ let tier = null;
+ let active = false;
+
+ if (build !== "saas") {
+ return { tier, active };
+ }
+
+ const { subscription, items } = await getOrgSubscriptionData(orgId);
+
+ if (items && items.length > 0) {
+ const tierPriceSet = getTierPriceSet();
+ // Iterate through tiers in order (earlier keys are higher tiers)
+ for (const [tierId, priceId] of Object.entries(tierPriceSet)) {
+ // Check if any subscription item matches this tier's price ID
+ const matchingItem = items.find((item) => item.priceId === priceId);
+ if (matchingItem) {
+ tier = tierId;
+ break;
+ }
+ }
+ }
+ if (subscription && subscription.status === "active") {
+ active = true;
+ }
+ return { tier, active };
+}
diff --git a/server/lib/private/billing/index.ts b/server/private/lib/billing/index.ts
similarity index 81%
rename from server/lib/private/billing/index.ts
rename to server/private/lib/billing/index.ts
index 0212ee1c..13ca3761 100644
--- a/server/lib/private/billing/index.ts
+++ b/server/private/lib/billing/index.ts
@@ -11,6 +11,5 @@
* This file is not licensed under the AGPLv3.
*/
-export * from "./limitSet";
-export * from "./features";
-export * from "./limitsService";
+export * from "./getOrgTierData";
+export * from "./createCustomer";
\ No newline at end of file
diff --git a/server/lib/remoteCertificates/privateCertificates.ts b/server/private/lib/certificates.ts
similarity index 87%
rename from server/lib/remoteCertificates/privateCertificates.ts
rename to server/private/lib/certificates.ts
index fabc9ea5..93eb5603 100644
--- a/server/lib/remoteCertificates/privateCertificates.ts
+++ b/server/private/lib/certificates.ts
@@ -11,10 +11,10 @@
* This file is not licensed under the AGPLv3.
*/
-import config from "../config";
+import config from "./config";
import { certificates, db } from "@server/db";
import { and, eq, isNotNull } from "drizzle-orm";
-import { decryptData } from "../encryption";
+import { decryptData } from "@server/lib/encryption";
import * as fs from "fs";
export async function getValidCertificatesForDomains(
@@ -97,20 +97,4 @@ export async function getValidCertificatesForDomains(
});
return validCertsDecrypted;
-}
-
-export async function getValidCertificatesForDomainsHybrid(
- domains: Set
-): Promise<
- Array<{
- id: number;
- domain: string;
- wildcard: boolean | null;
- certFile: string | null;
- keyFile: string | null;
- expiresAt: number | null;
- updatedAt?: number | null;
- }>
-> {
- return []; // stub
-}
+}
\ No newline at end of file
diff --git a/server/private/lib/config.ts b/server/private/lib/config.ts
new file mode 100644
index 00000000..dd32e313
--- /dev/null
+++ b/server/private/lib/config.ts
@@ -0,0 +1,165 @@
+/*
+ * 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 { z } from "zod";
+import { __DIRNAME } from "@server/lib/consts";
+import { SupporterKey } from "@server/db";
+import { fromError } from "zod-validation-error";
+import {
+ privateConfigSchema,
+ readPrivateConfigFile
+} from "#private/lib/readConfigFile";
+import { build } from "@server/build";
+
+export class PrivateConfig {
+ private rawPrivateConfig!: z.infer;
+
+ supporterData: SupporterKey | null = null;
+
+ supporterHiddenUntil: number | null = null;
+
+ isDev: boolean = process.env.ENVIRONMENT !== "prod";
+
+ constructor() {
+ const privateEnvironment = readPrivateConfigFile();
+
+ const {
+ data: parsedPrivateConfig,
+ success: privateSuccess,
+ error: privateError
+ } = privateConfigSchema.safeParse(privateEnvironment);
+
+ if (!privateSuccess) {
+ const errors = fromError(privateError);
+ throw new Error(`Invalid private configuration file: ${errors}`);
+ }
+
+ if (parsedPrivateConfig.branding?.colors) {
+ process.env.BRANDING_COLORS = JSON.stringify(
+ parsedPrivateConfig.branding?.colors
+ );
+ }
+
+ if (parsedPrivateConfig.branding?.logo?.light_path) {
+ process.env.BRANDING_LOGO_LIGHT_PATH =
+ parsedPrivateConfig.branding?.logo?.light_path;
+ }
+ if (parsedPrivateConfig.branding?.logo?.dark_path) {
+ process.env.BRANDING_LOGO_DARK_PATH =
+ parsedPrivateConfig.branding?.logo?.dark_path || undefined;
+ }
+
+ if (build != "oss") {
+ if (parsedPrivateConfig.branding?.logo?.light_path) {
+ process.env.BRANDING_LOGO_LIGHT_PATH =
+ parsedPrivateConfig.branding?.logo?.light_path;
+ }
+ if (parsedPrivateConfig.branding?.logo?.dark_path) {
+ process.env.BRANDING_LOGO_DARK_PATH =
+ parsedPrivateConfig.branding?.logo?.dark_path || undefined;
+ }
+
+ process.env.BRANDING_LOGO_AUTH_WIDTH = parsedPrivateConfig.branding
+ ?.logo?.auth_page?.width
+ ? parsedPrivateConfig.branding?.logo?.auth_page?.width.toString()
+ : undefined;
+ process.env.BRANDING_LOGO_AUTH_HEIGHT = parsedPrivateConfig.branding
+ ?.logo?.auth_page?.height
+ ? parsedPrivateConfig.branding?.logo?.auth_page?.height.toString()
+ : undefined;
+
+ process.env.BRANDING_LOGO_NAVBAR_WIDTH = parsedPrivateConfig
+ .branding?.logo?.navbar?.width
+ ? parsedPrivateConfig.branding?.logo?.navbar?.width.toString()
+ : undefined;
+ process.env.BRANDING_LOGO_NAVBAR_HEIGHT = parsedPrivateConfig
+ .branding?.logo?.navbar?.height
+ ? parsedPrivateConfig.branding?.logo?.navbar?.height.toString()
+ : undefined;
+
+ process.env.BRANDING_FAVICON_PATH =
+ parsedPrivateConfig.branding?.favicon_path;
+
+ process.env.BRANDING_APP_NAME =
+ parsedPrivateConfig.branding?.app_name || "Pangolin";
+
+ if (parsedPrivateConfig.branding?.footer) {
+ process.env.BRANDING_FOOTER = JSON.stringify(
+ parsedPrivateConfig.branding?.footer
+ );
+ }
+
+ process.env.LOGIN_PAGE_TITLE_TEXT =
+ parsedPrivateConfig.branding?.login_page?.title_text || "";
+ process.env.LOGIN_PAGE_SUBTITLE_TEXT =
+ parsedPrivateConfig.branding?.login_page?.subtitle_text || "";
+
+ process.env.SIGNUP_PAGE_TITLE_TEXT =
+ parsedPrivateConfig.branding?.signup_page?.title_text || "";
+ process.env.SIGNUP_PAGE_SUBTITLE_TEXT =
+ parsedPrivateConfig.branding?.signup_page?.subtitle_text || "";
+
+ process.env.RESOURCE_AUTH_PAGE_HIDE_POWERED_BY =
+ parsedPrivateConfig.branding?.resource_auth_page
+ ?.hide_powered_by === true
+ ? "true"
+ : "false";
+ process.env.RESOURCE_AUTH_PAGE_SHOW_LOGO =
+ parsedPrivateConfig.branding?.resource_auth_page?.show_logo ===
+ true
+ ? "true"
+ : "false";
+ process.env.RESOURCE_AUTH_PAGE_TITLE_TEXT =
+ parsedPrivateConfig.branding?.resource_auth_page?.title_text ||
+ "";
+ process.env.RESOURCE_AUTH_PAGE_SUBTITLE_TEXT =
+ parsedPrivateConfig.branding?.resource_auth_page
+ ?.subtitle_text || "";
+
+ if (parsedPrivateConfig.branding?.background_image_path) {
+ process.env.BACKGROUND_IMAGE_PATH =
+ parsedPrivateConfig.branding?.background_image_path;
+ }
+
+ if (parsedPrivateConfig.server.reo_client_id) {
+ process.env.REO_CLIENT_ID =
+ parsedPrivateConfig.server.reo_client_id;
+ }
+
+ if (parsedPrivateConfig.stripe?.s3Bucket) {
+ process.env.S3_BUCKET = parsedPrivateConfig.stripe.s3Bucket;
+ }
+ if (parsedPrivateConfig.stripe?.localFilePath) {
+ process.env.LOCAL_FILE_PATH =
+ parsedPrivateConfig.stripe.localFilePath;
+ }
+ if (parsedPrivateConfig.stripe?.s3Region) {
+ process.env.S3_REGION = parsedPrivateConfig.stripe.s3Region;
+ }
+ if (parsedPrivateConfig.flags.use_pangolin_dns) {
+ process.env.USE_PANGOLIN_DNS =
+ parsedPrivateConfig.flags.use_pangolin_dns.toString();
+ }
+ }
+
+ this.rawPrivateConfig = parsedPrivateConfig;
+ }
+
+ public getRawPrivateConfig() {
+ return this.rawPrivateConfig;
+ }
+}
+
+export const privateConfig = new PrivateConfig();
+
+export default privateConfig;
diff --git a/server/lib/exitNodes/privateExitNodeComms.ts b/server/private/lib/exitNodes/exitNodeComms.ts
similarity index 85%
rename from server/lib/exitNodes/privateExitNodeComms.ts
rename to server/private/lib/exitNodes/exitNodeComms.ts
index 163a962f..20c850a1 100644
--- a/server/lib/exitNodes/privateExitNodeComms.ts
+++ b/server/private/lib/exitNodes/exitNodeComms.ts
@@ -15,8 +15,9 @@ import axios from "axios";
import logger from "@server/logger";
import { db, ExitNode, remoteExitNodes } from "@server/db";
import { eq } from "drizzle-orm";
-import { sendToClient } from "../../routers/ws";
-import { config } from "../config";
+import { sendToClient } from "#private/routers/ws";
+import privateConfig from "#private/lib/config";
+import config from "@server/lib/config";
interface ExitNodeRequest {
remoteType?: string;
@@ -56,16 +57,16 @@ export async function sendToExitNode(
} else {
let hostname = exitNode.reachableAt;
- logger.debug(`Exit node details:`, {
- type: exitNode.type,
- name: exitNode.name,
- reachableAt: exitNode.reachableAt,
- });
+ // logger.debug(`Exit node details:`, {
+ // type: exitNode.type,
+ // name: exitNode.name,
+ // reachableAt: exitNode.reachableAt,
+ // });
- logger.debug(`Configured local exit node name: ${config.getRawConfig().gerbil.exit_node_name}`);
+ // logger.debug(`Configured local exit node name: ${config.getRawConfig().gerbil.exit_node_name}`);
if (exitNode.name == config.getRawConfig().gerbil.exit_node_name) {
- hostname = config.getRawPrivateConfig().gerbil.local_exit_node_reachable_at;
+ hostname = privateConfig.getRawPrivateConfig().gerbil.local_exit_node_reachable_at;
}
if (!hostname) {
@@ -74,10 +75,10 @@ export async function sendToExitNode(
);
}
- logger.debug(`Sending request to exit node at ${hostname}`, {
- type: request.remoteType,
- data: request.data
- });
+ // logger.debug(`Sending request to exit node at ${hostname}`, {
+ // type: request.remoteType,
+ // data: request.data
+ // });
// Handle local exit node with HTTP API
const method = request.method || "POST";
diff --git a/server/lib/exitNodes/privateExitNodes.ts b/server/private/lib/exitNodes/exitNodes.ts
similarity index 100%
rename from server/lib/exitNodes/privateExitNodes.ts
rename to server/private/lib/exitNodes/exitNodes.ts
diff --git a/server/private/lib/exitNodes/index.ts b/server/private/lib/exitNodes/index.ts
new file mode 100644
index 00000000..098a0580
--- /dev/null
+++ b/server/private/lib/exitNodes/index.ts
@@ -0,0 +1,15 @@
+/*
+ * 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.
+ */
+
+export * from "./exitNodeComms";
+export * from "./exitNodes";
\ No newline at end of file
diff --git a/server/db/private/rateLimit.test.ts b/server/private/lib/rateLimit.test.ts
similarity index 100%
rename from server/db/private/rateLimit.test.ts
rename to server/private/lib/rateLimit.test.ts
diff --git a/server/db/private/rateLimit.ts b/server/private/lib/rateLimit.ts
similarity index 98%
rename from server/db/private/rateLimit.ts
rename to server/private/lib/rateLimit.ts
index ff8589bc..44aa0748 100644
--- a/server/db/private/rateLimit.ts
+++ b/server/private/lib/rateLimit.ts
@@ -12,7 +12,7 @@
*/
import logger from "@server/logger";
-import redisManager from "@server/db/private/redis";
+import redisManager from "#private/lib/redis";
import { build } from "@server/build";
// Rate limiting configuration
@@ -451,8 +451,4 @@ export class RateLimitService {
}
// Export singleton instance
-export const rateLimitService = new RateLimitService();
-
-// Handle process termination
-process.on("SIGTERM", () => rateLimitService.cleanup());
-process.on("SIGINT", () => rateLimitService.cleanup());
\ No newline at end of file
+export const rateLimitService = new RateLimitService();
\ No newline at end of file
diff --git a/server/private/lib/rateLimitStore.ts b/server/private/lib/rateLimitStore.ts
new file mode 100644
index 00000000..20355125
--- /dev/null
+++ b/server/private/lib/rateLimitStore.ts
@@ -0,0 +1,32 @@
+/*
+ * 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 { build } from "@server/build";
+import privateConfig from "#private/lib/config";
+import { MemoryStore, Store } from "express-rate-limit";
+import RedisStore from "#private/lib/redisStore";
+
+export function createStore(): Store {
+ if (build != "oss" && privateConfig.getRawPrivateConfig().flags.enable_redis) {
+ const rateLimitStore: Store = new RedisStore({
+ prefix: "api-rate-limit", // Optional: customize Redis key prefix
+ skipFailedRequests: true, // Don't count failed requests
+ skipSuccessfulRequests: false // Count successful requests
+ });
+
+ return rateLimitStore;
+ } else {
+ const rateLimitStore: Store = new MemoryStore();
+ return rateLimitStore;
+ }
+}
diff --git a/server/private/lib/readConfigFile.ts b/server/private/lib/readConfigFile.ts
new file mode 100644
index 00000000..6651c1c6
--- /dev/null
+++ b/server/private/lib/readConfigFile.ts
@@ -0,0 +1,203 @@
+/*
+ * 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 fs from "fs";
+import yaml from "js-yaml";
+import { privateConfigFilePath1 } from "@server/lib/consts";
+import { z } from "zod";
+import { colorsSchema } from "@server/lib/colorsSchema";
+import { build } from "@server/build";
+
+const portSchema = z.number().positive().gt(0).lte(65535);
+
+export const privateConfigSchema = z.object({
+ app: z
+ .object({
+ region: z.string().optional().default("default"),
+ base_domain: z.string().optional()
+ })
+ .optional()
+ .default({
+ region: "default"
+ }),
+ server: z
+ .object({
+ encryption_key_path: z
+ .string()
+ .optional()
+ .default("./config/encryption.pem")
+ .pipe(z.string().min(8)),
+ resend_api_key: z.string().optional(),
+ reo_client_id: z.string().optional(),
+ fossorial_api_key: z.string().optional()
+ })
+ .optional()
+ .default({
+ encryption_key_path: "./config/encryption.pem"
+ }),
+ redis: z
+ .object({
+ host: z.string(),
+ port: portSchema,
+ password: z.string().optional(),
+ db: z.number().int().nonnegative().optional().default(0),
+ replicas: z
+ .array(
+ z.object({
+ host: z.string(),
+ port: portSchema,
+ password: z.string().optional(),
+ db: z.number().int().nonnegative().optional().default(0)
+ })
+ )
+ .optional()
+ // tls: z
+ // .object({
+ // reject_unauthorized: z
+ // .boolean()
+ // .optional()
+ // .default(true)
+ // })
+ // .optional()
+ })
+ .optional(),
+ gerbil: z
+ .object({
+ local_exit_node_reachable_at: z
+ .string()
+ .optional()
+ .default("http://gerbil:3003")
+ })
+ .optional()
+ .default({}),
+ flags: z
+ .object({
+ enable_redis: z.boolean().optional().default(false),
+ use_pangolin_dns: z.boolean().optional().default(false)
+ })
+ .optional()
+ .default({}),
+ branding: z
+ .object({
+ app_name: z.string().optional(),
+ background_image_path: z.string().optional(),
+ colors: z
+ .object({
+ light: colorsSchema.optional(),
+ dark: colorsSchema.optional()
+ })
+ .optional(),
+ logo: z
+ .object({
+ light_path: z.string().optional(),
+ dark_path: z.string().optional(),
+ auth_page: z
+ .object({
+ width: z.number().optional(),
+ height: z.number().optional()
+ })
+ .optional(),
+ navbar: z
+ .object({
+ width: z.number().optional(),
+ height: z.number().optional()
+ })
+ .optional()
+ })
+ .optional(),
+ favicon_path: z.string().optional(),
+ footer: z
+ .array(
+ z.object({
+ text: z.string(),
+ href: z.string().optional()
+ })
+ )
+ .optional(),
+ login_page: z
+ .object({
+ subtitle_text: z.string().optional(),
+ title_text: z.string().optional()
+ })
+ .optional(),
+ signup_page: z
+ .object({
+ subtitle_text: z.string().optional(),
+ title_text: z.string().optional()
+ })
+ .optional(),
+ resource_auth_page: z
+ .object({
+ show_logo: z.boolean().optional(),
+ hide_powered_by: z.boolean().optional(),
+ title_text: z.string().optional(),
+ subtitle_text: z.string().optional()
+ })
+ .optional(),
+ emails: z
+ .object({
+ signature: z.string().optional(),
+ colors: z
+ .object({
+ primary: z.string().optional()
+ })
+ .optional()
+ })
+ .optional()
+ })
+ .optional(),
+ stripe: z
+ .object({
+ secret_key: z.string(),
+ webhook_secret: z.string(),
+ s3Bucket: z.string(),
+ s3Region: z.string().default("us-east-1"),
+ localFilePath: z.string()
+ })
+ .optional()
+});
+
+export function readPrivateConfigFile() {
+ if (build == "oss") {
+ return {};
+ }
+
+ const loadConfig = (configPath: string) => {
+ try {
+ const yamlContent = fs.readFileSync(configPath, "utf8");
+ if (yamlContent.trim() === "") {
+ return {};
+ }
+ const config = yaml.load(yamlContent);
+ return config;
+ } catch (error) {
+ if (error instanceof Error) {
+ throw new Error(
+ `Error loading configuration file: ${error.message}`
+ );
+ }
+ throw error;
+ }
+ };
+
+ let environment: any = {};
+ if (fs.existsSync(privateConfigFilePath1)) {
+ environment = loadConfig(privateConfigFilePath1);
+ }
+
+ if (!environment) {
+ throw new Error("No private configuration file found.");
+ }
+
+ return environment;
+}
diff --git a/server/db/private/redis.ts b/server/private/lib/redis.ts
similarity index 98%
rename from server/db/private/redis.ts
rename to server/private/lib/redis.ts
index d6e67262..324a6a74 100644
--- a/server/db/private/redis.ts
+++ b/server/private/lib/redis.ts
@@ -13,7 +13,7 @@
import Redis, { RedisOptions } from "ioredis";
import logger from "@server/logger";
-import config from "@server/lib/config";
+import privateConfig from "#private/lib/config";
import { build } from "@server/build";
class RedisManager {
@@ -46,7 +46,7 @@ class RedisManager {
this.isEnabled = false;
return;
}
- this.isEnabled = config.getRawPrivateConfig().flags?.enable_redis || false;
+ this.isEnabled = privateConfig.getRawPrivateConfig().flags.enable_redis || false;
if (this.isEnabled) {
this.initializeClients();
}
@@ -93,7 +93,7 @@ class RedisManager {
}
private getRedisConfig(): RedisOptions {
- const redisConfig = config.getRawPrivateConfig().redis!;
+ const redisConfig = privateConfig.getRawPrivateConfig().redis!;
const opts: RedisOptions = {
host: redisConfig.host!,
port: redisConfig.port!,
@@ -108,7 +108,7 @@ class RedisManager {
}
private getReplicaRedisConfig(): RedisOptions | null {
- const redisConfig = config.getRawPrivateConfig().redis!;
+ const redisConfig = privateConfig.getRawPrivateConfig().redis!;
if (!redisConfig.replicas || redisConfig.replicas.length === 0) {
return null;
}
diff --git a/server/db/private/redisStore.ts b/server/private/lib/redisStore.ts
similarity index 100%
rename from server/db/private/redisStore.ts
rename to server/private/lib/redisStore.ts
diff --git a/server/lib/private/resend.ts b/server/private/lib/resend.ts
similarity index 96%
rename from server/lib/private/resend.ts
rename to server/private/lib/resend.ts
index 26c3f4a6..1aac3d07 100644
--- a/server/lib/private/resend.ts
+++ b/server/private/lib/resend.ts
@@ -12,7 +12,7 @@
*/
import { Resend } from "resend";
-import config from "../config";
+import privateConfig from "#private/lib/config";
import logger from "@server/logger";
export enum AudienceIds {
@@ -22,7 +22,7 @@ export enum AudienceIds {
}
const resend = new Resend(
- config.getRawPrivateConfig().server.resend_api_key || "missing"
+ privateConfig.getRawPrivateConfig().server.resend_api_key || "missing"
);
export default resend;
diff --git a/server/lib/private/stripe.ts b/server/private/lib/stripe.ts
similarity index 76%
rename from server/lib/private/stripe.ts
rename to server/private/lib/stripe.ts
index 1170202d..01aacb35 100644
--- a/server/lib/private/stripe.ts
+++ b/server/private/lib/stripe.ts
@@ -12,13 +12,13 @@
*/
import Stripe from "stripe";
-import config from "@server/lib/config";
+import privateConfig from "#private/lib/config";
import logger from "@server/logger";
-import { build } from "@server/build";
+import { noop } from "@server/lib/billing/usageService";
let stripe: Stripe | undefined = undefined;
-if (build == "saas") {
- const stripeApiKey = config.getRawPrivateConfig().stripe?.secret_key;
+if (!noop()) {
+ const stripeApiKey = privateConfig.getRawPrivateConfig().stripe?.secret_key;
if (!stripeApiKey) {
logger.error("Stripe secret key is not configured");
}
diff --git a/server/lib/traefik/privateGetTraefikConfig.ts b/server/private/lib/traefik/getTraefikConfig.ts
similarity index 84%
rename from server/lib/traefik/privateGetTraefikConfig.ts
rename to server/private/lib/traefik/getTraefikConfig.ts
index 1350e8b7..f6d1c8ab 100644
--- a/server/lib/traefik/privateGetTraefikConfig.ts
+++ b/server/private/lib/traefik/getTraefikConfig.ts
@@ -21,11 +21,11 @@ import {
} from "@server/db";
import { and, eq, inArray, or, isNull, ne, isNotNull, desc } from "drizzle-orm";
import logger from "@server/logger";
-import HttpCode from "@server/types/HttpCode";
import config from "@server/lib/config";
import { orgs, resources, sites, Target, targets } from "@server/db";
-import { build } from "@server/build";
-import { sanitize } from "./utils";
+import { sanitize, validatePathRewriteConfig } from "@server/lib/traefik/utils";
+import privateConfig from "#private/lib/config";
+import createPathRewriteMiddleware from "@server/lib/traefik/middleware";
const redirectHttpsMiddlewareName = "redirect-to-https";
const redirectToRootMiddlewareName = "redirect-to-root";
@@ -78,8 +78,10 @@ export async function getTraefikConfig(
hcHealth: targetHealthCheck.hcHealth,
path: targets.path,
pathMatchType: targets.pathMatchType,
+ rewritePath: targets.rewritePath,
+ rewritePathType: targets.rewritePathType,
priority: targets.priority,
-
+
// Site fields
siteId: sites.siteId,
siteType: sites.type,
@@ -131,18 +133,41 @@ export async function getTraefikConfig(
const resourceName = sanitize(row.resourceName) || "";
const targetPath = sanitize(row.path) || ""; // Handle null/undefined paths
const pathMatchType = row.pathMatchType || "";
+ const rewritePath = row.rewritePath || "";
+ const rewritePathType = row.rewritePathType || "";
const priority = row.priority ?? 100;
if (filterOutNamespaceDomains && row.domainNamespaceId) {
return;
}
- // Create a unique key combining resourceId and path+pathMatchType
- const pathKey = [targetPath, pathMatchType].filter(Boolean).join("-");
+ // Create a unique key combining resourceId, path config, and rewrite config
+ const pathKey = [
+ targetPath,
+ pathMatchType,
+ rewritePath,
+ rewritePathType
+ ]
+ .filter(Boolean)
+ .join("-");
const mapKey = [resourceId, pathKey].filter(Boolean).join("-");
const key = sanitize(mapKey);
if (!resourcesMap.has(key)) {
+ const validation = validatePathRewriteConfig(
+ row.path,
+ row.pathMatchType,
+ row.rewritePath,
+ row.rewritePathType
+ );
+
+ if (!validation.isValid) {
+ logger.error(
+ `Invalid path rewrite configuration for resource ${resourceId}: ${validation.error}`
+ );
+ return;
+ }
+
resourcesMap.set(key, {
resourceId: row.resourceId,
name: resourceName,
@@ -163,6 +188,8 @@ export async function getTraefikConfig(
headers: row.headers,
path: row.path, // the targets will all have the same path
pathMatchType: row.pathMatchType, // the targets will all have the same pathMatchType
+ rewritePath: row.rewritePath,
+ rewritePathType: row.rewritePathType,
priority: priority // may be null, we fallback later
});
}
@@ -176,7 +203,6 @@ export async function getTraefikConfig(
port: row.port,
internalPort: row.internalPort,
enabled: row.targetEnabled,
- priority: row.priority,
site: {
siteId: row.siteId,
type: row.siteType,
@@ -187,11 +213,6 @@ export async function getTraefikConfig(
});
});
- // make sure we have at least one resource
- if (resourcesMap.size === 0) {
- return {};
- }
-
const config_output: any = {
http: {
middlewares: {
@@ -234,12 +255,13 @@ export async function getTraefikConfig(
continue;
}
- if (resource.certificateStatus !== "valid") {
- logger.debug(
- `Resource ${resource.resourceId} has certificate stats ${resource.certificateStats}`
- );
- continue;
- }
+ // TODO: for now dont filter it out because if you have multiple domain ids and one is failed it causes all of them to fail
+ // if (resource.certificateStatus !== "valid" && privateConfig.getRawPrivateConfig().flags.use_pangolin_dns) {
+ // logger.debug(
+ // `Resource ${resource.resourceId} has certificate stats ${resource.certificateStats}`
+ // );
+ // continue;
+ // }
// add routers and services empty objects if they don't exist
if (!config_output.http.routers) {
@@ -264,18 +286,18 @@ export async function getTraefikConfig(
const configDomain = config.getDomain(resource.domainId);
- let certResolver: string, preferWildcardCert: boolean;
- if (!configDomain) {
- certResolver = config.getRawConfig().traefik.cert_resolver;
- preferWildcardCert =
- config.getRawConfig().traefik.prefer_wildcard_cert;
- } else {
- certResolver = configDomain.cert_resolver;
- preferWildcardCert = configDomain.prefer_wildcard_cert;
- }
-
let tls = {};
- if (build == "oss") {
+ if (!privateConfig.getRawPrivateConfig().flags.use_pangolin_dns) {
+ let certResolver: string, preferWildcardCert: boolean;
+ if (!configDomain) {
+ certResolver = config.getRawConfig().traefik.cert_resolver;
+ preferWildcardCert =
+ config.getRawConfig().traefik.prefer_wildcard_cert;
+ } else {
+ certResolver = configDomain.cert_resolver;
+ preferWildcardCert = configDomain.prefer_wildcard_cert;
+ }
+
tls = {
certResolver: certResolver,
...(preferWildcardCert
@@ -298,6 +320,55 @@ export async function getTraefikConfig(
...additionalMiddlewares
];
+ // Handle path rewriting middleware
+ if (
+ resource.rewritePath !== null &&
+ resource.path !== null &&
+ resource.pathMatchType &&
+ resource.rewritePathType
+ ) {
+ // Create a unique middleware name
+ const rewriteMiddlewareName = `rewrite-r${resource.resourceId}-${key}`;
+
+ try {
+ const rewriteResult = createPathRewriteMiddleware(
+ rewriteMiddlewareName,
+ resource.path,
+ resource.pathMatchType,
+ resource.rewritePath,
+ resource.rewritePathType
+ );
+
+ // Initialize middlewares object if it doesn't exist
+ if (!config_output.http.middlewares) {
+ config_output.http.middlewares = {};
+ }
+
+ // the middleware to the config
+ Object.assign(
+ config_output.http.middlewares,
+ rewriteResult.middlewares
+ );
+
+ // middlewares to the router middleware chain
+ if (rewriteResult.chain) {
+ // For chained middlewares (like stripPrefix + addPrefix)
+ routerMiddlewares.push(...rewriteResult.chain);
+ } else {
+ // Single middleware
+ routerMiddlewares.push(rewriteMiddlewareName);
+ }
+
+ logger.debug(
+ `Created path rewrite middleware ${rewriteMiddlewareName}: ${resource.pathMatchType}(${resource.path}) -> ${resource.rewritePathType}(${resource.rewritePath})`
+ );
+ } catch (error) {
+ logger.error(
+ `Failed to create path rewrite middleware for resource ${resource.resourceId}: ${error}`
+ );
+ }
+ }
+
if (resource.headers || resource.setHostHeader) {
// if there are headers, parse them into an object
const headersObj: { [key: string]: string } = {};
@@ -419,7 +490,7 @@ export async function getTraefikConfig(
return (
(targets as TargetWithSite[])
- .filter((target: TargetWithSite) => {
+ .filter((target: TargetWithSite) => {
if (!target.enabled) {
return false;
}
@@ -440,7 +511,7 @@ export async function getTraefikConfig(
) {
return false;
}
- } else if (target.site.type === "newt") {
+ } else if (target.site.type === "newt") {
if (
!target.internalPort ||
!target.method ||
@@ -448,10 +519,10 @@ export async function getTraefikConfig(
) {
return false;
}
- }
- return true;
- })
- .map((target: TargetWithSite) => {
+ }
+ return true;
+ })
+ .map((target: TargetWithSite) => {
if (
target.site.type === "local" ||
target.site.type === "wireguard"
@@ -459,14 +530,14 @@ export async function getTraefikConfig(
return {
url: `${target.method}://${target.ip}:${target.port}`
};
- } else if (target.site.type === "newt") {
+ } else if (target.site.type === "newt") {
const ip =
target.site.subnet!.split("/")[0];
return {
url: `${target.method}://${ip}:${target.internalPort}`
};
- }
- })
+ }
+ })
// filter out duplicates
.filter(
(v, i, a) =>
@@ -709,4 +780,4 @@ export async function getTraefikConfig(
}
return config_output;
-}
\ No newline at end of file
+}
diff --git a/server/private/lib/traefik/index.ts b/server/private/lib/traefik/index.ts
new file mode 100644
index 00000000..30d83181
--- /dev/null
+++ b/server/private/lib/traefik/index.ts
@@ -0,0 +1,14 @@
+/*
+ * 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.
+ */
+
+export * from "./getTraefikConfig";
\ No newline at end of file
diff --git a/server/private/license/license.ts b/server/private/license/license.ts
new file mode 100644
index 00000000..809f5ca9
--- /dev/null
+++ b/server/private/license/license.ts
@@ -0,0 +1,460 @@
+/*
+ * 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 { db, HostMeta } from "@server/db";
+import { hostMeta, licenseKey } from "@server/db";
+import logger from "@server/logger";
+import NodeCache from "node-cache";
+import { validateJWT } from "./licenseJwt";
+import { eq } from "drizzle-orm";
+import moment from "moment";
+import { encrypt, decrypt } from "@server/lib/crypto";
+import {
+ LicenseKeyCache,
+ LicenseKeyTier,
+ LicenseKeyType,
+ LicenseStatus
+} from "@server/license/license";
+import { setHostMeta } from "@server/lib/hostMeta";
+
+type ActivateLicenseKeyAPIResponse = {
+ data: {
+ instanceId: string;
+ };
+ success: boolean;
+ error: string;
+ message: string;
+ status: number;
+};
+
+type ValidateLicenseAPIResponse = {
+ data: {
+ licenseKeys: {
+ [key: string]: string;
+ };
+ };
+ success: boolean;
+ error: string;
+ message: string;
+ status: number;
+};
+
+type TokenPayload = {
+ valid: boolean;
+ type: LicenseKeyType;
+ tier: LicenseKeyTier;
+ quantity: number;
+ terminateAt: string; // ISO
+ iat: number; // Issued at
+};
+
+export class License {
+ private phoneHomeInterval = 6 * 60 * 60; // 6 hours = 6 * 60 * 60 = 21600 seconds
+ private serverBaseUrl = "https://api.fossorial.io";
+ private validationServerUrl = `${this.serverBaseUrl}/api/v1/license/enterprise/validate`;
+ private activationServerUrl = `${this.serverBaseUrl}/api/v1/license/enterprise/activate`;
+
+ private statusCache = new NodeCache({ stdTTL: this.phoneHomeInterval });
+ private licenseKeyCache = new NodeCache();
+
+ private statusKey = "status";
+ private serverSecret!: string;
+
+ private publicKey = `-----BEGIN PUBLIC KEY-----
+MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAx9RKc8cw+G8r7h/xeozF
+FNkRDggQfYO6Ae+EWHGujZ9WYAZ10spLh9F/zoLhhr3XhsjpoRXwMfgNuO5HstWf
+CYM20I0l7EUUMWEyWd4tZLd+5XQ4jY5xWOCWyFJAGQSp7flcRmxdfde+l+xg9eKl
+apbY84aVp09/GqM96hCS+CsQZrhohu/aOqYVB/eAhF01qsbmiZ7Y3WtdhTldveYt
+h4mZWGmjf8d/aEgePf/tk1gp0BUxf+Ae5yqoAqU+6aiFbjJ7q1kgxc18PWFGfE9y
+zSk+OZk887N5ThQ52154+oOUCMMR2Y3t5OH1hVZod51vuY2u5LsQXsf+87PwB91y
+LQIDAQAB
+-----END PUBLIC KEY-----`;
+
+ constructor(private hostMeta: HostMeta) {
+ setInterval(
+ async () => {
+ await this.check();
+ },
+ 1000 * 60 * 60
+ );
+ }
+
+ public listKeys(): LicenseKeyCache[] {
+ const keys = this.licenseKeyCache.keys();
+ return keys.map((key) => {
+ return this.licenseKeyCache.get(key)!;
+ });
+ }
+
+ public setServerSecret(secret: string) {
+ this.serverSecret = secret;
+ }
+
+ public async forceRecheck() {
+ this.statusCache.flushAll();
+ this.licenseKeyCache.flushAll();
+
+ return await this.check();
+ }
+
+ public async isUnlocked(): Promise {
+ const status = await this.check();
+ if (status.isHostLicensed) {
+ if (status.isLicenseValid) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ public async check(): Promise {
+ const status: LicenseStatus = {
+ hostId: this.hostMeta.hostMetaId,
+ isHostLicensed: true,
+ isLicenseValid: false
+ };
+
+ try {
+ if (this.statusCache.has(this.statusKey)) {
+ const res = this.statusCache.get("status") as LicenseStatus;
+ return res;
+ }
+ // Invalidate all
+ this.licenseKeyCache.flushAll();
+
+ const allKeysRes = await db.select().from(licenseKey);
+
+ if (allKeysRes.length === 0) {
+ status.isHostLicensed = false;
+ return status;
+ }
+
+ let foundHostKey = false;
+ // Validate stored license keys
+ for (const key of allKeysRes) {
+ try {
+ // Decrypt the license key and token
+ const decryptedKey = decrypt(
+ key.licenseKeyId,
+ this.serverSecret
+ );
+ const decryptedToken = decrypt(
+ key.token,
+ this.serverSecret
+ );
+
+ const payload = validateJWT(
+ decryptedToken,
+ this.publicKey
+ );
+
+ this.licenseKeyCache.set(decryptedKey, {
+ licenseKey: decryptedKey,
+ licenseKeyEncrypted: key.licenseKeyId,
+ valid: payload.valid,
+ type: payload.type,
+ tier: payload.tier,
+ iat: new Date(payload.iat * 1000),
+ terminateAt: new Date(payload.terminateAt)
+ });
+
+ if (payload.type === "host") {
+ foundHostKey = true;
+ }
+ } catch (e) {
+ logger.error(
+ `Error validating license key: ${key.licenseKeyId}`
+ );
+ logger.error(e);
+
+ this.licenseKeyCache.set(
+ key.licenseKeyId,
+ {
+ licenseKey: key.licenseKeyId,
+ licenseKeyEncrypted: key.licenseKeyId,
+ valid: false
+ }
+ );
+ }
+ }
+
+ if (!foundHostKey && allKeysRes.length) {
+ logger.debug("No host license key found");
+ status.isHostLicensed = false;
+ }
+
+ const keys = allKeysRes.map((key) => ({
+ licenseKey: decrypt(key.licenseKeyId, this.serverSecret),
+ instanceId: decrypt(key.instanceId, this.serverSecret)
+ }));
+
+ let apiResponse: ValidateLicenseAPIResponse | undefined;
+ try {
+ // Phone home to validate license keys
+ apiResponse = await this.phoneHome(keys, false);
+
+ if (!apiResponse?.success) {
+ throw new Error(apiResponse?.error);
+ }
+ } catch (e) {
+ logger.error("Error communicating with license server:");
+ logger.error(e);
+ }
+
+ // Check and update all license keys with server response
+ for (const key of keys) {
+ try {
+ const cached = this.licenseKeyCache.get(
+ key.licenseKey
+ )!;
+ const licenseKeyRes =
+ apiResponse?.data?.licenseKeys[key.licenseKey];
+
+ if (!apiResponse || !licenseKeyRes) {
+ logger.debug(
+ `No response from server for license key: ${key.licenseKey}`
+ );
+ if (cached.iat) {
+ const exp = moment(cached.iat)
+ .add(7, "days")
+ .toDate();
+ if (exp > new Date()) {
+ logger.debug(
+ `Using cached license key: ${key.licenseKey}, valid ${cached.valid}`
+ );
+ continue;
+ }
+ }
+
+ logger.debug(
+ `Can't trust license key: ${key.licenseKey}`
+ );
+ cached.valid = false;
+ this.licenseKeyCache.set(
+ key.licenseKey,
+ cached
+ );
+ continue;
+ }
+
+ const payload = validateJWT(
+ licenseKeyRes,
+ this.publicKey
+ );
+ cached.valid = payload.valid;
+ cached.type = payload.type;
+ cached.tier = payload.tier;
+ cached.iat = new Date(payload.iat * 1000);
+ cached.terminateAt = new Date(payload.terminateAt);
+
+ // Encrypt the updated token before storing
+ const encryptedKey = encrypt(
+ key.licenseKey,
+ this.serverSecret
+ );
+ const encryptedToken = encrypt(
+ licenseKeyRes,
+ this.serverSecret
+ );
+
+ await db
+ .update(licenseKey)
+ .set({
+ token: encryptedToken
+ })
+ .where(eq(licenseKey.licenseKeyId, encryptedKey));
+
+ this.licenseKeyCache.set(
+ key.licenseKey,
+ cached
+ );
+ } catch (e) {
+ logger.error(`Error validating license key: ${key}`);
+ logger.error(e);
+ }
+ }
+
+ // Compute host status
+ for (const key of keys) {
+ const cached = this.licenseKeyCache.get(
+ key.licenseKey
+ )!;
+
+ if (cached.type === "host") {
+ status.isLicenseValid = cached.valid;
+ status.tier = cached.tier;
+ }
+
+ if (!cached.valid) {
+ continue;
+ }
+ }
+ } catch (error) {
+ logger.error("Error checking license status:");
+ logger.error(error);
+ }
+
+ this.statusCache.set(this.statusKey, status);
+ return status;
+ }
+
+ public async activateLicenseKey(key: string) {
+ // Encrypt the license key before storing
+ const encryptedKey = encrypt(key, this.serverSecret);
+
+ const [existingKey] = await db
+ .select()
+ .from(licenseKey)
+ .where(eq(licenseKey.licenseKeyId, encryptedKey))
+ .limit(1);
+
+ if (existingKey) {
+ throw new Error("License key already exists");
+ }
+
+ let instanceId: string | undefined;
+ try {
+ // Call activate
+ const apiResponse = await fetch(this.activationServerUrl, {
+ method: "POST",
+ headers: {
+ "Content-Type": "application/json"
+ },
+ body: JSON.stringify({
+ licenseKey: key,
+ instanceName: this.hostMeta.hostMetaId
+ })
+ });
+
+ const data = await apiResponse.json();
+
+ if (!data.success) {
+ throw new Error(`${data.message || data.error}`);
+ }
+
+ const response = data as ActivateLicenseKeyAPIResponse;
+
+ if (!response.data) {
+ throw new Error("No response from server");
+ }
+
+ if (!response.data.instanceId) {
+ throw new Error("No instance ID in response");
+ }
+
+ logger.debug("Activated license key, instance ID:", {
+ instanceId: response.data.instanceId
+ });
+
+ instanceId = response.data.instanceId;
+ } catch (error) {
+ throw Error(`Error activating license key: ${error}`);
+ }
+
+ // Phone home to validate license key
+ const keys = [
+ {
+ licenseKey: key,
+ instanceId: instanceId!
+ }
+ ];
+
+ let validateResponse: ValidateLicenseAPIResponse;
+ try {
+ validateResponse = await this.phoneHome(keys, false);
+
+ if (!validateResponse) {
+ throw new Error("No response from server");
+ }
+
+ if (!validateResponse.success) {
+ throw new Error(validateResponse.error);
+ }
+
+ // Validate the license key
+ const licenseKeyRes = validateResponse.data.licenseKeys[key];
+ if (!licenseKeyRes) {
+ throw new Error("Invalid license key");
+ }
+
+ const payload = validateJWT(
+ licenseKeyRes,
+ this.publicKey
+ );
+
+ if (!payload.valid) {
+ throw new Error("Invalid license key");
+ }
+
+ const encryptedToken = encrypt(licenseKeyRes, this.serverSecret);
+ // Encrypt the instanceId before storing
+ const encryptedInstanceId = encrypt(instanceId!, this.serverSecret);
+
+ // Store the license key in the database
+ await db.insert(licenseKey).values({
+ licenseKeyId: encryptedKey,
+ token: encryptedToken,
+ instanceId: encryptedInstanceId
+ });
+ } catch (error) {
+ throw Error(`Error validating license key: ${error}`);
+ }
+
+ // Invalidate the cache and re-compute the status
+ return await this.forceRecheck();
+ }
+
+ private async phoneHome(
+ keys: {
+ licenseKey: string;
+ instanceId: string;
+ }[],
+ doDecrypt = true
+ ): Promise {
+ // Decrypt the instanceIds before sending to the server
+ const decryptedKeys = keys.map((key) => ({
+ licenseKey: key.licenseKey,
+ instanceId:
+ key.instanceId && doDecrypt
+ ? decrypt(key.instanceId, this.serverSecret)
+ : key.instanceId
+ }));
+
+ const response = await fetch(this.validationServerUrl, {
+ method: "POST",
+ headers: {
+ "Content-Type": "application/json"
+ },
+ body: JSON.stringify({
+ licenseKeys: decryptedKeys,
+ instanceName: this.hostMeta.hostMetaId
+ })
+ });
+
+ const data = await response.json();
+
+ return data as ValidateLicenseAPIResponse;
+ }
+}
+
+await setHostMeta();
+
+const [info] = await db.select().from(hostMeta).limit(1);
+
+if (!info) {
+ throw new Error("Host information not found");
+}
+
+export const license = new License(info);
+
+export default license;
diff --git a/server/license/licenseJwt.ts b/server/private/license/licenseJwt.ts
similarity index 88%
rename from server/license/licenseJwt.ts
rename to server/private/license/licenseJwt.ts
index 3d148e51..f137db30 100644
--- a/server/license/licenseJwt.ts
+++ b/server/private/license/licenseJwt.ts
@@ -1,3 +1,16 @@
+/*
+ * 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 * as crypto from "crypto";
/**
diff --git a/server/middlewares/private/index.ts b/server/private/middlewares/index.ts
similarity index 92%
rename from server/middlewares/private/index.ts
rename to server/private/middlewares/index.ts
index f034001d..c92b0d3d 100644
--- a/server/middlewares/private/index.ts
+++ b/server/private/middlewares/index.ts
@@ -15,4 +15,4 @@ export * from "./verifyCertificateAccess";
export * from "./verifyRemoteExitNodeAccess";
export * from "./verifyIdpAccess";
export * from "./verifyLoginPageAccess";
-export * from "./corsWithLoginPage";
\ No newline at end of file
+export * from "../../lib/corsWithLoginPage";
\ No newline at end of file
diff --git a/server/middlewares/private/verifyCertificateAccess.ts b/server/private/middlewares/verifyCertificateAccess.ts
similarity index 100%
rename from server/middlewares/private/verifyCertificateAccess.ts
rename to server/private/middlewares/verifyCertificateAccess.ts
diff --git a/server/middlewares/private/verifyIdpAccess.ts b/server/private/middlewares/verifyIdpAccess.ts
similarity index 100%
rename from server/middlewares/private/verifyIdpAccess.ts
rename to server/private/middlewares/verifyIdpAccess.ts
diff --git a/server/middlewares/private/verifyLoginPageAccess.ts b/server/private/middlewares/verifyLoginPageAccess.ts
similarity index 100%
rename from server/middlewares/private/verifyLoginPageAccess.ts
rename to server/private/middlewares/verifyLoginPageAccess.ts
diff --git a/server/middlewares/private/verifyRemoteExitNode.ts b/server/private/middlewares/verifyRemoteExitNode.ts
similarity index 94%
rename from server/middlewares/private/verifyRemoteExitNode.ts
rename to server/private/middlewares/verifyRemoteExitNode.ts
index 45c244e2..2f6d99d2 100644
--- a/server/middlewares/private/verifyRemoteExitNode.ts
+++ b/server/private/middlewares/verifyRemoteExitNode.ts
@@ -16,7 +16,7 @@ import ErrorResponse from "@server/types/ErrorResponse";
import config from "@server/lib/config";
import { unauthorized } from "@server/auth/unauthorizedResponse";
import logger from "@server/logger";
-import { validateRemoteExitNodeSessionToken } from "@server/auth/sessions/privateRemoteExitNode";
+import { validateRemoteExitNodeSessionToken } from "#private/auth/sessions/remoteExitNode";
export const verifySessionRemoteExitNodeMiddleware = async (
req: any,
diff --git a/server/middlewares/private/verifyRemoteExitNodeAccess.ts b/server/private/middlewares/verifyRemoteExitNodeAccess.ts
similarity index 100%
rename from server/middlewares/private/verifyRemoteExitNodeAccess.ts
rename to server/private/middlewares/verifyRemoteExitNodeAccess.ts
diff --git a/server/middlewares/verifyValidLicense.ts b/server/private/middlewares/verifyValidLicense.ts
similarity index 80%
rename from server/middlewares/verifyValidLicense.ts
rename to server/private/middlewares/verifyValidLicense.ts
index 7e3bfee3..fde9116e 100644
--- a/server/middlewares/verifyValidLicense.ts
+++ b/server/private/middlewares/verifyValidLicense.ts
@@ -1,7 +1,8 @@
import { Request, Response, NextFunction } from "express";
import createHttpError from "http-errors";
import HttpCode from "@server/types/HttpCode";
-import license from "@server/license/license";
+import license from "#private/license/license";
+import { build } from "@server/build";
export async function verifyValidLicense(
req: Request,
@@ -9,6 +10,10 @@ export async function verifyValidLicense(
next: NextFunction
) {
try {
+ if (build != "enterprise") {
+ return next();
+ }
+
const unlocked = await license.isUnlocked();
if (!unlocked) {
return next(
diff --git a/server/routers/auth/privateGetSessionTransferToken.ts b/server/private/routers/auth/getSessionTransferToken.ts
similarity index 100%
rename from server/routers/auth/privateGetSessionTransferToken.ts
rename to server/private/routers/auth/getSessionTransferToken.ts
diff --git a/server/private/routers/auth/index.ts b/server/private/routers/auth/index.ts
new file mode 100644
index 00000000..39a60031
--- /dev/null
+++ b/server/private/routers/auth/index.ts
@@ -0,0 +1,16 @@
+/*
+ * 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.
+ */
+
+export * from "./transferSession";
+export * from "./getSessionTransferToken";
+export * from "./quickStart";
\ No newline at end of file
diff --git a/server/routers/auth/privateQuickStart.ts b/server/private/routers/auth/quickStart.ts
similarity index 98%
rename from server/routers/auth/privateQuickStart.ts
rename to server/private/routers/auth/quickStart.ts
index 683c24a8..582ac4d5 100644
--- a/server/routers/auth/privateQuickStart.ts
+++ b/server/private/routers/auth/quickStart.ts
@@ -50,16 +50,16 @@ import config from "@server/lib/config";
import logger from "@server/logger";
import { hashPassword } from "@server/auth/password";
import { UserType } from "@server/types/UserTypes";
-import { createUserAccountOrg } from "@server/lib/private/createUserAccountOrg";
+import { createUserAccountOrg } from "@server/lib/createUserAccountOrg";
import { sendEmail } from "@server/emails";
import WelcomeQuickStart from "@server/emails/templates/WelcomeQuickStart";
import { alphabet, generateRandomString } from "oslo/crypto";
import { createDate, TimeSpan } from "oslo";
import { getUniqueResourceName, getUniqueSiteName } from "@server/db/names";
-import { pickPort } from "../target/helpers";
-import { addTargets } from "../newt/targets";
+import { pickPort } from "@server/routers/target/helpers";
+import { addTargets } from "@server/routers/newt/targets";
import { isTargetValid } from "@server/lib/validators";
-import { listExitNodes } from "@server/lib/exitNodes";
+import { listExitNodes } from "#private/lib/exitNodes";
const bodySchema = z.object({
email: z.string().toLowerCase().email(),
diff --git a/server/routers/auth/privateTransferSession.ts b/server/private/routers/auth/transferSession.ts
similarity index 97%
rename from server/routers/auth/privateTransferSession.ts
rename to server/private/routers/auth/transferSession.ts
index e75f77dd..52138a75 100644
--- a/server/routers/auth/privateTransferSession.ts
+++ b/server/private/routers/auth/transferSession.ts
@@ -26,6 +26,7 @@ import { sha256 } from "@oslojs/crypto/sha2";
import { serializeSessionCookie } from "@server/auth/sessions/app";
import { decrypt } from "@server/lib/crypto";
import config from "@server/lib/config";
+import { TransferSessionResponse } from "@server/routers/auth/types";
const bodySchema = z.object({
token: z.string()
@@ -33,11 +34,6 @@ const bodySchema = z.object({
export type TransferSessionBodySchema = z.infer;
-export type TransferSessionResponse = {
- valid: boolean;
- cookie?: string;
-};
-
export async function transferSession(
req: Request,
res: Response,
diff --git a/server/routers/private/billing/createCheckoutSession.ts b/server/private/routers/billing/createCheckoutSession.ts
similarity index 95%
rename from server/routers/private/billing/createCheckoutSession.ts
rename to server/private/routers/billing/createCheckoutSession.ts
index 67507b68..6e1e28c2 100644
--- a/server/routers/private/billing/createCheckoutSession.ts
+++ b/server/private/routers/billing/createCheckoutSession.ts
@@ -21,9 +21,9 @@ import createHttpError from "http-errors";
import logger from "@server/logger";
import config from "@server/lib/config";
import { fromError } from "zod-validation-error";
-import stripe from "@server/lib/private/stripe";
-import { getLineItems, getStandardFeaturePriceSet } from "@server/lib/private/billing";
-import { getTierPriceSet, TierId } from "@server/lib/private/billing/tiers";
+import stripe from "#private/lib/stripe";
+import { getLineItems, getStandardFeaturePriceSet } from "@server/lib/billing";
+import { getTierPriceSet, TierId } from "@server/lib/billing/tiers";
const createCheckoutSessionSchema = z
.object({
diff --git a/server/routers/private/billing/createPortalSession.ts b/server/private/routers/billing/createPortalSession.ts
similarity index 98%
rename from server/routers/private/billing/createPortalSession.ts
rename to server/private/routers/billing/createPortalSession.ts
index aa672377..eb55f007 100644
--- a/server/routers/private/billing/createPortalSession.ts
+++ b/server/private/routers/billing/createPortalSession.ts
@@ -21,7 +21,7 @@ import createHttpError from "http-errors";
import logger from "@server/logger";
import config from "@server/lib/config";
import { fromError } from "zod-validation-error";
-import stripe from "@server/lib/private/stripe";
+import stripe from "#private/lib/stripe";
const createPortalSessionSchema = z
.object({
diff --git a/server/routers/private/billing/getOrgSubscription.ts b/server/private/routers/billing/getOrgSubscription.ts
similarity index 97%
rename from server/routers/private/billing/getOrgSubscription.ts
rename to server/private/routers/billing/getOrgSubscription.ts
index 3e4f575e..b97ca39f 100644
--- a/server/routers/private/billing/getOrgSubscription.ts
+++ b/server/private/routers/billing/getOrgSubscription.ts
@@ -22,6 +22,8 @@ import createHttpError from "http-errors";
import logger from "@server/logger";
import { fromZodError } from "zod-validation-error";
import { OpenAPITags, registry } from "@server/openApi";
+import { GetOrgSubscriptionResponse } from "@server/routers/billing/types";
+
// Import tables for billing
import {
customers,
@@ -37,11 +39,6 @@ const getOrgSchema = z
})
.strict();
-export type GetOrgSubscriptionResponse = {
- subscription: Subscription | null;
- items: SubscriptionItem[];
-};
-
registry.registerPath({
method: "get",
path: "/org/{orgId}/billing/subscription",
diff --git a/server/routers/private/billing/getOrgUsage.ts b/server/private/routers/billing/getOrgUsage.ts
similarity index 94%
rename from server/routers/private/billing/getOrgUsage.ts
rename to server/private/routers/billing/getOrgUsage.ts
index e1544e06..bc879659 100644
--- a/server/routers/private/billing/getOrgUsage.ts
+++ b/server/private/routers/billing/getOrgUsage.ts
@@ -23,8 +23,9 @@ import logger from "@server/logger";
import { fromZodError } from "zod-validation-error";
import { OpenAPITags, registry } from "@server/openApi";
import { Limit, limits, Usage, usage } from "@server/db";
-import { usageService } from "@server/lib/private/billing/usageService";
-import { FeatureId } from "@server/lib/private/billing";
+import { usageService } from "@server/lib/billing/usageService";
+import { FeatureId } from "@server/lib/billing";
+import { GetOrgUsageResponse } from "@server/routers/billing/types";
const getOrgSchema = z
.object({
@@ -32,11 +33,6 @@ const getOrgSchema = z
})
.strict();
-export type GetOrgUsageResponse = {
- usage: Usage[];
- limits: Limit[];
-};
-
registry.registerPath({
method: "get",
path: "/org/{orgId}/billing/usage",
diff --git a/server/routers/private/billing/hooks/handleCustomerCreated.ts b/server/private/routers/billing/hooks/handleCustomerCreated.ts
similarity index 100%
rename from server/routers/private/billing/hooks/handleCustomerCreated.ts
rename to server/private/routers/billing/hooks/handleCustomerCreated.ts
diff --git a/server/routers/private/billing/hooks/handleCustomerDeleted.ts b/server/private/routers/billing/hooks/handleCustomerDeleted.ts
similarity index 100%
rename from server/routers/private/billing/hooks/handleCustomerDeleted.ts
rename to server/private/routers/billing/hooks/handleCustomerDeleted.ts
diff --git a/server/routers/private/billing/hooks/handleCustomerUpdated.ts b/server/private/routers/billing/hooks/handleCustomerUpdated.ts
similarity index 100%
rename from server/routers/private/billing/hooks/handleCustomerUpdated.ts
rename to server/private/routers/billing/hooks/handleCustomerUpdated.ts
diff --git a/server/routers/private/billing/hooks/handleSubscriptionCreated.ts b/server/private/routers/billing/hooks/handleSubscriptionCreated.ts
similarity index 97%
rename from server/routers/private/billing/hooks/handleSubscriptionCreated.ts
rename to server/private/routers/billing/hooks/handleSubscriptionCreated.ts
index ee6376c9..223a2545 100644
--- a/server/routers/private/billing/hooks/handleSubscriptionCreated.ts
+++ b/server/private/routers/billing/hooks/handleSubscriptionCreated.ts
@@ -22,9 +22,9 @@ import {
} from "@server/db";
import { eq, and } from "drizzle-orm";
import logger from "@server/logger";
-import stripe from "@server/lib/private/stripe";
+import stripe from "#private/lib/stripe";
import { handleSubscriptionLifesycle } from "../subscriptionLifecycle";
-import { AudienceIds, moveEmailToAudience } from "@server/lib/private/resend";
+import { AudienceIds, moveEmailToAudience } from "#private/lib/resend";
export async function handleSubscriptionCreated(
subscription: Stripe.Subscription
diff --git a/server/routers/private/billing/hooks/handleSubscriptionDeleted.ts b/server/private/routers/billing/hooks/handleSubscriptionDeleted.ts
similarity index 97%
rename from server/routers/private/billing/hooks/handleSubscriptionDeleted.ts
rename to server/private/routers/billing/hooks/handleSubscriptionDeleted.ts
index 95123731..114a4b30 100644
--- a/server/routers/private/billing/hooks/handleSubscriptionDeleted.ts
+++ b/server/private/routers/billing/hooks/handleSubscriptionDeleted.ts
@@ -16,7 +16,7 @@ import { subscriptions, db, subscriptionItems, customers, userOrgs, users } from
import { eq, and } from "drizzle-orm";
import logger from "@server/logger";
import { handleSubscriptionLifesycle } from "../subscriptionLifecycle";
-import { AudienceIds, moveEmailToAudience } from "@server/lib/private/resend";
+import { AudienceIds, moveEmailToAudience } from "#private/lib/resend";
export async function handleSubscriptionDeleted(
subscription: Stripe.Subscription
diff --git a/server/routers/private/billing/hooks/handleSubscriptionUpdated.ts b/server/private/routers/billing/hooks/handleSubscriptionUpdated.ts
similarity index 98%
rename from server/routers/private/billing/hooks/handleSubscriptionUpdated.ts
rename to server/private/routers/billing/hooks/handleSubscriptionUpdated.ts
index f1cbcafe..01086054 100644
--- a/server/routers/private/billing/hooks/handleSubscriptionUpdated.ts
+++ b/server/private/routers/billing/hooks/handleSubscriptionUpdated.ts
@@ -23,8 +23,8 @@ import {
} from "@server/db";
import { eq, and } from "drizzle-orm";
import logger from "@server/logger";
-import { getFeatureIdByMetricId } from "@server/lib/private/billing/features";
-import stripe from "@server/lib/private/stripe";
+import { getFeatureIdByMetricId } from "@server/lib/billing/features";
+import stripe from "#private/lib/stripe";
import { handleSubscriptionLifesycle } from "../subscriptionLifecycle";
export async function handleSubscriptionUpdated(
diff --git a/server/routers/private/billing/index.ts b/server/private/routers/billing/index.ts
similarity index 100%
rename from server/routers/private/billing/index.ts
rename to server/private/routers/billing/index.ts
diff --git a/server/routers/private/billing/internalGetOrgTier.ts b/server/private/routers/billing/internalGetOrgTier.ts
similarity index 66%
rename from server/routers/private/billing/internalGetOrgTier.ts
rename to server/private/routers/billing/internalGetOrgTier.ts
index 7f8cc642..cca96243 100644
--- a/server/routers/private/billing/internalGetOrgTier.ts
+++ b/server/private/routers/billing/internalGetOrgTier.ts
@@ -18,9 +18,8 @@ import HttpCode from "@server/types/HttpCode";
import createHttpError from "http-errors";
import logger from "@server/logger";
import { fromZodError } from "zod-validation-error";
-import { getTierPriceSet } from "@server/lib/private/billing/tiers";
-import { getOrgSubscriptionData } from "./getOrgSubscription";
-import { build } from "@server/build";
+import { getOrgTierData } from "#private/lib/billing";
+import { GetOrgTierResponse } from "@server/routers/billing/types";
const getOrgSchema = z
.object({
@@ -28,11 +27,6 @@ const getOrgSchema = z
})
.strict();
-export type GetOrgTierResponse = {
- tier: string | null;
- active: boolean;
-};
-
export async function getOrgTier(
req: Request,
res: Response,
@@ -87,33 +81,3 @@ export async function getOrgTier(
);
}
}
-
-export async function getOrgTierData(
- orgId: string
-): Promise<{ tier: string | null; active: boolean }> {
- let tier = null;
- let active = false;
-
- if (build !== "saas") {
- return { tier, active };
- }
-
- const { subscription, items } = await getOrgSubscriptionData(orgId);
-
- if (items && items.length > 0) {
- const tierPriceSet = getTierPriceSet();
- // Iterate through tiers in order (earlier keys are higher tiers)
- for (const [tierId, priceId] of Object.entries(tierPriceSet)) {
- // Check if any subscription item matches this tier's price ID
- const matchingItem = items.find((item) => item.priceId === priceId);
- if (matchingItem) {
- tier = tierId;
- break;
- }
- }
- }
- if (subscription && subscription.status === "active") {
- active = true;
- }
- return { tier, active };
-}
diff --git a/server/routers/private/billing/subscriptionLifecycle.ts b/server/private/routers/billing/subscriptionLifecycle.ts
similarity index 93%
rename from server/routers/private/billing/subscriptionLifecycle.ts
rename to server/private/routers/billing/subscriptionLifecycle.ts
index 82dbfdbe..06b2a2a8 100644
--- a/server/routers/private/billing/subscriptionLifecycle.ts
+++ b/server/private/routers/billing/subscriptionLifecycle.ts
@@ -11,8 +11,8 @@
* This file is not licensed under the AGPLv3.
*/
-import { freeLimitSet, limitsService, subscribedLimitSet } from "@server/lib/private/billing";
-import { usageService } from "@server/lib/private/billing/usageService";
+import { freeLimitSet, limitsService, subscribedLimitSet } from "@server/lib/billing";
+import { usageService } from "@server/lib/billing/usageService";
import logger from "@server/logger";
export async function handleSubscriptionLifesycle(orgId: string, status: string) {
diff --git a/server/routers/private/billing/webhooks.ts b/server/private/routers/billing/webhooks.ts
similarity index 95%
rename from server/routers/private/billing/webhooks.ts
rename to server/private/routers/billing/webhooks.ts
index 2844943a..24ad1074 100644
--- a/server/routers/private/billing/webhooks.ts
+++ b/server/private/routers/billing/webhooks.ts
@@ -11,8 +11,8 @@
* This file is not licensed under the AGPLv3.
*/
-import stripe from "@server/lib/private/stripe";
-import config from "@server/lib/config";
+import stripe from "#private/lib/stripe";
+import privateConfig from "#private/lib/config";
import logger from "@server/logger";
import createHttpError from "http-errors";
import { response } from "@server/lib/response";
@@ -26,13 +26,13 @@ import { handleCustomerUpdated } from "./hooks/handleCustomerUpdated";
import { handleSubscriptionDeleted } from "./hooks/handleSubscriptionDeleted";
import { handleCustomerDeleted } from "./hooks/handleCustomerDeleted";
-export async function stripeWebhookHandler(
+export async function billingWebhookHandler(
req: Request,
res: Response,
next: NextFunction
): Promise {
let event: Stripe.Event = req.body;
- const endpointSecret = config.getRawPrivateConfig().stripe?.webhook_secret;
+ const endpointSecret = privateConfig.getRawPrivateConfig().stripe?.webhook_secret;
if (!endpointSecret) {
logger.warn("Stripe webhook secret is not configured. Webhook events will not be priocessed.");
return next(
diff --git a/server/routers/private/certificates/createCertificate.ts b/server/private/routers/certificates/createCertificate.ts
similarity index 86%
rename from server/routers/private/certificates/createCertificate.ts
rename to server/private/routers/certificates/createCertificate.ts
index 210878ef..43a3426e 100644
--- a/server/routers/private/certificates/createCertificate.ts
+++ b/server/private/routers/certificates/createCertificate.ts
@@ -15,15 +15,19 @@ import { Certificate, certificates, db, domains } from "@server/db";
import logger from "@server/logger";
import { Transaction } from "@server/db";
import { eq, or, and, like } from "drizzle-orm";
-import { build } from "@server/build";
+import privateConfig from "#private/lib/config";
/**
* Checks if a certificate exists for the given domain.
* If not, creates a new certificate in 'pending' state.
* Wildcard certs cover subdomains.
*/
-export async function createCertificate(domainId: string, domain: string, trx: Transaction | typeof db) {
- if (build !== "saas") {
+export async function createCertificate(
+ domainId: string,
+ domain: string,
+ trx: Transaction | typeof db
+) {
+ if (!privateConfig.getRawPrivateConfig().flags.use_pangolin_dns) {
return;
}
@@ -39,7 +43,7 @@ export async function createCertificate(domainId: string, domain: string, trx: T
let existing: Certificate[] = [];
if (domainRecord.type == "ns") {
- const domainLevelDown = domain.split('.').slice(1).join('.');
+ const domainLevelDown = domain.split(".").slice(1).join(".");
existing = await trx
.select()
.from(certificates)
@@ -49,7 +53,7 @@ export async function createCertificate(domainId: string, domain: string, trx: T
eq(certificates.wildcard, true), // only NS domains can have wildcard certs
or(
eq(certificates.domain, domain),
- eq(certificates.domain, domainLevelDown),
+ eq(certificates.domain, domainLevelDown)
)
)
);
@@ -67,9 +71,7 @@ export async function createCertificate(domainId: string, domain: string, trx: T
}
if (existing.length > 0) {
- logger.info(
- `Certificate already exists for domain ${domain}`
- );
+ logger.info(`Certificate already exists for domain ${domain}`);
return;
}
diff --git a/server/routers/private/certificates/getCertificate.ts b/server/private/routers/certificates/getCertificate.ts
similarity index 93%
rename from server/routers/private/certificates/getCertificate.ts
rename to server/private/routers/certificates/getCertificate.ts
index a0bf74f6..8392cbc0 100644
--- a/server/routers/private/certificates/getCertificate.ts
+++ b/server/private/routers/certificates/getCertificate.ts
@@ -21,6 +21,7 @@ import createHttpError from "http-errors";
import logger from "@server/logger";
import { fromError } from "zod-validation-error";
import { registry } from "@server/openApi";
+import { GetCertificateResponse } from "@server/routers/certificates/types";
const getCertificateSchema = z
.object({
@@ -96,20 +97,6 @@ async function query(domainId: string, domain: string) {
return existing.length > 0 ? existing[0] : null;
}
-export type GetCertificateResponse = {
- certId: number;
- domain: string;
- domainId: string;
- wildcard: boolean;
- status: string; // pending, requested, valid, expired, failed
- expiresAt: string | null;
- lastRenewalAttempt: Date | null;
- createdAt: string;
- updatedAt: string;
- errorMessage?: string | null;
- renewalCount: number;
-}
-
registry.registerPath({
method: "get",
path: "/org/{orgId}/certificate/{domainId}/{domain}",
diff --git a/server/routers/private/certificates/index.ts b/server/private/routers/certificates/index.ts
similarity index 100%
rename from server/routers/private/certificates/index.ts
rename to server/private/routers/certificates/index.ts
diff --git a/server/routers/private/certificates/restartCertificate.ts b/server/private/routers/certificates/restartCertificate.ts
similarity index 100%
rename from server/routers/private/certificates/restartCertificate.ts
rename to server/private/routers/certificates/restartCertificate.ts
diff --git a/server/routers/domain/privateCheckDomainNamespaceAvailability.ts b/server/private/routers/domain/checkDomainNamespaceAvailability.ts
similarity index 94%
rename from server/routers/domain/privateCheckDomainNamespaceAvailability.ts
rename to server/private/routers/domain/checkDomainNamespaceAvailability.ts
index f1a4e103..745af9d3 100644
--- a/server/routers/domain/privateCheckDomainNamespaceAvailability.ts
+++ b/server/private/routers/domain/checkDomainNamespaceAvailability.ts
@@ -21,6 +21,7 @@ import { fromError } from "zod-validation-error";
import { OpenAPITags, registry } from "@server/openApi";
import { db, domainNamespaces, resources } from "@server/db";
import { inArray } from "drizzle-orm";
+import { CheckDomainAvailabilityResponse } from "@server/routers/domain/types";
const paramsSchema = z.object({}).strict();
@@ -30,15 +31,6 @@ const querySchema = z
})
.strict();
-export type CheckDomainAvailabilityResponse = {
- available: boolean;
- options: {
- domainNamespaceId: string;
- domainId: string;
- fullDomain: string;
- }[];
-};
-
registry.registerPath({
method: "get",
path: "/domain/check-namespace-availability",
diff --git a/server/private/routers/domain/index.ts b/server/private/routers/domain/index.ts
new file mode 100644
index 00000000..da9cec3f
--- /dev/null
+++ b/server/private/routers/domain/index.ts
@@ -0,0 +1,15 @@
+/*
+ * 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.
+ */
+
+export * from "./checkDomainNamespaceAvailability";
+export * from "./listDomainNamespaces";
\ No newline at end of file
diff --git a/server/routers/domain/privateListDomainNamespaces.ts b/server/private/routers/domain/listDomainNamespaces.ts
similarity index 100%
rename from server/routers/domain/privateListDomainNamespaces.ts
rename to server/private/routers/domain/listDomainNamespaces.ts
diff --git a/server/private/routers/external.ts b/server/private/routers/external.ts
new file mode 100644
index 00000000..74cd6b0c
--- /dev/null
+++ b/server/private/routers/external.ts
@@ -0,0 +1,336 @@
+/*
+ * 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 * as certificates from "#private/routers/certificates";
+import { createStore } from "#private/lib/rateLimitStore";
+import * as billing from "#private/routers/billing";
+import * as remoteExitNode from "#private/routers/remoteExitNode";
+import * as loginPage from "#private/routers/loginPage";
+import * as orgIdp from "#private/routers/orgIdp";
+import * as domain from "#private/routers/domain";
+import * as auth from "#private/routers/auth";
+import * as license from "#private/routers/license";
+import * as generateLicense from "./generatedLicense";
+
+import { Router } from "express";
+import {
+ verifyOrgAccess,
+ verifyUserHasAction,
+ verifyUserIsOrgOwner,
+ verifyUserIsServerAdmin
+} from "@server/middlewares";
+import { ActionsEnum } from "@server/auth/actions";
+import {
+ verifyCertificateAccess,
+ verifyIdpAccess,
+ verifyLoginPageAccess,
+ verifyRemoteExitNodeAccess
+} from "#private/middlewares";
+import rateLimit, { ipKeyGenerator } from "express-rate-limit";
+import createHttpError from "http-errors";
+import HttpCode from "@server/types/HttpCode";
+import { verifyValidLicense } from "../middlewares/verifyValidLicense";
+import { build } from "@server/build";
+import {
+ unauthenticated as ua,
+ authenticated as a,
+ authRouter as aa
+} from "@server/routers/external";
+
+export const authenticated = a;
+export const unauthenticated = ua;
+export const authRouter = aa;
+
+unauthenticated.post(
+ "/remote-exit-node/quick-start",
+ verifyValidLicense,
+ rateLimit({
+ windowMs: 60 * 60 * 1000,
+ max: 5,
+ keyGenerator: (req) => `${req.path}:${ipKeyGenerator(req.ip || "")}`,
+ handler: (req, res, next) => {
+ const message = `You can only create 5 remote exit nodes every hour. Please try again later.`;
+ return next(createHttpError(HttpCode.TOO_MANY_REQUESTS, message));
+ },
+ store: createStore()
+ }),
+ remoteExitNode.quickStartRemoteExitNode
+);
+
+authenticated.put(
+ "/org/:orgId/idp/oidc",
+ verifyValidLicense,
+ verifyOrgAccess,
+ verifyUserHasAction(ActionsEnum.createIdp),
+ orgIdp.createOrgOidcIdp
+);
+
+authenticated.post(
+ "/org/:orgId/idp/:idpId/oidc",
+ verifyValidLicense,
+ verifyOrgAccess,
+ verifyIdpAccess,
+ verifyUserHasAction(ActionsEnum.updateIdp),
+ orgIdp.updateOrgOidcIdp
+);
+
+authenticated.delete(
+ "/org/:orgId/idp/:idpId",
+ verifyValidLicense,
+ verifyOrgAccess,
+ verifyIdpAccess,
+ verifyUserHasAction(ActionsEnum.deleteIdp),
+ orgIdp.deleteOrgIdp
+);
+
+authenticated.get(
+ "/org/:orgId/idp/:idpId",
+ verifyValidLicense,
+ verifyOrgAccess,
+ verifyIdpAccess,
+ verifyUserHasAction(ActionsEnum.getIdp),
+ orgIdp.getOrgIdp
+);
+
+authenticated.get(
+ "/org/:orgId/idp",
+ verifyValidLicense,
+ verifyOrgAccess,
+ verifyUserHasAction(ActionsEnum.listIdps),
+ orgIdp.listOrgIdps
+);
+
+authenticated.get("/org/:orgId/idp", orgIdp.listOrgIdps); // anyone can see this; it's just a list of idp names and ids
+
+authenticated.get(
+ "/org/:orgId/certificate/:domainId/:domain",
+ verifyValidLicense,
+ verifyOrgAccess,
+ verifyCertificateAccess,
+ verifyUserHasAction(ActionsEnum.getCertificate),
+ certificates.getCertificate
+);
+
+authenticated.post(
+ "/org/:orgId/certificate/:certId/restart",
+ verifyValidLicense,
+ verifyOrgAccess,
+ verifyCertificateAccess,
+ verifyUserHasAction(ActionsEnum.restartCertificate),
+ certificates.restartCertificate
+);
+
+if (build === "saas") {
+ unauthenticated.post(
+ "/quick-start",
+ rateLimit({
+ windowMs: 15 * 60 * 1000,
+ max: 100,
+ keyGenerator: (req) => req.path,
+ handler: (req, res, next) => {
+ const message = `We're too busy right now. Please try again later.`;
+ return next(
+ createHttpError(HttpCode.TOO_MANY_REQUESTS, message)
+ );
+ },
+ store: createStore()
+ }),
+ auth.quickStart
+ );
+
+ authenticated.post(
+ "/org/:orgId/billing/create-checkout-session",
+ verifyOrgAccess,
+ verifyUserHasAction(ActionsEnum.billing),
+ billing.createCheckoutSession
+ );
+
+ authenticated.post(
+ "/org/:orgId/billing/create-portal-session",
+ verifyOrgAccess,
+ verifyUserHasAction(ActionsEnum.billing),
+ billing.createPortalSession
+ );
+
+ authenticated.get(
+ "/org/:orgId/billing/subscription",
+ verifyOrgAccess,
+ verifyUserHasAction(ActionsEnum.billing),
+ billing.getOrgSubscription
+ );
+
+ authenticated.get(
+ "/org/:orgId/billing/usage",
+ verifyOrgAccess,
+ verifyUserHasAction(ActionsEnum.billing),
+ billing.getOrgUsage
+ );
+
+ authenticated.get(
+ "/org/:orgId/license",
+ verifyOrgAccess,
+ generateLicense.listSaasLicenseKeys
+ );
+
+ authenticated.put(
+ "/org/:orgId/license",
+ verifyOrgAccess,
+ generateLicense.generateNewLicense
+ );
+}
+
+authenticated.get(
+ "/domain/namespaces",
+ verifyValidLicense,
+ domain.listDomainNamespaces
+);
+
+authenticated.get(
+ "/domain/check-namespace-availability",
+ verifyValidLicense,
+ domain.checkDomainNamespaceAvailability
+);
+
+authenticated.put(
+ "/org/:orgId/remote-exit-node",
+ verifyValidLicense,
+ verifyOrgAccess,
+ verifyUserHasAction(ActionsEnum.createRemoteExitNode),
+ remoteExitNode.createRemoteExitNode
+);
+
+authenticated.get(
+ "/org/:orgId/remote-exit-nodes",
+ verifyValidLicense,
+ verifyOrgAccess,
+ verifyUserHasAction(ActionsEnum.listRemoteExitNode),
+ remoteExitNode.listRemoteExitNodes
+);
+
+authenticated.get(
+ "/org/:orgId/remote-exit-node/:remoteExitNodeId",
+ verifyValidLicense,
+ verifyOrgAccess,
+ verifyRemoteExitNodeAccess,
+ verifyUserHasAction(ActionsEnum.getRemoteExitNode),
+ remoteExitNode.getRemoteExitNode
+);
+
+authenticated.get(
+ "/org/:orgId/pick-remote-exit-node-defaults",
+ verifyValidLicense,
+ verifyOrgAccess,
+ verifyUserHasAction(ActionsEnum.createRemoteExitNode),
+ remoteExitNode.pickRemoteExitNodeDefaults
+);
+
+authenticated.delete(
+ "/org/:orgId/remote-exit-node/:remoteExitNodeId",
+ verifyValidLicense,
+ verifyOrgAccess,
+ verifyRemoteExitNodeAccess,
+ verifyUserHasAction(ActionsEnum.deleteRemoteExitNode),
+ remoteExitNode.deleteRemoteExitNode
+);
+
+authenticated.put(
+ "/org/:orgId/login-page",
+ verifyValidLicense,
+ verifyOrgAccess,
+ verifyUserHasAction(ActionsEnum.createLoginPage),
+ loginPage.createLoginPage
+);
+
+authenticated.post(
+ "/org/:orgId/login-page/:loginPageId",
+ verifyValidLicense,
+ verifyOrgAccess,
+ verifyLoginPageAccess,
+ verifyUserHasAction(ActionsEnum.updateLoginPage),
+ loginPage.updateLoginPage
+);
+
+authenticated.delete(
+ "/org/:orgId/login-page/:loginPageId",
+ verifyValidLicense,
+ verifyOrgAccess,
+ verifyLoginPageAccess,
+ verifyUserHasAction(ActionsEnum.deleteLoginPage),
+ loginPage.deleteLoginPage
+);
+
+authenticated.get(
+ "/org/:orgId/login-page",
+ verifyValidLicense,
+ verifyOrgAccess,
+ verifyUserHasAction(ActionsEnum.getLoginPage),
+ loginPage.getLoginPage
+);
+
+authRouter.post(
+ "/remoteExitNode/get-token",
+ verifyValidLicense,
+ rateLimit({
+ windowMs: 15 * 60 * 1000,
+ max: 900,
+ keyGenerator: (req) =>
+ `remoteExitNodeGetToken:${req.body.newtId || ipKeyGenerator(req.ip || "")}`,
+ handler: (req, res, next) => {
+ const message = `You can only request an remoteExitNodeToken token ${900} times every ${15} minutes. Please try again later.`;
+ return next(createHttpError(HttpCode.TOO_MANY_REQUESTS, message));
+ },
+ store: createStore()
+ }),
+ remoteExitNode.getRemoteExitNodeToken
+);
+
+authRouter.post(
+ "/transfer-session-token",
+ verifyValidLicense,
+ rateLimit({
+ windowMs: 1 * 60 * 1000,
+ max: 60,
+ keyGenerator: (req) =>
+ `transferSessionToken:${ipKeyGenerator(req.ip || "")}`,
+ handler: (req, res, next) => {
+ const message = `You can only transfer a session token ${5} times every ${1} minute. Please try again later.`;
+ return next(createHttpError(HttpCode.TOO_MANY_REQUESTS, message));
+ },
+ store: createStore()
+ }),
+ auth.transferSession
+);
+
+authenticated.post(
+ "/license/activate",
+ verifyUserIsServerAdmin,
+ license.activateLicense
+);
+
+authenticated.get(
+ "/license/keys",
+ verifyUserIsServerAdmin,
+ license.listLicenseKeys
+);
+
+authenticated.delete(
+ "/license/:licenseKey",
+ verifyUserIsServerAdmin,
+ license.deleteLicenseKey
+);
+
+authenticated.post(
+ "/license/recheck",
+ verifyUserIsServerAdmin,
+ license.recheckStatus
+);
diff --git a/server/private/routers/generatedLicense/generateNewLicense.ts b/server/private/routers/generatedLicense/generateNewLicense.ts
new file mode 100644
index 00000000..fb6ce835
--- /dev/null
+++ b/server/private/routers/generatedLicense/generateNewLicense.ts
@@ -0,0 +1,73 @@
+import { Request, Response, NextFunction } from "express";
+import HttpCode from "@server/types/HttpCode";
+import createHttpError from "http-errors";
+import logger from "@server/logger";
+import { response as sendResponse } from "@server/lib/response";
+import privateConfig from "@server/private/lib/config";
+import { GenerateNewLicenseResponse } from "@server/routers/generatedLicense/types";
+
+async function createNewLicense(orgId: string, licenseData: any): Promise {
+ try {
+ const response = await fetch(
+ `https://api.fossorial.io/api/v1/license-internal/enterprise/${orgId}/create`,
+ {
+ method: "PUT",
+ headers: {
+ "api-key":
+ privateConfig.getRawPrivateConfig().server
+ .fossorial_api_key!,
+ "Content-Type": "application/json"
+ },
+ body: JSON.stringify(licenseData)
+ }
+ );
+
+ const data = await response.json();
+
+ logger.debug("Fossorial API response:", {data});
+ return data;
+ } catch (error) {
+ console.error("Error creating new license:", error);
+ throw error;
+ }
+}
+
+export async function generateNewLicense(
+ req: Request,
+ res: Response,
+ next: NextFunction
+): Promise {
+ try {
+ const { orgId } = req.params;
+
+ if (!orgId) {
+ return next(
+ createHttpError(
+ HttpCode.BAD_REQUEST,
+ "Organization ID is required"
+ )
+ );
+ }
+
+ logger.debug(`Generating new license for orgId: ${orgId}`);
+
+ const licenseData = req.body;
+ const apiResponse = await createNewLicense(orgId, licenseData);
+
+ return sendResponse(res, {
+ data: apiResponse.data,
+ success: apiResponse.success,
+ error: apiResponse.error,
+ message: apiResponse.message,
+ status: apiResponse.status
+ });
+ } catch (error) {
+ logger.error(error);
+ return next(
+ createHttpError(
+ HttpCode.INTERNAL_SERVER_ERROR,
+ "An error occurred while generating new license"
+ )
+ );
+ }
+}
diff --git a/server/private/routers/generatedLicense/index.ts b/server/private/routers/generatedLicense/index.ts
new file mode 100644
index 00000000..fa07430f
--- /dev/null
+++ b/server/private/routers/generatedLicense/index.ts
@@ -0,0 +1,2 @@
+export * from "./listGeneratedLicenses";
+export * from "./generateNewLicense";
diff --git a/server/private/routers/generatedLicense/listGeneratedLicenses.ts b/server/private/routers/generatedLicense/listGeneratedLicenses.ts
new file mode 100644
index 00000000..9b219453
--- /dev/null
+++ b/server/private/routers/generatedLicense/listGeneratedLicenses.ts
@@ -0,0 +1,72 @@
+import { Request, Response, NextFunction } from "express";
+import HttpCode from "@server/types/HttpCode";
+import createHttpError from "http-errors";
+import logger from "@server/logger";
+import { response as sendResponse } from "@server/lib/response";
+import privateConfig from "@server/private/lib/config";
+import { GeneratedLicenseKey, ListGeneratedLicenseKeysResponse } from "@server/routers/generatedLicense/types";
+
+async function fetchLicenseKeys(orgId: string): Promise {
+ try {
+ const response = await fetch(
+ `https://api.fossorial.io/api/v1/license-internal/enterprise/${orgId}/list`,
+ {
+ method: "GET",
+ headers: {
+ "api-key":
+ privateConfig.getRawPrivateConfig().server
+ .fossorial_api_key!,
+ "Content-Type": "application/json"
+ }
+ }
+ );
+
+ if (!response.ok) {
+ throw new Error(`HTTP error! status: ${response.status}`);
+ }
+
+ const data = await response.json();
+ return data;
+ } catch (error) {
+ console.error("Error fetching license keys:", error);
+ throw error;
+ }
+}
+
+export async function listSaasLicenseKeys(
+ req: Request,
+ res: Response,
+ next: NextFunction
+): Promise {
+ try {
+ const { orgId } = req.params;
+
+ if (!orgId) {
+ return next(
+ createHttpError(
+ HttpCode.BAD_REQUEST,
+ "Organization ID is required"
+ )
+ );
+ }
+
+ const apiResponse = await fetchLicenseKeys(orgId);
+ const keys: GeneratedLicenseKey[] = apiResponse.data.licenseKeys || [];
+
+ return sendResponse(res, {
+ data: keys,
+ success: true,
+ error: false,
+ message: "Successfully retrieved license keys",
+ status: HttpCode.OK
+ });
+ } catch (error) {
+ logger.error(error);
+ return next(
+ createHttpError(
+ HttpCode.INTERNAL_SERVER_ERROR,
+ "An error occurred while fetching license keys"
+ )
+ );
+ }
+}
diff --git a/server/routers/gerbil/privateCreateExitNode.ts b/server/private/routers/gerbil/createExitNode.ts
similarity index 98%
rename from server/routers/gerbil/privateCreateExitNode.ts
rename to server/private/routers/gerbil/createExitNode.ts
index 9c2a104d..9577d8e9 100644
--- a/server/routers/gerbil/privateCreateExitNode.ts
+++ b/server/private/routers/gerbil/createExitNode.ts
@@ -50,6 +50,7 @@ export async function createExitNode(
endpoint: `${subEndpoint}${subEndpoint != "" ? "." : ""}${config.getRawConfig().gerbil.base_endpoint}`,
address,
listenPort,
+ online: true,
reachableAt,
name: exitNodeName
})
diff --git a/server/routers/gerbil/privateReceiveBandwidth.ts b/server/private/routers/gerbil/receiveBandwidth.ts
similarity index 100%
rename from server/routers/gerbil/privateReceiveBandwidth.ts
rename to server/private/routers/gerbil/receiveBandwidth.ts
diff --git a/server/routers/private/hybrid.ts b/server/private/routers/hybrid.ts
similarity index 93%
rename from server/routers/private/hybrid.ts
rename to server/private/routers/hybrid.ts
index 2b59aa40..54c823a5 100644
--- a/server/routers/private/hybrid.ts
+++ b/server/private/routers/hybrid.ts
@@ -11,7 +11,7 @@
* This file is not licensed under the AGPLv3.
*/
-import { verifySessionRemoteExitNodeMiddleware } from "@server/middlewares/private/verifyRemoteExitNode";
+import { verifySessionRemoteExitNodeMiddleware } from "#private/middlewares/verifyRemoteExitNode";
import { Router } from "express";
import {
db,
@@ -55,22 +55,24 @@ import { NextFunction, Request, Response } from "express";
import createHttpError from "http-errors";
import { z } from "zod";
import { fromError } from "zod-validation-error";
-import { getTraefikConfig } from "../../lib/traefik";
+import { getTraefikConfig } from "#private/lib/traefik";
import {
generateGerbilConfig,
generateRelayMappings,
updateAndGenerateEndpointDestinations,
updateSiteBandwidth
-} from "../gerbil";
+} from "@server/routers/gerbil";
import * as gerbil from "@server/routers/gerbil";
import logger from "@server/logger";
import { decryptData } from "@server/lib/encryption";
-import { config } from "@server/lib/config";
+import config from "@server/lib/config";
+import privateConfig from "#private/lib/config";
import * as fs from "fs";
-import { exchangeSession } from "../badger";
+import { exchangeSession } from "@server/routers/badger";
import { validateResourceSessionToken } from "@server/auth/sessions/resource";
-import { checkExitNodeOrg, resolveExitNodes } from "@server/lib/exitNodes";
+import { checkExitNodeOrg, resolveExitNodes } from "#private/lib/exitNodes";
import { maxmindLookup } from "@server/db/maxmind";
+import { verifyResourceAccessToken } from "@server/auth/verifyResourceAccessToken";
// Zod schemas for request validation
const getResourceByDomainParamsSchema = z
@@ -161,6 +163,14 @@ const validateResourceSessionTokenBodySchema = z
})
.strict();
+const validateResourceAccessTokenBodySchema = z
+ .object({
+ accessTokenId: z.string().optional(),
+ resourceId: z.number().optional(),
+ accessToken: z.string()
+ })
+ .strict();
+
// Certificates by domains query validation
const getCertificatesByDomainsQuerySchema = z
.object({
@@ -211,9 +221,36 @@ export type UserSessionWithUser = {
};
// Root routes
-const hybridRouter = Router();
+export const hybridRouter = Router();
hybridRouter.use(verifySessionRemoteExitNodeMiddleware);
+hybridRouter.get(
+ "/general-config",
+ async (req: Request, res: Response, next: NextFunction) => {
+ return response(res, {
+ data: {
+ resource_session_request_param:
+ config.getRawConfig().server.resource_session_request_param,
+ resource_access_token_headers:
+ config.getRawConfig().server.resource_access_token_headers,
+ resource_access_token_param:
+ config.getRawConfig().server.resource_access_token_param,
+ session_cookie_name:
+ config.getRawConfig().server.session_cookie_name,
+ require_email_verification:
+ config.getRawConfig().flags?.require_email_verification ||
+ false,
+ resource_session_length_hours:
+ config.getRawConfig().server.resource_session_length_hours
+ },
+ success: true,
+ error: false,
+ message: "General config retrieved successfully",
+ status: HttpCode.OK
+ });
+ }
+);
+
hybridRouter.get(
"/traefik-config",
async (req: Request, res: Response, next: NextFunction) => {
@@ -232,8 +269,10 @@ hybridRouter.get(
const traefikConfig = await getTraefikConfig(
remoteExitNode.exitNodeId,
["newt", "local", "wireguard"], // Allow them to use all the site types
- true // But don't allow domain namespace resources
+ true, // But don't allow domain namespace resources
+ false // Dont include login pages
);
+
return response(res, {
data: traefikConfig,
success: true,
@@ -387,7 +426,7 @@ hybridRouter.get(
}
const encryptionKeyPath =
- config.getRawPrivateConfig().server.encryption_key_path;
+ privateConfig.getRawPrivateConfig().server.encryption_key_path;
if (!fs.existsSync(encryptionKeyPath)) {
throw new Error(
@@ -1100,6 +1139,52 @@ hybridRouter.post(
}
);
+// Validate resource session token
+hybridRouter.post(
+ "/resource/:resourceId/access-token/verify",
+ async (req: Request, res: Response, next: NextFunction) => {
+ try {
+ const parsedBody = validateResourceAccessTokenBodySchema.safeParse(
+ req.body
+ );
+ if (!parsedBody.success) {
+ return next(
+ createHttpError(
+ HttpCode.BAD_REQUEST,
+ fromError(parsedBody.error).toString()
+ )
+ );
+ }
+
+ const { accessToken, resourceId, accessTokenId } = parsedBody.data;
+
+ const result = await verifyResourceAccessToken({
+ accessTokenId,
+ accessToken,
+ resourceId
+ });
+
+ return response(res, {
+ data: result,
+ success: true,
+ error: false,
+ message: result.valid
+ ? "Resource access token is valid"
+ : "Resource access token is invalid or expired",
+ status: HttpCode.OK
+ });
+ } catch (error) {
+ logger.error(error);
+ return next(
+ createHttpError(
+ HttpCode.INTERNAL_SERVER_ERROR,
+ "Failed to validate resource session token"
+ )
+ );
+ }
+ }
+);
+
const geoIpLookupParamsSchema = z.object({
ip: z.string().ip()
});
@@ -1489,5 +1574,3 @@ hybridRouter.post(
}
}
);
-
-export default hybridRouter;
diff --git a/server/private/routers/integration.ts b/server/private/routers/integration.ts
new file mode 100644
index 00000000..d767424a
--- /dev/null
+++ b/server/private/routers/integration.ts
@@ -0,0 +1,42 @@
+/*
+ * 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 * as orgIdp from "#private/routers/orgIdp";
+import * as org from "#private/routers/org";
+
+import { Router } from "express";
+import {
+ verifyApiKey,
+ verifyApiKeyHasAction,
+ verifyApiKeyIsRoot,
+} from "@server/middlewares";
+import { ActionsEnum } from "@server/auth/actions";
+
+import { unauthenticated as ua, authenticated as a } from "@server/routers/integration";
+
+export const unauthenticated = ua;
+export const authenticated = a;
+
+authenticated.post(
+ `/org/:orgId/send-usage-notification`,
+ verifyApiKeyIsRoot, // We are the only ones who can use root key so its fine
+ verifyApiKeyHasAction(ActionsEnum.sendUsageNotification),
+ org.sendUsageNotification
+);
+
+authenticated.delete(
+ "/idp/:idpId",
+ verifyApiKeyIsRoot,
+ verifyApiKeyHasAction(ActionsEnum.deleteIdp),
+ orgIdp.deleteOrgIdp
+);
\ No newline at end of file
diff --git a/server/private/routers/internal.ts b/server/private/routers/internal.ts
new file mode 100644
index 00000000..b393b884
--- /dev/null
+++ b/server/private/routers/internal.ts
@@ -0,0 +1,38 @@
+/*
+ * 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 * as loginPage from "#private/routers/loginPage";
+import * as auth from "#private/routers/auth";
+import * as orgIdp from "#private/routers/orgIdp";
+import * as billing from "#private/routers/billing";
+import * as license from "#private/routers/license";
+
+import { verifySessionUserMiddleware } from "@server/middlewares";
+
+import { internalRouter as ir } from "@server/routers/internal";
+
+export const internalRouter = ir;
+
+internalRouter.get("/org/:orgId/idp", orgIdp.listOrgIdps);
+
+internalRouter.get("/org/:orgId/billing/tier", billing.getOrgTier);
+
+internalRouter.get("/login-page", loginPage.loadLoginPage);
+
+internalRouter.post(
+ "/get-session-transfer-token",
+ verifySessionUserMiddleware,
+ auth.getSessionTransferToken
+);
+
+internalRouter.get(`/license/status`, license.getLicenseStatus);
diff --git a/server/routers/license/activateLicense.ts b/server/private/routers/license/activateLicense.ts
similarity index 93%
rename from server/routers/license/activateLicense.ts
rename to server/private/routers/license/activateLicense.ts
index 832bc19d..f7745489 100644
--- a/server/routers/license/activateLicense.ts
+++ b/server/private/routers/license/activateLicense.ts
@@ -3,7 +3,7 @@ import HttpCode from "@server/types/HttpCode";
import createHttpError from "http-errors";
import logger from "@server/logger";
import { response as sendResponse } from "@server/lib/response";
-import license, { LicenseStatus } from "@server/license/license";
+import license from "#private/license/license";
import { z } from "zod";
import { fromError } from "zod-validation-error";
@@ -13,8 +13,6 @@ const bodySchema = z
})
.strict();
-export type ActivateLicenseStatus = LicenseStatus;
-
export async function activateLicense(
req: Request,
res: Response,
diff --git a/server/routers/license/deleteLicenseKey.ts b/server/private/routers/license/deleteLicenseKey.ts
similarity index 90%
rename from server/routers/license/deleteLicenseKey.ts
rename to server/private/routers/license/deleteLicenseKey.ts
index 37b74fee..922da0ad 100644
--- a/server/routers/license/deleteLicenseKey.ts
+++ b/server/private/routers/license/deleteLicenseKey.ts
@@ -8,9 +8,7 @@ import { fromError } from "zod-validation-error";
import { db } from "@server/db";
import { eq } from "drizzle-orm";
import { licenseKey } from "@server/db";
-import license, { LicenseStatus } from "@server/license/license";
-import { encrypt } from "@server/lib/crypto";
-import config from "@server/lib/config";
+import license from "#private/license/license";
const paramsSchema = z
.object({
@@ -18,8 +16,6 @@ const paramsSchema = z
})
.strict();
-export type DeleteLicenseKeyResponse = LicenseStatus;
-
export async function deleteLicenseKey(
req: Request,
res: Response,
diff --git a/server/routers/license/getLicenseStatus.ts b/server/private/routers/license/getLicenseStatus.ts
similarity index 87%
rename from server/routers/license/getLicenseStatus.ts
rename to server/private/routers/license/getLicenseStatus.ts
index e4f28882..838632f2 100644
--- a/server/routers/license/getLicenseStatus.ts
+++ b/server/private/routers/license/getLicenseStatus.ts
@@ -3,9 +3,8 @@ import HttpCode from "@server/types/HttpCode";
import createHttpError from "http-errors";
import logger from "@server/logger";
import { response as sendResponse } from "@server/lib/response";
-import license, { LicenseStatus } from "@server/license/license";
-
-export type GetLicenseStatusResponse = LicenseStatus;
+import license from "#private/license/license";
+import { GetLicenseStatusResponse } from "@server/routers/license/types";
export async function getLicenseStatus(
req: Request,
diff --git a/server/routers/license/index.ts b/server/private/routers/license/index.ts
similarity index 100%
rename from server/routers/license/index.ts
rename to server/private/routers/license/index.ts
diff --git a/server/routers/license/listLicenseKeys.ts b/server/private/routers/license/listLicenseKeys.ts
similarity index 86%
rename from server/routers/license/listLicenseKeys.ts
rename to server/private/routers/license/listLicenseKeys.ts
index d106abd7..4aa35335 100644
--- a/server/routers/license/listLicenseKeys.ts
+++ b/server/private/routers/license/listLicenseKeys.ts
@@ -3,9 +3,8 @@ import HttpCode from "@server/types/HttpCode";
import createHttpError from "http-errors";
import logger from "@server/logger";
import { response as sendResponse } from "@server/lib/response";
-import license, { LicenseKeyCache } from "@server/license/license";
-
-export type ListLicenseKeysResponse = LicenseKeyCache[];
+import license from "#private/license/license";
+import { ListLicenseKeysResponse } from "@server/routers/license/types";
export async function listLicenseKeys(
req: Request,
diff --git a/server/routers/license/recheckStatus.ts b/server/private/routers/license/recheckStatus.ts
similarity index 89%
rename from server/routers/license/recheckStatus.ts
rename to server/private/routers/license/recheckStatus.ts
index cd4bf779..27bf60bb 100644
--- a/server/routers/license/recheckStatus.ts
+++ b/server/private/routers/license/recheckStatus.ts
@@ -3,9 +3,7 @@ import HttpCode from "@server/types/HttpCode";
import createHttpError from "http-errors";
import logger from "@server/logger";
import { response as sendResponse } from "@server/lib/response";
-import license, { LicenseStatus } from "@server/license/license";
-
-export type RecheckStatusResponse = LicenseStatus;
+import license from "#private/license/license";
export async function recheckStatus(
req: Request,
diff --git a/server/routers/private/loginPage/createLoginPage.ts b/server/private/routers/loginPage/createLoginPage.ts
similarity index 95%
rename from server/routers/private/loginPage/createLoginPage.ts
rename to server/private/routers/loginPage/createLoginPage.ts
index fca29aae..cb0bb923 100644
--- a/server/routers/private/loginPage/createLoginPage.ts
+++ b/server/private/routers/loginPage/createLoginPage.ts
@@ -29,10 +29,11 @@ import logger from "@server/logger";
import { fromError } from "zod-validation-error";
import { eq, and } from "drizzle-orm";
import { validateAndConstructDomain } from "@server/lib/domainUtils";
-import { createCertificate } from "@server/routers/private/certificates/createCertificate";
-import { getOrgTierData } from "@server/routers/private/billing";
-import { TierId } from "@server/lib/private/billing/tiers";
+import { createCertificate } from "#private/routers/certificates/createCertificate";
+import { getOrgTierData } from "#private/lib/billing";
+import { TierId } from "@server/lib/billing/tiers";
import { build } from "@server/build";
+import { CreateLoginPageResponse } from "@server/routers/loginPage/types";
const paramsSchema = z
.object({
@@ -49,8 +50,6 @@ const bodySchema = z
export type CreateLoginPageBody = z.infer;
-export type CreateLoginPageResponse = LoginPage;
-
export async function createLoginPage(
req: Request,
res: Response,
diff --git a/server/routers/private/loginPage/deleteLoginPage.ts b/server/private/routers/loginPage/deleteLoginPage.ts
similarity index 97%
rename from server/routers/private/loginPage/deleteLoginPage.ts
rename to server/private/routers/loginPage/deleteLoginPage.ts
index 7cc957a2..bf7941e7 100644
--- a/server/routers/private/loginPage/deleteLoginPage.ts
+++ b/server/private/routers/loginPage/deleteLoginPage.ts
@@ -20,6 +20,7 @@ import createHttpError from "http-errors";
import logger from "@server/logger";
import { fromError } from "zod-validation-error";
import { eq, and } from "drizzle-orm";
+import { DeleteLoginPageResponse } from "@server/routers/loginPage/types";
const paramsSchema = z
.object({
@@ -28,8 +29,6 @@ const paramsSchema = z
})
.strict();
-export type DeleteLoginPageResponse = LoginPage;
-
export async function deleteLoginPage(
req: Request,
res: Response,
diff --git a/server/routers/private/loginPage/getLoginPage.ts b/server/private/routers/loginPage/getLoginPage.ts
similarity index 96%
rename from server/routers/private/loginPage/getLoginPage.ts
rename to server/private/routers/loginPage/getLoginPage.ts
index 9c9a18f5..76e20ffb 100644
--- a/server/routers/private/loginPage/getLoginPage.ts
+++ b/server/private/routers/loginPage/getLoginPage.ts
@@ -20,6 +20,7 @@ import HttpCode from "@server/types/HttpCode";
import createHttpError from "http-errors";
import logger from "@server/logger";
import { fromError } from "zod-validation-error";
+import { GetLoginPageResponse } from "@server/routers/loginPage/types";
const paramsSchema = z
.object({
@@ -40,10 +41,6 @@ async function query(orgId: string) {
return res?.loginPage;
}
-export type GetLoginPageResponse = NonNullable<
- Awaited>
->;
-
export async function getLoginPage(
req: Request,
res: Response,
diff --git a/server/routers/private/loginPage/index.ts b/server/private/routers/loginPage/index.ts
similarity index 100%
rename from server/routers/private/loginPage/index.ts
rename to server/private/routers/loginPage/index.ts
diff --git a/server/routers/private/loginPage/loadLoginPage.ts b/server/private/routers/loginPage/loadLoginPage.ts
similarity index 97%
rename from server/routers/private/loginPage/loadLoginPage.ts
rename to server/private/routers/loginPage/loadLoginPage.ts
index 91cc002e..133336b6 100644
--- a/server/routers/private/loginPage/loadLoginPage.ts
+++ b/server/private/routers/loginPage/loadLoginPage.ts
@@ -20,6 +20,7 @@ import HttpCode from "@server/types/HttpCode";
import createHttpError from "http-errors";
import logger from "@server/logger";
import { fromError } from "zod-validation-error";
+import { LoadLoginPageResponse } from "@server/routers/loginPage/types";
const querySchema = z.object({
resourceId: z.coerce.number().int().positive().optional(),
@@ -70,10 +71,6 @@ async function query(orgId: string | undefined, fullDomain: string) {
};
}
-export type LoadLoginPageResponse = NonNullable<
- Awaited>
-> & { orgId: string };
-
export async function loadLoginPage(
req: Request,
res: Response,
diff --git a/server/routers/private/loginPage/updateLoginPage.ts b/server/private/routers/loginPage/updateLoginPage.ts
similarity index 96%
rename from server/routers/private/loginPage/updateLoginPage.ts
rename to server/private/routers/loginPage/updateLoginPage.ts
index 9c19913d..4f2be084 100644
--- a/server/routers/private/loginPage/updateLoginPage.ts
+++ b/server/private/routers/loginPage/updateLoginPage.ts
@@ -22,10 +22,11 @@ import { fromError } from "zod-validation-error";
import { eq, and } from "drizzle-orm";
import { validateAndConstructDomain } from "@server/lib/domainUtils";
import { subdomainSchema } from "@server/lib/schemas";
-import { createCertificate } from "@server/routers/private/certificates/createCertificate";
-import { getOrgTierData } from "@server/routers/private/billing";
-import { TierId } from "@server/lib/private/billing/tiers";
+import { createCertificate } from "#private/routers/certificates/createCertificate";
+import { getOrgTierData } from "#private/lib/billing";
+import { TierId } from "@server/lib/billing/tiers";
import { build } from "@server/build";
+import { UpdateLoginPageResponse } from "@server/routers/loginPage/types";
const paramsSchema = z
.object({
@@ -55,8 +56,6 @@ const bodySchema = z
export type UpdateLoginPageBody = z.infer;
-export type UpdateLoginPageResponse = LoginPage;
-
export async function updateLoginPage(
req: Request,
res: Response,
diff --git a/server/private/routers/org/index.ts b/server/private/routers/org/index.ts
new file mode 100644
index 00000000..189c5323
--- /dev/null
+++ b/server/private/routers/org/index.ts
@@ -0,0 +1,14 @@
+/*
+ * 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.
+ */
+
+export * from "./sendUsageNotifications";
\ No newline at end of file
diff --git a/server/routers/org/privateSendUsageNotifications.ts b/server/private/routers/org/sendUsageNotifications.ts
similarity index 98%
rename from server/routers/org/privateSendUsageNotifications.ts
rename to server/private/routers/org/sendUsageNotifications.ts
index 8b2a773d..3ef27f91 100644
--- a/server/routers/org/privateSendUsageNotifications.ts
+++ b/server/private/routers/org/sendUsageNotifications.ts
@@ -22,8 +22,8 @@ import createHttpError from "http-errors";
import logger from "@server/logger";
import { fromError } from "zod-validation-error";
import { sendEmail } from "@server/emails";
-import NotifyUsageLimitApproaching from "@server/emails/templates/PrivateNotifyUsageLimitApproaching";
-import NotifyUsageLimitReached from "@server/emails/templates/PrivateNotifyUsageLimitReached";
+import NotifyUsageLimitApproaching from "@server/emails/templates/NotifyUsageLimitApproaching";
+import NotifyUsageLimitReached from "@server/emails/templates/NotifyUsageLimitReached";
import config from "@server/lib/config";
import { OpenAPITags, registry } from "@server/openApi";
diff --git a/server/routers/private/orgIdp/createOrgOidcIdp.ts b/server/private/routers/orgIdp/createOrgOidcIdp.ts
similarity index 96%
rename from server/routers/private/orgIdp/createOrgOidcIdp.ts
rename to server/private/routers/orgIdp/createOrgOidcIdp.ts
index 16697f98..02cef526 100644
--- a/server/routers/private/orgIdp/createOrgOidcIdp.ts
+++ b/server/private/routers/orgIdp/createOrgOidcIdp.ts
@@ -25,8 +25,9 @@ import { generateOidcRedirectUrl } from "@server/lib/idp/generateRedirectUrl";
import { encrypt } from "@server/lib/crypto";
import config from "@server/lib/config";
import { build } from "@server/build";
-import { getOrgTierData } from "@server/routers/private/billing";
-import { TierId } from "@server/lib/private/billing/tiers";
+import { getOrgTierData } from "#private/lib/billing";
+import { TierId } from "@server/lib/billing/tiers";
+import { CreateOrgIdpResponse } from "@server/routers/orgIdp/types";
const paramsSchema = z.object({ orgId: z.string().nonempty() }).strict();
@@ -47,11 +48,6 @@ const bodySchema = z
})
.strict();
-export type CreateOrgIdpResponse = {
- idpId: number;
- redirectUrl: string;
-};
-
// registry.registerPath({
// method: "put",
// path: "/idp/oidc",
diff --git a/server/private/routers/orgIdp/deleteOrgIdp.ts b/server/private/routers/orgIdp/deleteOrgIdp.ts
new file mode 100644
index 00000000..711d1ce3
--- /dev/null
+++ b/server/private/routers/orgIdp/deleteOrgIdp.ts
@@ -0,0 +1,108 @@
+/*
+ * 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 } from "@server/db";
+import response from "@server/lib/response";
+import HttpCode from "@server/types/HttpCode";
+import createHttpError from "http-errors";
+import logger from "@server/logger";
+import { fromError } from "zod-validation-error";
+import { idp, idpOidcConfig, idpOrg } from "@server/db";
+import { eq } from "drizzle-orm";
+import { OpenAPITags, registry } from "@server/openApi";
+
+const paramsSchema = z
+ .object({
+ orgId: z.string().optional(), // Optional; used with org idp in saas
+ idpId: z.coerce.number()
+ })
+ .strict();
+
+registry.registerPath({
+ method: "delete",
+ path: "/idp/{idpId}",
+ description: "Delete IDP.",
+ tags: [OpenAPITags.Idp],
+ request: {
+ params: paramsSchema
+ },
+ responses: {}
+});
+
+export async function deleteOrgIdp(
+ req: Request,
+ res: Response,
+ next: NextFunction
+): Promise {
+ try {
+ const parsedParams = paramsSchema.safeParse(req.params);
+ if (!parsedParams.success) {
+ return next(
+ createHttpError(
+ HttpCode.BAD_REQUEST,
+ fromError(parsedParams.error).toString()
+ )
+ );
+ }
+
+ const { idpId } = parsedParams.data;
+
+ // Check if IDP exists
+ const [existingIdp] = await db
+ .select()
+ .from(idp)
+ .where(eq(idp.idpId, idpId));
+
+ if (!existingIdp) {
+ return next(
+ createHttpError(
+ HttpCode.NOT_FOUND,
+ "IdP not found"
+ )
+ );
+ }
+
+ // Delete the IDP and its related records in a transaction
+ await db.transaction(async (trx) => {
+ // Delete OIDC config if it exists
+ await trx
+ .delete(idpOidcConfig)
+ .where(eq(idpOidcConfig.idpId, idpId));
+
+ // Delete IDP-org mappings
+ await trx
+ .delete(idpOrg)
+ .where(eq(idpOrg.idpId, idpId));
+
+ // Delete the IDP itself
+ await trx
+ .delete(idp)
+ .where(eq(idp.idpId, idpId));
+ });
+
+ return response(res, {
+ data: null,
+ success: true,
+ error: false,
+ message: "IdP deleted successfully",
+ status: HttpCode.OK
+ });
+ } catch (error) {
+ logger.error(error);
+ return next(
+ createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred")
+ );
+ }
+}
diff --git a/server/routers/private/orgIdp/getOrgIdp.ts b/server/private/routers/orgIdp/getOrgIdp.ts
similarity index 96%
rename from server/routers/private/orgIdp/getOrgIdp.ts
rename to server/private/routers/orgIdp/getOrgIdp.ts
index 73ccdcbb..0e6689fc 100644
--- a/server/routers/private/orgIdp/getOrgIdp.ts
+++ b/server/private/routers/orgIdp/getOrgIdp.ts
@@ -25,6 +25,7 @@ import { OpenAPITags, registry } from "@server/openApi";
import config from "@server/lib/config";
import { decrypt } from "@server/lib/crypto";
import { generateOidcRedirectUrl } from "@server/lib/idp/generateRedirectUrl";
+import { GetOrgIdpResponse } from "@server/routers/orgIdp/types";
const paramsSchema = z
.object({
@@ -47,10 +48,6 @@ async function query(idpId: number, orgId: string) {
return res;
}
-export type GetOrgIdpResponse = NonNullable<
- Awaited>
-> & { redirectUrl: string };
-
// registry.registerPath({
// method: "get",
// path: "/idp/{idpId}",
diff --git a/server/routers/private/orgIdp/index.ts b/server/private/routers/orgIdp/index.ts
similarity index 87%
rename from server/routers/private/orgIdp/index.ts
rename to server/private/routers/orgIdp/index.ts
index 99c30654..562582c6 100644
--- a/server/routers/private/orgIdp/index.ts
+++ b/server/private/routers/orgIdp/index.ts
@@ -14,4 +14,5 @@
export * from "./createOrgOidcIdp";
export * from "./getOrgIdp";
export * from "./listOrgIdps";
-export * from "./updateOrgOidcIdp";
\ No newline at end of file
+export * from "./updateOrgOidcIdp";
+export * from "./deleteOrgIdp";
\ No newline at end of file
diff --git a/server/routers/private/orgIdp/listOrgIdps.ts b/server/private/routers/orgIdp/listOrgIdps.ts
similarity index 95%
rename from server/routers/private/orgIdp/listOrgIdps.ts
rename to server/private/routers/orgIdp/listOrgIdps.ts
index 208732de..0c69ff8d 100644
--- a/server/routers/private/orgIdp/listOrgIdps.ts
+++ b/server/private/routers/orgIdp/listOrgIdps.ts
@@ -22,6 +22,7 @@ import { eq, sql } from "drizzle-orm";
import logger from "@server/logger";
import { fromError } from "zod-validation-error";
import { OpenAPITags, registry } from "@server/openApi";
+import { ListOrgIdpsResponse } from "@server/routers/orgIdp/types";
const querySchema = z
.object({
@@ -65,15 +66,6 @@ async function query(orgId: string, limit: number, offset: number) {
return res;
}
-export type ListOrgIdpsResponse = {
- idps: Awaited>;
- pagination: {
- total: number;
- limit: number;
- offset: number;
- };
-};
-
// registry.registerPath({
// method: "get",
// path: "/idp",
diff --git a/server/routers/private/orgIdp/updateOrgOidcIdp.ts b/server/private/routers/orgIdp/updateOrgOidcIdp.ts
similarity index 97%
rename from server/routers/private/orgIdp/updateOrgOidcIdp.ts
rename to server/private/routers/orgIdp/updateOrgOidcIdp.ts
index a3be85c3..c6e54240 100644
--- a/server/routers/private/orgIdp/updateOrgOidcIdp.ts
+++ b/server/private/routers/orgIdp/updateOrgOidcIdp.ts
@@ -24,10 +24,9 @@ import { idp, idpOidcConfig } from "@server/db";
import { eq, and } from "drizzle-orm";
import { encrypt } from "@server/lib/crypto";
import config from "@server/lib/config";
-import license from "@server/license/license";
import { build } from "@server/build";
-import { getOrgTierData } from "@server/routers/private/billing";
-import { TierId } from "@server/lib/private/billing/tiers";
+import { getOrgTierData } from "#private/lib/billing";
+import { TierId } from "@server/lib/billing/tiers";
const paramsSchema = z
.object({
diff --git a/server/routers/private/remoteExitNode/createRemoteExitNode.ts b/server/private/routers/remoteExitNode/createRemoteExitNode.ts
similarity index 86%
rename from server/routers/private/remoteExitNode/createRemoteExitNode.ts
rename to server/private/routers/remoteExitNode/createRemoteExitNode.ts
index ac4fd231..28102fab 100644
--- a/server/routers/private/remoteExitNode/createRemoteExitNode.ts
+++ b/server/private/routers/remoteExitNode/createRemoteExitNode.ts
@@ -21,25 +21,20 @@ import response from "@server/lib/response";
import { SqliteError } from "better-sqlite3";
import moment from "moment";
import { generateSessionToken } from "@server/auth/sessions/app";
-import { createRemoteExitNodeSession } from "@server/auth/sessions/privateRemoteExitNode";
+import { createRemoteExitNodeSession } from "#private/auth/sessions/remoteExitNode";
import { fromError } from "zod-validation-error";
import { hashPassword, verifyPassword } from "@server/auth/password";
import logger from "@server/logger";
import { and, eq } from "drizzle-orm";
import { getNextAvailableSubnet } from "@server/lib/exitNodes";
-import { usageService } from "@server/lib/private/billing/usageService";
-import { FeatureId } from "@server/lib/private/billing";
+import { usageService } from "@server/lib/billing/usageService";
+import { FeatureId } from "@server/lib/billing";
+import { CreateRemoteExitNodeResponse } from "@server/routers/remoteExitNode/types";
export const paramsSchema = z.object({
orgId: z.string()
});
-export type CreateRemoteExitNodeResponse = {
- token: string;
- remoteExitNodeId: string;
- secret: string;
-};
-
const bodySchema = z
.object({
remoteExitNodeId: z.string().length(15),
@@ -89,30 +84,25 @@ export async function createRemoteExitNode(
orgId,
FeatureId.REMOTE_EXIT_NODES
);
- if (!usage) {
- return next(
- createHttpError(
- HttpCode.NOT_FOUND,
- "No usage data found for this organization"
- )
- );
- }
- const rejectRemoteExitNodes = await usageService.checkLimitSet(
- orgId,
- false,
- FeatureId.REMOTE_EXIT_NODES,
- {
- ...usage,
- instantaneousValue: (usage.instantaneousValue || 0) + 1
- } // We need to add one to know if we are violating the limit
- );
- if (rejectRemoteExitNodes) {
- return next(
- createHttpError(
- HttpCode.FORBIDDEN,
- "Remote exit node limit exceeded. Please upgrade your plan or contact us at support@fossorial.io"
- )
+ if (usage) {
+ const rejectRemoteExitNodes = await usageService.checkLimitSet(
+ orgId,
+ false,
+ FeatureId.REMOTE_EXIT_NODES,
+ {
+ ...usage,
+ instantaneousValue: (usage.instantaneousValue || 0) + 1
+ } // We need to add one to know if we are violating the limit
);
+
+ if (rejectRemoteExitNodes) {
+ return next(
+ createHttpError(
+ HttpCode.FORBIDDEN,
+ "Remote exit node limit exceeded. Please upgrade your plan or contact us at support@fossorial.io"
+ )
+ );
+ }
}
const secretHash = await hashPassword(secret);
diff --git a/server/routers/private/remoteExitNode/deleteRemoteExitNode.ts b/server/private/routers/remoteExitNode/deleteRemoteExitNode.ts
similarity index 96%
rename from server/routers/private/remoteExitNode/deleteRemoteExitNode.ts
rename to server/private/routers/remoteExitNode/deleteRemoteExitNode.ts
index 84ef0fab..f7b9d56c 100644
--- a/server/routers/private/remoteExitNode/deleteRemoteExitNode.ts
+++ b/server/private/routers/remoteExitNode/deleteRemoteExitNode.ts
@@ -21,8 +21,8 @@ import HttpCode from "@server/types/HttpCode";
import createHttpError from "http-errors";
import logger from "@server/logger";
import { fromError } from "zod-validation-error";
-import { usageService } from "@server/lib/private/billing/usageService";
-import { FeatureId } from "@server/lib/private/billing";
+import { usageService } from "@server/lib/billing/usageService";
+import { FeatureId } from "@server/lib/billing";
const paramsSchema = z
.object({
diff --git a/server/routers/private/remoteExitNode/getRemoteExitNode.ts b/server/private/routers/remoteExitNode/getRemoteExitNode.ts
similarity index 97%
rename from server/routers/private/remoteExitNode/getRemoteExitNode.ts
rename to server/private/routers/remoteExitNode/getRemoteExitNode.ts
index 19c4f263..2ef3fb06 100644
--- a/server/routers/private/remoteExitNode/getRemoteExitNode.ts
+++ b/server/private/routers/remoteExitNode/getRemoteExitNode.ts
@@ -21,6 +21,7 @@ import HttpCode from "@server/types/HttpCode";
import createHttpError from "http-errors";
import logger from "@server/logger";
import { fromError } from "zod-validation-error";
+import { GetRemoteExitNodeResponse } from "@server/routers/remoteExitNode/types";
const getRemoteExitNodeSchema = z
.object({
@@ -52,8 +53,6 @@ async function query(remoteExitNodeId: string) {
return remoteExitNode;
}
-export type GetRemoteExitNodeResponse = Awaited>;
-
export async function getRemoteExitNode(
req: Request,
res: Response,
diff --git a/server/routers/private/remoteExitNode/getRemoteExitNodeToken.ts b/server/private/routers/remoteExitNode/getRemoteExitNodeToken.ts
similarity index 96%
rename from server/routers/private/remoteExitNode/getRemoteExitNodeToken.ts
rename to server/private/routers/remoteExitNode/getRemoteExitNodeToken.ts
index 3905f1f7..16ec4d5d 100644
--- a/server/routers/private/remoteExitNode/getRemoteExitNodeToken.ts
+++ b/server/private/routers/remoteExitNode/getRemoteExitNodeToken.ts
@@ -24,7 +24,7 @@ import { fromError } from "zod-validation-error";
import {
createRemoteExitNodeSession,
validateRemoteExitNodeSessionToken
-} from "@server/auth/sessions/privateRemoteExitNode";
+} from "#private/auth/sessions/remoteExitNode";
import { verifyPassword } from "@server/auth/password";
import logger from "@server/logger";
import config from "@server/lib/config";
@@ -35,8 +35,6 @@ export const remoteExitNodeGetTokenBodySchema = z.object({
token: z.string().optional()
});
-export type RemoteExitNodeGetTokenBody = z.infer;
-
export async function getRemoteExitNodeToken(
req: Request,
res: Response,
diff --git a/server/routers/private/remoteExitNode/handleRemoteExitNodePingMessage.ts b/server/private/routers/remoteExitNode/handleRemoteExitNodePingMessage.ts
similarity index 100%
rename from server/routers/private/remoteExitNode/handleRemoteExitNodePingMessage.ts
rename to server/private/routers/remoteExitNode/handleRemoteExitNodePingMessage.ts
diff --git a/server/routers/private/remoteExitNode/handleRemoteExitNodeRegisterMessage.ts b/server/private/routers/remoteExitNode/handleRemoteExitNodeRegisterMessage.ts
similarity index 100%
rename from server/routers/private/remoteExitNode/handleRemoteExitNodeRegisterMessage.ts
rename to server/private/routers/remoteExitNode/handleRemoteExitNodeRegisterMessage.ts
diff --git a/server/routers/private/remoteExitNode/index.ts b/server/private/routers/remoteExitNode/index.ts
similarity index 100%
rename from server/routers/private/remoteExitNode/index.ts
rename to server/private/routers/remoteExitNode/index.ts
diff --git a/server/routers/private/remoteExitNode/listRemoteExitNodes.ts b/server/private/routers/remoteExitNode/listRemoteExitNodes.ts
similarity index 94%
rename from server/routers/private/remoteExitNode/listRemoteExitNodes.ts
rename to server/private/routers/remoteExitNode/listRemoteExitNodes.ts
index d6d2466e..1029b1e9 100644
--- a/server/routers/private/remoteExitNode/listRemoteExitNodes.ts
+++ b/server/private/routers/remoteExitNode/listRemoteExitNodes.ts
@@ -21,6 +21,7 @@ import HttpCode from "@server/types/HttpCode";
import createHttpError from "http-errors";
import logger from "@server/logger";
import { fromError } from "zod-validation-error";
+import { ListRemoteExitNodesResponse } from "@server/routers/remoteExitNode/types";
const listRemoteExitNodesParamsSchema = z
.object({
@@ -43,7 +44,7 @@ const listRemoteExitNodesSchema = z.object({
.pipe(z.number().int().nonnegative())
});
-function queryRemoteExitNodes(orgId: string) {
+export function queryRemoteExitNodes(orgId: string) {
return db
.select({
remoteExitNodeId: remoteExitNodes.remoteExitNodeId,
@@ -65,11 +66,6 @@ function queryRemoteExitNodes(orgId: string) {
);
}
-export type ListRemoteExitNodesResponse = {
- remoteExitNodes: Awaited>;
- pagination: { total: number; limit: number; offset: number };
-};
-
export async function listRemoteExitNodes(
req: Request,
res: Response,
diff --git a/server/routers/private/remoteExitNode/pickRemoteExitNodeDefaults.ts b/server/private/routers/remoteExitNode/pickRemoteExitNodeDefaults.ts
similarity index 94%
rename from server/routers/private/remoteExitNode/pickRemoteExitNodeDefaults.ts
rename to server/private/routers/remoteExitNode/pickRemoteExitNodeDefaults.ts
index 684e616c..e5762f0d 100644
--- a/server/routers/private/remoteExitNode/pickRemoteExitNodeDefaults.ts
+++ b/server/private/routers/remoteExitNode/pickRemoteExitNodeDefaults.ts
@@ -19,11 +19,7 @@ import logger from "@server/logger";
import { generateId } from "@server/auth/sessions/app";
import { fromError } from "zod-validation-error";
import { z } from "zod";
-
-export type PickRemoteExitNodeDefaultsResponse = {
- remoteExitNodeId: string;
- secret: string;
-};
+import { PickRemoteExitNodeDefaultsResponse } from "@server/routers/remoteExitNode/types";
const paramsSchema = z
.object({
diff --git a/server/routers/private/remoteExitNode/quickStartRemoteExitNode.ts b/server/private/routers/remoteExitNode/quickStartRemoteExitNode.ts
similarity index 97%
rename from server/routers/private/remoteExitNode/quickStartRemoteExitNode.ts
rename to server/private/routers/remoteExitNode/quickStartRemoteExitNode.ts
index 689580b9..4d368152 100644
--- a/server/routers/private/remoteExitNode/quickStartRemoteExitNode.ts
+++ b/server/private/routers/remoteExitNode/quickStartRemoteExitNode.ts
@@ -12,7 +12,7 @@
*/
import { NextFunction, Request, Response } from "express";
-import { db, exitNodes, exitNodeOrgs } from "@server/db";
+import { db } from "@server/db";
import HttpCode from "@server/types/HttpCode";
import { remoteExitNodes } from "@server/db";
import createHttpError from "http-errors";
@@ -24,11 +24,7 @@ import { hashPassword } from "@server/auth/password";
import logger from "@server/logger";
import z from "zod";
import { fromError } from "zod-validation-error";
-
-export type QuickStartRemoteExitNodeResponse = {
- remoteExitNodeId: string;
- secret: string;
-};
+import { QuickStartRemoteExitNodeResponse } from "@server/routers/remoteExitNode/types";
const INSTALLER_KEY = "af4e4785-7e09-11f0-b93a-74563c4e2a7e";
diff --git a/src/lib/types/privateThemeTypes.tsx b/server/private/routers/ws/index.ts
similarity index 94%
rename from src/lib/types/privateThemeTypes.tsx
rename to server/private/routers/ws/index.ts
index de0b2d2b..4d803a3a 100644
--- a/src/lib/types/privateThemeTypes.tsx
+++ b/server/private/routers/ws/index.ts
@@ -11,3 +11,4 @@
* This file is not licensed under the AGPLv3.
*/
+export * from "./ws";
\ No newline at end of file
diff --git a/server/private/routers/ws/messageHandlers.ts b/server/private/routers/ws/messageHandlers.ts
new file mode 100644
index 00000000..71c2b253
--- /dev/null
+++ b/server/private/routers/ws/messageHandlers.ts
@@ -0,0 +1,26 @@
+/*
+ * 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 {
+ handleRemoteExitNodeRegisterMessage,
+ handleRemoteExitNodePingMessage,
+ startRemoteExitNodeOfflineChecker
+} from "#private/routers/remoteExitNode";
+import { MessageHandler } from "@server/routers/ws";
+
+export const messageHandlers: Record = {
+ "remoteExitNode/register": handleRemoteExitNodeRegisterMessage,
+ "remoteExitNode/ping": handleRemoteExitNodePingMessage
+};
+
+startRemoteExitNodeOfflineChecker(); // this is to handle the offline check for remote exit nodes
\ No newline at end of file
diff --git a/server/routers/ws/privateWs.ts b/server/private/routers/ws/ws.ts
similarity index 94%
rename from server/routers/ws/privateWs.ts
rename to server/private/routers/ws/ws.ts
index d94ccf5e..0122126f 100644
--- a/server/routers/ws/privateWs.ts
+++ b/server/private/routers/ws/ws.ts
@@ -14,7 +14,6 @@
import { Router, Request, Response } from "express";
import { Server as HttpServer } from "http";
import { WebSocket, WebSocketServer } from "ws";
-import { IncomingMessage } from "http";
import { Socket } from "net";
import {
Newt,
@@ -31,73 +30,20 @@ import { eq } from "drizzle-orm";
import { db } from "@server/db";
import { validateNewtSessionToken } from "@server/auth/sessions/newt";
import { validateOlmSessionToken } from "@server/auth/sessions/olm";
-import { messageHandlers } from "./messageHandlers";
import logger from "@server/logger";
-import redisManager from "@server/db/private/redis";
+import redisManager from "#private/lib/redis";
import { v4 as uuidv4 } from "uuid";
-import { validateRemoteExitNodeSessionToken } from "@server/auth/sessions/privateRemoteExitNode";
-import { rateLimitService } from "@server/db/private/rateLimit";
+import { validateRemoteExitNodeSessionToken } from "#private/auth/sessions/remoteExitNode";
+import { rateLimitService } from "#private/lib/rateLimit";
+import { messageHandlers } from "@server/routers/ws/messageHandlers";
+import { messageHandlers as privateMessageHandlers } from "#private/routers/ws/messageHandlers";
+import { AuthenticatedWebSocket, ClientType, WSMessage, TokenPayload, WebSocketRequest, RedisMessage } from "@server/routers/ws";
+
+// Merge public and private message handlers
+Object.assign(messageHandlers, privateMessageHandlers);
const MAX_PENDING_MESSAGES = 50; // Maximum messages to queue during connection setup
-// Custom interfaces
-interface WebSocketRequest extends IncomingMessage {
- token?: string;
-}
-
-type ClientType = "newt" | "olm" | "remoteExitNode";
-
-interface AuthenticatedWebSocket extends WebSocket {
- client?: Newt | Olm | RemoteExitNode;
- clientType?: ClientType;
- connectionId?: string;
- isFullyConnected?: boolean;
- pendingMessages?: Buffer[];
-}
-
-interface TokenPayload {
- client: Newt | Olm | RemoteExitNode;
- session: NewtSession | OlmSession | RemoteExitNodeSession;
- clientType: ClientType;
-}
-
-interface WSMessage {
- type: string;
- data: any;
-}
-
-interface HandlerResponse {
- message: WSMessage;
- broadcast?: boolean;
- excludeSender?: boolean;
- targetClientId?: string;
-}
-
-interface HandlerContext {
- message: WSMessage;
- senderWs: WebSocket;
- client: Newt | Olm | RemoteExitNode | undefined;
- clientType: ClientType;
- sendToClient: (clientId: string, message: WSMessage) => Promise;
- broadcastToAllExcept: (
- message: WSMessage,
- excludeClientId?: string
- ) => Promise;
- connectedClients: Map;
-}
-
-interface RedisMessage {
- type: "direct" | "broadcast";
- targetClientId?: string;
- excludeClientId?: string;
- message: WSMessage;
- fromNodeId: string;
-}
-
-export type MessageHandler = (
- context: HandlerContext
-) => Promise;
-
// Helper function to process a single message
const processMessage = async (
ws: AuthenticatedWebSocket,
@@ -875,10 +821,6 @@ const cleanup = async (): Promise => {
}
};
-// Handle process termination
-process.on("SIGTERM", cleanup);
-process.on("SIGINT", cleanup);
-
export {
router,
handleWSUpgrade,
diff --git a/server/routers/auth/index.ts b/server/routers/auth/index.ts
index 9db5931a..754478fc 100644
--- a/server/routers/auth/index.ts
+++ b/server/routers/auth/index.ts
@@ -10,10 +10,7 @@ export * from "./resetPassword";
export * from "./requestPasswordReset";
export * from "./setServerAdmin";
export * from "./initialSetupComplete";
-export * from "./privateQuickStart";
export * from "./validateSetupToken";
export * from "./changePassword";
export * from "./checkResourceSession";
-export * from "./securityKey";
-export * from "./privateGetSessionTransferToken";
-export * from "./privateTransferSession";
+export * from "./securityKey";
\ No newline at end of file
diff --git a/server/routers/auth/requestTotpSecret.ts b/server/routers/auth/requestTotpSecret.ts
index e6ae4fe4..7c122a44 100644
--- a/server/routers/auth/requestTotpSecret.ts
+++ b/server/routers/auth/requestTotpSecret.ts
@@ -110,10 +110,12 @@ export async function requestTotpSecret(
);
}
+ const appName = process.env.BRANDING_APP_NAME || "Pangolin"; // From the private config loading into env vars to seperate away the private config
+
const hex = crypto.getRandomValues(new Uint8Array(20));
const secret = encodeHex(hex);
const uri = createTOTPKeyURI(
- config.getRawPrivateConfig().branding?.app_name || "Pangolin",
+ appName,
user.email!,
hex
);
diff --git a/server/routers/auth/signup.ts b/server/routers/auth/signup.ts
index 0d4f6865..1f361b79 100644
--- a/server/routers/auth/signup.ts
+++ b/server/routers/auth/signup.ts
@@ -21,12 +21,12 @@ import { hashPassword } from "@server/auth/password";
import { checkValidInvite } from "@server/auth/checkValidInvite";
import { passwordSchema } from "@server/auth/passwordSchema";
import { UserType } from "@server/types/UserTypes";
-import { createUserAccountOrg } from "@server/lib/private/createUserAccountOrg";
+import { createUserAccountOrg } from "@server/lib/createUserAccountOrg";
import { build } from "@server/build";
import resend, {
AudienceIds,
moveEmailToAudience
-} from "@server/lib/private/resend";
+} from "#dynamic/lib/resend";
export const signupBodySchema = z.object({
email: z.string().toLowerCase().email(),
diff --git a/server/routers/auth/types.ts b/server/routers/auth/types.ts
new file mode 100644
index 00000000..bb5a1b4e
--- /dev/null
+++ b/server/routers/auth/types.ts
@@ -0,0 +1,8 @@
+export type TransferSessionResponse = {
+ valid: boolean;
+ cookie?: string;
+};
+
+export type GetSessionTransferTokenRenponse = {
+ token: string;
+};
\ No newline at end of file
diff --git a/server/routers/auth/verifyEmail.ts b/server/routers/auth/verifyEmail.ts
index 010ddf28..47a81c0a 100644
--- a/server/routers/auth/verifyEmail.ts
+++ b/server/routers/auth/verifyEmail.ts
@@ -10,7 +10,7 @@ import { eq } from "drizzle-orm";
import { isWithinExpirationDate } from "oslo";
import config from "@server/lib/config";
import logger from "@server/logger";
-import { freeLimitSet, limitsService } from "@server/lib/private/billing";
+import { freeLimitSet, limitsService } from "@server/lib/billing";
import { build } from "@server/build";
export const verifyEmailBody = z
diff --git a/server/routers/badger/verifySession.ts b/server/routers/badger/verifySession.ts
index c380e679..523163e6 100644
--- a/server/routers/badger/verifySession.ts
+++ b/server/routers/badger/verifySession.ts
@@ -33,9 +33,9 @@ import createHttpError from "http-errors";
import NodeCache from "node-cache";
import { z } from "zod";
import { fromError } from "zod-validation-error";
-import { getCountryCodeForIp, remoteGetCountryCodeForIp } from "@server/lib/geoip";
-import { getOrgTierData } from "@server/routers/private/billing";
-import { TierId } from "@server/lib/private/billing/tiers";
+import { getCountryCodeForIp } from "@server/lib/geoip";
+import { getOrgTierData } from "#dynamic/lib/billing";
+import { TierId } from "@server/lib/billing/tiers";
import { verifyPassword } from "@server/auth/password";
// We'll see if this speeds anything up
@@ -106,23 +106,23 @@ export async function verifyResourceSession(
const clientIp = requestIp
? (() => {
- logger.debug("Request IP:", { requestIp });
- if (requestIp.startsWith("[") && requestIp.includes("]")) {
- // if brackets are found, extract the IPv6 address from between the brackets
- const ipv6Match = requestIp.match(/\[(.*?)\]/);
- if (ipv6Match) {
- return ipv6Match[1];
- }
- }
+ logger.debug("Request IP:", { requestIp });
+ if (requestIp.startsWith("[") && requestIp.includes("]")) {
+ // if brackets are found, extract the IPv6 address from between the brackets
+ const ipv6Match = requestIp.match(/\[(.*?)\]/);
+ if (ipv6Match) {
+ return ipv6Match[1];
+ }
+ }
- // ivp4
- // split at last colon
- const lastColonIndex = requestIp.lastIndexOf(":");
- if (lastColonIndex !== -1) {
- return requestIp.substring(0, lastColonIndex);
- }
- return requestIp;
- })()
+ // ivp4
+ // split at last colon
+ const lastColonIndex = requestIp.lastIndexOf(":");
+ if (lastColonIndex !== -1) {
+ return requestIp.substring(0, lastColonIndex);
+ }
+ return requestIp;
+ })()
: undefined;
logger.debug("Client IP:", { clientIp });
@@ -137,11 +137,11 @@ export async function verifyResourceSession(
const resourceCacheKey = `resource:${cleanHost}`;
let resourceData:
| {
- resource: Resource | null;
- pincode: ResourcePincode | null;
- password: ResourcePassword | null;
- headerAuth: ResourceHeaderAuth | null;
- }
+ resource: Resource | null;
+ pincode: ResourcePincode | null;
+ password: ResourcePassword | null;
+ headerAuth: ResourceHeaderAuth | null;
+ }
| undefined = cache.get(resourceCacheKey);
if (!resourceData) {
@@ -194,11 +194,13 @@ export async function verifyResourceSession(
// otherwise its undefined and we pass
}
+ // IMPORTANT: ADD NEW AUTH CHECKS HERE OR WHEN TURNING OFF ALL OTHER AUTH METHODS IT WILL JUST PASS
if (
- !resource.sso &&
+ !sso &&
!pincode &&
!password &&
- !resource.emailWhitelistEnabled
+ !resource.emailWhitelistEnabled &&
+ !headerAuth
) {
logger.debug("Resource allowed because no auth");
return allowed(res);
@@ -213,21 +215,21 @@ export async function verifyResourceSession(
headers &&
headers[
config.getRawConfig().server.resource_access_token_headers.id
- ] &&
+ ] &&
headers[
config.getRawConfig().server.resource_access_token_headers.token
- ]
+ ]
) {
const accessTokenId =
headers[
config.getRawConfig().server.resource_access_token_headers
.id
- ];
+ ];
const accessToken =
headers[
config.getRawConfig().server.resource_access_token_headers
.token
- ];
+ ];
const { valid, error, tokenItem } = await verifyResourceAccessToken(
{
@@ -293,15 +295,42 @@ export async function verifyResourceSession(
}
// check for HTTP Basic Auth header
+ const clientHeaderAuthKey = `headerAuth:${clientHeaderAuth}`;
if (headerAuth && clientHeaderAuth) {
- if(cache.get(clientHeaderAuth)) {
- logger.debug("Resource allowed because header auth is valid (cached)");
+ if (cache.get(clientHeaderAuthKey)) {
+ logger.debug(
+ "Resource allowed because header auth is valid (cached)"
+ );
return allowed(res);
- }else if(await verifyPassword(clientHeaderAuth, headerAuth.headerAuthHash)){
- cache.set(clientHeaderAuth, clientHeaderAuth);
+ } else if (
+ await verifyPassword(
+ clientHeaderAuth,
+ headerAuth.headerAuthHash
+ )
+ ) {
+ cache.set(clientHeaderAuthKey, clientHeaderAuth);
logger.debug("Resource allowed because header auth is valid");
return allowed(res);
}
+
+ if ( // we dont want to redirect if this is the only auth method and we did not pass here
+ !sso &&
+ !pincode &&
+ !password &&
+ !resource.emailWhitelistEnabled
+ ) {
+ return notAllowed(res);
+ }
+ } else if (headerAuth) {
+ // if there are no other auth methods we need to return unauthorized if nothing is provided
+ if (
+ !sso &&
+ !pincode &&
+ !password &&
+ !resource.emailWhitelistEnabled
+ ) {
+ return notAllowed(res);
+ }
}
if (!sessions) {
@@ -477,7 +506,11 @@ function extractResourceSessionToken(
return latest.token;
}
-async function notAllowed(res: Response, redirectPath?: string, orgId?: string) {
+async function notAllowed(
+ res: Response,
+ redirectPath?: string,
+ orgId?: string
+) {
let loginPage: LoginPage | null = null;
if (orgId) {
const { tier } = await getOrgTierData(orgId); // returns null in oss
@@ -491,14 +524,11 @@ async function notAllowed(res: Response, redirectPath?: string, orgId?: string)
let endpoint: string;
if (loginPage && loginPage.domainId && loginPage.fullDomain) {
- const secure = config.getRawConfig().app.dashboard_url?.startsWith("https");
+ const secure = config
+ .getRawConfig()
+ .app.dashboard_url?.startsWith("https");
const method = secure ? "https" : "http";
endpoint = `${method}://${loginPage.fullDomain}`;
- } else if (config.isManagedMode()) {
- endpoint =
- config.getRawConfig().managed?.redirect_endpoint ||
- config.getRawConfig().managed?.endpoint ||
- "";
} else {
endpoint = config.getRawConfig().app.dashboard_url!;
}
@@ -530,39 +560,6 @@ function allowed(res: Response, userData?: BasicUserData) {
return response(res, data);
}
-async function createAccessTokenSession(
- res: Response,
- resource: Resource,
- tokenItem: ResourceAccessToken
-) {
- const token = generateSessionToken();
- const sess = await createResourceSession({
- resourceId: resource.resourceId,
- token,
- accessTokenId: tokenItem.accessTokenId,
- sessionLength: tokenItem.sessionLength,
- expiresAt: tokenItem.expiresAt,
- doNotExtend: tokenItem.expiresAt ? true : false
- });
- const cookieName = `${config.getRawConfig().server.session_cookie_name}`;
- const cookie = serializeResourceSessionCookie(
- cookieName,
- resource.fullDomain!,
- token,
- !resource.ssl,
- new Date(sess.expiresAt)
- );
- res.appendHeader("Set-Cookie", cookie);
- logger.debug("Access token is valid, creating new session");
- return response(res, {
- data: { valid: true },
- success: true,
- error: false,
- message: "Access allowed",
- status: HttpCode.OK
- });
-}
-
async function isUserAllowedToAccessResource(
userSessionId: string,
resource: Resource
@@ -803,11 +800,7 @@ async function isIpInGeoIP(ip: string, countryCode: string): Promise {
let cachedCountryCode: string | undefined = cache.get(geoIpCacheKey);
if (!cachedCountryCode) {
- if (config.isManagedMode()) {
- cachedCountryCode = await remoteGetCountryCodeForIp(ip);
- } else {
- cachedCountryCode = await getCountryCodeForIp(ip); // do it locally
- }
+ cachedCountryCode = await getCountryCodeForIp(ip); // do it locally
// Cache for longer since IP geolocation doesn't change frequently
cache.set(geoIpCacheKey, cachedCountryCode, 300); // 5 minutes
}
@@ -817,7 +810,9 @@ async function isIpInGeoIP(ip: string, countryCode: string): Promise {
return cachedCountryCode?.toUpperCase() === countryCode.toUpperCase();
}
-function extractBasicAuth(headers: Record | undefined): string | undefined {
+function extractBasicAuth(
+ headers: Record | undefined
+): string | undefined {
if (!headers || (!headers.authorization && !headers.Authorization)) {
return;
}
@@ -833,8 +828,9 @@ function extractBasicAuth(headers: Record | undefined): string |
try {
// Extract the base64 encoded credentials
return authHeader.slice("Basic ".length);
-
} catch (error) {
- logger.debug("Basic Auth: Failed to decode credentials", { error: error instanceof Error ? error.message : "Unknown error" });
+ logger.debug("Basic Auth: Failed to decode credentials", {
+ error: error instanceof Error ? error.message : "Unknown error"
+ });
}
}
diff --git a/server/routers/billing/types.ts b/server/routers/billing/types.ts
new file mode 100644
index 00000000..2ec5a1b1
--- /dev/null
+++ b/server/routers/billing/types.ts
@@ -0,0 +1,17 @@
+import { Limit, Subscription, SubscriptionItem, Usage } from "@server/db";
+
+export type GetOrgSubscriptionResponse = {
+ subscription: Subscription | null;
+ items: SubscriptionItem[];
+};
+
+export type GetOrgUsageResponse = {
+ usage: Usage[];
+ limits: Limit[];
+};
+
+export type GetOrgTierResponse = {
+ tier: string | null;
+ active: boolean;
+};
+
diff --git a/server/routers/billing/webhooks.ts b/server/routers/billing/webhooks.ts
new file mode 100644
index 00000000..0ca38a8a
--- /dev/null
+++ b/server/routers/billing/webhooks.ts
@@ -0,0 +1,14 @@
+import createHttpError from "http-errors";
+import { Request, Response, NextFunction } from "express";
+import HttpCode from "@server/types/HttpCode";
+
+export async function billingWebhookHandler(
+ req: Request,
+ res: Response,
+ next: NextFunction
+): Promise {
+ // return not found
+ return next(
+ createHttpError(HttpCode.NOT_FOUND, "This endpoint is not in use")
+ );
+}
\ No newline at end of file
diff --git a/server/routers/certificates/createCertificate.ts b/server/routers/certificates/createCertificate.ts
new file mode 100644
index 00000000..e160e644
--- /dev/null
+++ b/server/routers/certificates/createCertificate.ts
@@ -0,0 +1,5 @@
+import { db, Transaction } from "@server/db";
+
+export async function createCertificate(domainId: string, domain: string, trx: Transaction | typeof db) {
+ return;
+}
\ No newline at end of file
diff --git a/server/routers/certificates/types.ts b/server/routers/certificates/types.ts
new file mode 100644
index 00000000..80136de8
--- /dev/null
+++ b/server/routers/certificates/types.ts
@@ -0,0 +1,13 @@
+export type GetCertificateResponse = {
+ certId: number;
+ domain: string;
+ domainId: string;
+ wildcard: boolean;
+ status: string; // pending, requested, valid, expired, failed
+ expiresAt: string | null;
+ lastRenewalAttempt: Date | null;
+ createdAt: string;
+ updatedAt: string;
+ errorMessage?: string | null;
+ renewalCount: number;
+}
\ No newline at end of file
diff --git a/server/routers/client/createClient.ts b/server/routers/client/createClient.ts
index e7762223..cb2bbd6e 100644
--- a/server/routers/client/createClient.ts
+++ b/server/routers/client/createClient.ts
@@ -24,7 +24,7 @@ import { hashPassword } from "@server/auth/password";
import { isValidCIDR, isValidIP } from "@server/lib/validators";
import { isIpInCidr } from "@server/lib/ip";
import { OpenAPITags, registry } from "@server/openApi";
-import { listExitNodes } from "@server/lib/exitNodes";
+import { listExitNodes } from "#dynamic/lib/exitNodes";
const createClientParamsSchema = z
.object({
diff --git a/server/routers/client/targets.ts b/server/routers/client/targets.ts
index e34a23e9..e5c46d70 100644
--- a/server/routers/client/targets.ts
+++ b/server/routers/client/targets.ts
@@ -1,4 +1,4 @@
-import { sendToClient } from "../ws";
+import { sendToClient } from "#dynamic/routers/ws";
export async function addTargets(
newtId: string,
diff --git a/server/routers/client/updateClient.ts b/server/routers/client/updateClient.ts
index f60f14a0..884a9864 100644
--- a/server/routers/client/updateClient.ts
+++ b/server/routers/client/updateClient.ts
@@ -17,7 +17,7 @@ import {
addPeer as olmAddPeer,
deletePeer as olmDeletePeer
} from "../olm/peers";
-import { sendToExitNode } from "@server/lib/exitNodes";
+import { sendToExitNode } from "#dynamic/lib/exitNodes";
const updateClientParamsSchema = z
.object({
@@ -29,7 +29,7 @@ const updateClientSchema = z
.object({
name: z.string().min(1).max(255).optional(),
siteIds: z
- .array(z.string().transform(Number).pipe(z.number()))
+ .array(z.number().int().positive())
.optional()
})
.strict();
diff --git a/server/routers/domain/createOrgDomain.ts b/server/routers/domain/createOrgDomain.ts
index 3744f044..d0e8a72b 100644
--- a/server/routers/domain/createOrgDomain.ts
+++ b/server/routers/domain/createOrgDomain.ts
@@ -9,8 +9,8 @@ import { fromError } from "zod-validation-error";
import { subdomainSchema } from "@server/lib/schemas";
import { generateId } from "@server/auth/sessions/app";
import { eq, and } from "drizzle-orm";
-import { usageService } from "@server/lib/private/billing/usageService";
-import { FeatureId } from "@server/lib/private/billing";
+import { usageService } from "@server/lib/billing/usageService";
+import { FeatureId } from "@server/lib/billing";
import { isSecondLevelDomain, isValidDomain } from "@server/lib/validators";
import { build } from "@server/build";
import config from "@server/lib/config";
@@ -82,7 +82,7 @@ export async function createOrgDomain(
)
);
}
- } else if (build == "enterprise" || build == "saas") {
+ } else if (build == "saas") {
if (type !== "ns" && type !== "cname") {
return next(
createHttpError(
@@ -92,6 +92,7 @@ export async function createOrgDomain(
);
}
}
+ // allow wildacard, cname, and ns in enterprise
// Validate organization exists
if (!isValidDomain(baseDomain)) {
@@ -104,7 +105,7 @@ export async function createOrgDomain(
// many providers dont allow cname for this. Lets prevent it for the user for now
return next(
createHttpError(
- HttpCode.BAD_REQUEST,
+ HttpCode.BAD_REQUEST,
"You cannot create a CNAME record on a root domain. RFC 1912 § 2.4 prohibits CNAME records at the zone apex. Please use a subdomain."
)
);
@@ -253,7 +254,7 @@ export async function createOrgDomain(
domainId,
baseDomain,
type,
- verified: build == "oss" ? true : false
+ verified: type === "wildcard" ? true : false
})
.returning();
diff --git a/server/routers/domain/deleteOrgDomain.ts b/server/routers/domain/deleteOrgDomain.ts
index 8932733c..8836584b 100644
--- a/server/routers/domain/deleteOrgDomain.ts
+++ b/server/routers/domain/deleteOrgDomain.ts
@@ -7,8 +7,8 @@ import createHttpError from "http-errors";
import logger from "@server/logger";
import { fromError } from "zod-validation-error";
import { and, eq } from "drizzle-orm";
-import { usageService } from "@server/lib/private/billing/usageService";
-import { FeatureId } from "@server/lib/private/billing";
+import { usageService } from "@server/lib/billing/usageService";
+import { FeatureId } from "@server/lib/billing";
const paramsSchema = z
.object({
diff --git a/server/routers/domain/index.ts b/server/routers/domain/index.ts
index e833e532..c0cafafe 100644
--- a/server/routers/domain/index.ts
+++ b/server/routers/domain/index.ts
@@ -1,6 +1,4 @@
export * from "./listDomains";
export * from "./createOrgDomain";
export * from "./deleteOrgDomain";
-export * from "./privateListDomainNamespaces";
-export * from "./privateCheckDomainNamespaceAvailability";
export * from "./restartOrgDomain";
\ No newline at end of file
diff --git a/server/routers/domain/types.ts b/server/routers/domain/types.ts
new file mode 100644
index 00000000..4ae48fb1
--- /dev/null
+++ b/server/routers/domain/types.ts
@@ -0,0 +1,8 @@
+export type CheckDomainAvailabilityResponse = {
+ available: boolean;
+ options: {
+ domainNamespaceId: string;
+ domainId: string;
+ fullDomain: string;
+ }[];
+};
\ No newline at end of file
diff --git a/server/routers/external.ts b/server/routers/external.ts
index 3a96bb03..8bd72f62 100644
--- a/server/routers/external.ts
+++ b/server/routers/external.ts
@@ -13,7 +13,6 @@ import * as siteResource from "./siteResource";
import * as supporterKey from "./supporterKey";
import * as accessToken from "./accessToken";
import * as idp from "./idp";
-import * as license from "./license";
import * as apiKeys from "./apiKeys";
import HttpCode from "@server/types/HttpCode";
import {
@@ -38,25 +37,13 @@ import {
verifyUserIsOrgOwner,
verifySiteResourceAccess
} from "@server/middlewares";
-import {
- verifyCertificateAccess,
- verifyRemoteExitNodeAccess,
- verifyIdpAccess,
- verifyLoginPageAccess
-} from "@server/middlewares/private";
-import { createStore } from "@server/lib/private/rateLimitStore";
import { ActionsEnum } from "@server/auth/actions";
import { createNewt, getNewtToken } from "./newt";
import { getOlmToken } from "./olm";
import rateLimit, { ipKeyGenerator } from "express-rate-limit";
import createHttpError from "http-errors";
-import * as certificates from "./private/certificates";
-import * as billing from "@server/routers/private/billing";
-import { quickStart } from "./auth/privateQuickStart";
import { build } from "@server/build";
-import * as remoteExitNode from "@server/routers/private/remoteExitNode";
-import * as loginPage from "@server/routers/private/loginPage";
-import * as orgIdp from "@server/routers/private/orgIdp";
+import { createStore } from "#dynamic/lib/rateLimitStore";
// Root routes
export const unauthenticated = Router();
@@ -65,45 +52,6 @@ unauthenticated.get("/", (_, res) => {
res.status(HttpCode.OK).json({ message: "Healthy" });
});
-if (build === "saas") {
- unauthenticated.post(
- "/quick-start",
- rateLimit({
- windowMs: 15 * 60 * 1000,
- max: 100,
- keyGenerator: (req) => req.path,
- handler: (req, res, next) => {
- const message = `We're too busy right now. Please try again later.`;
- return next(
- createHttpError(HttpCode.TOO_MANY_REQUESTS, message)
- );
- },
- store: createStore()
- }),
- quickStart
- );
-}
-
-if (build !== "oss") {
- unauthenticated.post(
- "/remote-exit-node/quick-start",
- rateLimit({
- windowMs: 60 * 60 * 1000,
- max: 5,
- keyGenerator: (req) =>
- `${req.path}:${ipKeyGenerator(req.ip || "")}`,
- handler: (req, res, next) => {
- const message = `You can only create 5 remote exit nodes every hour. Please try again later.`;
- return next(
- createHttpError(HttpCode.TOO_MANY_REQUESTS, message)
- );
- },
- store: createStore()
- }),
- remoteExitNode.quickStartRemoteExitNode
- );
-}
-
// Authenticated Root routes
export const authenticated = Router();
authenticated.use(verifySessionUserMiddleware);
@@ -727,45 +675,7 @@ authenticated.post(
idp.updateOidcIdp
);
-if (build !== "oss") {
- authenticated.put(
- "/org/:orgId/idp/oidc",
- verifyOrgAccess,
- verifyUserHasAction(ActionsEnum.createIdp),
- orgIdp.createOrgOidcIdp
- );
- authenticated.post(
- "/org/:orgId/idp/:idpId/oidc",
- verifyOrgAccess,
- verifyIdpAccess,
- verifyUserHasAction(ActionsEnum.updateIdp),
- orgIdp.updateOrgOidcIdp
- );
-
- authenticated.delete(
- "/org/:orgId/idp/:idpId",
- verifyOrgAccess,
- verifyIdpAccess,
- verifyUserHasAction(ActionsEnum.deleteIdp),
- idp.deleteIdp
- );
-
- authenticated.get(
- "/org/:orgId/idp/:idpId",
- verifyOrgAccess,
- verifyIdpAccess,
- verifyUserHasAction(ActionsEnum.getIdp),
- orgIdp.getOrgIdp
- );
-
- authenticated.get(
- "/org/:orgId/idp",
- verifyOrgAccess,
- verifyUserHasAction(ActionsEnum.listIdps),
- orgIdp.listOrgIdps
- );
-}
authenticated.delete("/idp/:idpId", verifyUserIsServerAdmin, idp.deleteIdp);
@@ -795,36 +705,10 @@ authenticated.get(
idp.listIdpOrgPolicies
);
-if (build !== "oss") {
- authenticated.get("/org/:orgId/idp", orgIdp.listOrgIdps); // anyone can see this; it's just a list of idp names and ids
-}
+
authenticated.get("/idp", idp.listIdps); // anyone can see this; it's just a list of idp names and ids
authenticated.get("/idp/:idpId", verifyUserIsServerAdmin, idp.getIdp);
-authenticated.post(
- "/license/activate",
- verifyUserIsServerAdmin,
- license.activateLicense
-);
-
-authenticated.get(
- "/license/keys",
- verifyUserIsServerAdmin,
- license.listLicenseKeys
-);
-
-authenticated.delete(
- "/license/:licenseKey",
- verifyUserIsServerAdmin,
- license.deleteLicenseKey
-);
-
-authenticated.post(
- "/license/recheck",
- verifyUserIsServerAdmin,
- license.recheckStatus
-);
-
authenticated.get(
`/api-key/:apiKeyId`,
verifyUserIsServerAdmin,
@@ -930,126 +814,6 @@ authenticated.delete(
domain.deleteAccountDomain
);
-if (build !== "oss") {
- authenticated.get(
- "/org/:orgId/certificate/:domainId/:domain",
- verifyOrgAccess,
- verifyCertificateAccess,
- verifyUserHasAction(ActionsEnum.getCertificate),
- certificates.getCertificate
- );
-
- authenticated.post(
- "/org/:orgId/certificate/:certId/restart",
- verifyOrgAccess,
- verifyCertificateAccess,
- verifyUserHasAction(ActionsEnum.restartCertificate),
- certificates.restartCertificate
- );
-
- authenticated.post(
- "/org/:orgId/billing/create-checkout-session",
- verifyOrgAccess,
- verifyUserHasAction(ActionsEnum.billing),
- billing.createCheckoutSession
- );
-
- authenticated.post(
- "/org/:orgId/billing/create-portal-session",
- verifyOrgAccess,
- verifyUserHasAction(ActionsEnum.billing),
- billing.createPortalSession
- );
-
- authenticated.get(
- "/org/:orgId/billing/subscription",
- verifyOrgAccess,
- verifyUserHasAction(ActionsEnum.billing),
- billing.getOrgSubscription
- );
-
- authenticated.get(
- "/org/:orgId/billing/usage",
- verifyOrgAccess,
- verifyUserHasAction(ActionsEnum.billing),
- billing.getOrgUsage
- );
-
- authenticated.get("/domain/namespaces", domain.listDomainNamespaces);
-
- authenticated.get(
- "/domain/check-namespace-availability",
- domain.checkDomainNamespaceAvailability
- );
-
- authenticated.put(
- "/org/:orgId/remote-exit-node",
- verifyOrgAccess,
- verifyUserHasAction(ActionsEnum.createRemoteExitNode),
- remoteExitNode.createRemoteExitNode
- );
-
- authenticated.get(
- "/org/:orgId/remote-exit-nodes",
- verifyOrgAccess,
- verifyUserHasAction(ActionsEnum.listRemoteExitNode),
- remoteExitNode.listRemoteExitNodes
- );
-
- authenticated.get(
- "/org/:orgId/remote-exit-node/:remoteExitNodeId",
- verifyOrgAccess,
- verifyRemoteExitNodeAccess,
- verifyUserHasAction(ActionsEnum.getRemoteExitNode),
- remoteExitNode.getRemoteExitNode
- );
-
- authenticated.get(
- "/org/:orgId/pick-remote-exit-node-defaults",
- verifyOrgAccess,
- verifyUserHasAction(ActionsEnum.createRemoteExitNode),
- remoteExitNode.pickRemoteExitNodeDefaults
- );
-
- authenticated.delete(
- "/org/:orgId/remote-exit-node/:remoteExitNodeId",
- verifyOrgAccess,
- verifyRemoteExitNodeAccess,
- verifyUserHasAction(ActionsEnum.deleteRemoteExitNode),
- remoteExitNode.deleteRemoteExitNode
- );
-
- authenticated.put(
- "/org/:orgId/login-page",
- verifyOrgAccess,
- verifyUserHasAction(ActionsEnum.createLoginPage),
- loginPage.createLoginPage
- );
-
- authenticated.post(
- "/org/:orgId/login-page/:loginPageId",
- verifyOrgAccess,
- verifyLoginPageAccess,
- verifyUserHasAction(ActionsEnum.updateLoginPage),
- loginPage.updateLoginPage
- );
-
- authenticated.delete(
- "/org/:orgId/login-page/:loginPageId",
- verifyOrgAccess,
- verifyLoginPageAccess,
- verifyUserHasAction(ActionsEnum.deleteLoginPage),
- loginPage.deleteLoginPage
- );
-
- authenticated.get(
- "/org/:orgId/login-page",
- verifyOrgAccess,
- verifyUserHasAction(ActionsEnum.getLoginPage),
- loginPage.getLoginPage
- );
-}
-
// Auth routes
export const authRouter = Router();
unauthenticated.use("/auth", authRouter);
@@ -1129,26 +893,6 @@ authRouter.post(
getOlmToken
);
-if (build !== "oss") {
- authRouter.post(
- "/remoteExitNode/get-token",
- rateLimit({
- windowMs: 15 * 60 * 1000,
- max: 900,
- keyGenerator: (req) =>
- `remoteExitNodeGetToken:${req.body.newtId || ipKeyGenerator(req.ip || "")}`,
- handler: (req, res, next) => {
- const message = `You can only request an remoteExitNodeToken token ${900} times every ${15} minutes. Please try again later.`;
- return next(
- createHttpError(HttpCode.TOO_MANY_REQUESTS, message)
- );
- },
- store: createStore()
- }),
- remoteExitNode.getRemoteExitNodeToken
- );
-}
-
authRouter.post(
"/2fa/enable",
rateLimit({
@@ -1316,26 +1060,6 @@ authRouter.post(
resource.authWithWhitelist
);
-if (build !== "oss") {
- authRouter.post(
- "/transfer-session-token",
- rateLimit({
- windowMs: 1 * 60 * 1000,
- max: 60,
- keyGenerator: (req) =>
- `transferSessionToken:${ipKeyGenerator(req.ip || "")}`,
- handler: (req, res, next) => {
- const message = `You can only transfer a session token ${5} times every ${1} minute. Please try again later.`;
- return next(
- createHttpError(HttpCode.TOO_MANY_REQUESTS, message)
- );
- },
- store: createStore()
- }),
- auth.transferSession
- );
-}
-
authRouter.post(
"/resource/:resourceId/access-token",
resource.authWithAccessToken
diff --git a/server/routers/generatedLicense/types.ts b/server/routers/generatedLicense/types.ts
new file mode 100644
index 00000000..4c5efed7
--- /dev/null
+++ b/server/routers/generatedLicense/types.ts
@@ -0,0 +1,30 @@
+export type GeneratedLicenseKey = {
+ instanceName: string | null;
+ licenseKey: string;
+ expiresAt: string;
+ isValid: boolean;
+ createdAt: string;
+ tier: string;
+ type: string;
+};
+
+export type ListGeneratedLicenseKeysResponse = GeneratedLicenseKey[];
+
+export type NewLicenseKey = {
+ licenseKey: {
+ id: number;
+ instanceName: string | null;
+ instanceId: string;
+ licenseKey: string;
+ tier: string;
+ type: string;
+ quantity: number;
+ isValid: boolean;
+ updatedAt: string;
+ createdAt: string;
+ expiresAt: string;
+ orgId: string;
+ };
+};
+
+export type GenerateNewLicenseResponse = NewLicenseKey;
\ No newline at end of file
diff --git a/server/routers/gerbil/createExitNode.ts b/server/routers/gerbil/createExitNode.ts
index d4e6d43a..5903cdd4 100644
--- a/server/routers/gerbil/createExitNode.ts
+++ b/server/routers/gerbil/createExitNode.ts
@@ -30,6 +30,7 @@ export async function createExitNode(publicKey: string, reachableAt: string | un
publicKey,
endpoint: `${subEndpoint}${subEndpoint != "" ? "." : ""}${config.getRawConfig().gerbil.base_endpoint}`,
address,
+ online: true,
listenPort,
reachableAt,
name: exitNodeName
diff --git a/server/routers/gerbil/getConfig.ts b/server/routers/gerbil/getConfig.ts
index afae4009..bb581ced 100644
--- a/server/routers/gerbil/getConfig.ts
+++ b/server/routers/gerbil/getConfig.ts
@@ -1,19 +1,15 @@
import { Request, Response, NextFunction } from "express";
import { z } from "zod";
-import { sites, resources, targets, exitNodes, ExitNode } from "@server/db";
+import { sites, exitNodes, ExitNode } from "@server/db";
import { db } from "@server/db";
import { eq, isNotNull, and } from "drizzle-orm";
import HttpCode from "@server/types/HttpCode";
import createHttpError from "http-errors";
import logger from "@server/logger";
import config from "@server/lib/config";
-import { getUniqueExitNodeEndpointName } from "../../db/names";
-import { findNextAvailableCidr } from "@server/lib/ip";
import { fromError } from "zod-validation-error";
import { getAllowedIps } from "../target/helpers";
-import { proxyToRemote } from "@server/lib/remoteProxy";
-import { getNextAvailableSubnet } from "@server/lib/exitNodes";
-import { createExitNode } from "./privateCreateExitNode";
+import { createExitNode } from "#dynamic/routers/gerbil/createExitNode";
// Define Zod schema for request validation
const getConfigSchema = z.object({
@@ -66,16 +62,6 @@ export async function getConfig(
);
}
- // STOP HERE IN HYBRID MODE
- if (config.isManagedMode()) {
- req.body = {
- ...req.body,
- endpoint: exitNode.endpoint,
- listenPort: exitNode.listenPort
- };
- return proxyToRemote(req, res, next, "hybrid/gerbil/get-config");
- }
-
const configResponse = await generateGerbilConfig(exitNode);
logger.debug("Sending config: ", configResponse);
diff --git a/server/routers/gerbil/getResolvedHostname.ts b/server/routers/gerbil/getResolvedHostname.ts
index 17067c55..f06cd1b8 100644
--- a/server/routers/gerbil/getResolvedHostname.ts
+++ b/server/routers/gerbil/getResolvedHostname.ts
@@ -4,9 +4,9 @@ import HttpCode from "@server/types/HttpCode";
import createHttpError from "http-errors";
import logger from "@server/logger";
import { fromError } from "zod-validation-error";
-import { resolveExitNodes } from "@server/lib/exitNodes";
-import config from "@server/lib/config";
+import { resolveExitNodes } from "#dynamic/lib/exitNodes";
import { build } from "@server/build";
+import config from "@server/lib/config";
// Define Zod schema for request validation
const getResolvedHostnameSchema = z.object({
@@ -36,7 +36,15 @@ export async function getResolvedHostname(
const { hostname, publicKey } = parsedParams.data;
- const baseDomain = config.getRawPrivateConfig().app.base_domain;
+ const dashboardUrl = config.getRawConfig().app.dashboard_url;
+
+ // extract the domain removing the http and stuff
+ const baseDomain = dashboardUrl
+ ? dashboardUrl
+ .replace("http://", "")
+ .replace("https://", "")
+ .split("/")[0]
+ : null;
// if the hostname ends with the base domain then send back a empty array
if (baseDomain && hostname.endsWith(baseDomain)) {
@@ -50,6 +58,13 @@ export async function getResolvedHostname(
publicKey
);
+ if (resourceExitNodes.length === 0) {
+ // no exit nodes found, return empty array to force local routing
+ return res.status(HttpCode.OK).send({
+ endpoints: [] // this should force to route locally
+ });
+ }
+
endpoints = resourceExitNodes.map((node) => node.endpoint);
}
diff --git a/server/routers/gerbil/peers.ts b/server/routers/gerbil/peers.ts
index 1cdc9184..44af7fbd 100644
--- a/server/routers/gerbil/peers.ts
+++ b/server/routers/gerbil/peers.ts
@@ -2,7 +2,7 @@ import logger from "@server/logger";
import { db } from "@server/db";
import { exitNodes } from "@server/db";
import { eq } from "drizzle-orm";
-import { sendToExitNode } from "@server/lib/exitNodes";
+import { sendToExitNode } from "#dynamic/lib/exitNodes";
export async function addPeer(
exitNodeId: number,
diff --git a/server/routers/gerbil/receiveBandwidth.ts b/server/routers/gerbil/receiveBandwidth.ts
index d10141b9..3661dedd 100644
--- a/server/routers/gerbil/receiveBandwidth.ts
+++ b/server/routers/gerbil/receiveBandwidth.ts
@@ -6,9 +6,9 @@ import logger from "@server/logger";
import createHttpError from "http-errors";
import HttpCode from "@server/types/HttpCode";
import response from "@server/lib/response";
-import { usageService } from "@server/lib/private/billing/usageService";
-import { FeatureId } from "@server/lib/private/billing/features";
-import { checkExitNodeOrg } from "@server/lib/exitNodes";
+import { usageService } from "@server/lib/billing/usageService";
+import { FeatureId } from "@server/lib/billing/features";
+import { checkExitNodeOrg } from "#dynamic/lib/exitNodes";
import { build } from "@server/build";
// Track sites that are already offline to avoid unnecessary queries
diff --git a/server/routers/gerbil/updateHolePunch.ts b/server/routers/gerbil/updateHolePunch.ts
index 65217178..34bc2c6b 100644
--- a/server/routers/gerbil/updateHolePunch.ts
+++ b/server/routers/gerbil/updateHolePunch.ts
@@ -18,8 +18,7 @@ import logger from "@server/logger";
import { fromError } from "zod-validation-error";
import { validateNewtSessionToken } from "@server/auth/sessions/newt";
import { validateOlmSessionToken } from "@server/auth/sessions/olm";
-import axios from "axios";
-import { checkExitNodeOrg } from "@server/lib/exitNodes";
+import { checkExitNodeOrg } from "#dynamic/lib/exitNodes";
// Define Zod schema for request validation
const updateHolePunchSchema = z.object({
diff --git a/server/routers/hybrid.ts b/server/routers/hybrid.ts
new file mode 100644
index 00000000..235961f1
--- /dev/null
+++ b/server/routers/hybrid.ts
@@ -0,0 +1,4 @@
+import { Router } from "express";
+
+// Root routes
+export const hybridRouter = Router();
\ No newline at end of file
diff --git a/server/routers/idp/createOidcIdp.ts b/server/routers/idp/createOidcIdp.ts
index 223a08b8..67357d76 100644
--- a/server/routers/idp/createOidcIdp.ts
+++ b/server/routers/idp/createOidcIdp.ts
@@ -11,7 +11,6 @@ import { idp, idpOidcConfig, idpOrg, orgs } from "@server/db";
import { generateOidcRedirectUrl } from "@server/lib/idp/generateRedirectUrl";
import { encrypt } from "@server/lib/crypto";
import config from "@server/lib/config";
-import license from "@server/license/license";
const paramsSchema = z.object({}).strict();
diff --git a/server/routers/idp/generateOidcUrl.ts b/server/routers/idp/generateOidcUrl.ts
index 90144816..3c81ce0b 100644
--- a/server/routers/idp/generateOidcUrl.ts
+++ b/server/routers/idp/generateOidcUrl.ts
@@ -14,8 +14,8 @@ import jsonwebtoken from "jsonwebtoken";
import config from "@server/lib/config";
import { decrypt } from "@server/lib/crypto";
import { build } from "@server/build";
-import { getOrgTierData } from "@server/routers/private/billing";
-import { TierId } from "@server/lib/private/billing/tiers";
+import { getOrgTierData } from "#dynamic/lib/billing";
+import { TierId } from "@server/lib/billing/tiers";
const paramsSchema = z
.object({
diff --git a/server/routers/idp/updateOidcIdp.ts b/server/routers/idp/updateOidcIdp.ts
index 904d0d9e..53ece68e 100644
--- a/server/routers/idp/updateOidcIdp.ts
+++ b/server/routers/idp/updateOidcIdp.ts
@@ -11,7 +11,6 @@ import { idp, idpOidcConfig } from "@server/db";
import { eq } from "drizzle-orm";
import { encrypt } from "@server/lib/crypto";
import config from "@server/lib/config";
-import license from "@server/license/license";
const paramsSchema = z
.object({
diff --git a/server/routers/idp/validateOidcCallback.ts b/server/routers/idp/validateOidcCallback.ts
index fec21e41..98bdfe44 100644
--- a/server/routers/idp/validateOidcCallback.ts
+++ b/server/routers/idp/validateOidcCallback.ts
@@ -1,6 +1,6 @@
import { Request, Response, NextFunction } from "express";
import { z } from "zod";
-import { db } from "@server/db";
+import { db, Org } from "@server/db";
import response from "@server/lib/response";
import HttpCode from "@server/types/HttpCode";
import createHttpError from "http-errors";
@@ -30,8 +30,9 @@ import {
} from "@server/auth/sessions/app";
import { decrypt } from "@server/lib/crypto";
import { UserType } from "@server/types/UserTypes";
-import { FeatureId } from "@server/lib/private/billing";
-import { usageService } from "@server/lib/private/billing/usageService";
+import { FeatureId } from "@server/lib/billing";
+import { usageService } from "@server/lib/billing/usageService";
+import { build } from "@server/build";
const ensureTrailingSlash = (url: string): string => {
return url;
@@ -255,7 +256,18 @@ export async function validateOidcCallback(
);
if (existingIdp.idp.autoProvision) {
- const allOrgs = await db.select().from(orgs);
+ let allOrgs: Org[] = [];
+
+ if (build === "saas") {
+ const idpOrgs = await db
+ .select()
+ .from(idpOrg)
+ .where(eq(idpOrg.idpId, existingIdp.idp.idpId))
+ .innerJoin(orgs, eq(orgs.orgId, idpOrg.orgId));
+ allOrgs = idpOrgs.map((o) => o.orgs);
+ } else {
+ allOrgs = await db.select().from(orgs);
+ }
const defaultRoleMapping = existingIdp.idp.defaultRoleMapping;
const defaultOrgMapping = existingIdp.idp.defaultOrgMapping;
@@ -292,6 +304,8 @@ export async function validateOidcCallback(
}
}
+ // user could be allowed in this org, now find the role
+
const roleMapping =
idpOrgRes?.roleMapping || defaultRoleMapping;
if (roleMapping) {
@@ -336,6 +350,24 @@ export async function validateOidcCallback(
let existingUserId = existingUser?.userId;
+ if (!userOrgInfo.length) {
+ if (existingUser) {
+ // delete the user
+ // cascade will also delete org users
+
+ await db
+ .delete(users)
+ .where(eq(users.userId, existingUser.userId));
+ }
+
+ return next(
+ createHttpError(
+ HttpCode.UNAUTHORIZED,
+ `No policies matched for ${userIdentifier}. This user must be added to an organization before logging in.`
+ )
+ );
+ }
+
const orgUserCounts: { orgId: string; userCount: number }[] = [];
// sync the user with the orgs and roles
diff --git a/server/routers/integration.ts b/server/routers/integration.ts
index 879075c7..d0c7c5d5 100644
--- a/server/routers/integration.ts
+++ b/server/routers/integration.ts
@@ -555,13 +555,6 @@ authenticated.post(
idp.updateOidcIdp
);
-authenticated.delete(
- "/idp/:idpId",
- verifyApiKeyIsRoot,
- verifyApiKeyHasAction(ActionsEnum.deleteIdp),
- idp.deleteIdp
-);
-
authenticated.get(
"/idp",
verifyApiKeyIsRoot,
@@ -604,15 +597,6 @@ authenticated.get(
idp.listIdpOrgPolicies
);
-if (build == "saas") {
- authenticated.post(
- `/org/:orgId/send-usage-notification`,
- verifyApiKeyIsRoot, // We are the only ones who can use root key so its fine
- verifyApiKeyHasAction(ActionsEnum.sendUsageNotification),
- org.sendUsageNotification
- );
-}
-
authenticated.get(
"/org/:orgId/pick-client-defaults",
verifyClientsEnabled,
diff --git a/server/routers/internal.ts b/server/routers/internal.ts
index e4525118..2fa5239c 100644
--- a/server/routers/internal.ts
+++ b/server/routers/internal.ts
@@ -5,22 +5,15 @@ import * as resource from "./resource";
import * as badger from "./badger";
import * as auth from "@server/routers/auth";
import * as supporterKey from "@server/routers/supporterKey";
-import * as license from "@server/routers/license";
import * as idp from "@server/routers/idp";
-import * as loginPage from "@server/routers/private/loginPage";
-import { proxyToRemote } from "@server/lib/remoteProxy";
-import config from "@server/lib/config";
import HttpCode from "@server/types/HttpCode";
import {
verifyResourceAccess,
verifySessionUserMiddleware
} from "@server/middlewares";
-import { build } from "@server/build";
-import * as billing from "@server/routers/private/billing";
-import * as orgIdp from "@server/routers/private/orgIdp";
// Root routes
-const internalRouter = Router();
+export const internalRouter = Router();
internalRouter.get("/", (_, res) => {
res.status(HttpCode.OK).json({ message: "Healthy" });
@@ -45,50 +38,19 @@ internalRouter.get(
supporterKey.isSupporterKeyVisible
);
-internalRouter.get(`/license/status`, license.getLicenseStatus);
-
internalRouter.get("/idp", idp.listIdps);
internalRouter.get("/idp/:idpId", idp.getIdp);
-if (build !== "oss") {
- internalRouter.get("/org/:orgId/idp", orgIdp.listOrgIdps);
-
- internalRouter.get("/org/:orgId/billing/tier", billing.getOrgTier);
-}
-
// Gerbil routes
const gerbilRouter = Router();
internalRouter.use("/gerbil", gerbilRouter);
-if (config.isManagedMode()) {
- // Use proxy router to forward requests to remote cloud server
- // Proxy endpoints for each gerbil route
- gerbilRouter.post("/receive-bandwidth", (req, res, next) =>
- proxyToRemote(req, res, next, "hybrid/gerbil/receive-bandwidth")
- );
-
- gerbilRouter.post("/update-hole-punch", (req, res, next) =>
- proxyToRemote(req, res, next, "hybrid/gerbil/update-hole-punch")
- );
-
- gerbilRouter.post("/get-all-relays", (req, res, next) =>
- proxyToRemote(req, res, next, "hybrid/gerbil/get-all-relays")
- );
-
- gerbilRouter.post("/get-resolved-hostname", (req, res, next) =>
- proxyToRemote(req, res, next, `hybrid/gerbil/get-resolved-hostname`)
- );
-
- // GET CONFIG IS HANDLED IN THE ORIGINAL HANDLER
- // SO IT CAN REGISTER THE LOCAL EXIT NODE
-} else {
- // Use local gerbil endpoints
- gerbilRouter.post("/receive-bandwidth", gerbil.receiveBandwidth);
- gerbilRouter.post("/update-hole-punch", gerbil.updateHolePunch);
- gerbilRouter.post("/get-all-relays", gerbil.getAllRelays);
- gerbilRouter.post("/get-resolved-hostname", gerbil.getResolvedHostname);
-}
+// Use local gerbil endpoints
+gerbilRouter.post("/receive-bandwidth", gerbil.receiveBandwidth);
+gerbilRouter.post("/update-hole-punch", gerbil.updateHolePunch);
+gerbilRouter.post("/get-all-relays", gerbil.getAllRelays);
+gerbilRouter.post("/get-resolved-hostname", gerbil.getResolvedHostname);
// WE HANDLE THE PROXY INSIDE OF THIS FUNCTION
// SO IT REGISTERS THE EXIT NODE LOCALLY AS WELL
@@ -100,22 +62,4 @@ internalRouter.use("/badger", badgerRouter);
badgerRouter.post("/verify-session", badger.verifyResourceSession);
-if (config.isManagedMode()) {
- badgerRouter.post("/exchange-session", (req, res, next) =>
- proxyToRemote(req, res, next, "hybrid/badger/exchange-session")
- );
-} else {
- badgerRouter.post("/exchange-session", badger.exchangeSession);
-}
-
-if (build !== "oss") {
- internalRouter.get("/login-page", loginPage.loadLoginPage);
-
- internalRouter.post(
- "/get-session-transfer-token",
- verifySessionUserMiddleware,
- auth.getSessionTransferToken
- );
-}
-
-export default internalRouter;
+badgerRouter.post("/exchange-session", badger.exchangeSession);
diff --git a/server/routers/license/types.ts b/server/routers/license/types.ts
new file mode 100644
index 00000000..945bd368
--- /dev/null
+++ b/server/routers/license/types.ts
@@ -0,0 +1,11 @@
+import { LicenseStatus, LicenseKeyCache } from "@server/license/license";
+
+export type ActivateLicenseStatus = LicenseStatus;
+
+export type DeleteLicenseKeyResponse = LicenseStatus;
+
+export type GetLicenseStatusResponse = LicenseStatus;
+
+export type ListLicenseKeysResponse = LicenseKeyCache[];
+
+export type RecheckStatusResponse = LicenseStatus;
\ No newline at end of file
diff --git a/server/routers/loginPage/types.ts b/server/routers/loginPage/types.ts
new file mode 100644
index 00000000..26f59cab
--- /dev/null
+++ b/server/routers/loginPage/types.ts
@@ -0,0 +1,11 @@
+import { LoginPage } from "@server/db";
+
+export type CreateLoginPageResponse = LoginPage;
+
+export type DeleteLoginPageResponse = LoginPage;
+
+export type GetLoginPageResponse = LoginPage;
+
+export type UpdateLoginPageResponse = LoginPage;
+
+export type LoadLoginPageResponse = LoginPage & { orgId: string };
\ No newline at end of file
diff --git a/server/routers/newt/dockerSocket.ts b/server/routers/newt/dockerSocket.ts
index 0c59d354..3847d9a4 100644
--- a/server/routers/newt/dockerSocket.ts
+++ b/server/routers/newt/dockerSocket.ts
@@ -1,5 +1,5 @@
import NodeCache from "node-cache";
-import { sendToClient } from "../ws";
+import { sendToClient } from "#dynamic/routers/ws";
export const dockerSocketCache = new NodeCache({
stdTTL: 3600 // seconds
diff --git a/server/routers/newt/handleApplyBlueprintMessage.ts b/server/routers/newt/handleApplyBlueprintMessage.ts
index 68158799..62802fff 100644
--- a/server/routers/newt/handleApplyBlueprintMessage.ts
+++ b/server/routers/newt/handleApplyBlueprintMessage.ts
@@ -1,5 +1,5 @@
import { db, newts } from "@server/db";
-import { MessageHandler } from "../ws";
+import { MessageHandler } from "@server/routers/ws";
import { exitNodes, Newt, resources, sites, Target, targets } from "@server/db";
import { eq, and, sql, inArray } from "drizzle-orm";
import logger from "@server/logger";
diff --git a/server/routers/newt/handleGetConfigMessage.ts b/server/routers/newt/handleGetConfigMessage.ts
index 2b65fd06..3eba94b9 100644
--- a/server/routers/newt/handleGetConfigMessage.ts
+++ b/server/routers/newt/handleGetConfigMessage.ts
@@ -1,5 +1,5 @@
import { z } from "zod";
-import { MessageHandler } from "../ws";
+import { MessageHandler } from "@server/routers/ws";
import logger from "@server/logger";
import { fromError } from "zod-validation-error";
import {
@@ -14,7 +14,7 @@ import {
import { clients, clientSites, Newt, sites } from "@server/db";
import { eq, and, inArray } from "drizzle-orm";
import { updatePeer } from "../olm/peers";
-import { sendToExitNode } from "@server/lib/exitNodes";
+import { sendToExitNode } from "#dynamic/lib/exitNodes";
const inputSchema = z.object({
publicKey: z.string(),
diff --git a/server/routers/newt/handleNewtPingRequestMessage.ts b/server/routers/newt/handleNewtPingRequestMessage.ts
index aeb7a155..fea157fd 100644
--- a/server/routers/newt/handleNewtPingRequestMessage.ts
+++ b/server/routers/newt/handleNewtPingRequestMessage.ts
@@ -1,10 +1,9 @@
import { db, sites } from "@server/db";
-import { MessageHandler } from "../ws";
+import { MessageHandler } from "@server/routers/ws";
import { exitNodes, Newt } from "@server/db";
import logger from "@server/logger";
-import config from "@server/lib/config";
import { ne, eq, or, and, count } from "drizzle-orm";
-import { listExitNodes } from "@server/lib/exitNodes";
+import { listExitNodes } from "#dynamic/lib/exitNodes";
export const handleNewtPingRequestMessage: MessageHandler = async (context) => {
const { message, client, sendToClient } = context;
diff --git a/server/routers/newt/handleNewtRegisterMessage.ts b/server/routers/newt/handleNewtRegisterMessage.ts
index 021527ad..372f3677 100644
--- a/server/routers/newt/handleNewtRegisterMessage.ts
+++ b/server/routers/newt/handleNewtRegisterMessage.ts
@@ -1,5 +1,5 @@
import { db, exitNodeOrgs, newts } from "@server/db";
-import { MessageHandler } from "../ws";
+import { MessageHandler } from "@server/routers/ws";
import { exitNodes, Newt, resources, sites, Target, targets } from "@server/db";
import { targetHealthCheck } from "@server/db";
import { eq, and, sql, inArray } from "drizzle-orm";
@@ -10,12 +10,12 @@ import {
findNextAvailableCidr,
getNextAvailableClientSubnet
} from "@server/lib/ip";
-import { usageService } from "@server/lib/private/billing/usageService";
-import { FeatureId } from "@server/lib/private/billing";
+import { usageService } from "@server/lib/billing/usageService";
+import { FeatureId } from "@server/lib/billing";
import {
selectBestExitNode,
verifyExitNodeOrgAccess
-} from "@server/lib/exitNodes";
+} from "#dynamic/lib/exitNodes";
import { fetchContainers } from "./dockerSocket";
export type ExitNodePingResult = {
diff --git a/server/routers/newt/handleReceiveBandwidthMessage.ts b/server/routers/newt/handleReceiveBandwidthMessage.ts
index 89b24f78..f5170feb 100644
--- a/server/routers/newt/handleReceiveBandwidthMessage.ts
+++ b/server/routers/newt/handleReceiveBandwidthMessage.ts
@@ -1,5 +1,5 @@
import { db } from "@server/db";
-import { MessageHandler } from "../ws";
+import { MessageHandler } from "@server/routers/ws";
import { clients, Newt } from "@server/db";
import { eq } from "drizzle-orm";
import logger from "@server/logger";
diff --git a/server/routers/newt/handleSocketMessages.ts b/server/routers/newt/handleSocketMessages.ts
index aceca37d..0491393f 100644
--- a/server/routers/newt/handleSocketMessages.ts
+++ b/server/routers/newt/handleSocketMessages.ts
@@ -1,4 +1,4 @@
-import { MessageHandler } from "../ws";
+import { MessageHandler } from "@server/routers/ws";
import logger from "@server/logger";
import { dockerSocketCache } from "./dockerSocket";
import { Newt } from "@server/db";
diff --git a/server/routers/newt/peers.ts b/server/routers/newt/peers.ts
index ff57e6fd..03dc3460 100644
--- a/server/routers/newt/peers.ts
+++ b/server/routers/newt/peers.ts
@@ -1,7 +1,7 @@
import { db } from "@server/db";
import { newts, sites } from "@server/db";
import { eq } from "drizzle-orm";
-import { sendToClient } from "../ws";
+import { sendToClient } from "#dynamic/routers/ws";
import logger from "@server/logger";
export async function addPeer(
diff --git a/server/routers/newt/targets.ts b/server/routers/newt/targets.ts
index a886b00b..97e4030d 100644
--- a/server/routers/newt/targets.ts
+++ b/server/routers/newt/targets.ts
@@ -1,5 +1,5 @@
import { Target, TargetHealthCheck, db, targetHealthCheck } from "@server/db";
-import { sendToClient } from "../ws";
+import { sendToClient } from "#dynamic/routers/ws";
import logger from "@server/logger";
import { eq, inArray } from "drizzle-orm";
diff --git a/server/routers/olm/handleOlmPingMessage.ts b/server/routers/olm/handleOlmPingMessage.ts
index 6c4b5600..6f00640d 100644
--- a/server/routers/olm/handleOlmPingMessage.ts
+++ b/server/routers/olm/handleOlmPingMessage.ts
@@ -1,5 +1,5 @@
import { db } from "@server/db";
-import { MessageHandler } from "../ws";
+import { MessageHandler } from "@server/routers/ws";
import { clients, Olm } from "@server/db";
import { eq, lt, isNull, and, or } from "drizzle-orm";
import logger from "@server/logger";
diff --git a/server/routers/olm/handleOlmRegisterMessage.ts b/server/routers/olm/handleOlmRegisterMessage.ts
index 33d7f9cb..66128f0e 100644
--- a/server/routers/olm/handleOlmRegisterMessage.ts
+++ b/server/routers/olm/handleOlmRegisterMessage.ts
@@ -1,10 +1,10 @@
import { db, ExitNode } from "@server/db";
-import { MessageHandler } from "../ws";
+import { MessageHandler } from "@server/routers/ws";
import { clients, clientSites, exitNodes, Olm, olms, sites } from "@server/db";
import { and, eq, inArray } from "drizzle-orm";
import { addPeer, deletePeer } from "../newt/peers";
import logger from "@server/logger";
-import { listExitNodes } from "@server/lib/exitNodes";
+import { listExitNodes } from "#dynamic/lib/exitNodes";
export const handleOlmRegisterMessage: MessageHandler = async (context) => {
logger.info("Handling register olm message!");
diff --git a/server/routers/olm/handleOlmRelayMessage.ts b/server/routers/olm/handleOlmRelayMessage.ts
index cefc5b91..9b31754c 100644
--- a/server/routers/olm/handleOlmRelayMessage.ts
+++ b/server/routers/olm/handleOlmRelayMessage.ts
@@ -1,5 +1,5 @@
import { db, exitNodes, sites } from "@server/db";
-import { MessageHandler } from "../ws";
+import { MessageHandler } from "@server/routers/ws";
import { clients, clientSites, Olm } from "@server/db";
import { and, eq } from "drizzle-orm";
import { updatePeer } from "../newt/peers";
diff --git a/server/routers/olm/peers.ts b/server/routers/olm/peers.ts
index a8d6be9c..396866a1 100644
--- a/server/routers/olm/peers.ts
+++ b/server/routers/olm/peers.ts
@@ -1,7 +1,7 @@
import { db } from "@server/db";
import { clients, olms, newts, sites } from "@server/db";
import { eq } from "drizzle-orm";
-import { sendToClient } from "../ws";
+import { sendToClient } from "#dynamic/routers/ws";
import logger from "@server/logger";
export async function addPeer(
diff --git a/server/routers/org/createOrg.ts b/server/routers/org/createOrg.ts
index a12520a4..d8bcb9da 100644
--- a/server/routers/org/createOrg.ts
+++ b/server/routers/org/createOrg.ts
@@ -3,8 +3,6 @@ import { z } from "zod";
import { db } from "@server/db";
import { eq } from "drizzle-orm";
import {
- apiKeyOrg,
- apiKeys,
domains,
Org,
orgDomains,
@@ -24,9 +22,9 @@ import { fromError } from "zod-validation-error";
import { defaultRoleAllowedActions } from "../role";
import { OpenAPITags, registry } from "@server/openApi";
import { isValidCIDR } from "@server/lib/validators";
-import { createCustomer } from "@server/routers/private/billing/createCustomer";
-import { usageService } from "@server/lib/private/billing/usageService";
-import { FeatureId } from "@server/lib/private/billing";
+import { createCustomer } from "#dynamic/lib/billing";
+import { usageService } from "@server/lib/billing/usageService";
+import { FeatureId } from "@server/lib/billing";
import { build } from "@server/build";
const createOrgSchema = z
diff --git a/server/routers/org/deleteOrg.ts b/server/routers/org/deleteOrg.ts
index 63e9abb0..8a424e5b 100644
--- a/server/routers/org/deleteOrg.ts
+++ b/server/routers/org/deleteOrg.ts
@@ -9,7 +9,7 @@ import createHttpError from "http-errors";
import { ActionsEnum, checkUserActionPermission } from "@server/auth/actions";
import logger from "@server/logger";
import { fromError } from "zod-validation-error";
-import { sendToClient } from "../ws";
+import { sendToClient } from "#dynamic/routers/ws";
import { deletePeer } from "../gerbil/peers";
import { OpenAPITags, registry } from "@server/openApi";
diff --git a/server/routers/org/getOrg.ts b/server/routers/org/getOrg.ts
index e467374f..89c77f13 100644
--- a/server/routers/org/getOrg.ts
+++ b/server/routers/org/getOrg.ts
@@ -75,10 +75,6 @@ export async function getOrg(
}
}
- logger.info(
- `returning data: ${JSON.stringify({ ...org[0], settings: parsedSettings })}`
- );
-
return response(res, {
data: {
org: {
diff --git a/server/routers/org/index.ts b/server/routers/org/index.ts
index 013f6c6d..7887fcac 100644
--- a/server/routers/org/index.ts
+++ b/server/routers/org/index.ts
@@ -7,5 +7,4 @@ export * from "./checkId";
export * from "./getOrgOverview";
export * from "./listOrgs";
export * from "./pickOrgDefaults";
-export * from "./privateSendUsageNotifications";
export * from "./applyBlueprint";
diff --git a/server/routers/orgIdp/types.ts b/server/routers/orgIdp/types.ts
new file mode 100644
index 00000000..a8e205cc
--- /dev/null
+++ b/server/routers/orgIdp/types.ts
@@ -0,0 +1,27 @@
+import { Idp, IdpOidcConfig } from "@server/db";
+
+export type CreateOrgIdpResponse = {
+ idpId: number;
+ redirectUrl: string;
+};
+
+export type GetOrgIdpResponse = {
+ idp: Idp,
+ idpOidcConfig: IdpOidcConfig | null,
+ redirectUrl: string
+}
+
+export type ListOrgIdpsResponse = {
+ idps: {
+ idpId: number;
+ orgId: string;
+ name: string;
+ type: string;
+ variant: string;
+ }[],
+ pagination: {
+ total: number;
+ limit: number;
+ offset: number;
+ };
+};
diff --git a/server/routers/remoteExitNode/types.ts b/server/routers/remoteExitNode/types.ts
new file mode 100644
index 00000000..55d0a286
--- /dev/null
+++ b/server/routers/remoteExitNode/types.ts
@@ -0,0 +1,34 @@
+import { RemoteExitNode } from "@server/db";
+
+export type CreateRemoteExitNodeResponse = {
+ token: string;
+ remoteExitNodeId: string;
+ secret: string;
+};
+
+export type PickRemoteExitNodeDefaultsResponse = {
+ remoteExitNodeId: string;
+ secret: string;
+};
+
+export type QuickStartRemoteExitNodeResponse = {
+ remoteExitNodeId: string;
+ secret: string;
+};
+
+export type ListRemoteExitNodesResponse = {
+ remoteExitNodes: {
+ remoteExitNodeId: string;
+ dateCreated: string;
+ version: string | null;
+ exitNodeId: number | null;
+ name: string;
+ address: string;
+ endpoint: string;
+ online: boolean;
+ type: string | null;
+ }[];
+ pagination: { total: number; limit: number; offset: number };
+};
+
+export type GetRemoteExitNodeResponse = { remoteExitNodeId: string; dateCreated: string; version: string | null; exitNodeId: number | null; name: string; address: string; endpoint: string; online: boolean; type: string | null; }
\ No newline at end of file
diff --git a/server/routers/resource/createResource.ts b/server/routers/resource/createResource.ts
index 53af0b72..2a4e67a7 100644
--- a/server/routers/resource/createResource.ts
+++ b/server/routers/resource/createResource.ts
@@ -21,7 +21,7 @@ import { subdomainSchema } from "@server/lib/schemas";
import config from "@server/lib/config";
import { OpenAPITags, registry } from "@server/openApi";
import { build } from "@server/build";
-import { createCertificate } from "../private/certificates/createCertificate";
+import { createCertificate } from "#dynamic/routers/certificates/createCertificate";
import { getUniqueResourceName } from "@server/db/names";
import { validateAndConstructDomain } from "@server/lib/domainUtils";
@@ -37,7 +37,8 @@ const createHttpResourceSchema = z
subdomain: z.string().nullable().optional(),
http: z.boolean(),
protocol: z.enum(["tcp", "udp"]),
- domainId: z.string()
+ domainId: z.string(),
+ stickySession: z.boolean().optional(),
})
.strict()
.refine(
@@ -191,6 +192,7 @@ async function createHttpResource(
const { name, domainId } = parsedBody.data;
const subdomain = parsedBody.data.subdomain;
+ const stickySession=parsedBody.data.stickySession;
// Validate domain and construct full domain
const domainResult = await validateAndConstructDomain(
@@ -254,7 +256,8 @@ async function createHttpResource(
subdomain: finalSubdomain,
http: true,
protocol: "tcp",
- ssl: true
+ ssl: true,
+ stickySession: stickySession
})
.returning();
diff --git a/server/routers/resource/getResourceAuthInfo.ts b/server/routers/resource/getResourceAuthInfo.ts
index b7775251..834da7b3 100644
--- a/server/routers/resource/getResourceAuthInfo.ts
+++ b/server/routers/resource/getResourceAuthInfo.ts
@@ -103,18 +103,18 @@ export async function getResourceAuthInfo(
.limit(1);
const resource = result?.resources;
- const pincode = result?.resourcePincode;
- const password = result?.resourcePassword;
- const headerAuth = result?.resourceHeaderAuth;
-
- const url = `${resource.ssl ? "https" : "http"}://${resource.fullDomain}`;
-
if (!resource) {
return next(
createHttpError(HttpCode.NOT_FOUND, "Resource not found")
);
}
+ const pincode = result?.resourcePincode;
+ const password = result?.resourcePassword;
+ const headerAuth = result?.resourceHeaderAuth;
+
+ const url = `${resource.ssl ? "https" : "http"}://${resource.fullDomain}`;
+
return response(res, {
data: {
niceId: resource.niceId,
diff --git a/server/routers/resource/listResources.ts b/server/routers/resource/listResources.ts
index 27605be6..22a10605 100644
--- a/server/routers/resource/listResources.ts
+++ b/server/routers/resource/listResources.ts
@@ -1,6 +1,6 @@
import { Request, Response, NextFunction } from "express";
import { z } from "zod";
-import { db } from "@server/db";
+import { db, resourceHeaderAuth } from "@server/db";
import {
resources,
userResources,
@@ -56,7 +56,8 @@ function queryResources(accessibleResourceIds: number[], orgId: string) {
proxyPort: resources.proxyPort,
enabled: resources.enabled,
domainId: resources.domainId,
- niceId: resources.niceId
+ niceId: resources.niceId,
+ headerAuthId: resourceHeaderAuth.headerAuthId
})
.from(resources)
.leftJoin(
@@ -67,6 +68,10 @@ function queryResources(accessibleResourceIds: number[], orgId: string) {
resourcePincode,
eq(resourcePincode.resourceId, resources.resourceId)
)
+ .leftJoin(
+ resourceHeaderAuth,
+ eq(resourceHeaderAuth.resourceId, resources.resourceId)
+ )
.where(
and(
inArray(resources.resourceId, accessibleResourceIds),
diff --git a/server/routers/resource/updateResource.ts b/server/routers/resource/updateResource.ts
index 83fcf6f1..a9c3b5de 100644
--- a/server/routers/resource/updateResource.ts
+++ b/server/routers/resource/updateResource.ts
@@ -20,7 +20,7 @@ import { tlsNameSchema } from "@server/lib/schemas";
import { subdomainSchema } from "@server/lib/schemas";
import { registry } from "@server/openApi";
import { OpenAPITags } from "@server/openApi";
-import { createCertificate } from "../private/certificates/createCertificate";
+import { createCertificate } from "#dynamic/routers/certificates/createCertificate";
import { validateAndConstructDomain } from "@server/lib/domainUtils";
import { validateHeaders } from "@server/lib/validators";
import { build } from "@server/build";
diff --git a/server/routers/site/createSite.ts b/server/routers/site/createSite.ts
index 5ffa6954..0ffc5956 100644
--- a/server/routers/site/createSite.ts
+++ b/server/routers/site/createSite.ts
@@ -16,8 +16,7 @@ import { OpenAPITags, registry } from "@server/openApi";
import { hashPassword } from "@server/auth/password";
import { isValidIP } from "@server/lib/validators";
import { isIpInCidr } from "@server/lib/ip";
-import config from "@server/lib/config";
-import { verifyExitNodeOrgAccess } from "@server/lib/exitNodes";
+import { verifyExitNodeOrgAccess } from "#dynamic/lib/exitNodes";
const createSiteParamsSchema = z
.object({
diff --git a/server/routers/site/deleteSite.ts b/server/routers/site/deleteSite.ts
index 4af2feae..7a12e24a 100644
--- a/server/routers/site/deleteSite.ts
+++ b/server/routers/site/deleteSite.ts
@@ -9,7 +9,7 @@ import createHttpError from "http-errors";
import logger from "@server/logger";
import { deletePeer } from "../gerbil/peers";
import { fromError } from "zod-validation-error";
-import { sendToClient } from "../ws";
+import { sendToClient } from "#dynamic/routers/ws";
import { OpenAPITags, registry } from "@server/openApi";
const deleteSiteSchema = z
diff --git a/server/routers/site/pickSiteDefaults.ts b/server/routers/site/pickSiteDefaults.ts
index 46d3c53b..c4b3a087 100644
--- a/server/routers/site/pickSiteDefaults.ts
+++ b/server/routers/site/pickSiteDefaults.ts
@@ -15,7 +15,7 @@ import config from "@server/lib/config";
import { OpenAPITags, registry } from "@server/openApi";
import { fromError } from "zod-validation-error";
import { z } from "zod";
-import { listExitNodes } from "@server/lib/exitNodes";
+import { listExitNodes } from "#dynamic/lib/exitNodes";
export type PickSiteDefaultsResponse = {
exitNodeId: number;
diff --git a/server/routers/site/socketIntegration.ts b/server/routers/site/socketIntegration.ts
index 34084a0a..7b5160cb 100644
--- a/server/routers/site/socketIntegration.ts
+++ b/server/routers/site/socketIntegration.ts
@@ -9,7 +9,7 @@ import createHttpError from "http-errors";
import { z } from "zod";
import { fromError } from "zod-validation-error";
import stoi from "@server/lib/stoi";
-import { sendToClient } from "../ws";
+import { sendToClient } from "#dynamic/routers/ws";
import {
fetchContainers,
dockerSocketCache,
diff --git a/server/routers/supporterKey/isSupporterKeyVisible.ts b/server/routers/supporterKey/isSupporterKeyVisible.ts
index 0e958889..da995447 100644
--- a/server/routers/supporterKey/isSupporterKeyVisible.ts
+++ b/server/routers/supporterKey/isSupporterKeyVisible.ts
@@ -7,7 +7,6 @@ import config from "@server/lib/config";
import { db } from "@server/db";
import { count } from "drizzle-orm";
import { users } from "@server/db";
-import license from "@server/license/license";
import { build } from "@server/build";
export type IsSupporterKeyVisibleResponse = {
@@ -28,12 +27,6 @@ export async function isSupporterKeyVisible(
let visible = !hidden && key?.valid !== true;
- const licenseStatus = await license.check();
-
- if (licenseStatus.isLicenseValid) {
- visible = false;
- }
-
if (key?.tier === "Limited Supporter") {
const [numUsers] = await db.select({ count: count() }).from(users);
@@ -45,7 +38,7 @@ export async function isSupporterKeyVisible(
}
}
- if (config.getRawPrivateConfig().flags?.hide_supporter_key && build != "oss") {
+ if (build !== "oss") {
visible = false;
}
diff --git a/server/routers/target/handleHealthcheckStatusMessage.ts b/server/routers/target/handleHealthcheckStatusMessage.ts
index d726b5af..ee4e7950 100644
--- a/server/routers/target/handleHealthcheckStatusMessage.ts
+++ b/server/routers/target/handleHealthcheckStatusMessage.ts
@@ -1,5 +1,5 @@
import { db, targets, resources, sites, targetHealthCheck } from "@server/db";
-import { MessageHandler } from "../ws";
+import { MessageHandler } from "@server/routers/ws";
import { Newt } from "@server/db";
import { eq, and } from "drizzle-orm";
import logger from "@server/logger";
diff --git a/server/routers/traefik/traefikConfigProvider.ts b/server/routers/traefik/traefikConfigProvider.ts
index 50ec0770..6c9404e9 100644
--- a/server/routers/traefik/traefikConfigProvider.ts
+++ b/server/routers/traefik/traefikConfigProvider.ts
@@ -3,7 +3,7 @@ import logger from "@server/logger";
import HttpCode from "@server/types/HttpCode";
import config from "@server/lib/config";
import { build } from "@server/build";
-import { getTraefikConfig } from "@server/lib/traefik";
+import { getTraefikConfig } from "#dynamic/lib/traefik";
import { getCurrentExitNodeId } from "@server/lib/exitNodes";
const badgerMiddlewareName = "badger";
diff --git a/server/routers/user/acceptInvite.ts b/server/routers/user/acceptInvite.ts
index b553bd19..5e4264f9 100644
--- a/server/routers/user/acceptInvite.ts
+++ b/server/routers/user/acceptInvite.ts
@@ -10,8 +10,8 @@ import logger from "@server/logger";
import { fromError } from "zod-validation-error";
import { checkValidInvite } from "@server/auth/checkValidInvite";
import { verifySession } from "@server/auth/sessions/verifySession";
-import { usageService } from "@server/lib/private/billing/usageService";
-import { FeatureId } from "@server/lib/private/billing";
+import { usageService } from "@server/lib/billing/usageService";
+import { FeatureId } from "@server/lib/billing";
const acceptInviteBodySchema = z
.object({
diff --git a/server/routers/user/createOrgUser.ts b/server/routers/user/createOrgUser.ts
index b8f681c3..29f94641 100644
--- a/server/routers/user/createOrgUser.ts
+++ b/server/routers/user/createOrgUser.ts
@@ -10,11 +10,11 @@ import { db, UserOrg } from "@server/db";
import { and, eq } from "drizzle-orm";
import { idp, idpOidcConfig, roles, userOrgs, users } from "@server/db";
import { generateId } from "@server/auth/sessions/app";
-import { usageService } from "@server/lib/private/billing/usageService";
-import { FeatureId } from "@server/lib/private/billing";
+import { usageService } from "@server/lib/billing/usageService";
+import { FeatureId } from "@server/lib/billing";
import { build } from "@server/build";
-import { getOrgTierData } from "@server/routers/private/billing";
-import { TierId } from "@server/lib/private/billing/tiers";
+import { getOrgTierData } from "#dynamic/lib/billing";
+import { TierId } from "@server/lib/billing/tiers";
const paramsSchema = z
.object({
diff --git a/server/routers/user/inviteUser.ts b/server/routers/user/inviteUser.ts
index 746a383b..56098bea 100644
--- a/server/routers/user/inviteUser.ts
+++ b/server/routers/user/inviteUser.ts
@@ -17,8 +17,8 @@ import { sendEmail } from "@server/emails";
import SendInviteLink from "@server/emails/templates/SendInviteLink";
import { OpenAPITags, registry } from "@server/openApi";
import { UserType } from "@server/types/UserTypes";
-import { usageService } from "@server/lib/private/billing/usageService";
-import { FeatureId } from "@server/lib/private/billing";
+import { usageService } from "@server/lib/billing/usageService";
+import { FeatureId } from "@server/lib/billing";
import { build } from "@server/build";
const regenerateTracker = new NodeCache({ stdTTL: 3600, checkperiod: 600 });
diff --git a/server/routers/user/removeUserOrg.ts b/server/routers/user/removeUserOrg.ts
index 6d0c5359..babccdd0 100644
--- a/server/routers/user/removeUserOrg.ts
+++ b/server/routers/user/removeUserOrg.ts
@@ -9,8 +9,8 @@ import createHttpError from "http-errors";
import logger from "@server/logger";
import { fromError } from "zod-validation-error";
import { OpenAPITags, registry } from "@server/openApi";
-import { usageService } from "@server/lib/private/billing/usageService";
-import { FeatureId } from "@server/lib/private/billing";
+import { usageService } from "@server/lib/billing/usageService";
+import { FeatureId } from "@server/lib/billing";
import { build } from "@server/build";
import { UserType } from "@server/types/UserTypes";
diff --git a/server/routers/ws/index.ts b/server/routers/ws/index.ts
index 376a960b..16440ec9 100644
--- a/server/routers/ws/index.ts
+++ b/server/routers/ws/index.ts
@@ -1,24 +1,2 @@
-import { build } from "@server/build";
-
-// Import both modules
-import * as wsModule from "./ws";
-import * as privateWsModule from "./privateWs";
-
-// Conditionally export WebSocket implementation based on build type
-const wsImplementation = build === "oss" ? wsModule : privateWsModule;
-
-// Re-export all items from the selected implementation
-export const {
- router,
- handleWSUpgrade,
- sendToClient,
- broadcastToAllExcept,
- connectedClients,
- hasActiveConnections,
- getActiveNodes,
- NODE_ID,
- cleanup
-} = wsImplementation;
-
-// Re-export the MessageHandler type (both modules have the same type signature)
-export type { MessageHandler } from "./privateWs";
\ No newline at end of file
+export * from "./ws";
+export * from "./types";
\ No newline at end of file
diff --git a/server/routers/ws/messageHandlers.ts b/server/routers/ws/messageHandlers.ts
index 5b111eec..cbb023b3 100644
--- a/server/routers/ws/messageHandlers.ts
+++ b/server/routers/ws/messageHandlers.ts
@@ -13,10 +13,8 @@ import {
handleOlmPingMessage,
startOlmOfflineChecker
} from "../olm";
-import { handleRemoteExitNodeRegisterMessage, handleRemoteExitNodePingMessage, startRemoteExitNodeOfflineChecker } from "@server/routers/private/remoteExitNode";
-import { MessageHandler } from "./privateWs";
import { handleHealthcheckStatusMessage } from "../target";
-import { build } from "@server/build";
+import { MessageHandler } from "./types";
export const messageHandlers: Record = {
"newt/wg/register": handleNewtRegisterMessage,
@@ -30,12 +28,6 @@ export const messageHandlers: Record = {
"newt/ping/request": handleNewtPingRequestMessage,
"newt/blueprint/apply": handleApplyBlueprintMessage,
"newt/healthcheck/status": handleHealthcheckStatusMessage,
-
- "remoteExitNode/register": handleRemoteExitNodeRegisterMessage,
- "remoteExitNode/ping": handleRemoteExitNodePingMessage,
};
-startOlmOfflineChecker(); // this is to handle the offline check for olms
-if (build != "oss") {
- startRemoteExitNodeOfflineChecker(); // this is to handle the offline check for remote exit nodes
-}
\ No newline at end of file
+startOlmOfflineChecker(); // this is to handle the offline check for olms
\ No newline at end of file
diff --git a/server/routers/ws/types.ts b/server/routers/ws/types.ts
new file mode 100644
index 00000000..7063bc87
--- /dev/null
+++ b/server/routers/ws/types.ts
@@ -0,0 +1,70 @@
+import {
+ Newt,
+ newts,
+ NewtSession,
+ olms,
+ Olm,
+ OlmSession,
+ RemoteExitNode,
+ RemoteExitNodeSession,
+ remoteExitNodes
+} from "@server/db";
+import { IncomingMessage } from "http";
+import { WebSocket } from "ws";
+
+// Custom interfaces
+export interface WebSocketRequest extends IncomingMessage {
+ token?: string;
+}
+
+export type ClientType = "newt" | "olm" | "remoteExitNode";
+
+export interface AuthenticatedWebSocket extends WebSocket {
+ client?: Newt | Olm | RemoteExitNode;
+ clientType?: ClientType;
+ connectionId?: string;
+ isFullyConnected?: boolean;
+ pendingMessages?: Buffer[];
+}
+
+export interface TokenPayload {
+ client: Newt | Olm | RemoteExitNode;
+ session: NewtSession | OlmSession | RemoteExitNodeSession;
+ clientType: ClientType;
+}
+
+export interface WSMessage {
+ type: string;
+ data: any;
+}
+
+export interface HandlerResponse {
+ message: WSMessage;
+ broadcast?: boolean;
+ excludeSender?: boolean;
+ targetClientId?: string;
+}
+
+export interface HandlerContext {
+ message: WSMessage;
+ senderWs: WebSocket;
+ client: Newt | Olm | RemoteExitNode | undefined;
+ clientType: ClientType;
+ sendToClient: (clientId: string, message: WSMessage) => Promise;
+ broadcastToAllExcept: (
+ message: WSMessage,
+ excludeClientId?: string
+ ) => Promise;
+ connectedClients: Map;
+}
+
+export type MessageHandler = (context: HandlerContext) => Promise;
+
+// Redis message type for cross-node communication
+export interface RedisMessage {
+ type: "direct" | "broadcast";
+ targetClientId?: string;
+ excludeClientId?: string;
+ message: WSMessage;
+ fromNodeId: string;
+}
\ No newline at end of file
diff --git a/server/routers/ws/ws.ts b/server/routers/ws/ws.ts
index 8fb773d3..9bba41dc 100644
--- a/server/routers/ws/ws.ts
+++ b/server/routers/ws/ws.ts
@@ -1,7 +1,6 @@
import { Router, Request, Response } from "express";
import { Server as HttpServer } from "http";
import { WebSocket, WebSocketServer } from "ws";
-import { IncomingMessage } from "http";
import { Socket } from "net";
import { Newt, newts, NewtSession, olms, Olm, OlmSession } from "@server/db";
import { eq } from "drizzle-orm";
@@ -11,50 +10,15 @@ import { validateOlmSessionToken } from "@server/auth/sessions/olm";
import { messageHandlers } from "./messageHandlers";
import logger from "@server/logger";
import { v4 as uuidv4 } from "uuid";
+import { ClientType, TokenPayload, WebSocketRequest, WSMessage, AuthenticatedWebSocket } from "./types";
-// Custom interfaces
-interface WebSocketRequest extends IncomingMessage {
- token?: string;
-}
-
-type ClientType = 'newt' | 'olm';
-
-interface AuthenticatedWebSocket extends WebSocket {
- client?: Newt | Olm;
- clientType?: ClientType;
- connectionId?: string;
-}
-
-interface TokenPayload {
+// Subset of TokenPayload for public ws.ts (newt and olm only)
+interface PublicTokenPayload {
client: Newt | Olm;
session: NewtSession | OlmSession;
- clientType: ClientType;
+ clientType: "newt" | "olm";
}
-interface WSMessage {
- type: string;
- data: any;
-}
-
-interface HandlerResponse {
- message: WSMessage;
- broadcast?: boolean;
- excludeSender?: boolean;
- targetClientId?: string;
-}
-
-interface HandlerContext {
- message: WSMessage;
- senderWs: WebSocket;
- client: Newt | Olm | undefined;
- clientType: ClientType;
- sendToClient: (clientId: string, message: WSMessage) => Promise;
- broadcastToAllExcept: (message: WSMessage, excludeClientId?: string) => Promise;
- connectedClients: Map;
-}
-
-export type MessageHandler = (context: HandlerContext) => Promise;
-
const router: Router = Router();
const wss: WebSocketServer = new WebSocketServer({ noServer: true });
@@ -153,7 +117,7 @@ const getActiveNodes = async (clientType: ClientType, clientId: string): Promise
};
// Token verification middleware
-const verifyToken = async (token: string, clientType: ClientType): Promise => {
+const verifyToken = async (token: string, clientType: ClientType): Promise => {
try {
if (clientType === 'newt') {
@@ -169,7 +133,7 @@ try {
return null;
}
return { client: existingNewt[0], session, clientType };
- } else {
+ } else if (clientType === 'olm') {
const { session, olm } = await validateOlmSessionToken(token);
if (!session || !olm) {
return null;
@@ -183,13 +147,15 @@ try {
}
return { client: existingOlm[0], session, clientType };
}
+
+ return null;
} catch (error) {
logger.error("Token verification failed:", error);
return null;
}
};
-const setupConnection = async (ws: AuthenticatedWebSocket, client: Newt | Olm, clientType: ClientType): Promise => {
+const setupConnection = async (ws: AuthenticatedWebSocket, client: Newt | Olm, clientType: "newt" | "olm"): Promise => {
logger.info("Establishing websocket connection");
if (!client) {
logger.error("Connection attempt without client");
@@ -323,10 +289,6 @@ const cleanup = async (): Promise => {
}
};
-// Handle process termination
-process.on('SIGTERM', cleanup);
-process.on('SIGINT', cleanup);
-
export {
router,
handleWSUpgrade,
diff --git a/server/setup/ensureSetupToken.ts b/server/setup/ensureSetupToken.ts
index 078c99ee..1734b5e6 100644
--- a/server/setup/ensureSetupToken.ts
+++ b/server/setup/ensureSetupToken.ts
@@ -3,7 +3,6 @@ import { eq } from "drizzle-orm";
import { generateRandomString, RandomReader } from "@oslojs/crypto/random";
import moment from "moment";
import logger from "@server/logger";
-import config from "@server/lib/config";
const random: RandomReader = {
read(bytes: Uint8Array): void {
@@ -23,11 +22,6 @@ function generateId(length: number): string {
}
export async function ensureSetupToken() {
- if (config.isManagedMode()) {
- // LETS NOT WORRY ABOUT THE SERVER SECRET WHEN HYBRID
- return;
- }
-
try {
// Check if a server admin already exists
const [existingAdmin] = await db
diff --git a/server/setup/migrationsPg.ts b/server/setup/migrationsPg.ts
index 5c748c89..de3785f3 100644
--- a/server/setup/migrationsPg.ts
+++ b/server/setup/migrationsPg.ts
@@ -11,7 +11,7 @@ import m3 from "./scriptsPg/1.8.0";
import m4 from "./scriptsPg/1.9.0";
import m5 from "./scriptsPg/1.10.0";
import m6 from "./scriptsPg/1.10.2";
-import m7 from "./scriptsPg/1.10.4";
+import m7 from "./scriptsPg/1.11.0";
// THIS CANNOT IMPORT ANYTHING FROM THE SERVER
// EXCEPT FOR THE DATABASE AND THE SCHEMA
@@ -24,7 +24,7 @@ const migrations = [
{ version: "1.9.0", run: m4 },
{ version: "1.10.0", run: m5 },
{ version: "1.10.2", run: m6 },
- { version: "1.10.4", run: m7 },
+ { version: "1.11.0", run: m7 },
// Add new migrations here as they are created
] as {
version: string;
diff --git a/server/setup/migrationsSqlite.ts b/server/setup/migrationsSqlite.ts
index 80d2139d..b987b833 100644
--- a/server/setup/migrationsSqlite.ts
+++ b/server/setup/migrationsSqlite.ts
@@ -29,7 +29,7 @@ import m24 from "./scriptsSqlite/1.9.0";
import m25 from "./scriptsSqlite/1.10.0";
import m26 from "./scriptsSqlite/1.10.1";
import m27 from "./scriptsSqlite/1.10.2";
-import m28 from "./scriptsSqlite/1.10.4";
+import m28 from "./scriptsSqlite/1.11.0";
// THIS CANNOT IMPORT ANYTHING FROM THE SERVER
// EXCEPT FOR THE DATABASE AND THE SCHEMA
@@ -58,7 +58,7 @@ const migrations = [
{ version: "1.10.0", run: m25 },
{ version: "1.10.1", run: m26 },
{ version: "1.10.2", run: m27 },
- { version: "1.10.4", run: m28 },
+ { version: "1.11.0", run: m28 },
// Add new migrations here as they are created
] as const;
diff --git a/server/setup/scriptsPg/1.10.4.ts b/server/setup/scriptsPg/1.10.4.ts
deleted file mode 100644
index da862412..00000000
--- a/server/setup/scriptsPg/1.10.4.ts
+++ /dev/null
@@ -1,92 +0,0 @@
-import { db } from "@server/db/pg/driver";
-import { sql } from "drizzle-orm";
-import { isoBase64URL } from "@simplewebauthn/server/helpers";
-import { randomUUID } from "crypto";
-
-const version = "1.10.4";
-
-export default async function migration() {
- console.log(`Running setup script ${version}...`);
-
- try {
- await db.execute(sql`BEGIN`);
-
- const webauthnCredentialsQuery = await db.execute(
- sql`SELECT "credentialId", "publicKey", "userId", "signCount", "transports", "name", "lastUsed", "dateCreated" FROM "webauthnCredentials"`
- );
-
- const webauthnCredentials = webauthnCredentialsQuery.rows as {
- credentialId: string;
- publicKey: string;
- userId: string;
- signCount: number;
- transports: string | null;
- name: string | null;
- lastUsed: string;
- dateCreated: string;
- }[];
-
- for (const webauthnCredential of webauthnCredentials) {
- const newCredentialId = isoBase64URL.fromBuffer(
- new Uint8Array(
- Buffer.from(webauthnCredential.credentialId, "base64")
- )
- );
- const newPublicKey = isoBase64URL.fromBuffer(
- new Uint8Array(
- Buffer.from(webauthnCredential.publicKey, "base64")
- )
- );
-
- // Delete the old record
- await db.execute(sql`
- DELETE FROM "webauthnCredentials"
- WHERE "credentialId" = ${webauthnCredential.credentialId}
- `);
-
- // Insert the updated record with converted values
- await db.execute(sql`
- INSERT INTO "webauthnCredentials" ("credentialId", "publicKey", "userId", "signCount", "transports", "name", "lastUsed", "dateCreated")
- VALUES (${newCredentialId}, ${newPublicKey}, ${webauthnCredential.userId}, ${webauthnCredential.signCount}, ${webauthnCredential.transports}, ${webauthnCredential.name}, ${webauthnCredential.lastUsed}, ${webauthnCredential.dateCreated})
- `);
- }
-
- // 1. Add the column with placeholder so NOT NULL is satisfied
- await db.execute(sql`
- ALTER TABLE "resources"
- ADD COLUMN IF NOT EXISTS "resourceGuid" varchar(36) NOT NULL DEFAULT 'PLACEHOLDER'
- `);
-
- // 2. Fetch every row to backfill UUIDs
- const rows = await db.execute(
- sql`SELECT "resourceId" FROM "resources" WHERE "resourceGuid" = 'PLACEHOLDER'`
- );
- const resources = rows.rows as { resourceId: number }[];
-
- for (const r of resources) {
- await db.execute(sql`
- UPDATE "resources"
- SET "resourceGuid" = ${randomUUID()}
- WHERE "resourceId" = ${r.resourceId}
- `);
- }
-
- // 3. Add UNIQUE constraint now that values are filled
- await db.execute(sql`
- ALTER TABLE "resources"
- ADD CONSTRAINT "resources_resourceGuid_unique" UNIQUE("resourceGuid")
- `);
-
- await db.execute(sql`ALTER TABLE "orgs" ADD COLUMN IF NOT EXISTS "settings" text`);
-
- await db.execute(sql`COMMIT`);
- console.log(`Updated credentialId and publicKey`);
- } catch (e) {
- await db.execute(sql`ROLLBACK`);
- console.log("Unable to update credentialId and publicKey");
- console.log(e);
- throw e;
- }
-
- console.log(`${version} migration complete`);
-}
diff --git a/server/setup/scriptsPg/1.11.0.ts b/server/setup/scriptsPg/1.11.0.ts
new file mode 100644
index 00000000..13186b4f
--- /dev/null
+++ b/server/setup/scriptsPg/1.11.0.ts
@@ -0,0 +1,392 @@
+import { db } from "@server/db/pg/driver";
+import { sql } from "drizzle-orm";
+import { isoBase64URL } from "@simplewebauthn/server/helpers";
+import { randomUUID } from "crypto";
+
+const version = "1.11.0";
+
+export default async function migration() {
+ console.log(`Running setup script ${version}...`);
+
+ try {
+ await db.execute(sql`BEGIN`);
+
+ await db.execute(sql`
+ CREATE TABLE "account" (
+ "accountId" serial PRIMARY KEY NOT NULL,
+ "userId" varchar NOT NULL
+ );
+ `);
+
+ await db.execute(sql`
+ CREATE TABLE "accountDomains" (
+ "accountId" integer NOT NULL,
+ "domainId" varchar NOT NULL
+ );
+ `);
+
+ await db.execute(sql`
+ CREATE TABLE "certificates" (
+ "certId" serial PRIMARY KEY NOT NULL,
+ "domain" varchar(255) NOT NULL,
+ "domainId" varchar,
+ "wildcard" boolean DEFAULT false,
+ "status" varchar(50) DEFAULT 'pending' NOT NULL,
+ "expiresAt" bigint,
+ "lastRenewalAttempt" bigint,
+ "createdAt" bigint NOT NULL,
+ "updatedAt" bigint NOT NULL,
+ "orderId" varchar(500),
+ "errorMessage" text,
+ "renewalCount" integer DEFAULT 0,
+ "certFile" text,
+ "keyFile" text,
+ CONSTRAINT "certificates_domain_unique" UNIQUE("domain")
+ );
+ `);
+
+ await db.execute(sql`
+ CREATE TABLE "customers" (
+ "customerId" varchar(255) PRIMARY KEY NOT NULL,
+ "orgId" varchar(255) NOT NULL,
+ "email" varchar(255),
+ "name" varchar(255),
+ "phone" varchar(50),
+ "address" text,
+ "createdAt" bigint NOT NULL,
+ "updatedAt" bigint NOT NULL
+ );
+ `);
+
+ await db.execute(sql`
+ CREATE TABLE "dnsChallenges" (
+ "dnsChallengeId" serial PRIMARY KEY NOT NULL,
+ "domain" varchar(255) NOT NULL,
+ "token" varchar(255) NOT NULL,
+ "keyAuthorization" varchar(1000) NOT NULL,
+ "createdAt" bigint NOT NULL,
+ "expiresAt" bigint NOT NULL,
+ "completed" boolean DEFAULT false
+ );
+ `);
+
+ await db.execute(sql`
+ CREATE TABLE "domainNamespaces" (
+ "domainNamespaceId" varchar(255) PRIMARY KEY NOT NULL,
+ "domainId" varchar NOT NULL
+ );
+ `);
+
+ await db.execute(sql`
+ CREATE TABLE "exitNodeOrgs" (
+ "exitNodeId" integer NOT NULL,
+ "orgId" text NOT NULL
+ );
+ `);
+
+ await db.execute(sql`
+ CREATE TABLE "limits" (
+ "limitId" varchar(255) PRIMARY KEY NOT NULL,
+ "featureId" varchar(255) NOT NULL,
+ "orgId" varchar NOT NULL,
+ "value" real,
+ "description" text
+ );
+ `);
+
+ await db.execute(sql`
+ CREATE TABLE "loginPage" (
+ "loginPageId" serial PRIMARY KEY NOT NULL,
+ "subdomain" varchar,
+ "fullDomain" varchar,
+ "exitNodeId" integer,
+ "domainId" varchar
+ );
+ `);
+
+ await db.execute(sql`
+ CREATE TABLE "loginPageOrg" (
+ "loginPageId" integer NOT NULL,
+ "orgId" varchar NOT NULL
+ );
+ `);
+
+ await db.execute(sql`
+ CREATE TABLE "remoteExitNodeSession" (
+ "id" varchar PRIMARY KEY NOT NULL,
+ "remoteExitNodeId" varchar NOT NULL,
+ "expiresAt" bigint NOT NULL
+ );
+ `);
+
+ await db.execute(sql`
+ CREATE TABLE "remoteExitNode" (
+ "id" varchar PRIMARY KEY NOT NULL,
+ "secretHash" varchar NOT NULL,
+ "dateCreated" varchar NOT NULL,
+ "version" varchar,
+ "exitNodeId" integer
+ );
+ `);
+
+ await db.execute(sql`
+ CREATE TABLE "sessionTransferToken" (
+ "token" varchar PRIMARY KEY NOT NULL,
+ "sessionId" varchar NOT NULL,
+ "encryptedSession" text NOT NULL,
+ "expiresAt" bigint NOT NULL
+ );
+ `);
+
+ await db.execute(sql`
+ CREATE TABLE "subscriptionItems" (
+ "subscriptionItemId" serial PRIMARY KEY NOT NULL,
+ "subscriptionId" varchar(255) NOT NULL,
+ "planId" varchar(255) NOT NULL,
+ "priceId" varchar(255),
+ "meterId" varchar(255),
+ "unitAmount" real,
+ "tiers" text,
+ "interval" varchar(50),
+ "currentPeriodStart" bigint,
+ "currentPeriodEnd" bigint,
+ "name" varchar(255)
+ );
+ `);
+
+ await db.execute(sql`
+ CREATE TABLE "subscriptions" (
+ "subscriptionId" varchar(255) PRIMARY KEY NOT NULL,
+ "customerId" varchar(255) NOT NULL,
+ "status" varchar(50) DEFAULT 'active' NOT NULL,
+ "canceledAt" bigint,
+ "createdAt" bigint NOT NULL,
+ "updatedAt" bigint,
+ "billingCycleAnchor" bigint
+ );
+ `);
+
+ await db.execute(sql`
+ CREATE TABLE "usage" (
+ "usageId" varchar(255) PRIMARY KEY NOT NULL,
+ "featureId" varchar(255) NOT NULL,
+ "orgId" varchar NOT NULL,
+ "meterId" varchar(255),
+ "instantaneousValue" real,
+ "latestValue" real NOT NULL,
+ "previousValue" real,
+ "updatedAt" bigint NOT NULL,
+ "rolledOverAt" bigint,
+ "nextRolloverAt" bigint
+ );
+ `);
+
+ await db.execute(sql`
+ CREATE TABLE "usageNotifications" (
+ "notificationId" serial PRIMARY KEY NOT NULL,
+ "orgId" varchar NOT NULL,
+ "featureId" varchar(255) NOT NULL,
+ "limitId" varchar(255) NOT NULL,
+ "notificationType" varchar(50) NOT NULL,
+ "sentAt" bigint NOT NULL
+ );
+ `);
+
+ await db.execute(sql`
+ CREATE TABLE "resourceHeaderAuth" (
+ "headerAuthId" serial PRIMARY KEY NOT NULL,
+ "resourceId" integer NOT NULL,
+ "headerAuthHash" varchar NOT NULL
+ );
+ `);
+
+ await db.execute(sql`
+ CREATE TABLE "targetHealthCheck" (
+ "targetHealthCheckId" serial PRIMARY KEY NOT NULL,
+ "targetId" integer NOT NULL,
+ "hcEnabled" boolean DEFAULT false NOT NULL,
+ "hcPath" varchar,
+ "hcScheme" varchar,
+ "hcMode" varchar DEFAULT 'http',
+ "hcHostname" varchar,
+ "hcPort" integer,
+ "hcInterval" integer DEFAULT 30,
+ "hcUnhealthyInterval" integer DEFAULT 30,
+ "hcTimeout" integer DEFAULT 5,
+ "hcHeaders" varchar,
+ "hcFollowRedirects" boolean DEFAULT true,
+ "hcMethod" varchar DEFAULT 'GET',
+ "hcStatus" integer,
+ "hcHealth" text DEFAULT 'unknown'
+ );
+ `);
+
+ await db.execute(sql`ALTER TABLE "orgs" ADD COLUMN "settings" text;`);
+ await db.execute(
+ sql`ALTER TABLE "targets" ADD COLUMN "rewritePath" text;`
+ );
+ await db.execute(
+ sql`ALTER TABLE "targets" ADD COLUMN "rewritePathType" text;`
+ );
+ await db.execute(
+ sql`ALTER TABLE "targets" ADD COLUMN "priority" integer DEFAULT 100 NOT NULL;`
+ );
+ await db.execute(
+ sql`ALTER TABLE "account" ADD CONSTRAINT "account_userId_user_id_fk" FOREIGN KEY ("userId") REFERENCES "public"."user"("id") ON DELETE cascade ON UPDATE no action;`
+ );
+ await db.execute(
+ sql`ALTER TABLE "accountDomains" ADD CONSTRAINT "accountDomains_accountId_account_accountId_fk" FOREIGN KEY ("accountId") REFERENCES "public"."account"("accountId") ON DELETE cascade ON UPDATE no action;`
+ );
+ await db.execute(
+ sql`ALTER TABLE "accountDomains" ADD CONSTRAINT "accountDomains_domainId_domains_domainId_fk" FOREIGN KEY ("domainId") REFERENCES "public"."domains"("domainId") ON DELETE cascade ON UPDATE no action;`
+ );
+ await db.execute(
+ sql`ALTER TABLE "certificates" ADD CONSTRAINT "certificates_domainId_domains_domainId_fk" FOREIGN KEY ("domainId") REFERENCES "public"."domains"("domainId") ON DELETE cascade ON UPDATE no action;`
+ );
+ await db.execute(
+ sql`ALTER TABLE "customers" ADD CONSTRAINT "customers_orgId_orgs_orgId_fk" FOREIGN KEY ("orgId") REFERENCES "public"."orgs"("orgId") ON DELETE cascade ON UPDATE no action;`
+ );
+ await db.execute(
+ sql`ALTER TABLE "domainNamespaces" ADD CONSTRAINT "domainNamespaces_domainId_domains_domainId_fk" FOREIGN KEY ("domainId") REFERENCES "public"."domains"("domainId") ON DELETE set null ON UPDATE no action;`
+ );
+ await db.execute(
+ sql`ALTER TABLE "exitNodeOrgs" ADD CONSTRAINT "exitNodeOrgs_exitNodeId_exitNodes_exitNodeId_fk" FOREIGN KEY ("exitNodeId") REFERENCES "public"."exitNodes"("exitNodeId") ON DELETE cascade ON UPDATE no action;`
+ );
+ await db.execute(
+ sql`ALTER TABLE "exitNodeOrgs" ADD CONSTRAINT "exitNodeOrgs_orgId_orgs_orgId_fk" FOREIGN KEY ("orgId") REFERENCES "public"."orgs"("orgId") ON DELETE cascade ON UPDATE no action;`
+ );
+ await db.execute(
+ sql`ALTER TABLE "limits" ADD CONSTRAINT "limits_orgId_orgs_orgId_fk" FOREIGN KEY ("orgId") REFERENCES "public"."orgs"("orgId") ON DELETE cascade ON UPDATE no action;`
+ );
+ await db.execute(
+ sql`ALTER TABLE "loginPage" ADD CONSTRAINT "loginPage_exitNodeId_exitNodes_exitNodeId_fk" FOREIGN KEY ("exitNodeId") REFERENCES "public"."exitNodes"("exitNodeId") ON DELETE set null ON UPDATE no action;`
+ );
+ await db.execute(
+ sql`ALTER TABLE "loginPage" ADD CONSTRAINT "loginPage_domainId_domains_domainId_fk" FOREIGN KEY ("domainId") REFERENCES "public"."domains"("domainId") ON DELETE set null ON UPDATE no action;`
+ );
+ await db.execute(
+ sql`ALTER TABLE "loginPageOrg" ADD CONSTRAINT "loginPageOrg_loginPageId_loginPage_loginPageId_fk" FOREIGN KEY ("loginPageId") REFERENCES "public"."loginPage"("loginPageId") ON DELETE cascade ON UPDATE no action;`
+ );
+ await db.execute(
+ sql`ALTER TABLE "loginPageOrg" ADD CONSTRAINT "loginPageOrg_orgId_orgs_orgId_fk" FOREIGN KEY ("orgId") REFERENCES "public"."orgs"("orgId") ON DELETE cascade ON UPDATE no action;`
+ );
+ await db.execute(
+ sql`ALTER TABLE "remoteExitNodeSession" ADD CONSTRAINT "remoteExitNodeSession_remoteExitNodeId_remoteExitNode_id_fk" FOREIGN KEY ("remoteExitNodeId") REFERENCES "public"."remoteExitNode"("id") ON DELETE cascade ON UPDATE no action;`
+ );
+ await db.execute(
+ sql`ALTER TABLE "remoteExitNode" ADD CONSTRAINT "remoteExitNode_exitNodeId_exitNodes_exitNodeId_fk" FOREIGN KEY ("exitNodeId") REFERENCES "public"."exitNodes"("exitNodeId") ON DELETE cascade ON UPDATE no action;`
+ );
+ await db.execute(
+ sql`ALTER TABLE "sessionTransferToken" ADD CONSTRAINT "sessionTransferToken_sessionId_session_id_fk" FOREIGN KEY ("sessionId") REFERENCES "public"."session"("id") ON DELETE cascade ON UPDATE no action;`
+ );
+ await db.execute(
+ sql`ALTER TABLE "subscriptionItems" ADD CONSTRAINT "subscriptionItems_subscriptionId_subscriptions_subscriptionId_fk" FOREIGN KEY ("subscriptionId") REFERENCES "public"."subscriptions"("subscriptionId") ON DELETE cascade ON UPDATE no action;`
+ );
+ await db.execute(
+ sql`ALTER TABLE "subscriptions" ADD CONSTRAINT "subscriptions_customerId_customers_customerId_fk" FOREIGN KEY ("customerId") REFERENCES "public"."customers"("customerId") ON DELETE cascade ON UPDATE no action;`
+ );
+ await db.execute(
+ sql`ALTER TABLE "usage" ADD CONSTRAINT "usage_orgId_orgs_orgId_fk" FOREIGN KEY ("orgId") REFERENCES "public"."orgs"("orgId") ON DELETE cascade ON UPDATE no action;`
+ );
+ await db.execute(
+ sql`ALTER TABLE "usageNotifications" ADD CONSTRAINT "usageNotifications_orgId_orgs_orgId_fk" FOREIGN KEY ("orgId") REFERENCES "public"."orgs"("orgId") ON DELETE cascade ON UPDATE no action;`
+ );
+ await db.execute(
+ sql`ALTER TABLE "resourceHeaderAuth" ADD CONSTRAINT "resourceHeaderAuth_resourceId_resources_resourceId_fk" FOREIGN KEY ("resourceId") REFERENCES "public"."resources"("resourceId") ON DELETE cascade ON UPDATE no action;`
+ );
+ await db.execute(
+ sql`ALTER TABLE "targetHealthCheck" ADD CONSTRAINT "targetHealthCheck_targetId_targets_targetId_fk" FOREIGN KEY ("targetId") REFERENCES "public"."targets"("targetId") ON DELETE cascade ON UPDATE no action;`
+ );
+
+ const webauthnCredentialsQuery = await db.execute(
+ sql`SELECT "credentialId", "publicKey", "userId", "signCount", "transports", "name", "lastUsed", "dateCreated" FROM "webauthnCredentials"`
+ );
+
+ const webauthnCredentials = webauthnCredentialsQuery.rows as {
+ credentialId: string;
+ publicKey: string;
+ userId: string;
+ signCount: number;
+ transports: string | null;
+ name: string | null;
+ lastUsed: string;
+ dateCreated: string;
+ }[];
+
+ for (const webauthnCredential of webauthnCredentials) {
+ const newCredentialId = isoBase64URL.fromBuffer(
+ new Uint8Array(
+ Buffer.from(webauthnCredential.credentialId, "base64")
+ )
+ );
+ const newPublicKey = isoBase64URL.fromBuffer(
+ new Uint8Array(
+ Buffer.from(webauthnCredential.publicKey, "base64")
+ )
+ );
+
+ // Delete the old record
+ await db.execute(sql`
+ DELETE FROM "webauthnCredentials"
+ WHERE "credentialId" = ${webauthnCredential.credentialId}
+ `);
+
+ // Insert the updated record with converted values
+ await db.execute(sql`
+ INSERT INTO "webauthnCredentials" ("credentialId", "publicKey", "userId", "signCount", "transports", "name", "lastUsed", "dateCreated")
+ VALUES (${newCredentialId}, ${newPublicKey}, ${webauthnCredential.userId}, ${webauthnCredential.signCount}, ${webauthnCredential.transports}, ${webauthnCredential.name}, ${webauthnCredential.lastUsed}, ${webauthnCredential.dateCreated})
+ `);
+ }
+
+ // 1. Add the column with placeholder so NOT NULL is satisfied
+ await db.execute(sql`
+ ALTER TABLE "resources"
+ ADD COLUMN IF NOT EXISTS "resourceGuid" varchar(36) NOT NULL DEFAULT 'PLACEHOLDER'
+ `);
+
+ // 2. Fetch every row to backfill UUIDs
+ const rows = await db.execute(
+ sql`SELECT "resourceId" FROM "resources" WHERE "resourceGuid" = 'PLACEHOLDER'`
+ );
+ const resources = rows.rows as { resourceId: number }[];
+
+ for (const r of resources) {
+ await db.execute(sql`
+ UPDATE "resources"
+ SET "resourceGuid" = ${randomUUID()}
+ WHERE "resourceId" = ${r.resourceId}
+ `);
+ }
+
+ // get all of the targets
+ const targetsQuery = await db.execute(
+ sql`SELECT "targetId" FROM "targets"`
+ );
+ const targets = targetsQuery.rows as {
+ targetId: number;
+ }[];
+
+ for (const target of targets) {
+ await db.execute(sql`
+ INSERT INTO "targetHealthCheck" ("targetId")
+ VALUES (${target.targetId})
+ `);
+ }
+
+ // 3. Add UNIQUE constraint now that values are filled
+ await db.execute(sql`
+ ALTER TABLE "resources"
+ ADD CONSTRAINT "resources_resourceGuid_unique" UNIQUE("resourceGuid")
+ `);
+
+ await db.execute(sql`COMMIT`);
+ console.log(`Updated credentialId and publicKey`);
+ } catch (e) {
+ await db.execute(sql`ROLLBACK`);
+ console.log("Unable to update credentialId and publicKey");
+ console.log(e);
+ throw e;
+ }
+
+ console.log(`${version} migration complete`);
+}
diff --git a/server/setup/scriptsSqlite/1.10.4.ts b/server/setup/scriptsSqlite/1.10.4.ts
deleted file mode 100644
index 94f355a8..00000000
--- a/server/setup/scriptsSqlite/1.10.4.ts
+++ /dev/null
@@ -1,90 +0,0 @@
-import { APP_PATH } from "@server/lib/consts";
-import Database from "better-sqlite3";
-import path from "path";
-import { isoBase64URL } from "@simplewebauthn/server/helpers";
-import { randomUUID } from "crypto";
-
-const version = "1.10.4";
-
-export default async function migration() {
- console.log(`Running setup script ${version}...`);
-
- const location = path.join(APP_PATH, "db", "db.sqlite");
- const db = new Database(location);
-
- db.transaction(() => {
- const webauthnCredentials = db
- .prepare(
- `SELECT credentialId, publicKey, userId, signCount, transports, name, lastUsed, dateCreated FROM 'webauthnCredentials'`
- )
- .all() as {
- credentialId: string;
- publicKey: string;
- userId: string;
- signCount: number;
- transports: string | null;
- name: string | null;
- lastUsed: string;
- dateCreated: string;
- }[];
-
- for (const webauthnCredential of webauthnCredentials) {
- const newCredentialId = isoBase64URL.fromBuffer(
- new Uint8Array(
- Buffer.from(webauthnCredential.credentialId, "base64")
- )
- );
- const newPublicKey = isoBase64URL.fromBuffer(
- new Uint8Array(
- Buffer.from(webauthnCredential.publicKey, "base64")
- )
- );
-
- // Delete the old record
- db.prepare(
- `DELETE FROM 'webauthnCredentials' WHERE 'credentialId' = ?`
- ).run(webauthnCredential.credentialId);
-
- // Insert the updated record with converted values
- db.prepare(
- `INSERT INTO 'webauthnCredentials' (credentialId, publicKey, userId, signCount, transports, name, lastUsed, dateCreated) VALUES (?, ?, ?, ?, ?, ?, ?, ?)`
- ).run(
- newCredentialId,
- newPublicKey,
- webauthnCredential.userId,
- webauthnCredential.signCount,
- webauthnCredential.transports,
- webauthnCredential.name,
- webauthnCredential.lastUsed,
- webauthnCredential.dateCreated
- );
- }
-
- // 1. Add the column (nullable or with placeholder) if it doesn’t exist yet
- db.prepare(
- `ALTER TABLE resources ADD COLUMN resourceGuid TEXT DEFAULT 'PLACEHOLDER';`
- ).run();
-
- // 2. Select all rows
- const rows = db.prepare(`SELECT resourceId FROM resources`).all() as {
- resourceId: number;
- }[];
-
- // 3. Prefill with random UUIDs
- const updateStmt = db.prepare(
- `UPDATE resources SET resourceGuid = ? WHERE resourceId = ?`
- );
-
- for (const row of rows) {
- updateStmt.run(randomUUID(), row.resourceId);
- }
-
- db.prepare(
- `CREATE UNIQUE INDEX resources_resourceGuid_unique ON resources ('resourceGuid');`
- ).run();
-
- db.prepare(`ALTER TABLE "orgs" ADD COLUMN IF NOT EXISTS "settings" text`).run();
- })();
-
- console.log(`${version} migration complete`);
-}
diff --git a/server/setup/scriptsSqlite/1.11.0.ts b/server/setup/scriptsSqlite/1.11.0.ts
new file mode 100644
index 00000000..1247eee9
--- /dev/null
+++ b/server/setup/scriptsSqlite/1.11.0.ts
@@ -0,0 +1,342 @@
+import { APP_PATH } from "@server/lib/consts";
+import Database from "better-sqlite3";
+import path from "path";
+import { isoBase64URL } from "@simplewebauthn/server/helpers";
+import { randomUUID } from "crypto";
+
+const version = "1.11.0";
+
+export default async function migration() {
+ console.log(`Running setup script ${version}...`);
+
+ const location = path.join(APP_PATH, "db", "db.sqlite");
+ const db = new Database(location);
+
+ db.transaction(() => {
+
+ db.prepare(`
+ CREATE TABLE 'account' (
+ 'accountId' integer PRIMARY KEY AUTOINCREMENT NOT NULL,
+ 'userId' text NOT NULL,
+ FOREIGN KEY ('userId') REFERENCES 'user'('id') ON UPDATE no action ON DELETE cascade
+ );
+ `).run();
+
+ db.prepare(`
+ CREATE TABLE 'accountDomains' (
+ 'accountId' integer NOT NULL,
+ 'domainId' text NOT NULL,
+ FOREIGN KEY ('accountId') REFERENCES 'account'('accountId') ON UPDATE no action ON DELETE cascade,
+ FOREIGN KEY ('domainId') REFERENCES 'domains'('domainId') ON UPDATE no action ON DELETE cascade
+ );
+ `).run();
+
+ db.prepare(`
+ CREATE TABLE 'certificates' (
+ 'certId' integer PRIMARY KEY AUTOINCREMENT NOT NULL,
+ 'domain' text NOT NULL,
+ 'domainId' text,
+ 'wildcard' integer DEFAULT false,
+ 'status' text DEFAULT 'pending' NOT NULL,
+ 'expiresAt' integer,
+ 'lastRenewalAttempt' integer,
+ 'createdAt' integer NOT NULL,
+ 'updatedAt' integer NOT NULL,
+ 'orderId' text,
+ 'errorMessage' text,
+ 'renewalCount' integer DEFAULT 0,
+ 'certFile' text,
+ 'keyFile' text,
+ FOREIGN KEY ('domainId') REFERENCES 'domains'('domainId') ON UPDATE no action ON DELETE cascade
+ );
+ `).run();
+
+ db.prepare(`CREATE UNIQUE INDEX 'certificates_domain_unique' ON 'certificates' ('domain');`).run();
+
+ db.prepare(`
+ CREATE TABLE 'customers' (
+ 'customerId' text PRIMARY KEY NOT NULL,
+ 'orgId' text NOT NULL,
+ 'email' text,
+ 'name' text,
+ 'phone' text,
+ 'address' text,
+ 'createdAt' integer NOT NULL,
+ 'updatedAt' integer NOT NULL,
+ FOREIGN KEY ('orgId') REFERENCES 'orgs'('orgId') ON UPDATE no action ON DELETE cascade
+ );
+ `).run();
+
+ db.prepare(`
+ CREATE TABLE 'dnsChallenges' (
+ 'dnsChallengeId' integer PRIMARY KEY AUTOINCREMENT NOT NULL,
+ 'domain' text NOT NULL,
+ 'token' text NOT NULL,
+ 'keyAuthorization' text NOT NULL,
+ 'createdAt' integer NOT NULL,
+ 'expiresAt' integer NOT NULL,
+ 'completed' integer DEFAULT false
+ );
+ `).run();
+
+ db.prepare(`
+ CREATE TABLE 'domainNamespaces' (
+ 'domainNamespaceId' text PRIMARY KEY NOT NULL,
+ 'domainId' text NOT NULL,
+ FOREIGN KEY ('domainId') REFERENCES 'domains'('domainId') ON UPDATE no action ON DELETE set null
+ );
+ `).run();
+
+ db.prepare(`
+ CREATE TABLE 'exitNodeOrgs' (
+ 'exitNodeId' integer NOT NULL,
+ 'orgId' text NOT NULL,
+ FOREIGN KEY ('exitNodeId') REFERENCES 'exitNodes'('exitNodeId') ON UPDATE no action ON DELETE cascade,
+ FOREIGN KEY ('orgId') REFERENCES 'orgs'('orgId') ON UPDATE no action ON DELETE cascade
+ );
+ `).run();
+
+ db.prepare(`
+ CREATE TABLE 'loginPage' (
+ 'loginPageId' integer PRIMARY KEY AUTOINCREMENT NOT NULL,
+ 'subdomain' text,
+ 'fullDomain' text,
+ 'exitNodeId' integer,
+ 'domainId' text,
+ FOREIGN KEY ('exitNodeId') REFERENCES 'exitNodes'('exitNodeId') ON UPDATE no action ON DELETE set null,
+ FOREIGN KEY ('domainId') REFERENCES 'domains'('domainId') ON UPDATE no action ON DELETE set null
+ );
+ `).run();
+
+ db.prepare(`
+ CREATE TABLE 'loginPageOrg' (
+ 'loginPageId' integer NOT NULL,
+ 'orgId' text NOT NULL,
+ FOREIGN KEY ('loginPageId') REFERENCES 'loginPage'('loginPageId') ON UPDATE no action ON DELETE cascade,
+ FOREIGN KEY ('orgId') REFERENCES 'orgs'('orgId') ON UPDATE no action ON DELETE cascade
+ );
+ `).run();
+
+ db.prepare(`
+ CREATE TABLE 'remoteExitNodeSession' (
+ 'id' text PRIMARY KEY NOT NULL,
+ 'remoteExitNodeId' text NOT NULL,
+ 'expiresAt' integer NOT NULL,
+ FOREIGN KEY ('remoteExitNodeId') REFERENCES 'remoteExitNode'('id') ON UPDATE no action ON DELETE cascade
+ );
+ `).run();
+
+ db.prepare(`
+ CREATE TABLE 'remoteExitNode' (
+ 'id' text PRIMARY KEY NOT NULL,
+ 'secretHash' text NOT NULL,
+ 'dateCreated' text NOT NULL,
+ 'version' text,
+ 'exitNodeId' integer,
+ FOREIGN KEY ('exitNodeId') REFERENCES 'exitNodes'('exitNodeId') ON UPDATE no action ON DELETE cascade
+ );
+ `).run();
+
+ db.prepare(`
+ CREATE TABLE 'sessionTransferToken' (
+ 'token' text PRIMARY KEY NOT NULL,
+ 'sessionId' text NOT NULL,
+ 'encryptedSession' text NOT NULL,
+ 'expiresAt' integer NOT NULL,
+ FOREIGN KEY ('sessionId') REFERENCES 'session'('id') ON UPDATE no action ON DELETE cascade
+ );
+ `).run();
+
+ db.prepare(`
+ CREATE TABLE 'subscriptionItems' (
+ 'subscriptionItemId' integer PRIMARY KEY AUTOINCREMENT NOT NULL,
+ 'subscriptionId' text NOT NULL,
+ 'planId' text NOT NULL,
+ 'priceId' text,
+ 'meterId' text,
+ 'unitAmount' real,
+ 'tiers' text,
+ 'interval' text,
+ 'currentPeriodStart' integer,
+ 'currentPeriodEnd' integer,
+ 'name' text,
+ FOREIGN KEY ('subscriptionId') REFERENCES 'subscriptions'('subscriptionId') ON UPDATE no action ON DELETE cascade
+ );
+ `).run();
+
+ db.prepare(`
+ CREATE TABLE 'subscriptions' (
+ 'subscriptionId' text PRIMARY KEY NOT NULL,
+ 'customerId' text NOT NULL,
+ 'status' text DEFAULT 'active' NOT NULL,
+ 'canceledAt' integer,
+ 'createdAt' integer NOT NULL,
+ 'updatedAt' integer,
+ 'billingCycleAnchor' integer,
+ FOREIGN KEY ('customerId') REFERENCES 'customers'('customerId') ON UPDATE no action ON DELETE cascade
+ );
+ `).run();
+
+ db.prepare(`
+ CREATE TABLE 'usage' (
+ 'usageId' text PRIMARY KEY NOT NULL,
+ 'featureId' text NOT NULL,
+ 'orgId' text NOT NULL,
+ 'meterId' text,
+ 'instantaneousValue' real,
+ 'latestValue' real NOT NULL,
+ 'previousValue' real,
+ 'updatedAt' integer NOT NULL,
+ 'rolledOverAt' integer,
+ 'nextRolloverAt' integer,
+ FOREIGN KEY ('orgId') REFERENCES 'orgs'('orgId') ON UPDATE no action ON DELETE cascade
+ );
+ `).run();
+
+ db.prepare(`
+ CREATE TABLE 'usageNotifications' (
+ 'notificationId' integer PRIMARY KEY AUTOINCREMENT NOT NULL,
+ 'orgId' text NOT NULL,
+ 'featureId' text NOT NULL,
+ 'limitId' text NOT NULL,
+ 'notificationType' text NOT NULL,
+ 'sentAt' integer NOT NULL,
+ FOREIGN KEY ('orgId') REFERENCES 'orgs'('orgId') ON UPDATE no action ON DELETE cascade
+ );
+ `).run();
+
+ db.prepare(`
+ CREATE TABLE 'resourceHeaderAuth' (
+ 'headerAuthId' integer PRIMARY KEY AUTOINCREMENT NOT NULL,
+ 'resourceId' integer NOT NULL,
+ 'headerAuthHash' text NOT NULL,
+ FOREIGN KEY ('resourceId') REFERENCES 'resources'('resourceId') ON UPDATE no action ON DELETE cascade
+ );
+ `).run();
+
+ db.prepare(`
+ CREATE TABLE 'targetHealthCheck' (
+ 'targetHealthCheckId' integer PRIMARY KEY AUTOINCREMENT NOT NULL,
+ 'targetId' integer NOT NULL,
+ 'hcEnabled' integer DEFAULT false NOT NULL,
+ 'hcPath' text,
+ 'hcScheme' text,
+ 'hcMode' text DEFAULT 'http',
+ 'hcHostname' text,
+ 'hcPort' integer,
+ 'hcInterval' integer DEFAULT 30,
+ 'hcUnhealthyInterval' integer DEFAULT 30,
+ 'hcTimeout' integer DEFAULT 5,
+ 'hcHeaders' text,
+ 'hcFollowRedirects' integer DEFAULT true,
+ 'hcMethod' text DEFAULT 'GET',
+ 'hcStatus' integer,
+ 'hcHealth' text DEFAULT 'unknown',
+ FOREIGN KEY ('targetId') REFERENCES 'targets'('targetId') ON UPDATE no action ON DELETE cascade
+ );
+ `).run();
+
+ db.prepare(`DROP TABLE 'limits';`).run();
+
+ db.prepare(`
+ CREATE TABLE 'limits' (
+ 'limitId' text PRIMARY KEY NOT NULL,
+ 'featureId' text NOT NULL,
+ 'orgId' text NOT NULL,
+ 'value' real,
+ 'description' text,
+ FOREIGN KEY ('orgId') REFERENCES 'orgs'('orgId') ON UPDATE no action ON DELETE cascade
+ );
+ `).run();
+
+ db.prepare(`ALTER TABLE 'orgs' ADD 'settings' text;`).run();
+ db.prepare(`ALTER TABLE 'targets' ADD 'rewritePath' text;`).run();
+ db.prepare(`ALTER TABLE 'targets' ADD 'rewritePathType' text;`).run();
+ db.prepare(`ALTER TABLE 'targets' ADD 'priority' integer DEFAULT 100 NOT NULL;`).run();
+
+ const webauthnCredentials = db
+ .prepare(
+ `SELECT credentialId, publicKey, userId, signCount, transports, name, lastUsed, dateCreated FROM 'webauthnCredentials'`
+ )
+ .all() as {
+ credentialId: string;
+ publicKey: string;
+ userId: string;
+ signCount: number;
+ transports: string | null;
+ name: string | null;
+ lastUsed: string;
+ dateCreated: string;
+ }[];
+
+ for (const webauthnCredential of webauthnCredentials) {
+ const newCredentialId = isoBase64URL.fromBuffer(
+ new Uint8Array(
+ Buffer.from(webauthnCredential.credentialId, "base64")
+ )
+ );
+ const newPublicKey = isoBase64URL.fromBuffer(
+ new Uint8Array(
+ Buffer.from(webauthnCredential.publicKey, "base64")
+ )
+ );
+
+ // Delete the old record
+ db.prepare(
+ `DELETE FROM 'webauthnCredentials' WHERE 'credentialId' = ?`
+ ).run(webauthnCredential.credentialId);
+
+ // Insert the updated record with converted values
+ db.prepare(
+ `INSERT INTO 'webauthnCredentials' (credentialId, publicKey, userId, signCount, transports, name, lastUsed, dateCreated) VALUES (?, ?, ?, ?, ?, ?, ?, ?)`
+ ).run(
+ newCredentialId,
+ newPublicKey,
+ webauthnCredential.userId,
+ webauthnCredential.signCount,
+ webauthnCredential.transports,
+ webauthnCredential.name,
+ webauthnCredential.lastUsed,
+ webauthnCredential.dateCreated
+ );
+ }
+
+ // 1. Add the column (nullable or with placeholder) if it doesn’t exist yet
+ db.prepare(
+ `ALTER TABLE resources ADD COLUMN resourceGuid TEXT DEFAULT 'PLACEHOLDER';`
+ ).run();
+
+ // 2. Select all rows
+ const resources = db.prepare(`SELECT resourceId FROM resources`).all() as {
+ resourceId: number;
+ }[];
+
+ // 3. Prefill with random UUIDs
+ const updateStmt = db.prepare(
+ `UPDATE resources SET resourceGuid = ? WHERE resourceId = ?`
+ );
+
+ for (const row of resources) {
+ updateStmt.run(randomUUID(), row.resourceId);
+ }
+
+ // get all of the targets
+ const targets = db.prepare(`SELECT targetId FROM targets`).all() as {
+ targetId: number;
+ }[];
+
+ const insertTargetHealthCheckStmt = db.prepare(
+ `INSERT INTO targetHealthCheck (targetId) VALUES (?)`
+ );
+
+ for (const target of targets) {
+ insertTargetHealthCheckStmt.run(target.targetId);
+ }
+
+ db.prepare(
+ `CREATE UNIQUE INDEX resources_resourceGuid_unique ON resources ('resourceGuid');`
+ ).run();
+ })();
+
+ console.log(`${version} migration complete`);
+}
diff --git a/src/app/[orgId]/layout.tsx b/src/app/[orgId]/layout.tsx
index ecc7de01..0f67fc83 100644
--- a/src/app/[orgId]/layout.tsx
+++ b/src/app/[orgId]/layout.tsx
@@ -1,16 +1,14 @@
import { internal } from "@app/lib/api";
import { authCookieHeader } from "@app/lib/api/cookies";
-import ProfileIcon from "@app/components/ProfileIcon";
import { verifySession } from "@app/lib/auth/verifySession";
-import UserProvider from "@app/providers/UserProvider";
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 SetLastOrgCookie from "@app/components/SetLastOrgCookie";
-import PrivateSubscriptionStatusProvider from "@app/providers/PrivateSubscriptionStatusProvider";
-import { GetOrgSubscriptionResponse } from "@server/routers/private/billing/getOrgSubscription";
+import SubscriptionStatusProvider from "@app/providers/SubscriptionStatusProvider";
+import { GetOrgSubscriptionResponse } from "@server/routers/billing/types";
import { pullEnv } from "@app/lib/pullEnv";
import { build } from "@server/build";
@@ -56,7 +54,7 @@ export default async function OrgLayout(props: {
}
let subscriptionStatus = null;
- if (build != "oss") {
+ if (build === "saas") {
try {
const getSubscription = cache(() =>
internal.get>(
@@ -73,13 +71,13 @@ export default async function OrgLayout(props: {
}
return (
-
{props.children}
-
+
);
}
diff --git a/src/app/[orgId]/page.tsx b/src/app/[orgId]/page.tsx
index 4c3ac07b..25b3de1f 100644
--- a/src/app/[orgId]/page.tsx
+++ b/src/app/[orgId]/page.tsx
@@ -1,7 +1,6 @@
import { verifySession } from "@app/lib/auth/verifySession";
import UserProvider from "@app/providers/UserProvider";
import { cache } from "react";
-import OrganizationLandingCard from "../../components/OrganizationLandingCard";
import MemberResourcesPortal from "../../components/MemberResourcesPortal";
import { GetOrgOverviewResponse } from "@server/routers/org/getOrgOverview";
import { internal } from "@app/lib/api";
diff --git a/src/app/[orgId]/settings/(private)/billing/layout.tsx b/src/app/[orgId]/settings/(private)/billing/layout.tsx
index 8559ddf4..538c7fde 100644
--- a/src/app/[orgId]/settings/(private)/billing/layout.tsx
+++ b/src/app/[orgId]/settings/(private)/billing/layout.tsx
@@ -1,16 +1,3 @@
-/*
- * 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 { internal } from "@app/lib/api";
import { authCookieHeader } from "@app/lib/api/cookies";
import SettingsSectionTitle from "@app/components/SettingsSectionTitle";
@@ -73,13 +60,6 @@ export default async function BillingSettingsPage({
const t = await getTranslations();
- const navItems = [
- {
- title: t('billing'),
- href: `/{orgId}/settings/billing`,
- },
- ];
-
return (
<>
diff --git a/src/app/[orgId]/settings/(private)/billing/page.tsx b/src/app/[orgId]/settings/(private)/billing/page.tsx
index 1270c30f..a2841fb4 100644
--- a/src/app/[orgId]/settings/(private)/billing/page.tsx
+++ b/src/app/[orgId]/settings/(private)/billing/page.tsx
@@ -1,16 +1,3 @@
-/*
- * 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.
- */
-
"use client";
import { Button } from "@app/components/ui/button";
@@ -50,7 +37,7 @@ import { InfoPopup } from "@/components/ui/info-popup";
import {
GetOrgSubscriptionResponse,
GetOrgUsageResponse
-} from "@server/routers/private/billing";
+} from "@server/routers/billing/types";
import { useTranslations } from "use-intl";
import Link from "next/link";
diff --git a/src/app/[orgId]/settings/(private)/idp/[idpId]/general/page.tsx b/src/app/[orgId]/settings/(private)/idp/[idpId]/general/page.tsx
index 59f7aa85..1d0a682f 100644
--- a/src/app/[orgId]/settings/(private)/idp/[idpId]/general/page.tsx
+++ b/src/app/[orgId]/settings/(private)/idp/[idpId]/general/page.tsx
@@ -1,16 +1,3 @@
-/*
- * 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.
- */
-
"use client";
import { zodResolver } from "@hookform/resolvers/zod";
@@ -144,7 +131,7 @@ export default function GeneralPage() {
};
const form = useForm({
- resolver: zodResolver(getFormSchema()) as any, // is this right?
+ resolver: zodResolver(getFormSchema()) as any, // is this right?
defaultValues: {
name: "",
clientId: "",
@@ -236,17 +223,11 @@ export default function GeneralPage() {
let tenantId = "";
if (idpVariant === "azure" && data.idpOidcConfig?.authUrl) {
// Azure URL format: https://login.microsoftonline.com/{tenant-id}/oauth2/v2.0/authorize
- console.log(
- "Azure authUrl:",
- data.idpOidcConfig.authUrl
- );
const tenantMatch = data.idpOidcConfig.authUrl.match(
/login\.microsoftonline\.com\/([^\/]+)\/oauth2/
);
- console.log("Tenant match:", tenantMatch);
if (tenantMatch) {
tenantId = tenantMatch[1];
- console.log("Extracted tenantId:", tenantId);
}
}
@@ -262,8 +243,6 @@ export default function GeneralPage() {
: matchingRoleId || null
};
- console.log(formData);
-
// Add variant-specific fields
if (idpVariant === "oidc") {
formData.authUrl = data.idpOidcConfig.authUrl;
@@ -276,7 +255,6 @@ export default function GeneralPage() {
formData.scopes = data.idpOidcConfig.scopes;
} else if (idpVariant === "azure") {
formData.tenantId = tenantId;
- console.log("Setting tenantId in formData:", tenantId);
}
form.reset(formData);
@@ -284,7 +262,7 @@ export default function GeneralPage() {
// Set the role mapping mode based on the data
// Default to "expression" unless it's a simple roleId or basic '{role name}' pattern
setRoleMappingMode(
- isRoleId || isRoleName ? "role" : "expression"
+ matchingRoleId && isRoleName ? "role" : "expression"
);
}
} catch (e) {
@@ -851,7 +829,7 @@ export default function GeneralPage() {
{t(
"idpJmespathAboutDescription"
- )}
+ )}{" "}
diff --git a/src/app/[orgId]/settings/(private)/license/layout.tsx b/src/app/[orgId]/settings/(private)/license/layout.tsx
new file mode 100644
index 00000000..9083bb81
--- /dev/null
+++ b/src/app/[orgId]/settings/(private)/license/layout.tsx
@@ -0,0 +1,42 @@
+import SettingsSectionTitle from "@app/components/SettingsSectionTitle";
+import { verifySession } from "@app/lib/auth/verifySession";
+import { redirect } from "next/navigation";
+import { cache } from "react";
+import { getTranslations } from "next-intl/server";
+import { build } from "@server/build";
+
+type LicensesSettingsProps = {
+ children: React.ReactNode;
+ params: Promise<{ orgId: string }>;
+};
+
+export default async function LicensesSetingsLayoutProps({
+ children,
+ params
+}: LicensesSettingsProps) {
+ const { orgId } = await params;
+
+ if (build !== "saas") {
+ redirect(`/${orgId}/settings`);
+ }
+
+ const getUser = cache(verifySession);
+ const user = await getUser();
+
+ if (!user) {
+ redirect(`/`);
+ }
+
+ const t = await getTranslations();
+
+ return (
+ <>
+
+
+ {children}
+ >
+ );
+}
diff --git a/src/app/[orgId]/settings/(private)/license/page.tsx b/src/app/[orgId]/settings/(private)/license/page.tsx
new file mode 100644
index 00000000..1ecc94c1
--- /dev/null
+++ b/src/app/[orgId]/settings/(private)/license/page.tsx
@@ -0,0 +1,25 @@
+import GenerateLicenseKeysTable from "@app/components/GenerateLicenseKeysTable";
+import { internal } from "@app/lib/api";
+import { authCookieHeader } from "@app/lib/api/cookies";
+import { ListGeneratedLicenseKeysResponse } from "@server/routers/generatedLicense/types";
+import { AxiosResponse } from "axios";
+
+type Props = {
+ params: Promise<{ orgId: string }>;
+};
+
+export const dynamic = "force-dynamic";
+
+export default async function Page({ params }: Props) {
+ const { orgId } = await params;
+
+ let licenseKeys: ListGeneratedLicenseKeysResponse = [];
+ try {
+ const data = await internal.get<
+ AxiosResponse
+ >(`/org/${orgId}/license`, await authCookieHeader());
+ licenseKeys = data.data.data;
+ } catch {}
+
+ return ;
+}
diff --git a/src/app/[orgId]/settings/(private)/remote-exit-nodes/ExitNodesDataTable.tsx b/src/app/[orgId]/settings/(private)/remote-exit-nodes/ExitNodesDataTable.tsx
index 4a1db4ea..a1bb69c0 100644
--- a/src/app/[orgId]/settings/(private)/remote-exit-nodes/ExitNodesDataTable.tsx
+++ b/src/app/[orgId]/settings/(private)/remote-exit-nodes/ExitNodesDataTable.tsx
@@ -1,16 +1,3 @@
-/*
- * 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.
- */
-
"use client";
import { ColumnDef } from "@tanstack/react-table";
diff --git a/src/app/[orgId]/settings/(private)/remote-exit-nodes/ExitNodesTable.tsx b/src/app/[orgId]/settings/(private)/remote-exit-nodes/ExitNodesTable.tsx
index 11e0bcfc..c7e332d7 100644
--- a/src/app/[orgId]/settings/(private)/remote-exit-nodes/ExitNodesTable.tsx
+++ b/src/app/[orgId]/settings/(private)/remote-exit-nodes/ExitNodesTable.tsx
@@ -1,16 +1,3 @@
-/*
- * 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.
- */
-
"use client";
import { ColumnDef } from "@tanstack/react-table";
diff --git a/src/app/[orgId]/settings/(private)/remote-exit-nodes/[remoteExitNodeId]/general/page.tsx b/src/app/[orgId]/settings/(private)/remote-exit-nodes/[remoteExitNodeId]/general/page.tsx
index b5835e1b..191ce3f3 100644
--- a/src/app/[orgId]/settings/(private)/remote-exit-nodes/[remoteExitNodeId]/general/page.tsx
+++ b/src/app/[orgId]/settings/(private)/remote-exit-nodes/[remoteExitNodeId]/general/page.tsx
@@ -1,16 +1,3 @@
-/*
- * 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.
- */
-
export default function GeneralPage() {
return <>>;
}
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 653444e8..7a7b3611 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
@@ -1,24 +1,11 @@
-/*
- * 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 { internal } from "@app/lib/api";
-import { GetRemoteExitNodeResponse } from "@server/routers/private/remoteExitNode";
+import { GetRemoteExitNodeResponse } from "@server/routers/remoteExitNode/types";
import { AxiosResponse } from "axios";
import { redirect } from "next/navigation";
import { authCookieHeader } from "@app/lib/api/cookies";
import SettingsSectionTitle from "@app/components/SettingsSectionTitle";
import { getTranslations } from "next-intl/server";
-import RemoteExitNodeProvider from "@app/providers/PrivateRemoteExitNodeProvider";
+import RemoteExitNodeProvider from "@app/providers/RemoteExitNodeProvider";
interface SettingsLayoutProps {
children: React.ReactNode;
diff --git a/src/app/[orgId]/settings/(private)/remote-exit-nodes/[remoteExitNodeId]/page.tsx b/src/app/[orgId]/settings/(private)/remote-exit-nodes/[remoteExitNodeId]/page.tsx
index badf0971..6b39c1de 100644
--- a/src/app/[orgId]/settings/(private)/remote-exit-nodes/[remoteExitNodeId]/page.tsx
+++ b/src/app/[orgId]/settings/(private)/remote-exit-nodes/[remoteExitNodeId]/page.tsx
@@ -1,16 +1,3 @@
-/*
- * 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 { redirect } from "next/navigation";
export default async function RemoteExitNodePage(props: {
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 5daaa493..ca3e0cba 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
@@ -1,16 +1,3 @@
-/*
- * 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.
- */
-
"use client";
import {
@@ -43,7 +30,7 @@ import { useEnvContext } from "@app/hooks/useEnvContext";
import {
QuickStartRemoteExitNodeResponse,
PickRemoteExitNodeDefaultsResponse
-} from "@server/routers/private/remoteExitNode";
+} from "@server/routers/remoteExitNode/types";
import { toast } from "@app/hooks/useToast";
import { AxiosResponse } from "axios";
import { useParams, useRouter, useSearchParams } from "next/navigation";
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 b18df692..632dc0ad 100644
--- a/src/app/[orgId]/settings/(private)/remote-exit-nodes/page.tsx
+++ b/src/app/[orgId]/settings/(private)/remote-exit-nodes/page.tsx
@@ -1,19 +1,6 @@
-/*
- * 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 { internal } from "@app/lib/api";
import { authCookieHeader } from "@app/lib/api/cookies";
-import { ListRemoteExitNodesResponse } from "@server/routers/private/remoteExitNode";
+import { ListRemoteExitNodesResponse } from "@server/routers/remoteExitNode/types";
import { AxiosResponse } from "axios";
import ExitNodesTable, { RemoteExitNodeRow } from "./ExitNodesTable";
import SettingsSectionTitle from "@app/components/SettingsSectionTitle";
diff --git a/src/app/[orgId]/settings/access/users/create/page.tsx b/src/app/[orgId]/settings/access/users/create/page.tsx
index 3e6d2458..d789b2e2 100644
--- a/src/app/[orgId]/settings/access/users/create/page.tsx
+++ b/src/app/[orgId]/settings/access/users/create/page.tsx
@@ -47,8 +47,8 @@ import { ListIdpsResponse } from "@server/routers/idp";
import { useTranslations } from "next-intl";
import { build } from "@server/build";
import Image from "next/image";
-import { usePrivateSubscriptionStatusContext } from "@app/hooks/privateUseSubscriptionStatusContext";
-import { TierId } from "@server/lib/private/billing/tiers";
+import { useSubscriptionStatusContext } from "@app/hooks/useSubscriptionStatusContext";
+import { TierId } from "@server/lib/billing/tiers";
type UserType = "internal" | "oidc";
@@ -76,10 +76,11 @@ export default function Page() {
const api = createApiClient({ env });
const t = useTranslations();
- const subscription = usePrivateSubscriptionStatusContext();
- const subscribed = subscription?.getTier() === TierId.STANDARD;
+ const subscription = useSubscriptionStatusContext();
- const [selectedOption, setSelectedOption] = useState("internal");
+ const [selectedOption, setSelectedOption] = useState(
+ "internal"
+ );
const [inviteLink, setInviteLink] = useState(null);
const [loading, setLoading] = useState(false);
const [expiresInDays, setExpiresInDays] = useState(1);
@@ -204,7 +205,13 @@ export default function Page() {
googleAzureForm.reset();
genericOidcForm.reset();
}
- }, [selectedOption, env.email.emailEnabled, internalForm, googleAzureForm, genericOidcForm]);
+ }, [
+ selectedOption,
+ env.email.emailEnabled,
+ internalForm,
+ googleAzureForm,
+ genericOidcForm
+ ]);
useEffect(() => {
if (!selectedOption) {
@@ -232,7 +239,7 @@ export default function Page() {
}
async function fetchIdps() {
- if (build === "saas" && !subscribed) {
+ if (build === "saas" && !subscription?.subscribed) {
return;
}
@@ -345,7 +352,9 @@ export default function Page() {
async function onSubmitGoogleAzure(
values: z.infer
) {
- const selectedUserOption = userOptions.find(opt => opt.id === selectedOption);
+ const selectedUserOption = userOptions.find(
+ (opt) => opt.id === selectedOption
+ );
if (!selectedUserOption?.idpId) return;
setLoading(true);
@@ -385,7 +394,9 @@ export default function Page() {
async function onSubmitGenericOidc(
values: z.infer
) {
- const selectedUserOption = userOptions.find(opt => opt.id === selectedOption);
+ const selectedUserOption = userOptions.find(
+ (opt) => opt.id === selectedOption
+ );
if (!selectedUserOption?.idpId) return;
setLoading(true);
@@ -675,214 +686,284 @@ export default function Page() {
>
)}
- {selectedOption && selectedOption !== "internal" && dataLoaded && (
-
-
-
- {t("userSettings")}
-
-
- {t("userSettingsDescription")}
-
-
-
-
- {/* Google/Azure Form */}
- {(() => {
- const selectedUserOption = userOptions.find(opt => opt.id === selectedOption);
- return selectedUserOption?.variant === "google" || selectedUserOption?.variant === "azure";
- })() && (
-
- )}
-
- {/* Generic OIDC Form */}
- {(() => {
- const selectedUserOption = userOptions.find(opt => opt.id === selectedOption);
- return selectedUserOption?.variant !== "google" && selectedUserOption?.variant !== "azure";
- })() && (
-