Files
contracts/CHANGELOG.md
T

203 lines
9.9 KiB
Markdown
Raw Permalink Normal View History

# 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).
---
2026-08-13 23:10:05 -06:00
## [1.6.0] — 2026-08-13
Minor — coordinated framework release (lockstep versioning). No changes to this module's own API.
## [1.5.0] — 2026-08-09
Minor — coordinated framework version alignment. No code, API, or dependency changes.
## [1.4.0] — 2026-08-09
Minor — coordinated framework release: dependency refresh + lockstep version alignment.
### Changed
- Refreshed dependencies to their latest minor/patch where available.
## [1.3.0] — 2026-08-08
Minor — coordinated framework version alignment. No code, API, or dependency changes.
## [1.2.0] — 2026-08-08
Minor — coordinated framework version alignment. No code, API, or dependency changes.
## [1.1.3] — 2026-08-08
Patch — coordinated framework version alignment. No code, API, or dependency changes.
## [1.1.2] — 2026-08-08
Patch — coordinated framework version alignment. No code, API, or dependency changes.
## [1.1.1] — 2026-08-07
Patch — coordinated framework version alignment. Released in lockstep at v1.1.1
alongside a documentation patch in the data/infra starters. No code, API, or dependency changes.
## [1.1.0] — 2026-08-07
### 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