package contract import ( "encoding/json" "fmt" ) // StaticCellArtifact is the management-approved, immutable Cell/SIP hand-off // artifact. Its JSON shape is owned by static-cell-artifact.schema.json; this // type only provides a typed boundary after schema validation. type StaticCellArtifact struct { ArtifactID string `json:"artifact_id"` SourceRelease string `json:"source_release"` SourceDigest string `json:"source_digest"` ApprovalReference string `json:"approval_reference"` CellID string `json:"cell_id"` Revision uint64 `json:"revision"` ConfigSHA256 string `json:"config_sha256"` Mode string `json:"mode"` AllowedTargets []string `json:"allowed_targets"` Trunks []StaticTrunk `json:"trunks"` ARI *StaticARI `json:"ari,omitempty"` MediaProfiles map[string]StaticMediaProfile `json:"media_profiles,omitempty"` Media *StaticMedia `json:"media,omitempty"` Recording *StaticRecording `json:"recording,omitempty"` LoadEvidence *StaticLoadEvidence `json:"load_evidence"` } type StaticTrunk struct { TrunkID string `json:"trunk_id"` ProviderID string `json:"provider_id"` EgressPoolID string `json:"egress_pool_id"` Codec string `json:"codec"` CallerProfileIDs []string `json:"caller_profile_ids"` DialPrefix string `json:"dial_prefix"` Enabled bool `json:"enabled"` SIPEndpointRef string `json:"sip_endpoint_ref"` CredentialRef *string `json:"credential_ref"` MediaProfileID string `json:"media_profile_id,omitempty"` } type StaticARI struct { BaseURL string `json:"base_url"` WebsocketURL string `json:"websocket_url"` Application string `json:"application"` CredentialRef string `json:"credential_ref"` } type StaticMedia struct { BindAddress string `json:"bind_address"` Port int `json:"port"` Format string `json:"format"` SampleRateHz int `json:"sample_rate_hz"` Channels int `json:"channels"` PayloadType int `json:"payload_type"` } type StaticMediaProfile struct { Format string `json:"format"` SampleRateHz int `json:"sample_rate_hz"` Channels int `json:"channels"` PayloadType int `json:"payload_type"` } type StaticRecording struct { Enabled bool `json:"enabled"` Format string `json:"format"` Directory string `json:"directory"` MaxBytes int64 `json:"max_bytes"` } type StaticLoadEvidence struct { AsteriskConfigSHA256 string `json:"asterisk_config_sha256"` LoadedAt string `json:"loaded_at"` Status string `json:"status"` } // StaticArtifactExpectation contains deployment-local binding constraints. // Empty string/slice values leave the corresponding optional check disabled; // the source contract remains mandatory and is always validated first. type StaticArtifactExpectation struct { CellID string Mode string SourceRelease string SourceDigest string ConfigSHA256 string MinimumRevision uint64 AllowedEgressPoolIDs []string RequiredTrunkIDs []string } // ValidateStaticArtifact validates the imported artifact schema and then // applies the local Cell hand-off bindings. It deliberately does not claim // that Asterisk has loaded the artifact: load_evidence.status is explicitly // "not-yet-loaded" in the contract until an independent load check exists. func ValidateStaticArtifact(raw []byte, expected StaticArtifactExpectation) (StaticCellArtifact, error) { if err := ValidateSourceSchema("static-cell-artifact.schema.json", raw); err != nil { return StaticCellArtifact{}, err } var artifact StaticCellArtifact if err := json.Unmarshal(raw, &artifact); err != nil { return StaticCellArtifact{}, fmt.Errorf("decode static Cell artifact: %w", err) } if expected.CellID != "" && artifact.CellID != expected.CellID { return StaticCellArtifact{}, fmt.Errorf("static artifact cell binding mismatch: got %q, want %q", artifact.CellID, expected.CellID) } if expected.Mode != "" && artifact.Mode != expected.Mode { return StaticCellArtifact{}, fmt.Errorf("static artifact mode mismatch: got %q, want %q", artifact.Mode, expected.Mode) } if expected.SourceRelease != "" && artifact.SourceRelease != expected.SourceRelease { return StaticCellArtifact{}, fmt.Errorf("static artifact source release mismatch: got %q, want %q", artifact.SourceRelease, expected.SourceRelease) } if expected.SourceDigest != "" && artifact.SourceDigest != expected.SourceDigest { return StaticCellArtifact{}, fmt.Errorf("static artifact source digest mismatch: got %q, want %q", artifact.SourceDigest, expected.SourceDigest) } if expected.ConfigSHA256 != "" && artifact.ConfigSHA256 != expected.ConfigSHA256 { return StaticCellArtifact{}, fmt.Errorf("static artifact config digest mismatch: got %q, want %q", artifact.ConfigSHA256, expected.ConfigSHA256) } if expected.MinimumRevision != 0 && artifact.Revision < expected.MinimumRevision { return StaticCellArtifact{}, fmt.Errorf("static artifact revision %d is older than required %d", artifact.Revision, expected.MinimumRevision) } allowedEgress := make(map[string]struct{}, len(expected.AllowedEgressPoolIDs)) for _, egressPoolID := range expected.AllowedEgressPoolIDs { allowedEgress[egressPoolID] = struct{}{} } requiredTrunks := make(map[string]struct{}, len(expected.RequiredTrunkIDs)) for _, trunkID := range expected.RequiredTrunkIDs { requiredTrunks[trunkID] = struct{}{} } seenTrunks := make(map[string]struct{}, len(artifact.Trunks)) for _, trunk := range artifact.Trunks { if _, duplicate := seenTrunks[trunk.TrunkID]; duplicate { return StaticCellArtifact{}, fmt.Errorf("static artifact contains duplicate trunk_id %q", trunk.TrunkID) } seenTrunks[trunk.TrunkID] = struct{}{} if len(allowedEgress) != 0 { if _, ok := allowedEgress[trunk.EgressPoolID]; !ok { return StaticCellArtifact{}, fmt.Errorf("static artifact trunk %q uses disallowed egress pool %q", trunk.TrunkID, trunk.EgressPoolID) } } if _, required := requiredTrunks[trunk.TrunkID]; required && !trunk.Enabled { return StaticCellArtifact{}, fmt.Errorf("required static artifact trunk %q is disabled", trunk.TrunkID) } if trunk.MediaProfileID != "" { if _, ok := artifact.MediaProfiles[trunk.MediaProfileID]; !ok { return StaticCellArtifact{}, fmt.Errorf("static artifact trunk %q references unknown media profile %q", trunk.TrunkID, trunk.MediaProfileID) } } } for trunkID := range requiredTrunks { if _, present := seenTrunks[trunkID]; !present { return StaticCellArtifact{}, fmt.Errorf("required static artifact trunk %q is missing", trunkID) } } return artifact, nil }