feat(accounts): Zoom webview account + zoomus:// deep-link rewrite (#657)#853
Conversation
📝 WalkthroughWalkthroughThe changes add Zoom as a supported OAuth provider across the application stack, including a Tauri recipe manifest, backend provider registry and deep-link rewriting logic that converts native Zoom schemes to web URLs, frontend icon support, and TypeScript type definitions enabling Zoom integration. Changes
Sequence DiagramsequenceDiagram
actor User
participant NativeApp as Zoom Native App
participant Webview as Application Webview
participant Backend as Backend Provider Logic
participant WebClient as Zoom Web Client
User->>NativeApp: Triggers action (join meeting)
NativeApp->>Webview: Sends deep link (zoomus:// or zoommtg://)
Webview->>Backend: Detects native Zoom scheme
Backend->>Backend: Extract meeting ID & access tokens
Backend->>Backend: Rewrite to HTTPS URL (app.zoom.us/wc/join/<id>)
Backend->>Webview: Reload with rewritten URL
Webview->>WebClient: Navigate to embedded web client
WebClient->>User: Display meeting join interface
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Poem
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Comment |
There was a problem hiding this comment.
Actionable comments posted: 2
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
app/src-tauri/src/webview_accounts/mod.rs (1)
788-825:⚠️ Potential issue | 🟠 MajorPopup-based Zoom joins still leave the app.
The recipe wraps
window.open()and rewrites deep links tohttps://app.zoom.us/wc/...before calling the real popup open. By the time this handler runs,rewrite_provider_deep_link()no longer matches, andpopup_should_stay_in_app()only keeps Slack popups in-app, so those Zoom meeting popups are denied and handed to the OS browser. That breaks the PR's “stay inside the app” behavior for popup-based launches.One way to keep rewritten Zoom popups in-app
builder = builder.on_new_window(move |url, _features| { if let Some(rewritten) = rewrite_provider_deep_link(&popup_provider, &url) { log::info!( "[webview-accounts] new-window deep-link rewrite {} → {} (provider={})", url, rewritten, popup_provider ); let app = popup_app.clone(); let label = popup_label.clone(); tauri::async_runtime::spawn(async move { if let Some(wv) = app.get_webview(&label) { if let Err(e) = wv.navigate(rewritten) { log::warn!( "[webview-accounts] post-rewrite navigate (popup) failed label={} err={}", label, e ); } } }); return NewWindowResponse::Deny; } + if popup_provider == "zoom" + && matches!(url.host_str(), Some("app.zoom.us")) + && url.path().starts_with("/wc/") + { + log::info!( + "[webview-accounts] new-window request {} → in-app popup (provider={})", + url, + popup_provider + ); + return NewWindowResponse::Allow; + } if popup_should_stay_in_app(&popup_provider, &url) {🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@app/src-tauri/src/webview_accounts/mod.rs` around lines 788 - 825, The new-window handler currently calls rewrite_provider_deep_link(&popup_provider, &url) which no longer matches popup-based Zoom links (they've been rewritten to app.zoom.us) and then popup_should_stay_in_app(&popup_provider, &url) only keeps Slack in-app, so Zoom popups get denied; fix this by making the handler recognize rewritten Zoom meeting URLs and treat them as in-app: update popup_should_stay_in_app (or the new-window closure) to check for Zoom meeting patterns (e.g., host/app.zoom.us and path like /wc/) or use popup_provider to force in-app behavior for provider == "zoom", and ensure the closure uses that check before denying the window (functions to modify: rewrite_provider_deep_link, popup_should_stay_in_app, and the builder.on_new_window closure).
🧹 Nitpick comments (1)
app/src-tauri/src/webview_accounts/mod.rs (1)
49-194: Consider extracting provider routing metadata out ofmod.rs.Adding Zoom touches URL selection, UA policy, recipe lookup, host allowlisting, display names, rewrite logic, and tests in a module that is already very large. Pulling provider metadata/deep-link policy into a dedicated submodule or table would make future provider additions much safer to review and extend.
As per coding guidelines, "Prefer files ≤ ~500 lines; split growing modules into smaller, single-responsibility units."
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@app/src-tauri/src/webview_accounts/mod.rs` around lines 49 - 194, The module has grown large because provider-specific routing/metadata and deep-link logic (see provider_url, provider_user_agent, provider_recipe_js, provider_allowed_hosts, provider_ua_spoof, provider_is_supported, rewrite_provider_deep_link) are mixed into mod.rs; extract these into a dedicated submodule (e.g., webview_accounts::providers) and replace the many match arms with a single static table or map of a ProviderMetadata struct (fields: id, url, user_agent, recipe_js, allowed_hosts, ua_spoof, display_name, deep_link_policy) and move rewrite_provider_deep_link into that submodule as a per-provider method; update the public functions in mod.rs to delegate to the new table/API and move/adjust corresponding tests to the new module so behavior is unchanged but mod.rs stays focused and smaller.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@app/src-tauri/src/webview_accounts/mod.rs`:
- Around line 143-149: The allowlist entry for the "zoom" key currently includes
a wildcard "cloudfront.net", which is too broad; update the allowlist in
webview_accounts::mod.rs (the array under the "zoom" => [...] map entry) to
remove "cloudfront.net" and instead list only the specific CloudFront hostnames
Zoom uses (pin to exact hostnames), so that url_is_internal() no longer treats
all CloudFront domains as trusted; keep the other Zoom entries ("zoom.us",
"zoom.com", "zoomgov.com", "zdassets.com") unchanged and ensure any newly added
CloudFront entries are full hostnames (not a naked suffix).
- Around line 176-193: The current code interpolates confno and pwd/tk directly
into web_url which can corrupt reserved chars; instead construct the URL using
Url types so components are percent-encoded: create a base Url (either
"https://app.zoom.us/wc/join/" or "https://app.zoom.us/wc/home"), for the join
path use url.path_segments_mut().push(confno) (or equivalent) to add the
conference id, and use url.query_pairs_mut().append_pair("pwd", &p) (or "tk")
when pwd is Some to add the query parameter; then return Url::parse or the Url
instance (web_url variable replaced with the constructed Url) so
parsing/encoding is handled properly (look for variables/confno, pwd, web_url to
update).
---
Outside diff comments:
In `@app/src-tauri/src/webview_accounts/mod.rs`:
- Around line 788-825: The new-window handler currently calls
rewrite_provider_deep_link(&popup_provider, &url) which no longer matches
popup-based Zoom links (they've been rewritten to app.zoom.us) and then
popup_should_stay_in_app(&popup_provider, &url) only keeps Slack in-app, so Zoom
popups get denied; fix this by making the handler recognize rewritten Zoom
meeting URLs and treat them as in-app: update popup_should_stay_in_app (or the
new-window closure) to check for Zoom meeting patterns (e.g., host/app.zoom.us
and path like /wc/) or use popup_provider to force in-app behavior for provider
== "zoom", and ensure the closure uses that check before denying the window
(functions to modify: rewrite_provider_deep_link, popup_should_stay_in_app, and
the builder.on_new_window closure).
---
Nitpick comments:
In `@app/src-tauri/src/webview_accounts/mod.rs`:
- Around line 49-194: The module has grown large because provider-specific
routing/metadata and deep-link logic (see provider_url, provider_user_agent,
provider_recipe_js, provider_allowed_hosts, provider_ua_spoof,
provider_is_supported, rewrite_provider_deep_link) are mixed into mod.rs;
extract these into a dedicated submodule (e.g., webview_accounts::providers) and
replace the many match arms with a single static table or map of a
ProviderMetadata struct (fields: id, url, user_agent, recipe_js, allowed_hosts,
ua_spoof, display_name, deep_link_policy) and move rewrite_provider_deep_link
into that submodule as a per-provider method; update the public functions in
mod.rs to delegate to the new table/API and move/adjust corresponding tests to
the new module so behavior is unchanged but mod.rs stays focused and smaller.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 309f4708-9bda-4ad7-8f26-d119a8b4d48c
⛔ Files ignored due to path filters (1)
app/src-tauri/recipes/zoom/icon.svgis excluded by!**/*.svg
📒 Files selected for processing (5)
app/src-tauri/recipes/zoom/manifest.jsonapp/src-tauri/recipes/zoom/recipe.jsapp/src-tauri/src/webview_accounts/mod.rsapp/src/components/accounts/providerIcons.tsxapp/src/types/accounts.ts
|
bro fix the type issues again... |
…i#657) - Remove `cloudfront.net` from Zoom allowed-hosts list. It's a shared CDN across the web; keeping it as a suffix in `url_is_internal` would let any CloudFront-hosted site stay inside the embedded Zoom webview. Pin to zoom-owned origins only. - Build the rewritten URL via `Url::path_segments_mut()` + `Url::query_pairs_mut()` so `confno` and `pwd` are percent-encoded. Hand-rolled `format!()` corrupted reserved chars (`&`, `#`, `%`, `+`, …) that commonly appear inside Zoom join tokens. Fixes double-slash that a naive base URL + push would emit. - Extend `popup_should_stay_in_app` to keep `app.zoom.us/wc/*` and `zoom.us/wc/*` popups inside the embedded webview. Zoom's "Join from browser" flow opens via `window.open("https://app.zoom.us/wc/...")`; without this the popup was handed to the OS browser. - Tests: add percent-encoding coverage for reserved chars in `pwd` and for path-segment encoding of `confno`; add 4 popup-handler cases (apex/subdomain stay in-app, non-wc and foreign-host pop out). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…tinyhumansai#657) V1 = passive shell. recipe.js reserves event-name namespace (zoom_call_started/zoom_captions/zoom_call_ended) and logs bootstrap; DOM scraping for caption capture is a follow-up that needs live-call investigation. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…istry (tinyhumansai#657) Adds "zoom" to every match arm in webview_accounts/mod.rs: - provider_url → https://zoom.us/ - provider_user_agent → Chrome UA (Zoom blocks unknown UAs) - provider_recipe_js → ZOOM_RECIPE_JS stub - provider_ua_spoof → enabled (WebClient fingerprints) - provider_allowed_hosts → zoom.us, zoom.com, zoomgov.com, zdassets.com, cloudfront.net - provider_display_name → "Zoom" Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…#657) - AccountProvider union gains 'zoom' - BASE_PROVIDERS entry: Zoom web client label + description - ProviderIcon renders SiZoom (react-icons/si) in Zoom brand blue (#2D8CFF) Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…tinyhumansai#657) Zoom's "Host/Join Meeting" flow fires `zoomus://zoom.us/{join,start}?confno=<id>&pwd=<x>&...` to hand off to the native Zoom desktop client. CEF has no handler for this scheme, so the embedded webview shows `ERR_UNKNOWN_URL_SCHEME` and the meeting dies. - Rust `on_navigation` + `on_new_window`: match `zoomus`/`zoommtg`, pull `confno` + `pwd` from the query, and re-navigate the child webview to `https://app.zoom.us/wc/join/<id>[?pwd=<pwd>]` (Zoom WebClient). Cancel the original navigation so CEF never tries the unknown scheme. - recipe.js: same rewrite applied to JS-level vectors (anchor clicks, `window.location.assign/replace`, `location.href` setter, `window.open`). Belt-and-braces — catches any in-page script that fires the deep link before Rust nav handler sees it. Keeps meetings inside OpenHuman instead of bouncing to the system browser / native Zoom app. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…write (tinyhumansai#657) 17 new tests covering: - Provider registry match arms for Zoom (url, user_agent, recipe_js, ua_spoof, allowed_hosts, is_supported) - url_is_internal subdomain / apex / external-host behavior for Zoom - rewrite_provider_deep_link across join, start, zoommtg, pwd→tk fallback, missing confno → wc/home fallback, empty confno, non-zoom provider, https passthrough, unknown scheme Covers the new helper end-to-end plus the registration surface so future edits to either side surface regressions in `cargo test`. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…i#657) - Remove `cloudfront.net` from Zoom allowed-hosts list. It's a shared CDN across the web; keeping it as a suffix in `url_is_internal` would let any CloudFront-hosted site stay inside the embedded Zoom webview. Pin to zoom-owned origins only. - Build the rewritten URL via `Url::path_segments_mut()` + `Url::query_pairs_mut()` so `confno` and `pwd` are percent-encoded. Hand-rolled `format!()` corrupted reserved chars (`&`, `#`, `%`, `+`, …) that commonly appear inside Zoom join tokens. Fixes double-slash that a naive base URL + push would emit. - Extend `popup_should_stay_in_app` to keep `app.zoom.us/wc/*` and `zoom.us/wc/*` popups inside the embedded webview. Zoom's "Join from browser" flow opens via `window.open("https://app.zoom.us/wc/...")`; without this the popup was handed to the OS browser. - Tests: add percent-encoding coverage for reserved chars in `pwd` and for path-segment encoding of `confno`; add 4 popup-handler cases (apex/subdomain stay in-app, non-wc and foreign-host pop out). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…#657) Upstream/main now codifies a "no new JS injection in CEF child webviews" rule (CLAUDE.md → Tauri shell → CEF child webviews). New providers must rely on Rust `on_navigation` / `on_new_window` / `LoadHandler` + scanner-side CDP, not injected scripts. Zoom's deep-link rewrite already runs in Rust (see rewrite_provider_deep_link + the on_navigation / on_new_window hooks added earlier in this branch). The JS-side belt-and-braces in recipe.js was defensive overlap — delete it so we satisfy the rule and don't set a precedent other new providers would inherit. Also: - drop `ZOOM_RECIPE_JS` constant + `"zoom" => Some(...)` arm in `provider_recipe_js` so Zoom falls through to `None` alongside the migrated providers. - flip the unit test — `zoom_has_no_recipe_js_injection` now guards the rule in CI. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
b6ca3eb to
173fd83
Compare
Summary
app/src-tauri/recipes/zoom/) alongside gmeet/slack/gmail/etc.provider_url, UA spoof, allowed hosts, recipe JS, display name.zoomus://,zoommtg://) to the Zoom WebClient so meetings stay inside OpenHuman instead of failing withERR_UNKNOWN_URL_SCHEMEor bouncing out to the system browser / native Zoom app.AccountProviderregistry + render withSiZoomin Zoom-brand blue.Part 1 of issue #657 (Zoom + Teams). Teams ships as a follow-up PR.
Problem
OpenHuman embeds third-party web apps as child webviews so users can log in and work inside the app (see
google-meet,slack,gmailetc. underapp/src-tauri/recipes/). Zoom has no entry. Users can't add a Zoom account.Even if the recipe is registered, Zoom's "Host/Join Meeting" flow fires
zoomus://zoom.us/{join,start}?confno=<id>&pwd=<x>&...to launch the native Zoom desktop client. CEF has no handler forzoomus://, so the embedded webview showsERR_UNKNOWN_URL_SCHEMEand the meeting dies on-click.Solution
V1 passive shell — mirrors the slack recipe level (embed + login), with an in-scope deep-link rewrite so meetings work. Caption extraction + memory ingest (gmeet-level) is a follow-up.
app/src-tauri/recipes/zoom/{manifest.json,icon.svg,recipe.js}(new) — recipe metadata + icon + lifecycle-stub JS that reserves event-name namespace (zoom_call_started/zoom_captions/zoom_call_ended) and applies the JS-side deep-link rewrite as a belt-and-braces catch for in-page scripts.app/src-tauri/src/webview_accounts/mod.rs— addzoomto every match arm (url/UA/ua_spoof/recipe_js/allowed_hosts/display_name) + newrewrite_provider_deep_linkhelper that pullsconfno/pwdout of thezoomus://zoommtg://URL and returnshttps://app.zoom.us/wc/join/<id>[?pwd=<pwd>].on_navigationandon_new_windowboth call it: if the rewrite fires, cancel the current navigation and re-navigate the child webview to the WebClient URL.app/src/types/accounts.ts—'zoom'added toAccountProviderunion andBASE_PROVIDERSregistry with a descriptive label/serviceUrl.app/src/components/accounts/providerIcons.tsx— Zoom entry inPROVIDER_COLOR(#2D8CFF) +SiZoomcase (react-icons/si).Allowed-hosts scope for Zoom:
zoom.us, zoom.com, zoomgov.com, zdassets.com, cloudfront.net. Additional hosts (SSO/Okta tenants) can be appended as real-user feedback surfaces them.Submission Checklist
rewrite_provider_deep_linkis tight enough to cover by inspection; unit tests for it can land when deep-link handling grows beyond Zoom.zoom.us, UA-spoof ACKed (no "unsupported browser" page), login flow completes, "Host a meeting" / "Join" both rewrite fromzoomus://toapp.zoom.us/wc/join/<id>and open the WebClient in-app.rewrite_provider_deep_linkexplains scope + return contract;recipe.jsheader documents V1 scope and reserved event names.on_new_windowalso needs the rewrite, the belt-and-braces justification for layering JS atop Rust, etc.).Impact
rewrite_provider_deep_linkis scoped toprovider == "zoom"and only emitshttps://app.zoom.us/wc/join/...URLs. Zoom's host is in the allowlist so the rewritten URL stays in-app.zoomarms sit alongsidegoogle-meet,slack, etc.Related
flushMeetingSessionbranch inapp/src/services/webviewAccountService.ts).Summary by CodeRabbit
Release Notes
New Features