146 lines
10 KiB
JavaScript
146 lines
10 KiB
JavaScript
// 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])) }));
|