gdp-ts README

The project's README from github.com/rauchg/gdp-ts. As one markdown file: README.md.

A pixel-art ghost carrying a TypeScript document with a green checkmark

gdp-ts

A tiny library + linter and AI skill implementation of Ghosts of Departed Proofs for TypeScript, a verification system to make API contracts more secure at compile (type check) time.

gdp-ts makes an entire category of authorization ("can this user read this resource") or entitlement ("does this user pay for this resource") bugs very difficult for humans and coding agents to introduce, by catching them at compile time with negligible runtime overhead.

Features

Installation

$ npx skills add rauchg/gdp-ts
$ pnpm add @gdp-ts/core

Requires TypeScript 5.4 or newer.

The problem

A lot of codebases feature code like this:

async function handler(req, res) {
  await assertProjectAdmin(req.user.id, req.params.projectId); // throws 403
  await setPasswordProtection(req.params.projectId, req.body.password); // nothing connects this to the line above
  res.sendStatus(204);
}

Looks great, except: all it takes is

... for a security / business disaster to occur.

The solution

We change the signature above from something anyone can call from anywhere:

function setPasswordProtection(projectId: ProjectId, password: string): Promise<void>;

...to forcing the caller to present evidence (proof) that they can do (are admin) and can get (have the plan) the thing:

function setPasswordProtection<U, P>(
  project: Named<P, ProjectId>,
  password: string,
  _proofs: { admin: UserIsProjectAdmin<U, P>; plan: PlanIncludesPasswordProtection<P> },
): Promise<void>;

The skill takes care of guiding agents and humans towards this safer pattern, and away from the boolean blindness problem.

How it works

1️⃣ Name values. name(x, k) gives a runtime value a compile-time-only name N. Every call to name creates a different N, and N exists only inside the callback. For our example, wwo project ids named separately are now distinguishable to the compiler even though both are string:

// app.ts
import { name } from "@gdp-ts/core";
import { disablePasswordProtection } from "@/data/projects";
import { ProjectId } from "@/lib/ids";
import { userIsProjectAdmin } from "@/proofs/user-is-project-admin";
import { app, authenticated, HttpError } from "@/lib/server"; // your Express app, auth middleware, error type

app.delete(
  "/projects/:id/password-protection",
  authenticated((viewer, req, res) => // authenticated() resolves the logged-in user
    name(viewer.id, ProjectId(req.params.id), async (user, project) => {
      const admin = await userIsProjectAdmin(user, project);
      if (!admin) throw new HttpError(403, "Only Owners and Members can change this");
      await disablePasswordProtection(project, admin);
      res.sendStatus(204);
    }),
  ),
);

2️⃣ Prove facts about names. A small trusted module performs a check and returns a Proof<"UserIsProjectAdmin", [U, P]>, or null. Only that module can mint that proof. At runtime a proof is a frozen { kind } object.

// proofs/user-is-project-admin.ts  (trusted: the only place that can mint this proof)
import { defineProof, type Named, type Proof } from "@gdp-ts/core";
import { db } from "@/lib/db";
import type { ProjectId, UserId } from "@/lib/ids";

const UserIsProjectAdmin = defineProof("UserIsProjectAdmin"); // not exported
export interface UserIsProjectAdmin<U, P> extends Proof<"UserIsProjectAdmin", [U, P]> {}

export async function userIsProjectAdmin<U, P>(
  user: Named<U, UserId>,
  project: Named<P, ProjectId>,
): Promise<UserIsProjectAdmin<U, P> | null> {
  const role = await db.roleInProjectTeam(user.value, project.value);
  return role === "owner" || role === "member" ? UserIsProjectAdmin.prove(user, project) : null;
}

3️⃣ Demand proofs. Sensitive functions take a proof about their exact arguments. Calling them with the wrong proof, a proof about a different value, a raw id, or no proof is a compile error 🎉

// data/projects.ts  (sensitive: demands exact proofs)
import type { Named } from "@gdp-ts/core";
import { db } from "@/lib/db";
import type { ProjectId } from "@/lib/ids";
import type { UserIsProjectAdmin } from "@/proofs/user-is-project-admin";

export function disablePasswordProtection<U, P>(
  project: Named<P, ProjectId>,
  _proof: UserIsProjectAdmin<U, P>,
): Promise<void> {
  return db.writePasswordProtection(project.value, null);
}

TL;DR:

The package

@gdp-ts/core on npm ships two things:

The skill

npx skills add rauchg/gdp-ts installs a skill that guides coding agents through applying the pattern in your codebase. It is plain Markdown, so it doubles as the reference manual for humans:

Examples

All three use the same domain, loosely modeled after Vercel's Password Protection: team admins set a password on a project, and visitors must enter it to open the project's protected URLs.

Relationship to other solutions

Prior art & Credits

License

MIT

gdp-ts (rauchg/gdp-ts)

Searching Database…

812 ·GitHub
这是什么?gdp-ts 是一个很小的 TypeScript 库,附带一个 linter 和一个 AI 编码智能体技能,实现了「Ghosts of Departed Proofs」模式,让你能在编译期而非运行时捕获授权和权限类 bug(比如检查用户能否读取或为某个资源付费)。
这是一个框架,还是放进现有代码里的库?它是一个可渐进采用的零依赖库(@gdp-ts/core),附带 ESLint/Oxlint 预设和一个基于 Markdown 的技能文件 —— 它不是框架,因此不会改变你处理 I/O 或副作用的方式。
怎么把它添加到我的项目?从 npm 安装 @gdp-ts/core,即可获得 name()、defineProof()、Named 和 Proof;另外单独启用 @gdp-ts/core/lint/eslint 或 /oxlint 提供的 ESLint 或 Oxlint 预设。AI 技能通过 npx skills add rauchg/gdp-ts 添加。
我需要特定版本的 TypeScript 吗?需要,要求 TypeScript 5.4 或更新版本;该库本身没有任何运行时依赖。
编译器能捕获所有类型的授权 bug 吗?不能 —— gdp-ts 依靠 lint 来阻止人们用 'as' 之类的类型断言伪造证明,而且它明确不保证防范伪造的证明、过期的证明,以及 TypeScript 类型系统的其他固有局限。
附带的技能具体是做什么的?它是一份纯 Markdown 参考手册和检查清单,指导编码智能体(以及人类)完成六步流程:标记 id、定义证明模块、开启 lint 预设,通过 'npx skills add rauchg/gdp-ts' 安装。
它和 CASL 或 Oso 这类策略引擎有什么不同?策略引擎决定主体能否对资源执行操作;gdp-ts 本身不做这个决定,而是保证该决定切实传递到依赖它的函数,并且精确到所涉及的具体值。你甚至可以在证明函数内部调用策略引擎。
使用它的性能开销有多大?运行时开销只是对已检查 id 的一个很小的包装;在 TypeScript 7 冷检查下,每个授权调用点的类型检查开销约为 0.3ms,随使用量而非代码库规模扩展。
有可以学习的可用示例吗?有 —— 仓库附带三个示例:一个无框架的内存版演练、一个带 ESLint 预设的 Express 5 HTTP 示例,以及一个由 Postgres(PGlite)支持、带 SQL 连接证明的 Express + Drizzle 示例。
Creator
rauchg
Size
0.3 MB
Stars
812
Updated
CommitOct 07
Age
13d
Commits · 90 Days