fix(web): harden typed SPA chat lifecycle

Co-Authored-By: Claude Haiku 4.5 <[email protected]>
This commit is contained in:
shaggy (mosaic-dev box)
2026-08-10 00:24:20 -05:00
co-authored by Claude Haiku 4.5
parent b2e005f2b4
commit caebf9ef70
20 changed files with 1778 additions and 125 deletions
+41
View File
@@ -0,0 +1,41 @@
/**
* Socket.IO payloads are only statically typed at the call site — a
* misbehaving or compromised gateway can send anything at runtime. These
* guards protect the dereference sites that would otherwise throw (`.map` on
* a non-array, `.toFixed` on a non-number) or render an object as a React
* child.
*/
export function asString(value: unknown, fallback = ''): string {
return typeof value === 'string' ? value : fallback;
}
export function asFiniteNumber(value: unknown, fallback = 0): number {
return typeof value === 'number' && Number.isFinite(value) ? value : fallback;
}
/** Like `asFiniteNumber`, but returns `null` on failure instead of a numeric
* fallback — callers that must not fabricate a plausible-looking value (e.g.
* `0 tokens` / `$0.0000` for genuinely unknown usage) use this to render an
* honest "unavailable" label instead. */
export function asFiniteNumberOrNull(value: unknown): number | null {
return typeof value === 'number' && Number.isFinite(value) ? value : null;
}
export function asStringArray(value: unknown): string[] {
return Array.isArray(value) && value.every((item) => typeof item === 'string') ? value : [];
}
export function isRecord(value: unknown): value is Record<string, unknown> {
return typeof value === 'object' && value !== null;
}
/** The single point of truth for what counts as a valid conversation ID
* anywhere a scoped server event may adopt one into state — a non-empty
* string, nothing else. Every site that establishes or compares
* `state.conversationId` against a raw socket payload must route through
* this guard so a malformed first frame (null/object/number/empty string)
* can never be adopted verbatim. */
export function asConversationId(value: unknown): string | null {
return typeof value === 'string' && value.length > 0 ? value : null;
}