Explorar o código

fix: routing and redirection problems

Sv443 hai 4 meses
pai
achega
2714358c5a
Modificáronse 8 ficheiros con 364 adicións e 55 borrados
  1. 2 2
      package.json
  2. 298 13
      pnpm-lock.yaml
  3. 12 0
      src/constants.ts
  4. 7 9
      src/routes/index.ts
  5. 27 24
      src/server.ts
  6. 14 5
      src/utils.ts
  7. 1 1
      test/misc.spec.ts
  8. 3 1
      www/docs/.vuepress/config.mjs

+ 2 - 2
package.json

@@ -6,7 +6,7 @@
   "type": "module",
   "scripts": {
     "dev": "nodemon -e \"ts,d.ts\" -x \"pnpm start\"",
-    "start": "tsc && node --enable-source-maps out/src/index.js",
+    "start": "tsx src/index.ts",
     "dev-www": "cd www && pnpm run dev",
     "build-www": "cd www && pnpm run build",
     "dev-all": "concurrently \"pnpm dev\" \"pnpm dev-www\"",
@@ -50,6 +50,7 @@
     "request-ip": "^3.3.0",
     "svcorelib": "^1.18.2",
     "tcp-port-used": "^1.0.2",
+    "tsx": "^4.19.2",
     "typescript": "^5.7.2"
   },
   "devDependencies": {
@@ -73,7 +74,6 @@
     "pnpm": "^9.14.2",
     "start-server-and-test": "^2.0.8",
     "ts-jest": "^29.2.5",
-    "ts-node": "^10.9.2",
     "tslib": "^2.8.1"
   }
 }

+ 298 - 13
pnpm-lock.yaml

@@ -44,6 +44,9 @@ importers:
       tcp-port-used:
         specifier: ^1.0.2
         version: 1.0.2
+      tsx:
+        specifier: ^4.19.2
+        version: 4.19.2
       typescript:
         specifier: ^5.7.2
         version: 5.7.2
@@ -108,9 +111,6 @@ importers:
       ts-jest:
         specifier: ^29.2.5
         version: 29.2.5(@babel/[email protected])(@jest/[email protected])(@jest/[email protected])([email protected](@babel/[email protected]))([email protected](@types/[email protected])([email protected](@types/[email protected])([email protected])))([email protected])
-      ts-node:
-        specifier: ^10.9.2
-        version: 10.9.2(@types/[email protected])([email protected])
       tslib:
         specifier: ^2.8.1
         version: 2.8.1
@@ -292,138 +292,282 @@ packages:
     cpu: [ppc64]
     os: [aix]
 
+  '@esbuild/[email protected]':
+    resolution: {integrity: sha512-6VhYk1diRqrhBAqpJEdjASR/+WVRtfjpqKuNw11cLiaWpAT/Uu+nokB+UJnevzy/P9C/ty6AOe0dwueMrGh/iQ==}
+    engines: {node: '>=18'}
+    cpu: [ppc64]
+    os: [aix]
+
   '@esbuild/[email protected]':
     resolution: {integrity: sha512-P0UVNGIienjZv3f5zq0DP3Nt2IE/3plFzuaS96vihvD0Hd6H/q4WXUGpCxD/E8YrSXfNyRPbpTq+T8ZQioSuPA==}
     engines: {node: '>=12'}
     cpu: [arm64]
     os: [android]
 
+  '@esbuild/[email protected]':
+    resolution: {integrity: sha512-xw50ipykXcLstLeWH7WRdQuysJqejuAGPd30vd1i5zSyKK3WE+ijzHmLKxdiCMtH1pHz78rOg0BKSYOSB/2Khw==}
+    engines: {node: '>=18'}
+    cpu: [arm64]
+    os: [android]
+
   '@esbuild/[email protected]':
     resolution: {integrity: sha512-qg/Lj1mu3CdQlDEEiWrlC4eaPZ1KztwGJ9B6J+/6G+/4ewxJg7gqj8eVYWvao1bXrqGiW2rsBZFSX3q2lcW05w==}
     engines: {node: '>=12'}
     cpu: [arm]
     os: [android]
 
+  '@esbuild/[email protected]':
+    resolution: {integrity: sha512-uz6/tEy2IFm9RYOyvKl88zdzZfwEfKZmnX9Cj1BHjeSGNuGLuMD1kR8y5bteYmwqKm1tj8m4cb/aKEorr6fHWQ==}
+    engines: {node: '>=18'}
+    cpu: [arm]
+    os: [android]
+
   '@esbuild/[email protected]':
     resolution: {integrity: sha512-3k7ZoUW6Q6YqhdhIaq/WZ7HwBpnFBlW905Fa4s4qWJyiNOgT1dOqDiVAQFwBH7gBRZr17gLrlFCRzF6jFh7Kew==}
     engines: {node: '>=12'}
     cpu: [x64]
     os: [android]
 
+  '@esbuild/[email protected]':
+    resolution: {integrity: sha512-nlN9B69St9BwUoB+jkyU090bru8L0NA3yFvAd7k8dNsVH8bi9a8cUAUSEcEEgTp2z3dbEDGJGfP6VUnkQnlReg==}
+    engines: {node: '>=18'}
+    cpu: [x64]
+    os: [android]
+
   '@esbuild/[email protected]':
     resolution: {integrity: sha512-B6IeSgZgtEzGC42jsI+YYu9Z3HKRxp8ZT3cqhvliEHovq8HSX2YX8lNocDn79gCKJXOSaEot9MVYky7AKjCs8g==}
     engines: {node: '>=12'}
     cpu: [arm64]
     os: [darwin]
 
+  '@esbuild/[email protected]':
+    resolution: {integrity: sha512-YsS2e3Wtgnw7Wq53XXBLcV6JhRsEq8hkfg91ESVadIrzr9wO6jJDMZnCQbHm1Guc5t/CdDiFSSfWP58FNuvT3Q==}
+    engines: {node: '>=18'}
+    cpu: [arm64]
+    os: [darwin]
+
   '@esbuild/[email protected]':
     resolution: {integrity: sha512-hKoVkKzFiToTgn+41qGhsUJXFlIjxI/jSYeZf3ugemDYZldIXIxhvwN6erJGlX4t5h417iFuheZ7l+YVn05N3A==}
     engines: {node: '>=12'}
     cpu: [x64]
     os: [darwin]
 
+  '@esbuild/[email protected]':
+    resolution: {integrity: sha512-aClqdgTDVPSEGgoCS8QDG37Gu8yc9lTHNAQlsztQ6ENetKEO//b8y31MMu2ZaPbn4kVsIABzVLXYLhCGekGDqw==}
+    engines: {node: '>=18'}
+    cpu: [x64]
+    os: [darwin]
+
   '@esbuild/[email protected]':
     resolution: {integrity: sha512-4aRvFIXmwAcDBw9AueDQ2YnGmz5L6obe5kmPT8Vd+/+x/JMVKCgdcRwH6APrbpNXsPz+K653Qg8HB/oXvXVukA==}
     engines: {node: '>=12'}
     cpu: [arm64]
     os: [freebsd]
 
+  '@esbuild/[email protected]':
+    resolution: {integrity: sha512-h1k6yS8/pN/NHlMl5+v4XPfikhJulk4G+tKGFIOwURBSFzE8bixw1ebjluLOjfwtLqY0kewfjLSrO6tN2MgIhA==}
+    engines: {node: '>=18'}
+    cpu: [arm64]
+    os: [freebsd]
+
   '@esbuild/[email protected]':
     resolution: {integrity: sha512-EYoXZ4d8xtBoVN7CEwWY2IN4ho76xjYXqSXMNccFSx2lgqOG/1TBPW0yPx1bJZk94qu3tX0fycJeeQsKovA8gg==}
     engines: {node: '>=12'}
     cpu: [x64]
     os: [freebsd]
 
+  '@esbuild/[email protected]':
+    resolution: {integrity: sha512-lK1eJeyk1ZX8UklqFd/3A60UuZ/6UVfGT2LuGo3Wp4/z7eRTRYY+0xOu2kpClP+vMTi9wKOfXi2vjUpO1Ro76g==}
+    engines: {node: '>=18'}
+    cpu: [x64]
+    os: [freebsd]
+
   '@esbuild/[email protected]':
     resolution: {integrity: sha512-EoTjyYyLuVPfdPLsGVVVC8a0p1BFFvtpQDB/YLEhaXyf/5bczaGeN15QkR+O4S5LeJ92Tqotve7i1jn35qwvdA==}
     engines: {node: '>=12'}
     cpu: [arm64]
     os: [linux]
 
+  '@esbuild/[email protected]':
+    resolution: {integrity: sha512-/93bf2yxencYDnItMYV/v116zff6UyTjo4EtEQjUBeGiVpMmffDNUyD9UN2zV+V3LRV3/on4xdZ26NKzn6754g==}
+    engines: {node: '>=18'}
+    cpu: [arm64]
+    os: [linux]
+
   '@esbuild/[email protected]':
     resolution: {integrity: sha512-J5jPms//KhSNv+LO1S1TX1UWp1ucM6N6XuL6ITdKWElCu8wXP72l9MM0zDTzzeikVyqFE6U8YAV9/tFyj0ti+w==}
     engines: {node: '>=12'}
     cpu: [arm]
     os: [linux]
 
+  '@esbuild/[email protected]':
+    resolution: {integrity: sha512-CXXkzgn+dXAPs3WBwE+Kvnrf4WECwBdfjfeYHpMeVxWE0EceB6vhWGShs6wi0IYEqMSIzdOF1XjQ/Mkm5d7ZdQ==}
+    engines: {node: '>=18'}
+    cpu: [arm]
+    os: [linux]
+
   '@esbuild/[email protected]':
     resolution: {integrity: sha512-Thsa42rrP1+UIGaWz47uydHSBOgTUnwBwNq59khgIwktK6x60Hivfbux9iNR0eHCHzOLjLMLfUMLCypBkZXMHA==}
     engines: {node: '>=12'}
     cpu: [ia32]
     os: [linux]
 
+  '@esbuild/[email protected]':
+    resolution: {integrity: sha512-VTN4EuOHwXEkXzX5nTvVY4s7E/Krz7COC8xkftbbKRYAl96vPiUssGkeMELQMOnLOJ8k3BY1+ZY52tttZnHcXQ==}
+    engines: {node: '>=18'}
+    cpu: [ia32]
+    os: [linux]
+
   '@esbuild/[email protected]':
     resolution: {integrity: sha512-LiXdXA0s3IqRRjm6rV6XaWATScKAXjI4R4LoDlvO7+yQqFdlr1Bax62sRwkVvRIrwXxvtYEHHI4dm50jAXkuAA==}
     engines: {node: '>=12'}
     cpu: [loong64]
     os: [linux]
 
+  '@esbuild/[email protected]':
+    resolution: {integrity: sha512-Vx09LzEoBa5zDnieH8LSMRToj7ir/Jeq0Gu6qJ/1GcBq9GkfoEAoXvLiW1U9J1qE/Y/Oyaq33w5p2ZWrNNHNEw==}
+    engines: {node: '>=18'}
+    cpu: [loong64]
+    os: [linux]
+
   '@esbuild/[email protected]':
     resolution: {integrity: sha512-fEnAuj5VGTanfJ07ff0gOA6IPsvrVHLVb6Lyd1g2/ed67oU1eFzL0r9WL7ZzscD+/N6i3dWumGE1Un4f7Amf+w==}
     engines: {node: '>=12'}
     cpu: [mips64el]
     os: [linux]
 
+  '@esbuild/[email protected]':
+    resolution: {integrity: sha512-nrFzzMQ7W4WRLNUOU5dlWAqa6yVeI0P78WKGUo7lg2HShq/yx+UYkeNSE0SSfSure0SqgnsxPvmAUu/vu0E+3Q==}
+    engines: {node: '>=18'}
+    cpu: [mips64el]
+    os: [linux]
+
   '@esbuild/[email protected]':
     resolution: {integrity: sha512-nYJA2/QPimDQOh1rKWedNOe3Gfc8PabU7HT3iXWtNUbRzXS9+vgB0Fjaqr//XNbd82mCxHzik2qotuI89cfixg==}
     engines: {node: '>=12'}
     cpu: [ppc64]
     os: [linux]
 
+  '@esbuild/[email protected]':
+    resolution: {integrity: sha512-dKN8fgVqd0vUIjxuJI6P/9SSSe/mB9rvA98CSH2sJnlZ/OCZWO1DJvxj8jvKTfYUdGfcq2dDxoKaC6bHuTlgcw==}
+    engines: {node: '>=18'}
+    cpu: [ppc64]
+    os: [linux]
+
   '@esbuild/[email protected]':
     resolution: {integrity: sha512-2MueBrlPQCw5dVJJpQdUYgeqIzDQgw3QtiAHUC4RBz9FXPrskyyU3VI1hw7C0BSKB9OduwSJ79FTCqtGMWqJHg==}
     engines: {node: '>=12'}
     cpu: [riscv64]
     os: [linux]
 
+  '@esbuild/[email protected]':
+    resolution: {integrity: sha512-5AV4Pzp80fhHL83JM6LoA6pTQVWgB1HovMBsLQ9OZWLDqVY8MVobBXNSmAJi//Csh6tcY7e7Lny2Hg1tElMjIA==}
+    engines: {node: '>=18'}
+    cpu: [riscv64]
+    os: [linux]
+
   '@esbuild/[email protected]':
     resolution: {integrity: sha512-+Pil1Nv3Umes4m3AZKqA2anfhJiVmNCYkPchwFJNEJN5QxmTs1uzyy4TvmDrCRNT2ApwSari7ZIgrPeUx4UZDg==}
     engines: {node: '>=12'}
     cpu: [s390x]
     os: [linux]
 
+  '@esbuild/[email protected]':
+    resolution: {integrity: sha512-9ygs73tuFCe6f6m/Tb+9LtYxWR4c9yg7zjt2cYkjDbDpV/xVn+68cQxMXCjUpYwEkze2RcU/rMnfIXNRFmSoDw==}
+    engines: {node: '>=18'}
+    cpu: [s390x]
+    os: [linux]
+
   '@esbuild/[email protected]':
     resolution: {integrity: sha512-B71g1QpxfwBvNrfyJdVDexenDIt1CiDN1TIXLbhOw0KhJzE78KIFGX6OJ9MrtC0oOqMWf+0xop4qEU8JrJTwCg==}
     engines: {node: '>=12'}
     cpu: [x64]
     os: [linux]
 
+  '@esbuild/[email protected]':
+    resolution: {integrity: sha512-EV6+ovTsEXCPAp58g2dD68LxoP/wK5pRvgy0J/HxPGB009omFPv3Yet0HiaqvrIrgPTBuC6wCH1LTOY91EO5hQ==}
+    engines: {node: '>=18'}
+    cpu: [x64]
+    os: [linux]
+
   '@esbuild/[email protected]':
     resolution: {integrity: sha512-3ltjQ7n1owJgFbuC61Oj++XhtzmymoCihNFgT84UAmJnxJfm4sYCiSLTXZtE00VWYpPMYc+ZQmB6xbSdVh0JWA==}
     engines: {node: '>=12'}
     cpu: [x64]
     os: [netbsd]
 
+  '@esbuild/[email protected]':
+    resolution: {integrity: sha512-aevEkCNu7KlPRpYLjwmdcuNz6bDFiE7Z8XC4CPqExjTvrHugh28QzUXVOZtiYghciKUacNktqxdpymplil1beA==}
+    engines: {node: '>=18'}
+    cpu: [x64]
+    os: [netbsd]
+
+  '@esbuild/[email protected]':
+    resolution: {integrity: sha512-3x37szhLexNA4bXhLrCC/LImN/YtWis6WXr1VESlfVtVeoFJBRINPJ3f0a/6LV8zpikqoUg4hyXw0sFBt5Cr+Q==}
+    engines: {node: '>=18'}
+    cpu: [arm64]
+    os: [openbsd]
+
   '@esbuild/[email protected]':
     resolution: {integrity: sha512-RbrfTB9SWsr0kWmb9srfF+L933uMDdu9BIzdA7os2t0TXhCRjrQyCeOt6wVxr79CKD4c+p+YhCj31HBkYcXebw==}
     engines: {node: '>=12'}
     cpu: [x64]
     os: [openbsd]
 
+  '@esbuild/[email protected]':
+    resolution: {integrity: sha512-aY2gMmKmPhxfU+0EdnN+XNtGbjfQgwZj43k8G3fyrDM/UdZww6xrWxmDkuz2eCZchqVeABjV5BpildOrUbBTqA==}
+    engines: {node: '>=18'}
+    cpu: [x64]
+    os: [openbsd]
+
   '@esbuild/[email protected]':
     resolution: {integrity: sha512-HKjJwRrW8uWtCQnQOz9qcU3mUZhTUQvi56Q8DPTLLB+DawoiQdjsYq+j+D3s9I8VFtDr+F9CjgXKKC4ss89IeA==}
     engines: {node: '>=12'}
     cpu: [x64]
     os: [sunos]
 
+  '@esbuild/[email protected]':
+    resolution: {integrity: sha512-RBRT2gqEl0IKQABT4XTj78tpk9v7ehp+mazn2HbUeZl1YMdaGAQqhapjGTCe7uw7y0frDi4gS0uHzhvpFuI1sA==}
+    engines: {node: '>=18'}
+    cpu: [x64]
+    os: [sunos]
+
   '@esbuild/[email protected]':
     resolution: {integrity: sha512-URgtR1dJnmGvX864pn1B2YUYNzjmXkuJOIqG2HdU62MVS4EHpU2946OZoTMnRUHklGtJdJZ33QfzdjGACXhn1A==}
     engines: {node: '>=12'}
     cpu: [arm64]
     os: [win32]
 
+  '@esbuild/[email protected]':
+    resolution: {integrity: sha512-4O+gPR5rEBe2FpKOVyiJ7wNDPA8nGzDuJ6gN4okSA1gEOYZ67N8JPk58tkWtdtPeLz7lBnY6I5L3jdsr3S+A6A==}
+    engines: {node: '>=18'}
+    cpu: [arm64]
+    os: [win32]
+
   '@esbuild/[email protected]':
     resolution: {integrity: sha512-+ZOE6pUkMOJfmxmBZElNOx72NKpIa/HFOMGzu8fqzQJ5kgf6aTGrcJaFsNiVMH4JKpMipyK+7k0n2UXN7a8YKQ==}
     engines: {node: '>=12'}
     cpu: [ia32]
     os: [win32]
 
+  '@esbuild/[email protected]':
+    resolution: {integrity: sha512-BcaL0Vn6QwCwre3Y717nVHZbAa4UBEigzFm6VdsVdT/MbZ38xoj1X9HPkZhbmaBGUD1W8vxAfffbDe8bA6AKnQ==}
+    engines: {node: '>=18'}
+    cpu: [ia32]
+    os: [win32]
+
   '@esbuild/[email protected]':
     resolution: {integrity: sha512-T1QyPSDCyMXaO3pzBkF96E8xMkiRYbUEZADd29SyPGabqxMViNoii+NcK7eWJAEoU6RZyEm5lVSIjTmcdoB9HA==}
     engines: {node: '>=12'}
     cpu: [x64]
     os: [win32]
 
+  '@esbuild/[email protected]':
+    resolution: {integrity: sha512-BHpFFeslkWrXWyUPnbKm+xYYVYruCinGcftSBaa8zoF9hZO4BcSCFUvHVTtzpIY6YzUnYtuEhZ+C9iEXjxnasg==}
+    engines: {node: '>=18'}
+    cpu: [x64]
+    os: [win32]
+
   '@eslint-community/[email protected]':
     resolution: {integrity: sha512-s3O3waFUrMV8P/XaF/+ZTp1X9XBZW1a4B97ZnjQF2KYWaFD2A8KyFBsrsfSjEmjn3RGWAIuvlneuZm3CUK3jbA==}
     engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
@@ -1346,6 +1490,11 @@ packages:
     engines: {node: '>=12'}
     hasBin: true
 
+  [email protected]:
+    resolution: {integrity: sha512-VVNz/9Sa0bs5SELtn3f7qhJCDPCF5oMEl5cO9/SSinpE9hbPVvxbd572HH5AKiP7WD8INO53GgfDDhRjkylHEg==}
+    engines: {node: '>=18'}
+    hasBin: true
+
   [email protected]:
     resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==}
     engines: {node: '>=6'}
@@ -1545,6 +1694,9 @@ packages:
     resolution: {integrity: sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==}
     engines: {node: '>=10'}
 
+  [email protected]:
+    resolution: {integrity: sha512-k9PN+cFBmaLWtVz29SkUoqU5O0slLuHJXt/2P+tMVFT+phsSGXGkp9t3rQIqdz0e+06EHNGs3oM6ZX1s2zHxRg==}
+
   [email protected]:
     resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==}
     engines: {node: '>= 6'}
@@ -2362,6 +2514,9 @@ packages:
     resolution: {integrity: sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==}
     engines: {node: '>=8'}
 
+  [email protected]:
+    resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==}
+
   [email protected]:
     resolution: {integrity: sha512-X2UW6Nw3n/aMgDVy+0rSqgHlv39WZAlZrXCdnbyEiKm17DSqHX4MmQMaST3FbeWR5FTuRcUwYAziZajji0Y7mg==}
     engines: {node: '>=10'}
@@ -2651,6 +2806,11 @@ packages:
   [email protected]:
     resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==}
 
