// Package emit defines the render-format contract between Go-side emitters // and bash-side appliers (grill C-16). Every rendered artifact is a JSON // object with a versioned schema; both sides validate against it to prevent // emitter/applier drift. package emit import ( "encoding/json" "errors" "fmt" ) // SchemaVersion is the canonical versioned schema identifier for render // contracts. Bump the suffix when the contract shape changes. const SchemaVersion = "orca.emit/v1" // Kind enumerates the rendered-artifact kinds. Each maps to an emitter // implementation and a matching bash-side applier. type Kind string const ( KindSystemd Kind = "systemd" KindTraefik Kind = "traefik" KindSyncthing Kind = "syncthing" KindSudoers Kind = "sudoers" KindSSHD Kind = "sshd" KindEnvFile Kind = "envfile" KindCredential Kind = "credential" ) // Artifact is a single rendered file destined for a peer. The bash-side // applier reads this JSON and writes Content to Path with the given Mode. type Artifact struct { SchemaVersion string `json:"schema_version"` Kind Kind `json:"kind"` Path string `json:"path"` Content string `json:"content"` Mode string `json:"mode"` } // Validate checks that an Artifact conforms to the render contract. // Returns a structured error if any field is missing or invalid. func (a *Artifact) Validate() error { if a.SchemaVersion != SchemaVersion { return fmt.Errorf("emit: schema_version mismatch: got %q want %q", a.SchemaVersion, SchemaVersion) } if a.Kind == "" { return errors.New("emit: kind is required") } if a.Path == "" { return errors.New("emit: path is required") } if a.Mode == "" { return errors.New("emit: mode is required") } return nil } // Marshal serializes an Artifact to JSON for transport to the bash applier. func (a *Artifact) Marshal() ([]byte, error) { if err := a.Validate(); err != nil { return nil, err } return json.Marshal(a) } // UnmarshalArtifact parses a JSON byte slice into an Artifact and validates // it against the contract. The bash-side applier (via orca-verify-render.sh) // uses this same validation; the bash side rejects unparseable input with a // structured error, never silently (grill C-16). func UnmarshalArtifact(data []byte) (*Artifact, error) { var a Artifact if err := json.Unmarshal(data, &a); err != nil { return nil, fmt.Errorf("emit: unmarshal: %w", err) } if err := a.Validate(); err != nil { return nil, err } return &a, nil }