identityagent

package
v0.0.0-...-c8f541b Latest Latest
Warning

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

Go to latest
Published: Jul 3, 2026 License: Apache-2.0 Imports: 41 Imported by: 0

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

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

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

View Source
const (
	OutcomePass     = "pass"
	OutcomeFail     = "fail"
	OutcomeTimeout  = "timeout"
	OutcomeError    = "error"
	OutcomeCanceled = "canceled"
)

Outcome labels paired with stages. Bounded to a fixed set.

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

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

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

func LoadConfigFromEnv() (Config, error)

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.

func (Config) Validate

func (c Config) Validate() error

Validate reports configuration errors. All errors are joined; callers see every problem at once rather than fixing them one at a time.

type DecodedIdentity

type DecodedIdentity struct {
	UIDType tmproto.UIDType
	Bytes   []byte
}

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

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

type LiveRampSidecar interface {
	MappedID(ctx context.Context, env string) (string, error)
}

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

type LiveRampSidecarConfig struct {
	URL         string
	Timeout     time.Duration
	DialTimeout time.Duration
}

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

type MetricsConfig struct {
	Enabled   bool
	Namespace string
}

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).

func (*MetricsProvider) Shutdown

func (m *MetricsProvider) Shutdown(ctx context.Context) error

Shutdown flushes any pending metric data and tears down the meter provider. Safe to call when the provider is disabled (no meter provider was built).

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

func WithRelyingPartyID(rpID string) RunOption

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

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 TMPConfig

type TMPConfig struct {
	RegistryURL    string
	OwnEndpointURL string
	AllowUnsigned  bool
}

TMPConfig drives TMP signature verification on /identity.

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

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

type TmpxTokenDecoder interface {
	Decode(ctx context.Context, userToken string) ([]byte, error)
}

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).

Jump to

Keyboard shortcuts

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