// Read-only production drift probe. No authentication, writes, payments or // provider calls are performed. It intentionally fails when a required // deployment contract is missing. import { mkdir, writeFile } from 'node:fs/promises'; import crypto from 'node:crypto'; const base = String(process.argv[2] || process.env.IHALEIST_LIVE_URL || 'https://ihaleist.com').replace(/\/$/, ''); const evidencePath = process.env.IHALEIST_LIVE_EVIDENCE_PATH || 'docs/quality/evidence/qa-live-runtime.json'; const timeoutMs = 15_000; const requiredHeaders = [ 'x-content-type-options', 'referrer-policy', 'permissions-policy', 'x-frame-options', 'content-security-policy', ...(base.startsWith('https://') ? ['strict-transport-security'] : []), ]; const observedHeaders = [...new Set([...requiredHeaders, 'strict-transport-security'])]; const checks = {}; const expectedRelease = String(process.env.IHALEIST_EXPECTED_RELEASE || '').trim(); const expectedAssetSha256 = String(process.env.IHALEIST_EXPECTED_ASSET_SHA256 || '').trim(); async function probe(path, successField = null, captureText = false) { const controller = new AbortController(); const timer = setTimeout(() => controller.abort(), timeoutMs); try { const response = await fetch(base + path, { signal: controller.signal, redirect: 'manual' }); const result = { status: response.status, contentType: response.headers.get('content-type'), headers: Object.fromEntries(observedHeaders.map(name => [name, response.headers.get(name)])), }; if (successField || captureText) { const raw = await response.text(); if (captureText) result.text = raw; if (successField) { try { result.json = JSON.parse(raw); } catch { result.jsonError = 'JSON yanıtı ayrıştırılamadı.'; } result.apiSuccess = result.json?.[successField] === true; } } return result; } catch (error) { return { error: error.name === 'AbortError' ? `timeout after ${timeoutMs}ms` : error.message }; } finally { clearTimeout(timer); } } async function probeFrontendAsset(publicPath) { if (!publicPath) return { error: 'Ana sayfada Vite JavaScript asset yolu bulunamadı.' }; const controller = new AbortController(); const timer = setTimeout(() => controller.abort(), timeoutMs); try { const response = await fetch(new URL(publicPath, base), { signal: controller.signal, redirect: 'manual' }); const bytes = Buffer.from(await response.arrayBuffer()); return { status: response.status, contentType: response.headers.get('content-type'), headers: Object.fromEntries(observedHeaders.map(name => [name, response.headers.get(name)])), path: new URL(publicPath, base).pathname, bytes: bytes.length, sha256: crypto.createHash('sha256').update(bytes).digest('hex'), }; } catch (error) { return { error: error.name === 'AbortError' ? `timeout after ${timeoutMs}ms` : error.message }; } finally { clearTimeout(timer); } } checks.home = await probe('/', null, true); const assetMatch = checks.home.text?.match(/]+src=["']([^"']*\/assets\/[^"']+\.js(?:\?[^"']*)?)["']/i); delete checks.home.text; checks.health = await probe('/api/health', 'ok'); // /api/health and /api/ready use the same { ok: true } probe contract. The // readiness payload also includes a separate `ready` diagnostic field. checks.ready = await probe('/api/ready', 'ok'); checks.version = await probe('/api/version', 'ok'); // The EKAP document fallback depends on the bundled browser being available; // check its public capability endpoint without opening a document or mutating // any account data. checks.documents = await probe('/api/documents/config', 'tarayiciVar'); checks.frontendAsset = await probeFrontendAsset(assetMatch?.[1]); const release = checks.version?.json?.release; const releaseValid = typeof release === 'string' && /^[a-f0-9]{7,64}$/i.test(release); const releaseMatches = !expectedRelease || release === expectedRelease; const reportedAsset = checks.version?.json?.frontendAsset; const assetMatchesRuntime = Boolean(reportedAsset?.sha256) && reportedAsset.sha256 === checks.frontendAsset?.sha256 && reportedAsset.path === checks.frontendAsset?.path; const assetMatchesExpected = !expectedAssetSha256 || checks.frontendAsset?.sha256 === expectedAssetSha256; const publicChecks = [checks.home, checks.health, checks.ready, checks.version, checks.documents, checks.frontendAsset]; const missingHeaders = requiredHeaders.filter(name => publicChecks.some(check => check?.headers?.[name] == null)); const report = { base, readOnly: true, checkedAt: new Date().toISOString(), expectedRelease: expectedRelease || null, expectedAssetSha256: expectedAssetSha256 || null, checks, releaseValid, releaseMatches, assetMatchesRuntime, assetMatchesExpected, pass: checks.home?.status === 200 && checks.health?.status === 200 && checks.health.apiSuccess === true && checks.ready?.status === 200 && checks.ready.apiSuccess === true && checks.version?.status === 200 && checks.version.apiSuccess === true && checks.documents?.status === 200 && checks.documents.apiSuccess === true && checks.frontendAsset?.status === 200 && releaseValid && releaseMatches && assetMatchesRuntime && assetMatchesExpected && missingHeaders.length === 0, missingHeaders, }; await mkdir('docs/quality/evidence', { recursive: true }); await writeFile(evidencePath, JSON.stringify(report, null, 2) + '\n'); console.log(JSON.stringify(report)); if (!report.pass) process.exitCode = 1;