Expand description
M48 + M49 — verify-failure audit emission port + per-source rate limiter (RFC_2026-05-04_jwt-full-adoption Phase 9).
── Why pas-external owns the port, not the schema (β1) ─────────────────
AuditSink is purely abstract. pas-external ships only:
NoopAuditSink— explicit “I don’t want audit emission” choiceMemoryAuditSink—test-support-gated adapter for boundary verification (downstream consumers’ integration tests)
Production adapters (chat-auth in PCS, future RCW/CTW middleware)
live in their own crates and decide their own persistence schema
(Postgres table, tracing-subscriber piping into Cloud Logging, etc).
The SDK does not bake in sqlx or any specific schema — schema
decisions belong to whichever service operates the audit pipeline.
See super::token::port::BearerVerifier for the matching
port-and-adapter precedent (D-04 γ, locked 2026-05-05).
── Why composition, not orchestration (refinement #1) ─────────────────
[super::token::PasJwtVerifier] holds ONE port (Arc<dyn AuditSink>),
not two. Rate-limiting is a property of the sink, expressed by
wrapping any sink in a future RateLimitedAuditSink<S, L> (Phase
9.C). This matches epoch_revocation’s deep-module note:
composition lives in the adapter layer; the engine sees a single
port. Future stacking is free (BatchedAuditSink,
AsyncSpawnAuditSink, etc).
── Failure-mode contract — non-blocking ────────────────────────────────
AuditSink::record_failure returns () (no Result). M48 is
observability, NOT auth-flow critical. Adapters log internal substrate
failures via tracing::error! and continue. The verify hot path
MUST NEVER degrade because audit persistence failed; this contract
is enforced at the trait surface (no error to bubble in the first
place). Callers needing a Result for instrumentation can wrap in a
private struct that records the result internally.
── SLA contract ────────────────────────────────────────────────────────
Implementations SHOULD return within 10ms. Heavier work (HTTP
roundtrip, batch flush, retry) MUST be spawned onto a background
task so the verify hot path is not blocked. The &self (not
&mut self) + Send + Sync bounds let one verifier emit
concurrently without per-call locking.
── Phase 10 inheritance ────────────────────────────────────────────────
Phase 10.11 (RP middleware — pas-external::oidc::IdTokenVerifier)
emits through the same AuditSink port. id_token verify failures
and access_token verify failures share the audit pipeline; the
VerifyErrorKind enum gains id_token-specific variants in 10.11
without breaking the contract.
Re-exports§
pub use rate_limit::MemoryRateLimiter;pub use rate_limit::RateLimiter;pub use rate_limited_sink::RateLimitedAuditSink;pub use sink::AuditEvent;pub use sink::AuditSink;pub use sink::IdTokenFailureKind;pub use sink::NoopAuditSink;pub use sink::VerifyErrorKind;pub use sink::compose_id_token_source_id;pub use sink::compose_source_id;
Modules§
- rate_
limit RateLimiterport +MemoryRateLimitertoken-bucket adapter (M49).- rate_
limited_ sink RateLimitedAuditSink— composition adapter wrapping anyAuditSinkwith anyRateLimiter(refinement #1 from Phase 9 deep-module audit).- sink
AuditSinkport +AuditEventvalue type + ship-with adapters (NoopAuditSink,MemoryAuditSink).
Structs§
- Rate
Limit Key - Opaque per-source bucket key for a
RateLimiter.