在动手改不熟悉的代码之前,先让 Claude 把你"不知道自己不知道"的东西——未知的未知(unknown unknowns)——摆到台面上,并告诉你针对每个盲区该怎么写提示词。
I'm adding a new SSO auth provider to Acme but I've never touched the auth module. Do a blindspot pass: find my unknown unknowns in this part of the codebase, explain each one, and tell me how to prompt you better for the implementation.
中文大意:我要给 Acme 新增一个 SSO 认证提供方,但我从没碰过 auth 模块。请做一次盲点扫描:找出我在这部分代码库里的未知的未知,逐一解释,并告诉我在实现阶段该如何更好地向你提问。
已扫描 services/auth/(47 个文件)、2025 年 1 月以来涉及它的 214 次提交、3 个进行中的迁移、2 次被回滚的尝试。以下七件事,本来要靠踩坑才能学会。
"新增一个 SSO 提供方"——听起来无非是:实现一个接口、注册它、写好重定向处理器,收工。放在干净的代码库里,这是两天的活儿。
一个会话存储迁移到一半的模块;一个悄悄绕过了中间件的提供方,而所有人都默认那层中间件在跑;一次做过同样任务却被回滚的历史尝试;还有一套从提供方接口完全看不见的身份关联(identity linking)模型。
Acme 正在把会话从 Postgres 迁移到 Redis(MIG-118,三月起就停滞了)。目前每次会话写入都经过 SessionBridge.write(),再扇出到 RedisSessionStore 和旧的 pg_sessions 表两边。迁移一直没做完,所以读取仍然走 Postgres。
RedisSessionStore——它看着最正统,代码最新,文档也最好。直接往它里面写,登录在 dev 环境看似正常(那里开启了 Redis 读取),到了生产环境就悄悄失效,因为生产的读取仍然打到 Postgres。除 SAML 外,每个提供方都挂载在 authPipeline 之下,由它负责限流、审计日志和设备信任检查。saml/handler.ts 却把自己的 ACS 端点直接挂在路由器上——2024 年 SP 发起的 POST 回调冲垮了 CSRF 层,之后再没人回头收拾。文件里还留着一条 14 个月前的 // TODO(priya): fold back into pipeline。
PR #2841("Add Okta OIDC provider",2025 年 9 月)合并九天后被回滚。回滚提交里写明了原因:Acme 的回调 URL 必须带租户后缀(/auth/callback/:provider/:workspaceSlug),工作区级邀请链接才能在重定向的往返中存活。而 Okta 严格的 redirect-URI 匹配拒绝了动态后缀,被邀请的用户落进了错误的工作区。大家公认的修法——用 AUTH_STATE_SECRET 签名的 state 参数工作区声明——在回滚讨论串里设计好了,却从未落地。
Acme 把 users 和 identities 分开:一个账号可以关联多个身份(邮箱+密码、Google、SAML),在 identities 表里做关联。提供方接口只返回一份 profile;关联决策发生在 identity/linker.ts,以已验证邮箱为键。无法保证邮箱已验证的提供方(很多 SSO 身份提供商(IdP)都保证不了)必须设置 requiresManualLink: true,否则 linker 会痛快地把共用同一个企业邮箱别名的两个人合并成一个。
刷新令牌轮换由 flags.yaml 里的 auth.refresh_rotation 控制。它在 dev 和 staging 默认为 true,在生产为 false(五月一次时钟偏移事故后,灰度发布被暂停了)。开关关闭时,生产会话的寿命恰好是 ACCESS_TOKEN_TTL(12 小时),到点硬过期——根本不存在刷新路径。
编译通过不等于提供方已经生效。它必须:(1)在 providers/registry.ts 里注册;(2)通过一次迁移写进 auth_providers 表——管理后台读的是数据库,不是 registry;(3)加进 __tests__/provider-contract.spec.ts 的 fixture 列表,共享契约套件(state 校验、防重放、登出扇出)会对每个已注册提供方跑一遍。registry 和 fixture 列表不一致时,CI 会直接失败。
会话吊销会在内部事件总线上发布 auth.session.revoked。实时审片播放器持有自己的长效播放授权,只有消费到这个事件才会释放。logout.ts 已经替现有提供方处理好了这件事,但 SSO 单点登出(single logout,SLO)的回调走的是另一条路径,必须自己发布该事件。
Add a new SSO auth provider to Acme. Constraints from the blindspot pass: 1. Sessions: route all session writes through SessionBridge (MIG-118 is unfinished; prod still reads from pg_sessions). Never touch RedisSessionStore directly. 2. Template: base the structure on oauth/google.ts, NOT saml/handler.ts — SAML bypasses authPipeline. Mount all new routes inside authPipeline and show me the mount point. 3. History: read the revert of PR #2841 first. Implement the signed state-parameter workspace claim (AUTH_STATE_SECRET) instead of tenant-suffixed callback URLs, and test the workspace-invite flow. 4. Identity linking: walk me through identity/linker.ts before coding. If this IdP doesn't guarantee verified emails, set requiresManualLink and build the confirmation path — do not auto-link. 5. Flags: implement against auth.refresh_rotation=false (prod). Sessions hard-expire at 12h; no refresh path. Note what changes if rotation ships. 6. Registration: all three steps — providers/registry.ts, an auth_providers migration, and the provider-contract fixture list. Show the contract suite green. 7. Logout: every revocation path must publish auth.session.revoked, and add a test that the review player's playback grant is released on IdP-initiated logout. Work in this order: linker walkthrough → provider skeleton → callback + state claim → registration → SLO + events → contract tests. Stop and show me the plan after the walkthrough before writing code.
这七条约束,今天早上的你一条也写不出来——每一条都是别人用半天时间换来的。这正是盲点扫描的意义。