Audit complet de la codebase — 2026-06-13
Audit multi-dimensions (sécurité, multi-tenancy, billing/Stripe, base de données, couche IA, routes API, frontend/i18n/SEO/a11y, architecture, tests, ops/config/deps) réalisé en lecture seule sur la branche…
Audit multi-dimensions (sécurité, multi-tenancy, billing/Stripe, base de données, couche IA, routes API, frontend/i18n/SEO/a11y, architecture, tests, ops/config/deps) réalisé en lecture seule sur la branche
claude/laughing-darwin-ffm1e8. Chaque constat porte une référencefichier:ligne. Les sévérités sont Critique / Haute / Moyenne / Basse / Info.
Statut de remédiation (mise à jour 2026-06-13)
Les correctifs ci-dessous ont été appliqués sur cette branche (9 commits), avec
typecheck + 447 tests verts et pnpm audit --prod à 0 vulnérabilité.
✅ Corrigé
- Critiques : C1 (cross-tenant store via chat), C2 (comptabilité crédits FIFO,
- test), C3 (RBAC
shopifyAdminGraphQL, autonomie role-gated, approbations org+write, fencing unifiéfenceUntrusted).
- test), C3 (RBAC
- Haute — sécurité : preview/proxy (auth+SSRF+https), welcome admin, Shopify OAuth HMAC+domaine, callbacks connecteurs (session+ownership), exports sans secrets, chiffrement tokens wizard+figma, EVI rate limit, split-brain secret (proxy + HMAC).
- Haute — billing : daily-bonus (fenêtre 24h), subscription dédup/portal, grant gated active (created/updated/invoice), canaux WhatsApp/API gated+facturés.
- Haute — DB : contrainte Offer (unique partiel PENDING), RGPD Conversations, rétention AuditLog, races acceptOffer + flip PAID, index manquants.
- Médiums : OTP hashé, Turnstile fail-closed, rate limits status/DCR, SSRF dispatch webhook + Browserbase, conv cross-org, self-review/self-checkout, rate limit dispute GET, Connect viewer, JSON-LD escaping, M6 orgId fallback, M7 cache invalidation, stampede crons, BYOK purge, deps (30→0), image pathname.
- Frontend : formulaire contact (Resend), ISR intelligence, canonical, rel sponsored.
⏳ Reporté (gros refactors / décisions produit : non bloquants)
- Frontend : dynamic-import du chat sur
/(H-FE-2), i18n dashboard (H-FE-4), remplacement des tooltips CSSdata-tip(H-FE-5). - Billing : timing payout vendeur vs fenêtre dispute 14j (M3, mouvement d'argent structurel, à concevoir + tester avec soin) ; partiellement mitigé par le flip PAID conditionnel + guard orphelin.
- Marketplace : re-review sur édition de listing LIVE, FSM deal unilatérale, câblage webhook Stripe Identity (KYC).
- Architecture : split de
services/webhooks.ts, fusion des doublons (registries/loggers/formatMoney), refresh AGENTS.md/CLAUDE.md. - Tests : couverture des chemins argent restants (refunds/connect/disputes/handlers).
Verdict d'ensemble
La codebase est mature et au-dessus de la moyenne sur ses fondations :
guards d'authz centralisés et utilisés partout, idempotence Stripe en machine
à états atomique, crypto AES-256-GCM correcte, schema-guard pipeline
exemplaire, discipline TypeScript exceptionnelle (19 as any sur 2 448
fichiers), CSP nonce + strict-dynamic, 46/46 crons protégés timing-safe,
parité i18n parfaite (2 247 clés × 6 locales).
Le risque est concentré, pas diffus : (1) une faille cross-tenant dans le handler de chat, (2) un défaut structurel de comptabilité des crédits (expiry sans FIFO), (3) une grappe d'autorisations manquantes sur la couche IA (RBAC tools + autonomie + approbations), (4) des canaux secondaires (WhatsApp, API channel, EVI) qui contournent tous les garde-fous du chat web, (5) des chemins OAuth/connecteurs incomplets.
Vérifications mécaniques
| Vérification | Résultat |
|---|---|
pnpm typecheck | ✅ 0 erreur |
pnpm test | ✅ 440 passés, 9 skippés (intégration un-skip en CI avec Postgres réel — .github/workflows/ci.yml:78-92) |
pnpm lint | ✅ 0 erreur, ⚠️ 155 warnings (~80 unused vars/imports, 27 console.log, 24 any, 18 import() types) |
pnpm audit --prod | ⚠️ 30 vulnérabilités (28 modérées, 2 basses) — quasi toutes transitives |
Dépendances vulnérables :
nodemailer^7.0.7 (directe) — injection commande SMTP, patché ≥ 8.0.5. Peer de next-auth v4 ; à bumper.hono+@hono/node-server+ip-addressvia@modelcontextprotocol/sdk(~20 advisories), un bump du SDK MCP les résorbe.mermaidvia@streamdown/mermaid;ws/uuidviahume;postcssvianext.
🔴 CRITIQUE
C1 — Prise de contrôle cross-tenant d'un store via le chat
src/features/ai/orchestrator/runtime/handler.ts:166-206,
handler-platform-tools.ts:80-102, src/services/database/prisma-provider.ts:456
Le handler re-vérifie activeOrgId contre la session (getOrgAccess, 403),
mais storeContext.storeID vient du body de la requête et n'est jamais
validé contre l'org autorisée. getStoreIntegrations(storeId) requête sans
filtre orgId et déchiffre le token Shopify du store victime, puis
createShopifyBridge donne à l'IA des outils de mutation Admin live.
Impact : tout utilisateur authentifié d'une org quelconque peut piloter
le Shopify d'un autre tenant (les storeId cuid circulent dans les URLs).
Correctif : après getOrgAccess, faire getStoreAccess(userId, storeID) et
rejeter si sa.orgId !== activeOrgId ; défense en profondeur : ajouter un
paramètre orgId à getStoreIntegrations/getStore et filtrer
where: { storeId, store: { orgId } }.
C2 — Comptabilité des crédits : l'expiry double-compte les crédits dépensés
src/services/billing/credit-balance.ts:48-73,
src/features/ai/orchestrator/runtime/billing.ts:126-155,
prisma/schema.prisma:636,644
Balance = max(0, SUM(amount)) sur les lignes non expirées. Les grants
expirent (monthly 65 j, purchase 365 j, daily bonus minuit) mais les lignes
d'usage (négatives, sans expiresAt) comptent pour toujours. Aucune ligne
compensatoire expire ni allocation FIFO n'existe (l'index FIFO du schéma
est une intention non implémentée).
Mécanique : org Pro reçoit +49 $, dépense 49 $. À J+65 le +49 sort de la
somme → ledger −49 → clamp 0. Le grant suivant +49 affiche 0 $ malgré le
paiement. Le daily bonus utilisé devient une charge nette à minuit. Les
crédits achetés peuvent être absorbés par cette dette fantôme.
Impact : perte silencieuse pour chaque abonné payant à partir de ~J+65.
Le clamp à 0 masque la corruption. (Cause racine partagée avec le clawback
de refund — M-BILL-4.)
Correctif : à l'expiry d'un grant, n'expirer que le reliquat non dépensé —
allocation FIFO usage→grant (l'index existe) + ligne type:"expire" de
-(restant), ou balance par lots.
C3 — Couche IA : effondrement combiné de l'autorisation des tools
Quatre défauts qui se composent en un chemin injection → mutation destructive :
shopifyAdminGraphQLsans RBAC (src/features/ai/tools/shopify-admin.ts:195) : aucuncanWrite(ctx.userRole), un membreviewerpeut exécuter toute mutation Admin (produits, prix, clients, discounts). Les theme/store-tools le font (theme-tools.ts:157,190), pas le tool le plus puissant.autonomyModecontrôlé par le client, non role-gated (handler.ts:146,531,tool-permission-matrix.ts:125-129) : tout membre peut passer"autonomous"(niveau 3) → les opsdestructives'exécutent sans approbation.- Approbations auto-approuvables
(
src/features/ai/agents/agent-autonomy/index.ts:220) : seule contrainteexisting.userId !== decidedBy… qui compare au créateur de la demande, pas de gate owner/admin, le demandeur clique « oui » lui-même. - Fencing anti-injection appliqué à UN seul endroit
(
handler-tools-build.ts:67-72,99-102pour scrape/search web). Non fencés :- résultats intelligence/spy — texte concurrent attaquant-contrôlé
(
intelligence-tools.ts:342-352,495-535,614— dont une synthèse LLM secondaire sur les reviews minées) ; - contenu store indexé + payloads webhook Shopify : champs contrôlés par
les clients du marchand : notes de commande, noms
(
knowledge-tools.ts:140-145,186-191) ; - notes/snapshots de store interpolés dans le system prompt
(
format-situational.ts:70,handler.ts:313) ; - données Shopify Admin (
shopify-admin.ts:276-283) ; - messages WhatsApp entrants + facts mémoire org injectés
(
whatsapp-per-store.ts:269-282) — exfiltration de mémoire possible par un expéditeur externe.
- résultats intelligence/spy — texte concurrent attaquant-contrôlé
(
Correctif prioritaire : canWrite sur shopifyAdminGraphQL ; autonomyMode
en réglage serveur per-org gated par rôle ; approbation par un owner/admin
distinct ; helper unique fenceUntrusted() (avec sentinelle aléatoire
per-call, le délimiteur actuel est statique) appliqué à tous les ingress.
🟠 HAUTE
Sécurité / authz
| # | Constat | Référence |
|---|---|---|
| H-SEC-1 | /api/preview/proxy sans auth : renvoie le HTML upstream depuis l'origine boostecom.app avec CSP injectée script-src * unsafe-inline unsafe-eval + script injecté. Un dev store *.myshopify.com gratuit suffit → JS attaquant same-origin, fetch authentifié passant le check CSRF Origin. PREVIEW_ALLOWLIST=* (route.ts:28) = open proxy/SSRF complet, http: accepté. Le proxy frère /api/preview/[storeId] est correctement gated. | src/app/api/preview/proxy/route.ts:15-35,51,92-93 |
| H-SEC-2 | /api/auth/welcome auto-promotion ADMIN : promotion si userCount === 1 || adminCount === 0 — si la plateforme atteint 0 admin, le prochain utilisateur connecté devient admin global. Bootstrap à sortir vers un seed/script, ou allowlist ADMIN_EMAIL stricte. | src/app/api/auth/welcome/route.ts:90-103 |
| H-SEC-3 | Callback OAuth Shopify : pas de vérif HMAC + shop non validé *.myshopify.com — le shop attaquant-contrôlé construit l'URL d'échange qui reçoit le client_secret → exfiltration du secret + injection d'auth-code. | src/app/api/auth/shopify/{start,callback}/route.ts:26-27,32-60,82 |
| H-SEC-4 | Callbacks connecteurs (google/meta/klaviyo/figma) sans session ni getStoreAccess : écriture d'un connecteur pour un storeID attaquant-fourni = injection cross-tenant ; state OAuth non signé (base64 JSON, ni HMAC ni liaison session). | src/app/api/connectors/*/callback/route.ts |
| H-SEC-5 | Exports org/store dumpent les secrets : include: { connectors: true, integrationConnections: true } expose accessToken/refreshToken/secretHash/mcpKeyHash + PII membres — accessible à un membre viewer. Gater sur settings.update + select explicite sans secrets. | src/app/api/stores/[storeId]/export/route.ts:19-30, organizations/[orgId]/export/route.ts:19-32 |
| H-SEC-6 | Écritures de tokens contournant la couche de chiffrement : wizard connect écrit le shpat_ en clair (en attente du cron 02:30) ; le callback Figma stocke le refresh token dans metadata JSON que le cron de backfill ne chiffre jamais → plaintext définitif. | src/app/api/wizard/store/connect/route.ts:544-566, connectors/figma/callback/route.ts:103-109, cron/encrypt-tokens/route.ts:45,73 |
| H-SEC-7 | /api/evi/chat/completions : ni auth, ni rate limit, ni gate crédits sur un endpoint de streaming LLM (« security-through-unguessability » assumée en commentaire). | src/app/api/evi/chat/completions/route.ts:155 |
| H-SEC-8 | Anonymat marketplace inopérant : masquage uniquement au rendu UI ; /api/marketplace/listings, /recommendations (qui ne sélectionne même pas anonymous), la recherche FTS renvoient title/url/slug réels ; /sellers/[id] liste les listings anonymes sous le vrai nom. Masquer au data-layer (toCardData), slug opaque, exclure du profil public. | src/services/marketplace/listings.ts:96-101, api/marketplace/recommendations/route.ts:104-121, sellers.ts:89-102 |
Billing / argent
| # | Constat | Référence |
|---|---|---|
| H-BILL-1 | Daily bonus dépensable ~10 minutes : cron à 23:50 UTC, expiry au minuit suivant (= +10 min). Le perk affiché $1/$3/$5 est de facto nul (et net-négatif via C2). | vercel.json:16-18, cron/daily-bonus/route.ts:44-53,152 |
| H-BILL-2 | Changement de plan = 2ᵉ subscription Stripe vivante : le checkout ne vérifie pas l'existant ; l'upsert local écrase stripeSubscriptionId (l'ancienne facture en aveugle) ; handleSubscriptionDeleted ne compare pas l'id → annuler l'orpheline rétrograde l'org en free alors que la nouvelle paie. | api/stripe/checkout/route.ts:91-115, prisma-provider.ts:350-374, webhooks.ts:1046-1075 |
| H-BILL-3 | subscription.created force status:"active" + grant inconditionnel : une sub incomplete/trialing est provisionnée et créditée sans encaissement ; invoice.paid à 0 $ (trial) crédite aussi. | src/services/webhooks.ts:921-945,1146-1159 |
| H-BILL-4 | Canaux API & WhatsApp hors garde-fous : gate balance > 0 seulement — pas d'estimate, pas de hold, pas de cap mid-stream, pas de requirePaidPlan, modèle au choix de l'appelant (Opus inclus), échec de débit avalé = complétion gratuite ; conversations sans orgId ni gatées ni facturées. whatsapp-per-store.ts : zéro billing, zéro gate, zéro rate limit, déclenchable par tout expéditeur WhatsApp externe. | api/channels/api/route.ts:65-83,255-257, features/ai/bot/bot-handlers.ts:272-301,531-554, whatsapp-per-store.ts:136,344 |
| H-BILL-5 | Réservation concurrente uniquement sur le chat web : team-mode sub-agents, browser-session, canaux — read-then-allow → overdraft N×balance masqué par le clamp. | handler-team-mode.ts:119-135, store-runtime/session/service.ts:203-219 |
Base de données
| # | Constat | Référence |
|---|---|---|
| H-DB-1 | Offer @@unique([listingId, buyerEmail, status]) collisionne sur les statuts terminaux : re-offre après REJECTED/EXPIRED → P2002 500 ; un doublon EXPIRED fait échouer le updateMany du cron → expiry des offres jammée marketplace-wide. Remplacer par un index unique partiel WHERE status='PENDING' (EXTRA_STEPS) + expiry per-row error-isolée. | prisma/schema.prisma:1906, seller.ts:180-188, cron/marketplace-maintenance/route.ts:54-60 |
| H-DB-2 | RGPD : Conversation/Message survivent à la suppression de compte et d'org (pas de FK, pas de delete dans les transactions) — contenu chat = PII brute. Art. 17 incomplet. | prisma/schema.prisma:1151-1190, delete-account/route.ts, organizations/[orgId]/route.ts:196-202 |
| H-DB-3 | Rétention AuditLog contradictoire : vendu comme ground truth de replay Shopify + trail billing, mais purge totale à 180 j. Exempter billing.*/shopify.webhook.* ou corriger le contrat. | schema.prisma:281,4123, cron/prune-audit-log/route.ts:18,27-52 |
Ops / build
| # | Constat | Référence |
|---|---|---|
| H-OPS-1 | Split-brain secret d'auth : proxy.ts ne lit que NEXTAUTH_SECRET alors que NextAuth et .env.example acceptent l'alias AUTH_SECRET → config alias-only = tous les connectés éjectés des routes protégées. Miroir inverse : INTELLIGENCE_ANON_SECRET/VITALS_BEACON_SECRET ne fallback que sur AUTH_SECRET → clés HMAC vides (forgeables) si seul NEXTAUTH_SECRET est posé. | src/proxy.ts:275,317-319, src/env/server.ts:511-512,539-540 |
| H-OPS-2 | Aucun next build avant deploy : ignoreBuildErrors: true + CI sans job build → push direct sur main = déploiement de code cassé ; erreurs RSC/routes/MDX découvertes au deploy prod. Ajouter un job build CI + branch protection + Vercel required checks. | next.config.mjs:22, .github/workflows/ci.yml |
| H-OPS-3 | Les previews peuvent muter le schéma prod : db push au build et heal cold-start sans check VERCEL_ENV — preview pointée sur la DB prod = pollution de schéma silencieuse (valeurs d'enum irréversibles). Gater sur VERCEL_ENV === "production" + documenter l'invariant Neon branch-per-preview. | scripts/vercel-build.mjs:30-63, instrumentation-node.ts:52 |
Frontend / tests
| # | Constat | Référence |
|---|---|---|
| H-FE-1 | Formulaire de contact en action="mailto:" — soumission silencieusement perdue pour la plupart des utilisateurs (pas de client mail). Funnel principal. Passer par server action + Resend. | src/app/(marketing)/contact/page.tsx:140-144 |
| H-FE-2 | Landing / = arbre client géant : ShellClient (1 228 l.) importe statiquement AiChat/Hub/HomeInsightCards → Prism (2 thèmes dont oneLight en app dark-only), streamdown cjk/code/math/mermaid dans le first paint SEO. next/dynamic + supprimer oneLight. | shell-client.tsx, shell-home.tsx:36-46, code-block.tsx:6-10,60 |
| H-FE-3 | force-dynamic annule l'ISR documentée sur intelligence detail (revalidate=21_600 inopérant, after(rescan) par visite) + 7 autres pages marketing force-dynamic crawlées par ~25 UAs. | intelligence/[category]/[slug]/page.tsx:77,118,138 |
| H-FE-4 | Dashboard 100 % anglais : 0 useTranslations sur les 292 fichiers de (dashboard) malgré 6 locales en parité parfaite. À assumer dans CLAUDE.md ou traiter. Idem hub public (src/components/hub/, strings hardcodées sur /). | (dashboard)/**, hub/marketplace.tsx:342-915 |
| H-FE-5 | Tooltips CSS data-tip maison (61 usages hub, ~700 l. CSS, « NUCLEAR FIX » en commentaire) — pattern banni + inaccessibles clavier/AT, sur la landing publique. Remplacer par ui/tooltip. | src/components/hub/*.tsx, src/styles/hub/hub.css:6877-7570 |
| H-TEST-1 | Couverture inversée au risque : 0 test sur services/stripe/refunds.ts (clawback proportionnel), connect.ts, marketplace/disputes.ts, les ~1 200 lignes de handlers de handleStripeWebhook (seule la machine d'idempotence est testée), lib/security/permissions.ts + 24 fichiers lib/security (RBAC/tenancy), callbacks NextAuth, handler-prestream-gate/handler-cost-guard. Les tests existants sont par ailleurs d'excellente qualité (races réelles sur Postgres, incident-pinning). | cf. src/services/, src/lib/security/ |
🟡 MOYENNE (sélection structurante)
Sécurité
- OTP de login stocké en clair en DB (les magic-links sont hashés) → hasher
sha256(code)(api/auth/send-otp/route.ts:123-124,modules/auth/server.ts:127-133). - Turnstile contournable : vérifié seulement si le client envoie
turnstileToken, et passthrough siTURNSTILE_SECRET_KEYabsent en prod (send-otp/route.ts:57,lib/security/turnstile.ts:54-62). - Clé de chiffrement dérivée de
NEXTAUTH_SECRETsans key-versioning → rotation = perte de tout le chiffré + invalidation sessions (lib/security/crypto.ts:18-33). /api/status/subscribe|confirm|unsubscribesans rate limit (email-bombing Resend) + token confirm réutilisable à vie (api/status/subscribe/route.ts,confirm/route.ts:26-31).- DCR OAuth
/api/oauth/registerouvert sans rate limit ;redirect_urisacceptehttp://+ hôtes publics sans warning « unverified app » (register/route.ts:29). - Token Notion en cookie brut non chiffré, non lié au user (
connectors/notion/callback/route.ts:77-82). - Dispatch
StatusWebhooksans re-validation SSRF niredirect:"manual"(TOCTOU/rebinding) (services/status/notifications.ts:165). - JSON-LD
JSON.stringifynon échappé avec données vendeur → injection HTML (script-XSS bloqué par la CSP) (lib/seo/json-ld.tsx:43). - Browserbase navigate sans
validateScanUrl,about:accepté (api/preview/browserbase/active/navigate/route.ts:63-104). - Relay MCP : seules les mutations
critical(regex figée) sont refusées, lesdestructivepassent sans approbation pour tout client OAuth externe → inverser en allowlist (register-tools.ts:200-211,tool-permission-matrix.ts:169). /api/channels/api: lookup conversation parexternalIdclient sans checkorgIdde la clé → lecture/réassignation cross-org (route.ts:139-200).- Stripe Connect onboard sans check de rôle (un
viewerpeut connecter le compte de payout) (api/vendor/stripe/connect/onboard/route.ts:76-88). - KYC
contextType/contextIdnon whitelistés (api/marketplace/kyc/start/route.ts:21-28) ; URLsevidencedes disputes validées en syntaxe seulement → restreindre aux blobs Vercel (api/marketplace/disputes/route.ts:47). - Pages dashboard
[orgSlug]non edge-gated (voulu, layout côté serveur OK) ; 2 routes admin avec gate divergenteemail.endsWith("@boostecom.app")(admin/pipeline/health/route.ts:72,admin/shopify/backfill/route.ts:46).
Billing / marketplace
- Webhook Stripe : 500 sur échecs permanents (metadata manquante) → retries infinis 3 jours ; distinguer permanent (200 + DLQ) de transitoire (
api/webhooks/stripe/route.ts:64-70). - Payout vendeur immédiat au
checkout.session.completedalors que la fenêtre de dispute est de 14 j ; reversal best-effort → si le solde Connect est vide, la plateforme paie le refund ; stampstripeTransferIdhors de l'update PAID (webhooks.ts:624-725,refunds.ts:86-134,connect.ts:205). - Flip
PAIDnon conditionné surstatus=PENDING(évènements désordonnés peuvent ressusciter un ordre REFUNDED) (webhooks.ts:624-632). - Clawback refund sans comptabilité par lots (même racine que C2) (
webhooks.ts:257-298). - Webhooks subscription no-op si
metadata.orgIdabsent (subs créées au dashboard Stripe driftent) → fallbackstripeCustomerId(webhooks.ts:916,986,1055). subscription.deletedn'invalide pas le cache org (webhooks.ts:1046-1107).- Listing LIVE modifiable après review sans re-review (bait-and-switch URL) + éditable pendant
PENDING_REVIEW(vendor/marketplace/listings/[id]/route.ts:28-134). - FSM deal pilotable unilatéralement par le vendeur jusqu'à
ESCROW_FUNDED/CLOSED(badges « N deals closed » gonflables) ; stamps légaux click-through contournantLegalSignature(seller.ts:257-302,374-398). - Auto-review + auto-achat non bloqués (reviews « verified buyer » à ~3 % de coût via Connect) (
reviews/route.ts:25-132,checkout/route.ts:45-122). - Webhook Stripe Identity non câblé :
syncFromProviderSessiona zéro appelant → KYC provider bloquéIN_REVIEWà vie (kyc.ts:134-168,webhooks.ts:125-171). acceptOffer: pre-check non atomique → double-accept = 2 DealThreads ; lien offer→deal hors transaction (seller.ts:121-159).- OTP offres : 409 divulguant l'
offerIdd'autrui ; brute-force distribuable (limite par IP) ; compare non constant-time (offers/route.ts:93-102,verify/route.ts:35,57). Order.amount/Offer.amountenInt(cap ~21,4 M$) vs Zod max 100 M$ → 500 driver (offers/route.ts:32).MonthlyReset.amount,DailyBonus.amount,Plan.*Priceencore enFloat(le ledgerCreditest bien enDecimal(19,6)).Credit.typeen texte libre avec writers divergents → enum Postgres (schema.prisma:626,me/route.ts:361-368).- Ingester commandes Shopify sans guard anti-stale malgré le contrat du schéma (
features/shopify/ingester/handlers/order.ts:150-175). - Suppressions org/compte : cascade ~100 tables dans une
$transactionau timeout par défaut 5 s → P2028 sur grosse org, suppression impossible. - Mémoire IA : extraction de facts sans scrubbing PII, ré-injectés y compris sur WhatsApp (
features/ai/memory/extract.ts). - Coûts IA : image/vidéo non facturés + modèle image absent de
AI_PROVIDER_COSTS; cap mid-stream aveugle aux sub-agents délégués (abort non propagé) ; estimate pré-stream ignore les pièces jointes (handler-tools-build.ts:139,delegate-tool.ts:206-215,billing-plans.ts:96-279).
DB / perfs
- Index manquants :
AuditLog(resource, resourceId, createdAt)(scan complet depuis un tool de chat),AuditLog(userId, createdAt)(cron daily-bonus),DealThread(listingId), FKsSetNull/Cascadenon indexées (Dispute.resolvedById,SavedListing.listingId…). - Pool pg non configuré (défaut max 10/instance) + fallback d'URL préférant les non-poolées → épuisement Neon sous fan-out (
lib/core/database.ts:37-55). - Tables sans pruning malgré contrats :
ListingEvent(« prune > 90j » non implémenté),StripeEvent(payload JSON complet),CronExecution,Scan/ScanArtifact,MemoryEvent. - Filtre
querydelistListingsen 4×ILIKE %q%sans pg_trgm (seq scan) vs le chemin FTS propre (listings.ts:168-175). EXTRA_STEPS:textToEnumStepréécrit les données au cold-start (lock ACCESS EXCLUSIVE + rewrite deCredit), à sortir en migration opérateur ;CREATE INDEX CONCURRENTLYinterrompu = index INVALID invisible au guard (joindrepg_index.indisvalid) ; delta destructif au build = warning non pagé (pending-migrations.ts:106-145,vercel-build.mjs:55-66).
Ops / qualité
- Stampede crons à minute :00 — 04:00 = 10 jobs simultanés,
reset-creditsdans la mêlée de minuit → étaler les minutes (vercel.json). - Lint consultatif : tout en
warn, pas de--max-warnings 0,scripts/*.mjsignorés du lint, blocparserstring mort dans la flat config (eslint.config.mjs:17,44-85). - Pas de Sentry ni d'alerting d'erreurs/crons (CronExecution + page admin seulement).
- next-auth v4 sur Next 16/React 19 : matrice non supportée, migration Auth.js v5 à planifier (résout aussi H-OPS-1 et le peer nodemailer).
- Cold-start : sync marketplace + bootstrap discovery détachés à chaque cold start sans lease cross-instance →
SET NX EXRedis (instrumentation-node.ts:51-61). - Proxy : self-fetch
/api/auth/onboarding-statuspar navigation protégée sans timeout (proxy.ts:281-295,360-377). images.remotePatterns:*.myshopify.com+www.google.comsans pathname → optimizer en semi-open proxy de coût (next.config.mjs:41-47).- tsconfig :
noUncheckedIndexedAccessabsent (flag le plus rentable manquant). - 14 fichiers de
components/importent@/services/*(design system non extractible) ;features/ai(374 fichiers, 63 % du code feature) couplé circulairement àstore-runtime. - God-files :
services/webhooks.ts(1 531 l. — à splitter par famille d'événements avant tout autre refactor),wizard/store/connect/route.ts(615 l.),home-insight-cards.tsx(2 120 l.), dialogs 1 700-2 000 l. - Doublons : 2 identity-registries agents aux données divergentes (accentHex Faye
#2563ebvs#10b981), 2 loggers, 2withTimeout, 3formatMoney, 10+formatDatelocaux. - BYOK : permission
ai.byok_manageencore accordée aux admins + formulaire factice « coming soon » +"byok"vendu dans le JSON-LD/FAQ de la home (permissions.ts:38,74,129,account/settings/tokens/page.tsx:58-202,home-jsonld.tsx:86). - AGENTS.md matériellement périmé (Faye « Finance », chemins inexistants,
docs/workspace/absent) ; CLAUDE.md : modulepaymentsinexistant, 12/16 features non documentées, ligne « BYOK » mal étiquetée ;tasks/todo.md= plan v3.0 terminé avec 69 cases non cochées. - SEO :
canonical: "/"hérité du root layout (désindexe/status) ; sitemap sans/status,/sell,/sellers/[id], tutorials/courses/docs detail ; lien affilié Shopify du footer global sansrel="sponsored"; nofollow incohérent sur les liens produits des stores espionnés dans le hub ;howToJsonLdjamais émis (drift CLAUDE.md). - A11y : boutons à icône seule sans
aria-label(chat branches, file-manager, platform-center) ; drawerrole="dialog"sans focus trap ; redimensionnement au pointeur uniquement ; micro-typo 9-11px généralisée. - Theming : ~308 hex hardcodés hors
styles/(chrome#121212, vert Shopify inline) ; résidus light (oneLight,colorScheme: "dark light").
🟢 BASSE (échantillon)
hashtext 32-bit pour l'advisory lock (collision = sérialisation parasite, jamais d'under-lock) ; isEncryptedToken heuristique (préfixer enc:v1:) ; _debug dans une 403 (integrations/shopify/custom-app/route.ts:78) ; slug MDX non validé (confiné .mdx) ; /api/og SSRF aveugle via icon ; callback Notion validation de path incomplète ; recommendations-v2 interpole lookback/limit (serveur-contrôlés) ; preview-token compare non constant-time ; commande payable après dé-publication du listing (session Stripe 24 h) ; markup 2× à l'achat documenté mais non implémenté (1× réel, marge à la consommation seulement) ; analytics credits_purchased double-émis et taxe incluse ; onlyBuiltDependencies listant 4 paquets absents du lockfile ; entrées knip mortes (src/middleware.ts) ; logger sans redaction par clé ; headers sécurité absents de /api/* (matcher proxy) ; pre-push multi-minutes (retirer pnpm test, CI le couvre) ; @neondatabase/serverless quasi mort (1 consommateur).
Ce qui est remarquablement bien fait
- Schema-guard pipeline : catalogue généré via
migrate diffsans DB, DDL additif idempotent, heal cold-start + réactif rate-limité, dry-run opérateur, tests unitaires + intégration rejouant l'incident du 10 juin. - Idempotence Stripe : machine à états
StripeEventavec claim atomique et re-claim sur stale, supérieure au delete-on-failure ; clés partagéesMonthlyReset/DailyBonus; plan résolu par price id, jamais par metadata spoofable ; grants tax-exclusive corrects. - Chat web : réservation de crédits sous
pg_advisory_xact_lock, settlement exactly-once finish/abort, facturation de la consommation partielle,consumeStreampost-déconnexion. - OAuth 2.1 MCP : PKCE S256 only, codes/tokens hashés, single-use transactionnel, révocation sur replay, rotation refresh, scoping par store re-vérifié.
- Crons : 46/46 derrière
withCronAuthtiming-safe fail-closed, 1:1 exact avecvercel.json,maxDurationpartout, ledger d'exécution. - Webhooks entrants : Stripe/WhatsApp/Shopify (+3 GDPR) vérifiés timing-safe, fail-closed sur secret absent.
- Type safety : strict mode, 19
as any/ 1@ts-ignore/ 4 non-null sur 2 448 fichiers ; kebab-case 100 %. - i18n messages : parité parfaite 2 247 clés × 6 locales ; infra SEO profonde (sitemap dédupliqué, llms.txt/json, OG edge, noindex spy réellement implémenté, ISR marketplace conforme aux docs).
- CSP :
default-src 'none', nonce +strict-dynamic, exceptions datées et commentées. - Suppression de compte RGPD : transactionnelle, pseudonymisation tombstone des ordres/deals/disputes/signatures (gaps : Conversations en H-DB-2, rétention KYC/AML).
Plan d'action priorisé
Semaine 1 — argent & cross-tenant (bloquant)
- C1 : valider
storeContext.storeIDcontre l'org (3 lignes + défense en profondeur). - C3.1-3 :
canWritesurshopifyAdminGraphQL,autonomyModeserver-side role-gated, approbation owner/admin distinct. - H-BILL-4/H-SEC-7 : gater + facturer + rate-limiter WhatsApp per-store, API channel, EVI.
- H-SEC-1 : auth + sandbox sur
/api/preview/proxy, tuer*ethttp:. - H-SEC-2/3/4 : welcome admin, HMAC+domaine Shopify OAuth, session+state signé sur les callbacks connecteurs.
Semaines 2-3 — intégrité financière
6. C2 + M clawback : comptabilité FIFO/lots des crédits + lignes expire ; corriger la fenêtre du daily bonus (H-BILL-1).
7. H-BILL-2/3 : dédup subscription au checkout + narrow status sur created + gate trial.
8. H-DB-1 : index unique partiel PENDING sur Offer + cron expiry per-row.
9. M : payout vendeur différé post-fenêtre dispute (ou reverse_transfer sur le refund) ; flip PAID conditionnel.
10. H-SEC-5/6 : exports sans secrets ; écritures de tokens via la couche chiffrante.
Mois — durcissement & dette
11. C3.4 : fenceUntrusted() unique sur tous les ingress (intelligence, knowledge, webhooks, situational, Shopify data, WhatsApp).
12. H-DB-2/3 : purge Conversations au delete + rétention AuditLog différenciée.
13. H-OPS-1/2/3 : alias secrets unifiés, job next build CI + protections, gate VERCEL_ENV sur db push/heal.
14. H-TEST-1 : tests sur refunds/connect/disputes/handlers webhook/permissions/cost-guards (profiter du split de webhooks.ts).
15. Frontend : contact form → Resend ; dynamic-import du chat sur / ; retirer force-dynamic marketing ; canonical per-page ; tooltips ui/tooltip ; i18n dashboard (ou l'assumer).
16. Hygiène : bump nodemailer + SDK MCP ; purge BYOK ; fusion des registries/loggers dupliqués ; refresh AGENTS.md/CLAUDE.md ; rate limits status/DCR ; OTP hashé ; Turnstile fail-closed.