认识你的未知项 · 实现之前
← 全部示例

盲点扫描(Blindspot pass)

在动手改不熟悉的代码之前,先让 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 模块。请做一次盲点扫描:找出我在这部分代码库里的未知的未知,逐一解释,并告诉我在实现阶段该如何更好地向你提问。

Claude 的产出
blindspot-pass · acme/services/auth · 已扫描 47 个文件

未知的未知:Acme 的 auth 模块

已扫描 services/auth/(47 个文件)、2025 年 1 月以来涉及它的 214 次提交、3 个进行中的迁移、2 次被回滚的尝试。以下七件事,本来要靠踩坑才能学会。

你要求的

"新增一个 SSO 提供方"——听起来无非是:实现一个接口、注册它、写好重定向处理器,收工。放在干净的代码库里,这是两天的活儿。

你实际要面对的

一个会话存储迁移到一半的模块;一个悄悄绕过了中间件的提供方,而所有人都默认那层中间件在跑;一次做过同样任务却被回滚的历史尝试;还有一套从提供方接口完全看不见的身份关联(identity linking)模型。

4 个地雷 2 条不成文约定 1 个缺失概念 1 次同任务被回滚的尝试(PR #2841)
盲点清单
地雷 01

会话是双写的——你最先找到的那个 Redis 存储是错的

Acme 正在把会话从 Postgres 迁移到 Redis(MIG-118,三月起就停滞了)。目前每次会话写入都经过 SessionBridge.write(),再扇出到 RedisSessionStore 和旧的 pg_sessions两边。迁移一直没做完,所以读取仍然走 Postgres。

坑在哪grep "session" 时最先冒出来的是 RedisSessionStore——它看着最正统,代码最新,文档也最好。直接往它里面写,登录在 dev 环境看似正常(那里开启了 Redis 读取),到了生产环境就悄悄失效,因为生产的读取仍然打到 Postgres。
When you create sessions for the new provider, route everything through SessionBridge — never RedisSessionStore or pg_sessions directly. Explain the MIG-118 read/write split before writing any session code.
地雷 02

SAML 提供方——你最顺手的模板——绕过了认证中间件

除 SAML 外,每个提供方都挂载在 authPipeline 之下,由它负责限流、审计日志和设备信任检查。saml/handler.ts 却把自己的 ACS 端点直接挂在路由器上——2024 年 SP 发起的 POST 回调冲垮了 CSRF 层,之后再没人回头收拾。文件里还留着一条 14 个月前的 // TODO(priya): fold back into pipeline

坑在哪SAML 是最近才加的提供方,自然成了最想照抄的文件。照着它抄,你的新提供方上线时既没有限流也没有审计记录——而且不会挂掉任何测试,因为契约测试并不断言中间件是否存在。
Use oauth/google.ts as the structural template, not saml/handler.ts — SAML intentionally bypasses authPipeline. Confirm the new provider's routes are mounted inside authPipeline and show me the mount point.
历史 03

有人已经试过了——而回滚的原因至今仍然成立

PR #2841("Add Okta OIDC provider",2025 年 9 月)合并九天后被回滚。回滚提交里写明了原因:Acme 的回调 URL 必须带租户后缀(/auth/callback/:provider/:workspaceSlug),工作区级邀请链接才能在重定向的往返中存活。而 Okta 严格的 redirect-URI 匹配拒绝了动态后缀,被邀请的用户落进了错误的工作区。大家公认的修法——用 AUTH_STATE_SECRET 签名的 state 参数工作区声明——在回滚讨论串里设计好了,却从未落地。

坑在哪现有代码里没有任何线索指向这段历史。你会把 #2841 的方案原样重做一遍,本地测试全绿(dev 环境只有单个工作区),然后在生产环境里,第一个被邀请的用户登录时重新踩中这个 bug。
Read the revert of PR #2841 first. Implement the signed state-parameter workspace claim it proposed instead of tenant-suffixed callback URLs, and add a test for the workspace-scoped invite flow.
缺失概念 04

身份和账号是两码事——而提供方接口把这一点藏了起来

Acme 把 usersidentities 分开:一个账号可以关联多个身份(邮箱+密码、Google、SAML),在 identities 表里做关联。提供方接口只返回一份 profile;关联决策发生在 identity/linker.ts,以已验证邮箱为键。无法保证邮箱已验证的提供方(很多 SSO 身份提供商(IdP)都保证不了)必须设置 requiresManualLink: true,否则 linker 会痛快地把共用同一个企业邮箱别名的两个人合并成一个。

坑在哪照着接口老实实现,一切正常——直到某个客户的 IdP 发来一封未验证或已易主的邮箱,你的提供方就会不声不响地把陌生人的身份挂到已有账号上。这是账号接管(account takeover)级别的漏洞,不是登录 bug。
Walk me through identity/linker.ts before implementing. Decide explicitly whether this IdP guarantees verified emails; if not, set requiresManualLink and build the manual-link confirmation path.
地雷 05

令牌刷新藏在一个 dev 开、prod 关的特性开关后面

刷新令牌轮换由 flags.yaml 里的 auth.refresh_rotation 控制。它在 dev 和 staging 默认为 true,在生产为 false(五月一次时钟偏移事故后,灰度发布被暂停了)。开关关闭时,生产会话的寿命恰好是 ACCESS_TOKEN_TTL(12 小时),到点硬过期——根本不存在刷新路径。

坑在哪你写的任何刷新逻辑都只会按 dev 的行为被测试,在生产里则是死代码。更糟的是:如果你的提供方默认刷新会发生,生产用户每 12 小时就会被中途登出一次,而 bug 报告只会写"SSO 随机把我登出",矛头直指你的代码。
Implement against auth.refresh_rotation=false (prod reality). Make the provider work with hard 12h expiry, and note separately what changes if the rotation rollout resumes.
约定 06

注册一个提供方要三步,能自己摸出来的只有两步

编译通过不等于提供方已经生效。它必须:(1)在 providers/registry.ts 里注册;(2)通过一次迁移写进 auth_providers 表——管理后台读的是数据库,不是 registry;(3)加进 __tests__/provider-contract.spec.ts 的 fixture 列表,共享契约套件(state 校验、防重放、登出扇出)会对每个已注册提供方跑一遍。registry 和 fixture 列表不一致时,CI 会直接失败。

坑在哪漏掉迁移那一步,提供方照样编译、照样过测试,只是永远不会出现在任何工作区的 SSO 设置里。已经有两位工程师各自为此搭进去半天,而这事儿没写在任何文档里。
Do all three registration steps: registry.ts, an auth_providers migration, and the provider-contract fixture list. Show me the contract suite passing for the new provider before we call it done.
地雷 07

登出不是一次函数调用——它是一个事件,而审片播放器在监听

会话吊销会在内部事件总线上发布 auth.session.revoked。实时审片播放器持有自己的长效播放授权,只有消费到这个事件才会释放。logout.ts 已经替现有提供方处理好了这件事,但 SSO 单点登出(single logout,SLO)的回调走的是另一条路径,必须自己发布该事件。

坑在哪漏掉这个事件,登出看起来"成功了"——会话记录确实没了——但任何开着审片标签页的人仍能完整播放私有视频,直到授权 TTL 过期(最长 24 小时)。对一家向制片方售卖审片工作流的公司来说,这是安全事故,不是普通 bug。
Handle SLO: any revocation path in the new provider must publish auth.session.revoked. Add a test asserting the review player's grant is released on IdP-initiated logout.
改进后的提示词

把上面这一切,折进你真正该发给我的那条提示词

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.

这七条约束,今天早上的你一条也写不出来——每一条都是别人用半天时间换来的。这正是盲点扫描的意义。