chore: establish local platform baseline
This commit is contained in:
@@ -0,0 +1,746 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"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 {
|
||||
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
|
||||
DeviceSecretDirectory 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
|
||||
ProviderCallbackSkew time.Duration
|
||||
MockProviderBaseURL string
|
||||
MockProviderAPIKey string
|
||||
VoiceProvider string
|
||||
VolcRTCBaseURL string
|
||||
VolcAccessKeyID string
|
||||
VolcSecretAccessKey string
|
||||
VolcSessionToken string
|
||||
VolcRTCRegion string
|
||||
VolcRTCAPIVersion string
|
||||
VolcVoiceChatTemplate 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
|
||||
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
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
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
|
||||
}
|
||||
objectStoreSecure, err := boolEnv("OBJECT_STORE_SECURE", false)
|
||||
if err != nil {
|
||||
return Config{}, err
|
||||
}
|
||||
environment := stringEnv("APP_ENV", EnvironmentLocal)
|
||||
secretValues, err := loadSecretValues(
|
||||
"DATABASE_URL", "REDIS_PASSWORD", "DEVICE_LOCAL_SECRET", "RTC_APP_KEY", "RTC_TOKEN_SIGNING_SECRET",
|
||||
"CURSOR_SIGNING_SECRET", "SHARE_TOKEN_SIGNING_SECRET", "PROVIDER_CALLBACK_SECRET",
|
||||
"MOCK_PROVIDER_API_KEY", "VOLC_ACCESS_KEY_ID", "VOLC_SECRET_ACCESS_KEY", "VOLC_SESSION_TOKEN",
|
||||
"OBJECT_STORE_ACCESS_KEY", "OBJECT_STORE_SECRET_KEY", "ALERT_WEBHOOK_SECRET", "ARK_API_KEY", "CONTENT_SAFETY_API_KEY",
|
||||
)
|
||||
if err != nil {
|
||||
return Config{}, err
|
||||
}
|
||||
deviceLocalSecret := secretValues["DEVICE_LOCAL_SECRET"]
|
||||
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 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"
|
||||
}
|
||||
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: stringEnv("OTEL_EXPORTER_OTLP_ENDPOINT", "localhost:54317"),
|
||||
DependencyTimeout: dependencyTimeout,
|
||||
ExternalProbeInterval: externalProbeInterval,
|
||||
ShutdownTimeout: shutdownTimeout,
|
||||
WorkerPollInterval: pollInterval,
|
||||
RecoveryScanInterval: recoveryScanInterval,
|
||||
RecoveryStartingAfter: recoveryStartingAfter,
|
||||
RecoveryStoppingAfter: recoveryStoppingAfter,
|
||||
DeviceLocalSecret: deviceLocalSecret,
|
||||
DeviceSecretDirectory: strings.TrimSpace(os.Getenv("DEVICE_SECRET_DIRECTORY")),
|
||||
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,
|
||||
ProviderCallbackSkew: providerCallbackSkew,
|
||||
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")),
|
||||
ProviderRequestTimeout: providerRequestTimeout,
|
||||
ProviderCircuitCooldown: providerCircuitCooldown,
|
||||
ObjectStoreEndpoint: objectStoreEndpoint,
|
||||
ObjectStorePublic: objectStorePublic,
|
||||
ObjectStoreAccessKey: objectStoreAccessKey,
|
||||
ObjectStoreSecretKey: objectStoreSecretKey,
|
||||
ObjectStoreBucket: objectStoreBucket,
|
||||
ObjectStoreRegion: objectStoreRegion,
|
||||
ObjectStoreSecure: objectStoreSecure,
|
||||
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")),
|
||||
}
|
||||
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.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 {
|
||||
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 {
|
||||
return errors.New("rate limits, bursts and provider task capacity must be positive")
|
||||
}
|
||||
if c.ServiceName == "api" && c.DeviceSecretDirectory == "" && len(c.DeviceLocalSecret) < 32 {
|
||||
return errors.New("DEVICE_LOCAL_SECRET must contain at least 32 characters when DEVICE_SECRET_DIRECTORY is not configured")
|
||||
}
|
||||
if c.ServiceName == "api" && c.DeviceSecretDirectory != "" && !filepath.IsAbs(c.DeviceSecretDirectory) {
|
||||
return errors.New("DEVICE_SECRET_DIRECTORY must be an absolute path")
|
||||
}
|
||||
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.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 == "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.ProviderRequestTimeout <= 0 || c.ProviderFailureThreshold < 1 || c.ProviderCircuitCooldown <= 0 {
|
||||
return errors.New("provider callback skew and request timeout must be positive")
|
||||
}
|
||||
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.DeviceSecretDirectory == "" {
|
||||
return errors.New("production API must use DEVICE_SECRET_DIRECTORY for per-device credentials")
|
||||
}
|
||||
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 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 == "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 !c.ObjectStoreSecure {
|
||||
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 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
|
||||
}
|
||||
@@ -0,0 +1,218 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestLoadUsesServicePortDefaults(t *testing.T) {
|
||||
t.Setenv("HTTP_ADDR", "")
|
||||
t.Setenv("APP_ENV", "test")
|
||||
|
||||
tests := map[string]string{
|
||||
"api": "0.0.0.0:58080",
|
||||
"callbacks": "0.0.0.0:58081",
|
||||
"worker": "0.0.0.0:58082",
|
||||
}
|
||||
for service, expected := range tests {
|
||||
cfg, err := Load(service)
|
||||
if err != nil {
|
||||
t.Fatalf("Load(%s) returned error: %v", service, err)
|
||||
}
|
||||
if cfg.HTTPAddr != expected {
|
||||
t.Fatalf("Load(%s).HTTPAddr = %s, want %s", service, cfg.HTTPAddr, expected)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadRejectsInvalidDuration(t *testing.T) {
|
||||
t.Setenv("DEPENDENCY_TIMEOUT", "soon")
|
||||
if _, err := Load("api"); err == nil {
|
||||
t.Fatal("Load should reject an invalid duration")
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadReadsSecretsFromAbsoluteFiles(t *testing.T) {
|
||||
secret := strings.Repeat("k", 40)
|
||||
path := filepath.Join(t.TempDir(), "ark-api-key")
|
||||
if err := os.WriteFile(path, []byte(secret+"\n"), 0o600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Setenv("ARK_API_KEY", "")
|
||||
t.Setenv("ARK_API_KEY_FILE", path)
|
||||
databaseURL := "postgres://voice:secret@db.example.com:5432/voice?sslmode=verify-full"
|
||||
databasePath := filepath.Join(t.TempDir(), "database-url")
|
||||
if err := os.WriteFile(databasePath, []byte(databaseURL+"\r\n"), 0o600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Setenv("DATABASE_URL", "")
|
||||
t.Setenv("DATABASE_URL_FILE", databasePath)
|
||||
cfg, err := Load("worker")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if cfg.ArkAPIKey != secret {
|
||||
t.Fatalf("secret file value was not loaded")
|
||||
}
|
||||
if cfg.DatabaseURL != databaseURL {
|
||||
t.Fatalf("database URL file value was not loaded")
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadRejectsAmbiguousSecretSources(t *testing.T) {
|
||||
path := filepath.Join(t.TempDir(), "redis-password")
|
||||
if err := os.WriteFile(path, []byte(strings.Repeat("r", 40)), 0o600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Setenv("REDIS_PASSWORD", strings.Repeat("x", 40))
|
||||
t.Setenv("REDIS_PASSWORD_FILE", path)
|
||||
if _, err := Load("api"); err == nil || !strings.Contains(err.Error(), "cannot both") {
|
||||
t.Fatalf("Load() error=%v, want ambiguous secret source rejection", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateRejectsUnknownEnvironment(t *testing.T) {
|
||||
cfg, err := Load("api")
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected load error: %v", err)
|
||||
}
|
||||
cfg.Environment = "production-ish"
|
||||
if err := cfg.Validate(); err == nil {
|
||||
t.Fatal("Validate should reject an unknown environment")
|
||||
}
|
||||
}
|
||||
|
||||
func TestProductionRejectsLocalDefaults(t *testing.T) {
|
||||
cfg, err := Load("api")
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected load error: %v", err)
|
||||
}
|
||||
cfg.Environment = EnvironmentProd
|
||||
if err := cfg.Validate(); err == nil || !strings.Contains(err.Error(), "HTTPS") {
|
||||
t.Fatalf("Validate() error = %v, want HTTPS production gate", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestProductionAPIAcceptsHardenedConfiguration(t *testing.T) {
|
||||
cfg, err := Load("api")
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected load error: %v", err)
|
||||
}
|
||||
cfg.Environment = EnvironmentProd
|
||||
cfg.DatabaseURL = "postgres://voice:secret@postgres.example.internal:5432/voice?sslmode=verify-full"
|
||||
cfg.RedisPassword = strings.Repeat("r", 40)
|
||||
cfg.AuthIssuerURL = "https://identity.example.com/realms/voice"
|
||||
cfg.AuthJWKSURL = "https://identity.example.com/realms/voice/certs"
|
||||
cfg.DeviceLocalSecret = strings.Repeat("d", 40)
|
||||
cfg.DeviceSecretDirectory = t.TempDir()
|
||||
cfg.RTCTokenSecret = strings.Repeat("t", 40)
|
||||
cfg.RTCTokenIssuer = "volcengine"
|
||||
cfg.RTCAppKey = strings.Repeat("v", 40)
|
||||
cfg.CursorSigningSecret = strings.Repeat("c", 40)
|
||||
cfg.ShareTokenSecret = strings.Repeat("h", 40)
|
||||
cfg.RTCAppID = "661e7ae5543cf00000000000"
|
||||
cfg.ObjectStoreEndpoint = "objects.internal.example.com:9000"
|
||||
cfg.ObjectStorePublic = "objects.example.com"
|
||||
cfg.ObjectStoreAccessKey = strings.Repeat("a", 40)
|
||||
cfg.ObjectStoreSecretKey = strings.Repeat("s", 40)
|
||||
cfg.ObjectStoreSecure = true
|
||||
cfg.EmbeddingProvider = "ark"
|
||||
cfg.ArkBaseURL = "https://ark.cn-beijing.volces.com/api/v3"
|
||||
cfg.ArkAPIKey = strings.Repeat("k", 40)
|
||||
cfg.ArkEmbeddingModel = "doubao-production-embedding"
|
||||
if err := cfg.Validate(); err != nil {
|
||||
t.Fatalf("Validate() returned error for hardened production config: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestProductionRejectsReusedSigningSecrets(t *testing.T) {
|
||||
cfg, err := Load("api")
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected load error: %v", err)
|
||||
}
|
||||
cfg.Environment = EnvironmentProd
|
||||
cfg.DatabaseURL = "postgres://voice:secret@postgres.example.internal:5432/voice?sslmode=require"
|
||||
cfg.RedisPassword = strings.Repeat("r", 40)
|
||||
cfg.AuthIssuerURL = "https://identity.example.com/realms/voice"
|
||||
cfg.AuthJWKSURL = "https://identity.example.com/realms/voice/certs"
|
||||
cfg.DeviceLocalSecret = strings.Repeat("x", 40)
|
||||
cfg.DeviceSecretDirectory = t.TempDir()
|
||||
cfg.RTCTokenSecret = strings.Repeat("x", 40)
|
||||
cfg.RTCTokenIssuer = "volcengine"
|
||||
cfg.RTCAppKey = strings.Repeat("v", 40)
|
||||
cfg.CursorSigningSecret = strings.Repeat("x", 40)
|
||||
cfg.ShareTokenSecret = strings.Repeat("x", 40)
|
||||
cfg.RTCAppID = "661e7ae5543cf00000000000"
|
||||
cfg.ObjectStoreEndpoint = "objects.internal.example.com:9000"
|
||||
cfg.ObjectStorePublic = "objects.example.com"
|
||||
cfg.ObjectStoreAccessKey = strings.Repeat("a", 40)
|
||||
cfg.ObjectStoreSecretKey = strings.Repeat("s", 40)
|
||||
cfg.ObjectStoreSecure = true
|
||||
cfg.EmbeddingProvider = "ark"
|
||||
cfg.ArkBaseURL = "https://ark.cn-beijing.volces.com/api/v3"
|
||||
cfg.ArkAPIKey = strings.Repeat("k", 40)
|
||||
cfg.ArkEmbeddingModel = "doubao-production-embedding"
|
||||
if err := cfg.Validate(); err == nil || !strings.Contains(err.Error(), "distinct") {
|
||||
t.Fatalf("Validate() error = %v, want distinct-secret gate", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestProductionWorkerRequiresRealArkContentGenerator(t *testing.T) {
|
||||
cfg, err := Load("worker")
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected load error: %v", err)
|
||||
}
|
||||
cfg.Environment = EnvironmentProd
|
||||
cfg.DatabaseURL = "postgres://voice:secret@postgres.example.internal:5432/voice?sslmode=verify-full"
|
||||
cfg.RedisPassword = strings.Repeat("r", 40)
|
||||
cfg.AuthIssuerURL = "https://identity.example.com/realms/voice"
|
||||
cfg.AuthJWKSURL = "https://identity.example.com/realms/voice/certs"
|
||||
cfg.VoiceProvider = "volcengine"
|
||||
cfg.VolcRTCBaseURL = "https://rtc.volcengineapi.com"
|
||||
cfg.VolcAccessKeyID = strings.Repeat("a", 40)
|
||||
cfg.VolcSecretAccessKey = strings.Repeat("v", 40)
|
||||
cfg.VolcRTCRegion = "cn-north-1"
|
||||
cfg.VolcRTCAPIVersion = "2024-12-01"
|
||||
cfg.VolcVoiceChatTemplate = "/run/secrets/voice-chat-template.json"
|
||||
cfg.RTCAppID = "661e7ae5543cf00000000000"
|
||||
cfg.ObjectStoreEndpoint = "objects.internal.example.com:9000"
|
||||
cfg.ObjectStorePublic = "objects.example.com"
|
||||
cfg.ObjectStoreAccessKey = strings.Repeat("a", 40)
|
||||
cfg.ObjectStoreSecretKey = strings.Repeat("s", 40)
|
||||
cfg.ObjectStoreSecure = true
|
||||
if err = cfg.Validate(); err == nil || !strings.Contains(err.Error(), "content generation") {
|
||||
t.Fatalf("Validate() error=%v, want local content generator production gate", err)
|
||||
}
|
||||
cfg.ContentGenerator = "ark"
|
||||
cfg.ArkBaseURL = "https://ark.cn-beijing.volces.com/api/v3"
|
||||
cfg.ArkAPIKey = strings.Repeat("k", 40)
|
||||
cfg.ArkContentModel = "doubao-production-model"
|
||||
cfg.ContentSafetyProvider = "managed_http"
|
||||
cfg.ContentSafetyEndpoint = "https://content-safety.example.com/v1/classify"
|
||||
cfg.ContentSafetyAPIKey = strings.Repeat("p", 40)
|
||||
cfg.ContentSafetyPolicyRef = "elder-story-production-v1"
|
||||
cfg.EmbeddingProvider = "ark"
|
||||
cfg.ArkEmbeddingModel = "doubao-production-embedding"
|
||||
if err = cfg.Validate(); err != nil {
|
||||
t.Fatalf("Validate() rejected production Ark worker: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestWorkerManagedContentSafetyConfiguration(t *testing.T) {
|
||||
cfg, err := Load("worker")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
cfg.ContentSafetyProvider = "managed_http"
|
||||
if err = cfg.Validate(); err == nil || !strings.Contains(err.Error(), "content safety") {
|
||||
t.Fatalf("Validate() error=%v, want incomplete managed content safety rejection", err)
|
||||
}
|
||||
cfg.ContentSafetyEndpoint = "http://127.0.0.1:18080/v1/classify"
|
||||
cfg.ContentSafetyAPIKey = strings.Repeat("s", 40)
|
||||
cfg.ContentSafetyPolicyRef = "local-contract-test-v1"
|
||||
if err = cfg.Validate(); err != nil {
|
||||
t.Fatalf("Validate() rejected complete non-production managed content safety config: %v", err)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user