@smbcloud / cli / commits / 4ca94a1

Remove OpenSSL dependency and add wasm-friendly account wasm exports

- Drop openssl from smbcloud-networking-account and use sha2 for hashing - Add uuid "js" feature for wasm compatibility - Implement and export signup_with_client, login_with_client, logout_with_client, me_with_client, and remove_with_client in smbcloud-networking-account-wasm - Update README with wasm usage and build instructions - Make check_internet_connection a no-op on wasm targets - Clean up Cargo.lock to remove openssl-src and related entries

Seto Elkahfi committed Mar 28, 2026 at 17:22 UTC 4ca94a1f21b5dfe20a5e03cef42d24fe218aa34f
12 files changed +303 -33
.agents/skills/rust-cross-platform/SKILL.md new
+150
@@ -0,0 +1,150 @@
1 +---
2 +name: rust-cross-platform
3 +description: Use when building or debugging Rust code that targets multiple platforms or language surfaces, especially Tauri, wasm/npm, UniFFI, Swift/Kotlin bindings, shared SDK crates, deployment-target issues, and environment parity across desktop, mobile, CLI, and web.
4 +---
5 +
6 +# Rust Cross Platform
7 +
8 +Use this skill when a change spans Rust plus any of:
9 +
10 +- Tauri desktop or mobile
11 +- wasm or npm packaging
12 +- UniFFI-generated Swift/Kotlin bindings
13 +- shared SDK crates used by web, native, and CLI clients
14 +- Apple platform linker or deployment-target issues
15 +- environment-specific credentials or runtime configuration
16 +
17 +## Default approach
18 +
19 +Treat the Rust crate as the source of truth and keep platform adapters thin.
20 +
21 +Preferred layering:
22 +
23 +1. Shared Rust domain crate
24 +2. Platform wrapper crate or binding layer
25 +3. App-specific UI or shell code
26 +
27 +Do not duplicate API contracts in TypeScript, Swift, Kotlin, and Rust if the Rust crate can own the contract.
28 +
29 +## Environment parity
30 +
31 +Always make environment selection explicit.
32 +
33 +Use:
34 +
35 +- an enum in Rust for `development` vs `production`
36 +- one place for app ids, hosts, and secrets
37 +- thin adapters in Tauri, wasm, or UniFFI to pass the enum through
38 +
39 +Do not let frontend-only checks become the source of truth for runtime mode if Rust owns the actual behavior.
40 +
41 +## Tauri conventions
42 +
43 +For auth or networking features:
44 +
45 +- keep logic in `src-tauri/src/...`
46 +- expose narrow Tauri commands
47 +- store session state in plugin-store, not only React state
48 +- use Rust for build-mode checks such as `cfg!(debug_assertions)`
49 +
50 +### macOS
51 +
52 +`tauri.conf.json` `minimumSystemVersion` does not fix `tauri dev`.
53 +
54 +If Swift-linked plugins are present, add `src-tauri/.cargo/config.toml` with:
55 +
56 +- `MACOSX_DEPLOYMENT_TARGET`
57 +- `-mmacosx-version-min=...` for `aarch64-apple-darwin`
58 +- `-mmacosx-version-min=...` for `x86_64-apple-darwin`
59 +- host `CC` and `CXX` if native deps might pick the wrong toolchain
60 +
61 +If runtime fails on `libswift_Concurrency.dylib`, fix Cargo/macOS config before touching app code.
62 +
63 +## wasm and npm conventions
64 +
65 +For browser-facing Rust SDKs:
66 +
67 +- keep the core logic in a normal Rust crate
68 +- expose wasm bindings in a dedicated wrapper crate
69 +- make wasm functions explicit and app-oriented
70 +- prefer passing primitive strings and enums over complex borrowed structs
71 +
72 +Typical exported operations:
73 +
74 +- `signup_with_client`
75 +- `login_with_client`
76 +- `me_with_client`
77 +- `remove_with_client`
78 +
79 +### Packaging
80 +
81 +Use `wasm-pack` for npm distribution unless there is a strong reason not to.
82 +
83 +Recommended pattern:
84 +
85 +- Rust crate under `crates/...-wasm`
86 +- `wasm-bindgen` exports only the browser surface
87 +- README contains exact browser import example
88 +
89 +Do not make the HTML or JS app reimplement request signing or endpoint construction if the Rust crate already owns it.
90 +
91 +## UniFFI conventions
92 +
93 +Use UniFFI when Swift/Kotlin consumers need a stable SDK surface from Rust.
94 +
95 +Guidelines:
96 +
97 +- keep FFI-safe types small and explicit
98 +- avoid leaking internal crate structure into the public interface
99 +- prefer stringly transport only at the boundary, not internally
100 +- map platform-facing errors into a deliberate error enum
101 +- keep async boundaries clear; document whether the caller must dispatch off the main thread
102 +
103 +When designing UniFFI APIs:
104 +
105 +- expose nouns and use cases, not transport details
106 +- keep constructors simple
107 +- prefer one shared config object if several methods need the same environment or credentials
108 +
109 +## Shared auth and SDK work
110 +
111 +For auth-backed SDKs:
112 +
113 +- keep account flows in the shared crate
114 +- keep app credentials and environment resolution in one place
115 +- make desktop, mobile, and web call the same Rust path where possible
116 +
117 +If a web client must use wasm, bind the existing Rust account crate instead of rebuilding the flow in JavaScript.
118 +
119 +## Validation
120 +
121 +Use the smallest relevant checks.
122 +
123 +### Rust
124 +
125 +- `cargo check -p <crate>`
126 +- `cargo test -p <crate>` when tests exist and are relevant
127 +
128 +### Tauri
129 +
130 +- `cargo check` in `src-tauri`
131 +- frontend build, usually `pnpm build`
132 +
133 +### wasm
134 +
135 +- `cargo check -p <wasm-crate>`
136 +- `wasm-pack build --target web` when packaging behavior changed
137 +
138 +### UniFFI
139 +
140 +- regenerate bindings if applicable
141 +- build at least one native target that consumes the bindings
142 +
143 +## Common mistakes
144 +
145 +- duplicating business logic in each platform adapter
146 +- hiding environment selection in UI-only code
147 +- using app secrets in public web clients without noting the security tradeoff
148 +- assuming `tauri.conf.json` fixes macOS dev runtime issues
149 +- exposing unstable internal Rust types directly through wasm or UniFFI
150 +- changing backend error shapes without updating parser code in wrappers
Cargo.lock
+1 -11
@@ -1355,15 +1355,6 @@ version = "0.2.1"
1355 source = "registry+https://github.com/rust-lang/crates.io-index"
1356 checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe"
1357
1358 -[[package]]
1359 -name = "openssl-src"
1360 -version = "300.5.5+3.5.5"
1361 -source = "registry+https://github.com/rust-lang/crates.io-index"
1362 -checksum = "3f1787d533e03597a7934fd0a765f0d28e94ecc5fb7789f8053b1e699a56f709"
1363 -dependencies = [
1364 - "cc",
1365 -]
1366 -
1358 [[package]]
1359 name = "openssl-sys"
1360 version = "0.9.111"
@@ -1372,7 +1363,6 @@ checksum = "82cab2d520aa75e3c58898289429321eb788c3106963d0dc886ec7a5f4adc321"
1363 dependencies = [
1364 "cc",
1365 "libc",
1375 - "openssl-src",
1366 "pkg-config",
1367 "vcpkg",
1368 ]
@@ -2171,10 +2161,10 @@ version = "0.3.30"
2161 dependencies = [
2162 "base64",
2163 "log",
2174 - "openssl",
2164 "reqwest",
2165 "serde",
2166 "serde_json",
2167 + "sha2",
2168 "smbcloud-model",
2169 "smbcloud-network",
2170 "smbcloud-networking",
crates/smbcloud-network/src/network.rs
+10 -3
@@ -15,10 +15,12 @@ const LOG_RESPONSE_BODY: bool = true; // You know what to do here.
15 #[cfg(not(debug_assertions))]
16 const LOG_RESPONSE_BODY: bool = false;
17
18 -/// Check if there is an active internet connection
18 +/// Check if there is an active internet connection.
19 ///
20 -/// This function attempts to connect to a reliable server (dns.google)
21 -/// with a short timeout. Returns true if the connection was successful.
20 +/// Native clients can afford a preflight connectivity check. Browser/wasm
21 +/// clients should skip it and rely on the real request, otherwise the check
22 +/// itself becomes a separate cross-origin failure surface.
23 +#[cfg(not(target_arch = "wasm32"))]
24 pub async fn check_internet_connection() -> bool {
25 debug!("Checking internet connection");
26 let client = reqwest::Client::builder()
@@ -49,6 +51,11 @@ pub async fn check_internet_connection() -> bool {
51 }
52 }
53
54 +#[cfg(target_arch = "wasm32")]
55 +pub async fn check_internet_connection() -> bool {
56 + true
57 +}
58 +
59 pub async fn parse_error_response<T: DeserializeOwned>(
60 response: Response,
61 ) -> Result<T, ErrorResponse> {
crates/smbcloud-networking-account-wasm/README.md
+41
@@ -1 +1,42 @@
1 # smbcloud-networking-account-wasm
2 +
3 +Wasm bindings for smbCloud account authentication.
4 +
5 +This crate is intended to be published as an npm package via `wasm-pack` and
6 +consumed by browser clients that need tenant auth-app signup, sign-in, profile
7 +lookup, and account deletion against the smbCloud Auth service.
8 +
9 +## Exports
10 +
11 +- `signup_with_client`
12 +- `login_with_client`
13 +- `logout_with_client`
14 +- `me_with_client`
15 +- `remove_with_client`
16 +- `Environment`
17 +
18 +## Build
19 +
20 +```bash
21 +wasm-pack build --target web --release
22 +```
23 +
24 +## Browser usage
25 +
26 +```js
27 +import init, {
28 + Environment,
29 + login_with_client,
30 + signup_with_client,
31 +} from "smbcloud-networking-account-wasm";
32 +
33 +await init();
34 +
35 +await signup_with_client(
36 + Environment.Production,
37 + "app-id",
38 + "app-secret",
39 + "name@example.com",
40 + "password123",
41 +);
42 +```
crates/smbcloud-networking-account-wasm/src/lib.rs
+3
@@ -1,2 +1,5 @@
1 mod login;
2 +mod logout;
3 mod me;
4 +mod remove;
5 +mod signup;
crates/smbcloud-networking-account-wasm/src/login.rs
+11 -11
@@ -1,22 +1,22 @@
1 use smbcloud_network::environment::Environment;
2 -use smbcloud_networking::smb_client::SmbClient;
2 +use smbcloud_networking_account::client_credentials::ClientCredentials;
3 use wasm_bindgen::JsValue;
4 use wasm_bindgen::prelude::wasm_bindgen;
5
6 #[wasm_bindgen]
7 -pub async fn login(
7 +pub async fn login_with_client(
8 env: Environment,
9 - username: String,
9 + app_id: String,
10 + app_secret: String,
11 + email: String,
12 password: String,
13 ) -> Result<JsValue, JsValue> {
12 - let secret = "wasm-secret";
13 - match smbcloud_networking_account::login::login(
14 - env,
15 - (&SmbClient::Cli, secret),
16 - username,
17 - password,
18 - )
19 - .await
14 + let client = ClientCredentials {
15 + app_id: &app_id,
16 + app_secret: &app_secret,
17 + };
18 +
19 + match smbcloud_networking_account::login::login_with_client(env, client, email, password).await
20 {
21 Ok(response) => Ok(serde_wasm_bindgen::to_value(&response)?),
22 Err(error) => Err(serde_wasm_bindgen::to_value(&error)?),
crates/smbcloud-networking-account-wasm/src/logout.rs new
+22
@@ -0,0 +1,22 @@
1 +use smbcloud_network::environment::Environment;
2 +use smbcloud_networking_account::client_credentials::ClientCredentials;
3 +use wasm_bindgen::JsValue;
4 +use wasm_bindgen::prelude::wasm_bindgen;
5 +
6 +#[wasm_bindgen]
7 +pub async fn logout_with_client(
8 + env: Environment,
9 + app_id: String,
10 + app_secret: String,
11 + access_token: String,
12 +) -> Result<JsValue, JsValue> {
13 + let client = ClientCredentials {
14 + app_id: &app_id,
15 + app_secret: &app_secret,
16 + };
17 +
18 + match smbcloud_networking_account::logout::logout_with_client(env, client, access_token).await {
19 + Ok(_) => Ok(JsValue::UNDEFINED),
20 + Err(error) => Err(serde_wasm_bindgen::to_value(&error)?),
21 + }
22 +}
crates/smbcloud-networking-account-wasm/src/me.rs
+13 -4
@@ -1,12 +1,21 @@
1 use smbcloud_network::environment::Environment;
2 -use smbcloud_networking::smb_client::SmbClient;
2 +use smbcloud_networking_account::client_credentials::ClientCredentials;
3 use wasm_bindgen::JsValue;
4 use wasm_bindgen::prelude::wasm_bindgen;
5
6 #[wasm_bindgen]
7 -pub async fn me(env: Environment, access_token: String) -> Result<JsValue, JsValue> {
8 - let secret = "wasm-secret";
9 - match smbcloud_networking_account::me::me(env, (&SmbClient::Cli, secret), &access_token).await {
7 +pub async fn me_with_client(
8 + env: Environment,
9 + app_id: String,
10 + app_secret: String,
11 + access_token: String,
12 +) -> Result<JsValue, JsValue> {
13 + let client = ClientCredentials {
14 + app_id: &app_id,
15 + app_secret: &app_secret,
16 + };
17 +
18 + match smbcloud_networking_account::me::me_with_client(env, client, &access_token).await {
19 Ok(response) => Ok(serde_wasm_bindgen::to_value(&response)?),
20 Err(error) => Err(serde_wasm_bindgen::to_value(&error)?),
21 }
crates/smbcloud-networking-account-wasm/src/remove.rs new
+23
@@ -0,0 +1,23 @@
1 +use smbcloud_network::environment::Environment;
2 +use smbcloud_networking_account::client_credentials::ClientCredentials;
3 +use wasm_bindgen::JsValue;
4 +use wasm_bindgen::prelude::wasm_bindgen;
5 +
6 +#[wasm_bindgen]
7 +pub async fn remove_with_client(
8 + env: Environment,
9 + app_id: String,
10 + app_secret: String,
11 + access_token: String,
12 +) -> Result<JsValue, JsValue> {
13 + let client = ClientCredentials {
14 + app_id: &app_id,
15 + app_secret: &app_secret,
16 + };
17 +
18 + match smbcloud_networking_account::remove::remove_with_client(env, client, &access_token).await
19 + {
20 + Ok(_) => Ok(JsValue::UNDEFINED),
21 + Err(error) => Err(serde_wasm_bindgen::to_value(&error)?),
22 + }
23 +}
crates/smbcloud-networking-account-wasm/src/signup.rs new
+25
@@ -0,0 +1,25 @@
1 +use smbcloud_network::environment::Environment;
2 +use smbcloud_networking_account::client_credentials::ClientCredentials;
3 +use wasm_bindgen::JsValue;
4 +use wasm_bindgen::prelude::wasm_bindgen;
5 +
6 +#[wasm_bindgen]
7 +pub async fn signup_with_client(
8 + env: Environment,
9 + app_id: String,
10 + app_secret: String,
11 + email: String,
12 + password: String,
13 +) -> Result<JsValue, JsValue> {
14 + let client = ClientCredentials {
15 + app_id: &app_id,
16 + app_secret: &app_secret,
17 + };
18 +
19 + match smbcloud_networking_account::signup::signup_with_client(env, client, email, password)
20 + .await
21 + {
22 + Ok(response) => Ok(serde_wasm_bindgen::to_value(&response)?),
23 + Err(error) => Err(serde_wasm_bindgen::to_value(&error)?),
24 + }
25 +}
crates/smbcloud-networking-account/Cargo.toml
+2 -2
@@ -20,12 +20,12 @@ readme = "README.md"
20 [dependencies]
21 base64 = "0.22"
22 log = { workspace = true }
23 -openssl = { version = "0.10", features = ["vendored"] }
23 reqwest = { workspace = true, features = ["json", "rustls-tls-native-roots"] }
24 serde = { workspace = true, features = ["derive"] }
25 serde_json = { workspace = true }
26 +sha2 = "0.10"
27 smbcloud-model = { workspace = true }
28 smbcloud-network = { workspace = true }
29 smbcloud-networking = { workspace = true }
30 url-builder = { workspace = true }
31 -uuid = { version = "1", features = ["v4"] }
31 +uuid = { version = "1", features = ["v4", "js"] }
crates/smbcloud-networking-account/src/oidc.rs
+2 -2
@@ -1,8 +1,8 @@
1 use {
2 base64::{engine::general_purpose::URL_SAFE_NO_PAD, Engine as _},
3 - openssl::sha::sha256,
3 reqwest::{Client, Url},
4 serde::{Deserialize, Serialize},
5 + sha2::{Digest, Sha256},
6 smbcloud_model::error_codes::{ErrorCode, ErrorResponse},
7 smbcloud_network::{environment::Environment, network},
8 uuid::Uuid,
@@ -55,7 +55,7 @@ pub fn build_authorization_request(
55 Uuid::new_v4().simple(),
56 Uuid::new_v4().simple()
57 );
58 - let code_challenge = URL_SAFE_NO_PAD.encode(sha256(code_verifier.as_bytes()));
58 + let code_challenge = URL_SAFE_NO_PAD.encode(Sha256::digest(code_verifier.as_bytes()));
59 let state = Uuid::new_v4().to_string();
60
61 let mut url = issuer_base_url(env).join(AUTHORIZE_PATH).map_err(|err| ErrorResponse::Error {