ppoppo-token 0.3.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

ppoppo-token

JWT (RFC 9068, EdDSA) issuance and verification engine for the Ppoppo ecosystem.

A single deep module that hides RFC 8725 mitigations M01–M45, JWKS handling, algorithm pinning, header/claim/domain checks, and substrate ports (epoch, session, replay) behind a two-function surface: issue and verify.

Quick start — verifier (most consumers)

[dependencies]
ppoppo-token = "0.1"
use ppoppo_token::{verify, KeySet, VerifyConfig, AuthError};

async fn check_bearer(token: &str, key_set: &KeySet) -> Result<(), AuthError> {
    let cfg = VerifyConfig::access_token("accounts.ppoppo.com", "my-client-id");
    let claims = verify(token, &cfg, key_set).await?;
    println!("verified token for ppnum_id={}", claims.sub);
    Ok(())
}

The single verify entry-point funnels every flow through one engine — no direct jsonwebtoken calls outside the engine, no parallel decode paths, and no transport-aware branches (M38 invariant: cookie and Bearer-header are the same surface; consumer middleware extracts the bare token before calling verify).

What is hidden

The engine owns the algorithm. Consumers never:

  • Decode JWT segments or pick an alg — the engine pins EdDSA (sealed Algorithm enum, M51/M52/M54 structurally uncompilable outside the engine).
  • Manage replay caches, session liveness, or per-account epoch checks — these are substrate ports (ReplayDefense, SessionRevocation, EpochRevocation); pass an implementation in VerifyConfig.
  • Parse JWKS by hand — Jwks::into_key_set builds a KeySet, and KeySet rotates keys without consumer involvement.

Quick start — issuer (PAS only)

use std::time::Duration;
use ppoppo_token::{issue, IssueConfig, IssueRequest, SigningKey};

let key = SigningKey::from_ed25519_pem(pem_bytes, "kid-2026-05-05")?;
let cfg = IssueConfig::access_token(
    "accounts.ppoppo.com",
    "my-client-id",
    "kid-2026-05-05",
);
let req = IssueRequest::new(ppnum_id, "my-client-id", Duration::from_secs(3600));
let token: String = issue(&req, &cfg, &key)?;

Issuance is intentionally synchronous (no I/O) and lives in the same engine — adding a parallel issuance path would fork the algorithm and silently allow header drift between issuers and verifiers.

Shared cache contract (cross-substrate)

When multiple processes verify tokens against a shared session_version cache, all readers/writers MUST agree on the key shape and TTL:

use ppoppo_token::{sv_cache_key, SV_CACHE_TTL};

let key = sv_cache_key("01HK123ABCDEF456GHJ789KLMN");  // → "sv:01HK..."
// TTL contract: 60 seconds. Bounds post-break-glass staleness window.

Format-coupling the cache key through the engine prevents drift between PAS (writer) and consumers (PCS chat-auth, the pas-external SDK) — no string duplication on the consumer side.

Stability

  • 0.x: pre-1.0 SemVer. Minor bumps may break the public surface.
  • The engine surface (verify, issue, Claims, Algorithm, VerifyConfig, IssueConfig, IssueRequest, KeySet, Jwks, AuthError, IssueError) is the durable contract.
  • Substrate port traits (ReplayDefense, SessionRevocation, EpochRevocation) may add methods in 0.x; pin a minor version if you implement them outside this workspace.

License

MIT.