Files
contracts/CHANGELOG.md
Rene Nochebuena 098a2098f8 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.
2026-05-29 15:43:08 +00:00

8.8 KiB
Raw Blame History

Changelog

All notable changes to this module will be documented in this file.

The format is based on Keep a Changelog, and this module adheres to Semantic Versioning.


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