ppoppo_token/issue_config.rs
1//! Per-config issuance policy.
2//!
3//! Mirror of `VerifyConfig` for the issuance side: the same struct travels
4//! to PAS, PCS, and the `pas-external` consumer middleware so policy
5//! never drifts between the issue and verify halves of the same surface.
6//!
7//! Values are constructor-pinned (`access_token`); fields are
8//! `pub(crate)` so the engine reads them but external callers cannot
9//! reach in and mutate `typ` or `cat` to a value the verify side will
10//! reject. Multi-audience tokens are opt-in via `with_audiences`.
11
12use std::fmt;
13
14#[derive(Clone)]
15#[allow(dead_code)] // `kid`/`cat` consumed by `engine::encode::issue` (Phase 3 commit 3.3+)
16pub struct IssueConfig {
17 pub(crate) issuer: String,
18 pub(crate) audiences: Vec<String>,
19 pub(crate) typ: &'static str,
20 pub(crate) kid: String,
21 pub(crate) cat: &'static str,
22}
23
24impl IssueConfig {
25 /// Build the canonical access-token config: `at+jwt` typ, `access`
26 /// cat, single-audience array initialized from the supplied value.
27 /// Multi-aud tokens add audiences via `with_audiences`.
28 pub fn access_token(
29 issuer: impl Into<String>,
30 audience: impl Into<String>,
31 kid: impl Into<String>,
32 ) -> Self {
33 Self {
34 issuer: issuer.into(),
35 audiences: vec![audience.into()],
36 typ: "at+jwt",
37 kid: kid.into(),
38 cat: "access",
39 }
40 }
41
42 /// Replace the audience list. Engine emits the array form (M22) when
43 /// `audiences.len() > 1`, single string (M21) when length is 1.
44 /// Empty audience list is a logic error — the engine refuses to
45 /// emit such a token; M20 would reject it on the verify side.
46 #[must_use]
47 pub fn with_audiences(mut self, audiences: Vec<String>) -> Self {
48 self.audiences = audiences;
49 self
50 }
51}
52
53// Manual Debug — derive would expose pub(crate) field syntax in the
54// output, which both leaks naming and complicates assertion-based smoke
55// tests. We render the same fields in a stable order.
56impl fmt::Debug for IssueConfig {
57 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
58 f.debug_struct("IssueConfig")
59 .field("issuer", &self.issuer)
60 .field("audiences", &self.audiences)
61 .field("typ", &self.typ)
62 .field("kid", &self.kid)
63 .field("cat", &self.cat)
64 .finish()
65 }
66}