# 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)

```toml
[dependencies]
ppoppo-token = "0.1"
```

```rust
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)

```rust
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:

```rust
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.
