feat(contracts): initial implementation (v1.0.0)

Introduces code.nochebuena.dev/einherjar/contracts — the zero-dependency
foundation of the Einherjar framework. Defines the interfaces and minimal
types consumed by every starter. Zero external dependencies. Zero Einherjar
dependencies. Nothing is above it in the dependency graph.

lifecycle:
- Component — OnInit, OnStart, OnStop three-phase lifecycle hooks

observability:
- Level (LevelCritical=0, LevelDegraded); zero value is the safe default
- Checkable — HealthCheck, Name, Priority
- Identifiable — ModulePath, ModuleVersion; implemented by all starters to
  surface module identity and version in the startup banner

logging:
- Logger — Debug, Info, Warn, Error, With, WithContext

errs:
- CodedError — ErrorCode() string; satisfied by core/xerrors.Err
- ContextualError — ErrorContext() map[string]any; satisfied by core/xerrors.Err

security:
- Identity value type — UID, TenantID, DisplayName, Email; NewIdentity, WithTenant
- Permission (int64), MaxPermission=62, PermissionMask — Has, Grant
- PermissionProvider — ResolveMask(ctx, uid, resource) (PermissionMask, error)
- SecurityBag value type — immutable request-scoped security context; carries
  Identity and arbitrary typed attributes (hardware IDs, grant codes, etc.);
  With copies the attribute map on every call to preserve receiver-invariant behaviour
- NewSecurityBag, Identity, WithIdentity, Get, With
- SetBagInContext / BagFromContext — full bag context storage
- SetInContext / FromContext — backed by SecurityBag; all four cross-function
  combinations (SetInContext+BagFromContext, SetBagInContext+FromContext) are valid

One file per type; CT-6 enforced by compliance test AST walk.
This commit is contained in:
2026-05-29 15:43:08 +00:00
commit 098a2098f8
31 changed files with 2230 additions and 0 deletions

165
CHANGELOG.md Normal file
View File

@@ -0,0 +1,165 @@
# Changelog
All notable changes to this module will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this module adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
---
## [1.1.0] — 2026-05-28
### Added
**`security`**
- `SecurityBag` struct — request-scoped security context that carries both the
authenticated `Identity` and arbitrary string-keyed attributes injected during
the enrichment phase (e.g. hardware IDs, grant codes). Value type — all mutation
methods return a new value without modifying the receiver. The attribute map is
copied on every `With` call to preserve immutability guarantees.
- `NewSecurityBag(id Identity) SecurityBag` — constructs a `SecurityBag` wrapping
the given Identity with an empty attribute map
- `SecurityBag.Identity() Identity` — returns the authenticated Identity stored in
the bag
- `SecurityBag.WithIdentity(id Identity) SecurityBag` — returns a copy of the bag
with the Identity replaced; used by bag enrichers that modify the Identity (e.g.
applying a tenant ID from a request header)
- `SecurityBag.Get(key string) (any, bool)` — returns the attribute stored under
key, or `(nil, false)` if absent
- `SecurityBag.With(key string, value any) SecurityBag` — returns a copy of the
bag with the given key set to value; the receiver is not modified
- `SetBagInContext(ctx context.Context, bag SecurityBag) context.Context` — stores
the full `SecurityBag` in ctx; use instead of `SetInContext` when extra attributes
need to travel alongside the Identity
- `BagFromContext(ctx context.Context) (SecurityBag, bool)` — retrieves the
`SecurityBag` stored by `SetBagInContext` or `SetInContext`; permission providers
use this to access both the Identity and any enrichment attributes
### Changed
**`security`**
- `SetInContext` — now stores a `SecurityBag` wrapping the Identity internally,
replacing direct `Identity` storage. The function signature is **unchanged**;
all existing callers continue to work without modification.
- `FromContext` — now extracts the Identity from the stored `SecurityBag`
internally. The function signature is **unchanged**; all existing callers
continue to work without modification. `SetBagInContext` + `FromContext` is
valid (returns the bag's Identity). `SetInContext` + `BagFromContext` is valid
(returns a bag wrapping the identity with an empty attribute map).
### Design Notes
- `SecurityBag` is additive — no existing interface or function signature is
modified. Starters compiled against v1.0.0 continue to work against v1.1.0
without any code changes.
- The framework defines no string key constants for bag attributes. All
framework-known fields are typed fields on `Identity`. Callers define their
own constants to avoid collisions and maintain compile-time safety at the
type-assertion call site.
---
## [1.0.0] — 2026-05-27
### Added
**`lifecycle`**
- `Component` interface — `OnInit() error` (open connections, allocate resources),
`OnStart() error` (start background goroutines and listeners), `OnStop() error`
(graceful shutdown and resource release); the framework calls these in strict
phase order and shuts down components in reverse registration order
**`observability`**
- `Level` type — `int`-based criticality classifier for health reporting; zero value
is `LevelCritical`, making it a safe default for any component that forgets to set it
- `LevelCritical` constant — marks a component essential to application function;
a failing critical component sets overall health to DOWN (HTTP 503)
- `LevelDegraded` constant — marks a component important but not essential; a failing
degraded component sets overall health to DEGRADED (HTTP 200), not DOWN
- `Checkable` interface — `HealthCheck(ctx context.Context) error` (connectivity or
liveness probe), `Name() string` (display name in health responses), `Priority() Level`
(criticality of this component); implemented by all infrastructure components;
consumed by the `web` starter's health handler without the data layer ever importing HTTP
**`logging`**
- `Logger` interface — `Debug`, `Info`, `Warn(msg string, args ...any)`, `Error(msg string, err error, args ...any)`,
`With(args ...any) Logger`, `WithContext(ctx context.Context) Logger`; all Einherjar
starters accept `Logger` at their constructors and pass it to sub-components — never
the concrete implementation; `Error` treats `err` as a first-class parameter to enable
automatic enrichment from `errs.CodedError` and `errs.ContextualError`
**`errs`**
- `CodedError` interface — `ErrorCode() string`; implemented by structured errors that
carry a machine-readable SCREAMING_SNAKE_CASE code meaningful to frontend consumers;
consumed by the `core` logger to append `error_code` automatically to every log record
where the error satisfies this interface; internal or infrastructure errors must not
carry a code — those get a generic fallback on the frontend
- `ContextualError` interface — `ErrorContext() map[string]any`; implemented by
structured errors that carry key-value context fields; consumed by the `core` logger
to append all context fields to the log record automatically; the returned map must
not be modified by the caller
**`security`**
- `Identity` struct — `UID`, `TenantID`, `DisplayName`, `Email string`; value type,
always copied never a pointer, preventing nil-check burden and accidental mutation
across concurrent middleware
- `NewIdentity(uid, displayName, email string) Identity` — constructs an Identity from
token authentication data; `TenantID` is intentionally left empty for enrichment
in a later middleware step
- `Identity.WithTenant(id string) Identity` — returns a copy of the Identity with
`TenantID` set; the receiver is not mutated, safe to call from concurrent middleware
- `SetInContext(ctx context.Context, id Identity) context.Context` — stores an Identity
in the context using a package-private key type, preventing collisions with keys from
other packages
- `FromContext(ctx context.Context) (Identity, bool)` — retrieves the Identity stored by
`SetInContext`; returns the zero-value Identity and false if no identity is present
- `Permission` type — named `int64` bit position (062) representing a single
capability; applications define their own domain constants using this type
- `MaxPermission` constant — highest valid bit position (62); bit 63 is reserved for
the sign bit of the underlying `int64`
- `PermissionMask` type — resolved `int64` bit-mask for a user on a specific resource;
zero value means no permissions granted
- `PermissionMask.Has(p Permission) bool` — reports whether the given permission bit
is set; returns false for out-of-range values (p < 0 or p > MaxPermission)
- `PermissionMask.Grant(p Permission) PermissionMask` — returns a new mask with the
bit for p set; the receiver is not modified, safe to use in builder chains
- `PermissionProvider` interface — `ResolveMask(ctx context.Context, uid, resource string) (PermissionMask, error)`;
implementations may call `FromContext` to retrieve the Identity and its TenantID
when multi-tenancy is required
### Design Notes
- `contracts` has zero external dependencies and zero Einherjar dependencies. Its
`go.mod` requires nothing beyond the Go standard library. If adding something to
`contracts` requires a new `require` entry, it does not belong in `contracts`.
- Changes flow outward from `contracts`, never inward. A starter never drives a
contracts change. Before any modification, the blast radius is calculated: which
interfaces are affected → which starters implement them → which starters consume
those starters. Release sequence: `contracts` first, then implementors, then
consumers.
- The `errs` sub-package formalises a contract that previously existed only as
private duck-typed interfaces inside micro-lib's `logz`. The two interfaces are
intentionally separate (`CodedError` and `ContextualError` rather than one combined
`RichError`) to honour ISP: an error may carry a code but no context fields, or
context fields but no code. Implementors are never forced to provide both.
- `Checkable` lives in `contracts/observability`, not in the `web` starter. Data
starters (`db-postgres`, `db-sqlite`, etc.) implement `Checkable` without importing
the HTTP layer. The health handler in `web` consumes `Checkable` — the dependency
arrow points into the data layer, never out.
- Every file in `contracts` exports exactly one type or interface. This is enforced
structurally and mechanically by the compliance test using `go/ast` parsing. A
reviewer reading `permission_mask.go` knows exactly what they are reading without
opening any other file.
[1.0.0]: https://code.nochebuena.dev/einherjar/contracts/releases/tag/v1.0.0