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.
8.8 KiB
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
SecurityBagstruct — request-scoped security context that carries both the authenticatedIdentityand 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 everyWithcall to preserve immutability guarantees.NewSecurityBag(id Identity) SecurityBag— constructs aSecurityBagwrapping the given Identity with an empty attribute mapSecurityBag.Identity() Identity— returns the authenticated Identity stored in the bagSecurityBag.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 absentSecurityBag.With(key string, value any) SecurityBag— returns a copy of the bag with the given key set to value; the receiver is not modifiedSetBagInContext(ctx context.Context, bag SecurityBag) context.Context— stores the fullSecurityBagin ctx; use instead ofSetInContextwhen extra attributes need to travel alongside the IdentityBagFromContext(ctx context.Context) (SecurityBag, bool)— retrieves theSecurityBagstored bySetBagInContextorSetInContext; permission providers use this to access both the Identity and any enrichment attributes
Changed
security
SetInContext— now stores aSecurityBagwrapping the Identity internally, replacing directIdentitystorage. The function signature is unchanged; all existing callers continue to work without modification.FromContext— now extracts the Identity from the storedSecurityBaginternally. The function signature is unchanged; all existing callers continue to work without modification.SetBagInContext+FromContextis valid (returns the bag's Identity).SetInContext+BagFromContextis valid (returns a bag wrapping the identity with an empty attribute map).
Design Notes
SecurityBagis 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
Componentinterface —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
Leveltype —int-based criticality classifier for health reporting; zero value isLevelCritical, making it a safe default for any component that forgets to set itLevelCriticalconstant — marks a component essential to application function; a failing critical component sets overall health to DOWN (HTTP 503)LevelDegradedconstant — marks a component important but not essential; a failing degraded component sets overall health to DEGRADED (HTTP 200), not DOWNCheckableinterface —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 thewebstarter's health handler without the data layer ever importing HTTP
logging
Loggerinterface —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 acceptLoggerat their constructors and pass it to sub-components — never the concrete implementation;Errortreatserras a first-class parameter to enable automatic enrichment fromerrs.CodedErroranderrs.ContextualError
errs
CodedErrorinterface —ErrorCode() string; implemented by structured errors that carry a machine-readable SCREAMING_SNAKE_CASE code meaningful to frontend consumers; consumed by thecorelogger to appenderror_codeautomatically 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 frontendContextualErrorinterface —ErrorContext() map[string]any; implemented by structured errors that carry key-value context fields; consumed by thecorelogger to append all context fields to the log record automatically; the returned map must not be modified by the caller
security
Identitystruct —UID,TenantID,DisplayName,Email string; value type, always copied never a pointer, preventing nil-check burden and accidental mutation across concurrent middlewareNewIdentity(uid, displayName, email string) Identity— constructs an Identity from token authentication data;TenantIDis intentionally left empty for enrichment in a later middleware stepIdentity.WithTenant(id string) Identity— returns a copy of the Identity withTenantIDset; the receiver is not mutated, safe to call from concurrent middlewareSetInContext(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 packagesFromContext(ctx context.Context) (Identity, bool)— retrieves the Identity stored bySetInContext; returns the zero-value Identity and false if no identity is presentPermissiontype — namedint64bit position (0–62) representing a single capability; applications define their own domain constants using this typeMaxPermissionconstant — highest valid bit position (62); bit 63 is reserved for the sign bit of the underlyingint64PermissionMasktype — resolvedint64bit-mask for a user on a specific resource; zero value means no permissions grantedPermissionMask.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 chainsPermissionProviderinterface —ResolveMask(ctx context.Context, uid, resource string) (PermissionMask, error); implementations may callFromContextto retrieve the Identity and its TenantID when multi-tenancy is required
Design Notes
-
contractshas zero external dependencies and zero Einherjar dependencies. Itsgo.modrequires nothing beyond the Go standard library. If adding something tocontractsrequires a newrequireentry, it does not belong incontracts. -
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:contractsfirst, then implementors, then consumers. -
The
errssub-package formalises a contract that previously existed only as private duck-typed interfaces inside micro-lib'slogz. The two interfaces are intentionally separate (CodedErrorandContextualErrorrather than one combinedRichError) 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. -
Checkablelives incontracts/observability, not in thewebstarter. Data starters (db-postgres,db-sqlite, etc.) implementCheckablewithout importing the HTTP layer. The health handler inwebconsumesCheckable— the dependency arrow points into the data layer, never out. -
Every file in
contractsexports exactly one type or interface. This is enforced structurally and mechanically by the compliance test usinggo/astparsing. A reviewer readingpermission_mask.goknows exactly what they are reading without opening any other file.