From fc7927e1c89bc9da827f7159b0d75e02e78ef961 Mon Sep 17 00:00:00 2001 From: michilis Date: Sat, 31 May 2025 14:20:15 +0200 Subject: [PATCH] Initial commit --- .gitignore | 133 ++ README.md | 479 +++++++ env.example | 19 + package-lock.json | 2860 ++++++++++++++++++++++++++++++++++++++++ package.json | 52 + server.js | 617 +++++++++ services/cashu.js | 301 +++++ services/lightning.js | 276 ++++ services/redemption.js | 353 +++++ swagger.config.js | 554 ++++++++ 10 files changed, 5644 insertions(+) create mode 100644 .gitignore create mode 100644 README.md create mode 100644 env.example create mode 100644 package-lock.json create mode 100644 package.json create mode 100644 server.js create mode 100644 services/cashu.js create mode 100644 services/lightning.js create mode 100644 services/redemption.js create mode 100644 swagger.config.js diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e203ee9 --- /dev/null +++ b/.gitignore @@ -0,0 +1,133 @@ +# Dependencies +node_modules/ +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# Environment variables +.env +.env.local +.env.development.local +.env.test.local +.env.production.local + +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +lerna-debug.log* +.pnpm-debug.log* + +# Runtime data +pids +*.pid +*.seed +*.pid.lock + +# Coverage directory used by tools like istanbul +coverage/ +*.lcov + +# nyc test coverage +.nyc_output + +# Dependency directories +node_modules/ +jspm_packages/ + +# Snowpack dependency directory (https://snowpack.dev/) +web_modules/ + +# TypeScript cache +*.tsbuildinfo + +# Optional npm cache directory +.npm + +# Optional eslint cache +.eslintcache + +# Optional stylelint cache +.stylelintcache + +# Microbundle cache +.rpt2_cache/ +.rts2_cache_cjs/ +.rts2_cache_es/ +.rts2_cache_umd/ + +# Optional REPL history +.node_repl_history + +# Output of 'npm pack' +*.tgz + +# Yarn Integrity file +.yarn-integrity + +# parcel-bundler cache (https://parceljs.org/) +.cache +.parcel-cache + +# Next.js build output +.next +out + +# Nuxt.js build / generate output +.nuxt +dist + +# Gatsby files +.cache/ +public + +# Vuepress build output +.vuepress/dist + +# Serverless directories +.serverless/ + +# FuseBox cache +.fusebox/ + +# DynamoDB Local files +.dynamodb/ + +# TernJS port file +.tern-port + +# Stores VSCode versions used for testing VSCode extensions +.vscode-test + +# yarn v2 +.yarn/cache +.yarn/unplugged +.yarn/build-state.yml +.yarn/install-state.gz +.pnp.* + +# IDE files +.vscode/ +.idea/ +*.swp +*.swo +*~ + +# OS generated files +.DS_Store +.DS_Store? +._* +.Spotlight-V100 +.Trashes +ehthumbs.db +Thumbs.db + +# Database files +*.db +*.sqlite +*.sqlite3 + +# Redis dump file +dump.rdb \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..7937798 --- /dev/null +++ b/README.md @@ -0,0 +1,479 @@ +# Cashu Redeem API ๐Ÿช™โšก + +A production-grade API for redeeming Cashu tokens (ecash) to Lightning addresses using the cashu-ts library and LNURLp protocol. + +## ๐Ÿš€ Features + +- **Decode Cashu tokens** - Parse and validate token content +- **Redeem to Lightning addresses** - Convert ecash to Lightning payments via LNURLp +- **Real-time status tracking** - Monitor redemption progress with unique IDs +- **Security features** - Domain restrictions, rate limiting, input validation +- **Robust error handling** - Comprehensive error messages and status codes +- **In-memory caching** - Fast mint and wallet instances with connection pooling +- **Interactive API Documentation** - Complete Swagger/OpenAPI documentation at `/docs` + +## ๐Ÿ“– API Documentation + +**Interactive Swagger Documentation**: Visit `/docs` when running the server for a complete, interactive API reference. + +Example: `http://localhost:3000/docs` + +The documentation includes: +- Complete endpoint specifications +- Request/response schemas +- Try-it-out functionality +- Example requests and responses +- Authentication requirements +- Error code documentation + +## ๐Ÿ“ก API Endpoints + +### 1. `POST /api/decode` +Decode a Cashu token and return its content. Supports both v1 and v3 token formats. + +**Request:** +```json +{ + "token": "cashuAeyJhbGciOi..." +} +``` + +**Response:** +```json +{ + "success": true, + "decoded": { + "mint": "https://mint.azzamo.net", + "totalAmount": 21000, + "numProofs": 3, + "denominations": [1000, 10000, 10000], + "format": "cashuA" + }, + "mint_url": "https://mint.azzamo.net" +} +``` + +### 2. `POST /api/redeem` +Redeem a Cashu token to a Lightning address. Lightning address is optional - if not provided, uses the default address from configuration. + +**Request:** +```json +{ + "token": "cashuAeyJhbGciOi...", + "lightningAddress": "user@ln.tips" +} +``` + +**Request (using default address):** +```json +{ + "token": "cashuAeyJhbGciOi..." +} +``` + +**Success Response:** +```json +{ + "success": true, + "redeemId": "8e99101e-d034-4d2e-9ccf-dfda24d26762", + "paid": true, + "amount": 21000, + "to": "user@ln.tips", + "fee": 1000, + "actualFee": 420, + "netAmount": 20000, + "mint_url": "https://mint.azzamo.net", + "format": "cashuA", + "preimage": "abc123..." +} +``` + +**Success Response (using default address):** +```json +{ + "success": true, + "redeemId": "8e99101e-d034-4d2e-9ccf-dfda24d26762", + "paid": true, + "amount": 21000, + "to": "admin@your-domain.com", + "fee": 1000, + "actualFee": 420, + "netAmount": 20000, + "mint_url": "https://mint.azzamo.net", + "format": "cashuA", + "usingDefaultAddress": true, + "message": "Redeemed to default Lightning address: admin@your-domain.com" +} +``` + +### 3. `POST /api/status` +Check redemption status using redeemId. + +**Request:** +```json +{ + "redeemId": "8e99101e-d034-4d2e-9ccf-dfda24d26762" +} +``` + +**Response:** +```json +{ + "success": true, + "status": "paid", + "details": { + "amount": 21000, + "to": "user@ln.tips", + "paid": true, + "paidAt": "2025-01-14T12:00:00Z", + "fee": 1000, + "createdAt": "2025-01-14T11:59:30Z", + "updatedAt": "2025-01-14T12:00:00Z" + } +} +``` + +### 4. `GET /api/status/:redeemId` +Same as above, but via URL parameter for frontend polling. + +### 5. `GET /api/health` +Health check endpoint. + +**Response:** +```json +{ + "status": "ok", + "timestamp": "2025-01-14T12:00:00Z", + "uptime": 3600, + "memory": {...}, + "version": "1.0.0" +} +``` + +### 6. `POST /api/validate-address` +Validate a Lightning address without redemption. + +**Request:** +```json +{ + "lightningAddress": "user@ln.tips" +} +``` + +**Response:** +```json +{ + "success": true, + "valid": true, + "domain": "ln.tips", + "minSendable": 1, + "maxSendable": 100000000, + "commentAllowed": 144 +} +``` + +### 7. `GET /api/stats` +Get redemption statistics (admin endpoint). + +**Response:** +```json +{ + "success": true, + "stats": { + "total": 150, + "paid": 142, + "failed": 8, + "processing": 0, + "totalAmount": 2500000, + "totalFees": 15000 + } +} +``` + +### 8. `POST /api/check-spendable` +Check if a Cashu token is spendable at its mint before attempting redemption. + +**Request:** +```json +{ + "token": "cashuAeyJhbGciOi..." +} +``` + +**Response:** +```json +{ + "success": true, + "spendable": [true, true, false], + "pending": [], + "mintUrl": "https://mint.azzamo.net", + "totalAmount": 21000, + "message": "Token is spendable" +} +``` + +## ๐Ÿ›  Setup & Installation + +### Prerequisites +- Node.js >= 18.0.0 +- npm or yarn + +### Installation + +1. **Clone and install dependencies:** +```bash +git clone +cd cashu-redeem-api +npm install +``` + +2. **Setup environment variables:** +```bash +cp env.example .env +``` + +Edit `.env` file: +```bash +# Server Configuration +PORT=3000 +NODE_ENV=development + +# Security Configuration +ALLOW_REDEEM_DOMAINS=ln.tips,getalby.com,wallet.mutinywallet.com +API_SECRET=your-secret-key-here + +# Default Lightning Address (used when no address is provided in redeem requests) +DEFAULT_LIGHTNING_ADDRESS=admin@your-domain.com + +# Rate Limiting (requests per minute) +RATE_LIMIT=100 + +# CORS Configuration +ALLOWED_ORIGINS=http://localhost:3000,https://yourdomain.com +``` + +3. **Start the server:** +```bash +# Development +npm run dev + +# Production +npm start +``` + +The API will be available at `http://localhost:3000` + +## ๐Ÿ”ง Configuration + +### Environment Variables + +| Variable | Description | Default | Required | +|----------|-------------|---------|----------| +| `PORT` | Server port | `3000` | No | +| `NODE_ENV` | Environment | `development` | No | +| `ALLOW_REDEEM_DOMAINS` | Comma-separated allowed domains | All allowed | No | +| `DEFAULT_LIGHTNING_ADDRESS` | Default Lightning address for redemptions | None | No | +| `RATE_LIMIT` | Requests per minute per IP | `100` | No | +| `ALLOWED_ORIGINS` | CORS allowed origins | `http://localhost:3000` | No | + +### Domain Restrictions + +To restrict redemptions to specific Lightning address domains, set: +```bash +ALLOW_REDEEM_DOMAINS=ln.tips,getalby.com,wallet.mutinywallet.com +``` + +If not set, all domains are allowed. + +### Default Lightning Address + +To set a default Lightning address that will be used when no address is provided in redemption requests: +```bash +DEFAULT_LIGHTNING_ADDRESS=admin@your-domain.com +``` + +This allows users to redeem tokens without specifying a Lightning address - the tokens will automatically be sent to your configured default address. If no default is set, Lightning address becomes required for all redemption requests. + +## ๐Ÿ— Architecture + +### Services + +#### `services/cashu.js` +- Manages Cashu token parsing and validation +- Handles mint connections and wallet instances +- Performs token melting operations +- Caches mint/wallet connections for performance + +#### `services/lightning.js` +- Validates Lightning address formats +- Resolves LNURLp endpoints +- Generates Lightning invoices +- Handles domain restrictions + +#### `services/redemption.js` +- Coordinates the complete redemption process +- Manages redemption status tracking +- Handles duplicate token detection +- Provides statistics and cleanup + +### Data Flow + +1. **Token Validation** - Parse and validate Cashu token structure +2. **Address Resolution** - Resolve Lightning address to LNURLp endpoint +3. **Invoice Generation** - Create Lightning invoice for the amount +4. **Token Melting** - Use cashu-ts to melt token and pay invoice +5. **Status Tracking** - Store and update redemption status with UUID + +## ๐Ÿ”’ Security Features + +- **Input validation** - All inputs are sanitized and validated +- **Rate limiting** - 100 requests per minute per IP (configurable) +- **Domain restrictions** - Limit allowed Lightning address domains +- **CORS protection** - Configurable allowed origins +- **Error handling** - Comprehensive error messages without data leaks +- **Token deduplication** - Prevent double-spending with hash tracking + +## ๐Ÿšฆ Status Codes + +| Status | Description | +|--------|-------------| +| `processing` | Redemption is in progress | +| `parsing_token` | Validating and parsing the token | +| `resolving_invoice` | Resolving Lightning address to invoice | +| `melting_token` | Performing the melt operation | +| `paid` | Successfully paid and completed | +| `failed` | Redemption failed (see error details) | +| `checking_spendability` | Verifying token is spendable at mint | + +## ๐Ÿ“Š Monitoring + +### Health Check +```bash +curl http://localhost:3000/api/health +``` + +### Statistics +```bash +curl http://localhost:3000/api/stats +``` + +### Logs +The server logs all requests and errors to console. In production, consider using a proper logging solution like Winston. + +## ๐Ÿงช Testing + +### Interactive Testing with Swagger + +The easiest way to test the API is using the interactive Swagger documentation at `/docs`: +- Visit `http://localhost:3000/docs` +- Click "Try it out" on any endpoint +- Fill in the request parameters +- Execute the request directly from the browser + +### Example cURL commands + +**Decode a token:** +```bash +curl -X POST http://localhost:3000/api/decode \ + -H "Content-Type: application/json" \ + -d '{"token":"your-cashu-token-here"}' +``` + +**Check if token is spendable:** +```bash +curl -X POST http://localhost:3000/api/check-spendable \ + -H "Content-Type: application/json" \ + -d '{"token":"your-cashu-token-here"}' +``` + +**Redeem a token to specific address:** +```bash +curl -X POST http://localhost:3000/api/redeem \ + -H "Content-Type: application/json" \ + -d '{ + "token": "your-cashu-token-here", + "lightningAddress": "user@ln.tips" + }' +``` + +**Redeem a token to default address:** +```bash +curl -X POST http://localhost:3000/api/redeem \ + -H "Content-Type: application/json" \ + -d '{ + "token": "your-cashu-token-here" + }' +``` + +**Check status:** +```bash +curl -X POST http://localhost:3000/api/status \ + -H "Content-Type: application/json" \ + -d '{"redeemId":"your-redeem-id-here"}' +``` + +## ๐Ÿš€ Production Deployment + +### Recommendations + +1. **Use a process manager** (PM2, systemd) +2. **Set up reverse proxy** (nginx, Apache) +3. **Enable HTTPS** with SSL certificates +4. **Use Redis** for persistent storage instead of in-memory +5. **Set up monitoring** (Prometheus, Grafana) +6. **Configure logging** (Winston, structured logs) +7. **Set resource limits** and health checks + +### Docker Deployment + +Create a `Dockerfile`: +```dockerfile +FROM node:18-alpine +WORKDIR /app +COPY package*.json ./ +RUN npm ci --only=production +COPY . . +EXPOSE 3000 +CMD ["npm", "start"] +``` + +### Environment-specific configs + +**Production `.env`:** +```bash +NODE_ENV=production +PORT=3000 +ALLOW_REDEEM_DOMAINS=ln.tips,getalby.com +DEFAULT_LIGHTNING_ADDRESS=admin@your-domain.com +RATE_LIMIT=200 +ALLOWED_ORIGINS=https://yourdomain.com +``` + +## ๐Ÿค Contributing + +1. Fork the repository +2. Create a feature branch +3. Make your changes +4. Add tests if applicable +5. Submit a pull request + +## ๐Ÿ“ License + +MIT License - see LICENSE file for details. + +## ๐Ÿ†˜ Support + +For issues and questions: +- Create an issue on GitHub +- Check the logs for detailed error messages +- Verify your environment configuration +- Test with the health endpoint first + +## ๐Ÿ”ฎ Roadmap + +- [ ] Add Redis/database persistence +- [ ] Implement webhook notifications +- [ ] Add batch redemption support +- [ ] Enhanced monitoring and metrics +- [ ] WebSocket real-time status updates +- [ ] Multi-mint support optimization \ No newline at end of file diff --git a/env.example b/env.example new file mode 100644 index 0000000..6a16e66 --- /dev/null +++ b/env.example @@ -0,0 +1,19 @@ +# Server Configuration +PORT=3000 +NODE_ENV=development + +# Security Configuration +ALLOW_REDEEM_DOMAINS=* +API_SECRET=your-secret-key-here + +# Default Lightning Address (used when no address is provided in redeem requests) +DEFAULT_LIGHTNING_ADDRESS=admin@your-domain.com + +# Rate Limiting (requests per minute) +RATE_LIMIT=30 + +# Logging +LOG_LEVEL=info + +# CORS Configuration +ALLOWED_ORIGINS=* \ No newline at end of file diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..125a54f --- /dev/null +++ b/package-lock.json @@ -0,0 +1,2860 @@ +{ + "name": "cashu-redeem-api", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "cashu-redeem-api", + "version": "1.0.0", + "license": "MIT", + "dependencies": { + "@cashu/cashu-ts": "^1.1.0", + "axios": "^1.7.7", + "cors": "^2.8.5", + "dotenv": "^16.4.5", + "express": "^4.19.2", + "swagger-jsdoc": "^6.2.8", + "swagger-ui-express": "^5.0.1", + "uuid": "^10.0.0" + }, + "devDependencies": { + "eslint": "^9.9.1", + "nodemon": "^3.1.4" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + } + }, + "node_modules/@apidevtools/json-schema-ref-parser": { + "version": "9.1.2", + "resolved": "https://registry.npmjs.org/@apidevtools/json-schema-ref-parser/-/json-schema-ref-parser-9.1.2.tgz", + "integrity": "sha512-r1w81DpR+KyRWd3f+rk6TNqMgedmAxZP5v5KWlXQWlgMUUtyEJch0DKEci1SorPMiSeM8XPl7MZ3miJ60JIpQg==", + "license": "MIT", + "dependencies": { + "@jsdevtools/ono": "^7.1.3", + "@types/json-schema": "^7.0.6", + "call-me-maybe": "^1.0.1", + "js-yaml": "^4.1.0" + } + }, + "node_modules/@apidevtools/openapi-schemas": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@apidevtools/openapi-schemas/-/openapi-schemas-2.1.0.tgz", + "integrity": "sha512-Zc1AlqrJlX3SlpupFGpiLi2EbteyP7fXmUOGup6/DnkRgjP9bgMM/ag+n91rsv0U1Gpz0H3VILA/o3bW7Ua6BQ==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/@apidevtools/swagger-methods": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@apidevtools/swagger-methods/-/swagger-methods-3.0.2.tgz", + "integrity": "sha512-QAkD5kK2b1WfjDS/UQn/qQkbwF31uqRjPTrsCs5ZG9BQGAkjwvqGFjjPqAuzac/IYzpPtRzjCP1WrTuAIjMrXg==", + "license": "MIT" + }, + "node_modules/@apidevtools/swagger-parser": { + "version": "10.0.3", + "resolved": "https://registry.npmjs.org/@apidevtools/swagger-parser/-/swagger-parser-10.0.3.tgz", + "integrity": "sha512-sNiLY51vZOmSPFZA5TF35KZ2HbgYklQnTSDnkghamzLb3EkNtcQnrBQEj5AOCxHpTtXpqMCRM1CrmV2rG6nw4g==", + "license": "MIT", + "dependencies": { + "@apidevtools/json-schema-ref-parser": "^9.0.6", + "@apidevtools/openapi-schemas": "^2.0.4", + "@apidevtools/swagger-methods": "^3.0.2", + "@jsdevtools/ono": "^7.1.3", + "call-me-maybe": "^1.0.1", + "z-schema": "^5.0.1" + }, + "peerDependencies": { + "openapi-types": ">=7" + } + }, + "node_modules/@cashu/cashu-ts": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@cashu/cashu-ts/-/cashu-ts-1.2.1.tgz", + "integrity": "sha512-B0+e02S8DQA8KBt2FgKHgGYvtHQokXJ3sZcTyAdHqvb0T0jfo1zF7nHn19eU9iYcfk8VSWf5xNBTocpTfj1aNg==", + "license": "MIT", + "dependencies": { + "@cashu/crypto": "^0.2.7", + "@noble/curves": "^1.3.0", + "@noble/hashes": "^1.3.3", + "@scure/bip32": "^1.3.3", + "@scure/bip39": "^1.2.2", + "buffer": "^6.0.3" + } + }, + "node_modules/@cashu/crypto": { + "version": "0.2.7", + "resolved": "https://registry.npmjs.org/@cashu/crypto/-/crypto-0.2.7.tgz", + "integrity": "sha512-1aaDfUjiHNXoJqg8nW+341TLWV9W28DsVNXJUKcHL0yAmwLs5+56SSnb8LLDJzPamLVoYL0U0bda91klAzptig==", + "license": "MIT", + "dependencies": { + "@noble/curves": "^1.3.0", + "@noble/hashes": "^1.3.3", + "@scure/bip32": "^1.3.3", + "@scure/bip39": "^1.2.2", + "buffer": "^6.0.3" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.7.0.tgz", + "integrity": "sha512-dyybb3AcajC7uha6CvhdVRJqaKyn7w2YKqKyAN37NKYgZT36w+iRb0Dymmc5qEJ549c/S31cMMSFd75bteCpCw==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.12.1", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.1.tgz", + "integrity": "sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/config-array": { + "version": "0.20.0", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.20.0.tgz", + "integrity": "sha512-fxlS1kkIjx8+vy2SjuCB94q3htSNrufYTXubwiBFeaQHbH6Ipi43gFJq2zCMt6PHhImH3Xmr0NksKDvchWlpQQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/object-schema": "^2.1.6", + "debug": "^4.3.1", + "minimatch": "^3.1.2" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/config-array/node_modules/debug": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", + "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/@eslint/config-array/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/@eslint/config-helpers": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.2.2.tgz", + "integrity": "sha512-+GPzk8PlG0sPpzdU5ZvIRMPidzAnZDl/s9L+y13iodqvb8leL53bTannOrQ/Im7UkpsmFU5Ily5U60LWixnmLg==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/core": { + "version": "0.14.0", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.14.0.tgz", + "integrity": "sha512-qIbV0/JZr7iSDjqAc60IqbLdsj9GDt16xQtWD+B78d/HAlvysGdZZ6rpJHGAc2T0FQx1X6thsSPdnoiGKdNtdg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.1.tgz", + "integrity": "sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^10.0.1", + "globals": "^14.0.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.0", + "minimatch": "^3.1.2", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/eslintrc/node_modules/debug": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", + "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/@eslint/eslintrc/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/@eslint/js": { + "version": "9.28.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.28.0.tgz", + "integrity": "sha512-fnqSjGWd/CoIp4EXIxWVK/sHA6DOHN4+8Ix2cX5ycOY7LG0UY8nHCU5pIp2eaE1Mc7Qd8kHspYNzYXT2ojPLzg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + } + }, + "node_modules/@eslint/object-schema": { + "version": "2.1.6", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.6.tgz", + "integrity": "sha512-RBMg5FRL0I0gs51M/guSAj5/e14VQ4tpZnQNWwuDT66P14I43ItmPfIZRhO9fUVIPOAQXU47atlywZ/czoqFPA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/plugin-kit": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.3.1.tgz", + "integrity": "sha512-0J+zgWxHN+xXONWIyPWKFMgVuJoZuGiIFu8yxk7RJjxkzpGmyja5wRFqZIVtjDVOQpV+Rw0iOAjYPE2eQyjr0w==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.14.0", + "levn": "^0.4.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@humanfs/core": { + "version": "0.19.1", + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", + "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node": { + "version": "0.16.6", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.6.tgz", + "integrity": "sha512-YuI2ZHQL78Q5HbhDiBA1X4LmYdXCKCMQIfw0pw7piHJwyREFebJUvrQN4cMssyES6x+vfUbx1CIpaQUKYdQZOw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/core": "^0.19.1", + "@humanwhocodes/retry": "^0.3.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node/node_modules/@humanwhocodes/retry": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.3.1.tgz", + "integrity": "sha512-JBxkERygn7Bv/GbN5Rv8Ul6LVknS+5Bp6RgDC/O8gEBU/yeH5Ui5C/OlWrTb6qct7LjjfT6Re2NxB0ln0yYybA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/retry": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", + "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@jsdevtools/ono": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/@jsdevtools/ono/-/ono-7.1.3.tgz", + "integrity": "sha512-4JQNk+3mVzK3xh2rqd6RB4J46qUR19azEHBneZyTZM+c456qOrbbM/5xcR8huNCCcbVt7+UmizG6GuUvPvKUYg==", + "license": "MIT" + }, + "node_modules/@noble/curves": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.9.1.tgz", + "integrity": "sha512-k11yZxZg+t+gWvBbIswW0yoJlu8cHOC7dhunwOzoWH/mXGBiYyR4YY6hAEK/3EUs4UpB8la1RfdRpeGsFHkWsA==", + "license": "MIT", + "dependencies": { + "@noble/hashes": "1.8.0" + }, + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@noble/hashes": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz", + "integrity": "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==", + "license": "MIT", + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@scarf/scarf": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@scarf/scarf/-/scarf-1.4.0.tgz", + "integrity": "sha512-xxeapPiUXdZAE3che6f3xogoJPeZgig6omHEy1rIY5WVsB3H2BHNnZH+gHG6x91SCWyQCzWGsuL2Hh3ClO5/qQ==", + "hasInstallScript": true, + "license": "Apache-2.0" + }, + "node_modules/@scure/base": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@scure/base/-/base-1.2.6.tgz", + "integrity": "sha512-g/nm5FgUa//MCj1gV09zTJTaM6KBAHqLN907YVQqf7zC49+DcO4B1so4ZX07Ef10Twr6nuqYEH9GEggFXA4Fmg==", + "license": "MIT", + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@scure/bip32": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/@scure/bip32/-/bip32-1.7.0.tgz", + "integrity": "sha512-E4FFX/N3f4B80AKWp5dP6ow+flD1LQZo/w8UnLGYZO674jS6YnYeepycOOksv+vLPSpgN35wgKgy+ybfTb2SMw==", + "license": "MIT", + "dependencies": { + "@noble/curves": "~1.9.0", + "@noble/hashes": "~1.8.0", + "@scure/base": "~1.2.5" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@scure/bip39": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@scure/bip39/-/bip39-1.6.0.tgz", + "integrity": "sha512-+lF0BbLiJNwVlev4eKelw1WWLaiKXw7sSl8T6FvBlWkdX+94aGJ4o8XjUdlyhTCjd8c+B3KT3JfS8P0bLRNU6A==", + "license": "MIT", + "dependencies": { + "@noble/hashes": "~1.8.0", + "@scure/base": "~1.2.5" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@types/estree": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.7.tgz", + "integrity": "sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "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/acorn": { + "version": "8.14.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.1.tgz", + "integrity": "sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "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==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "license": "ISC", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "license": "Python-2.0" + }, + "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/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "license": "MIT" + }, + "node_modules/axios": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.9.0.tgz", + "integrity": "sha512-re4CqKTJaURpzbLHtIi6XpDv20/CnpXOtjRY5/CU32L8gU8ek9UIivcfvSWvmKEngmVbrUtPpdDwWDWL7DNHvg==", + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.15.6", + "form-data": "^4.0.0", + "proxy-from-env": "^1.1.0" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "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/binary-extensions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/body-parser": { + "version": "1.20.3", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz", + "integrity": "sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==", + "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.0", + "iconv-lite": "0.4.24", + "on-finished": "2.4.1", + "qs": "6.13.0", + "raw-body": "2.5.2", + "type-is": "~1.6.18", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/buffer": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" + } + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/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/call-me-maybe": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-me-maybe/-/call-me-maybe-1.0.2.tgz", + "integrity": "sha512-HpX65o1Hnr9HH25ojC1YGs7HCQLq0GCOibSaWER0eNpgJ/Z1MZv2mTc7+xh6WOPxbRVcmgbv4hGU+uSQ/2xFZQ==", + "license": "MIT" + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/chokidar/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "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==", + "dev": true, + "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==", + "dev": true, + "license": "MIT" + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/commander": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-6.2.0.tgz", + "integrity": "sha512-zP4jEKbe8SHzKJYQmq8Y9gYjtO/POJLgIdKgV7B9qNmABVFVc+ctqSX6iXh4mCpJfRBOabiZ2YKPg8ciDw6C+Q==", + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "license": "MIT" + }, + "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.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.1.tgz", + "integrity": "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", + "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==", + "license": "MIT" + }, + "node_modules/cors": { + "version": "2.8.5", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", + "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==", + "license": "MIT", + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "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/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "license": "MIT", + "engines": { + "node": ">=0.4.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/doctrine": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", + "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", + "license": "Apache-2.0", + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/dotenv": { + "version": "16.5.0", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.5.0.tgz", + "integrity": "sha512-m/C+AwOAr9/W1UOIZUo232ejMNnJAJtYQjUbHoNTBNTJSvqzzDh7vnrei3o3r3m9blf6ZoDkvcw0VmozNRFJxg==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/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/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/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "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/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "9.28.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.28.0.tgz", + "integrity": "sha512-ocgh41VhRlf9+fVpe7QKzwLj9c92fDiqOj8Y3Sd4/ZmVA4Btx4PlUYPq4pp9JDyupkf1upbEXecxL2mwNV7jPQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.2.0", + "@eslint-community/regexpp": "^4.12.1", + "@eslint/config-array": "^0.20.0", + "@eslint/config-helpers": "^0.2.1", + "@eslint/core": "^0.14.0", + "@eslint/eslintrc": "^3.3.1", + "@eslint/js": "9.28.0", + "@eslint/plugin-kit": "^0.3.1", + "@humanfs/node": "^0.16.6", + "@humanwhocodes/module-importer": "^1.0.1", + "@humanwhocodes/retry": "^0.4.2", + "@types/estree": "^1.0.6", + "@types/json-schema": "^7.0.15", + "ajv": "^6.12.4", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.6", + "debug": "^4.3.2", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^8.3.0", + "eslint-visitor-keys": "^4.2.0", + "espree": "^10.3.0", + "esquery": "^1.5.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^8.0.0", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.2", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "jiti": "*" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + } + } + }, + "node_modules/eslint-scope": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.3.0.tgz", + "integrity": "sha512-pUNxi75F8MJ/GdeKtVLSbYg4ZI34J6C0C7sbL4YOp2exGwen7ZsuBqKzUhXd0qMQ362yET3z+uPwKeg/0C2XCQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.0.tgz", + "integrity": "sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint/node_modules/debug": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", + "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/eslint/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/espree": { + "version": "10.3.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-10.3.0.tgz", + "integrity": "sha512-0QYC8b24HWY8zjRnDTL6RiHfDbAWn63qb4LMj1Z4b076A4une81+z03Kg7l7mn/48PUTqoLptSXez8oknU8Clg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.14.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^4.2.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esquery": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz", + "integrity": "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "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/express": { + "version": "4.21.2", + "resolved": "https://registry.npmjs.org/express/-/express-4.21.2.tgz", + "integrity": "sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==", + "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.13.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/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true, + "license": "MIT" + }, + "node_modules/file-entry-cache": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", + "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^4.0.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/finalhandler": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.1.tgz", + "integrity": "sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==", + "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.1", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat-cache": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", + "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.4" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/flatted": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz", + "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==", + "dev": true, + "license": "ISC" + }, + "node_modules/follow-redirects": { + "version": "1.15.9", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz", + "integrity": "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/form-data": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.2.tgz", + "integrity": "sha512-hGfm/slu0ZabnNt4oaRZ6uREyfCj6P4fT/n6A1rGV+Z0VdGXjfOhVUpkn6qVQONHGIFwmveGXyDs75+nr6FM8w==", + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "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.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "license": "ISC" + }, + "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/glob": { + "version": "7.1.6", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz", + "integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/globals": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", + "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "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/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "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.0", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", + "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", + "license": "MIT", + "dependencies": { + "depd": "2.0.0", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "toidentifier": "1.0.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/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/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/ignore-by-default": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/ignore-by-default/-/ignore-by-default-1.0.1.tgz", + "integrity": "sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA==", + "dev": true, + "license": "ISC" + }, + "node_modules/import-fresh": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", + "license": "ISC", + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "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/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/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "license": "MIT", + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lodash.get": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz", + "integrity": "sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ==", + "deprecated": "This package is deprecated. Use the optional chaining (?.) operator instead.", + "license": "MIT" + }, + "node_modules/lodash.isequal": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz", + "integrity": "sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ==", + "deprecated": "This package is deprecated. Use require('node:util').isDeepStrictEqual instead.", + "license": "MIT" + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.mergewith": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.mergewith/-/lodash.mergewith-4.6.2.tgz", + "integrity": "sha512-GK3g5RPZWTRSeLSpgP8Xhra+pnjBC56q9FZYe1d5RN3TJ35dbkGy3YqBSMbyCrlbi+CM9Z3Jk5yTL7RCsqboyQ==", + "license": "MIT" + }, + "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/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "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/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true, + "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/nodemon": { + "version": "3.1.10", + "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-3.1.10.tgz", + "integrity": "sha512-WDjw3pJ0/0jMFmyNDp3gvY2YizjLmmOUQo6DEBY+JgdvW/yQ9mEeSw6H5ythl5Ny2ytb7f9C2nIbjSxMNzbJXw==", + "dev": true, + "license": "MIT", + "dependencies": { + "chokidar": "^3.5.2", + "debug": "^4", + "ignore-by-default": "^1.0.1", + "minimatch": "^3.1.2", + "pstree.remy": "^1.1.8", + "semver": "^7.5.3", + "simple-update-notifier": "^2.0.0", + "supports-color": "^5.5.0", + "touch": "^3.1.0", + "undefsafe": "^2.0.5" + }, + "bin": { + "nodemon": "bin/nodemon.js" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/nodemon" + } + }, + "node_modules/nodemon/node_modules/debug": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", + "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/nodemon/node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/nodemon/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/nodemon/node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "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/openapi-types": { + "version": "12.1.3", + "resolved": "https://registry.npmjs.org/openapi-types/-/openapi-types-12.1.3.tgz", + "integrity": "sha512-N4YtSYJqghVu4iek2ZUvcN/0aqH1kRDuNqzcycDxhOUpg7GdvLa2F3DgS6yBNhInhv2r/6I0Flkn7CqL8+nIcw==", + "license": "MIT", + "peer": true + }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "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-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=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/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "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/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "license": "MIT" + }, + "node_modules/pstree.remy": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/pstree.remy/-/pstree.remy-1.1.8.tgz", + "integrity": "sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w==", + "dev": true, + "license": "MIT" + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/qs": { + "version": "6.13.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz", + "integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==", + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.0.6" + }, + "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.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz", + "integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==", + "license": "MIT", + "dependencies": { + "bytes": "3.1.2", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "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.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", + "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/send": { + "version": "0.19.0", + "resolved": "https://registry.npmjs.org/send/-/send-0.19.0.tgz", + "integrity": "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "mime": "1.6.0", + "ms": "2.1.3", + "on-finished": "2.4.1", + "range-parser": "~1.2.1", + "statuses": "2.0.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/send/node_modules/encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "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.2", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.2.tgz", + "integrity": "sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==", + "license": "MIT", + "dependencies": { + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "0.19.0" + }, + "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/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "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-update-notifier": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-2.0.0.tgz", + "integrity": "sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/swagger-jsdoc": { + "version": "6.2.8", + "resolved": "https://registry.npmjs.org/swagger-jsdoc/-/swagger-jsdoc-6.2.8.tgz", + "integrity": "sha512-VPvil1+JRpmJ55CgAtn8DIcpBs0bL5L3q5bVQvF4tAW/k/9JYSj7dCpaYCAv5rufe0vcCbBRQXGvzpkWjvLklQ==", + "license": "MIT", + "dependencies": { + "commander": "6.2.0", + "doctrine": "3.0.0", + "glob": "7.1.6", + "lodash.mergewith": "^4.6.2", + "swagger-parser": "^10.0.3", + "yaml": "2.0.0-1" + }, + "bin": { + "swagger-jsdoc": "bin/swagger-jsdoc.js" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/swagger-parser": { + "version": "10.0.3", + "resolved": "https://registry.npmjs.org/swagger-parser/-/swagger-parser-10.0.3.tgz", + "integrity": "sha512-nF7oMeL4KypldrQhac8RyHerJeGPD1p2xDh900GPvc+Nk7nWP6jX2FcC7WmkinMoAmoO774+AFXcWsW8gMWEIg==", + "license": "MIT", + "dependencies": { + "@apidevtools/swagger-parser": "10.0.3" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/swagger-ui-dist": { + "version": "5.22.0", + "resolved": "https://registry.npmjs.org/swagger-ui-dist/-/swagger-ui-dist-5.22.0.tgz", + "integrity": "sha512-8YlCSxiyb8uPFa7qoB1lRHYr1PBbT1NuV9RvQdFFPFPudRBTPf9coU5jl02KhzvrtmTEw4jXRgb0kg8pJvVuWQ==", + "license": "Apache-2.0", + "dependencies": { + "@scarf/scarf": "=1.4.0" + } + }, + "node_modules/swagger-ui-express": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/swagger-ui-express/-/swagger-ui-express-5.0.1.tgz", + "integrity": "sha512-SrNU3RiBGTLLmFU8GIJdOdanJTl4TOmT27tt3bWWHppqYmAZ6IDuEuBvMU6nZq0zLEe6b/1rACXCgLZqO6ZfrA==", + "license": "MIT", + "dependencies": { + "swagger-ui-dist": ">=5.0.0" + }, + "engines": { + "node": ">= v0.10.32" + }, + "peerDependencies": { + "express": ">=4.0.0 || >=5.0.0-beta" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "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/touch": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/touch/-/touch-3.1.1.tgz", + "integrity": "sha512-r0eojU4bI8MnHr8c5bNo7lJDdI2qXlWWJk6a9EAFG7vbhTjElYhBVS3/miuE0uOuoLdb8Mc/rVfsmm6eo5o9GA==", + "dev": true, + "license": "ISC", + "bin": { + "nodetouch": "bin/nodetouch.js" + } + }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "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/undefsafe": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.5.tgz", + "integrity": "sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA==", + "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/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "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/validator": { + "version": "13.15.15", + "resolved": "https://registry.npmjs.org/validator/-/validator-13.15.15.tgz", + "integrity": "sha512-BgWVbCI72aIQy937xbawcs+hrVaN/CZ2UwutgaJ36hGqRrLNM+f5LUT/YPRbo8IV/ASeFzXszezV+y2+rq3l8A==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "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/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "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/yaml": { + "version": "2.0.0-1", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.0.0-1.tgz", + "integrity": "sha512-W7h5dEhywMKenDJh2iX/LABkbFnBxasD27oyXWDS/feDsxiw0dD5ncXdYXgkvAsXIY2MpW/ZKkr9IU30DBdMNQ==", + "license": "ISC", + "engines": { + "node": ">= 6" + } + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/z-schema": { + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/z-schema/-/z-schema-5.0.5.tgz", + "integrity": "sha512-D7eujBWkLa3p2sIpJA0d1pr7es+a7m0vFAnZLlCEKq/Ij2k0MLi9Br2UPxoxdYystm5K1yeBGzub0FlYUEWj2Q==", + "license": "MIT", + "dependencies": { + "lodash.get": "^4.4.2", + "lodash.isequal": "^4.5.0", + "validator": "^13.7.0" + }, + "bin": { + "z-schema": "bin/z-schema" + }, + "engines": { + "node": ">=8.0.0" + }, + "optionalDependencies": { + "commander": "^9.4.1" + } + }, + "node_modules/z-schema/node_modules/commander": { + "version": "9.5.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-9.5.0.tgz", + "integrity": "sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ==", + "license": "MIT", + "optional": true, + "engines": { + "node": "^12.20.0 || >=14" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..09ae851 --- /dev/null +++ b/package.json @@ -0,0 +1,52 @@ +{ + "name": "cashu-redeem-api", + "version": "1.0.0", + "description": "Redeem ecash (Cashu tokens) to Lightning Address using cashu-ts library and LNURLp", + "main": "server.js", + "scripts": { + "start": "node server.js", + "dev": "nodemon server.js", + "test": "echo \"Error: no test specified\" && exit 1", + "lint": "eslint .", + "lint:fix": "eslint . --fix" + }, + "keywords": [ + "cashu", + "lightning", + "bitcoin", + "ecash", + "api", + "lnurl", + "lnurlp", + "mint", + "satoshi" + ], + "author": "", + "license": "MIT", + "dependencies": { + "@cashu/cashu-ts": "^1.1.0", + "axios": "^1.7.7", + "cors": "^2.8.5", + "dotenv": "^16.4.5", + "express": "^4.19.2", + "swagger-jsdoc": "^6.2.8", + "swagger-ui-express": "^5.0.1", + "uuid": "^10.0.0" + }, + "devDependencies": { + "nodemon": "^3.1.4", + "eslint": "^9.9.1" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/yourusername/cashu-redeem-api.git" + }, + "bugs": { + "url": "https://github.com/yourusername/cashu-redeem-api/issues" + }, + "homepage": "https://github.com/yourusername/cashu-redeem-api#readme" +} diff --git a/server.js b/server.js new file mode 100644 index 0000000..318f39d --- /dev/null +++ b/server.js @@ -0,0 +1,617 @@ +require('dotenv').config(); +const express = require('express'); +const cors = require('cors'); +const swaggerUi = require('swagger-ui-express'); +const swaggerSpecs = require('./swagger.config'); +const cashuService = require('./services/cashu'); +const lightningService = require('./services/lightning'); +const redemptionService = require('./services/redemption'); + +const app = express(); +const PORT = process.env.PORT || 3000; + +// Middleware +app.use(express.json({ limit: '10mb' })); +app.use(cors({ + origin: process.env.ALLOWED_ORIGINS + ? process.env.ALLOWED_ORIGINS.split(',').map(o => o.trim()) + : ['http://localhost:3000'], + methods: ['GET', 'POST'], + allowedHeaders: ['Content-Type', 'Authorization'] +})); + +// Swagger Documentation +app.use('/docs', swaggerUi.serve, swaggerUi.setup(swaggerSpecs, { + customCss: '.swagger-ui .topbar { display: none }', + customSiteTitle: 'Cashu Redeem API Documentation', + swaggerOptions: { + filter: true, + showRequestHeaders: true + } +})); + +// Basic rate limiting (simple in-memory implementation) +const rateLimitMap = new Map(); +const RATE_LIMIT = parseInt(process.env.RATE_LIMIT) || 100; // requests per minute + +function rateLimit(req, res, next) { + const clientId = req.ip || req.connection.remoteAddress; + const now = Date.now(); + const windowStart = now - 60000; // 1 minute window + + if (!rateLimitMap.has(clientId)) { + rateLimitMap.set(clientId, []); + } + + const requests = rateLimitMap.get(clientId); + + // Remove old requests outside the window + const validRequests = requests.filter(time => time > windowStart); + rateLimitMap.set(clientId, validRequests); + + if (validRequests.length >= RATE_LIMIT) { + return res.status(429).json({ + success: false, + error: 'Rate limit exceeded. Please try again later.' + }); + } + + validRequests.push(now); + next(); +} + +// Apply rate limiting to all routes +app.use(rateLimit); + +// Request logging middleware +app.use((req, res, next) => { + const timestamp = new Date().toISOString(); + console.log(`${timestamp} - ${req.method} ${req.path} - ${req.ip}`); + next(); +}); + +// Error handling middleware +function asyncHandler(fn) { + return (req, res, next) => { + Promise.resolve(fn(req, res, next)).catch(next); + }; +} + +// API Routes + +/** + * @swagger + * /api/decode: + * post: + * summary: Decode a Cashu token + * description: Decode a Cashu token and return its content. Supports both v1 and v3 token formats. + * tags: [Token Operations] + * requestBody: + * required: true + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/DecodeRequest' + * responses: + * 200: + * description: Token decoded successfully + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/DecodeResponse' + * 400: + * $ref: '#/components/responses/BadRequest' + * 429: + * $ref: '#/components/responses/TooManyRequests' + * 500: + * $ref: '#/components/responses/InternalServerError' + */ +app.post('/api/decode', asyncHandler(async (req, res) => { + const { token } = req.body; + + if (!token) { + return res.status(400).json({ + success: false, + error: 'Token is required' + }); + } + + try { + // Validate token format first + if (!cashuService.isValidTokenFormat(token)) { + return res.status(400).json({ + success: false, + error: 'Invalid token format. Must be a valid Cashu token' + }); + } + + const decoded = await cashuService.parseToken(token); + const mintUrl = await cashuService.getTokenMintUrl(token); + + res.json({ + success: true, + decoded: { + mint: decoded.mint, + totalAmount: decoded.totalAmount, + numProofs: decoded.numProofs, + denominations: decoded.denominations, + format: decoded.format + }, + mint_url: mintUrl + }); + } catch (error) { + res.status(400).json({ + success: false, + error: error.message + }); + } +})); + +/** + * @swagger + * /api/redeem: + * post: + * summary: Redeem a Cashu token to Lightning address + * description: | + * Redeem a Cashu token to a Lightning address (optional - uses default if not provided). + * + * The redemption process includes: + * 1. Token validation and parsing + * 2. Spendability checking at the mint + * 3. Lightning address resolution via LNURLp + * 4. Token melting and Lightning payment + * + * Fee calculation follows NUT-05 specification (2% of amount, minimum 1 satoshi). + * tags: [Token Operations] + * requestBody: + * required: true + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/RedeemRequest' + * responses: + * 200: + * description: Token redeemed successfully + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/RedeemResponse' + * 400: + * $ref: '#/components/responses/BadRequest' + * 429: + * $ref: '#/components/responses/TooManyRequests' + * 500: + * $ref: '#/components/responses/InternalServerError' + */ +app.post('/api/redeem', asyncHandler(async (req, res) => { + const { token, lightningAddress } = req.body; + + // Validate request (lightningAddress is now optional) + const validation = await redemptionService.validateRedemptionRequest(token, lightningAddress); + + if (!validation.valid) { + return res.status(400).json({ + success: false, + error: validation.errors.join(', ') + }); + } + + // Perform redemption + try { + const result = await redemptionService.performRedemption(token, lightningAddress); + + if (result.success) { + const response = { + success: true, + redeemId: result.redeemId, + paid: result.paid, + amount: result.amount, + to: result.to, + fee: result.fee, + actualFee: result.actualFee, + netAmount: result.netAmount, + mint_url: result.mint, + format: result.format + }; + + // Include info about whether default address was used + if (result.usingDefaultAddress) { + response.usingDefaultAddress = true; + response.message = `Redeemed to default Lightning address: ${result.to}`; + } + + // Include preimage if available + if (result.preimage) { + response.preimage = result.preimage; + } + + // Include change if any + if (result.change && result.change.length > 0) { + response.change = result.change; + } + + res.json(response); + } else { + res.status(400).json({ + success: false, + error: result.error, + redeemId: result.redeemId + }); + } + } catch (error) { + console.error('Error in redemption:', error); + res.status(500).json({ + success: false, + error: 'Internal server error during redemption' + }); + } +})); + +/** + * @swagger + * /api/status: + * post: + * summary: Check redemption status by redeemId + * description: Check the current status of a redemption using its unique ID. + * tags: [Status & Monitoring] + * requestBody: + * required: true + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/StatusRequest' + * responses: + * 200: + * description: Status retrieved successfully + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/StatusResponse' + * 400: + * $ref: '#/components/responses/BadRequest' + * 404: + * $ref: '#/components/responses/NotFound' + * 429: + * $ref: '#/components/responses/TooManyRequests' + */ +app.post('/api/status', asyncHandler(async (req, res) => { + const { redeemId } = req.body; + + if (!redeemId) { + return res.status(400).json({ + success: false, + error: 'redeemId is required' + }); + } + + const status = redemptionService.getRedemptionStatus(redeemId); + + if (!status) { + return res.status(404).json({ + success: false, + error: 'Redemption not found' + }); + } + + res.json(status); +})); + +/** + * @swagger + * /api/status/{redeemId}: + * get: + * summary: Check redemption status via URL parameter + * description: Same as POST /api/status but uses URL parameter - useful for frontend polling. + * tags: [Status & Monitoring] + * parameters: + * - in: path + * name: redeemId + * required: true + * description: Unique redemption ID to check status for + * schema: + * type: string + * format: uuid + * example: '8e99101e-d034-4d2e-9ccf-dfda24d26762' + * responses: + * 200: + * description: Status retrieved successfully + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/StatusResponse' + * 404: + * $ref: '#/components/responses/NotFound' + * 429: + * $ref: '#/components/responses/TooManyRequests' + */ +app.get('/api/status/:redeemId', asyncHandler(async (req, res) => { + const { redeemId } = req.params; + + const status = redemptionService.getRedemptionStatus(redeemId); + + if (!status) { + return res.status(404).json({ + success: false, + error: 'Redemption not found' + }); + } + + res.json(status); +})); + +/** + * @swagger + * /api/health: + * get: + * summary: Health check endpoint + * description: | + * Check the health and status of the API server. + * Returns server information including uptime, memory usage, and version. + * tags: [Status & Monitoring] + * responses: + * 200: + * description: Server is healthy + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/HealthResponse' + */ +app.get('/api/health', (req, res) => { + res.json({ + status: 'ok', + timestamp: new Date().toISOString(), + uptime: process.uptime(), + memory: process.memoryUsage(), + version: require('./package.json').version + }); +}); + +/** + * @swagger + * /api/stats: + * get: + * summary: Get redemption statistics + * description: | + * Get comprehensive statistics about redemptions (admin endpoint). + * Returns information about total redemptions, success rates, amounts, and fees. + * + * **Note**: In production, this endpoint should be protected with authentication. + * tags: [Status & Monitoring] + * responses: + * 200: + * description: Statistics retrieved successfully + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/StatsResponse' + * 429: + * $ref: '#/components/responses/TooManyRequests' + */ +app.get('/api/stats', asyncHandler(async (req, res) => { + // In production, add authentication here + const stats = redemptionService.getStats(); + + res.json({ + success: true, + stats + }); +})); + +/** + * @swagger + * /api/validate-address: + * post: + * summary: Validate a Lightning address + * description: | + * Validate a Lightning address without performing a redemption. + * Checks format validity and tests LNURLp resolution. + * + * Returns information about the Lightning address capabilities + * including min/max sendable amounts and comment allowance. + * tags: [Validation] + * requestBody: + * required: true + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/ValidateAddressRequest' + * responses: + * 200: + * description: Validation completed (check 'valid' field for result) + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/ValidateAddressResponse' + * 400: + * $ref: '#/components/responses/BadRequest' + * 429: + * $ref: '#/components/responses/TooManyRequests' + */ +app.post('/api/validate-address', asyncHandler(async (req, res) => { + const { lightningAddress } = req.body; + + if (!lightningAddress) { + return res.status(400).json({ + success: false, + error: 'Lightning address is required' + }); + } + + try { + const isValid = lightningService.validateLightningAddress(lightningAddress); + + if (!isValid) { + return res.json({ + success: false, + valid: false, + error: 'Invalid Lightning address format' + }); + } + + // Test resolution + const { domain } = lightningService.parseLightningAddress(lightningAddress); + const lnurlpUrl = lightningService.getLNURLpEndpoint(lightningAddress); + + try { + const lnurlpResponse = await lightningService.fetchLNURLpResponse(lnurlpUrl); + + res.json({ + success: true, + valid: true, + domain, + minSendable: lightningService.millisatsToSats(lnurlpResponse.minSendable), + maxSendable: lightningService.millisatsToSats(lnurlpResponse.maxSendable), + commentAllowed: lnurlpResponse.commentAllowed || 0 + }); + } catch (error) { + res.json({ + success: false, + valid: false, + error: `Lightning address resolution failed: ${error.message}` + }); + } + } catch (error) { + res.status(400).json({ + success: false, + valid: false, + error: error.message + }); + } +})); + +/** + * @swagger + * /api/check-spendable: + * post: + * summary: Check if Cashu token is spendable + * description: | + * Check if a Cashu token is spendable at its mint before attempting redemption. + * This is a pre-validation step that can save time and prevent failed redemptions. + * + * Returns an array indicating which proofs within the token are spendable, + * pending, or already spent. + * tags: [Validation] + * requestBody: + * required: true + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/CheckSpendableRequest' + * responses: + * 200: + * description: Spendability check completed + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/CheckSpendableResponse' + * 400: + * $ref: '#/components/responses/BadRequest' + * 429: + * $ref: '#/components/responses/TooManyRequests' + */ +app.post('/api/check-spendable', asyncHandler(async (req, res) => { + const { token } = req.body; + + if (!token) { + return res.status(400).json({ + success: false, + error: 'Token is required' + }); + } + + try { + // Validate token format first + if (!cashuService.isValidTokenFormat(token)) { + return res.status(400).json({ + success: false, + error: 'Invalid token format. Must be a valid Cashu token' + }); + } + + const spendabilityCheck = await cashuService.checkTokenSpendable(token); + + res.json({ + success: true, + spendable: spendabilityCheck.spendable, + pending: spendabilityCheck.pending, + mintUrl: spendabilityCheck.mintUrl, + totalAmount: spendabilityCheck.totalAmount, + message: spendabilityCheck.spendable && spendabilityCheck.spendable.length > 0 + ? 'Token is spendable' + : 'Token proofs are not spendable - may have already been used' + }); + } catch (error) { + res.status(400).json({ + success: false, + error: error.message + }); + } +})); + +// 404 handler +app.use('*', (req, res) => { + res.status(404).json({ + success: false, + error: 'Endpoint not found' + }); +}); + +// Global error handler +app.use((error, req, res, next) => { + console.error('Global error handler:', error); + + if (error.type === 'entity.parse.failed') { + return res.status(400).json({ + success: false, + error: 'Invalid JSON payload' + }); + } + + res.status(500).json({ + success: false, + error: 'Internal server error' + }); +}); + +// Cleanup old redemptions periodically (every hour) +setInterval(() => { + try { + redemptionService.cleanupOldRedemptions(); + console.log('Cleaned up old redemptions'); + } catch (error) { + console.error('Error cleaning up redemptions:', error); + } +}, 60 * 60 * 1000); // 1 hour + +// Graceful shutdown +process.on('SIGTERM', () => { + console.log('SIGTERM received, shutting down gracefully'); + process.exit(0); +}); + +process.on('SIGINT', () => { + console.log('SIGINT received, shutting down gracefully'); + process.exit(0); +}); + +// Start server +app.listen(PORT, () => { + console.log(`๐Ÿš€ Cashu Redeem API running on port ${PORT}`); + console.log(`๐Ÿ“– API Documentation: http://localhost:${PORT}/docs`); + console.log(`๐Ÿ“ Health check: http://localhost:${PORT}/api/health`); + console.log(`๐Ÿ”’ Environment: ${process.env.NODE_ENV || 'development'}`); + + if (process.env.ALLOW_REDEEM_DOMAINS) { + console.log(`๐ŸŒ Allowed domains: ${process.env.ALLOW_REDEEM_DOMAINS}`); + } else { + console.log('โš ๏ธ No domain restrictions (ALLOW_REDEEM_DOMAINS not set)'); + } + + if (process.env.DEFAULT_LIGHTNING_ADDRESS) { + console.log(`โšก Default Lightning address: ${process.env.DEFAULT_LIGHTNING_ADDRESS}`); + } else { + console.log('โš ๏ธ No default Lightning address configured - Lightning address will be required for redemptions'); + } +}); + +module.exports = app; \ No newline at end of file diff --git a/services/cashu.js b/services/cashu.js new file mode 100644 index 0000000..2161cf5 --- /dev/null +++ b/services/cashu.js @@ -0,0 +1,301 @@ +const { CashuMint, CashuWallet, getEncodedToken, getDecodedToken } = require('@cashu/cashu-ts'); + +class CashuService { + constructor() { + this.mints = new Map(); // Cache mint instances + this.wallets = new Map(); // Cache wallet instances + } + + /** + * Validate token format (supports both v1 and v3 formats) + * @param {string} token - The Cashu token + * @returns {boolean} Whether the token format is valid + */ + isValidTokenFormat(token) { + // Match both v1 and v3 token formats + return /^cashu[abAB][a-zA-Z0-9-_]+$/.test(token); + } + + /** + * Get token mint URL from decoded token + * @param {string} token - The encoded Cashu token + * @returns {string|null} Mint URL or null if not found + */ + async getTokenMintUrl(token) { + try { + const decoded = getDecodedToken(token); + if (!decoded) { + return null; + } + + // Handle both v1 and v3 token formats + if (decoded.mint) { + // v3 format + return decoded.mint; + } else if (decoded.token && decoded.token[0] && decoded.token[0].mint) { + // v1 format + return decoded.token[0].mint; + } + + return null; + } catch (error) { + console.error('Error getting token mint URL:', error); + return null; + } + } + + /** + * Decode token handling both v1 and v3 formats + * @param {string} token - The encoded Cashu token + * @returns {Object} Decoded token data + */ + async decodeTokenStructure(token) { + try { + const decoded = getDecodedToken(token); + if (!decoded) { + throw new Error('Failed to decode token'); + } + + // Handle both v1 and v3 token formats + if (decoded.proofs) { + // v3 format + return { + proofs: decoded.proofs, + mint: decoded.mint + }; + } else if (decoded.token && decoded.token[0]) { + // v1 format + return { + proofs: decoded.token[0].proofs, + mint: decoded.token[0].mint + }; + } + + throw new Error('Invalid token structure'); + } catch (error) { + throw new Error(`Token decoding failed: ${error.message}`); + } + } + + /** + * Calculate fee according to NUT-05 specification + * @param {number} amount - Amount in satoshis + * @returns {number} Fee amount + */ + calculateFee(amount) { + // Calculate 2% of the amount, rounded up + const fee = Math.ceil(amount * 0.02); + // Return the greater of 1 sat or the calculated fee + return Math.max(1, fee); + } + + /** + * Parse and validate a Cashu token + * @param {string} token - The encoded Cashu token + * @returns {Object} Parsed token data + */ + async parseToken(token) { + try { + if (!token || typeof token !== 'string') { + throw new Error('Invalid token format'); + } + + // Remove any whitespace and validate basic format + token = token.trim(); + + // Validate token format + if (!this.isValidTokenFormat(token)) { + throw new Error('Invalid token format. Must be a valid Cashu token'); + } + + // Decode token structure + const decoded = await this.decodeTokenStructure(token); + + if (!decoded.proofs || !Array.isArray(decoded.proofs) || decoded.proofs.length === 0) { + throw new Error('Invalid token structure - no proofs found'); + } + + // Calculate total amount + const totalAmount = decoded.proofs.reduce((sum, proof) => sum + (proof.amount || 0), 0); + + if (totalAmount <= 0) { + throw new Error('Token has no value'); + } + + const denominations = decoded.proofs.map(proof => proof.amount); + + return { + mint: decoded.mint, + totalAmount, + numProofs: decoded.proofs.length, + denominations, + proofs: decoded.proofs, + format: token.startsWith('cashuA') ? 'cashuA' : 'cashuB' + }; + } catch (error) { + throw new Error(`Token parsing failed: ${error.message}`); + } + } + + /** + * Get total amount from a token + * @param {string} token - The encoded Cashu token + * @returns {number} Total amount in satoshis + */ + async getTotalAmount(token) { + const parsed = await this.parseToken(token); + return parsed.totalAmount; + } + + /** + * Get or create a mint instance + * @param {string} mintUrl - The mint URL + * @returns {CashuMint} Mint instance + */ + async getMint(mintUrl) { + if (!this.mints.has(mintUrl)) { + try { + const mint = new CashuMint(mintUrl); + // Test connectivity + await mint.getInfo(); + this.mints.set(mintUrl, mint); + } catch (error) { + throw new Error(`Failed to connect to mint ${mintUrl}: ${error.message}`); + } + } + return this.mints.get(mintUrl); + } + + /** + * Get or create a wallet instance for a specific mint + * @param {string} mintUrl - The mint URL + * @returns {CashuWallet} Wallet instance + */ + async getWallet(mintUrl) { + if (!this.wallets.has(mintUrl)) { + try { + const mint = await this.getMint(mintUrl); + const wallet = new CashuWallet(mint); + this.wallets.set(mintUrl, wallet); + } catch (error) { + throw new Error(`Failed to create wallet for mint ${mintUrl}: ${error.message}`); + } + } + return this.wallets.get(mintUrl); + } + + /** + * Melt a Cashu token to pay a Lightning invoice + * @param {string} token - The encoded Cashu token + * @param {string} bolt11 - The Lightning invoice + * @returns {Object} Melt result + */ + async meltToken(token, bolt11) { + try { + const parsed = await this.parseToken(token); + const wallet = await this.getWallet(parsed.mint); + + // Get the decoded token structure + const decoded = await this.decodeTokenStructure(token); + const proofs = decoded.proofs; + + // Create melt quote to get fee estimate + const meltQuote = await wallet.createMeltQuote(bolt11); + + // Calculate expected fee + const expectedFee = this.calculateFee(parsed.totalAmount); + + // Check if we have sufficient funds including fees + const totalRequired = meltQuote.amount + meltQuote.fee_reserve; + if (totalRequired > parsed.totalAmount) { + throw new Error(`Insufficient funds. Required: ${totalRequired} sats (including ${meltQuote.fee_reserve} sats fee), Available: ${parsed.totalAmount} sats`); + } + + // Perform the melt operation using the quote and proofs + const meltResponse = await wallet.meltTokens(meltQuote, proofs); + + // Verify payment was successful + if (!meltResponse.paid) { + throw new Error('Payment failed - token melted but Lightning payment was not successful'); + } + + return { + success: true, + paid: meltResponse.paid, + preimage: meltResponse.payment_preimage, + change: meltResponse.change || [], + amount: meltQuote.amount, + fee: meltQuote.fee_reserve, + actualFee: expectedFee, + netAmount: parsed.totalAmount - meltQuote.fee_reserve, + quote: meltQuote.quote + }; + } catch (error) { + // Check if it's a cashu-ts specific error + if (error.message.includes('Insufficient funds') || + error.message.includes('Payment failed') || + error.message.includes('Quote not found')) { + throw error; // Re-throw specific cashu errors + } + throw new Error(`Melt operation failed: ${error.message}`); + } + } + + /** + * Validate if a token is properly formatted and has valid proofs + * @param {string} token - The encoded Cashu token + * @returns {boolean} Whether the token is valid + */ + async validateToken(token) { + try { + if (!this.isValidTokenFormat(token)) { + return false; + } + + const parsed = await this.parseToken(token); + return parsed.totalAmount > 0 && parsed.proofs.length > 0; + } catch (error) { + return false; + } + } + + /** + * Get mint info for a given mint URL + * @param {string} mintUrl - The mint URL + * @returns {Object} Mint information + */ + async getMintInfo(mintUrl) { + try { + const mint = await this.getMint(mintUrl); + return await mint.getInfo(); + } catch (error) { + throw new Error(`Failed to get mint info: ${error.message}`); + } + } + + /** + * Check if proofs are spendable at the mint + * @param {string} token - The encoded Cashu token + * @returns {Object} Spendability check result + */ + async checkTokenSpendable(token) { + try { + const parsed = await this.parseToken(token); + const mint = await this.getMint(parsed.mint); + + const secrets = parsed.proofs.map(proof => proof.secret); + const checkResult = await mint.check({ secrets }); + + return { + spendable: checkResult.spendable, + pending: checkResult.pending || [], + mintUrl: parsed.mint, + totalAmount: parsed.totalAmount + }; + } catch (error) { + throw new Error(`Failed to check token spendability: ${error.message}`); + } + } +} + +module.exports = new CashuService(); \ No newline at end of file diff --git a/services/lightning.js b/services/lightning.js new file mode 100644 index 0000000..2781f7e --- /dev/null +++ b/services/lightning.js @@ -0,0 +1,276 @@ +const axios = require('axios'); + +class LightningService { + constructor() { + this.allowedDomains = process.env.ALLOW_REDEEM_DOMAINS + ? process.env.ALLOW_REDEEM_DOMAINS.split(',').map(d => d.trim()) + : []; + this.defaultLightningAddress = process.env.DEFAULT_LIGHTNING_ADDRESS; + } + + /** + * Get the default Lightning address from environment + * @returns {string|null} Default Lightning address or null if not set + */ + getDefaultLightningAddress() { + return this.defaultLightningAddress || null; + } + + /** + * Get Lightning address to use - provided address or default + * @param {string|null} providedAddress - The provided Lightning address + * @returns {string} Lightning address to use + */ + getLightningAddressToUse(providedAddress) { + if (providedAddress && providedAddress.trim()) { + return providedAddress.trim(); + } + + const defaultAddress = this.getDefaultLightningAddress(); + if (!defaultAddress) { + throw new Error('No Lightning address provided and no default Lightning address configured'); + } + + return defaultAddress; + } + + /** + * Validate Lightning Address format + * @param {string} lightningAddress - The Lightning address (user@domain.com) + * @returns {boolean} Whether the address is valid + */ + validateLightningAddress(lightningAddress) { + if (!lightningAddress || typeof lightningAddress !== 'string') { + return false; + } + + const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; + return emailRegex.test(lightningAddress); + } + + /** + * Check if a domain is allowed for redemption + * @param {string} domain - The domain to check + * @returns {boolean} Whether the domain is allowed + */ + isDomainAllowed(domain) { + if (this.allowedDomains.length === 0) { + return true; // If no restrictions, allow all + } + + // Check for wildcard allowing all domains + if (this.allowedDomains.includes('*')) { + return true; + } + + return this.allowedDomains.includes(domain.toLowerCase()); + } + + /** + * Parse Lightning Address into username and domain + * @param {string} lightningAddress - The Lightning address + * @returns {Object} Parsed address components + */ + parseLightningAddress(lightningAddress) { + if (!this.validateLightningAddress(lightningAddress)) { + throw new Error('Invalid Lightning address format'); + } + + const [username, domain] = lightningAddress.split('@'); + + if (!this.isDomainAllowed(domain)) { + throw new Error(`Domain ${domain} is not allowed for redemption`); + } + + return { username, domain }; + } + + /** + * Resolve LNURLp endpoint from Lightning address + * @param {string} lightningAddress - The Lightning address + * @returns {string} LNURLp endpoint URL + */ + getLNURLpEndpoint(lightningAddress) { + const { username, domain } = this.parseLightningAddress(lightningAddress); + return `https://${domain}/.well-known/lnurlp/${username}`; + } + + /** + * Fetch LNURLp response from endpoint + * @param {string} lnurlpUrl - The LNURLp endpoint URL + * @returns {Object} LNURLp response data + */ + async fetchLNURLpResponse(lnurlpUrl) { + try { + const response = await axios.get(lnurlpUrl, { + timeout: 10000, + headers: { + 'User-Agent': 'Cashu-Redeem-API/1.0.0' + } + }); + + if (response.status !== 200) { + throw new Error(`HTTP ${response.status}: ${response.statusText}`); + } + + const data = response.data; + + if (data.status === 'ERROR') { + throw new Error(data.reason || 'LNURLp endpoint returned error'); + } + + if (!data.callback || !data.minSendable || !data.maxSendable) { + throw new Error('Invalid LNURLp response - missing required fields'); + } + + return data; + } catch (error) { + if (error.code === 'ECONNREFUSED' || error.code === 'ENOTFOUND') { + throw new Error('Unable to connect to Lightning address provider'); + } + throw new Error(`LNURLp fetch failed: ${error.message}`); + } + } + + /** + * Get Lightning invoice from LNURLp callback + * @param {string} callbackUrl - The callback URL from LNURLp response + * @param {number} amount - Amount in millisatoshis + * @param {string} comment - Optional comment + * @returns {Object} Invoice response + */ + async getInvoice(callbackUrl, amount, comment = '') { + try { + const url = new URL(callbackUrl); + url.searchParams.set('amount', amount.toString()); + + if (comment && comment.length > 0) { + url.searchParams.set('comment', comment.substring(0, 144)); // LN comment limit + } + + const response = await axios.get(url.toString(), { + timeout: 10000, + headers: { + 'User-Agent': 'Cashu-Redeem-API/1.0.0' + } + }); + + if (response.status !== 200) { + throw new Error(`HTTP ${response.status}: ${response.statusText}`); + } + + const data = response.data; + + if (data.status === 'ERROR') { + throw new Error(data.reason || 'Invoice generation failed'); + } + + if (!data.pr) { + throw new Error('No invoice returned from callback'); + } + + return { + bolt11: data.pr, + successAction: data.successAction, + verify: data.verify + }; + } catch (error) { + throw new Error(`Invoice generation failed: ${error.message}`); + } + } + + /** + * Convert satoshis to millisatoshis + * @param {number} sats - Amount in satoshis + * @returns {number} Amount in millisatoshis + */ + satsToMillisats(sats) { + return sats * 1000; + } + + /** + * Convert millisatoshis to satoshis + * @param {number} msats - Amount in millisatoshis + * @returns {number} Amount in satoshis + */ + millisatsToSats(msats) { + return Math.floor(msats / 1000); + } + + /** + * Validate amount against LNURLp constraints + * @param {number} amount - Amount in satoshis + * @param {Object} lnurlpResponse - LNURLp response data + * @returns {boolean} Whether amount is valid + */ + validateAmount(amount, lnurlpResponse) { + const amountMsats = this.satsToMillisats(amount); + const minSendable = parseInt(lnurlpResponse.minSendable); + const maxSendable = parseInt(lnurlpResponse.maxSendable); + + return amountMsats >= minSendable && amountMsats <= maxSendable; + } + + /** + * Full Lightning address to invoice resolution + * @param {string} lightningAddress - The Lightning address + * @param {number} amount - Amount in satoshis + * @param {string} comment - Optional comment + * @returns {Object} Invoice and metadata + */ + async resolveInvoice(lightningAddress, amount, comment = 'Cashu token redemption') { + try { + // Get LNURLp endpoint + const lnurlpUrl = this.getLNURLpEndpoint(lightningAddress); + + // Fetch LNURLp response + const lnurlpResponse = await this.fetchLNURLpResponse(lnurlpUrl); + + // Validate amount + if (!this.validateAmount(amount, lnurlpResponse)) { + const minSats = this.millisatsToSats(lnurlpResponse.minSendable); + const maxSats = this.millisatsToSats(lnurlpResponse.maxSendable); + throw new Error(`Amount ${amount} sats is outside allowed range: ${minSats}-${maxSats} sats`); + } + + // Get invoice + const amountMsats = this.satsToMillisats(amount); + const invoiceResponse = await this.getInvoice(lnurlpResponse.callback, amountMsats, comment); + + return { + bolt11: invoiceResponse.bolt11, + amount, + amountMsats, + lightningAddress, + domain: this.parseLightningAddress(lightningAddress).domain, + successAction: invoiceResponse.successAction, + lnurlpResponse + }; + } catch (error) { + throw new Error(`Lightning address resolution failed: ${error.message}`); + } + } + + /** + * Decode Lightning invoice (basic parsing) + * @param {string} bolt11 - Lightning invoice + * @returns {Object} Basic invoice info + */ + parseInvoice(bolt11) { + try { + // This is a simplified parser - for production use a proper library like bolt11 + if (!bolt11.toLowerCase().startsWith('lnbc') && !bolt11.toLowerCase().startsWith('lntb')) { + throw new Error('Invalid Lightning invoice format'); + } + + return { + bolt11, + network: bolt11.toLowerCase().startsWith('lnbc') ? 'mainnet' : 'testnet' + }; + } catch (error) { + throw new Error(`Invoice parsing failed: ${error.message}`); + } + } +} + +module.exports = new LightningService(); \ No newline at end of file diff --git a/services/redemption.js b/services/redemption.js new file mode 100644 index 0000000..269be69 --- /dev/null +++ b/services/redemption.js @@ -0,0 +1,353 @@ +const { v4: uuidv4 } = require('uuid'); +const cashuService = require('./cashu'); +const lightningService = require('./lightning'); + +class RedemptionService { + constructor() { + // In-memory storage for redemption status + // In production, use Redis or a proper database + this.redemptions = new Map(); + this.tokenHashes = new Map(); // Map token hashes to redemption IDs + } + + /** + * Generate a simple hash for a token (for duplicate detection) + * @param {string} token - The Cashu token + * @returns {string} Hash of the token + */ + generateTokenHash(token) { + const crypto = require('crypto'); + return crypto.createHash('sha256').update(token).digest('hex').substring(0, 16); + } + + /** + * Store redemption status + * @param {string} redeemId - The redemption ID + * @param {Object} status - The redemption status object + */ + storeRedemption(redeemId, status) { + this.redemptions.set(redeemId, { + ...status, + createdAt: new Date().toISOString(), + updatedAt: new Date().toISOString() + }); + } + + /** + * Update redemption status + * @param {string} redeemId - The redemption ID + * @param {Object} updates - Updates to apply + */ + updateRedemption(redeemId, updates) { + const existing = this.redemptions.get(redeemId); + if (existing) { + this.redemptions.set(redeemId, { + ...existing, + ...updates, + updatedAt: new Date().toISOString() + }); + } + } + + /** + * Get redemption status by ID + * @param {string} redeemId - The redemption ID + * @returns {Object|null} Redemption status or null if not found + */ + getRedemption(redeemId) { + return this.redemptions.get(redeemId) || null; + } + + /** + * Get redemption ID by token hash + * @param {string} tokenHash - The token hash + * @returns {string|null} Redemption ID or null if not found + */ + getRedemptionByTokenHash(tokenHash) { + const redeemId = this.tokenHashes.get(tokenHash); + return redeemId ? this.getRedemption(redeemId) : null; + } + + /** + * Check if a token has already been redeemed + * @param {string} token - The Cashu token + * @returns {Object|null} Existing redemption or null + */ + checkExistingRedemption(token) { + const tokenHash = this.generateTokenHash(token); + return this.getRedemptionByTokenHash(tokenHash); + } + + /** + * Validate redemption request + * @param {string} token - The Cashu token + * @param {string} lightningAddress - The Lightning address (optional) + * @returns {Object} Validation result + */ + async validateRedemptionRequest(token, lightningAddress) { + const errors = []; + + // Validate token format + if (!token || typeof token !== 'string') { + errors.push('Token is required and must be a string'); + } + + // Lightning address is now optional - we'll use default if not provided + let addressToUse = null; + try { + addressToUse = lightningService.getLightningAddressToUse(lightningAddress); + + if (!lightningService.validateLightningAddress(addressToUse)) { + errors.push('Invalid Lightning address format'); + } + } catch (error) { + errors.push(error.message); + } + + // Check for existing redemption + if (token) { + const existing = this.checkExistingRedemption(token); + if (existing) { + errors.push('Token has already been redeemed'); + } + } + + // Try to parse token + let tokenData = null; + if (token && errors.length === 0) { + try { + tokenData = await cashuService.parseToken(token); + if (tokenData.totalAmount <= 0) { + errors.push('Token has no value'); + } + } catch (error) { + errors.push(`Invalid token: ${error.message}`); + } + } + + return { + valid: errors.length === 0, + errors, + tokenData, + lightningAddressToUse: addressToUse + }; + } + + /** + * Perform the complete redemption process + * @param {string} token - The Cashu token + * @param {string} lightningAddress - The Lightning address (optional) + * @returns {Object} Redemption result + */ + async performRedemption(token, lightningAddress) { + const redeemId = uuidv4(); + const tokenHash = this.generateTokenHash(token); + + try { + // Determine which Lightning address to use + const lightningAddressToUse = lightningService.getLightningAddressToUse(lightningAddress); + const isUsingDefault = !lightningAddress || !lightningAddress.trim(); + + // Store initial status + this.storeRedemption(redeemId, { + status: 'processing', + token: token.substring(0, 50) + '...', // Store partial token for reference + tokenHash, + lightningAddress: lightningAddressToUse, + usingDefaultAddress: isUsingDefault, + amount: null, + paid: false, + error: null + }); + + // Also map token hash to redemption ID + this.tokenHashes.set(tokenHash, redeemId); + + // Step 1: Parse and validate token + this.updateRedemption(redeemId, { status: 'parsing_token' }); + const tokenData = await cashuService.parseToken(token); + + // Calculate expected fee according to NUT-05 + const expectedFee = cashuService.calculateFee(tokenData.totalAmount); + + this.updateRedemption(redeemId, { + amount: tokenData.totalAmount, + mint: tokenData.mint, + numProofs: tokenData.numProofs, + expectedFee: expectedFee, + format: tokenData.format + }); + + // Check if token is spendable + this.updateRedemption(redeemId, { status: 'checking_spendability' }); + try { + const spendabilityCheck = await cashuService.checkTokenSpendable(token); + if (!spendabilityCheck.spendable || spendabilityCheck.spendable.length === 0) { + throw new Error('Token proofs are not spendable - may have already been used'); + } + } catch (spendError) { + // Log but don't fail - some mints might not support this check + console.warn('Spendability check failed:', spendError.message); + } + + // Step 2: Resolve Lightning address to invoice + this.updateRedemption(redeemId, { status: 'resolving_invoice' }); + const invoiceData = await lightningService.resolveInvoice( + lightningAddressToUse, + tokenData.totalAmount, + `Cashu redemption ${redeemId.substring(0, 8)}` + ); + + this.updateRedemption(redeemId, { + bolt11: invoiceData.bolt11.substring(0, 50) + '...', + domain: invoiceData.domain + }); + + // Step 3: Melt the token to pay the invoice + this.updateRedemption(redeemId, { status: 'melting_token' }); + const meltResult = await cashuService.meltToken(token, invoiceData.bolt11); + + // Step 4: Update final status + this.updateRedemption(redeemId, { + status: meltResult.paid ? 'paid' : 'failed', + paid: meltResult.paid, + preimage: meltResult.preimage, + fee: meltResult.fee, + actualFee: meltResult.actualFee, + netAmount: meltResult.netAmount, + change: meltResult.change, + paidAt: meltResult.paid ? new Date().toISOString() : null + }); + + return { + success: true, + redeemId, + paid: meltResult.paid, + amount: tokenData.totalAmount, + to: lightningAddressToUse, + usingDefaultAddress: isUsingDefault, + fee: meltResult.fee, + actualFee: meltResult.actualFee, + netAmount: meltResult.netAmount, + preimage: meltResult.preimage, + change: meltResult.change, + mint: tokenData.mint, + format: tokenData.format + }; + + } catch (error) { + // Update redemption with error + this.updateRedemption(redeemId, { + status: 'failed', + paid: false, + error: error.message + }); + + return { + success: false, + redeemId, + error: error.message + }; + } + } + + /** + * Get redemption status for API response + * @param {string} redeemId - The redemption ID + * @returns {Object|null} Status response or null if not found + */ + getRedemptionStatus(redeemId) { + const redemption = this.getRedemption(redeemId); + + if (!redemption) { + return null; + } + + const response = { + success: true, + status: redemption.status, + details: { + amount: redemption.amount, + to: redemption.lightningAddress, + paid: redemption.paid, + createdAt: redemption.createdAt, + updatedAt: redemption.updatedAt + } + }; + + if (redemption.paidAt) { + response.details.paidAt = redemption.paidAt; + } + + if (redemption.fee) { + response.details.fee = redemption.fee; + } + + if (redemption.error) { + response.details.error = redemption.error; + } + + if (redemption.mint) { + response.details.mint = redemption.mint; + } + + if (redemption.domain) { + response.details.domain = redemption.domain; + } + + return response; + } + + /** + * Get all redemptions (for admin/debugging) + * @returns {Array} All redemptions + */ + getAllRedemptions() { + return Array.from(this.redemptions.entries()).map(([id, data]) => ({ + redeemId: id, + ...data + })); + } + + /** + * Clean up old redemptions (should be called periodically) + * @param {number} maxAgeMs - Maximum age in milliseconds + */ + cleanupOldRedemptions(maxAgeMs = 24 * 60 * 60 * 1000) { // 24 hours default + const cutoff = new Date(Date.now() - maxAgeMs); + + for (const [redeemId, redemption] of this.redemptions.entries()) { + const createdAt = new Date(redemption.createdAt); + if (createdAt < cutoff) { + this.redemptions.delete(redeemId); + // Also clean up token hash mapping + if (redemption.tokenHash) { + this.tokenHashes.delete(redemption.tokenHash); + } + } + } + } + + /** + * Get redemption statistics + * @returns {Object} Statistics + */ + getStats() { + const redemptions = Array.from(this.redemptions.values()); + + return { + total: redemptions.length, + paid: redemptions.filter(r => r.paid).length, + failed: redemptions.filter(r => r.status === 'failed').length, + processing: redemptions.filter(r => r.status === 'processing').length, + totalAmount: redemptions + .filter(r => r.paid && r.amount) + .reduce((sum, r) => sum + r.amount, 0), + totalFees: redemptions + .filter(r => r.paid && r.fee) + .reduce((sum, r) => sum + r.fee, 0) + }; + } +} + +module.exports = new RedemptionService(); \ No newline at end of file diff --git a/swagger.config.js b/swagger.config.js new file mode 100644 index 0000000..a9e0cf0 --- /dev/null +++ b/swagger.config.js @@ -0,0 +1,554 @@ +const swaggerJsdoc = require('swagger-jsdoc'); + +const options = { + definition: { + openapi: '3.0.0', + info: { + title: 'Cashu Redeem API', + version: '1.0.0', + description: 'A production-grade API for redeeming Cashu tokens (ecash) to Lightning addresses using the cashu-ts library and LNURLp protocol.', + contact: { + name: 'API Support', + email: 'support@example.com' + }, + license: { + name: 'MIT', + url: 'https://opensource.org/licenses/MIT' + } + }, + servers: [ + { + url: 'http://localhost:3000', + description: 'Development server' + }, + { + url: 'https://api.example.com', + description: 'Production server' + } + ], + components: { + schemas: { + // Error Response Schema + ErrorResponse: { + type: 'object', + required: ['success', 'error'], + properties: { + success: { + type: 'boolean', + example: false + }, + error: { + type: 'string', + example: 'Error message description' + } + } + }, + + // Token Decode Schemas + DecodeRequest: { + type: 'object', + required: ['token'], + properties: { + token: { + type: 'string', + description: 'Cashu token to decode (supports v1 and v3 formats)', + example: 'cashuAeyJwcm9vZnMiOlt7ImFtb3VudCI6MSwiaWQiOiIwMGZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmIn0seyJhbW91bnQiOjEsImlkIjoiMDBmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmIn1dLCJtaW50IjoiaHR0cHM6Ly9taW50LmV4YW1wbGUuY29tIn0' + } + } + }, + + DecodeResponse: { + type: 'object', + properties: { + success: { + type: 'boolean', + example: true + }, + decoded: { + type: 'object', + properties: { + mint: { + type: 'string', + format: 'uri', + example: 'https://mint.azzamo.net' + }, + totalAmount: { + type: 'integer', + description: 'Total amount in satoshis', + example: 21000 + }, + numProofs: { + type: 'integer', + description: 'Number of proofs in the token', + example: 3 + }, + denominations: { + type: 'array', + items: { + type: 'integer' + }, + description: 'Array of proof amounts', + example: [1000, 10000, 10000] + }, + format: { + type: 'string', + enum: ['cashuA', 'cashuB'], + description: 'Token format version', + example: 'cashuA' + } + } + }, + mint_url: { + type: 'string', + format: 'uri', + description: 'Mint URL extracted from token', + example: 'https://mint.azzamo.net' + } + } + }, + + // Redeem Schemas + RedeemRequest: { + type: 'object', + required: ['token'], + properties: { + token: { + type: 'string', + description: 'Cashu token to redeem', + example: 'cashuAeyJwcm9vZnMiOlt7ImFtb3VudCI6MSwiaWQiOiIwMGZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmIn0seyJhbW91bnQiOjEsImlkIjoiMDBmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmIn1dLCJtaW50IjoiaHR0cHM6Ly9taW50LmV4YW1wbGUuY29tIn0' + }, + lightningAddress: { + type: 'string', + format: 'email', + description: 'Lightning address to send payment to (optional - uses default if not provided)', + example: 'user@ln.tips' + } + } + }, + + RedeemResponse: { + type: 'object', + properties: { + success: { + type: 'boolean', + example: true + }, + redeemId: { + type: 'string', + format: 'uuid', + description: 'Unique redemption ID for tracking', + example: '8e99101e-d034-4d2e-9ccf-dfda24d26762' + }, + paid: { + type: 'boolean', + description: 'Whether the payment was successful', + example: true + }, + amount: { + type: 'integer', + description: 'Total amount redeemed in satoshis', + example: 21000 + }, + to: { + type: 'string', + description: 'Lightning address that received the payment', + example: 'user@ln.tips' + }, + fee: { + type: 'integer', + description: 'Actual fee charged by mint in satoshis', + example: 1000 + }, + actualFee: { + type: 'integer', + description: 'Calculated fee according to NUT-05 (2% min 1 sat)', + example: 420 + }, + netAmount: { + type: 'integer', + description: 'Net amount after fees in satoshis', + example: 20000 + }, + mint_url: { + type: 'string', + format: 'uri', + description: 'Mint URL used for redemption', + example: 'https://mint.azzamo.net' + }, + format: { + type: 'string', + enum: ['cashuA', 'cashuB'], + description: 'Token format that was redeemed', + example: 'cashuA' + }, + preimage: { + type: 'string', + description: 'Lightning payment preimage (if available)', + example: 'abc123def456...' + }, + change: { + type: 'array', + description: 'Change proofs returned (if any)', + items: { + type: 'object' + }, + example: [] + }, + usingDefaultAddress: { + type: 'boolean', + description: 'Whether default Lightning address was used', + example: false + }, + message: { + type: 'string', + description: 'Additional message (when using default address)', + example: 'Redeemed to default Lightning address: admin@example.com' + } + } + }, + + // Status Schemas + StatusRequest: { + type: 'object', + required: ['redeemId'], + properties: { + redeemId: { + type: 'string', + format: 'uuid', + description: 'Redemption ID to check status for', + example: '8e99101e-d034-4d2e-9ccf-dfda24d26762' + } + } + }, + + StatusResponse: { + type: 'object', + properties: { + success: { + type: 'boolean', + example: true + }, + status: { + type: 'string', + enum: ['processing', 'parsing_token', 'checking_spendability', 'resolving_invoice', 'melting_token', 'paid', 'failed'], + description: 'Current redemption status', + example: 'paid' + }, + details: { + type: 'object', + properties: { + amount: { + type: 'integer', + description: 'Amount in satoshis', + example: 21000 + }, + to: { + type: 'string', + description: 'Lightning address', + example: 'user@ln.tips' + }, + paid: { + type: 'boolean', + example: true + }, + createdAt: { + type: 'string', + format: 'date-time', + example: '2025-01-14T11:59:30Z' + }, + updatedAt: { + type: 'string', + format: 'date-time', + example: '2025-01-14T12:00:00Z' + }, + paidAt: { + type: 'string', + format: 'date-time', + example: '2025-01-14T12:00:00Z' + }, + fee: { + type: 'integer', + description: 'Fee charged in satoshis', + example: 1000 + }, + error: { + type: 'string', + description: 'Error message if failed', + example: null + }, + mint: { + type: 'string', + format: 'uri', + description: 'Mint URL', + example: 'https://mint.azzamo.net' + }, + domain: { + type: 'string', + description: 'Lightning address domain', + example: 'ln.tips' + } + } + } + } + }, + + // Health Schema + HealthResponse: { + type: 'object', + properties: { + status: { + type: 'string', + example: 'ok' + }, + timestamp: { + type: 'string', + format: 'date-time', + example: '2025-01-14T12:00:00Z' + }, + uptime: { + type: 'number', + description: 'Server uptime in seconds', + example: 3600 + }, + memory: { + type: 'object', + description: 'Memory usage information', + example: { + "rss": 45678912, + "heapTotal": 12345678, + "heapUsed": 8765432 + } + }, + version: { + type: 'string', + example: '1.0.0' + } + } + }, + + // Stats Schema + StatsResponse: { + type: 'object', + properties: { + success: { + type: 'boolean', + example: true + }, + stats: { + type: 'object', + properties: { + total: { + type: 'integer', + description: 'Total number of redemptions', + example: 150 + }, + paid: { + type: 'integer', + description: 'Number of successful redemptions', + example: 142 + }, + failed: { + type: 'integer', + description: 'Number of failed redemptions', + example: 8 + }, + processing: { + type: 'integer', + description: 'Number of currently processing redemptions', + example: 0 + }, + totalAmount: { + type: 'integer', + description: 'Total amount redeemed in satoshis', + example: 2500000 + }, + totalFees: { + type: 'integer', + description: 'Total fees collected in satoshis', + example: 15000 + } + } + } + } + }, + + // Validate Address Schemas + ValidateAddressRequest: { + type: 'object', + required: ['lightningAddress'], + properties: { + lightningAddress: { + type: 'string', + format: 'email', + description: 'Lightning address to validate', + example: 'user@ln.tips' + } + } + }, + + ValidateAddressResponse: { + type: 'object', + properties: { + success: { + type: 'boolean', + example: true + }, + valid: { + type: 'boolean', + example: true + }, + domain: { + type: 'string', + example: 'ln.tips' + }, + minSendable: { + type: 'integer', + description: 'Minimum sendable amount in satoshis', + example: 1 + }, + maxSendable: { + type: 'integer', + description: 'Maximum sendable amount in satoshis', + example: 100000000 + }, + commentAllowed: { + type: 'integer', + description: 'Maximum comment length allowed', + example: 144 + }, + error: { + type: 'string', + description: 'Error message if validation failed', + example: null + } + } + }, + + // Check Spendable Schemas + CheckSpendableRequest: { + type: 'object', + required: ['token'], + properties: { + token: { + type: 'string', + description: 'Cashu token to check spendability', + example: 'cashuAeyJwcm9vZnMiOlt7ImFtb3VudCI6MSwiaWQiOiIwMGZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmIn0seyJhbW91bnQiOjEsImlkIjoiMDBmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmIn1dLCJtaW50IjoiaHR0cHM6Ly9taW50LmV4YW1wbGUuY29tIn0' + } + } + }, + + CheckSpendableResponse: { + type: 'object', + properties: { + success: { + type: 'boolean', + example: true + }, + spendable: { + type: 'array', + items: { + type: 'boolean' + }, + description: 'Array indicating which proofs are spendable', + example: [true, true, false] + }, + pending: { + type: 'array', + items: { + type: 'boolean' + }, + description: 'Array indicating which proofs are pending', + example: [] + }, + mintUrl: { + type: 'string', + format: 'uri', + description: 'Mint URL where spendability was checked', + example: 'https://mint.azzamo.net' + }, + totalAmount: { + type: 'integer', + description: 'Total amount of the token in satoshis', + example: 21000 + }, + message: { + type: 'string', + description: 'Human-readable status message', + example: 'Token is spendable' + } + } + } + }, + + responses: { + BadRequest: { + description: 'Bad request - invalid input', + content: { + 'application/json': { + schema: { + $ref: '#/components/schemas/ErrorResponse' + } + } + } + }, + NotFound: { + description: 'Resource not found', + content: { + 'application/json': { + schema: { + $ref: '#/components/schemas/ErrorResponse' + } + } + } + }, + InternalServerError: { + description: 'Internal server error', + content: { + 'application/json': { + schema: { + $ref: '#/components/schemas/ErrorResponse' + } + } + } + }, + TooManyRequests: { + description: 'Rate limit exceeded', + content: { + 'application/json': { + schema: { + allOf: [ + { $ref: '#/components/schemas/ErrorResponse' }, + { + properties: { + error: { + example: 'Rate limit exceeded. Please try again later.' + } + } + } + ] + } + } + } + } + } + }, + tags: [ + { + name: 'Token Operations', + description: 'Operations for decoding and redeeming Cashu tokens' + }, + { + name: 'Status & Monitoring', + description: 'Endpoints for checking redemption status and API health' + }, + { + name: 'Validation', + description: 'Validation utilities for tokens and Lightning addresses' + } + ] + }, + apis: ['./server.js'], // paths to files containing OpenAPI definitions +}; + +const specs = swaggerJsdoc(options); +module.exports = specs; \ No newline at end of file