Skip to content

feat(seat-device): entry links in the device authorization answer (E-1) - #556

Merged
hzqian2026 merged 1 commit into
mainfrom
claude/seat-device-e1-entry-links-20261113
Nov 13, 2026
Merged

hzqian2026 merged 1 commit into
mainfrom
claude/seat-device-e1-entry-links-20261113

Conversation

@hzqian2026

@hzqian2026 hzqian2026 commented Nov 13, 2026 •

Copy link
Copy Markdown
Contributor

目标 / Goal

坐席桌面客户端设备授权(D1)的跟进片 E-1:在设备授权发起应答里下发入口地址 entryLinks(服务端设计 §8.3),让客户端不写死任何网址也能显示「去登录」「去激活」引导。地址只由 APP_URL 派生,属于 Core 的 D1 设备契约。

E-1 of the seat desktop device authorization (D1): the authorization answer now carries entryLinks, derived only from APP_URL, so the client needs no built-in website address.

变更与依赖

做了什么

文件 内容
lib/seat-device/contract.ts 新增 SEAT_DEVICE_SIGN_IN_PATH(/login)、SEAT_DEVICE_ACTIVATE_MEMBER_PATH(/activate-member)与纯函数 seatDeviceEntryLinks(origin, env)
lib/seat-device/device-routes.ts authorize 应答加 entryLinks,与 verificationUrl 用同一个已解析的 APP_URL 源
lib/seat-device/contract.test.ts 构造函数单测:只用传入的源;激活链接只在开关恰为 "true" 时出现;signUp 恒为 null
lib/seat-device/seat-device-routes.mysql.test.ts 真库路由测试:应答字段集合、敌意 Host / x-forwarded-host / x-forwarded-proto 下链接不变、激活开关、客户端传地址被拒、非 https 时 503 且不带任何链接

应答样例(占位域名)

APP_URL=https://seat.example.test,HELM_AUTH_MEMBER_ACTIVATION_ENABLED="true":

POST /api/public-auth/seat-device/device-authorizations
200  Cache-Control: no-store
{
  "requestId": "cm0examplerequestid",
  "pollToken": "hsdp_<43 个字符>",
  "pairingCode": "BCDF-GHJK",
  "verificationUrl": "https://seat.example.test/device",
  "expiresInSeconds": 600,
  "intervalSeconds": 5,
  "entryLinks": {
    "signIn": "https://seat.example.test/login",
    "activateMember": "https://seat.example.test/activate-member",
    "signUp": null
  }
}
  • entryLinks 三个键总是都在;值为字符串或 null。
  • activateMember:成员激活页 /activate-member 只在 HELM_AUTH_MEMBER_ACTIVATION_ENABLED === "true" 时存在,否则 404;所以开关不开时为 null,不下发一个打不开的地址。
  • signUp:本片恒为 null(理由见「与设计不一致处」)。客户端按设计 §8.4 显示「请在网站注册」,不显示链接。
  • 所有链接都与 verificationUrl 同源(即 APP_URL 的源),客户端「与 authBaseUrl 同源」的检查可以直接复用。
  • 客户端会丢弃多余字段(D1 §4.8),这是兼容的新增;只用 verificationUrl 的旧客户端不受影响。

错误码与 HTTP 状态(发起端点,供客户端对齐;本片未新增)

情形 状态 体 头
HELM_SEAT_DEVICE_AUTH_ENABLED 不为 "true";或非 POST 方法 404 空 no-store
请求体不是 JSON、超过 4096 字节、字段不全或带多余字段(含任何地址字段) 400 {"error":{"code":"invalid_input"}} no-store
clientType 不是 seat_desktop 400 {"error":{"code":"client_not_supported"}} no-store
同一可信来源段 10 分钟内超过 10 个待批请求,或总量上限 429 {"error":{"code":"rate_limited"}} no-store、Retry-After
APP_URL 缺失、不是 https(本地开发的回环地址除外)、带路径 / 查询串 / 账号;配对码重抽耗尽;其他意外失败 503 {"error":{"code":"temporarily_unavailable"}} no-store、Retry-After

非 https 时失败即关:APP_URL 在写库之前解析,解析失败直接 503,不建待批行,verificationUrl 与 entryLinks 都不下发(已有测试扩充为断言整个应答体只有错误码)。

