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 {