关于 gdp-ts
gdp-ts 是一个 TypeScript 库及工作流,将授权检查绑定到敏感函数实际使用的值。它通过命名值和不可伪造的证明类型,让缺失或不匹配的检查成为编译错误,并提供额外的 lint 规则。
主要功能
- 01将授权证明绑定到命名值
- 02要求敏感函数签名包含证明
- 03将证明创建限制在可信模块
- 04提供 ESLint 和 Oxlint 规则
- 05支持编译期回归检查
适用场景
- 01防止授权检查对应到错误资源
- 02将缺失授权凭据变为编译错误
- 03在 TypeScript 代码库中加入基于证明的访问控制
安装
复制下面的命令,在终端中运行。
$
npx skills add https:/ / github.com/ rauchg/ gdp-ts --skill gdp-ts安装后在 Claude Code 中输入/gdp-ts确认已加载。
包含的文件
除 SKILL.md 外,安装时一起下载的参考资料和脚本。
references/errors.mdlimits.mdpatterns.mdrecipe.mdwhere-to-stop.md详细介绍
gdp-ts
敏感函数(只能由部分调用方读取或更改某些内容的函数)需要接收针对其确切参数的证明,因此未经过检查就调用它,或使用针对其他值的检查,都无法通过编译。
- 为值命名。
name(viewer.id, projectId, async (user, project) => ...)会为每个值赋予一个仅在编译时存在的名称,该名称在回调中有效。 - 证明名称对应的事实。
proofs/中的一个小型可信模块会运行检查并返回证明,例如UserIsProjectAdmin<U, P>,或null。只有该模块能创建证明。 - 要求提供证明。 敏感函数接收
project: Named<P, ProjectId>和_proof: UserIsProjectAdmin<U, P>。错误的证明、针对另一个值的证明、原始 ID 或未提供证明,都会导致编译错误。
这不是定理证明器。只证明那些缺失时会导致事故的事实,然后就此为止(何时停止)。
工作流
在使用此模式的代码库中添加或更改授权逻辑时:
- ID 是否已添加品牌类型?如果没有,先添加。
proofs/中是否已有你需要的事实对应的证明?如果有,复用它。如果没有,添加一个文件:私有的defineProof、导出的interface X<...> extends Proof<"X", [...]> {},以及导出的async function x(...named): Promise<X<...> | null>。不要添加其他内容。- 敏感函数是否接收针对其已命名参数的证明?如果它接收原始 ID,就更改签名;让编译器列出需要修复的调用位置。
- 请求是否携带操作会信任的其他值(Cookie 中的令牌、请求正文中的 ID)?在可信模块中,针对这些值声称可以解锁的资源进行检查(
TokenUnlocksUrl<H>)。跨资源和跨租户漏洞就容易出现在这里。 - 是否存在非用户相关的前置条件(套餐、范围、状态)?也将它们设为证明(
PlanIncludesPasswordProtection<P>)。 - 在处理程序主体中(而非中间件中):调用
name(...),调用策略函数,将null转换为适当的 HTTP 响应,并在回调中完成所有敏感操作。如果这段代码很可能会在许多地方调用,可以考虑将其移到共享函数中(步骤 5:在处理程序中命名并证明,将 null 转换为响应)。 - 切勿为了满足证明或
Named类型而写as。如果你觉得有此必要,说明签名不正确,或者你不在proofs/中。如果代码库还没有 gdp-ts lint 预设,就添加它(步骤 6);如果已有,就修复 lint 错误,而不是禁用规则。 - 切勿导出证明器,切勿在
proofs/之外调用defineProof,也切勿从name()回调中返回Named,或提及该回调中名称的证明。 - 将你差点犯下的新错误添加到
mistakes.ts中(这是一个经过类型检查但永不运行的文件),作为// @ts-expect-error下的一行代码。这是你能编写的成本最低的回归测试(详情)。 - 测试你添加的证明函数。它是唯一可能在运行时出错的部分。
操作指南完整展示了每个步骤。
参考资料
阅读你需要的部分:
| 文件 | 阅读时机 |
|---|---|
references/recipe.md | 添加证明、策略或敏感函数,或采用此模式时。分为六个步骤:为 ID 添加品牌类型、可信模块、将策略表示为联合类型、敏感函数、在处理程序中命名、lint 规则。 |
references/patterns.md | 复用证明、由更强证明推出较弱证明、携带证据的证明、与用户无关的前置条件。 |
references/errors.md | 类型错误中提到 Named<...>、证明类型或“两个具有此名称的不同类型”时。 |
references/limits.md | 判断这些类型能保证和不能保证什么(伪造、过时的证明、较旧版本的 TypeScript)。 |
references/where-to-stop.md | 决定需要证明到什么程度时。 |
API
所有导出内容均来自 @gdp-ts/core。
| 导出项 | 说明 |
|---|---|
Named<N, A> | 一个类型为 A、具有编译时名称 N 的值。通过 .value 读取。N 是不变的。 |
name(a, k) / name(a, b, k) / name(a, b, c, k) | 为一个到三个值赋予新名称,作用域限定为 k。返回 k 的返回值(包括 Promise)。 |
Proof<Kind, About> | 证明事实 Kind 对名称元组 About 成立的证据。具有运行时属性 kind: Kind。两个参数都是不变的。 |
defineProof(kind) | 返回一个 Prover:{ kind, prove(...named) }。prove 根据其参数推断 About。请将其设为执行检查的模块私有内容。 |
NameOf<T> | Named<N, A> 中的 N。很少需要使用。 |
NamesOf<S> | Named 值元组中的名称。用于 prove 的签名。 |
针对类型系统无法弥补的缺口,可使用 lint 预设(参见配方步骤 6):
| 导入项 | 说明 |
|---|---|
@gdp-ts/core/lint/eslint | gdp(options?):ESLint 扁平配置项。放在 typescript-eslint 配置之后展开。 |
@gdp-ts/core/lint/oxlint | gdp(options?):Oxlint 配置(支持 TypeScript 7)。 |
@gdp-ts/core/lint/plugin | 规则本身(gdp-ts/no-define-proof、no-exported-prover、no-proof-assertion、no-type-assertion、no-any),适用于自定义配置。 |
选项:proofs(受信任模块的 glob 模式,默认值为 **/proofs/**)、strict
(同时禁止在这些模块之外使用所有 as 和 any,默认值为 false)、
allowAssertions(严格模式的例外项,默认值为 **/lib/ids.ts)、
proofImports 和 files。
库中唯一断言 proof 类型的地方是 prove 内部,因此使用它的代码无需进行断言。运行时开销为每次调用 defineProof 创建一个冻结的 { kind } 对象;
prove 每次都会返回同一个对象,而 name 会将每个值包装在一个冻结的 { value } 中。
评论
支持 Markdown