import { readFileSync, statSync } from 'node:fs'; import { E2E } from '../../playwright.config'; /** * Reading one-time codes back out of the API's own output. * * There is no local SMS gateway, and the code is not recoverable anywhere else: * `iam.user_verifications.verification_code` holds an argon2 hash, and the * notification rows it is delivered through store an empty body. The log line * the message composer emits is the only plaintext. * * Codes are matched by *position in the log*, not by recipient — the line names * no user. Callers take an offset before the action that triggers the send and * only read what was written after it, which is sound because the suite runs * single-worker and serially. */ const OTP_PATTERN = /is (\d{4,8})\./g; /** Byte offset to read from later. Zero when the log does not exist yet. */ export function logOffset(): number { try { return statSync(E2E.apiLog).size; } catch { return 0; } } /** Waits for a code to appear after `offset`, and returns the last one seen. */ export async function waitForOtp( offset: number, timeoutMs = 20_000, ): Promise { const deadline = Date.now() + timeoutMs; while (Date.now() < deadline) { const found = otpSince(offset); if (found) return found; await new Promise((resolve) => setTimeout(resolve, 250)); } throw new Error( `No one-time code appeared in ${E2E.apiLog} within ${timeoutMs}ms. ` + `Is the API teeing its output there?`, ); } function otpSince(offset: number): string | null { let text: string; try { text = readFileSync(E2E.apiLog, 'utf8').slice(offset); } catch { return null; } const codes = [...text.matchAll(OTP_PATTERN)].map((m) => m[1]); return codes.length > 0 ? codes[codes.length - 1] : null; }