commit 3734365463453ac101902b75d8186afd558b11a7 Author: Michaël Date: Thu Feb 26 18:33:00 2026 -0300 first commit Made-with: Cursor diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..73c0f2c --- /dev/null +++ b/.gitignore @@ -0,0 +1,40 @@ +# Dependencies +node_modules/ + +# Build / output +dist/ +build/ +*.tsbuildinfo + +# Environment and secrets +.env +.env.local +.env.*.local + +# Logs +logs/ +*.log +npm-debug.log* + +# OS +.DS_Store +Thumbs.db + +# IDE / editor +.idea/ +.vscode/ +*.swp +*.swo + +# SQLite / local DB +*.db +*.sqlite +*.sqlite3 +backend/data/ + +# Vite +frontend/dist/ + +# Misc +*.local +.cache/ diff --git a/backend/.env.example b/backend/.env.example new file mode 100644 index 0000000..b150fe9 --- /dev/null +++ b/backend/.env.example @@ -0,0 +1,62 @@ +# Server +PORT=3001 +TRUST_PROXY=false +# Comma-separated origins for CORS (default allows 5173 and 5174) +# ALLOWED_ORIGINS=http://localhost:5173,http://localhost:5174 + +# Database: omit for SQLite (default); set for Postgres +# DATABASE_URL=postgresql://user:pass@localhost:5432/faucet +# SQLITE_PATH=./data/faucet.db + +# Security (required) +HMAC_IP_SECRET=your-secret-key-min-32-chars +JWT_SECRET=your-jwt-secret-min-32-chars +JWT_EXPIRES_IN_SECONDS=604800 +NIP98_MAX_SKEW_SECONDS=300 +NONCE_TTL_SECONDS=600 + +# Faucet economics +FAUCET_ENABLED=true +EMERGENCY_STOP=false +FAUCET_MIN_SATS=10 +FAUCET_MAX_SATS=100 +PAYOUT_WEIGHT_SMALL=50 +PAYOUT_WEIGHT_MEDIUM=30 +PAYOUT_WEIGHT_LARGE=15 +PAYOUT_WEIGHT_JACKPOT=5 +PAYOUT_SMALL_SATS=10 +PAYOUT_MEDIUM_SATS=25 +PAYOUT_LARGE_SATS=50 +PAYOUT_JACKPOT_SATS=100 +DAILY_BUDGET_SATS=10000 +MAX_CLAIMS_PER_DAY=100 +MIN_WALLET_BALANCE_SATS=1000 + +# Eligibility +MIN_ACCOUNT_AGE_DAYS=14 +MIN_ACTIVITY_SCORE=30 +MIN_NOTES_COUNT=5 +MIN_FOLLOWING_COUNT=10 +MIN_FOLLOWERS_COUNT=0 +ACTIVITY_LOOKBACK_DAYS=90 + +# Cooldowns +COOLDOWN_DAYS=7 +IP_COOLDOWN_DAYS=7 +MAX_CLAIMS_PER_IP_PER_PERIOD=1 + +# Nostr +NOSTR_RELAYS=wss://relay.damus.io,wss://relay.nostr.band +RELAY_TIMEOUT_MS=5000 +MAX_EVENTS_FETCH=500 +METADATA_CACHE_HOURS=24 + +# LNbits +LNBITS_BASE_URL=https://azzamo.online +LNBITS_ADMIN_KEY=your-admin-key +LNBITS_WALLET_ID=your-wallet-id +DEPOSIT_LIGHTNING_ADDRESS=faucet@yourdomain.com +DEPOSIT_LNURLP=https://yourdomain.com/.well-known/lnurlp/faucet + +# Cashu redeem (optional; default: https://cashu-redeem.azzamo.net) +# CASHU_REDEEM_API_URL=https://cashu-redeem.azzamo.net diff --git a/backend/package-lock.json b/backend/package-lock.json new file mode 100644 index 0000000..b72e3b8 --- /dev/null +++ b/backend/package-lock.json @@ -0,0 +1,2303 @@ +{ + "name": "lnfaucet-backend", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "lnfaucet-backend", + "version": "1.0.0", + "dependencies": { + "better-sqlite3": "^11.6.0", + "cors": "^2.8.5", + "dotenv": "^16.4.5", + "express": "^4.21.1", + "express-rate-limit": "^7.4.1", + "nostr-tools": "^2.4.4", + "pg": "^8.13.1", + "uuid": "^10.0.0" + }, + "devDependencies": { + "@types/better-sqlite3": "^7.6.11", + "@types/cors": "^2.8.17", + "@types/express": "^4.17.21", + "@types/node": "^22.9.0", + "@types/pg": "^8.11.10", + "@types/uuid": "^10.0.0", + "tsx": "^4.19.2", + "typescript": "^5.6.3" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.3.tgz", + "integrity": "sha512-9fJMTNFTWZMh5qwrBItuziu834eOCUcEqymSH7pY+zoMVEZg3gcPuBNxH1EvfVYe9h0x/Ptw8KBzv7qxb7l8dg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.3.tgz", + "integrity": "sha512-i5D1hPY7GIQmXlXhs2w8AWHhenb00+GxjxRncS2ZM7YNVGNfaMxgzSGuO8o8SJzRc/oZwU2bcScvVERk03QhzA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.3.tgz", + "integrity": "sha512-YdghPYUmj/FX2SYKJ0OZxf+iaKgMsKHVPF1MAq/P8WirnSpCStzKJFjOjzsW0QQ7oIAiccHdcqjbHmJxRb/dmg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.3.tgz", + "integrity": "sha512-IN/0BNTkHtk8lkOM8JWAYFg4ORxBkZQf9zXiEOfERX/CzxW3Vg1ewAhU7QSWQpVIzTW+b8Xy+lGzdYXV6UZObQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.3.tgz", + "integrity": "sha512-Re491k7ByTVRy0t3EKWajdLIr0gz2kKKfzafkth4Q8A5n1xTHrkqZgLLjFEHVD+AXdUGgQMq+Godfq45mGpCKg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.3.tgz", + "integrity": "sha512-vHk/hA7/1AckjGzRqi6wbo+jaShzRowYip6rt6q7VYEDX4LEy1pZfDpdxCBnGtl+A5zq8iXDcyuxwtv3hNtHFg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.3.tgz", + "integrity": "sha512-ipTYM2fjt3kQAYOvo6vcxJx3nBYAzPjgTCk7QEgZG8AUO3ydUhvelmhrbOheMnGOlaSFUoHXB6un+A7q4ygY9w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.3.tgz", + "integrity": "sha512-dDk0X87T7mI6U3K9VjWtHOXqwAMJBNN2r7bejDsc+j03SEjtD9HrOl8gVFByeM0aJksoUuUVU9TBaZa2rgj0oA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.3.tgz", + "integrity": "sha512-s6nPv2QkSupJwLYyfS+gwdirm0ukyTFNl3KTgZEAiJDd+iHZcbTPPcWCcRYH+WlNbwChgH2QkE9NSlNrMT8Gfw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.3.tgz", + "integrity": "sha512-sZOuFz/xWnZ4KH3YfFrKCf1WyPZHakVzTiqji3WDc0BCl2kBwiJLCXpzLzUBLgmp4veFZdvN5ChW4Eq/8Fc2Fg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.3.tgz", + "integrity": "sha512-yGlQYjdxtLdh0a3jHjuwOrxQjOZYD/C9PfdbgJJF3TIZWnm/tMd/RcNiLngiu4iwcBAOezdnSLAwQDPqTmtTYg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.3.tgz", + "integrity": "sha512-WO60Sn8ly3gtzhyjATDgieJNet/KqsDlX5nRC5Y3oTFcS1l0KWba+SEa9Ja1GfDqSF1z6hif/SkpQJbL63cgOA==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.3.tgz", + "integrity": "sha512-APsymYA6sGcZ4pD6k+UxbDjOFSvPWyZhjaiPyl/f79xKxwTnrn5QUnXR5prvetuaSMsb4jgeHewIDCIWljrSxw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.3.tgz", + "integrity": "sha512-eizBnTeBefojtDb9nSh4vvVQ3V9Qf9Df01PfawPcRzJH4gFSgrObw+LveUyDoKU3kxi5+9RJTCWlj4FjYXVPEA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.3.tgz", + "integrity": "sha512-3Emwh0r5wmfm3ssTWRQSyVhbOHvqegUDRd0WhmXKX2mkHJe1SFCMJhagUleMq+Uci34wLSipf8Lagt4LlpRFWQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.3.tgz", + "integrity": "sha512-pBHUx9LzXWBc7MFIEEL0yD/ZVtNgLytvx60gES28GcWMqil8ElCYR4kvbV2BDqsHOvVDRrOxGySBM9Fcv744hw==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.3.tgz", + "integrity": "sha512-Czi8yzXUWIQYAtL/2y6vogER8pvcsOsk5cpwL4Gk5nJqH5UZiVByIY8Eorm5R13gq+DQKYg0+JyQoytLQas4dA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.3.tgz", + "integrity": "sha512-sDpk0RgmTCR/5HguIZa9n9u+HVKf40fbEUt+iTzSnCaGvY9kFP0YKBWZtJaraonFnqef5SlJ8/TiPAxzyS+UoA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.3.tgz", + "integrity": "sha512-P14lFKJl/DdaE00LItAukUdZO5iqNH7+PjoBm+fLQjtxfcfFE20Xf5CrLsmZdq5LFFZzb5JMZ9grUwvtVYzjiA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.3.tgz", + "integrity": "sha512-AIcMP77AvirGbRl/UZFTq5hjXK+2wC7qFRGoHSDrZ5v5b8DK/GYpXW3CPRL53NkvDqb9D+alBiC/dV0Fb7eJcw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.3.tgz", + "integrity": "sha512-DnW2sRrBzA+YnE70LKqnM3P+z8vehfJWHXECbwBmH/CU51z6FiqTQTHFenPlHmo3a8UgpLyH3PT+87OViOh1AQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.3.tgz", + "integrity": "sha512-NinAEgr/etERPTsZJ7aEZQvvg/A6IsZG/LgZy+81wON2huV7SrK3e63dU0XhyZP4RKGyTm7aOgmQk0bGp0fy2g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.3.tgz", + "integrity": "sha512-PanZ+nEz+eWoBJ8/f8HKxTTD172SKwdXebZ0ndd953gt1HRBbhMsaNqjTyYLGLPdoWHy4zLU7bDVJztF5f3BHA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.3.tgz", + "integrity": "sha512-B2t59lWWYrbRDw/tjiWOuzSsFh1Y/E95ofKz7rIVYSQkUYBjfSgf6oeYPNWHToFRr2zx52JKApIcAS/D5TUBnA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.3.tgz", + "integrity": "sha512-QLKSFeXNS8+tHW7tZpMtjlNb7HKau0QDpwm49u0vUp9y1WOF+PEzkU84y9GqYaAVW8aH8f3GcBck26jh54cX4Q==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.3.tgz", + "integrity": "sha512-4uJGhsxuptu3OcpVAzli+/gWusVGwZZHTlS63hh++ehExkVT8SgiEf7/uC/PclrPPkLhZqGgCTjd0VWLo6xMqA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@noble/ciphers": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@noble/ciphers/-/ciphers-2.1.1.tgz", + "integrity": "sha512-bysYuiVfhxNJuldNXlFEitTVdNnYUc+XNJZd7Qm2a5j1vZHgY+fazadNFWFaMK/2vye0JVlxV3gHmC0WDfAOQw==", + "license": "MIT", + "engines": { + "node": ">= 20.19.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@noble/curves": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-2.0.1.tgz", + "integrity": "sha512-vs1Az2OOTBiP4q0pwjW5aF0xp9n4MxVrmkFBxc6EKZc6ddYx5gaZiAsZoq0uRRXWbi3AT/sBqn05eRPtn1JCPw==", + "license": "MIT", + "dependencies": { + "@noble/hashes": "2.0.1" + }, + "engines": { + "node": ">= 20.19.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@noble/hashes": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-2.0.1.tgz", + "integrity": "sha512-XlOlEbQcE9fmuXxrVTXCTlG2nlRXa9Rj3rr5Ue/+tX+nmkgbX720YHh0VR3hBF9xDvwnb8D2shVGOwNx+ulArw==", + "license": "MIT", + "engines": { + "node": ">= 20.19.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@scure/base": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@scure/base/-/base-2.0.0.tgz", + "integrity": "sha512-3E1kpuZginKkek01ovG8krQ0Z44E3DHPjc5S2rjJw9lZn3KSQOs8S7wqikF/AH7iRanHypj85uGyxk0XAyC37w==", + "license": "MIT", + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@scure/bip32": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@scure/bip32/-/bip32-2.0.1.tgz", + "integrity": "sha512-4Md1NI5BzoVP+bhyJaY3K6yMesEFzNS1sE/cP+9nuvE7p/b0kx9XbpDHHFl8dHtufcbdHRUUQdRqLIPHN/s7yA==", + "license": "MIT", + "dependencies": { + "@noble/curves": "2.0.1", + "@noble/hashes": "2.0.1", + "@scure/base": "2.0.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@scure/bip39": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@scure/bip39/-/bip39-2.0.1.tgz", + "integrity": "sha512-PsxdFj/d2AcJcZDX1FXN3dDgitDDTmwf78rKZq1a6c1P1Nan1X/Sxc7667zU3U+AN60g7SxxP0YCVw2H/hBycg==", + "license": "MIT", + "dependencies": { + "@noble/hashes": "2.0.1", + "@scure/base": "2.0.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@types/better-sqlite3": { + "version": "7.6.13", + "resolved": "https://registry.npmjs.org/@types/better-sqlite3/-/better-sqlite3-7.6.13.tgz", + "integrity": "sha512-NMv9ASNARoKksWtsq/SHakpYAYnhBrQgGD8zkLYk/jaK8jUGn08CfEdTRgYhMypUQAfzSP8W6gNLe0q19/t4VA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/body-parser": { + "version": "1.19.6", + "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.6.tgz", + "integrity": "sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/connect": "*", + "@types/node": "*" + } + }, + "node_modules/@types/connect": { + "version": "3.4.38", + "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", + "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/cors": { + "version": "2.8.19", + "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.19.tgz", + "integrity": "sha512-mFNylyeyqN93lfe/9CSxOGREz8cpzAhH+E93xJ4xWQf62V8sQ/24reV2nyzUWM6H6Xji+GGHpkbLe7pVoUEskg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/express": { + "version": "4.17.25", + "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.25.tgz", + "integrity": "sha512-dVd04UKsfpINUnK0yBoYHDF3xu7xVH4BuDotC/xGuycx4CgbP48X/KF/586bcObxT0HENHXEU8Nqtu6NR+eKhw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/body-parser": "*", + "@types/express-serve-static-core": "^4.17.33", + "@types/qs": "*", + "@types/serve-static": "^1" + } + }, + "node_modules/@types/express-serve-static-core": { + "version": "4.19.8", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.19.8.tgz", + "integrity": "sha512-02S5fmqeoKzVZCHPZid4b8JH2eM5HzQLZWN2FohQEy/0eXTq8VXZfSN6Pcr3F6N9R/vNrj7cpgbhjie6m/1tCA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "@types/qs": "*", + "@types/range-parser": "*", + "@types/send": "*" + } + }, + "node_modules/@types/http-errors": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.5.tgz", + "integrity": "sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/mime": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz", + "integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "22.19.11", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.11.tgz", + "integrity": "sha512-BH7YwL6rA93ReqeQS1c4bsPpcfOmJasG+Fkr6Y59q83f9M1WcBRHR2vM+P9eOisYRcN3ujQoiZY8uk5W+1WL8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/@types/pg": { + "version": "8.16.0", + "resolved": "https://registry.npmjs.org/@types/pg/-/pg-8.16.0.tgz", + "integrity": "sha512-RmhMd/wD+CF8Dfo+cVIy3RR5cl8CyfXQ0tGgW6XBL8L4LM/UTEbNXYRbLwU6w+CgrKBNbrQWt4FUtTfaU5jSYQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "pg-protocol": "*", + "pg-types": "^2.2.0" + } + }, + "node_modules/@types/qs": { + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.14.0.tgz", + "integrity": "sha512-eOunJqu0K1923aExK6y8p6fsihYEn/BYuQ4g0CxAAgFc4b/ZLN4CrsRZ55srTdqoiLzU2B2evC+apEIxprEzkQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/range-parser": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz", + "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/send": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@types/send/-/send-1.2.1.tgz", + "integrity": "sha512-arsCikDvlU99zl1g69TcAB3mzZPpxgw0UQnaHeC1Nwb015xp8bknZv5rIfri9xTOcMuaVgvabfIRA7PSZVuZIQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/serve-static": { + "version": "1.15.10", + "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.10.tgz", + "integrity": "sha512-tRs1dB+g8Itk72rlSI2ZrW6vZg0YrLI81iQSTkMmOqnqCaNr/8Ek4VwWcN5vZgCYWbg/JJSGBlUaYGAOP73qBw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/http-errors": "*", + "@types/node": "*", + "@types/send": "<1" + } + }, + "node_modules/@types/serve-static/node_modules/@types/send": { + "version": "0.17.6", + "resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.6.tgz", + "integrity": "sha512-Uqt8rPBE8SY0RK8JB1EzVOIZ32uqy8HwdxCnoCOsYrvnswqmFZ/k+9Ikidlk/ImhsdvBsloHbAlewb2IEBV/Og==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/mime": "^1", + "@types/node": "*" + } + }, + "node_modules/@types/uuid": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-10.0.0.tgz", + "integrity": "sha512-7gqG38EyHgyP1S+7+xomFtL+ZNHcKv6DwNaCZmJmo1vgMugyF3TCnXVg4t1uk89mLNwnLtnY3TpOpCOyp1/xHQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "license": "MIT", + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", + "license": "MIT" + }, + "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/better-sqlite3": { + "version": "11.10.0", + "resolved": "https://registry.npmjs.org/better-sqlite3/-/better-sqlite3-11.10.0.tgz", + "integrity": "sha512-EwhOpyXiOEL/lKzHz9AW1msWFNzGc/z+LzeB3/jnFJpxu+th2yqvzsSWas1v9jgs9+xiXJcD5A8CJxAG2TaghQ==", + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "bindings": "^1.5.0", + "prebuild-install": "^7.1.1" + } + }, + "node_modules/bindings": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz", + "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==", + "license": "MIT", + "dependencies": { + "file-uri-to-path": "1.0.0" + } + }, + "node_modules/bl": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", + "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", + "license": "MIT", + "dependencies": { + "buffer": "^5.5.0", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + } + }, + "node_modules/body-parser": { + "version": "1.20.4", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.4.tgz", + "integrity": "sha512-ZTgYYLMOXY9qKU/57FAo8F+HA2dGX7bqGc71txDRC1rS4frdFI5R7NhluHxH6M0YItAP0sHB4uqAOcYKxO6uGA==", + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "content-type": "~1.0.5", + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "~1.2.0", + "http-errors": "~2.0.1", + "iconv-lite": "~0.4.24", + "on-finished": "~2.4.1", + "qs": "~6.14.0", + "raw-body": "~2.5.3", + "type-is": "~1.6.18", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "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.1.13" + } + }, + "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/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/chownr": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", + "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==", + "license": "ISC" + }, + "node_modules/content-disposition": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", + "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", + "license": "MIT", + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.7.tgz", + "integrity": "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==", + "license": "MIT" + }, + "node_modules/cors": { + "version": "2.8.6", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.6.tgz", + "integrity": "sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==", + "license": "MIT", + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "node": ">= 0.10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/decompress-response": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", + "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", + "license": "MIT", + "dependencies": { + "mimic-response": "^3.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/deep-extend": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", + "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", + "license": "MIT", + "engines": { + "node": ">=4.0.0" + } + }, + "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/destroy": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", + "license": "MIT", + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "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==", + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/dotenv": { + "version": "16.6.1", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz", + "integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "license": "MIT" + }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/end-of-stream": { + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", + "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", + "license": "MIT", + "dependencies": { + "once": "^1.4.0" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/esbuild": { + "version": "0.27.3", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.3.tgz", + "integrity": "sha512-8VwMnyGCONIs6cWue2IdpHxHnAjzxnw2Zr7MkVxB2vjmQ2ivqGFb4LEG3SMnv0Gb2F/G/2yA8zUaiL1gywDCCg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.27.3", + "@esbuild/android-arm": "0.27.3", + "@esbuild/android-arm64": "0.27.3", + "@esbuild/android-x64": "0.27.3", + "@esbuild/darwin-arm64": "0.27.3", + "@esbuild/darwin-x64": "0.27.3", + "@esbuild/freebsd-arm64": "0.27.3", + "@esbuild/freebsd-x64": "0.27.3", + "@esbuild/linux-arm": "0.27.3", + "@esbuild/linux-arm64": "0.27.3", + "@esbuild/linux-ia32": "0.27.3", + "@esbuild/linux-loong64": "0.27.3", + "@esbuild/linux-mips64el": "0.27.3", + "@esbuild/linux-ppc64": "0.27.3", + "@esbuild/linux-riscv64": "0.27.3", + "@esbuild/linux-s390x": "0.27.3", + "@esbuild/linux-x64": "0.27.3", + "@esbuild/netbsd-arm64": "0.27.3", + "@esbuild/netbsd-x64": "0.27.3", + "@esbuild/openbsd-arm64": "0.27.3", + "@esbuild/openbsd-x64": "0.27.3", + "@esbuild/openharmony-arm64": "0.27.3", + "@esbuild/sunos-x64": "0.27.3", + "@esbuild/win32-arm64": "0.27.3", + "@esbuild/win32-ia32": "0.27.3", + "@esbuild/win32-x64": "0.27.3" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "license": "MIT" + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/expand-template": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz", + "integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==", + "license": "(MIT OR WTFPL)", + "engines": { + "node": ">=6" + } + }, + "node_modules/express": { + "version": "4.22.1", + "resolved": "https://registry.npmjs.org/express/-/express-4.22.1.tgz", + "integrity": "sha512-F2X8g9P1X7uCPZMA3MVf9wcTqlyNp7IhH5qPCI0izhaOIYXaW9L535tGA3qmjRzpH+bZczqq7hVKxTR4NWnu+g==", + "license": "MIT", + "dependencies": { + "accepts": "~1.3.8", + "array-flatten": "1.1.1", + "body-parser": "~1.20.3", + "content-disposition": "~0.5.4", + "content-type": "~1.0.4", + "cookie": "~0.7.1", + "cookie-signature": "~1.0.6", + "debug": "2.6.9", + "depd": "2.0.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "~1.3.1", + "fresh": "~0.5.2", + "http-errors": "~2.0.0", + "merge-descriptors": "1.0.3", + "methods": "~1.1.2", + "on-finished": "~2.4.1", + "parseurl": "~1.3.3", + "path-to-regexp": "~0.1.12", + "proxy-addr": "~2.0.7", + "qs": "~6.14.0", + "range-parser": "~1.2.1", + "safe-buffer": "5.2.1", + "send": "~0.19.0", + "serve-static": "~1.16.2", + "setprototypeof": "1.2.0", + "statuses": "~2.0.1", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/express-rate-limit": { + "version": "7.5.1", + "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-7.5.1.tgz", + "integrity": "sha512-7iN8iPMDzOMHPUYllBEsQdWVB6fPDMPqwjBaFrgr4Jgr/+okjvzAy+UHlYYL/Vs0OsOrMkwS6PJDkFlJwoxUnw==", + "license": "MIT", + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://github.com/sponsors/express-rate-limit" + }, + "peerDependencies": { + "express": ">= 4.11" + } + }, + "node_modules/file-uri-to-path": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", + "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==", + "license": "MIT" + }, + "node_modules/finalhandler": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.2.tgz", + "integrity": "sha512-aA4RyPcd3badbdABGDuTXCMTtOneUCAYH/gxoYRTZlIJdF0YPWuGqiAsIrhNnnqdXGswYk6dGujem4w80UJFhg==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "on-finished": "~2.4.1", + "parseurl": "~1.3.3", + "statuses": "~2.0.2", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fs-constants": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", + "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", + "license": "MIT" + }, + "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/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/get-tsconfig": { + "version": "4.13.6", + "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.13.6.tgz", + "integrity": "sha512-shZT/QMiSHc/YBLxxOkMtgSid5HFoauqCE3/exfsEcwg1WkeqjG+V40yBbBrsD+jW2HDXcs28xOfcbm2jI8Ddw==", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve-pkg-maps": "^1.0.0" + }, + "funding": { + "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" + } + }, + "node_modules/github-from-package": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz", + "integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==", + "license": "MIT" + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/http-errors": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", + "license": "MIT", + "dependencies": { + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" + }, + "engines": { + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "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/ini": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", + "license": "ISC" + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/merge-descriptors": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", + "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mimic-response": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", + "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/mkdirp-classic": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", + "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==", + "license": "MIT" + }, + "node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/napi-build-utils": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-2.0.0.tgz", + "integrity": "sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==", + "license": "MIT" + }, + "node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/node-abi": { + "version": "3.87.0", + "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.87.0.tgz", + "integrity": "sha512-+CGM1L1CgmtheLcBuleyYOn7NWPVu0s0EJH2C4puxgEZb9h8QpR9G2dBfZJOAUhi7VQxuBPMd0hiISWcTyiYyQ==", + "license": "MIT", + "dependencies": { + "semver": "^7.3.5" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/nostr-tools": { + "version": "2.23.1", + "resolved": "https://registry.npmjs.org/nostr-tools/-/nostr-tools-2.23.1.tgz", + "integrity": "sha512-Q5SJ1omrseBFXtLwqDhufpFLA6vX3rS/IuBCc974qaYX6YKGwEPxa/ZsyxruUOr+b+5EpWL2hFmCB5AueYrfBw==", + "license": "Unlicense", + "dependencies": { + "@noble/ciphers": "2.1.1", + "@noble/curves": "2.0.1", + "@noble/hashes": "2.0.1", + "@scure/base": "2.0.0", + "@scure/bip32": "2.0.1", + "@scure/bip39": "2.0.1", + "nostr-wasm": "0.1.0" + }, + "peerDependencies": { + "typescript": ">=5.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/nostr-wasm": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/nostr-wasm/-/nostr-wasm-0.1.0.tgz", + "integrity": "sha512-78BTryCLcLYv96ONU8Ws3Q1JzjlAt+43pWQhIl86xZmWeegYCNLPml7yQ+gG3vR6V5h4XGj+TxO+SS5dsThQIA==", + "license": "MIT" + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-to-regexp": { + "version": "0.1.12", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz", + "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==", + "license": "MIT" + }, + "node_modules/pg": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/pg/-/pg-8.18.0.tgz", + "integrity": "sha512-xqrUDL1b9MbkydY/s+VZ6v+xiMUmOUk7SS9d/1kpyQxoJ6U9AO1oIJyUWVZojbfe5Cc/oluutcgFG4L9RDP1iQ==", + "license": "MIT", + "dependencies": { + "pg-connection-string": "^2.11.0", + "pg-pool": "^3.11.0", + "pg-protocol": "^1.11.0", + "pg-types": "2.2.0", + "pgpass": "1.0.5" + }, + "engines": { + "node": ">= 16.0.0" + }, + "optionalDependencies": { + "pg-cloudflare": "^1.3.0" + }, + "peerDependencies": { + "pg-native": ">=3.0.1" + }, + "peerDependenciesMeta": { + "pg-native": { + "optional": true + } + } + }, + "node_modules/pg-cloudflare": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/pg-cloudflare/-/pg-cloudflare-1.3.0.tgz", + "integrity": "sha512-6lswVVSztmHiRtD6I8hw4qP/nDm1EJbKMRhf3HCYaqud7frGysPv7FYJ5noZQdhQtN2xJnimfMtvQq21pdbzyQ==", + "license": "MIT", + "optional": true + }, + "node_modules/pg-connection-string": { + "version": "2.11.0", + "resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.11.0.tgz", + "integrity": "sha512-kecgoJwhOpxYU21rZjULrmrBJ698U2RxXofKVzOn5UDj61BPj/qMb7diYUR1nLScCDbrztQFl1TaQZT0t1EtzQ==", + "license": "MIT" + }, + "node_modules/pg-int8": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/pg-int8/-/pg-int8-1.0.1.tgz", + "integrity": "sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==", + "license": "ISC", + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/pg-pool": { + "version": "3.11.0", + "resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.11.0.tgz", + "integrity": "sha512-MJYfvHwtGp870aeusDh+hg9apvOe2zmpZJpyt+BMtzUWlVqbhFmMK6bOBXLBUPd7iRtIF9fZplDc7KrPN3PN7w==", + "license": "MIT", + "peerDependencies": { + "pg": ">=8.0" + } + }, + "node_modules/pg-protocol": { + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.11.0.tgz", + "integrity": "sha512-pfsxk2M9M3BuGgDOfuy37VNRRX3jmKgMjcvAcWqNDpZSf4cUmv8HSOl5ViRQFsfARFn0KuUQTgLxVMbNq5NW3g==", + "license": "MIT" + }, + "node_modules/pg-types": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/pg-types/-/pg-types-2.2.0.tgz", + "integrity": "sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==", + "license": "MIT", + "dependencies": { + "pg-int8": "1.0.1", + "postgres-array": "~2.0.0", + "postgres-bytea": "~1.0.0", + "postgres-date": "~1.0.4", + "postgres-interval": "^1.1.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/pgpass": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/pgpass/-/pgpass-1.0.5.tgz", + "integrity": "sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug==", + "license": "MIT", + "dependencies": { + "split2": "^4.1.0" + } + }, + "node_modules/postgres-array": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-2.0.0.tgz", + "integrity": "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/postgres-bytea": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/postgres-bytea/-/postgres-bytea-1.0.1.tgz", + "integrity": "sha512-5+5HqXnsZPE65IJZSMkZtURARZelel2oXUEO8rH83VS/hxH5vv1uHquPg5wZs8yMAfdv971IU+kcPUczi7NVBQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/postgres-date": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/postgres-date/-/postgres-date-1.0.7.tgz", + "integrity": "sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/postgres-interval": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/postgres-interval/-/postgres-interval-1.2.0.tgz", + "integrity": "sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==", + "license": "MIT", + "dependencies": { + "xtend": "^4.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/prebuild-install": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.3.tgz", + "integrity": "sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==", + "deprecated": "No longer maintained. Please contact the author of the relevant native addon; alternatives are available.", + "license": "MIT", + "dependencies": { + "detect-libc": "^2.0.0", + "expand-template": "^2.0.3", + "github-from-package": "0.0.0", + "minimist": "^1.2.3", + "mkdirp-classic": "^0.5.3", + "napi-build-utils": "^2.0.0", + "node-abi": "^3.3.0", + "pump": "^3.0.0", + "rc": "^1.2.7", + "simple-get": "^4.0.0", + "tar-fs": "^2.0.0", + "tunnel-agent": "^0.6.0" + }, + "bin": { + "prebuild-install": "bin.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "license": "MIT", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/pump": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.3.tgz", + "integrity": "sha512-todwxLMY7/heScKmntwQG8CXVkWUOdYxIvY2s0VWAAMh/nd8SoYiRaKjlr7+iCs984f2P8zvrfWcDDYVb73NfA==", + "license": "MIT", + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "node_modules/qs": { + "version": "6.14.2", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.2.tgz", + "integrity": "sha512-V/yCWTTF7VJ9hIh18Ugr2zhJMP01MY7c5kh4J870L7imm6/DIzBsNLTXzMwUA3yZ5b/KBqLx8Kp3uRvd7xSe3Q==", + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "2.5.3", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.3.tgz", + "integrity": "sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA==", + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "http-errors": "~2.0.1", + "iconv-lite": "~0.4.24", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/rc": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", + "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", + "license": "(BSD-2-Clause OR MIT OR Apache-2.0)", + "dependencies": { + "deep-extend": "^0.6.0", + "ini": "~1.3.0", + "minimist": "^1.2.0", + "strip-json-comments": "~2.0.1" + }, + "bin": { + "rc": "cli.js" + } + }, + "node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/resolve-pkg-maps": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", + "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "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/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/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/send": { + "version": "0.19.2", + "resolved": "https://registry.npmjs.org/send/-/send-0.19.2.tgz", + "integrity": "sha512-VMbMxbDeehAxpOtWJXlcUS5E8iXh6QmN+BkRX1GARS3wRaXEEgzCcB10gTQazO42tpNIya8xIyNx8fll1OFPrg==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "~0.5.2", + "http-errors": "~2.0.1", + "mime": "1.6.0", + "ms": "2.1.3", + "on-finished": "~2.4.1", + "range-parser": "~1.2.1", + "statuses": "~2.0.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/send/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/serve-static": { + "version": "1.16.3", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.3.tgz", + "integrity": "sha512-x0RTqQel6g5SY7Lg6ZreMmsOzncHFU7nhnRWkKgWuMTu5NN0DR5oruckMqRvacAN9d5w6ARnRBXl9xhDCgfMeA==", + "license": "MIT", + "dependencies": { + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "~0.19.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "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/side-channel": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/simple-concat": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz", + "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==", + "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/simple-get": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-4.0.1.tgz", + "integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==", + "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": { + "decompress-response": "^6.0.0", + "once": "^1.3.1", + "simple-concat": "^1.0.0" + } + }, + "node_modules/split2": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", + "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==", + "license": "ISC", + "engines": { + "node": ">= 10.x" + } + }, + "node_modules/statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/strip-json-comments": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", + "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/tar-fs": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.4.tgz", + "integrity": "sha512-mDAjwmZdh7LTT6pNleZ05Yt65HC3E+NiQzl672vQG38jIrehtJk/J3mNwIg+vShQPcLF/LV7CMnDW6vjj6sfYQ==", + "license": "MIT", + "dependencies": { + "chownr": "^1.1.1", + "mkdirp-classic": "^0.5.2", + "pump": "^3.0.0", + "tar-stream": "^2.1.4" + } + }, + "node_modules/tar-stream": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", + "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", + "license": "MIT", + "dependencies": { + "bl": "^4.0.3", + "end-of-stream": "^1.4.1", + "fs-constants": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1" + }, + "engines": { + "node": ">=6" + } + }, + "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/tsx": { + "version": "4.21.0", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.21.0.tgz", + "integrity": "sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "~0.27.0", + "get-tsconfig": "^4.7.5" + }, + "bin": { + "tsx": "dist/cli.mjs" + }, + "engines": { + "node": ">=18.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + } + }, + "node_modules/tunnel-agent": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", + "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==", + "license": "Apache-2.0", + "dependencies": { + "safe-buffer": "^5.0.1" + }, + "engines": { + "node": "*" + } + }, + "node_modules/type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "license": "MIT", + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "devOptional": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "dev": true, + "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/utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", + "license": "MIT", + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/uuid": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-10.0.0.tgz", + "integrity": "sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "license": "ISC" + }, + "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" + } + } + } +} diff --git a/backend/package.json b/backend/package.json new file mode 100644 index 0000000..07c481b --- /dev/null +++ b/backend/package.json @@ -0,0 +1,32 @@ +{ + "name": "lnfaucet-backend", + "version": "1.0.0", + "private": true, + "type": "module", + "scripts": { + "build": "tsc", + "start": "node dist/index.js", + "dev": "tsx watch src/index.ts", + "migrate": "tsx src/db/migrate.ts" + }, + "dependencies": { + "better-sqlite3": "^11.6.0", + "cors": "^2.8.5", + "dotenv": "^16.4.5", + "express": "^4.21.1", + "express-rate-limit": "^7.4.1", + "nostr-tools": "^2.4.4", + "pg": "^8.13.1", + "uuid": "^10.0.0" + }, + "devDependencies": { + "@types/better-sqlite3": "^7.6.11", + "@types/cors": "^2.8.17", + "@types/express": "^4.17.21", + "@types/node": "^22.9.0", + "@types/pg": "^8.11.10", + "@types/uuid": "^10.0.0", + "tsx": "^4.19.2", + "typescript": "^5.6.3" + } +} diff --git a/backend/src/auth/jwt.ts b/backend/src/auth/jwt.ts new file mode 100644 index 0000000..4fd8604 --- /dev/null +++ b/backend/src/auth/jwt.ts @@ -0,0 +1,32 @@ +import { createHmac } from "crypto"; +import { config } from "../config.js"; + +const HEADER = Buffer.from(JSON.stringify({ alg: "HS256", typ: "JWT" })).toString("base64url"); +const SEP = "."; + +export function signJwt(pubkey: string): string { + const exp = Math.floor(Date.now() / 1000) + config.jwtExpiresInSeconds; + const payload = Buffer.from(JSON.stringify({ pubkey, exp })).toString("base64url"); + const message = `${HEADER}${SEP}${payload}`; + const sig = createHmac("sha256", config.jwtSecret).update(message).digest("base64url"); + return `${message}${SEP}${sig}`; +} + +export function verifyJwt(token: string): { pubkey: string } | null { + try { + const parts = token.split(SEP); + if (parts.length !== 3) return null; + const [headerB64, payloadB64, sigB64] = parts; + const message = `${headerB64}${SEP}${payloadB64}`; + const expected = createHmac("sha256", config.jwtSecret).update(message).digest("base64url"); + if (sigB64 !== expected) return null; + const payload = JSON.parse( + Buffer.from(payloadB64, "base64url").toString("utf-8") + ) as { pubkey?: string; exp?: number }; + if (!payload.pubkey || typeof payload.pubkey !== "string") return null; + if (!payload.exp || payload.exp < Math.floor(Date.now() / 1000)) return null; + return { pubkey: payload.pubkey }; + } catch { + return null; + } +} diff --git a/backend/src/config.ts b/backend/src/config.ts new file mode 100644 index 0000000..8b3873f --- /dev/null +++ b/backend/src/config.ts @@ -0,0 +1,87 @@ +import "dotenv/config"; + +function env(key: string, defaultValue?: string): string { + const v = process.env[key]; + if (v !== undefined) return v; + if (defaultValue !== undefined) return defaultValue; + throw new Error(`Missing required env: ${key}`); +} + +function envInt(key: string, defaultValue: number): number { + const v = process.env[key]; + if (v === undefined) return defaultValue; + const n = parseInt(v, 10); + if (Number.isNaN(n)) throw new Error(`Invalid integer env: ${key}`); + return n; +} + +function envBool(key: string, defaultValue: boolean): boolean { + const v = process.env[key]; + if (v === undefined) return defaultValue; + return v === "true" || v === "1"; +} + +export const config = { + port: envInt("PORT", 3001), + trustProxy: envBool("TRUST_PROXY", false), + allowedOrigins: (process.env.ALLOWED_ORIGINS ?? process.env.FRONTEND_URL ?? "http://localhost:5173,http://localhost:5174").split(",").map((s) => s.trim()), + + // Database: omit DATABASE_URL for SQLite; set for Postgres + databaseUrl: process.env.DATABASE_URL as string | undefined, + sqlitePath: process.env.SQLITE_PATH ?? "./data/faucet.db", + + // Security + hmacIpSecret: env("HMAC_IP_SECRET"), + jwtSecret: env("JWT_SECRET", process.env.HMAC_IP_SECRET ?? ""), + jwtExpiresInSeconds: envInt("JWT_EXPIRES_IN_SECONDS", 86400 * 7), // 7 days + nip98MaxSkewSeconds: envInt("NIP98_MAX_SKEW_SECONDS", 300), + nonceTtlSeconds: envInt("NONCE_TTL_SECONDS", 600), + + // Faucet economics + faucetEnabled: envBool("FAUCET_ENABLED", true), + emergencyStop: envBool("EMERGENCY_STOP", false), + faucetMinSats: envInt("FAUCET_MIN_SATS", 1), + faucetMaxSats: envInt("FAUCET_MAX_SATS", 5), + payoutWeightSmall: envInt("PAYOUT_WEIGHT_SMALL", 50), + payoutWeightMedium: envInt("PAYOUT_WEIGHT_MEDIUM", 30), + payoutWeightLarge: envInt("PAYOUT_WEIGHT_LARGE", 15), + payoutWeightJackpot: envInt("PAYOUT_WEIGHT_JACKPOT", 5), + payoutSmallSats: envInt("PAYOUT_SMALL_SATS", 10), + payoutMediumSats: envInt("PAYOUT_MEDIUM_SATS", 25), + payoutLargeSats: envInt("PAYOUT_LARGE_SATS", 50), + payoutJackpotSats: envInt("PAYOUT_JACKPOT_SATS", 100), + dailyBudgetSats: envInt("DAILY_BUDGET_SATS", 10000), + maxClaimsPerDay: envInt("MAX_CLAIMS_PER_DAY", 100), + minWalletBalanceSats: envInt("MIN_WALLET_BALANCE_SATS", 1000), + + // Eligibility + minAccountAgeDays: envInt("MIN_ACCOUNT_AGE_DAYS", 14), + minActivityScore: envInt("MIN_ACTIVITY_SCORE", 30), + minNotesCount: envInt("MIN_NOTES_COUNT", 5), + minFollowingCount: envInt("MIN_FOLLOWING_COUNT", 10), + minFollowersCount: envInt("MIN_FOLLOWERS_COUNT", 0), + activityLookbackDays: envInt("ACTIVITY_LOOKBACK_DAYS", 90), + + // Cooldowns + cooldownDays: envInt("COOLDOWN_DAYS", 7), + ipCooldownDays: envInt("IP_COOLDOWN_DAYS", 7), + maxClaimsPerIpPerPeriod: envInt("MAX_CLAIMS_PER_IP_PER_PERIOD", 1), + + // Nostr (defaults include relays common for remote signers / NIP-05) + nostrRelays: (process.env.NOSTR_RELAYS ?? "wss://relay.damus.io,wss://relay.nostr.band,wss://relay.getalby.com,wss://nos.lol").split(",").map((s) => s.trim()), + relayTimeoutMs: envInt("RELAY_TIMEOUT_MS", 5000), + maxEventsFetch: envInt("MAX_EVENTS_FETCH", 500), + metadataCacheHours: envInt("METADATA_CACHE_HOURS", 24), + + // LNbits + lnbitsBaseUrl: env("LNBITS_BASE_URL").replace(/\/$/, ""), + lnbitsAdminKey: env("LNBITS_ADMIN_KEY"), + lnbitsWalletId: env("LNBITS_WALLET_ID"), + depositLightningAddress: process.env.DEPOSIT_LIGHTNING_ADDRESS ?? "", + depositLnurlp: process.env.DEPOSIT_LNURLP ?? "", + cashuRedeemApiUrl: (process.env.CASHU_REDEEM_API_URL ?? "https://cashu-redeem.azzamo.net").replace(/\/$/, ""), +}; + +export function usePostgres(): boolean { + return Boolean(config.databaseUrl); +} diff --git a/backend/src/db/index.ts b/backend/src/db/index.ts new file mode 100644 index 0000000..72e7a8c --- /dev/null +++ b/backend/src/db/index.ts @@ -0,0 +1,24 @@ +import { config, usePostgres } from "../config.js"; +import { createPgDb } from "./pg.js"; +import { createSqliteDb } from "./sqlite.js"; +import type { Db } from "./types.js"; +import { mkdirSync, existsSync } from "fs"; +import { dirname } from "path"; + +let dbInstance: Db | null = null; + +export function getDb(): Db { + if (!dbInstance) { + if (usePostgres() && config.databaseUrl) { + dbInstance = createPgDb(config.databaseUrl); + } else { + const dir = dirname(config.sqlitePath); + if (!existsSync(dir)) mkdirSync(dir, { recursive: true }); + dbInstance = createSqliteDb(config.sqlitePath); + } + } + return dbInstance; +} + +export type { Db } from "./types.js"; +export type { UserRow, ClaimRow, QuoteRow, IpLimitRow } from "./types.js"; diff --git a/backend/src/db/migrate.ts b/backend/src/db/migrate.ts new file mode 100644 index 0000000..9643902 --- /dev/null +++ b/backend/src/db/migrate.ts @@ -0,0 +1,14 @@ +import "dotenv/config"; +import { getDb } from "./index.js"; + +async function main() { + const db = getDb(); + await db.runMigrations(); + console.log("Migrations complete."); + process.exit(0); +} + +main().catch((err) => { + console.error(err); + process.exit(1); +}); diff --git a/backend/src/db/pg.ts b/backend/src/db/pg.ts new file mode 100644 index 0000000..5471099 --- /dev/null +++ b/backend/src/db/pg.ts @@ -0,0 +1,347 @@ +import pg from "pg"; +import { readFileSync } from "fs"; +import { dirname, join } from "path"; +import { fileURLToPath } from "url"; +import type { ClaimRow, Db, DepositSource, IpLimitRow, QuoteRow, UserRow } from "./types.js"; + +const __dirname = dirname(fileURLToPath(import.meta.url)); + +export function createPgDb(connectionString: string): Db { + const pool = new pg.Pool({ connectionString }); + + function toUserRow(r: pg.QueryResultRow): UserRow { + return { + pubkey: r.pubkey, + nostr_first_seen_at: r.nostr_first_seen_at != null ? Number(r.nostr_first_seen_at) : null, + notes_count: Number(r.notes_count), + followers_count: Number(r.followers_count), + following_count: Number(r.following_count), + activity_score: Number(r.activity_score), + last_metadata_fetch_at: r.last_metadata_fetch_at != null ? Number(r.last_metadata_fetch_at) : null, + lightning_address: r.lightning_address ?? null, + name: r.name ?? null, + created_at: Number(r.created_at), + updated_at: Number(r.updated_at), + }; + } + + function toClaimRow(r: pg.QueryResultRow): ClaimRow { + return { + id: r.id, + pubkey: r.pubkey, + claimed_at: Number(r.claimed_at), + payout_sats: r.payout_sats, + ip_hash: r.ip_hash, + payout_destination_hash: r.payout_destination_hash, + status: r.status, + lnbits_payment_hash: r.lnbits_payment_hash, + error_message: r.error_message, + }; + } + + function toQuoteRow(r: pg.QueryResultRow): QuoteRow { + return { + quote_id: r.quote_id, + pubkey: r.pubkey, + payout_sats: r.payout_sats, + lightning_address: r.lightning_address ?? null, + created_at: Number(r.created_at), + expires_at: Number(r.expires_at), + status: r.status, + }; + } + + return { + async runMigrations() { + const schema = readFileSync(join(__dirname, "schema.pg.sql"), "utf-8"); + await pool.query(schema); + try { + await pool.query("ALTER TABLE users ADD COLUMN lightning_address TEXT"); + } catch (_) {} + try { + await pool.query("ALTER TABLE users ADD COLUMN name TEXT"); + } catch (_) {} + try { + await pool.query( + `CREATE TABLE IF NOT EXISTS deposits ( + id SERIAL PRIMARY KEY, + created_at BIGINT NOT NULL, + amount_sats INTEGER NOT NULL, + source TEXT NOT NULL CHECK(source IN ('lightning','cashu')), + lnbits_payment_hash TEXT + )` + ); + await pool.query("CREATE INDEX IF NOT EXISTS idx_deposits_created_at ON deposits(created_at)"); + } catch (_) {} + try { + await pool.query("ALTER TABLE deposits ADD COLUMN lnbits_payment_hash TEXT"); + } catch (_) {} + try { + await pool.query("CREATE INDEX IF NOT EXISTS idx_deposits_lnbits_payment_hash ON deposits(lnbits_payment_hash)"); + } catch (_) {} + try { + await pool.query( + "UPDATE deposits SET amount_sats = amount_sats / 1000 WHERE source = 'lightning' AND lnbits_payment_hash IS NOT NULL AND amount_sats >= 1000" + ); + } catch (_) {} + }, + + async getUser(pubkey: string): Promise { + const res = await pool.query("SELECT * FROM users WHERE pubkey = $1", [pubkey]); + return res.rows.length ? toUserRow(res.rows[0]) : null; + }, + + async upsertUser(row: Omit): Promise { + const now = Math.floor(Date.now() / 1000); + await pool.query( + `INSERT INTO users (pubkey, nostr_first_seen_at, notes_count, followers_count, following_count, activity_score, last_metadata_fetch_at, lightning_address, name, created_at, updated_at) + VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $10) + ON CONFLICT(pubkey) DO UPDATE SET + nostr_first_seen_at = EXCLUDED.nostr_first_seen_at, + notes_count = EXCLUDED.notes_count, + followers_count = EXCLUDED.followers_count, + following_count = EXCLUDED.following_count, + activity_score = EXCLUDED.activity_score, + last_metadata_fetch_at = EXCLUDED.last_metadata_fetch_at, + lightning_address = EXCLUDED.lightning_address, + name = EXCLUDED.name, + updated_at = EXCLUDED.updated_at`, + [ + row.pubkey, + row.nostr_first_seen_at ?? null, + row.notes_count ?? 0, + row.followers_count ?? 0, + row.following_count ?? 0, + row.activity_score ?? 0, + row.last_metadata_fetch_at ?? null, + row.lightning_address ?? null, + row.name ?? null, + now, + ] + ); + }, + + async updateUserNostrCache( + pubkey: string, + data: { + nostr_first_seen_at: number | null; + notes_count: number; + followers_count: number; + following_count: number; + activity_score: number; + last_metadata_fetch_at: number; + } + ): Promise { + const now = Math.floor(Date.now() / 1000); + await pool.query( + `UPDATE users SET + nostr_first_seen_at = $1, notes_count = $2, followers_count = $3, following_count = $4, + activity_score = $5, last_metadata_fetch_at = $6, updated_at = $7 + WHERE pubkey = $8`, + [ + data.nostr_first_seen_at, + data.notes_count, + data.followers_count, + data.following_count, + data.activity_score, + data.last_metadata_fetch_at, + now, + pubkey, + ] + ); + }, + + async getLastSuccessfulClaimByPubkey(pubkey: string): Promise { + const res = await pool.query( + "SELECT * FROM claims WHERE pubkey = $1 AND status = 'paid' ORDER BY claimed_at DESC LIMIT 1", + [pubkey] + ); + return res.rows.length ? toClaimRow(res.rows[0]) : null; + }, + + async getLastClaimByIpHash(ipHash: string): Promise { + const res = await pool.query( + "SELECT * FROM claims WHERE ip_hash = $1 AND status = 'paid' ORDER BY claimed_at DESC LIMIT 1", + [ipHash] + ); + return res.rows.length ? toClaimRow(res.rows[0]) : null; + }, + + async getClaimCountForIpSince(ipHash: string, sinceTs: number): Promise { + const res = await pool.query( + "SELECT COUNT(*) as c FROM claims WHERE ip_hash = $1 AND status = 'paid' AND claimed_at >= $2", + [ipHash, sinceTs] + ); + return parseInt(res.rows[0]?.c ?? "0", 10); + }, + + async createClaim(row: Omit): Promise { + const res = await pool.query( + `INSERT INTO claims (pubkey, claimed_at, payout_sats, ip_hash, payout_destination_hash, status, lnbits_payment_hash, error_message) + VALUES ($1, $2, $3, $4, $5, $6, $7, $8) RETURNING id`, + [ + row.pubkey, + row.claimed_at, + row.payout_sats, + row.ip_hash, + row.payout_destination_hash ?? null, + row.status, + row.lnbits_payment_hash ?? null, + row.error_message ?? null, + ] + ); + return res.rows[0].id; + }, + + async updateClaimStatus( + id: number, + status: ClaimRow["status"], + lnbitsPaymentHash?: string, + errorMessage?: string + ): Promise { + await pool.query( + "UPDATE claims SET status = $1, lnbits_payment_hash = $2, error_message = $3 WHERE id = $4", + [status, lnbitsPaymentHash ?? null, errorMessage ?? null, id] + ); + }, + + async getIpLimit(ipHash: string): Promise { + const res = await pool.query("SELECT * FROM ip_limits WHERE ip_hash = $1", [ipHash]); + if (!res.rows.length) return null; + const r = res.rows[0]; + return { + ip_hash: r.ip_hash, + last_claimed_at: Number(r.last_claimed_at), + claim_count_period: Number(r.claim_count_period), + }; + }, + + async upsertIpLimit(ipHash: string, lastClaimedAt: number, claimCountPeriod: number): Promise { + await pool.query( + `INSERT INTO ip_limits (ip_hash, last_claimed_at, claim_count_period) VALUES ($1, $2, $3) + ON CONFLICT(ip_hash) DO UPDATE SET last_claimed_at = $2, claim_count_period = $3`, + [ipHash, lastClaimedAt, claimCountPeriod] + ); + }, + + async createQuote(quoteId: string, pubkey: string, payoutSats: number, lightningAddress: string, expiresAt: number): Promise { + const now = Math.floor(Date.now() / 1000); + await pool.query( + "INSERT INTO quotes (quote_id, pubkey, payout_sats, lightning_address, created_at, expires_at, status) VALUES ($1, $2, $3, $4, $5, $6, 'active')", + [quoteId, pubkey, payoutSats, lightningAddress, now, expiresAt] + ); + }, + + async getQuote(quoteId: string): Promise { + const res = await pool.query("SELECT * FROM quotes WHERE quote_id = $1", [quoteId]); + return res.rows.length ? toQuoteRow(res.rows[0]) : null; + }, + + async consumeQuote(quoteId: string): Promise { + await pool.query("UPDATE quotes SET status = 'consumed' WHERE quote_id = $1", [quoteId]); + }, + + async setNonce(nonce: string, expiresAt: number): Promise { + await pool.query( + "INSERT INTO nonces (nonce, expires_at) VALUES ($1, $2) ON CONFLICT (nonce) DO UPDATE SET expires_at = $2", + [nonce, expiresAt] + ); + }, + + async hasNonce(nonce: string): Promise { + const now = Math.floor(Date.now() / 1000); + const res = await pool.query("SELECT 1 FROM nonces WHERE nonce = $1 AND expires_at > $2", [nonce, now]); + return res.rows.length > 0; + }, + + async deleteExpiredNonces(): Promise { + const now = Math.floor(Date.now() / 1000); + await pool.query("DELETE FROM nonces WHERE expires_at <= $1", [now]); + }, + + async getTotalPaidSats(): Promise { + const res = await pool.query( + "SELECT COALESCE(SUM(payout_sats), 0)::bigint as total FROM claims WHERE status = 'paid'" + ); + return parseInt(res.rows[0]?.total ?? "0", 10); + }, + + async getTotalClaimsCount(): Promise { + const res = await pool.query("SELECT COUNT(*) as c FROM claims WHERE status = 'paid'"); + return parseInt(res.rows[0]?.c ?? "0", 10); + }, + + async getClaimsCountSince(sinceTs: number): Promise { + const res = await pool.query( + "SELECT COUNT(*) as c FROM claims WHERE status = 'paid' AND claimed_at >= $1", + [sinceTs] + ); + return parseInt(res.rows[0]?.c ?? "0", 10); + }, + + async getPaidSatsSince(sinceTs: number): Promise { + const res = await pool.query( + "SELECT COALESCE(SUM(payout_sats), 0)::bigint as total FROM claims WHERE status = 'paid' AND claimed_at >= $1", + [sinceTs] + ); + return parseInt(res.rows[0]?.total ?? "0", 10); + }, + + async getRecentPayouts( + limit: number + ): Promise<{ pubkey_prefix: string; payout_sats: number; claimed_at: number }[]> { + const res = await pool.query( + "SELECT pubkey, payout_sats, claimed_at FROM claims WHERE status = 'paid' ORDER BY claimed_at DESC LIMIT $1", + [limit] + ); + return res.rows.map((r) => ({ + pubkey_prefix: r.pubkey.slice(0, 8) + "…", + payout_sats: r.payout_sats, + claimed_at: Number(r.claimed_at), + })); + }, + + async insertDeposit( + amountSats: number, + source: DepositSource, + lnbitsPaymentHash?: string | null, + createdAt?: number + ): Promise { + const now = createdAt ?? Math.floor(Date.now() / 1000); + await pool.query( + "INSERT INTO deposits (created_at, amount_sats, source, lnbits_payment_hash) VALUES ($1, $2, $3, $4)", + [now, amountSats, source, lnbitsPaymentHash ?? null] + ); + }, + + async hasDepositWithPaymentHash(paymentHash: string): Promise { + const res = await pool.query( + "SELECT 1 FROM deposits WHERE lnbits_payment_hash = $1 LIMIT 1", + [paymentHash] + ); + return res.rows.length > 0; + }, + + async updateDepositCreatedAtIfMissing(paymentHash: string, createdAt: number): Promise { + const res = await pool.query( + "UPDATE deposits SET created_at = $1 WHERE lnbits_payment_hash = $2 AND (created_at IS NULL OR created_at < 1000000000) RETURNING id", + [createdAt, paymentHash] + ); + return res.rowCount !== null && res.rowCount > 0; + }, + + async getRecentDeposits( + limit: number + ): Promise<{ amount_sats: number; source: DepositSource; created_at: number }[]> { + const res = await pool.query( + "SELECT amount_sats, source, created_at FROM deposits ORDER BY created_at DESC LIMIT $1", + [limit] + ); + return res.rows.map((r) => ({ + amount_sats: Number(r.amount_sats), + source: r.source as DepositSource, + created_at: Number(r.created_at), + })); + }, + }; +} diff --git a/backend/src/db/schema-sqlite.sql b/backend/src/db/schema-sqlite.sql new file mode 100644 index 0000000..4181a99 --- /dev/null +++ b/backend/src/db/schema-sqlite.sql @@ -0,0 +1,50 @@ +-- SQLite schema for LNFaucet +CREATE TABLE IF NOT EXISTS users ( + pubkey TEXT PRIMARY KEY, + nostr_first_seen_at INTEGER, + notes_count INTEGER NOT NULL DEFAULT 0, + followers_count INTEGER NOT NULL DEFAULT 0, + following_count INTEGER NOT NULL DEFAULT 0, + activity_score INTEGER NOT NULL DEFAULT 0, + last_metadata_fetch_at INTEGER, + created_at INTEGER NOT NULL, + updated_at INTEGER NOT NULL +); + +CREATE TABLE IF NOT EXISTS claims ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + pubkey TEXT NOT NULL, + claimed_at INTEGER NOT NULL, + payout_sats INTEGER NOT NULL, + ip_hash TEXT NOT NULL, + payout_destination_hash TEXT, + status TEXT NOT NULL, + lnbits_payment_hash TEXT, + error_message TEXT, + quote_id TEXT +); + +CREATE TABLE IF NOT EXISTS ip_limits ( + ip_hash TEXT PRIMARY KEY, + last_claimed_at INTEGER, + claim_count_period INTEGER NOT NULL DEFAULT 0 +); + +CREATE TABLE IF NOT EXISTS quotes ( + quote_id TEXT PRIMARY KEY, + pubkey TEXT NOT NULL, + payout_sats INTEGER NOT NULL, + created_at INTEGER NOT NULL, + expires_at INTEGER NOT NULL, + status TEXT NOT NULL +); + +CREATE TABLE IF NOT EXISTS nonces ( + nonce TEXT PRIMARY KEY, + expires_at INTEGER NOT NULL +); + +CREATE INDEX IF NOT EXISTS idx_claims_pubkey ON claims(pubkey); +CREATE INDEX IF NOT EXISTS idx_claims_claimed_at ON claims(claimed_at); +CREATE INDEX IF NOT EXISTS idx_quotes_expires_at ON quotes(expires_at); +CREATE INDEX IF NOT EXISTS idx_quotes_status ON quotes(status); diff --git a/backend/src/db/schema.pg.sql b/backend/src/db/schema.pg.sql new file mode 100644 index 0000000..fc9e35a --- /dev/null +++ b/backend/src/db/schema.pg.sql @@ -0,0 +1,69 @@ +-- Postgres schema +CREATE TABLE IF NOT EXISTS users ( + pubkey TEXT PRIMARY KEY, + nostr_first_seen_at BIGINT, + notes_count INTEGER DEFAULT 0, + followers_count INTEGER DEFAULT 0, + following_count INTEGER DEFAULT 0, + activity_score INTEGER DEFAULT 0, + last_metadata_fetch_at BIGINT, + lightning_address TEXT, + name TEXT, + created_at BIGINT NOT NULL DEFAULT (EXTRACT(EPOCH FROM NOW())::BIGINT), + updated_at BIGINT NOT NULL DEFAULT (EXTRACT(EPOCH FROM NOW())::BIGINT) +); + +CREATE TABLE IF NOT EXISTS claims ( + id SERIAL PRIMARY KEY, + pubkey TEXT NOT NULL REFERENCES users(pubkey), + claimed_at BIGINT NOT NULL, + payout_sats INTEGER NOT NULL, + ip_hash TEXT NOT NULL, + payout_destination_hash TEXT, + status TEXT NOT NULL CHECK(status IN ('pending','paid','failed')), + lnbits_payment_hash TEXT, + error_message TEXT +); + +CREATE TABLE IF NOT EXISTS ip_limits ( + ip_hash TEXT PRIMARY KEY, + last_claimed_at BIGINT NOT NULL, + claim_count_period INTEGER NOT NULL DEFAULT 0 +); + +CREATE TABLE IF NOT EXISTS quotes ( + quote_id TEXT PRIMARY KEY, + pubkey TEXT NOT NULL, + payout_sats INTEGER NOT NULL, + lightning_address TEXT, + created_at BIGINT NOT NULL, + expires_at BIGINT NOT NULL, + status TEXT NOT NULL CHECK(status IN ('active','consumed','expired')) +); + +CREATE TABLE IF NOT EXISTS daily_stats ( + date TEXT PRIMARY KEY, + total_paid_sats INTEGER NOT NULL DEFAULT 0, + total_claims INTEGER NOT NULL DEFAULT 0, + unique_pubkeys INTEGER NOT NULL DEFAULT 0 +); + +CREATE TABLE IF NOT EXISTS nonces ( + nonce TEXT PRIMARY KEY, + expires_at BIGINT NOT NULL +); + +CREATE TABLE IF NOT EXISTS deposits ( + id SERIAL PRIMARY KEY, + created_at BIGINT NOT NULL, + amount_sats INTEGER NOT NULL, + source TEXT NOT NULL CHECK(source IN ('lightning','cashu')), + lnbits_payment_hash TEXT +); + +CREATE INDEX IF NOT EXISTS idx_claims_pubkey ON claims(pubkey); +CREATE INDEX IF NOT EXISTS idx_claims_claimed_at ON claims(claimed_at); +CREATE INDEX IF NOT EXISTS idx_quotes_expires_at ON quotes(expires_at); +CREATE INDEX IF NOT EXISTS idx_quotes_status ON quotes(status); +CREATE INDEX IF NOT EXISTS idx_deposits_created_at ON deposits(created_at); +CREATE INDEX IF NOT EXISTS idx_deposits_lnbits_payment_hash ON deposits(lnbits_payment_hash); diff --git a/backend/src/db/schema.sql b/backend/src/db/schema.sql new file mode 100644 index 0000000..d244c3b --- /dev/null +++ b/backend/src/db/schema.sql @@ -0,0 +1,70 @@ +-- SQLite schema +CREATE TABLE IF NOT EXISTS users ( + pubkey TEXT PRIMARY KEY, + nostr_first_seen_at INTEGER, + notes_count INTEGER DEFAULT 0, + followers_count INTEGER DEFAULT 0, + following_count INTEGER DEFAULT 0, + activity_score INTEGER DEFAULT 0, + last_metadata_fetch_at INTEGER, + lightning_address TEXT, + name TEXT, + created_at INTEGER NOT NULL DEFAULT (unixepoch()), + updated_at INTEGER NOT NULL DEFAULT (unixepoch()) +); + +CREATE TABLE IF NOT EXISTS claims ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + pubkey TEXT NOT NULL, + claimed_at INTEGER NOT NULL, + payout_sats INTEGER NOT NULL, + ip_hash TEXT NOT NULL, + payout_destination_hash TEXT, + status TEXT NOT NULL CHECK(status IN ('pending','paid','failed')), + lnbits_payment_hash TEXT, + error_message TEXT, + FOREIGN KEY (pubkey) REFERENCES users(pubkey) +); + +CREATE TABLE IF NOT EXISTS ip_limits ( + ip_hash TEXT PRIMARY KEY, + last_claimed_at INTEGER NOT NULL, + claim_count_period INTEGER NOT NULL DEFAULT 0 +); + +CREATE TABLE IF NOT EXISTS quotes ( + quote_id TEXT PRIMARY KEY, + pubkey TEXT NOT NULL, + payout_sats INTEGER NOT NULL, + lightning_address TEXT, + created_at INTEGER NOT NULL, + expires_at INTEGER NOT NULL, + status TEXT NOT NULL CHECK(status IN ('active','consumed','expired')) +); + +CREATE TABLE IF NOT EXISTS daily_stats ( + date TEXT PRIMARY KEY, + total_paid_sats INTEGER NOT NULL DEFAULT 0, + total_claims INTEGER NOT NULL DEFAULT 0, + unique_pubkeys INTEGER NOT NULL DEFAULT 0 +); + +CREATE TABLE IF NOT EXISTS nonces ( + nonce TEXT PRIMARY KEY, + expires_at INTEGER NOT NULL +); + +CREATE TABLE IF NOT EXISTS deposits ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + created_at INTEGER NOT NULL, + amount_sats INTEGER NOT NULL, + source TEXT NOT NULL CHECK(source IN ('lightning','cashu')), + lnbits_payment_hash TEXT +); + +CREATE INDEX IF NOT EXISTS idx_claims_pubkey ON claims(pubkey); +CREATE INDEX IF NOT EXISTS idx_claims_claimed_at ON claims(claimed_at); +CREATE INDEX IF NOT EXISTS idx_quotes_expires_at ON quotes(expires_at); +CREATE INDEX IF NOT EXISTS idx_quotes_status ON quotes(status); +CREATE INDEX IF NOT EXISTS idx_deposits_created_at ON deposits(created_at); +CREATE INDEX IF NOT EXISTS idx_deposits_lnbits_payment_hash ON deposits(lnbits_payment_hash); diff --git a/backend/src/db/sqlite.ts b/backend/src/db/sqlite.ts new file mode 100644 index 0000000..0155a69 --- /dev/null +++ b/backend/src/db/sqlite.ts @@ -0,0 +1,287 @@ +import Database from "better-sqlite3"; +import { readFileSync } from "fs"; +import { dirname, join } from "path"; +import { fileURLToPath } from "url"; +import type { ClaimRow, Db, DepositSource, IpLimitRow, QuoteRow, UserRow } from "./types.js"; + +const __dirname = dirname(fileURLToPath(import.meta.url)); + +export function createSqliteDb(path: string): Db { + const db = new Database(path); + + return { + async runMigrations() { + const schema = readFileSync(join(__dirname, "schema.sql"), "utf-8"); + db.exec(schema); + try { + db.exec("ALTER TABLE users ADD COLUMN lightning_address TEXT"); + } catch (_) {} + try { + db.exec("ALTER TABLE users ADD COLUMN name TEXT"); + } catch (_) {} + try { + db.exec( + "CREATE TABLE IF NOT EXISTS deposits (id INTEGER PRIMARY KEY AUTOINCREMENT, created_at INTEGER NOT NULL, amount_sats INTEGER NOT NULL, source TEXT NOT NULL CHECK(source IN ('lightning','cashu')), lnbits_payment_hash TEXT)" + ); + db.exec("CREATE INDEX IF NOT EXISTS idx_deposits_created_at ON deposits(created_at)"); + } catch (_) {} + try { + db.exec("ALTER TABLE deposits ADD COLUMN lnbits_payment_hash TEXT"); + } catch (_) {} + try { + db.exec("CREATE INDEX IF NOT EXISTS idx_deposits_lnbits_payment_hash ON deposits(lnbits_payment_hash)"); + } catch (_) {} + try { + db.exec( + "UPDATE deposits SET amount_sats = amount_sats / 1000 WHERE source = 'lightning' AND lnbits_payment_hash IS NOT NULL AND amount_sats >= 1000" + ); + } catch (_) {} + }, + + async getUser(pubkey: string): Promise { + const row = db.prepare("SELECT * FROM users WHERE pubkey = ?").get(pubkey) as UserRow | undefined; + return row ?? null; + }, + + async upsertUser(row: Omit): Promise { + const now = Math.floor(Date.now() / 1000); + db.prepare( + `INSERT INTO users (pubkey, nostr_first_seen_at, notes_count, followers_count, following_count, activity_score, last_metadata_fetch_at, lightning_address, name, created_at, updated_at) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + ON CONFLICT(pubkey) DO UPDATE SET + nostr_first_seen_at = excluded.nostr_first_seen_at, + notes_count = excluded.notes_count, + followers_count = excluded.followers_count, + following_count = excluded.following_count, + activity_score = excluded.activity_score, + last_metadata_fetch_at = excluded.last_metadata_fetch_at, + lightning_address = excluded.lightning_address, + name = excluded.name, + updated_at = excluded.updated_at` + ).run( + row.pubkey, + row.nostr_first_seen_at ?? null, + row.notes_count ?? 0, + row.followers_count ?? 0, + row.following_count ?? 0, + row.activity_score ?? 0, + row.last_metadata_fetch_at ?? null, + row.lightning_address ?? null, + row.name ?? null, + now, + now + ); + }, + + async updateUserNostrCache( + pubkey: string, + data: { + nostr_first_seen_at: number | null; + notes_count: number; + followers_count: number; + following_count: number; + activity_score: number; + last_metadata_fetch_at: number; + } + ): Promise { + const now = Math.floor(Date.now() / 1000); + db.prepare( + `UPDATE users SET + nostr_first_seen_at = ?, notes_count = ?, followers_count = ?, following_count = ?, + activity_score = ?, last_metadata_fetch_at = ?, updated_at = ? + WHERE pubkey = ?` + ).run( + data.nostr_first_seen_at, + data.notes_count, + data.followers_count, + data.following_count, + data.activity_score, + data.last_metadata_fetch_at, + now, + pubkey + ); + }, + + async getLastSuccessfulClaimByPubkey(pubkey: string): Promise { + const row = db + .prepare("SELECT * FROM claims WHERE pubkey = ? AND status = 'paid' ORDER BY claimed_at DESC LIMIT 1") + .get(pubkey) as ClaimRow | undefined; + return row ?? null; + }, + + async getLastClaimByIpHash(ipHash: string): Promise { + const row = db + .prepare("SELECT * FROM claims WHERE ip_hash = ? AND status = 'paid' ORDER BY claimed_at DESC LIMIT 1") + .get(ipHash) as ClaimRow | undefined; + return row ?? null; + }, + + async getClaimCountForIpSince(ipHash: string, sinceTs: number): Promise { + const row = db + .prepare( + "SELECT COUNT(*) as c FROM claims WHERE ip_hash = ? AND status = 'paid' AND claimed_at >= ?" + ) + .get(ipHash, sinceTs) as { c: number }; + return row?.c ?? 0; + }, + + async createClaim(row: Omit): Promise { + const result = db + .prepare( + `INSERT INTO claims (pubkey, claimed_at, payout_sats, ip_hash, payout_destination_hash, status, lnbits_payment_hash, error_message) + VALUES (?, ?, ?, ?, ?, ?, ?, ?)` + ) + .run( + row.pubkey, + row.claimed_at, + row.payout_sats, + row.ip_hash, + row.payout_destination_hash ?? null, + row.status, + row.lnbits_payment_hash ?? null, + row.error_message ?? null + ); + return result.lastInsertRowid as number; + }, + + async updateClaimStatus( + id: number, + status: ClaimRow["status"], + lnbitsPaymentHash?: string, + errorMessage?: string + ): Promise { + db.prepare( + "UPDATE claims SET status = ?, lnbits_payment_hash = ?, error_message = ? WHERE id = ?" + ).run(status, lnbitsPaymentHash ?? null, errorMessage ?? null, id); + }, + + async getIpLimit(ipHash: string): Promise { + const row = db.prepare("SELECT * FROM ip_limits WHERE ip_hash = ?").get(ipHash) as IpLimitRow | undefined; + return row ?? null; + }, + + async upsertIpLimit(ipHash: string, lastClaimedAt: number, claimCountPeriod: number): Promise { + db.prepare( + `INSERT INTO ip_limits (ip_hash, last_claimed_at, claim_count_period) VALUES (?, ?, ?) + ON CONFLICT(ip_hash) DO UPDATE SET last_claimed_at = ?, claim_count_period = ?` + ).run(ipHash, lastClaimedAt, claimCountPeriod, lastClaimedAt, claimCountPeriod); + }, + + async createQuote(quoteId: string, pubkey: string, payoutSats: number, lightningAddress: string, expiresAt: number): Promise { + const now = Math.floor(Date.now() / 1000); + db.prepare( + "INSERT INTO quotes (quote_id, pubkey, payout_sats, lightning_address, created_at, expires_at, status) VALUES (?, ?, ?, ?, ?, ?, 'active')" + ).run(quoteId, pubkey, payoutSats, lightningAddress, now, expiresAt); + }, + + async getQuote(quoteId: string): Promise { + const row = db.prepare("SELECT * FROM quotes WHERE quote_id = ?").get(quoteId) as QuoteRow | undefined; + return row ?? null; + }, + + async consumeQuote(quoteId: string): Promise { + db.prepare("UPDATE quotes SET status = 'consumed' WHERE quote_id = ?").run(quoteId); + }, + + async setNonce(nonce: string, expiresAt: number): Promise { + db.prepare("INSERT OR REPLACE INTO nonces (nonce, expires_at) VALUES (?, ?)").run(nonce, expiresAt); + }, + + async hasNonce(nonce: string): Promise { + const now = Math.floor(Date.now() / 1000); + const row = db.prepare("SELECT 1 FROM nonces WHERE nonce = ? AND expires_at > ?").get(nonce, now); + return !!row; + }, + + async deleteExpiredNonces(): Promise { + const now = Math.floor(Date.now() / 1000); + db.prepare("DELETE FROM nonces WHERE expires_at <= ?").run(now); + }, + + async getTotalPaidSats(): Promise { + const row = db + .prepare("SELECT COALESCE(SUM(payout_sats), 0) as total FROM claims WHERE status = 'paid'") + .get() as { total: number }; + return row?.total ?? 0; + }, + + async getTotalClaimsCount(): Promise { + const row = db + .prepare("SELECT COUNT(*) as c FROM claims WHERE status = 'paid'") + .get() as { c: number }; + return row?.c ?? 0; + }, + + async getClaimsCountSince(sinceTs: number): Promise { + const row = db + .prepare("SELECT COUNT(*) as c FROM claims WHERE status = 'paid' AND claimed_at >= ?") + .get(sinceTs) as { c: number }; + return row?.c ?? 0; + }, + + async getPaidSatsSince(sinceTs: number): Promise { + const row = db + .prepare("SELECT COALESCE(SUM(payout_sats), 0) as total FROM claims WHERE status = 'paid' AND claimed_at >= ?") + .get(sinceTs) as { total: number }; + return row?.total ?? 0; + }, + + async getRecentPayouts( + limit: number + ): Promise<{ pubkey_prefix: string; payout_sats: number; claimed_at: number }[]> { + const rows = db + .prepare( + "SELECT pubkey, payout_sats, claimed_at FROM claims WHERE status = 'paid' ORDER BY claimed_at DESC LIMIT ?" + ) + .all(limit) as { pubkey: string; payout_sats: number; claimed_at: number }[]; + return rows.map((r) => ({ + pubkey_prefix: r.pubkey.slice(0, 8) + "…", + payout_sats: r.payout_sats, + claimed_at: r.claimed_at, + })); + }, + + async insertDeposit( + amountSats: number, + source: DepositSource, + lnbitsPaymentHash?: string | null, + createdAt?: number + ): Promise { + const now = createdAt ?? Math.floor(Date.now() / 1000); + db.prepare( + "INSERT INTO deposits (created_at, amount_sats, source, lnbits_payment_hash) VALUES (?, ?, ?, ?)" + ).run(now, amountSats, source, lnbitsPaymentHash ?? null); + }, + + async hasDepositWithPaymentHash(paymentHash: string): Promise { + const row = db + .prepare("SELECT 1 FROM deposits WHERE lnbits_payment_hash = ? LIMIT 1") + .get(paymentHash); + return !!row; + }, + + async updateDepositCreatedAtIfMissing(paymentHash: string, createdAt: number): Promise { + const result = db + .prepare( + "UPDATE deposits SET created_at = ? WHERE lnbits_payment_hash = ? AND (created_at IS NULL OR created_at < 1000000000)" + ) + .run(createdAt, paymentHash); + return result.changes > 0; + }, + + async getRecentDeposits( + limit: number + ): Promise<{ amount_sats: number; source: DepositSource; created_at: number }[]> { + const rows = db + .prepare( + "SELECT amount_sats, source, created_at FROM deposits ORDER BY created_at DESC LIMIT ?" + ) + .all(limit) as { amount_sats: number; source: DepositSource; created_at: number }[]; + return rows.map((r) => ({ + amount_sats: r.amount_sats, + source: r.source as DepositSource, + created_at: r.created_at, + })); + }, + }; +} diff --git a/backend/src/db/types.ts b/backend/src/db/types.ts new file mode 100644 index 0000000..336a5d2 --- /dev/null +++ b/backend/src/db/types.ts @@ -0,0 +1,101 @@ +export interface UserRow { + pubkey: string; + nostr_first_seen_at: number | null; + notes_count: number; + followers_count: number; + following_count: number; + activity_score: number; + last_metadata_fetch_at: number | null; + lightning_address: string | null; + name: string | null; + created_at: number; + updated_at: number; +} + +export interface ClaimRow { + id: number; + pubkey: string; + claimed_at: number; + payout_sats: number; + ip_hash: string; + payout_destination_hash: string | null; + status: "pending" | "paid" | "failed"; + lnbits_payment_hash: string | null; + error_message: string | null; +} + +export interface QuoteRow { + quote_id: string; + pubkey: string; + payout_sats: number; + lightning_address: string | null; + created_at: number; + expires_at: number; + status: "active" | "consumed" | "expired"; +} + +export interface IpLimitRow { + ip_hash: string; + last_claimed_at: number; + claim_count_period: number; +} + +export type DepositSource = "lightning" | "cashu"; + +export interface DepositRow { + id: number; + created_at: number; + amount_sats: number; + source: DepositSource; +} + +export interface Db { + runMigrations(): Promise; + + getUser(pubkey: string): Promise; + upsertUser(row: Omit): Promise; + updateUserNostrCache( + pubkey: string, + data: { + nostr_first_seen_at: number | null; + notes_count: number; + followers_count: number; + following_count: number; + activity_score: number; + last_metadata_fetch_at: number; + } + ): Promise; + + getLastSuccessfulClaimByPubkey(pubkey: string): Promise; + getLastClaimByIpHash(ipHash: string): Promise; + getClaimCountForIpSince(ipHash: string, sinceTs: number): Promise; + createClaim(row: Omit): Promise; + updateClaimStatus(id: number, status: ClaimRow["status"], lnbitsPaymentHash?: string, errorMessage?: string): Promise; + + getIpLimit(ipHash: string): Promise; + upsertIpLimit(ipHash: string, lastClaimedAt: number, claimCountPeriod: number): Promise; + + createQuote(quoteId: string, pubkey: string, payoutSats: number, lightningAddress: string, expiresAt: number): Promise; + getQuote(quoteId: string): Promise; + consumeQuote(quoteId: string): Promise; + + setNonce(nonce: string, expiresAt: number): Promise; + hasNonce(nonce: string): Promise; + deleteExpiredNonces(): Promise; + + getTotalPaidSats(): Promise; + getTotalClaimsCount(): Promise; + getClaimsCountSince(sinceTs: number): Promise; + getPaidSatsSince(sinceTs: number): Promise; + getRecentPayouts(limit: number): Promise<{ pubkey_prefix: string; payout_sats: number; claimed_at: number }[]>; + + insertDeposit( + amountSats: number, + source: DepositSource, + lnbitsPaymentHash?: string | null, + createdAt?: number + ): Promise; + hasDepositWithPaymentHash(paymentHash: string): Promise; + updateDepositCreatedAtIfMissing(paymentHash: string, createdAt: number): Promise; + getRecentDeposits(limit: number): Promise<{ amount_sats: number; source: DepositSource; created_at: number }[]>; +} diff --git a/backend/src/index.ts b/backend/src/index.ts new file mode 100644 index 0000000..02f1040 --- /dev/null +++ b/backend/src/index.ts @@ -0,0 +1,64 @@ +import express from "express"; +import cors from "cors"; +import rateLimit from "express-rate-limit"; +import { config } from "./config.js"; +import { getDb } from "./db/index.js"; +import { startLnbitsDepositSync } from "./services/syncLnbitsDeposits.js"; +import publicRoutes from "./routes/public.js"; +import authRoutes from "./routes/auth.js"; +import claimRoutes from "./routes/claim.js"; +import userRoutes from "./routes/user.js"; + +async function main() { + const db = getDb(); + await db.runMigrations(); + db.deleteExpiredNonces().catch(() => {}); + + const app = express(); + if (config.trustProxy) app.set("trust proxy", 1); + app.use(express.json({ limit: "10kb" })); + app.use( + cors({ + origin: (origin, cb) => { + if (!origin) return cb(null, true); + if (origin.startsWith("http://localhost:") || origin.startsWith("http://127.0.0.1:")) return cb(null, true); + if (config.allowedOrigins.includes(origin)) return cb(null, true); + return cb(null, false); + }, + credentials: true, + }) + ); + + app.use("/", publicRoutes); + app.use("/auth", authRoutes); + app.use( + "/claim", + rateLimit({ + windowMs: 60 * 1000, + max: 20, + message: { code: "rate_limited", message: "Too many requests." }, + }), + claimRoutes + ); + app.use( + "/user", + rateLimit({ + windowMs: 60 * 1000, + max: 30, + message: { code: "rate_limited", message: "Too many requests." }, + }), + userRoutes + ); + + app.listen(config.port, () => { + console.log(`Faucet API listening on port ${config.port}`); + if (config.lnbitsBaseUrl && config.lnbitsAdminKey) { + startLnbitsDepositSync(); + } + }); +} + +main().catch((err) => { + console.error(err); + process.exit(1); +}); diff --git a/backend/src/middleware/auth.ts b/backend/src/middleware/auth.ts new file mode 100644 index 0000000..77a9dd3 --- /dev/null +++ b/backend/src/middleware/auth.ts @@ -0,0 +1,22 @@ +import { Request, Response, NextFunction } from "express"; +import { verifyJwt } from "../auth/jwt.js"; +import { nip98Auth } from "./nip98.js"; + +const BEARER_PREFIX = "Bearer "; + +/** + * Accept either Bearer JWT or NIP-98. Sets req.nostr = { pubkey }. + */ +export function authOrNip98(req: Request, res: Response, next: NextFunction): void { + const auth = req.headers.authorization; + if (auth?.startsWith(BEARER_PREFIX)) { + const token = auth.slice(BEARER_PREFIX.length).trim(); + const payload = verifyJwt(token); + if (payload) { + req.nostr = { pubkey: payload.pubkey, eventId: "" }; + next(); + return; + } + } + nip98Auth(req, res, next); +} diff --git a/backend/src/middleware/ip.ts b/backend/src/middleware/ip.ts new file mode 100644 index 0000000..15f59a7 --- /dev/null +++ b/backend/src/middleware/ip.ts @@ -0,0 +1,40 @@ +import { Request, Response, NextFunction } from "express"; +import { createHmac } from "crypto"; +import { config } from "../config.js"; + +declare global { + namespace Express { + interface Request { + ipHash?: string; + } + } +} + +/** + * Resolve client IP: X-Forwarded-For (first hop) when TRUST_PROXY, else socket remoteAddress. + */ +function getClientIp(req: Request): string { + if (config.trustProxy) { + const forwarded = req.headers["x-forwarded-for"]; + if (forwarded) { + const first = typeof forwarded === "string" ? forwarded.split(",")[0] : forwarded[0]; + const ip = first?.trim(); + if (ip) return ip; + } + } + const addr = req.socket?.remoteAddress; + return addr ?? "0.0.0.0"; +} + +/** + * HMAC-SHA256(ip, HMAC_IP_SECRET) as hex. + */ +function hmacIp(ip: string): string { + return createHmac("sha256", config.hmacIpSecret).update(ip).digest("hex"); +} + +export function ipHashMiddleware(req: Request, _res: Response, next: NextFunction): void { + const ip = getClientIp(req); + req.ipHash = hmacIp(ip); + next(); +} diff --git a/backend/src/middleware/nip98.ts b/backend/src/middleware/nip98.ts new file mode 100644 index 0000000..8e79583 --- /dev/null +++ b/backend/src/middleware/nip98.ts @@ -0,0 +1,153 @@ +import { Request, Response, NextFunction } from "express"; +import { verifyEvent, getEventHash } from "nostr-tools"; +import { config } from "../config.js"; +import { getDb } from "../db/index.js"; + +const AUTH_SCHEME = "Nostr "; + +export interface NostrAuthPayload { + pubkey: string; + eventId: string; +} + +declare global { + namespace Express { + interface Request { + nostr?: NostrAuthPayload; + } + } +} + +function getTag(event: { tags: string[][] }, name: string): string | null { + const row = event.tags.find((t) => t[0] === name); + return row && row[1] ? row[1] : null; +} + +export async function nip98Auth(req: Request, res: Response, next: NextFunction): Promise { + const auth = req.headers.authorization; + if (!auth || !auth.startsWith(AUTH_SCHEME)) { + res.status(401).json({ + code: "invalid_nip98", + message: "Missing or invalid Authorization header. Use NIP-98 Nostr scheme.", + }); + return; + } + + const base64 = auth.slice(AUTH_SCHEME.length).trim(); + let event: { id: string; pubkey: string; kind: number; created_at: number; tags: string[][]; content: string; sig: string }; + try { + const decoded = Buffer.from(base64, "base64").toString("utf-8"); + const parsed = JSON.parse(decoded) as Record; + event = { + id: String(parsed.id ?? ""), + pubkey: String(parsed.pubkey ?? ""), + kind: Number(parsed.kind), + created_at: Number(parsed.created_at), + tags: Array.isArray(parsed.tags) ? (parsed.tags as string[][]) : [], + content: typeof parsed.content === "string" ? parsed.content : "", + sig: String(parsed.sig ?? ""), + }; + } catch { + res.status(401).json({ + code: "invalid_nip98", + message: "Invalid NIP-98 payload: not valid base64 or JSON.", + }); + return; + } + + if (event.kind !== 27235) { + res.status(401).json({ + code: "invalid_nip98", + message: "NIP-98 event kind must be 27235.", + }); + return; + } + + const now = Math.floor(Date.now() / 1000); + if (Math.abs(event.created_at - now) > config.nip98MaxSkewSeconds) { + res.status(401).json({ + code: "invalid_nip98", + message: "NIP-98 event timestamp is outside allowed window.", + }); + return; + } + + const u = getTag(event, "u"); + const method = getTag(event, "method"); + if (!u || !method) { + res.status(401).json({ + code: "invalid_nip98", + message: "NIP-98 event must include 'u' and 'method' tags.", + }); + return; + } + + // Reconstruct absolute URL (protocol + host + path + query) + const proto = req.headers["x-forwarded-proto"] ?? (req.socket as { encrypted?: boolean }).encrypted ? "https" : "http"; + const host = req.headers["x-forwarded-host"] ?? req.headers.host ?? ""; + const path = req.originalUrl ?? req.url; + const absoluteUrl = `${proto}://${host}${path}`; + if (u !== absoluteUrl) { + res.status(401).json({ + code: "invalid_nip98", + message: "NIP-98 'u' tag does not match request URL.", + }); + return; + } + + if (method.toUpperCase() !== req.method) { + res.status(401).json({ + code: "invalid_nip98", + message: "NIP-98 'method' tag does not match request method.", + }); + return; + } + + const computedId = getEventHash({ + kind: event.kind, + pubkey: event.pubkey, + created_at: event.created_at, + tags: event.tags, + content: event.content, + sig: event.sig, + } as Parameters[0]); + if (computedId !== event.id) { + res.status(401).json({ + code: "invalid_nip98", + message: "NIP-98 event id does not match computed hash.", + }); + return; + } + + const valid = verifyEvent({ + id: event.id, + kind: event.kind, + pubkey: event.pubkey, + created_at: event.created_at, + tags: event.tags, + content: event.content, + sig: event.sig, + } as Parameters[0]); + if (!valid) { + res.status(401).json({ + code: "invalid_nip98", + message: "NIP-98 signature verification failed.", + }); + return; + } + + const db = getDb(); + const hasNonce = await db.hasNonce(event.id); + if (hasNonce) { + res.status(401).json({ + code: "invalid_nip98", + message: "NIP-98 nonce already used (replay).", + }); + return; + } + + const expiresAt = now + config.nonceTtlSeconds; + await db.setNonce(event.id, expiresAt); + req.nostr = { pubkey: event.pubkey, eventId: event.id }; + next(); +} diff --git a/backend/src/routes/auth.ts b/backend/src/routes/auth.ts new file mode 100644 index 0000000..64b2072 --- /dev/null +++ b/backend/src/routes/auth.ts @@ -0,0 +1,29 @@ +import { Router, Request, Response } from "express"; +import { nip98Auth } from "../middleware/nip98.js"; +import { signJwt, verifyJwt } from "../auth/jwt.js"; + +const router = Router(); + +/** Sign in with NIP-98 once; returns a JWT for subsequent requests. */ +router.post("/login", nip98Auth, (req: Request, res: Response) => { + const pubkey = req.nostr!.pubkey; + const token = signJwt(pubkey); + res.json({ token, pubkey }); +}); + +/** Return current user from JWT (Bearer only). Used to restore session. */ +router.get("/me", (req: Request, res: Response) => { + const auth = req.headers.authorization; + if (!auth?.startsWith("Bearer ")) { + res.status(401).json({ code: "unauthorized", message: "Bearer token required." }); + return; + } + const payload = verifyJwt(auth.slice(7).trim()); + if (!payload) { + res.status(401).json({ code: "invalid_token", message: "Invalid or expired token." }); + return; + } + res.json({ pubkey: payload.pubkey }); +}); + +export default router; diff --git a/backend/src/routes/claim.ts b/backend/src/routes/claim.ts new file mode 100644 index 0000000..afbbed5 --- /dev/null +++ b/backend/src/routes/claim.ts @@ -0,0 +1,159 @@ +import { Router, Request, Response } from "express"; +import { createHmac } from "crypto"; +import { config } from "../config.js"; +import { getDb } from "../db/index.js"; +import { checkEligibility } from "../services/eligibility.js"; +import { createQuote } from "../services/quote.js"; +import { payToLightningAddress } from "../services/lnbits.js"; +import { authOrNip98 } from "../middleware/auth.js"; +import { ipHashMiddleware } from "../middleware/ip.js"; + +const router = Router(); + +router.use(ipHashMiddleware); + +function hashDestination(lightningAddress: string): string { + return createHmac("sha256", config.hmacIpSecret).update(lightningAddress).digest("hex"); +} + +function parseLightningAddress(body: unknown): string | null { + if (body && typeof body === "object" && "lightning_address" in body && typeof (body as { lightning_address: unknown }).lightning_address === "string") { + const v = (body as { lightning_address: string }).lightning_address.trim(); + if (/^[^@]+@[^@]+$/.test(v)) return v; + } + return null; +} + +router.post("/quote", authOrNip98, async (req: Request, res: Response) => { + const pubkey = req.nostr!.pubkey; + const ipHash = req.ipHash!; + const lightningAddress = parseLightningAddress(req.body); + if (!lightningAddress) { + res.status(400).json({ + code: "invalid_lightning_address", + message: "Valid lightning_address (user@domain) is required.", + }); + return; + } + + const eligibility = await checkEligibility(pubkey, ipHash); + if (!eligibility.eligible) { + res.status(403).json({ + code: eligibility.denialCode, + message: eligibility.denialMessage, + next_eligible_at: eligibility.nextEligibleAt, + }); + return; + } + + const quote = await createQuote(pubkey, lightningAddress); + if (!quote) { + res.status(403).json({ + code: "daily_budget_exceeded", + message: "Daily budget reached. Try again tomorrow.", + }); + return; + } + + res.json({ + quote_id: quote.quoteId, + payout_sats: quote.payoutSats, + expires_at: quote.expiresAt, + }); +}); + +router.post("/confirm", authOrNip98, async (req: Request, res: Response) => { + const pubkey = req.nostr!.pubkey; + const ipHash = req.ipHash!; + const quoteId = typeof req.body?.quote_id === "string" ? req.body.quote_id.trim() : null; + if (!quoteId) { + res.status(400).json({ + code: "invalid_request", + message: "quote_id is required.", + }); + return; + } + + const db = getDb(); + const quote = await db.getQuote(quoteId); + if (!quote) { + res.status(404).json({ + code: "quote_expired", + message: "Quote not found or expired.", + }); + return; + } + if (quote.pubkey !== pubkey) { + res.status(403).json({ + code: "invalid_nip98", + message: "Quote does not belong to this pubkey.", + }); + return; + } + if (quote.status !== "active") { + res.status(200).json({ + success: true, + already_consumed: true, + message: "This quote was already used.", + payout_sats: quote.payout_sats, + next_eligible_at: undefined, + }); + return; + } + const now = Math.floor(Date.now() / 1000); + if (quote.expires_at < now) { + res.status(400).json({ + code: "quote_expired", + message: "Quote has expired.", + }); + return; + } + + const lightningAddress = quote.lightning_address; + if (!lightningAddress) { + res.status(400).json({ + code: "invalid_lightning_address", + message: "Quote has no payout address.", + }); + return; + } + + const claimId = await db.createClaim({ + pubkey: quote.pubkey, + claimed_at: now, + payout_sats: quote.payout_sats, + ip_hash: ipHash, + payout_destination_hash: hashDestination(lightningAddress), + status: "pending", + lnbits_payment_hash: null, + error_message: null, + }); + + try { + const { paymentHash } = await payToLightningAddress(lightningAddress, quote.payout_sats); + await db.updateClaimStatus(claimId, "paid", paymentHash); + await db.consumeQuote(quoteId); + const cooldownEnd = now + config.cooldownDays * 86400; + const ipSince = now - config.ipCooldownDays * 86400; + const ipCount = await db.getClaimCountForIpSince(ipHash, ipSince); + await db.upsertIpLimit(ipHash, now, ipCount); + res.json({ + success: true, + payout_sats: quote.payout_sats, + next_eligible_at: cooldownEnd, + }); + } catch (err) { + const message = err instanceof Error ? err.message : "Payment failed"; + const stack = err instanceof Error ? err.stack : undefined; + console.error("[claim/confirm] Lightning payment failed:", message); + if (stack) console.error("[claim/confirm] Stack:", stack); + await db.updateClaimStatus(claimId, "failed", undefined, message); + res.status(502).json({ + code: "payout_failed", + message: "Lightning payment failed. Your cooldown was not applied.", + details: message, + }); + } +}); + +export default router; diff --git a/backend/src/routes/public.ts b/backend/src/routes/public.ts new file mode 100644 index 0000000..2a3a700 --- /dev/null +++ b/backend/src/routes/public.ts @@ -0,0 +1,134 @@ +import { Router, Request, Response } from "express"; +import { config } from "../config.js"; +import { getDb } from "../db/index.js"; +import { getWalletBalanceSats } from "../services/lnbits.js"; + +const router = Router(); + +router.get("/health", (_req: Request, res: Response) => { + res.json({ status: "ok" }); +}); + +router.get("/config", (_req: Request, res: Response) => { + res.json({ + faucetEnabled: config.faucetEnabled, + emergencyStop: config.emergencyStop, + cooldownDays: config.cooldownDays, + minAccountAgeDays: config.minAccountAgeDays, + minActivityScore: config.minActivityScore, + faucetMinSats: config.faucetMinSats, + faucetMaxSats: config.faucetMaxSats, + }); +}); + +router.get("/stats", async (_req: Request, res: Response) => { + try { + const db = getDb(); + const [balance, totalPaid, totalClaims, claims24h, recent, recentDeposits] = await Promise.all([ + getWalletBalanceSats().catch(() => 0), + db.getTotalPaidSats(), + db.getTotalClaimsCount(), + db.getClaimsCountSince(Math.floor(Date.now() / 1000) - 86400), + db.getRecentPayouts(20), + db.getRecentDeposits(20), + ]); + res.json({ + balanceSats: balance, + totalPaidSats: totalPaid, + totalClaims, + claimsLast24h: claims24h, + dailyBudgetSats: config.dailyBudgetSats, + recentPayouts: recent, + recentDeposits, + }); + } catch (e) { + res.status(500).json({ + code: "internal_error", + message: "Failed to load stats", + }); + } +}); + +router.get("/deposit", (_req: Request, res: Response) => { + res.json({ + lightningAddress: config.depositLightningAddress, + lnurlp: config.depositLnurlp, + }); +}); + +router.post("/deposit/redeem-cashu", async (req: Request, res: Response) => { + const token = typeof req.body?.token === "string" ? req.body.token.trim() : null; + if (!token || !token.toLowerCase().startsWith("cashu")) { + res.status(400).json({ + success: false, + error: "Valid Cashu token (cashuA... or cashuB...) is required.", + }); + return; + } + const lightningAddress = config.depositLightningAddress; + if (!lightningAddress || !/^[^@]+@[^@]+$/.test(lightningAddress)) { + res.status(503).json({ + success: false, + error: "Faucet deposit Lightning address is not configured.", + }); + return; + } + const redeemUrl = `${config.cashuRedeemApiUrl}/api/redeem`; + try { + const redeemRes = await fetch(redeemUrl, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ token, lightningAddress }), + }); + const data = (await redeemRes.json().catch(() => ({}))) as { + success?: boolean; + error?: string; + errorType?: string; + paid?: boolean; + amount?: number; + invoiceAmount?: number; + to?: string; + netAmount?: number; + message?: string; + }; + if (!redeemRes.ok) { + const status = redeemRes.status >= 500 ? 502 : redeemRes.status; + res.status(status).json({ + success: false, + error: data.error ?? `Redeem failed: ${redeemRes.status}`, + ...(data.errorType && { errorType: data.errorType }), + }); + return; + } + if (!data.success) { + res.status(400).json({ + success: false, + error: data.error ?? "Redeem failed", + ...(data.errorType && { errorType: data.errorType }), + }); + return; + } + const amountSats = typeof data.amount === "number" && data.amount > 0 ? data.amount : data.netAmount; + if (typeof amountSats === "number" && amountSats > 0) { + getDb().insertDeposit(amountSats, "cashu").catch((err) => console.error("[deposit] record deposit", err)); + } + res.json({ + success: true, + paid: data.paid, + amount: data.amount, + invoiceAmount: data.invoiceAmount, + netAmount: data.netAmount, + to: data.to ?? lightningAddress, + message: data.message, + }); + } catch (e) { + const message = e instanceof Error ? e.message : "Redeem request failed"; + console.error("[deposit/redeem-cashu]", message); + res.status(502).json({ + success: false, + error: message, + }); + } +}); + +export default router; diff --git a/backend/src/routes/user.ts b/backend/src/routes/user.ts new file mode 100644 index 0000000..4eb717d --- /dev/null +++ b/backend/src/routes/user.ts @@ -0,0 +1,30 @@ +import { Router, Request, Response } from "express"; +import { getDb } from "../db/index.js"; +import { fetchAndScorePubkey } from "../services/nostr.js"; +import { authOrNip98 } from "../middleware/auth.js"; + +const router = Router(); + +/** + * Refresh Nostr profile (kind 0) and return cached lightning_address and name. + * Pre-fills the frontend and stores in DB. + */ +router.post("/refresh-profile", authOrNip98, async (req: Request, res: Response) => { + const pubkey = req.nostr!.pubkey; + try { + await fetchAndScorePubkey(pubkey, true); + const db = getDb(); + const user = await db.getUser(pubkey); + res.json({ + lightning_address: user?.lightning_address ?? null, + name: user?.name ?? null, + }); + } catch (e) { + res.status(500).json({ + code: "profile_fetch_failed", + message: e instanceof Error ? e.message : "Failed to fetch profile", + }); + } +}); + +export default router; diff --git a/backend/src/services/eligibility.ts b/backend/src/services/eligibility.ts new file mode 100644 index 0000000..2c939db --- /dev/null +++ b/backend/src/services/eligibility.ts @@ -0,0 +1,126 @@ +import { config } from "../config.js"; +import { getDb } from "../db/index.js"; +import { getWalletBalanceSats } from "./lnbits.js"; +import { fetchAndScorePubkey } from "./nostr.js"; + +export type DenialCode = + | "faucet_disabled" + | "emergency_stop" + | "insufficient_balance" + | "daily_budget_exceeded" + | "cooldown_pubkey" + | "cooldown_ip" + | "account_too_new" + | "low_activity" + | "invalid_nip98" + | "invalid_lightning_address" + | "quote_expired" + | "payout_failed"; + +export interface EligibilityResult { + eligible: boolean; + denialCode?: DenialCode; + denialMessage?: string; + nextEligibleAt?: number; +} + +const SECONDS_PER_DAY = 86400; + +export async function checkEligibility(pubkey: string, ipHash: string): Promise { + if (config.emergencyStop) { + return { + eligible: false, + denialCode: "emergency_stop", + denialMessage: "The faucet is temporarily in maintenance. Please try again later.", + }; + } + + if (!config.faucetEnabled) { + return { + eligible: false, + denialCode: "faucet_disabled", + denialMessage: "The faucet is currently disabled.", + }; + } + + let balanceSats: number; + try { + balanceSats = await getWalletBalanceSats(); + } catch { + return { + eligible: false, + denialCode: "insufficient_balance", + denialMessage: "Unable to check faucet balance. Please try again later.", + }; + } + + if (balanceSats < config.faucetMinSats) { + return { + eligible: false, + denialCode: "insufficient_balance", + denialMessage: balanceSats === 0 + ? "The faucet pool is empty. Donations welcome!" + : `The faucet pool is too low to pay out (${balanceSats} sats). Donations welcome!`, + }; + } + + const db = getDb(); + + const lastPubkeyClaim = await db.getLastSuccessfulClaimByPubkey(pubkey); + const cooldownEnd = lastPubkeyClaim + ? lastPubkeyClaim.claimed_at + config.cooldownDays * SECONDS_PER_DAY + : 0; + const now = Math.floor(Date.now() / 1000); + if (cooldownEnd > now) { + return { + eligible: false, + denialCode: "cooldown_pubkey", + denialMessage: "You have already claimed recently.", + nextEligibleAt: cooldownEnd, + }; + } + + const ipSince = now - config.ipCooldownDays * SECONDS_PER_DAY; + const ipClaimCount = await db.getClaimCountForIpSince(ipHash, ipSince); + if (ipClaimCount >= config.maxClaimsPerIpPerPeriod) { + const lastIpClaim = await db.getLastClaimByIpHash(ipHash); + const ipNextAt = lastIpClaim ? lastIpClaim.claimed_at + config.ipCooldownDays * SECONDS_PER_DAY : 0; + return { + eligible: false, + denialCode: "cooldown_ip", + denialMessage: "This IP has reached the claim limit for this period.", + nextEligibleAt: ipNextAt, + }; + } + + const profile = await fetchAndScorePubkey(pubkey); + const minAgeSec = config.minAccountAgeDays * SECONDS_PER_DAY; + const cutoff = now - minAgeSec; + if (profile.nostrFirstSeenAt === null || profile.nostrFirstSeenAt > cutoff) { + return { + eligible: false, + denialCode: "account_too_new", + denialMessage: `Your Nostr account must be at least ${config.minAccountAgeDays} days old.`, + }; + } + + if (profile.activityScore < config.minActivityScore) { + return { + eligible: false, + denialCode: "low_activity", + denialMessage: `Your account does not meet the minimum activity score (${config.minActivityScore}). Be more active on Nostr and try again.`, + }; + } + + const since24h = now - 86400; + const claims24h = await db.getClaimsCountSince(since24h); + if (claims24h >= config.maxClaimsPerDay) { + return { + eligible: false, + denialCode: "daily_budget_exceeded", + denialMessage: "Daily claim limit reached. Try again tomorrow.", + }; + } + + return { eligible: true }; +} diff --git a/backend/src/services/lnbits.ts b/backend/src/services/lnbits.ts new file mode 100644 index 0000000..c0d4c61 --- /dev/null +++ b/backend/src/services/lnbits.ts @@ -0,0 +1,190 @@ +import { config } from "../config.js"; + +const base = config.lnbitsBaseUrl; +const adminKey = config.lnbitsAdminKey; +const walletId = config.lnbitsWalletId; + +export async function getWalletBalanceSats(): Promise { + const res = await fetch(`${base}/api/v1/wallet`, { + headers: { "X-Api-Key": adminKey }, + }); + if (!res.ok) { + const text = await res.text(); + throw new Error(`LNbits wallet fetch failed: ${res.status} ${text}`); + } + const data = (await res.json()) as { balance?: number }; + return Math.floor((data.balance ?? 0) / 1000); +} + +/** + * Pay to a Lightning address via LNURL. + * 1. Resolve Lightning address to LNURL (GET https://domain/.well-known/lnurlp/user) + * 2. Call callback with amount in millisats + */ +export async function payToLightningAddress( + lightningAddress: string, + sats: number +): Promise<{ paymentHash: string }> { + const [user, domain] = lightningAddress.split("@"); + if (!user || !domain) { + console.error("[lnbits] Invalid Lightning address format:", lightningAddress); + throw new Error("Invalid Lightning address format"); + } + + const lnurlpUrl = `https://${domain}/.well-known/lnurlp/${user}`; + const lnurlRes = await fetch(lnurlpUrl); + if (!lnurlRes.ok) { + const text = await lnurlRes.text(); + console.error("[lnbits] LNURLp resolution failed:", { + lightningAddress, + lnurlpUrl, + status: lnurlRes.status, + statusText: lnurlRes.statusText, + body: text.slice(0, 500), + }); + throw new Error(`Could not resolve Lightning address: ${lnurlRes.status} ${text.slice(0, 200)}`); + } + const lnurlData = (await lnurlRes.json()) as { callback?: string; minSendable?: number; maxSendable?: number }; + const callback = lnurlData.callback; + if (!callback) { + console.error("[lnbits] No callback in LNURLp response:", { lightningAddress, lnurlpUrl, lnurlData }); + throw new Error("No callback in LNURLp"); + } + + const millisats = sats * 1000; + const separator = callback.includes("?") ? "&" : "?"; + const payReqUrl = `${callback}${separator}amount=${millisats}`; + const payRes = await fetch(payReqUrl); + const payBody = await payRes.text(); + if (!payRes.ok) { + let parsed: unknown; + try { + parsed = JSON.parse(payBody); + } catch { + parsed = payBody; + } + console.error("[lnbits] LNURL pay request failed:", { + lightningAddress, + sats, + millisats, + callbackHost: new URL(callback).host, + status: payRes.status, + statusText: payRes.statusText, + body: parsed, + }); + const detail = typeof parsed === "object" && parsed !== null && "reason" in parsed + ? (parsed as { reason?: string }).reason + : payBody.slice(0, 300); + throw new Error(`LNURL pay request failed: ${payRes.status} ${detail}`); + } + const payData = JSON.parse(payBody) as { pr?: string; reason?: string }; + const pr = payData.pr; + if (!pr) { + console.error("[lnbits] No invoice (pr) in pay response:", { lightningAddress, payData }); + throw new Error(`No invoice in pay response: ${payData.reason ?? JSON.stringify(payData).slice(0, 200)}`); + } + + const payResult = await fetch(`${base}/api/v1/payments`, { + method: "POST", + headers: { "Content-Type": "application/json", "X-Api-Key": adminKey }, + body: JSON.stringify({ out: true, bolt11: pr }), + }); + if (!payResult.ok) { + const errText = await payResult.text(); + console.error("[lnbits] LNbits bolt11 payment failed:", { + lightningAddress, + sats, + status: payResult.status, + body: errText.slice(0, 500), + }); + throw new Error(`LNbits pay failed: ${payResult.status} ${errText}`); + } + const result = (await payResult.json()) as { payment_hash?: string }; + return { paymentHash: result.payment_hash ?? "" }; +} + +/** LNbits payment list item (GET /api/v1/payments). Amount in millisatoshis; positive = incoming, negative = outgoing. */ +/** Per LNbits OpenAPI: time, created_at, updated_at are "string" format "date-time" (ISO 8601). */ +export interface LnbitsPaymentItem { + payment_hash?: string; + amount?: number; + pending?: boolean; + time?: number | string; + created_at?: number | string; + updated_at?: number | string; + timestamp?: number; + date?: number; + [key: string]: unknown; +} + +const MIN_VALID_UNIX = 1e9; + +function parsePaymentTime(raw: unknown): number { + if (raw == null) return 0; + if (typeof raw === "number") { + const ts = raw > 1e12 ? Math.floor(raw / 1000) : raw; + return ts >= MIN_VALID_UNIX ? ts : 0; + } + if (typeof raw === "string") { + const ms = Date.parse(raw); + if (Number.isNaN(ms)) return 0; + return Math.floor(ms / 1000); + } + return 0; +} + +function normalizePaymentTime(p: LnbitsPaymentItem): number { + const ts = + parsePaymentTime(p.time) || + parsePaymentTime(p.created_at) || + parsePaymentTime(p.updated_at) || + parsePaymentTime(p.timestamp) || + parsePaymentTime(p.date); + if (ts >= MIN_VALID_UNIX) return ts; + return Math.floor(Date.now() / 1000); +} + +/** + * Fetch recent payments from LNbits and return paid incoming ones (amount > 0, not pending). + * LNbits returns amount in millisatoshis; we convert to sats for storage. + */ +export async function getIncomingPaymentsFromLnbits(limit = 100): Promise< + { payment_hash: string; amount_sats: number; paid_at: number }[] +> { + const res = await fetch( + `${base}/api/v1/payments?limit=${limit}&sortby=time&direction=desc`, + { headers: { "X-Api-Key": adminKey } } + ); + if (!res.ok) { + const text = await res.text(); + throw new Error(`LNbits payments list failed: ${res.status} ${text}`); + } + const data = (await res.json()) as LnbitsPaymentItem[] | { detail?: string; payments?: LnbitsPaymentItem[] }; + let items: LnbitsPaymentItem[]; + if (Array.isArray(data)) { + items = data; + } else if (data && typeof data === "object" && Array.isArray((data as { payments?: LnbitsPaymentItem[] }).payments)) { + items = (data as { payments: LnbitsPaymentItem[] }).payments; + } else { + const detail = (data as { detail?: string })?.detail; + throw new Error(detail ?? "LNbits payments list invalid response"); + } + const incoming: { payment_hash: string; amount_sats: number; paid_at: number }[] = []; + for (const p of items) { + const hash = p.payment_hash; + const amountMsats = Number(p.amount ?? 0); + const pending = Boolean(p.pending); + const paidAt = normalizePaymentTime(p); + if (!hash || typeof hash !== "string") continue; + if (pending) continue; + if (amountMsats <= 0) continue; + const amountSats = Math.floor(amountMsats / 1000); + if (amountSats <= 0) continue; + incoming.push({ + payment_hash: hash, + amount_sats: amountSats, + paid_at: paidAt, + }); + } + return incoming; +} diff --git a/backend/src/services/nostr.ts b/backend/src/services/nostr.ts new file mode 100644 index 0000000..4c399d8 --- /dev/null +++ b/backend/src/services/nostr.ts @@ -0,0 +1,150 @@ +import { SimplePool } from "nostr-tools"; +import { config } from "../config.js"; +import { getDb } from "../db/index.js"; + +const pool = new SimplePool(); + +export interface NostrProfile { + nostrFirstSeenAt: number | null; + notesCount: number; + followingCount: number; + followersCount: number; + activityScore: number; +} + +function withTimeout(promise: Promise, ms: number): Promise { + return Promise.race([ + promise, + new Promise((_, rej) => setTimeout(() => rej(new Error("timeout")), ms)), + ]); +} + +/** + * Fetch events from relays in parallel (kinds 0, 1, 3), compute metrics, optionally cache. + * When forceRefreshProfile is true, always fetch from relays (skip cache) so kind 0 is parsed and lightning_address/name updated. + */ +export async function fetchAndScorePubkey(pubkey: string, forceRefreshProfile = false): Promise { + const db = getDb(); + const cached = await db.getUser(pubkey); + const nowSec = Math.floor(Date.now() / 1000); + const cacheHours = config.metadataCacheHours; + const cacheValidUntil = (cached?.last_metadata_fetch_at ?? 0) + cacheHours * 3600; + + if (!forceRefreshProfile && cached && cacheValidUntil > nowSec) { + return { + nostrFirstSeenAt: cached.nostr_first_seen_at, + notesCount: cached.notes_count, + followingCount: cached.following_count, + followersCount: cached.followers_count, + activityScore: cached.activity_score, + }; + } + + let events: { kind: number; created_at: number; content?: string; tags: string[][] }[] = []; + try { + const result = await withTimeout( + pool.querySync(config.nostrRelays, { kinds: [0, 1, 3], authors: [pubkey], limit: config.maxEventsFetch }), + config.relayTimeoutMs + ); + events = Array.isArray(result) ? result : []; + } catch (_) { + // Timeout or relay error: use cache if any; otherwise upsert minimal user so /refresh-profile returns a row + if (cached) { + return { + nostrFirstSeenAt: cached.nostr_first_seen_at, + notesCount: cached.notes_count, + followingCount: cached.following_count, + followersCount: cached.followers_count, + activityScore: cached.activity_score, + }; + } + const lastMetadataFetchAt = Math.floor(Date.now() / 1000); + await db.upsertUser({ + pubkey, + nostr_first_seen_at: null, + notes_count: 0, + followers_count: 0, + following_count: 0, + activity_score: 0, + last_metadata_fetch_at: lastMetadataFetchAt, + lightning_address: null, + name: null, + }); + return { + nostrFirstSeenAt: null, + notesCount: 0, + followingCount: 0, + followersCount: 0, + activityScore: 0, + }; + } + + const kind0 = events.filter((e) => e.kind === 0); + const kind1 = events.filter((e) => e.kind === 1); + const kind3 = events.filter((e) => e.kind === 3); + + const earliestCreatedAt = events.length + ? Math.min(...events.map((e) => e.created_at)) + : null; + + const lookbackSince = nowSec - config.activityLookbackDays * 86400; + const notesInLookback = kind1.filter((e) => e.created_at >= lookbackSince).length; + + let followingCount = 0; + if (kind3.length > 0) { + const contacts = kind3[0].tags?.filter((t) => t[0] === "p").length ?? 0; + followingCount = contacts; + } + + const hasMetadata = kind0.length > 0; + let score = 0; + if (hasMetadata) score += 10; + if (notesInLookback >= config.minNotesCount) score += 20; + if (followingCount >= config.minFollowingCount) score += 10; + if (0 >= config.minFollowersCount) score += 10; // followers not fetched for MVP; treat as 0 + + let lightning_address: string | null = null; + let name: string | null = null; + const lightningAddressRe = /^[^@]+@[^@]+$/; + if (kind0.length > 0 && kind0[0].content) { + try { + const meta = JSON.parse(kind0[0].content) as Record; + // NIP-19 / common: lud16 is the Lightning address (user@domain). Fallbacks for other clients. + for (const key of ["lud16", "lightning", "ln_address", "nip05"] as const) { + const v = meta[key]; + if (typeof v === "string") { + const s = v.trim(); + if (lightningAddressRe.test(s)) { + lightning_address = s; + break; + } + } + } + if (typeof meta.name === "string" && meta.name.trim()) name = meta.name.trim(); + else if (typeof meta.display_name === "string" && meta.display_name.trim()) name = meta.display_name.trim(); + } catch (_) {} + } + + const nostrFirstSeenAt = earliestCreatedAt; + const lastMetadataFetchAt = Math.floor(Date.now() / 1000); + + await db.upsertUser({ + pubkey, + nostr_first_seen_at: nostrFirstSeenAt, + notes_count: notesInLookback, + followers_count: 0, + following_count: followingCount, + activity_score: score, + last_metadata_fetch_at: lastMetadataFetchAt, + lightning_address, + name, + }); + + return { + nostrFirstSeenAt, + notesCount: notesInLookback, + followingCount, + followersCount: 0, + activityScore: score, + }; +} diff --git a/backend/src/services/quote.ts b/backend/src/services/quote.ts new file mode 100644 index 0000000..5b46068 --- /dev/null +++ b/backend/src/services/quote.ts @@ -0,0 +1,69 @@ +import { randomInt } from "crypto"; +import { v4 as uuidv4 } from "uuid"; +import { config } from "../config.js"; +import { getDb } from "../db/index.js"; +import { getWalletBalanceSats } from "./lnbits.js"; + +const QUOTE_TTL_SECONDS = 60; + +interface PayoutBucket { + sats: number; + weight: number; +} + +function getPayoutBuckets(): PayoutBucket[] { + return [ + { sats: config.payoutSmallSats, weight: config.payoutWeightSmall }, + { sats: config.payoutMediumSats, weight: config.payoutWeightMedium }, + { sats: config.payoutLargeSats, weight: config.payoutWeightLarge }, + { sats: config.payoutJackpotSats, weight: config.payoutWeightJackpot }, + ]; +} + +/** + * Weighted random selection. Returns sats amount. + */ +export function selectWeightedPayout(): number { + const buckets = getPayoutBuckets(); + const totalWeight = buckets.reduce((s, b) => s + b.weight, 0); + let r = randomInt(0, totalWeight); + for (const b of buckets) { + if (r < b.weight) return b.sats; + r -= b.weight; + } + return config.payoutSmallSats; +} + +/** + * Compute payout for this claim: weighted selection, capped by daily budget remaining. + */ +export function computePayoutForClaim(todayPaidSats: number): number { + const remaining = Math.max(0, config.dailyBudgetSats - todayPaidSats); + if (remaining < config.faucetMinSats) return 0; + const selected = selectWeightedPayout(); + return Math.min(selected, remaining, config.faucetMaxSats); +} + +export interface CreateQuoteResult { + quoteId: string; + payoutSats: number; + expiresAt: number; +} + +export async function createQuote(pubkey: string, lightningAddress: string): Promise { + const db = getDb(); + const now = Math.floor(Date.now() / 1000); + const dayStart = now - (now % 86400); + const todayPaid = await db.getPaidSatsSince(dayStart); + let payout = computePayoutForClaim(todayPaid); + if (payout <= 0) return null; + + const walletBalance = await getWalletBalanceSats(); + payout = Math.min(payout, Math.max(0, walletBalance)); + if (payout < config.faucetMinSats) return null; + + const quoteId = uuidv4(); + const expiresAt = now + QUOTE_TTL_SECONDS; + await db.createQuote(quoteId, pubkey, payout, lightningAddress, expiresAt); + return { quoteId, payoutSats: payout, expiresAt }; +} diff --git a/backend/src/services/syncLnbitsDeposits.ts b/backend/src/services/syncLnbitsDeposits.ts new file mode 100644 index 0000000..95367e8 --- /dev/null +++ b/backend/src/services/syncLnbitsDeposits.ts @@ -0,0 +1,44 @@ +import { getDb } from "../db/index.js"; +import { getIncomingPaymentsFromLnbits } from "./lnbits.js"; + +const SYNC_INTERVAL_MS = 2 * 60 * 1000; + +const MIN_VALID_UNIX = 1e9; + +export async function syncLnbitsDeposits(): Promise { + const db = getDb(); + try { + const payments = await getIncomingPaymentsFromLnbits(100); + let added = 0; + let updated = 0; + for (const p of payments) { + const exists = await db.hasDepositWithPaymentHash(p.payment_hash); + if (!exists) { + await db.insertDeposit( + p.amount_sats, + "lightning", + p.payment_hash, + p.paid_at + ); + added++; + } else if (p.paid_at >= MIN_VALID_UNIX) { + const didUpdate = await db.updateDepositCreatedAtIfMissing(p.payment_hash, p.paid_at); + if (didUpdate) updated++; + } + } + if (added > 0) { + console.log(`[sync] LNbits deposits: ${added} new incoming payment(s) synced`); + } + if (updated > 0) { + console.log(`[sync] LNbits deposits: ${updated} date(s) backfilled`); + } + } catch (e) { + const msg = e instanceof Error ? e.message : String(e); + console.error("[sync] LNbits deposits failed:", msg); + } +} + +export function startLnbitsDepositSync(): void { + syncLnbitsDeposits(); + setInterval(syncLnbitsDeposits, SYNC_INTERVAL_MS); +} diff --git a/backend/tsconfig.json b/backend/tsconfig.json new file mode 100644 index 0000000..7bad8a4 --- /dev/null +++ b/backend/tsconfig.json @@ -0,0 +1,17 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "NodeNext", + "moduleResolution": "NodeNext", + "outDir": "dist", + "rootDir": "src", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "declaration": true, + "declarationMap": true, + "sourceMap": true + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist"] +} diff --git a/context/Frontend_overview.md b/context/Frontend_overview.md new file mode 100644 index 0000000..65cb67b --- /dev/null +++ b/context/Frontend_overview.md @@ -0,0 +1,267 @@ +# frontend_overview.md + +## 1. Purpose + +This document defines the complete frontend architecture, user experience, and interaction model for the Sats Faucet application. + +The frontend must: + +* Provide a simple, clean, and trustworthy interface. +* Integrate Nostr login (NIP-07 or external signer). +* Guide users through eligibility check and claim confirmation. +* Display transparent faucet statistics and funding information. +* Clearly communicate rules, cooldowns, and denial reasons. + +The frontend must never expose secrets or LNbits keys. + +--- + +## 2. Tech Stack Requirements + +Recommended: + +* Framework: React (Next.js preferred) or similar SPA framework +* Styling: TailwindCSS or clean minimal CSS system +* QR generation library for Lightning deposit QR +* Nostr integration via NIP-07 browser extension (window.nostr) + +Frontend must be deployable as static site or server-rendered app. + +--- + +## 3. Global App Structure + +### 3.1 Pages + +1. Home Page (/) +2. Claim Modal or Claim Section +3. Transparency / Stats Section +4. Deposit Section +5. Optional: About / Rules page + +Navigation should be minimal and focused. + +--- + +## 4. Nostr Authentication + +### 4.1 Connect Flow + +* User clicks "Connect Nostr". +* Frontend requests pubkey via NIP-07. +* Pubkey stored in memory (not localStorage unless necessary). +* Show truncated pubkey (npub format preferred). + +### 4.2 NIP-98 Signing + +For protected API calls: + +* Build request payload (method + URL + timestamp + nonce). +* Request signature via NIP-07. +* Attach NIP-98 header to backend request. + +Frontend must: + +* Generate secure nonce. +* Handle signature errors gracefully. + +--- + +## 5. Home Page Layout + +### 5.1 Hero Section + +* Title: Sats Faucet +* Short description of rules +* Claim button + +### 5.2 Live Stats Section + +Display: + +* Current pool balance +* Total sats paid +* Total claims +* Claims in last 24h +* Daily budget progress bar + +Stats pulled from GET /stats. + +Must auto-refresh every 30–60 seconds. + +--- + +## 6. Claim Flow UI + +### 6.1 Step 1: Connect + +If not connected: + +* Disable claim button. +* Prompt user to connect Nostr. + +### 6.2 Step 2: Enter Lightning Address + +Input field: + +* Validate basic format (user@domain). +* Do not over-validate client-side. + +### 6.3 Step 3: Quote + +On "Check Eligibility": + +* Send POST /claim/quote with NIP-98. + +Possible responses: + +Eligible: + +* Show payout amount. +* Show Confirm button. + +Not eligible: + +* Show clear message from backend. +* If cooldown, show next eligible date. +* If account too new, show required age. + +### 6.4 Step 4: Confirm + +On confirm: + +* Call POST /claim/confirm. +* Show loading state. +* On success: + + * Show payout amount. + * Show next eligible time. +* On failure: + + * Show clear error. + +Must prevent double-click submission. + +--- + +## 7. Deposit Section + +Display: + +* Lightning Address (copyable). +* QR code of LNURL. +* Optional: "Create invoice" button. + +Copy buttons required for: + +* Lightning address +* LNURL + +Deposit section must feel transparent and trustworthy. + +--- + +## 8. Transparency Section + +Display: + +* Recent payouts (anonymized pubkey prefix). +* Payout amount. +* Timestamp. + +Optional: + +* Distribution breakdown (small/medium/large payouts). + +--- + +## 9. Error Handling UX + +Frontend must handle: + +* Network failures. +* Signature rejection. +* Expired quote. +* Payout failure. + +All errors must be displayed in a clean alert component. + +Do not expose internal error stack traces. + +--- + +## 10. State Management + +Minimal state required: + +* pubkey +* lightning_address +* eligibility result +* quote_id +* payout_sats +* next_eligible_at + +Use React state or lightweight state manager. + +No need for complex global store. + +--- + +## 11. Security Requirements + +Frontend must: + +* Never store secrets. +* Never expose LNbits keys. +* Never trust client-side eligibility logic. +* Rely entirely on backend for final validation. + +If Turnstile or CAPTCHA enabled: + +* Include widget only when backend indicates required. + +--- + +## 12. Design Principles + +* Minimal, modern UI. +* Clear visual feedback. +* No clutter. +* Show transparency clearly. +* Make it feel fair and legitimate. + +Recommended style: + +* Dark mode default. +* Bitcoin-inspired accent color. +* Clean typography. + +--- + +## 13. Performance Requirements + +* Fast initial load. +* Avoid blocking Nostr relay calls on frontend. +* All relay interaction handled by backend. + +--- + +## 14. Accessibility + +* Buttons must have disabled states. +* Inputs labeled clearly. +* QR accessible with copy option. + +--- + +## 15. Completion Criteria + +Frontend is complete when: + +* User can connect via NIP-07. +* Eligibility check works and displays correct messages. +* Confirm claim triggers payout. +* Deposit Lightning address and QR visible. +* Live stats update correctly. +* Cooldown and denial reasons clearly displayed. +* No sensitive data exposed in frontend code. diff --git a/context/backend_overview.md b/context/backend_overview.md new file mode 100644 index 0000000..82457cb --- /dev/null +++ b/context/backend_overview.md @@ -0,0 +1,359 @@ +# backend_overview.md + +## 1. Purpose + +This document defines the complete backend architecture and behavior for the Sats Faucet application. + +The backend is responsible for: + +* Verifying NIP-98 signed authentication. +* Enforcing all eligibility rules. +* Fetching and caching Nostr account data. +* Enforcing cooldowns and anti-abuse constraints. +* Generating and locking entropy payout quotes. +* Executing Lightning payouts via LNbits. +* Maintaining transparency statistics. +* Providing operational safety controls. + +This backend must be production-safe, abuse-resistant, and configurable entirely via environment variables. + +--- + +## 2. Tech Stack Requirements + +Recommended stack: + +* Language: Go, TypeScript (Node/Express), or Python (FastAPI) +* Database: PostgreSQL +* Optional: Redis (rate limiting + nonce replay protection) +* LNbits for Lightning payments +* Reverse proxy with TLS (nginx or Caddy) + +The backend must be stateless except for database persistence. + +--- + +## 3. Environment Variables + +All logic must be driven via .env configuration. + +Key categories: + +### 3.1 Security + +* JWT_SECRET +* HMAC_IP_SECRET +* NIP98_MAX_SKEW_SECONDS +* NONCE_TTL_SECONDS +* TRUST_PROXY +* ALLOWED_ORIGINS + +### 3.2 Faucet Economics + +* FAUCET_ENABLED +* EMERGENCY_STOP +* FAUCET_MIN_SATS +* FAUCET_MAX_SATS +* PAYOUT_WEIGHT_SMALL +* PAYOUT_WEIGHT_MEDIUM +* PAYOUT_WEIGHT_LARGE +* PAYOUT_WEIGHT_JACKPOT +* PAYOUT_SMALL_SATS +* PAYOUT_MEDIUM_SATS +* PAYOUT_LARGE_SATS +* PAYOUT_JACKPOT_SATS +* DAILY_BUDGET_SATS +* MAX_CLAIMS_PER_DAY +* MIN_WALLET_BALANCE_SATS + +### 3.3 Eligibility + +* MIN_ACCOUNT_AGE_DAYS +* MIN_ACTIVITY_SCORE +* MIN_NOTES_COUNT +* MIN_FOLLOWING_COUNT +* MIN_FOLLOWERS_COUNT +* ACTIVITY_LOOKBACK_DAYS + +### 3.4 Cooldowns + +* COOLDOWN_DAYS +* IP_COOLDOWN_DAYS +* MAX_CLAIMS_PER_IP_PER_PERIOD + +### 3.5 Nostr + +* NOSTR_RELAYS +* RELAY_TIMEOUT_MS +* MAX_EVENTS_FETCH +* METADATA_CACHE_HOURS + +### 3.6 LNbits + +* LNBITS_BASE_URL +* LNBITS_ADMIN_KEY +* LNBITS_WALLET_ID +* DEPOSIT_LIGHTNING_ADDRESS +* DEPOSIT_LNURLP + +--- + +## 4. Database Schema + +### 4.1 users + +* pubkey (PK) +* nostr_first_seen_at +* notes_count +* followers_count +* following_count +* activity_score +* last_metadata_fetch_at +* created_at +* updated_at + +### 4.2 claims + +* id (PK) +* pubkey (FK users.pubkey) +* claimed_at +* payout_sats +* ip_hash +* payout_destination_hash +* status (pending, paid, failed) +* lnbits_payment_hash +* error_message + +### 4.3 ip_limits + +* ip_hash (PK) +* last_claimed_at +* claim_count_period + +### 4.4 quotes + +* quote_id (PK) +* pubkey +* payout_sats +* created_at +* expires_at +* status (active, consumed, expired) + +### 4.5 daily_stats (optional) + +* date (PK) +* total_paid_sats +* total_claims +* unique_pubkeys + +All IP addresses must be stored as HMAC(IP, HMAC_IP_SECRET). + +--- + +## 5. NIP-98 Authentication + +Every protected endpoint must: + +1. Extract NIP-98 header or payload. +2. Verify signature against pubkey. +3. Verify HTTP method and URL match signed payload. +4. Verify timestamp within allowed skew. +5. Verify nonce not previously used. +6. Reject if invalid. + +Nonces must be stored in Redis or DB for NONCE_TTL_SECONDS. + +--- + +## 6. IP Resolution + +If TRUST_PROXY=true: + +* Read first valid IP from X-Forwarded-For. + Else: +* Use request remote address. + +Then: + +* Hash IP using HMAC_IP_SECRET. +* Never store raw IP. + +--- + +## 7. Eligibility Engine + +Eligibility flow: + +1. Check FAUCET_ENABLED. +2. Check EMERGENCY_STOP. +3. Check LNbits wallet balance >= MIN_WALLET_BALANCE_SATS. +4. Check pubkey cooldown. +5. Check IP cooldown. +6. Fetch or load cached Nostr profile. +7. Compute account age. +8. Compute activity score. +9. Compare with thresholds. + +Return structured result with denial code if failed. + +--- + +## 8. Nostr Data Fetching + +For a given pubkey: + +1. Query relays in parallel. +2. Fetch: + + * kind 0 (metadata) + * kind 1 (notes) + * kind 3 (contacts) +3. Compute: + + * earliest created_at + * notes count in ACTIVITY_LOOKBACK_DAYS + * following count + +Cache results for METADATA_CACHE_HOURS. + +If no events found: + +* Deny as account_too_new. + +--- + +## 9. Activity Scoring + +Example scoring logic: + +* Has metadata: +10 +* Notes >= MIN_NOTES_COUNT: +20 +* Following >= MIN_FOLLOWING_COUNT: +10 +* Followers >= MIN_FOLLOWERS_COUNT: +10 + +Score must be deterministic and logged. + +--- + +## 10. Entropy Payout System + +Weighted random selection: + +1. Build array based on configured weights. +2. Generate secure random number. +3. Select payout bucket. + +Before finalizing quote: + +* Check daily spend. +* Check MAX_CLAIMS_PER_DAY. + +If budget exceeded: + +* Either deny or downgrade payout to FAUCET_MIN_SATS. + +--- + +## 11. Claim Flow + +### 11.1 POST /claim/quote + +Input: + +* lightning_address + +Steps: + +* Run eligibility engine. +* If eligible, generate payout. +* Insert quote record with expiry (e.g., 60 seconds). +* Return quote_id and payout_sats. + +### 11.2 POST /claim/confirm + +Input: + +* quote_id + +Steps: + +* Verify quote exists and active. +* Re-check cooldown and budget. +* Execute LNbits payment. +* Update claim record. +* Mark quote consumed. + +Must be idempotent. + +--- + +## 12. LNbits Integration + +Payout flow: + +1. Resolve Lightning address to LNURL. +2. Fetch LNURL payRequest. +3. Call callback with amount in millisats. +4. Handle success/failure response. +5. Store payment hash. + +On failure: + +* Mark claim failed. +* Do not lock cooldown unless configured. + +--- + +## 13. Public Endpoints + +GET /health +GET /config +GET /stats +GET /deposit + +No authentication required. + +--- + +## 14. Logging and Monitoring + +Each claim attempt must log: + +* pubkey +* ip_hash +* eligibility result +* payout amount +* payment status + +Metrics to track: + +* denial reasons count +* payout distribution +* daily spend + +--- + +## 15. Security Hard Requirements + +* Strict CORS +* Rate limit /claim endpoints +* Nonce replay protection +* HMAC IP hashing +* Admin keys never exposed +* All secrets loaded from env + +--- + +## 16. Production Readiness Checklist + +Backend is complete when: + +* NIP-98 auth fully verified. +* Pubkey and IP cooldown enforced. +* Account age check enforced. +* Activity score enforced. +* Entropy payout cannot be rerolled. +* Daily budget cannot be exceeded. +* LNbits payout works and errors handled safely. +* Emergency stop disables claims instantly. +* Logs clearly show denial reasons. diff --git a/context/overview.md b/context/overview.md new file mode 100644 index 0000000..ca5c105 --- /dev/null +++ b/context/overview.md @@ -0,0 +1,386 @@ +# Sats Faucet App + +## 1. Purpose + +Build a sats faucet web app that lets Nostr users claim a small, randomized sats payout (entropy payout) on a cooldown schedule, with strong anti-abuse protections. + +Key goals: + +* Fun community experiment that can evolve into a long-term public faucet. +* Simple UX: connect Nostr, enter Lightning address, claim. +* Abuse-resistant: NIP-98 signed requests, account age checks, activity scoring, per-pubkey cooldown, per-IP cooldown, and budget guards. +* Transparent: public stats and recent payouts (anonymized). +* Sustainable: daily budget cap, emergency stop switch, and minimum wallet balance guard. + +Non-goals: + +* Perfect Sybil resistance. The system raises the cost of abuse rather than claiming to fully prevent it. + +## 2. Core Requirements + +### 2.1 Claim rules + +A user may claim only if all conditions are true: + +* Authenticated via Nostr using NIP-98 signature on the claim request. +* Account age is at least MIN_ACCOUNT_AGE_DAYS (default 14 days), based on earliest observed event timestamp on configured relays. +* Activity score meets or exceeds MIN_ACTIVITY_SCORE. +* Pubkey has not successfully claimed within the last COOLDOWN_DAYS (default 7 days). +* IP has not successfully claimed within the last IP_COOLDOWN_DAYS (default 7 days) and under MAX_CLAIMS_PER_IP_PER_PERIOD. +* Faucet is enabled and not in emergency stop. +* LNbits wallet balance is at or above MIN_WALLET_BALANCE_SATS. +* Daily budget not exceeded. + +### 2.2 Payout + +* Payout uses weighted randomness (entropy) configured by environment variables. +* Reward amount is fixed per bucket (example buckets: 10, 25, 50, 100 sats), selected by weight. +* The payout selected for a claim is locked to a short-lived quote to prevent re-rolling by refreshing. +* Payout is sent via LNbits to a Lightning address provided by the user. + +### 2.3 Deposits + +* The app must display a deposit Lightning address and QR for community funding. +* Preferred: LNURLp/Lightning Address for deposits (static address and QR). +* Optional: allow users to create an invoice for a chosen deposit amount. + +### 2.4 Transparency + +Publicly visible: + +* Current faucet pool balance (from LNbits). +* Total sats paid. +* Total claim count. +* Claims in last 24h. +* Daily budget and daily spend. +* Recent claims list (anonymized, limited number). + +## 3. System Overview + +### 3.1 Components + +1. Frontend Web App + + * Connect Nostr (NIP-07 or external signer). + * Collect Lightning address. + * Calls backend to get a quote and confirm claim. + * Shows eligibility failures with friendly reasons and next eligible time. + * Shows deposit info and public transparency stats. + +2. Faucet Backend API + + * Validates NIP-98 signed requests. + * Resolves client IP safely (with proxy support). + * Enforces cooldown locks and rate limiting. + * Fetches Nostr metadata + activity from relays and caches it. + * Computes account age and activity score. + * Computes payout quote, enforces daily budget, executes LNbits payout. + * Records claims, payout results, and stats. + +3. LNbits + + * Holds the faucet wallet. + * Executes outgoing payments. + * Provides deposit address (Lightning Address/LNURLp). + +### 3.2 Trust boundaries + +* LNbits keys live only on the backend. +* Frontend never sees LNbits keys. +* Backend stores only HMAC-hashed IPs, not raw IPs. +* Lightning address should not be stored in plaintext unless explicitly desired; store a hash for privacy. + +## 4. User Experience + +### 4.1 Home page + +* Primary call-to-action: Claim sats. +* Deposit section: + + * Show DEPOSIT_LIGHTNING_ADDRESS. + * Show QR code for DEPOSIT_LNURLP (or encoded LNURL). + * Optional button: Create deposit invoice. +* Live stats: + + * Balance, total paid, total claims, last 24h. + * Recent payouts list. +* Rules summary: + + * Cooldown days, min age, min score, per-IP limit. + +### 4.2 Claim flow + +1. User clicks Connect Nostr. +2. User enters Lightning address. +3. User clicks Check / Quote. +4. App shows: + + * If ineligible: exact reason and next eligible time. + * If eligible: payout amount and Confirm button. +5. User confirms. +6. App shows success state: + + * payout amount + * status + * next eligible date + +UI must clearly separate: + +* Eligibility check +* Payment attempt + +## 5. Backend Behavior + +### 5.1 NIP-98 authentication + +The backend must verify each protected request: + +* Signature is valid for the provided pubkey. +* Signed payload includes correct method and URL. +* Timestamp is within NIP98_MAX_SKEW_SECONDS. +* Nonce is present and not reused (store for NONCE_TTL_SECONDS). + +### 5.2 IP handling + +* Backend must support TRUST_PROXY mode. +* When TRUST_PROXY=true, derive IP from X-Forwarded-For correctly. +* Hash IP using HMAC_IP_SECRET and store only the hash. + +### 5.3 Eligibility engine + +For a given pubkey and ip_hash: + +* Check emergency stop and faucet enabled flags. +* Check wallet balance guard. +* Check per-pubkey cooldown: latest successful claim timestamp. +* Check per-IP cooldown: latest successful claim timestamp and quota. +* Fetch or load cached Nostr profile and activity metrics. +* Verify account age and activity score. + +Return structured result: + +* eligible: boolean +* denial_reason: enum +* denial_message: user-friendly text +* next_eligible_at: timestamp if applicable + +### 5.4 Quote then confirm + +Use a two-step claim to prevent payout rerolling. + +POST /claim/quote + +* Auth required (NIP-98) +* Input: lightning_address +* Performs full eligibility check. +* Selects payout using weighted randomness. +* Enforces daily budget guard: + + * If today_spent + payout > DAILY_BUDGET_SATS, either deny or reduce to FAUCET_MIN_SATS based on config. +* Creates a short-lived quote record (quote_id, pubkey, payout_sats, expires_at). +* Response includes payout_sats and quote_id. + +POST /claim/confirm + +* Auth required (NIP-98) +* Input: quote_id +* Re-check critical guards (pubkey lock, ip lock, budget, balance, quote expiry). +* Executes LNbits payment. +* Records claim row with status. +* Returns success/failure payload. + +Notes: + +* Confirm must be idempotent: multiple confirms for same quote_id should not pay twice. +* If confirm is called after quote expiry, return a clean error. + +### 5.5 LNbits payout + +Backend sends payment to the user’s Lightning address. + +Must handle: + +* Lightning address validation (basic format and domain resolve). +* LNURLp resolution: fetch payRequest, then call callback with amount. +* Handle failures: + + * Mark claim as failed, store error. + * Do not consume cooldown if payout did not actually send (configurable, but recommended). + +Retries: + +* For MVP, 0 retries. +* For long-term, add a background worker to retry transient failures. + +## 6. Nostr data sourcing + +### 6.1 Relay list + +Backend reads NOSTR_RELAYS from env. + +Must query multiple relays in parallel. + +### 6.2 Account age + +Compute nostr_first_seen_at as earliest observed event timestamp for the pubkey. + +Practical strategy: + +* Query kinds: 0, 1, 3. +* Use earliest created_at found. +* Cache result. + +If no events found: + +* Treat as new or unknown and deny with a friendly message. + +### 6.3 Activity score + +Compute a simple score (0 to 100) using cached metrics. + +Suggested inputs: + +* Has kind 0 metadata. +* Notes count in last ACTIVITY_LOOKBACK_DAYS. +* Following count. +* Followers count (optional if expensive). +* Optional zap receipts. + +The scoring formula must be controlled via env thresholds so it can be tuned as the faucet grows. + +## 7. Persistence and Storage + +### 7.1 Database + +Use Postgres. + +Required tables: + +* users: pubkey, nostr_first_seen_at, cached metrics, last fetch timestamps. +* claims: pubkey, claimed_at, payout, ip_hash, status, lnbits identifiers, errors. +* ip_limits: ip_hash, last_claimed_at, rolling counters. +* quotes: quote_id, pubkey, payout_sats, expires_at, status. +* stats_daily (optional): daily totals. + +### 7.2 Privacy + +* Store ip_hash only (HMAC). +* Store payout destination as hash. +* Never log raw Lightning address in plaintext logs. + +## 8. Observability + +Logging must include: + +* Request id +* pubkey +* eligibility denial reason +* payout amount (if eligible) +* LNbits payment result (success/failure) + +Metrics dashboard (optional): + +* total claims +* total paid +* denial counts by reason +* payout distribution + +## 9. Operational Controls + +Environment variables must support: + +* FAUCET_ENABLED +* EMERGENCY_STOP +* DAILY_BUDGET_SATS +* MAX_CLAIMS_PER_DAY +* MIN_WALLET_BALANCE_SATS + +Behavior: + +* If EMERGENCY_STOP=true, deny all claims with a maintenance message. +* If wallet balance below MIN_WALLET_BALANCE_SATS, deny claims and encourage deposits. + +## 10. API Surface + +Public endpoints + +* GET /health +* GET /config +* GET /stats +* GET /deposit + +Claim endpoints (auth: NIP-98) + +* POST /claim/quote +* POST /claim/confirm + +Optional admin endpoints (later) + +* GET /admin/claims +* GET /admin/users +* POST /admin/ban +* POST /admin/allow + +## 11. Error Handling + +All errors must return: + +* code (stable string) +* message (user-friendly) +* details (optional for debugging) + +Common denial codes: + +* faucet_disabled +* emergency_stop +* insufficient_balance +* daily_budget_exceeded +* cooldown_pubkey +* cooldown_ip +* account_too_new +* low_activity +* invalid_nip98 +* invalid_lightning_address +* quote_expired +* payout_failed + +## 12. Security Checklist + +Minimum required: + +* Strict NIP-98 verification (method, url, timestamp, nonce). +* Nonce replay prevention. +* IP hashing. +* Cloudflare or similar rate limiting on claim endpoints. +* CORS restricted to FRONTEND_URL. +* No secret keys in frontend. + +Nice to have: + +* Bot protection (Turnstile) behind a feature flag. +* VPN/proxy detection behind a feature flag. + +## 13. Deployment + +* Backend behind a reverse proxy with TLS. +* TRUST_PROXY=true when behind proxy. +* Use database migrations. + +Recommended: + +* Docker compose for backend + Postgres + optional Redis. +* Separate LNbits deployment. + +## 14. Acceptance Criteria + +A developer is done when: + +* A Nostr user can connect and claim sats successfully to a Lightning address. +* The faucet enforces cooldowns per pubkey and per IP. +* The faucet rejects accounts younger than 14 days. +* The faucet rejects accounts below activity threshold. +* Payouts are randomized and cannot be rerolled by refreshing. +* Public page shows deposit address and QR. +* Public stats show balance and transparency counters. +* Admin can stop the faucet instantly via env. +* No raw IPs or LNbits keys leak to the client. diff --git a/frontend/.env.example b/frontend/.env.example new file mode 100644 index 0000000..768805a --- /dev/null +++ b/frontend/.env.example @@ -0,0 +1,3 @@ +# Backend API URL (required in dev when frontend runs on different port) +# Leave empty if frontend is served from same origin as API +VITE_API_URL=http://localhost:3001 diff --git a/frontend/bitcoin-faucet_3.html b/frontend/bitcoin-faucet_3.html new file mode 100644 index 0000000..ecaeae1 --- /dev/null +++ b/frontend/bitcoin-faucet_3.html @@ -0,0 +1,374 @@ + + + + + + Free Bitcoins + + + + + +
+ +
+ + +
+
+ + + + + + + + + + + + + +

