tenant

package
v1.7.1 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jun 29, 2026 License: MIT Imports: 21 Imported by: 0

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

View Source
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.

View Source
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/*`.

View Source
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

View Source
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

func Channel(s string) types.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

type ChainError struct {
	Result RunResult
	Cause  error
	Reason string // "exhausted" | "terminal"
}

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

type Credentials map[string]string

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:

  1. brand/<requested>/plivo/* — the brand's own override.
  2. 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

func (c *ProviderChain) Run(ctx context.Context, to, subject, body string) (RunResult, error)

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

func New(app core.App, kms *kmsbridge.Client) *Resolver

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.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL