Files
stack/apps/web/src/spa/chat/runtime-guards.ts
T
2026-08-12 02:50:18 +00:00

151 lines
5.9 KiB
TypeScript

/**
* 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.
*/
import type {
HarnessAuthState,
HarnessCatalog,
HarnessCatalogEntry,
HarnessModelAvailability,
HarnessSelection,
HarnessSummary,
} from '@/lib/types';
export function asString(value: unknown, fallback = ''): string {
return typeof value === 'string' ? value : fallback;
}
/** Like `asString`, but an empty string also falls back — used for guarded
* contract-provided reason strings (e.g. a denial or failure message) where
* an empty string is not a meaningful value to display in place of the
* stable fallback copy. */
export function asNonEmptyString(value: unknown, fallback: string): string {
return typeof value === 'string' && value.length > 0 ? 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 HTTP harness/catalog/selection JSON bodies are as untrusted as the socket
* payloads above — a misbehaving or compromised gateway can send anything. The
* guards below normalize those bodies into the typed client shapes without ever
* rendering a raw body, so a 404/422/malformed response can never inject an
* object into React or a non-tuple into the selection state.
*/
/** Normalizes an untrusted `authState` to the closed set, defaulting to the
* safest value (`unavailable`) for anything unrecognized. */
export function asHarnessAuthState(value: unknown): HarnessAuthState {
return value === 'ready' || value === 'auth_required' || value === 'unavailable'
? value
: 'unavailable';
}
/** Normalizes an untrusted `availability` to the closed set, defaulting to
* `unavailable` so a malformed row can never present as sendable. */
export function asHarnessAvailability(value: unknown): HarnessModelAvailability {
return value === 'available' ? 'available' : 'unavailable';
}
/** A tuple is valid only when all three ids are non-empty strings — a partial
* or malformed selection is rejected (null) rather than half-adopted. */
export function asHarnessSelection(value: unknown): HarnessSelection | null {
if (!isRecord(value)) return null;
const harnessId = value.harnessId;
const providerId = value.providerId;
const modelId = value.modelId;
if (
typeof harnessId !== 'string' ||
typeof providerId !== 'string' ||
typeof modelId !== 'string' ||
harnessId.length === 0 ||
providerId.length === 0 ||
modelId.length === 0
) {
return null;
}
return { harnessId, providerId, modelId };
}
/** Normalizes an untrusted array into typed harness summaries, dropping any row
* without a usable id. */
export function asHarnessSummaries(value: unknown): HarnessSummary[] {
if (!Array.isArray(value)) return [];
const out: HarnessSummary[] = [];
for (const item of value) {
if (!isRecord(item)) continue;
const id = asString(item.id);
if (id.length === 0) continue;
out.push({
id,
displayName: asNonEmptyString(item.displayName, id),
capabilities: asStringArray(item.capabilities),
});
}
return out;
}
function asHarnessCatalogEntry(value: unknown): HarnessCatalogEntry | null {
const selection = asHarnessSelection(value);
if (selection === null || !isRecord(value)) return null;
return {
...selection,
displayName: asNonEmptyString(value.displayName, selection.modelId),
reasoningCapability: value.reasoningCapability === true,
inputTypes: asStringArray(value.inputTypes),
authState: asHarnessAuthState(value.authState),
availability: asHarnessAvailability(value.availability),
};
}
/** Normalizes an untrusted catalog body into the typed client catalog. The
* caller supplies `harnessId` (from the request path) so the returned catalog
* is scoped to the harness that was actually requested, never a body-echoed id.
* Malformed model rows are dropped rather than invalidating the whole catalog. */
export function asHarnessCatalog(value: unknown, harnessId: string): HarnessCatalog {
const record = isRecord(value) ? value : {};
const rawModels = Array.isArray(record.models) ? record.models : [];
const models: HarnessCatalogEntry[] = [];
for (const row of rawModels) {
const entry = asHarnessCatalogEntry(row);
if (entry !== null) models.push(entry);
}
return {
harnessId,
version: asString(record.version),
fingerprint: asString(record.fingerprint),
models,
};
}
/** 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;
}