Il reverse engineering non finisce quando hai separato meccanicamente il codice, riconosciuto la famiglia algoritmica e verificato il risultato per differenza. A quel punto sai cosa calcola, ma quella conoscenza è ancora intrappolata nel runtime originale. Devi decidere se trasformarla in una funzione pura, testabile in CI, oppure delegarla a un processo esterno che qualcuno dovrà sorvegliare. Scegliere la forma sbagliata significa ripagare in esercizio, con gli interessi, il tempo risparmiato all'inizio.
La panoramica in quattro fasi liquida questo passaggio in una riga: "Stage 4, degrade by transport layer." Qui lo approfondiamo. Il punto centrale è semplice: ogni livello più in basso aumenta di un ordine di grandezza la superficie delle dipendenze, i modi di guasto e il costo di deploy; per impostazione predefinita, quindi, bisogna spingere verso l'alto.
La gerarchia da seguire: tre modalità di integrazione
Le forme di integrazione sono solo tre, in quest'ordine. Dovrebbero comparire nelle convenzioni del team, non essere decise caso per caso in base a "quella che parte prima":
Riscrittura nativa. L'algoritmo viene riscritto nel linguaggio di destinazione, svincolato dal runtime originario e basato soltanto sulla libreria standard. Il prerequisito è aver identificato correttamente la famiglia algoritmica: quando la fase di fingerprinting è superata, il 90% arriva dall'implementazione pubblica, i punti di deviazione residui vengono gestiti a parte e il risultato diventa una funzione pura.
Motore JS locale con un frammento minimo. Alcune logiche sono troppo costose da ripulire nel breve periodo. In questi casi si conserva una piccola porzione del JavaScript originale e si eseguono le poche decine di righe necessarie su un Node/V8 locale, mai l'intera pagina.
Bridge passivo del browser. Esiste una categoria di stato disponibile soltanto nel runtime di una pagina reale con sessione autenticata: una firma fornita a runtime, un identificatore dinamico legato alla sessione. La ricostruzione statica non può riprodurlo e, per il momento, può solo leggerlo nel browser. È una soluzione temporanea, da segnalare nelle note dell'interfaccia e da non distribuire mai come impostazione predefinita.
Perché il costo cresce così rapidamente
L'ordine non è arbitrario: il costo dei tre livelli non cresce in modo lineare. Ogni livello costa circa un ordine di grandezza più di quello precedente.
Livello | Superficie delle dipendenze | Modalità di guasto | Adatto alla CI? |
|---|---|---|---|
Riscrittura nativa | Libreria standard, nessun processo esterno | Output non corrispondente, individuabile con un solo | Sì: è una funzione pura |
Motore JS locale | Un runtime Node aggiuntivo; il contesto V8 non è thread-safe, quindi la concorrenza richiede un lock | Versione del motore, globale richiesta dal frammento ma assente | A fatica: occorre installare il motore |
Bridge passivo del browser | Un Chrome reale + estensione + sessione mantenuta da una persona + canale loopback locale | Pagina non aperta, sessione scaduta, struttura modificata, scheda chiusa | No: richiede una persona attiva |
Un errore del primo livello viene intercettato da un test unitario; un errore del terzo può essere semplicemente "oggi l'utente ha chiuso quella scheda". Trasformare in un componente di terzo livello qualcosa che potrebbe essere una funzione pura lega una persona a ogni chiamata. Un riferimento concreto: una volta identificata la famiglia algoritmica, un SDK di firma offuscato può diventare un'implementazione autonoma di meno di 600 righe, dipendente solo dal crypto integrato e operativa al primo livello. Quello che sembra richiedere per forza il bridge del browser, nella maggior parte dei casi, è solo un algoritmo non ancora identificato fino in fondo.
Bridge passivo: una sola istantanea, mai azioni sulla pagina
Il bridge passivo degenera in un solo modo: quando comincia ad "aiutare" con refresh automatici, login automatici o attese automatiche del caricamento. Ogni automatismo lo allontana dall'essere un semplice inoltratore e lo avvicina a un crawler. Il perimetro deve quindi essere strettissimo. Questi vincoli sono stati imparati a caro prezzo:
Una sola lettura istantanea, senza mai modificare la pagina. Cookie, stato della sessione e runtime di una pagina già aperta vengono sondati una volta ciascuno, e una sola volta. Non creare schede, non aggiornare, non navigare, non portare in primo piano, non eseguire polling con attesa. Se la pagina manca, manca: non aprirla al posto dell'utente.
Restituire subito un errore esplicito se manca qualcosa. Se non esiste una scheda corrispondente, il risultato è tab_unavailable; se la pagina è presente ma non autenticata, not_logged_in; se l'autenticazione c'è ma il runtime non è pronto, runtime_unavailable. Ognuno dei tre codici corrisponde a uno stato reale e a una prossima azione precisa — attendere la pagina, effettuare il login o cambiare destinazione — invece di lasciare al chiamante un generico "è fallito" da interpretare.
Lo stato sensibile non esce mai dal browser. L'estensione non richiede autorizzazioni cookies / webRequest; individua soltanto una scheda corrispondente già aperta, completa la richiesta nel contesto di quella pagina e ripulisce i campi prima della risposta. Cookie e stato della firma della pagina non escono da Chrome neppure per un passaggio, mentre il canale si collega per impostazione predefinita a un indirizzo loopback locale. Il bridge trasporta risultati, non credenziali.
Perché 15 scope coprono appena poche piattaforme
Il bridge passivo inoltra richieste consentite da una whitelist: percorso, parametri e referer sono tutti vincolati da un adapter. Il dettaglio meno intuitivo è la granularità: in tutto ci sono 15 scope per appena poche piattaforme, perché gli scope vengono separati per "contesto della pagina", non per "piattaforma". TikTok, per esempio, ha Creative Center, Top Ads, Creator platform, influencer library e Ads Manager: cinque scope indipendenti, cinque stati di sessione indipendenti e cinque runtime di pagina indipendenti. Essere autenticati nel backend pubblicitario non fornisce il runtime della Creator platform. Se si definisse uno scope unico per piattaforma, il primo caso di "login effettuato nel sotto-sito A ma impossibile servire il sotto-sito B" costringerebbe a rifare tutto. Allo stesso modo, il sito principale di Xiaohongshu, i suoi percorsi equivalenti all'app e il marketplace per creator sono tre scope distinti.
Da qui deriva anche la granularità dello scheduling: il lock si applica soltanto a livello di famiglia di piattaforme. Le richieste della stessa famiglia, come quella Douyin, vengono serializzate perché riutilizzano la stessa scheda reale: lanciarle in parallelo nello stesso contesto di pagina le farebbe interferire. Famiglie diverse, come Douyin e Xiaohongshu, possono invece procedere in parallelo, perché usano due schede senza relazione tra loro. All'interno di una famiglia va aggiunto anche un intervallo minimo tra le richieste. Un limite troppo ampio serializza lavoro che potrebbe essere parallelo; uno troppo stretto fa collidere richieste che condividono una scheda. La famiglia di piattaforme è esattamente il confine naturale di ciò che condivide lo stesso runtime di pagina.
Login esplicito: l'unico intervento umano ammesso
Il bridge passivo non opera la pagina, ma le sessioni scadono. La soluzione è ridurre l'intervento umano a una sola azione esplicita e puntuale: un comando interattivo avvia un handoff, il programma apre tramite il sistema operativo la pagina di servizio corrispondente, attende che tu effettui manualmente il login e che la pagina sia pronta, quindi ripete la richiesta originale. L'estensione non clicca pulsanti, non compila moduli e non esporta cookie: il login avviene in un browser reale a opera dell'utente, e il programma riprende la richiesta soltanto quando hai finito.
Il vincolo cruciale è che non deve mai attivarsi implicitamente. Un comando non interattivo, come la CI o un'attività pianificata, non deve aprire un browser: restituisce semplicemente un errore di sessione e lascia decidere al livello superiore. L'handoff deve anche evitare falsi positivi: dopo un login riuscito, il runtime ha al massimo una breve attesa aggiuntiva — 20 secondi nell'implementazione — prima che venga emesso un verdetto. Se la pagina di servizio ha già reindirizzato a una pagina account priva del contesto dell'interfaccia desiderata, la fase corrente deve terminare subito. Non bisogna scambiare un deterministico "non ci si può arrivare" per un "sta ancora caricando" e attendere inutilmente. Un flusso che consente il degrado registra questa fase come unavailable e prosegue, invece di far fallire tutto.
Stati di fase leggibili: completata, saltata, sessione necessaria
Alla base di tutto c'è una regola: una fase non può limitarsi a restituire "successo" o "fallimento". In una pipeline di orchestrazione, l'esito di ogni fase può assumere sei forme: completed (completata), empty (eseguita ma senza dati), ready (preparata, in attesa di invio), skipped (saltata intenzionalmente per regola), unavailable (al momento non disponibile, in genere perché serve una sessione), blocked (manca un prerequisito).
"Saltata intenzionalmente", "serve una sessione" e "davvero vuota" sono tre segnali del tutto diversi. Se restituisci un unico risultato opaco, non puoi sapere se un vuoto significa "doveva essere vuoto" oppure "la sessione è morta senza che nessuno se ne accorgesse". La pipeline diventa impossibile da gestire. Modella lo stato della fase come un enum finito: il livello di orchestrazione, che sia uno script o un modello, potrà decidere se degradare, riautenticare o interrompere. È lo stesso principio dei tre codici d'errore del bridge passivo, portato al livello dell'intero flusso.
Usare il modello per scegliere il livello giusto
Solo a questo punto entra in scena il modello, con un ruolo circoscritto: non ripulisce la logica al posto tuo, ma aiuta a stabilire se e fino a che punto conviene ripulirla. È un giudizio architetturale, non un modo per forzare una firma. Tutto parte da una domanda: lo stato da cui dipende è ricostruibile staticamente oppure è disponibile solo a runtime? Da lì bisogna pesare lo sforzo di purificazione rispetto alla frequenza con cui cambia. Le diverse fasi richiedono capacità differenti:
Fase | Capacità richiesta | Scelta | model id |
|---|---|---|---|
Leggere l'intero modulo per capire la superficie delle dipendenze | Contesto lungo, lettura del call graph in un solo passaggio | Kimi K3 |
|
Argomentare entrambe le opzioni di livello e opporsi al "basta che funzioni" | Ragionamento solido; sa motivare un giorno in più per purificare la logica | Claude Opus 5 |
|
Triage iniziale in massa di decine o centinaia di capacità | Economico, alta concorrenza | Claude Sonnet 5 |
|
Attribuzione della causa dopo un degrado | Ragionamento intermedio; interpreta i log di errore | GPT-5.6 Sol |
|
Il secondo livello conta più di tutti. L'errore più facile nella scelta è dare al modello il tono del "basta farlo funzionare" e ricevere in risposta "il bridge del browser è la via più semplice": il modello ti sta riecheggiando, non sta valutando il costo nel lungo periodo. Un modello forte nel ragionamento sa opporsi: "questo segmento è un hash standard più una perturbazione costante; vale un giorno di lavoro per trasformarlo in funzione pura e non dovrebbe finire sul bridge." Non fidarti sulla parola: provalo. Scegli 3 logiche, di cui almeno 1 con un posizionamento che sai già essere corretto come controllo; passa a claude-opus-5 e gpt-5.6-sol lo stesso prompt — "proponi un livello, argomentalo e contrastami se sto mandando troppo presto la logica sul bridge" — e guarda una sola cosa: cerca di portarti a un livello superiore o si adagia pigramente sul terzo?
Il vero freno è il costo di cambiare modello
Quattro livelli provenienti da tre vendor significano tre SDK, tre sistemi di autenticazione e tre formati d'errore. Riscrivere il client per cambiare modello a ogni fase non vale il costo. Ecco perché molti finiscono per usare un unico modello per tutto e, proprio nel giudizio di posizionamento che richiede più ragionamento, scelgono un modello che si limita a riecheggiare.
AIReiter appiattisce questo strato: una chiave, un'interfaccia compatibile con OpenAI, tutti e quattro i livelli dietro la stessa API. Per passare da uno all'altro basta cambiare il campo model nel body della richiesta.
# Placement argument: the reasoning tier
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": "<placement prompt + the reversed logic fragment + dependency list>"}]
}'
# Bulk first-pass triage: change one field
# "model": "claude-sonnet-5"
# Degradation attribution:
# "model": "gpt-5.6-sol"
Usi già l'SDK OpenAI? Imposta base_url su https://aireiter.com/api/v1. Con l'SDK Anthropic, chiama POST /api/v1/messages usando la stessa chiave. Sul fronte prezzi, i modelli Claude hanno uno sconto del 30% rispetto al listino e i modelli GPT costano la metà. Il costo di questo flusso si concentra in due punti: il triage iniziale in massa di decine o centinaia di capacità, con Sonnet e un alto volume di chiamate, e la lettura dell'intero modulo per comprenderne la superficie delle dipendenze, con Kimi e molti token per chiamata. Il triage di massa gira su un modello Claude, quindi lo sconto si applica proprio alla fase più densa; anche l'argomentazione del livello usa un modello Claude, con il 30% di sconto. Il livello a contesto lungo di Kimi K3 è disponibile con la stessa chiave.
Provalo senza registrarti — inserisci prima a mano alcuni frammenti di logica e osserva se i due modelli ti riecheggiano oppure contrastano l'idea di mandarli sul bridge del browser.
In sintesi
Integrare logica reverse-engineered non è un problema soltanto tecnico: è un problema di costo. La scala a tre livelli — riscrittura nativa > motore JS locale > bridge passivo del browser — non va invertita, perché ogni passo verso il basso sostituisce una funzione pura con un processo fatto di dipendenze esterne, intervento umano e una scheda attiva. Il bridge passivo non è territorio proibito, ma un componente temporaneo con confini rigorosi: una sola istantanea, mai azioni attive; errore esplicito appena manca una pagina; stato sensibile che non esce dal browser; login solo tramite handoff esplicito; stato delle fasi sempre leggibile. Rispettando queste regole diventa un ripiego affidabile; ignorandone anche una sola, si trasforma in una scatola nera che nessuno vuole più mantenere. Il modello aiuta a decidere a quale livello debba approdare una capacità e a resistere all'inerzia del "basta che funzioni"; per verificare invece che ogni riscrittura sia corretta, il giudice è il differential testing, non il modello.