ppoppo-token 0.1.0

JWT (RFC 9068, EdDSA) issuance + verification engine for the Ppoppo ecosystem. Single deep module with a small interface (issue, verify) hiding RFC 8725 mitigations M01-M45, JWKS handling, and substrate ports (epoch, session, replay).
Documentation
//! Per-config issuance policy.
//!
//! Mirror of `VerifyConfig` for the issuance side: the same struct travels
//! to PAS, PCS, and the `pas-external` consumer middleware so policy
//! never drifts between the issue and verify halves of the same surface.
//!
//! Values are constructor-pinned (`access_token`); fields are
//! `pub(crate)` so the engine reads them but external callers cannot
//! reach in and mutate `typ` or `cat` to a value the verify side will
//! reject. Multi-audience tokens are opt-in via `with_audiences`.

use std::fmt;

#[derive(Clone)]
#[allow(dead_code)] // `kid`/`cat` consumed by `engine::encode::issue` (Phase 3 commit 3.3+)
pub struct IssueConfig {
    pub(crate) issuer: String,
    pub(crate) audiences: Vec<String>,
    pub(crate) typ: &'static str,
    pub(crate) kid: String,
    pub(crate) cat: &'static str,
}

impl IssueConfig {
    /// Build the canonical access-token config: `at+jwt` typ, `access`
    /// cat, single-audience array initialized from the supplied value.
    /// Multi-aud tokens add audiences via `with_audiences`.
    pub fn access_token(
        issuer: impl Into<String>,
        audience: impl Into<String>,
        kid: impl Into<String>,
    ) -> Self {
        Self {
            issuer: issuer.into(),
            audiences: vec![audience.into()],
            typ: "at+jwt",
            kid: kid.into(),
            cat: "access",
        }
    }

    /// Replace the audience list. Engine emits the array form (M22) when
    /// `audiences.len() > 1`, single string (M21) when length is 1.
    /// Empty audience list is a logic error — the engine refuses to
    /// emit such a token; M20 would reject it on the verify side.
    #[must_use]
    pub fn with_audiences(mut self, audiences: Vec<String>) -> Self {
        self.audiences = audiences;
        self
    }
}

// Manual Debug — derive would expose pub(crate) field syntax in the
// output, which both leaks naming and complicates assertion-based smoke
// tests. We render the same fields in a stable order.
impl fmt::Debug for IssueConfig {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        f.debug_struct("IssueConfig")
            .field("issuer", &self.issuer)
            .field("audiences", &self.audiences)
            .field("typ", &self.typ)
            .field("kid", &self.kid)
            .field("cat", &self.cat)
            .finish()
    }
}