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…

816 ·GitHub
Что это?gdp-ts — это крошечная библиотека для TypeScript в комплекте с линтером и навыком для AI-агентов, реализующая паттерн «Ghosts of Departed Proofs»: он позволяет ловить ошибки авторизации и прав доступа (например, проверку, может ли пользователь читать ресурс или заплатить за него) на этапе компиляции, а не во время выполнения.
Это фреймворк или просто библиотека, которую добавляешь в существующий код?Это библиотека (@gdp-ts/core) без зависимостей, которую можно внедрять постепенно, с пресетами ESLint/Oxlint и файлом навыка на Markdown — это не фреймворк, поэтому он не меняет то, как вы работаете с I/O или эффектами.
Как добавить его в свой проект?Установите @gdp-ts/core из npm, чтобы получить name(), defineProof(), Named и Proof, и отдельно включите пресет ESLint или Oxlint из @gdp-ts/core/lint/eslint или /oxlint. AI-навык добавляется командой npx skills add rauchg/gdp-ts.
Нужна ли определённая версия TypeScript?Да, требуется TypeScript 5.4 или новее; у самой библиотеки нет ни одной рантайм-зависимости.
Ловит ли компилятор любые ошибки авторизации?Нет — gdp-ts полагается на линтинг, чтобы не давать подделывать proof-объекты приведениями вроде 'as', и прямо не гарантирует защиты от поддельных proof-объектов, устаревших proof-объектов и прочих врождённых ограничений системы типов TypeScript.
Что именно делает входящий в комплект навык?Это справочник и чек-лист на простом Markdown, который проводит агентов (и людей) через шестиступенчатый рецепт: брендирование идентификаторов, определение proof-модулей и включение линт-пресета; устанавливается командой 'npx skills add rauchg/gdp-ts'.
Чем это отличается от движков политик вроде CASL или Oso?Движки политик решают, может ли субъект действовать над ресурсом; gdp-ts сам такое решение не принимает — он гарантирует, что решение действительно доходит до функции, которая от него зависит, для конкретных используемых значений. Можно даже вызывать движок политик внутри proof-функции.
Какова цена в производительности?Накладные расходы во время выполнения — лишь крошечная обёртка вокруг проверенных идентификаторов, а проверка типов стоит около 0.3 мс на каждое авторизованное место вызова при холодной проверке TypeScript 7, причём затраты растут с интенсивностью использования, а не с размером кодовой базы.
Есть ли рабочие примеры для изучения?Да — в репозитории три примера: пошаговый разбор без фреймворка на данных в памяти, HTTP-пример на Express 5 с пресетом ESLint и пример Express + Drizzle на Postgres (PGlite) с proof-объектами, собранными через SQL-соединения.
Creator
rauchg
Size
0.3 MB
Stars
816
Updated
CommitOct 07
Age
13d
Commits · 90 Days