+  [email protected]:
+    resolution: {integrity: sha512-pOUl6Vo2LUq/bSa8S5q7b91cgNSjctn9ugq/+Mvow99qW6x/UZYwzxy/3NmqoT66eHYfCVvFvACC58UBPFf28g==}
+    engines: {node: '>=18.0.0'}
+    hasBin: true
+
   [email protected]:
     resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==}
     engines: {node: '>= 0.8.0'}
@@ -3017,76 +3177,149 @@ snapshots:
   '@cspotcode/[email protected]':
     dependencies:
       '@jridgewell/trace-mapping': 0.3.9
+    optional: true
 
   '@esbuild/[email protected]':
     optional: true
 
+  '@esbuild/[email protected]':
+    optional: true
+
   '@esbuild/[email protected]':
     optional: true
 
+  '@esbuild/[email protected]':
+    optional: true
+
   '@esbuild/[email protected]':
     optional: true
 
+  '@esbuild/[email protected]':
+    optional: true
+
   '@esbuild/[email protected]':
     optional: true
 
+  '@esbuild/[email protected]':
+    optional: true
+
   '@esbuild/[email protected]':
     optional: true
 
+  '@esbuild/[email protected]':
+    optional: true
+
   '@esbuild/[email protected]':
     optional: true
 
+  '@esbuild/[email protected]':
+    optional: true
+
   '@esbuild/[email protected]':
     optional: true
 
+  '@esbuild/[email protected]':
+    optional: true
+
   '@esbuild/[email protected]':
     optional: true
 
+  '@esbuild/[email protected]':
+    optional: true
+
   '@esbuild/[email protected]':
     optional: true
 
+  '@esbuild/[email protected]':
+    optional: true
+
   '@esbuild/[email protected]':
     optional: true
 
+  '@esbuild/[email protected]':
+    optional: true
+
   '@esbuild/[email protected]':
     optional: true
 
+  '@esbuild/[email protected]':
+    optional: true
+
   '@esbuild/[email protected]':
     optional: true
 
+  '@esbuild/[email protected]':
+    optional: true
+
   '@esbuild/[email protected]':
     optional: true
 
+  '@esbuild/[email protected]':
+    optional: true
+
   '@esbuild/[email protected]':
     optional: true
 
+  '@esbuild/[email protected]':
+    optional: true
+
   '@esbuild/[email protected]':
     optional: true
 
+  '@esbuild/[email protected]':
+    optional: true
+
   '@esbuild/[email protected]':
     optional: true
 
+  '@esbuild/[email protected]':
+    optional: true
+
   '@esbuild/[email protected]':
     optional: true
 
+  '@esbuild/[email protected]':
+    optional: true
+
   '@esbuild/[email protected]':
     optional: true
 
