# Changelog

All notable changes to `pas-external` are documented in this file.

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

> **Pre-1.0 version reset (2026-04-30):** All previously-published
> versions (`1.0.1`, `2.0.0`, `3.0.0`, `3.1.0`, `4.0.0`, `4.0.1`,
> `4.0.2`, and the development series targeted at `5.0.0`) were
> yanked from crates.io on 2026-04-30 to align the public version
> cadence with the project's pre-production status. While in `0.x.y`,
> breaking changes are landed as **minor bumps** per SemVer §11
> (e.g., `0.2.0` may break compat with `0.1.0`). Historical changelog
> entries for the yanked versions are preserved below for archaeology
> — adopters of `0.1.0+` do not need to read them.

## [0.10.0] — 2026-05-08

Phase 11.Z continuation — closes the per-user RCW/CTW sv enforcement
gap, adds the L2 (session-row liveness) verifier slot, and removes
the 0.9.0 `UserinfoFetcher` that ignored its `_sub` parameter. Adds
the canonical KVRocks-backed `SharedCacheCache` (lifted from chat-api)
and ships the SessionLiveness port for consumer L2 substrate
adapters.

Trigger: `RFC_2026-05-08_pas-external-0.10.0-rcw-ctw-sv-axis-completion.md`
§4 (Slice 0 audit closure) + §3 Slice 1 (SDK shape change).

### Added

