package domain import ( "crypto/sha256" "encoding/hex" "encoding/json" "errors" "net/url" "strings" "git.cin.su/m/Assistant/contracts/capability" ) var ErrCandidateBindingRequired = errors.New("candidate capability binding required") type canonicalCandidateTargetDescriptor struct { Version string `json:"version"` BindingRevision int64 `json:"binding_revision"` TargetKind string `json:"target_kind"` TargetRef string `json:"target_ref"` Protocol string `json:"protocol"` Origin string `json:"origin"` ReleaseSetID string `json:"release_set_id"` Slot string `json:"slot"` TaskID string `json:"task_id"` TaskRevision int64 `json:"task_revision"` CapabilityID string `json:"capability_id"` ResourceScope string `json:"resource_scope"` Mode string `json:"mode"` } func CandidateTargetDigest(value capability.CandidateBinding) (string, error) { origin, err := url.Parse(value.TargetOrigin) if err != nil || origin.Host == "" || origin.User != nil || origin.RawQuery != "" || origin.Fragment != "" || (origin.Path != "" && origin.Path != "/") { return "", ErrCandidateBindingRequired } origin.Scheme, origin.Host, origin.Path = strings.ToLower(origin.Scheme), strings.ToLower(origin.Host), "" descriptor := canonicalCandidateTargetDescriptor{Version: "v1", BindingRevision: value.Revision, TargetKind: value.Context.TargetKind, TargetRef: value.Context.TargetRef, Protocol: string(value.Protocol), Origin: origin.String(), ReleaseSetID: value.Context.ReleaseSetID, Slot: value.Context.Slot, TaskID: value.Context.TaskID, TaskRevision: value.Context.TaskRevision, CapabilityID: string(value.Context.CapabilityID), ResourceScope: value.Context.ResourceScope, Mode: string(value.Context.Mode)} encoded, err := json.Marshal(descriptor) if err != nil { return "", err } sum := sha256.Sum256(encoded) return hex.EncodeToString(sum[:]), nil } // ValidateCandidateBinding validates Capability-owned candidate state. Stable // Binding validation intentionally remains stricter and HTTPS-only. func ValidateCandidateBinding(value capability.CandidateBinding) error { context := value.Context if value.ID == "" || value.Revision < 1 || value.BaseActiveBindingID == "" || value.BaseActiveBindingRevision < 1 || context.ReleaseSetID == "" || context.Slot == "" || context.TaskID == "" || context.TaskRevision < 1 || context.PrincipalID == "" || context.CapabilityID == "" || context.ResourceScope == "" || context.TargetKind == "" || context.TargetRef == "" || len(context.TargetDigest) != 64 || context.AuthorityGrantID == "" || (value.Status != capability.CandidateBindingActive && value.Status != capability.CandidateBindingRevoked) || (value.Context.Mode != capability.CandidateBindingModeCandidate && value.Context.Mode != capability.CandidateBindingModeShadow) || value.Protocol != capability.ProtocolIdempotentReceiptV1 || value.CreatedAtUnixMillis < 1 || value.ExpiresAtUnixMillis <= value.CreatedAtUnixMillis || !fixedPath(value.DeliveryPath) || !fixedPath(value.ReceiptPath) || value.DeliveryPath == value.ReceiptPath { return ErrCandidateBindingRequired } origin, err := url.Parse(value.TargetOrigin) if err != nil || origin.Host == "" || origin.User != nil || origin.RawQuery != "" || origin.Fragment != "" || (origin.Path != "" && origin.Path != "/") { return ErrCandidateBindingRequired } if context.Mode == capability.CandidateBindingModeCandidate && origin.Scheme != "https" { return ErrCandidateBindingRequired } if context.Mode == capability.CandidateBindingModeShadow && origin.Scheme != "http" && origin.Scheme != "https" { return ErrCandidateBindingRequired } if strings.TrimSpace(value.TargetOrigin) != value.TargetOrigin { return ErrCandidateBindingRequired } digest, err := CandidateTargetDigest(value) if err != nil || digest != context.TargetDigest { return ErrCandidateBindingRequired } return nil } func CandidateBindingInvocable(value capability.CandidateBinding, context capability.CandidateReleaseContext, nowUnixMillis int64) error { if ValidateCandidateBinding(value) != nil || value.Status != capability.CandidateBindingActive || nowUnixMillis < value.CreatedAtUnixMillis || nowUnixMillis >= value.ExpiresAtUnixMillis || value.Context != context { return ErrCandidateBindingRequired } return nil }