Vous avez vidé un bundle webpack entier, découpé l’AST, recensé les primitives terminales ; les empreintes, vous savez désormais les lire — la routine que vous maîtrisez déjà. Pourtant, une valeur de signature dans un en-tête de requête vous résiste : elle change à chaque fois, et vous cherchez la fonction qui la produit. Vous passez au crible tous les opérateurs binaires suspects et tous les squelettes de hash dans les assets statiques, sans rien trouver.
Ce n’est pas que votre recherche manque de rigueur. La logique en question ne se trouve tout simplement pas dans le code que vous avez extrait.
Il existe une catégorie de signatures pour lesquelles l’analyse statique échouera forcément : l’algorithme lui-même n’apparaît qu’à l’exécution. Face à ce type de cible, il ne faut pas s’acharner à chercher une fonction inexistante. Il faut changer d’approche : au lieu de rétroconcevoir l’algorithme, exécutez-le ou lisez sa valeur, avec le moins de surface possible.
Deux indices qu’une analyse statique ne suffira pas
Avant d’en tirer cette conclusion, vérifiez que vous êtes bien face à ce cas de figure et non devant un élément que vous auriez simplement manqué. Deux indices sont particulièrement faciles à repérer.
Premier indice : la valeur change à chaque session ou à chaque requête, sans qu’aucun asset statique ne la génère. Vous la voyez dans le trafic réseau, différente après chaque actualisation ; vous téléchargez tous les .js, lancez une recherche plein texte, et ne trouvez nulle part où elle est assemblée. Le code qui l’assemble est livré au runtime.
Second indice : la valeur est bien trouvable comme littéral dans le bundle, mais celle que vous avez relevée hier ne fonctionne déjà plus aujourd’hui. C’est une simple chaîne codée en dur dans la sortie de build, encore utilisable si vous la copiez le jour même. Quelques jours plus tard, l’endpoint renvoie une erreur, et vous constatez que cette soi-disant « constante » a été remplacée par un autre littéral, lui aussi trouvable. Elle est figée au build, mais renouvelée à chaque release du frontend.
Ces deux indices ont un point commun : la vue statique n’est qu’un instantané, tandis que la réalité est un flux qui évolue au fil du temps ou des sessions. Soit vous cherchez le code sans rien trouver, soit vous le trouvez mais sa valeur est vivante. Dans les deux cas, c’est l’hypothèse « extraire une fois statiquement et copier » qui ne tient plus. Le problème n’est pas de « ne pas reconnaître la famille d’algorithmes ». Ce n’est pas votre matching d’empreintes qui est insuffisant : l’élément à comparer n’est pas présent dans la copie du code que vous avez entre les mains.
Type 1 : le serveur vous livre un challenge à exécuter
Dans le premier cas, l’accès à un endpoint commence par la livraison, par le serveur, d’un fragment de JavaScript à usage unique. Ce script s’exécute dans le navigateur, génère un cookie ou un token, et vous devez l’emporter avec vous pour passer. Son contenu varie généralement selon la session, parfois même selon la requête.
L’analyse statique est ici vouée à échouer : la logique est livrée à l’exécution et n’existe tout simplement pas dans le bundle statique. Un script capturé à un instant donné n’est qu’une occurrence. Le code livré aujourd’hui et celui qui le sera demain peuvent être totalement différents ; le rétroconcevoir revient à viser une cible mouvante.
La bonne stratégie consiste à l’exécuter comme une boîte noire. Inutile de comprendre son calcul : il suffit de lui fournir un environnement assez réaliste pour qu’il produise son résultat, puis de récupérer celui-ci. En pratique :
Montez un shim minimal d’environnement navigateur : des stubs vides pour
document,location,navigator,cookie, ainsi que quelques Observers et timers, complétés uniquement jusqu’à ce que le script cesse d’échouer sur l’absence d’un objet global.Exécutez le source livré dans une sandbox Node/V8 locale (
node:vm, ou un processus lancé via execjs), avec un délai d’exécution maximal.Lors de son exécution, le script écrit un cookie — ou place une valeur dans un global. Interceptez cette écriture et récupérez le token dont vous avez besoin.
Le contrôle visiteur de Zhihu suit ce modèle : un script livré par le site dépose un identifiant visiteur dans les cookies du navigateur. Vous simulez l’environnement DOM pour lui faire croire qu’il tourne dans une vraie page, puis récupérez la valeur une fois l’exécution terminée. Vous n’avez rétroconçu aucune ligne de l’algorithme ; vous avez seulement installé un décor suffisamment crédible pour le laisser jouer sa propre scène. Le coût ne réside donc pas dans la « compréhension de l’algorithme », mais dans le fait de « maintenir un environnement suffisamment réaliste ». Le script sonde navigator.webdriver, vérifie l’existence d’un nœud, attend une réponse d’une API : votre shim doit le tromper avec précision, sans devenir trop volumineux à maintenir. C’est précisément cet arbitrage que traite la section sur la « surface d’exécution minimale » plus bas.
Type 2 : l’identifiant est dans le code, mais change à chaque release
Le second cas est l’inverse : la valeur est bel et bien présente dans le bundle statique sous forme de littéral, mais elle est produite au build et renouvelée à chaque release du frontend.
Le cas classique est celui d’un frontend moderne qui utilise des requêtes préenregistrées plutôt que du GraphQL en clair. Chaque opération — charger la timeline, récupérer les résultats de recherche — est associée, au moment du build, à un identifiant d’opération ou de requête transmis dans le chemin de l’endpoint. Cet identifiant est trouvable dans la sortie de build, mais dès qu’une nouvelle version du frontend est publiée, la même opération reçoit une nouvelle valeur.
L’analyse statique vous donne ici une fausse impression de réussite. Vous avez trouvé l’identifiant, vous l’avez copié, il fonctionnait ce jour-là, alors vous l’avez codé en dur. Deux semaines plus tard, l’endpoint retourne une 400 et vous découvrez seulement alors que cette « constante » était vivante. C’est plus insidieux que le premier type, car vous étiez convaincu d’avoir déjà trouvé ce qu’il fallait.
La bonne approche n’est pas de l’inverser — il n’y a aucun algorithme à inverser, c’est une constante de build — mais de lire la valeur actuelle depuis la page actuelle au moment de la requête, puis de la mettre en cache pour la session. Les méthodes de lecture s’échelonnent de la moins coûteuse à la plus lourde ; il faut les essayer dans cet ordre :
Examinez les ressources déjà chargées par la page. Celle-ci vient d’envoyer une requête contenant cet identifiant : sa valeur actuelle figure donc dans cette URL, qu’il suffit d’extraire de l’enregistrement de ressource. C’est l’option la moins coûteuse : vous lisez un fait existant, sans toucher à une ligne d’algorithme.
Si cette voie ne fonctionne pas, extrayez-le du source du script à l’aide d’un motif. Repérez dans le bundle courant la déclaration qui associe un nom d’opération à un identifiant, puis récupérez cette valeur.
Seulement en dernier recours, explorez la table des modules du bundler, ou récupérez et parsez les bundles associés à partir d’indices. C’est l’approche la plus coûteuse.
Les identifiants d’opération de timeline et de recherche de Twitter se lisent exactement ainsi : on les récupère dans l’URL d’une requête GraphQL déjà envoyée par la page, puis on les met en cache pour toute la session.
La version systématique de cette méthode — les différentes façons d’obtenir une requête préenregistrée, leur coût et le choix adapté selon le contexte — est détaillée dans l’article sur les opérations persistées. Ici, retenez simplement qu’elle appartient à la famille plus large des valeurs dérivées à l’exécution.
Ce qui distingue réellement les deux cas
Mis côte à côte, ces deux scénarios indiquent clairement la marche à suivre.
Type 1 : le challenge | Type 2 : l’identifiant dynamique | |
|---|---|---|
Où se trouve la vérité | Livrée à l’exécution, jamais dans le code statique | Dans le code statique, mais renouvelée à chaque release |
Ce qu’il faut faire | L’exécuter : lancer le véritable algorithme et récupérer son effet de bord | La lire : localiser et relever une constante, sans exécuter d’algorithme |
À quoi ressemble l’échec | Shim d’environnement trop léger, le code n’arrive pas au bout | Lecteur mis en défaut après une réorganisation du bundle ; valeur obsolète ou vide |
Qui déclenche le changement | Le serveur, à tout moment | Une release frontend, selon le rythme de déploiement |
En une phrase : les deux cas invalident l’idée d’« extraire une fois statiquement et copier » ; la seule différence est de savoir si le code doit réellement être exécuté. Identifier le type détermine directement si la prochaine étape est une sandbox ou un extracteur.
Pourquoi la purification n’est généralement pas le bon réflexe
On pourrait objecter : ne peut-on pas rétroconcevoir intégralement l’algorithme du script livré, ou reconstruire totalement la règle de génération de l’identifiant dynamique, afin d’en faire une implémentation native indépendante du runtime d’origine ? C’est la purification de l’étape quatre du workflow en quatre étapes : la forme la plus propre, un investissement unique pour une indépendance durable, intégrée à la CI.
Pour ces deux types de cibles, la réponse est généralement non : l’opération ne rentabilise pas son coût. Voici un modèle de retour sur investissement approximatif, mais utile :
La purification entraîne un coût unique C — rétroconcevoir l’algorithme et le valider par tests différentiels. Une fois réalisée, elle économise s par unité de temps par rapport à « exécuter ou lire à chaque fois ». Le délai de retour sur investissement est approximativement C / s.
La variable décisive n’est ni C ni s. C’est le cycle de changement T de la cible, autrement dit la fréquence à laquelle elle évolue.
T < C/s : la cible change avant que l’investissement soit amorti ; l’implémentation purifiée cesse de correspondre quelques jours après sa mise en production, et vous devez recommencer. Le retour est négatif.
T est très supérieur à C/s : la purification est un gain évident. Un investissement reste valable longtemps, et vous avez intérêt à gravir l’échelle de purification vers le niveau de réécriture native.
Les deux types de cibles se placent naturellement à des endroits différents :
Pour le type 1, T est contrôlé par le serveur et peut être arbitrairement court. Le serveur peut modifier à tout moment la logique du script livré, sans que vous puissiez le prévoir. Ce cas se situe donc presque toujours du côté « exécuter ». Forcer la purification d’un algorithme que quelqu’un peut changer demain revient à dépendre de son rythme de release.
Pour le type 2, T correspond au cycle de release du frontend, de quelques jours à quelques mois. Plus votre lecteur est robuste — quelques fallbacks supplémentaires, des ancres plus stables — plus C/s diminue. Les deux options peuvent alors être raisonnables : il faut réellement faire le calcul.
Voilà tout le sens du titre. Si vous ne trouvez pas la logique dans le code, ce n’est pas forcément parce que vous l’avez manquée. Il se peut qu’il n’existe aucun algorithme stable qui mérite d’être purifié.
Définir une surface d’exécution minimale
Une fois la décision prise d’« exécuter » plutôt que de « purifier », l’objectif d’ingénierie change. Il ne s’agit plus d’obtenir quelque chose de propre, mais de réduire la surface d’exécution au minimum, tout en la gardant contrôlée et attribuable. Cela repose sur trois principes.
N’exécutez que le fragment indispensable. N’embarquez pas tout le runtime de la page. Fournissez seulement le code dont dépend réellement l’algorithme, complété par un shim minimal. La taille du shim a un point d’équilibre : il doit être assez petit pour laisser le code s’exécuter sans exception. Chaque stub supplémentaire crée une nouvelle charge de maintenance — ils modifient une sonde, vous devez suivre — tandis que chaque stub manquant provoque un crash immédiat. N’importez pas un navigateur entier par facilité : sinon, vous ne maintenez plus un signataire, mais la moitié d’un navigateur.
Verrouillez le contexte. Un contexte V8/Node compilé n’est pas thread-safe. Un challenge à usage unique ouvre naturellement une sandbox neuve à chaque exécution, sans interférence. Mais dès que vous réutilisez un contexte compilé pour économiser le coût de démarrage, ou mettez en cache un parseur d’identifiant dynamique partagé entre requêtes, les appels concurrents doivent être sérialisés :
class RuntimeSigner:
def __init__(self, source: Path) -> None:
self._context = execjs.get("Node").compile(source.read_text())
self._lock = threading.Lock() # V8 context is not thread-safe
def call(self, fn: str, *args):
with self._lock:
return self._context.call(fn, *args)
Les échecs doivent être attribuables. C’est le point le plus souvent négligé, et pourtant celui qui fait gagner le plus de temps lorsqu’on récupère des valeurs par exécution. Un échec doit indiquer à quelle couche il se produit :
Environnement trop léger : le code lève une
ReferenceErrorou reste bloqué jusqu’au timeout. Votre shim ne fournit pas un élément qu’il attend.Protocole modifié : le code termine et produit une sortie, mais son format est incorrect ou le JSON ne peut pas être parsé. Le format de sortie a changé.
Sémantique non respectée : vous avez une valeur, elle se parse, mais elle est incomplète ou échoue à la validation lors de son utilisation. L’algorithme lui-même a été modifié.
Ces trois situations demandent trois corrections entièrement différentes : compléter le shim, suivre le protocole ou vérifier l’algorithme. Un exécuteur qui se contente de signaler « échec » vous oblige à repartir de zéro à chaque incident. Un exécuteur de challenge mature les distingue explicitement : échec d’exécution, sortie non parsable, résultat incomplet et timeout sont quatre erreurs distinctes.
Quel modèle utiliser à chaque étape
Le rôle du modèle diffère ici de celui qu’il joue lors du travail d’empreinte. Dans ce dernier cas, il identifie une famille d’algorithmes. Ici, il n’y a aucun algorithme à identifier : sa mission principale est d’aider à trancher l’architecture entre « purifier » et « exécuter », puis à attribuer les échecs d’exécution. Les quatre sous-étapes sollicitent des capacités très différentes :
Sous-étape | Capacité requise | Choix | model id |
|---|---|---|---|
Décider entre purifier et exécuter, en argumentant les deux positions | Raisonnement solide, capable de contester sa propre conclusion | Claude Opus 5 |
|
Déterminer où se cache l’identifiant dynamique : localiser le point d’injection de la constante et lire les candidats dans le bundle | Contexte long, lecture du bundle complet en une fois | Kimi K3 |
|
Filtrer en masse les scripts candidats susceptibles de contenir l’opération recherchée | Faible coût, centaines d’appels en forte concurrence | Claude Sonnet 5 |
|
Lors d’un échec d’exécution, lire le log ou la stack pour identifier la couche défaillante | Raisonnement intermédiaire, explication à partir d’une erreur précise | GPT-5.6 Sol |
|
La première ligne mérite d’être soulignée, car c’est la seule étape de cet article où changer de modèle modifie visiblement le résultat. Elle évalue précisément la capacité à défendre les deux options, puis à argumenter contre soi-même — la même capacité que la section consacrée aux contre-indices dans le travail d’empreinte. Un modèle plus faible choisit une voie et accumule les arguments qui la confortent, sans jamais examiner sérieusement l’autre. Un bon modèle de raisonnement pousse « purifier » et « exécuter » jusqu’au bout, formule pour chacun l’argument le plus fort et le principal mode d’échec, puis conclut par comparaison.
Ne vous contentez pas de me croire : testez la différence.
Prenez une cible réelle pour laquelle vous avez déjà pris la décision, comme contrôle : vous savez intuitivement si elle doit être exécutée ou purifiée.
Donnez les faits observés — logique livrée à l’exécution ou constante de release, fréquence de changement, profondeur des dépendances d’environnement — à
claude-opus-5etgpt-5.6-sol. Demandez à chacun une note de décision « purifier ou exécuter ».Examinez deux points : le modèle a-t-il identifié le « cycle de changement » comme variable décisive ? S’il se contente de comparer la difficulté d’implémentation, il échoue. Et les conditions qu’il donne pour changer d’avis sont-elles observables ? Une formule du type « si l’identifiant cesse de changer à la prochaine release, revenez à la purification », accompagnée d’un signal déclencheur, est exploitable.
Un seul essai suffit pour voir quel modèle prend une décision, et lequel se contente de décider à votre place.
Le vrai frein, c’est le coût de bascule
Quatre modèles venant de trois fournisseurs, trois SDK, trois schémas d’authentification et trois formats d’erreur. Réécrire votre client trois fois uniquement pour changer de modèle entre les sous-étapes n’en vaut pas la peine. La plupart des gens utilisent donc un seul modèle tout au long du processus, confient « purifier ou exécuter » à un modèle incapable de s’opposer à lui-même, se lancent tête baissée dans la purification d’une cible que l’autre partie modifiera demain, puis font un long détour avant de réaliser que la direction était mauvaise dès le départ.
AIReiter élimine cette couche : une clé, une interface compatible OpenAI et les quatre niveaux derrière elle. Il suffit de modifier le champ model dans le corps de la requête pour changer de modèle.
# Purify or execute: the reasoning tier arguing both sides
curl https://aireiter.com/api/v1/chat/completions \
-H "Authorization: Bearer $AIREITER_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "claude-opus-5",
"messages": [{"role": "user", "content": "<observed facts + argue the strongest case for both purify and execute>"}]
}'
# Locate the dynamic identifier across the bundle: change the model field, leave the rest
# "model": "kimi-k3"
# Bulk-filter candidate scripts:
# "model": "claude-sonnet-5"
# Attribute an execution failure:
# "model": "gpt-5.6-sol"
Si vous utilisez déjà le SDK OpenAI, pointez base_url vers https://aireiter.com/api/v1 sans rien modifier d’autre. Avec le SDK Anthropic, utilisez POST /api/v1/messages avec la même clé.
Côté prix, la consommation de tokens de ce workflow est concentrée. Kimi K3 lit le bundle frontend entier pour localiser l’identifiant dynamique, soit quelques centaines de milliers de tokens en entrée, tandis que Claude Sonnet 5 filtre les scripts candidats en masse, avec facilement des centaines d’appels. Ces deux usages représentent l’essentiel de la facture. La réduction de 30 % sur Claude s’applique précisément au filtrage de masse avec Sonnet et à l’argumentation de décision avec Opus ; GPT à moitié prix couvre l’attribution des échecs avec GPT-5.6 Sol ; et la lecture long contexte du bundle complet par K3 reste accessible avec la même clé. La remise porte sur le lot le plus lourd en tokens et sur le niveau de raisonnement le plus coûteux, pas sur une économie générique sans impact.
Essayer sans inscription : commencez par rédiger à la main quelques notes de décision « purifier ou exécuter », comparez les deux modèles sur leur capacité à reconnaître le cycle de changement, puis décidez s’il faut l’intégrer.
Pour conclure
Une analyse statique qui ne donne rien ne remet pas toujours vos compétences en cause. Parfois, c’est la nature même de la cible : la signature n’est pas dans le code parce qu’elle est livrée à l’exécution, ou parce qu’elle change à chaque release.
Pour ces deux cas, cessez de chercher une fonction qui n’existe pas. Avec un challenge, exécutez-la au minimum, dans une sandbox juste assez réaliste pour produire un résultat. Avec un identifiant dynamique, lisez-le à l’exécution et mettez-le en cache pour la session. Les deux approches commencent par le même constat : la vision de l’« instantané statique » ne fonctionne plus.
Et le choix entre « purifier » et « exécuter » relève entièrement de l’architecture : il dépend d’une seule variable, à savoir si le cycle de changement de la cible est plus court que votre délai de retour sur investissement initial. Pour les cibles au cycle court, surtout celles dont le serveur peut livrer une nouvelle logique à tout moment, forcer la purification produit un retour négatif. Confiez l’argumentation des deux positions à un niveau de raisonnement capable de se contredire lui-même ; ne tranchez pas à l’intuition, et ne laissez pas le modèle purifier à votre place. La purification est une discipline d’ingénierie déterministe, dont la validation relève des tests différentiels, comme l’expliquent les étapes trois et quatre du workflow en quatre étapes. Le modèle ne vous aide ici qu’à réfléchir à une seule question : cela vaut-il vraiment la peine d’être rétroconçu ?