From b42ed37850bec0d4b2c43b3a5958eef133b6041b Mon Sep 17 00:00:00 2001 From: bordumb Date: Thu, 12 Mar 2026 13:35:38 +0000 Subject: [PATCH 1/2] feat: add SealType::IdpBinding variant and anchor_idp_binding (fn-69.1) --- crates/auths-id/src/keri/anchor.rs | 20 +++++++++++++ crates/auths-id/src/keri/mod.rs | 4 +-- crates/auths-id/src/keri/seal.rs | 48 ++++++++++++++++++++++++++++++ docs/plans/launch_cleaning.md | 2 +- 4 files changed, 71 insertions(+), 3 deletions(-) diff --git a/crates/auths-id/src/keri/anchor.rs b/crates/auths-id/src/keri/anchor.rs index 92f530d2..240d088f 100644 --- a/crates/auths-id/src/keri/anchor.rs +++ b/crates/auths-id/src/keri/anchor.rs @@ -160,6 +160,26 @@ pub fn anchor_attestation( ) } +/// Anchor an IdP binding in the KEL. +/// +/// This is a convenience wrapper for `anchor_data` with the "idp-binding" seal type. +pub fn anchor_idp_binding( + repo: &Repository, + prefix: &Prefix, + binding: &T, + current_keypair: &Ed25519KeyPair, + now: chrono::DateTime, +) -> Result { + anchor_data( + repo, + prefix, + binding, + SealType::IdpBinding, + current_keypair, + now, + ) +} + /// Find the IXN event that anchors a specific data digest. /// /// # Arguments diff --git a/crates/auths-id/src/keri/mod.rs b/crates/auths-id/src/keri/mod.rs index ece15e7e..69879a7b 100644 --- a/crates/auths-id/src/keri/mod.rs +++ b/crates/auths-id/src/keri/mod.rs @@ -121,8 +121,8 @@ pub mod witness_integration; #[cfg(feature = "git-storage")] pub use anchor::{ - AnchorError, AnchorVerification, anchor_attestation, anchor_data, find_anchor_event, - verify_anchor, verify_anchor_by_digest, verify_attestation_anchor_by_issuer, + AnchorError, AnchorVerification, anchor_attestation, anchor_data, anchor_idp_binding, + find_anchor_event, verify_anchor, verify_anchor_by_digest, verify_attestation_anchor_by_issuer, }; pub use event::{Event, EventReceipts, IcpEvent, IxnEvent, KeriSequence, RotEvent}; #[cfg(feature = "git-storage")] diff --git a/crates/auths-id/src/keri/seal.rs b/crates/auths-id/src/keri/seal.rs index f004f251..10bfcfd2 100644 --- a/crates/auths-id/src/keri/seal.rs +++ b/crates/auths-id/src/keri/seal.rs @@ -12,10 +12,12 @@ use super::types::Said; /// Type of data anchored by a seal. #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] #[serde(rename_all = "kebab-case")] +#[non_exhaustive] pub enum SealType { DeviceAttestation, Revocation, Delegation, + IdpBinding, } impl fmt::Display for SealType { @@ -24,6 +26,7 @@ impl fmt::Display for SealType { SealType::DeviceAttestation => write!(f, "device-attestation"), SealType::Revocation => write!(f, "revocation"), SealType::Delegation => write!(f, "delegation"), + SealType::IdpBinding => write!(f, "idp-binding"), } } } @@ -68,6 +71,11 @@ impl Seal { pub fn delegation(delegation_digest: impl Into) -> Self { Self::new(delegation_digest, SealType::Delegation) } + + /// Create a seal for an IdP binding. + pub fn idp_binding(binding_digest: impl Into) -> Self { + Self::new(binding_digest, SealType::IdpBinding) + } } #[cfg(test)] @@ -104,4 +112,44 @@ mod tests { let parsed: Seal = serde_json::from_str(&json).unwrap(); assert_eq!(original, parsed); } + + #[test] + fn seal_creates_idp_binding() { + let seal = Seal::idp_binding("EBindingDigest"); + assert_eq!(seal.seal_type, SealType::IdpBinding); + assert_eq!(seal.d, "EBindingDigest"); + } + + #[test] + fn seal_idp_binding_serializes() { + let seal = Seal::idp_binding("ETest"); + let json = serde_json::to_string(&seal).unwrap(); + assert!(json.contains(r#""type":"idp-binding""#)); + } + + #[test] + fn seal_idp_binding_deserializes() { + let json = r#"{"d":"EDigest","type":"idp-binding"}"#; + let seal: Seal = serde_json::from_str(json).unwrap(); + assert_eq!(seal.seal_type, SealType::IdpBinding); + } + + #[test] + fn seal_idp_binding_roundtrips() { + let original = Seal::idp_binding("EBinding123"); + let json = serde_json::to_string(&original).unwrap(); + let parsed: Seal = serde_json::from_str(&json).unwrap(); + assert_eq!(original, parsed); + } + + #[test] + fn seal_type_display() { + assert_eq!( + SealType::DeviceAttestation.to_string(), + "device-attestation" + ); + assert_eq!(SealType::Revocation.to_string(), "revocation"); + assert_eq!(SealType::Delegation.to_string(), "delegation"); + assert_eq!(SealType::IdpBinding.to_string(), "idp-binding"); + } } diff --git a/docs/plans/launch_cleaning.md b/docs/plans/launch_cleaning.md index b03934e2..352809c8 100644 --- a/docs/plans/launch_cleaning.md +++ b/docs/plans/launch_cleaning.md @@ -354,7 +354,7 @@ These are the features that separate "impressive developer tool" from "enterpris --- -#### Epic 7: Enterprise SAML/OIDC Identity Binding +~~#### Epic 7: Enterprise SAML/OIDC Identity Binding~~ **Why it matters:** A CISO cannot mandate Auths if device identity cannot be tied to the corporate IdP. This is the single most common enterprise procurement question for developer security tools. **Scope:** From 44309e38aa35e4449a03fec25fc20b9ffcba05ad Mon Sep 17 00:00:00 2001 From: bordumb Date: Thu, 12 Mar 2026 15:25:43 +0000 Subject: [PATCH 2/2] refactor: make expires_in with seconds the single source of truth --- .pre-commit-config.yaml | 6 +- crates/auths-cli/src/commands/artifact/mod.rs | 10 +- .../auths-cli/src/commands/artifact/sign.rs | 4 +- .../src/commands/device/authorization.rs | 32 ++--- crates/auths-cli/src/commands/init/display.rs | 4 +- crates/auths-cli/src/commands/scim.rs | 12 +- crates/auths-cli/src/commands/sign.rs | 8 +- ...__status__tests__status_json_snapshot.snap | 8 +- ...atus__tests__status_json_snapshot.snap.new | 49 ++++++++ crates/auths-cli/src/commands/status.rs | 84 ++++++++----- crates/auths-core/src/ports/platform.rs | 2 +- crates/auths-id/src/agent_identity.rs | 18 +-- crates/auths-jwt/src/claims.rs | 112 ++++++++++++++++++ crates/auths-jwt/src/lib.rs | 2 +- crates/auths-policy/tests/cases/approval.rs | 8 +- crates/auths-sdk/README.md | 2 +- crates/auths-sdk/src/device.rs | 8 +- crates/auths-sdk/src/setup.rs | 2 +- crates/auths-sdk/src/signing.rs | 12 +- crates/auths-sdk/src/types.rs | 26 ++-- crates/auths-sdk/src/workflows/mcp.rs | 1 + crates/auths-sdk/tests/cases/artifact.rs | 6 +- crates/auths-sdk/tests/cases/device.rs | 6 +- docs/cli/commands/advanced.md | 10 +- docs/cli/commands/primary.md | 2 +- docs/guides/identity/multi-device.md | 4 +- docs/sdk/python/quickstart.md | 4 +- docs/sdk/rust/identity-operations.md | 2 +- docs/sdk/rust/signing-and-verification.md | 2 +- packages/auths-node/src/artifact.rs | 12 +- packages/auths-node/src/device.rs | 12 +- packages/auths-node/src/identity.rs | 6 +- .../auths-python/python/auths/__init__.pyi | 12 +- packages/auths-python/python/auths/_client.py | 12 +- packages/auths-python/python/auths/devices.py | 8 +- .../auths-python/python/auths/identity.py | 6 +- packages/auths-python/src/artifact_sign.rs | 20 ++-- packages/auths-python/src/device_ext.rs | 14 +-- packages/auths-python/src/identity.rs | 16 +-- packages/auths-python/tests/test_identity.py | 2 +- packages/auths-python/tests/test_rotation.py | 4 +- scripts/test_cli_default | 2 +- tests/e2e/test_device_attestation.py | 8 +- 43 files changed, 379 insertions(+), 201 deletions(-) create mode 100644 crates/auths-cli/src/commands/snapshots/auths_cli__commands__status__tests__status_json_snapshot.snap.new diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 14428921..67657da7 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -35,9 +35,9 @@ repos: types: [rust] pass_filenames: false - - id: gen-docs-check - name: cargo xtask gen-docs --check - entry: cargo run --package xtask -- gen-docs --check + - id: gen-docs + name: cargo xtask gen-docs (auto-fix) + entry: bash -c 'cargo run --package xtask -- gen-docs && git add docs/cli/commands/' language: system files: (crates/auths-cli/src/|crates/xtask/src/gen_docs|docs/cli/commands/) pass_filenames: false diff --git a/crates/auths-cli/src/commands/artifact/mod.rs b/crates/auths-cli/src/commands/artifact/mod.rs index 4828a4ff..b19bc462 100644 --- a/crates/auths-cli/src/commands/artifact/mod.rs +++ b/crates/auths-cli/src/commands/artifact/mod.rs @@ -47,9 +47,9 @@ pub enum ArtifactSubcommand { )] device_key_alias: String, - /// Number of days until the signature expires. - #[arg(long, visible_alias = "days", value_name = "N")] - expires_in_days: Option, + /// Duration in seconds until expiration (per RFC 6749). + #[arg(long = "expires-in", value_name = "N")] + expires_in: Option, /// Optional note to embed in the attestation. #[arg(long)] @@ -112,14 +112,14 @@ pub fn handle_artifact( sig_output, identity_key_alias, device_key_alias, - expires_in_days, + expires_in, note, } => sign::handle_sign( &file, sig_output, identity_key_alias.as_deref(), &device_key_alias, - expires_in_days, + expires_in, note, repo_opt, passphrase_provider, diff --git a/crates/auths-cli/src/commands/artifact/sign.rs b/crates/auths-cli/src/commands/artifact/sign.rs index 336dcb4b..2e1a07b7 100644 --- a/crates/auths-cli/src/commands/artifact/sign.rs +++ b/crates/auths-cli/src/commands/artifact/sign.rs @@ -17,7 +17,7 @@ pub fn handle_sign( output: Option, identity_key_alias: Option<&str>, device_key_alias: &str, - expires_in_days: Option, + expires_in: Option, note: Option, repo_opt: Option, passphrase_provider: Arc, @@ -32,7 +32,7 @@ pub fn handle_sign( identity_key: identity_key_alias .map(|a| SigningKeyMaterial::Alias(KeyAlias::new_unchecked(a))), device_key: SigningKeyMaterial::Alias(KeyAlias::new_unchecked(device_key_alias)), - expires_in_days: expires_in_days.map(|d| d as u32), + expires_in, note, }; diff --git a/crates/auths-cli/src/commands/device/authorization.rs b/crates/auths-cli/src/commands/device/authorization.rs index 09593cbd..75bb15e4 100644 --- a/crates/auths-cli/src/commands/device/authorization.rs +++ b/crates/auths-cli/src/commands/device/authorization.rs @@ -97,13 +97,13 @@ pub enum DeviceSubcommand { )] schema: Option, + /// Duration in seconds until expiration (per RFC 6749). #[arg( - long, - visible_alias = "days", - value_name = "DAYS", - help = "Optional number of days until this device authorization expires." + long = "expires-in", + value_name = "SECS", + help = "Optional number of seconds until this device authorization expires." )] - expires_in_days: Option, + expires_in: Option, #[arg( long, @@ -167,13 +167,13 @@ pub enum DeviceSubcommand { )] device_did: String, + /// Duration in seconds until expiration (per RFC 6749). #[arg( - long = "expires-in-days", - visible_alias = "days", - value_name = "DAYS", - help = "Number of days to extend the expiration by (from now)." + long = "expires-in", + value_name = "SECS", + help = "Number of seconds to extend the expiration by (from now)." )] - expires_in_days: i64, + expires_in: u64, #[arg( long = "identity-key-alias", @@ -236,7 +236,7 @@ pub fn handle_device( device_did, payload: payload_path_opt, schema: schema_path_opt, - expires_in_days, + expires_in, note, capabilities, } => { @@ -254,7 +254,7 @@ pub fn handle_device( device_key_alias: Some(KeyAlias::new_unchecked(device_key_alias)), device_did: Some(device_did.clone()), capabilities: caps, - expires_in_days: expires_in_days.map(|d| d as u32), + expires_in, note, payload, }; @@ -308,14 +308,14 @@ pub fn handle_device( DeviceSubcommand::Extend { device_did, - expires_in_days, + expires_in, identity_key_alias, device_key_alias, } => handle_extend( &repo_path, &config, &device_did, - expires_in_days, + expires_in, &identity_key_alias, &device_key_alias, passphrase_provider, @@ -427,7 +427,7 @@ fn handle_extend( repo_path: &Path, _config: &StorageLayoutConfig, device_did: &str, - days: i64, + expires_in: u64, identity_key_alias: &str, device_key_alias: &str, passphrase_provider: Arc, @@ -437,7 +437,7 @@ fn handle_extend( repo_path: repo_path.to_path_buf(), #[allow(clippy::disallowed_methods)] // INVARIANT: device_did from CLI arg validated upstream device_did: auths_verifier::types::DeviceDID::new_unchecked(device_did), - days: days as u32, + expires_in, identity_key_alias: KeyAlias::new_unchecked(identity_key_alias), device_key_alias: Some(KeyAlias::new_unchecked(device_key_alias)), }; diff --git a/crates/auths-cli/src/commands/init/display.rs b/crates/auths-cli/src/commands/init/display.rs index 3b038ff0..e6274caa 100644 --- a/crates/auths-cli/src/commands/init/display.rs +++ b/crates/auths-cli/src/commands/init/display.rs @@ -78,7 +78,7 @@ pub(crate) fn display_agent_dry_run( out.newline(); out.println(&format!(" Storage: {}", config.registry_path.display())); out.println(&format!(" Capabilities: {:?}", config.capabilities)); - if let Some(secs) = config.expires_in_secs { + if let Some(secs) = config.expires_in { out.println(&format!(" Expires in: {}s", secs)); } out.newline(); @@ -86,7 +86,7 @@ pub(crate) fn display_agent_dry_run( let provisioning_config = auths_id::agent_identity::AgentProvisioningConfig { agent_name: config.alias.to_string(), capabilities: config.capabilities.iter().map(|c| c.to_string()).collect(), - expires_in_secs: config.expires_in_secs, + expires_in: config.expires_in, delegated_by: None, storage_mode: auths_id::agent_identity::AgentStorageMode::Persistent { repo_path: None }, }; diff --git a/crates/auths-cli/src/commands/scim.rs b/crates/auths-cli/src/commands/scim.rs index b612c836..a6ef8cd1 100644 --- a/crates/auths-cli/src/commands/scim.rs +++ b/crates/auths-cli/src/commands/scim.rs @@ -96,9 +96,9 @@ pub struct ScimAddTenantCommand { /// PostgreSQL connection URL. #[arg(long)] pub database_url: String, - /// Token expiry duration (e.g., 90d, 365d). Omit for no expiry. - #[arg(long)] - pub expires_in: Option, + /// Duration in seconds until expiration (per RFC 6749). + #[arg(long = "expires-in")] + pub expires_in: Option, } /// Rotate bearer token for an existing tenant. @@ -110,9 +110,9 @@ pub struct ScimRotateTokenCommand { /// PostgreSQL connection URL. #[arg(long)] pub database_url: String, - /// Token expiry duration (e.g., 90d, 365d). - #[arg(long)] - pub expires_in: Option, + /// Duration in seconds until expiration (per RFC 6749). + #[arg(long = "expires-in")] + pub expires_in: Option, } /// Show SCIM sync state statistics. diff --git a/crates/auths-cli/src/commands/sign.rs b/crates/auths-cli/src/commands/sign.rs index 3e1360dd..77362ed1 100644 --- a/crates/auths-cli/src/commands/sign.rs +++ b/crates/auths-cli/src/commands/sign.rs @@ -110,9 +110,9 @@ pub struct SignCommand { #[arg(long)] pub device_key_alias: Option, - /// Number of days until the signature expires (for artifact signing). - #[arg(long, visible_alias = "days", value_name = "N")] - pub expires_in_days: Option, + /// Duration in seconds until expiration (per RFC 6749). + #[arg(long = "expires-in", value_name = "N")] + pub expires_in: Option, /// Optional note to embed in the attestation (for artifact signing). #[arg(long)] @@ -142,7 +142,7 @@ pub fn handle_sign_unified( cmd.sig_output, cmd.identity_key_alias.as_deref(), &device_key_alias, - cmd.expires_in_days, + cmd.expires_in, cmd.note, repo_opt, passphrase_provider, diff --git a/crates/auths-cli/src/commands/snapshots/auths_cli__commands__status__tests__status_json_snapshot.snap b/crates/auths-cli/src/commands/snapshots/auths_cli__commands__status__tests__status_json_snapshot.snap index 224a4894..485442dc 100644 --- a/crates/auths-cli/src/commands/snapshots/auths_cli__commands__status__tests__status_json_snapshot.snap +++ b/crates/auths-cli/src/commands/snapshots/auths_cli__commands__status__tests__status_json_snapshot.snap @@ -18,7 +18,7 @@ expression: report "expiring_soon": [ { "device_did": "did:key:zExpiringSoon", - "expires_in_days": 3 + "expires_in": 259200 } ], "devices_detail": [ @@ -27,21 +27,21 @@ expression: report "status": "active", "revoked_at": null, "expires_at": "2025-09-13T12:00:00Z", - "expires_in_days": 90 + "expires_in": 7776000 }, { "device_did": "did:key:zExpiringSoon", "status": "expiring_soon", "revoked_at": null, "expires_at": "2025-06-18T12:00:00Z", - "expires_in_days": 3 + "expires_in": 259200 }, { "device_did": "did:key:zRevokedDevice", "status": "revoked", "revoked_at": "2025-06-05T12:00:00Z", "expires_at": "2025-08-04T12:00:00Z", - "expires_in_days": null + "expires_in": null } ] } diff --git a/crates/auths-cli/src/commands/snapshots/auths_cli__commands__status__tests__status_json_snapshot.snap.new b/crates/auths-cli/src/commands/snapshots/auths_cli__commands__status__tests__status_json_snapshot.snap.new new file mode 100644 index 00000000..ce7c8d31 --- /dev/null +++ b/crates/auths-cli/src/commands/snapshots/auths_cli__commands__status__tests__status_json_snapshot.snap.new @@ -0,0 +1,49 @@ +--- +source: crates/auths-cli/src/commands/status.rs +assertion_line: 453 +expression: report +--- +{ + "identity": { + "controller_did": "did:keri:ETestController123", + "alias": "dev-machine" + }, + "agent": { + "running": true, + "pid": 12345, + "socket_path": "/tmp/agent.sock" + }, + "devices": { + "linked": 2, + "revoked": 1, + "expiring_soon": [ + { + "device_did": "did:key:zExpiringSoon", + "expires_in_days": 3 + } + ], + "devices_detail": [ + { + "device_did": "did:key:zActiveDevice", + "status": "active", + "revoked_at": null, + "expires_at": "2025-09-13T12:00:00Z", + "expires_in_days": 90 + }, + { + "device_did": "did:key:zExpiringSoon", + "status": "expiring_soon", + "revoked_at": null, + "expires_at": "2025-06-18T12:00:00Z", + "expires_in_days": 3 + }, + { + "device_did": "did:key:zRevokedDevice", + "status": "revoked", + "revoked_at": "2025-06-05T12:00:00Z", + "expires_at": "2025-08-04T12:00:00Z", + "expires_in_days": null + } + ] + } +} diff --git a/crates/auths-cli/src/commands/status.rs b/crates/auths-cli/src/commands/status.rs index 9ed76b4c..3356b7ed 100644 --- a/crates/auths-cli/src/commands/status.rs +++ b/crates/auths-cli/src/commands/status.rs @@ -64,14 +64,16 @@ pub struct DeviceStatus { pub status: String, pub revoked_at: Option>, pub expires_at: Option>, - pub expires_in_days: Option, + /// Duration in seconds until expiration (per RFC 6749). + pub expires_in: Option, } /// Device that is expiring soon. #[derive(Debug, Serialize)] pub struct ExpiringDevice { pub device_did: String, - pub expires_in_days: i64, + /// Duration in seconds until expiration (per RFC 6749). + pub expires_in: i64, } /// Handle the status command. @@ -145,20 +147,18 @@ fn print_status(report: &StatusReport, now: DateTime) { } if !report.devices.expiring_soon.is_empty() { let expiring_count = report.devices.expiring_soon.len(); - let min_days = report + let min_secs = report .devices .expiring_soon .iter() - .map(|e| e.expires_in_days) + .map(|e| e.expires_in) .min() .unwrap_or(0); - if min_days == 0 { - parts.push(format!("{} expiring today", expiring_count)); - } else if min_days == 1 { - parts.push(format!("{} expiring in 1 day", expiring_count)); - } else { - parts.push(format!("{} expiring in {} days", expiring_count, min_days)); - } + parts.push(format!( + "{} expiring in {}", + expiring_count, + format_duration_human(min_secs) + )); } if parts.is_empty() { @@ -180,6 +180,27 @@ fn print_status(report: &StatusReport, now: DateTime) { } } +/// Format seconds into a human-readable duration string. +fn format_duration_human(secs: i64) -> String { + if secs < 0 { + return "expired".to_string(); + } + let days = secs / 86400; + let hours = (secs % 86400) / 3600; + let mins = (secs % 3600) / 60; + let remaining_secs = secs % 60; + + if days > 0 { + format!("{}d {}h", days, hours) + } else if hours > 0 { + format!("{}h {}m", hours, mins) + } else if mins > 0 { + format!("{}m {}s", mins, remaining_secs) + } else { + format!("{}s", remaining_secs) + } +} + /// Display color-coded device expiry information. fn display_device_expiry(expires_at: Option>, out: &Output, now: DateTime) { let Some(expires_at) = expires_at else { @@ -187,25 +208,24 @@ fn display_device_expiry(expires_at: Option>, out: &Output, now: D return; }; - let remaining = expires_at - now; - let days = remaining.num_days(); + let remaining_secs = (expires_at - now).num_seconds(); - let (label, color_fn): (&str, fn(&Output, &str) -> String) = match days { - d if d < 0 => ("EXPIRED", Output::error), - 0..=6 => ("expiring soon", Output::warn), - 7..=29 => ("expiring", Output::warn), + let (label, color_fn): (&str, fn(&Output, &str) -> String) = match remaining_secs { + s if s < 0 => ("EXPIRED", Output::error), + 0..=604_799 => ("expiring soon", Output::warn), + 604_800..=2_591_999 => ("expiring", Output::warn), _ => ("active", Output::success), }; let display = format!( - "{} ({}, {}d remaining)", + "{} ({}, {} remaining)", expires_at.format("%Y-%m-%d"), label, - days + format_duration_human(remaining_secs) ); out.println(&format!(" Expires: {}", color_fn(out, &display))); - if (0..=7).contains(&days) { + if (0..=604_800).contains(&remaining_secs) { out.print_warn(" Run `auths device extend` to renew."); } } @@ -305,14 +325,14 @@ fn load_devices_summary(repo_path: &PathBuf, now: DateTime) -> DevicesSumma let mut devices_detail = Vec::new(); for (device_did, att) in &latest_by_device { - let (status, expires_in_days) = compute_device_status(att, now); + let (status, expires_in) = compute_device_status(att, now); devices_detail.push(DeviceStatus { device_did: device_did.clone(), status, revoked_at: att.revoked_at, expires_at: att.expires_at, - expires_in_days, + expires_in, }); if att.is_revoked() { @@ -323,16 +343,16 @@ fn load_devices_summary(repo_path: &PathBuf, now: DateTime) -> DevicesSumma && expires_at <= threshold && expires_at > now { - let days_left = (expires_at - now).num_days(); + let secs_left = (expires_at - now).num_seconds(); expiring_soon.push(ExpiringDevice { device_did: device_did.clone(), - expires_in_days: days_left, + expires_in: secs_left, }); } } } - expiring_soon.sort_by_key(|e| e.expires_in_days); + expiring_soon.sort_by_key(|e| e.expires_in); DevicesSummary { linked, @@ -352,15 +372,15 @@ fn compute_device_status( match att.expires_at { None => ("active".to_string(), None), Some(expires_at) => { - let days = (expires_at - now).num_days(); + let secs = (expires_at - now).num_seconds(); let status = if expires_at < now { "expired" - } else if days <= 7 { + } else if secs <= 7 * 86400 { "expiring_soon" } else { "active" }; - (status.to_string(), Some(days)) + (status.to_string(), Some(secs)) } } } @@ -422,7 +442,7 @@ mod tests { revoked: 1, expiring_soon: vec![ExpiringDevice { device_did: "did:key:zExpiringSoon".to_string(), - expires_in_days: 3, + expires_in: 259_200, }], devices_detail: vec![ DeviceStatus { @@ -430,21 +450,21 @@ mod tests { status: "active".to_string(), revoked_at: None, expires_at: Some(now + Duration::days(90)), - expires_in_days: Some(90), + expires_in: Some(7_776_000), }, DeviceStatus { device_did: "did:key:zExpiringSoon".to_string(), status: "expiring_soon".to_string(), revoked_at: None, expires_at: Some(now + Duration::days(3)), - expires_in_days: Some(3), + expires_in: Some(259_200), }, DeviceStatus { device_did: "did:key:zRevokedDevice".to_string(), status: "revoked".to_string(), revoked_at: Some(now - Duration::days(10)), expires_at: Some(now + Duration::days(50)), - expires_in_days: None, + expires_in: None, }, ], }, diff --git a/crates/auths-core/src/ports/platform.rs b/crates/auths-core/src/ports/platform.rs index db811eb1..deab441d 100644 --- a/crates/auths-core/src/ports/platform.rs +++ b/crates/auths-core/src/ports/platform.rs @@ -87,7 +87,7 @@ pub struct DeviceCodeResponse { pub user_code: String, /// URL where the user enters `user_code`. pub verification_uri: String, - /// Lifetime of the device code in seconds. + /// Duration in seconds until expiration (per RFC 6749). pub expires_in: u64, /// Minimum polling interval in seconds. pub interval: u64, diff --git a/crates/auths-id/src/agent_identity.rs b/crates/auths-id/src/agent_identity.rs index 6219eb97..0501bc78 100644 --- a/crates/auths-id/src/agent_identity.rs +++ b/crates/auths-id/src/agent_identity.rs @@ -20,7 +20,7 @@ //! let config = AgentProvisioningConfig { //! agent_name: "ci-bot".to_string(), //! capabilities: vec!["sign_commit".to_string()], -//! expires_in_secs: Some(86400), +//! expires_in: Some(86400), //! delegated_by: Some(IdentityDID::new_unchecked("did:keri:Eabc123")), //! storage_mode: AgentStorageMode::Persistent { repo_path: None }, //! }; @@ -70,8 +70,8 @@ pub struct AgentProvisioningConfig { pub agent_name: String, /// Capabilities to grant (e.g., `["sign_commit", "pr:create"]`). pub capabilities: Vec, - /// Optional expiry in seconds from now. - pub expires_in_secs: Option, + /// Duration in seconds until expiration (per RFC 6749). + pub expires_in: Option, /// DID of the human who authorized this agent. pub delegated_by: Option, /// Storage mode (persistent or ephemeral). @@ -355,7 +355,7 @@ fn build_attestation_meta( config: &AgentProvisioningConfig, ) -> AttestationMetadata { let expires_at = config - .expires_in_secs + .expires_in .map(|s| now + chrono::Duration::seconds(s as i64)); AttestationMetadata { @@ -404,8 +404,8 @@ pub fn format_agent_toml(did: &str, key_alias: &str, config: &AgentProvisioningC out.push_str(&format!("\n[capabilities]\ngranted = [{}]\n", caps)); - if let Some(secs) = config.expires_in_secs { - out.push_str(&format!("\n[expiry]\nexpires_in_secs = {}\n", secs)); + if let Some(secs) = config.expires_in { + out.push_str(&format!("\n[expiry]\nexpires_in = {}\n", secs)); } out @@ -423,7 +423,7 @@ mod tests { let config = AgentProvisioningConfig { agent_name: "ci-bot".to_string(), capabilities: vec!["sign_commit".to_string(), "pr:create".to_string()], - expires_in_secs: Some(86400), + expires_in: Some(86400), delegated_by: Some(IdentityDID::new_unchecked("did:keri:Eabc123")), storage_mode: AgentStorageMode::Persistent { repo_path: None }, }; @@ -432,7 +432,7 @@ mod tests { assert!(toml.contains("did = \"did:keri:Eagent\"")); assert!(toml.contains("delegated_by = \"did:keri:Eabc123\"")); assert!(toml.contains("\"sign_commit\", \"pr:create\"")); - assert!(toml.contains("expires_in_secs = 86400")); + assert!(toml.contains("expires_in = 86400")); } #[test] @@ -440,7 +440,7 @@ mod tests { let config = AgentProvisioningConfig { agent_name: "solo".to_string(), capabilities: vec![], - expires_in_secs: None, + expires_in: None, delegated_by: None, storage_mode: AgentStorageMode::InMemory, }; diff --git a/crates/auths-jwt/src/claims.rs b/crates/auths-jwt/src/claims.rs index 6e701d44..6a28c3f7 100644 --- a/crates/auths-jwt/src/claims.rs +++ b/crates/auths-jwt/src/claims.rs @@ -57,6 +57,28 @@ pub struct OidcClaims { /// SPIFFE ID from verified X.509-SVID. #[serde(skip_serializing_if = "Option::is_none")] pub spiffe_id: Option, + /// IdP binding data (populated when identity has an enterprise IdP binding). + #[serde(skip_serializing_if = "Option::is_none")] + pub idp_binding: Option, +} + +/// IdP binding claim embedded in the JWT when an identity is bound to an enterprise IdP. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct IdpBindingClaim { + /// IdP issuer URL (e.g. "https://company.okta.com") or SAML entity ID. + pub idp_issuer: String, + /// IdP protocol used for the binding. + pub idp_protocol: String, + /// IdP-side subject identifier (oid@tid for Entra, sub for others). + pub subject: String, + /// Subject email for display/audit. + #[serde(skip_serializing_if = "Option::is_none")] + pub subject_email: Option, + /// When the IdP authentication occurred (Unix timestamp). + pub auth_time: u64, + /// Authentication context class reference. + #[serde(skip_serializing_if = "Option::is_none")] + pub auth_context_class: Option, } /// Witness quorum info embedded in the JWT. @@ -67,3 +89,93 @@ pub struct WitnessQuorumClaim { /// Number of witness receipts verified. pub verified: usize, } + +#[cfg(test)] +mod tests { + use super::*; + + fn make_base_claims() -> OidcClaims { + OidcClaims { + iss: "https://auth.example.com".into(), + sub: "did:keri:ETest".into(), + aud: "api.example.com".into(), + exp: 1700000000, + iat: 1699999000, + jti: "test-jti".into(), + keri_prefix: "ETest".into(), + target_provider: None, + capabilities: vec!["sign-commit".into()], + witness_quorum: None, + github_actor: None, + github_repository: None, + act: None, + spiffe_id: None, + idp_binding: None, + } + } + + #[test] + fn claims_without_idp_binding_omits_field() { + let claims = make_base_claims(); + let json = serde_json::to_string(&claims).unwrap(); + assert!(!json.contains("idp_binding")); + } + + #[test] + fn claims_without_idp_binding_deserializes_to_none() { + let json = r#"{ + "iss": "https://auth.example.com", + "sub": "did:keri:ETest", + "aud": "api.example.com", + "exp": 1700000000, + "iat": 1699999000, + "jti": "test-jti", + "keri_prefix": "ETest", + "capabilities": ["sign-commit"] + }"#; + let claims: OidcClaims = serde_json::from_str(json).unwrap(); + assert!(claims.idp_binding.is_none()); + } + + #[test] + fn claims_with_idp_binding_roundtrips() { + let mut claims = make_base_claims(); + claims.idp_binding = Some(IdpBindingClaim { + idp_issuer: "https://company.okta.com".into(), + idp_protocol: "oidc".into(), + subject: "alice@company.com".into(), + subject_email: Some("alice@company.com".into()), + auth_time: 1699998000, + auth_context_class: Some( + "urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport".into(), + ), + }); + + let json = serde_json::to_string(&claims).unwrap(); + assert!(json.contains("idp_binding")); + assert!(json.contains("company.okta.com")); + + let parsed: OidcClaims = serde_json::from_str(&json).unwrap(); + let binding = parsed.idp_binding.unwrap(); + assert_eq!(binding.idp_issuer, "https://company.okta.com"); + assert_eq!(binding.idp_protocol, "oidc"); + assert_eq!(binding.subject, "alice@company.com"); + assert_eq!(binding.subject_email.as_deref(), Some("alice@company.com")); + assert_eq!(binding.auth_time, 1699998000); + } + + #[test] + fn idp_binding_claim_optional_fields_skipped() { + let binding = IdpBindingClaim { + idp_issuer: "https://company.okta.com".into(), + idp_protocol: "oidc".into(), + subject: "alice".into(), + subject_email: None, + auth_time: 1699998000, + auth_context_class: None, + }; + let json = serde_json::to_string(&binding).unwrap(); + assert!(!json.contains("subject_email")); + assert!(!json.contains("auth_context_class")); + } +} diff --git a/crates/auths-jwt/src/lib.rs b/crates/auths-jwt/src/lib.rs index 19046910..d36c5584 100644 --- a/crates/auths-jwt/src/lib.rs +++ b/crates/auths-jwt/src/lib.rs @@ -5,4 +5,4 @@ mod claims; -pub use claims::{ActorClaim, OidcClaims, WitnessQuorumClaim}; +pub use claims::{ActorClaim, IdpBindingClaim, OidcClaims, WitnessQuorumClaim}; diff --git a/crates/auths-policy/tests/cases/approval.rs b/crates/auths-policy/tests/cases/approval.rs index 06a81fd9..768b876f 100644 --- a/crates/auths-policy/tests/cases/approval.rs +++ b/crates/auths-policy/tests/cases/approval.rs @@ -27,16 +27,12 @@ fn approval_gate_expr(cap_name: &str, approver: &str) -> Expr { } } -fn make_approval( - approver: &str, - request_hash: [u8; 32], - expires_in_secs: i64, -) -> ApprovalAttestation { +fn make_approval(approver: &str, request_hash: [u8; 32], expires_in: i64) -> ApprovalAttestation { ApprovalAttestation { jti: "test-jti-001".into(), approver_did: did(approver), request_hash, - expires_at: Utc::now() + Duration::seconds(expires_in_secs), + expires_at: Utc::now() + Duration::seconds(expires_in), approved_capabilities: vec![], } } diff --git a/crates/auths-sdk/README.md b/crates/auths-sdk/README.md index daf3fe59..3a2fe034 100644 --- a/crates/auths-sdk/README.md +++ b/crates/auths-sdk/README.md @@ -125,7 +125,7 @@ let config = DeviceLinkConfig { device_key_alias: Some("macbook-pro".into()), device_did: None, capabilities: vec!["sign-commit".into()], - expires_in_days: Some(365), + expires_in: Some(31_536_000), note: Some("Work laptop".into()), payload: None, }; diff --git a/crates/auths-sdk/src/device.rs b/crates/auths-sdk/src/device.rs index 11d374fe..0844a91b 100644 --- a/crates/auths-sdk/src/device.rs +++ b/crates/auths-sdk/src/device.rs @@ -46,8 +46,8 @@ fn build_attestation_params( meta: AttestationMetadata { timestamp: Some(now), expires_at: config - .expires_in_days - .map(|d| now + chrono::Duration::days(d as i64)), + .expires_in + .map(|s| now + chrono::Duration::seconds(s as i64)), note: config.note.clone(), }, capabilities: config.capabilities.clone(), @@ -158,7 +158,7 @@ pub fn revoke_device( /// duration only, it does not change what the device is permitted to do. /// /// Args: -/// * `config`: Extension parameters (device DID, days, key aliases, registry path). +/// * `config`: Extension parameters (device DID, seconds until expiration, key aliases, registry path). /// * `ctx`: Runtime context providing storage adapters, key material, and passphrase provider. /// * `clock`: Clock provider for timestamp generation. /// @@ -200,7 +200,7 @@ pub fn extend_device( let previous_expires_at = latest.expires_at; let now = clock.now(); - let new_expires_at = now + chrono::Duration::days(config.days as i64); + let new_expires_at = now + chrono::Duration::seconds(config.expires_in as i64); let meta = AttestationMetadata { note: latest.note.clone(), diff --git a/crates/auths-sdk/src/setup.rs b/crates/auths-sdk/src/setup.rs index 5659d47a..eb3e8e8d 100644 --- a/crates/auths-sdk/src/setup.rs +++ b/crates/auths-sdk/src/setup.rs @@ -148,7 +148,7 @@ fn initialize_agent( let provisioning_config = AgentProvisioningConfig { agent_name: config.alias.to_string(), capabilities: cap_strings, - expires_in_secs: config.expires_in_secs, + expires_in: config.expires_in, delegated_by: config.parent_identity_did.clone().map(|did| { #[allow(clippy::disallowed_methods)] // INVARIANT: parent_identity_did is supplied by the CLI after resolving from identity storage, which stores only validated did:keri: DIDs diff --git a/crates/auths-sdk/src/signing.rs b/crates/auths-sdk/src/signing.rs index b3cb4bb0..e97cc542 100644 --- a/crates/auths-sdk/src/signing.rs +++ b/crates/auths-sdk/src/signing.rs @@ -198,7 +198,7 @@ pub enum SigningKeyMaterial { /// artifact: Arc::new(my_artifact), /// identity_key: Some(SigningKeyMaterial::Alias("my-identity".into())), /// device_key: SigningKeyMaterial::Direct(my_seed), -/// expires_in_days: Some(365), +/// expires_in: Some(31_536_000), /// note: None, /// }; /// ``` @@ -209,8 +209,8 @@ pub struct ArtifactSigningParams { pub identity_key: Option, /// Device key source. Required to produce a dual-signed attestation. pub device_key: SigningKeyMaterial, - /// Number of days until the attestation expires. `None` means no expiry. - pub expires_in_days: Option, + /// Duration in seconds until expiration (per RFC 6749). + pub expires_in: Option, /// Optional human-readable annotation embedded in the attestation. pub note: Option, } @@ -376,7 +376,7 @@ fn resolve_required_key( /// artifact: Arc::new(FileArtifact::new(Path::new("release.tar.gz"))), /// identity_key: Some(SigningKeyMaterial::Alias("my-key".into())), /// device_key: SigningKeyMaterial::Direct(seed), -/// expires_in_days: Some(365), +/// expires_in: Some(31_536_000), /// note: None, /// }; /// let result = sign_artifact(params, &ctx)?; @@ -434,8 +434,8 @@ pub fn sign_artifact( let meta = AttestationMetadata { timestamp: Some(now), expires_at: params - .expires_in_days - .map(|d| now + chrono::Duration::days(d as i64)), + .expires_in + .map(|s| now + chrono::Duration::seconds(s as i64)), note: params.note, }; diff --git a/crates/auths-sdk/src/types.rs b/crates/auths-sdk/src/types.rs index 43f953a0..58e54286 100644 --- a/crates/auths-sdk/src/types.rs +++ b/crates/auths-sdk/src/types.rs @@ -440,8 +440,8 @@ pub struct CreateAgentIdentityConfig { pub parent_identity_did: Option, /// Path to the auths registry directory. pub registry_path: PathBuf, - /// Optional agent key expiration time in seconds. - pub expires_in_secs: Option, + /// Duration in seconds until expiration (per RFC 6749). + pub expires_in: Option, /// If true, construct state without persisting. pub dry_run: bool, } @@ -466,7 +466,7 @@ impl CreateAgentIdentityConfig { capabilities: Vec::new(), parent_identity_did: None, registry_path: registry_path.into(), - expires_in_secs: None, + expires_in: None, dry_run: false, } } @@ -479,7 +479,7 @@ pub struct CreateAgentIdentityConfigBuilder { capabilities: Vec, parent_identity_did: Option, registry_path: PathBuf, - expires_in_secs: Option, + expires_in: Option, dry_run: bool, } @@ -528,7 +528,7 @@ impl CreateAgentIdentityConfigBuilder { /// .build(); /// ``` pub fn with_expiry(mut self, secs: u64) -> Self { - self.expires_in_secs = Some(secs); + self.expires_in = Some(secs); self } @@ -557,7 +557,7 @@ impl CreateAgentIdentityConfigBuilder { capabilities: self.capabilities, parent_identity_did: self.parent_identity_did, registry_path: self.registry_path, - expires_in_secs: self.expires_in_secs, + expires_in: self.expires_in, dry_run: self.dry_run, } } @@ -568,7 +568,7 @@ impl CreateAgentIdentityConfigBuilder { /// Args: /// * `repo_path`: Path to the auths registry. /// * `device_did`: The DID of the device whose authorization to extend. -/// * `days`: Number of days from now for the new expiration. +/// * `expires_in`: Duration in seconds until expiration (per RFC 6749). /// * `identity_key_alias`: Keychain alias for the identity key (for re-signing). /// * `device_key_alias`: Keychain alias for the device key (for re-signing). /// @@ -577,7 +577,7 @@ impl CreateAgentIdentityConfigBuilder { /// let config = DeviceExtensionConfig { /// repo_path: PathBuf::from("/home/user/.auths"), /// device_did: "did:key:z6Mk...".into(), -/// days: 365, +/// expires_in: 31_536_000, /// identity_key_alias: "my-identity".into(), /// device_key_alias: "my-device".into(), /// }; @@ -588,8 +588,8 @@ pub struct DeviceExtensionConfig { pub repo_path: PathBuf, /// DID of the device whose authorization to extend. pub device_did: DeviceDID, - /// Number of days from now for the new expiration. - pub days: u32, + /// Duration in seconds until expiration (per RFC 6749). + pub expires_in: u64, /// Keychain alias for the identity signing key. pub identity_key_alias: KeyAlias, /// Keychain alias for the device signing key (pass `None` to skip device co-signing). @@ -635,7 +635,7 @@ pub struct IdentityRotationConfig { /// device_key_alias: Some("macbook-pro".into()), /// device_did: None, /// capabilities: vec!["sign-commit".into()], -/// expires_in_days: Some(365), +/// expires_in: Some(31_536_000), /// note: Some("Work laptop".into()), /// payload: None, /// }; @@ -650,8 +650,8 @@ pub struct DeviceLinkConfig { pub device_did: Option, /// Capabilities to grant to the linked device. pub capabilities: Vec, - /// Optional expiration period in days. - pub expires_in_days: Option, + /// Duration in seconds until expiration (per RFC 6749). + pub expires_in: Option, /// Optional human-readable note for the attestation. pub note: Option, /// Optional JSON payload to embed in the attestation. diff --git a/crates/auths-sdk/src/workflows/mcp.rs b/crates/auths-sdk/src/workflows/mcp.rs index 2dac4d80..63c9c58d 100644 --- a/crates/auths-sdk/src/workflows/mcp.rs +++ b/crates/auths-sdk/src/workflows/mcp.rs @@ -26,6 +26,7 @@ struct McpTokenResponse { #[allow(dead_code)] token_type: String, #[allow(dead_code)] + /// Duration in seconds until expiration (per RFC 6749). expires_in: u64, #[allow(dead_code)] subject: String, diff --git a/crates/auths-sdk/tests/cases/artifact.rs b/crates/auths-sdk/tests/cases/artifact.rs index 60080076..280b2bbe 100644 --- a/crates/auths-sdk/tests/cases/artifact.rs +++ b/crates/auths-sdk/tests/cases/artifact.rs @@ -126,7 +126,7 @@ fn sign_artifact_with_alias_keys_produces_valid_json() { artifact, identity_key: Some(SigningKeyMaterial::Alias(key_alias.clone())), device_key: SigningKeyMaterial::Alias(key_alias), - expires_in_days: Some(365), + expires_in: Some(31_536_000), note: Some("integration test".into()), }; @@ -156,7 +156,7 @@ fn sign_artifact_with_direct_device_key_produces_valid_json() { artifact, identity_key: Some(SigningKeyMaterial::Alias(key_alias)), device_key: SigningKeyMaterial::Direct(device_seed), - expires_in_days: None, + expires_in: None, note: None, }; @@ -178,7 +178,7 @@ fn sign_artifact_identity_not_found_returns_error() { artifact, identity_key: None, device_key: SigningKeyMaterial::Direct(device_seed), - expires_in_days: None, + expires_in: None, note: None, }; diff --git a/crates/auths-sdk/tests/cases/device.rs b/crates/auths-sdk/tests/cases/device.rs index cb234a06..025f8146 100644 --- a/crates/auths-sdk/tests/cases/device.rs +++ b/crates/auths-sdk/tests/cases/device.rs @@ -63,7 +63,7 @@ fn link_test_device(registry_path: &std::path::Path, key_alias: &KeyAlias) -> St device_key_alias: Some(KeyAlias::new_unchecked("device-key")), device_did: None, capabilities: vec![], - expires_in_days: Some(30), + expires_in: Some(2_592_000), note: Some("test device".into()), payload: None, }; @@ -99,7 +99,7 @@ fn extend_device_updates_expiry() { let config = DeviceExtensionConfig { repo_path: registry_path, device_did: auths_verifier::types::DeviceDID::new_unchecked(device_did.clone()), - days: 365, + expires_in: 31_536_000, identity_key_alias: key_alias.clone(), device_key_alias: Some(KeyAlias::new_unchecked("device-key")), }; @@ -134,7 +134,7 @@ fn extend_device_nonexistent_device_returns_error() { let config = DeviceExtensionConfig { repo_path: registry_path, device_did: auths_verifier::types::DeviceDID::new_unchecked("did:key:zDoesNotExist"), - days: 30, + expires_in: 2_592_000, identity_key_alias: key_alias, device_key_alias: Some(KeyAlias::new_unchecked("device-key")), }; diff --git a/docs/cli/commands/advanced.md b/docs/cli/commands/advanced.md index bb32ca1b..07ddaf1a 100644 --- a/docs/cli/commands/advanced.md +++ b/docs/cli/commands/advanced.md @@ -18,7 +18,7 @@ Authorize a new device to act on behalf of the identity | `--device-did ` | — | Identity ID of the new device being authorized (must match device-key-alias). [aliases: --device] | | `--payload ` | — | Optional path to a JSON file containing arbitrary payload data for the authorization. | | `--schema ` | — | Optional path to a JSON schema for validating the payload (experimental). | -| `--expires-in-days ` | — | Optional number of days until this device authorization expires. [aliases: --days] | +| `--expires-in ` | — | Optional number of seconds until this device authorization expires. | | `--note ` | — | Optional description/note for this device authorization. | | `--capabilities ` | — | Permissions to grant this device (comma-separated) | | `--json` | — | Emit machine-readable JSON | @@ -62,7 +62,7 @@ Extend the expiration date of an existing device authorization | Flag | Default | Description | |------|---------|-------------| | `--device-did ` | — | Identity ID of the device authorization to extend. [aliases: --device] | -| `--expires-in-days ` | — | Number of days to extend the expiration by (from now). [aliases: --days] | +| `--expires-in ` | — | Number of seconds to extend the expiration by (from now). | | `--identity-key-alias ` | — | Local alias of the *identity's* key (required for re-signing). [aliases: --ika] | | `--device-key-alias ` | — | Local alias of the *device's* key (required for re-signing). [aliases: --dka] | | `--json` | — | Emit machine-readable JSON | @@ -897,7 +897,7 @@ Generate a new bearer token for an IdP tenant |------|---------|-------------| | `--name ` | — | Tenant name | | `--database-url ` | — | PostgreSQL connection URL | -| `--expires-in ` | — | Token expiry duration (e.g., 90d, 365d). Omit for no expiry | +| `--expires-in ` | — | Duration in seconds until expiration (per RFC 6749) | | `--json` | — | Emit machine-readable JSON | | `-q, --quiet` | — | Suppress non-essential output | | `--repo ` | — | Override the local storage directory (default: ~/.auths) | @@ -918,7 +918,7 @@ Rotate bearer token for an existing tenant |------|---------|-------------| | `--name ` | — | Tenant name | | `--database-url ` | — | PostgreSQL connection URL | -| `--expires-in ` | — | Token expiry duration (e.g., 90d, 365d) | +| `--expires-in ` | — | Duration in seconds until expiration (per RFC 6749) | | `--json` | — | Emit machine-readable JSON | | `-q, --quiet` | — | Suppress non-essential output | | `--repo ` | — | Override the local storage directory (default: ~/.auths) | @@ -1061,7 +1061,7 @@ Sign an artifact file with your Auths identity | `--sig-output ` | — | Output path for the signature file. Defaults to .auths.json | | `--identity-key-alias ` | — | Local alias of the identity key. Omit for device-only CI signing. [aliases: --ika] | | `--device-key-alias ` | — | Local alias of the device key (used for dual-signing). [aliases: --dka] | -| `--expires-in-days ` | — | Number of days until the signature expires [aliases: --days] | +| `--expires-in ` | — | Duration in seconds until expiration (per RFC 6749) | | `--note ` | — | Optional note to embed in the attestation | | `--json` | — | Emit machine-readable JSON | | `-q, --quiet` | — | Suppress non-essential output | diff --git a/docs/cli/commands/primary.md b/docs/cli/commands/primary.md index 5e440ae5..5bfa4b3d 100644 --- a/docs/cli/commands/primary.md +++ b/docs/cli/commands/primary.md @@ -41,7 +41,7 @@ Sign a Git commit or artifact file. | `--sig-output ` | — | Output path for the signature file. Defaults to .auths.json | | `--identity-key-alias ` | — | Local alias of the identity key (for artifact signing) | | `--device-key-alias ` | — | Local alias of the device key (for artifact signing, required for files) | -| `--expires-in-days ` | — | Number of days until the signature expires (for artifact signing) [aliases: --days] | +| `--expires-in ` | — | Duration in seconds until expiration (per RFC 6749) | | `--note ` | — | Optional note to embed in the attestation (for artifact signing) | | `--json` | — | Emit machine-readable JSON | | `-q, --quiet` | — | Suppress non-essential output | diff --git a/docs/guides/identity/multi-device.md b/docs/guides/identity/multi-device.md index 59bf0b04..0a9281b3 100644 --- a/docs/guides/identity/multi-device.md +++ b/docs/guides/identity/multi-device.md @@ -135,7 +135,7 @@ auths device link \ --device-key-alias laptop-key \ --device-did "$DEVICE_DID" \ --note "Work Laptop" \ - --expires-in-days 90 + --expires-in 7776000 ``` You will be prompted for passphrases for both the identity key and the device key, as the attestation requires dual signatures. @@ -146,7 +146,7 @@ Optional flags for `device link`: |------|-------------| | `--payload ` | Path to a JSON file with arbitrary payload data | | `--schema ` | JSON schema to validate the payload (experimental) | -| `--expires-in-days ` | Attestation expiry | +| `--expires-in ` | Attestation expiry | | `--note ` | Description for this device authorization | | `--capabilities ` | Comma-separated permissions to grant | diff --git a/docs/sdk/python/quickstart.md b/docs/sdk/python/quickstart.md index 77b577f9..f02e562e 100644 --- a/docs/sdk/python/quickstart.md +++ b/docs/sdk/python/quickstart.md @@ -25,7 +25,7 @@ print(result.signature_pem[:60] + "...") device = client.devices.link( identity.did, capabilities=["sign", "verify"], - expires_in_days=90, + expires_in=7_776_000, ) print(f"Device: {device.did}") ``` @@ -81,7 +81,7 @@ policy = ( signed = client.sign_artifact( "release.tar.gz", identity_did=identity.did, - expires_in_days=365, + expires_in=31_536_000, ) print(f"RID: {signed.rid}") print(f"Digest: {signed.digest}") diff --git a/docs/sdk/rust/identity-operations.md b/docs/sdk/rust/identity-operations.md index 838e56ce..3b037894 100644 --- a/docs/sdk/rust/identity-operations.md +++ b/docs/sdk/rust/identity-operations.md @@ -202,7 +202,7 @@ let config = DeviceLinkConfig { device_key_alias: Some(KeyAlias::new("macbook-pro")?), device_did: None, capabilities: vec!["sign-commit".into()], - expires_in_days: Some(365), + expires_in: Some(31_536_000), note: Some("Work laptop".into()), payload: None, }; diff --git a/docs/sdk/rust/signing-and-verification.md b/docs/sdk/rust/signing-and-verification.md index 328cb66a..109aa2fb 100644 --- a/docs/sdk/rust/signing-and-verification.md +++ b/docs/sdk/rust/signing-and-verification.md @@ -116,7 +116,7 @@ let params = ArtifactSigningParams { artifact: Arc::new(my_artifact_source), identity_key: Some(SigningKeyMaterial::Alias(KeyAlias::new("my-key")?)), device_key: SigningKeyMaterial::Direct(my_seed), - expires_in_days: Some(365), + expires_in: Some(31_536_000), note: Some("v1.0.0 release".into()), }; diff --git a/packages/auths-node/src/artifact.rs b/packages/auths-node/src/artifact.rs index 784073b7..44ecd160 100644 --- a/packages/auths-node/src/artifact.rs +++ b/packages/auths-node/src/artifact.rs @@ -99,7 +99,7 @@ fn build_context_and_sign( identity_key_alias: &str, repo_path: &str, passphrase: Option, - expires_in_days: Option, + expires_in: Option, note: Option, ) -> napi::Result { let passphrase_str = resolve_passphrase(passphrase); @@ -145,7 +145,7 @@ fn build_context_and_sign( artifact, identity_key: Some(SigningKeyMaterial::Alias(alias.clone())), device_key: SigningKeyMaterial::Alias(alias), - expires_in_days, + expires_in: expires_in.map(|s| s as u64), note, }; @@ -170,7 +170,7 @@ pub fn sign_artifact( identity_key_alias: String, repo_path: String, passphrase: Option, - expires_in_days: Option, + expires_in: Option, note: Option, ) -> napi::Result { let path = PathBuf::from(shellexpand::tilde(&file_path).as_ref()); @@ -189,7 +189,7 @@ pub fn sign_artifact( &identity_key_alias, &repo_path, passphrase, - expires_in_days, + expires_in, note, ) } @@ -200,7 +200,7 @@ pub fn sign_artifact_bytes( identity_key_alias: String, repo_path: String, passphrase: Option, - expires_in_days: Option, + expires_in: Option, note: Option, ) -> napi::Result { let artifact = Arc::new(BytesArtifact { @@ -211,7 +211,7 @@ pub fn sign_artifact_bytes( &identity_key_alias, &repo_path, passphrase, - expires_in_days, + expires_in, note, ) } diff --git a/packages/auths-node/src/device.rs b/packages/auths-node/src/device.rs index ac089e54..3dec6058 100644 --- a/packages/auths-node/src/device.rs +++ b/packages/auths-node/src/device.rs @@ -36,7 +36,7 @@ pub fn link_device_to_identity( capabilities: Vec, repo_path: String, passphrase: Option, - expires_in_days: Option, + expires_in: Option, ) -> napi::Result { let passphrase_str = resolve_passphrase(passphrase); let env_config = make_env_config(&passphrase_str, &repo_path); @@ -68,7 +68,7 @@ pub fn link_device_to_identity( device_key_alias: None, device_did: None, capabilities: parsed_caps, - expires_in_days, + expires_in: expires_in.map(|s| s as u64), note: None, payload: None, }; @@ -147,14 +147,14 @@ pub fn revoke_device_from_identity( pub fn extend_device_authorization( device_did: String, identity_key_alias: String, - days: u32, + expires_in: i64, repo_path: String, passphrase: Option, ) -> napi::Result { - if days == 0 { + if expires_in <= 0 { return Err(format_error( "AUTHS_INVALID_INPUT", - "days must be positive (> 0)", + "expires_in must be positive (> 0)", )); } @@ -180,7 +180,7 @@ pub fn extend_device_authorization( repo_path: repo, device_did: DeviceDID::parse(&device_did) .map_err(|e| format_error("AUTHS_INVALID_INPUT", e))?, - days, + expires_in: expires_in as u64, identity_key_alias: alias, device_key_alias: None, }; diff --git a/packages/auths-node/src/identity.rs b/packages/auths-node/src/identity.rs index d57d1e68..6382a414 100644 --- a/packages/auths-node/src/identity.rs +++ b/packages/auths-node/src/identity.rs @@ -158,7 +158,7 @@ pub fn create_agent_identity( device_key_alias: Some(result_alias.clone()), device_did: None, capabilities: parsed_caps, - expires_in_days: None, + expires_in: None, note: Some(format!("Agent: {}", agent_name)), payload: None, }; @@ -226,7 +226,7 @@ pub fn delegate_agent( capabilities: Vec, parent_repo_path: String, passphrase: Option, - expires_in_days: Option, + expires_in: Option, identity_did: Option, ) -> napi::Result { let passphrase_str = resolve_passphrase(passphrase); @@ -300,7 +300,7 @@ pub fn delegate_agent( device_key_alias: Some(agent_alias.clone()), device_did: None, capabilities: parsed_caps, - expires_in_days, + expires_in: expires_in.map(|s| s as u64), note: Some(format!("Agent: {}", agent_name)), payload: None, }; diff --git a/packages/auths-python/python/auths/__init__.pyi b/packages/auths-python/python/auths/__init__.pyi index e6fac0dc..99a82c14 100644 --- a/packages/auths-python/python/auths/__init__.pyi +++ b/packages/auths-python/python/auths/__init__.pyi @@ -21,8 +21,8 @@ class Auths: def sign_action(self, action_type: str, payload: str, identity_did: str, private_key: str) -> str: ... def verify_action(self, envelope_json: str, public_key: str) -> VerificationResult: ... def sign_commit(self, data: bytes, *, identity_did: str, passphrase: str | None = None) -> CommitSigningResult: ... - def sign_artifact(self, path: str, *, identity_did: str, expires_in_days: int | None = None, note: str | None = None) -> ArtifactSigningResult: ... - def sign_artifact_bytes(self, data: bytes, *, identity_did: str, expires_in_days: int | None = None, note: str | None = None) -> ArtifactSigningResult: ... + def sign_artifact(self, path: str, *, identity_did: str, expires_in: int | None = None, note: str | None = None) -> ArtifactSigningResult: ... + def sign_artifact_bytes(self, data: bytes, *, identity_did: str, expires_in: int | None = None, note: str | None = None) -> ArtifactSigningResult: ... def sign_as(self, message: bytes, identity: str, passphrase: str | None = None) -> str: ... def sign_action_as(self, action_type: str, payload: str, identity: str, passphrase: str | None = None) -> str: ... def get_public_key(self, identity: str, passphrase: str | None = None) -> str: ... @@ -143,7 +143,7 @@ class IdentityService: def create(self, label: str = "main", repo_path: str | None = None, passphrase: str | None = None) -> Identity: ... def rotate(self, identity_did: str, *, passphrase: str | None = None) -> IdentityRotationResult: ... def create_agent(self, name: str, capabilities: list[str], passphrase: str | None = None) -> AgentIdentity: ... - def delegate_agent(self, identity_did: str, name: str, capabilities: list[str], expires_in_days: int | None = None, passphrase: str | None = None) -> DelegatedAgent: ... + def delegate_agent(self, identity_did: str, name: str, capabilities: list[str], expires_in: int | None = None, passphrase: str | None = None) -> DelegatedAgent: ... # -- Device resources -- @@ -200,7 +200,7 @@ class AttestationService: def latest(self, device_did: str) -> Attestation | None: ... class DeviceService: - def link(self, identity_did: str, capabilities: list[str] | None = None, expires_in_days: int | None = None, passphrase: str | None = None) -> Device: ... + def link(self, identity_did: str, capabilities: list[str] | None = None, expires_in: int | None = None, passphrase: str | None = None) -> Device: ... def extend(self, device_did: str, identity_did: str, *, days: int = 90, passphrase: str | None = None) -> DeviceExtension: ... def revoke(self, device_did: str, identity_did: str, note: str | None = None, passphrase: str | None = None) -> None: ... @@ -252,8 +252,8 @@ class LayoutError(Exception): def sign_commit(data: bytes, identity_key_alias: str, repo_path: str, passphrase: str | None = None) -> CommitSigningResult: ... -def sign_artifact(file_path: str, identity_key_alias: str, repo_path: str, passphrase: str | None = None, expires_in_days: int | None = None, note: str | None = None) -> ArtifactSigningResult: ... -def sign_artifact_bytes(data: bytes, identity_key_alias: str, repo_path: str, passphrase: str | None = None, expires_in_days: int | None = None, note: str | None = None) -> ArtifactSigningResult: ... +def sign_artifact(file_path: str, identity_key_alias: str, repo_path: str, passphrase: str | None = None, expires_in: int | None = None, note: str | None = None) -> ArtifactSigningResult: ... +def sign_artifact_bytes(data: bytes, identity_key_alias: str, repo_path: str, passphrase: str | None = None, expires_in: int | None = None, note: str | None = None) -> ArtifactSigningResult: ... def list_attestations(repo_path: str) -> list[Attestation]: ... def list_attestations_by_device(repo_path: str, device_did: str) -> list[Attestation]: ... diff --git a/packages/auths-python/python/auths/_client.py b/packages/auths-python/python/auths/_client.py index 0175d07c..3d35b37a 100644 --- a/packages/auths-python/python/auths/_client.py +++ b/packages/auths-python/python/auths/_client.py @@ -541,7 +541,7 @@ def sign_artifact( path: str, *, identity_did: str, - expires_in_days: int | None = None, + expires_in: int | None = None, note: str | None = None, ) -> ArtifactSigningResult: """Sign a file artifact, producing a dual-signed attestation. @@ -552,7 +552,7 @@ def sign_artifact( Args: path: Path to the file to sign. identity_did: The identity DID to sign with (used as key alias). - expires_in_days: Optional expiry for the attestation. + expires_in: Duration in seconds until expiration (per RFC 6749). note: Optional human-readable note. Returns: @@ -573,7 +573,7 @@ def sign_artifact( pp = self._passphrase try: raw = _sign_artifact( - path, identity_did, self.repo_path, pp, expires_in_days, note, + path, identity_did, self.repo_path, pp, expires_in, note, ) return ArtifactSigningResult( attestation_json=raw.attestation_json, @@ -591,7 +591,7 @@ def sign_artifact_bytes( data: bytes, *, identity_did: str, - expires_in_days: int | None = None, + expires_in: int | None = None, note: str | None = None, ) -> ArtifactSigningResult: """Sign raw bytes, producing a dual-signed attestation. @@ -602,7 +602,7 @@ def sign_artifact_bytes( Args: data: The raw bytes to sign. identity_did: The identity DID to sign with (used as key alias). - expires_in_days: Optional expiry for the attestation. + expires_in: Duration in seconds until expiration (per RFC 6749). note: Optional human-readable note. Returns: @@ -622,7 +622,7 @@ def sign_artifact_bytes( pp = self._passphrase try: raw = _sign_artifact_bytes( - data, identity_did, self.repo_path, pp, expires_in_days, note, + data, identity_did, self.repo_path, pp, expires_in, note, ) return ArtifactSigningResult( attestation_json=raw.attestation_json, diff --git a/packages/auths-python/python/auths/devices.py b/packages/auths-python/python/auths/devices.py index 095a8968..9f0ec9b4 100644 --- a/packages/auths-python/python/auths/devices.py +++ b/packages/auths-python/python/auths/devices.py @@ -60,7 +60,7 @@ def link( self, identity_did: str, capabilities: list[str] | None = None, - expires_in_days: int | None = None, + expires_in: int | None = None, passphrase: str | None = None, ) -> Device: """Link a new device to an identity. @@ -68,7 +68,7 @@ def link( Args: identity_did: The identity to link this device to. capabilities: Device capabilities (default: []). - expires_in_days: Optional expiry in days. + expires_in: Duration in seconds until expiration (per RFC 6749). passphrase: Key passphrase override. Returns: @@ -80,7 +80,7 @@ def link( Examples: ```python - device = auths.devices.link(identity.did, capabilities=["sign"], expires_in_days=90) + device = auths.devices.link(identity.did, capabilities=["sign"], expires_in=7_776_000) ``` """ pp = passphrase or self._client._passphrase @@ -89,7 +89,7 @@ def link( capabilities or [], self._client.repo_path, pp, - expires_in_days, + expires_in, ) return Device(did=device_did, attestation_id=attestation_id) diff --git a/packages/auths-python/python/auths/identity.py b/packages/auths-python/python/auths/identity.py index dc74aad7..1e4d0be0 100644 --- a/packages/auths-python/python/auths/identity.py +++ b/packages/auths-python/python/auths/identity.py @@ -186,7 +186,7 @@ def delegate_agent( identity_did: str, name: str, capabilities: list[str], - expires_in_days: int | None = None, + expires_in: int | None = None, passphrase: str | None = None, ) -> DelegatedAgent: """Delegate an agent under an identity (`did:key:`). @@ -195,7 +195,7 @@ def delegate_agent( identity_did: The parent identity's DID. name: Human-readable agent name. capabilities: List of capabilities (e.g., ["sign", "verify"]). - expires_in_days: Optional TTL in days. + expires_in: Duration in seconds until expiration (per RFC 6749). passphrase: Key passphrase override. Returns: @@ -212,7 +212,7 @@ def delegate_agent( """ pp = passphrase or self._client._passphrase bundle = _delegate_agent( - name, capabilities, self._client.repo_path, pp, expires_in_days, + name, capabilities, self._client.repo_path, pp, expires_in, identity_did, ) return DelegatedAgent( diff --git a/packages/auths-python/src/artifact_sign.rs b/packages/auths-python/src/artifact_sign.rs index f87560ff..48c1d315 100644 --- a/packages/auths-python/src/artifact_sign.rs +++ b/packages/auths-python/src/artifact_sign.rs @@ -128,7 +128,7 @@ fn build_context_and_sign( identity_key_alias: &str, repo_path: &str, passphrase: Option, - expires_in_days: Option, + expires_in: Option, note: Option, ) -> PyResult { let passphrase_str = resolve_passphrase(passphrase); @@ -175,7 +175,7 @@ fn build_context_and_sign( artifact, identity_key: Some(SigningKeyMaterial::Alias(alias.clone())), device_key: SigningKeyMaterial::Alias(alias), - expires_in_days, + expires_in, note, }; @@ -200,7 +200,7 @@ fn build_context_and_sign( /// * `identity_key_alias`: Keychain alias for the identity key. /// * `repo_path`: Path to the auths repository. /// * `passphrase`: Optional passphrase for the keychain. -/// * `expires_in_days`: Optional expiry in days. +/// * `expires_in`: Duration in seconds until expiration (per RFC 6749). /// * `note`: Optional human-readable note. /// /// Usage: @@ -208,14 +208,14 @@ fn build_context_and_sign( /// let result = sign_artifact(py, "release.tar.gz", "main", "~/.auths", None, None, None)?; /// ``` #[pyfunction] -#[pyo3(signature = (file_path, identity_key_alias, repo_path, passphrase=None, expires_in_days=None, note=None))] +#[pyo3(signature = (file_path, identity_key_alias, repo_path, passphrase=None, expires_in=None, note=None))] pub fn sign_artifact( py: Python<'_>, file_path: &str, identity_key_alias: &str, repo_path: &str, passphrase: Option, - expires_in_days: Option, + expires_in: Option, note: Option, ) -> PyResult { let path = PathBuf::from(shellexpand::tilde(file_path).as_ref()); @@ -230,7 +230,7 @@ pub fn sign_artifact( let rp = repo_path.to_string(); py.allow_threads(move || { - build_context_and_sign(artifact, &alias, &rp, passphrase, expires_in_days, note) + build_context_and_sign(artifact, &alias, &rp, passphrase, expires_in, note) }) } @@ -241,7 +241,7 @@ pub fn sign_artifact( /// * `identity_key_alias`: Keychain alias for the identity key. /// * `repo_path`: Path to the auths repository. /// * `passphrase`: Optional passphrase for the keychain. -/// * `expires_in_days`: Optional expiry in days. +/// * `expires_in`: Duration in seconds until expiration (per RFC 6749). /// * `note`: Optional human-readable note. /// /// Usage: @@ -249,14 +249,14 @@ pub fn sign_artifact( /// let result = sign_artifact_bytes(py, b"manifest data", "main", "~/.auths", None, None, None)?; /// ``` #[pyfunction] -#[pyo3(signature = (data, identity_key_alias, repo_path, passphrase=None, expires_in_days=None, note=None))] +#[pyo3(signature = (data, identity_key_alias, repo_path, passphrase=None, expires_in=None, note=None))] pub fn sign_artifact_bytes( py: Python<'_>, data: &[u8], identity_key_alias: &str, repo_path: &str, passphrase: Option, - expires_in_days: Option, + expires_in: Option, note: Option, ) -> PyResult { let artifact = Arc::new(BytesArtifact { @@ -266,6 +266,6 @@ pub fn sign_artifact_bytes( let rp = repo_path.to_string(); py.allow_threads(move || { - build_context_and_sign(artifact, &alias, &rp, passphrase, expires_in_days, note) + build_context_and_sign(artifact, &alias, &rp, passphrase, expires_in, note) }) } diff --git a/packages/auths-python/src/device_ext.rs b/packages/auths-python/src/device_ext.rs index daf614a6..08cc472a 100644 --- a/packages/auths-python/src/device_ext.rs +++ b/packages/auths-python/src/device_ext.rs @@ -43,26 +43,26 @@ impl PyDeviceExtension { /// Args: /// * `device_did`: The DID of the device to extend. /// * `identity_key_alias`: Keychain alias for the identity key. -/// * `days`: Number of days to extend from now. +/// * `expires_in`: Duration in seconds until expiration (per RFC 6749). /// * `repo_path`: Path to the auths repository. /// * `passphrase`: Optional passphrase for the keychain. /// /// Usage: /// ```ignore -/// let result = extend_device_authorization_ffi(py, "did:key:...", "main", 90, "~/.auths", None)?; +/// let result = extend_device_authorization_ffi(py, "did:key:...", "main", 7_776_000, "~/.auths", None)?; /// ``` #[pyfunction] -#[pyo3(signature = (device_did, identity_key_alias, days, repo_path, passphrase=None))] +#[pyo3(signature = (device_did, identity_key_alias, expires_in, repo_path, passphrase=None))] pub fn extend_device_authorization_ffi( py: Python<'_>, device_did: &str, identity_key_alias: &str, - days: u32, + expires_in: u64, repo_path: &str, passphrase: Option, ) -> PyResult { - if days == 0 { - return Err(PyValueError::new_err("days must be positive (> 0)")); + if expires_in == 0 { + return Err(PyValueError::new_err("expires_in must be positive (> 0)")); } let passphrase_str = resolve_passphrase(passphrase); @@ -94,7 +94,7 @@ pub fn extend_device_authorization_ffi( repo_path: repo, device_did: DeviceDID::parse(device_did) .map_err(|e| PyValueError::new_err(format!("{e}")))?, - days, + expires_in, identity_key_alias: alias, device_key_alias: None, }; diff --git a/packages/auths-python/src/identity.rs b/packages/auths-python/src/identity.rs index 31d9b483..53fcc170 100644 --- a/packages/auths-python/src/identity.rs +++ b/packages/auths-python/src/identity.rs @@ -312,7 +312,7 @@ pub fn create_agent_identity( /// * `capabilities`: Capabilities to grant. /// * `parent_repo_path`: Path to the parent identity's repository. /// * `passphrase`: Optional passphrase for the keychain. -/// * `expires_in_days`: Optional expiry in days. +/// * `expires_in`: Duration in seconds until expiration (per RFC 6749). /// * `identity_did`: DID of the parent identity (did:keri:...). /// /// Usage: @@ -320,14 +320,14 @@ pub fn create_agent_identity( /// let bundle = delegate_agent(py, "ci-bot", vec!["sign".into()], "~/.auths", None, None, Some("did:keri:E..."))?; /// ``` #[pyfunction] -#[pyo3(signature = (agent_name, capabilities, parent_repo_path, passphrase=None, expires_in_days=None, identity_did=None))] +#[pyo3(signature = (agent_name, capabilities, parent_repo_path, passphrase=None, expires_in=None, identity_did=None))] pub fn delegate_agent( py: Python<'_>, agent_name: &str, capabilities: Vec, parent_repo_path: &str, passphrase: Option, - expires_in_days: Option, + expires_in: Option, identity_did: Option, ) -> PyResult { let passphrase_str = resolve_passphrase(passphrase); @@ -418,7 +418,7 @@ pub fn delegate_agent( device_key_alias: Some(agent_alias.clone()), device_did: None, capabilities: parsed_caps, - expires_in_days, + expires_in, note: Some(format!("Agent: {}", agent_name)), payload: None, }; @@ -483,21 +483,21 @@ pub fn delegate_agent( /// * `capabilities`: Capabilities to grant to the device. /// * `passphrase`: Optional passphrase for the keychain. /// * `repo_path`: Path to the auths repository. -/// * `expires_in_days`: Optional expiration period in days. +/// * `expires_in`: Duration in seconds until expiration (per RFC 6749). /// /// Usage: /// ```ignore /// let (device_did, att_id) = link_device_ffi(py, "my-id", vec!["sign".into()], None, "~/.auths", None)?; /// ``` #[pyfunction] -#[pyo3(signature = (identity_key_alias, capabilities, repo_path, passphrase=None, expires_in_days=None))] +#[pyo3(signature = (identity_key_alias, capabilities, repo_path, passphrase=None, expires_in=None))] pub fn link_device_to_identity( py: Python<'_>, identity_key_alias: &str, capabilities: Vec, repo_path: &str, passphrase: Option, - expires_in_days: Option, + expires_in: Option, ) -> PyResult<(String, String)> { let passphrase_str = resolve_passphrase(passphrase); let env_config = make_keychain_config(&passphrase_str, repo_path); @@ -538,7 +538,7 @@ pub fn link_device_to_identity( device_key_alias: None, device_did: None, capabilities: parsed_caps, - expires_in_days, + expires_in, note: None, payload: None, }; diff --git a/packages/auths-python/tests/test_identity.py b/packages/auths-python/tests/test_identity.py index 77342f2e..d24f52b3 100644 --- a/packages/auths-python/tests/test_identity.py +++ b/packages/auths-python/tests/test_identity.py @@ -42,7 +42,7 @@ def test_device_lifecycle(tmp_path): device = auths.devices.link( identity_did=identity.did, capabilities=["sign"], - expires_in_days=90, + expires_in=7_776_000, ) assert isinstance(device, Device) assert device.did.startswith("did:key:") diff --git a/packages/auths-python/tests/test_rotation.py b/packages/auths-python/tests/test_rotation.py index b658fe0d..1ea9136d 100644 --- a/packages/auths-python/tests/test_rotation.py +++ b/packages/auths-python/tests/test_rotation.py @@ -74,11 +74,11 @@ def test_rotate_with_two_delegated_agents(self): auths.identities.delegate_agent( operator.did, name="agent-a", - capabilities=["deploy:staging"], expires_in_days=7, + capabilities=["deploy:staging"], expires_in=604_800, ) auths.identities.delegate_agent( operator.did, name="agent-b", - capabilities=["audit"], expires_in_days=90, + capabilities=["audit"], expires_in=7_776_000, ) # Must NOT fail with "pre-committed next key '...-agent--next-0' not found" diff --git a/scripts/test_cli_default b/scripts/test_cli_default index f42835fc..9e8b4b86 100644 --- a/scripts/test_cli_default +++ b/scripts/test_cli_default @@ -129,7 +129,7 @@ echo -e "$PASSPHRASE_DEV\n$PASSPHRASE_ID\n$PASSPHRASE_DEV" | auths --repo "$REPO --device-key-alias "$DEVICE_ALIAS" \ --device-did "$DEVICE_DID" \ --note "Default layout test device" \ - --expires-in-days 30 + --expires-in 2592000 # --- Verification --- echo "" diff --git a/tests/e2e/test_device_attestation.py b/tests/e2e/test_device_attestation.py index b76a2b0e..a4cb8606 100644 --- a/tests/e2e/test_device_attestation.py +++ b/tests/e2e/test_device_attestation.py @@ -5,7 +5,7 @@ from helpers.cli import export_attestation, get_device_did, run_auths -def _link_device(auths_bin, env, *, capabilities=None, expires_in_days=None): +def _link_device(auths_bin, env, *, capabilities=None, expires_in=None): """Link a device and return the CLI result.""" did = get_device_did(auths_bin, env) args = [ @@ -20,8 +20,8 @@ def _link_device(auths_bin, env, *, capabilities=None, expires_in_days=None): ] if capabilities: args += ["--capabilities", capabilities] - if expires_in_days: - args += ["--expires-in-days", str(expires_in_days)] + if expires_in: + args += ["--expires-in", str(expires_in)] return run_auths(auths_bin, args, env=env) @@ -91,7 +91,7 @@ def test_attest_agent(self, auths_bin, init_identity): def test_attest_with_expiry(self, auths_bin, init_identity): result = _link_device( - auths_bin, init_identity, expires_in_days=1 + auths_bin, init_identity, expires_in=86_400 ) if result.returncode != 0: pytest.skip(f"device link with expiry not available: {result.stderr}")