From c951afd83337f22a69ecab068f02a5bbf0a2a9aa Mon Sep 17 00:00:00 2001 From: andreyudentsovottofeller Date: Wed, 4 Feb 2026 14:52:13 +0100 Subject: [PATCH 1/3] Implement Foreign NFC processor --- bedrock/src/migration/mod.rs | 3 + bedrock/src/migration/processors/mod.rs | 7 + .../processors/nfc_refresh_processor.rs | 223 ++++++++++++++++++ 3 files changed, 233 insertions(+) create mode 100644 bedrock/src/migration/processors/nfc_refresh_processor.rs diff --git a/bedrock/src/migration/mod.rs b/bedrock/src/migration/mod.rs index 14da91c8..dcd19da2 100644 --- a/bedrock/src/migration/mod.rs +++ b/bedrock/src/migration/mod.rs @@ -80,4 +80,7 @@ pub mod processors; pub use controller::{MigrationController, MigrationRunSummary}; pub use error::MigrationError; pub use processor::{MigrationProcessor, ProcessorResult}; +pub use processors::{ + ForeignNfcProcessor, ForeignProcessorResult, NfcRefreshProcessor, +}; pub use state::{MigrationRecord, MigrationStatus}; diff --git a/bedrock/src/migration/processors/mod.rs b/bedrock/src/migration/processors/mod.rs index 9e18149b..b3e1b904 100644 --- a/bedrock/src/migration/processors/mod.rs +++ b/bedrock/src/migration/processors/mod.rs @@ -3,3 +3,10 @@ /// This module contains skeleton implementations of migration processors /// that can be used as templates for actual migrations. mod example_processor; + +/// NFC credential refresh processor +pub mod nfc_refresh_processor; + +pub use nfc_refresh_processor::{ + ForeignNfcProcessor, ForeignProcessorResult, NfcRefreshProcessor, +}; diff --git a/bedrock/src/migration/processors/nfc_refresh_processor.rs b/bedrock/src/migration/processors/nfc_refresh_processor.rs new file mode 100644 index 00000000..c15ac5db --- /dev/null +++ b/bedrock/src/migration/processors/nfc_refresh_processor.rs @@ -0,0 +1,223 @@ +//! NFC Credential Refresh Processor +//! +//! Foreign trait for NFC refresh - the actual logic runs in the native app (iOS/Android) +//! because it needs access to Oxide, WalletKit, and CredentialStorage. +//! +//! ## Flow +//! +//! 1. App implements `ForeignNfcProcessor` +//! 2. `is_applicable()`: check if PCP exists && no v4 credential yet +//! 3. `execute()`: Oxide payload → WalletKit API call → save credential +//! +//! ## Example (Swift) +//! +//! ```swift +//! class NfcProcessorImpl: ForeignNfcProcessor { +//! func isApplicable() async throws -> Bool { +//! return hasDocumentPcp(fileSystem: fs) && !credentialStorage.hasNfcCredential() +//! } +//! +//! func execute() async throws -> ForeignProcessorResult { +//! // 1. Get payload from Oxide +//! guard let payload = try prepareNfcRefreshPayload( +//! fileSystem: fs, +//! documentEncryptionKey: key, +//! identity: identity, +//! sub: sub +//! ) else { return .terminal(errorCode: "NO_PCP", errorMessage: "No PCP") } +//! +//! // 2. Generate auth headers +//! let zkpHeader = try walletKit.generateZkpHeader(identity: identity) +//! let attestation = try await getAttestationToken(aud: "toolsforhumanity.com") +//! +//! // 3. Call WalletKit API +//! let credential = try await walletKit.refreshNfcCredential( +//! requestBody: payload.requestBody, +//! zkpHeader: zkpHeader, +//! attestation: attestation +//! ) +//! +//! // 4. Save +//! try credentialStorage.saveCredential(credential) +//! return .success +//! } +//! } +//! +//! // Register +//! let processor = NfcRefreshProcessor(foreign: NfcProcessorImpl(...)) +//! controller.register(processor) +//! ``` + +use crate::migration::error::MigrationError; +use crate::migration::processor::{MigrationProcessor, ProcessorResult}; +use async_trait::async_trait; +use log::info; +use std::sync::Arc; + +/// Result type for foreign processor execution (FFI-friendly version of `ProcessorResult`) +#[derive(Debug, Clone, uniffi::Enum)] +pub enum ForeignProcessorResult { + /// Migration completed successfully + Success, + /// Transient failure, can retry + Retryable { + /// Error code + error_code: String, + /// Error message + error_message: String, + /// Retry delay in ms + retry_after_ms: Option, + }, + /// Permanent failure, don't retry + Terminal { + /// Error code + error_code: String, + /// Error message + error_message: String, + }, + /// Needs user action + BlockedUserAction { + /// Reason + reason: String, + }, +} + +impl From for ProcessorResult { + fn from(result: ForeignProcessorResult) -> Self { + match result { + ForeignProcessorResult::Success => Self::Success, + ForeignProcessorResult::Retryable { + error_code, + error_message, + retry_after_ms, + } => Self::Retryable { + error_code, + error_message, + retry_after_ms, + }, + ForeignProcessorResult::Terminal { + error_code, + error_message, + } => Self::Terminal { + error_code, + error_message, + }, + ForeignProcessorResult::BlockedUserAction { reason } => { + Self::BlockedUserAction { reason } + } + } + } +} + +/// Implement this trait in the native app (iOS/Android). +/// +/// Dependencies needed: +/// - `Oxide.hasDocumentPcp()`, `Oxide.prepareNfcRefreshPayload()` +/// - `WalletKit.refreshNfcCredential()` +/// - `CredentialStorage` +#[uniffi::export(with_foreign)] +#[async_trait] +pub trait ForeignNfcProcessor: Send + Sync { + /// Return true if: `hasDocumentPcp() && !credentialStorage.hasNfcCredential()` + async fn is_applicable(&self) -> Result; + + /// Call Oxide → `WalletKit` → `CredentialStorage` + async fn execute(&self) -> Result; +} + +/// Wraps `ForeignNfcProcessor` for use with `MigrationController` +#[derive(uniffi::Object)] +pub struct NfcRefreshProcessor { + foreign: Arc, +} + +#[uniffi::export] +impl NfcRefreshProcessor { + /// Create new processor with foreign implementation + #[uniffi::constructor] + pub fn new(foreign: Arc) -> Arc { + Arc::new(Self { foreign }) + } +} + +#[async_trait] +impl MigrationProcessor for NfcRefreshProcessor { + fn migration_id(&self) -> String { + "worldid.credentials.nfc.refresh.v1".to_string() + } + + async fn is_applicable(&self) -> Result { + info!("NFC refresh: checking applicability"); + self.foreign.is_applicable().await + } + + async fn execute(&self) -> Result { + info!("NFC refresh: executing"); + self.foreign.execute().await.map(Into::into) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + struct MockForeignProcessor { + applicable: bool, + result: ForeignProcessorResult, + } + + #[async_trait] + impl ForeignNfcProcessor for MockForeignProcessor { + async fn is_applicable(&self) -> Result { + Ok(self.applicable) + } + + async fn execute(&self) -> Result { + Ok(self.result.clone()) + } + } + + #[tokio::test] + async fn test_processor_delegates_to_foreign() { + let foreign = Arc::new(MockForeignProcessor { + applicable: true, + result: ForeignProcessorResult::Success, + }); + let processor = NfcRefreshProcessor::new(foreign); + + assert!(processor.is_applicable().await.unwrap()); + assert!(matches!( + processor.execute().await.unwrap(), + ProcessorResult::Success + )); + } + + #[tokio::test] + async fn test_processor_not_applicable() { + let foreign = Arc::new(MockForeignProcessor { + applicable: false, + result: ForeignProcessorResult::Success, + }); + let processor = NfcRefreshProcessor::new(foreign); + assert!(!processor.is_applicable().await.unwrap()); + } + + #[tokio::test] + async fn test_terminal_error() { + let foreign = Arc::new(MockForeignProcessor { + applicable: true, + result: ForeignProcessorResult::Terminal { + error_code: "DOCUMENT_EXPIRED".to_string(), + error_message: "Document expired".to_string(), + }, + }); + let processor = NfcRefreshProcessor::new(foreign); + + match processor.execute().await.unwrap() { + ProcessorResult::Terminal { error_code, .. } => { + assert_eq!(error_code, "DOCUMENT_EXPIRED"); + } + _ => panic!("Expected Terminal"), + } + } +} From 276ec2fd9a029c6631ce62bff08b164883f73249 Mon Sep 17 00:00:00 2001 From: andreyudentsovottofeller Date: Wed, 4 Feb 2026 15:05:15 +0100 Subject: [PATCH 2/3] Change naming --- bedrock/src/migration/mod.rs | 2 +- bedrock/src/migration/processors/mod.rs | 2 +- .../processors/nfc_refresh_processor.rs | 30 +++++++++---------- 3 files changed, 17 insertions(+), 17 deletions(-) diff --git a/bedrock/src/migration/mod.rs b/bedrock/src/migration/mod.rs index dcd19da2..a0e39432 100644 --- a/bedrock/src/migration/mod.rs +++ b/bedrock/src/migration/mod.rs @@ -81,6 +81,6 @@ pub use controller::{MigrationController, MigrationRunSummary}; pub use error::MigrationError; pub use processor::{MigrationProcessor, ProcessorResult}; pub use processors::{ - ForeignNfcProcessor, ForeignProcessorResult, NfcRefreshProcessor, + ForeignNfcProcessor, NfcProcessorResult, NfcRefreshProcessor, }; pub use state::{MigrationRecord, MigrationStatus}; diff --git a/bedrock/src/migration/processors/mod.rs b/bedrock/src/migration/processors/mod.rs index b3e1b904..02188adf 100644 --- a/bedrock/src/migration/processors/mod.rs +++ b/bedrock/src/migration/processors/mod.rs @@ -8,5 +8,5 @@ mod example_processor; pub mod nfc_refresh_processor; pub use nfc_refresh_processor::{ - ForeignNfcProcessor, ForeignProcessorResult, NfcRefreshProcessor, + ForeignNfcProcessor, NfcProcessorResult, NfcRefreshProcessor, }; diff --git a/bedrock/src/migration/processors/nfc_refresh_processor.rs b/bedrock/src/migration/processors/nfc_refresh_processor.rs index c15ac5db..306467b0 100644 --- a/bedrock/src/migration/processors/nfc_refresh_processor.rs +++ b/bedrock/src/migration/processors/nfc_refresh_processor.rs @@ -17,7 +17,7 @@ //! return hasDocumentPcp(fileSystem: fs) && !credentialStorage.hasNfcCredential() //! } //! -//! func execute() async throws -> ForeignProcessorResult { +//! func execute() async throws -> NfcProcessorResult { //! // 1. Get payload from Oxide //! guard let payload = try prepareNfcRefreshPayload( //! fileSystem: fs, @@ -54,9 +54,9 @@ use async_trait::async_trait; use log::info; use std::sync::Arc; -/// Result type for foreign processor execution (FFI-friendly version of `ProcessorResult`) +/// Result type for NFC processor execution (FFI-friendly version of `ProcessorResult`) #[derive(Debug, Clone, uniffi::Enum)] -pub enum ForeignProcessorResult { +pub enum NfcProcessorResult { /// Migration completed successfully Success, /// Transient failure, can retry @@ -82,11 +82,11 @@ pub enum ForeignProcessorResult { }, } -impl From for ProcessorResult { - fn from(result: ForeignProcessorResult) -> Self { +impl From for ProcessorResult { + fn from(result: NfcProcessorResult) -> Self { match result { - ForeignProcessorResult::Success => Self::Success, - ForeignProcessorResult::Retryable { + NfcProcessorResult::Success => Self::Success, + NfcProcessorResult::Retryable { error_code, error_message, retry_after_ms, @@ -95,14 +95,14 @@ impl From for ProcessorResult { error_message, retry_after_ms, }, - ForeignProcessorResult::Terminal { + NfcProcessorResult::Terminal { error_code, error_message, } => Self::Terminal { error_code, error_message, }, - ForeignProcessorResult::BlockedUserAction { reason } => { + NfcProcessorResult::BlockedUserAction { reason } => { Self::BlockedUserAction { reason } } } @@ -122,7 +122,7 @@ pub trait ForeignNfcProcessor: Send + Sync { async fn is_applicable(&self) -> Result; /// Call Oxide → `WalletKit` → `CredentialStorage` - async fn execute(&self) -> Result; + async fn execute(&self) -> Result; } /// Wraps `ForeignNfcProcessor` for use with `MigrationController` @@ -163,7 +163,7 @@ mod tests { struct MockForeignProcessor { applicable: bool, - result: ForeignProcessorResult, + result: NfcProcessorResult, } #[async_trait] @@ -172,7 +172,7 @@ mod tests { Ok(self.applicable) } - async fn execute(&self) -> Result { + async fn execute(&self) -> Result { Ok(self.result.clone()) } } @@ -181,7 +181,7 @@ mod tests { async fn test_processor_delegates_to_foreign() { let foreign = Arc::new(MockForeignProcessor { applicable: true, - result: ForeignProcessorResult::Success, + result: NfcProcessorResult::Success, }); let processor = NfcRefreshProcessor::new(foreign); @@ -196,7 +196,7 @@ mod tests { async fn test_processor_not_applicable() { let foreign = Arc::new(MockForeignProcessor { applicable: false, - result: ForeignProcessorResult::Success, + result: NfcProcessorResult::Success, }); let processor = NfcRefreshProcessor::new(foreign); assert!(!processor.is_applicable().await.unwrap()); @@ -206,7 +206,7 @@ mod tests { async fn test_terminal_error() { let foreign = Arc::new(MockForeignProcessor { applicable: true, - result: ForeignProcessorResult::Terminal { + result: NfcProcessorResult::Terminal { error_code: "DOCUMENT_EXPIRED".to_string(), error_message: "Document expired".to_string(), }, From 5d9d807ce458b3cc629628197acf48fc6f83de7b Mon Sep 17 00:00:00 2001 From: andreyudentsovottofeller Date: Wed, 4 Feb 2026 15:06:47 +0100 Subject: [PATCH 3/3] Formatting --- bedrock/src/migration/mod.rs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/bedrock/src/migration/mod.rs b/bedrock/src/migration/mod.rs index a0e39432..bbb3019a 100644 --- a/bedrock/src/migration/mod.rs +++ b/bedrock/src/migration/mod.rs @@ -80,7 +80,5 @@ pub mod processors; pub use controller::{MigrationController, MigrationRunSummary}; pub use error::MigrationError; pub use processor::{MigrationProcessor, ProcessorResult}; -pub use processors::{ - ForeignNfcProcessor, NfcProcessorResult, NfcRefreshProcessor, -}; +pub use processors::{ForeignNfcProcessor, NfcProcessorResult, NfcRefreshProcessor}; pub use state::{MigrationRecord, MigrationStatus};