Skip to content

Guard remaining unguarded Tauri calls for web deployments - #3190

Open
jefflitt1 wants to merge 2 commits into
block:mainfrom
jefflitt1:upstream-web-tauri-guards
Open

jefflitt1 wants to merge 2 commits into
block:mainfrom
jefflitt1:upstream-web-tauri-guards

Conversation

@jefflitt1

@jefflitt1 jefflitt1 commented Jul 27, 2026

Copy link
Copy Markdown

Summary

  • Several call sites into @tauri-apps/api still assume a native Tauri runtime and throw when the desktop chat UI is served as a plain web page instead (no window.__TAURI_INTERNALS__): "Cannot read properties of undefined (reading 'transformCallback'/'invoke')". Most of the codebase already guards this correctly with isTauri() before calling listen()/invoke() (e.g. audioWorklet.ts); this closes the boot-reachable gaps found by exercising a web-served build end-to-end.
  • useAgentsDataRefresh, useNestNotifications, HuddleContext, HuddleBar, HuddleIndicator, MobilePairingCard: guard listen() registration with isTauri().
  • usePreventSleep: guard both effects. The listen() registration throws without Tauri internals, and setPreventSleepActive is fired fire-and-forget (void, no .catch), so an unguarded call there is a genuine unhandled rejection rather than a logged warning.
  • deep-link.ts: the pending-deep-link queue is populated by the Rust backend and only exists in the native shell, so both the invoke() drain and the listen()-based registrations are guarded. In listenForDeepLinks the guard precedes deps.onAddCommunityAvailable — that subscription only wakes the native queue, so in a browser it would register a callback that can never do work.
  • legacyCommunityStorage.ts: the legacy Sprout WebKit SQLite database read only exists in the native shell — skip it in a web runtime.

Guards stay at the consumer rather than inside shared/api/tauri.ts: that module is an API wrapper, not a policy layer, and its callers already handle failure (useCommunityInit wraps its calls in try/catch and falls back to needsSetup, App.tsx catches is_shared_identity and falls back to false). Guarding inside the wrapper would only suppress those console warnings, which are the signal that a packaged native build is misconfigured. It would also grow a file the repo's own size ratchet already holds at its limit.

Apart from the prevent-sleep unhandled rejection, these failures were already caught and logged as console warnings, so this doesn't change behavior for native Tauri users.

Scope note: this covers the boot-reachable native calls surfaced by a standalone-web smoke test, not an exhaustive audit of every invoke()/listen() in the desktop client.

Independent of #3189, but both were found while exercising the same web-served build, and neither on its own makes the bundle usable as a plain web page.

Test plan

  • pnpm build (tsc + vite build) — passes
  • pnpm test — 5799/5799 passing
  • just file-size-check — passes
  • pnpm check (biome + px-text + pubkey-truncation) — clean
  • New deep-link.web.test.mjs sets no Tauri globals, so it runs against the real @tauri-apps/api in the browser runtime the guards exist for. Verified by mutation, not just a green run: removing any one of the five guards fails that file (listenForDeepLinks fails two of six cases, the rest one each).
  • Manually verified in a browser: served the built bundle as a plain web page outside Tauri. Note this was verified against an earlier revision that also guarded shared/api/tauri.ts; those guards were dropped deliberately (see above), so a browser load still logs the is_shared_identity command failed warning from App.tsx's existing .catch. That warning is intended — it is the signal that the native bridge is absent, and suppressing it was the only thing those guards bought.

Related

Checked open PRs/issues for duplicates before submitting — no duplicate found. #3027 is open in an adjacent but different subsystem (relay-side SPA routing config, vs. this PR's desktop-client Tauri guards) for a similarly-motivated self-hosted deployment; not overlapping, noted for context.

@jefflitt1
jefflitt1 requested a review from a team as a code owner July 27, 2026 19:34
@jefflitt1
jefflitt1 force-pushed the upstream-web-tauri-guards branch from 480ee58 to 8e2d66b Compare August 29, 2026 02:35
@github-actions

