Initial commit: Upload Checkpoint project
This commit is contained in:
commit
c0e3781244
32 changed files with 6121 additions and 0 deletions
16
.cursor/rules/coding-practices.mdc
Normal file
16
.cursor/rules/coding-practices.mdc
Normal file
|
|
@ -0,0 +1,16 @@
|
||||||
|
---
|
||||||
|
description:
|
||||||
|
globs:
|
||||||
|
alwaysApply: true
|
||||||
|
---
|
||||||
|
# Code Quality Standards
|
||||||
|
|
||||||
|
- Follow established industry best practices for each programming language
|
||||||
|
- Maintain consistent code style throughout the project
|
||||||
|
- Write self-documenting code with descriptive variable and function names
|
||||||
|
- Keep functions small and focused on a single responsibility
|
||||||
|
- Add appropriate error handling where necessary
|
||||||
|
- Include comments only for complex logic that requires additional context
|
||||||
|
- Write DRY (Don't Repeat Yourself) code that minimizes duplication
|
||||||
|
- Consider performance implications of implemented solutions
|
||||||
|
|
||||||
5
.cursor/rules/comments.mdc
Normal file
5
.cursor/rules/comments.mdc
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
---
|
||||||
|
description:
|
||||||
|
globs:
|
||||||
|
alwaysApply: false
|
||||||
|
---
|
||||||
13
.cursor/rules/conversation-style.mdc
Normal file
13
.cursor/rules/conversation-style.mdc
Normal file
|
|
@ -0,0 +1,13 @@
|
||||||
|
---
|
||||||
|
description:
|
||||||
|
globs:
|
||||||
|
alwaysApply: true
|
||||||
|
---
|
||||||
|
# Conversation Style Guidelines
|
||||||
|
|
||||||
|
- Maintain a concise, direct communication style in all responses
|
||||||
|
- Focus on delivering information efficiently without unnecessary filler words
|
||||||
|
- Prioritize clarity and brevity while keeping responses informative
|
||||||
|
- Avoid repetitive phrasing or excessive explanations unless specifically requested
|
||||||
|
- Use straightforward language that conveys the point clearly and precisely
|
||||||
|
|
||||||
17
.cursor/rules/css-animations.mdc
Normal file
17
.cursor/rules/css-animations.mdc
Normal file
|
|
@ -0,0 +1,17 @@
|
||||||
|
---
|
||||||
|
description:
|
||||||
|
globs: *.css,*.html
|
||||||
|
alwaysApply: false
|
||||||
|
---
|
||||||
|
# CSS Animation Excellence Standards
|
||||||
|
|
||||||
|
- Prioritize CSS-based animations over JavaScript for better performance
|
||||||
|
- Target buttery-smooth 60fps animations with clean transitions
|
||||||
|
- Use hardware-accelerated properties (transform, opacity) whenever possible
|
||||||
|
- Implement appropriate easing functions that feel natural and responsive
|
||||||
|
- Ensure animations are subtle and enhance user experience without being distracting
|
||||||
|
- Design animations that meet Apple-level quality standards for smoothness and polish
|
||||||
|
- Consider reduced-motion preferences for accessibility
|
||||||
|
- Optimize animation performance by minimizing repaints and reflows
|
||||||
|
- Test animations across different device capabilities to ensure consistent experience
|
||||||
|
|
||||||
16
.cursor/rules/js-ts-naming.mdc
Normal file
16
.cursor/rules/js-ts-naming.mdc
Normal file
|
|
@ -0,0 +1,16 @@
|
||||||
|
---
|
||||||
|
description:
|
||||||
|
globs: *.ts,*.js
|
||||||
|
alwaysApply: false
|
||||||
|
---
|
||||||
|
# JavaScript/TypeScript Naming Conventions
|
||||||
|
|
||||||
|
- Use camelCase for all variable names (`userProfile`, `itemCount`, `fetchData`)
|
||||||
|
- Use camelCase for function and method names (`calculateTotal`, `getUserInfo`)
|
||||||
|
- Use SCREAMING_SNAKE_CASE for constants (`API_KEY`, `MAX_ATTEMPTS`, `DEFAULT_TIMEOUT`)
|
||||||
|
- Use PascalCase for class and component names (`UserProfile`, `PaymentForm`)
|
||||||
|
- Use descriptive names that clearly indicate the purpose or content
|
||||||
|
- Avoid single-letter variable names except in small loop contexts
|
||||||
|
- Prefix boolean variables with "is", "has", or "should" for clarity
|
||||||
|
- Maintain consistent naming patterns throughout the codebase
|
||||||
|
|
||||||
40
.gitignore
vendored
Normal file
40
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,40 @@
|
||||||
|
# dependencies (bun install)
|
||||||
|
node_modules
|
||||||
|
|
||||||
|
# output
|
||||||
|
out
|
||||||
|
dist
|
||||||
|
*.tgz
|
||||||
|
|
||||||
|
# code coverage
|
||||||
|
coverage
|
||||||
|
*.lcov
|
||||||
|
|
||||||
|
# logs
|
||||||
|
logs
|
||||||
|
_.log
|
||||||
|
report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json
|
||||||
|
|
||||||
|
# dotenv environment variable files
|
||||||
|
.env
|
||||||
|
.env.development.local
|
||||||
|
.env.test.local
|
||||||
|
.env.production.local
|
||||||
|
.env.local
|
||||||
|
|
||||||
|
# caches
|
||||||
|
.eslintcache
|
||||||
|
.cache
|
||||||
|
*.tsbuildinfo
|
||||||
|
|
||||||
|
# IntelliJ based IDEs
|
||||||
|
.idea
|
||||||
|
|
||||||
|
# Finder (MacOS) folder config
|
||||||
|
.DS_Store
|
||||||
|
|
||||||
|
# Data Folder
|
||||||
|
data
|
||||||
|
|
||||||
|
# DB Folder
|
||||||
|
db
|
||||||
7
.prettierrc
Normal file
7
.prettierrc
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
{
|
||||||
|
"singleQuote": true,
|
||||||
|
"trailingComma": "all",
|
||||||
|
"printWidth": 100,
|
||||||
|
"tabWidth": 2,
|
||||||
|
"semi": true
|
||||||
|
}
|
||||||
158
bun.lock
Normal file
158
bun.lock
Normal file
|
|
@ -0,0 +1,158 @@
|
||||||
|
{
|
||||||
|
"lockfileVersion": 1,
|
||||||
|
"workspaces": {
|
||||||
|
"": {
|
||||||
|
"name": "bunbun",
|
||||||
|
"dependencies": {
|
||||||
|
"@iarna/toml": "^2.2.5",
|
||||||
|
"cookie": "^1.0.2",
|
||||||
|
"dotenv": "^16.5.0",
|
||||||
|
"express-http-proxy": "^2.1.1",
|
||||||
|
"http-proxy": "^1.18.1",
|
||||||
|
"level": "^10.0.0",
|
||||||
|
"level-ttl": "^3.1.1",
|
||||||
|
"maxmind": "^4.3.25",
|
||||||
|
"string-dsa": "^2.1.0",
|
||||||
|
"tar": "^7.4.3",
|
||||||
|
"tar-stream": "^3.1.7",
|
||||||
|
"toml": "^3.0.0",
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"prettier": "^2.8.8",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"packages": {
|
||||||
|
"@iarna/toml": ["@iarna/toml@2.2.5", "", {}, "sha512-trnsAYxU3xnS1gPHPyU961coFyLkh4gAD/0zQ5mymY4yOZ+CYvsPqUbOFSw0aDM4y0tV7tiFxL/1XfXPNC6IPg=="],
|
||||||
|
|
||||||
|
"@isaacs/fs-minipass": ["@isaacs/fs-minipass@4.0.1", "", { "dependencies": { "minipass": "^7.0.4" } }, "sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w=="],
|
||||||
|
|
||||||
|
"abstract-level": ["abstract-level@3.1.0", "", { "dependencies": { "buffer": "^6.0.3", "is-buffer": "^2.0.5", "level-supports": "^6.2.0", "level-transcoder": "^1.0.1", "maybe-combine-errors": "^1.0.0", "module-error": "^1.0.1" } }, "sha512-j2e+TsAxy7Ri+0h7dJqwasymgt0zHBWX4+nMk3XatyuqgHfdstBJ9wsMfbiGwE1O+QovRyPcVAqcViMYdyPaaw=="],
|
||||||
|
|
||||||
|
"after": ["after@0.8.2", "", {}, "sha512-QbJ0NTQ/I9DI3uSJA4cbexiwQeRAfjPScqIbSjUDd9TOrcg6pTkdgziesOqxBMBzit8vFCTwrP27t13vFOORRA=="],
|
||||||
|
|
||||||
|
"b4a": ["b4a@1.6.7", "", {}, "sha512-OnAYlL5b7LEkALw87fUVafQw5rVR9RjwGd4KUwNQ6DrrNmaVaUCgLipfVlzrPQ4tWOR9P0IXGNOx50jYCCdSJg=="],
|
||||||
|
|
||||||
|
"bare-events": ["bare-events@2.5.4", "", {}, "sha512-+gFfDkR8pj4/TrWCGUGWmJIkBwuxPS5F+a5yWjOHQt2hHvNZd5YLzadjmDUtFmMM4y429bnKLa8bYBMHcYdnQA=="],
|
||||||
|
|
||||||
|
"base64-js": ["base64-js@1.5.1", "", {}, "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA=="],
|
||||||
|
|
||||||
|
"browser-level": ["browser-level@3.0.0", "", { "dependencies": { "abstract-level": "^3.1.0" } }, "sha512-kGXtLh29jMwqKaskz5xeDLtCtN1KBz/DbQSqmvH7QdJiyGRC7RAM8PPg6gvUiNMa+wVnaxS9eSmEtP/f5ajOVw=="],
|
||||||
|
|
||||||
|
"buffer": ["buffer@6.0.3", "", { "dependencies": { "base64-js": "^1.3.1", "ieee754": "^1.2.1" } }, "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA=="],
|
||||||
|
|
||||||
|
"bytes": ["bytes@3.1.2", "", {}, "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg=="],
|
||||||
|
|
||||||
|
"chownr": ["chownr@3.0.0", "", {}, "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g=="],
|
||||||
|
|
||||||
|
"classic-level": ["classic-level@3.0.0", "", { "dependencies": { "abstract-level": "^3.1.0", "module-error": "^1.0.1", "napi-macros": "^2.2.2", "node-gyp-build": "^4.3.0" } }, "sha512-yGy8j8LjPbN0Bh3+ygmyYvrmskVita92pD/zCoalfcC9XxZj6iDtZTAnz+ot7GG8p9KLTG+MZ84tSA4AhkgVZQ=="],
|
||||||
|
|
||||||
|
"cookie": ["cookie@1.0.2", "", {}, "sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA=="],
|
||||||
|
|
||||||
|
"core-util-is": ["core-util-is@1.0.3", "", {}, "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ=="],
|
||||||
|
|
||||||
|
"debug": ["debug@3.2.7", "", { "dependencies": { "ms": "^2.1.1" } }, "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ=="],
|
||||||
|
|
||||||
|
"depd": ["depd@2.0.0", "", {}, "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw=="],
|
||||||
|
|
||||||
|
"dotenv": ["dotenv@16.5.0", "", {}, "sha512-m/C+AwOAr9/W1UOIZUo232ejMNnJAJtYQjUbHoNTBNTJSvqzzDh7vnrei3o3r3m9blf6ZoDkvcw0VmozNRFJxg=="],
|
||||||
|
|
||||||
|
"es6-promise": ["es6-promise@4.2.8", "", {}, "sha512-HJDGx5daxeIvxdBxvG2cb9g4tEvwIk3i8+nhX0yGrYmZUzbkdg8QbDevheDB8gd0//uPj4c1EQua8Q+MViT0/w=="],
|
||||||
|
|
||||||
|
"eventemitter3": ["eventemitter3@4.0.7", "", {}, "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw=="],
|
||||||
|
|
||||||
|
"express-http-proxy": ["express-http-proxy@2.1.1", "", { "dependencies": { "debug": "^3.0.1", "es6-promise": "^4.1.1", "raw-body": "^2.3.0" } }, "sha512-4aRQRqDQU7qNPV5av0/hLcyc0guB9UP71nCYrQEYml7YphTo8tmWf3nDZWdTJMMjFikyz9xKXaURor7Chygdwg=="],
|
||||||
|
|
||||||
|
"fast-fifo": ["fast-fifo@1.3.2", "", {}, "sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ=="],
|
||||||
|
|
||||||
|
"follow-redirects": ["follow-redirects@1.15.9", "", {}, "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ=="],
|
||||||
|
|
||||||
|
"http-errors": ["http-errors@2.0.0", "", { "dependencies": { "depd": "2.0.0", "inherits": "2.0.4", "setprototypeof": "1.2.0", "statuses": "2.0.1", "toidentifier": "1.0.1" } }, "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ=="],
|
||||||
|
|
||||||
|
"http-proxy": ["http-proxy@1.18.1", "", { "dependencies": { "eventemitter3": "^4.0.0", "follow-redirects": "^1.0.0", "requires-port": "^1.0.0" } }, "sha512-7mz/721AbnJwIVbnaSv1Cz3Am0ZLT/UBwkC92VlxhXv/k/BBQfM2fXElQNC27BVGr0uwUpplYPQM9LnaBMR5NQ=="],
|
||||||
|
|
||||||
|
"iconv-lite": ["iconv-lite@0.4.24", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3" } }, "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA=="],
|
||||||
|
|
||||||
|
"ieee754": ["ieee754@1.2.1", "", {}, "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA=="],
|
||||||
|
|
||||||
|
"inherits": ["inherits@2.0.4", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="],
|
||||||
|
|
||||||
|
"is-buffer": ["is-buffer@2.0.5", "", {}, "sha512-i2R6zNFDwgEHJyQUtJEk0XFi1i0dPFn/oqjK3/vPCcDeJvW5NQ83V8QbicfF1SupOaB0h8ntgBC2YiE7dfyctQ=="],
|
||||||
|
|
||||||
|
"isarray": ["isarray@1.0.0", "", {}, "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ=="],
|
||||||
|
|
||||||
|
"level": ["level@10.0.0", "", { "dependencies": { "abstract-level": "^3.1.0", "browser-level": "^3.0.0", "classic-level": "^3.0.0" } }, "sha512-aZJvdfRr/f0VBbSRF5C81FHON47ZsC2TkGxbBezXpGGXAUEL/s6+GP73nnhAYRSCIqUNsmJjfeOF4lzRDKbUig=="],
|
||||||
|
|
||||||
|
"level-supports": ["level-supports@6.2.0", "", {}, "sha512-QNxVXP0IRnBmMsJIh+sb2kwNCYcKciQZJEt+L1hPCHrKNELllXhvrlClVHXBYZVT+a7aTSM6StgNXdAldoab3w=="],
|
||||||
|
|
||||||
|
"level-transcoder": ["level-transcoder@1.0.1", "", { "dependencies": { "buffer": "^6.0.3", "module-error": "^1.0.1" } }, "sha512-t7bFwFtsQeD8cl8NIoQ2iwxA0CL/9IFw7/9gAjOonH0PWTTiRfY7Hq+Ejbsxh86tXobDQ6IOiddjNYIfOBs06w=="],
|
||||||
|
|
||||||
|
"level-ttl": ["level-ttl@3.1.1", "", { "dependencies": { "after": ">=0.8.1 <0.9.0-0", "list-stream": ">=1.0.0 <1.1.0-0", "lock": "~0.1.2", "xtend": ">=4.0.0 <4.1.0-0" } }, "sha512-OeiHOD2IPkmdLqqU4feVJL7mnZX/Q03WEClrQi5t9558alkajVaecCgwJQZVVL/zFR9q74n5pWN1eozifa1Ghw=="],
|
||||||
|
|
||||||
|
"list-stream": ["list-stream@1.0.1", "", { "dependencies": { "readable-stream": "~2.0.5", "xtend": "~4.0.1" } }, "sha512-XheYsTtN+/nay6Co4N9NlTjQzo1ohknNlDJfxTeH0tvvssxBINUXwmjqPtj8+7rYMBwTRb3kO8C8d6ogeRwD1A=="],
|
||||||
|
|
||||||
|
"lock": ["lock@0.1.4", "", {}, "sha512-IcEe2R+NA7WgM622ppgmJFCFZl20f2owsA1YiJg7qpvO0wdOgOuZdfhQMxCYXdESVX+QIF/eikE4hB5ZPM2ipA=="],
|
||||||
|
|
||||||
|
"maxmind": ["maxmind@4.3.25", "", { "dependencies": { "mmdb-lib": "2.2.0", "tiny-lru": "11.2.11" } }, "sha512-u7L6LrbXZUtpdoovTVHo/l4/EoWUT2eHfCKWDMNNTsW9BaLa7h0jCHjqVx5ZeS5aWorLGZSsZwqxcpoollBw1g=="],
|
||||||
|
|
||||||
|
"maybe-combine-errors": ["maybe-combine-errors@1.0.0", "", {}, "sha512-eefp6IduNPT6fVdwPp+1NgD0PML1NU5P6j1Mj5nz1nidX8/sWY7119WL8vTAHgqfsY74TzW0w1XPgdYEKkGZ5A=="],
|
||||||
|
|
||||||
|
"minipass": ["minipass@7.1.2", "", {}, "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw=="],
|
||||||
|
|
||||||
|
"minizlib": ["minizlib@3.0.2", "", { "dependencies": { "minipass": "^7.1.2" } }, "sha512-oG62iEk+CYt5Xj2YqI5Xi9xWUeZhDI8jjQmC5oThVH5JGCTgIjr7ciJDzC7MBzYd//WvR1OTmP5Q38Q8ShQtVA=="],
|
||||||
|
|
||||||
|
"mkdirp": ["mkdirp@3.0.1", "", { "bin": { "mkdirp": "dist/cjs/src/bin.js" } }, "sha512-+NsyUUAZDmo6YVHzL/stxSu3t9YS1iljliy3BSDrXJ/dkn1KYdmtZODGGjLcc9XLgVVpH4KshHB8XmZgMhaBXg=="],
|
||||||
|
|
||||||
|
"mmdb-lib": ["mmdb-lib@2.2.0", "", {}, "sha512-V6DDh3v8tfZFWbeH6fsL5uBIlWL7SvRgGDaAZWFC5kjQ2xP5dl/mLpWwJQ1Ho6ZbEKVp/351QF1JXYTAmeZ/zA=="],
|
||||||
|
|
||||||
|
"module-error": ["module-error@1.0.2", "", {}, "sha512-0yuvsqSCv8LbaOKhnsQ/T5JhyFlCYLPXK3U2sgV10zoKQwzs/MyfuQUOZQ1V/6OCOJsK/TRgNVrPuPDqtdMFtA=="],
|
||||||
|
|
||||||
|
"ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
|
||||||
|
|
||||||
|
"napi-macros": ["napi-macros@2.2.2", "", {}, "sha512-hmEVtAGYzVQpCKdbQea4skABsdXW4RUh5t5mJ2zzqowJS2OyXZTU1KhDVFhx+NlWZ4ap9mqR9TcDO3LTTttd+g=="],
|
||||||
|
|
||||||
|
"node-gyp-build": ["node-gyp-build@4.8.4", "", { "bin": { "node-gyp-build": "bin.js", "node-gyp-build-optional": "optional.js", "node-gyp-build-test": "build-test.js" } }, "sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ=="],
|
||||||
|
|
||||||
|
"prettier": ["prettier@2.8.8", "", { "bin": { "prettier": "bin-prettier.js" } }, "sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q=="],
|
||||||
|
|
||||||
|
"process-nextick-args": ["process-nextick-args@1.0.7", "", {}, "sha512-yN0WQmuCX63LP/TMvAg31nvT6m4vDqJEiiv2CAZqWOGNWutc9DfDk1NPYYmKUFmaVM2UwDowH4u5AHWYP/jxKw=="],
|
||||||
|
|
||||||
|
"raw-body": ["raw-body@2.5.2", "", { "dependencies": { "bytes": "3.1.2", "http-errors": "2.0.0", "iconv-lite": "0.4.24", "unpipe": "1.0.0" } }, "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA=="],
|
||||||
|
|
||||||
|
"readable-stream": ["readable-stream@2.0.6", "", { "dependencies": { "core-util-is": "~1.0.0", "inherits": "~2.0.1", "isarray": "~1.0.0", "process-nextick-args": "~1.0.6", "string_decoder": "~0.10.x", "util-deprecate": "~1.0.1" } }, "sha512-TXcFfb63BQe1+ySzsHZI/5v1aJPCShfqvWJ64ayNImXMsN1Cd0YGk/wm8KB7/OeessgPc9QvS9Zou8QTkFzsLw=="],
|
||||||
|
|
||||||
|
"requires-port": ["requires-port@1.0.0", "", {}, "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ=="],
|
||||||
|
|
||||||
|
"safer-buffer": ["safer-buffer@2.1.2", "", {}, "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="],
|
||||||
|
|
||||||
|
"setprototypeof": ["setprototypeof@1.2.0", "", {}, "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw=="],
|
||||||
|
|
||||||
|
"statuses": ["statuses@2.0.1", "", {}, "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ=="],
|
||||||
|
|
||||||
|
"streamx": ["streamx@2.22.0", "", { "dependencies": { "fast-fifo": "^1.3.2", "text-decoder": "^1.1.0" }, "optionalDependencies": { "bare-events": "^2.2.0" } }, "sha512-sLh1evHOzBy/iWRiR6d1zRcLao4gGZr3C1kzNz4fopCOKJb6xD9ub8Mpi9Mr1R6id5o43S+d93fI48UC5uM9aw=="],
|
||||||
|
|
||||||
|
"string-dsa": ["string-dsa@2.1.0", "", {}, "sha512-ht+H83VtdA0JXmZsRfhQYzUSwqK3T7STPqiD/u3bIvYUHLEw8zzZyvP9WI3l9uKbK/2IpU+ZdshAe5BoRil3wA=="],
|
||||||
|
|
||||||
|
"string_decoder": ["string_decoder@0.10.31", "", {}, "sha512-ev2QzSzWPYmy9GuqfIVildA4OdcGLeFZQrq5ys6RtiuF+RQQiZWr8TZNyAcuVXyQRYfEO+MsoB/1BuQVhOJuoQ=="],
|
||||||
|
|
||||||
|
"tar": ["tar@7.4.3", "", { "dependencies": { "@isaacs/fs-minipass": "^4.0.0", "chownr": "^3.0.0", "minipass": "^7.1.2", "minizlib": "^3.0.1", "mkdirp": "^3.0.1", "yallist": "^5.0.0" } }, "sha512-5S7Va8hKfV7W5U6g3aYxXmlPoZVAwUMy9AOKyF2fVuZa2UD3qZjg578OrLRt8PcNN1PleVaL/5/yYATNL0ICUw=="],
|
||||||
|
|
||||||
|
"tar-stream": ["tar-stream@3.1.7", "", { "dependencies": { "b4a": "^1.6.4", "fast-fifo": "^1.2.0", "streamx": "^2.15.0" } }, "sha512-qJj60CXt7IU1Ffyc3NJMjh6EkuCFej46zUqJ4J7pqYlThyd9bO0XBTmcOIhSzZJVWfsLks0+nle/j538YAW9RQ=="],
|
||||||
|
|
||||||
|
"text-decoder": ["text-decoder@1.2.3", "", { "dependencies": { "b4a": "^1.6.4" } }, "sha512-3/o9z3X0X0fTupwsYvR03pJ/DjWuqqrfwBgTQzdWDiQSm9KitAyz/9WqsT2JQW7KV2m+bC2ol/zqpW37NHxLaA=="],
|
||||||
|
|
||||||
|
"tiny-lru": ["tiny-lru@11.2.11", "", {}, "sha512-27BIW0dIWTYYoWNnqSmoNMKe5WIbkXsc0xaCQHd3/3xT2XMuMJrzHdrO9QBFR14emBz1Bu0dOAs2sCBBrvgPQA=="],
|
||||||
|
|
||||||
|
"toidentifier": ["toidentifier@1.0.1", "", {}, "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA=="],
|
||||||
|
|
||||||
|
"toml": ["toml@3.0.0", "", {}, "sha512-y/mWCZinnvxjTKYhJ+pYxwD0mRLVvOtdS2Awbgxln6iEnt4rk0yBxeSBHkGJcPucRiG0e55mwWp+g/05rsrd6w=="],
|
||||||
|
|
||||||
|
"unpipe": ["unpipe@1.0.0", "", {}, "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ=="],
|
||||||
|
|
||||||
|
"util-deprecate": ["util-deprecate@1.0.2", "", {}, "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="],
|
||||||
|
|
||||||
|
"xtend": ["xtend@4.0.2", "", {}, "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ=="],
|
||||||
|
|
||||||
|
"yallist": ["yallist@5.0.0", "", {}, "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw=="],
|
||||||
|
}
|
||||||
|
}
|
||||||
961
checkpoint.js
Normal file
961
checkpoint.js
Normal file
|
|
@ -0,0 +1,961 @@
|
||||||
|
import { registerPlugin, loadConfig, rootDir } from './index.js';
|
||||||
|
import crypto from 'crypto';
|
||||||
|
import path from 'path';
|
||||||
|
import fs from 'fs';
|
||||||
|
import { promises as fsPromises } from 'fs';
|
||||||
|
import { dirname, join } from 'path';
|
||||||
|
import { fileURLToPath } from 'url';
|
||||||
|
import { Level } from 'level';
|
||||||
|
import cookie from 'cookie';
|
||||||
|
import { parseDuration } from './utils/time.js';
|
||||||
|
import { getRealIP } from './utils/network.js';
|
||||||
|
import ttl from 'level-ttl';
|
||||||
|
import { Readable } from 'stream';
|
||||||
|
import {
|
||||||
|
challengeStore,
|
||||||
|
generateRequestID as proofGenerateRequestID,
|
||||||
|
getChallengeParams,
|
||||||
|
deleteChallenge,
|
||||||
|
verifyPoW,
|
||||||
|
verifyPoS,
|
||||||
|
} from './utils/proof.js';
|
||||||
|
// Import recordEvent dynamically to avoid circular dependency issues
|
||||||
|
let recordEvent;
|
||||||
|
let statsLoadPromise = import('./plugins/stats.js')
|
||||||
|
.then((stats) => {
|
||||||
|
recordEvent = stats.recordEvent;
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
console.error('Failed to import stats module:', err);
|
||||||
|
recordEvent = null;
|
||||||
|
});
|
||||||
|
|
||||||
|
function sanitizePath(inputPath) {
|
||||||
|
let pathOnly = inputPath.replace(/[\x00-\x1F\x7F]/g, '');
|
||||||
|
|
||||||
|
pathOnly = pathOnly.replace(/[<>;"'`|]/g, '');
|
||||||
|
|
||||||
|
const parts = pathOnly.split('/').filter((seg) => seg && seg !== '.' && seg !== '..');
|
||||||
|
|
||||||
|
return '/' + parts.map((seg) => encodeURIComponent(seg)).join('/');
|
||||||
|
}
|
||||||
|
|
||||||
|
const checkpointConfig = {};
|
||||||
|
let hmacSecret = null;
|
||||||
|
const usedNonces = new Map();
|
||||||
|
const ipRateLimit = new Map();
|
||||||
|
|
||||||
|
const tokenCache = new Map();
|
||||||
|
|
||||||
|
let db;
|
||||||
|
|
||||||
|
const tokenExpirations = new Map();
|
||||||
|
|
||||||
|
let interstitialTemplate = null;
|
||||||
|
|
||||||
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||||
|
|
||||||
|
function simpleTemplate(str) {
|
||||||
|
return function (data) {
|
||||||
|
return str.replace(/\{\{\s*([^{}]+?)\s*\}\}/g, (_, key) => {
|
||||||
|
let value = data;
|
||||||
|
|
||||||
|
for (const part of key.trim().split('.')) {
|
||||||
|
value = value?.[part];
|
||||||
|
if (value == null) break;
|
||||||
|
}
|
||||||
|
return value != null ? String(value) : '';
|
||||||
|
});
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async function initConfig() {
|
||||||
|
await loadConfig('checkpoint', checkpointConfig);
|
||||||
|
|
||||||
|
// Handle new nested configuration structure
|
||||||
|
// Map nested structure to flat structure for internal use
|
||||||
|
checkpointConfig.Enabled = checkpointConfig.Core.Enabled;
|
||||||
|
checkpointConfig.CookieName = checkpointConfig.Core.CookieName;
|
||||||
|
checkpointConfig.CookieDomain = checkpointConfig.Core.CookieDomain;
|
||||||
|
checkpointConfig.SanitizeURLs = checkpointConfig.Core.SanitizeURLs;
|
||||||
|
|
||||||
|
// Proof of Work settings
|
||||||
|
checkpointConfig.Difficulty = checkpointConfig.ProofOfWork.Difficulty;
|
||||||
|
checkpointConfig.SaltLength = checkpointConfig.ProofOfWork.SaltLength;
|
||||||
|
checkpointConfig.ChallengeExpiration = parseDuration(
|
||||||
|
checkpointConfig.ProofOfWork.ChallengeExpiration,
|
||||||
|
);
|
||||||
|
checkpointConfig.MaxAttemptsPerHour = checkpointConfig.ProofOfWork.MaxAttemptsPerHour;
|
||||||
|
|
||||||
|
// Proof of Space-Time settings
|
||||||
|
checkpointConfig.CheckPoSTimes = checkpointConfig.ProofOfSpaceTime.Enabled;
|
||||||
|
checkpointConfig.PoSTimeConsistencyRatio = checkpointConfig.ProofOfSpaceTime.ConsistencyRatio;
|
||||||
|
|
||||||
|
// Token settings
|
||||||
|
checkpointConfig.TokenExpiration = parseDuration(checkpointConfig.Token.Expiration);
|
||||||
|
checkpointConfig.MaxNonceAge = parseDuration(checkpointConfig.Token.MaxNonceAge);
|
||||||
|
|
||||||
|
// Storage settings
|
||||||
|
checkpointConfig.SecretConfigPath = checkpointConfig.Storage.SecretPath;
|
||||||
|
checkpointConfig.TokenStoreDBPath = checkpointConfig.Storage.TokenDBPath;
|
||||||
|
checkpointConfig.InterstitialPaths = checkpointConfig.Storage.InterstitialTemplates;
|
||||||
|
|
||||||
|
// Process exclusions
|
||||||
|
checkpointConfig.ExclusionRules = checkpointConfig.Exclusion || [];
|
||||||
|
|
||||||
|
// Process bypass keys
|
||||||
|
checkpointConfig.BypassQueryKeys = [];
|
||||||
|
checkpointConfig.BypassHeaderKeys = [];
|
||||||
|
checkpointConfig.BypassKeys.forEach((key) => {
|
||||||
|
if (key.Type === 'query') {
|
||||||
|
checkpointConfig.BypassQueryKeys.push({
|
||||||
|
Key: key.Key,
|
||||||
|
Value: key.Value,
|
||||||
|
Domains: key.Hosts || [],
|
||||||
|
});
|
||||||
|
} else if (key.Type === 'header') {
|
||||||
|
checkpointConfig.BypassHeaderKeys.push({
|
||||||
|
Name: key.Key,
|
||||||
|
Value: key.Value,
|
||||||
|
Domains: key.Hosts || [],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Extension handling
|
||||||
|
checkpointConfig.HTMLCheckpointIncludedExtensions = checkpointConfig.Extensions.IncludeOnly;
|
||||||
|
checkpointConfig.HTMLCheckpointExcludedExtensions = checkpointConfig.Extensions.Exclude;
|
||||||
|
|
||||||
|
// Remove legacy arrays
|
||||||
|
checkpointConfig.HTMLCheckpointExclusions = [];
|
||||||
|
checkpointConfig.UserAgentValidationExclusions = [];
|
||||||
|
checkpointConfig.UserAgentRequiredPrefixes = {};
|
||||||
|
checkpointConfig.ReverseProxyMappings = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
function addReadStreamSupport(dbInstance) {
|
||||||
|
if (!dbInstance.createReadStream) {
|
||||||
|
dbInstance.createReadStream = (opts) =>
|
||||||
|
Readable.from(
|
||||||
|
(async function* () {
|
||||||
|
for await (const [key, value] of dbInstance.iterator(opts)) {
|
||||||
|
yield { key, value };
|
||||||
|
}
|
||||||
|
})(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return dbInstance;
|
||||||
|
}
|
||||||
|
|
||||||
|
function initTokenStore() {
|
||||||
|
try {
|
||||||
|
const storePath = join(rootDir, checkpointConfig.TokenStoreDBPath || 'db/tokenstore');
|
||||||
|
fs.mkdirSync(storePath, { recursive: true });
|
||||||
|
|
||||||
|
let rawDB = new Level(storePath, { valueEncoding: 'json' });
|
||||||
|
|
||||||
|
addReadStreamSupport(rawDB);
|
||||||
|
db = ttl(rawDB, { defaultTTL: checkpointConfig.TokenExpiration });
|
||||||
|
|
||||||
|
addReadStreamSupport(db);
|
||||||
|
console.log('Token store initialized with TTL');
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Failed to initialize token store:', err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getFullClientIP(request) {
|
||||||
|
const ip = getRealIP(request) || '';
|
||||||
|
const h = crypto.createHash('sha256').update(ip).digest();
|
||||||
|
return h.slice(0, 8).toString('hex');
|
||||||
|
}
|
||||||
|
|
||||||
|
function hashUserAgent(ua) {
|
||||||
|
if (!ua) return '';
|
||||||
|
const h = crypto.createHash('sha256').update(ua).digest();
|
||||||
|
return h.slice(0, 8).toString('hex');
|
||||||
|
}
|
||||||
|
|
||||||
|
function extractBrowserFingerprint(request) {
|
||||||
|
const headers = [
|
||||||
|
'sec-ch-ua',
|
||||||
|
'sec-ch-ua-platform',
|
||||||
|
'sec-ch-ua-mobile',
|
||||||
|
'sec-ch-ua-platform-version',
|
||||||
|
'sec-ch-ua-arch',
|
||||||
|
'sec-ch-ua-model',
|
||||||
|
];
|
||||||
|
const parts = headers.map((h) => request.headers.get(h)).filter(Boolean);
|
||||||
|
if (!parts.length) return '';
|
||||||
|
const buf = Buffer.from(parts.join('|'));
|
||||||
|
const h = crypto.createHash('sha256').update(buf).digest();
|
||||||
|
return h.slice(0, 12).toString('hex');
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getInterstitialTemplate() {
|
||||||
|
if (!interstitialTemplate) {
|
||||||
|
for (const p of checkpointConfig.InterstitialPaths) {
|
||||||
|
try {
|
||||||
|
let templatePath = join(__dirname, p);
|
||||||
|
if (fs.existsSync(templatePath)) {
|
||||||
|
const raw = await fsPromises.readFile(templatePath, 'utf8');
|
||||||
|
interstitialTemplate = simpleTemplate(raw);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
templatePath = join(rootDir, p);
|
||||||
|
if (fs.existsSync(templatePath)) {
|
||||||
|
const raw = await fsPromises.readFile(templatePath, 'utf8');
|
||||||
|
interstitialTemplate = simpleTemplate(raw);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.warn(`Failed to load interstitial template from path ${p}:`, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!interstitialTemplate) {
|
||||||
|
// Create a minimal fallback template
|
||||||
|
console.warn('Could not find interstitial HTML template, using minimal fallback');
|
||||||
|
interstitialTemplate = simpleTemplate(`
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title>Security Verification</title>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h1>Security Verification Required</h1>
|
||||||
|
<p>Please wait while we verify your request...</p>
|
||||||
|
<div id="verification-data"
|
||||||
|
data-target="{{TargetPath}}"
|
||||||
|
data-request-id="{{RequestID}}">
|
||||||
|
</div>
|
||||||
|
<script src="/js/c.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return interstitialTemplate;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper function for safe stats recording
|
||||||
|
function safeRecordEvent(metric, data) {
|
||||||
|
// If recordEvent is not yet loaded, try to wait for it
|
||||||
|
if (!recordEvent && statsLoadPromise) {
|
||||||
|
statsLoadPromise.then(() => {
|
||||||
|
if (recordEvent) {
|
||||||
|
try {
|
||||||
|
recordEvent(metric, data);
|
||||||
|
} catch (err) {
|
||||||
|
console.error(`Failed to record ${metric} event:`, err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof recordEvent === 'function') {
|
||||||
|
try {
|
||||||
|
recordEvent(metric, data);
|
||||||
|
} catch (err) {
|
||||||
|
console.error(`Failed to record ${metric} event:`, err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function serveInterstitial(request) {
|
||||||
|
const ip = getRealIP(request);
|
||||||
|
const requestPath = new URL(request.url).pathname;
|
||||||
|
safeRecordEvent('checkpoint.sent', { ip, path: requestPath });
|
||||||
|
let tpl;
|
||||||
|
try {
|
||||||
|
tpl = await getInterstitialTemplate();
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Interstitial template error:', err);
|
||||||
|
return new Response('Security verification required.', {
|
||||||
|
status: 200,
|
||||||
|
headers: { 'Content-Type': 'text/plain' },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const requestID = proofGenerateRequestID(request, checkpointConfig);
|
||||||
|
const url = new URL(request.url);
|
||||||
|
const host = request.headers.get('host') || url.hostname;
|
||||||
|
const targetPath = url.pathname;
|
||||||
|
const fullURL = request.url;
|
||||||
|
|
||||||
|
const html = tpl({
|
||||||
|
TargetPath: targetPath,
|
||||||
|
RequestID: requestID,
|
||||||
|
Host: host,
|
||||||
|
FullURL: fullURL,
|
||||||
|
});
|
||||||
|
|
||||||
|
return new Response(html, {
|
||||||
|
status: 200,
|
||||||
|
headers: { 'Content-Type': 'text/html; charset=utf-8' },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleGetCheckpointChallenge(request) {
|
||||||
|
const url = new URL(request.url);
|
||||||
|
const requestID = url.searchParams.get('id');
|
||||||
|
if (!requestID) {
|
||||||
|
return new Response(JSON.stringify({ error: 'Missing request ID' }), {
|
||||||
|
status: 400,
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const ip = getRealIP(request);
|
||||||
|
const attempts = (ipRateLimit.get(ip) || 0) + 1;
|
||||||
|
ipRateLimit.set(ip, attempts);
|
||||||
|
|
||||||
|
if (attempts > checkpointConfig.MaxAttemptsPerHour) {
|
||||||
|
return new Response(
|
||||||
|
JSON.stringify({ error: 'Too many challenge requests. Try again later.' }),
|
||||||
|
{
|
||||||
|
status: 429,
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const params = getChallengeParams(requestID);
|
||||||
|
if (!params) {
|
||||||
|
return new Response(JSON.stringify({ error: 'Challenge not found or expired' }), {
|
||||||
|
status: 404,
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ip !== params.ClientIP) {
|
||||||
|
return new Response(JSON.stringify({ error: 'IP address mismatch for challenge' }), {
|
||||||
|
status: 403,
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const payload = {
|
||||||
|
a: params.Challenge,
|
||||||
|
b: params.Salt,
|
||||||
|
c: params.Difficulty,
|
||||||
|
d: params.PoSSeed,
|
||||||
|
};
|
||||||
|
return new Response(JSON.stringify(payload), {
|
||||||
|
status: 200,
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function calculateTokenHash(token) {
|
||||||
|
const data = `${token.Nonce}:${token.Entropy}:${token.Created.getTime()}`;
|
||||||
|
return crypto.createHash('sha256').update(data).digest('hex');
|
||||||
|
}
|
||||||
|
|
||||||
|
function computeTokenSignature(token) {
|
||||||
|
const copy = { ...token, Signature: '' };
|
||||||
|
const serialized = JSON.stringify(copy);
|
||||||
|
return crypto.createHmac('sha256', hmacSecret).update(serialized).digest('hex');
|
||||||
|
}
|
||||||
|
|
||||||
|
function verifyTokenSignature(token) {
|
||||||
|
if (!token.Signature) return false;
|
||||||
|
const expected = computeTokenSignature(token);
|
||||||
|
try {
|
||||||
|
return crypto.timingSafeEqual(
|
||||||
|
Buffer.from(token.Signature, 'hex'),
|
||||||
|
Buffer.from(expected, 'hex'),
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function issueToken(request, token) {
|
||||||
|
const tokenHash = calculateTokenHash(token);
|
||||||
|
const storedData = {
|
||||||
|
ClientIPHash: token.ClientIP,
|
||||||
|
UserAgentHash: token.UserAgent,
|
||||||
|
BrowserHint: token.BrowserHint,
|
||||||
|
LastVerified: new Date(token.LastVerified).toISOString(),
|
||||||
|
ExpiresAt: new Date(token.ExpiresAt).toISOString(),
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
await addToken(tokenHash, storedData);
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Failed to store token:', err);
|
||||||
|
}
|
||||||
|
|
||||||
|
token.Signature = computeTokenSignature(token);
|
||||||
|
|
||||||
|
const tokenStr = Buffer.from(JSON.stringify(token)).toString('base64');
|
||||||
|
|
||||||
|
const url = new URL(request.url);
|
||||||
|
const cookieDomain = checkpointConfig.CookieDomain || '';
|
||||||
|
const sameSite = cookieDomain ? 'Lax' : 'Strict';
|
||||||
|
const secure = url.protocol === 'https:';
|
||||||
|
const expires = new Date(token.ExpiresAt).toUTCString();
|
||||||
|
|
||||||
|
const domainPart = cookieDomain ? `; Domain=${cookieDomain}` : '';
|
||||||
|
const securePart = secure ? '; Secure' : '';
|
||||||
|
const cookieStr =
|
||||||
|
`${checkpointConfig.CookieName}=${tokenStr}; Path=/` +
|
||||||
|
`${domainPart}; Expires=${expires}; HttpOnly; SameSite=${sameSite}${securePart}`;
|
||||||
|
return new Response(JSON.stringify({ token: tokenStr, expires_at: token.ExpiresAt }), {
|
||||||
|
status: 200,
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Set-Cookie': cookieStr,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleVerifyCheckpoint(request) {
|
||||||
|
let body;
|
||||||
|
try {
|
||||||
|
body = await request.json();
|
||||||
|
} catch (e) {
|
||||||
|
safeRecordEvent('checkpoint.failure', { reason: 'invalid_json', ip: getRealIP(request) });
|
||||||
|
return new Response(JSON.stringify({ error: 'Invalid JSON' }), {
|
||||||
|
status: 400,
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const ip = getRealIP(request);
|
||||||
|
const params = getChallengeParams(body.request_id);
|
||||||
|
|
||||||
|
if (!params) {
|
||||||
|
safeRecordEvent('checkpoint.failure', { reason: 'invalid_or_expired_request', ip });
|
||||||
|
return new Response(JSON.stringify({ error: 'Invalid or expired request ID' }), {
|
||||||
|
status: 400,
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ip !== params.ClientIP) {
|
||||||
|
safeRecordEvent('checkpoint.failure', { reason: 'ip_mismatch', ip });
|
||||||
|
return new Response(JSON.stringify({ error: 'IP address mismatch' }), {
|
||||||
|
status: 403,
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const challenge = params.Challenge;
|
||||||
|
const salt = params.Salt;
|
||||||
|
|
||||||
|
if (!body.g || !verifyPoW(challenge, salt, body.g, params.Difficulty)) {
|
||||||
|
safeRecordEvent('checkpoint.failure', { reason: 'invalid_pow', ip });
|
||||||
|
return new Response(JSON.stringify({ error: 'Invalid proof-of-work solution' }), {
|
||||||
|
status: 400,
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const nonceKey = body.g + challenge;
|
||||||
|
usedNonces.set(nonceKey, Date.now());
|
||||||
|
|
||||||
|
if (body.h?.length === 3 && body.i?.length === 3) {
|
||||||
|
try {
|
||||||
|
verifyPoS(body.h, body.i, checkpointConfig);
|
||||||
|
} catch (e) {
|
||||||
|
safeRecordEvent('checkpoint.failure', { reason: 'invalid_pos', ip });
|
||||||
|
return new Response(JSON.stringify({ error: e.message }), {
|
||||||
|
status: 400,
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
deleteChallenge(body.request_id);
|
||||||
|
safeRecordEvent('checkpoint.success', { ip });
|
||||||
|
const now = new Date();
|
||||||
|
const expiresAt = new Date(now.getTime() + checkpointConfig.TokenExpiration);
|
||||||
|
|
||||||
|
const token = {
|
||||||
|
Nonce: body.g,
|
||||||
|
ExpiresAt: expiresAt,
|
||||||
|
ClientIP: getFullClientIP(request),
|
||||||
|
UserAgent: hashUserAgent(request.headers.get('user-agent')),
|
||||||
|
BrowserHint: extractBrowserFingerprint(request),
|
||||||
|
Entropy: crypto.randomBytes(8).toString('hex'),
|
||||||
|
Created: now,
|
||||||
|
LastVerified: now,
|
||||||
|
TokenFormat: 2,
|
||||||
|
};
|
||||||
|
|
||||||
|
token.Signature = computeTokenSignature(token);
|
||||||
|
const tokenStr = Buffer.from(JSON.stringify(token)).toString('base64');
|
||||||
|
|
||||||
|
const tokenKey = crypto.createHash('sha256').update(tokenStr).digest('hex');
|
||||||
|
try {
|
||||||
|
await db.put(tokenKey, true);
|
||||||
|
tokenCache.set(tokenKey, true);
|
||||||
|
|
||||||
|
tokenExpirations.set(tokenKey, new Date(token.ExpiresAt).getTime());
|
||||||
|
console.log(`checkpoint: token stored in DB and cache key=${tokenKey}`);
|
||||||
|
} catch (e) {
|
||||||
|
console.error('checkpoint: failed to store token in DB:', e);
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Response(JSON.stringify({ token: tokenStr, expires_at: token.ExpiresAt }), {
|
||||||
|
status: 200,
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function generateUpdatedCookie(token, secure) {
|
||||||
|
token.Signature = computeTokenSignature(token);
|
||||||
|
const tokenStr = Buffer.from(JSON.stringify(token)).toString('base64');
|
||||||
|
const cookieDomain = checkpointConfig.CookieDomain || '';
|
||||||
|
const sameSite = cookieDomain ? 'Lax' : 'Strict';
|
||||||
|
const expires = new Date(token.ExpiresAt).toUTCString();
|
||||||
|
|
||||||
|
const domainPart = cookieDomain ? `; Domain=${cookieDomain}` : '';
|
||||||
|
const securePart = secure ? '; Secure' : '';
|
||||||
|
const cookieStr =
|
||||||
|
`${checkpointConfig.CookieName}=${tokenStr}; Path=/` +
|
||||||
|
`${domainPart}; Expires=${expires}; HttpOnly; SameSite=${sameSite}${securePart}`;
|
||||||
|
return cookieStr;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function validateToken(tokenStr, request) {
|
||||||
|
if (!tokenStr) return false;
|
||||||
|
let token;
|
||||||
|
try {
|
||||||
|
token = JSON.parse(Buffer.from(tokenStr, 'base64').toString());
|
||||||
|
} catch {
|
||||||
|
console.log('checkpoint: invalid token format');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Date.now() > new Date(token.ExpiresAt).getTime()) {
|
||||||
|
console.log('checkpoint: token expired');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!verifyTokenSignature(token)) {
|
||||||
|
console.log('checkpoint: invalid token signature');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const tokenKey = crypto.createHash('sha256').update(tokenStr).digest('hex');
|
||||||
|
|
||||||
|
if (tokenCache.has(tokenKey)) return true;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await db.get(tokenKey);
|
||||||
|
tokenCache.set(tokenKey, true);
|
||||||
|
|
||||||
|
tokenExpirations.set(tokenKey, new Date(token.ExpiresAt).getTime());
|
||||||
|
return true;
|
||||||
|
} catch {
|
||||||
|
console.log('checkpoint: token not found in DB');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function createProxyResponse(targetURL, request) {
|
||||||
|
const url = new URL(request.url);
|
||||||
|
const targetUrl = new URL(url.pathname + url.search, targetURL);
|
||||||
|
|
||||||
|
const headers = Object.fromEntries(request.headers.entries());
|
||||||
|
delete headers.host;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const method = request.method;
|
||||||
|
const options = {
|
||||||
|
method,
|
||||||
|
headers,
|
||||||
|
redirect: 'manual',
|
||||||
|
};
|
||||||
|
|
||||||
|
if (method !== 'GET' && method !== 'HEAD') {
|
||||||
|
options.body = await request.blob();
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await fetch(targetUrl.toString(), options);
|
||||||
|
|
||||||
|
const responseHeaders = new Headers(response.headers);
|
||||||
|
const hopByHopHeaders = [
|
||||||
|
'connection',
|
||||||
|
'keep-alive',
|
||||||
|
'proxy-authenticate',
|
||||||
|
'proxy-authorization',
|
||||||
|
'te',
|
||||||
|
'trailer',
|
||||||
|
'transfer-encoding',
|
||||||
|
'upgrade',
|
||||||
|
];
|
||||||
|
|
||||||
|
hopByHopHeaders.forEach((h) => responseHeaders.delete(h));
|
||||||
|
|
||||||
|
return new Response(response.body, {
|
||||||
|
status: response.status,
|
||||||
|
statusText: response.statusText,
|
||||||
|
headers: responseHeaders,
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Proxy error:', err);
|
||||||
|
return new Response('Bad Gateway', { status: 502 });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleTokenRedirect(request) {
|
||||||
|
const url = new URL(request.url);
|
||||||
|
const tokenStr = url.searchParams.get('token');
|
||||||
|
if (!tokenStr) return undefined;
|
||||||
|
|
||||||
|
let token;
|
||||||
|
try {
|
||||||
|
token = JSON.parse(Buffer.from(tokenStr, 'base64').toString());
|
||||||
|
|
||||||
|
if (Date.now() > new Date(token.ExpiresAt).getTime()) {
|
||||||
|
console.log('checkpoint: token in URL parameter expired');
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!verifyTokenSignature(token)) {
|
||||||
|
console.log('checkpoint: invalid token signature in URL parameter');
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
const tokenKey = crypto.createHash('sha256').update(tokenStr).digest('hex');
|
||||||
|
try {
|
||||||
|
await db.get(tokenKey);
|
||||||
|
} catch {
|
||||||
|
console.log('checkpoint: token in URL parameter not found in DB');
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.log('checkpoint: invalid token format in URL parameter', e);
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
const expires = new Date(token.ExpiresAt).toUTCString();
|
||||||
|
const cookieDomain = checkpointConfig.CookieDomain || '';
|
||||||
|
const sameSite = cookieDomain ? 'Lax' : 'Strict';
|
||||||
|
const securePart = url.protocol === 'https:' ? '; Secure' : '';
|
||||||
|
const domainPart = cookieDomain ? `; Domain=${cookieDomain}` : '';
|
||||||
|
const cookieStr =
|
||||||
|
`${checkpointConfig.CookieName}=${tokenStr}; Path=/` +
|
||||||
|
`${domainPart}; Expires=${expires}; HttpOnly; SameSite=${sameSite}${securePart}`;
|
||||||
|
|
||||||
|
url.searchParams.delete('token');
|
||||||
|
const cleanUrl = url.pathname + (url.search || '');
|
||||||
|
return new Response(null, {
|
||||||
|
status: 302,
|
||||||
|
headers: {
|
||||||
|
'Set-Cookie': cookieStr,
|
||||||
|
Location: cleanUrl,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function CheckpointMiddleware() {
|
||||||
|
return async (request) => {
|
||||||
|
// Check if checkpoint is enabled
|
||||||
|
if (checkpointConfig.Enabled === false) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
const urlObj = new URL(request.url);
|
||||||
|
const host = request.headers.get('host')?.split(':')[0];
|
||||||
|
const userAgent = request.headers.get('user-agent') || '';
|
||||||
|
|
||||||
|
// 1) Bypass via query keys
|
||||||
|
for (const { Key, Value, Domains } of checkpointConfig.BypassQueryKeys) {
|
||||||
|
if (urlObj.searchParams.get(Key) === Value) {
|
||||||
|
if (!Array.isArray(Domains) || Domains.length === 0 || Domains.includes(host)) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2) Bypass via header keys
|
||||||
|
for (const { Name, Value, Domains } of checkpointConfig.BypassHeaderKeys) {
|
||||||
|
const headerVal = request.headers.get(Name);
|
||||||
|
if (headerVal === Value) {
|
||||||
|
if (!Array.isArray(Domains) || Domains.length === 0 || Domains.includes(host)) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle token redirect for URL-token login
|
||||||
|
const tokenResponse = await handleTokenRedirect(request);
|
||||||
|
if (tokenResponse) return tokenResponse;
|
||||||
|
|
||||||
|
// Setup request context
|
||||||
|
const url = new URL(request.url);
|
||||||
|
let path = url.pathname;
|
||||||
|
if (checkpointConfig.SanitizeURLs) {
|
||||||
|
path = sanitizePath(path);
|
||||||
|
}
|
||||||
|
const method = request.method;
|
||||||
|
|
||||||
|
// Always allow challenge & verify endpoints
|
||||||
|
if (method === 'GET' && path === '/api/challenge') {
|
||||||
|
return handleGetCheckpointChallenge(request);
|
||||||
|
}
|
||||||
|
if (method === 'POST' && path === '/api/verify') {
|
||||||
|
return handleVerifyCheckpoint(request);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check new exclusion rules
|
||||||
|
if (checkpointConfig.ExclusionRules && checkpointConfig.ExclusionRules.length > 0) {
|
||||||
|
for (const rule of checkpointConfig.ExclusionRules) {
|
||||||
|
// Check if path matches
|
||||||
|
if (!rule.Path || !path.startsWith(rule.Path)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if host matches (if specified)
|
||||||
|
if (rule.Hosts && rule.Hosts.length > 0 && !rule.Hosts.includes(host)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if user agent matches (if specified)
|
||||||
|
if (rule.UserAgents && rule.UserAgents.length > 0) {
|
||||||
|
const matchesUA = rule.UserAgents.some((ua) => userAgent.includes(ua));
|
||||||
|
if (!matchesUA) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// All conditions match - exclude this request
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check file extensions
|
||||||
|
const ext = path.includes('.') ? path.slice(path.lastIndexOf('.')) : '';
|
||||||
|
|
||||||
|
// First check excluded extensions
|
||||||
|
if (ext && checkpointConfig.HTMLCheckpointExcludedExtensions.includes(ext)) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Then check if we should only include specific extensions
|
||||||
|
if (checkpointConfig.HTMLCheckpointIncludedExtensions.length > 0) {
|
||||||
|
// If extension list is specified and current extension is not in it, skip
|
||||||
|
if (!checkpointConfig.HTMLCheckpointIncludedExtensions.includes(ext)) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate session token
|
||||||
|
const cookies = cookie.parse(request.headers.get('cookie') || '');
|
||||||
|
const tokenCookie = cookies[checkpointConfig.CookieName];
|
||||||
|
const validation = await validateToken(tokenCookie, request);
|
||||||
|
if (validation) {
|
||||||
|
// Active session: bypass checkpoint
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Log new checkpoint flow
|
||||||
|
console.log(`checkpoint: incoming ${method} ${request.url}`);
|
||||||
|
console.log(`checkpoint: tokenCookie=${tokenCookie}`);
|
||||||
|
console.log(`checkpoint: validateToken => ${validation}`);
|
||||||
|
|
||||||
|
// Serve interstitial challenge
|
||||||
|
return serveInterstitial(request);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async function addToken(tokenHash, data) {
|
||||||
|
if (!db) return;
|
||||||
|
try {
|
||||||
|
const ttlMs = checkpointConfig.TokenExpiration;
|
||||||
|
|
||||||
|
await db.put(tokenHash, data);
|
||||||
|
|
||||||
|
tokenExpirations.set(tokenHash, Date.now() + ttlMs);
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Error adding token:', err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function updateTokenVerification(tokenHash) {
|
||||||
|
if (!db) return;
|
||||||
|
try {
|
||||||
|
const data = await db.get(tokenHash);
|
||||||
|
data.LastVerified = new Date().toISOString();
|
||||||
|
await db.put(tokenHash, data);
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Error updating token verification:', err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function lookupTokenData(tokenHash) {
|
||||||
|
if (!db) return { data: null, found: false };
|
||||||
|
try {
|
||||||
|
const expireTime = tokenExpirations.get(tokenHash);
|
||||||
|
if (!expireTime || expireTime <= Date.now()) {
|
||||||
|
if (expireTime) {
|
||||||
|
tokenExpirations.delete(tokenHash);
|
||||||
|
try {
|
||||||
|
await db.del(tokenHash);
|
||||||
|
} catch (e) {}
|
||||||
|
}
|
||||||
|
return { data: null, found: false };
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await db.get(tokenHash);
|
||||||
|
return { data, found: true };
|
||||||
|
} catch (err) {
|
||||||
|
if (err.code === 'LEVEL_NOT_FOUND') return { data: null, found: false };
|
||||||
|
console.error('Error looking up token:', err);
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function closeTokenStore() {
|
||||||
|
if (db) await db.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
function startCleanupTimer() {
|
||||||
|
// Cleanup expired data hourly
|
||||||
|
setInterval(() => {
|
||||||
|
cleanupExpiredData();
|
||||||
|
}, 3600000);
|
||||||
|
// Cleanup expired challenges at the challenge expiration interval
|
||||||
|
const challengeInterval = checkpointConfig.ChallengeExpiration || 60000;
|
||||||
|
setInterval(() => {
|
||||||
|
cleanupExpiredChallenges();
|
||||||
|
}, challengeInterval);
|
||||||
|
}
|
||||||
|
|
||||||
|
function cleanupExpiredData() {
|
||||||
|
const now = Date.now();
|
||||||
|
let count = 0;
|
||||||
|
|
||||||
|
try {
|
||||||
|
for (const [nonce, ts] of usedNonces.entries()) {
|
||||||
|
if (now - ts > checkpointConfig.MaxNonceAge) {
|
||||||
|
usedNonces.delete(nonce);
|
||||||
|
count++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (count) console.log(`Checkpoint: cleaned up ${count} expired nonces.`);
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Error cleaning up nonces:', err);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clean up expired tokens from cache
|
||||||
|
let tokenCacheCount = 0;
|
||||||
|
try {
|
||||||
|
for (const [tokenKey, _] of tokenCache.entries()) {
|
||||||
|
const expireTime = tokenExpirations.get(tokenKey);
|
||||||
|
if (!expireTime || expireTime <= now) {
|
||||||
|
tokenCache.delete(tokenKey);
|
||||||
|
tokenExpirations.delete(tokenKey);
|
||||||
|
tokenCacheCount++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (tokenCacheCount)
|
||||||
|
console.log(`Checkpoint: cleaned up ${tokenCacheCount} expired tokens from cache.`);
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Error cleaning up token cache:', err);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
ipRateLimit.clear();
|
||||||
|
console.log('Checkpoint: IP rate limits reset.');
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Error resetting IP rate limits:', err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function cleanupExpiredChallenges() {
|
||||||
|
const now = Date.now();
|
||||||
|
let count = 0;
|
||||||
|
for (const [id, params] of challengeStore.entries()) {
|
||||||
|
if (params.ExpiresAt && params.ExpiresAt < now) {
|
||||||
|
// Record failure for expired challenges that were never completed
|
||||||
|
safeRecordEvent('checkpoint.failure', {
|
||||||
|
reason: 'challenge_expired',
|
||||||
|
ip: params.ClientIP,
|
||||||
|
challenge_id: id.substring(0, 8), // Include partial ID for debugging
|
||||||
|
age_ms: now - params.CreatedAt, // How long the challenge existed
|
||||||
|
expiry_ms: checkpointConfig.ChallengeExpiration, // Configured expiry time
|
||||||
|
});
|
||||||
|
challengeStore.delete(id);
|
||||||
|
count++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (count) console.log(`Checkpoint: cleaned up ${count} expired challenges.`);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function initSecret() {
|
||||||
|
try {
|
||||||
|
if (!checkpointConfig.SecretConfigPath) {
|
||||||
|
checkpointConfig.SecretConfigPath = join(rootDir, 'data', 'checkpoint_secret.json');
|
||||||
|
}
|
||||||
|
|
||||||
|
const secretPath = checkpointConfig.SecretConfigPath;
|
||||||
|
const exists = fs.existsSync(secretPath);
|
||||||
|
|
||||||
|
if (exists) {
|
||||||
|
const loaded = loadSecretFromFile();
|
||||||
|
if (loaded) {
|
||||||
|
hmacSecret = loaded;
|
||||||
|
console.log(`Loaded existing HMAC secret from ${secretPath}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
hmacSecret = crypto.randomBytes(32);
|
||||||
|
fs.mkdirSync(path.dirname(secretPath), { recursive: true });
|
||||||
|
|
||||||
|
const secretCfg = {
|
||||||
|
hmac_secret: hmacSecret.toString('base64'),
|
||||||
|
created_at: new Date().toISOString(),
|
||||||
|
updated_at: new Date().toISOString(),
|
||||||
|
};
|
||||||
|
|
||||||
|
fs.writeFileSync(secretPath, JSON.stringify(secretCfg), { mode: 0o600 });
|
||||||
|
console.log(`Created and saved new HMAC secret to ${secretPath}`);
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Error initializing secret:', err);
|
||||||
|
|
||||||
|
hmacSecret = crypto.randomBytes(32);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadSecretFromFile() {
|
||||||
|
try {
|
||||||
|
const data = fs.readFileSync(checkpointConfig.SecretConfigPath, 'utf8');
|
||||||
|
const cfg = JSON.parse(data);
|
||||||
|
const buf = Buffer.from(cfg.hmac_secret, 'base64');
|
||||||
|
if (buf.length < 16) return null;
|
||||||
|
|
||||||
|
cfg.updated_at = new Date().toISOString();
|
||||||
|
fs.writeFileSync(checkpointConfig.SecretConfigPath, JSON.stringify(cfg), { mode: 0o600 });
|
||||||
|
return buf;
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('Could not load HMAC secret from file:', e);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
(async function initialize() {
|
||||||
|
await initConfig();
|
||||||
|
await initSecret();
|
||||||
|
initTokenStore();
|
||||||
|
startCleanupTimer();
|
||||||
|
|
||||||
|
// Only register plugin if enabled
|
||||||
|
if (checkpointConfig.Enabled !== false) {
|
||||||
|
registerPlugin('checkpoint', CheckpointMiddleware());
|
||||||
|
} else {
|
||||||
|
console.log('Checkpoint plugin disabled via configuration');
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
|
||||||
|
export { checkpointConfig, addToken, updateTokenVerification, lookupTokenData, closeTokenStore };
|
||||||
143
config/checkpoint.toml
Normal file
143
config/checkpoint.toml
Normal file
|
|
@ -0,0 +1,143 @@
|
||||||
|
# =============================================================================
|
||||||
|
# CHECKPOINT SECURITY CONFIGURATION
|
||||||
|
# =============================================================================
|
||||||
|
# This configuration controls the checkpoint security middleware that protects
|
||||||
|
# your services with proof-of-work challenges and token-based authentication.
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
# CORE SETTINGS
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
[Core]
|
||||||
|
# Enable or disable the checkpoint system entirely
|
||||||
|
Enabled = true
|
||||||
|
|
||||||
|
# Cookie name for storing checkpoint tokens
|
||||||
|
CookieName = "checkpoint_token"
|
||||||
|
|
||||||
|
# Cookie domain (empty = host-only cookie for localhost)
|
||||||
|
# Set to ".yourdomain.com" for all subdomains
|
||||||
|
CookieDomain = ""
|
||||||
|
|
||||||
|
# Enable URL path sanitization to prevent path traversal attacks
|
||||||
|
SanitizeURLs = true
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
# PROOF OF WORK SETTINGS
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
[ProofOfWork]
|
||||||
|
# Number of leading zeros required in the SHA-256 hash
|
||||||
|
Difficulty = 4
|
||||||
|
|
||||||
|
# Random salt length in bytes
|
||||||
|
SaltLength = 16
|
||||||
|
|
||||||
|
# Time allowed to solve a challenge before it expires
|
||||||
|
ChallengeExpiration = "3m"
|
||||||
|
|
||||||
|
# Maximum attempts per IP address per hour
|
||||||
|
MaxAttemptsPerHour = 10
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
# PROOF OF SPACE-TIME SETTINGS (Optional additional verification)
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
[ProofOfSpaceTime]
|
||||||
|
# Enable consistency checks for PoS-Time verification
|
||||||
|
Enabled = true
|
||||||
|
|
||||||
|
# Maximum allowed ratio between slowest and fastest PoS runs
|
||||||
|
ConsistencyRatio = 1.35
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
# TOKEN SETTINGS
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
[Token]
|
||||||
|
# How long tokens remain valid
|
||||||
|
Expiration = "24h"
|
||||||
|
|
||||||
|
# Maximum age for used nonces before cleanup
|
||||||
|
MaxNonceAge = "24h"
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
# STORAGE PATHS
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
[Storage]
|
||||||
|
# HMAC secret storage location
|
||||||
|
SecretPath = "./data/checkpoint_secret.json"
|
||||||
|
|
||||||
|
# Token database directory
|
||||||
|
TokenDBPath = "./db/tokenstore"
|
||||||
|
|
||||||
|
# Interstitial page templates (in order of preference)
|
||||||
|
InterstitialTemplates = [
|
||||||
|
"/pages/interstitial/page.html",
|
||||||
|
"/pages/ipfilter/default.html"
|
||||||
|
]
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
# EXCLUSION RULES
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
# Define which requests should bypass the checkpoint system.
|
||||||
|
# Each rule can specify:
|
||||||
|
# - Path (required): URL path or prefix to match
|
||||||
|
# - Hosts (optional): Specific hostnames this rule applies to
|
||||||
|
# - UserAgents (optional): User-Agent patterns to match
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
[[Exclusion]]
|
||||||
|
# Skip checkpoint for all API endpoints (required for Immich and similar apps)
|
||||||
|
Path = "/api"
|
||||||
|
Hosts = ["gallery.caileb.com"] # Optional: only for specific hosts
|
||||||
|
|
||||||
|
[[Exclusion]]
|
||||||
|
# Skip checkpoint for health checks
|
||||||
|
Path = "/health"
|
||||||
|
|
||||||
|
[[Exclusion]]
|
||||||
|
# Skip checkpoint for metrics endpoint
|
||||||
|
Path = "/metrics"
|
||||||
|
|
||||||
|
# [[Exclusion]]
|
||||||
|
# Example: Mobile app API with specific user agent
|
||||||
|
# Path = "/mobile-api"
|
||||||
|
# UserAgents = ["MyApp/", "Dart/"]
|
||||||
|
|
||||||
|
# [[Exclusion]]
|
||||||
|
# Example: Host-specific exclusion
|
||||||
|
# Path = "/admin"
|
||||||
|
# Hosts = ["admin.internal.com"]
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
# BYPASS KEYS
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
# Special keys that can bypass the checkpoint when provided
|
||||||
|
|
||||||
|
[[BypassKeys]]
|
||||||
|
# Query parameter bypass
|
||||||
|
Type = "query"
|
||||||
|
Key = "bypass_key"
|
||||||
|
Value = "your-secret-key-here"
|
||||||
|
Hosts = ["music.caileb.com"] # Optional: restrict to specific hosts
|
||||||
|
|
||||||
|
[[BypassKeys]]
|
||||||
|
# Header bypass
|
||||||
|
Type = "header"
|
||||||
|
Key = "X-Bypass-Token"
|
||||||
|
Value = "another-secret-key"
|
||||||
|
# Hosts = [] # If empty or omitted, applies to all hosts
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
# FILE EXTENSION HANDLING
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
[Extensions]
|
||||||
|
# Only apply checkpoint to these file extensions (for HTML content)
|
||||||
|
# Empty = check all paths
|
||||||
|
IncludeOnly = [".html", ".htm", ".shtml", ""]
|
||||||
|
|
||||||
|
# Never apply checkpoint to these file extensions
|
||||||
|
# This takes precedence over IncludeOnly
|
||||||
|
Exclude = [
|
||||||
|
".js", ".css", ".png", ".jpg", ".jpeg", ".gif", ".svg",
|
||||||
|
".ico", ".woff", ".woff2", ".ttf", ".eot", ".map",
|
||||||
|
".json", ".xml", ".txt", ".webp", ".avif"
|
||||||
|
]
|
||||||
92
config/ipfilter.toml
Normal file
92
config/ipfilter.toml
Normal file
|
|
@ -0,0 +1,92 @@
|
||||||
|
# =============================================================================
|
||||||
|
# IP FILTER CONFIGURATION
|
||||||
|
# =============================================================================
|
||||||
|
# This configuration controls the IP filtering middleware that blocks requests
|
||||||
|
# based on geographic location (country/continent) and network (ASN) information.
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
# CORE SETTINGS
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
[Core]
|
||||||
|
# Enable or disable the IP filter entirely
|
||||||
|
Enabled = true
|
||||||
|
|
||||||
|
# MaxMind account ID for downloading GeoIP databases
|
||||||
|
# Can also be set via MAXMIND_ACCOUNT_ID environment variable or .env file
|
||||||
|
AccountID = ""
|
||||||
|
|
||||||
|
# MaxMind license key for downloading GeoIP databases
|
||||||
|
# Can also be set via MAXMIND_LICENSE_KEY environment variable or .env file
|
||||||
|
LicenseKey = ""
|
||||||
|
|
||||||
|
# How often to check for database updates (in hours)
|
||||||
|
DBUpdateIntervalHours = 12
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
# CACHING SETTINGS
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
[Cache]
|
||||||
|
# TTL for cached IP block decisions (in seconds)
|
||||||
|
# 0 = cache indefinitely until server restart
|
||||||
|
IPBlockCacheTTLSec = 300
|
||||||
|
|
||||||
|
# Maximum number of cached IP decisions
|
||||||
|
# 0 = unlimited
|
||||||
|
IPBlockCacheMaxEntries = 10000
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
# BLOCKING RULES
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
[Blocking]
|
||||||
|
# ISO country codes to block (2-letter codes)
|
||||||
|
CountryCodes = [
|
||||||
|
"IN", "BH", "AE", "OM", "QA", "KW", "SA", "YE", "IR", "IQ",
|
||||||
|
"LB", "PS", "CY", "TR", "AZ", "AM", "TM", "UZ", "KZ", "KG",
|
||||||
|
"TJ", "KE", "ET", "SO", "SD", "SS", "KP", "UA", "IL"
|
||||||
|
]
|
||||||
|
|
||||||
|
# Continent codes to block
|
||||||
|
ContinentCodes = ["AF", "SA", "AS", "AN"]
|
||||||
|
|
||||||
|
# Default block page when no specific page is configured
|
||||||
|
DefaultBlockPage = "/pages/ipfilter/default.html"
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
# ASN BLOCKING
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
# Block by Autonomous System Number (ASN)
|
||||||
|
# Group ASNs by category for different block pages
|
||||||
|
|
||||||
|
# [ASN.Example]
|
||||||
|
# Numbers = [12345, 67890]
|
||||||
|
# BlockPage = "pages/ipfilter/example.html"
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
# ASN NAME BLOCKING
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
# Block by ASN organization name patterns
|
||||||
|
|
||||||
|
[ASNNames.DataCenter]
|
||||||
|
# Block data center and cloud providers
|
||||||
|
Patterns = [
|
||||||
|
"Cloudflare", "GOOGLE-CLOUD-PLATFORM", "Microsoft", "Amazon", "AWS",
|
||||||
|
"Digitalocean", "OVH", "HUAWEI CLOUDS", "HWCLOUDS", "M247",
|
||||||
|
"Datacamp", "Datapacket", "Amanah", "Hern Labs"
|
||||||
|
]
|
||||||
|
BlockPage = "/pages/ipfilter/datacenter.html"
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
# COUNTRY-SPECIFIC BLOCK PAGES
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
[CountryBlockPages]
|
||||||
|
# Custom block pages for specific countries
|
||||||
|
IN = "/pages/ipfilter/india.html"
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
# CONTINENT-SPECIFIC BLOCK PAGES
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
[ContinentBlockPages]
|
||||||
|
# Custom block pages for specific continents
|
||||||
|
# AS = "pages/ipfilter/asia.html"
|
||||||
|
# AF = "pages/ipfilter/africa.html"
|
||||||
55
config/proxy.toml
Normal file
55
config/proxy.toml
Normal file
|
|
@ -0,0 +1,55 @@
|
||||||
|
# =============================================================================
|
||||||
|
# PROXY CONFIGURATION
|
||||||
|
# =============================================================================
|
||||||
|
# This configuration controls the reverse proxy middleware that forwards
|
||||||
|
# requests to backend services based on hostname mappings.
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
# CORE SETTINGS
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
[Core]
|
||||||
|
# Enable or disable the proxy middleware
|
||||||
|
Enabled = true
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
# TIMEOUT SETTINGS
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
[Timeouts]
|
||||||
|
# WebSocket connection timeout in milliseconds
|
||||||
|
WebSocketTimeoutMs = 5000
|
||||||
|
|
||||||
|
# Upstream HTTP request timeout in milliseconds
|
||||||
|
UpstreamTimeoutMs = 30000
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
# PROXY MAPPINGS
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
# Map hostnames to backend service URLs
|
||||||
|
# Format: "hostname" = "backend_url"
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
[[Mapping]]
|
||||||
|
# Immich
|
||||||
|
Host = "gallery.caileb.com"
|
||||||
|
Target = "http://192.168.0.2:2283"
|
||||||
|
|
||||||
|
[[Mapping]]
|
||||||
|
# Navidrome
|
||||||
|
Host = "music.caileb.com"
|
||||||
|
Target = "http://192.168.0.2:4533"
|
||||||
|
|
||||||
|
[[Mapping]]
|
||||||
|
# ForgeJo
|
||||||
|
Host = "git.caileb.com"
|
||||||
|
Target = "http://192.168.0.2:3053"
|
||||||
|
|
||||||
|
# [[Mapping]]
|
||||||
|
# Example: API service
|
||||||
|
# Host = "api.example.com"
|
||||||
|
# Target = "http://localhost:3001"
|
||||||
|
|
||||||
|
# [[Mapping]]
|
||||||
|
# Example: Admin panel
|
||||||
|
# Host = "admin.example.com"
|
||||||
|
# Target = "http://localhost:3002"
|
||||||
31
config/stats.toml
Normal file
31
config/stats.toml
Normal file
|
|
@ -0,0 +1,31 @@
|
||||||
|
# =============================================================================
|
||||||
|
# STATS CONFIGURATION
|
||||||
|
# =============================================================================
|
||||||
|
# This configuration controls the statistics collection and visualization
|
||||||
|
# middleware that tracks events and provides a web UI for viewing metrics.
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
# CORE SETTINGS
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
[Core]
|
||||||
|
# Enable or disable the stats plugin
|
||||||
|
Enabled = true
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
# STORAGE SETTINGS
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
[Storage]
|
||||||
|
# TTL for stats entries
|
||||||
|
# Format: "30d", "24h", "1h", etc.
|
||||||
|
StatsTTL = "30d"
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
# WEB UI SETTINGS
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
[WebUI]
|
||||||
|
# Path for stats UI
|
||||||
|
StatsUIPath = "/stats"
|
||||||
|
|
||||||
|
# Path for stats API
|
||||||
|
StatsAPIPath = "/stats/api"
|
||||||
197
index.js
Normal file
197
index.js
Normal file
|
|
@ -0,0 +1,197 @@
|
||||||
|
import { mkdir, readFile } from 'fs/promises';
|
||||||
|
import { writeFileSync, readFileSync, unlinkSync, existsSync } from 'fs';
|
||||||
|
import { join, dirname } from 'path';
|
||||||
|
import { fileURLToPath } from 'url';
|
||||||
|
import { secureImportModule } from './utils/plugins.js';
|
||||||
|
import * as logs from './utils/logs.js';
|
||||||
|
|
||||||
|
// Load environment variables from .env file
|
||||||
|
import dotenv from 'dotenv';
|
||||||
|
dotenv.config();
|
||||||
|
|
||||||
|
// Stop daemon: if run with -k, kill the running process and exit.
|
||||||
|
if (process.argv.includes('-k')) {
|
||||||
|
const pidFile = join(dirname(fileURLToPath(import.meta.url)), 'checkpoint.pid');
|
||||||
|
if (existsSync(pidFile)) {
|
||||||
|
const pid = parseInt(readFileSync(pidFile, 'utf8'), 10);
|
||||||
|
try {
|
||||||
|
process.kill(pid);
|
||||||
|
unlinkSync(pidFile);
|
||||||
|
console.log(`Stopped daemon (pid ${pid})`);
|
||||||
|
} catch (err) {
|
||||||
|
console.error(`Failed to stop pid ${pid}: ${err}`);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.error(`No pid file found at ${pidFile}`);
|
||||||
|
}
|
||||||
|
process.exit(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Daemonize: if run with -d, kill any existing daemon, then re-spawn detached, write pid file, and exit parent.
|
||||||
|
if (process.argv.includes('-d')) {
|
||||||
|
const pidFile = join(dirname(fileURLToPath(import.meta.url)), 'checkpoint.pid');
|
||||||
|
// If already running, stop the old daemon
|
||||||
|
if (existsSync(pidFile)) {
|
||||||
|
const oldPid = parseInt(readFileSync(pidFile, 'utf8'), 10);
|
||||||
|
try {
|
||||||
|
process.kill(oldPid);
|
||||||
|
console.log(`Stopped old daemon (pid ${oldPid})`);
|
||||||
|
} catch (e) {
|
||||||
|
console.error(`Failed to stop old daemon (pid ${oldPid}): ${e}`);
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
unlinkSync(pidFile);
|
||||||
|
} catch {}
|
||||||
|
}
|
||||||
|
// Spawn new background process
|
||||||
|
const args = process.argv.slice(1).filter((arg) => arg !== '-d');
|
||||||
|
const cp = Bun.spawn({
|
||||||
|
cmd: [process.argv[0], ...args],
|
||||||
|
detached: true,
|
||||||
|
stdio: ['ignore', 'ignore', 'ignore'],
|
||||||
|
});
|
||||||
|
cp.unref();
|
||||||
|
writeFileSync(pidFile, cp.pid.toString(), 'utf8');
|
||||||
|
console.log(`Daemonized (pid ${cp.pid}), pid stored in ${pidFile}`);
|
||||||
|
process.exit(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
const pluginRegistry = [];
|
||||||
|
export function registerPlugin(pluginName, handler) {
|
||||||
|
pluginRegistry.push({ name: pluginName, handler });
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Return the array of middleware handlers in registration order.
|
||||||
|
*/
|
||||||
|
export function loadPlugins() {
|
||||||
|
return pluginRegistry.map((item) => item.handler);
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Return the names of all registered plugins.
|
||||||
|
*/
|
||||||
|
export function getRegisteredPluginNames() {
|
||||||
|
return pluginRegistry.map((item) => item.name);
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Freeze plugin registry to prevent further registration and log the final set.
|
||||||
|
*/
|
||||||
|
export function freezePlugins() {
|
||||||
|
Object.freeze(pluginRegistry);
|
||||||
|
pluginRegistry.forEach((item) => Object.freeze(item));
|
||||||
|
logs.msg('Plugin registration frozen');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Determine root directory for config loading
|
||||||
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||||
|
export const rootDir = __dirname;
|
||||||
|
|
||||||
|
export async function loadConfig(name, target) {
|
||||||
|
const configPath = join(rootDir, 'config', `${name}.toml`);
|
||||||
|
const txt = await readFile(configPath, 'utf8');
|
||||||
|
const { default: toml } = await import('@iarna/toml');
|
||||||
|
Object.assign(target, toml.parse(txt));
|
||||||
|
logs.config(name, 'loaded');
|
||||||
|
}
|
||||||
|
|
||||||
|
async function initDataDirectories() {
|
||||||
|
logs.section('INIT');
|
||||||
|
const directories = [join(rootDir, 'data'), join(rootDir, 'db'), join(rootDir, 'config')];
|
||||||
|
for (const dirPath of directories) {
|
||||||
|
try {
|
||||||
|
await mkdir(dirPath, { recursive: true });
|
||||||
|
} catch {}
|
||||||
|
}
|
||||||
|
logs.init('Data directories are now in place');
|
||||||
|
}
|
||||||
|
|
||||||
|
function staticFileMiddleware() {
|
||||||
|
return async (request) => {
|
||||||
|
const url = new URL(request.url);
|
||||||
|
const pathname = url.pathname;
|
||||||
|
if (pathname.startsWith('/webfont/') || pathname.startsWith('/js/')) {
|
||||||
|
const filePath = join(rootDir, 'pages/interstitial', pathname.slice(1));
|
||||||
|
try {
|
||||||
|
return new Response(Bun.file(filePath), {
|
||||||
|
headers: { 'Cache-Control': 'public, max-age=604800' },
|
||||||
|
});
|
||||||
|
} catch {
|
||||||
|
return new Response('Not Found', { status: 404 });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
await initDataDirectories();
|
||||||
|
|
||||||
|
logs.section('CONFIG');
|
||||||
|
logs.config('checkpoint', 'loaded');
|
||||||
|
logs.config('ipfilter', 'loaded');
|
||||||
|
logs.config('proxy', 'loaded');
|
||||||
|
logs.config('stats', 'loaded');
|
||||||
|
|
||||||
|
logs.section('OPERATIONS');
|
||||||
|
let wsHandler;
|
||||||
|
try {
|
||||||
|
await secureImportModule('checkpoint.js');
|
||||||
|
} catch (e) {
|
||||||
|
logs.error('checkpoint', `Failed to load checkpoint plugin: ${e}`);
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
await secureImportModule('plugins/ipfilter.js');
|
||||||
|
} catch (e) {
|
||||||
|
logs.error('ipfilter', `Failed to load IP filter plugin: ${e}`);
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
await secureImportModule('plugins/proxy.js');
|
||||||
|
const mod = await import('./plugins/proxy.js');
|
||||||
|
wsHandler = mod.proxyWebSocketHandler;
|
||||||
|
} catch (e) {
|
||||||
|
logs.error('proxy', `Failed to load proxy plugin: ${e}`);
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
await secureImportModule('plugins/stats.js');
|
||||||
|
} catch (e) {
|
||||||
|
logs.error('stats', `Failed to load stats plugin: ${e}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
registerPlugin('static', staticFileMiddleware());
|
||||||
|
|
||||||
|
logs.section('PLUGINS');
|
||||||
|
// Ensure ipfilter runs first by moving it to front of the registry
|
||||||
|
const ipIndex = pluginRegistry.findIndex((item) => item.name === 'ipfilter');
|
||||||
|
if (ipIndex > 0) {
|
||||||
|
const [ipEntry] = pluginRegistry.splice(ipIndex, 1);
|
||||||
|
pluginRegistry.unshift(ipEntry);
|
||||||
|
}
|
||||||
|
pluginRegistry.forEach((item) => logs.msg(item.name));
|
||||||
|
logs.section('SYSTEM');
|
||||||
|
freezePlugins();
|
||||||
|
|
||||||
|
logs.section('SERVER');
|
||||||
|
const portNumber = Number(process.env.PORT || 3000);
|
||||||
|
const middlewareHandlers = loadPlugins();
|
||||||
|
logs.server(`🚀 Server is up and running on port ${portNumber}...`);
|
||||||
|
logs.section('REQ LOGS');
|
||||||
|
Bun.serve({
|
||||||
|
port: portNumber,
|
||||||
|
async fetch(request, server) {
|
||||||
|
for (const handler of middlewareHandlers) {
|
||||||
|
try {
|
||||||
|
const resp = await handler(request, server);
|
||||||
|
if (resp instanceof Response) return resp;
|
||||||
|
} catch (err) {
|
||||||
|
logs.error('server', `Handler error: ${err}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return new Response('Not Found', { status: 404 });
|
||||||
|
},
|
||||||
|
websocket: wsHandler,
|
||||||
|
error(err) {
|
||||||
|
return new Response(`Server Error: ${err.message}`, { status: 500 });
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
main();
|
||||||
728
package-lock.json
generated
Normal file
728
package-lock.json
generated
Normal file
|
|
@ -0,0 +1,728 @@
|
||||||
|
{
|
||||||
|
"name": "checkpoint",
|
||||||
|
"lockfileVersion": 3,
|
||||||
|
"requires": true,
|
||||||
|
"packages": {
|
||||||
|
"": {
|
||||||
|
"name": "checkpoint",
|
||||||
|
"dependencies": {
|
||||||
|
"@iarna/toml": "^2.2.5",
|
||||||
|
"cookie": "^1.0.2",
|
||||||
|
"dotenv": "^16.5.0",
|
||||||
|
"express-http-proxy": "^2.1.1",
|
||||||
|
"http-proxy": "^1.18.1",
|
||||||
|
"level": "^10.0.0",
|
||||||
|
"level-ttl": "^3.1.1",
|
||||||
|
"maxmind": "^4.3.25",
|
||||||
|
"string-dsa": "^2.1.0",
|
||||||
|
"tar": "^7.4.3",
|
||||||
|
"tar-stream": "^3.1.7",
|
||||||
|
"toml": "^3.0.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"prettier": "^2.8.8"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@iarna/toml": {
|
||||||
|
"version": "2.2.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@iarna/toml/-/toml-2.2.5.tgz",
|
||||||
|
"integrity": "sha512-trnsAYxU3xnS1gPHPyU961coFyLkh4gAD/0zQ5mymY4yOZ+CYvsPqUbOFSw0aDM4y0tV7tiFxL/1XfXPNC6IPg==",
|
||||||
|
"license": "ISC"
|
||||||
|
},
|
||||||
|
"node_modules/@isaacs/fs-minipass": {
|
||||||
|
"version": "4.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@isaacs/fs-minipass/-/fs-minipass-4.0.1.tgz",
|
||||||
|
"integrity": "sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==",
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"minipass": "^7.0.4"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/abstract-level": {
|
||||||
|
"version": "3.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/abstract-level/-/abstract-level-3.1.0.tgz",
|
||||||
|
"integrity": "sha512-j2e+TsAxy7Ri+0h7dJqwasymgt0zHBWX4+nMk3XatyuqgHfdstBJ9wsMfbiGwE1O+QovRyPcVAqcViMYdyPaaw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"buffer": "^6.0.3",
|
||||||
|
"is-buffer": "^2.0.5",
|
||||||
|
"level-supports": "^6.2.0",
|
||||||
|
"level-transcoder": "^1.0.1",
|
||||||
|
"maybe-combine-errors": "^1.0.0",
|
||||||
|
"module-error": "^1.0.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/after": {
|
||||||
|
"version": "0.8.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/after/-/after-0.8.2.tgz",
|
||||||
|
"integrity": "sha512-QbJ0NTQ/I9DI3uSJA4cbexiwQeRAfjPScqIbSjUDd9TOrcg6pTkdgziesOqxBMBzit8vFCTwrP27t13vFOORRA==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/b4a": {
|
||||||
|
"version": "1.6.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/b4a/-/b4a-1.6.7.tgz",
|
||||||
|
"integrity": "sha512-OnAYlL5b7LEkALw87fUVafQw5rVR9RjwGd4KUwNQ6DrrNmaVaUCgLipfVlzrPQ4tWOR9P0IXGNOx50jYCCdSJg==",
|
||||||
|
"license": "Apache-2.0"
|
||||||
|
},
|
||||||
|
"node_modules/bare-events": {
|
||||||
|
"version": "2.5.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/bare-events/-/bare-events-2.5.4.tgz",
|
||||||
|
"integrity": "sha512-+gFfDkR8pj4/TrWCGUGWmJIkBwuxPS5F+a5yWjOHQt2hHvNZd5YLzadjmDUtFmMM4y429bnKLa8bYBMHcYdnQA==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"node_modules/base64-js": {
|
||||||
|
"version": "1.5.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
|
||||||
|
"integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==",
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/feross"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "patreon",
|
||||||
|
"url": "https://www.patreon.com/feross"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "consulting",
|
||||||
|
"url": "https://feross.org/support"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/browser-level": {
|
||||||
|
"version": "3.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/browser-level/-/browser-level-3.0.0.tgz",
|
||||||
|
"integrity": "sha512-kGXtLh29jMwqKaskz5xeDLtCtN1KBz/DbQSqmvH7QdJiyGRC7RAM8PPg6gvUiNMa+wVnaxS9eSmEtP/f5ajOVw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"abstract-level": "^3.1.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/buffer": {
|
||||||
|
"version": "6.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz",
|
||||||
|
"integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==",
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/feross"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "patreon",
|
||||||
|
"url": "https://www.patreon.com/feross"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "consulting",
|
||||||
|
"url": "https://feross.org/support"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"base64-js": "^1.3.1",
|
||||||
|
"ieee754": "^1.2.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/bytes": {
|
||||||
|
"version": "3.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz",
|
||||||
|
"integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.8"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/chownr": {
|
||||||
|
"version": "3.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/chownr/-/chownr-3.0.0.tgz",
|
||||||
|
"integrity": "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==",
|
||||||
|
"license": "BlueOak-1.0.0",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/classic-level": {
|
||||||
|
"version": "3.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/classic-level/-/classic-level-3.0.0.tgz",
|
||||||
|
"integrity": "sha512-yGy8j8LjPbN0Bh3+ygmyYvrmskVita92pD/zCoalfcC9XxZj6iDtZTAnz+ot7GG8p9KLTG+MZ84tSA4AhkgVZQ==",
|
||||||
|
"hasInstallScript": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"abstract-level": "^3.1.0",
|
||||||
|
"module-error": "^1.0.1",
|
||||||
|
"napi-macros": "^2.2.2",
|
||||||
|
"node-gyp-build": "^4.3.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/cookie": {
|
||||||
|
"version": "1.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/cookie/-/cookie-1.0.2.tgz",
|
||||||
|
"integrity": "sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/core-util-is": {
|
||||||
|
"version": "1.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz",
|
||||||
|
"integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/debug": {
|
||||||
|
"version": "3.2.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz",
|
||||||
|
"integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"ms": "^2.1.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/depd": {
|
||||||
|
"version": "2.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
|
||||||
|
"integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.8"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/dotenv": {
|
||||||
|
"version": "16.5.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.5.0.tgz",
|
||||||
|
"integrity": "sha512-m/C+AwOAr9/W1UOIZUo232ejMNnJAJtYQjUbHoNTBNTJSvqzzDh7vnrei3o3r3m9blf6ZoDkvcw0VmozNRFJxg==",
|
||||||
|
"license": "BSD-2-Clause",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://dotenvx.com"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/es6-promise": {
|
||||||
|
"version": "4.2.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-4.2.8.tgz",
|
||||||
|
"integrity": "sha512-HJDGx5daxeIvxdBxvG2cb9g4tEvwIk3i8+nhX0yGrYmZUzbkdg8QbDevheDB8gd0//uPj4c1EQua8Q+MViT0/w==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/eventemitter3": {
|
||||||
|
"version": "4.0.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz",
|
||||||
|
"integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/express-http-proxy": {
|
||||||
|
"version": "2.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/express-http-proxy/-/express-http-proxy-2.1.1.tgz",
|
||||||
|
"integrity": "sha512-4aRQRqDQU7qNPV5av0/hLcyc0guB9UP71nCYrQEYml7YphTo8tmWf3nDZWdTJMMjFikyz9xKXaURor7Chygdwg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"debug": "^3.0.1",
|
||||||
|
"es6-promise": "^4.1.1",
|
||||||
|
"raw-body": "^2.3.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/fast-fifo": {
|
||||||
|
"version": "1.3.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/fast-fifo/-/fast-fifo-1.3.2.tgz",
|
||||||
|
"integrity": "sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/follow-redirects": {
|
||||||
|
"version": "1.15.9",
|
||||||
|
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz",
|
||||||
|
"integrity": "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==",
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"type": "individual",
|
||||||
|
"url": "https://github.com/sponsors/RubenVerborgh"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=4.0"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"debug": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/http-errors": {
|
||||||
|
"version": "2.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz",
|
||||||
|
"integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"depd": "2.0.0",
|
||||||
|
"inherits": "2.0.4",
|
||||||
|
"setprototypeof": "1.2.0",
|
||||||
|
"statuses": "2.0.1",
|
||||||
|
"toidentifier": "1.0.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.8"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/http-proxy": {
|
||||||
|
"version": "1.18.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/http-proxy/-/http-proxy-1.18.1.tgz",
|
||||||
|
"integrity": "sha512-7mz/721AbnJwIVbnaSv1Cz3Am0ZLT/UBwkC92VlxhXv/k/BBQfM2fXElQNC27BVGr0uwUpplYPQM9LnaBMR5NQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"eventemitter3": "^4.0.0",
|
||||||
|
"follow-redirects": "^1.0.0",
|
||||||
|
"requires-port": "^1.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/iconv-lite": {
|
||||||
|
"version": "0.4.24",
|
||||||
|
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
|
||||||
|
"integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"safer-buffer": ">= 2.1.2 < 3"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.10.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/ieee754": {
|
||||||
|
"version": "1.2.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz",
|
||||||
|
"integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==",
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/feross"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "patreon",
|
||||||
|
"url": "https://www.patreon.com/feross"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "consulting",
|
||||||
|
"url": "https://feross.org/support"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"license": "BSD-3-Clause"
|
||||||
|
},
|
||||||
|
"node_modules/inherits": {
|
||||||
|
"version": "2.0.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
|
||||||
|
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
|
||||||
|
"license": "ISC"
|
||||||
|
},
|
||||||
|
"node_modules/is-buffer": {
|
||||||
|
"version": "2.0.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-2.0.5.tgz",
|
||||||
|
"integrity": "sha512-i2R6zNFDwgEHJyQUtJEk0XFi1i0dPFn/oqjK3/vPCcDeJvW5NQ83V8QbicfF1SupOaB0h8ntgBC2YiE7dfyctQ==",
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/feross"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "patreon",
|
||||||
|
"url": "https://www.patreon.com/feross"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "consulting",
|
||||||
|
"url": "https://feross.org/support"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/isarray": {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz",
|
||||||
|
"integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/level": {
|
||||||
|
"version": "10.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/level/-/level-10.0.0.tgz",
|
||||||
|
"integrity": "sha512-aZJvdfRr/f0VBbSRF5C81FHON47ZsC2TkGxbBezXpGGXAUEL/s6+GP73nnhAYRSCIqUNsmJjfeOF4lzRDKbUig==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"abstract-level": "^3.1.0",
|
||||||
|
"browser-level": "^3.0.0",
|
||||||
|
"classic-level": "^3.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/level"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/level-supports": {
|
||||||
|
"version": "6.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/level-supports/-/level-supports-6.2.0.tgz",
|
||||||
|
"integrity": "sha512-QNxVXP0IRnBmMsJIh+sb2kwNCYcKciQZJEt+L1hPCHrKNELllXhvrlClVHXBYZVT+a7aTSM6StgNXdAldoab3w==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=16"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/level-transcoder": {
|
||||||
|
"version": "1.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/level-transcoder/-/level-transcoder-1.0.1.tgz",
|
||||||
|
"integrity": "sha512-t7bFwFtsQeD8cl8NIoQ2iwxA0CL/9IFw7/9gAjOonH0PWTTiRfY7Hq+Ejbsxh86tXobDQ6IOiddjNYIfOBs06w==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"buffer": "^6.0.3",
|
||||||
|
"module-error": "^1.0.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/level-ttl": {
|
||||||
|
"version": "3.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/level-ttl/-/level-ttl-3.1.1.tgz",
|
||||||
|
"integrity": "sha512-OeiHOD2IPkmdLqqU4feVJL7mnZX/Q03WEClrQi5t9558alkajVaecCgwJQZVVL/zFR9q74n5pWN1eozifa1Ghw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"after": ">=0.8.1 <0.9.0-0",
|
||||||
|
"list-stream": ">=1.0.0 <1.1.0-0",
|
||||||
|
"lock": "~0.1.2",
|
||||||
|
"xtend": ">=4.0.0 <4.1.0-0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/list-stream": {
|
||||||
|
"version": "1.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/list-stream/-/list-stream-1.0.1.tgz",
|
||||||
|
"integrity": "sha512-XheYsTtN+/nay6Co4N9NlTjQzo1ohknNlDJfxTeH0tvvssxBINUXwmjqPtj8+7rYMBwTRb3kO8C8d6ogeRwD1A==",
|
||||||
|
"license": "MIT +no-false-attribs",
|
||||||
|
"dependencies": {
|
||||||
|
"readable-stream": "~2.0.5",
|
||||||
|
"xtend": "~4.0.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/lock": {
|
||||||
|
"version": "0.1.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/lock/-/lock-0.1.4.tgz",
|
||||||
|
"integrity": "sha512-IcEe2R+NA7WgM622ppgmJFCFZl20f2owsA1YiJg7qpvO0wdOgOuZdfhQMxCYXdESVX+QIF/eikE4hB5ZPM2ipA==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/maxmind": {
|
||||||
|
"version": "4.3.25",
|
||||||
|
"resolved": "https://registry.npmjs.org/maxmind/-/maxmind-4.3.25.tgz",
|
||||||
|
"integrity": "sha512-u7L6LrbXZUtpdoovTVHo/l4/EoWUT2eHfCKWDMNNTsW9BaLa7h0jCHjqVx5ZeS5aWorLGZSsZwqxcpoollBw1g==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"mmdb-lib": "2.2.0",
|
||||||
|
"tiny-lru": "11.2.11"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12",
|
||||||
|
"npm": ">=6"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/maybe-combine-errors": {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/maybe-combine-errors/-/maybe-combine-errors-1.0.0.tgz",
|
||||||
|
"integrity": "sha512-eefp6IduNPT6fVdwPp+1NgD0PML1NU5P6j1Mj5nz1nidX8/sWY7119WL8vTAHgqfsY74TzW0w1XPgdYEKkGZ5A==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/minipass": {
|
||||||
|
"version": "7.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz",
|
||||||
|
"integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==",
|
||||||
|
"license": "ISC",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=16 || 14 >=14.17"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/minizlib": {
|
||||||
|
"version": "3.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/minizlib/-/minizlib-3.0.2.tgz",
|
||||||
|
"integrity": "sha512-oG62iEk+CYt5Xj2YqI5Xi9xWUeZhDI8jjQmC5oThVH5JGCTgIjr7ciJDzC7MBzYd//WvR1OTmP5Q38Q8ShQtVA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"minipass": "^7.1.2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/mkdirp": {
|
||||||
|
"version": "3.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-3.0.1.tgz",
|
||||||
|
"integrity": "sha512-+NsyUUAZDmo6YVHzL/stxSu3t9YS1iljliy3BSDrXJ/dkn1KYdmtZODGGjLcc9XLgVVpH4KshHB8XmZgMhaBXg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"bin": {
|
||||||
|
"mkdirp": "dist/cjs/src/bin.js"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/isaacs"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/mmdb-lib": {
|
||||||
|
"version": "2.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/mmdb-lib/-/mmdb-lib-2.2.0.tgz",
|
||||||
|
"integrity": "sha512-V6DDh3v8tfZFWbeH6fsL5uBIlWL7SvRgGDaAZWFC5kjQ2xP5dl/mLpWwJQ1Ho6ZbEKVp/351QF1JXYTAmeZ/zA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10",
|
||||||
|
"npm": ">=6"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/module-error": {
|
||||||
|
"version": "1.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/module-error/-/module-error-1.0.2.tgz",
|
||||||
|
"integrity": "sha512-0yuvsqSCv8LbaOKhnsQ/T5JhyFlCYLPXK3U2sgV10zoKQwzs/MyfuQUOZQ1V/6OCOJsK/TRgNVrPuPDqtdMFtA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/ms": {
|
||||||
|
"version": "2.1.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
||||||
|
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/napi-macros": {
|
||||||
|
"version": "2.2.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/napi-macros/-/napi-macros-2.2.2.tgz",
|
||||||
|
"integrity": "sha512-hmEVtAGYzVQpCKdbQea4skABsdXW4RUh5t5mJ2zzqowJS2OyXZTU1KhDVFhx+NlWZ4ap9mqR9TcDO3LTTttd+g==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/node-gyp-build": {
|
||||||
|
"version": "4.8.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.8.4.tgz",
|
||||||
|
"integrity": "sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"bin": {
|
||||||
|
"node-gyp-build": "bin.js",
|
||||||
|
"node-gyp-build-optional": "optional.js",
|
||||||
|
"node-gyp-build-test": "build-test.js"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/prettier": {
|
||||||
|
"version": "2.8.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/prettier/-/prettier-2.8.8.tgz",
|
||||||
|
"integrity": "sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"bin": {
|
||||||
|
"prettier": "bin-prettier.js"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10.13.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/prettier/prettier?sponsor=1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/process-nextick-args": {
|
||||||
|
"version": "1.0.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-1.0.7.tgz",
|
||||||
|
"integrity": "sha512-yN0WQmuCX63LP/TMvAg31nvT6m4vDqJEiiv2CAZqWOGNWutc9DfDk1NPYYmKUFmaVM2UwDowH4u5AHWYP/jxKw==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/raw-body": {
|
||||||
|
"version": "2.5.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz",
|
||||||
|
"integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"bytes": "3.1.2",
|
||||||
|
"http-errors": "2.0.0",
|
||||||
|
"iconv-lite": "0.4.24",
|
||||||
|
"unpipe": "1.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.8"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/readable-stream": {
|
||||||
|
"version": "2.0.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.0.6.tgz",
|
||||||
|
"integrity": "sha512-TXcFfb63BQe1+ySzsHZI/5v1aJPCShfqvWJ64ayNImXMsN1Cd0YGk/wm8KB7/OeessgPc9QvS9Zou8QTkFzsLw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"core-util-is": "~1.0.0",
|
||||||
|
"inherits": "~2.0.1",
|
||||||
|
"isarray": "~1.0.0",
|
||||||
|
"process-nextick-args": "~1.0.6",
|
||||||
|
"string_decoder": "~0.10.x",
|
||||||
|
"util-deprecate": "~1.0.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/requires-port": {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz",
|
||||||
|
"integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/safer-buffer": {
|
||||||
|
"version": "2.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
|
||||||
|
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/setprototypeof": {
|
||||||
|
"version": "1.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz",
|
||||||
|
"integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==",
|
||||||
|
"license": "ISC"
|
||||||
|
},
|
||||||
|
"node_modules/statuses": {
|
||||||
|
"version": "2.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz",
|
||||||
|
"integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.8"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/streamx": {
|
||||||
|
"version": "2.22.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/streamx/-/streamx-2.22.0.tgz",
|
||||||
|
"integrity": "sha512-sLh1evHOzBy/iWRiR6d1zRcLao4gGZr3C1kzNz4fopCOKJb6xD9ub8Mpi9Mr1R6id5o43S+d93fI48UC5uM9aw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"fast-fifo": "^1.3.2",
|
||||||
|
"text-decoder": "^1.1.0"
|
||||||
|
},
|
||||||
|
"optionalDependencies": {
|
||||||
|
"bare-events": "^2.2.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/string_decoder": {
|
||||||
|
"version": "0.10.31",
|
||||||
|
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz",
|
||||||
|
"integrity": "sha512-ev2QzSzWPYmy9GuqfIVildA4OdcGLeFZQrq5ys6RtiuF+RQQiZWr8TZNyAcuVXyQRYfEO+MsoB/1BuQVhOJuoQ==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/string-dsa": {
|
||||||
|
"version": "2.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/string-dsa/-/string-dsa-2.1.0.tgz",
|
||||||
|
"integrity": "sha512-ht+H83VtdA0JXmZsRfhQYzUSwqK3T7STPqiD/u3bIvYUHLEw8zzZyvP9WI3l9uKbK/2IpU+ZdshAe5BoRil3wA==",
|
||||||
|
"license": "ISC"
|
||||||
|
},
|
||||||
|
"node_modules/tar": {
|
||||||
|
"version": "7.4.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/tar/-/tar-7.4.3.tgz",
|
||||||
|
"integrity": "sha512-5S7Va8hKfV7W5U6g3aYxXmlPoZVAwUMy9AOKyF2fVuZa2UD3qZjg578OrLRt8PcNN1PleVaL/5/yYATNL0ICUw==",
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"@isaacs/fs-minipass": "^4.0.0",
|
||||||
|
"chownr": "^3.0.0",
|
||||||
|
"minipass": "^7.1.2",
|
||||||
|
"minizlib": "^3.0.1",
|
||||||
|
"mkdirp": "^3.0.1",
|
||||||
|
"yallist": "^5.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/tar-stream": {
|
||||||
|
"version": "3.1.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-3.1.7.tgz",
|
||||||
|
"integrity": "sha512-qJj60CXt7IU1Ffyc3NJMjh6EkuCFej46zUqJ4J7pqYlThyd9bO0XBTmcOIhSzZJVWfsLks0+nle/j538YAW9RQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"b4a": "^1.6.4",
|
||||||
|
"fast-fifo": "^1.2.0",
|
||||||
|
"streamx": "^2.15.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/text-decoder": {
|
||||||
|
"version": "1.2.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/text-decoder/-/text-decoder-1.2.3.tgz",
|
||||||
|
"integrity": "sha512-3/o9z3X0X0fTupwsYvR03pJ/DjWuqqrfwBgTQzdWDiQSm9KitAyz/9WqsT2JQW7KV2m+bC2ol/zqpW37NHxLaA==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"b4a": "^1.6.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/tiny-lru": {
|
||||||
|
"version": "11.2.11",
|
||||||
|
"resolved": "https://registry.npmjs.org/tiny-lru/-/tiny-lru-11.2.11.tgz",
|
||||||
|
"integrity": "sha512-27BIW0dIWTYYoWNnqSmoNMKe5WIbkXsc0xaCQHd3/3xT2XMuMJrzHdrO9QBFR14emBz1Bu0dOAs2sCBBrvgPQA==",
|
||||||
|
"license": "BSD-3-Clause",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/toidentifier": {
|
||||||
|
"version": "1.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz",
|
||||||
|
"integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.6"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/toml": {
|
||||||
|
"version": "3.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/toml/-/toml-3.0.0.tgz",
|
||||||
|
"integrity": "sha512-y/mWCZinnvxjTKYhJ+pYxwD0mRLVvOtdS2Awbgxln6iEnt4rk0yBxeSBHkGJcPucRiG0e55mwWp+g/05rsrd6w==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/unpipe": {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
|
||||||
|
"integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.8"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/util-deprecate": {
|
||||||
|
"version": "1.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
|
||||||
|
"integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/xtend": {
|
||||||
|
"version": "4.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz",
|
||||||
|
"integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/yallist": {
|
||||||
|
"version": "5.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/yallist/-/yallist-5.0.0.tgz",
|
||||||
|
"integrity": "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==",
|
||||||
|
"license": "BlueOak-1.0.0",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
26
package.json
Normal file
26
package.json
Normal file
|
|
@ -0,0 +1,26 @@
|
||||||
|
{
|
||||||
|
"name": "checkpoint",
|
||||||
|
"module": "index.js",
|
||||||
|
"private": true,
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"start": "bun index.js"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"prettier": "^2.8.8"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@iarna/toml": "^2.2.5",
|
||||||
|
"cookie": "^1.0.2",
|
||||||
|
"dotenv": "^16.5.0",
|
||||||
|
"express-http-proxy": "^2.1.1",
|
||||||
|
"http-proxy": "^1.18.1",
|
||||||
|
"level": "^10.0.0",
|
||||||
|
"level-ttl": "^3.1.1",
|
||||||
|
"maxmind": "^4.3.25",
|
||||||
|
"string-dsa": "^2.1.0",
|
||||||
|
"tar": "^7.4.3",
|
||||||
|
"tar-stream": "^3.1.7",
|
||||||
|
"toml": "^3.0.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
518
pages/interstitial/js/c.js
Normal file
518
pages/interstitial/js/c.js
Normal file
|
|
@ -0,0 +1,518 @@
|
||||||
|
// Modal state variables
|
||||||
|
let isModalOpen = false;
|
||||||
|
let pendingAction = null; // Can be 'success' or 'error'
|
||||||
|
let storedErrorMessage = '';
|
||||||
|
let storedRedirectUrl = '';
|
||||||
|
// let redirectToken = ''; // This was defined but not used, removing for now.
|
||||||
|
const REDIRECT_DELAY = 1488; // Moved for wider accessibility
|
||||||
|
|
||||||
|
function workerFunction() {
|
||||||
|
self.onmessage = function (e) {
|
||||||
|
const { type, data } = e.data;
|
||||||
|
|
||||||
|
if (type === 'pow') {
|
||||||
|
const { challenge, salt, startNonce, endNonce, target, batchId } = data;
|
||||||
|
let count = 0;
|
||||||
|
let solution = null;
|
||||||
|
|
||||||
|
processNextNonce(startNonce);
|
||||||
|
|
||||||
|
function processNextNonce(nonce) {
|
||||||
|
const input = String(challenge) + String(salt) + nonce.toString();
|
||||||
|
const msgBuffer = new TextEncoder().encode(input);
|
||||||
|
|
||||||
|
crypto.subtle
|
||||||
|
.digest('SHA-256', msgBuffer)
|
||||||
|
.then((hashBuffer) => {
|
||||||
|
const hashArray = Array.from(new Uint8Array(hashBuffer));
|
||||||
|
const result = hashArray.map((b) => b.toString(16).padStart(2, '0')).join('');
|
||||||
|
|
||||||
|
count++;
|
||||||
|
|
||||||
|
if (result.startsWith(target)) {
|
||||||
|
solution = { nonce: nonce.toString(), found: true };
|
||||||
|
self.postMessage({
|
||||||
|
type: 'pow_result',
|
||||||
|
solution: solution,
|
||||||
|
count: count,
|
||||||
|
batchId: batchId,
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (count % 1000 === 0) {
|
||||||
|
self.postMessage({
|
||||||
|
type: 'progress',
|
||||||
|
count: count,
|
||||||
|
batchId: batchId,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (nonce < endNonce && !solution) {
|
||||||
|
setTimeout(() => processNextNonce(nonce + 1), 0);
|
||||||
|
} else if (!solution) {
|
||||||
|
self.postMessage({
|
||||||
|
type: 'pow_result',
|
||||||
|
solution: null,
|
||||||
|
count: count,
|
||||||
|
batchId: batchId,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
self.postMessage({
|
||||||
|
type: 'error',
|
||||||
|
error: 'Crypto API error: ' + err.message,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
self.postMessage({ type: 'error', error: 'Unknown message type: ' + type });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
const workerCode = '(' + workerFunction.toString() + ')()';
|
||||||
|
|
||||||
|
function posWorkerFunction() {
|
||||||
|
self.onmessage = async function (e) {
|
||||||
|
const { type, seedHex, isDecoy } = e.data;
|
||||||
|
if (type === 'pos') {
|
||||||
|
const minMB = 48,
|
||||||
|
maxMB = 160;
|
||||||
|
let seedInt = parseInt(seedHex.slice(0, 8), 16);
|
||||||
|
if (isNaN(seedInt)) seedInt = Math.floor(Math.random() * (maxMB - minMB + 1));
|
||||||
|
const CHUNK_MB = isDecoy
|
||||||
|
? minMB + ((seedInt * 3 + 17) % (maxMB - minMB + 1))
|
||||||
|
: minMB + (seedInt % (maxMB - minMB + 1));
|
||||||
|
const CHUNK_SIZE = CHUNK_MB * 1024 * 1024;
|
||||||
|
const chunkCount = 4 + (seedInt % 5);
|
||||||
|
const chunkSize = Math.floor(CHUNK_SIZE / chunkCount);
|
||||||
|
const FILL_STEP_4K = 4096,
|
||||||
|
FILL_STEP_1K = 1024;
|
||||||
|
const FILL_STEP_SWITCH = 35 * 1024 * 1024;
|
||||||
|
const runs = 3;
|
||||||
|
|
||||||
|
const mainBuf = new ArrayBuffer(CHUNK_SIZE);
|
||||||
|
const view = new Uint8Array(mainBuf);
|
||||||
|
const pressureBuf = new ArrayBuffer(16 * 1024 * 1024);
|
||||||
|
const pressureView = new Uint8Array(pressureBuf);
|
||||||
|
|
||||||
|
const hashes = [];
|
||||||
|
const times = [];
|
||||||
|
for (let r = 0; r < runs; r++) {
|
||||||
|
const prng = seededPRNG(seedHex + r.toString(16));
|
||||||
|
|
||||||
|
const order = Array.from({ length: chunkCount }, (_, i) => i);
|
||||||
|
for (let i = order.length - 1; i > 0; i--) {
|
||||||
|
const j = prng() % (i + 1);
|
||||||
|
[order[i], order[j]] = [order[j], order[i]];
|
||||||
|
}
|
||||||
|
|
||||||
|
const t0 = performance.now();
|
||||||
|
for (let c = 0; c < chunkCount; c++) {
|
||||||
|
const idx = order[c];
|
||||||
|
const start = idx * chunkSize;
|
||||||
|
const end = idx === chunkCount - 1 ? CHUNK_SIZE : start + chunkSize;
|
||||||
|
const step = start < FILL_STEP_SWITCH ? FILL_STEP_4K : FILL_STEP_1K;
|
||||||
|
for (let i = start; i < end; i += step) view[i] = prng() & 0xff;
|
||||||
|
}
|
||||||
|
const hashBuf = await crypto.subtle.digest('SHA-256', view);
|
||||||
|
const t2 = performance.now();
|
||||||
|
hashes.push(
|
||||||
|
Array.from(new Uint8Array(hashBuf))
|
||||||
|
.map((b) => b.toString(16).padStart(2, '0'))
|
||||||
|
.join(''),
|
||||||
|
);
|
||||||
|
times.push(Math.round(t2 - t0));
|
||||||
|
|
||||||
|
for (let i = 0; i < pressureView.length; i += 4096) pressureView[i] = prng() & 0xff;
|
||||||
|
}
|
||||||
|
|
||||||
|
self.postMessage({ type: 'pos_result', hashes, times });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
function seededPRNG(seedHex) {
|
||||||
|
const s = [];
|
||||||
|
for (let i = 0; i < 4; i++) s[i] = parseInt(seedHex.substr(i * 8, 8), 16) >>> 0;
|
||||||
|
function rotl(x, k) {
|
||||||
|
return ((x << k) | (x >>> (32 - k))) >>> 0;
|
||||||
|
}
|
||||||
|
return function () {
|
||||||
|
const t = s[1] << 9;
|
||||||
|
let r = (s[0] * 5) >>> 0;
|
||||||
|
r = (rotl(r, 7) * 9) >>> 0;
|
||||||
|
const tmp = s[0] ^ s[2];
|
||||||
|
s[2] ^= s[1];
|
||||||
|
s[1] ^= s[3];
|
||||||
|
s[0] ^= s[1];
|
||||||
|
s[3] ^= tmp;
|
||||||
|
s[2] ^= t;
|
||||||
|
s[3] = rotl(s[3], 11);
|
||||||
|
return r >>> 0;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const posWorkerCode = '(' + posWorkerFunction.toString() + ')()';
|
||||||
|
|
||||||
|
document.addEventListener('DOMContentLoaded', function () {
|
||||||
|
setTimeout(initVerification, 650);
|
||||||
|
|
||||||
|
// Modal elements
|
||||||
|
const infoButton = document.getElementById('infoBtn');
|
||||||
|
const infoModal = document.getElementById('infoModal');
|
||||||
|
const modalCloseButton = document.getElementById('modalCloseBtn');
|
||||||
|
|
||||||
|
if (infoButton) {
|
||||||
|
infoButton.addEventListener('click', () => {
|
||||||
|
if (isModalOpen) {
|
||||||
|
closeModal();
|
||||||
|
} else {
|
||||||
|
isModalOpen = true;
|
||||||
|
if (infoModal) infoModal.classList.add('active');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (modalCloseButton) {
|
||||||
|
modalCloseButton.addEventListener('click', closeModal);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close modal if overlay is clicked
|
||||||
|
if (infoModal) {
|
||||||
|
infoModal.addEventListener('click', (event) => {
|
||||||
|
if (event.target === infoModal) {
|
||||||
|
closeModal();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeModal() {
|
||||||
|
if (infoModal) infoModal.classList.remove('active');
|
||||||
|
isModalOpen = false;
|
||||||
|
if (pendingAction === 'success') {
|
||||||
|
triggerStoredSuccess();
|
||||||
|
} else if (pendingAction === 'error') {
|
||||||
|
triggerStoredError(storedErrorMessage);
|
||||||
|
}
|
||||||
|
pendingAction = null; // Reset after handling
|
||||||
|
}
|
||||||
|
|
||||||
|
// Moved triggerStoredSuccess and triggerStoredError to this scope
|
||||||
|
function triggerStoredSuccess() {
|
||||||
|
document.querySelector('.container').classList.add('success');
|
||||||
|
const statusEl = document.getElementById('status');
|
||||||
|
if (statusEl) statusEl.textContent = 'Redirecting'; // Ensure status is updated
|
||||||
|
if (storedRedirectUrl) {
|
||||||
|
setTimeout(() => {
|
||||||
|
window.location.href = storedRedirectUrl;
|
||||||
|
}, REDIRECT_DELAY); // Ensure REDIRECT_DELAY is accessible or define it here
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function triggerStoredError(message) {
|
||||||
|
const container = document.querySelector('.container');
|
||||||
|
const statusEl = document.getElementById('status');
|
||||||
|
const spinnerContainer = document.querySelector('.spinner-container');
|
||||||
|
|
||||||
|
container.classList.add('error');
|
||||||
|
container.classList.remove('success');
|
||||||
|
|
||||||
|
if (statusEl) {
|
||||||
|
statusEl.style.display = 'inline-block';
|
||||||
|
statusEl.textContent = 'Error';
|
||||||
|
statusEl.classList.add('error');
|
||||||
|
statusEl.classList.remove('success');
|
||||||
|
}
|
||||||
|
|
||||||
|
let errorDetails = document.getElementById('error-details');
|
||||||
|
if (!errorDetails && spinnerContainer) {
|
||||||
|
errorDetails = document.createElement('div');
|
||||||
|
errorDetails.id = 'error-details';
|
||||||
|
errorDetails.className = 'error-details';
|
||||||
|
spinnerContainer.appendChild(errorDetails);
|
||||||
|
}
|
||||||
|
if (errorDetails) {
|
||||||
|
// errorDetails.textContent = message; // Display the specific error message
|
||||||
|
errorDetails.style.display = 'block';
|
||||||
|
}
|
||||||
|
// Ensure any running workers are stopped on error
|
||||||
|
// This might need to be called from within Verifier's scope or Verifier needs a public method
|
||||||
|
// For now, if terminateWorkers is global or accessible, it would be called.
|
||||||
|
// However, terminateWorkers is defined within Verifier. This needs careful handling.
|
||||||
|
// Let's assume for now the original placement inside Verifier handles termination on actual error progression.
|
||||||
|
// The primary goal here is to show the UI error state.
|
||||||
|
}
|
||||||
|
|
||||||
|
function initVerification() {
|
||||||
|
const dataEl = document.getElementById('verification-data');
|
||||||
|
const targetPath = dataEl.getAttribute('data-target');
|
||||||
|
const requestID = dataEl.getAttribute('data-request-id');
|
||||||
|
|
||||||
|
startVerification();
|
||||||
|
|
||||||
|
async function startVerification() {
|
||||||
|
try {
|
||||||
|
const challengeResponse = await fetch(
|
||||||
|
'/api/challenge?id=' + encodeURIComponent(requestID),
|
||||||
|
{ method: 'GET', headers: { Accept: 'application/json' }, credentials: 'include' },
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!challengeResponse.ok) {
|
||||||
|
throw new Error('Failed to get challenge parameters');
|
||||||
|
}
|
||||||
|
|
||||||
|
const challengeData = await challengeResponse.json();
|
||||||
|
|
||||||
|
const verifier = new Verifier(challengeData, targetPath, requestID);
|
||||||
|
verifier.start();
|
||||||
|
} catch (error) {
|
||||||
|
showError('Verification setup failed: ' + error.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function createWorker() {
|
||||||
|
const blob = new Blob([workerCode], { type: 'text/javascript' });
|
||||||
|
return new Worker(URL.createObjectURL(blob));
|
||||||
|
}
|
||||||
|
|
||||||
|
function createPosWorker() {
|
||||||
|
const blob = new Blob([posWorkerCode], { type: 'text/javascript' });
|
||||||
|
return new Worker(URL.createObjectURL(blob));
|
||||||
|
}
|
||||||
|
|
||||||
|
function showError(message) {
|
||||||
|
const container = document.querySelector('.container');
|
||||||
|
const statusEl = document.getElementById('status');
|
||||||
|
const spinnerContainer = document.querySelector('.spinner-container');
|
||||||
|
|
||||||
|
storedErrorMessage = message; // Store for later
|
||||||
|
|
||||||
|
if (isModalOpen) {
|
||||||
|
pendingAction = 'error';
|
||||||
|
if (statusEl) {
|
||||||
|
statusEl.textContent = 'Error'; // Generic message while modal is open
|
||||||
|
statusEl.style.display = 'inline-block'; // Make sure status is visible
|
||||||
|
statusEl.classList.remove('success'); // Ensure no success styling
|
||||||
|
statusEl.classList.add('error'); // Add error styling for text color
|
||||||
|
}
|
||||||
|
// Do not show full error details or animations yet
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Proceed with normal error display if modal is not open
|
||||||
|
container.classList.add('error');
|
||||||
|
container.classList.remove('success');
|
||||||
|
|
||||||
|
if (statusEl) {
|
||||||
|
statusEl.style.display = 'inline-block';
|
||||||
|
statusEl.textContent = '';
|
||||||
|
statusEl.classList.add('error');
|
||||||
|
statusEl.classList.remove('success');
|
||||||
|
}
|
||||||
|
|
||||||
|
let errorDetails = document.getElementById('error-details');
|
||||||
|
if (!errorDetails && spinnerContainer) {
|
||||||
|
errorDetails = document.createElement('div');
|
||||||
|
errorDetails.id = 'error-details';
|
||||||
|
errorDetails.className = 'error-details';
|
||||||
|
spinnerContainer.appendChild(errorDetails);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (errorDetails) {
|
||||||
|
// errorDetails.textContent = message; // This was causing issues, let animations handle it or remove.
|
||||||
|
errorDetails.style.display = 'none'; // Keep this hidden, rely on class-based animation
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function showSuccess() {
|
||||||
|
const statusEl = document.getElementById('status');
|
||||||
|
if (statusEl) statusEl.textContent = 'Redirecting';
|
||||||
|
|
||||||
|
if (isModalOpen) {
|
||||||
|
pendingAction = 'success';
|
||||||
|
// Do not add 'success' class or start animations yet
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
document.querySelector('.container').classList.add('success');
|
||||||
|
// The redirect will be handled by submitSolution after a delay
|
||||||
|
}
|
||||||
|
|
||||||
|
function Verifier(params, targetPath, requestID) {
|
||||||
|
const workers = [];
|
||||||
|
const activeBatches = {};
|
||||||
|
let powSolution = null;
|
||||||
|
let isRunning = false;
|
||||||
|
|
||||||
|
const cpuCount = navigator.hardwareConcurrency || 4;
|
||||||
|
const workerCount = Math.max(1, Math.floor(cpuCount * 0.8));
|
||||||
|
|
||||||
|
// const REDIRECT_DELAY = 1488; // Defined globally now
|
||||||
|
|
||||||
|
this.start = function () {
|
||||||
|
setTimeout(findProofOfWork, 100);
|
||||||
|
};
|
||||||
|
|
||||||
|
async function findProofOfWork() {
|
||||||
|
try {
|
||||||
|
isRunning = true;
|
||||||
|
|
||||||
|
const challenge = params.a;
|
||||||
|
const salt = params.b;
|
||||||
|
const target = '0'.repeat(params.c);
|
||||||
|
|
||||||
|
for (let i = 0; i < workerCount; i++) {
|
||||||
|
const worker = createWorker();
|
||||||
|
|
||||||
|
worker.onmessage = (e) => handleWorkerMessage(e.data);
|
||||||
|
worker.onerror = (error) => {};
|
||||||
|
workers.push(worker);
|
||||||
|
}
|
||||||
|
|
||||||
|
const totalRange = Number.MAX_SAFE_INTEGER;
|
||||||
|
const rangePerWorker = Math.floor(totalRange / workerCount);
|
||||||
|
|
||||||
|
for (let i = 0; i < workers.length; i++) {
|
||||||
|
const startNonce = i * rangePerWorker;
|
||||||
|
const endNonce = i === workers.length - 1 ? totalRange : (i + 1) * rangePerWorker - 1;
|
||||||
|
|
||||||
|
const workerId = `pow-worker-${i}`;
|
||||||
|
|
||||||
|
activeBatches[workerId] = {
|
||||||
|
workerId: i,
|
||||||
|
startNonce,
|
||||||
|
endNonce,
|
||||||
|
};
|
||||||
|
|
||||||
|
workers[i].postMessage({
|
||||||
|
type: 'pow',
|
||||||
|
data: {
|
||||||
|
challenge: challenge,
|
||||||
|
salt: salt,
|
||||||
|
startNonce,
|
||||||
|
endNonce,
|
||||||
|
target,
|
||||||
|
batchId: workerId,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
terminateWorkers();
|
||||||
|
showError(error.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleWorkerMessage(data) {
|
||||||
|
if (!isRunning) return;
|
||||||
|
|
||||||
|
if (data.type === 'pow_result') {
|
||||||
|
if (activeBatches[data.batchId]) {
|
||||||
|
delete activeBatches[data.batchId];
|
||||||
|
|
||||||
|
if (data.solution && data.solution.found) {
|
||||||
|
if (!powSolution) {
|
||||||
|
powSolution = data.solution;
|
||||||
|
proofOfWorkFound(powSolution);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (data.type === 'error') {
|
||||||
|
showError('Compatibility error: ' + data.error);
|
||||||
|
terminateWorkers();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function proofOfWorkFound(solution) {
|
||||||
|
isRunning = false;
|
||||||
|
terminateWorkers();
|
||||||
|
try {
|
||||||
|
const posResult = await new Promise((res) => {
|
||||||
|
const w = createPosWorker();
|
||||||
|
w.onmessage = (e) => {
|
||||||
|
if (e.data.type === 'pos_result') {
|
||||||
|
res(e.data);
|
||||||
|
w.terminate();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
w.postMessage({ type: 'pos', seedHex: params.d, isDecoy: false });
|
||||||
|
});
|
||||||
|
|
||||||
|
await submitSolution({
|
||||||
|
requestID,
|
||||||
|
g: solution.nonce,
|
||||||
|
h: posResult.hashes,
|
||||||
|
i: posResult.times,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
showError(error.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function terminateWorkers() {
|
||||||
|
workers.forEach((worker) => worker.terminate());
|
||||||
|
}
|
||||||
|
|
||||||
|
async function submitSolution(solutionData) {
|
||||||
|
try {
|
||||||
|
const response = await fetch('/api/verify', {
|
||||||
|
method: 'POST',
|
||||||
|
credentials: 'include',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
request_id: solutionData.requestID,
|
||||||
|
g: solutionData.g,
|
||||||
|
h: solutionData.h,
|
||||||
|
i: solutionData.i,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
let errorMsg = `Verification failed: ${response.statusText}`;
|
||||||
|
try {
|
||||||
|
const errorData = await response.json();
|
||||||
|
if (errorData && errorData.error) {
|
||||||
|
errorMsg += ` - ${errorData.error}`;
|
||||||
|
} else {
|
||||||
|
const text = await response.text();
|
||||||
|
errorMsg += ` - Response: ${text}`;
|
||||||
|
}
|
||||||
|
} catch (parseError) {}
|
||||||
|
showError(errorMsg);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let result;
|
||||||
|
try {
|
||||||
|
result = await response.json();
|
||||||
|
} catch (e) {
|
||||||
|
showError('Invalid verification response');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const token = result.token;
|
||||||
|
if (!token) {
|
||||||
|
// Use existing showError for immediate display if modal not involved
|
||||||
|
// or to set up pending error if modal is open.
|
||||||
|
showError('Verification did not return a token');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Store for potential delayed redirect
|
||||||
|
const sep = targetPath.includes('?') ? '&' : '?';
|
||||||
|
storedRedirectUrl = `${targetPath}${sep}token=${encodeURIComponent(token)}`;
|
||||||
|
|
||||||
|
showSuccess(); // This will now respect isModalOpen
|
||||||
|
|
||||||
|
if (!isModalOpen) {
|
||||||
|
setTimeout(() => {
|
||||||
|
window.location.href = storedRedirectUrl;
|
||||||
|
}, REDIRECT_DELAY);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
// Use existing showError
|
||||||
|
showError('Verification failed. Please refresh the page.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
742
pages/interstitial/page.html
Normal file
742
pages/interstitial/page.html
Normal file
|
|
@ -0,0 +1,742 @@
|
||||||
|
<!doctype html>
|
||||||
|
<html lang=en>
|
||||||
|
<meta charset=UTF-8>
|
||||||
|
<meta http-equiv=X-UA-Compatible content="IE=edge">
|
||||||
|
<meta name=viewport content="width=device-width,initial-scale=1">
|
||||||
|
<title>Security Checkpoint</title>
|
||||||
|
<meta name=description content="Security checkpoint. Prove you're not a robot.">
|
||||||
|
<link rel=preload href=/webfont/Poppins-Regular.woff2 as=font type=font/woff2 crossorigin>
|
||||||
|
<link rel=preload href=/webfont/Poppins-SemiBold.woff2 as=font type=font/woff2 crossorigin>
|
||||||
|
<script defer src=/js/c.js></script>
|
||||||
|
<style>
|
||||||
|
* {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
box-sizing: border-box
|
||||||
|
}
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: Poppins;
|
||||||
|
src: url(/webfont/Poppins-Regular.woff2) format("woff2");
|
||||||
|
font-weight: 400;
|
||||||
|
font-style: normal;
|
||||||
|
font-display: swap
|
||||||
|
}
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: Poppins;
|
||||||
|
src: url(/webfont/Poppins-SemiBold.woff2) format("woff2");
|
||||||
|
font-weight: 600;
|
||||||
|
font-style: normal;
|
||||||
|
font-display: swap
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
font-family: Poppins, sans-serif
|
||||||
|
}
|
||||||
|
|
||||||
|
@view-transition {
|
||||||
|
navigation: auto
|
||||||
|
}
|
||||||
|
|
||||||
|
::view-transition-new(root),
|
||||||
|
::view-transition-old(root) {
|
||||||
|
animation-duration: .35s
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (prefers-reduced-motion) {
|
||||||
|
|
||||||
|
::view-transition-group(*),
|
||||||
|
::view-transition-new(*),
|
||||||
|
::view-transition-old(*) {
|
||||||
|
animation: none !important
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
:root {
|
||||||
|
--background-color: #1a1a1a;
|
||||||
|
--overlay-bg: rgba(28, 28, 28, 0.95);
|
||||||
|
--text-color: #fff;
|
||||||
|
--subtext-color: #ccc;
|
||||||
|
--accent-color: #9B59B6;
|
||||||
|
--success-color: #4CAF50;
|
||||||
|
--error-color: #F44336
|
||||||
|
}
|
||||||
|
|
||||||
|
html,
|
||||||
|
body {
|
||||||
|
background-color: #1a1a1a;
|
||||||
|
background-image: radial-gradient(circle at top right, rgba(155, 89, 182, .1), transparent 70%), linear-gradient(135deg, #121212, #1a1a1a);
|
||||||
|
overflow: hidden;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
height: 100%;
|
||||||
|
width: 100%
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar {
|
||||||
|
display: none
|
||||||
|
}
|
||||||
|
|
||||||
|
* {
|
||||||
|
scrollbar-width: none
|
||||||
|
}
|
||||||
|
|
||||||
|
* {
|
||||||
|
-ms-overflow-style: none
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
color: var(--text-color);
|
||||||
|
line-height: 1.6;
|
||||||
|
text-align: center;
|
||||||
|
min-height: 100vh;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center
|
||||||
|
}
|
||||||
|
|
||||||
|
#overlay {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
background-color: var(--overlay-bg);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
z-index: 9999;
|
||||||
|
padding: 20px
|
||||||
|
}
|
||||||
|
|
||||||
|
.container {
|
||||||
|
background: rgba(30, 30, 30, .85);
|
||||||
|
backdrop-filter: blur(8px);
|
||||||
|
border-radius: 20px;
|
||||||
|
padding: 20px;
|
||||||
|
box-shadow: 0 10px 30px rgba(0, 0, 0, .3), 0 1px 2px rgba(155, 89, 182, .2), 0 0 0 1px rgba(155, 89, 182, .2), 0 0 15px rgba(155, 89, 182, .15);
|
||||||
|
max-width: 555px;
|
||||||
|
width: 100%;
|
||||||
|
animation: floatIn .6s cubic-bezier(.25, .1, .25, 1);
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
color: var(--accent-color);
|
||||||
|
margin-bottom: 1.25rem;
|
||||||
|
font-size: clamp(1.75rem, 5vw, 2.5rem);
|
||||||
|
font-weight: 600;
|
||||||
|
text-shadow: 0 2px 4px rgba(0, 0, 0, .3)
|
||||||
|
}
|
||||||
|
|
||||||
|
p {
|
||||||
|
margin-bottom: 1.75rem;
|
||||||
|
color: var(--subtext-color);
|
||||||
|
font-size: clamp(1rem, 2.5vw, 1.1rem)
|
||||||
|
}
|
||||||
|
|
||||||
|
.spinner-container {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
margin: 20px 0;
|
||||||
|
height: clamp(48px, 12vw, 56px);
|
||||||
|
transition: all .4s cubic-bezier(.215, .61, .355, 1);
|
||||||
|
will-change: transform, opacity;
|
||||||
|
position: relative
|
||||||
|
}
|
||||||
|
|
||||||
|
.spinner {
|
||||||
|
border: 3px solid rgba(155, 89, 182, 8%);
|
||||||
|
border-radius: 50%;
|
||||||
|
border-top: 3px solid var(--accent-color);
|
||||||
|
width: clamp(48px, 12vw, 56px);
|
||||||
|
height: clamp(48px, 12vw, 56px);
|
||||||
|
animation: spin 1s linear infinite, pulse 2s ease-in-out infinite;
|
||||||
|
margin: 0 auto;
|
||||||
|
box-shadow: 0 0 15px rgba(155, 89, 182, .3);
|
||||||
|
transition: all .5s cubic-bezier(.19, 1, .22, 1);
|
||||||
|
will-change: transform, box-shadow;
|
||||||
|
position: relative
|
||||||
|
}
|
||||||
|
|
||||||
|
.spinner::after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
inset: -3px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: linear-gradient(135deg, rgba(155, 89, 182, .2) 0%, transparent 60%);
|
||||||
|
opacity: .3;
|
||||||
|
filter: blur(4px);
|
||||||
|
z-index: -1
|
||||||
|
}
|
||||||
|
|
||||||
|
.status {
|
||||||
|
display: none;
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: clamp(1.5rem, 4vw, 2rem);
|
||||||
|
margin: 0;
|
||||||
|
min-height: 1.5em;
|
||||||
|
text-align: center;
|
||||||
|
opacity: 0
|
||||||
|
}
|
||||||
|
|
||||||
|
.success .spinner-container {
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
max-width: none;
|
||||||
|
width: 100%;
|
||||||
|
margin: 1.5rem auto;
|
||||||
|
gap: 12px;
|
||||||
|
animation: container-animate .3s cubic-bezier(.25, .1, .25, 1)forwards
|
||||||
|
}
|
||||||
|
|
||||||
|
.success .spinner-container .error-details {
|
||||||
|
display: none
|
||||||
|
}
|
||||||
|
|
||||||
|
.success .spinner {
|
||||||
|
border-color: transparent;
|
||||||
|
animation: none;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
background-color: rgba(76, 175, 80, 5%);
|
||||||
|
box-shadow: 0 0 20px rgba(76, 175, 80, .4);
|
||||||
|
width: clamp(48px, 12vw, 56px);
|
||||||
|
height: clamp(48px, 12vw, 56px);
|
||||||
|
margin: 0;
|
||||||
|
animation: ui-animate .35s forwards cubic-bezier(.25, .1, .25, 1), float-animate 3s ease-in-out infinite .4s
|
||||||
|
}
|
||||||
|
|
||||||
|
.success .spinner::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: radial-gradient(circle, rgba(76, 175, 80, 5%) 0%, rgba(76, 175, 80, .1) 100%);
|
||||||
|
box-shadow: inset 0 0 0 3px rgba(76, 175, 80, 0);
|
||||||
|
animation: circle-animate .35s cubic-bezier(.25, .1, .25, 1)forwards;
|
||||||
|
opacity: 0;
|
||||||
|
inset: 0;
|
||||||
|
will-change: opacity, transform, box-shadow
|
||||||
|
}
|
||||||
|
|
||||||
|
.success .spinner::after {
|
||||||
|
content: '';
|
||||||
|
width: clamp(22px, 6vw, 28px);
|
||||||
|
height: clamp(22px, 6vw, 28px);
|
||||||
|
display: block;
|
||||||
|
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='%234CAF50'%3E%3Cpath d='M9 16.17L4.83 12l-1.42 1.41L9 19 21 7l-1.41-1.41L9 16.17z'/%3E%3C/svg%3E");
|
||||||
|
background-size: contain;
|
||||||
|
animation: checkmark-animate .35s cubic-bezier(.25, .1, .25, 1)forwards .05s;
|
||||||
|
opacity: 0;
|
||||||
|
transform: scale(0);
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
transform: translate(-50%, -50%)scale(0);
|
||||||
|
will-change: transform, opacity;
|
||||||
|
filter: drop-shadow(0 1px 2px rgba(0, 0, 0, .15))
|
||||||
|
}
|
||||||
|
|
||||||
|
.success .status {
|
||||||
|
display: inline-block;
|
||||||
|
color: var(--success-color);
|
||||||
|
animation: text-animate .4s forwards cubic-bezier(.25, .1, .25, 1);
|
||||||
|
animation-delay: .15s;
|
||||||
|
letter-spacing: .3px;
|
||||||
|
text-shadow: 0 1px 1px rgba(0, 0, 0, 5%);
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateX(-15px);
|
||||||
|
white-space: nowrap
|
||||||
|
}
|
||||||
|
|
||||||
|
.success .status::after {
|
||||||
|
content: "";
|
||||||
|
display: inline-block;
|
||||||
|
width: 24px;
|
||||||
|
text-align: left;
|
||||||
|
animation: ellipsis-animate .88s infinite cubic-bezier(.25, .1, .25, 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
.error .status {
|
||||||
|
display: block;
|
||||||
|
color: var(--error-color);
|
||||||
|
margin-bottom: .5rem
|
||||||
|
}
|
||||||
|
|
||||||
|
.error .spinner {
|
||||||
|
border-color: transparent;
|
||||||
|
animation: none;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
background-color: rgba(244, 67, 54, 5%);
|
||||||
|
box-shadow: 0 0 20px rgba(244, 67, 54, .4);
|
||||||
|
width: clamp(48px, 12vw, 56px);
|
||||||
|
height: clamp(48px, 12vw, 56px);
|
||||||
|
margin: 0;
|
||||||
|
animation: ui-animate .35s forwards cubic-bezier(.25, .1, .25, 1), float-animate 3s ease-in-out infinite .4s
|
||||||
|
}
|
||||||
|
|
||||||
|
.error .spinner::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: radial-gradient(circle, rgba(244, 67, 54, 5%) 0%, rgba(244, 67, 54, .1) 100%);
|
||||||
|
box-shadow: inset 0 0 0 3px rgba(244, 67, 54, 0);
|
||||||
|
animation: error-circle-animate .35s cubic-bezier(.25, .1, .25, 1)forwards;
|
||||||
|
opacity: 0;
|
||||||
|
inset: 0;
|
||||||
|
will-change: opacity, transform, box-shadow
|
||||||
|
}
|
||||||
|
|
||||||
|
.error .spinner::after {
|
||||||
|
content: '';
|
||||||
|
width: clamp(22px, 6vw, 28px);
|
||||||
|
height: clamp(22px, 6vw, 28px);
|
||||||
|
display: block;
|
||||||
|
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' stroke='%23F44336' stroke-width='3' fill='none'%3E%3Cpath stroke-linecap='round' d='M6 6 L18 18 M18 6 L6 18'/%3E%3C/svg%3E");
|
||||||
|
background-size: contain;
|
||||||
|
animation: checkmark-animate .35s cubic-bezier(.25, .1, .25, 1)forwards .05s;
|
||||||
|
opacity: 0;
|
||||||
|
transform: scale(0);
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
transform: translate(-50%, -50%)scale(0);
|
||||||
|
will-change: transform, opacity;
|
||||||
|
filter: drop-shadow(0 1px 2px rgba(0, 0, 0, .15))
|
||||||
|
}
|
||||||
|
|
||||||
|
.error-details {
|
||||||
|
margin: 10px auto 0;
|
||||||
|
padding: .75rem 1.25rem;
|
||||||
|
background-color: rgba(244, 67, 54, 8%);
|
||||||
|
border-left: 3px solid var(--error-color);
|
||||||
|
color: var(--subtext-color);
|
||||||
|
font-size: .9rem;
|
||||||
|
text-align: center;
|
||||||
|
border-radius: 4px;
|
||||||
|
width: 90%;
|
||||||
|
line-height: 1.5;
|
||||||
|
animation: fadeIn .3s ease-in-out forwards
|
||||||
|
}
|
||||||
|
|
||||||
|
.error-x-icon {
|
||||||
|
width: clamp(22px, 6vw, 28px);
|
||||||
|
height: clamp(22px, 6vw, 28px);
|
||||||
|
fill: none;
|
||||||
|
stroke: var(--error-color);
|
||||||
|
stroke-width: 2;
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
display: block;
|
||||||
|
overflow: visible;
|
||||||
|
box-sizing: border-box;
|
||||||
|
padding: 0;
|
||||||
|
animation: fadeInScale .5s ease-in-out forwards
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes floatIn {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(20px)
|
||||||
|
}
|
||||||
|
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes spin {
|
||||||
|
0% {
|
||||||
|
transform: rotate(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
100% {
|
||||||
|
transform: rotate(360deg)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes pulse {
|
||||||
|
|
||||||
|
0%,
|
||||||
|
100% {
|
||||||
|
box-shadow: 0 0 15px rgba(155, 89, 182, .3)
|
||||||
|
}
|
||||||
|
|
||||||
|
50% {
|
||||||
|
box-shadow: 0 0 25px rgba(155, 89, 182, .5)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes container-animate {
|
||||||
|
0% {
|
||||||
|
opacity: .95
|
||||||
|
}
|
||||||
|
|
||||||
|
100% {
|
||||||
|
opacity: 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes ui-animate {
|
||||||
|
0% {
|
||||||
|
transform: translate(60px, 0)scale(.9);
|
||||||
|
opacity: 0
|
||||||
|
}
|
||||||
|
|
||||||
|
40% {
|
||||||
|
opacity: 1
|
||||||
|
}
|
||||||
|
|
||||||
|
100% {
|
||||||
|
transform: translate(0, 0)scale(1);
|
||||||
|
opacity: 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes float-animate {
|
||||||
|
|
||||||
|
0%,
|
||||||
|
100% {
|
||||||
|
transform: translateY(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
50% {
|
||||||
|
transform: translateY(-2px)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes circle-animate {
|
||||||
|
0% {
|
||||||
|
opacity: 0;
|
||||||
|
transform: scale(.5);
|
||||||
|
box-shadow: inset 0 0 0 3px rgba(76, 175, 80, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
60% {
|
||||||
|
opacity: 1;
|
||||||
|
transform: scale(1.1);
|
||||||
|
box-shadow: inset 0 0 0 3px rgba(76, 175, 80, .9)
|
||||||
|
}
|
||||||
|
|
||||||
|
100% {
|
||||||
|
opacity: 1;
|
||||||
|
transform: scale(1);
|
||||||
|
box-shadow: inset 0 0 0 3px var(--success-color)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes checkmark-animate {
|
||||||
|
0% {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translate(-50%, -50%)scale(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
60% {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translate(-50%, -50%)scale(1.15)
|
||||||
|
}
|
||||||
|
|
||||||
|
100% {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translate(-50%, -50%)scale(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes text-animate {
|
||||||
|
0% {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateX(-15px)
|
||||||
|
}
|
||||||
|
|
||||||
|
70% {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateX(1px)
|
||||||
|
}
|
||||||
|
|
||||||
|
100% {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateX(0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes ellipsis-animate {
|
||||||
|
|
||||||
|
0%,
|
||||||
|
5% {
|
||||||
|
content: ""
|
||||||
|
}
|
||||||
|
|
||||||
|
20%,
|
||||||
|
30% {
|
||||||
|
content: "."
|
||||||
|
}
|
||||||
|
|
||||||
|
45%,
|
||||||
|
55% {
|
||||||
|
content: ".."
|
||||||
|
}
|
||||||
|
|
||||||
|
70%,
|
||||||
|
80% {
|
||||||
|
content: "..."
|
||||||
|
}
|
||||||
|
|
||||||
|
95%,
|
||||||
|
100% {
|
||||||
|
content: ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes fadeInScale {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: scale(.5)
|
||||||
|
}
|
||||||
|
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: scale(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes fadeIn {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(-5px)
|
||||||
|
}
|
||||||
|
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes error-circle-animate {
|
||||||
|
0% {
|
||||||
|
opacity: 0;
|
||||||
|
transform: scale(.5);
|
||||||
|
box-shadow: inset 0 0 0 3px rgba(244, 67, 54, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
60% {
|
||||||
|
opacity: 1;
|
||||||
|
transform: scale(1.1);
|
||||||
|
box-shadow: inset 0 0 0 3px rgba(244, 67, 54, .9)
|
||||||
|
}
|
||||||
|
|
||||||
|
100% {
|
||||||
|
opacity: 1;
|
||||||
|
transform: scale(1);
|
||||||
|
box-shadow: inset 0 0 0 3px var(--error-color)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media(max-width:480px) {
|
||||||
|
.container {
|
||||||
|
padding: 20px;
|
||||||
|
border-radius: 16px
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
font-size: 1.5rem
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.error .spinner-container,
|
||||||
|
.success .spinner-container {
|
||||||
|
height: 56px;
|
||||||
|
margin: 20px 0
|
||||||
|
}
|
||||||
|
|
||||||
|
.container,
|
||||||
|
.spinner,
|
||||||
|
.spinner-container,
|
||||||
|
.success .spinner,
|
||||||
|
.success .status {
|
||||||
|
-webkit-transform: translate3d(0, 0, 0);
|
||||||
|
transform: translate3d(0, 0, 0);
|
||||||
|
-webkit-backface-visibility: hidden;
|
||||||
|
backface-visibility: hidden
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Modal Styles */
|
||||||
|
.modal-overlay {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
background-color: rgba(0, 0, 0, .7);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
z-index: 10000;
|
||||||
|
opacity: 0;
|
||||||
|
visibility: hidden;
|
||||||
|
transition: opacity .3s ease, visibility .3s ease
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-overlay.active {
|
||||||
|
opacity: 1;
|
||||||
|
visibility: visible
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-content {
|
||||||
|
background: rgba(40, 40, 40, .9);
|
||||||
|
backdrop-filter: blur(10px);
|
||||||
|
padding: 30px;
|
||||||
|
border-radius: 15px;
|
||||||
|
box-shadow: 0 5px 25px rgba(0, 0, 0, .4), 0 0 0 1px rgba(155, 89, 182, .25);
|
||||||
|
max-width: 600px;
|
||||||
|
width: 90%;
|
||||||
|
text-align: left;
|
||||||
|
position: relative;
|
||||||
|
transform: scale(.9);
|
||||||
|
transition: transform .3s ease
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-overlay.active .modal-content {
|
||||||
|
transform: scale(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-content h2 {
|
||||||
|
color: var(--accent-color);
|
||||||
|
margin-bottom: 15px;
|
||||||
|
font-size: 1.8rem
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-content p {
|
||||||
|
color: var(--subtext-color);
|
||||||
|
font-size: 1rem;
|
||||||
|
line-height: 1.7;
|
||||||
|
margin-bottom: 12px
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-content p:last-of-type {
|
||||||
|
margin-bottom: 25px
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-close-btn {
|
||||||
|
position: absolute;
|
||||||
|
top: 15px;
|
||||||
|
right: 15px;
|
||||||
|
background: transparent;
|
||||||
|
border: none;
|
||||||
|
color: var(--subtext-color);
|
||||||
|
font-size: 1.8rem;
|
||||||
|
cursor: pointer;
|
||||||
|
line-height: 1;
|
||||||
|
padding: 5px
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-close-btn:hover {
|
||||||
|
color: var(--text-color)
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-button {
|
||||||
|
background-color: rgba(155, 89, 182, .15);
|
||||||
|
color: var(--accent-color);
|
||||||
|
border: 1px solid rgba(155, 89, 182, .3);
|
||||||
|
padding: 12px 24px;
|
||||||
|
border-radius: 25px;
|
||||||
|
font-size: 1rem;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all .3s ease;
|
||||||
|
text-decoration: none;
|
||||||
|
position: fixed;
|
||||||
|
bottom: 20px;
|
||||||
|
left: 50%;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
z-index: 10001;
|
||||||
|
font-weight: 500
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-button:hover {
|
||||||
|
background-color: rgba(155, 89, 182, .25);
|
||||||
|
border-color: rgba(155, 89, 182, .5);
|
||||||
|
box-shadow: 0 0 10px rgba(155, 89, 182, .2)
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Improved mobile styles */
|
||||||
|
@media (max-width: 480px) {
|
||||||
|
.container {
|
||||||
|
padding: 20px;
|
||||||
|
border-radius: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
font-size: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-content {
|
||||||
|
padding: 20px;
|
||||||
|
width: 95%;
|
||||||
|
max-height: 80vh;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-content h2 {
|
||||||
|
font-size: 1.5rem;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-content p {
|
||||||
|
font-size: 0.95rem;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-close-btn {
|
||||||
|
top: 10px;
|
||||||
|
right: 10px;
|
||||||
|
font-size: 1.5rem;
|
||||||
|
padding: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-button {
|
||||||
|
width: auto;
|
||||||
|
min-width: 200px;
|
||||||
|
max-width: 85%;
|
||||||
|
padding: 12px 20px;
|
||||||
|
bottom: 15px;
|
||||||
|
font-size: 0.95rem;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
<div id=overlay>
|
||||||
|
<div class=container>
|
||||||
|
<h1>Security Checkpoint</h1>
|
||||||
|
<p>Verifying your browser to protect from automated abuse. This may take a few seconds...</p>
|
||||||
|
<div class=spinner-container>
|
||||||
|
<div class=spinner></div>
|
||||||
|
<div id=status class=status>Redirecting</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button id="infoBtn" class="info-button">Why am I seeing this?</button>
|
||||||
|
<div id=verification-data data-target={{TargetPath}} data-request-id={{RequestID}}></div>
|
||||||
|
<div id="infoModal" class="modal-overlay">
|
||||||
|
<div class="modal-content">
|
||||||
|
<button id="modalCloseBtn" class="modal-close-btn">×</button>
|
||||||
|
<h2>Why am I seeing this?</h2>
|
||||||
|
<p>This website uses Checkpoint to protect against automated systems that can overload the server.</p>
|
||||||
|
<p>Checkpoint uses a couple quick proof of challenges similar to concepts like Hashcash. It's a small task for
|
||||||
|
your browser that's barely noticeable to you, but it becomes a significant hurdle for bots trying to access
|
||||||
|
the site en masse.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
BIN
pages/interstitial/webfont/Poppins-Regular.woff2
Normal file
BIN
pages/interstitial/webfont/Poppins-Regular.woff2
Normal file
Binary file not shown.
BIN
pages/interstitial/webfont/Poppins-SemiBold.woff2
Normal file
BIN
pages/interstitial/webfont/Poppins-SemiBold.woff2
Normal file
Binary file not shown.
1
pages/ipfilter/datacenter.html
Normal file
1
pages/ipfilter/datacenter.html
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
Blocked (Datacenter)
|
||||||
1
pages/ipfilter/default.html
Normal file
1
pages/ipfilter/default.html
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
Blocked (Default)
|
||||||
1
pages/ipfilter/india.html
Normal file
1
pages/ipfilter/india.html
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
Blocked (India)
|
||||||
1274
pages/stats/stats.html
Normal file
1274
pages/stats/stats.html
Normal file
File diff suppressed because it is too large
Load diff
470
plugins/ipfilter.js
Normal file
470
plugins/ipfilter.js
Normal file
|
|
@ -0,0 +1,470 @@
|
||||||
|
import { registerPlugin, loadConfig, rootDir } from '../index.js';
|
||||||
|
import fs from 'fs';
|
||||||
|
import { dirname, join } from 'path';
|
||||||
|
import { fileURLToPath } from 'url';
|
||||||
|
import maxmind from 'maxmind';
|
||||||
|
import { AhoCorasick } from 'string-dsa';
|
||||||
|
import { getRealIP } from '../utils/network.js';
|
||||||
|
import { createGunzip } from 'zlib';
|
||||||
|
import tarStream from 'tar-stream';
|
||||||
|
import { Buffer } from 'buffer';
|
||||||
|
import * as logs from '../utils/logs.js';
|
||||||
|
import { recordEvent } from './stats.js';
|
||||||
|
|
||||||
|
const cfg = {};
|
||||||
|
await loadConfig('ipfilter', cfg);
|
||||||
|
|
||||||
|
// Map configuration to internal structure
|
||||||
|
const enabled = cfg.Core.Enabled;
|
||||||
|
const accountId = cfg.Core.AccountID || process.env.MAXMIND_ACCOUNT_ID;
|
||||||
|
const licenseKey = cfg.Core.LicenseKey || process.env.MAXMIND_LICENSE_KEY;
|
||||||
|
const dbUpdateInterval = cfg.Core.DBUpdateIntervalHours;
|
||||||
|
|
||||||
|
const ipBlockCacheTTL = cfg.Cache.IPBlockCacheTTLSec * 1000;
|
||||||
|
const ipBlockCacheMaxEntries = cfg.Cache.IPBlockCacheMaxEntries;
|
||||||
|
|
||||||
|
const blockedCountryCodes = new Set(cfg.Blocking.CountryCodes);
|
||||||
|
const blockedContinentCodes = new Set(cfg.Blocking.ContinentCodes);
|
||||||
|
const defaultBlockPage = cfg.Blocking.DefaultBlockPage;
|
||||||
|
|
||||||
|
// Process ASN blocks
|
||||||
|
const blockedASNs = {};
|
||||||
|
const asnGroupBlockPages = {};
|
||||||
|
for (const [group, config] of Object.entries(cfg.ASN || {})) {
|
||||||
|
blockedASNs[group] = config.Numbers || [];
|
||||||
|
asnGroupBlockPages[group] = config.BlockPage;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Process ASN name blocks
|
||||||
|
const blockedASNNames = {};
|
||||||
|
for (const [group, config] of Object.entries(cfg.ASNNames || {})) {
|
||||||
|
blockedASNNames[group] = config.Patterns || [];
|
||||||
|
if (config.BlockPage) {
|
||||||
|
asnGroupBlockPages[group] = config.BlockPage;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const countryBlockPages = cfg.CountryBlockPages || {};
|
||||||
|
const continentBlockPages = cfg.ContinentBlockPages || {};
|
||||||
|
|
||||||
|
const ipBlockCache = new Map();
|
||||||
|
|
||||||
|
const blockPageCache = new Map();
|
||||||
|
async function loadBlockPage(filePath) {
|
||||||
|
if (!blockPageCache.has(filePath)) {
|
||||||
|
try {
|
||||||
|
const txt = await fs.promises.readFile(filePath, 'utf8');
|
||||||
|
blockPageCache.set(filePath, txt);
|
||||||
|
} catch {
|
||||||
|
blockPageCache.set(filePath, null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return blockPageCache.get(filePath);
|
||||||
|
}
|
||||||
|
|
||||||
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||||
|
|
||||||
|
const geoIPCountryDBPath = join(rootDir, 'data/GeoLite2-Country.mmdb');
|
||||||
|
const geoIPASNDBPath = join(rootDir, 'data/GeoLite2-ASN.mmdb');
|
||||||
|
const updateTimestampPath = join(rootDir, 'data/ipfilter_update.json');
|
||||||
|
|
||||||
|
let geoipCountryReader, geoipASNReader;
|
||||||
|
|
||||||
|
let isReloading = false;
|
||||||
|
let reloadLock = Promise.resolve();
|
||||||
|
|
||||||
|
async function getLastUpdateTimestamp() {
|
||||||
|
try {
|
||||||
|
if (fs.existsSync(updateTimestampPath)) {
|
||||||
|
const data = await fs.promises.readFile(updateTimestampPath, 'utf8');
|
||||||
|
const json = JSON.parse(data);
|
||||||
|
return json.lastUpdated || 0;
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
logs.warn('ipfilter', `Failed to read last update timestamp: ${err}`);
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function saveUpdateTimestamp() {
|
||||||
|
try {
|
||||||
|
const timestamp = Date.now();
|
||||||
|
await fs.promises.writeFile(
|
||||||
|
updateTimestampPath,
|
||||||
|
JSON.stringify({ lastUpdated: timestamp }),
|
||||||
|
'utf8',
|
||||||
|
);
|
||||||
|
return timestamp;
|
||||||
|
} catch (err) {
|
||||||
|
logs.error('ipfilter', `Failed to save update timestamp: ${err}`);
|
||||||
|
return Date.now();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure the update timestamp file exists on first run
|
||||||
|
if (!fs.existsSync(updateTimestampPath)) {
|
||||||
|
try {
|
||||||
|
await saveUpdateTimestamp();
|
||||||
|
} catch (err) {
|
||||||
|
logs.error('ipfilter', `Failed to initialize update timestamp file: ${err}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Download GeoIP databases if missing
|
||||||
|
async function downloadGeoIPDatabases() {
|
||||||
|
if (!licenseKey || !accountId) {
|
||||||
|
logs.warn(
|
||||||
|
'ipfilter',
|
||||||
|
'No MaxMind credentials found; skipping GeoIP database download. Please set MAXMIND_ACCOUNT_ID and MAXMIND_LICENSE_KEY environment variables or add AccountID and LicenseKey to config/ipfilter.toml',
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const editions = [
|
||||||
|
{ id: 'GeoLite2-Country', filePath: geoIPCountryDBPath },
|
||||||
|
{ id: 'GeoLite2-ASN', filePath: geoIPASNDBPath },
|
||||||
|
];
|
||||||
|
for (const { id, filePath } of editions) {
|
||||||
|
if (!fs.existsSync(filePath)) {
|
||||||
|
logs.plugin('ipfilter', `Downloading ${id} database...`);
|
||||||
|
const url = `https://download.maxmind.com/app/geoip_download?edition_id=${id}&license_key=${licenseKey}&suffix=tar.gz`;
|
||||||
|
const res = await fetch(url);
|
||||||
|
if (!res.ok) {
|
||||||
|
logs.error(
|
||||||
|
'ipfilter',
|
||||||
|
`Failed to download ${id} database: ${res.status} ${res.statusText}`,
|
||||||
|
);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const tempTar = join(rootDir, 'data', `${id}.tar.gz`);
|
||||||
|
// write response body into a .tar.gz file
|
||||||
|
const arrayBuf = await res.arrayBuffer();
|
||||||
|
await fs.promises.writeFile(tempTar, Buffer.from(arrayBuf));
|
||||||
|
// extract .mmdb files from the downloaded tar.gz
|
||||||
|
const extract = tarStream.extract();
|
||||||
|
extract.on('entry', (header, stream, next) => {
|
||||||
|
if (header.name.endsWith('.mmdb')) {
|
||||||
|
const filename = header.name.split('/').pop();
|
||||||
|
const outPath = join(rootDir, 'data', filename);
|
||||||
|
const ws = fs.createWriteStream(outPath);
|
||||||
|
stream
|
||||||
|
.pipe(ws)
|
||||||
|
.on('finish', next)
|
||||||
|
.on('error', (err) => {
|
||||||
|
logs.error('ipfilter', `Extraction error: ${err}`);
|
||||||
|
next();
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
stream.resume();
|
||||||
|
next();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
await new Promise((resolve, reject) => {
|
||||||
|
fs.createReadStream(tempTar)
|
||||||
|
.pipe(createGunzip())
|
||||||
|
.pipe(extract)
|
||||||
|
.on('finish', resolve)
|
||||||
|
.on('error', reject);
|
||||||
|
});
|
||||||
|
await fs.promises.unlink(tempTar);
|
||||||
|
logs.plugin('ipfilter', `${id} database downloaded and extracted.`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await downloadGeoIPDatabases();
|
||||||
|
|
||||||
|
async function loadGeoDatabases() {
|
||||||
|
if (isReloading) {
|
||||||
|
await reloadLock;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
isReloading = true;
|
||||||
|
let lockResolve;
|
||||||
|
reloadLock = new Promise((resolve) => {
|
||||||
|
lockResolve = resolve;
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
const countryStats = fs.statSync(geoIPCountryDBPath);
|
||||||
|
const asnStats = fs.statSync(geoIPASNDBPath);
|
||||||
|
|
||||||
|
if (countryStats.size > 1024 && asnStats.size > 1024) {
|
||||||
|
logs.plugin('ipfilter', 'Initializing GeoIP databases from disk...');
|
||||||
|
const newCountryReader = await maxmind.open(geoIPCountryDBPath);
|
||||||
|
const newASNReader = await maxmind.open(geoIPASNDBPath);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const testIP = '8.8.8.8';
|
||||||
|
const countryTest = newCountryReader.get(testIP);
|
||||||
|
const asnTest = newASNReader.get(testIP);
|
||||||
|
|
||||||
|
if (!countryTest || !asnTest) {
|
||||||
|
throw new Error('Database validation failed: test lookups returned empty results');
|
||||||
|
}
|
||||||
|
} catch (validationErr) {
|
||||||
|
logs.error('ipfilter', `GeoIP database validation failed: ${validationErr}`);
|
||||||
|
|
||||||
|
try {
|
||||||
|
await newCountryReader.close();
|
||||||
|
} catch (e) {}
|
||||||
|
try {
|
||||||
|
await newASNReader.close();
|
||||||
|
} catch (e) {}
|
||||||
|
throw new Error('Database validation failed');
|
||||||
|
}
|
||||||
|
|
||||||
|
const oldCountryReader = geoipCountryReader;
|
||||||
|
const oldASNReader = geoipASNReader;
|
||||||
|
|
||||||
|
geoipCountryReader = newCountryReader;
|
||||||
|
geoipASNReader = newASNReader;
|
||||||
|
if (oldCountryReader || oldASNReader) {
|
||||||
|
logs.plugin('ipfilter', 'GeoIP databases reloaded and active');
|
||||||
|
} else {
|
||||||
|
logs.plugin('ipfilter', 'GeoIP databases loaded and active');
|
||||||
|
}
|
||||||
|
|
||||||
|
ipBlockCache.clear();
|
||||||
|
|
||||||
|
await saveUpdateTimestamp();
|
||||||
|
|
||||||
|
if (oldCountryReader || oldASNReader) {
|
||||||
|
setTimeout(async () => {
|
||||||
|
if (oldCountryReader) {
|
||||||
|
try {
|
||||||
|
await oldCountryReader.close();
|
||||||
|
} catch (e) {}
|
||||||
|
}
|
||||||
|
if (oldASNReader) {
|
||||||
|
try {
|
||||||
|
await oldASNReader.close();
|
||||||
|
} catch (e) {}
|
||||||
|
}
|
||||||
|
logs.plugin('ipfilter', 'Old GeoIP database instances closed successfully');
|
||||||
|
}, 5000);
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
logs.warn(
|
||||||
|
'ipfilter',
|
||||||
|
'GeoIP database files are empty or too small. IP filtering will be disabled.',
|
||||||
|
);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
logs.error('ipfilter', `Failed to load GeoIP databases: ${err}`);
|
||||||
|
return false;
|
||||||
|
} finally {
|
||||||
|
isReloading = false;
|
||||||
|
lockResolve();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function checkAndUpdateDatabases() {
|
||||||
|
if (isReloading) return false;
|
||||||
|
|
||||||
|
const lastUpdate = await getLastUpdateTimestamp();
|
||||||
|
const now = Date.now();
|
||||||
|
const hoursSinceUpdate = (now - lastUpdate) / (1000 * 60 * 60);
|
||||||
|
|
||||||
|
if (hoursSinceUpdate >= dbUpdateInterval) {
|
||||||
|
logs.plugin(
|
||||||
|
'ipfilter',
|
||||||
|
`GeoIP databases last updated ${hoursSinceUpdate.toFixed(1)} hours ago, reloading...`,
|
||||||
|
);
|
||||||
|
return await loadGeoDatabases();
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function startPeriodicDatabaseUpdates() {
|
||||||
|
// Calculate interval in milliseconds
|
||||||
|
const intervalMs = dbUpdateInterval * 60 * 60 * 1000;
|
||||||
|
|
||||||
|
// Schedule periodic updates
|
||||||
|
setInterval(async () => {
|
||||||
|
try {
|
||||||
|
await checkAndUpdateDatabases();
|
||||||
|
} catch (err) {
|
||||||
|
logs.error('ipfilter', `Failed during periodic database update: ${err}`);
|
||||||
|
}
|
||||||
|
}, intervalMs);
|
||||||
|
|
||||||
|
logs.plugin('ipfilter', `Scheduled GeoIP database updates every ${dbUpdateInterval} hours`);
|
||||||
|
}
|
||||||
|
|
||||||
|
await loadGeoDatabases();
|
||||||
|
|
||||||
|
startPeriodicDatabaseUpdates();
|
||||||
|
|
||||||
|
const asnNameMatchers = new Map();
|
||||||
|
for (const [group, names] of Object.entries(blockedASNNames)) {
|
||||||
|
asnNameMatchers.set(group, new AhoCorasick(names));
|
||||||
|
}
|
||||||
|
|
||||||
|
function cacheAndReturn(ip, blocked, blockType, blockValue, customPage, asnOrgName) {
|
||||||
|
const expiresAt = Date.now() + ipBlockCacheTTL;
|
||||||
|
ipBlockCache.set(ip, { blocked, blockType, blockValue, customPage, asnOrgName, expiresAt });
|
||||||
|
// Enforce maximum cache size
|
||||||
|
if (ipBlockCacheMaxEntries > 0 && ipBlockCache.size > ipBlockCacheMaxEntries) {
|
||||||
|
// Remove the oldest entry (first key in insertion order)
|
||||||
|
const oldestKey = ipBlockCache.keys().next().value;
|
||||||
|
ipBlockCache.delete(oldestKey);
|
||||||
|
}
|
||||||
|
return [blocked, blockType, blockValue, customPage, asnOrgName];
|
||||||
|
}
|
||||||
|
|
||||||
|
function isBlockedIPExtended(ip) {
|
||||||
|
const now = Date.now();
|
||||||
|
const entry = ipBlockCache.get(ip);
|
||||||
|
if (entry) {
|
||||||
|
if (entry.expiresAt > now) {
|
||||||
|
// Refresh recency by re-inserting entry
|
||||||
|
ipBlockCache.delete(ip);
|
||||||
|
ipBlockCache.set(ip, entry);
|
||||||
|
return [entry.blocked, entry.blockType, entry.blockValue, entry.customPage, entry.asnOrgName];
|
||||||
|
} else {
|
||||||
|
// Entry expired, remove it
|
||||||
|
ipBlockCache.delete(ip);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const countryReader = geoipCountryReader;
|
||||||
|
const asnReader = geoipASNReader;
|
||||||
|
|
||||||
|
if (!countryReader || !asnReader) {
|
||||||
|
return [false, '', '', '', ''];
|
||||||
|
}
|
||||||
|
|
||||||
|
let countryInfo;
|
||||||
|
try {
|
||||||
|
countryInfo = countryReader.get(ip);
|
||||||
|
} catch (e) {}
|
||||||
|
if (countryInfo?.country && blockedCountryCodes.has(countryInfo.country.iso_code)) {
|
||||||
|
const page = countryBlockPages[countryInfo.country.iso_code] || defaultBlockPage;
|
||||||
|
return cacheAndReturn(ip, true, 'country', countryInfo.country.iso_code, page, '');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (countryInfo?.continent && blockedContinentCodes.has(countryInfo.continent.code)) {
|
||||||
|
const page = continentBlockPages[countryInfo.continent.code] || defaultBlockPage;
|
||||||
|
return cacheAndReturn(ip, true, 'continent', countryInfo.continent.code, page, '');
|
||||||
|
}
|
||||||
|
|
||||||
|
let asnInfo;
|
||||||
|
try {
|
||||||
|
asnInfo = asnReader.get(ip);
|
||||||
|
} catch (e) {}
|
||||||
|
if (asnInfo?.autonomous_system_number) {
|
||||||
|
const asn = asnInfo.autonomous_system_number;
|
||||||
|
const orgName = asnInfo.autonomous_system_organization || '';
|
||||||
|
|
||||||
|
for (const [group, arr] of Object.entries(blockedASNs)) {
|
||||||
|
if (arr.includes(asn)) {
|
||||||
|
const page = asnGroupBlockPages[group] || defaultBlockPage;
|
||||||
|
return cacheAndReturn(ip, true, 'asn', group, page, orgName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const [group, matcher] of asnNameMatchers.entries()) {
|
||||||
|
const matches = matcher.find(orgName);
|
||||||
|
if (matches.length) {
|
||||||
|
const page = asnGroupBlockPages[group] || defaultBlockPage;
|
||||||
|
return cacheAndReturn(ip, true, 'asn', group, page, orgName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return cacheAndReturn(ip, false, '', '', '', '');
|
||||||
|
}
|
||||||
|
|
||||||
|
function IPBlockMiddleware() {
|
||||||
|
return async (request, server) => {
|
||||||
|
const clientIP = getRealIP(request, server);
|
||||||
|
logs.plugin('ipfilter', `Incoming request from IP: ${clientIP}`);
|
||||||
|
const [blocked, blockType, blockValue, customPage, asnOrgName] = isBlockedIPExtended(clientIP);
|
||||||
|
|
||||||
|
if (blocked) {
|
||||||
|
recordEvent('ipfilter.block', {
|
||||||
|
type: blockType,
|
||||||
|
value: blockValue,
|
||||||
|
asn_org: asnOrgName,
|
||||||
|
ip: clientIP, // Include the IP address for stats
|
||||||
|
});
|
||||||
|
const url = new URL(request.url);
|
||||||
|
|
||||||
|
if (url.pathname.startsWith('/api')) {
|
||||||
|
return new Response(
|
||||||
|
JSON.stringify({
|
||||||
|
error: 'Access denied from your location or network.',
|
||||||
|
reason: 'geoip',
|
||||||
|
type: blockType,
|
||||||
|
value: blockValue,
|
||||||
|
asn_org: asnOrgName,
|
||||||
|
}),
|
||||||
|
{
|
||||||
|
status: 403,
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Normalize page paths by stripping leading slash
|
||||||
|
const cleanCustomPage = customPage.replace(/^\/+/, '');
|
||||||
|
const cleanDefaultPage = defaultBlockPage.replace(/^\/+/, '');
|
||||||
|
|
||||||
|
let html = '';
|
||||||
|
logs.plugin(
|
||||||
|
'ipfilter',
|
||||||
|
`Block pages: custom="${cleanCustomPage}", default="${cleanDefaultPage}"`,
|
||||||
|
);
|
||||||
|
logs.plugin('ipfilter', 'Searching for block page in the following locations:');
|
||||||
|
const paths = [
|
||||||
|
// allow absolute paths relative to project root first
|
||||||
|
join(rootDir, cleanCustomPage),
|
||||||
|
];
|
||||||
|
// Fallback to default block page if custom page isn't found
|
||||||
|
if (customPage !== defaultBlockPage) {
|
||||||
|
paths.push(
|
||||||
|
// check default page at root directory
|
||||||
|
join(rootDir, cleanDefaultPage),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const p of paths) {
|
||||||
|
logs.plugin('ipfilter', `Trying block page at: ${p}`);
|
||||||
|
const content = await loadBlockPage(p);
|
||||||
|
logs.plugin('ipfilter', `Load result for ${p}: ${content ? 'FOUND' : 'NOT FOUND'}`);
|
||||||
|
if (content) {
|
||||||
|
html = content;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (html) {
|
||||||
|
const output = html.replace('{{.ASNName}}', asnOrgName || 'Blocked Network');
|
||||||
|
return new Response(output, {
|
||||||
|
status: 403,
|
||||||
|
headers: { 'Content-Type': 'text/html; charset=utf-8' },
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
return new Response('Access denied from your location or network.', {
|
||||||
|
status: 403,
|
||||||
|
headers: { 'Content-Type': 'text/plain' },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return undefined;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (enabled) {
|
||||||
|
registerPlugin('ipfilter', IPBlockMiddleware());
|
||||||
|
} else {
|
||||||
|
logs.plugin('ipfilter', 'IP filter plugin disabled via config');
|
||||||
|
}
|
||||||
|
|
||||||
|
export { checkAndUpdateDatabases, loadGeoDatabases };
|
||||||
303
plugins/proxy.js
Normal file
303
plugins/proxy.js
Normal file
|
|
@ -0,0 +1,303 @@
|
||||||
|
import { registerPlugin, loadConfig } from '../index.js';
|
||||||
|
import * as logs from '../utils/logs.js';
|
||||||
|
|
||||||
|
const proxyConfig = {};
|
||||||
|
await loadConfig('proxy', proxyConfig);
|
||||||
|
|
||||||
|
// Map configuration to internal structure
|
||||||
|
const enabled = proxyConfig.Core.Enabled;
|
||||||
|
const wsTimeout = proxyConfig.Timeouts.WebSocketTimeoutMs;
|
||||||
|
const upstreamTimeout = proxyConfig.Timeouts.UpstreamTimeoutMs;
|
||||||
|
|
||||||
|
// Build proxy mappings from array format
|
||||||
|
const proxyMappings = {};
|
||||||
|
proxyConfig.Mapping.forEach((mapping) => {
|
||||||
|
proxyMappings[mapping.Host] = mapping.Target;
|
||||||
|
});
|
||||||
|
|
||||||
|
logs.plugin('proxy', `Proxy mappings loaded: ${JSON.stringify(proxyMappings)}`);
|
||||||
|
|
||||||
|
const HOP_BY_HOP_HEADERS = [
|
||||||
|
'connection',
|
||||||
|
'keep-alive',
|
||||||
|
'proxy-authenticate',
|
||||||
|
'proxy-authorization',
|
||||||
|
'te',
|
||||||
|
'trailer',
|
||||||
|
'transfer-encoding',
|
||||||
|
'upgrade',
|
||||||
|
];
|
||||||
|
|
||||||
|
// Connect to upstream WebSocket with handshake timeout
|
||||||
|
async function connectUpstreamWebSocket(url, headers) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const ws = new WebSocket(url, { headers });
|
||||||
|
const timer = setTimeout(() => {
|
||||||
|
ws.close();
|
||||||
|
reject(new Error('timeout'));
|
||||||
|
}, wsTimeout);
|
||||||
|
ws.onopen = () => {
|
||||||
|
clearTimeout(timer);
|
||||||
|
resolve(ws);
|
||||||
|
};
|
||||||
|
ws.onerror = (err) => {
|
||||||
|
clearTimeout(timer);
|
||||||
|
reject(err);
|
||||||
|
};
|
||||||
|
ws.onclose = () => {
|
||||||
|
clearTimeout(timer);
|
||||||
|
reject(new Error('closed'));
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function createProxyResponse(targetURL, request) {
|
||||||
|
try {
|
||||||
|
const url = new URL(request.url);
|
||||||
|
const targetPathAndQuery = url.pathname + url.search;
|
||||||
|
const fullTargetURL = new URL(targetPathAndQuery, targetURL).toString();
|
||||||
|
|
||||||
|
const startTime = Date.now();
|
||||||
|
|
||||||
|
const outgoingHeaders = new Headers(request.headers);
|
||||||
|
outgoingHeaders.delete('host');
|
||||||
|
|
||||||
|
// Set proper host header for the target
|
||||||
|
const targetHost = new URL(targetURL).host;
|
||||||
|
outgoingHeaders.set('host', targetHost);
|
||||||
|
|
||||||
|
// Forward the original host as X-Forwarded-Host for applications that need it
|
||||||
|
outgoingHeaders.set('x-forwarded-host', request.headers.get('host'));
|
||||||
|
outgoingHeaders.set('x-forwarded-proto', url.protocol.replace(':', ''));
|
||||||
|
|
||||||
|
// Preserve important headers for authentication
|
||||||
|
// Don't delete content-length or transfer-encoding here, handle them properly below
|
||||||
|
const options = {
|
||||||
|
method: request.method,
|
||||||
|
headers: outgoingHeaders,
|
||||||
|
// Follow redirects automatically for GET; forward redirects for non-GET
|
||||||
|
// Absolute requirement: DONT REMOVE
|
||||||
|
redirect: request.method === 'GET' ? 'follow' : 'manual',
|
||||||
|
credentials: 'include',
|
||||||
|
};
|
||||||
|
|
||||||
|
const isChunked = request.headers.get('transfer-encoding')?.toLowerCase() === 'chunked';
|
||||||
|
|
||||||
|
// Define methods that can legitimately have request bodies
|
||||||
|
const methodsWithBody = new Set(['POST', 'PUT', 'PATCH', 'DELETE']);
|
||||||
|
|
||||||
|
if (methodsWithBody.has(request.method) && request.body) {
|
||||||
|
if (isChunked) {
|
||||||
|
logs.plugin('proxy', `De-chunking request body for ${request.method} ${request.url}`);
|
||||||
|
try {
|
||||||
|
const bodyBuffer = await request.arrayBuffer();
|
||||||
|
options.body = bodyBuffer;
|
||||||
|
outgoingHeaders.set('content-length', String(bodyBuffer.byteLength));
|
||||||
|
outgoingHeaders.delete('transfer-encoding');
|
||||||
|
} catch (bufferError) {
|
||||||
|
logs.error('proxy', `Error buffering chunked request body: ${bufferError}`);
|
||||||
|
return new Response('Error processing chunked request body', { status: 500 });
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// For non-chunked bodies, preserve the body stream
|
||||||
|
options.body = request.body;
|
||||||
|
// Keep the original content-length if it exists
|
||||||
|
if (request.headers.has('content-length')) {
|
||||||
|
outgoingHeaders.set('content-length', request.headers.get('content-length'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add a timeout controller for the upstream fetch
|
||||||
|
const controller = new AbortController();
|
||||||
|
const timeoutId = setTimeout(() => {
|
||||||
|
logs.warn(
|
||||||
|
'proxy',
|
||||||
|
`Upstream request to ${fullTargetURL} timed out after ${upstreamTimeout}ms`,
|
||||||
|
);
|
||||||
|
controller.abort();
|
||||||
|
}, upstreamTimeout);
|
||||||
|
|
||||||
|
let response;
|
||||||
|
try {
|
||||||
|
response = await fetch(fullTargetURL, {
|
||||||
|
...options,
|
||||||
|
signal: controller.signal,
|
||||||
|
verbose: true,
|
||||||
|
});
|
||||||
|
} catch (fetchErr) {
|
||||||
|
clearTimeout(timeoutId);
|
||||||
|
if (fetchErr.name === 'AbortError') {
|
||||||
|
logs.error('proxy', `Upstream fetch aborted for ${fullTargetURL} (likely due to timeout)`);
|
||||||
|
return new Response('Gateway Timeout', { status: 504 });
|
||||||
|
}
|
||||||
|
throw fetchErr;
|
||||||
|
}
|
||||||
|
clearTimeout(timeoutId);
|
||||||
|
|
||||||
|
const latency = Date.now() - startTime;
|
||||||
|
|
||||||
|
logs.plugin(
|
||||||
|
'proxy',
|
||||||
|
`Proxied request to: ${fullTargetURL} (${response.status} ${response.statusText}) (${latency}ms)`,
|
||||||
|
);
|
||||||
|
|
||||||
|
const responseHeaders = new Headers(response.headers);
|
||||||
|
|
||||||
|
// Remove hop-by-hop headers
|
||||||
|
HOP_BY_HOP_HEADERS.forEach((h) => responseHeaders.delete(h));
|
||||||
|
|
||||||
|
// Remove content-encoding and content-length headers
|
||||||
|
// This is necessary because Bun/fetch automatically decompresses the response body
|
||||||
|
// but leaves the content-encoding header, causing the browser to try to decompress already decompressed content
|
||||||
|
responseHeaders.delete('content-encoding');
|
||||||
|
responseHeaders.delete('content-length');
|
||||||
|
|
||||||
|
// Add proxy information
|
||||||
|
responseHeaders.set('X-Proxy-Latency', `${latency}ms`);
|
||||||
|
|
||||||
|
// Handle Set-Cookie headers - rewrite domain if needed
|
||||||
|
const setCookieHeaders = response.headers.getSetCookie ? response.headers.getSetCookie() : [];
|
||||||
|
if (setCookieHeaders.length > 0) {
|
||||||
|
responseHeaders.delete('set-cookie');
|
||||||
|
|
||||||
|
setCookieHeaders.forEach((cookieStr) => {
|
||||||
|
// Parse and potentially rewrite the cookie domain
|
||||||
|
let modifiedCookie = cookieStr;
|
||||||
|
|
||||||
|
// Remove domain restrictions that might prevent the cookie from working
|
||||||
|
modifiedCookie = modifiedCookie.replace(/;\s*domain=[^;]*/gi, '');
|
||||||
|
|
||||||
|
// If the cookie has SameSite=None, ensure it also has Secure
|
||||||
|
if (modifiedCookie.match(/samesite\s*=\s*none/i) && !modifiedCookie.match(/secure/i)) {
|
||||||
|
modifiedCookie += '; Secure';
|
||||||
|
}
|
||||||
|
|
||||||
|
// For local development, you might need to adjust SameSite
|
||||||
|
if (url.protocol === 'http:' && modifiedCookie.match(/samesite\s*=\s*none/i)) {
|
||||||
|
modifiedCookie = modifiedCookie.replace(/;\s*samesite=[^;]*/gi, '; SameSite=Lax');
|
||||||
|
modifiedCookie = modifiedCookie.replace(/;\s*secure/gi, '');
|
||||||
|
}
|
||||||
|
|
||||||
|
responseHeaders.append('set-cookie', modifiedCookie);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Response(response.body, {
|
||||||
|
status: response.status,
|
||||||
|
statusText: response.statusText,
|
||||||
|
headers: responseHeaders,
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
logs.error(
|
||||||
|
'proxy',
|
||||||
|
`Proxy error processing ${request.method} ${request.url}: ${err.message}${
|
||||||
|
err.cause ? ' - Cause: ' + err.cause : ''
|
||||||
|
}`,
|
||||||
|
);
|
||||||
|
let causeDetails = '';
|
||||||
|
if (err.cause) {
|
||||||
|
causeDetails = typeof err.cause === 'object' ? JSON.stringify(err.cause) : String(err.cause);
|
||||||
|
}
|
||||||
|
logs.error(
|
||||||
|
'proxy',
|
||||||
|
`Full error details: ${err.stack}${err.cause ? '\nCause: ' + causeDetails : ''}`,
|
||||||
|
);
|
||||||
|
return new Response('Bad Gateway', { status: 502 });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function proxyMiddleware() {
|
||||||
|
return async (request, server) => {
|
||||||
|
const url = new URL(request.url);
|
||||||
|
const path = url.pathname;
|
||||||
|
|
||||||
|
// Skip checkpoint endpoints
|
||||||
|
if (path.startsWith('/api/challenge') || path.startsWith('/api/verify')) return undefined;
|
||||||
|
|
||||||
|
// Skip static assets
|
||||||
|
if (path.startsWith('/webfont/') || path.startsWith('/js/')) return undefined;
|
||||||
|
|
||||||
|
// Get the hostname from the request
|
||||||
|
const hostname = request.headers.get('host')?.split(':')[0];
|
||||||
|
const target = proxyMappings[hostname];
|
||||||
|
if (!target) return undefined;
|
||||||
|
|
||||||
|
// Handle WebSocket upgrade requests
|
||||||
|
const upgradeHeader = request.headers.get('upgrade')?.toLowerCase();
|
||||||
|
if (upgradeHeader === 'websocket') {
|
||||||
|
const targetUrl = new URL(url.pathname + url.search, target);
|
||||||
|
targetUrl.protocol = targetUrl.protocol.replace(/^http/, 'ws');
|
||||||
|
|
||||||
|
// Forward important headers for WebSocket
|
||||||
|
const wsHeaders = new Headers();
|
||||||
|
if (request.headers.has('cookie')) wsHeaders.set('Cookie', request.headers.get('cookie'));
|
||||||
|
if (request.headers.has('authorization'))
|
||||||
|
wsHeaders.set('Authorization', request.headers.get('authorization'));
|
||||||
|
if (request.headers.has('origin')) wsHeaders.set('Origin', request.headers.get('origin'));
|
||||||
|
if (request.headers.has('sec-websocket-protocol'))
|
||||||
|
wsHeaders.set('Sec-WebSocket-Protocol', request.headers.get('sec-websocket-protocol'));
|
||||||
|
if (request.headers.has('sec-websocket-extensions'))
|
||||||
|
wsHeaders.set('Sec-WebSocket-Extensions', request.headers.get('sec-websocket-extensions'));
|
||||||
|
|
||||||
|
let upstream;
|
||||||
|
try {
|
||||||
|
// Convert Headers object to a plain object for the WebSocket constructor
|
||||||
|
const plainWsHeaders = {};
|
||||||
|
for (const [key, value] of wsHeaders) {
|
||||||
|
plainWsHeaders[key] = value;
|
||||||
|
}
|
||||||
|
upstream = await connectUpstreamWebSocket(targetUrl.toString(), plainWsHeaders);
|
||||||
|
} catch (err) {
|
||||||
|
logs.error('proxy', `Upstream WebSocket connection failed: ${err}`);
|
||||||
|
return new Response('Bad Gateway', { status: 502 });
|
||||||
|
}
|
||||||
|
// Upgrade incoming client connection and attach upstream socket
|
||||||
|
const ok = server.upgrade(request, { data: { upstream } });
|
||||||
|
if (!ok) {
|
||||||
|
logs.error('proxy', 'WebSocket upgrade failed');
|
||||||
|
upstream.close();
|
||||||
|
return new Response('Bad Gateway', { status: 502 });
|
||||||
|
}
|
||||||
|
logs.plugin('proxy', `WebSocket proxied to: ${targetUrl.toString()}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
return createProxyResponse(target, request);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// WebSocket handlers for proxying messages between client and upstream
|
||||||
|
export const proxyWebSocketHandler = {
|
||||||
|
open(ws) {
|
||||||
|
const upstream = ws.data.upstream;
|
||||||
|
upstream.onopen = () => logs.plugin('proxy', 'Upstream WebSocket connected');
|
||||||
|
// Forward messages from target to client
|
||||||
|
upstream.onmessage = (event) => ws.send(event.data);
|
||||||
|
upstream.onerror = (err) => {
|
||||||
|
logs.error('proxy', `Upstream WebSocket error: ${err}`);
|
||||||
|
ws.close(1011, 'Upstream error');
|
||||||
|
};
|
||||||
|
upstream.onclose = ({ code, reason }) => ws.close(code, reason);
|
||||||
|
},
|
||||||
|
message(ws, message) {
|
||||||
|
const upstream = ws.data.upstream;
|
||||||
|
// Forward messages from client to target
|
||||||
|
upstream.send(message);
|
||||||
|
},
|
||||||
|
close(ws, code, reason) {
|
||||||
|
const upstream = ws.data.upstream;
|
||||||
|
upstream.close(code, reason);
|
||||||
|
},
|
||||||
|
error(ws, err) {
|
||||||
|
logs.error('proxy', `WebSocket proxy error: ${err}`);
|
||||||
|
const upstream = ws.data.upstream;
|
||||||
|
upstream.close();
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
if (enabled) {
|
||||||
|
registerPlugin('proxy', proxyMiddleware());
|
||||||
|
} else {
|
||||||
|
logs.plugin('proxy', 'Proxy plugin disabled via config');
|
||||||
|
}
|
||||||
132
plugins/stats.js
Normal file
132
plugins/stats.js
Normal file
|
|
@ -0,0 +1,132 @@
|
||||||
|
import { registerPlugin, rootDir, loadConfig } from '../index.js';
|
||||||
|
import { Level } from 'level';
|
||||||
|
import ttl from 'level-ttl';
|
||||||
|
import fs from 'fs/promises';
|
||||||
|
import path from 'path';
|
||||||
|
import { fileURLToPath } from 'url';
|
||||||
|
import { Readable } from 'stream';
|
||||||
|
import cookie from 'cookie';
|
||||||
|
import { getRealIP } from '../utils/network.js';
|
||||||
|
import { parseDuration } from '../utils/time.js';
|
||||||
|
|
||||||
|
// Load stats configuration
|
||||||
|
const statsConfig = {};
|
||||||
|
await loadConfig('stats', statsConfig);
|
||||||
|
|
||||||
|
// Map configuration to internal structure
|
||||||
|
const enabled = statsConfig.Core.Enabled;
|
||||||
|
const statsTTL = parseDuration(statsConfig.Storage.StatsTTL);
|
||||||
|
const statsUIPath = statsConfig.WebUI.StatsUIPath;
|
||||||
|
const statsAPIPath = statsConfig.WebUI.StatsAPIPath;
|
||||||
|
|
||||||
|
// Determine __dirname for ES modules
|
||||||
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds createReadStream support to LevelDB instances using async iterator.
|
||||||
|
*/
|
||||||
|
function addReadStreamSupport(dbInstance) {
|
||||||
|
if (!dbInstance.createReadStream) {
|
||||||
|
dbInstance.createReadStream = (opts) =>
|
||||||
|
Readable.from(
|
||||||
|
(async function* () {
|
||||||
|
for await (const [key, value] of dbInstance.iterator(opts)) {
|
||||||
|
yield { key, value };
|
||||||
|
}
|
||||||
|
})(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return dbInstance;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize LevelDB for stats under db/stats with TTL and stream support
|
||||||
|
const statsDBPath = path.join(rootDir, 'db', 'stats');
|
||||||
|
await fs.mkdir(statsDBPath, { recursive: true });
|
||||||
|
let rawStatsDB = new Level(statsDBPath, { valueEncoding: 'json' });
|
||||||
|
rawStatsDB = addReadStreamSupport(rawStatsDB);
|
||||||
|
const statsDB = ttl(rawStatsDB, { defaultTTL: statsTTL });
|
||||||
|
addReadStreamSupport(statsDB);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Record a stat event with a metric name and optional data.
|
||||||
|
* @param {string} metric
|
||||||
|
* @param {object} data
|
||||||
|
*/
|
||||||
|
function recordEvent(metric, data = {}) {
|
||||||
|
// Skip if statsDB is not initialized
|
||||||
|
if (typeof statsDB === 'undefined' || !statsDB || typeof statsDB.put !== 'function') {
|
||||||
|
console.warn(`stats: cannot record "${metric}", statsDB not available`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const timestamp = Date.now();
|
||||||
|
// key includes metric and timestamp and a random suffix to avoid collisions
|
||||||
|
const key = `${metric}:${timestamp}:${Math.random().toString(36).slice(2, 8)}`;
|
||||||
|
try {
|
||||||
|
// Use callback form to avoid promise chaining
|
||||||
|
statsDB.put(key, { timestamp, metric, ...data }, (err) => {
|
||||||
|
if (err) console.error('stats: failed to record event', err);
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
console.error('stats: failed to record event', err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handler for serving the stats HTML UI
|
||||||
|
async function handleStatsPage(request) {
|
||||||
|
const url = new URL(request.url);
|
||||||
|
if (url.pathname !== statsUIPath) return undefined;
|
||||||
|
try {
|
||||||
|
const html = await fs.readFile(path.join(__dirname, 'stats.html'), 'utf8');
|
||||||
|
return new Response(html, {
|
||||||
|
status: 200,
|
||||||
|
headers: { 'Content-Type': 'text/html; charset=utf-8' },
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
return new Response('Stats UI not found', { status: 404 });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handler for stats API
|
||||||
|
async function handleStatsAPI(request) {
|
||||||
|
const url = new URL(request.url);
|
||||||
|
if (url.pathname !== statsAPIPath) return undefined;
|
||||||
|
const metric = url.searchParams.get('metric');
|
||||||
|
const start = parseInt(url.searchParams.get('start') || '0', 10);
|
||||||
|
const end = parseInt(url.searchParams.get('end') || `${Date.now()}`, 10);
|
||||||
|
const result = [];
|
||||||
|
// Iterate over keys for this metric in the time range
|
||||||
|
for await (const [key, value] of statsDB.iterator({
|
||||||
|
gte: `${metric}:${start}`,
|
||||||
|
lte: `${metric}:${end}\uffff`,
|
||||||
|
})) {
|
||||||
|
result.push(value);
|
||||||
|
}
|
||||||
|
return new Response(JSON.stringify(result), {
|
||||||
|
status: 200,
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Middleware for stats plugin
|
||||||
|
function StatsMiddleware() {
|
||||||
|
return async (request) => {
|
||||||
|
// Always serve stats UI and API first, bypassing auth
|
||||||
|
const pageResp = await handleStatsPage(request);
|
||||||
|
if (pageResp) return pageResp;
|
||||||
|
const apiResp = await handleStatsAPI(request);
|
||||||
|
if (apiResp) return apiResp;
|
||||||
|
|
||||||
|
// For any other routes, do not handle
|
||||||
|
return undefined;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Register the stats plugin
|
||||||
|
if (enabled) {
|
||||||
|
registerPlugin('stats', StatsMiddleware());
|
||||||
|
} else {
|
||||||
|
console.log('Stats plugin disabled via config');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Export recordEvent for other plugins to use
|
||||||
|
export { recordEvent };
|
||||||
41
utils/logs.js
Normal file
41
utils/logs.js
Normal file
|
|
@ -0,0 +1,41 @@
|
||||||
|
const seenConfigs = new Set();
|
||||||
|
|
||||||
|
export function init(msg) {
|
||||||
|
console.log(msg);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function plugin(_name, msg) {
|
||||||
|
console.log(msg);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function config(name, msg) {
|
||||||
|
if (!seenConfigs.has(name)) {
|
||||||
|
console.log(`Config ${msg} for ${name}`);
|
||||||
|
seenConfigs.add(name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function db(msg) {
|
||||||
|
console.log(msg);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function server(msg) {
|
||||||
|
console.log(msg);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function section(title) {
|
||||||
|
console.log(`\n=== ${title.toUpperCase()} ===`);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function warn(_category, msg) {
|
||||||
|
console.warn(`WARNING: ${msg}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function error(_category, msg) {
|
||||||
|
console.error(`ERROR: ${msg}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// General message function for bullet items
|
||||||
|
export function msg(msg) {
|
||||||
|
console.log(msg);
|
||||||
|
}
|
||||||
13
utils/network.js
Normal file
13
utils/network.js
Normal file
|
|
@ -0,0 +1,13 @@
|
||||||
|
export function getRealIP(request, server) {
|
||||||
|
let ip = request.headers.get('x-forwarded-for') || request.headers.get('x-real-ip');
|
||||||
|
if (ip?.includes(',')) ip = ip.split(',')[0].trim();
|
||||||
|
if (!ip && server) {
|
||||||
|
ip = server.remoteAddress;
|
||||||
|
}
|
||||||
|
if (!ip) {
|
||||||
|
const url = new URL(request.url);
|
||||||
|
ip = url.hostname;
|
||||||
|
}
|
||||||
|
if (ip?.startsWith('::ffff:')) ip = ip.slice(7);
|
||||||
|
return ip;
|
||||||
|
}
|
||||||
28
utils/plugins.js
Normal file
28
utils/plugins.js
Normal file
|
|
@ -0,0 +1,28 @@
|
||||||
|
import { resolve, extname, sep, isAbsolute } from 'path';
|
||||||
|
import { pathToFileURL } from 'url';
|
||||||
|
import { rootDir } from '../index.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Securely import a JavaScript module from within the application root.
|
||||||
|
* Prevents path traversal and disallows non-.js extensions.
|
||||||
|
*
|
||||||
|
* @param {string} relPath - The relative path to the module from the application root.
|
||||||
|
* @returns {Promise<any>} The imported module.
|
||||||
|
*/
|
||||||
|
export async function secureImportModule(relPath) {
|
||||||
|
if (isAbsolute(relPath)) {
|
||||||
|
throw new Error('Absolute paths are not allowed for module imports');
|
||||||
|
}
|
||||||
|
if (relPath.includes('..')) {
|
||||||
|
throw new Error('Relative paths containing .. are not allowed for module imports');
|
||||||
|
}
|
||||||
|
if (extname(relPath) !== '.js') {
|
||||||
|
throw new Error(`Only .js files can be imported: ${relPath}`);
|
||||||
|
}
|
||||||
|
const absPath = resolve(rootDir, relPath);
|
||||||
|
if (!absPath.startsWith(rootDir + sep)) {
|
||||||
|
throw new Error(`Module path outside of application root: ${relPath}`);
|
||||||
|
}
|
||||||
|
const url = pathToFileURL(absPath).href;
|
||||||
|
return import(url);
|
||||||
|
}
|
||||||
72
utils/proof.js
Normal file
72
utils/proof.js
Normal file
|
|
@ -0,0 +1,72 @@
|
||||||
|
import crypto from 'crypto';
|
||||||
|
import { getRealIP } from './network.js';
|
||||||
|
|
||||||
|
export function generateChallenge(checkpointConfig) {
|
||||||
|
const challenge = crypto.randomBytes(16).toString('hex');
|
||||||
|
const salt = crypto.randomBytes(checkpointConfig.SaltLength).toString('hex');
|
||||||
|
return { challenge, salt };
|
||||||
|
}
|
||||||
|
|
||||||
|
export function calculateHash(input) {
|
||||||
|
return crypto.createHash('sha256').update(input).digest('hex');
|
||||||
|
}
|
||||||
|
|
||||||
|
export function verifyPoW(challenge, salt, nonce, difficulty) {
|
||||||
|
const hash = calculateHash(challenge + salt + nonce);
|
||||||
|
return hash.startsWith('0'.repeat(difficulty));
|
||||||
|
}
|
||||||
|
|
||||||
|
export function checkPoSTimes(times, enableCheck, ratio) {
|
||||||
|
if (!Array.isArray(times) || times.length !== 3) {
|
||||||
|
throw new Error('Invalid PoS run times length');
|
||||||
|
}
|
||||||
|
const minT = Math.min(...times);
|
||||||
|
const maxT = Math.max(...times);
|
||||||
|
if (enableCheck && maxT > minT * ratio) {
|
||||||
|
throw new Error(`PoS run times inconsistent (ratio ${maxT / minT} > ${ratio})`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const challengeStore = new Map();
|
||||||
|
|
||||||
|
export function generateRequestID(request, checkpointConfig) {
|
||||||
|
const { challenge, salt } = generateChallenge(checkpointConfig);
|
||||||
|
const posSeed = crypto.randomBytes(32).toString('hex');
|
||||||
|
const requestID = crypto.randomBytes(16).toString('hex');
|
||||||
|
const params = {
|
||||||
|
Challenge: challenge,
|
||||||
|
Salt: salt,
|
||||||
|
Difficulty: checkpointConfig.Difficulty,
|
||||||
|
ExpiresAt: Date.now() + checkpointConfig.ChallengeExpiration,
|
||||||
|
CreatedAt: Date.now(),
|
||||||
|
ClientIP: getRealIP(request),
|
||||||
|
PoSSeed: posSeed,
|
||||||
|
};
|
||||||
|
challengeStore.set(requestID, params);
|
||||||
|
return requestID;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getChallengeParams(requestID) {
|
||||||
|
return challengeStore.get(requestID);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function deleteChallenge(requestID) {
|
||||||
|
challengeStore.delete(requestID);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function verifyPoS(hashes, times, checkpointConfig) {
|
||||||
|
if (!Array.isArray(hashes) || hashes.length !== 3) {
|
||||||
|
throw new Error('Invalid PoS hashes length');
|
||||||
|
}
|
||||||
|
if (!Array.isArray(times) || times.length !== 3) {
|
||||||
|
throw new Error('Invalid PoS run times length');
|
||||||
|
}
|
||||||
|
if (hashes[0] !== hashes[1] || hashes[1] !== hashes[2]) {
|
||||||
|
throw new Error('PoS hashes do not match');
|
||||||
|
}
|
||||||
|
if (hashes[0].length !== 64) {
|
||||||
|
throw new Error('Invalid PoS hash length');
|
||||||
|
}
|
||||||
|
|
||||||
|
checkPoSTimes(times, checkpointConfig.CheckPoSTimes, checkpointConfig.PoSTimeConsistencyRatio);
|
||||||
|
}
|
||||||
20
utils/time.js
Normal file
20
utils/time.js
Normal file
|
|
@ -0,0 +1,20 @@
|
||||||
|
export function parseDuration(str) {
|
||||||
|
if (!str) return 0;
|
||||||
|
const m = /^([0-9]+)(ms|s|m|h|d)$/.exec(str);
|
||||||
|
if (!m) return 0;
|
||||||
|
const val = parseInt(m[1], 10);
|
||||||
|
switch (m[2]) {
|
||||||
|
case 'ms':
|
||||||
|
return val;
|
||||||
|
case 's':
|
||||||
|
return val * 1000;
|
||||||
|
case 'm':
|
||||||
|
return val * 60 * 1000;
|
||||||
|
case 'h':
|
||||||
|
return val * 60 * 60 * 1000;
|
||||||
|
case 'd':
|
||||||
|
return val * 24 * 60 * 60 * 1000;
|
||||||
|
default:
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue