use std::cell::RefCell;
use std::collections::HashMap;
use std::sync::atomic::{AtomicU64, Ordering};
use chrono::{DateTime, Utc};
use schemars::JsonSchema;
use serde::ser::{SerializeMap, Serializer};
use serde::{Deserialize, Serialize};
macro_rules! string_newtype {
(
$(#[$meta:meta])*
$name:ident
) => {
$(#[$meta])*
#[derive(
Debug,
Clone,
PartialEq,
Eq,
Hash,
PartialOrd,
Ord,
Serialize,
Deserialize,
JsonSchema,
)]
#[serde(transparent)]
pub struct $name(String);
impl $name {
pub fn new(s: impl Into<String>) -> Self {
Self(s.into())
}
pub fn as_str(&self) -> &str {
&self.0
}
pub fn into_string(self) -> String {
self.0
}
}
impl std::fmt::Display for $name {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(&self.0)
}
}
impl From<String> for $name {
fn from(s: String) -> Self {
Self(s)
}
}
impl From<&str> for $name {
fn from(s: &str) -> Self {
Self(s.to_owned())
}
}
};
}
pub use crate::capabilities::{CookieName, HeaderName, MethodPath};
string_newtype! {
Scope
}
string_newtype! {
Origin
}
string_newtype! {
CredentialScheme
}
string_newtype! {
CredentialKindName
}
string_newtype! {
ParamName
}
string_newtype! {
CredentialId
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, JsonSchema)]
#[serde(tag = "kind", rename_all = "snake_case")]
pub enum CredentialKind {
Bearer,
Cookie,
OauthAccess,
OauthRefresh,
OidcId,
AwsSts,
Macaroon,
Other {
name: CredentialKindName,
},
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, JsonSchema)]
#[serde(tag = "site", rename_all = "snake_case")]
pub enum AttachmentSite {
Header {
name: HeaderName,
},
Cookie {
name: CookieName,
},
FirstFrame {
setup_method: MethodPath,
param: ParamName,
},
InRpcParam {
param: ParamName,
},
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, JsonSchema)]
pub struct CredentialIssuer {
pub origin: Origin,
pub method: MethodPath,
}
impl CredentialIssuer {
pub fn new(origin: Origin, method: MethodPath) -> Self {
Self { origin, method }
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
pub struct CredentialMetadata {
pub kind: CredentialKind,
pub attach_as: AttachmentSite,
pub scheme: Option<CredentialScheme>,
pub scopes: Vec<Scope>,
pub expires_at: Option<DateTime<Utc>>,
pub refresh_via: Option<MethodPath>,
pub revoke_via: Option<MethodPath>,
pub issuer: CredentialIssuer,
pub sensitive: bool,
}
impl CredentialMetadata {
#[allow(clippy::too_many_arguments)]
pub fn new(
kind: CredentialKind,
attach_as: AttachmentSite,
scheme: Option<CredentialScheme>,
scopes: Vec<Scope>,
expires_at: Option<DateTime<Utc>>,
refresh_via: Option<MethodPath>,
revoke_via: Option<MethodPath>,
issuer: CredentialIssuer,
) -> Self {
Self {
kind,
attach_as,
scheme,
scopes,
expires_at,
refresh_via,
revoke_via,
issuer,
sensitive: true,
}
}
}
#[derive(Debug, Clone)]
pub struct Credential<T> {
inner: T,
metadata: CredentialMetadata,
id: CredentialId,
}
impl<T> Credential<T> {
pub(crate) fn new_sealed(inner: T, metadata: CredentialMetadata, id: CredentialId) -> Self {
Self {
inner,
metadata,
id,
}
}
pub fn metadata(&self) -> &CredentialMetadata {
&self.metadata
}
pub fn id(&self) -> &CredentialId {
&self.id
}
#[allow(dead_code)]
pub(crate) fn inner(&self) -> &T {
&self.inner
}
}
impl<T> Credential<T> {
pub fn audit_projection(&self) -> &CredentialMetadata {
&self.metadata
}
}
#[derive(Debug, Clone)]
pub struct CapturedCredential {
pub id: CredentialId,
pub value: serde_json::Value,
pub metadata: CredentialMetadata,
}
#[derive(Debug, Default)]
pub struct DispatchSidecar {
map: HashMap<CredentialId, CapturedCredential>,
}
impl DispatchSidecar {
pub fn new() -> Self {
Self::default()
}
pub fn drain(&mut self) -> HashMap<CredentialId, CapturedCredential> {
std::mem::take(&mut self.map)
}
pub fn is_empty(&self) -> bool {
self.map.is_empty()
}
pub fn len(&self) -> usize {
self.map.len()
}
}
thread_local! {
static DISPATCH_SIDECAR: RefCell<Option<DispatchSidecar>> = const { RefCell::new(None) };
}
static CREDENTIAL_ID_COUNTER: AtomicU64 = AtomicU64::new(0);
fn next_credential_id() -> CredentialId {
let n = CREDENTIAL_ID_COUNTER.fetch_add(1, Ordering::Relaxed);
CredentialId::new(format!("cred_{n}"))
}
pub struct DispatchCaptureGuard {
previous: Option<DispatchSidecar>,
}
impl DispatchCaptureGuard {
#[allow(dead_code)]
pub(crate) fn install() -> Self {
let previous = DISPATCH_SIDECAR.with(|cell| {
let prev = cell.borrow_mut().take();
*cell.borrow_mut() = Some(DispatchSidecar::new());
prev
});
Self { previous }
}
#[allow(dead_code)]
pub(crate) fn drain(&self) -> Option<HashMap<CredentialId, CapturedCredential>> {
DISPATCH_SIDECAR.with(|cell| cell.borrow_mut().as_mut().map(|s| s.drain()))
}
}
impl Drop for DispatchCaptureGuard {
fn drop(&mut self) {
let prev = self.previous.take();
DISPATCH_SIDECAR.with(|cell| {
*cell.borrow_mut() = prev;
});
}
}
#[allow(dead_code)]
pub(crate) fn with_dispatch_capture<F, R>(f: F) -> (R, HashMap<CredentialId, CapturedCredential>)
where
F: FnOnce() -> R,
{
let guard = DispatchCaptureGuard::install();
let out = f();
let captured = guard.drain().unwrap_or_default();
drop(guard);
(out, captured)
}
pub fn run_with_credential_capture<F, R>(f: F) -> (R, Vec<CapturedCredential>)
where
F: FnOnce() -> R,
{
let guard = DispatchCaptureGuard::install();
let out = f();
let captured_map = guard.drain().unwrap_or_default();
drop(guard);
let mut captured: Vec<CapturedCredential> = captured_map.into_values().collect();
captured.sort_by(|a, b| a.id.as_str().cmp(b.id.as_str()));
(out, captured)
}
impl<T> Serialize for Credential<T>
where
T: Serialize,
{
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
DISPATCH_SIDECAR.with(|cell| {
let mut borrow = cell.borrow_mut();
if let Some(sidecar) = borrow.as_mut() {
let value = serde_json::to_value(&self.inner).unwrap_or(serde_json::Value::Null);
sidecar.map.insert(
self.id.clone(),
CapturedCredential {
id: self.id.clone(),
value,
metadata: self.metadata.clone(),
},
);
}
});
let mut map = serializer.serialize_map(Some(1))?;
map.serialize_entry("$credential", self.id.as_str())?;
map.end()
}
}
#[derive(Debug, Clone)]
pub struct CredentialMinter {
default_issuer: CredentialIssuer,
}
impl CredentialMinter {
#[allow(dead_code)]
pub(crate) fn new_sealed(default_issuer: CredentialIssuer) -> Self {
Self { default_issuer }
}
#[cfg(feature = "test-support")]
#[doc(hidden)]
pub fn new_for_test(default_issuer: CredentialIssuer) -> Self {
Self { default_issuer }
}
pub fn issuer(&self) -> &CredentialIssuer {
&self.default_issuer
}
pub fn mint<T>(&self, payload: T, metadata: CredentialMetadata) -> Credential<T> {
let id = next_credential_id();
Credential::new_sealed(payload, metadata, id)
}
#[allow(clippy::too_many_arguments)]
pub fn mint_with_issuer<T>(
&self,
payload: T,
kind: CredentialKind,
attach_as: AttachmentSite,
scheme: Option<CredentialScheme>,
scopes: Vec<Scope>,
expires_at: Option<DateTime<Utc>>,
refresh_via: Option<MethodPath>,
revoke_via: Option<MethodPath>,
) -> Credential<T> {
let metadata = CredentialMetadata::new(
kind,
attach_as,
scheme,
scopes,
expires_at,
refresh_via,
revoke_via,
self.default_issuer.clone(),
);
self.mint(payload, metadata)
}
}
#[derive(Debug, Clone)]
pub struct CredentialFieldMarker {
pub variant: Option<&'static str>,
pub field: &'static str,
pub kind: CredentialKind,
pub attach_as: AttachmentSite,
pub scheme: Option<CredentialScheme>,
pub scopes: Vec<Scope>,
pub refresh_via: Option<MethodPath>,
pub revoke_via: Option<MethodPath>,
}
impl CredentialFieldMarker {
#[allow(clippy::too_many_arguments)]
pub fn new(
variant: Option<&'static str>,
field: &'static str,
kind: CredentialKind,
attach_as: AttachmentSite,
scheme: Option<CredentialScheme>,
scopes: Vec<Scope>,
refresh_via: Option<MethodPath>,
revoke_via: Option<MethodPath>,
) -> Self {
Self {
variant,
field,
kind,
attach_as,
scheme,
scopes,
refresh_via,
revoke_via,
}
}
pub fn to_metadata(
&self,
expires_at: Option<chrono::DateTime<chrono::Utc>>,
issuer: CredentialIssuer,
) -> CredentialMetadata {
CredentialMetadata::new(
self.kind.clone(),
self.attach_as.clone(),
self.scheme.clone(),
self.scopes.clone(),
expires_at,
self.refresh_via.clone(),
self.revoke_via.clone(),
issuer,
)
}
}
pub trait HasCredentialMarkers {
fn credential_markers() -> &'static [CredentialFieldMarker];
}
#[doc(hidden)]
pub struct CredentialsRegistryProbe<T: ?Sized>(::core::marker::PhantomData<fn() -> T>);
impl<T: ?Sized> CredentialsRegistryProbe<T> {
#[doc(hidden)]
pub const fn new() -> Self {
Self(::core::marker::PhantomData)
}
}
impl<T: ?Sized> Default for CredentialsRegistryProbe<T> {
fn default() -> Self {
Self::new()
}
}
impl<T: HasCredentialMarkers + ?Sized> CredentialsRegistryProbe<T> {
#[doc(hidden)]
pub fn marker_slice(&self) -> &'static [CredentialFieldMarker] {
T::credential_markers()
}
}
pub trait CredentialsRegistryFallback {
fn marker_slice(&self) -> &'static [CredentialFieldMarker] {
&[]
}
}
impl<T: ?Sized> CredentialsRegistryFallback for &CredentialsRegistryProbe<T> {}
#[deprecated(
since = "0.1.0",
note = "Use `CredentialsRegistryFallback` instead (renamed for clarity)."
)]
pub trait CredentialsRegistry: CredentialsRegistryFallback {}
#[allow(deprecated)]
impl<T: CredentialsRegistryFallback + ?Sized> CredentialsRegistry for T {}
#[cfg(test)]
mod tests {
use super::*;
fn sample_issuer() -> CredentialIssuer {
CredentialIssuer::new(
Origin::new("ws://localhost:4444"),
MethodPath::try_new("auth.login").unwrap(),
)
}
fn sample_metadata() -> CredentialMetadata {
CredentialMetadata::new(
CredentialKind::Bearer,
AttachmentSite::Header {
name: HeaderName::try_new("authorization").unwrap(),
},
Some(CredentialScheme::new("Bearer ")),
vec![Scope::new("cone.send_message")],
None,
Some(MethodPath::try_new("auth.refresh").unwrap()),
Some(MethodPath::try_new("auth.logout").unwrap()),
sample_issuer(),
)
}
#[test]
fn minter_mints_credential_via_internal_api() {
let minter = CredentialMinter::new_sealed(sample_issuer());
let cred: Credential<String> =
minter.mint("eyJhbGciOiJIUzI1NiIs...token".to_string(), sample_metadata());
assert_eq!(cred.metadata().kind, CredentialKind::Bearer);
assert_eq!(cred.audit_projection(), cred.metadata());
assert_eq!(cred.inner(), "eyJhbGciOiJIUzI1NiIs...token");
}
#[test]
fn metadata_sensitive_is_always_true() {
let m = sample_metadata();
assert!(m.sensitive, "sensitive must be initialized to true");
}
#[test]
fn credential_id_is_unique_per_mint() {
let minter = CredentialMinter::new_sealed(sample_issuer());
let c1: Credential<String> = minter.mint("a".to_string(), sample_metadata());
let c2: Credential<String> = minter.mint("b".to_string(), sample_metadata());
assert_ne!(c1.id(), c2.id());
}
#[test]
fn serialize_outside_dispatch_emits_only_sentinel() {
let minter = CredentialMinter::new_sealed(sample_issuer());
let cred: Credential<String> =
minter.mint("super-secret-token".to_string(), sample_metadata());
let v = serde_json::to_value(&cred).expect("serialize");
let obj = v.as_object().expect("object");
assert_eq!(obj.len(), 1, "sentinel is the only key");
let id = obj
.get("$credential")
.and_then(|v| v.as_str())
.expect("$credential id is a string");
assert_eq!(id, cred.id().as_str());
let serialized = serde_json::to_string(&cred).expect("string");
assert!(
!serialized.contains("super-secret-token"),
"inner value must not appear in default serialization, got: {serialized}"
);
}
#[test]
fn serialize_inside_dispatch_captures_value_into_sidecar() {
let minter = CredentialMinter::new_sealed(sample_issuer());
let cred: Credential<String> =
minter.mint("dispatch-secret".to_string(), sample_metadata());
let (json_value, captured) = with_dispatch_capture(|| {
serde_json::to_value(&cred).expect("serialize under guard")
});
let obj = json_value.as_object().expect("object");
assert!(obj.contains_key("$credential"));
assert!(!obj.contains_key("inner"));
assert_eq!(captured.len(), 1);
let entry = captured.get(cred.id()).expect("captured by id");
assert_eq!(
entry.value,
serde_json::Value::String("dispatch-secret".into())
);
assert_eq!(entry.metadata.kind, CredentialKind::Bearer);
let plain = serde_json::to_value(&cred).expect("serialize after guard");
assert!(plain
.as_object()
.expect("object")
.contains_key("$credential"));
let plain_str = serde_json::to_string(&plain).unwrap();
assert!(!plain_str.contains("dispatch-secret"));
}
#[test]
fn dispatch_capture_resets_on_panic() {
let minter = CredentialMinter::new_sealed(sample_issuer());
let cred: Credential<String> =
minter.mint("panic-time-secret".to_string(), sample_metadata());
let _ = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
let _guard = DispatchCaptureGuard::install();
DISPATCH_SIDECAR.with(|cell| {
assert!(cell.borrow().is_some(), "sidecar installed");
});
panic!("simulated panic mid-serialization");
}));
DISPATCH_SIDECAR.with(|cell| {
assert!(
cell.borrow().is_none(),
"sidecar must be cleared on panic — got Some"
);
});
let v = serde_json::to_value(&cred).expect("serialize after panic");
let obj = v.as_object().expect("object");
assert!(obj.contains_key("$credential"));
assert!(!serde_json::to_string(&v)
.unwrap()
.contains("panic-time-secret"));
}
#[test]
fn audit_projection_yields_metadata_only() {
let minter = CredentialMinter::new_sealed(sample_issuer());
let cred: Credential<String> =
minter.mint("audit-secret".to_string(), sample_metadata());
let projection: &CredentialMetadata = cred.audit_projection();
let audit_json = serde_json::to_string(projection).expect("audit serialize");
assert!(
!audit_json.contains("audit-secret"),
"audit projection must not include inner value, got: {audit_json}"
);
assert!(audit_json.contains("\"kind\""));
}
#[test]
fn metadata_serialization_round_trips() {
let original = sample_metadata();
let json = serde_json::to_string(&original).expect("serialize");
let parsed: CredentialMetadata = serde_json::from_str(&json).expect("deserialize");
assert_eq!(parsed, original);
}
#[test]
fn metadata_round_trips_with_oauth_other_variant() {
let mut m = sample_metadata();
m.kind = CredentialKind::Other {
name: CredentialKindName::new("trak.custom"),
};
let json = serde_json::to_string(&m).expect("serialize");
let parsed: CredentialMetadata = serde_json::from_str(&json).expect("deserialize");
assert_eq!(parsed, m);
}
#[test]
fn attachment_site_variants_round_trip() {
let variants = vec![
AttachmentSite::Header {
name: HeaderName::try_new("authorization").unwrap(),
},
AttachmentSite::Cookie {
name: CookieName::try_new("plexus_session").unwrap(),
},
AttachmentSite::FirstFrame {
setup_method: MethodPath::try_new("auth.connect").unwrap(),
param: ParamName::new("token"),
},
AttachmentSite::InRpcParam {
param: ParamName::new("session_token"),
},
];
for site in variants {
let json = serde_json::to_string(&site).expect("serialize");
let parsed: AttachmentSite = serde_json::from_str(&json).expect("deserialize");
assert_eq!(parsed, site);
}
}
#[test]
fn credential_kind_round_trips_all_variants() {
let variants = vec![
CredentialKind::Bearer,
CredentialKind::Cookie,
CredentialKind::OauthAccess,
CredentialKind::OauthRefresh,
CredentialKind::OidcId,
CredentialKind::AwsSts,
CredentialKind::Macaroon,
CredentialKind::Other {
name: CredentialKindName::new("custom_scheme"),
},
];
for k in variants {
let json = serde_json::to_string(&k).expect("serialize");
let parsed: CredentialKind = serde_json::from_str(&json).expect("deserialize");
assert_eq!(parsed, k);
}
}
#[test]
fn newtypes_serialize_transparently() {
let s = serde_json::to_string(&Scope::new("cone.send_message")).unwrap();
assert_eq!(s, "\"cone.send_message\"");
let h = serde_json::to_string(&HeaderName::try_new("authorization").unwrap()).unwrap();
assert_eq!(h, "\"authorization\"");
}
#[test]
fn nested_struct_with_credential_field_serializes_sentinel() {
#[derive(Serialize)]
struct LoginEvent {
user_id: String,
session: Credential<String>,
}
let minter = CredentialMinter::new_sealed(sample_issuer());
let event = LoginEvent {
user_id: "alice".to_string(),
session: minter.mint("jwt-bytes".to_string(), sample_metadata()),
};
let json = serde_json::to_value(&event).expect("serialize");
let session_field = json.get("session").expect("session field");
let sentinel = session_field
.get("$credential")
.and_then(|v| v.as_str())
.expect("sentinel string");
assert_eq!(sentinel, event.session.id().as_str());
assert_eq!(json.get("user_id").and_then(|v| v.as_str()), Some("alice"));
let s = serde_json::to_string(&event).unwrap();
assert!(!s.contains("jwt-bytes"), "inner JWT must not appear: {s}");
}
#[test]
fn nested_struct_with_credential_emits_sidecar_under_guard() {
#[derive(Serialize)]
struct LoginEvent {
user_id: String,
session: Credential<String>,
}
let minter = CredentialMinter::new_sealed(sample_issuer());
let event = LoginEvent {
user_id: "bob".to_string(),
session: minter.mint("oauth-access".to_string(), sample_metadata()),
};
let (json, captured) =
with_dispatch_capture(|| serde_json::to_value(&event).expect("serialize"));
assert!(json.get("session").unwrap().get("$credential").is_some());
let id = event.session.id();
let entry = captured.get(id).expect("captured");
assert_eq!(
entry.value,
serde_json::Value::String("oauth-access".into())
);
}
#[test]
fn run_with_credential_capture_empty_returns_no_captures() {
let (out, captured) = run_with_credential_capture(|| {
let v = serde_json::to_value(serde_json::json!({"x": 1})).unwrap();
v
});
assert_eq!(out, serde_json::json!({"x": 1}));
assert!(
captured.is_empty(),
"no credentials emitted -> empty Vec; got {captured:?}"
);
}
#[test]
fn run_with_credential_capture_populated_returns_real_captures() {
let minter = CredentialMinter::new_sealed(sample_issuer());
let cred: Credential<String> =
minter.mint("populated-secret".to_string(), sample_metadata());
let cred_id = cred.id().clone();
let (json_value, captured) =
run_with_credential_capture(|| serde_json::to_value(&cred).expect("serialize"));
let obj = json_value.as_object().expect("object");
assert_eq!(obj.len(), 1);
assert_eq!(
obj.get("$credential").and_then(|v| v.as_str()),
Some(cred_id.as_str())
);
assert_eq!(captured.len(), 1);
let entry = &captured[0];
assert_eq!(entry.id, cred_id);
assert_eq!(
entry.value,
serde_json::Value::String("populated-secret".into())
);
assert_eq!(entry.metadata.kind, CredentialKind::Bearer);
}
#[test]
fn run_with_credential_capture_nested_sidecars_do_not_interleave() {
let minter = CredentialMinter::new_sealed(sample_issuer());
let outer_cred: Credential<String> =
minter.mint("outer-secret".to_string(), sample_metadata());
let inner_cred: Credential<String> =
minter.mint("inner-secret".to_string(), sample_metadata());
let outer_id = outer_cred.id().clone();
let inner_id = inner_cred.id().clone();
let (outer_result, outer_captured) = run_with_credential_capture(|| {
let _outer_json = serde_json::to_value(&outer_cred).expect("outer serialize");
let (inner_json, inner_captured) =
run_with_credential_capture(|| serde_json::to_value(&inner_cred).expect("inner"));
assert_eq!(inner_captured.len(), 1, "inner scope captures inner only");
assert_eq!(inner_captured[0].id, inner_id);
assert_eq!(
inner_captured[0].value,
serde_json::Value::String("inner-secret".into())
);
(_outer_json, inner_json)
});
assert_eq!(
outer_captured.len(),
1,
"outer scope contains outer only; got {outer_captured:?}"
);
assert_eq!(outer_captured[0].id, outer_id);
assert_eq!(
outer_captured[0].value,
serde_json::Value::String("outer-secret".into())
);
let (outer_json, inner_json) = outer_result;
assert_eq!(
outer_json.get("$credential").and_then(|v| v.as_str()),
Some(outer_id.as_str())
);
assert_eq!(
inner_json.get("$credential").and_then(|v| v.as_str()),
Some(inner_id.as_str())
);
}
#[test]
fn run_with_credential_capture_resets_on_panic() {
let _ = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
run_with_credential_capture(|| {
DISPATCH_SIDECAR.with(|cell| assert!(cell.borrow().is_some()));
panic!("simulated panic inside run_with_credential_capture");
});
}));
DISPATCH_SIDECAR.with(|cell| {
assert!(
cell.borrow().is_none(),
"sidecar must be cleared on panic"
);
});
let (out, captured) = run_with_credential_capture(|| 7_u32);
assert_eq!(out, 7);
assert!(captured.is_empty());
}
#[test]
fn run_with_credential_capture_isolated_per_thread() {
use std::sync::Arc;
use std::thread;
let minter = Arc::new(CredentialMinter::new_sealed(sample_issuer()));
let m1 = minter.clone();
let m2 = minter.clone();
let h1 = thread::spawn(move || {
let cred: Credential<String> =
m1.mint("thread-1-secret".to_string(), sample_metadata());
let id = cred.id().clone();
let (_v, captured) =
run_with_credential_capture(|| serde_json::to_value(&cred).unwrap());
(id, captured)
});
let h2 = thread::spawn(move || {
let cred: Credential<String> =
m2.mint("thread-2-secret".to_string(), sample_metadata());
let id = cred.id().clone();
let (_v, captured) =
run_with_credential_capture(|| serde_json::to_value(&cred).unwrap());
(id, captured)
});
let (id1, c1) = h1.join().unwrap();
let (id2, c2) = h2.join().unwrap();
assert_eq!(c1.len(), 1, "thread 1 captures only its own credential");
assert_eq!(c1[0].id, id1);
assert_eq!(c1[0].value, serde_json::Value::String("thread-1-secret".into()));
assert_eq!(c2.len(), 1, "thread 2 captures only its own credential");
assert_eq!(c2[0].id, id2);
assert_eq!(c2[0].value, serde_json::Value::String("thread-2-secret".into()));
assert_ne!(id1, id2);
}
#[test]
fn multiple_credentials_get_distinct_ids() {
#[derive(Serialize)]
struct TokenSet {
access: Credential<String>,
refresh: Credential<String>,
}
let minter = CredentialMinter::new_sealed(sample_issuer());
let mut m_refresh = sample_metadata();
m_refresh.kind = CredentialKind::OauthRefresh;
let event = TokenSet {
access: minter.mint("access-tok".to_string(), sample_metadata()),
refresh: minter.mint("refresh-tok".to_string(), m_refresh),
};
assert_ne!(event.access.id(), event.refresh.id());
let (_json, captured) =
with_dispatch_capture(|| serde_json::to_value(&event).expect("serialize"));
assert_eq!(captured.len(), 2);
assert!(captured.contains_key(event.access.id()));
assert!(captured.contains_key(event.refresh.id()));
}
#[test]
fn credential_field_marker_constructs_via_new_and_struct_literal() {
let m1 = CredentialFieldMarker::new(
None,
"session",
CredentialKind::Bearer,
AttachmentSite::Header {
name: HeaderName::try_new("authorization").unwrap(),
},
Some(CredentialScheme::new("Bearer ")),
vec![Scope::new("cone.send_message")],
Some(MethodPath::try_new("auth.refresh").unwrap()),
Some(MethodPath::try_new("auth.logout").unwrap()),
);
assert_eq!(m1.field, "session");
assert!(m1.variant.is_none());
assert!(matches!(m1.kind, CredentialKind::Bearer));
assert!(matches!(m1.attach_as, AttachmentSite::Header { .. }));
assert_eq!(m1.scheme.as_ref().map(|s| s.as_str()), Some("Bearer "));
assert_eq!(m1.scopes.len(), 1);
let m2 = CredentialFieldMarker {
variant: Some("Issued"),
field: "session",
kind: CredentialKind::Bearer,
attach_as: AttachmentSite::Header {
name: HeaderName::try_new("authorization").unwrap(),
},
scheme: None,
scopes: vec![],
refresh_via: None,
revoke_via: None,
};
assert_eq!(m2.variant, Some("Issued"));
assert_eq!(m2.field, "session");
}
#[test]
fn credential_field_marker_composes_metadata() {
let marker = CredentialFieldMarker::new(
None,
"session",
CredentialKind::Bearer,
AttachmentSite::Header {
name: HeaderName::try_new("authorization").unwrap(),
},
Some(CredentialScheme::new("Bearer ")),
vec![Scope::new("cone.send_message")],
None,
None,
);
let issuer = CredentialIssuer::new(
Origin::new("ws://localhost:4444"),
MethodPath::try_new("auth.login").unwrap(),
);
let metadata = marker.to_metadata(None, issuer.clone());
assert_eq!(metadata.kind, CredentialKind::Bearer);
assert_eq!(metadata.issuer, issuer);
assert!(metadata.sensitive, "always true per CredentialMetadata::new");
}
#[test]
fn credentials_registry_fallback_returns_empty_slice() {
use super::CredentialsRegistryFallback as _;
struct PlainType;
let probe = CredentialsRegistryProbe::<PlainType>::new();
let markers: &'static [CredentialFieldMarker] = (&probe).marker_slice();
assert!(markers.is_empty(), "plain type yields empty marker slice");
}
#[test]
fn credentials_registry_probe_constructible_for_arbitrary_types() {
let _: CredentialsRegistryProbe<String> = CredentialsRegistryProbe::new();
let _: CredentialsRegistryProbe<Vec<u8>> = CredentialsRegistryProbe::new();
const _: CredentialsRegistryProbe<()> = CredentialsRegistryProbe::new();
}
#[test]
fn credentials_registry_specialized_path_returns_populated_slice() {
use super::CredentialsRegistryFallback as _;
struct WiredType;
impl HasCredentialMarkers for WiredType {
fn credential_markers() -> &'static [CredentialFieldMarker] {
use std::sync::OnceLock;
static M: OnceLock<Vec<CredentialFieldMarker>> = OnceLock::new();
M.get_or_init(|| {
vec![CredentialFieldMarker::new(
None,
"session",
CredentialKind::Bearer,
AttachmentSite::Header {
name: HeaderName::try_new("authorization").unwrap(),
},
Some(CredentialScheme::new("Bearer ")),
vec![],
None,
None,
)]
})
.as_slice()
}
}
let probe = CredentialsRegistryProbe::<WiredType>::new();
let m = probe.marker_slice();
assert_eq!(m.len(), 1);
assert_eq!(m[0].field, "session");
assert!(matches!(m[0].kind, CredentialKind::Bearer));
let m2 = (&probe).marker_slice();
assert_eq!(
m2.len(),
1,
"inherent specialized path wins over blanket fallback via autoref"
);
assert_eq!(m2[0].field, "session");
}
#[test]
#[allow(deprecated)]
fn credentials_registry_deprecated_alias_compiles() {
struct PlainType;
let probe = CredentialsRegistryProbe::<PlainType>::new();
use super::CredentialsRegistry as _;
let markers = (&probe).marker_slice();
assert!(markers.is_empty());
}
}