@nxgt/janus-mail 0.7.0 → 0.8.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.
@@ -1 +1 @@
1
- {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"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"}
package/docs/README.md CHANGED
@@ -5,7 +5,7 @@ the detail.
5
5
 
6
6
  | Page | Read it when |
7
7
  | --- | --- |
8
- | [Sending](guide/sending.md) | You are wiring `janusMail()` into your routes: the options, `clock` among them; each method, the welcome on `user.created`, the recovery code notice on `user.recoveryCodeUsed` and the change notices on `user.passwordChanged` and `user.emailChanged` among them; the expiry an e-mail states and how to set it, where each e-mail goes, what a failure looks like, retrying and tracing the mailer, and a test |
8
+ | [Sending](guide/sending.md) | You are wiring `janusMail()` into your routes: the options, `clock` among them; each method, the step-up's code, the welcome on `user.created`, the recovery code notice on `user.recoveryCodeUsed` and the change notices on `user.passwordChanged` and `user.emailChanged` among them; the expiry an e-mail states and how to set it, where each e-mail goes, what a failure looks like, retrying and tracing the mailer, and a test |
9
9
  | [Templates](guide/templates.md) | You want an e-mail of your own: what a template takes and answers, a partial override, `janusTemplates()`, and what the defaults say |
10
10
  | [Locales](guide/locales.md) | You send in more than one language: how the locale is picked, `fallbackLocale`, fewer locales, and adding one |
11
11
  | [Building](guide/building.md) | You work on this package: the Maizzle project in `mail/`, `mails/`, the generated files, and why none of it reaches a consumer's build |
@@ -23,11 +23,11 @@ They mean the same here. These pages add:
23
23
 
24
24
  | Word | Means | Not |
25
25
  | --- | --- | --- |
26
- | **e-mail** | One of the ten this package sends: `verify-email`, `reset-password`, `sign-in-code`, `magic-link`, `password-changed`, `email-changed`, `two-factor-enabled`, `two-factor-disabled`, `recovery-code-used`, `welcome` — by the name of its built file | "mail", "message" — a *message* is what a mailer sends, addressed |
26
+ | **e-mail** | One of the eleven this package sends: `verify-email`, `reset-password`, `sign-in-code`, `magic-link`, `confirm-action`, `password-changed`, `email-changed`, `two-factor-enabled`, `two-factor-disabled`, `recovery-code-used`, `welcome` — by the name of its built file | "mail", "message" — a *message* is what a mailer sends, addressed |
27
27
  | **template** | The function that renders one e-mail: `(variables & { locale }) => Rendered`. Named in camelCase, after the method that sends it: `verifyEmail`, `signInCode` | "renderer" — the renderer is `@nxgt/mail`'s, and reads the build |
28
28
  | **default template** | A template of `janusTemplates()`: the prebuilt e-mail, filled by the renderer | "preset" — a preset is `@nxgt/mail-presets`' source, which the build compiles |
29
29
  | **override** | A template of yours, passed in `templates`, used instead of the default | "custom template" |
30
- | **issued** | What a flow answered: an `IssuedToken` from `verifyEmail.send`, `resetPassword.request` or `magicLink.request`, an `IssuedCode` from `signInCode.request` | "result", "payload" |
30
+ | **issued** | What a flow answered: an `IssuedToken` from `verifyEmail.send`, `resetPassword.request` or `magicLink.request`, an `IssuedCode` from `signInCode.request`, a `StepUpByEmail` from `stepUp.request` once narrowed to `via: 'email'` | "result", "payload" |
31
31
  | **recipient** | Who an e-mail is for besides the address: a `Recipient`, `{ name, locale? }` | "user" — a user is a record of `@nxgt/janus`; "to" is the address |
32
32
  | **notice** | An e-mail that tells, and asks for nothing: `passwordChanged`, `emailChanged`, `twoFactorEnabled`, `twoFactorDisabled`, `recoveryCodeUsed`. It links to `links.secureAccount()` — `recoveryCodeUsed` to `links.recoveryCodes()` when given | "alert", "notification" |
33
33
  | **welcome** | The e-mail sent once a user was created, on `@nxgt/janus`'s `user.created`: `welcome`. It links to `links.getStarted()` — not a notice, since it warns of nothing | "onboarding", "greeting" |
@@ -55,7 +55,7 @@ packages/janus-mail/
55
55
  `src/generated/mail.ts` (`rendererTypes`), from the build.
56
56
  3. `scripts/build-mail.ts` reads the manifest and **fails unless its
57
57
  format is one every `@nxgt/mail` the peer admits reads** — see below —
58
- and **unless exactly the ten e-mails were built**: an eleventh from a new
58
+ and **unless exactly the eleven e-mails were built**: a twelfth from a new
59
59
  `@nxgt/mail-presets`, or one missing, stops the build.
60
60
  4. It writes `src/generated/locales.ts` from the manifest's locales, and
61
61
  `src/generated/codes-left.ts` — below — each only when its content
@@ -82,6 +82,7 @@ const templates: JanusMailTemplates<Locale> = {
82
82
  resetPassword: ({ name, link, expiresIn, locale }) => render('reset-password', locale, { name, link, expiresIn }),
83
83
  signInCode: ({ code, expiresIn, locale }) => render('sign-in-code', locale, { code, expiresIn }),
84
84
  magicLink: ({ link, expiresIn, locale }) => render('magic-link', locale, { link, expiresIn }),
85
+ stepUp: ({ name, code, expiresIn, link, locale }) => render('confirm-action', locale, { name, code, expiresIn, link }),
85
86
  passwordChanged: ({ name, link, locale }) => render('password-changed', locale, { name, link }),
86
87
  emailChanged: ({ name, link, newEmail, locale }) => render('email-changed', locale, { name, link, newEmail }),
87
88
  twoFactorEnabled: ({ name, link, locale }) => render('two-factor-enabled', locale, { name, link }),
@@ -101,8 +102,9 @@ mail.locales; // readonly ('en' | 'fr' | 'de')[]
101
102
  plural of the codes left in `en` and `fr` only, so a German recipient's
102
103
  `mail.recoveryCodeUsed` takes the sentence as text — "Sie haben noch 3
103
104
  Wiederherstellungscodes." — and a count is a `TypeError` there.
104
- `recoveryCodeUsed` is the ninth template, since 0.5.0, and `magicLink` the
105
- tenth, since 0.7.0: a `templates` written for an earlier version no longer
105
+ `recoveryCodeUsed` is the ninth template, since 0.5.0, `magicLink` the
106
+ tenth, since 0.7.0, and `stepUp` the eleventh, since 0.8.0: a `templates`
107
+ written for an earlier version no longer
106
108
  compiles with a locale beyond `en` and `fr` — add the one it lacks. A
107
109
  renderer over a Maizzle build of your own, with
108
110
  `@nxgt/mail-presets` and a `locales/de.json`, is the closest to the defaults.
@@ -110,13 +112,13 @@ With only some templates, the call does not compile — the error lands on
110
112
  `templates`:
111
113
 
112
114
  ```text
113
- TS2740: Type '{ verifyEmail: () => Rendered; }' is missing the following properties from type 'JanusMailTemplates<"en" | "fr" | "de">': welcome, resetPassword, signInCode, magicLink, and 5 more.
115
+ TS2740: Type '{ verifyEmail: () => Rendered; }' is missing the following properties from type 'JanusMailTemplates<"en" | "fr" | "de">': welcome, resetPassword, signInCode, magicLink, and 6 more.
114
116
  ```
115
117
 
116
118
  and in JavaScript it throws when `janusMail()` is called:
117
119
 
118
120
  ```text
119
- TypeError: janusMail: the default templates are built in en and fr only — with another locale in locales, pass every template in templates; resetPassword, signInCode, magicLink, passwordChanged, emailChanged, twoFactorEnabled, twoFactorDisabled, recoveryCodeUsed, welcome missing
121
+ TypeError: janusMail: the default templates are built in en and fr only — with another locale in locales, pass every template in templates; resetPassword, signInCode, magicLink, stepUp, passwordChanged, emailChanged, twoFactorEnabled, twoFactorDisabled, recoveryCodeUsed, welcome missing
120
122
  ```
121
123
 
122
124
  A default template is typed for `'en' | 'fr'`, so reusing one in a wider set
@@ -129,4 +131,4 @@ Built-in locales for more languages are on the [roadmap](../roadmap.md).
129
131
 
130
132
  - `@nxgt/mail`'s [locales guide](https://github.com/softistx/nxgt-mail/blob/develop/packages/mail/docs/guide/locales.md)
131
133
  — `pickLocale` and `parseAcceptLanguage` in full.
132
- - [Templates](templates.md) — writing the ten for a new locale.
134
+ - [Templates](templates.md) — writing the eleven for a new locale.
@@ -46,7 +46,7 @@ time one is sent, and kept.
46
46
  | `brand` | `string` | required | The name the e-mails show, in the header, the body and the footer. Text: escaped in the HTML |
47
47
  | `links.verifyEmail` | `(token) => string` | required | The page that confirms an address, given the one-time token |
48
48
  | `links.resetPassword` | `(token) => string` | required | The page that sets a new password, given the one-time token |
49
- | `links.secureAccount` | `() => string` | required | Where a user who made no change secures their account: the notices link to it |
49
+ | `links.secureAccount` | `() => string` | required | Where a user who made no change secures their account: the notices link to it, and so does the step-up's code, for a user who asked for none |
50
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
51
  | `links.magicLink` | `(token) => string` | none: `magicLink` is a `TypeError` without it | The page a sign-in link opens, given the one-time token. That page spends nothing: its button posts the token to the route that calls `auth.magicLink.confirm` |
52
52
  | `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 |
@@ -145,6 +145,44 @@ nobody by name — and only its `locale` is read.
145
145
  visitor's secret, and no `token`: the compiler refuses it here, and
146
146
  JavaScript gets `janusMail.magicLink: token must be a string`.
147
147
 
148
+ ### `stepUp(issued, to, options?)`
149
+
150
+ ```ts
151
+ const current = await auth.authenticate(request); // the signed-in user, their session
152
+ if (current === null) return new Response(null, { status: 401 });
153
+ const issued = await auth.stepUp.request(current.user); // StepUpByEmail, or StepUpByApp with a second factor
154
+ if (issued.via === 'email') {
155
+ await mail.stepUp(issued, { name: current.user.name, locale: current.user.locale });
156
+ }
157
+ return Response.json({ challenge: issued.challenge, via: issued.via }); // the challenge stays with the user
158
+ ```
159
+
160
+ The code of a step-up — a signed-in user proving again who they are before
161
+ a sensitive action, `@nxgt/janus`'s
162
+ [step-up guide](https://github.com/softistx/nxgt-janus/blob/develop/packages/janus/docs/guide/step-up.md) —
163
+ sent to `issued.email`, greeting `to.name`: "Your confirmation code",
164
+ "Votre code de confirmation", the code, how long it lasts — "This code
165
+ expires in 10 minutes." — and a **Secure my account** button to
166
+ `links.secureAccount()`, for a user who asked for nothing. The e-mail names
167
+ no action: it says "something sensitive", whatever the route guards.
168
+
169
+ - **Narrow `via` first.** A user whose second factor is active confirms with
170
+ their app: `request` answers `via: 'secondFactor'`, with no code and no
171
+ address, and there is nothing to send. The compiler refuses the answer
172
+ un-narrowed; JavaScript gets
173
+ `janusMail.stepUp: via must be 'email' — a step-up confirmed with the user's app sends no e-mail`,
174
+ before anything is rendered. A type without `secondFactor` in `janus()`
175
+ answers `via: 'email'` alone, and needs no check.
176
+ - **`to` is required**, unlike `signInCode`'s: the e-mail greets the user by
177
+ name, and it is the user of the session, whose name you hold.
178
+ - It reads `issued.via`, `issued.code`, `issued.email` and
179
+ `issued.expiresAt`, **and nothing else**: the challenge never reaches a
180
+ template.
181
+ - **The link has no fallback, and needs none.** `links.secureAccount` is
182
+ required by `janusMail()`: the notices link to it too.
183
+ - **Rate-limit `stepUp.request` per user**, as the step-up guide says: each
184
+ request sends this e-mail.
185
+
148
186
  ### `passwordChanged(to)`
149
187
 
150
188
  ```ts
@@ -161,8 +199,10 @@ const auth = janus({
161
199
 
162
200
  A notice: the password of the account at `to.email` changed, with a link to
163
201
  `links.secureAccount()` for a user who did not change it. Send it on the
164
- `user.passwordChanged` event, which `changePassword` and `setPassword` send.
165
- A reset sends `user.passwordReset` instead, never both: to tell a reset too,
202
+ `user.passwordChanged` event, which `changePassword` and `setPassword` send —
203
+ and, from `@nxgt/janus` 0.16, `signInCode.confirm` and `magicLink.confirm`
204
+ when their first proof of the e-mail dropped a password someone may have set
205
+ without holding the inbox. A reset sends `user.passwordReset` instead, never both: to tell a reset too,
166
206
  send the same notice on that type — `event.type === 'user.passwordChanged'
167
207
  || event.type === 'user.passwordReset'`. The event names the user by id
168
208
  alone, so read the name, the locale and the address from the user.
@@ -210,17 +250,21 @@ const auth = janus({
210
250
  });
211
251
  ```
212
252
 
213
- Two notices: two-factor authentication was turned on, or off, for the
214
- account at `to.email`, each with a link to `links.secureAccount()` — the
215
- user's security settings — for a user who did not make the change. Send
253
+ Two notices: two-factor authentication was turned on, or off, for the account
254
+ at `to.email`, each with a link to `links.secureAccount()` — the user's
255
+ security settings — for a user who did not make the change. Send
216
256
  `twoFactorEnabled` on the `user.secondFactorEnabled` event, which
217
257
  `auth.secondFactor.activate` sends once the factor is active, and
218
258
  `twoFactorDisabled` on `user.secondFactorDisabled`, which
219
259
  `auth.secondFactor.disable` sends only when it removed an active factor: a
220
- `disable` on a user who had none tells nobody anything. The event names the
221
- user by id alone, so read the name, the locale and the address from the
222
- user. Calling them right after `activate` or `disable` answered works as
223
- well — without the events, check `hasSecondFactor` before the `disable`.
260
+ `disable` on a user who had none tells nobody anything. From `@nxgt/janus`
261
+ 0.16, `signInCode.confirm` and `magicLink.confirm` send it too when their
262
+ first proof of the e-mail removed a factor someone may have enrolled without
263
+ holding the inbox — after `user.passwordChanged`, so the inbox's owner gets
264
+ both notices. The event names the user by id alone, so read the name, the
265
+ locale and the address from the user. Calling them right after `activate` or
266
+ `disable` answered works as well — without the events, check `hasSecondFactor`
267
+ before the `disable`.
224
268
 
225
269
  ### `welcome(to)`
226
270
 
@@ -307,8 +351,8 @@ when opening the session then fails.
307
351
 
308
352
  ## The expiry
309
353
 
310
- `verifyEmail`, `resetPassword`, `signInCode` and `magicLink` say how long
311
- the link or the code lasts — "This link expires in 1 hour.", "Ce code expire dans
354
+ `verifyEmail`, `resetPassword`, `signInCode`, `magicLink` and `stepUp` say
355
+ how long the link or the code lasts — "This link expires in 1 hour.", "Ce code expire dans
312
356
  10 minutes." The text is `expiresIn`, one of the template's variables,
313
357
  derived at send time from the flow's `issued.expiresAt`:
314
358
 
@@ -322,7 +366,7 @@ derived at send time from the flow's `issued.expiresAt`:
322
366
  `unitDisplay: 'long'`, in the recipient's locale: "3 hours", "3 heures".
323
367
 
324
368
  With `@nxgt/janus`'s defaults, a verification link says "1 day", a reset
325
- link "1 hour" and a sign-in code "10 minutes".
369
+ link "1 hour", and a sign-in code and a step-up's code "10 minutes".
326
370
 
327
371
  The time is `janusMail({ clock })`'s — pass the clock `janus({ clock })` was
328
372
  given, a `fixedClock` in tests — else the system clock:
@@ -357,6 +401,7 @@ a `TypeError`, and nothing reaches the mailer: revive the date with
357
401
  | `resetPassword` | `issued.email` | the `email` passed to `request` |
358
402
  | `signInCode` | `issued.email` | the `email` passed to `request` |
359
403
  | `magicLink` | `issued.email` | the `email` passed to `request` |
404
+ | `stepUp` | `issued.email` | an address the client sent |
360
405
  | `passwordChanged` | `to.email` | — |
361
406
  | `emailChanged` | `to.formerEmail` | `to.newEmail` |
362
407
  | `twoFactorEnabled`, `twoFactorDisabled` | `to.email` | — |
@@ -380,7 +425,7 @@ defines no error class and wraps nothing:
380
425
  | --- | --- | --- | --- |
381
426
  | `MailFailure` (`MAIL_FAILED`) | the mailer | The transport could not hand the e-mail over. Nothing is known to have been sent | A `503`, or a retry from a queue |
382
427
  | `MailRefused` (`MAIL_REFUSED`) | the renderer, or the mailer | The e-mail itself is wrong: a link that is not `http:`, `https:` or `mailto:`, an address that is not one | A bug to fix; sending it again fails again |
383
- | `TypeError` | this package, or the renderer | A call without the value a flow answered — `janusMail.resetPassword: token must be a string` — or, in JavaScript, a link that is not a string (a compile error in TypeScript) — `janusMail.verifyEmail: links.verifyEmail(token) must answer a string`. For [the expiry](#the-expiry): `janusMail.<method>: expiresAt must be a Date`, `janusMail.<method>: expiresAt is past — the link or code would not work`, `janusMail.<method>: expiresIn must be a string`, `janusMail.<method>: clock.now() must answer a Date` | A bug to fix — for a past `expiresAt`, issue a new token or code and send that |
428
+ | `TypeError` | this package, or the renderer | A call without the value a flow answered — `janusMail.resetPassword: token must be a string` — or, in JavaScript, a link that is not a string (a compile error in TypeScript) — `janusMail.verifyEmail: links.verifyEmail(token) must answer a string`. For [the expiry](#the-expiry): `janusMail.<method>: expiresAt must be a Date`, `janusMail.<method>: expiresAt is past — the link or code would not work`, `janusMail.<method>: expiresIn must be a string`, `janusMail.<method>: clock.now() must answer a Date`. For `stepUp`: `janusMail.stepUp: via must be 'email' — …` | A bug to fix — for a past `expiresAt`, issue a new token or code and send that |
384
429
  | `Error` from `createMailRenderer` | the renderer | `mails/` is missing where the package runs: a bundler inlined `@nxgt/janus-mail`, or a deploy kept `dist/` only | Keep `@nxgt/janus-mail` external to your bundle and deploy its `mails/` with it; see [troubleshooting](../troubleshooting.md#createmailrenderer-mailsmail-manifestjson-cannot-be-read--run-maizzle-build-and-deploy-its-output-folder) |
385
430
 
386
431
  `instanceof` holds against the classes of your own `@nxgt/mail`, since it is
@@ -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 ten, using the defaults on their
4
+ is given and answers, replacing some of the eleven, 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 eight others stay the defaults.
25
+ The sign-in code e-mail is now yours; the ten others stay the defaults.
26
26
 
27
27
  ## What a template is
28
28
 
@@ -63,6 +63,7 @@ e-mail.
63
63
  | `resetPassword` | `brand`, `name`, `link`, `expiresIn` | `mail.resetPassword(issued, to, options?)` |
64
64
  | `signInCode` | `brand`, `code`, `expiresIn` | `mail.signInCode(issued, to?, options?)` |
65
65
  | `magicLink` | `brand`, `link`, `expiresIn` | `mail.magicLink(issued, to?, options?)` |
66
+ | `stepUp` | `brand`, `name`, `code`, `expiresIn`, `link` | `mail.stepUp(issued, to, options?)` |
66
67
  | `passwordChanged` | `brand`, `name`, `link` | `mail.passwordChanged(to)` |
67
68
  | `emailChanged` | `brand`, `name`, `link`, `newEmail` | `mail.emailChanged(to)` |
68
69
  | `twoFactorEnabled` | `brand`, `name`, `link` | `mail.twoFactorEnabled(to)` |
@@ -72,7 +73,7 @@ e-mail.
72
73
 
73
74
  Every one also gets `locale`, one of `locales`. Every value is a string:
74
75
  `brand` from the options, `name` from the recipient, `link` from `links` —
75
- `links.secureAccount()` for the four notices, `links.recoveryCodes()` (else
76
+ `links.secureAccount()` for the four notices and `stepUp`, `links.recoveryCodes()` (else
76
77
  `links.secureAccount()`) for `recoveryCodeUsed`, `links.getStarted()` for
77
78
  `welcome` — `code`, `newEmail` and `when` from the
78
79
  call, and `expiresIn` derived from the flow's `expiresAt` in the locale, or
@@ -95,8 +96,8 @@ templates: {
95
96
  // await mail.recoveryCodeUsed(to, { when, recoveryCodesLeft: `Sie haben noch ${left} Wiederherstellungscodes.` });
96
97
  ```
97
98
 
98
- `signInCode` is **never given the challenge**: it is not in its variables,
99
- and the method never reads it. An override states the expiry from
99
+ `signInCode` and `stepUp` are **never given the challenge**: it is not in
100
+ their variables, and the methods never read it. An override states the expiry from
100
101
  `expiresIn`, already formatted:
101
102
 
102
103
  ```ts
@@ -125,11 +126,11 @@ methods live on its prototype and are refused — see
125
126
  [troubleshooting](../troubleshooting.md#janusmail-templatesname-is-not-an-own-enumerable-property--pass-a-plain-object-as--name-variables--rendered-).
126
127
 
127
128
  `templates` is `Partial` while `locales` stays within `en` and `fr`: give
128
- any of the ten, and the defaults render the rest. Once `locales` holds
129
- another locale, it takes **all ten** — see
129
+ any of the eleven, and the defaults render the rest. Once `locales` holds
130
+ another locale, it takes **all eleven** — `stepUp` included, since 0.8 — see
130
131
  [Adding a locale](locales.md#adding-a-locale).
131
132
 
132
- `mail.templates` answers the ten in use — yours, and the defaults for the
133
+ `mail.templates` answers the eleven in use — yours, and the defaults for the
133
134
  rest — frozen:
134
135
 
135
136
  ```ts
@@ -151,7 +152,7 @@ const { subject, html, text } = await defaults.resetPassword({
151
152
  });
152
153
  ```
153
154
 
154
- It answers the ten defaults, typed for `'en' | 'fr'`, without a mailer:
155
+ It answers the eleven defaults, typed for `'en' | 'fr'`, without a mailer:
155
156
  to preview an e-mail, to wrap one — add a line to the default text, say — or
156
157
  to send one through something other than `janusMail()`. A default reused for
157
158
  a wider set of locales is a compile error: it could not render the one it
@@ -177,7 +178,7 @@ them renders: that is when the package's `mails/` folder is read, once.
177
178
  ## What the defaults say
178
179
 
179
180
  The defaults are [`@nxgt/mail-presets`](https://www.npmjs.com/package/@nxgt/mail-presets)
180
- 1.0.1, built with a neutral grey theme and no logo:
181
+ 1.1.0, built with a neutral grey theme and no logo:
181
182
 
182
183
  | Template | Built from | Subject (`en`) | Subject (`fr`) |
183
184
  | --- | --- | --- | --- |
@@ -185,6 +186,7 @@ The defaults are [`@nxgt/mail-presets`](https://www.npmjs.com/package/@nxgt/mail
185
186
  | `resetPassword` | `reset-password` | Reset your password | Réinitialisez votre mot de passe |
186
187
  | `signInCode` | `sign-in-code` | Your sign-in code: `042817` | Votre code de connexion : `042817` |
187
188
  | `magicLink` | `magic-link` | Your sign-in link | Votre lien de connexion |
189
+ | `stepUp` | `confirm-action` | Your confirmation code | Votre code de confirmation |
188
190
  | `passwordChanged` | `password-changed` | Your password was changed | Votre mot de passe a été modifié |
189
191
  | `emailChanged` | `email-changed` | Your e-mail address was changed | Votre adresse e-mail a été modifiée |
190
192
  | `twoFactorEnabled` | `two-factor-enabled` | Two-factor authentication was turned on | L'authentification à deux facteurs a été activée |
@@ -194,16 +196,19 @@ The defaults are [`@nxgt/mail-presets`](https://www.npmjs.com/package/@nxgt/mail
194
196
 
195
197
  Each has a header and a footer with the brand, a heading, a greeting by name
196
198
  (but `signInCode` and `magicLink`), a button with its link and the link again in text for a
197
- client that shows no button, and a text part. The other four notices add a warning:
198
- *if this was not you, secure your account now*. `recoveryCodeUsed` says it
199
+ client that shows no button, and a text part. The four notices but `recoveryCodeUsed` add a warning:
200
+ *if this was not you, secure your account now*. `stepUp` shows its code as
201
+ the sign-in code does, names no action — *someone, we hope you, asked to do
202
+ something sensitive on your account* — and warns not to share the code if
203
+ it was not the user, its button saying **Secure my account**. `recoveryCodeUsed` says it
199
204
  too, after a warning banner — *a recovery code was used on your account at
200
205
  `when`* — the codes left, and a line on generating new ones; its button says
201
206
  **Secure my account**. `welcome` has the brand in
202
207
  its heading — "Welcome to Acme" — and the recipient's name in its subject,
203
208
  filled at send time like the body; its button says **Get started**.
204
209
 
205
- `verifyEmail`, `resetPassword`, `signInCode` and `magicLink` also say how
206
- long the link or the code lasts: "This link expires in 1 hour.", "Ce code expire dans
210
+ `verifyEmail`, `resetPassword`, `signInCode`, `magicLink` and `stepUp` also
211
+ say how long the link or the code lasts: "This link expires in 1 hour.", "Ce code expire dans
207
212
  10 minutes." The text part keeps each paragraph on one line.
208
213
 
209
214
  **The HTML follows dark mode.** Each HTML part declares
@@ -215,7 +220,7 @@ is near-black (`#27272a`) with light text in light mode, and near-white
215
220
  (`#fafafa`) with near-black text (`#18181b`) in dark mode — 17:1 against the
216
221
  dark card, where the light primary would be 1.2:1. The page behind the card
217
222
  is 5% of the light primary over the dark background, a shade darker than the
218
- card, which its border outlines. The sign-in code's box is a light grey
223
+ card, which its border outlines. The code's box — the sign-in code's and the step-up's — is a light grey
219
224
  (`#f1f5f9`) with near-black code (`#020918`) in light mode, and a slate a
220
225
  step above the dark card (`#1e293b`) with light code (`#cbd5e1`) in dark
221
226
  mode, 9.85:1. The muted text — the footer, and the closing *if you did not
package/docs/roadmap.md CHANGED
@@ -40,13 +40,23 @@ Nothing between releases.
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
42
42
  hook to turn off.
43
- - **The challenge in an e-mail** — a sign-in code e-mail carries the code and
44
- never the challenge, not even through an override.
43
+ - **The challenge in an e-mail** — a sign-in code's or a step-up's e-mail
44
+ carries the code and never the challenge, not even through an override.
45
45
 
46
46
  ## Shipped
47
47
 
48
48
  Newest first; from the first release on, the package's CHANGELOG holds every one.
49
49
 
50
+ - **The step-up's code, v0.8.0.** `stepUp(issued, to, options?)`, built
51
+ from `@nxgt/mail-presets` 1.1.0's `confirm-action` in English and French:
52
+ send what `@nxgt/janus`'s `auth.stepUp.request(user)` answered, once
53
+ narrowed to `via: 'email'`, to `issued.email` — "Your confirmation code",
54
+ the code, how long it lasts, and a **Secure my account** button to
55
+ `links.secureAccount()` for a user who asked for nothing. A step-up
56
+ confirmed with the user's app sends nothing, and is refused. Eleven
57
+ e-mails now, and eleven templates to pass for a locale beyond `en` and
58
+ `fr`.
59
+
50
60
  - **The sign-in link, v0.7.0.** `magicLink(issued, to?, options?)`, built
51
61
  from `@nxgt/mail-presets` 1.0.1's `magic-link` in English and French:
52
62
  send what `@nxgt/janus` 0.15's `auth.magicLink.request(email)` answered,
@@ -34,7 +34,7 @@ How the messages are shaped:
34
34
  - [`janusMail: locales holds the same locale twice`](#janusmail-locales-holds-the-same-locale-twice)
35
35
  - [`janusMail: fallbackLocale must be one of locales`](#janusmail-fallbacklocale-must-be-one-of-locales)
36
36
  - [`janusMail: templates must be an object of functions, as { verifyEmail: (variables) => rendered }`](#janusmail-templates-must-be-an-object-of-functions-as--verifyemail-variables--rendered-)
37
- - [`janusMail: templates has no template <name> — name one of verifyEmail, resetPassword, signInCode, magicLink, passwordChanged, emailChanged, twoFactorEnabled, twoFactorDisabled, recoveryCodeUsed, welcome`](#janusmail-templates-has-no-template-name--name-one-of-verifyemail-resetpassword-signincode-magiclink-passwordchanged-emailchanged-twofactorenabled-twofactordisabled-recoverycodeused-welcome)
37
+ - [`janusMail: templates has no template <name> — name one of verifyEmail, resetPassword, signInCode, magicLink, stepUp, passwordChanged, emailChanged, twoFactorEnabled, twoFactorDisabled, recoveryCodeUsed, welcome`](#janusmail-templates-has-no-template-name--name-one-of-verifyemail-resetpassword-signincode-magiclink-stepup-passwordchanged-emailchanged-twofactorenabled-twofactordisabled-recoverycodeused-welcome)
38
38
  - [`janusMail: templates.<name> must be a function`](#janusmail-templatesname-must-be-a-function)
39
39
  - [`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-)
40
40
  - [`janusMail: clock must be a Clock — an object with a now function`](#janusmail-clock-must-be-a-clock--an-object-with-a-now-function)
@@ -42,6 +42,7 @@ How the messages are shaped:
42
42
 
43
43
  **Sending**
44
44
  - [`janusMail.magicLink: links.magicLink is missing — pass it to janusMail({ links }) to send sign-in links`](#janusmailmagiclink-linksmagiclink-is-missing--pass-it-to-janusmail-links--to-send-sign-in-links)
45
+ - [`janusMail.stepUp: via must be 'email' — a step-up confirmed with the user's app sends no e-mail`](#janusmailstepup-via-must-be-email--a-step-up-confirmed-with-the-users-app-sends-no-e-mail)
45
46
  - [`janusMail.<method>: <field> must be a string`](#janusmailmethod-field-must-be-a-string)
46
47
  - [`janusMail.<method>: expiresAt must be a Date`](#janusmailmethod-expiresat-must-be-a-date)
47
48
  - [`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)
@@ -68,6 +69,8 @@ How the messages are shaped:
68
69
  - [`TS2739: Type '{ … }' is missing the following properties from type 'JanusMailTemplates<…>'`](#ts2739-type----is-missing-the-following-properties-from-type-janusmailtemplates)
69
70
  - [`TS2741: Property 'recoveryCodeUsed' is missing in type '{ … }' but required in type 'JanusMailTemplates<…>'.`](#ts2741-property-recoverycodeused-is-missing-in-type----but-required-in-type-janusmailtemplates)
70
71
  - [`TS2741: Property 'magicLink' is missing in type '{ … }' but required in type 'JanusMailTemplates<…>'.`](#ts2741-property-magiclink-is-missing-in-type----but-required-in-type-janusmailtemplates)
72
+ - [`TS2741: Property 'stepUp' is missing in type '{ … }' but required in type 'JanusMailTemplates<…>'.`](#ts2741-property-stepup-is-missing-in-type----but-required-in-type-janusmailtemplates)
73
+ - [`TS2345: Argument of type 'StepUpByEmail<…> | StepUpByApp<…>' is not assignable to parameter of type 'Pick<StepUpByEmail<unknown>, …>'.`](#ts2345-argument-of-type-stepupbyemail--stepupbyapp-is-not-assignable-to-parameter-of-type-pickstepupbyemailunknown-)
71
74
  - [`TS2322: Type '"de"' is not assignable to type '"en" | "fr"'.`](#ts2322-type-de-is-not-assignable-to-type-en--fr)
72
75
  - [`TS2741: Property 'getStarted' is missing in type '{ … }' but required in type 'JanusMailLinks'.`](#ts2741-property-getstarted-is-missing-in-type----but-required-in-type-janusmaillinks)
73
76
  - [`TS2322: Type 'number | null' is not assignable to type 'string | number'.`](#ts2322-type-number--null-is-not-assignable-to-type-string--number)
@@ -196,9 +199,9 @@ A `fallbackLocale` the list does not hold — `'de'` with the default
196
199
 
197
200
  `templates` given as a list or a function. Key each template by its name.
198
201
 
199
- ### `janusMail: templates has no template <name> — name one of verifyEmail, resetPassword, signInCode, magicLink, passwordChanged, emailChanged, twoFactorEnabled, twoFactorDisabled, recoveryCodeUsed, welcome`
202
+ ### `janusMail: templates has no template <name> — name one of verifyEmail, resetPassword, signInCode, magicLink, stepUp, passwordChanged, emailChanged, twoFactorEnabled, twoFactorDisabled, recoveryCodeUsed, welcome`
200
203
 
201
- A key that is not one of the ten — `newSignIn`, or the e-mail's file name
204
+ A key that is not one of the eleven — `newSignIn`, or the e-mail's file name
202
205
  `'verify-email'` rather than the template's, `verifyEmail`. Other e-mails are
203
206
  on the [roadmap](roadmap.md); send them with `@nxgt/mail` directly meanwhile.
204
207
 
@@ -232,7 +235,7 @@ as it is: fields are own properties.
232
235
  ### `janusMail: the default templates are built in en and fr only — with another locale in locales, pass every template in templates; <names> missing`
233
236
 
234
237
  `locales` holds a locale beyond `en` and `fr`, and `templates` leaves some
235
- out: the defaults could not render them in that locale. Pass all ten — see
238
+ out: the defaults could not render them in that locale. Pass all eleven — see
236
239
  [Adding a locale](guide/locales.md#adding-a-locale) — or drop the locale.
237
240
  The message lists the templates missing.
238
241
 
@@ -267,6 +270,26 @@ const mail = janusMail({
267
270
 
268
271
  Sending again fails again: it is a bug, not an outage.
269
272
 
273
+ ### `janusMail.stepUp: via must be 'email' — a step-up confirmed with the user's app sends no e-mail`
274
+
275
+ A `TypeError` from `mail.stepUp`, before anything is rendered or sent: it
276
+ was given what `auth.stepUp.request(user)` answered for a user whose second
277
+ factor is active — `via: 'secondFactor'`, with no code and no address — or
278
+ something with no `via` at all, such as a sign-in code's answer. A step-up
279
+ confirmed with the user's app sends nothing: ask for the code their app
280
+ shows. Check `via` first:
281
+
282
+ ```ts
283
+ const issued = await auth.stepUp.request(current.user);
284
+ if (issued.via === 'email') {
285
+ await mail.stepUp(issued, { name: current.user.name, locale: current.user.locale });
286
+ }
287
+ return Response.json({ challenge: issued.challenge, via: issued.via });
288
+ ```
289
+
290
+ In TypeScript the unchecked call does not compile — see
291
+ [`TS2345: Argument of type 'StepUpByEmail<…> | StepUpByApp<…>' …`](#ts2345-argument-of-type-stepupbyemail--stepupbyapp-is-not-assignable-to-parameter-of-type-pickstepupbyemailunknown-).
292
+
270
293
  ### `janusMail.<method>: <field> must be a string`
271
294
 
272
295
  A `TypeError` from a call, naming the method and the field it could not read
@@ -286,7 +309,9 @@ send it with `signInCode`, or ask `auth.magicLink.request` for a link.
286
309
 
287
310
  `issued` needs `token` and `email` (`code` and `email` for `signInCode`), and
288
311
  the recipient its `name` — a user whose schema has no name passes one
289
- anyway: `{ name: user.email }`. `janusMail.verifyEmail: expiresIn must be a
312
+ anyway: `{ name: user.email }`. `janusMail.stepUp: name must be a string`
313
+ is `stepUp` called without the recipient: unlike `signInCode`, its e-mail
314
+ greets the user by name. `janusMail.verifyEmail: expiresIn must be a
290
315
  string` is the third argument's `expiresIn` given as something else — a
291
316
  number of seconds: pass the text to show, `{ expiresIn: '1 hour' }`, or
292
317
  leave it out.
@@ -322,8 +347,8 @@ formatted in the recipient's locale and time zone —
322
347
 
323
348
  ### `janusMail.<method>: expiresAt must be a Date`
324
349
 
325
- A `TypeError` from `verifyEmail`, `resetPassword`, `signInCode` or
326
- `magicLink`: the e-mail says how long its link or code lasts, from `issued.expiresAt`, and that is
350
+ A `TypeError` from `verifyEmail`, `resetPassword`, `signInCode`,
351
+ `magicLink` or `stepUp`: the e-mail says how long its link or code lasts, from `issued.expiresAt`, and that is
327
352
  not a valid `Date`. The usual cause is a flow's answer that went through
328
353
  JSON — a job queue, a cache — where a `Date` becomes a string. Nothing is
329
354
  sent. Revive it when the job runs, or say the expiry yourself:
@@ -363,8 +388,8 @@ await mail.recoveryCodeUsed(to, { when, recoveryCodesLeft: `Sie haben noch ${lef
363
388
 
364
389
  ### `janusMail.<method>: clock.now() must answer a Date`
365
390
 
366
- A `TypeError` from `verifyEmail`, `resetPassword`, `signInCode` or
367
- `magicLink`: the `clock` given to `janusMail()` has a `now` that answered something else — a
391
+ A `TypeError` from `verifyEmail`, `resetPassword`, `signInCode`,
392
+ `magicLink` or `stepUp`: the `clock` given to `janusMail()` has a `now` that answered something else — a
368
393
  number from `Date.now()`, a string, an invalid `Date`. Nothing is sent.
369
394
  `clock` is `@nxgt/janus`'s `Clock`: pass the one `janus({ clock })` was
370
395
  given, or `{ now: () => new Date() }`.
@@ -497,7 +522,7 @@ avoids the read altogether. (In this repository: run `bun run build`.)
497
522
 
498
523
  The default templates read `mails/` with `node:fs`, through
499
524
  `@nxgt/mail/renderer`: Node, Bun or Deno only. On an edge runtime, pass all
500
- ten templates yourself; an inlined build is on the [roadmap](roadmap.md).
525
+ eleven templates yourself; an inlined build is on the [roadmap](roadmap.md).
501
526
 
502
527
  ### An e-mail stays light in dark mode
503
528
 
@@ -630,7 +655,7 @@ whole.
630
655
  ### `TS2739: Type '{ … }' is missing the following properties from type 'JanusMailTemplates<…>'`
631
656
 
632
657
  `locales` holds a locale beyond `en` and `fr`, and `templates` does not give
633
- all ten. With five or more missing, the code is `TS2740` and the list ends
658
+ all eleven. With five or more missing, the code is `TS2740` and the list ends
634
659
  `…, and 5 more.` See [Adding a locale](guide/locales.md#adding-a-locale).
635
660
 
636
661
  ### `TS2741: Property 'recoveryCodeUsed' is missing in type '{ … }' but required in type 'JanusMailTemplates<…>'.`
@@ -651,6 +676,32 @@ Add `magicLink` — it is given `brand`, `link` and `expiresIn` — even if you
651
676
  never send it; in JavaScript the same `templates` is
652
677
  [`janusMail: the default templates are built in en and fr only — …; magicLink missing`](#janusmail-the-default-templates-are-built-in-en-and-fr-only--with-another-locale-in-locales-pass-every-template-in-templates-names-missing).
653
678
 
679
+ ### `TS2741: Property 'stepUp' is missing in type '{ … }' but required in type 'JanusMailTemplates<…>'.`
680
+
681
+ `templates` written before 0.8, with a locale beyond `en` and `fr`: since
682
+ 0.8.0 there are eleven templates, and with such a locale every one is
683
+ yours. Add `stepUp` — it is given `brand`, `name`, `code`, `expiresIn` and
684
+ `link` — even if you never send it; in JavaScript the same `templates` is
685
+ [`janusMail: the default templates are built in en and fr only — …; stepUp missing`](#janusmail-the-default-templates-are-built-in-en-and-fr-only--with-another-locale-in-locales-pass-every-template-in-templates-names-missing).
686
+ See [Templates](guide/templates.md#what-each-template-is-given).
687
+
688
+ ### `TS2345: Argument of type 'StepUpByEmail<…> | StepUpByApp<…>' is not assignable to parameter of type 'Pick<StepUpByEmail<unknown>, …>'.`
689
+
690
+ What `auth.stepUp.request(user)` answered, passed to `mail.stepUp` without
691
+ checking `via`, on a user type that may have a second factor. The error
692
+ goes on: `Type 'StepUpByApp<…>' is missing the following properties …`,
693
+ naming `code` and `email` — a user whose factor is active confirms with their app, and
694
+ there is nothing to send. Narrow it first:
695
+
696
+ ```ts
697
+ const issued = await auth.stepUp.request(current.user);
698
+ if (issued.via === 'email') await mail.stepUp(issued, { name: current.user.name });
699
+ ```
700
+
701
+ A sign-in code's answer given to `stepUp` reads `Property 'via' is missing
702
+ in type 'IssuedCode<…>'`: a sign-in code signs in, it confirms no action —
703
+ send it with `signInCode`.
704
+
654
705
  ### `TS2322: Type '"de"' is not assignable to type '"en" | "fr"'.`
655
706
 
656
707
  A `fallbackLocale` outside `locales`. Add it to `locales` — with every
@@ -0,0 +1,52 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en" dir="ltr" xmlns:v="urn:schemas-microsoft-com:vml" xmlns:o="urn:schemas-microsoft-com:office:office" style="color-scheme:light dark;">
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <meta name="x-apple-disable-message-reformatting">
6
+ <meta name="viewport" content="width=device-width, initial-scale=1">
7
+ <meta name="color-scheme" content="light dark">
8
+ <meta name="supported-color-schemes" content="light dark">
9
+ <style>[data-ogsc] .nx-dark-bg-paper{background-color:#040a19!important;}[data-ogsb] .nx-dark-bg-paper{background-color:#040a19!important;}[data-ogsc] .nx-dark-bg-card{background-color:#0f172b!important;background-color:lab(7.78673% 1.82345 -15.0537)!important;}[data-ogsb] .nx-dark-bg-card{background-color:#0f172b!important;background-color:lab(7.78673% 1.82345 -15.0537)!important;}[data-ogsc] .nx-dark-bg-background{background-color:#020918!important;background-color:lab(2.35721% .367686
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
+ [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
+ [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-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
+ @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-bg-muted{background-color:#1e293b!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
+ </style>
19
+ </head>
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;">Enter this code to confirm your request.&#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;
21
+ &#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; &#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;
22
+ &#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; &#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;
23
+ &#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; &#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;
24
+ &#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; &#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;
25
+ &#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; &#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;
26
+ &#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; &#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;
27
+ &#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; &nbsp;</div><span style="display:none">
28
+ <!--[if mso]><xml><o:OfficeDocumentSettings><o:PixelsPerInch>96</o:PixelsPerInch></o:OfficeDocumentSettings><w:WordDocument><w:DontUseAdvancedTypographyReadingMail/></w:WordDocument></xml>
29
+ <![endif]--></span><div role="article" aria-roledescription="email" lang="en" dir="ltr" style="font-size:medium;font-size:max(16px,1rem)">
30
+ <table class="nx-dark-bg-paper" role="presentation" cellpadding="0" cellspacing="0" dir="ltr" style="width:100%;background-color:#f4f4f4;font-family:ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont, 'Segoe UI',Roboto, 'Noto Sans',Ubuntu,Cantarell, 'Helvetica Neue',sans-serif;"><tr><td align="center" style="padding:32px 16px;">
31
+ <!--[if mso]>
32
+ <table role="none" cellpadding="0" cellspacing="0" style="width:600px" align="center"><tr><td>
33
+ <![endif]--><div style="margin:0 auto;max-width:600px;">
34
+ <table role="presentation" cellpadding="0" cellspacing="0" dir="ltr" style="width:100%;"><tr><td style="padding-bottom:24px;text-align:center;"><span class="nx-dark-text-foreground" style="font-size:18px;line-height:28px;font-weight:600;color:#020918;color:lab(2.35721% .367686 -8.51797);">{{ brand }}</span>
35
+ </td></tr><tr><td class="nx-dark-border-border nx-dark-bg-card nx-dark-text-card-foreground" style="border-radius:14px;border-color:#e2e8f0;border-style:solid;border-width:1px;border-color:lab(91.7353% -.998765 -4.76968);background-color:#fff;background-color:lab(100% 0 0);padding:32px;font-size:14px;line-height:20px;color:#020618;color:lab(1.76974% 1.32743 -9.28855);box-shadow:0 0 rgba(0,0,0,0),0 0 rgba(0,0,0,0),0 0 rgba(0,0,0,0),0 0 rgba(0,0,0,0),0 1px 3px rgba(0,0,0,.1),0 1px 2px -1px rgba(0,
36
+ 0,0,.1);"><h1 class="nx-dark-text-foreground" style="margin:0 0 16px;font-size:24px;line-height:32px;font-weight:600;color:#020918;color:lab(2.35721% .367686 -8.51797);">Your confirmation code</h1><p class="nx-dark-text-foreground" style="margin:0 0 16px;font-size:14px;line-height:20px;color:#020918;color:lab(2.35721% .367686 -8.51797);">Hello {{ name }},</p><p class="nx-dark-text-foreground" style="margin:0 0 16px;font-size:14px;line-height:20px;color:#020918;color:lab(2.35721% .367686
37
+ -8.51797);">Someone, we hope you, asked to do something sensitive on your {{ brand }} account. Enter this code to confirm it.</p>
38
+ <table role="presentation" cellpadding="0" cellspacing="0" style="margin-bottom:16px;width:100%;"><tr><td class="nx-dark-bg-muted nx-dark-text-muted-foreground" style="border-radius:10px;background-color:#f1f5f9;background-color:lab(96.286% -.852436 -2.46847);padding:16px 24px;text-align:center;font-family:ui-monospace,Menlo,Consolas,monospace;font-size:30px;line-height:36px;font-weight:600;letter-spacing:.3em;color:#020918;color:lab(2.35721% .367686 -8.51797);">{{ code }}
39
+ </td></tr></table><p class="nx-dark-text-foreground" style="margin:0 0 16px;font-size:12px;line-height:16px;color:#020918;color:lab(2.35721% .367686 -8.51797);">This code expires in {{ expiresIn }}.</p><p class="nx-dark-text-foreground" style="margin:0 0 16px;font-size:14px;line-height:20px;color:#020918;color:lab(2.35721% .367686 -8.51797);">If this was not you, do not share this code: someone may be trying to act on your account. Secure your account now.</p><div><a style="display:inline-block;
40
+ border-radius:9999px;background-color:#27272a;padding:10px 16px;font-family:ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont, 'Segoe UI',Roboto, 'Noto Sans',Ubuntu,Cantarell, 'Helvetica Neue',sans-serif;font-size:14px;line-height:16px;font-weight:500;color:#f8fafc;color:lab(98.1434% -.369519 -1.05966);text-decoration:none;" href="{{ link }}" class="nx-dark-bg-primary nx-dark-text-primary-foreground">
41
+ <!--[if mso]><i style="mso-font-width:114%;mso-text-raise:19px;" hidden>&emsp;</i>
42
+ <![endif]--> <span style="mso-text-raise:10px;">Secure my account</span>
43
+ <!--[if mso]><i style="mso-font-width:114%;" hidden>&emsp;&#8203;</i>
44
+ <![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>
45
+ </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>
46
+ </td></tr></table></div>
47
+ <!--[if mso]>
48
+ </td></tr></table>
49
+ <![endif]-->
50
+ </td></tr></table></div>
51
+ </body>
52
+ </html>