ports.rs (28223B)
1 use std::future::Future; 2 use std::pin::Pin; 3 use std::time::Instant; 4 5 use harvestcircle_domain::{ 6 Kind0ProfileCandidate, NostrIdentity, Npub, Nsec, PublicKey, SafeError, SafeErrorCode, 7 SafeMessage, SecretKeyInput, SignerAvailability, UnixTimestamp, 8 }; 9 use radroots_transport_nostr::RelayEndpoint; 10 11 #[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)] 12 pub struct DurableRequestId(String); 13 14 impl DurableRequestId { 15 /// Validates a canonical caller-generated UUIDv7 idempotency key. 16 /// 17 /// # Errors 18 /// 19 /// Returns a safe validation error when the value is not canonical UUIDv7 text. 20 pub fn parse(value: impl AsRef<str>) -> Result<Self, SafeError> { 21 let value = value.as_ref(); 22 if value.len() != 36 23 || !value.is_ascii() 24 || value.bytes().enumerate().any(|(index, byte)| match index { 25 8 | 13 | 18 | 23 => byte != b'-', 26 19 => !matches!(byte, b'8' | b'9' | b'a' | b'b'), 27 _ => !matches!(byte, b'0'..=b'9' | b'a'..=b'f'), 28 }) 29 { 30 return Err(invalid_request_id()); 31 } 32 let parsed = uuid::Uuid::parse_str(value).map_err(|_| invalid_request_id())?; 33 if parsed.get_version_num() != 7 || parsed.hyphenated().to_string() != value { 34 return Err(invalid_request_id()); 35 } 36 Ok(Self(value.to_owned())) 37 } 38 39 #[must_use] 40 pub fn new_v7() -> Self { 41 Self(uuid::Uuid::now_v7().hyphenated().to_string()) 42 } 43 44 #[must_use] 45 pub fn as_str(&self) -> &str { 46 &self.0 47 } 48 } 49 50 #[derive(Clone, Copy, Debug, Eq, PartialEq)] 51 pub enum DurableOperationKind { 52 Create, 53 Import, 54 Repair, 55 Remove, 56 } 57 58 #[derive(Clone, Copy, Debug, Eq, PartialEq)] 59 pub enum DurableOperationPhase { 60 IntentRecorded, 61 CredentialWritten, 62 MetadataCommitted, 63 SelectionCommitted, 64 CompensationPending, 65 CredentialDeleted, 66 MetadataDeleted, 67 Finalized, 68 } 69 70 #[derive(Clone, Copy, Debug, Eq, PartialEq)] 71 pub enum DurableTerminalOutcome { 72 Completed, 73 Cancelled, 74 Failed, 75 } 76 77 #[derive(Clone, Copy, Debug, Eq, PartialEq)] 78 pub struct OperationPriorState { 79 selected_identity: Option<PublicKey>, 80 binding_availability: Option<SignerAvailability>, 81 } 82 83 impl OperationPriorState { 84 #[must_use] 85 pub const fn new( 86 selected_identity: Option<PublicKey>, 87 binding_availability: Option<SignerAvailability>, 88 ) -> Self { 89 Self { 90 selected_identity, 91 binding_availability, 92 } 93 } 94 95 #[must_use] 96 pub const fn selected_identity(self) -> Option<PublicKey> { 97 self.selected_identity 98 } 99 100 #[must_use] 101 pub const fn binding_availability(self) -> Option<SignerAvailability> { 102 self.binding_availability 103 } 104 } 105 106 #[derive(Clone, Debug, Eq, PartialEq)] 107 pub struct DurableOperationReceipt { 108 request_id: DurableRequestId, 109 identity: PublicKey, 110 outcome: DurableTerminalOutcome, 111 resulting_revision: Option<u64>, 112 completed_at: UnixTimestamp, 113 } 114 115 impl DurableOperationReceipt { 116 #[must_use] 117 pub const fn new( 118 request_id: DurableRequestId, 119 identity: PublicKey, 120 outcome: DurableTerminalOutcome, 121 resulting_revision: Option<u64>, 122 completed_at: UnixTimestamp, 123 ) -> Self { 124 Self { 125 request_id, 126 identity, 127 outcome, 128 resulting_revision, 129 completed_at, 130 } 131 } 132 133 #[must_use] 134 pub const fn request_id(&self) -> &DurableRequestId { 135 &self.request_id 136 } 137 138 #[must_use] 139 pub const fn identity(&self) -> PublicKey { 140 self.identity 141 } 142 143 #[must_use] 144 pub const fn outcome(&self) -> DurableTerminalOutcome { 145 self.outcome 146 } 147 148 #[must_use] 149 pub const fn resulting_revision(&self) -> Option<u64> { 150 self.resulting_revision 151 } 152 153 #[must_use] 154 pub const fn completed_at(&self) -> UnixTimestamp { 155 self.completed_at 156 } 157 } 158 159 #[derive(Clone, Debug, Eq, PartialEq)] 160 pub struct DurableIdentityOperation { 161 request_id: DurableRequestId, 162 kind: DurableOperationKind, 163 identity: PublicKey, 164 expected_revision: Option<u64>, 165 phase: DurableOperationPhase, 166 prior: OperationPriorState, 167 updated_at: UnixTimestamp, 168 diagnostic: Option<OperationDiagnostic>, 169 terminal: Option<DurableOperationReceipt>, 170 } 171 172 impl DurableIdentityOperation { 173 #[allow(clippy::too_many_arguments)] 174 #[must_use] 175 pub const fn new( 176 request_id: DurableRequestId, 177 kind: DurableOperationKind, 178 identity: PublicKey, 179 expected_revision: Option<u64>, 180 phase: DurableOperationPhase, 181 prior: OperationPriorState, 182 updated_at: UnixTimestamp, 183 diagnostic: Option<OperationDiagnostic>, 184 terminal: Option<DurableOperationReceipt>, 185 ) -> Self { 186 Self { 187 request_id, 188 kind, 189 identity, 190 expected_revision, 191 phase, 192 prior, 193 updated_at, 194 diagnostic, 195 terminal, 196 } 197 } 198 199 #[must_use] 200 pub const fn request_id(&self) -> &DurableRequestId { 201 &self.request_id 202 } 203 #[must_use] 204 pub const fn kind(&self) -> DurableOperationKind { 205 self.kind 206 } 207 #[must_use] 208 pub const fn identity(&self) -> PublicKey { 209 self.identity 210 } 211 #[must_use] 212 pub const fn expected_revision(&self) -> Option<u64> { 213 self.expected_revision 214 } 215 #[must_use] 216 pub const fn phase(&self) -> DurableOperationPhase { 217 self.phase 218 } 219 #[must_use] 220 pub const fn prior(&self) -> OperationPriorState { 221 self.prior 222 } 223 #[must_use] 224 pub const fn updated_at(&self) -> UnixTimestamp { 225 self.updated_at 226 } 227 #[must_use] 228 pub const fn diagnostic(&self) -> Option<OperationDiagnostic> { 229 self.diagnostic 230 } 231 #[must_use] 232 pub const fn terminal(&self) -> Option<&DurableOperationReceipt> { 233 self.terminal.as_ref() 234 } 235 } 236 237 #[derive(Clone, Debug, Eq, PartialEq)] 238 pub enum DurableOperationStart { 239 Started(DurableIdentityOperation), 240 Existing(DurableIdentityOperation), 241 } 242 243 const fn invalid_request_id() -> SafeError { 244 SafeError::new( 245 SafeErrorCode::InvalidApplicationState, 246 SafeMessage::new("The request identifier is invalid."), 247 ) 248 } 249 250 pub type BoxFuture<'a, T> = Pin<Box<dyn Future<Output = T> + Send + 'a>>; 251 252 #[derive(Clone, Copy, Debug, Eq, PartialEq)] 253 pub enum ProfileRefreshStatus { 254 Success, 255 Offline, 256 InvalidData, 257 } 258 259 #[derive(Clone, Copy, Debug, Eq, PartialEq)] 260 pub enum RelayFetchCompleteness { 261 Complete, 262 Partial, 263 } 264 265 #[derive(Clone, Debug, Eq, PartialEq)] 266 pub struct ProfileFetchResult { 267 candidate: Option<Kind0ProfileCandidate>, 268 completeness: RelayFetchCompleteness, 269 } 270 271 impl ProfileFetchResult { 272 #[must_use] 273 pub const fn complete(candidate: Option<Kind0ProfileCandidate>) -> Self { 274 Self { 275 candidate, 276 completeness: RelayFetchCompleteness::Complete, 277 } 278 } 279 280 #[must_use] 281 pub const fn partial(candidate: Option<Kind0ProfileCandidate>) -> Self { 282 Self { 283 candidate, 284 completeness: RelayFetchCompleteness::Partial, 285 } 286 } 287 288 #[must_use] 289 pub fn into_parts(self) -> (Option<Kind0ProfileCandidate>, RelayFetchCompleteness) { 290 (self.candidate, self.completeness) 291 } 292 } 293 294 #[derive(Clone, Debug, Eq, PartialEq)] 295 pub struct CachedProfile { 296 candidate: Kind0ProfileCandidate, 297 refreshed_at: UnixTimestamp, 298 refresh_status: ProfileRefreshStatus, 299 } 300 301 #[derive(Clone, Copy, Debug, Eq, PartialEq)] 302 pub enum IdentityPreferenceKey { 303 NamespaceProbe, 304 } 305 306 #[cfg(test)] 307 #[derive(Clone, Copy, Debug, Eq, PartialEq)] 308 pub enum IdentityOperationKind { 309 Add, 310 Import, 311 Remove, 312 } 313 314 #[cfg(test)] 315 #[derive(Clone, Copy, Debug, Eq, PartialEq)] 316 pub enum IdentityOperationPhase { 317 IntentRecorded, 318 CredentialWritten, 319 MetadataCommitted, 320 CompensationPending, 321 CredentialDeleted, 322 MetadataDeleted, 323 } 324 325 #[derive(Clone, Copy, Debug, Eq, PartialEq)] 326 pub enum OperationDiagnostic { 327 StorageUnavailable, 328 KeyringUnavailable, 329 CredentialMissing, 330 CompensationFailed, 331 Conflict, 332 Expired, 333 } 334 335 #[cfg(test)] 336 #[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] 337 pub struct OperationId(u64); 338 339 #[cfg(test)] 340 impl OperationId { 341 #[must_use] 342 pub const fn from_raw(value: u64) -> Self { 343 Self(value) 344 } 345 346 #[must_use] 347 pub const fn as_raw(self) -> u64 { 348 self.0 349 } 350 } 351 352 #[cfg(test)] 353 #[derive(Clone, Debug, Eq, PartialEq)] 354 pub struct PendingIdentityOperation { 355 id: OperationId, 356 kind: IdentityOperationKind, 357 subject: PublicKey, 358 phase: IdentityOperationPhase, 359 updated_at: UnixTimestamp, 360 diagnostic: Option<OperationDiagnostic>, 361 } 362 363 #[cfg(test)] 364 impl PendingIdentityOperation { 365 #[must_use] 366 pub const fn new( 367 id: OperationId, 368 kind: IdentityOperationKind, 369 subject: PublicKey, 370 phase: IdentityOperationPhase, 371 updated_at: UnixTimestamp, 372 diagnostic: Option<OperationDiagnostic>, 373 ) -> Self { 374 Self { 375 id, 376 kind, 377 subject, 378 phase, 379 updated_at, 380 diagnostic, 381 } 382 } 383 384 #[must_use] 385 pub const fn id(&self) -> OperationId { 386 self.id 387 } 388 #[must_use] 389 pub const fn kind(&self) -> IdentityOperationKind { 390 self.kind 391 } 392 #[must_use] 393 pub const fn subject(&self) -> PublicKey { 394 self.subject 395 } 396 #[must_use] 397 pub const fn phase(&self) -> IdentityOperationPhase { 398 self.phase 399 } 400 #[must_use] 401 pub const fn updated_at(&self) -> UnixTimestamp { 402 self.updated_at 403 } 404 #[must_use] 405 pub const fn diagnostic(&self) -> Option<OperationDiagnostic> { 406 self.diagnostic 407 } 408 } 409 410 impl CachedProfile { 411 #[must_use] 412 pub const fn new( 413 candidate: Kind0ProfileCandidate, 414 refreshed_at: UnixTimestamp, 415 refresh_status: ProfileRefreshStatus, 416 ) -> Self { 417 Self { 418 candidate, 419 refreshed_at, 420 refresh_status, 421 } 422 } 423 424 #[must_use] 425 pub const fn candidate(&self) -> &Kind0ProfileCandidate { 426 &self.candidate 427 } 428 429 #[must_use] 430 pub const fn refreshed_at(&self) -> UnixTimestamp { 431 self.refreshed_at 432 } 433 434 #[must_use] 435 pub const fn refresh_status(&self) -> ProfileRefreshStatus { 436 self.refresh_status 437 } 438 } 439 440 pub trait IdentityRepository: Send + Sync { 441 /// Lists saved public identity records in deterministic order. 442 /// 443 /// # Errors 444 /// 445 /// Returns a safe storage error when records cannot be read. 446 fn list_identities(&self) -> BoxFuture<'_, Result<Vec<NostrIdentity>, SafeError>>; 447 /// Finds one saved public identity record. 448 /// 449 /// # Errors 450 /// 451 /// Returns a safe storage error when the lookup cannot complete. 452 fn find_identity( 453 &self, 454 public_key: PublicKey, 455 ) -> BoxFuture<'_, Result<Option<NostrIdentity>, SafeError>>; 456 /// Inserts one public identity record. 457 /// 458 /// # Errors 459 /// 460 /// Returns a safe storage error when the durable write fails. 461 fn insert_identity<'a>( 462 &'a self, 463 identity: &'a NostrIdentity, 464 ) -> BoxFuture<'a, Result<(), SafeError>>; 465 /// Updates one existing public identity record. 466 /// 467 /// # Errors 468 /// 469 /// Returns a safe storage or identity-not-found error when the durable 470 /// update cannot complete. 471 fn update_identity<'a>( 472 &'a self, 473 identity: &'a NostrIdentity, 474 ) -> BoxFuture<'a, Result<(), SafeError>>; 475 /// Removes one public identity record. 476 /// 477 /// # Errors 478 /// 479 /// Returns a safe storage error when the durable delete fails. 480 fn remove_identity(&self, public_key: PublicKey) -> BoxFuture<'_, Result<(), SafeError>>; 481 } 482 483 pub trait ProfileRepository: Send + Sync { 484 /// Loads cached public profile metadata. 485 /// 486 /// # Errors 487 /// 488 /// Returns a safe storage error when the cache cannot be read. 489 fn load_profile( 490 &self, 491 public_key: PublicKey, 492 ) -> BoxFuture<'_, Result<Option<CachedProfile>, SafeError>>; 493 /// Saves a verified kind-0 profile candidate. 494 /// 495 /// # Errors 496 /// 497 /// Returns a safe storage error when the cache cannot be committed. 498 fn save_profile<'a>( 499 &'a self, 500 profile: &'a CachedProfile, 501 ) -> BoxFuture<'a, Result<(), SafeError>>; 502 /// Records the result of a profile refresh without replacing cached metadata. 503 /// 504 /// # Errors 505 /// 506 /// Returns a safe storage error when the cache cannot be committed. 507 fn record_refresh_status<'a>( 508 &'a self, 509 public_key: PublicKey, 510 refreshed_at: UnixTimestamp, 511 status: ProfileRefreshStatus, 512 ) -> BoxFuture<'a, Result<(), SafeError>>; 513 /// Removes cached profile metadata for an identity. 514 /// 515 /// # Errors 516 /// 517 /// Returns a safe storage error when the cache cannot be deleted. 518 fn remove_profile(&self, public_key: PublicKey) -> BoxFuture<'_, Result<(), SafeError>>; 519 } 520 521 pub trait IdentityNamespaceRepository: Send + Sync { 522 /// Reads one internal non-secret identity-scoped value. 523 /// 524 /// # Errors 525 /// 526 /// Returns a safe storage error when the value cannot be read. 527 fn get_value<'a>( 528 &'a self, 529 owner: PublicKey, 530 key: IdentityPreferenceKey, 531 ) -> BoxFuture<'a, Result<Option<String>, SafeError>>; 532 /// Writes one internal non-secret identity-scoped value. 533 /// 534 /// # Errors 535 /// 536 /// Returns a safe storage error when the value cannot be committed. 537 fn set_value<'a>( 538 &'a self, 539 owner: PublicKey, 540 key: IdentityPreferenceKey, 541 value: &'a str, 542 ) -> BoxFuture<'a, Result<(), SafeError>>; 543 /// Removes all internal values owned by an identity. 544 /// 545 /// # Errors 546 /// 547 /// Returns a safe storage error when cleanup cannot be committed. 548 fn clear_owner(&self, owner: PublicKey) -> BoxFuture<'_, Result<(), SafeError>>; 549 } 550 551 pub trait AppStateRepository: Send + Sync { 552 /// Loads the persisted selected identity. 553 /// 554 /// # Errors 555 /// 556 /// Returns a safe storage error when application state cannot be read. 557 fn load_selected_identity(&self) -> BoxFuture<'_, Result<Option<PublicKey>, SafeError>>; 558 /// Persists the selected identity or the empty selection. 559 /// 560 /// # Errors 561 /// 562 /// Returns a safe storage error when application state cannot be committed. 563 fn save_selected_identity( 564 &self, 565 public_key: Option<PublicKey>, 566 ) -> BoxFuture<'_, Result<(), SafeError>>; 567 } 568 569 #[cfg(test)] 570 pub trait OperationJournal: Send + Sync { 571 /// Records one cross-resource identity operation intent. 572 /// 573 /// # Errors 574 /// 575 /// Returns a safe storage error when the entry cannot be committed. 576 fn begin_operation<'a>( 577 &'a self, 578 kind: IdentityOperationKind, 579 subject: PublicKey, 580 updated_at: UnixTimestamp, 581 ) -> BoxFuture<'a, Result<OperationId, SafeError>>; 582 /// Advances an operation to a durable recovery phase. 583 /// 584 /// # Errors 585 /// 586 /// Returns a safe storage error when the entry cannot be updated. 587 fn update_operation<'a>( 588 &'a self, 589 id: OperationId, 590 phase: IdentityOperationPhase, 591 updated_at: UnixTimestamp, 592 diagnostic: Option<OperationDiagnostic>, 593 ) -> BoxFuture<'a, Result<(), SafeError>>; 594 /// Loads all unfinished operations in deterministic order. 595 /// 596 /// # Errors 597 /// 598 /// Returns a safe storage error when entries cannot be read. 599 fn list_pending_operations( 600 &self, 601 ) -> BoxFuture<'_, Result<Vec<PendingIdentityOperation>, SafeError>>; 602 /// Deletes one fully reconciled operation entry. 603 /// 604 /// # Errors 605 /// 606 /// Returns a safe storage error when finalization cannot be committed. 607 fn finalize_operation(&self, id: OperationId) -> BoxFuture<'_, Result<(), SafeError>>; 608 } 609 610 pub trait DurableOperationRepository: Send + Sync { 611 /// Records one idempotent durable operation or returns the existing matching request. 612 /// 613 /// # Errors 614 /// 615 /// Returns a safe conflict or storage error when the request cannot be recorded. 616 #[allow(clippy::too_many_arguments)] 617 fn begin_durable_operation<'a>( 618 &'a self, 619 request_id: &'a DurableRequestId, 620 kind: DurableOperationKind, 621 identity: PublicKey, 622 expected_revision: Option<u64>, 623 prior: OperationPriorState, 624 updated_at: UnixTimestamp, 625 ) -> BoxFuture<'a, Result<DurableOperationStart, SafeError>>; 626 /// Loads one durable operation by its idempotency key. 627 /// 628 /// # Errors 629 /// 630 /// Returns a safe storage error when the lookup cannot complete. 631 fn load_durable_operation<'a>( 632 &'a self, 633 request_id: &'a DurableRequestId, 634 ) -> BoxFuture<'a, Result<Option<DurableIdentityOperation>, SafeError>>; 635 /// Advances one operation only from the caller's expected phase. 636 /// 637 /// # Errors 638 /// 639 /// Returns a safe conflict or storage error when the transition cannot commit. 640 fn advance_durable_operation<'a>( 641 &'a self, 642 request_id: &'a DurableRequestId, 643 expected_phase: DurableOperationPhase, 644 next_phase: DurableOperationPhase, 645 updated_at: UnixTimestamp, 646 diagnostic: Option<OperationDiagnostic>, 647 ) -> BoxFuture<'a, Result<DurableIdentityOperation, SafeError>>; 648 /// Finalizes one operation and durably retains its recoverable receipt. 649 /// 650 /// # Errors 651 /// 652 /// Returns a safe conflict or storage error when finalization cannot commit. 653 fn finalize_durable_operation<'a>( 654 &'a self, 655 request_id: &'a DurableRequestId, 656 expected_phase: DurableOperationPhase, 657 outcome: DurableTerminalOutcome, 658 resulting_revision: Option<u64>, 659 updated_at: UnixTimestamp, 660 ) -> BoxFuture<'a, Result<DurableOperationReceipt, SafeError>>; 661 /// Lists unfinished operations in deterministic request order. 662 /// 663 /// # Errors 664 /// 665 /// Returns a safe storage error when operations cannot be read. 666 fn list_unfinished_durable_operations( 667 &self, 668 ) -> BoxFuture<'_, Result<Vec<DurableIdentityOperation>, SafeError>>; 669 } 670 671 pub trait NostrClient: Send + Sync { 672 fn fetch_profile<'a>( 673 &'a self, 674 public_key: PublicKey, 675 relays: &'a [RelayEndpoint], 676 deadline: Instant, 677 ) -> BoxFuture<'a, Result<ProfileFetchResult, SafeError>>; 678 } 679 680 pub struct GeneratedKeyMaterial { 681 public_key: PublicKey, 682 npub: Npub, 683 secret: SecretKeyInput, 684 nsec: Nsec, 685 } 686 687 impl GeneratedKeyMaterial { 688 #[must_use] 689 pub const fn new( 690 public_key: PublicKey, 691 npub: Npub, 692 secret: SecretKeyInput, 693 nsec: Nsec, 694 ) -> Self { 695 Self { 696 public_key, 697 npub, 698 secret, 699 nsec, 700 } 701 } 702 703 #[must_use] 704 pub fn into_parts(self) -> (PublicKey, Npub, SecretKeyInput, Nsec) { 705 (self.public_key, self.npub, self.secret, self.nsec) 706 } 707 } 708 709 pub struct ImportedKeyMaterial { 710 public_key: PublicKey, 711 npub: Npub, 712 secret: SecretKeyInput, 713 } 714 715 impl ImportedKeyMaterial { 716 #[must_use] 717 pub const fn new(public_key: PublicKey, npub: Npub, secret: SecretKeyInput) -> Self { 718 Self { 719 public_key, 720 npub, 721 secret, 722 } 723 } 724 725 #[must_use] 726 pub fn into_parts(self) -> (PublicKey, Npub, SecretKeyInput) { 727 (self.public_key, self.npub, self.secret) 728 } 729 } 730 731 pub trait KeyMaterialProvider: Send + Sync { 732 /// Generates one keypair from host-provided cryptographic entropy. 733 /// 734 /// # Errors 735 /// 736 /// Returns a redacted key or entropy error. 737 fn generate(&self) -> Result<GeneratedKeyMaterial, SafeError>; 738 739 /// Canonicalizes imported secret material and derives its public identity. 740 /// 741 /// # Errors 742 /// 743 /// Returns a redacted validation error. 744 fn import(&self, input: SecretKeyInput) -> Result<ImportedKeyMaterial, SafeError>; 745 } 746 747 pub trait Clock: Send + Sync { 748 fn now(&self) -> UnixTimestamp; 749 } 750 751 #[cfg(test)] 752 mod tests { 753 use std::time::Instant; 754 755 use std::sync::Mutex; 756 757 use harvestcircle_domain::{NostrIdentity, PublicKey, SafeError, UnixTimestamp}; 758 use radroots_transport_nostr::RelayEndpoint; 759 760 use super::{ 761 AppStateRepository, BoxFuture, CachedProfile, Clock, DurableOperationReceipt, 762 DurableRequestId, DurableTerminalOutcome, IdentityNamespaceRepository, 763 IdentityOperationKind, IdentityOperationPhase, IdentityPreferenceKey, IdentityRepository, 764 NostrClient, OperationDiagnostic, OperationId, OperationJournal, PendingIdentityOperation, 765 ProfileFetchResult, ProfileRefreshStatus, ProfileRepository, 766 }; 767 768 #[tokio::test] 769 async fn durable_request_ids_and_terminal_receipts_are_bounded_and_public() { 770 let request = 771 DurableRequestId::parse("01890f3e-7b1c-7000-8000-000000000031").expect("request id"); 772 let receipt = DurableOperationReceipt::new( 773 request.clone(), 774 PublicKey::from_bytes([7; 32]).expect("valid public key"), 775 DurableTerminalOutcome::Completed, 776 Some(42), 777 UnixTimestamp::from_seconds(7).expect("time"), 778 ); 779 assert_eq!(receipt.request_id(), &request); 780 assert_eq!(receipt.resulting_revision(), Some(42)); 781 assert_eq!(receipt.completed_at().as_seconds(), 7); 782 for invalid in [ 783 "", 784 "contains space", 785 "01890f3e-7b1c-4000-8000-000000000031", 786 "01890F3E-7B1C-7000-8000-000000000031", 787 "01890f3e-7b1c-6000-8000-000000000031", 788 "01890f3e-7b1c-7000-7000-000000000031", 789 ] { 790 assert!(DurableRequestId::parse(invalid).is_err()); 791 } 792 let oversized = "0".repeat(1_000_000); 793 assert!(DurableRequestId::parse(&oversized).is_err()); 794 } 795 796 #[tokio::test] 797 async fn durable_request_id_source_is_stateless_canonical_and_unique() { 798 let first = DurableRequestId::new_v7(); 799 let second = DurableRequestId::new_v7(); 800 801 assert_eq!(first.as_str().len(), 36); 802 assert_eq!(first.as_str().as_bytes()[14], b'7'); 803 assert!(matches!( 804 first.as_str().as_bytes()[19], 805 b'8' | b'9' | b'a' | b'b' 806 )); 807 assert_eq!( 808 DurableRequestId::parse(first.as_str()).expect("generated UUIDv7"), 809 first 810 ); 811 assert_ne!(first, second); 812 } 813 814 #[derive(Default)] 815 struct FakePorts { 816 selected: Mutex<Option<PublicKey>>, 817 } 818 819 impl IdentityRepository for FakePorts { 820 fn list_identities(&self) -> BoxFuture<'_, Result<Vec<NostrIdentity>, SafeError>> { 821 Box::pin(async { Ok(Vec::new()) }) 822 } 823 824 fn find_identity( 825 &self, 826 _public_key: PublicKey, 827 ) -> BoxFuture<'_, Result<Option<NostrIdentity>, SafeError>> { 828 Box::pin(async { Ok(None) }) 829 } 830 831 fn insert_identity<'a>( 832 &'a self, 833 _identity: &'a NostrIdentity, 834 ) -> BoxFuture<'a, Result<(), SafeError>> { 835 Box::pin(async { Ok(()) }) 836 } 837 838 fn update_identity<'a>( 839 &'a self, 840 _identity: &'a NostrIdentity, 841 ) -> BoxFuture<'a, Result<(), SafeError>> { 842 Box::pin(async { Ok(()) }) 843 } 844 845 fn remove_identity(&self, _public_key: PublicKey) -> BoxFuture<'_, Result<(), SafeError>> { 846 Box::pin(async { Ok(()) }) 847 } 848 } 849 850 impl ProfileRepository for FakePorts { 851 fn load_profile( 852 &self, 853 _public_key: PublicKey, 854 ) -> BoxFuture<'_, Result<Option<CachedProfile>, SafeError>> { 855 Box::pin(async { Ok(None) }) 856 } 857 858 fn save_profile<'a>( 859 &'a self, 860 _profile: &'a CachedProfile, 861 ) -> BoxFuture<'a, Result<(), SafeError>> { 862 Box::pin(async { Ok(()) }) 863 } 864 865 fn record_refresh_status<'a>( 866 &'a self, 867 _public_key: PublicKey, 868 _refreshed_at: UnixTimestamp, 869 _status: ProfileRefreshStatus, 870 ) -> BoxFuture<'a, Result<(), SafeError>> { 871 Box::pin(async { Ok(()) }) 872 } 873 874 fn remove_profile(&self, _public_key: PublicKey) -> BoxFuture<'_, Result<(), SafeError>> { 875 Box::pin(async { Ok(()) }) 876 } 877 } 878 879 impl IdentityNamespaceRepository for FakePorts { 880 fn get_value<'a>( 881 &'a self, 882 _owner: PublicKey, 883 _key: IdentityPreferenceKey, 884 ) -> BoxFuture<'a, Result<Option<String>, SafeError>> { 885 Box::pin(async { Ok(None) }) 886 } 887 888 fn set_value<'a>( 889 &'a self, 890 _owner: PublicKey, 891 _key: IdentityPreferenceKey, 892 _value: &'a str, 893 ) -> BoxFuture<'a, Result<(), SafeError>> { 894 Box::pin(async { Ok(()) }) 895 } 896 897 fn clear_owner(&self, _owner: PublicKey) -> BoxFuture<'_, Result<(), SafeError>> { 898 Box::pin(async { Ok(()) }) 899 } 900 } 901 902 impl AppStateRepository for FakePorts { 903 fn load_selected_identity(&self) -> BoxFuture<'_, Result<Option<PublicKey>, SafeError>> { 904 Box::pin(async move { Ok(*self.selected.lock().expect("selected lock")) }) 905 } 906 907 fn save_selected_identity( 908 &self, 909 public_key: Option<PublicKey>, 910 ) -> BoxFuture<'_, Result<(), SafeError>> { 911 Box::pin(async move { 912 *self.selected.lock().expect("selected lock") = public_key; 913 Ok(()) 914 }) 915 } 916 } 917 918 impl OperationJournal for FakePorts { 919 fn begin_operation<'a>( 920 &'a self, 921 _kind: IdentityOperationKind, 922 _subject: PublicKey, 923 _updated_at: UnixTimestamp, 924 ) -> BoxFuture<'a, Result<OperationId, SafeError>> { 925 Box::pin(async { Ok(OperationId::from_raw(1)) }) 926 } 927 928 fn update_operation<'a>( 929 &'a self, 930 _id: OperationId, 931 _phase: IdentityOperationPhase, 932 _updated_at: UnixTimestamp, 933 _diagnostic: Option<OperationDiagnostic>, 934 ) -> BoxFuture<'a, Result<(), SafeError>> { 935 Box::pin(async { Ok(()) }) 936 } 937 938 fn list_pending_operations( 939 &self, 940 ) -> BoxFuture<'_, Result<Vec<PendingIdentityOperation>, SafeError>> { 941 Box::pin(async { Ok(Vec::new()) }) 942 } 943 944 fn finalize_operation(&self, _id: OperationId) -> BoxFuture<'_, Result<(), SafeError>> { 945 Box::pin(async { Ok(()) }) 946 } 947 } 948 949 impl NostrClient for FakePorts { 950 fn fetch_profile<'a>( 951 &'a self, 952 _public_key: PublicKey, 953 _relays: &'a [RelayEndpoint], 954 _deadline: Instant, 955 ) -> BoxFuture<'a, Result<ProfileFetchResult, SafeError>> { 956 Box::pin(async { Ok(ProfileFetchResult::complete(None)) }) 957 } 958 } 959 960 impl Clock for FakePorts { 961 fn now(&self) -> UnixTimestamp { 962 UnixTimestamp::from_seconds(1).expect("valid fake time") 963 } 964 } 965 966 fn assert_send_sync<T: Send + Sync>() {} 967 968 #[tokio::test] 969 async fn ports_accept_send_sync_test_fakes() { 970 assert_send_sync::<FakePorts>(); 971 972 let ports = FakePorts::default(); 973 ports 974 .save_selected_identity(Some( 975 PublicKey::from_bytes([7_u8; 32]).expect("valid public key"), 976 )) 977 .await 978 .expect("save selection"); 979 assert_eq!( 980 ports 981 .load_selected_identity() 982 .await 983 .expect("load selection"), 984 Some(PublicKey::from_bytes([7_u8; 32]).expect("valid public key")) 985 ); 986 assert_eq!(ports.now().as_seconds(), 1); 987 } 988 }