- **`pas_external::epoch::SharedCacheCache`** (gated
  `feature = "shared-cache"`) — adapter implementing the SDK's
  sv-specific `Cache` (`Option<i64>`) over any
  `Arc<dyn ppoppo_infra::Cache>`. Substrate-agnostic by construction:
  KVRocks via `ppoppo-kvrocks::KvCache` for production, in-memory
  mocks for tests. Promoted from chat-api's `session_version::KvCache`
  (replace, don't layer — lift mirrors RFC 11.Z chat-auth promotion).
  RFC_2026-05-08 §4.1 lock.
- **`pas_external::session_liveness::SessionLiveness`** — new L2 port
  for per-request session-row revocation enforcement. Distinct from
  the existing `attempt_liveness_refresh` (PAS-callback periodic
  check). 3-state contract: `Ok(())` = live, `Err(Revoked)` = absent
  or revoked row, `Err(Transient(detail))` = substrate down
  (fail-closed). Trait + error enum are NOT gated on
  `feature = "session-liveness"` — the AES wrapper and the lookup
  port have orthogonal dep needs. RFC_2026-05-08 §4.2 lock.
- **`pas_external::session_liveness::SessionLivenessError`** — typed
  error (`Revoked` / `Transient(String)`).
- **`PasJwtVerifier::with_session_liveness(Arc<dyn SessionLiveness>)`** —
  opt-in verifier slot symmetric to `with_epoch_revocation`. With no
  port wired, the verifier short-circuits the L2 check (matches
  pre-0.10.0 behavior). With a port wired, every verify (after engine
  success) consults the port for the bearer's `sid` claim. **Lenient
  on no-`sid`**: tokens without a `sid` claim admit without consulting
  the port — non-session-bound tokens (machine credentials, AI-agent
  flows, R6 legacy admit) have no row to look up. RFC_2026-05-08 §4.2
  lock.
- **`VerifyError::SessionRevoked`** — typed variant for L2 row-revoked
  rejection. Distinct from `SessionVersionStale` (L1) so audit logs
  pivot per-session logout from cluster-wide break-glass.
- **`VerifyError::SessionLivenessLookupUnavailable`** — typed variant
  for L2 substrate-down rejection (HTTP 503, fail-closed). Distinct
  from `SessionVersionLookupUnavailable` (L1).
- **`audit::VerifyErrorKind::SessionRevoked`** +
  **`SessionLivenessLookupUnavailable`** — companion audit-pivot
  variants.
- New optional dep: **`ppoppo-infra` workspace crate** (trait-only,
  no Redis client deps). Pulled by `feature = "shared-cache"` only;
  default builds unchanged.

### Removed

- **`pas_external::epoch::UserinfoFetcher`** — deleted. The 0.9.0
  framing was misleading: PAS's userinfo authenticates the *caller*,
  not an arbitrary queried subject, so `Fetcher::fetch(_sub)` ignored
  `_sub` and returned the caller's own sv. Zero consumers across the
  3-monorepo workspace verified 2026-05-08. Replacement:
  `SharedCacheCache` over the canonical KVRocks `sv:{sub}` key
  (proper per-user shape). `feedback_no_backcompat_healthy_arch` +
  pre-launch state authorize delete-not-shim.

### Migration

For consumers upgrading from 0.9.0:

```diff
- use pas_external::epoch::{UserinfoFetcher, InProcessTtlCache, CompositeEpochRevocation};
+ use pas_external::epoch::{SharedCacheCache, CompositeEpochRevocation};

- let cache = Arc::new(InProcessTtlCache::new(Duration::from_secs(60)));
- let fetcher = Arc::new(UserinfoFetcher::new(pas_url).with_access_token(svc_token));
+ let cache: Arc<dyn pas_external::epoch::Cache> = Arc::new(SharedCacheCache::new(kv));
+ let fetcher: Arc<dyn pas_external::epoch::Fetcher> = /* consumer-specific — see below */;
  let port = Arc::new(CompositeEpochRevocation::new(cache, fetcher));
  verifier.with_epoch_revocation(port)
```

#### RCW/CTW L1 sv-axis Fetcher gap (deferred to Slice 3)

After 0.10.0, `pas_external::epoch::Fetcher` has **no opinionated
default implementation**. Consumers wiring `with_epoch_revocation`
for L1 sv-axis enforcement choose between:

- **Cache-only**: wire only `SharedCacheCache`, accept fail-closed on
  cache miss. Cache miss → `VerifyError::SessionVersionLookupUnavailable`
  (HTTP 503). PAS-side aggressive pre-warming required.
- **PAS admin-readout endpoint**: blocked on a separate PAS RFC
  adding `/admin/sv/{sub}` (or equivalent). Once available, ship
  `HttpAdminSvFetcher` in pas-external (or consumer-side adapter)
  and wire as the cache-miss authoritative source. Symmetric to
  chat-api's `PgFetcher` shape but transport-routed.
- **Engine relax**: ppoppo-token modifies `EpochRevocation` to make
  Fetcher optional. NOT recommended — changes fail-closed semantics
  that STANDARDS_AUTH_INVALIDATION §3 treats as load-bearing.

`pas-external 0.10.0` ships only the cache adapter; the Fetcher
choice is a Slice 3 deployment decision per consumer.
RFC_2026-05-08 §4.4.

#### L2 SessionLiveness wire shape (RCW/CTW Slice 3/4)

Consumer-side adapter (~10 LOC):

```rust,ignore
use async_trait::async_trait;
use pas_external::{SessionLiveness, SessionLivenessError};
use pas_external::types::SessionId;
use sqlx::PgPool;

#[derive(Debug)]
pub struct PgSessionLiveness { pool: PgPool }

#[async_trait]
impl SessionLiveness for PgSessionLiveness {
    async fn check(&self, sid: &SessionId) -> Result<(), SessionLivenessError> {
        let row: Option<(Option<time::OffsetDateTime>,)> =
            sqlx::query_as("SELECT revoked_at FROM scrcall.user_sessions WHERE id = $1")
                .bind(&sid.0).fetch_optional(&self.pool).await
                .map_err(|e| SessionLivenessError::Transient(format!("session lookup: {e}")))?;
        match row {
            None | Some((Some(_),)) => Err(SessionLivenessError::Revoked),
            Some((None,)) => Ok(()),
        }
    }
}
```

Wire at verifier construction:
`PasJwtVerifier::with_session_liveness(Arc::new(PgSessionLiveness{pool}))`.
RCW uses `scrcall.user_sessions`; CTW mirror uses
`scctime.user_sessions`.

### Workspace-internal consumer migrations (same commit as SDK shape change)

- **chat-api** — `services/chat/crates/chat-api/src/session_version.rs`
  shrinks: local `KvCache` deleted (lifted to SDK as
  `SharedCacheCache`); `PgFetcher` retained (chat-api-specific
  cross-schema reader, kept consumer-side). `chat-api/src/main.rs`
  rewires to `SharedCacheCache::new(cache)` directly. Replace, don't
  layer.
- **PAS userinfo handler doc-comment** —
  `services/accounts/crates/accounts-api/src/rest/userinfo.rs`
  updated to reflect `SharedCacheCache` as the canonical reader.

### Publish prerequisites (Slice 2 split)

Slice 2a (separate user-confirmed session) publishes
`ppoppo-infra v0.1.0` to crates.io before pas-external 0.10.0
publish (Slice 2b) because `feature = "shared-cache"` pulls the
trait crate. Workspace path-deps make local builds + tests work
without the registry; `cargo publish -p pas-external` requires
ppoppo-infra available on crates.io first. RFC_2026-05-08 §3
(Slice frame) + §4.1 closure note.

## [0.9.0] — 2026-05-09

Phase 11.Z — sv-axis enforcement port surfaced through the SDK
boundary. Previously the engine's `EpochRevocation` port was
unreachable from `PasJwtVerifier`-based consumers (RCW/CTW), so every
PAS-issued token admitted past the sv axis regardless of break-glass
state. 0.9.0 exposes the port and ships the canonical adapter set.

Trigger: `RFC_2026-05-09_pas-external-0.9.0-sv-axis-surfacing.md`
(§3 + §3.5 — Slice 1).

### Added

- **`pas_external::epoch`** — new module gated on
  `feature = "well-known-fetch"`. Re-exports the engine's
  [`EpochRevocation`] port + [`EpochRevocationError`] from
  `ppoppo-token`, plus the canonical adapter set:
  - **`Cache`** — best-effort `sv:{sub}` cache port
    (`get` / `set`). Promoted from chat-auth's private
    `SessionVersionCache` trait per RFC §3 Row 6 ("replace, don't
    layer").
  - **`Fetcher`** — authoritative substrate readout port
    (`fetch(sub) -> Result<i64, FetchError>`). Promoted from
    chat-auth's private `SessionVersionFetcher` trait.
  - **`InProcessTtlCache`** — opinionated in-process TTL `Cache`
    impl. Default for RCW/CTW (Slice 4/5); per-pod, lazy-evicting,
    `RwLock<HashMap>`-backed.
  - **`UserinfoFetcher`** — HTTP `Fetcher` impl reading
    `session_version` from PAS `/userinfo`. Requires the consumer's
    OAuth scope set to include `"session_version"` (re-enabled on
    PAS-side, scope-gated, in this same release).
  - **`CompositeEpochRevocation`** — combines a `Cache` + a
    `Fetcher` into the engine port. Cache hit short-circuits;
    cache miss fetches authoritative + writes back; fetcher
    transient surfaces as `EpochRevocationError::Transient`
    (fail-closed). Promoted from chat-auth's
    `ChatAuthEpochRevocation`.
  - Re-exports of `SV_CACHE_TTL` (60 s) and `sv_cache_key(sub)`
    so consumers writing custom `Cache` impls share the canonical
    namespace with PAS's writer
    (`STANDARDS_SHARED_CACHE.md` §3.1).
- **`PasJwtVerifier::with_epoch_revocation(Arc<dyn EpochRevocation>)`**
  — opt-in builder method. With no port wired the engine's
  `check_epoch` gate short-circuits (matching pre-11.Z behavior).
  With a port wired, every verify call queries
  `port.current(sub)` and the engine compares against the token's
  `sv` claim. Stale tokens reject as
  `VerifyError::SessionVersionStale`; substrate-down failures reject
  as `VerifyError::SessionVersionLookupUnavailable` (fail-closed).
- **`VerifyError::SessionVersionStale`** — typed variant for engine
  `AuthError::SessionVersionStale`. Pre-11.Z this collapsed to
  `Other(String)` along with every uncategorized engine error.
- **`VerifyError::SessionVersionLookupUnavailable`** — typed variant
  for engine `AuthError::SessionVersionLookupUnavailable`.
- **`audit::VerifyErrorKind::SessionVersionStale`** +
  **`SessionVersionLookupUnavailable`** — companion audit-pivot
  variants. Audit dashboards filter on these kinds to surface
  break-glass propagation lag (Stale) and substrate health problems
  (LookupUnavailable) distinct from cryptographic failure.

### Removed

- **`AuthSession::session_version() -> Option<i64>`** accessor. The
  doc-comment hedge — *"forward compatibility with a future engine
  that surfaces `sv` directly"* — was hedging in the wrong
  direction. Path A reshaped (RFC §1) confirms the forward direction
  is to NOT surface; Phase 4 Decision 1 keeps `sv` HIDDEN on engine
  `Claims`. Consumers wire `EpochRevocation` instead.

### PAS-side companion change

- **`UserInfoResponse.session_version: Option<i64>`** re-added,
  scope-gated on the new `"session_version"` scope. Reverses Phase
  10.13.B F6 cleanup partially — default-scope (`profile`/`email`)
  tokens still see no `session_version` on userinfo, preserving
  F6 spirit. Only consumers wiring `EpochRevocation` request the
  new scope. The `session_version` scope is added to PAS's
  `SUPPORTED_SCOPES`; consumers add it to their `RequestedScope`
  set at OAuth time.

### Migration

For consumers that want sv-axis enforcement (RCW/CTW canonical
shape):

```rust
use std::sync::Arc;
use std::time::Duration;
use pas_external::epoch::{
    CompositeEpochRevocation, InProcessTtlCache, UserinfoFetcher,
};

let cache: Arc<dyn pas_external::epoch::Cache> =
    Arc::new(InProcessTtlCache::new(Duration::from_secs(60)));
let fetcher: Arc<dyn pas_external::epoch::Fetcher> = Arc::new(
    UserinfoFetcher::new("https://accounts.ppoppo.com")
        .with_access_token(my_service_account_access_token),
);
let port: Arc<dyn pas_external::epoch::EpochRevocation> =
    Arc::new(CompositeEpochRevocation::new(cache, fetcher));

let verifier = pas_external::PasJwtVerifier::from_jwks_url(
    "https://accounts.ppoppo.com/.well-known/jwks.json",
    pas_external::Expectations::new(/*…*/),
)
.await?
.with_epoch_revocation(port);
```

Plus `RCW_SCOPES` / `CTW_SCOPES` (or equivalent) MUST contain
`"session_version"` so the consumer's access tokens grant the
substrate-readout scope.

For consumers that don't enforce sv-axis (no break-glass propagation
required), no migration is needed — `with_epoch_revocation` is opt-in
and the gate short-circuits without it.

### Deferred (out of 0.9.0)

- **`SharedCacheEpochRevocation`** (Cache-only adapter, no
  fetcher fall-through) — ships in 11.AB+ when `KVROCKS_URL` ACL
  extends to RCW/CTW workloads. The current 0.9.0 surface covers
  every consumer's needs; adding it later is non-breaking.
- **Bounded-capacity `InProcessTtlCache`** — current impl is
  unbounded. Pre-launch workloads with bounded user counts are
  unaffected. Future LRU-backed adapter ships when needed.

## [0.8.0] — 2026-05-08

Phase 11 SDK shape consolidation. Stabilization release with API
tightening; no runtime behavior change vs `0.8.0-beta.1`.

### Added

- `pas_external::test_support::FakePasServer` — wiremock-wrapped fake
  PAS Authorization Server. Replaces the
  `RelyingParty::for_test_with_parts` escape hatch from 0.7.x —
  consumer boundary tests now construct a real
  `RelyingParty::new(config, store)` against `FakePasServer.issuer_url()`
  and exercise the production HTTP discovery + JWKS bootstrap path.
  Gated on `feature = "test-support"`. Pulls in `wiremock` (optional dep).
- `pas_external::oidc::RefreshOutcome` — typed return for
  `RelyingParty::refresh`. Replaces direct exposure of the OAuth
  `TokenResponse` wire DTO at the SDK boundary; mirrors `Completion<S>`
  (the deep return of `complete`). `expires_in: Option<Duration>` (was
  `Option<u64>` on `TokenResponse`).
- `pas_external::test_support::TokenResponse` /
  `pas_external::test_support::PasIdTokenVerifier` — compatibility
  re-exports under the `test-support` feature. SDK boundary tests +
  downstream consumer integration tests use these to fabricate the
  OAuth wire DTO and instantiate the verifier directly. **11.Z migration
  target**: migrate those tests onto `FakePasServer` +
  `RelyingParty::complete`, then drop these re-exports.

### Changed

- `RelyingParty::refresh` return type: `oauth::TokenResponse` →
  `oidc::RefreshOutcome`. Migration: same field names; `expires_in:
  Option<u64>` → `Option<Duration>` (consumers drop a
  `.map(Duration::from_secs)` call from each callsite).

### Removed

- **`RelyingParty::for_test_with_parts`** — extracted-for-testability
  escape hatch. Use `test_support::FakePasServer` +
  `RelyingParty::new(...)` for the same coverage on the production HTTP
  boot path.
- **`pas_external::middleware::*`** — Phase 6.1 cookie-as-session-id
  flow (~1908 LOC across 13 files). Superseded by `pas_external::oidc::*`
  (composition root + post-verify shapes) + `pas_external::oidc::axum::*`
  (perimeter `BearerAuthLayer` + `AuthProvider` from Phase 11.X.C). All
  consumers (chat-auth, RCW, CTW) migrated in 11.X.A/B/C.
- **`pas_external::oauth::AuthorizationRequest`** type — sole consumer
  was the legacy middleware's `login` handler. `AuthClient::authorization_url`
  deleted with it.
- **`pas_external::oauth::UserInfo`** type — superseded by
  `oidc::IdAssertion<S>` for scope-bounded PII; sole consumer was the
  legacy middleware's userinfo flow.
- **`pas_external::pas_port::PasAuthPort::userinfo()`** method — no
  surviving consumer. The `refresh()` method is retained (consumed by
  `session_liveness::attempt_liveness_refresh` +
  `oidc::RelyingParty::refresh`).
- **`pas_external::token::jwt::peek_session_version`** — internal helper
  used only by the legacy middleware.
- **`OAuthConfig::with_userinfo_url` / `userinfo_url()`** — dead with
  the userinfo method removal.
- **`AuthClient::with_http_client`** — caller-supplied HTTP-client
  constructor; no consumer used it.
- SDK-internal tests `tests/session_validator_refresh.rs` +
  `tests/sv_aware_trait.rs` — covered the legacy middleware's
  internal `SessionValidator` + `SvAware`; deleted with them.
- `urlencoding` optional dep — was used only by the legacy middleware.

### Visibility tightened (`pub` → `pub(crate)` at module level)

- `oauth` module → `pub(crate)`. Consumers reach OAuth through
  `oidc::RelyingParty<S>` and `oidc::RefreshOutcome`. The `AuthClient`,
  `OAuthConfig`, `TokenResponse` types still exist but are unreachable
  externally.
- `pkce` module → `pub(crate)`. `RelyingParty::start` consumes PKCE
  primitives internally; the `generate_state` / `generate_code_verifier`
  / `generate_code_challenge` re-exports at the crate root are gone.
- `oidc::verifier` module → `pub(crate)`. `PasIdTokenVerifier` is no
  longer reachable via `pas_external::oidc::PasIdTokenVerifier` — use
  `RelyingParty::<S>::complete` for production verification.

### Surface after 0.8.0

The pas-external public surface is now:

- `oidc::RelyingParty<S>` — composition root for the OIDC RP flow.
- `oidc::axum::{BearerAuthLayer, AuthProvider, BearerAuthConfig, BearerAuthSession, ...}` — perimeter (Phase 11.X.C).
- `oidc::{IdAssertion<S>, IdTokenVerifier<S>, MemoryIdTokenVerifier<S>, RefreshOutcome, Completion<S>, Discovery, fetch_discovery, ...}` — post-verify shapes + ports.
- `oidc::{Config, State, RelativePath, AuthorizationRedirect, CallbackParams, StateStore, InMemoryStateStore, PendingAuthRequest, ...}` — state-store port + value types.
- `oidc::{Openid, Email, EmailProfile, ..., Nonce, ScopePiiReader, HasEmail, HasProfile, ...}` — scope markers.
- `token::{BearerVerifier, PasJwtVerifier, MemoryBearerVerifier, AuthSession, Expectations, VerifyError}` — γ port + adapters.
- `session_liveness::{TokenCipher, attempt_liveness_refresh, EncryptedRefreshToken, LivenessOutcome, LivenessFailure, RevokeCause, TransientCause, CipherError}`.
- `pas_port::{PasAuthPort, PasFailure, MemoryPasAuth, PasRefreshOutcome, pas_refresh, CipherFailure}`.
- `audit::{AuditEvent, AuditSink, NoopAuditSink, MemoryAuditSink, RateLimitedAuditSink, MemoryRateLimiter, RateLimiter, RateLimitKey, VerifyErrorKind, IdTokenFailureKind, compose_id_token_source_id, compose_source_id}`.
- `types::{KeyId, Ppnum, PpnumId, SessionId, UserId}`.
- `Url` — re-export of `url::Url` for consumer ergonomics.
- `test_support::{FakePasServer, TokenExchangeBody, TokenResponse, PasIdTokenVerifier}` — test substrate, gated `feature = "test-support"`.

No Phase 6.1 artifacts remain.

## [0.8.0-beta.1] — 2026-05-07

Phase 11.X.C **additive** pre-release. Surfaces the perimeter
`BearerAuthLayer` mechanism that chat-auth (1st integrator), RCW (2nd),
and CTW (3rd) all carry as near-identical ~600-LOC modules — N=3
evidence justifies the SDK promote per `feedback_deep_modules`. No
breaking changes vs `0.7.1`; existing imports continue to compile
unchanged. The pre-release tag (`-beta.1`) signals that the consumer
migration (Slices 2 / 4 / 5 of 11.X.C) is in flight; the stable `0.8.0`
will land when 11.Y closes the escape-hatch surface
(`oauth::AuthClient` + `PasIdTokenVerifier` `pub(crate)` lockdown).

Trigger: `RFC_2026-05-07_oidc-rp-phase-11x-rcw-ctw-migration.md` §11
(SDK contract lock).

### Added

- **`oidc::axum`** — new module gated by the existing `axum` feature.
  Four public types; consumers import via the explicit
  `pas_external::oidc::axum::*` path (no `oidc::*`-level re-export, so
  the framework dependency stays visible at the import site).
  - **`oidc::axum::AuthProvider<S>`** — generic perimeter port.
    `async fn verify_token(&self, bearer: &str) -> Result<S, VerifyError>`.
    Single async method spans the consumer's full security decision
    (cryptographic verify + substrate liveness lookup) and produces
    the consumer's perimeter session type `S` directly. The generic
    `S` lets each consumer keep its native session shape — chat-auth's
    5-field `AuthSession` (with `account_type`/`scopes`/`audience`
    for OAuth 3rd-party scope-gating) and RCW/CTW's 3-field
    `BearerAuthSession` coexist behind the same SDK Layer with no
    projection step.
  - **`oidc::axum::VerifyError`** — 2-variant enum
    (`Rejected(String)` / `SubstrateTransient(String)`) lifted
    verbatim from RCW/CTW. Layer maps `Rejected` to
    **401 + add-based cookie clearance** and `SubstrateTransient` to
    **503 with cookies preserved**. Richer per-substrate taxonomies
    (chat-auth's break-glass dashboard with `JtiReplayed`,
    `SessionVersionStale`, etc.) collapse here at the SDK boundary
    inside the consumer's `verify_token` impl, mirroring RCW's
    existing `map_verify_err` shape.
  - **`oidc::axum::BearerAuthConfig`** — value struct carrying
    `access_cookie_name: &'static str` and an
    `Arc<dyn Fn(CookieJar) -> CookieJar + Send + Sync>` clearance
    closure. Per-consumer cookie inventory stays consumer-side
    (`__Host-pcs_at` for chat-auth, `__Host-rcw_at` for RCW,
    `__Host-ctw_at` for CTW); the SDK never spells the literal.
    Future fields (e.g. `audit_sink: Option<Arc<dyn AuditSink>>` in
    11.Y) can be added with a `Default` impl — non-breaking.
  - **`oidc::axum::BearerAuthLayer<Sess, P>`** — tower
    [`Layer`](::tower::Layer) implementation. Two generic parameters:
    `Sess` (consumer's perimeter session type) and `P: ?Sized`
    (`AuthProvider<Sess>` impl, supporting both concrete `Arc<MyProvider>`
    and trait-object `Arc<dyn AuthProvider<Sess>>`). Mounts at axum
    HTTP-edge granularity; produces a single, type-safe `Sess` request
    extension. Single perimeter / single `verify_token` call site
    invariant per `feedback_perimeter_auth_layer`.
- **`oidc::axum::MemoryAuthProvider<S>`** (gated `cfg(any(test, feature = "test-support"))`)
  — in-memory test-support adapter mirroring the existing
  `token::MemoryBearerVerifier` / `oidc::MemoryIdTokenVerifier` pattern.
  Lets consumer integration tests and SDK boundary tests share one mock
  shape rather than re-defining a `MockValidator` struct in every file.

### Sources & RFC references

- Authorization-vs-cookie precedence: RFC 6750 §2.1 (header-preferred);
  RFC 9700 §6.3 (browser-context BCP).
- Bearer scheme case-insensitivity: RFC 7235 §2.1 — `Bearer` and
  `bearer` both accepted.
- Cookie name domain-scoping rationale: RFC 6265bis (`__Host-` prefix).
- N=3 evidence threshold: `feedback_deep_modules` (workspace memory) +
  RFC §11.1 audit summary.
- Single-perimeter invariant: `feedback_perimeter_auth_layer` (workspace
  memory) — `verify_token` call site count must remain at 1 across the
  consumer codebase.

### Test coverage

- New SDK boundary suite at `tests/oidc_axum_boundary.rs` (10 cases):
  header_path / cookie_path / header_wins_over_cookie /
  missing_credentials_returns_401_no_clear /
  rejected_returns_401_with_clear /
  substrate_transient_returns_503_no_clear / verbatim_cookie_value /
  empty_token_treated_as_missing / lowercase_bearer_prefix /
  unrelated_cookies_in_jar.
- 30 redundant boundary-test assertions (10 cases × 3 consumer modules)
  collapse into this 1 SDK suite once Slices 2 / 4 / 5 land in chat-auth
  + RCW + CTW. Substrate-specific tests (KVRocks sv-axis; PgPool
  session-row revocation) remain consumer-side — they exercise the
  `AuthProvider` impl, not the Layer.

### Internal — no consumer impact

- `tower = { workspace = true, optional = true }` added to dependencies,
  gated by the existing `axum` feature flag. Workspace-pinned to `0.5`,
  matching `axum 0.8`'s transitive tower major.

## [0.7.1] — 2026-05-08

Additive release that opens the Phase 11 OIDC Relying Party surface to
external consumers. No breaking changes; existing 0.7.0 imports continue
to compile unchanged. Trigger: `RFC_2026-05-15_oidc-rp-integration-phase-11`
(closed) and `RFC_2026-05-07_oidc-rp-phase-11x-rcw-ctw-migration` (RCW
migration depends on these symbols being on crates.io).

### Added

- **`oidc::RelyingParty<S>`** composition root with three lifecycle
  methods: `new`, `start`, `complete`, `refresh`. The phantom-typed
  scope marker `S: ScopePiiReader` (re-exported from `ppoppo-token::id_token::scopes`)
  carries a compile-time bound on which ID-token claims a given consumer
  is permitted to read. A consumer parameterised by `scopes::Profile` cannot
  call `claims.email()` even if `email` is present on the wire.
  - `RelyingParty::new` discovers the OP via `.well-known/openid-configuration`,
    instantiates a JWKS-backed `IdTokenVerifier`, and stores the
    user-provided `OAuthConfig` + `StateStore`.
  - `RelyingParty::start` performs PKCE, mints a `state` nonce, persists
    state via `StateStore`, and returns a `RequestedScope` carrying the
    authorization URL the consumer redirects to.
  - `RelyingParty::complete` exchanges code → tokens, verifies the ID
    token + at_hash + c_hash + nonce, and returns a `Completion<S>`
    that exposes `IdAssertion<S>` (the verified claims) + the access
    token. Verifies all OIDC required claims (iss/aud/exp/nbf/iat/azp).
  - `RelyingParty::refresh` performs RTR token exchange and re-verifies
    the new ID token (when present in the response).
- **`oidc::StateStore` port** — single-method async trait
  (`put`/`take_if_match`) covering CSRF state binding. In-memory
  implementation `InMemoryStateStore` available behind
  `feature = "test-support"`. Production consumers implement against
  KVRocks/Redis/PostgreSQL.
- **`oidc::discovery::{fetch_discovery, Discovery, DiscoveryError}`** —
  `.well-known/openid-configuration` fetcher with structured error
  variants (transport, JSON, schema mismatch). Used internally by
  `RelyingParty::new` and `PasIdTokenVerifier::from_jwks_url`; exposed
  for consumers that need raw discovery for sibling flows.
- **Error types**: `CallbackError`, `RefreshError`, `RelyingPartyInitError`,
  `StartError`. Each is `#[non_exhaustive]` and carries enough variant
  granularity for consumers to distinguish operator-actionable errors
  (network, OP misconfiguration) from user-actionable errors (CSRF
  mismatch, expired state).
- **`oidc::RequestedScope`** — return value of `RelyingParty::start`
  carrying the authorization URL plus the `state` token the consumer
  must persist for `complete` to validate.

### Feature gating

All new symbols are gated behind `feature = "well-known-fetch"`. The
`axum` feature transitively enables this, so consumers using
`features = ["axum"]` get the new surface automatically. Pure `oauth`
or `token` consumers who do *not* need RP flow are not pulled into
the wiremock-tested HTTP discovery path.

### Internal

- Discovery primitive consolidated: `RelyingParty::new` and
  `PasIdTokenVerifier::from_jwks_url` now share `fetch_discovery` so
  the well-known fetch happens once per `RelyingParty` instance, with
  the JWKS URL extracted from the same `Discovery` snapshot.
- 12 boundary tests added against `wiremock` covering: PKCE/state
  round-trip, ID token verification across all 5 OIDC scopes, at_hash
  / c_hash binding, nonce binding, RTR refresh path, expired-state
  rejection, malformed-discovery rejection, JWKS rotation, and the
  cross-scope phantom-type compile-fail family (M45-style — not
  runtime-tested but type-system enforced).

### Not changed

- 1st-party `BearerVerifier` / `PasJwtVerifier` / `MemoryBearerVerifier`
  surface: unchanged from 0.7.0.
- 1st-party `middleware` (Bearer-flow Axum middleware): unchanged.
- Session liveness (`session_liveness::*`) and `TokenCipher`: unchanged.
- Audit (`audit::*`): unchanged.

### Out of scope (explicitly deferred)

- **`BearerAuthLayer` SDK promotion** — the perimeter Layer that consumes
  `RelyingParty`-issued access tokens currently lives chat-auth-internal
  (in the ppoppo monorepo's `services/chat/crates/chat-auth/src/middleware.rs`).
  Promotion to `pas-external::oidc::axum::BearerAuthLayer` is gated on
  N=3 integrator evidence (chat-auth + RCW + CTW); planned for the 0.8.0
  release line per `RFC_2026-05-07_oidc-rp-phase-11x-rcw-ctw-migration`.
  RCW (the first external consumer of `RelyingParty<S>`) self-implements
  its perimeter Layer in 0.7.x — that implementation provides the second
  evidence point.

## [0.7.0] — 2026-05-15

Closes the JWT-adoption RFC (`RFC_2026-05-04_jwt-full-adoption`, Phase 10
arc — OIDC ID Token profile + engine module split + RP middleware +
discovery + session liveness consolidation). The single user-visible
break in this release is the removal of `UserInfo::session_version` from
the public surface — session liveness now flows exclusively through the
access_token's `sv` claim (Phase 10.13.B F6 single-SSOT). All other
0.7 changes are additive and architectural.

### Breaking

- **`UserInfo::session_version` field removed**. The 0.6 SDK exposed
  `session_version: Option<u64>` on the `UserInfo` struct (populated
  from the PAS `/userinfo` response's `session_version` field). 0.7
  removes both the field AND the
  `UserInfoBuilder::with_session_version` builder. Consumers that read
  `info.session_version` no longer compile.
  - **Why**: pre-0.7, `sv` lived in two places — the access_token's
    `sv` claim AND the userinfo response. Two SSOTs invite drift; F6
    audit (Phase 10.13.B) routed *every* SDK consumer (including the
    internal `middleware::sv` validator) through the access_token
    claim and removed the userinfo channel.
  - **Engine surface**: a new `pub(crate) token::jwt::peek_session_version`
    extracts `sv` from a TLS-trusted access_token without going through
    the engine's full verify (the validator is downstream of
    `PasJwtVerifier` so the bytes are already trusted by transport).
    External callers do not need this — the public `BearerVerifier`
    surface already exposes `AuthSession::session_version`.

- **`SvStep::UserInfo` collapsed**. The session-version state machine
  (`session_version::SvStep` enum) lost the `AwaitingUserinfo` variant
  along with its dependencies: `UserinfoFeed` (4 variants),
  `feed_userinfo`, `classify_userinfo`, and 3 `ExpiryCause` userinfo
  branches. Consumers that pattern-matched on the userinfo path
  (none expected — internal API) are affected.

### Improved

- **F6 single-SSOT for session liveness**. Post-0.7, `sv` exists in
  exactly one place at runtime: the access_token claim verified by
  `engine::check_epoch`. This is the only liveness-verification path.
  See [`STANDARDS_SESSION_LIVENESS.md`](0context/STANDARDS_SESSION_LIVENESS.md)
  §S-L6 for the architectural invariant + the reasoning behind dropping
  the userinfo channel.
- **Phase 10.11 RP middleware live since 0.6.5+**:
  `pas_external::oidc::*` exposes `IdTokenVerifier` trait,
  `PasIdTokenVerifier<S>` production adapter, and `IdAssertion<S>`
  with phantom-typed scope narrowing. Consumers integrating
  Login-with-ppoppo via OIDC id_token consume this surface; type system
  prevents reading PII outside the granted scope (M72 structural
  enforcement). Same `Arc<dyn AuditSink>` instance can be passed to
  both `PasJwtVerifier::with_audit` AND `PasIdTokenVerifier::with_audit`
  for unified audit-pipeline wiring.
- **Phase 10.12 discovery + UserInfo canonical**: PAS now serves
  `/.well-known/openid-configuration` (OIDC Discovery 1.0); the
  discovery `claims_supported` is sourced from the engine's
  `EmailProfilePhoneAddress::names()` compile-time SSOT. UserInfo
  endpoint canonicalized to `/userinfo` (was `/oauth/userinfo` —
  pre-launch β2 rename, no backcompat shim). Scope-aware response
  narrowing per OIDC Core §5.4 hardened: 1st-party tokens skip the
  filter; oauth tokens require explicit `claims.scopes` grants
  (no `is_empty()` bypass).
- **Phase 10.13.A positioning**: `STANDARDS_AUTH_PPOPPO §1` invariant
  #1 verbatim "OpenID Provider (OP)" — PAS' primary self-positioning
  per OIDC Core 1.0. Discovery `issuer` field is the
  machine-readable actor identifier (audit log gains no separate
  `actor` field — α3 design call).

### Migration

Most consumers need **no code change**. The breaking removal of
`UserInfo::session_version` only affects SDK callers that explicitly
read the field — and the SDK's own internal `middleware::sv`
validator is the only known reader, which migrated in Phase 10.13.B.

```rust
// before — pas-external 0.6
let info = client.userinfo(&access_token).await?;
let sv = info.session_version;          // ← Option<u64>
if let Some(sv) = sv { ... }

// after — pas-external 0.7
//
// The userinfo response no longer carries session_version. Read sv
// from the access_token's claim instead; the SDK's BearerVerifier
// exposes it on AuthSession after verify:
let session = verifier.verify(&access_token).await?;
let sv = session.session_version();      // ← u64
```

If you operate the access_token verify yourself (rare — most consumers
use `PasJwtVerifier::from_jwks_url`), the engine claim is `sv` (u64).

The `axum` middleware path (RCW/CTW) requires only a Cargo.toml bump
to `pas-external = "0.7"` — the trait surface
(`AccountResolver` / `SessionStore` / `PasAuthPort`) is unchanged.

### Tests

- 154 `pas-external` tests green under
  `--features "test-support well-known-fetch axum"` (unchanged from
  0.6.5+ totals; the F6 cleanup deleted dead test branches but the
  surviving tests synthesize JWS-shaped tokens via
  `token_response_with_sv` helper to feed the new
  `peek_session_version` path).
- 237 `ppoppo-token` engine tests green (unchanged — engine surface
  did not move in 0.7).
- 157 `accounts-api` lib tests green (Phase 10.12 discovery suite +
  UserInfo handler + scope-narrowing tests included).

### Links

- RFC `RFC_2026-05-04_jwt-full-adoption` §6.11.1 closure ([CLOSED 2026-05-15])
- New standard prose: `STANDARDS_JWT_DETAILS_ID_TOKEN_PPOPPO.md`
  (Phase 10.14.A authored end-to-end OIDC id_token profile spec)
- F6 invariant: `STANDARDS_SESSION_LIVENESS.md` §S-L6 (single-SSOT)
- Discovery: `STANDARDS_WELL_KNOWN.md` + Phase 10.12 commit `35037322`
- Phase 10 commit chain: `a2f211f1` (10.0 D1 split) →
  `c4148d9e` (10.10 D2 emission half) → `25a0e60c` (10.11 RP middleware) →
  `35037322` (10.12 discovery + /userinfo) → `b4baba84` (10.13.A
  positioning) → `adf77f3a` (10.13.B F6 sv single-SSOT) → this release.

## [0.6.0] — 2026-05-05

Implements Phase 6.1 of `RFC_2026-05-04_jwt-full-adoption` (D-04 = γ
port-and-adapter, locked 2026-05-05). The SDK retires its bundled
PASETO verification logic and consumes the published JWT engine
(`ppoppo-token` 0.1.0, RFC 9068, EdDSA) through a γ-shaped
[`BearerVerifier`] port. External Developers now verify against a
*format-blind, swap-able* port; the engine is the only place that
knows JWT. PASETO is gone from the dependency tree —
`cargo tree -p pas-external | grep pasetors` returns empty.

### Breaking

- **Token format: PASETO v4.public → JWT (RFC 9068, EdDSA).** Tokens
  issued by PAS post-Phase-6 are JWTs; this SDK verifies them via
  `ppoppo-token::verify` under a TTL-cached JWKS. Re-fetch keys at
  deployment from `/.well-known/jwks.json` (replaces
  `/.well-known/paseto`).
- **SDK surface restructured.** The function-style verifier API
  (`verify_v4_public_access_token`, `verify_v4_with_keyset`,
  `parse_public_key_hex`, `extract_unverified_kid`, `PublicKey`,
  `VerifiedClaims`) is replaced by a port-and-adapter shape:
  - `BearerVerifier` async trait with single `verify(bearer_token)` method
  - `PasJwtVerifier::from_jwks_url(url, expectations)` production adapter
  - `AuthSession` opaque result with typed accessors (`ppnum_id`,
    `ppnum`, `session_id`, `session_version`, `expires_at`)
  - `Expectations { issuer, audience }` held at verifier construction
  - `VerifyError` enum with engine M-codes mapped to boundary variants
  Consumer middleware injects `Arc<dyn BearerVerifier>` instead of
  calling free functions.
- **`KeySet` removed from public surface.** The TTL JWKS fetcher is
  now `pub(crate)` (Finding 2 of the deep-module audit) — consumers
  reach it only through `PasJwtVerifier::from_jwks_url`. Two-step
  construction (build KeySet → pass to verifier) was a leaky
  abstraction; the single-step constructor hides the cache.
- **`WellKnownPasetoDocument` / `WellKnownPasetoKey` /
  `WellKnownKeyStatus` removed.** JWKS shape now lives in
  `ppoppo_token::Jwks` (RFC 7517). Consumers that hand-built keysets
  use `Jwks::from_ed25519_keys(&[...]).into_key_set()` (engine SSOT).
- **`axum` feature now requires `token`.** The middleware path
  consumes `ppoppo_token::SV_CACHE_TTL` and `sv_cache_key()` directly
  (Finding G — type-enforced shared-cache contract). Adopters
  enabling `axum` already had `token` in default features, so no
  Cargo.toml change is required.

### New

- **`BearerVerifier` port** (`feature = "token"`) — the contract
  consumer middleware injects.
- **`PasJwtVerifier` production adapter** (`feature = "well-known-fetch"`)
  — verifies PAS-issued JWTs against a TTL-cached JWKS.
- **`MemoryBearerVerifier` test-support adapter**
  (`feature = "test-support"`) — in-memory port impl for consumer
  integration tests. Insert + verify round-trip without a live PAS
  process.
- **First external consumer of `ppoppo-token` 0.1.0** — the engine
  is now reachable via `cargo add ppoppo-token` for adopters that
  want a different SDK shape (e.g., a non-Axum framework).

### Migration

```rust
// before — pas-external 0.5
use pas_external::{verify_v4_public_access_token, KeySet, VerifiedClaims};

let keyset = KeySet::fetch("https://accounts.ppoppo.com/.well-known/paseto").await?;
let claims: VerifiedClaims = keyset
    .verify(token, "accounts.ppoppo.com", "my-client-id")
    .await?;
println!("ppnum_id={}", claims.sub().unwrap_or(""));

// after — pas-external 0.6
use pas_external::{BearerVerifier, Expectations, PasJwtVerifier};

let verifier = PasJwtVerifier::from_jwks_url(
    "https://accounts.ppoppo.com/.well-known/jwks.json",
    Expectations::new("accounts.ppoppo.com", "my-client-id"),
)
.await?;
let session = verifier.verify(token).await?;
println!("ppnum_id={}", session.ppnum_id());
```

For Axum middleware consumers, the wiring path through `PasAuth` /
`SessionValidator` / `AccountResolver` / `SessionStore` is **unchanged**
— the middleware internally upgraded from PASETO to JWT, but the public
trait surface is identical.

### Deep-module audit findings (applied)

- **Finding 1**: `Expectations` moved to verifier construction; `verify`
  takes only `bearer_token`.
- **Finding 2**: `KeySet` hidden as `pub(crate)`; `from_jwks_url` is
  the single public entry point.
- **Finding 3**: One constructor (`from_jwks_url`) replaces the
  `new` + `with_config` builder anti-pattern.
- **Finding 4**: `AuthSession::into_inner` escape hatch is NOT shipped
  — pre-flight grep audit confirmed RCW + CTW middleware never
  accesses raw claims. All needed fields surface as typed accessors.
- **Finding G**: `sv_cache_key` + `SV_CACHE_TTL` re-exported from
  `ppoppo-token` (engine SSOT) — drift between PAS, PCS, and SDK is
  now a compile-time ripple, not silent.

## [0.5.0] — 2026-05-01

Implements RFC_2026-05-01 (`deepen-session-validation-pipeline`) step 3.
Public-API rename: the SDK now speaks "session validator" instead of
"sv-aware session resolver" — the latter exposed an implementation
pattern in the public name.

### Breaking

- **Type rename: `SvAwareSessionResolver` → `SessionValidator`.** Same
  shape (`<S, P, B = MemorySvBackend>`), same semantics. Migration is a
  global find-and-replace.
- **Method rename: `SessionValidator::resolve(&jar)` →
  `SessionValidator::validate(&jar)`.** The base `SessionResolver` keeps
  its `.resolve(&jar)` method — the rename clarifies the distinction
  between "look up a session" (base resolver) and "validate a session
  including sv enforcement + refresh" (validator).
- **Accessor rename: `PasAuth::resolver()` →
  `PasAuth::session_validator()`.** Likewise `resolver_with_backend(b)`
  → `session_validator_with_backend(b)`.
- **File rename: `middleware/sv_aware.rs` → `middleware/validator.rs`.**
  Internal but visible in `cargo doc` source links and stack traces.
- **Test rename: `tests/sv_aware_refresh.rs` →
  `tests/session_validator_refresh.rs`.** Same 11 tests, unchanged.

No other behavior changes — this release is a pure rename. Behavioral
changes were front-loaded in 0.3.0 (cache fold) and 0.4.0 (driver fold).

### Migration

```rust
// before
use pas_external::middleware::SvAwareSessionResolver;
let resolver = pas_auth.resolver();
match resolver.resolve(&jar).await? { ... }

// after
use pas_external::middleware::SessionValidator;
let validator = pas_auth.session_validator();
match validator.validate(&jar).await? { ... }
```

Custom-backend consumers:

```rust
- let resolver = pas_auth.resolver_with_backend(MyRedisCache::new(client));
+ let validator = pas_auth.session_validator_with_backend(MyRedisCache::new(client));
```

## [0.4.0] — 2026-05-01

Implements RFC_2026-05-01 (`deepen-session-validation-pipeline`) step 2.
Internal cache surface is reduced to a single port; the strategy
wrapper that owned the spec contract folds into the SDK's sync state
machine driver.

### Breaking

- **`SvCacheBackend` trait renamed to `SvCachePort`.** Vocabulary
  alignment with the ports & adapters pattern the SDK now follows
  internally. Method shape unchanged (`load`, `store`). Migration is
  mechanical: `impl SvCacheBackend for MyCache` → `impl SvCachePort for
  MyCache`.
- **`SvCachePolicy<B>` removed from the public API.** It was a thin
  wrapper that owned the `sv:` namespace prefix and the spec-fixed 60 s
  TTL. Both concerns now live in the SDK-internal driver
  (`middleware::sv::adapter`); the port stays namespace- and
  TTL-agnostic. Consumers who only used `PasAuth::resolver()` /
  `PasAuth::resolver_with_backend(backend)` need no changes — the
  argument type changed from `SvCachePolicy<B>` to `B: SvCachePort`,
  but the `with_backend` call accepts the same value.
- **`SvAwareSessionResolver::new` takes `Arc<B>` instead of
  `SvCachePolicy<B>`.** Direct callers of the constructor (rare —
  primarily SDK boundary tests) wrap the backend in `Arc::new`
  themselves.
- **`CheckResult` no longer public.** It was only a return type of
  `SvCachePolicy::check`, which is now internal. Operators see the
  same `sv_cache_outcome` field on the resolver's tracing span,
  unchanged.

### Internal

- New module `middleware::sv` (`pub(super)`) hosts the synchronous
  `SvCore` state machine and the async driver loop introduced in step 1.
  Step 2 inlines the cache-strategy logic into the driver, so the
  former `SvCachePolicy::check` and `SvCachePolicy::record` are now
  visible as `classify_cache` and the `RecordCache` arm of the driver.
- `SV_CACHE_KEY_PREFIX` and `SV_CACHE_TTL` move from `pub(crate)` to
  `pub(super)`. They remain SDK-owned constants; sister crates
  (`ppoppo-token`, PCS chat-auth) hold their own private copy.

### Migration

```rust
// 90% case (default backend) — no changes needed.
let resolver = pas_auth.resolver();

// Custom backend (multi-pod) — rename the trait impl:
- impl SvCacheBackend for MyRedisCache { ... }
+ impl SvCachePort for MyRedisCache { ... }
let resolver = pas_auth.resolver_with_backend(MyRedisCache::new(client));
```

For SDK boundary tests calling `SvAwareSessionResolver::new` directly:

```rust
- let policy = SvCachePolicy::with_backend(backend);
- SvAwareSessionResolver::new(base, store, pas, policy, cipher)
+ SvAwareSessionResolver::new(base, store, pas, Arc::new(backend), cipher)
```

## [0.3.0] — 2026-05-01

Implements RFC_2026-05-01 (`fold-sv-cache-into-policy`). The cache
extension surface — previously split across the public
`SessionVersionCache` trait, `MemorySessionVersionCache`,
`SV_CACHE_KEY_PREFIX`, and `SV_CACHE_TTL` — is folded into a single
`SvCachePolicy` strategy. The previously-violated TTL invariant
(`SessionVersionCache::set` accepted a `Duration` that the in-memory
impl ignored) is now structurally enforced: the policy owns the
spec-fixed 60 s TTL and pushes it down to backends with native expiry
(Redis, KVRocks).

### Breaking

- **`session_version` module removed.** Replaced by
  `middleware::sv_cache`. The old top-level module path is gone; all
  cache types are re-exported only from `pas_external::middleware`.
- **`SessionVersionCache` trait removed.** Replaced by `SvCacheBackend`.
  Migration is mechanical:
  - `impl SessionVersionCache for MyCache` → `impl SvCacheBackend for MyCache`
  - Method renames: `get` → `load`, `set` → `store`
  - The `_ttl: Duration` parameter on `set` was previously ignored by
    in-memory impls; on `store` it must now be honored (Redis `SETEX`,
    KVRocks TTL, etc).
- **`MemorySessionVersionCache` renamed to `MemorySvBackend`.** The new
  name signals it's a substrate, not a protocol. Same Arc-internal
  shape, same 10 000-entry FIFO cap.
- **`SV_CACHE_KEY_PREFIX` and `SV_CACHE_TTL` are no longer public.**
  Consumers writing a custom backend never need them — the policy is
  the only public reader. Sister crates that share the namespace
  (`ppoppo-token`, PCS chat-auth) hold their own private const, same
  as today.
- **`SvAwareSessionResolver` generic count: `<S, C, P>` → `<S, P, B = MemorySvBackend>`.**
  The cache concern collapsed into a single defaulted backend. 90 % of
  consumers (single-pod, default cache) drop the `<C>` parameter from
  every type alias and never type the new `<B>`.
- **`PasAuth::resolver_with_cache(cache)` renamed to
  `resolver_with_backend(backend)`.** Same signature shape; takes
  anything implementing `SvCacheBackend` and wraps it in
  `SvCachePolicy::with_backend`.
- **`PasAuth::default_cache: Arc<MemorySessionVersionCache>` field
  renamed to `default_policy: SvCachePolicy<MemorySvBackend>`.**
  Internal field; affects only consumers reaching past the public
  API.

### Added

- `middleware::SvCachePolicy<B>` — strategy object owning the
  `sv:{ppnum_id}` namespace and the spec-fixed 60 s TTL. Single
  injection point for all cache concerns the resolver cares about.
- `middleware::SvCacheBackend` — narrow substrate trait (`load`,
  `store`). Honest about its TTL contract: backends with native expiry
  must apply the `Duration` the policy passes.
- `middleware::CheckResult` — three-arm result (`Fresh | Stale |
  Unknown`) now recorded on the resolver's tracing span as
  `sv_cache_outcome` so operators can distinguish "break-glass
  converged across pods" from "cold cache" without changing dispatch.
- `middleware::MemorySvBackend` — default per-pod in-memory backend.
  Now honors caller-provided TTL (regression-tested).

### Migration

For consumers using the default cache (most), the diff is two lines:

```rust
// before
use pas_external::MemorySessionVersionCache;
type MyResolver = SvAwareSessionResolver<MyStore, MemorySessionVersionCache, AuthClient>;

// after
type MyResolver = SvAwareSessionResolver<MyStore, AuthClient>;
```

For consumers with a custom Redis/KVRocks adapter, rename the trait
impl, drop the now-meaningful TTL ignore, and switch
`PasAuth::resolver_with_cache(...)` → `resolver_with_backend(...)`.

## [0.2.0] — 2026-05-01

Implements RFC_2026-05-01 (deepen S-L6 path through `pas_refresh`,
delete `RefreshTokenResolver`). The S-L6 sv-aware refresh path now
routes through the same `pas_refresh` deep core that S-L3 already
uses — decrypt happens once in the SDK for both paths, eliminating
the consumer-owned decrypt step.

### Breaking

- **`RefreshTokenResolver` trait removed.** Its single method folds
  into a new `SessionStore::get_refresh_ciphertext` returning
  `Option<EncryptedRefreshToken>` (the **encrypted** ciphertext, not
  plaintext). Consumers no longer decrypt; the SDK does it via
  `pas_refresh`. Migrate by deleting `impl RefreshTokenResolver for
  YourAdapter` and adding `get_refresh_ciphertext` to your existing
  `impl SessionStore for YourAdapter` block.
- **`pas_refresh` signature tightened** from `(cipher, port,
  ciphertext: &str)` to `(cipher, port, ct: &EncryptedRefreshToken)`.
  Plaintext-as-`&str` cannot accidentally flow into the SDK's decrypt
  site at any call site.
- **`attempt_liveness_refresh` signature tightened** identically:
  third argument is now `&EncryptedRefreshToken`. Wrap the stored
  column value via `EncryptedRefreshToken::from_stored(string)` before
  calling.
- **`SvAwareSessionResolver`: 4 generics → 3.** The `R:
  RefreshTokenResolver` parameter is gone. The public constructor
  gains a `cipher: Option<Arc<TokenCipher>>` parameter so the resolver
  can call `pas_refresh` directly. `None` is a soft misconfiguration
  (ciphertext present + no cipher = `Expired` with logged error).
- **`PasAuth`: 3 generics → 2.** The `R` parameter is removed.
  `PasAuth::new(...)` takes 3 arguments instead of 4 (drop the
  `refresh_resolver` argument).

### Migration

Per-consumer migration is mechanical and net-negative LOC:

1. `Cargo.toml`: bump `pas-external` to `"0.2"`.
2. Adapter file: delete `impl RefreshTokenResolver for ...` block
   (includes the consumer's `cipher.decrypt(...)` call).
3. Adapter file: add `get_refresh_ciphertext` method to existing
   `impl SessionStore for ...` block. Read the
   `refresh_token_ciphertext` column and wrap via
   `EncryptedRefreshToken::from_stored(...)`. **No decryption.**
4. Wiring code: drop the 4th argument from `PasAuth::new(...)`.
5. If a `TokenCipher` field on the adapter was held only to support
   the deleted `RefreshTokenResolver` impl, remove it. Keep the cipher
   passed to `PasAuthConfig::with_refresh_token_cipher`.
6. Test code that constructed `SvAwareSessionResolver` directly:
   the `new` signature is now `(base, store, pas, cache, cipher:
   Option<Arc<TokenCipher>>)` — pass `Some(Arc::new(cipher))` for
   real-refresh scenarios, `None` to exercise the misconfig path.

The SDK `tests/sv_aware_refresh.rs` ships 10 boundary tests covering
the S-L6 fail-CLOSED invariants (cipher failure / PAS 4xx / 5xx /
transport / userinfo session_version=None / userinfo 5xx / update_sv
DB failure / happy path / no ciphertext / no cipher configured).

## [0.1.0] — 2026-04-30

First public release after the pre-1.0 reset. Equivalent in scope to
the (yanked) `5.0.0` development line. Implements RFC_2026-04-30
(`PasAuthPort` port boundary at the PAS network seam) — see
`0context/STANDARDS_SESSION_LIVENESS.md` for the consumer-facing
contract.

### Breaking

- **`AuthClient::refresh_token` removed.** Use
  `<AuthClient as PasAuthPort>::refresh(&rt).await` — add
  `use pas_external::pas_port::PasAuthPort;` at the call site. Return
  type changes from `Result<TokenResponse, Error>` to
  `Result<TokenResponse, PasFailure>`.
- **`AuthClient::get_user_info` removed.** Use
  `<AuthClient as PasAuthPort>::userinfo(&at).await`. Same import;
  same return-type shift.
- **`classify_refresh_error` free function removed.** Its semantics
  live in `AuthClient::send_classified` (produces `PasFailure`) and
  the new `pas_refresh` deep core (translates `PasFailure` into
  `PasRefreshOutcome`). `PasFailure` is now the unified vocabulary for
  both the S-L3 fail-open path and the S-L6 fail-closed path.
- **`TransientCause::Transport` and `TransientCause::Unknown` removed.**
  Both are merged into `PasServerError`. Detail strings remain in the
  `detail` field; the cause-level distinction was a no-op for S-L3
  policy (every transient flavor serves cache).
- **`SvAwareSessionResolver<S, R, C>` is now
  `SvAwareSessionResolver<S, R, C, P>`.** The new `P: PasAuthPort`
  parameter allows test code to substitute `MemoryPasAuth`. Production
  callers of `PasAuth::resolver()` see no change — the default
  `P = AuthClient` is inferred. Test code that constructed
  `SvAwareSessionResolver` directly uses the new public
  `SvAwareSessionResolver::new(base, store, refresh_resolver, pas,
  cache)` constructor.

### Added

- **`pas_port` module** — `PasAuthPort` trait (`refresh` / `userinfo`),
  `PasFailure` enum (`Rejected` / `ServerError` / `Transport`), and
  `pas_refresh` deep core (decrypt → call PAS → translate failure).
  Both the S-L3 liveness path and the S-L6 sv-enforcement path compose
  this primitive.
- **`MemoryPasAuth` test adapter** — scriptable in-process PAS stub
  (`expect_refresh` / `expect_userinfo`). Exposed under the new
  `test-support` Cargo feature; zero runtime cost when disabled.
- **`test-support` Cargo feature** — re-exports `MemoryPasAuth` for
  downstream consumer integration tests. Add
  `pas-external = { version = "0.1", features = ["test-support"] }` in
  `[dev-dependencies]`.
- **`SvAwareSessionResolver::new` is now `pub`** (was `pub(super)`) —
  required for test code that substitutes `MemoryPasAuth`.
- **`SessionResolver::new` is now `pub`** — same rationale.
- **`PasAuth<S, R, P = AuthClient>`** — `P` default type parameter
  preserves source compatibility for all existing `PasAuth` use sites.

### Changed

- **`attempt_liveness_refresh` is now generic over `P: PasAuthPort`**
  (was concrete `&AuthClient`). Existing call sites compile unchanged;
  `&AuthClient` satisfies the bound.

### Migration

See `STANDARDS_SESSION_LIVENESS.md §10` (§ "v4.0.x → v0.1.0 consumer
migration checklist") for the full step-by-step. Also see the design
RFC: `0context/RFC_2026-04-30_deepen-pas-refresh-port.md`.

Production migration is typically **0 LOC** — `PasAuth::resolver()`
keeps inferring `P = AuthClient`. Test code touching the resolver
directly gets a one-line constructor update.

---

# Yanked versions (historical)

The entries below describe versions `1.0.1` through `4.0.2`, all yanked
from crates.io on 2026-04-30 as part of the pre-1.0 reset. They are
preserved for git archaeology only — new adopters do not need to read
them.

## [4.0.2] — 2026-04-30 (yanked)

### Fixed

- **(security) Fail-CLOSED when `update_sv` persistence fails after a
  refresh.** Previously, if the consumer's `SessionStore::update_sv`
  returned an error (DB outage, serialization conflict), the resolver
  logged a warning and *still admitted* the session — backed by the
  in-memory cache that had just been written with the new `sv`. That
  left the cache and the durable store divergent: subsequent requests
  on the same pod kept admitting based on the in-memory `sv`, while a
  pod restart or eviction reverted to the stale persisted `sv`. Multi-
  pod deployments could disagree indefinitely. Resolved by returning
  `SessionResolution::Expired` on `update_sv` error and *not* writing
  the cache, so the next request retries persistence.
- **(security) Fail-CLOSED when post-refresh userinfo returns
  `session_version=None` on a Human session.** A Human session reaches
  the refresh path only because `session.sv()` is `Some(_)`. If PAS then
  responds with `session_version=None`, that is anomalous (PAS regression,
  dev/prod skew, proxy mangling), not the documented AI-agent admit
  case. Previously the resolver admitted the **stale** session, silently
  bypassing the very check this module exists to perform. Now logs at
  `error` level and returns `Expired`.
- **`PasAuth::resolver()` now returns resolvers backed by a single
  shared `MemorySessionVersionCache`.** Previously each call constructed
  a new cache, so layered Axum setups that built resolvers per-route
  saw disjoint caches — a break-glass refresh on one cache wouldn't be
  visible to requests routed through another. The cache is now
  constructed once in `PasAuth::new` and shared via `Arc::clone`.
  `resolver_with_cache` is unchanged (callers continue to provide their
  own substrate).
- **`MemorySessionVersionCache` is now bounded at 10 000 entries.**
  Previously the in-memory cache had no cap and only evicted lazily on
  read. Long-lived consumer pods leaked one entry per unique
  `ppnum_id` ever resolved (~80 bytes each). On `set`, when the cache
  is at cap and the key is new, the cache first prunes expired entries,
  then evicts the single oldest entry (FIFO by write time). Under the
  fixed 60 s TTL, FIFO is effectively LRU — entries don't live long
  enough for hot/cold patterns to develop. Consumers needing larger
  caps should plug in their own substrate via `resolver_with_cache`.

### Documentation

- Fixed stale rustdoc references to removed types: `HttpUserInfoFetcher`
  in `oauth.rs`, the free function `validate_sv` in `token.rs`, and
  `SessionVersionFetcher` in `Cargo.toml` comments. All were superseded
  in 4.0.0 by `SvAwareSessionResolver`; doc-comments now point there.
- Updated the `middleware` Quick Start example to the 4-argument
  `PasAuth::new` signature (was: 3-argument, missing
  `refresh_resolver`).
- Updated the `SessionStore` impl example to include the
  `update_sv` method required since 4.0.0 (was: only `create` / `find` /
  `delete`).

### Backward compatibility

This is a **patch** release: no public API changes. The behavioral
changes (fail-closed on two anomalous paths, shared cache across
`resolver()` calls, bounded cache) all replace prior buggy behavior
with the documented contract. Consumers should observe **fewer**
spurious admissions during partial outages and a strict memory ceiling
on the in-memory cache; no consumer code changes are required.

### Tests

- `memory_cache_bounded_by_max_entries` — verifies cap enforcement
  after inserting `MAX_ENTRIES + 100` unique keys, plus that the
  most-recently-written entry survives FIFO eviction.

## [4.0.1] — 2026-04-30 (yanked)

### Fixed

- **`Ppnum` validation aligned with PAS DB CHECK constraint.** Previously
  `Ppnum::try_from` rejected any value not matching `len() == 11 &&
  starts_with("777")` — but the actual DB constraint is just
  `^[0-9]{11,}$`. The hardcoded `"777"` prefix and fixed `11`-digit
  length were never part of the contract: prefix is band-allocated
  (current canonical seed `100`, e.g. `123-1234-5678`) and length is
  variable (11 = independent, 15/19/... = dependent sub-agent
  hierarchy, +4 digits per nesting level). Production accounts with
  `100`-band ppnums or sub-agent ppnums could not authenticate via
  consumer apps using this SDK.
- **Constitution Principle III alignment.** Leading digits carry no
  semantic meaning — class is decided by `ppnums.entity_type` /
  `ppnums.number_class`, never by prefix. SDK no longer validates
  prefix.

### Backward compatibility

This is a **patch** release: every input that was accepted under
4.0.0 is still accepted under 4.0.1. The fix only widens the accepted
set (previously-rejected `100`-band and `15`-digit dependent ppnums
now pass), so no consumer code changes are required. SemVer-wise this
is a *patch* (bug fix to match the documented contract), not a *minor*
(new functionality).

### Tests

- `valid_ppnum_independent_11_digits` — 100/777 prefix, edge values
- `valid_ppnum_dependent_variable_length` — 15/19/23-digit sub-agents
- `invalid_ppnum_too_short` — `<11` digit rejection
- `invalid_ppnum_non_digits` — letters, hyphens (display form), spaces
- `invalid_ppnum_wrong_prefix` (removed — no longer applicable)

## [4.0.0] — 2026-04-25 (yanked)

### Breaking

- **`SessionStore::AuthContext` now requires the new `SvAware` trait.**
  Implementers must expose `ppnum_id() -> &str` and `sv() -> Option<i64>`
  on their auth context type. The bound is what lets the SDK enforce
  PASETO `sv`-claim invalidation per
  [`STANDARDS_AUTH_INVALIDATION §5`](https://github.com/hakchin/ppoppo).
- **`SessionStore` gains a fourth method, `update_sv(session_id, new_sv)`.**
  Called by `SvAwareSessionResolver` after a refresh that picks up a
  newer `sv` from PAS. Persist the updated value alongside the session
  row.
- **`PasAuth::new` takes a fourth argument: `refresh_resolver: Arc<R>`**
  where `R: RefreshTokenResolver`. The new trait is a small lookup that
  returns the **plaintext** PAS refresh token for a given session id —
  consumers typically read the encrypted ciphertext from their session
  table and decrypt with the same `TokenCipher` they passed to
  `PasAuthConfig::with_refresh_token_cipher`. See the trait docs for a
  complete example.
- **`PasAuth::resolver()` now returns `SvAwareSessionResolver` instead of
  the bare `SessionResolver`.** Consumers get sv enforcement
  *automatically* — no manual wrapping. The new resolver intercepts
  every authenticated request, compares the session's stored `sv` to
  a 60 s consumer-local cache, and refreshes via `/token` on miss /
  stale.
- **The free function `validate_sv` is removed.** Its API
  (per-request bearer token in hand) was incompatible with the
  cookie-session middleware pattern. Replacement: just use
  `PasAuth::resolver()` — sv enforcement is now built in.
  `SessionVersionCache`, `MemorySessionVersionCache`,
  `SV_CACHE_KEY_PREFIX`, and `SV_CACHE_TTL` are still exported for
  consumers that want to inject a custom cache substrate via
  `PasAuth::resolver_with_cache`.
- **`NewSession` gains a `sv: Option<i64>` field**, populated by the
  OAuth callback from `UserInfo::session_version`. Consumers must
  persist this alongside the session row so the resolver can
  enforce sv on subsequent requests. DEV_AUTH sessions and AI-agent
  tokens carry `None` and bypass sv enforcement (spec §4.2.1).

### Added

- `SvAware` supertrait — exposes `ppnum_id` + `sv` on the auth context.
- `RefreshTokenResolver` trait — consumer-provided plaintext refresh
  token lookup for the resolver's refresh-and-recheck path.
- `SvAwareSessionResolver<S, R, C>` — the new default resolver. Wraps
  the base `SessionResolver` with sv enforcement. Ships a cache-hit
  fast path (zero PAS round-trips) and a cache-miss / stale path
  (one `/token` + one `/userinfo` round-trip).
- `PasAuth::resolver_with_cache(cache)` — opt-in for consumers that
  want a shared cache substrate (Redis, KVRocks) so a break-glass on
  PAS converges across pods within network RTT instead of per-pod
  60 s TTL.

### Migration (3.1.0 → 4.0.0)

For each consumer (RCW, CTW, third-party):

1. Add a `sv BIGINT NULL` column to the session storage table
   (`scrcall.user_sessions` / `scctime.user_sessions` /
   equivalent). Existing rows get `NULL` and will be populated by
   the next refresh.
2. Add `pub sv: Option<i64>` to your session domain type and
   populate it in your `SessionStore::create` impl from
   `NewSession::sv`.
3. Add `pub ppnum_id: String` (or equivalent — must match PAS's
   `sub` claim ULID) to your `AuthContext` type if you didn't have
   it already.
4. `impl SvAware for AuthContext`.
5. Implement `SessionStore::update_sv` — straightforward UPDATE on
   the session row.
6. Implement `RefreshTokenResolver` for your adapter — typically a
   `find_by_id` + decrypt with the existing `TokenCipher`.
7. Update the `PasAuth::new(...)` call site to pass the new
   `Arc<RefreshResolver>`.
8. Remove any direct `validate_sv(...)` wrapping in your auth
   middleware — `PasAuth::resolver()` now handles it.

Per-consumer migration is ~80–100 LOC, mechanical. The compiler
guides every step via the new trait bounds.

## [3.1.0] — 2026-04-25 (yanked)

### Added

- **#005 break-glass `sv` claim support.** New `session_version` module
  (feature: `oauth`) ships the validator plumbing that PAS's break-glass
  recovery flow (spec 005) relies on downstream. Without this, a token
  stolen before a break-glass would remain valid for its full 1-hour TTL.
  - `SessionVersionCache` trait — abstracts the 60 s `sv:{ppnum_id}` cache
    so consumers can plug in KVRocks / Redis / in-memory.
  - `MemorySessionVersionCache` — default in-memory impl
    (`tokio::sync::RwLock<HashMap>`); suitable for single-pod consumers or
    consumers without a shared cache substrate.
  - `SessionVersionFetcher` trait — cache-miss source.
    `HttpUserInfoFetcher` is the default impl; calls
    [`AuthClient::get_user_info`] and reads the new
    `UserInfo.session_version` field.
  - `validate_sv(token_sv, ppnum_id, bearer_token, cache, fetcher)` — the
    entry point consumers wrap around their existing
    `verify_v4_public_access_token` call. Legacy tokens (no `sv` claim)
    admit unconditionally (R6 backwards-compat); stale tokens return
    `ValidateSvError::Stale`; fetch failure on cache miss returns
    `ValidateSvError::Transient` (fail-closed — consumers should reject
    so a DB/network blip can't silently admit a revoked token).
- `VerifiedClaims::session_version()` and `VerifiedClaims::magic_link_id()`
  getters on the existing `token` module (feature: `token`). The `mlt`
  claim is PAS-internal, exposed for SDK-consumer introspection /
  audit/debug purposes only.
- `UserInfo.session_version: Option<i64>` — populated by PAS for
  Human-entity tokens; `None` for AI agents.

### Changed

- The `oauth` feature now transitively pulls in `tokio` (sync) and
  `async-trait` because the new `session_version` module needs them.
  Consumers that already enable `oauth` see no change in their feature
  graph; consumers that enable only `token` are unaffected.

### Dependencies

- Added optional `async-trait = "0.1"`, gated on the `oauth` feature.
- `tokio` (previously only used by `well-known-fetch`) is now also used
  by `oauth` via `session_version`.

### Compatibility

Additive, no breaking changes. `UserInfo` already used `#[non_exhaustive]`
so adding `session_version` is SemVer-minor-safe; the field's
`#[serde(default)]` means decoding older userinfo responses (or responses
from PAS instances pre-#005) continues to work.

## [3.0.0] and earlier (yanked)

See git log.
