Stash

Torey Heinz committed Aug 24, 2025
commit f25eb6b254fcb14969bf99b9ee10b39a52709e38
Showing 60 changed files with 9446 additions and 1110 deletions
.gitignore +18 -1
@@ @@ -18,6 +18,7 @@ dist/
build/
# Logs
+ logs
*.log
npm-debug.log*
@@ @@ -36,4 +37,20 @@ coverage/
# Local database
*.sqlite
- *.sqlite3
\ No newline at end of file
+ *.sqlite3
+
+ # Nuxt dev/build outputs
+ .output
+ .data
+ .nuxt
+ .nitro
+ .cache
+ dist
+
+ # Node dependencies
+ node_modules
+
+ # Local env files
+ .env
+ .env.*
+ !.env.example
.tool-versions +1 -0
@@ @@ -0,0 +1 @@
+ nodejs 20.19.0
NUXT-CLOUDFLARE-DEPLOYMENT-PLAN.md +385 -0
@@ @@ -0,0 +1,385 @@
+ # Complete Plan: QRurl with Nuxt on Cloudflare Workers
+
+ ## Overview
+ Build a URL shortener with QR code generation using Nuxt (latest stable version) deployed to Cloudflare Workers with D1 database, R2 storage, and Postmark email integration.
+
+ ## Architecture Components
+ - **Frontend & Backend**: Single Nuxt application (SSR)
+ - **Database**: Cloudflare D1 (SQLite)
+ - **File Storage**: Cloudflare R2 (for logos)
+ - **Cache**: Cloudflare KV
+ - **Email**: Postmark API
+ - **Deployment**: Cloudflare Workers via Wrangler
+
+ ## Prerequisites
+ - Node.js 20.x or later (stable LTS)
+ - Cloudflare account with Workers, D1, R2, KV enabled
+ - Domain in Cloudflare (qrurl.us)
+ - Postmark account and API key
+ - GitHub account (optional for CI/CD)
+
+ ## Step-by-Step Implementation Plan
+
+ ### Phase 1: Project Setup
+
+ #### 1.1 Initialize Nuxt Project
+ ```bash
+ npx nuxi@latest init qrurl --package-manager npm
+ cd qrurl
+ ```
+
+ #### 1.2 Install Core Dependencies
+ ```bash
+ npm install --save-dev wrangler@latest
+ npm install @nuxt/ui @pinia/nuxt @vueuse/nuxt
+ npm install qrcode jsonwebtoken bcryptjs
+ npm install --save-dev @types/jsonwebtoken @types/bcryptjs
+ ```
+
+ #### 1.3 Configure Nuxt for Cloudflare
+ Create `nuxt.config.ts`:
+ - Set nitro preset to `cloudflare-pages` or `cloudflare-module`
+ - Configure build output for Workers
+ - Set up environment variables
+ - Configure TypeScript
+
+ ### Phase 2: Database Setup
+
+ #### 2.1 Create D1 Database
+ ```bash
+ wrangler d1 create qrurl-db
+ ```
+
+ #### 2.2 Database Schema
+ Create `schema.sql`:
+ ```sql
+ CREATE TABLE links (
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
+ slug TEXT UNIQUE NOT NULL,
+ url TEXT NOT NULL,
+ name TEXT,
+ logo_key TEXT,
+ user_email TEXT,
+ clicks INTEGER DEFAULT 0,
+ created_at DATETIME DEFAULT CURRENT_TIMESTAMP
+ );
+
+ CREATE TABLE auth_tokens (
+ token TEXT PRIMARY KEY,
+ email TEXT NOT NULL,
+ used INTEGER DEFAULT 0,
+ expires_at DATETIME NOT NULL
+ );
+
+ CREATE TABLE sessions (
+ id TEXT PRIMARY KEY,
+ user_email TEXT NOT NULL,
+ expires_at DATETIME NOT NULL
+ );
+
+ CREATE INDEX idx_links_slug ON links(slug);
+ CREATE INDEX idx_sessions_email ON sessions(user_email);
+ ```
+
+ #### 2.3 Initialize Database
+ ```bash
+ wrangler d1 execute qrurl-db --file=./schema.sql --local
+ wrangler d1 execute qrurl-db --file=./schema.sql --remote
+ ```
+
+ ### Phase 3: Cloudflare Resources Setup
+
+ #### 3.1 Create R2 Bucket
+ ```bash
+ wrangler r2 bucket create qrurl-logos
+ ```
+
+ #### 3.2 Create KV Namespace
+ ```bash
+ wrangler kv namespace create cache
+ ```
+
+ #### 3.3 Update wrangler.toml
+ ```toml
+ name = "qrurl"
+ compatibility_date = "2024-12-01"
+ pages_build_output_dir = ".output/public"
+
+ [[d1_databases]]
+ binding = "DB"
+ database_name = "qrurl-db"
+ database_id = "YOUR_DB_ID"
+
+ [[r2_buckets]]
+ binding = "STORAGE"
+ bucket_name = "qrurl-logos"
+
+ [[kv_namespaces]]
+ binding = "CACHE"
+ id = "YOUR_KV_ID"
+
+ [vars]
+ EMAIL_FROM = "noreply@qrurl.us"
+ ```
+
+ ### Phase 4: Application Development
+
+ #### 4.1 Directory Structure
+ ```
+ qrurl/
+ ├── server/
+ │ ├── api/
+ │ │ ├── auth/
+ │ │ │ ├── request.post.ts
+ │ │ │ └── verify.get.ts
+ │ │ ├── links/
+ │ │ │ ├── index.get.ts
+ │ │ │ ├── index.post.ts
+ │ │ │ └── [id].delete.ts
+ │ │ ├── qr/
+ │ │ │ └── [slug].get.ts
+ │ │ └── logo/
+ │ │ ├── upload.post.ts
+ │ │ └── [id].get.ts
+ │ ├── middleware/
+ │ │ └── auth.ts
+ │ └── utils/
+ │ ├── db.ts
+ │ ├── auth.ts
+ │ └── email.ts
+ ├── pages/
+ │ ├── index.vue
+ │ ├── login.vue
+ │ ├── dashboard.vue
+ │ └── [slug].vue
+ ├── components/
+ │ ├── NavBar.vue
+ │ ├── LinkForm.vue
+ │ ├── LinkList.vue
+ │ ├── QRCodeModal.vue
+ │ └── LogoUploader.vue
+ ├── stores/
+ │ └── auth.ts
+ ├── composables/
+ │ └── useApi.ts
+ └── public/
+ ```
+
+ #### 4.2 Server API Implementation
+
+ **Database Utils** (`server/utils/db.ts`):
+ - Direct D1 binding access
+ - Query builders
+ - Migration helpers
+
+ **Auth Utils** (`server/utils/auth.ts`):
+ - JWT token generation/verification
+ - Session management
+ - Cookie handling
+
+ **Email Utils** (`server/utils/email.ts`):
+ - Postmark integration
+ - Magic link generation
+ - Email templates
+
+ #### 4.3 Frontend Implementation
+
+ **Pages**:
+ - `index.vue`: Public URL shortener
+ - `login.vue`: Magic link request
+ - `dashboard.vue`: Authenticated link management
+ - `[slug].vue`: Redirect handler
+
+ **Components**:
+ - Form validation
+ - Real-time updates
+ - QR code generation with logo overlay
+ - File upload to R2
+
+ **State Management** (Pinia):
+ - Auth store
+ - Links store
+ - UI store
+
+ ### Phase 5: Authentication Flow
+
+ 1. User enters email on login page
+ 2. Server validates email against whitelist
+ 3. Generate magic link token, store in D1
+ 4. Send email via Postmark
+ 5. User clicks link
+ 6. Verify token, create session
+ 7. Set HTTP-only cookie
+ 8. Redirect to dashboard
+
+ ### Phase 6: Core Features
+
+ #### 6.1 URL Shortening
+ - Generate random slug or accept custom
+ - Validate URL format
+ - Check slug uniqueness
+ - Store in D1 with metadata
+
+ #### 6.2 QR Code Generation
+ - Use qrcode library
+ - High error correction for logo overlay
+ - Return as base64 or binary
+ - Cache in KV for performance
+
+ #### 6.3 Logo Upload
+ - Accept image upload
+ - Validate file type/size
+ - Store in R2 with unique key
+ - Reference in link record
+
+ #### 6.4 Analytics
+ - Track clicks in D1
+ - Store user agent, referrer
+ - Display in dashboard
+ - Export functionality
+
+ ### Phase 7: Environment Configuration
+
+ #### 7.1 Development (.env)
+ ```env
+ NUXT_JWT_SECRET=dev-secret
+ NUXT_EMAIL_API_KEY=your-postmark-key
+ NUXT_AUTHORIZED_EMAILS=email1@example.com,email2@example.com
+ ```
+
+ #### 7.2 Production Secrets
+ ```bash
+ wrangler secret put JWT_SECRET
+ wrangler secret put EMAIL_API_KEY
+ wrangler secret put AUTHORIZED_EMAILS
+ ```
+
+ ### Phase 8: Deployment
+
+ #### 8.1 Build Process
+ ```bash
+ npm run build
+ ```
+
+ #### 8.2 Deploy to Cloudflare
+ ```bash
+ wrangler pages deploy .output/public
+ ```
+
+ #### 8.3 Configure Custom Domain
+ 1. Cloudflare Dashboard → Pages → Custom domains
+ 2. Add qrurl.us
+ 3. Configure DNS if needed
+
+ ### Phase 9: Testing & Optimization
+
+ #### 9.1 Local Testing
+ ```bash
+ npm run dev # Development server
+ npm run preview # Production preview
+ ```
+
+ #### 9.2 Performance Optimization
+ - Implement caching strategies
+ - Optimize database queries
+ - Compress assets
+ - Lazy load components
+
+ #### 9.3 Security
+ - Rate limiting
+ - Input validation
+ - CSRF protection
+ - Content Security Policy
+
+ ### Phase 10: CI/CD Setup (Optional)
+
+ #### 10.1 GitHub Actions
+ ```yaml
+ name: Deploy
+ on:
+ push:
+ branches: [main]
+ jobs:
+ deploy:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v3
+ - uses: actions/setup-node@v3
+ - run: npm ci
+ - run: npm run build
+ - uses: cloudflare/wrangler-action@v3
+ with:
+ apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
+ ```
+
+ ## Common Issues & Solutions
+
+ ### Issue 1: EBADF Errors
+ - Use Node.js LTS (20.x)
+ - Avoid Node.js 23.x
+ - Check file descriptor limits
+
+ ### Issue 2: D1 Binding Issues
+ - Ensure database ID matches
+ - Check wrangler.toml configuration
+ - Verify local vs remote execution
+
+ ### Issue 3: CORS Problems
+ - Not needed with unified deployment
+ - Everything on same domain
+
+ ### Issue 4: Build Failures
+ - Clear .nuxt and node_modules
+ - Reinstall dependencies
+ - Check TypeScript errors
+
+ ## Key Differences from Framework-Heavy Approach
+
+ 1. **Simpler Structure**: Single deployment unit
+ 2. **No CORS**: API and frontend on same domain
+ 3. **Direct Bindings**: Use Cloudflare resources directly
+ 4. **Better Performance**: Edge-optimized
+ 5. **Easier Debugging**: Unified logs
+
+ ## Testing Checklist
+
+ - [ ] Homepage loads
+ - [ ] URL shortening works
+ - [ ] QR codes generate
+ - [ ] Redirects work
+ - [ ] Login flow completes
+ - [ ] Dashboard accessible
+ - [ ] Logo upload works
+ - [ ] Analytics track
+ - [ ] Session persistence
+ - [ ] Logout works
+
+ ## Production Checklist
+
+ - [ ] Database migrated
+ - [ ] Secrets configured
+ - [ ] Custom domain active
+ - [ ] SSL working
+ - [ ] Email sending
+ - [ ] Error handling
+ - [ ] Monitoring setup
+ - [ ] Backup strategy
+
+ ## Estimated Timeline
+
+ - **Phase 1-3**: 1 hour (setup)
+ - **Phase 4-6**: 4-6 hours (development)
+ - **Phase 7-8**: 1 hour (deployment)
+ - **Phase 9-10**: 2 hours (testing/optimization)
+
+ **Total**: 8-10 hours for complete implementation
+
+ ## Success Criteria
+
+ 1. App deploys to qrurl.us
+ 2. All features from original app work
+ 3. No framework complexity
+ 4. Fast performance (<100ms response)
+ 5. Reliable email delivery
+ 6. Secure authentication
+ 7. Clean, maintainable code
\ No newline at end of file
NUXT-MIGRATION-PLAN.md +267 -0
@@ @@ -0,0 +1,267 @@
+ # Migrating QRurl to Nuxt 3 with NuxtFlare
+
+ ## Why Nuxt 3 + NuxtFlare?
+
+ ### Current Architecture Problems:
+ - Separate frontend (Vue) and backend (Workers) codebases
+ - Complex CORS configuration
+ - Manual static file serving
+ - Two different routing systems
+ - Complicated deployment process
+
+ ### Nuxt 3 + NuxtFlare Benefits:
+ - **Single codebase** - Frontend and backend unified
+ - **SSR on Workers** - Better SEO and performance
+ - **Built-in API routes** - `/server/api/*` routes
+ - **Direct Cloudflare bindings** - D1, R2, KV without wrangler
+ - **Zero CORS issues** - Everything on same origin
+ - **Simple deployment** - `npm run deploy`
+ - **Hot module replacement** - Full-stack HMR in development
+ - **TypeScript support** - End-to-end type safety
+
+ ## Migration Strategy
+
+ ### Phase 1: Setup Nuxt 3 Project
+
+ ```bash
+ # Create new Nuxt 3 app
+ npx nuxi@latest init qrurl-nuxt
+
+ # Install dependencies
+ cd qrurl-nuxt
+ npm install
+
+ # Add NuxtFlare module
+ npm install @nuxflare/core
+ ```
+
+ ### Phase 2: Configure NuxtFlare
+
+ ```typescript
+ // nuxt.config.ts
+ export default defineNuxtConfig({
+ modules: ['@nuxflare/core'],
+
+ nitro: {
+ preset: 'cloudflare-pages',
+
+ // Cloudflare bindings
+ cloudflare: {
+ bindings: {
+ DB: {
+ type: 'd1',
+ databaseName: 'qrurl-db',
+ databaseId: '17eb6fdb-19da-4ed7-931c-a4cdef281f8c'
+ },
+ STORAGE: {
+ type: 'r2',
+ bucketName: 'qrurl-storage'
+ },
+ CACHE: {
+ type: 'kv',
+ namespaceId: '1cacb0f1b44b4324b62c1bc010ff15f5'
+ }
+ }
+ }
+ }
+ })
+ ```
+
+ ### Phase 3: Migrate Components
+
+ Move Vue components to Nuxt structure:
+ ```
+ frontend/src/components/* → components/
+ frontend/src/views/* → pages/
+ frontend/src/stores/* → stores/ (with Pinia)
+ ```
+
+ ### Phase 4: Convert API Routes
+
+ Transform Workers routes to Nuxt server routes:
+
+ **Before (Workers):**
+ ```javascript
+ // src/routes/api.js
+ export async function apiRoutes(request, env, ctx) {
+ if (path === '/api/links') {
+ return getLinks(request, env);
+ }
+ }
+ ```
+
+ **After (Nuxt):**
+ ```typescript
+ // server/api/links.get.ts
+ export default defineEventHandler(async (event) => {
+ const db = useD1Database('DB', event);
+ return await db.prepare('SELECT * FROM links').all();
+ })
+ ```
+
+ ### Phase 5: Authentication with Nuxt
+
+ ```typescript
+ // server/api/auth/request.post.ts
+ import { usePostmark } from '#nuxflare/postmark';
+
+ export default defineEventHandler(async (event) => {
+ const { email } = await readBody(event);
+ const postmark = usePostmark(event);
+
+ // Generate magic link
+ const token = generateToken();
+
+ // Store in D1
+ const db = useD1Database('DB', event);
+ await db.prepare('INSERT INTO auth_tokens...').run();
+
+ // Send email
+ await postmark.sendEmail({
+ From: 'noreply@qrurl.us',
+ To: email,
+ Subject: 'Your QRurl Login Link',
+ HtmlBody: `...`
+ });
+
+ return { success: true };
+ })
+ ```
+
+ ### Phase 6: File Uploads with R2
+
+ ```typescript
+ // server/api/logo/upload.post.ts
+ export default defineEventHandler(async (event) => {
+ const form = await readFormData(event);
+ const file = form.get('logo') as File;
+
+ const r2 = useR2Bucket('STORAGE', event);
+ await r2.put(`logos/${file.name}`, file);
+
+ return { url: `/api/logo/${file.name}` };
+ })
+ ```
+
+ ## File Structure (Nuxt)
+
+ ```
+ qrurl-nuxt/
+ ├── server/
+ │ ├── api/
+ │ │ ├── auth/
+ │ │ │ ├── request.post.ts
+ │ │ │ └── verify.post.ts
+ │ │ ├── links/
+ │ │ │ ├── index.get.ts
+ │ │ │ ├── index.post.ts
+ │ │ │ └── [id].delete.ts
+ │ │ └── logo/
+ │ │ ├── upload.post.ts
+ │ │ └── [id].get.ts
+ │ └── routes/
+ │ └── [slug].ts # Short link redirects
+ ├── pages/
+ │ ├── index.vue # Homepage
+ │ ├── dashboard.vue # Dashboard
+ │ └── auth/
+ │ └── verify.vue # Magic link verification
+ ├── components/
+ │ ├── NavBar.vue
+ │ ├── CreateLinkForm.vue
+ │ ├── QRCodeGenerator.vue
+ │ └── LogoUploader.vue
+ ├── stores/
+ │ └── auth.ts # Pinia store
+ ├── nuxt.config.ts
+ └── wrangler.toml # Minimal, just for secrets
+
+ ```
+
+ ## Deployment (Simplified!)
+
+ ### Development:
+ ```bash
+ npm run dev
+ # That's it! Full-stack hot reload
+ ```
+
+ ### Production:
+ ```bash
+ npm run build
+ npm run deploy
+ # Deploys everything to Cloudflare Workers/Pages
+ ```
+
+ ## Benefits Over Current Architecture
+
+ | Feature | Current (Vue + Workers) | Nuxt 3 + NuxtFlare |
+ |---------|------------------------|-------------------|
+ | **Codebase** | 2 separate | 1 unified |
+ | **Routing** | Vue Router + Workers Router | Nuxt file-based routing |
+ | **API** | Manual Workers setup | Built-in server routes |
+ | **SSR** | No (SPA only) | Yes (better SEO) |
+ | **Development** | 2 servers + CORS | 1 server, no CORS |
+ | **Type Safety** | Partial | Full-stack TypeScript |
+ | **Deployment** | Complex scripts | Single command |
+ | **HMR** | Frontend only | Full-stack |
+ | **Data Fetching** | Manual fetch + CORS | `$fetch` with auto-typing |
+
+ ## Migration Timeline
+
+ 1. **Week 1**: Set up Nuxt project, migrate components
+ 2. **Week 2**: Convert API routes to server routes
+ 3. **Week 3**: Integrate D1, R2, KV with NuxtFlare
+ 4. **Week 4**: Testing and deployment optimization
+
+ ## Example: Complete Link Creation Flow
+
+ ```vue
+ <!-- pages/dashboard.vue -->
+ <template>
+ <form @submit.prevent="createLink">
+ <input v-model="url" />
+ <button type="submit">Shorten</button>
+ </form>
+ </template>
+
+ <script setup>
+ const url = ref('');
+
+ async function createLink() {
+ // No CORS, no API URL config needed!
+ const { slug } = await $fetch('/api/links', {
+ method: 'POST',
+ body: { url: url.value }
+ });
+
+ // Redirect to success page
+ await navigateTo(`/link/${slug}`);
+ }
+ </script>
+ ```
+
+ ```typescript
+ // server/api/links.post.ts
+ export default defineEventHandler(async (event) => {
+ const { url } = await readBody(event);
+ const db = useD1Database('DB', event);
+
+ const slug = generateSlug();
+ await db.prepare(
+ 'INSERT INTO links (slug, url) VALUES (?, ?)'
+ ).bind(slug, url).run();
+
+ return { slug };
+ })
+ ```
+
+ ## Conclusion
+
+ Migrating to Nuxt 3 with NuxtFlare would:
+ - **Eliminate complexity** - No more CORS, separate deployments, or complex configs
+ - **Improve developer experience** - Single codebase, full-stack HMR
+ - **Enhance performance** - SSR, edge rendering, better caching
+ - **Simplify deployment** - One command to deploy everything
+
+ The migration effort would be worth it for the massive simplification and improved developer experience.
\ No newline at end of file
README.md +42 -168
@@ @@ -1,201 +1,75 @@
- # QRurl - URL Shortener with QR Codes
+ # Nuxt Minimal Starter
- A modern, serverless URL shortener with QR code generation built on Cloudflare Workers, D1, and R2.
+ Look at the [Nuxt documentation](https://nuxt.com/docs/getting-started/introduction) to learn more.
- ## Features
+ ## Setup
- - 🔗 **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
+ Make sure to install dependencies:
- ## 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
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
- ```
+ # pnpm
+ pnpm install
- 6. Initialize the database:
- ```bash
- # Local database
- npm run db:init
+ # yarn
+ yarn install
- # Remote database (production)
- npm run db:init:remote
+ # bun
+ bun install
```
- ### Development
+ ## Development Server
- 1. Start the backend (Cloudflare Workers):
- ```bash
- npm run dev
- # Backend runs on http://localhost:8787
- ```
+ Start the development server on `http://localhost:3000`:
- 2. In a new terminal, start the frontend:
```bash
- cd frontend
+ # npm
npm run dev
- # Frontend runs on http://localhost:3000
- ```
-
- 3. Open http://localhost:3000 in your browser
- ### API Endpoints
+ # pnpm
+ pnpm dev
- #### Authentication
- - `POST /api/auth/request` - Request magic link
- - `POST /api/auth/verify` - Verify magic link token
- - `POST /api/auth/logout` - Logout
+ # yarn
+ yarn dev
- #### 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
+ # bun
+ bun run dev
+ ```
- ### Deployment
+ ## Production
- 1. Deploy the backend to Cloudflare Workers:
- ```bash
- npm run deploy
- ```
+ Build the application for production:
- 2. Build and deploy the frontend:
```bash
- cd frontend
+ # npm
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
+ # pnpm
+ pnpm build
- ## Project Structure
+ # yarn
+ yarn build
+ # bun
+ bun run build
```
- 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
+ Locally preview production build:
- ## 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
+ ```bash
+ # npm
+ npm run preview
- ## Contributing
+ # pnpm
+ pnpm preview
- Pull requests are welcome! Please read the contributing guidelines first.
+ # yarn
+ yarn preview
- ## Support
+ # bun
+ bun run preview
+ ```
- For issues and questions, please use the GitHub issues page.
\ No newline at end of file
+ Check out the [deployment documentation](https://nuxt.com/docs/getting-started/deployment) for more information.
app/app.vue +6 -0
@@ @@ -0,0 +1,6 @@
+ <template>
+ <div>
+ <NuxtRouteAnnouncer />
+ <NuxtWelcome />
+ </div>
+ </template>
backup/frontend/.env.example +2 -0
@@ @@ -0,0 +1,2 @@
+ VITE_API_URL=http://localhost:8787/api
+ VITE_SHORT_URL=http://localhost:8787
\ No newline at end of file
backup/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).
backup/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>
backup/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"
+ }
+ }
+ }
+ }
backup/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"
+ }
+ }
backup/frontend/postcss.config.js +6 -0
@@ @@ -0,0 +1,6 @@
+ export default {
+ plugins: {
+ tailwindcss: {},
+ autoprefixer: {},
+ },
+ }
\ No newline at end of file
backup/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
backup/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
backup/frontend/src/components/EditEntryModal.vue +144 -0
@@ @@ -0,0 +1,144 @@
+ <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-2xl w-full p-6 max-h-[90vh] overflow-y-auto">
+ <div class="flex justify-between items-start mb-6">
+ <h3 class="text-xl font-semibold text-gray-900">Edit Link</h3>
+ <button
+ @click="$emit('close')"
+ class="text-gray-400 hover:text-gray-600 transition"
+ >
+ <XIcon class="h-6 w-6" />
+ </button>
+ </div>
+
+ <form @submit.prevent="handleSubmit" 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="formData.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">
+ Slug (cannot be changed)
+ </label>
+ <input
+ :value="entry.slug"
+ type="text"
+ disabled
+ class="w-full px-3 py-2 border border-gray-300 rounded-lg bg-gray-50 text-gray-500"
+ />
+ </div>
+ </div>
+
+ <div>
+ <label class="block text-sm font-medium text-gray-700 mb-1">
+ Destination URL
+ </label>
+ <input
+ v-model="formData.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>
+
+ <!-- Logo Management -->
+ <div class="border-t pt-4">
+ <h4 class="text-sm font-medium text-gray-700 mb-3">Logo for QR Code</h4>
+ <LogoUploader
+ :entry="entry"
+ @update="handleLogoUpdate"
+ />
+ </div>
+
+ <div class="flex justify-end space-x-3 pt-4 border-t">
+ <button
+ type="button"
+ @click="$emit('close')"
+ class="px-4 py-2 text-gray-700 bg-gray-100 rounded-lg hover:bg-gray-200 transition"
+ >
+ Cancel
+ </button>
+ <button
+ type="submit"
+ :disabled="saving"
+ class="px-4 py-2 bg-indigo-600 text-white rounded-lg hover:bg-indigo-700 transition disabled:opacity-50"
+ >
+ {{ saving ? 'Saving...' : 'Save Changes' }}
+ </button>
+ </div>
+ </form>
+ </div>
+ </div>
+ </template>
+
+ <script setup>
+ import { ref, reactive, onMounted } from 'vue'
+ import { X as XIcon } from 'lucide-vue-next'
+ import { useEntriesStore } from '../stores/entries'
+ import LogoUploader from './LogoUploader.vue'
+
+ const props = defineProps({
+ entry: {
+ type: Object,
+ required: true
+ }
+ })
+
+ const emit = defineEmits(['close', 'update'])
+
+ const entriesStore = useEntriesStore()
+ const saving = ref(false)
+
+ const formData = reactive({
+ name: '',
+ url: ''
+ })
+
+ onMounted(() => {
+ // Initialize form with current values
+ formData.name = props.entry.name
+ formData.url = props.entry.original_url || props.entry.url
+ })
+
+ async function handleSubmit() {
+ saving.value = true
+ try {
+ await entriesStore.updateEntry(props.entry.id, {
+ name: formData.name,
+ url: formData.url
+ })
+
+ // Update local entry
+ const updatedEntry = {
+ ...props.entry,
+ name: formData.name,
+ original_url: formData.url
+ }
+
+ emit('update', updatedEntry)
+ emit('close')
+
+ alert('Link updated successfully!')
+ } catch (error) {
+ alert(error.response?.data?.error || 'Failed to update link')
+ } finally {
+ saving.value = false
+ }
+ }
+
+ function handleLogoUpdate(updatedEntry) {
+ // Update the entry with new logo information
+ emit('update', updatedEntry)
+ }
+ </script>
\ No newline at end of file
backup/frontend/src/components/LogoUploader.vue +182 -0
@@ @@ -0,0 +1,182 @@
+ <template>
+ <div class="space-y-4">
+ <div v-if="entry.logo_url" class="space-y-3">
+ <div class="flex items-center space-x-4">
+ <img
+ :src="getLogoUrl(entry.logo_url)"
+ alt="Logo"
+ class="w-20 h-20 object-contain border rounded-lg p-2"
+ />
+ <div class="flex-1">
+ <p class="text-sm text-gray-600 mb-2">Current logo</p>
+ <button
+ @click="deleteLogo"
+ :disabled="deleting"
+ class="text-red-600 hover:text-red-700 text-sm font-medium"
+ >
+ {{ deleting ? 'Deleting...' : 'Remove Logo' }}
+ </button>
+ </div>
+ </div>
+ </div>
+
+ <div>
+ <label class="block text-sm font-medium text-gray-700 mb-1">
+ {{ entry.logo_url ? 'Replace Logo' : 'Add Logo' }}
+ </label>
+ <input
+ type="file"
+ ref="fileInput"
+ accept="image/png,image/jpeg,image/svg+xml,image/webp"
+ @change="handleFileSelect"
+ class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500 file:mr-4 file:py-2 file:px-4 file:rounded-lg file:border-0 file:text-sm file:font-semibold file:bg-indigo-50 file:text-indigo-700 hover:file:bg-indigo-100"
+ />
+ <p class="text-xs text-gray-500 mt-1">
+ PNG, JPG, SVG, or WebP. Max 5MB.
+ </p>
+ </div>
+
+ <div v-if="selectedFile" class="flex justify-end space-x-3">
+ <button
+ @click="cancelUpload"
+ class="px-4 py-2 text-gray-700 bg-gray-100 rounded-lg hover:bg-gray-200 transition"
+ >
+ Cancel
+ </button>
+ <button
+ @click="uploadLogo"
+ :disabled="uploading"
+ class="px-4 py-2 bg-indigo-600 text-white rounded-lg hover:bg-indigo-700 transition disabled:opacity-50"
+ >
+ {{ uploading ? 'Uploading...' : 'Upload Logo' }}
+ </button>
+ </div>
+ </div>
+ </template>
+
+ <script setup>
+ import { ref } from 'vue'
+ import { useAuthStore } from '../stores/auth'
+
+ const props = defineProps({
+ entry: {
+ type: Object,
+ required: true
+ }
+ })
+
+ const emit = defineEmits(['update'])
+
+ const authStore = useAuthStore()
+ const fileInput = ref(null)
+ const selectedFile = ref(null)
+ const uploading = ref(false)
+ const deleting = ref(false)
+
+ function getLogoUrl(logoUrl) {
+ // If it's a relative URL, prepend the backend URL
+ if (logoUrl?.startsWith('/')) {
+ return `${import.meta.env.VITE_API_URL?.replace('/api', '') || 'http://localhost:8787'}${logoUrl}`
+ }
+ return logoUrl
+ }
+
+ function handleFileSelect(event) {
+ const file = event.target.files[0]
+ if (file) {
+ // Validate file
+ const maxSize = 5 * 1024 * 1024 // 5MB
+ if (file.size > maxSize) {
+ alert('File too large. Maximum size: 5MB')
+ event.target.value = ''
+ return
+ }
+
+ const allowedTypes = ['image/png', 'image/jpeg', 'image/svg+xml', 'image/webp']
+ if (!allowedTypes.includes(file.type)) {
+ alert('Invalid file type. Allowed: PNG, JPG, SVG, WebP')
+ event.target.value = ''
+ return
+ }
+
+ selectedFile.value = file
+ }
+ }
+
+ function cancelUpload() {
+ selectedFile.value = null
+ if (fileInput.value) {
+ fileInput.value.value = ''
+ }
+ }
+
+ async function uploadLogo() {
+ if (!selectedFile.value) return
+
+ uploading.value = true
+ const formData = new FormData()
+ formData.append('logo', selectedFile.value)
+ formData.append('entryId', props.entry.id)
+
+ try {
+ const response = await fetch('/api/logo/upload', {
+ method: 'POST',
+ headers: {
+ 'Authorization': `Bearer ${authStore.token}`
+ },
+ body: formData
+ })
+
+ if (!response.ok) {
+ const error = await response.json()
+ throw new Error(error.error || 'Failed to upload logo')
+ }
+
+ const data = await response.json()
+
+ // Emit update event with new logo URL
+ emit('update', { ...props.entry, logo_url: data.logoUrl })
+
+ // Reset
+ selectedFile.value = null
+ if (fileInput.value) {
+ fileInput.value.value = ''
+ }
+
+ alert('Logo uploaded successfully!')
+ } catch (error) {
+ alert(error.message || 'Failed to upload logo')
+ } finally {
+ uploading.value = false
+ }
+ }
+
+ async function deleteLogo() {
+ if (!confirm('Are you sure you want to remove the logo?')) return
+
+ deleting.value = true
+
+ try {
+ const response = await fetch(`/api/logo/delete/${props.entry.id}`, {
+ method: 'DELETE',
+ headers: {
+ 'Authorization': `Bearer ${authStore.token}`
+ }
+ })
+
+ if (!response.ok) {
+ const error = await response.json()
+ throw new Error(error.error || 'Failed to delete logo')
+ }
+
+ // Emit update event with no logo
+ emit('update', { ...props.entry, logo_url: null })
+
+ alert('Logo removed successfully!')
+ } catch (error) {
+ alert(error.message || 'Failed to delete logo')
+ } finally {
+ deleting.value = false
+ }
+ }
+ </script>
\ No newline at end of file
backup/frontend/src/components/QRCodeModal.vue +119 -0
@@ @@ -0,0 +1,119 @@
+ <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 {
+ // Get full logo URL if logo exists
+ const logoUrl = props.entry.logo_url ? getFullLogoUrl(props.entry.logo_url) : null
+ qrCodeUrl.value = await generateQRCodeWithLogo(shortUrl.value, logoUrl)
+ } 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 getFullLogoUrl(logoUrl) {
+ // If it's a relative URL starting with /api/logo, prepend the backend URL
+ if (logoUrl?.startsWith('/api/logo')) {
+ return `${import.meta.env.VITE_API_URL?.replace('/api', '') || 'http://localhost:8787'}${logoUrl}`
+ }
+ return logoUrl
+ }
+
+ 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
backup/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
backup/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
backup/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
backup/frontend/src/services/qrcode.js +100 -0
@@ @@ -0,0 +1,100 @@
+ 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 = {}) {
+ console.log('Generating QR code with logo:', logoUrl)
+
+ 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) {
+ // Fetch logo as blob to avoid CORS issues
+ fetch(logoUrl)
+ .then(response => {
+ if (!response.ok) throw new Error('Failed to fetch logo')
+ return response.blob()
+ })
+ .then(blob => {
+ const logo = new Image()
+ const objectUrl = URL.createObjectURL(blob)
+
+ logo.onload = () => {
+ // Calculate logo size (max 25% of QR code for better scanning)
+ const logoSize = Math.floor(canvas.width * 0.25)
+ const logoX = (canvas.width - logoSize) / 2
+ const logoY = (canvas.height - logoSize) / 2
+
+ // Draw white background with rounded corners
+ ctx.fillStyle = 'white'
+ const padding = 8
+ ctx.fillRect(logoX - padding, logoY - padding, logoSize + padding * 2, logoSize + padding * 2)
+
+ // Draw logo
+ ctx.drawImage(logo, logoX, logoY, logoSize, logoSize)
+
+ // Clean up
+ URL.revokeObjectURL(objectUrl)
+
+ resolve(canvas.toDataURL())
+ }
+
+ logo.onerror = () => {
+ console.error('Failed to load logo image')
+ URL.revokeObjectURL(objectUrl)
+ resolve(qrDataUrl)
+ }
+
+ logo.src = objectUrl
+ })
+ .catch(error => {
+ console.error('Failed to fetch logo:', error)
+ resolve(qrDataUrl)
+ })
+ } else {
+ resolve(qrDataUrl)
+ }
+ }
+
+ qrImage.onerror = reject
+ })
+ }
\ No newline at end of file
backup/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
backup/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
backup/frontend/src/style.css +3 -0
@@ @@ -0,0 +1,3 @@
+ @tailwind base;
+ @tailwind components;
+ @tailwind utilities;
\ No newline at end of file
backup/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
backup/frontend/src/views/Dashboard.vue +379 -0
@@ @@ -0,0 +1,379 @@
+ <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>
+ <label class="block text-sm font-medium text-gray-700 mb-1">
+ Logo for QR Code (optional)
+ </label>
+ <input
+ type="file"
+ ref="logoFileInput"
+ accept="image/png,image/jpeg,image/svg+xml,image/webp"
+ @change="handleLogoSelect"
+ class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500 file:mr-4 file:py-2 file:px-4 file:rounded-lg file:border-0 file:text-sm file:font-semibold file:bg-indigo-50 file:text-indigo-700 hover:file:bg-indigo-100"
+ />
+ <p class="text-xs text-gray-500 mt-1">
+ PNG, JPG, SVG, or WebP. Max 5MB. Will be embedded in QR code center.
+ </p>
+ </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="editEntry(entry)"
+ class="p-2 text-gray-400 hover:text-gray-600 transition"
+ title="Edit"
+ >
+ <EditIcon class="h-5 w-5" />
+ </button>
+ <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"
+ />
+
+ <!-- Edit Entry Modal -->
+ <EditEntryModal
+ v-if="editingEntry"
+ :entry="editingEntry"
+ @close="editingEntry = null"
+ @update="handleEntryUpdate"
+ />
+ </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,
+ Edit as EditIcon
+ } from 'lucide-vue-next'
+ import { useAuthStore } from '../stores/auth'
+ import { useEntriesStore } from '../stores/entries'
+ import QRCodeModal from '../components/QRCodeModal.vue'
+ import EditEntryModal from '../components/EditEntryModal.vue'
+
+ const router = useRouter()
+ const authStore = useAuthStore()
+ const entriesStore = useEntriesStore()
+
+ const loading = ref(true)
+ const creating = ref(false)
+ const selectedEntry = ref(null)
+ const editingEntry = ref(null)
+ const logoFileInput = ref(null)
+ const selectedLogo = 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 {
+ // First create the entry
+ const entry = await entriesStore.createEntry(newEntry.value)
+
+ // Then upload logo if selected
+ if (selectedLogo.value && entry) {
+ await uploadLogo(entry.id)
+ }
+
+ // Reset form
+ newEntry.value = { name: '', url: '', customSlug: '' }
+ selectedLogo.value = null
+ if (logoFileInput.value) {
+ logoFileInput.value.value = ''
+ }
+ } catch (error) {
+ alert(error.response?.data?.error || 'Failed to create link')
+ } finally {
+ creating.value = false
+ }
+ }
+
+ function handleLogoSelect(event) {
+ const file = event.target.files[0]
+ if (file) {
+ // Validate file
+ const maxSize = 5 * 1024 * 1024 // 5MB
+ if (file.size > maxSize) {
+ alert('File too large. Maximum size: 5MB')
+ event.target.value = ''
+ return
+ }
+
+ const allowedTypes = ['image/png', 'image/jpeg', 'image/svg+xml', 'image/webp']
+ if (!allowedTypes.includes(file.type)) {
+ alert('Invalid file type. Allowed: PNG, JPG, SVG, WebP')
+ event.target.value = ''
+ return
+ }
+
+ selectedLogo.value = file
+ }
+ }
+
+ async function uploadLogo(entryId) {
+ if (!selectedLogo.value) return
+
+ const formData = new FormData()
+ formData.append('logo', selectedLogo.value)
+ formData.append('entryId', entryId)
+
+ try {
+ const response = await fetch('/api/logo/upload', {
+ method: 'POST',
+ headers: {
+ 'Authorization': `Bearer ${authStore.token}`
+ },
+ body: formData
+ })
+
+ if (!response.ok) {
+ throw new Error('Failed to upload logo')
+ }
+
+ const data = await response.json()
+
+ // Update the entry in the store with the logo URL
+ const entryIndex = entriesStore.entries.findIndex(e => e.id === entryId)
+ if (entryIndex !== -1) {
+ entriesStore.entries[entryIndex].logo_url = data.logoUrl
+ }
+ } catch (error) {
+ console.error('Logo upload error:', error)
+ // Don't throw - logo upload failure shouldn't break the flow
+ }
+ }
+
+ 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 editEntry(entry) {
+ editingEntry.value = entry
+ }
+
+ function handleEntryUpdate(updatedEntry) {
+ // Update the entry in the store
+ const index = entriesStore.entries.findIndex(e => e.id === updatedEntry.id)
+ if (index !== -1) {
+ entriesStore.entries[index] = updatedEntry
+ }
+
+ // Update selected entry if it's the same one
+ if (selectedEntry.value?.id === updatedEntry.id) {
+ selectedEntry.value = updatedEntry
+ }
+ }
+
+ 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
backup/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
backup/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
backup/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
backup/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
backup/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,
+ }
+ }
+ }
+ })
backup/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
backup/src/index.js +88 -0
@@ @@ -0,0 +1,88 @@
+ import { Router } from './router';
+ import { handleRedirect } from './routes/redirect';
+ import { authRoutes } from './routes/auth';
+ import { apiRoutes } from './routes/api';
+ import { logoRoutes } from './routes/logo';
+ import { applyMiddleware } from './middleware';
+ import { handleError } from './utils/errors';
+ import { addCorsHeaders } from './middleware/cors';
+ import { serveStaticAsset } from './lib/static';
+
+ export default {
+ async fetch(request, env, ctx) {
+ try {
+ // Handle OPTIONS 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',
+ }
+ });
+ }
+
+ 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/logo/*', (request, env, ctx) =>
+ applyMiddleware(request, env, ctx, logoRoutes)
+ );
+ router.all('/api/*', (request, env, ctx) =>
+ applyMiddleware(request, env, ctx, apiRoutes)
+ );
+
+ // Try to serve static assets first (for frontend)
+ router.all('*', async (request, env, ctx) => {
+ const url = new URL(request.url);
+
+ // Skip API routes and health check
+ if (url.pathname.startsWith('/api') || url.pathname === '/health') {
+ return null; // Let it fall through to 404
+ }
+
+ // Try serving static asset
+ const staticResponse = await serveStaticAsset(request, env, ctx);
+ if (staticResponse) {
+ return staticResponse;
+ }
+
+ // Try URL redirect (for short links)
+ if (url.pathname.length > 1 && !url.pathname.includes('.')) {
+ return handleRedirect(request, env, ctx);
+ }
+
+ // Default 404 for everything else
+ return new Response(JSON.stringify({
+ error: 'Not Found',
+ message: 'The requested resource was not found'
+ }), {
+ status: 404,
+ headers: { 'Content-Type': 'application/json' }
+ });
+ });
+
+ const response = await router.handle(request, env, ctx);
+
+ // Add CORS headers to all responses
+ return addCorsHeaders(response, env);
+ } catch (error) {
+ const errorResponse = handleError(error);
+ return addCorsHeaders(errorResponse, env);
+ }
+ }
+ };
\ No newline at end of file
db.js b/backup/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
static.js b/backup/src/lib/static.js +71 -0
@@ @@ -0,0 +1,71 @@
+ // Static file serving for frontend
+ import { getAssetFromKV } from '@cloudflare/kv-asset-handler';
+
+ const MIME_TYPES = {
+ 'html': 'text/html',
+ 'js': 'application/javascript',
+ 'css': 'text/css',
+ 'png': 'image/png',
+ 'jpg': 'image/jpeg',
+ 'jpeg': 'image/jpeg',
+ 'svg': 'image/svg+xml',
+ 'ico': 'image/x-icon',
+ 'json': 'application/json',
+ 'woff': 'font/woff',
+ 'woff2': 'font/woff2',
+ 'ttf': 'font/ttf',
+ 'eot': 'font/eot'
+ };
+
+ export async function serveStaticAsset(request, env, ctx) {
+ const url = new URL(request.url);
+ let pathname = url.pathname;
+
+ // Default to index.html for root
+ if (pathname === '/' || pathname === '') {
+ pathname = '/index.html';
+ }
+
+ // For client-side routing, serve index.html for non-asset paths
+ const ext = pathname.split('.').pop();
+ if (!MIME_TYPES[ext] && !pathname.startsWith('/api') && !pathname.startsWith('/health')) {
+ pathname = '/index.html';
+ }
+
+ try {
+ // Try to get the asset from R2 bucket (FRONTEND_ASSETS)
+ if (env.FRONTEND_ASSETS) {
+ const object = await env.FRONTEND_ASSETS.get(pathname.slice(1)); // Remove leading slash
+
+ if (object) {
+ const headers = new Headers();
+ headers.set('Content-Type', MIME_TYPES[ext] || 'application/octet-stream');
+ headers.set('Cache-Control', 'public, max-age=3600');
+
+ return new Response(object.body, {
+ status: 200,
+ headers
+ });
+ }
+ }
+
+ // Fallback to KV if configured (for Cloudflare Pages migration)
+ if (env.__STATIC_CONTENT) {
+ return await getAssetFromKV(
+ {
+ request,
+ waitUntil: ctx.waitUntil.bind(ctx),
+ },
+ {
+ ASSET_NAMESPACE: env.__STATIC_CONTENT,
+ ASSET_MANIFEST: JSON.parse(env.__STATIC_CONTENT_MANIFEST || '{}'),
+ }
+ );
+ }
+
+ return null;
+ } catch (e) {
+ console.error('Static asset error:', e);
+ return null;
+ }
+ }
\ No newline at end of file
storage.js b/backup/src/lib/storage.js +123 -0
@@ @@ -0,0 +1,123 @@
+ export class Storage {
+ constructor(r2Bucket) {
+ this.bucket = r2Bucket;
+ }
+
+ /**
+ * Upload a logo image to R2
+ * @param {string} key - The storage key (e.g., 'logos/user-id/filename.png')
+ * @param {ArrayBuffer|ReadableStream} data - The image data
+ * @param {string} contentType - The MIME type of the image
+ * @returns {Promise<{key: string, url: string}>}
+ */
+ async uploadLogo(key, data, contentType) {
+ try {
+ const object = await this.bucket.put(key, data, {
+ httpMetadata: {
+ contentType: contentType,
+ cacheControl: 'public, max-age=31536000', // Cache for 1 year
+ }
+ });
+
+ if (!object) {
+ throw new Error('Failed to upload logo');
+ }
+
+ // Return the key for later retrieval
+ // In production, you might want to use a CDN URL here
+ return {
+ key: key,
+ etag: object.etag,
+ uploaded: object.uploaded
+ };
+ } catch (error) {
+ console.error('Logo upload error:', error);
+ throw new Error('Failed to upload logo to storage');
+ }
+ }
+
+ /**
+ * Get a logo from R2
+ * @param {string} key - The storage key
+ * @returns {Promise<Response>}
+ */
+ async getLogo(key) {
+ try {
+ const object = await this.bucket.get(key);
+
+ if (!object) {
+ return null;
+ }
+
+ return new Response(object.body, {
+ headers: {
+ 'Content-Type': object.httpMetadata?.contentType || 'image/png',
+ 'Cache-Control': 'public, max-age=31536000',
+ 'ETag': object.etag
+ }
+ });
+ } catch (error) {
+ console.error('Logo fetch error:', error);
+ return null;
+ }
+ }
+
+ /**
+ * Delete a logo from R2
+ * @param {string} key - The storage key
+ * @returns {Promise<void>}
+ */
+ async deleteLogo(key) {
+ try {
+ await this.bucket.delete(key);
+ } catch (error) {
+ console.error('Logo deletion error:', error);
+ // Don't throw - deletion failures shouldn't break the flow
+ }
+ }
+
+ /**
+ * Generate a unique storage key for a logo
+ * @param {string} userId - The user ID
+ * @param {string} entryId - The entry ID
+ * @param {string} filename - Original filename
+ * @returns {string}
+ */
+ static generateLogoKey(userId, entryId, filename) {
+ // Extract extension from filename
+ const ext = filename.split('.').pop().toLowerCase();
+
+ // Validate extension
+ const allowedExtensions = ['png', 'jpg', 'jpeg', 'svg', 'webp'];
+ if (!allowedExtensions.includes(ext)) {
+ throw new Error('Invalid file type. Allowed: PNG, JPG, SVG, WebP');
+ }
+
+ // Generate unique key: logos/userId/entryId.extension
+ return `logos/${userId}/${entryId}.${ext}`;
+ }
+
+ /**
+ * Validate image file
+ * @param {File|ArrayBuffer} file - The image file
+ * @param {number} maxSize - Maximum size in bytes (default: 5MB)
+ * @returns {boolean}
+ */
+ static validateImage(file, maxSize = 5 * 1024 * 1024) {
+ // Check size
+ const size = file.size || file.byteLength;
+ if (size > maxSize) {
+ throw new Error(`File too large. Maximum size: ${maxSize / 1024 / 1024}MB`);
+ }
+
+ // Check MIME type if available
+ if (file.type) {
+ const allowedTypes = ['image/png', 'image/jpeg', 'image/svg+xml', 'image/webp'];
+ if (!allowedTypes.includes(file.type)) {
+ throw new Error('Invalid file type. Allowed: PNG, JPG, SVG, WebP');
+ }
+ }
+
+ return true;
+ }
+ }
\ No newline at end of file
backup/src/middleware/auth.js +124 -0
@@ @@ -0,0 +1,124 @@
+ 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 logo serving (public endpoint)
+ if (url.pathname.startsWith('/api/logo/get/')) {
+ return null;
+ }
+
+ // 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
backup/src/middleware/cors.js +35 -0
@@ @@ -0,0 +1,35 @@
+ 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) {
+ // If no FRONTEND_URL is set, we're using unified deployment (no CORS needed)
+ if (!env.FRONTEND_URL) {
+ return response;
+ }
+
+ // Add CORS headers for separate frontend deployment
+ 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
backup/src/middleware/index.js +15 -0
@@ @@ -0,0 +1,15 @@
+ import { verifyAuth } from './auth';
+ import { rateLimit } from './rateLimit';
+
+ export async function applyMiddleware(request, env, ctx, handler) {
+ // 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
backup/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
backup/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
backup/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
backup/src/routes/auth.js +198 -0
@@ @@ -0,0 +1,198 @@
+ 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) {
+ // Postmark API integration
+ const response = await fetch('https://api.postmarkapp.com/email', {
+ method: 'POST',
+ headers: {
+ 'Accept': 'application/json',
+ 'Content-Type': 'application/json',
+ 'X-Postmark-Server-Token': env.EMAIL_API_KEY || env.POSTMARK_SERVER_TOKEN
+ },
+ body: JSON.stringify({
+ From: env.EMAIL_FROM || 'noreply@qrurl.us',
+ To: email,
+ Subject: 'Your QRurl Login Link',
+ HtmlBody: `
+ <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>
+ `,
+ TextBody: `Login to QRurl\n\nClick the link below to log in to your QRurl account:\n${magicLink}\n\nThis link expires in 15 minutes. If you didn't request this, please ignore this email.`,
+ MessageStream: 'outbound'
+ })
+ });
+
+ if (!response.ok) {
+ const error = await response.text();
+ console.error('Postmark error:', error);
+ throw new Error('Failed to send email');
+ }
+
+ const result = await response.json();
+ console.log('Email sent successfully:', result.MessageID);
+ }
\ No newline at end of file
backup/src/routes/logo.js +186 -0
@@ @@ -0,0 +1,186 @@
+ import { Storage } from '../lib/storage';
+ import { Database } from '../lib/db';
+ import { AuthError, ValidationError, NotFoundError } from '../utils/errors';
+
+ export async function logoRoutes(request, env, ctx) {
+ const url = new URL(request.url);
+ const path = url.pathname;
+
+ // Upload logo for an entry
+ if (path === '/api/logo/upload' && request.method === 'POST') {
+ return uploadLogo(request, env);
+ }
+
+ // Get logo by key (public endpoint for serving logos)
+ if (path.startsWith('/api/logo/get/') && request.method === 'GET') {
+ const key = path.replace('/api/logo/get/', '');
+ return getLogo(key, env);
+ }
+
+ // Delete logo for an entry
+ if (path.startsWith('/api/logo/delete/') && request.method === 'DELETE') {
+ const entryId = path.replace('/api/logo/delete/', '');
+ return deleteLogo(request, env, entryId);
+ }
+
+ return new Response('Not Found', { status: 404 });
+ }
+
+ async function uploadLogo(request, env) {
+ try {
+ // Check authentication
+ if (!request.user) {
+ throw new AuthError('Authentication required');
+ }
+
+ // Parse multipart form data
+ const contentType = request.headers.get('content-type') || '';
+
+ if (!contentType.includes('multipart/form-data')) {
+ throw new ValidationError('Content-Type must be multipart/form-data');
+ }
+
+ const formData = await request.formData();
+ const file = formData.get('logo');
+ const entryId = formData.get('entryId');
+
+ console.log('Upload request - File:', file?.name, 'Size:', file?.size, 'Type:', file?.type);
+
+ if (!file || !entryId) {
+ throw new ValidationError('Logo file and entry ID are required');
+ }
+
+ // Validate file
+ Storage.validateImage(file);
+
+ // Verify entry ownership
+ const db = new Database(env.DB);
+ const entry = await db.getEntryById(entryId);
+
+ if (!entry) {
+ throw new NotFoundError('Entry not found');
+ }
+
+ if (entry.user_id !== request.user.id) {
+ throw new AuthError('You do not have permission to update this entry');
+ }
+
+ // Delete old logo if exists
+ if (entry.logo_url) {
+ const storage = new Storage(env.STORAGE);
+ const oldKey = entry.logo_url.replace('/api/logo/get/', '');
+ await storage.deleteLogo(oldKey);
+ }
+
+ // Generate storage key
+ const key = Storage.generateLogoKey(request.user.id, entryId, file.name);
+ console.log('Generated storage key:', key);
+
+ // Upload to R2
+ const storage = new Storage(env.STORAGE);
+ const arrayBuffer = await file.arrayBuffer();
+ console.log('Uploading to R2, size:', arrayBuffer.byteLength);
+
+ const result = await storage.uploadLogo(key, arrayBuffer, file.type);
+ console.log('Upload result:', result);
+
+ // Update database with logo URL
+ const logoUrl = `/api/logo/get/${key}`;
+ await db.updateEntry(entryId, { logoUrl });
+
+ // 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,
+ logoUrl: logoUrl,
+ key: result.key
+ }), {
+ status: 200,
+ headers: { 'Content-Type': 'application/json' }
+ });
+ } catch (error) {
+ console.error('Logo upload error:', error);
+ return new Response(JSON.stringify({
+ error: error.message || 'Failed to upload logo'
+ }), {
+ status: error.status || 500,
+ headers: { 'Content-Type': 'application/json' }
+ });
+ }
+ }
+
+ async function getLogo(key, env) {
+ try {
+ console.log('Fetching logo with key:', key);
+ const storage = new Storage(env.STORAGE);
+ const response = await storage.getLogo(key);
+
+ if (!response) {
+ console.log('Logo not found in R2:', key);
+ return new Response('Logo not found', { status: 404 });
+ }
+
+ console.log('Logo found, returning response');
+ return response;
+ } catch (error) {
+ console.error('Logo fetch error:', error);
+ return new Response('Failed to fetch logo', { status: 500 });
+ }
+ }
+
+ async function deleteLogo(request, env, entryId) {
+ try {
+ // Check authentication
+ if (!request.user) {
+ throw new AuthError('Authentication required');
+ }
+
+ // Verify entry ownership
+ const db = new Database(env.DB);
+ const entry = await db.getEntryById(entryId);
+
+ if (!entry) {
+ throw new NotFoundError('Entry not found');
+ }
+
+ if (entry.user_id !== request.user.id) {
+ throw new AuthError('You do not have permission to update this entry');
+ }
+
+ // Delete logo from R2
+ if (entry.logo_url) {
+ const storage = new Storage(env.STORAGE);
+ const key = entry.logo_url.replace('/api/logo/get/', '');
+ await storage.deleteLogo(key);
+
+ // Update database
+ await db.updateEntry(entryId, { logoUrl: null });
+
+ // 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: 'Logo deleted successfully'
+ }), {
+ status: 200,
+ headers: { 'Content-Type': 'application/json' }
+ });
+ } catch (error) {
+ console.error('Logo deletion error:', error);
+ return new Response(JSON.stringify({
+ error: error.message || 'Failed to delete logo'
+ }), {
+ status: error.status || 500,
+ headers: { 'Content-Type': 'application/json' }
+ });
+ }
+ }
\ No newline at end of file
backup/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
backup/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
backup/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
backup/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
dev/QRurl-nuxt-plan.md +385 -0
@@ @@ -0,0 +1,385 @@
+ # Complete Plan: QRurl with Nuxt on Cloudflare Workers
+
+ ## Overview
+ Build a URL shortener with QR code generation using Nuxt (latest stable version) deployed to Cloudflare Workers with D1 database, R2 storage, and Postmark email integration.
+
+ ## Architecture Components
+ - **Frontend & Backend**: Single Nuxt application (SSR)
+ - **Database**: Cloudflare D1 (SQLite)
+ - **File Storage**: Cloudflare R2 (for logos)
+ - **Cache**: Cloudflare KV
+ - **Email**: Postmark API
+ - **Deployment**: Cloudflare Workers via Wrangler
+
+ ## Prerequisites
+ - Node.js 20.x or later (stable LTS)
+ - Cloudflare account with Workers, D1, R2, KV enabled
+ - Domain in Cloudflare (qrurl.us)
+ - Postmark account and API key
+ - GitHub account (optional for CI/CD)
+
+ ## Step-by-Step Implementation Plan
+
+ ### Phase 1: Project Setup
+
+ #### 1.1 Initialize Nuxt Project
+ ```bash
+ npx nuxi@latest init qrurl --package-manager npm
+ cd qrurl
+ ```
+
+ #### 1.2 Install Core Dependencies
+ ```bash
+ npm install --save-dev wrangler@latest
+ npm install @nuxt/ui @pinia/nuxt @vueuse/nuxt
+ npm install qrcode jsonwebtoken bcryptjs
+ npm install --save-dev @types/jsonwebtoken @types/bcryptjs
+ ```
+
+ #### 1.3 Configure Nuxt for Cloudflare
+ Create `nuxt.config.ts`:
+ - Set nitro preset to `cloudflare-pages` or `cloudflare-module`
+ - Configure build output for Workers
+ - Set up environment variables
+ - Configure TypeScript
+
+ ### Phase 2: Database Setup
+
+ #### 2.1 Create D1 Database
+ ```bash
+ wrangler d1 create qrurl-db
+ ```
+
+ #### 2.2 Database Schema
+ Create `schema.sql`:
+ ```sql
+ CREATE TABLE links (
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
+ slug TEXT UNIQUE NOT NULL,
+ url TEXT NOT NULL,
+ name TEXT,
+ logo_key TEXT,
+ user_email TEXT,
+ clicks INTEGER DEFAULT 0,
+ created_at DATETIME DEFAULT CURRENT_TIMESTAMP
+ );
+
+ CREATE TABLE auth_tokens (
+ token TEXT PRIMARY KEY,
+ email TEXT NOT NULL,
+ used INTEGER DEFAULT 0,
+ expires_at DATETIME NOT NULL
+ );
+
+ CREATE TABLE sessions (
+ id TEXT PRIMARY KEY,
+ user_email TEXT NOT NULL,
+ expires_at DATETIME NOT NULL
+ );
+
+ CREATE INDEX idx_links_slug ON links(slug);
+ CREATE INDEX idx_sessions_email ON sessions(user_email);
+ ```
+
+ #### 2.3 Initialize Database
+ ```bash
+ wrangler d1 execute qrurl-db --file=./schema.sql --local
+ wrangler d1 execute qrurl-db --file=./schema.sql --remote
+ ```
+
+ ### Phase 3: Cloudflare Resources Setup
+
+ #### 3.1 Create R2 Bucket
+ ```bash
+ wrangler r2 bucket create qrurl-logos
+ ```
+
+ #### 3.2 Create KV Namespace
+ ```bash
+ wrangler kv namespace create cache
+ ```
+
+ #### 3.3 Update wrangler.toml
+ ```toml
+ name = "qrurl"
+ compatibility_date = "2024-12-01"
+ pages_build_output_dir = ".output/public"
+
+ [[d1_databases]]
+ binding = "DB"
+ database_name = "qrurl-db"
+ database_id = "YOUR_DB_ID"
+
+ [[r2_buckets]]
+ binding = "STORAGE"
+ bucket_name = "qrurl-logos"
+
+ [[kv_namespaces]]
+ binding = "CACHE"
+ id = "YOUR_KV_ID"
+
+ [vars]
+ EMAIL_FROM = "noreply@qrurl.us"
+ ```
+
+ ### Phase 4: Application Development
+
+ #### 4.1 Directory Structure
+ ```
+ qrurl/
+ ├── server/
+ │ ├── api/
+ │ │ ├── auth/
+ │ │ │ ├── request.post.ts
+ │ │ │ └── verify.get.ts
+ │ │ ├── links/
+ │ │ │ ├── index.get.ts
+ │ │ │ ├── index.post.ts
+ │ │ │ └── [id].delete.ts
+ │ │ ├── qr/
+ │ │ │ └── [slug].get.ts
+ │ │ └── logo/
+ │ │ ├── upload.post.ts
+ │ │ └── [id].get.ts
+ │ ├── middleware/
+ │ │ └── auth.ts
+ │ └── utils/
+ │ ├── db.ts
+ │ ├── auth.ts
+ │ └── email.ts
+ ├── pages/
+ │ ├── index.vue
+ │ ├── login.vue
+ │ ├── dashboard.vue
+ │ └── [slug].vue
+ ├── components/
+ │ ├── NavBar.vue
+ │ ├── LinkForm.vue
+ │ ├── LinkList.vue
+ │ ├── QRCodeModal.vue
+ │ └── LogoUploader.vue
+ ├── stores/
+ │ └── auth.ts
+ ├── composables/
+ │ └── useApi.ts
+ └── public/
+ ```
+
+ #### 4.2 Server API Implementation
+
+ **Database Utils** (`server/utils/db.ts`):
+ - Direct D1 binding access
+ - Query builders
+ - Migration helpers
+
+ **Auth Utils** (`server/utils/auth.ts`):
+ - JWT token generation/verification
+ - Session management
+ - Cookie handling
+
+ **Email Utils** (`server/utils/email.ts`):
+ - Postmark integration
+ - Magic link generation
+ - Email templates
+
+ #### 4.3 Frontend Implementation
+
+ **Pages**:
+ - `index.vue`: Public URL shortener
+ - `login.vue`: Magic link request
+ - `dashboard.vue`: Authenticated link management
+ - `[slug].vue`: Redirect handler
+
+ **Components**:
+ - Form validation
+ - Real-time updates
+ - QR code generation with logo overlay
+ - File upload to R2
+
+ **State Management** (Pinia):
+ - Auth store
+ - Links store
+ - UI store
+
+ ### Phase 5: Authentication Flow
+
+ 1. User enters email on login page
+ 2. Server validates email against whitelist
+ 3. Generate magic link token, store in D1
+ 4. Send email via Postmark
+ 5. User clicks link
+ 6. Verify token, create session
+ 7. Set HTTP-only cookie
+ 8. Redirect to dashboard
+
+ ### Phase 6: Core Features
+
+ #### 6.1 URL Shortening
+ - Generate random slug or accept custom
+ - Validate URL format
+ - Check slug uniqueness
+ - Store in D1 with metadata
+
+ #### 6.2 QR Code Generation
+ - Use qrcode library
+ - High error correction for logo overlay
+ - Return as base64 or binary
+ - Cache in KV for performance
+
+ #### 6.3 Logo Upload
+ - Accept image upload
+ - Validate file type/size
+ - Store in R2 with unique key
+ - Reference in link record
+
+ #### 6.4 Analytics
+ - Track clicks in D1
+ - Store user agent, referrer
+ - Display in dashboard
+ - Export functionality
+
+ ### Phase 7: Environment Configuration
+
+ #### 7.1 Development (.env)
+ ```env
+ NUXT_JWT_SECRET=dev-secret
+ NUXT_EMAIL_API_KEY=your-postmark-key
+ NUXT_AUTHORIZED_EMAILS=email1@example.com,email2@example.com
+ ```
+
+ #### 7.2 Production Secrets
+ ```bash
+ wrangler secret put JWT_SECRET
+ wrangler secret put EMAIL_API_KEY
+ wrangler secret put AUTHORIZED_EMAILS
+ ```
+
+ ### Phase 8: Deployment
+
+ #### 8.1 Build Process
+ ```bash
+ npm run build
+ ```
+
+ #### 8.2 Deploy to Cloudflare
+ ```bash
+ wrangler pages deploy .output/public
+ ```
+
+ #### 8.3 Configure Custom Domain
+ 1. Cloudflare Dashboard → Pages → Custom domains
+ 2. Add qrurl.us
+ 3. Configure DNS if needed
+
+ ### Phase 9: Testing & Optimization
+
+ #### 9.1 Local Testing
+ ```bash
+ npm run dev # Development server
+ npm run preview # Production preview
+ ```
+
+ #### 9.2 Performance Optimization
+ - Implement caching strategies
+ - Optimize database queries
+ - Compress assets
+ - Lazy load components
+
+ #### 9.3 Security
+ - Rate limiting
+ - Input validation
+ - CSRF protection
+ - Content Security Policy
+
+ ### Phase 10: CI/CD Setup (Optional)
+
+ #### 10.1 GitHub Actions
+ ```yaml
+ name: Deploy
+ on:
+ push:
+ branches: [main]
+ jobs:
+ deploy:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v3
+ - uses: actions/setup-node@v3
+ - run: npm ci
+ - run: npm run build
+ - uses: cloudflare/wrangler-action@v3
+ with:
+ apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
+ ```
+
+ ## Common Issues & Solutions
+
+ ### Issue 1: EBADF Errors
+ - Use Node.js LTS (20.x)
+ - Avoid Node.js 23.x
+ - Check file descriptor limits
+
+ ### Issue 2: D1 Binding Issues
+ - Ensure database ID matches
+ - Check wrangler.toml configuration
+ - Verify local vs remote execution
+
+ ### Issue 3: CORS Problems
+ - Not needed with unified deployment
+ - Everything on same domain
+
+ ### Issue 4: Build Failures
+ - Clear .nuxt and node_modules
+ - Reinstall dependencies
+ - Check TypeScript errors
+
+ ## Key Differences from Framework-Heavy Approach
+
+ 1. **Simpler Structure**: Single deployment unit
+ 2. **No CORS**: API and frontend on same domain
+ 3. **Direct Bindings**: Use Cloudflare resources directly
+ 4. **Better Performance**: Edge-optimized
+ 5. **Easier Debugging**: Unified logs
+
+ ## Testing Checklist
+
+ - [ ] Homepage loads
+ - [ ] URL shortening works
+ - [ ] QR codes generate
+ - [ ] Redirects work
+ - [ ] Login flow completes
+ - [ ] Dashboard accessible
+ - [ ] Logo upload works
+ - [ ] Analytics track
+ - [ ] Session persistence
+ - [ ] Logout works
+
+ ## Production Checklist
+
+ - [ ] Database migrated
+ - [ ] Secrets configured
+ - [ ] Custom domain active
+ - [ ] SSL working
+ - [ ] Email sending
+ - [ ] Error handling
+ - [ ] Monitoring setup
+ - [ ] Backup strategy
+
+ ## Estimated Timeline
+
+ - **Phase 1-3**: 1 hour (setup)
+ - **Phase 4-6**: 4-6 hours (development)
+ - **Phase 7-8**: 1 hour (deployment)
+ - **Phase 9-10**: 2 hours (testing/optimization)
+
+ **Total**: 8-10 hours for complete implementation
+
+ ## Success Criteria
+
+ 1. App deploys to qrurl.us
+ 2. All features from original app work
+ 3. No framework complexity
+ 4. Fast performance (<100ms response)
+ 5. Reliable email delivery
+ 6. Secure authentication
+ 7. Clean, maintainable code
\ No newline at end of file
frontend/.env.example +2 -0
@@ @@ -0,0 +1,2 @@
+ VITE_API_URL=http://localhost:8787/api
+ VITE_SHORT_URL=http://localhost:8787
\ No newline at end of file
package-lock.json +502 -767
@@ @@ -7,37 +7,35 @@
"": {
"name": "qrurl",
"version": "1.0.0",
- "license": "ISC",
"dependencies": {
- "postmark": "^4.0.5"
+ "qrcode": "^1.5.4"
},
"devDependencies": {
- "concurrently": "^9.2.0",
- "wrangler": "^4.32.0"
+ "wrangler": "^3.99.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==",
+ "version": "0.3.4",
+ "resolved": "https://registry.npmjs.org/@cloudflare/kv-asset-handler/-/kv-asset-handler-0.3.4.tgz",
+ "integrity": "sha512-YLPHc8yASwjNkmcDMQMY35yiWjoKAKnhUbPRszBRS0YgH+IXtsMp61j+yTcnCE3oO2DgP0U3iejLC8FTtKDC8Q==",
"dev": true,
"license": "MIT OR Apache-2.0",
"dependencies": {
"mime": "^3.0.0"
},
"engines": {
- "node": ">=18.0.0"
+ "node": ">=16.13"
}
},
"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==",
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/@cloudflare/unenv-preset/-/unenv-preset-2.0.2.tgz",
+ "integrity": "sha512-nyzYnlZjjV5xT3LizahG1Iu6mnrCaxglJ04rZLpDwlDVDZ7v46lNsfxhV3A/xtfgQuSHmLnc6SVI+KwBpc3Lwg==",
"dev": true,
"license": "MIT OR Apache-2.0",
"peerDependencies": {
- "unenv": "2.0.0-rc.19",
- "workerd": "^1.20250802.0"
+ "unenv": "2.0.0-rc.14",
+ "workerd": "^1.20250124.0"
},
"peerDependenciesMeta": {
"workerd": {
@@ @@ -46,9 +44,9 @@
}
},
"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==",
+ "version": "1.20250718.0",
+ "resolved": "https://registry.npmjs.org/@cloudflare/workerd-darwin-64/-/workerd-darwin-64-1.20250718.0.tgz",
+ "integrity": "sha512-FHf4t7zbVN8yyXgQ/r/GqLPaYZSGUVzeR7RnL28Mwj2djyw2ZergvytVc7fdGcczl6PQh+VKGfZCfUqpJlbi9g==",
"cpu": [
"x64"
],
@@ @@ -63,9 +61,9 @@
}
},
"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==",
+ "version": "1.20250718.0",
+ "resolved": "https://registry.npmjs.org/@cloudflare/workerd-darwin-arm64/-/workerd-darwin-arm64-1.20250718.0.tgz",
+ "integrity": "sha512-fUiyUJYyqqp4NqJ0YgGtp4WJh/II/YZsUnEb6vVy5Oeas8lUOxnN+ZOJ8N/6/5LQCVAtYCChRiIrBbfhTn5Z8Q==",
"cpu": [
"arm64"
],
@@ @@ -80,9 +78,9 @@
}
},
"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==",
+ "version": "1.20250718.0",
+ "resolved": "https://registry.npmjs.org/@cloudflare/workerd-linux-64/-/workerd-linux-64-1.20250718.0.tgz",
+ "integrity": "sha512-5+eb3rtJMiEwp08Kryqzzu8d1rUcK+gdE442auo5eniMpT170Dz0QxBrqkg2Z48SFUPYbj+6uknuA5tzdRSUSg==",
"cpu": [
"x64"
],
@@ @@ -97,9 +95,9 @@
}
},
"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==",
+ "version": "1.20250718.0",
+ "resolved": "https://registry.npmjs.org/@cloudflare/workerd-linux-arm64/-/workerd-linux-arm64-1.20250718.0.tgz",
+ "integrity": "sha512-Aa2M/DVBEBQDdATMbn217zCSFKE+ud/teS+fFS+OQqKABLn0azO2qq6ANAHYOIE6Q3Sq4CxDIQr8lGdaJHwUog==",
"cpu": [
"arm64"
],
@@ @@ -114,9 +112,9 @@
}
},
"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==",
+ "version": "1.20250718.0",
+ "resolved": "https://registry.npmjs.org/@cloudflare/workerd-windows-64/-/workerd-windows-64-1.20250718.0.tgz",
+ "integrity": "sha512-dY16RXKffmugnc67LTbyjdDHZn5NoTF1yHEf2fN4+OaOnoGSp3N1x77QubTDwqZ9zECWxgQfDLjddcH8dWeFhg==",
"cpu": [
"x64"
],
@@ @@ -154,27 +152,34 @@
"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"
- ],
+ "node_modules/@esbuild-plugins/node-globals-polyfill": {
+ "version": "0.2.3",
+ "resolved": "https://registry.npmjs.org/@esbuild-plugins/node-globals-polyfill/-/node-globals-polyfill-0.2.3.tgz",
+ "integrity": "sha512-r3MIryXDeXDOZh7ih1l/yE9ZLORCd5e8vWg02azWRGj5SPTuoh69A2AIyn0Z31V/kHBfZ4HgWJ+OK3GTTwLmnw==",
"dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "aix"
- ],
- "engines": {
- "node": ">=18"
+ "license": "ISC",
+ "peerDependencies": {
+ "esbuild": "*"
+ }
+ },
+ "node_modules/@esbuild-plugins/node-modules-polyfill": {
+ "version": "0.2.2",
+ "resolved": "https://registry.npmjs.org/@esbuild-plugins/node-modules-polyfill/-/node-modules-polyfill-0.2.2.tgz",
+ "integrity": "sha512-LXV7QsWJxRuMYvKbiznh+U1ilIop3g2TeKRzUxOG5X3YITc8JyyTa90BmLwqqv0YnX4v32CSlG+vsziZp9dMvA==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "escape-string-regexp": "^4.0.0",
+ "rollup-plugin-node-polyfills": "^0.2.1"
+ },
+ "peerDependencies": {
+ "esbuild": "*"
}
},
"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==",
+ "version": "0.17.19",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.17.19.tgz",
+ "integrity": "sha512-rIKddzqhmav7MSmoFCmDIb6e2W57geRsM94gV2l38fzhXMwq7hZoClug9USI2pFRGL06f4IOPHHpFNOkWieR8A==",
"cpu": [
"arm"
],
@@ @@ -185,13 +190,13 @@
"android"
],
"engines": {
- "node": ">=18"
+ "node": ">=12"
}
},
"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==",
+ "version": "0.17.19",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.17.19.tgz",
+ "integrity": "sha512-KBMWvEZooR7+kzY0BtbTQn0OAYY7CsiydT63pVEaPtVYF0hXbUaOyZog37DKxK7NF3XacBJOpYT4adIJh+avxA==",
"cpu": [
"arm64"
],
@@ @@ -202,13 +207,13 @@
"android"
],
"engines": {
- "node": ">=18"
+ "node": ">=12"
}
},
"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==",
+ "version": "0.17.19",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.17.19.tgz",
+ "integrity": "sha512-uUTTc4xGNDT7YSArp/zbtmbhO0uEEK9/ETW29Wk1thYUJBz3IVnvgEiEwEa9IeLyvnpKrWK64Utw2bgUmDveww==",
"cpu": [
"x64"
],
@@ @@ -219,13 +224,13 @@
"android"
],
"engines": {
- "node": ">=18"
+ "node": ">=12"
}
},
"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==",
+ "version": "0.17.19",
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.17.19.tgz",
+ "integrity": "sha512-80wEoCfF/hFKM6WE1FyBHc9SfUblloAWx6FJkFWTWiCoht9Mc0ARGEM47e67W9rI09YoUxJL68WHfDRYEAvOhg==",
"cpu": [
"arm64"
],
@@ @@ -236,13 +241,13 @@
"darwin"
],
"engines": {
- "node": ">=18"
+ "node": ">=12"
}
},
"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==",
+ "version": "0.17.19",
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.17.19.tgz",
+ "integrity": "sha512-IJM4JJsLhRYr9xdtLytPLSH9k/oxR3boaUIYiHkAawtwNOXKE8KoU8tMvryogdcT8AU+Bflmh81Xn6Q0vTZbQw==",
"cpu": [
"x64"
],
@@ @@ -253,13 +258,13 @@
"darwin"
],
"engines": {
- "node": ">=18"
+ "node": ">=12"
}
},
"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==",
+ "version": "0.17.19",
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.17.19.tgz",
+ "integrity": "sha512-pBwbc7DufluUeGdjSU5Si+P3SoMF5DQ/F/UmTSb8HXO80ZEAJmrykPyzo1IfNbAoaqw48YRpv8shwd1NoI0jcQ==",
"cpu": [
"arm64"
],
@@ @@ -270,13 +275,13 @@
"freebsd"
],
"engines": {
- "node": ">=18"
+ "node": ">=12"
}
},
"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==",
+ "version": "0.17.19",
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.17.19.tgz",
+ "integrity": "sha512-4lu+n8Wk0XlajEhbEffdy2xy53dpR06SlzvhGByyg36qJw6Kpfk7cp45DR/62aPH9mtJRmIyrXAS5UWBrJT6TQ==",
"cpu": [
"x64"
],
@@ @@ -287,13 +292,13 @@
"freebsd"
],
"engines": {
- "node": ">=18"
+ "node": ">=12"
}
},
"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==",
+ "version": "0.17.19",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.17.19.tgz",
+ "integrity": "sha512-cdmT3KxjlOQ/gZ2cjfrQOtmhG4HJs6hhvm3mWSRDPtZ/lP5oe8FWceS10JaSJC13GBd4eH/haHnqf7hhGNLerA==",
"cpu": [
"arm"
],
@@ @@ -304,13 +309,13 @@
"linux"
],
"engines": {
- "node": ">=18"
+ "node": ">=12"
}
},
"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==",
+ "version": "0.17.19",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.17.19.tgz",
+ "integrity": "sha512-ct1Tg3WGwd3P+oZYqic+YZF4snNl2bsnMKRkb3ozHmnM0dGWuxcPTTntAF6bOP0Sp4x0PjSF+4uHQ1xvxfRKqg==",
"cpu": [
"arm64"
],
@@ @@ -321,13 +326,13 @@
"linux"
],
"engines": {
- "node": ">=18"
+ "node": ">=12"
}
},
"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==",
+ "version": "0.17.19",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.17.19.tgz",
+ "integrity": "sha512-w4IRhSy1VbsNxHRQpeGCHEmibqdTUx61Vc38APcsRbuVgK0OPEnQ0YD39Brymn96mOx48Y2laBQGqgZ0j9w6SQ==",
"cpu": [
"ia32"
],
@@ @@ -338,13 +343,13 @@
"linux"
],
"engines": {
- "node": ">=18"
+ "node": ">=12"
}
},
"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==",
+ "version": "0.17.19",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.17.19.tgz",
+ "integrity": "sha512-2iAngUbBPMq439a+z//gE+9WBldoMp1s5GWsUSgqHLzLJ9WoZLZhpwWuym0u0u/4XmZ3gpHmzV84PonE+9IIdQ==",
"cpu": [
"loong64"
],
@@ @@ -355,13 +360,13 @@
"linux"
],
"engines": {
- "node": ">=18"
+ "node": ">=12"
}
},
"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==",
+ "version": "0.17.19",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.17.19.tgz",
+ "integrity": "sha512-LKJltc4LVdMKHsrFe4MGNPp0hqDFA1Wpt3jE1gEyM3nKUvOiO//9PheZZHfYRfYl6AwdTH4aTcXSqBerX0ml4A==",
"cpu": [
"mips64el"
],
@@ @@ -372,13 +377,13 @@
"linux"
],
"engines": {
- "node": ">=18"
+ "node": ">=12"
}
},
"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==",
+ "version": "0.17.19",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.17.19.tgz",
+ "integrity": "sha512-/c/DGybs95WXNS8y3Ti/ytqETiW7EU44MEKuCAcpPto3YjQbyK3IQVKfF6nbghD7EcLUGl0NbiL5Rt5DMhn5tg==",
"cpu": [
"ppc64"
],
@@ @@ -389,13 +394,13 @@
"linux"
],
"engines": {
- "node": ">=18"
+ "node": ">=12"
}
},
"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==",
+ "version": "0.17.19",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.17.19.tgz",
+ "integrity": "sha512-FC3nUAWhvFoutlhAkgHf8f5HwFWUL6bYdvLc/TTuxKlvLi3+pPzdZiFKSWz/PF30TB1K19SuCxDTI5KcqASJqA==",
"cpu": [
"riscv64"
],
@@ @@ -406,13 +411,13 @@
"linux"
],
"engines": {
- "node": ">=18"
+ "node": ">=12"
}
},
"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==",
+ "version": "0.17.19",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.17.19.tgz",
+ "integrity": "sha512-IbFsFbxMWLuKEbH+7sTkKzL6NJmG2vRyy6K7JJo55w+8xDk7RElYn6xvXtDW8HCfoKBFK69f3pgBJSUSQPr+4Q==",
"cpu": [
"s390x"
],
@@ @@ -423,13 +428,13 @@
"linux"
],
"engines": {
- "node": ">=18"
+ "node": ">=12"
}
},
"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==",
+ "version": "0.17.19",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.17.19.tgz",
+ "integrity": "sha512-68ngA9lg2H6zkZcyp22tsVt38mlhWde8l3eJLWkyLrp4HwMUr3c1s/M2t7+kHIhvMjglIBrFpncX1SzMckomGw==",
"cpu": [
"x64"
],
@@ @@ -440,30 +445,13 @@
"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": ">=12"
}
},
"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==",
+ "version": "0.17.19",
+ "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.17.19.tgz",
+ "integrity": "sha512-CwFq42rXCR8TYIjIfpXCbRX0rp1jo6cPIUPSaWwzbVI4aOfX96OXY8M6KNmtPcg7QjYeDmN+DD0Wp3LaBOLf4Q==",
"cpu": [
"x64"
],
@@ @@ -474,30 +462,13 @@
"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": ">=12"
}
},
"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==",
+ "version": "0.17.19",
+ "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.17.19.tgz",
+ "integrity": "sha512-cnq5brJYrSZ2CF6c35eCmviIN3k3RczmHz8eYaVlNasVqsNY+JKohZU5MKmaOI+KkllCdzOKKdPs762VCPC20g==",
"cpu": [
"x64"
],
@@ @@ -508,13 +479,13 @@
"openbsd"
],
"engines": {
- "node": ">=18"
+ "node": ">=12"
}
},
"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==",
+ "version": "0.17.19",
+ "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.17.19.tgz",
+ "integrity": "sha512-vCRT7yP3zX+bKWFeP/zdS6SqdWB8OIpaRq/mbXQxTGHnIxspRtigpkUcDMlSCOejlHowLqII7K2JKevwyRP2rg==",
"cpu": [
"x64"
],
@@ @@ -525,13 +496,13 @@
"sunos"
],
"engines": {
- "node": ">=18"
+ "node": ">=12"
}
},
"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==",
+ "version": "0.17.19",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.17.19.tgz",
+ "integrity": "sha512-yYx+8jwowUstVdorcMdNlzklLYhPxjniHWFKgRqH7IFlUEa0Umu3KuYplf1HUZZ422e3NU9F4LGb+4O0Kdcaag==",
"cpu": [
"arm64"
],
@@ @@ -542,13 +513,13 @@
"win32"
],
"engines": {
- "node": ">=18"
+ "node": ">=12"
}
},
"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==",
+ "version": "0.17.19",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.17.19.tgz",
+ "integrity": "sha512-eggDKanJszUtCdlVs0RB+h35wNlb5v4TWEkq4vZcmVt5u/HiDZrTXe2bWFQUez3RgNHwx/x4sk5++4NSSicKkw==",
"cpu": [
"ia32"
],
@@ @@ -559,13 +530,13 @@
"win32"
],
"engines": {
- "node": ">=18"
+ "node": ">=12"
}
},
"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==",
+ "version": "0.17.19",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.17.19.tgz",
+ "integrity": "sha512-lAhycmKnVOuRYNtRtatQR1LPQf2oYCkRGkSFnseDAKPl8lu5SOsK/e1sXe5a0Pc5kHIHe6P2I/ilntNv2xf3cA==",
"cpu": [
"x64"
],
@@ @@ -576,7 +547,17 @@
"win32"
],
"engines": {
- "node": ">=18"
+ "node": ">=12"
+ }
+ },
+ "node_modules/@fastify/busboy": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/@fastify/busboy/-/busboy-2.1.1.tgz",
+ "integrity": "sha512-vBZP4NlzfOlerQTnba4aqZoMhE/a9HY7HRqoOPaETQcSQuWEIyZMHGfVu6w9wGtGK5fED5qRs2DteVCjOH60sA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=14"
}
},
"node_modules/@img/sharp-darwin-arm64": {
@@ @@ -987,55 +968,6 @@
"@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",
@@ @@ -1063,7 +995,6 @@
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
- "dev": true,
"license": "MIT",
"engines": {
"node": ">=8"
@@ @@ -1073,7 +1004,6 @@
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
"integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
- "dev": true,
"license": "MIT",
"dependencies": {
"color-convert": "^2.0.1"
@@ @@ -1085,21 +1015,14 @@
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
}
},
- "node_modules/asynckit": {
- "version": "0.4.0",
- "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
- "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
- "license": "MIT"
- },
- "node_modules/axios": {
- "version": "1.11.0",
- "resolved": "https://registry.npmjs.org/axios/-/axios-1.11.0.tgz",
- "integrity": "sha512-1Lx3WLFQWm3ooKDYZD1eXmoGO9fxYQjrycfHFC8P0sCfQVXyROp0p9PFWBehewBOdCwHc+f/b8I0fMto5eSfwA==",
+ "node_modules/as-table": {
+ "version": "1.0.55",
+ "resolved": "https://registry.npmjs.org/as-table/-/as-table-1.0.55.tgz",
+ "integrity": "sha512-xvsWESUJn0JN421Xb9MQw6AsMHRCUknCe0Wjlxvjud80mU4E6hQf1A6NzQKcYNmYw62MfzEtXc+badstZP3JpQ==",
+ "dev": true,
"license": "MIT",
"dependencies": {
- "follow-redirects": "^1.15.6",
- "form-data": "^4.0.4",
- "proxy-from-env": "^1.1.0"
+ "printable-characters": "^1.0.42"
}
},
"node_modules/blake3-wasm": {
@@ @@ -1109,62 +1032,24 @@
"dev": true,
"license": "MIT"
},
- "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/chalk": {
- "version": "4.1.2",
- "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
- "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "ansi-styles": "^4.1.0",
- "supports-color": "^7.1.0"
- },
- "engines": {
- "node": ">=10"
- },
- "funding": {
- "url": "https://github.com/chalk/chalk?sponsor=1"
- }
- },
- "node_modules/chalk/node_modules/supports-color": {
- "version": "7.2.0",
- "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
- "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
- "dev": true,
+ "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",
- "dependencies": {
- "has-flag": "^4.0.0"
- },
"engines": {
- "node": ">=8"
+ "node": ">=6"
}
},
"node_modules/cliui": {
- "version": "8.0.1",
- "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz",
- "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==",
- "dev": true,
+ "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.1",
- "wrap-ansi": "^7.0.0"
- },
- "engines": {
- "node": ">=12"
+ "strip-ansi": "^6.0.0",
+ "wrap-ansi": "^6.2.0"
}
},
"node_modules/color": {
@@ @@ -1173,6 +1058,7 @@
"integrity": "sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==",
"dev": true,
"license": "MIT",
+ "optional": true,
"dependencies": {
"color-convert": "^2.0.1",
"color-string": "^1.9.0"
@@ @@ -1185,7 +1071,6 @@
"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"
@@ @@ -1198,7 +1083,6 @@
"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": {
@@ @@ -1207,73 +1091,36 @@
"integrity": "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==",
"dev": true,
"license": "MIT",
+ "optional": true,
"dependencies": {
"color-name": "^1.0.0",
"simple-swizzle": "^0.2.2"
}
},
- "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/concurrently": {
- "version": "9.2.0",
- "resolved": "https://registry.npmjs.org/concurrently/-/concurrently-9.2.0.tgz",
- "integrity": "sha512-IsB/fiXTupmagMW4MNp2lx2cdSN2FfZq78vF90LBB+zZHArbIQZjQtzXCiXnvTxCZSvXanTqFLWBjw2UkLx1SQ==",
+ "node_modules/cookie": {
+ "version": "0.7.2",
+ "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz",
+ "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==",
"dev": true,
"license": "MIT",
- "dependencies": {
- "chalk": "^4.1.2",
- "lodash": "^4.17.21",
- "rxjs": "^7.8.1",
- "shell-quote": "^1.8.1",
- "supports-color": "^8.1.1",
- "tree-kill": "^1.2.2",
- "yargs": "^17.7.2"
- },
- "bin": {
- "conc": "dist/bin/concurrently.js",
- "concurrently": "dist/bin/concurrently.js"
- },
"engines": {
- "node": ">=18"
- },
- "funding": {
- "url": "https://github.com/open-cli-tools/concurrently?sponsor=1"
+ "node": ">= 0.6"
}
},
- "node_modules/concurrently/node_modules/supports-color": {
- "version": "8.1.1",
- "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz",
- "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==",
+ "node_modules/data-uri-to-buffer": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-2.0.2.tgz",
+ "integrity": "sha512-ND9qDTLc6diwj+Xe5cdAgVTbLVdXbtxTJRXRhli8Mowuaan+0EJOtdqJ0QCHNSSPyoXGx9HX2/VMnKeC34AChA==",
"dev": true,
- "license": "MIT",
- "dependencies": {
- "has-flag": "^4.0.0"
- },
- "engines": {
- "node": ">=10"
- },
- "funding": {
- "url": "https://github.com/chalk/supports-color?sponsor=1"
- }
+ "license": "MIT"
},
- "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,
+ "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": ">=18"
+ "node": ">=0.10.0"
}
},
"node_modules/defu": {
@@ @@ -1283,105 +1130,33 @@
"dev": true,
"license": "MIT"
},
- "node_modules/delayed-stream": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
- "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
- "license": "MIT",
- "engines": {
- "node": ">=0.4.0"
- }
- },
"node_modules/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,
"engines": {
"node": ">=8"
}
},
- "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/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/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==",
- "dev": true,
"license": "MIT"
},
- "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/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.4",
- "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.4.tgz",
- "integrity": "sha512-8pgjLUcUjcgDg+2Q4NYXnPbo/vncAY4UmyaCm0jZevERqCHZIaWwdJHkf8XQtu4AxSKCdvrUbT0XUr1IdZzI8Q==",
+ "version": "0.17.19",
+ "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.17.19.tgz",
+ "integrity": "sha512-XQ0jAPFkK/u3LcVRcvVHQcTIqD6E2H1fvZMA5dQPSOWb3suUbWbfbRf94pjc0bNzRYLfIrDRQXr7X+LHIm5oHw==",
"dev": true,
"hasInstallScript": true,
"license": "MIT",
@@ @@ -1389,46 +1164,53 @@
"esbuild": "bin/esbuild"
},
"engines": {
- "node": ">=18"
+ "node": ">=12"
},
"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/escalade": {
- "version": "3.2.0",
- "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz",
- "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==",
+ "@esbuild/android-arm": "0.17.19",
+ "@esbuild/android-arm64": "0.17.19",
+ "@esbuild/android-x64": "0.17.19",
+ "@esbuild/darwin-arm64": "0.17.19",
+ "@esbuild/darwin-x64": "0.17.19",
+ "@esbuild/freebsd-arm64": "0.17.19",
+ "@esbuild/freebsd-x64": "0.17.19",
+ "@esbuild/linux-arm": "0.17.19",
+ "@esbuild/linux-arm64": "0.17.19",
+ "@esbuild/linux-ia32": "0.17.19",
+ "@esbuild/linux-loong64": "0.17.19",
+ "@esbuild/linux-mips64el": "0.17.19",
+ "@esbuild/linux-ppc64": "0.17.19",
+ "@esbuild/linux-riscv64": "0.17.19",
+ "@esbuild/linux-s390x": "0.17.19",
+ "@esbuild/linux-x64": "0.17.19",
+ "@esbuild/netbsd-x64": "0.17.19",
+ "@esbuild/openbsd-x64": "0.17.19",
+ "@esbuild/sunos-x64": "0.17.19",
+ "@esbuild/win32-arm64": "0.17.19",
+ "@esbuild/win32-ia32": "0.17.19",
+ "@esbuild/win32-x64": "0.17.19"
+ }
+ },
+ "node_modules/escape-string-regexp": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz",
+ "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==",
"dev": true,
"license": "MIT",
"engines": {
- "node": ">=6"
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
}
},
+ "node_modules/estree-walker": {
+ "version": "0.6.1",
+ "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-0.6.1.tgz",
+ "integrity": "sha512-SqmZANLWS0mnatqbSfRP5g8OXZC12Fgg1IwNtLsyHDzJizORW4khDfjPqJZsemPWBB2uqykUah5YpQ6epsqC/w==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/exit-hook": {
"version": "2.2.1",
"resolved": "https://registry.npmjs.org/exit-hook/-/exit-hook-2.2.1.tgz",
@@ @@ -1449,40 +1231,17 @@
"dev": true,
"license": "MIT"
},
- "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/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==",
+ "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": {
- "asynckit": "^0.4.0",
- "combined-stream": "^1.0.8",
- "es-set-tostringtag": "^2.1.0",
- "hasown": "^2.0.2",
- "mime-types": "^2.1.12"
+ "locate-path": "^5.0.0",
+ "path-exists": "^4.0.0"
},
"engines": {
- "node": ">= 6"
+ "node": ">=8"
}
},
"node_modules/fsevents": {
@@ @@ -1500,60 +1259,24 @@
"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==",
- "dev": true,
"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",
+ "node_modules/get-source": {
+ "version": "2.0.12",
+ "resolved": "https://registry.npmjs.org/get-source/-/get-source-2.0.12.tgz",
+ "integrity": "sha512-X5+4+iD+HoSeEED+uwrQ07BOQr0kEDFMVqqpBuI+RaZBpBpHCuXxo70bjar6f0b0u/DQJsJ7ssurpP0V60Az+w==",
+ "dev": true,
+ "license": "Unlicense",
"dependencies": {
- "dunder-proto": "^1.0.1",
- "es-object-atoms": "^1.0.0"
- },
- "engines": {
- "node": ">= 0.4"
+ "data-uri-to-buffer": "^2.0.0",
+ "source-map": "^0.6.1"
}
},
"node_modules/glob-to-regexp": {
@@ @@ -1563,108 +1286,43 @@
"dev": true,
"license": "BSD-2-Clause"
},
- "node_modules/gopd": {
- "version": "1.2.0",
- "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
- "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
- "license": "MIT",
- "engines": {
- "node": ">= 0.4"
- },
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
- }
- },
- "node_modules/has-flag": {
- "version": "4.0.0",
- "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
- "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=8"
- }
- },
- "node_modules/has-symbols": {
- "version": "1.1.0",
- "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
- "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
- "license": "MIT",
- "engines": {
- "node": ">= 0.4"
- },
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
- }
- },
- "node_modules/has-tostringtag": {
- "version": "1.0.2",
- "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
- "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
- "license": "MIT",
- "dependencies": {
- "has-symbols": "^1.0.3"
- },
- "engines": {
- "node": ">= 0.4"
- },
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
- }
- },
- "node_modules/hasown": {
- "version": "2.0.2",
- "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
- "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
- "license": "MIT",
- "dependencies": {
- "function-bind": "^1.1.2"
- },
- "engines": {
- "node": ">= 0.4"
- }
- },
"node_modules/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"
+ "license": "MIT",
+ "optional": true
},
"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==",
- "dev": true,
"license": "MIT",
"engines": {
"node": ">=8"
}
},
- "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,
+ "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": ">=6"
+ "node": ">=8"
}
},
- "node_modules/lodash": {
- "version": "4.17.21",
- "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
- "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==",
+ "node_modules/magic-string": {
+ "version": "0.25.9",
+ "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.25.9.tgz",
+ "integrity": "sha512-RmF0AsMzgt25qzqqLc1+MbHmhdx0ojF2Fvs4XnOqz2ZOBXzzkEwc/dJQZCYHAn7v1jbVOjAZfK8msRn4BxO4VQ==",
"dev": true,
- "license": "MIT"
- },
- "node_modules/math-intrinsics": {
- "version": "1.1.0",
- "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
- "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
"license": "MIT",
- "engines": {
- "node": ">= 0.4"
+ "dependencies": {
+ "sourcemap-codec": "^1.4.8"
}
},
"node_modules/mime": {
@@ @@ -1680,31 +1338,10 @@
"node": ">=10.0.0"
}
},
- "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/miniflare": {
- "version": "4.20250816.1",
- "resolved": "https://registry.npmjs.org/miniflare/-/miniflare-4.20250816.1.tgz",
- "integrity": "sha512-2X8yMy5wWw0dF1pNU4kztzZgp0jWv2KMqAOOb2FeQ/b11yck4aczmYHi7UYD3uyOgtj8WFhwG/KdRWAaATTtRA==",
+ "version": "3.20250718.1",
+ "resolved": "https://registry.npmjs.org/miniflare/-/miniflare-3.20250718.1.tgz",
+ "integrity": "sha512-9QAOHVKIVHmnQ1dJT9Fls8aVA8R5JjEizzV889Dinq/+bEPltqIepCvm9Z+fbNUgLvV7D/H1NUk8VdlLRgp9Wg==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ @@ -1713,19 +1350,28 @@
"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",
+ "undici": "^5.28.5",
+ "workerd": "1.20250718.0",
"ws": "8.18.0",
- "youch": "4.1.0-beta.10",
+ "youch": "3.3.4",
"zod": "3.22.3"
},
"bin": {
"miniflare": "bootstrap.js"
},
"engines": {
- "node": ">=18.0.0"
+ "node": ">=16.13"
+ }
+ },
+ "node_modules/mustache": {
+ "version": "4.2.0",
+ "resolved": "https://registry.npmjs.org/mustache/-/mustache-4.2.0.tgz",
+ "integrity": "sha512-71ippSywq5Yb7/tVYyGbkBggbU8H3u5Rz56fH60jGFgr8uHwxs+aSKeqmluIVzM0m0kB7xQjKS6qPfd0b2ZoqQ==",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "mustache": "bin/mustache"
}
},
"node_modules/ohash": {
@@ @@ -1735,6 +1381,51 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/p-limit": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz",
+ "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==",
+ "license": "MIT",
+ "dependencies": {
+ "p-try": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/p-locate": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz",
+ "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==",
+ "license": "MIT",
+ "dependencies": {
+ "p-limit": "^2.2.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/p-try": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz",
+ "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/path-exists": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
+ "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
"node_modules/path-to-regexp": {
"version": "6.3.0",
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-6.3.0.tgz",
@@ @@ -1749,39 +1440,85 @@
"dev": true,
"license": "MIT"
},
- "node_modules/postmark": {
- "version": "4.0.5",
- "resolved": "https://registry.npmjs.org/postmark/-/postmark-4.0.5.tgz",
- "integrity": "sha512-nerZdd3TwOH4CgGboZnlUM/q7oZk0EqpZgJL+Y3Nup8kHeaukxouQ6JcFF3EJEijc4QbuNv1TefGhboAKtf/SQ==",
+ "node_modules/pngjs": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/pngjs/-/pngjs-5.0.0.tgz",
+ "integrity": "sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw==",
"license": "MIT",
- "dependencies": {
- "axios": "^1.7.4"
+ "engines": {
+ "node": ">=10.13.0"
}
},
- "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/printable-characters": {
+ "version": "1.0.42",
+ "resolved": "https://registry.npmjs.org/printable-characters/-/printable-characters-1.0.42.tgz",
+ "integrity": "sha512-dKp+C4iXWK4vVYZmYSd0KBH5F/h1HoZRsbJ82AVKRO3PEo8L4lBS/vLwhVtpwwuYcoIsVY+1JYKR268yn480uQ==",
+ "dev": true,
+ "license": "Unlicense"
+ },
+ "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/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==",
- "dev": true,
"license": "MIT",
"engines": {
"node": ">=0.10.0"
}
},
- "node_modules/rxjs": {
- "version": "7.8.2",
- "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz",
- "integrity": "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==",
+ "node_modules/require-main-filename": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz",
+ "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==",
+ "license": "ISC"
+ },
+ "node_modules/rollup-plugin-inject": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/rollup-plugin-inject/-/rollup-plugin-inject-3.0.2.tgz",
+ "integrity": "sha512-ptg9PQwzs3orn4jkgXJ74bfs5vYz1NCZlSQMBUA0wKcGp5i5pA1AO3fOUEte8enhGUC+iapTCzEWw2jEFFUO/w==",
+ "deprecated": "This package has been deprecated and is no longer maintained. Please use @rollup/plugin-inject.",
"dev": true,
- "license": "Apache-2.0",
+ "license": "MIT",
+ "dependencies": {
+ "estree-walker": "^0.6.1",
+ "magic-string": "^0.25.3",
+ "rollup-pluginutils": "^2.8.1"
+ }
+ },
+ "node_modules/rollup-plugin-node-polyfills": {
+ "version": "0.2.1",
+ "resolved": "https://registry.npmjs.org/rollup-plugin-node-polyfills/-/rollup-plugin-node-polyfills-0.2.1.tgz",
+ "integrity": "sha512-4kCrKPTJ6sK4/gLL/U5QzVT8cxJcofO0OU74tnB19F40cmuAKSzH5/siithxlofFEjwvw1YAhPmbvGNA6jEroA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "rollup-plugin-inject": "^3.0.0"
+ }
+ },
+ "node_modules/rollup-pluginutils": {
+ "version": "2.8.2",
+ "resolved": "https://registry.npmjs.org/rollup-pluginutils/-/rollup-pluginutils-2.8.2.tgz",
+ "integrity": "sha512-EEp9NhnUkwY8aif6bxgovPHMoMoNr2FulJziTndpt5H9RdwC47GSGuII9XxpSdzVGM0GWrNPHV6ie1LTNJPaLQ==",
+ "dev": true,
+ "license": "MIT",
"dependencies": {
- "tslib": "^2.1.0"
+ "estree-walker": "^0.6.1"
}
},
"node_modules/semver": {
@@ @@ -1790,6 +1527,7 @@
"integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==",
"dev": true,
"license": "ISC",
+ "optional": true,
"bin": {
"semver": "bin/semver.js"
},
@@ @@ -1797,6 +1535,12 @@
"node": ">=10"
}
},
+ "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/sharp": {
"version": "0.33.5",
"resolved": "https://registry.npmjs.org/sharp/-/sharp-0.33.5.tgz",
@@ @@ -1804,6 +1548,7 @@
"dev": true,
"hasInstallScript": true,
"license": "Apache-2.0",
+ "optional": true,
"dependencies": {
"color": "^4.2.3",
"detect-libc": "^2.0.3",
@@ @@ -1837,29 +1582,46 @@
"@img/sharp-win32-x64": "0.33.5"
}
},
- "node_modules/shell-quote": {
- "version": "1.8.3",
- "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.8.3.tgz",
- "integrity": "sha512-ObmnIF4hXNg1BqhnHmgbDETF8dLPCggZWBjkQfhZpbszZnYur5DUljTcCHii5LC3J5E0yeO/1LIMyH+UvHQgyw==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">= 0.4"
- },
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
- }
- },
"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",
+ "optional": true,
"dependencies": {
"is-arrayish": "^0.3.1"
}
},
+ "node_modules/source-map": {
+ "version": "0.6.1",
+ "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
+ "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/sourcemap-codec": {
+ "version": "1.4.8",
+ "resolved": "https://registry.npmjs.org/sourcemap-codec/-/sourcemap-codec-1.4.8.tgz",
+ "integrity": "sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA==",
+ "deprecated": "Please use @jridgewell/sourcemap-codec instead",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/stacktracey": {
+ "version": "2.1.8",
+ "resolved": "https://registry.npmjs.org/stacktracey/-/stacktracey-2.1.8.tgz",
+ "integrity": "sha512-Kpij9riA+UNg7TnphqjH7/CzctQ/owJGNbFkfEeve4Z4uxT5+JapVLFXcsurIfN34gnTWZNJ/f7NMG0E8JDzTw==",
+ "dev": true,
+ "license": "Unlicense",
+ "dependencies": {
+ "as-table": "^1.0.36",
+ "get-source": "^2.0.12"
+ }
+ },
"node_modules/stoppable": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/stoppable/-/stoppable-1.1.0.tgz",
@@ @@ -1875,7 +1637,6 @@
"version": "4.2.3",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
"integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
- "dev": true,
"license": "MIT",
"dependencies": {
"emoji-regex": "^8.0.0",
@@ @@ -1890,7 +1651,6 @@
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
"integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
- "dev": true,
"license": "MIT",
"dependencies": {
"ansi-regex": "^5.0.1"
@@ @@ -1899,35 +1659,13 @@
"node": ">=8"
}
},
- "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/tree-kill": {
- "version": "1.2.2",
- "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz",
- "integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==",
- "dev": true,
- "license": "MIT",
- "bin": {
- "tree-kill": "cli.js"
- }
- },
"node_modules/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"
+ "license": "0BSD",
+ "optional": true
},
"node_modules/ufo": {
"version": "1.6.1",
@@ @@ -1937,33 +1675,42 @@
"license": "MIT"
},
"node_modules/undici": {
- "version": "7.15.0",
- "resolved": "https://registry.npmjs.org/undici/-/undici-7.15.0.tgz",
- "integrity": "sha512-7oZJCPvvMvTd0OlqWsIxTuItTpJBpU1tcbVl24FMn3xt3+VSunwUasmfPJRE57oNO1KsZ4PgA1xTdAX4hq8NyQ==",
+ "version": "5.29.0",
+ "resolved": "https://registry.npmjs.org/undici/-/undici-5.29.0.tgz",
+ "integrity": "sha512-raqeBD6NQK4SkWhQzeYKd1KmIG6dllBOTt55Rmkt4HtI9mwdWtJljnrXjAFUBLTSN67HWrOIZ3EPF4kjUw80Bg==",
"dev": true,
"license": "MIT",
+ "dependencies": {
+ "@fastify/busboy": "^2.0.0"
+ },
"engines": {
- "node": ">=20.18.1"
+ "node": ">=14.0"
}
},
"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==",
+ "version": "2.0.0-rc.14",
+ "resolved": "https://registry.npmjs.org/unenv/-/unenv-2.0.0-rc.14.tgz",
+ "integrity": "sha512-od496pShMen7nOy5VmVJCnq8rptd45vh6Nx/r2iPbrba6pa6p+tS2ywuIHRZ/OBvSbQZB0kWvpO9XBNVFXHD3Q==",
"dev": true,
"license": "MIT",
"dependencies": {
"defu": "^6.1.4",
- "exsolve": "^1.0.7",
- "ohash": "^2.0.11",
+ "exsolve": "^1.0.1",
+ "ohash": "^2.0.10",
"pathe": "^2.0.3",
- "ufo": "^1.6.1"
+ "ufo": "^1.5.4"
}
},
+ "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/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==",
+ "version": "1.20250718.0",
+ "resolved": "https://registry.npmjs.org/workerd/-/workerd-1.20250718.0.tgz",
+ "integrity": "sha512-kqkIJP/eOfDlUyBzU7joBg+tl8aB25gEAGqDap+nFWb+WHhnooxjGHgxPBy3ipw2hnShPFNOQt5lFRxbwALirg==",
"dev": true,
"hasInstallScript": true,
"license": "Apache-2.0",
@@ @@ -1974,41 +1721,44 @@
"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"
+ "@cloudflare/workerd-darwin-64": "1.20250718.0",
+ "@cloudflare/workerd-darwin-arm64": "1.20250718.0",
+ "@cloudflare/workerd-linux-64": "1.20250718.0",
+ "@cloudflare/workerd-linux-arm64": "1.20250718.0",
+ "@cloudflare/workerd-windows-64": "1.20250718.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==",
+ "version": "3.114.14",
+ "resolved": "https://registry.npmjs.org/wrangler/-/wrangler-3.114.14.tgz",
+ "integrity": "sha512-zytHJn5+S47sqgUHi71ieSSP44yj9mKsj0sTUCsY+Tw5zbH8EzB1d9JbRk2KHg7HFM1WpoTI7518EExPGenAmg==",
"dev": true,
"license": "MIT OR Apache-2.0",
"dependencies": {
- "@cloudflare/kv-asset-handler": "0.4.0",
- "@cloudflare/unenv-preset": "2.6.2",
+ "@cloudflare/kv-asset-handler": "0.3.4",
+ "@cloudflare/unenv-preset": "2.0.2",
+ "@esbuild-plugins/node-globals-polyfill": "0.2.3",
+ "@esbuild-plugins/node-modules-polyfill": "0.2.2",
"blake3-wasm": "2.1.5",
- "esbuild": "0.25.4",
- "miniflare": "4.20250816.1",
+ "esbuild": "0.17.19",
+ "miniflare": "3.20250718.1",
"path-to-regexp": "6.3.0",
- "unenv": "2.0.0-rc.19",
- "workerd": "1.20250816.0"
+ "unenv": "2.0.0-rc.14",
+ "workerd": "1.20250718.0"
},
"bin": {
"wrangler": "bin/wrangler.js",
"wrangler2": "bin/wrangler.js"
},
"engines": {
- "node": ">=18.0.0"
+ "node": ">=16.17.0"
},
"optionalDependencies": {
- "fsevents": "~2.3.2"
+ "fsevents": "~2.3.2",
+ "sharp": "^0.33.5"
},
"peerDependencies": {
- "@cloudflare/workers-types": "^4.20250816.0"
+ "@cloudflare/workers-types": "^4.20250408.0"
},
"peerDependenciesMeta": {
"@cloudflare/workers-types": {
@@ @@ -2017,10 +1767,9 @@
}
},
"node_modules/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==",
- "dev": true,
+ "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",
@@ @@ -2028,10 +1777,7 @@
"strip-ansi": "^6.0.0"
},
"engines": {
- "node": ">=10"
- },
- "funding": {
- "url": "https://github.com/chalk/wrap-ansi?sponsor=1"
+ "node": ">=8"
}
},
"node_modules/ws": {
@@ @@ -2057,67 +1803,56 @@
}
},
"node_modules/y18n": {
- "version": "5.0.8",
- "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz",
- "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==",
- "dev": true,
- "license": "ISC",
- "engines": {
- "node": ">=10"
- }
+ "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/yargs": {
- "version": "17.7.2",
- "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz",
- "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==",
- "dev": true,
+ "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": "^8.0.1",
- "escalade": "^3.1.1",
- "get-caller-file": "^2.0.5",
+ "cliui": "^6.0.0",
+ "decamelize": "^1.2.0",
+ "find-up": "^4.1.0",
+ "get-caller-file": "^2.0.1",
"require-directory": "^2.1.1",
- "string-width": "^4.2.3",
- "y18n": "^5.0.5",
- "yargs-parser": "^21.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": ">=12"
+ "node": ">=8"
}
},
"node_modules/yargs-parser": {
- "version": "21.1.1",
- "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz",
- "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==",
- "dev": true,
+ "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": ">=12"
+ "node": ">=6"
}
},
"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==",
+ "version": "3.3.4",
+ "resolved": "https://registry.npmjs.org/youch/-/youch-3.3.4.tgz",
+ "integrity": "sha512-UeVBXie8cA35DS6+nBkls68xaBBXCye0CNznrhszZjTbRVnJKQuNsyLKBTTL4ln1o1rh2PKtv35twV7irj5SEg==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@poppinss/exception": "^1.2.2",
- "error-stack-parser-es": "^1.0.5"
+ "cookie": "^0.7.1",
+ "mustache": "^4.2.0",
+ "stacktracey": "^2.1.8"
}
},
"node_modules/zod": {
package.json +4 -14
@@ @@ -1,27 +1,17 @@
{
"name": "qrurl",
"version": "1.0.0",
- "main": "index.js",
+ "private": true,
"scripts": {
"dev": "wrangler dev",
- "dev:unified": "concurrently \"npm run dev:backend\" \"npm run dev:frontend\"",
- "dev:backend": "wrangler dev --config wrangler.unified.toml --local",
- "dev:frontend": "cd frontend && npm run dev",
"deploy": "wrangler deploy",
- "deploy:unified": "./scripts/deploy-unified.sh",
- "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": {
- "concurrently": "^9.2.0",
- "wrangler": "^4.32.0"
+ "wrangler": "^3.99.0"
},
"dependencies": {
- "postmark": "^4.0.5"
+ "qrcode": "^1.5.4"
}
- }
+ }
\ No newline at end of file
public/favicon.ico +0 -0
public/robots.txt +2 -0
@@ @@ -0,0 +1,2 @@
+ User-Agent: *
+ Disallow:
schema/schema.sql +20 -53
@@ @@ -1,63 +1,30 @@
- -- 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 (
+ -- Simple schema for QRurl
+ CREATE TABLE IF NOT EXISTS links (
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
+ slug TEXT UNIQUE NOT NULL,
+ url TEXT NOT NULL,
+ name TEXT,
+ user_email TEXT,
+ clicks INTEGER DEFAULT 0,
+ created_at DATETIME DEFAULT CURRENT_TIMESTAMP
);
- -- 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,
+ used INTEGER DEFAULT 0,
+ expires_at DATETIME NOT NULL,
+ created_at DATETIME DEFAULT CURRENT_TIMESTAMP
+ );
+
+ CREATE TABLE IF NOT EXISTS sessions (
+ id 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
+ -- Create indexes
+ CREATE INDEX IF NOT EXISTS idx_links_slug ON links(slug);
+ CREATE INDEX IF NOT EXISTS idx_links_user ON links(user_email);
+ CREATE INDEX IF NOT EXISTS idx_sessions_email ON sessions(email);
\ No newline at end of file
src/index.js +227 -78
@@ @@ -1,88 +1,237 @@
- import { Router } from './router';
- import { handleRedirect } from './routes/redirect';
- import { authRoutes } from './routes/auth';
- import { apiRoutes } from './routes/api';
- import { logoRoutes } from './routes/logo';
- import { applyMiddleware } from './middleware';
- import { handleError } from './utils/errors';
- import { addCorsHeaders } from './middleware/cors';
- import { serveStaticAsset } from './lib/static';
+ import QRCode from 'qrcode';
+ import { html } from './pages/home';
+ import { dashboardHtml } from './pages/dashboard';
+ import { loginHtml } from './pages/login';
export default {
async fetch(request, env, ctx) {
- try {
- // Handle OPTIONS 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',
- }
- });
- }
+ const url = new URL(request.url);
+ const path = url.pathname;
+
+ // Serve static assets
+ if (path.endsWith('.js') || path.endsWith('.css')) {
+ return serveStatic(path);
+ }
- 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
+ if (path.startsWith('/api/')) {
+ return handleAPI(request, env, path);
+ }
+
+ // Pages
+ if (path === '/') {
+ return new Response(html(), {
+ headers: { 'Content-Type': 'text/html' }
});
+ }
- // API routes
- router.all('/api/auth/*', authRoutes);
- router.all('/api/logo/*', (request, env, ctx) =>
- applyMiddleware(request, env, ctx, logoRoutes)
- );
- router.all('/api/*', (request, env, ctx) =>
- applyMiddleware(request, env, ctx, apiRoutes)
- );
-
- // Try to serve static assets first (for frontend)
- router.all('*', async (request, env, ctx) => {
- const url = new URL(request.url);
-
- // Skip API routes and health check
- if (url.pathname.startsWith('/api') || url.pathname === '/health') {
- return null; // Let it fall through to 404
- }
-
- // Try serving static asset
- const staticResponse = await serveStaticAsset(request, env, ctx);
- if (staticResponse) {
- return staticResponse;
- }
-
- // Try URL redirect (for short links)
- if (url.pathname.length > 1 && !url.pathname.includes('.')) {
- return handleRedirect(request, env, ctx);
- }
-
- // Default 404 for everything else
- return new Response(JSON.stringify({
- error: 'Not Found',
- message: 'The requested resource was not found'
- }), {
- status: 404,
- headers: { 'Content-Type': 'application/json' }
- });
+ if (path === '/login') {
+ return new Response(loginHtml(), {
+ headers: { 'Content-Type': 'text/html' }
});
+ }
+
+ if (path === '/dashboard') {
+ return new Response(dashboardHtml(), {
+ headers: { 'Content-Type': 'text/html' }
+ });
+ }
+
+ // Check for short link redirect
+ const slug = path.slice(1);
+ if (slug && !slug.includes('/')) {
+ const link = await env.DB.prepare(
+ 'SELECT url FROM links WHERE slug = ?'
+ ).bind(slug).first();
+
+ if (link) {
+ // Update click count
+ await env.DB.prepare(
+ 'UPDATE links SET clicks = clicks + 1 WHERE slug = ?'
+ ).bind(slug).run();
+
+ return Response.redirect(link.url, 301);
+ }
+ }
+
+ return new Response('Not Found', { status: 404 });
+ }
+ };
+
+ async function handleAPI(request, env, path) {
+ const url = new URL(request.url);
+ const method = request.method;
+
+ // Auth endpoints
+ if (path === '/api/auth/request' && method === 'POST') {
+ const { email } = await request.json();
+
+ // Check if email is authorized
+ const authorized = env.AUTHORIZED_EMAILS?.split(',').map(e => e.trim()).includes(email);
+ if (!authorized) {
+ return jsonResponse({ error: 'Unauthorized email' }, 403);
+ }
+
+ // Generate token
+ const token = crypto.randomUUID();
+ const expires = new Date(Date.now() + 15 * 60 * 1000).toISOString();
+
+ await env.DB.prepare(
+ 'INSERT INTO auth_tokens (token, email, expires_at) VALUES (?, ?, ?)'
+ ).bind(token, email, expires).run();
+
+ // Send email via Postmark
+ const emailResponse = await fetch('https://api.postmarkapp.com/email', {
+ method: 'POST',
+ headers: {
+ 'Accept': 'application/json',
+ 'Content-Type': 'application/json',
+ 'X-Postmark-Server-Token': env.EMAIL_API_KEY
+ },
+ body: JSON.stringify({
+ From: env.EMAIL_FROM || 'noreply@qrurl.us',
+ To: email,
+ Subject: 'Your QRurl Login Link',
+ HtmlBody: `
+ <h2>Login to QRurl</h2>
+ <p>Click the link below to log in:</p>
+ <a href="${url.origin}/api/auth/verify?token=${token}">Log In</a>
+ <p>This link expires in 15 minutes.</p>
+ `,
+ TextBody: `Login to QRurl\n\nClick here: ${url.origin}/api/auth/verify?token=${token}`,
+ MessageStream: 'outbound'
+ })
+ });
+
+ if (!emailResponse.ok) {
+ return jsonResponse({ error: 'Failed to send email' }, 500);
+ }
+
+ return jsonResponse({ success: true, message: 'Check your email' });
+ }
- const response = await router.handle(request, env, ctx);
-
- // Add CORS headers to all responses
- return addCorsHeaders(response, env);
- } catch (error) {
- const errorResponse = handleError(error);
- return addCorsHeaders(errorResponse, env);
+ if (path === '/api/auth/verify' && method === 'GET') {
+ const token = url.searchParams.get('token');
+
+ const authToken = await env.DB.prepare(
+ 'SELECT * FROM auth_tokens WHERE token = ? AND used = 0 AND expires_at > datetime("now")'
+ ).bind(token).first();
+
+ if (!authToken) {
+ return new Response('Invalid or expired token', { status: 401 });
+ }
+
+ // Mark token as used
+ await env.DB.prepare(
+ 'UPDATE auth_tokens SET used = 1 WHERE token = ?'
+ ).bind(token).run();
+
+ // Create session
+ const sessionId = crypto.randomUUID();
+ const sessionExpires = new Date(Date.now() + 7 * 24 * 60 * 60 * 1000);
+
+ await env.DB.prepare(
+ 'INSERT INTO sessions (id, email, expires_at) VALUES (?, ?, ?)'
+ ).bind(sessionId, authToken.email, sessionExpires.toISOString()).run();
+
+ // Redirect to dashboard with session cookie
+ return new Response(null, {
+ status: 302,
+ headers: {
+ 'Location': '/dashboard',
+ 'Set-Cookie': `session=${sessionId}; Path=/; HttpOnly; Secure; SameSite=Strict; Expires=${sessionExpires.toUTCString()}`
+ }
+ });
+ }
+
+ // Link management
+ if (path === '/api/links' && method === 'GET') {
+ const session = getSession(request);
+ const links = await env.DB.prepare(
+ session
+ ? 'SELECT * FROM links WHERE user_email = ? ORDER BY created_at DESC'
+ : 'SELECT * FROM links WHERE user_email IS NULL ORDER BY created_at DESC LIMIT 10'
+ ).bind(session?.email).all();
+
+ return jsonResponse(links.results || []);
+ }
+
+ if (path === '/api/links' && method === 'POST') {
+ const { url: targetUrl, name, customSlug } = await request.json();
+
+ if (!targetUrl) {
+ return jsonResponse({ error: 'URL required' }, 400);
+ }
+
+ const session = getSession(request);
+ const slug = customSlug || generateSlug();
+
+ // Check if slug exists
+ const existing = await env.DB.prepare(
+ 'SELECT slug FROM links WHERE slug = ?'
+ ).bind(slug).first();
+
+ if (existing) {
+ return jsonResponse({ error: 'Slug already exists' }, 409);
}
+
+ await env.DB.prepare(
+ 'INSERT INTO links (slug, url, name, user_email) VALUES (?, ?, ?, ?)'
+ ).bind(slug, targetUrl, name || null, session?.email || null).run();
+
+ return jsonResponse({ slug, url: targetUrl });
+ }
+
+ // QR Code generation
+ if (path.startsWith('/api/qr/')) {
+ const slug = path.split('/')[3];
+ const shortUrl = `${url.origin}/${slug}`;
+
+ const qrDataUrl = await QRCode.toDataURL(shortUrl, {
+ errorCorrectionLevel: 'H',
+ width: 400,
+ margin: 1
+ });
+
+ return new Response(qrDataUrl.split(',')[1], {
+ headers: {
+ 'Content-Type': 'image/png',
+ 'Content-Encoding': 'base64'
+ }
+ });
}
- };
\ No newline at end of file
+
+ return jsonResponse({ error: 'Not found' }, 404);
+ }
+
+ function jsonResponse(data, status = 200) {
+ return new Response(JSON.stringify(data), {
+ status,
+ headers: { 'Content-Type': 'application/json' }
+ });
+ }
+
+ function generateSlug(length = 6) {
+ const chars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
+ let slug = '';
+ for (let i = 0; i < length; i++) {
+ slug += chars[Math.floor(Math.random() * chars.length)];
+ }
+ return slug;
+ }
+
+ function getSession(request) {
+ const cookie = request.headers.get('Cookie');
+ if (!cookie) return null;
+
+ const match = cookie.match(/session=([^;]+)/);
+ return match ? { id: match[1] } : null;
+ }
+
+ function serveStatic(path) {
+ // Simple static file serving
+ const contentType = path.endsWith('.js') ? 'application/javascript' : 'text/css';
+ return new Response('', {
+ headers: { 'Content-Type': contentType }
+ });
+ }
\ No newline at end of file
src/pages/dashboard.js +176 -0
@@ @@ -0,0 +1,176 @@
+ export function dashboardHtml() {
+ return `<!DOCTYPE html>
+ <html lang="en">
+ <head>
+ <meta charset="UTF-8">
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
+ <title>Dashboard - QRurl</title>
+ <style>
+ * { margin: 0; padding: 0; box-sizing: border-box; }
+ body { font-family: system-ui, -apple-system, sans-serif; line-height: 1.6; color: #333; background: #f5f5f5; }
+ header { background: #000; color: white; padding: 1rem 0; margin-bottom: 2rem; }
+ header .container { display: flex; justify-content: space-between; align-items: center; }
+ .container { max-width: 1200px; margin: 0 auto; padding: 20px; }
+ h1 { font-size: 1.5rem; }
+ nav a { color: white; text-decoration: none; margin-left: 1rem; }
+ nav a:hover { text-decoration: underline; }
+ .card { background: white; padding: 1.5rem; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); margin-bottom: 2rem; }
+ h2 { margin-bottom: 1rem; }
+ .form-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 1rem; margin-bottom: 1rem; }
+ .form-group { margin-bottom: 1rem; }
+ label { display: block; margin-bottom: 0.5rem; font-weight: 500; }
+ input, button { padding: 0.75rem; font-size: 1rem; border-radius: 4px; }
+ input { width: 100%; border: 1px solid #ddd; }
+ button { background: #000; color: white; border: none; cursor: pointer; }
+ button:hover { background: #333; }
+ .link-item { background: #f9f9f9; padding: 1rem; border-radius: 4px; margin-bottom: 1rem; }
+ .link-header { display: flex; justify-content: space-between; align-items: start; margin-bottom: 0.5rem; }
+ .link-title { font-weight: 600; }
+ .link-actions { display: flex; gap: 0.5rem; }
+ .link-actions button { padding: 0.25rem 0.75rem; font-size: 0.875rem; }
+ .link-url { color: #666; font-size: 0.875rem; margin-bottom: 0.25rem; }
+ .link-stats { color: #999; font-size: 0.875rem; }
+ .modal { display: none; position: fixed; top: 0; left: 0; right: 0; bottom: 0; background: rgba(0,0,0,0.5); align-items: center; justify-content: center; }
+ .modal.show { display: flex; }
+ .modal-content { background: white; padding: 2rem; border-radius: 8px; max-width: 400px; width: 90%; text-align: center; }
+ .modal-content img { max-width: 100%; margin: 1rem 0; }
+ .modal-content button { margin-top: 1rem; }
+ </style>
+ </head>
+ <body>
+ <header>
+ <div class="container">
+ <h1>QRurl Dashboard</h1>
+ <nav>
+ <a href="/">Home</a>
+ <a href="/api/auth/logout">Logout</a>
+ </nav>
+ </div>
+ </header>
+
+ <div class="container">
+ <div class="card">
+ <h2>Create New Link</h2>
+ <form id="createForm">
+ <div class="form-grid">
+ <div class="form-group">
+ <label for="url">URL</label>
+ <input type="url" id="url" required placeholder="https://example.com">
+ </div>
+ <div class="form-group">
+ <label for="name">Name (optional)</label>
+ <input type="text" id="name" placeholder="My Link">
+ </div>
+ </div>
+ <div class="form-group">
+ <label for="customSlug">Custom Slug (optional)</label>
+ <input type="text" id="customSlug" placeholder="my-custom-slug" pattern="[a-zA-Z0-9-]+">
+ </div>
+ <button type="submit">Create Short Link</button>
+ </form>
+ </div>
+
+ <div class="card">
+ <h2>Your Links</h2>
+ <div id="linksList"></div>
+ </div>
+ </div>
+
+ <div id="qrModal" class="modal">
+ <div class="modal-content">
+ <h3>QR Code</h3>
+ <img id="qrImage" alt="QR Code">
+ <button onclick="closeModal()">Close</button>
+ </div>
+ </div>
+
+ <script>
+ let links = [];
+
+ async function loadLinks() {
+ try {
+ const response = await fetch('/api/links');
+ if (response.ok) {
+ links = await response.json();
+ renderLinks();
+ }
+ } catch (error) {
+ console.error('Failed to load links:', error);
+ }
+ }
+
+ function renderLinks() {
+ const container = document.getElementById('linksList');
+ if (links.length === 0) {
+ container.innerHTML = '<p>No links yet. Create your first one above!</p>';
+ return;
+ }
+
+ container.innerHTML = links.map(link => \`
+ <div class="link-item">
+ <div class="link-header">
+ <div>
+ <div class="link-title">\${link.name || 'Untitled'}</div>
+ <div class="link-url">\${window.location.origin}/\${link.slug}</div>
+ <div class="link-url">→ \${link.url}</div>
+ </div>
+ <div class="link-actions">
+ <button onclick="copyLink('\${link.slug}')">Copy</button>
+ <button onclick="showQR('\${link.slug}')">QR Code</button>
+ </div>
+ </div>
+ <div class="link-stats">\${link.clicks || 0} clicks</div>
+ </div>
+ \`).join('');
+ }
+
+ document.getElementById('createForm').addEventListener('submit', async (e) => {
+ e.preventDefault();
+
+ const data = {
+ url: document.getElementById('url').value,
+ name: document.getElementById('name').value,
+ customSlug: document.getElementById('customSlug').value
+ };
+
+ try {
+ const response = await fetch('/api/links', {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify(data)
+ });
+
+ if (response.ok) {
+ document.getElementById('createForm').reset();
+ loadLinks();
+ } else {
+ const error = await response.json();
+ alert(error.error || 'Failed to create link');
+ }
+ } catch (error) {
+ alert('Network error');
+ }
+ });
+
+ function copyLink(slug) {
+ const url = window.location.origin + '/' + slug;
+ navigator.clipboard.writeText(url).then(() => {
+ alert('Copied to clipboard!');
+ });
+ }
+
+ function showQR(slug) {
+ document.getElementById('qrImage').src = '/api/qr/' + slug;
+ document.getElementById('qrModal').classList.add('show');
+ }
+
+ function closeModal() {
+ document.getElementById('qrModal').classList.remove('show');
+ }
+
+ // Load links on page load
+ loadLinks();
+ </script>
+ </body>
+ </html>`;
+ }
\ No newline at end of file
src/pages/home.js +119 -0
@@ @@ -0,0 +1,119 @@
+ export function html() {
+ return `<!DOCTYPE html>
+ <html lang="en">
+ <head>
+ <meta charset="UTF-8">
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
+ <title>QRurl - URL Shortener with QR Codes</title>
+ <style>
+ * { margin: 0; padding: 0; box-sizing: border-box; }
+ body { font-family: system-ui, -apple-system, sans-serif; line-height: 1.6; color: #333; }
+ .container { max-width: 800px; margin: 0 auto; padding: 20px; }
+ header { background: #000; color: white; padding: 1rem 0; margin-bottom: 2rem; }
+ header .container { display: flex; justify-content: space-between; align-items: center; }
+ h1 { font-size: 1.5rem; }
+ nav a { color: white; text-decoration: none; margin-left: 1rem; }
+ nav a:hover { text-decoration: underline; }
+ .hero { text-align: center; padding: 3rem 0; }
+ .hero h2 { font-size: 2.5rem; margin-bottom: 1rem; }
+ .hero p { font-size: 1.2rem; color: #666; margin-bottom: 2rem; }
+ .form-group { margin-bottom: 1rem; }
+ input, button { padding: 0.75rem; font-size: 1rem; border-radius: 4px; }
+ input { width: 100%; border: 1px solid #ddd; }
+ button { background: #000; color: white; border: none; cursor: pointer; width: 100%; }
+ button:hover { background: #333; }
+ .result { margin-top: 2rem; padding: 1rem; background: #f5f5f5; border-radius: 4px; display: none; }
+ .result.show { display: block; }
+ .short-url { display: flex; gap: 1rem; margin-bottom: 1rem; }
+ .short-url input { flex: 1; }
+ .short-url button { width: auto; padding: 0.75rem 1.5rem; }
+ .qr-code { text-align: center; margin-top: 1rem; }
+ .features { display: grid; grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); gap: 2rem; margin-top: 4rem; }
+ .feature { text-align: center; }
+ .feature h3 { margin: 1rem 0; }
+ </style>
+ </head>
+ <body>
+ <header>
+ <div class="container">
+ <h1>QRurl</h1>
+ <nav>
+ <a href="/login">Login</a>
+ <a href="/dashboard">Dashboard</a>
+ </nav>
+ </div>
+ </header>
+
+ <div class="container">
+ <div class="hero">
+ <h2>Shorten URLs & Generate QR Codes</h2>
+ <p>Create short, memorable links with QR codes instantly</p>
+
+ <form id="shortenForm">
+ <div class="form-group">
+ <input type="url" id="urlInput" placeholder="Enter your long URL" required>
+ </div>
+ <button type="submit">Shorten URL</button>
+ </form>
+
+ <div id="result" class="result">
+ <div class="short-url">
+ <input type="text" id="shortUrl" readonly>
+ <button onclick="copyUrl()">Copy</button>
+ </div>
+ <div class="qr-code">
+ <img id="qrCode" alt="QR Code">
+ </div>
+ </div>
+ </div>
+
+ <div class="features">
+ <div class="feature">
+ <h3>⚡ Fast & Reliable</h3>
+ <p>Powered by Cloudflare's global network</p>
+ </div>
+ <div class="feature">
+ <h3>📊 Analytics</h3>
+ <p>Track clicks and engagement</p>
+ </div>
+ <div class="feature">
+ <h3>🎨 QR Codes</h3>
+ <p>Generate QR codes for easy sharing</p>
+ </div>
+ </div>
+ </div>
+
+ <script>
+ document.getElementById('shortenForm').addEventListener('submit', async (e) => {
+ e.preventDefault();
+ const url = document.getElementById('urlInput').value;
+
+ try {
+ const response = await fetch('/api/links', {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ url })
+ });
+
+ const data = await response.json();
+ if (data.slug) {
+ const shortUrl = window.location.origin + '/' + data.slug;
+ document.getElementById('shortUrl').value = shortUrl;
+ document.getElementById('qrCode').src = '/api/qr/' + data.slug;
+ document.getElementById('result').classList.add('show');
+ }
+ } catch (error) {
+ alert('Failed to shorten URL');
+ }
+ });
+
+ function copyUrl() {
+ const input = document.getElementById('shortUrl');
+ input.select();
+ document.execCommand('copy');
+ alert('Copied to clipboard!');
+ }
+ </script>
+ </body>
+ </html>`;
+ }
\ No newline at end of file
src/pages/login.js +89 -0
@@ @@ -0,0 +1,89 @@
+ export function loginHtml() {
+ return `<!DOCTYPE html>
+ <html lang="en">
+ <head>
+ <meta charset="UTF-8">
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
+ <title>Login - QRurl</title>
+ <style>
+ * { margin: 0; padding: 0; box-sizing: border-box; }
+ body { font-family: system-ui, -apple-system, sans-serif; line-height: 1.6; color: #333; display: flex; min-height: 100vh; align-items: center; justify-content: center; background: #f5f5f5; }
+ .login-container { background: white; padding: 2rem; border-radius: 8px; box-shadow: 0 2px 10px rgba(0,0,0,0.1); width: 100%; max-width: 400px; }
+ h1 { margin-bottom: 1.5rem; text-align: center; }
+ .form-group { margin-bottom: 1rem; }
+ label { display: block; margin-bottom: 0.5rem; font-weight: 500; }
+ input, button { width: 100%; padding: 0.75rem; font-size: 1rem; border-radius: 4px; }
+ input { border: 1px solid #ddd; }
+ button { background: #000; color: white; border: none; cursor: pointer; margin-top: 1rem; }
+ button:hover { background: #333; }
+ button:disabled { opacity: 0.5; cursor: not-allowed; }
+ .message { padding: 0.75rem; border-radius: 4px; margin-bottom: 1rem; }
+ .message.success { background: #d4edda; color: #155724; border: 1px solid #c3e6cb; }
+ .message.error { background: #f8d7da; color: #721c24; border: 1px solid #f5c6cb; }
+ .back-link { text-align: center; margin-top: 1rem; }
+ .back-link a { color: #666; text-decoration: none; }
+ .back-link a:hover { text-decoration: underline; }
+ </style>
+ </head>
+ <body>
+ <div class="login-container">
+ <h1>Login to QRurl</h1>
+
+ <div id="message"></div>
+
+ <form id="loginForm">
+ <div class="form-group">
+ <label for="email">Email Address</label>
+ <input type="email" id="email" required placeholder="your@email.com">
+ </div>
+
+ <button type="submit" id="submitBtn">Send Magic Link</button>
+ </form>
+
+ <div class="back-link">
+ <a href="/">← Back to home</a>
+ </div>
+ </div>
+
+ <script>
+ document.getElementById('loginForm').addEventListener('submit', async (e) => {
+ e.preventDefault();
+
+ const email = document.getElementById('email').value;
+ const submitBtn = document.getElementById('submitBtn');
+ const messageDiv = document.getElementById('message');
+
+ submitBtn.disabled = true;
+ submitBtn.textContent = 'Sending...';
+ messageDiv.className = '';
+ messageDiv.textContent = '';
+
+ try {
+ const response = await fetch('/api/auth/request', {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ email })
+ });
+
+ const data = await response.json();
+
+ if (response.ok) {
+ messageDiv.className = 'message success';
+ messageDiv.textContent = 'Check your email for the login link!';
+ document.getElementById('loginForm').reset();
+ } else {
+ messageDiv.className = 'message error';
+ messageDiv.textContent = data.error || 'Failed to send login link';
+ }
+ } catch (error) {
+ messageDiv.className = 'message error';
+ messageDiv.textContent = 'Network error. Please try again.';
+ } finally {
+ submitBtn.disabled = false;
+ submitBtn.textContent = 'Send Magic Link';
+ }
+ });
+ </script>
+ </body>
+ </html>`;
+ }
\ No newline at end of file
wrangler.toml +4 -29
@@ @@ -2,6 +2,8 @@ name = "qrurl"
main = "src/index.js"
compatibility_date = "2024-12-01"
compatibility_flags = ["nodejs_compat"]
+
+ # Account ID
account_id = "b253e6fbfd2f7757cadd0386de5bde3f"
# D1 Database
@@ @@ -10,33 +12,6 @@ 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)
+ # Environment variables (use wrangler secret for sensitive values)
[vars]
- JWT_SECRET = ""
- EMAIL_API_KEY = ""
- EMAIL_FROM = "noreply@qrurl.us"
- 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
+ EMAIL_FROM = "noreply@qrurl.us"
\ No newline at end of file