Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion crates/auths-core/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,6 @@ pub mod api;
pub mod config;
pub mod crypto;
pub mod error;
pub mod keri_did;
pub mod pairing;
pub mod paths;
pub mod policy;
Expand All @@ -60,6 +59,7 @@ pub mod storage;
pub mod testing;
pub mod trust;
pub mod utils;
pub mod validated_identity_did;
pub mod witness;

pub use agent::{AgentCore, AgentHandle, AgentSession};
Expand Down
29 changes: 29 additions & 0 deletions crates/auths-core/src/signing.rs
Original file line number Diff line number Diff line change
Expand Up @@ -382,6 +382,35 @@ impl CachedPassphraseProvider {
}
}

/// Pre-fill the cache with a passphrase for session-based unlock.
///
/// This allows callers to unlock once and re-use the passphrase for
/// the configured TTL without re-prompting. The passphrase is stored
/// only in Rust memory (never crosses FFI boundary after this call).
///
/// The default prompt key is used so all subsequent signing operations
/// that use the same prompt will hit the cache.
pub fn unlock(&self, passphrase: &str) {
let mut cache = self.cache.lock().unwrap_or_else(|e| e.into_inner());
cache.insert(
String::new(),
(Zeroizing::new(passphrase.to_string()), Instant::now()),
);
}

/// Returns the remaining TTL in seconds, or `None` if no cached passphrase.
pub fn remaining_ttl(&self) -> Option<Duration> {
let cache = self.cache.lock().unwrap_or_else(|e| e.into_inner());
cache.values().next().and_then(|(_, cached_at)| {
let elapsed = cached_at.elapsed();
if elapsed < self.ttl {
Some(self.ttl - elapsed)
} else {
None
}
})
}

/// Clears all cached passphrases.
///
/// Call this on logout, lock, or when the session ends to ensure
Expand Down
8 changes: 8 additions & 0 deletions crates/auths-core/src/trust/pinned.rs
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,14 @@ impl PinnedIdentityStore {
}
}

/// Check if an identity is pinned (lightweight existence check).
///
/// More efficient than `lookup` when you only need a yes/no answer.
pub fn is_pinned(&self, did: &str) -> Result<bool, TrustError> {
let _lock = self.lock()?;
Ok(self.read_all()?.iter().any(|e| e.did == did))
}

