Tüm webpack bundle'ını indirdiniz, AST'yi parçaladınız, yaprak primitifleri listelediniz; artık bildiğiniz rutinle parmak izlerini okuyabiliyorsunuz. Ardından bir istek başlığındaki imza değeriyle karşılaşıyorsunuz. Her seferinde değişiyor ve onu üreten fonksiyonu bulmak istiyorsunuz. Tüm statik varlıklarda şüpheli bit işlemlerini ve hash iskeletlerini arıyorsunuz, fakat sonuç yok.
Sorun yeterince dikkatli aramamış olmanız değil. Aradığınız mantık, indirdiğiniz kodun içinde yok.
Statik analizin kaçınılmaz olarak boş döneceği bir imza sınıfı var: algoritmanın kendisi yalnızca çalışma anında ortaya çıkıyor. Böyle bir hedefte, var olmayan bir fonksiyonu aramayı sürdürmek yerine yaklaşımı değiştirmek gerekir. Algoritmayı tersine çevirmeyin; mümkün olan en küçük kapsamla çalıştırın veya değerini okuyun.
Statik analizin doğru hedefte boş döndüğünü gösteren iki işaret
Önce gerçekten bu tür bir hedefle uğraştığınızdan emin olun; belki de yalnızca bir ayrıntıyı gözden kaçırdınız. Ayırt etmesi kolay iki belirti bulunuyor.
İlk belirti şu: Değer her oturumda veya her istekte değişir, ancak statik varlıklar onu üretmez. Ağ isteğinde değeri görürsünüz, her yenilemede farklıdır; tüm .js dosyalarını indirip tam metin araması yaptığınızda ise değeri oluşturan hiçbir yer bulamazsınız. Çünkü bu kod çalışma anında gönderiliyordur.
İkinci belirti ise farklıdır: Değer bundle içinde literal olarak aranabilir durumdadır, ama dün not ettiğiniz değer bugün işe yaramaz. Derleme çıktısında düz bir string sabiti görür, o gün kopyalayıp kullanabilirsiniz. Birkaç gün sonra endpoint hata verir; geri dönüp baktığınızda o “sabitin” yerini yine aramayla bulabileceğiniz başka bir literal almıştır. Değer derleme sırasında sabitlenmiştir, ancak her frontend sürümünde yenilenir.
Bu iki belirtinin ortak noktası şu: Statik görünüm bir anlık görüntüdür, gerçekteki bilgi ise zamanla ya da oturuma göre değişen bir akıştır. Kodu ararsınız ve hiçbir şey bulamazsınız; ya da bulursunuz ama değer canlıdır. Her iki durumda da çalışmayan yöntem, “bir kez statik dump al ve kopyala” yaklaşımıdır. Bu, “algoritma ailesini tanıyamıyorum” probleminden farklıdır. Parmak izi eşleştirmeniz zayıf olduğu için değildir; eşleştireceğiniz şey, elinizdeki kod kopyasında bulunmuyordur.
Birinci tür: Sunucunun çalıştırmanız için gönderdiği challenge
İlk tür şöyle çalışır: Belirli bir endpoint'e erişebilmeniz için sunucu önce size tek kullanımlık bir JS parçası gönderir. Betik tarayıcıda çalışır, bir cookie veya token üretir; devam edebilmek için bunu taşımanız gerekir. Betiğin içeriği genellikle oturuma göre, bazen de isteğe göre değişir.
Statik analizin neden kesin olarak boş döneceği açık: Mantık çalışma anında gönderilir, statik bundle'da zaten yoktur. Yakaladığınız bir betik yalnızca o ana ait tek örnektir. Bugün dump aldığınız kod ile yarın gönderilen kod birbirinden tamamen farklı olabilir; tersine mühendislik yaptığınız şey hareketli bir hedef olur.
Doğru yaklaşım, betiği kara kutu olarak çalıştırmaktır. Ne hesapladığını anlamanız gerekmez; yalnızca sonuç üretecek kadar gerçekçi bir ortam sağlamanız ve sonucu almanız yeterlidir. Pratikte şunları yaparsınız:
Asgari bir tarayıcı ortamı shim'i kurun:
document,location,navigator,cookieile birkaç Observer ve timer için boş stub'lar tanımlayın. Betik eksik bir global nesne yüzünden hata vermeyi bırakana kadar bunları doldurun.Gönderilen kaynak kodu, yürütme zaman aşımı tanımlanmış yerel bir Node/V8 sandbox'ında çalıştırın:
node:vmya da execjs tarafından başlatılan bir süreç kullanılabilir.Betik çalışırken bir cookie yazar veya bir değeri global alana koyar. Bu yazımı yakalayın ve ihtiyacınız olan token'ı buradan alın.
Zhihu'nun ziyaretçi kontrolü tam olarak bu yapıdadır: Gönderilen betik tarayıcı cookie'sine bir ziyaretçi kimliği bırakır. DOM ortamını, gerçek bir sayfada çalıştığına inanacağı kadar shim'lersiniz; işini tamamladığında değeri alırsınız. Bu süreçte algoritmanın tek satırını bile tersine çevirmiş olmazsınız; yalnızca kendi oyununu sergileyebileceği kadar inandırıcı bir sahne kurarsınız. Dolayısıyla maliyet “algoritmayı anlamakta” değil, “ortamı yeterince gerçek tutmakta” oluşur. Betik navigator.webdriver değerini yoklayabilir, bir node'un varlığını kontrol edebilir, bir API'den sonuç bekleyebilir. Shim'iniz onu tam gerektiği kadar kandırmalı, fakat bakımı imkânsız bir boyuta da ulaşmamalıdır. Aşağıdaki “minimum yürütme yüzeyi” bölümü bu dengeyi ele alıyor.
İkinci tür: Kodda bulunan ama her sürümde değişen dinamik kimlik
İkinci tür bunun tersidir: Değer statik bundle'da literal olarak gerçekten vardır, ancak derleme sırasında üretilir ve her frontend sürümünde değişir.
Bunun klasik örneği, düz metin GraphQL yerine önceden kaydedilmiş sorgular kullanan modern frontend'lerdir. Her işlem — zaman akışını getirmek, arama sonuçlarını almak gibi — derleme zamanında üretilmiş bir operation veya query id ile eşlenir; bu kimlik endpoint yolunda taşınır. Bundle çıktısında aranabilir durumdadır, ama frontend yeni sürüm yayınladığı anda aynı işlemin id'si değişir.
Burada statik analiz size yanıltıcı bir “tamam, buldum” hissi verir. Değeri bulur, kopyalar, o gün çalıştırır ve hardcode edersiniz. İki hafta sonra endpoint 400 döndürür; ancak o zaman “sabit” sandığınız değerin aslında canlı olduğunu fark edersiniz. Birinci türden daha sinsi olmasının nedeni, onu çoktan bulduğunuzu düşünmenizdir.
Doğru strateji tersine mühendislik değildir; tersine çevrilecek bir algoritma yoktur, bu bir derleme sabitidir. Bunun yerine güncel değeri istek anında güncel sayfadan okuyun ve oturum boyunca cache'leyin. Okuma yöntemlerini maliyeti düşükten yükseğe sıralamak gerekir; önce erken basamakları deneyin:
Sayfanın zaten yüklediği kaynaklara bakın. Sayfa bu kimliği taşıyan bir isteği az önce gönderdi; güncel değer URL'nin içindedir ve kaynak kaydından çekilebilir. En ucuz yöntem budur: Algoritmaya dokunmadan, mevcut bir gerçeği okursunuz.
Buradan alamıyorsanız betik kaynağından pattern ile çıkarın. Güncel bundle içindeki “bu işlem adı şu id'ye eşlenir” tanımını bulun ve değeri alın.
Yalnızca bunlar başarısız olursa bundler'ın modül tablosunu inceleyin veya ipuçlarından ilgili bundle'ları indirip ayrıştırın. Maliyeti en yüksek yöntem olduğu için en sona kalmalıdır.
Twitter'ın zaman akışı ve arama operation id'leri tam olarak bu şekilde okunur: Sayfanın zaten gönderdiği GraphQL istek URL'sinden id alınır; ardından oturum boyunca cache'lenerek kullanılır.
Bu türün sistematik ele alınışı, önceden kaydedilmiş bir sorguyu elde etmenin kaç farklı yolu olduğu, bunların maliyetleri ve hangi durumda hangisinin seçileceği persisted operation'lar hakkındaki yazının konusu. Burada yalnızca bunun, çalışma anında türetilen değerler sınıfına ait olduğunu vurgulamak yeterli.
İki tür arasındaki net ayrım
İkisini yan yana koyduğunuzda, her biri için ne yapılması gerektiği belirginleşir.
Birinci tür: challenge | İkinci tür: dinamik kimlik | |
|---|---|---|
Asıl bilginin bulunduğu yer | Çalışma anında gönderilir; statik kodda hiç bulunmaz | Statik kodun içindedir, ancak her sürümde değişir |
Yapılacak iş | Çalıştırın: gerçek algoritmayı yürütün, yan etkisini alın | Okuyun: sabiti bulun ve alın, algoritma çalıştırmayın |
Hata biçimi | Ortam shim'i yetersizdir, kod tamamlanamaz | Bundle yeniden düzenlendiğinde okuyucu değeri kaçırır; eski veya boş değer alır |
Değişikliği kim tetikler? | Sunucu, istediği anda | Frontend sürümü, deployment takvimine göre |
Özet tek cümle: Her iki tür de “bir kez statik dump al ve kopyala” yaklaşımını bozar; fark yalnızca kodu gerçekten çalıştırmanız gerekip gerekmediğidir. Hangi türle karşı karşıya olduğunuzu bilmek, sıradaki adımın sandbox mı yoksa extractor mı olacağını doğrudan belirler.
Bu hedeflerde neden saflaştırmayı zorlamamalısınız?
Burada şu itiraz gelebilir: Gönderilen betiğin algoritmasını tamamen tersine çeviremez veya dinamik kimliğin üretim kuralını orijinal runtime'dan bağımsız native bir uygulamaya dönüştüremez misiniz? Dört aşamalı iş akışının dördüncü aşamasındaki saflaştırma budur: En temiz biçim, uzun vadede sıfır bağımlılık için tek seferlik yatırım ve CI'a taşınan çözüm.
Bu iki hedef türünde yanıt genellikle hayırdır; ekonomik değildir. Kaba ama işe yarar bir geri dönüş modeli şöyle kurulabilir:
Saflaştırmanın tek seferlik maliyeti C'dir: algoritmayı tersine çevirmek ve diferansiyel testle doğrulamak. Saflaştırma sonrasında “her seferinde çalıştır veya oku” yöntemine göre zaman birimi başına s tasarruf sağlanır. Geri ödeme süresi yaklaşık C / s olur.
Belirleyici değişken C veya s değil, hedefin ne sıklıkta değiştiğini gösteren değişim döngüsü T'dir.
T < C/s: Yatırım kendini geri ödemeden hedef değişir. Saflaştırılmış uygulama yayına çıktıktan birkaç gün içinde eşleşmeyi bırakır ve işi yeniden yapmanız gerekir. Getiri negatiftir.
T, C/s'den çok büyükse: Saflaştırma kesin kazançtır. Tek yatırım uzun süre dayanır; saflaştırma merdiveninde native-rewrite katmanına doğru ilerlemelisiniz.
Bu iki hedef türü doğal olarak farklı konumlara düşer:
Birinci türde T, sunucunun kontrolündedir ve keyfi biçimde kısa olabilir. Sunucu gönderdiği betiğin mantığını her an değiştirebilir; bunu öngöremezsiniz. Bu nedenle neredeyse her zaman “çalıştır” tarafında yer alır. Birinin yarın değiştireceği algoritmayı zorla saflaştırmaya çalışmak, kaderinizi onun sürüm takvimine teslim etmektir.
İkinci türde T, günlerden aylara uzanabilen frontend sürüm döngüsüdür. Okuyucunuz ne kadar dayanıklıysa — birkaç ek fallback, daha kararlı anchor'lar — C/s o kadar düşer. Bu nedenle iki taraf da makul olabilir; hesabı gerçekten yapmanız gerekir.
Başlığın asıl anlamı da budur. Mantığı kodda bulamıyorsanız, mutlaka bir ayrıntıyı kaçırdığınız anlamına gelmez. Saflaştırmaya değecek kararlı bir algoritma ortada hiç olmayabilir.
Minimum yürütme yüzeyi nasıl belirlenir?
“Saflaştır” yerine “çalıştır” kararını verdiğiniz anda mühendislik hedefi değişir. Amaç temiz bir çözüm değil; yürütme yüzeyini mümkün olan en küçük, kontrollü ve hatası izlenebilir hâle getirmektir. Bunun üç ayağı var.
Yalnızca gerekli parçayı çalıştırın. Tüm sayfa runtime'ını taşımayın. Sadece algoritmanın gerçekten bağlı olduğu kodu ve minimal shim'i verin. Shim boyutunun bir tatlı noktası vardır: Kodun hata atmadan çalışmasını sağlayacak kadar küçük olmalıdır. Her ek stub yeni bir bakım yüküdür; yoklamayı değiştirirlerse sizin de onu takip etmeniz gerekir. Eksik her stub ise kodu anında çökertir. Kestirme olsun diye tam bir tarayıcı ortamı taşımayın; aksi halde bakımını yaptığınız şey bir signer değil, yarım tarayıcı olur.
Context'i kilitleyin. Derlenmiş bir V8/Node context'i thread-safe değildir. Tek kullanımlık bir challenge için her seferinde yeni sandbox açmak doğaldır ve örnekler birbirine karışmaz. Ancak başlangıç maliyetini azaltmak için derlenmiş context'i yeniden kullanmaya veya dinamik kimlik parser'ını istekler arasında paylaşmak üzere cache'lemeye başladığınız anda eşzamanlı çağrıları serialize etmeniz gerekir:
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)
Hata kaynağı ayırt edilebilir olmalı. Çalıştırma temelli değer alma yaklaşımında en sık gözden kaçan, fakat en çok zaman kazandıran nokta budur. Bir hata, hangi katmanda oluştuğunu söylemelidir:
Ortam yetersizdir: Kod bir
ReferenceErrorfırlatır veya zaman aşımına kadar takılır. Shim'inizde betiğin beklediği bir şey eksiktir.Protokol değişmiştir: Kod tamamlanır ve çıktı alırsınız, ancak biçim yanlıştır ya da JSON ayrıştırılamaz. Çıktı formatı değişmiştir.
Anlamsal koşullar sağlanmamıştır: Bir değer alırsınız, ayrıştırılır; fakat eksiktir ya da kullandığınızda doğrulamadan geçmez. Algoritmanın kendisi değişmiştir.
Bu üç durum üç ayrı çözüm ister: shim'i düzeltmek, protokolü takip etmek veya algoritmayı kontrol etmek. Yalnızca genel bir “başarısız oldu” hatası bildiren executor, her seferinde soruşturmaya sıfırdan başlamanıza neden olur. Olgun bir challenge executor'ı yürütme hatasını, ayrıştırılamayan çıktıyı, eksik sonucu ve zaman aşımını ayrı hata türleri olarak tanımlar.
Hangi adımda hangi model kullanılmalı?
Bu akışta modelin rolü, parmak izi çıkarma çalışmasındakinden farklıdır. Orada model algoritma ailesini tanımlar. Burada tanımlanacak bir algoritma yoktur; modelin asıl görevi “saflaştır mı, çalıştır mı?” mimari kararına yardımcı olmak ve yürütme hatalarının kaynağını belirlemektir. Dört alt adım, modelden birbirinden tamamen farklı yetenekler ister:
Alt adım | Gereken yetenek | Tercih | model id |
|---|---|---|---|
Saflaştırma mı çalıştırma mı kararını vermek; iki tarafı da savunmak | Güçlü muhakeme, kendi tezine karşı argüman kurabilme | Claude Opus 5 |
|
Dinamik kimliğin saklandığı yeri bulmak: sabitin enjekte edildiği noktayı saptamak ve bundle genelinde adayları okumak | Uzun context, tüm bundle'ı tek seferde okuyabilme | Kimi K3 |
|
Hedef işlemi tutabilecek aday betikleri toplu olarak elemek | Düşük maliyet, yüksek eşzamanlılıkta yüzlerce çağrı | Claude Sonnet 5 |
|
Yürütme hatasında log veya stack'i okuyup hangi katmanın başarısız olduğunu belirlemek | Orta düzey muhakeme, belirli bir hatayı açıklayabilme | GPT-5.6 Sol |
|
İlk satır özellikle önemli; bu yazıda model değiştirmenin sonucu görünür biçimde değiştirdiği tek adım budur. Test edilen yetenek, her iki tarafı savunmak ve kendi tezine karşı çıkabilmektir; parmak izi çalışmasındaki karşı kanıt bölümüyle aynı yetenek. Daha zayıf bir model sizin yerinize bir yol seçer ve bu seçimi destekleyen gerekçeleri sıralar; diğer tarafı ciddi biçimde ele almaz. Güçlü muhakeme modeli ise hem “saflaştır” hem “çalıştır” seçeneğini sonuna kadar tartışır, her biri için en güçlü gerekçeyi ve hata biçimini yazar, ardından karşılaştırmaya dayalı bir sonuç verir.
Aradaki farkı bana inanarak değil, deneyerek görün:
Kararını daha önce verdiğiniz gerçek bir hedefi kontrol örneği olarak alın; bunun çalıştırılması mı, yoksa saflaştırılması mı gerektiğine dair sezgisel olarak zaten fikriniz vardır.
Gözlemlediğiniz bilgileri — mantığın çalışma anında gönderilip gönderilmediğini veya sürüm sabiti olup olmadığını, ne sıklıkta değiştiğini, ortam bağımlılığının ne kadar derin olduğunu —
claude-opus-5vegpt-5.6-solmodellerine verin. Her birinden “saflaştırma mı çalıştırma mı” karar notu yazmasını isteyin.İki noktaya bakın: Değişim döngüsünü belirleyici değişken olarak tanıdı mı? Yalnızca uygulama zorluğunu karşılaştırıyorsa kaybeder. Ayrıca fikrini tersine çevirmek için sunduğu koşullar gözlemlenebilir mi? “Kimlik bir sonraki sürümde değişmezse saflaştırmaya dön” gibi tetikleyici sinyal içeren koşullar kullanılabilirdir.
Tek tur sonunda hangi modelin karar verdiğini, hangisinin yalnızca sizin yerinize karar açıkladığını anlarsınız.
Asıl engel model değiştirme maliyeti
Üç sağlayıcıdan dört model; üç SDK, üç kimlik doğrulama şeması, üç hata formatı. Alt adımlar arasında model değiştirmek için istemcinizi üç kez yeniden yazmak mantıklı değildir. Bu yüzden çoğu kişi sürecin tamamında tek model kullanır; “saflaştır mı, çalıştır mı?” aşamasında kendi tezine karşı argüman üretemeyen bir modelle ilerler, karşı tarafın yarın değiştireceği hedefi saflaştırmaya balıklama dalar ve yönün en baştan yanlış olduğunu anlamadan önce uzun bir dolambaçlı yola girer.
AIReiter bu katmanı ortadan kaldırır: Tek anahtar, OpenAI uyumlu tek arayüz, arkasında dört katmanın tamamı. İstek gövdesindeki model alanını değiştirmeniz yeterlidir.
# 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"
OpenAI SDK kullanıyorsanız base_url değerini https://aireiter.com/api/v1 olarak ayarlayın; başka hiçbir şeyi değiştirmeniz gerekmez. Anthropic SDK tarafında ise aynı anahtarla POST /api/v1/messages çağrısını kullanın.
Fiyat açısından bu akışın token yükü belirli noktalarda yoğunlaşır: Kimi K3, dinamik kimliği bulmak için tüm frontend bundle'ını okur; her girdi birkaç yüz bin token'a ulaşabilir. Claude Sonnet 5 ise aday betikleri toplu olarak eler ve çağrı sayısı rahatlıkla yüzleri bulur. Toplam maliyetin büyük kısmını bu ikisi belirler. %30 indirimli Claude, toplu eleme işine (Sonnet) ve karar muhakemesine (Opus) denk gelir; yarı fiyatlı GPT, hata kaynağı belirleme işine (GPT-5.6 Sol) denk gelir. Tüm bundle'ı uzun context ile okuyan K3 de aynı anahtarla çağrılabilir. İndirim, genel bir ucuzluğa değil; token yükü en yüksek batch'e ve en pahalı muhakeme katmanına uygulanır.
Kayıt olmadan deneyin: Önce elle birkaç “saflaştırma mı çalıştırma mı” karar notu çalıştırın, iki modeli değişim döngüsünü tanıma bakımından karşılaştırın; ardından entegrasyon yapıp yapmayacağınıza karar verin.
Sonuç
Statik analizin boş dönmesi her zaman becerinizle ilgili değildir; bazen hedefin doğası böyledir. İmza kodda olmayabilir, çünkü çalışma anında gönderiliyordur ya da her sürümde değişiyordur.
Bu iki türde var olmayan fonksiyonu aramayı bırakın. Challenge türünde, sonuç üretecek kadar gerçekçi bir sandbox kurarak betiği minimum kapsamla çalıştırın. Dinamik kimlik türünde ise değeri çalışma anında okuyun ve oturum için cache'leyin. Her iki yolun ilk adımı, “statik anlık görüntü” yaklaşımının artık işe yaramadığını kabul etmektir.
“Saflaştırma mı, çalıştırma mı?” kararı ise tamamen mimari bir karardır ve tek değişkene bağlıdır: Hedefin değişim döngüsü, tek seferlik yatırımınızın geri ödeme süresinden kısa mı? Kısa döngülü hedeflerde, özellikle sunucunun her an yeni betik gönderebildiği türde, saflaştırmayı zorlamak negatif getiri yaratır. Bu iki taraflı tartışmayı kendi tezine karşı çıkabilen bir muhakeme katmanına verin; kararı kafadan vermeyin ve modelin sizin yerinize saflaştırmasına izin vermeyin. Saflaştırma deterministik bir mühendislik işidir; doğrulaması da diferansiyel testle yapılmalıdır. Bu konu, dört aşamalı iş akışının üçüncü ve dördüncü aşamalarında ele alınıyor. Modelin buradaki tek katkısı şudur: Bunun tersine mühendisliğe gerçekten değip değmediğini düşünmenize yardım etmek.