diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 23d606062..892ee969a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -29,7 +29,9 @@ jobs: - uses: worldcoin/bedrock/.github/actions/version-alignment@857c252013ac52cea24f5d7add3475a909d663f2 # main with: - uniffi_version: ${{ vars.UNIFFI_VERSION }} + # Match the temporary generator compatibility pin in Cargo.toml. + # Restore vars.UNIFFI_VERSION when the web generator supports 0.32. + uniffi_version: "0.31.2" rust_toolchain_channel: ${{ vars.RUST_TOOLCHAIN_CHANNEL }} lint: diff --git a/Cargo.lock b/Cargo.lock index 4ceabacc0..0630d9213 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1479,11 +1479,11 @@ dependencies = [ [[package]] name = "askama" -version = "0.15.6" +version = "0.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b8246bcbf8eb97abef10c2d92166449680d41d55c0fc6978a91dec2e3619608" +checksum = "f75363874b771be265f4ffe307ca705ef6f3baa19011c149da8674a87f1b75c4" dependencies = [ - "askama_macros 0.15.6", + "askama_derive 0.14.0", "itoa", "percent-encoding", "serde", @@ -1492,11 +1492,11 @@ dependencies = [ [[package]] name = "askama" -version = "0.16.0" +version = "0.15.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1bf825125edd887a019d0a3a837dcc5499a68b0d034cc3eb594070c3e18addc" +checksum = "9b8246bcbf8eb97abef10c2d92166449680d41d55c0fc6978a91dec2e3619608" dependencies = [ - "askama_macros 0.16.0", + "askama_macros", "itoa", "percent-encoding", "serde", @@ -1505,11 +1505,11 @@ dependencies = [ [[package]] name = "askama_derive" -version = "0.15.6" +version = "0.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f9670bc84a28bb3da91821ef74226949ab63f1265aff7c751634f1dd0e6f97c" +checksum = "129397200fe83088e8a68407a8e2b1f826cf0086b21ccdb866a722c8bcd3a94f" dependencies = [ - "askama_parser 0.15.6", + "askama_parser 0.14.0", "basic-toml", "memchr", "proc-macro2", @@ -1522,13 +1522,12 @@ dependencies = [ [[package]] name = "askama_derive" -version = "0.16.0" +version = "0.15.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e1c7065972a130eafa84215f21352ae15b4a7393da48c1f5e103904490736738" +checksum = "2f9670bc84a28bb3da91821ef74226949ab63f1265aff7c751634f1dd0e6f97c" dependencies = [ - "askama_parser 0.16.0", + "askama_parser 0.15.6", "basic-toml", - "glob", "memchr", "proc-macro2", "quote", @@ -1547,33 +1546,23 @@ dependencies = [ "askama_derive 0.15.6", ] -[[package]] -name = "askama_macros" -version = "0.16.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0e23b1d2c4bd39a41971f6124cef4cc6fd0540913ecb90919b69ab3bbe44ae1a" -dependencies = [ - "askama_derive 0.16.0", -] - [[package]] name = "askama_parser" -version = "0.15.6" +version = "0.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d0af3691ba3af77949c0b5a3925444b85cb58a0184cc7fec16c68ba2e7be868" +checksum = "d6ab5630b3d5eaf232620167977f95eb51f3432fc76852328774afbd242d4358" dependencies = [ - "rustc-hash", + "memchr", "serde", "serde_derive", - "unicode-ident", - "winnow 1.0.3", + "winnow 0.7.15", ] [[package]] name = "askama_parser" -version = "0.16.0" +version = "0.15.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7db09fde9143e7ac4513358fb32ee32847125b63b18ea715afd487956da715da" +checksum = "5d0af3691ba3af77949c0b5a3925444b85cb58a0184cc7fec16c68ba2e7be868" dependencies = [ "rustc-hash", "serde", @@ -2073,19 +2062,18 @@ dependencies = [ [[package]] name = "cargo-platform" -version = "0.3.3" +version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd0061da739915fae12ea00e16397555ed4371a6bb285431aab930f61b0aa4ba" +checksum = "e35af189006b9c0f00a064685c727031e3ed2d8020f7ba284d78cc2671bd36ea" dependencies = [ "serde", - "serde_core", ] [[package]] name = "cargo_metadata" -version = "0.23.1" +version = "0.19.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef987d17b0a113becdd19d3d0022d04d7ef41f9efe4f3fb63ac44ba61df3ade9" +checksum = "dd5eb614ed4c27c5d706420e4320fbe3216ab31fa1c33cd8246ac36dae4479ba" dependencies = [ "camino", "cargo-platform", @@ -3379,9 +3367,9 @@ dependencies = [ [[package]] name = "fs-err" -version = "3.3.1" +version = "2.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b91aa448ca50d7e79433bdf3ee8d99215430d2ec02ade5aefab2a073a1822e8a" +checksum = "88a41f105fe1d5b6b34b2055e3dc59bb79b46b48b2040b9e6c7b4b5de097aa41" dependencies = [ "autocfg", ] @@ -9023,9 +9011,9 @@ checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" [[package]] name = "uniffi" -version = "0.32.0" +version = "0.31.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a782a48d72cfd7a2d65cfc7c691dbf5375c43104b3c195f7eccc716dcc3540c8" +checksum = "46eefd5468602930da46b1f49d3448c6dfc2e81295f93120f23f8174fd70267f" dependencies = [ "anyhow", "camino", @@ -9044,14 +9032,20 @@ dependencies = [ "uniffi", ] +[[package]] +name = "uniffi-runtime-wasm" +version = "0.31.0-5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb05d9a7313e50a448bbf2e28804c3acf477c379f25199476a7fff231b67f2f1" + [[package]] name = "uniffi_bindgen" -version = "0.32.0" +version = "0.31.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "533b0312c73e3b54eb78a4b257ceae390962dd4767995778309a74644643f9ac" +checksum = "c4a0c9b375d32e1365cdb2bdd7cb495eecf6fac851ddbad077412b4ee1888514" dependencies = [ "anyhow", - "askama 0.16.0", + "askama 0.14.0", "camino", "cargo_metadata", "fs-err", @@ -9072,9 +9066,9 @@ dependencies = [ [[package]] name = "uniffi_core" -version = "0.32.0" +version = "0.31.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e32e261c5b0dfaba6488f536e71957dddd6b1a498ac7eb791bee56b60a086be" +checksum = "eec017b112701681f6fbbe5d92014b5c468eb0b177a94389de03ceec40665095" dependencies = [ "anyhow", "async-compat", @@ -9085,9 +9079,9 @@ dependencies = [ [[package]] name = "uniffi_internal_macros" -version = "0.32.0" +version = "0.31.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "84ae78069a5e6772ef694fd5bdb628532c88d2c2f0e7142bf6a384636eadb1af" +checksum = "4641669b48fefbc5e80ff08c5004d9c7617fb91232131a6734ab6712779cb04c" dependencies = [ "anyhow", "indexmap 2.14.0", @@ -9098,9 +9092,9 @@ dependencies = [ [[package]] name = "uniffi_macros" -version = "0.32.0" +version = "0.31.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "330be6770532e86320df31f54c70bb0be67588594e8e77fa56e9083a3fed5d0d" +checksum = "eeb8617ee814de22caf7417bf514715ba0b3f46bd9d5a5d794413fd8282cb737" dependencies = [ "camino", "fs-err", @@ -9115,9 +9109,9 @@ dependencies = [ [[package]] name = "uniffi_meta" -version = "0.32.0" +version = "0.31.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78de021f5547e56ab16c665a49d67d4fd3d31e77422f7739a2e9359d328cd9e7" +checksum = "58d5b94fc92803d21b2928bd15c6f06e57609b95caf98ea561c99cda1b6d2a25" dependencies = [ "anyhow", "siphasher", @@ -9127,9 +9121,9 @@ dependencies = [ [[package]] name = "uniffi_pipeline" -version = "0.32.0" +version = "0.31.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f8201bb1907ed8a42d80e11cbc25c8a033e7a31c3cff1d911f56eedb81d4948" +checksum = "032739b3ec725576914c15899dedaf080163ced86b6934566c20ec2b20ce90ca" dependencies = [ "anyhow", "heck 0.5.0", @@ -9140,9 +9134,9 @@ dependencies = [ [[package]] name = "uniffi_udl" -version = "0.32.0" +version = "0.31.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a6e57996bc58009cc29bf04845d627ae313c2547b87171c1c349d6c51a1656c0" +checksum = "fc0a1d0a0252ce1af9e8ce78ba67ac0d8937fb2bedaf10cbddd43d3614d06ec6" dependencies = [ "anyhow", "textwrap", @@ -9282,6 +9276,7 @@ name = "walletkit" version = "0.22.0" dependencies = [ "uniffi", + "uniffi-runtime-wasm", "walletkit-core", ] @@ -9968,6 +9963,9 @@ name = "winnow" version = "0.7.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "df79d97927682d2fd8adb29682d1140b343be4ac0f08fd68b7765d9c059d3945" +dependencies = [ + "memchr", +] [[package]] name = "winnow" diff --git a/Cargo.toml b/Cargo.toml index 771670d43..8cf975bd4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -75,7 +75,9 @@ tokio-test = "0.4" tracing = "0.1" tracing-log = "0.2" tracing-subscriber = "0.3" -uniffi = { version = "0.32.0", features = [ +# uniffi-bindgen-react-native 0.31.0-5 cannot parse UniFFI 0.32 metadata yet. +# Remove this pin once https://github.com/jhugman/uniffi-bindgen-react-native/pull/409 lands. +uniffi = { version = "0.31.2", features = [ "tokio", "wasm-unstable-single-threaded", ] } diff --git a/README.md b/README.md index 55cadeeaf..be1fff89b 100644 --- a/README.md +++ b/README.md @@ -51,6 +51,21 @@ binding tests with `cargo xtask swift test`. See [`swift/README.md`](swift/README.md) for package integration details. +## Local development (browser/WASM) + +The experimental `walletkit-web` package builds directly from the +`walletkit` crate and contains the generated bindings, wasm-bindgen glue, and +optimized WASM module. Build it with the pinned WASM toolchain: + +```bash +nix develop .#wasm --command bun install --cwd web/walletkit --frozen-lockfile +nix develop .#wasm --command bun run --cwd web/walletkit build +``` + +The Next.js integration probe under `examples/uniffi-web-authenticator-poc` +installs the published package and consumes its public `initializeWalletKit()` +interface. + ## Local development (Android/Kotlin) ### Prerequisites diff --git a/crates/walletkit-core/Cargo.toml b/crates/walletkit-core/Cargo.toml index 074830429..7c739fe34 100644 --- a/crates/walletkit-core/Cargo.toml +++ b/crates/walletkit-core/Cargo.toml @@ -116,6 +116,10 @@ compress-zkeys = [ issuers = [] +# Exports authenticator futures without UniFFI's Tokio compatibility adapter. +# Browser WASM has no thread on which async-compat can host its fallback runtime. +uniffi-wasm = [] + # Embeds compiled zkeys into the binary at compile time for `EmbeddedZkArtifacts`. # On native targets, `CachingZkArtifacts` can cache the embedded material on disk. # Disable this feature for environments where binary size matters (e.g. WASM). diff --git a/crates/walletkit-core/src/authenticator/mod.rs b/crates/walletkit-core/src/authenticator/mod.rs index 94031a096..3c0baf0dc 100644 --- a/crates/walletkit-core/src/authenticator/mod.rs +++ b/crates/walletkit-core/src/authenticator/mod.rs @@ -100,7 +100,8 @@ fn parse_authenticator_pubkey( Ok(pubkey) } -#[uniffi::export(async_runtime = "tokio")] +#[cfg_attr(feature = "uniffi-wasm", uniffi::export)] +#[cfg_attr(not(feature = "uniffi-wasm"), uniffi::export(async_runtime = "tokio"))] impl Authenticator { /// Returns the packed account data for the holder's World ID. /// @@ -494,7 +495,8 @@ impl Authenticator { } } -#[uniffi::export(async_runtime = "tokio")] +#[cfg_attr(feature = "uniffi-wasm", uniffi::export)] +#[cfg_attr(not(feature = "uniffi-wasm"), uniffi::export(async_runtime = "tokio"))] impl Authenticator { /// Initializes a new Authenticator from a seed and with SDK defaults. /// @@ -842,7 +844,8 @@ impl From for RegistrationStatus { #[derive(uniffi::Object)] pub struct InitializingAuthenticator(CoreInitializingAuthenticator); -#[uniffi::export(async_runtime = "tokio")] +#[cfg_attr(feature = "uniffi-wasm", uniffi::export)] +#[cfg_attr(not(feature = "uniffi-wasm"), uniffi::export(async_runtime = "tokio"))] impl InitializingAuthenticator { /// Registers a new World ID with SDK defaults. /// diff --git a/crates/walletkit-core/src/credential.rs b/crates/walletkit-core/src/credential.rs index 6d51cc8a7..4d1095a68 100644 --- a/crates/walletkit-core/src/credential.rs +++ b/crates/walletkit-core/src/credential.rs @@ -125,7 +125,6 @@ impl Deref for Credential { &self.0 } } - #[cfg(test)] mod tests { use ruint::aliases::U256; diff --git a/crates/walletkit-core/src/storage/credential_storage.rs b/crates/walletkit-core/src/storage/credential_storage.rs index 34ca23fd9..cd98a13b1 100644 --- a/crates/walletkit-core/src/storage/credential_storage.rs +++ b/crates/walletkit-core/src/storage/credential_storage.rs @@ -346,6 +346,26 @@ impl CredentialStore { } } +#[cfg(all(target_arch = "wasm32", feature = "uniffi-wasm"))] +#[uniffi::export] +impl CredentialStore { + /// Creates process-local credential storage for browser demos and tests. + /// + /// The store is discarded when the page is refreshed. Its key envelope is + /// kept in memory without device-bound encryption, so callers must not use + /// this constructor for production credentials. + /// + /// # Errors + /// + /// Returns an error if the in-memory storage handle cannot be created. + #[uniffi::constructor] + pub fn new_ephemeral() -> StorageResult { + Self::from_provider_arc(Arc::new( + super::ephemeral::EphemeralStorageProvider::new(), + )) + } +} + #[uniffi::export] impl CredentialStore { /// Permanently destroys all credential storage data. diff --git a/crates/walletkit-core/src/storage/ephemeral.rs b/crates/walletkit-core/src/storage/ephemeral.rs new file mode 100644 index 000000000..1b7b492e8 --- /dev/null +++ b/crates/walletkit-core/src/storage/ephemeral.rs @@ -0,0 +1,154 @@ +//! Process-local storage components for browser demos and tests. + +#[cfg(all(target_arch = "wasm32", feature = "uniffi-wasm"))] +use std::sync::Arc; +use std::{collections::HashMap, sync::Mutex}; + +use sha2::{Digest as _, Sha256}; +use subtle::ConstantTimeEq as _; + +use super::{AtomicBlobStore, DeviceKeystore, StorageError, StorageResult}; +#[cfg(all(target_arch = "wasm32", feature = "uniffi-wasm"))] +use super::{StoragePaths, StorageProvider}; + +const ASSOCIATED_DATA_DIGEST_LENGTH: usize = 32; + +struct EphemeralKeystore; + +impl DeviceKeystore for EphemeralKeystore { + fn seal( + &self, + associated_data: Vec, + plaintext: Vec, + ) -> StorageResult> { + let mut sealed = + Vec::with_capacity(ASSOCIATED_DATA_DIGEST_LENGTH + plaintext.len()); + sealed.extend_from_slice(&Sha256::digest(associated_data)); + sealed.extend_from_slice(&plaintext); + Ok(sealed) + } + + fn open_sealed( + &self, + associated_data: Vec, + sealed: Vec, + ) -> StorageResult> { + let Some((stored_digest, plaintext)) = + sealed.split_at_checked(ASSOCIATED_DATA_DIGEST_LENGTH) + else { + return Err(StorageError::InvalidEnvelope( + "ephemeral keystore payload is too short".to_string(), + )); + }; + let expected_digest = Sha256::digest(associated_data); + if !bool::from(stored_digest.ct_eq(expected_digest.as_slice())) { + return Err(StorageError::InvalidEnvelope( + "ephemeral keystore associated data mismatch".to_string(), + )); + } + Ok(plaintext.to_vec()) + } +} + +#[derive(Default)] +struct EphemeralBlobStore { + blobs: Mutex>>, +} + +impl AtomicBlobStore for EphemeralBlobStore { + fn read(&self, path: String) -> StorageResult>> { + Ok(self + .blobs + .lock() + .map_err(|_| StorageError::BlobStore("mutex poisoned".to_string()))? + .get(&path) + .cloned()) + } + + fn write_atomic(&self, path: String, bytes: Vec) -> StorageResult<()> { + self.blobs + .lock() + .map_err(|_| StorageError::BlobStore("mutex poisoned".to_string()))? + .insert(path, bytes); + Ok(()) + } + + fn delete(&self, path: String) -> StorageResult<()> { + self.blobs + .lock() + .map_err(|_| StorageError::BlobStore("mutex poisoned".to_string()))? + .remove(&path); + Ok(()) + } +} + +#[cfg(all(target_arch = "wasm32", feature = "uniffi-wasm"))] +pub(super) struct EphemeralStorageProvider { + keystore: Arc, + blob_store: Arc, + paths: Arc, +} + +#[cfg(all(target_arch = "wasm32", feature = "uniffi-wasm"))] +impl EphemeralStorageProvider { + pub(super) fn new() -> Self { + Self { + keystore: Arc::new(EphemeralKeystore), + blob_store: Arc::new(EphemeralBlobStore::default()), + paths: Arc::new(StoragePaths::new(format!( + "walletkit-ephemeral-{}", + uuid::Uuid::new_v4() + ))), + } + } +} + +#[cfg(all(target_arch = "wasm32", feature = "uniffi-wasm"))] +impl StorageProvider for EphemeralStorageProvider { + fn keystore(&self) -> Arc { + self.keystore.clone() + } + + fn blob_store(&self) -> Arc { + self.blob_store.clone() + } + + fn paths(&self) -> Arc { + self.paths.clone() + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn ephemeral_keystore_checks_associated_data() { + let keystore = EphemeralKeystore; + let sealed = keystore + .seal(b"expected".to_vec(), b"secret".to_vec()) + .expect("seal"); + + assert_eq!( + keystore + .open_sealed(b"expected".to_vec(), sealed.clone()) + .expect("open"), + b"secret" + ); + assert!(keystore.open_sealed(b"wrong".to_vec(), sealed).is_err()); + } + + #[test] + fn ephemeral_blob_store_roundtrips_and_deletes() { + let store = EphemeralBlobStore::default(); + store + .write_atomic("key".to_string(), b"value".to_vec()) + .expect("write"); + assert_eq!( + store.read("key".to_string()).expect("read"), + Some(b"value".to_vec()) + ); + store.delete("key".to_string()).expect("delete"); + assert_eq!(store.read("key".to_string()).expect("read"), None); + } +} diff --git a/crates/walletkit-core/src/storage/mod.rs b/crates/walletkit-core/src/storage/mod.rs index 529b5c64e..10c4ab018 100644 --- a/crates/walletkit-core/src/storage/mod.rs +++ b/crates/walletkit-core/src/storage/mod.rs @@ -46,6 +46,8 @@ pub mod cache; pub mod credential_storage; pub mod credential_vault; +#[cfg(any(test, all(target_arch = "wasm32", feature = "uniffi-wasm")))] +mod ephemeral; pub mod error; pub mod keys; pub mod paths; diff --git a/crates/walletkit/Cargo.toml b/crates/walletkit/Cargo.toml index 63c292b17..8170e32c1 100644 --- a/crates/walletkit/Cargo.toml +++ b/crates/walletkit/Cargo.toml @@ -23,6 +23,11 @@ name = "walletkit" walletkit-core = { workspace = true } uniffi = { workspace = true } +[target.'cfg(all(target_arch = "wasm32", target_os = "unknown"))'.dependencies] +# ubrn's browser player supplies the UniFFI runtime imports for the generated module. +uniffi-runtime-wasm = "0.31.0-5" +walletkit-core = { workspace = true, features = ["uniffi-wasm"] } + [features] default = ["issuers"] semaphore = ["walletkit-core/semaphore"] diff --git a/crates/walletkit/src/lib.rs b/crates/walletkit/src/lib.rs index 864eac6fb..3ea1cc11c 100644 --- a/crates/walletkit/src/lib.rs +++ b/crates/walletkit/src/lib.rs @@ -5,6 +5,10 @@ //! [`walletkit_core`]. extern crate walletkit_core; + +#[cfg(all(target_arch = "wasm32", target_os = "unknown"))] +extern crate uniffi_runtime_wasm as _; + walletkit_core::uniffi_reexport_scaffolding!(); pub use walletkit_core::*; diff --git a/examples/uniffi-web-authenticator-poc/.gitignore b/examples/uniffi-web-authenticator-poc/.gitignore new file mode 100644 index 000000000..230962c61 --- /dev/null +++ b/examples/uniffi-web-authenticator-poc/.gitignore @@ -0,0 +1,4 @@ +.next/ +dist/ +node_modules/ +tsconfig.tsbuildinfo diff --git a/examples/uniffi-web-authenticator-poc/AGENTS.md b/examples/uniffi-web-authenticator-poc/AGENTS.md new file mode 100644 index 000000000..643577dfa --- /dev/null +++ b/examples/uniffi-web-authenticator-poc/AGENTS.md @@ -0,0 +1,9 @@ + + +# This is NOT the Next.js you know + +This version has breaking changes — APIs, conventions, and file structure may all differ from your training data. Read the relevant guide in `node_modules/next/dist/docs/` (resolved from this file's directory; in monorepos the `next` package may not be visible from the repo root) before writing any code. Heed deprecation notices. + +This block is written and re-added by `next dev` — verify at `node_modules/next/dist/server/lib/generate-agent-files.js`. Removing it from a diff only re-creates the uncommitted change; committing it with your work keeps the tree clean. + + diff --git a/examples/uniffi-web-authenticator-poc/CLAUDE.md b/examples/uniffi-web-authenticator-poc/CLAUDE.md new file mode 100644 index 000000000..43c994c2d --- /dev/null +++ b/examples/uniffi-web-authenticator-poc/CLAUDE.md @@ -0,0 +1 @@ +@AGENTS.md diff --git a/examples/uniffi-web-authenticator-poc/README.md b/examples/uniffi-web-authenticator-poc/README.md new file mode 100644 index 000000000..01e6a33d8 --- /dev/null +++ b/examples/uniffi-web-authenticator-poc/README.md @@ -0,0 +1,65 @@ +# WalletKit web package Next.js example + +> PROTOTYPE: integration probe for the published `walletkit-web` package. + +This example verifies that a Next.js App Router application can consume +WalletKit as an ordinary package without owning its Rust wrapper, UniFFI +generation, WASM optimization, or asset staging. + +Run it from the repository root: + +```sh +bun install --cwd examples/uniffi-web-authenticator-poc --frozen-lockfile +bun run --cwd examples/uniffi-web-authenticator-poc dev +``` + +The example installs `walletkit-web` from the npm registry and does not build the +package's Rust, generated bindings, or WASM locally. Use `bun run build` to prove the +production bundle as well. + +To test the package from this checkout instead, run `bun run walletkit:local` +from the example directory inside the WASM Nix shell. This builds and links the +local package. Run `bun run walletkit:published` to restore the registry package. + +## What the POC proves + +- `walletkit-web` hides generation and WASM loading behind + `initializeWalletKit()`. +- The package exposes WalletKit records, errors, objects, callbacks, and async + authenticator methods. +- The generated WASM loads in a browser and calls WalletKit synchronously to + derive authenticator recovery material from secure browser randomness. +- Next.js can bundle the package's generated JavaScript glue and emit its WASM + asset from a Client Component. +- The UI drives a real opt-in staging flow: account registration, ephemeral + credential-store initialization, faux credential issuance, and uniqueness + proof generation. +- A staging RP proof request is signed in the browser with the intentionally + public test key used by `walletkit-testkit`. +- A same-origin Next.js route forwards the issuance request because the hosted + staging faux issuer does not allow browser CORS preflights. + +## Experimental compatibility pins + +The published `walletkit-web` 0.21.3 package was generated with UniFFI 0.31.2 +because the released generator (`0.31.0-5`) cannot parse UniFFI 0.32 metadata. +Its internal wasm-bindgen processor is pinned to 0.2.100, while +`sqlite-wasm-rs` requires a newer schema; the package build aligns the +generator to WalletKit's 0.2.126 schema. + +WalletKit's native bindings ask UniFFI to adapt exported futures to Tokio. That +adapter creates a fallback thread when it is polled without a Tokio runtime, +which browser WASM cannot do. The `walletkit` crate enables +`walletkit-core/uniffi-wasm` only for `wasm32-unknown-unknown`, so the +authenticator exports are polled directly by the generator's WASM player while +native builds keep their existing Tokio behavior. + +The package is imported dynamically from a Client Component. This keeps the +WASM player and browser-only APIs out of Next.js server rendering. + +The package's WASM is optimized with Binaryen's `wasm-opt -Oz --converge` and +resolved from the package with `new URL(..., import.meta.url)`. Proof generation +currently embeds the proving artifacts, making the optimized WASM roughly 40 MB. +The example uses a WASM-only ephemeral store whose data and key envelope are +discarded on refresh. No attempt has been made to productionize persistent +storage, worker placement, artifact delivery, or bundle splitting. diff --git a/examples/uniffi-web-authenticator-poc/bun.lock b/examples/uniffi-web-authenticator-poc/bun.lock new file mode 100644 index 000000000..d7969f932 --- /dev/null +++ b/examples/uniffi-web-authenticator-poc/bun.lock @@ -0,0 +1,181 @@ +{ + "lockfileVersion": 1, + "configVersion": 0, + "workspaces": { + "": { + "name": "walletkit-uniffi-web-authenticator-poc", + "dependencies": { + "lossless-json": "^4.3.1", + "next": "16.3.2", + "react": "19.2.8", + "react-dom": "19.2.8", + "viem": "^2.56.0", + "walletkit-web": "0.21.3", + }, + "devDependencies": { + "@types/node": "25.0.3", + "@types/react": "19.2.14", + "@types/react-dom": "19.2.3", + "prettier": "3.6.2", + "typescript": "5.9.2", + }, + }, + }, + "packages": { + "@adraffy/ens-normalize": ["@adraffy/ens-normalize@1.11.1", "", {}, "sha512-nhCBV3quEgesuf7c7KYfperqSS14T8bYuvJ8PcLJp6znkZpFc0AuW4qBtr8eKVyPPe/8RSr7sglCWPU5eaxwKQ=="], + + "@emnapi/runtime": ["@emnapi/runtime@1.11.3", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-Xz4Tpyki7XyrpbUK1jR1AhdAdaXyhhY4lZ3neLodmhpuWfy2PAQN5B46sAiU4liOXGLkHypn/qU+jvfWSCYYLA=="], + + "@img/colour": ["@img/colour@1.1.0", "", {}, "sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ=="], + + "@img/sharp-darwin-arm64": ["@img/sharp-darwin-arm64@0.35.3", "", { "optionalDependencies": { "@img/sharp-libvips-darwin-arm64": "1.3.2" }, "os": "darwin", "cpu": "arm64" }, "sha512-RMnFX7YQsMoh7lWfcM4NEHHymBX/rLuKNPVM84XE9ONPcaSCDgE7CHIHpSgPcO2xcRthgBy1HfNO319mwhIAkg=="], + + "@img/sharp-darwin-x64": ["@img/sharp-darwin-x64@0.35.3", "", { "optionalDependencies": { "@img/sharp-libvips-darwin-x64": "1.3.2" }, "os": "darwin", "cpu": "x64" }, "sha512-Xo+5uFBtLN0BKqieTxiFzFPQAUlBbbH5iBKyRX/z1JrbnYsHTfKJnUfL8+p2TPXr1pXqao4eeL4Rl144uDpK9w=="], + + "@img/sharp-freebsd-wasm32": ["@img/sharp-freebsd-wasm32@0.35.3", "", { "dependencies": { "@img/sharp-wasm32": "0.35.3" }, "os": "freebsd" }, "sha512-lUxcqWIj2wMQ9BrwNjngcr1gWUr5xgaGThBRqPPalIC2n67Cqj1uPh8NnA/ZhAg8hUbKl+kVHKwgUIwe6ZYPrg=="], + + "@img/sharp-libvips-darwin-arm64": ["@img/sharp-libvips-darwin-arm64@1.3.2", "", { "os": "darwin", "cpu": "arm64" }, "sha512-9J6ypZFpQBj4YnePGoq/S38w6nz+vqg5WZLrLGY4YuSemdMq47GMLBPO42MzwdGwpg/agZ7xzZcFHa48xlywfg=="], + + "@img/sharp-libvips-darwin-x64": ["@img/sharp-libvips-darwin-x64@1.3.2", "", { "os": "darwin", "cpu": "x64" }, "sha512-m2pW1n6cns9VaubNwsZ+c3CRYjxNQWgJ5gPlnL1nbBcpkBvFm6SCFN5o0psFHI8w9n11NKhFkeEDns98tiqbEw=="], + + "@img/sharp-libvips-linux-arm": ["@img/sharp-libvips-linux-arm@1.3.2", "", { "os": "linux", "cpu": "arm" }, "sha512-1eMLzy92I4J6rmi4mAT8yC3HxOtniyGELlzGbNMLLeqe052ahFQ0h6LFq+lh5DsDIdYViIDst08abvSbcEdLXQ=="], + + "@img/sharp-libvips-linux-arm64": ["@img/sharp-libvips-linux-arm64@1.3.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-dqVSFynCox4C/J8kT16V7SIFAns0IjgLwkvYT7p8LQVmJ5OS5b6tI9IGflxTeuBS//zXeFIUbwt5dwxyZ17cnA=="], + + "@img/sharp-libvips-linux-ppc64": ["@img/sharp-libvips-linux-ppc64@1.3.2", "", { "os": "linux", "cpu": "ppc64" }, "sha512-3z0NHDxD6n5I9gc05U1eW1AyRm+Gznzq3naMrthPNqE6oYykcogW0l/jfpJdjYnuNl8R7yI9pNbE1XiUeyq0Aw=="], + + "@img/sharp-libvips-linux-riscv64": ["@img/sharp-libvips-linux-riscv64@1.3.2", "", { "os": "linux", "cpu": "none" }, "sha512-bsb4rI+NldGOsXuej2r8OdSS8+zXDVaCWxyWrcv6kneTOlgAHtZABRzBBCwdsPiD90J4myNJuHpg6kA20ImW/w=="], + + "@img/sharp-libvips-linux-s390x": ["@img/sharp-libvips-linux-s390x@1.3.2", "", { "os": "linux", "cpu": "s390x" }, "sha512-/ABshyj8gCpyIrNXnHn4LorDJ0HHm1VhXPBlxZ8zAtfVPAaSafXPGn+sUSIRiwaSBy0mmFjSjiXI5mkcwdChKQ=="], + + "@img/sharp-libvips-linux-x64": ["@img/sharp-libvips-linux-x64@1.3.2", "", { "os": "linux", "cpu": "x64" }, "sha512-ITPEtgffGJ0S6G9dRyw/366tJQqFRcHWPHhC+Stpg3Z8AEMrDrTr2lhdz4f/Y/HMbRh//7Z5mBzEpVdi62Oc3w=="], + + "@img/sharp-libvips-linuxmusl-arm64": ["@img/sharp-libvips-linuxmusl-arm64@1.3.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-zE9EdiUzUmg5mDT5a1rk5fYJ6GWPloTwWBYDS14naqHsL+EaMpDj1AWnpLgh3u0YCORv2Tt50wrcrpYqkP97Kw=="], + + "@img/sharp-libvips-linuxmusl-x64": ["@img/sharp-libvips-linuxmusl-x64@1.3.2", "", { "os": "linux", "cpu": "x64" }, "sha512-m0lrLiUt+lBYnCFr8qV/65yMR4E/c7/wf78I5eKTdkEakFAlZ9QlzEM3QIhhAwVeUhLAHLcCq7a7Vszq/oFNZQ=="], + + "@img/sharp-linux-arm": ["@img/sharp-linux-arm@0.35.3", "", { "optionalDependencies": { "@img/sharp-libvips-linux-arm": "1.3.2" }, "os": "linux", "cpu": "arm" }, "sha512-affVWCTLooy8TSxbDx2qkzuDeaWLNVBA+P//FNBirHsXpP2fuBhk5AuboYUnrDnzoXes8GFjpTx0SBFOCRg+FA=="], + + "@img/sharp-linux-arm64": ["@img/sharp-linux-arm64@0.35.3", "", { "optionalDependencies": { "@img/sharp-libvips-linux-arm64": "1.3.2" }, "os": "linux", "cpu": "arm64" }, "sha512-QgKDspHPnrU+GQ55XPhGwyhC8acLVOOSyAvo1oVfFmrIXLkDNmGWzAfDZ4xK8oSA1qBQrALcHX0G5UZni/SuFQ=="], + + "@img/sharp-linux-ppc64": ["@img/sharp-linux-ppc64@0.35.3", "", { "optionalDependencies": { "@img/sharp-libvips-linux-ppc64": "1.3.2" }, "os": "linux", "cpu": "ppc64" }, "sha512-sMd8rDxmpLOwv/7N44klFjOD5DUO7FLdjiXDI0hoxYaf7Ar262dQIEkosE98bps+5HPLtp/EvNqeqQtOycP/IA=="], + + "@img/sharp-linux-riscv64": ["@img/sharp-linux-riscv64@0.35.3", "", { "optionalDependencies": { "@img/sharp-libvips-linux-riscv64": "1.3.2" }, "os": "linux", "cpu": "none" }, "sha512-0Eob78yjlYPfL5vMNWAW55l3R9Y6BQS/gOfe0ZcP9mEz9ohhKSt4im1hayiknXgf8AWrFqMvJcKIdmLmEe7yeQ=="], + + "@img/sharp-linux-s390x": ["@img/sharp-linux-s390x@0.35.3", "", { "optionalDependencies": { "@img/sharp-libvips-linux-s390x": "1.3.2" }, "os": "linux", "cpu": "s390x" }, "sha512-KgAxQ0DxpNOq1rG2t5cgTgShJFGSuU7XO45cqC+1NVOuZnP6tlgZRuSYOfNupGkHID0o3cJOsw4DVeJpMovcGw=="], + + "@img/sharp-linux-x64": ["@img/sharp-linux-x64@0.35.3", "", { "optionalDependencies": { "@img/sharp-libvips-linux-x64": "1.3.2" }, "os": "linux", "cpu": "x64" }, "sha512-8pqvxubL2PGdhlPy6GLqzDYMUjyRmKAwKHYKixpdJYBUK7PJ0C029XdsnpFIdgRZG68fZiGdHVWcKPvtiPB4cA=="], + + "@img/sharp-linuxmusl-arm64": ["@img/sharp-linuxmusl-arm64@0.35.3", "", { "optionalDependencies": { "@img/sharp-libvips-linuxmusl-arm64": "1.3.2" }, "os": "linux", "cpu": "arm64" }, "sha512-Vz0iQjzzcSX3HCbfwFfCSG/9SCIqyO0mH2sXyiHaAYfBk0cRsCWXRyQYX0ovCK/PAQBbTzQ0dsPQHh5MAFL59w=="], + + "@img/sharp-linuxmusl-x64": ["@img/sharp-linuxmusl-x64@0.35.3", "", { "optionalDependencies": { "@img/sharp-libvips-linuxmusl-x64": "1.3.2" }, "os": "linux", "cpu": "x64" }, "sha512-6O1NPKcDVj9QEdg7Hx549EX8U0rp6yXQERqru6yRN7fGBn32UvIRJUlWnk+8xDCiG76hXVBbX82NZ/ZKr0euIg=="], + + "@img/sharp-wasm32": ["@img/sharp-wasm32@0.35.3", "", { "dependencies": { "@emnapi/runtime": "^1.11.1" } }, "sha512-cZ0XkcYGpHZkqW6iCkqTcmUC0CD9DhD5d/qeZlZkfRBn6GnHniZXLUo5+9xw8Iv76YE6LQFN9YNBlKREcCG76w=="], + + "@img/sharp-webcontainers-wasm32": ["@img/sharp-webcontainers-wasm32@0.35.3", "", { "dependencies": { "@img/sharp-wasm32": "0.35.3" }, "cpu": "none" }, "sha512-2rnq7bX3NzeR2T4YWgz8qiG4h3TSdMe+vN1iQXpJleSJ3SM5zQ8Fy2SyyXAWlbxpEZ2Y+Z4u1BePgJEYbSy80Q=="], + + "@img/sharp-win32-arm64": ["@img/sharp-win32-arm64@0.35.3", "", { "os": "win32", "cpu": "arm64" }, "sha512-4bPwFdMbeC4JQ8L8LOyWp6nsHcboP5fxkp6iPOXz2Vg49R42TuMs2whkJ5OAP4/Ul035qOzy0AecOF9VOscn4w=="], + + "@img/sharp-win32-ia32": ["@img/sharp-win32-ia32@0.35.3", "", { "os": "win32", "cpu": "ia32" }, "sha512-r53mXsBN6lFUDiST764SvgwUdHAqM4rPAiDzAmf4fLoB6X/rkfyTrLCg6+g17wJJiCmB3JYgHuUldCWUIRFSXw=="], + + "@img/sharp-win32-x64": ["@img/sharp-win32-x64@0.35.3", "", { "os": "win32", "cpu": "x64" }, "sha512-D4y1vNeZrIIJCN+uHaWVtH86B+aCrdMYYjicy9pXHvbGZeGYLLSd3wdVuC37FxVXlU1ARsk84eKWfWMXGYEqvA=="], + + "@next/env": ["@next/env@16.3.2", "", {}, "sha512-8k4YoG8cM7LWlkfzGNYCRBbFNlernLiMw4s0btVl+CmmWqn3VpYypA72/5Feb1UWdxe6tHqr5KHP4p4Y4m9luA=="], + + "@next/swc-darwin-arm64": ["@next/swc-darwin-arm64@16.3.2", "", { "os": "darwin", "cpu": "arm64" }, "sha512-ib5Llm93YCKoKWDh6ZaHq6QWTuOZ2bRkSnUwMmX8dsRIOkBNL1vVlSiUKSfixPL9SSh9pvukzqajk/klkn5vqg=="], + + "@next/swc-darwin-x64": ["@next/swc-darwin-x64@16.3.2", "", { "os": "darwin", "cpu": "x64" }, "sha512-qd98fX2+I5nYJDioW2o7nSjoxM5KvWdeDefM80igia4+C/qSIEhH4MhTE+hO/7qKM7W37/Mq+dOWp8UePSyLHw=="], + + "@next/swc-linux-arm64-gnu": ["@next/swc-linux-arm64-gnu@16.3.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-vqsgb6FAOzcrCccsLXiKtAy5t8EzO+uOazuFaSkQxeY0tNONG3vpHYy8pyBafcI5SNFPTeyard6yTr6SzNGo2A=="], + + "@next/swc-linux-arm64-musl": ["@next/swc-linux-arm64-musl@16.3.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-xIe1eujfHUB2XcxHGddxJyu6TJRPjC5NpIkQYB/32ESkt5VkQyIAjmLRS38c+s6QY+qjtY/4KarVDzXRuD7lZQ=="], + + "@next/swc-linux-x64-gnu": ["@next/swc-linux-x64-gnu@16.3.2", "", { "os": "linux", "cpu": "x64" }, "sha512-Fe0SA2j8X0kmc3aveuHD7UktO3AE2+mH3LguP60vGbz7u0z+MrDXbeb5iZFYAwR7EzzzXJ2Yk966w9mGTFMqfA=="], + + "@next/swc-linux-x64-musl": ["@next/swc-linux-x64-musl@16.3.2", "", { "os": "linux", "cpu": "x64" }, "sha512-TFBipb+gyesI/2Ve4zVu7kGltBWN/R466G5/1gtt2lECfc22G1pjkTxu68Q9aFcOaXiRGTQfvDbQQFe7mYgxiQ=="], + + "@next/swc-win32-arm64-msvc": ["@next/swc-win32-arm64-msvc@16.3.2", "", { "os": "win32", "cpu": "arm64" }, "sha512-rVtmnNpBYIosDnKD/96dKxFsJnwnn1WRGG/HioSe8XCm2ksSHNrd2R6+hSjvTBxeMNhJ9pYeu/90cWB1nQLuNA=="], + + "@next/swc-win32-x64-msvc": ["@next/swc-win32-x64-msvc@16.3.2", "", { "os": "win32", "cpu": "x64" }, "sha512-H4Y2o2/JcHu8LtwzD5CXfHhwxwz8gfsx2HXDEw46Mtev5xHnEmB7HNtZtmriw5ReUOjRtcDqo7XSbU01FT9NlA=="], + + "@noble/ciphers": ["@noble/ciphers@1.3.0", "", {}, "sha512-2I0gnIVPtfnMw9ee9h1dJG7tp81+8Ob3OJb3Mv37rx5L40/b0i7djjCVvGOVqc9AEIQyvyu1i6ypKdFw8R8gQw=="], + + "@noble/curves": ["@noble/curves@1.9.1", "", { "dependencies": { "@noble/hashes": "1.8.0" } }, "sha512-k11yZxZg+t+gWvBbIswW0yoJlu8cHOC7dhunwOzoWH/mXGBiYyR4YY6hAEK/3EUs4UpB8la1RfdRpeGsFHkWsA=="], + + "@noble/hashes": ["@noble/hashes@1.8.0", "", {}, "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A=="], + + "@scure/base": ["@scure/base@1.2.6", "", {}, "sha512-g/nm5FgUa//MCj1gV09zTJTaM6KBAHqLN907YVQqf7zC49+DcO4B1so4ZX07Ef10Twr6nuqYEH9GEggFXA4Fmg=="], + + "@scure/bip32": ["@scure/bip32@1.7.0", "", { "dependencies": { "@noble/curves": "~1.9.0", "@noble/hashes": "~1.8.0", "@scure/base": "~1.2.5" } }, "sha512-E4FFX/N3f4B80AKWp5dP6ow+flD1LQZo/w8UnLGYZO674jS6YnYeepycOOksv+vLPSpgN35wgKgy+ybfTb2SMw=="], + + "@scure/bip39": ["@scure/bip39@1.6.0", "", { "dependencies": { "@noble/hashes": "~1.8.0", "@scure/base": "~1.2.5" } }, "sha512-+lF0BbLiJNwVlev4eKelw1WWLaiKXw7sSl8T6FvBlWkdX+94aGJ4o8XjUdlyhTCjd8c+B3KT3JfS8P0bLRNU6A=="], + + "@swc/helpers": ["@swc/helpers@0.5.23", "", { "dependencies": { "tslib": "^2.8.0" } }, "sha512-5lSsMOTXURePglDfvuAQUqkGek9Hg2kksOYay2m0+XR++b2NWYL/4sWyuvVBIs8oKnJaxkdi9whaL/sqN13afw=="], + + "@types/node": ["@types/node@25.0.3", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-W609buLVRVmeW693xKfzHeIV6nJGGz98uCPfeXI1ELMLXVeKYZ9m15fAMSaUPBHYLGFsVRcMmSCksQOrZV9BYA=="], + + "@types/react": ["@types/react@19.2.14", "", { "dependencies": { "csstype": "^3.2.2" } }, "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w=="], + + "@types/react-dom": ["@types/react-dom@19.2.3", "", { "peerDependencies": { "@types/react": "^19.2.0" } }, "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ=="], + + "@ubjs/core": ["@ubjs/core@0.31.0-5", "", {}, "sha512-oRBRtyYOhaodiOY3rLMZFgAHGFMmD44XefMpsAx4Ja0/rO+45kVdQB1VZsblQMLYxP965i3IHm56eymgo3I5eQ=="], + + "@ubjs/wasm": ["@ubjs/wasm@0.31.0-5", "", { "peerDependencies": { "@ubjs/core": "^0.31.0-5" } }, "sha512-dO7i67DQ1UKBSegznLVjVrYowmxY7gaXXluBdtJXPYgcR5vzwYf2ZbxAUqj4qL1JkqxRmeTKgXtuesFI+D5dPg=="], + + "abitype": ["abitype@1.2.3", "", { "peerDependencies": { "typescript": ">=5.0.4", "zod": "^3.22.0 || ^4.0.0" }, "optionalPeers": ["zod"] }, "sha512-Ofer5QUnuUdTFsBRwARMoWKOH1ND5ehwYhJ3OJ/BQO+StkwQjHw0XyVh4vDttzHB7QOFhPHa/o413PJ82gU/Tg=="], + + "baseline-browser-mapping": ["baseline-browser-mapping@2.11.19", "", { "bin": "dist/cli.cjs" }, "sha512-Grytf1xOxOEMTGRwx6rLGKkTabd4vMg3VrKdj/7joCmV0qgh4QwMMO6xh34YEXQqirAuUdgQGa5orJQQ+69RBw=="], + + "caniuse-lite": ["caniuse-lite@1.0.30001810", "", {}, "sha512-TITQPUkaz+aVk5GL6NhOdwk1aEaNTSDPsGFWrTuhKGtjTF70jL/Oht2W4c6rXUe5fu7Ie19VIahAXHIIiWWNeg=="], + + "client-only": ["client-only@0.0.1", "", {}, "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA=="], + + "csstype": ["csstype@3.2.3", "", {}, "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ=="], + + "detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="], + + "eventemitter3": ["eventemitter3@5.0.1", "", {}, "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA=="], + + "isows": ["isows@1.0.7", "", { "peerDependencies": { "ws": "*" } }, "sha512-I1fSfDCZL5P0v33sVqeTDSpcstAg/N+wF5HS033mogOVIp4B+oHC7oOCsA3axAbBSGTJ8QubbNmnIRN/h8U7hg=="], + + "lossless-json": ["lossless-json@4.3.1", "", {}, "sha512-SqD/Bg3ZfltBJ2Z14hJ/BihnvtV553WO4g9/ePtlp4lrnl9jF3AdIJt53A/Wkg/0Li+LMfxaBqgx1MiFZdQlpQ=="], + + "nanoid": ["nanoid@3.3.18", "", { "bin": "bin/nanoid.cjs" }, "sha512-DTg4MJbGMWkfi6VZFdNt2/caMbQy4Ou+Op/hJQvGEWcnVfoA1QA+xzRKAzw9jD6+GVOOeYr/mIcuDSdug6F6+w=="], + + "next": ["next@16.3.2", "", { "dependencies": { "@next/env": "16.3.2", "@swc/helpers": "0.5.23", "baseline-browser-mapping": "^2.9.19", "caniuse-lite": "^1.0.30001579", "postcss": "8.5.23", "styled-jsx": "5.1.6" }, "optionalDependencies": { "@next/swc-darwin-arm64": "16.3.2", "@next/swc-darwin-x64": "16.3.2", "@next/swc-linux-arm64-gnu": "16.3.2", "@next/swc-linux-arm64-musl": "16.3.2", "@next/swc-linux-x64-gnu": "16.3.2", "@next/swc-linux-x64-musl": "16.3.2", "@next/swc-win32-arm64-msvc": "16.3.2", "@next/swc-win32-x64-msvc": "16.3.2", "sharp": "^0.35.3" }, "peerDependencies": { "@opentelemetry/api": "^1.1.0", "@playwright/test": "^1.51.1", "babel-plugin-react-compiler": "*", "react": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", "react-dom": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", "sass": "^1.3.0" }, "optionalPeers": ["@opentelemetry/api", "@playwright/test", "babel-plugin-react-compiler", "sass"], "bin": "dist/bin/next" }, "sha512-/ZCaubUy17Lld1SiPWxuPbCk2ihqAxF2QNQaPZeEaEb7t1I58qhsJN187D7AfpapHAqUPXH0f/thtdW9dWgWFg=="], + + "ox": ["ox@0.14.34", "", { "dependencies": { "@adraffy/ens-normalize": "^1.11.0", "@noble/ciphers": "^1.3.0", "@noble/curves": "1.9.1", "@noble/hashes": "^1.8.0", "@scure/bip32": "^1.7.0", "@scure/bip39": "^1.6.0", "abitype": "^1.2.3", "eventemitter3": "5.0.1" }, "peerDependencies": { "typescript": ">=5.4.0" } }, "sha512-12seOIk7dv8eAoGQhcWaeKZxNz304IVcDvb9U5Y7JZAEVe21Nm1YMxLjhWah+su5BD4Omx4Zz0z5x3ij9M4GYQ=="], + + "picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="], + + "postcss": ["postcss@8.5.23", "", { "dependencies": { "nanoid": "^3.3.16", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-g50586zr4bZmwFiTlflMu8E0bDTb5I5gertgwAKmsdUlTQIhZtunzUlD1WSzwcVWPoAVpsrA6vlfCD7oXvRwgg=="], + + "prettier": ["prettier@3.6.2", "", { "bin": "bin/prettier.cjs" }, "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ=="], + + "react": ["react@19.2.8", "", {}, "sha512-PWaYA1L/q9u2u7xYQi+Y3L3Yfnie7XyLeaJICV1MGD6LprsBxcAqGjYyr0eY3p+QdsA+x/Irkt4Qif8D63+Sbw=="], + + "react-dom": ["react-dom@19.2.8", "", { "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { "react": "^19.2.8" } }, "sha512-rVprimfGBG3DR+Tq0IQG2DT5PxKth1WIGDmj5yPmlzr4YBe7uyE+Du4oVqTDXZSHGGGXRtTJEGSSePyQCMBglQ=="], + + "scheduler": ["scheduler@0.27.0", "", {}, "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q=="], + + "semver": ["semver@7.8.5", "", { "bin": "bin/semver.js" }, "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA=="], + + "sharp": ["sharp@0.35.3", "", { "dependencies": { "@img/colour": "^1.1.0", "detect-libc": "^2.1.2", "semver": "^7.8.5" }, "optionalDependencies": { "@img/sharp-darwin-arm64": "0.35.3", "@img/sharp-darwin-x64": "0.35.3", "@img/sharp-freebsd-wasm32": "0.35.3", "@img/sharp-libvips-darwin-arm64": "1.3.2", "@img/sharp-libvips-darwin-x64": "1.3.2", "@img/sharp-libvips-linux-arm": "1.3.2", "@img/sharp-libvips-linux-arm64": "1.3.2", "@img/sharp-libvips-linux-ppc64": "1.3.2", "@img/sharp-libvips-linux-riscv64": "1.3.2", "@img/sharp-libvips-linux-s390x": "1.3.2", "@img/sharp-libvips-linux-x64": "1.3.2", "@img/sharp-libvips-linuxmusl-arm64": "1.3.2", "@img/sharp-libvips-linuxmusl-x64": "1.3.2", "@img/sharp-linux-arm": "0.35.3", "@img/sharp-linux-arm64": "0.35.3", "@img/sharp-linux-ppc64": "0.35.3", "@img/sharp-linux-riscv64": "0.35.3", "@img/sharp-linux-s390x": "0.35.3", "@img/sharp-linux-x64": "0.35.3", "@img/sharp-linuxmusl-arm64": "0.35.3", "@img/sharp-linuxmusl-x64": "0.35.3", "@img/sharp-webcontainers-wasm32": "0.35.3", "@img/sharp-win32-arm64": "0.35.3", "@img/sharp-win32-ia32": "0.35.3", "@img/sharp-win32-x64": "0.35.3" } }, "sha512-ej0zVHuZGHCiABXcNxeYhpRnPNPAcvbG8RMdBAhDAxLKkCRVSpK3Iyu7qbqw3JMzoj0REeM6f3tJLtVwl0023Q=="], + + "source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="], + + "styled-jsx": ["styled-jsx@5.1.6", "", { "dependencies": { "client-only": "0.0.1" }, "peerDependencies": { "react": ">= 16.8.0 || 17.x.x || ^18.0.0-0 || ^19.0.0-0" } }, "sha512-qSVyDTeMotdvQYoHWLNGwRFJHC+i+ZvdBRYosOFgC+Wg1vx4frN2/RG/NA7SYqqvKNLf39P2LSRA2pu6n0XYZA=="], + + "tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], + + "typescript": ["typescript@5.9.2", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A=="], + + "undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="], + + "viem": ["viem@2.56.0", "", { "dependencies": { "@noble/curves": "1.9.1", "@noble/hashes": "1.8.0", "@scure/bip32": "1.7.0", "@scure/bip39": "1.6.0", "abitype": "1.2.3", "isows": "1.0.7", "ox": "0.14.34", "ws": "8.21.0" }, "peerDependencies": { "typescript": ">=5.0.4" } }, "sha512-JmkgIk4jN+im4oguLxwPv19pwSaGH9kcGSq25Ilm55106ULBBMNR3eWKKA0wZoeyLPSHIiOwZ4sGceEYEIq7LA=="], + + "walletkit-web": ["walletkit-web@0.21.3", "", { "dependencies": { "@ubjs/core": "0.31.0-5", "@ubjs/wasm": "0.31.0-5" } }, "sha512-i6TSdctv40RLVFNiHd8zWLO4VLYNpGDzoLa5s6EcO3cXuKgfoJccZ7KEYisaW9d8rpTL+9driOWNlLLQVUhqdg=="], + + "ws": ["ws@8.21.0", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g=="], + } +} diff --git a/examples/uniffi-web-authenticator-poc/next-env.d.ts b/examples/uniffi-web-authenticator-poc/next-env.d.ts new file mode 100644 index 000000000..a419cbe4e --- /dev/null +++ b/examples/uniffi-web-authenticator-poc/next-env.d.ts @@ -0,0 +1,7 @@ +/// +/// +import "./.next/dev/types/routes.d.ts"; +import "./.next/dev/types/root-params.d.ts"; + +// NOTE: This file should not be edited +// see https://nextjs.org/docs/app/api-reference/config/typescript for more information. diff --git a/examples/uniffi-web-authenticator-poc/next.config.ts b/examples/uniffi-web-authenticator-poc/next.config.ts new file mode 100644 index 000000000..78a721b64 --- /dev/null +++ b/examples/uniffi-web-authenticator-poc/next.config.ts @@ -0,0 +1,13 @@ +import type { NextConfig } from "next"; +import { fileURLToPath } from "node:url"; + +const nextConfig: NextConfig = { + // The local file dependency is symlinked outside the example directory. + // Published consumers resolve the package from their own node_modules and + // do not need this development-only Turbopack root. + turbopack: { + root: fileURLToPath(new URL("../..", import.meta.url)), + }, +}; + +export default nextConfig; diff --git a/examples/uniffi-web-authenticator-poc/package.json b/examples/uniffi-web-authenticator-poc/package.json new file mode 100644 index 000000000..636a0af2d --- /dev/null +++ b/examples/uniffi-web-authenticator-poc/package.json @@ -0,0 +1,33 @@ +{ + "name": "walletkit-uniffi-web-authenticator-poc", + "private": true, + "version": "0.0.0", + "type": "module", + "packageManager": "bun@1.3.11", + "repository": { + "type": "git", + "url": "https://github.com/worldcoin/walletkit" + }, + "scripts": { + "dev": "next dev", + "build": "next build", + "start": "next start", + "walletkit:local": "bun run --cwd ../../web/walletkit build && bun link --cwd ../../web/walletkit && bun link --no-save walletkit-web", + "walletkit:published": "bun install --frozen-lockfile --force" + }, + "dependencies": { + "lossless-json": "^4.3.1", + "next": "16.3.2", + "react": "19.2.8", + "react-dom": "19.2.8", + "viem": "^2.56.0", + "walletkit-web": "0.21.3" + }, + "devDependencies": { + "@types/node": "25.0.3", + "@types/react": "19.2.14", + "@types/react-dom": "19.2.3", + "prettier": "3.6.2", + "typescript": "5.9.2" + } +} diff --git a/examples/uniffi-web-authenticator-poc/src/app/api/faux-credential/route.ts b/examples/uniffi-web-authenticator-poc/src/app/api/faux-credential/route.ts new file mode 100644 index 000000000..48f0831a1 --- /dev/null +++ b/examples/uniffi-web-authenticator-poc/src/app/api/faux-credential/route.ts @@ -0,0 +1,30 @@ +import { NextResponse } from "next/server"; + +const FAUX_ISSUER_URL = "https://faux-issuer.us.id-infra.worldcoin.dev/issue"; +const FIELD_ELEMENT_PATTERN = /^0x[0-9a-f]{64}$/; + +export async function POST(request: Request) { + const body = (await request.json()) as { sub?: unknown }; + if (typeof body.sub !== "string" || !FIELD_ELEMENT_PATTERN.test(body.sub)) { + return NextResponse.json( + { error: "sub must be a 32-byte lowercase hex field element" }, + { status: 400 }, + ); + } + + const response = await fetch(FAUX_ISSUER_URL, { + method: "POST", + headers: { "content-type": "application/json" }, + body: JSON.stringify({ sub: body.sub }), + cache: "no-store", + }); + const responseBody = await response.text(); + + return new NextResponse(responseBody, { + status: response.status, + headers: { + "content-type": + response.headers.get("content-type") ?? "application/json", + }, + }); +} diff --git a/examples/uniffi-web-authenticator-poc/src/app/globals.css b/examples/uniffi-web-authenticator-poc/src/app/globals.css new file mode 100644 index 000000000..831d4afc0 --- /dev/null +++ b/examples/uniffi-web-authenticator-poc/src/app/globals.css @@ -0,0 +1,169 @@ +:root { + color: #15261f; + background: #eaf1ec; + font-family: Inter, ui-sans-serif, system-ui, sans-serif; +} + +* { + box-sizing: border-box; +} + +body { + min-height: 100vh; + margin: 0; +} + +main { + min-height: 100vh; + padding: 1.5rem; +} + +.card-grid { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 1.5rem; + width: 100%; + max-width: 90rem; + margin: 0 auto; +} + +.card { + min-width: 0; + padding: 2rem; + border: 1px solid #bdcbc3; + border-radius: 1rem; + background: #f9fbf9; + box-shadow: 0 1rem 3rem rgb(24 54 42 / 10%); +} + +.eyebrow { + color: #167552; + font-size: 0.75rem; + font-weight: 700; + letter-spacing: 0.12em; + text-transform: uppercase; +} + +h1 { + margin: 0.5rem 0 1rem; +} + +h2 { + margin-top: 0; +} + +h3 { + margin: 1.5rem 0 0; +} + +pre { + overflow: auto; + overflow-wrap: anywhere; + margin: 0.75rem 0 0; + padding: 1rem; + border-radius: 0.5rem; + color: #d9f7e8; + background: #10231b; + white-space: pre-wrap; +} + +.action-output { + height: 16rem; +} + +dl { + display: grid; + gap: 0.75rem; + margin: 1.5rem 0; +} + +dl div { + padding-bottom: 0.75rem; + border-bottom: 1px solid #dce5df; +} + +dt { + margin-bottom: 0.25rem; + color: #567064; + font-size: 0.75rem; + font-weight: 700; + text-transform: uppercase; +} + +dd { + overflow-wrap: anywhere; + margin: 0; + font-family: ui-monospace, SFMono-Regular, Menlo, monospace; + font-size: 0.85rem; +} + +button { + margin: 0.5rem 0.5rem 0.5rem 0; + padding: 0.7rem 1rem; + border: 0; + border-radius: 0.5rem; + color: white; + background: #167552; + font-weight: 700; + cursor: pointer; +} + +button:disabled { + cursor: not-allowed; + opacity: 0.45; +} + +.actions-card ol { + display: grid; + gap: 0.5rem; + margin: 1.5rem 0 0; + padding-left: 1.75rem; +} + +.progress-track { + position: relative; + overflow: hidden; + width: 100%; + max-width: 90rem; + height: 0.5rem; + margin: 1.5rem auto 0; + border-radius: 999px; + background: #d1ddd5; +} + +.progress-track span { + position: absolute; + width: 35%; + height: 100%; + border-radius: inherit; + background: #167552; + opacity: 0; +} + +.progress-track.is-active span { + opacity: 1; + animation: progress-indeterminate 1.2s ease-in-out infinite; +} + +@keyframes progress-indeterminate { + from { + transform: translateX(-100%); + } + + to { + transform: translateX(386%); + } +} + +@media (prefers-reduced-motion: reduce) { + .progress-track.is-active span { + width: 100%; + animation: none; + } +} + +@media (max-width: 60rem) { + .card-grid { + grid-template-columns: minmax(0, 1fr); + } +} diff --git a/examples/uniffi-web-authenticator-poc/src/app/layout.tsx b/examples/uniffi-web-authenticator-poc/src/app/layout.tsx new file mode 100644 index 000000000..d3eb0d3c9 --- /dev/null +++ b/examples/uniffi-web-authenticator-poc/src/app/layout.tsx @@ -0,0 +1,19 @@ +import type { Metadata } from "next"; +import type { ReactNode } from "react"; + +import "./globals.css"; + +export const metadata: Metadata = { + title: "WalletKit UniFFI browser POC", + description: "WalletKit authenticator running as browser WASM in Next.js", +}; + +export default function RootLayout({ + children, +}: Readonly<{ children: ReactNode }>) { + return ( + + {children} + + ); +} diff --git a/examples/uniffi-web-authenticator-poc/src/app/page.tsx b/examples/uniffi-web-authenticator-poc/src/app/page.tsx new file mode 100644 index 000000000..a7c2b6b7f --- /dev/null +++ b/examples/uniffi-web-authenticator-poc/src/app/page.tsx @@ -0,0 +1,150 @@ +"use client"; + +import { useEffect, useRef, useState } from "react"; + +import type { RecoveryData } from "walletkit-web"; + +type Action = "derive" | "register" | "initialize" | "issue" | "prove"; + +type WorkerState = { + runtime?: string; + recovery?: RecoveryData; + registered?: boolean; + authenticatorReady?: boolean; + credentialIssued?: boolean; + busy?: boolean; + status?: string; +}; + +export default function Home() { + const worker = useRef(null); + const [runtime, setRuntime] = useState("Loading…"); + const [recovery, setRecovery] = useState(); + const [registered, setRegistered] = useState(false); + const [authenticatorReady, setAuthenticatorReady] = useState(false); + const [credentialIssued, setCredentialIssued] = useState(false); + const [busy, setBusy] = useState(false); + const [status, setStatus] = useState("Initializing generated bindings…"); + + useEffect(() => { + const walletKitWorker = new Worker( + new URL("./walletkit.worker.ts", import.meta.url), + { type: "module" }, + ); + worker.current = walletKitWorker; + walletKitWorker.onmessage = (event: MessageEvent) => { + const state = event.data; + if (state.runtime !== undefined) setRuntime(state.runtime); + if (state.recovery !== undefined) setRecovery(state.recovery); + if (state.registered !== undefined) setRegistered(state.registered); + if (state.authenticatorReady !== undefined) { + setAuthenticatorReady(state.authenticatorReady); + } + if (state.credentialIssued !== undefined) { + setCredentialIssued(state.credentialIssued); + } + if (state.busy !== undefined) setBusy(state.busy); + if (state.status !== undefined) setStatus(state.status); + }; + + return () => { + worker.current = null; + walletKitWorker.terminate(); + }; + }, []); + + function perform(action: Action) { + worker.current?.postMessage(action); + } + + return ( +
+
+
+

