@nxgt/janus-mail 0.7.1 → 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.
- package/README.md +50 -16
- package/dist/generated/mail.d.ts +7 -0
- package/dist/generated/mail.d.ts.map +1 -1
- package/dist/index.d.ts +3 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +30 -1
- package/dist/index.js.map +7 -6
- package/dist/janus-mail.d.ts.map +1 -1
- package/dist/options.d.ts.map +1 -1
- package/dist/step-up-mail.d.ts +5 -0
- package/dist/step-up-mail.d.ts.map +1 -0
- package/dist/template-types.d.ts +13 -2
- package/dist/template-types.d.ts.map +1 -1
- package/dist/templates.d.ts +1 -1
- package/dist/templates.d.ts.map +1 -1
- package/dist/types.d.ts +15 -3
- package/dist/types.d.ts.map +1 -1
- package/docs/README.md +3 -3
- package/docs/guide/building.md +1 -1
- package/docs/guide/locales.md +7 -5
- package/docs/guide/sending.md +44 -5
- package/docs/guide/templates.md +20 -15
- package/docs/roadmap.md +12 -2
- package/docs/troubleshooting.md +62 -11
- package/mails/en/confirm-action.html +52 -0
- package/mails/en/confirm-action.txt +25 -0
- package/mails/fr/confirm-action.html +52 -0
- package/mails/fr/confirm-action.txt +25 -0
- package/mails/mail-manifest.json +26 -0
- package/package.json +5 -5
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
|
|
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" |
|
package/docs/guide/building.md
CHANGED
|
@@ -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
|
|
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
|
package/docs/guide/locales.md
CHANGED
|
@@ -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,
|
|
105
|
-
tenth, since 0.7.0
|
|
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
|
|
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
|
|
134
|
+
- [Templates](templates.md) — writing the eleven for a new locale.
|
package/docs/guide/sending.md
CHANGED
|
@@ -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
|
|
@@ -313,8 +351,8 @@ when opening the session then fails.
|
|
|
313
351
|
|
|
314
352
|
## The expiry
|
|
315
353
|
|
|
316
|
-
`verifyEmail`, `resetPassword`, `signInCode` and `
|
|
317
|
-
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
|
|
318
356
|
10 minutes." The text is `expiresIn`, one of the template's variables,
|
|
319
357
|
derived at send time from the flow's `issued.expiresAt`:
|
|
320
358
|
|
|
@@ -328,7 +366,7 @@ derived at send time from the flow's `issued.expiresAt`:
|
|
|
328
366
|
`unitDisplay: 'long'`, in the recipient's locale: "3 hours", "3 heures".
|
|
329
367
|
|
|
330
368
|
With `@nxgt/janus`'s defaults, a verification link says "1 day", a reset
|
|
331
|
-
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".
|
|
332
370
|
|
|
333
371
|
The time is `janusMail({ clock })`'s — pass the clock `janus({ clock })` was
|
|
334
372
|
given, a `fixedClock` in tests — else the system clock:
|
|
@@ -363,6 +401,7 @@ a `TypeError`, and nothing reaches the mailer: revive the date with
|
|
|
363
401
|
| `resetPassword` | `issued.email` | the `email` passed to `request` |
|
|
364
402
|
| `signInCode` | `issued.email` | the `email` passed to `request` |
|
|
365
403
|
| `magicLink` | `issued.email` | the `email` passed to `request` |
|
|
404
|
+
| `stepUp` | `issued.email` | an address the client sent |
|
|
366
405
|
| `passwordChanged` | `to.email` | — |
|
|
367
406
|
| `emailChanged` | `to.formerEmail` | `to.newEmail` |
|
|
368
407
|
| `twoFactorEnabled`, `twoFactorDisabled` | `to.email` | — |
|
|
@@ -386,7 +425,7 @@ defines no error class and wraps nothing:
|
|
|
386
425
|
| --- | --- | --- | --- |
|
|
387
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 |
|
|
388
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 |
|
|
389
|
-
| `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 |
|
|
390
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) |
|
|
391
430
|
|
|
392
431
|
`instanceof` holds against the classes of your own `@nxgt/mail`, since it is
|
package/docs/guide/templates.md
CHANGED
|
@@ -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
|
|
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
|
|
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
|
|
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`
|
|
99
|
-
and the
|
|
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
|
|
129
|
-
another locale, it takes **all
|
|
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
|
|
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
|
|
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
|
|
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
|
|
198
|
-
*if this was not you, secure your account now*. `
|
|
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 `
|
|
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
|
|
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
|
|
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,
|
package/docs/troubleshooting.md
CHANGED
|
@@ -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
|
|
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
|
|
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.
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
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. ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏
|
|
21
|
+
 ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏
|
|
22
|
+
 ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏
|
|
23
|
+
 ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏
|
|
24
|
+
 ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏
|
|
25
|
+
 ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏
|
|
26
|
+
 ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏
|
|
27
|
+
 ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏ </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> </i>
|
|
42
|
+
<![endif]--> <span style="mso-text-raise:10px;">Secure my account</span>
|
|
43
|
+
<!--[if mso]><i style="mso-font-width:114%;" hidden> ​</i>
|
|
44
|
+
<![endif]--></a></div><div role="separator" style="line-height:24px;">‍</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>
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
Enter this code to confirm your request.
|
|
2
|
+
|
|
3
|
+
{{ brand }}
|
|
4
|
+
|
|
5
|
+
Your confirmation code
|
|
6
|
+
|
|
7
|
+
Hello {{ name }},
|
|
8
|
+
|
|
9
|
+
Someone, we hope you, asked to do something sensitive on your {{ brand }} account. Enter this code to confirm it.
|
|
10
|
+
|
|
11
|
+
{{ code }}
|
|
12
|
+
|
|
13
|
+
This code expires in {{ expiresIn }}.
|
|
14
|
+
|
|
15
|
+
If this was not you, do not share this code: someone may be trying to act on your account. Secure your account now.
|
|
16
|
+
|
|
17
|
+
Secure my account
|
|
18
|
+
|
|
19
|
+
{{ link }}
|
|
20
|
+
|
|
21
|
+
If the button does not work, open this link: {{ link }}
|
|
22
|
+
|
|
23
|
+
You received this e-mail because you have an account with {{ brand }}.
|
|
24
|
+
|
|
25
|
+
{{ brand }}
|