QRurl.us

Torey Heinz committed Aug 22, 2025
commit 9cfc8c23144affc85a6395bf5fbad05e674b46a0
Showing 43 changed files with 8365 additions and 0 deletions
.dev.vars.example +3 -0
@@ @@ -0,0 +1,3 @@
+ JWT_SECRET=your-secret-key-change-in-production
+ EMAIL_API_KEY=your-resend-or-sendgrid-api-key
+ AUTHORIZED_EMAILS=admin@example.com,user@example.com
\ No newline at end of file
.gitignore +36 -0
@@ @@ -0,0 +1,36 @@
+ # Dependencies
+ node_modules/
+
+ # Environment files
+ .env
+ .env.*
+ .dev.vars
+
+ # Wrangler
+ .wrangler/
+ .dev.vars
+
+ # Build output
+ dist/
+ build/
+
+ # Logs
+ *.log
+ npm-debug.log*
+
+ # OS files
+ .DS_Store
+ Thumbs.db
+
+ # IDE
+ .idea/
+ .vscode/
+ *.swp
+ *.swo
+
+ # Test coverage
+ coverage/
+
+ # Local database
+ *.sqlite
+ *.sqlite3
\ No newline at end of file
README.md +201 -0
@@ @@ -0,0 +1,201 @@
+ # QRurl - URL Shortener with QR Codes
+
+ A modern, serverless URL shortener with QR code generation built on Cloudflare Workers, D1, and R2.
+
+ ## Features
+
+ - ๐Ÿ”— **Custom Short URLs** - Create memorable short links with custom slugs
+ - ๐Ÿ“ฑ **QR Code Generation** - Generate QR codes with optional logo embedding
+ - ๐Ÿ” **Magic Link Authentication** - Passwordless login via email
+ - ๐Ÿ“Š **Analytics Dashboard** - Track clicks, geographic data, and referrers
+ - โšก **Edge Performance** - Global low-latency redirects via Cloudflare Workers
+ - ๐Ÿ’พ **Serverless Architecture** - Cost-efficient with automatic scaling
+
+ ## Tech Stack
+
+ ### Backend
+ - **Cloudflare Workers** - Edge compute platform
+ - **D1 Database** - SQLite-based serverless database
+ - **R2 Storage** - Object storage for images and QR codes
+ - **KV Namespace** - Key-value storage for caching
+
+ ### Frontend
+ - **Vue 3** - Progressive JavaScript framework
+ - **Vite** - Fast build tool
+ - **Tailwind CSS** - Utility-first CSS framework
+ - **Vue Router** - Client-side routing
+ - **Pinia** - State management
+ - **Axios** - HTTP client
+
+ ## Getting Started
+
+ ### Prerequisites
+ - Node.js 18+ and npm
+ - Cloudflare account
+ - Wrangler CLI (`npm install -g wrangler`)
+
+ ### Installation
+
+ 1. Clone the repository:
+ ```bash
+ git clone https://github.com/yourusername/qrurl.git
+ cd qrurl
+ ```
+
+ 2. Install backend dependencies:
+ ```bash
+ npm install
+ ```
+
+ 3. Install frontend dependencies:
+ ```bash
+ cd frontend
+ npm install
+ cd ..
+ ```
+
+ 4. Configure Cloudflare services:
+ ```bash
+ # Login to Cloudflare
+ wrangler login
+
+ # The D1 database, R2 bucket, and KV namespace are already configured
+ # Check wrangler.toml for the configuration
+ ```
+
+ 5. Set up environment variables:
+ ```bash
+ # Backend (.dev.vars)
+ cp .dev.vars.example .dev.vars
+ # Edit .dev.vars with your configuration
+
+ # Frontend (frontend/.env)
+ cp frontend/.env.example frontend/.env
+ # Edit frontend/.env if needed
+ ```
+
+ 6. Initialize the database:
+ ```bash
+ # Local database
+ npm run db:init
+
+ # Remote database (production)
+ npm run db:init:remote
+ ```
+
+ ### Development
+
+ 1. Start the backend (Cloudflare Workers):
+ ```bash
+ npm run dev
+ # Backend runs on http://localhost:8787
+ ```
+
+ 2. In a new terminal, start the frontend:
+ ```bash
+ cd frontend
+ npm run dev
+ # Frontend runs on http://localhost:3000
+ ```
+
+ 3. Open http://localhost:3000 in your browser
+
+ ### API Endpoints
+
+ #### Authentication
+ - `POST /api/auth/request` - Request magic link
+ - `POST /api/auth/verify` - Verify magic link token
+ - `POST /api/auth/logout` - Logout
+
+ #### Entries (Protected)
+ - `GET /api/entries` - List user's entries
+ - `POST /api/entries` - Create new entry
+ - `GET /api/entries/:id` - Get entry details
+ - `PUT /api/entries/:id` - Update entry
+ - `DELETE /api/entries/:id` - Delete entry
+
+ #### Analytics (Protected)
+ - `GET /api/analytics/:id` - Get entry analytics
+
+ #### Public
+ - `GET /health` - Health check
+ - `GET /:slug` - Redirect to original URL
+
+ ### Deployment
+
+ 1. Deploy the backend to Cloudflare Workers:
+ ```bash
+ npm run deploy
+ ```
+
+ 2. Build and deploy the frontend:
+ ```bash
+ cd frontend
+ npm run build
+ # Deploy the dist folder to Cloudflare Pages or your preferred hosting
+ ```
+
+ ### Configuration
+
+ #### Email Service
+ The application uses magic links for authentication. Configure your email service provider in `.dev.vars`:
+ - Resend: Set `EMAIL_API_KEY` with your Resend API key
+ - SendGrid: Set `EMAIL_API_KEY` with your SendGrid API key
+
+ #### Authorized Emails
+ Add authorized emails in:
+ 1. Environment variable: `AUTHORIZED_EMAILS` (comma-separated)
+ 2. Database: `authorized_emails` table
+
+ ## Project Structure
+
+ ```
+ qrurl/
+ โ”œโ”€โ”€ src/ # Backend source code
+ โ”‚ โ”œโ”€โ”€ index.js # Main worker entry
+ โ”‚ โ”œโ”€โ”€ routes/ # API route handlers
+ โ”‚ โ”œโ”€โ”€ middleware/ # Auth, CORS, rate limiting
+ โ”‚ โ”œโ”€โ”€ lib/ # Database operations
+ โ”‚ โ””โ”€โ”€ utils/ # Utilities
+ โ”œโ”€โ”€ frontend/ # Vue.js frontend
+ โ”‚ โ”œโ”€โ”€ src/
+ โ”‚ โ”‚ โ”œโ”€โ”€ views/ # Page components
+ โ”‚ โ”‚ โ”œโ”€โ”€ components/ # Reusable components
+ โ”‚ โ”‚ โ”œโ”€โ”€ stores/ # Pinia stores
+ โ”‚ โ”‚ โ”œโ”€โ”€ services/ # API and QR services
+ โ”‚ โ”‚ โ””โ”€โ”€ router/ # Vue Router config
+ โ”‚ โ””โ”€โ”€ public/
+ โ”œโ”€โ”€ schema/ # Database schema
+ โ”œโ”€โ”€ dev/ # Development docs
+ โ””โ”€โ”€ wrangler.toml # Cloudflare Workers config
+ ```
+
+ ## Security
+
+ - JWT-based authentication with secure tokens
+ - Magic link authentication (passwordless)
+ - Email whitelist for access control
+ - Rate limiting on API endpoints
+ - Input validation and sanitization
+ - CORS protection
+ - SQL injection prevention
+
+ ## Performance
+
+ - Edge deployment for global low latency
+ - KV caching for frequently accessed data
+ - Optimized database queries with indexes
+ - QR code caching in R2
+ - Lazy loading in frontend
+
+ ## License
+
+ MIT
+
+ ## Contributing
+
+ Pull requests are welcome! Please read the contributing guidelines first.
+
+ ## Support
+
+ For issues and questions, please use the GitHub issues page.
\ No newline at end of file
dev/qrurl-mvp.md +228 -0
@@ @@ -0,0 +1,228 @@
+ # Feature Plan: QRurl MVP Implementation
+
+ **Status**: Planning
+ **Created**: 2025-08-22
+ **Author**: Claude
+
+ ## Overview
+ Building a serverless URL shortener with QR code generation capabilities using Cloudflare Workers, D1 database, and R2 storage. The MVP will include magic link authentication, custom branded QR codes, basic analytics, and a simple management interface.
+
+ ## Requirements
+ - URL shortening with custom slugs
+ - QR code generation with logo embedding
+ - Magic link authentication (passwordless)
+ - Email whitelist for access control
+ - Basic analytics tracking (clicks, referrers, geographic data)
+ - Admin interface for managing links
+ - Serverless deployment on Cloudflare Workers
+
+ ## Technical Approach
+ Leverage Cloudflare's edge infrastructure for global low-latency redirects. Use D1 for data persistence, R2 for image/QR storage, and Workers for all compute. Frontend will be a SPA served from Workers or Pages.
+
+ ### Architecture Decisions
+ - **Cloudflare Workers**: Chosen for edge performance and cost-efficiency
+ - **D1 Database**: SQLite-based, perfect for read-heavy workloads
+ - **R2 Storage**: Cost-effective object storage for QR codes and logos
+ - **Magic Links**: Simpler than password management, better UX
+ - **QR with High Error Correction**: Allows 30% obstruction for logo overlay
+
+ ## Implementation Tasks
+
+ ### Phase 1: Foundation (Week 1)
+ - [ ] Initialize Cloudflare Workers project with Wrangler
+ - [ ] Set up development environment and local testing
+ - [ ] Create D1 database and implement schema
+ - [ ] Configure R2 bucket for storage
+ - [ ] Set up basic routing structure
+ - [ ] Implement health check endpoint
+ - [ ] Configure environment variables and secrets
+
+ ### Phase 2: Core URL Functionality (Week 1-2)
+ - [ ] Implement slug generation (nanoid with custom alphabet)
+ - [ ] Create URL entry CRUD operations
+ - [ ] Build redirect handler with analytics capture
+ - [ ] Add URL validation and sanitization
+ - [ ] Implement slug collision handling
+ - [ ] Create basic rate limiting
+ - [ ] Write unit tests for URL operations
+
+ ### Phase 3: Authentication System (Week 2)
+ - [ ] Design magic link token generation
+ - [ ] Create email whitelist management
+ - [ ] Implement token storage with expiry (15 min)
+ - [ ] Build email sending integration (Resend/SendGrid)
+ - [ ] Create JWT session management
+ - [ ] Add authentication middleware
+ - [ ] Implement logout functionality
+ - [ ] Write auth system tests
+
+ ### Phase 4: QR Code Generation (Week 3)
+ - [ ] Research and select QR library for Workers
+ - [ ] Implement basic QR code generation
+ - [ ] Add logo overlay functionality (max 30% area)
+ - [ ] Create QR caching strategy in R2
+ - [ ] Build image upload and processing
+ - [ ] Implement logo resizing (128x128 standard)
+ - [ ] Add QR customization options (colors, margins)
+ - [ ] Test QR scanning reliability with logos
+
+ ### Phase 5: Analytics & Tracking (Week 3-4)
+ - [ ] Design analytics data model
+ - [ ] Implement click tracking
+ - [ ] Add geographic detection via CF headers
+ - [ ] Create user agent parsing
+ - [ ] Build referrer tracking
+ - [ ] Implement analytics aggregation
+ - [ ] Add batch write optimization
+ - [ ] Create analytics API endpoints
+
+ ### Phase 6: Frontend Interface (Week 4-5)
+ - [ ] Set up frontend framework (React/Vue)
+ - [ ] Create authentication flow UI
+ - [ ] Build link management dashboard
+ - [ ] Implement QR code preview
+ - [ ] Add analytics visualization
+ - [ ] Create link creation form
+ - [ ] Build responsive mobile interface
+ - [ ] Add dark mode support
+
+ ### Phase 7: Polish & Optimization (Week 5-6)
+ - [ ] Implement comprehensive error handling
+ - [ ] Add input validation across all endpoints
+ - [ ] Set up monitoring and alerting
+ - [ ] Optimize database queries
+ - [ ] Implement caching strategy
+ - [ ] Add security headers
+ - [ ] Create API documentation
+ - [ ] Write deployment scripts
+
+ ## Files to Create/Modify
+
+ ### Project Structure
+ ```
+ /
+ โ”œโ”€โ”€ wrangler.toml # Workers configuration
+ โ”œโ”€โ”€ package.json # Dependencies
+ โ”œโ”€โ”€ src/
+ โ”‚ โ”œโ”€โ”€ index.js # Main worker entry
+ โ”‚ โ”œโ”€โ”€ routes/
+ โ”‚ โ”‚ โ”œโ”€โ”€ auth.js # Authentication endpoints
+ โ”‚ โ”‚ โ”œโ”€โ”€ api.js # API routes
+ โ”‚ โ”‚ โ””โ”€โ”€ redirect.js # Redirect handler
+ โ”‚ โ”œโ”€โ”€ lib/
+ โ”‚ โ”‚ โ”œโ”€โ”€ db.js # Database operations
+ โ”‚ โ”‚ โ”œโ”€โ”€ qr.js # QR generation
+ โ”‚ โ”‚ โ”œโ”€โ”€ email.js # Email service
+ โ”‚ โ”‚ โ””โ”€โ”€ analytics.js # Analytics tracking
+ โ”‚ โ”œโ”€โ”€ middleware/
+ โ”‚ โ”‚ โ”œโ”€โ”€ auth.js # Auth middleware
+ โ”‚ โ”‚ โ””โ”€โ”€ rateLimit.js # Rate limiting
+ โ”‚ โ””โ”€โ”€ utils/
+ โ”‚ โ”œโ”€โ”€ slug.js # Slug generation
+ โ”‚ โ””โ”€โ”€ validation.js # Input validation
+ โ”œโ”€โ”€ schema/
+ โ”‚ โ””โ”€โ”€ schema.sql # D1 database schema
+ โ”œโ”€โ”€ frontend/
+ โ”‚ โ”œโ”€โ”€ src/ # Frontend application
+ โ”‚ โ””โ”€โ”€ public/ # Static assets
+ โ””โ”€โ”€ tests/
+ โ”œโ”€โ”€ unit/ # Unit tests
+ โ””โ”€โ”€ integration/ # Integration tests
+ ```
+
+ ## Testing Strategy
+
+ ### Unit Tests
+ - Slug generation uniqueness and format
+ - URL validation and sanitization
+ - Token generation and expiry
+ - QR code generation with various inputs
+ - Analytics data aggregation
+
+ ### Integration Tests
+ - Full authentication flow
+ - URL creation โ†’ redirect โ†’ analytics
+ - QR generation with logo upload
+ - Rate limiting behavior
+ - Email delivery
+
+ ### Performance Tests
+ - Redirect latency under load
+ - QR generation performance
+ - Database query optimization
+ - Concurrent user handling
+
+ ## Edge Cases & Error Handling
+ - **Duplicate slugs**: Retry with different random slug or return error
+ - **Invalid URLs**: Validate protocol, prevent javascript: and data: URIs
+ - **Large logo files**: Resize before storage, reject if > 5MB
+ - **Email delivery failure**: Queue for retry, provide alternate auth method
+ - **Rate limit exceeded**: Return 429 with retry-after header
+ - **Database connection issues**: Implement circuit breaker pattern
+ - **QR generation timeout**: Pre-generate and cache popular QRs
+
+ ## Security Considerations
+ - Input sanitization for all user inputs
+ - CSRF protection on state-changing operations
+ - Secure random token generation (crypto.getRandomValues)
+ - Content Security Policy headers
+ - SQL injection prevention via parameterized queries
+ - XSS prevention in frontend
+ - Rate limiting per IP and per user
+ - Secure cookie settings (httpOnly, secure, sameSite)
+
+ ## Performance Targets
+ - Redirect latency: < 50ms globally
+ - QR generation: < 500ms with logo
+ - API response time: < 200ms
+ - Time to interactive (frontend): < 2s
+ - Database queries: < 10ms for reads
+
+ ## Cost Estimation
+ - Workers: ~$5/month (10M requests)
+ - D1: Free tier (5GB, 500M reads)
+ - R2: Free tier (10GB storage)
+ - Email service: ~$10/month (Resend starter)
+ - **Total**: ~$15/month for moderate usage
+
+ ## Open Questions
+ - [ ] Should we support custom domains for branded short links?
+ - [ ] How should we handle GDPR/privacy for analytics?
+ - [ ] Should QR codes support dynamic content updates?
+ - [ ] Do we need bulk import/export functionality?
+ - [ ] Should we add team/organization support later?
+
+ ## Future Considerations
+ - Custom domain support for enterprise users
+ - API access for programmatic link creation
+ - Webhook support for analytics events
+ - A/B testing for destination URLs
+ - Link expiration and scheduling
+ - Password-protected links
+ - Browser extension for quick shortening
+ - Mobile app for QR scanning and management
+
+ ## Documentation Plan
+ After implementation, create `dev/docs/20250822-qrurl-mvp.md` with:
+ - System architecture overview
+ - API documentation with examples
+ - Deployment guide
+ - Configuration options
+ - Troubleshooting common issues
+ - Performance tuning guide
+ - Security best practices
+
+ ## Success Metrics
+ - Successfully deploy to production
+ - Handle 1000+ redirects/day without issues
+ - Generate QR codes with logos that scan reliably
+ - Achieve < 50ms redirect latency globally
+ - Pass security audit
+ - Complete implementation within 6 weeks
+
+ ## Next Steps
+ 1. Review and approve this plan
+ 2. Set up Cloudflare account and services
+ 3. Initialize project repository
+ 4. Begin Phase 1 implementation
+ 5. Set up CI/CD pipeline
\ No newline at end of file
frontend/README.md +5 -0
@@ @@ -0,0 +1,5 @@
+ # Vue 3 + Vite
+
+ This template should help get you started developing with Vue 3 in Vite. The template uses Vue 3 `<script setup>` SFCs, check out the [script setup docs](https://v3.vuejs.org/api/sfc-script-setup.html#sfc-script-setup) to learn more.
+
+ Learn more about IDE Support for Vue in the [Vue Docs Scaling up Guide](https://vuejs.org/guide/scaling-up/tooling.html#ide-support).
frontend/index.html +13 -0
@@ @@ -0,0 +1,13 @@
+ <!doctype html>
+ <html lang="en">
+ <head>
+ <meta charset="UTF-8" />
+ <link rel="icon" type="image/svg+xml" href="/vite.svg" />
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+ <title>QRurl - Short Links & QR Codes</title>
+ </head>
+ <body>
+ <div id="app"></div>
+ <script type="module" src="/src/main.js"></script>
+ </body>
+ </html>
frontend/package-lock.json +3684 -0
@@ @@ -0,0 +1,3684 @@
+ {
+ "name": "frontend",
+ "version": "0.0.0",
+ "lockfileVersion": 3,
+ "requires": true,
+ "packages": {
+ "": {
+ "name": "frontend",
+ "version": "0.0.0",
+ "dependencies": {
+ "@tailwindcss/forms": "^0.5.10",
+ "@vueuse/core": "^13.7.0",
+ "autoprefixer": "^10.4.21",
+ "axios": "^1.11.0",
+ "lucide-vue-next": "^0.541.0",
+ "pinia": "^3.0.3",
+ "postcss": "^8.5.6",
+ "qrcode": "^1.5.4",
+ "qrcode.js": "^0.0.1",
+ "tailwindcss": "^3.4.17",
+ "vue": "^3.5.18",
+ "vue-router": "^4.5.1"
+ },
+ "devDependencies": {
+ "@vitejs/plugin-vue": "^6.0.1",
+ "vite": "^7.1.2"
+ }
+ },
+ "node_modules/@alloc/quick-lru": {
+ "version": "5.2.0",
+ "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz",
+ "integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/@babel/helper-string-parser": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz",
+ "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-validator-identifier": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.27.1.tgz",
+ "integrity": "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/parser": {
+ "version": "7.28.3",
+ "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.3.tgz",
+ "integrity": "sha512-7+Ey1mAgYqFAx2h0RuoxcQT5+MlG3GTV0TQrgr7/ZliKsm/MNDxVVutlWaziMq7wJNAz8MTqz55XLpWvva6StA==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/types": "^7.28.2"
+ },
+ "bin": {
+ "parser": "bin/babel-parser.js"
+ },
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/@babel/types": {
+ "version": "7.28.2",
+ "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.2.tgz",
+ "integrity": "sha512-ruv7Ae4J5dUYULmeXw1gmb7rYRz57OWCPM57pHojnLq/3Z1CK2lNSLTCVjxVk1F/TZHwOZZrOWi0ur95BbLxNQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-string-parser": "^7.27.1",
+ "@babel/helper-validator-identifier": "^7.27.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@esbuild/aix-ppc64": {
+ "version": "0.25.9",
+ "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.9.tgz",
+ "integrity": "sha512-OaGtL73Jck6pBKjNIe24BnFE6agGl+6KxDtTfHhy1HmhthfKouEcOhqpSL64K4/0WCtbKFLOdzD/44cJ4k9opA==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "aix"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/android-arm": {
+ "version": "0.25.9",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.9.tgz",
+ "integrity": "sha512-5WNI1DaMtxQ7t7B6xa572XMXpHAaI/9Hnhk8lcxF4zVN4xstUgTlvuGDorBguKEnZO70qwEcLpfifMLoxiPqHQ==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/android-arm64": {
+ "version": "0.25.9",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.9.tgz",
+ "integrity": "sha512-IDrddSmpSv51ftWslJMvl3Q2ZT98fUSL2/rlUXuVqRXHCs5EUF1/f+jbjF5+NG9UffUDMCiTyh8iec7u8RlTLg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/android-x64": {
+ "version": "0.25.9",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.9.tgz",
+ "integrity": "sha512-I853iMZ1hWZdNllhVZKm34f4wErd4lMyeV7BLzEExGEIZYsOzqDWDf+y082izYUE8gtJnYHdeDpN/6tUdwvfiw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/darwin-arm64": {
+ "version": "0.25.9",
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.9.tgz",
+ "integrity": "sha512-XIpIDMAjOELi/9PB30vEbVMs3GV1v2zkkPnuyRRURbhqjyzIINwj+nbQATh4H9GxUgH1kFsEyQMxwiLFKUS6Rg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/darwin-x64": {
+ "version": "0.25.9",
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.9.tgz",
+ "integrity": "sha512-jhHfBzjYTA1IQu8VyrjCX4ApJDnH+ez+IYVEoJHeqJm9VhG9Dh2BYaJritkYK3vMaXrf7Ogr/0MQ8/MeIefsPQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/freebsd-arm64": {
+ "version": "0.25.9",
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.9.tgz",
+ "integrity": "sha512-z93DmbnY6fX9+KdD4Ue/H6sYs+bhFQJNCPZsi4XWJoYblUqT06MQUdBCpcSfuiN72AbqeBFu5LVQTjfXDE2A6Q==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/freebsd-x64": {
+ "version": "0.25.9",
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.9.tgz",
+ "integrity": "sha512-mrKX6H/vOyo5v71YfXWJxLVxgy1kyt1MQaD8wZJgJfG4gq4DpQGpgTB74e5yBeQdyMTbgxp0YtNj7NuHN0PoZg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-arm": {
+ "version": "0.25.9",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.9.tgz",
+ "integrity": "sha512-HBU2Xv78SMgaydBmdor38lg8YDnFKSARg1Q6AT0/y2ezUAKiZvc211RDFHlEZRFNRVhcMamiToo7bDx3VEOYQw==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-arm64": {
+ "version": "0.25.9",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.9.tgz",
+ "integrity": "sha512-BlB7bIcLT3G26urh5Dmse7fiLmLXnRlopw4s8DalgZ8ef79Jj4aUcYbk90g8iCa2467HX8SAIidbL7gsqXHdRw==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-ia32": {
+ "version": "0.25.9",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.9.tgz",
+ "integrity": "sha512-e7S3MOJPZGp2QW6AK6+Ly81rC7oOSerQ+P8L0ta4FhVi+/j/v2yZzx5CqqDaWjtPFfYz21Vi1S0auHrap3Ma3A==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-loong64": {
+ "version": "0.25.9",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.9.tgz",
+ "integrity": "sha512-Sbe10Bnn0oUAB2AalYztvGcK+o6YFFA/9829PhOCUS9vkJElXGdphz0A3DbMdP8gmKkqPmPcMJmJOrI3VYB1JQ==",
+ "cpu": [
+ "loong64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-mips64el": {
+ "version": "0.25.9",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.9.tgz",
+ "integrity": "sha512-YcM5br0mVyZw2jcQeLIkhWtKPeVfAerES5PvOzaDxVtIyZ2NUBZKNLjC5z3/fUlDgT6w89VsxP2qzNipOaaDyA==",
+ "cpu": [
+ "mips64el"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-ppc64": {
+ "version": "0.25.9",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.9.tgz",
+ "integrity": "sha512-++0HQvasdo20JytyDpFvQtNrEsAgNG2CY1CLMwGXfFTKGBGQT3bOeLSYE2l1fYdvML5KUuwn9Z8L1EWe2tzs1w==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-riscv64": {
+ "version": "0.25.9",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.9.tgz",
+ "integrity": "sha512-uNIBa279Y3fkjV+2cUjx36xkx7eSjb8IvnL01eXUKXez/CBHNRw5ekCGMPM0BcmqBxBcdgUWuUXmVWwm4CH9kg==",
+ "cpu": [
+ "riscv64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-s390x": {
+ "version": "0.25.9",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.9.tgz",
+ "integrity": "sha512-Mfiphvp3MjC/lctb+7D287Xw1DGzqJPb/J2aHHcHxflUo+8tmN/6d4k6I2yFR7BVo5/g7x2Monq4+Yew0EHRIA==",
+ "cpu": [
+ "s390x"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-x64": {
+ "version": "0.25.9",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.9.tgz",
+ "integrity": "sha512-iSwByxzRe48YVkmpbgoxVzn76BXjlYFXC7NvLYq+b+kDjyyk30J0JY47DIn8z1MO3K0oSl9fZoRmZPQI4Hklzg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/netbsd-arm64": {
+ "version": "0.25.9",
+ "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.9.tgz",
+ "integrity": "sha512-9jNJl6FqaUG+COdQMjSCGW4QiMHH88xWbvZ+kRVblZsWrkXlABuGdFJ1E9L7HK+T0Yqd4akKNa/lO0+jDxQD4Q==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "netbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/netbsd-x64": {
+ "version": "0.25.9",
+ "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.9.tgz",
+ "integrity": "sha512-RLLdkflmqRG8KanPGOU7Rpg829ZHu8nFy5Pqdi9U01VYtG9Y0zOG6Vr2z4/S+/3zIyOxiK6cCeYNWOFR9QP87g==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "netbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/openbsd-arm64": {
+ "version": "0.25.9",
+ "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.9.tgz",
+ "integrity": "sha512-YaFBlPGeDasft5IIM+CQAhJAqS3St3nJzDEgsgFixcfZeyGPCd6eJBWzke5piZuZ7CtL656eOSYKk4Ls2C0FRQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/openbsd-x64": {
+ "version": "0.25.9",
+ "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.9.tgz",
+ "integrity": "sha512-1MkgTCuvMGWuqVtAvkpkXFmtL8XhWy+j4jaSO2wxfJtilVCi0ZE37b8uOdMItIHz4I6z1bWWtEX4CJwcKYLcuA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/openharmony-arm64": {
+ "version": "0.25.9",
+ "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.9.tgz",
+ "integrity": "sha512-4Xd0xNiMVXKh6Fa7HEJQbrpP3m3DDn43jKxMjxLLRjWnRsfxjORYJlXPO4JNcXtOyfajXorRKY9NkOpTHptErg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openharmony"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/sunos-x64": {
+ "version": "0.25.9",
+ "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.9.tgz",
+ "integrity": "sha512-WjH4s6hzo00nNezhp3wFIAfmGZ8U7KtrJNlFMRKxiI9mxEK1scOMAaa9i4crUtu+tBr+0IN6JCuAcSBJZfnphw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "sunos"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/win32-arm64": {
+ "version": "0.25.9",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.9.tgz",
+ "integrity": "sha512-mGFrVJHmZiRqmP8xFOc6b84/7xa5y5YvR1x8djzXpJBSv/UsNK6aqec+6JDjConTgvvQefdGhFDAs2DLAds6gQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/win32-ia32": {
+ "version": "0.25.9",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.9.tgz",
+ "integrity": "sha512-b33gLVU2k11nVx1OhX3C8QQP6UHQK4ZtN56oFWvVXvz2VkDoe6fbG8TOgHFxEvqeqohmRnIHe5A1+HADk4OQww==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/win32-x64": {
+ "version": "0.25.9",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.9.tgz",
+ "integrity": "sha512-PPOl1mi6lpLNQxnGoyAfschAodRFYXJ+9fs6WHXz7CSWKbOqiMZsubC+BQsVKuul+3vKLuwTHsS2c2y9EoKwxQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@isaacs/cliui": {
+ "version": "8.0.2",
+ "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz",
+ "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==",
+ "license": "ISC",
+ "dependencies": {
+ "string-width": "^5.1.2",
+ "string-width-cjs": "npm:string-width@^4.2.0",
+ "strip-ansi": "^7.0.1",
+ "strip-ansi-cjs": "npm:strip-ansi@^6.0.1",
+ "wrap-ansi": "^8.1.0",
+ "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@isaacs/cliui/node_modules/ansi-regex": {
+ "version": "6.2.0",
+ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.0.tgz",
+ "integrity": "sha512-TKY5pyBkHyADOPYlRT9Lx6F544mPl0vS5Ew7BJ45hA08Q+t3GjbueLliBWN3sMICk6+y7HdyxSzC4bWS8baBdg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/ansi-regex?sponsor=1"
+ }
+ },
+ "node_modules/@isaacs/cliui/node_modules/ansi-styles": {
+ "version": "6.2.1",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz",
+ "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/ansi-styles?sponsor=1"
+ }
+ },
+ "node_modules/@isaacs/cliui/node_modules/emoji-regex": {
+ "version": "9.2.2",
+ "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz",
+ "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==",
+ "license": "MIT"
+ },
+ "node_modules/@isaacs/cliui/node_modules/string-width": {
+ "version": "5.1.2",
+ "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz",
+ "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==",
+ "license": "MIT",
+ "dependencies": {
+ "eastasianwidth": "^0.2.0",
+ "emoji-regex": "^9.2.2",
+ "strip-ansi": "^7.0.1"
+ },
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/@isaacs/cliui/node_modules/strip-ansi": {
+ "version": "7.1.0",
+ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz",
+ "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==",
+ "license": "MIT",
+ "dependencies": {
+ "ansi-regex": "^6.0.1"
+ },
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/strip-ansi?sponsor=1"
+ }
+ },
+ "node_modules/@isaacs/cliui/node_modules/wrap-ansi": {
+ "version": "8.1.0",
+ "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz",
+ "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==",
+ "license": "MIT",
+ "dependencies": {
+ "ansi-styles": "^6.1.0",
+ "string-width": "^5.0.1",
+ "strip-ansi": "^7.0.1"
+ },
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/wrap-ansi?sponsor=1"
+ }
+ },
+ "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==",
+ "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==",
+ "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==",
+ "license": "MIT"
+ },
+ "node_modules/@jridgewell/trace-mapping": {
+ "version": "0.3.30",
+ "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.30.tgz",
+ "integrity": "sha512-GQ7Nw5G2lTu/BtHTKfXhKHok2WGetd4XYcVKGx00SjAk8GMwgJM3zr6zORiPGuOE+/vkc90KtTosSSvaCjKb2Q==",
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/resolve-uri": "^3.1.0",
+ "@jridgewell/sourcemap-codec": "^1.4.14"
+ }
+ },
+ "node_modules/@nodelib/fs.scandir": {
+ "version": "2.1.5",
+ "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
+ "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==",
+ "license": "MIT",
+ "dependencies": {
+ "@nodelib/fs.stat": "2.0.5",
+ "run-parallel": "^1.1.9"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/@nodelib/fs.stat": {
+ "version": "2.0.5",
+ "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz",
+ "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/@nodelib/fs.walk": {
+ "version": "1.2.8",
+ "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz",
+ "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==",
+ "license": "MIT",
+ "dependencies": {
+ "@nodelib/fs.scandir": "2.1.5",
+ "fastq": "^1.6.0"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/@pkgjs/parseargs": {
+ "version": "0.11.0",
+ "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz",
+ "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==",
+ "license": "MIT",
+ "optional": true,
+ "engines": {
+ "node": ">=14"
+ }
+ },
+ "node_modules/@rolldown/pluginutils": {
+ "version": "1.0.0-beta.29",
+ "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.29.tgz",
+ "integrity": "sha512-NIJgOsMjbxAXvoGq/X0gD7VPMQ8j9g0BiDaNjVNVjvl+iKXxL3Jre0v31RmBYeLEmkbj2s02v8vFTbUXi5XS2Q==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@rollup/rollup-android-arm-eabi": {
+ "version": "4.47.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.47.1.tgz",
+ "integrity": "sha512-lTahKRJip0knffA/GTNFJMrToD+CM+JJ+Qt5kjzBK/sFQ0EWqfKW3AYQSlZXN98tX0lx66083U9JYIMioMMK7g==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ]
+ },
+ "node_modules/@rollup/rollup-android-arm64": {
+ "version": "4.47.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.47.1.tgz",
+ "integrity": "sha512-uqxkb3RJLzlBbh/bbNQ4r7YpSZnjgMgyoEOY7Fy6GCbelkDSAzeiogxMG9TfLsBbqmGsdDObo3mzGqa8hps4MA==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ]
+ },
+ "node_modules/@rollup/rollup-darwin-arm64": {
+ "version": "4.47.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.47.1.tgz",
+ "integrity": "sha512-tV6reObmxBDS4DDyLzTDIpymthNlxrLBGAoQx6m2a7eifSNEZdkXQl1PE4ZjCkEDPVgNXSzND/k9AQ3mC4IOEQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ]
+ },
+ "node_modules/@rollup/rollup-darwin-x64": {
+ "version": "4.47.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.47.1.tgz",
+ "integrity": "sha512-XuJRPTnMk1lwsSnS3vYyVMu4x/+WIw1MMSiqj5C4j3QOWsMzbJEK90zG+SWV1h0B1ABGCQ0UZUjti+TQK35uHQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ]
+ },
+ "node_modules/@rollup/rollup-freebsd-arm64": {
+ "version": "4.47.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.47.1.tgz",
+ "integrity": "sha512-79BAm8Ag/tmJ5asCqgOXsb3WY28Rdd5Lxj8ONiQzWzy9LvWORd5qVuOnjlqiWWZJw+dWewEktZb5yiM1DLLaHw==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ]
+ },
+ "node_modules/@rollup/rollup-freebsd-x64": {
+ "version": "4.47.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.47.1.tgz",
+ "integrity": "sha512-OQ2/ZDGzdOOlyfqBiip0ZX/jVFekzYrGtUsqAfLDbWy0jh1PUU18+jYp8UMpqhly5ltEqotc2miLngf9FPSWIA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm-gnueabihf": {
+ "version": "4.47.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.47.1.tgz",
+ "integrity": "sha512-HZZBXJL1udxlCVvoVadstgiU26seKkHbbAMLg7680gAcMnRNP9SAwTMVet02ANA94kXEI2VhBnXs4e5nf7KG2A==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm-musleabihf": {
+ "version": "4.47.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.47.1.tgz",
+ "integrity": "sha512-sZ5p2I9UA7T950JmuZ3pgdKA6+RTBr+0FpK427ExW0t7n+QwYOcmDTK/aRlzoBrWyTpJNlS3kacgSlSTUg6P/Q==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm64-gnu": {
+ "version": "4.47.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.47.1.tgz",
+ "integrity": "sha512-3hBFoqPyU89Dyf1mQRXCdpc6qC6At3LV6jbbIOZd72jcx7xNk3aAp+EjzAtN6sDlmHFzsDJN5yeUySvorWeRXA==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm64-musl": {
+ "version": "4.47.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.47.1.tgz",
+ "integrity": "sha512-49J4FnMHfGodJWPw73Ve+/hsPjZgcXQGkmqBGZFvltzBKRS+cvMiWNLadOMXKGnYRhs1ToTGM0sItKISoSGUNA==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-loongarch64-gnu": {
+ "version": "4.47.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.47.1.tgz",
+ "integrity": "sha512-4yYU8p7AneEpQkRX03pbpLmE21z5JNys16F1BZBZg5fP9rIlb0TkeQjn5du5w4agConCCEoYIG57sNxjryHEGg==",
+ "cpu": [
+ "loong64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-ppc64-gnu": {
+ "version": "4.47.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.47.1.tgz",
+ "integrity": "sha512-fAiq+J28l2YMWgC39jz/zPi2jqc0y3GSRo1yyxlBHt6UN0yYgnegHSRPa3pnHS5amT/efXQrm0ug5+aNEu9UuQ==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-riscv64-gnu": {
+ "version": "4.47.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.47.1.tgz",
+ "integrity": "sha512-daoT0PMENNdjVYYU9xec30Y2prb1AbEIbb64sqkcQcSaR0zYuKkoPuhIztfxuqN82KYCKKrj+tQe4Gi7OSm1ow==",
+ "cpu": [
+ "riscv64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-riscv64-musl": {
+ "version": "4.47.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.47.1.tgz",
+ "integrity": "sha512-JNyXaAhWtdzfXu5pUcHAuNwGQKevR+6z/poYQKVW+pLaYOj9G1meYc57/1Xv2u4uTxfu9qEWmNTjv/H/EpAisw==",
+ "cpu": [
+ "riscv64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-s390x-gnu": {
+ "version": "4.47.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.47.1.tgz",
+ "integrity": "sha512-U/CHbqKSwEQyZXjCpY43/GLYcTVKEXeRHw0rMBJP7fP3x6WpYG4LTJWR3ic6TeYKX6ZK7mrhltP4ppolyVhLVQ==",
+ "cpu": [
+ "s390x"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-x64-gnu": {
+ "version": "4.47.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.47.1.tgz",
+ "integrity": "sha512-uTLEakjxOTElfeZIGWkC34u2auLHB1AYS6wBjPGI00bWdxdLcCzK5awjs25YXpqB9lS8S0vbO0t9ZcBeNibA7g==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-x64-musl": {
+ "version": "4.47.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.47.1.tgz",
+ "integrity": "sha512-Ft+d/9DXs30BK7CHCTX11FtQGHUdpNDLJW0HHLign4lgMgBcPFN3NkdIXhC5r9iwsMwYreBBc4Rho5ieOmKNVQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-win32-arm64-msvc": {
+ "version": "4.47.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.47.1.tgz",
+ "integrity": "sha512-N9X5WqGYzZnjGAFsKSfYFtAShYjwOmFJoWbLg3dYixZOZqU7hdMq+/xyS14zKLhFhZDhP9VfkzQnsdk0ZDS9IA==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@rollup/rollup-win32-ia32-msvc": {
+ "version": "4.47.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.47.1.tgz",
+ "integrity": "sha512-O+KcfeCORZADEY8oQJk4HK8wtEOCRE4MdOkb8qGZQNun3jzmj2nmhV/B/ZaaZOkPmJyvm/gW9n0gsB4eRa1eiQ==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@rollup/rollup-win32-x64-msvc": {
+ "version": "4.47.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.47.1.tgz",
+ "integrity": "sha512-CpKnYa8eHthJa3c+C38v/E+/KZyF1Jdh2Cz3DyKZqEWYgrM1IHFArXNWvBLPQCKUEsAqqKX27tTqVEFbDNUcOA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@tailwindcss/forms": {
+ "version": "0.5.10",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/forms/-/forms-0.5.10.tgz",
+ "integrity": "sha512-utI1ONF6uf/pPNO68kmN1b8rEwNXv3czukalo8VtJH8ksIkZXr3Q3VYudZLkCsDd4Wku120uF02hYK25XGPorw==",
+ "license": "MIT",
+ "dependencies": {
+ "mini-svg-data-uri": "^1.2.3"
+ },
+ "peerDependencies": {
+ "tailwindcss": ">=3.0.0 || >= 3.0.0-alpha.1 || >= 4.0.0-alpha.20 || >= 4.0.0-beta.1"
+ }
+ },
+ "node_modules/@types/estree": {
+ "version": "1.0.8",
+ "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz",
+ "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/web-bluetooth": {
+ "version": "0.0.21",
+ "resolved": "https://registry.npmjs.org/@types/web-bluetooth/-/web-bluetooth-0.0.21.tgz",
+ "integrity": "sha512-oIQLCGWtcFZy2JW77j9k8nHzAOpqMHLQejDA48XXMWH6tjCQHz5RCFz1bzsmROyL6PUm+LLnUiI4BCn221inxA==",
+ "license": "MIT"
+ },
+ "node_modules/@vitejs/plugin-vue": {
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-6.0.1.tgz",
+ "integrity": "sha512-+MaE752hU0wfPFJEUAIxqw18+20euHHdxVtMvbFcOEpjEyfqXH/5DCoTHiVJ0J29EhTJdoTkjEv5YBKU9dnoTw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@rolldown/pluginutils": "1.0.0-beta.29"
+ },
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ },
+ "peerDependencies": {
+ "vite": "^5.0.0 || ^6.0.0 || ^7.0.0",
+ "vue": "^3.2.25"
+ }
+ },
+ "node_modules/@vue/compiler-core": {
+ "version": "3.5.19",
+ "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.19.tgz",
+ "integrity": "sha512-/afpyvlkrSNYbPo94Qu8GtIOWS+g5TRdOvs6XZNw6pWQQmj5pBgSZvEPOIZlqWq0YvoUhDDQaQ2TnzuJdOV4hA==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/parser": "^7.28.3",
+ "@vue/shared": "3.5.19",
+ "entities": "^4.5.0",
+ "estree-walker": "^2.0.2",
+ "source-map-js": "^1.2.1"
+ }
+ },
+ "node_modules/@vue/compiler-dom": {
+ "version": "3.5.19",
+ "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.19.tgz",
+ "integrity": "sha512-Drs6rPHQZx/pN9S6ml3Z3K/TWCIRPvzG2B/o5kFK9X0MNHt8/E+38tiRfojufrYBfA6FQUFB2qBBRXlcSXWtOA==",
+ "license": "MIT",
+ "dependencies": {
+ "@vue/compiler-core": "3.5.19",
+ "@vue/shared": "3.5.19"
+ }
+ },
+ "node_modules/@vue/compiler-sfc": {
+ "version": "3.5.19",
+ "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.19.tgz",
+ "integrity": "sha512-YWCm1CYaJ+2RvNmhCwI7t3I3nU+hOrWGWMsn+Z/kmm1jy5iinnVtlmkiZwbLlbV1SRizX7vHsc0/bG5dj0zRTg==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/parser": "^7.28.3",
+ "@vue/compiler-core": "3.5.19",
+ "@vue/compiler-dom": "3.5.19",
+ "@vue/compiler-ssr": "3.5.19",
+ "@vue/shared": "3.5.19",
+ "estree-walker": "^2.0.2",
+ "magic-string": "^0.30.17",
+ "postcss": "^8.5.6",
+ "source-map-js": "^1.2.1"
+ }
+ },
+ "node_modules/@vue/compiler-ssr": {
+ "version": "3.5.19",
+ "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.19.tgz",
+ "integrity": "sha512-/wx0VZtkWOPdiQLWPeQeqpHWR/LuNC7bHfSX7OayBTtUy8wur6vT6EQIX6Et86aED6J+y8tTw43qo2uoqGg5sw==",
+ "license": "MIT",
+ "dependencies": {
+ "@vue/compiler-dom": "3.5.19",
+ "@vue/shared": "3.5.19"
+ }
+ },
+ "node_modules/@vue/devtools-api": {
+ "version": "7.7.7",
+ "resolved": "https://registry.npmjs.org/@vue/devtools-api/-/devtools-api-7.7.7.tgz",
+ "integrity": "sha512-lwOnNBH2e7x1fIIbVT7yF5D+YWhqELm55/4ZKf45R9T8r9dE2AIOy8HKjfqzGsoTHFbWbr337O4E0A0QADnjBg==",
+ "license": "MIT",
+ "dependencies": {
+ "@vue/devtools-kit": "^7.7.7"
+ }
+ },
+ "node_modules/@vue/devtools-kit": {
+ "version": "7.7.7",
+ "resolved": "https://registry.npmjs.org/@vue/devtools-kit/-/devtools-kit-7.7.7.tgz",
+ "integrity": "sha512-wgoZtxcTta65cnZ1Q6MbAfePVFxfM+gq0saaeytoph7nEa7yMXoi6sCPy4ufO111B9msnw0VOWjPEFCXuAKRHA==",
+ "license": "MIT",
+ "dependencies": {
+ "@vue/devtools-shared": "^7.7.7",
+ "birpc": "^2.3.0",
+ "hookable": "^5.5.3",
+ "mitt": "^3.0.1",
+ "perfect-debounce": "^1.0.0",
+ "speakingurl": "^14.0.1",
+ "superjson": "^2.2.2"
+ }
+ },
+ "node_modules/@vue/devtools-shared": {
+ "version": "7.7.7",
+ "resolved": "https://registry.npmjs.org/@vue/devtools-shared/-/devtools-shared-7.7.7.tgz",
+ "integrity": "sha512-+udSj47aRl5aKb0memBvcUG9koarqnxNM5yjuREvqwK6T3ap4mn3Zqqc17QrBFTqSMjr3HK1cvStEZpMDpfdyw==",
+ "license": "MIT",
+ "dependencies": {
+ "rfdc": "^1.4.1"
+ }
+ },
+ "node_modules/@vue/reactivity": {
+ "version": "3.5.19",
+ "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.19.tgz",
+ "integrity": "sha512-4bueZg2qs5MSsK2dQk3sssV0cfvxb/QZntTC8v7J448GLgmfPkQ+27aDjlt40+XFqOwUq5yRxK5uQh14Fc9eVA==",
+ "license": "MIT",
+ "dependencies": {
+ "@vue/shared": "3.5.19"
+ }
+ },
+ "node_modules/@vue/runtime-core": {
+ "version": "3.5.19",
+ "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.5.19.tgz",
+ "integrity": "sha512-TaooCr8Hge1sWjLSyhdubnuofs3shhzZGfyD11gFolZrny76drPwBVQj28/z/4+msSFb18tOIg6VVVgf9/IbIA==",
+ "license": "MIT",
+ "dependencies": {
+ "@vue/reactivity": "3.5.19",
+ "@vue/shared": "3.5.19"
+ }
+ },
+ "node_modules/@vue/runtime-dom": {
+ "version": "3.5.19",
+ "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.5.19.tgz",
+ "integrity": "sha512-qmahqeok6ztuUTmV8lqd7N9ymbBzctNF885n8gL3xdCC1u2RnM/coX16Via0AiONQXUoYpxPojL3U1IsDgSWUQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@vue/reactivity": "3.5.19",
+ "@vue/runtime-core": "3.5.19",
+ "@vue/shared": "3.5.19",
+ "csstype": "^3.1.3"
+ }
+ },
+ "node_modules/@vue/server-renderer": {
+ "version": "3.5.19",
+ "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.5.19.tgz",
+ "integrity": "sha512-ZJ/zV9SQuaIO+BEEVq/2a6fipyrSYfjKMU3267bPUk+oTx/hZq3RzV7VCh0Unlppt39Bvh6+NzxeopIFv4HJNg==",
+ "license": "MIT",
+ "dependencies": {
+ "@vue/compiler-ssr": "3.5.19",
+ "@vue/shared": "3.5.19"
+ },
+ "peerDependencies": {
+ "vue": "3.5.19"
+ }
+ },
+ "node_modules/@vue/shared": {
+ "version": "3.5.19",
+ "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.19.tgz",
+ "integrity": "sha512-IhXCOn08wgKrLQxRFKKlSacWg4Goi1BolrdEeLYn6tgHjJNXVrWJ5nzoxZqNwl5p88aLlQ8LOaoMa3AYvaKJ/Q==",
+ "license": "MIT"
+ },
+ "node_modules/@vueuse/core": {
+ "version": "13.7.0",
+ "resolved": "https://registry.npmjs.org/@vueuse/core/-/core-13.7.0.tgz",
+ "integrity": "sha512-myagn09+c6BmS6yHc1gTwwsdZilAovHslMjyykmZH3JNyzI5HoWhv114IIdytXiPipdHJ2gDUx0PB93jRduJYg==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/web-bluetooth": "^0.0.21",
+ "@vueuse/metadata": "13.7.0",
+ "@vueuse/shared": "13.7.0"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/antfu"
+ },
+ "peerDependencies": {
+ "vue": "^3.5.0"
+ }
+ },
+ "node_modules/@vueuse/metadata": {
+ "version": "13.7.0",
+ "resolved": "https://registry.npmjs.org/@vueuse/metadata/-/metadata-13.7.0.tgz",
+ "integrity": "sha512-8okFhS/1ite8EwUdZZfvTYowNTfXmVCOrBFlA31O0HD8HKXhY+WtTRyF0LwbpJfoFPc+s9anNJIXMVrvP7UTZg==",
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/antfu"
+ }
+ },
+ "node_modules/@vueuse/shared": {
+ "version": "13.7.0",
+ "resolved": "https://registry.npmjs.org/@vueuse/shared/-/shared-13.7.0.tgz",
+ "integrity": "sha512-Wi2LpJi4UA9kM0OZ0FCZslACp92HlVNw1KPaDY6RAzvQ+J1s7seOtcOpmkfbD5aBSmMn9NvOakc8ZxMxmDXTIg==",
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/antfu"
+ },
+ "peerDependencies": {
+ "vue": "^3.5.0"
+ }
+ },
+ "node_modules/ansi-regex": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
+ "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/ansi-styles": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
+ "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
+ "license": "MIT",
+ "dependencies": {
+ "color-convert": "^2.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/ansi-styles?sponsor=1"
+ }
+ },
+ "node_modules/any-promise": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz",
+ "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==",
+ "license": "MIT"
+ },
+ "node_modules/anymatch": {
+ "version": "3.1.3",
+ "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz",
+ "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==",
+ "license": "ISC",
+ "dependencies": {
+ "normalize-path": "^3.0.0",
+ "picomatch": "^2.0.4"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/anymatch/node_modules/picomatch": {
+ "version": "2.3.1",
+ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
+ "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=8.6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/jonschlinkert"
+ }
+ },
+ "node_modules/arg": {
+ "version": "5.0.2",
+ "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz",
+ "integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==",
+ "license": "MIT"
+ },
+ "node_modules/asynckit": {
+ "version": "0.4.0",
+ "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
+ "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
+ "license": "MIT"
+ },
+ "node_modules/autoprefixer": {
+ "version": "10.4.21",
+ "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.21.tgz",
+ "integrity": "sha512-O+A6LWV5LDHSJD3LjHYoNi4VLsj/Whi7k6zG12xTYaU4cQ8oxQGckXNX8cRHK5yOZ/ppVHe0ZBXGzSV9jXdVbQ==",
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/postcss/"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/autoprefixer"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "browserslist": "^4.24.4",
+ "caniuse-lite": "^1.0.30001702",
+ "fraction.js": "^4.3.7",
+ "normalize-range": "^0.1.2",
+ "picocolors": "^1.1.1",
+ "postcss-value-parser": "^4.2.0"
+ },
+ "bin": {
+ "autoprefixer": "bin/autoprefixer"
+ },
+ "engines": {
+ "node": "^10 || ^12 || >=14"
+ },
+ "peerDependencies": {
+ "postcss": "^8.1.0"
+ }
+ },
+ "node_modules/axios": {
+ "version": "1.11.0",
+ "resolved": "https://registry.npmjs.org/axios/-/axios-1.11.0.tgz",
+ "integrity": "sha512-1Lx3WLFQWm3ooKDYZD1eXmoGO9fxYQjrycfHFC8P0sCfQVXyROp0p9PFWBehewBOdCwHc+f/b8I0fMto5eSfwA==",
+ "license": "MIT",
+ "dependencies": {
+ "follow-redirects": "^1.15.6",
+ "form-data": "^4.0.4",
+ "proxy-from-env": "^1.1.0"
+ }
+ },
+ "node_modules/balanced-match": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
+ "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
+ "license": "MIT"
+ },
+ "node_modules/binary-extensions": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz",
+ "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/birpc": {
+ "version": "2.5.0",
+ "resolved": "https://registry.npmjs.org/birpc/-/birpc-2.5.0.tgz",
+ "integrity": "sha512-VSWO/W6nNQdyP520F1mhf+Lc2f8pjGQOtoHHm7Ze8Go1kX7akpVIrtTa0fn+HB0QJEDVacl6aO08YE0PgXfdnQ==",
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/antfu"
+ }
+ },
+ "node_modules/brace-expansion": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz",
+ "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==",
+ "license": "MIT",
+ "dependencies": {
+ "balanced-match": "^1.0.0"
+ }
+ },
+ "node_modules/braces": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz",
+ "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==",
+ "license": "MIT",
+ "dependencies": {
+ "fill-range": "^7.1.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/browserslist": {
+ "version": "4.25.3",
+ "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.25.3.tgz",
+ "integrity": "sha512-cDGv1kkDI4/0e5yON9yM5G/0A5u8sf5TnmdX5C9qHzI9PPu++sQ9zjm1k9NiOrf3riY4OkK0zSGqfvJyJsgCBQ==",
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/browserslist"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/browserslist"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "caniuse-lite": "^1.0.30001735",
+ "electron-to-chromium": "^1.5.204",
+ "node-releases": "^2.0.19",
+ "update-browserslist-db": "^1.1.3"
+ },
+ "bin": {
+ "browserslist": "cli.js"
+ },
+ "engines": {
+ "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7"
+ }
+ },
+ "node_modules/call-bind-apply-helpers": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
+ "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0",
+ "function-bind": "^1.1.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/camelcase": {
+ "version": "5.3.1",
+ "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz",
+ "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/camelcase-css": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz",
+ "integrity": "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/caniuse-lite": {
+ "version": "1.0.30001737",
+ "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001737.tgz",
+ "integrity": "sha512-BiloLiXtQNrY5UyF0+1nSJLXUENuhka2pzy2Fx5pGxqavdrxSCW4U6Pn/PoG3Efspi2frRbHpBV2XsrPE6EDlw==",
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/browserslist"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/caniuse-lite"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "CC-BY-4.0"
+ },
+ "node_modules/chokidar": {
+ "version": "3.6.0",
+ "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz",
+ "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==",
+ "license": "MIT",
+ "dependencies": {
+ "anymatch": "~3.1.2",
+ "braces": "~3.0.2",
+ "glob-parent": "~5.1.2",
+ "is-binary-path": "~2.1.0",
+ "is-glob": "~4.0.1",
+ "normalize-path": "~3.0.0",
+ "readdirp": "~3.6.0"
+ },
+ "engines": {
+ "node": ">= 8.10.0"
+ },
+ "funding": {
+ "url": "https://paulmillr.com/funding/"
+ },
+ "optionalDependencies": {
+ "fsevents": "~2.3.2"
+ }
+ },
+ "node_modules/chokidar/node_modules/glob-parent": {
+ "version": "5.1.2",
+ "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
+ "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
+ "license": "ISC",
+ "dependencies": {
+ "is-glob": "^4.0.1"
+ },
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/cliui": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/cliui/-/cliui-6.0.0.tgz",
+ "integrity": "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==",
+ "license": "ISC",
+ "dependencies": {
+ "string-width": "^4.2.0",
+ "strip-ansi": "^6.0.0",
+ "wrap-ansi": "^6.2.0"
+ }
+ },
+ "node_modules/color-convert": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
+ "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
+ "license": "MIT",
+ "dependencies": {
+ "color-name": "~1.1.4"
+ },
+ "engines": {
+ "node": ">=7.0.0"
+ }
+ },
+ "node_modules/color-name": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
+ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
+ "license": "MIT"
+ },
+ "node_modules/combined-stream": {
+ "version": "1.0.8",
+ "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
+ "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
+ "license": "MIT",
+ "dependencies": {
+ "delayed-stream": "~1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/commander": {
+ "version": "4.1.1",
+ "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz",
+ "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/copy-anything": {
+ "version": "3.0.5",
+ "resolved": "https://registry.npmjs.org/copy-anything/-/copy-anything-3.0.5.tgz",
+ "integrity": "sha512-yCEafptTtb4bk7GLEQoM8KVJpxAfdBJYaXyzQEgQQQgYrZiDp8SJmGKlYza6CYjEDNstAdNdKA3UuoULlEbS6w==",
+ "license": "MIT",
+ "dependencies": {
+ "is-what": "^4.1.8"
+ },
+ "engines": {
+ "node": ">=12.13"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/mesqueeb"
+ }
+ },
+ "node_modules/cross-spawn": {
+ "version": "7.0.6",
+ "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
+ "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==",
+ "license": "MIT",
+ "dependencies": {
+ "path-key": "^3.1.0",
+ "shebang-command": "^2.0.0",
+ "which": "^2.0.1"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/cssesc": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz",
+ "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==",
+ "license": "MIT",
+ "bin": {
+ "cssesc": "bin/cssesc"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/csstype": {
+ "version": "3.1.3",
+ "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
+ "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==",
+ "license": "MIT"
+ },
+ "node_modules/decamelize": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz",
+ "integrity": "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/delayed-stream": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
+ "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.4.0"
+ }
+ },
+ "node_modules/detect-libc": {
+ "version": "2.0.4",
+ "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.4.tgz",
+ "integrity": "sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "optional": true,
+ "peer": true,
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/didyoumean": {
+ "version": "1.2.2",
+ "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz",
+ "integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==",
+ "license": "Apache-2.0"
+ },
+ "node_modules/dijkstrajs": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/dijkstrajs/-/dijkstrajs-1.0.3.tgz",
+ "integrity": "sha512-qiSlmBq9+BCdCA/L46dw8Uy93mloxsPSbwnm5yrKn2vMPiy8KyAskTF6zuV/j5BMsmOGZDPs7KjU+mjb670kfA==",
+ "license": "MIT"
+ },
+ "node_modules/dlv": {
+ "version": "1.1.3",
+ "resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz",
+ "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==",
+ "license": "MIT"
+ },
+ "node_modules/dunder-proto": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
+ "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bind-apply-helpers": "^1.0.1",
+ "es-errors": "^1.3.0",
+ "gopd": "^1.2.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/eastasianwidth": {
+ "version": "0.2.0",
+ "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz",
+ "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==",
+ "license": "MIT"
+ },
+ "node_modules/electron-to-chromium": {
+ "version": "1.5.208",
+ "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.208.tgz",
+ "integrity": "sha512-ozZyibehoe7tOhNaf16lKmljVf+3npZcJIEbJRVftVsmAg5TeA1mGS9dVCZzOwr2xT7xK15V0p7+GZqSPgkuPg==",
+ "license": "ISC"
+ },
+ "node_modules/emoji-regex": {
+ "version": "8.0.0",
+ "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
+ "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
+ "license": "MIT"
+ },
+ "node_modules/entities": {
+ "version": "4.5.0",
+ "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz",
+ "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==",
+ "license": "BSD-2-Clause",
+ "engines": {
+ "node": ">=0.12"
+ },
+ "funding": {
+ "url": "https://github.com/fb55/entities?sponsor=1"
+ }
+ },
+ "node_modules/es-define-property": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
+ "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/es-errors": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
+ "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/es-object-atoms": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
+ "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/es-set-tostringtag": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz",
+ "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==",
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0",
+ "get-intrinsic": "^1.2.6",
+ "has-tostringtag": "^1.0.2",
+ "hasown": "^2.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/esbuild": {
+ "version": "0.25.9",
+ "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.9.tgz",
+ "integrity": "sha512-CRbODhYyQx3qp7ZEwzxOk4JBqmD/seJrzPa/cGjY1VtIn5E09Oi9/dB4JwctnfZ8Q8iT7rioVv5k/FNT/uf54g==",
+ "dev": true,
+ "hasInstallScript": true,
+ "license": "MIT",
+ "bin": {
+ "esbuild": "bin/esbuild"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "optionalDependencies": {
+ "@esbuild/aix-ppc64": "0.25.9",
+ "@esbuild/android-arm": "0.25.9",
+ "@esbuild/android-arm64": "0.25.9",
+ "@esbuild/android-x64": "0.25.9",
+ "@esbuild/darwin-arm64": "0.25.9",
+ "@esbuild/darwin-x64": "0.25.9",
+ "@esbuild/freebsd-arm64": "0.25.9",
+ "@esbuild/freebsd-x64": "0.25.9",
+ "@esbuild/linux-arm": "0.25.9",
+ "@esbuild/linux-arm64": "0.25.9",
+ "@esbuild/linux-ia32": "0.25.9",
+ "@esbuild/linux-loong64": "0.25.9",
+ "@esbuild/linux-mips64el": "0.25.9",
+ "@esbuild/linux-ppc64": "0.25.9",
+ "@esbuild/linux-riscv64": "0.25.9",
+ "@esbuild/linux-s390x": "0.25.9",
+ "@esbuild/linux-x64": "0.25.9",
+ "@esbuild/netbsd-arm64": "0.25.9",
+ "@esbuild/netbsd-x64": "0.25.9",
+ "@esbuild/openbsd-arm64": "0.25.9",
+ "@esbuild/openbsd-x64": "0.25.9",
+ "@esbuild/openharmony-arm64": "0.25.9",
+ "@esbuild/sunos-x64": "0.25.9",
+ "@esbuild/win32-arm64": "0.25.9",
+ "@esbuild/win32-ia32": "0.25.9",
+ "@esbuild/win32-x64": "0.25.9"
+ }
+ },
+ "node_modules/escalade": {
+ "version": "3.2.0",
+ "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz",
+ "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/estree-walker": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz",
+ "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==",
+ "license": "MIT"
+ },
+ "node_modules/fast-glob": {
+ "version": "3.3.3",
+ "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz",
+ "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==",
+ "license": "MIT",
+ "dependencies": {
+ "@nodelib/fs.stat": "^2.0.2",
+ "@nodelib/fs.walk": "^1.2.3",
+ "glob-parent": "^5.1.2",
+ "merge2": "^1.3.0",
+ "micromatch": "^4.0.8"
+ },
+ "engines": {
+ "node": ">=8.6.0"
+ }
+ },
+ "node_modules/fast-glob/node_modules/glob-parent": {
+ "version": "5.1.2",
+ "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
+ "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
+ "license": "ISC",
+ "dependencies": {
+ "is-glob": "^4.0.1"
+ },
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/fastq": {
+ "version": "1.19.1",
+ "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.1.tgz",
+ "integrity": "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==",
+ "license": "ISC",
+ "dependencies": {
+ "reusify": "^1.0.4"
+ }
+ },
+ "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/fill-range": {
+ "version": "7.1.1",
+ "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
+ "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==",
+ "license": "MIT",
+ "dependencies": {
+ "to-regex-range": "^5.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/find-up": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz",
+ "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==",
+ "license": "MIT",
+ "dependencies": {
+ "locate-path": "^5.0.0",
+ "path-exists": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/follow-redirects": {
+ "version": "1.15.11",
+ "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz",
+ "integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==",
+ "funding": [
+ {
+ "type": "individual",
+ "url": "https://github.com/sponsors/RubenVerborgh"
+ }
+ ],
+ "license": "MIT",
+ "engines": {
+ "node": ">=4.0"
+ },
+ "peerDependenciesMeta": {
+ "debug": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/foreground-child": {
+ "version": "3.3.1",
+ "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz",
+ "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==",
+ "license": "ISC",
+ "dependencies": {
+ "cross-spawn": "^7.0.6",
+ "signal-exit": "^4.0.1"
+ },
+ "engines": {
+ "node": ">=14"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/form-data": {
+ "version": "4.0.4",
+ "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz",
+ "integrity": "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==",
+ "license": "MIT",
+ "dependencies": {
+ "asynckit": "^0.4.0",
+ "combined-stream": "^1.0.8",
+ "es-set-tostringtag": "^2.1.0",
+ "hasown": "^2.0.2",
+ "mime-types": "^2.1.12"
+ },
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/fraction.js": {
+ "version": "4.3.7",
+ "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.3.7.tgz",
+ "integrity": "sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==",
+ "license": "MIT",
+ "engines": {
+ "node": "*"
+ },
+ "funding": {
+ "type": "patreon",
+ "url": "https://github.com/sponsors/rawify"
+ }
+ },
+ "node_modules/fsevents": {
+ "version": "2.3.3",
+ "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
+ "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
+ "hasInstallScript": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
+ }
+ },
+ "node_modules/function-bind": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
+ "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/get-caller-file": {
+ "version": "2.0.5",
+ "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz",
+ "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==",
+ "license": "ISC",
+ "engines": {
+ "node": "6.* || 8.* || >= 10.*"
+ }
+ },
+ "node_modules/get-intrinsic": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
+ "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bind-apply-helpers": "^1.0.2",
+ "es-define-property": "^1.0.1",
+ "es-errors": "^1.3.0",
+ "es-object-atoms": "^1.1.1",
+ "function-bind": "^1.1.2",
+ "get-proto": "^1.0.1",
+ "gopd": "^1.2.0",
+ "has-symbols": "^1.1.0",
+ "hasown": "^2.0.2",
+ "math-intrinsics": "^1.1.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/get-proto": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
+ "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
+ "license": "MIT",
+ "dependencies": {
+ "dunder-proto": "^1.0.1",
+ "es-object-atoms": "^1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/glob": {
+ "version": "10.4.5",
+ "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz",
+ "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==",
+ "license": "ISC",
+ "dependencies": {
+ "foreground-child": "^3.1.0",
+ "jackspeak": "^3.1.2",
+ "minimatch": "^9.0.4",
+ "minipass": "^7.1.2",
+ "package-json-from-dist": "^1.0.0",
+ "path-scurry": "^1.11.1"
+ },
+ "bin": {
+ "glob": "dist/esm/bin.mjs"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/glob-parent": {
+ "version": "6.0.2",
+ "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz",
+ "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==",
+ "license": "ISC",
+ "dependencies": {
+ "is-glob": "^4.0.3"
+ },
+ "engines": {
+ "node": ">=10.13.0"
+ }
+ },
+ "node_modules/gopd": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
+ "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/has-symbols": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
+ "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/has-tostringtag": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
+ "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
+ "license": "MIT",
+ "dependencies": {
+ "has-symbols": "^1.0.3"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/hasown": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
+ "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
+ "license": "MIT",
+ "dependencies": {
+ "function-bind": "^1.1.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/hookable": {
+ "version": "5.5.3",
+ "resolved": "https://registry.npmjs.org/hookable/-/hookable-5.5.3.tgz",
+ "integrity": "sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ==",
+ "license": "MIT"
+ },
+ "node_modules/is-binary-path": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz",
+ "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==",
+ "license": "MIT",
+ "dependencies": {
+ "binary-extensions": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/is-core-module": {
+ "version": "2.16.1",
+ "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz",
+ "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==",
+ "license": "MIT",
+ "dependencies": {
+ "hasown": "^2.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-extglob": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
+ "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/is-fullwidth-code-point": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
+ "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/is-glob": {
+ "version": "4.0.3",
+ "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
+ "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==",
+ "license": "MIT",
+ "dependencies": {
+ "is-extglob": "^2.1.1"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/is-number": {
+ "version": "7.0.0",
+ "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
+ "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.12.0"
+ }
+ },
+ "node_modules/is-what": {
+ "version": "4.1.16",
+ "resolved": "https://registry.npmjs.org/is-what/-/is-what-4.1.16.tgz",
+ "integrity": "sha512-ZhMwEosbFJkA0YhFnNDgTM4ZxDRsS6HqTo7qsZM08fehyRYIYa0yHu5R6mgo1n/8MgaPBXiPimPD77baVFYg+A==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=12.13"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/mesqueeb"
+ }
+ },
+ "node_modules/isexe": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
+ "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==",
+ "license": "ISC"
+ },
+ "node_modules/jackspeak": {
+ "version": "3.4.3",
+ "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz",
+ "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==",
+ "license": "BlueOak-1.0.0",
+ "dependencies": {
+ "@isaacs/cliui": "^8.0.2"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ },
+ "optionalDependencies": {
+ "@pkgjs/parseargs": "^0.11.0"
+ }
+ },
+ "node_modules/jiti": {
+ "version": "2.5.1",
+ "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.5.1.tgz",
+ "integrity": "sha512-twQoecYPiVA5K/h6SxtORw/Bs3ar+mLUtoPSc7iMXzQzK8d7eJ/R09wmTwAjiamETn1cXYPGfNnu7DMoHgu12w==",
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "peer": true,
+ "bin": {
+ "jiti": "lib/jiti-cli.mjs"
+ }
+ },
+ "node_modules/lightningcss": {
+ "version": "1.30.1",
+ "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.30.1.tgz",
+ "integrity": "sha512-xi6IyHML+c9+Q3W0S4fCQJOym42pyurFiJUHEcEyHS0CeKzia4yZDEsLlqOFykxOdHpNy0NmvVO31vcSqAxJCg==",
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "peer": true,
+ "dependencies": {
+ "detect-libc": "^2.0.3"
+ },
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ },
+ "optionalDependencies": {
+ "lightningcss-darwin-arm64": "1.30.1",
+ "lightningcss-darwin-x64": "1.30.1",
+ "lightningcss-freebsd-x64": "1.30.1",
+ "lightningcss-linux-arm-gnueabihf": "1.30.1",
+ "lightningcss-linux-arm64-gnu": "1.30.1",
+ "lightningcss-linux-arm64-musl": "1.30.1",
+ "lightningcss-linux-x64-gnu": "1.30.1",
+ "lightningcss-linux-x64-musl": "1.30.1",
+ "lightningcss-win32-arm64-msvc": "1.30.1",
+ "lightningcss-win32-x64-msvc": "1.30.1"
+ }
+ },
+ "node_modules/lightningcss-darwin-arm64": {
+ "version": "1.30.1",
+ "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.30.1.tgz",
+ "integrity": "sha512-c8JK7hyE65X1MHMN+Viq9n11RRC7hgin3HhYKhrMyaXflk5GVplZ60IxyoVtzILeKr+xAJwg6zK6sjTBJ0FKYQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "peer": true,
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-darwin-x64": {
+ "version": "1.30.1",
+ "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.30.1.tgz",
+ "integrity": "sha512-k1EvjakfumAQoTfcXUcHQZhSpLlkAuEkdMBsI/ivWw9hL+7FtilQc0Cy3hrx0AAQrVtQAbMI7YjCgYgvn37PzA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "peer": true,
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-freebsd-x64": {
+ "version": "1.30.1",
+ "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.30.1.tgz",
+ "integrity": "sha512-kmW6UGCGg2PcyUE59K5r0kWfKPAVy4SltVeut+umLCFoJ53RdCUWxcRDzO1eTaxf/7Q2H7LTquFHPL5R+Gjyig==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "peer": true,
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-linux-arm-gnueabihf": {
+ "version": "1.30.1",
+ "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.30.1.tgz",
+ "integrity": "sha512-MjxUShl1v8pit+6D/zSPq9S9dQ2NPFSQwGvxBCYaBYLPlCWuPh9/t1MRS8iUaR8i+a6w7aps+B4N0S1TYP/R+Q==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "peer": true,
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-linux-arm64-gnu": {
+ "version": "1.30.1",
+ "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.30.1.tgz",
+ "integrity": "sha512-gB72maP8rmrKsnKYy8XUuXi/4OctJiuQjcuqWNlJQ6jZiWqtPvqFziskH3hnajfvKB27ynbVCucKSm2rkQp4Bw==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "peer": true,
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-linux-arm64-musl": {
+ "version": "1.30.1",
+ "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.30.1.tgz",
+ "integrity": "sha512-jmUQVx4331m6LIX+0wUhBbmMX7TCfjF5FoOH6SD1CttzuYlGNVpA7QnrmLxrsub43ClTINfGSYyHe2HWeLl5CQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "peer": true,
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-linux-x64-gnu": {
+ "version": "1.30.1",
+ "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.30.1.tgz",
+ "integrity": "sha512-piWx3z4wN8J8z3+O5kO74+yr6ze/dKmPnI7vLqfSqI8bccaTGY5xiSGVIJBDd5K5BHlvVLpUB3S2YCfelyJ1bw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "peer": true,
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-linux-x64-musl": {
+ "version": "1.30.1",
+ "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.30.1.tgz",
+ "integrity": "sha512-rRomAK7eIkL+tHY0YPxbc5Dra2gXlI63HL+v1Pdi1a3sC+tJTcFrHX+E86sulgAXeI7rSzDYhPSeHHjqFhqfeQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "peer": true,
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-win32-arm64-msvc": {
+ "version": "1.30.1",
+ "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.30.1.tgz",
+ "integrity": "sha512-mSL4rqPi4iXq5YVqzSsJgMVFENoa4nGTT/GjO2c0Yl9OuQfPsIfncvLrEW6RbbB24WtZ3xP/2CCmI3tNkNV4oA==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "peer": true,
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-win32-x64-msvc": {
+ "version": "1.30.1",
+ "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.30.1.tgz",
+ "integrity": "sha512-PVqXh48wh4T53F/1CCu8PIPCxLzWyCnn/9T5W1Jpmdy5h9Cwd+0YQS6/LwhHXSafuc61/xg9Lv5OrCby6a++jg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "peer": true,
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "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==",
+ "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==",
+ "license": "MIT"
+ },
+ "node_modules/locate-path": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz",
+ "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==",
+ "license": "MIT",
+ "dependencies": {
+ "p-locate": "^4.1.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/lru-cache": {
+ "version": "10.4.3",
+ "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz",
+ "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==",
+ "license": "ISC"
+ },
+ "node_modules/lucide-vue-next": {
+ "version": "0.541.0",
+ "resolved": "https://registry.npmjs.org/lucide-vue-next/-/lucide-vue-next-0.541.0.tgz",
+ "integrity": "sha512-BXY//i7H0ojCDRmux7WzhTl2FiKVmE42fyaLuQOKBGaeBRLEGkkSgYMBxIk9ZjAKa+JELRmFVV1xAFUumB89QA==",
+ "license": "ISC",
+ "peerDependencies": {
+ "vue": ">=3.0.1"
+ }
+ },
+ "node_modules/magic-string": {
+ "version": "0.30.18",
+ "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.18.tgz",
+ "integrity": "sha512-yi8swmWbO17qHhwIBNeeZxTceJMeBvWJaId6dyvTSOwTipqeHhMhOrz6513r1sOKnpvQ7zkhlG8tPrpilwTxHQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/sourcemap-codec": "^1.5.5"
+ }
+ },
+ "node_modules/math-intrinsics": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
+ "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/merge2": {
+ "version": "1.4.1",
+ "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz",
+ "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/micromatch": {
+ "version": "4.0.8",
+ "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz",
+ "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==",
+ "license": "MIT",
+ "dependencies": {
+ "braces": "^3.0.3",
+ "picomatch": "^2.3.1"
+ },
+ "engines": {
+ "node": ">=8.6"
+ }
+ },
+ "node_modules/micromatch/node_modules/picomatch": {
+ "version": "2.3.1",
+ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
+ "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=8.6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/jonschlinkert"
+ }
+ },
+ "node_modules/mime-db": {
+ "version": "1.52.0",
+ "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
+ "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/mime-types": {
+ "version": "2.1.35",
+ "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
+ "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
+ "license": "MIT",
+ "dependencies": {
+ "mime-db": "1.52.0"
+ },
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/mini-svg-data-uri": {
+ "version": "1.4.4",
+ "resolved": "https://registry.npmjs.org/mini-svg-data-uri/-/mini-svg-data-uri-1.4.4.tgz",
+ "integrity": "sha512-r9deDe9p5FJUPZAk3A59wGH7Ii9YrjjWw0jmw/liSbHl2CHiyXj6FcDXDu2K3TjVAXqiJdaw3xxwlZZr9E6nHg==",
+ "license": "MIT",
+ "bin": {
+ "mini-svg-data-uri": "cli.js"
+ }
+ },
+ "node_modules/minimatch": {
+ "version": "9.0.5",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz",
+ "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==",
+ "license": "ISC",
+ "dependencies": {
+ "brace-expansion": "^2.0.1"
+ },
+ "engines": {
+ "node": ">=16 || 14 >=14.17"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/minipass": {
+ "version": "7.1.2",
+ "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz",
+ "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==",
+ "license": "ISC",
+ "engines": {
+ "node": ">=16 || 14 >=14.17"
+ }
+ },
+ "node_modules/mitt": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/mitt/-/mitt-3.0.1.tgz",
+ "integrity": "sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==",
+ "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==",
+ "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==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "bin": {
+ "nanoid": "bin/nanoid.cjs"
+ },
+ "engines": {
+ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
+ }
+ },
+ "node_modules/node-releases": {
+ "version": "2.0.19",
+ "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.19.tgz",
+ "integrity": "sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==",
+ "license": "MIT"
+ },
+ "node_modules/normalize-path": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
+ "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/normalize-range": {
+ "version": "0.1.2",
+ "resolved": "https://registry.npmjs.org/normalize-range/-/normalize-range-0.1.2.tgz",
+ "integrity": "sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/object-assign": {
+ "version": "4.1.1",
+ "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
+ "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/object-hash": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz",
+ "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/p-limit": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz",
+ "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==",
+ "license": "MIT",
+ "dependencies": {
+ "p-try": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/p-locate": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz",
+ "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==",
+ "license": "MIT",
+ "dependencies": {
+ "p-limit": "^2.2.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/p-try": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz",
+ "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/package-json-from-dist": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz",
+ "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==",
+ "license": "BlueOak-1.0.0"
+ },
+ "node_modules/path-exists": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
+ "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/path-key": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
+ "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/path-parse": {
+ "version": "1.0.7",
+ "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz",
+ "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==",
+ "license": "MIT"
+ },
+ "node_modules/path-scurry": {
+ "version": "1.11.1",
+ "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz",
+ "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==",
+ "license": "BlueOak-1.0.0",
+ "dependencies": {
+ "lru-cache": "^10.2.0",
+ "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0"
+ },
+ "engines": {
+ "node": ">=16 || 14 >=14.18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/perfect-debounce": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/perfect-debounce/-/perfect-debounce-1.0.0.tgz",
+ "integrity": "sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA==",
+ "license": "MIT"
+ },
+ "node_modules/picocolors": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
+ "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
+ "license": "ISC"
+ },
+ "node_modules/picomatch": {
+ "version": "4.0.3",
+ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
+ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/jonschlinkert"
+ }
+ },
+ "node_modules/pify": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz",
+ "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/pinia": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/pinia/-/pinia-3.0.3.tgz",
+ "integrity": "sha512-ttXO/InUULUXkMHpTdp9Fj4hLpD/2AoJdmAbAeW2yu1iy1k+pkFekQXw5VpC0/5p51IOR/jDaDRfRWRnMMsGOA==",
+ "license": "MIT",
+ "dependencies": {
+ "@vue/devtools-api": "^7.7.2"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/posva"
+ },
+ "peerDependencies": {
+ "typescript": ">=4.4.4",
+ "vue": "^2.7.0 || ^3.5.11"
+ },
+ "peerDependenciesMeta": {
+ "typescript": {
+ "optional": true
+ }
+ }
+ },
+ "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==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/pngjs": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/pngjs/-/pngjs-5.0.0.tgz",
+ "integrity": "sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=10.13.0"
+ }
+ },
+ "node_modules/postcss": {
+ "version": "8.5.6",
+ "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz",
+ "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==",
+ "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-import": {
+ "version": "15.1.0",
+ "resolved": "https://registry.npmjs.org/postcss-import/-/postcss-import-15.1.0.tgz",
+ "integrity": "sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==",
+ "license": "MIT",
+ "dependencies": {
+ "postcss-value-parser": "^4.0.0",
+ "read-cache": "^1.0.0",
+ "resolve": "^1.1.7"
+ },
+ "engines": {
+ "node": ">=14.0.0"
+ },
+ "peerDependencies": {
+ "postcss": "^8.0.0"
+ }
+ },
+ "node_modules/postcss-js": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/postcss-js/-/postcss-js-4.0.1.tgz",
+ "integrity": "sha512-dDLF8pEO191hJMtlHFPRa8xsizHaM82MLfNkUHdUtVEV3tgTp5oj+8qbEqYM57SLfc74KSbw//4SeJma2LRVIw==",
+ "license": "MIT",
+ "dependencies": {
+ "camelcase-css": "^2.0.1"
+ },
+ "engines": {
+ "node": "^12 || ^14 || >= 16"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/postcss/"
+ },
+ "peerDependencies": {
+ "postcss": "^8.4.21"
+ }
+ },
+ "node_modules/postcss-load-config": {
+ "version": "4.0.2",
+ "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-4.0.2.tgz",
+ "integrity": "sha512-bSVhyJGL00wMVoPUzAVAnbEoWyqRxkjv64tUl427SKnPrENtq6hJwUojroMz2VB+Q1edmi4IfrAPpami5VVgMQ==",
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/postcss/"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "lilconfig": "^3.0.0",
+ "yaml": "^2.3.4"
+ },
+ "engines": {
+ "node": ">= 14"
+ },
+ "peerDependencies": {
+ "postcss": ">=8.0.9",
+ "ts-node": ">=9.0.0"
+ },
+ "peerDependenciesMeta": {
+ "postcss": {
+ "optional": true
+ },
+ "ts-node": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/postcss-nested": {
+ "version": "6.2.0",
+ "resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-6.2.0.tgz",
+ "integrity": "sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ==",
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/postcss/"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "postcss-selector-parser": "^6.1.1"
+ },
+ "engines": {
+ "node": ">=12.0"
+ },
+ "peerDependencies": {
+ "postcss": "^8.2.14"
+ }
+ },
+ "node_modules/postcss-selector-parser": {
+ "version": "6.1.2",
+ "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz",
+ "integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==",
+ "license": "MIT",
+ "dependencies": {
+ "cssesc": "^3.0.0",
+ "util-deprecate": "^1.0.2"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/postcss-value-parser": {
+ "version": "4.2.0",
+ "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz",
+ "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==",
+ "license": "MIT"
+ },
+ "node_modules/proxy-from-env": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
+ "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==",
+ "license": "MIT"
+ },
+ "node_modules/qrcode": {
+ "version": "1.5.4",
+ "resolved": "https://registry.npmjs.org/qrcode/-/qrcode-1.5.4.tgz",
+ "integrity": "sha512-1ca71Zgiu6ORjHqFBDpnSMTR2ReToX4l1Au1VFLyVeBTFavzQnv5JxMFr3ukHVKpSrSA2MCk0lNJSykjUfz7Zg==",
+ "license": "MIT",
+ "dependencies": {
+ "dijkstrajs": "^1.0.1",
+ "pngjs": "^5.0.0",
+ "yargs": "^15.3.1"
+ },
+ "bin": {
+ "qrcode": "bin/qrcode"
+ },
+ "engines": {
+ "node": ">=10.13.0"
+ }
+ },
+ "node_modules/qrcode.js": {
+ "version": "0.0.1",
+ "resolved": "https://registry.npmjs.org/qrcode.js/-/qrcode.js-0.0.1.tgz",
+ "integrity": "sha512-EpHuKYzjYH/+SQtAlo4NYe3wpsYzKadKa154Ch163K+jrUlz6pVInWgJlsj5d1dxngYEjuqnClfzutL8Z9rqcg==",
+ "license": "Apache"
+ },
+ "node_modules/queue-microtask": {
+ "version": "1.2.3",
+ "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz",
+ "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ],
+ "license": "MIT"
+ },
+ "node_modules/read-cache": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz",
+ "integrity": "sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==",
+ "license": "MIT",
+ "dependencies": {
+ "pify": "^2.3.0"
+ }
+ },
+ "node_modules/readdirp": {
+ "version": "3.6.0",
+ "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz",
+ "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==",
+ "license": "MIT",
+ "dependencies": {
+ "picomatch": "^2.2.1"
+ },
+ "engines": {
+ "node": ">=8.10.0"
+ }
+ },
+ "node_modules/readdirp/node_modules/picomatch": {
+ "version": "2.3.1",
+ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
+ "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=8.6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/jonschlinkert"
+ }
+ },
+ "node_modules/require-directory": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",
+ "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/require-main-filename": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz",
+ "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==",
+ "license": "ISC"
+ },
+ "node_modules/resolve": {
+ "version": "1.22.10",
+ "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz",
+ "integrity": "sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==",
+ "license": "MIT",
+ "dependencies": {
+ "is-core-module": "^2.16.0",
+ "path-parse": "^1.0.7",
+ "supports-preserve-symlinks-flag": "^1.0.0"
+ },
+ "bin": {
+ "resolve": "bin/resolve"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/reusify": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz",
+ "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==",
+ "license": "MIT",
+ "engines": {
+ "iojs": ">=1.0.0",
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/rfdc": {
+ "version": "1.4.1",
+ "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.4.1.tgz",
+ "integrity": "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==",
+ "license": "MIT"
+ },
+ "node_modules/rollup": {
+ "version": "4.47.1",
+ "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.47.1.tgz",
+ "integrity": "sha512-iasGAQoZ5dWDzULEUX3jiW0oB1qyFOepSyDyoU6S/OhVlDIwj5knI5QBa5RRQ0sK7OE0v+8VIi2JuV+G+3tfNg==",
+ "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.47.1",
+ "@rollup/rollup-android-arm64": "4.47.1",
+ "@rollup/rollup-darwin-arm64": "4.47.1",
+ "@rollup/rollup-darwin-x64": "4.47.1",
+ "@rollup/rollup-freebsd-arm64": "4.47.1",
+ "@rollup/rollup-freebsd-x64": "4.47.1",
+ "@rollup/rollup-linux-arm-gnueabihf": "4.47.1",
+ "@rollup/rollup-linux-arm-musleabihf": "4.47.1",
+ "@rollup/rollup-linux-arm64-gnu": "4.47.1",
+ "@rollup/rollup-linux-arm64-musl": "4.47.1",
+ "@rollup/rollup-linux-loongarch64-gnu": "4.47.1",
+ "@rollup/rollup-linux-ppc64-gnu": "4.47.1",
+ "@rollup/rollup-linux-riscv64-gnu": "4.47.1",
+ "@rollup/rollup-linux-riscv64-musl": "4.47.1",
+ "@rollup/rollup-linux-s390x-gnu": "4.47.1",
+ "@rollup/rollup-linux-x64-gnu": "4.47.1",
+ "@rollup/rollup-linux-x64-musl": "4.47.1",
+ "@rollup/rollup-win32-arm64-msvc": "4.47.1",
+ "@rollup/rollup-win32-ia32-msvc": "4.47.1",
+ "@rollup/rollup-win32-x64-msvc": "4.47.1",
+ "fsevents": "~2.3.2"
+ }
+ },
+ "node_modules/run-parallel": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz",
+ "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "queue-microtask": "^1.2.2"
+ }
+ },
+ "node_modules/set-blocking": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz",
+ "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==",
+ "license": "ISC"
+ },
+ "node_modules/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==",
+ "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==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "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==",
+ "license": "ISC",
+ "engines": {
+ "node": ">=14"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "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==",
+ "license": "BSD-3-Clause",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/speakingurl": {
+ "version": "14.0.1",
+ "resolved": "https://registry.npmjs.org/speakingurl/-/speakingurl-14.0.1.tgz",
+ "integrity": "sha512-1POYv7uv2gXoyGFpBCmpDVSNV74IfsWlDW216UPjbWufNf+bSU6GdbDsxdcxtfwb4xlI3yxzOTKClUosxARYrQ==",
+ "license": "BSD-3-Clause",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/string-width": {
+ "version": "4.2.3",
+ "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
+ "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
+ "license": "MIT",
+ "dependencies": {
+ "emoji-regex": "^8.0.0",
+ "is-fullwidth-code-point": "^3.0.0",
+ "strip-ansi": "^6.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/string-width-cjs": {
+ "name": "string-width",
+ "version": "4.2.3",
+ "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
+ "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
+ "license": "MIT",
+ "dependencies": {
+ "emoji-regex": "^8.0.0",
+ "is-fullwidth-code-point": "^3.0.0",
+ "strip-ansi": "^6.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/strip-ansi": {
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
+ "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
+ "license": "MIT",
+ "dependencies": {
+ "ansi-regex": "^5.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/strip-ansi-cjs": {
+ "name": "strip-ansi",
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
+ "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
+ "license": "MIT",
+ "dependencies": {
+ "ansi-regex": "^5.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/sucrase": {
+ "version": "3.35.0",
+ "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.0.tgz",
+ "integrity": "sha512-8EbVDiu9iN/nESwxeSxDKe0dunta1GOlHufmSSXxMD2z2/tMZpDMpvXQGsc+ajGo8y2uYUmixaSRUc/QPoQ0GA==",
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/gen-mapping": "^0.3.2",
+ "commander": "^4.0.0",
+ "glob": "^10.3.10",
+ "lines-and-columns": "^1.1.6",
+ "mz": "^2.7.0",
+ "pirates": "^4.0.1",
+ "ts-interface-checker": "^0.1.9"
+ },
+ "bin": {
+ "sucrase": "bin/sucrase",
+ "sucrase-node": "bin/sucrase-node"
+ },
+ "engines": {
+ "node": ">=16 || 14 >=14.17"
+ }
+ },
+ "node_modules/superjson": {
+ "version": "2.2.2",
+ "resolved": "https://registry.npmjs.org/superjson/-/superjson-2.2.2.tgz",
+ "integrity": "sha512-5JRxVqC8I8NuOUjzBbvVJAKNM8qoVuH0O77h4WInc/qC2q5IreqKxYwgkga3PfA22OayK2ikceb/B26dztPl+Q==",
+ "license": "MIT",
+ "dependencies": {
+ "copy-anything": "^3.0.2"
+ },
+ "engines": {
+ "node": ">=16"
+ }
+ },
+ "node_modules/supports-preserve-symlinks-flag": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz",
+ "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/tailwindcss": {
+ "version": "3.4.17",
+ "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.17.tgz",
+ "integrity": "sha512-w33E2aCvSDP0tW9RZuNXadXlkHXqFzSkQew/aIa2i/Sj8fThxwovwlXHSPXTbAHwEIhBFXAedUhP2tueAKP8Og==",
+ "license": "MIT",
+ "dependencies": {
+ "@alloc/quick-lru": "^5.2.0",
+ "arg": "^5.0.2",
+ "chokidar": "^3.6.0",
+ "didyoumean": "^1.2.2",
+ "dlv": "^1.1.3",
+ "fast-glob": "^3.3.2",
+ "glob-parent": "^6.0.2",
+ "is-glob": "^4.0.3",
+ "jiti": "^1.21.6",
+ "lilconfig": "^3.1.3",
+ "micromatch": "^4.0.8",
+ "normalize-path": "^3.0.0",
+ "object-hash": "^3.0.0",
+ "picocolors": "^1.1.1",
+ "postcss": "^8.4.47",
+ "postcss-import": "^15.1.0",
+ "postcss-js": "^4.0.1",
+ "postcss-load-config": "^4.0.2",
+ "postcss-nested": "^6.2.0",
+ "postcss-selector-parser": "^6.1.2",
+ "resolve": "^1.22.8",
+ "sucrase": "^3.35.0"
+ },
+ "bin": {
+ "tailwind": "lib/cli.js",
+ "tailwindcss": "lib/cli.js"
+ },
+ "engines": {
+ "node": ">=14.0.0"
+ }
+ },
+ "node_modules/tailwindcss/node_modules/jiti": {
+ "version": "1.21.7",
+ "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.7.tgz",
+ "integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==",
+ "license": "MIT",
+ "bin": {
+ "jiti": "bin/jiti.js"
+ }
+ },
+ "node_modules/thenify": {
+ "version": "3.3.1",
+ "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz",
+ "integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==",
+ "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==",
+ "license": "MIT",
+ "dependencies": {
+ "thenify": ">= 3.1.0 < 4"
+ },
+ "engines": {
+ "node": ">=0.8"
+ }
+ },
+ "node_modules/tinyglobby": {
+ "version": "0.2.14",
+ "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.14.tgz",
+ "integrity": "sha512-tX5e7OM1HnYr2+a2C/4V0htOcSQcoSTH9KgJnVvNm5zm/cyEWKJ7j7YutsH9CxMdtOkkLFy2AHrMci9IM8IPZQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "fdir": "^6.4.4",
+ "picomatch": "^4.0.2"
+ },
+ "engines": {
+ "node": ">=12.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/SuperchupuDev"
+ }
+ },
+ "node_modules/to-regex-range": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
+ "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
+ "license": "MIT",
+ "dependencies": {
+ "is-number": "^7.0.0"
+ },
+ "engines": {
+ "node": ">=8.0"
+ }
+ },
+ "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==",
+ "license": "Apache-2.0"
+ },
+ "node_modules/update-browserslist-db": {
+ "version": "1.1.3",
+ "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.3.tgz",
+ "integrity": "sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw==",
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/browserslist"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/browserslist"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "escalade": "^3.2.0",
+ "picocolors": "^1.1.1"
+ },
+ "bin": {
+ "update-browserslist-db": "cli.js"
+ },
+ "peerDependencies": {
+ "browserslist": ">= 4.21.0"
+ }
+ },
+ "node_modules/util-deprecate": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
+ "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
+ "license": "MIT"
+ },
+ "node_modules/vite": {
+ "version": "7.1.3",
+ "resolved": "https://registry.npmjs.org/vite/-/vite-7.1.3.tgz",
+ "integrity": "sha512-OOUi5zjkDxYrKhTV3V7iKsoS37VUM7v40+HuwEmcrsf11Cdx9y3DIr2Px6liIcZFwt3XSRpQvFpL3WVy7ApkGw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "esbuild": "^0.25.0",
+ "fdir": "^6.5.0",
+ "picomatch": "^4.0.3",
+ "postcss": "^8.5.6",
+ "rollup": "^4.43.0",
+ "tinyglobby": "^0.2.14"
+ },
+ "bin": {
+ "vite": "bin/vite.js"
+ },
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ },
+ "funding": {
+ "url": "https://github.com/vitejs/vite?sponsor=1"
+ },
+ "optionalDependencies": {
+ "fsevents": "~2.3.3"
+ },
+ "peerDependencies": {
+ "@types/node": "^20.19.0 || >=22.12.0",
+ "jiti": ">=1.21.0",
+ "less": "^4.0.0",
+ "lightningcss": "^1.21.0",
+ "sass": "^1.70.0",
+ "sass-embedded": "^1.70.0",
+ "stylus": ">=0.54.8",
+ "sugarss": "^5.0.0",
+ "terser": "^5.16.0",
+ "tsx": "^4.8.1",
+ "yaml": "^2.4.2"
+ },
+ "peerDependenciesMeta": {
+ "@types/node": {
+ "optional": true
+ },
+ "jiti": {
+ "optional": true
+ },
+ "less": {
+ "optional": true
+ },
+ "lightningcss": {
+ "optional": true
+ },
+ "sass": {
+ "optional": true
+ },
+ "sass-embedded": {
+ "optional": true
+ },
+ "stylus": {
+ "optional": true
+ },
+ "sugarss": {
+ "optional": true
+ },
+ "terser": {
+ "optional": true
+ },
+ "tsx": {
+ "optional": true
+ },
+ "yaml": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/vue": {
+ "version": "3.5.19",
+ "resolved": "https://registry.npmjs.org/vue/-/vue-3.5.19.tgz",
+ "integrity": "sha512-ZRh0HTmw6KChRYWgN8Ox/wi7VhpuGlvMPrHjIsdRbzKNgECFLzy+dKL5z9yGaBSjCpmcfJCbh3I1tNSRmBz2tg==",
+ "license": "MIT",
+ "dependencies": {
+ "@vue/compiler-dom": "3.5.19",
+ "@vue/compiler-sfc": "3.5.19",
+ "@vue/runtime-dom": "3.5.19",
+ "@vue/server-renderer": "3.5.19",
+ "@vue/shared": "3.5.19"
+ },
+ "peerDependencies": {
+ "typescript": "*"
+ },
+ "peerDependenciesMeta": {
+ "typescript": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/vue-router": {
+ "version": "4.5.1",
+ "resolved": "https://registry.npmjs.org/vue-router/-/vue-router-4.5.1.tgz",
+ "integrity": "sha512-ogAF3P97NPm8fJsE4by9dwSYtDwXIY1nFY9T6DyQnGHd1E2Da94w9JIolpe42LJGIl0DwOHBi8TcRPlPGwbTtw==",
+ "license": "MIT",
+ "dependencies": {
+ "@vue/devtools-api": "^6.6.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/posva"
+ },
+ "peerDependencies": {
+ "vue": "^3.2.0"
+ }
+ },
+ "node_modules/vue-router/node_modules/@vue/devtools-api": {
+ "version": "6.6.4",
+ "resolved": "https://registry.npmjs.org/@vue/devtools-api/-/devtools-api-6.6.4.tgz",
+ "integrity": "sha512-sGhTPMuXqZ1rVOk32RylztWkfXTRhuS7vgAKv0zjqk8gbsHkJ7xfFf+jbySxt7tWObEJwyKaHMikV/WGDiQm8g==",
+ "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==",
+ "license": "ISC",
+ "dependencies": {
+ "isexe": "^2.0.0"
+ },
+ "bin": {
+ "node-which": "bin/node-which"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/which-module": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.1.tgz",
+ "integrity": "sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ==",
+ "license": "ISC"
+ },
+ "node_modules/wrap-ansi": {
+ "version": "6.2.0",
+ "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz",
+ "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==",
+ "license": "MIT",
+ "dependencies": {
+ "ansi-styles": "^4.0.0",
+ "string-width": "^4.1.0",
+ "strip-ansi": "^6.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/wrap-ansi-cjs": {
+ "name": "wrap-ansi",
+ "version": "7.0.0",
+ "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
+ "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==",
+ "license": "MIT",
+ "dependencies": {
+ "ansi-styles": "^4.0.0",
+ "string-width": "^4.1.0",
+ "strip-ansi": "^6.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/wrap-ansi?sponsor=1"
+ }
+ },
+ "node_modules/y18n": {
+ "version": "4.0.3",
+ "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.3.tgz",
+ "integrity": "sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==",
+ "license": "ISC"
+ },
+ "node_modules/yaml": {
+ "version": "2.8.1",
+ "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.1.tgz",
+ "integrity": "sha512-lcYcMxX2PO9XMGvAJkJ3OsNMw+/7FKes7/hgerGUYWIoWu5j/+YQqcZr5JnPZWzOsEBgMbSbiSTn/dv/69Mkpw==",
+ "license": "ISC",
+ "bin": {
+ "yaml": "bin.mjs"
+ },
+ "engines": {
+ "node": ">= 14.6"
+ }
+ },
+ "node_modules/yargs": {
+ "version": "15.4.1",
+ "resolved": "https://registry.npmjs.org/yargs/-/yargs-15.4.1.tgz",
+ "integrity": "sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==",
+ "license": "MIT",
+ "dependencies": {
+ "cliui": "^6.0.0",
+ "decamelize": "^1.2.0",
+ "find-up": "^4.1.0",
+ "get-caller-file": "^2.0.1",
+ "require-directory": "^2.1.1",
+ "require-main-filename": "^2.0.0",
+ "set-blocking": "^2.0.0",
+ "string-width": "^4.2.0",
+ "which-module": "^2.0.0",
+ "y18n": "^4.0.0",
+ "yargs-parser": "^18.1.2"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/yargs-parser": {
+ "version": "18.1.3",
+ "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-18.1.3.tgz",
+ "integrity": "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==",
+ "license": "ISC",
+ "dependencies": {
+ "camelcase": "^5.0.0",
+ "decamelize": "^1.2.0"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ }
+ }
+ }
frontend/package.json +29 -0
@@ @@ -0,0 +1,29 @@
+ {
+ "name": "frontend",
+ "private": true,
+ "version": "0.0.0",
+ "type": "module",
+ "scripts": {
+ "dev": "vite",
+ "build": "vite build",
+ "preview": "vite preview"
+ },
+ "dependencies": {
+ "@tailwindcss/forms": "^0.5.10",
+ "@vueuse/core": "^13.7.0",
+ "autoprefixer": "^10.4.21",
+ "axios": "^1.11.0",
+ "lucide-vue-next": "^0.541.0",
+ "pinia": "^3.0.3",
+ "postcss": "^8.5.6",
+ "qrcode": "^1.5.4",
+ "qrcode.js": "^0.0.1",
+ "tailwindcss": "^3.4.17",
+ "vue": "^3.5.18",
+ "vue-router": "^4.5.1"
+ },
+ "devDependencies": {
+ "@vitejs/plugin-vue": "^6.0.1",
+ "vite": "^7.1.2"
+ }
+ }
frontend/postcss.config.js +6 -0
@@ @@ -0,0 +1,6 @@
+ export default {
+ plugins: {
+ tailwindcss: {},
+ autoprefixer: {},
+ },
+ }
\ No newline at end of file
frontend/public/vite.svg +1 -0
@@ @@ -0,0 +1 @@
+ <svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>
\ No newline at end of file
frontend/src/App.vue +19 -0
@@ @@ -0,0 +1,19 @@
+ <template>
+ <div id="app" class="min-h-screen bg-gray-50">
+ <router-view />
+ </div>
+ </template>
+
+ <script setup>
+ import { onMounted } from 'vue'
+ import { useAuthStore } from './stores/auth'
+
+ const authStore = useAuthStore()
+
+ onMounted(() => {
+ // Check if user is authenticated on app load
+ if (authStore.isAuthenticated) {
+ console.log('User authenticated:', authStore.user)
+ }
+ })
+ </script>
\ No newline at end of file
frontend/src/components/QRCodeModal.vue +109 -0
@@ @@ -0,0 +1,109 @@
+ <template>
+ <div class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center p-4 z-50" @click.self="$emit('close')">
+ <div class="bg-white rounded-2xl shadow-xl max-w-md w-full p-6">
+ <div class="flex justify-between items-start mb-4">
+ <h3 class="text-xl font-semibold text-gray-900">QR Code</h3>
+ <button
+ @click="$emit('close')"
+ class="text-gray-400 hover:text-gray-600 transition"
+ >
+ <XIcon class="h-6 w-6" />
+ </button>
+ </div>
+
+ <div class="space-y-4">
+ <div class="bg-gray-50 rounded-lg p-4">
+ <p class="text-sm text-gray-600 mb-2">{{ entry.name }}</p>
+ <p class="text-xs text-gray-500">{{ shortUrl }}</p>
+ </div>
+
+ <div class="flex justify-center">
+ <div v-if="loading" class="py-8">
+ <Loader2Icon class="h-8 w-8 text-gray-400 animate-spin" />
+ </div>
+ <img
+ v-else
+ :src="qrCodeUrl"
+ alt="QR Code"
+ class="w-64 h-64"
+ />
+ </div>
+
+ <div class="flex space-x-3">
+ <button
+ @click="downloadQR"
+ class="flex-1 bg-indigo-600 text-white py-2 px-4 rounded-lg font-semibold hover:bg-indigo-700 transition flex items-center justify-center"
+ >
+ <DownloadIcon class="h-4 w-4 mr-2" />
+ Download
+ </button>
+ <button
+ @click="copyQR"
+ class="flex-1 bg-gray-100 text-gray-700 py-2 px-4 rounded-lg font-semibold hover:bg-gray-200 transition flex items-center justify-center"
+ >
+ <CopyIcon class="h-4 w-4 mr-2" />
+ Copy
+ </button>
+ </div>
+ </div>
+ </div>
+ </div>
+ </template>
+
+ <script setup>
+ import { ref, computed, onMounted } from 'vue'
+ import { X as XIcon, Loader2 as Loader2Icon, Download as DownloadIcon, Copy as CopyIcon } from 'lucide-vue-next'
+ import { generateQRCodeWithLogo } from '../services/qrcode'
+
+ const props = defineProps({
+ entry: {
+ type: Object,
+ required: true
+ }
+ })
+
+ const emit = defineEmits(['close'])
+
+ const loading = ref(true)
+ const qrCodeUrl = ref('')
+
+ const shortUrl = computed(() => {
+ return `${import.meta.env.VITE_SHORT_URL || 'http://localhost:8787'}/${props.entry.slug}`
+ })
+
+ onMounted(async () => {
+ try {
+ qrCodeUrl.value = await generateQRCodeWithLogo(shortUrl.value, props.entry.logo_url)
+ } catch (error) {
+ console.error('Failed to generate QR code:', error)
+ // Try without logo as fallback
+ const { default: QRCode } = await import('qrcode')
+ qrCodeUrl.value = await QRCode.toDataURL(shortUrl.value, {
+ errorCorrectionLevel: 'H',
+ width: 512,
+ margin: 2
+ })
+ } finally {
+ loading.value = false
+ }
+ })
+
+ function downloadQR() {
+ const link = document.createElement('a')
+ link.download = `qr-${props.entry.slug}.png`
+ link.href = qrCodeUrl.value
+ link.click()
+ }
+
+ async function copyQR() {
+ try {
+ const blob = await fetch(qrCodeUrl.value).then(r => r.blob())
+ await navigator.clipboard.write([
+ new ClipboardItem({ 'image/png': blob })
+ ])
+ alert('QR code copied to clipboard!')
+ } catch (error) {
+ alert('Failed to copy QR code')
+ }
+ }
+ </script>
\ No newline at end of file
frontend/src/main.js +12 -0
@@ @@ -0,0 +1,12 @@
+ import { createApp } from 'vue'
+ import { createPinia } from 'pinia'
+ import router from './router'
+ import App from './App.vue'
+ import './style.css'
+
+ const app = createApp(App)
+
+ app.use(createPinia())
+ app.use(router)
+
+ app.mount('#app')
\ No newline at end of file
frontend/src/router/index.js +53 -0
@@ @@ -0,0 +1,53 @@
+ import { createRouter, createWebHistory } from 'vue-router'
+ import { useAuthStore } from '../stores/auth'
+
+ const router = createRouter({
+ history: createWebHistory(),
+ routes: [
+ {
+ path: '/',
+ name: 'home',
+ component: () => import('../views/Home.vue'),
+ meta: { requiresAuth: false }
+ },
+ {
+ path: '/login',
+ name: 'login',
+ component: () => import('../views/Login.vue'),
+ meta: { requiresAuth: false }
+ },
+ {
+ path: '/auth/verify',
+ name: 'verify',
+ component: () => import('../views/Verify.vue'),
+ meta: { requiresAuth: false }
+ },
+ {
+ path: '/dashboard',
+ name: 'dashboard',
+ component: () => import('../views/Dashboard.vue'),
+ meta: { requiresAuth: true }
+ },
+ {
+ path: '/analytics/:id',
+ name: 'analytics',
+ component: () => import('../views/Analytics.vue'),
+ meta: { requiresAuth: true }
+ }
+ ]
+ })
+
+ // Navigation guard for authentication
+ router.beforeEach((to, from, next) => {
+ const authStore = useAuthStore()
+
+ if (to.meta.requiresAuth && !authStore.isAuthenticated) {
+ next('/login')
+ } else if (to.path === '/login' && authStore.isAuthenticated) {
+ next('/dashboard')
+ } else {
+ next()
+ }
+ })
+
+ export default router
\ No newline at end of file
frontend/src/services/api.js +24 -0
@@ @@ -0,0 +1,24 @@
+ import axios from 'axios'
+
+ const api = axios.create({
+ baseURL: import.meta.env.VITE_API_URL || 'http://localhost:8787/api',
+ headers: {
+ 'Content-Type': 'application/json'
+ }
+ })
+
+ // Request interceptor for error handling
+ api.interceptors.response.use(
+ response => response,
+ error => {
+ if (error.response?.status === 401) {
+ // Clear auth and redirect to login
+ localStorage.removeItem('token')
+ localStorage.removeItem('user')
+ window.location.href = '/login'
+ }
+ return Promise.reject(error)
+ }
+ )
+
+ export default api
\ No newline at end of file
frontend/src/services/qrcode.js +81 -0
@@ @@ -0,0 +1,81 @@
+ import QRCode from 'qrcode'
+
+ export async function generateQRCode(text, options = {}) {
+ const defaultOptions = {
+ errorCorrectionLevel: 'H', // High - allows 30% damage for logo overlay
+ type: 'image/png',
+ quality: 0.92,
+ margin: 2,
+ color: {
+ dark: '#000000',
+ light: '#FFFFFF'
+ },
+ width: 512
+ }
+
+ const qrOptions = { ...defaultOptions, ...options }
+
+ try {
+ const dataUrl = await QRCode.toDataURL(text, qrOptions)
+ return dataUrl
+ } catch (error) {
+ console.error('QR Code generation error:', error)
+ throw error
+ }
+ }
+
+ export async function generateQRCodeWithLogo(text, logoUrl, options = {}) {
+ const canvas = document.createElement('canvas')
+ const ctx = canvas.getContext('2d')
+
+ canvas.width = options.width || 512
+ canvas.height = options.height || 512
+
+ // Generate QR code
+ const qrDataUrl = await generateQRCode(text, {
+ ...options,
+ errorCorrectionLevel: 'H'
+ })
+
+ // Draw QR code
+ const qrImage = new Image()
+ qrImage.src = qrDataUrl
+
+ return new Promise((resolve, reject) => {
+ qrImage.onload = () => {
+ ctx.drawImage(qrImage, 0, 0, canvas.width, canvas.height)
+
+ if (logoUrl) {
+ // Add logo
+ const logo = new Image()
+ logo.crossOrigin = 'anonymous'
+ logo.src = logoUrl
+
+ logo.onload = () => {
+ // Calculate logo size (max 30% of QR code)
+ const logoSize = Math.floor(canvas.width * 0.3)
+ const logoX = (canvas.width - logoSize) / 2
+ const logoY = (canvas.height - logoSize) / 2
+
+ // Draw white background for logo
+ ctx.fillStyle = 'white'
+ ctx.fillRect(logoX - 10, logoY - 10, logoSize + 20, logoSize + 20)
+
+ // Draw logo
+ ctx.drawImage(logo, logoX, logoY, logoSize, logoSize)
+
+ resolve(canvas.toDataURL())
+ }
+
+ logo.onerror = () => {
+ // If logo fails to load, return QR without logo
+ resolve(qrDataUrl)
+ }
+ } else {
+ resolve(qrDataUrl)
+ }
+ }
+
+ qrImage.onerror = reject
+ })
+ }
\ No newline at end of file
frontend/src/stores/auth.js +55 -0
@@ @@ -0,0 +1,55 @@
+ import { defineStore } from 'pinia'
+ import { ref, computed } from 'vue'
+ import api from '../services/api'
+
+ export const useAuthStore = defineStore('auth', () => {
+ const token = ref(localStorage.getItem('token'))
+ const user = ref(JSON.parse(localStorage.getItem('user') || 'null'))
+
+ const isAuthenticated = computed(() => !!token.value)
+
+ async function requestMagicLink(email) {
+ const response = await api.post('/auth/request', { email })
+ return response.data
+ }
+
+ async function verifyMagicLink(verifyToken) {
+ const response = await api.post('/auth/verify', { token: verifyToken })
+ const { token: authToken, user: userData } = response.data
+
+ token.value = authToken
+ user.value = userData
+
+ localStorage.setItem('token', authToken)
+ localStorage.setItem('user', JSON.stringify(userData))
+
+ // Set default authorization header
+ api.defaults.headers.common['Authorization'] = `Bearer ${authToken}`
+
+ return response.data
+ }
+
+ function logout() {
+ token.value = null
+ user.value = null
+
+ localStorage.removeItem('token')
+ localStorage.removeItem('user')
+
+ delete api.defaults.headers.common['Authorization']
+ }
+
+ // Initialize auth header if token exists
+ if (token.value) {
+ api.defaults.headers.common['Authorization'] = `Bearer ${token.value}`
+ }
+
+ return {
+ token,
+ user,
+ isAuthenticated,
+ requestMagicLink,
+ verifyMagicLink,
+ logout
+ }
+ })
\ No newline at end of file
frontend/src/stores/entries.js +94 -0
@@ @@ -0,0 +1,94 @@
+ import { defineStore } from 'pinia'
+ import { ref } from 'vue'
+ import api from '../services/api'
+
+ export const useEntriesStore = defineStore('entries', () => {
+ const entries = ref([])
+ const loading = ref(false)
+ const error = ref(null)
+
+ async function fetchEntries() {
+ loading.value = true
+ error.value = null
+ try {
+ const response = await api.get('/entries')
+ entries.value = response.data.entries
+ } catch (err) {
+ error.value = err.response?.data?.error || 'Failed to fetch entries'
+ throw err
+ } finally {
+ loading.value = false
+ }
+ }
+
+ async function createEntry(data) {
+ loading.value = true
+ error.value = null
+ try {
+ const response = await api.post('/entries', data)
+ entries.value.unshift(response.data.entry)
+ return response.data.entry
+ } catch (err) {
+ error.value = err.response?.data?.error || 'Failed to create entry'
+ throw err
+ } finally {
+ loading.value = false
+ }
+ }
+
+ async function updateEntry(id, data) {
+ loading.value = true
+ error.value = null
+ try {
+ await api.put(`/entries/${id}`, data)
+ const index = entries.value.findIndex(e => e.id === id)
+ if (index !== -1) {
+ entries.value[index] = { ...entries.value[index], ...data }
+ }
+ } catch (err) {
+ error.value = err.response?.data?.error || 'Failed to update entry'
+ throw err
+ } finally {
+ loading.value = false
+ }
+ }
+
+ async function deleteEntry(id) {
+ loading.value = true
+ error.value = null
+ try {
+ await api.delete(`/entries/${id}`)
+ entries.value = entries.value.filter(e => e.id !== id)
+ } catch (err) {
+ error.value = err.response?.data?.error || 'Failed to delete entry'
+ throw err
+ } finally {
+ loading.value = false
+ }
+ }
+
+ async function fetchAnalytics(id) {
+ loading.value = true
+ error.value = null
+ try {
+ const response = await api.get(`/analytics/${id}`)
+ return response.data
+ } catch (err) {
+ error.value = err.response?.data?.error || 'Failed to fetch analytics'
+ throw err
+ } finally {
+ loading.value = false
+ }
+ }
+
+ return {
+ entries,
+ loading,
+ error,
+ fetchEntries,
+ createEntry,
+ updateEntry,
+ deleteEntry,
+ fetchAnalytics
+ }
+ })
\ No newline at end of file
frontend/src/style.css +3 -0
@@ @@ -0,0 +1,3 @@
+ @tailwind base;
+ @tailwind components;
+ @tailwind utilities;
\ No newline at end of file
frontend/src/views/Analytics.vue +229 -0
@@ @@ -0,0 +1,229 @@
+ <template>
+ <div class="min-h-screen bg-gray-50">
+ <!-- Header -->
+ <header class="bg-white shadow-sm border-b">
+ <div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
+ <div class="flex justify-between items-center h-16">
+ <div class="flex items-center space-x-4">
+ <router-link to="/dashboard" class="flex items-center space-x-2">
+ <LinkIcon class="h-6 w-6 text-indigo-600" />
+ <span class="text-xl font-bold text-gray-900">QRurl</span>
+ </router-link>
+ <span class="text-gray-400">/</span>
+ <span class="text-gray-600">Analytics</span>
+ </div>
+
+ <router-link
+ to="/dashboard"
+ class="text-gray-500 hover:text-gray-700 transition flex items-center space-x-2"
+ >
+ <ArrowLeftIcon class="h-4 w-4" />
+ <span>Back to Dashboard</span>
+ </router-link>
+ </div>
+ </div>
+ </header>
+
+ <!-- Main Content -->
+ <main class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
+ <div v-if="loading" class="bg-white rounded-lg shadow-sm p-8">
+ <div class="text-center">
+ <Loader2Icon class="h-8 w-8 text-gray-400 animate-spin mx-auto mb-2" />
+ <p class="text-gray-500">Loading analytics...</p>
+ </div>
+ </div>
+
+ <div v-else-if="error" class="bg-white rounded-lg shadow-sm p-8">
+ <div class="text-center">
+ <AlertCircleIcon class="h-12 w-12 text-red-400 mx-auto mb-3" />
+ <p class="text-gray-900 font-semibold">Failed to load analytics</p>
+ <p class="text-gray-500 mt-1">{{ error }}</p>
+ </div>
+ </div>
+
+ <div v-else>
+ <!-- Link Info -->
+ <div class="bg-white rounded-lg shadow-sm p-6 mb-6">
+ <h1 class="text-2xl font-bold text-gray-900 mb-2">{{ analyticsData.entry.name }}</h1>
+ <div class="space-y-2">
+ <p class="text-sm text-gray-600">
+ <span class="font-medium">Short URL:</span>
+ <a :href="getShortUrl(analyticsData.entry.slug)" target="_blank" class="text-indigo-600 hover:text-indigo-700 ml-2">
+ {{ getShortUrl(analyticsData.entry.slug) }}
+ </a>
+ </p>
+ </div>
+ </div>
+
+ <!-- Stats Cards -->
+ <div class="grid md:grid-cols-3 gap-6 mb-6">
+ <div class="bg-white rounded-lg shadow-sm p-6">
+ <div class="flex items-center justify-between">
+ <div>
+ <p class="text-sm font-medium text-gray-600">Total Clicks</p>
+ <p class="text-3xl font-bold text-gray-900 mt-1">{{ analyticsData.entry.clickCount || 0 }}</p>
+ </div>
+ <div class="p-3 bg-indigo-100 rounded-lg">
+ <MousePointerClickIcon class="h-6 w-6 text-indigo-600" />
+ </div>
+ </div>
+ </div>
+
+ <div class="bg-white rounded-lg shadow-sm p-6">
+ <div class="flex items-center justify-between">
+ <div>
+ <p class="text-sm font-medium text-gray-600">Unique Visitors</p>
+ <p class="text-3xl font-bold text-gray-900 mt-1">{{ uniqueVisitors }}</p>
+ </div>
+ <div class="p-3 bg-green-100 rounded-lg">
+ <UsersIcon class="h-6 w-6 text-green-600" />
+ </div>
+ </div>
+ </div>
+
+ <div class="bg-white rounded-lg shadow-sm p-6">
+ <div class="flex items-center justify-between">
+ <div>
+ <p class="text-sm font-medium text-gray-600">Countries</p>
+ <p class="text-3xl font-bold text-gray-900 mt-1">{{ uniqueCountries }}</p>
+ </div>
+ <div class="p-3 bg-purple-100 rounded-lg">
+ <GlobeIcon class="h-6 w-6 text-purple-600" />
+ </div>
+ </div>
+ </div>
+ </div>
+
+ <!-- Clicks by Date -->
+ <div class="bg-white rounded-lg shadow-sm p-6 mb-6">
+ <h2 class="text-lg font-semibold mb-4">Clicks Over Time</h2>
+ <div v-if="analyticsData.analytics.length === 0" class="text-center py-8 text-gray-500">
+ No click data available yet
+ </div>
+ <div v-else class="space-y-2">
+ <div v-for="day in clicksByDate" :key="day.date" class="flex items-center">
+ <span class="text-sm text-gray-600 w-24">{{ formatDate(day.date) }}</span>
+ <div class="flex-1 mx-4">
+ <div class="h-6 bg-gray-100 rounded-full overflow-hidden">
+ <div
+ class="h-full bg-indigo-600 rounded-full"
+ :style="`width: ${(day.clicks / maxClicks) * 100}%`"
+ ></div>
+ </div>
+ </div>
+ <span class="text-sm font-medium text-gray-900 w-12 text-right">{{ day.clicks }}</span>
+ </div>
+ </div>
+ </div>
+
+ <!-- Countries -->
+ <div class="bg-white rounded-lg shadow-sm p-6">
+ <h2 class="text-lg font-semibold mb-4">Top Countries</h2>
+ <div v-if="countriesData.length === 0" class="text-center py-8 text-gray-500">
+ No geographic data available yet
+ </div>
+ <div v-else class="space-y-3">
+ <div v-for="country in countriesData" :key="country.name" class="flex items-center justify-between">
+ <span class="text-sm text-gray-700">{{ country.name || 'Unknown' }}</span>
+ <span class="text-sm font-medium text-gray-900">{{ country.clicks }} clicks</span>
+ </div>
+ </div>
+ </div>
+ </div>
+ </main>
+ </div>
+ </template>
+
+ <script setup>
+ import { ref, computed, onMounted } from 'vue'
+ import { useRoute } from 'vue-router'
+ import {
+ Link as LinkIcon,
+ ArrowLeft as ArrowLeftIcon,
+ Loader2 as Loader2Icon,
+ AlertCircle as AlertCircleIcon,
+ MousePointerClick as MousePointerClickIcon,
+ Users as UsersIcon,
+ Globe as GlobeIcon
+ } from 'lucide-vue-next'
+ import { useEntriesStore } from '../stores/entries'
+
+ const route = useRoute()
+ const entriesStore = useEntriesStore()
+
+ const loading = ref(true)
+ const error = ref('')
+ const analyticsData = ref(null)
+
+ const uniqueVisitors = computed(() => {
+ if (!analyticsData.value?.analytics) return 0
+ const unique = new Set(analyticsData.value.analytics.map(a => a.ip_hash))
+ return unique.size
+ })
+
+ const uniqueCountries = computed(() => {
+ if (!analyticsData.value?.analytics) return 0
+ const countries = new Set(analyticsData.value.analytics.map(a => a.country).filter(Boolean))
+ return countries.size
+ })
+
+ const clicksByDate = computed(() => {
+ if (!analyticsData.value?.analytics) return []
+
+ const grouped = {}
+ analyticsData.value.analytics.forEach(item => {
+ const date = item.date
+ if (!grouped[date]) {
+ grouped[date] = 0
+ }
+ grouped[date] += item.clicks || 1
+ })
+
+ return Object.entries(grouped)
+ .map(([date, clicks]) => ({ date, clicks }))
+ .sort((a, b) => new Date(b.date) - new Date(a.date))
+ .slice(0, 7)
+ })
+
+ const maxClicks = computed(() => {
+ return Math.max(...clicksByDate.value.map(d => d.clicks), 1)
+ })
+
+ const countriesData = computed(() => {
+ if (!analyticsData.value?.analytics) return []
+
+ const grouped = {}
+ analyticsData.value.analytics.forEach(item => {
+ const country = item.country || 'Unknown'
+ if (!grouped[country]) {
+ grouped[country] = 0
+ }
+ grouped[country] += item.clicks || 1
+ })
+
+ return Object.entries(grouped)
+ .map(([name, clicks]) => ({ name, clicks }))
+ .sort((a, b) => b.clicks - a.clicks)
+ .slice(0, 10)
+ })
+
+ onMounted(async () => {
+ try {
+ const id = route.params.id
+ analyticsData.value = await entriesStore.fetchAnalytics(id)
+ } catch (err) {
+ error.value = err.response?.data?.error || 'Failed to load analytics'
+ } finally {
+ loading.value = false
+ }
+ })
+
+ function getShortUrl(slug) {
+ return `${import.meta.env.VITE_SHORT_URL || 'http://localhost:8787'}/${slug}`
+ }
+
+ function formatDate(dateString) {
+ const date = new Date(dateString)
+ return date.toLocaleDateString('en-US', { month: 'short', day: 'numeric' })
+ }
+ </script>
\ No newline at end of file
frontend/src/views/Dashboard.vue +259 -0
@@ @@ -0,0 +1,259 @@
+ <template>
+ <div class="min-h-screen bg-gray-50">
+ <!-- Header -->
+ <header class="bg-white shadow-sm border-b">
+ <div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
+ <div class="flex justify-between items-center h-16">
+ <div class="flex items-center space-x-2">
+ <LinkIcon class="h-6 w-6 text-indigo-600" />
+ <span class="text-xl font-bold text-gray-900">QRurl</span>
+ </div>
+
+ <div class="flex items-center space-x-4">
+ <span class="text-sm text-gray-600">{{ authStore.user?.email }}</span>
+ <button
+ @click="handleLogout"
+ class="text-gray-500 hover:text-gray-700 transition"
+ >
+ <LogOutIcon class="h-5 w-5" />
+ </button>
+ </div>
+ </div>
+ </div>
+ </header>
+
+ <!-- Main Content -->
+ <main class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
+ <!-- Create New Link -->
+ <div class="bg-white rounded-lg shadow-sm p-6 mb-8">
+ <h2 class="text-lg font-semibold mb-4">Create New Short Link</h2>
+
+ <form @submit.prevent="handleCreate" class="space-y-4">
+ <div class="grid md:grid-cols-2 gap-4">
+ <div>
+ <label class="block text-sm font-medium text-gray-700 mb-1">
+ Name
+ </label>
+ <input
+ v-model="newEntry.name"
+ type="text"
+ required
+ class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500"
+ placeholder="My awesome link"
+ />
+ </div>
+
+ <div>
+ <label class="block text-sm font-medium text-gray-700 mb-1">
+ Custom Slug (optional)
+ </label>
+ <input
+ v-model="newEntry.customSlug"
+ type="text"
+ class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500"
+ placeholder="my-custom-slug"
+ pattern="[a-zA-Z0-9-]{3,50}"
+ />
+ </div>
+ </div>
+
+ <div>
+ <label class="block text-sm font-medium text-gray-700 mb-1">
+ Destination URL
+ </label>
+ <input
+ v-model="newEntry.url"
+ type="url"
+ required
+ class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500"
+ placeholder="https://example.com"
+ />
+ </div>
+
+ <div class="flex justify-end">
+ <button
+ type="submit"
+ :disabled="creating"
+ class="bg-indigo-600 text-white px-6 py-2 rounded-lg font-semibold hover:bg-indigo-700 transition disabled:opacity-50"
+ >
+ {{ creating ? 'Creating...' : 'Create Link' }}
+ </button>
+ </div>
+ </form>
+ </div>
+
+ <!-- Links List -->
+ <div class="bg-white rounded-lg shadow-sm">
+ <div class="px-6 py-4 border-b">
+ <h2 class="text-lg font-semibold">Your Links</h2>
+ </div>
+
+ <div v-if="loading" class="p-8 text-center">
+ <Loader2Icon class="h-8 w-8 text-gray-400 animate-spin mx-auto mb-2" />
+ <p class="text-gray-500">Loading your links...</p>
+ </div>
+
+ <div v-else-if="entriesStore.entries.length === 0" class="p-8 text-center">
+ <LinkIcon class="h-12 w-12 text-gray-300 mx-auto mb-3" />
+ <p class="text-gray-500">No links yet. Create your first one above!</p>
+ </div>
+
+ <div v-else class="divide-y">
+ <div
+ v-for="entry in entriesStore.entries"
+ :key="entry.id"
+ class="p-6 hover:bg-gray-50 transition"
+ >
+ <div class="flex items-start justify-between">
+ <div class="flex-1">
+ <h3 class="font-semibold text-gray-900">{{ entry.name }}</h3>
+ <div class="mt-1 space-y-1">
+ <p class="text-sm text-indigo-600">
+ {{ getShortUrl(entry.slug) }}
+ <button
+ @click="copyToClipboard(getShortUrl(entry.slug))"
+ class="ml-2 text-gray-400 hover:text-gray-600"
+ >
+ <CopyIcon class="h-4 w-4 inline" />
+ </button>
+ </p>
+ <p class="text-sm text-gray-500 truncate">
+ โ†’ {{ entry.original_url }}
+ </p>
+ </div>
+ <div class="mt-2 flex items-center space-x-4 text-sm text-gray-500">
+ <span>{{ entry.click_count || 0 }} clicks</span>
+ <span>Created {{ formatDate(entry.created_at) }}</span>
+ </div>
+ </div>
+
+ <div class="flex items-center space-x-2 ml-4">
+ <button
+ @click="showQRCode(entry)"
+ class="p-2 text-gray-400 hover:text-gray-600 transition"
+ title="Show QR Code"
+ >
+ <QrCodeIcon class="h-5 w-5" />
+ </button>
+ <router-link
+ :to="`/analytics/${entry.id}`"
+ class="p-2 text-gray-400 hover:text-gray-600 transition"
+ title="View Analytics"
+ >
+ <ChartBarIcon class="h-5 w-5" />
+ </router-link>
+ <button
+ @click="deleteEntry(entry.id)"
+ class="p-2 text-gray-400 hover:text-red-600 transition"
+ title="Delete"
+ >
+ <TrashIcon class="h-5 w-5" />
+ </button>
+ </div>
+ </div>
+ </div>
+ </div>
+ </div>
+ </main>
+
+ <!-- QR Code Modal -->
+ <QRCodeModal
+ v-if="selectedEntry"
+ :entry="selectedEntry"
+ @close="selectedEntry = null"
+ />
+ </div>
+ </template>
+
+ <script setup>
+ import { ref, onMounted } from 'vue'
+ import { useRouter } from 'vue-router'
+ import {
+ Link as LinkIcon,
+ LogOut as LogOutIcon,
+ Loader2 as Loader2Icon,
+ Copy as CopyIcon,
+ QrCode as QrCodeIcon,
+ BarChart3 as ChartBarIcon,
+ Trash2 as TrashIcon
+ } from 'lucide-vue-next'
+ import { useAuthStore } from '../stores/auth'
+ import { useEntriesStore } from '../stores/entries'
+ import QRCodeModal from '../components/QRCodeModal.vue'
+
+ const router = useRouter()
+ const authStore = useAuthStore()
+ const entriesStore = useEntriesStore()
+
+ const loading = ref(true)
+ const creating = ref(false)
+ const selectedEntry = ref(null)
+
+ const newEntry = ref({
+ name: '',
+ url: '',
+ customSlug: ''
+ })
+
+ onMounted(async () => {
+ try {
+ await entriesStore.fetchEntries()
+ } finally {
+ loading.value = false
+ }
+ })
+
+ async function handleCreate() {
+ creating.value = true
+ try {
+ await entriesStore.createEntry(newEntry.value)
+ newEntry.value = { name: '', url: '', customSlug: '' }
+ } catch (error) {
+ alert(error.response?.data?.error || 'Failed to create link')
+ } finally {
+ creating.value = false
+ }
+ }
+
+ async function deleteEntry(id) {
+ if (confirm('Are you sure you want to delete this link?')) {
+ try {
+ await entriesStore.deleteEntry(id)
+ } catch (error) {
+ alert('Failed to delete link')
+ }
+ }
+ }
+
+ function handleLogout() {
+ authStore.logout()
+ router.push('/')
+ }
+
+ function getShortUrl(slug) {
+ return `${import.meta.env.VITE_SHORT_URL || 'http://localhost:8787'}/${slug}`
+ }
+
+ function copyToClipboard(text) {
+ navigator.clipboard.writeText(text)
+ .then(() => alert('Copied to clipboard!'))
+ .catch(() => alert('Failed to copy'))
+ }
+
+ function showQRCode(entry) {
+ selectedEntry.value = entry
+ }
+
+ function formatDate(dateString) {
+ const date = new Date(dateString)
+ const now = new Date()
+ const diff = now - date
+ const days = Math.floor(diff / (1000 * 60 * 60 * 24))
+
+ if (days === 0) return 'today'
+ if (days === 1) return 'yesterday'
+ if (days < 30) return `${days} days ago`
+
+ return date.toLocaleDateString()
+ }
+ </script>
\ No newline at end of file
frontend/src/views/Home.vue +98 -0
@@ @@ -0,0 +1,98 @@
+ <template>
+ <div class="min-h-screen bg-gradient-to-br from-blue-50 to-indigo-100">
+ <div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
+ <nav class="flex justify-between items-center py-6">
+ <div class="flex items-center space-x-2">
+ <LinkIcon class="h-8 w-8 text-indigo-600" />
+ <span class="text-2xl font-bold text-gray-900">QRurl</span>
+ </div>
+ <div class="flex items-center space-x-4">
+ <router-link
+ v-if="!authStore.isAuthenticated"
+ to="/login"
+ class="text-gray-700 hover:text-gray-900"
+ >
+ Sign In
+ </router-link>
+ <router-link
+ v-if="authStore.isAuthenticated"
+ to="/dashboard"
+ class="bg-indigo-600 text-white px-4 py-2 rounded-lg hover:bg-indigo-700 transition"
+ >
+ Dashboard
+ </router-link>
+ </div>
+ </nav>
+
+ <main class="py-20">
+ <div class="text-center">
+ <h1 class="text-5xl font-bold text-gray-900 mb-6">
+ Short Links, Beautiful QR Codes
+ </h1>
+ <p class="text-xl text-gray-600 mb-12 max-w-2xl mx-auto">
+ Create custom short URLs with branded QR codes. Track analytics, manage your links,
+ and share them with style.
+ </p>
+
+ <div v-if="!authStore.isAuthenticated" class="space-y-4">
+ <router-link
+ to="/login"
+ class="inline-block bg-indigo-600 text-white px-8 py-4 rounded-lg text-lg font-semibold hover:bg-indigo-700 transition"
+ >
+ Get Started Free
+ </router-link>
+ <p class="text-sm text-gray-500">No credit card required</p>
+ </div>
+
+ <div v-else>
+ <router-link
+ to="/dashboard"
+ class="inline-block bg-indigo-600 text-white px-8 py-4 rounded-lg text-lg font-semibold hover:bg-indigo-700 transition"
+ >
+ Go to Dashboard
+ </router-link>
+ </div>
+ </div>
+
+ <div class="mt-20 grid md:grid-cols-3 gap-8">
+ <div class="bg-white p-8 rounded-xl shadow-sm">
+ <div class="w-12 h-12 bg-indigo-100 rounded-lg flex items-center justify-center mb-4">
+ <LinkIcon class="h-6 w-6 text-indigo-600" />
+ </div>
+ <h3 class="text-xl font-semibold mb-2">Custom Short URLs</h3>
+ <p class="text-gray-600">
+ Create memorable short links with custom slugs or auto-generated codes
+ </p>
+ </div>
+
+ <div class="bg-white p-8 rounded-xl shadow-sm">
+ <div class="w-12 h-12 bg-indigo-100 rounded-lg flex items-center justify-center mb-4">
+ <QrCodeIcon class="h-6 w-6 text-indigo-600" />
+ </div>
+ <h3 class="text-xl font-semibold mb-2">Branded QR Codes</h3>
+ <p class="text-gray-600">
+ Generate QR codes with your logo embedded for professional sharing
+ </p>
+ </div>
+
+ <div class="bg-white p-8 rounded-xl shadow-sm">
+ <div class="w-12 h-12 bg-indigo-100 rounded-lg flex items-center justify-center mb-4">
+ <ChartBarIcon class="h-6 w-6 text-indigo-600" />
+ </div>
+ <h3 class="text-xl font-semibold mb-2">Analytics & Insights</h3>
+ <p class="text-gray-600">
+ Track clicks, geographic data, and referrers for your links
+ </p>
+ </div>
+ </div>
+ </main>
+ </div>
+ </div>
+ </template>
+
+ <script setup>
+ import { Link as LinkIcon, QrCode as QrCodeIcon, BarChart3 as ChartBarIcon } from 'lucide-vue-next'
+ import { useAuthStore } from '../stores/auth'
+
+ const authStore = useAuthStore()
+ </script>
\ No newline at end of file
frontend/src/views/Login.vue +104 -0
@@ @@ -0,0 +1,104 @@
+ <template>
+ <div class="min-h-screen bg-gradient-to-br from-blue-50 to-indigo-100 flex items-center justify-center px-4">
+ <div class="max-w-md w-full">
+ <div class="bg-white rounded-2xl shadow-xl p-8">
+ <div class="text-center mb-8">
+ <router-link to="/" class="inline-flex items-center space-x-2 mb-6">
+ <LinkIcon class="h-8 w-8 text-indigo-600" />
+ <span class="text-2xl font-bold text-gray-900">QRurl</span>
+ </router-link>
+ <h2 class="text-3xl font-bold text-gray-900">Welcome back</h2>
+ <p class="text-gray-600 mt-2">Sign in with your email</p>
+ </div>
+
+ <form @submit.prevent="handleSubmit" class="space-y-6">
+ <div>
+ <label for="email" class="block text-sm font-medium text-gray-700 mb-2">
+ Email address
+ </label>
+ <input
+ id="email"
+ v-model="email"
+ type="email"
+ required
+ class="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500 transition"
+ placeholder="you@example.com"
+ :disabled="loading"
+ />
+ </div>
+
+ <button
+ type="submit"
+ :disabled="loading"
+ class="w-full bg-indigo-600 text-white py-3 px-4 rounded-lg font-semibold hover:bg-indigo-700 transition disabled:opacity-50 disabled:cursor-not-allowed flex items-center justify-center"
+ >
+ <span v-if="!loading">Send Magic Link</span>
+ <span v-else class="flex items-center">
+ <Loader2Icon class="animate-spin -ml-1 mr-3 h-5 w-5" />
+ Sending...
+ </span>
+ </button>
+ </form>
+
+ <div v-if="success" class="mt-6 p-4 bg-green-50 border border-green-200 rounded-lg">
+ <div class="flex items-start">
+ <CheckCircleIcon class="h-5 w-5 text-green-500 mt-0.5 mr-2 flex-shrink-0" />
+ <div>
+ <p class="text-green-800 font-medium">Magic link sent!</p>
+ <p class="text-green-700 text-sm mt-1">
+ Check your email for a login link. It will expire in 15 minutes.
+ </p>
+ </div>
+ </div>
+ </div>
+
+ <div v-if="error" class="mt-6 p-4 bg-red-50 border border-red-200 rounded-lg">
+ <div class="flex items-start">
+ <AlertCircleIcon class="h-5 w-5 text-red-500 mt-0.5 mr-2 flex-shrink-0" />
+ <div>
+ <p class="text-red-800 font-medium">Error</p>
+ <p class="text-red-700 text-sm mt-1">{{ error }}</p>
+ </div>
+ </div>
+ </div>
+
+ <div class="mt-8 text-center text-sm text-gray-600">
+ <p>
+ By signing in, you agree to our
+ <a href="#" class="text-indigo-600 hover:text-indigo-700"> Terms of Service</a>
+ and
+ <a href="#" class="text-indigo-600 hover:text-indigo-700"> Privacy Policy</a>
+ </p>
+ </div>
+ </div>
+ </div>
+ </div>
+ </template>
+
+ <script setup>
+ import { ref } from 'vue'
+ import { Link as LinkIcon, Loader2 as Loader2Icon, CheckCircle as CheckCircleIcon, AlertCircle as AlertCircleIcon } from 'lucide-vue-next'
+ import { useAuthStore } from '../stores/auth'
+
+ const authStore = useAuthStore()
+
+ const email = ref('')
+ const loading = ref(false)
+ const success = ref(false)
+ const error = ref('')
+
+ async function handleSubmit() {
+ loading.value = true
+ error.value = ''
+ success.value = false
+
+ try {
+ await authStore.requestMagicLink(email.value)
+ success.value = true
+ } catch (err) {
+ error.value = err.response?.data?.error || 'Failed to send magic link. Please try again.'
+ } finally {
+ loading.value = false
+ }
+ }
+ </script>
\ No newline at end of file
frontend/src/views/Verify.vue +76 -0
@@ @@ -0,0 +1,76 @@
+ <template>
+ <div class="min-h-screen bg-gradient-to-br from-blue-50 to-indigo-100 flex items-center justify-center px-4">
+ <div class="max-w-md w-full">
+ <div class="bg-white rounded-2xl shadow-xl p-8">
+ <div class="text-center">
+ <router-link to="/" class="inline-flex items-center space-x-2 mb-6">
+ <LinkIcon class="h-8 w-8 text-indigo-600" />
+ <span class="text-2xl font-bold text-gray-900">QRurl</span>
+ </router-link>
+
+ <div v-if="loading" class="py-8">
+ <Loader2Icon class="h-12 w-12 text-indigo-600 animate-spin mx-auto mb-4" />
+ <p class="text-gray-600">Verifying your login...</p>
+ </div>
+
+ <div v-else-if="error" class="py-8">
+ <AlertCircleIcon class="h-12 w-12 text-red-500 mx-auto mb-4" />
+ <h3 class="text-xl font-semibold text-gray-900 mb-2">Verification Failed</h3>
+ <p class="text-gray-600 mb-6">{{ error }}</p>
+ <router-link
+ to="/login"
+ class="inline-block bg-indigo-600 text-white px-6 py-3 rounded-lg font-semibold hover:bg-indigo-700 transition"
+ >
+ Back to Login
+ </router-link>
+ </div>
+
+ <div v-else-if="success" class="py-8">
+ <CheckCircleIcon class="h-12 w-12 text-green-500 mx-auto mb-4" />
+ <h3 class="text-xl font-semibold text-gray-900 mb-2">Success!</h3>
+ <p class="text-gray-600">Redirecting to dashboard...</p>
+ </div>
+ </div>
+ </div>
+ </div>
+ </div>
+ </template>
+
+ <script setup>
+ import { ref, onMounted } from 'vue'
+ import { useRoute, useRouter } from 'vue-router'
+ import { Link as LinkIcon, Loader2 as Loader2Icon, CheckCircle as CheckCircleIcon, AlertCircle as AlertCircleIcon } from 'lucide-vue-next'
+ import { useAuthStore } from '../stores/auth'
+
+ const route = useRoute()
+ const router = useRouter()
+ const authStore = useAuthStore()
+
+ const loading = ref(true)
+ const success = ref(false)
+ const error = ref('')
+
+ onMounted(async () => {
+ const token = route.query.token
+
+ if (!token) {
+ error.value = 'No verification token provided'
+ loading.value = false
+ return
+ }
+
+ try {
+ await authStore.verifyMagicLink(token)
+ success.value = true
+
+ // Redirect to dashboard after 2 seconds
+ setTimeout(() => {
+ router.push('/dashboard')
+ }, 2000)
+ } catch (err) {
+ error.value = err.response?.data?.error || 'Invalid or expired token'
+ } finally {
+ loading.value = false
+ }
+ })
+ </script>
\ No newline at end of file
frontend/tailwind.config.js +13 -0
@@ @@ -0,0 +1,13 @@
+ /** @type {import('tailwindcss').Config} */
+ export default {
+ content: [
+ "./index.html",
+ "./src/**/*.{vue,js,ts,jsx,tsx}",
+ ],
+ theme: {
+ extend: {},
+ },
+ plugins: [
+ require('@tailwindcss/forms'),
+ ],
+ }
\ No newline at end of file
frontend/vite.config.js +16 -0
@@ @@ -0,0 +1,16 @@
+ import { defineConfig } from 'vite'
+ import vue from '@vitejs/plugin-vue'
+
+ // https://vite.dev/config/
+ export default defineConfig({
+ plugins: [vue()],
+ server: {
+ port: 3000,
+ proxy: {
+ '/api': {
+ target: 'http://localhost:8787',
+ changeOrigin: true,
+ }
+ }
+ }
+ })
package-lock.json +1548 -0
@@ @@ -0,0 +1,1548 @@
+ {
+ "name": "qrurl",
+ "version": "1.0.0",
+ "lockfileVersion": 3,
+ "requires": true,
+ "packages": {
+ "": {
+ "name": "qrurl",
+ "version": "1.0.0",
+ "license": "ISC",
+ "devDependencies": {
+ "wrangler": "^4.32.0"
+ }
+ },
+ "node_modules/@cloudflare/kv-asset-handler": {
+ "version": "0.4.0",
+ "resolved": "https://registry.npmjs.org/@cloudflare/kv-asset-handler/-/kv-asset-handler-0.4.0.tgz",
+ "integrity": "sha512-+tv3z+SPp+gqTIcImN9o0hqE9xyfQjI1XD9pL6NuKjua9B1y7mNYv0S9cP+QEbA4ppVgGZEmKOvHX5G5Ei1CVA==",
+ "dev": true,
+ "license": "MIT OR Apache-2.0",
+ "dependencies": {
+ "mime": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "node_modules/@cloudflare/unenv-preset": {
+ "version": "2.6.2",
+ "resolved": "https://registry.npmjs.org/@cloudflare/unenv-preset/-/unenv-preset-2.6.2.tgz",
+ "integrity": "sha512-C7/tW7Qy+wGOCmHXu7xpP1TF3uIhRoi7zVY7dmu/SOSGjPilK+lSQ2lIRILulZsT467ZJNlI0jBxMbd8LzkGRg==",
+ "dev": true,
+ "license": "MIT OR Apache-2.0",
+ "peerDependencies": {
+ "unenv": "2.0.0-rc.19",
+ "workerd": "^1.20250802.0"
+ },
+ "peerDependenciesMeta": {
+ "workerd": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@cloudflare/workerd-darwin-64": {
+ "version": "1.20250816.0",
+ "resolved": "https://registry.npmjs.org/@cloudflare/workerd-darwin-64/-/workerd-darwin-64-1.20250816.0.tgz",
+ "integrity": "sha512-yN1Rga4ufTdrJPCP4gEqfB47i1lWi3teY5IoeQbUuKnjnCtm4pZvXur526JzCmaw60Jx+AEWf5tizdwRd5hHBQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "Apache-2.0",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">=16"
+ }
+ },
+ "node_modules/@cloudflare/workerd-darwin-arm64": {
+ "version": "1.20250816.0",
+ "resolved": "https://registry.npmjs.org/@cloudflare/workerd-darwin-arm64/-/workerd-darwin-arm64-1.20250816.0.tgz",
+ "integrity": "sha512-WyKPMQhbU+TTf4uDz3SA7ZObspg7WzyJMv/7J4grSddpdx2A4Y4SfPu3wsZleAOIMOAEVi0A1sYDhdltKM7Mxg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "Apache-2.0",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">=16"
+ }
+ },
+ "node_modules/@cloudflare/workerd-linux-64": {
+ "version": "1.20250816.0",
+ "resolved": "https://registry.npmjs.org/@cloudflare/workerd-linux-64/-/workerd-linux-64-1.20250816.0.tgz",
+ "integrity": "sha512-NWHOuFnVBaPRhLHw8kjPO9GJmc2P/CTYbnNlNm0EThyi57o/oDx0ldWLJqEHlrdEPOw7zEVGBqM/6M+V9agC6w==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "Apache-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=16"
+ }
+ },
+ "node_modules/@cloudflare/workerd-linux-arm64": {
+ "version": "1.20250816.0",
+ "resolved": "https://registry.npmjs.org/@cloudflare/workerd-linux-arm64/-/workerd-linux-arm64-1.20250816.0.tgz",
+ "integrity": "sha512-FR+/yhaWs7FhfC3GKsM3+usQVrGEweJ9qyh7p+R6HNwnobgKr/h5ATWvJ4obGJF6ZHHodgSe+gOSYR7fkJ1xAQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "Apache-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=16"
+ }
+ },
+ "node_modules/@cloudflare/workerd-windows-64": {
+ "version": "1.20250816.0",
+ "resolved": "https://registry.npmjs.org/@cloudflare/workerd-windows-64/-/workerd-windows-64-1.20250816.0.tgz",
+ "integrity": "sha512-0lqClj2UMhFa8tCBiiX7Zhd5Bjp0V+X8oNBG6V6WsR9p9/HlIHAGgwRAM7aYkyG+8KC8xlbC89O2AXUXLpHx0g==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "Apache-2.0",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=16"
+ }
+ },
+ "node_modules/@cspotcode/source-map-support": {
+ "version": "0.8.1",
+ "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz",
+ "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/trace-mapping": "0.3.9"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@emnapi/runtime": {
+ "version": "1.4.5",
+ "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.4.5.tgz",
+ "integrity": "sha512-++LApOtY0pEEz1zrd9vy1/zXVaVJJ/EbAF3u0fXIzPJEDtnITsBGbbK0EkM72amhl/R5b+5xx0Y/QhcVOpuulg==",
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "tslib": "^2.4.0"
+ }
+ },
+ "node_modules/@esbuild/aix-ppc64": {
+ "version": "0.25.4",
+ "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.4.tgz",
+ "integrity": "sha512-1VCICWypeQKhVbE9oW/sJaAmjLxhVqacdkvPLEjwlttjfwENRSClS8EjBz0KzRyFSCPDIkuXW34Je/vk7zdB7Q==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "aix"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/android-arm": {
+ "version": "0.25.4",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.4.tgz",
+ "integrity": "sha512-QNdQEps7DfFwE3hXiU4BZeOV68HHzYwGd0Nthhd3uCkkEKK7/R6MTgM0P7H7FAs5pU/DIWsviMmEGxEoxIZ+ZQ==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/android-arm64": {
+ "version": "0.25.4",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.4.tgz",
+ "integrity": "sha512-bBy69pgfhMGtCnwpC/x5QhfxAz/cBgQ9enbtwjf6V9lnPI/hMyT9iWpR1arm0l3kttTr4L0KSLpKmLp/ilKS9A==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/android-x64": {
+ "version": "0.25.4",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.4.tgz",
+ "integrity": "sha512-TVhdVtQIFuVpIIR282btcGC2oGQoSfZfmBdTip2anCaVYcqWlZXGcdcKIUklfX2wj0JklNYgz39OBqh2cqXvcQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/darwin-arm64": {
+ "version": "0.25.4",
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.4.tgz",
+ "integrity": "sha512-Y1giCfM4nlHDWEfSckMzeWNdQS31BQGs9/rouw6Ub91tkK79aIMTH3q9xHvzH8d0wDru5Ci0kWB8b3up/nl16g==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/darwin-x64": {
+ "version": "0.25.4",
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.4.tgz",
+ "integrity": "sha512-CJsry8ZGM5VFVeyUYB3cdKpd/H69PYez4eJh1W/t38vzutdjEjtP7hB6eLKBoOdxcAlCtEYHzQ/PJ/oU9I4u0A==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/freebsd-arm64": {
+ "version": "0.25.4",
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.4.tgz",
+ "integrity": "sha512-yYq+39NlTRzU2XmoPW4l5Ifpl9fqSk0nAJYM/V/WUGPEFfek1epLHJIkTQM6bBs1swApjO5nWgvr843g6TjxuQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/freebsd-x64": {
+ "version": "0.25.4",
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.4.tgz",
+ "integrity": "sha512-0FgvOJ6UUMflsHSPLzdfDnnBBVoCDtBTVyn/MrWloUNvq/5SFmh13l3dvgRPkDihRxb77Y17MbqbCAa2strMQQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-arm": {
+ "version": "0.25.4",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.4.tgz",
+ "integrity": "sha512-kro4c0P85GMfFYqW4TWOpvmF8rFShbWGnrLqlzp4X1TNWjRY3JMYUfDCtOxPKOIY8B0WC8HN51hGP4I4hz4AaQ==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-arm64": {
+ "version": "0.25.4",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.4.tgz",
+ "integrity": "sha512-+89UsQTfXdmjIvZS6nUnOOLoXnkUTB9hR5QAeLrQdzOSWZvNSAXAtcRDHWtqAUtAmv7ZM1WPOOeSxDzzzMogiQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-ia32": {
+ "version": "0.25.4",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.4.tgz",
+ "integrity": "sha512-yTEjoapy8UP3rv8dB0ip3AfMpRbyhSN3+hY8mo/i4QXFeDxmiYbEKp3ZRjBKcOP862Ua4b1PDfwlvbuwY7hIGQ==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-loong64": {
+ "version": "0.25.4",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.4.tgz",
+ "integrity": "sha512-NeqqYkrcGzFwi6CGRGNMOjWGGSYOpqwCjS9fvaUlX5s3zwOtn1qwg1s2iE2svBe4Q/YOG1q6875lcAoQK/F4VA==",
+ "cpu": [
+ "loong64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-mips64el": {
+ "version": "0.25.4",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.4.tgz",
+ "integrity": "sha512-IcvTlF9dtLrfL/M8WgNI/qJYBENP3ekgsHbYUIzEzq5XJzzVEV/fXY9WFPfEEXmu3ck2qJP8LG/p3Q8f7Zc2Xg==",
+ "cpu": [
+ "mips64el"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-ppc64": {
+ "version": "0.25.4",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.4.tgz",
+ "integrity": "sha512-HOy0aLTJTVtoTeGZh4HSXaO6M95qu4k5lJcH4gxv56iaycfz1S8GO/5Jh6X4Y1YiI0h7cRyLi+HixMR+88swag==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-riscv64": {
+ "version": "0.25.4",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.4.tgz",
+ "integrity": "sha512-i8JUDAufpz9jOzo4yIShCTcXzS07vEgWzyX3NH2G7LEFVgrLEhjwL3ajFE4fZI3I4ZgiM7JH3GQ7ReObROvSUA==",
+ "cpu": [
+ "riscv64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-s390x": {
+ "version": "0.25.4",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.4.tgz",
+ "integrity": "sha512-jFnu+6UbLlzIjPQpWCNh5QtrcNfMLjgIavnwPQAfoGx4q17ocOU9MsQ2QVvFxwQoWpZT8DvTLooTvmOQXkO51g==",
+ "cpu": [
+ "s390x"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-x64": {
+ "version": "0.25.4",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.4.tgz",
+ "integrity": "sha512-6e0cvXwzOnVWJHq+mskP8DNSrKBr1bULBvnFLpc1KY+d+irZSgZ02TGse5FsafKS5jg2e4pbvK6TPXaF/A6+CA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/netbsd-arm64": {
+ "version": "0.25.4",
+ "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.4.tgz",
+ "integrity": "sha512-vUnkBYxZW4hL/ie91hSqaSNjulOnYXE1VSLusnvHg2u3jewJBz3YzB9+oCw8DABeVqZGg94t9tyZFoHma8gWZQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "netbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/netbsd-x64": {
+ "version": "0.25.4",
+ "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.4.tgz",
+ "integrity": "sha512-XAg8pIQn5CzhOB8odIcAm42QsOfa98SBeKUdo4xa8OvX8LbMZqEtgeWE9P/Wxt7MlG2QqvjGths+nq48TrUiKw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "netbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/openbsd-arm64": {
+ "version": "0.25.4",
+ "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.4.tgz",
+ "integrity": "sha512-Ct2WcFEANlFDtp1nVAXSNBPDxyU+j7+tId//iHXU2f/lN5AmO4zLyhDcpR5Cz1r08mVxzt3Jpyt4PmXQ1O6+7A==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/openbsd-x64": {
+ "version": "0.25.4",
+ "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.4.tgz",
+ "integrity": "sha512-xAGGhyOQ9Otm1Xu8NT1ifGLnA6M3sJxZ6ixylb+vIUVzvvd6GOALpwQrYrtlPouMqd/vSbgehz6HaVk4+7Afhw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/sunos-x64": {
+ "version": "0.25.4",
+ "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.4.tgz",
+ "integrity": "sha512-Mw+tzy4pp6wZEK0+Lwr76pWLjrtjmJyUB23tHKqEDP74R3q95luY/bXqXZeYl4NYlvwOqoRKlInQialgCKy67Q==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "sunos"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/win32-arm64": {
+ "version": "0.25.4",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.4.tgz",
+ "integrity": "sha512-AVUP428VQTSddguz9dO9ngb+E5aScyg7nOeJDrF1HPYu555gmza3bDGMPhmVXL8svDSoqPCsCPjb265yG/kLKQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/win32-ia32": {
+ "version": "0.25.4",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.4.tgz",
+ "integrity": "sha512-i1sW+1i+oWvQzSgfRcxxG2k4I9n3O9NRqy8U+uugaT2Dy7kLO9Y7wI72haOahxceMX8hZAzgGou1FhndRldxRg==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/win32-x64": {
+ "version": "0.25.4",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.4.tgz",
+ "integrity": "sha512-nOT2vZNw6hJ+z43oP1SPea/G/6AbN6X+bGNhNuq8NtRHy4wsMhw765IKLNmnjek7GvjWBYQ8Q5VBoYTFg9y1UQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@img/sharp-darwin-arm64": {
+ "version": "0.33.5",
+ "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.33.5.tgz",
+ "integrity": "sha512-UT4p+iz/2H4twwAoLCqfA9UH5pI6DggwKEGuaPy7nCVQ8ZsiY5PIcrRvD1DzuY3qYL07NtIQcWnBSY/heikIFQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "Apache-2.0",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ },
+ "optionalDependencies": {
+ "@img/sharp-libvips-darwin-arm64": "1.0.4"
+ }
+ },
+ "node_modules/@img/sharp-darwin-x64": {
+ "version": "0.33.5",
+ "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.33.5.tgz",
+ "integrity": "sha512-fyHac4jIc1ANYGRDxtiqelIbdWkIuQaI84Mv45KvGRRxSAa7o7d1ZKAOBaYbnepLC1WqxfpimdeWfvqqSGwR2Q==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "Apache-2.0",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ },
+ "optionalDependencies": {
+ "@img/sharp-libvips-darwin-x64": "1.0.4"
+ }
+ },
+ "node_modules/@img/sharp-libvips-darwin-arm64": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.0.4.tgz",
+ "integrity": "sha512-XblONe153h0O2zuFfTAbQYAX2JhYmDHeWikp1LM9Hul9gVPjFY427k6dFEcOL72O01QxQsWi761svJ/ev9xEDg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "LGPL-3.0-or-later",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ }
+ },
+ "node_modules/@img/sharp-libvips-darwin-x64": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.0.4.tgz",
+ "integrity": "sha512-xnGR8YuZYfJGmWPvmlunFaWJsb9T/AO2ykoP3Fz/0X5XV2aoYBPkX6xqCQvUTKKiLddarLaxpzNe+b1hjeWHAQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "LGPL-3.0-or-later",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ }
+ },
+ "node_modules/@img/sharp-libvips-linux-arm": {
+ "version": "1.0.5",
+ "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.0.5.tgz",
+ "integrity": "sha512-gvcC4ACAOPRNATg/ov8/MnbxFDJqf/pDePbBnuBDcjsI8PssmjoKMAz4LtLaVi+OnSb5FK/yIOamqDwGmXW32g==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "LGPL-3.0-or-later",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ }
+ },
+ "node_modules/@img/sharp-libvips-linux-arm64": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.0.4.tgz",
+ "integrity": "sha512-9B+taZ8DlyyqzZQnoeIvDVR/2F4EbMepXMc/NdVbkzsJbzkUjhXv/70GQJ7tdLA4YJgNP25zukcxpX2/SueNrA==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "LGPL-3.0-or-later",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ }
+ },
+ "node_modules/@img/sharp-libvips-linux-s390x": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.0.4.tgz",
+ "integrity": "sha512-u7Wz6ntiSSgGSGcjZ55im6uvTrOxSIS8/dgoVMoiGE9I6JAfU50yH5BoDlYA1tcuGS7g/QNtetJnxA6QEsCVTA==",
+ "cpu": [
+ "s390x"
+ ],
+ "dev": true,
+ "license": "LGPL-3.0-or-later",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ }
+ },
+ "node_modules/@img/sharp-libvips-linux-x64": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.0.4.tgz",
+ "integrity": "sha512-MmWmQ3iPFZr0Iev+BAgVMb3ZyC4KeFc3jFxnNbEPas60e1cIfevbtuyf9nDGIzOaW9PdnDciJm+wFFaTlj5xYw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "LGPL-3.0-or-later",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ }
+ },
+ "node_modules/@img/sharp-libvips-linuxmusl-arm64": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.0.4.tgz",
+ "integrity": "sha512-9Ti+BbTYDcsbp4wfYib8Ctm1ilkugkA/uscUn6UXK1ldpC1JjiXbLfFZtRlBhjPZ5o1NCLiDbg8fhUPKStHoTA==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "LGPL-3.0-or-later",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ }
+ },
+ "node_modules/@img/sharp-libvips-linuxmusl-x64": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.0.4.tgz",
+ "integrity": "sha512-viYN1KX9m+/hGkJtvYYp+CCLgnJXwiQB39damAO7WMdKWlIhmYTfHjwSbQeUK/20vY154mwezd9HflVFM1wVSw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "LGPL-3.0-or-later",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ }
+ },
+ "node_modules/@img/sharp-linux-arm": {
+ "version": "0.33.5",
+ "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.33.5.tgz",
+ "integrity": "sha512-JTS1eldqZbJxjvKaAkxhZmBqPRGmxgu+qFKSInv8moZ2AmT5Yib3EQ1c6gp493HvrvV8QgdOXdyaIBrhvFhBMQ==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "Apache-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ },
+ "optionalDependencies": {
+ "@img/sharp-libvips-linux-arm": "1.0.5"
+ }
+ },
+ "node_modules/@img/sharp-linux-arm64": {
+ "version": "0.33.5",
+ "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.33.5.tgz",
+ "integrity": "sha512-JMVv+AMRyGOHtO1RFBiJy/MBsgz0x4AWrT6QoEVVTyh1E39TrCUpTRI7mx9VksGX4awWASxqCYLCV4wBZHAYxA==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "Apache-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ },
+ "optionalDependencies": {
+ "@img/sharp-libvips-linux-arm64": "1.0.4"
+ }
+ },
+ "node_modules/@img/sharp-linux-s390x": {
+ "version": "0.33.5",
+ "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.33.5.tgz",
+ "integrity": "sha512-y/5PCd+mP4CA/sPDKl2961b+C9d+vPAveS33s6Z3zfASk2j5upL6fXVPZi7ztePZ5CuH+1kW8JtvxgbuXHRa4Q==",
+ "cpu": [
+ "s390x"
+ ],
+ "dev": true,
+ "license": "Apache-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ },
+ "optionalDependencies": {
+ "@img/sharp-libvips-linux-s390x": "1.0.4"
+ }
+ },
+ "node_modules/@img/sharp-linux-x64": {
+ "version": "0.33.5",
+ "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.33.5.tgz",
+ "integrity": "sha512-opC+Ok5pRNAzuvq1AG0ar+1owsu842/Ab+4qvU879ippJBHvyY5n2mxF1izXqkPYlGuP/M556uh53jRLJmzTWA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "Apache-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ },
+ "optionalDependencies": {
+ "@img/sharp-libvips-linux-x64": "1.0.4"
+ }
+ },
+ "node_modules/@img/sharp-linuxmusl-arm64": {
+ "version": "0.33.5",
+ "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.33.5.tgz",
+ "integrity": "sha512-XrHMZwGQGvJg2V/oRSUfSAfjfPxO+4DkiRh6p2AFjLQztWUuY/o8Mq0eMQVIY7HJ1CDQUJlxGGZRw1a5bqmd1g==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "Apache-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ },
+ "optionalDependencies": {
+ "@img/sharp-libvips-linuxmusl-arm64": "1.0.4"
+ }
+ },
+ "node_modules/@img/sharp-linuxmusl-x64": {
+ "version": "0.33.5",
+ "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.33.5.tgz",
+ "integrity": "sha512-WT+d/cgqKkkKySYmqoZ8y3pxx7lx9vVejxW/W4DOFMYVSkErR+w7mf2u8m/y4+xHe7yY9DAXQMWQhpnMuFfScw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "Apache-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ },
+ "optionalDependencies": {
+ "@img/sharp-libvips-linuxmusl-x64": "1.0.4"
+ }
+ },
+ "node_modules/@img/sharp-wasm32": {
+ "version": "0.33.5",
+ "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.33.5.tgz",
+ "integrity": "sha512-ykUW4LVGaMcU9lu9thv85CbRMAwfeadCJHRsg2GmeRa/cJxsVY9Rbd57JcMxBkKHag5U/x7TSBpScF4U8ElVzg==",
+ "cpu": [
+ "wasm32"
+ ],
+ "dev": true,
+ "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT",
+ "optional": true,
+ "dependencies": {
+ "@emnapi/runtime": "^1.2.0"
+ },
+ "engines": {
+ "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ }
+ },
+ "node_modules/@img/sharp-win32-ia32": {
+ "version": "0.33.5",
+ "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.33.5.tgz",
+ "integrity": "sha512-T36PblLaTwuVJ/zw/LaH0PdZkRz5rd3SmMHX8GSmR7vtNSP5Z6bQkExdSK7xGWyxLw4sUknBuugTelgw2faBbQ==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "license": "Apache-2.0 AND LGPL-3.0-or-later",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ }
+ },
+ "node_modules/@img/sharp-win32-x64": {
+ "version": "0.33.5",
+ "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.33.5.tgz",
+ "integrity": "sha512-MpY/o8/8kj+EcnxwvrP4aTJSWw/aZ7JIGR4aBeZkZw5B7/Jn+tY9/VNwtcoGmdT7GfggGIU4kygOMSbYnOrAbg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "Apache-2.0 AND LGPL-3.0-or-later",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ }
+ },
+ "node_modules/@jridgewell/resolve-uri": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
+ "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/@jridgewell/sourcemap-codec": {
+ "version": "1.5.5",
+ "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",
+ "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@jridgewell/trace-mapping": {
+ "version": "0.3.9",
+ "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz",
+ "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/resolve-uri": "^3.0.3",
+ "@jridgewell/sourcemap-codec": "^1.4.10"
+ }
+ },
+ "node_modules/@poppinss/colors": {
+ "version": "4.1.5",
+ "resolved": "https://registry.npmjs.org/@poppinss/colors/-/colors-4.1.5.tgz",
+ "integrity": "sha512-FvdDqtcRCtz6hThExcFOgW0cWX+xwSMWcRuQe5ZEb2m7cVQOAVZOIMt+/v9RxGiD9/OY16qJBXK4CVKWAPalBw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "kleur": "^4.1.5"
+ }
+ },
+ "node_modules/@poppinss/dumper": {
+ "version": "0.6.4",
+ "resolved": "https://registry.npmjs.org/@poppinss/dumper/-/dumper-0.6.4.tgz",
+ "integrity": "sha512-iG0TIdqv8xJ3Lt9O8DrPRxw1MRLjNpoqiSGU03P/wNLP/s0ra0udPJ1J2Tx5M0J3H/cVyEgpbn8xUKRY9j59kQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@poppinss/colors": "^4.1.5",
+ "@sindresorhus/is": "^7.0.2",
+ "supports-color": "^10.0.0"
+ }
+ },
+ "node_modules/@poppinss/exception": {
+ "version": "1.2.2",
+ "resolved": "https://registry.npmjs.org/@poppinss/exception/-/exception-1.2.2.tgz",
+ "integrity": "sha512-m7bpKCD4QMlFCjA/nKTs23fuvoVFoA83brRKmObCUNmi/9tVu8Ve3w4YQAnJu4q3Tjf5fr685HYIC/IA2zHRSg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@sindresorhus/is": {
+ "version": "7.0.2",
+ "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-7.0.2.tgz",
+ "integrity": "sha512-d9xRovfKNz1SKieM0qJdO+PQonjnnIfSNWfHYnBSJ9hkjm0ZPw6HlxscDXYstp3z+7V2GOFHc+J0CYrYTjqCJw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sindresorhus/is?sponsor=1"
+ }
+ },
+ "node_modules/@speed-highlight/core": {
+ "version": "1.2.7",
+ "resolved": "https://registry.npmjs.org/@speed-highlight/core/-/core-1.2.7.tgz",
+ "integrity": "sha512-0dxmVj4gxg3Jg879kvFS/msl4s9F3T9UXC1InxgOf7t5NvcPD97u/WTA5vL/IxWHMn7qSxBozqrnnE2wvl1m8g==",
+ "dev": true,
+ "license": "CC0-1.0"
+ },
+ "node_modules/acorn": {
+ "version": "8.14.0",
+ "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.0.tgz",
+ "integrity": "sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA==",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "acorn": "bin/acorn"
+ },
+ "engines": {
+ "node": ">=0.4.0"
+ }
+ },
+ "node_modules/acorn-walk": {
+ "version": "8.3.2",
+ "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.2.tgz",
+ "integrity": "sha512-cjkyv4OtNCIeqhHrfS81QWXoCBPExR/J62oyEqepVw8WaQeSqpW2uhuLPh1m9eWhDuOo/jUXVTlifvesOWp/4A==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.4.0"
+ }
+ },
+ "node_modules/blake3-wasm": {
+ "version": "2.1.5",
+ "resolved": "https://registry.npmjs.org/blake3-wasm/-/blake3-wasm-2.1.5.tgz",
+ "integrity": "sha512-F1+K8EbfOZE49dtoPtmxUQrpXaBIl3ICvasLh+nJta0xkz+9kF/7uet9fLnwKqhDrmj6g+6K3Tw9yQPUg2ka5g==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/color": {
+ "version": "4.2.3",
+ "resolved": "https://registry.npmjs.org/color/-/color-4.2.3.tgz",
+ "integrity": "sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "color-convert": "^2.0.1",
+ "color-string": "^1.9.0"
+ },
+ "engines": {
+ "node": ">=12.5.0"
+ }
+ },
+ "node_modules/color-convert": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
+ "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "color-name": "~1.1.4"
+ },
+ "engines": {
+ "node": ">=7.0.0"
+ }
+ },
+ "node_modules/color-name": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
+ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/color-string": {
+ "version": "1.9.1",
+ "resolved": "https://registry.npmjs.org/color-string/-/color-string-1.9.1.tgz",
+ "integrity": "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "color-name": "^1.0.0",
+ "simple-swizzle": "^0.2.2"
+ }
+ },
+ "node_modules/cookie": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.0.2.tgz",
+ "integrity": "sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/defu": {
+ "version": "6.1.4",
+ "resolved": "https://registry.npmjs.org/defu/-/defu-6.1.4.tgz",
+ "integrity": "sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/detect-libc": {
+ "version": "2.0.4",
+ "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.4.tgz",
+ "integrity": "sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/error-stack-parser-es": {
+ "version": "1.0.5",
+ "resolved": "https://registry.npmjs.org/error-stack-parser-es/-/error-stack-parser-es-1.0.5.tgz",
+ "integrity": "sha512-5qucVt2XcuGMcEGgWI7i+yZpmpByQ8J1lHhcL7PwqCwu9FPP3VUXzT4ltHe5i2z9dePwEHcDVOAfSnHsOlCXRA==",
+ "dev": true,
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/antfu"
+ }
+ },
+ "node_modules/esbuild": {
+ "version": "0.25.4",
+ "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.4.tgz",
+ "integrity": "sha512-8pgjLUcUjcgDg+2Q4NYXnPbo/vncAY4UmyaCm0jZevERqCHZIaWwdJHkf8XQtu4AxSKCdvrUbT0XUr1IdZzI8Q==",
+ "dev": true,
+ "hasInstallScript": true,
+ "license": "MIT",
+ "bin": {
+ "esbuild": "bin/esbuild"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "optionalDependencies": {
+ "@esbuild/aix-ppc64": "0.25.4",
+ "@esbuild/android-arm": "0.25.4",
+ "@esbuild/android-arm64": "0.25.4",
+ "@esbuild/android-x64": "0.25.4",
+ "@esbuild/darwin-arm64": "0.25.4",
+ "@esbuild/darwin-x64": "0.25.4",
+ "@esbuild/freebsd-arm64": "0.25.4",
+ "@esbuild/freebsd-x64": "0.25.4",
+ "@esbuild/linux-arm": "0.25.4",
+ "@esbuild/linux-arm64": "0.25.4",
+ "@esbuild/linux-ia32": "0.25.4",
+ "@esbuild/linux-loong64": "0.25.4",
+ "@esbuild/linux-mips64el": "0.25.4",
+ "@esbuild/linux-ppc64": "0.25.4",
+ "@esbuild/linux-riscv64": "0.25.4",
+ "@esbuild/linux-s390x": "0.25.4",
+ "@esbuild/linux-x64": "0.25.4",
+ "@esbuild/netbsd-arm64": "0.25.4",
+ "@esbuild/netbsd-x64": "0.25.4",
+ "@esbuild/openbsd-arm64": "0.25.4",
+ "@esbuild/openbsd-x64": "0.25.4",
+ "@esbuild/sunos-x64": "0.25.4",
+ "@esbuild/win32-arm64": "0.25.4",
+ "@esbuild/win32-ia32": "0.25.4",
+ "@esbuild/win32-x64": "0.25.4"
+ }
+ },
+ "node_modules/exit-hook": {
+ "version": "2.2.1",
+ "resolved": "https://registry.npmjs.org/exit-hook/-/exit-hook-2.2.1.tgz",
+ "integrity": "sha512-eNTPlAD67BmP31LDINZ3U7HSF8l57TxOY2PmBJ1shpCvpnxBF93mWCE8YHBnXs8qiUZJc9WDcWIeC3a2HIAMfw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/exsolve": {
+ "version": "1.0.7",
+ "resolved": "https://registry.npmjs.org/exsolve/-/exsolve-1.0.7.tgz",
+ "integrity": "sha512-VO5fQUzZtI6C+vx4w/4BWJpg3s/5l+6pRQEHzFRM8WFi4XffSP1Z+4qi7GbjWbvRQEbdIco5mIMq+zX4rPuLrw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/fsevents": {
+ "version": "2.3.3",
+ "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
+ "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
+ "dev": true,
+ "hasInstallScript": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
+ }
+ },
+ "node_modules/glob-to-regexp": {
+ "version": "0.4.1",
+ "resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz",
+ "integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==",
+ "dev": true,
+ "license": "BSD-2-Clause"
+ },
+ "node_modules/is-arrayish": {
+ "version": "0.3.2",
+ "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.2.tgz",
+ "integrity": "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/kleur": {
+ "version": "4.1.5",
+ "resolved": "https://registry.npmjs.org/kleur/-/kleur-4.1.5.tgz",
+ "integrity": "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/mime": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/mime/-/mime-3.0.0.tgz",
+ "integrity": "sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A==",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "mime": "cli.js"
+ },
+ "engines": {
+ "node": ">=10.0.0"
+ }
+ },
+ "node_modules/miniflare": {
+ "version": "4.20250816.1",
+ "resolved": "https://registry.npmjs.org/miniflare/-/miniflare-4.20250816.1.tgz",
+ "integrity": "sha512-2X8yMy5wWw0dF1pNU4kztzZgp0jWv2KMqAOOb2FeQ/b11yck4aczmYHi7UYD3uyOgtj8WFhwG/KdRWAaATTtRA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@cspotcode/source-map-support": "0.8.1",
+ "acorn": "8.14.0",
+ "acorn-walk": "8.3.2",
+ "exit-hook": "2.2.1",
+ "glob-to-regexp": "0.4.1",
+ "sharp": "^0.33.5",
+ "stoppable": "1.1.0",
+ "undici": "^7.10.0",
+ "workerd": "1.20250816.0",
+ "ws": "8.18.0",
+ "youch": "4.1.0-beta.10",
+ "zod": "3.22.3"
+ },
+ "bin": {
+ "miniflare": "bootstrap.js"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "node_modules/ohash": {
+ "version": "2.0.11",
+ "resolved": "https://registry.npmjs.org/ohash/-/ohash-2.0.11.tgz",
+ "integrity": "sha512-RdR9FQrFwNBNXAr4GixM8YaRZRJ5PUWbKYbE5eOsrwAjJW0q2REGcf79oYPsLyskQCZG1PLN+S/K1V00joZAoQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/path-to-regexp": {
+ "version": "6.3.0",
+ "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-6.3.0.tgz",
+ "integrity": "sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/pathe": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz",
+ "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/semver": {
+ "version": "7.7.2",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz",
+ "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==",
+ "dev": true,
+ "license": "ISC",
+ "bin": {
+ "semver": "bin/semver.js"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/sharp": {
+ "version": "0.33.5",
+ "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.33.5.tgz",
+ "integrity": "sha512-haPVm1EkS9pgvHrQ/F3Xy+hgcuMV0Wm9vfIBSiwZ05k+xgb0PkBQpGsAA/oWdDobNaZTH5ppvHtzCFbnSEwHVw==",
+ "dev": true,
+ "hasInstallScript": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "color": "^4.2.3",
+ "detect-libc": "^2.0.3",
+ "semver": "^7.6.3"
+ },
+ "engines": {
+ "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ },
+ "optionalDependencies": {
+ "@img/sharp-darwin-arm64": "0.33.5",
+ "@img/sharp-darwin-x64": "0.33.5",
+ "@img/sharp-libvips-darwin-arm64": "1.0.4",
+ "@img/sharp-libvips-darwin-x64": "1.0.4",
+ "@img/sharp-libvips-linux-arm": "1.0.5",
+ "@img/sharp-libvips-linux-arm64": "1.0.4",
+ "@img/sharp-libvips-linux-s390x": "1.0.4",
+ "@img/sharp-libvips-linux-x64": "1.0.4",
+ "@img/sharp-libvips-linuxmusl-arm64": "1.0.4",
+ "@img/sharp-libvips-linuxmusl-x64": "1.0.4",
+ "@img/sharp-linux-arm": "0.33.5",
+ "@img/sharp-linux-arm64": "0.33.5",
+ "@img/sharp-linux-s390x": "0.33.5",
+ "@img/sharp-linux-x64": "0.33.5",
+ "@img/sharp-linuxmusl-arm64": "0.33.5",
+ "@img/sharp-linuxmusl-x64": "0.33.5",
+ "@img/sharp-wasm32": "0.33.5",
+ "@img/sharp-win32-ia32": "0.33.5",
+ "@img/sharp-win32-x64": "0.33.5"
+ }
+ },
+ "node_modules/simple-swizzle": {
+ "version": "0.2.2",
+ "resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.2.tgz",
+ "integrity": "sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "is-arrayish": "^0.3.1"
+ }
+ },
+ "node_modules/stoppable": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/stoppable/-/stoppable-1.1.0.tgz",
+ "integrity": "sha512-KXDYZ9dszj6bzvnEMRYvxgeTHU74QBFL54XKtP3nyMuJ81CFYtABZ3bAzL2EdFUaEwJOBOgENyFj3R7oTzDyyw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=4",
+ "npm": ">=6"
+ }
+ },
+ "node_modules/supports-color": {
+ "version": "10.2.0",
+ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-10.2.0.tgz",
+ "integrity": "sha512-5eG9FQjEjDbAlI5+kdpdyPIBMRH4GfTVDGREVupaZHmVoppknhM29b/S9BkQz7cathp85BVgRi/As3Siln7e0Q==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/supports-color?sponsor=1"
+ }
+ },
+ "node_modules/tslib": {
+ "version": "2.8.1",
+ "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
+ "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
+ "dev": true,
+ "license": "0BSD",
+ "optional": true
+ },
+ "node_modules/ufo": {
+ "version": "1.6.1",
+ "resolved": "https://registry.npmjs.org/ufo/-/ufo-1.6.1.tgz",
+ "integrity": "sha512-9a4/uxlTWJ4+a5i0ooc1rU7C7YOw3wT+UGqdeNNHWnOF9qcMBgLRS+4IYUqbczewFx4mLEig6gawh7X6mFlEkA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/undici": {
+ "version": "7.15.0",
+ "resolved": "https://registry.npmjs.org/undici/-/undici-7.15.0.tgz",
+ "integrity": "sha512-7oZJCPvvMvTd0OlqWsIxTuItTpJBpU1tcbVl24FMn3xt3+VSunwUasmfPJRE57oNO1KsZ4PgA1xTdAX4hq8NyQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=20.18.1"
+ }
+ },
+ "node_modules/unenv": {
+ "version": "2.0.0-rc.19",
+ "resolved": "https://registry.npmjs.org/unenv/-/unenv-2.0.0-rc.19.tgz",
+ "integrity": "sha512-t/OMHBNAkknVCI7bVB9OWjUUAwhVv9vsPIAGnNUxnu3FxPQN11rjh0sksLMzc3g7IlTgvHmOTl4JM7JHpcv5wA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "defu": "^6.1.4",
+ "exsolve": "^1.0.7",
+ "ohash": "^2.0.11",
+ "pathe": "^2.0.3",
+ "ufo": "^1.6.1"
+ }
+ },
+ "node_modules/workerd": {
+ "version": "1.20250816.0",
+ "resolved": "https://registry.npmjs.org/workerd/-/workerd-1.20250816.0.tgz",
+ "integrity": "sha512-5gIvHPE/3QVlQR1Sc1NdBkWmqWj/TSgIbY/f/qs9lhiLBw/Da+HbNBTVYGjvwYqEb3NQ+XQM4gAm5b2+JJaUJg==",
+ "dev": true,
+ "hasInstallScript": true,
+ "license": "Apache-2.0",
+ "bin": {
+ "workerd": "bin/workerd"
+ },
+ "engines": {
+ "node": ">=16"
+ },
+ "optionalDependencies": {
+ "@cloudflare/workerd-darwin-64": "1.20250816.0",
+ "@cloudflare/workerd-darwin-arm64": "1.20250816.0",
+ "@cloudflare/workerd-linux-64": "1.20250816.0",
+ "@cloudflare/workerd-linux-arm64": "1.20250816.0",
+ "@cloudflare/workerd-windows-64": "1.20250816.0"
+ }
+ },
+ "node_modules/wrangler": {
+ "version": "4.32.0",
+ "resolved": "https://registry.npmjs.org/wrangler/-/wrangler-4.32.0.tgz",
+ "integrity": "sha512-q7TRSavBW3Eg3pp4rxqKJwSK+u/ieFOBdNvUsq1P1EMmyj3//tN/iXDokFak+dkW0vDYjsVG3PfOfHxU92OS6w==",
+ "dev": true,
+ "license": "MIT OR Apache-2.0",
+ "dependencies": {
+ "@cloudflare/kv-asset-handler": "0.4.0",
+ "@cloudflare/unenv-preset": "2.6.2",
+ "blake3-wasm": "2.1.5",
+ "esbuild": "0.25.4",
+ "miniflare": "4.20250816.1",
+ "path-to-regexp": "6.3.0",
+ "unenv": "2.0.0-rc.19",
+ "workerd": "1.20250816.0"
+ },
+ "bin": {
+ "wrangler": "bin/wrangler.js",
+ "wrangler2": "bin/wrangler.js"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ },
+ "optionalDependencies": {
+ "fsevents": "~2.3.2"
+ },
+ "peerDependencies": {
+ "@cloudflare/workers-types": "^4.20250816.0"
+ },
+ "peerDependenciesMeta": {
+ "@cloudflare/workers-types": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/ws": {
+ "version": "8.18.0",
+ "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.0.tgz",
+ "integrity": "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=10.0.0"
+ },
+ "peerDependencies": {
+ "bufferutil": "^4.0.1",
+ "utf-8-validate": ">=5.0.2"
+ },
+ "peerDependenciesMeta": {
+ "bufferutil": {
+ "optional": true
+ },
+ "utf-8-validate": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/youch": {
+ "version": "4.1.0-beta.10",
+ "resolved": "https://registry.npmjs.org/youch/-/youch-4.1.0-beta.10.tgz",
+ "integrity": "sha512-rLfVLB4FgQneDr0dv1oddCVZmKjcJ6yX6mS4pU82Mq/Dt9a3cLZQ62pDBL4AUO+uVrCvtWz3ZFUL2HFAFJ/BXQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@poppinss/colors": "^4.1.5",
+ "@poppinss/dumper": "^0.6.4",
+ "@speed-highlight/core": "^1.2.7",
+ "cookie": "^1.0.2",
+ "youch-core": "^0.3.3"
+ }
+ },
+ "node_modules/youch-core": {
+ "version": "0.3.3",
+ "resolved": "https://registry.npmjs.org/youch-core/-/youch-core-0.3.3.tgz",
+ "integrity": "sha512-ho7XuGjLaJ2hWHoK8yFnsUGy2Y5uDpqSTq1FkHLK4/oqKtyUU1AFbOOxY4IpC9f0fTLjwYbslUz0Po5BpD1wrA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@poppinss/exception": "^1.2.2",
+ "error-stack-parser-es": "^1.0.5"
+ }
+ },
+ "node_modules/zod": {
+ "version": "3.22.3",
+ "resolved": "https://registry.npmjs.org/zod/-/zod-3.22.3.tgz",
+ "integrity": "sha512-EjIevzuJRiRPbVH4mGc8nApb/lVLKVpmUhAaR5R5doKGfAnGJ6Gr3CViAVjP+4FWSxCsybeWQdcgCtbX+7oZug==",
+ "dev": true,
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/colinhacks"
+ }
+ }
+ }
+ }
package.json +19 -0
@@ @@ -0,0 +1,19 @@
+ {
+ "name": "qrurl",
+ "version": "1.0.0",
+ "main": "index.js",
+ "scripts": {
+ "dev": "wrangler dev",
+ "deploy": "wrangler deploy",
+ "test": "echo \"Error: no test specified\" && exit 1",
+ "db:init": "wrangler d1 execute qrurl-db --local --file=./schema/schema.sql",
+ "db:init:remote": "wrangler d1 execute qrurl-db --file=./schema/schema.sql"
+ },
+ "keywords": [],
+ "author": "",
+ "license": "ISC",
+ "description": "",
+ "devDependencies": {
+ "wrangler": "^4.32.0"
+ }
+ }
schema/schema.sql +63 -0
@@ @@ -0,0 +1,63 @@
+ -- Users table
+ CREATE TABLE IF NOT EXISTS users (
+ id TEXT PRIMARY KEY,
+ email TEXT UNIQUE NOT NULL,
+ created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
+ updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
+ );
+
+ -- Authorized emails (whitelist)
+ CREATE TABLE IF NOT EXISTS authorized_emails (
+ email TEXT PRIMARY KEY,
+ authorized BOOLEAN DEFAULT true,
+ added_at DATETIME DEFAULT CURRENT_TIMESTAMP
+ );
+
+ -- URL entries
+ CREATE TABLE IF NOT EXISTS entries (
+ id TEXT PRIMARY KEY,
+ user_id TEXT NOT NULL,
+ name TEXT NOT NULL,
+ original_url TEXT NOT NULL,
+ slug TEXT UNIQUE NOT NULL,
+ logo_url TEXT,
+ qr_code_url TEXT,
+ click_count INTEGER DEFAULT 0,
+ created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
+ updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
+ FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
+ );
+
+ -- Create index for faster slug lookups
+ CREATE INDEX IF NOT EXISTS idx_entries_slug ON entries(slug);
+ CREATE INDEX IF NOT EXISTS idx_entries_user_id ON entries(user_id);
+
+ -- Analytics
+ CREATE TABLE IF NOT EXISTS analytics (
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
+ entry_id TEXT NOT NULL,
+ timestamp DATETIME DEFAULT CURRENT_TIMESTAMP,
+ ip_hash TEXT,
+ user_agent TEXT,
+ referer TEXT,
+ country TEXT,
+ city TEXT,
+ FOREIGN KEY (entry_id) REFERENCES entries(id) ON DELETE CASCADE
+ );
+
+ -- Create index for analytics queries
+ CREATE INDEX IF NOT EXISTS idx_analytics_entry_id ON analytics(entry_id);
+ CREATE INDEX IF NOT EXISTS idx_analytics_timestamp ON analytics(timestamp);
+
+ -- Magic link tokens
+ CREATE TABLE IF NOT EXISTS auth_tokens (
+ token TEXT PRIMARY KEY,
+ email TEXT NOT NULL,
+ expires_at DATETIME NOT NULL,
+ used BOOLEAN DEFAULT false,
+ created_at DATETIME DEFAULT CURRENT_TIMESTAMP
+ );
+
+ -- Create index for token lookups
+ CREATE INDEX IF NOT EXISTS idx_auth_tokens_email ON auth_tokens(email);
+ CREATE INDEX IF NOT EXISTS idx_auth_tokens_expires_at ON auth_tokens(expires_at);
\ No newline at end of file
src/index.js +49 -0
@@ @@ -0,0 +1,49 @@
+ import { Router } from './router';
+ import { handleRedirect } from './routes/redirect';
+ import { authRoutes } from './routes/auth';
+ import { apiRoutes } from './routes/api';
+ import { applyMiddleware } from './middleware';
+ import { handleError } from './utils/errors';
+
+ export default {
+ async fetch(request, env, ctx) {
+ try {
+ const router = new Router();
+
+ // Health check
+ router.get('/health', () => {
+ return new Response(JSON.stringify({
+ status: 'ok',
+ timestamp: new Date().toISOString()
+ }), {
+ status: 200,
+ headers: { 'Content-Type': 'application/json' }
+ });
+ });
+
+ // API routes
+ router.all('/api/auth/*', authRoutes);
+ router.all('/api/*', (request, env, ctx) =>
+ applyMiddleware(request, env, ctx, apiRoutes)
+ );
+
+ // Redirect handler (must be last)
+ router.get('/:slug', handleRedirect);
+
+ // Default route
+ router.all('*', () => {
+ return new Response(JSON.stringify({
+ error: 'Not Found',
+ message: 'The requested resource was not found'
+ }), {
+ status: 404,
+ headers: { 'Content-Type': 'application/json' }
+ });
+ });
+
+ return await router.handle(request, env, ctx);
+ } catch (error) {
+ return handleError(error);
+ }
+ }
+ };
\ No newline at end of file
db.js b/src/lib/db.js +164 -0
@@ @@ -0,0 +1,164 @@
+ import { NotFoundError, ValidationError } from '../utils/errors';
+
+ export class Database {
+ constructor(db) {
+ this.db = db;
+ }
+
+ // User operations
+ async createUser(id, email) {
+ const stmt = this.db.prepare(
+ 'INSERT INTO users (id, email) VALUES (?, ?)'
+ );
+ return await stmt.bind(id, email).run();
+ }
+
+ async getUserByEmail(email) {
+ const stmt = this.db.prepare('SELECT * FROM users WHERE email = ?');
+ return await stmt.bind(email).first();
+ }
+
+ async getUserById(id) {
+ const stmt = this.db.prepare('SELECT * FROM users WHERE id = ?');
+ return await stmt.bind(id).first();
+ }
+
+ // Authorization operations
+ async isEmailAuthorized(email) {
+ const stmt = this.db.prepare(
+ 'SELECT * FROM authorized_emails WHERE email = ? AND authorized = true'
+ );
+ const result = await stmt.bind(email).first();
+ return !!result;
+ }
+
+ async addAuthorizedEmail(email) {
+ const stmt = this.db.prepare(
+ 'INSERT OR REPLACE INTO authorized_emails (email) VALUES (?)'
+ );
+ return await stmt.bind(email).run();
+ }
+
+ // Entry operations
+ async createEntry(data) {
+ const { id, userId, name, originalUrl, slug, logoUrl } = data;
+ const stmt = this.db.prepare(
+ `INSERT INTO entries (id, user_id, name, original_url, slug, logo_url)
+ VALUES (?, ?, ?, ?, ?, ?)`
+ );
+ return await stmt.bind(id, userId, name, originalUrl, slug, logoUrl).run();
+ }
+
+ async getEntryBySlug(slug) {
+ const stmt = this.db.prepare('SELECT * FROM entries WHERE slug = ?');
+ return await stmt.bind(slug).first();
+ }
+
+ async getEntryById(id) {
+ const stmt = this.db.prepare('SELECT * FROM entries WHERE id = ?');
+ return await stmt.bind(id).first();
+ }
+
+ async getUserEntries(userId, limit = 50, offset = 0) {
+ const stmt = this.db.prepare(
+ `SELECT * FROM entries
+ WHERE user_id = ?
+ ORDER BY created_at DESC
+ LIMIT ? OFFSET ?`
+ );
+ const result = await stmt.bind(userId, limit, offset).all();
+ return result.results;
+ }
+
+ async updateEntry(id, updates) {
+ const fields = [];
+ const values = [];
+
+ for (const [key, value] of Object.entries(updates)) {
+ // Convert camelCase to snake_case
+ const dbField = key.replace(/[A-Z]/g, letter => `_${letter.toLowerCase()}`);
+ fields.push(`${dbField} = ?`);
+ values.push(value);
+ }
+
+ values.push(id);
+ const stmt = this.db.prepare(
+ `UPDATE entries
+ SET ${fields.join(', ')}, updated_at = CURRENT_TIMESTAMP
+ WHERE id = ?`
+ );
+ return await stmt.bind(...values).run();
+ }
+
+ async deleteEntry(id) {
+ const stmt = this.db.prepare('DELETE FROM entries WHERE id = ?');
+ return await stmt.bind(id).run();
+ }
+
+ async incrementClickCount(slug) {
+ const stmt = this.db.prepare(
+ 'UPDATE entries SET click_count = click_count + 1 WHERE slug = ?'
+ );
+ return await stmt.bind(slug).run();
+ }
+
+ // Analytics operations
+ async recordAnalytics(data) {
+ const { entryId, ipHash, userAgent, referer, country, city } = data;
+ const stmt = this.db.prepare(
+ `INSERT INTO analytics (entry_id, ip_hash, user_agent, referer, country, city)
+ VALUES (?, ?, ?, ?, ?, ?)`
+ );
+ return await stmt.bind(entryId, ipHash, userAgent, referer, country, city).run();
+ }
+
+ async getEntryAnalytics(entryId, days = 30) {
+ const stmt = this.db.prepare(
+ `SELECT
+ COUNT(*) as total_clicks,
+ COUNT(DISTINCT ip_hash) as unique_visitors,
+ DATE(timestamp) as date,
+ country,
+ COUNT(*) as clicks
+ FROM analytics
+ WHERE entry_id = ?
+ AND timestamp > datetime('now', '-' || ? || ' days')
+ GROUP BY DATE(timestamp), country
+ ORDER BY date DESC`
+ );
+ const result = await stmt.bind(entryId, days).all();
+ return result.results;
+ }
+
+ // Auth token operations
+ async createAuthToken(token, email, expiresAt) {
+ const stmt = this.db.prepare(
+ 'INSERT INTO auth_tokens (token, email, expires_at) VALUES (?, ?, ?)'
+ );
+ return await stmt.bind(token, email, expiresAt).run();
+ }
+
+ async getAuthToken(token) {
+ const stmt = this.db.prepare(
+ `SELECT * FROM auth_tokens
+ WHERE token = ?
+ AND expires_at > datetime('now')
+ AND used = false`
+ );
+ return await stmt.bind(token).first();
+ }
+
+ async markTokenUsed(token) {
+ const stmt = this.db.prepare(
+ 'UPDATE auth_tokens SET used = true WHERE token = ?'
+ );
+ return await stmt.bind(token).run();
+ }
+
+ async cleanupExpiredTokens() {
+ const stmt = this.db.prepare(
+ 'DELETE FROM auth_tokens WHERE expires_at < datetime("now")'
+ );
+ return await stmt.run();
+ }
+ }
\ No newline at end of file
src/middleware/auth.js +119 -0
@@ @@ -0,0 +1,119 @@
+ import { AuthError } from '../utils/errors';
+
+ export async function verifyAuth(request, env) {
+ // Skip auth for public endpoints
+ const url = new URL(request.url);
+ const publicPaths = ['/health', '/api/health', '/api/auth/request', '/api/auth/verify'];
+
+ // Skip auth for non-API paths (like redirects) or public API paths
+ if (!url.pathname.startsWith('/api') || publicPaths.includes(url.pathname)) {
+ return null;
+ }
+
+ // Get token from Authorization header
+ const authHeader = request.headers.get('Authorization');
+ if (!authHeader || !authHeader.startsWith('Bearer ')) {
+ return new Response(JSON.stringify({ error: 'Unauthorized' }), {
+ status: 401,
+ headers: { 'Content-Type': 'application/json' }
+ });
+ }
+
+ const token = authHeader.slice(7);
+
+ try {
+ // Verify JWT token
+ const payload = await verifyJWT(token, env.JWT_SECRET);
+
+ // Add user info to request
+ request.user = {
+ id: payload.sub,
+ email: payload.email
+ };
+
+ return null; // Continue to handler
+ } catch (error) {
+ return new Response(JSON.stringify({ error: 'Invalid token' }), {
+ status: 401,
+ headers: { 'Content-Type': 'application/json' }
+ });
+ }
+ }
+
+ export async function createJWT(payload, secret, expiresIn = '7d') {
+ const encoder = new TextEncoder();
+
+ const header = {
+ alg: 'HS256',
+ typ: 'JWT'
+ };
+
+ const now = Math.floor(Date.now() / 1000);
+ const exp = expiresIn === '7d' ? now + (7 * 24 * 60 * 60) : now + 3600;
+
+ const tokenPayload = {
+ ...payload,
+ iat: now,
+ exp: exp
+ };
+
+ const encodedHeader = btoa(JSON.stringify(header)).replace(/=/g, '');
+ const encodedPayload = btoa(JSON.stringify(tokenPayload)).replace(/=/g, '');
+
+ const message = `${encodedHeader}.${encodedPayload}`;
+ const key = await crypto.subtle.importKey(
+ 'raw',
+ encoder.encode(secret),
+ { name: 'HMAC', hash: 'SHA-256' },
+ false,
+ ['sign']
+ );
+
+ const signature = await crypto.subtle.sign('HMAC', key, encoder.encode(message));
+ const encodedSignature = btoa(String.fromCharCode(...new Uint8Array(signature)))
+ .replace(/=/g, '')
+ .replace(/\+/g, '-')
+ .replace(/\//g, '_');
+
+ return `${message}.${encodedSignature}`;
+ }
+
+ export async function verifyJWT(token, secret) {
+ const encoder = new TextEncoder();
+ const [header, payload, signature] = token.split('.');
+
+ if (!header || !payload || !signature) {
+ throw new Error('Invalid token format');
+ }
+
+ const key = await crypto.subtle.importKey(
+ 'raw',
+ encoder.encode(secret),
+ { name: 'HMAC', hash: 'SHA-256' },
+ false,
+ ['verify']
+ );
+
+ const message = `${header}.${payload}`;
+ const signatureBytes = Uint8Array.from(atob(signature.replace(/-/g, '+').replace(/_/g, '/')), c => c.charCodeAt(0));
+
+ const valid = await crypto.subtle.verify(
+ 'HMAC',
+ key,
+ signatureBytes,
+ encoder.encode(message)
+ );
+
+ if (!valid) {
+ throw new Error('Invalid signature');
+ }
+
+ const decodedPayload = JSON.parse(atob(payload));
+
+ // Check expiration
+ if (decodedPayload.exp && decodedPayload.exp < Math.floor(Date.now() / 1000)) {
+ throw new Error('Token expired');
+ }
+
+ return decodedPayload;
+ }
\ No newline at end of file
src/middleware/cors.js +29 -0
@@ @@ -0,0 +1,29 @@
+ export function cors(request, env) {
+ // Handle preflight requests
+ if (request.method === 'OPTIONS') {
+ return new Response(null, {
+ status: 204,
+ headers: {
+ 'Access-Control-Allow-Origin': env.FRONTEND_URL || '*',
+ 'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS',
+ 'Access-Control-Allow-Headers': 'Content-Type, Authorization',
+ 'Access-Control-Max-Age': '86400',
+ }
+ });
+ }
+
+ return null;
+ }
+
+ export function addCorsHeaders(response, env) {
+ const headers = new Headers(response.headers);
+ headers.set('Access-Control-Allow-Origin', env.FRONTEND_URL || '*');
+ headers.set('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
+ headers.set('Access-Control-Allow-Headers', 'Content-Type, Authorization');
+
+ return new Response(response.body, {
+ status: response.status,
+ statusText: response.statusText,
+ headers
+ });
+ }
\ No newline at end of file
src/middleware/index.js +20 -0
@@ @@ -0,0 +1,20 @@
+ import { verifyAuth } from './auth';
+ import { rateLimit } from './rateLimit';
+ import { cors } from './cors';
+
+ export async function applyMiddleware(request, env, ctx, handler) {
+ // Apply CORS
+ const corsResponse = cors(request, env);
+ if (corsResponse) return corsResponse;
+
+ // Apply rate limiting
+ const rateLimitResponse = await rateLimit(request, env);
+ if (rateLimitResponse) return rateLimitResponse;
+
+ // Apply auth middleware for protected routes
+ const authResponse = await verifyAuth(request, env);
+ if (authResponse) return authResponse;
+
+ // Call the actual handler
+ return handler(request, env, ctx);
+ }
\ No newline at end of file
src/middleware/rateLimit.js +61 -0
@@ @@ -0,0 +1,61 @@
+ export async function rateLimit(request, env) {
+ // Get client IP
+ const ip = request.headers.get('CF-Connecting-IP') ||
+ request.headers.get('X-Forwarded-For') ||
+ 'unknown';
+
+ // Skip rate limiting for health checks
+ const url = new URL(request.url);
+ if (url.pathname === '/health') {
+ return null;
+ }
+
+ // Simple rate limiting using KV
+ if (env.CACHE) {
+ const key = `ratelimit:${ip}`;
+ const now = Date.now();
+ const window = 60000; // 1 minute window
+ const limit = 60; // 60 requests per minute
+
+ const data = await env.CACHE.get(key, 'json');
+
+ if (data) {
+ // Reset if window has passed
+ if (now - data.start > window) {
+ await env.CACHE.put(key, JSON.stringify({
+ start: now,
+ count: 1
+ }), { expirationTtl: 60 });
+ return null;
+ }
+
+ // Check if limit exceeded
+ if (data.count >= limit) {
+ return new Response(JSON.stringify({
+ error: 'Too many requests',
+ retryAfter: Math.ceil((data.start + window - now) / 1000)
+ }), {
+ status: 429,
+ headers: {
+ 'Content-Type': 'application/json',
+ 'Retry-After': String(Math.ceil((data.start + window - now) / 1000))
+ }
+ });
+ }
+
+ // Increment counter
+ await env.CACHE.put(key, JSON.stringify({
+ ...data,
+ count: data.count + 1
+ }), { expirationTtl: 60 });
+ } else {
+ // First request
+ await env.CACHE.put(key, JSON.stringify({
+ start: now,
+ count: 1
+ }), { expirationTtl: 60 });
+ }
+ }
+
+ return null;
+ }
\ No newline at end of file
src/router.js +82 -0
@@ @@ -0,0 +1,82 @@
+ export class Router {
+ constructor() {
+ this.routes = [];
+ }
+
+ addRoute(method, pattern, handler) {
+ this.routes.push({ method, pattern, handler });
+ }
+
+ get(pattern, handler) {
+ this.addRoute('GET', pattern, handler);
+ }
+
+ post(pattern, handler) {
+ this.addRoute('POST', pattern, handler);
+ }
+
+ put(pattern, handler) {
+ this.addRoute('PUT', pattern, handler);
+ }
+
+ delete(pattern, handler) {
+ this.addRoute('DELETE', pattern, handler);
+ }
+
+ all(pattern, handler) {
+ this.addRoute('*', pattern, handler);
+ }
+
+ async handle(request, env, ctx) {
+ const url = new URL(request.url);
+ const path = url.pathname;
+ const method = request.method;
+
+ for (const route of this.routes) {
+ if (route.method !== '*' && route.method !== method) continue;
+
+ const params = this.matchRoute(route.pattern, path);
+ if (params) {
+ request.params = params;
+ request.query = Object.fromEntries(url.searchParams);
+ return await route.handler(request, env, ctx);
+ }
+ }
+
+ return new Response('Not Found', { status: 404 });
+ }
+
+ matchRoute(pattern, path) {
+ if (pattern === '*') return {};
+
+ // Handle wildcard patterns like /api/*
+ if (pattern.endsWith('/*')) {
+ const base = pattern.slice(0, -2);
+ if (path.startsWith(base + '/') || path === base) {
+ return {};
+ }
+ return null;
+ }
+
+ // Handle exact matches
+ if (pattern === path) return {};
+
+ // Handle parameterized routes like /:slug
+ const patternParts = pattern.split('/');
+ const pathParts = path.split('/');
+
+ if (patternParts.length !== pathParts.length) return null;
+
+ const params = {};
+ for (let i = 0; i < patternParts.length; i++) {
+ if (patternParts[i].startsWith(':')) {
+ const paramName = patternParts[i].slice(1);
+ params[paramName] = pathParts[i];
+ } else if (patternParts[i] !== pathParts[i]) {
+ return null;
+ }
+ }
+
+ return params;
+ }
+ }
\ No newline at end of file
src/routes/api.js +322 -0
@@ @@ -0,0 +1,322 @@
+ import { Database } from '../lib/db';
+ import { generateSlug, validateCustomSlug } from '../utils/slug';
+ import { validateUrl, sanitizeInput } from '../utils/validation';
+ import { ValidationError, NotFoundError, AuthError } from '../utils/errors';
+
+ export async function apiRoutes(request, env, ctx) {
+ const url = new URL(request.url);
+ const path = url.pathname;
+ const method = request.method;
+
+ // Entry routes
+ if (path === '/api/entries' && method === 'GET') {
+ return getUserEntries(request, env);
+ }
+
+ if (path === '/api/entries' && method === 'POST') {
+ return createEntry(request, env);
+ }
+
+ if (path.startsWith('/api/entries/') && method === 'GET') {
+ const id = path.split('/')[3];
+ return getEntry(request, env, id);
+ }
+
+ if (path.startsWith('/api/entries/') && method === 'PUT') {
+ const id = path.split('/')[3];
+ return updateEntry(request, env, id);
+ }
+
+ if (path.startsWith('/api/entries/') && method === 'DELETE') {
+ const id = path.split('/')[3];
+ return deleteEntry(request, env, id);
+ }
+
+ // Analytics routes
+ if (path.startsWith('/api/analytics/')) {
+ const id = path.split('/')[3];
+ return getAnalytics(request, env, id);
+ }
+
+ return new Response('Not Found', { status: 404 });
+ }
+
+ async function getUserEntries(request, env) {
+ try {
+ const db = new Database(env.DB);
+ const url = new URL(request.url);
+ const limit = Math.min(parseInt(url.searchParams.get('limit') || '50'), 100);
+ const offset = parseInt(url.searchParams.get('offset') || '0');
+
+ const entries = await db.getUserEntries(request.user.id, limit, offset);
+
+ return new Response(JSON.stringify({
+ success: true,
+ entries
+ }), {
+ status: 200,
+ headers: { 'Content-Type': 'application/json' }
+ });
+ } catch (error) {
+ console.error('Get entries error:', error);
+ return new Response(JSON.stringify({
+ error: error.message || 'Failed to fetch entries'
+ }), {
+ status: error.status || 500,
+ headers: { 'Content-Type': 'application/json' }
+ });
+ }
+ }
+
+ async function createEntry(request, env) {
+ try {
+ const body = await request.json();
+ const { name, url, customSlug, logoUrl } = body;
+
+ // Validate inputs
+ if (!name || !url) {
+ throw new ValidationError('Name and URL are required');
+ }
+
+ validateUrl(url);
+ const sanitizedName = sanitizeInput(name);
+
+ // Generate or validate slug
+ let slug;
+ if (customSlug) {
+ validateCustomSlug(customSlug);
+ slug = customSlug;
+ } else {
+ slug = generateSlug();
+ }
+
+ const db = new Database(env.DB);
+
+ // Check if slug already exists
+ const existing = await db.getEntryBySlug(slug);
+ if (existing) {
+ // If custom slug, throw error
+ if (customSlug) {
+ throw new ValidationError('This slug is already in use');
+ }
+ // Otherwise, generate a new one
+ slug = generateSlug(8); // Try with longer slug
+ }
+
+ // Create entry
+ const entryId = crypto.randomUUID();
+ await db.createEntry({
+ id: entryId,
+ userId: request.user.id,
+ name: sanitizedName,
+ originalUrl: url,
+ slug,
+ logoUrl: logoUrl || null
+ });
+
+ // Clear cache for user entries
+ if (env.CACHE) {
+ await env.CACHE.delete(`user-entries:${request.user.id}`);
+ }
+
+ return new Response(JSON.stringify({
+ success: true,
+ entry: {
+ id: entryId,
+ name: sanitizedName,
+ originalUrl: url,
+ slug,
+ shortUrl: `${env.BACKEND_URL}/${slug}`,
+ logoUrl: logoUrl || null
+ }
+ }), {
+ status: 201,
+ headers: { 'Content-Type': 'application/json' }
+ });
+ } catch (error) {
+ console.error('Create entry error:', error);
+ return new Response(JSON.stringify({
+ error: error.message || 'Failed to create entry'
+ }), {
+ status: error.status || 400,
+ headers: { 'Content-Type': 'application/json' }
+ });
+ }
+ }
+
+ async function getEntry(request, env, id) {
+ try {
+ const db = new Database(env.DB);
+ const entry = await db.getEntryById(id);
+
+ if (!entry) {
+ throw new NotFoundError('Entry not found');
+ }
+
+ // Check ownership
+ if (entry.user_id !== request.user.id) {
+ throw new AuthError('You do not have permission to view this entry');
+ }
+
+ return new Response(JSON.stringify({
+ success: true,
+ entry
+ }), {
+ status: 200,
+ headers: { 'Content-Type': 'application/json' }
+ });
+ } catch (error) {
+ console.error('Get entry error:', error);
+ return new Response(JSON.stringify({
+ error: error.message || 'Failed to fetch entry'
+ }), {
+ status: error.status || 500,
+ headers: { 'Content-Type': 'application/json' }
+ });
+ }
+ }
+
+ async function updateEntry(request, env, id) {
+ try {
+ const body = await request.json();
+ const { name, url, logoUrl } = body;
+
+ const db = new Database(env.DB);
+ const entry = await db.getEntryById(id);
+
+ if (!entry) {
+ throw new NotFoundError('Entry not found');
+ }
+
+ // Check ownership
+ if (entry.user_id !== request.user.id) {
+ throw new AuthError('You do not have permission to update this entry');
+ }
+
+ const updates = {};
+
+ if (name !== undefined) {
+ updates.name = sanitizeInput(name);
+ }
+
+ if (url !== undefined) {
+ validateUrl(url);
+ updates.originalUrl = url;
+ }
+
+ if (logoUrl !== undefined) {
+ updates.logoUrl = logoUrl;
+ }
+
+ if (Object.keys(updates).length > 0) {
+ await db.updateEntry(id, updates);
+
+ // Clear cache
+ if (env.CACHE) {
+ await env.CACHE.delete(`entry:${entry.slug}`);
+ await env.CACHE.delete(`user-entries:${request.user.id}`);
+ }
+ }
+
+ return new Response(JSON.stringify({
+ success: true,
+ message: 'Entry updated successfully'
+ }), {
+ status: 200,
+ headers: { 'Content-Type': 'application/json' }
+ });
+ } catch (error) {
+ console.error('Update entry error:', error);
+ return new Response(JSON.stringify({
+ error: error.message || 'Failed to update entry'
+ }), {
+ status: error.status || 400,
+ headers: { 'Content-Type': 'application/json' }
+ });
+ }
+ }
+
+ async function deleteEntry(request, env, id) {
+ try {
+ const db = new Database(env.DB);
+ const entry = await db.getEntryById(id);
+
+ if (!entry) {
+ throw new NotFoundError('Entry not found');
+ }
+
+ // Check ownership
+ if (entry.user_id !== request.user.id) {
+ throw new AuthError('You do not have permission to delete this entry');
+ }
+
+ await db.deleteEntry(id);
+
+ // Clear cache
+ if (env.CACHE) {
+ await env.CACHE.delete(`entry:${entry.slug}`);
+ await env.CACHE.delete(`user-entries:${request.user.id}`);
+ }
+
+ // TODO: Delete associated QR code from R2
+
+ return new Response(JSON.stringify({
+ success: true,
+ message: 'Entry deleted successfully'
+ }), {
+ status: 200,
+ headers: { 'Content-Type': 'application/json' }
+ });
+ } catch (error) {
+ console.error('Delete entry error:', error);
+ return new Response(JSON.stringify({
+ error: error.message || 'Failed to delete entry'
+ }), {
+ status: error.status || 500,
+ headers: { 'Content-Type': 'application/json' }
+ });
+ }
+ }
+
+ async function getAnalytics(request, env, id) {
+ try {
+ const db = new Database(env.DB);
+ const entry = await db.getEntryById(id);
+
+ if (!entry) {
+ throw new NotFoundError('Entry not found');
+ }
+
+ // Check ownership
+ if (entry.user_id !== request.user.id) {
+ throw new AuthError('You do not have permission to view analytics for this entry');
+ }
+
+ const url = new URL(request.url);
+ const days = parseInt(url.searchParams.get('days') || '30');
+
+ const analytics = await db.getEntryAnalytics(id, days);
+
+ return new Response(JSON.stringify({
+ success: true,
+ entry: {
+ id: entry.id,
+ name: entry.name,
+ slug: entry.slug,
+ clickCount: entry.click_count
+ },
+ analytics
+ }), {
+ status: 200,
+ headers: { 'Content-Type': 'application/json' }
+ });
+ } catch (error) {
+ console.error('Get analytics error:', error);
+ return new Response(JSON.stringify({
+ error: error.message || 'Failed to fetch analytics'
+ }), {
+ status: error.status || 500,
+ headers: { 'Content-Type': 'application/json' }
+ });
+ }
+ }
\ No newline at end of file
src/routes/auth.js +192 -0
@@ @@ -0,0 +1,192 @@
+ import { Database } from '../lib/db';
+ import { createJWT } from '../middleware/auth';
+ import { validateEmail } from '../utils/validation';
+ import { ValidationError, AuthError } from '../utils/errors';
+
+ export async function authRoutes(request, env, ctx) {
+ const url = new URL(request.url);
+ const path = url.pathname;
+
+ if (path === '/api/auth/request' && request.method === 'POST') {
+ return requestMagicLink(request, env);
+ }
+
+ if (path === '/api/auth/verify' && request.method === 'POST') {
+ return verifyMagicLink(request, env);
+ }
+
+ if (path === '/api/auth/logout' && request.method === 'POST') {
+ return logout(request, env);
+ }
+
+ return new Response('Not Found', { status: 404 });
+ }
+
+ async function requestMagicLink(request, env) {
+ try {
+ const body = await request.json();
+ const { email } = body;
+
+ // Validate email
+ validateEmail(email);
+
+ const db = new Database(env.DB);
+
+ // Check if email is authorized
+ const authorized = await db.isEmailAuthorized(email);
+ if (!authorized) {
+ // Check environment variable whitelist as fallback
+ const authorizedEmails = env.AUTHORIZED_EMAILS?.split(',').map(e => e.trim()) || [];
+ if (!authorizedEmails.includes(email)) {
+ return new Response(JSON.stringify({
+ error: 'This email is not authorized to use this service'
+ }), {
+ status: 403,
+ headers: { 'Content-Type': 'application/json' }
+ });
+ }
+ // Add to database for future
+ await db.addAuthorizedEmail(email);
+ }
+
+ // Generate secure token
+ const tokenBytes = new Uint8Array(32);
+ crypto.getRandomValues(tokenBytes);
+ const token = Array.from(tokenBytes, b => b.toString(16).padStart(2, '0')).join('');
+
+ // Set expiry to 15 minutes from now
+ const expiresAt = new Date(Date.now() + 15 * 60 * 1000).toISOString();
+
+ // Store token in database
+ await db.createAuthToken(token, email, expiresAt);
+
+ // Send email (mock for now, integrate with email service)
+ const magicLink = `${env.FRONTEND_URL}/auth/verify?token=${token}`;
+
+ // TODO: Integrate with actual email service
+ console.log(`Magic link for ${email}: ${magicLink}`);
+
+ // In production, send actual email
+ if (env.EMAIL_API_KEY) {
+ await sendEmail(env, email, magicLink);
+ }
+
+ return new Response(JSON.stringify({
+ success: true,
+ message: 'Magic link sent to your email'
+ }), {
+ status: 200,
+ headers: { 'Content-Type': 'application/json' }
+ });
+ } catch (error) {
+ console.error('Magic link request error:', error);
+ return new Response(JSON.stringify({
+ error: error.message || 'Failed to send magic link'
+ }), {
+ status: 400,
+ headers: { 'Content-Type': 'application/json' }
+ });
+ }
+ }
+
+ async function verifyMagicLink(request, env) {
+ try {
+ const body = await request.json();
+ const { token } = body;
+
+ if (!token) {
+ throw new ValidationError('Token is required');
+ }
+
+ const db = new Database(env.DB);
+
+ // Get token from database
+ const authToken = await db.getAuthToken(token);
+ if (!authToken) {
+ throw new AuthError('Invalid or expired token');
+ }
+
+ // Mark token as used
+ await db.markTokenUsed(token);
+
+ // Get or create user
+ let user = await db.getUserByEmail(authToken.email);
+ if (!user) {
+ const userId = crypto.randomUUID();
+ await db.createUser(userId, authToken.email);
+ user = { id: userId, email: authToken.email };
+ }
+
+ // Create JWT
+ const jwt = await createJWT({
+ sub: user.id,
+ email: user.email
+ }, env.JWT_SECRET);
+
+ return new Response(JSON.stringify({
+ success: true,
+ token: jwt,
+ user: {
+ id: user.id,
+ email: user.email
+ }
+ }), {
+ status: 200,
+ headers: { 'Content-Type': 'application/json' }
+ });
+ } catch (error) {
+ console.error('Magic link verification error:', error);
+ return new Response(JSON.stringify({
+ error: error.message || 'Failed to verify token'
+ }), {
+ status: error.status || 400,
+ headers: { 'Content-Type': 'application/json' }
+ });
+ }
+ }
+
+ async function logout(request, env) {
+ // Since we're using JWTs, we can't invalidate them server-side
+ // Client should remove the token
+ return new Response(JSON.stringify({
+ success: true,
+ message: 'Logged out successfully'
+ }), {
+ status: 200,
+ headers: { 'Content-Type': 'application/json' }
+ });
+ }
+
+ async function sendEmail(env, email, magicLink) {
+ // Example with Resend API
+ if (env.EMAIL_API_KEY && env.EMAIL_API_KEY.startsWith('re_')) {
+ const response = await fetch('https://api.resend.com/emails', {
+ method: 'POST',
+ headers: {
+ 'Authorization': `Bearer ${env.EMAIL_API_KEY}`,
+ 'Content-Type': 'application/json'
+ },
+ body: JSON.stringify({
+ from: 'QRurl <noreply@qrurl.app>',
+ to: email,
+ subject: 'Your QRurl Login Link',
+ html: `
+ <div style="font-family: sans-serif; max-width: 600px; margin: 0 auto;">
+ <h2>Login to QRurl</h2>
+ <p>Click the link below to log in to your QRurl account:</p>
+ <a href="${magicLink}" style="display: inline-block; padding: 12px 24px; background: #000; color: #fff; text-decoration: none; border-radius: 4px;">
+ Log In
+ </a>
+ <p style="color: #666; font-size: 14px; margin-top: 20px;">
+ This link expires in 15 minutes. If you didn't request this, please ignore this email.
+ </p>
+ </div>
+ `
+ })
+ });
+
+ if (!response.ok) {
+ throw new Error('Failed to send email');
+ }
+ }
+ }
\ No newline at end of file
src/routes/redirect.js +82 -0
@@ @@ -0,0 +1,82 @@
+ import { Database } from '../lib/db';
+ import { NotFoundError } from '../utils/errors';
+
+ export async function handleRedirect(request, env, ctx) {
+ const { slug } = request.params;
+
+ if (!slug) {
+ return new Response('Not Found', { status: 404 });
+ }
+
+ try {
+ const db = new Database(env.DB);
+
+ // Try to get from cache first
+ let entry;
+ if (env.CACHE) {
+ const cached = await env.CACHE.get(`entry:${slug}`, 'json');
+ if (cached) {
+ entry = cached;
+ }
+ }
+
+ // If not in cache, get from database
+ if (!entry) {
+ entry = await db.getEntryBySlug(slug);
+ if (!entry) {
+ return new Response('Not Found', { status: 404 });
+ }
+
+ // Cache for 5 minutes
+ if (env.CACHE) {
+ await env.CACHE.put(`entry:${slug}`, JSON.stringify(entry), {
+ expirationTtl: 300
+ });
+ }
+ }
+
+ // Record analytics asynchronously
+ ctx.waitUntil(recordAnalytics(request, env, entry.id));
+
+ // Increment click count asynchronously
+ ctx.waitUntil(db.incrementClickCount(slug));
+
+ // Redirect to the original URL
+ return Response.redirect(entry.original_url, 301);
+ } catch (error) {
+ console.error('Redirect error:', error);
+ return new Response('Not Found', { status: 404 });
+ }
+ }
+
+ async function recordAnalytics(request, env, entryId) {
+ try {
+ const db = new Database(env.DB);
+
+ // Get client info
+ const ip = request.headers.get('CF-Connecting-IP') || 'unknown';
+ const userAgent = request.headers.get('User-Agent') || 'unknown';
+ const referer = request.headers.get('Referer') || null;
+ const country = request.cf?.country || null;
+ const city = request.cf?.city || null;
+
+ // Hash IP for privacy
+ const encoder = new TextEncoder();
+ const data = encoder.encode(ip + env.JWT_SECRET);
+ const hashBuffer = await crypto.subtle.digest('SHA-256', data);
+ const hashArray = Array.from(new Uint8Array(hashBuffer));
+ const ipHash = hashArray.map(b => b.toString(16).padStart(2, '0')).join('');
+
+ await db.recordAnalytics({
+ entryId,
+ ipHash: ipHash.substring(0, 16), // Use first 16 chars of hash
+ userAgent: userAgent.substring(0, 255),
+ referer: referer ? referer.substring(0, 255) : null,
+ country,
+ city
+ });
+ } catch (error) {
+ console.error('Analytics error:', error);
+ // Don't throw - analytics shouldn't break redirects
+ }
+ }
\ No newline at end of file
src/utils/errors.js +51 -0
@@ @@ -0,0 +1,51 @@
+ export function handleError(error) {
+ console.error('Error:', error);
+
+ const status = error.status || 500;
+ const message = error.message || 'Internal Server Error';
+
+ return new Response(JSON.stringify({
+ error: true,
+ message: message,
+ ...(process.env.NODE_ENV === 'development' && { stack: error.stack })
+ }), {
+ status,
+ headers: { 'Content-Type': 'application/json' }
+ });
+ }
+
+ export class AppError extends Error {
+ constructor(message, status = 500) {
+ super(message);
+ this.status = status;
+ this.name = 'AppError';
+ }
+ }
+
+ export class ValidationError extends AppError {
+ constructor(message) {
+ super(message, 400);
+ this.name = 'ValidationError';
+ }
+ }
+
+ export class AuthError extends AppError {
+ constructor(message = 'Unauthorized') {
+ super(message, 401);
+ this.name = 'AuthError';
+ }
+ }
+
+ export class NotFoundError extends AppError {
+ constructor(message = 'Resource not found') {
+ super(message, 404);
+ this.name = 'NotFoundError';
+ }
+ }
+
+ export class RateLimitError extends AppError {
+ constructor(message = 'Too many requests') {
+ super(message, 429);
+ this.name = 'RateLimitError';
+ }
+ }
\ No newline at end of file
src/utils/slug.js +31 -0
@@ @@ -0,0 +1,31 @@
+ // Custom alphabet without confusing characters (no 0, O, I, l)
+ const ALPHABET = '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghjkmnpqrstuvwxyz';
+
+ export function generateSlug(length = 7) {
+ let slug = '';
+ const randomValues = new Uint8Array(length);
+ crypto.getRandomValues(randomValues);
+
+ for (let i = 0; i < length; i++) {
+ slug += ALPHABET[randomValues[i] % ALPHABET.length];
+ }
+
+ return slug;
+ }
+
+ export function validateCustomSlug(slug) {
+ // Allow alphanumeric and hyphens, 3-50 characters
+ const pattern = /^[a-zA-Z0-9-]{3,50}$/;
+
+ if (!pattern.test(slug)) {
+ throw new Error('Slug must be 3-50 characters and contain only letters, numbers, and hyphens');
+ }
+
+ // Don't allow reserved paths
+ const reserved = ['api', 'health', 'auth', 'admin', 'dashboard', 'login', 'logout'];
+ if (reserved.includes(slug.toLowerCase())) {
+ throw new Error('This slug is reserved');
+ }
+
+ return true;
+ }
\ No newline at end of file
src/utils/validation.js +41 -0
@@ @@ -0,0 +1,41 @@
+ export function validateUrl(url) {
+ try {
+ const parsed = new URL(url);
+
+ // Only allow http and https protocols
+ if (!['http:', 'https:'].includes(parsed.protocol)) {
+ throw new Error('URL must use HTTP or HTTPS protocol');
+ }
+
+ // Prevent javascript: and data: URIs
+ if (url.toLowerCase().includes('javascript:') || url.toLowerCase().includes('data:')) {
+ throw new Error('Invalid URL protocol');
+ }
+
+ return true;
+ } catch (error) {
+ throw new Error('Invalid URL format');
+ }
+ }
+
+ export function validateEmail(email) {
+ const pattern = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
+
+ if (!pattern.test(email)) {
+ throw new Error('Invalid email format');
+ }
+
+ // Basic length check
+ if (email.length > 255) {
+ throw new Error('Email address too long');
+ }
+
+ return true;
+ }
+
+ export function sanitizeInput(input) {
+ if (typeof input !== 'string') return input;
+
+ // Remove any HTML tags
+ return input.replace(/<[^>]*>/g, '').trim();
+ }
\ No newline at end of file
wrangler.toml +41 -0
@@ @@ -0,0 +1,41 @@
+ name = "qrurl"
+ main = "src/index.js"
+ compatibility_date = "2024-12-01"
+ compatibility_flags = ["nodejs_compat"]
+ account_id = "b253e6fbfd2f7757cadd0386de5bde3f"
+
+ # D1 Database
+ [[d1_databases]]
+ binding = "DB"
+ database_name = "qrurl-db"
+ database_id = "17eb6fdb-19da-4ed7-931c-a4cdef281f8c"
+
+ # R2 Storage
+ [[r2_buckets]]
+ binding = "STORAGE"
+ bucket_name = "qrurl-storage"
+
+ # KV Namespace for caching
+ [[kv_namespaces]]
+ binding = "CACHE"
+ id = "1cacb0f1b44b4324b62c1bc010ff15f5"
+ preview_id = "981af79732c84684b54fbbe10aa81f6e"
+
+ # Environment Variables (set these in dashboard or .dev.vars)
+ [vars]
+ JWT_SECRET = ""
+ EMAIL_API_KEY = ""
+ FRONTEND_URL = "http://localhost:3000"
+ BACKEND_URL = "http://localhost:8787"
+
+ # Development settings
+ [dev]
+ port = 8787
+ local_protocol = "http"
+
+ # Rate limiting
+ [[unsafe.bindings]]
+ name = "RATE_LIMITER"
+ type = "ratelimit"
+ namespace_id = "1"
+ simple = { limit = 10, period = 60 }
\ No newline at end of file