Replaces EmailJS-based welcome-back (disabled in PR #6 because the
EmailJS account is Melissa-only with obsolete '(c) 2025' footer) with
a Worker action that delegates to Resend, ensuring same '\xc2\xa9 2026'
footer as all other transactional emails.
Worker side:
- New action 'sendWelcomeBack' validates Turnstile + searches HubSpot
contact by email (404 if not found, prevents spam to non-existing).
- New function sendWelcomeBackViaResend: same navy/gold visual
identity as other Resend emails. Subject includes existing ref
(= 'Vous \xc3\xaates d\xc3\xa9j\xc3\xa0 inscrit chez MVA Global Fret \xe2\x80\x94 R\xc3\xa9f\xc3\xa9rence MVA-XXX').
Idempotent: zero HubSpot writes.
Frontend (form-handler.js):
- sendWelcomeBackEmail rewritten to POST Worker action sendWelcomeBack
instead of emailjs.send. Reuses window.turnstileToken.
- Re-enabled call inside showAlreadyRegistered.
Smoke tests:
- 400 'email requis' without email
- 403 'Turnstile validation failed' without valid token
- Full flow testable via browser only (= real Turnstile challenge).
Refs: post-cutover polish, fixes Issue 1 footer 2025 from E2E retest.
Direct CRM API needed crm.objects.contacts.write scope which the
existing Personal Access Key doesn't have. Using HubSpot Forms API
instead — same endpoint the browser used to call directly, but now
called server-side from the Worker after email verification.
This means HubSpot contact creation works without granting any
additional scopes to the token: anyone can submit a public form.
Also updates the welcome email warning to tell the customer to copy
the address EXACTLY as shown (the reference is now baked into line 1
of the address) and not to modify anything.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Previously the Forms API created the contact at form submission time —
which meant unverified signups (bots that pass Turnstile, typos, fake
emails) polluted HubSpot. Now:
- Form submit → Worker stores all data in KV (24h TTL) + sends Brevo
verification email (no HubSpot write)
- User clicks email link → Worker generates ref + creates HubSpot
contact via CRM API + sends welcome email with ref + Paris address
Plus this commit:
- Email header gets the MVA logo on the left of the dark blue banner
- Welcome email's first address line auto-injects (MVA-XXX) so the
customer can copy it directly onto their package
Also handles idempotency — clicking the verification link a second time
returns the existing ref without creating a duplicate.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Resend requires a verified domain to send to arbitrary recipients —
mvaglobalfret.com isn't registered. Brevo accepts single-sender
verification on a free email address, so we can send from
mvaglobalfret@gmail.com without owning a domain.
- Worker: replace resendSend() with brevoSend() (api.brevo.com/v3/smtp/email)
- Env vars: BREVO_API_KEY, BREVO_SENDER_EMAIL, BREVO_SENDER_NAME
- Update comments in confirmation.js and form-handler.js
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Architecture finale :
1. User remplit formulaire + passe Turnstile CAPTCHA → form-handler.js
2. form-handler.js POST au Worker avec action 'requestVerification'
3. Worker valide Turnstile, génère un token UUID, le stocke en KV (TTL 24h)
avec firstname/email/reference_client, puis envoie un email via Resend
avec un lien : confirmation.html?token=XXX
4. User reçoit email, clique 'Confirmer mon email'
5. confirmation.html lit le token de l'URL, POST au Worker avec action
'verifyToken'
6. Worker valide le token, envoie le welcome email via Resend (avec ref +
adresse Paris depuis env var), marque le token comme utilisé
7. confirmation.html affiche 'Inscription confirmée !'
Ainsi : ref + adresse Paris ne sortent JAMAIS avant validation email,
et les bots sont bloqués à l'étape 1 par Turnstile.
Setup Cloudflare requis (côté user) :
- RESEND_API_KEY : clé API Resend (re_...)
- RESEND_FROM : adresse expéditrice ('onboarding@resend.dev' pour test,
ou domain vérifié pour prod)
- SITE_URL : optionnel, défaut https://mva-global-fret.github.io/site-mva-global-fret
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- contact.html: ajout du widget Turnstile (site key: 0x4AAAAAADKDuc7Rmlb1svIL)
- form-handler.js: blocage de la soumission si pas de token Turnstile valide
- Worker: validation server-side du token via /turnstile/v0/siteverify
avant chaque appel sendWelcomeNow → bloque les bots qui n'auraient
pas passé le challenge côté client.
Le secret Turnstile est en env var Cloudflare (TURNSTILE_SECRET).
Limite humain : Turnstile détecte les bots avec très peu d'interaction
côté utilisateur (mode 'Managed', le plus souvent invisible).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Bug 1 — Ref MVA-001 dupliquée :
Le filtre HubSpot 'HAS_PROPERTY' avec value:'' retournait 0 résultats.
Suppression du value:'' → maintenant le worker liste correctement les
contacts avec reference_client et incrémente bien (testé : MVA-004).
Bug 2 — Email post-inscription jamais reçu :
Le double opt-in HubSpot ne se déclenche pas via Forms API sans
subscription consent (impossible à configurer sans nouveaux scopes
Private App). Pivot vers une approche plus simple :
- L'email de bienvenue est désormais envoyé directement après
soumission du formulaire (pas de DOI HubSpot)
- L'envoi passe par le Cloudflare Worker (action sendWelcomeNow)
pour que l'adresse Paris reste dans les env vars Cloudflare et
ne soit JAMAIS dans le JS public
- Worker appelle EmailJS REST avec firstname + reference + paris_address
Cleanup : message de succès reverti à 'Inscription réussie' (FR/EN/MG).
Anti-spam : protection légère via filtre email/téléphone côté formulaire.
La cron-based welcome (post-DOI) reste en place mais sera inerte tant
que aucun contact n'a le statut CONFIRMED côté HubSpot.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Architecture finale (Option A choisie) :
1. User submit form → contact créé en HubSpot avec reference_client
2. HubSpot envoie l'email de double opt-in (sans la ref ni l'adresse Paris)
3. User clique 'Confirmer' → HubSpot met hs_emailconfirmationstatus = CONFIRMED
4. Cron Cloudflare (toutes les 5 min) :
- Liste les contacts CONFIRMED + créés après le cutoff
- Filtre via Cloudflare KV (welcomed:<email>) pour idempotence
- Envoie le welcome email via EmailJS REST API avec :
• firstname
• reference_client
• paris_address (depuis env var PARIS_DEPOT_ADDRESS)
- Marque envoyé dans KV avec TTL 1 an
Protection :
- L'adresse du dépôt Paris ne quitte JAMAIS Cloudflare/EmailJS
- Elle n'arrive au client que dans le mail de bienvenue post-opt-in
- Bots qui n'ont pas un vrai email ne peuvent pas valider → ne reçoivent rien
- Anti-spam et anti-cartons-vides blindé
Ajout d'une action 'triggerWelcomeQueue' pour debug/manual run.
Doc complète dans cloudflare-worker/DEPLOIEMENT.md (étapes 1 à 6).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Supprime HUBSPOT_SERVICE_KEY du code JS public (évite l'exposition du token)
- Remplace l'appel CORS-bloqué vers api.hubapi.com par un appel au proxy
Cloudflare Worker (WORKER_PROXY_URL, vide par défaut = sans blocage)
- Ajoute cloudflare-worker/hubspot-proxy.js : code complet du Worker à
déployer gratuitement depuis dash.cloudflare.com (sans CLI ni Node.js)
- Quand WORKER_PROXY_URL est renseigné, la détection doublon est active :
un client existant voit son message "déjà inscrit" sans re-soumission
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>