mirror of
https://github.com/tinyhumansai/openhuman.git
synced 2026-07-27 21:08:00 +00:00
fix(ipc): guard isTauri() on __TAURI_INTERNALS__.invoke (OPENHUMAN-REACT-S) (#1556)
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> Co-authored-by: Steven Enamakel <enamakel@tinyhumans.ai>
This commit is contained in:
co-authored by
Claude Opus 4.7
Steven Enamakel
parent
3d4e4d278c
commit
84d86da70e
@@ -9,7 +9,6 @@
|
||||
* Visual language follows ServiceBlockingGate.tsx (bg-stone-950/80 overlay,
|
||||
* bg-stone-900 panel, ocean-500 / coral-500 semantics).
|
||||
*/
|
||||
import { isTauri } from '@tauri-apps/api/core';
|
||||
import debug from 'debug';
|
||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||
|
||||
@@ -29,6 +28,7 @@ import {
|
||||
storeCoreToken,
|
||||
storeRpcUrl,
|
||||
} from '../../utils/configPersistence';
|
||||
import { isTauri } from '../../utils/tauriCommands/common';
|
||||
|
||||
const log = debug('boot-check');
|
||||
const logError = debug('boot-check:error');
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import { invoke, isTauri } from '@tauri-apps/api/core';
|
||||
import { invoke } from '@tauri-apps/api/core';
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
import { triggerSentryTestEvent } from '../../../services/analytics';
|
||||
import { useAppSelector } from '../../../store/hooks';
|
||||
import { APP_ENVIRONMENT } from '../../../utils/config';
|
||||
import { isTauri } from '../../../utils/tauriCommands/common';
|
||||
import SettingsHeader from '../components/SettingsHeader';
|
||||
import SettingsMenuItem from '../components/SettingsMenuItem';
|
||||
import { useSettingsNavigation } from '../hooks/useSettingsNavigation';
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import { invoke, isTauri } from '@tauri-apps/api/core';
|
||||
import { invoke } from '@tauri-apps/api/core';
|
||||
import debug from 'debug';
|
||||
|
||||
import { isTauri } from '../../utils/tauriCommands/common';
|
||||
|
||||
const log = debug('native-notifications:bridge');
|
||||
const errLog = debug('native-notifications:bridge:error');
|
||||
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { isTauri } from '@tauri-apps/api/core';
|
||||
import { listen, type UnlistenFn } from '@tauri-apps/api/event';
|
||||
import debug from 'debug';
|
||||
|
||||
@@ -9,6 +8,7 @@ import {
|
||||
noteWebviewNotificationFired,
|
||||
} from '../../store/accountsSlice';
|
||||
import { addIntegrationNotification } from '../../store/notificationSlice';
|
||||
import { isTauri } from '../../utils/tauriCommands/common';
|
||||
import { WEBVIEW_NOTIFICATION_FIRED_EVENT, type WebviewNotificationFired } from './types';
|
||||
|
||||
const log = debug('webview-notifications');
|
||||
|
||||
+5
-2
@@ -1,5 +1,4 @@
|
||||
// IMPORTANT: Polyfills must be imported FIRST
|
||||
import { isTauri as tauriRuntimeAvailable } from '@tauri-apps/api/core';
|
||||
import { getCurrentWindow } from '@tauri-apps/api/window';
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom/client';
|
||||
@@ -16,6 +15,7 @@ import { primeActiveUserId } from './store/userScopedStorage';
|
||||
import { APP_VERSION } from './utils/config';
|
||||
import { setupDesktopDeepLinkListener } from './utils/desktopDeepLinkListener';
|
||||
import { getActiveUserIdFromCore } from './utils/tauriCommands';
|
||||
import { isTauri as tauriRuntimeAvailable } from './utils/tauriCommands/common';
|
||||
|
||||
setStoreForApiClient(() => getCoreStateSnapshot().snapshot.sessionToken);
|
||||
|
||||
@@ -23,7 +23,10 @@ setStoreForApiClient(() => getCoreStateSnapshot().snapshot.sessionToken);
|
||||
// that lives OUTSIDE Tauri's runtime (the vendored tauri-cef can't render
|
||||
// transparent windowed-mode browsers). That webview can't read a Tauri
|
||||
// window label, so the Rust shell appends `?window=mascot` to the URL it
|
||||
// loads. Detect it before we touch any Tauri APIs.
|
||||
// loads. Detect it via the URL param so we can skip `getCurrentWindow()`
|
||||
// — which would either throw or trigger the CEF IPC-bootstrap gap that
|
||||
// `tauriRuntimeAvailable()` (= the hardened `isTauri()`) now guards
|
||||
// against by reading `window.__TAURI_INTERNALS__.invoke`.
|
||||
const urlWindowParam = (() => {
|
||||
try {
|
||||
return new URLSearchParams(window.location.search).get('window');
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { isTauri as coreIsTauri } from '@tauri-apps/api/core';
|
||||
|
||||
import { BACKEND_URL } from '../utils/config';
|
||||
import { isTauri as coreIsTauri } from '../utils/tauriCommands/common';
|
||||
import { callCoreRpc } from './coreRpcClient';
|
||||
|
||||
let resolvedBackendUrl: string | null = null;
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import { isTauri as coreIsTauri, invoke } from '@tauri-apps/api/core';
|
||||
import { invoke } from '@tauri-apps/api/core';
|
||||
import debug from 'debug';
|
||||
|
||||
import { dispatchLocalAiMethod } from '../lib/ai/localCoreAiMemory';
|
||||
import { CORE_RPC_TIMEOUT_MS, CORE_RPC_URL } from '../utils/config';
|
||||
import { getStoredCoreToken, peekStoredRpcUrl } from '../utils/configPersistence';
|
||||
import { sanitizeError } from '../utils/sanitize';
|
||||
import { isTauri as coreIsTauri } from '../utils/tauriCommands/common';
|
||||
import { normalizeRpcMethod } from './rpcMethods';
|
||||
|
||||
interface CoreRpcRelayRequest {
|
||||
|
||||
@@ -9,8 +9,9 @@
|
||||
//
|
||||
// Splitting it this way keeps platform-specific window code in the shell
|
||||
// while the validation rules live (and are tested) in the core.
|
||||
import { invoke, isTauri } from '@tauri-apps/api/core';
|
||||
import { invoke } from '@tauri-apps/api/core';
|
||||
|
||||
import { isTauri } from '../utils/tauriCommands/common';
|
||||
import { callCoreRpc } from './coreRpcClient';
|
||||
|
||||
export type MeetJoinCallInput = { meetUrl: string; displayName: string };
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import * as Sentry from '@sentry/react';
|
||||
import { invoke, isTauri } from '@tauri-apps/api/core';
|
||||
import { invoke } from '@tauri-apps/api/core';
|
||||
import { listen, type UnlistenFn } from '@tauri-apps/api/event';
|
||||
import debug from 'debug';
|
||||
|
||||
@@ -13,6 +13,7 @@ import {
|
||||
import { addIntegrationNotification } from '../store/notificationSlice';
|
||||
import { fetchRespondQueue } from '../store/providerSurfaceSlice';
|
||||
import type { AccountProvider, IngestedMessage } from '../types/accounts';
|
||||
import { isTauri } from '../utils/tauriCommands/common';
|
||||
import { openhumanGetMeetSettings } from '../utils/tauriCommands/config';
|
||||
import { trackEvent } from './analytics';
|
||||
import { threadApi } from './api/threadApi';
|
||||
@@ -25,6 +26,10 @@ const MEET_ORCHESTRATOR_MODEL = 'reasoning-v1';
|
||||
const log = debug('webview-accounts');
|
||||
const errLog = debug('webview-accounts:error');
|
||||
|
||||
// Re-export the canonical Tauri guard so existing imports
|
||||
// `import { isTauri } from '.../webviewAccountService'` keep working.
|
||||
// The implementation lives in `utils/tauriCommands/common.ts` and accounts
|
||||
// for the CEF IPC injection race (see comment there).
|
||||
export { isTauri };
|
||||
|
||||
/**
|
||||
|
||||
@@ -98,6 +98,17 @@ if (typeof Element !== 'undefined' && !Element.prototype.scrollIntoView) {
|
||||
Element.prototype.scrollIntoView = function () {};
|
||||
}
|
||||
|
||||
// The hardened `isTauri()` (in `utils/tauriCommands/common.ts`) checks both
|
||||
// `coreIsTauri()` and `window.__TAURI_INTERNALS__.invoke`. Many existing test
|
||||
// files mock `@tauri-apps/api/core::isTauri` to `true` to exercise the
|
||||
// Tauri branch; without a matching IPC handle on `window` they would now
|
||||
// regress to the non-Tauri path. Seed a no-op handle once globally so the
|
||||
// IPC-readiness check passes by default. Tests that *want* the CEF gap
|
||||
// behaviour can `delete window.__TAURI_INTERNALS__` in a `beforeEach`.
|
||||
(
|
||||
window as unknown as { __TAURI_INTERNALS__: { invoke: () => Promise<unknown> } }
|
||||
).__TAURI_INTERNALS__ = { invoke: vi.fn(() => Promise.resolve()) };
|
||||
|
||||
// Mock Tauri APIs (not available in test env)
|
||||
vi.mock('@tauri-apps/api/core', () => ({ invoke: vi.fn(), isTauri: vi.fn(() => false) }));
|
||||
|
||||
@@ -223,6 +234,13 @@ if (!process.env.DEBUG_TESTS) {
|
||||
afterEach(() => {
|
||||
clearRequestLog();
|
||||
cleanup();
|
||||
// Re-seed the IPC handle after any test that may have deleted it
|
||||
// (e.g. tests exercising the CEF-gap branch of `isTauri()`). Without
|
||||
// this, sibling tests in the same jsdom worker would silently regress
|
||||
// to the non-Tauri path. Per graycyrus review on PR #1556.
|
||||
(
|
||||
window as unknown as { __TAURI_INTERNALS__: { invoke: () => Promise<unknown> } }
|
||||
).__TAURI_INTERNALS__ = { invoke: vi.fn(() => Promise.resolve()) };
|
||||
});
|
||||
afterAll(async () => {
|
||||
await stopMockServer();
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import * as Sentry from '@sentry/react';
|
||||
import { isTauri as coreIsTauri } from '@tauri-apps/api/core';
|
||||
import { getCurrentWindow } from '@tauri-apps/api/window';
|
||||
import { getCurrent, onOpenUrl } from '@tauri-apps/plugin-deep-link';
|
||||
|
||||
@@ -14,6 +13,7 @@ import { BILLING_DASHBOARD_URL } from './links';
|
||||
import { evaluateOAuthAppVersionGate } from './oauthAppVersionGate';
|
||||
import { openUrl } from './openUrl';
|
||||
import { storeSession } from './tauriCommands';
|
||||
import { isTauri as coreIsTauri } from './tauriCommands/common';
|
||||
|
||||
const SESSION_TOKEN_UPDATED_EVENT = 'core-state:session-token-updated';
|
||||
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { getVersion } from '@tauri-apps/api/app';
|
||||
import { isTauri } from '@tauri-apps/api/core';
|
||||
|
||||
import { LATEST_APP_DOWNLOAD_URL, MINIMUM_SUPPORTED_APP_VERSION } from './config';
|
||||
import { isVersionAtLeast, parseSemverParts } from './semver';
|
||||
import { isTauri } from './tauriCommands/common';
|
||||
|
||||
export type OAuthAppVersionGateResult =
|
||||
| { ok: true }
|
||||
|
||||
@@ -11,7 +11,7 @@ const isTauriMock = vi.fn();
|
||||
const tauriOpenUrlMock = vi.fn();
|
||||
const addBreadcrumbMock = vi.fn();
|
||||
|
||||
vi.mock('@tauri-apps/api/core', () => ({ isTauri: () => isTauriMock() }));
|
||||
vi.mock('./tauriCommands/common', () => ({ isTauri: () => isTauriMock() }));
|
||||
|
||||
vi.mock('@tauri-apps/plugin-opener', () => ({ openUrl: (url: string) => tauriOpenUrlMock(url) }));
|
||||
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import * as Sentry from '@sentry/react';
|
||||
import { isTauri } from '@tauri-apps/api/core';
|
||||
import { openUrl as tauriOpenUrl } from '@tauri-apps/plugin-opener';
|
||||
|
||||
import { isTauri } from './tauriCommands/common';
|
||||
|
||||
const isHttpUrl = (url: string): boolean => /^https?:\/\//i.test(url);
|
||||
|
||||
/**
|
||||
|
||||
@@ -0,0 +1,92 @@
|
||||
/**
|
||||
* Unit tests for `isTauri()` — the canonical Tauri-runtime guard used across
|
||||
* `app/src/`. Beyond delegating to `@tauri-apps/api/core::isTauri()`, this
|
||||
* wrapper also confirms that the IPC transport (`window.__TAURI_INTERNALS__
|
||||
* .invoke`) is wired before reporting `true`.
|
||||
*
|
||||
* Why it matters: under CEF, `globalThis.isTauri` (which the underlying
|
||||
* `coreIsTauri()` checks) is injected by the webview bootstrap BEFORE the
|
||||
* `postMessage` IPC bridge is connected. An `invoke()` landing in that gap
|
||||
* throws `TypeError: Cannot read properties of undefined (reading
|
||||
* 'postMessage')` deep inside Tauri's `sendIpcMessage`, which surfaces as
|
||||
* the OPENHUMAN-REACT-S Sentry issue (#1472 follow-up). All call sites that
|
||||
* gate on `isTauri()` should now route through the non-Tauri branch during
|
||||
* the gap instead of bursting into IPC.
|
||||
*/
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import { isTauri } from './common';
|
||||
|
||||
const coreIsTauriMock = vi.fn();
|
||||
|
||||
vi.mock('@tauri-apps/api/core', () => ({ isTauri: () => coreIsTauriMock() }));
|
||||
|
||||
describe('isTauri (tauriCommands/common)', () => {
|
||||
// We mutate `window` to simulate Tauri-runtime bootstrap state across cases.
|
||||
// Stash + restore so other tests in the suite (which share the jsdom global)
|
||||
// see a pristine window.
|
||||
let originalInternals: unknown;
|
||||
|
||||
beforeEach(() => {
|
||||
coreIsTauriMock.mockReset();
|
||||
originalInternals = (window as unknown as { __TAURI_INTERNALS__?: unknown })
|
||||
.__TAURI_INTERNALS__;
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
if (originalInternals === undefined) {
|
||||
delete (window as unknown as { __TAURI_INTERNALS__?: unknown }).__TAURI_INTERNALS__;
|
||||
} else {
|
||||
(window as unknown as { __TAURI_INTERNALS__?: unknown }).__TAURI_INTERNALS__ =
|
||||
originalInternals;
|
||||
}
|
||||
});
|
||||
|
||||
it('returns false when not running in Tauri at all (browser/Vitest)', () => {
|
||||
coreIsTauriMock.mockReturnValue(false);
|
||||
delete (window as unknown as { __TAURI_INTERNALS__?: unknown }).__TAURI_INTERNALS__;
|
||||
|
||||
expect(isTauri()).toBe(false);
|
||||
});
|
||||
|
||||
it('returns true when both the runtime flag and the IPC `invoke` handle are present', () => {
|
||||
coreIsTauriMock.mockReturnValue(true);
|
||||
(window as unknown as { __TAURI_INTERNALS__?: { invoke: unknown } }).__TAURI_INTERNALS__ = {
|
||||
invoke: () => Promise.resolve(),
|
||||
};
|
||||
|
||||
expect(isTauri()).toBe(true);
|
||||
});
|
||||
|
||||
// The OPENHUMAN-REACT-S regression: Tauri sets `globalThis.isTauri = true`
|
||||
// (so the official check returns true) before CEF wires the IPC postMessage
|
||||
// bridge. During that gap any unguarded `invoke(...)` blows up inside
|
||||
// `sendIpcMessage` with the "Cannot read properties of undefined (reading
|
||||
// 'postMessage')" TypeError. Our guard must short-circuit to `false` so
|
||||
// call sites skip the IPC path instead of trusting the runtime flag alone.
|
||||
it('returns false during the CEF gap when runtime flag is set but __TAURI_INTERNALS__ is missing', () => {
|
||||
coreIsTauriMock.mockReturnValue(true);
|
||||
delete (window as unknown as { __TAURI_INTERNALS__?: unknown }).__TAURI_INTERNALS__;
|
||||
|
||||
expect(isTauri()).toBe(false);
|
||||
});
|
||||
|
||||
it('returns false during the partial-bootstrap gap when __TAURI_INTERNALS__ exists but `invoke` is not yet wired', () => {
|
||||
coreIsTauriMock.mockReturnValue(true);
|
||||
// Some CEF bootstrap stages set the object literal before the IPC handle
|
||||
// is attached. Treat that as "not ready".
|
||||
(window as unknown as { __TAURI_INTERNALS__?: Record<string, unknown> }).__TAURI_INTERNALS__ =
|
||||
{};
|
||||
|
||||
expect(isTauri()).toBe(false);
|
||||
});
|
||||
|
||||
it('returns false when __TAURI_INTERNALS__.invoke is present but not a function', () => {
|
||||
coreIsTauriMock.mockReturnValue(true);
|
||||
(window as unknown as { __TAURI_INTERNALS__?: { invoke: unknown } }).__TAURI_INTERNALS__ = {
|
||||
invoke: 'not-a-function',
|
||||
};
|
||||
|
||||
expect(isTauri()).toBe(false);
|
||||
});
|
||||
});
|
||||
@@ -2,11 +2,41 @@
|
||||
* Common utilities and types for Tauri Commands.
|
||||
*/
|
||||
import { isTauri as coreIsTauri } from '@tauri-apps/api/core';
|
||||
import debug from 'debug';
|
||||
|
||||
// Check if we're running in Tauri
|
||||
const log = debug('tauri:ipc-guard');
|
||||
|
||||
/**
|
||||
* True when the Tauri runtime is present AND the underlying IPC transport is
|
||||
* wired. The official `coreIsTauri()` check (which reads `globalThis.isTauri`)
|
||||
* is set early by Tauri's webview bootstrap, but on CEF `__TAURI_INTERNALS__`
|
||||
* (and the `postMessage` bridge it dispatches through) is injected *after*
|
||||
* `on_after_created` fires. An `invoke()` landing in that gap throws
|
||||
* `TypeError: Cannot read properties of undefined (reading 'postMessage')`
|
||||
* deep inside Tauri's `sendIpcMessage` — see OPENHUMAN-REACT-S / #1472.
|
||||
*
|
||||
* Callers that gate on `isTauri()` BEFORE invoking should therefore use this
|
||||
* function; it returns `false` during the bootstrap gap so the call site
|
||||
* takes the non-Tauri branch (skip / fallback) instead of synchronously
|
||||
* throwing into a `new Promise` body where the rejection escapes the local
|
||||
* try/catch and lands as an unhandled Sentry event.
|
||||
*/
|
||||
export const isTauri = (): boolean => {
|
||||
// Tauri v2: prefer the official runtime check over window globals.
|
||||
return coreIsTauri();
|
||||
if (!coreIsTauri()) return false;
|
||||
if (typeof window === 'undefined') return false;
|
||||
// Narrow `window` access through a single optional chain so the check is
|
||||
// resilient to either `__TAURI_INTERNALS__` being absent or `.invoke`
|
||||
// being missing while the rest of the object is partially populated.
|
||||
const internals = (window as unknown as { __TAURI_INTERNALS__?: { invoke?: unknown } })
|
||||
.__TAURI_INTERNALS__;
|
||||
if (typeof internals?.invoke !== 'function') {
|
||||
// Bridge-missing branch: distinct from `!coreIsTauri()` (= not in Tauri
|
||||
// at all). Logging here makes the CEF bootstrap gap observable in dev
|
||||
// and is a no-op in production (debug namespace disabled by default).
|
||||
log('isTauri() -> false: IPC bridge not wired (CEF bootstrap gap or non-Tauri)');
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
export interface CommandResponse<T> {
|
||||
|
||||
Reference in New Issue
Block a user