api-bones 4.0.0

Opinionated REST API types: errors (RFC 9457), pagination, health checks, and more
Documentation
# Changelog

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

The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [Unreleased]

## [4.0.0] - 2026-04-20

### BREAKING CHANGE

- Removed `impl FromRequestParts for OrgId`. The bare axum extractor allowed tenant-confusion — a client holding a valid token for org A could send `X-Org-Id: <org B>` and land at a handler that wrote to org B with org A's principal. Handlers must take `OrganizationContext` exclusively, which reconciles token claims, the `X-Org-Id` header, and any path parameter (see ADR platform/0015).

### Added

- `OrgId::try_from_headers(&http::HeaderMap) -> Result<OrgId, OrgIdHeaderError>` (feature `http`) — non-extractor parser for callers without an `AuthLayer` (webhook verifiers, out-of-band tooling).
- `OrgIdHeaderError` with `Missing`, `NotUtf8`, `Invalid` variants.
- `examples/header_parser.rs` demonstrating the sanctioned non-axum usage.

### Migration

```rust
// Before (rejected — tenant-confusion loophole):
async fn handler(org_id: OrgId) { /* writes to org_id without reconciling auth */ }

// After:
async fn handler(ctx: OrganizationContext) {
    let org_id = ctx.org_id;
    // ctx also carries the authenticated Principal, RequestId, roles, and attestation.
}
```

## [3.1.0] - 2026-04-19

### Added

- `Principal::org_path_display() -> String` — returns the org ancestry path as a
  comma-separated UUID string (`""` for platform-internal actors). Gated behind the
  `uuid` feature. Canonical formatter for the `enduser.org_path` OTEL span attribute
  (platform/0010).

## [3.0.0] - 2026-04-19

### Added

- `PrincipalId(Cow<'static, str>)` — identity string newtype extracted from `Principal`.
- `PrincipalKind``User | Service | System` enum on `Principal`.
- `Principal.org_path: Vec<OrgId>` — org ancestry carried on the principal (requires `uuid` feature).
- `RoleScope``Self_ | Subtree | Specific(OrgId)` scoping for role bindings.
- `RoleBinding { role: Role, scope: RoleScope }` — scope-aware role assignment.
- `OrgPath(Vec<OrgId>)``X-Org-Path` header newtype with `HeaderId`, `FromStr`, and axum extractor.
- `OrganizationContext.org_path: Vec<OrgId>` + `with_org_path` builder.
- `ErrorCode::OrgOutsideSubtree`, `AncestorRequired`, `CrossSubtreeAccess` — new 403 variants.

### Changed

- **BREAKING** `Principal` is now `struct { id: PrincipalId, kind: PrincipalKind, org_path: Vec<OrgId> }` (was a `Cow<str>` newtype). Serde wire format changed.
- **BREAKING** `Principal::system()` is no longer `const` (contains `Vec`).
- **BREAKING** `OrganizationContext.roles` is now `Vec<RoleBinding>` (was `Vec<Role>`).

### Removed

- **BREAKING** `Principal::from_owned(String)` moved to `PrincipalId::from_owned`.

## [2.3.1] - 2026-04-19

### Added

- `Principal::from_owned(String) -> Self` — construct a principal from a runtime-owned
  string for database round-trips. Accepts any string without UUID validation. Use only for
  deserialization; prefer `Principal::human` / `Principal::system` for new actors.

## [2.3.0] - 2026-04-18

### Added

- `OrgId` — UUID v4 tenant identifier newtype, propagated via the `X-Org-Id` HTTP header.
  Implements `HeaderId`, `FromStr`, `Display`, `From<Uuid>`, axum `FromRequestParts`
  (behind the `axum` feature), and serde. Requires the `uuid` feature.
- `OrganizationContext` — cross-cutting platform context bundle carrying `OrgId`,
  `Principal`, `RequestId`, `Vec<Role>`, and `Option<Attestation>` in a single
  cheap-to-clone struct. Constructor: `OrganizationContext::new(org_id, principal,
  request_id)`. Builder methods: `with_roles`, `with_attestation`. Serde derives gated on
  the `serde` feature.
- `Role(Arc<str>)` — token-neutral role label newtype. No permission semantics in
  api-bones; permission evaluation belongs to consumer auth layers.
- `Attestation { kind: AttestationKind, raw: Vec<u8> }` — opaque credential payload with
  kind tag. Downstream auth crates decode the raw bytes per kind.
- `AttestationKind``#[non_exhaustive]` enum: `Biscuit`, `Jwt`, `ApiKey`, `Mtls`.

## [2.2.1] - 2026-04-18

### Added

