/** * src/http/statusline.ts — parse a Claude Code statusLine JSON body (B2). * * Pure function, never throws. Maps the statusLine stdin JSON (R0-confirmed * field names, see StatusTelemetry comment in types.ts) into a StatusTelemetry. * * Security: SEC-M7 — unknown input narrowed field-by-field; string lengths capped. * * Field mapping (JSON → StatusTelemetry): * context_window.used_percentage → contextUsedPct * cost.total_cost_usd → costUsd * cost.total_lines_added → linesAdded * cost.total_lines_removed → linesRemoved * model.display_name → model * effort.level → effort * pr → pr (same shape, required: number+url) * rate_limits → rate (fiveHourPct, sevenDayPct) */ import type { StatusTelemetry } from '../types.js'; // String length limits (SEC-M7 — prevent DoS through overly long strings) const MODEL_MAX_LEN = 200; const EFFORT_MAX_LEN = 100; const URL_MAX_LEN = 2000; const REVIEW_STATE_MAX_LEN = 100; /** * Parse the body of a POST /hook/status request into a StatusTelemetry. * * @param body - raw untrusted JSON body (unknown type) * @returns StatusTelemetry with at always set to Date.now(), or null when * body is not a plain object. Missing/dirty fields → undefined. * Never throws. */ export function parseStatusLine(body: unknown): StatusTelemetry | null { // Non-objects (including null and arrays) → null if (body === null || typeof body !== 'object' || Array.isArray(body)) return null; const b = body as Record; const contextUsedPct = safeFiniteNumber(nestedValue(b, 'context_window', 'used_percentage')); const costObj = asObjectOrNull(b['cost']); const costUsd = safeFiniteNumber(costObj?.['total_cost_usd']); const linesAdded = safeFiniteNumber(costObj?.['total_lines_added']); const linesRemoved = safeFiniteNumber(costObj?.['total_lines_removed']); const modelObj = asObjectOrNull(b['model']); const model = safeBoundedString(modelObj?.['display_name'], MODEL_MAX_LEN); const effortObj = asObjectOrNull(b['effort']); const effort = safeBoundedString(effortObj?.['level'], EFFORT_MAX_LEN); const pr = parsePr(b['pr']); const rate = parseRate(b['rate_limits']); return { at: Date.now(), ...(contextUsedPct !== undefined && { contextUsedPct }), ...(costUsd !== undefined && { costUsd }), ...(linesAdded !== undefined && { linesAdded }), ...(linesRemoved !== undefined && { linesRemoved }), ...(model !== undefined && { model }), ...(effort !== undefined && { effort }), ...(pr !== undefined && { pr }), ...(rate !== undefined && { rate }), }; } // ─── Private helpers ───────────────────────────────────────────────────────── /** Get obj[key1][key2], or undefined if either level is not a plain object. */ function nestedValue(obj: Record, key1: string, key2: string): unknown { const inner = asObjectOrNull(obj[key1]); return inner !== null ? inner[key2] : undefined; } /** Return v if it is a finite number; otherwise undefined. */ function safeFiniteNumber(v: unknown): number | undefined { return typeof v === 'number' && Number.isFinite(v) ? v : undefined; } /** Return v if it is a string within maxLen; otherwise undefined. */ function safeBoundedString(v: unknown, maxLen: number): string | undefined { if (typeof v !== 'string') return undefined; return v.length <= maxLen ? v : undefined; } /** Cast v to a Record if it is a non-null, non-array object; otherwise null. */ function asObjectOrNull(v: unknown): Record | null { if (v === null || typeof v !== 'object' || Array.isArray(v)) return null; return v as Record; } /** Parse the pr field. Returns undefined unless both number (finite) and url (bounded) are valid. */ function parsePr( v: unknown, ): { number: number; url: string; reviewState?: string } | undefined { const obj = asObjectOrNull(v); if (obj === null) return undefined; const num = safeFiniteNumber(obj['number']); if (num === undefined) return undefined; const url = safeBoundedString(obj['url'], URL_MAX_LEN); if (url === undefined) return undefined; const reviewState = safeBoundedString(obj['reviewState'], REVIEW_STATE_MAX_LEN); return { number: num, url, ...(reviewState !== undefined && { reviewState }), }; } /** Parse rate_limits → rate. Returns undefined if rate_limits is not an object. * Returns a (possibly empty) object with undefined sub-fields when the object * exists but sub-field values fail Number.isFinite validation. */ function parseRate(v: unknown): { fiveHourPct?: number; sevenDayPct?: number } | undefined { const obj = asObjectOrNull(v); if (obj === null) return undefined; const fiveHourPct = safeFiniteNumber(obj['fiveHourPct']); const sevenDayPct = safeFiniteNumber(obj['sevenDayPct']); return { ...(fiveHourPct !== undefined && { fiveHourPct }), ...(sevenDayPct !== undefined && { sevenDayPct }), }; }