commit 2d243420eeb295ecd770446ec472c58d9df62929
parent 9656841cfa620eca297778e5414475ab50643edc
Author: triesap <tyson@radroots.org>
Date: Tue, 30 Jun 2026 11:07:49 +0000
feat: expose sdk identity module
Diffstat:
3 files changed, 179 insertions(+), 1 deletion(-)
diff --git a/crates/sdk/src/lib.rs b/crates/sdk/src/lib.rs
@@ -25,7 +25,7 @@ mod geonames;
#[cfg(feature = "runtime")]
mod idempotency;
#[cfg(feature = "identity-models")]
-mod identity;
+pub mod identity;
#[cfg(feature = "runtime")]
mod listings_runtime;
#[cfg(feature = "runtime")]
diff --git a/crates/sdk/tests/identity_public_api.rs b/crates/sdk/tests/identity_public_api.rs
@@ -0,0 +1,102 @@
+#![cfg(feature = "identity-models")]
+
+#[test]
+fn identity_models_are_public_through_identity_module() {
+ use radroots_sdk::identity::{
+ DEFAULT_IDENTITY_PATH, IdentityError, RADROOTS_USERNAME_MAX_LEN, RADROOTS_USERNAME_MIN_LEN,
+ RADROOTS_USERNAME_REGEX, RadrootsIdentity, RadrootsIdentityFile, RadrootsIdentityId,
+ RadrootsIdentityProfile, RadrootsIdentityPublic, RadrootsIdentitySecretKeyFormat,
+ radroots_username_is_valid, radroots_username_normalize,
+ };
+
+ assert_eq!(DEFAULT_IDENTITY_PATH, "default.json");
+ assert!(RADROOTS_USERNAME_MIN_LEN <= RADROOTS_USERNAME_MAX_LEN);
+ assert!(RADROOTS_USERNAME_REGEX.contains("[a-z0-9._-]"));
+
+ let normalized = radroots_username_normalize(" Field_User ").expect("normalized username");
+ assert!(radroots_username_is_valid(normalized.as_str()));
+
+ let identity = RadrootsIdentity::generate();
+ let identity_id = RadrootsIdentityId::parse(identity.public_key_hex().as_str())
+ .expect("identity id parses from public key");
+ let public_identity = RadrootsIdentityPublic::new(identity.public_key());
+ let empty_profile = RadrootsIdentityProfile::default();
+ let identity_file = identity.to_file_with_secret_format(RadrootsIdentitySecretKeyFormat::Hex);
+
+ assert_eq!(identity_id.as_str(), identity.public_key_hex());
+ assert_eq!(public_identity.public_key_hex, identity.public_key_hex());
+ assert!(empty_profile.is_empty());
+ assert!(!identity_file.secret_key.is_empty());
+ assert!(matches!(
+ RadrootsIdentityId::parse("not-a-public-key"),
+ Err(IdentityError::InvalidPublicKey(_))
+ ));
+ let _: RadrootsIdentityFile = identity_file;
+}
+
+#[cfg(feature = "identity-storage")]
+#[test]
+fn identity_storage_is_public_through_identity_module() {
+ use radroots_sdk::identity::{
+ RADROOTS_ENCRYPTED_IDENTITY_DEFAULT_KEY_SLOT, RADROOTS_ENCRYPTED_IDENTITY_KEY_SUFFIX,
+ RadrootsEncryptedIdentityFile, RadrootsIdentity, encrypted_identity_wrapping_key_path,
+ load_encrypted_identity, load_encrypted_identity_with_key_slot, load_identity_profile,
+ rotate_encrypted_identity, rotate_encrypted_identity_with_key_slot,
+ store_encrypted_identity, store_encrypted_identity_with_key_slot, store_identity_profile,
+ };
+
+ let temp = tempfile::tempdir().expect("tempdir");
+ let encrypted_path = temp.path().join("sdk-identity.enc.json");
+ let profile_path = temp.path().join("sdk-profile.json");
+ let identity = RadrootsIdentity::generate();
+
+ let encrypted_file = RadrootsEncryptedIdentityFile::new(encrypted_path.clone());
+ encrypted_file.store(&identity).expect("store identity");
+ assert_eq!(
+ encrypted_file
+ .load()
+ .expect("load identity")
+ .public_key_hex(),
+ identity.public_key_hex()
+ );
+
+ store_encrypted_identity(encrypted_path.as_path(), &identity).expect("store encrypted");
+ assert_eq!(
+ load_encrypted_identity(encrypted_path.as_path())
+ .expect("load encrypted")
+ .public_key_hex(),
+ identity.public_key_hex()
+ );
+
+ store_encrypted_identity_with_key_slot(encrypted_path.as_path(), "sdk-api", &identity)
+ .expect("store encrypted with key slot");
+ assert_eq!(
+ load_encrypted_identity_with_key_slot(encrypted_path.as_path(), "sdk-api")
+ .expect("load encrypted with key slot")
+ .public_key_hex(),
+ identity.public_key_hex()
+ );
+
+ rotate_encrypted_identity(encrypted_path.as_path()).expect("rotate default key slot");
+ rotate_encrypted_identity_with_key_slot(encrypted_path.as_path(), "sdk-api")
+ .expect("rotate named key slot");
+ store_identity_profile(profile_path.as_path(), &identity).expect("store profile");
+
+ assert_eq!(
+ load_identity_profile(profile_path.as_path())
+ .expect("load profile")
+ .public_key_hex,
+ identity.public_key_hex()
+ );
+ assert_eq!(
+ RADROOTS_ENCRYPTED_IDENTITY_DEFAULT_KEY_SLOT,
+ "radroots_identity"
+ );
+ assert_eq!(RADROOTS_ENCRYPTED_IDENTITY_KEY_SUFFIX, ".key");
+ assert_eq!(
+ encrypted_identity_wrapping_key_path(encrypted_path.as_path())
+ .file_name()
+ .and_then(|name| name.to_str()),
+ Some("sdk-identity.enc.json.key")
+ );
+}
diff --git a/crates/sdk/tests/source_boundary.rs b/crates/sdk/tests/source_boundary.rs
@@ -131,6 +131,37 @@ const REQUIRED_DVM_RUNTIME_EXPORTS: &[&str] = &[
"RadrootsTradeInventoryBinWitnessDto",
];
+const REQUIRED_IDENTITY_MODEL_EXPORTS: &[&str] = &[
+ "DEFAULT_IDENTITY_PATH",
+ "IdentityError",
+ "RADROOTS_USERNAME_MAX_LEN",
+ "RADROOTS_USERNAME_MIN_LEN",
+ "RADROOTS_USERNAME_REGEX",
+ "RadrootsIdentity",
+ "RadrootsIdentityFile",
+ "RadrootsIdentityId",
+ "RadrootsIdentityProfile",
+ "RadrootsIdentityPublic",
+ "RadrootsIdentitySecretKeyFormat",
+ "radroots_username_is_valid",
+ "radroots_username_normalize",
+];
+
+const REQUIRED_IDENTITY_STORAGE_EXPORTS: &[&str] = &[
+ "RADROOTS_ENCRYPTED_IDENTITY_DEFAULT_KEY_SLOT",
+ "RADROOTS_ENCRYPTED_IDENTITY_KEY_SUFFIX",
+ "RadrootsEncryptedIdentityFile",
+ "encrypted_identity_wrapping_key_path",
+ "load_encrypted_identity",
+ "load_encrypted_identity_with_key_slot",
+ "load_identity_profile",
+ "rotate_encrypted_identity",
+ "rotate_encrypted_identity_with_key_slot",
+ "store_encrypted_identity",
+ "store_encrypted_identity_with_key_slot",
+ "store_identity_profile",
+];
+
const REQUIRED_TRADE_POLICY_EXPORTS: &[&str] = &[
"AckPolicy",
"PublishMode",
@@ -507,6 +538,51 @@ fn dvm_runtime_public_exports_are_explicit() {
}
#[test]
+fn identity_public_surface_is_an_explicit_feature_module() {
+ let lib_source = read_source(
+ Path::new(env!("CARGO_MANIFEST_DIR"))
+ .join("src/lib.rs")
+ .as_path(),
+ );
+ let identity_source = read_source(
+ Path::new(env!("CARGO_MANIFEST_DIR"))
+ .join("src/identity.rs")
+ .as_path(),
+ );
+
+ assert!(
+ lib_source
+ .lines()
+ .any(|line| line.trim() == "pub mod identity;"),
+ "src/lib.rs must expose identity as an explicit feature-gated public module"
+ );
+ assert!(
+ lib_source
+ .lines()
+ .all(|line| line.trim() != "mod identity;"),
+ "src/lib.rs must not hide identity exports in a private module"
+ );
+ assert!(
+ !lib_source.contains("pub use crate::identity::{"),
+ "src/lib.rs must not flatten identity exports into root aliases"
+ );
+
+ for export in REQUIRED_IDENTITY_MODEL_EXPORTS {
+ assert!(
+ identity_source.contains(export),
+ "src/identity.rs must expose identity model export `{export}`"
+ );
+ }
+
+ for export in REQUIRED_IDENTITY_STORAGE_EXPORTS {
+ assert!(
+ identity_source.contains(export),
+ "src/identity.rs must expose identity storage export `{export}`"
+ );
+ }
+}
+
+#[test]
fn orders_client_surface_is_inventory_guarded() {
let source = read_source(
Path::new(env!("CARGO_MANIFEST_DIR"))