Skip to content

feat(sdk): the propgate API from Node - #94

Merged
joaopcm merged 5 commits into
mainfrom
feat/sdk
Aug 8, 2026
Merged

feat(sdk): the propgate API from Node#94
joaopcm merged 5 commits into
mainfrom
feat/sdk

Conversation

@joaopcm

@joaopcm joaopcm commented Aug 7, 2026

Copy link
Copy Markdown
Owner

@propgate/sdk — a typed client with a method for every route the API serves, except signup.

import { Propgate } from "@propgate/sdk";

const propgate = new Propgate("pg_live_...");
const { data, error } = await propgate.domains.check("dom_01J...");

What it covers

checks.run, the whole domains lifecycle (create / list / listAll / get / update / check / timeline / remove), profiles, webhooks (including rotateSecret and the delivery ledger), apiKeys, members, health. Twenty-one routes.

Signup is deliberately absent: it is a mailbox flow — a six-digit code out, a key back — and by the time you are holding a server-side SDK you already have a key. @propgate/cli covers it, and NOT_IN_SDK in the coverage spec is where that decision is written down.

The DX decisions worth arguing with

  • Nothing throws. Every method returns { data, error, meta }, the same envelope the API writes. A catch binds unknown and the compiler never mentions the case you forgot; returning the failure makes the branch type-checked. PropgateError is still an Error, so throw result.error works for anyone who prefers that.
  • meta stays beside the data, because that is where nextCursor, created, resolver, superseded, alreadyRevoked and previousSecretExpiresAt live, and it is typed per call rather than as a bag.
  • Retries are per method, not per status. Connection failures, timeouts, 429s and 5xx are repeated — but never a POST that may already have been applied, because POST /v1/api-keys mints a key every time it is called. A 429 is the one exception: the server refused before doing anything.
  • A Retry-After longer than 5s is not waited out. POST /v1/domains/:id/checks answers Retry-After: 47, and honouring that inside the call is a 47-second stall the caller never asked for — twice, at the default maxRetries. It comes back as error.retryAfterSeconds instead. Worst case for one call is stated in the code: 65 seconds at the defaults, and only if every attempt times out.
  • Missing key fails locally, naming PROPGATE_API_KEY, rather than spending a round trip to be told 401. checks.run and health need no key at all.
  • Zero runtime dependencies. fetch and nothing else. @propgate/dns is a dependency for its types — the diagnosis taxonomy is a public contract and a hand-copied union is one that drifts.

Tests

  • packages/sdk — 62 specs over request construction, the retry policy, envelope handling, pagination and the README.
  • apps/api/src/sdk-coverage.spec.ts — reads app.routes and fails when a route exists that no SDK method reaches, in both directions. Ungated, no containers. Verified it fails by deleting one call.
  • apps/api/src/e2e/sdk.e2e.spec.ts — the client against createApp() over a socket, with the real fixture DNS tier and Postgres. Eight specs covering the full lifecycle, the paging cursor, the delivery ledger, key rotation and error mapping.
  • packages/sdk/src/readme.spec.ts — finds the methods by reflection, so a method added without a paragraph fails.

pnpm lint, pnpm check, pnpm test and the gated apps/api suite (332 specs, all three tiers) all pass.

Not in this PR

The docs site still shows curl and CLI snippets only. Adding an SDK tab touches ~20 _snippets.ts files and the tab component, which is its own change rather than a rider on this one.

Greptile Summary

The PR adds and publishes @propgate/sdk, a typed, dependency-light Node client covering the API’s non-signup routes.

  • Adds resource clients, typed response envelopes, pagination, retry, timeout, and cancellation handling.
  • Adds unit, route-coverage, README-coverage, and end-to-end tests.
  • Integrates the SDK into workspace builds, releases, documentation, and CI.

Confidence Score: 5/5

The PR appears safe to merge.

No blocking failure remains.

Important Files Changed

Filename Overview
packages/sdk/src/http.ts Implements request construction, bounded retries, timeout validation, abortable backoff, and envelope-safe transport failure handling.
packages/sdk/src/client.ts Defines the public client, configuration defaults, resource wiring, and health endpoint handling.
packages/sdk/src/caller.ts Centralizes requests and cursor pagination with repeated-cursor termination.
apps/api/src/sdk-coverage.spec.ts Verifies bidirectional coverage between registered API routes and SDK methods.
apps/api/src/e2e/sdk.e2e.spec.ts Exercises the SDK against the real API and database across principal resource lifecycles.
packages/sdk/package.json Configures the SDK package’s exports, build, publication metadata, and dependencies.

Reviews (2): Last reviewed commit: "ci(sdk): build the published package, so..." | Re-trigger Greptile

@propgate/sdk: a method for every route except signup, which is a mailbox
flow the CLI owns.

Every call returns `{ data, error, meta }` and none of them throw, so the
failure branch is type-checked instead of an `unknown` from a `catch`. `meta`
stays beside the data because that is where `nextCursor`, `created` and
`resolver` live. `listAll` walks a cursor to the end at the page size the
server clamps to.

Retries cover connection failures, timeouts, 429s and 5xx, and never repeat a
POST that may already have been applied — `POST /v1/api-keys` mints a key every
time it is called. A `Retry-After` past five seconds comes back as
`error.retryAfterSeconds` rather than stalling inside an await nobody can see.

Three things keep the coverage claim honest: sdk-coverage.spec.ts reads the
app's own router and fails when a route has no method reaching it,
sdk.e2e.spec.ts drives the client against createApp() over a socket with real
DNS and Postgres, and readme.spec.ts finds the methods by reflection so a
method added without a paragraph fails.
Comment thread packages/sdk/src/http.ts Outdated
Comment thread packages/sdk/src/http.ts Outdated
joaopcm added 4 commits August 7, 2026 23:51
…en the caller does

Three ways an exception could escape a client that promises never to throw:
a body that stops arriving after the headers, a `timeoutMs` that
`AbortSignal.timeout` refuses, and a backoff that grew unbounded with
`maxRetries`.

The first two are now errors like any other — `connection_error` and a new
`invalid_option` naming the option and the value. The ceiling that already
capped `Retry-After` now covers the exponential backoff too, so
`maxRetries: 10` stops after six attempts rather than waiting two minutes
inside one await.

A backoff also no longer outlives the signal: aborting mid-wait returns
`aborted` at once instead of sleeping to the end and spending one more fetch
on a signal that is already aborted.
@joaopcm

joaopcm commented Aug 7, 2026

Copy link
Copy Markdown
Owner Author

Review round, for the morning read.

Greptile raised two, both real, both fixed in 8b0d612 and their threads resolved:

  • P1, response failures escaping the envelope. response.text() rejecting after the headers arrived, and AbortSignal.timeout() rejecting an unusable timeoutMs, were the two ways an exception could leave a client that promises never to throw. The first is now connection_error; the second is a new invalid_option code naming the option and the value.
  • P2, retry waits ignoring cancellation. Aborting mid-backoff now returns aborted at once instead of sleeping to the end and spending one more fetch on a signal that is already aborted.

Reviewing the same loop turned up two more it did not flag:

  • The five-second ceiling covered Retry-After but not the exponential backoff, so maxRetries: 10 would have waited about two minutes inside one await. Both paths go through one nextWaitMs now (568152b was the follow-up for the related timeoutMs: 0 case, which reads as "no limit" everywhere else and here aborted before the request left).
  • A cancelled or timed-out call now gets the same answer whether it drops while reading the headers or the body (d1bc0ac).

And 872589f adds @propgate/sdk to the build matrix, so a broken .d.ts fails in CI rather than at publish.

Each fix ships a regression spec. 14/14 checks green.

@joaopcm
joaopcm merged commit 7c2c4e5 into main Aug 8, 2026
14 checks passed
@joaopcm
joaopcm deleted the feat/sdk branch August 8, 2026 00:11
@github-actions github-actions Bot mentioned this pull request Aug 8, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant