Skip to content

fix(mobile): 恢复 SSO Canary 匿名 OTA 更新 - #3824

Closed
guyong-zapo wants to merge 1 commit into
mainfrom
fix/mobile-sso-canary-ota
Closed

fix(mobile): 恢复 SSO Canary 匿名 OTA 更新#3824
guyong-zapo wants to merge 1 commit into
mainfrom
fix/mobile-sso-canary-ota

Conversation

@guyong-zapo

Copy link
Copy Markdown
Collaborator

这次改了什么

摘要

#3359 为阻止用户同意隐私政策前发送单安装 EAS-Client-ID,把启动、回前台和手动 OTA 都绑定到了 analytics consent。企业 SSO 按既有产品约定不经过协议门、也不写 analytics consent,因此 consent=false + Canary=true 的用户会被永久挡在 OTA 之外。

本 PR 将两件事解耦:

  • 自建 OTA 的 release / canary / beta 请求都用固定共享 UUID 覆盖 EAS-Client-ID,不再发送单设备标识;Canary/Beta 继续携带 x-cindy-update-channel
  • 启动、回前台、手动检查不再依赖 analytics consent。
  • TapDB consent 逻辑保持原样;企业 SSO 不调用 acceptPrivacyConsent()
  • 增加 SSO + consent=false + Canary=true 回归测试,并守护 SSO 不进入 TapDB opt-in 路径。

Expo 会把下载更新时的 URL / requestHeaders 记录下来,并在下次启动时据此筛选 bundle。因此固定 ID 同时写入自建包原生 updates.requestHeaders,与 JS override 共用同一配置源,避免只改 JS 后出现已下载 bundle 的 header 不匹配。

变更类型

  • fix 缺陷修复
  • feat 新功能
  • refactor / perf 重构或性能优化
  • docs / test / chore 文档、测试或工程维护
  • 其他:

范围

  • 关联 Issue / 需求:fix(mobile): 同意隐私政策前禁止发起携带 eas-client-id 的 OTA 更新检查 #3359 的企业 SSO Canary OTA 后续修复
  • 本 PR 包含:自建 OTA header 匿名化;移除三条 OTA 路径的 analytics consent 闸门;原生配置与回归测试
  • 明确不包含:服务端改动;EAS / TestFlight 更新链路;TapDB consent 产品逻辑;SSO 协议豁免逻辑
  • 用户可见变化:企业 SSO 用户即使 analytics consent 为 false,也可在 Canary 通道通过启动、回前台和手动检查获得 OTA
  • 是否存在 breaking change:无 API breaking change;但自建 Mobile runtime fingerprint 会变化,需冷包交付

UI 变化

不涉及:settings.tsx 仅删除更新检查的 consent 依赖注入,没有修改渲染、交互、样式或 UI 文案。

  • 引用的设计规范:不涉及:纯更新链路与测试改动

怎么验证的

自动验证

pnpm --filter mobile run typecheck
结果:通过

pnpm test:unit:related
结果:通过。因本地 origin/main 落后而保守升级为全仓单测;test runner 502 passed / 1 skipped,所有可运行 workspace(含 Desktop、Mobile、maker-core)均通过

pnpm check:dco
结果:通过,1 个真实提交已签名

git diff --check upstream/main...HEAD
结果:通过

手工验证

不涉及:未生成签名 APK/IPA,未进行双平台真机 OTA 流程。

未执行的验证

  • Android / iOS 真机抓包未执行:需用新版自建包确认 manifest 与资源请求均只携带共享 EAS-Client-ID,且两台设备取值一致。
  • 新旧 runtime 的完整发布演练未执行:需在发布链路验证旧包经 /latest 安装新 APK/IPA 后进入新 runtime,并只接收新 runtime 的 OTA。

风险

风险分类

  • 无已知风险
  • SQLite / migration
  • system prompt
  • 协议兼容
  • 权限 / 安全 / 用户数据
  • 存量插件兼容(批准状态 / 指纹 / manifest 校验 / 安装布局 / 包格式)
  • 原生层 / fingerprint / OTA
  • 跨平台差异
  • 其他:

