Skip to content

feat(accounts): Zoom webview account + zoomus:// deep-link rewrite (#657)#853

Merged
graycyrus merged 7 commits intotinyhumansai:mainfrom
oxoxDev:feat/657-zoom-webview
Apr 24, 2026
Merged

feat(accounts): Zoom webview account + zoomus:// deep-link rewrite (#657)#853
graycyrus merged 7 commits intotinyhumansai:mainfrom
oxoxDev:feat/657-zoom-webview

Conversation

@oxoxDev
Copy link
Copy Markdown
Contributor

@oxoxDev oxoxDev commented Apr 23, 2026

Summary

  • Add Zoom as a webview-account recipe (app/src-tauri/recipes/zoom/) alongside gmeet/slack/gmail/etc.
  • Wire Zoom into the existing webview-account pipeline: provider_url, UA spoof, allowed hosts, recipe JS, display name.
  • Rewrite Zoom's native-app deep links (zoomus://, zoommtg://) to the Zoom WebClient so meetings stay inside OpenHuman instead of failing with ERR_UNKNOWN_URL_SCHEME or bouncing out to the system browser / native Zoom app.
  • Add Zoom to the frontend AccountProvider registry + render with SiZoom in 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, gmail etc. under app/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 for zoomus://, so the embedded webview shows ERR_UNKNOWN_URL_SCHEME and 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.

  1. 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.
  2. app/src-tauri/src/webview_accounts/mod.rs — add zoom to every match arm (url/UA/ua_spoof/recipe_js/allowed_hosts/display_name) + new rewrite_provider_deep_link helper that pulls confno/pwd out of the zoomus://zoommtg:// URL and returns https://app.zoom.us/wc/join/<id>[?pwd=<pwd>]. on_navigation and on_new_window both call it: if the rewrite fires, cancel the current navigation and re-navigate the child webview to the WebClient URL.
  3. app/src/types/accounts.ts'zoom' added to AccountProvider union and BASE_PROVIDERS registry with a descriptive label/serviceUrl.
  4. app/src/components/accounts/providerIcons.tsx — Zoom entry in PROVIDER_COLOR (#2D8CFF) + SiZoom case (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

  • Unit tests — N/A for recipe metadata + provider wiring (edits are additive match-arm data + pure helper). rewrite_provider_deep_link is tight enough to cover by inspection; unit tests for it can land when deep-link handling grows beyond Zoom.
  • E2E / integration — smoke-tested locally: embedded webview loads zoom.us, UA-spoof ACKed (no "unsupported browser" page), login flow completes, "Host a meeting" / "Join" both rewrite from zoomus:// to app.zoom.us/wc/join/<id> and open the WebClient in-app.
  • Doc commentsrewrite_provider_deep_link explains scope + return contract; recipe.js header documents V1 scope and reserved event names.
  • Inline comments — added where the flow isn't obvious from names (why on_new_window also needs the rewrite, the belt-and-braces justification for layering JS atop Rust, etc.).

Impact

  • Runtime: desktop only (openhuman is desktop-shipped). Zero mobile/web impact (no files under those paths touched).
  • Performance: neutral. Rewrite helper runs once per navigation event, string-op only.
  • Security: rewrite_provider_deep_link is scoped to provider == "zoom" and only emits https://app.zoom.us/wc/join/... URLs. Zoom's host is in the allowlist so the rewritten URL stays in-app.
  • Compatibility: additive — no existing provider affected. New zoom arms sit alongside google-meet, slack, etc.

Related

  • Issue: Closes P2: Add Zoom, We Chat and Microsoft Teams channel support #657
  • Follow-up PR: Microsoft Teams webview account (same pattern, different allowed-hosts list for MS auth bounces).
  • Follow-up issue (TBD): Zoom caption DOM capture + meeting-session memory ingest (parity with gmeet's flushMeetingSession branch in app/src/services/webviewAccountService.ts).

Summary by CodeRabbit

Release Notes

New Features

  • Added Zoom as a supported account provider
  • Zoom links and invitations now open seamlessly within the application

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Apr 23, 2026

📝 Walkthrough

Walkthrough

The 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

Cohort / File(s) Summary
Zoom Recipe Configuration
app/src-tauri/recipes/zoom/manifest.json
New Tauri recipe manifest defining Zoom provider metadata including identifier, display name, version, service URL, and icon reference.
Backend Provider Registry & Deep-Link Rewriting
app/src-tauri/src/webview_accounts/mod.rs
Adds Zoom to provider URL mappings, Chrome UA selection, and host allowlist. Implements rewrite_provider_deep_link helper converting native schemes (zoomus://, zoommtg://) to embedded web-client HTTPS URLs (app.zoom.us), extracting meeting IDs and preserving access tokens. Updates webview navigation to intercept and reload Zoom deep links asynchronously. Includes comprehensive test suite validating provider registry, URL rewrites, and popup classification.
Frontend Provider Icon Support
app/src/components/accounts/providerIcons.tsx
Imports SiZoom icon and adds Zoom entry to provider color mapping. Extends ProviderIcon component to render Zoom icon when provider matches 'zoom'.
Type Definitions & Provider Metadata
app/src/types/accounts.ts
Adds 'zoom' to AccountProvider union type and defines corresponding Zoom provider descriptor in BASE_PROVIDERS with identifier, label, and service URL.

Sequence Diagram

sequenceDiagram
    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
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Poem

🐰 Zoom, zoom, hop into the frame!
Deep links rewritten, native schemes tamed,
From app to web, a seamless leap—
Meeting IDs extracted, tokens kept safe and deep. 🎯✨

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately summarizes the main changes: adding Zoom as a webview account provider with deep-link rewriting for zoomus:// URLs.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Comment @coderabbitai help to get the list of available commands and usage tips.

@oxoxDev oxoxDev marked this pull request as ready for review April 23, 2026 20:52
@oxoxDev oxoxDev requested a review from a team April 23, 2026 20:52
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 | 🟠 Major

Popup-based Zoom joins still leave the app.

The recipe wraps window.open() and rewrites deep links to https://app.zoom.us/wc/... before calling the real popup open. By the time this handler runs, rewrite_provider_deep_link() no longer matches, and popup_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 of mod.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

📥 Commits

Reviewing files that changed from the base of the PR and between 2ba7fd5 and ddd033e.

⛔ Files ignored due to path filters (1)
  • app/src-tauri/recipes/zoom/icon.svg is excluded by !**/*.svg
📒 Files selected for processing (5)
  • app/src-tauri/recipes/zoom/manifest.json
  • app/src-tauri/recipes/zoom/recipe.js
  • app/src-tauri/src/webview_accounts/mod.rs
  • app/src/components/accounts/providerIcons.tsx
  • app/src/types/accounts.ts

Comment thread app/src-tauri/src/webview_accounts/mod.rs Outdated
Comment thread app/src-tauri/src/webview_accounts/mod.rs Outdated
senamakel
senamakel previously approved these changes Apr 24, 2026
@senamakel
Copy link
Copy Markdown
Member

bro fix the type issues again...

oxoxDev added a commit to oxoxDev/openhuman that referenced this pull request Apr 24, 2026
…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>
oxoxDev and others added 7 commits April 24, 2026 16:28
…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>
@oxoxDev oxoxDev force-pushed the feat/657-zoom-webview branch from b6ca3eb to 173fd83 Compare April 24, 2026 11:09
@graycyrus graycyrus merged commit 5a246bc into tinyhumansai:main Apr 24, 2026
7 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

P2: Add Zoom, We Chat and Microsoft Teams channel support

3 participants