/// List all pinned identities.
pub fn list(&self) -> Result<Vec<PinnedIdentity>, TrustError> {
let _lock = self.lock()?;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,50 +12,56 @@ const PREFIX: &str = "did:keri:";
/// A validated `did:keri:<prefix>` identifier.
///
/// The inner string always starts with `"did:keri:"` followed by a non-empty
/// KERI prefix. Construction is fallible — use [`KeriDid::parse`] or
/// KERI prefix. Construction is fallible — use [`ValidatedIdentityDID::parse`] or
/// [`TryFrom<String>`].
///
/// This is the validated form of `IdentityDID` (from `auths-verifier`).
/// `IdentityDID` is an unvalidated newtype for API boundaries;
/// `ValidatedIdentityDID` enforces format invariants at construction.
///
/// Usage:
/// ```ignore
/// let did = KeriDid::parse("did:keri:EXq5abc")?;
/// let did = ValidatedIdentityDID::parse("did:keri:EXq5abc")?;
/// assert_eq!(did.prefix(), "EXq5abc");
/// assert_eq!(did.as_str(), "did:keri:EXq5abc");
/// ```
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
#[serde(try_from = "String", into = "String")]
pub struct KeriDid(String);
pub struct ValidatedIdentityDID(String);

impl KeriDid {
impl ValidatedIdentityDID {
/// Parse a `did:keri:` string, returning an error if the format is invalid.
///
/// Args:
/// * `s`: A string that must start with `"did:keri:"` followed by a non-empty prefix.
///
/// Usage:
/// ```ignore
/// let did = KeriDid::parse("did:keri:EXq5")?;
/// let did = ValidatedIdentityDID::parse("did:keri:EXq5")?;
/// ```
pub fn parse(s: &str) -> Result<Self, KeriDidError> {
let keri_prefix = s.strip_prefix(PREFIX).ok_or(KeriDidError::MissingPrefix)?;
pub fn parse(s: &str) -> Result<Self, IdentityDIDError> {
let keri_prefix = s
.strip_prefix(PREFIX)
.ok_or(IdentityDIDError::MissingPrefix)?;
if keri_prefix.is_empty() {
return Err(KeriDidError::EmptyPrefix);
return Err(IdentityDIDError::EmptyPrefix);
}
Ok(Self(s.to_string()))
}

/// Build a `KeriDid` from a raw KERI prefix (without the `did:keri:` scheme).
/// Build a `ValidatedIdentityDID` from a raw KERI prefix (without the `did:keri:` scheme).
///
/// Args:
/// * `prefix`: The bare KERI prefix string (e.g. `"EXq5abc"`).
///
/// Usage:
/// ```ignore
/// let did = KeriDid::from_prefix("EXq5abc");
/// let did = ValidatedIdentityDID::from_prefix("EXq5abc");
/// assert_eq!(did.as_str(), "did:keri:EXq5abc");
/// ```
pub fn from_prefix(prefix: &str) -> Result<Self, KeriDidError> {
pub fn from_prefix(prefix: &str) -> Result<Self, IdentityDIDError> {
if prefix.is_empty() {
return Err(KeriDidError::EmptyPrefix);
return Err(IdentityDIDError::EmptyPrefix);
}
Ok(Self(format!("{}{}", PREFIX, prefix)))
}
Expand All @@ -72,38 +78,40 @@ impl KeriDid {
}
}

impl fmt::Display for KeriDid {
impl fmt::Display for ValidatedIdentityDID {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(&self.0)
}
}

impl AsRef<str> for KeriDid {
impl AsRef<str> for ValidatedIdentityDID {
fn as_ref(&self) -> &str {
&self.0
}
}

impl From<KeriDid> for String {
fn from(did: KeriDid) -> Self {
impl From<ValidatedIdentityDID> for String {
fn from(did: ValidatedIdentityDID) -> Self {
did.0
}
}

impl TryFrom<String> for KeriDid {
type Error = KeriDidError;
impl TryFrom<String> for ValidatedIdentityDID {
type Error = IdentityDIDError;

fn try_from(s: String) -> Result<Self, Self::Error> {
let keri_prefix = s.strip_prefix(PREFIX).ok_or(KeriDidError::MissingPrefix)?;
let keri_prefix = s
.strip_prefix(PREFIX)
.ok_or(IdentityDIDError::MissingPrefix)?;
if keri_prefix.is_empty() {
return Err(KeriDidError::EmptyPrefix);
return Err(IdentityDIDError::EmptyPrefix);
}
Ok(Self(s))
}
}

impl TryFrom<&str> for KeriDid {
type Error = KeriDidError;
impl TryFrom<&str> for ValidatedIdentityDID {
type Error = IdentityDIDError;

fn try_from(s: &str) -> Result<Self, Self::Error> {
Self::parse(s)
Expand All @@ -113,7 +121,7 @@ impl TryFrom<&str> for KeriDid {
/// Error from parsing an invalid `did:keri:` string.
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
#[non_exhaustive]
pub enum KeriDidError {
pub enum IdentityDIDError {
/// The `did:keri:` prefix is absent.
#[error("not a did:keri: identifier")]
MissingPrefix,
Expand All @@ -129,67 +137,76 @@ mod tests {

#[test]
fn parse_valid() {
let did = KeriDid::parse("did:keri:EXq5abc123").unwrap();
let did = ValidatedIdentityDID::parse("did:keri:EXq5abc123").unwrap();
assert_eq!(did.prefix(), "EXq5abc123");
assert_eq!(did.as_str(), "did:keri:EXq5abc123");
assert_eq!(did.to_string(), "did:keri:EXq5abc123");
}

#[test]
fn from_prefix_valid() {
let did = KeriDid::from_prefix("EXq5abc123").unwrap();
let did = ValidatedIdentityDID::from_prefix("EXq5abc123").unwrap();
assert_eq!(did.as_str(), "did:keri:EXq5abc123");
assert_eq!(did.prefix(), "EXq5abc123");
}

#[test]
fn rejects_non_keri() {
assert_eq!(
KeriDid::parse("did:key:z6Mk123"),
Err(KeriDidError::MissingPrefix)
ValidatedIdentityDID::parse("did:key:z6Mk123"),
Err(IdentityDIDError::MissingPrefix)
);
}

#[test]
fn rejects_empty_prefix() {
assert_eq!(KeriDid::parse("did:keri:"), Err(KeriDidError::EmptyPrefix));
assert_eq!(
ValidatedIdentityDID::parse("did:keri:"),
Err(IdentityDIDError::EmptyPrefix)
);
}

#[test]
fn rejects_missing_scheme() {
assert_eq!(KeriDid::parse("EXq5abc"), Err(KeriDidError::MissingPrefix));
assert_eq!(
ValidatedIdentityDID::parse("EXq5abc"),
Err(IdentityDIDError::MissingPrefix)
);
}

#[test]
fn from_prefix_rejects_empty() {
assert_eq!(KeriDid::from_prefix(""), Err(KeriDidError::EmptyPrefix));
assert_eq!(
ValidatedIdentityDID::from_prefix(""),
Err(IdentityDIDError::EmptyPrefix)
);
}

#[test]
fn try_from_string() {
let did: KeriDid = "did:keri:EXq5".to_string().try_into().unwrap();
let did: ValidatedIdentityDID = "did:keri:EXq5".to_string().try_into().unwrap();
assert_eq!(did.prefix(), "EXq5");
}

#[test]
fn into_string() {
let did = KeriDid::parse("did:keri:EXq5").unwrap();
let did = ValidatedIdentityDID::parse("did:keri:EXq5").unwrap();
let s: String = did.into();
assert_eq!(s, "did:keri:EXq5");
}

#[test]
fn serde_roundtrip() {
let did = KeriDid::parse("did:keri:EXq5abc").unwrap();
let did = ValidatedIdentityDID::parse("did:keri:EXq5abc").unwrap();
let json = serde_json::to_string(&did).unwrap();
assert_eq!(json, r#""did:keri:EXq5abc""#);
let parsed: KeriDid = serde_json::from_str(&json).unwrap();
let parsed: ValidatedIdentityDID = serde_json::from_str(&json).unwrap();
assert_eq!(parsed, did);
}

#[test]
fn serde_rejects_invalid() {
let result: Result<KeriDid, _> = serde_json::from_str(r#""did:key:z6Mk""#);
let result: Result<ValidatedIdentityDID, _> = serde_json::from_str(r#""did:key:z6Mk""#);
assert!(result.is_err());
}
}
2 changes: 1 addition & 1 deletion crates/auths-id/src/keri/inception.rs
Original file line number Diff line number Diff line change
Expand Up @@ -339,7 +339,7 @@ pub fn prefix_to_did(prefix: &str) -> String {

/// Extract the prefix from a did:keri DID.
///
/// Prefer [`auths_core::keri_did::KeriDid`] at API boundaries for type safety.
/// Prefer [`auths_core::validated_identity_did::ValidatedIdentityDID`] at API boundaries for type safety.
pub fn did_to_prefix(did: &str) -> Option<&str> {
did.strip_prefix("did:keri:")
}
Expand Down
13 changes: 13 additions & 0 deletions crates/auths-policy/src/builder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,19 @@ impl PolicyBuilder {
}
}

/// Reconstruct a `PolicyBuilder` from a JSON policy expression.
///
/// Enables round-tripping saved policy JSON back to a builder for
/// modification or recompilation.
pub fn from_json(json_str: &str) -> Result<Self, serde_json::Error> {
let expr: Expr = serde_json::from_str(json_str)?;
let conditions = match expr {
Expr::And(children) => children,
single => vec![single],
};
Ok(Self { conditions })
}

/// Require a specific capability.
pub fn require_capability(mut self, cap: impl Into<String>) -> Self {
self.conditions.push(Expr::HasCapability(cap.into()));
Expand Down
Loading