# Changelog
All notable changes to this project are documented here. Format: [Keep a
Changelog](https://keepachangelog.com/): versions follow [semver](https://semver.org).
## [Unreleased]
## [1.0.0] - 2026-10-24
### Declared stable
First 1.0. `tokenkit` issues and verifies production JWT sessions (including the
estate's own accessctl and barbican consumers); this release commits the encode,
validation, rotation, and revocation surfaces as API-stable.
### Guarantees from here on
- Breaking changes to public items require a minor bump plus a `#[deprecated]`
window of at least one minor release.
- MSRV bumps are minor releases, not patch releases.
- `#![deny(missing_docs)]` and `#![forbid(unsafe_code)]` are enforced.
- Tokens written by any 1.x release verify against every later 1.x release.
### Explicitly not guaranteed
- Private claim shapes used by estate consumers are documented but may gain
optional fields in minors.
- New mandatory claims will never be introduced in a patch release.
## [0.4.1] - 2026-09-12
### Added
- `tests/config_matrix.rs`: revocation-wiring coverage (feature `revocation`)
for `JwtService::with_revocation` — revoked jti rejected with
`JwtError::Revoked` via `decode_standard`, live jti accepted, revoked
refresh token rejected, and the no-jti-bypasses-store contract documented.
This was the last untested knob: the full `JwtConfig` matrix (leeway,
required_claims, audiences, validate_exp/nbf, TTLs, rotation, JWKS,
revocation limits) is now behavior-proven. Dead-knob sweep found zero dead
knobs; `build_validation()` verified to flow every validation knob.
### Fixed
- Doc link `crate::revocation::InMemoryRevocationStore` on
`DEFAULT_REVOCATION_MAX_ENTRIES` broke `cargo doc` without the
`revocation` feature; reworded feature-agnostically (`cargo doc --no-deps`
is warning-free again).
## [0.4.0] - 2026-09-12
### Added
- **`JwtService::encode_refresh_standard`** — mints refresh tokens expiring
after `JwtConfig::refresh_token_ttl` (default 7 days). This *wires* the
`refresh_token_ttl` config field, which was previously accepted and
defaulted (604800) but **never read by any behavior** — a dead knob:
callers (e.g. clawdius-auth) that set it got no effect. Access and
refresh lifetimes now come from the same config so the two cannot drift
apart unnoticed; revocation applies to refresh tokens through the same
`decode_standard` path.
### Tests
- New `tests/config_matrix.rs` proving each config knob observably changes
behavior: `access_token_ttl` / `refresh_token_ttl` stamped expiry (and
their divergence), `with_audiences` accept/reject, JWKS `with_client`
(fetch routed through the provided client, proven via a wire probe),
`with_min_refresh_interval` (on-miss refreshes skipped inside the window,
performed at ZERO), and `with_ttl` (zero TTL re-fetches even cached kids)
— all against a loopback mock JWKS endpoint with a fetch counter, no
sleeps, no external network.
## [0.3.0] - 2026-09-11
Security release: closes the JWKS alg-confusion surface, removes
blocking/blocking-in-async revocation paths, and bounds memory in the
in-memory revocation store.
### Security
- **JWKS algorithm pinning**: a JWK that advertises an `alg` member now
must match the token header's `alg`; mismatches are rejected with the
new `JwtError::AlgorithmMismatch` (HS256 token presented against an
RS256-published key no longer reaches verification). JWKs without an
`alg` accept only the caller's configured algorithm.
- **Async-correct revocation**: `JwtService::decode_standard` is now
`async` and awaits the revocation store directly — the
`block_in_place` + `block_on` wrapper (which panics on current-thread
runtimes) is gone.
- **Redis store is async**: `RedisRevocationStore` uses a multiplexed
`redis::aio::MultiplexedConnection` (`tokio-comp` feature) instead of
blocking `redis::Connection` calls inside async fns; the connection is
lazy, re-established on failure, and commands retry once (fail-closed).
- **Bounded in-memory store**: `InMemoryRevocationStore` now holds at
most `max_entries` (default 100k, `with_limits`) with oldest-first
eviction and an optional per-entry TTL (swept on insert, checked on
lookup) — revocation floods can no longer grow memory without bound.
### Added
- Algorithms: `ES256`, `ES384`, `EdDSA`, `PS256`, `PS384`, `PS512` (with
`HS*`/`RS*` now covering all twelve jsonwebtoken 11 algorithms). Key
loaders `JwtConfig::from_ec_pem` (ES*), `from_ed_pem` (EdDSA),
`from_ed_der`, plus builder methods `with_public_key`,
`with_der_public_key`, `with_leeway`, `with_audiences`,
`with_required_claims`, `with_validate_exp`, `with_validate_nbf`.
- Configurable validation on `JwtConfig`: `leeway` (explicit, default
60s — no longer relies on jsonwebtoken's hidden default),
`required_claims` (default `["exp", "iss"]`), multi-audience
`audiences` (with `audience` kept as single-aud convenience, merged),
`validate_exp` (default true), `validate_nbf` (default false).
- JWKS hardening: single-flight refresh (N concurrent misses → one
fetch), minimum refresh interval (default 10s,
`with_min_refresh_interval`), `JwksCache::from_static_keys` for
air-gapped/test use.
- `kid`-based key rotation: `JwtConfig::with_rotation_keys` /
`RotationKey` pair secrets with key ids; decoding verifies a token
against the key its `kid` names. The try-every-key fallback applies
only to legacy `kid`-less tokens; an unknown `kid` is rejected
outright (no key-injection oracle).
### Changed
- **Breaking:** `decode_standard` is `async` (see Security).
- **Breaking:** `JwtConfig.rotation_secrets: Vec<String>` →
`rotation_keys: Vec<RotationKey>` (`with_rotation_secrets` still
exists for secret-only rotation). `RotationKey` carries an optional
`key_id` and public key material.
- **Breaking:** asymmetric verification now uses public key material
(`public_key` / `der_public_key`). In 0.2.0, decoding RS* tokens
through the *private* PEM in `secret` silently failed with
`InvalidKeyFormat` — a full sign+verify service was impossible for
RSA. Verify side now requires public key material (HMAC unaffected;
decode-only configs with a public PEM in `secret` keep working).
- **Breaking:** `JwtError` gained `AlgorithmMismatch` (and the config
gained public fields), so exhaustive matches/struct literals
downstream need updates.
- `JwksCache::decoding_key` refresh behavior: on-miss refreshes are
single-flight and rate-limited; `refresh()` remains a forced fetch.
## [0.2.0] - 2026-09-11
### Added
- `jwks` feature: `JwksCache` fetches and caches JSON Web Key Sets with
TTL caching and automatic re-fetch on unknown `kid` (key rotation
without restarts).
- `JwtError::Jwks` variant for JWKS fetch/parse failures.
### Changed
- **Breaking:** `JwtError` gained the `Jwks` variant without
`#[non_exhaustive]`, so exhaustive matches downstream need a new arm.
Semver-checks therefore requires a major (0.x → 0.2.0) bump relative
to the `v0.1.1` tag, whose tree predates the JWKS work.
- `jsonwebtoken` 9 → 11 (RustCrypto backend).
## [0.1.1] - 2026-09-03
### Fixed
- `StandardClaims` `exp` / `iat` now serialize as numeric timestamps
(seconds since epoch) for JWT interoperability, instead of RFC 3339
strings.
## [0.1.0] - 2026-08-31
### Added
- Type-safe JWT encode/decode: `StandardClaims` with serde derives,
HS256/384/512 and RS256/384/512 algorithms.
- Built-in validation: issuer, audience, and expiration checks.
- Token revocation: pluggable `TokenRevocationStore` trait with in-memory
and Redis-backed implementations.
- Secret & key rotation: swap `JwtConfig` without downtime; SWR caching.
- JWT helpers: `extract_bearer_token()`, `build_auth_cookie()`.
- `#![forbid(unsafe_code)]`; criterion benches, proptest suites, and
cargo-fuzz targets (`jwt_encode` / `jwt_decode`).