// D09 read-only production probe. It checks public configuration, source reads // and unauthenticated guards. It never signs in, sends a message, creates a // checkout, calls an AI model, changes data or submits a tender. import { mkdir, writeFile } from 'node:fs/promises'; import { dirname } from 'node:path'; const base = String(process.argv[2] || 'https://ihaleist.com').replace(/\/$/, ''); const evidencePath = process.env.IHALEIST_LIVE_INTEGRATIONS_EVIDENCE_PATH || 'docs/quality/evidence/qa-live-d09-integrations.json'; const timeoutMs = 45_000; async function request(path, options = {}) { const controller = new AbortController(); const timer = setTimeout(() => controller.abort(), timeoutMs); try { const response = await fetch(`${base}${path}`, { ...options, signal: controller.signal, headers: { 'User-Agent': 'ihaleist-live-integrations-probe/1.0', ...(options.headers || {}), }, }); const contentType = response.headers.get('content-type') || ''; const body = await response.text(); let json = null; if (contentType.includes('json')) { try { json = JSON.parse(body); } catch { /* malformed JSON stays visible as a failed check */ } } return { path, method: options.method || 'GET', status: response.status, contentType, json, textChars: body.length, error: response.ok ? null : String(json?.error?.message || json?.error || body || `HTTP ${response.status}`).slice(0, 300), }; } catch (error) { return { path, method: options.method || 'GET', status: 0, contentType: '', json: null, textChars: 0, error: error?.message || String(error) }; } finally { clearTimeout(timer); } } const raw = {}; for (const [name, path, options] of [ ['google', '/api/auth/google/config'], ['tcmb', '/api/exchange-rates'], ['documents', '/api/documents/config'], ['ai', '/api/ai/config'], ['billing', '/api/billing/plans'], ['telegram', '/api/health/takip'], ['scheduler', '/api/health/scheduler'], ['items', '/api/health/kalem'], ['embedding', '/api/health/uyum'], ['bik', '/api/stats'], ['history', '/api/history/stats'], ['resultsRss', '/api/feeds/sonuc.rss?limit=1'], ['protectedRss', '/api/feeds/tenders.rss'], ['manifest', '/manifest.webmanifest'], ['serviceWorker', '/sw.js'], ['ted', '/api/ted?mod=turkiye&since=20990101&limit=1'], ['kik', '/api/kik/search?limit=1'], ['okas', '/api/okas/ara?q=hemogram'], ['mcpGuard', '/mcp', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'initialize', params: {} }) }], ]) { raw[name] = await request(path, options); } const guardPaths = { authSession: ['/api/auth/me'], documents: ['/api/me/documents'], documentText: ['/api/documents/text?ikn=2026%2F1634449'], apiKeys: ['/api/me/apikeys'], emailConfig: ['/api/notify/email/config'], plan: ['/api/me/plan'], subscriptions: ['/api/notify/subscriptions'], sourceHistory: ['/api/tenders/source-history?ikn=2026%2F1634449'], companyScan: ['/api/company/scan', { method: 'POST' }], aiAnalyze: ['/api/ai/analyze', { method: 'POST' }], checkout: ['/api/billing/checkout', { method: 'POST' }], emailSend: ['/api/notify/email/test', { method: 'POST' }], telegramMutation: ['/api/me/telegram', { method: 'POST' }], notificationRun: ['/api/notify/run', { method: 'POST' }], }; const guards = {}; for (const [name, [path, options]] of Object.entries(guardPaths)) { const probe = await request(path, options); guards[name] = { status: probe.status, passed: [401, 403].includes(probe.status) }; } function row(id, label, status, evidence, remaining) { return { id, label, status, evidence, remaining }; } const googleConfigured = raw.google.status === 200 && Boolean(raw.google.json?.clientId); const tcmbReady = raw.tcmb.status === 200 && raw.tcmb.json?.success === true && Number(raw.tcmb.json?.rates?.USD) > 0 && Number(raw.tcmb.json?.rates?.EUR) > 0; const aiConfigured = raw.ai.status === 200 && raw.ai.json?.configured === true; const billingConfigured = raw.billing.status === 200 && raw.billing.json?.configured === true; const billingWebhookReady = raw.billing.status === 200 && raw.billing.json?.webhookHazir === true; const telegramConfigured = raw.telegram.status === 200 && raw.telegram.json?.telegram === true; const kikManualGate = raw.kik.status === 428 && raw.kik.json?.code === 'HUMAN_VERIFICATION_REQUIRED'; const ekapManualGate = raw.okas.status === 428 && raw.okas.json?.code === 'HUMAN_VERIFICATION_REQUIRED'; const integrations = [ row('BIK', 'ilan.gov.tr ihale havuzu', raw.bik.status === 200 && raw.bik.json?.success ? 'passed' : 'blocked', { status: raw.bik.status, source: raw.bik.json?.kaynak || null, loaded: raw.bik.json?.yuklenenIlan ?? null }, 'Güncel liste okuması doğrulandı; veri kalitesi D10/D04 kapsamında izlenir.'), row('TCMB', 'TCMB döviz satış kurları', tcmbReady ? 'passed' : 'blocked', { status: raw.tcmb.status, bulletinDate: raw.tcmb.json?.bulletinDate || null, source: raw.tcmb.json?.source || null, cached: raw.tcmb.json?.cached ?? null }, tcmbReady ? 'Hata ve eski bülten UX testleri yerel otomasyonda kapsanıyor.' : raw.tcmb.error), row('GOOGLE', 'Google ile giriş', googleConfigured ? 'configured' : 'blocked', { status: raw.google.status, configured: googleConfigured }, 'Gerçek test hesabıyla giriş, çıkış ve oturum yenileme kabulü gerekli.'), row('EKAP', 'EKAP arama ve resmi doküman', raw.okas.status === 200 ? 'partial' : ekapManualGate ? 'manual-gate' : 'blocked', { browserAvailable: raw.documents.json?.tarayiciVar ?? null, okasStatus: raw.okas.status, okasCode: raw.okas.json?.code || null, d10DocumentCode: 'HUMAN_VERIFICATION_REQUIRED' }, 'Yetkili kullanıcı güvenlik doğrulamasından sonra beş resmi paketi indirip yüklemeli.'), row('KIK', 'KİK kurul kararları', raw.kik.status === 200 && raw.kik.json?.success ? 'passed' : kikManualGate ? 'manual-gate' : 'blocked', { status: raw.kik.status, code: raw.kik.json?.code || null, resultCount: raw.kik.json?.results?.length ?? null, error: raw.kik.error }, raw.kik.status === 200 ? 'Bir kararın ayrıntı bağlantısı ayrıca kullanıcı akışında açılmalı.' : kikManualGate ? 'EKAP güvenlik doğrulamasından sonra arama ve karar metni tekrar denenmeli.' : 'Kaynak erişimi düzeltildikten sonra arama ve karar metni tekrar denenmeli.'), row('TED', 'TED Avrupa ihaleleri', raw.ted.status === 200 && raw.ted.json?.success ? 'passed' : 'blocked', { status: raw.ted.status, totalCount: raw.ted.json?.totalCount ?? null, timedOut: raw.ted.json?.timedOut ?? null, error: raw.ted.error }, raw.ted.status === 200 ? 'Gerçek sonuçlu sorgu ve çeviri, kontrollü AI test bütçesiyle yapılmalı.' : 'TED kaynağı tekrar denenmeli.'), row('AI', 'AI analiz ve teklif yardımcıları', aiConfigured ? 'configured' : 'blocked', { status: raw.ai.status, configured: aiConfigured, providerCount: Array.isArray(raw.ai.json?.providers) ? raw.ai.json.providers.length : 0 }, 'Yetkili test hesabı ve incelemesi tamamlanmış test belgesiyle analiz, soru, taslak ve denetim zinciri gerekli.'), row('STRIPE', 'Stripe plan, ödeme ve webhook', billingConfigured && billingWebhookReady ? 'configured' : 'blocked', { status: raw.billing.status, checkoutConfigured: billingConfigured, webhookConfigured: billingWebhookReady, planCount: Array.isArray(raw.billing.json?.plans) ? raw.billing.json.plans.length : 0 }, 'Yalnız Stripe test ortamında başarılı/başarısız ödeme, webhook tekrarı ve iptal senaryoları gerekli.'), row('EMAIL', 'E-posta bildirimleri', guards.emailConfig?.passed ? 'guarded' : 'blocked', { unauthenticatedStatus: guards.emailConfig?.status ?? null }, 'Yetkili test hesabında yapılandırma okunmalı; izinli test alıcısına tek gönderim ayrıca onaylanmalı.'), row('TELEGRAM', 'Telegram bağlantısı ve bildirim', telegramConfigured ? 'configured' : 'blocked', { status: raw.telegram.status, configured: telegramConfigured }, 'Test sohbetinde bağlama kodu ve tek test mesajı için açık gönderim yetkisi gerekli.'), row('PWA', 'PWA manifest ve offline worker', raw.manifest.status === 200 && raw.serviceWorker.status === 200 ? 'passed' : 'blocked', { manifestStatus: raw.manifest.status, serviceWorkerStatus: raw.serviceWorker.status }, 'Gerçek cihazda kurulum, offline düzenleme ve yeniden bağlanma kabulü ayrıca izlenir.'), row('API', 'API anahtarı, RSS ve MCP', raw.protectedRss.status === 401 && raw.mcpGuard.status === 401 && guards.apiKeys?.passed ? 'guarded' : 'blocked', { apiKeyGuard: guards.apiKeys?.status ?? null, rssGuard: raw.protectedRss.status, mcpGuard: raw.mcpGuard.status, publicResultsRssStatus: raw.resultsRss.status }, 'Yetkili test hesabında anahtar oluşturma, RSS/MCP kullanma ve iptal zinciri gerekli.'), row('SCHEDULER', 'Zeyilname/sonuç zamanlayıcısı', raw.scheduler.status === 200 && raw.scheduler.json?.success ? 'observable' : 'blocked', { status: raw.scheduler.status, browserAvailable: raw.scheduler.json?.tarayiciVar ?? null, resultScanActive: raw.scheduler.json?.sonucTaramasi?.aktif ?? null, resultScanRunning: raw.scheduler.json?.sonucTaramasi?.calisiyor ?? null, directProcurementActive: raw.scheduler.json?.dogrudanTemin?.aktif ?? null, directProcurementRunning: raw.scheduler.json?.dogrudanTemin?.calisiyor ?? null }, 'Yetkili test kaydıyla zeyilname/sonuç olayı ve tekrar engeli doğrulanmalı.'), ]; const result = { checkedAt: new Date().toISOString(), base, readOnly: true, scope: 'Yapılandırma, public kaynak okuması ve kimlik doğrulama kapıları denetlendi; giriş, veri değişikliği, AI üretimi, ödeme, mesaj veya teklif gönderimi yapılmadı.', integrations, guards, summary: { total: integrations.length, passed: integrations.filter(({ status }) => status === 'passed').length, configured: integrations.filter(({ status }) => status === 'configured').length, guarded: integrations.filter(({ status }) => status === 'guarded').length, observable: integrations.filter(({ status }) => status === 'observable').length, manualGate: integrations.filter(({ status }) => status === 'manual-gate').length, blocked: integrations.filter(({ status }) => status === 'blocked').length, protectedGuardsPassed: Object.values(guards).filter(({ passed }) => passed).length, protectedGuardsTotal: Object.keys(guards).length, }, }; await mkdir(dirname(evidencePath), { recursive: true }); await writeFile(evidencePath, `${JSON.stringify(result, null, 2)}\n`); console.log(JSON.stringify({ checkedAt: result.checkedAt, ...result.summary, statuses: Object.fromEntries(integrations.map(({ id, status }) => [id, status])) }));