diff --git a/.env.example b/.env.example index dbb029b..2d008f4 100644 --- a/.env.example +++ b/.env.example @@ -1,3 +1,25 @@ -VITE_APP_NAME=SimplePasswordGen -VITE_DEFAULT_LANG=en -# no secrets required \ No newline at end of file +# Next.js Configuration +NEXT_PUBLIC_SITE_URL=https://passmaster.app + +# IndexNow Configuration +INDEXNOW_KEY=your-indexnow-key-here +SITE_HOST=passmaster.app + +# SEO/AEO Configuration +ORG_NAME="PassMaster" +ORG_LOGO_URL=https://passmaster.app/icons/icon-512.png +CONTACT_EMAIL=contact@passmaster.app +DEFAULT_AUTHOR_NAME="PassMaster Team" +DEFAULT_AUTHOR_URL=https://passmaster.app/about + +# Social Media +LINKEDIN_URL=https://www.linkedin.com/company/passmaster +TWITTER_URL=https://twitter.com/passmaster + +# Feature Flags +ENABLE_INDEXNOW=true +ENABLE_SCHEMA_VALIDATION=true + +# Legacy (for compatibility) +VITE_APP_NAME=PassMaster +VITE_DEFAULT_LANG=de \ No newline at end of file diff --git a/README.md b/README.md index b89703a..527fbca 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,9 @@ A modern, privacy-first password generator built with Next.js, TypeScript, and T - 🔧 **Customizable Options** - Length, character types, and exclude similar characters - 📋 **One-Click Copy** - Copy passwords to clipboard with visual feedback - ♿ **Accessible** - Full keyboard navigation and screen reader support -- 📊 **SEO Optimized** - Structured data, meta tags, and semantic HTML +- 📊 **SEO & AEO Optimized** - Answer Engine Optimization for ChatGPT/Perplexity, structured data, meta tags, and semantic HTML +- 🔍 **IndexNow Integration** - Automatic search engine notifications for content updates +- 🤖 **Answer Engine Ready** - Optimized for AI crawlers (GPTBot, PerplexityBot, Claude-Web) ## 🚀 Getting Started @@ -78,7 +80,27 @@ src/ Create a `.env.local` file: ```env -NEXT_PUBLIC_SITE_URL=https://your-domain.com +# Next.js Configuration +NEXT_PUBLIC_SITE_URL=https://passmaster.app + +# IndexNow Configuration +INDEXNOW_KEY=your-indexnow-key-here +SITE_HOST=passmaster.app + +# SEO/AEO Configuration +ORG_NAME="PassMaster" +ORG_LOGO_URL=https://passmaster.app/icons/icon-512.png +CONTACT_EMAIL=contact@passmaster.app +DEFAULT_AUTHOR_NAME="PassMaster Team" +DEFAULT_AUTHOR_URL=https://passmaster.app/about + +# Social Media +LINKEDIN_URL=https://www.linkedin.com/company/passmaster +TWITTER_URL=https://twitter.com/passmaster + +# Feature Flags +ENABLE_INDEXNOW=true +ENABLE_SCHEMA_VALIDATION=true ``` ### PWA Configuration @@ -93,6 +115,177 @@ The PWA is configured in `next.config.js` and `public/manifest.json`. Update the - **Splash Screen**: Custom loading screen - **Theme Colors**: Consistent branding +## 🤖 Answer Engine Optimization (AEO) Setup + +PassMaster is optimized for AI search engines like ChatGPT, Perplexity, and Claude. Here's how to set it up: + +### 1. IndexNow Configuration + +Get your IndexNow API key from [Microsoft IndexNow](https://www.indexnow.org/) and add it to your environment: + +```bash +INDEXNOW_KEY=your-unique-key +SITE_HOST=passmaster.app +ENABLE_INDEXNOW=true +``` + +The key file will be automatically created at `/public/{INDEXNOW_KEY}.txt`. + +### 2. Manual IndexNow Pinging + +Use the CLI tool to manually notify search engines about updates: + +```bash +# Test with homepage +npx tsx scripts/ping-indexnow.ts --test + +# Ping specific URLs +npx tsx scripts/ping-indexnow.ts https://passmaster.app/offline https://passmaster.app/client-side + +# Ping all main pages +npx tsx scripts/ping-indexnow.ts --all + +# Check status +npx tsx scripts/ping-indexnow.ts --status +``` + +### 3. Automatic Pinging + +IndexNow automatically triggers when: +- Pages are updated or published +- New content is added +- Sitemap is updated + +Use the API endpoint for programmatic pinging: + +```javascript +// Queue URLs for IndexNow notification +await fetch('/api/indexnow', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + urls: ['https://passmaster.app/new-page'] + }) +}) +``` + +### 4. JSON-LD Schema Usage + +Add structured data to your pages: + +```tsx +import { FAQPageJsonLd, HowToJsonLd, ArticleJsonLd } from '@/components/seo/JsonLd' + +// FAQ Page + + +// How-To Content + + +// Article Content + +``` + +### 5. Content Metadata + +Add publication and update dates to your content: + +```tsx +import { ContentMeta, AuthorBox } from '@/components/seo/ContentMeta' + + +``` + +### 6. Canonical URLs + +Ensure proper canonical URLs on all pages: + +```tsx +import { Canonical } from '@/components/seo/Canonical' + +// Automatic canonical based on current path + + +// Custom canonical URL + +``` + +## 🔍 AEO Validation + +### Robots.txt Validation + +```bash +curl https://passmaster.app/robots.txt +# Should show: User-agent: PerplexityBot, Allow: / +# Should show: User-agent: GPTBot, Allow: / +``` + +### Schema Validation + +```bash +# Check JSON-LD on any page +curl https://passmaster.app/ | grep -o '' + +# Validate with structured data testing tool +# https://search.google.com/test/rich-results +``` + +### Sitemap Validation + +```bash +curl https://passmaster.app/sitemap.xml | head -20 +# Should show proper XML structure with dates +``` + +### IndexNow Testing + +```bash +# Test IndexNow API manually +npx tsx scripts/ping-indexnow.ts --test + +# Check queue status +curl http://localhost:3000/api/indexnow +``` + +## 🤖 Answer Engine Features + +- **PerplexityBot Support**: Explicitly allowed in robots.txt +- **GPTBot Support**: Optimized for ChatGPT crawling +- **Claude-Web Support**: Compatible with Claude's web search +- **FAQ Schema**: Structured Q&A data for answer engines +- **HowTo Schema**: Step-by-step instructions for AI responses +- **Article Schema**: Rich content metadata for citations +- **Updated Dates**: Visible "Zuletzt aktualisiert" timestamps +- **Author Information**: Clear attribution for content +- **Canonical URLs**: Single source of truth for content +- **IndexNow Integration**: Real-time search engine notifications + ## 🎨 Customization ### Colors diff --git a/app/client-side/page.tsx b/app/client-side/page.tsx new file mode 100644 index 0000000..9f9d9d1 --- /dev/null +++ b/app/client-side/page.tsx @@ -0,0 +1,338 @@ +"use client" + +import { motion } from 'framer-motion' +import { + Shield, + Lock, + Eye, + Server, + FileText, + CheckCircle, + ArrowLeft, + Key, + Globe, + Zap +} from 'lucide-react' +import Link from 'next/link' + +export default function ClientSidePage() { + const securityFeatures = [ + { + icon: Lock, + title: "Client-Side Encryption", + description: "All password generation happens locally in your browser using the Web Crypto API. Your passwords never leave your device." + }, + { + icon: Eye, + title: "No Server Communication", + description: "After the initial page load, the app works completely offline. No data is sent to or received from any servers." + }, + { + icon: Server, + title: "Zero Data Storage", + description: "We don't store any passwords, user data, or personal information. Everything is processed locally and immediately discarded." + }, + { + icon: Shield, + title: "Open Source Verification", + description: "All code is publicly available and auditable. You can verify our security claims by reviewing the source code." + } + ] + + const technicalDetails = [ + { + title: "Web Crypto API", + items: [ + "Cryptographically secure random number generation", + "Industry-standard encryption algorithms", + "Hardware-based entropy when available", + "No reliance on Math.random() or weak PRNGs" + ] + }, + { + title: "Local Processing", + items: [ + "All password generation in JavaScript", + "No network requests during generation", + "Immediate memory cleanup after use", + "No persistent storage of generated passwords" + ] + }, + { + title: "Privacy Protection", + items: [ + "No user tracking or analytics", + "No cookies or local storage", + "No third-party services", + "No data collection whatsoever" + ] + } + ] + + const securityBenefits = [ + { + icon: Key, + title: "Maximum Security", + description: "Your passwords are generated using the same cryptographic standards used by banks and government agencies." + }, + { + icon: Globe, + title: "Complete Privacy", + description: "No one, including us, can see or access your generated passwords. They exist only on your device." + }, + { + icon: Zap, + title: "Instant Generation", + description: "Generate passwords in milliseconds without any network delays or server dependencies." + } + ] + + return ( +
+ {/* Header */} +
+
+
+ + + Zurück zu PassMaster + +
+
+
+ +
+ {/* Hero Section */} + +
+
+ +
+
+

