-
Notifications
You must be signed in to change notification settings - Fork 3.8k
fix(mobile): add missing tauri CLI shim for Android Gradle rust-build #5813
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
dce1e45
348c0fc
62d770e
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,8 @@ | ||
| #!/usr/bin/env node | ||
| // Shim so Gradle's Rust-build task (buildSrc/BuildTask.kt: `node tauri android | ||
| // android-studio-script`) can find the tauri CLI. tauri-cli's own `android init` | ||
| // is supposed to leave this in place, but under pnpm's shell-wrapper .bin shims | ||
| // (rather than npm's direct symlink-to-JS), that step doesn't produce it. This | ||
| // just forwards into the real CLI entry resolved the normal node_modules way. | ||
| // ESM because this directory's package.json sets "type": "module". | ||
| await import("@tauri-apps/cli/tauri.js"); |
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -8,6 +8,7 @@ | |||||||||||||||||||||||
| //! | ||||||||||||||||||||||||
| //! Frame cap: 64 KB. Rate limit: callers are expected to stay ≤ 100 frames/s. | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| use chrono::{SecondsFormat, TimeZone, Utc}; | ||||||||||||||||||||||||
| use serde::{Deserialize, Serialize}; | ||||||||||||||||||||||||
| use serde_json::json; | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
|
|
@@ -30,10 +31,39 @@ pub struct TunnelRegisterResponse { | |||||||||||||||||||||||
| pub channel_id: String, | ||||||||||||||||||||||||
| #[serde(rename = "pairingToken")] | ||||||||||||||||||||||||
| pub pairing_token: String, | ||||||||||||||||||||||||
| #[serde(rename = "pairingExpiresAt")] | ||||||||||||||||||||||||
| /// Backend has been observed sending this as either an ISO 8601 string | ||||||||||||||||||||||||
| /// or an epoch-millisecond integer — normalize both to an ISO 8601 | ||||||||||||||||||||||||
| /// string so every downstream consumer (QR `exp` field, frontend TTL | ||||||||||||||||||||||||
| /// checks) keeps seeing the contract's documented shape. | ||||||||||||||||||||||||
| #[serde(rename = "pairingExpiresAt", deserialize_with = "deserialize_expires_at")] | ||||||||||||||||||||||||
| pub pairing_expires_at: String, | ||||||||||||||||||||||||
| } | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| fn deserialize_expires_at<'de, D>(deserializer: D) -> Result<String, D::Error> | ||||||||||||||||||||||||
| where | ||||||||||||||||||||||||
| D: serde::Deserializer<'de>, | ||||||||||||||||||||||||
| { | ||||||||||||||||||||||||
| #[derive(Deserialize)] | ||||||||||||||||||||||||
| #[serde(untagged)] | ||||||||||||||||||||||||
| enum StringOrEpochMs { | ||||||||||||||||||||||||
| Str(String), | ||||||||||||||||||||||||
| EpochMs(i64), | ||||||||||||||||||||||||
| } | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| match StringOrEpochMs::deserialize(deserializer)? { | ||||||||||||||||||||||||
| StringOrEpochMs::Str(s) => Ok(s), | ||||||||||||||||||||||||
| StringOrEpochMs::EpochMs(ms) => Utc | ||||||||||||||||||||||||
| .timestamp_millis_opt(ms) | ||||||||||||||||||||||||
| .single() | ||||||||||||||||||||||||
| .map(|dt| dt.to_rfc3339_opts(SecondsFormat::Millis, true)) | ||||||||||||||||||||||||
| .ok_or_else(|| { | ||||||||||||||||||||||||
| serde::de::Error::custom(format!( | ||||||||||||||||||||||||
| "pairingExpiresAt: epoch-ms {ms} is out of range" | ||||||||||||||||||||||||
| )) | ||||||||||||||||||||||||
| }), | ||||||||||||||||||||||||
| } | ||||||||||||||||||||||||
| } | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| /// Payload emitted as `tunnel:connect` to join a channel. | ||||||||||||||||||||||||
| #[derive(Debug, Serialize)] | ||||||||||||||||||||||||
| pub struct TunnelConnectPayload { | ||||||||||||||||||||||||
|
|
@@ -87,10 +117,49 @@ pub async fn emit_register() -> Result<TunnelRegisterResponse, String> { | |||||||||||||||||||||||
| .await | ||||||||||||||||||||||||
| .map_err(|e| format!("[devices/tunnel] emit tunnel:register failed: {e}"))?; | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| // Logged at warn (not debug) so it shows up under the default RUST_LOG=info | ||||||||||||||||||||||||
| // — this backend's ack shape has been observed changing between attempts | ||||||||||||||||||||||||
| // (wrong-typed pairingExpiresAt, then a missing channelId entirely), so | ||||||||||||||||||||||||
| // seeing the exact raw payload is the fastest way to tell "flaky backend" | ||||||||||||||||||||||||
| // from "our struct is wrong." | ||||||||||||||||||||||||
| log::warn!("[devices/tunnel] raw tunnel:register ack: {ack}"); | ||||||||||||||||||||||||
|
Comment on lines
+120
to
+125
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🔒 Security & Privacy | 🟠 Major | ⚡ Quick win Remove the raw registration ACK log. Line 125 logs the full ACK at Log only non-sensitive metadata after successful parsing, such as Proposed fix- log::warn!("[devices/tunnel] raw tunnel:register ack: {ack}");As per coding guidelines: “Never log secrets or full PII.” 📝 Committable suggestion
Suggested change
🤖 Prompt for AI AgentsSource: Coding guidelines |
||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| // The backend acks a rejected registration with an error object shaped | ||||||||||||||||||||||||
| // like `{"error": "<code>", "ok": false}` rather than an HTTP-level | ||||||||||||||||||||||||
| // error — e.g. `tunnel_limit_reached` when too many pending/unreleased | ||||||||||||||||||||||||
| // channels are already open for this account (there is no backend | ||||||||||||||||||||||||
| // "release early" endpoint yet; pending channels only clear via their | ||||||||||||||||||||||||
| // ~10 minute TTL, see devices/README.md). Detecting this shape first | ||||||||||||||||||||||||
| // turns a confusing "missing field `channelId`" parse failure into the | ||||||||||||||||||||||||
| // real reason. | ||||||||||||||||||||||||
| if let Some(err) = register_ack_error(&ack) { | ||||||||||||||||||||||||
| return Err(err); | ||||||||||||||||||||||||
| } | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| serde_json::from_value::<TunnelRegisterResponse>(ack) | ||||||||||||||||||||||||
| .map_err(|e| format!("[devices/tunnel] parse tunnel:register ack failed: {e}")) | ||||||||||||||||||||||||
| } | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| /// Recognizes the backend's `{"error": "<code>", "ok": false}` rejection | ||||||||||||||||||||||||
| /// shape for `tunnel:register` and turns it into a clear message. Returns | ||||||||||||||||||||||||
| /// `None` for anything else (including a genuine success payload), leaving | ||||||||||||||||||||||||
| /// that to the normal `TunnelRegisterResponse` parse. | ||||||||||||||||||||||||
| fn register_ack_error(ack: &serde_json::Value) -> Option<String> { | ||||||||||||||||||||||||
| if ack.get("ok").and_then(|v| v.as_bool()) != Some(false) { | ||||||||||||||||||||||||
| return None; | ||||||||||||||||||||||||
| } | ||||||||||||||||||||||||
| let code = ack.get("error").and_then(|v| v.as_str()).unwrap_or("unknown_error"); | ||||||||||||||||||||||||
| Some(match code { | ||||||||||||||||||||||||
| "tunnel_limit_reached" => { | ||||||||||||||||||||||||
| "[devices/tunnel] tunnel:register rejected: too many pending device pairings \ | ||||||||||||||||||||||||
| are already open for this account. Wait a few minutes for old ones to expire \ | ||||||||||||||||||||||||
| (~10 min TTL) and try again." | ||||||||||||||||||||||||
| .to_string() | ||||||||||||||||||||||||
| } | ||||||||||||||||||||||||
| other => format!("[devices/tunnel] tunnel:register rejected: {other}"), | ||||||||||||||||||||||||
| }) | ||||||||||||||||||||||||
| } | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| /// Emit `tunnel:connect` to start listening on a channel as `role:"core"`. | ||||||||||||||||||||||||
| pub async fn emit_connect(channel_id: &str) -> Result<(), String> { | ||||||||||||||||||||||||
| log::debug!("[devices/tunnel] emit_connect channel_id={channel_id}"); | ||||||||||||||||||||||||
|
|
@@ -153,6 +222,45 @@ mod tests { | |||||||||||||||||||||||
| assert_eq!(response.pairing_expires_at, "2026-06-30T15:00:00Z"); | ||||||||||||||||||||||||
| } | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| #[test] | ||||||||||||||||||||||||
| fn register_ack_error_recognizes_tunnel_limit_reached() { | ||||||||||||||||||||||||
| let ack = json!({"error": "tunnel_limit_reached", "ok": false}); | ||||||||||||||||||||||||
| let err = register_ack_error(&ack).expect("should recognize the error shape"); | ||||||||||||||||||||||||
| assert!(err.contains("too many pending device pairings"), "got: {err}"); | ||||||||||||||||||||||||
| } | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| #[test] | ||||||||||||||||||||||||
| fn register_ack_error_passes_through_unknown_error_codes() { | ||||||||||||||||||||||||
| let ack = json!({"error": "something_else", "ok": false}); | ||||||||||||||||||||||||
| let err = register_ack_error(&ack).expect("should recognize any ok:false shape"); | ||||||||||||||||||||||||
| assert!(err.contains("something_else"), "got: {err}"); | ||||||||||||||||||||||||
| } | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| #[test] | ||||||||||||||||||||||||
| fn register_ack_error_ignores_success_shapes() { | ||||||||||||||||||||||||
| let ack = json!({ | ||||||||||||||||||||||||
| "channelId": "ch_123", | ||||||||||||||||||||||||
| "pairingToken": "pt_123", | ||||||||||||||||||||||||
| "pairingExpiresAt": "2026-06-30T15:00:00Z" | ||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||
| assert_eq!(register_ack_error(&ack), None); | ||||||||||||||||||||||||
| } | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| #[test] | ||||||||||||||||||||||||
| fn tunnel_register_response_accepts_epoch_ms_pairing_expires_at() { | ||||||||||||||||||||||||
| // Observed live from api.tinyhumans.ai: pairingExpiresAt sent as an | ||||||||||||||||||||||||
| // integer epoch-ms timestamp rather than the documented ISO 8601 | ||||||||||||||||||||||||
| // string. Must normalize to a string, not fail to parse. | ||||||||||||||||||||||||
| let response: TunnelRegisterResponse = serde_json::from_value(json!({ | ||||||||||||||||||||||||
| "channelId": "ch_123", | ||||||||||||||||||||||||
| "pairingToken": "pt_123", | ||||||||||||||||||||||||
| "pairingExpiresAt": 1787784497036i64 | ||||||||||||||||||||||||
| })) | ||||||||||||||||||||||||
| .expect("epoch-ms pairingExpiresAt should parse"); | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| assert_eq!(response.pairing_expires_at, "2026-08-26T22:48:17.036Z"); | ||||||||||||||||||||||||
| } | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| #[test] | ||||||||||||||||||||||||
| fn build_core_connect_payload_omits_session_token_for_core_role() { | ||||||||||||||||||||||||
| let payload = build_core_connect_payload("ch_123"); | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
🔎 Supported by static analysis
🏁 Script executed:
Repository: tinyhumansai/openhuman
Length of output: 28474
🏁 Script executed:
Repository: tinyhumansai/openhuman
Length of output: 18764
Guard the boot-check effect on mobile.
getIsMobile()bypasses only rendering. The effect still callsrunCheck(coreMode)for a non-unsetmode in thecheckingphase, andrunCheckinvokesrunBootCheck. Add a mobile guard to the effect and test a non-unsetmode to ensurerunBootCheckis not called.🤖 Prompt for AI Agents