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