lit_rust_sdk/
session.rs

1use crate::auth::{AuthConfig, AuthSig, LitAbility, SessionKeyPair};
2use crate::error::LitSdkError;
3use chrono::Timelike;
4use ed25519_dalek::{Signer, SigningKey};
5use ethers::types::U256;
6use serde_json::json;
7use std::collections::HashMap;
8
9/// Issue session signatures per node URL.
10pub fn issue_session_sigs(
11    session_key_pair: &SessionKeyPair,
12    auth_config: &AuthConfig,
13    delegation_auth_sig: &AuthSig,
14    node_urls: &[String],
15) -> Result<HashMap<String, AuthSig>, LitSdkError> {
16    issue_session_sigs_with_max_price(
17        session_key_pair,
18        auth_config,
19        delegation_auth_sig,
20        node_urls,
21        None,
22    )
23}
24
25/// Issue session signatures per node URL with an optional maxPrice (wei) override.
26///
27/// When max_price_per_node_wei is None, defaults to unlimited (u128::MAX) to mirror JS behavior
28/// for unpriced flows. For priced flows (payments/sponsorship), pass a per-node wei cap that
29/// keeps total max spend within the sponsor/user budget.
30pub fn issue_session_sigs_with_max_price(
31    session_key_pair: &SessionKeyPair,
32    auth_config: &AuthConfig,
33    delegation_auth_sig: &AuthSig,
34    node_urls: &[String],
35    max_price_per_node_wei: Option<U256>,
36) -> Result<HashMap<String, AuthSig>, LitSdkError> {
37    let mut capabilities = auth_config.capability_auth_sigs.clone();
38    capabilities.push(delegation_auth_sig.clone());
39
40    // JS uses `new Date().toISOString()` for issuedAt (millis precision, Z suffix).
41    let now = chrono::Utc::now();
42    let issued_at = now
43        .with_nanosecond((now.nanosecond() / 1_000_000) * 1_000_000)
44        .expect("valid nanosecond")
45        .format("%Y-%m-%dT%H:%M:%S%.3fZ")
46        .to_string();
47
48    let template = json!({
49        "sessionKey": session_key_pair.public_key,
50        "resourceAbilityRequests": auth_config.resources.iter().filter(|r| r.ability != LitAbility::ResolvedAuthContext).map(|r| {
51            json!({
52                "resource": {
53                    "resourcePrefix": r.ability.resource_prefix(),
54                    "resource": r.resource_id,
55                },
56                "ability": r.ability.as_str(),
57                "data": r.data.clone().unwrap_or_else(|| json!({})),
58            })
59        }).collect::<Vec<_>>(),
60        "capabilities": capabilities,
61        "issuedAt": issued_at,
62        "expiration": auth_config.expiration,
63    });
64
65    let secret_bytes = hex::decode(session_key_pair.secret_key.trim_start_matches("0x"))
66        .map_err(|e| LitSdkError::Crypto(e.to_string()))?;
67    let signing_key = SigningKey::from_bytes(
68        &secret_bytes
69            .try_into()
70            .map_err(|_| LitSdkError::Crypto("invalid session secret key length".into()))?,
71    );
72
73    let mut out = HashMap::new();
74    // By default, mirror JS unlimited maxPrice (Unsigned 128 max) if pricing is not provided.
75    let max_price_hex = if let Some(p) = max_price_per_node_wei {
76        format!("0x{:x}", p)
77    } else {
78        format!("0x{:x}", u128::MAX)
79    };
80
81    for url in node_urls {
82        let mut to_sign = template.clone();
83        if let Some(obj) = to_sign.as_object_mut() {
84            obj.insert("nodeAddress".into(), json!(url));
85            obj.insert("maxPrice".into(), json!(max_price_hex.clone()));
86        }
87        let signed_message =
88            serde_json::to_string(&to_sign).map_err(|e| LitSdkError::Crypto(e.to_string()))?;
89        let sig = signing_key.sign(signed_message.as_bytes());
90
91        out.insert(
92            url.clone(),
93            AuthSig {
94                sig: hex::encode(sig.to_bytes()),
95                derived_via: "litSessionSignViaNacl".into(),
96                signed_message,
97                address: session_key_pair.public_key.clone(),
98                algo: Some("ed25519".into()),
99            },
100        );
101    }
102
103    Ok(out)
104}