Documentation
¶
Overview ¶
Package tenant resolves per-tenant Notifier instances on demand.
On every Send, the routes layer asks the resolver for a Notifier configured for (tenant, channel). The resolver looks up the active Provider row, fetches credentials from KMS, and constructs the matching provider impl from the library. No cache — credentials may rotate, and provider rows may change at runtime; the cost of one extra KMS Get per send is the price of correctness.
Phase 1: plivo (SMS/WhatsApp/Voice) + mail (SMTP). Other providers land as small functions that follow the same shape — the library already exposes 33 implementations; this package wraps them.
Index ¶
- Constants
- Variables
- func Channel(s string) types.Channel
- func DefaultChainFor(ch ChainChannel) []string
- type Attempt
- type ChainChannel
- type ChainError
- type ChainProvider
- type ChainResolver
- type Credentials
- type PlivoConfig
- type PlivoResolver
- type ProviderChain
- type RawSender
- type Resolver
- type RunResult
Constants ¶
const ( ProviderPlivo = "plivo" ProviderTwilio = "twilio" // Twilio SMS ProviderTwilioEmail = "twilio_email" // Twilio native Email API ProviderSendGrid = "sendgrid" )
Canonical provider ids. These are the strings used in KMS-stored chain JSON, in telemetry logs, and on the Message row's `provider` field. They MUST stay stable — they are part of the wire contract with the platform UI's "current effective chain" indicator.
const DefaultBrand = "hanzo"
DefaultBrand is the brand slug whose KMS credentials are used when the caller's brand has not configured an override. Lives in KMS at `brand/hanzo/plivo/*`.
const PlivoBrandKMSPathPrefix = "brand"
PlivoBrandKMSPathPrefix is the KMS path under which per-brand Plivo credentials live. Combined with brand slug:
brand/<slug>/plivo/auth-id brand/<slug>/plivo/auth-token brand/<slug>/plivo/sender-id brand/<slug>/plivo/from-email
Variables ¶
var ErrNoProviders = errors.New("provider chain: no providers configured for channel/brand")
ErrNoProviders is returned when a ProviderChain has no providers to try (typically because the brand has not configured any provider for the channel and the default chain's providers all failed to construct due to missing KMS secrets). Distinct from ChainError so the caller can return 503 rather than 502.
Functions ¶
func Channel ¶
Channel narrows a string to a known types.Channel or returns "". Used by the routes layer when reading the body's channel field.
func DefaultChainFor ¶ added in v1.6.17
func DefaultChainFor(ch ChainChannel) []string
DefaultChainFor returns the default ordered provider id list for a channel. Per-tenant overrides supersede this; see ChainResolver.
The default chain is the hanzo fleet default (DefaultBrand). Email is Twilio-native — every email class leads with Twilio's Email API, which shares the SMS account credentials:
SMS → plivo, twilio email_txn → twilio_email email_otp → twilio_email email_marketing → twilio_email, sendgrid
Marketing keeps sendgrid as a fallback because that class needs RFC 8058 List-Unsubscribe headers; transactional/OTP need only the native Twilio Email API.
All providers are optional at the per-brand level: a missing secret for a fallback provider is treated as "this provider not configured for this brand" and the chain skips it without erroring (provided at least one provider in the chain succeeded or surfaced a terminal error).
Types ¶
type Attempt ¶ added in v1.6.17
type Attempt struct {
Provider string `json:"provider"`
Started time.Time `json:"started"`
Duration time.Duration `json:"duration"`
Outcome string `json:"outcome"` // "ok" | "retryable" | "terminal"
Error string `json:"error,omitempty"`
}
Attempt is one provider attempt's result, recorded in the per-send telemetry. The provider id stays stable across schema migrations; success/duration/error are observable knobs the dashboards key on.
type ChainChannel ¶ added in v1.6.17
type ChainChannel string
ChainChannel selects which provider chain to resolve. It is finer- grained than types.Channel because email has three distinct routing profiles (transactional, OTP, marketing) that can want different providers — the transactional/OTP classes lead with Twilio's native Email API; marketing keeps sendgrid as a fallback for its RFC 8058 header needs. SMS has a single chain today.
const ( ChainSMS ChainChannel = "sms" ChainEmailTxn ChainChannel = "email_txn" ChainEmailOTP ChainChannel = "email_otp" ChainEmailMarketing ChainChannel = "email_marketing" )
type ChainError ¶ added in v1.6.17
ChainError wraps the underlying cause with the full attempt trace. Activities.Deliver records the cause on the Message row and emits the trace via the logger.
func (*ChainError) Error ¶ added in v1.6.17
func (e *ChainError) Error() string
func (*ChainError) Unwrap ¶ added in v1.6.17
func (e *ChainError) Unwrap() error
type ChainProvider ¶ added in v1.6.17
type ChainProvider interface {
ID() string
Send(ctx context.Context, subject, body string, to string) error
}
ChainProvider is the narrow surface ProviderChain consumes per attempt. The notify library's Notifier covers Send; ID is metadata.
type ChainResolver ¶ added in v1.6.17
type ChainResolver struct {
// contains filtered or unexported fields
}
ChainResolver constructs ProviderChain instances for a (brand, channel) pair. It owns the KMS bridge — every constructed provider reads its secrets via the same fall-back path as PlivoResolver (brand/<slug>/<provider>/* → brand/hanzo/<provider>/* on miss).
func NewChainResolver ¶ added in v1.6.17
func NewChainResolver(kms *kmsbridge.Client) (*ChainResolver, error)
NewChainResolver returns a ChainResolver bound to a KMS client. A nil kms is rejected — the resolver fail-closes at boot rather than at first send.
func (*ChainResolver) InvalidateChainCache ¶ added in v1.6.17
func (r *ChainResolver) InvalidateChainCache(brand string, channel ChainChannel)
InvalidateChainCache drops the cached chain order for (brand, channel). The platform UI calls this after writing a new chain so the next send picks up the change without a pod restart.
func (*ChainResolver) Resolve ¶ added in v1.6.17
func (r *ChainResolver) Resolve(ctx context.Context, brand string, channel ChainChannel) (*ProviderChain, error)
Resolve constructs a ProviderChain for the (brand, channel) pair. The order comes from KMS at brand/<slug>/notify-chain/<channel> as a JSON array of provider ids; absent → DefaultChainFor(channel). For each provider id in order, the resolver builds the provider with the brand's KMS secrets (falling back to brand/hanzo/<provider>/* on any missing secret), skipping providers the brand has not configured (and the Hanzo default has not configured either).
The resolver returns ErrNoProviders if every provider in the resolved order failed to construct.
type Credentials ¶
Credentials is the flat key/value bag the library providers consume. The KMS values land here as plain strings; envelope decoding happens inside kmsclient.
type PlivoConfig ¶ added in v1.6.6
type PlivoConfig struct {
// Brand is the slug whose credentials produced this config. When a
// brand override exists, Brand == the requested brand. When the
// resolver fell back to the Hanzo default, Brand == "hanzo".
// Callers use this to log which Plivo account actually sent.
Brand string
// AuthID is the Plivo Auth ID (account-level credential).
AuthID string
// AuthToken is the Plivo Auth Token (account-level credential).
AuthToken string
// SenderID is the source number / shortcode / alphanumeric ID that
// will appear as the From on the SMS.
SenderID string
// FromEmail is the email address the brand sends from when notify
// channel=email is wired to the same per-brand config. Optional.
FromEmail string
// Override is true when this config came from the requested brand's
// own KMS entries (not the Hanzo default). Used by the platform
// UI's "current effective provider" indicator.
Override bool
}
PlivoConfig is the resolved per-brand Plivo configuration. The SenderID doubles as the Plivo "Source" — either an E.164 number or a Powerpack UUID.
type PlivoResolver ¶ added in v1.6.6
type PlivoResolver struct {
// contains filtered or unexported fields
}
PlivoResolver resolves per-brand Plivo configuration via KMS. One instance per process is enough — the underlying KMSClient already memoizes secrets for 1m TTL.
func NewPlivoResolver ¶ added in v1.6.6
func NewPlivoResolver(kms *kmsbridge.Client) (*PlivoResolver, error)
NewPlivoResolver returns a PlivoResolver bound to the given KMS client. A nil KMS client is rejected at boot — fail-closed.
func (*PlivoResolver) ResolvePlivoConfig ¶ added in v1.6.6
func (r *PlivoResolver) ResolvePlivoConfig(ctx context.Context, brand string) (*PlivoConfig, error)
ResolvePlivoConfig returns the Plivo credentials that should be used when sending for brand. The lookup order is:
- brand/<requested>/plivo/* — the brand's own override.
- brand/hanzo/plivo/* — the Hanzo default.
On step 1 the resolver does NOT short-circuit on any non-EOF error: a KMS access error against the requested brand falls through to the default ONLY when the error indicates "secret not found". Any other error (auth fail, transport, 5xx) is surfaced — silently degrading to the Hanzo creds for someone else's transient KMS outage would risk sending the wrong brand's SMS during the outage window.
On step 2 the resolver fail-closes: a missing or unreachable Hanzo default returns an error. notify callers surface 503.
The empty string for `brand` is rejected as a programming error — the platform plugin always injects X-Org-Id before this fires.
type ProviderChain ¶ added in v1.6.17
type ProviderChain struct {
Channel ChainChannel
Tenant string
Brand string
Providers []ChainProvider
}
ProviderChain is the ordered list of providers attempted for a single send. The first provider that succeeds wins; on retryable failure the next provider is attempted. On terminal failure (e.g. bad recipient, blocklist) we stop the chain.
func (*ProviderChain) Run ¶ added in v1.6.17
Run walks the chain. It returns the winning provider id and the telemetry trace. If every provider fails it returns ErrChainExhausted with the trace attached.
Retry policy (one and only one):
- per-attempt context: a 10s deadline derived from the parent
- per-attempt outcome: ok — the provider succeeded; return immediately terminal — the recipient is invalid; stop, no fallback retryable — anything else; advance to the next provider
- whole-chain context: a 30s deadline ceiling
Logger is optional; nil means no per-attempt log line is emitted. The structured Attempt list is always returned in RunResult.
type RawSender ¶ added in v1.6.17
type RawSender interface {
ChainProvider
SendRaw(ctx context.Context, subject, body, to string, headers map[string]string) error
}
RawSender is the optional capability a ChainProvider advertises when it can attach caller-supplied MIME headers to an email — the RFC 8058 List-Unsubscribe / List-Unsubscribe-Post pair the marketing path needs. The plain Send surface cannot inject headers, so the marketing sender type-asserts to RawSender and falls back to Send when a provider does not implement it. Twilio's native Email API carries a headers map, so twilio_email is the implementer.
type Resolver ¶
type Resolver struct {
// contains filtered or unexported fields
}
Resolver is the entry point routes call. It owns the base app (to look up provider rows) and the KMS bridge (to fetch credentials).
func New ¶
New returns a Resolver bound to the given app + KMS client. A nil KMS client is allowed; in that mode the resolver falls back to env var credentials, which is the local-dev / scratch-image path.
func (*Resolver) Resolve ¶
func (r *Resolver) Resolve(ctx context.Context, tenant, channel, service string, to []string) (notify.Notifier, string, error)
Resolve returns a notifier wired with credentials for (tenant, channel). service may be empty to take the tenant's default provider for the channel. The returned notifier targets `to` so caller can call Send directly — providers in the library are constructed with a fixed recipient list per the casdoor/nikoksr design.
type RunResult ¶ added in v1.6.17
type RunResult struct {
Channel ChainChannel `json:"channel"`
Tenant string `json:"tenant"`
Brand string `json:"brand"` // brand whose chain was used
Winner string `json:"winner,omitempty"`
Attempts []Attempt `json:"attempts"`
}
RunResult is the per-send outcome of executing a ProviderChain. On success, Winner is the provider that won (and Attempts ends with outcome="ok"). On exhausted-chain failure, Winner is empty and Attempts shows every provider that was tried.