Documentation
¶
Overview ¶
Package identityagent assembles a production-ready TMP identity-match service. It composes the audience-only IdentityEngine from the parent targeting package with a frequency-cap gate, surfaces them behind TMP-signature verification, exports Prometheus-compatible metrics via the OpenTelemetry meter provider, and orchestrates a coordinated graceful shutdown.
Index ¶
- Constants
- Variables
- func BuildKeyStore(runCtx context.Context, registryURL string, requireSignature bool, ...) (tmproto.KeyStore, error)
- func NewAdminServer(cfg AdminServerConfig) *http.Server
- func NewIdentityHandler(cfg IdentityHandlerConfig) http.Handler
- func NewServer(cfg ServerConfig) *http.Server
- func Run(ctx context.Context, cfg Config, logger *slog.Logger, version string, ...) error
- type AdminServerConfig
- type Config
- type DecodedIdentity
- type IdentityCanonicalizer
- type IdentityConfigSourceConfig
- type IdentityHandlerConfig
- type LiveRampSidecar
- type LiveRampSidecarConfig
- type MetricsConfig
- type MetricsProvider
- type PprofConfig
- type RecipientKey
- type Recorder
- type RunOption
- type ServerConfig
- type Service
- type ServiceConfig
- type TMPConfig
- type TMPXConfig
- type TMPXSealer
- func (s *TMPXSealer) Decode(ctx context.Context, ids []tmproto.IdentityToken) []DecodedIdentity
- func (s *TMPXSealer) MacroEntry(token string) (tmproto.TmpxMacro, bool)
- func (s *TMPXSealer) Seal(ctx context.Context, ids []tmproto.IdentityToken) (string, error)
- func (s *TMPXSealer) SealDecoded(ctx context.Context, decoded []DecodedIdentity) (string, error)
- type TmpxTokenDecoder
- type ValkeyBlock
Constants ¶
const ( StartModeRetry = "retry" StartModeFailFast = "fail-fast" StartModeBestEffort = "best-effort" )
Valid CONFIG_START_MODE values. Exported so callers and tests can refer to them by name rather than literal strings.
const ( StageResolve = "resolve" StageFCap = "fcap" StageAudience = "audience" StageRequest = "request" StageTMPX = "tmpx" StageVerifiedIdentity = "verified_identity" )
Stage labels for the request pipeline. Values are bounded for low cardinality.
const ( OutcomePass = "pass" OutcomeFail = "fail" OutcomeTimeout = "timeout" OutcomeError = "error" OutcomeCanceled = "canceled" )
Outcome labels paired with stages. Bounded to a fixed set.
const ( TmpxDropUnmapped = "unmapped" // UIDType has no TMPX type-ID mapping TmpxDropNoDecoder = "no_decoder" // UIDType mapped but no decoder configured (e.g. RampID with LiveRamp disabled) TmpxDropDecoderDrop = "decoder_drop" // decoder returned ErrDropIdentity (e.g. LiveRamp miss) TmpxDropDecoderError = "decoder_error" // decoder returned a non-drop error (transport, parse) TmpxDropSizeMismatch = "size_mismatch" // decoder returned wrong byte length for the type )
TMPX per-identity drop reasons. Each reason corresponds to one branch in selectEntries; cardinality is bounded by the constant set so the resulting `<ns>_tmpx_identity_drops_total{reason, uid_type}` series is finite (5 reasons × 9 UID types = 45 series).
const VIDropVerifierError = "verifier_error"
VIDropVerifierError is the drop label for a verifier that returned an error. The receiver loop reports verifier failure out-of-band (targeting's CredentialObserver.VerifierFailed) rather than as a labeled drop, so the stage applies this label itself — a non-zero verifier_error rate distinguishes "verifier down/misconfigured" from organic absence (no metric ticks). The remaining drop labels are emitted by the loop from the targeting.Drop* and targeting.PreCheck* constant sets, which bound the verified-identity drop cardinality.
Variables ¶
var ErrDropIdentity = tmpxdecoders.ErrDropFromSeal
ErrDropIdentity is the sentinel a TmpxTokenDecoder returns to signal "consumed the input, but produce no entry for this identity" — e.g. the LiveRamp sidecar returned no mapping for a RampID. Decode treats it as a silent drop, not an error.
Functions ¶
func BuildKeyStore ¶
func BuildKeyStore(runCtx context.Context, registryURL string, requireSignature bool, logger *slog.Logger, recorder Recorder) (tmproto.KeyStore, error)
BuildKeyStore constructs a tmproto.KeyStore from the configured registry URL. Returns (nil, nil) when no registry URL is set and signature verification is not required — callers then accept unsigned requests.
runCtx governs the long-lived background refresh goroutine; cancel it during shutdown to drain. The synchronous initial fetch is bounded to 10 seconds independently.
The background refresh goroutine runs under safeGo: a panic inside the upstream library is logged at ERROR and recorded on recorder.BackgroundPanic("keystore-refresh") instead of taking down the process. recorder may be nil for callers without observability.
func NewAdminServer ¶
func NewAdminServer(cfg AdminServerConfig) *http.Server
NewAdminServer builds the *http.Server hosting /metrics, /live, and (when enabled) /debug/pprof on a separate port. /health stays on the main server (see NewServer) — it's part of the protocol surface and must share the listener publisher routers reach externally. The mux is wrapped in recoverMiddleware so a panic in any observability handler doesn't take the process down with it.
func NewIdentityHandler ¶
func NewIdentityHandler(cfg IdentityHandlerConfig) http.Handler
NewIdentityHandler returns the http.Handler for POST /identity. Callers must supply positive RequestTimeout, RequestBodyLimit, and ResponseTTL; the agent's Config.Validate enforces this at startup.
func NewServer ¶
func NewServer(cfg ServerConfig) *http.Server
NewServer builds the *http.Server for /identity and /health. When AdminPort == 0 the operator endpoints (/live, /metrics, /debug/pprof) also mount on this server's mux. When AdminPort > 0 those are omitted here and the caller wires NewAdminServer onto a second listener; /health stays on the main mux unconditionally because it's part of the TMP protocol surface that publisher routers probe externally.
The handler chain on POST /identity reads outermost-to-innermost:
otelhttp.NewHandler # extract inbound traceparent
→ recoverMiddleware # trap panics, record + log + 500
→ requestIDMiddleware # echo X-Request-ID
→ accessLogMiddleware # one structured line per request
→ contentTypeMiddleware # 415 unless application/json
→ tmproto.VerifyIdentityMatchHandler # TMP signature
→ identityHandler # body decode + Service.Evaluate
func Run ¶
func Run(ctx context.Context, cfg Config, logger *slog.Logger, version string, opts ...RunOption) error
Run executes the agent lifecycle: build dependencies, start the HTTP server, then block until SIGINT/SIGTERM and run an orderly shutdown. Returns a non-nil error only when startup fails; once the server is running, errors during shutdown are logged and joined into the return value but the function still returns.
The supplied logger is used for structured event logs. version is stamped into /live responses; /health intentionally omits it per the TMP spec.
Types ¶
type AdminServerConfig ¶
type AdminServerConfig struct {
Port int
Registry *prometheus.Registry
Version string
PprofEnabled bool
ReadHeaderTimeout time.Duration
ReadTimeout time.Duration
WriteTimeout time.Duration
IdleTimeout time.Duration
MaxHeaderBytes int
Recorder Recorder
Logger *slog.Logger
}
AdminServerConfig packages the inputs for NewAdminServer. Only used when Config.AdminPort > 0.
type Config ¶
type Config struct {
HTTPPort int
RequestTimeout time.Duration
// HTTP server timeouts. RequestTimeout is the per-request handler-internal
// budget enforced via context.WithTimeout; the four below are outer
// listener-level bounds that absorb slow-client behavior and shut the
// door on slow-header / slow-body / slow-read attacks.
HTTPReadHeaderTimeout time.Duration
HTTPReadTimeout time.Duration
HTTPWriteTimeout time.Duration
HTTPIdleTimeout time.Duration
// ShutdownGrace is the readiness-flip propagation window; ShutdownTimeout
// is the hard ceiling on graceful shutdown after that window expires.
// Together they must fit inside the orchestrator's terminationGracePeriod.
ShutdownGrace time.Duration
ShutdownTimeout time.Duration
// RequestBodyLimitBytes is the maximum POST /identity body size in
// bytes. Anything larger is rejected at decode time.
RequestBodyLimitBytes int
// MaxHeaderBytes caps the total size of request headers. Goes onto
// http.Server.MaxHeaderBytes; tighter than Go's 1 MiB default to
// reject malformed bloat early.
MaxHeaderBytes int
// MaxOpenConnections caps the number of concurrently-accepted TCP
// connections to the listener. New SYNs queue in the kernel backlog
// once the cap is reached and are eventually rejected. Pair with the
// container's file-descriptor ulimit.
MaxOpenConnections int
// ResponseTTL is the cache TTL hint returned to callers in
// IdentityMatchResponse.ServeWindowSec.
ResponseTTL time.Duration
// StrictContentType rejects requests whose Content-Type is not
// application/json with 415 Unsupported Media Type. Enabled by default
// to close content-type confusion attacks; disable only for legacy
// callers that send a different type.
StrictContentType bool
// AccessLogEnabled emits one structured INFO log line per request
// (method, path, status, bytes, latency, request_id). Off by default
// to avoid log-volume cost at 10k QPS; useful in staging or for
// incident triage.
AccessLogEnabled bool
// AdminPort, when > 0, moves /metrics, /live, and /debug/pprof onto a
// second HTTP listener on that port. /identity and /health stay on
// HTTPPort — both are part of the TMP protocol surface that publisher
// routers probe externally, so they MUST share the listener serving
// /identity. When 0 (default) all endpoints share HTTPPort. Splitting
// lets ops apply different network policies to observability vs the
// public attack surface.
AdminPort int
// SupportedADCPMajorVersions enumerates the AdCP major versions this
// agent accepts on inbound `adcp_major_version`. Requests carrying an
// unsupported value are rejected with HTTP 400. Empty means "accept any
// value" — but Validate rejects empty, so deployments must declare
// support explicitly.
SupportedADCPMajorVersions []int
LogLevel string
TMP TMPConfig
TMPX TMPXConfig
LiveRamp LiveRampSidecarConfig
IdentityConfig IdentityConfigSourceConfig
AudienceValkey ValkeyBlock
FCapValkey ValkeyBlock
AudienceTimeout time.Duration
FCapTimeout time.Duration
Metrics MetricsConfig
Pprof PprofConfig
}
Config is the env-derived configuration for a single identity-agent process. Build with LoadConfigFromEnv and inspect with Validate before passing to Run.
func LoadConfigFromEnv ¶
LoadConfigFromEnv reads every recognized environment variable into a Config. Validation is the caller's responsibility — call Validate before using the result. Defaults are applied for unset optional fields so the returned Config is directly usable when only the required vars are set.
type DecodedIdentity ¶
DecodedIdentity is the per-request decode result for one inbound IdentityToken. Both the TMPX seal path and the audience/fcap lookup path consume this so identities are decoded exactly once per request (a hot concern for LiveRamp-backed RampIDs that make a sidecar call) and both paths see consistent drop decisions.
Bytes is the canonical decoded form the buyer master keys its downstream stores on. A nil or zero-length slice means the identity was dropped during decode (no mapped type, no decoder, sentinel, transport error, size mismatch) — selectEntries and audienceEligibleIdentities skip such entries.
type IdentityCanonicalizer ¶
type IdentityCanonicalizer struct {
// contains filtered or unexported fields
}
IdentityCanonicalizer decodes inbound IdentityToken.UserToken strings into their canonical binary form so audience and frequency-cap lookups key on the same shape ExposureLog.user_token publishes downstream — lowercase-hex of the decoded bytes — regardless of whether TMPX sealing is enabled on this deployment.
It exists as a sibling of TMPXSealer so the canonicalization step runs independently of TMPX recipient configuration. Deployments that don't emit TMPX tokens still benefit from the consistent key form; deployments that do still hand the same decoded slice to the sealer so the per-request decode pass (in particular the LiveRamp sidecar call for RampIDs) happens at most once.
Construct with NewIdentityCanonicalizer. A nil *IdentityCanonicalizer signals "no canonicalization configured" — the handler falls back to the legacy pass-through behavior in that case. The handler treats a non-nil canonicalizer with an empty decoder map the same way as a populated one: every identity is dropped at decode time, which matches the "missing-config = silent drop" pattern used elsewhere in the agent.
func NewIdentityCanonicalizer ¶
func NewIdentityCanonicalizer(lrClient LiveRampSidecar, logger *slog.Logger, recorder Recorder) *IdentityCanonicalizer
NewIdentityCanonicalizer builds an IdentityCanonicalizer backed by the default decoder registry. lrClient is the optional LiveRamp sidecar used to decode RampID and RampID-derived identities. When nil, those UID types have no decoder registered and identities of those types are silently dropped from the canonicalized shadow request (the same behavior the TMPX path applies). Pass nil — not a typed nil pointer to a concrete client — to disable (Go's interface-nil rules treat a typed nil as non-nil).
Returns a populated canonicalizer for any LiveRamp configuration: even without the sidecar the default registry yields the format-only decoders (MAID, HashedEmail, ID5) which cover the common case. Callers that explicitly want to opt out of canonicalization entirely (and preserve the legacy "publisher wire string flows through to audience/fcap" behavior) should pass a nil *IdentityCanonicalizer to the handler.
func (*IdentityCanonicalizer) Decode ¶
func (c *IdentityCanonicalizer) Decode(ctx context.Context, ids []tmproto.IdentityToken) []DecodedIdentity
Decode runs every supplied identity through its UID-type decoder and returns the results in input order. Per-identity drops (unmapped, no decoder, decoder drop sentinel, decoder error, size mismatch) are surfaced on the recorder's TmpxIdentityDrop counter — see the constants in metrics.go for the reason set.
Dropped identities have Bytes == nil in the returned slice; downstream stages (audienceEligibleIdentities, TMPXSealer.SealDecoded) skip them silently.
Decode shares its result between the audience/fcap shadow path and the TMPX seal path so LiveRamp-backed RampIDs make at most one sidecar call per request.
type IdentityConfigSourceConfig ¶
type IdentityConfigSourceConfig struct {
URL string
Token string
Timeout time.Duration
RefreshInterval time.Duration
StartMode string
StartRetryDeadline time.Duration
// ExtraHeaders are added to every outbound config-source request on top
// of the headers the source manages itself (Authorization, Content-Type,
// Accept). Loaded from CONFIG_SOURCE_EXTRA_HEADERS as a JSON object of
// name→value pairs. Collisions with the managed headers are rejected by
// the source constructor.
ExtraHeaders map[string]string
}
IdentityConfigSourceConfig drives the Scope3 identity-config refresh service.
StartMode controls behavior when the initial LoadAll fails:
- "retry" (default) — block startup retrying until StartRetryDeadline
- "fail-fast" — exit on the first failure
- "best-effort" — start with an empty snapshot; rely on the normal refresh tick to populate
StartRetryDeadline bounds the StartMode=retry retry loop. Ignored for the other modes. A pod whose config source is down for longer than this will exit; pick a value that matches your upstream's recovery SLO.
type IdentityHandlerConfig ¶
type IdentityHandlerConfig struct {
Service *Service
TMPXSealer *TMPXSealer
// Canonicalizer decodes inbound IdentityToken.UserToken strings into
// the canonical lowercase-hex key form audience/fcap services lookup
// on. Construct via NewIdentityCanonicalizer (typically alongside
// TMPXSealer) so the per-request decode pass runs once and feeds
// both the shadow request and the TMPX seal step. Nil disables
// canonicalization — the inbound request is forwarded to
// service.Evaluate unmodified, preserving the legacy "publisher wire
// string is the key" behavior.
Canonicalizer *IdentityCanonicalizer
RequestTimeout time.Duration
RequestBodyLimit int64
ResponseTTL time.Duration
// SupportedADCPMajorVersions enumerates the AdCP major versions this
// agent will accept on inbound `adcp_major_version`. When the field is
// present on a request and not in this set, the handler rejects with
// HTTP 400 and ErrorCodeInvalidRequest. When the field is omitted, the
// seller assumes its highest supported version (per the TMP schema).
SupportedADCPMajorVersions []int
Recorder Recorder
Logger *slog.Logger
}
IdentityHandlerConfig packages the inputs for NewIdentityHandler.
type LiveRampSidecar ¶
LiveRampSidecar is the interface NewTMPXSealer accepts for decoding RampID identities. The production implementation lives in targeting/internal/liveramp; the interface is declared here so tests can supply a fake without spinning up an httptest server and without importing the internal package.
type LiveRampSidecarConfig ¶
LiveRampSidecarConfig optionally enables calls to the Scope3 LiveRamp mapping sidecar for decoding RampID and RampID-derived identities into the binary form TMPX expects.
When URL is empty the sidecar is disabled: any RampID arriving on /identity is silently dropped from the TMPX wire (other UID types are unaffected). Timeout and DialTimeout default to 2s / 1s respectively when zero. The sidecar is assumed to be reachable in the same network trust boundary as the agent, so no auth is sent.
func (LiveRampSidecarConfig) Enabled ¶
func (c LiveRampSidecarConfig) Enabled() bool
Enabled reports whether a LiveRamp sidecar URL was configured.
type MetricsConfig ¶
MetricsConfig drives the Prometheus exporter and /metrics endpoint.
type MetricsProvider ¶
type MetricsProvider struct {
Registry *prometheus.Registry
Recorder Recorder
// contains filtered or unexported fields
}
MetricsProvider wires together a Prometheus registry, an OTEL meter provider that writes to it, and a Recorder. Build is the only constructor.
A disabled provider returns a noop Recorder and a nil Registry. The /metrics endpoint isn't mounted when Registry is nil.
func Build ¶
func Build(cfg MetricsConfig) (*MetricsProvider, error)
Build constructs a MetricsProvider per the supplied config.
- cfg.Enabled=false → returns a provider with a noop recorder and nil Registry, never fails.
- cfg.Enabled=true → constructs prometheus.NewRegistry, plugs it into the OTEL Prometheus exporter, builds the OtelRecorder. Any failure surfaces as a startup error per the contract: invalid metrics config fails startup.
func (*MetricsProvider) RegisterConfigEntriesObserver ¶
func (m *MetricsProvider) RegisterConfigEntriesObserver(observerFn func() int64) error
RegisterConfigEntriesObserver registers an OTEL ObservableGauge named "<namespace>_config_entries" whose value is read from observerFn each time the Prometheus exporter scrapes. Intended source is identityconfig.Service.Len — the number of (sellerAgentURL, packageID) entries currently held in the in-memory snapshot. A flat-line or dropping value over time, paired with refresh errors, is a strong signal the snapshot has gone stale or empty. No-op when metrics are disabled.
observerFn is invoked from arbitrary goroutines on the exporter's schedule; it must be safe to call concurrently and should be cheap.
func (*MetricsProvider) RegisterOpenConnectionsObserver ¶
func (m *MetricsProvider) RegisterOpenConnectionsObserver(observerFn func() int64) error
RegisterOpenConnectionsObserver registers an OTEL ObservableGauge named "<namespace>_open_connections" whose value is read from observerFn each time the Prometheus exporter scrapes. No-op when metrics are disabled — the gauge simply isn't published.
observerFn is invoked from arbitrary goroutines on the exporter's schedule; it must be safe to call concurrently and should be cheap (an atomic load is the intended shape).
type PprofConfig ¶
type PprofConfig struct {
Enabled bool
}
PprofConfig toggles /debug/pprof on the observability mux.
type RecipientKey ¶
type RecipientKey = targeting.RecipientKey
RecipientKey is the HPKE recipient identity for one audience_kid — the X25519 private key plus the relying party this deployment acts as for that audience. See targeting.RecipientKey for the receiver-binding semantics.
type Recorder ¶
type Recorder interface {
RequestStarted(ctx context.Context)
RequestCompleted(ctx context.Context, status string, d time.Duration)
StageOutcome(ctx context.Context, stage, outcome string)
StageDuration(ctx context.Context, stage string, d time.Duration)
StoreError(ctx context.Context, store string)
ConfigRefresh(ctx context.Context, outcome string)
ShutdownPanic(ctx context.Context)
HandlerPanic(ctx context.Context)
BackgroundPanic(ctx context.Context, where string)
// TmpxIdentityDrop records one inbound identity dropped from a TMPX
// wire token. Distinguishes the drop reason and the UIDType so
// operators can attribute "we're shipping zero RampIDs" to either a
// LiveRamp outage (TmpxDropDecoderError on rampid) or no RampIDs in
// inbound traffic (no metric ticks).
TmpxIdentityDrop(ctx context.Context, reason, uidType string)
// VerifiedIdentityDrop records one attestation/sealed-credential dropped
// from the verified-identity stage, by reason. A verifier_error rate
// makes a down/unwired verifier observable instead of indistinguishable
// from organic absence (the spike's silent-degradation blast radius).
VerifiedIdentityDrop(ctx context.Context, reason string)
}
Recorder is the metric API the identity-agent hot path uses. The OTEL implementation is the production recorder; tests use noopRecorder.
type RunOption ¶
type RunOption func(*runOptions)
RunOption injects optional dependencies into Run that cannot come from environment configuration because they are constructed objects, not strings — chiefly the verified-identity verifier and its HPKE recipient keys. These are issuer-specific (e.g. World ID, which carries an HTTP client to World's backend), so they are kept out of this core package's dependency graph: the deployable binary constructs them and passes them in, and this package only ever sees the targeting.AttestationVerifier / targeting.AgeResolver interfaces and the RecipientKey data.
With no options, Run behaves exactly as before: no verifier ⇒ every attestation is treated as absent (fail-closed), and no age gating.
func WithAgeResolver ¶
func WithAgeResolver(r targeting.AgeResolver) RunOption
WithAgeResolver injects the age-policy resolver (e.g. AdCP Policy Registry backed). Without it, no age gating is applied.
func WithAttestationVerifier ¶
func WithAttestationVerifier(v targeting.AttestationVerifier) RunOption
WithAttestationVerifier injects the verified-identity verifier. Without it, attestations and sealed credentials are treated as absent and eligibility is unchanged — trust-without-verify is unreachable by omission.
func WithRecipientKeys ¶
func WithRecipientKeys(keys map[string]RecipientKey) RunOption
WithRecipientKeys injects the HPKE recipient keys (audience_kid → key + the relying party this deployment acts as) used to open sealed_credentials.
func WithRelyingPartyID ¶
WithRelyingPartyID sets the relying party this deployment acts as for in-band attestation verification (req.Identities[].Attestation). Without it, only the sealed_credentials carrier is active; in-band attestations are treated as absent.
type ServerConfig ¶
type ServerConfig struct {
Port int
IdentityHandler http.Handler
KeyStore tmproto.KeyStore
OwnEndpointURL string
RequireSig bool
Registry *prometheus.Registry
IsRunning func() bool
Version string
PprofEnabled bool
ReadHeaderTimeout time.Duration
ReadTimeout time.Duration
WriteTimeout time.Duration
IdleTimeout time.Duration
MaxHeaderBytes int
// AdminPort decides whether observability endpoints share the main
// listener (=0) or split onto a second listener built by NewAdminServer
// (>0). Callers that get an AdminPort > 0 must also instantiate the
// admin server themselves; NewServer only signals whether to mount the
// observability endpoints on the main mux.
AdminPort int
// Middleware knobs for the main mux. Match Config flags 1:1.
StrictContentType bool
AccessLogEnabled bool
Recorder Recorder
Logger *slog.Logger
}
ServerConfig packages the inputs for NewServer. All four HTTP timeouts are required (Config.Validate rejects zero values) and act as outer listener bounds — the per-request 40ms budget is enforced inside the identity handler via context.WithTimeout.
/identity and /health always stay on Port — both are part of the TMP protocol surface that publisher routers probe externally, so they MUST share the listener exposed at the registered base URL. When AdminPort > 0 the operator-facing endpoints (/live, /metrics, /debug/pprof) move onto a second listener built by NewAdminServer.
type Service ¶
type Service struct {
// contains filtered or unexported fields
}
Service composes the audience-only IdentityEngine with a frequency-cap gate. fcap and audience lookups run in parallel under a single request context with per-component sub-timeouts; either side short-circuits the other on a determining outcome (fcap caps everything → audience cancels; audience rules out every package → fcap cancels). Both fail closed on timeout or store error.
func NewService ¶
func NewService(cfg ServiceConfig) (*Service, error)
NewService validates the supplied dependencies and returns a Service. FCap and ConfigService are required; Audience may be nil when the deployment has no audience Valkey wired up.
func (*Service) Evaluate ¶
func (s *Service) Evaluate(ctx context.Context, req *tmproto.IdentityMatchRequest) *targeting.IdentityResult
Evaluate runs the full identity-match pipeline for one request. The returned IdentityResult honors both audience gating and fcap gating; a package is eligible only when both stages pass for it.
The caller's IdentityMatchRequest is treated as read-only — Evaluate computes the effective package set internally and never mutates req.
Parent-context expiry (the handler's 40ms budget) terminates both goroutines and forces a fail-closed result.
type ServiceConfig ¶
type ServiceConfig struct {
Engine *targeting.IdentityEngine
FCap *fcap.Service
Audience *audience.Service
ConfigService *identityconfig.Service
FCapTimeout time.Duration
AudienceTimeout time.Duration
Recorder Recorder
// Verifier validates attestations; nil disables the verified-identity
// stage (fail-closed — attestations are treated as absent).
Verifier targeting.AttestationVerifier
// RecipientKeys maps audience_kid → the HPKE recipient key + the relying
// party this deployment acts as for that audience. nil/empty disables the
// stage. The values contain secret key material — never log this map.
RecipientKeys map[string]RecipientKey
// AgeResolver resolves a package's required age threshold by geo; nil
// means no age gating.
AgeResolver targeting.AgeResolver
// RelyingPartyID is the RP this deployment acts as for in-band attestation
// verification (req.Identities[].Attestation). Empty disables that carrier.
RelyingPartyID string
}
ServiceConfig packages the dependencies for NewService.
type TMPXConfig ¶
type TMPXConfig struct {
EncryptJWKSURL string
EncryptJWKSTTL time.Duration
Country string
Priority string
// MacroNames is the ordered list of ad-server macro slot names this
// provider's TMPX response fills, matching the provider's registered
// `tmpx_macros` (provider-registration.json). The sealer pairs the
// sealed token with these names to populate IdentityMatchResponse's
// `tmpx_macros[]`. When empty the response carries only the legacy
// singular `tmpx` field (deprecated, removed in 4.0). Capped at 2 by
// the spec; multi-chunk encoding is not yet implemented — when
// configured with more than one name only the first slot is filled,
// matching the single-slot deployment shape.
MacroNames []string
}
TMPXConfig drives TMPX response sealing. Disabled when EncryptJWKSURL is empty.
type TMPXSealer ¶
type TMPXSealer struct {
// contains filtered or unexported fields
}
TMPXSealer holds the resolved TMPX recipient state used to seal tokens alongside identity-match responses. Construct with NewTMPXSealer; call Seal at request time to produce a per-request HPKE token.
String→binary conversion is delegated to a per-UID-type registry of TmpxTokenDecoders (default constructed by NewTMPXSealer from targeting/internal/tmpxdecoders). UID types without a registered decoder are silently dropped from both the TMPX wire and the audience/fcap shadow request.
func NewTMPXSealer ¶
func NewTMPXSealer(runCtx context.Context, cfg TMPXConfig, lrClient LiveRampSidecar, logger *slog.Logger, recorder Recorder) (*TMPXSealer, error)
NewTMPXSealer builds a TMPXSealer from the supplied config, starts the underlying JWKS refresh goroutine bound to runCtx, and validates the initial key fetch. Returns (nil, nil) when TMPX is not configured (every relevant field empty). Returns an error when the configuration is partially set, the initial JWKS fetch fails, or the JWKS publishes no recipient key.
The decoder registry is intentionally allowed to be incomplete: any UID type in uidToTmpxTypeID without a registered decoder is treated as a silent drop at Seal time. Decode counts these drops via the `no_decoder` reason on the TmpxIdentityDrop counter so operators can see them.
lrClient is the optional LiveRamp sidecar used to decode RampID / RampIDDerived identities. When nil, those UID types have no decoder registered and selectEntries drops them silently from the TMPX wire. Pass nil — not a typed nil pointer to a concrete client — to disable (Go's interface-nil rules treat a typed nil as non-nil).
runCtx governs the long-lived refresh goroutine; cancel it during shutdown to drain.
The background refresh runs under safeGo: a panic inside the upstream library is logged at ERROR and recorded on recorder.BackgroundPanic("tmpx-jwks-refresh") rather than taking down the process. recorder may be nil for callers without observability.
func (*TMPXSealer) Decode ¶
func (s *TMPXSealer) Decode(ctx context.Context, ids []tmproto.IdentityToken) []DecodedIdentity
Decode runs every supplied identity through its UID-type decoder and returns the results in input order. Per-identity drops (unmapped, no decoder, decoder drop sentinel, decoder error, size mismatch) are surfaced on the recorder's TmpxIdentityDrop counter with these reasons:
- unmapped — UIDType has no TMPX type-ID mapping
- no_decoder — UIDType mapped but no decoder configured (LiveRamp off)
- decoder_drop — decoder returned ErrDropIdentity (LiveRamp miss)
- decoder_error — decoder returned a transport/parse error
- size_mismatch — decoder produced wrong byte length for the type
Dropped identities have Bytes == nil in the returned slice; downstream stages (SealDecoded, audience/fcap lookups) skip them silently.
In production the per-request decode pass is owned by the canonicalizer (see IdentityCanonicalizer.Decode); the handler hands SealDecoded the already-decoded slice so LiveRamp-backed RampIDs make at most one sidecar call per request. This method remains so callers (including the Seal convenience wrapper and the reference / test code) that already hold a *TMPXSealer can still decode against the sealer's own registry without taking a dependency on IdentityCanonicalizer.
func (*TMPXSealer) MacroEntry ¶
func (s *TMPXSealer) MacroEntry(token string) (tmproto.TmpxMacro, bool)
MacroEntry returns the {name, value} pair that fills the provider's first registered macro slot for the given sealed token, or (zero, false) when no macro names are configured. Single-slot is the only emission shape today; multi-chunk splitting across more than one registered name is deferred until production deployments actually exceed the 255-char ad-server slot budget.
func (*TMPXSealer) Seal ¶
func (s *TMPXSealer) Seal(ctx context.Context, ids []tmproto.IdentityToken) (string, error)
Seal is a convenience that runs Decode and then SealDecoded. Callers that share decoded identities across the TMPX and audience/fcap paths should call Decode and SealDecoded directly so the per-request decode pass happens exactly once.
func (*TMPXSealer) SealDecoded ¶
func (s *TMPXSealer) SealDecoded(ctx context.Context, decoded []DecodedIdentity) (string, error)
SealDecoded produces an HPKE TMPX token from a pre-decoded identity slice. Identities with no Bytes (those Decode dropped) are skipped.
When TMPX_PRIORITY is set, entries are packed in priority order and trailing ones that don't fit the 255-byte wire budget are dropped. When TMPX_PRIORITY is empty, the spec's "no arbitrary truncation" rule applies: if the set overflows the budget, SealDecoded returns an error and the handler omits TMPX from the response.
Returns "" without error when no identity is encodable.
type TmpxTokenDecoder ¶
TmpxTokenDecoder converts a user_token string supplied on an inbound IdentityToken into the binary form TMPX packs into its encrypted plaintext. Concrete implementations live in targeting/internal/tmpxdecoders; the interface is declared here so tests can supply fakes without taking a dependency on the internal package.
Decode receives the request's context.Context so HTTP-backed decoders (LiveRamp sidecar, identity graph lookups) can honor the caller's deadline. Format-only decoders ignore it.
Implementations must return exactly tmproto.TmpxTokenSize(typeID) bytes for the UID type they are registered against; selectEntries validates the length before adding the entry to the wire payload. A decoder may return ErrDropIdentity to signal that the user_token was consumed but the resulting identity should be omitted from the wire (LiveRamp miss is the canonical example).
type ValkeyBlock ¶
type ValkeyBlock struct {
Enabled bool
Mode string
Shards map[string]string
Username string
Password string
DB int
TLS bool
DialTimeout time.Duration
ReadTimeout time.Duration
PoolSize int
}
ValkeyBlock is the per-backend Valkey configuration. Use ToRedisStoreConfig to project onto the redisstore.Config the Build helper consumes.
The identity-agent only issues read commands (HEXISTS) against Valkey; fcap writes come from the frequency-writer and audience writes from a separate audience-writer. WriteTimeout is intentionally not exposed.
func (ValkeyBlock) ToRedisStoreConfig ¶
func (b ValkeyBlock) ToRedisStoreConfig() redisstore.Config
ToRedisStoreConfig projects onto the redisstore.Config the Build helper consumes. Only meaningful when Enabled is true; the caller is responsible for gating the call (the identityagent.Config.Validate flow does this).