/* RFC-0354: Zod schemas for the Sternsystem bundle contract — the durable site unit, fleet registry, and version pin. These schemas are the machine-checkable contract for all Sternsystem operations. RFC-0561: fleetRegistryEntrySchema gains optional owner field (did:web VC subject id). RFC-0574: replace repo/mirror with parameterized mirrors[] array. RFC-0752: add cloudflareZoneId to fleetRegistryEntrySchema, services[] with subdomains to fleetRegistrySchema. RFC-0751: extend serviceEntrySchema with deployment fields (kind, url, publicEndpoints, routes, upstreams, lastDeployed, healthCheckPath). RFC-0790: add systemConfigSchema, systemStateSchema, servicesRegistrySchema for convention-based discovery (replaces fleetRegistrySchema). Do not perform file IO or git operations — pure shape only. Do not define mission or release schemas — those live in subsequent RFCs. ADR-0091: add externalEditBaselineSha to systemStateSchema — operator-set baseline that grandfathers pre-bordbuch commits in the external-edit guard. RFC-1210: closed-loop signal ingest to bounded missions (RFC-1210) RFC-1263 RFC-1284: systemStateSchema gains nullable accessPinSuspended {at,by,operationId} — durable crash-safe suspension marker; accessPin stays the desired-state authority RFC-1307: step 1 — shared PBP layout map, canonical writer, typed onboarding state RFC-0354, RFC-0479, RFC-0561, RFC-0574, RFC-0751, RFC-0752, RFC-0790, RFC-0806, RFC-0902, RFC-0964, RFC-0966, RFC-0967, RFC-1065, RFC-1097 */ import { z } from "zod"; import { starNameSchema } from "@warpgogol/werkstatt-shared/ontology/cosmic"; import { deploymentStaticConfigSchema, lastPropagatedChannelSchema } from "./leitstand.ts"; import { remediationAutonomyConfigSchema } from "../../remediation/remediation-classes.ts"; const semverRe = /^\d+\.\d+\.\d+$/; const sha256Re = /^sha256:[0-9a-f]{64}$/; const kebabRe = /^[a-z0-9]+(-[a-z0-9]+)*$/; const didWebRe = /^did:web:[a-z0-9.-]+#.+$/; export const systemPinSchema = z.object({ schemaVersion: z.string().min(1), systemId: z .string() .regex(kebabRe, "systemId must be kebab-case, lowercase, latin-only, no TLD suffix"), cosmicStar: starNameSchema, pinnedAt: z.string().datetime(), platform: z.object({ version: z.string().regex(semverRe, "platform.version must be x.y.z"), commit: z.string().min(7), rfcHead: z.string().regex(/^RFC-\d{4}$/, "rfcHead must be RFC-NNNN"), platformSemanticHash: z .string() .regex(sha256Re, "platformSemanticHash must be sha256: prefixed hex"), }), migratorCursor: z.array(z.string()), capabilities: z.array( z.object({ semanticId: z.string().min(1), version: z.string().min(1), intent: z.array(z.string()), }), ), }); export const mirrorStorageTypeSchema = z.enum(["non-bare", "bare", "bundle"]); export type MirrorStorageType = z.infer; export const mirrorEntrySchema = z.object({ path: z.string().min(1, "mirror path must be non-empty"), storageType: mirrorStorageTypeSchema, }); export type MirrorEntry = z.infer; export const serviceSubdomainSchema = z.object({ domain: z.string().min(1, "subdomain domain must be non-empty"), zone: z.string().min(1, "subdomain zone must be non-empty"), }); export const serviceEntrySchema = z.object({ id: z .string() .regex(kebabRe, "service id must be kebab-case, lowercase, latin-only, no TLD suffix"), kind: z.enum(["proxy-worker", "scheduled-worker", "cloudflare-worker"]), workerName: z.string().min(1, "workerName must be non-empty"), hostedBy: z.enum(["studio"]), url: z.string().min(1, "url must be non-empty"), workersDevUrl: z.string().min(1, "workersDevUrl must be non-empty").optional(), publicEndpoints: z.boolean().default(false), routes: z.array(z.string()).optional(), upstreams: z.array(z.string()).optional(), subdomains: z.array(serviceSubdomainSchema).default([]), lastDeployed: z .object({ at: z.string().datetime().nullable(), state: z.enum(["succeeded", "failed"]).nullable(), operationId: z.string().nullable(), }) .default({ at: null, state: null, operationId: null }), lastDevDeployed: z .object({ at: z.string().datetime().nullable(), state: z.enum(["succeeded", "failed"]).nullable(), operationId: z.string().nullable(), }) .default({ at: null, state: null, operationId: null }) .describe("RFC-0806: last dev-channel deployment state"), healthCheckPath: z.string().optional(), }); // RFC-0790: Convention-based discovery schemas (replaces fleet registry) // RFC-1275: `integrations` declares sternsystem-level service integrations // (integrations.journal → CLIENT_JOURNAL [[services]] binding + JOURNAL_TENANT_ID // var in the generated wrangler config). Loose objects at every level — // system-config.yaml is the persisted source of truth and schema-driven // read→write passes (auto-pin, state writes) must preserve extension keys // declared by newer platform versions rather than silently stripping them. export const systemConfigIntegrationsSchema = z.looseObject({ journal: z .looseObject({ service: z.string().min(1, "integrations.journal.service must be non-empty"), tenantId: z.string().min(1, "integrations.journal.tenantId must be non-empty"), }) .optional() .describe("RFC-1275: client-journal service binding declaration"), }); export const systemConfigSchema = z.looseObject({ schemaVersion: z.string().min(1), id: z.string().regex(kebabRe, "id must be kebab-case, lowercase, latin-only, no TLD suffix"), cosmicStar: starNameSchema, mirrors: z.array(mirrorEntrySchema).min(1, "mirrors must contain at least 1 entry"), pinnedPlatform: z.string().regex(semverRe, "pinnedPlatform must be x.y.z"), status: z.enum(["registered", "active", "paused", "archived"]), registeredAt: z.string().datetime(), integrations: systemConfigIntegrationsSchema .optional() .describe("RFC-1275: sternsystem-level service integrations (journal binding, ...)"), deployment: deploymentStaticConfigSchema.optional(), cloudflareZoneId: z .string() .min(1, "cloudflareZoneId must be non-empty") .optional() .describe("Cloudflare zone ID for DNS and Workers route management (RFC-0752)"), owner: z .string() .regex(didWebRe, "owner must be a did:web identifier (did:web:#)") .optional() .describe("VC subject id of the site owner (RFC-0558, RFC-0561)"), notes: z.string().default(""), fleet: z .object({ canary: z.boolean().default(false), }) .optional() .describe("RFC-0964: fleet wave orchestration config"), }); // Loose for the same reason as systemConfigSchema — system-state.yaml is // rewritten by writeSystemState* on every state transition; undeclared // extension keys must round-trip rather than be eaten by parse→write. export const systemStateSchema = z.looseObject({ schemaVersion: z.string().min(1), systemId: z .string() .regex(kebabRe, "systemId must be kebab-case, lowercase, latin-only, no TLD suffix"), currentMission: z.string().nullable().default(null), lastRelease: z.string().nullable().default(null), lastPropagated: z .object({ dev: lastPropagatedChannelSchema.optional(), alt: lastPropagatedChannelSchema.optional(), main: lastPropagatedChannelSchema.optional(), }) .default({}), // RFC-0899: 4-digit PIN for dev/alt subdomain access protection. Null when not set. accessPin: z .string() .regex(/^\d{4}$/, "accessPin must be a 4-digit numeric string") .nullable() .default(null), // RFC-1284: transient suspension of access protection. Present only while // protection is suspended; accessPin keeps the desired value throughout. accessPinSuspended: z .object({ at: z.string(), // ISO-8601 suspension timestamp by: z.string(), // invoker, e.g. "leitstand.certify" operationId: z.string().nullable().default(null), }) .nullable() .default(null), // RFC-0966: Controls PASSPORT-01 warning vs blocking. false during rollout, // set to true by sternsystem.passport.generate after first successful generation. passportRequired: z.boolean().default(false), // RFC-0967: Controls OWNERSHIP-01 warning vs blocking. false during rollout, // set to true per-site when ready for blocking enforcement. ownershipRequired: z.boolean().default(false), // RFC-1307: provenance of the werkstatt:intake/v1 bundle the system was // born from (sternsystem.register --intake). Undefined for pre-intake systems. onboarding: z .object({ intakeId: z.string().regex(/^int_[0-9a-f-]+$/, "intakeId must be int_"), intakeSha256: z .string() .regex(/^sha256:[0-9a-f]{64}$/, "intakeSha256 must be sha256:<64 hex>"), contractSnapshot: z .string() .regex(/^sha256:[0-9a-f]{64}$/, "contractSnapshot must be sha256:<64 hex>"), }) .strict() .optional(), // RFC-1065: Lagebild connection metadata. Undefined when not connected. lagebild: z .object({ connected: z.boolean(), apiUrl: z.string(), tenantId: z.string(), sourceSystemId: z.string(), connectedAt: z.string(), channel: z.string(), }) .optional(), // ADR-0091: optional git SHA baseline — commits at-or-older-than this SHA in // rev-list order are grandfathered by the external-edit guard (pre-bordbuch // history). Operator-set mutable state; no kernel command writes it. externalEditBaselineSha: z .string() .regex(/^[0-9a-f]{40}$/, "externalEditBaselineSha must be a full 40-char lowercase git SHA") .optional(), // RFC-1263: content fast-lane activation state — absent/"advisory" runs // mission.content-edit gates 1–6 and stops before close; "active" also // closes the mission. Operator-set per site. contentEditLane: z.enum(["advisory", "active"]).optional(), // RFC-1210: closed-loop remediation autonomy — opt-in per site. Fields carry // their own defaults; an absent block means autonomy: off with the default // signal-source allowlist (validated lazily by remediation.autonomy.validate). remediation: remediationAutonomyConfigSchema.optional(), }); export const servicesRegistrySchema = z.object({ schemaVersion: z.string().min(1), services: z.array(serviceEntrySchema), }); export type SystemPin = z.infer; export type ServiceSubdomain = z.infer; export type ServiceEntry = z.infer; export type SystemConfig = z.infer; export type SystemState = z.infer; export type ServicesRegistry = z.infer;