mirror of
https://github.com/warmbly/warmbly.git
synced 2026-09-06 16:01:28 +00:00
897 lines
37 KiB
Go
897 lines
37 KiB
Go
package instanceconfig
|
|
|
|
import (
|
|
"context"
|
|
"os"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"github.com/warmbly/warmbly/internal/config"
|
|
)
|
|
|
|
// Docs anchors. Kept as constants so a renamed section is one edit.
|
|
const (
|
|
docsDeployment = "/development/configuration/#deployment"
|
|
docsSecrets = "/development/configuration/#secrets"
|
|
docsAddresses = "/development/configuration/#addresses"
|
|
docsProxy = "/development/configuration/#network-and-proxy"
|
|
docsSignIn = "/development/configuration/#authentication"
|
|
docsMail = "/development/configuration/#platform-mail"
|
|
docsEncryption = "/development/configuration/#encryption"
|
|
docsStorage = "/development/configuration/#storage"
|
|
docsEventBus = "/development/configuration/#event-bus"
|
|
docsDatabase = "/development/configuration/#database"
|
|
docsGeoIP = "/development/configuration/#geoip"
|
|
docsWorkers = "/development/configuration/#workers"
|
|
docsCaptcha = "/development/configuration/#captcha"
|
|
docsSSO = "/development/accounts-and-access/#single-sign-on"
|
|
docsFirstOwner = "/development/accounts-and-access/#first-owner"
|
|
docsUpdates = "/development/updates/"
|
|
// The database-backed settings document, which is the one tier the
|
|
// environment does not own.
|
|
docsSettingsDoc = "/development/configuration/#settings-stored-in-the-database"
|
|
)
|
|
|
|
// table is the static inventory. Declaration order is display order.
|
|
var table = []Entry{
|
|
// Deployment.
|
|
{
|
|
Key: "APP_ENV", Group: GroupDeployment, RuntimeChangeable: ChangeBootOnly,
|
|
Effect: "dev tolerates the published default secrets and turns on Gin debug logging. Set prod for anything other people can reach.",
|
|
DocsAnchor: docsDeployment,
|
|
Resolve: envOr("APP_ENV", "dev"),
|
|
},
|
|
{
|
|
Key: "DEPLOYMENT_MODE", Group: GroupDeployment, RuntimeChangeable: ChangeBootOnly,
|
|
Effect: "Picks the auth defaults (login code, signup lockdown, email verification). Each stays individually overridable.",
|
|
DocsAnchor: docsDeployment, WhenUnset: SourceDerived,
|
|
Resolve: func(*Runtime) string {
|
|
if v := trimmed("DEPLOYMENT_MODE"); v != "" {
|
|
return v
|
|
}
|
|
if config.SelfHosted() {
|
|
return "self_hosted"
|
|
}
|
|
return "cloud"
|
|
},
|
|
},
|
|
{
|
|
Key: "ALLOW_INSECURE_DEFAULTS", Group: GroupDeployment, RuntimeChangeable: ChangeBootOnly,
|
|
Effect: "true lets the backend start while a published default secret is still in use, instead of refusing.",
|
|
DocsAnchor: docsSecrets,
|
|
Resolve: boolOr("ALLOW_INSECURE_DEFAULTS", false),
|
|
},
|
|
{
|
|
Key: "GIN_MODE", Group: GroupDeployment, RuntimeChangeable: ChangeBootOnly,
|
|
Effect: "debug logs every route and request body; release is the production posture.",
|
|
DocsAnchor: docsDeployment, WhenUnset: SourceDerived,
|
|
Resolve: func(*Runtime) string {
|
|
if v := trimmed("GIN_MODE"); v != "" {
|
|
return v
|
|
}
|
|
if trimmed("APP_ENV") == "prod" {
|
|
return "release"
|
|
}
|
|
return "debug"
|
|
},
|
|
},
|
|
{
|
|
Key: "ENV_LABEL", Group: GroupDeployment, RuntimeChangeable: ChangeBootOnly,
|
|
Effect: "The badge the admin panel shows so a production panel is not mistaken for a local one.",
|
|
DocsAnchor: docsDeployment,
|
|
Resolve: envValue("ENV_LABEL"),
|
|
},
|
|
{
|
|
Key: "BILLING_PROVIDER", Group: GroupDeployment, RuntimeChangeable: ChangeBootOnly,
|
|
Effect: "none runs with no Stripe, every feature unlocked and no trial expiry.",
|
|
DocsAnchor: docsDeployment,
|
|
Resolve: func(*Runtime) string { return config.BillingProvider() },
|
|
},
|
|
{
|
|
Key: "TASKS_PROVIDER", Group: GroupDeployment, RuntimeChangeable: ChangeBootOnly,
|
|
Effect: "local runs an in-process Postgres poller for campaign ticks and scheduled sends, with no external service.",
|
|
DocsAnchor: docsDeployment,
|
|
Resolve: func(*Runtime) string { return config.TasksProvider() },
|
|
},
|
|
{
|
|
Key: "TASKS_LOCAL_POLL_INTERVAL", Group: GroupDeployment, RuntimeChangeable: ChangeBootOnly,
|
|
Effect: "How often the local task poller looks for due work.",
|
|
DocsAnchor: docsDeployment,
|
|
Resolve: envOr("TASKS_LOCAL_POLL_INTERVAL", "1s"),
|
|
},
|
|
{
|
|
Key: "WARMBLY_ALLOW_UNSAFE_WEBHOOK_URLS", Group: GroupDeployment, RuntimeChangeable: ChangePerRequest,
|
|
Effect: "true lets customer webhooks point at http:// and private addresses. Development only.",
|
|
DocsAnchor: docsDeployment,
|
|
Resolve: boolOr("WARMBLY_ALLOW_UNSAFE_WEBHOOK_URLS", false),
|
|
},
|
|
{
|
|
Key: "AI_PROVIDER", Group: GroupDeployment, RuntimeChangeable: ChangeBootOnly,
|
|
Effect: "Selects the LLM backend for the writing assistant, the dashboard agent and warmup content. Empty runs with AI off.",
|
|
DocsAnchor: docsDeployment,
|
|
Resolve: envValue("AI_PROVIDER"),
|
|
},
|
|
{
|
|
Key: "AI_MODEL", Group: GroupDeployment, RuntimeChangeable: ChangeBootOnly,
|
|
Effect: "The model id sent to the AI provider.",
|
|
DocsAnchor: docsDeployment,
|
|
Resolve: envValue("AI_MODEL"),
|
|
},
|
|
{
|
|
Key: "AI_BASE_URL", Group: GroupDeployment, RuntimeChangeable: ChangeBootOnly,
|
|
Effect: "The API base for a custom or self-hosted AI provider.",
|
|
DocsAnchor: docsDeployment,
|
|
Resolve: envValue("AI_BASE_URL"),
|
|
},
|
|
{
|
|
Key: "AI_API_KEY", Group: GroupDeployment, RuntimeChangeable: ChangeBootOnly,
|
|
Effect: "Credential for the AI provider. An empty AI_PROVIDER with this set sends the key to api.openai.com.",
|
|
DocsAnchor: docsDeployment,
|
|
Resolve: envValue("AI_API_KEY"),
|
|
},
|
|
|
|
// Addresses.
|
|
{
|
|
Key: "APP_URL", Aliases: []string{"FRONTEND_BASE_URL"}, Group: GroupAddresses, RuntimeChangeable: ChangePerRequest,
|
|
Effect: "The dashboard origin every emailed link is built from: password resets, invitations and the setup link.",
|
|
DocsAnchor: docsAddresses,
|
|
Resolve: func(*Runtime) string { return config.AppBaseURL() },
|
|
},
|
|
{
|
|
Key: "API_PUBLIC_URL", Group: GroupAddresses, RuntimeChangeable: ChangeBootOnly,
|
|
Effect: "This backend's public base. The OIDC redirect URL and the frontends' API base derive from it.",
|
|
DocsAnchor: docsAddresses,
|
|
Resolve: envValue("API_PUBLIC_URL"),
|
|
},
|
|
{
|
|
Key: "API_HOST", Group: GroupAddresses, RuntimeChangeable: ChangeBootOnly,
|
|
Effect: "The address the API binds. 0.0.0.0:8080 already binds every interface.",
|
|
DocsAnchor: docsAddresses,
|
|
Resolve: envOr("API_HOST", "0.0.0.0:8080"),
|
|
},
|
|
{
|
|
Key: "APP_ORIGIN", Group: GroupAddresses, RuntimeChangeable: ChangePerRequest,
|
|
Effect: "The target origin the mailbox OAuth callback posts the authorization code back to. Empty means a wildcard target.",
|
|
DocsAnchor: docsAddresses,
|
|
Resolve: envValue("APP_ORIGIN"),
|
|
},
|
|
{
|
|
Key: "WEBSOCKET_URL", Group: GroupAddresses, RuntimeChangeable: ChangeBootOnly,
|
|
Effect: "The realtime endpoint the dashboard connects to for live updates and presence.",
|
|
DocsAnchor: docsAddresses,
|
|
Resolve: func(rt *Runtime) string {
|
|
if rt != nil && rt.WebsocketURL != "" {
|
|
return rt.WebsocketURL
|
|
}
|
|
return os.Getenv("WEBSOCKET_URL")
|
|
},
|
|
},
|
|
{
|
|
Key: "CORS_ALLOW_ORIGINS", Group: GroupAddresses, RuntimeChangeable: ChangeBootOnly,
|
|
Effect: "The browser origins allowed to call this API. When unset it derives from APP_URL, the websocket origin and, outside prod, the local dev ports.",
|
|
DocsAnchor: docsAddresses, WhenUnset: SourceDerived,
|
|
Resolve: func(rt *Runtime) string {
|
|
if rt != nil && len(rt.CORSOrigins) > 0 {
|
|
return strings.Join(rt.CORSOrigins, ", ")
|
|
}
|
|
return os.Getenv("CORS_ALLOW_ORIGINS")
|
|
},
|
|
},
|
|
{
|
|
Key: "TRUSTED_PROXIES", Group: GroupAddresses, RuntimeChangeable: ChangeBootOnly,
|
|
Effect: "CIDRs allowed to set X-Forwarded-For. Empty trusts nothing, which is correct for a directly exposed backend.",
|
|
DocsAnchor: docsProxy,
|
|
Resolve: envValue("TRUSTED_PROXIES"),
|
|
},
|
|
|
|
// Database, cache and GeoIP.
|
|
{
|
|
Key: "PRIMARY_DB", Group: GroupDatabase, RuntimeChangeable: ChangeBootOnly,
|
|
Effect: "The Postgres connection string. Migrations are applied against it at boot.",
|
|
DocsAnchor: docsDatabase,
|
|
Resolve: envValue("PRIMARY_DB"),
|
|
},
|
|
{
|
|
Key: "GEODB_PATH", Group: GroupDatabase, RuntimeChangeable: ChangeBootOnly,
|
|
Effect: "Path to a GeoLite2 database. It must be set on the backend in every environment; a missing file at that path is tolerated and only costs city labels.",
|
|
DocsAnchor: docsGeoIP,
|
|
Resolve: envValue("GEODB_PATH"),
|
|
},
|
|
{
|
|
Key: "REDIS", Group: GroupCache, RuntimeChangeable: ChangeBootOnly,
|
|
Effect: "The Redis connection string. Rate limits, the organization key cache, the setup token and the realtime bridge all live there.",
|
|
DocsAnchor: docsDatabase,
|
|
Resolve: envValue("REDIS"),
|
|
},
|
|
|
|
// Platform mail.
|
|
{
|
|
Key: "MAIL_TRANSPORT", Group: GroupMail, RuntimeChangeable: ChangeBootOnly,
|
|
Effect: "smtp delivers through a relay, ses through AWS, log writes messages to the backend log and delivers nothing.",
|
|
DocsAnchor: docsMail, WhenUnset: SourceDerived,
|
|
Resolve: func(rt *Runtime) string {
|
|
if rt != nil && rt.MailTransportKind != "" {
|
|
return rt.MailTransportKind
|
|
}
|
|
return config.MailTransport()
|
|
},
|
|
},
|
|
{
|
|
Key: "EMAIL_NAME", Group: GroupMail, RuntimeChangeable: ChangeBootOnly,
|
|
Effect: "The display name on platform mail. The backend refuses to start without it; the consumer only warns and silently sends nothing.",
|
|
DocsAnchor: docsMail,
|
|
Resolve: envValue("EMAIL_NAME"),
|
|
},
|
|
{
|
|
Key: "EMAIL_ADDRESS", Group: GroupMail, RuntimeChangeable: ChangeBootOnly,
|
|
Effect: "The From address on platform mail.",
|
|
DocsAnchor: docsMail,
|
|
Resolve: envValue("EMAIL_ADDRESS"),
|
|
},
|
|
{
|
|
Key: "SMTP_HOST", Group: GroupMail, RuntimeChangeable: ChangeBootOnly,
|
|
Effect: "The submission relay. Setting it selects the smtp transport when MAIL_TRANSPORT is unset.",
|
|
DocsAnchor: docsMail,
|
|
Resolve: envValue("SMTP_HOST"),
|
|
},
|
|
{
|
|
Key: "SMTP_PORT", Group: GroupMail, RuntimeChangeable: ChangeBootOnly,
|
|
Effect: "Defaults per security mode: 465 for tls, 25 for none, 587 otherwise.",
|
|
DocsAnchor: docsMail, WhenUnset: SourceDerived,
|
|
Resolve: func(*Runtime) string {
|
|
if s := smtpConfig(); s != nil {
|
|
return s.Port
|
|
}
|
|
return ""
|
|
},
|
|
},
|
|
{
|
|
Key: "SMTP_SECURITY", Group: GroupMail, RuntimeChangeable: ChangeBootOnly,
|
|
Effect: "starttls upgrades in-band on 587, tls is implicit TLS on 465, none is cleartext and only legitimate for a local sink.",
|
|
DocsAnchor: docsMail, WhenUnset: SourceDerived,
|
|
Resolve: func(*Runtime) string {
|
|
if s := smtpConfig(); s != nil {
|
|
return s.Security
|
|
}
|
|
return ""
|
|
},
|
|
},
|
|
{
|
|
Key: "SMTP_AUTH", Group: GroupMail, RuntimeChangeable: ChangeBootOnly,
|
|
Effect: "auto picks the strongest mechanism the relay advertises.",
|
|
DocsAnchor: docsMail, WhenUnset: SourceDerived,
|
|
Resolve: func(*Runtime) string {
|
|
if s := smtpConfig(); s != nil {
|
|
return s.Auth
|
|
}
|
|
return ""
|
|
},
|
|
},
|
|
{
|
|
Key: "SMTP_USERNAME", Group: GroupMail, RuntimeChangeable: ChangeBootOnly,
|
|
Effect: "The relay account. Credentials are never sent over an unencrypted connection.",
|
|
DocsAnchor: docsMail,
|
|
Resolve: envValue("SMTP_USERNAME"),
|
|
},
|
|
{
|
|
Key: "SMTP_PASSWORD", Group: GroupMail, RuntimeChangeable: ChangeBootOnly,
|
|
Effect: "The relay password.",
|
|
DocsAnchor: docsMail,
|
|
Resolve: envValue("SMTP_PASSWORD"),
|
|
},
|
|
{
|
|
Key: "SMTP_EHLO_NAME", Group: GroupMail, RuntimeChangeable: ChangeBootOnly,
|
|
Effect: "The name announced to the relay. Defaults to the sender domain, because announcing localhost scores as suspicious.",
|
|
DocsAnchor: docsMail, WhenUnset: SourceDerived,
|
|
Resolve: envValue("SMTP_EHLO_NAME"),
|
|
},
|
|
{
|
|
Key: "SMTP_TLS_INSECURE_SKIP_VERIFY", Group: GroupMail, RuntimeChangeable: ChangeBootOnly,
|
|
Effect: "true disables certificate verification on the platform relay. Only for a relay with a private certificate authority.",
|
|
DocsAnchor: docsMail,
|
|
Resolve: boolOr("SMTP_TLS_INSECURE_SKIP_VERIFY", false),
|
|
},
|
|
{
|
|
Key: "NOTIFICATION_EMAIL_DAILY_CAP", Group: GroupMail, RuntimeChangeable: ChangePerRequest,
|
|
Effect: "How many notification digest emails one user can receive in a day.",
|
|
DocsAnchor: docsMail,
|
|
Resolve: envValue("NOTIFICATION_EMAIL_DAILY_CAP"),
|
|
},
|
|
{
|
|
Key: "EMAIL_BRAND_NAME", Group: GroupMail, RuntimeChangeable: ChangePerRequest,
|
|
Effect: "The brand platform mail is attributed to. A self-hosted install should not send mail attributed to another company.",
|
|
DocsAnchor: docsMail,
|
|
Resolve: envValue("EMAIL_BRAND_NAME"),
|
|
},
|
|
|
|
// Authentication and access.
|
|
{
|
|
Key: "AUTH_SECRET", Group: GroupAuth, RuntimeChangeable: ChangeBootOnly,
|
|
Effect: "Signs every session and JWT. It must be byte-identical to the realtime service's JWT_SECRET; compare the fingerprints.",
|
|
DocsAnchor: docsSecrets,
|
|
Resolve: envValue("AUTH_SECRET"),
|
|
},
|
|
{
|
|
Key: "TWOFA_SECRET", Group: GroupAuth, RuntimeChangeable: ChangeBootOnly,
|
|
Effect: "Seals stored TOTP secrets. Falls back to AUTH_SECRET; rotating it invalidates every enrolled authenticator.",
|
|
DocsAnchor: docsSecrets, WhenUnset: SourceDerived,
|
|
Resolve: func(*Runtime) string {
|
|
if v := trimmed("TWOFA_SECRET"); v != "" {
|
|
return v
|
|
}
|
|
return os.Getenv("AUTH_SECRET")
|
|
},
|
|
},
|
|
{
|
|
Key: "AUTH_LOGIN_CODE", Group: GroupAuth, RuntimeChangeable: ChangeBootOnly,
|
|
Effect: "always emails a code on every login, new_device only on an unrecognised device, off never. A transport that does not deliver demotes always to new_device.",
|
|
DocsAnchor: docsSignIn, WhenUnset: SourceDerived,
|
|
Resolve: func(rt *Runtime) string { return policy(rt).LoginCode },
|
|
},
|
|
{
|
|
Key: "DISABLE_REGISTRATION", Group: GroupAuth, RuntimeChangeable: ChangeBootOnly,
|
|
Effect: "false is open signup, invite_only accepts only invited addresses, true closes signup and invitations both.",
|
|
DocsAnchor: docsSignIn, WhenUnset: SourceDerived,
|
|
Resolve: func(rt *Runtime) string { return policy(rt).Registration },
|
|
},
|
|
{
|
|
Key: "REQUIRE_EMAIL_VERIFICATION", Group: GroupAuth, RuntimeChangeable: ChangeBootOnly,
|
|
Effect: "true makes registration complete an emailed code before the account exists.",
|
|
DocsAnchor: docsSignIn, WhenUnset: SourceDerived,
|
|
Resolve: func(rt *Runtime) string { return yesNo(policy(rt).RequireEmailVerification) },
|
|
},
|
|
{
|
|
Key: "DISABLE_PASSWORD_LOGIN", Group: GroupAuth, RuntimeChangeable: ChangeBootOnly,
|
|
Effect: "true turns off email and password entirely, for deployments that authenticate through OIDC only.",
|
|
DocsAnchor: docsSignIn, WhenUnset: SourceDerived,
|
|
Resolve: func(rt *Runtime) string { return yesNo(policy(rt).DisablePasswordLogin) },
|
|
},
|
|
{
|
|
Key: "SSO_AUTO_PROVISION", Group: GroupAuth, RuntimeChangeable: ChangeBootOnly,
|
|
Effect: "true lets a verified identity-provider assertion create an account regardless of DISABLE_REGISTRATION.",
|
|
DocsAnchor: docsSSO, WhenUnset: SourceDerived,
|
|
Resolve: func(rt *Runtime) string { return yesNo(policy(rt).SSOAutoProvision) },
|
|
},
|
|
{
|
|
Key: "AUTH_IP_RATE_LIMIT", Group: GroupAuth, RuntimeChangeable: ChangePerRequest,
|
|
Effect: "Unauthenticated auth requests allowed per IP per 15 minutes.",
|
|
DocsAnchor: docsSignIn,
|
|
Resolve: envOr("AUTH_IP_RATE_LIMIT", "60"),
|
|
},
|
|
{
|
|
Key: "WEBAUTHN_RP_ID", Group: GroupAuth, RuntimeChangeable: ChangeBootOnly,
|
|
Effect: "The domain passkeys are cryptographically bound to. Changing it invalidates every enrolled passkey.",
|
|
DocsAnchor: docsSignIn, WhenUnset: SourceDerived,
|
|
Resolve: func(rt *Runtime) string {
|
|
if rt != nil && rt.WebAuthnRPID != "" {
|
|
return rt.WebAuthnRPID
|
|
}
|
|
return os.Getenv("WEBAUTHN_RP_ID")
|
|
},
|
|
},
|
|
{
|
|
Key: "WEBAUTHN_RP_ORIGINS", Group: GroupAuth, RuntimeChangeable: ChangeBootOnly,
|
|
Effect: "The full origins allowed to run passkey ceremonies. Derived from CORS_ALLOW_ORIGINS or APP_URL when unset.",
|
|
DocsAnchor: docsSignIn, WhenUnset: SourceDerived,
|
|
Resolve: func(rt *Runtime) string {
|
|
if rt != nil && len(rt.WebAuthnRPOrigins) > 0 {
|
|
return strings.Join(rt.WebAuthnRPOrigins, ", ")
|
|
}
|
|
return os.Getenv("WEBAUTHN_RP_ORIGINS")
|
|
},
|
|
},
|
|
{
|
|
Key: "OIDC_ISSUER_URL", Group: GroupAuth, RuntimeChangeable: ChangeBootOnly,
|
|
Effect: "The OpenID Connect issuer. Setting it enables the single sign-on button, which is the only sign-in path with no dependency on outbound mail.",
|
|
DocsAnchor: docsSSO,
|
|
Resolve: envValue("OIDC_ISSUER_URL"),
|
|
},
|
|
{
|
|
Key: "OIDC_CLIENT_ID", Group: GroupAuth, RuntimeChangeable: ChangeBootOnly,
|
|
Effect: "The client this backend registers as with the issuer.",
|
|
DocsAnchor: docsSSO,
|
|
Resolve: envValue("OIDC_CLIENT_ID"),
|
|
},
|
|
{
|
|
Key: "OIDC_CLIENT_SECRET", Group: GroupAuth, RuntimeChangeable: ChangeBootOnly,
|
|
Effect: "The client secret for the OIDC token exchange.",
|
|
DocsAnchor: docsSSO,
|
|
Resolve: envValue("OIDC_CLIENT_SECRET"),
|
|
},
|
|
{
|
|
Key: "OIDC_REDIRECT_URL", Group: GroupAuth, RuntimeChangeable: ChangeBootOnly,
|
|
Effect: "Where the provider sends the browser back. Derived from API_PUBLIC_URL when unset; empty disables the OIDC path.",
|
|
DocsAnchor: docsSSO, WhenUnset: SourceDerived,
|
|
Resolve: func(rt *Runtime) string {
|
|
if rt != nil && rt.OIDCRedirectURL != "" {
|
|
return rt.OIDCRedirectURL
|
|
}
|
|
if v := trimmed("OIDC_REDIRECT_URL"); v != "" {
|
|
return v
|
|
}
|
|
if base := strings.TrimRight(trimmed("API_PUBLIC_URL"), "/"); base != "" {
|
|
return base + "/v1/auth/oidc/callback"
|
|
}
|
|
return ""
|
|
},
|
|
},
|
|
{
|
|
Key: "OIDC_SCOPES", Group: GroupAuth, RuntimeChangeable: ChangeBootOnly,
|
|
Effect: "Scopes requested from the issuer.",
|
|
DocsAnchor: docsSSO,
|
|
Resolve: envOr("OIDC_SCOPES", "openid,profile,email"),
|
|
},
|
|
{
|
|
Key: "OIDC_ALLOWED_DOMAINS", Group: GroupAuth, RuntimeChangeable: ChangeBootOnly,
|
|
Effect: "Restricts single sign-on to these email domains. Empty allows every address the issuer asserts.",
|
|
DocsAnchor: docsSSO,
|
|
Resolve: envValue("OIDC_ALLOWED_DOMAINS"),
|
|
},
|
|
{
|
|
Key: "OIDC_DEFAULT_ORG", Group: GroupAuth, RuntimeChangeable: ChangeBootOnly,
|
|
Effect: "The organization every single sign-on user joins. Without it each new user gets their own single-member organization.",
|
|
DocsAnchor: docsSSO,
|
|
Resolve: envValue("OIDC_DEFAULT_ORG"),
|
|
},
|
|
{
|
|
Key: "OIDC_PROVIDER_NAME", Group: GroupAuth, RuntimeChangeable: ChangeBootOnly,
|
|
Effect: "The label on the single sign-on button.",
|
|
DocsAnchor: docsSSO,
|
|
Resolve: envOr("OIDC_PROVIDER_NAME", "Single sign-on"),
|
|
},
|
|
{
|
|
Key: "GOOGLE_CLIENT_ID", Group: GroupAuth, RuntimeChangeable: ChangeBootOnly,
|
|
Effect: "The Google sign-in client, separate from the BOX_GOOGLE_* mailbox client.",
|
|
DocsAnchor: docsSignIn,
|
|
Resolve: envValue("GOOGLE_CLIENT_ID"),
|
|
},
|
|
{
|
|
Key: "GOOGLE_CLIENT_SECRET", Group: GroupAuth, RuntimeChangeable: ChangeBootOnly,
|
|
Effect: "The Google sign-in client secret.",
|
|
DocsAnchor: docsSignIn,
|
|
Resolve: envValue("GOOGLE_CLIENT_SECRET"),
|
|
},
|
|
{
|
|
Key: "GOOGLE_REDIRECT_URI", Group: GroupAuth, RuntimeChangeable: ChangeBootOnly,
|
|
Effect: "Where Google sends the browser back after sign-in. Served by the API, not the dashboard; derived from API_PUBLIC_URL when unset. This is the URI to register at the provider.",
|
|
DocsAnchor: docsSignIn, WhenUnset: SourceDerived,
|
|
Resolve: ssoRedirect("GOOGLE_REDIRECT_URI", "google"),
|
|
},
|
|
{
|
|
Key: "APPLE_APP_ID", Group: GroupAuth, RuntimeChangeable: ChangeBootOnly,
|
|
Effect: "The Sign in with Apple service identifier (the Services ID, not the app's bundle id).",
|
|
DocsAnchor: docsSignIn,
|
|
Resolve: envValue("APPLE_APP_ID"),
|
|
},
|
|
{
|
|
Key: "APPLE_REDIRECT_URI", Group: GroupAuth, RuntimeChangeable: ChangeBootOnly,
|
|
Effect: "Where Apple sends the browser back after sign-in. Must be https; derived from API_PUBLIC_URL when unset.",
|
|
DocsAnchor: docsSignIn, WhenUnset: SourceDerived,
|
|
Resolve: ssoRedirect("APPLE_REDIRECT_URI", "apple"),
|
|
},
|
|
{
|
|
Key: "APPLE_TEAM_ID", Group: GroupAuth, RuntimeChangeable: ChangeBootOnly,
|
|
Effect: "The Apple developer team the key belongs to.",
|
|
DocsAnchor: docsSignIn,
|
|
Resolve: envValue("APPLE_TEAM_ID"),
|
|
},
|
|
{
|
|
Key: "APPLE_KEY_ID", Group: GroupAuth, RuntimeChangeable: ChangeBootOnly,
|
|
Effect: "The Apple private key identifier.",
|
|
DocsAnchor: docsSignIn,
|
|
Resolve: envValue("APPLE_KEY_ID"),
|
|
},
|
|
{
|
|
Key: "APPLE_KEY_SECRET", Group: GroupAuth, RuntimeChangeable: ChangeBootOnly,
|
|
Effect: "The Apple private key used to sign client assertions.",
|
|
DocsAnchor: docsSignIn,
|
|
Resolve: envValue("APPLE_KEY_SECRET"),
|
|
},
|
|
{
|
|
Key: "WARMBLY_BOOTSTRAP_EMAIL", Group: GroupAuth, RuntimeChangeable: ChangeBootOnly,
|
|
Effect: "Creates the first owner unattended. Read only while the users table is empty.",
|
|
DocsAnchor: docsFirstOwner,
|
|
Resolve: envValue("WARMBLY_BOOTSTRAP_EMAIL"),
|
|
},
|
|
{
|
|
Key: "WARMBLY_BOOTSTRAP_PASSWORD_HASH", Group: GroupAuth, RuntimeChangeable: ChangeBootOnly,
|
|
Effect: "The argon2 PHC string for the first owner. Preferred over the plaintext variant.",
|
|
DocsAnchor: docsFirstOwner,
|
|
Resolve: envValue("WARMBLY_BOOTSTRAP_PASSWORD_HASH"),
|
|
},
|
|
{
|
|
Key: "WARMBLY_BOOTSTRAP_PASSWORD", Group: GroupAuth, RuntimeChangeable: ChangeBootOnly,
|
|
Effect: "A plaintext first-owner password. Remove it once the account exists: it is read only while the users table is empty.",
|
|
DocsAnchor: docsFirstOwner,
|
|
Resolve: envValue("WARMBLY_BOOTSTRAP_PASSWORD"),
|
|
},
|
|
{
|
|
Key: "WARMBLY_BOOTSTRAP_ORG", Group: GroupAuth, RuntimeChangeable: ChangeBootOnly,
|
|
Effect: "The name of the organization created alongside the first owner.",
|
|
DocsAnchor: docsFirstOwner,
|
|
Resolve: envValue("WARMBLY_BOOTSTRAP_ORG"),
|
|
},
|
|
{
|
|
Key: "WARMBLY_SETTINGS_BOOTSTRAP", Group: GroupDeployment, RuntimeChangeable: ChangeBootOnly,
|
|
Effect: "Seeds the database-backed settings document (sync budgets, retention windows) on an instance that has never saved one. A no-op from the first save in Instance settings onwards, so leaving it here cannot undo an edit made there.",
|
|
DocsAnchor: docsSettingsDoc,
|
|
Resolve: envValue("WARMBLY_SETTINGS_BOOTSTRAP"),
|
|
},
|
|
|
|
// Captcha.
|
|
{
|
|
Key: "CAPTCHA_PROVIDER", Group: GroupCaptcha, RuntimeChangeable: ChangeBootOnly,
|
|
Effect: "turnstile requires a Cloudflare Turnstile solution on the auth endpoints. Auto-off when no secret is set.",
|
|
DocsAnchor: docsCaptcha, WhenUnset: SourceDerived,
|
|
Resolve: func(*Runtime) string { return config.CaptchaProvider() },
|
|
},
|
|
{
|
|
Key: "TURNSTILE_SECRET", Group: GroupCaptcha, RuntimeChangeable: ChangeBootOnly,
|
|
Effect: "The Turnstile server-side secret. Empty with CAPTCHA_PROVIDER=turnstile fails every verification.",
|
|
DocsAnchor: docsCaptcha,
|
|
Resolve: envValue("TURNSTILE_SECRET"),
|
|
},
|
|
{
|
|
Key: "TURNSTILE_BYPASS_TOKEN", Group: GroupCaptcha, RuntimeChangeable: ChangePerRequest,
|
|
Effect: "A token that skips captcha verification. Honoured only when APP_ENV is dev.",
|
|
DocsAnchor: docsCaptcha,
|
|
Resolve: envValue("TURNSTILE_BYPASS_TOKEN"),
|
|
},
|
|
{
|
|
Key: "TURNSTILE_SITE_KEY", Group: GroupCaptcha, RuntimeChangeable: ChangePerRequest,
|
|
Effect: "The public Turnstile widget key hosted form pages render with. Unset leaves spam-protected forms without a captcha.",
|
|
DocsAnchor: docsCaptcha,
|
|
Resolve: envValue("TURNSTILE_SITE_KEY"),
|
|
},
|
|
|
|
// Encryption.
|
|
{
|
|
Key: "KMS_PROVIDER", Group: GroupEncryption, RuntimeChangeable: ChangeBootOnly,
|
|
Effect: "local wraps organization keys with the master key below; aws wraps them with AWS KMS.",
|
|
DocsAnchor: docsEncryption,
|
|
Resolve: func(*Runtime) string { return config.KMSProvider() },
|
|
},
|
|
{
|
|
Key: "KMS_LOCAL_MASTER_KEY", Group: GroupEncryption, RuntimeChangeable: ChangeBootOnly,
|
|
Effect: "The base64 32-byte root key every organization key is wrapped with. Back it up: losing it is unrecoverable.",
|
|
DocsAnchor: docsEncryption,
|
|
Resolve: envValue("KMS_LOCAL_MASTER_KEY"),
|
|
},
|
|
{
|
|
Key: "KMS_LOCAL_MASTER_KEY_FILE", Group: GroupEncryption, RuntimeChangeable: ChangeBootOnly,
|
|
Effect: "A file holding the root key, as an alternative to the inline value. Setting both is an error.",
|
|
DocsAnchor: docsEncryption,
|
|
Resolve: envValue("KMS_LOCAL_MASTER_KEY_FILE"),
|
|
},
|
|
{
|
|
Key: "KMS_AWS_KEY_ID", Group: GroupEncryption, RuntimeChangeable: ChangeBootOnly,
|
|
Effect: "The AWS KMS key or alias used when KMS_PROVIDER is aws.",
|
|
DocsAnchor: docsEncryption,
|
|
Resolve: envValue("KMS_AWS_KEY_ID"),
|
|
},
|
|
{
|
|
Key: "CREDENTIALS_ENCRYPTION_KEY", Group: GroupEncryption, RuntimeChangeable: ChangeBootOnly,
|
|
Effect: "Seals mailbox SMTP and IMAP passwords at rest. Empty stores them unsealed. Back it up: losing it makes connected mailboxes unrecoverable.",
|
|
DocsAnchor: docsEncryption,
|
|
Resolve: envValue("CREDENTIALS_ENCRYPTION_KEY"),
|
|
},
|
|
{
|
|
Key: "ENCRYPTED_KEYS_PROVIDER", Group: GroupEncryption, RuntimeChangeable: ChangeBootOnly,
|
|
Effect: "postgres for the backend and consumer, http for workers, which proxy through the backend internal API instead of opening SQL.",
|
|
DocsAnchor: docsEncryption,
|
|
Resolve: envOr("ENCRYPTED_KEYS_PROVIDER", "postgres"),
|
|
},
|
|
|
|
// Storage.
|
|
{
|
|
Key: "BLOB_PROVIDER", Group: GroupStorage, RuntimeChangeable: ChangeBootOnly,
|
|
Effect: "filesystem stores email bodies, attachments and avatars on disk; s3 stores them in any S3-compatible bucket.",
|
|
DocsAnchor: docsStorage,
|
|
Resolve: func(*Runtime) string { return config.BlobProvider() },
|
|
},
|
|
{
|
|
Key: "BLOB_FS_ROOT", Group: GroupStorage, RuntimeChangeable: ChangeBootOnly,
|
|
Effect: "The directory the filesystem provider writes to. The backend, the consumer and every worker on this host must share it.",
|
|
DocsAnchor: docsStorage,
|
|
Resolve: envValue("BLOB_FS_ROOT"),
|
|
},
|
|
{
|
|
Key: "BLOB_BUCKET", Group: GroupStorage, RuntimeChangeable: ChangeBootOnly,
|
|
Effect: "The bucket the s3 provider writes to.",
|
|
DocsAnchor: docsStorage,
|
|
Resolve: envValue("BLOB_BUCKET"),
|
|
},
|
|
{
|
|
Key: "BLOB_PUBLIC_BASE_URL", Group: GroupStorage, RuntimeChangeable: ChangePerRequest,
|
|
Effect: "The public URL base avatars and logos are served from.",
|
|
DocsAnchor: docsStorage,
|
|
Resolve: envValue("BLOB_PUBLIC_BASE_URL"),
|
|
},
|
|
{
|
|
Key: "AWS_ENDPOINT_URL_S3", Group: GroupStorage, RuntimeChangeable: ChangeBootOnly,
|
|
Effect: "A non-AWS S3 endpoint (MinIO, R2, B2).",
|
|
DocsAnchor: docsStorage,
|
|
Resolve: envValue("AWS_ENDPOINT_URL_S3"),
|
|
},
|
|
{
|
|
Key: "AWS_REGION", Group: GroupStorage, RuntimeChangeable: ChangeBootOnly,
|
|
Effect: "The AWS region for S3, KMS and SES. Only needed when one of those providers is selected.",
|
|
DocsAnchor: docsStorage,
|
|
Resolve: envValue("AWS_REGION"),
|
|
},
|
|
{
|
|
Key: "AWS_CONFIG_ENABLED", Group: GroupStorage, RuntimeChangeable: ChangeBootOnly,
|
|
Effect: "true reads unset secrets from AWS SSM and Secrets Manager instead of failing.",
|
|
DocsAnchor: docsStorage,
|
|
Resolve: boolOr("AWS_CONFIG_ENABLED", false),
|
|
},
|
|
|
|
// Event bus.
|
|
{
|
|
Key: "EVENTBUS_PROVIDER", Group: GroupEventBus, RuntimeChangeable: ChangeBootOnly,
|
|
Effect: "nats runs one small JetStream binary; kafka needs the kafka build tag and the KAFKA_* settings.",
|
|
DocsAnchor: docsEventBus,
|
|
Resolve: func(*Runtime) string { return config.EventBusProvider() },
|
|
},
|
|
{
|
|
Key: "CODEC_PROVIDER", Group: GroupEventBus, RuntimeChangeable: ChangeBootOnly,
|
|
Effect: "json is required wherever workers run: worker command and result envelopes carry untyped bodies Avro cannot serialize.",
|
|
DocsAnchor: docsEventBus,
|
|
Resolve: func(*Runtime) string { return config.CodecProvider() },
|
|
},
|
|
{
|
|
Key: "NATS_URL", Group: GroupEventBus, RuntimeChangeable: ChangeBootOnly,
|
|
Effect: "The NATS server campaign sends, mailbox syncs and tracking events flow through.",
|
|
DocsAnchor: docsEventBus,
|
|
Resolve: envOr("NATS_URL", "nats://localhost:4222"),
|
|
},
|
|
{
|
|
Key: "NATS_STREAM_NAME", Group: GroupEventBus, RuntimeChangeable: ChangeBootOnly,
|
|
Effect: "The JetStream stream every subject is bound to.",
|
|
DocsAnchor: docsEventBus,
|
|
Resolve: envOr("NATS_STREAM_NAME", "warmbly"),
|
|
},
|
|
{
|
|
Key: "NATS_SUBJECT_PREFIX", Group: GroupEventBus, RuntimeChangeable: ChangeBootOnly,
|
|
Effect: "The prefix every subject carries. It must match across the backend, the consumer and every worker.",
|
|
DocsAnchor: docsEventBus,
|
|
Resolve: envOr("NATS_SUBJECT_PREFIX", "warmbly"),
|
|
},
|
|
{
|
|
Key: "KAFKA_BOOTSTRAP_SERVERS", Group: GroupEventBus, RuntimeChangeable: ChangeBootOnly,
|
|
Effect: "The Kafka brokers, when EVENTBUS_PROVIDER is kafka.",
|
|
DocsAnchor: docsEventBus,
|
|
Resolve: envValue("KAFKA_BOOTSTRAP_SERVERS"),
|
|
},
|
|
{
|
|
Key: "SCHEMA_REGISTRY_URL", Group: GroupEventBus, RuntimeChangeable: ChangeBootOnly,
|
|
Effect: "The Avro schema registry. Only used with the kafka bus and the avro codec.",
|
|
DocsAnchor: docsEventBus,
|
|
Resolve: envValue("SCHEMA_REGISTRY_URL"),
|
|
},
|
|
{
|
|
Key: "EVENTBUS_HANDLER_TIMEOUT", Group: GroupEventBus, RuntimeChangeable: ChangeBootOnly,
|
|
Effect: "How long one consumer handler may run before the message is redelivered.",
|
|
DocsAnchor: docsEventBus,
|
|
Resolve: envValue("EVENTBUS_HANDLER_TIMEOUT"),
|
|
},
|
|
|
|
// Workers.
|
|
{
|
|
Key: "INTERNAL_API_TOKEN", Group: GroupWorkers, RuntimeChangeable: ChangeBootOnly,
|
|
Effect: "The shared token on /api/v1/internal. Workers fetch organization keys with it and the tracking service resolves click links with it.",
|
|
DocsAnchor: docsWorkers,
|
|
Resolve: envValue("INTERNAL_API_TOKEN"),
|
|
},
|
|
{
|
|
Key: "ENCRYPTED_KEYS_BACKEND_URL", Group: GroupWorkers, RuntimeChangeable: ChangeBootOnly,
|
|
Effect: "The backend base a worker reaches organization keys through. Empty makes the worker start normally and never register.",
|
|
DocsAnchor: docsWorkers,
|
|
Resolve: envValue("ENCRYPTED_KEYS_BACKEND_URL"),
|
|
},
|
|
{
|
|
Key: "ENCRYPTED_KEYS_WORKER_TOKEN", Group: GroupWorkers, RuntimeChangeable: ChangeBootOnly,
|
|
Effect: "The worker's copy of INTERNAL_API_TOKEN. It must match the backend's value exactly.",
|
|
DocsAnchor: docsWorkers,
|
|
Resolve: envValue("ENCRYPTED_KEYS_WORKER_TOKEN"),
|
|
},
|
|
{
|
|
Key: "WORKER_ID", Group: GroupWorkers, RuntimeChangeable: ChangeBootOnly,
|
|
Effect: "A worker's stable identity. Derived from the hostname when unset, which changes on every container recreate.",
|
|
DocsAnchor: docsWorkers,
|
|
Resolve: envValue("WORKER_ID"),
|
|
},
|
|
{
|
|
Key: "WORKER_TIER", Group: GroupWorkers, RuntimeChangeable: ChangeBootOnly,
|
|
Effect: "Which placement tier a worker accepts. Free-trial organizations place onto free workers, paid ones onto premium.",
|
|
DocsAnchor: docsWorkers,
|
|
Resolve: envValue("WORKER_TIER"),
|
|
},
|
|
{
|
|
Key: "MAIL_TLS_INSECURE", Group: GroupWorkers, RuntimeChangeable: ChangeBootOnly,
|
|
Effect: "true disables certificate verification on customer mailbox connections.",
|
|
DocsAnchor: docsWorkers,
|
|
Resolve: boolOr("MAIL_TLS_INSECURE", false),
|
|
},
|
|
{
|
|
Key: "BOX_GOOGLE_CLIENT_ID", Group: GroupWorkers, RuntimeChangeable: ChangeBootOnly,
|
|
Effect: "Your Google Cloud OAuth client for connecting Gmail mailboxes. Needed on the backend and on every worker.",
|
|
DocsAnchor: docsWorkers,
|
|
Resolve: envValue("BOX_GOOGLE_CLIENT_ID"),
|
|
},
|
|
{
|
|
Key: "BOX_GOOGLE_CLIENT_SECRET", Group: GroupWorkers, RuntimeChangeable: ChangeBootOnly,
|
|
Effect: "The secret for the Gmail mailbox OAuth client.",
|
|
DocsAnchor: docsWorkers,
|
|
Resolve: envValue("BOX_GOOGLE_CLIENT_SECRET"),
|
|
},
|
|
{
|
|
Key: "BOX_OUTLOOK_CLIENT_ID", Group: GroupWorkers, RuntimeChangeable: ChangeBootOnly,
|
|
Effect: "Your Microsoft 365 OAuth client for connecting Outlook mailboxes.",
|
|
DocsAnchor: docsWorkers,
|
|
Resolve: envValue("BOX_OUTLOOK_CLIENT_ID"),
|
|
},
|
|
{
|
|
Key: "BOX_OUTLOOK_CLIENT_SECRET", Group: GroupWorkers, RuntimeChangeable: ChangeBootOnly,
|
|
Effect: "The secret for the Outlook mailbox OAuth client.",
|
|
DocsAnchor: docsWorkers,
|
|
Resolve: envValue("BOX_OUTLOOK_CLIENT_SECRET"),
|
|
},
|
|
|
|
// Tracking.
|
|
{
|
|
Key: "FORMS_DOMAIN", Group: GroupAddresses, RuntimeChangeable: ChangePerRequest,
|
|
Effect: "The host hosted form pages and embeds are served on (forms.example.com, routed to the forms service). Share links and embed codes point here; unset leaves forms without a public URL.",
|
|
DocsAnchor: docsAddresses,
|
|
Resolve: envValue("FORMS_DOMAIN"),
|
|
},
|
|
{
|
|
Key: "FORM_IP_RATE_LIMIT", Group: GroupAddresses, RuntimeChangeable: ChangeBootOnly,
|
|
Effect: "Public form submissions allowed per source IP per 10 minutes, enforced by the forms service. Default 30.",
|
|
DocsAnchor: docsAddresses, WhenUnset: SourceDerived,
|
|
Resolve: envValue("FORM_IP_RATE_LIMIT"),
|
|
},
|
|
{
|
|
Key: "TRACKING_DOMAIN", Group: GroupTracking, RuntimeChangeable: ChangePerRequest,
|
|
Effect: "The host open pixels and click links point at. Recipients still receive mail when it is wrong, but nothing is recorded.",
|
|
DocsAnchor: docsAddresses,
|
|
Resolve: envValue("TRACKING_DOMAIN"),
|
|
},
|
|
{
|
|
Key: "TRACKING_SERVICE_URL", Group: GroupTracking, RuntimeChangeable: ChangeBootOnly,
|
|
Effect: "Where the backend probes the tracking service. Unset means the System status page cannot report on it.",
|
|
DocsAnchor: docsAddresses,
|
|
Resolve: envValue("TRACKING_SERVICE_URL"),
|
|
},
|
|
{
|
|
Key: "BACKEND_INTERNAL_URL", Group: GroupTracking, RuntimeChangeable: ChangeBootOnly,
|
|
Effect: "The backend base the tracking service resolves opaque click tickets through.",
|
|
DocsAnchor: docsWorkers,
|
|
Resolve: envValue("BACKEND_INTERNAL_URL"),
|
|
},
|
|
{
|
|
Key: "TRACKING_RATE_LIMIT_PER_MIN", Group: GroupTracking, RuntimeChangeable: ChangeBootOnly,
|
|
Effect: "Tracking events counted per source address per minute. Over-budget pixels are still served, just not counted.",
|
|
DocsAnchor: docsAddresses,
|
|
Resolve: envOr("TRACKING_RATE_LIMIT_PER_MIN", "300"),
|
|
},
|
|
|
|
// Observability.
|
|
{
|
|
Key: "SENTRY_DSN", Group: GroupObservability, RuntimeChangeable: ChangeBootOnly,
|
|
Effect: "Error reporting. Optional in every environment; unset simply logs instead.",
|
|
DocsAnchor: docsDeployment,
|
|
Resolve: envValue("SENTRY_DSN"),
|
|
},
|
|
|
|
// Updates.
|
|
{
|
|
Key: "UPDATE_CHECK_ENABLED", Group: GroupUpdates, RuntimeChangeable: ChangeBootOnly,
|
|
Effect: "Polls GitHub Releases for a newer Warmbly and shows it in the admin panel's top bar and on Setup and health.",
|
|
DocsAnchor: docsUpdates,
|
|
Resolve: boolOr("UPDATE_CHECK_ENABLED", true),
|
|
},
|
|
{
|
|
Key: "UPDATE_CHECK_INTERVAL", Group: GroupUpdates, RuntimeChangeable: ChangeBootOnly,
|
|
Effect: "How often the release check runs. Minimum 5m.",
|
|
DocsAnchor: docsUpdates,
|
|
Resolve: envOr("UPDATE_CHECK_INTERVAL", "30m"),
|
|
},
|
|
{
|
|
Key: "UPDATE_CHANNEL", Group: GroupUpdates, RuntimeChangeable: ChangeBootOnly,
|
|
Effect: "stable follows releases; dev also offers prereleases.",
|
|
DocsAnchor: docsUpdates,
|
|
Resolve: envOr("UPDATE_CHANNEL", "stable"),
|
|
},
|
|
{
|
|
Key: "RELEASES_GITHUB_REPO", Group: GroupUpdates, RuntimeChangeable: ChangeBootOnly,
|
|
Effect: "The owner/repo whose releases count as Warmbly versions. Point a fork's instance at the fork.",
|
|
DocsAnchor: docsUpdates,
|
|
Resolve: envOr("RELEASES_GITHUB_REPO", "warmbly/warmbly"),
|
|
},
|
|
{
|
|
Key: "UPDATER_URL", Group: GroupUpdates, RuntimeChangeable: ChangeBootOnly,
|
|
Effect: "The host-side updater that applies an update (pull, rebuild, restart). Unset leaves the panel report-only.",
|
|
DocsAnchor: docsUpdates,
|
|
Resolve: envValue("UPDATER_URL"),
|
|
},
|
|
{
|
|
Key: "UPDATER_TOKEN", Group: GroupUpdates, RuntimeChangeable: ChangeBootOnly,
|
|
Effect: "The bearer token the backend presents to the updater. Falls back to INTERNAL_API_TOKEN.",
|
|
DocsAnchor: docsUpdates, WhenUnset: SourceDerived,
|
|
Resolve: func(*Runtime) string {
|
|
if v := trimmed("UPDATER_TOKEN"); v != "" {
|
|
return v
|
|
}
|
|
return trimmed("INTERNAL_API_TOKEN")
|
|
},
|
|
},
|
|
}
|
|
|
|
// ssoRedirect mirrors the backend's own derivation, so the configuration page
|
|
// shows the URI that has to be registered at the provider rather than an empty
|
|
// cell whenever the operator left the override unset.
|
|
func ssoRedirect(key, provider string) func(*Runtime) string {
|
|
return func(*Runtime) string {
|
|
if v := trimmed(key); v != "" {
|
|
return v
|
|
}
|
|
if base := strings.TrimRight(trimmed("API_PUBLIC_URL"), "/"); base != "" {
|
|
return base + "/v1/auth/" + provider + "/callback"
|
|
}
|
|
return ""
|
|
}
|
|
}
|
|
|
|
func envValue(key string) func(*Runtime) string {
|
|
return func(*Runtime) string { return os.Getenv(key) }
|
|
}
|
|
|
|
func envOr(key, def string) func(*Runtime) string {
|
|
return func(*Runtime) string {
|
|
if v := trimmed(key); v != "" {
|
|
return v
|
|
}
|
|
return def
|
|
}
|
|
}
|
|
|
|
func boolOr(key string, def bool) func(*Runtime) string {
|
|
return func(*Runtime) string {
|
|
v := strings.ToLower(trimmed(key))
|
|
switch v {
|
|
case "1", "true", "yes", "on":
|
|
return "true"
|
|
case "0", "false", "no", "off":
|
|
return "false"
|
|
}
|
|
return strconv.FormatBool(def)
|
|
}
|
|
}
|
|
|
|
func trimmed(key string) string { return strings.TrimSpace(os.Getenv(key)) }
|
|
|
|
func yesNo(v bool) string { return strconv.FormatBool(v) }
|
|
|
|
// smtpConfig reads the same derived SMTP shape the transport is built from, so
|
|
// the displayed port and security mode are the ones actually dialled.
|
|
func smtpConfig() *config.SMTPConfig {
|
|
return (&config.Config{}).LoadSMTPConfig(context.Background())
|
|
}
|
|
|
|
// policy resolves the auth policy, preferring the one boot already built.
|
|
func policy(rt *Runtime) *config.AuthPolicy {
|
|
if rt != nil && rt.Policy != nil {
|
|
return rt.Policy
|
|
}
|
|
return config.LoadAuthPolicy(MailDelivers(rt))
|
|
}
|
|
|
|
// MailDelivers reports whether the configured transport puts mail on the wire.
|
|
func MailDelivers(rt *Runtime) bool {
|
|
if rt != nil && rt.MailTransportKind != "" {
|
|
return rt.MailDelivers
|
|
}
|
|
return config.MailTransport() != config.MailTransportLog
|
|
}
|