v0.1.12 — ADR-016 stable auth-failure wire surface
Changed — wire surface tightening (ADR-016)
Transport auth-failure wire surface is now stable and deliberately
narrower than prior releases. No public A2A-protocol API changes;
the changes below affect only the 401/403 responses produced by
turul-a2a-auth middleware and by any adopter-supplied middleware
that constructs MiddlewareError directly.
- 401/403 JSON body changed from
{"error": {"code": <status>, "message": "<Debug of internal enum>"}}
to{"error": "<kind_string>"}where<kind_string>is one of
missing_credential,invalid_token,invalid_api_key,
empty_principal,insufficient_scope. The previous body text was
format!("{err:?}")of an internal enum — not a stable contract.
Adopters pattern-matching on the oldmessagetext need to read the
errorstring instead. - Bearer
WWW-Authenticatechanged from
Bearer realm="a2a", error="invalid_token", error_description="<validator internals>"
toBearer realm="a2a", error="<rfc6750_code>". Theerror=code
derives fromAuthFailureKindper RFC 6750 §3 and can only be
invalid_request,invalid_token, orinsufficient_scope.
error_descriptionis omitted intentionally — it previously leaked
validator internals (JWKS URLs, jsonwebtoken errors, token
fragments). - API-key failures never emit
WWW-Authenticate(no canonical
non-Bearer challenge vocabulary). RequestContext::Debugno longer derives — manual impl redacts
bearer_token, redacts everyheadersvalue that isn't in a fixed
safe allowlist (Content-Type,Content-Length,Accept,
User-Agent,Host), and printsextensionskeys only. Header
names remain visible. Any adopter using{:?}to debug auth state
sees less — readctx.bearer_token,ctx.identity.owner(), etc.
explicitly.AuthIdentity::Debugno longer derives —Authenticatedvariant
showsownerbut redactsclaims.
Changed — error type shape
MiddlewareErrorvariants changed to carryAuthFailureKind
instead of ad-hocString/ status + www_authenticate fields:Unauthenticated(String)→Unauthenticated(AuthFailureKind)HttpChallenge { status, www_authenticate }→HttpChallenge(AuthFailureKind)Forbidden(String)→Forbidden(AuthFailureKind)Internal(String)unchanged.
- Adopter middleware constructing these variants directly needs
trivial translation.matches!(err, MiddlewareError::Unauthenticated(_))
patterns keep working. AnyOfMiddlewareno longer concatenates childWWW-Authenticate
values; the highest-precedence selected error's kind drives the
single emitted header at the transport layer.
Added
turul_a2a::middleware::AuthFailureKind— new public enum,
#[non_exhaustive]. Methods:body_string()→ stable wire string;
bearer_rfc6750_code()→ optional RFC 6750error=code.turul_a2a::middleware::MiddlewareError::kind()→Option<AuthFailureKind>.turul_a2a_auth::RedactedApiKeyLookup— first-partyApiKeyLookup
reference implementation with a redactedDebugimpl that never
emits key material. Adopters can use it directly or treat it as a
template for backend-specific lookups.- Type-level guard: compile-fail (via
static_assertions) that
ApiKeyMiddleware,BearerMiddleware, andStaticApiKeyLookupdo
not implementDebug. Guards against accidental
#[derive(Debug)]leaking credential material. - New test suite
crates/turul-a2a/tests/auth_wire_tests.rs—
six E2E assertions on body + header shape for every
AuthFailureKind/ variant combination (ADR-016 §4). - Unit tests covering
AuthFailureKindmapping,RequestContext::Debug
redaction (bearer token, sensitive headers, allowlist passthrough,
extensions), andAuthIdentity::Debugredaction.
Migration
For most adopters: no code changes needed. turul-a2a-auth's
BearerMiddleware and ApiKeyMiddleware continue to work and now
emit the cleaner wire shape automatically.
For adopters who wrote their own A2aMiddleware impls:
- Replace
MiddlewareError::Unauthenticated("message")with
MiddlewareError::Unauthenticated(AuthFailureKind::<appropriate>). - Replace
MiddlewareError::HttpChallenge { status, www_authenticate }
withMiddlewareError::HttpChallenge(AuthFailureKind::<appropriate>). - Replace
MiddlewareError::Forbidden("message")with
MiddlewareError::Forbidden(AuthFailureKind::InsufficientScope)
(or whatever kind fits). MiddlewareError::Internal(String)is unchanged.
For adopters parsing 401/403 response bodies: switch from reading
body.error.code / body.error.message to body.error (string).