diff --git a/CODEOWNERS b/CODEOWNERS index d4e467b..984738d 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -1 +1 @@ -* @paolodamico @decentralgabe @andy-t-wang @SeanROlszewski @Guardiola31337 +* @paolodamico @andy-t-wang @SeanROlszewski @Guardiola31337 diff --git a/Cargo.lock b/Cargo.lock index 3fe5ec5..8a0f9ef 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1310,9 +1310,11 @@ dependencies = [ "base64", "getrandom 0.2.16", "hex", + "js-sys", "reqwest", "ruint", "serde", + "serde-wasm-bindgen", "serde_json", "thiserror 1.0.69", "tiny-keccak", @@ -1321,32 +1323,8 @@ dependencies = [ "url", "urlencoding", "uuid", -] - -[[package]] -name = "idkit-uniffi" -version = "4.0.1" -dependencies = [ - "hex", - "idkit-core", - "serde_json", - "thiserror 1.0.69", - "tokio", - "uniffi", -] - -[[package]] -name = "idkit-wasm" -version = "4.0.1" -dependencies = [ - "idkit-core", - "js-sys", - "serde", - "serde-wasm-bindgen", - "serde_json", "wasm-bindgen", "wasm-bindgen-futures", - "wasm-bindgen-test", "web-sys", ] @@ -1556,16 +1534,6 @@ version = "2.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" -[[package]] -name = "minicov" -version = "0.3.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f27fe9f1cc3c22e1687f9446c2083c4c5fc7f0bcf1c7a86bdbded14985895b4b" -dependencies = [ - "cc", - "walkdir", -] - [[package]] name = "minimal-lexical" version = "0.2.1" @@ -2209,15 +2177,6 @@ version = "1.0.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" -[[package]] -name = "same-file" -version = "1.0.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" -dependencies = [ - "winapi-util", -] - [[package]] name = "scopeguard" version = "1.2.0" @@ -3043,16 +3002,6 @@ dependencies = [ "libc", ] -[[package]] -name = "walkdir" -version = "2.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" -dependencies = [ - "same-file", - "winapi-util", -] - [[package]] name = "want" version = "0.3.1" @@ -3135,30 +3084,6 @@ dependencies = [ "unicode-ident", ] -[[package]] -name = "wasm-bindgen-test" -version = "0.3.55" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bfc379bfb624eb59050b509c13e77b4eb53150c350db69628141abce842f2373" -dependencies = [ - "js-sys", - "minicov", - "wasm-bindgen", - "wasm-bindgen-futures", - "wasm-bindgen-test-macro", -] - -[[package]] -name = "wasm-bindgen-test-macro" -version = "0.3.55" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "085b2df989e1e6f9620c1311df6c996e83fe16f57792b272ce1e024ac16a90f1" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.108", -] - [[package]] name = "web-sys" version = "0.3.82" @@ -3197,15 +3122,6 @@ dependencies = [ "nom", ] -[[package]] -name = "winapi-util" -version = "0.1.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" -dependencies = [ - "windows-sys 0.61.2", -] - [[package]] name = "windows-link" version = "0.2.1" diff --git a/Cargo.toml b/Cargo.toml index 64de786..9dcd493 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,9 +3,7 @@ resolver = "2" members = [ "rust/core", - "rust/uniffi-bindings", "rust/uniffi-bindgen-bin", - "rust/wasm", ] [workspace.package] diff --git a/README.md b/README.md index c3dc90f..b5cd0fa 100644 --- a/README.md +++ b/README.md @@ -1,16 +1,17 @@ -# IDKit - World ID SDK (Rust Core) +# IDKit - World ID SDK for Relying Parties IDKit is the toolkit for identity online. With IDKit you can easily interact with the [World ID Protocol](https://world.org/world-id). ## Packages -### Core -- **`rust/core`**: Core Rust library with shared types, credential handling, session management, and cryptography - -### Language Bindings -- **`js/packages/core`**: ✅ JavaScript/TypeScript with WASM for browser & Node.js ([docs](./js/packages/core/README.md)) -- **`kotlin/`**: Kotlin bindings via UniFFI for Android/JVM -- **`swift/`**: Swift bindings via UniFFI for iOS/macOS +``` +world-id-core (https://github.com/worldcoin/world-id-protocol) +└── idkit-core (Rust Crate with base functionality) + ├── idkit-core-js — Raw bindings with base functionality [Package: idkit-core] + │ └── idkit — React package with UI support (Package: idkit) + ├── idkit-swift (Package: IDKit) + └── idkit-kotlin (Package: com.worldcoin.idkit) +``` ## Documentation @@ -18,4 +19,4 @@ All the technical docs for the World ID SDK, World ID Protocol, examples, guides ## License -MIT License - see [LICENSE](./LICENSE) for details. \ No newline at end of file +MIT License - see [LICENSE](./LICENSE) for details. diff --git a/rust/core/Cargo.toml b/rust/core/Cargo.toml index 995a555..4579218 100644 --- a/rust/core/Cargo.toml +++ b/rust/core/Cargo.toml @@ -7,6 +7,10 @@ repository.workspace = true authors.workspace = true description = "Core Rust implementation of IDKit - World ID SDK for Relying Parties" +[lib] +name = "idkit" +crate-type = ["rlib", "cdylib", "staticlib"] + [dependencies] serde = { workspace = true } serde_json = { workspace = true } @@ -22,6 +26,11 @@ ruint = { workspace = true } uniffi = { workspace = true, optional = true } uuid = { workspace = true, optional = true } urlencoding = { workspace = true, optional = true } +wasm-bindgen = { workspace = true, optional = true } +wasm-bindgen-futures = { workspace = true, optional = true } +js-sys = { workspace = true, optional = true } +web-sys = { workspace = true, optional = true } +serde-wasm-bindgen = { version = "0.6", optional = true } # Platform-specific HTTP client [target.'cfg(not(target_arch = "wasm32"))'.dependencies] @@ -39,9 +48,11 @@ native-crypto = ["aes-gcm"] # AES-256-GCM encryption for native platforms wasm-crypto = ["aes-gcm"] # AES-256-GCM encryption for WebAssembly # Language bindings -uniffi-bindings = ["uniffi"] # Enable UniFFI scaffolding for Swift/Kotlin bindings +ffi = ["uniffi", "native-crypto", "bridge", "dep:tokio"] # Enable UniFFI scaffolding for Swift/Kotlin bindings +uniffi-bindings = ["ffi"] # Deprecated alias for ffi feature +wasm-bindings = ["wasm-crypto", "bridge-wasm", "dep:wasm-bindgen", "dep:wasm-bindgen-futures", "dep:js-sys", "dep:web-sys", "dep:serde-wasm-bindgen"] # Enable WASM bindings # Protocol features bridge = ["uuid/v4", "reqwest", "dep:tokio", "dep:urlencoding"] # Bridge client and session management for verifications bridge-wasm = ["uuid/js", "reqwest", "dep:urlencoding"] # Bridge for WASM (no tokio) -verification = ["reqwest", "dep:tokio"] # Backend proof verification via Developer Portal API \ No newline at end of file +verification = ["reqwest", "dep:tokio"] # Backend proof verification via Developer Portal API diff --git a/rust/core/src/bridge.rs b/rust/core/src/bridge.rs index 1088391..2d4d45c 100644 --- a/rust/core/src/bridge.rs +++ b/rust/core/src/bridge.rs @@ -12,6 +12,9 @@ use uuid::Uuid; #[cfg(feature = "native-crypto")] use crate::crypto::CryptoKey; +#[cfg(feature = "ffi")] +use std::sync::Arc; + /// Bridge request payload sent to initialize a session #[derive(Debug, Serialize)] struct BridgeRequestPayload { @@ -329,6 +332,197 @@ impl Session { } } +// UniFFI wrapper for Session with tokio runtime +#[cfg(feature = "ffi")] +#[derive(uniffi::Object)] +pub struct SessionWrapper { + runtime: tokio::runtime::Runtime, + inner: Session, +} + +#[cfg(feature = "ffi")] +#[derive(Debug, Clone, uniffi::Enum)] +pub enum StatusWrapper { + /// Waiting for World App to retrieve the request + WaitingForConnection, + /// World App has retrieved the request, waiting for user confirmation + AwaitingConfirmation, + /// User has confirmed and provided a proof + Confirmed { proof: Proof }, + /// Request has failed + Failed { error: String }, +} + +#[cfg(feature = "ffi")] +impl From for StatusWrapper { + fn from(status: Status) -> Self { + match status { + Status::WaitingForConnection => Self::WaitingForConnection, + Status::AwaitingConfirmation => Self::AwaitingConfirmation, + Status::Confirmed(proof) => Self::Confirmed { proof }, + Status::Failed(app_error) => Self::Failed { + error: app_error.to_string(), + }, + } + } +} + +#[cfg(feature = "ffi")] +#[uniffi::export] +#[allow(clippy::needless_pass_by_value)] +impl SessionWrapper { + /// Creates a new session + /// + /// # Errors + /// + /// Returns an error if the session cannot be created or the request fails + #[uniffi::constructor] + pub fn create( + app_id: String, + action: String, + requests: Vec>, + ) -> std::result::Result { + let runtime = + tokio::runtime::Runtime::new().map_err(|e| crate::error::IdkitError::BridgeError { + details: format!("Failed to create runtime: {e}"), + })?; + + let app_id_parsed = AppId::new(&app_id)?; + let core_requests: Vec = requests.iter().map(|r| (**r).clone()).collect(); + + let inner = runtime + .block_on(Session::create(app_id_parsed, action, core_requests)) + .map_err(crate::error::IdkitError::from)?; + + Ok(Self { runtime, inner }) + } + + /// Creates a new session with optional configuration + /// + /// # Errors + /// + /// Returns an error if the session cannot be created or the request fails + #[uniffi::constructor] + #[allow(clippy::too_many_arguments)] + pub fn create_with_options( + app_id: String, + action: String, + requests: Vec>, + action_description: Option, + constraints: Option>, + bridge_url: Option, + ) -> std::result::Result { + let runtime = + tokio::runtime::Runtime::new().map_err(|e| crate::error::IdkitError::BridgeError { + details: format!("Failed to create runtime: {e}"), + })?; + + let app_id_parsed = AppId::new(&app_id)?; + let core_requests: Vec = requests.iter().map(|r| (**r).clone()).collect(); + let core_constraints = constraints.map(|c| (*c).clone()); + let bridge_url_parsed = bridge_url.map(|url| BridgeUrl::new(&url)).transpose()?; + + let inner = runtime + .block_on(Session::create_with_options( + app_id_parsed, + action, + core_requests, + action_description, + core_constraints, + bridge_url_parsed, + )) + .map_err(crate::error::IdkitError::from)?; + + Ok(Self { runtime, inner }) + } + + /// Creates a session from a verification level + /// + /// # Errors + /// + /// Returns an error if the session cannot be created or the request fails + #[uniffi::constructor] + pub fn from_verification_level( + app_id: String, + action: String, + verification_level: VerificationLevel, + signal: String, + ) -> std::result::Result { + let runtime = + tokio::runtime::Runtime::new().map_err(|e| crate::error::IdkitError::BridgeError { + details: format!("Failed to create runtime: {e}"), + })?; + + let app_id_parsed = AppId::new(&app_id)?; + + let inner = runtime + .block_on(Session::from_verification_level( + app_id_parsed, + action, + verification_level, + signal, + )) + .map_err(crate::error::IdkitError::from)?; + + Ok(Self { runtime, inner }) + } + + /// Returns the connect URL for World App + #[must_use] + pub fn connect_url(&self) -> String { + self.inner.connect_url() + } + + /// Returns the request ID for this session + #[must_use] + pub fn request_id(&self) -> String { + self.inner.request_id().to_string() + } + + /// Polls the session for updates until completion + pub fn poll_status( + &self, + poll_interval_ms: Option, + timeout_ms: Option, + ) -> StatusWrapper { + let poll_interval = std::time::Duration::from_millis(poll_interval_ms.unwrap_or(2000)); + let timeout = timeout_ms.map(std::time::Duration::from_millis); + + let mut elapsed = std::time::Duration::from_millis(0); + + loop { + match self.runtime.block_on(self.inner.poll_for_status()) { + Ok(status) => match status { + Status::WaitingForConnection | Status::AwaitingConfirmation => { + if let Some(timeout) = timeout { + if elapsed >= timeout { + return StatusWrapper::Failed { + error: "Timed out waiting for confirmation".to_string(), + }; + } + } + std::thread::sleep(poll_interval); + elapsed += poll_interval; + } + Status::Confirmed(proof) => { + return StatusWrapper::Confirmed { proof }; + } + Status::Failed(app_error) => { + return StatusWrapper::Failed { + error: app_error.to_string(), + }; + } + }, + Err(err) => { + return StatusWrapper::Failed { + error: err.to_string(), + }; + } + } + } + } +} + #[cfg(test)] mod tests { use super::*; diff --git a/rust/core/src/constraints.rs b/rust/core/src/constraints.rs index 19b85e0..689f94d 100644 --- a/rust/core/src/constraints.rs +++ b/rust/core/src/constraints.rs @@ -8,8 +8,12 @@ use crate::types::CredentialType; use serde::{Deserialize, Serialize}; use std::collections::HashSet; +#[cfg(feature = "ffi")] +use std::sync::Arc; + /// A node in the constraint tree #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[cfg_attr(feature = "ffi", derive(uniffi::Object))] #[serde(untagged)] pub enum ConstraintNode { /// A leaf node representing a single credential type @@ -19,13 +23,13 @@ pub enum ConstraintNode { /// Order matters: earlier credentials have higher priority Any { /// Child constraints (priority ordered) - any: Vec, + any: Vec, }, /// An AND node - all children must be satisfied All { /// Child constraints (all required) - all: Vec, + all: Vec, }, } @@ -162,6 +166,7 @@ impl ConstraintNode { /// Top-level constraints for a request #[derive(Debug, Clone, Serialize, Deserialize)] +#[cfg_attr(feature = "ffi", derive(uniffi::Object))] pub struct Constraints { /// The root constraint node #[serde(flatten)] @@ -223,6 +228,106 @@ impl Constraints { } } +// UniFFI exports for ConstraintNode +#[cfg(feature = "ffi")] +#[uniffi::export] +#[allow(clippy::needless_pass_by_value)] +impl ConstraintNode { + /// Creates a credential constraint node + #[must_use] + #[uniffi::constructor] + pub fn new_credential(credential_type: CredentialType) -> Arc { + Arc::new(Self::credential(credential_type)) + } + + /// Creates an "any" (OR) constraint node + #[must_use] + #[uniffi::constructor] + pub fn new_any(nodes: Vec>) -> Arc { + let core_nodes = nodes.iter().map(|n| (**n).clone()).collect(); + Arc::new(Self::any(core_nodes)) + } + + /// Creates an "all" (AND) constraint node + #[must_use] + #[uniffi::constructor] + pub fn new_all(nodes: Vec>) -> Arc { + let core_nodes = nodes.iter().map(|n| (**n).clone()).collect(); + Arc::new(Self::all(core_nodes)) + } + + /// Serializes a constraint node to JSON + /// + /// # Errors + /// + /// Returns an error if JSON serialization fails + pub fn to_json(&self) -> std::result::Result { + serde_json::to_string(&self) + .map_err(|e| crate::error::IdkitError::from(crate::Error::from(e))) + } + + /// Deserializes a constraint node from JSON + /// + /// # Errors + /// + /// Returns an error if JSON deserialization fails + #[uniffi::constructor] + pub fn new_from_json(json: &str) -> std::result::Result, crate::error::IdkitError> { + serde_json::from_str(json) + .map(Arc::new) + .map_err(|e| crate::error::IdkitError::from(crate::Error::from(e))) + } +} + +// UniFFI exports for Constraints +#[cfg(feature = "ffi")] +#[uniffi::export] +#[allow(clippy::needless_pass_by_value)] +impl Constraints { + /// Creates constraints from a root node + #[must_use] + #[uniffi::constructor] + pub fn new_from_root(root: Arc) -> Arc { + Arc::new(Self::new((*root).clone())) + } + + /// Creates an "any" constraint (at least one credential must match) + #[must_use] + #[uniffi::constructor] + pub fn new_any(credentials: Vec) -> Arc { + Arc::new(Self::any(credentials)) + } + + /// Creates an "all" constraint (all credentials must match) + #[must_use] + #[uniffi::constructor] + pub fn new_all(credentials: Vec) -> Arc { + Arc::new(Self::all(credentials)) + } + + /// Serializes constraints to JSON + /// + /// # Errors + /// + /// Returns an error if JSON serialization fails + pub fn to_json(&self) -> std::result::Result { + serde_json::to_string(&self) + .map_err(|e| crate::error::IdkitError::from(crate::Error::from(e))) + } + + /// Deserializes constraints from JSON + /// + /// # Errors + /// + /// Returns an error if JSON deserialization fails + #[uniffi::constructor] + pub fn new_from_json(json: &str) -> std::result::Result, crate::error::IdkitError> { + serde_json::from_str(json) + .map(Arc::new) + .map_err(|e| crate::error::IdkitError::from(crate::Error::from(e))) + } +} + #[cfg(test)] mod tests { use super::*; diff --git a/rust/core/src/error.rs b/rust/core/src/error.rs index 48e3316..c3c10d6 100644 --- a/rust/core/src/error.rs +++ b/rust/core/src/error.rs @@ -60,7 +60,7 @@ pub enum Error { /// Errors returned by the World App #[derive(Debug, Clone, Copy, PartialEq, Eq, Error, serde::Serialize, serde::Deserialize)] -#[cfg_attr(feature = "uniffi-bindings", derive(uniffi::Enum))] +#[cfg_attr(feature = "ffi", derive(uniffi::Enum))] #[serde(rename_all = "snake_case")] pub enum AppError { /// User rejected the request @@ -99,3 +99,102 @@ pub enum AppError { #[error("An error occurred")] GenericError, } + +// UniFFI error type wrapper +#[cfg(feature = "ffi")] +#[derive(Debug, thiserror::Error, uniffi::Error)] +pub enum IdkitError { + /// Invalid configuration provided + #[error("Invalid configuration: {details}")] + InvalidConfiguration { details: String }, + + /// JSON serialization/deserialization error + #[error("JSON error: {details}")] + JsonError { details: String }, + + /// Cryptographic operation error + #[error("Cryptography error: {details}")] + CryptoError { details: String }, + + /// Base64 encoding/decoding error + #[error("Base64 error: {details}")] + Base64Error { details: String }, + + /// URL parsing error + #[error("URL error: {details}")] + UrlError { details: String }, + + /// Invalid proof provided + #[error("Invalid proof: {details}")] + InvalidProof { details: String }, + + /// Bridge communication error + #[error("Bridge error: {details}")] + BridgeError { details: String }, + + /// Application-level error + #[error("App error: {details}")] + AppError { details: String }, + + /// Unexpected response from bridge + #[error("Unexpected response from bridge")] + UnexpectedResponse, + + /// Connection to bridge failed + #[error("Connection to bridge failed")] + ConnectionFailed, + + /// Request timed out + #[error("Request timed out")] + Timeout, +} + +#[cfg(feature = "ffi")] +impl From for IdkitError { + fn from(e: Error) -> Self { + match e { + Error::InvalidConfiguration(message) => Self::InvalidConfiguration { details: message }, + Error::Json(err) => Self::JsonError { + details: err.to_string(), + }, + Error::Crypto(message) => Self::CryptoError { details: message }, + Error::Base64(err) => Self::Base64Error { + details: err.to_string(), + }, + Error::Url(err) => Self::UrlError { + details: err.to_string(), + }, + Error::InvalidProof(message) => Self::InvalidProof { details: message }, + Error::BridgeError(message) => Self::BridgeError { details: message }, + Error::AppError(app_err) => Self::AppError { + details: app_err.to_string(), + }, + Error::UnexpectedResponse => Self::UnexpectedResponse, + Error::ConnectionFailed => Self::ConnectionFailed, + Error::Timeout => Self::Timeout, + #[cfg(any(feature = "bridge", feature = "bridge-wasm", feature = "verification"))] + Error::Http(err) => Self::BridgeError { + details: format!("HTTP error: {err}"), + }, + } + } +} + +#[cfg(feature = "ffi")] +impl From for Error { + fn from(e: IdkitError) -> Self { + match e { + IdkitError::InvalidConfiguration { details } => Self::InvalidConfiguration(details), + IdkitError::CryptoError { details } => Self::Crypto(details), + IdkitError::InvalidProof { details } => Self::InvalidProof(details), + IdkitError::JsonError { details } + | IdkitError::Base64Error { details } + | IdkitError::UrlError { details } + | IdkitError::BridgeError { details } + | IdkitError::AppError { details } => Self::BridgeError(details), + IdkitError::UnexpectedResponse => Self::UnexpectedResponse, + IdkitError::ConnectionFailed => Self::ConnectionFailed, + IdkitError::Timeout => Self::Timeout, + } + } +} diff --git a/rust/core/src/lib.rs b/rust/core/src/lib.rs index 832a821..ea53ab1 100644 --- a/rust/core/src/lib.rs +++ b/rust/core/src/lib.rs @@ -8,6 +8,7 @@ #![deny(clippy::all, clippy::pedantic, clippy::nursery)] #![allow(clippy::module_name_repetitions)] #![allow(clippy::missing_const_for_fn)] +#![cfg_attr(target_arch = "wasm32", allow(clippy::future_not_send))] #[cfg(any(feature = "bridge", feature = "bridge-wasm"))] pub mod bridge; @@ -18,6 +19,9 @@ pub mod types; #[cfg(feature = "verification")] pub mod verification; +#[cfg(feature = "wasm-bindings")] +pub mod wasm_bindings; + #[cfg(any(feature = "bridge", feature = "bridge-wasm"))] pub use bridge::{Session, Status}; pub use constraints::{ConstraintNode, Constraints}; @@ -29,5 +33,5 @@ pub use types::{AppId, BridgeUrl, CredentialType, Proof, Request, Signal, Verifi pub use verification::{verify_proof, verify_proof_with_endpoint}; // UniFFI scaffolding for core types -#[cfg(feature = "uniffi-bindings")] +#[cfg(feature = "ffi")] uniffi::setup_scaffolding!("idkit_core"); diff --git a/rust/core/src/types.rs b/rust/core/src/types.rs index c48587d..4a267b3 100644 --- a/rust/core/src/types.rs +++ b/rust/core/src/types.rs @@ -2,9 +2,12 @@ use serde::{Deserialize, Serialize}; +#[cfg(feature = "ffi")] +use std::sync::Arc; + /// Credential types that can be requested #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)] -#[cfg_attr(feature = "uniffi-bindings", derive(uniffi::Enum))] +#[cfg_attr(feature = "ffi", derive(uniffi::Enum))] #[serde(rename_all = "snake_case")] pub enum CredentialType { /// Orb credential @@ -51,6 +54,7 @@ impl CredentialType { /// - UTF-8 strings (common case for off-chain usage) /// - ABI-encoded bytes (for on-chain use cases) #[derive(Debug, Clone, PartialEq, Eq)] +#[cfg_attr(feature = "ffi", derive(uniffi::Object))] pub enum Signal { /// UTF-8 string signal String(String), @@ -101,6 +105,38 @@ impl Signal { } } +// UniFFI exports for Signal +#[cfg(feature = "ffi")] +#[uniffi::export] +#[allow(clippy::needless_pass_by_value)] +impl Signal { + /// Creates a signal from a string + #[must_use] + #[uniffi::constructor] + pub fn new_from_string(s: String) -> Arc { + Arc::new(Self::from_string(s)) + } + + /// Creates a signal from ABI-encoded bytes + #[must_use] + #[uniffi::constructor] + pub fn new_from_abi_encoded(bytes: Vec) -> Arc { + Arc::new(Self::from_abi_encoded(bytes)) + } + + /// Gets the signal as raw bytes + #[must_use] + pub fn bytes(&self) -> Vec { + self.to_bytes() + } + + /// Gets the signal as a string if it's a UTF-8 string signal + #[must_use] + pub fn string(&self) -> Option { + self.as_str().map(String::from) + } +} + impl Serialize for Signal { fn serialize(&self, serializer: S) -> Result where @@ -134,6 +170,7 @@ impl<'de> Deserialize<'de> for Signal { /// A single credential request #[derive(Debug, Clone, Serialize, Deserialize)] +#[cfg_attr(feature = "ffi", derive(uniffi::Object))] pub struct Request { /// The type of credential being requested #[serde(rename = "type")] @@ -193,9 +230,57 @@ impl Request { } } +// UniFFI exports for Request +#[cfg(feature = "ffi")] +#[uniffi::export] +#[allow(clippy::needless_pass_by_value)] +impl Request { + /// Creates a new credential request + #[must_use] + #[uniffi::constructor] + pub fn create(credential_type: CredentialType, signal: Option>) -> Arc { + let signal_opt = signal.map(|s| (*s).clone()); + Arc::new(Self::new(credential_type, signal_opt)) + } + + /// Sets the face authentication requirement on a request + #[must_use] + pub fn set_face_auth(&self, face_auth: bool) -> Arc { + Arc::new(self.clone().with_face_auth(face_auth)) + } + + /// Gets the signal as raw bytes from a request + #[must_use] + pub fn get_signal_bytes(&self) -> Option> { + self.signal_bytes() + } + + /// Serializes a request to JSON + /// + /// # Errors + /// + /// Returns an error if JSON serialization fails + pub fn to_json(&self) -> std::result::Result { + serde_json::to_string(&self) + .map_err(|e| crate::error::IdkitError::from(crate::Error::from(e))) + } + + /// Deserializes a request from JSON + /// + /// # Errors + /// + /// Returns an error if JSON deserialization fails + #[uniffi::constructor] + pub fn new_from_json(json: &str) -> std::result::Result, crate::error::IdkitError> { + serde_json::from_str(json) + .map(Arc::new) + .map_err(|e| crate::error::IdkitError::from(crate::Error::from(e))) + } +} + /// The proof of verification returned by the World ID protocol #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -#[cfg_attr(feature = "uniffi-bindings", derive(uniffi::Record))] +#[cfg_attr(feature = "ffi", derive(uniffi::Record))] pub struct Proof { /// The Zero-knowledge proof of the verification (hex string, ABI encoded) pub proof: String, @@ -210,6 +295,29 @@ pub struct Proof { pub verification_level: CredentialType, } +// UniFFI helper functions for Proof +#[cfg(feature = "ffi")] +/// Serializes a proof to JSON +/// +/// # Errors +/// +/// Returns an error if JSON serialization fails +#[uniffi::export] +pub fn proof_to_json(proof: &Proof) -> std::result::Result { + serde_json::to_string(proof).map_err(|e| crate::error::IdkitError::from(crate::Error::from(e))) +} + +#[cfg(feature = "ffi")] +/// Deserializes a proof from JSON +/// +/// # Errors +/// +/// Returns an error if JSON deserialization fails +#[uniffi::export] +pub fn proof_from_json(json: &str) -> std::result::Result { + serde_json::from_str(json).map_err(|e| crate::error::IdkitError::from(crate::Error::from(e))) +} + /// Application ID for World ID #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub struct AppId(String); @@ -316,7 +424,7 @@ impl<'de> Deserialize<'de> for BridgeUrl { /// Verification level (for backward compatibility) #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] -#[cfg_attr(feature = "uniffi-bindings", derive(uniffi::Enum))] +#[cfg_attr(feature = "ffi", derive(uniffi::Enum))] #[serde(rename_all = "snake_case")] pub enum VerificationLevel { /// Orb-only verification @@ -349,6 +457,15 @@ impl VerificationLevel { } } +// UniFFI helper function for CredentialType +#[cfg(feature = "ffi")] +/// Gets the string representation of a credential type +#[must_use] +#[uniffi::export] +pub fn credential_to_string(credential: &CredentialType) -> String { + credential.as_str().to_string() +} + #[cfg(test)] mod tests { use super::*; diff --git a/rust/wasm/src/lib.rs b/rust/core/src/wasm_bindings.rs similarity index 83% rename from rust/wasm/src/lib.rs rename to rust/core/src/wasm_bindings.rs index e083e07..a73e21e 100644 --- a/rust/wasm/src/lib.rs +++ b/rust/core/src/wasm_bindings.rs @@ -1,19 +1,20 @@ //! WASM bindings for `IDKit` //! -//! This crate provides WebAssembly bindings for the core `IDKit` library, +//! This module provides WebAssembly bindings for the core `IDKit` library, //! allowing it to be used in browser environments. -#![deny(clippy::all, clippy::pedantic, clippy::nursery)] -#![allow(clippy::module_name_repetitions)] +#![allow(clippy::needless_pass_by_value)] +#![allow(clippy::missing_panics_doc)] +#![allow(clippy::future_not_send)] -use idkit_core::{CredentialType, Signal, VerificationLevel}; +use crate::{CredentialType, Signal, VerificationLevel}; use std::cell::RefCell; use std::rc::Rc; use wasm_bindgen::prelude::*; use wasm_bindgen_futures::future_to_promise; #[wasm_bindgen] -pub struct IDKitRequest(idkit_core::Request); +pub struct IDKitRequest(crate::Request); #[wasm_bindgen] impl IDKitRequest { @@ -30,7 +31,7 @@ impl IDKitRequest { pub fn new(credential_type: JsValue, signal: Option) -> Result { let cred: CredentialType = serde_wasm_bindgen::from_value(credential_type)?; let signal_opt = signal.map(Signal::from_string); - Ok(Self(idkit_core::Request::new(cred, signal_opt))) + Ok(Self(crate::Request::new(cred, signal_opt))) } /// Creates a new request with ABI-encoded bytes for the signal @@ -44,7 +45,7 @@ impl IDKitRequest { #[wasm_bindgen(js_name = withBytes)] pub fn with_bytes(credential_type: JsValue, signal_bytes: &[u8]) -> Result { let cred: CredentialType = serde_wasm_bindgen::from_value(credential_type)?; - Ok(Self(idkit_core::Request::new( + Ok(Self(crate::Request::new( cred, Some(Signal::from_abi_encoded(signal_bytes)), ))) @@ -69,7 +70,7 @@ impl IDKitRequest { } #[wasm_bindgen] -pub struct IDKitProof(idkit_core::Proof); +pub struct IDKitProof(crate::Proof); #[wasm_bindgen] impl IDKitProof { @@ -86,7 +87,7 @@ impl IDKitProof { verification_level: JsValue, ) -> Result { let cred: CredentialType = serde_wasm_bindgen::from_value(verification_level)?; - Ok(Self(idkit_core::Proof { + Ok(Self(crate::Proof { proof, merkle_root, nullifier_hash, @@ -121,7 +122,7 @@ impl BridgeEncryption { /// Returns an error if key generation fails #[wasm_bindgen(constructor)] pub fn new() -> Result { - let (key, nonce) = idkit_core::crypto::generate_key() + let (key, nonce) = crate::crypto::generate_key() .map_err(|e| JsValue::from_str(&format!("Failed to generate key: {e}")))?; Ok(Self { key: key.to_vec(), @@ -135,9 +136,9 @@ impl BridgeEncryption { /// /// Returns an error if encryption fails pub fn encrypt(&self, plaintext: &str) -> Result { - let ciphertext = idkit_core::crypto::encrypt(&self.key, &self.nonce, plaintext.as_bytes()) + let ciphertext = crate::crypto::encrypt(&self.key, &self.nonce, plaintext.as_bytes()) .map_err(|e| JsValue::from_str(&format!("Encryption failed: {e}")))?; - Ok(idkit_core::crypto::base64_encode(&ciphertext)) + Ok(crate::crypto::base64_encode(&ciphertext)) } /// Decrypts a base64-encoded ciphertext using AES-256-GCM @@ -146,10 +147,10 @@ impl BridgeEncryption { /// /// Returns an error if decryption fails or the output is not valid UTF-8 pub fn decrypt(&self, ciphertext_base64: &str) -> Result { - let ciphertext = idkit_core::crypto::base64_decode(ciphertext_base64) + let ciphertext = crate::crypto::base64_decode(ciphertext_base64) .map_err(|e| JsValue::from_str(&format!("Base64 decode failed: {e}")))?; - let plaintext_bytes = idkit_core::crypto::decrypt(&self.key, &self.nonce, &ciphertext) + let plaintext_bytes = crate::crypto::decrypt(&self.key, &self.nonce, &ciphertext) .map_err(|e| JsValue::from_str(&format!("Decryption failed: {e}")))?; String::from_utf8(plaintext_bytes) @@ -160,14 +161,14 @@ impl BridgeEncryption { #[must_use] #[wasm_bindgen(js_name = keyBase64)] pub fn key_base64(&self) -> String { - idkit_core::crypto::base64_encode(&self.key) + crate::crypto::base64_encode(&self.key) } /// Returns the nonce as a base64-encoded string #[must_use] #[wasm_bindgen(js_name = nonceBase64)] pub fn nonce_base64(&self) -> String { - idkit_core::crypto::base64_encode(&self.nonce) + crate::crypto::base64_encode(&self.nonce) } } @@ -175,7 +176,7 @@ impl BridgeEncryption { #[must_use] #[wasm_bindgen(js_name = hashSignal)] pub fn hash_signal(signal: &str) -> String { - use idkit_core::crypto::hash_to_field; + use crate::crypto::hash_to_field; let hash = hash_to_field(signal.as_bytes()); format!("{hash:#066x}") } @@ -184,7 +185,7 @@ pub fn hash_signal(signal: &str) -> String { #[must_use] #[wasm_bindgen(js_name = hashSignalBytes)] pub fn hash_signal_bytes(bytes: &[u8]) -> String { - use idkit_core::crypto::hash_to_field; + use crate::crypto::hash_to_field; let hash = hash_to_field(bytes); format!("{hash:#066x}") } @@ -201,10 +202,10 @@ struct JsRequestDto { face_auth: Option, } -fn js_request_to_core(req: JsRequestDto) -> Result { +fn js_request_to_core(req: JsRequestDto) -> Result { let signal = match (req.signal, req.signal_bytes) { - (Some(s), None) => Some(idkit_core::Signal::from_string(s)), - (None, Some(bytes)) => Some(idkit_core::Signal::from_abi_encoded(bytes)), + (Some(s), None) => Some(Signal::from_string(s)), + (None, Some(bytes)) => Some(Signal::from_abi_encoded(bytes)), (None, None) => None, (Some(_), Some(_)) => { return Err(JsValue::from_str( @@ -213,7 +214,7 @@ fn js_request_to_core(req: JsRequestDto) -> Result } }; - let mut core_req = idkit_core::Request::new(req.credential_type, signal); + let mut core_req = crate::Request::new(req.credential_type, signal); if let Some(face) = req.face_auth { core_req = core_req.with_face_auth(face); } @@ -224,7 +225,7 @@ fn js_request_to_core(req: JsRequestDto) -> Result #[must_use] #[wasm_bindgen(js_name = base64Encode)] pub fn base64_encode(data: &[u8]) -> String { - idkit_core::crypto::base64_encode(data) + crate::crypto::base64_encode(data) } /// Decodes base64 data @@ -234,7 +235,7 @@ pub fn base64_encode(data: &[u8]) -> String { /// Returns an error if decoding fails #[wasm_bindgen(js_name = base64Decode)] pub fn base64_decode(data: &str) -> Result, JsValue> { - idkit_core::crypto::base64_decode(data) + crate::crypto::base64_decode(data) .map_err(|e| JsValue::from_str(&format!("Base64 decode failed: {e}"))) } @@ -244,7 +245,7 @@ pub fn base64_decode(data: &str) -> Result, JsValue> { #[wasm_bindgen] pub struct Session { #[wasm_bindgen(skip)] - inner: Rc>>, + inner: Rc>>, } #[wasm_bindgen] @@ -274,7 +275,7 @@ impl Session { ) -> js_sys::Promise { future_to_promise(async move { // Parse app_id - let app_id = idkit_core::AppId::new(app_id) + let app_id = crate::AppId::new(app_id) .map_err(|e| JsValue::from_str(&format!("Invalid app_id: {e}")))?; // Parse verification level @@ -282,7 +283,7 @@ impl Session { .map_err(|e| JsValue::from_str(&format!("Invalid verification_level: {e}")))?; // Create session - let session = idkit_core::Session::from_verification_level( + let session = crate::Session::from_verification_level( app_id, action, vl, @@ -316,7 +317,7 @@ impl Session { bridge_url: Option, ) -> js_sys::Promise { future_to_promise(async move { - let app_id = idkit_core::AppId::new(app_id) + let app_id = crate::AppId::new(app_id) .map_err(|e| JsValue::from_str(&format!("Invalid app_id: {e}")))?; let req_vec: Vec = serde_wasm_bindgen::from_value(requests) @@ -324,14 +325,14 @@ impl Session { if req_vec.is_empty() { return Err(JsValue::from_str("At least one request is required")); } - let core_requests: Vec = req_vec + let core_requests: Vec = req_vec .into_iter() .map(js_request_to_core) .collect::>()?; let core_constraints = if let Some(c) = constraints { Some( - serde_wasm_bindgen::from_value::(c).map_err(|e| { + serde_wasm_bindgen::from_value::(c).map_err(|e| { JsValue::from_str(&format!("Invalid constraints payload: {e}")) })?, ) @@ -339,19 +340,17 @@ impl Session { // Default: any-of the provided credentials in order let nodes = core_requests .iter() - .map(|r| idkit_core::ConstraintNode::credential(r.credential_type)) + .map(|r| crate::ConstraintNode::credential(r.credential_type)) .collect(); - Some(idkit_core::Constraints::new( - idkit_core::ConstraintNode::any(nodes), - )) + Some(crate::Constraints::new(crate::ConstraintNode::any(nodes))) }; let bridge_url_parsed = bridge_url - .map(idkit_core::BridgeUrl::new) + .map(crate::BridgeUrl::new) .transpose() .map_err(|e| JsValue::from_str(&format!("Invalid bridge_url: {e}")))?; - let session = idkit_core::Session::create_with_options( + let session = crate::Session::create_with_options( app_id, action, core_requests, @@ -381,7 +380,7 @@ impl Session { .borrow() .as_ref() .ok_or_else(|| JsValue::from_str("Session closed")) - .map(idkit_core::Session::connect_url) + .map(crate::Session::connect_url) } /// Returns the request ID for this session @@ -430,28 +429,26 @@ impl Session { // Convert Rust Status enum to JS object let js_status = match status { - idkit_core::Status::WaitingForConnection => { + crate::Status::WaitingForConnection => { serde_wasm_bindgen::to_value(&serde_json::json!({ "type": "waiting_for_connection" })) } - idkit_core::Status::AwaitingConfirmation => { + crate::Status::AwaitingConfirmation => { serde_wasm_bindgen::to_value(&serde_json::json!({ "type": "awaiting_confirmation" })) } - idkit_core::Status::Confirmed(proof) => { + crate::Status::Confirmed(proof) => { serde_wasm_bindgen::to_value(&serde_json::json!({ "type": "confirmed", "proof": proof })) } - idkit_core::Status::Failed(error) => { - serde_wasm_bindgen::to_value(&serde_json::json!({ - "type": "failed", - "error": error - })) - } + crate::Status::Failed(error) => serde_wasm_bindgen::to_value(&serde_json::json!({ + "type": "failed", + "error": error + })), } .map_err(|e| JsValue::from_str(&format!("Serialization failed: {e}")))?; diff --git a/rust/uniffi-bindings/Cargo.toml b/rust/uniffi-bindings/Cargo.toml deleted file mode 100644 index 5fff3af..0000000 --- a/rust/uniffi-bindings/Cargo.toml +++ /dev/null @@ -1,20 +0,0 @@ -[package] -name = "idkit-uniffi" -version.workspace = true -edition.workspace = true -license.workspace = true -repository.workspace = true -authors.workspace = true -description = "UniFFI bindings for IDKit - generates native Swift and Kotlin bindings" - -[lib] -crate-type = ["staticlib", "cdylib"] -name = "idkit" - -[dependencies] -idkit-core = { path = "../core", default-features = false, features = ["uniffi-bindings", "native-crypto", "bridge"] } -uniffi = { workspace = true } -thiserror = { workspace = true } -serde_json = { workspace = true } -hex = { workspace = true } -tokio = { workspace = true, features = ["rt", "rt-multi-thread"] } diff --git a/rust/uniffi-bindings/src/lib.rs b/rust/uniffi-bindings/src/lib.rs deleted file mode 100644 index 64416a6..0000000 --- a/rust/uniffi-bindings/src/lib.rs +++ /dev/null @@ -1,711 +0,0 @@ -//! `UniFFI` bindings for `IDKit` -//! -//! This crate generates Swift and Kotlin bindings for the core `IDKit` library. -//! Provides a mobile-friendly API for creating and validating World ID requests and proofs. - -#![deny(clippy::all, clippy::pedantic, clippy::nursery)] -#![allow(clippy::module_name_repetitions)] -// UniFFI requires specific function signatures for FFI, so we allow these -#![allow(clippy::needless_pass_by_value)] - -use idkit_core::{ - bridge::{Session as CoreSession, Status as CoreStatus}, - ConstraintNode as CoreConstraintNode, Constraints as CoreConstraints, CredentialType, Proof, - Request as CoreRequest, Signal as CoreSignal, VerificationLevel, -}; -use std::sync::Arc; - -/// Signal wrapper for `UniFFI` -/// -/// Represents a signal that can be either a string or ABI-encoded bytes. -#[derive(Debug, Clone, uniffi::Object)] -pub struct Signal(CoreSignal); - -/// Opaque request handle for `UniFFI` -/// -/// This wraps the core `Request` type to work around `UniFFI` limitations with custom types. -#[derive(Debug, Clone, uniffi::Object)] -pub struct Request(CoreRequest); - -/// Constraint node for `UniFFI` -/// -/// Represents a node in a constraint tree (Credential, Any, or All). -#[derive(Debug, Clone, uniffi::Object)] -pub struct ConstraintNode(CoreConstraintNode); - -/// Constraints wrapper for `UniFFI` -/// -/// Represents the top-level constraints for a session. -#[derive(Debug, Clone, uniffi::Object)] -pub struct Constraints(CoreConstraints); - -/// Session wrapper for `UniFFI` -/// -/// Manages a World ID verification session. -#[derive(uniffi::Object)] -pub struct Session { - runtime: tokio::runtime::Runtime, - inner: CoreSession, -} - -/// Status enum for `UniFFI` -/// -/// Represents the status of a verification request. -#[derive(Debug, Clone, uniffi::Enum)] -pub enum Status { - /// Waiting for World App to retrieve the request - WaitingForConnection, - /// World App has retrieved the request, waiting for user confirmation - AwaitingConfirmation, - /// User has confirmed and provided a proof - Confirmed { proof: Proof }, - /// Request has failed - Failed { error: String }, -} - -/// Error type for `UniFFI` bindings -#[derive(Debug, thiserror::Error, uniffi::Error)] -pub enum IdkitError { - /// Invalid configuration provided - #[error("Invalid configuration: {details}")] - InvalidConfiguration { details: String }, - - /// JSON serialization/deserialization error - #[error("JSON error: {details}")] - JsonError { details: String }, - - /// Cryptographic operation error - #[error("Cryptography error: {details}")] - CryptoError { details: String }, - - /// Base64 encoding/decoding error - #[error("Base64 error: {details}")] - Base64Error { details: String }, - - /// URL parsing error - #[error("URL error: {details}")] - UrlError { details: String }, - - /// Invalid proof provided - #[error("Invalid proof: {details}")] - InvalidProof { details: String }, - - /// Bridge communication error - #[error("Bridge error: {details}")] - BridgeError { details: String }, - - /// Application-level error - #[error("App error: {details}")] - AppError { details: String }, - - /// Unexpected response from bridge - #[error("Unexpected response from bridge")] - UnexpectedResponse, - - /// Connection to bridge failed - #[error("Connection to bridge failed")] - ConnectionFailed, - - /// Request timed out - #[error("Request timed out")] - Timeout, -} - -impl From for IdkitError { - fn from(e: idkit_core::Error) -> Self { - match e { - idkit_core::Error::InvalidConfiguration(message) => { - Self::InvalidConfiguration { details: message } - } - idkit_core::Error::Json(e) => Self::JsonError { - details: e.to_string(), - }, - idkit_core::Error::Crypto(message) => Self::CryptoError { details: message }, - idkit_core::Error::Base64(e) => Self::Base64Error { - details: e.to_string(), - }, - idkit_core::Error::Url(e) => Self::UrlError { - details: e.to_string(), - }, - idkit_core::Error::InvalidProof(message) => Self::InvalidProof { details: message }, - idkit_core::Error::BridgeError(message) => Self::BridgeError { details: message }, - idkit_core::Error::AppError(app_err) => Self::AppError { - details: app_err.to_string(), - }, - idkit_core::Error::UnexpectedResponse => Self::UnexpectedResponse, - idkit_core::Error::ConnectionFailed => Self::ConnectionFailed, - idkit_core::Error::Timeout => Self::Timeout, - idkit_core::Error::Http(_) => Self::BridgeError { - details: format!("HTTP error: {e}"), - }, - } - } -} - -impl From for Status { - fn from(status: CoreStatus) -> Self { - match status { - CoreStatus::WaitingForConnection => Self::WaitingForConnection, - CoreStatus::AwaitingConfirmation => Self::AwaitingConfirmation, - CoreStatus::Confirmed(proof) => Self::Confirmed { proof }, - CoreStatus::Failed(app_error) => Self::Failed { - error: app_error.to_string(), - }, - } - } -} - -// Signal constructors - -#[uniffi::export] -impl Signal { - /// Creates a signal from a string - #[must_use] - #[uniffi::constructor] - pub fn from_string(s: String) -> Self { - Self(CoreSignal::from_string(s)) - } - - /// Creates a signal from ABI-encoded bytes - /// - /// Use this for on-chain use cases where the signal needs to be ABI-encoded - /// according to Solidity encoding rules. - #[must_use] - #[uniffi::constructor] - pub fn from_abi_encoded(bytes: Vec) -> Self { - Self(CoreSignal::from_abi_encoded(bytes)) - } - - /// Gets the signal as raw bytes - #[must_use] - pub fn as_bytes(&self) -> Vec { - self.0.as_bytes().to_vec() - } - - /// Gets the signal as a string if it's a UTF-8 string signal - #[must_use] - pub fn as_string(&self) -> Option { - self.0.as_str().map(String::from) - } -} - -// Request constructors and methods - -#[uniffi::export] -impl Request { - /// Creates a new credential request - /// - /// # Arguments - /// * `credential_type` - The type of credential to request (Orb, Face, Device, etc.) - /// * `signal` - Optional signal for the proof. Use `Signal::from_string()` or `Signal::from_abi_encoded()` - #[must_use] - #[uniffi::constructor] - pub fn new(credential_type: CredentialType, signal: Option>) -> Self { - let signal_opt = signal.map(|s| s.0.clone()); - Self(CoreRequest::new(credential_type, signal_opt)) - } - - /// Sets the face authentication requirement on a request - /// - /// Returns a new request with the face auth set - #[must_use] - pub fn with_face_auth(&self, face_auth: bool) -> Self { - let mut new_request = self.0.clone(); - new_request.face_auth = Some(face_auth); - Self(new_request) - } - - /// Gets the signal as raw bytes from a request - #[must_use] - pub fn get_signal_bytes(&self) -> Option> { - self.0.signal_bytes() - } - - /// Gets the credential type - #[must_use] - pub const fn credential_type(&self) -> CredentialType { - self.0.credential_type - } - - /// Gets the `face_auth` setting - #[must_use] - pub const fn face_auth(&self) -> Option { - self.0.face_auth - } - - /// Serializes a request to JSON - /// - /// # Errors - /// - /// Returns an error if JSON serialization fails - pub fn to_json(&self) -> Result { - serde_json::to_string(&self.0).map_err(|e| IdkitError::JsonError { - details: e.to_string(), - }) - } - - /// Deserializes a request from JSON - /// - /// # Errors - /// - /// Returns an error if JSON deserialization fails - #[uniffi::constructor] - pub fn from_json(json: &str) -> Result { - serde_json::from_str(json) - .map(Self) - .map_err(|e| IdkitError::JsonError { - details: e.to_string(), - }) - } -} - -// Proof methods - -/// Serializes a proof to JSON -/// -/// # Errors -/// -/// Returns an error if JSON serialization fails -#[uniffi::export] -pub fn proof_to_json(proof: &Proof) -> Result { - serde_json::to_string(proof).map_err(|e| IdkitError::JsonError { - details: e.to_string(), - }) -} - -/// Deserializes a proof from JSON -/// -/// # Errors -/// -/// Returns an error if JSON deserialization fails -#[uniffi::export] -pub fn proof_from_json(json: &str) -> Result { - serde_json::from_str(json).map_err(|e| IdkitError::JsonError { - details: e.to_string(), - }) -} - -// ConstraintNode constructors and methods - -#[uniffi::export] -impl ConstraintNode { - /// Creates a credential constraint node - #[must_use] - #[uniffi::constructor] - pub fn credential(credential_type: CredentialType) -> Self { - Self(CoreConstraintNode::credential(credential_type)) - } - - /// Creates an "any" (OR) constraint node - /// - /// At least one of the child constraints must be satisfied. - /// Order matters: earlier constraints have higher priority. - #[must_use] - #[uniffi::constructor] - pub fn any(nodes: Vec>) -> Self { - let core_nodes = nodes.iter().map(|n| n.0.clone()).collect(); - Self(CoreConstraintNode::any(core_nodes)) - } - - /// Creates an "all" (AND) constraint node - /// - /// All child constraints must be satisfied. - #[must_use] - #[uniffi::constructor] - pub fn all(nodes: Vec>) -> Self { - let core_nodes = nodes.iter().map(|n| n.0.clone()).collect(); - Self(CoreConstraintNode::all(core_nodes)) - } - - /// Serializes a constraint node to JSON - /// - /// # Errors - /// - /// Returns an error if JSON serialization fails - pub fn to_json(&self) -> Result { - serde_json::to_string(&self.0).map_err(|e| IdkitError::JsonError { - details: e.to_string(), - }) - } - - /// Deserializes a constraint node from JSON - /// - /// # Errors - /// - /// Returns an error if JSON deserialization fails - #[uniffi::constructor] - pub fn from_json(json: &str) -> Result { - serde_json::from_str(json) - .map(Self) - .map_err(|e| IdkitError::JsonError { - details: e.to_string(), - }) - } -} - -// Constraints constructors and methods - -#[uniffi::export] -impl Constraints { - /// Creates constraints from a root node - #[must_use] - #[uniffi::constructor] - pub fn new(root: Arc) -> Self { - Self(CoreConstraints::new(root.0.clone())) - } - - /// Creates an "any" constraint (at least one credential must match) - #[must_use] - #[uniffi::constructor] - pub fn any(credentials: Vec) -> Self { - let nodes: Vec = credentials - .into_iter() - .map(CoreConstraintNode::credential) - .collect(); - Self(CoreConstraints::new(CoreConstraintNode::any(nodes))) - } - - /// Creates an "all" constraint (all credentials must match) - #[must_use] - #[uniffi::constructor] - pub fn all(credentials: Vec) -> Self { - let nodes: Vec = credentials - .into_iter() - .map(CoreConstraintNode::credential) - .collect(); - Self(CoreConstraints::new(CoreConstraintNode::all(nodes))) - } - - /// Serializes constraints to JSON - /// - /// # Errors - /// - /// Returns an error if JSON serialization fails - pub fn to_json(&self) -> Result { - serde_json::to_string(&self.0).map_err(|e| IdkitError::JsonError { - details: e.to_string(), - }) - } - - /// Deserializes constraints from JSON - /// - /// # Errors - /// - /// Returns an error if JSON deserialization fails - #[uniffi::constructor] - pub fn from_json(json: &str) -> Result { - serde_json::from_str(json) - .map(Self) - .map_err(|e| IdkitError::JsonError { - details: e.to_string(), - }) - } -} - -// Session constructors and methods - -#[uniffi::export] -impl Session { - /// Creates a new session - /// - /// # Arguments - /// - /// * `app_id` - Application ID from the Developer Portal (e.g., `"app_123"`) - /// * `action` - Action identifier - /// * `requests` - One or more credential requests - /// - /// # Errors - /// - /// Returns an error if the session cannot be created or the request fails - #[uniffi::constructor] - pub fn create( - app_id: String, - action: String, - requests: Vec>, - ) -> Result { - let runtime = tokio::runtime::Runtime::new().map_err(|e| IdkitError::BridgeError { - details: format!("Failed to create runtime: {e}"), - })?; - - let app_id_parsed = idkit_core::types::AppId::new(&app_id)?; - let core_requests: Vec = requests.iter().map(|r| r.0.clone()).collect(); - - let inner = runtime - .block_on(CoreSession::create(app_id_parsed, action, core_requests)) - .map_err(IdkitError::from)?; - - Ok(Self { runtime, inner }) - } - - /// Creates a new session with optional configuration - /// - /// # Arguments - /// - /// * `app_id` - Application ID from the Developer Portal - /// * `action` - Action identifier - /// * `requests` - One or more credential requests - /// * `action_description` - Optional action description shown to users - /// * `constraints` - Optional constraints on which credentials are acceptable - /// * `bridge_url` - Optional bridge URL (defaults to production) - /// - /// # Errors - /// - /// Returns an error if the session cannot be created or the request fails - #[uniffi::constructor] - pub fn create_with_options( - app_id: String, - action: String, - requests: Vec>, - action_description: Option, - constraints: Option>, - bridge_url: Option, - ) -> Result { - let runtime = tokio::runtime::Runtime::new().map_err(|e| IdkitError::BridgeError { - details: format!("Failed to create runtime: {e}"), - })?; - - let app_id_parsed = idkit_core::types::AppId::new(&app_id)?; - let core_requests: Vec = requests.iter().map(|r| r.0.clone()).collect(); - let core_constraints = constraints.map(|c| c.0.clone()); - let bridge_url_parsed = bridge_url - .map(|url| idkit_core::types::BridgeUrl::new(&url)) - .transpose()?; - - let inner = runtime - .block_on(CoreSession::create_with_options( - app_id_parsed, - action, - core_requests, - action_description, - core_constraints, - bridge_url_parsed, - )) - .map_err(IdkitError::from)?; - - Ok(Self { runtime, inner }) - } - - /// Creates a session from a verification level - /// - /// This is a convenience method that maps a verification level to the appropriate - /// set of credential requests and constraints. - /// - /// # Errors - /// - /// Returns an error if the session cannot be created or the request fails - #[uniffi::constructor] - pub fn from_verification_level( - app_id: String, - action: String, - verification_level: VerificationLevel, - signal: String, - ) -> Result { - let runtime = tokio::runtime::Runtime::new().map_err(|e| IdkitError::BridgeError { - details: format!("Failed to create runtime: {e}"), - })?; - - let app_id_parsed = idkit_core::types::AppId::new(&app_id)?; - - let inner = runtime - .block_on(CoreSession::from_verification_level( - app_id_parsed, - action, - verification_level, - signal, - )) - .map_err(IdkitError::from)?; - - Ok(Self { runtime, inner }) - } - - /// Returns the connect URL for World App - #[must_use] - pub fn connect_url(&self) -> String { - self.inner.connect_url() - } - - /// Returns the request ID for this session - #[must_use] - pub fn request_id(&self) -> String { - self.inner.request_id().to_string() - } - - /// Polls the bridge for the current status (non-blocking) - /// - /// Mirrors the `idkit-rs` `poll_for_status` helper so higher-level SDKs can - /// stream updates by repeatedly invoking this method. - /// - /// # Errors - /// - /// Returns an error if the request fails or the response is invalid - pub fn poll_for_status(&self) -> Result { - self.runtime - .block_on(self.inner.poll_for_status()) - .map(Status::from) - .map_err(IdkitError::from) - } - - /// Waits for a proof with default timeout (15 minutes) - /// - /// This is a blocking convenience method that polls the bridge until completion. - /// For async Rust code, use `poll_for_status()` in a loop instead. - /// - /// # Errors - /// - /// Returns an error if polling fails, verification fails, or timeout is reached - pub fn wait_for_proof(&self) -> Result { - self.wait_for_proof_with_timeout(900) // 15 minutes - } - - /// Waits for a proof with a specific timeout (in seconds) - /// - /// This is a blocking convenience method that polls the bridge until completion. - /// For async Rust code, use `poll_for_status()` in a loop instead. - /// - /// # Errors - /// - /// Returns an error if polling fails, verification fails, or timeout is reached - pub fn wait_for_proof_with_timeout(&self, timeout_seconds: u64) -> Result { - use std::time::{Duration, Instant}; - - let start = Instant::now(); - let timeout = Duration::from_secs(timeout_seconds); - let poll_interval = Duration::from_secs(3); - - loop { - if start.elapsed() > timeout { - return Err(IdkitError::Timeout); - } - - match self.poll_for_status()? { - Status::Confirmed { proof } => return Ok(proof), - Status::Failed { error } => return Err(IdkitError::AppError { details: error }), - Status::WaitingForConnection | Status::AwaitingConfirmation => { - std::thread::sleep(poll_interval); - } - } - } - } -} - -// Credential methods - -/// Gets the string representation of a credential type -#[must_use] -#[uniffi::export] -pub fn credential_to_string(credential: &CredentialType) -> String { - match credential { - CredentialType::Orb => "orb".to_string(), - CredentialType::Face => "face".to_string(), - CredentialType::SecureDocument => "secure_document".to_string(), - CredentialType::Document => "document".to_string(), - CredentialType::Device => "device".to_string(), - } -} - -// Generate UniFFI scaffolding -uniffi::setup_scaffolding!(); - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_create_request() { - let signal = Signal::from_string("test_signal".to_string()); - let request = Request::new(CredentialType::Orb, Some(std::sync::Arc::new(signal))); - assert_eq!(request.credential_type(), CredentialType::Orb); - assert!(request.get_signal_bytes().is_some()); - assert_eq!(request.get_signal_bytes().unwrap(), b"test_signal"); - assert_eq!(request.face_auth(), None); - - let with_face_auth = request.with_face_auth(true); - assert_eq!(with_face_auth.face_auth(), Some(true)); - } - - #[test] - fn test_create_request_without_signal() { - let request = Request::new(CredentialType::Device, None); - assert_eq!(request.credential_type(), CredentialType::Device); - assert_eq!(request.get_signal_bytes(), None); - assert_eq!(request.face_auth(), None); - } - - #[test] - fn test_create_request_with_abi_encoded() { - let bytes = vec![0xFF, 0xFE, 0xFD, 0x00, 0x01]; - let signal = Signal::from_abi_encoded(bytes.clone()); - let request = Request::new(CredentialType::Orb, Some(std::sync::Arc::new(signal))); - - assert_eq!(request.credential_type(), CredentialType::Orb); - assert!(request.get_signal_bytes().is_some()); - - // Verify we can get bytes back - let decoded = request.get_signal_bytes().unwrap(); - assert_eq!(decoded, bytes); - - let with_face_auth = request.with_face_auth(true); - assert_eq!(with_face_auth.face_auth(), Some(true)); - } - - #[test] - fn test_get_signal_bytes_string() { - let signal = Signal::from_string("my_signal".to_string()); - let request = Request::new(CredentialType::Face, Some(std::sync::Arc::new(signal))); - let bytes = request.get_signal_bytes().unwrap(); - assert_eq!(bytes, b"my_signal"); - } - - #[test] - fn test_get_signal_bytes_none() { - let request = Request::new(CredentialType::Device, None); - let bytes = request.get_signal_bytes(); - assert_eq!(bytes, None); - } - - #[test] - fn test_request_json_roundtrip() { - let signal = Signal::from_string("signal_123".to_string()); - let request = Request::new(CredentialType::Face, Some(std::sync::Arc::new(signal))); - - let json = request.to_json().unwrap(); - assert!(json.contains("face")); - assert!(json.contains("signal_123")); - - let parsed = Request::from_json(&json).unwrap(); - assert_eq!(parsed.credential_type(), CredentialType::Face); - assert!(parsed.get_signal_bytes().is_some()); - assert_eq!(parsed.get_signal_bytes().unwrap(), b"signal_123"); - assert_eq!(parsed.face_auth(), None); - } - - #[test] - fn test_proof_json_roundtrip() { - let proof = Proof { - proof: "0x123".to_string(), - merkle_root: "0x456".to_string(), - nullifier_hash: "0x789".to_string(), - verification_level: CredentialType::Orb, - }; - - let json = proof_to_json(&proof).unwrap(); - let parsed = proof_from_json(&json).unwrap(); - - assert_eq!(parsed.proof, "0x123"); - assert_eq!(parsed.merkle_root, "0x456"); - assert_eq!(parsed.nullifier_hash, "0x789"); - assert_eq!(parsed.verification_level, CredentialType::Orb); - } - - #[test] - fn test_credential_to_string() { - assert_eq!(credential_to_string(&CredentialType::Orb), "orb"); - assert_eq!(credential_to_string(&CredentialType::Face), "face"); - assert_eq!(credential_to_string(&CredentialType::Device), "device"); - assert_eq!( - credential_to_string(&CredentialType::SecureDocument), - "secure_document" - ); - assert_eq!(credential_to_string(&CredentialType::Document), "document"); - } -} diff --git a/rust/wasm/Cargo.toml b/rust/wasm/Cargo.toml deleted file mode 100644 index fd31818..0000000 --- a/rust/wasm/Cargo.toml +++ /dev/null @@ -1,27 +0,0 @@ -[package] -name = "idkit-wasm" -version.workspace = true -edition.workspace = true -license.workspace = true -repository.workspace = true -authors.workspace = true -description = "WASM bindings for IDKit - for use in browsers" - -[package.metadata.wasm-pack.profile.release] -wasm-opt = false - -[lib] -crate-type = ["cdylib", "rlib"] - -[dependencies] -idkit-core = { path = "../core", default-features = false, features = ["wasm-crypto", "bridge-wasm"] } -wasm-bindgen = { workspace = true } -wasm-bindgen-futures = { workspace = true } -js-sys = { workspace = true } -web-sys = { workspace = true } -serde = { workspace = true } -serde_json = { workspace = true } -serde-wasm-bindgen = "0.6" - -[dev-dependencies] -wasm-bindgen-test = "0.3" diff --git a/rust/wasm/test-crypto.mjs b/rust/wasm/test-crypto.mjs deleted file mode 100644 index 3f2f1fc..0000000 --- a/rust/wasm/test-crypto.mjs +++ /dev/null @@ -1,136 +0,0 @@ -/** - * Quick test to verify WASM crypto functions work correctly - */ - -import { readFile } from 'fs/promises'; -import { fileURLToPath } from 'url'; -import { dirname, join } from 'path'; - -// Resolve paths from project root for portability -const __dirname = dirname(fileURLToPath(import.meta.url)); -const projectRoot = join(__dirname, '..', '..'); -const wasmDir = join(projectRoot, 'js', 'packages', 'core', 'wasm'); - -// Dynamic import to allow path resolution -const { default: init, BridgeEncryption, hashSignal, base64Encode, base64Decode } = await import( - join(wasmDir, 'idkit_wasm.js') -); - -console.log('Testing WASM Crypto Functions...\n'); - -// Initialize WASM -console.log('Initializing WASM...'); -const wasmPath = join(wasmDir, 'idkit_wasm_bg.wasm'); -const wasmBuffer = await readFile(wasmPath); -await init(wasmBuffer); -console.log('✓ WASM initialized\n'); - -// Test 1: BridgeEncryption -console.log('1. Testing BridgeEncryption...'); -try { - const encryption = new BridgeEncryption(); - console.log('✓ BridgeEncryption created'); - - const keyB64 = encryption.keyBase64(); - const nonceB64 = encryption.nonceBase64(); - console.log(`✓ Key (base64): ${keyB64.substring(0, 20)}...`); - console.log(`✓ Nonce (base64): ${nonceB64.substring(0, 20)}...`); - - // Test encrypt/decrypt - const plaintext = 'Hello, World! This is a test message.'; - const encrypted = encryption.encrypt(plaintext); - console.log(`✓ Encrypted: ${encrypted.substring(0, 30)}...`); - - const decrypted = encryption.decrypt(encrypted); - console.log(`✓ Decrypted: ${decrypted}`); - - if (decrypted === plaintext) { - console.log('✓ Encryption/decryption round-trip successful\n'); - } else { - console.error('✗ Decrypted text does not match original\n'); - process.exit(1); - } -} catch (e) { - console.error(`✗ BridgeEncryption test failed: ${e}\n`); - process.exit(1); -} - -// Test 2: Multiple encryption instances -console.log('2. Testing multiple encryption instances...'); -try { - const encryption1 = new BridgeEncryption(); - const encryption2 = new BridgeEncryption(); - console.log('✓ Created two independent encryption instances'); - - const plaintext = 'Test message for independent instances'; - const encrypted1 = encryption1.encrypt(plaintext); - const decrypted1 = encryption1.decrypt(encrypted1); - - const encrypted2 = encryption2.encrypt(plaintext); - const decrypted2 = encryption2.decrypt(encrypted2); - - if (decrypted1 === plaintext && decrypted2 === plaintext && encrypted1 !== encrypted2) { - console.log('✓ Each instance encrypts/decrypts independently with different keys\n'); - } else { - console.error('✗ Independent encryption failed\n'); - process.exit(1); - } -} catch (e) { - console.error(`✗ Multiple instances test failed: ${e}\n`); - process.exit(1); -} - -// Test 3: hashSignal -console.log('3. Testing hashSignal...'); -try { - const signal = 'test_signal_123'; - const hash = hashSignal(signal); - console.log(`✓ Hash of "${signal}": ${hash}`); - - if (hash.startsWith('0x') && hash.length === 66) { - console.log('✓ Hash format correct (0x + 64 hex chars)\n'); - } else { - console.error('✗ Hash format incorrect\n'); - process.exit(1); - } - - // Test consistency - const hash2 = hashSignal(signal); - if (hash === hash2) { - console.log('✓ Hash is deterministic\n'); - } else { - console.error('✗ Hash is not deterministic\n'); - process.exit(1); - } -} catch (e) { - console.error(`✗ hashSignal test failed: ${e}\n`); - process.exit(1); -} - -// Test 4: base64 encode/decode -console.log('4. Testing base64 encode/decode...'); -try { - const data = new Uint8Array([1, 2, 3, 4, 5, 255, 254, 253]); - const encoded = base64Encode(data); - console.log(`✓ Encoded: ${encoded}`); - - const decoded = base64Decode(encoded); - console.log(`✓ Decoded: ${Array.from(decoded).join(', ')}`); - - if (data.length === decoded.length && data.every((val, i) => val === decoded[i])) { - console.log('✓ Base64 round-trip successful\n'); - } else { - console.error('✗ Base64 round-trip failed\n'); - process.exit(1); - } -} catch (e) { - console.error(`✗ base64 test failed: ${e}\n`); - process.exit(1); -} - -console.log('✅ All WASM crypto tests passed!'); -console.log('\nThe WASM crypto implementation is working correctly and can be used for:'); -console.log(' - Bridge encryption (AES-256-GCM)'); -console.log(' - Signal hashing (Keccak256)'); -console.log(' - Base64 encoding/decoding'); -console.log('\nNext steps: Integrate with JavaScript bridge client'); diff --git a/scripts/build-kotlin.sh b/scripts/build-kotlin.sh index 80684a9..5d73508 100755 --- a/scripts/build-kotlin.sh +++ b/scripts/build-kotlin.sh @@ -22,7 +22,7 @@ esac HOST_LIB="$PROJECT_ROOT/target/release/libidkit.$LIB_EXT" echo "🔧 Building Rust library (host) for binding generation" -cargo build --package idkit-uniffi --release --locked +cargo build --package idkit-core --release --locked --features uniffi-bindings echo "🧬 Generating Kotlin bindings" cargo run -p uniffi-bindgen generate \ @@ -39,7 +39,6 @@ if [ -n "${CI:-}" ]; then echo "🧹 Cleaning host build artifacts to free disk space for Android builds" # Keep the final library in resources, but clean the target directory if [ -f "$RES_DIR/$(basename "$HOST_LIB")" ]; then - cargo clean --package idkit-uniffi --release || true cargo clean --package idkit-core --release || true rm -rf ~/.cargo/registry/cache || true fi @@ -68,7 +67,7 @@ else for entry in "${TARGETS[@]}"; do IFS=":" read -r TARGET ABI <<< "$entry" echo " • $TARGET -> $ABI" - CROSS_NO_WARNINGS=1 cross build --package idkit-uniffi --target "$TARGET" --release --locked + CROSS_NO_WARNINGS=1 cross build --package idkit-core --target "$TARGET" --release --locked --features uniffi-bindings mkdir -p "$JNI_DIR/$ABI" cp "$PROJECT_ROOT/target/$TARGET/release/libidkit.so" "$JNI_DIR/$ABI/libidkit.so" # Clean up Docker resources to save disk space during multi-target builds (CI only) diff --git a/scripts/build-swift.sh b/scripts/build-swift.sh index 99ac993..97d12f1 100755 --- a/scripts/build-swift.sh +++ b/scripts/build-swift.sh @@ -6,7 +6,7 @@ set -euo pipefail SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" PROJECT_ROOT="$( cd "$SCRIPT_DIR/.." && pwd )" -RUST_DIR="$PROJECT_ROOT/rust/uniffi-bindings" +RUST_DIR="$PROJECT_ROOT/rust/core" SWIFT_DIR="$PROJECT_ROOT/swift" GENERATED_DIR="$SWIFT_DIR/Sources/IDKit/Generated" @@ -17,7 +17,7 @@ echo "Project root: $PROJECT_ROOT" echo "" echo "📦 Step 1/3: Building Rust library..." cd "$PROJECT_ROOT" -cargo build --release --package idkit-uniffi +cargo build --release --package idkit-core --features uniffi-bindings # Step 2: Generate Swift bindings echo "" diff --git a/scripts/build-wasm.sh b/scripts/build-wasm.sh index cbd81cd..d5b3502 100755 --- a/scripts/build-wasm.sh +++ b/scripts/build-wasm.sh @@ -21,7 +21,7 @@ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" PROJECT_ROOT="$(dirname "$SCRIPT_DIR")" # Paths -WASM_CRATE="$PROJECT_ROOT/rust/wasm" +CORE_CRATE="$PROJECT_ROOT/rust/core" OUTPUT_DIR="$PROJECT_ROOT/js/packages/core/wasm" # Check if wasm-pack is installed @@ -31,9 +31,9 @@ if ! command -v wasm-pack &> /dev/null; then exit 1 fi -# Check if the wasm crate exists -if [ ! -d "$WASM_CRATE" ]; then - echo -e "${RED}Error: WASM crate not found at $WASM_CRATE${NC}" +# Check if the core crate exists +if [ ! -d "$CORE_CRATE" ]; then + echo -e "${RED}Error: Core crate not found at $CORE_CRATE${NC}" exit 1 fi @@ -46,12 +46,14 @@ echo -e "${YELLOW}Compiling Rust to WASM...${NC}" # --target web: Generate ES modules for use in browsers # --out-dir: Output directory for generated files # --out-name: Name of the generated WASM file (default: package name) -cd "$WASM_CRATE" +# --features wasm-bindings: Enable WASM bindings in idkit-core +cd "$CORE_CRATE" wasm-pack build \ --target web \ --out-dir "$OUTPUT_DIR" \ --out-name idkit_wasm \ - --release + --release \ + -- --features wasm-bindings # wasm-pack generates a package.json and .gitignore we don't need echo -e "${YELLOW}Cleaning up unnecessary files...${NC}" diff --git a/scripts/package-kotlin.sh b/scripts/package-kotlin.sh index 2b3f07d..869f50f 100755 --- a/scripts/package-kotlin.sh +++ b/scripts/package-kotlin.sh @@ -12,7 +12,7 @@ echo "📦 Packaging Kotlin bindings" SKIP_ANDROID=${SKIP_ANDROID:-0} "$SCRIPT_DIR/build-kotlin.sh" VERSION=$(cargo metadata --no-deps --format-version 1 \ - | jq -r '.packages[] | select(.name=="idkit-uniffi") | .version') + | jq -r '.packages[] | select(.name=="idkit-core") | .version') rm -rf "$DIST_DIR" mkdir -p "$DIST_DIR" diff --git a/scripts/package-swift.sh b/scripts/package-swift.sh index 547f465..aaccdd2 100755 --- a/scripts/package-swift.sh +++ b/scripts/package-swift.sh @@ -29,11 +29,11 @@ cd "$PROJECT_ROOT" rustup target add aarch64-apple-ios-sim x86_64-apple-ios aarch64-apple-ios aarch64-apple-darwin x86_64-apple-darwin >/dev/null echo "🔧 Building Rust library for Apple targets" -cargo build --package idkit-uniffi --target aarch64-apple-ios-sim --release --locked -cargo build --package idkit-uniffi --target x86_64-apple-ios --release --locked -cargo build --package idkit-uniffi --target aarch64-apple-ios --release --locked -cargo build --package idkit-uniffi --target aarch64-apple-darwin --release --locked -cargo build --package idkit-uniffi --target x86_64-apple-darwin --release --locked +cargo build --package idkit-core --target aarch64-apple-ios-sim --release --locked --features uniffi-bindings +cargo build --package idkit-core --target x86_64-apple-ios --release --locked --features uniffi-bindings +cargo build --package idkit-core --target aarch64-apple-ios --release --locked --features uniffi-bindings +cargo build --package idkit-core --target aarch64-apple-darwin --release --locked --features uniffi-bindings +cargo build --package idkit-core --target x86_64-apple-darwin --release --locked --features uniffi-bindings cp target/aarch64-apple-ios/release/libidkit.a target/aarch64-apple-ios/release/libidkitFFI.a cp target/x86_64-apple-ios/release/libidkit.a target/x86_64-apple-ios/release/libidkitFFI.a @@ -68,28 +68,21 @@ cargo run -p uniffi-bindgen generate \ --out-dir "$IOS_BUILD/bindings" rm -f "$GENERATED_DIR"/* -cp "$IOS_BUILD/bindings"/idkit.swift "$GENERATED_DIR/" cp "$IOS_BUILD/bindings"/idkit_core.swift "$GENERATED_DIR/" -cp "$IOS_BUILD/bindings"/idkitFFI.h "$GENERATED_DIR/" -cp "$IOS_BUILD/bindings"/idkitFFI.modulemap "$GENERATED_DIR/" cp "$IOS_BUILD/bindings"/idkit_coreFFI.h "$GENERATED_DIR/" cp "$IOS_BUILD/bindings"/idkit_coreFFI.modulemap "$GENERATED_DIR/" -rm -f "$FFI_INCLUDE_DIR"/idkitFFI.h "$FFI_INCLUDE_DIR"/idkit_coreFFI.h "$FFI_INCLUDE_DIR"/module.modulemap -cp "$IOS_BUILD/bindings"/idkitFFI.h "$FFI_INCLUDE_DIR/" +rm -f "$FFI_INCLUDE_DIR"/idkit_coreFFI.h "$FFI_INCLUDE_DIR"/module.modulemap cp "$IOS_BUILD/bindings"/idkit_coreFFI.h "$FFI_INCLUDE_DIR/" cat <<'EOF' > "$FFI_INCLUDE_DIR/module.modulemap" module idkitFFI { - header "idkitFFI.h" header "idkit_coreFFI.h" export * } EOF -cp "$IOS_BUILD/bindings"/idkitFFI.h "$IOS_BUILD/Headers/IDKit/" cp "$IOS_BUILD/bindings"/idkit_coreFFI.h "$IOS_BUILD/Headers/IDKit/" -cat "$IOS_BUILD/bindings"/idkitFFI.modulemap > "$IOS_BUILD/Headers/IDKit/module.modulemap" -cat "$IOS_BUILD/bindings"/idkit_coreFFI.modulemap >> "$IOS_BUILD/Headers/IDKit/module.modulemap" +cp "$IOS_BUILD/bindings"/idkit_coreFFI.modulemap "$IOS_BUILD/Headers/IDKit/module.modulemap" echo "🏗️ Creating XCFramework" xcodebuild -create-xcframework \ diff --git a/swift/README.md b/swift/README.md index 4775e11..363d681 100644 --- a/swift/README.md +++ b/swift/README.md @@ -267,8 +267,8 @@ rustup target add aarch64-apple-ios-sim x86_64-apple-ios aarch64-apple-ios rustup target add aarch64-apple-darwin x86_64-apple-darwin # 2. Build for all platforms -cargo build --release --package idkit-uniffi --target aarch64-apple-ios -cargo build --release --package idkit-uniffi --target aarch64-apple-darwin +cargo build --release --package idkit-core --target aarch64-apple-ios --features uniffi-bindings +cargo build --release --package idkit-core --target aarch64-apple-darwin --features uniffi-bindings # ... (see scripts/package-swift.sh for complete build steps) # 3. Generate bindings