fix: proxy MCP and expose EKAP verification gates

This commit is contained in:
2026-09-15 17:54:14 +03:00
parent 26b250735c
commit 6c32581952
7 changed files with 190 additions and 6 deletions
+145
View File
@@ -0,0 +1,145 @@
// 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])) }));
+10
View File
@@ -0,0 +1,10 @@
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { readFileSync } from 'node:fs';
test('Nginx proxies MCP JSON-RPC requests to the authenticated Node endpoint', () => {
const config = readFileSync(new URL('../nginx.conf', import.meta.url), 'utf8');
const block = /location\s*=\s*\/mcp\s*\{([\s\S]*?)\n\s*\}/.exec(config)?.[1] || '';
assert.match(block, /proxy_pass\s+http:\/\/127\.0\.0\.1:3001;/);
assert.match(block, /client_max_body_size\s+1m;/);
});
+7
View File
@@ -22,3 +22,10 @@ test('ordinary upstream failures stay retryable', () => {
retryable: true,
});
});
test('KIK HTTP 428 is also exposed as the same manual verification gate', () => {
const result = sourceFailure(new Error('KİK sunucusu isteği reddetti (HTTP 428).'));
assert.equal(result.status, 428);
assert.equal(result.body.code, 'HUMAN_VERIFICATION_REQUIRED');
assert.equal(result.body.manualAction, true);
});