Audit de vérification — 2026-06-14
Audit de l'état courant de la branche après remédiation de l'audit complet du 2026-06-13 (PR #292/#293 mergées). Objectif double : (1) vérifier au code — pas aux messages de commit : que chaque correctif…
Audit de l'état courant de la branche après remédiation de l'audit complet du 2026-06-13 (PR #292/#293 mergées). Objectif double : (1) vérifier au code — pas aux messages de commit : que chaque correctif critique/haut/moyen tient réellement ; (2) détecter les nouveaux problèmes non couverts. Réalisé en lecture seule via 6 analyses parallèles (sécurité/multi-tenancy, billing/Stripe, couche IA, base de données, ops/CI/deps, frontend/SEO/a11y). Chaque constat porte une référence
fichier:ligne. Sévérités : Critique / Haute / Moyenne / Basse / Info.
Vérifications mécaniques
| Vérification | Résultat |
|---|---|
pnpm typecheck | ✅ 0 erreur |
pnpm lint | ✅ 0 erreur, ⚠️ 155 warnings (inchangé vs 13/06 — tout en warn, pas de --max-warnings 0) |
pnpm test | ✅ 447 passés, 9 skippés (55 fichiers) |
pnpm deadcode (knip) | ✅ exit 0 (fichiers/deps bloquants verts) |
pnpm audit --prod | ✅ 0 vulnérabilité (le correctif 30→0 tient) |
Statut de remédiation (session 2026-06-14)
Suite à cet audit, une passe de remédiation a été appliquée sur cette branche
(17 commits, dont les 2 décisions produit M3 + N18), avec typecheck + 451 tests
verts, lint 0 erreur, schema-guard régénéré et pnpm audit --prod à
0 vulnérabilité.
✅ Corrigé
- Argent (P0) : réservation atomique des crédits (
reserveCreditsForChannel)- cap mid-stream + settle via
trackUsage(holdId)étendus aux 3 canaux (bot WhatsApp, WhatsApp per-store, API channel) : H-BILL-4 et le volet canaux de H-BILL-5 ; rate-limit ajouté sur l'API channel ; EVI gate par secret partagéEVI_CLM_SECRET(timing-safe, fail-closed) : H-SEC-7 ; bornes int4 sur les Zod money marketplace — N7. Cap de coût + abort propagés aux sous-agents délégués (addExternalUsage+parentSignalviaAbortSignal.any) : N20 / volet sous-agents de H-BILL-5.
- cap mid-stream + settle via
- Marketplace : anonymat enforced au data-layer (url masquée dans
tous les mappers (toCardData/search/related/recommendations-v2/sellers) +
route
recommendations, listings anonymes exclus du profil vendeur public, lien vendeur masqué sur la page detail) H-SEC-8. - Sécurité / fuite : exports org/me en
selectwhitelist (dropStore.apiKey/stripe*), H-SEC-5/N6 ; token Notion (session requise + chiffrement AES-256-GCM), N5 ; IDORThemeBranch(updateMany+storeId), N16 ; bug spelling custom-app + retrait_debug— N17. - Injection IA (C3.4) : fence du bloc mémoire (system prompt), du DOM
navigateur, des hits RAG externes, des docs scrapées, et des sorties
concurrents (sections/catalog/ads/angles intelligence) ;
canWritesur les outils navigateur mutants +run*(aeo/aov/vitals) ;canReadsur les 8 commerce tools + neutralisation des labels UTM, N1/N8/N9. - DB :
onDelete: Restrictexplicite surOrder.listing, N2 ; indextierExpiresAt,Dispute.resolvedById,SavedListing/ListingVote.listingId, N3/#4 ; timeout 30s sur les transactions de suppression cascade — N10. - RGPD : redaction
Message.contentdu user supprimé (convos non-possédées), N12 ; rétentionStripeEvent.payload(90j) dans le cron de purge, N11 ; purgeMemoryEventpar scope à la suppression compte/org + rétention 180j +@@index([ts]), N10 ; scrubbing PII (emails/téléphones) à l'extraction des facts mémoire + test unitaire — N19. - Ops / SEO : job
build(next build) en CI, N4/H-OPS-2 ; timeout sur les self-fetch du proxy, N14 ;pathnamesur l'image*.myshopify.com, N14 ; contradictiongenerateStaticParams+force-dynamicretirée, N13 ;/sell/statusajoutés au sitemap.
- Décisions produit (tranchées) : M3, payout vendeur en
escrow, versé après la fenêtre de dispute 14j via le cron
marketplace-payouts(plus au checkout) ; pendant la fenêtre aucun transfer n'existe donc un refund ne nécessite aucune reversal. N18 — relay MCP passé en lecture seule (refusedestructive+critical, writes via le dashboard).
⏳ Reporté — chantiers dédiés (hors périmètre d'une passe de remédiation)
- H-OPS-3 : garde
VERCEL_ENVsur le push/heal schéma, un gate brut casserait les déploiements preview (config par-environnement nécessaire). - Lint bloquant (
--max-warnings 0) : nécessite de résorber les 155 warnings d'abord. - Auth.js v5 (~2-3 j),
noUncheckedIndexedAccess(remédiation repo-wide), i18n dashboard + tokens couleur (N15), émissionhowToJsonLd(besoin de steps structurés dans le contenu).
Verdict d'ensemble
La remédiation du 13/06 tient sur l'essentiel : les trois critiques sont
réellement corrigés au code (C1 cross-tenant, C2 comptabilité FIFO des crédits,
C3.1-3 autorisation des tools IA), de même que la majorité des hauts sécurité
et billing. La discipline tenant-auth reste exemplaire (getStoreAccess/
getOrgAccess centralisés et appliqués quasi partout) et aucun nouveau chemin
d'accès cross-tenant aux données n'a été trouvé dans les tools IA ni les routes.
Le risque résiduel est concentré sur deux axes :
-
Argent — les garde-fous restent limités au chat web. La réservation atomique sous
pg_advisory_xact_lock(H-BILL-5) n'a toujours qu'un seul appelant : le chat web. Les sub-agents délégués, les canaux API/WhatsApp et la voix EVI lisent le solde sans verrou puis débitent après coup : course TOCTOU de surcharge non mitigée. Les canaux n'ont ni estimate pré-stream ni cap mid-stream (H-BILL-4 partiel). EVI reste sans auth ni gate crédits (H-SEC-7 partiel). Le payout vendeur part immédiatement alors que la fenêtre de dispute est de 14 j (M3, reporté). -
Injection IA —
fenceUntrusted()existe mais sa couverture est incomplète (C3.4 partiel). Le helper est bien conçu (sentinelle aléatoire par appel) mais ~5 points d'ingress seulement sont fencés. Restent bruts : DOM navigateur, sous-outils intelligence concurrents, hits RAG, facts mémoire ré-injectés dans le system prompt, et data Shopify Admin. Le relay MCP laisse encore passer les mutationsdestructivesans approbation, et l'extraction mémoire ne scrub aucune PII.
Le reste est de la dette structurée (retention DB, types money, i18n dashboard, ISR marketing) et des reports produit déjà documentés.
Statut de remédiation vérifié
🔴 Critiques — tous corrigés
| ID | Statut | Preuve |
|---|---|---|
| C1 prise de contrôle cross-tenant store via chat | ✅ CORRIGÉ | handler.ts:186-194 valide storeContext.storeID via getStoreAccess et rejette 403 si storeAccess.orgId !== activeOrgId ; activeOrgId re-dérivé DB (:172-178), rôle écrasé par orgAccess.role (:201). Réserve : getStore/getStoreIntegrations (prisma-provider.ts:179,456) ne se filtrent pas par org — la défense en profondeur manque, mais le guard du handler ferme le trou. |
| C2 double-comptage des crédits dépensés à l'expiry | ✅ CORRIGÉ | credit-balance.ts:80-113 : vraie comptabilité FIFO par lots, forme close fifoBalance(N,E,D)=max(0, N−max(0, D−E)) (les débits consomment le pool expiré E avant le non-expiré N). Hold release + write usage atomiques (runtime/billing.ts:167-172). Implémentation réelle, pas qu'un index. |
C3.1 shopifyAdminGraphQL sans RBAC | ✅ CORRIGÉ | shopify-admin.ts:224-231 : isMutation && !canWrite(ctx.userRole) → denied. |
C3.2 autonomyMode client-contrôlé | ✅ CORRIGÉ | handler.ts:210-213 : autonomous seulement si hasMinRole(user.role,"admin") (rôle DB-dérivé), sinon clampé approval. |
| C3.3 approbations auto-approuvables | ✅ CORRIGÉ | services/agent-autonomy/index.ts:208-247 : exige decidedByOrgId === existing.orgId et decidedByCanWrite (owner/admin). |
| C3.4 fencing anti-injection à un seul endroit | ⚠️ PARTIEL | fence.ts:22-37 : helper réel à sentinelle aléatoire randomBytes(9). Appliqué à ~5 ingress (intelligence summary/teardown, store content, webhook payloads). Non fencés : sous-outils intelligence concurrents (intelligence-tools.ts:506-516,539-545,591-602,754,774,801), hits RAG (knowledge-tools.ts:40-46,85-91), facts mémoire dans le system prompt (memory/format.ts:54,73 → handler-system-layers.ts:150), data Shopify Admin (shopify-admin.ts:170,298 = simple clé _untrusted, pas de fence), situational via sanitizeInline au lieu de fence (format-situational.ts:74-84). |
🟠 Hautes — sécurité
| ID | Statut | Preuve |
|---|---|---|
| H-SEC-1 preview proxy SSRF/auth | ✅ CORRIGÉ | Auth (preview/proxy/route.ts:65,213), * rejeté (:32), HTTPS-only (:57), validateScanUrl (:59), rate-limit (:68), iframe sandbox CSP (:118). |
| H-SEC-2 auto-promotion ADMIN | ✅ CORRIGÉ | Branche adminCount===0 retirée ; promotion sur userCount===1 ou ADMIN_EMAIL exact, sous withSessionAuth (auth/welcome/route.ts:29,92-104). |
| H-SEC-3 OAuth Shopify HMAC + domaine | ✅ CORRIGÉ | isValidShopDomain regex exacte *.myshopify.com + verifyShopifyOAuthHmac constant-time fail-closed + state cookie comparé (shopify-oauth.ts:18-74, callback:43,48,57). |
| H-SEC-4 callbacks connecteurs | ⚠️ PARTIEL | Google/Meta/Klaviyo/Figma : session + getStoreAccess/getOrgAccess + chiffrement tokens corrigés. Mais OAuth state = base64 JSON non signé (ni HMAC ni cookie CSRF), PKCE helpers présents mais inutilisés. Notion reste ouvert (cf. nouveaux constats). |
| H-SEC-5 exports dumpent les secrets | ⚠️ PARTIEL | stores/[storeId]/export corrigé (select whitelist, route.ts:37-60). organizations/[orgId]/export ouvert : include:{stores:true,subscription:true} (route.ts:31-37) expose Store.apiKey + stripeCustomerId/SubscriptionId. Idem me/export/route.ts:36,39. |
| H-SEC-6 écritures tokens hors chiffrement | ✅ CORRIGÉ | Wizard (wizard/store/connect/route.ts:546) + Figma callback (:116,118) chiffrent avant write ; cron backfill idempotent. |
| H-SEC-7 EVI sans auth/rate-limit/crédits | ⚠️ PARTIEL | Rate-limit ajouté (evi/chat/completions/route.ts:166, 20/min/IP). Toujours sans auth ni gate crédits — stream sur clés plateforme, brûlage de crédits borné par la seule IP. |
| H-SEC-8 anonymat marketplace inopérant | ❌ OUVERT | Masquage UI-only par design (listings.ts:96-104, url brut). Domaine réel dans le Flight payload via toCardData/search.ts:126,173/sellers.ts:112/recommendations-v2.ts:124 ; recommendations/route.ts:104-121 (non-auth) renvoie url brut ; page detail expose sellerId + lien public (marketplace/[type]/[slug]/page.tsx:207-211,817-827). Dé-anonymisation triviale. |
🟠 Hautes — billing
| ID | Statut | Preuve |
|---|---|---|
| H-BILL-1 daily bonus dépensable ~10 min | ✅ CORRIGÉ | Cron 5 0 * * * (00:05), expiresAt = startOfToday + 24h (daily-bonus/route.ts:153) → ~24 h utilisables. (Docstring :5 encore « 23:50 » = commentaire périmé, code correct.) |
| H-BILL-2 changement plan = 2ᵉ subscription | ✅ CORRIGÉ | Checkout dédup → portal si sub live (stripe/checkout/route.ts:94-113) ; Subscription.orgId @unique ; deleted-handler compare l'id (webhooks.ts:1137-1149). |
H-BILL-3 subscription.created force active + grant | ✅ CORRIGÉ | Statut restreint (webhooks.ts:960-963), grant gaté active (:990-997) ; invoice.paid exclut trialing (:1242). |
| H-BILL-4 canaux API/WhatsApp hors garde-fous | ⚠️ PARTIEL | Ajoutés : requirePaidPlan + floor balance<=0 (3 canaux) + rate-limit (WhatsApp ; manque sur API channel). Manquent toujours : estimate pré-stream, hold/réservation, cap mid-stream (channels/api/route.ts:72-86, bot-handlers.ts:277-301, whatsapp-per-store.ts:410-419). Solde quasi-nul → requête arbitrairement coûteuse autorisée. |
| H-BILL-5 réservation concurrente chat web seulement | ❌ OUVERT | reserveCredits (advisory lock atomique) correct mais un seul appelant : handler-prestream-gate.ts:66 (chat web). Sub-agents (delegate-tool.ts:215 sans hold ; handler-team-mode.ts:119-135 trackUsage sans holdId), API channel, WhatsApp ×2, EVI : read-then-debit → course de surcharge. Non étendu. |
🟠 Hautes — base de données
| ID | Statut | Preuve |
|---|---|---|
H-DB-1 Offer @@unique collision statuts terminaux | ⚠️ PARTIEL | Index unique partiel WHERE status='PENDING' posé (pending-migrations.ts:137-151) → plus de jam. Mais l'expiry reste un updateMany en bloc (cron/marketplace-maintenance/route.ts:54-60), pas d'isolation par ligne comme spécifié (dé-risqué par l'index partiel). |
| H-DB-2 RGPD Conversation/Message survivent | ✅ CORRIGÉ | Purge dans delete-account/route.ts:118-135 + organizations/[orgId]/route.ts:201-205 (Message cascade). |
| H-DB-3 rétention AuditLog contradictoire | ✅ CORRIGÉ | cron/prune-audit-log/route.ts:44-49 exempte billing.* + shopify.webhook.*. |
| #4 index AuditLog/DealThread | ⚠️ PARTIEL | AuditLog(resource,resourceId,createdAt) + (userId,createdAt) + DealThread(listingId) posés (schema.prisma:939,941,2035). Mais FK Dispute.resolvedById / SavedListing.listingId toujours non indexées (contraintes ≠ index). |
🟠 Hautes — ops / frontend
| ID | Statut | Preuve |
|---|---|---|
| H-OPS-1 split-brain secret auth | ✅ CORRIGÉ | proxy.ts:278,324 : NEXTAUTH_SECRET || AUTH_SECRET ; miroir inverse env/server.ts:516-520,547-551 fallback sur les deux. |
H-OPS-2 aucun next build avant deploy | ⚠️ PARTIEL | CI a maintenant typecheck + lint + test (.github/workflows/ci.yml:26,44,60). Mais toujours aucun job build — une casse build-only (RSC, import server/client, route config) atteint main et n'échoue qu'au deploy. ignoreBuildErrors:true toujours là (next.config.mjs:22). |
| H-OPS-3 previews peuvent muter le schéma prod | ❌ OUVERT | vercel-build.mjs:44-63 (db push) et instrumentation-node.ts:52 (heal cold-start) sans garde VERCEL_ENV==='production'. Mitigé seulement opérationnellement (Neon n'expose pas l'URL au build, push non destructif). |
| #4-crons stampede minuit/04:00 | ✅ CORRIGÉ | vercel.json étalé : pire slot = 3 jobs à 0 2 (rarement simultanés) ; reset-credits 8 0, daily-bonus 5 0. 47/47 crons mappés avec maxDuration. |
H-FE-1 contact form mailto: | ✅ CORRIGÉ | contact-form.tsx:43 → POST /api/contact (Node, zod, rate-limit, Resend). |
H-FE-3 force-dynamic annule l'ISR intelligence | ⚠️ PARTIEL | intelligence/[category]/[slug] corrigé (revalidate=21_600 + noindex). Mais 16 pages marketing encore force-dynamic + contradiction generateStaticParams+force-dynamic sur [category]/page.tsx:56,60 (cf. nouveaux constats). |
| H-FE-2 / H-FE-4 / H-FE-5 | ⏳ REPORTÉ (confirmé ouvert) | Pas de next/dynamic sur / + oneLight toujours bundlé ; 0 useTranslations sur tout (dashboard) ; tooltips data-tip du hub (améliorés clavier via use-shared-tooltip.ts mais toujours hors registry + #fff en dark-only). |
SEO / divers (déjà signalés 13/06)
- ✅ Canonical racine
/retiré (layout.tsx:90-96) ; ✅ lien affilié footerrel="...sponsored"(app-footer.tsx:111). - ❌
howToJsonLdtoujours jamais émis (helperlib/seo/structured-data.ts:353sans appelant : drift CLAUDE.md « Tutorial detail (How-To JSON-LD) »). - ❌ Sitemap toujours sans
/status,/sell,/sellers/[id], détails tutorials/courses/docs (sitemap.ts:44-107).
Nouveaux constats (non couverts le 13/06)
🟠 Hautes
| # | Constat | Référence |
|---|---|---|
| N1 | Outils navigateur sans RBAC + DOM brut non fencé. browserGetDOM renvoie le outerHTML (≤64 K) d'une page arbitraire au modèle, seuls les <script> retirés — vecteur d'injection texte/attributs/commentaires, non fencé (tools/browser/get-dom.ts:93). Les 6 outils navigateur (navigate/click/type/scroll/screenshot/get-dom) mutent l'état web externe sans canWrite — seul le backstop autonomie protège, un viewer peut piloter le navigateur. | tools/browser/* |
| N2 | Order.listing sans onDelete → Restrict implicite. Toutes les autres relations de MarketplaceListing sont Cascade ; supprimer/purger un listing ayant une commande lève une violation FK et bloque la suppression. Incohérent avec l'intent cascade, non géré côté app. | prisma/schema.prisma:2056 |
| N3 | Scan cron tierExpiresAt non indexé. marketplaceListing.updateMany (featured-expiry + tier-downgrade) scanne tierExpiresAt sans index support (schema.prisma:1903-1909 couvre type/status/featured et status/tier, pas tierExpiresAt) → full scan ×2 par run quotidien. | cron/marketplace-maintenance/route.ts:35-50 |
| N4 | Pas de job build en CI (cf. H-OPS-2) — c'est la clé de voûte sur laquelle repose ignoreBuildErrors, et elle n'existe pas. | .github/workflows/ci.yml |
🟡 Moyennes
| # | Constat | Référence |
|---|---|---|
| N5 | Token Notion en clair dans un cookie, sans session ni liaison tenant. Écrit brut (httpOnly+secure mais non chiffré), seul IP rate-limit + CSRF state ; quiconque a la session du navigateur a un token Notion exploitable. Bypasse la couche de chiffrement DB ; lecteurs downstream (chat/actions/notion.ts, api/health/chat/notion) le lisent brut. | auth/notion/callback/route.ts:77-82 |
| N6 | Exports org/me fuient les secrets (= H-SEC-5 ouvert) : include brut → Store.apiKey (clair) + identifiants Stripe sérialisés dans la réponse. | organizations/[orgId]/export/route.ts:31-37, me/export/route.ts:36,39 |
| N7 | Overflow int4 sur les colonnes argent marketplace. Offer.amount/counterAmount, Order.amount/fee, MarketplaceListing.askingPrice/recurringPrice sont Int (max ~21,47 M$ en cents) ; les Zod des prix listing n'ont aucune borne haute et les offres autorisent .max(100_000_000_00) ($100 M) → « integer out of range » → 500 non géré. | api/marketplace/offers/route.ts:32, api/vendor/marketplace/listings/route.ts:57-58, checkout/route.ts:171 |
| N8 | commerce-tools (8 outils) sans canRead ni re-check orgId : revenu, segments RFM clients, attribution lisibles par un viewer. getAttributionByUtm renvoie les chaînes UTM (visiteur-contrôlables) brutes, non fencées. | tools/commerce-tools.ts:~421 |
| N9 | runTrackingScan / searchShopifyDocs : sans RBAC + sortie scrapée non fencée. runTrackingScan écrit un AuditLog + déclenche un scan d'URL externe sans gate, renvoie summary/stack détectés bruts ; searchShopifyDocs renvoie du markdown Firecrawl (≤4000 c.) brut. | tools/tracking-scan.ts:109,159,272, tools/shopify-docs.ts:148-153 |
| N10 | MemoryEvent orphelin + rétention PII. Aucune FK vers le fact parent (factId/scopeId = strings) alors que le commentaire schéma promet « cascade-delete RGPD » → supprimer un fact/org/user orphelinise chaque MemoryEvent (PII dans oldValue/newValue Json survit). Jamais pruné non plus (croissance illimitée). | prisma/schema.prisma:485-498 |
| N11 | StripeEvent.payload (PII) jamais purgé. Stocke le corps complet de l'event Stripe (email, adresse, métadonnées carte) indéfiniment ; commentaire schéma promet un job de rétention inexistant. L'idempotence n'a besoin que de event.id. | prisma/schema.prisma:769-786 |
| N12 | Message.content non purgé à la suppression de compte pour les conversations non-possédées. Seules les convos userId/orgId du user sont supprimées ; dans une convo store multi-membre possédée par un autre membre, les messages tapés par l'utilisateur supprimé survivent (authorUserId null mais content PII intact). | prisma/schema.prisma:1331, delete-account/route.ts |
| N13 | 16 pages marketing en force-dynamic (le correctif ISR du [slug] n'a pas été propagé) : SSR par visite, TTFB/CWV dégradés sur pages indexables ; intelligence/[category]/page.tsx:60 définit generateStaticParams() que force-dynamic annule silencieusement. | intelligence/{page,radar,lookup}, [category]/page.tsx, community/{events,forum,levels}, roadmap, changelog, sponsor/*, feedback |
| N14 | Lint non-bloquant + pas de garde VERCEL_ENV + fetch proxy sans timeout : toutes les règles ESLint en warn sans --max-warnings 0 (package.json:15) ; push/heal schéma sans garde env (vercel-build.mjs:40, instrumentation-node.ts:52) ; proxy.ts:285,368 fetch /api/auth/onboarding-status sans AbortSignal.timeout (stall site-wide possible). | (multi) |
| N15 | Couleurs/locale hardcodées dashboard : panneaux bg-[#000000]/bg-[#121212] (shell-client.tsx:865,899) hors tokens ; donut RFM 12 hex littéraux + label LABEL_FR français en dur (customer-segment-donut.tsx:22-36) dans un dashboard sinon 100 % anglais. | (multi) |
🟢 Basses / Info
| # | Constat | Référence |
|---|---|---|
| N16 | IDOR cross-tenant ThemeBranch : POST vérifie l'org de storeId puis themeBranch.update({where:{id:branchId}}) avec branchId du body jamais lié à storeId — un membre peut bumper lastActiveAt de la branche d'une autre org. Utiliser updateMany({where:{id,storeId}}) + 404 (pattern déjà présent ailleurs). | stores/[storeId]/branches/route.ts:144-147 |
| N17 | Bug fonctionnel Custom App / MCP key : l'écriture pose metadata.type="custom-app" (tiret) mais le générateur de clé exige "custom_app" (underscore) → génération de clé MCP cassée (« Connect Custom App first ») pour toute connexion légitime ; mis-bucketing aussi dans status-by-store. | custom-app/route.ts:134 vs mcp-key/route.ts:81 |
| N18 | Relay MCP : mutations destructive non gardées — seul critical est refusé (register-tools.ts:200-212), les writes destructifs (productUpdate, priceUpdate, discount/inventory) s'exécutent sans approbation pour tout client OAuth externe. Inverser en allowlist. | register-tools.ts:200-212 |
| N19 | Extraction mémoire sans scrubbing PII : emails/téléphones/noms (y compris WhatsApp d'expéditeurs arbitraires) persistés comme facts et ré-injectés dans les prompts. | memory/extract.ts |
| N20 | Cap mid-stream aveugle aux sub-agents : handler-cost-guard.ts:64-108 ne compte que le stream parent ; les generateText spécialistes ont leur propre AbortController (delegate-tool.ts:206), jamais relié au cost-guard → un tour team peut dépasser le solde / MAX_CONVERSATION_USD. | handler-cost-guard.ts, delegate-tool.ts:215 |
| N21 | Divers : _debug (userId/orgId) sur 403 (custom-app/route.ts:78) ; status/route.ts:84-93 métadonnées connexion sans auth (UUID-bearer, pas de secret) ; console.log non-leveled sur le hot path OAuth/email (identifiants seulement, pas de secret) ; Order.stripeTransferId sans @unique (idempotence Stripe seule) ; outils run* (aeo/aov/vitals) sans canWrite ; pool pg non configuré (database.ts:55) ; noUncheckedIndexedAccess absent du tsconfig ; image *.myshopify.com sans pathname. | (multi) |
Risque structurel reporté (confirmé ouvert, déjà documenté)
- Payout vendeur immédiat vs fenêtre dispute 14 j (M3) : le transfer Connect part au
checkout.session.completed(webhooks.ts:719) ; le reversal au refund est best-effort (refunds.ts:102-117) → si le vendeur a déjà retiré son solde Connect, la plateforme paie le refund. Pas d'escrow jusqu'à fin de fenêtre. - Webhook Stripe 500 sur échecs permanents (metadata manquante) → retries Stripe ~3 j (
webhooks/stripe/route.ts:64-70) ; pas de perte d'argent (StripeEvent idempotent) mais bruit. Distinguer permanent (200+DLQ) de transitoire. - next-auth v4 sur Next 16 / React 19 : matrice non supportée ; migration Auth.js v5 planifiée (
docs/ops/security-roadmap-2026-q3.md§2). - Tables sans pruning :
ListingEvent,CronExecution,Scan/ScanArtifact(en plus de N10/N11). - Suppression org/compte : cascade ~100 tables en
$transactionau timeout défaut 5 s (delete-account/route.ts:48) → P2028 sur grosse org.
Plan d'action priorisé
P0 — argent (les garde-fous doivent couvrir tous les chemins, pas juste le chat web)
- H-BILL-5 : étendre
reserveCredits(advisory lock) aux sub-agents délégués, canaux API/WhatsApp et EVI, c'est l'exposition de surcharge la plus directe. - H-SEC-7 / H-BILL-4 : auth + gate crédits + estimate pré-stream + cap mid-stream sur EVI et les canaux ; rate-limit l'API channel.
- N20 : relier le cost-guard parent aux spécialistes (propagation de l'abort).
- M3 : différer le payout vendeur post-fenêtre dispute (ou garantir le
reverse_transfer).
P1 — sécurité / fuite / injection
5. N5 (Notion) : exiger une session, lier au tenant via IntegrationConnection chiffré (comme les autres connecteurs).
6. H-SEC-5 / N6 : select whitelist sur organizations/[orgId]/export + me/export (retirer Store.apiKey + ids Stripe).
7. H-SEC-8 : masquer l'anonymat au data-layer (toCardData/search/recommendations), slug opaque, exclure du profil public.
8. C3.4 / N1 / N8 / N9 / N18 / N19 : compléter fenceUntrusted() sur tous les ingress (DOM navigateur, intelligence concurrents, RAG, facts mémoire, Shopify Admin, scan/docs) ; canWrite/canRead sur browser/commerce/run* tools ; allowlist sur le relay MCP ; scrubbing PII mémoire.
9. N16 : binder themeBranch.update à storeId (updateMany+404).
P2 — base de données / intégrité
10. N2 : rendre explicite onDelete sur Order.listing (Restrict assumé ou SetNull).
11. N7 : borner les Zod money au plafond int4 (ou migrer en BigInt/Decimal).
12. N3 / #4-FK : index tierExpiresAt, Dispute.resolvedById, SavedListing.listingId.
13. N10 / N11 / N12 : FK réelles ou purge explicite MemoryEvent ; rétention StripeEvent.payload ; redaction Message.content des convos non-possédées.
14. Pool pg configuré ; timeout sur la transaction de suppression cascade.
P3 — ops / frontend / dette
15. N4 / H-OPS-2 : job build en CI (la garantie sur laquelle repose ignoreBuildErrors).
16. H-OPS-3 / N14 : garde VERCEL_ENV sur push/heal ; --max-warnings 0 (ou promouvoir les règles clés) ; timeout sur le fetch proxy.
17. N13 : remplacer force-dynamic par revalidate sur les 16 pages marketing ; résoudre la contradiction generateStaticParams.
18. SEO : émettre howToJsonLd ; compléter le sitemap (/status, /sell, /sellers, détails community).
19. N15 : tokens couleur dashboard + i18n (ou assumer le dark/anglais-only).
20. Custom App spelling (N17), _debug 403, retirer oneLight.
Ce qui reste remarquablement solide
- Mutation IA bien défendue : C3.1-3 corrigés, autonomie role-gated server-side, wrapper
wrap-tools-with-autonomybackstop sur chaqueexecute, accès store cross-tenant bloqué en amont (C1). - Comptabilité crédits : FIFO par lots en forme close, hold/usage atomiques, settlement exactly-once.
- Idempotence Stripe : machine d'états
StripeEvent, plan résolu par price id, grants tax-exclusive, clawback en delta, flip PAID conditionnel sur PENDING. - Tenant-auth :
getStoreAccess/getOrgAccesscentralisés et appliqués partout ; pattern anti-IDOR explicite (keys/[keyId],intelligence/api-keys), réponses secrets en booléenshasX. - Crons : 47/47 derrière
withCronAuth, mappés 1:1 àvercel.json,maxDurationpartout, stampede résolu. - Outillage : typecheck/lint/test/knip verts, 0 vulnérabilité prod, 0 secret commité,
.env.exampletemplate propre. - Code mort : knip en CI bloquant, l'audit dead-code (PR #274) tient (allowlist vide hors guard généré + vendor ai-elements).