逐项对照(任务书 / 设计 §8)

项 落实 依据 / 测试
发起应答下发 entryLinks:登录、激活、注册 是:signIn、activateMember、signUp 三键,与设计 §8.3 第 2 条的字段名一致 真库:字段集合与值
配对确认页 verificationUrl 保持在应答顶层,原样不动(客户端缺它会整份拒收);不在 entryLinks 里重复 既有测试保留
地址一律由 APP_URL 派生,与请求 Host 无关(设计 §8.4) 是:用 resolveTrustedPublicOrigin(process.env) 的结果,不读 request.url、Host、转发头 真库:post() 带敌意 Host / 转发头,链接仍是 APP_URL
只用 Core 现有路径 是:/login、/activate-member 两页都已在 Core 单测
非 https 失败即关(设计 §8.4:503,不下发错误地址) 是:沿用已有的写库前解析,503 temporarily_unavailable 真库:503 应答体恰为错误码;待批行数不变
不接受客户端传入的地址 是:请求 schema 是 strict,entryLinks、signUpUrl、verificationUrl 等字段一律 400 真库
默认关闭,跟随 HELM_SEAT_DEVICE_AUTH_ENABLED 是:开关检查在读请求体之前,未改 既有 device-routes.test.ts
授权页 / 登录页上的「去注册」「去激活」链接(设计 §8.3 第 1 条「首选」) 本片不做,见下 —

与设计不一致处

  1. signUp 恒为 null,不新增 HELM_SEAT_DEVICE_SIGNUP_URL。 设计 §8.3 把它写成「提议」的部署配置,允许与 APP_URL 不同源;任务书要求「地址一律由 APP_URL 派生」,任务书优先。Core 没有注册页。字段先占位为 null,以后加配置是兼容变更。
  2. 本地开发的回环地址例外。 NODE_ENV 为 development / test 时,resolveTrustedPublicOrigin 仍接受 http://localhost 等回环地址,链接随之为 http;这与 2a 的 verificationUrl 一致,也对应客户端「未打包的开发运行允许回环地址」。生产环境非 https 一律 503。
  3. 登录页、/device 页上的引导链接不在本片。 未登录时访问 /device 会先跳到 /login;而 /login 不受设备开关控制,在那里加链接会改动所有站点的登录页。是否做、怎么做留给 owner 定。

变异(9 个,全部变红,逐个还原;结束时工作区干净)

# 变异 被哪条测试抓到
M1 链接用 request.url 的源 真库:APP_URL 用例、激活用例
M2 激活链接不看开关 单测、真库
M3 激活开关按「非空即真」判断 单测("false"、"1" 等)
M4 登录路径改为 /signin 单测、真库
M5 激活链接误用 /device 路径 单测、真库
M6 应答不带 entryLinks 真库:字段集合
M7 APP_URL 无效时退回请求的源(失败即开) 真库:503 用例
M8 发起请求 schema 由 strict 改为 strip(接受客户端地址字段) 真库:地址字段用例、既有多余字段用例;单测
M9 signUp 取自环境变量 单测、真库

验证证据

检查 命令 Head / 环境 结果
类型 npm run typecheck 1638dbe9,Node 25.9.0 exit 0
Lint npm run lint:strict 同上 exit 0
边界 npm run check:boundaries 同上(pre-commit / pre-push 钩子也执行) exit 0
公开文档 npm run check:public-docs 同上 exit 0
公开发布 npm run check:public-release 同上 exit 0
历史扫描 npm run check:secret-history 同上 exit 0
真库 npm run test:seat-device:mysql(SEAT_DEVICE_MYSQL_REQUIRED=1,缺库即失败) 同上;自建临时 MySQL 8.0.36,私有 socket、关网络、退出回收 5 文件 78 用例全过
全量单测 npm run test 同上 4 用例失败,均为 scripts/sarp-proof.test.ts、scripts/golden-path-proof.test.ts 的「输出目录不能在临时目录内」:本地工作树本身就在系统临时目录下,与本 PR 无关;exact-head CI 的 Test 作业「Run unit tests」通过(run 43378230780),即这 4 条在 CI 上通过
CI .github/workflows/ci.yml 的「Verify the seat device authorization store against MySQL」步骤运行 npm run test:seat-device:mysql;单测由 npm run test 全量运行 1638dbe9 20/20 check-run success;CI run 43378230780 中 Member Gateway MySQL 作业的「Verify the seat device authorization store against MySQL」步骤与 Test 作业「Run unit tests」均 success

