@nxgt/janus-mail 0.4.2 → 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.
Files changed (54) hide show
  1. package/README.md +78 -25
  2. package/dist/codes-left.d.ts +6 -0
  3. package/dist/codes-left.d.ts.map +1 -0
  4. package/dist/generated/codes-left.d.ts +3 -0
  5. package/dist/generated/codes-left.d.ts.map +1 -0
  6. package/dist/generated/mail.d.ts +7 -0
  7. package/dist/generated/mail.d.ts.map +1 -1
  8. package/dist/index.d.ts +1 -1
  9. package/dist/index.js +130 -21
  10. package/dist/index.js.map +11 -6
  11. package/dist/janus-mail.d.ts +1 -0
  12. package/dist/janus-mail.d.ts.map +1 -1
  13. package/dist/links.d.ts +10 -0
  14. package/dist/links.d.ts.map +1 -0
  15. package/dist/options.d.ts.map +1 -1
  16. package/dist/recovery-mail.d.ts +5 -0
  17. package/dist/recovery-mail.d.ts.map +1 -0
  18. package/dist/refuse.d.ts +3 -0
  19. package/dist/refuse.d.ts.map +1 -0
  20. package/dist/template-types.d.ts +15 -2
  21. package/dist/template-types.d.ts.map +1 -1
  22. package/dist/templates.d.ts +1 -1
  23. package/dist/templates.d.ts.map +1 -1
  24. package/dist/types.d.ts +25 -0
  25. package/dist/types.d.ts.map +1 -1
  26. package/docs/README.md +4 -3
  27. package/docs/guide/building.md +58 -9
  28. package/docs/guide/locales.md +13 -4
  29. package/docs/guide/sending.md +61 -7
  30. package/docs/guide/templates.md +41 -16
  31. package/docs/roadmap.md +15 -1
  32. package/docs/troubleshooting.md +114 -21
  33. package/mails/en/email-changed.html +5 -5
  34. package/mails/en/password-changed.html +5 -5
  35. package/mails/en/recovery-code-used.html +53 -0
  36. package/mails/en/recovery-code-used.txt +23 -0
  37. package/mails/en/reset-password.html +6 -6
  38. package/mails/en/sign-in-code.html +3 -3
  39. package/mails/en/two-factor-disabled.html +5 -5
  40. package/mails/en/two-factor-enabled.html +5 -5
  41. package/mails/en/verify-email.html +6 -6
  42. package/mails/en/welcome.html +5 -5
  43. package/mails/fr/email-changed.html +5 -5
  44. package/mails/fr/password-changed.html +5 -5
  45. package/mails/fr/recovery-code-used.html +52 -0
  46. package/mails/fr/recovery-code-used.txt +23 -0
  47. package/mails/fr/reset-password.html +6 -6
  48. package/mails/fr/sign-in-code.html +3 -3
  49. package/mails/fr/two-factor-disabled.html +5 -5
  50. package/mails/fr/two-factor-enabled.html +5 -5
  51. package/mails/fr/verify-email.html +6 -6
  52. package/mails/fr/welcome.html +5 -5
  53. package/mails/mail-manifest.json +26 -0
  54. package/package.json +11 -10
@@ -27,6 +27,7 @@ export const mail = janusMail({
27
27
  resetPassword: (token) => `https://acme.example/reset?token=${encodeURIComponent(token)}`,
28
28
  secureAccount: () => 'https://acme.example/account/security',
29
29
  getStarted: () => 'https://acme.example/',
30
+ recoveryCodes: () => 'https://acme.example/account/recovery-codes', // optional
30
31
  },
31
32
  });
32
33
  ```
@@ -47,6 +48,7 @@ time one is sent, and kept.
47
48
  | `links.resetPassword` | `(token) => string` | required | The page that sets a new password, given the one-time token |
48
49
  | `links.secureAccount` | `() => string` | required | Where a user who made no change secures their account: the notices link to it |
49
50
  | `links.getStarted` | `() => string` | required | Where a new user starts — your home page, or your sign-in page for an account someone else created: the welcome's **Get started** button links to it |
51
+ | `links.recoveryCodes` | `() => string` | `links.secureAccount` | Where a user regenerates their recovery codes: the recovery code notice links to it, from its **Secure my account** button |
50
52
  | `locales` | `readonly L[]` | `['en', 'fr']` | The locales sent in — see [Locales](locales.md) |
51
53
  | `fallbackLocale` | one of `locales` | `'en'`, else the first of `locales` | The locale when the recipient wants none of `locales` |
52
54
  | `templates` | `Partial<JanusMailTemplates<L>>` | none | Your own templates, over the defaults — see [Templates](templates.md) |
@@ -198,6 +200,54 @@ and the address from the user. With several user types, check
198
200
  and `signUp` answers as it would have. For a welcome that must arrive, put
199
201
  the event in a queue that retries a `MAIL_FAILED`, and send from there.
200
202
 
203
+ ### `recoveryCodeUsed(to, { when, recoveryCodesLeft })`
204
+
205
+ ```ts
206
+ // On the user event @nxgt/janus sends once a recovery code is spent:
207
+ const auth = janus({
208
+ ...config,
209
+ async events(event) {
210
+ if (event.type !== 'user.recoveryCodeUsed') return;
211
+ const user = await auth.get(event.userId);
212
+ const recoveryCodesLeft = await auth.secondFactor.recoveryCodesLeft(user); // @nxgt/janus 0.11
213
+ if (recoveryCodesLeft === null) return; // the factor was turned off since
214
+ const when = new Intl.DateTimeFormat(user.locale, {
215
+ dateStyle: 'long',
216
+ timeStyle: 'short',
217
+ timeZone: user.timeZone, // a field of yours, when you keep one
218
+ }).format(event.occurredAt);
219
+ await mail.recoveryCodeUsed(
220
+ { name: user.name, locale: user.locale, email: user.email },
221
+ { when, recoveryCodesLeft },
222
+ );
223
+ },
224
+ });
225
+ ```
226
+
227
+ A notice for the account at `to.email`: a recovery code was used — a
228
+ sign-in without the user's phone — at `when`, and how many codes are left,
229
+ with a **Secure my account** button. Send it on the `user.recoveryCodeUsed`
230
+ event, which `auth.secondFactor.recover` sends once the code is spent, even
231
+ when opening the session then fails.
232
+
233
+ - **`when` is text you write**, in the recipient's locale and time zone:
234
+ "28 septembre 2026 à 14:05". The event's `occurredAt` is when the code
235
+ was spent.
236
+ - **`recoveryCodesLeft` is the count, or the sentence.** The event does not
237
+ carry it: `auth.secondFactor.recoveryCodesLeft(user)` reads it after the
238
+ write, the code just spent already out, and answers `null` for a user
239
+ with no active factor — check it, the compiler asks you to. A count is
240
+ written as `@nxgt/mail-presets`' plural in the recipient's locale: "You
241
+ have no recovery codes left.", "You have 1 recovery code left.", "Il vous
242
+ reste 9 codes de récupération." — the build's own catalogue, parsed when
243
+ this package is built, and formatted at send time with the locale's
244
+ plural rules. Text is sent as it is: pass the sentence yourself for
245
+ another wording, or a locale the defaults are not built in, where a count
246
+ is a `TypeError`.
247
+ - **The link is `links.recoveryCodes()`**, where the user regenerates their
248
+ codes; without it, `links.secureAccount()`. `recoveryCodes` is optional,
249
+ so a `links` written for 0.4 still compiles.
250
+
201
251
  ## The expiry
202
252
 
203
253
  `verifyEmail`, `resetPassword` and `signInCode` say how long the link or the
@@ -252,6 +302,7 @@ a `TypeError`, and nothing reaches the mailer: revive the date with
252
302
  | `passwordChanged` | `to.email` | — |
253
303
  | `emailChanged` | `to.formerEmail` | `to.newEmail` |
254
304
  | `twoFactorEnabled`, `twoFactorDisabled` | `to.email` | — |
305
+ | `recoveryCodeUsed` | `to.email` | — |
255
306
  | `welcome` | `to.email` | — |
256
307
 
257
308
  `issued.email` is the address the user's record holds, as they registered
@@ -307,10 +358,12 @@ retries a `MAIL_FAILED`, rather than to a promise nobody awaits.
307
358
  ### Retrying and tracing the mailer
308
359
 
309
360
  Wrap the mailer before you hand it to `janusMail()`: `withRetry`, from
310
- `@nxgt/mail` 0.8, retries a `MailFailure` with backoff, reusing one
311
- idempotency key for every attempt, and never retries a `MailRefused`;
312
- `withMailTelemetry`, from `@nxgt/mail/telemetry` 0.9 (`withTelemetry` on
313
- 0.8), opens a `mail.send` span per send. That entry
361
+ `@nxgt/mail` 0.8 or later, retries a `MailFailure` with backoff, reusing one
362
+ idempotency key for every attempt, and never retries a `MailRefused` — its
363
+ options are `MailRetryOptions` since 1.0, which removed the older name
364
+ `RetryOptions`; `withMailTelemetry`, from `@nxgt/mail/telemetry` 0.9 or
365
+ later (`withTelemetry` on 0.8, removed in 1.0), opens a `mail.send` span
366
+ per send. That entry
314
367
  needs `@opentelemetry/api` installed (an optional peer, loaded by it alone);
315
368
  with no OpenTelemetry SDK registered, its spans are no-ops. Put telemetry on the outside, so one
316
369
  send is one span:
@@ -334,9 +387,10 @@ export const mail = janusMail({
334
387
  which scopes a telemetry to a block. `mail.send` is recorded through
335
388
  OpenTelemetry, not `@nxgt/telemetry`, so it does not nest under
336
389
  `@nxgt/janus-telemetry`'s spans. `@nxgt/mail` 0.9 renamed its decorator
337
- `withMailTelemetry` so the two no longer share a name; on 0.8 it is
338
- `withTelemetry`, still exported by 0.9 as a deprecated alias until 1.0 —
339
- import it under another name there when a module uses both.
390
+ `withMailTelemetry` so the two no longer share a name, and 1.0 removed the
391
+ old `withTelemetry` — with `withRendererTelemetry`, which
392
+ `@nxgt/mail/telemetry` 1.0 names `withMailRendererTelemetry`. On 0.8, import
393
+ its `withTelemetry` under another name when a module uses both.
340
394
 
341
395
  **SMTP ignores the idempotency key**, so a retry after an ambiguous SMTP
342
396
  timeout can deliver an e-mail twice. If a duplicate sign-in code or reset
@@ -1,7 +1,7 @@
1
1
  # Templates
2
2
 
3
3
  This page is for changing what an e-mail says or looks like: what a template
4
- is given and answers, replacing some of the eight, using the defaults on their
4
+ is given and answers, replacing some of the nine, using the defaults on their
5
5
  own, and what the defaults say.
6
6
 
7
7
  ```ts