Free Bitcoins

+
+ +
+

Get Bitcoins from the Bitcoin Faucet

+

I'm giving away 1 to 5 satoshis per visitor; just solve the "captcha" then enter your Lightning address and press Get Some:

+ + +
+
+
You will receive
+
+
?
+
sats
+
+
pending roll…
+
+
+
+
+
+
+
+
+
+
+ +
+
+
+ + +
+
+
+
+ + +
+ + + +
+
+
+
+ + + + + diff --git a/frontend/index.html b/frontend/index.html new file mode 100644 index 0000000..8a77665 --- /dev/null +++ b/frontend/index.html @@ -0,0 +1,12 @@ + + + + + + Sats Faucet + + +
+ + + diff --git a/frontend/package-lock.json b/frontend/package-lock.json new file mode 100644 index 0000000..1cbcb4b --- /dev/null +++ b/frontend/package-lock.json @@ -0,0 +1,2297 @@ +{ + "name": "lnfaucet-frontend", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "lnfaucet-frontend", + "version": "1.0.0", + "dependencies": { + "canvas-confetti": "^1.9.4", + "framer-motion": "^11.11.17", + "nostr-tools": "^2.4.4", + "qrcode": "^1.5.4", + "react": "^18.3.1", + "react-dom": "^18.3.1", + "react-router-dom": "^7.13.1" + }, + "devDependencies": { + "@types/canvas-confetti": "^1.9.0", + "@types/react": "^18.3.12", + "@types/react-dom": "^18.3.1", + "@types/react-router-dom": "^5.3.3", + "@vitejs/plugin-react": "^4.3.3", + "typescript": "^5.6.3", + "vite": "^5.4.10" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz", + "integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.28.5", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.0.tgz", + "integrity": "sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.0.tgz", + "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.0", + "@babel/generator": "^7.29.0", + "@babel/helper-compilation-targets": "^7.28.6", + "@babel/helper-module-transforms": "^7.28.6", + "@babel/helpers": "^7.28.6", + "@babel/parser": "^7.29.0", + "@babel/template": "^7.28.6", + "@babel/traverse": "^7.29.0", + "@babel/types": "^7.29.0", + "@jridgewell/remapping": "^2.3.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/generator": { + "version": "7.29.1", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.1.tgz", + "integrity": "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.0", + "@babel/types": "^7.29.0", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.28.6.tgz", + "integrity": "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.28.6", + "@babel/helper-validator-option": "^7.27.1", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", + "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.28.6.tgz", + "integrity": "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.6.tgz", + "integrity": "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.28.6", + "@babel/helper-validator-identifier": "^7.28.5", + "@babel/traverse": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.28.6.tgz", + "integrity": "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", + "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.6.tgz", + "integrity": "sha512-xOBvwq86HHdB7WUDTfKfT/Vuxh7gElQ+Sfti2Cy6yIWNW05P8iUslOVcZ4/sKbE+/jQaukQAdz/gf3724kYdqw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.0.tgz", + "integrity": "sha512-IyDgFV5GeDUVX4YdF/3CPULtVGSXXMLh1xVIgdCgxApktqnQV0r7/8Nqthg+8YLGaAtdyIlo2qIdZrbCv4+7ww==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.0" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-self": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.27.1.tgz", + "integrity": "sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-source": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.27.1.tgz", + "integrity": "sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/template": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz", + "integrity": "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.28.6", + "@babel/parser": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.0.tgz", + "integrity": "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.0", + "@babel/generator": "^7.29.0", + "@babel/helper-globals": "^7.28.0", + "@babel/parser": "^7.29.0", + "@babel/template": "^7.28.6", + "@babel/types": "^7.29.0", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz", + "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", + "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", + "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", + "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", + "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", + "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", + "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", + "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", + "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", + "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", + "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", + "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", + "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", + "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", + "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", + "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", + "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", + "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", + "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", + "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", + "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", + "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", + "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", + "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "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.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@noble/ciphers": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@noble/ciphers/-/ciphers-2.1.1.tgz", + "integrity": "sha512-bysYuiVfhxNJuldNXlFEitTVdNnYUc+XNJZd7Qm2a5j1vZHgY+fazadNFWFaMK/2vye0JVlxV3gHmC0WDfAOQw==", + "license": "MIT", + "engines": { + "node": ">= 20.19.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@noble/curves": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-2.0.1.tgz", + "integrity": "sha512-vs1Az2OOTBiP4q0pwjW5aF0xp9n4MxVrmkFBxc6EKZc6ddYx5gaZiAsZoq0uRRXWbi3AT/sBqn05eRPtn1JCPw==", + "license": "MIT", + "dependencies": { + "@noble/hashes": "2.0.1" + }, + "engines": { + "node": ">= 20.19.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@noble/hashes": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-2.0.1.tgz", + "integrity": "sha512-XlOlEbQcE9fmuXxrVTXCTlG2nlRXa9Rj3rr5Ue/+tX+nmkgbX720YHh0VR3hBF9xDvwnb8D2shVGOwNx+ulArw==", + "license": "MIT", + "engines": { + "node": ">= 20.19.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.0-beta.27", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz", + "integrity": "sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.59.0.tgz", + "integrity": "sha512-upnNBkA6ZH2VKGcBj9Fyl9IGNPULcjXRlg0LLeaioQWueH30p6IXtJEbKAgvyv+mJaMxSm1l6xwDXYjpEMiLMg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.59.0.tgz", + "integrity": "sha512-hZ+Zxj3SySm4A/DylsDKZAeVg0mvi++0PYVceVyX7hemkw7OreKdCvW2oQ3T1FMZvCaQXqOTHb8qmBShoqk69Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.59.0.tgz", + "integrity": "sha512-W2Psnbh1J8ZJw0xKAd8zdNgF9HRLkdWwwdWqubSVk0pUuQkoHnv7rx4GiF9rT4t5DIZGAsConRE3AxCdJ4m8rg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.59.0.tgz", + "integrity": "sha512-ZW2KkwlS4lwTv7ZVsYDiARfFCnSGhzYPdiOU4IM2fDbL+QGlyAbjgSFuqNRbSthybLbIJ915UtZBtmuLrQAT/w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.59.0.tgz", + "integrity": "sha512-EsKaJ5ytAu9jI3lonzn3BgG8iRBjV4LxZexygcQbpiU0wU0ATxhNVEpXKfUa0pS05gTcSDMKpn3Sx+QB9RlTTA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.59.0.tgz", + "integrity": "sha512-d3DuZi2KzTMjImrxoHIAODUZYoUUMsuUiY4SRRcJy6NJoZ6iIqWnJu9IScV9jXysyGMVuW+KNzZvBLOcpdl3Vg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.59.0.tgz", + "integrity": "sha512-t4ONHboXi/3E0rT6OZl1pKbl2Vgxf9vJfWgmUoCEVQVxhW6Cw/c8I6hbbu7DAvgp82RKiH7TpLwxnJeKv2pbsw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.59.0.tgz", + "integrity": "sha512-CikFT7aYPA2ufMD086cVORBYGHffBo4K8MQ4uPS/ZnY54GKj36i196u8U+aDVT2LX4eSMbyHtyOh7D7Zvk2VvA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.59.0.tgz", + "integrity": "sha512-jYgUGk5aLd1nUb1CtQ8E+t5JhLc9x5WdBKew9ZgAXg7DBk0ZHErLHdXM24rfX+bKrFe+Xp5YuJo54I5HFjGDAA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.59.0.tgz", + "integrity": "sha512-peZRVEdnFWZ5Bh2KeumKG9ty7aCXzzEsHShOZEFiCQlDEepP1dpUl/SrUNXNg13UmZl+gzVDPsiCwnV1uI0RUA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.59.0.tgz", + "integrity": "sha512-gbUSW/97f7+r4gHy3Jlup8zDG190AuodsWnNiXErp9mT90iCy9NKKU0Xwx5k8VlRAIV2uU9CsMnEFg/xXaOfXg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.59.0.tgz", + "integrity": "sha512-yTRONe79E+o0FWFijasoTjtzG9EBedFXJMl888NBEDCDV9I2wGbFFfJQQe63OijbFCUZqxpHz1GzpbtSFikJ4Q==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.59.0.tgz", + "integrity": "sha512-sw1o3tfyk12k3OEpRddF68a1unZ5VCN7zoTNtSn2KndUE+ea3m3ROOKRCZxEpmT9nsGnogpFP9x6mnLTCaoLkA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.59.0.tgz", + "integrity": "sha512-+2kLtQ4xT3AiIxkzFVFXfsmlZiG5FXYW7ZyIIvGA7Bdeuh9Z0aN4hVyXS/G1E9bTP/vqszNIN/pUKCk/BTHsKA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.59.0.tgz", + "integrity": "sha512-NDYMpsXYJJaj+I7UdwIuHHNxXZ/b/N2hR15NyH3m2qAtb/hHPA4g4SuuvrdxetTdndfj9b1WOmy73kcPRoERUg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.59.0.tgz", + "integrity": "sha512-nLckB8WOqHIf1bhymk+oHxvM9D3tyPndZH8i8+35p/1YiVoVswPid2yLzgX7ZJP0KQvnkhM4H6QZ5m0LzbyIAg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.59.0.tgz", + "integrity": "sha512-oF87Ie3uAIvORFBpwnCvUzdeYUqi2wY6jRFWJAy1qus/udHFYIkplYRW+wo+GRUP4sKzYdmE1Y3+rY5Gc4ZO+w==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.59.0.tgz", + "integrity": "sha512-3AHmtQq/ppNuUspKAlvA8HtLybkDflkMuLK4DPo77DfthRb71V84/c4MlWJXixZz4uruIH4uaa07IqoAkG64fg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.59.0.tgz", + "integrity": "sha512-2UdiwS/9cTAx7qIUZB/fWtToJwvt0Vbo0zmnYt7ED35KPg13Q0ym1g442THLC7VyI6JfYTP4PiSOWyoMdV2/xg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.59.0.tgz", + "integrity": "sha512-M3bLRAVk6GOwFlPTIxVBSYKUaqfLrn8l0psKinkCFxl4lQvOSz8ZrKDz2gxcBwHFpci0B6rttydI4IpS4IS/jQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.59.0.tgz", + "integrity": "sha512-tt9KBJqaqp5i5HUZzoafHZX8b5Q2Fe7UjYERADll83O4fGqJ49O1FsL6LpdzVFQcpwvnyd0i+K/VSwu/o/nWlA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.59.0.tgz", + "integrity": "sha512-V5B6mG7OrGTwnxaNUzZTDTjDS7F75PO1ae6MJYdiMu60sq0CqN5CVeVsbhPxalupvTX8gXVSU9gq+Rx1/hvu6A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.59.0.tgz", + "integrity": "sha512-UKFMHPuM9R0iBegwzKF4y0C4J9u8C6MEJgFuXTBerMk7EJ92GFVFYBfOZaSGLu6COf7FxpQNqhNS4c4icUPqxA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.59.0.tgz", + "integrity": "sha512-laBkYlSS1n2L8fSo1thDNGrCTQMmxjYY5G0WFWjFFYZkKPjsMBsgJfGf4TLxXrF6RyhI60L8TMOjBMvXiTcxeA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.59.0.tgz", + "integrity": "sha512-2HRCml6OztYXyJXAvdDXPKcawukWY2GpR5/nxKp4iBgiO3wcoEGkAaqctIbZcNB6KlUQBIqt8VYkNSj2397EfA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@scure/base": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@scure/base/-/base-2.0.0.tgz", + "integrity": "sha512-3E1kpuZginKkek01ovG8krQ0Z44E3DHPjc5S2rjJw9lZn3KSQOs8S7wqikF/AH7iRanHypj85uGyxk0XAyC37w==", + "license": "MIT", + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@scure/bip32": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@scure/bip32/-/bip32-2.0.1.tgz", + "integrity": "sha512-4Md1NI5BzoVP+bhyJaY3K6yMesEFzNS1sE/cP+9nuvE7p/b0kx9XbpDHHFl8dHtufcbdHRUUQdRqLIPHN/s7yA==", + "license": "MIT", + "dependencies": { + "@noble/curves": "2.0.1", + "@noble/hashes": "2.0.1", + "@scure/base": "2.0.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@scure/bip39": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@scure/bip39/-/bip39-2.0.1.tgz", + "integrity": "sha512-PsxdFj/d2AcJcZDX1FXN3dDgitDDTmwf78rKZq1a6c1P1Nan1X/Sxc7667zU3U+AN60g7SxxP0YCVw2H/hBycg==", + "license": "MIT", + "dependencies": { + "@noble/hashes": "2.0.1", + "@scure/base": "2.0.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", + "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", + "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.2" + } + }, + "node_modules/@types/canvas-confetti": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@types/canvas-confetti/-/canvas-confetti-1.9.0.tgz", + "integrity": "sha512-aBGj/dULrimR1XDZLtG9JwxX1b4HPRF6CX9Yfwh3NvstZEm1ZL7RBnel4keCPSqs1ANRu1u2Aoz9R+VmtjYuTg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/history": { + "version": "4.7.11", + "resolved": "https://registry.npmjs.org/@types/history/-/history-4.7.11.tgz", + "integrity": "sha512-qjDJRrmvBMiTx+jyLxvLfJU7UznFuokDv4f3WRuriHKERccVpFU+8XMQUAbDzoiJCsmexxRExQeMwwCdamSKDA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/prop-types": { + "version": "15.7.15", + "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz", + "integrity": "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/react": { + "version": "18.3.28", + "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.28.tgz", + "integrity": "sha512-z9VXpC7MWrhfWipitjNdgCauoMLRdIILQsAEV+ZesIzBq/oUlxk0m3ApZuMFCXdnS4U7KrI+l3WRUEGQ8K1QKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/prop-types": "*", + "csstype": "^3.2.2" + } + }, + "node_modules/@types/react-dom": { + "version": "18.3.7", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.7.tgz", + "integrity": "sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "^18.0.0" + } + }, + "node_modules/@types/react-router": { + "version": "5.1.20", + "resolved": "https://registry.npmjs.org/@types/react-router/-/react-router-5.1.20.tgz", + "integrity": "sha512-jGjmu/ZqS7FjSH6owMcD5qpq19+1RS9DeVRqfl1FeBMxTDQAGwlMWOcs52NDoXaNKyG3d1cYQFMs9rCrb88o9Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/history": "^4.7.11", + "@types/react": "*" + } + }, + "node_modules/@types/react-router-dom": { + "version": "5.3.3", + "resolved": "https://registry.npmjs.org/@types/react-router-dom/-/react-router-dom-5.3.3.tgz", + "integrity": "sha512-kpqnYK4wcdm5UaWI3fLcELopqLrHgLqNsdpHauzlQktfkHL3npOSwtj1Uz9oKBAzs7lFtVkV8j83voAz2D8fhw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/history": "^4.7.11", + "@types/react": "*", + "@types/react-router": "*" + } + }, + "node_modules/@vitejs/plugin-react": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.7.0.tgz", + "integrity": "sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.28.0", + "@babel/plugin-transform-react-jsx-self": "^7.27.1", + "@babel/plugin-transform-react-jsx-source": "^7.27.1", + "@rolldown/pluginutils": "1.0.0-beta.27", + "@types/babel__core": "^7.20.5", + "react-refresh": "^0.17.0" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "peerDependencies": { + "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/baseline-browser-mapping": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.0.tgz", + "integrity": "sha512-lIyg0szRfYbiy67j9KN8IyeD7q7hcmqnJ1ddWmNt19ItGpNN64mnllmxUNFIOdOm6by97jlL6wfpTTJrmnjWAA==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/browserslist": { + "version": "4.28.1", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.1.tgz", + "integrity": "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "baseline-browser-mapping": "^2.9.0", + "caniuse-lite": "^1.0.30001759", + "electron-to-chromium": "^1.5.263", + "node-releases": "^2.0.27", + "update-browserslist-db": "^1.2.0" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001774", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001774.tgz", + "integrity": "sha512-DDdwPGz99nmIEv216hKSgLD+D4ikHQHjBC/seF98N9CPqRX4M5mSxT9eTV6oyisnJcuzxtZy4n17yKKQYmYQOA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/canvas-confetti": { + "version": "1.9.4", + "resolved": "https://registry.npmjs.org/canvas-confetti/-/canvas-confetti-1.9.4.tgz", + "integrity": "sha512-yxQbJkAVrFXWNbTUjPqjF7G+g6pDotOUHGbkZq2NELZUMDpiJ85rIEazVb8GTaAptNW2miJAXbs1BtioA251Pw==", + "license": "ISC", + "funding": { + "type": "donate", + "url": "https://www.paypal.me/kirilvatev" + } + }, + "node_modules/cliui": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-6.0.0.tgz", + "integrity": "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==", + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^6.2.0" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "license": "MIT" + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "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==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/decamelize": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", + "integrity": "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/dijkstrajs": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/dijkstrajs/-/dijkstrajs-1.0.3.tgz", + "integrity": "sha512-qiSlmBq9+BCdCA/L46dw8Uy93mloxsPSbwnm5yrKn2vMPiy8KyAskTF6zuV/j5BMsmOGZDPs7KjU+mjb670kfA==", + "license": "MIT" + }, + "node_modules/electron-to-chromium": { + "version": "1.5.302", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.302.tgz", + "integrity": "sha512-sM6HAN2LyK82IyPBpznDRqlTQAtuSaO+ShzFiWTvoMJLHyZ+Y39r8VMfHzwbU8MVBzQ4Wdn85+wlZl2TLGIlwg==", + "dev": true, + "license": "ISC" + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT" + }, + "node_modules/esbuild": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", + "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.21.5", + "@esbuild/android-arm": "0.21.5", + "@esbuild/android-arm64": "0.21.5", + "@esbuild/android-x64": "0.21.5", + "@esbuild/darwin-arm64": "0.21.5", + "@esbuild/darwin-x64": "0.21.5", + "@esbuild/freebsd-arm64": "0.21.5", + "@esbuild/freebsd-x64": "0.21.5", + "@esbuild/linux-arm": "0.21.5", + "@esbuild/linux-arm64": "0.21.5", + "@esbuild/linux-ia32": "0.21.5", + "@esbuild/linux-loong64": "0.21.5", + "@esbuild/linux-mips64el": "0.21.5", + "@esbuild/linux-ppc64": "0.21.5", + "@esbuild/linux-riscv64": "0.21.5", + "@esbuild/linux-s390x": "0.21.5", + "@esbuild/linux-x64": "0.21.5", + "@esbuild/netbsd-x64": "0.21.5", + "@esbuild/openbsd-x64": "0.21.5", + "@esbuild/sunos-x64": "0.21.5", + "@esbuild/win32-arm64": "0.21.5", + "@esbuild/win32-ia32": "0.21.5", + "@esbuild/win32-x64": "0.21.5" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "license": "MIT", + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/framer-motion": { + "version": "11.18.2", + "resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-11.18.2.tgz", + "integrity": "sha512-5F5Och7wrvtLVElIpclDT0CBzMVg3dL22B64aZwHtsIY8RB4mXICLrkajK4G9R+ieSAGcgrLeae2SeUTg2pr6w==", + "license": "MIT", + "dependencies": { + "motion-dom": "^11.18.1", + "motion-utils": "^11.18.1", + "tslib": "^2.4.0" + }, + "peerDependencies": { + "@emotion/is-prop-valid": "*", + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@emotion/is-prop-valid": { + "optional": true + }, + "react": { + "optional": true + }, + "react-dom": { + "optional": true + } + } + }, + "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/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "license": "MIT" + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "license": "MIT", + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "license": "MIT", + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/motion-dom": { + "version": "11.18.1", + "resolved": "https://registry.npmjs.org/motion-dom/-/motion-dom-11.18.1.tgz", + "integrity": "sha512-g76KvA001z+atjfxczdRtw/RXOM3OMSdd1f4DL77qCTF/+avrRJiawSG4yDibEQ215sr9kpinSlX2pCTJ9zbhw==", + "license": "MIT", + "dependencies": { + "motion-utils": "^11.18.1" + } + }, + "node_modules/motion-utils": { + "version": "11.18.1", + "resolved": "https://registry.npmjs.org/motion-utils/-/motion-utils-11.18.1.tgz", + "integrity": "sha512-49Kt+HKjtbJKLtgO/LKj9Ld+6vw9BjH5d9sc40R/kVyH8GLAXgT42M2NnuPcJNuA3s9ZfZBUcwIgpmZWGEE+hA==", + "license": "MIT" + }, + "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==", + "dev": true, + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/node-releases": { + "version": "2.0.27", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.27.tgz", + "integrity": "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/nostr-tools": { + "version": "2.23.1", + "resolved": "https://registry.npmjs.org/nostr-tools/-/nostr-tools-2.23.1.tgz", + "integrity": "sha512-Q5SJ1omrseBFXtLwqDhufpFLA6vX3rS/IuBCc974qaYX6YKGwEPxa/ZsyxruUOr+b+5EpWL2hFmCB5AueYrfBw==", + "license": "Unlicense", + "dependencies": { + "@noble/ciphers": "2.1.1", + "@noble/curves": "2.0.1", + "@noble/hashes": "2.0.1", + "@scure/base": "2.0.0", + "@scure/bip32": "2.0.1", + "@scure/bip39": "2.0.1", + "nostr-wasm": "0.1.0" + }, + "peerDependencies": { + "typescript": ">=5.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/nostr-wasm": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/nostr-wasm/-/nostr-wasm-0.1.0.tgz", + "integrity": "sha512-78BTryCLcLYv96ONU8Ws3Q1JzjlAt+43pWQhIl86xZmWeegYCNLPml7yQ+gG3vR6V5h4XGj+TxO+SS5dsThQIA==", + "license": "MIT" + }, + "node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "license": "MIT", + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "license": "MIT", + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/pngjs": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/pngjs/-/pngjs-5.0.0.tgz", + "integrity": "sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw==", + "license": "MIT", + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/postcss": { + "version": "8.5.6", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", + "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/qrcode": { + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/qrcode/-/qrcode-1.5.4.tgz", + "integrity": "sha512-1ca71Zgiu6ORjHqFBDpnSMTR2ReToX4l1Au1VFLyVeBTFavzQnv5JxMFr3ukHVKpSrSA2MCk0lNJSykjUfz7Zg==", + "license": "MIT", + "dependencies": { + "dijkstrajs": "^1.0.1", + "pngjs": "^5.0.0", + "yargs": "^15.3.1" + }, + "bin": { + "qrcode": "bin/qrcode" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/react": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", + "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", + "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0", + "scheduler": "^0.23.2" + }, + "peerDependencies": { + "react": "^18.3.1" + } + }, + "node_modules/react-refresh": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz", + "integrity": "sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-router": { + "version": "7.13.1", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.13.1.tgz", + "integrity": "sha512-td+xP4X2/6BJvZoX6xw++A2DdEi++YypA69bJUV5oVvqf6/9/9nNlD70YO1e9d3MyamJEBQFEzk6mbfDYbqrSA==", + "license": "MIT", + "dependencies": { + "cookie": "^1.0.1", + "set-cookie-parser": "^2.6.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "react": ">=18", + "react-dom": ">=18" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + } + } + }, + "node_modules/react-router-dom": { + "version": "7.13.1", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.13.1.tgz", + "integrity": "sha512-UJnV3Rxc5TgUPJt2KJpo1Jpy0OKQr0AjgbZzBFjaPJcFOb2Y8jA5H3LT8HUJAiRLlWrEXWHbF1Z4SCZaQjWDHw==", + "license": "MIT", + "dependencies": { + "react-router": "7.13.1" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "react": ">=18", + "react-dom": ">=18" + } + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/require-main-filename": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz", + "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==", + "license": "ISC" + }, + "node_modules/rollup": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.59.0.tgz", + "integrity": "sha512-2oMpl67a3zCH9H79LeMcbDhXW/UmWG/y2zuqnF2jQq5uq9TbM9TVyXvA4+t+ne2IIkBdrLpAaRQAvo7YI/Yyeg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.59.0", + "@rollup/rollup-android-arm64": "4.59.0", + "@rollup/rollup-darwin-arm64": "4.59.0", + "@rollup/rollup-darwin-x64": "4.59.0", + "@rollup/rollup-freebsd-arm64": "4.59.0", + "@rollup/rollup-freebsd-x64": "4.59.0", + "@rollup/rollup-linux-arm-gnueabihf": "4.59.0", + "@rollup/rollup-linux-arm-musleabihf": "4.59.0", + "@rollup/rollup-linux-arm64-gnu": "4.59.0", + "@rollup/rollup-linux-arm64-musl": "4.59.0", + "@rollup/rollup-linux-loong64-gnu": "4.59.0", + "@rollup/rollup-linux-loong64-musl": "4.59.0", + "@rollup/rollup-linux-ppc64-gnu": "4.59.0", + "@rollup/rollup-linux-ppc64-musl": "4.59.0", + "@rollup/rollup-linux-riscv64-gnu": "4.59.0", + "@rollup/rollup-linux-riscv64-musl": "4.59.0", + "@rollup/rollup-linux-s390x-gnu": "4.59.0", + "@rollup/rollup-linux-x64-gnu": "4.59.0", + "@rollup/rollup-linux-x64-musl": "4.59.0", + "@rollup/rollup-openbsd-x64": "4.59.0", + "@rollup/rollup-openharmony-arm64": "4.59.0", + "@rollup/rollup-win32-arm64-msvc": "4.59.0", + "@rollup/rollup-win32-ia32-msvc": "4.59.0", + "@rollup/rollup-win32-x64-gnu": "4.59.0", + "@rollup/rollup-win32-x64-msvc": "4.59.0", + "fsevents": "~2.3.2" + } + }, + "node_modules/scheduler": { + "version": "0.23.2", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", + "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0" + } + }, + "node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/set-blocking": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", + "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==", + "license": "ISC" + }, + "node_modules/set-cookie-parser": { + "version": "2.7.2", + "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.2.tgz", + "integrity": "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==", + "license": "MIT" + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "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==", + "license": "0BSD" + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "devOptional": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", + "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/vite": { + "version": "5.4.21", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz", + "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.21.3", + "postcss": "^8.4.43", + "rollup": "^4.20.0" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || >=20.0.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.4.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + } + } + }, + "node_modules/which-module": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.1.tgz", + "integrity": "sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ==", + "license": "ISC" + }, + "node_modules/wrap-ansi": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", + "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/y18n": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.3.tgz", + "integrity": "sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==", + "license": "ISC" + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true, + "license": "ISC" + }, + "node_modules/yargs": { + "version": "15.4.1", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-15.4.1.tgz", + "integrity": "sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==", + "license": "MIT", + "dependencies": { + "cliui": "^6.0.0", + "decamelize": "^1.2.0", + "find-up": "^4.1.0", + "get-caller-file": "^2.0.1", + "require-directory": "^2.1.1", + "require-main-filename": "^2.0.0", + "set-blocking": "^2.0.0", + "string-width": "^4.2.0", + "which-module": "^2.0.0", + "y18n": "^4.0.0", + "yargs-parser": "^18.1.2" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/yargs-parser": { + "version": "18.1.3", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-18.1.3.tgz", + "integrity": "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==", + "license": "ISC", + "dependencies": { + "camelcase": "^5.0.0", + "decamelize": "^1.2.0" + }, + "engines": { + "node": ">=6" + } + } + } +} diff --git a/frontend/package.json b/frontend/package.json new file mode 100644 index 0000000..cd0bbd9 --- /dev/null +++ b/frontend/package.json @@ -0,0 +1,29 @@ +{ + "name": "lnfaucet-frontend", + "private": true, + "version": "1.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "tsc -b && vite build", + "preview": "vite preview" + }, + "dependencies": { + "canvas-confetti": "^1.9.4", + "framer-motion": "^11.11.17", + "nostr-tools": "^2.4.4", + "qrcode": "^1.5.4", + "react": "^18.3.1", + "react-dom": "^18.3.1", + "react-router-dom": "^7.13.1" + }, + "devDependencies": { + "@types/canvas-confetti": "^1.9.0", + "@types/react": "^18.3.12", + "@types/react-dom": "^18.3.1", + "@types/react-router-dom": "^5.3.3", + "@vitejs/plugin-react": "^4.3.3", + "typescript": "^5.6.3", + "vite": "^5.4.10" + } +} diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx new file mode 100644 index 0000000..23a7b4f --- /dev/null +++ b/frontend/src/App.tsx @@ -0,0 +1,101 @@ +import { useState, useEffect, useCallback } from "react"; +import { BrowserRouter, Routes, Route } from "react-router-dom"; +import { getToken, getAuthMe, clearToken } from "./api"; +import { Header } from "./components/Header"; +import { Footer } from "./components/Footer"; +import { ClaimWizard } from "./components/ClaimWizard"; +import { StatsSection } from "./components/StatsSection"; +import { DepositSection } from "./components/DepositSection"; +import { TransactionsPage } from "./pages/TransactionsPage"; + +const FaucetSvg = () => ( + + + + + + + + + + + + + +); + +export default function App() { + const [pubkey, setPubkey] = useState(null); + const [statsRefetchTrigger, setStatsRefetchTrigger] = useState(0); + + useEffect(() => { + const token = getToken(); + if (!token) return; + getAuthMe() + .then((r) => setPubkey(r.pubkey)) + .catch(() => { + clearToken(); + setPubkey(null); + }); + }, []); + + const handleClaimSuccess = useCallback(() => { + setStatsRefetchTrigger((t) => t + 1); + }, []); + + return ( + +
+
+
+
+ + + +
+
+ +

Sats Faucet

+
+ +
+ +
+ } + /> + +
+ +
+
+ } + /> + +
+