影响与回滚

  • 影响范围:自建 Android / iOS 的启动、回前台、手动 OTA;非自建链路不变。
  • 冷更不可避免:原生 updates.requestHeaders 必须与 JS override 保持一致,否则 Expo 可能因 requestHeaders 不匹配而不再选择已下载 bundle。该配置会形成新 runtime。
  • 存量装机:consent=false 的旧客户端不会主动检查 OTA,无法通过旧 runtime 的热更自救;必须先通过匿名 /latest 分发新版 APK/IPA,再向新 runtime 发布 OTA。
  • 发版节奏建议:先完成 Android / iOS 冷包和真机抓包,再更新 /latest,最后只向新 runtime 发布 OTA。
  • 回滚 / 降级方式:revert 本提交并重新发布冷包;不要用旧 runtime OTA 单独回滚 header 配置。
  • 合并门槛:本 PR 改变 Mobile runtime fingerprint,合并前必须取得仓库指定把关人针对冷更的明确确认。

提交前检查

  • 已 review 完整 diff
  • 每个 commit 都带 DCO 签名(git commit -s,见 DCO
  • UI 改动已在「UI 变化」注明引用的设计规范(不涉及视觉/交互)
  • 未提交凭证、令牌或授权文件
  • 已补充必要测试
  • 已确认测试结果并说明未执行项

Signed-off-by: guyong <guyong@xd.com>
@guyong-zapo
guyong-zapo requested a review from a team as a code owner September 2, 2026 17:21
@greptile-apps

greptile-apps Bot commented Sep 2, 2026

Copy link
Copy Markdown

Greptile Summary

本 PR 为自建 Mobile OTA 的 release、canary 和 beta 请求引入共享 EAS-Client-ID,并移除启动、回前台及手动检查的 analytics consent 限制。

  • 自建原生配置和 JS override 共用固定 UUID
  • Canary/Beta 继续通过 x-cindy-update-channel 分流
  • 更新流程不再依赖 analytics consent,并新增 SSO/Canary 回归测试
  • 当前手动检查的解耦范围也覆盖了未匿名化的非自建 EAS 更新链路

Confidence Score: 3/5

该 PR 暂不适合合并,因为非自建 EAS 更新的手动检查会在用户未同意隐私政策时发送单安装标识。

自建 OTA 的匿名化配置仅作用于 self-host 分支,但通用手动更新流程移除了 consent 检查,并且设置页仍允许启用了 Expo Updates 的非自建包直接请求 u.expo.dev。

Files Needing Attention: apps/mobile/src/update/manualUpdateCheck.ts, apps/mobile/app/settings.tsx, apps/mobile/app.config.js

Security Review

非自建 EAS/TestFlight 包的手动检查仍使用 Expo 生成的单安装 EAS-Client-ID;移除通用 consent 闸门后,未同意隐私政策的用户可触发该标识外发。How this was verified: 已确认设置页在非自建 Updates.isEnabled 包中直接调用 Expo 更新 API,而共享 header 的原生和运行时 override 均仅存在于自建分支。

Important Files Changed

Filename Overview
apps/mobile/src/update/manualUpdateCheck.ts 删除通用 consent 检查后,自建匿名 OTA 可以放行,但非自建手动检查也失去了隐私闸门。
apps/mobile/app/settings.tsx 手动检查仍以 Updates.isEnabled 对所有 Expo Updates 包开放,并直接调用 Expo check/fetch API。
apps/mobile/app.config.js 自建原生更新配置新增共享 EAS-Client-ID requestHeaders,非自建 EAS 配置保持原有标识行为。
apps/mobile/src/update/canaryChannelStore.ts 所有自建通道统一覆盖共享 client ID,Canary/Beta 额外保留通道 header。
apps/mobile/src/update/useStartupOtaGate.ts 自建启动 OTA 不再等待 analytics consent,并在检查前配置共享 ID 和通道 header。
apps/mobile/src/update/resumeUpdateCheck.ts 回前台 OTA 移除 consent 检查,但其 hook 仍由自建构建条件约束。
apps/mobile/src/update/otaRequestConfig.json 新增原生配置与 JS 共用的 OTA header 名称和固定共享 UUID。

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
  U[用户手动检查更新] --> S{自建更新包?}
  S -->|是| H[覆盖共享 EAS-Client-ID]
  H --> O[请求自建 OTA manifest]
  S -->|否| E[Updates.isEnabled]
  E --> N[直接请求 u.expo.dev]
  N --> I[携带原生单安装 EAS-Client-ID]
Loading

Comments Outside Diff (1)

  1. apps/mobile/src/update/manualUpdateCheck.ts, line 62 (link)

    P1 security 非自建更新泄露安装标识

    当未同意隐私政策的用户在启用了 Expo Updates 的非自建 EAS/TestFlight 包中手动检查更新时,这里不再检查 consent,并直接请求 u.expo.dev;由于共享 EAS-Client-ID 的原生与运行时 override 都只存在于自建分支,请求仍会发送 Expo 生成的单安装标识。How this was verified: 设置页在非自建包中以 Updates.isEnabled 直接调用 Expo 更新 API,而仓库中唯一的运行时 header override 和原生共享 header 均受 self-host 条件限制。

    Context Used: 使用和PR描述相同的语言进行评论 (source)

    Prompt To Fix With AI
    This is a comment left during a code review.
    Path: apps/mobile/src/update/manualUpdateCheck.ts
    Line: 62
    
    Comment:
    **非自建更新泄露安装标识**
    
    当未同意隐私政策的用户在启用了 Expo Updates 的非自建 EAS/TestFlight 包中手动检查更新时,这里不再检查 consent,并直接请求 `u.expo.dev`;由于共享 `EAS-Client-ID` 的原生与运行时 override 都只存在于自建分支,请求仍会发送 Expo 生成的单安装标识。**How this was verified:** 设置页在非自建包中以 `Updates.isEnabled` 直接调用 Expo 更新 API,而仓库中唯一的运行时 header override 和原生共享 header 均受 self-host 条件限制。
    
    **Context Used:** 使用和PR描述相同的语言进行评论 ([source](https://app.greptile.com/review/custom-context?memory=instruction-0))
    
    ---
    
    For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

    Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Prompt To Fix All With AI
### Issue 1
apps/mobile/src/update/manualUpdateCheck.ts:62
**非自建更新泄露安装标识**

当未同意隐私政策的用户在启用了 Expo Updates 的非自建 EAS/TestFlight 包中手动检查更新时,这里不再检查 consent,并直接请求 `u.expo.dev`;由于共享 `EAS-Client-ID` 的原生与运行时 override 都只存在于自建分支,请求仍会发送 Expo 生成的单安装标识。**How this was verified:** 设置页在非自建包中以 `Updates.isEnabled` 直接调用 Expo 更新 API,而仓库中唯一的运行时 header override 和原生共享 header 均受 self-host 条件限制。

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

Reviews (1): Last reviewed commit: "fix(mobile): 恢复 SSO Canary 匿名 OTA 更新" | Re-trigger Greptile

@MagicLizi MagicLizi added awaiting-discussion 等待维护者讨论(review-pr) touches:cold-update 改动影响移动端冷更新(review-pr 自动维护,仅展示) touches:product-ui 改动碰到产品 / UI 面(review-pr 自动维护,仅展示) labels Sep 2, 2026
@MagicLizi

Copy link
Copy Markdown
Contributor

这个 PR 改了 Mobile 原生 updates.requestHeaders,会形成新的 runtime fingerprint,需要维护者确认冷更是否必要、以及存量装机怎么发版。

球不在作者这边。维护者请直接在 PR 上 Approve;若要改,请 Request Changes。讨论 issue:#3825

@MagicLizi

Copy link
Copy Markdown
Contributor

命中 UI 路径(apps/mobile/app.config.js / apps/mobile/app/settings.tsx / apps/mobile/src/update/canaryChannelStore.ts 等)但 description 未附界面效果证据——建议补充改动后效果:截图/录屏,或改动后界面的 HTML 页面(```html 代码块、.html 附件或在线预览链接),便于确认界面符合 DESIGN.md 设计规范

@guyong-zapo

Copy link
Copy Markdown
Collaborator Author

按新的交付约束撤销本方案:后续改为不修改 Mobile 原生配置与 runtime fingerprint 的实现,并从最新 main 重新提交。

需要保留的边界是:纯 OTA 代码无法自动触达已被 #3359consent=false 闸门挡住的存量 SSO 客户端;替代方案会如实说明这一启动可达性限制,不把“代码可热更”表述成“受影响旧包可自动热更自救”。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

awaiting-discussion 等待维护者讨论(review-pr) touches:cold-update 改动影响移动端冷更新(review-pr 自动维护,仅展示) touches:product-ui 改动碰到产品 / UI 面(review-pr 自动维护,仅展示)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants