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
|