921 lines
42 KiB
Go
921 lines
42 KiB
Go
package config
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/hex"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"net/url"
|
|
"os"
|
|
"path/filepath"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
const (
|
|
EnvironmentLocal = "local"
|
|
EnvironmentTest = "test"
|
|
EnvironmentDev = "dev"
|
|
EnvironmentStage = "stage"
|
|
EnvironmentProd = "prod"
|
|
)
|
|
|
|
type Config struct {
|
|
VikingResourceID string
|
|
VikingCollectionName string
|
|
VikingAPIKey string
|
|
VikingEventType string
|
|
VikingProfileType string
|
|
ServiceName string
|
|
Environment string
|
|
LogLevel string
|
|
HTTPAddr string
|
|
DatabaseURL string
|
|
RedisAddr string
|
|
RedisPassword string
|
|
RedisDB int
|
|
AuthIssuerURL string
|
|
AuthJWKSURL string
|
|
AuthAudience string
|
|
OTLPEndpoint string
|
|
RateLimitRPS float64
|
|
RateLimitBurst int
|
|
RateLimitUserRPS float64
|
|
RateLimitUserBurst int
|
|
RateLimitTenantRPS float64
|
|
RateLimitTenantBurst int
|
|
RateLimitDeviceRPS float64
|
|
RateLimitDeviceBurst int
|
|
ProviderStartRPS float64
|
|
ProviderStartBurst int
|
|
ProviderMaxActiveTasks int
|
|
DeviceLocalSecret string
|
|
DeviceCredentialMasterKey string
|
|
ContentModelMasterKey string
|
|
DeviceClockSkew time.Duration
|
|
RTCAppID string
|
|
RTCAppKey string
|
|
RTCTokenIssuer string
|
|
RTCTokenSecret string
|
|
CursorSigningSecret string
|
|
ShareTokenSecret string
|
|
RTCTokenTTL time.Duration
|
|
AllocationTTL time.Duration
|
|
HeartbeatInterval int
|
|
ProviderCallbackSecret string
|
|
SubtitleCallbackURL string
|
|
SubtitleCallbackSecret string
|
|
SubtitlePrimary bool
|
|
ProviderCallbackSkew time.Duration
|
|
PlatformHealthSecret string
|
|
PlatformHealthSkew time.Duration
|
|
MockProviderBaseURL string
|
|
MockProviderAPIKey string
|
|
VoiceProvider string
|
|
VolcRTCBaseURL string
|
|
VolcAccessKeyID string
|
|
VolcSecretAccessKey string
|
|
VolcSessionToken string
|
|
VolcRTCRegion string
|
|
VolcRTCAPIVersion string
|
|
VolcVoiceChatTemplate string
|
|
VolcHardwareAgentBotID string
|
|
VolcHardwareTransport string
|
|
VolcHardwareMinimumSDKVersion string
|
|
VolcHardwareProvisioningEnabled bool
|
|
VolcHardwareInstanceID string
|
|
VolcHardwareProductKey string
|
|
VolcHardwareProductSecret string
|
|
VolcHardwareDeviceCredentialMasterKey string
|
|
VolcHardwareRegisterURL string
|
|
VolcHardwareRegisterTimeout time.Duration
|
|
VolcHardwareDeviceNamePrefix string
|
|
ProviderRequestTimeout time.Duration
|
|
ProviderFailureThreshold int
|
|
ProviderCircuitCooldown time.Duration
|
|
DependencyTimeout time.Duration
|
|
ExternalProbeInterval time.Duration
|
|
ShutdownTimeout time.Duration
|
|
WorkerPollInterval time.Duration
|
|
RecoveryScanInterval time.Duration
|
|
RecoveryStartingAfter time.Duration
|
|
RecoveryStoppingAfter time.Duration
|
|
ObjectStoreEndpoint string
|
|
ObjectStorePublic string
|
|
ObjectStoreAccessKey string
|
|
ObjectStoreSecretKey string
|
|
ObjectStoreBucket string
|
|
ObjectStoreRegion string
|
|
ObjectStoreSecure bool
|
|
ObjectStorePublicSecure bool
|
|
ExportDownloadTTL time.Duration
|
|
ExportObjectTTL time.Duration
|
|
MediaLocalDeleteGrace time.Duration
|
|
AlertWebhookURL string
|
|
AlertWebhookSecret string
|
|
AlertWebhookTimeout time.Duration
|
|
ContentGenerator string
|
|
ArkBaseURL string
|
|
ArkAPIKey string
|
|
ArkContentModel string
|
|
ArkRequestTimeout time.Duration
|
|
ContentSafetyProvider string
|
|
ContentSafetyEndpoint string
|
|
ContentSafetyAPIKey string
|
|
ContentSafetyPolicyRef string
|
|
ContentSafetyTimeout time.Duration
|
|
EmbeddingProvider string
|
|
ArkEmbeddingModel string
|
|
MemoryToolBaseURL string
|
|
MemoryToolSigningSecret string
|
|
MemoryToolTokenTTL time.Duration
|
|
MemoryToolRateRPS float64
|
|
MemoryToolRateBurst int
|
|
ProviderMemoryMode string
|
|
ProviderMemoryBaseURL string
|
|
ProviderMemoryAPIKey string
|
|
ProviderMemoryTimeout time.Duration
|
|
}
|
|
|
|
func Load(serviceName string) (Config, error) {
|
|
if strings.TrimSpace(serviceName) == "" {
|
|
return Config{}, errors.New("service name is required")
|
|
}
|
|
|
|
redisDB, err := intEnv("REDIS_DB", 0)
|
|
if err != nil {
|
|
return Config{}, err
|
|
}
|
|
dependencyTimeout, err := durationEnv("DEPENDENCY_TIMEOUT", 2*time.Second)
|
|
if err != nil {
|
|
return Config{}, err
|
|
}
|
|
externalProbeInterval, err := durationEnv("EXTERNAL_PROBE_INTERVAL", 30*time.Second)
|
|
if err != nil {
|
|
return Config{}, err
|
|
}
|
|
shutdownTimeout, err := durationEnv("SHUTDOWN_TIMEOUT", 10*time.Second)
|
|
if err != nil {
|
|
return Config{}, err
|
|
}
|
|
pollInterval, err := durationEnv("WORKER_POLL_INTERVAL", 2*time.Second)
|
|
if err != nil {
|
|
return Config{}, err
|
|
}
|
|
recoveryScanInterval, err := durationEnv("RECOVERY_SCAN_INTERVAL", 5*time.Second)
|
|
if err != nil {
|
|
return Config{}, err
|
|
}
|
|
recoveryStartingAfter, err := durationEnv("RECOVERY_STARTING_AFTER", 20*time.Second)
|
|
if err != nil {
|
|
return Config{}, err
|
|
}
|
|
recoveryStoppingAfter, err := durationEnv("RECOVERY_STOPPING_AFTER", 20*time.Second)
|
|
if err != nil {
|
|
return Config{}, err
|
|
}
|
|
deviceClockSkew, err := durationEnv("DEVICE_CLOCK_SKEW", 5*time.Minute)
|
|
if err != nil {
|
|
return Config{}, err
|
|
}
|
|
rtcTokenTTL, err := durationEnv("RTC_TOKEN_TTL", 5*time.Minute)
|
|
if err != nil {
|
|
return Config{}, err
|
|
}
|
|
allocationTTL, err := durationEnv("CONVERSATION_ALLOCATION_TTL", 30*time.Second)
|
|
if err != nil {
|
|
return Config{}, err
|
|
}
|
|
providerCallbackSkew, err := durationEnv("PROVIDER_CALLBACK_CLOCK_SKEW", 5*time.Minute)
|
|
if err != nil {
|
|
return Config{}, err
|
|
}
|
|
platformHealthSkew, err := durationEnv("PLATFORM_HEALTH_CALLBACK_CLOCK_SKEW", 5*time.Minute)
|
|
if err != nil {
|
|
return Config{}, err
|
|
}
|
|
providerRequestTimeout, err := durationEnv("PROVIDER_REQUEST_TIMEOUT", 5*time.Second)
|
|
if err != nil {
|
|
return Config{}, err
|
|
}
|
|
providerCircuitCooldown, err := durationEnv("PROVIDER_CIRCUIT_COOLDOWN", time.Minute)
|
|
if err != nil {
|
|
return Config{}, err
|
|
}
|
|
exportDownloadTTL, err := durationEnv("EXPORT_DOWNLOAD_TTL", 10*time.Minute)
|
|
if err != nil {
|
|
return Config{}, err
|
|
}
|
|
exportObjectTTL, err := durationEnv("EXPORT_OBJECT_TTL", 24*time.Hour)
|
|
if err != nil {
|
|
return Config{}, err
|
|
}
|
|
mediaLocalDeleteGrace, err := durationEnv("MEDIA_LOCAL_DELETE_GRACE", time.Hour)
|
|
if err != nil {
|
|
return Config{}, err
|
|
}
|
|
alertWebhookTimeout, err := durationEnv("ALERT_WEBHOOK_TIMEOUT", 5*time.Second)
|
|
if err != nil {
|
|
return Config{}, err
|
|
}
|
|
arkRequestTimeout, err := durationEnv("ARK_REQUEST_TIMEOUT", 30*time.Second)
|
|
if err != nil {
|
|
return Config{}, err
|
|
}
|
|
contentSafetyTimeout, err := durationEnv("CONTENT_SAFETY_TIMEOUT", 5*time.Second)
|
|
if err != nil {
|
|
return Config{}, err
|
|
}
|
|
providerMemoryTimeout, err := durationEnv("PROVIDER_MEMORY_TIMEOUT", 5*time.Second)
|
|
if err != nil {
|
|
return Config{}, err
|
|
}
|
|
memoryToolTokenTTL, err := durationEnv("MEMORY_TOOL_TOKEN_TTL", 4*time.Hour)
|
|
if err != nil {
|
|
return Config{}, err
|
|
}
|
|
volcHardwareProvisioningEnabled, err := boolEnv("VOLC_HARDWARE_PROVISIONING_ENABLED", false)
|
|
if err != nil {
|
|
return Config{}, err
|
|
}
|
|
volcHardwareRegisterTimeout, err := durationEnv("VOLC_HARDWARE_REGISTER_TIMEOUT", 10*time.Second)
|
|
if err != nil {
|
|
return Config{}, err
|
|
}
|
|
objectStoreSecure, err := boolEnv("OBJECT_STORE_SECURE", false)
|
|
if err != nil {
|
|
return Config{}, err
|
|
}
|
|
objectStorePublicSecure, err := boolEnv("OBJECT_STORE_PUBLIC_SECURE", objectStoreSecure)
|
|
if err != nil {
|
|
return Config{}, err
|
|
}
|
|
environment := stringEnv("APP_ENV", EnvironmentLocal)
|
|
secretValues, err := loadSecretValues(
|
|
"DATABASE_URL", "REDIS_PASSWORD", "DEVICE_LOCAL_SECRET", "DEVICE_CREDENTIAL_MASTER_KEY", "MODEL_CREDENTIAL_MASTER_KEY", "RTC_APP_KEY", "RTC_TOKEN_SIGNING_SECRET",
|
|
"CURSOR_SIGNING_SECRET", "SHARE_TOKEN_SIGNING_SECRET", "PROVIDER_CALLBACK_SECRET", "PLATFORM_HEALTH_CALLBACK_SECRET",
|
|
"VOLC_SUBTITLE_CALLBACK_SECRET", "VIKING_MEMORY_API_KEY",
|
|
"MOCK_PROVIDER_API_KEY", "VOLC_ACCESS_KEY_ID", "VOLC_SECRET_ACCESS_KEY", "VOLC_SESSION_TOKEN",
|
|
"VOLC_HARDWARE_PRODUCT_SECRET", "VOLC_HARDWARE_DEVICE_CREDENTIAL_MASTER_KEY",
|
|
"OBJECT_STORE_ACCESS_KEY", "OBJECT_STORE_SECRET_KEY", "ALERT_WEBHOOK_SECRET", "ARK_API_KEY", "CONTENT_SAFETY_API_KEY", "MEMORY_TOOL_SIGNING_SECRET", "PROVIDER_MEMORY_API_KEY",
|
|
)
|
|
if err != nil {
|
|
return Config{}, err
|
|
}
|
|
deviceLocalSecret := secretValues["DEVICE_LOCAL_SECRET"]
|
|
deviceCredentialMasterKey := secretValues["DEVICE_CREDENTIAL_MASTER_KEY"]
|
|
contentModelMasterKey := secretValues["MODEL_CREDENTIAL_MASTER_KEY"]
|
|
rtcTokenSecret := secretValues["RTC_TOKEN_SIGNING_SECRET"]
|
|
cursorSigningSecret := secretValues["CURSOR_SIGNING_SECRET"]
|
|
shareTokenSecret := secretValues["SHARE_TOKEN_SIGNING_SECRET"]
|
|
providerCallbackSecret := secretValues["PROVIDER_CALLBACK_SECRET"]
|
|
objectStoreEndpoint := strings.TrimSpace(os.Getenv("OBJECT_STORE_ENDPOINT"))
|
|
objectStorePublic := strings.TrimSpace(os.Getenv("OBJECT_STORE_PUBLIC_ENDPOINT"))
|
|
objectStoreAccessKey := secretValues["OBJECT_STORE_ACCESS_KEY"]
|
|
objectStoreSecretKey := secretValues["OBJECT_STORE_SECRET_KEY"]
|
|
objectStoreBucket := strings.TrimSpace(os.Getenv("OBJECT_STORE_EXPORT_BUCKET"))
|
|
objectStoreRegion := strings.TrimSpace(os.Getenv("OBJECT_STORE_REGION"))
|
|
if deviceLocalSecret == "" && serviceName == "api" && (environment == EnvironmentLocal || environment == EnvironmentTest) {
|
|
deviceLocalSecret = "local-simulated-device-secret-32-bytes-minimum"
|
|
}
|
|
if deviceCredentialMasterKey == "" && serviceName == "api" && (environment == EnvironmentLocal || environment == EnvironmentTest) {
|
|
deviceCredentialMasterKey = "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f"
|
|
}
|
|
if contentModelMasterKey == "" && (serviceName == "api" || serviceName == "worker") && (environment == EnvironmentLocal || environment == EnvironmentTest) {
|
|
contentModelMasterKey = "1f1e1d1c1b1a191817161514131211100f0e0d0c0b0a09080706050403020100"
|
|
}
|
|
if rtcTokenSecret == "" && serviceName == "api" && (environment == EnvironmentLocal || environment == EnvironmentTest) {
|
|
rtcTokenSecret = "local-simulated-rtc-token-secret-32-bytes-minimum"
|
|
}
|
|
if cursorSigningSecret == "" && serviceName == "api" && (environment == EnvironmentLocal || environment == EnvironmentTest) {
|
|
cursorSigningSecret = "local-signed-pagination-cursor-secret-32-bytes-minimum"
|
|
}
|
|
if shareTokenSecret == "" && serviceName == "api" && (environment == EnvironmentLocal || environment == EnvironmentTest) {
|
|
shareTokenSecret = "local-share-token-signing-secret-32-bytes-minimum"
|
|
}
|
|
memoryToolSigningSecret := secretValues["MEMORY_TOOL_SIGNING_SECRET"]
|
|
if memoryToolSigningSecret == "" && (serviceName == "api" || serviceName == "worker") && (environment == EnvironmentLocal || environment == EnvironmentTest) {
|
|
memoryToolSigningSecret = "local-memory-tool-signing-secret-32-bytes-minimum"
|
|
}
|
|
if providerCallbackSecret == "" && serviceName == "callbacks" && (environment == EnvironmentLocal || environment == EnvironmentTest) {
|
|
providerCallbackSecret = "local-provider-callback-secret-32-bytes-minimum"
|
|
}
|
|
if (serviceName == "api" || serviceName == "worker") && (environment == EnvironmentLocal || environment == EnvironmentTest) {
|
|
if objectStoreEndpoint == "" {
|
|
objectStoreEndpoint = "localhost:59000"
|
|
}
|
|
if objectStorePublic == "" {
|
|
objectStorePublic = "localhost:59000"
|
|
}
|
|
if objectStoreAccessKey == "" {
|
|
objectStoreAccessKey = "local_minio"
|
|
}
|
|
if objectStoreSecretKey == "" {
|
|
objectStoreSecretKey = "local_minio_password"
|
|
}
|
|
if objectStoreBucket == "" {
|
|
objectStoreBucket = "private-exports"
|
|
}
|
|
if objectStoreRegion == "" {
|
|
objectStoreRegion = "us-east-1"
|
|
}
|
|
}
|
|
|
|
cfg := Config{
|
|
ServiceName: serviceName,
|
|
Environment: environment,
|
|
LogLevel: stringEnv("LOG_LEVEL", "info"),
|
|
HTTPAddr: stringEnv("HTTP_ADDR", defaultHTTPAddr(serviceName)),
|
|
DatabaseURL: secretDefault(secretValues["DATABASE_URL"], "postgres://voice:voice_dev_only@localhost:55432/voice_platform?sslmode=disable"),
|
|
RedisAddr: stringEnv("REDIS_ADDR", "localhost:56379"),
|
|
RedisPassword: secretValues["REDIS_PASSWORD"],
|
|
RedisDB: redisDB,
|
|
AuthIssuerURL: stringEnv("AUTH_ISSUER_URL", "http://localhost:58180/realms/voice-local"),
|
|
AuthJWKSURL: stringEnv("AUTH_JWKS_URL", "http://localhost:58180/realms/voice-local/protocol/openid-connect/certs"),
|
|
AuthAudience: stringEnv("AUTH_AUDIENCE", "voice-platform-web"),
|
|
OTLPEndpoint: stringEnvAllowEmpty("OTEL_EXPORTER_OTLP_ENDPOINT", "localhost:54317"),
|
|
DependencyTimeout: dependencyTimeout,
|
|
ExternalProbeInterval: externalProbeInterval,
|
|
ShutdownTimeout: shutdownTimeout,
|
|
WorkerPollInterval: pollInterval,
|
|
RecoveryScanInterval: recoveryScanInterval,
|
|
RecoveryStartingAfter: recoveryStartingAfter,
|
|
RecoveryStoppingAfter: recoveryStoppingAfter,
|
|
DeviceLocalSecret: deviceLocalSecret,
|
|
DeviceCredentialMasterKey: deviceCredentialMasterKey,
|
|
ContentModelMasterKey: contentModelMasterKey,
|
|
DeviceClockSkew: deviceClockSkew,
|
|
RTCAppID: stringEnv("RTC_APP_ID", "local-public-rtc-app"),
|
|
RTCAppKey: secretValues["RTC_APP_KEY"],
|
|
RTCTokenIssuer: stringEnv("RTC_TOKEN_ISSUER", "local"),
|
|
RTCTokenSecret: rtcTokenSecret,
|
|
CursorSigningSecret: cursorSigningSecret,
|
|
ShareTokenSecret: shareTokenSecret,
|
|
RTCTokenTTL: rtcTokenTTL,
|
|
AllocationTTL: allocationTTL,
|
|
ProviderCallbackSecret: providerCallbackSecret,
|
|
SubtitleCallbackURL: strings.TrimSpace(os.Getenv("VOLC_SUBTITLE_CALLBACK_URL")),
|
|
SubtitleCallbackSecret: secretValues["VOLC_SUBTITLE_CALLBACK_SECRET"],
|
|
SubtitlePrimary: os.Getenv("VOLC_SUBTITLE_PRIMARY") == "true",
|
|
ProviderCallbackSkew: providerCallbackSkew,
|
|
PlatformHealthSecret: secretValues["PLATFORM_HEALTH_CALLBACK_SECRET"],
|
|
PlatformHealthSkew: platformHealthSkew,
|
|
MockProviderBaseURL: stringEnv("MOCK_PROVIDER_BASE_URL", "http://localhost:59200"),
|
|
MockProviderAPIKey: secretDefault(secretValues["MOCK_PROVIDER_API_KEY"], "local-mock-provider-key"),
|
|
VoiceProvider: stringEnv("VOICE_PROVIDER", "mock"),
|
|
VolcRTCBaseURL: stringEnv("VOLC_RTC_BASE_URL", "https://rtc.volcengineapi.com"),
|
|
VolcAccessKeyID: secretValues["VOLC_ACCESS_KEY_ID"],
|
|
VolcSecretAccessKey: secretValues["VOLC_SECRET_ACCESS_KEY"],
|
|
VolcSessionToken: secretValues["VOLC_SESSION_TOKEN"],
|
|
VolcRTCRegion: stringEnv("VOLC_RTC_REGION", "cn-north-1"),
|
|
VolcRTCAPIVersion: stringEnv("VOLC_RTC_API_VERSION", "2024-12-01"),
|
|
VolcVoiceChatTemplate: strings.TrimSpace(os.Getenv("VOLC_VOICE_CHAT_TEMPLATE_FILE")),
|
|
VolcHardwareAgentBotID: strings.TrimSpace(os.Getenv("VOLC_HARDWARE_AGENT_BOT_ID")),
|
|
VolcHardwareTransport: stringEnv("VOLC_HARDWARE_AGENT_TRANSPORT", "high_quality"),
|
|
VolcHardwareMinimumSDKVersion: stringEnv("VOLC_HARDWARE_AGENT_MINIMUM_SDK_VERSION", "1.0.0"),
|
|
VolcHardwareProvisioningEnabled: volcHardwareProvisioningEnabled,
|
|
VolcHardwareInstanceID: strings.TrimSpace(os.Getenv("VOLC_HARDWARE_INSTANCE_ID")),
|
|
VolcHardwareProductKey: strings.TrimSpace(os.Getenv("VOLC_HARDWARE_PRODUCT_KEY")),
|
|
VolcHardwareProductSecret: secretValues["VOLC_HARDWARE_PRODUCT_SECRET"],
|
|
VolcHardwareDeviceCredentialMasterKey: secretValues["VOLC_HARDWARE_DEVICE_CREDENTIAL_MASTER_KEY"],
|
|
VolcHardwareRegisterURL: stringEnv("VOLC_HARDWARE_REGISTER_URL", "https://iot-cn-shanghai.iot.volces.com/2021-12-14/DynamicRegister?Action=DynamicRegister&Version=2021-12-14"),
|
|
VolcHardwareRegisterTimeout: volcHardwareRegisterTimeout,
|
|
VolcHardwareDeviceNamePrefix: stringEnv("VOLC_HARDWARE_DEVICE_NAME_PREFIX", "dt-"),
|
|
ProviderRequestTimeout: providerRequestTimeout,
|
|
ProviderCircuitCooldown: providerCircuitCooldown,
|
|
ObjectStoreEndpoint: objectStoreEndpoint,
|
|
ObjectStorePublic: objectStorePublic,
|
|
ObjectStoreAccessKey: objectStoreAccessKey,
|
|
ObjectStoreSecretKey: objectStoreSecretKey,
|
|
ObjectStoreBucket: objectStoreBucket,
|
|
ObjectStoreRegion: objectStoreRegion,
|
|
ObjectStoreSecure: objectStoreSecure,
|
|
ObjectStorePublicSecure: objectStorePublicSecure,
|
|
ExportDownloadTTL: exportDownloadTTL,
|
|
ExportObjectTTL: exportObjectTTL,
|
|
MediaLocalDeleteGrace: mediaLocalDeleteGrace,
|
|
AlertWebhookURL: strings.TrimSpace(os.Getenv("ALERT_WEBHOOK_URL")),
|
|
AlertWebhookSecret: secretValues["ALERT_WEBHOOK_SECRET"],
|
|
AlertWebhookTimeout: alertWebhookTimeout,
|
|
ContentGenerator: stringEnv("CONTENT_GENERATOR", "local"),
|
|
ArkBaseURL: stringEnv("ARK_BASE_URL", "https://ark.cn-beijing.volces.com/api/v3"),
|
|
ArkAPIKey: secretValues["ARK_API_KEY"],
|
|
ArkContentModel: strings.TrimSpace(os.Getenv("ARK_CONTENT_MODEL")),
|
|
ArkRequestTimeout: arkRequestTimeout,
|
|
ContentSafetyProvider: stringEnv("CONTENT_SAFETY_PROVIDER", "local"),
|
|
ContentSafetyEndpoint: strings.TrimSpace(os.Getenv("CONTENT_SAFETY_ENDPOINT")),
|
|
ContentSafetyAPIKey: secretValues["CONTENT_SAFETY_API_KEY"],
|
|
ContentSafetyPolicyRef: strings.TrimSpace(os.Getenv("CONTENT_SAFETY_POLICY_REF")),
|
|
ContentSafetyTimeout: contentSafetyTimeout,
|
|
EmbeddingProvider: stringEnv("EMBEDDING_PROVIDER", "local"),
|
|
ArkEmbeddingModel: strings.TrimSpace(os.Getenv("ARK_EMBEDDING_MODEL")),
|
|
MemoryToolBaseURL: strings.TrimSpace(os.Getenv("MEMORY_TOOL_BASE_URL")),
|
|
MemoryToolSigningSecret: memoryToolSigningSecret,
|
|
MemoryToolTokenTTL: memoryToolTokenTTL,
|
|
VikingResourceID: strings.TrimSpace(os.Getenv("VIKING_MEMORY_RESOURCE_ID")),
|
|
VikingCollectionName: strings.TrimSpace(os.Getenv("VIKING_MEMORY_COLLECTION_NAME")),
|
|
VikingAPIKey: secretValues["VIKING_MEMORY_API_KEY"],
|
|
VikingEventType: stringEnv("VIKING_MEMORY_EVENT_TYPE", "event_v1"),
|
|
VikingProfileType: stringEnv("VIKING_MEMORY_PROFILE_TYPE", "profile_v1"),
|
|
ProviderMemoryMode: stringEnv("PROVIDER_MEMORY_MODE", "disabled"),
|
|
ProviderMemoryBaseURL: strings.TrimSpace(os.Getenv("PROVIDER_MEMORY_BASE_URL")),
|
|
ProviderMemoryAPIKey: secretValues["PROVIDER_MEMORY_API_KEY"],
|
|
ProviderMemoryTimeout: providerMemoryTimeout,
|
|
}
|
|
cfg.RateLimitRPS, err = floatEnv("RATE_LIMIT_RPS", 50)
|
|
if err != nil {
|
|
return Config{}, err
|
|
}
|
|
cfg.RateLimitBurst, err = intEnv("RATE_LIMIT_BURST", 100)
|
|
if err != nil {
|
|
return Config{}, err
|
|
}
|
|
cfg.RateLimitUserRPS, err = floatEnv("RATE_LIMIT_USER_RPS", 20)
|
|
if err != nil {
|
|
return Config{}, err
|
|
}
|
|
cfg.RateLimitUserBurst, err = intEnv("RATE_LIMIT_USER_BURST", 40)
|
|
if err != nil {
|
|
return Config{}, err
|
|
}
|
|
cfg.RateLimitTenantRPS, err = floatEnv("RATE_LIMIT_TENANT_RPS", 200)
|
|
if err != nil {
|
|
return Config{}, err
|
|
}
|
|
cfg.RateLimitTenantBurst, err = intEnv("RATE_LIMIT_TENANT_BURST", 400)
|
|
if err != nil {
|
|
return Config{}, err
|
|
}
|
|
cfg.RateLimitDeviceRPS, err = floatEnv("RATE_LIMIT_DEVICE_RPS", 10)
|
|
if err != nil {
|
|
return Config{}, err
|
|
}
|
|
cfg.RateLimitDeviceBurst, err = intEnv("RATE_LIMIT_DEVICE_BURST", 20)
|
|
if err != nil {
|
|
return Config{}, err
|
|
}
|
|
cfg.ProviderStartRPS, err = floatEnv("PROVIDER_START_RPS", 20)
|
|
if err != nil {
|
|
return Config{}, err
|
|
}
|
|
cfg.ProviderStartBurst, err = intEnv("PROVIDER_START_BURST", 40)
|
|
if err != nil {
|
|
return Config{}, err
|
|
}
|
|
cfg.ProviderMaxActiveTasks, err = intEnv("PROVIDER_MAX_ACTIVE_TASKS", 100)
|
|
if err != nil {
|
|
return Config{}, err
|
|
}
|
|
cfg.MemoryToolRateRPS, err = floatEnv("MEMORY_TOOL_RATE_RPS", 2)
|
|
if err != nil {
|
|
return Config{}, err
|
|
}
|
|
cfg.MemoryToolRateBurst, err = intEnv("MEMORY_TOOL_RATE_BURST", 5)
|
|
if err != nil {
|
|
return Config{}, err
|
|
}
|
|
cfg.HeartbeatInterval, err = intEnv("HEARTBEAT_INTERVAL_SECONDS", 15)
|
|
if err != nil {
|
|
return Config{}, err
|
|
}
|
|
cfg.ProviderFailureThreshold, err = intEnv("PROVIDER_FAILURE_THRESHOLD", 3)
|
|
if err != nil {
|
|
return Config{}, err
|
|
}
|
|
|
|
if err := cfg.Validate(); err != nil {
|
|
return Config{}, err
|
|
}
|
|
return cfg, nil
|
|
}
|
|
|
|
func (c Config) Validate() error {
|
|
if c.SubtitleCallbackURL != "" || c.SubtitleCallbackSecret != "" || c.SubtitlePrimary {
|
|
u, err := url.Parse(c.SubtitleCallbackURL)
|
|
if err != nil || u.Scheme != "https" || u.Host == "" || u.RawQuery != "" || u.Fragment != "" || u.User != nil || len(c.SubtitleCallbackSecret) < 32 {
|
|
return errors.New("native subtitle callback requires an HTTPS URL without query and a dedicated secret of at least 32 bytes")
|
|
}
|
|
}
|
|
validEnvironment := map[string]bool{
|
|
EnvironmentLocal: true,
|
|
EnvironmentTest: true,
|
|
EnvironmentDev: true,
|
|
EnvironmentStage: true,
|
|
EnvironmentProd: true,
|
|
}
|
|
if !validEnvironment[c.Environment] {
|
|
return fmt.Errorf("APP_ENV %q is not supported", c.Environment)
|
|
}
|
|
if strings.TrimSpace(c.HTTPAddr) == "" {
|
|
return errors.New("HTTP_ADDR is required")
|
|
}
|
|
if strings.TrimSpace(c.DatabaseURL) == "" {
|
|
return errors.New("DATABASE_URL is required")
|
|
}
|
|
if strings.TrimSpace(c.RedisAddr) == "" {
|
|
return errors.New("REDIS_ADDR is required")
|
|
}
|
|
if strings.TrimSpace(c.AuthIssuerURL) == "" || strings.TrimSpace(c.AuthJWKSURL) == "" || strings.TrimSpace(c.AuthAudience) == "" {
|
|
return errors.New("AUTH_ISSUER_URL, AUTH_JWKS_URL and AUTH_AUDIENCE are required")
|
|
}
|
|
if c.DependencyTimeout <= 0 || c.ExternalProbeInterval <= 0 || c.ShutdownTimeout <= 0 || c.WorkerPollInterval <= 0 ||
|
|
c.RecoveryScanInterval <= 0 || c.RecoveryStartingAfter <= 0 || c.RecoveryStoppingAfter <= 0 {
|
|
return errors.New("timeouts and worker poll interval must be positive")
|
|
}
|
|
if c.RateLimitRPS <= 0 || c.RateLimitBurst <= 0 ||
|
|
c.RateLimitUserRPS <= 0 || c.RateLimitUserBurst <= 0 ||
|
|
c.RateLimitTenantRPS <= 0 || c.RateLimitTenantBurst <= 0 ||
|
|
c.RateLimitDeviceRPS <= 0 || c.RateLimitDeviceBurst <= 0 ||
|
|
c.ProviderStartRPS <= 0 || c.ProviderStartBurst <= 0 || c.ProviderMaxActiveTasks <= 0 ||
|
|
c.MemoryToolRateRPS <= 0 || c.MemoryToolRateBurst <= 0 {
|
|
return errors.New("rate limits, bursts and provider task capacity must be positive")
|
|
}
|
|
if c.ServiceName == "api" {
|
|
decodedMasterKey, err := hex.DecodeString(c.DeviceCredentialMasterKey)
|
|
if err != nil || len(decodedMasterKey) != 32 {
|
|
return errors.New("DEVICE_CREDENTIAL_MASTER_KEY must be 32 bytes encoded as 64 hexadecimal characters")
|
|
}
|
|
}
|
|
if c.ServiceName == "api" || c.ServiceName == "worker" {
|
|
decodedModelKey, err := hex.DecodeString(c.ContentModelMasterKey)
|
|
if err != nil || len(decodedModelKey) != 32 {
|
|
return errors.New("MODEL_CREDENTIAL_MASTER_KEY must be 32 bytes encoded as 64 hexadecimal characters")
|
|
}
|
|
}
|
|
if c.DeviceClockSkew <= 0 {
|
|
return errors.New("DEVICE_CLOCK_SKEW must be positive")
|
|
}
|
|
if c.ServiceName == "api" {
|
|
if c.RTCTokenIssuer != "local" && c.RTCTokenIssuer != "volcengine" {
|
|
return errors.New("RTC_TOKEN_ISSUER must be local or volcengine")
|
|
}
|
|
if c.RTCTokenIssuer == "local" && len(c.RTCTokenSecret) < 32 {
|
|
return errors.New("RTC_TOKEN_SIGNING_SECRET must contain at least 32 characters for the local issuer")
|
|
}
|
|
if c.RTCTokenIssuer == "volcengine" && len(c.RTCAppKey) < 16 {
|
|
return errors.New("RTC_APP_KEY must contain at least 16 characters for the Volcengine token issuer")
|
|
}
|
|
if c.RTCTokenIssuer == "volcengine" && len(c.RTCAppID) != 24 {
|
|
return errors.New("RTC_APP_ID must contain exactly 24 characters for the Volcengine token issuer")
|
|
}
|
|
}
|
|
if c.ServiceName == "api" && len(c.CursorSigningSecret) < 32 {
|
|
return errors.New("CURSOR_SIGNING_SECRET must contain at least 32 characters")
|
|
}
|
|
if c.ServiceName == "api" && len(c.ShareTokenSecret) < 32 {
|
|
return errors.New("SHARE_TOKEN_SIGNING_SECRET must contain at least 32 characters")
|
|
}
|
|
if (c.ServiceName == "api" || c.ServiceName == "worker") && (len(c.MemoryToolSigningSecret) < 32 || c.MemoryToolTokenTTL <= 0) {
|
|
return errors.New("MEMORY_TOOL_SIGNING_SECRET must contain at least 32 characters and token TTL must be positive")
|
|
}
|
|
if c.MemoryToolBaseURL != "" && !isAbsoluteHTTPURL(c.MemoryToolBaseURL) {
|
|
return errors.New("MEMORY_TOOL_BASE_URL must be an absolute HTTP(S) URL")
|
|
}
|
|
if c.ProviderMemoryMode != "disabled" && c.ProviderMemoryMode != "managed_http" {
|
|
return errors.New("PROVIDER_MEMORY_MODE must be disabled or managed_http")
|
|
}
|
|
if (c.ServiceName == "api" || c.ServiceName == "worker") && c.ProviderMemoryMode == "managed_http" && (!isAbsoluteHTTPURL(c.ProviderMemoryBaseURL) || c.ProviderMemoryAPIKey == "" || c.ProviderMemoryTimeout <= 0) {
|
|
return errors.New("managed provider memory requires an HTTP(S) base URL, API key and positive timeout")
|
|
}
|
|
if c.RTCAppID == "" || c.RTCTokenTTL <= 0 || c.AllocationTTL <= 0 || c.HeartbeatInterval <= 0 {
|
|
return errors.New("RTC app, token TTL, allocation TTL and heartbeat interval must be valid")
|
|
}
|
|
if c.ServiceName == "callbacks" && len(c.ProviderCallbackSecret) < 32 {
|
|
return errors.New("PROVIDER_CALLBACK_SECRET must contain at least 32 characters")
|
|
}
|
|
if c.ServiceName == "callbacks" && len(c.PlatformHealthSecret) < 32 {
|
|
return errors.New("PLATFORM_HEALTH_CALLBACK_SECRET must contain at least 32 characters")
|
|
}
|
|
if c.ServiceName == "worker" {
|
|
if c.VoiceProvider != "mock" && c.VoiceProvider != "volcengine" {
|
|
return errors.New("VOICE_PROVIDER must be mock or volcengine")
|
|
}
|
|
if c.VoiceProvider == "mock" && (c.MockProviderBaseURL == "" || c.MockProviderAPIKey == "") {
|
|
return errors.New("MOCK_PROVIDER_BASE_URL and MOCK_PROVIDER_API_KEY are required for the mock voice provider")
|
|
}
|
|
if c.VoiceProvider == "volcengine" {
|
|
if !isAbsoluteHTTPURL(c.VolcRTCBaseURL) || c.VolcAccessKeyID == "" || c.VolcSecretAccessKey == "" || c.VolcRTCRegion == "" || c.VolcRTCAPIVersion == "" || c.VolcVoiceChatTemplate == "" {
|
|
return errors.New("Volcengine voice provider requires an RTC URL, AK/SK, region, API version and voice-chat template file")
|
|
}
|
|
}
|
|
}
|
|
if c.ProviderCallbackSkew <= 0 || c.PlatformHealthSkew <= 0 || c.ProviderRequestTimeout <= 0 || c.ProviderFailureThreshold < 1 || c.ProviderCircuitCooldown <= 0 {
|
|
return errors.New("provider callback skew and request timeout must be positive")
|
|
}
|
|
if c.VolcHardwareTransport != "high_quality" && c.VolcHardwareTransport != "low_load" {
|
|
return errors.New("VOLC_HARDWARE_AGENT_TRANSPORT must be high_quality or low_load")
|
|
}
|
|
if c.ServiceName == "api" && c.VolcHardwareProvisioningEnabled {
|
|
providerKey, err := hex.DecodeString(c.VolcHardwareDeviceCredentialMasterKey)
|
|
if err != nil || len(providerKey) != 32 {
|
|
return errors.New("VOLC_HARDWARE_DEVICE_CREDENTIAL_MASTER_KEY must be 32 bytes encoded as 64 hexadecimal characters")
|
|
}
|
|
if c.VolcHardwareInstanceID == "" || c.VolcHardwareProductKey == "" || c.VolcHardwareAgentBotID == "" ||
|
|
c.VolcHardwareMinimumSDKVersion == "" || len(c.VolcHardwareMinimumSDKVersion) > 80 ||
|
|
len(c.VolcHardwareProductSecret) < 16 || c.VolcHardwareRegisterTimeout <= 0 {
|
|
return errors.New("enabled hardware provisioning requires instance ID, product key, default Bot ID, minimum SDK, product secret and positive registration timeout")
|
|
}
|
|
}
|
|
if c.AlertWebhookTimeout <= 0 {
|
|
return errors.New("ALERT_WEBHOOK_TIMEOUT must be positive")
|
|
}
|
|
if c.ArkRequestTimeout <= 0 {
|
|
return errors.New("ARK_REQUEST_TIMEOUT must be positive")
|
|
}
|
|
if c.ContentSafetyTimeout <= 0 {
|
|
return errors.New("CONTENT_SAFETY_TIMEOUT must be positive")
|
|
}
|
|
if c.ServiceName == "worker" && c.AlertWebhookURL != "" {
|
|
parsed, err := url.Parse(c.AlertWebhookURL)
|
|
if err != nil || parsed.Host == "" || (parsed.Scheme != "http" && parsed.Scheme != "https") {
|
|
return errors.New("ALERT_WEBHOOK_URL must be an absolute HTTP(S) URL")
|
|
}
|
|
if len(c.AlertWebhookSecret) < 32 {
|
|
return errors.New("ALERT_WEBHOOK_SECRET must contain at least 32 characters when webhook delivery is enabled")
|
|
}
|
|
}
|
|
if c.ServiceName == "worker" {
|
|
if c.ContentSafetyProvider != "local" && c.ContentSafetyProvider != "managed_http" {
|
|
return errors.New("CONTENT_SAFETY_PROVIDER must be local or managed_http")
|
|
}
|
|
if c.ContentSafetyProvider == "managed_http" {
|
|
if !isAbsoluteHTTPURL(c.ContentSafetyEndpoint) || c.ContentSafetyAPIKey == "" || c.ContentSafetyPolicyRef == "" {
|
|
return errors.New("managed content safety requires an HTTP(S) endpoint, API key and policy reference")
|
|
}
|
|
}
|
|
if c.ContentGenerator != "local" && c.ContentGenerator != "ark" {
|
|
return errors.New("CONTENT_GENERATOR must be local or ark")
|
|
}
|
|
if c.ContentGenerator == "ark" {
|
|
parsed, err := url.Parse(c.ArkBaseURL)
|
|
if err != nil || parsed.Host == "" || (parsed.Scheme != "http" && parsed.Scheme != "https") {
|
|
return errors.New("ARK_BASE_URL must be an absolute HTTP(S) URL")
|
|
}
|
|
if c.ArkAPIKey == "" || c.ArkContentModel == "" {
|
|
return errors.New("ARK_API_KEY and ARK_CONTENT_MODEL are required for the Ark content generator")
|
|
}
|
|
}
|
|
}
|
|
if c.ServiceName == "api" || c.ServiceName == "worker" {
|
|
if c.EmbeddingProvider != "local" && c.EmbeddingProvider != "ark" {
|
|
return errors.New("EMBEDDING_PROVIDER must be local or ark")
|
|
}
|
|
if c.EmbeddingProvider == "ark" {
|
|
parsed, err := url.Parse(c.ArkBaseURL)
|
|
if err != nil || parsed.Host == "" || (parsed.Scheme != "http" && parsed.Scheme != "https") {
|
|
return errors.New("ARK_BASE_URL must be an absolute HTTP(S) URL")
|
|
}
|
|
if c.ArkAPIKey == "" || c.ArkEmbeddingModel == "" {
|
|
return errors.New("ARK_API_KEY and ARK_EMBEDDING_MODEL are required for the Ark embedding provider")
|
|
}
|
|
}
|
|
}
|
|
if c.ServiceName == "api" || c.ServiceName == "worker" {
|
|
if c.ObjectStoreEndpoint == "" || c.ObjectStorePublic == "" || c.ObjectStoreAccessKey == "" || c.ObjectStoreSecretKey == "" || c.ObjectStoreBucket == "" || c.ObjectStoreRegion == "" {
|
|
return errors.New("object store endpoint, public endpoint, credentials, region and export bucket are required")
|
|
}
|
|
if c.ExportDownloadTTL <= 0 || c.ExportDownloadTTL > 24*time.Hour || c.ExportObjectTTL <= 0 {
|
|
return errors.New("export download and object TTL must be positive, and download TTL cannot exceed 24 hours")
|
|
}
|
|
if c.MediaLocalDeleteGrace <= 0 || c.MediaLocalDeleteGrace > 7*24*time.Hour {
|
|
return errors.New("media local delete grace must be positive and cannot exceed 7 days")
|
|
}
|
|
}
|
|
if c.Environment == EnvironmentProd {
|
|
if err := c.validateProduction(); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (c Config) validateProduction() error {
|
|
if !isHTTPSURL(c.AuthIssuerURL) || !isHTTPSURL(c.AuthJWKSURL) {
|
|
return errors.New("production OIDC issuer and JWKS URLs must use HTTPS")
|
|
}
|
|
databaseURL, err := url.Parse(c.DatabaseURL)
|
|
if err != nil || databaseURL.Scheme == "" || strings.EqualFold(databaseURL.Query().Get("sslmode"), "disable") {
|
|
return errors.New("production DATABASE_URL must enable transport security")
|
|
}
|
|
if strings.TrimSpace(c.RedisPassword) == "" || isPlaceholderSecret(c.RedisPassword) {
|
|
return errors.New("production REDIS_PASSWORD must be a non-placeholder secret")
|
|
}
|
|
|
|
if c.ServiceName == "api" {
|
|
if c.DeviceCredentialMasterKey == "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f" {
|
|
return errors.New("production API must use a non-default DEVICE_CREDENTIAL_MASTER_KEY")
|
|
}
|
|
if isPlaceholderSecret(c.CursorSigningSecret) || isPlaceholderSecret(c.ShareTokenSecret) {
|
|
return errors.New("production API signing secrets must not use local or example values")
|
|
}
|
|
if c.CursorSigningSecret == c.ShareTokenSecret {
|
|
return errors.New("production API signing secrets must be distinct")
|
|
}
|
|
if isPlaceholderSecret(c.MemoryToolSigningSecret) || c.MemoryToolSigningSecret == c.CursorSigningSecret || c.MemoryToolSigningSecret == c.ShareTokenSecret {
|
|
return errors.New("production memory tool signing secret must be non-placeholder and distinct")
|
|
}
|
|
if c.MemoryToolBaseURL != "" && !isHTTPSURL(c.MemoryToolBaseURL) {
|
|
return errors.New("production memory tool URL must use HTTPS")
|
|
}
|
|
if isLocalValue(c.RTCAppID) || c.RTCTokenIssuer != "volcengine" || isPlaceholderSecret(c.RTCAppKey) {
|
|
return errors.New("production RTC credentials must use a real app ID, Volcengine token issuer and non-placeholder app key")
|
|
}
|
|
if c.EmbeddingProvider != "ark" || !isHTTPSURL(c.ArkBaseURL) || isPlaceholderSecret(c.ArkAPIKey) || isLocalValue(c.ArkEmbeddingModel) {
|
|
return errors.New("production memory retrieval must use Ark HTTPS, a non-placeholder API key and a real embedding model ID")
|
|
}
|
|
}
|
|
if c.ServiceName == "callbacks" && isPlaceholderSecret(c.ProviderCallbackSecret) {
|
|
return errors.New("production PROVIDER_CALLBACK_SECRET must not use a local or example value")
|
|
}
|
|
if c.ServiceName == "callbacks" && isPlaceholderSecret(c.PlatformHealthSecret) {
|
|
return errors.New("production PLATFORM_HEALTH_CALLBACK_SECRET must not use a local or example value")
|
|
}
|
|
if c.ServiceName == "worker" {
|
|
if c.VoiceProvider != "volcengine" || len(c.RTCAppID) != 24 || isLocalValue(c.RTCAppID) || !isHTTPSURL(c.VolcRTCBaseURL) || isLocalValue(c.VolcRTCBaseURL) || isPlaceholderSecret(c.VolcAccessKeyID) || isPlaceholderSecret(c.VolcSecretAccessKey) {
|
|
return errors.New("production voice provider must use signed Volcengine RTC HTTPS with non-placeholder AK/SK")
|
|
}
|
|
if c.AlertWebhookURL != "" && (!isHTTPSURL(c.AlertWebhookURL) || isPlaceholderSecret(c.AlertWebhookSecret)) {
|
|
return errors.New("production alert webhook must use HTTPS and a non-placeholder signing secret")
|
|
}
|
|
if c.ContentGenerator != "ark" || !isHTTPSURL(c.ArkBaseURL) || isPlaceholderSecret(c.ArkAPIKey) || isLocalValue(c.ArkContentModel) {
|
|
return errors.New("production content generation must use Ark HTTPS, a non-placeholder API key and a real model ID")
|
|
}
|
|
if c.ContentSafetyProvider != "managed_http" || !isHTTPSURL(c.ContentSafetyEndpoint) || isLocalValue(c.ContentSafetyEndpoint) || isPlaceholderSecret(c.ContentSafetyAPIKey) || isLocalValue(c.ContentSafetyPolicyRef) {
|
|
return errors.New("production content safety must use managed HTTPS with a non-placeholder API key and policy reference")
|
|
}
|
|
if c.EmbeddingProvider != "ark" || isLocalValue(c.ArkEmbeddingModel) {
|
|
return errors.New("production memory indexing must use a real Ark embedding model ID")
|
|
}
|
|
}
|
|
if c.ServiceName == "api" || c.ServiceName == "worker" {
|
|
if isPlaceholderSecret(c.MemoryToolSigningSecret) {
|
|
return errors.New("production memory tool signing secret must be non-placeholder")
|
|
}
|
|
if c.ProviderMemoryMode == "managed_http" && (!isHTTPSURL(c.ProviderMemoryBaseURL) || isLocalValue(c.ProviderMemoryBaseURL) || isPlaceholderSecret(c.ProviderMemoryAPIKey)) {
|
|
return errors.New("production provider memory must use managed HTTPS and a non-placeholder API key")
|
|
}
|
|
if !c.ObjectStoreSecure || !c.ObjectStorePublicSecure {
|
|
return errors.New("production object storage must use TLS")
|
|
}
|
|
if isLocalValue(c.ObjectStoreEndpoint) || isLocalValue(c.ObjectStorePublic) {
|
|
return errors.New("production object storage endpoints must not target localhost")
|
|
}
|
|
if isPlaceholderSecret(c.ObjectStoreAccessKey) || isPlaceholderSecret(c.ObjectStoreSecretKey) {
|
|
return errors.New("production object storage credentials must not use local or example values")
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func isHTTPSURL(value string) bool {
|
|
parsed, err := url.Parse(strings.TrimSpace(value))
|
|
return err == nil && strings.EqualFold(parsed.Scheme, "https") && parsed.Host != ""
|
|
}
|
|
|
|
func isAbsoluteHTTPURL(value string) bool {
|
|
parsed, err := url.Parse(strings.TrimSpace(value))
|
|
return err == nil && (strings.EqualFold(parsed.Scheme, "http") || strings.EqualFold(parsed.Scheme, "https")) && parsed.Host != ""
|
|
}
|
|
|
|
func isLocalValue(value string) bool {
|
|
normalized := strings.ToLower(strings.TrimSpace(value))
|
|
allZeroes := normalized != "" && strings.Trim(normalized, "0") == ""
|
|
return strings.Contains(normalized, "localhost") || strings.Contains(normalized, "127.0.0.1") ||
|
|
strings.Contains(normalized, "mock-provider") || strings.Contains(normalized, "example.invalid") ||
|
|
strings.Contains(normalized, "replace-with") || strings.HasPrefix(normalized, "local-") ||
|
|
strings.HasPrefix(normalized, "local_") || allZeroes
|
|
}
|
|
|
|
func isPlaceholderSecret(value string) bool {
|
|
normalized := strings.ToLower(strings.TrimSpace(value))
|
|
return len(normalized) < 32 || strings.Contains(normalized, "local-") || strings.Contains(normalized, "local_") || strings.Contains(normalized, "dev_only") || strings.Contains(normalized, "changeme") || strings.Contains(normalized, "example")
|
|
}
|
|
|
|
func loadSecretValues(names ...string) (map[string]string, error) {
|
|
values := make(map[string]string, len(names))
|
|
for _, name := range names {
|
|
value, err := secretEnv(name)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
values[name] = value
|
|
}
|
|
return values, nil
|
|
}
|
|
|
|
func secretEnv(name string) (string, error) {
|
|
inline := strings.TrimSpace(os.Getenv(name))
|
|
filePath := strings.TrimSpace(os.Getenv(name + "_FILE"))
|
|
if inline != "" && filePath != "" {
|
|
return "", fmt.Errorf("%s and %s_FILE cannot both be configured", name, name)
|
|
}
|
|
if filePath == "" {
|
|
return inline, nil
|
|
}
|
|
if !filepath.IsAbs(filePath) {
|
|
return "", fmt.Errorf("%s_FILE must be an absolute path", name)
|
|
}
|
|
file, err := os.Open(filepath.Clean(filePath))
|
|
if err != nil {
|
|
return "", fmt.Errorf("read %s_FILE: secret file is unavailable", name)
|
|
}
|
|
defer file.Close()
|
|
info, err := file.Stat()
|
|
if err != nil || !info.Mode().IsRegular() {
|
|
return "", fmt.Errorf("read %s_FILE: secret path must be a regular file", name)
|
|
}
|
|
content, err := io.ReadAll(io.LimitReader(file, 64*1024+1))
|
|
if err != nil || len(content) > 64*1024 {
|
|
return "", fmt.Errorf("read %s_FILE: secret file exceeds 64 KiB", name)
|
|
}
|
|
content = bytes.TrimRight(content, "\r\n")
|
|
if bytes.IndexByte(content, 0) >= 0 {
|
|
return "", fmt.Errorf("read %s_FILE: secret contains a NUL byte", name)
|
|
}
|
|
return string(content), nil
|
|
}
|
|
|
|
func secretDefault(value, fallback string) string {
|
|
if value == "" {
|
|
return fallback
|
|
}
|
|
return value
|
|
}
|
|
|
|
func defaultHTTPAddr(serviceName string) string {
|
|
switch serviceName {
|
|
case "callbacks":
|
|
return "0.0.0.0:58081"
|
|
case "worker":
|
|
return "0.0.0.0:58082"
|
|
default:
|
|
return "0.0.0.0:58080"
|
|
}
|
|
}
|
|
|
|
func stringEnv(key, fallback string) string {
|
|
value := strings.TrimSpace(os.Getenv(key))
|
|
if value == "" {
|
|
return fallback
|
|
}
|
|
return value
|
|
}
|
|
|
|
func stringEnvAllowEmpty(key, fallback string) string {
|
|
value, exists := os.LookupEnv(key)
|
|
if !exists {
|
|
return fallback
|
|
}
|
|
return strings.TrimSpace(value)
|
|
}
|
|
|
|
func intEnv(key string, fallback int) (int, error) {
|
|
value := strings.TrimSpace(os.Getenv(key))
|
|
if value == "" {
|
|
return fallback, nil
|
|
}
|
|
parsed, err := strconv.Atoi(value)
|
|
if err != nil {
|
|
return 0, fmt.Errorf("%s must be an integer: %w", key, err)
|
|
}
|
|
return parsed, nil
|
|
}
|
|
|
|
func floatEnv(key string, fallback float64) (float64, error) {
|
|
value := strings.TrimSpace(os.Getenv(key))
|
|
if value == "" {
|
|
return fallback, nil
|
|
}
|
|
parsed, err := strconv.ParseFloat(value, 64)
|
|
if err != nil {
|
|
return 0, fmt.Errorf("%s must be a number: %w", key, err)
|
|
}
|
|
return parsed, nil
|
|
}
|
|
|
|
func boolEnv(key string, fallback bool) (bool, error) {
|
|
value := strings.TrimSpace(os.Getenv(key))
|
|
if value == "" {
|
|
return fallback, nil
|
|
}
|
|
parsed, err := strconv.ParseBool(value)
|
|
if err != nil {
|
|
return false, fmt.Errorf("%s must be a boolean: %w", key, err)
|
|
}
|
|
return parsed, nil
|
|
}
|
|
|
|
func durationEnv(key string, fallback time.Duration) (time.Duration, error) {
|
|
value := strings.TrimSpace(os.Getenv(key))
|
|
if value == "" {
|
|
return fallback, nil
|
|
}
|
|
parsed, err := time.ParseDuration(value)
|
|
if err != nil {
|
|
return 0, fmt.Errorf("%s must be a duration: %w", key, err)
|
|
}
|
|
return parsed, nil
|
|
}
|