first commit

This commit is contained in:
Beyhan Oğur
2026-04-26 21:52:23 +03:00
commit 880f412e2c
2662 changed files with 866266 additions and 0 deletions

View File

@@ -0,0 +1,64 @@
package tables
import (
"fmt"
"time"
"gorm.io/gorm"
)
// TableBudget defines spending limits with configurable reset periods
type TableBudget struct {
ID string `gorm:"primaryKey;type:varchar(255)" json:"id"`
MaxLimit float64 `gorm:"not null" json:"max_limit"` // Maximum budget in dollars
ResetDuration string `gorm:"type:varchar(50);not null" json:"reset_duration"` // e.g., "30s", "5m", "1h", "1d", "1w", "1M", "1Y"
LastReset time.Time `gorm:"index" json:"last_reset"` // Last time budget was reset
CurrentUsage float64 `gorm:"default:0" json:"current_usage"` // Current usage in dollars
// Owner FKs: a budget belongs to at most one Team, one VK, or one ProviderConfig
TeamID *string `gorm:"type:varchar(255);index" json:"team_id,omitempty"`
VirtualKeyID *string `gorm:"type:varchar(255);index" json:"virtual_key_id,omitempty"`
ProviderConfigID *uint `gorm:"index" json:"provider_config_id,omitempty"`
CalendarAligned bool `gorm:"default:false" json:"calendar_aligned"` // When true, all budgets under this VK reset at clean calendar boundaries
// Config hash is used to detect the changes synced from config.json file
// Every time we sync the config.json file, we will update the config hash
ConfigHash string `gorm:"type:varchar(255);null" json:"config_hash"`
CreatedAt time.Time `gorm:"index;not null" json:"created_at"`
UpdatedAt time.Time `gorm:"index;not null" json:"updated_at"`
}
// TableName sets the table name for each model
func (TableBudget) TableName() string { return "governance_budgets" }
// BeforeSave hook for Budget to validate reset duration format and max limit
func (b *TableBudget) BeforeSave(tx *gorm.DB) error {
// A budget belongs to at most one owner type
owners := 0
if b.TeamID != nil {
owners++
}
if b.VirtualKeyID != nil {
owners++
}
if b.ProviderConfigID != nil {
owners++
}
if owners > 1 {
return fmt.Errorf("budget cannot have more than one owner (team/virtual key/provider config)")
}
// Validate that ResetDuration is in correct format (e.g., "30s", "5m", "1h", "1d", "1w", "1M", "1Y")
if d, err := ParseDuration(b.ResetDuration); err != nil {
return fmt.Errorf("invalid reset duration format: %s", b.ResetDuration)
} else if d <= 0 {
return fmt.Errorf("reset duration must be > 0: %s", b.ResetDuration)
}
// Validate that MaxLimit is not negative (budgets should be positive)
if b.MaxLimit < 0 {
return fmt.Errorf("budget max_limit cannot be negative: %.2f", b.MaxLimit)
}
return nil
}

View File

@@ -0,0 +1,187 @@
package tables
import (
"encoding/json"
"time"
"gorm.io/gorm"
)
// TableClientConfig represents global client configuration in the database
type TableClientConfig struct {
ID uint `gorm:"primaryKey;autoIncrement" json:"id"`
DropExcessRequests bool `gorm:"default:false" json:"drop_excess_requests"`
PrometheusLabelsJSON string `gorm:"type:text" json:"-"` // JSON serialized []string
AllowedOriginsJSON string `gorm:"type:text" json:"-"` // JSON serialized []string
AllowedHeadersJSON string `gorm:"type:text" json:"-"` // JSON serialized []string
HeaderFilterConfigJSON string `gorm:"type:text" json:"-"` // JSON serialized GlobalHeaderFilterConfig
InitialPoolSize int `gorm:"default:300" json:"initial_pool_size"`
EnableLogging *bool `gorm:"default:true" json:"enable_logging"`
DisableContentLogging bool `gorm:"default:false" json:"disable_content_logging"` // DisableContentLogging controls whether sensitive content (inputs, outputs, embeddings, etc.) is logged
DisableDBPingsInHealth bool `gorm:"default:false" json:"disable_db_pings_in_health"`
LogRetentionDays int `gorm:"default:365" json:"log_retention_days" validate:"min=1"` // Number of days to retain logs (minimum 1 day)
EnforceAuthOnInference bool `gorm:"default:false" json:"enforce_auth_on_inference"`
EnforceGovernanceHeader bool `gorm:"" json:"enforce_governance_header"`
EnforceSCIMAuth bool `gorm:"default:false" json:"enforce_scim_auth"`
AllowDirectKeys bool `gorm:"" json:"allow_direct_keys"`
MaxRequestBodySizeMB int `gorm:"default:100" json:"max_request_body_size_mb"`
MCPAgentDepth int `gorm:"default:10" json:"mcp_agent_depth"`
MCPToolExecutionTimeout int `gorm:"default:30" json:"mcp_tool_execution_timeout"` // Timeout for individual tool execution in seconds (default: 30)
MCPCodeModeBindingLevel string `gorm:"default:server" json:"mcp_code_mode_binding_level"` // How tools are exposed in VFS: "server" or "tool"
MCPToolSyncInterval int `gorm:"default:10" json:"mcp_tool_sync_interval"` // Global tool sync interval in minutes (default: 10, 0 = disabled)
MCPDisableAutoToolInject bool `gorm:"default:false" json:"mcp_disable_auto_tool_inject"` // When true, MCP tools are not injected into requests by default
AsyncJobResultTTL int `gorm:"default:3600" json:"async_job_result_ttl"` // Default TTL for async job results in seconds (default: 3600 = 1 hour)
RequiredHeadersJSON string `gorm:"type:text" json:"-"` // JSON serialized []string
LoggingHeadersJSON string `gorm:"type:text" json:"-"` // JSON serialized []string
HideDeletedVirtualKeysInFilters bool `gorm:"default:false" json:"hide_deleted_virtual_keys_in_filters"` // Hide deleted virtual keys in logs filter dropdowns
RoutingChainMaxDepth int `gorm:"default:10" json:"routing_chain_max_depth"` // Maximum depth for routing rule chain evaluation (default: 10)
WhitelistedRoutesJSON string `gorm:"type:text" json:"-"` // JSON serialized []string
// Compat plugin feature flags
CompatConvertTextToChat bool `gorm:"column:compat_convert_text_to_chat;default:false" json:"-"`
CompatConvertChatToResponses bool `gorm:"column:compat_convert_chat_to_responses;default:false" json:"-"`
CompatShouldDropParams bool `gorm:"column:compat_should_drop_params;default:false" json:"-"`
CompatShouldConvertParams bool `gorm:"column:compat_should_convert_params;default:false" json:"-"`
// Config hash is used to detect the changes synced from config.json file
// Every time we sync the config.json file, we will update the config hash
ConfigHash string `gorm:"type:varchar(255);null" json:"config_hash"`
CreatedAt time.Time `gorm:"index;not null" json:"created_at"`
UpdatedAt time.Time `gorm:"index;not null" json:"updated_at"`
// Virtual fields for runtime use (not stored in DB)
PrometheusLabels []string `gorm:"-" json:"prometheus_labels"`
AllowedOrigins []string `gorm:"-" json:"allowed_origins,omitempty"`
AllowedHeaders []string `gorm:"-" json:"allowed_headers,omitempty"`
RequiredHeaders []string `gorm:"-" json:"required_headers,omitempty"`
LoggingHeaders []string `gorm:"-" json:"logging_headers,omitempty"`
WhitelistedRoutes []string `gorm:"-" json:"whitelisted_routes,omitempty"`
HeaderFilterConfig *GlobalHeaderFilterConfig `gorm:"-" json:"header_filter_config,omitempty"`
}
// TableName sets the table name for each model
func (TableClientConfig) TableName() string { return "config_client" }
func (cc *TableClientConfig) BeforeSave(tx *gorm.DB) error {
if cc.PrometheusLabels != nil {
data, err := json.Marshal(cc.PrometheusLabels)
if err != nil {
return err
}
cc.PrometheusLabelsJSON = string(data)
} else {
cc.PrometheusLabelsJSON = "[]"
}
if cc.AllowedOrigins != nil {
data, err := json.Marshal(cc.AllowedOrigins)
if err != nil {
return err
}
cc.AllowedOriginsJSON = string(data)
} else {
cc.AllowedOriginsJSON = "[]"
}
if cc.AllowedHeaders != nil {
data, err := json.Marshal(cc.AllowedHeaders)
if err != nil {
return err
}
cc.AllowedHeadersJSON = string(data)
} else {
cc.AllowedHeadersJSON = "[]"
}
if cc.WhitelistedRoutes != nil {
data, err := json.Marshal(cc.WhitelistedRoutes)
if err != nil {
return err
}
cc.WhitelistedRoutesJSON = string(data)
} else {
cc.WhitelistedRoutesJSON = "[]"
}
if cc.RequiredHeaders != nil {
data, err := json.Marshal(cc.RequiredHeaders)
if err != nil {
return err
}
cc.RequiredHeadersJSON = string(data)
} else {
cc.RequiredHeadersJSON = "[]"
}
if cc.LoggingHeaders != nil {
data, err := json.Marshal(cc.LoggingHeaders)
if err != nil {
return err
}
cc.LoggingHeadersJSON = string(data)
} else {
cc.LoggingHeadersJSON = "[]"
}
if cc.HeaderFilterConfig != nil {
data, err := json.Marshal(cc.HeaderFilterConfig)
if err != nil {
return err
}
cc.HeaderFilterConfigJSON = string(data)
} else {
cc.HeaderFilterConfigJSON = ""
}
return nil
}
// AfterFind hooks for deserialization
func (cc *TableClientConfig) AfterFind(tx *gorm.DB) error {
if cc.PrometheusLabelsJSON != "" {
if err := json.Unmarshal([]byte(cc.PrometheusLabelsJSON), &cc.PrometheusLabels); err != nil {
return err
}
}
if cc.AllowedOriginsJSON != "" {
if err := json.Unmarshal([]byte(cc.AllowedOriginsJSON), &cc.AllowedOrigins); err != nil {
return err
}
}
if cc.AllowedHeadersJSON != "" {
if err := json.Unmarshal([]byte(cc.AllowedHeadersJSON), &cc.AllowedHeaders); err != nil {
return err
}
}
if cc.WhitelistedRoutesJSON != "" {
if err := json.Unmarshal([]byte(cc.WhitelistedRoutesJSON), &cc.WhitelistedRoutes); err != nil {
return err
}
}
if cc.RequiredHeadersJSON != "" {
if err := json.Unmarshal([]byte(cc.RequiredHeadersJSON), &cc.RequiredHeaders); err != nil {
return err
}
}
if cc.LoggingHeadersJSON != "" {
if err := json.Unmarshal([]byte(cc.LoggingHeadersJSON), &cc.LoggingHeaders); err != nil {
return err
}
}
if cc.HeaderFilterConfigJSON != "" {
var headerFilterConfig GlobalHeaderFilterConfig
if err := json.Unmarshal([]byte(cc.HeaderFilterConfigJSON), &headerFilterConfig); err != nil {
return err
}
cc.HeaderFilterConfig = &headerFilterConfig
}
return nil
}

View File

@@ -0,0 +1,56 @@
package tables
import "github.com/maximhq/bifrost/core/network"
const (
ConfigAdminUsernameKey = "admin_username"
ConfigAdminPasswordKey = "admin_password"
ConfigIsAuthEnabledKey = "is_auth_enabled"
ConfigDisableAuthOnInferenceKey = "disable_auth_on_inference"
ConfigProxyKey = "proxy_config"
ConfigRestartRequiredKey = "restart_required"
ConfigHeaderFilterKey = "header_filter_config"
)
// RestartRequiredConfig represents the restart required configuration
// This is set when a config change requires a server restart to take effect
type RestartRequiredConfig struct {
Required bool `json:"required"`
Reason string `json:"reason,omitempty"`
}
// GlobalProxyConfig represents the global proxy configuration
type GlobalProxyConfig struct {
Enabled bool `json:"enabled"`
Type network.GlobalProxyType `json:"type"` // "http", "socks5", "tcp"
URL string `json:"url"` // Proxy URL (e.g., http://proxy.example.com:8080)
Username string `json:"username,omitempty"` // Optional authentication username
Password string `json:"password,omitempty"` // Optional authentication password
NoProxy string `json:"no_proxy,omitempty"` // Comma-separated list of hosts to bypass proxy
Timeout int `json:"timeout,omitempty"` // Connection timeout in seconds
SkipTLSVerify bool `json:"skip_tls_verify,omitempty"` // Skip TLS certificate verification
// Entity enablement flags
EnableForSCIM bool `json:"enable_for_scim"` // Enable proxy for SCIM requests (enterprise only)
EnableForInference bool `json:"enable_for_inference"` // Enable proxy for inference requests
EnableForAPI bool `json:"enable_for_api"` // Enable proxy for API requests
}
// GlobalHeaderFilterConfig represents global header filtering configuration
// for headers forwarded to LLM providers via the x-bf-eh-* prefix.
// Filter logic:
// - If allowlist is non-empty, only headers in the allowlist are forwarded
// - If denylist is non-empty, headers in the denylist are dropped
// - If both are non-empty, allowlist takes precedence first, then denylist filters the result
type GlobalHeaderFilterConfig struct {
Allowlist []string `json:"allowlist,omitempty"` // If non-empty, only these headers are allowed
Denylist []string `json:"denylist,omitempty"` // Headers to always block
}
// TableGovernanceConfig represents generic configuration key-value pairs
type TableGovernanceConfig struct {
Key string `gorm:"primaryKey;type:varchar(255)" json:"key"`
Value string `gorm:"type:text" json:"value"`
}
// TableName sets the table name for each model
func (TableGovernanceConfig) TableName() string { return "governance_config" }

View File

@@ -0,0 +1,15 @@
// Package tables contains the database tables for the configstore.
package tables
import "time"
// TableConfigHash represents the configuration hash in the database
type TableConfigHash struct {
ID uint `gorm:"primaryKey;autoIncrement" json:"id"`
Hash string `gorm:"type:varchar(255);uniqueIndex;not null" json:"hash"`
CreatedAt time.Time `gorm:"index;not null" json:"created_at"`
UpdatedAt time.Time `gorm:"index;not null" json:"updated_at"`
}
// TableName sets the table name for each model
func (TableConfigHash) TableName() string { return "config_hashes" }

View File

@@ -0,0 +1,27 @@
package tables
import "time"
// TableCustomer represents a customer entity with budget and rate limit
type TableCustomer struct {
ID string `gorm:"primaryKey;type:varchar(255)" json:"id"`
Name string `gorm:"type:varchar(255);not null" json:"name"`
BudgetID *string `gorm:"type:varchar(255);index" json:"budget_id,omitempty"`
RateLimitID *string `gorm:"type:varchar(255);index" json:"rate_limit_id,omitempty"`
// Relationships
Budget *TableBudget `gorm:"foreignKey:BudgetID" json:"budget,omitempty"`
RateLimit *TableRateLimit `gorm:"foreignKey:RateLimitID" json:"rate_limit,omitempty"`
Teams []TableTeam `gorm:"foreignKey:CustomerID" json:"teams"`
VirtualKeys []TableVirtualKey `gorm:"foreignKey:CustomerID" json:"virtual_keys"`
// Config hash is used to detect the changes synced from config.json file
// Every time we sync the config.json file, we will update the config hash
ConfigHash string `gorm:"type:varchar(255);null" json:"config_hash"`
CreatedAt time.Time `gorm:"index;not null" json:"created_at"`
UpdatedAt time.Time `gorm:"index;not null" json:"updated_at"`
}
// TableName sets the table name for each model
func (TableCustomer) TableName() string { return "governance_customers" }

View File

@@ -0,0 +1,17 @@
package tables
import "time"
// TableDistributedLock represents a distributed lock entry in the database.
// This table is used to implement distributed locking across multiple instances.
type TableDistributedLock struct {
LockKey string `gorm:"primaryKey;column:lock_key;size:255" json:"lock_key"`
HolderID string `gorm:"column:holder_id;size:255;not null" json:"holder_id"`
ExpiresAt time.Time `gorm:"column:expires_at;not null;index" json:"expires_at"`
CreatedAt time.Time `gorm:"column:created_at;autoCreateTime" json:"created_at"`
}
// TableName returns the table name for the distributed lock table.
func (TableDistributedLock) TableName() string {
return "distributed_locks"
}

View File