+  '@esbuild/[email protected]':
+    optional: true
+
+  '@esbuild/[email protected]':
+    optional: true
+
   '@esbuild/[email protected]':
     optional: true
 
+  '@esbuild/[email protected]':
+    optional: true
+
   '@esbuild/[email protected]':
     optional: true
 
+  '@esbuild/[email protected]':
+    optional: true
+
   '@esbuild/[email protected]':
     optional: true
 
+  '@esbuild/[email protected]':
+    optional: true
+
   '@esbuild/[email protected]':
     optional: true
 
+  '@esbuild/[email protected]':
+    optional: true
+
   '@esbuild/[email protected]':
     optional: true
 
+  '@esbuild/[email protected]':
+    optional: true
+
   '@eslint-community/[email protected]([email protected])':
     dependencies:
       eslint: 8.57.1
@@ -3321,6 +3554,7 @@ snapshots:
     dependencies:
       '@jridgewell/resolve-uri': 3.1.2
       '@jridgewell/sourcemap-codec': 1.5.0
+    optional: true
 
   '@mdit-vue/[email protected]':
     dependencies:
@@ -3401,13 +3635,17 @@ snapshots:
     dependencies:
       '@sinonjs/commons': 3.0.1
 
-  '@tsconfig/[email protected]': {}
+  '@tsconfig/[email protected]':
+    optional: true
 
-  '@tsconfig/[email protected]': {}
+  '@tsconfig/[email protected]':
+    optional: true
 
-  '@tsconfig/[email protected]': {}
+  '@tsconfig/[email protected]':
+    optional: true
 
-  '@tsconfig/[email protected]': {}
+  '@tsconfig/[email protected]':
+    optional: true
 
   '@types/[email protected]':
     dependencies:
@@ -3505,7 +3743,7 @@ snapshots:
 
   '@types/[email protected]':
     dependencies:
-      '@types/markdown-it': 13.0.9
+      '@types/markdown-it': 14.1.2
 
   '@types/[email protected]':
     dependencies:
@@ -3847,6 +4085,7 @@ snapshots:
   [email protected]:
     dependencies:
       acorn: 8.14.0
+    optional: true
 
   [email protected]: {}
 
@@ -3876,7 +4115,8 @@ snapshots:
       normalize-path: 3.0.0
       picomatch: 2.3.1
 
-  [email protected]: {}
+  [email protected]:
+    optional: true
 
   [email protected]: {}
 
@@ -4170,7 +4410,8 @@ snapshots:
       - supports-color
       - ts-node
 
-  [email protected]: {}
+  [email protected]:
+    optional: true
 
   [email protected]:
     dependencies:
@@ -4228,7 +4469,8 @@ snapshots:
 
   [email protected]: {}
 
-  [email protected]: {}
+  [email protected]:
+    optional: true
 
   [email protected]:
     dependencies:
@@ -4318,6 +4560,33 @@ snapshots:
       '@esbuild/win32-ia32': 0.19.12
       '@esbuild/win32-x64': 0.19.12
 
+  [email protected]:
+    optionalDependencies:
+      '@esbuild/aix-ppc64': 0.23.1
+      '@esbuild/android-arm': 0.23.1
+      '@esbuild/android-arm64': 0.23.1
+      '@esbuild/android-x64': 0.23.1
+      '@esbuild/darwin-arm64': 0.23.1
+      '@esbuild/darwin-x64': 0.23.1
+      '@esbuild/freebsd-arm64': 0.23.1
+      '@esbuild/freebsd-x64': 0.23.1
+      '@esbuild/linux-arm': 0.23.1
+      '@esbuild/linux-arm64': 0.23.1
+      '@esbuild/linux-ia32': 0.23.1
+      '@esbuild/linux-loong64': 0.23.1
+      '@esbuild/linux-mips64el': 0.23.1
+      '@esbuild/linux-ppc64': 0.23.1
+      '@esbuild/linux-riscv64': 0.23.1
+      '@esbuild/linux-s390x': 0.23.1
+      '@esbuild/linux-x64': 0.23.1
+      '@esbuild/netbsd-x64': 0.23.1
+      '@esbuild/openbsd-arm64': 0.23.1
+      '@esbuild/openbsd-x64': 0.23.1
+      '@esbuild/sunos-x64': 0.23.1
+      '@esbuild/win32-arm64': 0.23.1
+      '@esbuild/win32-ia32': 0.23.1
+      '@esbuild/win32-x64': 0.23.1
+
   [email protected]: {}
 
   [email protected]: {}
@@ -4585,6 +4854,10 @@ snapshots:
 
   [email protected]: {}
 
+  [email protected]:
+    dependencies:
+      resolve-pkg-maps: 1.0.0
+
   [email protected]:
     dependencies:
       is-glob: 4.0.3
@@ -5538,6 +5811,8 @@ snapshots:
 
   [email protected]: {}
 
+  [email protected]: {}
+
   [email protected]: {}
 
   [email protected]:
@@ -5830,9 +6105,17 @@ snapshots:
       typescript: 5.7.2
       v8-compile-cache-lib: 3.0.1
       yn: 3.1.1
+    optional: true
 
   [email protected]: {}
 
+  [email protected]:
+    dependencies:
+      esbuild: 0.23.1
+      get-tsconfig: 4.8.1
+    optionalDependencies:
+      fsevents: 2.3.3
+
   [email protected]:
     dependencies:
       prelude-ls: 1.2.1
@@ -5879,7 +6162,8 @@ snapshots:
 
   [email protected]: {}
 
-  [email protected]: {}
+  [email protected]:
+    optional: true
 
   [email protected]:
     dependencies:
@@ -5973,6 +6257,7 @@ snapshots:
       y18n: 5.0.8
       yargs-parser: 21.1.1
 
-  [email protected]: {}
+  [email protected]:
+    optional: true
 
   [email protected]: {}

+ 12 - 0
src/constants.ts

@@ -1,5 +1,12 @@
 import type { IRateLimiterOptions } from "rate-limiter-flexible";
+import packageJson from "../package.json" with { type: "json" };
 
+/** The version from package.json, split into a tuple of major, minor, and patch number */
+export const splitVersion = packageJson.version.split(".").map(v => Number(v)) as [major: number, minor: number, patch: number];
+
+export const [verMajor, verMinor, verPatch] = splitVersion;
+
+/** Options for the rate limiter */
 export const rateLimitOptions: IRateLimiterOptions = {
   points: 20,
   duration: 30,
@@ -16,3 +23,8 @@ export const charReplacements = new Map<string, string>([
   ["—─ ", "-"],
   ["     ", " "],
 ]);
+
+/** Any requests to paths starting with one of these will not be subject to rate limiting */
+export const rlIgnorePaths = [
+  "/docs",
+];

+ 7 - 9
src/routes/index.ts

@@ -1,11 +1,10 @@
 import { resolve } from "node:path";
 import express, { Application, Router } from "express";
-import packageJson from "../../package.json";
+import { verMajor } from "../constants.js";
 
 import { initSearchRoutes } from "./search.js";
 import { initTranslationsRoutes } from "./translations.js";
 import { initAlbumRoutes } from "./album.js";
-import { fileURLToPath } from "node:url";
 
 const routeFuncs: ((router: Router) => unknown)[] = [
   initSearchRoutes,
@@ -19,13 +18,12 @@ export function initRouter(app: Application) {
   for(const initRoute of routeFuncs)
     initRoute(router);
 
-  // redirect to GitHub page
-  router.get("/", (_req, res) => res.redirect(packageJson.homepage));
+  // host docs files
+  router.use("/docs", express.static(resolve("./www/docs/.vuepress/dist")));
 
-  // redirect to docs page
-  router.get("/docs", (_req, res) => res.redirect("/docs/"));
+  // mount router
+  app.use(`/v${verMajor}`, router);
 
-  // host docs files
-  router.use("/docs", express.static(resolve(fileURLToPath(import.meta.url), "../../www/docs/.vuepress/dist")));
-  app.use("/", router);
+  // redirect to docs page
+  app.get("/docs", (_req, res) => res.redirect(`/v${verMajor}/docs/`));
 }

+ 27 - 24
src/server.ts

@@ -11,7 +11,7 @@ import packageJson from "../package.json" with { type: "json" };
 import { error } from "./error.js";
 import { initRouter } from "./routes/index.js";
 import { hashStr, respond } from "./utils.js";
-import { rateLimitOptions } from "./constants.js";
+import { rateLimitOptions, rlIgnorePaths } from "./constants.js";
 
 const { env } = process;
 const app = express();
@@ -67,41 +67,36 @@ export async function init() {
     if(authHeader && authTokens.has(authHeader))
       return next();
 
-    const setRateLimitHeaders = (rateLimiterRes: RateLimiterRes) => {
-      if(rateLimiterRes.remainingPoints === 0)
-        res.setHeader("Retry-After", Math.ceil(rateLimiterRes.msBeforeNext / 1000));
-      res.setHeader("X-RateLimit-Limit", rateLimiter.points);
-      res.setHeader("X-RateLimit-Remaining", rateLimiterRes.remainingPoints);
-      res.setHeader("X-RateLimit-Reset", new Date(Date.now() + rateLimiterRes.msBeforeNext).toISOString());
-    };
-
     const ipHash = await hashStr(getClientIp(req) ?? "IP_RESOLUTION_ERROR");
 
-    rateLimiter.consume(ipHash)
-      .then((rateLimiterRes: RateLimiterRes) => {
-        setRateLimitHeaders(rateLimiterRes);
-        return next();
-      })
-      .catch((err) => {
-        if(err instanceof RateLimiterRes) {
-          setRateLimitHeaders(err);
-          return respond(res, 429, { error: true, matches: null, message: "You are being rate limited. Refer to the Retry-After header for when to try again." }, fmt);
-        }
-        else return respond(res, 500, { message: "Encountered an internal error. Please try again a little later." }, fmt);
-      });
+    if(rlIgnorePaths.every((path) => !req.path.match(new RegExp(`^(/?v\\d+)?${path}`)))) {
+      rateLimiter.consume(ipHash)
+        .then((rateLimiterRes: RateLimiterRes) => {
+          setRateLimitHeaders(res, rateLimiterRes);
+          return next();
+        })
+        .catch((err) => {
+          if(err instanceof RateLimiterRes) {
+            setRateLimitHeaders(res, err);
+            return respond(res, 429, { error: true, matches: null, message: "You are being rate limited. Refer to the Retry-After header for when to try again." }, fmt);
+          }
+          else return respond(res, 500, { message: "Encountered an internal error. Please try again a little later." }, fmt);
+        });
+    }
+    else
+      return next();
   });
 
   const listener = app.listen(port, host, () => {
     registerRoutes();
 
-    console.log(k.green(`Listening on ${host}:${port}`));
+    console.log(k.green(`Listening on ${host}:${port}\n`));
   });
 
   listener.on("error", (err) => error("General server error", err, true));
 }
 
