收紧移动壳通知权限边界
移动壳 Android 包配置阻断重启和精确定时通知权限 移动壳配置检查拒绝远程推送 token 和通知响应监听流程 宿主壳方案和共享决策记录即时本地通知边界
This commit is contained in:
@@ -52,7 +52,10 @@
|
||||
"allowBackup": false,
|
||||
"softwareKeyboardLayoutMode": "resize",
|
||||
"blockedPermissions": [
|
||||
"android.permission.RECORD_AUDIO"
|
||||
"android.permission.RECORD_AUDIO",
|
||||
"android.permission.RECEIVE_BOOT_COMPLETED",
|
||||
"android.permission.SCHEDULE_EXACT_ALARM",
|
||||
"android.permission.USE_EXACT_ALARM"
|
||||
],
|
||||
"adaptiveIcon": {
|
||||
"foregroundImage": "./assets/icon.png",
|
||||
|
||||
@@ -38,6 +38,12 @@ const devScaffoldTerms = [
|
||||
'模' + '拟',
|
||||
'伪' + '造',
|
||||
];
|
||||
const blockedAndroidPermissions = [
|
||||
'android.permission.RECORD_AUDIO',
|
||||
'android.permission.RECEIVE_BOOT_COMPLETED',
|
||||
'android.permission.SCHEDULE_EXACT_ALARM',
|
||||
'android.permission.USE_EXACT_ALARM',
|
||||
];
|
||||
|
||||
function extractStringArrayExport(source, exportName) {
|
||||
const match = source.match(
|
||||
@@ -267,18 +273,16 @@ if (appConfig.android?.softwareKeyboardLayoutMode !== 'resize') {
|
||||
throw new Error('mobile shell Android keyboard layout must resize the WebView');
|
||||
}
|
||||
|
||||
if (
|
||||
!appConfig.android?.blockedPermissions?.includes(
|
||||
'android.permission.RECORD_AUDIO',
|
||||
)
|
||||
) {
|
||||
throw new Error('mobile shell Android package must block microphone permission');
|
||||
for (const permission of blockedAndroidPermissions) {
|
||||
if (!appConfig.android?.blockedPermissions?.includes(permission)) {
|
||||
throw new Error(`mobile shell Android package must block ${permission}`);
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
appConfig.android?.permissions?.includes('android.permission.RECORD_AUDIO')
|
||||
) {
|
||||
throw new Error('mobile shell Android package must not request microphone');
|
||||
for (const permission of blockedAndroidPermissions) {
|
||||
if (appConfig.android?.permissions?.includes(permission)) {
|
||||
throw new Error(`mobile shell Android package must not request ${permission}`);
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
@@ -398,6 +402,19 @@ if (Array.isArray(notificationsPlugin)) {
|
||||
}
|
||||
}
|
||||
|
||||
for (const forbiddenNotificationSnippet of [
|
||||
'getExpoPushTokenAsync',
|
||||
'getDevicePushTokenAsync',
|
||||
'addPushTokenListener',
|
||||
'addNotificationResponseReceivedListener',
|
||||
]) {
|
||||
if (bridgeSource.includes(forbiddenNotificationSnippet)) {
|
||||
throw new Error(
|
||||
`mobile shell must not register remote or background notification flow: ${forbiddenNotificationSnippet}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
for (const snippet of [
|
||||
'file.exportText',
|
||||
'file.importText',
|
||||
|
||||
@@ -130,6 +130,17 @@ assertIncludes(
|
||||
'android.permission.RECORD_AUDIO',
|
||||
'Android blocked permissions',
|
||||
);
|
||||
for (const permission of [
|
||||
'android.permission.RECEIVE_BOOT_COMPLETED',
|
||||
'android.permission.SCHEDULE_EXACT_ALARM',
|
||||
'android.permission.USE_EXACT_ALARM',
|
||||
]) {
|
||||
assertIncludes(
|
||||
expoConfig.android?.blockedPermissions,
|
||||
permission,
|
||||
'Android background notification blocked permissions',
|
||||
);
|
||||
}
|
||||
assertEqual(
|
||||
expoConfig.android?.adaptiveIcon?.foregroundImage,
|
||||
'./assets/icon.png',
|
||||
|
||||
@@ -50,6 +50,7 @@
|
||||
- 2026-06-18 H5 背景音乐接入宿主生命周期:`useBackgroundMusic` 通过 `useHostLifecycleActive()` 消费 `subscribeHostAppLifecycle()` 的归一结果,宿主进入后台、inactive 或桌面窗口失焦时降低音量并暂停音频循环,同时 `suspend` WebAudio context;回到 `active + focused` 且用户原本开启音乐时再恢复播放,不改变用户音量设置。
|
||||
- 2026-06-18 固定玩法音频接入宿主生命周期:前端新增 `useHostLifecycleActive()` 统一消费 `subscribeHostAppLifecycle()`,`useBackgroundMusic`、拼图运行态和抓大鹅运行态都只依赖该归一状态判断音频可播放性;宿主 inactive、background 或窗口失焦时暂停 `<audio>` / WebAudio,回到 `active + focused` 后仅在运行态仍在播放、音源存在且用户音乐音量大于 0 时恢复,不改变用户音量设置。
|
||||
- 2026-06-18 本地通知能力:新增 `notification.showLocal` HostBridge capability,H5 只能传必填 `title` 和可选 `body`,共享契约负责修剪、折叠普通空白、限制长度并拒绝控制字符;Expo 壳通过 `expo-notifications` 请求系统通知权限、创建 Android 本地 channel 并发送即时本地通知,Tauri 壳通过 Rust 侧 `tauri-plugin-notification` 发送系统通知且不开放插件 JS guest API。该能力不包含远程推送、token 注册、定时提醒或后台远程通知,权限拒绝、系统失败或宿主未声明时由 H5 视作失败并继续主流程。
|
||||
- 2026-06-18 移动壳通知权限边界:Expo 移动壳的 Android 包配置必须阻断 `RECEIVE_BOOT_COMPLETED`、`SCHEDULE_EXACT_ALARM` 和 `USE_EXACT_ALARM`,只保留即时本地通知所需权限和前台展示 handler;移动壳源码不得调用 Expo push token、设备 push token、push token listener 或通知响应跳转 listener。配置检查和 Expo public config smoke 会拒绝这些权限或远程 / 后台通知流程被重新打开。
|
||||
- 2026-06-18 草稿生成完成 / 失败通知:平台壳层的 `markDraftReady` / `markDraftFailed` 统一收口会在原生壳声明 `notification.showLocal` 时请求即时本地通知;通知 payload 只包含生成完成 / 失败标题和草稿来源正文,按草稿来源去重,同一草稿重新进入生成中后才允许再次通知。该能力不替代现有完成 / 错误弹窗、作品架红点、队列概览或后端状态回读,通知失败不阻断主流程。
|
||||
- 2026-06-18 剪贴板读取能力:新增 `clipboard.readText` HostBridge capability,H5 只能读取纯文本结果,契约限制返回文本最多 100000 字符;Expo 壳通过 `expo-clipboard` 读取系统剪贴板文本,Tauri 壳通过 Rust 侧 `tauri-plugin-clipboard-manager` 读取文本且不开放插件 JS guest API。该能力不读取图片、HTML、文件列表或剪贴板监听事件,宿主未声明或读取失败时由 H5 视作失败并保留原流程。
|
||||
- 2026-06-18 文本文件导入能力:新增 `file.importText` HostBridge capability,H5 统一通过 `importHostTextFile()` 读取宿主返回的纯文本内容;Expo 壳通过 `expo-document-picker` 打开系统文档选择器,Tauri 壳通过系统文件选择框读取真实文本文件。两端只接受 `text/plain`、`text/markdown`、`text/csv`、`application/json` 或对应扩展名,单次不超过 5 MiB,成功只返回清洗后的文件名、MIME、UTF-8 文本内容和字节数,不暴露设备 URI / 本机绝对路径,也不开放通用文件系统。
|
||||
|
||||
@@ -238,6 +238,7 @@ GameBridge 禁止:
|
||||
- Tauri 主 WebView 禁止默认下载落盘;桌面文件保存只能通过受控 HostBridge 导出能力进入系统保存对话框。
|
||||
- Tauri 主 WebView 禁止默认打开 DevTools;不得通过配置或 Cargo feature 为分发壳启用浏览器检查器。
|
||||
- RN WebView 禁止打开任意 URL 后仍保留完整 HostBridge;跳外链只允许 `http:`、`https:`、`mailto:`、`tel:`,并使用系统浏览器或降级能力,危险协议直接阻断。
|
||||
- Expo 移动壳的通知能力只覆盖即时本地通知;Android 包配置必须阻断重启后通知恢复和精确定时权限,前端代码不得注册 Expo push token、远程推送监听或通知响应跳转流程。
|
||||
- AI sandbox iframe 必须使用独立 CSP、`sandbox` 属性和单独 GameBridge allowlist。
|
||||
|
||||
## 分阶段落地
|
||||
@@ -269,6 +270,8 @@ GameBridge 禁止:
|
||||
|
||||
2026-06-18 追加:H5 的草稿生成完成 / 失败收口开始消费 `notification.showLocal`。Expo 壳仍只发送即时本地通知,不注册远程推送 token、不做定时提醒;H5 按草稿来源对完成和失败通知去重,同一草稿重新进入生成中后才允许再次通知,通知失败不阻断弹窗、作品架和后端状态回读。
|
||||
|
||||
2026-06-18 追加:移动壳 Android 包配置显式阻断 `RECEIVE_BOOT_COMPLETED`、`SCHEDULE_EXACT_ALARM` 和 `USE_EXACT_ALARM`,保留即时本地通知所需的普通通知权限和前台展示 handler,但不允许重启后恢复通知、精确定时提醒、远程推送 token 或通知响应跳转流程进入当前壳。配置检查会同时扫描 Expo public config 和移动壳通知源码,拒绝接入 `getExpoPushTokenAsync`、设备 push token、push token listener 或通知响应 listener。
|
||||
|
||||
2026-06-18 追加:H5 的作品架未读草稿生成完成更新开始消费 `app.setBadgeCount`。Expo 移动壳仍只在 iOS 声明该能力,Android 不声明、不伪造成功;H5 只同步可见作品架内未读完成草稿数量,同一草稿多恢复 ID 只计 1,宿主不支持或设置失败不影响 H5 红点与作品架状态。
|
||||
|
||||
2026-06-18 追加:H5 的平台外部生成队列概览开始消费 `network.status` / `network.statusChanged`。宿主未声明网络能力时继续按原逻辑轮询;宿主明确离线或不可达时暂停概览请求,恢复在线后重新刷新,不改变生成任务、作品架或后端回读事实。
|
||||
|
||||
Reference in New Issue
Block a user