commit c3103e6ecb13a0effea5a8ad7891d77af09bb26c Author: King Omar Date: Sat Jul 25 15:02:18 2026 +1000 PaywallFree: read paywalled articles free β€” Radical reader + proxy + method hub Co-Authored-By: Claude Opus 4.8 (1M context) diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..538af3f --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +node_modules/ +.wrangler/ +.dev.vars +*.log diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..68e9e21 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,1554 @@ +{ + "name": "paywall", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "paywall", + "version": "1.0.0", + "dependencies": { + "hono": "^4.6.0" + }, + "devDependencies": { + "wrangler": "^4.0.0" + } + }, + "node_modules/@cloudflare/kv-asset-handler": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/@cloudflare/kv-asset-handler/-/kv-asset-handler-0.5.0.tgz", + "integrity": "sha512-jxQYkj8dSIzc0cD6cMMNdOc1UVjqSqu8BZdor5s8cGjW2I8BjODt/kWPVdY+u9zj3ms75Q5qaZgnxUad83+eAg==", + "dev": true, + "license": "MIT OR Apache-2.0", + "engines": { + "node": ">=22.0.0" + } + }, + "node_modules/@cloudflare/unenv-preset": { + "version": "2.16.1", + "resolved": "https://registry.npmjs.org/@cloudflare/unenv-preset/-/unenv-preset-2.16.1.tgz", + "integrity": "sha512-ECxObrMfyTl5bhQf/lZCXwo5G6xX9IAUo+nDMKK4SZ8m4Jvvxp52vilxyySSWh2YTZz8+HQ07qGH/2rEom1vDw==", + "dev": true, + "license": "MIT OR Apache-2.0", + "peerDependencies": { + "unenv": "2.0.0-rc.24", + "workerd": ">1.20260305.0 <2.0.0-0" + }, + "peerDependenciesMeta": { + "workerd": { + "optional": true + } + } + }, + "node_modules/@cloudflare/workerd-darwin-64": { + "version": "1.20260722.1", + "resolved": "https://registry.npmjs.org/@cloudflare/workerd-darwin-64/-/workerd-darwin-64-1.20260722.1.tgz", + "integrity": "sha512-vZOP8vIS3NwnuaO+gz0FZ7kIGeiO3bZmxV35Ph9zOXKSREhDFlH7wQ7mkCdhW3O4jnXsew+XT7b+DNEI2CcJGQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=16" + } + }, + "node_modules/@cloudflare/workerd-darwin-arm64": { + "version": "1.20260722.1", + "resolved": "https://registry.npmjs.org/@cloudflare/workerd-darwin-arm64/-/workerd-darwin-arm64-1.20260722.1.tgz", + "integrity": "sha512-EmIQymihDq6WNdER4+LF8Qn80yqayBUpJ+tkOO7wmY8pmgfyXjIUFNXotl21AHovTeu2seR7HdVUgeN/BilCWw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=16" + } + }, + "node_modules/@cloudflare/workerd-linux-64": { + "version": "1.20260722.1", + "resolved": "https://registry.npmjs.org/@cloudflare/workerd-linux-64/-/workerd-linux-64-1.20260722.1.tgz", + "integrity": "sha512-jvZ3k9fxcnEn04s80CgIYxQfpOyAiz/8qC42DP8EBa9tR27qWyg9wmm31zIobVlrgBZn/+8NfdP73avRGcQOjQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16" + } + }, + "node_modules/@cloudflare/workerd-linux-arm64": { + "version": "1.20260722.1", + "resolved": "https://registry.npmjs.org/@cloudflare/workerd-linux-arm64/-/workerd-linux-arm64-1.20260722.1.tgz", + "integrity": "sha512-BOSB55SMNdy+DA5uj2WirgiNanpHGis5PVvXH1wSfvjRKr4JGgWK+EZzxz0RFUo6QjjQQC/NimEzNZ7va7jmKg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16" + } + }, + "node_modules/@cloudflare/workerd-windows-64": { + "version": "1.20260722.1", + "resolved": "https://registry.npmjs.org/@cloudflare/workerd-windows-64/-/workerd-windows-64-1.20260722.1.tgz", + "integrity": "sha512-sYM8YgUpKnRz2xjvdJLX1Ojzoi4MlA4gk8WTTExhGydjYB2UTs5NIbv0ZmpKgMoK9io3ixgmiW56ZnTbcWOdiA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=16" + } + }, + "node_modules/@cspotcode/source-map-support": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", + "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "0.3.9" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@emnapi/runtime": { + "version": "1.11.2", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.2.tgz", + "integrity": "sha512-kyOl3X0DuTiT1h2ft8r2fYO8JYtU9a9Xis/zBSiGArNaagCOWx90N1k2wxp18czFDH+OgcWGb5ZP/XMt3dcyPA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.1.tgz", + "integrity": "sha512-Svl7tq8k/08+p6CXPpRjQ1fKX+1odH/BQbb48fV6fj3CWHhsoIOoY87w1oHXm0qEpkIK3ZfVgp0hed3XBXzXMQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.1.tgz", + "integrity": "sha512-0k2F129Xdio1TdJfzJ8sy1Q47vUD2NnwdhiAf7drUN1EBTfPf4hsFCtmMgu/6m8JSzsBrlmVjudMBQqOfG8usQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.1.tgz", + "integrity": "sha512-34EGEbCIAgosYz6goLcopX6Mo7NyGv9tfwEM2/7Ce2VcVRk568iSvniGWcUXIy7wEDR1wzolcxcriFVrWYcwBg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.1.tgz", + "integrity": "sha512-dbwY7ltSMDWsRatcRpCnES4F+im88OCUgGZjy52shC7GqHRE/cYlxNbB4Z4UpJswpcc4Qxd2oE/ufM0p61IKng==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.1.tgz", + "integrity": "sha512-TZbWkQY7kvTAXbXUT7uVACR5cMHsDiSz9z7ZKAX/RTq/WJEk3QyRr0wZpNhBDX+/0CtdqUIJlOiodQcta6tY3Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.1.tgz", + "integrity": "sha512-zfdzgK9ACBNZLI/CyHTOx81SyNbM6YXn7rxSgX97VjyiPl9W1i4Ka4fgKECEoFCKGpvBj5qArWIGgQjOwkgskQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.1.tgz", + "integrity": "sha512-wG2EA8ENdEI0qhkSZMjfqrdY+ziCYCPMmtZjjIwOmXFjmyzEHn+UUxk5of+SYsjtfs3VpnlC7QLzSI5hY/rOAw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.1.tgz", + "integrity": "sha512-i7dZ9vQgnvSCzi/rYCXNgtF/U+eKZNJBzu3eTQbRgHnM7tNSizLOkRFAl3qzVc/Op/u5YkHHa4pf/3DOYHthLQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.1.tgz", + "integrity": "sha512-qVXBOHQS+d5Y722GwJzJUtOLlX7km3CraOaGormF1pDtPd2C/l1SHRPgjLunLGe51Sh5YYWKMFDyV4SxgMQYTQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.1.tgz", + "integrity": "sha512-yHs+0uc8+nvEAfAfxrWQKK5peSNzBc4PegcMO0EJ2hT71uA7vB8Ihg2e77R2P7SG5uYjPbHlLLmve4LLLRCf0g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.1.tgz", + "integrity": "sha512-d1z4ZuP0ajrfz/FhGT4vv278rX8KnPPJx8i5+AtK7TYbx9Le9F1hyzurZpkEyjkGa9dUGhQow4C1NmeGvqxN2w==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.1.tgz", + "integrity": "sha512-M5sRjUVZrkm1OAPR3dlOYzNmN+loZKGVi1VUQGrwuqLcbR6qeAz+famMhjASeH3YVKvZz+zT1jlh/keC3Rj/lg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.1.tgz", + "integrity": "sha512-mRObBZeHh2OxcBFPWE/FjylkRgZdYuiTR3vaTozquCGOH14iP9oN4x4Ge81CoIDYQrXmIxpFumJBu5MtZpnQJQ==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.1.tgz", + "integrity": "sha512-slScBsMAb3GFDcdrCgLwZtPYRoH2H/youv10QiZyRjmsP48fznoveWytSgCI/R0ZcUgpc0ZhIUEx6LHts8yrfQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.1.tgz", + "integrity": "sha512-kw0owk1o0GFETUJyW0jc0G4Yzs0BHZn0JDZ8JRT088vjJYX777BAs1fDGxAC+q831qOs2DTC96mNsG2opdfyyQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.1.tgz", + "integrity": "sha512-/lAIjX8aYFRByhh6L5rYtPEDRqa9de/4V/juOXcta5frjvzXO4/sqEtyytse0g3zZFuWu5cDN0MkLz2qRDD2Ag==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.1.tgz", + "integrity": "sha512-u/anNYF2mmVOEDwLtnQ1wOr3EZ9sTNGLWrsYGYwHWzGA3Si84IOkHXlbWTD1NB+9/1lcnweYKO54uhxZydNzfA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.1.tgz", + "integrity": "sha512-oks0DYbLwWMmaakTsCb+zL4E+aHRVLom9IJZOAthMQEPiQmydXHkziYEsGYRx0uNV/IjEKGAV941JzH02pflqw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.1.tgz", + "integrity": "sha512-aeL6lAnN89Hz43Mlh1G8ARasbuoYvSITDEx0tHh5b7jJnHcssqgjy9Yx430GDpmCa6OyrKoS0aNRjKundRizGg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.1.tgz", + "integrity": "sha512-MEFJe5C3R8pwXdZ5Y21oo6m7ePiS0d9pWucn99O/wvyJZChoIQKrQDxKrGeW8F5+T0okTHesAmDeiHDTIq0V/Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.1.tgz", + "integrity": "sha512-i/ZLIOafE0Z8cI/XANJAixoJL/uRAoS2xOA3rb0xN+KK0K177cMAsQYkzHtBrtMXAKuAc7HGgcWiZ/sRC1Nxgw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.1.tgz", + "integrity": "sha512-ge+Z7EXFNt2BO1oAMsVpiQ8EwndV9i1xXerAeTIK7AtPs3bKFXQM7nlRxDSIUIMeueR1CNXxqztLzdNeReKBJg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.1.tgz", + "integrity": "sha512-BEjgtECkL3vY+SaSQ6nzVfiALUeFxpawyp8Jmf5PtYhf1Ug40N1h/hxlhts+f1FvSvarEigdxS3BlSMI2PJLcQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.1.tgz", + "integrity": "sha512-lCv9eK/H6ZJWbE7bh2nw54CZ9M2nupBxJcTsdk/QQnWkdSjKGuxmmH8/GWrlT1eMmZfn4dGcCjRte397WqfQXA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.1.tgz", + "integrity": "sha512-zvb/mB2bSCoJOpoCBgYKKpX6YM6mJBlBUVUtVj41DlZJVEB6/0CKlRYxP5wWl1C1ILiCoAU5wZZ4q1P3qeS6Eg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.1.tgz", + "integrity": "sha512-bm4Mowrv+GXMlpWX++EcXw/iLyd1o3+bJkC2DkWXYVvgZCqD/bSj9ctZeAMC3cIxgjRVR2Dufaiu4YPxr5gW1A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@img/colour": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@img/colour/-/colour-1.1.0.tgz", + "integrity": "sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@img/sharp-darwin-arm64": { + "version": "0.35.2", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.35.2.tgz", + "integrity": "sha512-eEieHsMksAW4IiO5NzauESRl2D2qz3J/kwUxUrSfV06A93eEaRfMpHXyUb1mAqrR7i8U9A0GRqE9pjn6u1Jjpg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-arm64": "1.3.1" + } + }, + "node_modules/@img/sharp-darwin-x64": { + "version": "0.35.2", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.35.2.tgz", + "integrity": "sha512-BaktuGPCeHJMARpodR8jK4uKiZrPAy9WrfQW0sdI37clracq8Bp01AYS3SZgi5FS/y5twa9t4+LIuuxQjqRrWw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-x64": "1.3.1" + } + }, + "node_modules/@img/sharp-freebsd-wasm32": { + "version": "0.35.2", + "resolved": "https://registry.npmjs.org/@img/sharp-freebsd-wasm32/-/sharp-freebsd-wasm32-0.35.2.tgz", + "integrity": "sha512-YoAxdnd8hPUkvLHd3bWY+YA8nw3xM/RyRopYucNsWHVSan8NLVM3X2volsfoRDcXdUJPg6tXahSd7HXPK7lRnw==", + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "dependencies": { + "@img/sharp-wasm32": "0.35.2" + }, + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-darwin-arm64": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.3.1.tgz", + "integrity": "sha512-4V/M3roRMTYjiwZY9IOVQOE8OyeCxFAkYmyZDrZl51uOKjibm3oeEJ4WAmLxutAfzFbC9jqUiPs2gbnGflH+7g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-darwin-x64": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.3.1.tgz", + "integrity": "sha512-c0/DxItpJv2+dGhgycJBBgotdqruGYDvA79drdh0MD1dFpy7JzJ/PlXwi1H4rFf0eTy8tgbI91aHDnZIceY3jQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.3.1.tgz", + "integrity": "sha512-aGGy9aWzXgHBG7HNyQPWorZthlp7+x6fDRoPAQbGO3ThcttuTyKIx3NuSHb6zb4gBNq6/yNn9f1cy9nFKS/Vmg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm64": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.3.1.tgz", + "integrity": "sha512-JznefmcK9j1JKPz8AkQDh89kjojubyfOasWBPKfzMIhPwsgDy9evpE/naJTXXXmghS1iFwR8u/kTwh/I2/+GCw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-ppc64": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.3.1.tgz", + "integrity": "sha512-1EkwGNCZk6iWNCMWqrvdJ+r1j0PT1zIz60CNPhYnJlK/zyeWqlsPZIe+ocBVqPF8k/Ssee/NCk+tE9Ryrko6ng==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-riscv64": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-riscv64/-/sharp-libvips-linux-riscv64-1.3.1.tgz", + "integrity": "sha512-Ilays+w2bXdnxzxtQdmXR62u8o8GYa3eL4+Gr+1KiE4xperMZUslRaVPJwwPkzlHEjGfXAfRVAa/7CYCtSqsBw==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-s390x": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.3.1.tgz", + "integrity": "sha512-VfBwVHQTbRoj4XlpA/KLZ7ltgMpz+4WSejFzQ+GnoImjo1PtEJ59QB2qR1xQEeRPYIkNrPIm2L4cICMvz4C2ew==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-x64": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.3.1.tgz", + "integrity": "sha512-+c8ukgwU62DS54nCAjw7keOfHUkmr0B5QHEdcOqRnodF/MNXJbVI8Eopoj4B/0H8Asr65I+A4Amrn7a85/md6A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-arm64": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.3.1.tgz", + "integrity": "sha512-qlKb/pwbkAi1WMsJrYHk7CuDrd12s27U2QnRhFYUoJNrRCmkosMTttuRFat/DDB3IlDm5qE1TJgZ4JDnHX8Ldw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-x64": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.3.1.tgz", + "integrity": "sha512-yO21HwoUVLN8Qa+/SBjQLMYwBWAVJjeGPNe+hc0OUeMeifEtJqu5a1c4HayE1nNpDih9y3/KkoltfkDodmKAlg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-linux-arm": { + "version": "0.35.2", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.35.2.tgz", + "integrity": "sha512-SE4kzF2mepn6z+6E7L6lsV8FzuLL6IPQdyX8ZiwROAG/G8td+hP/m7FsFPwidtrF19gvajuC9l6TxAVcsA4S7A==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm": "1.3.1" + } + }, + "node_modules/@img/sharp-linux-arm64": { + "version": "0.35.2", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.35.2.tgz", + "integrity": "sha512-af12Pnd0ZGu2HfP8NayB0kk6eC/lrfbQE6HlR4jD+34wdJ1Vw9TF6TMn6ZvffT+WgqVsl0hRbmNvz2u/23VmwA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm64": "1.3.1" + } + }, + "node_modules/@img/sharp-linux-ppc64": { + "version": "0.35.2", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.35.2.tgz", + "integrity": "sha512-hYSBm7zcNtDCozCxQHYZJiu63b/bXsgRZuOxCIBZsStMM9Vap47iFHdbX4kCvQsblPB/k+clhELpdQJHQLSHvg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-ppc64": "1.3.1" + } + }, + "node_modules/@img/sharp-linux-riscv64": { + "version": "0.35.2", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-riscv64/-/sharp-linux-riscv64-0.35.2.tgz", + "integrity": "sha512-qQt0Kc13+Hoan/Awq/qMSQw3L+RI1NCRPgD5cUJ/1WSSmIoysLOc72jlRM3E0OHN9Yr313jgeQ2T+zW+F03QFA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-riscv64": "1.3.1" + } + }, + "node_modules/@img/sharp-linux-s390x": { + "version": "0.35.2", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.35.2.tgz", + "integrity": "sha512-E4fLLfRPzDLlEeDaTzI98OFLcv++WL5ChLLMwPoVd0CIoZQqupBSNbOisPL5am9XsbQ9T84+iiMpUvbFtkunbA==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-s390x": "1.3.1" + } + }, + "node_modules/@img/sharp-linux-x64": { + "version": "0.35.2", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.35.2.tgz", + "integrity": "sha512-gi0zFJJRLswfCZmHtJdikXPOc5u7qamSOS3NHedLqLd4W8Q0NqjdBr6TTRIgsfFjqfTsHFgdfvJ9LwqSgcHiAA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-x64": "1.3.1" + } + }, + "node_modules/@img/sharp-linuxmusl-arm64": { + "version": "0.35.2", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.35.2.tgz", + "integrity": "sha512-siWbOW1u6HFnFLrp0waKyW7VEf7jYvcDWdrXEFa8AkdAQgEvuu5Fz8/Y70w9EeqAdwDtfU012BhEHHaDqvQNzg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-arm64": "1.3.1" + } + }, + "node_modules/@img/sharp-linuxmusl-x64": { + "version": "0.35.2", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.35.2.tgz", + "integrity": "sha512-YBqMMcjDi4QGYiSn4vNOYBhmlC4z5AXqkOUUqI2e0AFA4urNv4ESgOgwNl3K+4etQhha0twXlzeF20bbULm9Yg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-x64": "1.3.1" + } + }, + "node_modules/@img/sharp-wasm32": { + "version": "0.35.2", + "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.35.2.tgz", + "integrity": "sha512-Mrv4JQNYVQ94xH+jzZ9r+gowleN8mv2FTgKT+PI6bx5C0G8TdNYndu161pg2i7uoBwxy2ImPMHrJOM2LZef7Bw==", + "dev": true, + "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT", + "optional": true, + "dependencies": { + "@emnapi/runtime": "^1.11.1" + }, + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-webcontainers-wasm32": { + "version": "0.35.2", + "resolved": "https://registry.npmjs.org/@img/sharp-webcontainers-wasm32/-/sharp-webcontainers-wasm32-0.35.2.tgz", + "integrity": "sha512-QNV27pxs9wpApEiCfvHM1RDoP1w1+2KrUWWDPEhEwg+latvOrfuhWrHWZKwdSFwU6jh3myjw/yOCRsUIuOft3g==", + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "@img/sharp-wasm32": "0.35.2" + }, + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-arm64": { + "version": "0.35.2", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.35.2.tgz", + "integrity": "sha512-BiVRYc/t6/Vl3e1hBx0hugG4oN9Pydf4fgMSpxTQJmwGUg/YoXTWHiFeRymHfCZzifxu4F4rpk/I67D0LQ20wQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-ia32": { + "version": "0.35.2", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.35.2.tgz", + "integrity": "sha512-YYEhx9PImCC7T0tI8JDMi4DB9LwLCXCU5OWNYEXAxh5Q1ShKkyC6byxzoBJ3gEFDnH2lQckWuDe70G7mB2XJog==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-x64": { + "version": "0.35.2", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.35.2.tgz", + "integrity": "sha512-imoOyBcoM/iiUr4J6VPpCNjPnjvP/Gks95898yB8YqoGGYmHYbOyCuNv9FMhFgtaiHFGbHW8bxKqRV6VjtXThQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", + "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.0.3", + "@jridgewell/sourcemap-codec": "^1.4.10" + } + }, + "node_modules/@poppinss/colors": { + "version": "4.1.6", + "resolved": "https://registry.npmjs.org/@poppinss/colors/-/colors-4.1.6.tgz", + "integrity": "sha512-H9xkIdFswbS8n1d6vmRd8+c10t2Qe+rZITbbDHHkQixH5+2x1FDGmi/0K+WgWiqQFKPSlIYB7jlH6Kpfn6Fleg==", + "dev": true, + "license": "MIT", + "dependencies": { + "kleur": "^4.1.5" + } + }, + "node_modules/@poppinss/dumper": { + "version": "0.6.5", + "resolved": "https://registry.npmjs.org/@poppinss/dumper/-/dumper-0.6.5.tgz", + "integrity": "sha512-NBdYIb90J7LfOI32dOewKI1r7wnkiH6m920puQ3qHUeZkxNkQiFnXVWoE6YtFSv6QOiPPf7ys6i+HWWecDz7sw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@poppinss/colors": "^4.1.5", + "@sindresorhus/is": "^7.0.2", + "supports-color": "^10.0.0" + } + }, + "node_modules/@poppinss/exception": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@poppinss/exception/-/exception-1.2.3.tgz", + "integrity": "sha512-dCED+QRChTVatE9ibtoaxc+WkdzOSjYTKi/+uacHWIsfodVfpsueo3+DKpgU5Px8qXjgmXkSvhXvSCz3fnP9lw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@sindresorhus/is": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-7.2.0.tgz", + "integrity": "sha512-P1Cz1dWaFfR4IR+U13mqqiGsLFf1KbayybWwdd2vfctdV6hDpUkgCY0nKOLLTMSoRd/jJNjtbqzf13K8DCCXQw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sindresorhus/is?sponsor=1" + } + }, + "node_modules/@speed-highlight/core": { + "version": "1.2.17", + "resolved": "https://registry.npmjs.org/@speed-highlight/core/-/core-1.2.17.tgz", + "integrity": "sha512-Z92FwKpCtfaW1V0jTU/fh3QzYEZN8wDwrzRIBoADCJfn4mJCNcJN/XegifX7BDrQ8/h9Xh/JnbyMchL0FqXrkg==", + "dev": true, + "license": "CC0-1.0" + }, + "node_modules/blake3-wasm": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/blake3-wasm/-/blake3-wasm-2.1.5.tgz", + "integrity": "sha512-F1+K8EbfOZE49dtoPtmxUQrpXaBIl3ICvasLh+nJta0xkz+9kF/7uet9fLnwKqhDrmj6g+6K3Tw9yQPUg2ka5g==", + "dev": true, + "license": "MIT" + }, + "node_modules/cookie": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.1.1.tgz", + "integrity": "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/error-stack-parser-es": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/error-stack-parser-es/-/error-stack-parser-es-1.0.5.tgz", + "integrity": "sha512-5qucVt2XcuGMcEGgWI7i+yZpmpByQ8J1lHhcL7PwqCwu9FPP3VUXzT4ltHe5i2z9dePwEHcDVOAfSnHsOlCXRA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/esbuild": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.1.tgz", + "integrity": "sha512-HrJrvZv5ayxBzPfwphOoNzkzOIIlifzk0KJrGK2c8R4+LKpMtpYLQeUdjnwjWv/LZlkH2laZk+4w78pi99D4Vw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.28.1", + "@esbuild/android-arm": "0.28.1", + "@esbuild/android-arm64": "0.28.1", + "@esbuild/android-x64": "0.28.1", + "@esbuild/darwin-arm64": "0.28.1", + "@esbuild/darwin-x64": "0.28.1", + "@esbuild/freebsd-arm64": "0.28.1", + "@esbuild/freebsd-x64": "0.28.1", + "@esbuild/linux-arm": "0.28.1", + "@esbuild/linux-arm64": "0.28.1", + "@esbuild/linux-ia32": "0.28.1", + "@esbuild/linux-loong64": "0.28.1", + "@esbuild/linux-mips64el": "0.28.1", + "@esbuild/linux-ppc64": "0.28.1", + "@esbuild/linux-riscv64": "0.28.1", + "@esbuild/linux-s390x": "0.28.1", + "@esbuild/linux-x64": "0.28.1", + "@esbuild/netbsd-arm64": "0.28.1", + "@esbuild/netbsd-x64": "0.28.1", + "@esbuild/openbsd-arm64": "0.28.1", + "@esbuild/openbsd-x64": "0.28.1", + "@esbuild/openharmony-arm64": "0.28.1", + "@esbuild/sunos-x64": "0.28.1", + "@esbuild/win32-arm64": "0.28.1", + "@esbuild/win32-ia32": "0.28.1", + "@esbuild/win32-x64": "0.28.1" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/hono": { + "version": "4.12.32", + "resolved": "https://registry.npmjs.org/hono/-/hono-4.12.32.tgz", + "integrity": "sha512-XcuyW9qE2kJn07PkecMOBd5Vq/hMy7mmGw+idz1yblbg9N17ijJODrvPkn7/dwL3Kulj8LcRJ69DLOWf91dRUg==", + "license": "MIT", + "engines": { + "node": ">=16.9.0" + } + }, + "node_modules/kleur": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/kleur/-/kleur-4.1.5.tgz", + "integrity": "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/miniflare": { + "version": "4.20260722.0", + "resolved": "https://registry.npmjs.org/miniflare/-/miniflare-4.20260722.0.tgz", + "integrity": "sha512-LW6ABMhCx/yIEFBLC/DO4yAhdm2T/G7jp7pr5T2kj895+CCIaHZqpMXdW9O6YE48LcYcCJChwWc8aEs1vpbTXw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@cspotcode/source-map-support": "0.8.1", + "sharp": "0.35.2", + "undici": "7.28.0", + "workerd": "1.20260722.1", + "ws": "8.21.0", + "youch": "4.1.0-beta.10" + }, + "bin": { + "miniflare": "bootstrap.js" + }, + "engines": { + "node": ">=22.0.0" + } + }, + "node_modules/path-to-regexp": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-6.3.0.tgz", + "integrity": "sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "dev": true, + "license": "MIT" + }, + "node_modules/semver": { + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/sharp": { + "version": "0.35.2", + "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.35.2.tgz", + "integrity": "sha512-FVtFjtBCMiJS6yb5CX7Sop45WFMpeGw6oRKuJnXYgf/f1ms/D7LE/ZUSNxnW7rZ/dbslQWYkoqFHGPaDBtaK4w==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@img/colour": "^1.1.0", + "detect-libc": "^2.1.2", + "semver": "^7.8.4" + }, + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-darwin-arm64": "0.35.2", + "@img/sharp-darwin-x64": "0.35.2", + "@img/sharp-freebsd-wasm32": "0.35.2", + "@img/sharp-libvips-darwin-arm64": "1.3.1", + "@img/sharp-libvips-darwin-x64": "1.3.1", + "@img/sharp-libvips-linux-arm": "1.3.1", + "@img/sharp-libvips-linux-arm64": "1.3.1", + "@img/sharp-libvips-linux-ppc64": "1.3.1", + "@img/sharp-libvips-linux-riscv64": "1.3.1", + "@img/sharp-libvips-linux-s390x": "1.3.1", + "@img/sharp-libvips-linux-x64": "1.3.1", + "@img/sharp-libvips-linuxmusl-arm64": "1.3.1", + "@img/sharp-libvips-linuxmusl-x64": "1.3.1", + "@img/sharp-linux-arm": "0.35.2", + "@img/sharp-linux-arm64": "0.35.2", + "@img/sharp-linux-ppc64": "0.35.2", + "@img/sharp-linux-riscv64": "0.35.2", + "@img/sharp-linux-s390x": "0.35.2", + "@img/sharp-linux-x64": "0.35.2", + "@img/sharp-linuxmusl-arm64": "0.35.2", + "@img/sharp-linuxmusl-x64": "0.35.2", + "@img/sharp-webcontainers-wasm32": "0.35.2", + "@img/sharp-win32-arm64": "0.35.2", + "@img/sharp-win32-ia32": "0.35.2", + "@img/sharp-win32-x64": "0.35.2" + } + }, + "node_modules/supports-color": { + "version": "10.2.2", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-10.2.2.tgz", + "integrity": "sha512-SS+jx45GF1QjgEXQx4NJZV9ImqmO2NPz5FNsIHrsDjh2YsHnawpan7SNQ1o8NuhrbHZy9AZhIoCUiCeaW/C80g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "dev": true, + "license": "0BSD", + "optional": true + }, + "node_modules/undici": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-7.28.0.tgz", + "integrity": "sha512-cRZYrTDwWznlnRiPjggAGxZXanty6M8RV1ff8Wm4LWXBp7/IG8v5DnOm74DtUBp9OONpK75YlPnIjQqX0dBDtA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20.18.1" + } + }, + "node_modules/unenv": { + "version": "2.0.0-rc.24", + "resolved": "https://registry.npmjs.org/unenv/-/unenv-2.0.0-rc.24.tgz", + "integrity": "sha512-i7qRCmY42zmCwnYlh9H2SvLEypEFGye5iRmEMKjcGi7zk9UquigRjFtTLz0TYqr0ZGLZhaMHl/foy1bZR+Cwlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "pathe": "^2.0.3" + } + }, + "node_modules/workerd": { + "version": "1.20260722.1", + "resolved": "https://registry.npmjs.org/workerd/-/workerd-1.20260722.1.tgz", + "integrity": "sha512-NycKuc1x2onvsRfGGpM093vRlLFU2zHDAM0+APpccfg4+gZxDGCH27RmdDvkeBuoZyYqgLo3oAfF6re4mvC3vQ==", + "dev": true, + "hasInstallScript": true, + "license": "Apache-2.0", + "bin": { + "workerd": "bin/workerd" + }, + "engines": { + "node": ">=16" + }, + "optionalDependencies": { + "@cloudflare/workerd-darwin-64": "1.20260722.1", + "@cloudflare/workerd-darwin-arm64": "1.20260722.1", + "@cloudflare/workerd-linux-64": "1.20260722.1", + "@cloudflare/workerd-linux-arm64": "1.20260722.1", + "@cloudflare/workerd-windows-64": "1.20260722.1" + } + }, + "node_modules/wrangler": { + "version": "4.114.0", + "resolved": "https://registry.npmjs.org/wrangler/-/wrangler-4.114.0.tgz", + "integrity": "sha512-M65P25t5UHA1TIJfgZXDcj+YzVobgKdRguM2QPz0xnxLFuOcuE3ErgllDht0iaho7MS4o0g/Bb4YK2+GT+bibg==", + "dev": true, + "license": "MIT OR Apache-2.0", + "dependencies": { + "@cloudflare/kv-asset-handler": "0.5.0", + "@cloudflare/unenv-preset": "2.16.1", + "blake3-wasm": "2.1.5", + "esbuild": "0.28.1", + "miniflare": "4.20260722.0", + "path-to-regexp": "6.3.0", + "unenv": "2.0.0-rc.24", + "workerd": "1.20260722.1" + }, + "bin": { + "cf-wrangler": "bin/cf-wrangler.js", + "wrangler": "bin/wrangler.js", + "wrangler2": "bin/wrangler.js" + }, + "engines": { + "node": ">=22.0.0" + }, + "optionalDependencies": { + "fsevents": "2.3.3" + }, + "peerDependencies": { + "@cloudflare/workers-types": "^5.20260722.1" + }, + "peerDependenciesMeta": { + "@cloudflare/workers-types": { + "optional": true + } + } + }, + "node_modules/ws": { + "version": "8.21.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.21.0.tgz", + "integrity": "sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/youch": { + "version": "4.1.0-beta.10", + "resolved": "https://registry.npmjs.org/youch/-/youch-4.1.0-beta.10.tgz", + "integrity": "sha512-rLfVLB4FgQneDr0dv1oddCVZmKjcJ6yX6mS4pU82Mq/Dt9a3cLZQ62pDBL4AUO+uVrCvtWz3ZFUL2HFAFJ/BXQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@poppinss/colors": "^4.1.5", + "@poppinss/dumper": "^0.6.4", + "@speed-highlight/core": "^1.2.7", + "cookie": "^1.0.2", + "youch-core": "^0.3.3" + } + }, + "node_modules/youch-core": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/youch-core/-/youch-core-0.3.3.tgz", + "integrity": "sha512-ho7XuGjLaJ2hWHoK8yFnsUGy2Y5uDpqSTq1FkHLK4/oqKtyUU1AFbOOxY4IpC9f0fTLjwYbslUz0Po5BpD1wrA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@poppinss/exception": "^1.2.2", + "error-stack-parser-es": "^1.0.5" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..1882337 --- /dev/null +++ b/package.json @@ -0,0 +1,16 @@ +{ + "name": "paywall", + "version": "1.0.0", + "type": "module", + "private": true, + "scripts": { + "dev": "wrangler dev", + "deploy": "wrangler deploy" + }, + "dependencies": { + "hono": "^4.6.0" + }, + "devDependencies": { + "wrangler": "^4.0.0" + } +} diff --git a/src/index.js b/src/index.js new file mode 100644 index 0000000..b146f69 --- /dev/null +++ b/src/index.js @@ -0,0 +1,103 @@ +import { Hono } from 'hono' +import { fetchAsCrawler, stripPaywall, extractArticle } from './proxy.js' +import { homePage, readPage, readerPage, errorPage, howPage, aboutPage, bookmarklet } from './views.js' + +const app = new Hono() + +// Normalise a user-supplied URL: add https://, validate it's http(s). +function normalizeUrl(raw) { + if (!raw) return null + let u = raw.trim() + if (!/^https?:\/\//i.test(u)) { + // allow "example.com/x" and accidental "https:/x" typos + u = 'https://' + u.replace(/^\/+/, '').replace(/^https?:\/?/i, '') + } + try { + const parsed = new URL(u) + if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') return null + if (!parsed.hostname.includes('.')) return null + return parsed.href + } catch { return null } +} + +const HOST = (c) => c.req.header('host') || 'paywall.theradicalparty.com' + +app.get('/', (c) => c.html(homePage(HOST(c)))) +app.get('/how', (c) => c.html(howPage(HOST(c)))) +app.get('/about', (c) => c.html(aboutPage())) + +app.get('/bookmarklet.js', (c) => + c.text(bookmarklet(HOST(c)), 200, { 'Content-Type': 'text/plain; charset=utf-8' })) + +// Methods hub for a given article URL. +app.get('/read', (c) => { + const url = normalizeUrl(c.req.query('url')) + if (!url) return c.redirect('/') + return c.html(readPage(url, HOST(c))) +}) + +// Clean reader: fetch as crawler, extract article text. +app.get('/reader', async (c) => { + const url = normalizeUrl(c.req.query('url')) + if (!url) return c.redirect('/') + try { + const res = await fetchAsCrawler(url, { ref: c.req.query('ref') || 'google' }) + const ct = res.headers.get('content-type') || '' + if (!res.ok && res.status !== 403 && res.status !== 401) { + return c.html(errorPage(url, `origin returned ${res.status}`), 200) + } + if (!ct.includes('html')) { + // non-HTML (pdf, etc.) β€” just hand it through + return c.redirect(`/proxy?url=${encodeURIComponent(url)}`) + } + const html = await res.text() + const art = extractArticle(html, url) + if (!art.bodyHtml || art.wordCount < 80) { + return c.html(errorPage(url, 'not enough readable text'), 200) + } + return c.html(readerPage(art, url, HOST(c))) + } catch (e) { + return c.html(errorPage(url, e.message), 200) + } +}) + +// Full-page proxy: fetch as crawler, strip paywall scripts/overlays, keep layout. +app.get('/proxy', async (c) => { + const url = normalizeUrl(c.req.query('url')) + if (!url) return c.redirect('/') + try { + const res = await fetchAsCrawler(url, { ref: c.req.query('ref') || 'google' }) + const ct = res.headers.get('content-type') || '' + if (!ct.includes('html')) { + // pass binary/other content straight through + const h = new Headers(res.headers) + h.delete('content-security-policy'); h.delete('x-frame-options') + return new Response(res.body, { status: res.status, headers: h }) + } + return stripPaywall(res, url) + } catch (e) { + return c.html(errorPage(url, e.message), 200) + } +}) + +app.get('/health', (c) => c.json({ ok: true, service: 'paywall', ts: Date.now() })) + +app.get('/robots.txt', (c) => + c.text(`User-agent: *\nAllow: /$\nAllow: /how\nAllow: /about\nDisallow: /proxy\nDisallow: /reader\nDisallow: /read\nSitemap: https://${HOST(c)}/sitemap.xml\n`, 200, { 'Content-Type': 'text/plain' })) + +app.get('/sitemap.xml', (c) => { + const h = HOST(c) + const urls = ['/', '/how', '/about'].map((p) => `https://${h}${p}`).join('') + return c.text(`${urls}`, 200, { 'Content-Type': 'application/xml' }) +}) + +// Path-style: paywall.theradicalparty.com/https://example.com/article +app.get('/*', (c) => { + const path = c.req.path.slice(1) + const qs = new URL(c.req.url).search + const candidate = normalizeUrl(decodeURIComponent(path) + (qs || '')) + if (candidate) return c.redirect(`/read?url=${encodeURIComponent(candidate)}`) + return c.redirect('/') +}) + +export default app diff --git a/src/methods.js b/src/methods.js new file mode 100644 index 0000000..35120a5 --- /dev/null +++ b/src/methods.js @@ -0,0 +1,118 @@ +// Bypass methods. Each returns a target URL for a given article URL. +// `kind`: 'internal' = handled by our own Worker, 'external' = redirect to a 3rd-party service. + +export const METHODS = [ + { + id: 'reader', + kind: 'internal', + name: 'Radical Reader', + tagline: 'Clean, distraction-free text', + desc: 'We fetch the page as a search engine crawler, extract the article, and rebuild it as plain readable text. Best first try for most soft paywalls.', + icon: 'πŸ“–', + build: (u) => `/reader?url=${encodeURIComponent(u)}`, + }, + { + id: 'proxy', + kind: 'internal', + name: 'Radical Proxy', + tagline: 'Full page, paywall stripped', + desc: 'Loads the original page (styles and images intact) through our server, spoofing a Googlebot fetch and stripping paywall overlays. Use when Reader misses the layout.', + icon: 'πŸ›°οΈ', + build: (u) => `/proxy?url=${encodeURIComponent(u)}`, + }, + { + id: 'archive-ph', + kind: 'external', + name: 'Archive.today', + tagline: 'Community snapshots', + desc: 'Crowd-sourced permanent snapshots. Excellent for NYT, WSJ, FT, Bloomberg, The Atlantic and most hard paywalls.', + icon: 'πŸ—„οΈ', + build: (u) => `https://archive.ph/newest/${u}`, + }, + { + id: 'wayback', + kind: 'external', + name: 'Wayback Machine', + tagline: 'Internet Archive', + desc: "archive.org's public archive. Great fallback when a snapshot exists but Archive.today doesn't have one.", + icon: 'πŸ•°οΈ', + build: (u) => `https://web.archive.org/web/2/${u}`, + }, + { + id: 'google-cache', + kind: 'external', + name: 'Google Referer', + tagline: 'Arrive "from Google"', + desc: 'Opens the article as though you clicked it from Google search results β€” many metered sites unlock the first click.', + icon: 'πŸ”Ž', + build: (u) => `/proxy?url=${encodeURIComponent(u)}&ref=google`, + }, + { + id: 'freedium', + kind: 'external', + name: 'Freedium', + tagline: 'Medium articles', + desc: 'Purpose-built reader for Medium and Medium-hosted publications.', + icon: '✍️', + domains: ['medium.com', 'towardsdatascience.com', 'levelup.gitconnected.com', 'betterprogramming.pub', 'uxdesign.cc'], + build: (u) => `https://freedium.cfd/${u}`, + }, + { + id: 'twelveft', + kind: 'external', + name: '12ft.io', + tagline: 'Classic prepender', + desc: 'Loads the Google-cached version of the page. Hit or miss lately, but free and worth a shot.', + icon: 'πŸͺœ', + build: (u) => `https://12ft.io/proxy?q=${encodeURIComponent(u)}`, + }, + { + id: 'smry', + kind: 'external', + name: 'smry.ai', + tagline: 'AI summary + text', + desc: 'Pulls the article text and adds an AI summary. Handy when you just want the gist.', + icon: 'πŸ€–', + build: (u) => `https://smry.ai/${u}`, + }, + { + id: 'printfriendly', + kind: 'external', + name: 'PrintFriendly', + tagline: 'Print layout', + desc: 'Renders the print-optimised version of the page, which frequently omits the paywall script.', + icon: 'πŸ–¨οΈ', + build: (u) => `https://www.printfriendly.com/print/?url=${encodeURIComponent(u)}`, + }, + { + id: 'gtranslate', + kind: 'external', + name: 'Translate Trick', + tagline: 'Google Translate proxy', + desc: 'Routes the page through Google Translate (ENβ†’EN-ish), which acts as a lightweight proxy that dodges some scripts.', + icon: '🌐', + build: (u) => { + try { + const host = new URL(u).host + return `https://${host.replace(/\./g, '-')}.translate.goog/${new URL(u).pathname}${new URL(u).search}?_x_tr_sl=en&_x_tr_tl=fr&_x_tr_hl=en` + } catch { return `https://translate.google.com/translate?sl=en&tl=fr&u=${encodeURIComponent(u)}` } + }, + }, +] + +// Order methods so domain-specific + likely-to-work ones bubble to the top. +export function rankMethods(url) { + let host = '' + try { host = new URL(url).host.replace(/^www\./, '') } catch {} + const scored = METHODS.map((m) => { + let score = 0 + if (m.domains && m.domains.some((d) => host === d || host.endsWith('.' + d))) score += 100 + if (m.kind === 'internal') score += 10 + return { m, score } + }) + // stable sort by score desc, preserving original order for ties + return scored + .map((s, i) => ({ ...s, i })) + .sort((a, b) => b.score - a.score || a.i - b.i) + .map((s) => s.m) +} diff --git a/src/proxy.js b/src/proxy.js new file mode 100644 index 0000000..46bf00c --- /dev/null +++ b/src/proxy.js @@ -0,0 +1,200 @@ +// Server-side fetching + paywall stripping. +// Soft/metered paywalls serve full article HTML to search-engine + social crawlers +// for SEO, then hide it client-side with JS overlays. We fetch AS a crawler and +// either (a) drop the scripts + unlock the layout (proxy mode) or (b) extract the +// article text into a clean reader (reader mode). + +const CRAWLER_UAS = { + google: 'Mozilla/5.0 (compatible; Googlebot/2.1; +http://www.google.com/bot.html)', + bing: 'Mozilla/5.0 (compatible; bingbot/2.0; +http://www.bing.com/bingbot.htm)', + facebook: 'facebookexternalhit/1.1 (+http://www.facebook.com/externalhit_uatext.php)', + twitter: 'Twitterbot/1.0', +} + +const REFERERS = { + google: 'https://www.google.com/', + facebook: 'https://www.facebook.com/', + twitter: 'https://t.co/', + reddit: 'https://www.reddit.com/', +} + +export async function fetchAsCrawler(url, { ref = 'google', bot = 'google' } = {}) { + const res = await fetch(url, { + headers: { + 'User-Agent': CRAWLER_UAS[bot] || CRAWLER_UAS.google, + 'Referer': REFERERS[ref] || REFERERS.google, + 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8', + 'Accept-Language': 'en-US,en;q=0.9', + 'X-Forwarded-For': '66.249.66.1', + 'Cache-Control': 'no-cache', + }, + redirect: 'follow', + cf: { cacheTtl: 300, cacheEverything: false }, + }) + return res +} + +// ---- Full-page proxy: keep the layout, drop the paywall ---- + +const UNLOCK_CSS = ` + html, body { overflow: auto !important; position: static !important; height: auto !important; max-height: none !important; } + body { filter: none !important; -webkit-filter: none !important; } + * { -webkit-user-select: auto !important; user-select: auto !important; } + /* Common paywall / regwall vendors + generic markers */ + .tp-modal, .tp-backdrop, .tp-iframe-wrapper, #piano-id, [id^="piano"], + [class*="paywall"], [class*="Paywall"], [id*="paywall"], [id*="Paywall"], + [class*="regwall"], [class*="reg-wall"], [class*="regGate"], [class*="reg-gate"], + [class*="subscribe-wall"], [class*="subscription-wall"], [class*="meteredContent"], + [class*="poool"], .poool-widget, [class*="piano-"], [class*="pelcro"], + [data-testid*="paywall"], [data-testid*="gate"], [aria-label*="paywall" i], + [class*="article-gate"], [class*="content-gate"], [class*="barrier"] { + display: none !important; visibility: hidden !important; opacity: 0 !important; pointer-events: none !important; + } +` + +const UNLOCK_JS = `(function(){ + try { + var kill = /paywall|reg-?wall|reg-?gate|subscribe-?wall|meter|barrier|piano|poool|pelcro|content-gate|article-gate/i; + document.querySelectorAll('body *').forEach(function(el){ + try { + var id = (el.id||'') + ' ' + (el.className && el.className.toString ? el.className.toString() : ''); + if (kill.test(id)) { el.remove(); return; } + var s = getComputedStyle(el); + // strip full-viewport fixed/sticky overlays + if ((s.position === 'fixed' || s.position === 'sticky') && el.offsetHeight > innerHeight*0.6 && el.offsetWidth > innerWidth*0.6) { + el.remove(); + } + } catch(e){} + }); + // restore scrolling that JS-locked the body + document.documentElement.style.overflow='auto'; document.body.style.overflow='auto'; + document.documentElement.style.position='static'; document.body.style.position='static'; + } catch(e){} +})();` + +export function stripPaywall(res, url) { + const banner = `
+ RADICAL paywall bypass Β· try another method Β· reader view +
` + + const rewriter = new HTMLRewriter() + .on('head', { + element(el) { + el.prepend(``, { html: true }) + el.append(``, { html: true }) + }, + }) + // Drop scripts β€” the paywall overlay is injected by JS; static crawler HTML already has the text. + .on('script', { element(el) { el.remove() } }) + .on('link[as="script"]', { element(el) { el.remove() } }) + .on('link[rel="preload"][as="script"]', { element(el) { el.remove() } }) + .on('body', { + element(el) { + el.prepend(banner, { html: true }) + el.append(``, { html: true }) + }, + }) + + const out = rewriter.transform(res) + const headers = new Headers(out.headers) + headers.delete('content-security-policy') + headers.delete('content-security-policy-report-only') + headers.delete('x-frame-options') + headers.set('cache-control', 'public, max-age=300') + return new Response(out.body, { status: 200, headers }) +} + +// ---- Reader mode: extract the article into clean text ---- + +function meta(html, ...names) { + for (const n of names) { + const re = new RegExp(`]+(?:property|name)=["']${n}["'][^>]+content=["']([^"']+)["']`, 'i') + const m = html.match(re) || html.match(new RegExp(`]+content=["']([^"']+)["'][^>]+(?:property|name)=["']${n}["']`, 'i')) + if (m) return decodeEntities(m[1]) + } + return '' +} + +function decodeEntities(s) { + return (s || '') + .replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>') + .replace(/"/g, '"').replace(/'/g, "'").replace(/'/g, "'") + .replace(/ /g, ' ').replace(/’/g, '’').replace(/‘/g, 'β€˜') + .replace(/“/g, 'β€œ').replace(/”/g, '”').replace(/—/g, 'β€”') + .replace(/–/g, '–').replace(/…/g, '…').replace(/&#(\d+);/g, (_, d) => String.fromCodePoint(+d)) +} + +// Pick the densest content region, then keep only meaningful tags. +export function extractArticle(html, url) { + const title = meta(html, 'og:title', 'twitter:title') || (html.match(/]*>([^<]+)<\/title>/i)?.[1] || '').trim() + const description = meta(html, 'og:description', 'description', 'twitter:description') + const image = meta(html, 'og:image', 'twitter:image') + const author = meta(html, 'author', 'article:author') || (html.match(/<[^>]+rel=["']author["'][^>]*>([^<]+) { try { return new URL(url).host.replace(/^www\./, '') } catch { return '' } })() + + // Isolate a candidate container: prefer
, then JSON-LD articleBody, then
, then body. + let region = '' + const artMatch = html.match(/]*>([\s\S]*?)<\/article>/i) + if (artMatch && countParagraphs(artMatch[1]) >= 2) region = artMatch[1] + if (!region) { + const ld = extractJsonLdBody(html) + if (ld) region = ld + } + if (!region) { + const mainMatch = html.match(/]*>([\s\S]*?)<\/main>/i) + if (mainMatch) region = mainMatch[1] + } + if (!region) region = html.match(/]*>([\s\S]*)<\/body>/i)?.[1] || html + + const bodyHtml = cleanRegion(region, url) + const text = bodyHtml.replace(/<[^>]+>/g, ' ').replace(/\s+/g, ' ').trim() + return { title: decodeEntities(title), description, image, author: decodeEntities(author), published, site, bodyHtml, wordCount: text ? text.split(' ').length : 0 } +} + +function countParagraphs(s) { return (s.match(/]/gi) || []).length } + +function extractJsonLdBody(html) { + const blocks = [...html.matchAll(/]+type=["']application\/ld\+json["'][^>]*>([\s\S]*?)<\/script>/gi)] + for (const b of blocks) { + try { + const data = JSON.parse(b[1].trim()) + const arr = Array.isArray(data) ? data : (data['@graph'] || [data]) + for (const node of arr) { + if (node && node.articleBody && node.articleBody.length > 200) { + return node.articleBody.split(/\n{1,}/).map((p) => `

${p.trim()}

`).join('') + } + } + } catch {} + } + return '' +} + +function cleanRegion(region, url) { + let h = region + // Remove non-content blocks entirely + h = h.replace(/<(script|style|noscript|svg|form|iframe|nav|header|footer|aside|button|figure[^>]*aria-hidden)[\s\S]*?<\/\1>/gi, ' ') + h = h.replace(//g, ' ') + // Drop obvious junk containers by class/id + h = h.replace(/<(div|section|ul|span)[^>]*(?:class|id)=["'][^"']*(share|social|newsletter|promo|related|recommend|comment|advert|ad-|subscribe|paywall|nav|menu|breadcrumb|tags?|caption|byline-social)[^"']*["'][\s\S]*?<\/\1>/gi, ' ') + + // Collect meaningful blocks in document order + const keep = [] + const re = /<(h1|h2|h3|h4|p|blockquote|li|pre)[^>]*>([\s\S]*?)<\/\1>/gi + let m + while ((m = re.exec(h))) { + const tag = m[1].toLowerCase() + let inner = m[2].replace(/<(?!\/?(a|b|i|em|strong|code|br)\b)[^>]+>/gi, '') // keep light inline tags + inner = inner.replace(/\s+/g, ' ').trim() + const plain = inner.replace(/<[^>]+>/g, '').trim() + if (!plain) continue + if (tag === 'p' && plain.length < 2) continue + keep.push(tag === 'li' ? `
  • ${inner}
  • ` : `<${tag}>${inner}`) + } + let out = keep.join('\n') + // wrap consecutive
  • in a
      + out = out.replace(/(?:
    • [\s\S]*?<\/li>\n?)+/g, (block) => `
        ${block}
      `) + // absolutise anchor hrefs + out = out.replace(/href=["'](\/[^"']*)["']/gi, (_, p) => { try { return `href="${new URL(p, url).href}"` } catch { return `href="${p}"` } }) + return decodeEntities(out) +} diff --git a/src/views.js b/src/views.js new file mode 100644 index 0000000..7b6d142 --- /dev/null +++ b/src/views.js @@ -0,0 +1,260 @@ +import { METHODS, rankMethods } from './methods.js' + +const esc = (s) => (s || '').replace(/&/g, '&').replace(//g, '>').replace(/"/g, '"') + +const FONTS = 'https://fonts.googleapis.com/css2?family=Syne:wght@700;800&family=IBM+Plex+Mono:wght@400;500;700&display=swap' + +const CSS = ` +:root{ + --red:#FF2D55;--pink:#FF6B82;--bg:#0B0B09;--raised:#12100d;--card:#17140f; + --cream:#F0EDE6;--muted:#9c948a;--gold:#c9a84c;--line:#26221b; +} +*{box-sizing:border-box} +html,body{margin:0;padding:0} +body{background:var(--bg);color:var(--cream);font-family:system-ui,-apple-system,Inter,sans-serif;line-height:1.6;-webkit-font-smoothing:antialiased} +a{color:var(--pink);text-decoration:none} +a:hover{text-decoration:underline} +.wrap{max-width:960px;margin:0 auto;padding:0 20px} +.mono{font-family:'IBM Plex Mono',monospace} +header.top{border-bottom:1px solid var(--line);background:rgba(11,11,9,.85);backdrop-filter:blur(10px);position:sticky;top:0;z-index:50} +header.top .wrap{display:flex;align-items:center;justify-content:space-between;height:62px} +.brand{font-family:'Syne',sans-serif;font-weight:800;font-size:20px;letter-spacing:-.5px} +.brand b{color:var(--red)} +.brand small{color:var(--muted);font-weight:700;font-size:11px;letter-spacing:1px;margin-left:8px;font-family:'IBM Plex Mono',monospace} +.hero{text-align:center;padding:70px 0 30px} +.hero h1{font-family:'Syne',sans-serif;font-weight:800;font-size:clamp(34px,6vw,58px);line-height:1.02;margin:0 0 16px;letter-spacing:-1.5px} +.hero h1 span{color:var(--red)} +.hero p.sub{font-size:clamp(16px,2.4vw,20px);color:var(--muted);max-width:620px;margin:0 auto 34px} +form.search{display:flex;gap:10px;max-width:680px;margin:0 auto;flex-wrap:wrap} +form.search input{flex:1;min-width:240px;background:var(--raised);border:1px solid var(--line);color:var(--cream);font-size:16px;padding:16px 18px;border-radius:12px;font-family:'IBM Plex Mono',monospace} +form.search input:focus{outline:none;border-color:var(--red);box-shadow:0 0 0 3px rgba(255,45,85,.15)} +.btn{background:var(--red);color:#fff;border:none;font-weight:700;font-size:16px;padding:16px 26px;border-radius:12px;cursor:pointer;font-family:'Syne',sans-serif;transition:transform .1s,background .15s} +.btn:hover{background:#ff1744;transform:translateY(-1px);text-decoration:none} +.btn.ghost{background:transparent;border:1px solid var(--line);color:var(--cream)} +.btn.ghost:hover{border-color:var(--red);background:var(--raised)} +.grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(280px,1fr));gap:16px;margin:30px 0} +.card{background:var(--card);border:1px solid var(--line);border-radius:14px;padding:20px;display:flex;flex-direction:column;transition:border-color .15s,transform .1s} +.card:hover{border-color:var(--red);transform:translateY(-2px)} +.card .ic{font-size:26px;margin-bottom:10px} +.card h3{font-family:'Syne',sans-serif;margin:0 0 2px;font-size:19px;display:flex;align-items:center;gap:8px;flex-wrap:wrap} +.card .tag{font-size:11px;font-weight:700;color:var(--muted);text-transform:uppercase;letter-spacing:.5px} +.card p{color:var(--muted);font-size:14px;flex:1;margin:8px 0 16px} +.card a.go{align-self:flex-start;font-weight:700;font-family:'IBM Plex Mono',monospace;font-size:14px;background:var(--raised);border:1px solid var(--line);padding:9px 16px;border-radius:9px;color:var(--cream)} +.card a.go:hover{border-color:var(--red);text-decoration:none;color:var(--pink)} +.badge{display:inline-block;background:rgba(201,168,76,.15);color:var(--gold);font-size:10px;font-weight:700;letter-spacing:.5px;padding:3px 8px;border-radius:20px;text-transform:uppercase} +.badge.rec{background:rgba(255,45,85,.15);color:var(--pink)} +.section{padding:20px 0 10px} +.section h2{font-family:'Syne',sans-serif;font-size:24px;margin:0 0 4px} +.section p.lead{color:var(--muted);margin:0 0 6px} +.target{background:var(--raised);border:1px solid var(--line);border-radius:12px;padding:14px 18px;margin:18px 0;font-family:'IBM Plex Mono',monospace;font-size:14px;word-break:break-all} +.target b{color:var(--gold)} +.steps{display:grid;grid-template-columns:repeat(auto-fit,minmax(220px,1fr));gap:20px;margin:24px 0} +.step{background:var(--raised);border:1px solid var(--line);border-radius:14px;padding:22px} +.step .n{font-family:'Syne',sans-serif;font-weight:800;font-size:28px;color:var(--red)} +.step h4{margin:6px 0 6px;font-family:'Syne',sans-serif;font-size:17px} +.step p{color:var(--muted);font-size:14px;margin:0} +.bm{display:inline-block;background:var(--gold);color:#1a1408;font-weight:800;padding:10px 18px;border-radius:10px;font-family:'Syne',sans-serif;cursor:grab} +footer{border-top:1px solid var(--line);margin-top:60px;padding:30px 0;color:var(--muted);font-size:13px;text-align:center} +.legal{background:var(--raised);border:1px solid var(--line);border-left:3px solid var(--gold);border-radius:10px;padding:16px 20px;color:var(--muted);font-size:13px;margin:30px 0} +.reader{max-width:720px} +.reader h1.title{font-family:'Syne',sans-serif;font-size:clamp(28px,4vw,40px);line-height:1.1;letter-spacing:-1px;margin:26px 0 10px} +.reader .meta{color:var(--muted);font-size:14px;font-family:'IBM Plex Mono',monospace;border-bottom:1px solid var(--line);padding-bottom:16px;margin-bottom:8px} +.reader .lead{font-size:19px;color:#d8d2c8;margin:18px 0} +.reader .content{font-size:18px;line-height:1.75} +.reader .content p{margin:0 0 22px} +.reader .content h2,.reader .content h3{font-family:'Syne',sans-serif;margin:34px 0 12px} +.reader .content blockquote{border-left:3px solid var(--red);margin:24px 0;padding:4px 0 4px 20px;color:#d8d2c8;font-style:italic} +.reader .content a{color:var(--pink);border-bottom:1px solid rgba(255,107,130,.4)} +.reader .content img{max-width:100%;height:auto;border-radius:10px;margin:16px 0} +.reader .heroimg{width:100%;max-height:380px;object-fit:cover;border-radius:14px;margin:8px 0 4px} +.toolbar{display:flex;gap:10px;flex-wrap:wrap;margin:20px 0;padding:14px 0;border-top:1px solid var(--line);border-bottom:1px solid var(--line)} +.toolbar a{font-family:'IBM Plex Mono',monospace;font-size:13px;background:var(--raised);border:1px solid var(--line);padding:8px 14px;border-radius:9px;color:var(--cream)} +.toolbar a:hover{border-color:var(--red);text-decoration:none;color:var(--pink)} +.notice{background:var(--card);border:1px solid var(--line);border-radius:12px;padding:22px;margin:20px 0} +@media(max-width:640px){.hero{padding:40px 0 20px}} +` + +export function layout(title, body, { desc = '', wide = false } = {}) { + return ` + +${esc(title)} + + + + + + + + +
      +
      ${body}
      + +` +} + +function methodCard(m, url) { + const href = m.build(url) + const rec = m._recommended + return `
      +
      ${m.icon}
      +

      ${esc(m.name)} ${rec ? 'Recommended' : m.kind === 'internal' ? 'Built-in' : ''}

      + ${esc(m.tagline)} +

      ${esc(m.desc)}

      + ${m.kind === 'internal' ? 'Open β†’' : 'Try it β†—'} +
      ` +} + +export function homePage(host) { + const bm = bookmarklet(host) + const cards = METHODS.slice(0, 6).map((m) => methodCard(m, 'https://example.com/article')).join('') + return layout('PaywallFree β€” read paywalled articles free', ` +
      +

      Read any paywalled
      article. Free.

      +

      Paste a link. We fetch it the way Google's crawler does and give you a clean, readable copy β€” plus every archive and bypass trick in one place.

      + +

      or drag this to your bookmarks bar β†’ πŸ”“ PaywallFree

      +
      + +
      +

      Every method, one page

      +

      When you paste a link we rank the tricks most likely to work for that site. A preview of what's on offer:

      +
      ${cards}
      +
      + + + +
      + See how it works β†’ +
      + `, { desc: 'Paste any paywalled article link and read it free β€” reader mode, archive snapshots, and every bypass trick in one place.' }) +} + +export function readPage(url, host) { + const ranked = rankMethods(url) + // mark the top-ranked non-generic method as recommended + if (ranked[0]) ranked[0]._recommended = true + let display = url + try { const u = new URL(url); display = `${esc(u.host)}${esc(u.pathname)}` } catch {} + const cards = ranked.map((m) => methodCard(m, url)).join('') + return layout(`Unlock: ${safeHost(url)} β€” PaywallFree`, ` +
      +

      Pick your bypass

      +

      Methods ranked for this article. Start at the top β€” if one falls short, try the next.

      +
      πŸ”— ${display}
      +
      +
      ${cards}
      + +
      ← Try another link
      + `, { desc: `Bypass the paywall on ${safeHost(url)} β€” reader mode, archives and more.` }) +} + +export function readerPage(art, url, host) { + const body = ` +
      + + ${art.image ? `` : ''} +

      ${esc(art.title || 'Untitled')}

      +
      ${art.site ? esc(art.site) : safeHost(url)}${art.author ? ' Β· ' + esc(art.author) : ''}${art.published ? ' Β· ' + esc(art.published.slice(0, 10)) : ''}${art.wordCount ? ' Β· ' + art.wordCount + ' words' : ''}
      + ${art.description ? `

      ${esc(art.description)}

      ` : ''} +
      ${art.bodyHtml || ''}
      + +
      ` + return layout(`${art.title || 'Reader'} β€” PaywallFree`, body, { desc: art.description || `Read ${safeHost(url)} free.` }) +} + +export function errorPage(url, message) { + return layout('Reader unavailable β€” PaywallFree', ` +
      +

      That one needs a different trick

      +
      +

      Our reader couldn't pull clean text from this page${message ? ` (${esc(message)})` : ''}. That's normal for hard paywalls and JavaScript-only sites β€” an archive snapshot usually works instead.

      +
      πŸ”— ${esc(url)}
      +
      +
      +
      ${rankMethods(url).filter((m) => m.id !== 'reader').slice(0, 6).map((m) => methodCard(m, url)).join('')}
      +
      ← Try another link
      + `) +} + +export function howPage(host) { + const bm = bookmarklet(host) + return layout('How it works β€” PaywallFree', ` +
      +

      How it works

      +

      Three ways in. No account, no extension required.

      +
      +
      +
      1

      Paste the link

      Drop any article URL into the box on the home page and hit Unlock.

      +
      2

      We rank the tricks

      Based on the site, we sort the methods most likely to work β€” reader mode, archives, Freedium and more.

      +
      3

      Read it

      Open the top method. If a wall's still up, the next one down almost always clears it.

      +
      + +
      +

      The bookmarklet

      +

      On any paywalled page, click this to jump straight to PaywallFree for that URL. Drag it to your bookmarks bar:

      +

      πŸ”“ PaywallFree

      +

      Can't drag? Make a new bookmark and paste the code as the URL:
      ${esc(bm)}

      +
      + +
      +

      Or just edit the address bar

      +

      Put ${esc(host)}/ in front of any link:

      +
      ${esc(host)}/https://www.nytimes.com/2026/...
      +
      + +
      +

      What each method does

      +
      ${METHODS.map((m) => `
      ${m.icon}

      ${esc(m.name)}

      ${esc(m.tagline)}

      ${esc(m.desc)}

      `).join('')}
      +
      + + +
      Try it β†’
      + `, { desc: 'How PaywallFree unlocks paywalled articles: paste a link, pick a ranked bypass method, read free.' }) +} + +export function aboutPage() { + return layout('About β€” PaywallFree', ` +
      +

      About PaywallFree

      +

      A single page that collects every reliable way to read a paywalled article β€” a built-in reader that fetches pages the way search crawlers see them, plus a ranked menu of archives and bypass services.

      +
      +

      Why it exists

      +

      Information wants to be reachable. Metered walls, "you've read your 3 free articles" gates and reg-walls block a lot of the public web β€” including public-interest journalism people are trying to cite, share and read once. This puts the open copies in one place.

      +

      The ask

      +

      Back journalism you value. This is for the occasional wall, not for freeloading a paper you read daily. Support a free press worth having.

      +
      +

      Open source Β· a Radical Party project Β· source

      + ← Home +
      + `, { desc: 'About PaywallFree β€” a Radical Party project collecting every way to read paywalled articles free.' }) +} + +// ---- helpers ---- + +export function safeHost(url) { try { return new URL(url).host.replace(/^www\./, '') } catch { return 'this page' } } + +export function bookmarklet(host) { + const code = `(function(){var u=encodeURIComponent(location.href);location.href='https://${host}/read?url='+u;})();` + return 'javascript:' + code +} + +export { esc } diff --git a/wrangler.toml b/wrangler.toml new file mode 100644 index 0000000..c7378e1 --- /dev/null +++ b/wrangler.toml @@ -0,0 +1,9 @@ +name = "paywall" +main = "src/index.js" +compatibility_date = "2024-11-01" +compatibility_flags = ["nodejs_compat"] + +routes = [{ pattern = "paywall.theradicalparty.com", custom_domain = true }] + +[observability] +enabled = true