@@ -22,7 +22,7 @@ const mail = janusMail({
22
22
  });
23
23
  ```
24
24
 
25
- The sign-in code e-mail is now yours; the seven others stay the defaults.
25
+ The sign-in code e-mail is now yours; the eight others stay the defaults.
26
26
 
27
27
  ## What a template is
28
28
 
@@ -66,15 +66,34 @@ e-mail.
66
66
  | `emailChanged` | `brand`, `name`, `link`, `newEmail` | `mail.emailChanged(to)` |
67
67
  | `twoFactorEnabled` | `brand`, `name`, `link` | `mail.twoFactorEnabled(to)` |
68
68
  | `twoFactorDisabled` | `brand`, `name`, `link` | `mail.twoFactorDisabled(to)` |
69
+ | `recoveryCodeUsed` | `brand`, `name`, `when`, `recoveryCodesLeft`, `link` | `mail.recoveryCodeUsed(to, { when, recoveryCodesLeft })` |
69
70
  | `welcome` | `brand`, `name`, `link` | `mail.welcome(to)` |
70
71
 
71
72
  Every one also gets `locale`, one of `locales`. Every value is a string:
72
73
  `brand` from the options, `name` from the recipient, `link` from `links` —
73
- `links.secureAccount()` for the four notices, `links.getStarted()` for
74
- `welcome` — `code` and `newEmail` from the
74
+ `links.secureAccount()` for the four notices, `links.recoveryCodes()` (else
75
+ `links.secureAccount()`) for `recoveryCodeUsed`, `links.getStarted()` for
76
+ `welcome` — `code`, `newEmail` and `when` from the
75
77
  call, and `expiresIn` derived from the flow's `expiresAt` in the locale, or
76
78
  the send's own `expiresIn` ([Sending](sending.md#the-expiry)).
77
79
 
80
+ `recoveryCodesLeft` is a **sentence**, not the count: "You have 9 recovery
81
+ codes left." The method writes it from the count with the preset's plural in
82
+ the recipient's locale, or passes the send's own text — so an override for
83
+ a locale of its own is given what the caller wrote:
84
+
85
+ ```ts
86
+ // `escape` is your HTML escaper: a template of yours escapes what it writes.
87
+ templates: {
88
+ recoveryCodeUsed: ({ name, when, recoveryCodesLeft, link }) => ({
89
+ subject: 'Ein Wiederherstellungscode wurde verwendet',
90
+ html: `<p>Hallo ${escape(name)}, am ${escape(when)}.</p><p>${escape(recoveryCodesLeft)}</p><p><a href="${escape(link)}">Konto sichern</a></p>`,
91
+ text: `Hallo ${name}, am ${when}.\n\n${recoveryCodesLeft}\n\n${link}`,
92
+ }),
93
+ },
94
+ // await mail.recoveryCodeUsed(to, { when, recoveryCodesLeft: `Sie haben noch ${left} Wiederherstellungscodes.` });
95
+ ```
96
+
78
97
  `signInCode` is **never given the challenge**: it is not in its variables,
79
98
  and the method never reads it. An override states the expiry from
80
99
  `expiresIn`, already formatted:
@@ -105,11 +124,11 @@ methods live on its prototype and are refused — see
105
124
  [troubleshooting](../troubleshooting.md#janusmail-templatesname-is-not-an-own-enumerable-property--pass-a-plain-object-as--name-variables--rendered-).
106
125
 
107
126
  `templates` is `Partial` while `locales` stays within `en` and `fr`: give
108
- any of the eight, and the defaults render the rest. Once `locales` holds
109
- another locale, it takes **all eight** — see
127
+ any of the nine, and the defaults render the rest. Once `locales` holds
128
+ another locale, it takes **all nine** — see
110
129
  [Adding a locale](locales.md#adding-a-locale).
111
130
 
112
- `mail.templates` answers the eight in use — yours, and the defaults for the
131
+ `mail.templates` answers the nine in use — yours, and the defaults for the
113
132
  rest — frozen:
114
133
 
115
134
  ```ts
@@ -131,7 +150,7 @@ const { subject, html, text } = await defaults.resetPassword({
131
150
  });
132
151
  ```
133
152
 
134
- It answers the eight defaults, typed for `'en' | 'fr'`, without a mailer:
153
+ It answers the nine defaults, typed for `'en' | 'fr'`, without a mailer:
135
154
  to preview an e-mail, to wrap one — add a line to the default text, say — or
136
155
  to send one through something other than `janusMail()`. A default reused for
137
156
  a wider set of locales is a compile error: it could not render the one it
@@ -157,7 +176,7 @@ them renders: that is when the package's `mails/` folder is read, once.
157
176
  ## What the defaults say
158
177
 
159
178
  The defaults are [`@nxgt/mail-presets`](https://www.npmjs.com/package/@nxgt/mail-presets)
160
- 0.4.3, built with a neutral grey theme and no logo:
179
+ 1.0.1, built with a neutral grey theme and no logo:
161
180
 
162
181
  | Template | Built from | Subject (`en`) | Subject (`fr`) |
163
182
  | --- | --- | --- | --- |
@@ -168,12 +187,16 @@ The defaults are [`@nxgt/mail-presets`](https://www.npmjs.com/package/@nxgt/mail
168
187
  | `emailChanged` | `email-changed` | Your e-mail address was changed | Votre adresse e-mail a été modifiée |
169
188
  | `twoFactorEnabled` | `two-factor-enabled` | Two-factor authentication was turned on | L'authentification à deux facteurs a été activée |
170
189
  | `twoFactorDisabled` | `two-factor-disabled` | Two-factor authentication was turned off | L'authentification à deux facteurs a été désactivée |
190
+ | `recoveryCodeUsed` | `recovery-code-used` | A recovery code was used on your account | Un code de récupération a été utilisé sur votre compte |
171
191
  | `welcome` | `welcome` | Welcome, `Ada` | Bienvenue, `Ada` |
172
192
 
173
193
  Each has a header and a footer with the brand, a heading, a greeting by name
174
194
  (but `signInCode`), a button with its link and the link again in text for a
175
- client that shows no button, and a text part. The four notices add a warning:
176
- *if this was not you, secure your account now*. `welcome` has the brand in
195
+ client that shows no button, and a text part. The other four notices add a warning:
196
+ *if this was not you, secure your account now*. `recoveryCodeUsed` says it
197
+ too, after a warning banner — *a recovery code was used on your account at
198
+ `when`* — the codes left, and a line on generating new ones; its button says
199
+ **Secure my account**. `welcome` has the brand in
177
200
  its heading — "Welcome to Acme" — and the recipient's name in its subject,
178
201
  filled at send time like the body; its button says **Get started**.
179
202
 
@@ -197,11 +220,13 @@ mode, 9.85:1. The muted text — the footer, and the closing *if you did not
197
220
  ask for this* — is a slate grey (`#5f718a`) in light mode, 4.53:1 on the
198
221
  page, and turns `#cbd5e1` in dark mode, 12.01:1 on the dark card and
199
222
  13.31:1 on the page. The notices' alerts keep their light ground and grey
200
- text in both modes, 4.56:1 at the least. Every
201
- text of the defaults reads at 4.5:1 or more in both modes, but the link
202
- under the button, which `@nxgt/mail-ui` colours with its info blue in
203
- both: 6.78:1 on the dark card, 2.63:1 on the light one, and no theme token
204
- moves it without breaking the other mode. Nothing is passed for any of it,
223
+ text in both modes, 4.56:1 at the least; the recovery code banner keeps
224
+ its light ground and near-black text, 17.57:1. The link under the button is
225
+ a blue of each mode's own: `#1d4ed8` on the light card, 6.70:1, and
226
+ `#93c5fd` on the dark one, 9.89:1 — `@nxgt/mail-ui` 1.0 gives the info
227
+ blue a dark value (`color-info-dark`), where one blue served both modes and
228
+ read 2.63:1 on the light card. Every text of the defaults reads at 4.5:1 or
229
+ more in both modes. Nothing is passed for any of it,
205
230
  the text part has no colours, and a template of your own gets none of it. `@nxgt/mail-ui`'s