+ Client-Side Sicherheit +

+

+ Maximale Sicherheit durch lokale Verarbeitung. Ihre Passwörter werden ausschließlich in Ihrem Browser generiert und verlassen niemals Ihr Gerät. +

+
+ + {/* Security Features */} +
+ +

+ Sicherheits-Features +

+

+ Jeder Aspekt von PassMaster ist darauf ausgelegt, Ihre Sicherheit zu maximieren. +

+
+ +
+ {securityFeatures.map((feature, index) => ( + +
+
+
+ +
+
+
+

+ {feature.title} +

+

+ {feature.description} +

+
+
+
+ ))} +
+
+ + {/* Security Benefits */} +
+ +

+ Warum Client-Side Sicherheit? +

+

+ Die Vorteile der lokalen Passwort-Generierung. +

+
+ +
+ {securityBenefits.map((benefit, index) => ( + +
+
+ +
+
+

+ {benefit.title} +

+

+ {benefit.description} +

+
+ ))} +
+
+ + {/* Technical Details */} +
+ +

+ Technische Details +

+

+ Wie PassMaster Ihre Sicherheit gewährleistet. +

+
+ +
+ {technicalDetails.map((detail, index) => ( + +

+ {detail.title} +

+
    + {detail.items.map((item, itemIndex) => ( +
  • + + {item} +
  • + ))} +
+
+ ))} +
+
+ + {/* Implementation Details */} +
+ +
+ +

+ Technische Implementierung +

+
+ +
+

Wie PassMaster funktioniert

+
    +
  • + + Lokale Verarbeitung: Alle Passwort-Generierung erfolgt in Ihrem Browser mit JavaScript +
  • +
  • + + Keine Netzwerk-Anfragen: Die App funktioniert nach dem ersten Laden vollständig offline +
  • +
  • + + Open Source: Der gesamte Code ist öffentlich auf GitHub verfügbar zur Überprüfung +
  • +
  • + + Keine Abhängigkeiten: Wir verwenden keine externen Services oder Drittanbieter-Bibliotheken +
  • +
+
+
+
+ + {/* Call to Action */} +
+ +

+ Fragen zur Sicherheit? +

+

+ Wir sind verpflichtet zu Transparenz. Wenn Sie Fragen zu unseren Sicherheitspraktiken haben, + überprüfen Sie unseren Quellcode oder kontaktieren Sie uns. +

