Files
Momento/memento-note/app/(public)/privacy/page.tsx
Antigravity a7c15e9fc3
All checks were successful
CI / Lint, Unit Tests & Build (push) Successful in 5m49s
CI / Deploy production (on server) (push) Successful in 24s
feat(privacy+extension): publier la page /privacy et préparer les builds Chrome/Firefox Store
- Nouvelle page /privacy bilingue (FR/EN) avec sélection par Accept-Language
  ou ?lang=, sections complètes (données collectées, IA/BYOK, RGPD, sécurité,
  contact). Style sombre aligné sur la landing (x.ai premium).
- Liens footer landing.footer.legal.* mis à jour vers /privacy dans les 15 locales.
- Extension Web Clipper rendue cross-browser :
  * background.js détecte chrome.sidePanel vs browser.sidebarAction
  * sidepanel.js ajoute un shim chrome<->browser pour Firefox
  * manifest.json inclut sidebar_action + browser_specific_settings.gecko
    + optional_host_permissions (Chrome les ignore)
- Script scripts/build-firefox.mjs pour produire un .xpi signé pour AMO,
  symétrique de build-chrome-store.mjs. Exclusion mutuelle des artefacts
  Chrome/Firefox pour éviter la cross-contamination.
- Branding corrigé : 176 occurrences de 'Momento' -> 'Memento' dans le
  bundle de traductions embarqué.
- Version bumpée 0.3.1 -> 0.4.0.
2026-07-18 09:00:44 +00:00

399 lines
24 KiB
TypeScript

import { headers } from 'next/headers'
import Link from 'next/link'
import { parseAcceptLanguage } from '@/lib/i18n/detect-user-language'
export const metadata = {
title: 'Privacy Policy — Memento',
description: 'How Memento handles your data, the Chrome and Firefox web clipper extension, and AI features.',
alternates: { canonical: 'https://memento-note.com/privacy' },
robots: { index: true, follow: true },
}
type Locale = 'en' | 'fr'
const SUPPORTED: Locale[] = ['en', 'fr']
const RTL: Locale[] = []
async function pickLocale(searchLang?: string | string[]): Promise<Locale> {
// 1. Explicit ?lang= override
const raw = Array.isArray(searchLang) ? searchLang[0] : searchLang
if (raw && SUPPORTED.includes(raw.toLowerCase() as Locale)) return raw.toLowerCase() as Locale
// 2. Accept-Language header
try {
const h = await headers()
const accept = parseAcceptLanguage(h.get('accept-language'))
const first = accept?.[0]?.toLowerCase()
if (first && SUPPORTED.includes(first as Locale)) return first as Locale
if (first && first.startsWith('fr')) return 'fr'
} catch {
/* fall through */
}
return 'en'
}
type Section = { id: string; title: string; body: string[] }
type Doc = {
title: string
intro: string
lastUpdated: string
sections: Section[]
}
const DOCS: Record<Locale, Doc> = {
fr: {
title: 'Politique de confidentialité',
intro:
'Cette politique décrit comment Memento (Memento Labs) collecte, utilise et protège vos données, y compris via l\'application web, l\'extension navigateur Web Clipper et les fonctionnalités d\'intelligence artificielle. Nous concevons Memento comme un Second Brain personnel : par défaut, vos données vous appartiennent et ne sont pas partagées.',
lastUpdated: 'Dernière mise à jour : juillet 2026',
sections: [
{
id: 'data-collected',
title: '1. Données collectées',
body: [
'Compte et authentification : adresse e-mail, nom facultatif, mot de passe haché (bcrypt), ou identifiant Google via OAuth (Google Auth Platform).',
'Carnets et notes : contenu, titre, étiquettes, images jointes, pièces jointes (PDF, images, documents), métadonnées (dates, ordre, couleur, épinglé, archivé), et historique des versions (snapshots).',
'Rappels et notifications : date/heure, récurrence, lieu facultatif, état (fait/non fait).',
'Données IA : titre et corps des notes envoyés à votre fournisseur d\'IA configuré pour générer résumés, tags, embeddings, Memory Echo, reformulations, traduction, suggestions. Avec BYOK (votre propre clé), l\'envoi va directement vers votre fournisseur ; sinon, il transite par notre routeur IA mutualisé.',
'Extension Web Clipper (Chrome / Firefox) : lorsque vous cliquez explicitement « Clipper », l\'extension lit l\'URL active, le titre de la page, la sélection de texte et (selon votre choix) le HTML complet de la page. Aucune lecture du contenu de page n\'a lieu tant que vous n\'initiez pas l\'action.',
'Cookies techniques et de session : strictement nécessaires à l\'authentification (cookies NextAuth) et aux préférences (langue, thème, panneau de configuration de l\'extension). Aucun cookie publicitaire, aucun tracker tiers par défaut.',
'Facturation et quotas : pour les tiers payants, référence de commande et jetons d\'utilisation IA stockés pour appliquer les quotas (réservation atomique). Aucune donnée bancaire (traitée par Stripe).',
'Métadonnées techniques : adresse IP (journalisée ~30 jours à des fins anti-abus), user-agent, version de l\'extension, codes d\'erreur anonymisés.',
],
},
{
id: 'usage',
title: '2. Usage des données',
body: [
'Fournir le service : synchronisation de vos carnets, recherche, Memory Echo, Memory Echo insights, agents, publication web de notes, clips depuis l\'extension.',
'Amélioration : journaux d\'erreur anonymisés et métriques agrégées (Prometheus + dashboards Grafana internes) pour diagnostiquer les pannes et améliorer les performances.',
'Sécurité : détection d\'abus, protection contre le credential stuffing et les dépassements de quotas.',
'Nous ne vendons jamais vos données. Nous ne les partageons pas avec des annonceurs. Aucun profilage publicitaire.',
],
},
{
id: 'third-parties',
title: '3. Services tiers et sous-traitants',
body: [
'Google OAuth : authentification optionnelle (Branding → Audience « In production »).',
'Google Cloud Storage (GCP) : hébergement des fichiers joints et des pièces jointes, région Europe (eu-west-3 / europe-west9).',
'PostgreSQL (Docker self-hosted) : base principale, sauvegardes chiffrées au repos.',
'Redis (Docker self-hosted) : cache de session, rate limiting, files de tâches.',
'Fournisseurs d\'IA (OpenAI, Anthropic, Google Gemini, Mistral, OpenRouter, ou votre propre clé BYOK) : envoi du titre et du contenu de la note sélectionnée pour les fonctionnalités IA. Aucun entraînement n\'est effectué sur vos données par notre intermédiaire ; voir la politique du fournisseur retenu.',
'Stripe : facturation des abonnements. Les données de paiement sont gérées exclusivement par Stripe (PCI-DSS).',
'Resend : e-mails transactionnels (vérification de compte, réinitialisation de mot de passe, alertes quota).',
'Google Fonts (CDN fonts.googleapis.com) : polices web sur la landing page et le side panel de l\'extension — adresse IP communiquée à Google.',
'Google Favicons (google.com/s2/favicons) : récupération d\'icônes de sites dans l\'extension — l\'URL du domaine est communiquée à Google.',
'Grafana Cloud (monitoring optionnel) : métriques agrégées anonymisées.',
],
},
{
id: 'extension',
title: '4. Permissions de l\'extension navigateur',
body: [
'activeTab : lecture du contenu de l\'onglet actif uniquement lorsque vous cliquez le bouton « Clipper ». Aucune lecture passive.',
'scripting : injection d\'un petit script de capture de sélection après votre clic explicite.',
'storage : mémorisation de l\'URL de votre serveur Memento et du dernier carnet utilisé.',
'sidePanel (Chrome) / sidebar_action (Firefox) : ouverture du panneau latéral pour afficher l\'interface du clipper.',
'tabs : récupération de l\'URL et du titre de l\'onglet actif pour le contexte du clip.',
'host_permissions : limitées à memento-note.com en production. Les hôtes étendus (localhost / LAN) sont disponibles uniquement en build développeur.',
'L\'extension ne lit jamais l\'historique de navigation. Elle ne communique avec aucun serveur autre que l\'instance Memento que vous avez explicitement configurée.',
],
},
{
id: 'ai',
title: '5. Intelligence artificielle et BYOK',
body: [
'Les fonctionnalités IA (résumé, tags, Memory Echo, embeddings sémantiques, agents) consomment un quota mesuré en tokens. Le quota est réservé atomiquement avant chaque appel.',
'Vous pouvez fournir votre propre clé d\'API (BYOK) pour contourner les quotas mutualisés et faire transiter les appels directement par votre fournisseur. La clé est chiffrée au repos (AES-256-GCM) et n\'est jamais affichée en clair après saisie.',
'Aucun entraînement de modèle n\'est effectué sur vos données par notre intermédiaire.',
'Vous pouvez désactiver individuellement chaque fonctionnalité IA dans Réglages → IA.',
],
},
{
id: 'retention',
title: '6. Conservation des données',
body: [
'Carnets et notes : conservés tant que votre compte est actif. Suppression définitive dans les 30 jours suivant la suppression du compte (sauf obligation légale contraire).',
'Corbeille : notes supprimées conservées 30 jours avant purge définitive.',
'Journaux d\'erreur : 90 jours maximum, agrégés ensuite.',
'Sauvegardes base de données : conservées 30 jours en rotation chiffrée.',
'Métadonnées de facturation : conservées 10 ans (obligation comptable).',
],
},
{
id: 'rights',
title: '7. Vos droits (RGPD / CCPA)',
body: [
'Accès : export complet de vos notes, carnets, étiquettes et paramètres via Réglages → Données.',
'Rectification : édition directe dans l\'interface.',
'Effacement : suppression de compte dans Réglages → Données. Effacement définitif sous 30 jours.',
'Portabilité : export JSON des notes, carnets et étiquettes (équivalent de l\'outil `export_notes` du serveur MCP).',
'Opposition au traitement IA : désactivation des fonctionnalités IA dans Réglages → IA.',
'Réclamation : vous pouvez saisir la CNIL (France) ou l\'autorité de contrôle de votre lieu de résidence.',
'Pour exercer vos droits : privacy@memento-note.com.',
],
},
{
id: 'security',
title: '8. Sécurité',
body: [
'Transport : HTTPS obligatoire, HSTS activé, en-têtes de sécurité (CSP, X-Frame-Options, X-Content-Type-Options).',
'Stockage : mots de passe hachés bcrypt, clés API chiffrées AES-256-GCM, secrets en variables d\'environnement chiffrées au repos.',
'Authentification : cookies de session NextAuth, JWT signés, expiration glissante.',
'Audit : journaux d\'audit log pour les actions sensibles (changement de mot de passe, export de données, suppression de compte).',
'Bug bounty : security@memento-note.com pour signaler une vulnérabilité. Programme responsable de divulgation.',
],
},
{
id: 'minors',
title: '9. Mineurs',
body: [
'Memento n\'est pas destiné aux enfants de moins de 16 ans (RGPD) ou 13 ans (COPPA). Nous ne collectons pas sciemment de données auprès de mineurs. Si vous pensez qu\'un mineur a fourni des données, contactez privacy@memento-note.com pour suppression.',
],
},
{
id: 'changes',
title: '10. Modifications de cette politique',
body: [
'Toute modification substantielle est annoncée par e-mail au moins 30 jours à l\'avance et par bannière dans l\'application. La date de « dernière mise à jour » en haut de cette page est mise à jour à chaque révision.',
'Les modifications non substantielles (corrections de formulation, liens mis à jour) sont publiées sans préavis. La version précédente reste accessible sur demande.',
],
},
{
id: 'contact',
title: '11. Contact et responsable de traitement',
body: [
'Memento Labs — Responsable de traitement : privacy@memento-note.com',
'Délégué à la protection des données (DPO) : dpo@memento-note.com',
'Adresse postale : Memento Labs, [adresse], France',
'Hébergeur des données : serveur self-hosted sur infrastructure Memento Labs (PostgreSQL + Redis Docker, région Europe).',
],
},
],
},
en: {
title: 'Privacy Policy',
intro:
'This policy describes how Memento (Memento Labs) collects, uses and protects your data, including through the web app, the Web Clipper browser extension, and AI features. We design Memento as a personal Second Brain: by default your data belongs to you and is not shared.',
lastUpdated: 'Last updated: July 2026',
sections: [
{
id: 'data-collected',
title: '1. Data we collect',
body: [
'Account and authentication: email address, optional name, hashed password (bcrypt), or Google identifier via OAuth (Google Auth Platform).',
'Notebooks and notes: content, title, labels, attached images and files (PDF, images, documents), metadata (dates, order, color, pinned, archived), and version history (snapshots).',
'Reminders and notifications: date/time, recurrence, optional location, status (done/not done).',
'AI data: note title and body sent to your configured AI provider to generate summaries, tags, embeddings, Memory Echo, reformulations, translations, suggestions. With BYOK (your own key), requests go directly to your provider; otherwise they transit through our shared AI router.',
'Web Clipper extension (Chrome / Firefox): when you explicitly click "Clip", the extension reads the active URL, page title, text selection and (depending on your choice) the full page HTML. No page content is read unless you initiate the action.',
'Technical and session cookies: strictly necessary for authentication (NextAuth cookies) and preferences (language, theme, extension settings panel). No advertising cookies, no third-party trackers by default.',
'Billing and quotas: for paid tiers, order reference and AI usage tokens stored to enforce quotas (atomic reservation). No bank card data (handled by Stripe).',
'Technical metadata: IP address (logged ~30 days for anti-abuse), user agent, extension version, anonymized error codes.',
],
},
{
id: 'usage',
title: '2. How we use data',
body: [
'Provide the service: notebook sync, search, Memory Echo, Memory Echo insights, agents, web publishing of notes, clips from the extension.',
'Improvement: anonymized error logs and aggregated metrics (internal Prometheus + Grafana dashboards) to diagnose failures and improve performance.',
'Security: abuse detection, protection against credential stuffing and quota overruns.',
'We never sell your data. We never share it with advertisers. No advertising profiling.',
],
},
{
id: 'third-parties',
title: '3. Third parties and sub-processors',
body: [
'Google OAuth: optional authentication (Branding → Audience "In production").',
'Google Cloud Storage (GCP): hosting of attached files and attachments, Europe region (eu-west-3 / europe-west9).',
'PostgreSQL (Docker self-hosted): main database, encrypted backups at rest.',
'Redis (Docker self-hosted): session cache, rate limiting, job queues.',
'AI providers (OpenAI, Anthropic, Google Gemini, Mistral, OpenRouter, or your own BYOK key): the title and content of the selected note are sent for AI features. No training is performed on your data through us; see the chosen provider\'s policy.',
'Stripe: subscription billing. Payment data is handled exclusively by Stripe (PCI-DSS).',
'Resend: transactional emails (account verification, password reset, quota alerts).',
'Google Fonts (CDN fonts.googleapis.com): web fonts on the landing page and the extension side panel — IP address is shared with Google.',
'Google Favicons (google.com/s2/favicons): site icon retrieval in the extension — the domain URL is shared with Google.',
'Grafana Cloud (optional monitoring): anonymized aggregated metrics.',
],
},
{
id: 'extension',
title: '4. Browser extension permissions',
body: [
'activeTab: read the active tab content only when you click the "Clip" button. No passive reading.',
'scripting: inject a small selection-capture script after your explicit click.',
'storage: remember your Memento server URL and the last-used notebook.',
'sidePanel (Chrome) / sidebar_action (Firefox): open the side panel to display the clipper UI.',
'tabs: retrieve the active tab URL and title for the clip context.',
'host_permissions: limited to memento-note.com in production. Extended hosts (localhost / LAN) are only available in the developer build.',
'The extension never reads your browsing history. It never communicates with any server other than the Memento instance you explicitly configured.',
],
},
{
id: 'ai',
title: '5. Artificial intelligence and BYOK',
body: [
'AI features (summary, tags, Memory Echo, semantic embeddings, agents) consume a token-based quota. The quota is atomically reserved before each call.',
'You can provide your own API key (BYOK) to bypass shared quotas and route calls directly through your provider. The key is encrypted at rest (AES-256-GCM) and is never displayed in clear text after entry.',
'No model training is performed on your data through us.',
'You can disable each AI feature individually in Settings → AI.',
],
},
{
id: 'retention',
title: '6. Data retention',
body: [
'Notebooks and notes: kept as long as your account is active. Permanently deleted within 30 days of account deletion (unless a legal obligation requires otherwise).',
'Trash: deleted notes kept for 30 days before permanent purge.',
'Error logs: maximum 90 days, then aggregated.',
'Database backups: kept 30 days in encrypted rotation.',
'Billing metadata: kept 10 years (accounting obligation).',
],
},
{
id: 'rights',
title: '7. Your rights (GDPR / CCPA)',
body: [
'Access: full export of your notes, notebooks, labels and settings via Settings → Data.',
'Rectification: edit directly in the interface.',
'Erasure: account deletion in Settings → Data. Definitive erasure within 30 days.',
'Portability: JSON export of notes, notebooks and labels (equivalent of the `export_notes` MCP server tool).',
'Opt-out of AI processing: disable AI features in Settings → AI.',
'Complaint: you may contact the CNIL (France) or the supervisory authority of your place of residence.',
'To exercise your rights: privacy@memento-note.com.',
],
},
{
id: 'security',
title: '8. Security',
body: [
'Transport: HTTPS enforced, HSTS enabled, security headers (CSP, X-Frame-Options, X-Content-Type-Options).',
'Storage: bcrypt-hashed passwords, AES-256-GCM-encrypted API keys, secrets in encrypted environment variables at rest.',
'Authentication: NextAuth session cookies, signed JWT, sliding expiration.',
'Audit: audit logs for sensitive actions (password change, data export, account deletion).',
'Bug bounty: security@memento-note.com to report a vulnerability. Responsible disclosure program.',
],
},
{
id: 'minors',
title: '9. Minors',
body: [
'Memento is not intended for children under 16 (GDPR) or 13 (COPPA). We do not knowingly collect data from minors. If you believe a minor has provided data, contact privacy@memento-note.com for deletion.',
],
},
{
id: 'changes',
title: '10. Changes to this policy',
body: [
'Any substantial change is announced by email at least 30 days in advance and via an in-app banner. The "Last updated" date at the top of this page is updated with each revision.',
'Non-substantial changes (wording fixes, updated links) are published without notice. The previous version remains accessible on request.',
],
},
{
id: 'contact',
title: '11. Contact and data controller',
body: [
'Memento Labs — Data controller: privacy@memento-note.com',
'Data Protection Officer (DPO): dpo@memento-note.com',
'Postal address: Memento Labs, [address], France',
'Data hosting: self-hosted on Memento Labs infrastructure (PostgreSQL + Redis Docker, Europe region).',
],
},
],
},
}
export default async function PrivacyPage({
searchParams,
}: {
searchParams?: Promise<{ lang?: string | string[] }>
}) {
const sp = searchParams ? (await searchParams) : undefined
const locale = await pickLocale(sp?.lang)
const doc = DOCS[locale]
const isRtl = RTL.includes(locale)
return (
<main
lang={locale}
dir={isRtl ? 'rtl' : 'ltr'}
className="min-h-screen bg-[#0B0A09] text-[#F4F1EA] font-[family-name:var(--font-manrope)] selection:bg-[#D4A373]/40 selection:text-white"
>
<header className="px-5 sm:px-8 py-6 border-b border-white/[0.06] flex items-center justify-between">
<Link href="/" className="flex items-center gap-2.5">
<div className="w-9 h-9 bg-[#F4F1EA] text-[#0B0A09] flex items-center justify-center rounded-lg">
<span className="font-serif text-xl font-bold leading-none">M</span>
</div>
<span className="font-serif text-xl font-medium tracking-tight">Memento</span>
</Link>
<nav className="flex items-center gap-3">
<a
href="?lang=fr"
aria-current={locale === 'fr' ? 'true' : undefined}
className={`text-xs uppercase tracking-widest px-3 py-1 rounded-full border transition-colors ${
locale === 'fr'
? 'bg-[#F4F1EA] text-[#0B0A09] border-[#F4F1EA]'
: 'border-white/15 text-white/55 hover:border-white/40 hover:text-white'
}`}
>
FR
</a>
<a
href="?lang=en"
aria-current={locale === 'en' ? 'true' : undefined}
className={`text-xs uppercase tracking-widest px-3 py-1 rounded-full border transition-colors ${
locale === 'en'
? 'bg-[#F4F1EA] text-[#0B0A09] border-[#F4F1EA]'
: 'border-white/15 text-white/55 hover:border-white/40 hover:text-white'
}`}
>
EN
</a>
</nav>
</header>
<article className="max-w-3xl mx-auto px-5 sm:px-8 py-16 sm:py-24">
<p className="text-[11px] uppercase tracking-[0.3em] text-[#D4A373] mb-4">Privacy</p>
<h1 className="font-serif text-4xl sm:text-5xl tracking-tight mb-4">{doc.title}</h1>
<p className="text-sm text-white/40 mb-3">{doc.lastUpdated}</p>
<p className="text-white/65 leading-relaxed text-lg mb-12">{doc.intro}</p>
<nav aria-label="Sommaire" className="mb-12 p-5 rounded-xl bg-white/[0.03] border border-white/[0.06]">
<p className="text-[11px] uppercase tracking-[0.2em] text-white/40 mb-3">
{locale === 'fr' ? 'Sommaire' : 'Contents'}
</p>
<ul className="space-y-1.5 text-sm">
{doc.sections.map((s) => (
<li key={s.id}>
<a href={`#${s.id}`} className="text-white/65 hover:text-[#D4A373] transition-colors">
{s.title}
</a>
</li>
))}
</ul>
</nav>
<div className="space-y-12">
{doc.sections.map((s) => (
<section key={s.id} id={s.id} className="scroll-mt-24">
<h2 className="font-serif text-2xl tracking-tight mb-4 text-[#F4F1EA]">{s.title}</h2>
<div className="space-y-3 text-white/70 leading-relaxed">
{s.body.map((p, i) => (
<p key={i}>{p}</p>
))}
</div>
</section>
))}
</div>
<footer className="mt-20 pt-8 border-t border-white/[0.06] text-xs text-white/35 flex flex-wrap items-center justify-between gap-4">
<span>© 2026 Memento Labs</span>
<Link href="/" className="hover:text-white transition-colors">
{locale === 'fr' ? 'Retour à l\'accueil' : 'Back to home'}
</Link>
</footer>
</article>
</main>
)
}