2026-09-23·by Sijie Wang#standmeet#architecture#design

coded-landing-and-code-rotation

带码落地 /c/<slug> 与轮换 code 字符串

父级:access-control · 兄弟:embed-credential-never-carries-the-code

结论(2026-09-07): 一张 access code 有三个可分离的部分。行 id 是所有关联的键(embed、application、成员、拒绝表)。slug 是一个 snowflake 短 id,命名这张 code 的落地路径 /c/<slug>——是定位符,永远不是凭据。code 字符串LABEL-<16 hex>)是唯一的凭据,也是 codes.rotate 唯一替换的部分。落地页在首次绘制时就把 ?code= 从地址栏吸收掉,所以明文只活在纸上(QR——quick-response——码、PDF)和会话存储里,不在 URL、历史记录或 Referer 头里。

触发问题。 简历 PDF(resume-composer-one-renderer)在二维码里印的是 <public_url>/?code=<code>。这是 URL 里的凭据:它会进浏览器历史、进每个跨源子资源的 Referer、进截图。而且 code 是整体泄露的——招聘者会转发 PDF。2026-09-06 之前唯一的恢复手段是 codes.revoke,它连带杀掉这张 code 所有按 id 关联的东西(embed、过去的 application、成员列表)。两个缺口:明文字符串可寻址泄露恢复要付出整行的代价

slug 是定位符,不是凭据

  • 生成器。 backend/internal/infra/snowflake/snowflake.go:7-8:41 位自 epoch 起的毫秒、10 位节点、12 位序列;Slug():71)做 base62 编码(0-9a-zA-Z,最多 11 字符)。进程级节点在 access/repo/codes_slug.go:7-21;在唯一的创建点 access/repo/codes.go:84 盖上。
  • owner 覆盖。 DeriveSlug(provided, generated)access/entity/code_derive.go:69-75):小写 [a-z0-9-_],最长 32;保留名(homegateadminapisetuploginpwikioutputassets)回退到 snowflake(:61-64)。owner 的 HIRING-2026 读作 /c/hiring-2026
  • 存储。 access_codes.slug citext NOT NULLschema.sql:354),每个 owner 内唯一(:357)。migration 2026-09-06-access-code-slug.sql:10-13:先加可空列 → 从行 id 回填 → NOT NULL → 唯一索引,所以升级上来的实例保住每一张已有 code。
  • 没有服务端解析。 app/src/app/c/[slug]/page.tsx:28-33 从不读 slug 参数:它以 hasCode={false} 渲染 VisitorRoot,授权只来自已存的会话。backend/internal/access 里没有任何按 slug 查 code 的地方。守卫:coded-landing-slug.spec.ts:41-51(D3)无会话访问 /c/some-locator,断言是默认首页且聊天输入框为零。

落地:吸收、改写、分流

  • 吸收。 app/src/lib/gate/use-absorb-code.ts:55-58 从 query 里删掉 code,在发会话请求之前就调 history.replaceState——别的任何东西跑起来之前,字符串已经不在地址栏里了。
  • 改写。 签发响应带 slugmicrosite_slugroutes/public/sessions.go:62-65);use-issue-pending-code.ts:124applyLanding(landAfterIssue(...))app/src/lib/visitor/code-landing.ts:25-40:绑定了 microsite(access_codes.microsite_idschema.sql:344:756-759ON DELETE SET NULL,由 codes.set_microsite 设置,access/ops/codes.go:70-78)就整页跳到 /p/<microsite-slug>——microsite 是独立的构建产物;否则访客留在已经渲染好的聊天里,只有 URL 变成 /c/<slug>。守卫:coded-landing-slug.spec.ts:28(D1)断言 URL 匹配 /c/[^/?#]+$ 且没有 code=
  • Referer。 吸收在 JS(JavaScript)里跑;首次绘制在它之前。app/next.config.ts:122-125 给每条路径设 Referrer-Policy: strict-origin-when-cross-originfc841f41f,2026-09-01),所以首次绘制期间加载的跨源子资源只看得到 origin。守卫:security-referrer-policy.spec.ts:18-25
  • 刷新。 刷新时的 /c/<slug>localStorage 里的访客会话来服务;slug 是浏览器已持有的那个会话的书签。

新 code 切换会话;无效 code 落到 /gate

app/src/app/visitor-root.tsx:71——pending ? 'picker' : viewFor(isChatSession(session), hasCode):旧会话还在时来了一个 ?code=,就为 code 打开身份选择器,而不是悄悄留在旧聊天里(owner 的场景:已经凭一张 code 来过的招聘者扫了第二份简历)。签发时的 401 变成 'invalid'use-issue-pending-code.ts:141-144),VisitorNamePicker.tsx:81-89 把访客送到 /gate230a17025(2026-09-06),单元测试在 visitor-root.test.ts