-function registerRoutes()
-{
+function registerRoutes() {
   try {
     initRouter(app);
   }
@@ -121,3 +116,11 @@ function getAuthTokens() {
 
   return new Set<string>(tokens);
 }
+
+function setRateLimitHeaders (res: Response, rateLimiterRes: RateLimiterRes) {
+  if(rateLimiterRes.remainingPoints === 0)
+    res.setHeader("Retry-After", Math.ceil(rateLimiterRes.msBeforeNext / 1000));
+  res.setHeader("X-RateLimit-Limit", rateLimiter.points);
+  res.setHeader("X-RateLimit-Remaining", rateLimiterRes.remainingPoints);
+  res.setHeader("X-RateLimit-Reset", new Date(Date.now() + rateLimiterRes.msBeforeNext).toISOString());
+}

+ 14 - 5
src/utils.ts

@@ -1,7 +1,7 @@
-import { createHash } from "node:crypto";
+import { createHash, type BinaryToTextEncoding } from "node:crypto";
 import { Response } from "express";
-import { Stringifiable, byteLength } from "svcorelib";
 import { parse as jsonToXml } from "js2xmlparser";
+import type { Stringifiable } from "svcorelib";
 import { ResponseType } from "./types.js";
 
 /** Checks if the value of a passed URL parameter is a string with length > 0 */
@@ -65,22 +65,31 @@ export function respond(res: Response, type: ResponseType | number, data: String
   };
 
   const finalData = format === "xml" ? jsonToXml("data", resData) : resData;
-  const contentLen = byteLength(typeof finalData === "string" ? finalData : JSON.stringify(finalData));
+  const contentLen = getByteLength(typeof finalData === "string" ? finalData : JSON.stringify(finalData));
 
   res.setHeader("Content-Type", format === "xml" ? "application/xml" : "application/json");
   contentLen > -1 && res.setHeader("Content-Length", contentLen);
   res.status(statusCode).send(finalData);
 }
 
-export function hashStr(str: string): Promise<string> {
+/** Hashes a string using SHA-512, encoded as "hex" by default */
+export function hashStr(str: string, encoding: BinaryToTextEncoding = "hex"): Promise<string> {
   return new Promise((resolve, reject) => {
     try {
       const hash = createHash("sha512");
       hash.update(str);
-      resolve(hash.digest("hex"));
+      resolve(hash.digest(encoding));
     }
     catch(e) {
       reject(e);
     }
   });
 }
+
+/** Returns the length of the given data - returns -1 if the data couldn't be stringified */
+export function getByteLength(data: string | Record<string, unknown>) {
+  if(typeof data === "string" || "toString" in data)
+    return Buffer.byteLength(String(data), "utf8");
+
+  return -1;
+}

+ 1 - 1
test/misc.spec.ts

@@ -10,6 +10,6 @@ describe("Misc", () => {
     });
 
     expect(res.status).toBe(200);
-    expect(res.headers.get("api-info")).toBeDefined();
+    expect(res.headers.get("api-info")).not.toBeNull();
   });
 });

+ 3 - 1
www/docs/.vuepress/config.mjs

@@ -5,9 +5,11 @@ import { seoPlugin } from "@vuepress/plugin-seo";
 import { sitemapPlugin } from "@vuepress/plugin-sitemap";
 import rootPkgJson from "../../../package.json";
 
+const verMajor = Number(rootPkgJson.version.split(".")[0]);
+
 export default defineUserConfig({
   lang: "en-US",
-  base: "/v2/docs/",
+  base: `/v${verMajor}/docs/`,
   title: "geniURL",
   description: "A simple JSON and XML REST API to search for song metadata, the lyrics URL and lyrics translations on genius.com",
   theme: defaultTheme({