github-actions Bot commented Aug 29, 2026

Copy link
Copy Markdown

🔐 Codex Security Review

Status: review required for the current range.

The current range is 00e61eafa917d296104006576b7a2ddbfd58bb5a...48baee22c35824a6295d0016f2771a52f70a9743.
A new review must complete for this exact range. When manual authorization
is required, a Block organization member must comment exactly
@buzz-security-review 48baee22c35824a6295d0016f2771a52f70a9743 to authorize a new review.
Any previous review applies only to its recorded range.

@jefflitt1
jefflitt1 force-pushed the upstream-web-tauri-guards branch from 8e2d66b to e37caa1 Compare August 29, 2026 10:12
Several call sites into @tauri-apps/api still assume a native Tauri
runtime and throw when the desktop chat UI is served as a plain web
page instead (no window.__TAURI_INTERNALS__): "Cannot read properties
of undefined (reading 'transformCallback'/'invoke')". Most of the
codebase already guards this correctly with isTauri() before calling
listen()/invoke() (e.g. audioWorklet.ts); these were the remaining
gaps, found by exercising a web-served build end-to-end:

- useAgentsDataRefresh, useNestNotifications, HuddleContext, HuddleBar,
  HuddleIndicator, MobilePairingCard: guard listen() registration with
  isTauri().
- usePreventSleep: guard both effects. The listen() registration throws
  without Tauri internals, and setPreventSleepActive is fired
  fire-and-forget (`void`, no .catch), so an unguarded call there is a
  genuine unhandled rejection rather than a logged warning. Sleep
  prevention is a native OS concept with no browser equivalent.
- deep-link.ts: the pending-deep-link queue is populated by the Rust
  backend and only exists in the native shell, so both the invoke()
  drain and the listen()-based registrations are guarded. In
  listenForDeepLinks the guard precedes deps.onAddCommunityAvailable —
  that subscription only wakes the native queue, so in a browser it
  would register a callback that can never do work.
- legacyCommunityStorage.ts: the legacy Sprout WebKit SQLite database
  read only exists in the native shell — skip it in a web runtime.

deep-link.test.mjs fakes a Tauri runtime with __TAURI_INTERNALS__, but
isTauri() reads globalThis.isTauri, which the native shell injects
alongside the IPC internals. The fake runtime now sets it too, so those
tests continue to exercise the native branch.

Guards stay at the consumer rather than inside shared/api/tauri.ts:
that module is an API wrapper, not a policy layer, and its callers
already handle failure (useCommunityInit wraps its calls in try/catch
and falls back to needsSetup, App.tsx catches is_shared_identity and
falls back to false). Guarding inside the wrapper would only suppress
those console warnings, which are the signal that a packaged native
build is misconfigured.

Signed-off-by: Jeff Litt <jglittell@gmail.com>
deep-link.test.mjs sets globalThis.isTauri = true so its fake runtime
matches what isTauri() actually reads, which means every test in that
file exercises the native branch. Nothing covered the guards themselves,
so they could all regress with the suite still green.

deep-link.web.test.mjs sets no Tauri globals at all — that is exactly
the browser runtime the guards exist for, so it needs no mocking and
runs against the real @tauri-apps/api. Each guarded export is asserted
to resolve to a callable cleanup without routing a payload or reaching
the bridge; if a guard leaks, the real invoke()/listen() reject and the
await fails.

It is a separate file because node's per-process ESM module cache would
otherwise hand a second import of deep-link.ts the instance already
evaluated under the other file's globals. node --test runs each matched
file in its own process, which sidesteps that.

Verified by mutation rather than by a passing run: removing any one of
the five guards fails this file — listenForDeepLinks fails two of the
six cases, the other four fail one each.

Signed-off-by: Jeff Litt <jglittell@gmail.com>
@jefflitt1
jefflitt1 force-pushed the upstream-web-tauri-guards branch from e37caa1 to 48baee2 Compare August 29, 2026 16:00
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.

1 participant