WalletKit web package integration probe

+

WalletKit credential proof in browser WASM

+

+ Derive and register a temporary staging authenticator, issue a faux + credential, then generate a proof for it entirely in the browser. +

+
+
+
WASM runtime
+
{runtime}
+
+
+
Authenticator address
+
{recovery?.authenticatorAddress ?? "—"}
+
+
+
Authenticator public key
+
{recovery?.authenticatorPubkey ?? "—"}
+
+
+
Signer commitment
+
{recovery?.offchainSignerCommitment ?? "—"}
+
+
+ +
+ +
+

Staging credential proof

+

+ This creates a real temporary account and credential in staging. The + seed and credential store are discarded when this tab reloads. +

+
    +
  1. + +
  2. +
  3. + +
  4. +
  5. + +
  6. +
  7. + +
  8. +
+

Action output

+
{status}
+
+
+
+ +
+
+ ); +} diff --git a/examples/uniffi-web-authenticator-poc/src/app/staging.ts b/examples/uniffi-web-authenticator-poc/src/app/staging.ts new file mode 100644 index 000000000..85d3ff4bf --- /dev/null +++ b/examples/uniffi-web-authenticator-poc/src/app/staging.ts @@ -0,0 +1,134 @@ +import { parse, stringify } from "lossless-json"; +import { privateKeyToAccount } from "viem/accounts"; + +import type { + AuthenticatorLike, + CredentialStoreLike, + WalletKit, +} from "walletkit-web"; + +const FAUX_ISSUER_SCHEMA_ID = 128n; +const STAGING_RP_ID = 46n; +const STAGING_RP_PRIVATE_KEY = + "0x1111111111111111111111111111111111111111111111111111111111111111"; + +function fixedWidthBytes(value: bigint, width: number) { + const bytes = new Uint8Array(width); + for (let index = width - 1; index >= 0; index -= 1) { + bytes[index] = Number(value & 0xffn); + value >>= 8n; + } + return bytes; +} + +function hex(bytes: Uint8Array) { + return `0x${Array.from(bytes, (byte) => + byte.toString(16).padStart(2, "0"), + ).join("")}`; +} + +function concat(...chunks: Uint8Array[]) { + const result = new Uint8Array( + chunks.reduce((length, chunk) => length + chunk.length, 0), + ); + let offset = 0; + for (const chunk of chunks) { + result.set(chunk, offset); + offset += chunk.length; + } + return result; +} + +export async function issueFauxCredential( + module: WalletKit, + authenticator: AuthenticatorLike, + store: CredentialStoreLike, +) { + const blindingFactor = + await authenticator.generateCredentialBlindingFactorRemote( + FAUX_ISSUER_SCHEMA_ID, + ); + const sub = authenticator.computeCredentialSub(blindingFactor); + const response = await fetch("/api/faux-credential", { + method: "POST", + headers: { "content-type": "application/json" }, + body: JSON.stringify({ sub: sub.toHexString() }), + }); + if (!response.ok) { + throw new Error( + `Faux issuer returned ${response.status}: ${await response.text()}`, + ); + } + + const body = parse(await response.text()) as { credential?: unknown }; + if (body.credential === undefined) { + throw new Error("Faux issuer response did not contain a credential"); + } + + const serializedCredential = stringify(body.credential); + if (serializedCredential === undefined) { + throw new Error("Faux issuer response contained an invalid credential"); + } + const credentialBytes = new TextEncoder().encode(serializedCredential); + const credential = module.Credential.fromBytes(credentialBytes.buffer); + const now = BigInt(Math.floor(Date.now() / 1000)); + const credentialId = store.storeCredential( + credential, + blindingFactor, + credential.expiresAt(), + undefined, + now, + ); + + return { + credentialId, + issuerSchemaId: credential.issuerSchemaId(), + sub: sub.toHexString(), + }; +} + +export async function createStagingProofRequest( + module: WalletKit, + signal: string, +) { + const nonce = crypto.getRandomValues(new Uint8Array(32)); + // A 31-byte random value is always within the BabyJubJub base field. + nonce[0] = 0; + const action = fixedWidthBytes(1n, 32); + const createdAt = BigInt(Math.floor(Date.now() / 1000)); + const expiresAt = createdAt + 300n; + const message = concat( + new Uint8Array([1]), + nonce, + fixedWidthBytes(createdAt, 8), + fixedWidthBytes(expiresAt, 8), + action, + ); + const account = privateKeyToAccount(STAGING_RP_PRIVATE_KEY); + const signature = await account.signMessage({ message: { raw: message } }); + + return module.ProofRequest.fromJson( + JSON.stringify({ + id: crypto.randomUUID(), + version: 1, + proof_type: "uniqueness", + created_at: Number(createdAt), + expires_at: Number(expiresAt), + rp_id: `rp_${STAGING_RP_ID.toString(16).padStart(16, "0")}`, + oprf_key_id: `0x${STAGING_RP_ID.toString(16)}`, + session_id: null, + action: hex(action), + signature, + nonce: hex(nonce), + proof_requests: [ + { + identifier: "faux-credential", + issuer_schema_id: Number(FAUX_ISSUER_SCHEMA_ID), + signal: hex(new TextEncoder().encode(signal)), + genesis_issued_at_min: null, + expires_at_min: null, + }, + ], + }), + ); +} diff --git a/examples/uniffi-web-authenticator-poc/src/app/walletkit.worker.ts b/examples/uniffi-web-authenticator-poc/src/app/walletkit.worker.ts new file mode 100644 index 000000000..c00dc8a9d --- /dev/null +++ b/examples/uniffi-web-authenticator-poc/src/app/walletkit.worker.ts @@ -0,0 +1,188 @@ +import type { + AuthenticatorLike, + CredentialStoreLike, + InitializingAuthenticatorLike, + WalletKit, +} from "walletkit-web"; + +import { createStagingProofRequest, issueFauxCredential } from "./staging"; + +type Action = "derive" | "register" | "initialize" | "issue" | "prove"; + +type WorkerState = { + runtime?: string; + recovery?: { + authenticatorAddress: string; + authenticatorPubkey: string; + offchainSignerCommitment: string; + }; + registered?: boolean; + authenticatorReady?: boolean; + credentialIssued?: boolean; + busy?: boolean; + status?: string; +}; + +let bindings: WalletKit; +let seed = new Uint8Array(32); +let registration: InitializingAuthenticatorLike | undefined; +let authenticator: AuthenticatorLike | undefined; +let store: CredentialStoreLike | undefined; + +const REGISTRATION_POLL_INTERVAL_MS = 500; + +function wait(milliseconds: number) { + return new Promise((resolve) => setTimeout(resolve, milliseconds)); +} + +function printableState(state: string, details = {}) { + return JSON.stringify( + { + state, + environment: "staging", + region: "us", + ...details, + }, + (_, value) => (typeof value === "bigint" ? value.toString() : value), + 2, + ); +} + +function printableError(error: unknown) { + if (error instanceof Error && "inner" in error) { + return `${error.message}: ${JSON.stringify(error.inner)}`; + } + + return String(error); +} + +function update(state: WorkerState) { + self.postMessage(state); +} + +function deriveAuthenticator() { + seed = crypto.getRandomValues(new Uint8Array(32)); + const recovery = bindings.recoveryDataFromSeed(new Uint8Array(seed).buffer); + registration = undefined; + authenticator = undefined; + store = undefined; + update({ + recovery: { + authenticatorAddress: recovery.authenticatorAddress, + authenticatorPubkey: recovery.authenticatorPubkey, + offchainSignerCommitment: recovery.offchainSignerCommitment, + }, + registered: false, + authenticatorReady: false, + credentialIssued: false, + status: printableState("authenticator-derived", { + seed: "32 random bytes held only in memory", + }), + }); +} + +async function perform(action: Action) { + update({ busy: true }); + try { + switch (action) { + case "derive": + deriveAuthenticator(); + break; + case "register": + update({ status: "Sending registration request to the staging gateway…" }); + registration = + await bindings.InitializingAuthenticator.registerWithDefaults( + new Uint8Array(seed).buffer, + undefined, + bindings.Environment.Staging, + bindings.Region.Us, + undefined, + ); + update({ status: printableState("registration-requested") }); + while (registration) { + const result = await registration.pollStatus(); + const finalized = + bindings.RegistrationStatus.Finalized.instanceOf(result); + update({ + registered: finalized, + status: printableState( + finalized ? "registration-finalized" : "registration-pending", + { status: result.constructor.name }, + ), + }); + if (finalized) break; + await wait(REGISTRATION_POLL_INTERVAL_MS); + } + break; + case "initialize": { + update({ + status: "Initializing the registered authenticator and ephemeral store…", + }); + const ephemeralStore = bindings.CredentialStore.newEphemeral(); + const artifacts = new bindings.EmbeddedZkArtifacts().asZkArtifactSource(); + const initializedAuthenticator = + await bindings.Authenticator.initWithDefaults( + new Uint8Array(seed).buffer, + undefined, + bindings.Environment.Staging, + bindings.Region.Us, + artifacts, + ephemeralStore, + ); + initializedAuthenticator.initStorage( + BigInt(Math.floor(Date.now() / 1000)), + ); + store = ephemeralStore; + authenticator = initializedAuthenticator; + update({ + authenticatorReady: true, + status: printableState("authenticator-initialized"), + }); + break; + } + case "issue": { + if (!authenticator || !store) return; + update({ status: "Requesting a credential from the staging faux issuer…" }); + const issued = await issueFauxCredential(bindings, authenticator, store); + update({ + credentialIssued: true, + status: printableState("credential-issued", issued), + }); + break; + } + case "prove": { + if (!authenticator) return; + update({ status: "Generating a uniqueness proof in browser WASM…" }); + const request = await createStagingProofRequest( + bindings, + "walletkit-web-example", + ); + const response = await authenticator.generateProof( + request, + BigInt(Math.floor(Date.now() / 1000)), + ); + update({ status: response.toJson() }); + break; + } + } + } catch (error) { + update({ status: printableError(error) }); + } finally { + update({ busy: false }); + } +} + +self.onmessage = (event: MessageEvent) => { + void perform(event.data); +}; + +void (async () => { + try { + const { initializeWalletKit } = await import("walletkit-web"); + bindings = await initializeWalletKit(); + update({ runtime: "Ready" }); + deriveAuthenticator(); + } catch (error) { + update({ runtime: "Failed", status: printableError(error) }); + } +})(); diff --git a/examples/uniffi-web-authenticator-poc/tsconfig.json b/examples/uniffi-web-authenticator-poc/tsconfig.json new file mode 100644 index 000000000..470da820c --- /dev/null +++ b/examples/uniffi-web-authenticator-poc/tsconfig.json @@ -0,0 +1,30 @@ +{ + "compilerOptions": { + "target": "ES2022", + "lib": ["ES2022", "DOM", "DOM.Iterable"], + "allowJs": true, + "strict": true, + "noEmit": true, + "esModuleInterop": true, + "module": "ESNext", + "moduleResolution": "Bundler", + "resolveJsonModule": true, + "isolatedModules": true, + "jsx": "react-jsx", + "incremental": true, + "plugins": [ + { + "name": "next" + } + ], + "skipLibCheck": true + }, + "include": [ + "next-env.d.ts", + "src/**/*.ts", + "src/**/*.tsx", + ".next/types/**/*.ts", + ".next/dev/types/**/*.ts" + ], + "exclude": ["node_modules"] +} diff --git a/flake.nix b/flake.nix index 49b3ac9d0..d2f317bdc 100644 --- a/flake.nix +++ b/flake.nix @@ -35,6 +35,7 @@ default = pkgs.mkShell { packages = [ rustToolchain + pkgs.bun pkgs.curl pkgs.git pkgs.nargo diff --git a/nix/README.md b/nix/README.md index a384f27f3..be1c65b30 100644 --- a/nix/README.md +++ b/nix/README.md @@ -15,7 +15,7 @@ ordinary `cargo build` invocations run inside a shell. |---|---|---| | `default` | Host builds, uniffi-bindgen, nargo | all | | `android` | Cross-compile the 4 Android targets (NDK, linkers, API 23) | linux/darwin x86_64, darwin aarch64 | -| `wasm` | `wasm32-unknown-unknown` builds and headless browser tests | all | +| `wasm` | `wasm32-unknown-unknown` builds and headless browser tests with Node and Binaryen | all | ```bash nix develop .#android # enter a shell @@ -28,6 +28,7 @@ Convenience wrappers (they enter the right shell for you): nix/build-android.sh --target aarch64-linux-android nix/build-wasm.sh nix develop .#wasm --command cargo test -p walletkit-sqlite --target wasm32-unknown-unknown +nix develop .#wasm --command bun run --cwd web/walletkit build nix develop .#android --command cargo xtask kotlin build # full Android jniLibs + bindings nix develop .#android --command cargo xtask kotlin local 0.3.1 # publish to Maven Local ``` diff --git a/nix/wasm.nix b/nix/wasm.nix index c1993828e..a26f5cacc 100644 --- a/nix/wasm.nix +++ b/nix/wasm.nix @@ -18,12 +18,15 @@ in pkgs.mkShell { packages = [ rustToolchain + pkgs.bun llvm.clang-unwrapped llvm.bintools-unwrapped wasmBindgenCli + pkgs.binaryen pkgs.curl pkgs.git pkgs.nargo + pkgs.nodejs ] ++ pkgs.lib.optionals firefoxAvailable [ pkgs.firefox diff --git a/web/walletkit/.gitignore b/web/walletkit/.gitignore new file mode 100644 index 000000000..f816e0f8b --- /dev/null +++ b/web/walletkit/.gitignore @@ -0,0 +1,3 @@ +dist/ +node_modules/ +src/generated/ diff --git a/web/walletkit/README.md b/web/walletkit/README.md new file mode 100644 index 000000000..6257d7902 --- /dev/null +++ b/web/walletkit/README.md @@ -0,0 +1,28 @@ +# `walletkit-web` + +Browser WebAssembly bindings for WalletKit. + +> PROTOTYPE: the package is published while its generation and release workflow +> is being validated. + +The package owns the compiled WalletKit module, generated TypeScript bindings, +UniFFI player setup, and WASM asset resolution. Browser applications initialize +it through one interface: + +```ts +const { initializeWalletKit } = await import("walletkit-web"); +const walletKit = await initializeWalletKit(); +``` + +Build it from the repository root with the pinned toolchain: + +```sh +nix develop .#wasm --command bun install --cwd web/walletkit --frozen-lockfile +nix develop .#wasm --command bun run --cwd web/walletkit build +``` + +The release build uses `crates/walletkit` directly, disables its default +features except for `embed-zkeys`, and optimizes the generated module with +`wasm-opt -Oz --converge`. Embedding the proving artifacts currently makes the +optimized WASM roughly 40 MB; lazy artifact delivery and worker placement are +still future production work. diff --git a/web/walletkit/bun.lock b/web/walletkit/bun.lock new file mode 100644 index 000000000..3356e880e --- /dev/null +++ b/web/walletkit/bun.lock @@ -0,0 +1,84 @@ +{ + "lockfileVersion": 1, + "configVersion": 0, + "workspaces": { + "": { + "name": "walletkit-web", + "dependencies": { + "@ubjs/core": "0.31.0-5", + "@ubjs/wasm": "0.31.0-5", + }, + "devDependencies": { + "esbuild": "0.25.9", + "prettier": "3.6.2", + "typescript": "5.9.2", + "uniffi-bindgen-react-native": "0.31.0-5", + }, + }, + }, + "packages": { + "@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.25.9", "", { "os": "aix", "cpu": "ppc64" }, "sha512-OaGtL73Jck6pBKjNIe24BnFE6agGl+6KxDtTfHhy1HmhthfKouEcOhqpSL64K4/0WCtbKFLOdzD/44cJ4k9opA=="], + + "@esbuild/android-arm": ["@esbuild/android-arm@0.25.9", "", { "os": "android", "cpu": "arm" }, "sha512-5WNI1DaMtxQ7t7B6xa572XMXpHAaI/9Hnhk8lcxF4zVN4xstUgTlvuGDorBguKEnZO70qwEcLpfifMLoxiPqHQ=="], + + "@esbuild/android-arm64": ["@esbuild/android-arm64@0.25.9", "", { "os": "android", "cpu": "arm64" }, "sha512-IDrddSmpSv51ftWslJMvl3Q2ZT98fUSL2/rlUXuVqRXHCs5EUF1/f+jbjF5+NG9UffUDMCiTyh8iec7u8RlTLg=="], + + "@esbuild/android-x64": ["@esbuild/android-x64@0.25.9", "", { "os": "android", "cpu": "x64" }, "sha512-I853iMZ1hWZdNllhVZKm34f4wErd4lMyeV7BLzEExGEIZYsOzqDWDf+y082izYUE8gtJnYHdeDpN/6tUdwvfiw=="], + + "@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.25.9", "", { "os": "darwin", "cpu": "arm64" }, "sha512-XIpIDMAjOELi/9PB30vEbVMs3GV1v2zkkPnuyRRURbhqjyzIINwj+nbQATh4H9GxUgH1kFsEyQMxwiLFKUS6Rg=="], + + "@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.25.9", "", { "os": "darwin", "cpu": "x64" }, "sha512-jhHfBzjYTA1IQu8VyrjCX4ApJDnH+ez+IYVEoJHeqJm9VhG9Dh2BYaJritkYK3vMaXrf7Ogr/0MQ8/MeIefsPQ=="], + + "@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.25.9", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-z93DmbnY6fX9+KdD4Ue/H6sYs+bhFQJNCPZsi4XWJoYblUqT06MQUdBCpcSfuiN72AbqeBFu5LVQTjfXDE2A6Q=="], + + "@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.25.9", "", { "os": "freebsd", "cpu": "x64" }, "sha512-mrKX6H/vOyo5v71YfXWJxLVxgy1kyt1MQaD8wZJgJfG4gq4DpQGpgTB74e5yBeQdyMTbgxp0YtNj7NuHN0PoZg=="], + + "@esbuild/linux-arm": ["@esbuild/linux-arm@0.25.9", "", { "os": "linux", "cpu": "arm" }, "sha512-HBU2Xv78SMgaydBmdor38lg8YDnFKSARg1Q6AT0/y2ezUAKiZvc211RDFHlEZRFNRVhcMamiToo7bDx3VEOYQw=="], + + "@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.25.9", "", { "os": "linux", "cpu": "arm64" }, "sha512-BlB7bIcLT3G26urh5Dmse7fiLmLXnRlopw4s8DalgZ8ef79Jj4aUcYbk90g8iCa2467HX8SAIidbL7gsqXHdRw=="], + + "@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.25.9", "", { "os": "linux", "cpu": "ia32" }, "sha512-e7S3MOJPZGp2QW6AK6+Ly81rC7oOSerQ+P8L0ta4FhVi+/j/v2yZzx5CqqDaWjtPFfYz21Vi1S0auHrap3Ma3A=="], + + "@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.25.9", "", { "os": "linux", "cpu": "none" }, "sha512-Sbe10Bnn0oUAB2AalYztvGcK+o6YFFA/9829PhOCUS9vkJElXGdphz0A3DbMdP8gmKkqPmPcMJmJOrI3VYB1JQ=="], + + "@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.25.9", "", { "os": "linux", "cpu": "none" }, "sha512-YcM5br0mVyZw2jcQeLIkhWtKPeVfAerES5PvOzaDxVtIyZ2NUBZKNLjC5z3/fUlDgT6w89VsxP2qzNipOaaDyA=="], + + "@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.25.9", "", { "os": "linux", "cpu": "ppc64" }, "sha512-++0HQvasdo20JytyDpFvQtNrEsAgNG2CY1CLMwGXfFTKGBGQT3bOeLSYE2l1fYdvML5KUuwn9Z8L1EWe2tzs1w=="], + + "@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.25.9", "", { "os": "linux", "cpu": "none" }, "sha512-uNIBa279Y3fkjV+2cUjx36xkx7eSjb8IvnL01eXUKXez/CBHNRw5ekCGMPM0BcmqBxBcdgUWuUXmVWwm4CH9kg=="], + + "@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.25.9", "", { "os": "linux", "cpu": "s390x" }, "sha512-Mfiphvp3MjC/lctb+7D287Xw1DGzqJPb/J2aHHcHxflUo+8tmN/6d4k6I2yFR7BVo5/g7x2Monq4+Yew0EHRIA=="], + + "@esbuild/linux-x64": ["@esbuild/linux-x64@0.25.9", "", { "os": "linux", "cpu": "x64" }, "sha512-iSwByxzRe48YVkmpbgoxVzn76BXjlYFXC7NvLYq+b+kDjyyk30J0JY47DIn8z1MO3K0oSl9fZoRmZPQI4Hklzg=="], + + "@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.25.9", "", { "os": "none", "cpu": "arm64" }, "sha512-9jNJl6FqaUG+COdQMjSCGW4QiMHH88xWbvZ+kRVblZsWrkXlABuGdFJ1E9L7HK+T0Yqd4akKNa/lO0+jDxQD4Q=="], + + "@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.25.9", "", { "os": "none", "cpu": "x64" }, "sha512-RLLdkflmqRG8KanPGOU7Rpg829ZHu8nFy5Pqdi9U01VYtG9Y0zOG6Vr2z4/S+/3zIyOxiK6cCeYNWOFR9QP87g=="], + + "@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.25.9", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-YaFBlPGeDasft5IIM+CQAhJAqS3St3nJzDEgsgFixcfZeyGPCd6eJBWzke5piZuZ7CtL656eOSYKk4Ls2C0FRQ=="], + + "@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.25.9", "", { "os": "openbsd", "cpu": "x64" }, "sha512-1MkgTCuvMGWuqVtAvkpkXFmtL8XhWy+j4jaSO2wxfJtilVCi0ZE37b8uOdMItIHz4I6z1bWWtEX4CJwcKYLcuA=="], + + "@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.25.9", "", { "os": "none", "cpu": "arm64" }, "sha512-4Xd0xNiMVXKh6Fa7HEJQbrpP3m3DDn43jKxMjxLLRjWnRsfxjORYJlXPO4JNcXtOyfajXorRKY9NkOpTHptErg=="], + + "@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.25.9", "", { "os": "sunos", "cpu": "x64" }, "sha512-WjH4s6hzo00nNezhp3wFIAfmGZ8U7KtrJNlFMRKxiI9mxEK1scOMAaa9i4crUtu+tBr+0IN6JCuAcSBJZfnphw=="], + + "@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.25.9", "", { "os": "win32", "cpu": "arm64" }, "sha512-mGFrVJHmZiRqmP8xFOc6b84/7xa5y5YvR1x8djzXpJBSv/UsNK6aqec+6JDjConTgvvQefdGhFDAs2DLAds6gQ=="], + + "@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.25.9", "", { "os": "win32", "cpu": "ia32" }, "sha512-b33gLVU2k11nVx1OhX3C8QQP6UHQK4ZtN56oFWvVXvz2VkDoe6fbG8TOgHFxEvqeqohmRnIHe5A1+HADk4OQww=="], + + "@esbuild/win32-x64": ["@esbuild/win32-x64@0.25.9", "", { "os": "win32", "cpu": "x64" }, "sha512-PPOl1mi6lpLNQxnGoyAfschAodRFYXJ+9fs6WHXz7CSWKbOqiMZsubC+BQsVKuul+3vKLuwTHsS2c2y9EoKwxQ=="], + + "@ubjs/core": ["@ubjs/core@0.31.0-5", "", {}, "sha512-oRBRtyYOhaodiOY3rLMZFgAHGFMmD44XefMpsAx4Ja0/rO+45kVdQB1VZsblQMLYxP965i3IHm56eymgo3I5eQ=="], + + "@ubjs/wasm": ["@ubjs/wasm@0.31.0-5", "", { "peerDependencies": { "@ubjs/core": "^0.31.0-5" } }, "sha512-dO7i67DQ1UKBSegznLVjVrYowmxY7gaXXluBdtJXPYgcR5vzwYf2ZbxAUqj4qL1JkqxRmeTKgXtuesFI+D5dPg=="], + + "esbuild": ["esbuild@0.25.9", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.25.9", "@esbuild/android-arm": "0.25.9", "@esbuild/android-arm64": "0.25.9", "@esbuild/android-x64": "0.25.9", "@esbuild/darwin-arm64": "0.25.9", "@esbuild/darwin-x64": "0.25.9", "@esbuild/freebsd-arm64": "0.25.9", "@esbuild/freebsd-x64": "0.25.9", "@esbuild/linux-arm": "0.25.9", "@esbuild/linux-arm64": "0.25.9", "@esbuild/linux-ia32": "0.25.9", "@esbuild/linux-loong64": "0.25.9", "@esbuild/linux-mips64el": "0.25.9", "@esbuild/linux-ppc64": "0.25.9", "@esbuild/linux-riscv64": "0.25.9", "@esbuild/linux-s390x": "0.25.9", "@esbuild/linux-x64": "0.25.9", "@esbuild/netbsd-arm64": "0.25.9", "@esbuild/netbsd-x64": "0.25.9", "@esbuild/openbsd-arm64": "0.25.9", "@esbuild/openbsd-x64": "0.25.9", "@esbuild/openharmony-arm64": "0.25.9", "@esbuild/sunos-x64": "0.25.9", "@esbuild/win32-arm64": "0.25.9", "@esbuild/win32-ia32": "0.25.9", "@esbuild/win32-x64": "0.25.9" }, "bin": "bin/esbuild" }, "sha512-CRbODhYyQx3qp7ZEwzxOk4JBqmD/seJrzPa/cGjY1VtIn5E09Oi9/dB4JwctnfZ8Q8iT7rioVv5k/FNT/uf54g=="], + + "prettier": ["prettier@3.6.2", "", { "bin": "bin/prettier.cjs" }, "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ=="], + + "typescript": ["typescript@5.9.2", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A=="], + + "uniffi-bindgen-react-native": ["uniffi-bindgen-react-native@0.31.0-5", "", { "bin": { "ubrn": "bin/cli.cjs", "uniffi-bindgen-react-native": "bin/cli.cjs" } }, "sha512-YtL3gTe+RVXAQx/2P2Iuypic/CunRZC65Cs47v//YRfwWNYU4Ufd1jT3rqxSrH5b5QFCYblP89Nf/9ssZRvNgg=="], + } +} diff --git a/web/walletkit/package.json b/web/walletkit/package.json new file mode 100644 index 000000000..cd070cb64 --- /dev/null +++ b/web/walletkit/package.json @@ -0,0 +1,42 @@ +{ + "name": "walletkit-web", + "version": "0.21.3", + "description": "WalletKit enables users to hold credentials and prove their humanity with World ID .", + "type": "module", + "packageManager": "bun@1.3.11", + "files": [ + "dist/", + "README.md" + ], + "exports": { + ".": { + "types": "./dist/index.d.ts", + "import": "./dist/index.js" + } + }, + "scripts": { + "clean": "node scripts/clean.mjs", + "patch:generator": "node scripts/patch-ubrn-wasm-bindgen.mjs", + "generate": "bun run patch:generator && ubrn build wasm2 --config ubrn.config.yaml --release && node scripts/optimize-wasm.mjs && node scripts/patch-generated-index.mjs", + "build": "bun run clean && bun run generate && tsc -p tsconfig.build.json && esbuild src/index.ts --bundle --format=esm --platform=browser --target=es2022 --outfile=dist/index.js --external:@ubjs/core --external:@ubjs/wasm && node scripts/copy-wasm.mjs", + "pack:check": "bun pm pack --dry-run" + }, + "dependencies": { + "@ubjs/core": "0.31.0-5", + "@ubjs/wasm": "0.31.0-5" + }, + "devDependencies": { + "esbuild": "0.25.9", + "prettier": "3.6.2", + "typescript": "5.9.2", + "uniffi-bindgen-react-native": "0.31.0-5" + }, + "publishConfig": { + "access": "public" + }, + "repository": { + "type": "git", + "url": "https://github.com/worldcoin/walletkit", + "directory": "web/walletkit" + } +} diff --git a/web/walletkit/scripts/clean.mjs b/web/walletkit/scripts/clean.mjs new file mode 100644 index 000000000..16ed422b5 --- /dev/null +++ b/web/walletkit/scripts/clean.mjs @@ -0,0 +1,3 @@ +import { rmSync } from "node:fs"; + +rmSync(new URL("../dist/", import.meta.url), { force: true, recursive: true }); diff --git a/web/walletkit/scripts/copy-wasm.mjs b/web/walletkit/scripts/copy-wasm.mjs new file mode 100644 index 000000000..6e5851db0 --- /dev/null +++ b/web/walletkit/scripts/copy-wasm.mjs @@ -0,0 +1,8 @@ +import { copyFileSync, mkdirSync } from "node:fs"; + +const output = new URL("../dist/generated/", import.meta.url); +mkdirSync(output, { recursive: true }); +copyFileSync( + new URL("../src/generated/walletkit.wasm", import.meta.url), + new URL("walletkit.wasm", output), +); diff --git a/web/walletkit/scripts/optimize-wasm.mjs b/web/walletkit/scripts/optimize-wasm.mjs new file mode 100644 index 000000000..0b0cbe37f --- /dev/null +++ b/web/walletkit/scripts/optimize-wasm.mjs @@ -0,0 +1,15 @@ +// Optimize the generated module before it is included in the npm package. +// Binaryen is supplied by the repository's `nix develop .#wasm` shell. +import { execFileSync } from "node:child_process"; +import { renameSync } from "node:fs"; +import { fileURLToPath } from "node:url"; + +const wasm = fileURLToPath( + new URL("../src/generated/walletkit.wasm", import.meta.url), +); +const optimized = `${wasm}.optimized`; + +execFileSync("wasm-opt", [wasm, "-Oz", "--converge", "-o", optimized], { + stdio: "inherit", +}); +renameSync(optimized, wasm); diff --git a/web/walletkit/scripts/patch-generated-index.mjs b/web/walletkit/scripts/patch-generated-index.mjs new file mode 100644 index 000000000..7a2813a4c --- /dev/null +++ b/web/walletkit/scripts/patch-generated-index.mjs @@ -0,0 +1,10 @@ +// PROTOTYPE workaround: ubrn 0.31.0-5 emits readonly player definitions that +// @ubjs/wasm 0.31.0-5 types as mutable, and omits a declaration for its +// generated wasm-bindgen glue module. Runtime shapes are compatible. +import { readFileSync, writeFileSync } from "node:fs"; + +const generatedIndex = new URL("../src/generated/index.ts", import.meta.url); +const source = readFileSync(generatedIndex, "utf8"); +if (!source.startsWith("// @ts-nocheck")) { + writeFileSync(generatedIndex, `// @ts-nocheck\n${source}`); +} diff --git a/web/walletkit/scripts/patch-ubrn-wasm-bindgen.mjs b/web/walletkit/scripts/patch-ubrn-wasm-bindgen.mjs new file mode 100644 index 000000000..3a0224d53 --- /dev/null +++ b/web/walletkit/scripts/patch-ubrn-wasm-bindgen.mjs @@ -0,0 +1,40 @@ +// PROTOTYPE workaround: sqlite-wasm-rs requires wasm-bindgen >= 0.2.104, +// while ubrn 0.31.0-5 pins its post-processor to 0.2.100. Both sides must use +// the exact same schema, so align the installed generator with WalletKit. +import { execFileSync } from "node:child_process"; +import { existsSync, readFileSync, writeFileSync } from "node:fs"; + +const root = new URL( + "../node_modules/uniffi-bindgen-react-native/", + import.meta.url, +); +const manifest = new URL("Cargo.toml", root); +const lockfile = new URL("Cargo.lock", root); +const oldDependency = 'wasm-bindgen-cli-support = "=0.2.100"'; +const newDependency = 'wasm-bindgen-cli-support = "=0.2.126"'; + +const source = readFileSync(manifest, "utf8"); +if (source.includes(oldDependency)) { + writeFileSync(manifest, source.replace(oldDependency, newDependency)); +} + +if ( + existsSync(lockfile) && + /name = "wasm-bindgen-cli-support"\nversion = "0\.2\.100"/.test( + readFileSync(lockfile, "utf8"), + ) +) { + execFileSync( + "cargo", + [ + "update", + "--manifest-path", + manifest.pathname, + "-p", + "wasm-bindgen-cli-support@0.2.100", + "--precise", + "0.2.126", + ], + { stdio: "inherit" }, + ); +} diff --git a/web/walletkit/src/index.ts b/web/walletkit/src/index.ts new file mode 100644 index 000000000..ea4feef01 --- /dev/null +++ b/web/walletkit/src/index.ts @@ -0,0 +1,19 @@ +import * as generated from "./generated"; + +export type * from "./generated"; + +type GeneratedWalletKit = typeof generated; + +/** WalletKit's generated domain interface without its raw runtime hooks. */ +export type WalletKit = Omit; + +const wasm = new URL("./generated/walletkit.wasm", import.meta.url); + +/** + * Loads WalletKit's browser WASM module and returns its generated bindings. + * Repeated calls share the generated runtime's initialization promise. + */ +export async function initializeWalletKit(): Promise { + await generated.uniffiInitAsync(wasm); + return generated; +} diff --git a/web/walletkit/tsconfig.build.json b/web/walletkit/tsconfig.build.json new file mode 100644 index 000000000..34193e502 --- /dev/null +++ b/web/walletkit/tsconfig.build.json @@ -0,0 +1,17 @@ +{ + "compilerOptions": { + "allowJs": true, + "declaration": true, + "emitDeclarationOnly": true, + "esModuleInterop": true, + "lib": ["ES2022", "DOM", "DOM.Iterable"], + "module": "ESNext", + "moduleResolution": "Bundler", + "outDir": "dist", + "rootDir": "src", + "skipLibCheck": true, + "strict": true, + "target": "ES2022" + }, + "include": ["src/**/*.ts", "src/**/*.js"] +} diff --git a/web/walletkit/ubrn.config.yaml b/web/walletkit/ubrn.config.yaml new file mode 100644 index 000000000..5a1b5ec83 --- /dev/null +++ b/web/walletkit/ubrn.config.yaml @@ -0,0 +1,9 @@ +rust: + directory: ../../crates/walletkit + manifestPath: Cargo.toml + +wasm2: + defaultFeatures: false + features: + - embed-zkeys + ts: src/generated