+
+ + Quellcode ansehen + + + Zum Generator + +
+
+
+
+
+ ) +} diff --git a/app/exclude-similar/page.tsx b/app/exclude-similar/page.tsx new file mode 100644 index 0000000..2de4188 --- /dev/null +++ b/app/exclude-similar/page.tsx @@ -0,0 +1,404 @@ +"use client" + +import { motion } from 'framer-motion' +import { + Eye, + CheckCircle, + XCircle, + ArrowLeft, + BookOpen, + Target, + Users, + Zap +} from 'lucide-react' +import Link from 'next/link' + +export default function ExcludeSimilarPage() { + const readabilityFeatures = [ + { + icon: Eye, + title: "Ähnliche Zeichen ausschließen", + description: "Verwirrende Zeichen wie 0/O, 1/l/I werden automatisch ausgeschlossen, um Lesbarkeit zu verbessern." + }, + { + icon: BookOpen, + title: "Bessere Lesbarkeit", + description: "Passwörter sind leichter zu lesen und zu tippen, ohne die Sicherheit zu beeinträchtigen." + }, + { + icon: Target, + title: "Weniger Fehler", + description: "Reduziert Tippfehler beim manuellen Eingeben von Passwörtern erheblich." + }, + { + icon: Users, + title: "Benutzerfreundlich", + description: "Besonders nützlich für ältere Benutzer oder bei der Eingabe auf mobilen Geräten." + } + ] + + const excludedCharacters = [ + { + category: "Zahlen und Buchstaben", + characters: ["0 (Null)", "O (Großes O)", "1 (Eins)", "l (kleines L)", "I (Großes i)"], + reason: "Diese Zeichen sehen in vielen Schriftarten identisch aus" + }, + { + category: "Sonderzeichen", + characters: ["| (Pipe)", "` (Backtick)", "' (Apostroph)", "\" (Anführungszeichen)"], + reason: "Können in verschiedenen Kontexten verwirrend sein" + }, + { + category: "Leerzeichen", + characters: [" (Leerzeichen)", " (Mehrfache Leerzeichen)"], + reason: "Können beim Kopieren/Einfügen Probleme verursachen" + } + ] + + const benefits = [ + { + icon: Zap, + title: "Schnellere Eingabe", + description: "Weniger Verwirrung beim manuellen Tippen von Passwörtern." + }, + { + icon: CheckCircle, + title: "Weniger Fehler", + description: "Reduziert Tippfehler und damit verbundene Frustration." + }, + { + icon: Eye, + title: "Bessere UX", + description: "Verbessert die Benutzererfahrung ohne Sicherheitsverlust." + } + ] + + const securityImpact = [ + { + title: "Sicherheit bleibt hoch", + items: [ + "Entropie wird nur minimal reduziert", + "Noch immer über 80 Zeichen verfügbar", + "Kryptographisch sichere Generierung", + "Ausreichend für alle praktischen Zwecke" + ] + }, + { + title: "Praktische Vorteile", + items: [ + "Einfachere manuelle Eingabe", + "Weniger Support-Anfragen", + "Bessere Benutzerakzeptanz", + "Reduzierte Fehlerrate" + ] + }, + { + title: "Empfohlene Verwendung", + items: [ + "Für manuell eingegebene Passwörter", + "Bei älteren Benutzern", + "Auf mobilen Geräten", + "In Umgebungen mit schlechter Sichtbarkeit" + ] + } + ] + + return ( +
+ {/* Header */} +
+
+
+ + + Zurück zu PassMaster + +
+
+
+ +
+ {/* Hero Section */} + +
+
+ +
+
+

+ Lesbarkeit & Benutzerfreundlichkeit +

+

+ Verbessern Sie die Lesbarkeit Ihrer Passwörter ohne Sicherheit zu opfern. + Ähnliche Zeichen werden automatisch ausgeschlossen. +

+
+ + {/* Readability Features */} +
+ +

+ Lesbarkeits-Features +

+

+ Wie PassMaster die Benutzerfreundlichkeit verbessert. +

+
+ +
+ {readabilityFeatures.map((feature, index) => ( + +
+
+
+ +
+
+
+

+ {feature.title} +

+

+ {feature.description} +

+
+
+
+ ))} +
+
+ + {/* Excluded Characters */} +
+ +

+ Ausgeschlossene Zeichen +

+

+ Diese Zeichen werden automatisch ausgeschlossen, um Verwirrung zu vermeiden. +

+
+ +
+ {excludedCharacters.map((category, index) => ( + +

+ {category.category} +

+
    + {category.characters.map((char, charIndex) => ( +
  • + + {char} +
  • + ))} +
+

+ {category.reason} +

+
+ ))} +
+
+ + {/* Benefits */} +
+ +

+ Vorteile der Lesbarkeit +

+

+ Warum lesbare Passwörter wichtig sind. +

+
+ +
+ {benefits.map((benefit, index) => ( + +
+
+ +
+
+

+ {benefit.title} +

+

+ {benefit.description} +

+
+ ))} +
+
+ + {/* Security Impact */} +
+ +

+ Sicherheit vs. Lesbarkeit +

+

+ Wie wir das perfekte Gleichgewicht finden. +

+
+ +
+ {securityImpact.map((impact, index) => ( + +

+ {impact.title} +

+
    + {impact.items.map((item, itemIndex) => ( +
  • + + {item} +
  • + ))} +
+
+ ))} +
+
+ + {/* Example Comparison */} +
+ +

+ Beispiel-Vergleich +

+ +
+
+

+ + Ohne Lesbarkeits-Filter +

+
+
Schwer lesbar:
+
K9mP0lI|nQ2v
+
+ Verwirrende Zeichen: 0, l, I, | +
+
+
+ +
+

+ + Mit Lesbarkeits-Filter +

+
+
Leicht lesbar:
+
K9mP3nQ2vX7w
+
+ Keine verwirrenden Zeichen +
+
+
+
+
+
+ + {/* Call to Action */} +
+ +

+ Bereit für bessere Lesbarkeit? +

+

+ Aktivieren Sie die Lesbarkeits-Option in PassMaster und generieren Sie + benutzerfreundliche, aber dennoch sichere Passwörter. +

+
+ + Jetzt ausprobieren + + + Sicherheit erfahren + +
+
+
+
+
+ ) +} diff --git a/package-lock.json b/package-lock.json index 4f73b26..a809487 100644 --- a/package-lock.json +++ b/package-lock.json @@ -21,6 +21,7 @@ "zustand": "^4.4.7" }, "devDependencies": { + "@playwright/test": "^1.55.0", "@types/node": "^20.10.5", "@types/react": "^18.2.45", "@types/react-dom": "^18.2.18", @@ -28,11 +29,14 @@ "@typescript-eslint/eslint-plugin": "^6.16.0", "@typescript-eslint/parser": "^6.16.0", "autoprefixer": "^10.4.16", + "dotenv": "^16.6.1", "eslint": "^8.56.0", "eslint-config-next": "^14.0.4", "postcss": "^8.4.32", "prettier": "^3.1.1", + "sharp": "^0.33.2", "tailwindcss": "^3.4.0", + "tsx": "^4.20.5", "typescript": "^5.3.3" } }, @@ -1592,6 +1596,448 @@ "license": "MIT", "optional": true }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.9.tgz", + "integrity": "sha512-OaGtL73Jck6pBKjNIe24BnFE6agGl+6KxDtTfHhy1HmhthfKouEcOhqpSL64K4/0WCtbKFLOdzD/44cJ4k9opA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.9.tgz", + "integrity": "sha512-5WNI1DaMtxQ7t7B6xa572XMXpHAaI/9Hnhk8lcxF4zVN4xstUgTlvuGDorBguKEnZO70qwEcLpfifMLoxiPqHQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.9.tgz", + "integrity": "sha512-IDrddSmpSv51ftWslJMvl3Q2ZT98fUSL2/rlUXuVqRXHCs5EUF1/f+jbjF5+NG9UffUDMCiTyh8iec7u8RlTLg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.9.tgz", + "integrity": "sha512-I853iMZ1hWZdNllhVZKm34f4wErd4lMyeV7BLzEExGEIZYsOzqDWDf+y082izYUE8gtJnYHdeDpN/6tUdwvfiw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.9.tgz", + "integrity": "sha512-XIpIDMAjOELi/9PB30vEbVMs3GV1v2zkkPnuyRRURbhqjyzIINwj+nbQATh4H9GxUgH1kFsEyQMxwiLFKUS6Rg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.9.tgz", + "integrity": "sha512-jhHfBzjYTA1IQu8VyrjCX4ApJDnH+ez+IYVEoJHeqJm9VhG9Dh2BYaJritkYK3vMaXrf7Ogr/0MQ8/MeIefsPQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.9.tgz", + "integrity": "sha512-z93DmbnY6fX9+KdD4Ue/H6sYs+bhFQJNCPZsi4XWJoYblUqT06MQUdBCpcSfuiN72AbqeBFu5LVQTjfXDE2A6Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.9.tgz", + "integrity": "sha512-mrKX6H/vOyo5v71YfXWJxLVxgy1kyt1MQaD8wZJgJfG4gq4DpQGpgTB74e5yBeQdyMTbgxp0YtNj7NuHN0PoZg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.9.tgz", + "integrity": "sha512-HBU2Xv78SMgaydBmdor38lg8YDnFKSARg1Q6AT0/y2ezUAKiZvc211RDFHlEZRFNRVhcMamiToo7bDx3VEOYQw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.9.tgz", + "integrity": "sha512-BlB7bIcLT3G26urh5Dmse7fiLmLXnRlopw4s8DalgZ8ef79Jj4aUcYbk90g8iCa2467HX8SAIidbL7gsqXHdRw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.9.tgz", + "integrity": "sha512-e7S3MOJPZGp2QW6AK6+Ly81rC7oOSerQ+P8L0ta4FhVi+/j/v2yZzx5CqqDaWjtPFfYz21Vi1S0auHrap3Ma3A==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.9.tgz", + "integrity": "sha512-Sbe10Bnn0oUAB2AalYztvGcK+o6YFFA/9829PhOCUS9vkJElXGdphz0A3DbMdP8gmKkqPmPcMJmJOrI3VYB1JQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.9.tgz", + "integrity": "sha512-YcM5br0mVyZw2jcQeLIkhWtKPeVfAerES5PvOzaDxVtIyZ2NUBZKNLjC5z3/fUlDgT6w89VsxP2qzNipOaaDyA==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.9.tgz", + "integrity": "sha512-++0HQvasdo20JytyDpFvQtNrEsAgNG2CY1CLMwGXfFTKGBGQT3bOeLSYE2l1fYdvML5KUuwn9Z8L1EWe2tzs1w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.9.tgz", + "integrity": "sha512-uNIBa279Y3fkjV+2cUjx36xkx7eSjb8IvnL01eXUKXez/CBHNRw5ekCGMPM0BcmqBxBcdgUWuUXmVWwm4CH9kg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.9.tgz", + "integrity": "sha512-Mfiphvp3MjC/lctb+7D287Xw1DGzqJPb/J2aHHcHxflUo+8tmN/6d4k6I2yFR7BVo5/g7x2Monq4+Yew0EHRIA==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.9.tgz", + "integrity": "sha512-iSwByxzRe48YVkmpbgoxVzn76BXjlYFXC7NvLYq+b+kDjyyk30J0JY47DIn8z1MO3K0oSl9fZoRmZPQI4Hklzg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.9.tgz", + "integrity": "sha512-9jNJl6FqaUG+COdQMjSCGW4QiMHH88xWbvZ+kRVblZsWrkXlABuGdFJ1E9L7HK+T0Yqd4akKNa/lO0+jDxQD4Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.9.tgz", + "integrity": "sha512-RLLdkflmqRG8KanPGOU7Rpg829ZHu8nFy5Pqdi9U01VYtG9Y0zOG6Vr2z4/S+/3zIyOxiK6cCeYNWOFR9QP87g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.9.tgz", + "integrity": "sha512-YaFBlPGeDasft5IIM+CQAhJAqS3St3nJzDEgsgFixcfZeyGPCd6eJBWzke5piZuZ7CtL656eOSYKk4Ls2C0FRQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.9.tgz", + "integrity": "sha512-1MkgTCuvMGWuqVtAvkpkXFmtL8XhWy+j4jaSO2wxfJtilVCi0ZE37b8uOdMItIHz4I6z1bWWtEX4CJwcKYLcuA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.9.tgz", + "integrity": "sha512-4Xd0xNiMVXKh6Fa7HEJQbrpP3m3DDn43jKxMjxLLRjWnRsfxjORYJlXPO4JNcXtOyfajXorRKY9NkOpTHptErg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.9.tgz", + "integrity": "sha512-WjH4s6hzo00nNezhp3wFIAfmGZ8U7KtrJNlFMRKxiI9mxEK1scOMAaa9i4crUtu+tBr+0IN6JCuAcSBJZfnphw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.9.tgz", + "integrity": "sha512-mGFrVJHmZiRqmP8xFOc6b84/7xa5y5YvR1x8djzXpJBSv/UsNK6aqec+6JDjConTgvvQefdGhFDAs2DLAds6gQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.9.tgz", + "integrity": "sha512-b33gLVU2k11nVx1OhX3C8QQP6UHQK4ZtN56oFWvVXvz2VkDoe6fbG8TOgHFxEvqeqohmRnIHe5A1+HADk4OQww==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.9.tgz", + "integrity": "sha512-PPOl1mi6lpLNQxnGoyAfschAodRFYXJ+9fs6WHXz7CSWKbOqiMZsubC+BQsVKuul+3vKLuwTHsS2c2y9EoKwxQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, "node_modules/@eslint-community/eslint-utils": { "version": "4.7.0", "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.7.0.tgz", @@ -1741,6 +2187,386 @@ "dev": true, "license": "BSD-3-Clause" }, + "node_modules/@img/sharp-darwin-arm64": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.33.5.tgz", + "integrity": "sha512-UT4p+iz/2H4twwAoLCqfA9UH5pI6DggwKEGuaPy7nCVQ8ZsiY5PIcrRvD1DzuY3qYL07NtIQcWnBSY/heikIFQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-arm64": "1.0.4" + } + }, + "node_modules/@img/sharp-darwin-x64": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.33.5.tgz", + "integrity": "sha512-fyHac4jIc1ANYGRDxtiqelIbdWkIuQaI84Mv45KvGRRxSAa7o7d1ZKAOBaYbnepLC1WqxfpimdeWfvqqSGwR2Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-x64": "1.0.4" + } + }, + "node_modules/@img/sharp-libvips-darwin-arm64": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.0.4.tgz", + "integrity": "sha512-XblONe153h0O2zuFfTAbQYAX2JhYmDHeWikp1LM9Hul9gVPjFY427k6dFEcOL72O01QxQsWi761svJ/ev9xEDg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-darwin-x64": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.0.4.tgz", + "integrity": "sha512-xnGR8YuZYfJGmWPvmlunFaWJsb9T/AO2ykoP3Fz/0X5XV2aoYBPkX6xqCQvUTKKiLddarLaxpzNe+b1hjeWHAQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.0.5.tgz", + "integrity": "sha512-gvcC4ACAOPRNATg/ov8/MnbxFDJqf/pDePbBnuBDcjsI8PssmjoKMAz4LtLaVi+OnSb5FK/yIOamqDwGmXW32g==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm64": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.0.4.tgz", + "integrity": "sha512-9B+taZ8DlyyqzZQnoeIvDVR/2F4EbMepXMc/NdVbkzsJbzkUjhXv/70GQJ7tdLA4YJgNP25zukcxpX2/SueNrA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-s390x": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.0.4.tgz", + "integrity": "sha512-u7Wz6ntiSSgGSGcjZ55im6uvTrOxSIS8/dgoVMoiGE9I6JAfU50yH5BoDlYA1tcuGS7g/QNtetJnxA6QEsCVTA==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-x64": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.0.4.tgz", + "integrity": "sha512-MmWmQ3iPFZr0Iev+BAgVMb3ZyC4KeFc3jFxnNbEPas60e1cIfevbtuyf9nDGIzOaW9PdnDciJm+wFFaTlj5xYw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-arm64": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.0.4.tgz", + "integrity": "sha512-9Ti+BbTYDcsbp4wfYib8Ctm1ilkugkA/uscUn6UXK1ldpC1JjiXbLfFZtRlBhjPZ5o1NCLiDbg8fhUPKStHoTA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-x64": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.0.4.tgz", + "integrity": "sha512-viYN1KX9m+/hGkJtvYYp+CCLgnJXwiQB39damAO7WMdKWlIhmYTfHjwSbQeUK/20vY154mwezd9HflVFM1wVSw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-linux-arm": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.33.5.tgz", + "integrity": "sha512-JTS1eldqZbJxjvKaAkxhZmBqPRGmxgu+qFKSInv8moZ2AmT5Yib3EQ1c6gp493HvrvV8QgdOXdyaIBrhvFhBMQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm": "1.0.5" + } + }, + "node_modules/@img/sharp-linux-arm64": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.33.5.tgz", + "integrity": "sha512-JMVv+AMRyGOHtO1RFBiJy/MBsgz0x4AWrT6QoEVVTyh1E39TrCUpTRI7mx9VksGX4awWASxqCYLCV4wBZHAYxA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm64": "1.0.4" + } + }, + "node_modules/@img/sharp-linux-s390x": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.33.5.tgz", + "integrity": "sha512-y/5PCd+mP4CA/sPDKl2961b+C9d+vPAveS33s6Z3zfASk2j5upL6fXVPZi7ztePZ5CuH+1kW8JtvxgbuXHRa4Q==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-s390x": "1.0.4" + } + }, + "node_modules/@img/sharp-linux-x64": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.33.5.tgz", + "integrity": "sha512-opC+Ok5pRNAzuvq1AG0ar+1owsu842/Ab+4qvU879ippJBHvyY5n2mxF1izXqkPYlGuP/M556uh53jRLJmzTWA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-x64": "1.0.4" + } + }, + "node_modules/@img/sharp-linuxmusl-arm64": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.33.5.tgz", + "integrity": "sha512-XrHMZwGQGvJg2V/oRSUfSAfjfPxO+4DkiRh6p2AFjLQztWUuY/o8Mq0eMQVIY7HJ1CDQUJlxGGZRw1a5bqmd1g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-arm64": "1.0.4" + } + }, + "node_modules/@img/sharp-linuxmusl-x64": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.33.5.tgz", + "integrity": "sha512-WT+d/cgqKkkKySYmqoZ8y3pxx7lx9vVejxW/W4DOFMYVSkErR+w7mf2u8m/y4+xHe7yY9DAXQMWQhpnMuFfScw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-x64": "1.0.4" + } + }, + "node_modules/@img/sharp-wasm32": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.33.5.tgz", + "integrity": "sha512-ykUW4LVGaMcU9lu9thv85CbRMAwfeadCJHRsg2GmeRa/cJxsVY9Rbd57JcMxBkKHag5U/x7TSBpScF4U8ElVzg==", + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT", + "optional": true, + "dependencies": { + "@emnapi/runtime": "^1.2.0" + }, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-ia32": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.33.5.tgz", + "integrity": "sha512-T36PblLaTwuVJ/zw/LaH0PdZkRz5rd3SmMHX8GSmR7vtNSP5Z6bQkExdSK7xGWyxLw4sUknBuugTelgw2faBbQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-x64": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.33.5.tgz", + "integrity": "sha512-MpY/o8/8kj+EcnxwvrP4aTJSWw/aZ7JIGR4aBeZkZw5B7/Jn+tY9/VNwtcoGmdT7GfggGIU4kygOMSbYnOrAbg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, "node_modules/@isaacs/cliui": { "version": "8.0.2", "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", @@ -2062,6 +2888,22 @@ "node": ">=14" } }, + "node_modules/@playwright/test": { + "version": "1.55.0", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.55.0.tgz", + "integrity": "sha512-04IXzPwHrW69XusN/SIdDdKZBzMfOT9UNT/YiJit/xpy2VuAoB8NHc8Aplb96zsWDddLnbkPL3TsmrS04ZU2xQ==", + "devOptional": true, + "license": "Apache-2.0", + "dependencies": { + "playwright": "1.55.0" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/@rollup/plugin-babel": { "version": "5.3.1", "resolved": "https://registry.npmjs.org/@rollup/plugin-babel/-/plugin-babel-5.3.1.tgz", @@ -3767,6 +4609,20 @@ "integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==", "license": "MIT" }, + "node_modules/color": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/color/-/color-4.2.3.tgz", + "integrity": "sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1", + "color-string": "^1.9.0" + }, + "engines": { + "node": ">=12.5.0" + } + }, "node_modules/color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", @@ -3787,6 +4643,17 @@ "dev": true, "license": "MIT" }, + "node_modules/color-string": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/color-string/-/color-string-1.9.1.tgz", + "integrity": "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "^1.0.0", + "simple-swizzle": "^0.2.2" + } + }, "node_modules/commander": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", @@ -4117,6 +4984,16 @@ "rimraf": "bin.js" } }, + "node_modules/detect-libc": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.4.tgz", + "integrity": "sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, "node_modules/didyoumean": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz", @@ -4156,6 +5033,19 @@ "node": ">=6.0.0" } }, + "node_modules/dotenv": { + "version": "16.6.1", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz", + "integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, "node_modules/dunder-proto": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", @@ -4406,6 +5296,48 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/esbuild": { + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.9.tgz", + "integrity": "sha512-CRbODhYyQx3qp7ZEwzxOk4JBqmD/seJrzPa/cGjY1VtIn5E09Oi9/dB4JwctnfZ8Q8iT7rioVv5k/FNT/uf54g==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.25.9", + "@esbuild/android-arm": "0.25.9", + "@esbuild/android-arm64": "0.25.9", + "@esbuild/android-x64": "0.25.9", + "@esbuild/darwin-arm64": "0.25.9", + "@esbuild/darwin-x64": "0.25.9", + "@esbuild/freebsd-arm64": "0.25.9", + "@esbuild/freebsd-x64": "0.25.9", + "@esbuild/linux-arm": "0.25.9", + "@esbuild/linux-arm64": "0.25.9", + "@esbuild/linux-ia32": "0.25.9", + "@esbuild/linux-loong64": "0.25.9", + "@esbuild/linux-mips64el": "0.25.9", + "@esbuild/linux-ppc64": "0.25.9", + "@esbuild/linux-riscv64": "0.25.9", + "@esbuild/linux-s390x": "0.25.9", + "@esbuild/linux-x64": "0.25.9", + "@esbuild/netbsd-arm64": "0.25.9", + "@esbuild/netbsd-x64": "0.25.9", + "@esbuild/openbsd-arm64": "0.25.9", + "@esbuild/openbsd-x64": "0.25.9", + "@esbuild/openharmony-arm64": "0.25.9", + "@esbuild/sunos-x64": "0.25.9", + "@esbuild/win32-arm64": "0.25.9", + "@esbuild/win32-ia32": "0.25.9", + "@esbuild/win32-x64": "0.25.9" + } + }, "node_modules/escalade": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", @@ -5681,6 +6613,13 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-arrayish": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.2.tgz", + "integrity": "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==", + "dev": true, + "license": "MIT" + }, "node_modules/is-async-function": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-async-function/-/is-async-function-2.1.1.tgz", @@ -7287,6 +8226,53 @@ "node": ">=8" } }, + "node_modules/playwright": { + "version": "1.55.0", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.55.0.tgz", + "integrity": "sha512-sdCWStblvV1YU909Xqx0DhOjPZE4/5lJsIS84IfN9dAZfcl/CIZ5O8l3o0j7hPMjDvqoTF8ZUcc+i/GL5erstA==", + "devOptional": true, + "license": "Apache-2.0", + "dependencies": { + "playwright-core": "1.55.0" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "fsevents": "2.3.2" + } + }, + "node_modules/playwright-core": { + "version": "1.55.0", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.55.0.tgz", + "integrity": "sha512-GvZs4vU3U5ro2nZpeiwyb0zuFaqb9sUiAJuyrWpcGouD8y9/HLgGbNRjIph7zU9D3hnPaisMl9zG9CgFi/biIg==", + "devOptional": true, + "license": "Apache-2.0", + "bin": { + "playwright-core": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/playwright/node_modules/fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, "node_modules/possible-typed-array-names": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz", @@ -8036,6 +9022,46 @@ "node": ">= 0.4" } }, + "node_modules/sharp": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.33.5.tgz", + "integrity": "sha512-haPVm1EkS9pgvHrQ/F3Xy+hgcuMV0Wm9vfIBSiwZ05k+xgb0PkBQpGsAA/oWdDobNaZTH5ppvHtzCFbnSEwHVw==", + "dev": true, + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "color": "^4.2.3", + "detect-libc": "^2.0.3", + "semver": "^7.6.3" + }, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-darwin-arm64": "0.33.5", + "@img/sharp-darwin-x64": "0.33.5", + "@img/sharp-libvips-darwin-arm64": "1.0.4", + "@img/sharp-libvips-darwin-x64": "1.0.4", + "@img/sharp-libvips-linux-arm": "1.0.5", + "@img/sharp-libvips-linux-arm64": "1.0.4", + "@img/sharp-libvips-linux-s390x": "1.0.4", + "@img/sharp-libvips-linux-x64": "1.0.4", + "@img/sharp-libvips-linuxmusl-arm64": "1.0.4", + "@img/sharp-libvips-linuxmusl-x64": "1.0.4", + "@img/sharp-linux-arm": "0.33.5", + "@img/sharp-linux-arm64": "0.33.5", + "@img/sharp-linux-s390x": "0.33.5", + "@img/sharp-linux-x64": "0.33.5", + "@img/sharp-linuxmusl-arm64": "0.33.5", + "@img/sharp-linuxmusl-x64": "0.33.5", + "@img/sharp-wasm32": "0.33.5", + "@img/sharp-win32-ia32": "0.33.5", + "@img/sharp-win32-x64": "0.33.5" + } + }, "node_modules/shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", @@ -8144,6 +9170,16 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/simple-swizzle": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.2.tgz", + "integrity": "sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-arrayish": "^0.3.1" + } + }, "node_modules/slash": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", @@ -8929,6 +9965,26 @@ "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", "license": "0BSD" }, + "node_modules/tsx": { + "version": "4.20.5", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.20.5.tgz", + "integrity": "sha512-+wKjMNU9w/EaQayHXb7WA7ZaHY6hN8WgfvHNQ3t1PnU91/7O8TcTnIhCDYTZwnt8JsO9IBqZ30Ln1r7pPF52Aw==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "~0.25.0", + "get-tsconfig": "^4.7.5" + }, + "bin": { + "tsx": "dist/cli.mjs" + }, + "engines": { + "node": ">=18.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + } + }, "node_modules/type-check": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", diff --git a/package.json b/package.json index f9297c7..2eb23cb 100644 --- a/package.json +++ b/package.json @@ -11,23 +11,29 @@ "setup": "node scripts/setup.js", "status": "node scripts/check-status.js", "generate-icons": "node scripts/generate-icons.js", - "create-screenshots": "node scripts/create-screenshots.js" + "create-screenshots": "node scripts/create-screenshots.js", + "indexnow:ping": "tsx scripts/ping-indexnow.ts", + "indexnow:test": "tsx scripts/ping-indexnow.ts --test", + "indexnow:all": "tsx scripts/ping-indexnow.ts --all", + "indexnow:status": "tsx scripts/ping-indexnow.ts --status", + "test:e2e": "playwright test", + "test:e2e:ui": "playwright test --ui" }, "dependencies": { + "framer-motion": "^10.16.16", + "idb": "^8.0.0", + "lucide-react": "^0.294.0", "next": "^14.0.4", + "next-pwa": "^5.6.0", + "next-themes": "^0.2.1", "react": "^18.2.0", "react-dom": "^18.2.0", - "lucide-react": "^0.294.0", - "framer-motion": "^10.16.16", - "next-themes": "^0.2.1", - "next-pwa": "^5.6.0", - "zustand": "^4.4.7", - "zod": "^3.22.4", "uuid": "^9.0.1", - "idb": "^8.0.0" + "zod": "^3.22.4", + "zustand": "^4.4.7" }, - "devDependencies": { + "@playwright/test": "^1.55.0", "@types/node": "^20.10.5", "@types/react": "^18.2.45", "@types/react-dom": "^18.2.18", @@ -35,12 +41,14 @@ "@typescript-eslint/eslint-plugin": "^6.16.0", "@typescript-eslint/parser": "^6.16.0", "autoprefixer": "^10.4.16", + "dotenv": "^16.6.1", "eslint": "^8.56.0", "eslint-config-next": "^14.0.4", "postcss": "^8.4.32", "prettier": "^3.1.1", "sharp": "^0.33.2", "tailwindcss": "^3.4.0", + "tsx": "^4.20.5", "typescript": "^5.3.3" } } diff --git a/playwright.config.ts b/playwright.config.ts new file mode 100644 index 0000000..3533382 --- /dev/null +++ b/playwright.config.ts @@ -0,0 +1,35 @@ +import { defineConfig, devices } from '@playwright/test' + +export default defineConfig({ + testDir: './tests/e2e', + fullyParallel: true, + forbidOnly: !!process.env.CI, + retries: process.env.CI ? 2 : 0, + workers: process.env.CI ? 1 : undefined, + reporter: 'html', + use: { + baseURL: process.env.NEXT_PUBLIC_SITE_URL || 'http://localhost:3000', + trace: 'on-first-retry', + }, + + projects: [ + { + name: 'chromium', + use: { ...devices['Desktop Chrome'] }, + }, + { + name: 'firefox', + use: { ...devices['Desktop Firefox'] }, + }, + { + name: 'webkit', + use: { ...devices['Desktop Safari'] }, + }, + ], + + webServer: { + command: 'npm run build && npm run start', + url: 'http://localhost:3000', + reuseExistingServer: !process.env.CI, + }, +}) \ No newline at end of file diff --git a/public/manifest.json b/public/manifest.json index a39676f..cac5813 100644 --- a/public/manifest.json +++ b/public/manifest.json @@ -1,7 +1,7 @@ { - "name": "PassMaster - Free Offline Secure Password Generator", + "name": "PassMaster - Passwort Generator offline, DSGVO-konform", "short_name": "PassMaster", - "description": "Generate ultra-secure passwords instantly, offline with client-side encryption. 100% open-source, private, and free.", + "description": "Sichere Passwörter generieren - 100% client-seitig, offline, DSGVO-konform, open-source. Web Crypto API für maximale Sicherheit.", "id": "passmaster-pwa", "start_url": "/", "display": "standalone", @@ -9,7 +9,7 @@ "theme_color": "#3b82f6", "orientation": "portrait-primary", "scope": "/", - "lang": "en", + "lang": "de", "categories": ["security", "utilities", "productivity", "developer-tools"], "prefer_related_applications": false, "related_applications": [], @@ -68,9 +68,9 @@ ], "shortcuts": [ { - "name": "Generate Password", - "short_name": "Generate", - "description": "Quickly generate a new secure password", + "name": "Passwort generieren", + "short_name": "Generieren", + "description": "Schnell ein neues sicheres Passwort generieren", "url": "/#generator", "icons": [ { @@ -80,10 +80,22 @@ ] }, { - "name": "Password History", - "short_name": "History", - "description": "View recently generated passwords", - "url": "/#history", + "name": "Offline nutzen", + "short_name": "Offline", + "description": "Passwörter ohne Internetverbindung generieren", + "url": "/offline", + "icons": [ + { + "src": "/icons/icon-96.png", + "sizes": "96x96" + } + ] + }, + { + "name": "Ähnliche Zeichen ausschließen", + "short_name": "Lesbarkeit", + "description": "Passwörter ohne verwirrende ähnliche Zeichen", + "url": "/exclude-similar", "icons": [ { "src": "/icons/icon-96.png", diff --git a/scripts/ping-indexnow.ts b/scripts/ping-indexnow.ts new file mode 100644 index 0000000..a9bdebf --- /dev/null +++ b/scripts/ping-indexnow.ts @@ -0,0 +1,223 @@ +#!/usr/bin/env tsx + +/** + * Manual IndexNow ping script for PassMaster + * Usage: npx tsx scripts/ping-indexnow.ts [urls...] + * Example: npx tsx scripts/ping-indexnow.ts https://passmaster.app/offline https://passmaster.app/client-side + */ + +import { config } from 'dotenv' +import { writeFileSync, existsSync, mkdirSync } from 'fs' +import { join } from 'path' + +// Load environment variables +config({ path: '.env.local' }) +config({ path: '.env' }) + +interface IndexNowRequest { + host: string + key: string + keyLocation: string + urlList: string[] +} + +class IndexNowPinger { + private readonly key: string + private readonly host: string + private readonly enabled: boolean + + constructor() { + this.key = process.env.INDEXNOW_KEY || '' + this.host = process.env.SITE_HOST || 'passmaster.app' + this.enabled = Boolean(this.key) + + if (!this.enabled) { + throw new Error('INDEXNOW_KEY is required in environment variables') + } + + this.ensureKeyFile() + } + + private ensureKeyFile() { + try { + const publicDir = join(process.cwd(), 'public') + if (!existsSync(publicDir)) { + mkdirSync(publicDir, { recursive: true }) + } + + const keyFilePath = join(publicDir, `${this.key}.txt`) + if (!existsSync(keyFilePath)) { + writeFileSync(keyFilePath, this.key) + console.log(`✅ Created key file: public/${this.key}.txt`) + } + } catch (error) { + console.error('❌ Failed to create key file:', error) + throw error + } + } + + private validateUrls(urls: string[]): string[] { + return urls.filter(url => { + try { + const parsed = new URL(url) + if (parsed.hostname !== this.host) { + console.warn(`⚠️ Skipping URL with wrong hostname: ${url} (expected: ${this.host})`) + return false + } + if (parsed.protocol !== 'https:') { + console.warn(`⚠️ Skipping non-HTTPS URL: ${url}`) + return false + } + return true + } catch { + console.warn(`⚠️ Skipping invalid URL: ${url}`) + return false + } + }) + } + + async ping(urls: string[]): Promise { + const validUrls = this.validateUrls(urls) + + if (validUrls.length === 0) { + console.error('❌ No valid URLs to ping') + return false + } + + if (validUrls.length > 10000) { + console.warn(`⚠️ Too many URLs (${validUrls.length}), limiting to 10,000`) + validUrls.splice(10000) + } + + const payload: IndexNowRequest = { + host: this.host, + key: this.key, + keyLocation: `https://${this.host}/${this.key}.txt`, + urlList: validUrls + } + + console.log(`🚀 Pinging IndexNow with ${validUrls.length} URLs...`) + console.log(' URLs:', validUrls.join(', ')) + + try { + const response = await fetch('https://api.indexnow.org/indexnow', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'User-Agent': 'PassMaster IndexNow Manual Client' + }, + body: JSON.stringify(payload) + }) + + console.log(`📡 Response status: ${response.status} ${response.statusText}`) + + if (response.ok || response.status === 202) { + console.log('✅ IndexNow ping successful!') + return true + } else { + const errorText = await response.text().catch(() => 'Unknown error') + console.error('❌ IndexNow ping failed:', errorText) + return false + } + } catch (error) { + console.error('❌ Network error:', error) + return false + } + } + + getStatus() { + return { + enabled: this.enabled, + key: this.key ? `${this.key.substring(0, 8)}...` : 'Not set', + host: this.host, + keyFileUrl: `https://${this.host}/${this.key}.txt` + } + } +} + +// CLI Interface +async function main() { + console.log('🔧 PassMaster IndexNow Manual Ping Tool\n') + + try { + const pinger = new IndexNowPinger() + const status = pinger.getStatus() + + console.log('📊 Configuration:') + console.log(` Host: ${status.host}`) + console.log(` Key: ${status.key}`) + console.log(` Key File: ${status.keyFileUrl}`) + console.log(` Enabled: ${status.enabled}\n`) + + const args = process.argv.slice(2) + + if (args.length === 0) { + console.log('ℹ️ Usage: npx tsx scripts/ping-indexnow.ts [urls...]') + console.log(' Example: npx tsx scripts/ping-indexnow.ts https://passmaster.app/offline') + console.log('\n🎯 Common URLs to ping:') + console.log(' • https://passmaster.app/') + console.log(' • https://passmaster.app/offline') + console.log(' • https://passmaster.app/client-side') + console.log(' • https://passmaster.app/exclude-similar') + console.log(' • https://passmaster.app/privacy') + process.exit(0) + } + + // Special commands + if (args[0] === '--all') { + const allUrls = [ + `https://${status.host}/`, + `https://${status.host}/offline`, + `https://${status.host}/client-side`, + `https://${status.host}/exclude-similar`, + `https://${status.host}/privacy` + ] + + console.log('🎯 Pinging all main pages...') + const success = await pinger.ping(allUrls) + process.exit(success ? 0 : 1) + } + + if (args[0] === '--test') { + console.log('🧪 Testing with homepage only...') + const testUrl = `https://${status.host}/` + const success = await pinger.ping([testUrl]) + process.exit(success ? 0 : 1) + } + + if (args[0] === '--help' || args[0] === '-h') { + console.log('📖 IndexNow Ping Commands:') + console.log(' --all Ping all main pages') + console.log(' --test Test with homepage only') + console.log(' --status Show current configuration') + console.log(' [urls...] Ping specific URLs') + process.exit(0) + } + + if (args[0] === '--status') { + console.log('✅ Configuration looks good!') + process.exit(0) + } + + // Ping provided URLs + const success = await pinger.ping(args) + process.exit(success ? 0 : 1) + + } catch (error) { + console.error('💥 Fatal error:', error instanceof Error ? error.message : error) + process.exit(1) + } +} + +// Handle SIGINT gracefully +process.on('SIGINT', () => { + console.log('\n👋 Interrupted by user') + process.exit(130) +}) + +// Run main function +if (require.main === module) { + main() +} + +export { IndexNowPinger } \ No newline at end of file diff --git a/src/app/api/indexnow/route.ts b/src/app/api/indexnow/route.ts new file mode 100644 index 0000000..6a2308d --- /dev/null +++ b/src/app/api/indexnow/route.ts @@ -0,0 +1,99 @@ +import { NextRequest, NextResponse } from 'next/server' +import { queueIndexNowPing, getIndexNowStatus } from '@/lib/indexnow' + +// POST /api/indexnow - Queue URLs for IndexNow pinging +export async function POST(request: NextRequest) { + try { + const body = await request.json() + const { urls } = body + + if (!urls || !Array.isArray(urls)) { + return NextResponse.json( + { error: 'URLs array is required' }, + { status: 400 } + ) + } + + if (urls.length === 0) { + return NextResponse.json( + { error: 'At least one URL is required' }, + { status: 400 } + ) + } + + // Validate URLs belong to this domain + const siteHost = process.env.SITE_HOST || 'passmaster.app' + const validUrls = urls.filter(url => { + try { + const parsed = new URL(url) + return parsed.hostname === siteHost && parsed.protocol === 'https:' + } catch { + return false + } + }) + + if (validUrls.length === 0) { + return NextResponse.json( + { error: 'No valid URLs found' }, + { status: 400 } + ) + } + + const success = await queueIndexNowPing(validUrls) + + return NextResponse.json({ + success, + queued: validUrls.length, + urls: validUrls + }) + } catch (error) { + console.error('IndexNow API error:', error) + return NextResponse.json( + { error: 'Internal server error' }, + { status: 500 } + ) + } +} + +// GET /api/indexnow - Get IndexNow status +export async function GET() { + try { + const status = await getIndexNowStatus() + + return NextResponse.json(status) + } catch (error) { + console.error('IndexNow status error:', error) + return NextResponse.json( + { error: 'Internal server error' }, + { status: 500 } + ) + } +} + +// Rate limiting helper +const rateLimit = new Map() + +function checkRateLimit(ip: string): boolean { + const now = Date.now() + const windowMs = 60 * 1000 // 1 minute + const maxRequests = 10 + + if (!rateLimit.has(ip)) { + rateLimit.set(ip, { count: 1, resetTime: now + windowMs }) + return true + } + + const limitInfo = rateLimit.get(ip) + + if (now > limitInfo.resetTime) { + rateLimit.set(ip, { count: 1, resetTime: now + windowMs }) + return true + } + + if (limitInfo.count >= maxRequests) { + return false + } + + limitInfo.count++ + return true +} \ No newline at end of file diff --git a/src/app/client-side/page.tsx b/src/app/client-side/page.tsx new file mode 100644 index 0000000..3aa03d4 --- /dev/null +++ b/src/app/client-side/page.tsx @@ -0,0 +1,441 @@ +"use client" + +import { motion } from 'framer-motion' +import { + Shield, + Lock, + Server, + Eye, + Code, + CheckCircle, + XCircle, + ArrowRight, + GitBranch, + Database, + Network +} from 'lucide-react' +import type { Metadata } from 'next' + +export const metadata: Metadata = { + title: 'Client-side Passwort Generator | PassMaster', + description: '100% code-auditierbar, local-only. Keine Cloud, volle Transparenz. Web Crypto API, kein Math.random, DSGVO-konform.', + keywords: ['client-side password generator', 'Web Crypto API', 'local-only', 'Math.random', 'DSGVO', 'auditierbar', 'transparenz'], + openGraph: { + title: 'Client-side Passwort Generator | PassMaster', + description: '100% code-auditierbar, local-only. Keine Cloud, volle Transparenz.', + }, +} + +export default function ClientSidePage() { + const securityFeatures = [ + { + icon: Lock, + title: "Web Crypto API", + description: "Verwendet window.crypto.getRandomValues() für kryptographisch sichere Zufallszahlen. NIST SP 800-63B konform.", + status: "secure" + }, + { + icon: XCircle, + title: "Kein Math.random()", + description: "Niemals unsichere Math.random() Funktion. Nur hardwarebasierte Zufallsgeneratoren.", + status: "secure" + }, + { + icon: Shield, + title: "Keine Server-Kommunikation", + description: "Zero externe Requests während der Passwort-Generierung. 100% lokale Verarbeitung.", + status: "secure" + }, + { + icon: Code, + title: "Open Source Audit", + description: "Vollständig auditierbar auf GitHub. Jede Zeile Code ist öffentlich einsehbar.", + status: "secure" + } + ] + + const dataFlow = [ + { + step: 1, + title: "Eingabe im Browser", + description: "Benutzer wählt Passwort-Parameter (Länge, Zeichensätze)", + location: "🌐 Ihr Browser" + }, + { + step: 2, + title: "Lokale Verarbeitung", + description: "Web Crypto API generiert sichere Zufallswerte", + location: "💻 Lokale Hardware" + }, + { + step: 3, + title: "Passwort-Aufbau", + description: "Zeichensätze werden basierend auf Zufallswerten zusammengesetzt", + location: "🔒 Browser-Speicher" + }, + { + step: 4, + title: "Anzeige & Kopieren", + description: "Fertiges Passwort wird angezeigt, optional in Zwischenablage", + location: "📋 Lokale Zwischenablage" + } + ] + + const comparisonData = [ + { + feature: "Datenübertragung", + clientSide: "Keine", + serverSide: "Vollständige Passwort-Daten", + icon: Network + }, + { + feature: "Zufallsqualität", + clientSide: "Hardware-RNG (Web Crypto)", + serverSide: "Unbekannt/Math.random", + icon: Shield + }, + { + feature: "Auditierbarkeit", + clientSide: "100% Open Source", + serverSide: "Server-Code verborgen", + icon: Eye + }, + { + feature: "DSGVO-Konformität", + clientSide: "Vollständig konform", + serverSide: "Abhängig vom Anbieter", + icon: CheckCircle + } + ] + + return ( +
+
+ {/* Header */} + +
+
+ +
+
+ +

+ Client-seitige Passwort-Generierung +

+ +

+ 100% transparente, auditierbare Sicherheit. Ihre Passwörter verlassen niemals Ihren Browser. +

+ + + + Code auf GitHub prüfen + +
+ + {/* Security Features */} + +

+ Web Crypto API | Datenfluss | DSGVO-Fakten +

+ +
+ {securityFeatures.map((feature, index) => ( + +
+
+ +
+
+

+ {feature.title} +

+

+ {feature.description} +

+
+ ))} +
+
+ + {/* Data Flow */} + +

+ Datenfluss-Diagramm: Was passiert im Browser? +

+ +
+ {dataFlow.map((step, index) => ( + +
+ {step.step} +
+ +
+
+
+

+ {step.title} +

+

+ {step.description} +

+
+
+
+ {step.location} +
+
+
+
+ + {index < dataFlow.length - 1 && ( + + )} +
+ ))} +
+
+ + {/* Comparison Table */} + +

+ Client-seitig vs. Server-basiert +

+ +
+
+ + + + + + + + + + {comparisonData.map((row, index) => ( + + + + + + ))} + +
+ Sicherheitsaspekt + + Client-seitig (PassMaster) + + Server-basiert +
+
+ + + {row.feature} + +
+
+
+ + + {row.clientSide} + +
+
+
+ + + {row.serverSide} + +
+
+
+
+
+ + {/* Technical Details */} + +
+

+ Technische Sicherheitsdetails +

+ +
+
+

+ Kryptographische Standards +

+
    +
  • + + NIST SP 800-63B konform +
  • +
  • + + BSI-konforme Zufallsgenerierung +
  • +
  • + + Hardware-basierte Entropie +
  • +
+
+ +
+

+ Datenschutz & DSGVO +

+
    +
  • + + Keine Datenverarbeitung auf Servern +
  • +
  • + + Keine Cookies oder Tracking +
  • +
  • + + Vollständige Datenhoheit +
  • +
+
+
+
+
+ + {/* FAQ */} + +

+ Häufige Fragen zur Client-seitigen Sicherheit +

+ +
+
+

+ Was passiert im Browser? +

+

+ Alle Passwort-Generierungsprozesse laufen ausschließlich in JavaScript in Ihrem Browser. + Die Web Crypto API stellt sichere Zufallszahlen bereit, die nie das Gerät verlassen. +

+
+ +
+

+ Wie kann ich den Datenfluss überprüfen? +

+

+ Öffnen Sie die Browser-Entwicklertools (F12), gehen Sie zum "Network" Tab und + generieren Sie ein Passwort. Sie werden sehen: Keine Netzwerk-Requests während der Generierung. +

+
+ +
+

+ Wie kann man den Code auditieren? +

+

+ Der gesamte Quellcode ist auf GitHub verfügbar. Jede Zeile kann überprüft werden. + Es gibt keine verschleierten oder minimifizierten Teile in der Passwort-Generierungslogik. +

+
+
+
+ + {/* Disclaimer */} + +
+ +
+

+ Sicherheitshinweis +

+

+ Es werden keine Daten übertragen oder gespeichert. Optional Local-only Mode für maximale Sicherheit. + Diese Implementierung entspricht BSI- und NIST-Standards für Passwort-Generierung. +

+
+
+
+ + {/* JSON-LD */} +