# Architecture Decision Records — `einherjar/auth` ## ADR-001: No root factory **Status:** Accepted **Context:** Previous starters (`web`) expose a root `New(logger, ...Config)` factory that pre-wires a complete, opinionated stack for the common case. The same pattern was evaluated for `auth`. **Decision:** `auth` exposes no root factory. The module is composition-only. **Rationale:** `web.New` works because the right middleware stack is always the same (Recover → RequestID → RequestLogger → CORS). Auth has no equivalent universal default: - The auth provider is application-specific (JWT vs Firebase vs custom) - The `IdentityEnricher` is always application code - `AuthzMiddleware` is per-route, not global - The `PermissionProvider` chain depends on the application's permission model A root factory would either be a thin wrapper (adds no value) or force premature architectural choices on the caller. --- ## ADR-002: Logger added to both middleware functions **Status:** Accepted **Context:** micro-lib's `EnrichmentMiddleware` and `AuthzMiddleware` were logger-free — errors were written as HTTP responses with no log emission. **Decision:** Both middleware functions accept `logging.Logger` and route all errors through `httputil.Error(logger, w, r, err)`. **Rationale:** - An `IdentityEnricher` returning a 500 is a server-side failure. It must be logged — silent 500s are invisible in production. - `httputil.Error` is the single error-processing point for the framework. Using it in auth keeps error response shape, log levels, and field enrichment identical to application handlers. - 401 (missing token) and 403 (permission denied) log at Warn — client errors, not operator alerts. - Log level routing is already handled by `httputil.Error`; no per-middleware logic needed. --- ## ADR-003: auth depends on web for httputil.Error **Status:** Accepted **Context:** auth error responses could be written independently (custom JSON write, like micro-lib's `WriteJSONError`) to avoid a dependency on `web`. **Decision:** `auth` depends on `code.nochebuena.dev/einherjar/web` solely for `httputil.Error`. **Rationale:** - Consistent error shape across the entire API surface — auth errors and handler errors are indistinguishable to clients. - `httputil.Error` handles log level routing, error code extraction, and platform code inclusion. Duplicating this in `auth` would create two implementations that drift over time. - The dependency is one-directional: `web` does not depend on `auth`. The dependency graph remains acyclic. **Trade-off accepted:** `auth` cannot be used without `web`. This is acceptable — `auth` is an HTTP middleware layer and `web` is the HTTP layer. Separating them was never a goal. --- ## ADR-004: BagEnricher chain over arbitrary context injection **Status:** Accepted **Context:** The original design used `WithTenantHeader` as the sole enrichment option, storing additional attributes via arbitrary `context.WithValue` calls. This made enrichment opaque — permission providers had no structured way to access request attributes like hardware IDs or grant codes without knowing the exact context key used elsewhere. **Decision:** Enrichment is modeled as a chain of `BagEnricher` functions that transform a `security.SecurityBag`. `EnrichmentMiddleware` creates the bag from the base Identity, runs all registered `BagEnricher` functions in order, and stores the final bag via `security.SetBagInContext`. Permission providers retrieve the bag once with `security.BagFromContext(ctx)` and have access to all enriched attributes via `bag.Get(key)`. **Rationale:** - Single structured object per request instead of scattered `context.WithValue` entries; easier to reason about what's in the security context at any point in the chain. - Permission providers access any bag attribute in one call rather than knowing which context key to look up. - `WithCacheKey` on `CachedPermissionProvider` receives the full bag — no new parameters needed to include extra dimensions (hardware IDs, grant codes) in the cache key. - `WithTenantHeader` is now a thin wrapper over `WithBagEnricher`, so the same extension point covers all enrichment use cases. - Framework-owned attributes remain typed fields on `Identity`. Only application-defined attributes go into the bag's string-keyed map. Callers define their own key constants to maintain compile-time safety at the type-assertion call site. **Trade-off accepted:** Bag attributes are `any` — type assertions are required at read sites. This is inherent to a string-keyed heterogeneous map. The contract is: the package that writes an attribute owns the constant and documents the type; callers that read it perform the assertion.