206
231
  [Dark mode](https://github.com/softistx/nxgt-mail/blob/develop/packages/mail-ui/docs/guide/dark-mode.md)
207
232
  guide has the technique, client by client.
package/docs/roadmap.md CHANGED
@@ -35,7 +35,7 @@ Nothing between releases.
35
35
  `@nxgt/mail` answers every failure, whichever package sent the e-mail.
36
36
  - **Retries or a queue** — a retry is the application's decision, made where
37
37
  it can see it; hand a send to a queue that retries a `MAIL_FAILED`, or pass
38
- a mailer wrapped in `@nxgt/mail` 0.8's `withRetry`.
38
+ a mailer wrapped in `@nxgt/mail`'s `withRetry` (0.8 or later).
39
39
  - **Sending from inside `janus()`** — `@nxgt/janus` stays free of e-mail:
40
40
  its flows answer what to send, and this package is called with that answer.
41
41
  An application that sends its own way needs neither this package nor a
@@ -47,6 +47,20 @@ Nothing between releases.
47
47
 
48
48
  Newest first; from the first release on, the package's CHANGELOG holds every one.
49
49
 
50
+ - **The recovery code notice, and a readable link in both modes, v0.5.0.**
51
+ `recoveryCodeUsed(to, { when, recoveryCodesLeft })`, built from
52
+ `@nxgt/mail-presets` 1.0.0's `recovery-code-used` in English and French:
53
+ send it on `@nxgt/janus`'s `user.recoveryCodeUsed` event, with the count
54
+ `auth.secondFactor.recoveryCodesLeft(user)` reads (`@nxgt/janus` 0.11),
55
+ written as the preset's plural in the recipient's locale — "You have 1
56
+ recovery code left." Its button links to the new, optional
57
+ `links.recoveryCodes()`, else `links.secureAccount()`. Nine e-mails now,
58
+ and nine templates to pass for a locale beyond `en` and `fr`. The link
59
+ under the button is a blue of each mode's own — 6.70:1 on the light card,
60
+ where it read 2.63:1, and 9.89:1 on the dark one — from `@nxgt/mail-ui`
61
+ 1.0.0's `color-info-dark`. The `@nxgt/mail` peer widens to `>=0.1.0 <2`,
62
+ so `@nxgt/mail` 1.0 and its transports are admitted; the manifest is still
63
+ format 1.
50
64
  - **Muted text at 4.5:1 or more in both modes, v0.4.1.** Under dark mode the
51
65
  sign-in code's box is a slate a step above the dark card with light code on
52
66
  it, 9.85:1, and the muted text turns light, 13.31:1 on the page and
@@ -27,12 +27,13 @@ How the messages are shaped:
27
27
  - [`janusMail: brand must be the name the e-mails show, as 'Acme'`](#janusmail-brand-must-be-the-name-the-e-mails-show-as-acme)
28
28
  - [`janusMail: links must be an object, as { verifyEmail, resetPassword, secureAccount, getStarted }`](#janusmail-links-must-be-an-object-as--verifyemail-resetpassword-secureaccount-getstarted-)
29
29
  - [`janusMail: links.<name> must be a function`](#janusmail-linksname-must-be-a-function)
30
+ - [`janusMail: links.recoveryCodes must be a function, or left out`](#janusmail-linksrecoverycodes-must-be-a-function-or-left-out)
30
31
  - [`janusMail: locales must list at least one locale, as ['en', 'fr']`](#janusmail-locales-must-list-at-least-one-locale-as-en-fr)
31
32
  - [`janusMail: locales must be BCP 47 language tags, as 'fr-CA'`](#janusmail-locales-must-be-bcp-47-language-tags-as-fr-ca)
32
33
  - [`janusMail: locales holds the same locale twice`](#janusmail-locales-holds-the-same-locale-twice)
33
34
  - [`janusMail: fallbackLocale must be one of locales`](#janusmail-fallbacklocale-must-be-one-of-locales)
34
35
  - [`janusMail: templates must be an object of functions, as { verifyEmail: (variables) => rendered }`](#janusmail-templates-must-be-an-object-of-functions-as--verifyemail-variables--rendered-)
35
- - [`janusMail: templates has no template <name> — name one of verifyEmail, resetPassword, signInCode, passwordChanged, emailChanged, twoFactorEnabled, twoFactorDisabled, welcome`](#janusmail-templates-has-no-template-name--name-one-of-verifyemail-resetpassword-signincode-passwordchanged-emailchanged-twofactorenabled-twofactordisabled-welcome)
36
+ - [`janusMail: templates has no template <name> — name one of verifyEmail, resetPassword, signInCode, passwordChanged, emailChanged, twoFactorEnabled, twoFactorDisabled, recoveryCodeUsed, welcome`](#janusmail-templates-has-no-template-name--name-one-of-verifyemail-resetpassword-signincode-passwordchanged-emailchanged-twofactorenabled-twofactordisabled-recoverycodeused-welcome)
36
37
  - [`janusMail: templates.<name> must be a function`](#janusmail-templatesname-must-be-a-function)
37
38
  - [`janusMail: templates.<name> is not an own enumerable property — pass a plain object, as { <name>: (variables) => rendered }`](#janusmail-templatesname-is-not-an-own-enumerable-property--pass-a-plain-object-as--name-variables--rendered-)
38
39
  - [`janusMail: clock must be a Clock — an object with a now function`](#janusmail-clock-must-be-a-clock--an-object-with-a-now-function)
@@ -41,6 +42,8 @@ How the messages are shaped:
41
42
  **Sending**
42
43
  - [`janusMail.<method>: <field> must be a string`](#janusmailmethod-field-must-be-a-string)
43
44
  - [`janusMail.<method>: expiresAt must be a Date`](#janusmailmethod-expiresat-must-be-a-date)
45
+ - [`janusMail.recoveryCodeUsed: recoveryCodesLeft must be a count — a whole number, 0 or more — or the sentence to show`](#janusmailrecoverycodeused-recoverycodesleft-must-be-a-count--a-whole-number-0-or-more--or-the-sentence-to-show)
46
+ - [`janusMail.recoveryCodeUsed: recoveryCodesLeft must be the sentence to show in a locale the default e-mails are not built in`](#janusmailrecoverycodeused-recoverycodesleft-must-be-the-sentence-to-show-in-a-locale-the-default-e-mails-are-not-built-in)
44
47
  - [`janusMail.<method>: clock.now() must answer a Date`](#janusmailmethod-clocknow-must-answer-a-date)
45
48
  - [`janusMail.<method>: expiresAt is past — the link or code would not work`](#janusmailmethod-expiresat-is-past--the-link-or-code-would-not-work)
46
49
  - [`This link expires in 1 hour.` — in the wrong language, or shorter than the `tokens` TTL](#this-link-expires-in-1-hour--in-the-wrong-language-or-shorter-than-the-tokens-ttl)
@@ -55,14 +58,17 @@ How the messages are shaped:
55
58
  - [The button all but vanishes in dark mode](#the-button-all-but-vanishes-in-dark-mode)
56
59
  - [The sign-in code's box stays a light slab in dark mode](#the-sign-in-codes-box-stays-a-light-slab-in-dark-mode)
57
60
  - [The sign-in code turns unreadable in dark mode after upgrading `@nxgt/mail-ui` to 0.7](#the-sign-in-code-turns-unreadable-in-dark-mode-after-upgrading-nxgtmail-ui-to-07)
58
- - [The link under the button is faint in light mode](#the-link-under-the-button-is-faint-in-light-mode)
61
+ - [The link under the button is faint in light mode, or in dark mode](#the-link-under-the-button-is-faint-in-light-mode-or-in-dark-mode)
59
62
 
60
63
  **Compile errors**
61
64
  - [`TS2345: Argument of type '(IssuedToken & { user: … }) | null' is not assignable to parameter of type 'IssuedToken'.`](#ts2345-argument-of-type-issuedtoken---user----null-is-not-assignable-to-parameter-of-type-issuedtoken)
62
65
  - [`TS2345: … Types of property 'expiresAt' are incompatible. Type 'string' is not assignable to type 'Date'.`](#ts2345--types-of-property-expiresat-are-incompatible-type-string-is-not-assignable-to-type-date)
63
66
  - [`TS2739: Type '{ … }' is missing the following properties from type 'JanusMailTemplates<…>'`](#ts2739-type----is-missing-the-following-properties-from-type-janusmailtemplates)
67
+ - [`TS2741: Property 'recoveryCodeUsed' is missing in type '{ … }' but required in type 'JanusMailTemplates<…>'.`](#ts2741-property-recoverycodeused-is-missing-in-type----but-required-in-type-janusmailtemplates)
64
68
  - [`TS2322: Type '"de"' is not assignable to type '"en" | "fr"'.`](#ts2322-type-de-is-not-assignable-to-type-en--fr)
65
69
  - [`TS2741: Property 'getStarted' is missing in type '{ … }' but required in type 'JanusMailLinks'.`](#ts2741-property-getstarted-is-missing-in-type----but-required-in-type-janusmaillinks)
70
+ - [`TS2322: Type 'number | null' is not assignable to type 'string | number'.`](#ts2322-type-number--null-is-not-assignable-to-type-string--number)
71
+ - [`TS2322: Type 'Date' is not assignable to type 'string'.`](#ts2322-type-date-is-not-assignable-to-type-string)
66
72
 
67
73
  ---
68
74
 
@@ -107,8 +113,8 @@ janusMail({ mailer, from, brand: 'Acme', links });
107
113
 
108
114
  ### `janusMail: links must be an object, as { verifyEmail, resetPassword, secureAccount, getStarted }`
109
115
 
110
- No `links`. All four are required: the notices link to `secureAccount`, and
111
- the welcome to `getStarted`.
116
+ No `links`. Four are required: the notices link to `secureAccount`, and
117
+ the welcome to `getStarted`. A fifth, `recoveryCodes`, is optional.
112
118
 
113
119
  ### `janusMail: links.<name> must be a function`
114
120
 
@@ -131,6 +137,17 @@ link the welcome e-mail added in 0.4.0: `links` written for 0.3 has no
131
137
  sign-in page — even if you never send the welcome. In TypeScript the same
132
138
  `links` is a compile error on `links`, `Property 'getStarted' is missing`.
133
139
 
140
+ ### `janusMail: links.recoveryCodes must be a function, or left out`
141
+
142
+ `links.recoveryCodes` given as a URL, or as anything but a function. It is
143
+ optional — the recovery code notice links to `secureAccount` without it —
144
+ but when given it takes nothing and answers the page where the user
145
+ regenerates their codes:
146
+
147
+ ```ts
148
+ links: { …, recoveryCodes: () => 'https://acme.example/account/recovery-codes' },
149
+ ```
150
+
134
151
  ### `janusMail: locales must list at least one locale, as ['en', 'fr']`
135
152
 
136
153
  `locales: []`, `locales: 'en'`, or a list holding something that is not a
@@ -163,9 +180,9 @@ A `fallbackLocale` the list does not hold — `'de'` with the default
163
180
 
164
181
  `templates` given as a list or a function. Key each template by its name.
165
182
 
166
- ### `janusMail: templates has no template <name> — name one of verifyEmail, resetPassword, signInCode, passwordChanged, emailChanged, twoFactorEnabled, twoFactorDisabled, welcome`
183
+ ### `janusMail: templates has no template <name> — name one of verifyEmail, resetPassword, signInCode, passwordChanged, emailChanged, twoFactorEnabled, twoFactorDisabled, recoveryCodeUsed, welcome`
167
184
 
168
- A key that is not one of the eight — `magicLink`, or the e-mail's file name
185
+ A key that is not one of the nine — `magicLink`, or the e-mail's file name
169
186
  `'verify-email'` rather than the template's, `verifyEmail`. Other e-mails are
170
187
  on the [roadmap](roadmap.md); send them with `@nxgt/mail` directly meanwhile.
171
188
 
@@ -199,7 +216,7 @@ as it is: fields are own properties.
199
216
  ### `janusMail: the default templates are built in en and fr only — with another locale in locales, pass every template in templates; <names> missing`
200
217
 
201
218
  `locales` holds a locale beyond `en` and `fr`, and `templates` leaves some
202
- out: the defaults could not render them in that locale. Pass all eight — see
219
+ out: the defaults could not render them in that locale. Pass all nine — see
203
220
  [Adding a locale](guide/locales.md#adding-a-locale) — or drop the locale.
204
221
  The message lists the templates missing.
205
222
 
@@ -238,14 +255,19 @@ number of seconds: pass the text to show, `{ expiresIn: '1 hour' }`, or
238
255
  leave it out.
239
256
 
240
257
  `janusMail.twoFactorEnabled: email must be a string` (or `twoFactorDisabled`,
241
- or `welcome`) is the user event itself given to the e-mail: an event names the user by id
242
- alone. Read the user first:
258
+ `recoveryCodeUsed` or `welcome`) is the user event itself given to the
259
+ e-mail: an event names the user by id alone. Read the user first:
243
260
 
244
261
  ```ts
245
262
  const user = await auth.get(event.userId);
246
263
  await mail.twoFactorDisabled({ name: user.name, locale: user.locale, email: user.email });
247
264
  ```
248
265
 
266
+ `janusMail.recoveryCodeUsed: when must be a string` is the second argument
267
+ missing, or its `when` given as the event's `Date`: pass the text to show,
268
+ formatted in the recipient's locale and time zone —
269
+ `new Intl.DateTimeFormat(user.locale, { dateStyle: 'long', timeStyle: 'short', timeZone }).format(event.occurredAt)`.
270
+
249
271
  ### `janusMail.<method>: expiresAt must be a Date`
250
272
 
251
273
  A `TypeError` from `verifyEmail`, `resetPassword` or `signInCode`: the e-mail
@@ -261,6 +283,32 @@ await mail.verifyEmail(issued, { name: job.name });
261
283
  await mail.verifyEmail(job.issued, { name: job.name }, { expiresIn: '1 day' });
262
284
  ```
263
285
 
286
+ ### `janusMail.recoveryCodeUsed: recoveryCodesLeft must be a count — a whole number, 0 or more — or the sentence to show`
287
+
288
+ A `TypeError`, before anything is rendered or sent. `recoveryCodesLeft` was
289
+ `null`, negative, a fraction, `NaN`, or missing. The usual cause is
290
+ `auth.secondFactor.recoveryCodesLeft(user)`'s answer passed unchecked: it is
291
+ `null` for a user with no active factor — turned off since the code was
292
+ spent — who has no codes to count. Check it, and send nothing then:
293
+
294
+ ```ts
295
+ const recoveryCodesLeft = await auth.secondFactor.recoveryCodesLeft(user);
296
+ if (recoveryCodesLeft !== null) {
297
+ await mail.recoveryCodeUsed(to, { when, recoveryCodesLeft });
298
+ }
299
+ ```
300
+
301
+ ### `janusMail.recoveryCodeUsed: recoveryCodesLeft must be the sentence to show in a locale the default e-mails are not built in`
302
+
303
+ A `TypeError`, before anything is sent: the recipient's locale is one of
304
+ yours beyond `en` and `fr`, and `recoveryCodesLeft` is a count. The plural
305
+ of the codes left is in the build's catalogue, in `en` and `fr` only, so the
306
+ count cannot be written for that locale. Pass the sentence as text:
307
+
308
+ ```ts
309
+ await mail.recoveryCodeUsed(to, { when, recoveryCodesLeft: `Sie haben noch ${left} Wiederherstellungscodes.` });
310
+ ```
311
+
264
312
  ### `janusMail.<method>: clock.now() must answer a Date`
265
313
 
266
314
  A `TypeError` from `verifyEmail`, `resetPassword` or `signInCode`: the
@@ -320,6 +368,7 @@ A `TypeError` from a call, before anything is rendered or sent
320
368
  (`mailer.attempts` stays 0) — `janusMail.verifyEmail: links.verifyEmail(token)
321
369
  must answer a string`, `janusMail.emailChanged: links.secureAccount() must
322
370
  answer a string`, `janusMail.welcome: links.getStarted() must answer a
371
+ string`, `janusMail.recoveryCodeUsed: links.recoveryCodes() must answer a
323
372
  string`. The `links` function answered something other than a
324
373
  string: a `URL` object, `undefined`, or a promise — an `async` function.
325
374
  Answer `url.href`, and compute the link synchronously:
@@ -379,7 +428,7 @@ try {
379
428
  ```
380
429
 
381
430
  This package retries nothing: a retry is your decision. To retry in
382
- process, pass a mailer wrapped in `@nxgt/mail` 0.8's `withRetry`; once its
431
+ process, pass a mailer wrapped in `@nxgt/mail`'s `withRetry` (0.8 or later); once its
383
432
  attempts run out it rejects with the last `MailFailure`, `attempts` on it —
384
433
  see [Retrying and tracing the mailer](guide/sending.md#retrying-and-tracing-the-mailer).
385
434
 
@@ -396,7 +445,7 @@ avoids the read altogether. (In this repository: run `bun run build`.)
396
445
 
397
446
  The default templates read `mails/` with `node:fs`, through
398
447
  `@nxgt/mail/renderer`: Node, Bun or Deno only. On an edge runtime, pass all
399
- eight templates yourself; an inlined build is on the [roadmap](roadmap.md).
448
+ nine templates yourself; an inlined build is on the [roadmap](roadmap.md).
400
449
 
401
450
  ### An e-mail stays light in dark mode
402
451
 
@@ -466,17 +515,33 @@ the code at 1.86:1 once rebuilt with `@nxgt/mail-ui` 0.7.0. Through 0.6.0,
466
515
  no one value suits a mid-slate box: set a dark `color-muted-dark` and a
467
516
  light `color-muted-foreground-dark`, as in the entry above.
468
517
 
469
- ### The link under the button is faint in light mode
518
+ ### The link under the button is faint in light mode, or in dark mode
470
519
 
471
520
  The link repeated in text under the button, for a client that shows no
472
- button, is `@nxgt/mail-ui`'s `NxLink`, coloured with its info blue
473
- (`#54a2ff`) in both modes: 2.63:1 on the light card, 6.78:1 on the dark
474
- one. It is the one text of the defaults under 4.5:1. `color-info` has no
475
- dark twin, and no single value reaches 4.5:1 on both the white card and the
476
- dark card, so the defaults leave it as it is rather than trade the dark
477
- mode for the light one; it is `@nxgt/mail-ui`'s to fix. A template of your
478
- own can colour its link with the primary instead, which has a dark twin:
479
- `NxLink` merges a `class` over its own `text-info`, and
521
+ button, is `@nxgt/mail-ui`'s `NxLink`, coloured with its info blue. Through
522
+ 0.4, the defaults kept `@nxgt/mail-ui`'s `#54a2ff` in both modes: 2.63:1 on
523
+ the light card, 6.78:1 on the dark one. Since 0.5.0 they are built with
524
+ `@nxgt/mail-ui` 1.0, whose info blue has a dark twin, and set one blue per
525
+ mode: `#1d4ed8` on the light card, 6.70:1, and `#93c5fd` on the dark card,
526
+ 9.89:1. Upgrade to read it.
527
+
528
+ A template of your own built with `@nxgt/mail-ui` 1.0.0 or later sets the
529
+ same pair in its `theme` — a light `color-info` dark enough for the white
530
+ card, and a `color-info-dark` light enough for the dark one; no single
531
+ value reaches 4.5:1 on both:
532
+
533
+ ```ts
534
+ ui({
535
+ brand: { name: 'Acme' },
536
+ theme: {
537
+ 'color-info': '#1d4ed8', // the link on the light card, 6.70:1
538
+ 'color-info-dark': '#93c5fd', // the link on the dark card, 9.89:1
539
+ },
540
+ });
541
+ ```
542
+
543
+ Before 1.0, colour the link with the primary instead, which has a dark
544
+ twin: `NxLink` merges a `class` over its own `text-info`, and
480
545
  `nx-dark-text-primary` follows `color-primary-dark` in dark mode:
481
546
 
482
547
  ```vue
@@ -511,7 +576,18 @@ whole.
511
576
  ### `TS2739: Type '{ … }' is missing the following properties from type 'JanusMailTemplates<…>'`
512
577
 
513
578
  `locales` holds a locale beyond `en` and `fr`, and `templates` does not give
514
- all eight. See [Adding a locale](guide/locales.md#adding-a-locale).
579
+ all nine. With five or more missing, the code is `TS2740` and the list ends
580
+ `…, and 4 more.` See [Adding a locale](guide/locales.md#adding-a-locale).
581
+
582
+ ### `TS2741: Property 'recoveryCodeUsed' is missing in type '{ … }' but required in type 'JanusMailTemplates<…>'.`
583
+
584
+ `templates` written for 0.4, with a locale beyond `en` and `fr`: since
585
+ 0.5.0 there are nine templates, and with such a locale every one is yours.
586
+ Add `recoveryCodeUsed` — it is given `brand`, `name`, `when`,
587
+ `recoveryCodesLeft` (a sentence) and `link` — even if you never send it; in
588
+ JavaScript the same `templates` is
589
+ [`janusMail: the default templates are built in en and fr only — …; recoveryCodeUsed missing`](#janusmail-the-default-templates-are-built-in-en-and-fr-only--with-another-locale-in-locales-pass-every-template-in-templates-names-missing).
590
+ See [Templates](guide/templates.md#what-each-template-is-given).
515
591
 
516
592
  ### `TS2322: Type '"de"' is not assignable to type '"en" | "fr"'.`
517
593
 
@@ -533,3 +609,20 @@ links: {
533
609
  getStarted: () => 'https://acme.example/',
534
610
  },
535
611
  ```
612
+
613
+ ### `TS2322: Type 'number | null' is not assignable to type 'string | number'.`
614
+
615
+ On `recoveryCodesLeft`: `auth.secondFactor.recoveryCodesLeft(user)`'s
616
+ answer, passed without checking it for `null`. A user with no active factor
617
+ has no codes to count; check it first, as in
618
+ [`recoveryCodesLeft must be a count`](#janusmailrecoverycodeused-recoverycodesleft-must-be-a-count--a-whole-number-0-or-more--or-the-sentence-to-show).
619
+
620
+ ### `TS2322: Type 'Date' is not assignable to type 'string'.`
621
+
622
+ On `when`: the event's `occurredAt` passed as it is. `when` is the text the
623
+ e-mail shows — format it in the recipient's locale and time zone first:
624
+
625
+ ```ts
626
+ const when = new Intl.DateTimeFormat(user.locale, { dateStyle: 'long', timeStyle: 'short', timeZone: 'Europe/Paris' })
627
+ .format(event.occurredAt);
628
+ ```
@@ -10,11 +10,11 @@
10
10
  -8.51797)!important;}[data-ogsb] .nx-dark-bg-background{background-color:#020918!important;background-color:lab(2.35721% .367686 -8.51797)!important;}[data-ogsc] .nx-dark-text-foreground{color:#f8fafc!important;color:lab(98.1434% -.369519 -1.05966)!important;}[data-ogsb] .nx-dark-text-foreground{color:#f8fafc!important;color:lab(98.1434% -.369519 -1.05966)!important;}[data-ogsc] .nx-dark-text-foreground-25{color:#404551!important;}[data-ogsb] .nx-dark-text-foreground-25{color:#404551!important;}
11
11
  [data-ogsc] .nx-dark-text-card-foreground{color:#f8fafc!important;color:lab(98.1434% -.369519 -1.05966)!important;}[data-ogsb] .nx-dark-text-card-foreground{color:#f8fafc!important;color:lab(98.1434% -.369519 -1.05966)!important;}[data-ogsc] .nx-dark-border-border{border-color:#1c212f!important;}[data-ogsb] .nx-dark-border-border{border-color:#1c212f!important;}[data-ogsc] .nx-dark-bg-muted{background-color:#1e293b!important;}[data-ogsb] .nx-dark-bg-muted{background-color:#1e293b!important;}
12
12
  [data-ogsc] .nx-dark-text-muted-foreground{color:#cbd5e1!important;}[data-ogsb] .nx-dark-text-muted-foreground{color:#cbd5e1!important;}[data-ogsc] .nx-dark-border-muted-foreground{border-color:#cbd5e1!important;}[data-ogsb] .nx-dark-border-muted-foreground{border-color:#cbd5e1!important;}[data-ogsc] .nx-dark-border-muted{border-color:#1e293b!important;}[data-ogsb] .nx-dark-border-muted{border-color:#1e293b!important;}[data-ogsc] .nx-dark-bg-primary{background-color:#fafafa!important;}
13
- [data-ogsb] .nx-dark-bg-primary{background-color:#fafafa!important;}[data-ogsc] .nx-dark-text-primary{color:#fafafa!important;}[data-ogsb] .nx-dark-text-primary{color:#fafafa!important;}[data-ogsc] .nx-dark-text-primary-foreground{color:#18181b!important;}[data-ogsb] .nx-dark-text-primary-foreground{color:#18181b!important;}[data-ogsc] .nx-dark-border-primary{border-color:#fafafa!important;}[data-ogsb] .nx-dark-border-primary{border-color:#fafafa!important;}[data-ogsc] .nx-dark-bg-primary-15{
14
- background-color:#272d3a!important;}[data-ogsb] .nx-dark-bg-primary-15{background-color:#272d3a!important;}[data-ogsc] .nx-dark-bg-primary-20{background-color:#343945!important;}[data-ogsb] .nx-dark-bg-primary-20{background-color:#343945!important;}[data-ogsc] .nx-dark-border-primary-40{border-color:#656972!important;}[data-ogsb] .nx-dark-border-primary-40{border-color:#656972!important;}[data-ogsc] .nx-dark-border-primary-50{border-color:#7e8189!important;}[data-ogsb] .nx-dark-border-primary-50
15
- {border-color:#7e8189!important;}[data-ogsc] .nx-light-only{display:none!important;}[data-ogsb] .nx-light-only{display:none!important;}[data-ogsc] .nx-dark-only{display:inline-block!important;}[data-ogsb] .nx-dark-only{display:inline-block!important;}
13
+ [data-ogsb] .nx-dark-bg-primary{background-color:#fafafa!important;}[data-ogsc] .nx-dark-text-primary{color:#fafafa!important;}[data-ogsb] .nx-dark-text-primary{color:#fafafa!important;}[data-ogsc] .nx-dark-text-primary-foreground{color:#18181b!important;}[data-ogsb] .nx-dark-text-primary-foreground{color:#18181b!important;}[data-ogsc] .nx-dark-text-info{color:#93c5fd!important;}[data-ogsb] .nx-dark-text-info{color:#93c5fd!important;}[data-ogsc] .nx-dark-border-primary{border-color:#fafafa
14
+ !important;}[data-ogsb] .nx-dark-border-primary{border-color:#fafafa!important;}[data-ogsc] .nx-dark-bg-primary-15{background-color:#272d3a!important;}[data-ogsb] .nx-dark-bg-primary-15{background-color:#272d3a!important;}[data-ogsc] .nx-dark-bg-primary-20{background-color:#343945!important;}[data-ogsb] .nx-dark-bg-primary-20{background-color:#343945!important;}[data-ogsc] .nx-dark-border-primary-40{border-color:#656972!important;}[data-ogsb] .nx-dark-border-primary-40{border-color:#656972
15
+ !important;}[data-ogsc] .nx-dark-border-primary-50{border-color:#7e8189!important;}[data-ogsb] .nx-dark-border-primary-50{border-color:#7e8189!important;}[data-ogsc] .nx-light-only{display:none!important;}[data-ogsb] .nx-light-only{display:none!important;}[data-ogsc] .nx-dark-only{display:inline-block!important;}[data-ogsb] .nx-dark-only{display:inline-block!important;}
16
16
  @media (prefers-color-scheme:dark){html{--lightningcss-light:;--lightningcss-dark:initial;}.nx-dark-bg-paper{background-color:#040a19!important;}.nx-dark-bg-card{background-color:#0f172b!important;background-color:lab(7.78673% 1.82345 -15.0537)!important;}.nx-dark-text-foreground{color:#f8fafc!important;color:lab(98.1434% -.369519 -1.05966)!important;}.nx-dark-text-card-foreground{color:#f8fafc!important;color:lab(98.1434% -.369519 -1.05966)!important;}.nx-dark-border-border{border-color:#1c212f
17
- !important;}.nx-dark-text-muted-foreground{color:#cbd5e1!important;}.nx-dark-bg-primary{background-color:#fafafa!important;}.nx-dark-text-primary-foreground{color:#18181b!important;} }
17
+ !important;}.nx-dark-text-muted-foreground{color:#cbd5e1!important;}.nx-dark-bg-primary{background-color:#fafafa!important;}.nx-dark-text-primary-foreground{color:#18181b!important;}.nx-dark-text-info{color:#93c5fd!important;} }
18
18
  </style>
19
19
  </head>
20
20
  <body xml:lang="en" dir="ltr" style="margin:0;padding:0;width:100%;height:100%;word-break:break-word;background-color:#f4f4f4;" class="nx-dark-bg-paper"><div style="display:none;">Your account now signs in with a new address.&#8199;&#65279;&#847; &#8199;&#65279;&#847; &#8199;&#65279;&#847; &#8199;&#65279;&#847; &#8199;&#65279;&#847; &#8199;&#65279;&#847; &#8199;&#65279;&#847; &#8199;&#65279;&#847; &#8199;&#65279;&#847; &#8199;&#65279;&#847; &#8199;&#65279;&#847; &#8199;&#65279;&#847; &#8199;&#65279;&#847;
@@ -42,7 +42,7 @@ background-color:#272d3a!important;}[data-ogsb] .nx-dark-bg-primary-15{backgroun
42
42
  <!--[if mso]><i style="mso-font-width:114%;mso-text-raise:19px;" hidden>&emsp;</i>
43
43
  <![endif]--> <span style="mso-text-raise:10px;">Undo this change</span>
44
44
  <!--[if mso]><i style="mso-font-width:114%;" hidden>&emsp;&#8203;</i>
45
- <![endif]--></a></div><div role="separator" style="line-height:24px;">&zwj;</div><p class="nx-dark-text-foreground" style="margin:0 0 16px;font-size:12px;line-height:16px;color:#5f718a;">If the button does not work, open this link: <a href="{{ link }}" style="color:#54a2ff;color:lab(65.0361% -1.42065 -56.9802);text-decoration:underline;">{{ link }}</a></p>
45
+ <![endif]--></a></div><div role="separator" style="line-height:24px;">&zwj;</div><p class="nx-dark-text-foreground" style="margin:0 0 16px;font-size:12px;line-height:16px;color:#5f718a;">If the button does not work, open this link: <a href="{{ link }}" class="nx-dark-text-info" style="color:#1d4ed8;text-decoration:underline;">{{ link }}</a></p>
46
46
  </td></tr><tr><td class="nx-dark-text-muted-foreground" style="padding-left:24px;padding-right:24px;padding-top:24px;text-align:center;font-size:12px;line-height:16px;color:#5f718a;"><p style="margin:0 0 8px;">You received this e-mail because you have an account with {{ brand }}.</p><p style="margin:0;">{{ brand }}</p>
47
47
  </td></tr></table></div>
48
48
  <!--[if mso]>
@@ -10,11 +10,11 @@
10
10
  -8.51797)!important;}[data-ogsb] .nx-dark-bg-background{background-color:#020918!important;background-color:lab(2.35721% .367686 -8.51797)!important;}[data-ogsc] .nx-dark-text-foreground{color:#f8fafc!important;color:lab(98.1434% -.369519 -1.05966)!important;}[data-ogsb] .nx-dark-text-foreground{color:#f8fafc!important;color:lab(98.1434% -.369519 -1.05966)!important;}[data-ogsc] .nx-dark-text-foreground-25{color:#404551!important;}[data-ogsb] .nx-dark-text-foreground-25{color:#404551!important;}
11
11
  [data-ogsc] .nx-dark-text-card-foreground{color:#f8fafc!important;color:lab(98.1434% -.369519 -1.05966)!important;}[data-ogsb] .nx-dark-text-card-foreground{color:#f8fafc!important;color:lab(98.1434% -.369519 -1.05966)!important;}[data-ogsc] .nx-dark-border-border{border-color:#1c212f!important;}[data-ogsb] .nx-dark-border-border{border-color:#1c212f!important;}[data-ogsc] .nx-dark-bg-muted{background-color:#1e293b!important;}[data-ogsb] .nx-dark-bg-muted{background-color:#1e293b!important;}
12
12
  [data-ogsc] .nx-dark-text-muted-foreground{color:#cbd5e1!important;}[data-ogsb] .nx-dark-text-muted-foreground{color:#cbd5e1!important;}[data-ogsc] .nx-dark-border-muted-foreground{border-color:#cbd5e1!important;}[data-ogsb] .nx-dark-border-muted-foreground{border-color:#cbd5e1!important;}[data-ogsc] .nx-dark-border-muted{border-color:#1e293b!important;}[data-ogsb] .nx-dark-border-muted{border-color:#1e293b!important;}[data-ogsc] .nx-dark-bg-primary{background-color:#fafafa!important;}
13
- [data-ogsb] .nx-dark-bg-primary{background-color:#fafafa!important;}[data-ogsc] .nx-dark-text-primary{color:#fafafa!important;}[data-ogsb] .nx-dark-text-primary{color:#fafafa!important;}[data-ogsc] .nx-dark-text-primary-foreground{color:#18181b!important;}[data-ogsb] .nx-dark-text-primary-foreground{color:#18181b!important;}[data-ogsc] .nx-dark-border-primary{border-color:#fafafa!important;}[data-ogsb] .nx-dark-border-primary{border-color:#fafafa!important;}[data-ogsc] .nx-dark-bg-primary-15{
14
- background-color:#272d3a!important;}[data-ogsb] .nx-dark-bg-primary-15{background-color:#272d3a!important;}[data-ogsc] .nx-dark-bg-primary-20{background-color:#343945!important;}[data-ogsb] .nx-dark-bg-primary-20{background-color:#343945!important;}[data-ogsc] .nx-dark-border-primary-40{border-color:#656972!important;}[data-ogsb] .nx-dark-border-primary-40{border-color:#656972!important;}[data-ogsc] .nx-dark-border-primary-50{border-color:#7e8189!important;}[data-ogsb] .nx-dark-border-primary-50
15
- {border-color:#7e8189!important;}[data-ogsc] .nx-light-only{display:none!important;}[data-ogsb] .nx-light-only{display:none!important;}[data-ogsc] .nx-dark-only{display:inline-block!important;}[data-ogsb] .nx-dark-only{display:inline-block!important;}
13
+ [data-ogsb] .nx-dark-bg-primary{background-color:#fafafa!important;}[data-ogsc] .nx-dark-text-primary{color:#fafafa!important;}[data-ogsb] .nx-dark-text-primary{color:#fafafa!important;}[data-ogsc] .nx-dark-text-primary-foreground{color:#18181b!important;}[data-ogsb] .nx-dark-text-primary-foreground{color:#18181b!important;}[data-ogsc] .nx-dark-text-info{color:#93c5fd!important;}[data-ogsb] .nx-dark-text-info{color:#93c5fd!important;}[data-ogsc] .nx-dark-border-primary{border-color:#fafafa
14
+ !important;}[data-ogsb] .nx-dark-border-primary{border-color:#fafafa!important;}[data-ogsc] .nx-dark-bg-primary-15{background-color:#272d3a!important;}[data-ogsb] .nx-dark-bg-primary-15{background-color:#272d3a!important;}[data-ogsc] .nx-dark-bg-primary-20{background-color:#343945!important;}[data-ogsb] .nx-dark-bg-primary-20{background-color:#343945!important;}[data-ogsc] .nx-dark-border-primary-40{border-color:#656972!important;}[data-ogsb] .nx-dark-border-primary-40{border-color:#656972
15
+ !important;}[data-ogsc] .nx-dark-border-primary-50{border-color:#7e8189!important;}[data-ogsb] .nx-dark-border-primary-50{border-color:#7e8189!important;}[data-ogsc] .nx-light-only{display:none!important;}[data-ogsb] .nx-light-only{display:none!important;}[data-ogsc] .nx-dark-only{display:inline-block!important;}[data-ogsb] .nx-dark-only{display:inline-block!important;}
16
16
  @media (prefers-color-scheme:dark){html{--lightningcss-light:;--lightningcss-dark:initial;}.nx-dark-bg-paper{background-color:#040a19!important;}.nx-dark-bg-card{background-color:#0f172b!important;background-color:lab(7.78673% 1.82345 -15.0537)!important;}.nx-dark-text-foreground{color:#f8fafc!important;color:lab(98.1434% -.369519 -1.05966)!important;}.nx-dark-text-card-foreground{color:#f8fafc!important;color:lab(98.1434% -.369519 -1.05966)!important;}.nx-dark-border-border{border-color:#1c212f
17
- !important;}.nx-dark-text-muted-foreground{color:#cbd5e1!important;}.nx-dark-bg-primary{background-color:#fafafa!important;}.nx-dark-text-primary-foreground{color:#18181b!important;} }
17
+ !important;}.nx-dark-text-muted-foreground{color:#cbd5e1!important;}.nx-dark-bg-primary{background-color:#fafafa!important;}.nx-dark-text-primary-foreground{color:#18181b!important;}.nx-dark-text-info{color:#93c5fd!important;} }
18
18
  </style>
19
19
  </head>
20
20
  <body xml:lang="en" dir="ltr" style="margin:0;padding:0;width:100%;height:100%;word-break:break-word;background-color:#f4f4f4;" class="nx-dark-bg-paper"><div style="display:none;">The password of your account was just changed.&#8199;&#65279;&#847; &#8199;&#65279;&#847; &#8199;&#65279;&#847; &#8199;&#65279;&#847; &#8199;&#65279;&#847; &#8199;&#65279;&#847; &#8199;&#65279;&#847; &#8199;&#65279;&#847; &#8199;&#65279;&#847; &#8199;&#65279;&#847; &#8199;&#65279;&#847; &#8199;&#65279;&#847; &#8199;&#65279;&#847;
@@ -42,7 +42,7 @@ background-color:#272d3a!important;}[data-ogsb] .nx-dark-bg-primary-15{backgroun
42
42
  <!--[if mso]><i style="mso-font-width:114%;mso-text-raise:19px;" hidden>&emsp;</i>
43
43
  <![endif]--> <span style="mso-text-raise:10px;">Secure my account</span>
44
44
  <!--[if mso]><i style="mso-font-width:114%;" hidden>&emsp;&#8203;</i>
45
- <![endif]--></a></div><div role="separator" style="line-height:24px;">&zwj;</div><p class="nx-dark-text-foreground" style="margin:0 0 16px;font-size:12px;line-height:16px;color:#5f718a;">If the button does not work, open this link: <a href="{{ link }}" style="color:#54a2ff;color:lab(65.0361% -1.42065 -56.9802);text-decoration:underline;">{{ link }}</a></p>
45
+ <![endif]--></a></div><div role="separator" style="line-height:24px;">&zwj;</div><p class="nx-dark-text-foreground" style="margin:0 0 16px;font-size:12px;line-height:16px;color:#5f718a;">If the button does not work, open this link: <a href="{{ link }}" class="nx-dark-text-info" style="color:#1d4ed8;text-decoration:underline;">{{ link }}</a></p>
46
46
  </td></tr><tr><td class="nx-dark-text-muted-foreground" style="padding-left:24px;padding-right:24px;padding-top:24px;text-align:center;font-size:12px;line-height:16px;color:#5f718a;"><p style="margin:0 0 8px;">You received this e-mail because you have an account with {{ brand }}.</p><p style="margin:0;">{{ brand }}</p>
47
47
  </td></tr></table></div>
48
48
  <!--[if mso]>