Skip to content

fix(#71): 模块验签改部署期公钥注入(B 方案)+ 壳桥挂载时机修复 - #72

Merged
HandyWote merged 4 commits into
m0/devfrom
m0/issue-71
Sep 8, 2026
Merged

fix(#71): 模块验签改部署期公钥注入(B 方案)+ 壳桥挂载时机修复#72
HandyWote merged 4 commits into
m0/devfrom
m0/issue-71

Conversation

@HandyWote

Copy link
Copy Markdown
Contributor

修复 issue #71 真机验收发现的两个缺陷。目标分支 m0/dev,rebase 合并。

根因与修法

根因①:模块验签跨 Worker 拉 core JWKS 被 CF 同 zone 禁令拦截 → /api/count 恒 401

链路:壳发 token → 模块 Worker 运行时 fetch https://<domain>/.well-known/jwks.json 验签 → CF 平台规则「同一 zone 内 Routes 不能作为 fetch() 目标」→ jose 拉 JWKS 失败 → 模块回 401「token invalid or expired」。子域(Custom Domain)形态下该 fetch 合法,B 方案切 zone 路径路由后变非法。

修法(用户拍板的 B 方案:部署期公钥注入,不用 service binding)

  • packages/module-sdk/src/verify.tsverifyModuleToken(token, { coreJwksJson, audience }) 改本地验签 —— createLocalJWKSet(JSON.parse(coreJwksJson)) + jwtVerify(aud 锁定不变);删除远程 JWKS 路径(唯一使用方 hello,杜绝死代码)。未来模块共用此工具。
  • modules/hello/src/index.ts:Bindings CORE_JWKS_URLCORE_JWKS_JSON?: stringrequireAuth/createAuthMiddleware 走 SDK 本地验签;未注入 → 503 jwks not provisioned(不再把「拿不到钥匙」谎报成 401)。
  • deploy/cloudflare/src/assemble.tsmoduleWranglerConfig 增必填 jwksJsonvars.CORE_JWKS_JSON;删除 jwksUrl/workers.dev 占位分支。
  • deploy/cloudflare/src/steps.ts:步骤④模块部署前解析公钥,三级顺序 —— 本运行刚生成 keypair → 内存 public JWK(首部署,不抓公网);否则 GET https://<domain>/.well-known/jwks.json(部署器在公网,无同 zone 问题);失败 → 硬报错(提示 DNS/路由未就绪可重跑)。
  • deploy/cloudflare/src/keypair.ts:导出 publicJwksJson(pair),与 core toPublicJwks 同形状(含 kid)。
  • 换钥 = wrangler secret put JWT_PRIVATE_KEY 更新 → 重跑部署即把新公钥同步进各模块 vars(部署器本就是密钥真值持有者)。

根因②:壳桥挂载时机错误 → 「模块加载失败」概率性出现

watch(activeModule) 默认 pre-flush,回调执行时 iframe 尚未进 DOM → frameEl 为 null → 误判「模块入口配置无效」;retryFrame 不重载 iframe,模块 ready 消息早已发出无人接收。

修法(实现级时机修复)

  • watch(activeModule){ flush: 'post' },回调内 await nextTick() 兜底确认 frameEl 非空;iframe 未挂载时不再静默判 failed,等模板 ref 就位后再挂桥(waitForFrameEl)。仅 frameOriginFor(...) === null(入口配置真无效)才失败。
  • retryFrame 自增 frameReload 并并入 frameKey → iframe 按 key 重挂 → 模块重新发 ready,消息不再丢失;重挂后对新帧重新挂桥。

验收对照表

# issue 验收标准 落地证据
1 hello 验签集成:真 keypair→200;坏 token→401;缺 CORE_JWKS_JSON→503;stub fetch 抛错仍 200;换回远程→红→还原绿 modules/hello/test/hello.test.ts(12 用例);零网络用例 expect(fetchSpy).toHaveBeenCalledTimes(0) 且 200;破坏性红测见下
2 steps 集成:内存 keypair / 公网抓取 / 抓取失败硬报错三分支 deploy/cloudflare/test/steps.test.ts 分支 A/B/C(A 断言 fetchJwks 零调用 + 产物合法 JWKS;B 断言收到 baseUrl + vars 原样;C 断言 rejects 含「无法获取 Core 公钥」「可重跑部署(幂等)」)
3 装配:模块配置产物含 CORE_JWKS_JSON 且 shape 合法;删除 workers.dev 分支 deploy/cloudflare/test/assemble.test.ts 断言 keys[0]kty/crv/x/y/kid/use/algCORE_JWKS_URL 不存在;moduleWranglerConfig 已删占位分支
4 壳组件级测试(@vue/test-utils + jsdom):桥在 iframe 挂载后 attach;retry 触发 frameKey 变化;改回旧实现→红→还原绿 新增 apps/shell/src/App.test.ts(jsdom + 真实 attachModuleBridge,未 mock 桥):ready→fetchModuleToken('hello')→帧就位;retry 断言 iframe DOM 元素被替换;破坏性红测见下
5 三件套绿 pnpm -r typecheck && pnpm -r test && pnpm -r build 全绿(contracts 23 / ui 7 / module-sdk 39 / core-api 74 / shell 41 / deploy 68 / hello 12)
6 PR 中文描述 + 对照表,目标 m0/dev,rebase 不自行合并 本 PR;不合并

破坏性回验(红 → 绿,均实测)

场景 临时改回旧实现 结果
零网络验签 verify.ts 改回 createRemoteJWKSet hello 测试 6 failed / 6 passed(零网络用例 network must not be used)→ 还原后 12/12 绿
部署注入 assemble.ts 输出 CORE_JWKS_URL deploy 测试 3 failed / 65 passed(装配 shape + steps 分支 A/B)→ 还原后 68/68 绿
桥挂载时机 App.vue 还原旧实现(pre-flush + 立即读 frameEl 判 failed) shell 测试 2 failed(复现「模块入口配置无效」误判 + token spy 0 次)→ 还原后 41/41 绿

说明:单独把 flush: 'post' 改回 'pre' 仍绿——因为修复是两层(flush:'post' + nextTick/waitForFrameEl 兜底),单撤一层会被另一层吸收。破坏性红测按「完整还原旧实现」执行,证明力更强。

SPEC 措辞缺口(未改 docs/,以 docs 为准)

docs/PRODUCT_SPEC.md §5.3 称「Custom Domain 与 zone 路径路由两者等价」。该「等价」在服务器间通信层不成立:CF 同一 zone 内 Routes 不能作为 fetch() 目标,内部 Worker 互通需 service binding 或部署期注入(本 PR 的 B 方案),而 Docker 形态用内网 HTTP。建议后续以 docs 为准补一句限定(本 PR 不修改 docs/)。

边界

验证命令

pnpm -r typecheck && pnpm -r test && pnpm -r build

根因①:模块 Worker 运行时跨 Worker 拉 core JWKS 被 CF 同 zone 禁令拦截 → /api/count 恒 401。
B 方案:verifyModuleToken 改为入参 CORE_JWKS_JSON 字符串 + createLocalJWKSet 本地验签,
删除远程 JWKS 路径(唯一使用方 hello);hello Bindings 由 CORE_JWKS_URL 改 CORE_JWKS_JSON,
缺注入回 503 jwks not provisioned(不再谎报 401)。

测试:真 keypair 签 token→200;坏 token/aud 不符/篡改 payload→401;缺 CORE_JWKS_JSON→503;
破坏性红测:全局 fetch 抛错下合法 token 仍 200 且 fetch 零调用。

Signed-off-by: HandyWote <huangyinghui01@corp.netease.com>
moduleWranglerConfig 增必填 jwksJson → vars.CORE_JWKS_JSON,删除 jwksUrl/workers.dev 占位分支;
keypair 导出 publicJwksJson(与 core toPublicJwks 同形状);步骤④前解析公钥:
本运行新生成 keypair → 内存公钥(不抓公网);否则 GET <baseUrl>/.well-known/jwks.json;
失败硬报错并提示 DNS/路由未就绪可重跑。README 补模块 vars、换钥流程、Docker 等价注记。

测试:装配产物 CORE_JWKS_JSON shape 合法且无 CORE_JWKS_URL;steps 内存/公网/失败三分支。

Signed-off-by: HandyWote <huangyinghui01@corp.netease.com>
Signed-off-by: HandyWote <huangyinghui01@corp.netease.com>
根因②:watch(activeModule) 默认 pre-flush,回调时 iframe 未进 DOM → frameEl 为 null →
误判「模块入口配置无效」;retry 不重载 iframe,模块 ready 已发出无人接收(概率性失败)。
改 flush:'post' + nextTick 兜底,iframe 未挂载则等模板 ref 就位(不再静默 failed);
retry 自增 frameReload 并入 frameKey,强制 iframe 重挂使模块重新发 ready。

测试:jsdom 组件测试断言桥在 iframe 挂载后 attach、ready→token→帧就位、retry 替换 iframe DOM。

Signed-off-by: HandyWote <huangyinghui01@corp.netease.com>
@HandyWote
HandyWote merged commit 082ee78 into m0/dev Sep 8, 2026
1 check passed
@HandyWote
HandyWote deleted the m0/issue-71 branch September 8, 2026 15:52
HandyWote added a commit that referenced this pull request Sep 9, 2026
provisionAll 中 apps/shell/dist 一旦存在即跳过 vite build、直接 cp 复用旧产物,
源码变更被静默丢弃(真机实证:PR #72 合并重部署后线上 bundle hash 未变化)。

- 删除 dist 复用捷径:每次部署无条件重建 shell,部署器职责=始终搬运当前源码树;
- 新增 buildShell 注入口(同 steps.ts putSecret/resolveZone 模式),测试注入 fake 避免真实 vite;
- runNineSteps 转发 buildShell;steps.test.ts 注入快速 fake(真实 vite ~4.4s/次会使套件超时);
- 修正 provisionAll JSDoc 与文件头注释中 FORCE_BUILD/dist 复用措辞;
- 新增 provisionAll 用例:预置陈旧 dist + stale.marker,断言新产物入 assets/shell 且无陈旧残留。

Signed-off-by: HandyWote <huangyinghui01@corp.netease.com>
HandyWote added a commit that referenced this pull request Sep 9, 2026
provisionAll 中 apps/shell/dist 一旦存在即跳过 vite build、直接 cp 复用旧产物,
源码变更被静默丢弃(真机实证:PR #72 合并重部署后线上 bundle hash 未变化)。

- 删除 dist 复用捷径:每次部署无条件重建 shell,部署器职责=始终搬运当前源码树;
- 新增 buildShell 注入口(同 steps.ts putSecret/resolveZone 模式),测试注入 fake 避免真实 vite;
- runNineSteps 转发 buildShell;steps.test.ts 注入快速 fake(真实 vite ~4.4s/次会使套件超时);
- 修正 provisionAll JSDoc 与文件头注释中 FORCE_BUILD/dist 复用措辞;
- 新增 provisionAll 用例:预置陈旧 dist + stale.marker,断言新产物入 assets/shell 且无陈旧残留。

Signed-off-by: HandyWote <huangyinghui01@corp.netease.com>
HandyWote added a commit that referenced this pull request Sep 9, 2026
provisionAll 中 apps/shell/dist 一旦存在即跳过 vite build、直接 cp 复用旧产物,
源码变更被静默丢弃(真机实证:PR #72 合并重部署后线上 bundle hash 未变化)。

- 删除 dist 复用捷径:每次部署无条件重建 shell,部署器职责=始终搬运当前源码树;
- 新增 buildShell 注入口(同 steps.ts putSecret/resolveZone 模式),测试注入 fake 避免真实 vite;
- runNineSteps 转发 buildShell;steps.test.ts 注入快速 fake(真实 vite ~4.4s/次会使套件超时);
- 修正 provisionAll JSDoc 与文件头注释中 FORCE_BUILD/dist 复用措辞;
- 新增 provisionAll 用例:预置陈旧 dist + stale.marker,断言新产物入 assets/shell 且无陈旧残留。

Signed-off-by: HandyWote <huangyinghui01@corp.netease.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant