@ultimat3/mail 11.1.0 → 11.2.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 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 — that HTTP set
55
- lives in `RETRYABLE_STATUSES` (`driver-resend.ts`) and is edited there, never restated.
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.1.0",
3
+ "version": "11.2.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.1.0",
35
- "@ultimat3/i18n": "11.1.0",
36
- "@ultimat3/jobs": "11.1.0",
37
- "@ultimat3/schema": "11.1.0",
38
- "@ultimat3/time": "11.1.0"
34
+ "@ultimat3/core": "11.2.0",
35
+ "@ultimat3/i18n": "11.2.0",
36
+ "@ultimat3/jobs": "11.2.0",
37
+ "@ultimat3/schema": "11.2.0",
38
+ "@ultimat3/time": "11.2.0"
39
39
  }
40
40
  }
@@ -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';