1use super::{KeyType, registry::*, tempo_home};
4use alloy_primitives::{Address, B256, Selector, U256};
5use alloy_signer::Signer;
6use eyre::ensure;
7use foundry_wallets::{TempoAccessKeyConfig, WalletSigner};
8use serde::{Deserialize, Serialize};
9use std::{fmt, num::NonZeroU64, path::PathBuf};
10use tempo_primitives::transaction::{
11 CallScope, KeyAuthorization, SelectorRule, SignatureType, SignedKeyAuthorization, TokenLimit,
12};
13
14pub const WALLET_SESSIONS_PATH: &str = "wallet/sessions.toml";
16
17const SESSIONS_HEADER: &str =
18 "# Tempo session registry — managed by Foundry / Tempo CLI.\n# Do not edit manually.";
19
20#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Deserialize, Serialize)]
22#[serde(rename_all = "snake_case")]
23pub enum SessionStatus {
24 #[default]
25 Pending,
26 Active,
27 Revoking,
30 Revoked,
31 Expired,
32 Failed,
33}
34
35impl SessionStatus {
36 pub const fn is_terminal(self) -> bool {
38 matches!(self, Self::Revoked | Self::Expired | Self::Failed)
39 }
40
41 const fn clears_key_material(self) -> bool {
43 matches!(self, Self::Revoking) || self.is_terminal()
44 }
45
46 pub const fn is_live(self) -> bool {
49 !self.is_terminal()
50 }
51}
52
53#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
55pub struct SessionTokenLimit {
56 pub currency: Address,
57 pub limit: String,
58}
59
60#[derive(Clone, PartialEq, Eq, Deserialize, Serialize)]
66pub struct SessionKeyMaterial {
67 #[serde(default)]
68 pub key_type: KeyType,
69 pub key: String,
71 #[serde(default, skip_serializing_if = "Option::is_none")]
74 pub key_authorization: Option<String>,
75}
76
77impl fmt::Debug for SessionKeyMaterial {
79 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
80 f.debug_struct("SessionKeyMaterial")
81 .field("key_type", &self.key_type)
82 .field("key", &super::redacted_debug(&self.key))
83 .field(
84 "key_authorization",
85 &self.key_authorization.as_deref().map(super::redacted_debug),
86 )
87 .finish()
88 }
89}
90
91impl SessionKeyMaterial {
92 pub fn has_inline_key(&self) -> bool {
94 !self.key.trim().is_empty()
95 }
96}
97
98#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
100pub struct SessionSelectorRule {
101 pub selector: Selector,
102 #[serde(default, skip_serializing_if = "Vec::is_empty")]
103 pub recipients: Vec<Address>,
104}
105
106#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
108pub struct SessionCallScope {
109 pub target: Address,
110 #[serde(default, skip_serializing_if = "Vec::is_empty")]
112 pub selector_rules: Vec<SessionSelectorRule>,
113}
114
115#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
117pub struct SessionEntry {
118 pub session_id: B256,
119 pub root_account: Address,
120 pub chain_id: u64,
121 pub key_address: Address,
122 pub expiry: u64,
127 #[serde(default, skip_serializing_if = "Option::is_none")]
130 pub scope: Option<Vec<SessionCallScope>>,
131 #[serde(default, skip_serializing_if = "Option::is_none")]
134 pub limits: Option<Vec<SessionTokenLimit>>,
135 #[serde(default)]
136 pub status: SessionStatus,
137 #[serde(default, skip_serializing_if = "Option::is_none")]
140 pub key: Option<SessionKeyMaterial>,
141}
142
143impl SessionEntry {
144 pub const fn is_expired_at(&self, now: u64) -> bool {
146 now >= self.expiry
147 }
148
149 pub fn has_inline_key(&self) -> bool {
151 self.key.as_ref().is_some_and(SessionKeyMaterial::has_inline_key)
152 }
153
154 pub fn has_live_key_at(&self, now: u64) -> bool {
156 self.status == SessionStatus::Active && !self.is_expired_at(now) && self.has_inline_key()
157 }
158}
159
160#[derive(Debug, Default, Clone, PartialEq, Eq, Deserialize, Serialize)]
162pub struct SessionRecord {
163 #[serde(default)]
164 pub sessions: Vec<SessionEntry>,
165}
166
167impl SessionRecord {
168 pub const fn is_empty(&self) -> bool {
170 self.sessions.is_empty()
171 }
172
173 pub fn upsert(&mut self, entry: SessionEntry) {
175 self.sessions.retain(|session| session.session_id != entry.session_id);
176 self.sessions.push(entry);
177 }
178
179 pub fn remove(&mut self, session_id: B256) -> bool {
181 let before = self.sessions.len();
182 self.sessions.retain(|session| session.session_id != session_id);
183 self.sessions.len() != before
184 }
185
186 pub fn get(&self, session_id: B256) -> Option<&SessionEntry> {
188 self.sessions.iter().find(|session| session.session_id == session_id)
189 }
190
191 pub fn live_key(&self, session_id: B256, now: u64) -> Option<&SessionEntry> {
193 self.get(session_id).filter(|session| session.has_live_key_at(now))
194 }
195
196 pub fn set_status(&mut self, session_id: B256, status: SessionStatus) -> bool {
201 let Some(session) =
202 self.sessions.iter_mut().find(|session| session.session_id == session_id)
203 else {
204 return false;
205 };
206
207 set_session_status(session, status)
208 }
209
210 pub fn mark_expired(&mut self, now: u64) -> usize {
212 let mut updated = 0;
213 for session in &mut self.sessions {
214 let should_expire = session.status.is_live() && session.is_expired_at(now);
215 let should_clear_key =
216 session.key.is_some() && (should_expire || session.status.clears_key_material());
217
218 if should_expire {
219 session.status = SessionStatus::Expired;
220 }
221 if should_clear_key {
222 session.key = None;
223 }
224 if should_expire || should_clear_key {
225 updated += 1;
226 }
227 }
228 updated
229 }
230}
231
232fn set_session_status(session: &mut SessionEntry, status: SessionStatus) -> bool {
233 let changed = session.status != status || status.clears_key_material() && session.key.is_some();
234 if !changed {
235 return false;
236 }
237
238 session.status = status;
239 if status.clears_key_material() {
240 session.key = None;
241 }
242 true
243}
244
245#[derive(Debug)]
247pub struct ResolvedSessionSigner {
248 pub session: SessionEntry,
249 pub signer: WalletSigner,
250 pub access_key: TempoAccessKeyConfig,
251}
252
253pub fn session_registry_path() -> Option<PathBuf> {
255 tempo_home().map(|home| home.join(WALLET_SESSIONS_PATH))
256}
257
258pub fn read_session_record() -> Option<SessionRecord> {
263 let path = session_registry_path()?;
264 match read_toml_file(&path, "tempo sessions") {
265 Ok(value) => value,
266 Err(e) => {
267 tracing::warn!(?path, %e, "failed to load tempo sessions file");
268 None
269 }
270 }
271}
272
273pub fn read_live_session_key(session_id: B256, now: u64) -> Option<SessionEntry> {
275 read_session_record()?.live_key(session_id, now).cloned()
276}
277
278pub fn read_session_entry(session_id: B256) -> eyre::Result<Option<SessionEntry>> {
280 let path =
281 session_registry_path().ok_or_else(|| eyre::eyre!("could not resolve tempo home"))?;
282 Ok(read_toml_file::<SessionRecord>(&path, "tempo sessions")?
283 .and_then(|record| record.get(session_id).cloned()))
284}
285
286pub fn resolve_live_session_signer(
288 session_id: B256,
289 now: u64,
290) -> eyre::Result<Option<ResolvedSessionSigner>> {
291 mark_expired_session_entries(now)?;
292
293 let path =
294 session_registry_path().ok_or_else(|| eyre::eyre!("could not resolve tempo home"))?;
295 let Some(record) = read_toml_file::<SessionRecord>(&path, "tempo sessions")? else {
296 return Ok(None);
297 };
298 let Some(session) = record.live_key(session_id, now).cloned() else {
299 return Ok(None);
300 };
301 let key =
302 session.key.as_ref().ok_or_else(|| eyre::eyre!("live session has no key material"))?;
303
304 let signer = foundry_wallets::utils::create_private_key_signer(&key.key)?;
305 let signer_address = signer.address();
306 if signer_address != session.key_address {
307 eyre::bail!(
308 "session {} key material resolves to {}, expected {}",
309 session.session_id,
310 signer_address,
311 session.key_address
312 );
313 }
314
315 let key_authorization = key
316 .key_authorization
317 .as_deref()
318 .map(|raw| {
319 super::decode_key_authorization::<SignedKeyAuthorization>(raw)
320 .map_err(|err| eyre::eyre!("failed to decode session key_authorization: {err}"))
321 })
322 .transpose()?;
323 if let Some(auth) = &key_authorization {
324 validate_signed_session_authorization(
325 &session,
326 key_type_to_signature_type(key.key_type),
327 auth,
328 )?;
329 }
330 let access_key = TempoAccessKeyConfig {
331 wallet_address: session.root_account,
332 key_address: session.key_address,
333 key_authorization,
334 };
335
336 Ok(Some(ResolvedSessionSigner { session, signer, access_key }))
337}
338
339pub(crate) fn validate_signed_session_authorization(
341 session: &SessionEntry,
342 expected_key_type: SignatureType,
343 authorization: &SignedKeyAuthorization,
344) -> eyre::Result<()> {
345 let auth = &authorization.authorization;
346 ensure!(
347 auth.key_id == session.key_address,
348 "session {} key_authorization key_id is {}, expected {}",
349 session.session_id,
350 auth.key_id,
351 session.key_address
352 );
353 ensure!(
354 auth.chain_id == session.chain_id,
355 "session {} key_authorization chain_id is {}, expected {}",
356 session.session_id,
357 auth.chain_id,
358 session.chain_id
359 );
360 ensure!(
361 auth.key_type == expected_key_type,
362 "session {} key_authorization key_type is {:?}, expected {:?}",
363 session.session_id,
364 auth.key_type,
365 expected_key_type
366 );
367 ensure!(
369 auth.witness == Some(session.session_id),
370 "session {} key_authorization witness is {:?}, expected {}",
371 session.session_id,
372 auth.witness,
373 session.session_id
374 );
375 let recovered = authorization
376 .recover_signer()
377 .map_err(|err| eyre::eyre!("failed to recover session key_authorization signer: {err}"))?;
378 ensure!(
379 recovered == session.root_account,
380 "session {} key_authorization signer is {}, expected {}",
381 session.session_id,
382 recovered,
383 session.root_account
384 );
385 validate_session_authorization_policy(session, auth)
386}
387
388fn validate_session_authorization_policy(
390 session: &SessionEntry,
391 auth: &KeyAuthorization,
392) -> eyre::Result<()> {
393 let expected_expiry = NonZeroU64::new(session.expiry)
394 .ok_or_else(|| eyre::eyre!("session {} has invalid zero expiry", session.session_id))?;
395 ensure!(
396 auth.expiry == Some(expected_expiry),
397 "session {} key_authorization expiry is {:?}, expected {}",
398 session.session_id,
399 auth.expiry.map(NonZeroU64::get),
400 session.expiry
401 );
402
403 let expected_limits = session_authorization_limits(session)?;
404 let actual_limits = auth.limits.as_deref().map(authorization_limits);
405 ensure!(
406 actual_limits == expected_limits,
407 "session {} key_authorization limits do not match session limits",
408 session.session_id
409 );
410
411 let expected_scope = session_authorization_scope(session);
412 let actual_scope = auth.allowed_calls.as_deref().map(authorization_scope);
413 ensure!(
414 actual_scope == expected_scope,
415 "session {} key_authorization allowed_calls do not match session scope",
416 session.session_id
417 );
418
419 Ok(())
420}
421
422#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
424struct CanonicalTokenLimit {
425 token: Address,
426 limit: U256,
427 period: u64,
428}
429
430#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
432struct CanonicalCallScope {
433 target: Address,
434 selector_rules: Vec<CanonicalSelectorRule>,
435}
436
437#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
439struct CanonicalSelectorRule {
440 selector: [u8; 4],
441 recipients: Vec<Address>,
442}
443
444fn session_authorization_limits(
446 session: &SessionEntry,
447) -> eyre::Result<Option<Vec<CanonicalTokenLimit>>> {
448 let Some(limits) = session.limits.as_deref() else {
449 return Ok(None);
450 };
451 let mut limits = limits
452 .iter()
453 .map(|limit| {
454 Ok(CanonicalTokenLimit {
455 token: limit.currency,
456 limit: parse_session_limit(&limit.limit)?,
457 period: 0,
458 })
459 })
460 .collect::<eyre::Result<Vec<_>>>()?;
461 limits.sort();
462 Ok(Some(limits))
463}
464
465fn authorization_limits(limits: &[TokenLimit]) -> Vec<CanonicalTokenLimit> {
467 let mut limits = limits
468 .iter()
469 .map(|limit| CanonicalTokenLimit {
470 token: limit.token,
471 limit: limit.limit,
472 period: limit.period,
473 })
474 .collect::<Vec<_>>();
475 limits.sort();
476 limits
477}
478
479fn parse_session_limit(raw: &str) -> eyre::Result<U256> {
481 let raw = raw.trim();
482 if let Some(hex) = raw.strip_prefix("0x") { U256::from_str_radix(hex, 16) } else { raw.parse() }
483 .map_err(|err| eyre::eyre!("invalid session spending limit `{raw}`: {err}"))
484}
485
486fn session_authorization_scope(session: &SessionEntry) -> Option<Vec<CanonicalCallScope>> {
488 let mut scope = session
489 .scope
490 .as_deref()?
491 .iter()
492 .map(|scope| CanonicalCallScope {
493 target: scope.target,
494 selector_rules: session_authorization_selector_rules(&scope.selector_rules),
495 })
496 .collect::<Vec<_>>();
497 scope.sort();
498 Some(scope)
499}
500
501fn authorization_scope(scope: &[CallScope]) -> Vec<CanonicalCallScope> {
503 let mut scope = scope
504 .iter()
505 .map(|scope| CanonicalCallScope {
506 target: scope.target,
507 selector_rules: authorization_selector_rules(&scope.selector_rules),
508 })
509 .collect::<Vec<_>>();
510 scope.sort();
511 scope
512}
513
514fn session_authorization_selector_rules(
516 rules: &[SessionSelectorRule],
517) -> Vec<CanonicalSelectorRule> {
518 let mut rules = rules
519 .iter()
520 .map(|rule| {
521 let mut recipients = rule.recipients.clone();
522 recipients.sort();
523 CanonicalSelectorRule { selector: rule.selector.into(), recipients }
524 })
525 .collect::<Vec<_>>();
526 rules.sort();
527 rules
528}
529
530fn authorization_selector_rules(rules: &[SelectorRule]) -> Vec<CanonicalSelectorRule> {
532 let mut rules = rules
533 .iter()
534 .map(|rule| {
535 let mut recipients = rule.recipients.clone();
536 recipients.sort();
537 CanonicalSelectorRule { selector: rule.selector, recipients }
538 })
539 .collect::<Vec<_>>();
540 rules.sort();
541 rules
542}
543
544const fn key_type_to_signature_type(key_type: KeyType) -> SignatureType {
546 match key_type {
547 KeyType::Secp256k1 => SignatureType::Secp256k1,
548 KeyType::P256 => SignatureType::P256,
549 KeyType::WebAuthn => SignatureType::WebAuthn,
550 }
551}
552
553fn mutate_session_record<R>(f: impl FnOnce(&mut SessionRecord) -> (R, bool)) -> eyre::Result<R> {
554 let path =
555 session_registry_path().ok_or_else(|| eyre::eyre!("could not resolve tempo home"))?;
556 let mut record = read_toml_file::<SessionRecord>(&path, "tempo sessions")?.unwrap_or_default();
557 let (result, changed) = f(&mut record);
558 if changed {
559 write_toml_file_atomic(&path, &record, SESSIONS_HEADER)?;
560 }
561 Ok(result)
562}
563
564pub fn upsert_session_entry(entry: SessionEntry) -> eyre::Result<()> {
566 mutate_session_record(|record| {
567 record.upsert(entry);
568 ((), true)
569 })
570}
571
572pub fn update_session_status(session_id: B256, status: SessionStatus) -> eyre::Result<bool> {
577 mutate_session_record(|record| {
578 let changed = record.set_status(session_id, status);
579 (changed, changed)
580 })
581}
582
583pub fn update_session_status_if(
588 session_id: B256,
589 current: SessionStatus,
590 status: SessionStatus,
591) -> eyre::Result<bool> {
592 mutate_session_record(|record| {
593 let Some(session) =
594 record.sessions.iter_mut().find(|session| session.session_id == session_id)
595 else {
596 return (false, false);
597 };
598 if session.status != current {
599 return (false, false);
600 }
601
602 let changed = set_session_status(session, status);
603 (true, changed)
604 })
605}
606
607pub fn remove_session_entry(session_id: B256) -> eyre::Result<bool> {
609 mutate_session_record(|record| {
610 let removed = record.remove(session_id);
611 (removed, removed)
612 })
613}
614
615pub fn mark_expired_session_entries(now: u64) -> eyre::Result<usize> {
617 mutate_session_record(|record| {
618 let updated = record.mark_expired(now);
619 (updated, updated != 0)
620 })
621}
622
623#[cfg(test)]
624mod tests {
625 use super::*;
626 use crate::tempo::with_tempo_home;
627 use alloy_primitives::hex;
628 use alloy_rlp::Encodable;
629 use alloy_signer::SignerSync;
630 use alloy_signer_local::PrivateKeySigner;
631 use std::{fs, str::FromStr};
632 use tempo_primitives::transaction::PrimitiveSignature;
633
634 const ROOT_PRIVATE_KEY: &str =
635 "0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80";
636 const SESSION_PRIVATE_KEY: &str =
637 "0x59c6995e998f97a5a004497e5da3b5d2b2b66a87f064d39c44da0b6d6e4f8ff0";
638
639 #[test]
640 fn debug_redacts_session_key_material() {
641 let mut entry = sample_entry_with_key(B256::from([0x77; 32]), 200, SessionStatus::Active);
643 let key = entry.key.as_mut().unwrap();
644 key.key = "0xPRIVATE_KEY_MUST_NOT_LEAK".to_string();
645 key.key_authorization = Some("0xKEY_AUTH_MUST_NOT_LEAK".to_string());
646
647 let entry_dbg = format!("{entry:?}");
648 let record_dbg = format!("{:?}", SessionRecord { sessions: vec![entry] });
649
650 for rendered in [&entry_dbg, &record_dbg] {
651 assert!(!rendered.contains("PRIVATE_KEY_MUST_NOT_LEAK"), "key leaked in: {rendered}");
652 assert!(!rendered.contains("KEY_AUTH_MUST_NOT_LEAK"), "auth leaked in: {rendered}");
653 }
654 assert!(entry_dbg.contains("key: \"<redacted>\""), "got: {entry_dbg}");
655 assert!(entry_dbg.contains("key_authorization: Some(\"<redacted>\")"), "got: {entry_dbg}");
656 assert!(entry_dbg.contains("key_type"));
658 }
659
660 fn sample_entry(session_id: B256, expiry: u64, status: SessionStatus) -> SessionEntry {
661 SessionEntry {
662 session_id,
663 root_account: Address::from_str("0x0000000000000000000000000000000000000001").unwrap(),
664 chain_id: 4217,
665 key_address: Address::from_str("0x0000000000000000000000000000000000000abc").unwrap(),
666 expiry,
667 scope: Some(vec![SessionCallScope {
668 target: Address::from_str("0x00000000000000000000000000000000000000aa").unwrap(),
669 selector_rules: vec![SessionSelectorRule {
670 selector: Selector::from_slice(&[0x12, 0x34, 0x56, 0x78]),
671 recipients: vec![],
672 }],
673 }]),
674 limits: Some(vec![SessionTokenLimit {
675 currency: Address::from_str("0x00000000000000000000000000000000000000ff").unwrap(),
676 limit: "0".to_string(),
677 }]),
678 status,
679 key: None,
680 }
681 }
682
683 fn sample_entry_with_key(session_id: B256, expiry: u64, status: SessionStatus) -> SessionEntry {
684 SessionEntry {
685 key: Some(SessionKeyMaterial {
686 key_type: KeyType::Secp256k1,
687 key: "0xdeadbeef".to_string(),
688 key_authorization: Some("0xfeed".to_string()),
689 }),
690 ..sample_entry(session_id, expiry, status)
691 }
692 }
693
694 fn sample_entry_with_valid_key(
696 session_id: B256,
697 expiry: u64,
698 status: SessionStatus,
699 ) -> SessionEntry {
700 let root_signer: PrivateKeySigner = ROOT_PRIVATE_KEY.parse().unwrap();
701 let signer = foundry_wallets::utils::create_private_key_signer(SESSION_PRIVATE_KEY)
702 .expect("valid test private key");
703 SessionEntry {
704 root_account: root_signer.address(),
705 key_address: signer.address(),
706 key: Some(SessionKeyMaterial {
707 key_type: KeyType::Secp256k1,
708 key: SESSION_PRIVATE_KEY.to_string(),
709 key_authorization: None,
710 }),
711 ..sample_entry(session_id, expiry, status)
712 }
713 }
714
715 fn signed_key_authorization_hex(entry: &SessionEntry) -> String {
717 signed_key_authorization_hex_with(entry, std::convert::identity)
718 }
719
720 fn signed_key_authorization_hex_with(
722 entry: &SessionEntry,
723 update: impl FnOnce(KeyAuthorization) -> KeyAuthorization,
724 ) -> String {
725 let root_signer: PrivateKeySigner = ROOT_PRIVATE_KEY.parse().unwrap();
726 let auth = update(session_key_authorization(entry));
727 let signature = root_signer.sign_hash_sync(&auth.signature_hash()).unwrap();
728 let signed = auth.into_signed(PrimitiveSignature::Secp256k1(signature));
729 let mut buf = Vec::new();
730 signed.encode(&mut buf);
731 hex::encode_prefixed(buf)
732 }
733
734 fn session_key_authorization(entry: &SessionEntry) -> KeyAuthorization {
736 let mut authorization = KeyAuthorization::unrestricted(
737 entry.chain_id,
738 SignatureType::Secp256k1,
739 entry.key_address,
740 )
741 .with_expiry(entry.expiry)
742 .with_witness(entry.session_id);
743 if let Some(limits) = &entry.limits {
744 authorization = authorization.with_limits(
745 limits
746 .iter()
747 .map(|limit| TokenLimit {
748 token: limit.currency,
749 limit: parse_session_limit(&limit.limit).unwrap(),
750 period: 0,
751 })
752 .collect(),
753 );
754 }
755 if let Some(scope) = &entry.scope {
756 authorization = authorization.with_allowed_calls(
757 scope
758 .iter()
759 .map(|scope| CallScope {
760 target: scope.target,
761 selector_rules: scope
762 .selector_rules
763 .iter()
764 .map(|rule| SelectorRule {
765 selector: rule.selector.into(),
766 recipients: rule.recipients.clone(),
767 })
768 .collect(),
769 })
770 .collect(),
771 );
772 }
773 authorization
774 }
775
776 #[test]
777 fn session_registry_is_separate_from_keys_registry() {
778 with_tempo_home(|| {
779 let session_id = B256::from([0x11; 32]);
780 upsert_session_entry(sample_entry(session_id, 100, SessionStatus::Pending)).unwrap();
781
782 let session_path = session_registry_path().unwrap();
783 let keys_path = crate::tempo::tempo_keys_path().unwrap();
784 assert_eq!(session_path.file_name().and_then(|s| s.to_str()), Some("sessions.toml"));
785 assert_eq!(keys_path.file_name().and_then(|s| s.to_str()), Some("keys.toml"));
786 assert_ne!(session_path, keys_path);
787
788 let record = read_session_record().unwrap();
789 assert_eq!(record.sessions.len(), 1);
790 assert_eq!(record.sessions[0].session_id, session_id);
791 });
792 }
793
794 #[test]
795 fn session_registry_upsert_replaces_matching_session_id() {
796 with_tempo_home(|| {
797 let session_id = B256::from([0x22; 32]);
798 upsert_session_entry(sample_entry(session_id, 100, SessionStatus::Pending)).unwrap();
799 upsert_session_entry(sample_entry(session_id, 200, SessionStatus::Active)).unwrap();
800
801 let record = read_session_record().unwrap();
802 assert_eq!(record.sessions.len(), 1);
803 assert_eq!(record.sessions[0].expiry, 200);
804 assert_eq!(record.sessions[0].status, SessionStatus::Active);
805 });
806 }
807
808 #[test]
809 fn session_registry_remove_deletes_entry() {
810 with_tempo_home(|| {
811 let session_id = B256::from([0x33; 32]);
812 upsert_session_entry(sample_entry(session_id, 100, SessionStatus::Active)).unwrap();
813 assert!(remove_session_entry(session_id).unwrap());
814 assert!(read_session_record().unwrap().is_empty());
815 });
816 }
817
818 #[test]
819 fn session_record_marks_expired_live_entries() {
820 let mut record = SessionRecord {
821 sessions: vec![
822 sample_entry(B256::from([0x44; 32]), 10, SessionStatus::Pending),
823 sample_entry(B256::from([0x55; 32]), 10, SessionStatus::Revoked),
824 ],
825 };
826
827 assert_eq!(record.mark_expired(11), 1);
828 assert_eq!(record.sessions[0].status, SessionStatus::Expired);
829 assert_eq!(record.sessions[1].status, SessionStatus::Revoked);
830 }
831
832 #[test]
833 fn session_record_status_updates_clear_cleanup_and_terminal_keys() {
834 let active_id = B256::from([0x67; 32]);
835 let revoking_id = B256::from([0x68; 32]);
836 let revoked_id = B256::from([0x69; 32]);
837 let failed_id = B256::from([0x6a; 32]);
838 let missing_id = B256::from([0x6b; 32]);
839 let mut record = SessionRecord {
840 sessions: vec![
841 sample_entry_with_key(active_id, 200, SessionStatus::Pending),
842 sample_entry_with_key(revoking_id, 200, SessionStatus::Active),
843 sample_entry_with_key(revoked_id, 200, SessionStatus::Revoking),
844 sample_entry_with_key(failed_id, 200, SessionStatus::Pending),
845 ],
846 };
847
848 assert!(record.set_status(active_id, SessionStatus::Active));
849 assert!(record.set_status(revoking_id, SessionStatus::Revoking));
850 assert!(record.set_status(revoked_id, SessionStatus::Revoked));
851 assert!(record.set_status(failed_id, SessionStatus::Failed));
852 assert!(!record.set_status(missing_id, SessionStatus::Active));
853 assert!(!record.set_status(active_id, SessionStatus::Active));
854
855 assert_eq!(record.get(active_id).unwrap().status, SessionStatus::Active);
856 assert!(record.get(active_id).unwrap().key.is_some());
857 assert_eq!(record.get(revoking_id).unwrap().status, SessionStatus::Revoking);
858 assert!(record.get(revoking_id).unwrap().key.is_none());
859 assert_eq!(record.get(revoked_id).unwrap().status, SessionStatus::Revoked);
860 assert!(record.get(revoked_id).unwrap().key.is_none());
861 assert_eq!(record.get(failed_id).unwrap().status, SessionStatus::Failed);
862 assert!(record.get(failed_id).unwrap().key.is_none());
863 }
864
865 #[test]
866 fn session_entry_roundtrips_scope_limits_and_status() {
867 let entry = sample_entry(B256::from([0x66; 32]), 1234, SessionStatus::Revoking);
868 let toml = toml::to_string(&entry).unwrap();
869 let decoded: SessionEntry = toml::from_str(&toml).unwrap();
870
871 assert_eq!(decoded.session_id, entry.session_id);
872 assert_eq!(decoded.scope.as_ref().unwrap().len(), 1);
873 assert_eq!(decoded.limits.as_ref().unwrap().len(), 1);
874 assert_eq!(decoded.status, SessionStatus::Revoking);
875 assert!(decoded.key.is_none());
876 assert!(!decoded.has_inline_key());
877 assert!(decoded.is_expired_at(1234));
878 }
879
880 #[test]
881 fn live_session_key_requires_key_material_live_status_and_unexpired_entry() {
882 let live_id = B256::from([0x01; 32]);
883 let expired_id = B256::from([0x02; 32]);
884 let revoked_id = B256::from([0x03; 32]);
885 let no_key_id = B256::from([0x04; 32]);
886 let pending_id = B256::from([0x05; 32]);
887 let revoking_id = B256::from([0x06; 32]);
888
889 let record = SessionRecord {
890 sessions: vec![
891 sample_entry_with_key(live_id, 200, SessionStatus::Active),
892 sample_entry_with_key(expired_id, 100, SessionStatus::Active),
893 sample_entry_with_key(revoked_id, 200, SessionStatus::Revoked),
894 sample_entry(no_key_id, 200, SessionStatus::Active),
895 sample_entry_with_key(pending_id, 200, SessionStatus::Pending),
896 sample_entry_with_key(revoking_id, 200, SessionStatus::Revoking),
897 ],
898 };
899
900 assert_eq!(record.live_key(live_id, 100).unwrap().session_id, live_id);
901 assert!(record.live_key(expired_id, 100).is_none());
902 assert!(record.live_key(revoked_id, 100).is_none());
903 assert!(record.live_key(no_key_id, 100).is_none());
904 assert!(record.live_key(pending_id, 100).is_none());
905 assert!(record.live_key(revoking_id, 100).is_none());
906 }
907
908 #[test]
909 fn resolve_live_session_signer_returns_signer_and_access_key_config() {
910 with_tempo_home(|| {
911 let session_id = B256::from([0x06; 32]);
912 let entry = sample_entry_with_valid_key(session_id, 200, SessionStatus::Active);
913 upsert_session_entry(entry.clone()).unwrap();
914
915 let resolved = resolve_live_session_signer(session_id, 100).unwrap().unwrap();
916
917 assert_eq!(resolved.session, entry);
918 assert_eq!(Signer::address(&resolved.signer), entry.key_address);
919 assert_eq!(resolved.access_key.wallet_address, entry.root_account);
920 assert_eq!(resolved.access_key.key_address, entry.key_address);
921 assert!(resolved.access_key.key_authorization.is_none());
922 });
923 }
924
925 #[test]
926 fn resolve_live_session_signer_rejects_mismatched_private_key() {
927 with_tempo_home(|| {
928 let session_id = B256::from([0x07; 32]);
929 let mut entry = sample_entry_with_valid_key(session_id, 200, SessionStatus::Active);
930 entry.key_address =
931 Address::from_str("0x0000000000000000000000000000000000000abc").unwrap();
932 upsert_session_entry(entry).unwrap();
933
934 let error = resolve_live_session_signer(session_id, 100).unwrap_err();
935
936 assert!(error.to_string().contains("key material resolves to"));
937 });
938 }
939
940 #[test]
941 fn resolve_live_session_signer_expires_stale_entries_before_resolving() {
942 with_tempo_home(|| {
943 let session_id = B256::from([0x08; 32]);
944 upsert_session_entry(sample_entry_with_valid_key(
945 session_id,
946 100,
947 SessionStatus::Active,
948 ))
949 .unwrap();
950
951 assert!(resolve_live_session_signer(session_id, 100).unwrap().is_none());
952
953 let record = read_session_record().unwrap();
954 let session = record.get(session_id).unwrap();
955 assert_eq!(session.status, SessionStatus::Expired);
956 assert!(session.key.is_none());
957 });
958 }
959
960 #[test]
961 fn resolve_live_session_signer_decodes_and_validates_key_authorization() {
962 with_tempo_home(|| {
963 let session_id = B256::from([0x09; 32]);
964 let mut entry = sample_entry_with_valid_key(session_id, 200, SessionStatus::Active);
965 let auth = signed_key_authorization_hex(&entry);
966 entry.key.as_mut().unwrap().key_authorization = Some(auth);
967 upsert_session_entry(entry.clone()).unwrap();
968
969 let resolved = resolve_live_session_signer(session_id, 100).unwrap().unwrap();
970 let key_authorization = resolved.access_key.key_authorization.unwrap();
971
972 assert_eq!(key_authorization.authorization.key_id, entry.key_address);
973 assert_eq!(key_authorization.authorization.chain_id, entry.chain_id);
974 assert_eq!(key_authorization.authorization.key_type, SignatureType::Secp256k1);
975 assert_eq!(key_authorization.authorization.expiry.unwrap().get(), entry.expiry);
976 assert!(key_authorization.authorization.limits.is_some());
977 assert!(key_authorization.authorization.allowed_calls.is_some());
978 assert_eq!(key_authorization.recover_signer().unwrap(), entry.root_account);
979 });
980 }
981
982 #[test]
983 fn resolve_live_session_signer_accepts_unrestricted_authorization_when_policy_is_omitted() {
984 with_tempo_home(|| {
985 let session_id = B256::from([0x13; 32]);
986 let mut entry = sample_entry_with_valid_key(session_id, 200, SessionStatus::Active);
987 entry.limits = None;
988 entry.scope = None;
989 entry.key.as_mut().unwrap().key_authorization =
990 Some(signed_key_authorization_hex(&entry));
991 upsert_session_entry(entry.clone()).unwrap();
992
993 let resolved = resolve_live_session_signer(session_id, 100).unwrap().unwrap();
994 let key_authorization = resolved.access_key.key_authorization.unwrap();
995
996 assert!(key_authorization.authorization.limits.is_none());
997 assert!(key_authorization.authorization.allowed_calls.is_none());
998 });
999 }
1000
1001 #[test]
1002 fn resolve_live_session_signer_rejects_unrestricted_authorization_when_policy_is_empty() {
1003 with_tempo_home(|| {
1004 let session_id = B256::from([0x14; 32]);
1005 let mut entry = sample_entry_with_valid_key(session_id, 200, SessionStatus::Active);
1006 let mut auth_entry = entry.clone();
1007 auth_entry.limits = None;
1008 auth_entry.scope = None;
1009 entry.limits = Some(vec![]);
1010 entry.scope = Some(vec![]);
1011 entry.key.as_mut().unwrap().key_authorization =
1012 Some(signed_key_authorization_hex(&auth_entry));
1013 upsert_session_entry(entry).unwrap();
1014
1015 let error = resolve_live_session_signer(session_id, 100).unwrap_err();
1016
1017 assert!(error.to_string().contains("limits"));
1018 });
1019 }
1020
1021 #[test]
1022 fn resolve_live_session_signer_rejects_invalid_key_authorization() {
1023 with_tempo_home(|| {
1024 let session_id = B256::from([0x0a; 32]);
1025 let mut entry = sample_entry_with_valid_key(session_id, 200, SessionStatus::Active);
1026 entry.key.as_mut().unwrap().key_authorization = Some("0xdeadbeef".to_string());
1027 upsert_session_entry(entry).unwrap();
1028
1029 let error = resolve_live_session_signer(session_id, 100).unwrap_err();
1030
1031 assert!(error.to_string().contains("key_authorization"));
1032 });
1033 }
1034
1035 #[test]
1036 fn resolve_live_session_signer_rejects_authorization_for_wrong_chain() {
1037 with_tempo_home(|| {
1038 let session_id = B256::from([0x0b; 32]);
1039 let mut entry = sample_entry_with_valid_key(session_id, 200, SessionStatus::Active);
1040 let mut auth_entry = entry.clone();
1041 auth_entry.chain_id += 1;
1042 entry.key.as_mut().unwrap().key_authorization =
1043 Some(signed_key_authorization_hex(&auth_entry));
1044 upsert_session_entry(entry).unwrap();
1045
1046 let error = resolve_live_session_signer(session_id, 100).unwrap_err();
1047
1048 assert!(error.to_string().contains("chain_id"));
1049 });
1050 }
1051
1052 #[test]
1053 fn resolve_live_session_signer_rejects_authorization_without_session_expiry() {
1054 with_tempo_home(|| {
1055 let session_id = B256::from([0x0d; 32]);
1056 let mut entry = sample_entry_with_valid_key(session_id, 200, SessionStatus::Active);
1057 entry.key.as_mut().unwrap().key_authorization =
1058 Some(signed_key_authorization_hex_with(&entry, |mut auth| {
1059 auth.expiry = None;
1060 auth
1061 }));
1062 upsert_session_entry(entry).unwrap();
1063
1064 let error = resolve_live_session_signer(session_id, 100).unwrap_err();
1065
1066 assert!(error.to_string().contains("expiry"));
1067 });
1068 }
1069
1070 #[test]
1071 fn resolve_live_session_signer_rejects_authorization_without_session_limits() {
1072 with_tempo_home(|| {
1073 let session_id = B256::from([0x0e; 32]);
1074 let mut entry = sample_entry_with_valid_key(session_id, 200, SessionStatus::Active);
1075 entry.key.as_mut().unwrap().key_authorization =
1076 Some(signed_key_authorization_hex_with(&entry, |mut auth| {
1077 auth.limits = None;
1078 auth
1079 }));
1080 upsert_session_entry(entry).unwrap();
1081
1082 let error = resolve_live_session_signer(session_id, 100).unwrap_err();
1083
1084 assert!(error.to_string().contains("limits"));
1085 });
1086 }
1087
1088 #[test]
1089 fn resolve_live_session_signer_rejects_authorization_without_session_scope() {
1090 with_tempo_home(|| {
1091 let session_id = B256::from([0x0f; 32]);
1092 let mut entry = sample_entry_with_valid_key(session_id, 200, SessionStatus::Active);
1093 entry.key.as_mut().unwrap().key_authorization =
1094 Some(signed_key_authorization_hex_with(&entry, |mut auth| {
1095 auth.allowed_calls = None;
1096 auth
1097 }));
1098 upsert_session_entry(entry).unwrap();
1099
1100 let error = resolve_live_session_signer(session_id, 100).unwrap_err();
1101
1102 assert!(error.to_string().contains("allowed_calls"));
1103 });
1104 }
1105
1106 #[test]
1107 fn resolve_live_session_signer_rejects_authorization_for_wrong_session_id() {
1108 with_tempo_home(|| {
1109 let session_id = B256::from([0x15; 32]);
1110 let mut entry = sample_entry_with_valid_key(session_id, 200, SessionStatus::Active);
1111 entry.key.as_mut().unwrap().key_authorization =
1112 Some(signed_key_authorization_hex_with(&entry, |auth| {
1113 auth.with_witness(B256::from([0x16; 32]))
1114 }));
1115 upsert_session_entry(entry).unwrap();
1116
1117 let error = resolve_live_session_signer(session_id, 100).unwrap_err();
1118
1119 assert!(error.to_string().contains("witness"));
1120 });
1121 }
1122
1123 #[test]
1124 fn resolve_live_session_signer_rejects_authorization_with_wider_session_limit() {
1125 with_tempo_home(|| {
1126 let session_id = B256::from([0x10; 32]);
1127 let mut entry = sample_entry_with_valid_key(session_id, 200, SessionStatus::Active);
1128 entry.key.as_mut().unwrap().key_authorization =
1129 Some(signed_key_authorization_hex_with(&entry, |mut auth| {
1130 auth.limits.as_mut().unwrap()[0].limit = U256::from(1);
1131 auth
1132 }));
1133 upsert_session_entry(entry).unwrap();
1134
1135 let error = resolve_live_session_signer(session_id, 100).unwrap_err();
1136
1137 assert!(error.to_string().contains("limits"));
1138 });
1139 }
1140
1141 #[test]
1142 fn resolve_live_session_signer_rejects_authorization_with_wider_session_scope() {
1143 with_tempo_home(|| {
1144 let session_id = B256::from([0x12; 32]);
1145 let mut entry = sample_entry_with_valid_key(session_id, 200, SessionStatus::Active);
1146 entry.key.as_mut().unwrap().key_authorization =
1147 Some(signed_key_authorization_hex_with(&entry, |mut auth| {
1148 auth.allowed_calls.as_mut().unwrap()[0].selector_rules.clear();
1149 auth
1150 }));
1151 upsert_session_entry(entry).unwrap();
1152
1153 let error = resolve_live_session_signer(session_id, 100).unwrap_err();
1154
1155 assert!(error.to_string().contains("allowed_calls"));
1156 });
1157 }
1158
1159 #[test]
1160 fn resolve_live_session_signer_fails_closed_when_session_file_is_corrupt() {
1161 with_tempo_home(|| {
1162 let path = session_registry_path().unwrap();
1163 fs::create_dir_all(path.parent().unwrap()).unwrap();
1164 fs::write(&path, "sessions = [").unwrap();
1165 let original = fs::read_to_string(&path).unwrap();
1166
1167 assert!(resolve_live_session_signer(B256::from([0x0c; 32]), 100).is_err());
1168 assert_eq!(fs::read_to_string(&path).unwrap(), original);
1169 });
1170 }
1171
1172 #[test]
1173 fn session_key_storage_does_not_replace_persistent_keys_file() {
1174 with_tempo_home(|| {
1175 let keys_path = crate::tempo::tempo_keys_path().unwrap();
1176 fs::create_dir_all(keys_path.parent().unwrap()).unwrap();
1177 let original_keys = r#"[[keys]]
1178wallet_type = "local"
1179wallet_address = "0x0000000000000000000000000000000000000001"
1180chain_id = 4217
1181key_type = "secp256k1"
1182key_address = "0x0000000000000000000000000000000000000001"
1183key = "0x1111"
1184expiry = 999
1185"#;
1186 fs::write(&keys_path, original_keys).unwrap();
1187
1188 let session_id = B256::from([0x99; 32]);
1189 upsert_session_entry(sample_entry_with_key(session_id, 200, SessionStatus::Active))
1190 .unwrap();
1191
1192 assert_eq!(fs::read_to_string(&keys_path).unwrap(), original_keys);
1193 let session = read_live_session_key(session_id, 100).unwrap();
1194 assert_eq!(session.key.unwrap().key, "0xdeadbeef");
1195 });
1196 }
1197
1198 #[test]
1199 fn removing_session_key_preserves_persistent_key() {
1200 with_tempo_home(|| {
1201 let keys_path = crate::tempo::tempo_keys_path().unwrap();
1202 fs::create_dir_all(keys_path.parent().unwrap()).unwrap();
1203 let original_keys = r#"[[keys]]
1204wallet_type = "local"
1205wallet_address = "0x0000000000000000000000000000000000000001"
1206chain_id = 4217
1207key_type = "secp256k1"
1208key_address = "0x0000000000000000000000000000000000000001"
1209key = "0x1111"
1210"#;
1211 fs::write(&keys_path, original_keys).unwrap();
1212
1213 let session_id = B256::from([0xaa; 32]);
1214 upsert_session_entry(sample_entry_with_key(session_id, 200, SessionStatus::Active))
1215 .unwrap();
1216 assert!(remove_session_entry(session_id).unwrap());
1217
1218 assert_eq!(fs::read_to_string(&keys_path).unwrap(), original_keys);
1219 assert!(read_session_record().unwrap().is_empty());
1220 });
1221 }
1222
1223 #[test]
1224 fn mark_expired_session_entries_persists_status_without_touching_keys_file() {
1225 with_tempo_home(|| {
1226 let keys_path = crate::tempo::tempo_keys_path().unwrap();
1227 fs::create_dir_all(keys_path.parent().unwrap()).unwrap();
1228 let original_keys = "[[keys]]\nkey = \"0x1111\"\n";
1229 fs::write(&keys_path, original_keys).unwrap();
1230
1231 let session_id = B256::from([0xbb; 32]);
1232 upsert_session_entry(sample_entry_with_key(session_id, 100, SessionStatus::Active))
1233 .unwrap();
1234
1235 assert_eq!(mark_expired_session_entries(100).unwrap(), 1);
1236 let record = read_session_record().unwrap();
1237 let session = record.get(session_id).unwrap();
1238 assert_eq!(session.status, SessionStatus::Expired);
1239 assert!(session.key.is_none());
1240 assert!(read_live_session_key(session_id, 100).is_none());
1241 assert_eq!(fs::read_to_string(&keys_path).unwrap(), original_keys);
1242 });
1243 }
1244
1245 #[test]
1246 fn mark_expired_session_entries_clears_unusable_session_keys() {
1247 with_tempo_home(|| {
1248 let expired_id = B256::from([0xbc; 32]);
1249 let revoked_id = B256::from([0xbd; 32]);
1250 let failed_id = B256::from([0xbe; 32]);
1251 let revoking_id = B256::from([0xbf; 32]);
1252
1253 upsert_session_entry(sample_entry_with_key(expired_id, 100, SessionStatus::Expired))
1254 .unwrap();
1255 upsert_session_entry(sample_entry_with_key(revoked_id, 200, SessionStatus::Revoked))
1256 .unwrap();
1257 upsert_session_entry(sample_entry_with_key(failed_id, 200, SessionStatus::Failed))
1258 .unwrap();
1259 upsert_session_entry(sample_entry_with_key(revoking_id, 200, SessionStatus::Revoking))
1260 .unwrap();
1261
1262 assert_eq!(mark_expired_session_entries(100).unwrap(), 4);
1263 let record = read_session_record().unwrap();
1264 for session_id in [expired_id, revoked_id, failed_id, revoking_id] {
1265 assert!(record.get(session_id).unwrap().key.is_none());
1266 }
1267 assert_eq!(record.get(expired_id).unwrap().status, SessionStatus::Expired);
1268 assert_eq!(record.get(revoked_id).unwrap().status, SessionStatus::Revoked);
1269 assert_eq!(record.get(failed_id).unwrap().status, SessionStatus::Failed);
1270 assert_eq!(record.get(revoking_id).unwrap().status, SessionStatus::Revoking);
1271 });
1272 }
1273
1274 #[test]
1275 fn update_session_status_persists_lifecycle_state_and_key_cleanup() {
1276 with_tempo_home(|| {
1277 let session_id = B256::from([0xbf; 32]);
1278 upsert_session_entry(sample_entry_with_key(session_id, 200, SessionStatus::Pending))
1279 .unwrap();
1280
1281 assert!(update_session_status(session_id, SessionStatus::Active).unwrap());
1282 let record = read_session_record().unwrap();
1283 let session = record.get(session_id).unwrap();
1284 assert_eq!(session.status, SessionStatus::Active);
1285 assert!(session.key.is_some());
1286
1287 assert!(update_session_status(session_id, SessionStatus::Revoking).unwrap());
1288 let record = read_session_record().unwrap();
1289 let session = record.get(session_id).unwrap();
1290 assert_eq!(session.status, SessionStatus::Revoking);
1291 assert!(session.key.is_none());
1292
1293 assert!(update_session_status(session_id, SessionStatus::Revoked).unwrap());
1294 let record = read_session_record().unwrap();
1295 let session = record.get(session_id).unwrap();
1296 assert_eq!(session.status, SessionStatus::Revoked);
1297 assert!(session.key.is_none());
1298 assert!(read_live_session_key(session_id, 100).is_none());
1299
1300 assert!(!update_session_status(session_id, SessionStatus::Revoked).unwrap());
1301 assert!(!update_session_status(B256::from([0xc0; 32]), SessionStatus::Failed).unwrap());
1302 });
1303 }
1304
1305 #[test]
1306 fn update_session_status_to_failed_clears_key_material() {
1307 with_tempo_home(|| {
1308 let session_id = B256::from([0xc1; 32]);
1309 upsert_session_entry(sample_entry_with_key(session_id, 200, SessionStatus::Active))
1310 .unwrap();
1311
1312 assert!(update_session_status(session_id, SessionStatus::Failed).unwrap());
1313
1314 let record = read_session_record().unwrap();
1315 let session = record.get(session_id).unwrap();
1316 assert_eq!(session.status, SessionStatus::Failed);
1317 assert!(session.key.is_none());
1318 });
1319 }
1320
1321 #[test]
1322 fn update_session_status_if_only_updates_matching_current_status() {
1323 with_tempo_home(|| {
1324 let session_id = B256::from([0xc2; 32]);
1325 upsert_session_entry(sample_entry_with_key(session_id, 200, SessionStatus::Active))
1326 .unwrap();
1327
1328 assert!(
1329 update_session_status_if(
1330 session_id,
1331 SessionStatus::Active,
1332 SessionStatus::Revoking,
1333 )
1334 .unwrap()
1335 );
1336 let record = read_session_record().unwrap();
1337 let session = record.get(session_id).unwrap();
1338 assert_eq!(session.status, SessionStatus::Revoking);
1339 assert!(session.key.is_none());
1340
1341 assert!(!update_session_status_if(
1342 session_id,
1343 SessionStatus::Active,
1344 SessionStatus::Failed,
1345 )
1346 .unwrap());
1347 assert_eq!(
1348 read_session_record().unwrap().get(session_id).unwrap().status,
1349 SessionStatus::Revoking
1350 );
1351 });
1352 }
1353
1354 #[test]
1355 fn upsert_fails_closed_when_session_file_is_corrupt() {
1356 with_tempo_home(|| {
1357 let path = session_registry_path().unwrap();
1358 fs::create_dir_all(path.parent().unwrap()).unwrap();
1359 fs::write(&path, "sessions = [").unwrap();
1360 let original = fs::read_to_string(&path).unwrap();
1361
1362 let session_id = B256::from([0x77; 32]);
1363 let entry = sample_entry(session_id, 100, SessionStatus::Pending);
1364
1365 assert!(read_session_record().is_none());
1366 assert!(upsert_session_entry(entry).is_err());
1367 assert_eq!(fs::read_to_string(&path).unwrap(), original);
1368 });
1369 }
1370
1371 #[test]
1372 fn remove_fails_closed_when_session_file_is_corrupt() {
1373 with_tempo_home(|| {
1374 let path = session_registry_path().unwrap();
1375 fs::create_dir_all(path.parent().unwrap()).unwrap();
1376 fs::write(&path, "sessions = [").unwrap();
1377 let original = fs::read_to_string(&path).unwrap();
1378
1379 assert!(remove_session_entry(B256::from([0x88; 32])).is_err());
1380 assert_eq!(fs::read_to_string(&path).unwrap(), original);
1381 });
1382 }
1383
1384 #[test]
1385 fn mark_expired_fails_closed_when_session_file_is_corrupt() {
1386 with_tempo_home(|| {
1387 let path = session_registry_path().unwrap();
1388 fs::create_dir_all(path.parent().unwrap()).unwrap();
1389 fs::write(&path, "sessions = [").unwrap();
1390 let original = fs::read_to_string(&path).unwrap();
1391
1392 assert!(mark_expired_session_entries(100).is_err());
1393 assert_eq!(fs::read_to_string(&path).unwrap(), original);
1394 });
1395 }
1396
1397 #[test]
1398 fn update_session_status_fails_closed_when_session_file_is_corrupt() {
1399 with_tempo_home(|| {
1400 let path = session_registry_path().unwrap();
1401 fs::create_dir_all(path.parent().unwrap()).unwrap();
1402 fs::write(&path, "sessions = [").unwrap();
1403 let original = fs::read_to_string(&path).unwrap();
1404
1405 assert!(update_session_status(B256::from([0xc2; 32]), SessionStatus::Failed).is_err());
1406 assert_eq!(fs::read_to_string(&path).unwrap(), original);
1407 });
1408 }
1409}