// Copyright 2026 certctl LLC. All rights reserved. // SPDX-License-Identifier: BUSL-1.1 package domain import ( "time" ) // ManagedCertificate represents a certificate managed by the control plane. type ManagedCertificate struct { ID string `json:"id"` Name string `json:"name"` CommonName string `json:"common_name"` SANs []string `json:"sans"` Environment string `json:"environment"` OwnerID string `json:"owner_id"` TeamID string `json:"team_id"` IssuerID string `json:"issuer_id"` TargetIDs []string `json:"target_ids"` RenewalPolicyID string `json:"renewal_policy_id"` CertificateProfileID string `json:"certificate_profile_id,omitempty"` Status CertificateStatus `json:"status"` ExpiresAt time.Time `json:"expires_at"` Tags map[string]string `json:"tags"` LastRenewalAt *time.Time `json:"last_renewal_at,omitempty"` LastDeploymentAt *time.Time `json:"last_deployment_at,omitempty"` RevokedAt *time.Time `json:"revoked_at,omitempty"` RevocationReason string `json:"revocation_reason,omitempty"` CreatedAt time.Time `json:"created_at"` UpdatedAt time.Time `json:"updated_at"` // Source tags how this managed certificate was created. EST RFC 7030 // hardening master bundle Phase 11.1 — operators bulk-revoke // EST-issued certs by filtering on Source=EST. Empty value preserves // the v2.X.0 behavior (the bulk-revoke handler treats empty as // equivalent to legacy/manual; new EST issuances stamp Source=EST, // new SCEP issuances will eventually stamp Source=SCEP under a // future bundle). Source CertificateSource `json:"source,omitempty"` } // CertificateSource is the enum of provenance values stamped on each // managed-certificate row when it's created. The empty string is the // back-compat default — pre-Phase-11 rows have it set to "" by the // migration's DEFAULT clause; the bulk-revoke filter treats empty as // "any source" so existing call paths see no behavior change. // // EST RFC 7030 hardening master bundle Phase 11.1. type CertificateSource string const ( // CertificateSourceUnspecified preserves the v2.X.0 default (""). CertificateSourceUnspecified CertificateSource = "" // CertificateSourceEST stamps every cert issued through one of the // EST endpoints (simpleenroll / simplereenroll / serverkeygen). CertificateSourceEST CertificateSource = "EST" // CertificateSourceSCEP / API / Agent reserve future provenance // values — not stamped today; SCEP-issued certs continue to land // with Source="" until a follow-up bundle wires the stamp at the // SCEP service layer. CertificateSourceSCEP CertificateSource = "SCEP" CertificateSourceAPI CertificateSource = "API" CertificateSourceAgent CertificateSource = "Agent" // CertificateSourceACME stamps every cert issued through the // built-in ACME server endpoint (RFC 8555 finalize → cert // download). The ACME service (internal/service/acme.go) // pins this on every managed_certificates row it inserts at // finalize time. Operators bulk-revoke ACME-issued certs by // filtering on Source=ACME. CertificateSourceACME CertificateSource = "ACME" ) // CertificateVersion represents a specific version of a certificate. type CertificateVersion struct { ID string `json:"id"` CertificateID string `json:"certificate_id"` SerialNumber string `json:"serial_number"` NotBefore time.Time `json:"not_before"` NotAfter time.Time `json:"not_after"` FingerprintSHA256 string `json:"fingerprint_sha256"` PEMChain string `json:"pem_chain"` CSRPEM string `json:"csr_pem"` KeyAlgorithm string `json:"key_algorithm,omitempty"` KeySize int `json:"key_size,omitempty"` CreatedAt time.Time `json:"created_at"` } // CertificateStatus represents the lifecycle status of a managed certificate. type CertificateStatus string const ( CertificateStatusPending CertificateStatus = "Pending" CertificateStatusActive CertificateStatus = "Active" CertificateStatusExpiring CertificateStatus = "Expiring" CertificateStatusExpired CertificateStatus = "Expired" CertificateStatusRenewalInProgress CertificateStatus = "RenewalInProgress" CertificateStatusFailed CertificateStatus = "Failed" CertificateStatusRevoked CertificateStatus = "Revoked" CertificateStatusArchived CertificateStatus = "Archived" ) // RenewalPolicy defines renewal parameters for a managed certificate. type RenewalPolicy struct { ID string `json:"id"` Name string `json:"name"` RenewalWindowDays int `json:"renewal_window_days"` AutoRenew bool `json:"auto_renew"` MaxRetries int `json:"max_retries"` RetryInterval int `json:"retry_interval_seconds"` AlertThresholdsDays []int `json:"alert_thresholds_days"` CertificateProfileID string `json:"certificate_profile_id,omitempty"` CreatedAt time.Time `json:"created_at"` UpdatedAt time.Time `json:"updated_at"` // AlertChannels is the per-policy channel-matrix that maps each // severity tier ("informational" / "warning" / "critical") to the // set of NotificationChannel values that receive expiry alerts at // that tier. Values are slices of channel-name strings matching // the NotificationChannel constants ("Email", "Slack", "Teams", // "PagerDuty", "OpsGenie", "Webhook"). nil or empty falls back to // DefaultAlertChannels (Email-only across all tiers, the pre-2026-05-03 // behaviour preserved as the safe default for operators who have // not yet opted into multi-channel routing). // // Off-enum severity keys or channel values are silently dropped at // the dispatch site (closed-enum discipline; we do NOT dynamically // grow Prometheus cardinality on a typo). // // Rank 4 of the 2026-05-03 Infisical deep-research deliverable // (the project's deep-research deliverable, Part 5). AlertChannels map[string][]string `json:"alert_channels,omitempty"` // AlertSeverityMap maps each threshold-day value to its severity // tier. Off-map thresholds default to "informational". Operators // with non-default AlertThresholdsDays values supply their own // severity mapping; operators on the canonical 30/14/7/0 thresholds // can leave this empty to inherit DefaultAlertSeverityMap which // maps: // // 30 → informational // 14 → warning // 7 → warning // 0 → critical AlertSeverityMap map[int]string `json:"alert_severity_map,omitempty"` } // DefaultAlertThresholds returns the standard alert thresholds when none are configured. func DefaultAlertThresholds() []int { return []int{30, 14, 7, 0} } // EffectiveAlertThresholds returns the configured thresholds or defaults if empty. func (p *RenewalPolicy) EffectiveAlertThresholds() []int { if len(p.AlertThresholdsDays) > 0 { return p.AlertThresholdsDays } return DefaultAlertThresholds() } // Severity-tier names for the channel matrix. Closed-enum to keep // Prometheus cardinality bounded and operator typos surfaceable in // audit logs (off-enum tier values are dropped at dispatch). const ( AlertSeverityInformational = "informational" AlertSeverityWarning = "warning" AlertSeverityCritical = "critical" ) // DefaultAlertChannels returns the back-compat default channel matrix // — Email only at every tier. This preserves the pre-2026-05-03 // behaviour for operators who have not yet opted into multi-channel // routing. Nil or empty AlertChannels on a RenewalPolicy is read as // "use this default." func DefaultAlertChannels() map[string][]string { return map[string][]string{ AlertSeverityInformational: {string(NotificationChannelEmail)}, AlertSeverityWarning: {string(NotificationChannelEmail)}, AlertSeverityCritical: {string(NotificationChannelEmail)}, } } // DefaultAlertSeverityMap returns the canonical threshold-to-tier // mapping for the standard 30/14/7/0 thresholds. Operators with // custom thresholds supply their own mapping. func DefaultAlertSeverityMap() map[int]string { return map[int]string{ 30: AlertSeverityInformational, 14: AlertSeverityWarning, 7: AlertSeverityWarning, 0: AlertSeverityCritical, } } // EffectiveAlertChannels returns the configured channel matrix on // the policy, or the default if unset. Used by the dispatch site in // RenewalService.sendThresholdAlerts to resolve the channel set for // a given tier. // // A returned map is safe to mutate by the caller — the default-path // branch returns a fresh map; the configured-path branch returns the // caller-supplied map (which the caller already owns). func (p *RenewalPolicy) EffectiveAlertChannels() map[string][]string { if p == nil || len(p.AlertChannels) == 0 { return DefaultAlertChannels() } return p.AlertChannels } // EffectiveAlertSeverity returns the severity tier for a given // threshold. Off-map thresholds resolve to "informational" so a // custom-thresholds policy without an explicit severity map still // gets dispatch (just at the lowest tier). func (p *RenewalPolicy) EffectiveAlertSeverity(threshold int) string { if p != nil { if tier, ok := p.AlertSeverityMap[threshold]; ok { return tier } } if tier, ok := DefaultAlertSeverityMap()[threshold]; ok { return tier } return AlertSeverityInformational } // IsValidAlertSeverityTier reports whether tier is one of the closed-enum // severity values. Used by the policy validation path in // service.RenewalPolicyService to reject typos at write time. func IsValidAlertSeverityTier(tier string) bool { switch tier { case AlertSeverityInformational, AlertSeverityWarning, AlertSeverityCritical: return true } return false } // IsValidNotificationChannel reports whether channel is one of the // closed-enum NotificationChannel values. Used by the policy // validation path to reject typos at write time AND by the dispatch // site to defensively drop off-enum values that survived a migration. func IsValidNotificationChannel(channel string) bool { switch NotificationChannel(channel) { case NotificationChannelEmail, NotificationChannelWebhook, NotificationChannelSlack, NotificationChannelTeams, NotificationChannelPagerDuty, NotificationChannelOpsGenie: return true } return false }