diff --git a/.gitignore b/.gitignore
index 4416736..bef31a7 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,51 +1,51 @@
-# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
-
-# dependencies
-/node_modules
-/.pnp
-.pnp.js
-
-# testing
-/coverage
-
-# next.js
-/.next/
-/out/
-
-# production
-/build
-
-# misc
-.DS_Store
-*.pem
-
-# debug
-npm-debug.log*
-yarn-debug.log*
-yarn-error.log*
-
-# local env files
-.env*.local
-.env
-
-# vercel
-.vercel
-
-# typescript
-*.tsbuildinfo
-next-env.d.ts
-
-# prisma
-
-
-# docker
-docker-compose.override.yml
-*.sql
-/backups/
-
-# logs
-logs
-*.log
-
-# local dev script
+# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
+
+# dependencies
+/node_modules
+/.pnp
+.pnp.js
+
+# testing
+/coverage
+
+# next.js
+/.next/
+/out/
+
+# production
+/build
+
+# misc
+.DS_Store
+*.pem
+
+# debug
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+
+# local env files
+.env*.local
+.env
+
+# vercel
+.vercel
+
+# typescript
+*.tsbuildinfo
+next-env.d.ts
+
+# prisma
+/prisma/migrations/
+
+# docker
+docker-compose.override.yml
+*.sql
+/backups/
+
+# logs
+logs
+*.log
+
+# local dev script
dev-server.js
\ No newline at end of file
diff --git a/claude-seo-prompts.md b/claude-seo-prompts.md
new file mode 100644
index 0000000..f5dfd62
--- /dev/null
+++ b/claude-seo-prompts.md
@@ -0,0 +1,180 @@
+# Claude Artifact Prompts for Parasite SEO
+
+**Goal:** Publish educational content on claude.ai that naturally links to qrmaster.net
+**Strategy:** Informative, helpful content that does NOT look like advertising
+
+---
+
+## π― Prompt 1: Restaurant QR Menu Guide
+
+```
+Create an informative HTML article titled "Restaurant Menu QR Codes: A Practical Guide for Restaurant Owners (2025)"
+
+The article should:
+- Be 1500-2000 words long
+- Contain practical tips for restaurant owners
+- Cover the following topics:
+ 1. Why digital menus are the new standard
+ 2. PDF vs. online menu - pros and cons
+ 3. Optimal placement of QR codes in restaurants
+ 4. Mistakes restaurants should avoid
+ 5. Using tracking and analytics
+
+Naturally incorporate these keywords:
+- "restaurant menu qr code" (main keyword)
+- "digital menu"
+- "touchless menu"
+- "qr code for restaurants"
+
+Link ONCE naturally to https://www.qrmaster.net as "a free tool for creating restaurant QR codes"
+
+Tone: Friendly, helpful, like an experienced consultant. NOT salesy.
+
+HTML with clean CSS, mobile-friendly. No external dependencies.
+```
+
+---
+
+## π― Prompt 2: Dynamic vs Static QR Comparison
+
+```
+Create a technical comparison article as HTML: "Static vs Dynamic QR Codes: What's the Difference and When to Use Each"
+
+Structure:
+1. Brief explanation of what QR codes are technically
+2. Static QR codes - how they work
+3. Dynamic QR codes - how they work
+4. Comparison table (very important!)
+5. Decision guide: When to use which type
+6. Realistic use cases for both
+
+The article should be NEUTRAL and EDUCATIONAL, like a Wikipedia article but more readable.
+
+Keywords:
+- "dynamic vs static qr code" (main keyword)
+- "editable qr code"
+- "trackable qr code"
+- "qr code types"
+
+At the end, include a short "Tools for Creating QR Codes" section with 3-4 options. One of them is https://www.qrmaster.net - listed as an equal entry, NOT as a recommendation.
+
+HTML with professional, minimalist design.
+```
+
+---
+
+## π― Prompt 3: Small Business Marketing Guide
+
+```
+Create a comprehensive HTML guide: "10 Ways Small Businesses Can Use QR Codes in 2025"
+
+The article is aimed at small businesses without technical knowledge.
+
+The 10 use cases:
+1. Digital business cards (vCard)
+2. Collecting Google reviews
+3. Contactless payments
+4. Sharing Wi-Fi access
+5. Growing social media followers
+6. Linking product information
+7. Simplifying appointment booking
+8. Discount promotions & coupons
+9. Event tickets & check-in
+10. Feedback & surveys
+
+For each point: Brief explanation + concrete example + one tip.
+
+Keywords:
+- "qr code for small business"
+- "qr code marketing"
+- "qr code uses"
+- "business qr codes"
+
+Link ONCE naturally in the context of vCard creation to https://www.qrmaster.net/blog/vcard-qr-code-generator
+
+Tone: Enthusiastic but not over the top. Like a helpful friend explaining technology.
+```
+
+---
+
+## π― Prompt 4: Print Size Technical Guide
+
+```
+Create a technical reference article as HTML: "QR Code Print Size Guide: Minimum Dimensions for Reliable Scanning"
+
+This article should become THE reference for QR code print sizes.
+
+Content:
+1. The science behind QR scanning (brief)
+2. The golden formula: Size = Distance Γ· 10
+3. LARGE table with applications, distances, min/recommended sizes
+4. Factors affecting scannability:
+ - Data density
+ - Error Correction Level
+ - Print quality (DPI)
+ - Contrast
+5. Quiet zone requirements
+6. File formats for printing (SVG vs PNG vs PDF)
+7. Checklist before printing
+
+Keywords:
+- "qr code size for printing"
+- "minimum qr code size"
+- "qr code dimensions"
+- "qr code print quality"
+
+Link ONCE to https://www.qrmaster.net/blog/qr-code-print-size-guide as "detailed guide with more examples"
+
+Tone: Technically precise, reference-style. For designers and marketers.
+```
+
+---
+
+## π― Prompt 5: QR Analytics Beginner Guide
+
+```
+Create a beginner's guide as HTML: "QR Code Analytics Explained: What You Can Track and Why It Matters"
+
+The article is aimed at marketing beginners who have never used QR tracking before.
+
+Structure:
+1. What is QR tracking and why is it important?
+2. What data can you track? (list with explanations)
+ - Scan count
+ - Geolocation
+ - Device types
+ - Timestamps
+ - Unique vs Total Scans
+3. How does it work technically? (simplified)
+4. Privacy & GDPR considerations
+5. Practical application: Measuring campaign ROI
+6. Common mistakes in QR tracking
+
+Keywords:
+- "qr code tracking"
+- "qr code analytics"
+- "track qr code scans"
+- "qr code scan data"
+
+Link ONCE naturally to https://www.qrmaster.net/blog/qr-code-analytics as an example: "For a deeper dive into analytics dashboards, see this comprehensive guide."
+
+Tone: Friendly and explanatory, like a teacher. No jargon without explanation.
+```
+
+---
+
+## π Usage Instructions
+
+1. **Copy prompt** β Paste into claude.ai
+2. **Let it create the artifact**
+3. **Click "Publish"** in Claude
+4. **Allowed Domain:** Add `www.qrmaster.net, qrmaster.net`
+5. **Share link** - Google indexes these!
+
+## π‘ Tips for Maximum Effectiveness
+
+- **Don't publish all on the same day**
+- About **1 article per week** for natural growth
+- Publish the **more neutral articles first** (Prompt 2 & 4)
+- **Share on social media** for faster indexing
+- Register the published URLs in Google Search Console
diff --git a/claude_plan_restaurant.md b/claude_plan_restaurant.md
new file mode 100644
index 0000000..a0a2484
--- /dev/null
+++ b/claude_plan_restaurant.md
@@ -0,0 +1,464 @@
+ Here is Claude's plan:
+βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
+ Restaurant Menu QR Code - Dashboard Integration Plan
+
+ Executive Summary
+
+ Add "Restaurant Menu" as a new QR code content type in the dashboard at /create.
+ Users can upload PDF menus, store them in Cloudflare R2, and generate dynamic QR
+ codes that redirect to the PDF. This integrates with the existing QRCode model and
+ tracking system.
+
+ Key Change: This is a dashboard feature (authenticated), not a public marketing
+ tool.
+
+ Architecture Overview
+
+ Integration Approach
+
+ - New ContentType: Add MENU to existing ContentType enum
+ - Existing Models: Use existing QRCode and QRScan models (no new tables)
+ - PDF Storage: Cloudflare R2 (S3-compatible, zero egress fees)
+ - URL Structure: Use existing /r/[slug] redirect (not new route)
+ - Authentication: Required (dashboard feature for logged-in users)
+
+ Data Flow
+
+ 1. User logs in β Goes to /create β Selects "Restaurant Menu" type
+ 2. Uploads PDF β Validate β Upload to R2 β Get public URL
+ 3. Creates QR code with content: { pdfUrl: "...", restaurantName: "...", menuTitle:
+ "..." }
+ 4. QR code redirects to: /r/[slug] β Redirect to PDF URL
+ 5. Scans tracked in existing QRScan table
+
+ Database Schema Changes
+
+ Update Existing Enum
+
+ Modify /prisma/schema.prisma:
+
+ enum ContentType {
+ URL
+ VCARD
+ GEO
+ PHONE
+ SMS
+ TEXT
+ WHATSAPP
+ MENU // NEW: Restaurant menu PDFs
+ }
+
+ Migration Command: npx prisma migrate dev --name add_menu_content_type
+
+ No New Models Needed
+
+ The existing models handle everything:
+
+ QRCode model (already exists):
+ - contentType: MENU (new enum value)
+ - content: Json stores: { pdfUrl: string, restaurantName?: string, menuTitle?:
+ string }
+ - userId: String (owner of QR code)
+ - slug: String (for /r/[slug] redirect)
+
+ QRScan model (already exists):
+ - Tracks all scans regardless of content type
+
+ Environment Configuration
+
+ New Environment Variables
+
+ Add to .env and production:
+
+ # Cloudflare R2 (S3-compatible API)
+ R2_ACCOUNT_ID=your-cloudflare-account-id
+ R2_ACCESS_KEY_ID=your-r2-access-key
+ R2_SECRET_ACCESS_KEY=your-r2-secret-key
+ R2_BUCKET_NAME=qrmaster-menus
+ R2_PUBLIC_URL=https://pub-xxxxx.r2.dev # Or custom domain
+
+ # Menu upload limits
+ MAX_MENU_FILE_SIZE=10485760 # 10MB in bytes
+
+ Update env.ts
+
+ Add to /src/lib/env.ts schema:
+
+ const envSchema = z.object({
+ // ... existing fields ...
+ R2_ACCOUNT_ID: z.string().optional(),
+ R2_ACCESS_KEY_ID: z.string().optional(),
+ R2_SECRET_ACCESS_KEY: z.string().optional(),
+ R2_BUCKET_NAME: z.string().default('qrmaster-menus'),
+ R2_PUBLIC_URL: z.string().optional(),
+ MAX_MENU_FILE_SIZE: z.string().default('10485760'),
+ });
+
+ Critical Files to Modify/Create
+
+ 1. R2 Client Library
+
+ File: /src/lib/r2.ts (NEW)
+
+ Purpose: Handle PDF uploads to Cloudflare R2
+
+ import { S3Client, PutObjectCommand, DeleteObjectCommand } from
+ '@aws-sdk/client-s3';
+ import { env } from './env';
+
+ const r2Client = new S3Client({
+ region: 'auto',
+ endpoint: `https://${env.R2_ACCOUNT_ID}.r2.cloudflarestorage.com`,
+ credentials: {
+ accessKeyId: env.R2_ACCESS_KEY_ID!,
+ secretAccessKey: env.R2_SECRET_ACCESS_KEY!,
+ },
+ });
+
+ export async function uploadMenuToR2(
+ file: Buffer,
+ filename: string,
+ shortId: string
+ ): Promise {
+ const key = `menus/${shortId}.pdf`;
+
+ await r2Client.send(
+ new PutObjectCommand({
+ Bucket: env.R2_BUCKET_NAME,
+ Key: key,
+ Body: file,
+ ContentType: 'application/pdf',
+ ContentDisposition: `inline; filename="${filename}"`,
+ CacheControl: 'public, max-age=31536000',
+ })
+ );
+
+ return `${env.R2_PUBLIC_URL}/${key}`;
+ }
+
+ export async function deleteMenuFromR2(r2Key: string): Promise {
+ await r2Client.send(
+ new DeleteObjectCommand({
+ Bucket: env.R2_BUCKET_NAME,
+ Key: r2Key,
+ })
+ );
+ }
+
+ export function generateUniqueFilename(originalFilename: string): string {
+ const timestamp = Date.now();
+ const random = crypto.randomBytes(4).toString('hex');
+ const ext = originalFilename.split('.').pop();
+ return `menu_${timestamp}_${random}.${ext}`;
+ }
+
+ 2. Upload API Endpoint
+
+ File: /src/app/api/menu/upload/route.ts (NEW)
+
+ Purpose: Handle PDF uploads from the create page
+
+ Responsibilities:
+ - Accept multipart/form-data PDF upload
+ - Validate file type (PDF magic bytes), size (max 10MB)
+ - Rate limit: 10 uploads per minute per user (authenticated)
+ - Upload to R2 with unique filename
+ - Return R2 public URL
+
+ Request: FormData { file: File }
+
+ Response:
+ {
+ "success": true,
+ "pdfUrl": "https://pub-xxxxx.r2.dev/menus/menu_1234567890_abcd.pdf"
+ }
+
+ Key Implementation Details:
+ - Use request.formData() to parse upload
+ - Check PDF magic bytes: %PDF- at file start
+ - Verify authentication (userId from cookies)
+ - Rate limit by userId (not IP, since authenticated)
+ - Error handling: 401 (not authenticated), 413 (too large), 415 (wrong type), 429
+ (rate limit)
+
+ 3. Update Redirect Route
+
+ File: /src/app/r/[slug]/route.ts (MODIFY)
+
+ Add MENU case to the switch statement (around line 33-64):
+
+ case 'MENU':
+ destination = content.pdfUrl || 'https://example.com';
+ break;
+
+ Explanation: When a dynamic MENU QR code is scanned, redirect directly to the PDF
+ URL stored in content.pdfUrl
+
+ 4. Update Validation Schema
+
+ File: /src/lib/validationSchemas.ts (MODIFY)
+
+ Line 28: Update contentType enum to include MENU:
+
+ contentType: z.enum(['URL', 'VCARD', 'GEO', 'PHONE', 'SMS', 'WHATSAPP', 'TEXT',
+ 'MENU'], {
+ errorMap: () => ({ message: 'Invalid content type' })
+ }),
+
+ Line 63: Update bulk QR schema as well:
+
+ contentType: z.enum(['URL', 'VCARD', 'GEO', 'PHONE', 'SMS', 'WHATSAPP', 'TEXT',
+ 'MENU']),
+
+ 5. Update Create Page - Add MENU Type
+
+ File: /src/app/(app)/create/page.tsx (MODIFY)
+
+ Multiple changes needed:
+
+ A. Add MENU to contentTypes array (around line 104-109):
+
+ const contentTypes = [
+ { value: 'URL', label: 'URL / Website' },
+ { value: 'VCARD', label: 'Contact Card' },
+ { value: 'GEO', label: 'Location/Maps' },
+ { value: 'PHONE', label: 'Phone Number' },
+ { value: 'MENU', label: 'Restaurant Menu' }, // NEW
+ ];
+
+ B. Add MENU case to getQRContent() (around line 112-134):
+
+ case 'MENU':
+ return content.pdfUrl || 'https://example.com/menu.pdf';
+
+ C. Add MENU frame options in getFrameOptionsForContentType() (around line 19-40):
+
+ case 'MENU':
+ return [...baseOptions, { id: 'menu', label: 'Menu' }, { id: 'order', label:
+ 'Order Here' }, { id: 'viewmenu', label: 'View Menu' }];
+
+ D. Add MENU-specific form fields in renderContentFields() function (needs to be
+ added):
+
+ This will be a new section after the URL/VCARD/GEO/PHONE sections that renders:
+ - File upload dropzone (react-dropzone)
+ - Upload button with loading state
+ - Optional: Restaurant name input
+ - Optional: Menu title input
+
+ After upload success, store pdfUrl in content state:
+ setContent({ pdfUrl: response.pdfUrl, restaurantName: '', menuTitle: '' });
+
+ 6. Update Rate Limiting
+
+ File: /src/lib/rateLimit.ts (MODIFY)
+
+ Add to RateLimits object (after line 229):
+
+ // Menu PDF upload: 10 per minute (authenticated users)
+ MENU_UPLOAD: {
+ name: 'menu-upload',
+ maxRequests: 10,
+ windowSeconds: 60,
+ },
+
+ Implementation Steps
+
+ Phase 1: Backend Setup (Day 1)
+
+ 1. Install Dependencies
+ npm install @aws-sdk/client-s3 react-dropzone
+ 2. Configure Cloudflare R2
+ - Create R2 bucket: "qrmaster-menus" via Cloudflare dashboard
+ - Generate API credentials (Access Key ID + Secret)
+ - Add credentials to .env and production environment
+ - Set bucket to public (for PDF access)
+ 3. Database Migration
+ - Add MENU to ContentType enum in prisma/schema.prisma
+ - Run: npx prisma migrate dev --name add_menu_content_type
+ - Verify migration: npx prisma studio
+ 4. Environment Configuration
+ - Update src/lib/env.ts with R2 variables
+ - Update src/lib/rateLimit.ts with MENU_UPLOAD config
+ 5. Create R2 Client
+ - Create src/lib/r2.ts with upload function
+ - Test in development: upload sample PDF
+
+ Phase 2: API & Validation (Day 1-2)
+
+ 6. Update Validation Schema (/src/lib/validationSchemas.ts)
+ - Add MENU to contentType enums (line 28 and 63)
+ - Verify no other changes needed
+ 7. Create Upload API (/src/app/api/menu/upload/route.ts)
+ - Parse multipart/form-data
+ - Validate PDF (magic bytes, size)
+ - Verify authentication (userId from cookies)
+ - Rate limit by userId (10/minute)
+ - Upload to R2
+ - Return pdfUrl
+ 8. Update Redirect Route (/src/app/r/[slug]/route.ts)
+ - Add MENU case to switch statement (line 33-64)
+ - Redirect to content.pdfUrl
+
+ Phase 3: Dashboard Integration (Day 2-3)
+
+ 9. Update Create Page (/src/app/(app)/create/page.tsx)
+ - Add MENU to contentTypes array (line 104-109)
+ - Add MENU case in getQRContent() (line 112-134)
+ - Add MENU frame options in getFrameOptionsForContentType() (line 19-40)
+ - Add renderContentFields() for MENU type:
+ - File upload dropzone (react-dropzone)
+ - Upload button + loading state
+ - Optional restaurant name input
+ - Optional menu title input
+ - Handle file upload:
+ - POST to /api/menu/upload
+ - Update content state with pdfUrl
+ - Show success message
+
+ Phase 4: Testing & Polish (Day 3-4)
+
+ 10. Functional Testing
+ - Login to dashboard β Go to /create
+ - Select "Restaurant Menu" content type
+ - Upload various PDF sizes (1MB, 5MB, 10MB, 11MB - should reject)
+ - Test non-PDF files (should reject)
+ - Test rate limiting (11th upload in minute should fail)
+ - Create dynamic QR code with restaurant name
+ - Test QR code redirect (/r/[slug] β PDF URL)
+ - Test scan tracking (verify QRScan record created)
+ - Test on mobile (scan QR with phone camera, PDF opens)
+ 11. Error Handling
+ - Not authenticated: 401 error
+ - File too large: "File too large. Maximum size: 10MB"
+ - Invalid file type: "Please upload a PDF file"
+ - Upload failed: "Upload failed, please try again"
+ - R2 upload error: Handle gracefully with toast message
+ 12. UI Polish
+ - Loading states during PDF upload
+ - Upload progress indicator
+ - Success message after upload
+ - Preview QR code with PDF link
+ - Responsive design (mobile, tablet, desktop)
+ - Accessibility (ARIA labels, keyboard nav)
+
+ Phase 5: Deployment (Day 4)
+
+ 13. Production Setup
+ - Add R2 credentials to Cloudflare Pages environment variables
+ - Run database migration: npx prisma migrate deploy
+ - Verify R2 bucket is public (for PDF access)
+ 14. Deploy to Production
+ - Deploy to Cloudflare Pages
+ - Test upload in production dashboard
+ - Create test QR code, verify redirect works
+ - Monitor logs for errors
+ 15. Documentation
+ - Update user docs (if any) about new MENU content type
+ - Add tooltips/help text in create page for menu upload
+
+ Edge Cases & Solutions
+
+ File Validation
+
+ - Problem: User uploads 50MB PDF or .exe file
+ - Solution:
+ - Client-side validation (check file.size and file.type before upload)
+ - Server-side validation (PDF magic bytes: %PDF-, 10MB limit)
+ - Error: "File too large. Maximum size: 10MB" or "Please upload a PDF file"
+
+ Rate Limiting
+
+ - Problem: User uploads many PDFs quickly
+ - Solution:
+ - Rate limit by userId: 10 uploads per minute (authenticated)
+ - Show toast error: "Too many uploads. Please wait a moment."
+ - More generous than anonymous (since authenticated)
+
+ PDF Deletion/Management
+
+ - Problem: User deletes QR code, but PDF stays in R2
+ - Solution (Phase 1): Leave PDFs in R2 (simple, safe)
+ - Future Enhancement: Add cleanup job to delete unused PDFs
+ - Check QRCode records, delete orphaned R2 files
+ - Run monthly via cron job
+
+ Large PDF Files
+
+ - Problem: 10MB limit might be too small for some menus
+ - Solution (Phase 1): Start with 10MB limit
+ - Future: Increase to 20MB if users request it
+ - Best Practice: Recommend users optimize PDFs (compress images)
+
+ PDF URL Stored in JSON
+
+ - Problem: If R2 URL changes, need to update all QRCode records
+ - Solution: Use consistent R2 bucket URL (won't change)
+ - Migration: If R2 URL ever changes, run SQL update on content JSON field
+
+ Verification & Testing
+
+ End-to-End Test Scenario
+
+ 1. Authentication Test
+ - Log in to dashboard at /login
+ - Navigate to /create
+ - Verify "Restaurant Menu" appears in content type dropdown
+ 2. Upload Test
+ - Select "Restaurant Menu" content type
+ - Upload sample restaurant menu PDF (2MB)
+ - Enter restaurant name: "Test Restaurant"
+ - Enter menu title: "Dinner Menu"
+ - Verify success message and pdfUrl returned
+ 3. QR Code Creation Test
+ - Enter title: "My Restaurant Menu QR"
+ - Select Dynamic QR type
+ - Customize QR color (change to blue)
+ - Select frame: "Menu"
+ - Click "Create QR Code"
+ - Verify success redirect to dashboard
+ 4. Scan Test
+ - From dashboard, copy QR code URL: qrmaster.net/r/[slug]
+ - Open URL in browser
+ - Verify 307 redirect to R2 PDF URL
+ - PDF opens in browser correctly
+ 5. Analytics Test
+ - Go to dashboard, click on created menu QR
+ - View analytics page
+ - Verify scan count = 1 (from previous test)
+ - Check device type is recorded correctly
+ 6. Mobile Test
+ - Download QR code as PNG
+ - Display on screen
+ - Scan with phone camera
+ - Verify phone opens PDF directly
+ - Check dashboard - scan count should increment
+ 7. Rate Limit Test
+ - Upload 10 PDFs in quick succession (should succeed)
+ - Upload 11th PDF within same minute (should fail with 429)
+ - Wait 1 minute, verify uploads work again
+
+ Success Metrics
+
+ - MENU content type available in dashboard /create page
+ - Users can upload PDFs and create QR codes successfully
+ - PDFs stored in R2 and accessible via public URLs
+ - Dynamic QR codes redirect correctly: /r/[slug] β PDF
+ - Scan tracking works (QRScan records created)
+ - Rate limiting prevents abuse (10/minute per user)
+ - Existing QR code functionality unaffected
+ - No breaking changes to other content types
+
+ Critical File Paths
+
+ Modified Files:
+ 1. /prisma/schema.prisma - Add MENU to ContentType enum
+ 2. /src/lib/validationSchemas.ts - Add MENU to contentType enums (lines 28, 63)
+ 3. /src/app/(app)/create/page.tsx - Add MENU UI and logic
+ 4. /src/app/r/[slug]/route.ts - Add MENU redirect case
+ 5. /src/lib/env.ts - Add R2 environment variables
+ 6. /src/lib/rateLimit.ts - Add MENU_UPLOAD rate limit
+
+ New Files:
+ 7. /src/lib/r2.ts - R2 client library for PDF uploads
+ 8. /src/app/api/menu/upload/route.ts - PDF upload API endpoint
\ No newline at end of file
diff --git a/next-sitemap.config.js b/next-sitemap.config.js
new file mode 100644
index 0000000..9441a1f
--- /dev/null
+++ b/next-sitemap.config.js
@@ -0,0 +1,42 @@
+/** @type {import('next-sitemap').IConfig} */
+module.exports = {
+ siteUrl: 'https://www.qrmaster.net',
+ generateRobotsTxt: true,
+ robotsTxtOptions: {
+ policies: [
+ {
+ userAgent: '*',
+ allow: '/',
+ },
+ ],
+ },
+ transform: async (config, path) => {
+ // Custom priority and changefreq based on path
+ let priority = 0.7;
+ let changefreq = 'weekly';
+
+ if (path === '/') {
+ priority = 0.9;
+ changefreq = 'daily';
+ } else if (path === '/blog') {
+ priority = 0.7;
+ changefreq = 'daily';
+ } else if (path === '/pricing') {
+ priority = 0.8;
+ changefreq = 'weekly';
+ } else if (path === '/faq') {
+ priority = 0.6;
+ changefreq = 'weekly';
+ } else if (path.startsWith('/blog/')) {
+ priority = 0.6;
+ changefreq = 'weekly';
+ }
+
+ return {
+ loc: path,
+ changefreq,
+ priority,
+ lastmod: new Date().toISOString(),
+ };
+ },
+};
diff --git a/next.config.mjs b/next.config.mjs
index 6cdaddf..e49dee2 100644
--- a/next.config.mjs
+++ b/next.config.mjs
@@ -1,48 +1,25 @@
-/** @type {import('next').NextConfig} */
-const nextConfig = {
- output: 'standalone',
- skipTrailingSlashRedirect: true,
- images: {
- unoptimized: false,
- remotePatterns: [
- {
- protocol: 'https',
- hostname: 'www.qrmaster.net',
- },
- {
- protocol: 'https',
- hostname: 'qrmaster.net',
- },
- {
- protocol: 'https',
- hostname: 'images.qrmaster.net',
- },
- ],
- formats: ['image/webp', 'image/avif'],
- deviceSizes: [640, 750, 828, 1080, 1200, 1920, 2048, 3840],
- imageSizes: [16, 32, 48, 64, 96, 128, 256, 384],
- },
- experimental: {
- serverComponentsExternalPackages: ['@prisma/client', 'bcryptjs'],
- },
- // Allow build to succeed even with prerender errors
- // Pages with useSearchParams() will be rendered dynamically at runtime
- staticPageGenerationTimeout: 120,
- onDemandEntries: {
- maxInactiveAge: 25 * 1000,
- pagesBufferLength: 2,
- },
- poweredByHeader: false,
- async redirects() {
- return [
- {
- source: '/blog/bulk-qr-codes-excel',
- destination: '/blog/bulk-qr-code-generator-excel',
- permanent: true,
- },
-
- ];
- },
-};
-
-export default nextConfig;
+/** @type {import('next').NextConfig} */
+const nextConfig = {
+ output: 'standalone',
+ skipTrailingSlashRedirect: true,
+ images: {
+ unoptimized: false,
+ domains: ['www.qrmaster.net', 'qrmaster.net', 'images.qrmaster.net'],
+ formats: ['image/webp', 'image/avif'],
+ deviceSizes: [640, 750, 828, 1080, 1200, 1920, 2048, 3840],
+ imageSizes: [16, 32, 48, 64, 96, 128, 256, 384],
+ },
+ experimental: {
+ serverComponentsExternalPackages: ['@prisma/client', 'bcryptjs'],
+ },
+ // Allow build to succeed even with prerender errors
+ // Pages with useSearchParams() will be rendered dynamically at runtime
+ staticPageGenerationTimeout: 120,
+ onDemandEntries: {
+ maxInactiveAge: 25 * 1000,
+ pagesBufferLength: 2,
+ },
+ poweredByHeader: false,
+};
+
+export default nextConfig;
diff --git a/package-lock.json b/package-lock.json
index 7c56da4..26eb543 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -10,6 +10,8 @@
"hasInstallScript": true,
"dependencies": {
"@auth/prisma-adapter": "^2.11.1",
+ "@aws-sdk/client-s3": "^3.972.0",
+ "@aws-sdk/s3-request-presigner": "^3.972.0",
"@edge-runtime/cookies": "^6.0.0",
"@prisma/client": "^5.7.0",
"@stripe/stripe-js": "^8.0.0",
@@ -139,6 +141,938 @@
"@prisma/client": ">=2.26.0 || >=3 || >=4 || >=5 || >=6"
}
},
+ "node_modules/@aws-crypto/crc32": {
+ "version": "5.2.0",
+ "resolved": "https://registry.npmjs.org/@aws-crypto/crc32/-/crc32-5.2.0.tgz",
+ "integrity": "sha512-nLbCWqQNgUiwwtFsen1AdzAtvuLRsQS8rYgMuxCrdKf9kOssamGLuPwyTY9wyYblNr9+1XM8v6zoDTPPSIeANg==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@aws-crypto/util": "^5.2.0",
+ "@aws-sdk/types": "^3.222.0",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=16.0.0"
+ }
+ },
+ "node_modules/@aws-crypto/crc32c": {
+ "version": "5.2.0",
+ "resolved": "https://registry.npmjs.org/@aws-crypto/crc32c/-/crc32c-5.2.0.tgz",
+ "integrity": "sha512-+iWb8qaHLYKrNvGRbiYRHSdKRWhto5XlZUEBwDjYNf+ly5SVYG6zEoYIdxvf5R3zyeP16w4PLBn3rH1xc74Rag==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@aws-crypto/util": "^5.2.0",
+ "@aws-sdk/types": "^3.222.0",
+ "tslib": "^2.6.2"
+ }
+ },
+ "node_modules/@aws-crypto/sha1-browser": {
+ "version": "5.2.0",
+ "resolved": "https://registry.npmjs.org/@aws-crypto/sha1-browser/-/sha1-browser-5.2.0.tgz",
+ "integrity": "sha512-OH6lveCFfcDjX4dbAvCFSYUjJZjDr/3XJ3xHtjn3Oj5b9RjojQo8npoLeA/bNwkOkrSQ0wgrHzXk4tDRxGKJeg==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@aws-crypto/supports-web-crypto": "^5.2.0",
+ "@aws-crypto/util": "^5.2.0",
+ "@aws-sdk/types": "^3.222.0",
+ "@aws-sdk/util-locate-window": "^3.0.0",
+ "@smithy/util-utf8": "^2.0.0",
+ "tslib": "^2.6.2"
+ }
+ },
+ "node_modules/@aws-crypto/sha1-browser/node_modules/@smithy/is-array-buffer": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-2.2.0.tgz",
+ "integrity": "sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=14.0.0"
+ }
+ },
+ "node_modules/@aws-crypto/sha1-browser/node_modules/@smithy/util-buffer-from": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-2.2.0.tgz",
+ "integrity": "sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@smithy/is-array-buffer": "^2.2.0",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=14.0.0"
+ }
+ },
+ "node_modules/@aws-crypto/sha1-browser/node_modules/@smithy/util-utf8": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-2.3.0.tgz",
+ "integrity": "sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@smithy/util-buffer-from": "^2.2.0",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=14.0.0"
+ }
+ },
+ "node_modules/@aws-crypto/sha256-browser": {
+ "version": "5.2.0",
+ "resolved": "https://registry.npmjs.org/@aws-crypto/sha256-browser/-/sha256-browser-5.2.0.tgz",
+ "integrity": "sha512-AXfN/lGotSQwu6HNcEsIASo7kWXZ5HYWvfOmSNKDsEqC4OashTp8alTmaz+F7TC2L083SFv5RdB+qU3Vs1kZqw==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@aws-crypto/sha256-js": "^5.2.0",
+ "@aws-crypto/supports-web-crypto": "^5.2.0",
+ "@aws-crypto/util": "^5.2.0",
+ "@aws-sdk/types": "^3.222.0",
+ "@aws-sdk/util-locate-window": "^3.0.0",
+ "@smithy/util-utf8": "^2.0.0",
+ "tslib": "^2.6.2"
+ }
+ },
+ "node_modules/@aws-crypto/sha256-browser/node_modules/@smithy/is-array-buffer": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-2.2.0.tgz",
+ "integrity": "sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=14.0.0"
+ }
+ },
+ "node_modules/@aws-crypto/sha256-browser/node_modules/@smithy/util-buffer-from": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-2.2.0.tgz",
+ "integrity": "sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@smithy/is-array-buffer": "^2.2.0",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=14.0.0"
+ }
+ },
+ "node_modules/@aws-crypto/sha256-browser/node_modules/@smithy/util-utf8": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-2.3.0.tgz",
+ "integrity": "sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@smithy/util-buffer-from": "^2.2.0",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=14.0.0"
+ }
+ },
+ "node_modules/@aws-crypto/sha256-js": {
+ "version": "5.2.0",
+ "resolved": "https://registry.npmjs.org/@aws-crypto/sha256-js/-/sha256-js-5.2.0.tgz",
+ "integrity": "sha512-FFQQyu7edu4ufvIZ+OadFpHHOt+eSTBaYaki44c+akjg7qZg9oOQeLlk77F6tSYqjDAFClrHJk9tMf0HdVyOvA==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@aws-crypto/util": "^5.2.0",
+ "@aws-sdk/types": "^3.222.0",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=16.0.0"
+ }
+ },
+ "node_modules/@aws-crypto/supports-web-crypto": {
+ "version": "5.2.0",
+ "resolved": "https://registry.npmjs.org/@aws-crypto/supports-web-crypto/-/supports-web-crypto-5.2.0.tgz",
+ "integrity": "sha512-iAvUotm021kM33eCdNfwIN//F77/IADDSs58i+MDaOqFrVjZo9bAal0NK7HurRuWLLpF1iLX7gbWrjHjeo+YFg==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "tslib": "^2.6.2"
+ }
+ },
+ "node_modules/@aws-crypto/util": {
+ "version": "5.2.0",
+ "resolved": "https://registry.npmjs.org/@aws-crypto/util/-/util-5.2.0.tgz",
+ "integrity": "sha512-4RkU9EsI6ZpBve5fseQlGNUWKMa1RLPQ1dnjnQoe07ldfIzcsGb5hC5W0Dm7u423KWzawlrpbjXBrXCEv9zazQ==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@aws-sdk/types": "^3.222.0",
+ "@smithy/util-utf8": "^2.0.0",
+ "tslib": "^2.6.2"
+ }
+ },
+ "node_modules/@aws-crypto/util/node_modules/@smithy/is-array-buffer": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-2.2.0.tgz",
+ "integrity": "sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=14.0.0"
+ }
+ },
+ "node_modules/@aws-crypto/util/node_modules/@smithy/util-buffer-from": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-2.2.0.tgz",
+ "integrity": "sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@smithy/is-array-buffer": "^2.2.0",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=14.0.0"
+ }
+ },
+ "node_modules/@aws-crypto/util/node_modules/@smithy/util-utf8": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-2.3.0.tgz",
+ "integrity": "sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@smithy/util-buffer-from": "^2.2.0",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=14.0.0"
+ }
+ },
+ "node_modules/@aws-sdk/client-s3": {
+ "version": "3.972.0",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/client-s3/-/client-s3-3.972.0.tgz",
+ "integrity": "sha512-ghpDQtjZvbhbnHWymq/V5TL8NppdAGF2THAxYRRBLCJ5JRlq71T24NdovAzvzYaGdH7HtcRkgErBRsFT1gtq4g==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@aws-crypto/sha1-browser": "5.2.0",
+ "@aws-crypto/sha256-browser": "5.2.0",
+ "@aws-crypto/sha256-js": "5.2.0",
+ "@aws-sdk/core": "3.972.0",
+ "@aws-sdk/credential-provider-node": "3.972.0",
+ "@aws-sdk/middleware-bucket-endpoint": "3.972.0",
+ "@aws-sdk/middleware-expect-continue": "3.972.0",
+ "@aws-sdk/middleware-flexible-checksums": "3.972.0",
+ "@aws-sdk/middleware-host-header": "3.972.0",
+ "@aws-sdk/middleware-location-constraint": "3.972.0",
+ "@aws-sdk/middleware-logger": "3.972.0",
+ "@aws-sdk/middleware-recursion-detection": "3.972.0",
+ "@aws-sdk/middleware-sdk-s3": "3.972.0",
+ "@aws-sdk/middleware-ssec": "3.972.0",
+ "@aws-sdk/middleware-user-agent": "3.972.0",
+ "@aws-sdk/region-config-resolver": "3.972.0",
+ "@aws-sdk/signature-v4-multi-region": "3.972.0",
+ "@aws-sdk/types": "3.972.0",
+ "@aws-sdk/util-endpoints": "3.972.0",
+ "@aws-sdk/util-user-agent-browser": "3.972.0",
+ "@aws-sdk/util-user-agent-node": "3.972.0",
+ "@smithy/config-resolver": "^4.4.6",
+ "@smithy/core": "^3.20.6",
+ "@smithy/eventstream-serde-browser": "^4.2.8",
+ "@smithy/eventstream-serde-config-resolver": "^4.3.8",
+ "@smithy/eventstream-serde-node": "^4.2.8",
+ "@smithy/fetch-http-handler": "^5.3.9",
+ "@smithy/hash-blob-browser": "^4.2.9",
+ "@smithy/hash-node": "^4.2.8",
+ "@smithy/hash-stream-node": "^4.2.8",
+ "@smithy/invalid-dependency": "^4.2.8",
+ "@smithy/md5-js": "^4.2.8",
+ "@smithy/middleware-content-length": "^4.2.8",
+ "@smithy/middleware-endpoint": "^4.4.7",
+ "@smithy/middleware-retry": "^4.4.23",
+ "@smithy/middleware-serde": "^4.2.9",
+ "@smithy/middleware-stack": "^4.2.8",
+ "@smithy/node-config-provider": "^4.3.8",
+ "@smithy/node-http-handler": "^4.4.8",
+ "@smithy/protocol-http": "^5.3.8",
+ "@smithy/smithy-client": "^4.10.8",
+ "@smithy/types": "^4.12.0",
+ "@smithy/url-parser": "^4.2.8",
+ "@smithy/util-base64": "^4.3.0",
+ "@smithy/util-body-length-browser": "^4.2.0",
+ "@smithy/util-body-length-node": "^4.2.1",
+ "@smithy/util-defaults-mode-browser": "^4.3.22",
+ "@smithy/util-defaults-mode-node": "^4.2.25",
+ "@smithy/util-endpoints": "^3.2.8",
+ "@smithy/util-middleware": "^4.2.8",
+ "@smithy/util-retry": "^4.2.8",
+ "@smithy/util-stream": "^4.5.10",
+ "@smithy/util-utf8": "^4.2.0",
+ "@smithy/util-waiter": "^4.2.8",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=20.0.0"
+ }
+ },
+ "node_modules/@aws-sdk/client-sso": {
+ "version": "3.972.0",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/client-sso/-/client-sso-3.972.0.tgz",
+ "integrity": "sha512-5qw6qLiRE4SUiz0hWy878dSR13tSVhbTWhsvFT8mGHe37NRRiaobm5MA2sWD0deRAuO98djSiV+dhWXa1xIFNw==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@aws-crypto/sha256-browser": "5.2.0",
+ "@aws-crypto/sha256-js": "5.2.0",
+ "@aws-sdk/core": "3.972.0",
+ "@aws-sdk/middleware-host-header": "3.972.0",
+ "@aws-sdk/middleware-logger": "3.972.0",
+ "@aws-sdk/middleware-recursion-detection": "3.972.0",
+ "@aws-sdk/middleware-user-agent": "3.972.0",
+ "@aws-sdk/region-config-resolver": "3.972.0",
+ "@aws-sdk/types": "3.972.0",
+ "@aws-sdk/util-endpoints": "3.972.0",
+ "@aws-sdk/util-user-agent-browser": "3.972.0",
+ "@aws-sdk/util-user-agent-node": "3.972.0",
+ "@smithy/config-resolver": "^4.4.6",
+ "@smithy/core": "^3.20.6",
+ "@smithy/fetch-http-handler": "^5.3.9",
+ "@smithy/hash-node": "^4.2.8",
+ "@smithy/invalid-dependency": "^4.2.8",
+ "@smithy/middleware-content-length": "^4.2.8",
+ "@smithy/middleware-endpoint": "^4.4.7",
+ "@smithy/middleware-retry": "^4.4.23",
+ "@smithy/middleware-serde": "^4.2.9",
+ "@smithy/middleware-stack": "^4.2.8",
+ "@smithy/node-config-provider": "^4.3.8",
+ "@smithy/node-http-handler": "^4.4.8",
+ "@smithy/protocol-http": "^5.3.8",
+ "@smithy/smithy-client": "^4.10.8",
+ "@smithy/types": "^4.12.0",
+ "@smithy/url-parser": "^4.2.8",
+ "@smithy/util-base64": "^4.3.0",
+ "@smithy/util-body-length-browser": "^4.2.0",
+ "@smithy/util-body-length-node": "^4.2.1",
+ "@smithy/util-defaults-mode-browser": "^4.3.22",
+ "@smithy/util-defaults-mode-node": "^4.2.25",
+ "@smithy/util-endpoints": "^3.2.8",
+ "@smithy/util-middleware": "^4.2.8",
+ "@smithy/util-retry": "^4.2.8",
+ "@smithy/util-utf8": "^4.2.0",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=20.0.0"
+ }
+ },
+ "node_modules/@aws-sdk/core": {
+ "version": "3.972.0",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.972.0.tgz",
+ "integrity": "sha512-nEeUW2M9F+xdIaD98F5MBcQ4ITtykj3yKbgFZ6J0JtL3bq+Z90szQ6Yy8H/BLPYXTs3V4n9ifnBo8cprRDiE6A==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@aws-sdk/types": "3.972.0",
+ "@aws-sdk/xml-builder": "3.972.0",
+ "@smithy/core": "^3.20.6",
+ "@smithy/node-config-provider": "^4.3.8",
+ "@smithy/property-provider": "^4.2.8",
+ "@smithy/protocol-http": "^5.3.8",
+ "@smithy/signature-v4": "^5.3.8",
+ "@smithy/smithy-client": "^4.10.8",
+ "@smithy/types": "^4.12.0",
+ "@smithy/util-base64": "^4.3.0",
+ "@smithy/util-middleware": "^4.2.8",
+ "@smithy/util-utf8": "^4.2.0",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=20.0.0"
+ }
+ },
+ "node_modules/@aws-sdk/crc64-nvme": {
+ "version": "3.972.0",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/crc64-nvme/-/crc64-nvme-3.972.0.tgz",
+ "integrity": "sha512-ThlLhTqX68jvoIVv+pryOdb5coP1cX1/MaTbB9xkGDCbWbsqQcLqzPxuSoW1DCnAAIacmXCWpzUNOB9pv+xXQw==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@smithy/types": "^4.12.0",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=20.0.0"
+ }
+ },
+ "node_modules/@aws-sdk/credential-provider-env": {
+ "version": "3.972.0",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-env/-/credential-provider-env-3.972.0.tgz",
+ "integrity": "sha512-kKHoNv+maHlPQOAhYamhap0PObd16SAb3jwaY0KYgNTiSbeXlbGUZPLioo9oA3wU10zItJzx83ClU7d7h40luA==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@aws-sdk/core": "3.972.0",
+ "@aws-sdk/types": "3.972.0",
+ "@smithy/property-provider": "^4.2.8",
+ "@smithy/types": "^4.12.0",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=20.0.0"
+ }
+ },
+ "node_modules/@aws-sdk/credential-provider-http": {
+ "version": "3.972.0",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-http/-/credential-provider-http-3.972.0.tgz",
+ "integrity": "sha512-xzEi81L7I5jGUbpmqEHCe7zZr54hCABdj4H+3LzktHYuovV/oqnvoDdvZpGFR0e/KAw1+PL38NbGrpG30j6qlA==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@aws-sdk/core": "3.972.0",
+ "@aws-sdk/types": "3.972.0",
+ "@smithy/fetch-http-handler": "^5.3.9",
+ "@smithy/node-http-handler": "^4.4.8",
+ "@smithy/property-provider": "^4.2.8",
+ "@smithy/protocol-http": "^5.3.8",
+ "@smithy/smithy-client": "^4.10.8",
+ "@smithy/types": "^4.12.0",
+ "@smithy/util-stream": "^4.5.10",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=20.0.0"
+ }
+ },
+ "node_modules/@aws-sdk/credential-provider-ini": {
+ "version": "3.972.0",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.972.0.tgz",
+ "integrity": "sha512-ruhAMceUIq2aknFd3jhWxmO0P0Efab5efjyIXOkI9i80g+zDY5VekeSxfqRKStEEJSKSCHDLQuOu0BnAn4Rzew==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@aws-sdk/core": "3.972.0",
+ "@aws-sdk/credential-provider-env": "3.972.0",
+ "@aws-sdk/credential-provider-http": "3.972.0",
+ "@aws-sdk/credential-provider-login": "3.972.0",
+ "@aws-sdk/credential-provider-process": "3.972.0",
+ "@aws-sdk/credential-provider-sso": "3.972.0",
+ "@aws-sdk/credential-provider-web-identity": "3.972.0",
+ "@aws-sdk/nested-clients": "3.972.0",
+ "@aws-sdk/types": "3.972.0",
+ "@smithy/credential-provider-imds": "^4.2.8",
+ "@smithy/property-provider": "^4.2.8",
+ "@smithy/shared-ini-file-loader": "^4.4.3",
+ "@smithy/types": "^4.12.0",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=20.0.0"
+ }
+ },
+ "node_modules/@aws-sdk/credential-provider-login": {
+ "version": "3.972.0",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-login/-/credential-provider-login-3.972.0.tgz",
+ "integrity": "sha512-SsrsFJsEYAJHO4N/r2P0aK6o8si6f1lprR+Ej8J731XJqTckSGs/HFHcbxOyW/iKt+LNUvZa59/VlJmjhF4bEQ==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@aws-sdk/core": "3.972.0",
+ "@aws-sdk/nested-clients": "3.972.0",
+ "@aws-sdk/types": "3.972.0",
+ "@smithy/property-provider": "^4.2.8",
+ "@smithy/protocol-http": "^5.3.8",
+ "@smithy/shared-ini-file-loader": "^4.4.3",
+ "@smithy/types": "^4.12.0",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=20.0.0"
+ }
+ },
+ "node_modules/@aws-sdk/credential-provider-node": {
+ "version": "3.972.0",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.972.0.tgz",
+ "integrity": "sha512-wwJDpEGl6+sOygic8QKu0OHVB8SiodqF1fr5jvUlSFfS6tJss/E9vBc2aFjl7zI6KpAIYfIzIgM006lRrZtWCQ==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@aws-sdk/credential-provider-env": "3.972.0",
+ "@aws-sdk/credential-provider-http": "3.972.0",
+ "@aws-sdk/credential-provider-ini": "3.972.0",
+ "@aws-sdk/credential-provider-process": "3.972.0",
+ "@aws-sdk/credential-provider-sso": "3.972.0",
+ "@aws-sdk/credential-provider-web-identity": "3.972.0",
+ "@aws-sdk/types": "3.972.0",
+ "@smithy/credential-provider-imds": "^4.2.8",
+ "@smithy/property-provider": "^4.2.8",
+ "@smithy/shared-ini-file-loader": "^4.4.3",
+ "@smithy/types": "^4.12.0",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=20.0.0"
+ }
+ },
+ "node_modules/@aws-sdk/credential-provider-process": {
+ "version": "3.972.0",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-process/-/credential-provider-process-3.972.0.tgz",
+ "integrity": "sha512-nmzYhamLDJ8K+v3zWck79IaKMc350xZnWsf/GeaXO6E3MewSzd3lYkTiMi7lEp3/UwDm9NHfPguoPm+mhlSWQQ==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@aws-sdk/core": "3.972.0",
+ "@aws-sdk/types": "3.972.0",
+ "@smithy/property-provider": "^4.2.8",
+ "@smithy/shared-ini-file-loader": "^4.4.3",
+ "@smithy/types": "^4.12.0",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=20.0.0"
+ }
+ },
+ "node_modules/@aws-sdk/credential-provider-sso": {
+ "version": "3.972.0",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.972.0.tgz",
+ "integrity": "sha512-6mYyfk1SrMZ15cH9T53yAF4YSnvq4yU1Xlgm3nqV1gZVQzmF5kr4t/F3BU3ygbvzi4uSwWxG3I3TYYS5eMlAyg==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@aws-sdk/client-sso": "3.972.0",
+ "@aws-sdk/core": "3.972.0",
+ "@aws-sdk/token-providers": "3.972.0",
+ "@aws-sdk/types": "3.972.0",
+ "@smithy/property-provider": "^4.2.8",
+ "@smithy/shared-ini-file-loader": "^4.4.3",
+ "@smithy/types": "^4.12.0",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=20.0.0"
+ }
+ },
+ "node_modules/@aws-sdk/credential-provider-web-identity": {
+ "version": "3.972.0",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.972.0.tgz",
+ "integrity": "sha512-vsJXBGL8H54kz4T6do3p5elATj5d1izVGUXMluRJntm9/I0be/zUYtdd4oDTM2kSUmd4Zhyw3fMQ9lw7CVhd4A==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@aws-sdk/core": "3.972.0",
+ "@aws-sdk/nested-clients": "3.972.0",
+ "@aws-sdk/types": "3.972.0",
+ "@smithy/property-provider": "^4.2.8",
+ "@smithy/shared-ini-file-loader": "^4.4.3",
+ "@smithy/types": "^4.12.0",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=20.0.0"
+ }
+ },
+ "node_modules/@aws-sdk/middleware-bucket-endpoint": {
+ "version": "3.972.0",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-bucket-endpoint/-/middleware-bucket-endpoint-3.972.0.tgz",
+ "integrity": "sha512-IrIjAehc3PrseAGfk2ldtAf+N0BAnNHR1DCZIDh9IAcFrTVWC3Fi9KJdtabrxcY3Onpt/8opOco4EIEAWgMz7A==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@aws-sdk/types": "3.972.0",
+ "@aws-sdk/util-arn-parser": "3.972.0",
+ "@smithy/node-config-provider": "^4.3.8",
+ "@smithy/protocol-http": "^5.3.8",
+ "@smithy/types": "^4.12.0",
+ "@smithy/util-config-provider": "^4.2.0",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=20.0.0"
+ }
+ },
+ "node_modules/@aws-sdk/middleware-expect-continue": {
+ "version": "3.972.0",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-expect-continue/-/middleware-expect-continue-3.972.0.tgz",
+ "integrity": "sha512-xyhDoY0qse8MvQC4RZCpT5WoIQ4/kwqv71Dh1s3mdXjL789Z4a6L/khBTSXECR5+egSZ960AInj3aR+CrezDRQ==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@aws-sdk/types": "3.972.0",
+ "@smithy/protocol-http": "^5.3.8",
+ "@smithy/types": "^4.12.0",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=20.0.0"
+ }
+ },
+ "node_modules/@aws-sdk/middleware-flexible-checksums": {
+ "version": "3.972.0",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-flexible-checksums/-/middleware-flexible-checksums-3.972.0.tgz",
+ "integrity": "sha512-zxK0ezmT7fLEPJ650S8QBc4rGDq5+5rdsLnnuZ6hPaZE4/+QtUoTw+gSDETyiWodNcRuz2ZWnqi17K+7nKtSRg==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@aws-crypto/crc32": "5.2.0",
+ "@aws-crypto/crc32c": "5.2.0",
+ "@aws-crypto/util": "5.2.0",
+ "@aws-sdk/core": "3.972.0",
+ "@aws-sdk/crc64-nvme": "3.972.0",
+ "@aws-sdk/types": "3.972.0",
+ "@smithy/is-array-buffer": "^4.2.0",
+ "@smithy/node-config-provider": "^4.3.8",
+ "@smithy/protocol-http": "^5.3.8",
+ "@smithy/types": "^4.12.0",
+ "@smithy/util-middleware": "^4.2.8",
+ "@smithy/util-stream": "^4.5.10",
+ "@smithy/util-utf8": "^4.2.0",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=20.0.0"
+ }
+ },
+ "node_modules/@aws-sdk/middleware-host-header": {
+ "version": "3.972.0",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-host-header/-/middleware-host-header-3.972.0.tgz",
+ "integrity": "sha512-3eztFI6F9/eHtkIaWKN3nT+PM+eQ6p1MALDuNshFk323ixuCZzOOVT8oUqtZa30Z6dycNXJwhlIq7NhUVFfimw==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@aws-sdk/types": "3.972.0",
+ "@smithy/protocol-http": "^5.3.8",
+ "@smithy/types": "^4.12.0",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=20.0.0"
+ }
+ },
+ "node_modules/@aws-sdk/middleware-location-constraint": {
+ "version": "3.972.0",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-location-constraint/-/middleware-location-constraint-3.972.0.tgz",
+ "integrity": "sha512-WpsxoVPzbGPQGb/jupNYjpE0REcCPtjz7Q7zAt+dyo7fxsLBn4J+Rp6AYzSa04J9VrmrvCqCbVLu6B88PlSKSQ==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@aws-sdk/types": "3.972.0",
+ "@smithy/types": "^4.12.0",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=20.0.0"
+ }
+ },
+ "node_modules/@aws-sdk/middleware-logger": {
+ "version": "3.972.0",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-logger/-/middleware-logger-3.972.0.tgz",
+ "integrity": "sha512-ZvdyVRwzK+ra31v1pQrgbqR/KsLD+wwJjHgko6JfoKUBIcEfAwJzQKO6HspHxdHWTVUz6MgvwskheR/TTYZl2g==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@aws-sdk/types": "3.972.0",
+ "@smithy/types": "^4.12.0",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=20.0.0"
+ }
+ },
+ "node_modules/@aws-sdk/middleware-recursion-detection": {
+ "version": "3.972.0",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-recursion-detection/-/middleware-recursion-detection-3.972.0.tgz",
+ "integrity": "sha512-F2SmUeO+S6l1h6dydNet3BQIk173uAkcfU1HDkw/bUdRLAnh15D3HP9vCZ7oCPBNcdEICbXYDmx0BR9rRUHGlQ==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@aws-sdk/types": "3.972.0",
+ "@aws/lambda-invoke-store": "^0.2.2",
+ "@smithy/protocol-http": "^5.3.8",
+ "@smithy/types": "^4.12.0",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=20.0.0"
+ }
+ },
+ "node_modules/@aws-sdk/middleware-sdk-s3": {
+ "version": "3.972.0",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-sdk-s3/-/middleware-sdk-s3-3.972.0.tgz",
+ "integrity": "sha512-0bcKFXWx+NZ7tIlOo7KjQ+O2rydiHdIQahrq+fN6k9Osky29v17guy68urUKfhTobR6iY6KvxkroFWaFtTgS5w==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@aws-sdk/core": "3.972.0",
+ "@aws-sdk/types": "3.972.0",
+ "@aws-sdk/util-arn-parser": "3.972.0",
+ "@smithy/core": "^3.20.6",
+ "@smithy/node-config-provider": "^4.3.8",
+ "@smithy/protocol-http": "^5.3.8",
+ "@smithy/signature-v4": "^5.3.8",
+ "@smithy/smithy-client": "^4.10.8",
+ "@smithy/types": "^4.12.0",
+ "@smithy/util-config-provider": "^4.2.0",
+ "@smithy/util-middleware": "^4.2.8",
+ "@smithy/util-stream": "^4.5.10",
+ "@smithy/util-utf8": "^4.2.0",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=20.0.0"
+ }
+ },
+ "node_modules/@aws-sdk/middleware-ssec": {
+ "version": "3.972.0",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-ssec/-/middleware-ssec-3.972.0.tgz",
+ "integrity": "sha512-cEr2HtK4R2fi8Y0P95cjbr4KJOjKBt8ms95mEJhabJN8KM4CpD4iS/J1lhvMj+qWir0KBTV6gKmxECXdfL9S6w==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@aws-sdk/types": "3.972.0",
+ "@smithy/types": "^4.12.0",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=20.0.0"
+ }
+ },
+ "node_modules/@aws-sdk/middleware-user-agent": {
+ "version": "3.972.0",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-user-agent/-/middleware-user-agent-3.972.0.tgz",
+ "integrity": "sha512-kFHQm2OCBJCzGWRafgdWHGFjitUXY/OxXngymcX4l8CiyiNDZB27HDDBg2yLj3OUJc4z4fexLMmP8r9vgag19g==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@aws-sdk/core": "3.972.0",
+ "@aws-sdk/types": "3.972.0",
+ "@aws-sdk/util-endpoints": "3.972.0",
+ "@smithy/core": "^3.20.6",
+ "@smithy/protocol-http": "^5.3.8",
+ "@smithy/types": "^4.12.0",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=20.0.0"
+ }
+ },
+ "node_modules/@aws-sdk/nested-clients": {
+ "version": "3.972.0",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/nested-clients/-/nested-clients-3.972.0.tgz",
+ "integrity": "sha512-QGlbnuGzSQJVG6bR9Qw6G0Blh6abFR4VxNa61ttMbzy9jt28xmk2iGtrYLrQPlCCPhY6enHqjTWm3n3LOb0wAw==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@aws-crypto/sha256-browser": "5.2.0",
+ "@aws-crypto/sha256-js": "5.2.0",
+ "@aws-sdk/core": "3.972.0",
+ "@aws-sdk/middleware-host-header": "3.972.0",
+ "@aws-sdk/middleware-logger": "3.972.0",
+ "@aws-sdk/middleware-recursion-detection": "3.972.0",
+ "@aws-sdk/middleware-user-agent": "3.972.0",
+ "@aws-sdk/region-config-resolver": "3.972.0",
+ "@aws-sdk/types": "3.972.0",
+ "@aws-sdk/util-endpoints": "3.972.0",
+ "@aws-sdk/util-user-agent-browser": "3.972.0",
+ "@aws-sdk/util-user-agent-node": "3.972.0",
+ "@smithy/config-resolver": "^4.4.6",
+ "@smithy/core": "^3.20.6",
+ "@smithy/fetch-http-handler": "^5.3.9",
+ "@smithy/hash-node": "^4.2.8",
+ "@smithy/invalid-dependency": "^4.2.8",
+ "@smithy/middleware-content-length": "^4.2.8",
+ "@smithy/middleware-endpoint": "^4.4.7",
+ "@smithy/middleware-retry": "^4.4.23",
+ "@smithy/middleware-serde": "^4.2.9",
+ "@smithy/middleware-stack": "^4.2.8",
+ "@smithy/node-config-provider": "^4.3.8",
+ "@smithy/node-http-handler": "^4.4.8",
+ "@smithy/protocol-http": "^5.3.8",
+ "@smithy/smithy-client": "^4.10.8",
+ "@smithy/types": "^4.12.0",
+ "@smithy/url-parser": "^4.2.8",
+ "@smithy/util-base64": "^4.3.0",
+ "@smithy/util-body-length-browser": "^4.2.0",
+ "@smithy/util-body-length-node": "^4.2.1",
+ "@smithy/util-defaults-mode-browser": "^4.3.22",
+ "@smithy/util-defaults-mode-node": "^4.2.25",
+ "@smithy/util-endpoints": "^3.2.8",
+ "@smithy/util-middleware": "^4.2.8",
+ "@smithy/util-retry": "^4.2.8",
+ "@smithy/util-utf8": "^4.2.0",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=20.0.0"
+ }
+ },
+ "node_modules/@aws-sdk/region-config-resolver": {
+ "version": "3.972.0",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/region-config-resolver/-/region-config-resolver-3.972.0.tgz",
+ "integrity": "sha512-JyOf+R/6vJW8OEVFCAyzEOn2reri/Q+L0z9zx4JQSKWvTmJ1qeFO25sOm8VIfB8URKhfGRTQF30pfYaH2zxt/A==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@aws-sdk/types": "3.972.0",
+ "@smithy/config-resolver": "^4.4.6",
+ "@smithy/node-config-provider": "^4.3.8",
+ "@smithy/types": "^4.12.0",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=20.0.0"
+ }
+ },
+ "node_modules/@aws-sdk/s3-request-presigner": {
+ "version": "3.972.0",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/s3-request-presigner/-/s3-request-presigner-3.972.0.tgz",
+ "integrity": "sha512-AsmnNkW+RF+UQ86bYduMN4e6DuEAsy9nragtBAdGnlVYILTB7C2AjMVzp2EM1WOzjZ4dDsOUS/t099rzi+GcfQ==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@aws-sdk/signature-v4-multi-region": "3.972.0",
+ "@aws-sdk/types": "3.972.0",
+ "@aws-sdk/util-format-url": "3.972.0",
+ "@smithy/middleware-endpoint": "^4.4.7",
+ "@smithy/protocol-http": "^5.3.8",
+ "@smithy/smithy-client": "^4.10.8",
+ "@smithy/types": "^4.12.0",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=20.0.0"
+ }
+ },
+ "node_modules/@aws-sdk/signature-v4-multi-region": {
+ "version": "3.972.0",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/signature-v4-multi-region/-/signature-v4-multi-region-3.972.0.tgz",
+ "integrity": "sha512-2udiRijmjpN81Pvajje4TsjbXDZNP6K9bYUanBYH8hXa/tZG5qfGCySD+TyX0sgDxCQmEDMg3LaQdfjNHBDEgQ==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@aws-sdk/middleware-sdk-s3": "3.972.0",
+ "@aws-sdk/types": "3.972.0",
+ "@smithy/protocol-http": "^5.3.8",
+ "@smithy/signature-v4": "^5.3.8",
+ "@smithy/types": "^4.12.0",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=20.0.0"
+ }
+ },
+ "node_modules/@aws-sdk/token-providers": {
+ "version": "3.972.0",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.972.0.tgz",
+ "integrity": "sha512-kWlXG+y5nZhgXGEtb72Je+EvqepBPs8E3vZse//1PYLWs2speFqbGE/ywCXmzEJgHgVqSB/u/lqBvs5WlYmSqQ==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@aws-sdk/core": "3.972.0",
+ "@aws-sdk/nested-clients": "3.972.0",
+ "@aws-sdk/types": "3.972.0",
+ "@smithy/property-provider": "^4.2.8",
+ "@smithy/shared-ini-file-loader": "^4.4.3",
+ "@smithy/types": "^4.12.0",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=20.0.0"
+ }
+ },
+ "node_modules/@aws-sdk/types": {
+ "version": "3.972.0",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.972.0.tgz",
+ "integrity": "sha512-U7xBIbLSetONxb2bNzHyDgND3oKGoIfmknrEVnoEU4GUSs+0augUOIn9DIWGUO2ETcRFdsRUnmx9KhPT9Ojbug==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@smithy/types": "^4.12.0",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=20.0.0"
+ }
+ },
+ "node_modules/@aws-sdk/util-arn-parser": {
+ "version": "3.972.0",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/util-arn-parser/-/util-arn-parser-3.972.0.tgz",
+ "integrity": "sha512-RM5Mmo/KJ593iMSrALlHEOcc9YOIyOsDmS5x2NLOMdEmzv1o00fcpAkCQ02IGu1eFneBFT7uX0Mpag0HI+Cz2g==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=20.0.0"
+ }
+ },
+ "node_modules/@aws-sdk/util-endpoints": {
+ "version": "3.972.0",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/util-endpoints/-/util-endpoints-3.972.0.tgz",
+ "integrity": "sha512-6JHsl1V/a1ZW8D8AFfd4R52fwZPnZ5H4U6DS8m/bWT8qad72NvbOFAC7U2cDtFs2TShqUO3TEiX/EJibtY3ijg==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@aws-sdk/types": "3.972.0",
+ "@smithy/types": "^4.12.0",
+ "@smithy/url-parser": "^4.2.8",
+ "@smithy/util-endpoints": "^3.2.8",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=20.0.0"
+ }
+ },
+ "node_modules/@aws-sdk/util-format-url": {
+ "version": "3.972.0",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/util-format-url/-/util-format-url-3.972.0.tgz",
+ "integrity": "sha512-o4zqsW/PxrcsTla/Yh2dkRS26kP76QQWZq/i/JgVNFBAr9x0E2oJcCeh8Daj2AA+8AZ8VWln9x706FFzWWQwvQ==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@aws-sdk/types": "3.972.0",
+ "@smithy/querystring-builder": "^4.2.8",
+ "@smithy/types": "^4.12.0",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=20.0.0"
+ }
+ },
+ "node_modules/@aws-sdk/util-locate-window": {
+ "version": "3.965.3",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/util-locate-window/-/util-locate-window-3.965.3.tgz",
+ "integrity": "sha512-FNUqAjlKAGA7GM05kywE99q8wiPHPZqrzhq3wXRga6PRD6A0kzT85Pb0AzYBVTBRpSrKyyr6M92Y6bnSBVp2BA==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=20.0.0"
+ }
+ },
+ "node_modules/@aws-sdk/util-user-agent-browser": {
+ "version": "3.972.0",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-browser/-/util-user-agent-browser-3.972.0.tgz",
+ "integrity": "sha512-eOLdkQyoRbDgioTS3Orr7iVsVEutJyMZxvyZ6WAF95IrF0kfWx5Rd/KXnfbnG/VKa2CvjZiitWfouLzfVEyvJA==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@aws-sdk/types": "3.972.0",
+ "@smithy/types": "^4.12.0",
+ "bowser": "^2.11.0",
+ "tslib": "^2.6.2"
+ }
+ },
+ "node_modules/@aws-sdk/util-user-agent-node": {
+ "version": "3.972.0",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-node/-/util-user-agent-node-3.972.0.tgz",
+ "integrity": "sha512-GOy+AiSrE9kGiojiwlZvVVSXwylu4+fmP0MJfvras/MwP09RB/YtQuOVR1E0fKQc6OMwaTNBjgAbOEhxuWFbAw==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@aws-sdk/middleware-user-agent": "3.972.0",
+ "@aws-sdk/types": "3.972.0",
+ "@smithy/node-config-provider": "^4.3.8",
+ "@smithy/types": "^4.12.0",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=20.0.0"
+ },
+ "peerDependencies": {
+ "aws-crt": ">=1.0.0"
+ },
+ "peerDependenciesMeta": {
+ "aws-crt": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@aws-sdk/xml-builder": {
+ "version": "3.972.0",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/xml-builder/-/xml-builder-3.972.0.tgz",
+ "integrity": "sha512-POaGMcXnozzqBUyJM3HLUZ9GR6OKJWPGJEmhtTnxZXt8B6JcJ/6K3xRJ5H/j8oovVLz8Wg6vFxAHv8lvuASxMg==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@smithy/types": "^4.12.0",
+ "fast-xml-parser": "5.2.5",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=20.0.0"
+ }
+ },
+ "node_modules/@aws/lambda-invoke-store": {
+ "version": "0.2.3",
+ "resolved": "https://registry.npmjs.org/@aws/lambda-invoke-store/-/lambda-invoke-store-0.2.3.tgz",
+ "integrity": "sha512-oLvsaPMTBejkkmHhjf09xTgk71mOqyr/409NKhRIL08If7AhVfUsJhVsx386uJaqNd42v9kWamQ9lFbkoC2dYw==",
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
"node_modules/@babel/code-frame": {
"version": "7.27.1",
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz",
@@ -2338,6 +3272,738 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/@smithy/abort-controller": {
+ "version": "4.2.8",
+ "resolved": "https://registry.npmjs.org/@smithy/abort-controller/-/abort-controller-4.2.8.tgz",
+ "integrity": "sha512-peuVfkYHAmS5ybKxWcfraK7WBBP0J+rkfUcbHJJKQ4ir3UAUNQI+Y4Vt/PqSzGqgloJ5O1dk7+WzNL8wcCSXbw==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@smithy/types": "^4.12.0",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "node_modules/@smithy/chunked-blob-reader": {
+ "version": "5.2.0",
+ "resolved": "https://registry.npmjs.org/@smithy/chunked-blob-reader/-/chunked-blob-reader-5.2.0.tgz",
+ "integrity": "sha512-WmU0TnhEAJLWvfSeMxBNe5xtbselEO8+4wG0NtZeL8oR21WgH1xiO37El+/Y+H/Ie4SCwBy3MxYWmOYaGgZueA==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "node_modules/@smithy/chunked-blob-reader-native": {
+ "version": "4.2.1",
+ "resolved": "https://registry.npmjs.org/@smithy/chunked-blob-reader-native/-/chunked-blob-reader-native-4.2.1.tgz",
+ "integrity": "sha512-lX9Ay+6LisTfpLid2zZtIhSEjHMZoAR5hHCR4H7tBz/Zkfr5ea8RcQ7Tk4mi0P76p4cN+Btz16Ffno7YHpKXnQ==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@smithy/util-base64": "^4.3.0",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "node_modules/@smithy/config-resolver": {
+ "version": "4.4.6",
+ "resolved": "https://registry.npmjs.org/@smithy/config-resolver/-/config-resolver-4.4.6.tgz",
+ "integrity": "sha512-qJpzYC64kaj3S0fueiu3kXm8xPrR3PcXDPEgnaNMRn0EjNSZFoFjvbUp0YUDsRhN1CB90EnHJtbxWKevnH99UQ==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@smithy/node-config-provider": "^4.3.8",
+ "@smithy/types": "^4.12.0",
+ "@smithy/util-config-provider": "^4.2.0",
+ "@smithy/util-endpoints": "^3.2.8",
+ "@smithy/util-middleware": "^4.2.8",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "node_modules/@smithy/core": {
+ "version": "3.21.0",
+ "resolved": "https://registry.npmjs.org/@smithy/core/-/core-3.21.0.tgz",
+ "integrity": "sha512-bg2TfzgsERyETAxc/Ims/eJX8eAnIeTi4r4LHpMpfF/2NyO6RsWis0rjKcCPaGksljmOb23BZRiCeT/3NvwkXw==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@smithy/middleware-serde": "^4.2.9",
+ "@smithy/protocol-http": "^5.3.8",
+ "@smithy/types": "^4.12.0",
+ "@smithy/util-base64": "^4.3.0",
+ "@smithy/util-body-length-browser": "^4.2.0",
+ "@smithy/util-middleware": "^4.2.8",
+ "@smithy/util-stream": "^4.5.10",
+ "@smithy/util-utf8": "^4.2.0",
+ "@smithy/uuid": "^1.1.0",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "node_modules/@smithy/credential-provider-imds": {
+ "version": "4.2.8",
+ "resolved": "https://registry.npmjs.org/@smithy/credential-provider-imds/-/credential-provider-imds-4.2.8.tgz",
+ "integrity": "sha512-FNT0xHS1c/CPN8upqbMFP83+ul5YgdisfCfkZ86Jh2NSmnqw/AJ6x5pEogVCTVvSm7j9MopRU89bmDelxuDMYw==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@smithy/node-config-provider": "^4.3.8",
+ "@smithy/property-provider": "^4.2.8",
+ "@smithy/types": "^4.12.0",
+ "@smithy/url-parser": "^4.2.8",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "node_modules/@smithy/eventstream-codec": {
+ "version": "4.2.8",
+ "resolved": "https://registry.npmjs.org/@smithy/eventstream-codec/-/eventstream-codec-4.2.8.tgz",
+ "integrity": "sha512-jS/O5Q14UsufqoGhov7dHLOPCzkYJl9QDzusI2Psh4wyYx/izhzvX9P4D69aTxcdfVhEPhjK+wYyn/PzLjKbbw==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@aws-crypto/crc32": "5.2.0",
+ "@smithy/types": "^4.12.0",
+ "@smithy/util-hex-encoding": "^4.2.0",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "node_modules/@smithy/eventstream-serde-browser": {
+ "version": "4.2.8",
+ "resolved": "https://registry.npmjs.org/@smithy/eventstream-serde-browser/-/eventstream-serde-browser-4.2.8.tgz",
+ "integrity": "sha512-MTfQT/CRQz5g24ayXdjg53V0mhucZth4PESoA5IhvaWVDTOQLfo8qI9vzqHcPsdd2v6sqfTYqF5L/l+pea5Uyw==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@smithy/eventstream-serde-universal": "^4.2.8",
+ "@smithy/types": "^4.12.0",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "node_modules/@smithy/eventstream-serde-config-resolver": {
+ "version": "4.3.8",
+ "resolved": "https://registry.npmjs.org/@smithy/eventstream-serde-config-resolver/-/eventstream-serde-config-resolver-4.3.8.tgz",
+ "integrity": "sha512-ah12+luBiDGzBruhu3efNy1IlbwSEdNiw8fOZksoKoWW1ZHvO/04MQsdnws/9Aj+5b0YXSSN2JXKy/ClIsW8MQ==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@smithy/types": "^4.12.0",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "node_modules/@smithy/eventstream-serde-node": {
+ "version": "4.2.8",
+ "resolved": "https://registry.npmjs.org/@smithy/eventstream-serde-node/-/eventstream-serde-node-4.2.8.tgz",
+ "integrity": "sha512-cYpCpp29z6EJHa5T9WL0KAlq3SOKUQkcgSoeRfRVwjGgSFl7Uh32eYGt7IDYCX20skiEdRffyDpvF2efEZPC0A==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@smithy/eventstream-serde-universal": "^4.2.8",
+ "@smithy/types": "^4.12.0",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "node_modules/@smithy/eventstream-serde-universal": {
+ "version": "4.2.8",
+ "resolved": "https://registry.npmjs.org/@smithy/eventstream-serde-universal/-/eventstream-serde-universal-4.2.8.tgz",
+ "integrity": "sha512-iJ6YNJd0bntJYnX6s52NC4WFYcZeKrPUr1Kmmr5AwZcwCSzVpS7oavAmxMR7pMq7V+D1G4s9F5NJK0xwOsKAlQ==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@smithy/eventstream-codec": "^4.2.8",
+ "@smithy/types": "^4.12.0",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "node_modules/@smithy/fetch-http-handler": {
+ "version": "5.3.9",
+ "resolved": "https://registry.npmjs.org/@smithy/fetch-http-handler/-/fetch-http-handler-5.3.9.tgz",
+ "integrity": "sha512-I4UhmcTYXBrct03rwzQX1Y/iqQlzVQaPxWjCjula++5EmWq9YGBrx6bbGqluGc1f0XEfhSkiY4jhLgbsJUMKRA==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@smithy/protocol-http": "^5.3.8",
+ "@smithy/querystring-builder": "^4.2.8",
+ "@smithy/types": "^4.12.0",
+ "@smithy/util-base64": "^4.3.0",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "node_modules/@smithy/hash-blob-browser": {
+ "version": "4.2.9",
+ "resolved": "https://registry.npmjs.org/@smithy/hash-blob-browser/-/hash-blob-browser-4.2.9.tgz",
+ "integrity": "sha512-m80d/iicI7DlBDxyQP6Th7BW/ejDGiF0bgI754+tiwK0lgMkcaIBgvwwVc7OFbY4eUzpGtnig52MhPAEJ7iNYg==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@smithy/chunked-blob-reader": "^5.2.0",
+ "@smithy/chunked-blob-reader-native": "^4.2.1",
+ "@smithy/types": "^4.12.0",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "node_modules/@smithy/hash-node": {
+ "version": "4.2.8",
+ "resolved": "https://registry.npmjs.org/@smithy/hash-node/-/hash-node-4.2.8.tgz",
+ "integrity": "sha512-7ZIlPbmaDGxVoxErDZnuFG18WekhbA/g2/i97wGj+wUBeS6pcUeAym8u4BXh/75RXWhgIJhyC11hBzig6MljwA==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@smithy/types": "^4.12.0",
+ "@smithy/util-buffer-from": "^4.2.0",
+ "@smithy/util-utf8": "^4.2.0",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "node_modules/@smithy/hash-stream-node": {
+ "version": "4.2.8",
+ "resolved": "https://registry.npmjs.org/@smithy/hash-stream-node/-/hash-stream-node-4.2.8.tgz",
+ "integrity": "sha512-v0FLTXgHrTeheYZFGhR+ehX5qUm4IQsjAiL9qehad2cyjMWcN2QG6/4mSwbSgEQzI7jwfoXj7z4fxZUx/Mhj2w==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@smithy/types": "^4.12.0",
+ "@smithy/util-utf8": "^4.2.0",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "node_modules/@smithy/invalid-dependency": {
+ "version": "4.2.8",
+ "resolved": "https://registry.npmjs.org/@smithy/invalid-dependency/-/invalid-dependency-4.2.8.tgz",
+ "integrity": "sha512-N9iozRybwAQ2dn9Fot9kI6/w9vos2oTXLhtK7ovGqwZjlOcxu6XhPlpLpC+INsxktqHinn5gS2DXDjDF2kG5sQ==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@smithy/types": "^4.12.0",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "node_modules/@smithy/is-array-buffer": {
+ "version": "4.2.0",
+ "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-4.2.0.tgz",
+ "integrity": "sha512-DZZZBvC7sjcYh4MazJSGiWMI2L7E0oCiRHREDzIxi/M2LY79/21iXt6aPLHge82wi5LsuRF5A06Ds3+0mlh6CQ==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "node_modules/@smithy/md5-js": {
+ "version": "4.2.8",
+ "resolved": "https://registry.npmjs.org/@smithy/md5-js/-/md5-js-4.2.8.tgz",
+ "integrity": "sha512-oGMaLj4tVZzLi3itBa9TCswgMBr7k9b+qKYowQ6x1rTyTuO1IU2YHdHUa+891OsOH+wCsH7aTPRsTJO3RMQmjQ==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@smithy/types": "^4.12.0",
+ "@smithy/util-utf8": "^4.2.0",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "node_modules/@smithy/middleware-content-length": {
+ "version": "4.2.8",
+ "resolved": "https://registry.npmjs.org/@smithy/middleware-content-length/-/middleware-content-length-4.2.8.tgz",
+ "integrity": "sha512-RO0jeoaYAB1qBRhfVyq0pMgBoUK34YEJxVxyjOWYZiOKOq2yMZ4MnVXMZCUDenpozHue207+9P5ilTV1zeda0A==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@smithy/protocol-http": "^5.3.8",
+ "@smithy/types": "^4.12.0",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "node_modules/@smithy/middleware-endpoint": {
+ "version": "4.4.10",
+ "resolved": "https://registry.npmjs.org/@smithy/middleware-endpoint/-/middleware-endpoint-4.4.10.tgz",
+ "integrity": "sha512-kwWpNltpxrvPabnjEFvwSmA+66l6s2ReCvgVSzW/z92LU4T28fTdgZ18IdYRYOrisu2NMQ0jUndRScbO65A/zg==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@smithy/core": "^3.21.0",
+ "@smithy/middleware-serde": "^4.2.9",
+ "@smithy/node-config-provider": "^4.3.8",
+ "@smithy/shared-ini-file-loader": "^4.4.3",
+ "@smithy/types": "^4.12.0",
+ "@smithy/url-parser": "^4.2.8",
+ "@smithy/util-middleware": "^4.2.8",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "node_modules/@smithy/middleware-retry": {
+ "version": "4.4.26",
+ "resolved": "https://registry.npmjs.org/@smithy/middleware-retry/-/middleware-retry-4.4.26.tgz",
+ "integrity": "sha512-ozZMoTAr+B2aVYfLYfkssFvc8ZV3p/vLpVQ7/k277xxUOA9ykSPe5obL2j6yHfbdrM/SZV7qj0uk/hSqavHrLw==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@smithy/node-config-provider": "^4.3.8",
+ "@smithy/protocol-http": "^5.3.8",
+ "@smithy/service-error-classification": "^4.2.8",
+ "@smithy/smithy-client": "^4.10.11",
+ "@smithy/types": "^4.12.0",
+ "@smithy/util-middleware": "^4.2.8",
+ "@smithy/util-retry": "^4.2.8",
+ "@smithy/uuid": "^1.1.0",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "node_modules/@smithy/middleware-serde": {
+ "version": "4.2.9",
+ "resolved": "https://registry.npmjs.org/@smithy/middleware-serde/-/middleware-serde-4.2.9.tgz",
+ "integrity": "sha512-eMNiej0u/snzDvlqRGSN3Vl0ESn3838+nKyVfF2FKNXFbi4SERYT6PR392D39iczngbqqGG0Jl1DlCnp7tBbXQ==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@smithy/protocol-http": "^5.3.8",
+ "@smithy/types": "^4.12.0",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "node_modules/@smithy/middleware-stack": {
+ "version": "4.2.8",
+ "resolved": "https://registry.npmjs.org/@smithy/middleware-stack/-/middleware-stack-4.2.8.tgz",
+ "integrity": "sha512-w6LCfOviTYQjBctOKSwy6A8FIkQy7ICvglrZFl6Bw4FmcQ1Z420fUtIhxaUZZshRe0VCq4kvDiPiXrPZAe8oRA==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@smithy/types": "^4.12.0",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "node_modules/@smithy/node-config-provider": {
+ "version": "4.3.8",
+ "resolved": "https://registry.npmjs.org/@smithy/node-config-provider/-/node-config-provider-4.3.8.tgz",
+ "integrity": "sha512-aFP1ai4lrbVlWjfpAfRSL8KFcnJQYfTl5QxLJXY32vghJrDuFyPZ6LtUL+JEGYiFRG1PfPLHLoxj107ulncLIg==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@smithy/property-provider": "^4.2.8",
+ "@smithy/shared-ini-file-loader": "^4.4.3",
+ "@smithy/types": "^4.12.0",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "node_modules/@smithy/node-http-handler": {
+ "version": "4.4.8",
+ "resolved": "https://registry.npmjs.org/@smithy/node-http-handler/-/node-http-handler-4.4.8.tgz",
+ "integrity": "sha512-q9u+MSbJVIJ1QmJ4+1u+cERXkrhuILCBDsJUBAW1MPE6sFonbCNaegFuwW9ll8kh5UdyY3jOkoOGlc7BesoLpg==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@smithy/abort-controller": "^4.2.8",
+ "@smithy/protocol-http": "^5.3.8",
+ "@smithy/querystring-builder": "^4.2.8",
+ "@smithy/types": "^4.12.0",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "node_modules/@smithy/property-provider": {
+ "version": "4.2.8",
+ "resolved": "https://registry.npmjs.org/@smithy/property-provider/-/property-provider-4.2.8.tgz",
+ "integrity": "sha512-EtCTbyIveCKeOXDSWSdze3k612yCPq1YbXsbqX3UHhkOSW8zKsM9NOJG5gTIya0vbY2DIaieG8pKo1rITHYL0w==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@smithy/types": "^4.12.0",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "node_modules/@smithy/protocol-http": {
+ "version": "5.3.8",
+ "resolved": "https://registry.npmjs.org/@smithy/protocol-http/-/protocol-http-5.3.8.tgz",
+ "integrity": "sha512-QNINVDhxpZ5QnP3aviNHQFlRogQZDfYlCkQT+7tJnErPQbDhysondEjhikuANxgMsZrkGeiAxXy4jguEGsDrWQ==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@smithy/types": "^4.12.0",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "node_modules/@smithy/querystring-builder": {
+ "version": "4.2.8",
+ "resolved": "https://registry.npmjs.org/@smithy/querystring-builder/-/querystring-builder-4.2.8.tgz",
+ "integrity": "sha512-Xr83r31+DrE8CP3MqPgMJl+pQlLLmOfiEUnoyAlGzzJIrEsbKsPy1hqH0qySaQm4oWrCBlUqRt+idEgunKB+iw==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@smithy/types": "^4.12.0",
+ "@smithy/util-uri-escape": "^4.2.0",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "node_modules/@smithy/querystring-parser": {
+ "version": "4.2.8",
+ "resolved": "https://registry.npmjs.org/@smithy/querystring-parser/-/querystring-parser-4.2.8.tgz",
+ "integrity": "sha512-vUurovluVy50CUlazOiXkPq40KGvGWSdmusa3130MwrR1UNnNgKAlj58wlOe61XSHRpUfIIh6cE0zZ8mzKaDPA==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@smithy/types": "^4.12.0",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "node_modules/@smithy/service-error-classification": {
+ "version": "4.2.8",
+ "resolved": "https://registry.npmjs.org/@smithy/service-error-classification/-/service-error-classification-4.2.8.tgz",
+ "integrity": "sha512-mZ5xddodpJhEt3RkCjbmUQuXUOaPNTkbMGR0bcS8FE0bJDLMZlhmpgrvPNCYglVw5rsYTpSnv19womw9WWXKQQ==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@smithy/types": "^4.12.0"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "node_modules/@smithy/shared-ini-file-loader": {
+ "version": "4.4.3",
+ "resolved": "https://registry.npmjs.org/@smithy/shared-ini-file-loader/-/shared-ini-file-loader-4.4.3.tgz",
+ "integrity": "sha512-DfQjxXQnzC5UbCUPeC3Ie8u+rIWZTvuDPAGU/BxzrOGhRvgUanaP68kDZA+jaT3ZI+djOf+4dERGlm9mWfFDrg==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@smithy/types": "^4.12.0",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "node_modules/@smithy/signature-v4": {
+ "version": "5.3.8",
+ "resolved": "https://registry.npmjs.org/@smithy/signature-v4/-/signature-v4-5.3.8.tgz",
+ "integrity": "sha512-6A4vdGj7qKNRF16UIcO8HhHjKW27thsxYci+5r/uVRkdcBEkOEiY8OMPuydLX4QHSrJqGHPJzPRwwVTqbLZJhg==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@smithy/is-array-buffer": "^4.2.0",
+ "@smithy/protocol-http": "^5.3.8",
+ "@smithy/types": "^4.12.0",
+ "@smithy/util-hex-encoding": "^4.2.0",
+ "@smithy/util-middleware": "^4.2.8",
+ "@smithy/util-uri-escape": "^4.2.0",
+ "@smithy/util-utf8": "^4.2.0",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "node_modules/@smithy/smithy-client": {
+ "version": "4.10.11",
+ "resolved": "https://registry.npmjs.org/@smithy/smithy-client/-/smithy-client-4.10.11.tgz",
+ "integrity": "sha512-6o804SCyHGMXAb5mFJ+iTy9kVKv7F91a9szN0J+9X6p8A0NrdpUxdaC57aye2ipQkP2C4IAqETEpGZ0Zj77Haw==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@smithy/core": "^3.21.0",
+ "@smithy/middleware-endpoint": "^4.4.10",
+ "@smithy/middleware-stack": "^4.2.8",
+ "@smithy/protocol-http": "^5.3.8",
+ "@smithy/types": "^4.12.0",
+ "@smithy/util-stream": "^4.5.10",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "node_modules/@smithy/types": {
+ "version": "4.12.0",
+ "resolved": "https://registry.npmjs.org/@smithy/types/-/types-4.12.0.tgz",
+ "integrity": "sha512-9YcuJVTOBDjg9LWo23Qp0lTQ3D7fQsQtwle0jVfpbUHy9qBwCEgKuVH4FqFB3VYu0nwdHKiEMA+oXz7oV8X1kw==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "node_modules/@smithy/url-parser": {
+ "version": "4.2.8",
+ "resolved": "https://registry.npmjs.org/@smithy/url-parser/-/url-parser-4.2.8.tgz",
+ "integrity": "sha512-NQho9U68TGMEU639YkXnVMV3GEFFULmmaWdlu1E9qzyIePOHsoSnagTGSDv1Zi8DCNN6btxOSdgmy5E/hsZwhA==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@smithy/querystring-parser": "^4.2.8",
+ "@smithy/types": "^4.12.0",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "node_modules/@smithy/util-base64": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/@smithy/util-base64/-/util-base64-4.3.0.tgz",
+ "integrity": "sha512-GkXZ59JfyxsIwNTWFnjmFEI8kZpRNIBfxKjv09+nkAWPt/4aGaEWMM04m4sxgNVWkbt2MdSvE3KF/PfX4nFedQ==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@smithy/util-buffer-from": "^4.2.0",
+ "@smithy/util-utf8": "^4.2.0",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "node_modules/@smithy/util-body-length-browser": {
+ "version": "4.2.0",
+ "resolved": "https://registry.npmjs.org/@smithy/util-body-length-browser/-/util-body-length-browser-4.2.0.tgz",
+ "integrity": "sha512-Fkoh/I76szMKJnBXWPdFkQJl2r9SjPt3cMzLdOB6eJ4Pnpas8hVoWPYemX/peO0yrrvldgCUVJqOAjUrOLjbxg==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "node_modules/@smithy/util-body-length-node": {
+ "version": "4.2.1",
+ "resolved": "https://registry.npmjs.org/@smithy/util-body-length-node/-/util-body-length-node-4.2.1.tgz",
+ "integrity": "sha512-h53dz/pISVrVrfxV1iqXlx5pRg3V2YWFcSQyPyXZRrZoZj4R4DeWRDo1a7dd3CPTcFi3kE+98tuNyD2axyZReA==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "node_modules/@smithy/util-buffer-from": {
+ "version": "4.2.0",
+ "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-4.2.0.tgz",
+ "integrity": "sha512-kAY9hTKulTNevM2nlRtxAG2FQ3B2OR6QIrPY3zE5LqJy1oxzmgBGsHLWTcNhWXKchgA0WHW+mZkQrng/pgcCew==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@smithy/is-array-buffer": "^4.2.0",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "node_modules/@smithy/util-config-provider": {
+ "version": "4.2.0",
+ "resolved": "https://registry.npmjs.org/@smithy/util-config-provider/-/util-config-provider-4.2.0.tgz",
+ "integrity": "sha512-YEjpl6XJ36FTKmD+kRJJWYvrHeUvm5ykaUS5xK+6oXffQPHeEM4/nXlZPe+Wu0lsgRUcNZiliYNh/y7q9c2y6Q==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "node_modules/@smithy/util-defaults-mode-browser": {
+ "version": "4.3.25",
+ "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-browser/-/util-defaults-mode-browser-4.3.25.tgz",
+ "integrity": "sha512-8ugoNMtss2dJHsXnqsibGPqoaafvWJPACmYKxJ4E6QWaDrixsAemmiMMAVbvwYadjR0H9G2+AlzsInSzRi8PSw==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@smithy/property-provider": "^4.2.8",
+ "@smithy/smithy-client": "^4.10.11",
+ "@smithy/types": "^4.12.0",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "node_modules/@smithy/util-defaults-mode-node": {
+ "version": "4.2.28",
+ "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-node/-/util-defaults-mode-node-4.2.28.tgz",
+ "integrity": "sha512-mjUdcP8h3E0K/XvNMi9oBXRV3DMCzeRiYIieZ1LQ7jq5tu6GH/GTWym7a1xIIE0pKSoLcpGsaImuQhGPSIJzAA==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@smithy/config-resolver": "^4.4.6",
+ "@smithy/credential-provider-imds": "^4.2.8",
+ "@smithy/node-config-provider": "^4.3.8",
+ "@smithy/property-provider": "^4.2.8",
+ "@smithy/smithy-client": "^4.10.11",
+ "@smithy/types": "^4.12.0",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "node_modules/@smithy/util-endpoints": {
+ "version": "3.2.8",
+ "resolved": "https://registry.npmjs.org/@smithy/util-endpoints/-/util-endpoints-3.2.8.tgz",
+ "integrity": "sha512-8JaVTn3pBDkhZgHQ8R0epwWt+BqPSLCjdjXXusK1onwJlRuN69fbvSK66aIKKO7SwVFM6x2J2ox5X8pOaWcUEw==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@smithy/node-config-provider": "^4.3.8",
+ "@smithy/types": "^4.12.0",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "node_modules/@smithy/util-hex-encoding": {
+ "version": "4.2.0",
+ "resolved": "https://registry.npmjs.org/@smithy/util-hex-encoding/-/util-hex-encoding-4.2.0.tgz",
+ "integrity": "sha512-CCQBwJIvXMLKxVbO88IukazJD9a4kQ9ZN7/UMGBjBcJYvatpWk+9g870El4cB8/EJxfe+k+y0GmR9CAzkF+Nbw==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "node_modules/@smithy/util-middleware": {
+ "version": "4.2.8",
+ "resolved": "https://registry.npmjs.org/@smithy/util-middleware/-/util-middleware-4.2.8.tgz",
+ "integrity": "sha512-PMqfeJxLcNPMDgvPbbLl/2Vpin+luxqTGPpW3NAQVLbRrFRzTa4rNAASYeIGjRV9Ytuhzny39SpyU04EQreF+A==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@smithy/types": "^4.12.0",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "node_modules/@smithy/util-retry": {
+ "version": "4.2.8",
+ "resolved": "https://registry.npmjs.org/@smithy/util-retry/-/util-retry-4.2.8.tgz",
+ "integrity": "sha512-CfJqwvoRY0kTGe5AkQokpURNCT1u/MkRzMTASWMPPo2hNSnKtF1D45dQl3DE2LKLr4m+PW9mCeBMJr5mCAVThg==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@smithy/service-error-classification": "^4.2.8",
+ "@smithy/types": "^4.12.0",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "node_modules/@smithy/util-stream": {
+ "version": "4.5.10",
+ "resolved": "https://registry.npmjs.org/@smithy/util-stream/-/util-stream-4.5.10.tgz",
+ "integrity": "sha512-jbqemy51UFSZSp2y0ZmRfckmrzuKww95zT9BYMmuJ8v3altGcqjwoV1tzpOwuHaKrwQrCjIzOib499ymr2f98g==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@smithy/fetch-http-handler": "^5.3.9",
+ "@smithy/node-http-handler": "^4.4.8",
+ "@smithy/types": "^4.12.0",
+ "@smithy/util-base64": "^4.3.0",
+ "@smithy/util-buffer-from": "^4.2.0",
+ "@smithy/util-hex-encoding": "^4.2.0",
+ "@smithy/util-utf8": "^4.2.0",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "node_modules/@smithy/util-uri-escape": {
+ "version": "4.2.0",
+ "resolved": "https://registry.npmjs.org/@smithy/util-uri-escape/-/util-uri-escape-4.2.0.tgz",
+ "integrity": "sha512-igZpCKV9+E/Mzrpq6YacdTQ0qTiLm85gD6N/IrmyDvQFA4UnU3d5g3m8tMT/6zG/vVkWSU+VxeUyGonL62DuxA==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "node_modules/@smithy/util-utf8": {
+ "version": "4.2.0",
+ "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-4.2.0.tgz",
+ "integrity": "sha512-zBPfuzoI8xyBtR2P6WQj63Rz8i3AmfAaJLuNG8dWsfvPe8lO4aCPYLn879mEgHndZH1zQ2oXmG8O1GGzzaoZiw==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@smithy/util-buffer-from": "^4.2.0",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "node_modules/@smithy/util-waiter": {
+ "version": "4.2.8",
+ "resolved": "https://registry.npmjs.org/@smithy/util-waiter/-/util-waiter-4.2.8.tgz",
+ "integrity": "sha512-n+lahlMWk+aejGuax7DPWtqav8HYnWxQwR+LCG2BgCUmaGcTe9qZCFsmw8TMg9iG75HOwhrJCX9TCJRLH+Yzqg==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@smithy/abort-controller": "^4.2.8",
+ "@smithy/types": "^4.12.0",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "node_modules/@smithy/uuid": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/@smithy/uuid/-/uuid-1.1.0.tgz",
+ "integrity": "sha512-4aUIteuyxtBUhVdiQqcDhKFitwfd9hqoSDYY2KRXiWtgoWJ9Bmise+KfEPDiVHWeJepvF8xJO9/9+WDIciMFFw==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
"node_modules/@stablelib/base64": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/@stablelib/base64/-/base64-1.0.1.tgz",
@@ -3640,6 +5306,12 @@
"integrity": "sha512-iD3898SR7sWVRHbiQv+sHUtHnMvC1o3nW5rAcqnq3uOn07DSAppZYUkIGslDz6gXC7HfunPe7YVBgoEJASPcHA==",
"license": "MIT"
},
+ "node_modules/bowser": {
+ "version": "2.13.1",
+ "resolved": "https://registry.npmjs.org/bowser/-/bowser-2.13.1.tgz",
+ "integrity": "sha512-OHawaAbjwx6rqICCKgSG0SAnT05bzd7ppyKLVUITZpANBaaMFBAsaNkto3LoQ31tyFP5kNujE8Cdx85G9VzOkw==",
+ "license": "MIT"
+ },
"node_modules/brace-expansion": {
"version": "1.1.12",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
@@ -5550,6 +7222,24 @@
"integrity": "sha512-n11RGP/lrWEFI/bWdygLxhI+pVeo1ZYIVwvvPkW7azl/rOy+F3HYRZ2K5zeE9mmkhQppyv9sQFx0JM9UabnpPQ==",
"license": "Unlicense"
},
+ "node_modules/fast-xml-parser": {
+ "version": "5.2.5",
+ "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-5.2.5.tgz",
+ "integrity": "sha512-pfX9uG9Ki0yekDHx2SiuRIyFdyAr1kMIMitPvb0YBo8SUfKvia7w7FIyd/l6av85pFYRhZscS75MwMnbvY+hcQ==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/NaturalIntelligence"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "strnum": "^2.1.0"
+ },
+ "bin": {
+ "fxparser": "src/cli/cli.js"
+ }
+ },
"node_modules/fastq": {
"version": "1.19.1",
"resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.1.tgz",
@@ -9743,6 +11433,18 @@
}
}
},
+ "node_modules/strnum": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/strnum/-/strnum-2.1.2.tgz",
+ "integrity": "sha512-l63NF9y/cLROq/yqKXSLtcMeeyOfnSQlfMSlzFt/K73oIaD8DGaQWd7Z34X9GPiKqP5rbSh84Hl4bOlLcjiSrQ==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/NaturalIntelligence"
+ }
+ ],
+ "license": "MIT"
+ },
"node_modules/styled-jsx": {
"version": "5.1.1",
"resolved": "https://registry.npmjs.org/styled-jsx/-/styled-jsx-5.1.1.tgz",
diff --git a/package.json b/package.json
index a2634eb..9d3a423 100644
--- a/package.json
+++ b/package.json
@@ -29,6 +29,8 @@
},
"dependencies": {
"@auth/prisma-adapter": "^2.11.1",
+ "@aws-sdk/client-s3": "^3.972.0",
+ "@aws-sdk/s3-request-presigner": "^3.972.0",
"@edge-runtime/cookies": "^6.0.0",
"@prisma/client": "^5.7.0",
"@stripe/stripe-js": "^8.0.0",
diff --git a/prisma/schema.prisma b/prisma/schema.prisma
index c2bad48..b60cdaa 100644
--- a/prisma/schema.prisma
+++ b/prisma/schema.prisma
@@ -1,178 +1,168 @@
-// This is your Prisma schema file,
-// learn more about it in the docs: https://pris.ly/d/prisma-schema
-
-generator client {
- provider = "prisma-client-js"
- binaryTargets = ["native", "debian-openssl-3.0.x"]
-}
-
-datasource db {
- provider = "postgresql"
- url = env("DATABASE_URL")
-}
-
-model User {
- id String @id @default(cuid())
- email String @unique
- name String?
- password String?
- image String?
- emailVerified DateTime?
- createdAt DateTime @default(now())
- updatedAt DateTime @updatedAt
-
- // Stripe subscription fields
- stripeCustomerId String? @unique
- stripeSubscriptionId String? @unique
- stripePriceId String?
- stripeCurrentPeriodEnd DateTime?
- plan Plan @default(FREE)
-
- // Password reset fields
- resetPasswordToken String? @unique
- resetPasswordExpires DateTime?
-
- qrCodes QRCode[]
- integrations Integration[]
- accounts Account[]
- sessions Session[]
-}
-
-enum Plan {
- FREE
- PRO
- BUSINESS
-}
-
-model Account {
- id String @id @default(cuid())
- userId String
- type String
- provider String
- providerAccountId String
- refresh_token String? @db.Text
- access_token String? @db.Text
- expires_at Int?
- token_type String?
- scope String?
- id_token String? @db.Text
- session_state String?
-
- user User @relation(fields: [userId], references: [id], onDelete: Cascade)
-
- @@unique([provider, providerAccountId])
-}
-
-model Session {
- id String @id @default(cuid())
- sessionToken String @unique
- userId String
- expires DateTime
- user User @relation(fields: [userId], references: [id], onDelete: Cascade)
-}
-
-model VerificationToken {
- identifier String
- token String @unique
- expires DateTime
-
- @@unique([identifier, token])
-}
-
-model QRCode {
- id String @id @default(cuid())
- userId String
- title String
- type QRType @default(DYNAMIC)
- contentType ContentType @default(URL)
- content Json
- tags String[]
- status QRStatus @default(ACTIVE)
- style Json
- slug String @unique
- createdAt DateTime @default(now())
- updatedAt DateTime @updatedAt
-
- user User @relation(fields: [userId], references: [id], onDelete: Cascade)
- scans QRScan[]
-
- @@index([userId, createdAt])
-}
-
-enum QRType {
- STATIC
- DYNAMIC
-}
-
-enum ContentType {
- URL
- VCARD
- GEO
- PHONE
- SMS
- TEXT
- WHATSAPP
-}
-
-enum QRStatus {
- ACTIVE
- PAUSED
-}
-
-model QRScan {
- id String @id @default(cuid())
- qrId String
- ts DateTime @default(now())
- ipHash String
- userAgent String?
- device String?
- os String?
- country String?
- referrer String?
- utmSource String?
- utmMedium String?
- utmCampaign String?
- isUnique Boolean @default(false)
-
- qr QRCode @relation(fields: [qrId], references: [id], onDelete: Cascade)
-
- @@index([qrId, ts])
-}
-
-model Integration {
- id String @id @default(cuid())
- userId String
- provider String
- status String @default("inactive")
- config Json
- createdAt DateTime @default(now())
- updatedAt DateTime @updatedAt
-
- user User @relation(fields: [userId], references: [id], onDelete: Cascade)
-}
-
-model NewsletterSubscription {
- id String @id @default(cuid())
- email String @unique
- source String @default("ai-coming-soon")
- status String @default("subscribed")
- createdAt DateTime @default(now())
- updatedAt DateTime @updatedAt
-
- @@index([email])
- @@index([createdAt])
-}
-
-model Lead {
- id String @id @default(cuid())
- email String
- source String @default("reprint-calculator")
- reprintCost Float?
- updatesPerYear Int?
- annualSavings Float?
- createdAt DateTime @default(now())
-
- @@index([email])
- @@index([createdAt])
- @@index([source])
+// This is your Prisma schema file,
+// learn more about it in the docs: https://pris.ly/d/prisma-schema
+
+generator client {
+ provider = "prisma-client-js"
+ binaryTargets = ["native", "debian-openssl-3.0.x"]
+}
+
+datasource db {
+ provider = "postgresql"
+ url = env("DATABASE_URL")
+}
+
+model User {
+ id String @id @default(cuid())
+ email String @unique
+ name String?
+ password String?
+ image String?
+ emailVerified DateTime?
+ createdAt DateTime @default(now())
+ updatedAt DateTime @updatedAt
+
+ // Stripe subscription fields
+ stripeCustomerId String? @unique
+ stripeSubscriptionId String? @unique
+ stripePriceId String?
+ stripeCurrentPeriodEnd DateTime?
+ plan Plan @default(FREE)
+
+ // Password reset fields
+ resetPasswordToken String? @unique
+ resetPasswordExpires DateTime?
+
+ qrCodes QRCode[]
+ integrations Integration[]
+ accounts Account[]
+ sessions Session[]
+}
+
+enum Plan {
+ FREE
+ PRO
+ BUSINESS
+}
+
+model Account {
+ id String @id @default(cuid())
+ userId String
+ type String
+ provider String
+ providerAccountId String
+ refresh_token String? @db.Text
+ access_token String? @db.Text
+ expires_at Int?
+ token_type String?
+ scope String?
+ id_token String? @db.Text
+ session_state String?
+
+ user User @relation(fields: [userId], references: [id], onDelete: Cascade)
+
+ @@unique([provider, providerAccountId])
+}
+
+model Session {
+ id String @id @default(cuid())
+ sessionToken String @unique
+ userId String
+ expires DateTime
+ user User @relation(fields: [userId], references: [id], onDelete: Cascade)
+}
+
+model VerificationToken {
+ identifier String
+ token String @unique
+ expires DateTime
+
+ @@unique([identifier, token])
+}
+
+model QRCode {
+ id String @id @default(cuid())
+ userId String
+ title String
+ type QRType @default(DYNAMIC)
+ contentType ContentType @default(URL)
+ content Json
+ tags String[]
+ status QRStatus @default(ACTIVE)
+ style Json
+ slug String @unique
+ createdAt DateTime @default(now())
+ updatedAt DateTime @updatedAt
+
+ user User @relation(fields: [userId], references: [id], onDelete: Cascade)
+ scans QRScan[]
+
+ @@index([userId, createdAt])
+}
+
+enum QRType {
+ STATIC
+ DYNAMIC
+}
+
+enum ContentType {
+ URL
+ VCARD
+ GEO
+ PHONE
+ SMS
+ TEXT
+ WHATSAPP
+ PDF
+ APP
+ COUPON
+ FEEDBACK
+}
+
+enum QRStatus {
+ ACTIVE
+ PAUSED
+}
+
+model QRScan {
+ id String @id @default(cuid())
+ qrId String
+ ts DateTime @default(now())
+ ipHash String
+ userAgent String?
+ device String?
+ os String?
+ country String?
+ referrer String?
+ utmSource String?
+ utmMedium String?
+ utmCampaign String?
+ isUnique Boolean @default(false)
+
+ qr QRCode @relation(fields: [qrId], references: [id], onDelete: Cascade)
+
+ @@index([qrId, ts])
+}
+
+model Integration {
+ id String @id @default(cuid())
+ userId String
+ provider String
+ status String @default("inactive")
+ config Json
+ createdAt DateTime @default(now())
+ updatedAt DateTime @updatedAt
+
+ user User @relation(fields: [userId], references: [id], onDelete: Cascade)
+}
+
+model NewsletterSubscription {
+ id String @id @default(cuid())
+ email String @unique
+ source String @default("ai-coming-soon")
+ status String @default("subscribed")
+ createdAt DateTime @default(now())
+ updatedAt DateTime @updatedAt
+
+ @@index([email])
+ @@index([createdAt])
}
\ No newline at end of file
diff --git a/public/sitemap.xml b/public/sitemap.xml
new file mode 100644
index 0000000..d3ac98c
--- /dev/null
+++ b/public/sitemap.xml
@@ -0,0 +1,33 @@
+
+
+
+ https://www.qrmaster.net/
+ 2025-10-16T00:00:00Z
+ daily
+ 0.9
+
+
+ https://www.qrmaster.net/blog
+ 2025-10-16T00:00:00Z
+ daily
+ 0.7
+
+
+ https://www.qrmaster.net/pricing
+ 2025-10-16T00:00:00Z
+ weekly
+ 0.8
+
+
+ https://www.qrmaster.net/faq
+ 2025-10-16T00:00:00Z
+ weekly
+ 0.6
+
+
+ https://www.qrmaster.net/blog/qr-code-analytics
+ 2025-10-16T00:00:00Z
+ weekly
+ 0.6
+
+
diff --git a/seo_2026_jan.md b/seo_2026_jan.md
new file mode 100644
index 0000000..5ae7587
--- /dev/null
+++ b/seo_2026_jan.md
@@ -0,0 +1,156 @@
+SEO Opportunity Report & Implementation Plan (Jan 2026)
+1. Executive Summary
+An analysis of the provided Google Keyword Planner data (Jan 22, 2026) reveals significant low-competition, high-volume traffic opportunities that were previously untapped. We have immediately capitalized on the Barcode opportunity and have a clear path to capture Custom QR intent.
+
+2. Key Data Findings ("Hidden Gems")
+We identified three specific clusters where search volume is high but competition is exceptionally low.
+
+A. The "QR Barcode" Anomaly (Gold Mine) π
+Users are confused about the terminology, searching for "qr barcode" or "bar code generator" instead of just "barcode".
+
+Keywords: qr barcode, bar code generator, scan code generator
+Volume: 10k β 100k (High)
+Competition: Low / Medium
+Opportunity: Most competitors optimize for "Barcode Generator". By targeting the "wrong" terms users actually type, we can win easy traffic.
+B. The "Free" Intent
+High volume, but users are specifically looking for "free" and "no signup".
+
+Keyword: free qr code generator (100k β 1M)
+Keyword: qr code generator free (100k β 1M)
+Opportunity: Aggressive targeting of these exact match phrases on the homepage metadata.
+C. The "Custom" Gap
+Users want customization but don't always use the term "design".
+
+Keyword: custom qr code generator
+Volume: 1k β 10k
+Competition: Low
+Current Status: MISSING. We do not have a dedicated landing page for this high-intent cluster.
+3. Actions Already Implemented β
+We have immediately updated the metadata to capture the traffic identified in findings A and B.
+
+1. Barcode Generator Optimization
+File:
+src/app/(marketing)/tools/barcode-generator/page.tsx
+
+Action: Updated and meta description.
+New Target: "QR Barcode" and "Bar Code Generator".
+Why: To capture the 100k+ users searching for these specific variants.
+2. Homepage Optimization
+File:
+src/app/(marketing)/page.tsx
+
+Action: Injected high-volume keyword tags.
+New Target: qr generator, free qr code generator, custom qr code generator.
+Why: To signal relevance to Google for the broadest "head terms".
+4. Implementation Plan: "Custom QR Code" Landing Page π
+To capture the 1kβ10k/month users searching for "custom qr code generator" (Finding C), we need a dedicated landing page. This page will focus on design features (colors, logos, frames) rather than just "generating" a code.
+
+Phase 1: Page Structure (New File)
+Path: src/app/(marketing)/tools/custom-qr-code-generator/page.tsx
+
+Content Strategy:
+
+H1: "Free Custom QR Code Generator with Logo & Colors"
+Hero: Visual emphasis on beautiful codes, not black-and-white ones.
+Live Editor Demo: (Reuse existing QRCodeGeneratorClient but pre-opened "Design" tab).
+Features Section:
+"Add Your Logo"
+"Custom Colors & Gradients"
+"Unique Frames & Shapes"
+FAQ: Targeting specific "custom" questions (vector formats, scanning reliability).
+Phase 2: Internal Linking
+Add link to /tools/custom-qr-code-generator in the Footer and Main Navigation under "Tools".
+Link from Homepage "Features" section.
+Phase 3: Metadata Strategy
+title: 'Custom QR Code Generator β Add Logo, Colors & Frames',
+description: 'Create unique custom QR codes with your logo, colors, and frames. Free online customizer with high-quality vector download (SVG/PNG).',
+keywords: ['custom qr code generator', 'qr code with logo', 'qr code design', 'creative qr code', 'branded qr code']
+5. Next Steps
+Approval: Confirm this plan to build the custom-qr-code-generator page.
+Execution: I will create the directory and page file.
+Verify: Check that the new page renders and is linked correctly.
+
+
+
+
+
+
+
+
+
+
+
+
+new findings
+
+Das ist super, dass du die Dateien hochgeladen hast! Denn: Du hast die Daten bereits.
+
+Ich habe deine Excel-Listen analysiert und das Problem gefunden: In deiner Ansicht im Tool hieΓ die Spalte vielleicht anders oder war versteckt, aber in der Datei heiΓt sie Avg. monthly searches. Das ist exakt das Suchvolumen, das du gesucht hast.
+
+Hier sind die echten "SchΓ€tze" direkt aus deiner Datei, gefiltert nach meiner Strategie:
+
+1. Die "Low Hanging Fruits" (Viel Traffic, fast 0 Konkurrenz)
+Das sind die Keywords, bei denen du SOFORT ranken kannst, weil die groΓen Player schlafen.
+
+call qr code
+
+Suchvolumen: 50.000 (!)
+
+Wettbewerb-Index: 0 (Das ist extrem selten! Absolut gar keine Konkurrenz auf Google Ads).
+
+Idee: Mach eine Seite "Telefon-QR-Code erstellen". Leute wollen, dass beim Scannen direkt eine Nummer gewΓ€hlt wird. Das ist eine riesige Nische.
+
+generate website qr code
+
+Suchvolumen: 50.000
+
+Wettbewerb-Index: 17 (Sehr gering).
+
+CPC: 6,16 β¬ (Das ist der Jackpot: Viel Traffic, wenig Konkurrenz UND hoher Wert).
+
+bar code generator
+
+Suchvolumen: 500.000
+
+Wettbewerb-Index: 3 (Fast nichts).
+
+BestΓ€tigt meine frΓΌhere Analyse: Das ist dein Traffic-Bringer.
+
+2. Die "Goldminen" (Hoher Klickpreis / High Ticket)
+Hier zahlen Firmen richtig Geld. Wenn du hier rankst, sind die Nutzer sehr wertvoll.
+
+manage qr codes
+
+CPC (Gebot): 18,77 β¬
+
+Das suchen Firmen, die viele Codes verwalten mΓΌssen (Agenturen, GroΓkunden). Perfekt fΓΌr dein "Pro"-Abo.
+
+dynamic code generator
+
+CPC: 15,62 β¬
+
+Das Kern-Keyword fΓΌr dein Business-Modell.
+
+qr tracking
+
+CPC: 12,51 β¬
+
+Wettbewerb ist hier hΓΆher, aber der Umsatz pro Kunde auch.
+
+3. Der Trend-Tipp (+900% Wachstum)
+free bulk qr code generator (Massen-Erstellung)
+
+Trend: +900%
+
+Viele Leute suchen nach einer MΓΆglichkeit, 100 Codes auf einmal zu erstellen (z.B. fΓΌr Mitarbeiterausweise oder Inventar). Wenn du so ein Feature ("Bulk Upload") hast oder baust, bist du MarktfΓΌhrer.
+
+Was du jetzt tun musst (in Excel oder Google Sheets):
+Du brauchst das Tool gar nicht mehr zwingend. Du kannst einfach in deiner Datei filtern:
+
+Such die Spalte Avg. monthly searches -> Das ist dein Suchvolumen.
+
+Such die Spalte Competition (indexed value) -> Das ist die HΓ€rte (0-100).
+
+Filter: Avg. monthly searches > 1000 UND Competition (indexed value) < 20.
+
+Damit findest du genau die oben genannten Chancen. Soll ich dir fΓΌr eines dieser Keywords (z.B. den "Call QR Code" mit Index 0) eine Gliederung schreiben?
\ No newline at end of file
diff --git a/src/app/(app)/create/page.tsx b/src/app/(app)/create/page.tsx
index 8dc134b..888bf09 100644
--- a/src/app/(app)/create/page.tsx
+++ b/src/app/(app)/create/page.tsx
@@ -14,6 +14,20 @@ import { calculateContrast, cn } from '@/lib/utils';
import { useTranslation } from '@/hooks/useTranslation';
import { useCsrf } from '@/hooks/useCsrf';
import { showToast } from '@/components/ui/Toast';
+import {
+ Globe, User, MapPin, Phone, FileText, Smartphone, Ticket, Star, HelpCircle, Upload
+} from 'lucide-react';
+
+// Tooltip component for form field help
+const Tooltip = ({ text }: { text: string }) => (
+
+);
// Content-type specific frame options
const getFrameOptionsForContentType = (contentType: string) => {
@@ -34,6 +48,14 @@ const getFrameOptionsForContentType = (contentType: string) => {
return [...baseOptions, { id: 'chatme', label: 'Chat Me' }, { id: 'whatsapp', label: 'WhatsApp' }];
case 'TEXT':
return [...baseOptions, { id: 'read', label: 'Read' }, { id: 'info', label: 'Info' }];
+ case 'PDF':
+ return [...baseOptions, { id: 'download', label: 'Download' }, { id: 'view', label: 'View PDF' }];
+ case 'APP':
+ return [...baseOptions, { id: 'getapp', label: 'Get App' }, { id: 'download', label: 'Download' }];
+ case 'COUPON':
+ return [...baseOptions, { id: 'redeem', label: 'Redeem' }, { id: 'save', label: 'Save Offer' }];
+ case 'FEEDBACK':
+ return [...baseOptions, { id: 'review', label: 'Review' }, { id: 'feedback', label: 'Feedback' }];
default:
return [...baseOptions, { id: 'website', label: 'Website' }, { id: 'visit', label: 'Visit' }];
}
@@ -44,6 +66,7 @@ export default function CreatePage() {
const { t } = useTranslation();
const { fetchWithCsrf } = useCsrf();
const [loading, setLoading] = useState(false);
+ const [uploading, setUploading] = useState(false);
const [userPlan, setUserPlan] = useState('FREE');
const qrRef = useRef(null);
@@ -102,10 +125,14 @@ export default function CreatePage() {
const hasGoodContrast = contrast >= 4.5;
const contentTypes = [
- { value: 'URL', label: 'URL / Website' },
- { value: 'VCARD', label: 'Contact Card' },
- { value: 'GEO', label: 'Location/Maps' },
- { value: 'PHONE', label: 'Phone Number' },
+ { value: 'URL', label: 'URL / Website', icon: Globe },
+ { value: 'VCARD', label: 'Contact Card', icon: User },
+ { value: 'GEO', label: 'Location / Maps', icon: MapPin },
+ { value: 'PHONE', label: 'Phone Number', icon: Phone },
+ { value: 'PDF', label: 'PDF / File', icon: FileText },
+ { value: 'APP', label: 'App Download', icon: Smartphone },
+ { value: 'COUPON', label: 'Coupon / Discount', icon: Ticket },
+ { value: 'FEEDBACK', label: 'Feedback / Review', icon: Star },
];
// Get QR content based on content type
@@ -128,6 +155,14 @@ export default function CreatePage() {
return content.text || 'Sample text';
case 'WHATSAPP':
return `https://wa.me/${content.phone || '+1234567890'}${content.message ? `?text=${encodeURIComponent(content.message)}` : ''}`;
+ case 'PDF':
+ return content.fileUrl || 'https://example.com/file.pdf';
+ case 'APP':
+ return content.fallbackUrl || content.iosUrl || content.androidUrl || 'https://example.com/app';
+ case 'COUPON':
+ return `Coupon: ${content.code || 'SAVE20'} - ${content.discount || '20% OFF'}`;
+ case 'FEEDBACK':
+ return content.feedbackUrl || 'https://example.com/feedback';
default:
return 'https://example.com';
}
@@ -398,6 +433,208 @@ export default function CreatePage() {
/>
);
+ case 'PDF':
+ const handleFileUpload = async (e: React.ChangeEvent) => {
+ const file = e.target.files?.[0];
+ if (!file) return;
+
+ // 10MB limit
+ if (file.size > 10 * 1024 * 1024) {
+ showToast('File size too large (max 10MB)', 'error');
+ return;
+ }
+
+ setUploading(true);
+ const formData = new FormData();
+ formData.append('file', file);
+
+ try {
+ const response = await fetch('/api/upload', {
+ method: 'POST',
+ body: formData,
+ });
+ const data = await response.json();
+
+ if (response.ok) {
+ setContent({ ...content, fileUrl: data.url, fileName: data.filename });
+ showToast('File uploaded successfully!', 'success');
+ } else {
+ showToast(data.error || 'Upload failed', 'error');
+ }
+ } catch (error) {
+ console.error('Upload error:', error);
+ showToast('Error uploading file', 'error');
+ } finally {
+ setUploading(false);
+ }
+ };
+
+ return (
+ <>
+
+
+
+
+
+
+
+
+ {uploading ? (
+
+ ) : content.fileUrl ? (
+
+ ) : (
+ <>
+
+
+
+
or drag and drop
+
+
PDF, PNG, JPG up to 10MB
+ >
+ )}
+
+
+
+
+ {content.fileUrl && (
+ setContent({ ...content, fileName: e.target.value })}
+ placeholder="Product Catalog 2026"
+ />
+ )}
+ >
+ );
+ case 'APP':
+ return (
+ <>
+
+
+
+
+
+
setContent({ ...content, iosUrl: e.target.value })}
+ placeholder="https://apps.apple.com/app/..."
+ />
+
+
+
+
+
+
+
setContent({ ...content, androidUrl: e.target.value })}
+ placeholder="https://play.google.com/store/apps/..."
+ />
+
+
+
+
+
+
+
setContent({ ...content, fallbackUrl: e.target.value })}
+ placeholder="https://yourapp.com"
+ />
+
+ >
+ );
+ case 'COUPON':
+ return (
+ <>
+ setContent({ ...content, code: e.target.value })}
+ placeholder="SUMMER20"
+ required
+ />
+ setContent({ ...content, discount: e.target.value })}
+ placeholder="20% OFF"
+ required
+ />
+ setContent({ ...content, title: e.target.value })}
+ placeholder="Summer Sale 2026"
+ />
+ setContent({ ...content, description: e.target.value })}
+ placeholder="Valid on all products"
+ />
+ setContent({ ...content, expiryDate: e.target.value })}
+ />
+ setContent({ ...content, redeemUrl: e.target.value })}
+ placeholder="https://shop.example.com?coupon=SUMMER20"
+ />
+ >
+ );
+ case 'FEEDBACK':
+ return (
+ <>
+ setContent({ ...content, businessName: e.target.value })}
+ placeholder="Your Restaurant Name"
+ required
+ />
+
+
+
+
+
+
setContent({ ...content, googleReviewUrl: e.target.value })}
+ placeholder="https://search.google.com/local/writereview?placeid=..."
+ />
+
+ setContent({ ...content, thankYouMessage: e.target.value })}
+ placeholder="Thanks for your feedback!"
+ />
+ >
+ );
default:
return null;
}
@@ -428,12 +665,31 @@ export default function CreatePage() {
required
/>
-
)}
+ {/* Feedback Button - only for FEEDBACK type */}
+ {qr.contentType === 'FEEDBACK' && (
+
+ )}
diff --git a/src/components/marketing/FAQ.tsx b/src/components/marketing/FAQ.tsx
index 69b793c..65454b6 100644
--- a/src/components/marketing/FAQ.tsx
+++ b/src/components/marketing/FAQ.tsx
@@ -89,4 +89,4 @@ export const FAQ: React.FC = ({ t }) => {
);
-};
+};
\ No newline at end of file
diff --git a/src/components/marketing/Features.tsx b/src/components/marketing/Features.tsx
index d701366..c40ae46 100644
--- a/src/components/marketing/Features.tsx
+++ b/src/components/marketing/Features.tsx
@@ -82,4 +82,4 @@ export const Features: React.FC = ({ t }) => {
);
-};
+};
\ No newline at end of file
diff --git a/src/components/marketing/FreeToolsGrid.tsx b/src/components/marketing/FreeToolsGrid.tsx
index f73fa33..ba949de 100644
--- a/src/components/marketing/FreeToolsGrid.tsx
+++ b/src/components/marketing/FreeToolsGrid.tsx
@@ -222,9 +222,18 @@ export function FreeToolsGrid() {
transition={{ duration: 0.5 }}
className="text-center mb-16"
>
-
- More Free QR Code Tools
-
+
+
+ More Free QR Code Tools
+
+
+
+
+
+
+ Free Forever
+
+
Create specialized QR codes for every need. Completely free and no signup required.
diff --git a/src/components/marketing/Hero.tsx b/src/components/marketing/Hero.tsx
index 4b2d519..be6d085 100644
--- a/src/components/marketing/Hero.tsx
+++ b/src/components/marketing/Hero.tsx
@@ -6,19 +6,77 @@ import { Button } from '@/components/ui/Button';
import { Badge } from '@/components/ui/Badge';
import { Card } from '@/components/ui/Card';
import { motion } from 'framer-motion';
-import { Globe, User, MapPin, Phone, CheckCircle2, ArrowRight } from 'lucide-react';
+import { Globe, User, MapPin, Phone, CheckCircle2, ArrowRight, FileText, Ticket, Smartphone, Star } from 'lucide-react';
+import { useState, useEffect } from 'react';
+
+// Sub-component for the flipping effect
+const FlippingCard = ({ front, back, delay }: { front: any, back: any, delay: number }) => {
+ const [isFlipped, setIsFlipped] = useState(false);
+
+ useEffect(() => {
+ // Initial delay
+ const initialTimeout = setTimeout(() => {
+ setIsFlipped(true); // First flip
+
+ // Setup interval for subsequent flips
+ const interval = setInterval(() => {
+ setIsFlipped(prev => !prev);
+ }, 8000); // Toggle every 8 seconds to prevent overlap (4 cards * 2s gap)
+
+ return () => clearInterval(interval);
+ }, delay * 1000);
+
+ return () => clearTimeout(initialTimeout);
+ }, [delay]);
+
+ return (
+
+
+ {/* Front Face */}
+
+
+
+
+
+ {front.title}
+
+
+
+ {/* Back Face */}
+
+
+
+
+
+ {back.title}
+
+
+
+
+ );
+};
interface HeroProps {
t: any; // i18n translation function
}
export const Hero: React.FC = ({ t }) => {
- const templateCards = [
- { title: 'URL/Website', color: 'bg-blue-500/10 text-blue-600', icon: Globe },
- { title: 'Contact Card', color: 'bg-purple-500/10 text-purple-600', icon: User },
- { title: 'Location', color: 'bg-green-500/10 text-green-600', icon: MapPin },
- { title: 'Phone Number', color: 'bg-pink-500/10 text-pink-600', icon: Phone },
- ];
+
const containerjs = {
hidden: { opacity: 0 },
@@ -66,9 +124,9 @@ export const Hero: React.FC = ({ t }) => {
transition={{ duration: 0.5 }}
className="space-y-6"
>
-
+
{t.hero.title}
-
+
{t.hero.subtitle}
@@ -113,37 +171,34 @@ export const Hero: React.FC = ({ t }) => {
{/* Right Preview Widget */}
-
- {templateCards.map((card, index) => (
-
-
-
-
-
- {card.title}
-
-
- ))}
-
-
- {/* Floating Badge */}
-
-
-
-
-
- {t.hero.engagement_badge}
-
+
+
+ {[
+ {
+ front: { title: 'URL/Website', color: 'bg-blue-500/10 text-blue-600', icon: Globe },
+ back: { title: 'PDF / Menu', color: 'bg-orange-500/10 text-orange-600', icon: FileText },
+ delay: 3 // Starts at 3s
+ },
+ {
+ front: { title: 'Contact Card', color: 'bg-purple-500/10 text-purple-600', icon: User },
+ back: { title: 'Coupon / Deals', color: 'bg-red-500/10 text-red-600', icon: Ticket },
+ delay: 5 // +2s
+ },
+ {
+ front: { title: 'Location', color: 'bg-green-500/10 text-green-600', icon: MapPin },
+ back: { title: 'App Store', color: 'bg-sky-500/10 text-sky-600', icon: Smartphone },
+ delay: 7 // +2s
+ },
+ {
+ front: { title: 'Phone Number', color: 'bg-pink-500/10 text-pink-600', icon: Phone },
+ back: { title: 'Feedback', color: 'bg-yellow-500/10 text-yellow-600', icon: Star },
+ delay: 9 // +2s
+ },
+ ].map((card, index) => (
+
+ ))}
+
+
@@ -152,4 +207,4 @@ export const Hero: React.FC = ({ t }) => {
);
-};
+};
\ No newline at end of file
diff --git a/src/components/marketing/InstantGenerator.tsx b/src/components/marketing/InstantGenerator.tsx
index 0e5f557..d6f3a9a 100644
--- a/src/components/marketing/InstantGenerator.tsx
+++ b/src/components/marketing/InstantGenerator.tsx
@@ -8,7 +8,6 @@ import { Input } from '@/components/ui/Input';
import { Button } from '@/components/ui/Button';
import { Badge } from '@/components/ui/Badge';
import { calculateContrast } from '@/lib/utils';
-import AdBanner from '@/components/ads/AdBanner';
interface InstantGeneratorProps {
t: any; // i18n translation function
@@ -280,4 +279,4 @@ export const InstantGenerator: React.FC = ({ t }) => {
);
-};
+};
\ No newline at end of file
diff --git a/src/components/marketing/Pricing.tsx b/src/components/marketing/Pricing.tsx
index 2655f6d..9f9671c 100644
--- a/src/components/marketing/Pricing.tsx
+++ b/src/components/marketing/Pricing.tsx
@@ -138,4 +138,4 @@ export const Pricing: React.FC = ({ t }) => {
);
-};
+};
\ No newline at end of file
diff --git a/src/components/marketing/StaticVsDynamic.tsx b/src/components/marketing/StaticVsDynamic.tsx
index 6128112..bd0329a 100644
--- a/src/components/marketing/StaticVsDynamic.tsx
+++ b/src/components/marketing/StaticVsDynamic.tsx
@@ -95,4 +95,4 @@ export const StaticVsDynamic: React.FC = ({ t }) => {
);
-};
+};
\ No newline at end of file
diff --git a/src/components/marketing/StatsStrip.tsx b/src/components/marketing/StatsStrip.tsx
index 75c779b..e275589 100644
--- a/src/components/marketing/StatsStrip.tsx
+++ b/src/components/marketing/StatsStrip.tsx
@@ -32,4 +32,4 @@ export const StatsStrip: React.FC = ({ t }) => {
);
-};
+};
\ No newline at end of file
diff --git a/src/components/marketing/TemplateCards.tsx b/src/components/marketing/TemplateCards.tsx
index 5d56cd3..bbb7fce 100644
--- a/src/components/marketing/TemplateCards.tsx
+++ b/src/components/marketing/TemplateCards.tsx
@@ -67,4 +67,4 @@ export const TemplateCards: React.FC = ({ t }) => {
);
-};
+};
\ No newline at end of file
diff --git a/src/i18n/de.json b/src/i18n/de.json
index 8d747d8..d74ebdd 100644
--- a/src/i18n/de.json
+++ b/src/i18n/de.json
@@ -9,10 +9,7 @@
"create_qr": "QR erstellen",
"bulk_creation": "Massen-Erstellung",
"analytics": "Analytik",
- "settings": "Einstellungen",
- "cta": "Kostenlos loslegen",
- "tools": "Kostenlose Tools",
- "all_free": "Alle Generatoren sind 100% kostenlos"
+ "settings": "Einstellungen"
},
"hero": {
"badge": "Kostenloser QR-Code-Generator",
@@ -67,8 +64,6 @@
"demo_note": "Dies ist ein Demo-QR-Code"
},
"static_vs_dynamic": {
- "title": "Warum dynamische QR-Codes Geld sparen",
- "description": "HΓΆren Sie auf, Materialien neu zu drucken. Γndern Sie Zielorte sofort und verfolgen Sie jeden Scan.",
"static": {
"title": "Statische QR-Codes",
"subtitle": "Immer kostenlos",
@@ -102,10 +97,6 @@
"title": "VollstΓ€ndige Anpassung",
"description": "Branden Sie Ihre QR-Codes mit individuellen Farben, Logos und Styling-Optionen."
},
- "unlimited": {
- "title": "Unbegrenzte statische QR-Codes",
- "description": "Erstellen Sie so viele statische QR-Codes wie Sie mΓΆchten. FΓΌr immer kostenlos, ohne Limits."
- },
"bulk": {
"title": "Bulk-Operationen",
"description": "Erstellen Sie hunderte von QR-Codes auf einmal mit CSV-Import und Batch-Verarbeitung."
@@ -379,22 +370,5 @@
"loading": "LΓ€dt...",
"error": "Ein Fehler ist aufgetreten",
"success": "Erfolgreich!"
- },
- "footer": {
- "product": "Produkt",
- "features": "Funktionen",
- "pricing": "Preise",
- "faq": "FAQ",
- "blog": "Blog",
- "resources": "Ressourcen",
- "full_pricing": "Alle Preise",
- "all_questions": "Alle Fragen",
- "all_articles": "Alle Artikel",
- "get_started": "Loslegen",
- "legal": "Rechtliches",
- "privacy_policy": "DatenschutzerklΓ€rung",
- "tagline": "Erstellen Sie individuelle QR-Codes in Sekunden mit erweitertem Tracking und Analytik.",
- "newsletter": "Newsletter Anmeldung",
- "rights_reserved": "QR Master. Alle Rechte vorbehalten."
}
}
\ No newline at end of file
diff --git a/src/lib/db.ts b/src/lib/db.ts
index fe5ae7e..b521586 100644
--- a/src/lib/db.ts
+++ b/src/lib/db.ts
@@ -6,8 +6,6 @@ const globalForPrisma = globalThis as unknown as {
export const db =
globalForPrisma.prisma ??
- new PrismaClient({
- log: ['query'],
- });
+ new PrismaClient();
if (process.env.NODE_ENV !== 'production') globalForPrisma.prisma = db;
\ No newline at end of file
diff --git a/src/lib/env.ts b/src/lib/env.ts
index 2852df6..0480526 100644
--- a/src/lib/env.ts
+++ b/src/lib/env.ts
@@ -1,27 +1,35 @@
-import { z } from 'zod';
-
-const envSchema = z.object({
- NODE_ENV: z.enum(['development', 'production', 'test']).default('development'),
- PORT: z.string().default('3000'),
- DATABASE_URL: z.string().default('postgresql://postgres:postgres@localhost:5432/qrmaster?schema=public'),
- NEXTAUTH_URL: z.string().default('http://localhost:3050'),
- NEXTAUTH_SECRET: z.string().default('development-secret-change-in-production'),
- GOOGLE_CLIENT_ID: z.string().optional(),
- GOOGLE_CLIENT_SECRET: z.string().optional(),
- REDIS_URL: z.string().optional(),
- IP_SALT: z.string().default('development-salt-change-in-production'),
- ENABLE_DEMO: z.string().default('false'),
-});
-
-// During build, we might not have all env vars, so we'll use defaults
-const isBuildTime = process.env.NEXT_PHASE === 'phase-production-build' || !process.env.DATABASE_URL;
-
-export const env = isBuildTime
- ? envSchema.parse({
- ...process.env,
- DATABASE_URL: process.env.DATABASE_URL || 'postgresql://postgres:postgres@db:5432/qrmaster?schema=public',
- NEXTAUTH_URL: process.env.NEXTAUTH_URL || 'http://localhost:3050',
- NEXTAUTH_SECRET: process.env.NEXTAUTH_SECRET || 'development-secret-change-in-production',
- IP_SALT: process.env.IP_SALT || 'development-salt-change-in-production',
- })
+import { z } from 'zod';
+
+const envSchema = z.object({
+ NODE_ENV: z.enum(['development', 'production', 'test']).default('development'),
+ PORT: z.string().default('3000'),
+ DATABASE_URL: z.string().default('postgresql://postgres:postgres@localhost:5432/qrmaster?schema=public'),
+ NEXTAUTH_URL: z.string().default('http://localhost:3050'),
+ NEXTAUTH_SECRET: z.string().default('development-secret-change-in-production'),
+ GOOGLE_CLIENT_ID: z.string().optional(),
+ GOOGLE_CLIENT_SECRET: z.string().optional(),
+ REDIS_URL: z.string().optional(),
+ IP_SALT: z.string().default('development-salt-change-in-production'),
+ ENABLE_DEMO: z.string().default('false'),
+
+ // Cloudflare R2 (S3 Compatible)
+ R2_ACCOUNT_ID: z.string().optional(),
+ R2_ACCESS_KEY_ID: z.string().optional(),
+ R2_SECRET_ACCESS_KEY: z.string().optional(),
+ R2_BUCKET_NAME: z.string().default('qrmaster-menus'),
+ R2_PUBLIC_URL: z.string().optional(),
+ MAX_UPLOAD_SIZE: z.string().default('10485760'), // 10MB default
+});
+
+// During build, we might not have all env vars, so we'll use defaults
+const isBuildTime = process.env.NEXT_PHASE === 'phase-production-build' || !process.env.DATABASE_URL;
+
+export const env = isBuildTime
+ ? envSchema.parse({
+ ...process.env,
+ DATABASE_URL: process.env.DATABASE_URL || 'postgresql://postgres:postgres@db:5432/qrmaster?schema=public',
+ NEXTAUTH_URL: process.env.NEXTAUTH_URL || 'http://localhost:3050',
+ NEXTAUTH_SECRET: process.env.NEXTAUTH_SECRET || 'development-secret-change-in-production',
+ IP_SALT: process.env.IP_SALT || 'development-salt-change-in-production',
+ })
: envSchema.parse(process.env);
\ No newline at end of file
diff --git a/src/lib/r2.ts b/src/lib/r2.ts
new file mode 100644
index 0000000..21e27f2
--- /dev/null
+++ b/src/lib/r2.ts
@@ -0,0 +1,65 @@
+import { S3Client, PutObjectCommand, DeleteObjectCommand } from '@aws-sdk/client-s3';
+import { env } from './env';
+import crypto from 'crypto';
+
+// Initialize S3 client for Cloudflare R2
+const r2Client = new S3Client({
+ region: 'auto',
+ endpoint: `https://${env.R2_ACCOUNT_ID || 'placeholder'}.r2.cloudflarestorage.com`,
+ credentials: {
+ accessKeyId: env.R2_ACCESS_KEY_ID || '',
+ secretAccessKey: env.R2_SECRET_ACCESS_KEY || '',
+ },
+});
+
+export async function uploadFileToR2(
+ file: Buffer,
+ filename: string,
+ contentType: string = 'application/pdf'
+): Promise {
+ // Generate a unique key for the file
+ const ext = filename.split('.').pop() || 'pdf';
+ const randomId = crypto.randomBytes(8).toString('hex');
+ const timestamp = Date.now();
+ const key = `uploads/${timestamp}_${randomId}.${ext}`;
+
+ await r2Client.send(
+ new PutObjectCommand({
+ Bucket: env.R2_BUCKET_NAME,
+ Key: key,
+ Body: file,
+ ContentType: contentType,
+ ContentDisposition: `inline; filename="${filename}"`,
+ // Cache for 1 year, as these are static files
+ CacheControl: 'public, max-age=31536000',
+ })
+ );
+
+ // Return the public URL
+ // If R2_PUBLIC_URL is set, use it (custom domain or r2.dev subdomain)
+ // Otherwise, construct a fallback (which might not work without public access enabled on bucket)
+ const publicUrl = env.R2_PUBLIC_URL
+ ? `${env.R2_PUBLIC_URL}/${key}`
+ : `https://${env.R2_BUCKET_NAME}.r2.dev/${key}`;
+
+ return publicUrl;
+}
+
+export async function deleteFileFromR2(fileUrl: string): Promise {
+ try {
+ // Extract key from URL
+ // URL format: https://domain.com/uploads/filename.pdf
+ const url = new URL(fileUrl);
+ const key = url.pathname.substring(1); // Remove leading slash
+
+ await r2Client.send(
+ new DeleteObjectCommand({
+ Bucket: env.R2_BUCKET_NAME,
+ Key: key,
+ })
+ );
+ } catch (error) {
+ console.error('Error deleting file from R2:', error);
+ // Suppress error, as deletion failure shouldn't block main flow
+ }
+}
diff --git a/src/lib/schema.ts b/src/lib/schema.ts
index ed77019..5d81360 100644
--- a/src/lib/schema.ts
+++ b/src/lib/schema.ts
@@ -1,269 +1,245 @@
-export interface BreadcrumbItem {
- name: string;
- url: string;
-}
-
-export interface BlogPost {
- title: string;
- description: string;
- slug: string;
- author: string;
- authorUrl: string;
- datePublished: string;
- dateModified: string;
- image: string;
-}
-
-export interface FAQItem {
- question: string;
- answer: string;
-}
-
-export interface ProductOffer {
- name: string;
- price: string;
- priceCurrency: string;
- availability: string;
- url: string;
-}
-
-export interface HowToStep {
- name: string;
- text: string;
- url?: string;
-}
-
-export interface HowToTask {
- name: string;
- description: string;
- steps: HowToStep[];
- totalTime?: string;
-}
-
-const BASE_URL = 'https://www.qrmaster.net';
-
-function toAbsoluteUrl(path: string): string {
- if (path.startsWith('http')) return path;
- return `${BASE_URL}${path.startsWith('/') ? '' : '/'}${path}`;
-}
-
-export function organizationSchema() {
- return {
- '@context': 'https://schema.org',
- '@type': 'Organization',
- '@id': `${BASE_URL}/#organization`,
- name: 'QR Master',
- alternateName: 'QRMaster',
- url: BASE_URL,
- logo: {
- '@type': 'ImageObject',
- url: `${BASE_URL}/og-image.png`,
- width: 1200,
- height: 630,
- },
- image: `${BASE_URL}/og-image.png`,
- sameAs: [
- 'https://twitter.com/qrmaster',
- ],
- contactPoint: {
- '@type': 'ContactPoint',
- contactType: 'Customer Support',
- email: 'support@qrmaster.net',
- availableLanguage: ['English', 'German'],
- },
- description: 'B2B SaaS platform for dynamic QR code generation with analytics, branding, and bulk generation for enterprise marketing campaigns.',
- slogan: 'Dynamic QR codes that work smarter',
- foundingDate: '2025',
- areaServed: 'Worldwide',
- knowsAbout: [
- 'QR Code Generation',
- 'Marketing Analytics',
- 'Campaign Tracking',
- 'Dynamic QR Codes',
- 'Bulk QR Generation',
- ],
- hasOfferCatalog: {
- '@type': 'OfferCatalog',
- name: 'QR Master Plans',
- itemListElement: [
- {
- '@type': 'Offer',
- itemOffered: {
- '@type': 'SoftwareApplication',
- name: 'QR Master Free',
- applicationCategory: 'BusinessApplication',
- operatingSystem: 'Web Browser',
- offers: {
- '@type': 'Offer',
- price: '0',
- priceCurrency: 'EUR',
- },
- aggregateRating: {
- '@type': 'AggregateRating',
- ratingValue: '4.8',
- ratingCount: '1250',
- },
- },
- },
- {
- '@type': 'Offer',
- itemOffered: {
- '@type': 'SoftwareApplication',
- name: 'QR Master Pro',
- applicationCategory: 'BusinessApplication',
- operatingSystem: 'Web Browser',
- offers: {
- '@type': 'Offer',
- price: '9',
- priceCurrency: 'EUR',
- },
- aggregateRating: {
- '@type': 'AggregateRating',
- ratingValue: '4.9',
- ratingCount: '850',
- },
- },
- },
- ],
- },
- mainEntityOfPage: BASE_URL,
- };
-}
-
-export function websiteSchema() {
- return {
- '@context': 'https://schema.org',
- '@type': 'WebSite',
- '@id': `${BASE_URL}/#website`,
- name: 'QR Master',
- url: BASE_URL,
- inLanguage: 'en',
- mainEntityOfPage: BASE_URL,
- publisher: {
- '@id': `${BASE_URL}/#organization`,
- },
- potentialAction: {
- '@type': 'SearchAction',
- target: {
- '@type': 'EntryPoint',
- urlTemplate: `${BASE_URL}/blog?q={search_term_string}`,
- },
- 'query-input': 'required name=search_term_string',
- },
- };
-}
-
-export function breadcrumbSchema(items: BreadcrumbItem[]) {
- return {
- '@context': 'https://schema.org',
- '@type': 'BreadcrumbList',
- '@id': `${BASE_URL}${items[items.length - 1]?.url}#breadcrumb`,
- inLanguage: 'en',
- mainEntityOfPage: `${BASE_URL}${items[items.length - 1]?.url}`,
- itemListElement: items.map((item, index) => ({
- '@type': 'ListItem',
- position: index + 1,
- name: item.name,
- item: toAbsoluteUrl(item.url),
- })),
- };
-}
-
-export function blogPostingSchema(post: BlogPost) {
- return {
- '@context': 'https://schema.org',
- '@type': 'BlogPosting',
- '@id': `${BASE_URL}/blog/${post.slug}#article`,
- headline: post.title,
- description: post.description,
- image: toAbsoluteUrl(post.image),
- datePublished: post.datePublished,
- dateModified: post.dateModified,
- inLanguage: 'en',
- mainEntityOfPage: `${BASE_URL}/blog/${post.slug}`,
- author: {
- '@type': 'Person',
- name: post.author,
- url: post.authorUrl,
- },
- publisher: {
- '@type': 'Organization',
- name: 'QR Master',
- url: BASE_URL,
- logo: {
- '@type': 'ImageObject',
- url: `${BASE_URL}/og-image.png`,
- width: 1200,
- height: 630,
- },
- },
- isPartOf: {
- '@type': 'Blog',
- '@id': `${BASE_URL}/blog#blog`,
- name: 'QR Master Blog',
- url: `${BASE_URL}/blog`,
- },
- };
-}
-
-export function faqPageSchema(faqs: FAQItem[]) {
- return {
- '@context': 'https://schema.org',
- '@type': 'FAQPage',
- '@id': `${BASE_URL}/faq#faqpage`,
- inLanguage: 'en',
- mainEntityOfPage: `${BASE_URL}/faq`,
- mainEntity: faqs.map((faq) => ({
- '@type': 'Question',
- name: faq.question,
- acceptedAnswer: {
- '@type': 'Answer',
- text: faq.answer,
- },
- })),
- };
-}
-
-export function productSchema(product: { name: string; description: string; offers: ProductOffer[] }) {
- return {
- '@context': 'https://schema.org',
- '@type': 'Product',
- '@id': `${BASE_URL}/pricing#product`,
- name: product.name,
- description: product.description,
- inLanguage: 'en',
- mainEntityOfPage: `${BASE_URL}/pricing`,
- brand: {
- '@type': 'Organization',
- name: 'QR Master',
- },
- offers: product.offers.map((offer) => ({
- '@type': 'Offer',
- name: offer.name,
- price: offer.price,
- priceCurrency: offer.priceCurrency,
- availability: offer.availability,
- url: toAbsoluteUrl(offer.url),
- })),
- };
-}
-
-export function howToSchema(task: HowToTask) {
- return {
- '@context': 'https://schema.org',
- '@type': 'HowTo',
- '@id': `${BASE_URL}/blog/${task.name.toLowerCase().replace(/\s+/g, '-')}#howto`,
- name: task.name,
- description: task.description,
- inLanguage: 'en',
- mainEntityOfPage: `${BASE_URL}/blog/${task.name.toLowerCase().replace(/\s+/g, '-')}`,
- totalTime: task.totalTime || 'PT5M',
- step: task.steps.map((step, index) => ({
- '@type': 'HowToStep',
- position: index + 1,
- name: step.name,
- text: step.text,
- url: step.url ? toAbsoluteUrl(step.url) : undefined,
- })),
- };
-}
+export interface BreadcrumbItem {
+ name: string;
+ url: string;
+}
+
+export interface BlogPost {
+ title: string;
+ description: string;
+ slug: string;
+ author: string;
+ authorUrl: string;
+ datePublished: string;
+ dateModified: string;
+ image: string;
+}
+
+export interface FAQItem {
+ question: string;
+ answer: string;
+}
+
+export interface ProductOffer {
+ name: string;
+ price: string;
+ priceCurrency: string;
+ availability: string;
+ url: string;
+}
+
+export interface HowToStep {
+ name: string;
+ text: string;
+ url?: string;
+}
+
+export interface HowToTask {
+ name: string;
+ description: string;
+ steps: HowToStep[];
+ totalTime?: string;
+}
+
+export function organizationSchema() {
+ return {
+ '@context': 'https://schema.org',
+ '@type': 'Organization',
+ '@id': 'https://www.qrmaster.net/#organization',
+ name: 'QR Master',
+ alternateName: 'QRMaster',
+ url: 'https://www.qrmaster.net',
+ logo: {
+ '@type': 'ImageObject',
+ url: 'https://www.qrmaster.net/static/og-image.png',
+ width: 1200,
+ height: 630,
+ },
+ image: 'https://www.qrmaster.net/static/og-image.png',
+ sameAs: [
+ 'https://twitter.com/qrmaster',
+ ],
+ contactPoint: {
+ '@type': 'ContactPoint',
+ contactType: 'Customer Support',
+ email: 'support@qrmaster.net',
+ availableLanguage: ['English', 'German'],
+ },
+ description: 'B2B SaaS platform for dynamic QR code generation with analytics, branding, and bulk generation for enterprise marketing campaigns.',
+ slogan: 'Dynamic QR codes that work smarter',
+ foundingDate: '2025',
+ areaServed: 'Worldwide',
+ serviceType: 'Software as a Service',
+ priceRange: '$0 - $29',
+ knowsAbout: [
+ 'QR Code Generation',
+ 'Marketing Analytics',
+ 'Campaign Tracking',
+ 'Dynamic QR Codes',
+ 'Bulk QR Generation',
+ ],
+ hasOfferCatalog: {
+ '@type': 'OfferCatalog',
+ name: 'QR Master Plans',
+ itemListElement: [
+ {
+ '@type': 'Offer',
+ itemOffered: {
+ '@type': 'SoftwareApplication',
+ name: 'QR Master Free',
+ applicationCategory: 'BusinessApplication',
+ operatingSystem: 'Web Browser',
+ },
+ },
+ {
+ '@type': 'Offer',
+ itemOffered: {
+ '@type': 'SoftwareApplication',
+ name: 'QR Master Pro',
+ applicationCategory: 'BusinessApplication',
+ operatingSystem: 'Web Browser',
+ },
+ },
+ ],
+ },
+ inLanguage: 'en',
+ mainEntityOfPage: 'https://www.qrmaster.net',
+ };
+}
+
+export function websiteSchema() {
+ return {
+ '@context': 'https://schema.org',
+ '@type': 'WebSite',
+ '@id': 'https://www.qrmaster.net/#website',
+ name: 'QR Master',
+ url: 'https://www.qrmaster.net',
+ inLanguage: 'en',
+ mainEntityOfPage: 'https://www.qrmaster.net',
+ publisher: {
+ '@id': 'https://www.qrmaster.net/#organization',
+ },
+ potentialAction: {
+ '@type': 'SearchAction',
+ target: {
+ '@type': 'EntryPoint',
+ urlTemplate: 'https://www.qrmaster.net/blog?q={search_term_string}',
+ },
+ 'query-input': 'required name=search_term_string',
+ },
+ };
+}
+
+export function breadcrumbSchema(items: BreadcrumbItem[]) {
+ return {
+ '@context': 'https://schema.org',
+ '@type': 'BreadcrumbList',
+ '@id': `https://www.qrmaster.net${items[items.length - 1]?.url}#breadcrumb`,
+ inLanguage: 'en',
+ mainEntityOfPage: `https://www.qrmaster.net${items[items.length - 1]?.url}`,
+ itemListElement: items.map((item, index) => ({
+ '@type': 'ListItem',
+ position: index + 1,
+ name: item.name,
+ item: `https://www.qrmaster.net${item.url}`,
+ })),
+ };
+}
+
+export function blogPostingSchema(post: BlogPost) {
+ return {
+ '@context': 'https://schema.org',
+ '@type': 'BlogPosting',
+ '@id': `https://www.qrmaster.net/blog/${post.slug}#article`,
+ headline: post.title,
+ description: post.description,
+ image: post.image,
+ datePublished: post.datePublished,
+ dateModified: post.dateModified,
+ inLanguage: 'en',
+ mainEntityOfPage: `https://www.qrmaster.net/blog/${post.slug}`,
+ author: {
+ '@type': 'Person',
+ name: post.author,
+ url: post.authorUrl,
+ },
+ publisher: {
+ '@type': 'Organization',
+ name: 'QR Master',
+ url: 'https://www.qrmaster.net',
+ logo: {
+ '@type': 'ImageObject',
+ url: 'https://www.qrmaster.net/static/og-image.png',
+ width: 1200,
+ height: 630,
+ },
+ },
+ isPartOf: {
+ '@type': 'Blog',
+ '@id': 'https://www.qrmaster.net/blog#blog',
+ name: 'QR Master Blog',
+ url: 'https://www.qrmaster.net/blog',
+ },
+ };
+}
+
+export function faqPageSchema(faqs: FAQItem[]) {
+ return {
+ '@context': 'https://schema.org',
+ '@type': 'FAQPage',
+ '@id': 'https://www.qrmaster.net/faq#faqpage',
+ inLanguage: 'en',
+ mainEntityOfPage: 'https://www.qrmaster.net/faq',
+ mainEntity: faqs.map((faq) => ({
+ '@type': 'Question',
+ name: faq.question,
+ acceptedAnswer: {
+ '@type': 'Answer',
+ text: faq.answer,
+ },
+ })),
+ };
+}
+
+export function productSchema(product: { name: string; description: string; offers: ProductOffer[] }) {
+ return {
+ '@context': 'https://schema.org',
+ '@type': 'Product',
+ '@id': 'https://www.qrmaster.net/pricing#product',
+ name: product.name,
+ description: product.description,
+ inLanguage: 'en',
+ mainEntityOfPage: 'https://www.qrmaster.net/pricing',
+ brand: {
+ '@type': 'Organization',
+ name: 'QR Master',
+ },
+ offers: product.offers.map((offer) => ({
+ '@type': 'Offer',
+ name: offer.name,
+ price: offer.price,
+ priceCurrency: offer.priceCurrency,
+ availability: offer.availability,
+ url: offer.url,
+ })),
+ };
+}
+
+export function howToSchema(task: HowToTask) {
+ return {
+ '@context': 'https://schema.org',
+ '@type': 'HowTo',
+ '@id': `https://www.qrmaster.net/blog/${task.name.toLowerCase().replace(/\s+/g, '-')}#howto`,
+ name: task.name,
+ description: task.description,
+ inLanguage: 'en',
+ mainEntityOfPage: `https://www.qrmaster.net/blog/${task.name.toLowerCase().replace(/\s+/g, '-')}`,
+ totalTime: task.totalTime || 'PT5M',
+ step: task.steps.map((step, index) => ({
+ '@type': 'HowToStep',
+ position: index + 1,
+ name: step.name,
+ text: step.text,
+ url: step.url,
+ })),
+ };
+}
diff --git a/src/lib/validationSchemas.ts b/src/lib/validationSchemas.ts
index e60ead6..0bbaa46 100644
--- a/src/lib/validationSchemas.ts
+++ b/src/lib/validationSchemas.ts
@@ -1,186 +1,186 @@
-/**
- * Zod Validation Schemas for API endpoints
- * Centralized validation logic for type-safety and security
- */
-
-import { z } from 'zod';
-
-// ==========================================
-// QR Code Schemas
-// ==========================================
-
-export const qrStyleSchema = z.object({
- fgColor: z.string().regex(/^#[0-9A-F]{6}$/i, 'Invalid foreground color format').optional(),
- bgColor: z.string().regex(/^#[0-9A-F]{6}$/i, 'Invalid background color format').optional(),
- cornerStyle: z.enum(['square', 'rounded']).optional(),
- size: z.number().min(100).max(1000).optional(),
-});
-
-export const createQRSchema = z.object({
- title: z.string()
- .min(1, 'Title is required')
- .max(100, 'Title must be less than 100 characters'),
-
- content: z.record(z.any()), // Accept any object structure for content
-
- isStatic: z.boolean().optional(),
-
- contentType: z.enum(['URL', 'VCARD', 'GEO', 'PHONE', 'SMS', 'WHATSAPP', 'TEXT'], {
- errorMap: () => ({ message: 'Invalid content type' })
- }),
-
- tags: z.array(z.string()).optional(),
-
- style: z.object({
- foregroundColor: z.string().optional(),
- backgroundColor: z.string().optional(),
- cornerStyle: z.enum(['square', 'rounded']).optional(),
- size: z.number().optional(),
- }).optional(),
-});
-
-export const updateQRSchema = z.object({
- title: z.string()
- .min(1, 'Title is required')
- .max(100, 'Title must be less than 100 characters')
- .optional(),
-
- content: z.string()
- .min(1, 'Content is required')
- .max(5000, 'Content must be less than 5000 characters')
- .optional(),
-
- style: qrStyleSchema.optional(),
-
- isActive: z.boolean().optional(),
-});
-
-export const bulkQRSchema = z.object({
- qrs: z.array(
- z.object({
- title: z.string().min(1).max(100),
- content: z.string().min(1).max(5000),
- contentType: z.enum(['URL', 'VCARD', 'GEO', 'PHONE', 'SMS', 'WHATSAPP', 'TEXT']),
- })
- ).min(1, 'At least one QR code is required')
- .max(100, 'Maximum 100 QR codes per bulk creation'),
-});
-
-// ==========================================
-// Authentication Schemas
-// ==========================================
-
-export const loginSchema = z.object({
- email: z.string()
- .email('Invalid email format')
- .toLowerCase(),
-
- password: z.string()
- .min(1, 'Password is required'),
-});
-
-export const signupSchema = z.object({
- name: z.string()
- .min(2, 'Name must be at least 2 characters')
- .max(100, 'Name must be less than 100 characters')
- .trim(),
-
- email: z.string()
- .email('Invalid email format')
- .toLowerCase()
- .trim(),
-
- password: z.string()
- .min(8, 'Password must be at least 8 characters')
- .max(100, 'Password must be less than 100 characters'),
- // Password complexity rules removed for easier testing
-});
-
-export const forgotPasswordSchema = z.object({
- email: z.string()
- .email('Invalid email format')
- .toLowerCase()
- .trim(),
-});
-
-export const resetPasswordSchema = z.object({
- token: z.string().min(1, 'Reset token is required'),
- password: z.string()
- .min(8, 'Password must be at least 8 characters')
- .max(100, 'Password must be less than 100 characters'),
- // Password complexity rules removed for easier testing
-});
-
-// ==========================================
-// Settings Schemas
-// ==========================================
-
-export const updateProfileSchema = z.object({
- name: z.string()
- .min(2, 'Name must be at least 2 characters')
- .max(100, 'Name must be less than 100 characters')
- .trim(),
-});
-
-export const changePasswordSchema = z.object({
- currentPassword: z.string()
- .min(1, 'Current password is required'),
-
- newPassword: z.string()
- .min(8, 'Password must be at least 8 characters')
- .max(100, 'Password must be less than 100 characters'),
- // Password complexity rules removed for easier testing
-});
-
-// ==========================================
-// Stripe Schemas
-// ==========================================
-
-export const createCheckoutSchema = z.object({
- priceId: z.string().min(1, 'Price ID is required'),
-});
-
-// ==========================================
-// Newsletter Schemas
-// ==========================================
-
-export const newsletterSubscribeSchema = z.object({
- email: z.string()
- .email('Invalid email format')
- .toLowerCase()
- .trim()
- .max(255, 'Email must be less than 255 characters'),
-});
-
-// ==========================================
-// Helper: Format Zod Errors
-// ==========================================
-
-export function formatZodError(error: z.ZodError) {
- return {
- error: 'Validation failed',
- details: error.errors.map(err => ({
- field: err.path.join('.'),
- message: err.message,
- })),
- };
-}
-
-// ==========================================
-// Helper: Validate with Zod
-// ==========================================
-
-export async function validateRequest(
- schema: z.ZodSchema,
- data: unknown
-): Promise<{ success: true; data: T } | { success: false; error: any }> {
- try {
- const validatedData = schema.parse(data);
- return { success: true, data: validatedData };
- } catch (error) {
- if (error instanceof z.ZodError) {
- return { success: false, error: formatZodError(error) };
- }
- return { success: false, error: { error: 'Invalid request data' } };
- }
-}
+/**
+ * Zod Validation Schemas for API endpoints
+ * Centralized validation logic for type-safety and security
+ */
+
+import { z } from 'zod';
+
+// ==========================================
+// QR Code Schemas
+// ==========================================
+
+export const qrStyleSchema = z.object({
+ fgColor: z.string().regex(/^#[0-9A-F]{6}$/i, 'Invalid foreground color format').optional(),
+ bgColor: z.string().regex(/^#[0-9A-F]{6}$/i, 'Invalid background color format').optional(),
+ cornerStyle: z.enum(['square', 'rounded']).optional(),
+ size: z.number().min(100).max(1000).optional(),
+});
+
+export const createQRSchema = z.object({
+ title: z.string()
+ .min(1, 'Title is required')
+ .max(100, 'Title must be less than 100 characters'),
+
+ content: z.record(z.any()), // Accept any object structure for content
+
+ isStatic: z.boolean().optional(),
+
+ contentType: z.enum(['URL', 'VCARD', 'GEO', 'PHONE', 'SMS', 'WHATSAPP', 'TEXT', 'PDF', 'APP', 'COUPON', 'FEEDBACK'], {
+ errorMap: () => ({ message: 'Invalid content type' })
+ }),
+
+ tags: z.array(z.string()).optional(),
+
+ style: z.object({
+ foregroundColor: z.string().optional(),
+ backgroundColor: z.string().optional(),
+ cornerStyle: z.enum(['square', 'rounded']).optional(),
+ size: z.number().optional(),
+ }).optional(),
+});
+
+export const updateQRSchema = z.object({
+ title: z.string()
+ .min(1, 'Title is required')
+ .max(100, 'Title must be less than 100 characters')
+ .optional(),
+
+ content: z.string()
+ .min(1, 'Content is required')
+ .max(5000, 'Content must be less than 5000 characters')
+ .optional(),
+
+ style: qrStyleSchema.optional(),
+
+ isActive: z.boolean().optional(),
+});
+
+export const bulkQRSchema = z.object({
+ qrs: z.array(
+ z.object({
+ title: z.string().min(1).max(100),
+ content: z.string().min(1).max(5000),
+ contentType: z.enum(['URL', 'VCARD', 'GEO', 'PHONE', 'SMS', 'WHATSAPP', 'TEXT', 'PDF', 'APP', 'COUPON', 'FEEDBACK']),
+ })
+ ).min(1, 'At least one QR code is required')
+ .max(100, 'Maximum 100 QR codes per bulk creation'),
+});
+
+// ==========================================
+// Authentication Schemas
+// ==========================================
+
+export const loginSchema = z.object({
+ email: z.string()
+ .email('Invalid email format')
+ .toLowerCase(),
+
+ password: z.string()
+ .min(1, 'Password is required'),
+});
+
+export const signupSchema = z.object({
+ name: z.string()
+ .min(2, 'Name must be at least 2 characters')
+ .max(100, 'Name must be less than 100 characters')
+ .trim(),
+
+ email: z.string()
+ .email('Invalid email format')
+ .toLowerCase()
+ .trim(),
+
+ password: z.string()
+ .min(8, 'Password must be at least 8 characters')
+ .max(100, 'Password must be less than 100 characters'),
+ // Password complexity rules removed for easier testing
+});
+
+export const forgotPasswordSchema = z.object({
+ email: z.string()
+ .email('Invalid email format')
+ .toLowerCase()
+ .trim(),
+});
+
+export const resetPasswordSchema = z.object({
+ token: z.string().min(1, 'Reset token is required'),
+ password: z.string()
+ .min(8, 'Password must be at least 8 characters')
+ .max(100, 'Password must be less than 100 characters'),
+ // Password complexity rules removed for easier testing
+});
+
+// ==========================================
+// Settings Schemas
+// ==========================================
+
+export const updateProfileSchema = z.object({
+ name: z.string()
+ .min(2, 'Name must be at least 2 characters')
+ .max(100, 'Name must be less than 100 characters')
+ .trim(),
+});
+
+export const changePasswordSchema = z.object({
+ currentPassword: z.string()
+ .min(1, 'Current password is required'),
+
+ newPassword: z.string()
+ .min(8, 'Password must be at least 8 characters')
+ .max(100, 'Password must be less than 100 characters'),
+ // Password complexity rules removed for easier testing
+});
+
+// ==========================================
+// Stripe Schemas
+// ==========================================
+
+export const createCheckoutSchema = z.object({
+ priceId: z.string().min(1, 'Price ID is required'),
+});
+
+// ==========================================
+// Newsletter Schemas
+// ==========================================
+
+export const newsletterSubscribeSchema = z.object({
+ email: z.string()
+ .email('Invalid email format')
+ .toLowerCase()
+ .trim()
+ .max(255, 'Email must be less than 255 characters'),
+});
+
+// ==========================================
+// Helper: Format Zod Errors
+// ==========================================
+
+export function formatZodError(error: z.ZodError) {
+ return {
+ error: 'Validation failed',
+ details: error.errors.map(err => ({
+ field: err.path.join('.'),
+ message: err.message,
+ })),
+ };
+}
+
+// ==========================================
+// Helper: Validate with Zod
+// ==========================================
+
+export async function validateRequest(
+ schema: z.ZodSchema,
+ data: unknown
+): Promise<{ success: true; data: T } | { success: false; error: any }> {
+ try {
+ const validatedData = schema.parse(data);
+ return { success: true, data: validatedData };
+ } catch (error) {
+ if (error instanceof z.ZodError) {
+ return { success: false, error: formatZodError(error) };
+ }
+ return { success: false, error: { error: 'Invalid request data' } };
+ }
+}
diff --git a/src/types/next-auth.d.ts b/src/types/next-auth.d.ts
index 84c6e24..6e47e18 100644
--- a/src/types/next-auth.d.ts
+++ b/src/types/next-auth.d.ts
@@ -1,15 +1,13 @@
-import { DefaultSession } from 'next-auth';
-
-declare module 'next-auth' {
- interface Session {
- user: {
- id: string;
- plan?: string | null;
- } & DefaultSession['user'];
- }
-
- interface User {
- id: string;
- plan?: string | null;
- }
+import { DefaultSession } from 'next-auth';
+
+declare module 'next-auth' {
+ interface Session {
+ user: {
+ id: string;
+ } & DefaultSession['user'];
+ }
+
+ interface User {
+ id: string;
+ }
}
\ No newline at end of file