pas-external 0.12.0

Ppoppo Accounts System (PAS) external SDK — OAuth2 PKCE, JWT verification port, Axum middleware, session liveness
Documentation
[package]
name = "pas-external"
version = "0.12.0"
edition = "2024"
rust-version = "1.94"
license = "MIT OR Apache-2.0"
description = "Ppoppo Accounts System (PAS) external SDK — OAuth2 PKCE, JWT verification port, Axum middleware, session liveness"
repository = "https://github.com/hakchin/ppoppo"
homepage = "https://accounts.ppoppo.com"
documentation = "https://docs.rs/pas-external"
keywords = ["oauth2", "pkce", "jwt", "axum", "authentication"]
categories = ["authentication", "web-programming"]

[features]
default = ["oauth", "token"]
# `oauth` pulls in `tokio::sync` to carry the #005 break-glass
# sv-validator (`session_version` module). The validator depends on
# `<AuthClient as PasAuthPort>::userinfo` (HTTP userinfo fallback) so
# it lives behind the `oauth` feature; non-oauth consumers never
# compile it. `async-trait` is unconditional (Phase 9 — required by
# the always-compiled `audit::AuditSink` trait).
oauth = [
    "dep:reqwest",
    "dep:sha2",
    "dep:rand",
    "dep:base64",
    "dep:url",
    "dep:tokio",
]
token = ["dep:ppoppo-token", "ppoppo-sdk-core/token"]
# Well-known JWKS (RFC 7517) fetcher with TTL cache + rotation handling.
# Pulls in the HTTP client (oauth) and the token-verification primitives.
well-known-fetch = ["oauth", "token", "dep:tokio", "ppoppo-sdk-core/well-known-fetch"]
# Phase 11.Z 0.10.0 — `epoch::SharedCacheCache` (per-user sv readout
# over the canonical `STANDARDS_SHARED_CACHE.md §3.1` `sv:{sub}`
# namespace). Pulls `ppoppo-infra` trait crate; substrate is the
# consumer's choice (KVRocks, Redis, in-memory). Pulls `well-known-fetch`
# transitively so the `epoch` module is available.
shared-cache = ["well-known-fetch", "dep:ppoppo-infra"]
# Server-side session liveness: AES-256-GCM refresh_token encryption at rest
# plus a PAS-backed liveness classifier. Consumers that persist PAS refresh
# tokens server-side should enable this feature.
session-liveness = ["oauth", "dep:aes-gcm", "dep:base64"]
# Axum middleware. Pulls in session-liveness so that the OAuth callback can
# encrypt PAS refresh_tokens before handing them to the consumer's
# SessionStore — the plaintext never reaches consumer code. The `BearerAuthLayer`
# tower::Layer + tower::Service impls live in `ppoppo-sdk-core::bearer` (Phase A
# Slice 4 lift); pas-external re-exports them at `pas_external::bearer::*` for
# 3rd-party consumers (RCW/CTW; audit decision D in
# `RFC_2026-05-08_app-credential-collapse.md`). pas-external still depends on
# `axum` directly because the OIDC RP composition root (`oidc::relying_party`,
# `oidc::state_store`) builds axum responses + extracts cookies in its own
# handler bodies.
axum = ["oauth", "session-liveness", "token", "well-known-fetch", "ppoppo-sdk-core/axum", "dep:axum", "dep:axum-extra", "dep:tower", "dep:tracing"]
# Exposes:
# - `pas_port::MemoryPasAuth` — the in-memory `PasAuthPort` adapter
#   used by SDK boundary tests and recommended for downstream consumers'
#   integration tests.
# - `test_support::FakePasServer` — wiremock-wrapped fake PAS
#   Authorization Server for consumer boundary tests against the real
#   `RelyingParty::new(...)` boot path (Phase 11.Y replacement for
#   the deleted `for_test_with_parts` escape hatch). Pulls in
#   `wiremock` + `serde_json` + `ppoppo-token` to stand up the
#   discovery + JWKS + token endpoints in-process.
# No runtime cost when disabled.
test-support = ["dep:wiremock", "well-known-fetch", "ppoppo-sdk-core/test-support"]

[dependencies]
# Core types
derive_more = { version = "2", default-features = false, features = ["display", "from_str", "from", "into"] }
ulid = { version = "1", features = ["serde"] }

# 1st-party shared SDK primitives (verifier port, audit trait, session
# liveness port, identity types). Path-dep only — `ppoppo-sdk-core` is
# never published to crates.io. RFC_2026-05-08_app-credential-collapse
# Phase A.
ppoppo-sdk-core = { workspace = true, default-features = false }

# Clock port — injected wall-clock abstraction for deterministic testing.
ppoppo-clock = { workspace = true, features = ["native"] }

# Token verification — Ppoppo JWT engine (RFC 9068, EdDSA).
# 0.x SemVer: workspace-pinned; γ port-and-adapter consumes engine
# internally, never re-exports `jsonwebtoken::*` through the SDK
# boundary. Phase 10.0 split (D1) introduces `access_token::*` and
# `id_token::*` namespaces — pas-external imports the access-token
# profile only; id_token RP middleware lives in `pas-external::oidc`
# (Phase 10.11+) or a sibling SDK crate.
ppoppo-token = { workspace = true, optional = true }

# Backend-agnostic infrastructure traits (`Cache`, `CacheExt`).
# Phase 11.Z 0.10.0 (RFC_2026-05-08 §4.1 lock) — `epoch::SharedCacheCache`
# adapts any `ppoppo_infra::Cache` impl to the SDK's sv-specific
# `epoch::Cache` shape. Substrate-agnostic by construction:
# consumers wire `ppoppo-kvrocks::KvCache` for production or any
# in-memory test impl. `ppoppo-infra` is trait-only (no Redis client
# deps) so the published SDK surface stays small. Optional + gated
# behind `feature = "shared-cache"` so non-sv consumers don't pay
# the dep cost.
ppoppo-infra = { workspace = true, optional = true }

# OAuth2 HTTP client
reqwest = { version = "0.13", default-features = false, features = ["json", "form", "rustls"], optional = true }
url = { version = "2", features = ["serde"], optional = true }

# PKCE
sha2 = { version = "0.11", optional = true }
base64 = { version = "0.22", optional = true }
rand = { version = "0.10", optional = true }

# Session liveness: AES-256-GCM at-rest encryption for refresh_token
aes-gcm = { version = "0.10", features = ["aes"], optional = true }

# Well-known keyset and #005 sv-validator use `tokio::sync::RwLock`.
tokio = { version = "1", default-features = false, features = ["sync"], optional = true }

# Async trait methods. Phase 9 promoted this from `oauth` / `token`
# feature gates to unconditional: the always-compiled `audit::AuditSink`
# trait requires it across every build configuration. Compile-time
# overhead is ~50 KB; the alternative (gating the audit module behind
# a feature) would re-introduce matrix complexity for zero dep saving.
async-trait = "0.1"

# Serialization
serde = { version = "1", features = ["derive"] }
serde_json = "1"

# Error handling
thiserror = "2"

# Axum middleware (feature = "axum")
axum = { version = "0.8", optional = true }
axum-extra = { version = "0.12", features = ["cookie-private"], optional = true }
# Tower Layer + Service traits for the `oidc::axum::BearerAuthLayer`
# perimeter mechanism (Phase 11.X.C lift). Workspace-pinned at 0.5;
# axum 0.8 transitively depends on the same major.
tower = { workspace = true, optional = true }
tracing = { version = "0.1", optional = true }
time = { version = "0.3", features = ["serde", "serde-well-known", "parsing"] }

# HTTP test substrate for `test_support::FakePasServer` (mocks PAS
# discovery + JWKS + token endpoints in-process). Optional dep, gated
# on the `test-support` feature so non-test consumers do not pay the
# wiremock compile cost.
wiremock = { version = "0.6", optional = true }

[dev-dependencies]
static_assertions = "1"
tokio = { version = "1", features = ["rt-multi-thread", "macros"] }
async-trait = "0.1"
serde_json = "1"
thiserror = "2"

[[test]]
name = "liveness_boundary"
required-features = ["test-support", "session-liveness"]

[[test]]
name = "bearer_verifier_boundary"
required-features = ["test-support", "well-known-fetch"]

[[test]]
name = "id_token_verifier_boundary"
required-features = ["test-support", "well-known-fetch"]

[[test]]
name = "audit_sink_boundary"
required-features = ["test-support"]

# Perimeter `BearerAuthLayer` boundary tests for the SDK re-export
# surface (`pas_external::bearer::*`). Phase A Slice 4 renamed from
# `oidc_axum_boundary` after the Layer kit moved to
# `ppoppo_sdk_core::bearer` and pas-external dropped the
# `oidc::axum::*` namespace (audit decisions D + F in
# `RFC_2026-05-08_app-credential-collapse.md`).
[[test]]
name = "bearer_boundary"
required-features = ["test-support", "axum"]

# 0.10.0 boundary tests — RFC_2026-05-08 §4.1/§4.2 lock validation.
[[test]]
name = "epoch_shared_cache_boundary"
required-features = ["test-support", "shared-cache"]

[[test]]
name = "session_liveness_lookup_boundary"
required-features = ["test-support", "well-known-fetch"]

[lints.rust]
unsafe_code = "forbid"

[lints.clippy]
unwrap_used = "deny"
expect_used = "deny"
panic = "deny"