Compare commits

..

1 Commits

Author SHA1 Message Date
dependabot[bot] 30dc7e1a2f Bump the npm-dependencies group across 1 directory with 17 updates
Bumps the npm-dependencies group with 17 updates in the / directory:

| Package | From | To |
| --- | --- | --- |
| [@aws-sdk/client-s3](https://github.com/aws/aws-sdk-js-v3/tree/HEAD/clients/client-s3) | `3.1131.0` | `3.1135.0` |
| [@simplewebauthn/server](https://github.com/MasterKale/SimpleWebAuthn/tree/HEAD/packages/server) | `14.0.1` | `14.0.2` |
| [@tanstack/react-query](https://github.com/TanStack/query/tree/HEAD/packages/react-query) | `5.102.8` | `5.103.1` |
| [@tanstack/react-table](https://github.com/TanStack/table/tree/HEAD/packages/react-table) | `8.21.3` | `9.2.4` |
| [js-yaml](https://github.com/nodeca/js-yaml) | `5.4.1` | `5.4.2` |
| [lucide-react](https://github.com/lucide-icons/lucide/tree/HEAD/packages/lucide-react) | `1.45.0` | `1.47.0` |
| [moment](https://github.com/moment/moment) | `2.30.1` | `2.31.0` |
| [next-intl](https://github.com/amannn/next-intl) | `4.14.4` | `4.14.5` |
| [nodemailer](https://github.com/nodemailer/nodemailer) | `10.0.9` | `10.0.10` |
| [posthog-node](https://github.com/PostHog/posthog-js/tree/HEAD/packages/node) | `5.52.1` | `5.52.4` |
| [tailwind-merge](https://github.com/dcastil/tailwind-merge/tree/HEAD/packages/tailwind-merge) | `3.6.0` | `3.7.0` |
| [zod](https://github.com/colinhacks/zod) | `4.6.2` | `4.6.5` |
| [@dotenvx/dotenvx](https://github.com/dotenvx/dotenvx) | `2.24.1` | `2.28.0` |
| [@tanstack/react-query-devtools](https://github.com/TanStack/query/tree/HEAD/packages/react-query-devtools) | `5.102.8` | `5.103.1` |
| [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node) | `26.5.1` | `26.6.1` |
| [@types/nodemailer](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/nodemailer) | `8.0.1` | `8.0.2` |
| [prettier](https://github.com/prettier/prettier) | `3.9.6` | `3.9.8` |



Updates `@aws-sdk/client-s3` from 3.1131.0 to 3.1135.0
- [Release notes](https://github.com/aws/aws-sdk-js-v3/releases)
- [Changelog](https://github.com/aws/aws-sdk-js-v3/blob/main/clients/client-s3/CHANGELOG.md)
- [Commits](https://github.com/aws/aws-sdk-js-v3/commits/v3.1135.0/clients/client-s3)

Updates `@simplewebauthn/server` from 14.0.1 to 14.0.2
- [Release notes](https://github.com/MasterKale/SimpleWebAuthn/releases)
- [Changelog](https://github.com/MasterKale/SimpleWebAuthn/blob/master/CHANGELOG.md)
- [Commits](https://github.com/MasterKale/SimpleWebAuthn/commits/v14.0.2/packages/server)

Updates `@tanstack/react-query` from 5.102.8 to 5.103.1
- [Release notes](https://github.com/TanStack/query/releases)
- [Changelog](https://github.com/TanStack/query/blob/main/packages/react-query/CHANGELOG.md)
- [Commits](https://github.com/TanStack/query/commits/@tanstack/react-query@5.103.1/packages/react-query)

Updates `@tanstack/react-table` from 8.21.3 to 9.2.4
- [Release notes](https://github.com/TanStack/table/releases)
- [Changelog](https://github.com/TanStack/table/blob/main/packages/react-table/CHANGELOG.md)
- [Commits](https://github.com/TanStack/table/commits/@tanstack/react-table@9.2.4/packages/react-table)

Updates `js-yaml` from 5.4.1 to 5.4.2
- [Changelog](https://github.com/nodeca/js-yaml/blob/master/CHANGELOG.md)
- [Commits](https://github.com/nodeca/js-yaml/compare/5.4.1...5.4.2)

Updates `lucide-react` from 1.45.0 to 1.47.0
- [Release notes](https://github.com/lucide-icons/lucide/releases)
- [Commits](https://github.com/lucide-icons/lucide/commits/1.47.0/packages/lucide-react)

Updates `moment` from 2.30.1 to 2.31.0
- [Release notes](https://github.com/moment/moment/releases)
- [Changelog](https://github.com/moment/moment/blob/develop/CHANGELOG.md)
- [Commits](https://github.com/moment/moment/compare/2.30.1...2.31.0)

Updates `next-intl` from 4.14.4 to 4.14.5
- [Release notes](https://github.com/amannn/next-intl/releases)
- [Changelog](https://github.com/amannn/next-intl/blob/main/CHANGELOG.md)
- [Commits](https://github.com/amannn/next-intl/compare/v4.14.4...v4.14.5)

Updates `nodemailer` from 10.0.9 to 10.0.10
- [Release notes](https://github.com/nodemailer/nodemailer/releases)
- [Changelog](https://github.com/nodemailer/nodemailer/blob/master/CHANGELOG.md)
- [Commits](https://github.com/nodemailer/nodemailer/compare/v10.0.9...v10.0.10)

Updates `posthog-node` from 5.52.1 to 5.52.4
- [Release notes](https://github.com/PostHog/posthog-js/releases)
- [Changelog](https://github.com/PostHog/posthog-js/blob/main/packages/node/CHANGELOG.md)
- [Commits](https://github.com/PostHog/posthog-js/commits/posthog-node@5.52.4/packages/node)

Updates `tailwind-merge` from 3.6.0 to 3.7.0
- [Release notes](https://github.com/dcastil/tailwind-merge/releases)
- [Commits](https://github.com/dcastil/tailwind-merge/commits/tailwind-merge@3.7.0/packages/tailwind-merge)

Updates `zod` from 4.6.2 to 4.6.5
- [Release notes](https://github.com/colinhacks/zod/releases)
- [Commits](https://github.com/colinhacks/zod/compare/v4.6.2...v4.6.5)

Updates `@dotenvx/dotenvx` from 2.24.1 to 2.28.0
- [Release notes](https://github.com/dotenvx/dotenvx/releases)
- [Changelog](https://github.com/dotenvx/dotenvx/blob/main/CHANGELOG.md)
- [Commits](https://github.com/dotenvx/dotenvx/compare/v2.24.1...v2.28.0)

Updates `@tanstack/react-query-devtools` from 5.102.8 to 5.103.1
- [Release notes](https://github.com/TanStack/query/releases)
- [Changelog](https://github.com/TanStack/query/blob/main/packages/react-query-devtools/CHANGELOG.md)
- [Commits](https://github.com/TanStack/query/commits/@tanstack/react-query-devtools@5.103.1/packages/react-query-devtools)

Updates `@types/node` from 26.5.1 to 26.6.1
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/node)

Updates `@types/nodemailer` from 8.0.1 to 8.0.2
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/nodemailer)

Updates `prettier` from 3.9.6 to 3.9.8
- [Release notes](https://github.com/prettier/prettier/releases)
- [Changelog](https://github.com/prettier/prettier/blob/main/CHANGELOG.md)
- [Commits](https://github.com/prettier/prettier/compare/3.9.6...3.9.8)

---
updated-dependencies:
- dependency-name: "@aws-sdk/client-s3"
  dependency-version: 3.1135.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: npm-dependencies
- dependency-name: "@simplewebauthn/server"
  dependency-version: 14.0.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: npm-dependencies
- dependency-name: "@tanstack/react-query"
  dependency-version: 5.103.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: npm-dependencies
- dependency-name: "@tanstack/react-table"
  dependency-version: 9.2.4
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: npm-dependencies
- dependency-name: js-yaml
  dependency-version: 5.4.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: npm-dependencies
- dependency-name: lucide-react
  dependency-version: 1.47.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: npm-dependencies
- dependency-name: moment
  dependency-version: 2.31.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: npm-dependencies
- dependency-name: next-intl
  dependency-version: 4.14.5
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: npm-dependencies
- dependency-name: nodemailer
  dependency-version: 10.0.10
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: npm-dependencies
- dependency-name: posthog-node
  dependency-version: 5.52.4
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: npm-dependencies
- dependency-name: tailwind-merge
  dependency-version: 3.7.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: npm-dependencies
- dependency-name: zod
  dependency-version: 4.6.5
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: npm-dependencies
- dependency-name: "@dotenvx/dotenvx"
  dependency-version: 2.28.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: npm-dependencies
- dependency-name: "@tanstack/react-query-devtools"
  dependency-version: 5.103.1
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: npm-dependencies
- dependency-name: "@types/node"
  dependency-version: 26.6.1
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: npm-dependencies
- dependency-name: "@types/nodemailer"
  dependency-version: 8.0.2
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: npm-dependencies
- dependency-name: prettier
  dependency-version: 3.9.8
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: npm-dependencies
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-09-21 01:39:05 +00:00
21 changed files with 774 additions and 654 deletions
+2 -2
View File
@@ -2,8 +2,8 @@
import yargs from "yargs"; import yargs from "yargs";
import { hideBin } from "yargs/helpers"; import { hideBin } from "yargs/helpers";
import { setAdminCredentials } from "./commands/setAdminCredentials"; import { setAdminCredentials } from "@cli/commands/setAdminCredentials";
import { resetUserSecurityKeys } from "./commands/resetUserSecurityKeys"; import { resetUserSecurityKeys } from "@cli/commands/resetUserSecurityKeys";
import { clearExitNodes } from "./commands/clearExitNodes"; import { clearExitNodes } from "./commands/clearExitNodes";
import { rotateServerSecret } from "./commands/rotateServerSecret"; import { rotateServerSecret } from "./commands/rotateServerSecret";
import { clearLicenseKeys } from "./commands/clearLicenseKeys"; import { clearLicenseKeys } from "./commands/clearLicenseKeys";
+1 -1
View File
@@ -1,7 +1,7 @@
name: pangolin name: pangolin
services: services:
pangolin: pangolin:
image: docker.io/fosrl/pangolin:ee-postgresql-latest image: docker.io/fosrl/pangolin:ee-latest
container_name: pangolin container_name: pangolin
restart: unless-stopped restart: unless-stopped
volumes: volumes:
+1 -1
View File
@@ -1,7 +1,7 @@
name: pangolin name: pangolin
services: services:
pangolin: pangolin:
image: docker.io/fosrl/pangolin:ee-postgresql-latest image: docker.io/fosrl/pangolin:ee-latest
container_name: pangolin container_name: pangolin
restart: unless-stopped restart: unless-stopped
volumes: volumes:
+4 -4
View File
@@ -459,13 +459,13 @@
"searchApiKeys": "API sleutels zoeken...", "searchApiKeys": "API sleutels zoeken...",
"apiKeysAdd": "API sleutel genereren", "apiKeysAdd": "API sleutel genereren",
"apiKeysErrorDelete": "Fout bij verwijderen API sleutel", "apiKeysErrorDelete": "Fout bij verwijderen API sleutel",
"apiKeysErrorDeleteMessage": "Fout bij verwijderen API- leutel", "apiKeysErrorDeleteMessage": "Fout bij verwijderen API sleutel",
"apiKeysQuestionRemove": "Weet u zeker dat u de API sleutel van de organisatie wilt verwijderen?", "apiKeysQuestionRemove": "Weet u zeker dat u de API sleutel van de organisatie wilt verwijderen?",
"apiKeysMessageRemove": "Eenmaal verwijderd, kan de APIsleutel niet meer worden gebruikt.", "apiKeysMessageRemove": "Eenmaal verwijderd, kan de API sleutel niet meer worden gebruikt.",
"apiKeysDeleteConfirm": "Bevestig verwijderen API sleutel", "apiKeysDeleteConfirm": "Bevestig Verwijderen API sleutel",
"apiKeysDelete": "API sleutel verwijderen", "apiKeysDelete": "API sleutel verwijderen",
"apiKeysManage": "API sleutels beheren", "apiKeysManage": "API sleutels beheren",
"apiKeysDescription": "API sleutels worden gebruikt om te verifiëren met de integratie-API", "apiKeysDescription": "API sleutels worden gebruikt om toegang te verifiëren met de integratie API ",
"orgsManage": "Organisaties Beheren", "orgsManage": "Organisaties Beheren",
"orgsDescription": "Bekijk en beheer alle organisaties op dit systeem", "orgsDescription": "Bekijk en beheer alle organisaties op dit systeem",
"provisioningKeysTitle": "Vertrekkende sleutel", "provisioningKeysTitle": "Vertrekkende sleutel",
+697 -96
View File
File diff suppressed because it is too large Load Diff
+17 -17
View File
@@ -33,7 +33,7 @@
}, },
"dependencies": { "dependencies": {
"@asteasolutions/zod-to-openapi": "9.1.0", "@asteasolutions/zod-to-openapi": "9.1.0",
"@aws-sdk/client-s3": "3.1131.0", "@aws-sdk/client-s3": "3.1135.0",
"@devolutions/iron-remote-desktop": "https://static.pangolin.net/packages/devolutions-iron-remote-desktop-0.0.0.tgz", "@devolutions/iron-remote-desktop": "https://static.pangolin.net/packages/devolutions-iron-remote-desktop-0.0.0.tgz",
"@devolutions/iron-remote-desktop-rdp": "https://static.pangolin.net/packages/devolutions-iron-remote-desktop-rdp-0.0.1.tgz", "@devolutions/iron-remote-desktop-rdp": "https://static.pangolin.net/packages/devolutions-iron-remote-desktop-rdp-0.0.1.tgz",
"@headlessui/react": "2.2.10", "@headlessui/react": "2.2.10",
@@ -66,10 +66,10 @@
"@react-email/render": "2.1.0", "@react-email/render": "2.1.0",
"@react-email/tailwind": "2.0.7", "@react-email/tailwind": "2.0.7",
"@simplewebauthn/browser": "14.0.0", "@simplewebauthn/browser": "14.0.0",
"@simplewebauthn/server": "14.0.1", "@simplewebauthn/server": "14.0.2",
"@tailwindcss/forms": "0.5.11", "@tailwindcss/forms": "0.5.11",
"@tanstack/react-query": "5.102.8", "@tanstack/react-query": "5.103.1",
"@tanstack/react-table": "8.21.3", "@tanstack/react-table": "9.2.4",
"@xterm/addon-fit": "^0.11.0", "@xterm/addon-fit": "^0.11.0",
"@xterm/addon-web-links": "^0.12.0", "@xterm/addon-web-links": "^0.12.0",
"@xterm/xterm": "^6.0.0", "@xterm/xterm": "^6.0.0",
@@ -96,20 +96,20 @@
"input-otp": "1.5.0", "input-otp": "1.5.0",
"ioredis": "6.0.0", "ioredis": "6.0.0",
"jmespath": "0.16.0", "jmespath": "0.16.0",
"js-yaml": "5.4.1", "js-yaml": "5.4.2",
"jsonwebtoken": "9.0.3", "jsonwebtoken": "9.0.3",
"lru-cache": "11.5.2", "lru-cache": "11.5.2",
"lucide-react": "1.45.0", "lucide-react": "1.47.0",
"maxmind": "5.0.7", "maxmind": "5.0.7",
"moment": "2.30.1", "moment": "2.31.0",
"next": "16.3.5", "next": "16.3.5",
"next-intl": "4.14.4", "next-intl": "4.14.5",
"next-themes": "0.4.6", "next-themes": "0.4.6",
"nextjs-toploader": "3.9.17", "nextjs-toploader": "3.9.17",
"nodemailer": "10.0.9", "nodemailer": "10.0.10",
"oslo": "1.2.1", "oslo": "1.2.1",
"pg": "8.23.0", "pg": "8.23.0",
"posthog-node": "5.52.1", "posthog-node": "5.52.4",
"qrcode.react": "4.2.0", "qrcode.react": "4.2.0",
"react": "19.3.0", "react": "19.3.0",
"react-day-picker": "10.0.1", "react-day-picker": "10.0.1",
@@ -123,7 +123,7 @@
"sshpk": "1.18.0", "sshpk": "1.18.0",
"stripe": "22.6.2", "stripe": "22.6.2",
"swagger-ui-express": "5.0.1", "swagger-ui-express": "5.0.1",
"tailwind-merge": "3.6.0", "tailwind-merge": "3.7.0",
"topojson-client": "3.1.0", "topojson-client": "3.1.0",
"tw-animate-css": "1.4.0", "tw-animate-css": "1.4.0",
"use-debounce": "10.1.1", "use-debounce": "10.1.1",
@@ -135,15 +135,15 @@
"ws": "8.21.3", "ws": "8.21.3",
"yaml": "2.9.1", "yaml": "2.9.1",
"yargs": "18.1.0", "yargs": "18.1.0",
"zod": "4.6.2", "zod": "4.6.5",
"zod-validation-error": "5.0.0" "zod-validation-error": "5.0.0"
}, },
"devDependencies": { "devDependencies": {
"@dotenvx/dotenvx": "2.24.1", "@dotenvx/dotenvx": "2.28.0",
"@esbuild-plugins/tsconfig-paths": "0.1.2", "@esbuild-plugins/tsconfig-paths": "0.1.2",
"@react-email/ui": "^6.9.5", "@react-email/ui": "^6.9.5",
"@tailwindcss/postcss": "4.3.3", "@tailwindcss/postcss": "4.3.3",
"@tanstack/react-query-devtools": "5.102.8", "@tanstack/react-query-devtools": "5.103.1",
"@types/better-sqlite3": "7.6.13", "@types/better-sqlite3": "7.6.13",
"@types/cookie-parser": "1.4.10", "@types/cookie-parser": "1.4.10",
"@types/cors": "2.8.19", "@types/cors": "2.8.19",
@@ -155,8 +155,8 @@
"@types/jmespath": "0.15.2", "@types/jmespath": "0.15.2",
"@types/js-yaml": "4.0.9", "@types/js-yaml": "4.0.9",
"@types/jsonwebtoken": "9.0.10", "@types/jsonwebtoken": "9.0.10",
"@types/node": "26.5.1", "@types/node": "26.6.1",
"@types/nodemailer": "8.0.1", "@types/nodemailer": "8.0.2",
"@types/nprogress": "0.2.3", "@types/nprogress": "0.2.3",
"@types/pg": "8.23.1", "@types/pg": "8.23.1",
"@types/react": "19.3.0", "@types/react": "19.3.0",
@@ -174,7 +174,7 @@
"eslint": "10.10.0", "eslint": "10.10.0",
"eslint-config-next": "16.3.5", "eslint-config-next": "16.3.5",
"postcss": "8.5.28", "postcss": "8.5.28",
"prettier": "3.9.6", "prettier": "3.9.8",
"react-email": "6.9.5", "react-email": "6.9.5",
"tailwindcss": "4.3.3", "tailwindcss": "4.3.3",
"tsc-alias": "1.9.5", "tsc-alias": "1.9.5",
+5 -42
View File
@@ -1,4 +1,4 @@
import { formatBackupFileName, formatBackupTimestamp } from "./backupFileName"; import { formatBackupTimestamp } from "./backupFileName";
import { assertEquals } from "@test/assert"; import { assertEquals } from "@test/assert";
// Local-time constructors are used throughout, matching formatBackupTimestamp, // Local-time constructors are used throughout, matching formatBackupTimestamp,
@@ -29,9 +29,7 @@ function testMonthIsOneIndexed() {
} }
{ {
const result = formatBackupTimestamp( const result = formatBackupTimestamp(new Date(2026, 11, 31, 23, 59, 59));
new Date(2026, 11, 31, 23, 59, 59)
);
assertEquals( assertEquals(
result, result,
"2026-12-31_23-59-59", "2026-12-31_23-59-59",
@@ -75,7 +73,9 @@ function testNamesSortChronologically() {
new Date(2026, 11, 31, 23, 59, 59) new Date(2026, 11, 31, 23, 59, 59)
]; ];
const sorted = taken.map((date) => formatBackupTimestamp(date)).sort(); const sorted = taken
.map((date) => formatBackupTimestamp(date))
.sort();
assertEquals( assertEquals(
sorted.join(","), sorted.join(","),
@@ -89,48 +89,11 @@ function testNamesSortChronologically() {
); );
} }
function testFormatBackupFileName() {
console.log("Running backup file name formatting tests...");
const date = new Date(2026, 8, 12, 20, 35, 56);
// With semver version string without leading 'v'
assertEquals(
formatBackupFileName("1.22.0", date),
"db_2026-09-12_20-35-56_v1.22.0.sqlite",
"Filename must include timestamp and prefixed version tag"
);
// With version string already containing 'v'
assertEquals(
formatBackupFileName("v1.22.0", date),
"db_2026-09-12_20-35-56_v1.22.0.sqlite",
"Filename must not duplicate 'v' prefix if already present"
);
// Without version (fallback/default)
assertEquals(
formatBackupFileName(undefined, date),
"db_2026-09-12_20-35-56.sqlite",
"Filename without version must match default timestamped format"
);
// Distinct versions within the exact same second do not collide
const sameSecondFile1 = formatBackupFileName("1.21.0", date);
const sameSecondFile2 = formatBackupFileName("1.22.0", date);
if (sameSecondFile1 === sameSecondFile2) {
throw new Error(
"Backup file names for different versions in the same second must not collide"
);
}
}
// Run all tests // Run all tests
try { try {
testMonthIsOneIndexed(); testMonthIsOneIndexed();
testEveryFieldIsZeroPadded(); testEveryFieldIsZeroPadded();
testNamesSortChronologically(); testNamesSortChronologically();
testFormatBackupFileName();
console.log("All tests passed successfully!"); console.log("All tests passed successfully!");
} catch (error) { } catch (error) {
console.error("Test failed:", error); console.error("Test failed:", error);
-23
View File
@@ -26,26 +26,3 @@ export function formatBackupTimestamp(date: Date = new Date()): string {
return `${datePart}_${timePart}`; return `${datePart}_${timePart}`;
} }
/**
* Builds the full database backup file name, including timestamp and optional version tag.
*
* When a migration version is provided, the filename includes `_v<version>`,
* preventing collisions between multiple migrations running in the same second and making it easy
* to identify the migration state contained in the backup.
*
* @param version Optional migration version being run.
* @param date The moment the backup is being taken. Defaults to now.
* @returns A filename of the form `db_YYYY-MM-DD_HH-MM-SS_v<version>.sqlite` or `db_YYYY-MM-DD_HH-MM-SS.sqlite`.
*/
export function formatBackupFileName(
version?: string,
date: Date = new Date()
): string {
const timestamp = formatBackupTimestamp(date);
if (version) {
const versionTag = version.startsWith("v") ? version : `v${version}`;
return `db_${timestamp}_${versionTag}.sqlite`;
}
return `db_${timestamp}.sqlite`;
}
+1 -6
View File
@@ -20,11 +20,6 @@ function getSegmentRegex(patternPart: string): RegExp {
// resolves `.` / `..` segments, so a request like `/public%2F..%2Fadmin/` // resolves `.` / `..` segments, so a request like `/public%2F..%2Fadmin/`
// or `/public/../admin/` is matched as `/admin/`, not as a literal segment // or `/public/../admin/` is matched as `/admin/`, not as a literal segment
// or a wildcard-swallowed sequence under `/public/*`. // or a wildcard-swallowed sequence under `/public/*`.
//
// Applied to both the request path and the rule pattern: the pattern
// validator only accepts spaces / non-ASCII in percent-encoded form, so a
// rule like `/my%20docs/*` must be compared against the decoded segment
// `my docs`, not the literal text `my%20docs`.
function decodeAndResolvePath(p: string): string[] { function decodeAndResolvePath(p: string): string[] {
const rawParts = p.split("/").filter(Boolean); const rawParts = p.split("/").filter(Boolean);
@@ -53,7 +48,7 @@ function decodeAndResolvePath(p: string): string[] {
} }
export function isPathAllowed(pattern: string, path: string): boolean { export function isPathAllowed(pattern: string, path: string): boolean {
const patternParts = decodeAndResolvePath(pattern); const patternParts = pattern.split("/").filter(Boolean);
const pathParts = decodeAndResolvePath(path); const pathParts = decodeAndResolvePath(path);
function matchSegments( function matchSegments(
+8 -3
View File
@@ -70,8 +70,13 @@ export async function verifyApiKeyAccess(
); );
} }
if (!apiKey.apiKeyOrg?.orgId) { if (!apiKeyOrg.orgId) {
return next(createHttpError(HttpCode.INTERNAL_SERVER_ERROR, `API key with ID ${apiKeyId} does not have an organization ID`)); return next(
createHttpError(
HttpCode.INTERNAL_SERVER_ERROR,
`API key with ID ${apiKeyId} does not have an organization ID`
)
);
} }
if (!req.userOrg) { if (!req.userOrg) {
@@ -81,7 +86,7 @@ export async function verifyApiKeyAccess(
.where( .where(
and( and(
eq(userOrgs.userId, userId), eq(userOrgs.userId, userId),
eq(userOrgs.orgId, apiKey.apiKeyOrg.orgId) eq(userOrgs.orgId, apiKeyOrg.orgId)
) )
) )
.limit(1); .limit(1);
@@ -97,7 +97,7 @@ export async function exportConnectionAuditLogs(
const baseQuery = queryConnection(data); const baseQuery = queryConnection(data);
const log = await baseQuery.limit(MAX_EXPORT_LIMIT); const log = await baseQuery.limit(data.limit).offset(data.offset);
const csvData = generateCSV(log); const csvData = generateCSV(log);
@@ -386,38 +386,6 @@ function runSpecialCharacterTests() {
console.log("All special character tests passed!"); console.log("All special character tests passed!");
} }
function runEncodedPatternTests() {
console.log("\nRunning percent-encoded pattern tests...");
// isValidUrlGlobPattern accepts percent-encoded sequences and rejects
// raw spaces / non-ASCII, so `%20` and `%C3%A9` are the only way to write
// a PATH rule for such a path. Badger sends the request path already
// decoded (Go's req.URL.Path), and isPathAllowed decodes it again, so the
// rule pattern must be decoded the same way or it can never match.
assertEquals(
isPathAllowed("/my%20docs/*", "/my docs/report.pdf"),
true,
"Percent-encoded space in pattern should match decoded request path"
);
assertEquals(
isPathAllowed("/my%20docs/*", "/my%20docs/report.pdf"),
true,
"Percent-encoded space in pattern should match raw-encoded request path"
);
assertEquals(
isPathAllowed("/caf%C3%A9", "/café"),
true,
"Percent-encoded UTF-8 in pattern should match decoded request path"
);
assertEquals(
isPathAllowed("/my%20docs/*", "/my-docs/report.pdf"),
false,
"Decoded pattern must still reject a different path"
);
console.log("All percent-encoded pattern tests passed!");
}
function runRegionTests() { function runRegionTests() {
console.log("\nRunning isIpInRegion tests..."); console.log("\nRunning isIpInRegion tests...");
@@ -478,7 +446,6 @@ function runRegionTests() {
try { try {
runTests(); runTests();
runSpecialCharacterTests(); runSpecialCharacterTests();
runEncodedPatternTests();
runRegionTests(); runRegionTests();
console.log("\n✅ All tests passed!"); console.log("\n✅ All tests passed!");
} catch (error) { } catch (error) {
+1 -1
View File
@@ -31,7 +31,7 @@ export async function addPeer(
.where(eq(newts.siteId, siteId)) .where(eq(newts.siteId, siteId))
.limit(1); .limit(1);
if (!newt) { if (!newt) {
throw new Error(`Newt not found for site ${siteId}`); throw new Error(`Site found for site ${siteId}`);
} }
newtId = newt.newtId; newtId = newt.newtId;
} }
+1 -2
View File
@@ -500,8 +500,7 @@ async function updateHttpResource(
} }
// catch when the resource policy changes or gets cleared // catch when the resource policy changes or gets cleared
if (updateData.resourcePolicyId !== undefined && if (resource.resourcePolicyId != updateData.resourcePolicyId) {
resource.resourcePolicyId !== updateData.resourcePolicyId) {
await clearResourceSpecificSettings( await clearResourceSpecificSettings(
resource.resourceId, resource.resourceId,
resource.orgId, resource.orgId,
+1 -1
View File
@@ -263,7 +263,7 @@ export async function createSite(
const { value: newClientAddress, release } = const { value: newClientAddress, release } =
await getNextAvailableClientSubnet(orgId); await getNextAvailableClientSubnet(orgId);
releaseSubnetLock = release; releaseSubnetLock = release;
updatedAddress = `${newClientAddress.split("/")[0]}/${org.subnet ? org.subnet.split("/")[1] : "32"}`; updatedAddress = newClientAddress.split("/")[0];
} }
let newSite: Site | undefined; let newSite: Site | undefined;
+1 -1
View File
@@ -113,7 +113,7 @@ export async function updateSite(
.where( .where(
and( and(
eq(sites.niceId, updateData.niceId), eq(sites.niceId, updateData.niceId),
eq(sites.orgId, existingSite.orgId), eq(sites.orgId, sites.orgId),
ne(sites.siteId, siteId) ne(sites.siteId, siteId)
) )
) )
@@ -1,58 +0,0 @@
import { assertEquals } from "@test/assert";
import { getSiteResourceParamsSchema } from "./getSiteResource";
function testSiteResourceIdOnlyParams() {
const result = getSiteResourceParamsSchema.safeParse({
siteResourceId: "42"
});
assertEquals(
result.success,
true,
"siteResourceId-only integration routes should pass validation"
);
if (result.success) {
assertEquals(
result.data.siteResourceId,
42,
"siteResourceId should be parsed as a number"
);
assertEquals(
result.data.orgId,
undefined,
"orgId should remain optional"
);
}
}
function testOrgScopedParamsRemainSupported() {
const result = getSiteResourceParamsSchema.safeParse({
siteResourceId: "42",
orgId: "org-id"
});
assertEquals(
result.success,
true,
"org-scoped routes should continue to pass validation"
);
}
function testInvalidSiteResourceId() {
const result = getSiteResourceParamsSchema.safeParse({
siteResourceId: "not-a-number"
});
assertEquals(
result.success,
false,
"non-numeric siteResourceIds should fail validation"
);
}
testSiteResourceIdOnlyParams();
testOrgScopedParamsRemainSupported();
testInvalidSiteResourceId();
console.log("All getSiteResource parameter validation tests passed.");
@@ -10,7 +10,7 @@ import { fromError } from "zod-validation-error";
import logger from "@server/logger"; import logger from "@server/logger";
import { OpenAPITags, registry } from "@server/openApi"; import { OpenAPITags, registry } from "@server/openApi";
export const getSiteResourceParamsSchema = z.strictObject({ const getSiteResourceParamsSchema = z.strictObject({
siteResourceId: z siteResourceId: z
.string() .string()
.optional() .optional()
@@ -22,17 +22,15 @@ export const getSiteResourceParamsSchema = z.strictObject({
}); });
async function query(siteResourceId?: number, niceId?: string, orgId?: string) { async function query(siteResourceId?: number, niceId?: string, orgId?: string) {
if (siteResourceId) { if (siteResourceId && orgId) {
const [siteResource] = await db const [siteResource] = await db
.select() .select()
.from(siteResources) .from(siteResources)
.where( .where(
orgId and(
? and( eq(siteResources.siteResourceId, siteResourceId),
eq(siteResources.siteResourceId, siteResourceId), eq(siteResources.orgId, orgId)
eq(siteResources.orgId, orgId) )
)
: eq(siteResources.siteResourceId, siteResourceId)
) )
.limit(1); .limit(1);
return siteResource; return siteResource;
@@ -1,342 +0,0 @@
import { execFileSync } from "child_process";
import fs from "fs";
import os from "os";
import path from "path";
import { fileURLToPath } from "url";
import Database from "better-sqlite3";
import { assertEquals } from "@test/assert";
const here = path.dirname(fileURLToPath(import.meta.url));
const repoRoot = path.resolve(here, "..", "..");
const migrationsScript = path.join(here, "migrationsSqlite.ts");
const SEED_STATEMENTS = [
`CREATE TABLE versionMigrations (version TEXT PRIMARY KEY, executedAt INTEGER NOT NULL)`,
`INSERT INTO versionMigrations (version, executedAt) VALUES ('1.21.0', 1750000000000)`,
`CREATE TABLE sites (siteId INTEGER PRIMARY KEY AUTOINCREMENT, subnet TEXT)`,
`INSERT INTO sites (subnet) VALUES ('10.0.0.0/24')`,
`CREATE TABLE roles (roleId INTEGER PRIMARY KEY AUTOINCREMENT, orgId TEXT, isAdmin INTEGER DEFAULT 0, sshSudoMode TEXT DEFAULT 'none')`,
`INSERT INTO roles (orgId, isAdmin, sshSudoMode) VALUES ('org1', 0, 'none')`,
`CREATE TABLE licenseKey (licenseKeyId INTEGER PRIMARY KEY AUTOINCREMENT)`,
`CREATE TABLE targets (targetId INTEGER PRIMARY KEY AUTOINCREMENT, resourceId INTEGER, siteId INTEGER NOT NULL, ip TEXT NOT NULL, method TEXT, port INTEGER NOT NULL, internalPort INTEGER, enabled INTEGER DEFAULT 1, path TEXT, pathMatchType TEXT, rewritePath TEXT, rewritePathType TEXT, priority INTEGER DEFAULT 100, mode TEXT DEFAULT 'http', authToken TEXT)`,
`CREATE TABLE subscriptions (subscriptionId INTEGER PRIMARY KEY AUTOINCREMENT)`,
`CREATE TABLE clients (clientId INTEGER PRIMARY KEY AUTOINCREMENT)`,
`CREATE TABLE orgs (orgId TEXT PRIMARY KEY)`,
`INSERT INTO orgs (orgId) VALUES ('org1')`,
`CREATE TABLE siteResources (siteResourceId INTEGER PRIMARY KEY AUTOINCREMENT)`,
`CREATE TABLE eventStreamingDestinations (destinationId INTEGER PRIMARY KEY AUTOINCREMENT)`,
`CREATE TABLE roleActions (roleId INTEGER, actionId TEXT, orgId TEXT)`,
`CREATE TABLE newt (newtId INTEGER PRIMARY KEY AUTOINCREMENT)`
];
function seedDatabase(dbPath: string) {
const db = new Database(dbPath);
try {
for (const statement of SEED_STATEMENTS) {
db.exec(statement);
}
} finally {
db.close();
}
}
function tableColumns(dbPath: string, tableName: string): string[] {
const db = new Database(dbPath, { readonly: true });
try {
return (
db.prepare(`PRAGMA table_info(${tableName})`).all() as Array<{
name: unknown;
}>
).map((row) => String(row.name));
} finally {
db.close();
}
}
function executedMigrationVersions(dbPath: string): string[] {
const db = new Database(dbPath, { readonly: true });
try {
return (
db.prepare(`SELECT version FROM versionMigrations`).all() as Array<{
version: unknown;
}>
).map((row) => String(row.version));
} finally {
db.close();
}
}
function runMigrations(
workdir: string,
env: Record<string, string> = {}
): {
exitCode: number;
output: string;
} {
const tsconfig = ["tsconfig.json", "tsconfig.oss.json"]
.map((file) => path.join(repoRoot, file))
.find((file) => fs.existsSync(file));
if (!tsconfig) {
throw new Error("No tsconfig found for @server path aliases");
}
const tsxCli = path.join(
repoRoot,
"node_modules",
"tsx",
"dist",
"cli.mjs"
);
if (!fs.existsSync(tsxCli)) {
throw new Error("tsx is not installed; run npm ci first");
}
try {
const output = execFileSync(
process.execPath,
[tsxCli, "--tsconfig", tsconfig, migrationsScript],
{
cwd: workdir,
timeout: 120000,
encoding: "utf8",
env: { ...process.env, NODE_ENV: "test", ...env }
}
);
return { exitCode: 0, output };
} catch (error) {
const output =
error instanceof Error
? (error as Error & { stdout?: unknown }).stdout
: "";
return { exitCode: 1, output: String(output ?? "") };
}
}
function createTestEnvironment(): string {
for (const generated of ["server/build.ts", "server/db/index.ts"]) {
if (!fs.existsSync(path.join(repoRoot, generated))) {
throw new Error(
`Missing ${generated}; run npm run set:oss && npm run set:sqlite first`
);
}
}
const workdir = fs.mkdtempSync(
path.join(os.tmpdir(), "pangolin-backup-test-")
);
fs.mkdirSync(path.join(workdir, "config", "db"), { recursive: true });
fs.copyFileSync(
path.join(repoRoot, "config", "config.example.yml"),
path.join(workdir, "config", "config.yml")
);
const traefikSrc = path.join(repoRoot, "config", "traefik");
if (fs.existsSync(traefikSrc)) {
fs.cpSync(traefikSrc, path.join(workdir, "config", "traefik"), {
recursive: true
});
}
fs.symlinkSync(
path.join(repoRoot, "server"),
path.join(workdir, "server"),
process.platform === "win32" ? "junction" : "dir"
);
return workdir;
}
function testMultipleSequentialMigrations() {
console.log("Running multiple sequential migrations test...");
const workdir = createTestEnvironment();
try {
seedDatabase(path.join(workdir, "config", "db", "db.sqlite"));
const result = runMigrations(workdir);
assertEquals(result.exitCode, 0, "Seeded migrations must run cleanly");
if (!result.output.includes("All migrations completed successfully")) {
throw new Error(
"Seeded migrations did not complete; the backup assertions below would be vacuous"
);
}
const backupsDir = path.join(workdir, "config", "db", "backups");
const backups = fs.existsSync(backupsDir)
? fs
.readdirSync(backupsDir)
.filter((file) => file.endsWith(".sqlite"))
: [];
// Upgrading from 1.21.0 runs 1.22.0 and 1.23.0 -> produces 2 distinct backups
assertEquals(
backups.length,
2,
"Each migration must have its own distinct backup snapshot"
);
const v122Backup = backups.find((file) =>
file.includes("_v1.22.0.sqlite")
);
const v123Backup = backups.find((file) =>
file.includes("_v1.23.0.sqlite")
);
if (!v122Backup || !v123Backup) {
throw new Error(
`Expected backups for v1.22.0 and v1.23.0, found: ${backups.join(", ")}`
);
}
// Verify pre-1.22.0 snapshot state: sites has 'subnet' (not exitNodeSubnet), versions = [1.21.0]
const v122Columns = tableColumns(
path.join(backupsDir, v122Backup),
"sites"
);
assertEquals(
v122Columns.includes("subnet") &&
!v122Columns.includes("exitNodeSubnet"),
true,
"Backup before 1.22.0 must retain pre-1.22.0 schema (sites.subnet)"
);
const v122Versions = executedMigrationVersions(
path.join(backupsDir, v122Backup)
);
assertEquals(
v122Versions.includes("1.21.0") && !v122Versions.includes("1.22.0"),
true,
"Backup before 1.22.0 must only record version 1.21.0"
);
// Verify pre-1.23.0 snapshot state: sites has 'exitNodeSubnet' (1.22.0 applied), newt has no agent
const v123Columns = tableColumns(
path.join(backupsDir, v123Backup),
"sites"
);
assertEquals(
v123Columns.includes("exitNodeSubnet"),
true,
"Backup before 1.23.0 must contain successfully applied 1.22.0 schema (sites.exitNodeSubnet)"
);
const v123NewtCols = tableColumns(
path.join(backupsDir, v123Backup),
"newt"
);
assertEquals(
!v123NewtCols.includes("agent"),
true,
"Backup before 1.23.0 must not contain 1.23.0 schema changes yet"
);
const v123Versions = executedMigrationVersions(
path.join(backupsDir, v123Backup)
);
assertEquals(
v123Versions.includes("1.21.0") && v123Versions.includes("1.22.0"),
true,
"Backup before 1.23.0 must record both 1.21.0 and 1.22.0"
);
} finally {
fs.rmSync(workdir, { recursive: true, force: true });
}
}
function testFailureInLaterMigrationPreservesRestorePoints() {
console.log("Running failure in later migration test...");
const workdir = createTestEnvironment();
try {
const dbPath = path.join(workdir, "config", "db", "db.sqlite");
seedDatabase(dbPath);
// Intentionally drop table 'newt' so migration 1.23.0 fails on ALTER TABLE newt ADD COLUMN agent
const db = new Database(dbPath);
db.exec("DROP TABLE newt;");
db.close();
const result = runMigrations(workdir);
assertEquals(
result.exitCode,
1,
"Migration suite must fail when 1.23.0 errors"
);
const backupsDir = path.join(workdir, "config", "db", "backups");
const backups = fs.existsSync(backupsDir)
? fs
.readdirSync(backupsDir)
.filter((file) => file.endsWith(".sqlite"))
: [];
// Both pre-1.22.0 and pre-1.23.0 backups must exist
assertEquals(
backups.length,
2,
"Backups for earlier successful migration and the failed migration must both exist"
);
const v122Backup = backups.find((file) =>
file.includes("_v1.22.0.sqlite")
);
const v123Backup = backups.find((file) =>
file.includes("_v1.23.0.sqlite")
);
if (!v122Backup || !v123Backup) {
throw new Error(
`Expected restore points for v1.22.0 and v1.23.0, found: ${backups.join(", ")}`
);
}
// Verify pre-1.23.0 backup is a valid restore point with 1.22.0 changes applied
const v123SitesCols = tableColumns(
path.join(backupsDir, v123Backup),
"sites"
);
assertEquals(
v123SitesCols.includes("exitNodeSubnet"),
true,
"Pre-failure restore point must have 1.22.0 changes intact"
);
const v123Versions = executedMigrationVersions(
path.join(backupsDir, v123Backup)
);
assertEquals(
v123Versions.includes("1.22.0"),
true,
"Pre-failure restore point must record successful 1.22.0 migration"
);
} finally {
fs.rmSync(workdir, { recursive: true, force: true });
}
}
function testDisableBackupOnMigration() {
console.log("Running DISABLE_BACKUP_ON_MIGRATION test...");
const workdir = createTestEnvironment();
try {
seedDatabase(path.join(workdir, "config", "db", "db.sqlite"));
const result = runMigrations(workdir, {
DISABLE_BACKUP_ON_MIGRATION: "1"
});
assertEquals(
result.exitCode,
0,
"Migrations must succeed with backups disabled"
);
const backupsDir = path.join(workdir, "config", "db", "backups");
const backups = fs.existsSync(backupsDir)
? fs
.readdirSync(backupsDir)
.filter((file) => file.endsWith(".sqlite"))
: [];
assertEquals(
backups.length,
0,
"No backup files should be created when DISABLE_BACKUP_ON_MIGRATION is set"
);
} finally {
fs.rmSync(workdir, { recursive: true, force: true });
}
}
try {
testMultipleSequentialMigrations();
testFailureInLaterMigrationPreservesRestorePoints();
testDisableBackupOnMigration();
console.log("All backup migration regression tests passed successfully!");
} catch (error) {
console.error("Test failed:", error);
process.exit(1);
}
+7 -12
View File
@@ -5,7 +5,7 @@ import path from "path";
import semver from "semver"; import semver from "semver";
import { versionMigrations } from "../db/sqlite"; import { versionMigrations } from "../db/sqlite";
import { __DIRNAME, APP_PATH, APP_VERSION } from "@server/lib/consts"; import { __DIRNAME, APP_PATH, APP_VERSION } from "@server/lib/consts";
import { formatBackupFileName } from "@server/lib/backupFileName"; import { formatBackupTimestamp } from "@server/lib/backupFileName";
import { SqliteError } from "better-sqlite3"; import { SqliteError } from "better-sqlite3";
import fs from "fs"; import fs from "fs";
import { build } from "@server/build"; import { build } from "@server/build";
@@ -107,7 +107,7 @@ async function run() {
await runMigrations(); await runMigrations();
} }
function backupDb(version?: string) { function backupDb() {
// make dir config/db/backups // make dir config/db/backups
const appPath = APP_PATH; const appPath = APP_PATH;
const dbDir = path.join(appPath, "db"); const dbDir = path.join(appPath, "db");
@@ -120,10 +120,11 @@ function backupDb(version?: string) {
} }
// copy the db.sqlite file to backups // copy the db.sqlite file to backups
// add the date and migration version to the filename // add the date to the filename
const fileName = formatBackupFileName(version); const date = new Date();
const dateString = formatBackupTimestamp(date);
const dbPath = path.join(dbDir, "db.sqlite"); const dbPath = path.join(dbDir, "db.sqlite");
const backupPath = path.join(backupsDir, fileName); const backupPath = path.join(backupsDir, `db_${dateString}.sqlite`);
fs.copyFileSync(dbPath, backupPath); fs.copyFileSync(dbPath, backupPath);
} }
@@ -162,12 +163,6 @@ export async function runMigrations() {
} }
} catch (e) { } catch (e) {
console.error("Error running migrations:", e); console.error("Error running migrations:", e);
if (
process.env.NODE_ENV === "test" ||
process.env.ENVIRONMENT === "test"
) {
throw e;
}
await new Promise((resolve) => await new Promise((resolve) =>
setTimeout(resolve, 1000 * 60 * 60 * 24 * 1) setTimeout(resolve, 1000 * 60 * 60 * 24 * 1)
); );
@@ -202,7 +197,7 @@ async function executeScripts() {
try { try {
if (!process.env.DISABLE_BACKUP_ON_MIGRATION) { if (!process.env.DISABLE_BACKUP_ON_MIGRATION) {
// Backup the database before running the migration // Backup the database before running the migration
backupDb(migration.version); backupDb();
} }
await migration.run(); await migration.run();
+20
View File
@@ -10,6 +10,7 @@ import {
users users
} from "../../db/sqlite"; } from "../../db/sqlite";
import { APP_PATH, configFilePath1, configFilePath2 } from "@server/lib/consts"; import { APP_PATH, configFilePath1, configFilePath2 } from "@server/lib/consts";
import { formatBackupTimestamp } from "@server/lib/backupFileName";
import { eq, sql } from "drizzle-orm"; import { eq, sql } from "drizzle-orm";
import fs from "fs"; import fs from "fs";
import * as yaml from "js-yaml"; import * as yaml from "js-yaml";
@@ -20,6 +21,25 @@ import { fromZodError } from "zod-validation-error";
export default async function migration() { export default async function migration() {
console.log("Running setup script 1.0.0-beta.9..."); console.log("Running setup script 1.0.0-beta.9...");
// make dir config/db/backups
const appPath = APP_PATH;
const dbDir = path.join(appPath, "db");
const backupsDir = path.join(dbDir, "backups");
// check if the backups directory exists and create it if it doesn't
if (!fs.existsSync(backupsDir)) {
fs.mkdirSync(backupsDir, { recursive: true });
}
// copy the db.sqlite file to backups
// add the date to the filename
const date = new Date();
const dateString = formatBackupTimestamp(date);
const dbPath = path.join(dbDir, "db.sqlite");
const backupPath = path.join(backupsDir, `db_${dateString}.sqlite`);
fs.copyFileSync(dbPath, backupPath);
await db.transaction(async (trx) => { await db.transaction(async (trx) => {
try { try {
// Determine which config file exists // Determine which config file exists