Skip to content
AI Primer
release

gdp-ts makes authorization proofs compile-time requirements

The gdp-ts library, linter, and AI skill require typed proofs that authorization checks ran before sensitive calls. Its rules also flag forged proofs and unsafe escape hatches.

5 min read
gdp-ts makes authorization proofs compile-time requirements
gdp-ts makes authorization proofs compile-time requirements

TL;DR

  • gdp-ts makes sensitive functions uncallable without typed authorization evidence, a contract rauchg's announcement presents for human and agent-written TypeScript.
  • The mechanism gives runtime values compile-time identities and requires proofs about those exact values, as ctatedev's summary breaks down.
  • TypeScript assertions can bypass the type checker, so the package adds lint rules against forged proofs and proof constructors outside proofs/, a risk rauchg's reply calls out with as any.
  • The project frames cheaper code generation as a reason to spend more on deterministic verification, an argument cramforce's post makes in the release discussion.

The gdp-ts README uses a Vercel-like password-protection API to show how the contract works in real code. Ultracite's gdp preset documentation adds the lint layer that catches the escape hatches the type checker cannot see.

Proofs at the call site

Conventional authorization often leaves the check and the sensitive operation as separate calls. gdp-ts changes the operation's signature so the caller must present evidence that the relevant check passed, according to the gdp-ts README.

The example changes a function resembling setPasswordProtection(projectId, password) into one that accepts a named project, the password, and proofs such as UserIsProjectAdmin and PlanIncludesPasswordProtection. A missing proof, a proof for another project, or a raw ID becomes a type error.

The result is a contract enforced where the mutation happens. The README's model lets a route, Server Action, or job call the data layer without moving the authorization check into every caller.

The announcement record also contains a separate short reply from rauchg's reply that reads only “@maria_rcks Jan ’27”; the implementation detail is in the repository rather than that reply.

Named values bind the proof

The README describes three mechanics for connecting a check to the exact operation it authorizes:

  1. name(x, k) assigns a compile-time-only identity to a runtime value. Two strings can both be ProjectId, while the type checker still treats them as different named projects.
  2. A trusted module in proofs/ runs the database or policy check and returns a typed Proof, or null. Its proof type carries the identities of the user and project it checked.
  3. The sensitive function demands that proof in its parameter list, so a proof about another named value does not satisfy the signature.

The proof object is deliberately not a second authorization system. The check still runs in the trusted module, while the type checker verifies that its result reaches the function that needs it. The gdp-ts README describes this as a way to layer relationship proofs on top of ordinary branded IDs.

The linter closes escape hatches

TypeScript's type checker cannot distinguish an honest proof from a forged assertion such as {} as UserIsProjectAdmin<U, P>. gdp-ts therefore ships ESLint and Oxlint presets that add checks around the type system.

The lint rules cover three concrete bypasses:

  • Forging a proof with a type assertion.
  • Calling defineProof outside the trusted proofs/ directory.
  • Treating the proof parameter as an ordinary unused-variable error, even though the sensitive function intentionally never reads it at runtime.

Ultracite exposes the preset as ultracite/eslint/gdp or its Oxlint equivalent. Its setup spreads the preset after the core configuration, and Ultracite's documentation says the plugin is already shipped inside @gdp-ts/core.

Ghosts at runtime

The proofs are “ghosts” in the runtime sense. The README says they are frozen objects with a small runtime representation, while the safety property comes from type checking and the only application-level cost is a tiny wrapper around checked IDs.

The repository reports roughly 0.3 milliseconds per authorized call site on a cold TypeScript 7 check. It also describes the cost as growing with the number of authorized call sites rather than the overall size of the codebase.

The package does not make the authorization decision itself. A proof function can call a policy engine or plain SQL, then return a proof only for the values it checked. The README explicitly lists forged proofs, stale proofs, and other limits of TypeScript among what the pattern does not guarantee.

Deterministic checks

The release lands in a broader argument about what software teams do when code generation becomes cheap. cramforce described the shift as becoming “verification engineers” and connected the proof pattern to reducing IDOR bugs on a platform cramforce's post.

unclebobmartin argued that critical, load-bearing code may still deserve line-by-line scrutiny unclebobmartin's reply. In a follow-up, he described a workflow that disengages from syntax and surrounds AI output with deterministic measures of software quality and test coverage unclebobmartin's follow-up.

Those comments put gdp-ts in a narrower category than a general style rule. It checks one security invariant at the boundary where an operation consumes the authorization result.

The AI skill and package

Installation has two parts: npx skills add rauchg/gdp-ts installs the agent-facing skill, and pnpm add @gdp-ts/core installs the library and linter. The package requires TypeScript 5.4 or newer, according to the gdp-ts README.

The skill is plain Markdown, so it also serves as a human reference manual. Its six-step recipe is:

  1. Brand IDs.
  2. Put one trusted module for each fact in proofs/.
  3. Represent policies as unions.
  4. Make sensitive functions demand proofs.
  5. Name and prove values in the handler.
  6. Enable the lint preset.

The repository includes three progressively realistic examples: basic uses in-memory data, express-basic adds Express and the ESLint preset, and express-drizzle uses Drizzle with in-process Postgres, SQL-backed proofs, HTTP tests, and the Oxlint preset on TypeScript 7.

Further reading

Discussion across the web

Where this story is being discussed, in original context.

On X· 4 threads
TL;DR1 post
Proofs at the call site1 post
Ghosts at runtime1 post
Deterministic checks2 posts
Share on X