Oracle3
Open-source trading engine and MCP server for prediction markets. Fee-aware no-arbitrage trading across related contracts, live on Kalshi, Polymarket and Solana or on paper.
Upload your receipt, share the items among your friends, and work out what everyone owes.
FinanceTypeScript · PLpgSQLGitHub @SayloolOnlineOwner unconfirmed
Have you tried it? One tap is enough. No sign-in needed.
No updates detected yet. GitHub releases will appear here.
Owners of public repositories under a personal GitHub account can sign in to verify ownership and manage the introduction themselves. No administrator approval is needed.
Sign in, generate a verification file, and commit it to this repository’s default branch within 30 minutes. Files on other branches or forks do not count. Then check the file below to manage the project without administrator approval. Keep the file to retain your access.
If needed, you will go to sign-in. After signing in, return here and press your chosen verification button again.
Run /nomorevibe verify in your project folder to verify ownership and manage the introduction yourself.
Verify in one minuteAsk about other verification options
Loading the full README. The saved excerpt is shown below.
Ortak hesabı adil biçimde bölmek için küçük bir hackathon MVP'si. Hedeflenen akış: fişin fotoğrafını yükle → fişteki ürünleri kişilere dağıt → herkesin borcunu hesapla → Arc Testnet üzerinden USDC ile öde. Bu depo fiş analizi, paylaşım/borç hesabı, Arc Testnet ödeme akışı ve creator işlemleri için Google oturum temelini içerir. Kota uygulaması bu değişikliğin parçası değildir. Node.js 22 veya üzeri gerekir (Arc App Kit ileride bunu şart koşacak). .env.local içine kendi OpenAI API anahtarını yaz:Read the full README ↗
Ortak hesabı adil biçimde bölmek için küçük bir hackathon MVP'si. Hedeflenen akış: fişin fotoğrafını yükle → fişteki ürünleri kişilere dağıt → herkesin borcunu hesapla → Arc Testnet üzerinden USDC ile öde.
Bu depo fiş analizi, paylaşım/borç hesabı, Arc Testnet ödeme akışı ve creator işlemleri için Google oturum temelini içerir. Kota uygulaması bu değişikliğin parçası değildir.
Node.js 22 veya üzeri gerekir (Arc App Kit ileride bunu şart koşacak).
npm install
Ortam değişkenlerini hazırla:
cp .env.example .env.local
.env.local içine kendi OpenAI API anahtarını yaz:
| Değişken | Zorunlu | Açıklama |
|---|---|---|
OPENAI_API_KEY | evet | Fiş analizi için OpenAI anahtarı. Yalnızca sunucuda okunur. |
OPENAI_RECEIPT_MODEL | hayır | Kullanılacak model. Varsayılan: gpt-5.6-luna |
COINGECKO_DEMO_API_KEY | evet | USDC/TRY kuru için CoinGecko Demo anahtarı. Yalnızca sunucuda okunur. |
RATE_QUOTE_SECRET | evet | Kur teklifini imzalayan HMAC sırrı. Yalnızca sunucuda okunur. |
DATABASE_URL | hayır (Part 1) | Neon Postgres bağlantısı; paylaşılan ortak hesap deposu. Yalnızca sunucuda okunur. |
SHARED_BILL_AUTH_SECRET | evet (borçlu bağlantısı) | Cüzdan challenge zarfının HMAC sırrı. Yalnızca sunucuda okunur. |
APP_ORIGIN | üretimde evet | Güvenilen açık uygulama origin'i; OAuth ve cüzdan challenge hedefi istemci Host başlıklarından türetilmez. |
AUTH_SECRET | Google girişinde evet | Auth.js JWT/cookie şifreleme sırrı: tam 32 rastgele baytı temsil eden tam 64 küçük hexadecimal karakter (^[0-9a-f]{64}$). Yalnızca sunucuda okunur. |
AUTH_GOOGLE_ID | Google girişinde evet | Google OAuth Web client ID. Yalnızca sunucuda okunur. |
AUTH_GOOGLE_SECRET | Google girişinde evet | Google OAuth Web client secret. Yalnızca sunucuda okunur. |
NEXT_PUBLIC_WALLETCONNECT_PROJECT_ID | hayır | WalletConnect Cloud proje kimliği. Tanımlı değilse mobil cüzdan seçeneği hiç gösterilmez; masaüstündeki EIP-6963 akışı etkilenmez. Sır değildir, aşağıya bakın. |
ANDROID_PACKAGE_NAME | hayır | TWA paketinin adı (ters DNS). Tanımlı değilse /.well-known/assetlinks.json 404 döner. Yalnızca sunucuda okunur. |
ANDROID_APP_FINGERPRINTS | hayır | Virgülle ayrılmış SHA-256 sertifika parmak izleri. İKİSİ de gerekir: yükleme anahtarın ve Google'ın uygulama imzalama anahtarı. Yalnızca sunucuda okunur. |
Yukarıdaki sunucu değişkenlerinin hiçbiri NEXT_PUBLIC_ önekiyle
tanımlanmaz ve hiçbiri istemci paketine girmez.
Tek istisna NEXT_PUBLIC_WALLETCONNECT_PROJECT_ID'dir ve istisna olması
kasıtlıdır: WalletConnect projectId'si tanımı gereği tarayıcı paketinde
bulunur, çünkü eşleşme isteğini röleye gönderen taraf istemcidir. Bir sır
değildir, tek başına hiçbir şeye yetki vermez ve hiçbir sunucu sırrının yerine
geçmez. Sunucu sırlarının NEXT_PUBLIC_ ile tanımlanmaması kuralı
değişmemiştir.
AUTH_SECRET kriptografik rastgelelikle üretilmelidir. macOS'ta değeri terminal
çıktısına yazmadan doğrudan panoya almak için:
openssl rand -hex 32 | tr -d '\n' | pbcopy
Üretilen değer değiştirilmeden kullanılmalı; başında/sonunda boşluk, büyük hex
harfi veya başka karakter bulunmamalıdır. AUTH_SECRET, RATE_QUOTE_SECRET ve
SHARED_BILL_AUTH_SECRET üç ayrı sunucu sırrıdır; aynı değer paylaşılmaz.
Hiçbiri NEXT_PUBLIC_ adıyla tanımlanmaz veya istemciye gönderilmez.
APP_ORIGIN, AUTH_SECRET, AUTH_GOOGLE_ID ya da AUTH_GOOGLE_SECRET eksik
veya geçersizse Auth.js başlatılmaz. OAuth uç noktaları ile Google korumalı
creator API'leri ayrıntı sızdırmayan, Cache-Control: no-store taşıyan
503 SERVICE_NOT_CONFIGURED döndürür. Geçerli yapılandırmada yalnızca oturum
yoksa creator API'leri 401 AUTH_REQUIRED döndürür. Borçlu cüzdan ve kur
rotaları bu Google yapılandırmasından bağımsız kalır.
DATABASE_URL tanımlı olmasa bile uygulama ve testler derlenir; yalnızca
POST /api/shared-bills kontrollü bir 503 döner. Şema ve geçiş için
"Tek bağlantılı ortak hesap" bölümüne bak.
Anahtar tanımlı olmasa bile uygulama açılır ve fiş yükleme ekranı çalışır; yalnızca analiz isteği kontrollü bir "servis yapılandırılmamış" hatası döner.
npm run dev
Uygulama http://localhost:3000 adresinde açılır.
Uygulama /manifest.webmanifest yayınlar ve ana ekrana kurulabilir. Kurulan
uygulamanın adı Split Bill'dir. Marka adı ÇEVRİLMEZ: iki sözlükte de
aynıdır, çünkü uygulama her ülkede tek bir isimle tanınmalı — telefonda
ikonun altında görünen ad da budur ve o değer dile göre değişemez.
Manifest tek dillidir: <link rel="manifest"> varsayılan olarak çerez
göndermez, bu yüzden dil çerezine bakan bir manifest güvenilir çalışmaz.
Sayfa metinleri her zamanki gibi kullanıcının dilinde kalır.
Servis çalışanı YOKTUR ve bilerek yoktur. Sayfalar no-store; bir
önbellek ödeme sayfasını ya da imzalı bir yükü saklayabilir, o sınırı delerdi.
Çevrimdışı bir hedef de yok. Android'de "ana ekrana ekle" istemi çıkmazsa ilk
bakılacak yer burasıdır.
İşaret yalnızca geometriyle çizilir ve hiçbir fonta bağlı değildir:
kaynağı src/lib/brand/mark.ts, başlıktaki işaret ile uygulama ikonları aynı
yollardan üretilir. Daha önce bir metin düğümüydü, yani her cihazda o cihazın
fontuyla çiziliyordu.
İşaret bir para birimi simgesi taşımaz. Eskiden Türk lirası simgesiydi; bu hem uygulamayı tek bir ülkeye bağlıyordu hem de yanlıştı — ödemeler lirayla değil, Arc Testnet üzerindeki test USDC'siyle yapılıyor. Yerine yan yana duran, biri diğerinden kısa iki fiş kondu: hangi dil konuşulursa konuşulsun aynı şeyi anlatır — bir hesap, iki pay. Tutar satırları boya değil deliktir, bu yüzden işaret hangi zemine basılırsa basılsın doğru görünür.
İkonlar depoya işlenmiştir; çalışma zamanında hiçbir şey yeniden çizilmez. Geometri değişirse yeniden üretmek gerekir:
node scripts/generate-icons.mjs
Betik npm run build'in parçası değildir ve sharp'a ihtiyaç duyar.
sharp bir bağımlılık olarak tanımlı değildir; Next ile birlikte geldiği için
genelde bulunur, bulunmazsa betik bunu açıkça söyler.
İşaret maskelenebilir güvenli alanın (merkezi %80 çaplı daire) içinde kaldığı
için aynı görsel hem any hem maskable amacıyla bildirilir; ayrı bir
maskelenebilir dosya gerekmez. Bu, src/lib/brand/pwa-shell.test.ts içinde
ölçülür — geometri büyürse test kırmızıya döner.
Politika /privacy adresindedir, tek adres iki dil (uygulamanın geri kalanı
gibi dil yol ön eki yoktur). Play Console'a verilecek adres budur. Metnin
kendisi src/lib/legal/privacy.ts içindedir; sayfa yalnızca onu çizer.
Metin kaynak kodun denetiminden çıkarılmıştır. Süreler koddaki sabitlerden
türetilir ve privacy.test.ts şunları zorlar: kaynakta geçen her dış alan
adı politikada bildirilmiş olmalıdır, süreler sabitlerin karşılığı olmalıdır,
ve iki dil aynı belgeyi anlatmalıdır. Yeni bir servise bağlanan kod,
politikayı güncellemeden testlerden geçemez.
Süresi dolan ortak hesap kayıtları artık siliniyor. Süre dolunca hesap
açılamaz ve ödenemez; kayıt bir süre daha durur, sonra günlük çalışan bir
temizlikle (/api/cron/retention) borç satırlarıyla birlikte kaldırılır.
Süreler koddaki sabitlerden (src/lib/db/retention.ts) türetilir ve
politikada yazar; burada tekrarlanmaz.
Temizliğin iki ayrıntısı kazara bozulabilir:
shared_bill_payment_attempts, teklifleri
ON DELETE RESTRICT ile referanslıyor; tek bir cascade'li silme sıraya
bağlı olarak yabancı anahtar hatasıyla düşebilir. Silme, çocuktan
ebeveyne AÇIK sırayla ve tek işlemde yapılır.Uç CRON_SECRET ile korunur ve sır tanımlı değilse hiç çalışmaz.
Zincire yazılan hiçbir şey geri alınamaz. Silme hakkı oraya ulaşmaz; politika bunu yumuşatmadan söyler.
review_required bir borçBu duruma düşen her satır gerçek bir insanın "ödedim ama görünmüyor" anıdır: zincirde bir şey oldu, beklenen transferi kanıtlamadı, kilit kaldı.
GET /api/admin/review bekleyenleri en eski önce listeler; REVIEW_SECRET
ile korunur.
curl -sS -H "Authorization: Bearer $REVIEW_SECRET" https://<alan-adi>/api/admin/review
Yanıt bilerek dardır. Ölçüm ucu yalnızca toplam döndürür ve döndürmelidir;
burası farklıdır çünkü takılı bir ödemeyi çözmek için hangi kayıt olduğunu
bilmek şarttır. İstisna en dar hâliyle tutulur: yalnızca zincirde zaten açık
olanlar döner — adresler ve işlem hash'i — artı hesap kimliği ve tutar. İnsan
adları ve uygulama kullanıcısı kimliği dönmez; mutabakat zincire karşı
yapılır, kişiye karşı değil. METRICS_SECRET değil ayrı bir sır kullanır:
farklı duyarlılık, farklı patlama yarıçapı.
Şema review_required → paid geçişini anlatıyordu ama uygulayan bir kod yolu
yok — bu runbook yazılırken ölçüldü:
unknowndur ve ALLOWED_SETTLEMENTS.unknown boştur:
yeni bir makbuz doğrulaması bile onu paide taşıyamaz.unpaid olmasını
şart koşar.Yani "elle mutabakat" bir mecaz değil. İki yanıltıcı yorum bu sırada düzeltildi; eskisi takılı bir borcun kendiliğinden düzeleceğini sandırıyordu.
curl).txHash yoksa zincirde bakılacak bir şey yoktur: deneme hash
bildirilmeden belirsizleşmiş. Kullanıcıya cüzdan geçmişini sor — gerçekten
bir gönderim var mı?txHash varsa ArcScan'de aç ve dördünü birden doğrula:
debtor adresi mi,recipient adresi mi,microUsdc ile birebir eşit mi.paid
olmalıdır. Çıkış yalnızca gözden geçirilmiş bir veritabanı işlemidir:BEGIN;
UPDATE shared_bill_debts
SET payment_status = 'paid', paid_tx_hash = '0x...', paid_at = now()
WHERE bill_id = '0x...' AND lower(debtor_address) = lower('0x...')
AND payment_status = 'review_required';
-- 1 satır beklenir. Değilse ROLLBACK.
COMMIT;
paid yazmak, geri
dönüşü olmayan tek hatadır — paid hiçbir koşulda geri alınmaz.payment_status = 'review_required' ile sınırlıdır ki yanlış bir satır
yakalanmasın.debts.needsReview ve attempts.needsAttention sayaçlarına bak; artan bir
eğri genellikle RPC tarafında bir arıza demektir.Faz 5'te Play Console formu doldurulurken kullanılacak karşılıklar. "Paylaşım" Play'in tanımıdır: veri cihazdan çıkıp üçüncü bir tarafa ulaşıyorsa paylaşım sayılır.
| Play veri türü | Toplanır | Paylaşılır | Geçici mi | Zorunlu mu | Amaç |
|---|---|---|---|---|---|
| Kişisel bilgi → E-posta adresi | Evet | Hayır | Hayır | Google girişi seçilirse | Hesap yönetimi |
| Kişisel bilgi → Ad | Evet | Hayır | Hayır | Google girişi seçilirse | Hesap yönetimi |
| Fotoğraf ve video → Fotoğraflar | Evet | Evet (OpenAI) | Evet | Fiş yüklenirse | Uygulama işlevi |
| Finansal bilgi → Diğer finansal bilgi (cüzdan adresi, borç tutarı) | Evet | Evet (herkese açık blok zinciri) | Hayır | Evet | Uygulama işlevi |
| Uygulama etkinliği → Uygulama içi etkileşimler | Evet | Hayır | Hayır | Giriş yapıldıysa | Günlük analiz sınırı, kötüye kullanımın önlenmesi |
| Cihaz veya diğer kimlikler | Hayır | — | — | — | Reklam kimliği kullanılmaz |
| Uygulama bilgileri ve performansı → Tanılama | Evet | Hayır | Hayır | Hayır | CSP ihlal raporları |
En sık yanlış işaretlenen satırlar kalın olanlardır:
(hesap kimliği, gün, adet) olarak
veritabanında durur ve Play'in "uygulama içi etkileşimler" tanımına girer.Son iki madde bu rehbere sonradan eklendi. Play Console'daki gönderilmiş form bu iki satır için gözden geçirilmelidir; rehberi düzeltmek formu düzeltmez.
Güvenlik uygulamaları bölümü:
| Soru | Cevap |
|---|---|
| Veri aktarım sırasında şifreleniyor mu | Evet (HTTPS/TLS) |
| Kullanıcı silme talep edebiliyor mu | Evet |
| Uygulama içinde hesap silme yolu var mı | Evet, /account |
| Silme talebi için web adresi | https://splitstable.com/account |
| Bağımsız güvenlik incelemesinden geçti mi | Hayır |
Play, hesap açtıran uygulamalardan iki şey ister: uygulama İÇİNDE bir silme
yolu ve silme talebi için herkesin açabileceği bir web adresi. /account
sayfası ikisini birden karşılar; forma verilecek adres budur.
Oturum çerezi silme yanıtında ölür. Oturum bir JWT'dir ve 30 gün yaşar;
çerez temizlenmezse silinen hesap istek göndermeye devam eder. Çerez adlarını
elle üretmek yerine Auth.js kendi çerezlerini temizler (adlar ortama göre
değişir: __Secure- öneki, parçalı çerezler).
Ama bu yalnızca isteği gönderen tarayıcıyı kapatır. Sunucu bir JWT'yi
iptal edemez; başka bir cihazdaki oturum süresi dolana kadar geçerli kalır.
O yolu kapatan şey /api/receipts/analyze üzerindeki varlık kontrolüdür.
Neden yalnızca o rota: saved_contacts ve shared_bills tablolarının
app_users'a yabancı anahtarı var, silinmiş kullanıcının yazması düşer ve
okuması boş döner. receipt_analysis_quota'nın yabancı anahtarı yok (genel
satır yüzünden) ve o rota para harcıyor — zincirin dışında kalan tek yer
orasıydı.
Erişilememe "yok" ile karıştırılmaz: veritabanı bir an aksadı diye var olan hesabıyla gelen kullanıcı dışarı atılmaz (503, 401 değil).
Silme kapsamı şemadaki yabancı anahtarlarla belirlenir, kodda ayrıca silinmez:
app_users satırı gider (doğrulanmış e-posta, görünen ad, avatar adresi).saved_contacts ON DELETE CASCADE ile gider.shared_bills ON DELETE SET NULL ile SAHİPSİZ kalır ama durur.
İçlerinde başkalarının borcu vardır; hesabını silen kişi onların ödeme
yolunu kapatamaz. Politika bunu ve zincirin silinemezliğini açıkça söyler.Google girişi yalnızca hesap oluşturan akış için gerekir. Bağlantıyla gelen borçlu, Google hesabı olmadan yalnızca cüzdan imzasıyla kendi borcunu görür ve öder; o akışta e-posta ve ad hiç toplanmaz.
Ana creator arayüzü oturum açmadan görüntülenebilir. Aşağıdaki pahalı/yazıcı
işlemler sunucuda Google oturumu gerektirir ve oturum yoksa gövde okunmadan
genel, önbelleksiz 401 AUTH_REQUIRED JSON yanıtı döner:
POST /api/receipts/analyzePOST /api/shared-billsFiş seçimi oturum yönlendirmesinde tutulmaz veya yüklenmez. Kullanıcı Google girişinden döndüğünde görseli yeniden seçer. Fiş görseli yalnızca kimliği doğrulanmış kullanıcı Fişi analiz et işlemini açıkça başlattığında OpenAI'ye gönderilir; sunucuda veya kullanıcı tablosunda saklanmaz.
Auth.js v5 ve resmi Google OIDC provider kullanılır. OAuth state, PKCE, nonce,
callback doğrulaması ve güvenli cookie davranışı kütüphanenin sınırındadır.
İstenen scope tam olarak openid email profiledır; Google API erişimi yoktur.
Uygulama kimliği e-posta değildir: provider = google ile Google'ın kararlı
provider account ID'si (sub) birlikte benzersizdir. Her hesap için rastgele,
opak bir uygulama kullanıcı UUID'si üretilir. Aynı e-postayı bildiren iki ayrı
Google hesabı birleşmez; aynı Google hesabının eşzamanlı ilk girişleri Postgres
ON CONFLICT upsert ile tek kayda çözülür.
app_users yalnızca opak kullanıcı ID'si, provider/provider hesap ID'si,
normalize e-posta, doğrulanma bayrağı, isteğe bağlı görünen ad/avatar ve zaman
damgalarını saklar. E-posta profil metadatasıdır; yetkilendirme anahtarı değildir.
Google access token, refresh token ve ID token hiçbir veritabanına veya oturum
JWT'sine yazılmaz ve loglanmaz. Auth.js adapter kullanılmaz. Oturum, yalnızca
minimal uygulama kullanıcı kimliğini ve güvenli görünen profil alanlarını taşıyan,
HttpOnly, SameSite=Lax ve üretimde Secure şifreli JWT-cookie'dir; browser
storage kullanılmaz.
Google oturumu aşağıdaki sayfa ve API'lerde istenmez; global auth middleware'i yoktur:
/pay, /pay/[billId]/api/shared-bills/[billId]/challenge, /resolve, /me/api/shared-bills/[billId]/payment/*/api/rates/usdc-try, /api/rates/verifyBorçlu kendi borcunu yalnızca mevcut cüzdan challenge/imza oturumuyla görür ve öder. Google oturumu cüzdan sahipliği kanıtının yerini alamaz; borçlu cüzdanı Google kullanıcısıyla ilişkilendirilmez ve creator'ın Google profili paylaşılmaz.
Bu depodaki değişiklik Google Cloud, Vercel veya Neon'u kendiliğinden yapılandırmaz. Daha sonra şu adımlar elle uygulanmalıdır:
http://localhost:3000 ve
https://splitstable.com ekle.http://localhost:3000/api/auth/callback/googlehttps://splitstable.com/api/auth/callback/googleAUTH_SECRET, AUTH_GOOGLE_ID ve
AUTH_GOOGLE_SECRET değerlerini güvenli biçimde tanımla; Google girişini
kullanmak için APP_ORIGIN değerini de açıkça tanımla (yerelde
http://localhost:3000 olabilir). AUTH_SECRET için yukarıdaki 32 rastgele
bayt / 64 küçük hex üretim komutunu kullan.APP_ORIGIN=https://splitstable.com değerini elle tanımla.
Hiçbir sırra NEXT_PUBLIC_ öneki verme.migrations/0002_app_users.sql geçişini hedef Neon ortamına
ayrıca ve kontrollü biçimde uygula. Uygulama şemayı istek sırasında yaratmaz.Preview deployment hostları bilerek OAuth origin listesine alınmamıştır. Auth.js
origin'i gelen Host, Origin, Referer veya X-Forwarded-Host başlıklarından
değil, doğrulanmış APP_ORIGIN değerinden kurar.
Diğer komutlar:
npm run lint
npm run typecheck
npm test
npm run build
Kur artık elle girilmez. Sunucu kuru CoinGecko'dan alır, kanonik biçime indirger ve HMAC-SHA-256 ile imzalar; ödeme talebi bu imzalı teklife bağlanır.
Talebi oluşturan kişi kendi cüzdanıyla istediği alanı imzalayabilir. Bu yüzden EIP-712 imzası kurun piyasadan geldiğini kanıtlamaz — yalnızca alanları kimin imzaladığını kanıtlar. Kuru sunucunun HMAC etiketi korur: etiket kurun kendisini de kapsadığı için, oluşturucu kuru değiştirip tutarı yeniden hesaplasa bile borçlunun tarayıcısında yapılan sunucu doğrulaması düşer.
.env.local içindeki COINGECKO_DEMO_API_KEY alanına yaz.Anahtar yalnızca x-cg-demo-api-key başlığında taşınır; sorgu dizesine
konmaz ve istemciye hiç gönderilmez.
Sırrı depoya yazmadan yerelde üret:
node -e "console.log(require('node:crypto').randomBytes(32).toString('hex'))"
Çıktıyı .env.local içindeki RATE_QUOTE_SECRET alanına yapıştır. En az 32
karakter olmalıdır. .env.local git tarafından yok sayılır; sırrı asla
.env.example, test, log veya commit içine koyma.
GET /api/rates/usdc-try ile taze bir teklif basar.POST /api/rates/verify ile sunucuya doğrulatır;
doğrulama geçmeden cüzdan, tahmin ve gönderim kontrolleri görünmez.Sağlayıcı değeri sınırda bir kez altı ondalıklı kanonik metne çevrilir
(ör. 42.123456), sonra 42123456 / 1000000 rasyoneline dönüşür. Borç ve
mikro-USDC hesabının tamamı BigInt'tir; kanonikleştirmeden sonra hiçbir yerde
kayan nokta aritmetiği kullanılmaz. USDC tutarı her zaman 6 ondalıklı tam sayı
mikro-USDC'dir.
| Süre | Değer |
|---|---|
| Kur teklifi ömrü | 5 dakika |
| Ödeme talebi ömrü | teklifin bitişini aşamaz |
| Sağlayıcı gözlem yaşı üst sınırı | 10 dakika |
| Sunucu önbelleği | ~60 saniye |
Ödeme talebi dayandığı teklifden uzun yaşayamaz. Bu, paylaşılan bağlantının pratikte 5 dakika içinde ödenmesi gerektiği anlamına gelir.
CoinGecko Demo planı kredi sınırlıdır ve verisi yaklaşık 60 saniye tazeliktedir. Sağlayıcı sonucu sunucu belleğinde ~60 saniye önbelleklenir ve aynı penceredeki eşzamanlı istekler tek bir yukarı akış çağrısında birleşir; böylece her render veya her kullanıcı bir kredi harcamaz.
Önbellek süreç içidir. Sunucusuz ortamda her soğuk başlangıç ve her eşzamanlı örnek kendi önbelleğini tutar; bu yüzden önbellek tek başına bir garanti değil, bir iyileştirmedir.
Üstündeki garantiyi paylaşılan çağrı bütçesi verir: yukarı akışa gerçekten
gidilecekse, önce Postgres'teki provider_call_budget sayacından bir kredi
atomik olarak ayrılır. Ayrılamazsa CoinGecko hiç çağrılmaz. Böylece
dakikadaki yukarı akış çağrısı, örnek sayısından bağımsız olarak sınırlıdır.
Ayrıntı: "Örnekler arası çağrı bütçesi".
Sağlayıcı 429/5xx/zaman aşımı döndüğünde her istek yeni bir yukarı akış çağrısı üretirse Demo kotası hızla tükenir. Bu yüzden ardışık hatalarda sınırlı bir soğuma uygulanır:
| Davranış | Değer |
|---|---|
| Taban soğuma | 5 sn |
| Üstel büyüme | her ardışık hatada iki katı |
| Tavan | 120 sn |
Retry-After üst sınırı | 300 sn |
Retry-After başlığı döner.Retry-After başlığı yalnızca saniye biçiminde ve güvenli bir
aralığa kırpılarak dikkate alınır.Soğuma ve önbellek hâlâ SÜREÇ İÇİDİR ve örnekler arasında paylaşılmaz; tek başlarına toplam yukarı akış hızını sınırlamazlar.
Toplam hızı sınırlayan şey paylaşılan çağrı bütçesidir: soğuma penceresini geçen bir istek bile, yukarı akışa gitmeden önce Postgres'teki sayaçtan kredi ayırmak zorundadır. Bu iki katman farklı işler yapar — soğuma arızalı bir sağlayıcıyı korur, bütçe kotayı korur.
kit.send çağrıldıktan sonra ortaya çıkan her hata "gönderilemedi" demek
değildir: işlem zincire düşmüş olabilir. Bu yüzden sonuçlar ikiye ayrılır.
Sınıflandırma yalnızca belgelenmiş yapısal alanlara bakar (EIP-1193 kodu,
App Kit errorCategory, KitError ad+kod çifti). Hata metni hiçbir karara
girmez: "insufficient ..." veya "user rejected" yazması işlemin yayınlanmadığını
kanıtlamaz.
| Durum | Davranış |
|---|---|
UserRejectedRequestError / user_rejected / ham EIP-1193 4001, hash yok | Yayın öncesi kesin; yeniden denenebilir |
RPC_ENDPOINT_ERROR, type: "RPC" veya 4001 kullanan ağ KitError'ı | Belirsiz; 4001 tek başına ret kanıtı DEĞİLDİR |
BALANCE_INSUFFICIENT_TOKEN/GAS/ALLOWANCE (9001–9003), hash yok | Yayın öncesi kesin; yeniden denenebilir |
state: "success" ve geçerli hash | Başarı |
state: "error" + geçerli hash + kategori yok | Revert (kurulu SDK'nın onaylanmış makbuz şekli); asla "ödendi" değil |
chain_revert, reverted_onchain, partial_reverted | Revert; hash korunur |
pending, noop, tanınmayan durum veya kategori | Belirsiz; gönderim kilidi açılmaz, hash korunur |
viem WaitForTransactionReceiptTimeoutError | Belirsiz; hash mesajdan kurtarılır ve korunur |
| Herhangi bir hata hash taşıyorsa | Yayın öncesi sayılmaz; belirsiz |
Revert ve belirsizlikte işlem hash'i kaybedilmez: yerel kayıtta ve ekranda tutulur ki ArcScan'de mutabakat yapılabilsin.
Hata incelemesi hiçbir koşulda fırlatmaz. Cüzdan/sağlayıcı hataları
fırlatan getter, durumlu erişimci veya iptal edilmiş proxy içerebilir; böyle
bir nesnede error.code ya da Array.isArray bile TypeError atar. Bu yüzden
incelenen her alan korumalı okunur, en fazla bir kez okunup düz bir anlık
görüntüye alınır (durumlu bir getter hash analizi ile ret analizi arasında
değer değiştiremez) ve okunamayan bir alan dolaşımı eksik işaretler. Eksik
dolaşımda ya da geçerli bir hash bulunduğunda sonuç submissionUnknown
olur, rezervasyon kilitli kalır; kurtarılmış hash yine de ArcScan için
saklanır. kit.send çağrıldıktan sonra sınıflandırıcının kendisi çökse bile
sonuç asla yeniden denenebilir sendFailed olmaz.
Hata grafiği sınırlı ve döngüye dayanıklı biçimde dolaşılır; yalnızca
beklenen bağlantılar (cause, trace, rawError, errors) izlenir. Bir
bağlantı dizi/kap değerliyse (ör. AggregateError.errors ya da dizi
değerli bir cause) dolaşım eksik işaretlenir: desteklenen tekil nesne
şekli değildir ve içinde gizli bir ret kimliği veya işlem hash'i olabilir.
Elemanlar yine de yalnızca hash kurtarmak için taranır; sonuç hiçbir koşulda
yeniden denenebilir olmaz.
Bağlantı değerleri bir izin listesiyle kabul edilir: yokluk, sıradan
kayıt/hata nesnesi (düz nesne, prototipsiz nesne veya Error türevi) ve
güvenle taranan dizi. Set, Map, WeakSet, WeakMap, tipli diziler,
ArrayBuffer görünümleri, özel yinelenebilirler, dizi benzeri nesneler,
thenable'lar, fonksiyonlar ve tanınmayan kap prototipleri desteklenmez:
içleri güvenle sayılamadığı için dolaşım eksik işaretlenir ve içlerine
girilmez. errors alanı yalnızca dizi şeklinde taranır. Her kap türü için
ayrı bir gezinme uygulaması eklenmez; tercih edilen davranış fail-closed'dur. Kurulu yığın
gerçek cüzdan reddini cause.trace.rawError.rawError gibi derin bir yola
gömebildiği için yalnızca cause zincirini izlemek yetmez. Buna karşılık
kod 4001 tek başına ret kanıtı değildir: kurulu App Kit'te
RpcError.ENDPOINT_ERROR da 4001 kullanır (type: "RPC"), bu bir uç nokta
arızasıdır ve kit.send sonrası belirsiz bir gönderimi temsil edebilir. Ret
yalnızca olumlu bir iptal kimliğinden tanınır ve grafikte geçerli bir
işlem hash'i varsa asla ret sayılmaz.
Kurulu viem (2.55.19) onay bekleme zaman aşımında hash'i tipli bir alanda
tutmaz, yalnızca hata cümlesinin içinde geçirir; kurulu adaptör de bu çağrıyı
sarmalamaz. Hash olmadan işlem ArcScan'de bulunamayacağı için bu tek durumda
metin okunur — ama yalnızca hata adı birebir tutuyorsa, tam cümle
kalıbıyla ve katı hash doğrulayıcısıyla. Rastgele bir mesajın içinden hash
çıkarılmaz. Sonuç yine de belirsizdir: onay alınamamıştır.
Belirsiz durumda kullanıcıya önce MetaMask işlem geçmişini ve ArcScan'i kontrol
etmesi söylenir. Ayrıca chainId + requestId anahtarlı, gizli veri
içermeyen yerel bir işaretçi tutulur; aynı tarayıcıda kazara ikinci gönderimi
azaltır.
Kayıt talep başına ayrı bir localStorage anahtarındadır (v2 şeması).
Tek bir ortak dizi kullanılsaydı, farklı talepler eşzamanlı yazdığında "oku,
diziyi değiştir, yaz" adımları birbirinin kaydını silebilirdi. Yazılan kayıt
geri okunur ve chainId, requestId, sonuç, sahip jetonu, zaman damgası ve
varsa işlem hash'i birebir doğrulanır; doğrulanamazsa gönderime
geçilmez. Sonuç pending / success / reverted / unknown olarak ayrı
ayrı saklanır ve geçerli bir işlem hash'i de kalıcıdır: sayfa yenilense bile
ArcScan mutabakat bağlantısı görünmeye devam eder. Bozuk hash asla yazılmaz;
depoda bozulmuş bir hash sonucu düşürmez, yalnızca bağlantı gösterilmez.
Bu işaretçi yetkili bir koruma değildir.
localStoragecihaz ve tarayıcı başınadır: gizli sekmede, başka bir cihazda veya temizlenmiş depoda hiçbir şey hatırlanmaz.unknownkaydı ödemenin yapıldığını da yapılmadığını da söylemez; yalnızca sonucun doğrulanamadığını söyler.
Sonuç, SDK'nın belgelenmiş BridgeStep sözleşmesine göre yorumlanır. Başarı
için hem state: 'success' hem de geçerli bir işlem hash'i gerekir;
tek başına geçerli hash yeterli değildir. chain_revert / reverted_onchain
gibi kategoriler asla "ödendi" sayılmaz, ayrı bir terminal sonuç üretir ve
hash ArcScan bağlantısı için korunur. pending, noop veya sınıflandırılamayan
durumlar belirsizdir.
kit.send başladıktan sonra serbest metin eşleştirilmez: "insufficient
confirmations" gibi bir mesaj işlemin gönderilmediğini kanıtlamaz. Yalnızca
EIP-1193 kodu 4001 veya SDK'nın errorCategory: 'user_rejected' sınıfı yayın
öncesi sayılır — SDK dokümanı da makine kararları için errorCategory
kullanılmasını önerir.
Gönderime girmeden hemen önce chainId + requestId kaydı eşzamanlı olarak
yeniden okunur; success, unknown veya pending görülürse gönderim
engellenir. Gönderim başlamadan önce pending rezervasyonu yazılır, diğer
sekmeler storage olayı ve BroadcastChannel ile uyarılır ve mümkünse Web
Locks ile aynı anda tek gönderim çalışır. Rezervasyon yalnızca yayın öncesi
olduğu kanıtlanmış hatalarda serbest bırakılır; başarı, revert ve belirsiz
sonuçta korunur.
Cüzdan akışı açılmadan önce en az 60 saniye kalan süre aranır; bu kontrol
preflight ve App Kit kurulumundan sonra, kit.send'den hemen önce bir kez
daha yapılır. Teklif basımı da bu payı gözetir: paydan kısa ömürlü bir teklif
üretilmez.
Sınır: doğrudan bir ERC-20 transferinde, cüzdan istemi açıldıktan sonra son tarihi zincire dayatmanın yolu yoktur. Kullanıcı istemi dakikalarca açık bırakıp onaylarsa işlem, süresi dolmuş bir kurla zincire düşebilir. Gerçek değerli kullanım bunun için zincir üstü son tarih veya ayrık imzala-yayınla mimarisi gerektirir.
Bu sürüm Arc Testnet içindir. Gerçek parasal değer taşıyan bir dağıtım için aşağıdakiler hâlâ gereklidir ve bu depoda yoktur:
requestId'nin arka uçta veya zincir
üstünde atomik olarak tüketilmesi. Tarayıcı içi işaretçi ve bağlantının
tek kullanımlık sayılması bunun yerine geçmez.localStorage ve
BroadcastChannel yalnızca tek tarayıcı içindir. Başka cihaz, başka tarayıcı
veya gizli sekme hiçbir şey bilmez; yetkili engel için arka uçta ya da zincir
üstünde atomik requestId tüketimi şarttır.Kur alınamaz veya doğrulanamazsa elle girilen bir kura düşülmez ve ödeme talebi oluşturulamaz. Borçlu tarafında teklif doğrulanamazsa cüzdan ve gönderim kontrolleri hiç gösterilmez.
Kur verisi CoinGecko'dan alınır ve arayüzde Data provided by CoinGecko bağlantısıyla belirtilir.
Arc Testnet USDC'sinin gerçek parasal değeri yoktur. Gösterilen kur yalnızca test amaçlıdır ve hiçbir gerçek varlığı temsil etmez.
Dağıtımda şu sunucu ortam değişkenleri tanımlanmalıdır:
OPENAI_API_KEYCOINGECKO_DEMO_API_KEYRATE_QUOTE_SECRETDATABASE_URL (paylaşılan ortak hesap deposu; Part 2'de zorunlu olacak)Hepsi NEXT_PUBLIC_ olmadan, yalnızca sunucu tarafı değişken olarak eklenir.
Mobil cüzdan bağlantısı isteniyorsa ayrıca NEXT_PUBLIC_WALLETCONNECT_PROJECT_ID
tanımlanır; bu tek değişken bilinçli olarak istemci paketine girer.
NEXT_PUBLIC_ bir değişken eklendikten sonraBu değişkenler derleme anında koda gömülür, çalışma anında okunmaz. Vercel'e ekleyip kaydetmek tek başına hiçbir şey değiştirmez: yeni bir derleme gerekir.
Boş bir commit itmek bunun için yeterli OLMAYABİLİR — kaynak ağacı birebir aynı kaldığında Vercel önceki derleme çıktısını yeniden kullanabilir ve değişken yine pakete girmez. Güvenilir iki yol:
Değerin pakete girip girmediği doğrudan ölçülebilir: derlemeden sonra istemci paketinde değişkenin ADI kalmamalıdır. Kaldıysa yerine koyma olmamış demektir.
grep -rl "env.NEXT_PUBLIC_WALLETCONNECT_PROJECT_ID" .next/static
Neon veritabanı bu bölümde sağlanmadı; Marketplace kurulumu, geçişin
uygulanması ve DATABASE_URL'in tanımlanması ayrı bir adımdır.
Ayrıca bir dağıtım gereksinimi vardır ve kod tarafında karşılanamaz: CoinGecko kotasını örnekler arasında koruyacak paylaşılan bir sayaç/oran sınırlayıcı (Redis/KV) ya da Vercel firewall/rate limiting yapılandırması. Uygulamadaki soğuma süreç içidir ve tek başına yeterli değildir.
Bugünkü akışta fişi ödeyen kişi her borçlu için ayrı bir talep imzalar ve
ayrı bir bağlantı üretir. Hedef mimari tek bağlantıdır: ödeyen cüzdanını bir
kez bağlar, her borçlu için bir adres girer, tek bir EIP-712 manifest imzalar
ve herkes aynı /pay/<billId> bağlantısını alır.
Part 1 yalnızca TEMELİ kurar. İmzalı manifest, kalıcı depo, katı doğrulama, oluşturma API'si ve oluşturucu arayüzü hazırdır. Borçlu tarafı henüz yoktur:
/pay/<billId>rotası, ödeme rezervasyonu ve işlem kesinleştirme Part 2'dedir. Bu yüzden yeni akışSHARED_BILL_FLOW_ENABLEDbayrağıyla kapalıdır ve üretimde eski, borçlu başına ayrı bağlantı üreten akış çalışmaya devam eder.
Tek bağlantı, borç listesini URL'ye gömemez: aksi hâlde bağlantıyı eline geçiren
herkes bütün borçluları, adresleri ve tutarları görürdü ve bağlantı
kilometrelerce uzun olurdu. Bu yüzden liste sunucuda saklanır ve URL yalnızca
tahmin edilemez bir kimlik taşır. Üretimde Neon Postgres (Vercel
Marketplace üzerinden) kullanılır; sunucu değişkeni DATABASE_URL'dir.
| Saklanır | Saklanmaz |
|---|---|
| Genel hesap kimliği (0x + 64 hex) | Fiş görseli |
| Şema sürümü, Arc chainId | Ürün satırları, vergi/servis/indirim |
| Alıcı adresi ve etiketi | OpenAI çıktısı |
| Borç listesinin kriptografik taahhüdü | API anahtarları, HMAC etiketi |
| Borç sayısı, veriliş/bitiş anı | Kur teklifi (bilerek) |
| Alıcının EIP-712 imzası | İlgisiz katılımcılar |
| Borç satırları: adres, etiket, borç kimliği, TRY minor tutar |
Kur bilerek saklanmaz. Alıcı yalnızca TRY minor unit borçları imzalar; USDC tutarı, borçlu ödediği anda alınan taze ve sunucu kimliklendirmeli bir USDC/TRY teklifinden türetilir (Part 2). Böylece günlerce yaşayan bir bağlantı, dakikalar ömürlü bir kura çakılmaz.
Ayrı bir EIP-712 alanı kullanılır (Split Bill Shared Bill); ödeme talebi
imzası paylaşılan hesap imzası olarak kullanılamaz. İmza şunları kapsar:
schemaVersion, billId, chainId, recipient, recipientLabel,
debtsRoot, debtCount, issuedAt, expiresAt.
Borç listesi manifeste gömülmez; yerine kanonik bir taahhüt imzalanır:
chainId, billId ve satır sayısını bağlar.Ayrıntı ve gerekçe için aşağıdaki Merkle gizlilik modeli bölümüne bak. Part 1'deki toplu (aggregate) hash, bir borçlunun kendi satırını diğerlerini görmeden doğrulamasına izin vermediği için değiştirildi.
JSON.stringify çıktısı taahhüt olarak kullanılmaz: anahtar sırası ve
Unicode kaçışları uygulamadan uygulamaya değişir. Sunucu, gönderilen satırlardan
taahhüdü yeniden hesaplar; istemcinin bildirdiği debtsRoota güvenilmez.
POST /api/shared-bills — Content-Type: application/json, Cache-Control: no-store.
İstek: { manifest, debts, signature }. Yanıt (201 yeni / 200 idempotent):
{ "billId": "0x…", "path": "/pay/0x…", "expiresAt": 1700000000 }
Borç listesi, adresler, etiketler, taahhüt ve imza dönmez. Hata kodları:
INVALID_CONTENT_TYPE, BODY_TOO_LARGE, MALFORMED_JSON, DUPLICATE_FIELD,
INVALID_SHARED_BILL, INVALID_SIGNATURE, BILL_ID_UNAVAILABLE,
STORAGE_REJECTED, SERVICE_NOT_CONFIGURED, SERVICE_UNAVAILABLE.
Sıra kritiktir: gövde sınırlanır → yinelenen anahtar taranır → manifest, satırlar ve taahhüt doğrulanır → alıcı imzası doğrulanır → ancak ondan sonra veritabanı işlemi açılır. Hesap ve tüm borç satırları atomik yazılır; kısmi bir hesap kalmaz.
Idempotency: aynı billId ile gelen tekrar, yalnızca depodaki taahhüt,
alıcı ve imza birebir eşleşirse güvenli sayılır ve yeniden yazılmaz. Aksi hâlde
üzerine yazılmaz, BILL_ID_UNAVAILABLE döner.
Hepsi HttpOnly oturum çerezi ister ve Cache-Control: private, no-store
döner. Hiçbiri istemciden tutar, kur, alıcı veya borç kabul etmez.
| Uç nokta | Gövde | Ne yapar |
|---|---|---|
POST …/payment/prepare | (yok) | Taze sunucu kuruyla yetkili teklif basar. Borcu rezerve etmez, cüzdan çağırmaz. |
POST …/payment/claim | { offerId } | Borcu atomik rezerve eder, tek deneme yaratır, gönderim snapshot'ını döner. |
POST …/payment/outcome | { attemptId, outcome, txHash? } | İstemci sonucunu katı enum ile alır. success kabul edilmez. |
POST …/payment/finalize | { attemptId, txHash } | Arc Testnet'ten makbuzu doğrular; borcu yalnızca kanıt varsa paid yapar. |
GET …/payment/status | (yok) | Yalnızca kimliği doğrulanmış borçlunun kendi ödeme durumu. |
Yanıtlar başka bir borç satırı, başka bir katılımcı, tam manifest listesi, ham oturum jetonu, HMAC etiketi ya da veritabanı ayrıntısı taşımaz.
.env.local içine (bu dosya asla commit edilmez):
DATABASE_URL=
Şema elle uygulanır; tablolar istek işleyicisi içinde tembel oluşturulmaz. Geçişler numara sırasıyla ve eksiksiz uygulanır — biri atlanırsa uygulama derlenir ve testler geçer, ama o tabloya dokunan akış çalışma anında düşer.
Uygulama ve takip scripts/migrate.mjs ile yapılır:
npm run migrate
npm run migrate -- --apply
Argümansız çalışma hiçbir şey uygulamaz; yalnızca durumu yazar. Ayrıntı ve tek seferlik temel alma adımı için aşağıdaki Geçiş çalıştırıcısı bölümüne bak.
Geçişlerin listesi ve sırası:
| Geçiş | Ne ekler | Atlanırsa ne kırılır |
|---|---|---|
0001_shared_bills.sql | Ortak hesap şeması; Part 3'ün ödeme tabloları da bunun içindedir | Ortak hesabın tamamı |
0002_app_users.sql | Uygulama kullanıcıları (Google kimlik eşlemesi) | Google girişi |
0003_shared_bill_owner.sql | Hesabı oluşturan kullanıcı (sahiplik atfı) | "Hesaplarım" listesi ve sahiplik |
0004_saved_contacts.sql | Kayıtlı kişiler (kullanıcının adres defteri) | Kişi kaydetme ve öneriler |
0005_receipt_analysis_quota.sql | Fiş analizi kotası (OpenAI maliyet sınırı) | Kota sayımı — analiz sınırsız çalışır |
0006_provider_call_budget.sql | Örnekler arası CoinGecko çağrı bütçesi | Örnekler arası koruma — kur servisi tek örneklik korumaya düşer |
0007_provider_rate_cache.sql | Örnekler arası kur önbelleği (son gözlem) | Paylaşılan önbellek — kur yalnızca süreç belleğinde tutulur; krediyi kapamayan örnekler kullanıcıya hata döner |
0008_schema_migrations.sql | Uygulanmış geçişlerin kaydı (sürüm, ad, checksum) | Takip — hangi geçişin uygulandığı yine bilinmez |
Bu listenin eksiksizliği bir testle zorlanır (migrations.test.ts):
migrations/ altına eklenen ama burada anılmayan bir dosya kapıyı düşürür.
DATABASE_URL yoksa uygulama ve testler yine derlenir; depo gerektiren rota
kontrollü 503 SERVICE_NOT_CONFIGURED döner ve bellek içi bir yedeğe
asla düşmez.
Geçişlerin hepsi IF NOT EXISTS kullanır, yani bugüne kadar tekrar
çalıştırmak zararsızdı. Bu garantiye yaslanılmaz: IF NOT EXISTS var olan
bir tabloyu değiştirmez, bu yüzden ilk ALTER TABLE ... ADD COLUMN yazıldığı
gün tekrar çalıştırmak ya düşer ya iki kez uygulanır. Çalıştırıcı bu yüzden
neyin uygulandığını kaydeder ve uygulanmışı bir daha çalıştırmaz.
Part 3'ün ödeme tabloları (teklif, deneme ve borç ödeme durumu) ayrı bir dosya
yerine aynı 0001 geçişine eklenmişti; o karar alındığında şema henüz
hiçbir ortamda uygulanmamıştı. Bu gerekçe artık geçerli değildir:
CREATE TABLE IF NOT EXISTS var olan bir tabloyu değiştirmez, bu yüzden
bundan sonra eklenecek her alan kendi geçiş dosyasını ister.
0008'e kadar geçişler elle uygulanıyordu ve hangisinin uygulandığını kaydeden hiçbir şey yoktu; tek öğrenme yolu veritabanına bakıp "şu tablo var mı" diye çıkarım yapmaktı. Onbeş test kullanıcısındayken veritabanı silinip yeniden kurulabilir; binde kurulamaz.
Üç mod vardır ve varsayılan hiçbir şey uygulamaz:
| Komut | Ne yapar |
|---|---|
npm run migrate | Durumu yazar: uygulanmış, bekleyen, sorunlu. Hiçbir geçiş uygulamaz |
npm run migrate -- --apply | Bekleyenleri sırayla uygular |
npm run migrate -- --baseline NNNN | Tek seferlik; aşağıya bak |
Tek istisna: üç mod da, daha ilk okumadan önce kayıt tablosunun kendisini
(0008) koşulsuz uygular. Tavuk-yumurta: kaydı okuyabilmek için tablonun var
olması gerekir, ama tabloyu getiren de bir geçiştir. Dosya IF NOT EXISTS
kullandığı için bu tekrarlanabilir ve uygulama şemasına dokunmaz — yalnızca
takip tablosunu yaratır. Yani argümansız çalıştırmak da veritabanına yazar;
"salt okunur" değildir.
Çalışma biçimi:
BEGIN;/COMMIT;
satırları çıkarılır, gövde ile kayıt satırı tek işleme alınır. Aksi hâlde
arada kalan bir çökme, uygulanmış ama kaydedilmemiş bir geçiş bırakırdı.
SQL noktalı virgülden bölünmez — dize içindeki bir noktalı virgülde
bozulan klasik hata budur; yalnızca kendi satırında duran iki anahtar sözcük
çıkarılır ve dosyanın gerçekten öyle sarıldığı doğrulanır.DATABASE_URL asla yazdırılmaz — ne günlüğe, ne hata mesajına.connect() sonsuza
kadar bekler: ağ takılırsa araç hata vermez, sadece susar — ve elle
çalıştırılan bir araçta susmak hata vermekten kötüdür. Süre cömerttir çünkü
Neon uykudaki bir dalı uyandırırken birkaç saniye harcayabilir; amaç yavaşı
kesmek değil, ölünün sonsuza sürmesini engellemek. Bağlantı kurulamazsa şema
değişmez ve hiçbir geçiş uygulanmaz.query_timeout istemci
genelindedir; koyulsaydı büyük bir tabloyu dolduran meşru bir geçiş de yarıda
kesilirdi. Bir geçişi beklemek, onu kesmekten iyidir.Çalıştırıcı, geçişleri zaten elle uygulanmış bir veritabanına geliyor. O
geçişler yeniden çalıştırılmamalı, ama kaydedilmezlerse "bekliyor" sanılır.
--baseline, verilen sürüme kadarki geçişleri çalıştırmadan kaydeder:
npm run migrate -- --baseline 0007
Sınır elle verilir ve varsayılanı yoktur. "Hepsini işaretle" demek, henüz uygulanmamış bir geçişi de uygulanmış saymak olurdu — sessizce atlanan bir tablo en pahalı hata sınıfıdır. Bu yüzden komut, veritabanında gerçekten uygulanmış son sürümü ister.
İddia ayrıca sınanır: işaretlenecek her geçişin yarattığı tabloların
gerçekten var olduğu doğrulanır; biri yoksa temel alma reddedilir. Yalnızca
ALTER TABLE yapan bir geçiş (ör. 0003) doğrulanamaz ve bu açıkça bildirilir.
Temel alma yalnızca boş bir kayıt tablosunda çalışır, yani bir kez.
Sıra: önce argümansız npm run migrate — hangi geçişin gerçekten
uygulandığını o yazar, tahmin edilmez. Sonra veritabanında gerçekten var
olan son sürümle --baseline, sonra --apply. Bundan sonrası sadece
--apply.
Bugünkü somut durum: 0007 depoya yeni girdi ve elle uygulanmadıysa sınır
0006'dır; --apply o zaman 0007'yi de uygular. 0007 zaten elle
uygulanmışsa sınır 0007'dir. Sadece bu ikisini ayırt etmek için elle SQL
çalıştırmaya gerek yoktur — durum çıktısı zaten söyler. Eksik bir geçişi
elle uygulamak, çalıştırıcının önlemek için var olduğu hatanın ta kendisidir.
Testin kapsamı, migrate.test.ts: karar
mantığının tamamı — hangi geçiş bekliyor, hangisi sorunlu, sarmalayıcı ayıklama,
tablo çıkarımı. Kapsamadığı: gerçek uygulama, çünkü CI'da Postgres yoktur.
Neon veritabanı Frankfurt'tadır (eu-central-1). Vercel fonksiyonları da
vercel.json içindeki "regions": ["fra1"] ile aynı yere sabitlenmiştir —
fra1 Vercel'in Frankfurt bölgesidir ve Neon'unkiyle aynı AWS bölgesine
denk gelir.
Bu alan yazılmazsa Vercel iad1e (Washington) düşer; belgelenmiş
varsayılan budur ve hiçbir yerde uyarı vermez. O hâlde her SQL gidiş-dönüşü
Atlantik'i geçerdi:
| Fonksiyon ↔ veritabanı | Ödeme hazırlama (5-7 ardışık sorgu) | |
|---|---|---|
iad1 (varsayılan) | ~90 ms | ~450-630 ms yalnızca ağ beklemesi |
fra1 (Neon ile aynı) | birkaç ms | ~10-15 ms |
Kazanç iki yönlüdür: kullanıcılar da Türkiye'de olduğu için fra1 onlara da
Washington'dan çok daha yakındır.
Statik dosyalar etkilenmez — onlar her bölgeye dağıtılır; bu ayar yalnızca fonksiyonların nerede çalıştığını belirler.
Tek bölge bilinçlidir. Hobby planı zaten tek bölge seçtirir; ötesinde, ikinci bir bölge oradaki fonksiyonları veritabanından uzağa koyar ve düzeltilen sorunu geri getirir.
Seçim vercel-region.test.ts ile kapıda
tutulur. Testin kanıtlayamadığı şey: veritabanının gerçekten Frankfurt'ta
olduğu — o bilgi DATABASE_URL içindedir ve bu depoda okunmaz. Veritabanı bir
gün taşınırsa bu bölge de elle değiştirilmelidir.
Bağlantıyı eline geçiren herkes hesabı açabilir. Bağlantı borç listesini taşımaz, ama sayfayı açmak listeyi getirir (Part 2). Bu yüzden bağlantı yalnızca ilgili kişilerle paylaşılmalıdır. Adresler, etiketler, imzalar, manifestler ve üretilen bağlantılar loglanmaz; hata mesajları belirli bir cüzdanın bir hesapta olup olmadığını açığa vurmaz.
Hesaplar sonludur: üst sınır yedi gündür
(SHARED_BILL_MAX_LIFETIME_MS). Süresi dolmuş bir hesap, fiziksel olarak
silinmemiş olsa bile kullanılamaz sayılır; sorgular her hâlükârda bitiş anını
filtreler. Fiziksel temizlik (cleanup job) Part 2'ye ertelenmiştir: bugün
süresi dolmuş satırlar tabloda kalmaya devam eder.
Herkes aynı /pay/<billId> bağlantısını alır. Bir borçlunun yalnızca
kendi borcunu görebilmesi için bağlı cüzdanın kontrolünü kanıtlaması
gerekir:
/me yalnızca o borçlunun tek satırını ve Merkle kanıtını döner.| Yapar | Yapmaz |
|---|---|
| Adresin kontrolünü kanıtlar | Token onaylamaz (approve değildir) |
| Tek bir hesap için görüntüleme yetkisi verir | Transfer yetkisi vermez |
| Beş dakika geçerlidir | İşlem göndermez |
| Ödeme talebi/hesap oluşturamaz |
İmza bir kimlik veya KYC kanıtı değildir ve borcun gerçek dünyada meşru olduğunu kanıtlamaz. Yalnızca "bu adresi kontrol eden kişi buradayım" der.
Erişim imzası ayrı bir EIP-712 alanındadır (Split Bill Shared Bill Access). Ne paylaşılan hesap manifesti ne de ödeme talebi imzası buraya
geçebilir; bu imza da oralarda kullanılamaz.
Part 1'de borç taahhüdü tüm satırların tek bir toplu (aggregate) hash'iydi. Bu, bir borçlunun kendi satırını doğrulayabilmesi için ona tüm borç satırlarını vermeyi gerektirirdi — yani herkes herkesin adresini, adını ve tutarını görürdü. Bu, tek bağlantının gizlilik hedefiyle doğrudan çelişiyordu.
Bu yüzden taahhüt bir Merkle kökü oldu (debtsRoot) ve şema sürümü 2'ye
yükseltildi; sürüm 1 manifestleri fail-closed reddedilir. Sürüm 1 hiçbir
yerde yayımlanmadı ve hiçbir veritabanına yazılmadı.
keccak256(etiket) + keccak256(borç kimliği) + TRY minor tutar.leafIndex ve kardeş özetleri. Yön ve kardeş varlığı
kanıttan okunmaz, leafIndex + debtCounttan türetilir; bu yüzden yön
çevrilemez, kanıt uzatılamaz veya kısaltılamaz.Bir borçlu böylece kendi satırını doğrular ama başka hiçbir satırın adresini, etiketini, borç kimliğini veya tutarını öğrenemez.
Hepsi Cache-Control: no-store; hiçbiri hesap kimliği, adres, nonce, imza,
oturum jetonu veya etiket loglamaz.
| Uç nokta | Ne yapar | Ne döner |
|---|---|---|
POST /api/shared-bills/[billId]/challenge | Meydan okuma üretir. Hesabın var olup olmadığına bakmaz, veritabanına dokunmaz | { challenge, tag } — hiçbir hesap verisi yok |
POST /api/shared-bills/[billId]/resolve | Etiketi + EIP-712 imzayı doğrular, nonce'u atomik tüketir, oturum kurar | { authenticated: true } + HttpOnly çerez |
GET /api/shared-bills/[billId]/me | Oturumu doğrular | manifest, alıcı adresi/etiketi, tek borç satırı, Merkle kanıtı, bitiş, durum |
Hedef (audience) yalnızca sunucudaki APP_ORIGINdan gelir. Host,
Origin, Referer ve X-Forwarded-Host başlıklarına asla güvenilmez.
Üyelik sızdırılmaz: hesabın olmaması, kapalı olması, süresinin dolması ve cüzdanın o hesapta bulunmaması aynı genel yanıtı üretir.
| Şey | Süre |
|---|---|
| Erişim meydan okuması | 5 dakika (üst sınır) |
| Oturum | 15 dakika (üst sınır; veritabanı kısıtı da uygular) |
| Paylaşılan hesap | 7 gün (üst sınır) |
Ham oturum jetonu yalnızca HttpOnly; SameSite=Strict; Path=/ çerezinde
taşınır (üretimde ayrıca Secure). Depoda yalnızca SHA-256 özeti saklanır:
veritabanını okuyan biri geçerli bir çerez üretemez. Jeton URL'de, JSON
gövdesinde, tarayıcı deposunda, logda, React state'inde veya HTML'de hiç
yer almaz.
Nonce tek kullanımlıktır ve atomik olarak tüketilir; eşzamanlı iki çözümleme denemesinden en fazla biri başarılı olur.
SHARED_BILL_AUTH_SECRET=
APP_ORIGIN=
SHARED_BILL_AUTH_SECRET — meydan okuma zarfının HMAC sırrı (en az 32
karakter). RATE_QUOTE_SECRET ile paylaşılmaz: birini ele geçiren
diğerinin etiketlerini üretemez.APP_ORIGIN — güvenilen üretim origin'i (https://…, yol/sorgu içermez).Üretimde ikisinden biri eksik veya bozuksa erişim uçları kontrollü
503 SERVICE_NOT_CONFIGURED döner. Geliştirmede (NODE_ENV !== "production")
APP_ORIGIN için açık http://localhost:3000 yedeği kullanılır; üretimde
bu yedeğe asla düşülmez.
| Tablo | Saklanır | Saklanmaz |
|---|---|---|
shared_bill_auth_nonces | billId, nonce, borçlu adresi, tüketim/bitiş anı | — |
shared_bill_sessions | jetonun SHA-256 özeti, billId, borçlu adresi, chainId, bitiş | ham jeton, fiş, ürün, API anahtarı |
Süresi dolmuş satırlar fiziksel olarak silinmemiş olsalar bile kullanılamaz sayılır; sorgular her hâlükârda bitiş anını filtreler. Sınırlı ve fırsatçı bir temizlik vardır (çağrı başına en fazla 50 satır); sınırsız silme yapan bir istek yolu yoktur.
SHARED_BILL_FLOW_ENABLED = false ile kapalı;
üretimde eski, borçlu başına bağlantı üreten akış çalışmaya devam ediyor.Bir veritabanı satırı, zincir üstünde yinelenen transferleri engellemez ve kriptografik bir tek-kullanım garantisi değildir. Cüzdan imzası borcun meşruluğunu kanıtlamaz. Bir istemcinin "başarılı" demesi ödemenin yapıldığını kanıtlamaz. Bu sürüm Arc Testnet içindir ve mainnet'e hazır değildir.
Part 3, kimliği doğrulanmış borçlu sayfasına tam Arc Testnet ödeme yaşam döngüsünü ekler: taze sunucu kuru → tam sayı tutar türetme → tahmin → inceleme → sunucu rezervasyonu → mevcut gönderim sınırı → sunucunun zincirden makbuz doğrulaması → ancak o zaman "ödendi".
Borç (shared_bill_debts.payment_status):
unpaid ──claim──> reserved ──onaylı makbuz──> paid (SON)
^ │
│ ├── KANITLI yayın öncesi hata ──> unpaid
│ ├── onaylı REVERT makbuzu ──────> unpaid
└──────────────────┘
└── belirsiz sonuç ────────> review_required
review_required ── OTOMATİK ──/──> paid (ASLA; elle mutabakat)
review_required ── OTOMATİK ──/──> unpaid (ASLA; elle mutabakat)
paid ──/──> herhangi bir durum (ASLA geri dönmez)
Deneme (shared_bill_payment_attempts.status):
reserved ── istemci hash bildirdi ──────> submitted
reserved ── KANITLI yayın öncesi hata ──> released (SON)
reserved | submitted ── onaylı makbuz ──> confirmed (SON)
reserved | submitted ── revert makbuzu ─> reverted (SON)
reserved | submitted ── çözülemedi ─────> unknown (SON)
confirmed, reverted, unknown ve released son durumlardır.
submitted durumundan serbest bırakma yoktur: kit.send çağrıldıktan
sonra rezervasyon açılmaz. unknown da rezervasyonu tutar — belirsiz bir
deneme kendiliğinden yeniden denenebilir hâle gelmez.
Veritabanı kısıtları: borçlu başına en fazla bir aktif deneme (kısmi
benzersiz indeks), küresel olarak benzersiz işlem hash'i, teklif başına en
fazla bir deneme, numeric(30, 0) tam sayı tutarlar, kanonik kur paydası
(10^0..10^6) ve paid satırın doğrulanmış hash + zaman taşıma zorunluluğu.
Tüm geçişler karşılaştır-ve-yaz'dır; son yazan kazanan güncelleme yoktur.
Kur manifeste gömülmez. Borçlu ödemeye başladığında sunucu taze bir
USDC/TRY teklifi basar (POST /api/shared-bills/<billId>/payment/prepare).
Sunucu bunu kendi kur servisinden doğrudan alır; uygulamanın kendi
/api/rates rotasına HTTP isteği yapmaz. Mevcut CoinGecko önbelleği, soğuma,
doğrulama, HMAC ve altı ondalık kanonikleştirme aynen kullanılır. Elle
girilen bir kura asla düşülmez: kur servisi çalışmıyorsa teklif basılmaz.
Teklif ne kurdan ne de bağlantıdan uzun yaşar (min alınır) ve gönderim
için gereken 60 saniyelik paydan kısa ömürlü bir teklif hiç üretilmez.
POST /api/shared-bills/<billId>/payment/claim gövdesi yalnızca teklif
kimliğini taşır. Tutar, kur, alıcı ve borç istemciden kabul edilmez;
hepsi saklanan hesaptan okunur ve mikro USDC bu adımda yeniden türetilip
teklifle birebir karşılaştırılır. Rezervasyon başarılı olana kadar kit.send
erişilemezdir. Yanıt, mevcut gönderim sınırının beklediği değişmez
snapshot'tır; istemci onu incelediği teklifle birebir karşılaştırır ve tek
bir alan bile farklıysa göndermez.
POST /api/shared-bills/<billId>/payment/finalize yalnızca deneme kimliği ve
aday işlem hash'i alır. Tüm ekonomik alanlar saklanan denemeden gelir.
Sunucu Arc Testnet'e kendisi bağlanır ve şunların hepsini arar:
ARC_MIN_CONFIRMATIONS),Transfer(address,address,uint256) katı çözümleme (üç konu, adres
konularının üst 12 baytı sıfır, veri tam 32 bayt),"En az bir eşleşen olay" yetmez: toplam beklenenden az da fazla da olamaz.
Borç yalnızca bu doğrulama geçerse paid olur; hesap ise her borç bağımsız
olarak onaylandığında kapanır.
Makbuz yoksa veya onay yetersizse: kontrollü beklemede durumu, rezervasyon korunur, sınırlı yoklamaya izin verilir, ödendi denmez.
Yoklama aralığı üstel geri çekilir: 4 → 6 → 9 → 13,5 → 20 saniye, son bekleme pencerenin kalanı. Önceden her 4 saniyede bir soruluyordu; 40 saniye süren bir onayda bu 10 istek, her biri iki veritabanı sorgusu demekti. Onay ilk saniyelerde gelmediyse bir sonraki saniyede de gelme ihtimali düşüktür.
Toplam pencere değişmedi ve bu bilinçli: 60 saniye, yani eski 15 × 4 sn
ile birebir aynı. Kullanıcıya verilen söz "yaklaşık bir dakika izliyoruz,
sonra tekrar bak"tır; geri çekilmeyi pencereyi büyüterek uygulamak o sözü
sessizce üç dakikaya çevirirdi. Değişen tek şey aynı pencerede kaç kez
sorulduğu: 15 istek yerine 7 — yarısından fazlası gitti, ilk saniyelerdeki
deneyim aynı.
Çizelgenin toplamının pencereye tam oturması ve tavanın gerçekten devreye
girmesi testle tutulur
(poll-backoff.test.ts). Tavan olmasaydı
çarpan pencerenin sonunda tek bir uzun beklemeye dönüşürdü: onay 21. saniyede
gelse bile kullanıcı 40. saniyeye kadar bunu görmezdi.
Revert: kanıt
saklanır, hash ArcScan için korunur, borç ödenmemiş kalır. Sonuç
çözülemiyorsa: unknown / review_required, kilit korunur, otomatik tekrar
yoktur.
unknown bir deneme son durumdur: sonradan gelen bir makbuz doğrulaması
bile onu otomatik olarak confirmede taşımaz ve review_required bir borcu
paid yapmaz. Bu bilerek katıdır — belirsiz bir denemenin hash'ine bağlanmış
herhangi bir makbuz, kilidi tek başına açabilirdi. Bu durumdan çıkış, insan
tarafından yürütülen açık bir mutabakat gerektirir (ArcScan + cüzdan geçmişi).
Bu Arc Testnet MVP'si bir onayı yeterli sayar: makbuz bir bloğa girmiştir. Bu, derin bir yeniden düzenleme (reorg) karşısında kesinlik değildir. Test USDC'sinin gerçek parasal değeri olmadığı için bu denge kabul edilmiştir; gerçek değer taşıyan bir ağda bu sayı yükseltilmelidir.
POST /api/shared-bills/<billId>/payment/outcome katı bir enum kabul eder:
rejected, insufficientFunds, preflightFailed, submitted, ambiguous.
success yoktur: istemci bir ödemeyi başarılı ilan edemez. Elinde bir hash
varsa submitted bildirir ve mutabakatı sunucu yapar. Serbest bırakan üç
sonuç hash taşıyamaz ve yalnızca mevcut sınıflandırıcının yayın öncesi
olduğunu kanıtladığı hâllerde üretilir.
Kötü niyetli bir istemci "reddedildi" diye yalan söyleyebilir ve uygulama düzeyindeki rezervasyonu açtırabilir. Bu zincir üstü bir garanti değildir; cüzdan her transfer için borçlunun kendi onayını istemeye devam eder, dolayısıyla bu yalan kimseye para harcatamaz — yalnızca ikinci bir denemeye izin verir.
Gönderim sınırındaki ArcPaymentSnapshot.tryMinor artık number değil,
kanonik ondalık metintir. Gösterilen, tahmin edilen, rezerve edilen,
gönderilen ve mutabakatı yapılan tutarın hepsi aynı tam sayıdan türer;
hiçbir aşamada parseFloat, Number(...) veya kayan nokta para aritmetiği
kullanılmaz. Eski (borçlu başına ayrı bağlantılı) ödeme talebi akışı
davranışını korur: imzalı gövde zaten kanonik ondalık metin taşıdığı için
sınırda daraltma yapılmadan aktarılır.
Manifest katmanının kendi sınırı korunmuştur: canonicalizeSharedBillDebts
borç tutarlarını bilerek Number.MAX_SAFE_INTEGER ile sınırlar ve bu görevde
gevşetilmemiştir. Ödeme yolunun tamamının daraltma yapmadığı, satırı
doğrudan depoya yerleştiren ayrı regresyon testleriyle ölçülür.
Bu bölüm bir zamanlar Part 4'e ERTELENENLERİ sayıyordu. O maddelerin hepsi kapandı; liste artık yapılmış olanı kaydediyor:
0001_shared_bills.sql → 0005_receipt_analysis_quota.sql).DATABASE_URL ve SHARED_BILL_AUTH_SECRET tanımlandı; dağıtım yapıldı ve
uygulama Vercel'de yayında.SHARED_BILL_FLOW_ENABLED = true. Bunu bir
test zorlar, yani sessizce geri kapanamaz.Listedeki son açık madde olan örnekler arası oran sınırlama da kapandı: kur servisi artık paylaşılan bir çağrı bütçesi kullanıyor (aşağıya bakınız).
Kişi bazlı sınırlama da yerinde: Vercel Firewall'da GET /api/rates/usdc-try
için IP başına 60 saniyede 10 istek kuralı etkindir ve aşıldığında 429
döner (2026-09-06'da panelde doğrulandı). Bu bir dağıtım ayarıdır, depoda
kodu yoktur — firewall kuralı kapatılırsa bu koruma da kalkar.
Fiş analizinin OpenAI maliyeti bundan ayrıdır ve zaten korunuyordu: günlük
genel tavan (DAILY_ANALYSES_TOTAL) kullanıcı başına hakla birlikte
Postgres'te receipt_analysis_quota üzerinde atomik olarak ayrılır, bu
yüzden örnek sayısından bağımsızdır.
Kur servisinin önbelleği, tek uçuşu ve soğuması süreç içidir: yalnızca tek bir Node.js örneğini korur. Vercel'de her soğuk başlangıç ve her eşzamanlı sunucusuz örnek kendi kopyasını taşıdığı için toplam CoinGecko hızını hiçbir şey sınırlamıyordu.
migrations/0006_provider_call_budget.sql bunu kapatır. Yukarı akışa
gerçekten gidilecekse, önce provider_call_budget tablosundan bir kredi
atomik olarak ayrılır; ayrılamazsa CoinGecko hiç çağrılmaz.
src/lib/rates/provider-budget.ts içindedir. Kova
hesabı tam sayı bölmedir; SQL ve TypeScript aynı hesabı yapar, sunucu saat
dilimine bağlı değildir.Kesintide engellenmez (bilinçli karar): sayaca ulaşılamazsa bugünkü süreç içi korumaya düşülür — bugünkünden kötü değildir, ve kur servisini kapatmak bir veritabanı hıçkırığını ödeme akışının durmasına çevirirdi. Sessiz de kalmaz: pencere başına bir kez günlüğe düşer.
DATABASE_URL tanımlı değilse bu bir olay sayılmaz ve günlüğe düşmez;
yerel geliştirmede ve testlerde beklenen durumdur. Ama o zaman örnekler
arası koruma da yoktur — koruma geçişin uygulanmış olmasına bağlıdır.
Ayrıca APP_ORIGIN yerelde tanımlı değildir. Geliştirmede sorun çıkarmaz,
çünkü DEVELOPMENT_APP_ORIGIN (http://localhost:3000) devreye girer. Üretimde
ise zorunludur: eksikse ortak hesap erişim uçları
(challenge ve resolve) 503 SERVICE_NOT_CONFIGURED döner.
0006 çağrıların sayısını paylaştırdı ama sonucunu paylaştırmadı. Tek
başına bu bileşim zararlıdır: limit paylaşılıp sonuç paylaşılmayınca korumanın
bedelini önbellek isabeti değil kullanıcı öder. Penceredeki krediyi
kapamayan eşzamanlı örnekler — taze kur yan taraftaki örneğin belleğinde
dururken — exhausted görüp "kur alınamadı" döndürüyordu. Arıza kullanıcı
sayısıyla değil eşzamanlılıkla büyür; ilk kalabalık akşamda görülür.
migrations/0007_provider_rate_cache.sql bunu kapatır. Önbellek artık iki
katmanlıdır:
| Katman | Nerede | Ne yapar |
|---|---|---|
| L1 | süreç belleği | Bedava ve anlık; soğuk başlangıçta boş, örnekler arası paylaşılmaz |
| L2 | provider_rate_cache | Bütün örneklerin gördüğü son gözlem |
Sıra: L1 → L2 → soğuma → bütçe → sağlayıcı.
QUOTE_MAX_OBSERVATION_AGE_MS yaş sınırı. Verinin paylaşılan bir yerden
gelmesi hiçbir doğrulamayı gevşetmez; teklif her durumda taze basılır
(yeni quoteId, yeni pencere, yeni HMAC etiketi).TTL 60 → 240 saniye. Teklif ömrü min(300 sn, 600 sn − gözlem yaşı)
olduğu için 300 saniyeye kadar eskimiş bir gözlem hâlâ tam 300 saniyelik
teklif verir; 240 saniyelik TTL bu eşiğin altında kalır ve teklif ömründen
hiçbir şey götürmez. Eski 60 saniye, izin verilen gözlem yaşının altıda
biriydi ve bedelini CoinGecko Demo kotasından ödüyordu.
Kalan sınır: iki örnek tam aynı anda gelip ikisi de L2'yi boş bulursa, kazanan yazana kadar kaybeden yine hata döndürebilir. Bunu tümüyle kapatmak dağıtık bir kilit ister; TTL başına bir kez ve saniyenin altında süren bu artık, düzeltilen sürekli duruma göre önemsizdir.
GET /api/admin/metrics. Bu uçtan önce depoda console.error dışında hiçbir
ölçüm yoktu: Vercel'in runtime günlükleri kısa saklamalı, toplanmıyor ve alarmı
yok. Onbeş test kullanıcısında bu sorun değildi; binde, biri "çalışmıyor"
dediğinde bunun herkeste mi tek kişide mi olduğunu söyleyecek hiçbir şey
yok demektir.
Yeni tablo gerektirmez. Veritabanı bu sayıları zaten tutuyordu —
shared_bill_payment_attempts.status bedava bir huni, receipt_analysis_quota
günlük analizi, provider_call_budget sağlayıcı çağrılarını veriyor. Eksik
olan tek şey onları okuyan bir yoldu. Altı sorgu tek işlemde çalışır:
sayaçlar tutarlı bir andan gelir ve tek gidiş-dönüş olur.
Yanıt ham sayıların yanında eşikleri de taşır, çünkü ham bir sayı "harekete geç" demez:
| Alan | Ne zaman bakılmalı |
|---|---|
analyses.warning | Günlük tavanın %80'i aşıldı; bugün içinde bakılmalı |
retention.behind | Silinmeye uygun sayısı parti sınırına ulaştı — temizlik geriye düşüyor |
attempts.needsAttention | reverted + unknown; zincirde ne olduğu bilinmeyen ödemeler |
debts.needsReview | review_required borç satırları |
provider.windowsAtCap | Bütçesi dolmuş pencereler; her biri reddedilmiş bir kur çağrısı |
SALT OKUR. Hiçbir sayacı tüketmez, hiçbir satır yazmaz; ölçmek için ölçüleni değiştirmez.
Yalnızca toplam döner. Kullanıcı kimliği, cüzdan adresi, hesap kimliği,
etiket ya da işlem hash'i bu yanıttan geçmez — sayım için gereken "kaç
tane", "kim" değil. Bu uç tanımı gereği ayrıcalıklıdır ve ayrıcalıklı bir ucun
sızdırdığı şey en çok sızan şeydir. Sınır iki yerde zorlanır: SQL'in çıktı
sütunları izinli bir listeyle sınırlıdır
(metrics-driver.test.ts) ve yanıtın
tamamı gerçek şekilli veriyle taranır
(metrics-route.test.ts).
Uç METRICS_SECRET ile korunur ve sır tanımlı değilse hiç çalışmaz —
/api/cron/retention ile aynı karar. Yetkisiz yanıt tek biçimdir; sırrın var
olup olmadığını bile sızdırmaz.
curl -sS -H "Authorization: Bearer $METRICS_SECRET" https://<alan-adi>/api/admin/metrics
Sayaçlar "kaç tane" der; hata takibi "ne kırıldı" der. İkisi ayrı işler ve biri ötekinin yerine geçmez.
SENTRY_DSN tanımlıysa src/instrumentation.ts
sunucu ve edge çalışma zamanlarında Sentry'yi başlatır. Tanımlı değilse SDK
hiç yüklenmez — yerel geliştirme, testler ve CI hata takibi olmadan çalışır
ve bu bir hata sayılmaz.
Kapsam bilinçli olarak SUNUCU + EDGE ile sınırlı. Tarayıcıya hiçbir şey gönderilmez:
connect-src hâlâ ölçülüyor ve kapatılmayı bekliyor; tarayıcı tarafı bir
SDK oraya kalıcı bir üçüncü taraf hedefi ekler ve o işi zorlaştırır.Sınır bir testle tutulur: instrumentation-client.ts ya da
sentry.client.config.ts diskte belirirse
sentry-config.test.ts düşer ve
CSP kararı yeniden sorulur. DSN de bilerek NEXT_PUBLIC_ değildir.
Hata takibi, tanımı gereği uygulamanın iç durumunu üçüncü bir tarafa
gönderir. Varsayılan haliyle bağlamak bu depodaki gizlilik sınırını sessizce
delerdi — en sinsi yol URL'dir: bill_id bir yol parametresidir
(/pay/0x…), yani istisna mesajında hiçbir şey olmasa bile her olayla
birlikte giderdi.
scrub.ts bu yüzden tek tek alanlara değil,
olayın tamamına uygulanır — anahtarlar dahil. Yaklaşım şekil tanımadır,
alan listesi değil: alan saymak, yeni bir alan eklendiği gün sessizce eksik
kalır.
| Şekil | Yerine | Kapsadığı |
|---|---|---|
0x + 20 ve üzeri hex | 0x<hex:N> | Adres (40), hesap/işlem/nonce/oturum (64), imza (130) |
| UUID | <uuid> | Uygulama kullanıcısı, kayıtlı kişi |
| E-posta | <email> | Google girişi |
Değer silinmez, uzunluğu bırakılır: hata ayıklayan kişi "burada bir hesap
kimliği vardı" ile "burada bir adres vardı"yı ayırt edebilmeli. Kısa
onaltılık sayılar (0x2b74 gibi chainId) korunur — kimlik değiller ve
gerçekten gerekiyorlar.
Şekil tanımayla korunamayan şeyler tümüyle silinir: user, ve istekteki
cookies / headers / data / env. Bir yetkilendirme başlığı ya da istek
gövdesi, tanınacak hiçbir desene uymadan sır taşır. request.url kalır
(temizlenmiş olarak): hangi rotanın düştüğü görülmeli.
Ayrıca sendDefaultPii: false açıkça yazılır ve başarım izleme kapalıdır
(tracesSampleRate: 0) — işlem adları URL'den türer, ve aranan şey "ne
kırıldı", "ne kadar sürdü" değil.
withSentryConfig sarmalayıcısı eklenmedi. Tek getirisi kaynak haritası
yüklemesi ve sürüm etiketlemedir; ikisi de Sentry kimlik bilgileri ister ve
karşılığında derleme yapılandırması değişir. Yığın izleri derlenmiş dosyaya
işaret eder — hangi rotanın neyle düştüğünü görmek için yeterli. Kimlik
bilgileri geldiğinde ayrı bir iş olarak eklenebilir.openai.responses.parse(...).openai/helpers/zod paketindeki
zodTextFormat(...) ile text.format olarak verilir.input_image içinde gönderilir.store: false ile yapılır; yanıt OpenAI tarafında saklanmaz.POST /api/receipts/analyze route handler'ında, Node.js runtime'da çalışır.
API anahtarı istemciye hiçbir biçimde gönderilmez, NEXT_PUBLIC_* değişkeni yoktur.Tüm para değerleri minor unit (tam sayı) olarak taşınır: 1 TL = 100 kuruş. Uygulama state'inde hiçbir yerde floating-point para tutulmaz.
Sunucudaki OpenAI çağrısı 30 saniye sonra kesilir (timeout), otomatik retry
kapalıdır (maxRetries: 0) — böylece demoda bekleme süresi öngörülebilir kalır.
Timeout, SDK'nın kendi APIConnectionTimeoutError sınıfıyla yakalanır ve
ANALYSIS_TIMEOUT koduyla 504 olarak döner. İstemci ayrıca AbortController
ile 35 saniyelik bir üst sınır uygular; istek takılırsa "Analiz zaman aşımına
uğradı" mesajı gösterilir, seçilen görsel korunur ve tekrar denenebilir.
Yukarıdaki 30 saniye uygulamanın kendi zaman aşımıdır. Bir de altında platformun kestiği süre vardır ve ikisi karıştırılmamalıdır.
Vercel'in varsayılanı — fluid compute açıkken, her planda — 300 saniyedir. Bu uygulamada hiçbir rotanın bütçesi 60 saniyeyi geçmez, yani varsayılan gerçek ihtiyacın 5-20 katıdır. Fark bedava değildir: asılı kalan bir istek beş dakika boyunca bir örneği meşgul eder ve kullanıcı o süre boyunca temiz hata mesajını göremez.
Bu yüzden her rota kendi tavanını export const maxDuration ile bildirir:
| Kademe | Süre | Nerede | Bütçe |
|---|---|---|---|
fast | 15 sn | 16 rota | Yalnızca veritabanı; ve varsa 5 sn'lik kur çağrısı |
rpc | 30 sn | payment/finalize | Üç ARDIŞIK Arc RPC çağrısı × 8 sn |
slow | 60 sn | receipts/analyze, cron/retention | OpenAI 30 sn + 10 MB gövde · altı tabloda 500'lük partiler |
Kural: tavan, rotanın kendi bütçesinin hemen ÜSTÜNDEDİR. Sıra önemlidir —
uygulama kendi zaman aşımını yakalayıp anlamlı bir hata döndürebilmelidir
(ANALYSIS_TIMEOUT → 504); platform onu daha önce keserse o hata yolu hiç
çalışmaz ve kullanıcı kopuk bir bağlantı görür.
Değer sayı sabiti olmak zorundadır: Next.js rota segmenti yapılandırmasını
derleme anında statik okur, içe aktarılmış bir sabiti çözemez. Bu yüzden ortak
bir sabit modülü yoktur; kademeler
function-duration.test.ts içinde
tanımlıdır ve orada koddaki gerçek zaman aşımı sabitlerine bağlanır. Analiz
zaman aşımı bir gün 30 saniyeden büyütülürse test düşer.
Kapsam rota işleyicileridir. Sayfalar platform varsayılanında kalır ve bu bilinçlidir: sunucu tarafında dış çağrı yapmazlar, oturum durumu JWT'den okunur.
Türkiye'deki fişlerde KDV çoğunlukla ürün satır fiyatlarının içindedir ve fişte yalnızca bilgilendirme amacıyla yazılır. Böyle bir KDV'yi ürün toplamının üzerine tekrar eklemek yanlış genel toplam ve ileride çift borçlandırma üretir.
Bu yüzden vergi, servis ücreti ve indirimin her biri kendi treatment değerini taşır:
| Değer | Anlamı |
|---|---|
included_in_items | Tutar ürün fiyatlarının içinde; toplama tekrar uygulanmaz |
separate | Vergi/servis ürünlerin üzerine eklenir, indirim düşülür |
unknown | Fişten güvenle anlaşılamadı |
Model bu değeri fişte görünen matematikten belirler (ürün toplamları, ara
toplam ve genel toplam ilişkisinden), "KDV" satırının varlığından değil.
Belirleyemezse unknown döner.
Toplam kontrolü yalnızca separate işaretli kalemleri uygular. Sıfırdan farklı
bir kalem unknown ise yanlış bir uyuşmazlık iddiası üretilmez; sonuç
"belirsiz" olarak gösterilir. Kullanıcı her satırdaki seçimi arayüzden
değiştirebilir ve toplam kontrolü anında güncellenir; hiçbir değer otomatik
olarak değiştirilmez.
src/lib/arc/transfer-client.ts, doğrudan viem ile: simüle et → gönder →
makbuzu oku):
standalone-request-feature.ts): akış
sunucusuzdur, tekrar oynatma engeli yereldir ve gerçek parada bu kabul
edilemez; test ağı olmayan bir profilde /pay ödeyiciyi hiç kurmaz ve
oluşturma ekranı bu akışı sunmaz.Başlıklar src/lib/security/headers.ts içinde VERİ olarak durur;
next.config.ts yalnızca listeyi bağlar. Böylece testler gerçek değerleri
okuyabiliyor.
Bugün uygulananlar: X-Frame-Options: DENY, X-Content-Type-Options,
Referrer-Policy, Permissions-Policy. Hepsi CSP'den bağımsız çalışır ve
hiçbir isteği kırma riski taşımaz.
En somut kazanç çerçeveleme: ödeme sayfası daha önce birinin sitesinde
iframe'e alınabiliyordu. X-Frame-Options bunu bugün kapatıyor.
CSP uygulanıyor — connect-src hariç. İkisi birden gönderiliyor:
| Başlık | Kapsam |
|---|---|
Content-Security-Policy | connect-src dışında her şey zorlayıcı |
Content-Security-Policy-Report-Only | yalnızca connect-src katı, ölçüyor |
Kapsamı kanıt belirledi. Üretimde tam bir ödeme akışı rapor kipiyle
çalıştırıldı ve connect-src dışında tek bir ihlal bile çıkmadı; yani
frame-ancestors, frame-src, img-src, style-src, worker-src
kısıtlarının hepsi ölçülmüş, tahmin edilmemiştir.
script-src nonce ile katı; 'unsafe-inline' yok. Saklamasız bir cüzdan
uygulamasında tek gerçek saldırı yüzeyi ön yüzdür: sayfaya sızan bir satır
içi betik alıcı adresini değiştirir ve kullanıcı cüzdanda onaylar — her
doğrulama geçer, çünkü doğrulamayı yapan kod da ele geçmiştir. Bu yüzden CSP
statik bir başlık olamaz: src/proxy.ts her isteğe 16 baytlık taze bir nonce
üretir (src/lib/security/csp-proxy.ts), onu isteğin
content-security-policy başlığına (Next kendi önyükleme betiklerini oradan
damgalar) ve x-nonce başlığına (düzen, tema betiğini oradan damgalar) yazar.
İstemciden gelen x-nonce üzerine yazılır; dışarıdan seçilmiş bir nonce
saldırganın kendi betiğini damgalaması olurdu. next.config.ts'teki statik
liste artık CSP taşımaz — iki yerden basılsaydı tarayıcı ikisini birden
uygular, nonce'suz olan her betiği engellerdi; bir test bunu zorlar.
style-src bilerek 'unsafe-inline' kalıyor: Tailwind ve Next satır içi
stil üretir, stil enjeksiyonu betik gibi cüzdana ulaşamaz.
connect-src neden hâlâ ölçümde: o ölçüm yalnızca masaüstü/eklenti borçlu
akışını kapsadı. Mobil WalletConnect yolu ve hesabı oluşturan akış henüz
çalıştırılmadı. Eksik bir liste parayı göndermeyi kırar; onlar da temiz
çıktığında uygulanana taşınacak.
Bir test, iki politikanın connect-src dışında birebir aynı kalmasını
zorluyor — ayrışırlarsa ölçüm, uygulananla ilgisiz bir şeyi ölçmeye başlar.
Uygulama yalnızca hukuken çalışabildiği ülkelerde açılır. Liste
ALLOWED_COUNTRIES ortam değişkeninden gelir (ISO 3166-1 alpha-2, virgülle:
US,DE). Değişken tanımsız ya da boşsa kapı yoktur ve herkes girer —
bugünkü testnet dağıtımı böyle çalışır. Kod src/lib/security/geo-gate.ts,
sıralama src/proxy.ts: önce kapı, sonra CSP; engellenen istek betiksiz bir
451 sayfası alır (dil çerezden ya da tarayıcıdan, metin sözlükten) ve o
sayfa da nonce'lu CSP taşır.
Üç davranış bilinçlidir:
cf-ipcountry, Vercel
x-vercel-ip-country. Başlık yoksa ülke bilinmiyor sayılır ve kapı
açıkken içeri alınmaz (fail-closed). Vekili atlayıp doğrudan sunucuya
gelen istek kapıyı da atlayamaz.USA ya da D3 ilk istekte
fırlatır ve dağıtım hemen görünür şekilde düşer. Sessizce açık kalan bir
kapı, hiç kapı olmamasından kötüdür.Başlığa güvenmenin ön koşulu sunucudadır ve koddan yapılamaz: 80/443
yalnızca Cloudflare'ın IP aralıklarına açık olmalı, yoksa origin'e doğrudan
gelen biri cf-ipcountry: DE yazıp geçer. Kontrol listesi (bir kez, VPS'te):
for ip in $(curl -s https://www.cloudflare.com/ips-v4) $(curl -s https://www.cloudflare.com/ips-v6); do ufw allow proto tcp from "$ip" to any port 80,443; done
ufw delete allow 80/tcp && ufw delete allow 443/tcp && ufw status numbered
nginx'e de gerçek istemci adresini öğretmek gerekir (set_real_ip_from ile
aynı aralıklar, real_ip_header CF-Connecting-IP); bugün IP tabanlı hiçbir
kısıt Cloudflare'ın adresini görmesin diye bu da bekliyordu. Cloudflare
aralıkları nadiren değişir; listeyi yenilemek aynı iki komuttur.
Play Console'daki ülke listesi ayrı bir katmandır ve mağaza görünürlüğünü belirler; bu kapı ise siteye kim girerse girsin uygulanır.
İki politika da report-uri /api/csp-report taşıyor. Konsola bakmak
masaüstünde kolay ama telefonda neredeyse imkânsız; connect-src'yi
engelleyici yapabilmek için mobil yolun ne yaptığını da bilmemiz gerekiyor.
Uç sayesinde cihaz kendisi bildiriyor ve satırlar Vercel günlüklerine düşüyor:
[csp] connect-src <- https://ornek.invalid (report)
Gizlilik, bu ucun en önemli kısıtı. Rapor gövdesi sayfanın TAM ADRESİNİ
taşır ve ortak hesap adresleri billId içerir — o bağlantıyı bilen herkes
hesabı açabilir. Bu yüzden günlüğe yalnızca yönerge ve engellenen adresin
KÖKENİ yazılır; sayfanın adresi ve engellenen adresin yolu/sorgusu atılır.
Bir test bunu zorluyor: billId benzeri bir dizge günlük satırında geçerse
test düşer.
Uç kimlik doğrulaması İSTEMEZ ve isteyemez — raporu tarayıcı, oturumdan bağımsız gönderir. Bu yüzden hiçbir şey okumaz, hiçbir şey yazmaz ve veritabanına dokunmaz; tek yaptığı kısa bir satır günlüğe düşürmek.
connect-src, tarayıcının bağlandığı adreslerle sınırlıdır ve bir test bu
listeyi gizlilik politikasındaki DISCLOSED_HOSTS ile karşılaştırır — yeni bir
dış bağlantı, politikaya da yazılmadan geçemez. OpenAI ve veritabanı burada
YOKTUR: onlara sunucu bağlanır.
Politikanın alan adı taraması kaynak kodu okur. Bir adres bağımlılığın içindeyse tarama onu göremez.
Üretimde gerçek bir ödeme akışı sırasında tam olarak bu oldu: uygulama
rpc.testnet.arc.io kullanıyor, ama Circle App Kit viem'in gömülü
arcTestnet tanımını kullandığı için tarayıcı ayrıca
rpc.testnet.arc.network adresine de bağlanıyor. İkinci sunucu politikada
bildirilmemişti ve hiçbir test bunu yakalayamazdı.
Ortaya çıkaran şey CSP'nin rapor kipi oldu. Bu, kipin varlık sebebi: bağımlılıkların gerçekte nereye bağlandığını çalışırken ölçmek.
Makbuz doğrulaması önceden tek genel adrese bağlıydı: rpc.testnet.arc.io
yavaşladığında ya da hız sınırına takıldığında makbuz okunamıyor ve ödeme
kesinleştirilemiyordu. Genel testnet RPC'leri bunu düzenli olarak yapar, ve
bu, düzeltilmesi en pahalı hata sınıfının kapısıdır: zincirde işlem
başarılıyken uygulamanın onu görememesi.
Artık Arc'ın resmî dokümanındaki adresler sırayla denenir — birincil,
sonra Blockdaemon, dRPC ve QuickNode. Hepsi aynı zinciri sunar ve
arc-receipt.ts her çağrıda chainId'yi ayrıca doğrular: yanlış zincire bağlı
bir yedek sonucu kabul ettiremez.
Süre bütçesi DEĞİŞMEDİ ve bu bilinçli. finalize üç ardışık RPC çağrısı
yapar; her uca ayrı ayrı 8 saniye verilseydi en kötü durum uç sayısıyla
çarpılır ve rotanın süre tavanını aşardı. Toplam sabit kalır, uçlara bölünür.
Takas açık: yavaş ama çalışan bir birincilden erken devredilir — sağlıklı bir
yedeğe geçmek, ölü bir uçta sekiz saniye beklemekten iyidir.
Yeniden deneme kapalıdır (retryCount: 0): burada "tekrar denemek",
sıradaki uca geçmektir. Aynı ölü uca ısrar etmek bütçeyi yer ve yedeğe hiç
sıra gelmezdi. Sıralama da ölçüme bırakılmaz (rank: false) — gecikme ölçen
bir sıralayıcı fazladan istek atar ve devretmeyi öngörülemez kılar.
Tarayıcının yüzeyi büyümez. Yedeklere yalnızca sunucu bağlanır: cüzdana
bildirilen adres (wallet_addEthereumChain) hâlâ tek, ve CSP'nin
connect-src listesi olduğu gibi kalır. Üçü de testle tutulur
(rpc-fallback.test.ts).
Gizlilik politikası ise genişler: tarayıcı gitmiyor diye bildirim
gerekmiyor değil — uygulama o sunuculara bağlanıyor. Üç adres de
DISCLOSED_HOSTS içindedir ve politika metni artık sunucunun da birden fazla
sağlayıcıya sorduğunu söyler.
Kalan yarım: RPC'ye hiç ulaşılamadığında sonuç zaten "başarısız" değil
unavailable sayılıyor ve kilit korunuyor — ama sunucu tarafında o kaydı
sonradan yeniden denemeye alan bir görev yok. Kullanıcı uygulamayı
kapatırsa uzlaştırma yalnızca istemcinin yoklamasına bağlı kalır.
script-srcscript-src bilerek 'unsafe-inline' içerir. Ölçüldü: sayfa 10 satır içi
script taşıyor (Next.js önyüklemesi ve temanın yanıp sönmeyi önleyen script'i)
ve hiçbirinde nonce yok; kaldırılırsa uygulama açılmaz.
Sıkılaştırmak, her istekte nonce üreten bir middleware ister. O gelene kadar
katı sürüm Content-Security-Policy-Report-Only olarak yayında duruyor ve
ihlalleri tarayıcı konsoluna yazıyor — önce ölç, sonra uygula.
Google girişi vardı ama sayı sınırı yoktu: oturum açmış bir kullanıcı istediği kadar analiz çağırabiliyordu ve her çağrı OpenAI'de gerçek para harcıyor. Üst sınırı olmayan tek maliyet buydu.
İki sayaç, iki farklı iş (src/lib/receipt/quota.ts):
| Sayaç | Ne yapar | Değer |
|---|---|---|
| Kullanıcı başına | Adalet — bir kişi diğerlerinin hakkını yiyemez | 25/gün |
| Genel tavan | Fatura — hesap sayısından bağımsız | 250/gün |
Kullanıcı başına kota tek başına faturayı korumaz: Google hesabı açmak bedava ve sınırsızdır. Genel tavan bunun içindir.
Atomiklik — iki sayaç, TEK işlem. Genel tavan ve kullanıcı hakkı birlikte
ayrılır: ya ikisi de düşülür ya da hiçbiri. Üç deyim tek işlemde çalışır —
iki satırı sayaç artırmadan var et, quota_key sırasına göre kilitle
(FOR UPDATE), sonra ikisini de oku ve ikisi de sınırın altındaysa tek
UPDATE ile ikisini birden artır. Sınır kontrolü yazan değil okuyan
CTE'ye dayanır; kardeş CTE'ler birbirinin yazdığını görmez.
Düzeltilen kusur. Eskiden ikisi ayrı ayrı düşülüyordu ve burada sıranın "güvenli yöne saptığı" yazıyordu. Yanlıştı: genel tavan ÖNCE düşüldüğü için hakkı dolmuş bir kullanıcının reddedilen her isteği genel sayacı artırıyordu. O kullanıcı tek başına genel tavanı tüketip diğer herkesi kilitleyebiliyordu — hiç OpenAI çağrısı yapmadan. Sırayı ters çevirmek çözüm değildi; o zaman da genel tavan dolduğunda kullanıcı hakkı boşuna yanardı.
Reddedilen istek hak tüketmez. Hangi sınıra takılırsa takılsın hiçbir sayaç artmaz. Sağlayıcı hatası ise ayrı: kabul edilmiş bir istek OpenAI tarafında düşerse hak geri verilmez, bu bilinçli bir üründür kararıdır.
Arayüz iki sınırı ayırır. DAILY_LIMIT_REACHED kişinin kendi hakkının
bittiğini söyler ve kalan sıfıra çekilir; SERVICE_BUSY genel tavanın
dolduğunu söyler ve kişisel hakka dokunulmaz. Karar
src/lib/receipt/quota-feedback.ts içinde saf bir fonksiyondur.
Ne zaman düşülür. Bütün doğrulamalardan sonra, OpenAI'ye gitmeden önce. Bozuk bir dosya yüzünden hak yanmaz; sağlayıcıya ulaşan her deneme sayılır.
Gün UTC'dir. current_date veritabanının saat dilimine bağlıdır; sınırın
ne zaman sıfırlandığı sunucu ayarına bırakılmaz.
Kullanıcı kalan hakkını yükleme ekranında görür; sayıyı sunucu bildirir, istemci saymaz.
Kapı (lint, typecheck, test, build, git diff --check) her PR'da
GitHub Actions ile zorunlu çalışır: .github/workflows/ci.yml.
Asıl kazanç tek tek PR'lar değil, birleşme sonucu. Bu projede bir kez
yaşandı: #14–#18 ayrı ayrı yeşilken, birleşmiş hâlde bir test kırıldı —
gizlilik politikasının alan adı taraması, başka bir daldan gelen SVG'nin
xmlns değerini yakaladı. GitHub pull_request olayında PR'ı hedef dalla
birleştirip çalıştırdığı için tam da o sınıfı yakalar.
CI'ın yapamadıkları: cüzdan akışı, fiş analizi ve gerçek bir Postgres burada çalışmaz. Kapsam artmıyor, mevcut kapı zorunlu hâle geliyor.
Kapının kendisi de korunuyor: src/lib/ci/workflow.test.ts, package.json
betikleriyle CI'da çalıştırılanları karşılaştırır — bir adım sessizce
düşerse test kırılır.
.nvmrc ve engines bilerek eklenmedi: ikisi de Vercel'in derleme
imajını değiştirir ve bu ayrı, açıkça istenmesi gereken bir karardır.
src/lib/db/migrations.test.ts numaralandırmayı zorlar. Sebep bu depoya
özgü: birden fazla dal art arda merge ediliyor ve iki dal da 0005_ eklerse
merge sırasında çakışma olmaz — iki dosya sorunsuzca yan yana durur ve
sıra belirsizleşir. Git bunu yakalamaz, çünkü metin çakışması yoktur.
Bir denetimde çıkan ve kasıtlı olarak ele alınmayan maddeler. Her incelemede yeniden tartışılmasın diye gerekçeleriyle burada:
Merkezi hata izleme (Sentry vb.) eklenmedi. Yeni bir üçüncü taraf demektir; bu projede bedeli yüksektir çünkü gizlilik politikası kodda geçen her dış alan adını bildirmeye zorlar ve bir test bunu denetler. Hata yükleri cüzdan adresi taşıyabilir. Vercel'in kendi günlükleri şu ölçek için yeterli.
Playwright/Cypress E2E yok. En riskli yollar — cüzdan bağlama, imza, transfer — tarayıcı otomasyonuyla sürülemez. Geriye kalan kısım zaten birim testleriyle kaplı; kazanç "parçalar tarayıcıda birbirine bağlanıyor mu" sorusuyla sınırlı kalır.
Büyük dosyalar bölünmedi. neon-shared-bill-repository.ts'te bütün
SQL'in tek yerde olması bilinçlidir (eşleşme testleri bu dosyayı okur),
tr.ts Dictionary tipinin kaynağıdır, send.ts'e dokunulmaz. Satır
sayısı için yeniden düzenleme, kullanıcıya hiçbir şey kazandırmadan gerileme
riski taşır.
npm audit bulguları düzeltilmedi, takip ediliyor. Aşağıdaki tablo
ölçümdür, tahmin değil: derlenmiş istemci ve sunucu paketleri tarandı.
24 bulgu 5 kök uyarıdan ve 4 paketten geliyor; dördü de
@circle-fin/app-kit / @circle-fin/adapter-viem-v2 zincirinden.
| Paket | Şiddet | İstemci paketinde | Sunucu paketinde | Uygulama yolunda | Güvenilmeyen girdi |
|---|---|---|---|---|---|
toml | high ×2 | hayır | evet | hayır | hayır |
stream-json | moderate | hayır | hayır | hayır | hayır |
elliptic | low | evet | evet | dolaylı | hayır |
uuid | moderate | evet | evet | hayır | hayır |
Ayrıntılar:
toml (@coral-xyz/anchor → app-kit). Uyarılar güvenilmeyen TOML
ayrıştırmakla ilgili. Sunucu paketinde bulunuyor ama çağıran kod,
Anchor'ın Solana çalışma alanı yükleyicisi: girdisi
readFileSync("Anchor.toml"), yani yerel bir dosya, kullanıcı girdisi
değil. Bu uygulamanın kaynağında / ve
dağıtımda böyle bir dosya yok.anchorsolanastream-json (@solana/web3.js → jayson). Hiçbir pakette yok.elliptic (ethers v5 → @ethersproject/signing-key). İstemci
paketinde var. Ama uygulamanın kaynağında ethers hiç geçmiyor;
imzayı kullanıcının cüzdanı atar. Ayrıca yamalı sürüm yok: kurulu
6.6.1, npm'deki en son sürüm de 6.6.1.uuid. Uyarı v3/v5/v6yı buf argümanıyla çağırmayı gerektiriyor.
Uygulama uuid paketini hiç kullanmıyor; kimlikler node:crypto
randomUUID'den geliyor.npm'in önerdiği tek çare, dördü için de @circle-fin/app-kit'i
1.12.1'den 1.8.1'e DÜŞÜRMEK. Çalıştırılmayan kod yollarındaki uyarılar
için ödeme kütüphanesini geri almak kötü bir takas; bu yüzden yapılmadı.
Ne npm audit fix ne de --force çalıştırılır.
Yeniden bakma koşulu: app-kit yükseltildiğinde ya da elliptic için
yamalı bir sürüm çıktığında tablo yeniden ölçülür. Tarama komutu:
npm audit --omit=dev --json, sonra derlenmiş paketlerde paket izleri.