package store import ( "context" "strings" "time" ) // Backend is the persistence abstraction for the control plane (memory, PostgreSQL, SQLite). type Backend interface { MaterializedPrefixStats() (max int, sum int) PeerCount() int PeerSessionCountsByState() map[string]int // DemoIDs is non-empty only after SeedDemo (memory or seeded SQL). DemoIDs() (tenant, moduleCDN, moduleIP, revision, speaker string) // ListTenantIDs returns distinct tenant identifiers (for background workers). ListTenantIDs() ([]string, error) // ListModules returns all modules for a tenant (control plane may paginate in httpapi). ListModules(tenantID string) []*Module // ListModulesPage returns one page of modules (limit capped by caller). ListModulesPage(tenantID, cursor string, limit int) ([]*Module, string, bool) GetModule(tenantID, moduleID string) (*Module, error) CreateModule(tenantID string, in *Module) (*Module, error) UpdateModule(tenantID, moduleID string, patch *ModulePatch) (*Module, error) SoftDeleteModule(tenantID, moduleID string) error ListCDNSources(tenantID, moduleID string) ([]*CDNSource, error) CreateCDNSource(tenantID, moduleID string, in *CDNSource) (*CDNSource, error) UpdateCDNSource(tenantID, moduleID, sourceID string, patch *CDNSourcePatch) (*CDNSource, error) DeleteCDNSource(tenantID, moduleID, sourceID string) error ListASEntries(tenantID, moduleID string) ([]*ASEntry, error) CreateASEntry(tenantID, moduleID string, in *ASEntry) (*ASEntry, error) UpdateASEntry(tenantID, moduleID, entryID string, patch *ASEntryPatch) (*ASEntry, error) DeleteASEntry(tenantID, moduleID, entryID string) error // UpdateASEntryResolveMeta записывает имя AS, число объявленных префиксов и время успешного резолва (pipeline). UpdateASEntryResolveMeta(tenantID, moduleID, entryID string, asnName string, prefixCount int64, resolvedAt time.Time) error UpdateASEntryResolveMetaBatch(tenantID, moduleID string, updates []ASEntryResolveMetaUpdate, resolvedAt time.Time) error ListDomainEntries(tenantID, moduleID string) ([]*DomainEntry, error) CreateDomainEntry(tenantID, moduleID string, in *DomainEntry) (*DomainEntry, error) UpdateDomainEntry(tenantID, moduleID, entryID string, patch *DomainEntryPatch) (*DomainEntry, error) DeleteDomainEntry(tenantID, moduleID, entryID string) error ListIPRangeEntries(tenantID, moduleID string) ([]*IPRangeEntry, error) CreateIPRangeEntry(tenantID, moduleID string, in *IPRangeEntry) (*IPRangeEntry, error) UpdateIPRangeEntry(tenantID, moduleID, entryID string, patch *IPRangePatch) (*IPRangeEntry, error) DeleteIPRangeEntry(tenantID, moduleID, entryID string) error ListDohProfiles(tenantID string) ([]*DohProfile, error) GetDohProfile(tenantID, id string) (*DohProfile, error) CreateDohProfile(tenantID string, in *DohProfile) (*DohProfile, error) UpdateDohProfile(tenantID, id string, patch *DohProfilePatch) (*DohProfile, error) DeleteDohProfile(tenantID, id string) error ListCommunities(tenantID string) ([]*Community, error) GetCommunity(tenantID, id string) (*Community, error) CreateCommunity(tenantID string, in *Community) (*Community, error) UpdateCommunity(tenantID, id string, patch *CommunityPatch) (*Community, error) DeleteCommunity(tenantID, id string) error // ListPeers returns all BGP peers for a tenant (control plane may paginate in httpapi). ListPeers(tenantID string) []*BGPPeer GetPeer(tenantID, id string) (*BGPPeer, error) CreatePeer(tenantID string, in *BGPPeer) (*BGPPeer, error) UpdatePeer(tenantID, id string, patch *PeerPatch) (*BGPPeer, error) DeletePeer(tenantID, id string) error ListSpeakersForTenant(tenantID string) []*Speaker GetSpeaker(tenantID, speakerID string) (*Speaker, error) GetSpeakerAnyTenant(speakerID string) (*Speaker, error) CreateSpeaker(tenantID string, in *Speaker) (*Speaker, error) UpdateSpeaker(tenantID, id string, patch *SpeakerPatch) (*Speaker, error) GetRevision(tenantID, revisionID string) (*Revision, error) ListRevisions(tenantID, moduleID string, cursor string, limit int) (items []*Revision, nextCursor string, hasMore bool) ListRevisionPrefixes(tenantID, revisionID string, cursor string, limit int) (prefixes []PrefixRow, next string, more bool) CreateRollbackRevision(tenantID, sourceRevisionID string) (newID string, err error) // CreateRenderRevision inserts a new config_revision (revID must be unique) with materialized prefixes and preview fragments. CreateRenderRevision(revisionID, tenantID, moduleID string, parentRevisionID *string, contentHash string, previewFragments map[string]string, prefixes []PrefixRow) error RevisionDiff(tenantID, aID, bID string) (map[string]any, error) PruneRevisionsBefore(tenantID string, cutoff time.Time) (deleted int, err error) SetLastAppliedRevision(tenantID, speakerID, revisionID string) error PublishRevisionForSpeaker(speakerID, revisionID string) error LatestPublishedRevision(speakerID string) (revisionID string, publishedAt time.Time, err error) ListGlobalSettings(tenantID string) (map[string]any, error) PatchGlobalSettings(tenantID string, patch map[string]any) error ListAPIKeys(tenantID string) ([]*APIKey, error) GetAPIKey(tenantID, id string) (*APIKey, error) CreateAPIKey(tenantID string, in *APIKeyCreate) (*APIKeyWithSecret, error) UpdateAPIKey(tenantID, id string, patch *APIKeyPatch) (*APIKey, error) RevokeAPIKey(tenantID, id string) error RotateAPIKey(tenantID, id string) (*APIKeyWithSecret, error) ListActiveAPIKeyHashes() ([]APIKeyAuthRow, error) TouchAPIKeyLastUsed(id string) error // Module prefix snapshots cache last successful collect per module (pipeline ingest/render). GetModulePrefixSnapshot(tenantID, moduleID string) (*ModulePrefixSnapshot, bool, error) SetModulePrefixSnapshot(tenantID, moduleID, inputHash string, prefixes []PrefixRow) error DeleteModulePrefixSnapshot(tenantID, moduleID string) error // ASNPrefixCache stores RIPEstat announced-prefixes per ASN (global TTL cache). GetASNPrefixCache(asn int64) (*ASNPrefixCacheEntry, bool, error) SetASNPrefixCache(asn int64, holder string, prefixes []string) error // Ping verifies backend connectivity (no-op for in-memory). Ping(ctx context.Context) error } // ASNPrefixCacheEntry is a cached RIPEstat response for one ASN. type ASNPrefixCacheEntry struct { ASN int64 Holder string Prefixes []string FetchedAt time.Time } // ModulePrefixSnapshot is the cached materialization for one module between refreshes. type ModulePrefixSnapshot struct { InputHash string CollectedAt time.Time Prefixes []PrefixRow } // ModulePatch is a partial update for module. type ModulePatch struct { Name *string `json:"name,omitempty"` Enabled *bool `json:"enabled,omitempty"` Priority *int `json:"priority,omitempty"` RefreshIntervalSec *int `json:"refresh_interval_sec,omitempty"` CronExpr *string `json:"cron_expr,omitempty"` DefaultCommunityID *string `json:"default_community_id,omitempty"` DohProfileID *string `json:"doh_profile_id,omitempty"` DohProfileIDs *[]string `json:"doh_profile_ids,omitempty"` DohResolverPolicy *string `json:"doh_resolver_policy,omitempty"` LastRefreshedAt *time.Time `json:"-"` } // CDNSource is a row under a CDN module. type CDNSource struct { ID string `json:"id,omitempty"` ModuleID string `json:"module_id,omitempty"` SourceKind string `json:"source_kind"` URL string `json:"url"` PrefixPath string `json:"prefix_path,omitempty"` Etag string `json:"etag"` RefreshIntervalSec *int `json:"refresh_interval_sec"` CommunityID *string `json:"community_id"` LastRefreshedAt *time.Time `json:"-"` } type CDNSourcePatch struct { SourceKind *string `json:"source_kind,omitempty"` URL *string `json:"url,omitempty"` PrefixPath *string `json:"prefix_path,omitempty"` Etag *string `json:"etag,omitempty"` RefreshIntervalSec *int `json:"refresh_interval_sec,omitempty"` CommunityID *string `json:"community_id,omitempty"` LastRefreshedAt *time.Time `json:"-"` } type ASEntry struct { ID string `json:"id,omitempty"` ModuleID string `json:"module_id,omitempty"` ASN int64 `json:"asn"` CommunityID *string `json:"community_id"` ASNName string `json:"asn_name,omitempty"` PrefixCount *int64 `json:"prefix_count,omitempty"` ASNResolvedAt *time.Time `json:"asn_resolved_at,omitempty"` } type ASEntryPatch struct { ASN *int64 `json:"asn,omitempty"` CommunityID *string `json:"community_id,omitempty"` } // ValidASN reports whether n is a usable BGP ASN (1..4294967295). func ValidASN(n int64) bool { return n >= 1 && n <= 4294967295 } type DomainEntry struct { ID string `json:"id,omitempty"` ModuleID string `json:"module_id,omitempty"` FQDN string `json:"fqdn"` CommunityID *string `json:"community_id"` } type DomainEntryPatch struct { FQDN *string `json:"fqdn,omitempty"` CommunityID *string `json:"community_id,omitempty"` } type IPRangeEntry struct { ID string `json:"id,omitempty"` ModuleID string `json:"module_id,omitempty"` Prefix string `json:"prefix"` CommunityID *string `json:"community_id"` } type IPRangePatch struct { Prefix *string `json:"prefix,omitempty"` CommunityID *string `json:"community_id,omitempty"` } type DohProfile struct { ID string `json:"id,omitempty"` TenantID string `json:"tenant_id,omitempty"` Name string `json:"name"` URL string `json:"url"` TimeoutMs *int `json:"timeout_ms"` SecretRef *string `json:"vault_secret_ref"` } type DohProfilePatch struct { Name *string `json:"name,omitempty"` URL *string `json:"url,omitempty"` TimeoutMs *int `json:"timeout_ms,omitempty"` SecretRef *string `json:"vault_secret_ref,omitempty"` } type Community struct { ID string `json:"id,omitempty"` TenantID string `json:"tenant_id,omitempty"` Community string `json:"community"` Title string `json:"title"` ValueJSON string `json:"value_json"` } type CommunityPatch struct { Community *string `json:"community,omitempty"` Title *string `json:"title,omitempty"` ValueJSON *string `json:"value_json,omitempty"` } // APIKey is tenant-scoped API key metadata (secret never stored in plaintext). type APIKey struct { ID string `json:"id"` TenantID string `json:"tenant_id,omitempty"` Name string `json:"name"` Role string `json:"role"` Prefix string `json:"prefix"` CreatedAt time.Time `json:"created_at"` UpdatedAt time.Time `json:"updated_at"` ExpiresAt *time.Time `json:"expires_at,omitempty"` RevokedAt *time.Time `json:"revoked_at,omitempty"` LastUsedAt *time.Time `json:"last_used_at,omitempty"` } // APIKeyCreate is input for issuing a new key. type APIKeyCreate struct { Name string `json:"name"` Role string `json:"role"` ExpiresAt *time.Time `json:"expires_at,omitempty"` } // APIKeyPatch is a partial update (role change affects auth after resolver reload). type APIKeyPatch struct { Name *string `json:"name,omitempty"` Role *string `json:"role,omitempty"` ExpiresAt *time.Time `json:"expires_at,omitempty"` ClearExpiresAt bool `json:"-"` } // APIKeyWithSecret is returned only on create/rotate. type APIKeyWithSecret struct { APIKey Token string `json:"token"` } // APIKeyAuthRow is used to build the in-process auth index. type APIKeyAuthRow struct { ID string TenantID string Role string TokenHash []byte } // ValidAPIKeyRole reports whether role is allowed for API keys. func ValidAPIKeyRole(role string) bool { switch strings.ToLower(strings.TrimSpace(role)) { case "viewer", "editor", "operator", "node": return true default: return false } } type PeerPatch struct { Neighbor *string `json:"neighbor,omitempty"` RemoteASN *int64 `json:"remote_asn,omitempty"` SpeakerID *string `json:"bgp_speaker_id,omitempty"` Enabled *bool `json:"enabled,omitempty"` Name *string `json:"name,omitempty"` SessionState *string `json:"session_state,omitempty"` PoliciesJSON *string `json:"policies_json,omitempty"` } type SpeakerPatch struct { Role *string `json:"role,omitempty"` Endpoint *string `json:"endpoint,omitempty"` MetaJSON *string `json:"meta_json,omitempty"` } // PrefixRow is one materialized prefix for GET /revisions/.../prefixes. type PrefixRow struct { Prefix string CommunityID *string Source string }