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 | |