与 #3846 (marketplaceApi.ts 把 runtime 声明成开放 string)同一类:消费方比契约宽松。#3546 切片七枚举 organization.invitations.status. 前缀家族时量出来的。
生产方自己就写了枚举,只是写在注释里
packages/auth/src/types.ts:404
export interface AuthInvitation {
…
/** Status: 'pending' | 'accepted' | 'rejected' | 'canceled' */
status: string; ← 类型是开放 string
}
createAuthClient.ts 的 listInvitations 直接 as AuthInvitation[],没有运行时校验,所以后端返回什么都进得来。
由此长出的两处宽松
packages/app-shell/src/console/organizations/manage/InvitationsPage.tsx:
徽章配色 有 default: 兜底 —— 未知状态渲染成 secondary,不报错:
function statusBadgeVariant ( status : string ) : 'outline' | 'default' | 'destructive' | 'secondary' {
switch ( status ) { case 'pending' : … default : return 'secondary' ; }
}
徽章文案 是模板 key,defaultValue 取 wire 原值:
{ t ( `organization.invitations.status.${ inv . status } ` , { defaultValue : inv . status } ) }
258 个 t() 调用点引用的 key 在任何语言包里都不存在(#3530 守卫首跑实测),其中 8 处直接把 raw key 渲染给用户 #3546 切片七按同文件的封闭联合 type StatusFilter = 'all' | 'pending' | 'accepted' | 'rejected' | 'canceled' 枚举了五个成员并回填十包,并在测试里钉住"key 集合 === StatusFilter",所以第五个后端状态一出现,那条断言会红 —— 这是好事,它逼人正面处理。但在门禁红之前,线上行为是把 wire 字符串(英文、小写、CSS 首字母大写后)当界面文案给用户看,十种语言都是。
为什么现在算 finding 而不是 bug
今天后端的取值域就是这四个,所以没有用户能触发。它是契约方向 的问题:枚举写在注释里而不是类型里,于是消费侧只能宽松。
建议修法
把注释里的枚举提到类型里:
export type AuthInvitationStatus = 'pending' | 'accepted' | 'rejected' | 'canceled' ;
export interface AuthInvitation { … ; status : AuthInvitationStatus ; }
再让 listInvitations 的边界处对未知值响亮失败或显式降级 ,而不是让 statusBadgeVariant 的 default: 和 defaultValue: inv.status 各自悄悄兜住。这正是 AGENTS.md 的 contract-first 取向:声明即强制,宽松的消费方是 AI 生成的元数据错误藏身的地方。
⚠️ 与 #3546 切片七同触 InvitationsPage.tsx 的读取面(切片七零组件改动,只读源码做断言),但改的文件不同(packages/auth/src/types.ts),无冲突。
关联:#3846 (同类,marketplace runtime)、#3790 / #3447 (同类"未知 status 无兜底/悄悄兜底")、#3546 (切片七,发现路径)。
与 #3846(
marketplaceApi.ts把runtime声明成开放string)同一类:消费方比契约宽松。#3546 切片七枚举organization.invitations.status.前缀家族时量出来的。生产方自己就写了枚举,只是写在注释里
createAuthClient.ts的listInvitations直接as AuthInvitation[],没有运行时校验,所以后端返回什么都进得来。由此长出的两处宽松
packages/app-shell/src/console/organizations/manage/InvitationsPage.tsx:default:兜底 —— 未知状态渲染成secondary,不报错:defaultValue取 wire 原值:type StatusFilter = 'all' | 'pending' | 'accepted' | 'rejected' | 'canceled'枚举了五个成员并回填十包,并在测试里钉住"key 集合 === StatusFilter",所以第五个后端状态一出现,那条断言会红 —— 这是好事,它逼人正面处理。但在门禁红之前,线上行为是把 wire 字符串(英文、小写、CSS 首字母大写后)当界面文案给用户看,十种语言都是。为什么现在算 finding 而不是 bug
今天后端的取值域就是这四个,所以没有用户能触发。它是契约方向的问题:枚举写在注释里而不是类型里,于是消费侧只能宽松。
建议修法
把注释里的枚举提到类型里:
再让
listInvitations的边界处对未知值响亮失败或显式降级,而不是让statusBadgeVariant的default:和defaultValue: inv.status各自悄悄兜住。这正是 AGENTS.md 的 contract-first 取向:声明即强制,宽松的消费方是 AI 生成的元数据错误藏身的地方。InvitationsPage.tsx的读取面(切片七零组件改动,只读源码做断言),但改的文件不同(packages/auth/src/types.ts),无冲突。关联:#3846(同类,marketplace runtime)、#3790 / #3447(同类"未知 status 无兜底/悄悄兜底")、#3546(切片七,发现路径)。