收口H5桥接载荷边界
让H5 HostBridge facade 复用共享MIME清单 增加原生壳门禁拦截H5本地复制MIME边界 补充共享决策记录中的载荷边界约定
This commit is contained in:
@@ -30,6 +30,7 @@
|
||||
- 2026-06-18 宿主 runtime 回读:主 App 启动时会通过真实 `host.getRuntime` 回读 Expo / Tauri runtime 并缓存过滤后的能力清单,能力来源为 URL `hostCapabilities` 与宿主真实回包的并集;裁剪壳或旧入口 URL 缺少 `hostCapabilities` 时也能启用真实声明能力,但仍不会仅凭 `native_app` 或 transport 存在推断能力可用。
|
||||
- 2026-06-18 壳能力防漂移:`npm run mobile-shell:typecheck` 与 `npm run desktop-shell:typecheck` 会校验 Expo / Tauri 壳声明的 capability 均来自共享 HostBridge 白名单,并校验壳 runtime 回包、H5 URL `hostCapabilities` 和实现分支保持一致;微信小程序 `WECHAT_HOST_CAPABILITIES` 由 `miniprogram/host-bridge/protocol.test.js` 和根级 `npm run check:native-shells` 反查共享 `HOST_BRIDGE_WECHAT_MINI_PROGRAM_CAPABILITIES`。新增能力必须先更新契约和真实壳实现,再通过这些检查。
|
||||
- 2026-06-19 宿主上下文 query 契约收口:`packages/shared/src/contracts/hostBridge.ts` 是宿主上下文 query 字段和值的唯一 TypeScript 来源;`HOST_BRIDGE_RUNTIME_CONTEXT_QUERY_KEY` 覆盖 H5 runtime parser 字段,`HOST_BRIDGE_NATIVE_APP_QUERY_KEY` / `HOST_BRIDGE_NATIVE_APP_QUERY_KEYS` / `HOST_BRIDGE_NATIVE_APP_QUERY` 固定 Expo / Tauri 原生壳入口 query,`HOST_BRIDGE_WECHAT_MINI_PROGRAM_SOURCE_QUERY` 固定微信 WebView 来源标记,`HOST_BRIDGE_PRESERVED_RUNTIME_CONTEXT_QUERY_KEYS` 固定 H5 页面内导航需要保留的宿主字段。Expo 壳直接引用共享常量,Tauri Rust 和微信 CommonJS 镜像由 `npm run check:native-shells` / 单壳配置检查反查;微信请求头必须从 `WEB_VIEW_SOURCE_QUERY` 读取 `clientType` / `clientRuntime`,不得另起常量。
|
||||
- 2026-06-19 H5 HostBridge 载荷边界收口:`src/services/host-bridge/hostBridge.ts` 作为 H5 facade 也必须直接导入 `HOST_BRIDGE_TEXT_MIME_TYPES`、`HOST_BRIDGE_DOCUMENT_MIME_TYPES`、`HOST_BRIDGE_IMAGE_MIME_TYPES` 和 `HOST_BRIDGE_AUDIO_MIME_TYPES`,只能从共享契约派生本地 Set 用于归一化,不得重新写 MIME 字面量清单;`npm run check:native-shells` 会拒绝 H5 facade 重新复制文本、图片或音频 MIME 边界。
|
||||
- 2026-06-18 原生壳统一验收门禁:根级 `npm run check:native-shells` 统一执行 H5 HostBridge 关键测试、Expo 壳 typecheck / test / config smoke / Metro export smoke、Tauri 壳 typecheck / cargo test、桌面 release `--no-bundle` 构建烟测,以及可分发壳与 H5 HostBridge 真实调用链的临时替身词扫描;根级 `npm run check` 会在 lint、主站测试、构建和内容检查后继续执行该门禁,避免 HostBridge、三端壳、Expo managed config、移动端 production bundle、桌面 release 入口和 H5 HostBridge 真实调用链禁替身验收散落成容易漏跑的单项命令。
|
||||
- 2026-06-19 原生壳临时替身扫描范围:`npm run check:native-shells` 的生产替身词扫描必须覆盖微信小程序壳生产 `.js`、Expo / Tauri 壳源码与配置、共享 HostBridge 契约、H5 native transport,并自动覆盖已接入真实宿主能力 facade 的 H5 生产调用链文件;新增 H5 调用点接入 HostBridge 时,必须让自动扫描覆盖对应文件或在同等门禁中证明生产代码没有 mock / fake / stub / TODO / FIXME / 模拟 / 伪造。
|
||||
- 2026-06-18 微信壳桥接层纳入统一验收:`npm run check:native-shells` 还会运行 `miniprogram/host-bridge/`、`miniprogram/shell/`、`pages/web-view` 样式和 `scripts/miniprogram-web-view-auth.test.ts` 的微信壳测试,覆盖 WebView 入口、登录触发、分享目标、支付结果、订阅消息结果和九宫切图行为;三端桥接层文件结构检查只证明目录边界,行为回归必须由同一门禁中的微信壳测试证明。
|
||||
|
||||
@@ -965,6 +965,52 @@ function assertH5HostBridgeEventSubscriptionGates() {
|
||||
}
|
||||
}
|
||||
|
||||
function assertH5HostBridgePayloadBoundaries() {
|
||||
const h5HostBridgeSource = fs.readFileSync(
|
||||
'src/services/host-bridge/hostBridge.ts',
|
||||
'utf8',
|
||||
);
|
||||
|
||||
for (const sharedBoundary of [
|
||||
'HOST_BRIDGE_TEXT_MIME_TYPES',
|
||||
'HOST_BRIDGE_DOCUMENT_MIME_TYPES',
|
||||
'HOST_BRIDGE_IMAGE_MIME_TYPES',
|
||||
'HOST_BRIDGE_AUDIO_MIME_TYPES',
|
||||
]) {
|
||||
if (!h5HostBridgeSource.includes(`${sharedBoundary},`)) {
|
||||
throw new Error(
|
||||
`H5 HostBridge facade must import shared payload boundary ${sharedBoundary}`,
|
||||
);
|
||||
}
|
||||
if (new RegExp(`const ${sharedBoundary}\\s*=\\s*new Set`).test(h5HostBridgeSource)) {
|
||||
throw new Error(
|
||||
`H5 HostBridge facade must not redeclare shared payload boundary ${sharedBoundary}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
for (const mimeLiteral of [
|
||||
"'text/plain'",
|
||||
"'text/markdown'",
|
||||
"'text/csv'",
|
||||
"'application/json'",
|
||||
"'image/png'",
|
||||
"'image/jpeg'",
|
||||
"'image/webp'",
|
||||
"'audio/mpeg'",
|
||||
"'audio/mp4'",
|
||||
"'audio/wav'",
|
||||
"'audio/ogg'",
|
||||
"'audio/webm'",
|
||||
]) {
|
||||
if (h5HostBridgeSource.includes(mimeLiteral)) {
|
||||
throw new Error(
|
||||
`H5 HostBridge facade must read MIME boundaries from shared contract instead of ${mimeLiteral}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function extractDocumentCapabilityList(source, marker) {
|
||||
const markerIndex = source.indexOf(marker);
|
||||
if (markerIndex === -1) {
|
||||
@@ -1613,6 +1659,9 @@ assertWechatMiniProgramCapabilityFlows();
|
||||
console.log('[check:native-shells] h5-host-bridge-event-subscription-gates');
|
||||
assertH5HostBridgeEventSubscriptionGates();
|
||||
|
||||
console.log('[check:native-shells] h5-host-bridge-payload-boundaries');
|
||||
assertH5HostBridgePayloadBoundaries();
|
||||
|
||||
console.log('[check:native-shells] production-shell-dev-scaffold-scan');
|
||||
assertNoProductionShellDevScaffoldTerms();
|
||||
|
||||
|
||||
@@ -13,13 +13,9 @@ import type {
|
||||
FileImportImageResult,
|
||||
FileImportTextResult,
|
||||
HapticsImpactPayload,
|
||||
HostBridgeAudioMimeType,
|
||||
HostBridgeCapability,
|
||||
HostBridgeDocumentMimeType,
|
||||
HostBridgeImageMimeType,
|
||||
HostBridgeMethod,
|
||||
HostBridgeRuntimeResult,
|
||||
HostBridgeTextMimeType,
|
||||
LocalNotificationPayload,
|
||||
NavigationCanGoBackEventPayload,
|
||||
NetworkStatusResult,
|
||||
@@ -29,9 +25,12 @@ import type {
|
||||
ShareOpenPayload,
|
||||
} from '../../../packages/shared/src/contracts/hostBridge';
|
||||
import {
|
||||
HOST_BRIDGE_AUDIO_MIME_TYPES,
|
||||
HOST_BRIDGE_DOCUMENT_MIME_TYPES,
|
||||
HOST_BRIDGE_IMAGE_MIME_TYPES,
|
||||
HOST_BRIDGE_NATIVE_APP_QUERY,
|
||||
HOST_BRIDGE_RUNTIME_CONTEXT_QUERY_KEY,
|
||||
HOST_BRIDGE_TEXT_MIME_TYPES,
|
||||
HOST_BRIDGE_WECHAT_MINI_PROGRAM_SOURCE_QUERY,
|
||||
isHostBridgeCapability,
|
||||
normalizeHostBridgeBadgeCount,
|
||||
@@ -835,11 +834,7 @@ export async function exportHostAudioFile(
|
||||
}
|
||||
}
|
||||
|
||||
const HOST_BRIDGE_IMAGE_MIME_TYPES = new Set<HostBridgeImageMimeType>([
|
||||
'image/png',
|
||||
'image/jpeg',
|
||||
'image/webp',
|
||||
]);
|
||||
const HOST_BRIDGE_IMAGE_MIME_TYPE_SET = new Set(HOST_BRIDGE_IMAGE_MIME_TYPES);
|
||||
|
||||
function normalizeHostImageImportResult(
|
||||
payload: FileImportImageResult | null | undefined,
|
||||
@@ -850,7 +845,7 @@ function normalizeHostImageImportResult(
|
||||
payload.action !== 'selected' &&
|
||||
payload.action !== 'dropped' &&
|
||||
payload.action !== 'captured' ||
|
||||
!HOST_BRIDGE_IMAGE_MIME_TYPES.has(payload.mimeType) ||
|
||||
!HOST_BRIDGE_IMAGE_MIME_TYPE_SET.has(payload.mimeType) ||
|
||||
typeof payload.fileName !== 'string' ||
|
||||
!payload.fileName.trim() ||
|
||||
typeof payload.base64Data !== 'string' ||
|
||||
@@ -950,15 +945,8 @@ export async function scanHostQrCode() {
|
||||
}
|
||||
}
|
||||
|
||||
const HOST_BRIDGE_TEXT_MIME_TYPES = new Set<HostBridgeTextMimeType>([
|
||||
'text/plain',
|
||||
'text/markdown',
|
||||
'text/csv',
|
||||
'application/json',
|
||||
]);
|
||||
const HOST_BRIDGE_DOCUMENT_MIME_TYPE_SET = new Set<HostBridgeDocumentMimeType>(
|
||||
HOST_BRIDGE_DOCUMENT_MIME_TYPES,
|
||||
);
|
||||
const HOST_BRIDGE_TEXT_MIME_TYPE_SET = new Set(HOST_BRIDGE_TEXT_MIME_TYPES);
|
||||
const HOST_BRIDGE_DOCUMENT_MIME_TYPE_SET = new Set(HOST_BRIDGE_DOCUMENT_MIME_TYPES);
|
||||
|
||||
function normalizeHostTextImportResult(
|
||||
payload: FileImportTextResult | null | undefined,
|
||||
@@ -967,7 +955,7 @@ function normalizeHostTextImportResult(
|
||||
!payload ||
|
||||
typeof payload !== 'object' ||
|
||||
payload.action !== 'selected' ||
|
||||
!HOST_BRIDGE_TEXT_MIME_TYPES.has(payload.mimeType) ||
|
||||
!HOST_BRIDGE_TEXT_MIME_TYPE_SET.has(payload.mimeType) ||
|
||||
typeof payload.fileName !== 'string' ||
|
||||
!payload.fileName.trim() ||
|
||||
typeof payload.content !== 'string' ||
|
||||
@@ -1057,13 +1045,7 @@ export async function importHostDocumentFile() {
|
||||
}
|
||||
}
|
||||
|
||||
const HOST_BRIDGE_AUDIO_MIME_TYPES = new Set<HostBridgeAudioMimeType>([
|
||||
'audio/mpeg',
|
||||
'audio/mp4',
|
||||
'audio/wav',
|
||||
'audio/ogg',
|
||||
'audio/webm',
|
||||
]);
|
||||
const HOST_BRIDGE_AUDIO_MIME_TYPE_SET = new Set(HOST_BRIDGE_AUDIO_MIME_TYPES);
|
||||
|
||||
function normalizeHostAudioImportResult(
|
||||
payload: FileImportAudioResult | null | undefined,
|
||||
@@ -1072,7 +1054,7 @@ function normalizeHostAudioImportResult(
|
||||
!payload ||
|
||||
typeof payload !== 'object' ||
|
||||
payload.action !== 'selected' ||
|
||||
!HOST_BRIDGE_AUDIO_MIME_TYPES.has(payload.mimeType) ||
|
||||
!HOST_BRIDGE_AUDIO_MIME_TYPE_SET.has(payload.mimeType) ||
|
||||
typeof payload.fileName !== 'string' ||
|
||||
!payload.fileName.trim() ||
|
||||
typeof payload.base64Data !== 'string' ||
|
||||
|
||||
Reference in New Issue
Block a user