import type { MatrixClient } from 'matrix-js-sdk'; import pkg from '../../../package.json'; // Lotus E2EE investigation kit — capture-only console diagnostics. // // Installs pass-through wrappers around `console.warn` / `console.error` that // ring-buffer any log line matching the KE-1..KE-4 bug-cluster signatures // (E2EE KE-1..4 capture; see cinny #201). It NEVER swallows a log call — the // original console method is always invoked — and it performs NO network I/O. // The report metadata is limited to SDK version / device id / user id / sync // state; the captured log lines themselves are intentional evidence and may // contain event ids or matrix ids exactly as the SDK logged them. export type CryptoDiagLevel = 'warn' | 'error'; export type CryptoDiagEntry = { /** ISO-8601 UTC timestamp of when the line was captured. */ ts: string; level: CryptoDiagLevel; /** Which KE bucket the signature belongs to, e.g. `KE-1`. */ ke: string; /** Human-readable label of the matched signature. */ signature: string; /** The serialized console line (best-effort). */ message: string; }; type Signature = { ke: string; label: string; re: RegExp; }; // Ordered most-specific-first so the recorded label is the tightest match. const SIGNATURES: Signature[] = [ { ke: 'KE-1', label: 'already exists', re: /already exists/i }, { ke: 'KE-2', label: 'missing key at index', re: /missing key at index/i }, { ke: 'KE-2', label: 'io.element.call.encryption_keys', re: /io\.element\.call\.encryption_keys/, }, { ke: 'KE-2', label: 'MissingKey', re: /MissingKey/ }, { ke: 'KE-3', label: 'DecryptionError', re: /DecryptionError/ }, { ke: 'KE-4', label: 'update_delayed_event', re: /update_delayed_event/ }, { ke: 'KE-4', label: 'delayed event', re: /delayed event/i }, ]; const MAX_ENTRIES = 200; const entries: CryptoDiagEntry[] = []; let installed = false; let originalWarn: ((...data: unknown[]) => void) | undefined; let originalError: ((...data: unknown[]) => void) | undefined; const stringifyArg = (arg: unknown): string => { if (typeof arg === 'string') return arg; if (arg instanceof Error) return `${arg.name}: ${arg.message}`; try { return JSON.stringify(arg); } catch { return String(arg); } }; const capture = (level: CryptoDiagLevel, args: unknown[]): void => { const message = args.map(stringifyArg).join(' '); const sig = SIGNATURES.find((s) => s.re.test(message)); if (!sig) return; entries.push({ ts: new Date().toISOString(), level, ke: sig.ke, signature: sig.label, message, }); // Ring-buffer: keep only the most recent MAX_ENTRIES. while (entries.length > MAX_ENTRIES) { entries.shift(); } }; /** * Install the capture-only console wrappers. Idempotent — calling it more than * once is a no-op. Safe to call as early as possible during app boot. */ export const installCryptoDiagLog = (): void => { if (installed) return; installed = true; originalWarn = console.warn.bind(console); originalError = console.error.bind(console); console.warn = (...args: unknown[]): void => { capture('warn', args); originalWarn?.(...args); }; console.error = (...args: unknown[]): void => { capture('error', args); originalError?.(...args); }; }; /** A snapshot copy of the current capture buffer (most-recent-last). */ export const getCryptoDiagEntries = (): CryptoDiagEntry[] => entries.slice(); const readSdkVersion = (mx?: MatrixClient): string => { // Prefer the value the running client reports; fall back to the declared pin. const declared = (pkg.dependencies as Record | undefined)?.['matrix-js-sdk']; const clientVersion = (mx as unknown as { getSdkVersion?: () => string } | undefined) ?.getSdkVersion; if (typeof clientVersion === 'function') { try { return clientVersion.call(mx) || declared || 'unknown'; } catch { // fall through to the declared pin } } return declared ?? 'unknown'; }; /** * Build a self-contained JSON diagnostic report string. Contains only the SDK * version, device id, user id, sync state, crypto readiness, and the captured * KE signature buffer — no message content, tokens, or other PII. */ export const buildCryptoDiagReport = (mx?: MatrixClient): string => { const buffer = getCryptoDiagEntries(); const countsByKe: Record = {}; buffer.forEach((entry) => { countsByKe[entry.ke] = (countsByKe[entry.ke] ?? 0) + 1; }); const report = { kind: 'lotus-crypto-diag', generatedAt: new Date().toISOString(), sdkVersion: readSdkVersion(mx), deviceId: mx?.getDeviceId() ?? null, userId: mx?.getUserId() ?? null, syncState: mx?.getSyncState() ?? null, cryptoReady: Boolean(mx?.getCrypto()), entryCount: buffer.length, maxEntries: MAX_ENTRIES, countsByKe, entries: buffer, }; return JSON.stringify(report, null, 2); };