Skip to content

feat!: Lettermint Node SDK 3.0 - #36

Merged
bjarn merged 8 commits into
mainfrom
feat/v3
Oct 4, 2026
Merged

bjarn merged 8 commits into
mainfrom
feat/v3

Conversation

@bjarn

@bjarn bjarn commented Oct 3, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Lettermint Node SDK 3.0, a new major version. The main reason is safety: in 2.x, Lettermint.email(token) returned one mutable builder per client. Two emails built at the same time on one client could mix recipients, content and Idempotency-Key, and an email abandoned halfway leaked into the next send. 3.0 stores nothing about a message on the client.

const lettermint = new Lettermint('lm_…');             // or { sendingToken }, { teamToken }, or both
await lettermint.emails.send({ from, to, subject, html }, { idempotencyKey: 'order-123' });
const base = lettermint.emails.compose().from('Acme <hi@acme.com>');   // immutable builder
for await (const domain of lettermint.domains.iterate()) { … }
const event = await new Webhook(secret).verify(rawBody, req.headers);

What changes

  • One client. new Lettermint({ sendingToken, teamToken }) replaces Lettermint.email() and Lettermint.api(). One token is enough.
    • Each part uses its own token and never falls back to the other: emails.* uses the sending token, the Team API uses the team token. ping(), messages.reschedule() and messages.cancel() accept either.
    • A single string is classified by format: lm_team_ plus letters and digits is a team token, lm_ plus letters and digits is a sending token. Anything else (such as lm_sso_…) throws.
  • Stateless sending. emails.send(), emails.sendBatch(), and an immutable emails.compose() builder where every setter returns a new builder. The idempotency key is a per-call option. Attachments are objects.
  • Typed errors. ApiError (with status and API code) and its subclasses for 401, 403, 404, 409, 422, 429 and 5xx. Also TimeoutError, ConnectionError, UnexpectedResponseError (empty or HTML bodies) and RedirectError. Class names survive the build.
  • Transport.
    • Redirects are never followed, so tokens never go to another host.
    • The timeout covers the response body.
    • Tokens never appear in console.log, util.inspect or JSON.stringify.
  • Runtimes. Node 20+, Bun, Deno and edge runtimes. No dependencies, no Buffer, no node: imports.
  • Team API. Typed query objects, CursorPage<T>, and iterate() helpers. A few methods moved, e.g. projects.routes(id) → routes.list(projectId).
  • Webhooks. verify() is async (Web Crypto). It requires both X-Lettermint-Signature and X-Lettermint-Delivery, accepts any matching v1, and accepts strings or bytes.
  • Types. Generated from the current API spec (lettermint/lettermint#2582) with its names, e.g. ListDomainsResponse, ProjectMutationResponse, ApiError. Enums are open, so new API values don't break typing.
  • Package. ESM and CommonJS with default and named exports. Unminified, without source maps (582 kB unpacked).
  • Upgrade with a coding agent: UPGRADE.md has a ready-to-copy instruction for Claude Code, Codex and similar tools. It ships in the package, so agents can read the guide from node_modules.
  • 2.x no longer receives updates. UPGRADE.md covers every changed call and the full type rename table. All 81 removed or renamed 2.x exports are listed (checked by script).
  • Includes the unreleased feat(webhooks)!: add Basic Auth and required read flags #35 (webhook Basic Auth types).

Release notes for the maintainer

  • Release as 3.0.0 by publishing a GitHub release v3.0.0. release.yaml sets the version from the tag. package.json says 3.0.0-dev on the branch.
  • The docs update is a separate PR: lettermint/lettermint#2590. Merge it after 3.0.0 is on npm.

Verification

  • 300 Jest tests pass on Node 20, 22, 24 and 26; a Bun run of the built package also passes.
  • Lint, type-check and build pass.
  • npm run test:smoke packs the tarball and checks:
    • the file list;
    • ESM import and CJS require;
    • types under nodenext;
    • that tokens stay out of debug output.
  • Shared conformance suite (private lettermint/sdk-generator): 42 of 42 scenarios pass with --strict. They cover concurrent and half-built emails, unknown enum values, empty and HTML bodies, redirects, timeouts, token redaction, token detection and 25 webhook vectors. For comparison, 2.8.0 fails 7.
  • README and UPGRADE snippets type-check against the build.

bjarn added 6 commits October 3, 2026 19:38
Adds src/generated/types.ts and operations.ts, emitted by the private
lettermint/sdk-generator (TypeScript emitter, next naming profile) from
the lettermint#2582 spec pinned at 80d8ab2a2d. scripts/generate.mjs
regenerates them, or with --check verifies them; without the generator
it only checks the generated headers.
… tokens

One client, `new Lettermint({ sendingToken, teamToken })` (or a token
string whose prefix selects the surface), replaces Lettermint.email() and
Lettermint.api(). Nothing about a message is stored on the client:
emails.send(message, { idempotencyKey }), emails.sendBatch() and an
immutable emails.compose() builder whose setters return new builders.
Concurrent sends on one client can no longer mix recipients, content or
Idempotency-Keys, and a failed build cannot leak into the next send.

- Team API sub-clients are thin wrappers over the generated operation
  table, with typed nested query objects and async iterate() helpers
  that follow next_cursor.
- Transport: fetch with redirect: 'manual' (3xx raises RedirectError),
  a timeout that covers headers and body, optional AbortSignal, no
  retries, and typed errors for every outcome (ApiError subclasses by
  status, TimeoutError, ConnectionError, UnexpectedResponseError for
  empty or non-JSON bodies).
- Tokens live in private fields; util.inspect and JSON.stringify of the
  client, sub-clients, builders and the webhook verifier redact them.
- Webhook verification uses Web Crypto, requires both the signature and
  the delivery header, accepts Headers or Node header records and
  reports a reason code.
- Runs on Node 20+, Bun, Deno and edge runtimes: no Buffer, no node:
  imports, no process. Built unminified as ESM and CJS with a default
  and named exports.

BREAKING CHANGE: Lettermint.email(), Lettermint.api(), ApiClient,
EmailEndpoint, Endpoint, LettermintClient, the .email property,
positional attach(), HttpRequestError, ClientError and QueryParams are
removed; error classes, type names and Webhook.verify() changed, and
Node 18 is no longer supported. See UPGRADE.md.
Drops Node 18 and 23 from the matrix, checks the generated-file headers
(the generator is private) and adds scripts/smoke.mjs, which packs the
package, installs the tarball into a temporary project and checks the
file list, ESM import, CJS require, token redaction and the type
declarations under moduleResolution nodenext.
The README covers tokens, the immutable builder, batch sending,
idempotency, scheduling, Sandbox, tags, attachments, the Team API with
pagination, errors, webhooks (Express and Fetch runtimes) and runtime
support. UPGRADE.md gives before/after code for every changed call, the
error and type rename tables and the removed exports, and keeps the
1.x to 2.0 guide below it.
…in docs

- Drop source maps from dist, which cuts the unpacked package from 1.0 MB
  to 582 kB.
- Name the sending token environment variable LETTERMINT_PROJECT_TOKEN,
  matching the documentation and the other SDKs.
- State that 2.x no longer receives updates.
@bjarn
bjarn requested a review from a team as a code owner October 3, 2026 18:35
bjarn added 2 commits October 3, 2026 23:08
The spec at lettermint/lettermint@0b4ecbdd23 (main, the #2582 merge) is
identical to the previous pin; only the generated file headers change.
@bjarn
bjarn merged commit 423c35d into main Oct 4, 2026
5 checks passed
@bjarn
bjarn deleted the feat/v3 branch October 4, 2026 10:43
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.

2 participants