summaryrefslogtreecommitdiffhomepage
path: root/src/features/named-agents/logic/normalize.ts
blob: c1acec2a71fe5aa78ca393dfefe11833195bc743 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
/**
 * Pure network-seam normalizers for the Named Agents API — zero DOM, zero
 * effects. The composition root coerces the UNTYPED JSON it receives from
 * `GET/POST/PUT/DELETE /named-agents` through these so a malformed / partial
 * response can never crash the renderer. Mirrors the concurrency / heartbeat
 * network-seam guard pattern.
 */

import type { NamedAgent, NamedAgentInput } from "./types";

function isRecord(value: unknown): value is Record<string, unknown> {
  return value !== null && typeof value === "object";
}

function asString(value: unknown): string | null {
  return typeof value === "string" && value.length > 0 ? value : null;
}

/**
 * Coerce one untrusted record into a typed `NamedAgent`, or `null` when it is
 * malformed (missing `id`, `name`, or `model` — or a non-string `model`).
 */
export function normalizeNamedAgent(value: unknown): NamedAgent | null {
  if (!isRecord(value)) return null;
  const id = asString(value.id);
  const name = asString(value.name);
  const model = asString(value.model);
  if (id === null || name === null || model === null) return null;
  return { id, name, model };
}

/**
 * Coerce an untrusted `GET /named-agents` body into a typed Named Agent list.
 * When the list key is absent/malformed, returns `[]` (defensive — the renderer
 * shows the empty state rather than crashing).
 */
export function normalizeNamedAgents(data: unknown): readonly NamedAgent[] {
  if (!isRecord(data) || !Array.isArray(data.namedAgents)) return [];
  const out: NamedAgent[] = [];
  for (const entry of data.namedAgents as readonly unknown[]) {
    const agent = normalizeNamedAgent(entry);
    if (agent !== null) out.push(agent);
  }
  return out;
}

/**
 * Coerce an untrusted `POST`/`PUT /named-agents` body (the canonical saved
 * record) into a typed `NamedAgent`, or `null` when malformed. The composition
 * root surfaces an error when this returns null on an ok response.
 */
export function normalizeNamedAgentRecord(data: unknown): NamedAgent | null {
  if (!isRecord(data)) return null;
  return normalizeNamedAgent(data.namedAgent);
}

/**
 * Coerce an untrusted `NamedAgentInput` at the create/update boundary so only
 * clean strings reach the wire. Non-string fields drop to "" (name nonempty is
 * validated separately; model nonempty too). The caller still runs name/model
 * validation so a malformed body never reaches the backend.
 */
export function toNamedAgentInput(name: unknown, model: unknown): NamedAgentInput {
  return {
    name: typeof name === "string" ? name.trim() : "",
    model: typeof model === "string" ? model : "",
  };
}