@@ -0,0 +1,87 @@
package tables
import (
"github.com/maximhq/bifrost/core/schemas"
"github.com/maximhq/bifrost/framework/encrypt"
)
const (
// EncryptionStatusPlainText indicates the row's sensitive fields are stored as plaintext.
EncryptionStatusPlainText = "plain_text"
// EncryptionStatusEncrypted indicates the row's sensitive fields have been encrypted.
EncryptionStatusEncrypted = "encrypted"
)
// encryptEnvVar encrypts the Val field of an EnvVar in place using AES-256-GCM.
// It is a no-op if the field is nil, references an environment variable, or has an empty value.
func encryptEnvVar(field *schemas.EnvVar) error {
if field == nil || field.IsFromEnv() || field.GetValue() == "" {
return nil
}
encrypted, err := encrypt.Encrypt(field.Val)
if err != nil {
return err
}
field.Val = encrypted
return nil
}
// decryptEnvVar decrypts the Val field of an EnvVar in place using AES-256-GCM.
// It is a no-op if the field is nil, references an environment variable, or has an empty value.
func decryptEnvVar(field *schemas.EnvVar) error {
if field == nil || field.IsFromEnv() || field.GetValue() == "" {
return nil
}
decrypted, err := encrypt.Decrypt(field.Val)
if err != nil {
return err
}
field.Val = decrypted
return nil
}
// encryptEnvVarPtr encrypts the Val field of a pointer-to-EnvVar in place.
// It is a no-op if the pointer or the EnvVar it points to is nil.
func encryptEnvVarPtr(field **schemas.EnvVar) error {
if field == nil || *field == nil {
return nil
}
return encryptEnvVar(*field)
}
// decryptEnvVarPtr decrypts the Val field of a pointer-to-EnvVar in place.
// It is a no-op if the pointer or the EnvVar it points to is nil.
func decryptEnvVarPtr(field **schemas.EnvVar) error {
if field == nil || *field == nil {
return nil
}
return decryptEnvVar(*field)
}
// encryptString encrypts the string pointed to by value in place using AES-256-GCM.
// It is a no-op if the pointer is nil or the string is empty.
func encryptString(value *string) error {
if value == nil || *value == "" {
return nil
}
encrypted, err := encrypt.Encrypt(*value)
if err != nil {
return err
}
*value = encrypted
return nil
}
// decryptString decrypts the string pointed to by value in place using AES-256-GCM.
// It is a no-op if the pointer is nil or the string is empty.
func decryptString(value *string) error {
if value == nil || *value == "" {
return nil
}
decrypted, err := encrypt.Decrypt(*value)
if err != nil {
return err
}
*value = decrypted
return nil
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,17 @@
package tables
import "time"
// TableEnvKey represents environment variable tracking in the database
type TableEnvKey struct {
ID uint `gorm:"primaryKey;autoIncrement" json:"id"`
EnvVar string `gorm:"type:varchar(255);index;not null" json:"env_var"`
Provider string `gorm:"type:varchar(50);index" json:"provider"` // Empty for MCP/client configs
KeyType string `gorm:"type:varchar(50);not null" json:"key_type"` // "api_key", "azure_config", "vertex_config", "bedrock_config", "connection_string"
ConfigPath string `gorm:"type:varchar(500);not null" json:"config_path"` // Descriptive path of where this env var is used
KeyID string `gorm:"type:varchar(255);index" json:"key_id"` // Key UUID (empty for non-key configs)
CreatedAt time.Time `gorm:"index;not null" json:"created_at"`
}
// TableName sets the table name for each model
func (TableEnvKey) TableName() string { return "config_env_keys" }

View File

@@ -0,0 +1,22 @@
// Package tables provides tables for the configstore
package tables
import (
"time"
)
// TableFolder represents a generic folder that can contain prompts
type TableFolder struct {
ID string `gorm:"type:varchar(36);primaryKey" json:"id"`
Name string `gorm:"type:varchar(255);not null" json:"name"`
Description *string `gorm:"type:text" json:"description,omitempty"`
CreatedAt time.Time `gorm:"not null" json:"created_at"`
UpdatedAt time.Time `gorm:"not null" json:"updated_at"`
ConfigHash string `gorm:"type:varchar(64)" json:"-"`
// Virtual fields (not stored in DB)
PromptsCount int `gorm:"-" json:"prompts_count,omitempty"`
}
// TableName for TableFolder
func (TableFolder) TableName() string { return "folders" }

View File

@@ -0,0 +1,12 @@
package tables
// TableFrameworkConfig represents the framework configurations
// We will keep on adding different columns here as we add new features to the framework
type TableFrameworkConfig struct {
ID uint `gorm:"primaryKey;autoIncrement" json:"id"`
PricingURL *string `gorm:"type:text" json:"pricing_url"`
PricingSyncInterval *int64 `gorm:"" json:"pricing_sync_interval"`
}
// TableName sets the table name for each model
func (TableFrameworkConfig) TableName() string { return "framework_configs" }

View File

@@ -0,0 +1,644 @@
package tables
import (
"encoding/json"
"fmt"
"time"
"github.com/bytedance/sonic"
"github.com/maximhq/bifrost/core/schemas"
"github.com/maximhq/bifrost/framework/encrypt"
"gorm.io/gorm"
)
// TableKey represents an API key configuration in the database
type TableKey struct {
ID uint `gorm:"primaryKey;autoIncrement" json:"id"`
Name string `gorm:"type:varchar(255);uniqueIndex:idx_key_name;not null" json:"name"`
ProviderID uint `gorm:"index;not null" json:"provider_id"`
Provider string `gorm:"index;type:varchar(50)" json:"provider"` // ModelProvider as string
KeyID string `gorm:"type:varchar(255);uniqueIndex:idx_key_id;not null" json:"key_id"` // UUID from schemas.Key
Value schemas.EnvVar `gorm:"type:text;not null" json:"value"`
ModelsJSON string `gorm:"type:text" json:"-"` // JSON serialized []string
BlacklistedModelsJSON string `gorm:"type:text" json:"-"` // JSON serialized []string
Weight *float64 `json:"weight"`
Enabled *bool `gorm:"default:true" json:"enabled,omitempty"`
CreatedAt time.Time `gorm:"index;not null" json:"created_at"`
UpdatedAt time.Time `gorm:"index;not null" json:"updated_at"`
// Config hash is used to detect changes synced from config.json file
ConfigHash string `gorm:"type:varchar(255);null" json:"config_hash"`
// Unified aliases
AliasesJSON *string `gorm:"type:text" json:"-"` // JSON serialized schemas.KeyAliases
// Azure config fields (embedded instead of separate table for simplicity)
AzureEndpoint *schemas.EnvVar `gorm:"type:text" json:"azure_endpoint,omitempty"`
AzureAPIVersion *schemas.EnvVar `gorm:"type:text" json:"azure_api_version,omitempty"`
AzureClientID *schemas.EnvVar `gorm:"type:text" json:"azure_client_id,omitempty"`
AzureClientSecret *schemas.EnvVar `gorm:"type:text" json:"azure_client_secret,omitempty"`
AzureTenantID *schemas.EnvVar `gorm:"type:text" json:"azure_tenant_id,omitempty"`
AzureScopesJSON *string `gorm:"column:azure_scopes;type:text" json:"-"` // JSON serialized []string
// Vertex config fields (embedded)
VertexProjectID *schemas.EnvVar `gorm:"type:text" json:"vertex_project_id,omitempty"`
VertexProjectNumber *schemas.EnvVar `gorm:"type:text" json:"vertex_project_number,omitempty"`
VertexRegion *schemas.EnvVar `gorm:"type:text" json:"vertex_region,omitempty"`
VertexAuthCredentials *schemas.EnvVar `gorm:"type:text" json:"vertex_auth_credentials,omitempty"`
// Bedrock config fields (embedded)
BedrockAccessKey *schemas.EnvVar `gorm:"type:text" json:"bedrock_access_key,omitempty"`
BedrockSecretKey *schemas.EnvVar `gorm:"type:text" json:"bedrock_secret_key,omitempty"`
BedrockSessionToken *schemas.EnvVar `gorm:"type:text" json:"bedrock_session_token,omitempty"`
BedrockRegion *schemas.EnvVar `gorm:"type:text" json:"bedrock_region,omitempty"`
BedrockARN *schemas.EnvVar `gorm:"type:text" json:"bedrock_arn,omitempty"`
BedrockRoleARN *schemas.EnvVar `gorm:"type:text" json:"bedrock_role_arn,omitempty"`
BedrockExternalID *schemas.EnvVar `gorm:"type:text" json:"bedrock_external_id,omitempty"`
BedrockRoleSessionName *schemas.EnvVar `gorm:"type:text" json:"bedrock_role_session_name,omitempty"`
BedrockBatchS3ConfigJSON *string `gorm:"type:text" json:"-"` // JSON serialized schemas.BatchS3Config
// VLLM config fields (embedded)
VLLMUrl *schemas.EnvVar `gorm:"type:text" json:"vllm_url,omitempty"`
VLLMModelName *string `gorm:"type:varchar(255)" json:"vllm_model_name,omitempty"`
// Replicate config fields (embedded)
ReplicateUseDeploymentsEndpoint *bool `gorm:"column:replicate_use_deployments_endpoint" json:"replicate_use_deployments_endpoint,omitempty"`
// Ollama config fields (embedded)
OllamaUrl *schemas.EnvVar `gorm:"type:text" json:"ollama_url,omitempty"`
// SGL config fields (embedded)
SGLUrl *schemas.EnvVar `gorm:"type:text" json:"sgl_url,omitempty"`
// Batch API configuration
UseForBatchAPI *bool `gorm:"default:false" json:"use_for_batch_api,omitempty"` // Whether this key can be used for batch API operations
Status string `gorm:"type:varchar(50);default:'unknown'" json:"status"`
Description string `gorm:"type:text" json:"description,omitempty"`
EncryptionStatus string `gorm:"type:varchar(20);default:'plain_text'" json:"-"`
// Virtual fields for runtime use (not stored in DB)
Models schemas.WhiteList `gorm:"-" json:"models"` // ["*"] allows all models; empty denies all (deny-by-default)
BlacklistedModels schemas.BlackList `gorm:"-" json:"blacklisted_models"`
Aliases schemas.KeyAliases `gorm:"-" json:"aliases,omitempty"`
AzureKeyConfig *schemas.AzureKeyConfig `gorm:"-" json:"azure_key_config,omitempty"`
VertexKeyConfig *schemas.VertexKeyConfig `gorm:"-" json:"vertex_key_config,omitempty"`
BedrockKeyConfig *schemas.BedrockKeyConfig `gorm:"-" json:"bedrock_key_config,omitempty"`
VLLMKeyConfig *schemas.VLLMKeyConfig `gorm:"-" json:"vllm_key_config,omitempty"`
ReplicateKeyConfig *schemas.ReplicateKeyConfig `gorm:"-" json:"replicate_key_config,omitempty"`
OllamaKeyConfig *schemas.OllamaKeyConfig `gorm:"-" json:"ollama_key_config,omitempty"`
SGLKeyConfig *schemas.SGLKeyConfig `gorm:"-" json:"sgl_key_config,omitempty"`
}
// TableName sets the table name for each model
func (TableKey) TableName() string { return "config_keys" }
// BeforeSave is a GORM hook that serializes runtime config structs into JSON columns and
// encrypts sensitive fields (API key value, Azure endpoint/client ID/secret/tenant ID/API version,
// Vertex project ID/project number/region/credentials, Bedrock keys/region/ARN/deployments/
// batch S3 config) before writing to the database. Encryption runs last to ensure it
// operates on the final serialized values.
func (k *TableKey) BeforeSave(tx *gorm.DB) error {
if err := k.Models.Validate(); err != nil {
return err
}
data, err := json.Marshal(k.Models)
if err != nil {
return err
}
k.ModelsJSON = string(data)
if err := k.BlacklistedModels.Validate(); err != nil {
return err
}
data, err = json.Marshal(k.BlacklistedModels)
if err != nil {
return err
}
k.BlacklistedModelsJSON = string(data)
if k.Enabled == nil {
enabled := true // DB default
k.Enabled = &enabled
}
if k.UseForBatchAPI == nil {
useForBatchAPI := false // DB default
k.UseForBatchAPI = &useForBatchAPI
}
// IMPORTANT: All *EnvVar fields assigned from provider config structs (AzureKeyConfig,
// VertexKeyConfig, BedrockKeyConfig) MUST be value-copied before assignment. The caller
// may retain the config struct pointer; if BeforeSave (or future encryption) mutates a
// shared pointer, the caller's in-memory config is silently corrupted.
// See: TestBeforeSave_DoesNotMutateSharedProviderConfigs
if k.AzureKeyConfig != nil {
if k.AzureKeyConfig.Endpoint.IsSet() {
ep := k.AzureKeyConfig.Endpoint
k.AzureEndpoint = &ep
} else {
k.AzureEndpoint = nil
}
if k.AzureKeyConfig.APIVersion != nil {
av := *k.AzureKeyConfig.APIVersion
k.AzureAPIVersion = &av
} else {
k.AzureAPIVersion = nil
}
if k.AzureKeyConfig.ClientID != nil {
cid := *k.AzureKeyConfig.ClientID
k.AzureClientID = &cid
} else {
k.AzureClientID = nil
}
if k.AzureKeyConfig.ClientSecret != nil {
cs := *k.AzureKeyConfig.ClientSecret
k.AzureClientSecret = &cs
} else {
k.AzureClientSecret = nil
}
if k.AzureKeyConfig.TenantID != nil {
tid := *k.AzureKeyConfig.TenantID
k.AzureTenantID = &tid
} else {
k.AzureTenantID = nil
}
if len(k.AzureKeyConfig.Scopes) > 0 {
data, err := json.Marshal(k.AzureKeyConfig.Scopes)
if err != nil {
return err
}
s := string(data)
k.AzureScopesJSON = &s
} else {
k.AzureScopesJSON = nil
}
} else {
k.AzureEndpoint = nil
k.AzureAPIVersion = nil
k.AzureClientID = nil
k.AzureClientSecret = nil
k.AzureTenantID = nil
k.AzureScopesJSON = nil
}
if k.VertexKeyConfig != nil {
if k.VertexKeyConfig.ProjectID.IsSet() {
pid := k.VertexKeyConfig.ProjectID
k.VertexProjectID = &pid
} else {
k.VertexProjectID = nil
}
if k.VertexKeyConfig.ProjectNumber.IsSet() {
pn := k.VertexKeyConfig.ProjectNumber
k.VertexProjectNumber = &pn
} else {
k.VertexProjectNumber = nil
}
if k.VertexKeyConfig.Region.IsSet() {
vr := k.VertexKeyConfig.Region
k.VertexRegion = &vr
} else {
k.VertexRegion = nil
}
if k.VertexKeyConfig.AuthCredentials.IsSet() {
ac := k.VertexKeyConfig.AuthCredentials
k.VertexAuthCredentials = &ac
} else {
k.VertexAuthCredentials = nil
}
} else {
k.VertexProjectID = nil
k.VertexProjectNumber = nil
k.VertexRegion = nil
k.VertexAuthCredentials = nil
}
if k.BedrockKeyConfig != nil {
if k.BedrockKeyConfig.AccessKey.IsSet() {
// Copy to avoid encrypting the shared BedrockKeyConfig through the pointer
ak := k.BedrockKeyConfig.AccessKey
k.BedrockAccessKey = &ak
} else {
k.BedrockAccessKey = nil
}
if k.BedrockKeyConfig.SecretKey.IsSet() {
// Copy to avoid encrypting the shared BedrockKeyConfig through the pointer
sk := k.BedrockKeyConfig.SecretKey
k.BedrockSecretKey = &sk
} else {
k.BedrockSecretKey = nil
}
// Copy to avoid encrypting the shared BedrockKeyConfig through the pointer
if k.BedrockKeyConfig.SessionToken != nil {
st := *k.BedrockKeyConfig.SessionToken
k.BedrockSessionToken = &st
} else {
k.BedrockSessionToken = nil
}
if k.BedrockKeyConfig.Region != nil {
br := *k.BedrockKeyConfig.Region
k.BedrockRegion = &br
} else {
k.BedrockRegion = nil
}
if k.BedrockKeyConfig.ARN != nil {
ba := *k.BedrockKeyConfig.ARN
k.BedrockARN = &ba
} else {
k.BedrockARN = nil
}
if k.BedrockKeyConfig.RoleARN != nil {
bra := *k.BedrockKeyConfig.RoleARN
k.BedrockRoleARN = &bra
} else {
k.BedrockRoleARN = nil
}
if k.BedrockKeyConfig.ExternalID != nil {
ei := *k.BedrockKeyConfig.ExternalID
k.BedrockExternalID = &ei
} else {
k.BedrockExternalID = nil
}
if k.BedrockKeyConfig.RoleSessionName != nil {
rsn := *k.BedrockKeyConfig.RoleSessionName
k.BedrockRoleSessionName = &rsn
} else {
k.BedrockRoleSessionName = nil
}
if k.BedrockKeyConfig.BatchS3Config != nil {
data, err := sonic.Marshal(k.BedrockKeyConfig.BatchS3Config)
if err != nil {
return err
}
s := string(data)
k.BedrockBatchS3ConfigJSON = &s
} else {
k.BedrockBatchS3ConfigJSON = nil
}
} else {
k.BedrockAccessKey = nil
k.BedrockSecretKey = nil
k.BedrockSessionToken = nil
k.BedrockRegion = nil
k.BedrockARN = nil
k.BedrockRoleARN = nil
k.BedrockExternalID = nil
k.BedrockRoleSessionName = nil
k.BedrockBatchS3ConfigJSON = nil
}
if k.Aliases != nil {
if err := k.Aliases.Validate(); err != nil {
return err
}
data, err := sonic.Marshal(k.Aliases)
if err != nil {
return err
}
s := string(data)
k.AliasesJSON = &s
} else {
k.AliasesJSON = nil
}
if k.VLLMKeyConfig != nil {
if k.VLLMKeyConfig.URL.IsSet() {
u := k.VLLMKeyConfig.URL // Value-copy to prevent shared pointer mutation
k.VLLMUrl = &u
} else {
k.VLLMUrl = nil
}
if k.VLLMKeyConfig.ModelName != "" {
mn := k.VLLMKeyConfig.ModelName
k.VLLMModelName = &mn
} else {
k.VLLMModelName = nil
}
} else {
k.VLLMUrl = nil
k.VLLMModelName = nil
}
if k.ReplicateKeyConfig != nil {
v := k.ReplicateKeyConfig.UseDeploymentsEndpoint
k.ReplicateUseDeploymentsEndpoint = &v
} else {
k.ReplicateUseDeploymentsEndpoint = nil
}
if k.OllamaKeyConfig != nil && k.OllamaKeyConfig.URL.IsSet() {
u := k.OllamaKeyConfig.URL
k.OllamaUrl = &u
} else {
k.OllamaUrl = nil
}
if k.SGLKeyConfig != nil && k.SGLKeyConfig.URL.IsSet() {
u := k.SGLKeyConfig.URL
k.SGLUrl = &u
} else {
k.SGLUrl = nil
}
// Encrypt sensitive fields after serialization
if encrypt.IsEnabled() {
if err := encryptEnvVar(&k.Value); err != nil {
return fmt.Errorf("failed to encrypt key value: %w", err)
}
// Azure
if err := encryptEnvVarPtr(&k.AzureEndpoint); err != nil {
return fmt.Errorf("failed to encrypt azure endpoint: %w", err)
}
if err := encryptEnvVarPtr(&k.AzureClientID); err != nil {
return fmt.Errorf("failed to encrypt azure client id: %w", err)
}
if err := encryptEnvVarPtr(&k.AzureClientSecret); err != nil {
return fmt.Errorf("failed to encrypt azure client secret: %w", err)
}
if err := encryptEnvVarPtr(&k.AzureTenantID); err != nil {
return fmt.Errorf("failed to encrypt azure tenant id: %w", err)
}
if err := encryptEnvVarPtr(&k.AzureAPIVersion); err != nil {
return fmt.Errorf("failed to encrypt azure api version: %w", err)
}
// Vertex
if err := encryptEnvVarPtr(&k.VertexProjectID); err != nil {
return fmt.Errorf("failed to encrypt vertex project id: %w", err)
}
if err := encryptEnvVarPtr(&k.VertexProjectNumber); err != nil {
return fmt.Errorf("failed to encrypt vertex project number: %w", err)
}
if err := encryptEnvVarPtr(&k.VertexRegion); err != nil {
return fmt.Errorf("failed to encrypt vertex region: %w", err)
}
if err := encryptEnvVarPtr(&k.VertexAuthCredentials); err != nil {
return fmt.Errorf("failed to encrypt vertex auth credentials: %w", err)
}
// Bedrock
if err := encryptEnvVarPtr(&k.BedrockAccessKey); err != nil {
return fmt.Errorf("failed to encrypt bedrock access key: %w", err)
}
if err := encryptEnvVarPtr(&k.BedrockSecretKey); err != nil {
return fmt.Errorf("failed to encrypt bedrock secret key: %w", err)
}
if err := encryptEnvVarPtr(&k.BedrockSessionToken); err != nil {
return fmt.Errorf("failed to encrypt bedrock session token: %w", err)
}
if err := encryptEnvVarPtr(&k.BedrockRegion); err != nil {
return fmt.Errorf("failed to encrypt bedrock region: %w", err)
}
if err := encryptEnvVarPtr(&k.BedrockARN); err != nil {
return fmt.Errorf("failed to encrypt bedrock arn: %w", err)
}
if err := encryptEnvVarPtr(&k.BedrockRoleARN); err != nil {
return fmt.Errorf("failed to encrypt bedrock role arn: %w", err)
}
if err := encryptEnvVarPtr(&k.BedrockExternalID); err != nil {
return fmt.Errorf("failed to encrypt bedrock external id: %w", err)
}
if err := encryptEnvVarPtr(&k.BedrockRoleSessionName); err != nil {
return fmt.Errorf("failed to encrypt bedrock role session name: %w", err)
}
if err := encryptString(k.BedrockBatchS3ConfigJSON); err != nil {
return fmt.Errorf("failed to encrypt bedrock batch s3 config: %w", err)
}
// Aliases
if err := encryptString(k.AliasesJSON); err != nil {
return fmt.Errorf("failed to encrypt aliases: %w", err)
}
// VLLM
if err := encryptEnvVarPtr(&k.VLLMUrl); err != nil {
return fmt.Errorf("failed to encrypt vllm url: %w", err)
}
// Ollama
if err := encryptEnvVarPtr(&k.OllamaUrl); err != nil {
return fmt.Errorf("failed to encrypt ollama url: %w", err)
}
// SGL
if err := encryptEnvVarPtr(&k.SGLUrl); err != nil {
return fmt.Errorf("failed to encrypt sgl url: %w", err)
}
k.EncryptionStatus = EncryptionStatusEncrypted
}
return nil
}
// AfterFind is a GORM hook that decrypts sensitive fields and reconstructs runtime config
// structs after reading from the database. Decryption runs first so that value copies into
// AzureKeyConfig, VertexKeyConfig, etc. receive plaintext data.
func (k *TableKey) AfterFind(tx *gorm.DB) error {
// Decrypt sensitive fields before deserialization/reconstruction
if k.EncryptionStatus == EncryptionStatusEncrypted {
if err := decryptEnvVar(&k.Value); err != nil {
return fmt.Errorf("failed to decrypt key value: %w", err)
}
// Azure
if err := decryptEnvVarPtr(&k.AzureEndpoint); err != nil {
return fmt.Errorf("failed to decrypt azure endpoint: %w", err)
}
if err := decryptEnvVarPtr(&k.AzureClientID); err != nil {
return fmt.Errorf("failed to decrypt azure client id: %w", err)
}
if err := decryptEnvVarPtr(&k.AzureClientSecret); err != nil {
return fmt.Errorf("failed to decrypt azure client secret: %w", err)
}
if err := decryptEnvVarPtr(&k.AzureTenantID); err != nil {
return fmt.Errorf("failed to decrypt azure tenant id: %w", err)
}
if err := decryptEnvVarPtr(&k.AzureAPIVersion); err != nil {
return fmt.Errorf("failed to decrypt azure api version: %w", err)
}
// Vertex
if err := decryptEnvVarPtr(&k.VertexProjectID); err != nil {
return fmt.Errorf("failed to decrypt vertex project id: %w", err)
}
if err := decryptEnvVarPtr(&k.VertexProjectNumber); err != nil {
return fmt.Errorf("failed to decrypt vertex project number: %w", err)
}
if err := decryptEnvVarPtr(&k.VertexRegion); err != nil {
return fmt.Errorf("failed to decrypt vertex region: %w", err)
}
if err := decryptEnvVarPtr(&k.VertexAuthCredentials); err != nil {
return fmt.Errorf("failed to decrypt vertex auth credentials: %w", err)
}
// Bedrock
if err := decryptEnvVarPtr(&k.BedrockAccessKey); err != nil {
return fmt.Errorf("failed to decrypt bedrock access key: %w", err)
}
if err := decryptEnvVarPtr(&k.BedrockSecretKey); err != nil {
return fmt.Errorf("failed to decrypt bedrock secret key: %w", err)
}
if err := decryptEnvVarPtr(&k.BedrockSessionToken); err != nil {
return fmt.Errorf("failed to decrypt bedrock session token: %w", err)
}
if err := decryptEnvVarPtr(&k.BedrockRegion); err != nil {
return fmt.Errorf("failed to decrypt bedrock region: %w", err)
}
if err := decryptEnvVarPtr(&k.BedrockARN); err != nil {
return fmt.Errorf("failed to decrypt bedrock arn: %w", err)
}
if err := decryptEnvVarPtr(&k.BedrockRoleARN); err != nil {
return fmt.Errorf("failed to decrypt bedrock role arn: %w", err)
}
if err := decryptEnvVarPtr(&k.BedrockExternalID); err != nil {
return fmt.Errorf("failed to decrypt bedrock external id: %w", err)
}
if err := decryptEnvVarPtr(&k.BedrockRoleSessionName); err != nil {
return fmt.Errorf("failed to decrypt bedrock role session name: %w", err)
}
if err := decryptString(k.BedrockBatchS3ConfigJSON); err != nil {
return fmt.Errorf("failed to decrypt bedrock batch s3 config: %w", err)
}
// Aliases
if err := decryptString(k.AliasesJSON); err != nil {
return fmt.Errorf("failed to decrypt aliases: %w", err)
}
// VLLM
if err := decryptEnvVarPtr(&k.VLLMUrl); err != nil {
return fmt.Errorf("failed to decrypt vllm url: %w", err)
}
// Ollama
if err := decryptEnvVarPtr(&k.OllamaUrl); err != nil {
return fmt.Errorf("failed to decrypt ollama url: %w", err)
}
// SGL
if err := decryptEnvVarPtr(&k.SGLUrl); err != nil {
return fmt.Errorf("failed to decrypt sgl url: %w", err)
}
}
if k.ModelsJSON != "" {
if err := json.Unmarshal([]byte(k.ModelsJSON), &k.Models); err != nil {
return err
}
}
if k.BlacklistedModelsJSON != "" {
if err := json.Unmarshal([]byte(k.BlacklistedModelsJSON), &k.BlacklistedModels); err != nil {
return err
}
}
if k.Enabled == nil {
enabled := true // DB default
k.Enabled = &enabled
}
if k.UseForBatchAPI == nil {
useForBatchAPI := false // DB default
k.UseForBatchAPI = &useForBatchAPI
}
// Reconstruct Azure config if fields are present
if k.AzureEndpoint != nil || k.AzureAPIVersion != nil || k.AzureClientID != nil || k.AzureClientSecret != nil || k.AzureTenantID != nil || (k.AzureScopesJSON != nil && *k.AzureScopesJSON != "") {
var scopes []string
if k.AzureScopesJSON != nil && *k.AzureScopesJSON != "" {
if err := json.Unmarshal([]byte(*k.AzureScopesJSON), &scopes); err != nil {
return err
}
}
azureConfig := &schemas.AzureKeyConfig{
Endpoint: *schemas.NewEnvVar(""),
APIVersion: k.AzureAPIVersion,
ClientID: k.AzureClientID,
ClientSecret: k.AzureClientSecret,
TenantID: k.AzureTenantID,
Scopes: scopes,
}
if k.AzureEndpoint != nil {
azureConfig.Endpoint = *k.AzureEndpoint
}
k.AzureKeyConfig = azureConfig
}
// Reconstruct Vertex config if fields are present
if k.VertexProjectID != nil || k.VertexProjectNumber != nil || k.VertexRegion != nil || k.VertexAuthCredentials != nil {
config := &schemas.VertexKeyConfig{}
if k.VertexProjectID != nil {
config.ProjectID = *k.VertexProjectID
}
if k.VertexProjectNumber != nil {
config.ProjectNumber = *k.VertexProjectNumber
}
if k.VertexRegion != nil {
config.Region = *k.VertexRegion
}
if k.VertexAuthCredentials != nil {
config.AuthCredentials = *k.VertexAuthCredentials
}
k.VertexKeyConfig = config
}
// Reconstruct Bedrock config if fields are present
if k.BedrockAccessKey != nil || k.BedrockSecretKey != nil || k.BedrockSessionToken != nil || k.BedrockRegion != nil || k.BedrockARN != nil || k.BedrockRoleARN != nil || k.BedrockExternalID != nil || k.BedrockRoleSessionName != nil || (k.BedrockBatchS3ConfigJSON != nil && *k.BedrockBatchS3ConfigJSON != "") {
bedrockConfig := &schemas.BedrockKeyConfig{}
if k.BedrockAccessKey != nil {
bedrockConfig.AccessKey = *k.BedrockAccessKey
}
bedrockConfig.SessionToken = k.BedrockSessionToken
bedrockConfig.Region = k.BedrockRegion
bedrockConfig.ARN = k.BedrockARN
bedrockConfig.RoleARN = k.BedrockRoleARN
bedrockConfig.ExternalID = k.BedrockExternalID
bedrockConfig.RoleSessionName = k.BedrockRoleSessionName
if k.BedrockSecretKey != nil {
bedrockConfig.SecretKey = *k.BedrockSecretKey
}
if k.BedrockBatchS3ConfigJSON != nil && *k.BedrockBatchS3ConfigJSON != "" {
var batchS3Config schemas.BatchS3Config
if err := json.Unmarshal([]byte(*k.BedrockBatchS3ConfigJSON), &batchS3Config); err != nil {
return err
}
bedrockConfig.BatchS3Config = &batchS3Config
}
k.BedrockKeyConfig = bedrockConfig
}
// Reconstruct Aliases
if k.AliasesJSON != nil && *k.AliasesJSON != "" {
var aliases schemas.KeyAliases
if err := sonic.Unmarshal([]byte(*k.AliasesJSON), &aliases); err != nil {
return err
}
k.Aliases = aliases
} else {
k.Aliases = nil
}
// Reconstruct VLLM config if fields are present
if k.VLLMUrl != nil || (k.VLLMModelName != nil && *k.VLLMModelName != "") {
vllmConfig := &schemas.VLLMKeyConfig{}
if k.VLLMUrl != nil {
vllmConfig.URL = *k.VLLMUrl
}
if k.VLLMModelName != nil {
vllmConfig.ModelName = *k.VLLMModelName
}
k.VLLMKeyConfig = vllmConfig
} else {
k.VLLMKeyConfig = nil
}
// Reconstruct Replicate config if fields are present
if k.ReplicateUseDeploymentsEndpoint != nil {
k.ReplicateKeyConfig = &schemas.ReplicateKeyConfig{
UseDeploymentsEndpoint: *k.ReplicateUseDeploymentsEndpoint,
}
} else {
k.ReplicateKeyConfig = nil
}
// Reconstruct Ollama config if fields are present
if k.OllamaUrl != nil {
k.OllamaKeyConfig = &schemas.OllamaKeyConfig{
URL: *k.OllamaUrl,
}
} else {
k.OllamaKeyConfig = nil
}
// Reconstruct SGL config if fields are present
if k.SGLUrl != nil {
k.SGLKeyConfig = &schemas.SGLKeyConfig{
URL: *k.SGLUrl,
}
} else {
k.SGLKeyConfig = nil
}
return nil
}

View File

@@ -0,0 +1,16 @@
package tables
import "time"
// TableLogStoreConfig represents the configuration for the log store in the database
type TableLogStoreConfig struct {
ID uint `gorm:"primaryKey;autoIncrement" json:"id"`
Enabled bool `json:"enabled"`
Type string `gorm:"type:varchar(50);not null" json:"type"` // "sqlite"
Config *string `gorm:"type:text" json:"config"` // JSON serialized logstore.Config
CreatedAt time.Time `gorm:"index;not null" json:"created_at"`
UpdatedAt time.Time `gorm:"index;not null" json:"updated_at"`
}
// TableName sets the table name for each model
func (TableLogStoreConfig) TableName() string { return "config_log_store" }

View File

@@ -0,0 +1,252 @@
package tables
import (
"encoding/json"
"fmt"
"time"
"github.com/bytedance/sonic"
"github.com/maximhq/bifrost/core/schemas"
"github.com/maximhq/bifrost/framework/encrypt"
"gorm.io/gorm"
)
// TableMCPClient represents an MCP client configuration in the database
type TableMCPClient struct {
ID uint `gorm:"primaryKey;autoIncrement" json:"id"` // ID is used as the internal primary key and is also accessed by public methods, so it must be present.
ClientID string `gorm:"type:varchar(255);uniqueIndex;not null" json:"client_id"`
Name string `gorm:"type:varchar(255);uniqueIndex;not null" json:"name"`
IsCodeModeClient bool `gorm:"default:false" json:"is_code_mode_client"` // Whether the client is a code mode client
ConnectionType string `gorm:"type:varchar(20);not null" json:"connection_type"` // schemas.MCPConnectionType
ConnectionString *schemas.EnvVar `gorm:"type:text" json:"connection_string,omitempty"`
StdioConfigJSON *string `gorm:"type:text" json:"-"` // JSON serialized schemas.MCPStdioConfig
ToolsToExecuteJSON string `gorm:"type:text" json:"-"` // JSON serialized []string
ToolsToAutoExecuteJSON string `gorm:"type:text" json:"-"` // JSON serialized []string
HeadersJSON string `gorm:"type:text" json:"-"` // JSON serialized map[string]string
AllowedExtraHeadersJSON string `gorm:"type:text" json:"-"` // JSON serialized []string
IsPingAvailable *bool `gorm:"default:true" json:"is_ping_available,omitempty"` // Whether the MCP server supports ping for health checks
ToolPricingJSON string `gorm:"type:text" json:"-"` // JSON serialized map[string]float64
ToolSyncInterval int `gorm:"default:0" json:"tool_sync_interval"` // Per-client tool sync interval in minutes (0 = use global, -1 = disabled)
// Per-user OAuth: discovered tools persisted so they survive restart
DiscoveredToolsJSON string `gorm:"type:text" json:"-"` // JSON serialized map[string]schemas.ChatTool
ToolNameMappingJSON string `gorm:"type:text" json:"-"` // JSON serialized map[string]string
// OAuth authentication fields
AuthType string `gorm:"type:varchar(20);default:'headers'" json:"auth_type"` // "none", "headers", "oauth"
OauthConfigID *string `gorm:"type:varchar(255);index;constraint:OnDelete:CASCADE" json:"oauth_config_id"` // Foreign key to oauth_configs.ID with CASCADE delete
OauthConfig *TableOauthConfig `gorm:"foreignKey:OauthConfigID;references:ID;constraint:OnDelete:CASCADE" json:"-"` // Gorm relationship
AllowOnAllVirtualKeys bool `gorm:"default:false" json:"allow_on_all_virtual_keys"` // Whether to allow the MCP client to run on all virtual keys
// Config hash is used to detect the changes synced from config.json file
// Every time we sync the config.json file, we will update the config hash
ConfigHash string `gorm:"type:varchar(255);null" json:"config_hash"`
EncryptionStatus string `gorm:"type:varchar(20);default:'plain_text'" json:"-"`
CreatedAt time.Time `gorm:"index;not null" json:"created_at"`
UpdatedAt time.Time `gorm:"index;not null" json:"updated_at"`
// Virtual fields for runtime use (not stored in DB)
StdioConfig *schemas.MCPStdioConfig `gorm:"-" json:"stdio_config,omitempty"`
ToolsToExecute schemas.WhiteList `gorm:"-" json:"tools_to_execute"`
ToolsToAutoExecute schemas.WhiteList `gorm:"-" json:"tools_to_auto_execute"`
Headers map[string]schemas.EnvVar `gorm:"-" json:"headers"`
AllowedExtraHeaders schemas.WhiteList `gorm:"-" json:"allowed_extra_headers"`
ToolPricing map[string]float64 `gorm:"-" json:"tool_pricing"`
DiscoveredTools map[string]schemas.ChatTool `gorm:"-" json:"-"`
DiscoveredToolNameMapping map[string]string `gorm:"-" json:"-"`
}
// TableName sets the table name for each model
func (TableMCPClient) TableName() string { return "config_mcp_clients" }
// BeforeSave is a GORM hook that serializes runtime fields (stdio config, tools, headers,
// pricing) into JSON columns and encrypts the connection string and headers before writing
// to the database. Environment-variable-backed connection strings are not encrypted.
func (c *TableMCPClient) BeforeSave(tx *gorm.DB) error {
if c.StdioConfig != nil {
data, err := json.Marshal(c.StdioConfig)
if err != nil {
return err
}
config := string(data)
c.StdioConfigJSON = &config
} else {
c.StdioConfigJSON = nil
}
if c.ToolsToExecute != nil {
if err := c.ToolsToExecute.Validate(); err != nil {
return fmt.Errorf("invalid tools_to_execute: %w", err)
}
data, err := json.Marshal(c.ToolsToExecute)
if err != nil {
return err
}
c.ToolsToExecuteJSON = string(data)
} else {
c.ToolsToExecuteJSON = "[]"
}
if c.ToolsToAutoExecute != nil {
if err := c.ToolsToAutoExecute.Validate(); err != nil {
return fmt.Errorf("invalid tools_to_auto_execute: %w", err)
}
data, err := json.Marshal(c.ToolsToAutoExecute)
if err != nil {
return err
}
c.ToolsToAutoExecuteJSON = string(data)
} else {
c.ToolsToAutoExecuteJSON = "[]"
}
if c.Headers != nil {
headersToSerialize := make(map[string]string, len(c.Headers))
for key, value := range c.Headers {
if value.IsFromEnv() {
headersToSerialize[key] = value.EnvVar
} else {
headersToSerialize[key] = value.GetValue()
}
}
data, err := json.Marshal(headersToSerialize)
if err != nil {
return err
}
c.HeadersJSON = string(data)
} else {
c.HeadersJSON = "{}"
}
if c.AllowedExtraHeaders != nil {
if err := c.AllowedExtraHeaders.Validate(); err != nil {
return fmt.Errorf("invalid allowed_extra_headers: %w", err)
}
data, err := json.Marshal(c.AllowedExtraHeaders)
if err != nil {
return err
}
c.AllowedExtraHeadersJSON = string(data)
} else {
c.AllowedExtraHeadersJSON = "[]"
}
if c.ToolPricing != nil {
data, err := json.Marshal(c.ToolPricing)
if err != nil {
return err
}
c.ToolPricingJSON = string(data)
} else {
c.ToolPricingJSON = "{}"
}
if c.DiscoveredTools != nil {
data, err := json.Marshal(c.DiscoveredTools)
if err != nil {
return err
}
c.DiscoveredToolsJSON = string(data)
}
if c.DiscoveredToolNameMapping != nil {
data, err := json.Marshal(c.DiscoveredToolNameMapping)
if err != nil {
return err
}
c.ToolNameMappingJSON = string(data)
}
// Encrypt sensitive fields after serialization.
// Always set EncryptionStatus when encryption is enabled so the startup
// batch pass does not re-process this row indefinitely.
if encrypt.IsEnabled() {
if c.ConnectionString != nil && !c.ConnectionString.IsFromEnv() && c.ConnectionString.GetValue() != "" {
// Copy to avoid encrypting the shared ConnectionString through the pointer
cs := *c.ConnectionString
enc, err := encrypt.Encrypt(cs.Val)
if err != nil {
return fmt.Errorf("failed to encrypt mcp connection string: %w", err)
}
cs.Val = enc
c.ConnectionString = &cs
}
if c.HeadersJSON != "" && c.HeadersJSON != "{}" {
enc, err := encrypt.Encrypt(c.HeadersJSON)
if err != nil {
return fmt.Errorf("failed to encrypt mcp headers: %w", err)
}
c.HeadersJSON = enc
}
c.EncryptionStatus = EncryptionStatusEncrypted
}
return nil
}
// AfterFind is a GORM hook that decrypts the connection string and headers (if encrypted)
// and deserializes JSON columns back into runtime structs after reading from the database.
func (c *TableMCPClient) AfterFind(tx *gorm.DB) error {
if c.EncryptionStatus == "encrypted" {
if c.HeadersJSON != "" && c.HeadersJSON != "{}" {
decrypted, err := encrypt.Decrypt(c.HeadersJSON)
if err != nil {
return fmt.Errorf("failed to decrypt mcp headers: %w", err)
}
c.HeadersJSON = decrypted
}
if c.ConnectionString != nil && !c.ConnectionString.IsFromEnv() && c.ConnectionString.GetValue() != "" {
decrypted, err := encrypt.Decrypt(c.ConnectionString.Val)
if err != nil {
return fmt.Errorf("failed to decrypt mcp connection string: %w", err)
}
c.ConnectionString.Val = decrypted
}
}
if c.StdioConfigJSON != nil {
var config schemas.MCPStdioConfig
if err := sonic.Unmarshal([]byte(*c.StdioConfigJSON), &config); err != nil {
return err
}
c.StdioConfig = &config
}
if c.ToolsToExecuteJSON != "" {
if err := sonic.Unmarshal([]byte(c.ToolsToExecuteJSON), &c.ToolsToExecute); err != nil {
return err
}
}
if c.ToolsToAutoExecuteJSON != "" {
if err := sonic.Unmarshal([]byte(c.ToolsToAutoExecuteJSON), &c.ToolsToAutoExecute); err != nil {
return err
}
}
if c.HeadersJSON != "" {
if err := sonic.Unmarshal([]byte(c.HeadersJSON), &c.Headers); err != nil {
return err
}
}
if c.AllowedExtraHeadersJSON != "" {
if err := sonic.Unmarshal([]byte(c.AllowedExtraHeadersJSON), &c.AllowedExtraHeaders); err != nil {
return err
}
}
if c.ToolPricingJSON != "" {
if err := json.Unmarshal([]byte(c.ToolPricingJSON), &c.ToolPricing); err != nil {
return err
}
}
if c.DiscoveredToolsJSON != "" {
if err := sonic.Unmarshal([]byte(c.DiscoveredToolsJSON), &c.DiscoveredTools); err != nil {
return err
}
}
if c.ToolNameMappingJSON != "" {
if err := sonic.Unmarshal([]byte(c.ToolNameMappingJSON), &c.DiscoveredToolNameMapping); err != nil {
return err
}
}
return nil
}

View File

@@ -0,0 +1,15 @@
package tables
import "time"
// TableModel represents a model configuration in the database
type TableModel struct {
ID string `gorm:"primaryKey" json:"id"`
ProviderID uint `gorm:"index;not null;uniqueIndex:idx_provider_name" json:"provider_id"`
Name string `gorm:"uniqueIndex:idx_provider_name" json:"name"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
// TableName sets the table name for each model
func (TableModel) TableName() string { return "config_models" }

View File

@@ -0,0 +1,59 @@
package tables
import (
"fmt"
"strings"
"time"
"gorm.io/gorm"
)
// TableModelConfig represents a model configuration with rate limiting and budgeting
type TableModelConfig struct {
ID string `gorm:"primaryKey;type:varchar(255)" json:"id"`
ModelName string `gorm:"type:varchar(255);not null;uniqueIndex:idx_model_provider" json:"model_name"`
Provider *string `gorm:"type:varchar(50);uniqueIndex:idx_model_provider" json:"provider,omitempty"` // Optional provider, nullable
BudgetID *string `gorm:"type:varchar(255);index:idx_model_config_budget" json:"budget_id,omitempty"`
RateLimitID *string `gorm:"type:varchar(255);index:idx_model_config_rate_limit" json:"rate_limit_id,omitempty"`
// Relationships
Budget *TableBudget `gorm:"foreignKey:BudgetID;onDelete:CASCADE" json:"budget,omitempty"`
RateLimit *TableRateLimit `gorm:"foreignKey:RateLimitID;onDelete:CASCADE" json:"rate_limit,omitempty"`
// Config hash is used to detect the changes synced from config.json file
// Every time we sync the config.json file, we will update the config hash
ConfigHash string `gorm:"type:varchar(255);null" json:"config_hash"`
CreatedAt time.Time `gorm:"index;not null" json:"created_at"`
UpdatedAt time.Time `gorm:"index;not null" json:"updated_at"`
}
// TableName sets the table name for each model
func (TableModelConfig) TableName() string {
return "governance_model_configs"
}
// BeforeSave hook for ModelConfig to validate required fields
func (mc *TableModelConfig) BeforeSave(tx *gorm.DB) error {
// Validate that ModelName is not empty
if strings.TrimSpace(mc.ModelName) == "" {
return fmt.Errorf("model_name cannot be empty")
}
// Validate that if BudgetID is provided, it's not an empty string
if mc.BudgetID != nil && strings.TrimSpace(*mc.BudgetID) == "" {
return fmt.Errorf("budget_id cannot be an empty string")
}
// Validate that if RateLimitID is provided, it's not an empty string
if mc.RateLimitID != nil && strings.TrimSpace(*mc.RateLimitID) == "" {
return fmt.Errorf("rate_limit_id cannot be an empty string")
}
// Validate that if Provider is provided, it's not an empty string
if mc.Provider != nil && strings.TrimSpace(*mc.Provider) == "" {
return fmt.Errorf("provider cannot be an empty string")
}
return nil
}

View File

@@ -0,0 +1,13 @@
package tables
// TableModelParameters stores model parameters and capabilities data
// synced from the external datasheet API. Each row holds one model's
// full parameter/capability JSON blob.
type TableModelParameters struct {
ID uint `gorm:"primaryKey;autoIncrement" json:"id"`
Model string `gorm:"type:varchar(255);not null;uniqueIndex:idx_model_params_model" json:"model"`
Data string `gorm:"type:text;not null" json:"data"` // Raw JSON blob
}
// TableName sets the table name
func (TableModelParameters) TableName() string { return "governance_model_parameters" }

View File

@@ -0,0 +1,97 @@
package tables
import "github.com/maximhq/bifrost/core/schemas"
// TableModelPricing represents pricing information for AI models
type TableModelPricing struct {
ID uint `gorm:"primaryKey;autoIncrement" json:"id"`
Model string `gorm:"type:varchar(255);not null;uniqueIndex:idx_model_provider_mode" json:"model"`
BaseModel string `gorm:"type:varchar(255);default:null" json:"base_model,omitempty"`
Provider string `gorm:"type:varchar(50);not null;uniqueIndex:idx_model_provider_mode" json:"provider"`
Mode string `gorm:"type:varchar(50);not null;uniqueIndex:idx_model_provider_mode" json:"mode"`
ContextLength *int `gorm:"default:null" json:"context_length,omitempty"`
MaxInputTokens *int `gorm:"default:null" json:"max_input_tokens,omitempty"`
MaxOutputTokens *int `gorm:"default:null" json:"max_output_tokens,omitempty"`
Architecture *schemas.Architecture `gorm:"type:text;serializer:json;default:null" json:"architecture,omitempty"`
// Costs - Text
InputCostPerToken *float64 `gorm:"default:null" json:"input_cost_per_token,omitempty"`
OutputCostPerToken *float64 `gorm:"default:null" json:"output_cost_per_token,omitempty"`
InputCostPerTokenBatches *float64 `gorm:"default:null;column:input_cost_per_token_batches" json:"input_cost_per_token_batches,omitempty"`
OutputCostPerTokenBatches *float64 `gorm:"default:null;column:output_cost_per_token_batches" json:"output_cost_per_token_batches,omitempty"`
InputCostPerTokenPriority *float64 `gorm:"default:null;column:input_cost_per_token_priority" json:"input_cost_per_token_priority,omitempty"`
OutputCostPerTokenPriority *float64 `gorm:"default:null;column:output_cost_per_token_priority" json:"output_cost_per_token_priority,omitempty"`
InputCostPerTokenFlex *float64 `gorm:"default:null;column:input_cost_per_token_flex" json:"input_cost_per_token_flex,omitempty"`
OutputCostPerTokenFlex *float64 `gorm:"default:null;column:output_cost_per_token_flex" json:"output_cost_per_token_flex,omitempty"`
InputCostPerCharacter *float64 `gorm:"default:null;column:input_cost_per_character" json:"input_cost_per_character,omitempty"`
// Costs - 128k Tier
InputCostPerTokenAbove128kTokens *float64 `gorm:"default:null;column:input_cost_per_token_above_128k_tokens" json:"input_cost_per_token_above_128k_tokens,omitempty"`
InputCostPerImageAbove128kTokens *float64 `gorm:"default:null;column:input_cost_per_image_above_128k_tokens" json:"input_cost_per_image_above_128k_tokens,omitempty"`
InputCostPerVideoPerSecondAbove128kTokens *float64 `gorm:"default:null;column:input_cost_per_video_per_second_above_128k_tokens" json:"input_cost_per_video_per_second_above_128k_tokens,omitempty"`
InputCostPerAudioPerSecondAbove128kTokens *float64 `gorm:"default:null;column:input_cost_per_audio_per_second_above_128k_tokens" json:"input_cost_per_audio_per_second_above_128k_tokens,omitempty"`
OutputCostPerTokenAbove128kTokens *float64 `gorm:"default:null;column:output_cost_per_token_above_128k_tokens" json:"output_cost_per_token_above_128k_tokens,omitempty"`
// Costs - 200k Tier
InputCostPerTokenAbove200kTokens *float64 `gorm:"default:null;column:input_cost_per_token_above_200k_tokens" json:"input_cost_per_token_above_200k_tokens,omitempty"`
InputCostPerTokenAbove200kTokensPriority *float64 `gorm:"default:null;column:input_cost_per_token_above_200k_tokens_priority" json:"input_cost_per_token_above_200k_tokens_priority,omitempty"`
OutputCostPerTokenAbove200kTokens *float64 `gorm:"default:null;column:output_cost_per_token_above_200k_tokens" json:"output_cost_per_token_above_200k_tokens,omitempty"`
OutputCostPerTokenAbove200kTokensPriority *float64 `gorm:"default:null;column:output_cost_per_token_above_200k_tokens_priority" json:"output_cost_per_token_above_200k_tokens_priority,omitempty"`
// Costs - 272k Tier
InputCostPerTokenAbove272kTokens *float64 `gorm:"default:null;column:input_cost_per_token_above_272k_tokens" json:"input_cost_per_token_above_272k_tokens,omitempty"`
InputCostPerTokenAbove272kTokensPriority *float64 `gorm:"default:null;column:input_cost_per_token_above_272k_tokens_priority" json:"input_cost_per_token_above_272k_tokens_priority,omitempty"`
OutputCostPerTokenAbove272kTokens *float64 `gorm:"default:null;column:output_cost_per_token_above_272k_tokens" json:"output_cost_per_token_above_272k_tokens,omitempty"`
OutputCostPerTokenAbove272kTokensPriority *float64 `gorm:"default:null;column:output_cost_per_token_above_272k_tokens_priority" json:"output_cost_per_token_above_272k_tokens_priority,omitempty"`
// Costs - Cache
CacheCreationInputTokenCost *float64 `gorm:"default:null;column:cache_creation_input_token_cost" json:"cache_creation_input_token_cost,omitempty"`
CacheReadInputTokenCost *float64 `gorm:"default:null;column:cache_read_input_token_cost" json:"cache_read_input_token_cost,omitempty"`
CacheCreationInputTokenCostAbove200kTokens *float64 `gorm:"default:null;column:cache_creation_input_token_cost_above_200k_tokens" json:"cache_creation_input_token_cost_above_200k_tokens,omitempty"`
CacheReadInputTokenCostAbove200kTokens *float64 `gorm:"default:null;column:cache_read_input_token_cost_above_200k_tokens" json:"cache_read_input_token_cost_above_200k_tokens,omitempty"`
CacheReadInputTokenCostAbove200kTokensPriority *float64 `gorm:"default:null;column:cache_read_input_token_cost_above_200k_tokens_priority" json:"cache_read_input_token_cost_above_200k_tokens_priority,omitempty"`
CacheCreationInputTokenCostAbove1hr *float64 `gorm:"default:null;column:cache_creation_input_token_cost_above_1hr" json:"cache_creation_input_token_cost_above_1hr,omitempty"`
CacheCreationInputTokenCostAbove1hrAbove200kTokens *float64 `gorm:"default:null;column:cache_creation_input_token_cost_above_1hr_above_200k_tokens" json:"cache_creation_input_token_cost_above_1hr_above_200k_tokens,omitempty"`
CacheCreationInputAudioTokenCost *float64 `gorm:"default:null;column:cache_creation_input_audio_token_cost" json:"cache_creation_input_audio_token_cost,omitempty"`
CacheReadInputTokenCostPriority *float64 `gorm:"default:null;column:cache_read_input_token_cost_priority" json:"cache_read_input_token_cost_priority,omitempty"`
CacheReadInputTokenCostFlex *float64 `gorm:"default:null;column:cache_read_input_token_cost_flex" json:"cache_read_input_token_cost_flex,omitempty"`
CacheReadInputImageTokenCost *float64 `gorm:"default:null;column:cache_read_input_image_token_cost" json:"cache_read_input_image_token_cost,omitempty"`
CacheReadInputTokenCostAbove272kTokens *float64 `gorm:"default:null;column:cache_read_input_token_cost_above_272k_tokens" json:"cache_read_input_token_cost_above_272k_tokens,omitempty"`
CacheReadInputTokenCostAbove272kTokensPriority *float64 `gorm:"default:null;column:cache_read_input_token_cost_above_272k_tokens_priority" json:"cache_read_input_token_cost_above_272k_tokens_priority,omitempty"`
// Costs - Image
InputCostPerImage *float64 `gorm:"default:null;column:input_cost_per_image" json:"input_cost_per_image,omitempty"`
InputCostPerPixel *float64 `gorm:"default:null;column:input_cost_per_pixel" json:"input_cost_per_pixel,omitempty"`
OutputCostPerImage *float64 `gorm:"default:null;column:output_cost_per_image" json:"output_cost_per_image,omitempty"`
OutputCostPerPixel *float64 `gorm:"default:null;column:output_cost_per_pixel" json:"output_cost_per_pixel,omitempty"`
OutputCostPerImagePremiumImage *float64 `gorm:"default:null;column:output_cost_per_image_premium_image" json:"output_cost_per_image_premium_image,omitempty"`
OutputCostPerImageAbove512x512Pixels *float64 `gorm:"default:null;column:output_cost_per_image_above_512_and_512_pixels" json:"output_cost_per_image_above_512_and_512_pixels,omitempty"`
OutputCostPerImageAbove512x512PixelsPremium *float64 `gorm:"default:null;column:output_cost_per_image_above_512x512_pixels_premium" json:"output_cost_per_image_above_512_and_512_pixels_and_premium_image,omitempty"`
OutputCostPerImageAbove1024x1024Pixels *float64 `gorm:"default:null;column:output_cost_per_image_above_1024_and_1024_pixels" json:"output_cost_per_image_above_1024_and_1024_pixels,omitempty"`
OutputCostPerImageAbove1024x1024PixelsPremium *float64 `gorm:"default:null;column:output_cost_per_image_above_1024x1024_pixels_premium" json:"output_cost_per_image_above_1024_and_1024_pixels_and_premium_image,omitempty"`
OutputCostPerImageAbove2048x2048Pixels *float64 `gorm:"default:null;column:output_cost_per_image_above_2048_and_2048_pixels" json:"output_cost_per_image_above_2048_and_2048_pixels,omitempty"`
OutputCostPerImageAbove4096x4096Pixels *float64 `gorm:"default:null;column:output_cost_per_image_above_4096_and_4096_pixels" json:"output_cost_per_image_above_4096_and_4096_pixels,omitempty"`
OutputCostPerImageLowQuality *float64 `gorm:"default:null;column:output_cost_per_image_low_quality" json:"output_cost_per_image_low_quality,omitempty"`
OutputCostPerImageMediumQuality *float64 `gorm:"default:null;column:output_cost_per_image_medium_quality" json:"output_cost_per_image_medium_quality,omitempty"`
OutputCostPerImageHighQuality *float64 `gorm:"default:null;column:output_cost_per_image_high_quality" json:"output_cost_per_image_high_quality,omitempty"`
OutputCostPerImageAutoQuality *float64 `gorm:"default:null;column:output_cost_per_image_auto_quality" json:"output_cost_per_image_auto_quality,omitempty"`
InputCostPerImageToken *float64 `gorm:"default:null;column:input_cost_per_image_token" json:"input_cost_per_image_token,omitempty"`
OutputCostPerImageToken *float64 `gorm:"default:null;column:output_cost_per_image_token" json:"output_cost_per_image_token,omitempty"`
// Costs - Audio/Video
InputCostPerAudioToken *float64 `gorm:"default:null;column:input_cost_per_audio_token" json:"input_cost_per_audio_token,omitempty"`
InputCostPerAudioPerSecond *float64 `gorm:"default:null;column:input_cost_per_audio_per_second" json:"input_cost_per_audio_per_second,omitempty"`
InputCostPerSecond *float64 `gorm:"default:null;column:input_cost_per_second" json:"input_cost_per_second,omitempty"` // Only for transcription models
InputCostPerVideoPerSecond *float64 `gorm:"default:null;column:input_cost_per_video_per_second" json:"input_cost_per_video_per_second,omitempty"`
OutputCostPerAudioToken *float64 `gorm:"default:null;column:output_cost_per_audio_token" json:"output_cost_per_audio_token,omitempty"`
OutputCostPerVideoPerSecond *float64 `gorm:"default:null;column:output_cost_per_video_per_second" json:"output_cost_per_video_per_second,omitempty"`
OutputCostPerSecond *float64 `gorm:"default:null;column:output_cost_per_second" json:"output_cost_per_second,omitempty"` // For both speech and video models
// Costs - Other
SearchContextCostPerQuery *float64 `gorm:"default:null;column:search_context_cost_per_query" json:"search_context_cost_per_query,omitempty"`
CodeInterpreterCostPerSession *float64 `gorm:"default:null;column:code_interpreter_cost_per_session" json:"code_interpreter_cost_per_session,omitempty"`
// Costs - OCR
OCRCostPerPage *float64 `gorm:"default:null;column:ocr_cost_per_page" json:"ocr_cost_per_page,omitempty"`
AnnotationCostPerPage *float64 `gorm:"default:null;column:annotation_cost_per_page" json:"annotation_cost_per_page,omitempty"`
}
// TableName sets the table name for each model
func (TableModelPricing) TableName() string { return "governance_model_pricing" }

View File

@@ -0,0 +1,379 @@
package tables
import (
"fmt"
"time"
"github.com/maximhq/bifrost/framework/encrypt"
"gorm.io/gorm"
)
// TableOauthConfig represents an OAuth configuration in the database
// This stores the OAuth client configuration and flow state
type TableOauthConfig struct {
ID string `gorm:"type:varchar(255);primaryKey" json:"id"` // UUID
ClientID string `gorm:"type:varchar(512)" json:"client_id"` // OAuth provider's client ID (optional for public clients)
ClientSecret string `gorm:"type:text" json:"-"` // Encrypted OAuth client secret (optional for public clients)
AuthorizeURL string `gorm:"type:text" json:"authorize_url"` // Provider's authorization endpoint (optional, can be discovered)
TokenURL string `gorm:"type:text" json:"token_url"` // Provider's token endpoint (optional, can be discovered)
RegistrationURL *string `gorm:"type:text" json:"registration_url,omitempty"` // Provider's dynamic registration endpoint (optional, can be discovered)
RedirectURI string `gorm:"type:text;not null" json:"redirect_uri"` // Callback URL
Scopes string `gorm:"type:text" json:"scopes"` // JSON array of scopes (optional, can be discovered)
State string `gorm:"type:varchar(255);uniqueIndex;not null" json:"-"` // CSRF state token
CodeVerifier string `gorm:"type:text" json:"-"` // PKCE code verifier (generated, kept secret)
CodeChallenge string `gorm:"type:varchar(255)" json:"code_challenge"` // PKCE code challenge (sent to provider)
Status string `gorm:"type:varchar(50);not null;index" json:"status"` // "pending", "authorized", "failed", "expired", "revoked"
TokenID *string `gorm:"type:varchar(255);index" json:"token_id"` // Foreign key to oauth_tokens.ID (set after callback)
ServerURL string `gorm:"type:text" json:"server_url"` // MCP server URL for OAuth discovery
UseDiscovery bool `gorm:"default:false" json:"use_discovery"` // Flag to enable OAuth discovery
MCPClientConfigJSON *string `gorm:"type:text" json:"-"` // JSON serialized MCPClientConfig for multi-instance support (pending MCP client waiting for OAuth completion)
EncryptionStatus string `gorm:"type:varchar(20);default:'plain_text'" json:"-"`
CreatedAt time.Time `gorm:"index;not null" json:"created_at"`
UpdatedAt time.Time `gorm:"index;not null" json:"updated_at"`
ExpiresAt time.Time `gorm:"index;not null" json:"expires_at"` // State expiry (15 min)
}
// TableName sets the table name
func (TableOauthConfig) TableName() string {
return "oauth_configs"
}
// BeforeSave hook
func (c *TableOauthConfig) BeforeSave(tx *gorm.DB) error {
// Ensure status is valid
if c.Status == "" {
c.Status = "pending"
}
// Encrypt sensitive fields
if encrypt.IsEnabled() {
encrypted := false
if c.ClientSecret != "" {
if err := encryptString(&c.ClientSecret); err != nil {
return fmt.Errorf("failed to encrypt oauth client secret: %w", err)
}
encrypted = true
}
if c.CodeVerifier != "" {
if err := encryptString(&c.CodeVerifier); err != nil {
return fmt.Errorf("failed to encrypt oauth code verifier: %w", err)
}
encrypted = true
}
if encrypted {
c.EncryptionStatus = EncryptionStatusEncrypted
}
}
return nil
}
// AfterFind hook to decrypt sensitive fields
func (c *TableOauthConfig) AfterFind(tx *gorm.DB) error {
if c.EncryptionStatus == EncryptionStatusEncrypted {
if err := decryptString(&c.ClientSecret); err != nil {
return fmt.Errorf("failed to decrypt oauth client secret: %w", err)
}
if err := decryptString(&c.CodeVerifier); err != nil {
return fmt.Errorf("failed to decrypt oauth code verifier: %w", err)
}
}
return nil
}
// TableOauthToken represents an OAuth token in the database
// This stores the actual access and refresh tokens
type TableOauthToken struct {
ID string `gorm:"type:varchar(255);primaryKey" json:"id"` // UUID
AccessToken string `gorm:"type:text;not null" json:"-"` // Encrypted access token
RefreshToken string `gorm:"type:text" json:"-"` // Encrypted refresh token (optional)
TokenType string `gorm:"type:varchar(50);not null" json:"token_type"` // "Bearer"
ExpiresAt time.Time `gorm:"index;not null" json:"expires_at"` // Token expiration
Scopes string `gorm:"type:text" json:"scopes"` // JSON array of granted scopes
LastRefreshedAt *time.Time `gorm:"index" json:"last_refreshed_at,omitempty"` // Track when token was last refreshed
EncryptionStatus string `gorm:"type:varchar(20);default:'plain_text'" json:"-"`
CreatedAt time.Time `gorm:"index;not null" json:"created_at"`
UpdatedAt time.Time `gorm:"index;not null" json:"updated_at"`
}
// TableName sets the table name
func (TableOauthToken) TableName() string {
return "oauth_tokens"
}
// BeforeSave hook
func (t *TableOauthToken) BeforeSave(tx *gorm.DB) error {
// Ensure token type is set
if t.TokenType == "" {
t.TokenType = "Bearer"
}
// Encrypt sensitive fields
if encrypt.IsEnabled() {
if err := encryptString(&t.AccessToken); err != nil {
return fmt.Errorf("failed to encrypt oauth access token: %w", err)
}
if err := encryptString(&t.RefreshToken); err != nil {
return fmt.Errorf("failed to encrypt oauth refresh token: %w", err)
}
t.EncryptionStatus = EncryptionStatusEncrypted
}
return nil
}
// AfterFind hook to decrypt sensitive fields
func (t *TableOauthToken) AfterFind(tx *gorm.DB) error {
if t.EncryptionStatus == EncryptionStatusEncrypted {
if err := decryptString(&t.AccessToken); err != nil {
return fmt.Errorf("failed to decrypt oauth access token: %w", err)
}
if err := decryptString(&t.RefreshToken); err != nil {
return fmt.Errorf("failed to decrypt oauth refresh token: %w", err)
}
}
return nil
}
// ---------- Per-User OAuth Tables ----------
// TableOauthUserSession tracks pending per-user OAuth flows.
// Each record maps an OAuth state token to a specific MCP client, allowing
// the callback to associate the resulting tokens with the correct user session.
type TableOauthUserSession struct {
ID string `gorm:"type:varchar(255);primaryKey" json:"id"` // Session UUID
MCPClientID string `gorm:"type:varchar(255);not null;index" json:"mcp_client_id"` // Which MCP server this auth is for
OauthConfigID string `gorm:"type:varchar(255);not null;index" json:"oauth_config_id"` // Template OAuth config (holds client_id, token_url, etc.)
State string `gorm:"type:varchar(255);uniqueIndex;not null" json:"-"` // CSRF state token sent to OAuth provider
RedirectURI string `gorm:"type:text" json:"-"` // Per-request redirect URI used in authorize step
CodeVerifier string `gorm:"type:text" json:"-"` // PKCE code verifier (kept secret)
SessionToken string `gorm:"type:varchar(255)" json:"-"` // Bifrost session ID (links to oauth_per_user_sessions)
SessionTokenHash string `gorm:"type:varchar(64);uniqueIndex" json:"-"` // SHA-256 hash of SessionToken for secure lookups
GatewaySessionID string `gorm:"type:varchar(255);index" json:"-"` // Bifrost MCP gateway session ID (separate from SessionToken)
VirtualKeyID *string `gorm:"type:varchar(255);index" json:"virtual_key_id"` // VK identity (propagated to oauth_user_tokens)
UserID *string `gorm:"type:varchar(255);index" json:"user_id"` // Enterprise user identity (propagated to oauth_user_tokens)
Status string `gorm:"type:varchar(50);not null;index" json:"status"` // "pending", "authorized", "failed", "expired"
EncryptionStatus string `gorm:"type:varchar(20);default:'plain_text'" json:"-"`
ExpiresAt time.Time `gorm:"index;not null" json:"expires_at"` // Flow expiration (15 min)
CreatedAt time.Time `gorm:"index;not null" json:"created_at"`
UpdatedAt time.Time `gorm:"index;not null" json:"updated_at"`
}
func (TableOauthUserSession) TableName() string {
return "oauth_user_sessions"
}
func (s *TableOauthUserSession) BeforeSave(tx *gorm.DB) error {
if s.Status == "" {
s.Status = "pending"
}
if s.SessionToken != "" {
s.SessionTokenHash = encrypt.HashSHA256(s.SessionToken)
}
if encrypt.IsEnabled() {
if s.CodeVerifier != "" {
if err := encryptString(&s.CodeVerifier); err != nil {
return fmt.Errorf("failed to encrypt oauth user session code verifier: %w", err)
}
}
s.EncryptionStatus = EncryptionStatusEncrypted
}
return nil
}
func (s *TableOauthUserSession) AfterFind(tx *gorm.DB) error {
if s.EncryptionStatus == EncryptionStatusEncrypted && s.CodeVerifier != "" {
if err := decryptString(&s.CodeVerifier); err != nil {
return fmt.Errorf("failed to decrypt oauth user session code verifier: %w", err)
}
}
return nil
}
// TableOauthUserToken stores per-user OAuth credentials.
// Each record holds the access/refresh tokens for a specific user session + MCP client pair.
// Lookup is by SessionToken.
type TableOauthUserToken struct {
ID string `gorm:"type:varchar(255);primaryKey" json:"id"` // Token UUID
SessionToken string `gorm:"type:varchar(255)" json:"-"` // Maps to Bifrost session (fallback for anonymous users)
SessionTokenHash string `gorm:"type:varchar(64);index" json:"-"` // SHA-256 hash of SessionToken for secure lookups
VirtualKeyID *string `gorm:"type:varchar(255);index:idx_vk_mcp" json:"virtual_key_id"` // VK identity (persistent across sessions)
UserID *string `gorm:"type:varchar(255);index:idx_user_mcp" json:"user_id"` // Enterprise user identity (persistent across sessions)
MCPClientID string `gorm:"type:varchar(255);not null;index:idx_vk_mcp;index:idx_user_mcp" json:"mcp_client_id"` // Which MCP server
OauthConfigID string `gorm:"type:varchar(255);not null;index" json:"oauth_config_id"` // Template OAuth config
AccessToken string `gorm:"type:text;not null" json:"-"` // Encrypted user's OAuth access token
RefreshToken string `gorm:"type:text" json:"-"` // Encrypted user's OAuth refresh token
TokenType string `gorm:"type:varchar(50);not null" json:"token_type"` // "Bearer"
ExpiresAt time.Time `gorm:"index;not null" json:"expires_at"` // Token expiry
Scopes string `gorm:"type:text" json:"scopes"` // JSON array of granted scopes
LastRefreshedAt *time.Time `gorm:"index" json:"last_refreshed_at,omitempty"` // Last refresh time
EncryptionStatus string `gorm:"type:varchar(20);default:'plain_text'" json:"-"`
CreatedAt time.Time `gorm:"index;not null" json:"created_at"`
UpdatedAt time.Time `gorm:"index;not null" json:"updated_at"`
}
func (TableOauthUserToken) TableName() string {
return "oauth_user_tokens"
}
func (t *TableOauthUserToken) BeforeSave(tx *gorm.DB) error {
if t.TokenType == "" {
t.TokenType = "Bearer"
}
if t.SessionToken != "" {
t.SessionTokenHash = encrypt.HashSHA256(t.SessionToken)
}
if encrypt.IsEnabled() {
if err := encryptString(&t.AccessToken); err != nil {
return fmt.Errorf("failed to encrypt oauth user access token: %w", err)
}
if err := encryptString(&t.RefreshToken); err != nil {
return fmt.Errorf("failed to encrypt oauth user refresh token: %w", err)
}
t.EncryptionStatus = EncryptionStatusEncrypted
}
return nil
}
func (t *TableOauthUserToken) AfterFind(tx *gorm.DB) error {
if t.EncryptionStatus == EncryptionStatusEncrypted {
if err := decryptString(&t.AccessToken); err != nil {
return fmt.Errorf("failed to decrypt oauth user access token: %w", err)
}
if err := decryptString(&t.RefreshToken); err != nil {
return fmt.Errorf("failed to decrypt oauth user refresh token: %w", err)
}
}
return nil
}
// ---------- Per-User OAuth Authorization Server Tables ----------
// TablePerUserOAuthClient stores dynamically registered OAuth clients (RFC 7591).
// MCP clients (like Claude Code) register themselves with Bifrost's OAuth
// authorization server to obtain a client_id for the authorization code flow.
type TablePerUserOAuthClient struct {
ID string `gorm:"type:varchar(255);primaryKey" json:"id"`
ClientID string `gorm:"type:varchar(255);uniqueIndex;not null" json:"client_id"`
ClientName string `gorm:"type:varchar(255)" json:"client_name"`
RedirectURIs string `gorm:"type:text;not null" json:"redirect_uris"` // JSON array of allowed redirect URIs
GrantTypes string `gorm:"type:text" json:"grant_types"` // JSON array of grant types
CreatedAt time.Time `gorm:"index;not null" json:"created_at"`
UpdatedAt time.Time `gorm:"index;not null" json:"updated_at"`
}
// TableName returns the table name for per-user OAuth clients.
func (TablePerUserOAuthClient) TableName() string {
return "oauth_per_user_clients"
}
// TablePerUserOAuthSession stores Bifrost-issued access tokens for authenticated
// MCP connections. When a user authenticates via Bifrost's OAuth flow, a session
// is created. The access token is included in all subsequent MCP requests.
// Upstream provider tokens are linked via the oauth_user_tokens table.
type TablePerUserOAuthSession struct {
ID string `gorm:"type:varchar(255);primaryKey" json:"id"`
AccessToken string `gorm:"type:text;not null" json:"-"` // Bifrost-issued access token (encrypted)
AccessTokenHash string `gorm:"type:varchar(64);uniqueIndex" json:"-"` // SHA-256 hash for secure lookups
RefreshToken string `gorm:"type:text" json:"-"` // Bifrost-issued refresh token (encrypted, optional)
RefreshTokenHash string `gorm:"type:varchar(64);index" json:"-"` // SHA-256 hash for secure lookups (not unique — refresh tokens are optional)
ClientID string `gorm:"type:varchar(255);not null;index" json:"client_id"` // Which OAuth client registered this session
VirtualKeyID *string `gorm:"type:varchar(255);index" json:"virtual_key_id"` // Linked VK identity (set when VK is present during auth)
VirtualKey *TableVirtualKey `gorm:"foreignKey:VirtualKeyID" json:"-"` // Linked VK identity (server-only, not serialized)
UserID *string `gorm:"type:varchar(255);index" json:"user_id"` // Linked enterprise user identity (set when user ID is present)
ExpiresAt time.Time `gorm:"index;not null" json:"expires_at"`
EncryptionStatus string `gorm:"type:varchar(20);default:'plain_text'" json:"-"`
CreatedAt time.Time `gorm:"index;not null" json:"created_at"`
UpdatedAt time.Time `gorm:"index;not null" json:"updated_at"`
}
// TableName returns the table name for per-user OAuth sessions.
func (TablePerUserOAuthSession) TableName() string {
return "oauth_per_user_sessions"
}
// BeforeSave encrypts sensitive fields.
func (s *TablePerUserOAuthSession) BeforeSave(tx *gorm.DB) error {
if s.AccessToken != "" {
s.AccessTokenHash = encrypt.HashSHA256(s.AccessToken)
}
if s.RefreshToken != "" {
s.RefreshTokenHash = encrypt.HashSHA256(s.RefreshToken)
}
if encrypt.IsEnabled() {
if err := encryptString(&s.AccessToken); err != nil {
return fmt.Errorf("failed to encrypt per-user oauth access token: %w", err)
}
if s.RefreshToken != "" {
if err := encryptString(&s.RefreshToken); err != nil {
return fmt.Errorf("failed to encrypt per-user oauth refresh token: %w", err)
}
}
s.EncryptionStatus = EncryptionStatusEncrypted
}
return nil
}
// AfterFind decrypts sensitive fields.
func (s *TablePerUserOAuthSession) AfterFind(tx *gorm.DB) error {
if s.EncryptionStatus == EncryptionStatusEncrypted {
if err := decryptString(&s.AccessToken); err != nil {
return fmt.Errorf("failed to decrypt per-user oauth access token: %w", err)
}
if s.RefreshToken != "" {
if err := decryptString(&s.RefreshToken); err != nil {
return fmt.Errorf("failed to decrypt per-user oauth refresh token: %w", err)
}
}
}
return nil
}
// TablePerUserOAuthCode stores authorization codes during the OAuth flow.
// Codes are short-lived (5 minutes) and single-use.
type TablePerUserOAuthCode struct {
ID string `gorm:"type:varchar(255);primaryKey" json:"id"`
Code string `gorm:"type:text;not null" json:"-"` // Authorization code
CodeHash string `gorm:"type:varchar(64);uniqueIndex" json:"-"` // SHA-256 hash for secure lookups
ClientID string `gorm:"type:varchar(255);not null;index" json:"client_id"`
RedirectURI string `gorm:"type:text;not null" json:"redirect_uri"`
CodeChallenge string `gorm:"type:varchar(255);not null" json:"-"` // PKCE S256 challenge
Scopes string `gorm:"type:text" json:"scopes"` // JSON array of requested scopes
SessionID string `gorm:"type:varchar(255);index" json:"-"` // Links to the TablePerUserOAuthSession created during consent submit
ExpiresAt time.Time `gorm:"index;not null" json:"expires_at"` // 5 min TTL
Used bool `gorm:"default:false;not null" json:"used"` // Single-use flag
CreatedAt time.Time `gorm:"index;not null" json:"created_at"`
}
// BeforeSave hashes the code for secure lookups.
func (c *TablePerUserOAuthCode) BeforeSave(tx *gorm.DB) error {
if c.Code != "" {
c.CodeHash = encrypt.HashSHA256(c.Code)
}
return nil
}
// TableName returns the table name for per-user OAuth authorization codes.
func (TablePerUserOAuthCode) TableName() string {
return "oauth_per_user_codes"
}
// TablePerUserOAuthPendingFlow stores OAuth parameters between the authorize step
// and the final code issuance. It carries state through the multi-step consent
// screen (VK entry + per-MCP upstream auth) before a real authorization code is issued.
type TablePerUserOAuthPendingFlow struct {
ID string `gorm:"type:varchar(255);primaryKey" json:"id"`
ClientID string `gorm:"type:varchar(255);not null;index" json:"client_id"` // Registered OAuth client (from authorize request)
RedirectURI string `gorm:"type:text;not null" json:"redirect_uri"` // Client's callback URL
CodeChallenge string `gorm:"type:varchar(255);not null" json:"-"` // PKCE S256 challenge (echoed into the final code)
State string `gorm:"type:text;not null" json:"-"` // Original OAuth state (echoed back on final redirect)
VirtualKeyID *string `gorm:"type:varchar(255);index" json:"virtual_key_id"` // Set if user chose VK identity
UserID *string `gorm:"type:varchar(255);index" json:"user_id"` // Set if user chose User ID identity
BrowserSecretHash string `gorm:"type:varchar(255)" json:"-"` // SHA-256 hash of browser-binding cookie secret
ExpiresAt time.Time `gorm:"index;not null" json:"expires_at"` // 15-min TTL
CreatedAt time.Time `gorm:"index;not null" json:"created_at"`
UpdatedAt time.Time `gorm:"index;not null" json:"updated_at"`
}
// TableName returns the table name for per-user OAuth pending flows.
func (TablePerUserOAuthPendingFlow) TableName() string {
return "oauth_per_user_pending_flows"
}

View File

@@ -0,0 +1,87 @@
package tables
import (
"encoding/json"
"fmt"
"time"
"github.com/maximhq/bifrost/core/schemas"
"github.com/maximhq/bifrost/framework/encrypt"
"gorm.io/gorm"
)
// TablePlugin represents a plugin configuration in the database
type TablePlugin struct {
ID uint `gorm:"primaryKey;autoIncrement" json:"id"`
Name string `gorm:"type:varchar(255);uniqueIndex;not null" json:"name"`
Enabled bool `json:"enabled"`
Path *string `json:"path,omitempty"`
ConfigJSON string `gorm:"type:text" json:"-"` // JSON serialized plugin.Config
CreatedAt time.Time `gorm:"index;not null" json:"created_at"`
Version int16 `gorm:"not null;default:1" json:"version"`
UpdatedAt time.Time `gorm:"index;not null" json:"updated_at"`
IsCustom bool `gorm:"not null;default:false" json:"isCustom"`
Placement *schemas.PluginPlacement `gorm:"column:placement;type:varchar(20);null" json:"placement,omitempty"`
Order *int `gorm:"column:exec_order;type:int;null" json:"order,omitempty"`
// Config hash is used to detect the changes synced from config.json file
// Every time we sync the config.json file, we will update the config hash
ConfigHash string `gorm:"type:varchar(255);null" json:"config_hash"`
EncryptionStatus string `gorm:"type:varchar(20);default:'plain_text'" json:"-"`
// Virtual fields for runtime use (not stored in DB)
Config any `gorm:"-" json:"config,omitempty"`
}
// TableName sets the table name for each model
func (TablePlugin) TableName() string { return "config_plugins" }
// BeforeSave is a GORM hook that serializes the plugin Config into a JSON column and
// encrypts it before writing to the database. Empty configs ("{}") are not encrypted.
func (p *TablePlugin) BeforeSave(tx *gorm.DB) error {
if p.Config != nil {
data, err := json.Marshal(p.Config)
if err != nil {
return err
}
p.ConfigJSON = string(data)
} else {
p.ConfigJSON = "{}"
}
// Encrypt config after serialization
if encrypt.IsEnabled() && p.ConfigJSON != "" && p.ConfigJSON != "{}" {
encrypted, err := encrypt.Encrypt(p.ConfigJSON)
if err != nil {
return fmt.Errorf("failed to encrypt plugin config: %w", err)
}
p.ConfigJSON = encrypted
p.EncryptionStatus = EncryptionStatusEncrypted
}
return nil
}
// AfterFind is a GORM hook that decrypts the plugin config JSON (if encrypted) and
// deserializes it back into the runtime Config field after reading from the database.
func (p *TablePlugin) AfterFind(tx *gorm.DB) error {
if p.EncryptionStatus == "encrypted" && p.ConfigJSON != "" {
decrypted, err := encrypt.Decrypt(p.ConfigJSON)
if err != nil {
return fmt.Errorf("failed to decrypt plugin config: %w", err)
}
p.ConfigJSON = decrypted
}
if p.ConfigJSON != "" {
if err := json.Unmarshal([]byte(p.ConfigJSON), &p.Config); err != nil {
return err
}
} else {
p.Config = nil
}
return nil
}

View File

@@ -0,0 +1,55 @@
package tables
import (
"encoding/json"
"time"
"github.com/maximhq/bifrost/core/schemas"
"gorm.io/gorm"
)
// TablePricingOverride is the persistence model for governance pricing overrides.
type TablePricingOverride struct {
ID string `gorm:"primaryKey;type:varchar(255)" json:"id"`
Name string `gorm:"type:varchar(255);not null" json:"name"`
ScopeKind string `gorm:"type:varchar(50);index:idx_pricing_override_scope;not null" json:"scope_kind"`
VirtualKeyID *string `gorm:"type:varchar(255);index:idx_pricing_override_scope" json:"virtual_key_id,omitempty"`
ProviderID *string `gorm:"type:varchar(255);index:idx_pricing_override_scope" json:"provider_id,omitempty"`
ProviderKeyID *string `gorm:"type:varchar(255);index:idx_pricing_override_scope" json:"provider_key_id,omitempty"`
MatchType string `gorm:"type:varchar(20);index:idx_pricing_override_match;not null" json:"match_type"`
Pattern string `gorm:"type:varchar(255);not null" json:"pattern"`
RequestTypesJSON string `gorm:"type:text" json:"-"`
PricingPatchJSON string `gorm:"type:text" json:"pricing_patch,omitempty"`
ConfigHash string `gorm:"type:varchar(255);null" json:"config_hash,omitempty"`
CreatedAt time.Time `gorm:"index;not null" json:"created_at"`
UpdatedAt time.Time `gorm:"index;not null" json:"updated_at"`
RequestTypes []schemas.RequestType `gorm:"-" json:"request_types,omitempty"`
}
// TableName returns the backing table name for governance pricing overrides.
func (TablePricingOverride) TableName() string { return "governance_pricing_overrides" }
// BeforeSave serializes virtual fields into their JSON columns before persistence.
func (p *TablePricingOverride) BeforeSave(tx *gorm.DB) error {
if len(p.RequestTypes) > 0 {
b, err := json.Marshal(p.RequestTypes)
if err != nil {
return err
}
p.RequestTypesJSON = string(b)
} else {
p.RequestTypesJSON = "[]"
}
return nil
}
// AfterFind restores virtual fields from their persisted JSON columns.
func (p *TablePricingOverride) AfterFind(tx *gorm.DB) error {
if p.RequestTypesJSON != "" {
if err := json.Unmarshal([]byte(p.RequestTypesJSON), &p.RequestTypes); err != nil {
return err
}
}
return nil
}

View File

@@ -0,0 +1,112 @@
// Package tables provides tables for the configstore
package tables
import (
"encoding/json"
"strings"
"time"
"gorm.io/gorm"
)
// TablePromptSession represents a mutable working draft/session for a prompt
// Sessions belong to a prompt and can optionally be based on a specific version
type TablePromptSession struct {
ID uint `gorm:"primaryKey;autoIncrement" json:"id"`
PromptID string `gorm:"type:varchar(36);not null;index" json:"prompt_id"`
Prompt *TablePrompt `gorm:"foreignKey:PromptID" json:"prompt,omitempty"`
VersionID *uint `gorm:"index" json:"version_id,omitempty"` // Optional - session may or may not be based on a version
Version *TablePromptVersion `gorm:"foreignKey:VersionID;constraint:OnDelete:SET NULL" json:"version,omitempty"`
Name string `gorm:"type:varchar(255)" json:"name"`
ModelParamsJSON *string `gorm:"type:text;column:model_params_json" json:"-"`
ModelParams ModelParams `gorm:"-" json:"model_params"`
Provider string `gorm:"type:varchar(100)" json:"provider"`
Model string `gorm:"type:varchar(100)" json:"model"`
VariablesJSON *string `gorm:"type:text;column:variables_json" json:"-"`
Variables PromptVariables `gorm:"-" json:"variables,omitempty"` // {key: value} map for Jinja2 variables
CreatedAt time.Time `gorm:"not null" json:"created_at"`
UpdatedAt time.Time `gorm:"not null" json:"updated_at"`
// Relationships
Messages []TablePromptSessionMessage `gorm:"foreignKey:SessionID;constraint:OnDelete:CASCADE" json:"messages,omitempty"`
}
// TableName for TablePromptSession
func (TablePromptSession) TableName() string { return "prompt_sessions" }
// BeforeSave GORM hook to serialize JSON fields
func (s *TablePromptSession) BeforeSave(tx *gorm.DB) error {
data, err := json.Marshal(s.ModelParams)
if err != nil {
return err
}
paramsStr := string(data)
s.ModelParamsJSON = &paramsStr
if s.Variables != nil {
varsData, err := json.Marshal(s.Variables)
if err != nil {
return err
}
varsStr := string(varsData)
s.VariablesJSON = &varsStr
} else {
s.VariablesJSON = nil
}
return nil
}
// AfterFind GORM hook to deserialize JSON fields
func (s *TablePromptSession) AfterFind(tx *gorm.DB) error {
if s.ModelParamsJSON != nil && *s.ModelParamsJSON != "" {
dec := json.NewDecoder(strings.NewReader(*s.ModelParamsJSON))
dec.UseNumber()
if err := dec.Decode(&s.ModelParams); err != nil {
return err
}
}
if s.VariablesJSON != nil && *s.VariablesJSON != "" {
var vars PromptVariables
if err := json.Unmarshal([]byte(*s.VariablesJSON), &vars); err != nil {
return err
}
s.Variables = vars
} else {
s.Variables = nil
}
return nil
}
// TablePromptSessionMessage represents a message in a mutable prompt session
type TablePromptSessionMessage struct {
ID uint `gorm:"primaryKey;autoIncrement" json:"id"`
PromptID string `gorm:"type:varchar(36);not null;index" json:"prompt_id"`
SessionID uint `gorm:"not null;index;uniqueIndex:idx_session_order" json:"session_id"`
Session *TablePromptSession `gorm:"foreignKey:SessionID" json:"-"`
OrderIndex int `gorm:"not null;uniqueIndex:idx_session_order" json:"order_index"`
MessageJSON string `gorm:"type:text;not null;column:message_json" json:"-"`
Message PromptMessage `gorm:"-" json:"message"`
}
// TableName for TablePromptSessionMessage
func (TablePromptSessionMessage) TableName() string { return "prompt_session_messages" }
// BeforeSave GORM hook to serialize JSON fields
func (m *TablePromptSessionMessage) BeforeSave(tx *gorm.DB) error {
data, err := json.Marshal(m.Message)
if err != nil {
return err
}
m.MessageJSON = string(data)
return nil
}
// AfterFind GORM hook to deserialize JSON fields
func (m *TablePromptSessionMessage) AfterFind(tx *gorm.DB) error {
if m.MessageJSON != "" {
if err := json.Unmarshal([]byte(m.MessageJSON), &m.Message); err != nil {
return err
}
}
return nil
}

View File

@@ -0,0 +1,120 @@
// Package tables provides tables for the configstore
package tables
import (
"encoding/json"
"strings"
"time"
"gorm.io/gorm"
)
// TablePromptVersion represents an immutable version of a prompt
// Once created, a version cannot be modified - to make changes, create a new version
type TablePromptVersion struct {
ID uint `gorm:"primaryKey;autoIncrement" json:"id"`
PromptID string `gorm:"type:varchar(36);not null;index;uniqueIndex:idx_prompt_version" json:"prompt_id"`
Prompt *TablePrompt `gorm:"foreignKey:PromptID" json:"prompt,omitempty"`
VersionNumber int `gorm:"not null;uniqueIndex:idx_prompt_version" json:"version_number"`
CommitMessage string `gorm:"type:text" json:"commit_message"`
ModelParamsJSON *string `gorm:"type:text;column:model_params_json" json:"-"`
ModelParams ModelParams `gorm:"-" json:"model_params"`
Provider string `gorm:"type:varchar(100)" json:"provider"`
Model string `gorm:"type:varchar(100)" json:"model"`
VariablesJSON *string `gorm:"type:text;column:variables_json" json:"-"`
Variables PromptVariables `gorm:"-" json:"variables,omitempty"` // {key: value} map for Jinja2 variables
IsLatest bool `gorm:"not null;default:false" json:"is_latest"`
CreatedAt time.Time `gorm:"not null" json:"created_at"`
// No UpdatedAt - versions are immutable
// Relationships
Messages []TablePromptVersionMessage `gorm:"foreignKey:VersionID;constraint:OnDelete:CASCADE" json:"messages,omitempty"`
}
// TableName for TablePromptVersion
func (TablePromptVersion) TableName() string { return "prompt_versions" }
// ModelParams represents model configuration parameters as a flexible map
// so that any provider-specific params (response_format, seed, logprobs, etc.) are preserved.
type ModelParams map[string]interface{}
// PromptVariables represents a map of Jinja2 variable names to their values.
// Sessions store full {key: value} pairs; versions store {key: ""} (keys only).
type PromptVariables map[string]string
// BeforeSave GORM hook to serialize JSON fields
func (v *TablePromptVersion) BeforeSave(tx *gorm.DB) error {
if v.ModelParams != nil {
data, err := json.Marshal(v.ModelParams)
if err != nil {
return err
}
paramsStr := string(data)
v.ModelParamsJSON = &paramsStr
}
if v.Variables != nil {
varsData, err := json.Marshal(v.Variables)
if err != nil {
return err
}
varsStr := string(varsData)
v.VariablesJSON = &varsStr
}
return nil
}
// AfterFind GORM hook to deserialize JSON fields
func (v *TablePromptVersion) AfterFind(tx *gorm.DB) error {
if v.ModelParamsJSON != nil && *v.ModelParamsJSON != "" {
dec := json.NewDecoder(strings.NewReader(*v.ModelParamsJSON))
dec.UseNumber()
if err := dec.Decode(&v.ModelParams); err != nil {
return err
}
}
if v.VariablesJSON != nil && *v.VariablesJSON != "" {
if err := json.Unmarshal([]byte(*v.VariablesJSON), &v.Variables); err != nil {
return err
}
}
return nil
}
// TablePromptVersionMessage represents a message in an immutable prompt version
type TablePromptVersionMessage struct {
ID uint `gorm:"primaryKey;autoIncrement" json:"id"`
PromptID string `gorm:"type:varchar(36);not null;index" json:"prompt_id"`
VersionID uint `gorm:"not null;index;uniqueIndex:idx_version_order" json:"version_id"`
Version *TablePromptVersion `gorm:"foreignKey:VersionID" json:"-"`
OrderIndex int `gorm:"not null;uniqueIndex:idx_version_order" json:"order_index"`
MessageJSON string `gorm:"type:text;not null;column:message_json" json:"-"`
Message PromptMessage `gorm:"-" json:"message"`
}
// TableName for TablePromptVersionMessage
func (TablePromptVersionMessage) TableName() string { return "prompt_version_messages" }
// PromptMessage is a raw JSON message stored in the database.
// The frontend handles serialization/deserialization of the message format.
// The backend treats it as opaque JSON to remain format-agnostic and backward-compatible.
type PromptMessage = json.RawMessage
// BeforeSave GORM hook to serialize JSON fields
func (m *TablePromptVersionMessage) BeforeSave(tx *gorm.DB) error {
data, err := json.Marshal(m.Message)
if err != nil {
return err
}
m.MessageJSON = string(data)
return nil
}
// AfterFind GORM hook to deserialize JSON fields
func (m *TablePromptVersionMessage) AfterFind(tx *gorm.DB) error {
if m.MessageJSON != "" {
if err := json.Unmarshal([]byte(m.MessageJSON), &m.Message); err != nil {
return err
}
}
return nil
}

View File

@@ -0,0 +1,27 @@
// Package tables provides tables for the configstore
package tables
import (
"time"
)
// TablePrompt represents a prompt entity that can have multiple versions and sessions
type TablePrompt struct {
ID string `gorm:"type:varchar(36);primaryKey" json:"id"`
Name string `gorm:"type:varchar(255);not null" json:"name"`
FolderID *string `gorm:"type:varchar(36);index" json:"folder_id,omitempty"`
Folder *TableFolder `gorm:"foreignKey:FolderID;constraint:OnDelete:CASCADE" json:"folder,omitempty"`
CreatedAt time.Time `gorm:"not null" json:"created_at"`
UpdatedAt time.Time `gorm:"not null" json:"updated_at"`
ConfigHash string `gorm:"type:varchar(64)" json:"-"`
// Relationships
Versions []TablePromptVersion `gorm:"foreignKey:PromptID;constraint:OnDelete:CASCADE" json:"versions,omitempty"`
Sessions []TablePromptSession `gorm:"foreignKey:PromptID;constraint:OnDelete:CASCADE" json:"sessions,omitempty"`
// Virtual fields (not stored in DB)
LatestVersion *TablePromptVersion `gorm:"-" json:"latest_version,omitempty"`
}
// TableName for TablePrompt
func (TablePrompt) TableName() string { return "prompts" }

View File

@@ -0,0 +1,184 @@
package tables
import (
"encoding/json"
"fmt"
"strings"
"time"
"github.com/maximhq/bifrost/core/schemas"
"github.com/maximhq/bifrost/framework/encrypt"
"gorm.io/gorm"
)
// TableProvider represents a provider configuration in the database
// NOTE: Any changes to the provider configuration should be reflected in the GenerateConfigHash function
// That helps us detect changes between config file and database config
type TableProvider struct {
ID uint `gorm:"primaryKey;autoIncrement" json:"id"`
Name string `gorm:"type:varchar(50);uniqueIndex;not null" json:"name"` // ModelProvider as string
NetworkConfigJSON string `gorm:"type:text" json:"-"` // JSON serialized schemas.NetworkConfig
ConcurrencyBufferJSON string `gorm:"type:text" json:"-"` // JSON serialized schemas.ConcurrencyAndBufferSize
ProxyConfigJSON string `gorm:"type:text" json:"-"` // JSON serialized schemas.ProxyConfig
CustomProviderConfigJSON string `gorm:"type:text" json:"-"` // JSON serialized schemas.CustomProviderConfig
OpenAIConfigJSON string `gorm:"type:text" json:"-"` // JSON serialized schemas.OpenAIConfig
SendBackRawRequest bool `json:"send_back_raw_request"`
SendBackRawResponse bool `json:"send_back_raw_response"`
StoreRawRequestResponse bool `json:"store_raw_request_response"`
CreatedAt time.Time `gorm:"index;not null" json:"created_at"`
UpdatedAt time.Time `gorm:"index;not null" json:"updated_at"`
// Relationships
Keys []TableKey `gorm:"foreignKey:ProviderID;constraint:OnDelete:CASCADE" json:"keys"`
// Virtual fields for runtime use (not stored in DB)
NetworkConfig *schemas.NetworkConfig `gorm:"-" json:"network_config,omitempty"`
ConcurrencyAndBufferSize *schemas.ConcurrencyAndBufferSize `gorm:"-" json:"concurrency_and_buffer_size,omitempty"`
ProxyConfig *schemas.ProxyConfig `gorm:"-" json:"proxy_config,omitempty"`
// Custom provider fields
CustomProviderConfig *schemas.CustomProviderConfig `gorm:"-" json:"custom_provider_config,omitempty"`
OpenAIConfig *schemas.OpenAIConfig `gorm:"-" json:"openai_config,omitempty"`
// Foreign keys
Models []TableModel `gorm:"foreignKey:ProviderID;constraint:OnDelete:CASCADE" json:"models"`
// Governance fields - Budget and Rate Limit for provider-level governance
BudgetID *string `gorm:"type:varchar(255);index:idx_provider_budget" json:"budget_id,omitempty"`
RateLimitID *string `gorm:"type:varchar(255);index:idx_provider_rate_limit" json:"rate_limit_id,omitempty"`
// Governance relationships
Budget *TableBudget `gorm:"foreignKey:BudgetID;onDelete:CASCADE" json:"budget,omitempty"`
RateLimit *TableRateLimit `gorm:"foreignKey:RateLimitID;onDelete:CASCADE" json:"rate_limit,omitempty"`
// Config hash is used to detect the changes synced from config.json file
// Every time we sync the config.json file, we will update the config hash
ConfigHash string `gorm:"type:varchar(255);null" json:"config_hash"`
// Model discovery status tracking for keyless providers
Status string `gorm:"type:varchar(50);default:'unknown'" json:"status"`
Description string `gorm:"type:text" json:"description,omitempty"`
EncryptionStatus string `gorm:"type:varchar(20);default:'plain_text'" json:"-"`
}
// TableName represents a provider configuration in the database
func (TableProvider) TableName() string { return "config_providers" }
// BeforeSave is a GORM hook that serializes runtime config structs into JSON columns,
// validates governance fields, and encrypts the proxy configuration before writing
// to the database.
func (p *TableProvider) BeforeSave(tx *gorm.DB) error {
if p.NetworkConfig != nil {
data, err := json.Marshal(p.NetworkConfig)
if err != nil {
return err
}
p.NetworkConfigJSON = string(data)
}
if p.ConcurrencyAndBufferSize != nil {
data, err := json.Marshal(p.ConcurrencyAndBufferSize)
if err != nil {
return err
}
p.ConcurrencyBufferJSON = string(data)
}
if p.ProxyConfig != nil {
data, err := json.Marshal(p.ProxyConfig)
if err != nil {
return err
}
p.ProxyConfigJSON = string(data)
}
if p.CustomProviderConfig != nil && p.CustomProviderConfig.BaseProviderType == "" {
return fmt.Errorf("base_provider_type is required when custom_provider_config is set")
}
if p.CustomProviderConfig != nil {
data, err := json.Marshal(p.CustomProviderConfig)
if err != nil {
return err
}
p.CustomProviderConfigJSON = string(data)
}
if p.OpenAIConfig != nil {
data, err := json.Marshal(p.OpenAIConfig)
if err != nil {
return err
}
p.OpenAIConfigJSON = string(data)
} else {
p.OpenAIConfigJSON = ""
}
// Validate governance fields
if p.BudgetID != nil && strings.TrimSpace(*p.BudgetID) == "" {
return fmt.Errorf("budget_id cannot be an empty string")
}
if p.RateLimitID != nil && strings.TrimSpace(*p.RateLimitID) == "" {
return fmt.Errorf("rate_limit_id cannot be an empty string")
}
// Encrypt proxy config after serialization (only if there's data to encrypt)
if encrypt.IsEnabled() && p.ProxyConfigJSON != "" {
encrypted, err := encrypt.Encrypt(p.ProxyConfigJSON)
if err != nil {
return fmt.Errorf("failed to encrypt proxy config: %w", err)
}
p.ProxyConfigJSON = encrypted
p.EncryptionStatus = EncryptionStatusEncrypted
}
return nil
}
// AfterFind is a GORM hook that decrypts the proxy configuration (if encrypted) and
// deserializes JSON columns back into runtime config structs after reading from the database.
func (p *TableProvider) AfterFind(tx *gorm.DB) error {
if p.NetworkConfigJSON != "" {
var config schemas.NetworkConfig
if err := json.Unmarshal([]byte(p.NetworkConfigJSON), &config); err != nil {
return err
}
p.NetworkConfig = &config
}
if p.ConcurrencyBufferJSON != "" {
var config schemas.ConcurrencyAndBufferSize
if err := json.Unmarshal([]byte(p.ConcurrencyBufferJSON), &config); err != nil {
return err
}
p.ConcurrencyAndBufferSize = &config
}
if p.EncryptionStatus == "encrypted" && p.ProxyConfigJSON != "" {
decrypted, err := encrypt.Decrypt(p.ProxyConfigJSON)
if err != nil {
return fmt.Errorf("failed to decrypt proxy config: %w", err)
}
p.ProxyConfigJSON = decrypted
}
if p.ProxyConfigJSON != "" {
var proxyConfig schemas.ProxyConfig
if err := json.Unmarshal([]byte(p.ProxyConfigJSON), &proxyConfig); err != nil {
return err
}
p.ProxyConfig = &proxyConfig
}
if p.CustomProviderConfigJSON != "" {
var customConfig schemas.CustomProviderConfig
if err := json.Unmarshal([]byte(p.CustomProviderConfigJSON), &customConfig); err != nil {
return err
}
p.CustomProviderConfig = &customConfig
}
if p.OpenAIConfigJSON != "" {
var openaiConfig schemas.OpenAIConfig
if err := json.Unmarshal([]byte(p.OpenAIConfigJSON), &openaiConfig); err != nil {
return err
}
p.OpenAIConfig = &openaiConfig
}
return nil
}

View File

@@ -0,0 +1,79 @@
package tables
import (
"fmt"
"time"
"gorm.io/gorm"
)
// TableRateLimit defines rate limiting rules for virtual keys using flexible max+reset approach
type TableRateLimit struct {
ID string `gorm:"primaryKey;type:varchar(255)" json:"id"`
// Token limits with flexible duration
TokenMaxLimit *int64 `gorm:"default:null" json:"token_max_limit,omitempty"` // Maximum tokens allowed
TokenResetDuration *string `gorm:"type:varchar(50)" json:"token_reset_duration,omitempty"` // e.g., "30s", "5m", "1h", "1d", "1w", "1M", "1Y"
TokenCurrentUsage int64 `gorm:"default:0" json:"token_current_usage"` // Current token usage
TokenLastReset time.Time `gorm:"index" json:"token_last_reset"` // Last time token counter was reset
// Request limits with flexible duration
RequestMaxLimit *int64 `gorm:"default:null" json:"request_max_limit,omitempty"` // Maximum requests allowed
RequestResetDuration *string `gorm:"type:varchar(50)" json:"request_reset_duration,omitempty"` // e.g., "30s", "5m", "1h", "1d", "1w", "1M", "1Y"
RequestCurrentUsage int64 `gorm:"default:0" json:"request_current_usage"` // Current request usage
RequestLastReset time.Time `gorm:"index" json:"request_last_reset"` // Last time request counter was reset
CalendarAligned bool `gorm:"default:false" json:"calendar_aligned"` // When true, all budgets under this VK reset at clean calendar boundaries
// Config hash is used to detect the changes synced from config.json file
// Every time we sync the config.json file, we will update the config hash
ConfigHash string `gorm:"type:varchar(255);null" json:"config_hash"`
CreatedAt time.Time `gorm:"index;not null" json:"created_at"`
UpdatedAt time.Time `gorm:"index;not null" json:"updated_at"`
}
// TableName sets the table name for each model
func (TableRateLimit) TableName() string { return "governance_rate_limits" }
// BeforeSave hook for RateLimit to validate reset duration formats
func (rl *TableRateLimit) BeforeSave(tx *gorm.DB) error {
// Validate token reset duration if provided
if rl.TokenResetDuration != nil {
if d, err := ParseDuration(*rl.TokenResetDuration); err != nil {
return fmt.Errorf("invalid token reset duration format: %s", *rl.TokenResetDuration)
} else if d <= 0 {
return fmt.Errorf("token reset duration cannot be zero or negative: %s", *rl.TokenResetDuration)
}
}
// Validate request reset duration if provided
if rl.RequestResetDuration != nil {
if d, err := ParseDuration(*rl.RequestResetDuration); err != nil {
return fmt.Errorf("invalid request reset duration format: %s", *rl.RequestResetDuration)
} else if d <= 0 {
return fmt.Errorf("request reset duration cannot be zero or negative: %s", *rl.RequestResetDuration)
}
}
// Validate that if a max limit is set, a reset duration is also provided
if rl.TokenMaxLimit != nil && rl.TokenResetDuration == nil {
return fmt.Errorf("token_reset_duration is required when token_max_limit is set")
}
if rl.RequestMaxLimit != nil && rl.RequestResetDuration == nil {
return fmt.Errorf("request_reset_duration is required when request_max_limit is set")
}
// Making sure token limit is greater than zero
if rl.TokenMaxLimit != nil && *rl.TokenMaxLimit <= 0 {
return fmt.Errorf("token_max_limit cannot be zero or negative: %d", *rl.TokenMaxLimit)
}
// Making sure request limit is greater than zero
if rl.RequestMaxLimit != nil && *rl.RequestMaxLimit <= 0 {
return fmt.Errorf("request_max_limit cannot be zero or negative: %d", *rl.RequestMaxLimit)
}
return nil
}

View File

@@ -0,0 +1,99 @@
package tables
import (
"strings"
"time"
"github.com/bytedance/sonic"
bifrost "github.com/maximhq/bifrost/core"
"gorm.io/gorm"
)
// TableRoutingRule represents a routing rule in the database
type TableRoutingRule struct {
ID string `gorm:"primaryKey;type:varchar(255)" json:"id"`
ConfigHash string `gorm:"type:varchar(255)" json:"config_hash"` // Hash of config.json version, used for change detection
Name string `gorm:"type:varchar(255);not null;uniqueIndex:idx_routing_rule_scope_name" json:"name"`
Description string `gorm:"type:text" json:"description"`
Enabled bool `gorm:"not null;default:true" json:"enabled"`
CelExpression string `gorm:"type:text;not null" json:"cel_expression"`
// Routing Targets (output) — 1:many relationship; weights must sum to 1
Targets []TableRoutingTarget `gorm:"foreignKey:RuleID;constraint:OnDelete:CASCADE" json:"targets"`
Fallbacks *string `gorm:"type:text" json:"-"` // JSON array of fallback chains
ParsedFallbacks []string `gorm:"-" json:"fallbacks,omitempty"` // Parsed fallbacks from JSON
Query *string `gorm:"type:text" json:"-"`
ParsedQuery map[string]any `gorm:"-" json:"query,omitempty"`
// Scope: where this rule applies
Scope string `gorm:"type:varchar(50);not null;uniqueIndex:idx_routing_rule_scope_name" json:"scope"` // "global" | "team" | "customer" | "virtual_key"
ScopeID *string `gorm:"type:varchar(255);uniqueIndex:idx_routing_rule_scope_name" json:"scope_id"` // nil for global, otherwise entity ID
// Chaining
ChainRule bool `gorm:"not null;default:false" json:"chain_rule"` // If true, re-evaluates routing chain after this rule matches
// Execution
Priority int `gorm:"type:int;not null;default:0;index" json:"priority"` // Lower = evaluated first within scope
// Timestamps
CreatedAt time.Time `gorm:"index;not null" json:"created_at"`
UpdatedAt time.Time `gorm:"index;not null" json:"updated_at"`
}
// TableName for TableRoutingRule
func (TableRoutingRule) TableName() string { return "routing_rules" }
// BeforeSave hook for TableRoutingRule to serialize JSON fields
func (r *TableRoutingRule) BeforeSave(tx *gorm.DB) error {
if len(r.ParsedFallbacks) > 0 {
data, err := sonic.Marshal(r.ParsedFallbacks)
if err != nil {
return err
}
r.Fallbacks = bifrost.Ptr(string(data))
} else {
r.Fallbacks = nil
}
if r.ParsedQuery != nil {
data, err := sonic.Marshal(r.ParsedQuery)
if err != nil {
return err
}
r.Query = bifrost.Ptr(string(data))
} else {
r.Query = nil
}
return nil
}
// AfterFind hook for TableRoutingRule to deserialize JSON fields
func (r *TableRoutingRule) AfterFind(tx *gorm.DB) error {
if r.Fallbacks != nil && strings.TrimSpace(*r.Fallbacks) != "" {
if err := sonic.Unmarshal([]byte(*r.Fallbacks), &r.ParsedFallbacks); err != nil {
return err
}
}
if r.Query != nil && strings.TrimSpace(*r.Query) != "" {
if err := sonic.Unmarshal([]byte(*r.Query), &r.ParsedQuery); err != nil {
return err
}
}
return nil
}
// TableRoutingTarget represents a weighted routing target for probabilistic routing.
// Multiple targets can be associated with a single routing rule; weights determine
// the probability of each target being selected and must sum to 1 across all targets in a rule.
// The composite (RuleID, Provider, Model, KeyID) is unique to prevent duplicate target configs.
type TableRoutingTarget struct {
RuleID string `gorm:"type:varchar(255);not null;index;uniqueIndex:idx_routing_target_config" json:"-"`
Provider *string `gorm:"type:varchar(255);uniqueIndex:idx_routing_target_config" json:"provider,omitempty"` // nil = use incoming provider
Model *string `gorm:"type:varchar(255);uniqueIndex:idx_routing_target_config" json:"model,omitempty"` // nil = use incoming model
KeyID *string `gorm:"type:varchar(255);uniqueIndex:idx_routing_target_config" json:"key_id,omitempty"` // nil = no key pin
Weight float64 `gorm:"not null;default:1" json:"weight"` // must sum to 1 across all targets in a rule
}
// TableName for TableRoutingTarget
func (TableRoutingTarget) TableName() string { return "routing_targets" }

View File

@@ -0,0 +1,48 @@
package tables
import (
"fmt"
"time"
"github.com/maximhq/bifrost/framework/encrypt"
"gorm.io/gorm"
)
// SessionsTable represents a session in the database
type SessionsTable struct {
ID int `gorm:"primaryKey;autoIncrement" json:"id"`
Token string `gorm:"type:text;not null;uniqueIndex" json:"token"`
ExpiresAt time.Time `gorm:"index;not null" json:"expires_at,omitempty"`
CreatedAt time.Time `gorm:"index;not null" json:"created_at"`
UpdatedAt time.Time `gorm:"index;not null" json:"updated_at"`
EncryptionStatus string `gorm:"type:varchar(20);default:'plain_text'" json:"-"`
TokenHash string `gorm:"type:varchar(64);index:idx_session_token_hash,unique" json:"-"`
}
// TableName sets the table name for each model
func (SessionsTable) TableName() string { return "sessions" }
// BeforeSave hook to hash and encrypt the session token
func (s *SessionsTable) BeforeSave(tx *gorm.DB) error {
// Hash must be computed before encryption (from plaintext value)
if s.Token != "" {
s.TokenHash = encrypt.HashSHA256(s.Token)
}
if encrypt.IsEnabled() && s.Token != "" {
if err := encryptString(&s.Token); err != nil {
return fmt.Errorf("failed to encrypt session token: %w", err)
}
s.EncryptionStatus = EncryptionStatusEncrypted
}
return nil
}
// AfterFind hook to decrypt the session token
func (s *SessionsTable) AfterFind(tx *gorm.DB) error {
if s.EncryptionStatus == EncryptionStatusEncrypted {
if err := decryptString(&s.Token); err != nil {
return fmt.Errorf("failed to decrypt session token: %w", err)
}
}
return nil
}

View File

@@ -0,0 +1,96 @@
package tables
import (
"encoding/json"
"time"
"gorm.io/gorm"
)
// TableTeam represents a team entity with budget, rate limit and customer association
type TableTeam struct {
ID string `gorm:"primaryKey;type:varchar(255)" json:"id"`
Name string `gorm:"type:varchar(255);not null" json:"name"`
CustomerID *string `gorm:"type:varchar(255);index" json:"customer_id,omitempty"` // A team can belong to a customer
RateLimitID *string `gorm:"type:varchar(255);index" json:"rate_limit_id,omitempty"`
// Relationships
Customer *TableCustomer `gorm:"foreignKey:CustomerID" json:"customer,omitempty"`
Budgets []TableBudget `gorm:"foreignKey:TeamID;constraint:OnDelete:CASCADE" json:"budgets,omitempty"` // Multiple budgets with different reset intervals
RateLimit *TableRateLimit `gorm:"foreignKey:RateLimitID" json:"rate_limit,omitempty"`
VirtualKeys []TableVirtualKey `gorm:"foreignKey:TeamID" json:"virtual_keys,omitempty"`
// Computed (not a DB column) — populated via correlated subquery in query layer, hence no migration
VirtualKeyCount int64 `gorm:"->;-:migration" json:"virtual_key_count"`
Profile *string `gorm:"type:text" json:"-"`
ParsedProfile map[string]any `gorm:"-" json:"profile"`
Config *string `gorm:"type:text" json:"-"`
ParsedConfig map[string]any `gorm:"-" json:"config"`
Claims *string `gorm:"type:text" json:"-"`
ParsedClaims map[string]any `gorm:"-" json:"claims"`
// Config hash is used to detect the changes synced from config.json file
// Every time we sync the config.json file, we will update the config hash
ConfigHash string `gorm:"type:varchar(255);null" json:"config_hash"`
CreatedAt time.Time `gorm:"index;not null" json:"created_at"`
UpdatedAt time.Time `gorm:"index;not null" json:"updated_at"`
}
// TableName sets the table name for each model
func (TableTeam) TableName() string { return "governance_teams" }
// BeforeSave hook for TableTeam to serialize JSON fields
func (t *TableTeam) BeforeSave(tx *gorm.DB) error {
if t.ParsedProfile != nil {
data, err := json.Marshal(t.ParsedProfile)
if err != nil {
return err
}
t.Profile = new(string(data))
} else {
t.Profile = nil
}
if t.ParsedConfig != nil {
data, err := json.Marshal(t.ParsedConfig)
if err != nil {
return err
}
t.Config = new(string(data))
} else {
t.Config = nil
}
if t.ParsedClaims != nil {
data, err := json.Marshal(t.ParsedClaims)
if err != nil {
return err
}
t.Claims = new(string(data))
} else {
t.Claims = nil
}
return nil
}
// AfterFind hook for TableTeam to deserialize JSON fields
func (t *TableTeam) AfterFind(tx *gorm.DB) error {
if t.Profile != nil {
if err := json.Unmarshal([]byte(*t.Profile), &t.ParsedProfile); err != nil {
return err
}
}
if t.Config != nil {
if err := json.Unmarshal([]byte(*t.Config), &t.ParsedConfig); err != nil {
return err
}
}
if t.Claims != nil {
if err := json.Unmarshal([]byte(*t.Claims), &t.ParsedClaims); err != nil {
return err
}
}
return nil
}

View File

@@ -0,0 +1,91 @@
package tables
import (
"fmt"
"time"
)
// IsCalendarAlignableDuration reports whether the given duration string supports calendar-aligned resets.
// Only day ("d"), week ("w"), month ("M"), and year ("Y") suffixes have natural calendar boundaries.
// Sub-day durations like "1h", "30m" are not alignable.
func IsCalendarAlignableDuration(duration string) bool {
if duration == "" {
return false
}
switch duration[len(duration)-1] {
case 'd', 'w', 'M', 'Y':
return true
default:
return false
}
}
// GetCalendarPeriodStart returns the start of the current calendar period for the given duration and time.
// For calendar-scale durations (daily, weekly, monthly, yearly) it snaps to clean boundaries in UTC:
// - "Nd" → midnight UTC on the current day
// - "Nw" → midnight UTC on the most recent Monday
// - "NM" → midnight UTC on the 1st of the current month
// - "NY" → midnight UTC on Jan 1 of the current year
//
// For all other durations (e.g. "1h", "30m") the original time t is returned unchanged,
// since sub-day periods don't have a natural calendar boundary.
func GetCalendarPeriodStart(duration string, t time.Time) time.Time {
if duration == "" {
return t
}
t = t.UTC()
suffix := duration[len(duration)-1:]
switch suffix {
case "d":
return time.Date(t.Year(), t.Month(), t.Day(), 0, 0, 0, 0, time.UTC)
case "w":
weekday := int(t.Weekday())
// Sunday = 0, so shift to Monday = 0
daysFromMonday := (weekday + 6) % 7
monday := t.AddDate(0, 0, -daysFromMonday)
return time.Date(monday.Year(), monday.Month(), monday.Day(), 0, 0, 0, 0, time.UTC)
case "M":
return time.Date(t.Year(), t.Month(), 1, 0, 0, 0, 0, time.UTC)
case "Y":
return time.Date(t.Year(), time.January, 1, 0, 0, 0, 0, time.UTC)
default:
return t
}
}
// ParseDuration function to parse duration strings
func ParseDuration(duration string) (time.Duration, error) {
if duration == "" {
return 0, fmt.Errorf("duration is empty")
}
// Handle special cases for days, weeks, months, years
switch {
case duration[len(duration)-1:] == "d":
days := duration[:len(duration)-1]
if d, err := time.ParseDuration(days + "h"); err == nil {
return d * 24, nil
}
return 0, fmt.Errorf("invalid day duration: %s", duration)
case duration[len(duration)-1:] == "w":
weeks := duration[:len(duration)-1]
if w, err := time.ParseDuration(weeks + "h"); err == nil {
return w * 24 * 7, nil
}
return 0, fmt.Errorf("invalid week duration: %s", duration)
case duration[len(duration)-1:] == "M":
months := duration[:len(duration)-1]
if m, err := time.ParseDuration(months + "h"); err == nil {
return m * 24 * 30, nil // Approximate month as 30 days
}
return 0, fmt.Errorf("invalid month duration: %s", duration)
case duration[len(duration)-1:] == "Y":
years := duration[:len(duration)-1]
if y, err := time.ParseDuration(years + "h"); err == nil {
return y * 24 * 365, nil // Approximate year as 365 days
}
return 0, fmt.Errorf("invalid year duration: %s", duration)
default:
return time.ParseDuration(duration)
}
}

View File

@@ -0,0 +1,47 @@
package tables
import (
"fmt"
"time"
"github.com/maximhq/bifrost/framework/encrypt"
"gorm.io/gorm"
)
// TableVectorStoreConfig represents Cache plugin configuration in the database
type TableVectorStoreConfig struct {
ID uint `gorm:"primaryKey;autoIncrement" json:"id"`
Enabled bool `json:"enabled"` // Enable vector store
Type string `gorm:"type:varchar(50);not null" json:"type"` // "weaviate, redis, qdrant."
TTLSeconds int `gorm:"default:300" json:"ttl_seconds"` // TTL in seconds (default: 5 minutes)
CacheByModel bool `gorm:"" json:"cache_by_model"` // Include model in cache key
CacheByProvider bool `gorm:"" json:"cache_by_provider"` // Include provider in cache key
Config *string `gorm:"type:text" json:"config"` // JSON serialized schemas.RedisVectorStoreConfig
EncryptionStatus string `gorm:"type:varchar(20);default:'plain_text'" json:"-"`
CreatedAt time.Time `gorm:"index;not null" json:"created_at"`
UpdatedAt time.Time `gorm:"index;not null" json:"updated_at"`
}
// TableName sets the table name for each model
func (TableVectorStoreConfig) TableName() string { return "config_vector_store" }
// BeforeSave hook to encrypt sensitive config
func (vs *TableVectorStoreConfig) BeforeSave(tx *gorm.DB) error {
if encrypt.IsEnabled() && vs.Config != nil && *vs.Config != "" {
if err := encryptString(vs.Config); err != nil {
return fmt.Errorf("failed to encrypt vector store config: %w", err)
}
vs.EncryptionStatus = EncryptionStatusEncrypted
}
return nil
}
// AfterFind hook to decrypt sensitive config
func (vs *TableVectorStoreConfig) AfterFind(tx *gorm.DB) error {
if vs.EncryptionStatus == EncryptionStatusEncrypted && vs.Config != nil && *vs.Config != "" {
if err := decryptString(vs.Config); err != nil {
return fmt.Errorf("failed to decrypt vector store config: %w", err)
}
}
return nil
}

View File

@@ -0,0 +1,269 @@
package tables
import (
"encoding/json"
"fmt"
"time"
"github.com/maximhq/bifrost/core/schemas"
"github.com/maximhq/bifrost/framework/encrypt"
"gorm.io/gorm"
)
// TableVirtualKeyProviderConfigKey is the join table for the many2many relationship
// between TableVirtualKeyProviderConfig and TableKey
type TableVirtualKeyProviderConfigKey struct {
TableVirtualKeyProviderConfigID uint `gorm:"primaryKey;uniqueIndex:idx_vk_provider_config_key"`
TableKeyID uint `gorm:"primaryKey;uniqueIndex:idx_vk_provider_config_key"`
}
// TableName sets the table name for the join table
func (TableVirtualKeyProviderConfigKey) TableName() string {
return "governance_virtual_key_provider_config_keys"
}
// TableVirtualKeyProviderConfig represents a provider configuration for a virtual key
type TableVirtualKeyProviderConfig struct {
ID uint `gorm:"primaryKey;autoIncrement" json:"id"`
VirtualKeyID string `gorm:"type:varchar(255);not null" json:"virtual_key_id"`
Provider string `gorm:"type:varchar(50);not null" json:"provider"`
Weight *float64 `json:"weight"`
AllowedModels schemas.WhiteList `gorm:"type:text;serializer:json" json:"allowed_models"` // ["*"] allows all models; empty denies all (deny-by-default)
AllowAllKeys bool `gorm:"default:false" json:"allow_all_keys"` // True means all keys allowed; false with empty Keys means no keys allowed (deny-by-default)
RateLimitID *string `gorm:"type:varchar(255);index" json:"rate_limit_id,omitempty"`
// Relationships
RateLimit *TableRateLimit `gorm:"foreignKey:RateLimitID;onDelete:CASCADE" json:"rate_limit,omitempty"`
Budgets []TableBudget `gorm:"foreignKey:ProviderConfigID;constraint:OnDelete:CASCADE" json:"budgets,omitempty"` // Multiple budgets with different reset intervals
Keys []TableKey `gorm:"many2many:governance_virtual_key_provider_config_keys;constraint:OnDelete:CASCADE" json:"keys"` // Empty means all keys allowed for this provider
}
// TableName sets the table name for each model
func (TableVirtualKeyProviderConfig) TableName() string {
return "governance_virtual_key_provider_configs"
}
// UnmarshalJSON custom unmarshaller to handle "key_ids" ([]string) config-file format
func (pc *TableVirtualKeyProviderConfig) UnmarshalJSON(data []byte) error {
type Alias TableVirtualKeyProviderConfig
type TempProviderConfig struct {
Alias
KeyIDs []string `json:"key_ids"` // Config file format: key identifiers (TableKey.KeyID); use ["*"] to allow all keys, empty denies all
}
var temp TempProviderConfig
if err := json.Unmarshal(data, &temp); err != nil {
return err
}
// Copy all standard fields
*pc = TableVirtualKeyProviderConfig(temp.Alias)
// If key_ids is provided, convert to Keys or set AllowAllKeys
if len(temp.KeyIDs) > 0 && len(pc.Keys) == 0 {
// ["*"] means allow all keys
if len(temp.KeyIDs) == 1 && temp.KeyIDs[0] == "*" {
pc.AllowAllKeys = true
pc.Keys = nil
} else {
pc.AllowAllKeys = false
pc.Keys = make([]TableKey, len(temp.KeyIDs))
for i, keyID := range temp.KeyIDs {
pc.Keys[i] = TableKey{KeyID: keyID}
}
}
}
return nil
}
// BeforeSave validates WhiteList fields before GORM persists the record.
func (pc *TableVirtualKeyProviderConfig) BeforeSave(tx *gorm.DB) error {
if err := pc.AllowedModels.Validate(); err != nil {
return fmt.Errorf("invalid allowed_models: %w", err)
}
return nil
}
// MarshalJSON custom marshaller to ensure AllowedModels is always an array (never null)
func (pc TableVirtualKeyProviderConfig) MarshalJSON() ([]byte, error) {
type Alias TableVirtualKeyProviderConfig
// Ensure AllowedModels is an empty slice instead of nil
allowedModels := pc.AllowedModels
if allowedModels == nil {
allowedModels = []string{}
}
return json.Marshal(&struct {
Alias
AllowedModels []string `json:"allowed_models"`
}{
Alias: Alias(pc),
AllowedModels: allowedModels,
})
}
// AfterFind hook for TableVirtualKeyProviderConfig to clear sensitive data from associated keys
func (pc *TableVirtualKeyProviderConfig) AfterFind(tx *gorm.DB) error {
if pc.Keys != nil {
// Clear sensitive data from associated keys, keeping only key IDs and non-sensitive metadata
for i := range pc.Keys {
key := &pc.Keys[i]
// Clear the actual API key value
key.Value = *schemas.NewEnvVar("")
// Clear all Azure-related sensitive fields
key.AzureEndpoint = nil
key.AzureAPIVersion = nil
key.AzureClientID = nil
key.AzureClientSecret = nil
key.AzureTenantID = nil
key.AzureScopesJSON = nil
key.AzureKeyConfig = nil
// Clear all Vertex-related sensitive fields
key.VertexProjectID = nil
key.VertexProjectNumber = nil
key.VertexRegion = nil
key.VertexAuthCredentials = nil
key.VertexKeyConfig = nil
// Clear all Bedrock-related sensitive fields
key.BedrockAccessKey = nil
key.BedrockSecretKey = nil
key.BedrockSessionToken = nil
key.BedrockRegion = nil
key.BedrockARN = nil
key.BedrockRoleARN = nil
key.BedrockExternalID = nil
key.BedrockRoleSessionName = nil
key.BedrockKeyConfig = nil
pc.Keys[i] = *key
}
}
return nil
}
type TableVirtualKeyMCPConfig struct {
ID uint `gorm:"primaryKey;autoIncrement" json:"id"`
VirtualKeyID string `gorm:"type:varchar(255);not null;uniqueIndex:idx_vk_mcpclient" json:"virtual_key_id"`
MCPClientID uint `gorm:"not null;uniqueIndex:idx_vk_mcpclient" json:"mcp_client_id"`
MCPClient TableMCPClient `gorm:"foreignKey:MCPClientID" json:"mcp_client"`
ToolsToExecute schemas.WhiteList `gorm:"type:text;serializer:json" json:"tools_to_execute"`
// MCPClientName is used during config file parsing to resolve the MCP client by name.
// This field is not persisted to the database - it's only used to capture
// "mcp_client_name" from config.json and then resolve it to MCPClientID.
MCPClientName string `gorm:"-" json:"-"`
}
// TableName sets the table name for each model
func (TableVirtualKeyMCPConfig) TableName() string {
return "governance_virtual_key_mcp_configs"
}
// BeforeSave validates WhiteList fields before GORM persists the record.
func (mc *TableVirtualKeyMCPConfig) BeforeSave(tx *gorm.DB) error {
if err := mc.ToolsToExecute.Validate(); err != nil {
return fmt.Errorf("invalid tools_to_execute: %w", err)
}
return nil
}
// UnmarshalJSON custom unmarshaller to handle both "mcp_client_id" (database format)
// and "mcp_client_name" (config file format) for MCP client references.
func (mc *TableVirtualKeyMCPConfig) UnmarshalJSON(data []byte) error {
// Temporary struct to capture all fields including mcp_client_name
type Alias TableVirtualKeyMCPConfig
type TempMCPConfig struct {
Alias
MCPClientName string `json:"mcp_client_name"` // Config file format: MCP client name
}
var temp TempMCPConfig
if err := json.Unmarshal(data, &temp); err != nil {
return err
}
// Copy all standard fields
*mc = TableVirtualKeyMCPConfig(temp.Alias)
// Capture mcp_client_name for later resolution to MCPClientID
if temp.MCPClientName != "" {
mc.MCPClientName = temp.MCPClientName
}
return nil
}
// TableVirtualKey represents a virtual key with budget, rate limits, and team/customer association
type TableVirtualKey struct {
ID string `gorm:"primaryKey;type:varchar(255)" json:"id"`
Name string `gorm:"uniqueIndex:idx_virtual_key_name;type:varchar(255);not null" json:"name"`
Description string `gorm:"type:text" json:"description,omitempty"`
Value string `gorm:"uniqueIndex:idx_virtual_key_value;type:text;not null" json:"value"` // The virtual key value
IsActive bool `gorm:"default:true" json:"is_active"`
ProviderConfigs []TableVirtualKeyProviderConfig `gorm:"foreignKey:VirtualKeyID;constraint:OnDelete:CASCADE" json:"provider_configs"` // Empty means no providers allowed (deny-by-default)
MCPConfigs []TableVirtualKeyMCPConfig `gorm:"foreignKey:VirtualKeyID;constraint:OnDelete:CASCADE" json:"mcp_configs"`
// Foreign key relationships (mutually exclusive: either TeamID or CustomerID, not both)
TeamID *string `gorm:"type:varchar(255);index" json:"team_id,omitempty"`
CustomerID *string `gorm:"type:varchar(255);index" json:"customer_id,omitempty"`
RateLimitID *string `gorm:"type:varchar(255);index" json:"rate_limit_id,omitempty"`
// Deprecated
// Calendar aligned is not the property of virtual key but its property of the budget and ratelimit
// So in the migration we will move this to the budget/ratelimit table
// And this won't be referred
CalendarAligned bool `gorm:"default:false" json:"calendar_aligned"` // When true, all budgets under this VK reset at clean calendar boundaries
// Relationships
Team *TableTeam `gorm:"foreignKey:TeamID" json:"team,omitempty"`
Customer *TableCustomer `gorm:"foreignKey:CustomerID" json:"customer,omitempty"`
RateLimit *TableRateLimit `gorm:"foreignKey:RateLimitID;onDelete:CASCADE" json:"rate_limit,omitempty"`
Budgets []TableBudget `gorm:"foreignKey:VirtualKeyID;constraint:OnDelete:CASCADE" json:"budgets,omitempty"` // Multiple budgets with different reset intervals
// Config hash is used to detect the changes synced from config.json file
// Every time we sync the config.json file, we will update the config hash
ConfigHash string `gorm:"type:varchar(255);null" json:"config_hash"`
EncryptionStatus string `gorm:"type:varchar(20);default:'plain_text'" json:"-"`
ValueHash string `gorm:"type:varchar(64);index:idx_virtual_key_value_hash,unique" json:"-"`
CreatedAt time.Time `gorm:"index;not null" json:"created_at"`
UpdatedAt time.Time `gorm:"index;not null" json:"updated_at"`
}
// TableName sets the table name for each model
func (TableVirtualKey) TableName() string { return "governance_virtual_keys" }
// BeforeSave is a GORM hook that enforces mutual exclusion (team vs customer), computes
// a SHA-256 hash of the plaintext value for indexed lookups, and encrypts the virtual key
// value before writing to the database.
func (vk *TableVirtualKey) BeforeSave(tx *gorm.DB) error {
// Enforce mutual exclusion: VK can belong to either Team OR Customer, not both
if vk.TeamID != nil && vk.CustomerID != nil {
return fmt.Errorf("virtual key cannot belong to both team and customer")
}
// Hash must be computed before encryption (from plaintext value)
if vk.Value != "" {
vk.ValueHash = encrypt.HashSHA256(vk.Value)
}
if encrypt.IsEnabled() && vk.Value != "" {
if err := encryptString(&vk.Value); err != nil {
return fmt.Errorf("failed to encrypt virtual key value: %w", err)
}
vk.EncryptionStatus = EncryptionStatusEncrypted
}
return nil
}
// AfterFind is a GORM hook that decrypts the virtual key value after reading from the database.
func (vk *TableVirtualKey) AfterFind(tx *gorm.DB) error {
if vk.EncryptionStatus == EncryptionStatusEncrypted {
if err := decryptString(&vk.Value); err != nil {
return fmt.Errorf("failed to decrypt virtual key value: %w", err)
}
}
return nil
}