diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4436540..35e3a39 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -72,3 +72,23 @@ jobs: - run: npm ci - run: npm run build + + sdk: + name: SDK — Build & Test + runs-on: ubuntu-latest + defaults: + run: + working-directory: packages/sdk + + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-node@v4 + with: + node-version: 20 + cache: npm + cache-dependency-path: packages/sdk/package-lock.json + + - run: npm ci + - run: npm run build + - run: npm test diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..9a87582 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 BridgeShield + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md index 9303381..4f94ede 100644 --- a/README.md +++ b/README.md @@ -1,24 +1,46 @@ # BridgeShield — AML Gateway for LI.FI Cross-Chain Trades +[![GitHub license](https://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/easyshellworld/bridgeshield/blob/main/LICENSE) +[![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg)](https://github.com/easyshellworld/bridgeshield/pulls) +[![GitHub Repo stars](https://img.shields.io/github/stars/easyshellworld/bridgeshield?style=social)](https://github.com/easyshellworld/bridgeshield/stargazers) +[![Open Issues](https://img.shields.io/github/issues/easyshellworld/bridgeshield)](https://github.com/easyshellworld/bridgeshield/issues) +[![GitHub closed pull requests](https://img.shields.io/github/issues-pr-closed/easyshellworld/bridgeshield)](https://github.com/easyshellworld/bridgeshield/pulls?q=is%3Apr+is%3Aclosed) +[![Last Commit](https://img.shields.io/github/last-commit/easyshellworld/bridgeshield)](https://github.com/easyshellworld/bridgeshield/commits/main) + +[![Node.js](https://img.shields.io/badge/Node.js-20+-339933?logo=node.js)](https://nodejs.org/) +[![TypeScript](https://img.shields.io/badge/TypeScript-5.0+-3178C6?logo=typescript)](https://www.typescriptlang.org/) +[![Express](https://img.shields.io/badge/Express-4.x-000000?logo=express)](https://expressjs.com/) +[![React](https://img.shields.io/badge/React-18-61DAFB?logo=react)](https://react.dev/) +[![Tailwind CSS](https://img.shields.io/badge/Tailwind_CSS-3.0+-06B6D4?logo=tailwindcss)](https://tailwindcss.com/) +[![Prisma](https://img.shields.io/badge/Prisma-5.x-5A67D8?logo=prisma)](https://prisma.io/) +[![Vitest](https://img.shields.io/badge/Vitest-1.x-6B9DF8?logo=vitest)](https://vitest.io/) + +[![CI/CD](https://img.shields.io/badge/CI%2FCD-GitHub%20Actions-2088FF?logo=githubactions)](https://github.com/easyshellworld/bridgeshield/actions) +[![Tests](https://img.shields.io/badge/tests-88%20%2B%2021-brightgreen)](https://github.com/easyshellworld/bridgeshield/actions) +[![Docker Ready](https://img.shields.io/badge/Docker-Ready-2496ED?logo=docker)](https://www.docker.com/) + BridgeShield is an Anti-Money Laundering (AML) compliance gateway designed specifically for cross-chain trading platforms like LI.FI. It provides real-time risk assessment, transaction monitoring, and regulatory compliance for decentralized finance (DeFi) transactions. ## 🏗️ Architecture ``` -┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ -│ │ │ │ │ │ -│ Frontend Demo │ │ Backend API │ │ Frontend Admin │ -│ (Port: 5173) │◄───┤ (Port: 3000) │───►│ (Port: 5174) │ -│ │ │ │ │ │ -└─────────────────┘ └─────────────────┘ └─────────────────┘ - │ │ │ - │ │ │ - ▼ ▼ ▼ -┌─────────────────────────────────────────────────────────────┐ -│ │ -│ Docker Compose │ -│ │ -└─────────────────────────────────────────────────────────────┘ +┌─────────────────────────────────────────────────────────────────────────┐ +│ │ +│ ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ │ +│ │ │ │ │ │ │ │ +│ │ Frontend Demo │ │ Backend API │ │ Frontend Admin │ │ +│ │ (Port: 5173) │◄───┤ (Port: 3000) │───►│ (Port: 5174) │ │ +│ │ │ │ │ │ │ │ +│ └─────────────────┘ └─────────────────┘ └─────────────────┘ │ +│ │ │ +│ ▼ │ +│ ┌─────────────────┐ │ +│ │ SDK Package │ │ +│ │ @bridgeshield/ │ │ +│ │ sdk │ │ +│ └─────────────────┘ │ +│ │ +└─────────────────────────────────────────────────────────────────────────┘ ``` ## 🚀 Quick Start @@ -27,7 +49,7 @@ BridgeShield is an Anti-Money Laundering (AML) compliance gateway designed speci 1. **Clone the repository:** ```bash - git clone + git clone https://github.com/easyshellworld/bridgeshield cd bridgeshield ``` @@ -55,6 +77,32 @@ For hot-reload during development: docker-compose -f docker-compose.yml -f docker-compose.dev.yml up -d ``` +## 📦 SDK Package + +For easy integration into your own applications: + +```bash +npm install @bridgeshield/sdk +``` + +```typescript +import { BridgeShieldClient } from '@bridgeshield/sdk'; + +const client = new BridgeShieldClient({ + baseUrl: 'https://api.bridgeshield.io', +}); + +const result = await client.checkAddress({ + address: '0x1234567890abcdef1234567890abcdef12345678', + chainId: 1, +}); + +console.log(result.riskLevel); // 'LOW' | 'MEDIUM' | 'HIGH' +console.log(result.decision); // 'ALLOW' | 'REVIEW' | 'BLOCK' +``` + +See [packages/sdk/README.md](packages/sdk/README.md) for full documentation. + ## 📁 Project Structure ``` @@ -62,16 +110,23 @@ bridgeshield/ ├── backend/ # Node.js/TypeScript backend API │ ├── src/ # Source code │ ├── prisma/ # Database schema and migrations -│ ├── Dockerfile # Production Dockerfile +│ ├── tests/ # Unit and integration tests +│ ├── Dockerfile # Production Dockerfile │ └── package.json ├── frontend-demo/ # Demo interface for users -│ ├── src/ # Vue.js/TypeScript source -│ ├── Dockerfile # Production Dockerfile +│ ├── src/ # React/TypeScript source +│ ├── Dockerfile # Production Dockerfile │ └── package.json ├── frontend-admin/ # Admin dashboard -│ ├── src/ # Vue.js/TypeScript source -│ ├── Dockerfile # Production Dockerfile +│ ├── src/ # React/TypeScript source +│ ├── Dockerfile # Production Dockerfile │ └── package.json +├── packages/ +│ └── sdk/ # @bridgeshield/sdk npm package +│ ├── src/ # TypeScript source +│ ├── __tests__/ # Unit tests +│ ├── README.md # SDK documentation +│ └── package.json ├── docker-compose.yml # Production services ├── docker-compose.dev.yml # Development overrides └── README.md # This file @@ -142,63 +197,93 @@ bridgeshield/ npm run dev ``` +### SDK Development + +1. **Navigate to SDK directory:** + ```bash + cd packages/sdk + ``` + +2. **Install dependencies:** + ```bash + npm install + ``` + +3. **Build:** + ```bash + npm run build + ``` + +4. **Run tests:** + ```bash + npm test + ``` + ## 🌐 API Endpoints -### Core API (Port 3000) +### Core AML API (Port 3000) | Method | Endpoint | Description | |--------|----------|-------------| -| GET | `/api/v1/health` | Health check | -| GET | `/api/v1/docs` | OpenAPI documentation | -| POST | `/api/v1/transactions/analyze` | Analyze transaction for AML risks | -| GET | `/api/v1/transactions/:id` | Get transaction details | -| GET | `/api/v1/transactions` | List transactions with filtering | -| POST | `/api/v1/addresses/check` | Check wallet address against risk databases | -| GET | `/api/v1/addresses/:address` | Get address risk profile | -| GET | `/api/v1/compliance/rules` | List active compliance rules | -| POST | `/api/v1/compliance/rules` | Create new compliance rule | -| GET | `/api/v1/reports/risk-summary` | Generate risk summary report | - -### WebSocket Endpoints - -- `/ws/transactions` - Real-time transaction monitoring -- `/ws/alerts` - Compliance alert notifications +| GET | `/api/v1/health` | Health check with service status | +| POST | `/api/v1/aml/check` | Check address risk score | +| GET | `/api/v1/aml/whitelist` | Get whitelist summary | +| POST | `/api/v1/aml/appeal` | Submit appeal for flagged address | +| GET | `/api/v1/aml/appeal/status/:ticketId` | Check appeal status | + +### Admin API (Port 3000) + +| Method | Endpoint | Description | +|--------|----------|-------------| +| GET | `/api/v1/admin/dashboard/stats` | Dashboard statistics | +| GET | `/api/v1/admin/dashboard/risk-trend` | 7-day risk trend | +| GET | `/api/v1/admin/dashboard/risk-distribution` | Risk level distribution | +| GET | `/api/v1/admin/appeals` | List all appeals | +| POST | `/api/v1/admin/appeal/:id/approve` | Approve appeal | +| POST | `/api/v1/admin/appeal/:id/reject` | Reject appeal | +| GET | `/api/v1/admin/whitelist` | List whitelist entries | +| POST | `/api/v1/admin/whitelist` | Add to whitelist | +| DELETE | `/api/v1/admin/whitelist/:id` | Remove from whitelist | +| GET | `/api/v1/admin/logs` | View check logs | ## 🛡️ Features ### Risk Assessment - **Wallet Screening:** Check addresses against OFAC, UN, EU sanctions lists -- **Transaction Pattern Analysis:** Detect suspicious transaction patterns -- **Cross-chain Tracking:** Monitor fund flows across multiple blockchains -- **Risk Scoring:** Assign risk scores based on multiple factors +- **Real-time Scoring:** Risk score 0-100 with HIGH/MEDIUM/LOW classification +- **Risk Factors:** Detailed breakdown of risk indicators +- **Caching:** Multi-tier in-memory caching with TTL ### Compliance Tools -- **Custom Rules Engine:** Define custom compliance rules -- **Audit Trail:** Comprehensive logging of all compliance decisions -- **Report Generation:** Generate regulatory compliance reports -- **Alert System:** Real-time alerts for high-risk transactions +- **Appeal System:** Users can contest flagged addresses +- **Whitelist Management:** Admin can manage permanent whitelists +- **Audit Trail:** Complete logging of all checks and decisions +- **Transaction Monitoring:** Track checked transactions -### Integration -- **LI.FI Compatible:** Designed specifically for LI.FI cross-chain transactions -- **REST API:** Full-featured API for integration with other systems -- **WebSocket Support:** Real-time updates and notifications +### Integration Options +- **REST API:** Full-featured API for direct integration +- **@bridgeshield/sdk:** Official JavaScript/TypeScript SDK +- **Demo Frontend:** Working example with React +- **Admin Dashboard:** Admin panel for managing whitelist and appeals ## 🧪 Testing ### Backend Tests ```bash cd backend -npm test # Run unit tests -npm run test:e2e # Run end-to-end tests +npm test # Run all tests (88 tests) ``` -### Frontend Tests +### SDK Tests ```bash -cd frontend-demo -npm test # Run component tests +cd packages/sdk +npm test # Run SDK tests (21 tests) +``` -cd ../frontend-admin -npm test # Run component tests +### Frontend Builds +```bash +cd frontend-demo && npm run build +cd frontend-admin && npm run build ``` ## 🐳 Docker Commands @@ -236,40 +321,33 @@ docker-compose logs -f backend ## 🔐 Security ### Environment Variables -- `DATABASE_URL`: PostgreSQL connection string -- `JWT_SECRET`: Secret for JWT token generation -- `SANCTIONS_API_KEY`: API key for sanctions list checking +- `DATABASE_URL`: SQLite/PostgreSQL connection string - `LOG_LEVEL`: Logging level (debug, info, warn, error) -### Security Best Practices -- All API endpoints require authentication -- Sensitive data is encrypted at rest -- Regular security audits and dependency updates +### Security Features - Rate limiting on public endpoints +- Input validation on all endpoints +- Helmet security headers +- CORS configuration +- Parameterized queries (Prisma) ## 📊 Tech Stack | Component | Technology | |-----------|------------| | **Backend** | Node.js, TypeScript, Express, Prisma | -| **Database** | SQLite (development), PostgreSQL (production) | -| **Frontend** | Vue.js 3, TypeScript, Vite, Tailwind CSS | +| **Database** | SQLite (dev), PostgreSQL (prod) | +| **Frontend** | React 18, TypeScript, Vite, Tailwind CSS | +| **SDK** | TypeScript, tsup (ESM + CJS) | | **Container** | Docker, Docker Compose | -| **API** | REST, WebSocket, OpenAPI/Swagger | -| **Testing** | Jest, Vitest, Supertest | +| **Testing** | Vitest, Supertest | | **Monitoring** | Winston logging, Health checks | ## 📈 Monitoring & Logging ### Health Checks - Backend: `GET http://localhost:3000/api/v1/health` -- Returns service status and uptime - -### Logging Levels -- `DEBUG`: Detailed debugging information -- `INFO`: General operational information -- `WARN`: Warning conditions -- `ERROR`: Error conditions +- Returns service status, uptime, and dependency health ### View Logs ```bash @@ -280,7 +358,7 @@ docker-compose logs docker-compose logs backend # Follow logs in real-time -docker-compose logs -f +docker-compose logs -f backend ``` ## 🔄 Development Workflow @@ -322,14 +400,10 @@ docker-compose logs --tail=100 ## 📄 License -MIT License - see LICENSE file for details. +MIT License - see [LICENSE](LICENSE) file for details. ## 🙏 Acknowledgments - **LI.FI** for cross-chain transaction infrastructure -- **Vue.js** and **TypeScript** communities +- **React** and **TypeScript** communities - **Open-source contributors** to all dependencies - ---- - -**BridgeShield** is developed with ❤️ for secure and compliant DeFi transactions. \ No newline at end of file diff --git a/backend/package.json b/backend/package.json index c44ef51..9b629bb 100644 --- a/backend/package.json +++ b/backend/package.json @@ -1,6 +1,6 @@ { "name": "bridgeshield-backend", - "version": "2.0.0", + "version": "0.0.0", "description": "AML risk check API for LI.FI cross-chain transactions", "main": "dist/app.js", "scripts": { diff --git a/backend/src/api/routes/admin.ts b/backend/src/api/routes/admin.ts index 1c43d6d..616c377 100644 --- a/backend/src/api/routes/admin.ts +++ b/backend/src/api/routes/admin.ts @@ -1,11 +1,45 @@ import { Router, Request, Response } from 'express'; import { PrismaService } from '../../db/prisma-client'; -import { RiskDataLoader } from '../../data/risk-data-loader'; +import { CacheService, CacheTier } from '../../services/cache-service'; import { logger } from '../middleware/logger'; const router = Router(); const prismaService = PrismaService.getInstance(); -const riskDataLoader = RiskDataLoader.getInstance(); +const cacheService = CacheService.getInstance(); + +const parseRiskFactors = (responseData: string | null): string[] => { + if (!responseData) { + return []; + } + + try { + const parsed = JSON.parse(responseData) as any; + + if (parsed.isWhitelisted) { + return ['Whitelisted address']; + } + + if (parsed.factors?.details && Array.isArray(parsed.factors.details)) { + return parsed.factors.details; + } + + if (Array.isArray(parsed.factors)) { + return parsed.factors; + } + + if (parsed.riskType && typeof parsed.riskType === 'string') { + return [parsed.riskType]; + } + + if (parsed.fallback) { + return [parsed.fallbackReason || 'Fallback response']; + } + } catch (error) { + logger.warn('Failed to parse risk factors from check log', { error }); + } + + return []; +}; router.get('/dashboard/stats', async (req: Request, res: Response) => { try { @@ -232,7 +266,18 @@ router.get('/appeals', async (req: Request, res: Response) => { } }); - res.json(appeals); + res.json(appeals.map((appeal) => ({ + id: appeal.id, + ticketId: appeal.ticketId, + address: appeal.address, + chainId: appeal.chainId, + reason: appeal.reason, + contact: appeal.contact, + status: appeal.status, + reviewNote: appeal.notes, + reviewedAt: appeal.reviewedAt?.toISOString() || null, + createdAt: appeal.createdAt.toISOString() + }))); } catch (error) { logger.error('Failed to get appeals', { error }); @@ -247,27 +292,89 @@ router.post('/appeal/:id/approve', async (req: Request, res: Response) => { try { const { id } = req.params; const now = new Date(); + const appeal = await prismaService.getClient().$transaction(async (tx) => { + const existingAppeal = await tx.appeal.findUnique({ + where: { id } + }); - const appeal = await prismaService.getClient().appeal.update({ - where: { id }, - data: { - status: 'APPROVED', - reviewedAt: now, - reviewer: 'admin', - decision: 'APPROVED' + if (!existingAppeal) { + throw new Error('APPEAL_NOT_FOUND'); } - }); - await prismaService.createWhitelistEntry({ - address: appeal.address.toLowerCase(), - chainId: appeal.chainId, - category: 'APPEAL_APPROVED', - description: `Permanent whitelist from approved appeal - Ticket: ${appeal.ticketId}` + if (existingAppeal.status !== 'PENDING') { + throw new Error('APPEAL_ALREADY_REVIEWED'); + } + + const normalizedAddress = existingAppeal.address.toLowerCase(); + const existingWhitelistEntry = await tx.whitelistEntry.findUnique({ + where: { address: normalizedAddress } + }); + + if (!existingWhitelistEntry) { + await tx.whitelistEntry.create({ + data: { + address: normalizedAddress, + chainId: existingAppeal.chainId, + category: 'APPEAL_APPROVED', + description: `Permanent whitelist from approved appeal - Ticket: ${existingAppeal.ticketId}` + } + }); + } else if ( + existingWhitelistEntry.category === 'APPEAL_TEMPORARY' || + existingWhitelistEntry.category === 'APPEAL_APPROVED' + ) { + await tx.whitelistEntry.update({ + where: { id: existingWhitelistEntry.id }, + data: { + chainId: existingAppeal.chainId, + category: 'APPEAL_APPROVED', + description: `Permanent whitelist from approved appeal - Ticket: ${existingAppeal.ticketId}`, + expiresAt: null + } + }); + } + + return tx.appeal.update({ + where: { id }, + data: { + status: 'APPROVED', + reviewedAt: now, + reviewer: 'admin', + decision: 'APPROVED' + } + }); }); + cacheService.set( + appeal.address, + appeal.chainId, + { + riskScore: 0, + riskLevel: 'LOW', + decision: 'ALLOW' + }, + CacheTier.WHITELIST + ); + res.json({ success: true }); } catch (error) { + if (error instanceof Error && error.message === 'APPEAL_NOT_FOUND') { + res.status(404).json({ + error: 'Not found', + message: 'Appeal not found' + }); + return; + } + + if (error instanceof Error && error.message === 'APPEAL_ALREADY_REVIEWED') { + res.status(409).json({ + error: 'Conflict', + message: 'Appeal has already been reviewed' + }); + return; + } + logger.error('Failed to approve appeal', { error, appealId: req.params.id }); res.status(500).json({ error: 'Internal server error', @@ -282,20 +389,71 @@ router.post('/appeal/:id/reject', async (req: Request, res: Response) => { const { notes } = req.body; const now = new Date(); - await prismaService.getClient().appeal.update({ - where: { id }, - data: { - status: 'REJECTED', - reviewedAt: now, - reviewer: 'admin', - decision: 'REJECTED', - notes: notes || null + const result = await prismaService.getClient().$transaction(async (tx) => { + const existingAppeal = await tx.appeal.findUnique({ + where: { id } + }); + + if (!existingAppeal) { + throw new Error('APPEAL_NOT_FOUND'); + } + + if (existingAppeal.status !== 'PENDING') { + throw new Error('APPEAL_ALREADY_REVIEWED'); } + + const normalizedAddress = existingAppeal.address.toLowerCase(); + const existingWhitelistEntry = await tx.whitelistEntry.findUnique({ + where: { address: normalizedAddress } + }); + + if (existingWhitelistEntry?.category === 'APPEAL_TEMPORARY') { + await tx.whitelistEntry.delete({ + where: { id: existingWhitelistEntry.id } + }); + } + + await tx.appeal.update({ + where: { id }, + data: { + status: 'REJECTED', + reviewedAt: now, + reviewer: 'admin', + decision: 'REJECTED', + notes: notes || null + } + }); + + return { + address: normalizedAddress, + chainId: existingAppeal.chainId, + removedTemporaryWhitelist: existingWhitelistEntry?.category === 'APPEAL_TEMPORARY' + }; }); + if (result.removedTemporaryWhitelist) { + cacheService.clear(result.address, result.chainId); + } + res.json({ success: true }); } catch (error) { + if (error instanceof Error && error.message === 'APPEAL_NOT_FOUND') { + res.status(404).json({ + error: 'Not found', + message: 'Appeal not found' + }); + return; + } + + if (error instanceof Error && error.message === 'APPEAL_ALREADY_REVIEWED') { + res.status(409).json({ + error: 'Conflict', + message: 'Appeal has already been reviewed' + }); + return; + } + logger.error('Failed to reject appeal', { error, appealId: req.params.id }); res.status(500).json({ error: 'Internal server error', @@ -400,21 +558,7 @@ router.get('/logs', async (req: Request, res: Response) => { }); const formattedLogs = logs.map(log => { - let riskFactors: string[] = []; - - try { - if (log.responseData) { - const responseData = JSON.parse(log.responseData) as any; - - if (responseData.factors && Array.isArray(responseData.factors)) { - riskFactors = responseData.factors; - } else if (responseData.riskType && typeof responseData.riskType === 'string') { - riskFactors = [responseData.riskType]; - } - } - } catch (e) { - // Parse errors for malformed JSON are expected for some entries - } + const riskFactors = parseRiskFactors(log.responseData); return { id: log.id, @@ -443,4 +587,4 @@ router.get('/logs', async (req: Request, res: Response) => { } }); -export default router; \ No newline at end of file +export default router; diff --git a/backend/src/api/routes/appeal.ts b/backend/src/api/routes/appeal.ts index 6a87dd7..8e00de9 100644 --- a/backend/src/api/routes/appeal.ts +++ b/backend/src/api/routes/appeal.ts @@ -1,3 +1,4 @@ +import { Prisma } from '@prisma/client'; import { Router, Request, Response } from 'express'; import { PrismaService } from '../../db/prisma-client'; import { CacheService, CacheTier } from '../../services/cache-service'; @@ -13,13 +14,53 @@ router.post('/', appealRateLimiter, appealValidator, async (req: Request, res: R try { const { address, chainId = 1, reason, contact } = req.body; const normalizedAddress = address.toLowerCase(); + const prisma = prismaService.getClient(); + const now = new Date(); - const today = new Date(); - const year = today.getFullYear(); - const month = String(today.getMonth() + 1).padStart(2, '0'); - const day = String(today.getDate()).padStart(2, '0'); + const year = now.getFullYear(); + const month = String(now.getMonth() + 1).padStart(2, '0'); + const day = String(now.getDate()).padStart(2, '0'); + + const existingPendingAppeal = await prisma.appeal.findFirst({ + where: { + address: normalizedAddress, + chainId, + status: 'PENDING' + } + }); + + if (existingPendingAppeal) { + res.status(409).json({ + error: 'Conflict', + message: 'An appeal for this address is already pending' + }); + return; + } + + const existingWhitelistEntry = await prisma.whitelistEntry.findUnique({ + where: { address: normalizedAddress } + }); + + if (existingWhitelistEntry) { + const isExpiredTemporaryEntry = + existingWhitelistEntry.category === 'APPEAL_TEMPORARY' && + existingWhitelistEntry.expiresAt !== null && + existingWhitelistEntry.expiresAt <= now; + + if (isExpiredTemporaryEntry) { + await prisma.whitelistEntry.delete({ + where: { id: existingWhitelistEntry.id } + }); + } else { + res.status(409).json({ + error: 'Conflict', + message: 'This address is already whitelisted or under review' + }); + return; + } + } - const appealCount = await prismaService.getClient().appeal.count({ + const appealCount = await prisma.appeal.count({ where: { createdAt: { gte: new Date(`${year}-${month}-${day}T00:00:00.000Z`), @@ -32,26 +73,47 @@ router.post('/', appealRateLimiter, appealValidator, async (req: Request, res: R const estimatedReviewAt = new Date(); estimatedReviewAt.setDate(estimatedReviewAt.getDate() + 7); - - const appeal = await prismaService.createAppeal({ - ticketId, - address: normalizedAddress, - chainId, - reason: reason.trim(), - contact: contact ? contact.trim() : undefined, - status: 'PENDING', - estimatedReviewAt - }); - + const temporaryExpiry = new Date(); temporaryExpiry.setDate(temporaryExpiry.getDate() + 1); - - await prismaService.createWhitelistEntry({ - address: normalizedAddress, - chainId, - category: 'APPEAL_TEMPORARY', - description: `Temporary whitelist during appeal review - Ticket: ${ticketId}`, - expiresAt: temporaryExpiry + + await prisma.$transaction(async (tx) => { + await tx.appeal.create({ + data: { + ticketId, + address: normalizedAddress, + chainId, + reason: reason.trim(), + contact: contact ? contact.trim() : undefined, + status: 'PENDING', + estimatedReviewAt + } + }); + + await tx.whitelistEntry.create({ + data: { + address: normalizedAddress, + chainId, + category: 'APPEAL_TEMPORARY', + description: `Temporary whitelist during appeal review - Ticket: ${ticketId}`, + expiresAt: temporaryExpiry + } + }); + + await tx.auditLog.create({ + data: { + action: 'APPEAL_CREATED', + entityType: 'APPEAL', + entityId: ticketId, + ipAddress: req.ip, + userAgent: req.get('User-Agent'), + metadata: JSON.stringify({ + address: normalizedAddress, + chainId, + ticketId + }) + } + }); }); cacheService.set( @@ -65,19 +127,6 @@ router.post('/', appealRateLimiter, appealValidator, async (req: Request, res: R CacheTier.WHITELIST ); - await prismaService.createAuditLog({ - action: 'APPEAL_CREATED', - entityType: 'APPEAL', - entityId: ticketId, - ipAddress: req.ip, - userAgent: req.get('User-Agent'), - metadata: { - address: normalizedAddress, - chainId, - ticketId - } - }); - logger.info('Appeal created', { ticketId, address: normalizedAddress, @@ -104,10 +153,10 @@ router.post('/', appealRateLimiter, appealValidator, async (req: Request, res: R body: req.body }); - if (error instanceof Error && error.message.includes('Unique constraint')) { + if (error instanceof Prisma.PrismaClientKnownRequestError && error.code === 'P2002') { res.status(409).json({ error: 'Conflict', - message: 'An appeal for this address is already pending' + message: 'This address is already whitelisted or under review' }); return; } @@ -168,4 +217,4 @@ router.get('/status/:ticketId', async (req: Request, res: Response) => { } }); -export default router; \ No newline at end of file +export default router; diff --git a/backend/src/api/routes/health.ts b/backend/src/api/routes/health.ts index 26ed41d..4e45f82 100644 --- a/backend/src/api/routes/health.ts +++ b/backend/src/api/routes/health.ts @@ -18,7 +18,7 @@ router.get('/', async (req: Request, res: Response) => { const healthChecks = { status: 'healthy', timestamp: new Date().toISOString(), - version: '2.0.0', + version: '0.0.0', uptime: `${Math.floor(uptime / 1000)}s`, services: { database: await checkDatabase(), diff --git a/backend/src/api/routes/whitelist.ts b/backend/src/api/routes/whitelist.ts index 7405d7e..7adda43 100644 --- a/backend/src/api/routes/whitelist.ts +++ b/backend/src/api/routes/whitelist.ts @@ -20,7 +20,7 @@ router.get('/', async (req: Request, res: Response) => { total: stats.totalWhitelistAddresses, categories, lastSyncedAt: new Date().toISOString(), - version: '2.0.0' + version: '0.0.0' }; logger.debug('Whitelist summary retrieved', { diff --git a/backend/src/app.ts b/backend/src/app.ts index 5eef2f3..7382dd2 100644 --- a/backend/src/app.ts +++ b/backend/src/app.ts @@ -42,7 +42,7 @@ app.use(`/api/${API_VERSION}/admin`, adminRouter); app.get('/', (req, res) => { res.json({ name: 'BridgeShield Backend API', - version: '2.0.0', + version: '0.0.0', description: 'AML risk check API for LI.FI cross-chain transactions', endpoints: { health: `/api/${API_VERSION}/health`, diff --git a/backend/tests/integration/api.test.ts b/backend/tests/integration/api.test.ts index d90b9f5..851935a 100644 --- a/backend/tests/integration/api.test.ts +++ b/backend/tests/integration/api.test.ts @@ -2,8 +2,15 @@ import { describe, it, expect, beforeAll, afterAll } from 'vitest'; import request from 'supertest'; import type { Express } from 'express'; import { PrismaService } from '../../src/db/prisma-client'; +import { CacheService } from '../../src/services/cache-service'; let app: Express; +let uniqueCounter = 0; + +const createUniqueAddress = () => { + uniqueCounter += 1; + return `0x${(Date.now() + uniqueCounter).toString(16).padStart(40, '0')}`.slice(0, 42); +}; beforeAll(async () => { const prisma = PrismaService.getInstance(); @@ -118,8 +125,7 @@ describe('GET /api/v1/aml/whitelist', () => { describe('POST /api/v1/aml/appeal', () => { it('creates appeal successfully', async () => { - // Use a unique address with timestamp to avoid conflicts - const uniqueAddress = `0x${Date.now().toString(16).padStart(40, '0')}`.slice(0, 42); + const uniqueAddress = createUniqueAddress(); const res = await request(app) .post('/api/v1/aml/appeal') .send({ @@ -134,7 +140,7 @@ describe('POST /api/v1/aml/appeal', () => { }); it('returns 400 for missing reason', async () => { - const uniqueAddress = `0x${(Date.now() + 1).toString(16).padStart(40, '0')}`.slice(0, 42); + const uniqueAddress = createUniqueAddress(); const res = await request(app) .post('/api/v1/aml/appeal') .send({ @@ -156,12 +162,81 @@ describe('POST /api/v1/aml/appeal', () => { }); }); +describe('POST /api/v1/admin/appeal/:id/approve', () => { + it('approves an appeal without creating a duplicate whitelist entry', async () => { + const uniqueAddress = createUniqueAddress(); + + const appealResponse = await request(app) + .post('/api/v1/aml/appeal') + .send({ + address: uniqueAddress, + chainId: 1, + reason: 'Please review and approve', + contact: 'user@example.com', + }); + + expect(appealResponse.status).toBe(201); + + const appealsResponse = await request(app).get('/api/v1/admin/appeals'); + const createdAppeal = appealsResponse.body.find((appeal: any) => appeal.address === uniqueAddress.toLowerCase()); + expect(createdAppeal).toBeDefined(); + + const approveResponse = await request(app) + .post(`/api/v1/admin/appeal/${createdAppeal.id}/approve`) + .send(); + + expect(approveResponse.status).toBe(200); + expect(approveResponse.body.success).toBe(true); + + const whitelistResponse = await request(app).get('/api/v1/admin/whitelist'); + const whitelistEntries = whitelistResponse.body.filter((entry: any) => entry.address === uniqueAddress.toLowerCase()); + expect(whitelistEntries).toHaveLength(1); + expect(whitelistEntries[0].type).toBe('APPEAL_APPROVED'); + expect(whitelistEntries[0].expiresAt).toBeNull(); + }); +}); + +describe('POST /api/v1/admin/appeal/:id/reject', () => { + it('rejects an appeal and removes temporary whitelist access from db and cache', async () => { + const uniqueAddress = createUniqueAddress(); + const cacheService = CacheService.getInstance(); + + const appealResponse = await request(app) + .post('/api/v1/aml/appeal') + .send({ + address: uniqueAddress, + chainId: 1, + reason: 'Please review and reject', + contact: 'user@example.com', + }); + + expect(appealResponse.status).toBe(201); + expect(cacheService.get(uniqueAddress, 1)).not.toBeNull(); + + const appealsResponse = await request(app).get('/api/v1/admin/appeals'); + const createdAppeal = appealsResponse.body.find((appeal: any) => appeal.address === uniqueAddress.toLowerCase()); + expect(createdAppeal).toBeDefined(); + + const rejectResponse = await request(app) + .post(`/api/v1/admin/appeal/${createdAppeal.id}/reject`) + .send({ notes: 'Rejected during test' }); + + expect(rejectResponse.status).toBe(200); + expect(rejectResponse.body.success).toBe(true); + expect(cacheService.get(uniqueAddress, 1)).toBeNull(); + + const whitelistResponse = await request(app).get('/api/v1/admin/whitelist'); + const whitelistEntries = whitelistResponse.body.filter((entry: any) => entry.address === uniqueAddress.toLowerCase()); + expect(whitelistEntries).toHaveLength(0); + }); +}); + describe('GET / (root)', () => { it('returns API info', async () => { const res = await request(app).get('/'); expect(res.status).toBe(200); expect(res.body.name).toBe('BridgeShield Backend API'); - expect(res.body.version).toBe('2.0.0'); + expect(res.body.version).toBe('0.0.0'); }); }); diff --git a/docs/bridgeshield_pr_review_zh.md b/docs/bridgeshield_pr_review_zh.md new file mode 100644 index 0000000..2491470 --- /dev/null +++ b/docs/bridgeshield_pr_review_zh.md @@ -0,0 +1,98 @@ +# BridgeShield 修复说明(评审版) + +## 变更背景 +本次提交聚焦处理会影响真实业务流转和后台操作正确性的高优先级问题,暂不改动你要求延后的 P2 项。 + +本轮修复的目标是两件事: +1. 让申诉审批链路在真实环境下保持一致性,不再出现“接口失败但状态已部分写入”的问题。 +2. 补齐后台管理端几个已经暴露出来、但尚未真正接线的交互点,降低误操作和排障成本。 + +## 核心修复点 + +### 1. 修复申诉批准时的 500 和脏状态问题 +申诉创建后,系统会先写入一条临时白名单;而批准申诉时,旧逻辑会再次尝试为同一地址插入白名单。由于数据库对 `WhitelistEntry.address` 设了唯一约束,批准动作会直接报 500。 + +更严重的是,申诉状态更新发生在白名单插入之前,导致接口虽然失败,但申诉记录已经被写成 `APPROVED`,形成数据不一致。 + +现在的处理方式: +- 在创建申诉前先检查是否已有待处理申诉或有效白名单。 +- 将“创建申诉 + 创建临时白名单 + 写审计日志”放进同一个事务。 +- 将“批准申诉 + 升级临时白名单为正式白名单”放进同一个事务。 +- 对于已存在的 `APPEAL_TEMPORARY` 记录,不再重复插入,而是原地升级为 `APPEAL_APPROVED`。 + +这样可以保证批准流程要么全部成功,要么全部不生效。 + +### 2. 修复申诉拒绝后临时白名单未清理的问题 +旧逻辑在拒绝申诉时只更新申诉状态,没有同步删除申诉创建时生成的临时白名单,也没有清理缓存。 + +这会带来一个明显风险:地址虽然被拒绝,但在临时白名单有效期内仍可能继续被系统放行。 + +现在的处理方式: +- 将“拒绝申诉 + 删除临时白名单”放进同一个事务。 +- 当事务成功后,再同步清理该地址对应的内存缓存。 + +这样拒绝操作就能真正收回临时放行权限。 + +### 3. 修复后台申诉详情中的 review note 显示问题 +后端真实数据字段是 `notes`,前端使用的是 `reviewNote`。这导致后台在走真实接口时,审批备注可能无法正常显示。 + +现在的处理方式: +- 后端返回申诉列表时,直接把字段规范成前端使用的结构。 +- 前端 API 层也增加了一层归一化处理,兼容已有 mock 数据和真实接口响应。 + +### 4. 补齐后台白名单页的新增能力 +后台白名单页原本有 `Add Address` 按钮,但点击后没有任何动作,属于明显的未完成交互。 + +现在的处理方式: +- 新增白名单创建表单。 +- 接入真实的新增接口。 +- 新增成功后自动刷新列表。 +- 新增失败时在页面内给出反馈。 + +### 5. 完善后台白名单与申诉列表的联动刷新 +批准/拒绝申诉之后,原来只刷新申诉列表,不会同步刷新白名单和日志,页面数据容易暂时不一致。 + +现在的处理方式: +- 在审批操作成功后,同时失效 `appeals`、`whitelist`、`logs` 三类查询。 +- 修复申诉列表里无 key 的 Fragment,消除 React 渲染警告。 + +### 6. 补齐后台日志页的真实可用性 +日志页原本存在两类问题: +- 页面上的日期筛选和风险等级筛选只是展示,没有实际过滤逻辑。 +- 后端日志里真实的风险因子结构是 `factors.details`,旧逻辑没有正确解析,导致风险因子信息可能丢失。 + +现在的处理方式: +- 后端增加对真实响应结构的风险因子解析。 +- 前端为日期和风险等级补齐过滤逻辑。 +- 增加风险因子列和空状态提示,方便运营和排查。 + +## 影响范围 +后端: +- 申诉创建 +- 申诉批准 +- 申诉拒绝 +- 管理后台日志数据组装 + +前端:admin +- 申诉列表 +- 白名单管理 +- 日志筛选与展示 + +## 风险控制 +本次改动尽量集中在已有业务边界内,没有引入新的外部依赖,也没有修改你要求暂缓的 P2 fallback 逻辑。 + +后端最关键的状态流转均改为事务执行,重点降低了“部分成功、部分失败”的数据一致性风险。 + +## 验证结果 +已完成以下验证: +- 后端测试通过:`88/88` +- Demo 前端构建通过 +- Admin 前端构建通过 +- 本地联调通过:后端、demo、admin 以及两侧 Vite 代理均可正常访问 + +## 暂未处理 +按要求,本轮没有继续处理以下 P2 问题: +- demo 前端的 fallback mock 行为 +- admin 前端的接口失败后 mock 回退行为 + +后续如果继续推进,可以在这一轮稳定基础上再单独处理。 diff --git a/docs/build_report.md b/docs/build_report.md index 6e85c54..b9946d7 100644 --- a/docs/build_report.md +++ b/docs/build_report.md @@ -1,7 +1,7 @@ -# BridgeShield v2.0 — 构建与测试报告 +# BridgeShield v0.0 — 构建与测试报告 > 生成时间:2026-04-09 -> 项目版本:v2.0.0 +> 项目版本:v0.0.0 > 构建状态:✅ 全部通过 --- diff --git a/docs/dev_docs.md b/docs/dev_docs.md index ababc30..43def8c 100644 --- a/docs/dev_docs.md +++ b/docs/dev_docs.md @@ -682,7 +682,7 @@ class RiskScorer { "circuitBreaker": "closed" }, "uptime": 3600, - "version": "2.0.0" + "version": "0.0.0" } ``` diff --git a/docs/dev_log.md b/docs/dev_log.md index 1866eae..930b53b 100644 --- a/docs/dev_log.md +++ b/docs/dev_log.md @@ -2,60 +2,146 @@ --- -## 2026-04-09 — v2.0.0 初始构建 +## 2026-04-10 — v0.0.4 新增 @bridgeshield/sdk 包 ### 概述 -从零完成 BridgeShield 全栈项目的初始构建,包含后端 API、Demo 前端、Admin 后台管理三个子项目。 +新增官方 SDK 包,方便外部应用集成 BridgeShield AML API。 ### 变更内容 -**Backend API (`backend/`)** -- 搭建 Express + TypeScript + Prisma + SQLite 项目框架 -- 实现 4 个核心 API:`/aml/check`、`/aml/whitelist`、`/aml/appeal`、`/health` -- 实现风险评分引擎(加权规则:SANCTION +85, HACKER +75, MIXER +70, SCAM +55) -- 实现内存缓存服务(node-cache,分级 TTL,无 Redis) -- 实现 Opossum 熔断器(超时 3s,错误率 50% 触发,30s 恢复) -- 实现本地风险数据预加载(Map/Set, O(1) 查询) -- 准备 5 个 JSON 数据文件(OFAC、黑客、混币器、诈骗、白名单共 30+ 条记录) -- 添加 express-rate-limit 限流、helmet 安全头、CORS、winston 结构化日志 -- 修复:check 路由 DB 日志写入改为非阻塞(`.catch()`),避免 DB 故障影响 API 响应 -- 修复:熔断器每请求使用唯一名称,解决闭包复用 bug +**packages/sdk/ — 新增 SDK 包** +- `BridgeShieldClient` 类:封装所有 API 调用 +- `checkAddress()` — 地址风险检查 +- `submitAppeal()` — 提交申诉 +- `getAppealStatus()` — 查询申诉状态 +- `getWhitelistSummary()` — 白名单摘要 +- `healthCheck()` — 健康检查 -**Frontend Demo (`frontend-demo/`)** -- 搭建 React 18 + Vite + TypeScript + TailwindCSS v3 项目 -- 实现 CheckerPage(地址风险检查主页)和 ComparePage(LI.FI 对比演示页) -- 实现 7 个组件:AddressInput、RiskResultCard、RiskScoreMeter(SVG 环形仪表盘)、FactorList、LiveStats、CodeSnippet、ComparePanel -- 集成 Framer Motion 动画(评分滚动、因子逐条出现、页面切换) -- 集成 TanStack Query + API Mock 降级(后端不可用时自动使用本地数据) -- 暗色赛博安全主题(#0A0E1A 背景、#00D4AA 主色、#FF3B3B 危险色) +**SDK 类型定义 (`packages/sdk/src/types.ts`)** +- 完整 TypeScript 类型定义 +- 错误类层次结构:`BridgeShieldError` → `ApiError`、`NetworkError`、`ValidationError` +- 所有请求/响应类型 -**Frontend Admin (`frontend-admin/`)** -- 搭建 React 18 + Vite + TypeScript + TailwindCSS 项目 -- 实现 4 个页面:DashboardPage(仪表盘)、AppealPage(申诉管理)、WhitelistPage(白名单管理)、LogsPage(检查日志) -- 集成 Recharts 图表(折线图、饼图) -- 浅色专业管理风格(与 Demo 完全不同) -- 全部页面配备 Mock 数据降级 +**SDK 测试 (`packages/sdk/__tests__/client.test.ts`)** +- 21 个单元测试用例 +- 覆盖所有 API 方法 +- 覆盖错误处理路径 -**测试 (`backend/tests/`)** -- 67 个测试用例,全部通过 -- 单元测试:validator (25)、risk-scorer (9)、cache-service (8)、risk-data-loader (9) -- 集成测试:API 端点 (16),使用 supertest 对 Express app 进行请求级验证 +**SDK 文档 (`packages/sdk/README.md`)** +- 完整使用文档 +- API 参考 +- 错误处理指南 +- 代码示例 -**基础设施** -- docker-compose.yml + docker-compose.dev.yml(无 Redis) -- 各子项目 Dockerfile -- README.md +### 构建验证 +| 子项目 | `npm run build` | `npm test` | +|--------|-----------------|------------| +| SDK | ✅ 通过 | ✅ 21/21 | + +--- + +## 2026-04-10 — v0.0.3 前后端分级错误处理 + +### 概述 +实现分级错误处理策略:4xx 客户端错误直接抛出,5xx/网络错误降级到 mock 数据,防止错误被静默隐藏。 + +### 变更内容 + +**Frontend Demo — API 客户端 (`frontend-demo/src/api/bridgeshield.ts`)** +- `checkAddress`:4xx 错误直接抛出,5xx/网络错误 fallback 到 mock +- `submitAppeal`:错误时返回 `{ success: false, error }` 而非伪装成功 +- fallback 时附带明确提示信息 + +**Frontend Admin — API 客户端 (`frontend-admin/src/api/admin-api.ts`)** +- `apiFetch`:4xx 错误抛出(console.error),5xx/网络错误返回 mock 数据(console.warn) +- 所有 Admin 页面支持错误状态展示 + +### 错误处理策略 +| 错误类型 | Demo 前端 | Admin 前端 | +|----------|-----------|------------| +| 4xx (输入错误) | 直接抛出 | 抛出 + console.error | +| 5xx (服务器错误) | Fallback + 提示 | Mock 数据 + console.warn | +| 网络/超时错误 | Fallback + 提示 | Mock 数据 + console.warn | ### 构建验证 | 子项目 | `npm run build` | `npm test` | |--------|-----------------|------------| -| Backend | ✅ 通过 | ✅ 67/67 | | Frontend Demo | ✅ 通过 | — | | Frontend Admin | ✅ 通过 | — | --- -## 2026-04-09 — v2.0.1 真实数据 + 前后端联动修复 +## 2026-04-10 — v0.0.2 修复申诉流转与后台管理问题 + +### 概述 +修复申诉审批链路的 500 错误和脏状态问题,补齐后台管理端未完成的交互点。 + +### 变更内容 + +**Backend — Admin 路由 (`src/api/routes/admin.ts`)** +- 新增 `parseRiskFactors()` 函数:统一解析 `factors.details`、`factors` 数组、`riskType` 等多种响应结构 +- 修复 `GET /admin/appeals` 返回格式:`notes` → `reviewNote`,添加 `reviewedAt` +- 修复 `POST /admin/appeal/:id/approve`:使用事务防止重复创建白名单,将 `APPEAL_TEMPORARY` 升级为 `APPEAL_APPROVED` +- 修复 `POST /admin/appeal/:id/reject`:使用事务删除临时白名单,清理缓存 +- 添加 404/409 错误处理(申诉不存在/已审批) + +**Backend — Appeal 路由 (`src/api/routes/appeal.ts`)** +- 新增创建申诉前的重复检查:防止重复待处理申诉、清理已过期临时白名单 +- 将"创建申诉 + 创建临时白名单 + 写审计日志"放入同一事务 +- 修复 Prisma 错误处理:使用 `PrismaClientKnownRequestError` 的 `P2002` 码 + +**Frontend Admin — API 客户端 (`src/api/admin-api.ts`)** +- 新增 `normalizeAppeal()` 和 `normalizeWhitelistEntry()` 归一化函数 +- `getAppeals()`、`getWhitelist()` 返回归一化后的数据 +- `rejectAppeal()` 支持发送 `notes` 参数 + +**Frontend Admin — AppealTable (`src/components/AppealTable.tsx`)** +- 修复 React key 警告:使用 `` 替代空 `<>` +- 审批/拒绝成功后同时失效 `appeals`、`whitelist`、`logs` 三类查询 +- 添加空状态提示和错误信息展示 + +**Frontend Admin — WhitelistTable (`src/components/WhitelistTable.tsx`)** +- 新增 `APPEAL_TEMPORARY` 状态的橙色样式 +- 添加空状态提示和错误信息展示 + +**Frontend Admin — LogsPage (`src/pages/LogsPage.tsx`)** +- 实现日期和风险等级筛选功能(`useMemo` 优化) +- 新增"风险因子"列,正确展示 `riskFactors` +- 添加空状态提示和错误信息展示 + +**Frontend Admin — WhitelistPage (`src/pages/WhitelistPage.tsx`)** +- 新增白名单新增表单(地址、类型、Chain ID、标签) +- 接入 `addToWhitelist` API,新增成功后自动刷新列表 +- 添加错误状态展示 + +**Frontend Admin — Types (`src/types/index.ts`)** +- `WhitelistEntry.type` 新增 `APPEAL_TEMPORARY` 类型 + +**测试 (`backend/tests/integration/api.test.ts`)** +- 新增 `createUniqueAddress()` 辅助函数避免测试地址冲突 +- 新增 `POST /api/v1/admin/appeal/:id/approve` 测试 +- 新增 `POST /api/v1/admin/appeal/:id/reject` 测试 + +### 修复的问题 +| 问题 | 状态 | +|------|------| +| 申诉批准时 500 错误(唯一约束冲突) | ✅ 已修复 | +| 申诉状态部分更新(脏状态) | ✅ 已修复 | +| 申诉拒绝后临时白名单未清理 | ✅ 已修复 | +| 审批备注字段不匹配(notes vs reviewNote) | ✅ 已修复 | +| 白名单页 Add Address 按钮无效 | ✅ 已修复 | +| 日志页筛选功能无效 | ✅ 已修复 | +| 风险因子信息丢失 | ✅ 已修复 | + +### 构建验证 +| 子项目 | `npm run build` | `npm test` | +|--------|-----------------|------------| +| Backend | ✅ 通过 | ✅ 88/88 | +| Frontend Admin | ✅ 通过 | — | + +--- + +## 2026-04-09 — v0.0.1 真实数据 + 前后端联动修复 ### 概述 替换虚假数据为真实 OFAC/黑客地址,修复前后端 API 合约不匹配问题,实现完整联动。 @@ -120,3 +206,56 @@ | Backend | ✅ 通过 | ✅ 86/86 | | Frontend Demo | ✅ 通过 | — | | Frontend Admin | ✅ 通过 | — | + +--- + +## 2026-04-09 — v0.0.0 初始构建 + +### 概述 +从零完成 BridgeShield 全栈项目的初始构建,包含后端 API、Demo 前端、Admin 后台管理三个子项目。 + +### 变更内容 + +**Backend API (`backend/`)** +- 搭建 Express + TypeScript + Prisma + SQLite 项目框架 +- 实现 4 个核心 API:`/aml/check`、`/aml/whitelist`、`/aml/appeal`、`/health` +- 实现风险评分引擎(加权规则:SANCTION +85, HACKER +75, MIXER +70, SCAM +55) +- 实现内存缓存服务(node-cache,分级 TTL,无 Redis) +- 实现 Opossum 熔断器(超时 3s,错误率 50% 触发,30s 恢复) +- 实现本地风险数据预加载(Map/Set, O(1) 查询) +- 准备 5 个 JSON 数据文件(OFAC、黑客、混币器、诈骗、白名单共 30+ 条记录) +- 添加 express-rate-limit 限流、helmet 安全头、CORS、winston 结构化日志 +- 修复:check 路由 DB 日志写入改为非阻塞(`.catch()`),避免 DB 故障影响 API 响应 +- 修复:熔断器每请求使用唯一名称,解决闭包复用 bug + +**Frontend Demo (`frontend-demo/`)** +- 搭建 React 18 + Vite + TypeScript + TailwindCSS v3 项目 +- 实现 CheckerPage(地址风险检查主页)和 ComparePage(LI.FI 对比演示页) +- 实现 7 个组件:AddressInput、RiskResultCard、RiskScoreMeter(SVG 环形仪表盘)、FactorList、LiveStats、CodeSnippet、ComparePanel +- 集成 Framer Motion 动画(评分滚动、因子逐条出现、页面切换) +- 集成 TanStack Query + API Mock 降级(后端不可用时自动使用本地数据) +- 暗色赛博安全主题(#0A0E1A 背景、#00D4AA 主色、#FF3B3B 危险色) + +**Frontend Admin (`frontend-admin/`)** +- 搭建 React 18 + Vite + TypeScript + TailwindCSS 项目 +- 实现 4 个页面:DashboardPage(仪表盘)、AppealPage(申诉管理)、WhitelistPage(白名单管理)、LogsPage(检查日志) +- 集成 Recharts 图表(折线图、饼图) +- 浅色专业管理风格(与 Demo 完全不同) +- 全部页面配备 Mock 数据降级 + +**测试 (`backend/tests/`)** +- 67 个测试用例,全部通过 +- 单元测试:validator (25)、risk-scorer (9)、cache-service (8)、risk-data-loader (9) +- 集成测试:API 端点 (16),使用 supertest 对 Express app 进行请求级验证 + +**基础设施** +- docker-compose.yml + docker-compose.dev.yml(无 Redis) +- 各子项目 Dockerfile +- README.md + +### 构建验证 +| 子项目 | `npm run build` | `npm test` | +|--------|-----------------|------------| +| Backend | ✅ 通过 | ✅ 67/67 | +| Frontend Demo | ✅ 通过 | — | +| Frontend Admin | ✅ 通过 | — | diff --git a/frontend-admin/package.json b/frontend-admin/package.json index 73eb783..542b92b 100644 --- a/frontend-admin/package.json +++ b/frontend-admin/package.json @@ -1,6 +1,6 @@ { "name": "bridgeshield-admin", - "version": "2.0.0", + "version": "0.0.0", "type": "module", "scripts": { "dev": "vite --port 5174", diff --git a/frontend-admin/src/api/admin-api.ts b/frontend-admin/src/api/admin-api.ts index 85cd27e..3c57128 100644 --- a/frontend-admin/src/api/admin-api.ts +++ b/frontend-admin/src/api/admin-api.ts @@ -11,14 +11,50 @@ async function apiFetch(endpoint: string, options: RequestInit = {}): Promise }, ...options, }); - if (!res.ok) throw new Error('API request failed'); + + if (res.status >= 400 && res.status < 500) { + const errorData = await res.json().catch(() => ({})); + console.error(`[BridgeShield API] Client error (${res.status}):`, errorData.message || endpoint); + throw new Error(errorData.message || `Client error: ${res.status}`); + } + + if (!res.ok) { + console.warn(`[BridgeShield API] Server error (${res.status}), using mock data for ${endpoint}`); + return getMockData(endpoint, options.method || 'GET') as T; + } + return await res.json(); - } catch (e) { - console.warn('API unavailable, using mock data', e); + } catch (error) { + if (error instanceof Error && error.message.startsWith('Client error:')) { + throw error; + } + console.warn(`[BridgeShield API] Network/timeout error, using mock data for ${endpoint}`, error); return getMockData(endpoint, options.method || 'GET') as T; } } +const normalizeAppeal = (appeal: any): Appeal => ({ + id: appeal.id, + ticketId: appeal.ticketId, + address: appeal.address, + reason: appeal.reason, + contact: appeal.contact, + status: appeal.status, + reviewNote: appeal.reviewNote || appeal.notes, + reviewedAt: appeal.reviewedAt || undefined, + createdAt: appeal.createdAt, +}); + +const normalizeWhitelistEntry = (entry: any): WhitelistEntry => ({ + id: entry.id, + address: entry.address, + type: entry.type, + label: entry.label, + chainId: entry.chainId, + expiresAt: entry.expiresAt || undefined, + createdAt: entry.createdAt, +}); + // Mock data generators function getMockData(endpoint: string, method: string): any { // Dashboard stats @@ -207,15 +243,25 @@ export const adminApi = { getRiskTrend: () => apiFetch('/api/v1/admin/dashboard/risk-trend'), getRiskDistribution: () => apiFetch<{ levels: RiskDistributionItem[]; sources: RiskDistributionItem[] }>('/api/v1/admin/dashboard/risk-distribution'), - getAppeals: () => apiFetch('/api/v1/admin/appeals'), + getAppeals: async () => { + const appeals = await apiFetch('/api/v1/admin/appeals'); + return appeals.map(normalizeAppeal); + }, approveAppeal: (id: string) => apiFetch(`/api/v1/admin/appeal/${id}/approve`, { method: 'POST' }), - rejectAppeal: (id: string) => apiFetch(`/api/v1/admin/appeal/${id}/reject`, { method: 'POST' }), + rejectAppeal: (id: string, notes?: string) => + apiFetch(`/api/v1/admin/appeal/${id}/reject`, { + method: 'POST', + body: JSON.stringify(notes ? { notes } : {}), + }), - getWhitelist: () => apiFetch('/api/v1/admin/whitelist'), + getWhitelist: async () => { + const entries = await apiFetch('/api/v1/admin/whitelist'); + return entries.map(normalizeWhitelistEntry); + }, addToWhitelist: (entry: Omit) => apiFetch('/api/v1/admin/whitelist', { method: 'POST', body: JSON.stringify(entry) }), removeFromWhitelist: (id: string) => apiFetch(`/api/v1/admin/whitelist/${id}`, { method: 'DELETE' }), getLogs: () => apiFetch('/api/v1/admin/logs'), -}; \ No newline at end of file +}; diff --git a/frontend-admin/src/components/AppealTable.tsx b/frontend-admin/src/components/AppealTable.tsx index 51556d2..4fd3cec 100644 --- a/frontend-admin/src/components/AppealTable.tsx +++ b/frontend-admin/src/components/AppealTable.tsx @@ -1,4 +1,4 @@ -import { useState } from 'react'; +import { Fragment, useState } from 'react'; import { Appeal } from '../types'; import { adminApi } from '../api/admin-api'; import { useMutation, useQueryClient } from '@tanstack/react-query'; @@ -16,12 +16,24 @@ export default function AppealTable({ appeals, filter }: AppealTableProps) { const approveMutation = useMutation({ mutationFn: (id: string) => adminApi.approveAppeal(id), - onSuccess: () => queryClient.invalidateQueries({ queryKey: ['appeals'] }), + onSuccess: async () => { + await Promise.all([ + queryClient.invalidateQueries({ queryKey: ['appeals'] }), + queryClient.invalidateQueries({ queryKey: ['whitelist'] }), + queryClient.invalidateQueries({ queryKey: ['logs'] }), + ]); + }, }); const rejectMutation = useMutation({ mutationFn: (id: string) => adminApi.rejectAppeal(id), - onSuccess: () => queryClient.invalidateQueries({ queryKey: ['appeals'] }), + onSuccess: async () => { + await Promise.all([ + queryClient.invalidateQueries({ queryKey: ['appeals'] }), + queryClient.invalidateQueries({ queryKey: ['whitelist'] }), + queryClient.invalidateQueries({ queryKey: ['logs'] }), + ]); + }, }); const getStatusBadge = (status: string) => { @@ -51,7 +63,7 @@ export default function AppealTable({ appeals, filter }: AppealTableProps) { {filteredAppeals.map((appeal) => ( - <> + setExpandedId(expandedId === appeal.id ? null : appeal.id)}> {appeal.ticketId} {appeal.address.slice(0, 6)}...{appeal.address.slice(-4)} @@ -100,10 +112,24 @@ export default function AppealTable({ appeals, filter }: AppealTableProps) { )} - + ))} + + {filteredAppeals.length === 0 && ( + + + No appeals match the selected filter. + + + )} + + {(approveMutation.error instanceof Error || rejectMutation.error instanceof Error) && ( +
+ {approveMutation.error instanceof Error ? approveMutation.error.message : rejectMutation.error instanceof Error ? rejectMutation.error.message : ''} +
+ )} ); -} \ No newline at end of file +} diff --git a/frontend-admin/src/components/WhitelistTable.tsx b/frontend-admin/src/components/WhitelistTable.tsx index 8a542bb..0188e50 100644 --- a/frontend-admin/src/components/WhitelistTable.tsx +++ b/frontend-admin/src/components/WhitelistTable.tsx @@ -26,6 +26,7 @@ export default function WhitelistTable({ entries, searchQuery }: WhitelistTableP KNOWN_PROTOCOL: 'bg-purple-100 text-purple-800', BRIDGE_CONTRACT: 'bg-green-100 text-green-800', APPEAL_APPROVED: 'bg-yellow-100 text-yellow-800', + APPEAL_TEMPORARY: 'bg-orange-100 text-orange-800', }; return ( @@ -72,8 +73,22 @@ export default function WhitelistTable({ entries, searchQuery }: WhitelistTableP ))} + + {filteredEntries.length === 0 && ( + + + No whitelist entries match the current search. + + + )} + + {removeMutation.error instanceof Error && ( +
+ {removeMutation.error.message} +
+ )} ); -} \ No newline at end of file +} diff --git a/frontend-admin/src/pages/LogsPage.tsx b/frontend-admin/src/pages/LogsPage.tsx index 6e6e266..7644d11 100644 --- a/frontend-admin/src/pages/LogsPage.tsx +++ b/frontend-admin/src/pages/LogsPage.tsx @@ -1,12 +1,24 @@ +import { useMemo, useState } from 'react'; import { useQuery } from '@tanstack/react-query'; import { adminApi } from '../api/admin-api'; export default function LogsPage() { - const { data: logs = [] } = useQuery({ + const [selectedDate, setSelectedDate] = useState(''); + const [selectedRiskLevel, setSelectedRiskLevel] = useState<'ALL' | 'LOW' | 'MEDIUM' | 'HIGH'>('ALL'); + + const { data: logs = [], error } = useQuery({ queryKey: ['logs'], queryFn: adminApi.getLogs, }); + const filteredLogs = useMemo(() => { + return logs.filter((log) => { + const matchesRiskLevel = selectedRiskLevel === 'ALL' || log.riskLevel === selectedRiskLevel; + const matchesDate = !selectedDate || log.createdAt.slice(0, 10) === selectedDate; + return matchesRiskLevel && matchesDate; + }); + }, [logs, selectedDate, selectedRiskLevel]); + const getRiskLevelBadge = (level: string) => { switch (level) { case 'LOW': @@ -38,8 +50,17 @@ export default function LogsPage() {

Check Logs

- - setSelectedDate(event.target.value)} + className="px-3 py-2 border border-gray-300 rounded-md text-sm" + /> + setFormValues((current) => ({ ...current, address: event.target.value }))} + className="w-full rounded-md border border-gray-300 px-4 py-2 font-mono text-sm focus:border-transparent focus:outline-none focus:ring-2 focus:ring-primary" + placeholder="0x..." + /> +
+ +
+ + +
+ +
+ + setFormValues((current) => ({ ...current, chainId: event.target.value }))} + className="w-full rounded-md border border-gray-300 px-4 py-2 text-sm focus:border-transparent focus:outline-none focus:ring-2 focus:ring-primary" + /> +
+ +
+ + setFormValues((current) => ({ ...current, label: event.target.value }))} + className="w-full rounded-md border border-gray-300 px-4 py-2 text-sm focus:border-transparent focus:outline-none focus:ring-2 focus:ring-primary" + placeholder="Describe why this address is safe" + /> +
+ + {addMutation.error instanceof Error && ( +
+ {addMutation.error.message} +
+ )} + +
+ +
+ + )} +
); -} \ No newline at end of file +} diff --git a/frontend-admin/src/types/index.ts b/frontend-admin/src/types/index.ts index e8de1b8..3d12df3 100644 --- a/frontend-admin/src/types/index.ts +++ b/frontend-admin/src/types/index.ts @@ -13,7 +13,7 @@ export interface Appeal { export interface WhitelistEntry { id: string; address: string; - type: 'LIFI_OFFICIAL' | 'KNOWN_PROTOCOL' | 'BRIDGE_CONTRACT' | 'APPEAL_APPROVED'; + type: 'LIFI_OFFICIAL' | 'KNOWN_PROTOCOL' | 'BRIDGE_CONTRACT' | 'APPEAL_APPROVED' | 'APPEAL_TEMPORARY'; label: string; chainId?: number; expiresAt?: string; @@ -54,4 +54,4 @@ export interface RiskDistributionItem { name: string; value: number; color: string; -} \ No newline at end of file +} diff --git a/frontend-demo/package.json b/frontend-demo/package.json index 608df8a..c6ff419 100644 --- a/frontend-demo/package.json +++ b/frontend-demo/package.json @@ -1,6 +1,6 @@ { "name": "bridgeshield-demo", - "version": "2.0.0", + "version": "0.0.0", "type": "module", "scripts": { "dev": "vite", diff --git a/frontend-demo/src/api/bridgeshield.ts b/frontend-demo/src/api/bridgeshield.ts index 7835920..c256d9d 100644 --- a/frontend-demo/src/api/bridgeshield.ts +++ b/frontend-demo/src/api/bridgeshield.ts @@ -132,39 +132,60 @@ export const checkAddress = async (address: string, chainId: number = 1): Promis signal, }); + // 4xx errors: input problem - throw to show real error + if (response.status >= 400 && response.status < 500) { + const errorData = await response.json().catch(() => ({})); + throw new Error(errorData.message || `Input error: ${response.status}`); + } + + // 5xx errors: server problem - fallback to mock if (!response.ok) { - throw new Error(`API error: ${response.status}`); + // Fallback to mock data for server errors + const mockKey = Object.keys(MOCK_RESULTS).find(k => k.toLowerCase() === address.toLowerCase()); + if (mockKey) { + const mockResult = MOCK_RESULTS[mockKey]; + await new Promise(resolve => setTimeout(resolve, Math.random() * 200 + 50)); + return { ...mockResult, fallbackReason: 'Server error, using cached data' }; + } + return { + address, + riskScore: Math.floor(Math.random() * 50) + 10, + riskLevel: 'MEDIUM', + action: 'REVIEW', + riskFactors: ['Server temporarily unavailable', 'Please try again later'], + recommendation: 'System degraded - manual review recommended.', + cached: false, + processingTimeMs: 150, + fallback: true, + fallbackReason: 'Server error, using fallback data' + }; } const backendResponse = await response.json(); return transformCheckResult(backendResponse); - } catch (error) { - // First, check if we have mock data for this address (case-insensitive) - const mockKey = Object.keys(MOCK_RESULTS).find(k => k.toLowerCase() === address.toLowerCase()); - if (mockKey) { - const mockResult = MOCK_RESULTS[mockKey]; - // Simulate network delay - await new Promise(resolve => setTimeout(resolve, Math.random() * 200 + 50)); - return mockResult; + } catch (error: unknown) { + if (error instanceof TypeError || error instanceof DOMException || (error as any)?.name === 'AbortError') { + const mockKey = Object.keys(MOCK_RESULTS).find(k => k.toLowerCase() === address.toLowerCase()); + if (mockKey) { + const mockResult = MOCK_RESULTS[mockKey]; + await new Promise(resolve => setTimeout(resolve, Math.random() * 200 + 50)); + return { ...mockResult, fallbackReason: 'Network error, using cached data' }; + } + return { + address, + riskScore: Math.floor(Math.random() * 50) + 10, + riskLevel: 'MEDIUM', + action: 'REVIEW', + riskFactors: ['Network connection failed', 'Unable to reach AML service'], + recommendation: 'System unavailable - manual review required.', + cached: false, + processingTimeMs: 150, + fallback: true, + fallbackReason: 'Network error, using fallback data' + }; } - - // Fallback to generic mock result if API fails and no specific mock exists - return { - address, - riskScore: Math.floor(Math.random() * 50) + 10, - riskLevel: 'MEDIUM', - action: 'REVIEW', - riskFactors: [ - 'Unknown address, no AML history available', - 'No negative flags found, but not whitelisted', - 'Limited transaction history on chain' - ], - recommendation: 'Review manually before allowing transaction.', - cached: false, - processingTimeMs: 150, - fallback: true, - fallbackReason: 'API unavailable, using fallback data' - }; + // Re-throw 4xx validation errors + throw error; } }; @@ -182,7 +203,7 @@ export const getWhitelist = async () => { } }; -export const submitAppeal = async (address: string, reason: string, contact: string) => { +export const submitAppeal = async (address: string, reason: string, contact: string): Promise<{ success: boolean; error?: string }> => { try { const signal = createTimeoutSignal(TIMEOUT); const response = await fetch(`${BASE_URL}/api/v1/aml/appeal`, { @@ -191,10 +212,19 @@ export const submitAppeal = async (address: string, reason: string, contact: str body: JSON.stringify({ address, chainId: 1, reason, contact }), signal, }); - return response.ok; + + if (response.status >= 400 && response.status < 500) { + const errorData = await response.json().catch(() => ({})); + return { success: false, error: errorData.message || 'Invalid input' }; + } + + if (!response.ok) { + return { success: false, error: 'Server error, please try again later' }; + } + + return { success: true }; } catch (error) { - // Mock success - return true; + return { success: false, error: 'Network error, submission failed' }; } }; diff --git a/packages/sdk/README.md b/packages/sdk/README.md new file mode 100644 index 0000000..dfbfe7d --- /dev/null +++ b/packages/sdk/README.md @@ -0,0 +1,320 @@ +# @bridgeshield/sdk + +[![npm version](https://img.shields.io/npm/v/@bridgeshield/sdk.svg)](https://www.npmjs.com/package/@bridgeshield/sdk) +[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) +[![TypeScript](https://img.shields.io/badge/TypeScript-5.0%2B-blue.svg)](https://www.typescriptlang.org/) + +Official JavaScript/TypeScript SDK for BridgeShield AML Gateway. Check blockchain addresses for AML risk, submit appeals, and integrate LI.FI compliance into your DeFi applications. + +## Features + +- **TypeScript First**: Full TypeScript support with complete type definitions +- **Tree Shakeable**: ESM and CJS builds optimized for bundlers +- **Error Handling**: Hierarchical error classes for precise error handling +- **Isomorphic**: Works in both Node.js (18+) and browsers +- **Zero Dependencies**: Lightweight with no external runtime dependencies + +## Installation + +```bash +# npm +npm install @bridgeshield/sdk + +# yarn +yarn add @bridgeshield/sdk + +# pnpm +pnpm add @bridgeshield/sdk +``` + +## Quick Start + +```typescript +import { BridgeShieldClient } from '@bridgeshield/sdk'; + +const client = new BridgeShieldClient({ + baseUrl: 'https://api.bridgeshield.io', +}); + +const result = await client.checkAddress({ + address: '0x1234567890abcdef1234567890abcdef12345678', + chainId: 1, +}); + +console.log(result.riskLevel); // 'LOW' | 'MEDIUM' | 'HIGH' +console.log(result.decision); // 'ALLOW' | 'REVIEW' | 'BLOCK' +``` + +## API Reference + +### Constructor + +```typescript +const client = new BridgeShieldClient({ + baseUrl: string; // Required: API base URL + apiKey?: string; // Optional: API key for authentication + timeout?: number; // Optional: Request timeout in ms (default: 5000) +}); +``` + +### Methods + +#### `checkAddress(params)` + +Check a blockchain address for AML risk. + +```typescript +const result = await client.checkAddress({ + address: string; // Required: Address to check + chainId?: number; // Optional: Chain ID (default: 1) + amount?: string; // Optional: Transaction amount + senderAddress?: string; // Optional: Sender address +}); + +// Response +interface CheckAddressResponse { + checkId: string; + address: string; + chainId: number; + riskScore: number; // 0-100 + riskLevel: 'LOW' | 'MEDIUM' | 'HIGH'; + decision: 'ALLOW' | 'REVIEW' | 'BLOCK'; + riskType?: string; + factors?: { details?: string[] }; + isWhitelisted: boolean; + cacheHit: boolean; + cacheTier?: string; + fallback?: boolean; + fallbackReason?: string; +} +``` + +#### `submitAppeal(params)` + +Submit an appeal for an incorrectly flagged address. + +```typescript +const result = await client.submitAppeal({ + address: string; // Required: Address to appeal + reason: string; // Required: Reason for appeal + contact?: string; // Optional: Contact email +}); + +// Response +interface SubmitAppealResponse { + ticketId: string; + address: string; + chainId: number; + status: 'PENDING' | 'APPROVED' | 'REJECTED'; + estimatedReviewAt: string; + message: string; + nextSteps: string[]; +} +``` + +#### `getAppealStatus(ticketId)` + +Check the status of an appeal. + +```typescript +const result = await client.getAppealStatus('APT-20260410-001'); + +// Response +interface AppealStatusResponse { + ticketId: string; + status: 'PENDING' | 'APPROVED' | 'REJECTED'; + createdAt: string; + estimatedReviewAt: string; + address?: string; + chainId?: number; + reason?: string; + contact?: string; + reviewedAt?: string; + reviewer?: string; + decision?: 'APPROVED' | 'REJECTED'; + notes?: string; +} +``` + +#### `getWhitelistSummary()` + +Get whitelist statistics. + +```typescript +const result = await client.getWhitelistSummary(); + +// Response +interface WhitelistSummary { + total: number; + categories: Array<{ category: string; count: number }>; + lastSyncedAt: string; + version: string; +} +``` + +#### `healthCheck()` + +Check API health status. + +```typescript +const result = await client.healthCheck(); + +// Response +interface HealthCheckResponse { + status: 'healthy' | 'degraded' | 'unhealthy'; + timestamp: string; + version: string; + uptime: string; + services: { + database: { healthy: boolean; status: string }; + riskData: { healthy: boolean; status: string }; + cache: { healthy: boolean; status: string }; + redis: string; + }; +} +``` + +### Utility Methods + +```typescript +client.setBaseUrl('https://new-api.bridgeshield.io'); +client.setApiKey('your-api-key'); +``` + +## Error Handling + +The SDK provides hierarchical error classes for precise error handling: + +```typescript +import { + BridgeShieldClient, + BridgeShieldError, + ApiError, + NetworkError, + ValidationError, +} from '@bridgeshield/sdk'; + +try { + const result = await client.checkAddress({ address: '0x...' }); +} catch (error) { + if (error instanceof ValidationError) { + console.log('Invalid input:', error.message); + console.log('Field:', error.field); + } else if (error instanceof ApiError) { + console.log('API error:', error.statusCode, error.message); + console.log('Response:', error.response); + } else if (error instanceof NetworkError) { + console.log('Network issue:', error.message); + } else if (error instanceof BridgeShieldError) { + console.log('General error:', error.message); + } +} +``` + +### Error Types + +| Error Type | HTTP Status | Description | +|------------|-------------|-------------| +| `ValidationError` | 400 | Invalid input parameters | +| `ApiError` | 4xx/5xx | Server responded with error | +| `NetworkError` | - | Network failure or timeout | + +## Examples + +### Check Multiple Addresses + +```typescript +const addresses = [ + '0x098B716B8Aaf21512996dC57EB0615e2383E2f96', + '0xd90e2f925DA726b50C4Ed8D0Fb90Ad053324F31b', + '0xae7ab96520DE3A18E5e111B5EaAb095312D7fE84', +]; + +const results = await Promise.all( + addresses.map(addr => client.checkAddress({ address: addr })) +); + +results.forEach((result, i) => { + console.log(`${addresses[i]}: ${result.decision}`); +}); +``` + +### Submit Appeal with Contact + +```typescript +try { + const appeal = await client.submitAppeal({ + address: '0x123...', + reason: 'This is my personal wallet, not associated with any hack.', + contact: 'user@example.com', + }); + + console.log(`Appeal submitted: ${appeal.ticketId}`); + console.log(`Status: ${appeal.status}`); + console.log(`Estimated review: ${appeal.estimatedReviewAt}`); +} catch (error) { + if (error instanceof ApiError && error.statusCode === 409) { + console.log('Appeal already exists for this address'); + } +} +``` + +### Check Appeal Status + +```typescript +const status = await client.getAppealStatus('APT-20260410-001'); + +if (status.status === 'APPROVED') { + console.log('Appeal approved!'); +} else if (status.status === 'REJECTED') { + console.log('Appeal rejected'); + console.log('Notes:', status.notes); +} else { + console.log('Still pending...'); +} +``` + +### Monitor API Health + +```typescript +const health = await client.healthCheck(); + +if (health.status === 'healthy') { + console.log('All systems operational'); +} else if (health.status === 'degraded') { + console.log('System degraded'); +} else { + console.log('System unavailable'); +} +``` + +## Configuration + +### Node.js 18+ + +Node.js 18+ has native `fetch` support. No additional configuration needed. + +### Browser + +Works with any modern browser that supports `fetch` API. + +### API Keys + +For production use, you can set an API key: + +```typescript +const client = new BridgeShieldClient({ + baseUrl: 'https://api.bridgeshield.io', + apiKey: process.env.BRIDGESHIELD_API_KEY, +}); +``` + +## License + +MIT License - see [LICENSE](LICENSE) for details. + +## Links + +- [Documentation](https://docs.bridgeshield.io) +- [API Reference](https://api.bridgeshield.io/api/v1/docs) +- [BridgeShield Website](https://bridgeshield.io) diff --git a/packages/sdk/__tests__/client.test.ts b/packages/sdk/__tests__/client.test.ts new file mode 100644 index 0000000..209abc9 --- /dev/null +++ b/packages/sdk/__tests__/client.test.ts @@ -0,0 +1,364 @@ +import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; +import { BridgeShieldClient } from '../src/client'; +import type { ApiError, NetworkError, ValidationError } from '../src/types'; + +describe('BridgeShieldClient', () => { + let client: BridgeShieldClient; + const baseUrl = 'https://api.test.bridgeshield.io'; + + beforeEach(() => { + client = new BridgeShieldClient({ baseUrl }); + vi.stubGlobal('fetch', vi.fn()); + }); + + afterEach(() => { + vi.restoreAllMocks(); + }); + + describe('constructor', () => { + it('should create client with baseUrl', () => { + const c = new BridgeShieldClient({ baseUrl: 'https://test.com' }); + expect(c).toBeInstanceOf(BridgeShieldClient); + }); + + it('should trim trailing slash from baseUrl', () => { + const c = new BridgeShieldClient({ baseUrl: 'https://test.com/' }); + expect(c).toBeInstanceOf(BridgeShieldClient); + }); + + it('should use default timeout of 5000ms', () => { + const c = new BridgeShieldClient({ baseUrl: 'https://test.com' }); + expect(c).toBeInstanceOf(BridgeShieldClient); + }); + }); + + describe('checkAddress', () => { + it('should call /api/v1/aml/check with correct params', async () => { + const mockResponse = { + checkId: 'chk_123', + address: '0x1234567890abcdef1234567890abcdef12345678', + chainId: 1, + riskScore: 85, + riskLevel: 'HIGH', + decision: 'BLOCK', + isWhitelisted: false, + cacheHit: false, + }; + + vi.mocked(fetch).mockResolvedValueOnce({ + ok: true, + status: 200, + json: () => Promise.resolve(mockResponse), + } as Response); + + const result = await client.checkAddress({ + address: '0x1234567890abcdef1234567890abcdef12345678', + chainId: 1, + }); + + expect(fetch).toHaveBeenCalledWith( + 'https://api.test.bridgeshield.io/api/v1/aml/check', + expect.objectContaining({ + method: 'POST', + body: JSON.stringify({ address: '0x1234567890abcdef1234567890abcdef12345678', chainId: 1 }), + }) + ); + expect(result.checkId).toBe('chk_123'); + expect(result.riskScore).toBe(85); + }); + + it('should throw ValidationError when address is empty', async () => { + await expect( + client.checkAddress({ address: '' }) + ).rejects.toThrow('Address is required'); + }); + + it('should throw ApiError on 4xx response', async () => { + vi.mocked(fetch).mockResolvedValueOnce({ + ok: false, + status: 400, + json: () => Promise.resolve({ message: 'Invalid address format' }), + } as Response); + + await expect( + client.checkAddress({ address: '0x123' }) + ).rejects.toThrow('Invalid address format'); + }); + + it('should throw ApiError on 5xx response', async () => { + vi.mocked(fetch).mockResolvedValueOnce({ + ok: false, + status: 500, + json: () => Promise.resolve({ message: 'Internal server error' }), + } as Response); + + await expect( + client.checkAddress({ address: '0x123' }) + ).rejects.toThrow('Internal server error'); + }); + + it('should throw NetworkError on network failure', async () => { + vi.mocked(fetch).mockRejectedValueOnce(new TypeError('Failed to fetch')); + + await expect( + client.checkAddress({ address: '0x123' }) + ).rejects.toThrow('Failed to fetch'); + }); + + it('should include optional params when provided', async () => { + const mockResponse = { + checkId: 'chk_123', + address: '0x123', + chainId: 1, + riskScore: 50, + riskLevel: 'MEDIUM', + decision: 'REVIEW', + isWhitelisted: false, + cacheHit: false, + }; + + vi.mocked(fetch).mockResolvedValueOnce({ + ok: true, + status: 200, + json: () => Promise.resolve(mockResponse), + } as Response); + + await client.checkAddress({ + address: '0x123', + chainId: 1, + amount: '1000', + senderAddress: '0xabc', + }); + + expect(fetch).toHaveBeenCalledWith( + expect.any(String), + expect.objectContaining({ + body: JSON.stringify({ + address: '0x123', + chainId: 1, + amount: '1000', + senderAddress: '0xabc', + }), + }) + ); + }); + }); + + describe('submitAppeal', () => { + it('should call /api/v1/aml/appeal with correct params', async () => { + const mockResponse = { + ticketId: 'APT-20260410-001', + address: '0x123', + chainId: 1, + status: 'PENDING', + estimatedReviewAt: '2026-04-17T00:00:00.000Z', + message: 'Appeal submitted', + nextSteps: ['Wait for review'], + }; + + vi.mocked(fetch).mockResolvedValueOnce({ + ok: true, + status: 201, + json: () => Promise.resolve(mockResponse), + } as Response); + + const result = await client.submitAppeal({ + address: '0x123', + reason: 'My address was flagged incorrectly', + contact: 'user@example.com', + }); + + expect(result.ticketId).toBe('APT-20260410-001'); + expect(result.status).toBe('PENDING'); + }); + + it('should throw ValidationError when address is empty', async () => { + await expect( + client.submitAppeal({ address: '', reason: 'Test reason' }) + ).rejects.toThrow('Address is required'); + }); + + it('should throw ValidationError when reason is empty', async () => { + await expect( + client.submitAppeal({ address: '0x123', reason: '' }) + ).rejects.toThrow('Reason is required'); + }); + + it('should throw ApiError on 409 conflict', async () => { + vi.mocked(fetch).mockResolvedValueOnce({ + ok: false, + status: 409, + json: () => Promise.resolve({ message: 'Appeal already exists' }), + } as Response); + + await expect( + client.submitAppeal({ address: '0x123', reason: 'Test' }) + ).rejects.toThrow('Appeal already exists'); + }); + }); + + describe('getAppealStatus', () => { + it('should call /api/v1/aml/appeal/status/:ticketId', async () => { + const mockResponse = { + ticketId: 'APT-20260410-001', + status: 'PENDING', + createdAt: '2026-04-10T00:00:00.000Z', + estimatedReviewAt: '2026-04-17T00:00:00.000Z', + }; + + vi.mocked(fetch).mockResolvedValueOnce({ + ok: true, + status: 200, + json: () => Promise.resolve(mockResponse), + } as Response); + + const result = await client.getAppealStatus('APT-20260410-001'); + + expect(fetch).toHaveBeenCalledWith( + 'https://api.test.bridgeshield.io/api/v1/aml/appeal/status/APT-20260410-001', + expect.any(Object) + ); + expect(result.ticketId).toBe('APT-20260410-001'); + }); + + it('should throw ValidationError when ticketId is empty', async () => { + await expect( + client.getAppealStatus('') + ).rejects.toThrow('Ticket ID is required'); + }); + + it('should throw ApiError on 404', async () => { + vi.mocked(fetch).mockResolvedValueOnce({ + ok: false, + status: 404, + json: () => Promise.resolve({ message: 'Appeal not found' }), + } as Response); + + await expect( + client.getAppealStatus('APT-INVALID') + ).rejects.toThrow('Appeal not found'); + }); + }); + + describe('getWhitelistSummary', () => { + it('should call /api/v1/aml/whitelist', async () => { + const mockResponse = { + total: 25, + categories: [{ category: 'KNOWN_PROTOCOL', count: 15 }], + lastSyncedAt: '2026-04-10T00:00:00.000Z', + version: '0.0.0', + }; + + vi.mocked(fetch).mockResolvedValueOnce({ + ok: true, + status: 200, + json: () => Promise.resolve(mockResponse), + } as Response); + + const result = await client.getWhitelistSummary(); + + expect(fetch).toHaveBeenCalledWith( + 'https://api.test.bridgeshield.io/api/v1/aml/whitelist', + expect.any(Object) + ); + expect(result.total).toBe(25); + }); + }); + + describe('healthCheck', () => { + it('should call /api/v1/health', async () => { + const mockResponse = { + status: 'healthy', + timestamp: '2026-04-10T00:00:00.000Z', + version: '0.0.0', + uptime: '3600s', + services: { + database: { healthy: true, status: 'connected' }, + riskData: { healthy: true, status: 'loaded' }, + cache: { healthy: true, status: 'ready' }, + redis: 'disabled', + }, + }; + + vi.mocked(fetch).mockResolvedValueOnce({ + ok: true, + status: 200, + json: () => Promise.resolve(mockResponse), + } as Response); + + const result = await client.healthCheck(); + + expect(result.status).toBe('healthy'); + expect(result.version).toBe('0.0.0'); + }); + + it('should return degraded status when services are down', async () => { + const mockResponse = { + status: 'degraded', + timestamp: '2026-04-10T00:00:00.000Z', + version: '0.0.0', + uptime: '3600s', + services: { + database: { healthy: false, status: 'disconnected' }, + riskData: { healthy: true, status: 'loaded' }, + cache: { healthy: true, status: 'ready' }, + redis: 'disabled', + }, + }; + + vi.mocked(fetch).mockResolvedValueOnce({ + ok: true, + status: 200, + json: () => Promise.resolve(mockResponse), + } as Response); + + const result = await client.healthCheck(); + + expect(result.status).toBe('degraded'); + }); + }); + + describe('setBaseUrl', () => { + it('should update baseUrl for subsequent calls', async () => { + const mockResponse = { status: 'healthy' }; + + vi.mocked(fetch).mockResolvedValueOnce({ + ok: true, + status: 200, + json: () => Promise.resolve(mockResponse), + } as Response); + + client.setBaseUrl('https://new-api.test.com'); + await client.healthCheck(); + + expect(fetch).toHaveBeenCalledWith( + 'https://new-api.test.com/api/v1/health', + expect.any(Object) + ); + }); + }); + + describe('setApiKey', () => { + it('should include Authorization header when apiKey is set', async () => { + const mockResponse = { status: 'healthy' }; + + vi.mocked(fetch).mockResolvedValueOnce({ + ok: true, + status: 200, + json: () => Promise.resolve(mockResponse), + } as Response); + + client.setApiKey('test-api-key-123'); + await client.healthCheck(); + + expect(fetch).toHaveBeenCalledWith( + expect.any(String), + expect.objectContaining({ + headers: expect.objectContaining({ + Authorization: 'Bearer test-api-key-123', + }), + }) + ); + }); + }); +}); diff --git a/packages/sdk/package-lock.json b/packages/sdk/package-lock.json new file mode 100644 index 0000000..4969213 --- /dev/null +++ b/packages/sdk/package-lock.json @@ -0,0 +1,2791 @@ +{ + "name": "@bridgeshield/sdk", + "version": "0.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "@bridgeshield/sdk", + "version": "0.0.0", + "license": "MIT", + "devDependencies": { + "tsup": "^8.0.0", + "typescript": "^5.3.0", + "vitest": "^1.0.0" + }, + "peerDependencies": { + "typescript": ">=5.0.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.7.tgz", + "integrity": "sha512-EKX3Qwmhz1eMdEJokhALr0YiD0lhQNwDqkPYyPhiSwKrh7/4KRjQc04sZ8db+5DVVnZ1LmbNDI1uAMPEUBnQPg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.7.tgz", + "integrity": "sha512-jbPXvB4Yj2yBV7HUfE2KHe4GJX51QplCN1pGbYjvsyCZbQmies29EoJbkEc+vYuU5o45AfQn37vZlyXy4YJ8RQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.7.tgz", + "integrity": "sha512-62dPZHpIXzvChfvfLJow3q5dDtiNMkwiRzPylSCfriLvZeq0a1bWChrGx/BbUbPwOrsWKMn8idSllklzBy+dgQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.7.tgz", + "integrity": "sha512-x5VpMODneVDb70PYV2VQOmIUUiBtY3D3mPBG8NxVk5CogneYhkR7MmM3yR/uMdITLrC1ml/NV1rj4bMJuy9MCg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.7.tgz", + "integrity": "sha512-5lckdqeuBPlKUwvoCXIgI2D9/ABmPq3Rdp7IfL70393YgaASt7tbju3Ac+ePVi3KDH6N2RqePfHnXkaDtY9fkw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.7.tgz", + "integrity": "sha512-rYnXrKcXuT7Z+WL5K980jVFdvVKhCHhUwid+dDYQpH+qu+TefcomiMAJpIiC2EM3Rjtq0sO3StMV/+3w3MyyqQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.7.tgz", + "integrity": "sha512-B48PqeCsEgOtzME2GbNM2roU29AMTuOIN91dsMO30t+Ydis3z/3Ngoj5hhnsOSSwNzS+6JppqWsuhTp6E82l2w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.7.tgz", + "integrity": "sha512-jOBDK5XEjA4m5IJK3bpAQF9/Lelu/Z9ZcdhTRLf4cajlB+8VEhFFRjWgfy3M1O4rO2GQ/b2dLwCUGpiF/eATNQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.7.tgz", + "integrity": "sha512-RkT/YXYBTSULo3+af8Ib0ykH8u2MBh57o7q/DAs3lTJlyVQkgQvlrPTnjIzzRPQyavxtPtfg0EopvDyIt0j1rA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.7.tgz", + "integrity": "sha512-RZPHBoxXuNnPQO9rvjh5jdkRmVizktkT7TCDkDmQ0W2SwHInKCAV95GRuvdSvA7w4VMwfCjUiPwDi0ZO6Nfe9A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.7.tgz", + "integrity": "sha512-GA48aKNkyQDbd3KtkplYWT102C5sn/EZTY4XROkxONgruHPU72l+gW+FfF8tf2cFjeHaRbWpOYa/uRBz/Xq1Pg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.7.tgz", + "integrity": "sha512-a4POruNM2oWsD4WKvBSEKGIiWQF8fZOAsycHOt6JBpZ+JN2n2JH9WAv56SOyu9X5IqAjqSIPTaJkqN8F7XOQ5Q==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.7.tgz", + "integrity": "sha512-KabT5I6StirGfIz0FMgl1I+R1H73Gp0ofL9A3nG3i/cYFJzKHhouBV5VWK1CSgKvVaG4q1RNpCTR2LuTVB3fIw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.7.tgz", + "integrity": "sha512-gRsL4x6wsGHGRqhtI+ifpN/vpOFTQtnbsupUF5R5YTAg+y/lKelYR1hXbnBdzDjGbMYjVJLJTd2OFmMewAgwlQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.7.tgz", + "integrity": "sha512-hL25LbxO1QOngGzu2U5xeXtxXcW+/GvMN3ejANqXkxZ/opySAZMrc+9LY/WyjAan41unrR3YrmtTsUpwT66InQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.7.tgz", + "integrity": "sha512-2k8go8Ycu1Kb46vEelhu1vqEP+UeRVj2zY1pSuPdgvbd5ykAw82Lrro28vXUrRmzEsUV0NzCf54yARIK8r0fdw==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.7.tgz", + "integrity": "sha512-hzznmADPt+OmsYzw1EE33ccA+HPdIqiCRq7cQeL1Jlq2gb1+OyWBkMCrYGBJ+sxVzve2ZJEVeePbLM2iEIZSxA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.7.tgz", + "integrity": "sha512-b6pqtrQdigZBwZxAn1UpazEisvwaIDvdbMbmrly7cDTMFnw/+3lVxxCTGOrkPVnsYIosJJXAsILG9XcQS+Yu6w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.7.tgz", + "integrity": "sha512-OfatkLojr6U+WN5EDYuoQhtM+1xco+/6FSzJJnuWiUw5eVcicbyK3dq5EeV/QHT1uy6GoDhGbFpprUiHUYggrw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.7.tgz", + "integrity": "sha512-AFuojMQTxAz75Fo8idVcqoQWEHIXFRbOc1TrVcFSgCZtQfSdc1RXgB3tjOn/krRHENUB4j00bfGjyl2mJrU37A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.7.tgz", + "integrity": "sha512-+A1NJmfM8WNDv5CLVQYJ5PshuRm/4cI6WMZRg1by1GwPIQPCTs1GLEUHwiiQGT5zDdyLiRM/l1G0Pv54gvtKIg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.7.tgz", + "integrity": "sha512-+KrvYb/C8zA9CU/g0sR6w2RBw7IGc5J2BPnc3dYc5VJxHCSF1yNMxTV5LQ7GuKteQXZtspjFbiuW5/dOj7H4Yw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.7.tgz", + "integrity": "sha512-ikktIhFBzQNt/QDyOL580ti9+5mL/YZeUPKU2ivGtGjdTYoqz6jObj6nOMfhASpS4GU4Q/Clh1QtxWAvcYKamA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.7.tgz", + "integrity": "sha512-7yRhbHvPqSpRUV7Q20VuDwbjW5kIMwTHpptuUzV+AA46kiPze5Z7qgt6CLCK3pWFrHeNfDd1VKgyP4O+ng17CA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.7.tgz", + "integrity": "sha512-SmwKXe6VHIyZYbBLJrhOoCJRB/Z1tckzmgTLfFYOfpMAx63BJEaL9ExI8x7v0oAO3Zh6D/Oi1gVxEYr5oUCFhw==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.7.tgz", + "integrity": "sha512-56hiAJPhwQ1R4i+21FVF7V8kSD5zZTdHcVuRFMW0hn753vVfQN8xlx4uOPT4xoGH0Z/oVATuR82AiqSTDIpaHg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@jest/schemas": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz", + "integrity": "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@sinclair/typebox": "^0.27.8" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.60.1.tgz", + "integrity": "sha512-d6FinEBLdIiK+1uACUttJKfgZREXrF0Qc2SmLII7W2AD8FfiZ9Wjd+rD/iRuf5s5dWrr1GgwXCvPqOuDquOowA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.60.1.tgz", + "integrity": "sha512-YjG/EwIDvvYI1YvYbHvDz/BYHtkY4ygUIXHnTdLhG+hKIQFBiosfWiACWortsKPKU/+dUwQQCKQM3qrDe8c9BA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.60.1.tgz", + "integrity": "sha512-mjCpF7GmkRtSJwon+Rq1N8+pI+8l7w5g9Z3vWj4T7abguC4Czwi3Yu/pFaLvA3TTeMVjnu3ctigusqWUfjZzvw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.60.1.tgz", + "integrity": "sha512-haZ7hJ1JT4e9hqkoT9R/19XW2QKqjfJVv+i5AGg57S+nLk9lQnJ1F/eZloRO3o9Scy9CM3wQ9l+dkXtcBgN5Ew==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.60.1.tgz", + "integrity": "sha512-czw90wpQq3ZsAVBlinZjAYTKduOjTywlG7fEeWKUA7oCmpA8xdTkxZZlwNJKWqILlq0wehoZcJYfBvOyhPTQ6w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.60.1.tgz", + "integrity": "sha512-KVB2rqsxTHuBtfOeySEyzEOB7ltlB/ux38iu2rBQzkjbwRVlkhAGIEDiiYnO2kFOkJp+Z7pUXKyrRRFuFUKt+g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.60.1.tgz", + "integrity": "sha512-L+34Qqil+v5uC0zEubW7uByo78WOCIrBvci69E7sFASRl0X7b/MB6Cqd1lky/CtcSVTydWa2WZwFuWexjS5o6g==", + "cpu": [ + "arm" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.60.1.tgz", + "integrity": "sha512-n83O8rt4v34hgFzlkb1ycniJh7IR5RCIqt6mz1VRJD6pmhRi0CXdmfnLu9dIUS6buzh60IvACM842Ffb3xd6Gg==", + "cpu": [ + "arm" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.60.1.tgz", + "integrity": "sha512-Nql7sTeAzhTAja3QXeAI48+/+GjBJ+QmAH13snn0AJSNL50JsDqotyudHyMbO2RbJkskbMbFJfIJKWA6R1LCJQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.60.1.tgz", + "integrity": "sha512-+pUymDhd0ys9GcKZPPWlFiZ67sTWV5UU6zOJat02M1+PiuSGDziyRuI/pPue3hoUwm2uGfxdL+trT6Z9rxnlMA==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.60.1.tgz", + "integrity": "sha512-VSvgvQeIcsEvY4bKDHEDWcpW4Yw7BtlKG1GUT4FzBUlEKQK0rWHYBqQt6Fm2taXS+1bXvJT6kICu5ZwqKCnvlQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.60.1.tgz", + "integrity": "sha512-4LqhUomJqwe641gsPp6xLfhqWMbQV04KtPp7/dIp0nzPxAkNY1AbwL5W0MQpcalLYk07vaW9Kp1PBhdpZYYcEw==", + "cpu": [ + "loong64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.60.1.tgz", + "integrity": "sha512-tLQQ9aPvkBxOc/EUT6j3pyeMD6Hb8QF2BTBnCQWP/uu1lhc9AIrIjKnLYMEroIz/JvtGYgI9dF3AxHZNaEH0rw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.60.1.tgz", + "integrity": "sha512-RMxFhJwc9fSXP6PqmAz4cbv3kAyvD1etJFjTx4ONqFP9DkTkXsAMU4v3Vyc5BgzC+anz7nS/9tp4obsKfqkDHg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.60.1.tgz", + "integrity": "sha512-QKgFl+Yc1eEk6MmOBfRHYF6lTxiiiV3/z/BRrbSiW2I7AFTXoBFvdMEyglohPj//2mZS4hDOqeB0H1ACh3sBbg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.60.1.tgz", + "integrity": "sha512-RAjXjP/8c6ZtzatZcA1RaQr6O1TRhzC+adn8YZDnChliZHviqIjmvFwHcxi4JKPSDAt6Uhf/7vqcBzQJy0PDJg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.60.1.tgz", + "integrity": "sha512-wcuocpaOlaL1COBYiA89O6yfjlp3RwKDeTIA0hM7OpmhR1Bjo9j31G1uQVpDlTvwxGn2nQs65fBFL5UFd76FcQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.60.1.tgz", + "integrity": "sha512-77PpsFQUCOiZR9+LQEFg9GClyfkNXj1MP6wRnzYs0EeWbPcHs02AXu4xuUbM1zhwn3wqaizle3AEYg5aeoohhg==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.60.1.tgz", + "integrity": "sha512-5cIATbk5vynAjqqmyBjlciMJl1+R/CwX9oLk/EyiFXDWd95KpHdrOJT//rnUl4cUcskrd0jCCw3wpZnhIHdD9w==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.60.1.tgz", + "integrity": "sha512-cl0w09WsCi17mcmWqqglez9Gk8isgeWvoUZ3WiJFYSR3zjBQc2J5/ihSjpl+VLjPqjQ/1hJRcqBfLjssREQILw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.60.1.tgz", + "integrity": "sha512-4Cv23ZrONRbNtbZa37mLSueXUCtN7MXccChtKpUnQNgF010rjrjfHx3QxkS2PI7LqGT5xXyYs1a7LbzAwT0iCA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.60.1.tgz", + "integrity": "sha512-i1okWYkA4FJICtr7KpYzFpRTHgy5jdDbZiWfvny21iIKky5YExiDXP+zbXzm3dUcFpkEeYNHgQ5fuG236JPq0g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.60.1.tgz", + "integrity": "sha512-u09m3CuwLzShA0EYKMNiFgcjjzwqtUMLmuCJLeZWjjOYA3IT2Di09KaxGBTP9xVztWyIWjVdsB2E9goMjZvTQg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.60.1.tgz", + "integrity": "sha512-k+600V9Zl1CM7eZxJgMyTUzmrmhB/0XZnF4pRypKAlAgxmedUA+1v9R+XOFv56W4SlHEzfeMtzujLJD22Uz5zg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.60.1.tgz", + "integrity": "sha512-lWMnixq/QzxyhTV6NjQJ4SFo1J6PvOX8vUx5Wb4bBPsEb+8xZ89Bz6kOXpfXj9ak9AHTQVQzlgzBEc1SyM27xQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@sinclair/typebox": { + "version": "0.27.10", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.10.tgz", + "integrity": "sha512-MTBk/3jGLNB2tVxv6uLlFh1iu64iYOQ2PbdOSK3NW8JZsmlaOh2q6sdtKowBhfw8QFLmYNzTW4/oK4uATIi6ZA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@vitest/expect": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-1.6.1.tgz", + "integrity": "sha512-jXL+9+ZNIJKruofqXuuTClf44eSpcHlgj3CiuNihUF3Ioujtmc0zIa3UJOW5RjDK1YLBJZnWBlPuqhYycLioog==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "1.6.1", + "@vitest/utils": "1.6.1", + "chai": "^4.3.10" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-1.6.1.tgz", + "integrity": "sha512-3nSnYXkVkf3mXFfE7vVyPmi3Sazhb/2cfZGGs0JRzFsPFvAMBEcrweV1V1GsrstdXeKCTXlJbvnQwGWgEIHmOA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "1.6.1", + "p-limit": "^5.0.0", + "pathe": "^1.1.1" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner/node_modules/pathe": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-1.1.2.tgz", + "integrity": "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@vitest/snapshot": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-1.6.1.tgz", + "integrity": "sha512-WvidQuWAzU2p95u8GAKlRMqMyN1yOJkGHnx3M1PL9Raf7AQ1kwLKg04ADlCa3+OXUZE7BceOhVZiuWAbzCKcUQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "magic-string": "^0.30.5", + "pathe": "^1.1.1", + "pretty-format": "^29.7.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot/node_modules/pathe": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-1.1.2.tgz", + "integrity": "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@vitest/spy": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-1.6.1.tgz", + "integrity": "sha512-MGcMmpGkZebsMZhbQKkAf9CX5zGvjkBTqf8Zx3ApYWXr3wG+QvEu2eXWfnIIWYSJExIp4V9FCKDEeygzkYrXMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyspy": "^2.2.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-1.6.1.tgz", + "integrity": "sha512-jOrrUvXM4Av9ZWiG1EajNto0u96kWAhJ1LmPmJhXXQx/32MecEKd10pOLYgS2BQx1TgkGhloPU1ArDW2vvaY6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "diff-sequences": "^29.6.3", + "estree-walker": "^3.0.3", + "loupe": "^2.3.7", + "pretty-format": "^29.7.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/acorn": { + "version": "8.16.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", + "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-walk": { + "version": "8.3.5", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.5.tgz", + "integrity": "sha512-HEHNfbars9v4pgpW6SO1KSPkfoS0xVOM/9UzkJltjlsHZmJasxg8aXkuZa7SMf8vKGIBhpUsPluQSqhJFCqebw==", + "dev": true, + "license": "MIT", + "dependencies": { + "acorn": "^8.11.0" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/any-promise": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", + "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==", + "dev": true, + "license": "MIT" + }, + "node_modules/assertion-error": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-1.1.0.tgz", + "integrity": "sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/bundle-require": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/bundle-require/-/bundle-require-5.1.0.tgz", + "integrity": "sha512-3WrrOuZiyaaZPWiEt4G3+IffISVC9HYlWueJEBWED4ZH4aIAC2PnkdnuRrR94M+w6yGWn4AglWtJtBI8YqvgoA==", + "dev": true, + "license": "MIT", + "dependencies": { + "load-tsconfig": "^0.2.3" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "peerDependencies": { + "esbuild": ">=0.18" + } + }, + "node_modules/cac": { + "version": "6.7.14", + "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", + "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/chai": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/chai/-/chai-4.5.0.tgz", + "integrity": "sha512-RITGBfijLkBddZvnn8jdqoTypxvqbOLYQkGGxXzeFjVHvudaPw0HNFD9x928/eUwYWd2dPCugVqspGALTZZQKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "assertion-error": "^1.1.0", + "check-error": "^1.0.3", + "deep-eql": "^4.1.3", + "get-func-name": "^2.0.2", + "loupe": "^2.3.6", + "pathval": "^1.1.1", + "type-detect": "^4.1.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/check-error": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.3.tgz", + "integrity": "sha512-iKEoDYaRmd1mxM90a2OEfWhjsjPpYPuQ+lMYsoxB126+t8fw7ySEO48nmDg5COTjxDI65/Y2OWpeEHk3ZOe8zg==", + "dev": true, + "license": "MIT", + "dependencies": { + "get-func-name": "^2.0.2" + }, + "engines": { + "node": "*" + } + }, + "node_modules/chokidar": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", + "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "readdirp": "^4.0.1" + }, + "engines": { + "node": ">= 14.16.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/commander": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", + "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/confbox": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/confbox/-/confbox-0.1.8.tgz", + "integrity": "sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/consola": { + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/consola/-/consola-3.4.2.tgz", + "integrity": "sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.18.0 || >=16.10.0" + } + }, + "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": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/deep-eql": { + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-4.1.4.tgz", + "integrity": "sha512-SUwdGfqdKOwxCPeVYjwSyRpJ7Z+fhpwIAtmCUdZIWZ/YP5R9WAsyuSgpLVDi9bjWoN2LXHNss/dk3urXtdQxGg==", + "dev": true, + "license": "MIT", + "dependencies": { + "type-detect": "^4.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/diff-sequences": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.6.3.tgz", + "integrity": "sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/esbuild": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.7.tgz", + "integrity": "sha512-IxpibTjyVnmrIQo5aqNpCgoACA/dTKLTlhMHihVHhdkxKyPO1uBBthumT0rdHmcsk9uMonIWS0m4FljWzILh3w==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.27.7", + "@esbuild/android-arm": "0.27.7", + "@esbuild/android-arm64": "0.27.7", + "@esbuild/android-x64": "0.27.7", + "@esbuild/darwin-arm64": "0.27.7", + "@esbuild/darwin-x64": "0.27.7", + "@esbuild/freebsd-arm64": "0.27.7", + "@esbuild/freebsd-x64": "0.27.7", + "@esbuild/linux-arm": "0.27.7", + "@esbuild/linux-arm64": "0.27.7", + "@esbuild/linux-ia32": "0.27.7", + "@esbuild/linux-loong64": "0.27.7", + "@esbuild/linux-mips64el": "0.27.7", + "@esbuild/linux-ppc64": "0.27.7", + "@esbuild/linux-riscv64": "0.27.7", + "@esbuild/linux-s390x": "0.27.7", + "@esbuild/linux-x64": "0.27.7", + "@esbuild/netbsd-arm64": "0.27.7", + "@esbuild/netbsd-x64": "0.27.7", + "@esbuild/openbsd-arm64": "0.27.7", + "@esbuild/openbsd-x64": "0.27.7", + "@esbuild/openharmony-arm64": "0.27.7", + "@esbuild/sunos-x64": "0.27.7", + "@esbuild/win32-arm64": "0.27.7", + "@esbuild/win32-ia32": "0.27.7", + "@esbuild/win32-x64": "0.27.7" + } + }, + "node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, + "node_modules/execa": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-8.0.1.tgz", + "integrity": "sha512-VyhnebXciFV2DESc+p6B+y0LjSm0krU4OgJN44qFAhBY0TJ+1V61tYD2+wHusZ6F9n5K+vl8k0sTy7PEfV4qpg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cross-spawn": "^7.0.3", + "get-stream": "^8.0.1", + "human-signals": "^5.0.0", + "is-stream": "^3.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^5.1.0", + "onetime": "^6.0.0", + "signal-exit": "^4.1.0", + "strip-final-newline": "^3.0.0" + }, + "engines": { + "node": ">=16.17" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/fix-dts-default-cjs-exports": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/fix-dts-default-cjs-exports/-/fix-dts-default-cjs-exports-1.0.1.tgz", + "integrity": "sha512-pVIECanWFC61Hzl2+oOCtoJ3F17kglZC/6N94eRWycFgBH35hHx0Li604ZIzhseh97mf2p0cv7vVrOZGoqhlEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "magic-string": "^0.30.17", + "mlly": "^1.7.4", + "rollup": "^4.34.8" + } + }, + "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/get-func-name": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.2.tgz", + "integrity": "sha512-8vXOvuE167CtIc3OyItco7N/dpRtBbYOsPsXCz7X/PMnlGjYjSGuZJgM1Y7mmew7BKf9BqvLX2tnOVy1BBUsxQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/get-stream": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-8.0.1.tgz", + "integrity": "sha512-VaUJspBffn/LMCJVoMvSAdmscJyS1auj5Zulnn5UoYcY531UWmdwhRWkcGKnGU93m5HSXP9LP2usOryrBtQowA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/human-signals": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-5.0.0.tgz", + "integrity": "sha512-AXcZb6vzzrFAUE61HnN4mpLqd/cSIwNQjtNWR0euPm6y0iqx3G4gOXaIDdtdDwZmhwe82LA6+zinmW4UBWVePQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=16.17.0" + } + }, + "node_modules/is-stream": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-3.0.0.tgz", + "integrity": "sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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/joycon": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/joycon/-/joycon-3.1.1.tgz", + "integrity": "sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/js-tokens": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-9.0.1.tgz", + "integrity": "sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/lilconfig": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz", + "integrity": "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antonk52" + } + }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "dev": true, + "license": "MIT" + }, + "node_modules/load-tsconfig": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/load-tsconfig/-/load-tsconfig-0.2.5.tgz", + "integrity": "sha512-IXO6OCs9yg8tMKzfPZ1YmheJbZCiEsnBdcB03l0OcfK9prKnJb96siuHCr5Fl37/yo9DnKU+TLpxzTUspw9shg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + } + }, + "node_modules/local-pkg": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/local-pkg/-/local-pkg-0.5.1.tgz", + "integrity": "sha512-9rrA30MRRP3gBD3HTGnC6cDFpaE1kVDWxWgqWJUN0RvDNAo+Nz/9GxB+nHOH0ifbVFy0hSA1V6vFDvnx54lTEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "mlly": "^1.7.3", + "pkg-types": "^1.2.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/loupe": { + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/loupe/-/loupe-2.3.7.tgz", + "integrity": "sha512-zSMINGVYkdpYSOBmLi0D1Uo7JU9nVdQKrHxC8eYlV+9YKK9WePqAlL7lSlorG/U2Fw1w0hTBmaa/jrQ3UbPHtA==", + "dev": true, + "license": "MIT", + "dependencies": { + "get-func-name": "^2.0.1" + } + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", + "dev": true, + "license": "MIT" + }, + "node_modules/mimic-fn": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-4.0.0.tgz", + "integrity": "sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/mlly": { + "version": "1.8.2", + "resolved": "https://registry.npmjs.org/mlly/-/mlly-1.8.2.tgz", + "integrity": "sha512-d+ObxMQFmbt10sretNDytwt85VrbkhhUA/JBGm1MPaWJ65Cl4wOgLaB1NYvJSZ0Ef03MMEU/0xpPMXUIQ29UfA==", + "dev": true, + "license": "MIT", + "dependencies": { + "acorn": "^8.16.0", + "pathe": "^2.0.3", + "pkg-types": "^1.3.1", + "ufo": "^1.6.3" + } + }, + "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/mz": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", + "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "any-promise": "^1.0.0", + "object-assign": "^4.0.1", + "thenify-all": "^1.0.0" + } + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/npm-run-path": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-5.3.0.tgz", + "integrity": "sha512-ppwTtiJZq0O/ai0z7yfudtBpWIoxM8yE6nHi1X47eFR2EWORqfbu6CnPlNsjeN683eT0qG6H/Pyf9fCcvjnnnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^4.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/npm-run-path/node_modules/path-key": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-4.0.0.tgz", + "integrity": "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/onetime": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-6.0.0.tgz", + "integrity": "sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "mimic-fn": "^4.0.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-limit": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-5.0.0.tgz", + "integrity": "sha512-/Eaoq+QyLSiXQ4lyYV23f14mZRQcXnxfHrN0vCai+ak9G0pp9iEQukIIZq5NccEvwRB8PUnZT0KsOoDCINS1qQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^1.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "dev": true, + "license": "MIT" + }, + "node_modules/pathval": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-1.1.1.tgz", + "integrity": "sha512-Dp6zGqpTdETdR63lehJYPeIOqpiNBNtc7BpWSLrOje7UaIsE5aY92r/AunQA7rsXvet3lrJ3JnZX29UPTKXyKQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pirates": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz", + "integrity": "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/pkg-types": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-1.3.1.tgz", + "integrity": "sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "confbox": "^0.1.8", + "mlly": "^1.7.4", + "pathe": "^2.0.1" + } + }, + "node_modules/postcss": { + "version": "8.5.9", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.9.tgz", + "integrity": "sha512-7a70Nsot+EMX9fFU3064K/kdHWZqGVY+BADLyXc8Dfv+mTLLVl6JzJpPaCZ2kQL9gIJvKXSLMHhqdRRjwQeFtw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/postcss-load-config": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-6.0.1.tgz", + "integrity": "sha512-oPtTM4oerL+UXmx+93ytZVN82RrlY/wPUV8IeDxFrzIjXOLF1pN+EmKPLbubvKHT2HC20xXsCAH2Z+CKV6Oz/g==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "lilconfig": "^3.1.1" + }, + "engines": { + "node": ">= 18" + }, + "peerDependencies": { + "jiti": ">=1.21.0", + "postcss": ">=8.0.9", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + }, + "postcss": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "dev": true, + "license": "MIT" + }, + "node_modules/readdirp": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", + "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14.18.0" + }, + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/rollup": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.60.1.tgz", + "integrity": "sha512-VmtB2rFU/GroZ4oL8+ZqXgSA38O6GR8KSIvWmEFv63pQ0G6KaBH9s07PO8XTXP4vI+3UJUEypOfjkGfmSBBR0w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.60.1", + "@rollup/rollup-android-arm64": "4.60.1", + "@rollup/rollup-darwin-arm64": "4.60.1", + "@rollup/rollup-darwin-x64": "4.60.1", + "@rollup/rollup-freebsd-arm64": "4.60.1", + "@rollup/rollup-freebsd-x64": "4.60.1", + "@rollup/rollup-linux-arm-gnueabihf": "4.60.1", + "@rollup/rollup-linux-arm-musleabihf": "4.60.1", + "@rollup/rollup-linux-arm64-gnu": "4.60.1", + "@rollup/rollup-linux-arm64-musl": "4.60.1", + "@rollup/rollup-linux-loong64-gnu": "4.60.1", + "@rollup/rollup-linux-loong64-musl": "4.60.1", + "@rollup/rollup-linux-ppc64-gnu": "4.60.1", + "@rollup/rollup-linux-ppc64-musl": "4.60.1", + "@rollup/rollup-linux-riscv64-gnu": "4.60.1", + "@rollup/rollup-linux-riscv64-musl": "4.60.1", + "@rollup/rollup-linux-s390x-gnu": "4.60.1", + "@rollup/rollup-linux-x64-gnu": "4.60.1", + "@rollup/rollup-linux-x64-musl": "4.60.1", + "@rollup/rollup-openbsd-x64": "4.60.1", + "@rollup/rollup-openharmony-arm64": "4.60.1", + "@rollup/rollup-win32-arm64-msvc": "4.60.1", + "@rollup/rollup-win32-ia32-msvc": "4.60.1", + "@rollup/rollup-win32-x64-gnu": "4.60.1", + "@rollup/rollup-win32-x64-msvc": "4.60.1", + "fsevents": "~2.3.2" + } + }, + "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/siginfo": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", + "dev": true, + "license": "ISC" + }, + "node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/source-map": { + "version": "0.7.6", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.6.tgz", + "integrity": "sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">= 12" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/stackback": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", + "dev": true, + "license": "MIT" + }, + "node_modules/std-env": { + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz", + "integrity": "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==", + "dev": true, + "license": "MIT" + }, + "node_modules/strip-final-newline": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-3.0.0.tgz", + "integrity": "sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/strip-literal": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/strip-literal/-/strip-literal-2.1.1.tgz", + "integrity": "sha512-631UJ6O00eNGfMiWG78ck80dfBab8X6IVFB51jZK5Icd7XAs60Z5y7QdSd/wGIklnWvRbUNloVzhOKKmutxQ6Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "js-tokens": "^9.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/sucrase": { + "version": "3.35.1", + "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.1.tgz", + "integrity": "sha512-DhuTmvZWux4H1UOnWMB3sk0sbaCVOoQZjv8u1rDoTV0HTdGem9hkAZtl4JZy8P2z4Bg0nT+YMeOFyVr4zcG5Tw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.2", + "commander": "^4.0.0", + "lines-and-columns": "^1.1.6", + "mz": "^2.7.0", + "pirates": "^4.0.1", + "tinyglobby": "^0.2.11", + "ts-interface-checker": "^0.1.9" + }, + "bin": { + "sucrase": "bin/sucrase", + "sucrase-node": "bin/sucrase-node" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/thenify": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz", + "integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "any-promise": "^1.0.0" + } + }, + "node_modules/thenify-all": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz", + "integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "thenify": ">= 3.1.0 < 4" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/tinybench": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", + "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyexec": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.2.tgz", + "integrity": "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyglobby": { + "version": "0.2.16", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.16.tgz", + "integrity": "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tinypool": { + "version": "0.8.4", + "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-0.8.4.tgz", + "integrity": "sha512-i11VH5gS6IFeLY3gMBQ00/MmLncVP7JLXOw1vlgkytLmJK7QnEr7NXf0LBdxfmNPAeyetukOk0bOYrJrFGjYJQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tinyspy": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-2.2.1.tgz", + "integrity": "sha512-KYad6Vy5VDWV4GH3fjpseMQ/XU2BhIYP7Vzd0LG44qRWm/Yt2WCOTicFdvmgo6gWaqooMQCawTtILVQJupKu7A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tree-kill": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz", + "integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==", + "dev": true, + "license": "MIT", + "bin": { + "tree-kill": "cli.js" + } + }, + "node_modules/ts-interface-checker": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz", + "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/tsup": { + "version": "8.5.1", + "resolved": "https://registry.npmjs.org/tsup/-/tsup-8.5.1.tgz", + "integrity": "sha512-xtgkqwdhpKWr3tKPmCkvYmS9xnQK3m3XgxZHwSUjvfTjp7YfXe5tT3GgWi0F2N+ZSMsOeWeZFh7ZZFg5iPhing==", + "dev": true, + "license": "MIT", + "dependencies": { + "bundle-require": "^5.1.0", + "cac": "^6.7.14", + "chokidar": "^4.0.3", + "consola": "^3.4.0", + "debug": "^4.4.0", + "esbuild": "^0.27.0", + "fix-dts-default-cjs-exports": "^1.0.0", + "joycon": "^3.1.1", + "picocolors": "^1.1.1", + "postcss-load-config": "^6.0.1", + "resolve-from": "^5.0.0", + "rollup": "^4.34.8", + "source-map": "^0.7.6", + "sucrase": "^3.35.0", + "tinyexec": "^0.3.2", + "tinyglobby": "^0.2.11", + "tree-kill": "^1.2.2" + }, + "bin": { + "tsup": "dist/cli-default.js", + "tsup-node": "dist/cli-node.js" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@microsoft/api-extractor": "^7.36.0", + "@swc/core": "^1", + "postcss": "^8.4.12", + "typescript": ">=4.5.0" + }, + "peerDependenciesMeta": { + "@microsoft/api-extractor": { + "optional": true + }, + "@swc/core": { + "optional": true + }, + "postcss": { + "optional": true + }, + "typescript": { + "optional": true + } + } + }, + "node_modules/type-detect": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.1.0.tgz", + "integrity": "sha512-Acylog8/luQ8L7il+geoSxhEkazvkslg7PSNKOX59mbB9cOveP5aq9h74Y7YU8yDpJwetzQQrfIwtf4Wp4LKcw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/ufo": { + "version": "1.6.3", + "resolved": "https://registry.npmjs.org/ufo/-/ufo-1.6.3.tgz", + "integrity": "sha512-yDJTmhydvl5lJzBmy/hyOAA0d+aqCBuwl818haVdYCRrWV84o7YyeVm4QlVHStqNrrJSTb6jKuFAVqAFsr+K3Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/vite": { + "version": "5.4.21", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz", + "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.21.3", + "postcss": "^8.4.43", + "rollup": "^4.20.0" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || >=20.0.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.4.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + } + } + }, + "node_modules/vite-node": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-1.6.1.tgz", + "integrity": "sha512-YAXkfvGtuTzwWbDSACdJSg4A4DZiAqckWe90Zapc/sEX3XvHcw1NdurM/6od8J207tSDqNbSsgdCacBgvJKFuA==", + "dev": true, + "license": "MIT", + "dependencies": { + "cac": "^6.7.14", + "debug": "^4.3.4", + "pathe": "^1.1.1", + "picocolors": "^1.0.0", + "vite": "^5.0.0" + }, + "bin": { + "vite-node": "vite-node.mjs" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/vite-node/node_modules/pathe": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-1.1.2.tgz", + "integrity": "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/vite/node_modules/@esbuild/aix-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", + "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/android-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", + "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/android-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", + "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/android-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", + "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/darwin-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", + "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/darwin-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", + "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/freebsd-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", + "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/freebsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", + "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", + "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", + "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", + "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-loong64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", + "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-mips64el": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", + "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", + "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-riscv64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", + "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-s390x": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", + "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", + "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/netbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", + "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/openbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", + "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/sunos-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", + "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/win32-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", + "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/win32-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", + "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/win32-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", + "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/esbuild": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", + "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.21.5", + "@esbuild/android-arm": "0.21.5", + "@esbuild/android-arm64": "0.21.5", + "@esbuild/android-x64": "0.21.5", + "@esbuild/darwin-arm64": "0.21.5", + "@esbuild/darwin-x64": "0.21.5", + "@esbuild/freebsd-arm64": "0.21.5", + "@esbuild/freebsd-x64": "0.21.5", + "@esbuild/linux-arm": "0.21.5", + "@esbuild/linux-arm64": "0.21.5", + "@esbuild/linux-ia32": "0.21.5", + "@esbuild/linux-loong64": "0.21.5", + "@esbuild/linux-mips64el": "0.21.5", + "@esbuild/linux-ppc64": "0.21.5", + "@esbuild/linux-riscv64": "0.21.5", + "@esbuild/linux-s390x": "0.21.5", + "@esbuild/linux-x64": "0.21.5", + "@esbuild/netbsd-x64": "0.21.5", + "@esbuild/openbsd-x64": "0.21.5", + "@esbuild/sunos-x64": "0.21.5", + "@esbuild/win32-arm64": "0.21.5", + "@esbuild/win32-ia32": "0.21.5", + "@esbuild/win32-x64": "0.21.5" + } + }, + "node_modules/vitest": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-1.6.1.tgz", + "integrity": "sha512-Ljb1cnSJSivGN0LqXd/zmDbWEM0RNNg2t1QW/XUhYl/qPqyu7CsqeWtqQXHVaJsecLPuDoak2oJcZN2QoRIOag==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/expect": "1.6.1", + "@vitest/runner": "1.6.1", + "@vitest/snapshot": "1.6.1", + "@vitest/spy": "1.6.1", + "@vitest/utils": "1.6.1", + "acorn-walk": "^8.3.2", + "chai": "^4.3.10", + "debug": "^4.3.4", + "execa": "^8.0.1", + "local-pkg": "^0.5.0", + "magic-string": "^0.30.5", + "pathe": "^1.1.1", + "picocolors": "^1.0.0", + "std-env": "^3.5.0", + "strip-literal": "^2.0.0", + "tinybench": "^2.5.1", + "tinypool": "^0.8.3", + "vite": "^5.0.0", + "vite-node": "1.6.1", + "why-is-node-running": "^2.2.2" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@types/node": "^18.0.0 || >=20.0.0", + "@vitest/browser": "1.6.1", + "@vitest/ui": "1.6.1", + "happy-dom": "*", + "jsdom": "*" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + } + } + }, + "node_modules/vitest/node_modules/pathe": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-1.1.2.tgz", + "integrity": "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==", + "dev": true, + "license": "MIT" + }, + "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/why-is-node-running": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/yocto-queue": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-1.2.2.tgz", + "integrity": "sha512-4LCcse/U2MHZ63HAJVE+v71o7yOdIe4cZ70Wpf8D/IyjDKYQLV5GD46B+hSTjJsvV5PztjvHoU580EftxjDZFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + } + } +} diff --git a/packages/sdk/package.json b/packages/sdk/package.json new file mode 100644 index 0000000..ed4c8bd --- /dev/null +++ b/packages/sdk/package.json @@ -0,0 +1,48 @@ +{ + "name": "@bridgeshield/sdk", + "version": "0.0.0", + "description": "Official SDK for BridgeShield AML Gateway - Check addresses, submit appeals, and integrate LI.FI compliance", + "main": "./dist/index.cjs", + "module": "./dist/index.js", + "types": "./dist/index.d.ts", + "exports": { + ".": { + "types": "./dist/index.d.ts", + "import": "./dist/index.js", + "require": "./dist/index.cjs" + } + }, + "files": [ + "dist" + ], + "scripts": { + "build": "tsup", + "dev": "tsup --watch", + "test": "vitest run", + "test:watch": "vitest", + "lint": "eslint src/", + "typecheck": "tsc --noEmit", + "clean": "rm -rf dist" + }, + "keywords": [ + "aml", + "kyc", + "blockchain", + "defi", + "lifinance", + "li-fi", + "compliance", + "sanctions", + "cryptocurrency" + ], + "author": "BridgeShield", + "license": "MIT", + "peerDependencies": { + "typescript": ">=5.0.0" + }, + "devDependencies": { + "tsup": "^8.0.0", + "typescript": "^5.3.0", + "vitest": "^1.0.0" + } +} \ No newline at end of file diff --git a/packages/sdk/src/client.ts b/packages/sdk/src/client.ts new file mode 100644 index 0000000..11872d6 --- /dev/null +++ b/packages/sdk/src/client.ts @@ -0,0 +1,138 @@ +import type { + BridgeShieldConfig, + CheckAddressParams, + CheckAddressResponse, + SubmitAppealParams, + SubmitAppealResponse, + AppealStatusResponse, + WhitelistSummary, + HealthCheckResponse, +} from './types'; +import { ApiError, NetworkError, ValidationError } from './types'; + +const DEFAULT_TIMEOUT = 5000; + +export class BridgeShieldClient { + private baseUrl: string; + private apiKey?: string; + private timeout: number; + + constructor(config: BridgeShieldConfig) { + this.baseUrl = config.baseUrl.replace(/\/$/, ''); + this.apiKey = config.apiKey; + this.timeout = config.timeout ?? DEFAULT_TIMEOUT; + } + + private async request( + endpoint: string, + options: RequestInit = {} + ): Promise { + const controller = new AbortController(); + const timeoutId = setTimeout(() => controller.abort(), this.timeout); + + const headers: Record = { + 'Content-Type': 'application/json', + ...(this.apiKey ? { Authorization: `Bearer ${this.apiKey}` } : {}), + ...((options.headers as Record) || {}), + }; + + try { + const response = await fetch(`${this.baseUrl}${endpoint}`, { + ...options, + headers, + signal: controller.signal, + }); + + clearTimeout(timeoutId); + + if (response.status >= 400) { + const errorData = await response.json().catch(() => ({})); + const error = new ApiError( + errorData.message || `Request failed: ${response.status}`, + response.status, + errorData + ); + throw error; + } + + return response.json(); + } catch (err) { + clearTimeout(timeoutId); + + if (err instanceof Error && err.name === 'AbortError') { + throw new NetworkError('Request timeout'); + } + + if (err instanceof ApiError) { + throw err; + } + + throw new NetworkError( + err instanceof Error ? err.message : 'Network error' + ); + } + } + + async checkAddress( + params: CheckAddressParams + ): Promise { + if (!params.address) { + throw new ValidationError('Address is required', 'address'); + } + + return this.request('/api/v1/aml/check', { + method: 'POST', + body: JSON.stringify({ + address: params.address, + chainId: params.chainId ?? 1, + amount: params.amount, + senderAddress: params.senderAddress, + }), + }); + } + + async submitAppeal(params: SubmitAppealParams): Promise { + if (!params.address) { + throw new ValidationError('Address is required', 'address'); + } + if (!params.reason) { + throw new ValidationError('Reason is required', 'reason'); + } + + return this.request('/api/v1/aml/appeal', { + method: 'POST', + body: JSON.stringify({ + address: params.address, + chainId: params.chainId ?? 1, + reason: params.reason, + contact: params.contact, + }), + }); + } + + async getAppealStatus(ticketId: string): Promise { + if (!ticketId) { + throw new ValidationError('Ticket ID is required', 'ticketId'); + } + + return this.request( + `/api/v1/aml/appeal/status/${encodeURIComponent(ticketId)}` + ); + } + + async getWhitelistSummary(): Promise { + return this.request('/api/v1/aml/whitelist'); + } + + async healthCheck(): Promise { + return this.request('/api/v1/health'); + } + + setBaseUrl(url: string): void { + this.baseUrl = url.replace(/\/$/, ''); + } + + setApiKey(key: string): void { + this.apiKey = key; + } +} \ No newline at end of file diff --git a/packages/sdk/src/index.ts b/packages/sdk/src/index.ts new file mode 100644 index 0000000..d860d06 --- /dev/null +++ b/packages/sdk/src/index.ts @@ -0,0 +1,2 @@ +export * from './client'; +export * from './types'; diff --git a/packages/sdk/src/types.ts b/packages/sdk/src/types.ts new file mode 100644 index 0000000..673a582 --- /dev/null +++ b/packages/sdk/src/types.ts @@ -0,0 +1,142 @@ +// Risk levels and action types +export type RiskLevel = 'LOW' | 'MEDIUM' | 'HIGH'; +export type Action = 'ALLOW' | 'REVIEW' | 'BLOCK'; +export type AppealStatus = 'PENDING' | 'APPROVED' | 'REJECTED'; + +// Check address params and response +export interface CheckAddressParams { + address: string; + chainId?: number; + amount?: string; + senderAddress?: string; +} + +export interface CheckAddressResponse { + checkId: string; + address: string; + chainId: number; + riskScore: number; + riskLevel: RiskLevel; + decision: Action; + riskType?: string; + factors?: { + details?: string[]; + }; + isWhitelisted: boolean; + cacheHit: boolean; + cacheTier?: string; + cachedAt?: string; + expiresAt?: string; + fallback?: boolean; + fallbackReason?: string; +} + +// Appeal params and response +export interface SubmitAppealParams { + address: string; + chainId?: number; + reason: string; + contact?: string; +} + +export interface SubmitAppealResponse { + ticketId: string; + address: string; + chainId: number; + status: AppealStatus; + estimatedReviewAt: string; + message: string; + nextSteps: string[]; +} + +export interface AppealStatusResponse { + ticketId: string; + status: AppealStatus; + createdAt: string; + estimatedReviewAt: string; + address?: string; + chainId?: number; + reason?: string; + contact?: string; + reviewedAt?: string; + reviewer?: string; + decision?: AppealStatus; + notes?: string; +} + +// Whitelist response +export interface WhitelistCategory { + category: string; + count: number; +} + +export interface WhitelistSummary { + total: number; + categories: WhitelistCategory[]; + lastSyncedAt: string; + version: string; +} + +// Health check response +export interface ServiceHealth { + healthy: boolean; + status: string; + error?: string; +} + +export interface HealthCheckResponse { + status: 'healthy' | 'degraded' | 'unhealthy'; + timestamp: string; + version: string; + uptime: string; + services: { + database: ServiceHealth | string; + riskData: ServiceHealth | string; + cache: ServiceHealth | string; + redis: string; + }; +} + +// Client configuration +export interface BridgeShieldConfig { + baseUrl: string; + apiKey?: string; + timeout?: number; +} + +// Error types +export class BridgeShieldError extends Error { + constructor(message: string) { + super(message); + this.name = 'BridgeShieldError'; + } +} + +export class ApiError extends BridgeShieldError { + statusCode: number; + response?: unknown; + + constructor(message: string, statusCode: number, response?: unknown) { + super(message); + this.name = 'ApiError'; + this.statusCode = statusCode; + this.response = response; + } +} + +export class NetworkError extends BridgeShieldError { + constructor(message: string) { + super(message); + this.name = 'NetworkError'; + } +} + +export class ValidationError extends BridgeShieldError { + field?: string; + + constructor(message: string, field?: string) { + super(message); + this.name = 'ValidationError'; + this.field = field; + } +} diff --git a/packages/sdk/tsconfig.json b/packages/sdk/tsconfig.json new file mode 100644 index 0000000..d3d1601 --- /dev/null +++ b/packages/sdk/tsconfig.json @@ -0,0 +1,20 @@ +{ + "compilerOptions": { + "target": "ES2020", + "module": "ESNext", + "lib": ["ES2020", "DOM"], + "moduleResolution": "bundler", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "declaration": true, + "declarationMap": true, + "outDir": "./dist", + "rootDir": "./src", + "resolveJsonModule": true, + "isolatedModules": true + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist", "__tests__"] +} \ No newline at end of file diff --git a/packages/sdk/tsup.config.ts b/packages/sdk/tsup.config.ts new file mode 100644 index 0000000..10e0398 --- /dev/null +++ b/packages/sdk/tsup.config.ts @@ -0,0 +1,12 @@ +import { defineConfig } from 'tsup'; + +export default defineConfig({ + entry: ['src/index.ts'], + format: ['esm', 'cjs'], + dts: true, + splitting: false, + sourcemap: true, + clean: true, + treeshake: true, + minify: false, +});