package ai import ( "encoding/json" "errors" "fmt" "time" "git.ipao.vip/rogee/go-sip/internal/contract" ) type Authorization struct { AuthorizationID string `json:"authorization_id"` TenantID string `json:"tenant_id"` TenantKey string `json:"tenant_key"` AgentVersionID string `json:"agent_version_id"` ConfigSHA256 string `json:"config_sha256"` Mode Mode `json:"mode"` IssuedAt string `json:"issued_at"` ExpiresAt string `json:"expires_at"` Source string `json:"source"` CredentialRefs map[string]string `json:"credential_refs"` Revoked bool `json:"revoked"` RevocationReason string `json:"revocation_reason"` } // DecodeBoundAuthorization validates identity and immutable configuration binding. // It deliberately preserves revoked/expired grants as facts, not permissions. func DecodeBoundAuthorization(raw []byte, snapshot Snapshot, tenantID, tenantKey string) (Authorization, error) { if err := contract.ValidateLocalAIAuthorization(raw); err != nil { return Authorization{}, err } var authorization Authorization if err := json.Unmarshal(raw, &authorization); err != nil { return Authorization{}, err } if authorization.TenantID != tenantID || authorization.TenantKey != tenantKey { return Authorization{}, errors.New("AI authorization tenant binding mismatch") } if authorization.AgentVersionID != snapshot.AgentVersionID || authorization.ConfigSHA256 != snapshot.Digest || authorization.Mode != snapshot.Mode { return Authorization{}, errors.New("AI authorization does not match immutable snapshot") } issuedAt, err := time.Parse(time.RFC3339, authorization.IssuedAt) if err != nil { return Authorization{}, fmt.Errorf("parse AI authorization issued_at: %w", err) } expiresAt, err := time.Parse(time.RFC3339, authorization.ExpiresAt) if err != nil { return Authorization{}, fmt.Errorf("parse AI authorization expires_at: %w", err) } if !issuedAt.Before(expiresAt) { return Authorization{}, errors.New("AI authorization has an invalid validity window") } return authorization, nil } func ValidateAuthorization(raw []byte, snapshot Snapshot, tenantID, tenantKey string, now time.Time) (Authorization, error) { authorization, err := DecodeBoundAuthorization(raw, snapshot, tenantID, tenantKey) if err != nil { return Authorization{}, err } if authorization.Revoked { return Authorization{}, errors.New("AI authorization is revoked") } issuedAt, err := time.Parse(time.RFC3339, authorization.IssuedAt) if err != nil { return Authorization{}, err } expiresAt, err := time.Parse(time.RFC3339, authorization.ExpiresAt) if err != nil { return Authorization{}, err } if now.Before(issuedAt) || !now.Before(expiresAt) { return Authorization{}, errors.New("AI authorization is outside its validity window") } return authorization, nil }