- `Principal::from_stored(String) -> Principal` — reconstruct a `Principal` from a
  previously persisted string (database column, serialised message). Accepts both UUID
  and system-name formats without validation, since the value was already validated at
  write time. Use on read paths only; prefer `human` or `system` for new principals.

## [2.2.0] - 2026-04-18

### Added

- `Principal::human(uuid: Uuid) -> Principal` — UUID-typed constructor for human/operator
  actors. Requires the `uuid` feature (default). Prevents PII (emails, display names) from
  entering audit logs and OTEL spans.
- `Principal::try_parse(&str) -> Result<Principal, PrincipalParseError>` — parses any
  valid UUID text form; rejects arbitrary strings, emails, and empty input.
- `PrincipalParseError` — error type returned by `try_parse`; exposes the offending input
  string. Implements `Display` and `std::error::Error`.
- `ResolvedPrincipal { id: Principal, display_name: Option<String> }` — read-path display
  helper that pairs an opaque `Principal` with an optional name resolved at read time by
  the identity service. Never persisted. Implements `Display` fallback to UUID, serde
  (skips `display_name` when `None`), and `From<Principal>`.

### Changed (BREAKING)

- `Principal::new(impl Into<String>)` **removed**. Passing arbitrary strings is a
  GDPR/PII violation (see issue #204). Migrate:
  - Human actors: `Principal::human(uuid)` or `Principal::try_parse(uuid_str)?`
  - System actors: `Principal::system("my-service.worker")` (unchanged)
- `fake` / `arbitrary` / `proptest` impls for `Principal` now generate UUID-backed values
  instead of random strings.

### Changed

- CI: migrated to reusable shared workflow.

## [2.1.0] - 2026-04-16

### Added

- `audit::Principal` — canonical actor-identity newtype (`Cow<'static, str>`-backed).
  - `Principal::new(impl Into<String>)` for end-user/operator IDs.
  - `Principal::system(&'static str)``const`, infallible, zero-alloc for autonomous actors.
  - `as_str`, `Display`, non-redacting `Debug`, `Eq`, `Hash`, `Clone`.
  - Feature-gated integrations: `serde` (transparent string), `utoipa`, `schemars`,
    `arbitrary`, `proptest`.
- Re-export: `api_bones::Principal`.

### Changed (BREAKING)

- `AuditInfo::created_by` and `AuditInfo::updated_by` are now non-optional
  `Principal` fields (previously `Option<String>`). System processes are still
  actors and must declare themselves via `Principal::system`.
- `AuditInfo::new(created_at, updated_at, created_by: Principal, updated_by: Principal)`.
- `AuditInfo::now(created_by: Principal)``updated_by` now initialized to a
  clone of `created_by` rather than `None`.
- `AuditInfo::touch(&mut self, updated_by: Principal)` — no longer accepts
  `Option`.

### Migration

Downstream crates that previously passed `Option<String>` must migrate to
`Principal`:

```rust
// Before
AuditInfo::now(Some("alice".to_string()));
audit.touch(None);

// After
use api_bones::Principal;
AuditInfo::now(Principal::new("alice"));
audit.touch(Principal::system("my-service.cleanup"));
```

## [2.0.3] - 2026-04-10

### Added

- `api-bones-tower`: optional `uuid` and `chrono` feature passthroughs for parity with `api-bones-reqwest`
- `api-bones-tower`, `api-bones-reqwest`: rustdoc feature tables documenting default and optional `api-bones` features

### Changed

- `api-bones-tower`, `api-bones-reqwest`: versions aligned to `2.0.3`

## [2.0.2] - 2026-04-10

### Fixed

- `ApiError::causes` annotated with `#[schema(value_type = Vec<Object>)]` to prevent infinite recursion in utoipa schema generation (`causes: Vec<Self>` caused a stack overflow when any crate called `OpenApi::openapi()` with `ApiError` in its components)

## [2.0.1] - 2026-04-10

### Fixed

- CI: publish pipeline now correctly publishes all workspace crates (`api-bones-tower`, `api-bones-reqwest`)
- `api-bones-tower` version aligned to `2.0.1` (was incorrectly left at `0.1.0`)

## [2.0.0] - 2026-04-09

### Breaking Changes

- **`response::Links` removed** — use `links::Links` everywhere. `links::Links` is `Vec<Link>`-backed and supports arbitrary `rel` types; the old 3-field struct is gone.
- **`models::ErrorResponse` removed** — use `ApiError` directly for all error responses.
- **`reqwest` feature removed from main crate**`ApiError::from_response` moved to the new `api-bones-reqwest` satellite crate.
- **`RequestIdParseError` renamed to `RequestIdError`** — now an enum with meaningful variants.

### Added

- **`api-bones-tower`** satellite crate — Tower `RequestIdLayer` and `ProblemJsonLayer` extracted from main crate; main crate is now pure types.
- **`api-bones-reqwest`** satellite crate — reqwest client extensions (`RequestBuilderExt`, `ResponseExt`) extracted from main crate.
- **`HeaderId` trait** — shared abstraction over `RequestId`, `CorrelationId`, `IdempotencyKey` HTTP header newtypes (`as_str()`, `header_name()`).
- **`TryFrom<StatusCode> for ErrorCode`** — 4xx/5xx status codes now convert back to their canonical `ErrorCode`.
- **`QueryBuilder`** — typed query parameter builder with `.set()`, `.set_opt()`, `.extend_from_struct<T: Serialize>()`, `.merge_into_url()`.
- **Fallible constructors** for all constrained param types: `PaginationParams::new()`, `CursorPaginationParams::new()`, `KeysetPaginationParams::new()`, `SearchParams::new()` — enforce constraints without the `validator` feature.
- **`FromRequestParts`** implemented directly on core types (`PaginationParams`, `SortParams`, `IfMatch`, `IfNoneMatch`) when `axum` feature is enabled — no wrapper newtypes needed.
- **`ETag::parse_list()`** — parse comma-separated `If-Match`/`If-None-Match` header values.
- **Feature powerset CI** via `cargo-hack` — all feature combinations tested.
- New runnable examples: `auth_flow`, `bulk_envelope`, `cursor_hmac`, `error_construction`, `cache_control`, `etag_conditional`, `range_headers`, `trace_context`, `axum_core_extractors`, `query_builder`.

### Changed

- `api-bones-tower` and `api-bones-reqwest` version aligned with main crate (`2.0.0`).
- `api-bones-tower` adds optional `uuid` and `chrono` feature passthroughs.
- `PaginationParams` fields use `#[serde(default)]` matching the rest of the query param types.
- `CorrelationId` redundant constructors (`new_random`, `new_id`) removed — keep only `new_uuid()`.
- `ErrorTypeMode` std-only limitation (`error_type_mode()`, `set_error_type_mode()`) documented explicitly.

### Removed

- `models::ErrorResponse` (use `ApiError`)
- `response::Links` (use `links::Links`)
- `reqwest` feature from main crate (moved to `api-bones-reqwest`)
- `tower` feature from main crate (moved to `api-bones-tower`)
- `CorrelationId::new_random()`, `CorrelationId::new_id()` (redundant)

## [1.10.0] - 2026-04-09

### Added

- 12 new `ErrorCode` variants rounding out common HTTP error codes (#82):
  - `MethodNotAllowed` (405), `NotAcceptable` (406), `RequestTimeout` (408)
  - `Gone` (410), `PreconditionFailed` (412), `PayloadTooLarge` (413)
  - `UnsupportedMediaType` (415), `PreconditionRequired` (428)
  - `RequestHeaderFieldsTooLarge` (431), `NotImplemented` (501)
  - `BadGateway` (502), `GatewayTimeout` (504)
- Each variant wired through `status_code()`, `title()`, `urn_slug()`, and `from_type_uri()` roundtrip

## [1.9.0] - 2026-04-09

### Added

- **Axum extractors** (`axum` feature) — `FromRequestParts` impls removing the need for downstream newtype wrappers:
  - `PaginationParams` and `CursorPaginationParams` — parse query string and run `validator` range checks (limit 1..=100)
  - `SortParams` — parse `sort_by` / `direction` query params
  - `IfMatch` and `IfNoneMatch` — parse the matching conditional request headers, including the `*` wildcard and comma-separated tag lists
  - All rejections are `ApiError::bad_request`, so consumers get Problem+JSON bodies for free
- **`ETag` wire-format parsing** (`http` feature):
  - `impl FromStr for ETag` — accepts `"v1"` / `W/"v1"`, rejects unquoted, empty, or malformed input
  - `ETag::parse_list(&str)` — handles comma-separated header values (e.g. `If-Match: "a", W/"b"`)
  - `ParseETagError` enum with `Display` + `std::error::Error`
- **Structured rate-limit metadata on `ApiError`**:
  - `ApiError::with_rate_limit(RateLimitInfo)` — attaches quota data to any error
  - `ApiError::rate_limited_with(RateLimitInfo)` — 429 constructor that derives `detail` from `retry_after`
  - New optional `rate_limit` field serializes inline on `ApiError` and propagates to `ProblemJson` as the `rate_limit` extension member
- Runnable example `axum_extractors_and_ratelimit` demonstrating the new extractors and structured 429 bodies

## [1.8.0] - 2026-04-08

### Added

- `ProblemJson` — RFC 7807 / 9457 wire-format response type with flat extension members
  - Fields: `type`, `title`, `status`, `detail`, `instance`, `extensions` (flattened `HashMap`)
  - `ProblemJson::new(type, title, status, detail)` constructor
  - `ProblemJson::with_instance(uri)` builder method
  - `ProblemJson::extend(key, value)` for inserting extension members (e.g. `trace_id`)
  - `From<ApiError> for ProblemJson` — converts `request_id``instance`, `errors` → extension
  - `IntoResponse` for `ProblemJson` (requires `axum` feature) with `application/problem+json` Content-Type
  - `utoipa::ToSchema` and `schemars::JsonSchema` derives (requires respective features)
  - `problem_json` runnable example

## [1.7.0] - 2026-04-08

### Fixed

- `ErrorCode` utoipa schema now emits the actual wire format (`urn:api-bones:error:*` strings) instead of Rust variant names, fixing SDK client deserialization of error responses

## [1.6.0] - 2026-04-06

### Added

- `schemars` support: `JsonSchema` derive on all public types
- Runnable usage examples for axum, pagination, and health checks
- Comprehensive per-type example files
- Doc-tests on all public items

### Changed

- Renamed crate from `shared-types` to `api-bones`

### Fixed

- `ErrorTypeMode` now uses `RwLock` instead of `OnceLock` for test safety
- CI: correct crate name in publish summary
- CI: add Forgejo cargo registry configuration

## [1.5.0] - 2026-04-06

### Added

- `Slug` validated newtype for URL-friendly identifiers
- `source()` chaining for `ApiError` and `ValidationError` implementations
- `no_std` support behind feature flags

## [1.4.0] - 2026-04-04

### Added

- `SortParams`, `FilterParams`, and `SearchParams` query types
- Generic `ApiResponse<T>` envelope with metadata
- HATEOAS `Link` and `Links` types
- `RateLimitInfo` type for rate limit metadata
- `ETag` and conditional request types (RFC 7232)
- `arbitrary` and `proptest` feature flags
- `BulkRequest`, `BulkResponse`, and `BulkItemResult` types
- `AuditInfo` struct for resource audit metadata with arbitrary/proptest support
- `fake` crate integration for test fixture generation
- Typestate builder patterns for `ApiError`, `HealthCheck`, `ReadinessResponse`
- `CursorPaginationParams` and usage guidance

### Fixed

- Restored `lib.rs` modules and synced `Cargo.toml` after bulk types addition

## [1.3.1] - 2026-04-04

### Fixed

- Removed duplicate page-based pagination types

### Changed

- Pinned Rust toolchain to 1.94
- Removed Gherkin feature file from shared-types

## [1.3.0] - 2026-04-04

### Added

- Flat `PaginatedResponse` and validated `PaginationParams`

## [1.2.0] - 2026-03-25

## [1.1.0] - 2026-03-25

### Added

- iCalendar RFC 5545 support as optional `calendar` feature

## [1.0.0] - 2026-03-14

### Added

- Initial release with core API types: `ApiError`, `ValidationError`, `HealthCheck`, `ReadinessResponse`, `PaginationParams`

[Unreleased]: https://github.com/brefwiz/api-bones/compare/v4.0.0...HEAD
[4.0.0]: https://github.com/brefwiz/api-bones/compare/v3.1.0...v4.0.0
[3.1.0]: https://github.com/brefwiz/api-bones/compare/v3.0.0...v3.1.0
[3.0.0]: https://github.com/brefwiz/api-bones/compare/v2.0.0...v3.0.0
[2.0.0]: https://github.com/brefwiz/api-bones/compare/v1.10.0...v2.0.0
[1.10.0]: https://github.com/brefwiz/api-bones/compare/v1.9.0...v1.10.0
[1.9.0]: https://github.com/brefwiz/api-bones/compare/v1.8.0...v1.9.0
[1.8.0]: https://github.com/brefwiz/api-bones/compare/v1.7.0...v1.8.0
[1.7.0]: https://github.com/brefwiz/api-bones/compare/v1.6.0...v1.7.0
[1.6.0]: https://github.com/brefwiz/api-bones/compare/v1.5.0...v1.6.0
[1.5.0]: https://github.com/brefwiz/api-bones/compare/v1.4.0...v1.5.0
[1.4.0]: https://github.com/brefwiz/api-bones/compare/v1.3.1...v1.4.0
[1.3.1]: https://github.com/brefwiz/api-bones/compare/v1.3.0...v1.3.1
[1.3.0]: https://github.com/brefwiz/api-bones/compare/v1.2.0...v1.3.0
[1.2.0]: https://github.com/brefwiz/api-bones/compare/v1.1.0...v1.2.0
[1.1.0]: https://github.com/brefwiz/api-bones/compare/v1.0.0...v1.1.0
[1.0.0]: https://github.com/brefwiz/api-bones/releases/tag/v1.0.0