// Yeti Cüzdan JavaScript istemcisi (Node 18+ ya da fetch + node:crypto olan sunucu ortamı; Supabase Edge, Deno). // Kaynak: projects/yeti-cuzdan/sdk/js/yeti-cuzdan.mjs — bağımlılık yok, uygulamaya kopyalanır. Belgeler: sdk/README.md. // Yalnız SUNUCUDA kullanın: API anahtarı ve sırlar tarayıcıya ya da mobil uygulamaya girmez. // // const cuzdan = new YetiCuzdan({ adres: 'https://cuzdan.example', apiAnahtari: process.env.YETI_CUZDAN_ANAHTAR, // webhookSirri: process.env.YETI_CUZDAN_WEBHOOK_SIRRI }); // // Tutarlar tam sayı Yeti'dir (1 Yeti = 1 kuruş). Yeti hareketi yapan çağrılar tekrar anahtarı ister: aynı anahtarla // tekrar gönderilen istek ikinci kez işlem yapmaz. import { createHash, createHmac, randomBytes, timingSafeEqual } from 'node:crypto'; export const SURUM = '1.0.0'; const base64url = (tampon) => Buffer.from(tampon).toString('base64').replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, ''); /** Cüzdanın döndürdüğü hata: `kod` makine-okur ('baglanti_yok', 'yetersiz_bakiye', 'hizmet_kapali', ...). */ export class YetiCuzdanHatasi extends Error { constructor(mesaj, kod, durum, yanit = null) { super(mesaj); this.name = 'YetiCuzdanHatasi'; this.kod = kod; this.durum = durum; this.yanit = yanit; } } /** "t=,v1=" — cüzdan webhook'larının ve Yeduty görev bildirimlerinin biçimi. */ export function imzala(govde, sir, zaman = Math.floor(Date.now() / 1000)) { return `t=${zaman},v1=${createHmac('sha256', sir).update(`${zaman}.${govde}`).digest('hex')}`; } export class YetiCuzdan { /** * @param {{ adres: string, apiAnahtari: string, webhookSirri?: string, zamanAsimiMs?: number, fetch?: typeof fetch }} ayar */ constructor({ adres, apiAnahtari, webhookSirri = null, zamanAsimiMs = 15000, fetch: getir = globalThis.fetch }) { this.adres = adres.replace(/\/+$/, ''); this.apiAnahtari = apiAnahtari; this.webhookSirri = webhookSirri; this.zamanAsimiMs = zamanAsimiMs; this.getir = getir; } // --- Yeti ile giriş (PKCE) ------------------------------------------------------------------------------- static yeniDurum() { return randomBytes(20).toString('hex'); } static yeniDogrulayici() { return base64url(randomBytes(48)); } /** base64url(sha256(doğrulayıcı)) — RFC 7636 S256 */ static kodOzeti(dogrulayici) { return base64url(createHash('sha256').update(dogrulayici).digest()); } girisAdresi(uygulamaKodu, donus, durum, dogrulayici = null) { const sorgu = new URLSearchParams({ donus, durum }); if (dogrulayici) sorgu.set('kod_ozeti', YetiCuzdan.kodOzeti(dogrulayici)); return `${this.adres}/baglan/${encodeURIComponent(uygulamaKodu)}?${sorgu}`; } baglantiCoz(kod, dogrulayici = null) { return this.#istek('POST', '/baglanti', dogrulayici ? { kod, kod_dogrulayici: dogrulayici } : { kod }); } // --- Yeti ile öde ------------------------------------------------------------------------------------------ /** secenek: { siparis_no, donus_url, iptal_url, yeti_kodu } */ odemeTalebi(tutar, aciklama, tekrarAnahtari, secenek = {}) { return this.#istek('POST', '/odeme-talepleri', { tutar: String(tutar), aciklama, ...secenek }, tekrarAnahtari); } odemeTalebiGetir(id) { return this.#istek('GET', `/odeme-talepleri/${encodeURIComponent(id)}`); } /** tutar null ise kalanın tamamı */ iadeEt(id, tutar, tekrarAnahtari) { return this.#istek('POST', `/odeme-talepleri/${encodeURIComponent(id)}/iade`, tutar == null ? {} : { tutar: String(tutar) }, tekrarAnahtari); } // --- İzin, harcama, ödül, bildirim ------------------------------------------------------------------------- izin(yetiKodu) { return this.#istek('GET', `/izinler/${encodeURIComponent(yetiKodu)}`); } harca(yetiKodu, tutar, aciklama, tekrarAnahtari) { return this.#istek('POST', '/harcamalar', { yeti_kodu: yetiKodu, tutar: String(tutar), aciklama }, tekrarAnahtari); } odulVer(yetiKodu, tutar, aciklama, tekrarAnahtari, kova = 'kazanilan') { return this.#istek('POST', '/oduller', { yeti_kodu: yetiKodu, tutar: String(tutar), aciklama, kova }, tekrarAnahtari); } bildirimGonder(yetiKodu, baslik, govde, baglanti, tekrarAnahtari) { const veri = { yeti_kodu: yetiKodu, baslik, govde }; if (baglanti) veri.baglanti = baglanti; return this.#istek('POST', '/bildirimler', veri, tekrarAnahtari); } // --- Çevirici ---------------------------------------------------------------------------------------------- async cevirmeler(durum = null) { return (await this.#istek('GET', `/cevirmeler${durum ? `?durum=${encodeURIComponent(durum)}` : ''}`)).veri ?? []; } cevirme(id) { return this.#istek('GET', `/cevirmeler/${encodeURIComponent(id)}`); } cevirmeTeslim(id) { return this.#istek('POST', `/cevirmeler/${encodeURIComponent(id)}/teslim`, {}); } cevirmeIade(id, tekrarAnahtari, neden = null) { return this.#istek('POST', `/cevirmeler/${encodeURIComponent(id)}/iade`, neden ? { neden } : {}, tekrarAnahtari); } durum() { return this.#istek('GET', '/durum'); } // --- Webhook ----------------------------------------------------------------------------------------------- /** "Yeti-Imza"yı doğrular (≤ 5 dk) ve olayı döndürür: { id, olay, olusturma, veri }; geçersizse null. */ webhookDogrula(govde, imzaBasligi, simdi = Math.floor(Date.now() / 1000)) { const m = /^t=(\d+),v1=([a-f0-9]{64})$/.exec(String(imzaBasligi ?? '').trim()); if (!this.webhookSirri || !m || Math.abs(simdi - Number(m[1])) > 300) return null; const beklenen = Buffer.from(createHmac('sha256', this.webhookSirri).update(`${m[1]}.${govde}`).digest('hex')); const gelen = Buffer.from(m[2]); if (beklenen.length !== gelen.length || !timingSafeEqual(beklenen, gelen)) return null; try { const olay = JSON.parse(govde); return olay && typeof olay === 'object' ? olay : null; } catch { return null; } } // --- İç ---------------------------------------------------------------------------------------------------- async #istek(yontem, yol, govde = null, tekrarAnahtari = null) { const basliklar = { Authorization: `Bearer ${this.apiAnahtari}`, Accept: 'application/json', 'User-Agent': `YetiCuzdanJS/${SURUM}` }; if (govde !== null) basliklar['Content-Type'] = 'application/json'; if (tekrarAnahtari) basliklar['Idempotency-Key'] = tekrarAnahtari; let yanit; try { yanit = await this.getir(`${this.adres}/api/v1${yol}`, { method: yontem, headers: basliklar, body: govde === null ? undefined : JSON.stringify(govde), signal: AbortSignal.timeout(this.zamanAsimiMs), }); } catch (e) { throw new YetiCuzdanHatasi(`Yeti Cüzdan'a ulaşılamadı: ${e.message}`, 'baglanti_yok', 0); } const veri = await yanit.json().catch(() => null); if (yanit.ok && veri && typeof veri === 'object') return veri; throw new YetiCuzdanHatasi(veri?.mesaj ?? `Yeti Cüzdan yanıt vermedi (HTTP ${yanit.status}).`, veri?.hata ?? `http_${yanit.status}`, yanit.status, veri); } } /** Yeduty'de "uygulama doğrular" görevleri: üye görevi tamamlayınca bildirin, ödül kendiliğinden ödenir. */ export class YedutyGorevleri { constructor({ yedutyAdresi, gorevSirri, fetch: getir = globalThis.fetch }) { this.adres = yedutyAdresi.replace(/\/+$/, ''); this.sir = gorevSirri; this.getir = getir; } async tamamlandi(gorevKimligi, yetiKodu, referans) { const govde = JSON.stringify({ gorev: gorevKimligi, yeti_kodu: yetiKodu, referans }); const yanit = await this.getir(`${this.adres}/api/gorev-tamamlandi`, { method: 'POST', headers: { 'Content-Type': 'application/json', Accept: 'application/json', 'Yeduty-Imza': imzala(govde, this.sir) }, body: govde, }); const veri = await yanit.json().catch(() => null); if (yanit.status === 200 && veri) return veri; throw new YetiCuzdanHatasi(veri?.mesaj ?? `Yeduty yanıt vermedi (HTTP ${yanit.status}).`, veri?.hata ?? `http_${yanit.status}`, yanit.status, veri); } }