/** * T2 — the frozen CLIENT-contract surface. Re-exports the `relay-contracts` §4.2/§4.3 shapes * VERBATIM (never redefines them) and adds ONLY response envelopes needed to parse HTTP JSON. * * INV3: there is NO `account_id`/`tenant_id` REQUEST field anywhere here — identity is always the * cookie/passkey session the browser already holds; the server derives the account. * * Wire note: over JSON, `HostRecord.agentPubkey` arrives base64url-encoded (a Uint8Array cannot * ride JSON). The wire schema decodes it back to `Uint8Array` so callers receive a genuine * `HostRecord` — validate-at-boundary (coding-style.md "Input Validation"). */ import { z } from 'zod' import { HostStatusSchema, decodeBase64UrlBytes, type HostRecord, type HostStatus, type CapabilityRight, } from 'relay-contracts' // Re-export the frozen types verbatim so P6 modules import them from one place (the src/types.ts // discipline: a new shared field is changed in relay-contracts, never redeclared locally). export type { HostRecord, HostStatus, CapabilityRight } export { HostStatusSchema } /** Wire form of a HostRecord: `agentPubkey` is base64url text, decoded to bytes on parse. */ export const HostRecordWireSchema = z .object({ hostId: z.string().uuid(), accountId: z.string().uuid(), subdomain: z.string().min(1), agentPubkey: z.string().min(1), enrollFpr: z.string().min(1), status: HostStatusSchema, lastSeen: z.string().min(1), createdAt: z.string().min(1), revokedAt: z.string().min(1).nullable(), }) .strict() .transform( (w): HostRecord => ({ hostId: w.hostId, accountId: w.accountId, subdomain: w.subdomain, // Copy into a fresh ArrayBuffer-backed Uint8Array so the type matches HostRecord exactly. agentPubkey: new Uint8Array(decodeBase64UrlBytes(w.agentPubkey)), enrollFpr: w.enrollFpr, status: w.status, lastSeen: w.lastSeen, createdAt: w.createdAt, revokedAt: w.revokedAt, }), ) /** `GET /api/hosts` → array of HostRecord. */ export const HostListWireSchema = z.array(HostRecordWireSchema).readonly() /** `GET /api/hosts/:id/status` → the host's current status. */ export const HostStatusResponseSchema = z.object({ status: HostStatusSchema }).strict() /** `POST /api/pairing-codes` → single-use short-TTL code + its expiry (§4.5 ISSUE). */ export const PairingCodeResponseSchema = z .object({ code: z.string().min(1), expiresAt: z.string().min(1), }) .strict() .readonly() export type PairingCodeResponse = z.infer /** `POST /api/hosts/:id/capability-token` → opaque raw §4.3 token (v0.9+; never decoded to trust). */ export const CapabilityTokenResponseSchema = z.object({ token: z.string().min(1) }).strict()