ADRADR-0044 · Mirror reconstruit une section, il ne copie pas une page

ADR-0044 — Mirror reconstruit une section, il ne copie pas une page

Etend le tableau des surfaces du stage de l'ADR 0032 : mirror est un sixieme mode de la scene, a cote de Preview, Worktree, Builder, Workflow et Studio. Il n'est ni un panneau du Builder, ni une couche de la vitrine.

Statut

Accepté · 2026-09-26

Piliers : ai-platform

Etend le tableau des surfaces du stage de l'ADR 0032 : mirror est un sixieme mode de la scene, a cote de Preview, Worktree, Builder, Workflow et Studio. Il n'est ni un panneau du Builder, ni une couche de la vitrine.

Contexte

Un marchand voit sur un autre site une section qui lui plait (un hero, une grille de cartes, un bandeau de reassurance) et veut « la meme » sur sa boutique. Deux risques, de nature differente, rendent la reponse evidente dangereuse :

  • Securite. Afficher la page d'un tiers dans le tableau de bord, c'est executer du balisage choisi par un inconnu sur l'origine qui porte la session. Le proxy de la Preview (/api/preview/[storeId]) tourne volontairement sans sandbox, parce que la vitrine du marchand a besoin de ses cookies ; il ne peut pas servir de modele pour une page tierce. Un srcdoc n'est pas une issue non plus : il herite de la CSP du tableau de bord, donc les scripts de capture n'ont pas le nonce, les images et polices tierces sont bloquees en production et tout fonctionne en developpement (CSP en report-only), ce qui est le pire des deux mondes.
  • Droit. Copier le HTML, le CSS, les images, les polices ou le texte d'un site tiers, c'est reproduire une oeuvre protegee (droit d'auteur sur le texte et les visuels, licence des polices, parfois la marque). L'outil deviendrait un outil de contrefacon, et c'est BoostEcom qui l'aurait ecrit, heberge et facture.

Décision

Mirror mesure une section et la reconstruit en section Shopify OS 2.0 a la marque de la boutique ; il n'exporte jamais un fichier du site source.

  1. Isolation par l'en-tete. Le snapshot est servi par GET /api/stores/[storeId]/mirror/frame avec une CSP d'EN-TETE (jamais <meta>, jamais report-only) qui commence par sandbox allow-scripts : origine opaque, donc aucun acces aux cookies, au stockage ni au DOM du tableau de bord, et Origin: null refuse par checkStateChangingOrigin. script-src ne porte que le nonce de la reponse : le seul script qui tourne est capture-script.ts. frame-ancestors 'self', connect-src 'none', form-action 'none', base-uri 'none'. L'iframe cote client porte en plus sandbox="allow-scripts", jamais allow-same-origin.
  2. Lecture sous garde. L'URL passe validateScanUrl au saut 0 puis safeFetch (SSRF revalide a chaque redirection), text/html seulement, corps lu en flux sous MIRROR_MAX_HTML_BYTES. Le HTML est assaini avant d'etre servi : scripts, gestionnaires on*, frames, objets, formulaires et javascript: retires, URLs absolutisees, liens rendus inertes.
  3. Protocole par identite, pas par origine. Le cadre poste vers l'origine explicite de l'application ; le parent n'accepte un message que si event.source === iframe.contentWindow ET que le jeton de chargement correspond. Le parent commande le cadre avec la cible * et n'y met jamais de secret. La capture est validee par zod (capture-schema.ts) avant tout calcul : elle a ete fabriquee dans une page que nous ne controlons pas.
  4. Rien du tiers ne sort. Le compilateur n'emet ni image, ni SVG, ni police, ni feuille de style, ni script du site source : les images deviennent des reglages image_picker vides, les icones des glyphes integres, les fonds url() un degrade de marque. Le texte devient un placeholder de meme longueur, sauf si le marchand coche « je detiens les droits sur ce texte » (copy: "keep" exige rightsAttested: true, refuse en 422 sinon). Les couleurs sont reexprimees comme melanges des trois roles de marque de la boutique (fond, texte, accent), exposes en reglages color modifiables ; les polices restent celles du theme.
  5. Themes non publies seulement. L'installation passe decideThemeWrite sans allowLiveTheme : un theme live repond 409, sans echappatoire. Un fichier existant n'est jamais ecrase (le handle porte un hash). Le theme cible est choisi parmi les brouillons ; aucun theme n'est cree en silence.
  6. Plan payant. Les deux routes lisent resolveEntitlement ; le mode est verrouille pour Free par PaidCanvasOverlay comme les autres etapes.

Alternatives écartées

OptionPourquoi non
Afficher la page par le proxy de la PreviewIl tourne sans sandbox et avec les cookies de session : du balisage tiers y aurait l'origine du tableau de bord (le trou decrit en tete de api/preview/[storeId]/route.ts).
<iframe srcdoc sandbox="allow-scripts">Herite de la CSP du parent : pas de nonce pour notre script, images et polices tierces bloquees en production, violations envoyees en masse a /api/security/csp-report, et un comportement different en developpement.
Elargir buildCsp (img-src https:)Casse la garde csp-creatives.test.ts et elargit la politique de TOUT le tableau de bord pour une seule surface.
Exporter le HTML/CSS assaini tel quelC'est une copie : droits d'auteur, licences de polices, images hotlinkees. Et un fichier Liquid illisible pour le marchand.
Un panneau dans le Builder (Onlook)Code vendore // @ts-nocheck epingle par des tests ; ses cadres rendent des documents de theme, pas une page tierce opaque.
Ecrire sur le theme live « avec confirmation »La regle du depot est qu'une ecriture live exige un drapeau explicite ; Mirror n'a aucune raison de le porter. Le marchand publie son brouillon lui-meme.

Conséquences

  • Fidelite partielle, assumee : ce qui sort est une structure reconstruite (grille, espacements, hierarchie, roles de couleur), pas un pixel perfect. Une police tierce, une animation ou une video n'y sont pas, et le rapport le dit (warnings).
  • Un site qui refuse les requetes sans cookie, ou qui ne rend rien sans JavaScript, donne un snapshot vide : mirror:ready porte emptyBody et l'ecran le dit. Rendre la page dans Browserbase serait la suite, avec son cout par session.
  • Les polices tierces d'un CDN qui ne repond pas Access-Control-Allow-Origin: * retombent sur une police systeme dans le snapshot (requete Origin: null). C'est l'affichage de la source qui en patit, pas la section produite.
  • Signal pour revisiter : un marchand qui doit systematiquement « garder le texte » pour obtenir une section utilisable, ou des sections installees que personne n'ajoute a un template.