codes.rotate——什么活下来,什么死

SQL 只动一列:UPDATE access_codes SET code = \$3 WHERE id = \$1 AND owner_id = \$2 RETURNING *db/queries/access/access_codes.sql:33-36)。然后 usecase 清掉会话(access/usecase/codes.go:124-126d.Sessions.DeleteByCode——和 RevokeCode:103 用的是同一个 Redis 清场;visitor_session.go:124-139)。

活下来(别的什么都不写)
idslug/c/<slug> 继续可达旧的明文字符串——每一张已打印的二维码、已发出的 PDF、分享过的 ?code= 链接
assumed_role_id 和冻结的 RoleSnapshot(role-snapshot-frozen这张 code 下每一个活着的访客会话(Redis code:<id> 集合,逐个 token 删除)
code_*_denialscode_members、配额、有效期(acl-and-quota-granularity
microsite_id、embed(embeds.code_id)、application 行——全按 id 关联
  • 新字符串必须非空且不含空白和 /?#&codes.go:134-136,否则 ErrCodeInvalid);citext 撞车 → ErrCodeTaken → 409(access/repo/codes_rotate.go:41-48)。
  • Admin:PATCH /codes/{id}/coderoutes/admin/codes.go:51use-codes.ts:117-123),前面挡一个警告弹窗(admin-access.json:33-37,8 个 locale):"Rotating the code makes every copy already handed out stop working — résumé PDFs and QRs you've already sent, shared ?code= links — and signs out anyone using it now. Embeds and past applications keep working (they're bound by id). This can't be undone." MCP:codes.rotate {code_id, code}access/ops/codes_write.go:105-127)。
  • 守卫:code-rotation.spec.ts:46-72(id 不变、旧字符串被拒、撞车 409)和 code-change-ui.spec.ts:31-48(弹窗 + 确认)。

熵与签发日志

DeriveCodeaccess/entity/code_derive.go:47-56)= 大写 label([A-Z0-9],最长 12,默认 CODE)+ - + 8 个随机字节写成 16 个大写十六进制字符:64 位后缀(1ca9d9564,2026-09-01,起因是渗透测试发现旧的 16 位后缀)。crypto/rand 失败时后缀是十六个 0——看得见的哨兵,不是一个看起来正常的弱 code(:146)。IssueCode 打一条 access code issued 日志,带 owner_idcode_idslugrole_idlabel——从不带字符串本身(access/usecase/codes.go:60-630e6104a6f,2026-09-07),所以线上看到的 slug 能追回它的签发。

诚实的天花板

  • 二维码仍然带明文。 没有会话的招聘者需要一个凭据;slug 替不了它(D3 证明它什么都不授予)。吸收-改写把字符串的暴露缩短到首次绘制;它去不掉纸上的字符串。轮换是恢复手段,而且是有意做成全杀的。
  • 轮换会杀会话,但没有 e2e 断言这一点。 code-rotation.spec.ts:8-9 把清场推给 visitor_session_revoke_test;弹窗文案的断言是一个宽松的正则。codes_rotate.go:17-18 有一条过期注释说活着的会话不受影响——对 SQL 成立,对整个操作不成立;以 usecase/codes.go:109-112 为准。
  • slug 是每个 owner 内唯一,不是全局唯一。 唯一性是 (owner_id, slug)/c/<slug> 无歧义只因为 v1 实例是单 owner。多租户路由需要把 handle 放进路径。
  • 切换只有单元测试。 230a17025visitor-root.test.ts 加了视图选择用例;没有浏览器 spec 端到端驱动"有了第一个会话之后扫第二张二维码"。

已实现 2026-09-01 → 2026-09-07。 Referrer-Policy fc841f41f;熵 1ca9d9564backend/internal/access/entity/code_derive.go);snowflake dbb803288backend/internal/infra/snowflake/snowflake.go);slug 列 4e383c2a7 + migration 2026-09-06-access-code-slug.sql;落地路径 c6c54ce88app/src/lib/visitor/code-landing.tsapp/src/app/c/[slug]/page.tsx);spec 1b 59f83db86e2e/test/coded-landing-slug.spec.ts);会话切换 230a17025RotateCode 505b3fc4fbackend/internal/access/usecase/codes.goaccess/repo/codes_rotate.goe2e/test/code-rotation.spec.ts);admin 弹窗 6ed1759bbe2e/test/code-change-ui.spec.ts);签发日志 0e6104a6f。它服务的链:chain-selective-access

about this entry

One of sijie's wiki entries. The AI on this site is grounded in the same corpus and answers in sijie's voice, with citations back to entries like this one — answering costs sijie money, so it waits behind a code: enter an access code →