/** * Zod schemas describing the rpki_query_service API contract. * * Schemas are intentionally `.passthrough()` so additive backend changes do * not break the UI; fields the UI depends on are typed strictly. */ import { z } from "zod"; /* ------------------------------------------------------------------ */ /* Records */ /* ------------------------------------------------------------------ */ export const runCountsSchema = z .object({ publicationPoints: z.number().optional(), objects: z.number().optional(), freshObjects: z.number().optional(), cachedObjects: z.number().optional(), rejectedObjects: z.number().optional(), freshRejectedObjects: z.number().optional(), cachedRejectedObjects: z.number().optional(), trustAnchors: z.number().optional(), vrps: z.number().optional(), aspas: z.number().optional(), warnings: z.number().optional(), }) .passthrough(); export const runRecordSchema = z .object({ schemaVersion: z.number().optional(), runId: z.string(), runSeq: z.number().nullish(), runDir: z.string().optional(), validationTime: z.string().nullish(), syncMode: z.string().nullish(), startedAt: z.string().nullish(), finishedAt: z.string().nullish(), wallMs: z.number().nullish(), maxRssKb: z.number().nullish(), artifactPaths: z.record(z.string()).optional(), counts: runCountsSchema.optional(), indexStatus: z.string().optional(), indexError: z.string().nullish(), }) .passthrough(); export const repositoryRecordSchema = z .object({ repoId: z.string(), uri: z.string(), host: z.string(), transport: z.string().nullish(), publicationPoints: z.number().nullish(), objects: z.number().nullish(), rejectedObjects: z.number().nullish(), downloadBytes: z.number().nullish(), syncDurationMsTotal: z.number().nullish(), phases: z.record(z.number()).optional(), terminalStates: z.record(z.number()).optional(), }) .passthrough(); export const publicationPointRecordSchema = z .object({ ppId: z.string(), repoId: z.string(), nodeId: z.number().nullish(), parentNodeId: z.number().nullish(), rsyncBaseUri: z.string().nullish(), manifestRsyncUri: z.string().nullish(), publicationPointRsyncUri: z.string().nullish(), rrdpNotificationUri: z.string().nullish(), source: z.string().nullish(), repoSyncSource: z.string().nullish(), repoSyncPhase: z.string().nullish(), repoSyncDurationMs: z.number().nullish(), repoSyncError: z.string().nullish(), repoTerminalState: z.string().nullish(), thisUpdate: z.string().nullish(), nextUpdate: z.string().nullish(), verifiedAt: z.string().nullish(), objects: z.number().nullish(), rejectedObjects: z.number().nullish(), warnings: z.number().nullish(), }) .passthrough(); export const objectInstanceRecordSchema = z .object({ objectInstanceId: z.string(), uri: z.string(), uriHash: z.string().optional(), sha256: z.string().optional(), objectType: z.string(), result: z.string().nullish(), detailSummary: z.string().nullish(), repoId: z.string(), ppId: z.string(), sourceSection: z.string().nullish(), rejected: z.boolean().optional(), rejectReason: z.string().nullish(), sizeBytes: z.number().nullish(), rawAvailable: z.boolean().optional(), }) .passthrough(); export const chainEdgeRecordSchema = z .object({ relation: z.string(), fromUri: z.string().nullish(), toUri: z.string().nullish(), toObjectInstanceId: z.string().nullish(), toSha256: z.string().nullish(), status: z.string().nullish(), evidence: z.unknown().optional(), }) .passthrough(); /** Sparse record returned by /objects/by-uri (index hit, no lazy scan). */ export const objectUriIndexRecordSchema = z .object({ uri: z.string(), sha256: z.string(), objectInstanceId: z.string(), repoId: z.string(), ppId: z.string(), }) .passthrough(); export const exportJobRecordSchema = z .object({ jobId: z.string(), runId: z.string(), scope: z.string(), repoId: z.string().nullish(), ppId: z.string().nullish(), status: z.string(), createdAt: z.string().nullish(), finishedAt: z.string().nullish(), outputPath: z.string().nullish(), objectCount: z.number().nullish(), bytesWritten: z.number().nullish(), error: z.string().nullish(), }) .passthrough(); export const projectionRecordSchema = z .object({ schemaVersion: z.number().optional(), sha256: z.string().optional(), objectType: z.string().optional(), parseStatus: z.string().optional(), errorSummary: z.string().nullish(), projection: z.record(z.unknown()).nullish(), }) .passthrough(); export const validationIssueSchema = z .object({ severity: z.string().nullish(), reasonCode: z.string().nullish(), reasonText: z.string().nullish(), rfcRefs: z.array(z.string()).optional(), evidence: z.unknown().optional(), }) .passthrough(); export const validationStageSchema = z .object({ status: z.string().nullish(), stage: z.string().nullish(), mode: z.string().nullish(), projectionStatus: z.string().nullish(), issues: z.array(validationIssueSchema).optional(), detailSummary: z.string().nullish(), edgesCount: z.number().nullish(), note: z.string().nullish(), }) .passthrough(); export const validationSummarySchema = z .object({ finalStatus: z.string().nullish(), auditResult: z.string().nullish(), detailSummary: z.string().nullish(), parsevalidate: validationStageSchema.nullish(), chainvalidate: validationStageSchema.nullish(), }) .passthrough(); export const explainRecordSchema = z .object({ finalStatus: z.string().nullish(), auditResult: z.string().nullish(), authoritative: z.boolean().optional(), explainMode: z.string().nullish(), parsevalidate: validationStageSchema.nullish(), chainvalidate: validationStageSchema.nullish(), chainEdges: z.array(chainEdgeRecordSchema).optional(), }) .passthrough(); export const searchResultSchema = z .object({ objects: z.array(objectInstanceRecordSchema), repos: z.array(repositoryRecordSchema), publicationPoints: z.array(publicationPointRecordSchema), }) .passthrough(); export const vrpRecordSchema = z .object({ runId: z.string(), asn: z.number(), prefix: z.string(), maxLength: z.number(), }) .passthrough(); export const healthSchema = z .object({ status: z.string(), service: z.string().optional(), version: z.number().optional(), latestReadyRun: z.string().nullish(), }) .passthrough(); export const serviceInfoSchema = z .object({ service: z.string(), version: z.number(), }) .passthrough(); export const manifestFileEntrySchema = z .object({ fileName: z.string(), hashHex: z.string(), }) .passthrough(); export const revokedCertEntrySchema = z .object({ serialNumberHex: z.string().nullish(), // Backend serializes CRL serials as a JSON number when they fit, else a string. serialNumber: z.union([z.string(), z.number()]).nullish(), revocationDate: z.string().nullish(), }) .passthrough(); export const runSummarySchema = z .object({ publicationPoints: z.number().optional(), objects: z.number().optional(), repos: z.number().optional(), vrps: z.number().optional(), aspas: z.number().optional(), }) .passthrough(); export const statsMapSchema = z.record(z.number()); /* ------------------------------------------------------------------ */ /* Envelope */ /* ------------------------------------------------------------------ */ const pageSchema = z.object({ nextCursor: z.string().nullable(), limit: z.number(), }); const metaSchema = z .object({ runId: z.string().nullish(), schemaVersion: z.number().optional(), }) .passthrough(); export function envelopeSchema(dataSchema: T) { return z .object({ data: dataSchema, page: pageSchema.nullish(), meta: metaSchema.optional(), }) .passthrough(); } /* ------------------------------------------------------------------ */ /* Inferred types */ /* ------------------------------------------------------------------ */ export type RunCounts = z.infer; export type RunRecord = z.infer; export type RepositoryRecord = z.infer; export type PublicationPointRecord = z.infer; export type ObjectInstanceRecord = z.infer; export type ChainEdgeRecord = z.infer; export type ExportJobRecord = z.infer; export type ProjectionRecord = z.infer; export type ValidationIssue = z.infer; export type ValidationStage = z.infer; export type ValidationSummary = z.infer; export type ExplainRecord = z.infer; export type SearchResult = z.infer; export type VrpRecord = z.infer; export type HealthStatus = z.infer; export type ServiceInfo = z.infer; export type ManifestFileEntry = z.infer; export type RevokedCertEntry = z.infer; export type RunSummary = z.infer; export interface ListResult { items: T[]; nextCursor: string | null; runId: string | undefined; }