@ultimat3/mail 11.1.0 → 11.3.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/CLAUDE.md +7 -2
- package/README.md +1 -1
- package/package.json +6 -6
- package/src/driver-resend.ts +5 -9
package/CLAUDE.md
CHANGED
|
@@ -51,8 +51,13 @@
|
|
|
51
51
|
while its own `cause` said retrying cannot help. `errors.test.ts` asserts it through the QUEUE's
|
|
52
52
|
`nextRetryForError`, never against the table.
|
|
53
53
|
`stage` is the `SendStage` union in `errors.ts`; a new step goes there first.
|
|
54
|
-
The transient set is 4xx over SMTP, and 408/409/425/429 + 5xx over HTTP —
|
|
55
|
-
|
|
54
|
+
The transient set is 4xx over SMTP, and 408/409/425/429 + 5xx over HTTP — the HTTP half is
|
|
55
|
+
`isRetryableStatus` in **`@ultimat3/core`** (`retryable-status.ts`), `As of 2026-08-23`, and is
|
|
56
|
+
edited there. It was a private `RETRYABLE_STATUSES` in `driver-resend.ts` that was byte-identical
|
|
57
|
+
to `packages/cache/src/purge-http.ts`'s, in two packages that cannot import each other — so one
|
|
58
|
+
of the two was always going to be edited alone. Never restate it here.
|
|
59
|
+
`driver-resend.test.ts`'s "every status maps to the same retryable verdict" pins the whole table
|
|
60
|
+
through the driver, so the answer cannot move under this package without a failing test.
|
|
56
61
|
- A transport is selected from the environment by `selectMailDriver`, never from an `app.config.ts`
|
|
57
62
|
field — nothing loads that file's contents at runtime, so a `mail:` config block would be a
|
|
58
63
|
setting no boot could read. Two credentials at once is refused, not resolved: mail leaving by
|
package/README.md
CHANGED
|
@@ -93,7 +93,7 @@ with STARTTLS. Credentials are percent-decoded, so a password with `@` or `/` wo
|
|
|
93
93
|
|
|
94
94
|
`RESEND_API_KEY` and a verified sending domain. Every request carries `Idempotency-Key:
|
|
95
95
|
mailIdempotencyKey(message)` — a job retry after a timeout hands Resend the identical message, and
|
|
96
|
-
that header is what makes it one email. 408/409/425/429 and 5xx are retryable; every other non-2xx
|
|
96
|
+
that header is what makes it one email. 408/409/425/429 and 5xx are retryable (`isRetryableStatus`, `@ultimat3/core`); every other non-2xx
|
|
97
97
|
is a configuration problem that retrying cannot fix.
|
|
98
98
|
|
|
99
99
|
## Framework mails
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ultimat3/mail",
|
|
3
|
-
"version": "11.
|
|
3
|
+
"version": "11.3.0",
|
|
4
4
|
"description": "Transactional email as data: one template renders HTML and text, sent through a job.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -31,10 +31,10 @@
|
|
|
31
31
|
"test": "bun test"
|
|
32
32
|
},
|
|
33
33
|
"dependencies": {
|
|
34
|
-
"@ultimat3/core": "11.
|
|
35
|
-
"@ultimat3/i18n": "11.
|
|
36
|
-
"@ultimat3/jobs": "11.
|
|
37
|
-
"@ultimat3/schema": "11.
|
|
38
|
-
"@ultimat3/time": "11.
|
|
34
|
+
"@ultimat3/core": "11.3.0",
|
|
35
|
+
"@ultimat3/i18n": "11.3.0",
|
|
36
|
+
"@ultimat3/jobs": "11.3.0",
|
|
37
|
+
"@ultimat3/schema": "11.3.0",
|
|
38
|
+
"@ultimat3/time": "11.3.0"
|
|
39
39
|
}
|
|
40
40
|
}
|
package/src/driver-resend.ts
CHANGED
|
@@ -5,6 +5,11 @@
|
|
|
5
5
|
import {
|
|
6
6
|
ConfigInvalidError,
|
|
7
7
|
EnvMissingError,
|
|
8
|
+
// The status table is core's: this package's copy and `packages/cache/src/purge-http.ts`'s were
|
|
9
|
+
// byte-identical in two packages that cannot import each other, so one was always going to be
|
|
10
|
+
// edited alone. A throttle or a transient conflict can land unchanged; every other 4xx here is a
|
|
11
|
+
// config problem no retry fixes.
|
|
12
|
+
isRetryableStatus,
|
|
8
13
|
logger,
|
|
9
14
|
nanoid,
|
|
10
15
|
renderThrowable,
|
|
@@ -22,10 +27,6 @@ export type MailFetch = (input: string, init: RequestInit) => Promise<Response>;
|
|
|
22
27
|
const DEFAULT_TIMEOUT_MS = 15_000;
|
|
23
28
|
const MAX_DETAIL_LENGTH = 200;
|
|
24
29
|
|
|
25
|
-
// A 4xx here means the same request, unchanged, might land: a throttle or a momentary
|
|
26
|
-
// conflict. Every other 4xx is a config problem retrying cannot fix.
|
|
27
|
-
const RETRYABLE_STATUSES = new Set([408, 409, 425, 429]);
|
|
28
|
-
|
|
29
30
|
export interface ResendDriverOptions {
|
|
30
31
|
/** Read from `RESEND_API_KEY`. A literal key in app.config.ts is a key in git. */
|
|
31
32
|
readonly apiKey: string;
|
|
@@ -55,11 +56,6 @@ function requireFrom(from: string): string {
|
|
|
55
56
|
});
|
|
56
57
|
}
|
|
57
58
|
|
|
58
|
-
/** `retryable` for a throttle or a transient conflict; everything else is a config problem. */
|
|
59
|
-
function isRetryableStatus(status: number): boolean {
|
|
60
|
-
return status >= 500 || RETRYABLE_STATUSES.has(status);
|
|
61
|
-
}
|
|
62
|
-
|
|
63
59
|
function fixFor(status: number): string {
|
|
64
60
|
if (status === 401 || status === 403) {
|
|
65
61
|
return 'set RESEND_API_KEY to a current key from https://resend.com/api-keys, then retry';
|