UNPKG

@inso_web/els-mcp

Version:

MCP-сервер поверх INSO Error Logs Service. Read-only tools (search, analytics, fingerprinting, correlations) для подключения Claude Desktop/Code и ChatGPT к логам ошибок. Streamable HTTP transport + stdio для npx-запуска.

131 lines 4.73 kB
/** * Audit chain verification. * * Проходит по строкам `mcp_audit.audit_log` в порядке `createdAt ASC` для * заданного `appId` (и опционального диапазона дат) и проверяет, что * `rowHash = sha256(prevRowHash + content)` совпадает со stored значением. * * Используется: * - в CLI команде `els-mcp verify-audit --app=X --from=Y --to=Z` * (см. `src/cli.ts`) * - в smoke / health-check скриптах * * Поведение при отсутствии Prisma client — корректно возвращает пустой * результат с `prismaAvailable=false`. */ import { getPrisma } from './prisma.js'; import { rowContent, sha256Hex } from './service.js'; function toIso(d) { if (d === undefined) return undefined; return typeof d === 'string' ? d : d.toISOString(); } function pickIso(v) { if (v instanceof Date) return v.toISOString(); if (typeof v === 'string') return v; return String(v); } function pickRowId(v) { return v === undefined || v === null ? '<unknown>' : String(v); } /** * Verifies the audit hash-chain integrity for `appId`. * Не модифицирует БД, читает в batch'ах для крупных таблиц. */ export async function verifyChain(opts) { const batchSize = Math.max(1, opts.batchSize ?? 1000); const fromIso = toIso(opts.from); const toIsoStr = toIso(opts.to); const client = opts.prismaOverride !== undefined ? opts.prismaOverride : await getPrisma({ ...(opts.databaseUrl ? { databaseUrl: opts.databaseUrl } : {}), ...(opts.log ? { log: opts.log } : {}), }); if (!client) { return { totalChecked: 0, prismaAvailable: false, breakAt: null }; } const where = { appId: opts.appId }; if (fromIso || toIsoStr) { const createdAt = {}; if (fromIso) createdAt.gte = fromIso; if (toIsoStr) createdAt.lt = toIsoStr; where.createdAt = createdAt; } let prevHash = null; let totalChecked = 0; let skip = 0; for (;;) { const rows = (await client.mcpAuditLog.findMany({ where, orderBy: { createdAt: 'asc' }, take: batchSize, skip, })); if (rows.length === 0) break; for (const row of rows) { const args = (typeof row.args === 'object' && row.args !== null ? row.args : {}); const createdAtIso = pickIso(row.createdAt); const content = rowContent({ appId: row.appId, keyId: row.keyId, tool: row.tool, args, resultBytes: row.resultBytes, latencyMs: row.latencyMs, cacheHit: row.cacheHit === true, statusCode: row.statusCode, error: row.error ?? null, createdAt: createdAtIso, prevHash, }); const expectedHash = sha256Hex((prevHash ?? '') + content); if (expectedHash !== row.rowHash) { return { totalChecked: totalChecked + 1, prismaAvailable: true, breakAt: { id: pickRowId(row.id), createdAt: createdAtIso, expectedHash, actualHash: row.rowHash, }, }; } prevHash = row.rowHash; totalChecked += 1; } if (rows.length < batchSize) break; skip += batchSize; } return { totalChecked, prismaAvailable: true, breakAt: null }; } /** * Pretty-printer для CLI: возвращает многострочный отчёт. */ export function formatVerifyResult(result) { if (!result.prismaAvailable) { return [ 'Audit verification skipped: Prisma client not available.', 'Ensure MCP_DATABASE_URL is set and `npm run prisma:generate` has been run.', ].join('\n'); } if (result.breakAt) { return [ `Audit chain BROKEN after ${result.totalChecked} rows.`, ` break at id=${result.breakAt.id}, createdAt=${result.breakAt.createdAt}`, ` expected rowHash=${result.breakAt.expectedHash}`, ` stored rowHash=${result.breakAt.actualHash}`, ].join('\n'); } return `Audit chain OK: ${result.totalChecked} rows verified.`; } //# sourceMappingURL=verify.js.map