mcpField
G
gdp-ts

gdp-ts

github.com/rauchgSecurity847116 installsUpdated 3w ago
Repository

About gdp-ts

gdp-ts is a TypeScript library and workflow for tying authorization checks to the exact values a sensitive function uses. It uses named values and unforgeable proof types so missing or mismatched checks become compile-time errors, with lint rules for additional safeguards.

Key features

  1. 01Bind authorization proofs to named values
  2. 02Require proofs in sensitive function signatures
  3. 03Keep proof creation in trusted modules
  4. 04Provide ESLint and Oxlint rules
  5. 05Support compile-time regression checks

Use cases

  1. 01Prevent authorization checks from applying to the wrong resource
  2. 02Make missing authorization evidence a compile-time error
  3. 03Add proof-based access control to a TypeScript codebase

Install

Copy the command and run it in a terminal.

$npx skills add https://github.com/rauchg/gdp-ts --skill gdp-ts

After installing, type/gdp-tsin Claude Code to confirm it loaded.

Bundled files

References and scripts installed along with SKILL.md.

references/
errors.md
limits.md
patterns.md
recipe.md
where-to-stop.md

Details

From the project README

gdp-ts

A sensitive function (one that reads or changes something only some callers may touch) takes a proof about its exact arguments, so calling it without the check, or with a check about a different value, does not compile.

  1. Name values. name(viewer.id, projectId, async (user, project) => ...) gives each value a compile-time-only name, valid inside the callback.
  2. Prove facts about names. A small trusted module in proofs/ runs the check and returns a proof such as UserIsProjectAdmin<U, P>, or null. Only that module can mint it.
  3. Demand proofs. The sensitive function takes project: Named<P, ProjectId> and _proof: UserIsProjectAdmin<U, P>. The wrong proof, a proof about another value, a raw id or no proof is a compile error.

This is not a theorem prover. Prove the facts whose absence would be an incident, and stop there (where to stop).

Workflow

When adding or changing authorization in a codebase that uses this pattern:

  1. Is the id branded? If not, brand it first.
  2. Is there a proof for the fact you need in proofs/? If yes, reuse it. If no, add one file: private defineProof, exported interface X<...> extends Proof<"X", [...]> {}, exported async function x(...named): Promise<X<...> | null>. Nothing else.
  3. Does the sensitive function take proofs about its named arguments? If it takes raw ids, change the signature; let the compiler list the call sites to fix.
  4. Does the request carry other values the operation trusts (a token in a cookie, an id in the body)? Check them in a trusted module against the resource they claim to unlock (TokenUnlocksUrl<H>). This is where cross-resource and cross-tenant bugs live.
  5. Are there non-user preconditions (plan, scope, state)? Make them proofs too (PlanIncludesPasswordProtection<P>).
  6. In the handler body (not middleware): name(...), call the policy function, turn null into the right HTTP response, do all sensitive work inside the callback. If that block will likely be called from many places, consider moving it into a shared function (recipe step 5).
  7. Never write as to satisfy a proof or Named type. If you feel the need, the signature is wrong or you are outside proofs/. If the codebase has no gdp-ts lint preset yet, add it (recipe step 6); if it does, fix lint errors rather than disabling them.
  8. Never export a prover, never call defineProof outside proofs/, never return a Named, or a proof that mentions that callback's names, from a name() callback.
  9. Add the new mistake you almost made to mistakes.ts (a type-checked file that never runs) as one line under // @ts-expect-error. It is the cheapest regression test you will write (details).
  10. Test the proof function you added. It is the only thing that can be wrong at runtime.

The recipe shows each step in full.

References

Read the one you need:

FileRead it when
references/recipe.mdAdding a proof, a policy or a sensitive function, or adopting the pattern. Six steps: brand ids, trusted modules, policies as unions, sensitive functions, naming in the handler, lint rules.
references/patterns.mdReusing proofs, weaker-from-stronger proofs, proofs that carry evidence, preconditions that are not about users.
references/errors.mdA type error mentions Named<...>, a proof type, or "two different types with this name".
references/limits.mdJudging what the types do and do not guarantee (forging, stale proofs, older TypeScript).
references/where-to-stop.mdDeciding how much to prove.

API

Everything is exported from @gdp-ts/core.

ExportWhat it is
Named<N, A>A value of type A with compile-time name N. Read it with .value. N is invariant.
name(a, k) / name(a, b, k) / name(a, b, c, k)Gives one to three values fresh names, scoped to k. Returns what k returns (Promises included).
Proof<Kind, About>Evidence that fact Kind holds about the tuple of names About. Has a runtime kind: Kind. Both parameters are invariant.
defineProof(kind)Returns a Prover: { kind, prove(...named) }. prove infers About from its arguments. Keep it private to the module that performs the check.
NameOf<T>The N of a Named<N, A>. Rarely needed.
NamesOf<S>The names of a tuple of Named values. Used in prove's signature.

Lint presets, for the gaps the type system cannot close (recipe step 6):

ImportWhat it is
@gdp-ts/core/lint/eslintgdp(options?): ESLint flat-config entries. Spread after your typescript-eslint config.
@gdp-ts/core/lint/oxlintgdp(options?): an Oxlint config (works on TypeScript 7).
@gdp-ts/core/lint/pluginThe rules themselves (gdp-ts/no-define-proof, no-exported-prover, no-proof-assertion, no-type-assertion, no-any), for custom setups.

Options: proofs (trusted-module globs, default **/proofs/**), strict (also ban every as and any outside them, default false), allowAssertions (strict-mode exceptions, default **/lib/ids.ts), proofImports and files.

The only place the library asserts a proof type is inside prove, so code using it never needs to. The runtime footprint is one frozen { kind } object per defineProof call; prove returns that same object every time, and name wraps each value in a frozen { value }.

Comments

Markdown supported