ci/woodpecker/push/publish Pipeline failed
Co-authored-by: Jason Woltje <[email protected]>
151 lines
5.9 KiB
TypeScript
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;
|
|
}
|