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.
166 lines
8.8 KiB
Markdown
166 lines
8.8 KiB
Markdown
# 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 (0–62) 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
|