@wtfalch/mailer 0.3.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,7 +1,7 @@
1
1
  # @wtfalch/mailer
2
2
 
3
3
  One `send(template, to, data)` call for the estate's transactional email,
4
- over Resend, with a dev transport that writes to disk instead of sending.
4
+ over Lettermint, with a dev transport that writes to disk instead of sending.
5
5
 
6
6
  ## Install
7
7
 
@@ -28,9 +28,9 @@ await send({
28
28
  brand: { name: 'Acme' }, // optional
29
29
  },
30
30
  from: {
31
- fromAddress: 'no-reply@acme.wtfalch.dev',
31
+ fromAddress: 'no-reply@acme.example.com',
32
32
  fromName: 'Acme', // optional
33
- replyTo: 'support@acme.wtfalch.dev', // optional
33
+ replyTo: 'support@acme.example.com', // optional
34
34
  locale: 'nb', // optional, defaults to 'en' -- picks which language's copy renders
35
35
  },
36
36
  attachments: [ // optional
@@ -45,9 +45,19 @@ await send({
45
45
  missing transport fails loudly instead of silently no-op'ing or making a
46
46
  live call with no key.
47
47
 
48
- A held key: however you resolve `apiKey` for `createResendTransport`,
48
+ Two env names change that default, in every `NODE_ENV`: with
49
+ `LETTERMINT_API_URL` set (a base URL such as `https://sink.example.com/v1`;
50
+ https, or http for `localhost`, `127.0.0.1` or `[::1]` only) `send()` sends
51
+ through Lettermint's API shape to that server, with the token from
52
+ `LETTERMINT_API_TOKEN` in the `x-lettermint-token` header. This is how a
53
+ stage app, a production build holding real addresses, sends every mail to a
54
+ sink. A malformed URL, or a URL with no token, throws `MailerConfigError`
55
+ when the default transport is built, before any send. The same URL is
56
+ available per transport as `createLettermintTransport({ apiKey, baseUrl })`.
57
+
58
+ A held key: however you resolve `apiKey` for `createLettermintTransport`,
49
59
  mailer never fetches or stores it. In the estate, the caller resolves the
50
- org's Resend key as a held key from `@wtfalch/keys` and passes it in on
60
+ org's Lettermint key as a held key from `@wtfalch/keys` and passes it in on
51
61
  every call.
52
62
 
53
63
  ### Locale
@@ -60,22 +70,14 @@ already uses (`WORDS[sender.locale] ?? WORDS.en`).
60
70
 
61
71
  ### Transports
62
72
 
63
- A held key: however you resolve `apiKey` for either transport below, mailer
73
+ A held key: however you resolve `apiKey` for the transport below, mailer
64
74
  never fetches or stores it. In the estate, the caller resolves the org's
65
- Resend or Lettermint key as a held key from `@wtfalch/keys` and passes it
75
+ Lettermint key as a held key from `@wtfalch/keys` and passes it
66
76
  in on every call.
67
77
 
68
78
  - `createDevTransport({ dir })` -- writes each message to
69
79
  `<dir>/<timestamp>-<id>.json` (default `dir`: `.mailer/dev`). Never
70
80
  opens a network connection.
71
- - `createResendTransport({ apiKey })` -- sends over Resend. Needs either
72
- `apiKey` (a plain string -- resolve it yourself, e.g. from
73
- `@wtfalch/keys`) or `client`, a minimal structural `ResendLike` for
74
- injecting a fake in tests. `resend` is an optional peer dependency
75
- (`^4.1.0`) -- install it yourself if you use this transport with `apiKey`;
76
- not needed if you always inject `client`, or if you only use
77
- `createLettermintTransport` below. Attachments passed to `send()` are
78
- forwarded as Resend's own `{ filename, content, content_type }` shape.
79
81
  - `createLettermintTransport({ apiKey })` -- sends over Lettermint's
80
82
  Sending API (EU-hosted). Needs either `apiKey` (the org's Lettermint
81
83
  Project API token, `lm_...` -- resolve it yourself, e.g. from
@@ -126,7 +128,7 @@ await send({
126
128
  template: 'verify',
127
129
  to: 'person@example.com',
128
130
  data: { productName: 'Acme', url: 'https://app.example.com/verify/abc', code: '482913' },
129
- from: { fromAddress: 'no-reply@acme.wtfalch.dev', locale: 'nb' },
131
+ from: { fromAddress: 'no-reply@acme.example.com', locale: 'nb' },
130
132
  });
131
133
  ```
132
134
 
@@ -142,5 +144,5 @@ From `packages/mailer`:
142
144
  pnpm test
143
145
  ```
144
146
 
145
- Tests run against the dev transport and a fake `ResendLike` client --
147
+ Tests run against the dev transport and a fake `LettermintLike` client --
146
148
  never a live key, never the network.
@@ -7,7 +7,7 @@ function isControlCharCode(code) {
7
7
  * True if `value` contains a CR, LF or any other control character. Checked
8
8
  * by character code rather than a regex, since a header-bound field is
9
9
  * built in part from caller-supplied strings (an inviter's name, an org's
10
- * name) and this package must not depend on Resend's own defence against
10
+ * name) and this package must not depend on the mail provider's own defence against
11
11
  * header injection to reject them (see issue #3).
12
12
  */
13
13
  function hasControlCharacter(value) {
package/dist/index.d.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  export type { Attachment, Brand, Locale, MailMessage, SenderIdentity, SendResult, } from './types.js';
2
2
  export { DEFAULT_LOCALE, MailerConfigError } from './types.js';
3
- export type { DevTransportOptions, LettermintAttachment, LettermintFetchLike, LettermintLike, LettermintSendPayload, LettermintTransportOptions, OutgoingMail, ResendLike, ResendTransportOptions, Transport, } from './transport.js';
4
- export { createDevTransport, createLettermintTransport, createResendTransport, defaultTransport, LettermintDomainRefusedError, } from './transport.js';
3
+ export type { DevTransportOptions, LettermintAttachment, LettermintFetchLike, LettermintLike, LettermintSendPayload, LettermintTransportOptions, OutgoingMail, Transport, } from './transport.js';
4
+ export { createDevTransport, createLettermintTransport, defaultTransport, LettermintDomainRefusedError, } from './transport.js';
5
5
  export type { SendOptions } from './send.js';
6
6
  export { send } from './send.js';
7
7
  export type { TemplateData, TemplateName } from './templates/index.js';
package/dist/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  export { DEFAULT_LOCALE, MailerConfigError } from './types.js';
2
- export { createDevTransport, createLettermintTransport, createResendTransport, defaultTransport, LettermintDomainRefusedError, } from './transport.js';
2
+ export { createDevTransport, createLettermintTransport, defaultTransport, LettermintDomainRefusedError, } from './transport.js';
3
3
  export { send } from './send.js';
4
4
  export { templates } from './templates/index.js';
5
5
  export { renderInvitation } from './templates/invitation.js';
package/dist/layout.d.ts CHANGED
@@ -23,7 +23,7 @@ export interface LayoutOptions {
23
23
  * given, except for `brand.name`, which it escapes itself.
24
24
  *
25
25
  * `subject` is checked for CR/LF and other control characters before it is
26
- * returned, since it is not HTML-escaped and reaches Resend as a header
26
+ * returned, since it is not HTML-escaped and reaches the mail provider as a header
27
27
  * value -- see `header-safe.ts`.
28
28
  */
29
29
  export declare function layout(options: LayoutOptions): MailMessage;
package/dist/layout.js CHANGED
@@ -20,7 +20,7 @@ const BRAND_STYLE = 'margin:0 0 20px;font-size:13px;color:#545b66;text-transform
20
20
  * given, except for `brand.name`, which it escapes itself.
21
21
  *
22
22
  * `subject` is checked for CR/LF and other control characters before it is
23
- * returned, since it is not HTML-escaped and reaches Resend as a header
23
+ * returned, since it is not HTML-escaped and reaches the mail provider as a header
24
24
  * value -- see `header-safe.ts`.
25
25
  */
26
26
  export function layout(options) {
@@ -2,7 +2,7 @@ import { type Attachment, type MailMessage, type SendResult } from './types.js';
2
2
  /** A rendered message, addressed and ready for a transport. */
3
3
  export interface OutgoingMail extends MailMessage {
4
4
  readonly to: string;
5
- /** Already formatted, e.g. `"Acme <no-reply@acme.wtfalch.dev>"` or a bare address. */
5
+ /** Already formatted, e.g. `"Acme <no-reply@acme.example.com>"` or a bare address. */
6
6
  readonly from: string;
7
7
  readonly replyTo?: string;
8
8
  readonly attachments?: readonly Attachment[];
@@ -16,63 +16,10 @@ export interface DevTransportOptions {
16
16
  }
17
17
  /**
18
18
  * Writes every message to `<dir>/<timestamp>-<id>.json` instead of sending
19
- * it. Never opens a network connection -- the gap the estate had nowhere:
20
- * `outbox.ts` and `queue.ts` both call `new Resend()` unconditionally, so
21
- * nothing that sends mail could be tested without a live key.
19
+ * it. Never opens a network connection, so nothing that sends mail needs
20
+ * a live key to be tested.
22
21
  */
23
22
  export declare function createDevTransport(options?: DevTransportOptions): Transport;
24
- /**
25
- * The minimal shape this package calls on a Resend client: `emails.send`
26
- * only, with the fields mailer actually sets. The real `Resend` class
27
- * satisfies this structurally, and so does a fake with no network access,
28
- * which is what this package's own tests inject (never a real key, never a
29
- * real send -- see `transport.test.ts`).
30
- */
31
- export interface ResendLike {
32
- emails: {
33
- send(payload: {
34
- from: string;
35
- to: string;
36
- subject: string;
37
- html: string;
38
- text: string;
39
- reply_to?: string;
40
- /** Resend's own field names: `content` is base64 (or a Buffer), `content_type` is
41
- * optional and derived from `filename` when absent --
42
- * https://resend.com/docs/api-reference/emails/send-email. */
43
- attachments?: Array<{
44
- filename: string;
45
- content: string;
46
- content_type?: string;
47
- }>;
48
- }): Promise<{
49
- data: {
50
- id: string;
51
- } | null;
52
- error: {
53
- message: string;
54
- } | null;
55
- }>;
56
- };
57
- }
58
- export interface ResendTransportOptions {
59
- /** The Resend API key. The caller resolves this from `@wtfalch/keys`; mailer never fetches or stores it. */
60
- readonly apiKey?: string;
61
- /** A client to use instead of constructing one from `apiKey` -- how tests inject a fake. */
62
- readonly client?: ResendLike;
63
- }
64
- /**
65
- * Sends over Resend. The one place in the estate that imports the `resend`
66
- * package (README: "This package is the one place that knows about
67
- * Resend") -- and, since [0006](../docs/adr/0006-lettermint-transport-resend-optional-peer.md),
68
- * it imports it lazily: `resend` is now an optional peer, not a runtime
69
- * dependency, so a consumer that only ever passes `client` (every test in
70
- * this repo, and any caller that only sends over Lettermint) never needs
71
- * the package installed at all. The `import('resend')` below runs at most
72
- * once per transport, on the first `send()` call, only when no `client` was
73
- * given.
74
- */
75
- export declare function createResendTransport(options: ResendTransportOptions): Transport;
76
23
  /**
77
24
  * The minimal shape this package calls on `fetch` to speak Lettermint's
78
25
  * Sending API directly -- the same no-SDK, injectable-`fetch` posture
@@ -84,6 +31,7 @@ export type LettermintFetchLike = (url: string, init: {
84
31
  method: string;
85
32
  headers: Record<string, string>;
86
33
  body: string;
34
+ signal?: AbortSignal;
87
35
  }) => Promise<{
88
36
  ok: boolean;
89
37
  status: number;
@@ -93,7 +41,7 @@ export interface LettermintAttachment {
93
41
  readonly filename: string;
94
42
  /**
95
43
  * Snake-case, matching the Sending API's own wire field exactly --
96
- * unlike `ResendLike`, `LettermintSendPayload` has no translation step
44
+ * `LettermintSendPayload` has no translation step
97
45
  * before `JSON.stringify` (`createLettermintClient`'s `send`), so this
98
46
  * interface's own field names ARE the request body's JSON keys.
99
47
  */
@@ -121,7 +69,7 @@ export interface LettermintSendPayload {
121
69
  }
122
70
  /**
123
71
  * The minimal shape this package calls on a Lettermint client: `send`
124
- * only, the same structural-injection pattern `ResendLike` uses. The
72
+ * only, a structural interface so a test can inject a fake. The
125
73
  * default client (built from `apiKey`) satisfies this over plain `fetch`;
126
74
  * a fake with no network access is what this package's own tests inject.
127
75
  */
@@ -140,6 +88,12 @@ export interface LettermintTransportOptions {
140
88
  /** Injectable `fetch`, used only when building the default client from
141
89
  * `apiKey`. Defaults to `globalThis.fetch`. */
142
90
  readonly fetch?: LettermintFetchLike;
91
+ /** The Sending API base URL, without `/send`. Default
92
+ * `https://api.lettermint.co/v1`. Must be https, except for a local sink
93
+ * host (`localhost`, `127.0.0.1`, `[::1]`), so a misconfigured value
94
+ * cannot send mail or the token over plain http. Ignored when `client`
95
+ * is given. */
96
+ readonly baseUrl?: string;
143
97
  }
144
98
  /**
145
99
  * Thrown when Lettermint refuses to send because the `from` domain is not
@@ -159,8 +113,8 @@ export declare class LettermintDomainRefusedError extends Error {
159
113
  constructor(message: string);
160
114
  }
161
115
  /**
162
- * Sends over Lettermint (EU, GDPR -- William's 2026-09-28 choice over
163
- * Resend, see [0006](../docs/adr/0006-lettermint-transport-resend-optional-peer.md)),
116
+ * Sends over Lettermint (EU, GDPR; see
117
+ * [0006](../docs/adr/0006-lettermint-transport.md)),
164
118
  * via plain `fetch` against Lettermint's Sending API. Base URL, auth header
165
119
  * and request/response shapes:
166
120
  * https://lettermint.co/docs/api-reference/sending/0.0.1/lettermint-sending-openapi.json
@@ -175,10 +129,13 @@ export declare class LettermintDomainRefusedError extends Error {
175
129
  */
176
130
  export declare function createLettermintTransport(options: LettermintTransportOptions): Transport;
177
131
  /**
178
- * `dev`, unconditionally, outside `NODE_ENV=production`. In production
179
- * there is no default: `send()` must be given an explicit transport,
180
- * because mailer never holds a Resend API key of its own to fall back to.
181
- * Calling `send()` in production with no transport is a configuration bug,
182
- * not a silent no-op and not an accidental live send.
132
+ * When `LETTERMINT_API_URL` is set (in any `NODE_ENV`), returns a Lettermint
133
+ * transport that sends to that base URL with the token in
134
+ * `LETTERMINT_API_TOKEN`: how a stage app, a production build holding real
135
+ * addresses, sends every mail to a sink server. A malformed URL, or a URL
136
+ * with no token, throws `MailerConfigError` here, before any send.
137
+ *
138
+ * Otherwise: `dev` outside `NODE_ENV=production`; in production there is no
139
+ * default, and `send()` must be given an explicit transport.
183
140
  */
184
141
  export declare function defaultTransport(): Transport;
package/dist/transport.js CHANGED
@@ -4,9 +4,8 @@ import { join } from 'node:path';
4
4
  import { MailerConfigError } from './types.js';
5
5
  /**
6
6
  * Writes every message to `<dir>/<timestamp>-<id>.json` instead of sending
7
- * it. Never opens a network connection -- the gap the estate had nowhere:
8
- * `outbox.ts` and `queue.ts` both call `new Resend()` unconditionally, so
9
- * nothing that sends mail could be tested without a live key.
7
+ * it. Never opens a network connection, so nothing that sends mail needs
8
+ * a live key to be tested.
10
9
  */
11
10
  export function createDevTransport(options = {}) {
12
11
  const dir = options.dir ?? '.mailer/dev';
@@ -20,56 +19,6 @@ export function createDevTransport(options = {}) {
20
19
  },
21
20
  };
22
21
  }
23
- /**
24
- * Sends over Resend. The one place in the estate that imports the `resend`
25
- * package (README: "This package is the one place that knows about
26
- * Resend") -- and, since [0006](../docs/adr/0006-lettermint-transport-resend-optional-peer.md),
27
- * it imports it lazily: `resend` is now an optional peer, not a runtime
28
- * dependency, so a consumer that only ever passes `client` (every test in
29
- * this repo, and any caller that only sends over Lettermint) never needs
30
- * the package installed at all. The `import('resend')` below runs at most
31
- * once per transport, on the first `send()` call, only when no `client` was
32
- * given.
33
- */
34
- export function createResendTransport(options) {
35
- if (!options.client && !options.apiKey) {
36
- throw new MailerConfigError('createResendTransport needs either "apiKey" or "client".');
37
- }
38
- let resolvedClient;
39
- function resolveClient() {
40
- if (options.client)
41
- return Promise.resolve(options.client);
42
- resolvedClient ??= import('resend').then(({ Resend }) => new Resend(options.apiKey));
43
- return resolvedClient;
44
- }
45
- return {
46
- async send(message) {
47
- const client = await resolveClient();
48
- const { data, error } = await client.emails.send({
49
- from: message.from,
50
- to: message.to,
51
- subject: message.subject,
52
- html: message.html,
53
- text: message.text,
54
- ...(message.replyTo ? { reply_to: message.replyTo } : {}),
55
- ...(message.attachments
56
- ? {
57
- attachments: message.attachments.map((attachment) => ({
58
- filename: attachment.filename,
59
- content: attachment.content,
60
- ...(attachment.contentType ? { content_type: attachment.contentType } : {}),
61
- })),
62
- }
63
- : {}),
64
- });
65
- if (error)
66
- throw new Error(`mailer: resend refused the message: ${error.message}`);
67
- if (!data)
68
- throw new Error('mailer: resend returned no data and no error');
69
- return { id: data.id, transport: 'resend' };
70
- },
71
- };
72
- }
73
22
  /**
74
23
  * Thrown when Lettermint refuses to send because the `from` domain is not
75
24
  * one the sending project may use -- either unverified, or verified but
@@ -90,7 +39,33 @@ export class LettermintDomainRefusedError extends Error {
90
39
  this.name = 'LettermintDomainRefusedError';
91
40
  }
92
41
  }
93
- const LETTERMINT_SEND_URL = 'https://api.lettermint.co/v1/send';
42
+ const LETTERMINT_BASE_URL = 'https://api.lettermint.co/v1';
43
+ const LETTERMINT_TIMEOUT_MS = 30_000;
44
+ const LOCAL_SINK_HOSTS = new Set(['localhost', '127.0.0.1', '[::1]']);
45
+ /**
46
+ * Validates a Sending API base URL where it is read and returns it without a
47
+ * trailing slash. Rule: https, or http only for a local sink host. Anything
48
+ * else throws `MailerConfigError`, so a bad value fails at start, not at the
49
+ * first send.
50
+ */
51
+ function parseBaseUrl(raw, source) {
52
+ let url;
53
+ try {
54
+ url = new URL(raw);
55
+ }
56
+ catch {
57
+ throw new MailerConfigError(`mailer: ${source} is not a valid URL.`);
58
+ }
59
+ // A single-label host (a container name such as `acme-stage-sink`) only resolves on the internal network.
60
+ const local = url.protocol === 'http:' && (LOCAL_SINK_HOSTS.has(url.hostname) || !url.hostname.includes('.'));
61
+ if (url.protocol !== 'https:' && !local) {
62
+ throw new MailerConfigError(`mailer: ${source} must be an https URL (http is allowed only for localhost, 127.0.0.1, [::1] or a single-label internal host).`);
63
+ }
64
+ if (url.username || url.password || url.search || url.hash) {
65
+ throw new MailerConfigError(`mailer: ${source} must not carry credentials, a query string or a fragment.`);
66
+ }
67
+ return raw.replace(/\/+$/, '');
68
+ }
94
69
  /**
95
70
  * Matches Lettermint's confirmed domain-restriction refusal message
96
71
  * (`LettermintDomainRefusedError`'s own doc comment has the exact quote and
@@ -108,13 +83,14 @@ function defaultLettermintFetch() {
108
83
  }
109
84
  return globalThis.fetch.bind(globalThis);
110
85
  }
111
- function createLettermintClient(apiKey, fetchImpl) {
86
+ function createLettermintClient(apiKey, fetchImpl, baseUrl) {
112
87
  return {
113
88
  async send(payload) {
114
- const response = await fetchImpl(LETTERMINT_SEND_URL, {
89
+ const response = await fetchImpl(`${baseUrl}/send`, {
115
90
  method: 'POST',
116
91
  headers: { 'x-lettermint-token': apiKey, 'content-type': 'application/json' },
117
92
  body: JSON.stringify(payload),
93
+ signal: AbortSignal.timeout(LETTERMINT_TIMEOUT_MS),
118
94
  });
119
95
  const body = (await response.json());
120
96
  if (!response.ok) {
@@ -128,8 +104,8 @@ function createLettermintClient(apiKey, fetchImpl) {
128
104
  };
129
105
  }
130
106
  /**
131
- * Sends over Lettermint (EU, GDPR -- William's 2026-09-28 choice over
132
- * Resend, see [0006](../docs/adr/0006-lettermint-transport-resend-optional-peer.md)),
107
+ * Sends over Lettermint (EU, GDPR; see
108
+ * [0006](../docs/adr/0006-lettermint-transport.md)),
133
109
  * via plain `fetch` against Lettermint's Sending API. Base URL, auth header
134
110
  * and request/response shapes:
135
111
  * https://lettermint.co/docs/api-reference/sending/0.0.1/lettermint-sending-openapi.json
@@ -146,8 +122,9 @@ export function createLettermintTransport(options) {
146
122
  if (!options.client && !options.apiKey) {
147
123
  throw new MailerConfigError('createLettermintTransport needs either "apiKey" or "client".');
148
124
  }
125
+ const baseUrl = options.baseUrl === undefined ? LETTERMINT_BASE_URL : parseBaseUrl(options.baseUrl, 'baseUrl');
149
126
  const client = options.client ??
150
- createLettermintClient(options.apiKey, options.fetch ?? defaultLettermintFetch());
127
+ createLettermintClient(options.apiKey, options.fetch ?? defaultLettermintFetch(), baseUrl);
151
128
  return {
152
129
  async send(message) {
153
130
  const { message_id, status } = await client.send({
@@ -172,15 +149,27 @@ export function createLettermintTransport(options) {
172
149
  };
173
150
  }
174
151
  /**
175
- * `dev`, unconditionally, outside `NODE_ENV=production`. In production
176
- * there is no default: `send()` must be given an explicit transport,
177
- * because mailer never holds a Resend API key of its own to fall back to.
178
- * Calling `send()` in production with no transport is a configuration bug,
179
- * not a silent no-op and not an accidental live send.
152
+ * When `LETTERMINT_API_URL` is set (in any `NODE_ENV`), returns a Lettermint
153
+ * transport that sends to that base URL with the token in
154
+ * `LETTERMINT_API_TOKEN`: how a stage app, a production build holding real
155
+ * addresses, sends every mail to a sink server. A malformed URL, or a URL
156
+ * with no token, throws `MailerConfigError` here, before any send.
157
+ *
158
+ * Otherwise: `dev` outside `NODE_ENV=production`; in production there is no
159
+ * default, and `send()` must be given an explicit transport.
180
160
  */
181
161
  export function defaultTransport() {
162
+ const sinkUrl = process.env.LETTERMINT_API_URL;
163
+ if (sinkUrl) {
164
+ const baseUrl = parseBaseUrl(sinkUrl, 'LETTERMINT_API_URL');
165
+ const apiKey = process.env.LETTERMINT_API_TOKEN;
166
+ if (!apiKey) {
167
+ throw new MailerConfigError('mailer: LETTERMINT_API_URL is set but LETTERMINT_API_TOKEN is not.');
168
+ }
169
+ return createLettermintTransport({ apiKey, baseUrl });
170
+ }
182
171
  if (process.env.NODE_ENV === 'production') {
183
- throw new MailerConfigError('send() needs an explicit transport in production: pass { transport: createResendTransport({ apiKey }) }.');
172
+ throw new MailerConfigError('send() needs an explicit transport in production: pass { transport: createLettermintTransport({ apiKey }) }.');
184
173
  }
185
174
  return createDevTransport();
186
175
  }
package/dist/types.d.ts CHANGED
@@ -42,10 +42,8 @@ export interface SenderIdentity {
42
42
  }
43
43
  /**
44
44
  * One file attached to a send. `content` is base64-encoded bytes -- the
45
- * convention both transports' real APIs use (Resend:
46
- * https://resend.com/docs/api-reference/emails/send-email, "Content of an
47
- * attached file, passed as a buffer or Base64 string"; Lettermint's Sending
48
- * API OpenAPI spec, `attachments[].content`, and its official SDKs, which
45
+ * convention the Lettermint Sending API uses (its
46
+ * OpenAPI spec, `attachments[].content`, and its official SDKs, which
49
47
  * all base64-encode before setting it).
50
48
  */
51
49
  export interface Attachment {
@@ -62,11 +60,11 @@ export interface MailMessage {
62
60
  /** What a transport returns once it has accepted a message. */
63
61
  export interface SendResult {
64
62
  readonly id: string;
65
- readonly transport: 'resend' | 'dev' | 'lettermint';
63
+ readonly transport: 'dev' | 'lettermint';
66
64
  }
67
65
  /**
68
66
  * `send()` has no usable default (no transport given, and either
69
- * production with no explicit transport, or a Resend transport built with
67
+ * production with no explicit transport, or a transport built with
70
68
  * neither `apiKey` nor `client`). Thrown rather than a silent no-op or an
71
69
  * accidental network call.
72
70
  */
package/dist/types.js CHANGED
@@ -2,7 +2,7 @@
2
2
  export const DEFAULT_LOCALE = 'en';
3
3
  /**
4
4
  * `send()` has no usable default (no transport given, and either
5
- * production with no explicit transport, or a Resend transport built with
5
+ * production with no explicit transport, or a transport built with
6
6
  * neither `apiKey` nor `client`). Thrown rather than a silent no-op or an
7
7
  * accidental network call.
8
8
  */
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@wtfalch/mailer",
3
- "version": "0.3.0",
4
- "description": "One send(template, to, data) call for the estate's transactional email, over Resend, with a dev transport that writes to disk instead of sending.",
3
+ "version": "0.5.0",
4
+ "description": "One send(template, to, data) call for the estate's transactional email, over Lettermint, with a dev transport that writes to disk instead of sending.",
5
5
  "repository": {
6
6
  "type": "git",
7
7
  "url": "https://github.com/wtfalch/mailer",
@@ -33,17 +33,8 @@
33
33
  "dependencies": {
34
34
  "@wtfalch/utils": "0.1.0"
35
35
  },
36
- "peerDependencies": {
37
- "resend": "^4.1.0"
38
- },
39
- "peerDependenciesMeta": {
40
- "resend": {
41
- "optional": true
42
- }
43
- },
44
36
  "devDependencies": {
45
37
  "@types/node": "^22",
46
- "resend": "^4.1.0",
47
38
  "typescript": "^5.9.0",
48
39
  "vitest": "^4.1.6"
49
40
  }