| 双语混写 | npm run check:bilingual-mixing | 同上 | exit 0,无新增(21 条均在基线内) |

未运行命令及原因 / Commands not run and why:

  • npm run build、npm run e2e、npm run self-check、npm run quality:regression、npm run db:reset:本地未运行。本片只改一个 API 应答的字段与对应测试,无页面、无构建配置、无迁移;构建与端到端以 CI 结果为准。

  • 新增 test:*:无(新用例落在已接入 CI 的既有测试文件里)。

审计自闭环

  • ① 实现:组装后路径 POST /api/public-auth/seat-device/device-authorizations;链接只取已解析的 APP_URL 源;无权限、幂等、SQL collation 变化。
  • ② 测试:单测 1 条、真库路由测试新增 1 条并扩充 2 条;负向:敌意 Host / 转发头、客户端地址字段、非 https、激活开关关闭。
  • ② CI:用例在 seat-device-routes.mysql.test.ts,CI 链为 ci.yml「Verify the seat device authorization store against MySQL」→ npm run test:seat-device:mysql → vitest;SEAT_DEVICE_MYSQL_REQUIRED=1,缺库报错不跳过。单测由 npm run test 全量覆盖。
  • ③ 登记:N/A。无新表、迁移、env;读取的 HELM_AUTH_MEMBER_ACTIVATION_ENABLED 已登记在 .env.example。
  • ③ 密钥:N/A。
  • ④ 审计:check:public-release、check:public-docs、check:secret-history、check:bilingual-mixing 均 PASS,无新增命中。
  • ④ 安全:基线未改;无豁免;示例只用 example.test。
  • ⑤ 收口:本 PR 未引入门禁回归(本地 npm run test 的 4 条失败见上表)。
  • ⑤ 证据:exact-head 1638dbe9 CI 20/20 check-run success(含真库步骤与全量单测)。
  • 审计:check:public-release、check:secret-history 无新增命中;示例只用 example.test 占位域名。

规模

4 个文件,+72 / −5(含测试)。

边界检查 / Boundary Check

  • 不包含客户名称、私人联系方式、私有域名、内网 IP、凭据、私有部署证据,或商业 Pack / Overlay 实现细节。
  • 本 PR 不引入自动对外发送、广泛自动写入、自动审批、执行、结算或客户承诺路径。
  • N/A:不改客户可见措辞。
  • N/A:不触及 integration、connector 或 data ingest。

风险、回滚与交付边界

  • 回滚:回退本提交即可;无数据、无迁移、无 env。客户端丢弃未知字段,回退后只是没有 entryLinks。
  • 状态:仅代码 PR。未合入、未部署、未改任何环境配置;租户可用性不变(开关默认关闭)。
  • 未验证:真实客户端对 entryLinks 的读取(客户端尚未实现,接口清单 §9.4);Windows 与安装包环境。

评审与合并纪律 / Review And Merge Discipline

  • 通过 PR 合入,不直接推 main;作者不合并。
  • 未使用 --no-verify、未跳过 CI 或本地硬门禁。

截图或录屏 / Screenshots Or Recording

N/A(无 UI 变化)。

贡献权利 / Contribution Rights

  • 我有权按 Apache-2.0 贡献该工作。
  • 无第三方代码或资产。

Owner Gate

  • Owner decision needed: no(本片可单独合入);后续两个选择题见作者回报。

🤖 Generated with Claude Code

https://claude.ai/code/session_01XEu1dNhDYNsyuhABjFw6Tn

The authorization answer now carries entryLinks {signIn, activateMember,
signUp} next to verificationUrl (server design §8.3). All links come from
the resolved APP_URL origin, never from the request; a non-https APP_URL
still answers 503 before anything is stored, so no link leaves. The
activation link is null while its page is switched off; signUp stays null
because Core has no sign-up page and no sign-up address is configured.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XEu1dNhDYNsyuhABjFw6Tn
@hzqian2026
hzqian2026 merged commit 107c1fa into main Nov 13, 2026
20 checks passed
@hzqian2026
hzqian2026 deleted the claude/seat-device-e1-entry-links-20261113 branch November 13, 2026 07:57
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