收口原生能力可信边界

H5 原生能力只信任真实 host.getRuntime 回包

移除桌面壳未声明网络事件白名单

补齐原生壳能力门控测试和共享决策记录
This commit is contained in:
2026-06-21 18:08:02 +08:00
parent 0ecafa128c
commit ba000111d7
16 changed files with 336 additions and 69 deletions
+3
View File
@@ -87,6 +87,7 @@ const hostBridgeMocks = vi.hoisted(() => ({
hostPlatform: null as string | null,
hostVersion: null as string | null,
hostCapabilities: [],
nativeRuntimeTrusted: false,
miniProgramEnv: null as string | null,
})),
requestHostLogin: vi.fn(),
@@ -195,6 +196,7 @@ beforeEach(() => {
hostPlatform: null,
hostVersion: null,
hostCapabilities: [],
nativeRuntimeTrusted: false,
miniProgramEnv: null,
});
hostBridgeMocks.requestHostLogin.mockResolvedValue(true);
@@ -486,6 +488,7 @@ test('auth gate uses mini program auth bridge instead of opening login modal in
hostPlatform: null,
hostVersion: null,
hostCapabilities: [],
nativeRuntimeTrusted: false,
miniProgramEnv: null,
});
authMocks.getAuthLoginOptions.mockResolvedValue({
@@ -4,7 +4,10 @@ import { fireEvent, render, screen, waitFor } from '@testing-library/react';
import type { ComponentProps } from 'react';
import { afterEach, expect, test, vi } from 'vitest';
import { resetHostRuntimeCacheForTest } from '../../services/host-bridge/hostBridge';
import {
resetHostRuntimeCacheForTest,
setHostRuntimeCacheForTest,
} from '../../services/host-bridge/hostBridge';
import { resetNativeAppHostBridgeForTest } from '../../services/host-bridge/nativeAppHostBridge';
import type { CreativeAudioAsset } from './creativeAudioFileAsset';
import { CreativeAudioInputPanel } from './CreativeAudioInputPanel';
@@ -57,6 +60,16 @@ function buildExportableAsset(
};
}
function trustNativeHostRuntime(capabilities: Parameters<typeof setHostRuntimeCacheForTest>[0]['capabilities']) {
setHostRuntimeCacheForTest({
shell: 'tauri_desktop',
platform: 'linux',
hostVersion: '0.1.0',
bridgeVersion: 1,
capabilities,
});
}
function renderPanel(
overrides: Partial<
ComponentProps<typeof CreativeAudioInputPanel<TestAudioAsset>>
@@ -206,6 +219,7 @@ test('原生 App 宿主可用时上传按钮走 HostBridge 音频导入', async
'',
'/?clientRuntime=native_app&hostCapabilities=file.importAudio',
);
trustNativeHostRuntime(['file.importAudio']);
window.__TAURI__ = {
core: {
invoke: async <Result,>(
@@ -256,6 +270,7 @@ test('原生 App 宿主可用且当前音频为本地资产时可以导出音频
'',
'/?clientRuntime=native_app&hostCapabilities=file.exportAudio',
);
trustNativeHostRuntime(['file.exportAudio']);
window.__TAURI__ = {
core: {
invoke: async <Result,>(
@@ -298,6 +313,7 @@ test('非本地音频资产或未声明能力时不显示导出入口', () => {
'',
'/?clientRuntime=native_app&hostCapabilities=file.exportAudio',
);
trustNativeHostRuntime(['file.exportAudio']);
rerender(
<CreativeAudioInputPanel<TestAudioAsset>
title="敲击音效"
@@ -333,6 +349,7 @@ test('导出音频失败时提示错误', async () => {
'',
'/?clientRuntime=native_app&hostCapabilities=file.exportAudio',
);
trustNativeHostRuntime(['file.exportAudio']);
window.__TAURI__ = {
core: {
invoke: async <Result,>(
@@ -11,6 +11,10 @@ import { afterEach, describe, expect, test, vi } from 'vitest';
import { HOST_BRIDGE_PUBLIC_WEB_ORIGIN } from '../../../packages/shared/src/contracts/hostBridge';
import * as clipboardService from '../../services/clipboard';
import {
resetHostRuntimeCacheForTest,
setHostRuntimeCacheForTest,
} from '../../services/host-bridge/hostBridge';
import { PublishShareModal } from './PublishShareModal';
import {
buildMiniProgramPublishSharePath,
@@ -39,6 +43,7 @@ afterEach(() => {
delete window.ReactNativeWebView;
delete window.__TAURI__;
window.wx = undefined;
resetHostRuntimeCacheForTest();
});
function asTauriInvoke(
@@ -52,6 +57,19 @@ function asTauriInvoke(
};
}
function trustNativeHostRuntime(
shell: 'expo_mobile' | 'tauri_desktop',
capabilities: Parameters<typeof setHostRuntimeCacheForTest>[0]['capabilities'],
) {
setHostRuntimeCacheForTest({
shell,
platform: shell === 'expo_mobile' ? 'ios' : 'linux',
hostVersion: '0.1.0',
bridgeVersion: 1,
capabilities,
});
}
describe('PublishShareModal', () => {
test('builds the publish share text with title, code and public url', () => {
const text = buildPublishShareText(payload);
@@ -180,6 +198,7 @@ describe('PublishShareModal', () => {
'',
'/?clientRuntime=native_app&hostShell=tauri_desktop&hostCapabilities=share.open',
);
trustNativeHostRuntime('tauri_desktop', ['share.open']);
window.__TAURI__ = {
core: {
invoke: asTauriInvoke(invoke),
@@ -222,6 +241,7 @@ describe('PublishShareModal', () => {
'',
'/?clientRuntime=native_app&hostShell=expo_mobile&hostCapabilities=share.open',
);
trustNativeHostRuntime('expo_mobile', ['share.open']);
const postMessage = vi.fn((rawMessage: string) => {
const request = JSON.parse(rawMessage) as { id: string };
window.dispatchEvent(
@@ -328,6 +348,7 @@ describe('PublishShareModal', () => {
'',
'/?clientRuntime=native_app&hostShell=tauri_desktop&hostCapabilities=file.exportImage',
);
trustNativeHostRuntime('tauri_desktop', ['file.exportImage']);
window.__TAURI__ = {
core: {
invoke: asTauriInvoke(invoke),
@@ -26,6 +26,7 @@ vi.mock('../../services/host-bridge/hostBridge', () => ({
hostPlatform: null,
hostVersion: null,
hostCapabilities: [],
nativeRuntimeTrusted: false,
miniProgramEnv: null,
})),
openHostExternalUrl: vi.fn(async () => false),
@@ -49,6 +50,7 @@ beforeEach(() => {
hostPlatform: null,
hostVersion: null,
hostCapabilities: [],
nativeRuntimeTrusted: false,
miniProgramEnv: null,
});
vi.mocked(openHostExternalUrl).mockResolvedValue(false);
@@ -132,6 +134,7 @@ describe('PlatformProfilePrimitives', () => {
hostPlatform: 'ios',
hostVersion: '0.1.0',
hostCapabilities: ['app.openExternalUrl'],
nativeRuntimeTrusted: true,
miniProgramEnv: null,
});
vi.mocked(openHostExternalUrl).mockResolvedValue(true);
@@ -6,7 +6,10 @@ import type {
HostBridgeCapability,
HostBridgeRequest,
} from '../../../packages/shared/src/contracts/hostBridge';
import { resetHostRuntimeCacheForTest } from '../../services/host-bridge/hostBridge';
import {
resetHostRuntimeCacheForTest,
setHostRuntimeCacheForTest,
} from '../../services/host-bridge/hostBridge';
import { resetNativeAppHostBridgeForTest } from '../../services/host-bridge/nativeAppHostBridge';
import {
sendPlatformHostDraftNotificationToHost,
@@ -35,6 +38,16 @@ function nativeAppPath(capabilities: HostBridgeCapability[] = []) {
return `/?${params.toString()}`;
}
function trustNativeHostRuntime(capabilities: HostBridgeCapability[]) {
setHostRuntimeCacheForTest({
shell: 'tauri_desktop',
platform: 'linux',
hostVersion: '0.1.0',
bridgeVersion: 1,
capabilities,
});
}
function installTauriHostBridgeRecorder() {
const requests: HostBridgeRequest[] = [];
const invoke = vi.fn(async (_command: string, args?: Record<string, unknown>) => {
@@ -73,6 +86,7 @@ describe('platformHostBridgeSync', () => {
'',
nativeAppPath(['notification.showLocal']),
);
trustNativeHostRuntime(['notification.showLocal']);
const { invoke, requests } = installTauriHostBridgeRecorder();
await expect(
@@ -100,6 +114,7 @@ describe('platformHostBridgeSync', () => {
'',
nativeAppPath(['app.setBadgeCount']),
);
trustNativeHostRuntime(['app.setBadgeCount']);
const { invoke, requests } = installTauriHostBridgeRecorder();
await expect(syncPlatformHostDraftBadgeCountToHost(3)).resolves.toBe(true);
@@ -20,6 +20,7 @@ vi.mock('../../services/host-bridge/hostBridge', () => ({
hostPlatform: null,
hostVersion: null,
hostCapabilities: [],
nativeRuntimeTrusted: false,
miniProgramEnv: null,
})),
openHostExternalUrl: vi.fn(async () => false),
@@ -39,6 +40,7 @@ beforeEach(() => {
hostPlatform: null,
hostVersion: null,
hostCapabilities: [],
nativeRuntimeTrusted: false,
miniProgramEnv: null,
});
vi.mocked(openHostExternalUrl).mockResolvedValue(false);
@@ -217,6 +219,7 @@ test('RPG asset debug panel 在原生 App 中通过宿主打开原图外链', as
hostPlatform: 'linux',
hostVersion: '0.1.0',
hostCapabilities: ['app.openExternalUrl'],
nativeRuntimeTrusted: true,
miniProgramEnv: null,
});
vi.mocked(openHostExternalUrl).mockResolvedValue(true);
@@ -98,7 +98,10 @@ import {
regenerateBabyObjectMatchDraftAssets,
saveBabyObjectMatchDraft,
} from '../../services/edutainment-baby-object';
import { resetHostRuntimeCacheForTest } from '../../services/host-bridge/hostBridge';
import {
resetHostRuntimeCacheForTest,
setHostRuntimeCacheForTest,
} from '../../services/host-bridge/hostBridge';
import { resetNativeAppHostBridgeForTest } from '../../services/host-bridge/nativeAppHostBridge';
import { jumpHopClient } from '../../services/jump-hop/jumpHopClient';
import { match3dCreationClient } from '../../services/match3d-creation';
@@ -315,11 +318,26 @@ function nativeAppPath(capabilities: HostBridgeCapability[] = []) {
return `/?${params.toString()}`;
}
function installTauriHostBridgeRecorder() {
function installTauriHostBridgeRecorder(capabilities: HostBridgeCapability[] = []) {
const requests: HostBridgeRequest[] = [];
const invoke = vi.fn(async (_command: string, args?: Record<string, unknown>) => {
const request = (args as { request: HostBridgeRequest }).request;
requests.push(request);
if (request.method === 'host.getRuntime') {
return {
bridge: 'GenarrativeHostBridge',
version: 1,
id: request.id,
ok: true,
result: {
shell: 'tauri_desktop',
platform: 'linux',
hostVersion: '0.1.0',
bridgeVersion: 1,
capabilities: ['host.getRuntime', ...capabilities],
},
};
}
return {
bridge: 'GenarrativeHostBridge',
version: 1,
@@ -9199,7 +9217,17 @@ test('native app jump hop draft completion sends host notification and badge fro
resolveCompile = resolve;
}),
);
const { requests } = installTauriHostBridgeRecorder();
const { requests } = installTauriHostBridgeRecorder([
'app.setBadgeCount',
'notification.showLocal',
]);
setHostRuntimeCacheForTest({
shell: 'tauri_desktop',
platform: 'linux',
hostVersion: '0.1.0',
bridgeVersion: 1,
capabilities: ['app.setBadgeCount', 'notification.showLocal'],
});
window.history.replaceState(
null,
'',
@@ -9250,7 +9278,16 @@ test('native app jump hop draft completion sends host notification and badge fro
test('native app opens child motion demo through host navigation bridge', async () => {
const user = userEvent.setup();
const { requests } = installTauriHostBridgeRecorder();
const { requests } = installTauriHostBridgeRecorder([
'navigation.openNativePage',
]);
setHostRuntimeCacheForTest({
shell: 'tauri_desktop',
platform: 'linux',
hostVersion: '0.1.0',
bridgeVersion: 1,
capabilities: ['navigation.openNativePage'],
});
window.history.replaceState(
null,
'',