@nxgt/janus-mail 0.7.1 → 0.9.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 +104 -18
- package/dist/generated/mail.d.ts +15 -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 +84 -16
- package/dist/index.js.map +8 -6
- package/dist/janus-mail.d.ts.map +1 -1
- package/dist/link-types.d.ts +51 -0
- package/dist/link-types.d.ts.map +1 -0
- package/dist/new-sign-in-mail.d.ts +16 -0
- package/dist/new-sign-in-mail.d.ts.map +1 -0
- 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 +26 -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 +33 -47
- package/dist/types.d.ts.map +1 -1
- package/docs/README.md +4 -4
- package/docs/guide/building.md +1 -1
- package/docs/guide/locales.md +10 -5
- package/docs/guide/sending.md +91 -5
- package/docs/guide/templates.md +32 -18
- package/docs/roadmap.md +24 -25
- package/docs/troubleshooting.md +100 -14
- package/mails/en/confirm-action.html +52 -0
- package/mails/en/confirm-action.txt +25 -0
- package/mails/en/new-sign-in.html +60 -0
- package/mails/en/new-sign-in.txt +30 -0
- package/mails/fr/confirm-action.html +52 -0
- package/mails/fr/confirm-action.txt +25 -0
- package/mails/fr/new-sign-in.html +58 -0
- package/mails/fr/new-sign-in.txt +30 -0
- package/mails/mail-manifest.json +53 -0
- package/package.json +5 -5
package/docs/guide/locales.md
CHANGED
|
@@ -82,12 +82,15 @@ 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 }),
|
|
88
89
|
twoFactorDisabled: ({ name, link, locale }) => render('two-factor-disabled', locale, { name, link }),
|
|
89
90
|
recoveryCodeUsed: ({ name, when, recoveryCodesLeft, link, locale }) =>
|
|
90
91
|
render('recovery-code-used', locale, { name, when, recoveryCodesLeft, link }),
|
|
92
|
+
newSignIn: ({ name, device, location, time, link, locale }) =>
|
|
93
|
+
render('new-sign-in', locale, { name, device, location, time, link }),
|
|
91
94
|
welcome: ({ name, link, locale }) => render('welcome', locale, { name, link }),
|
|
92
95
|
};
|
|
93
96
|
|
|
@@ -101,8 +104,10 @@ mail.locales; // readonly ('en' | 'fr' | 'de')[]
|
|
|
101
104
|
plural of the codes left in `en` and `fr` only, so a German recipient's
|
|
102
105
|
`mail.recoveryCodeUsed` takes the sentence as text — "Sie haben noch 3
|
|
103
106
|
Wiederherstellungscodes." — and a count is a `TypeError` there.
|
|
104
|
-
`recoveryCodeUsed` is the ninth template, since 0.5.0,
|
|
105
|
-
tenth, since 0.7.0
|
|
107
|
+
`recoveryCodeUsed` is the ninth template, since 0.5.0, `magicLink` the
|
|
108
|
+
tenth, since 0.7.0, `stepUp` the eleventh, since 0.8.0, and `newSignIn` the
|
|
109
|
+
twelfth, since 0.9.0: a `templates`
|
|
110
|
+
written for an earlier version no longer
|
|
106
111
|
compiles with a locale beyond `en` and `fr` — add the one it lacks. A
|
|
107
112
|
renderer over a Maizzle build of your own, with
|
|
108
113
|
`@nxgt/mail-presets` and a `locales/de.json`, is the closest to the defaults.
|
|
@@ -110,13 +115,13 @@ With only some templates, the call does not compile — the error lands on
|
|
|
110
115
|
`templates`:
|
|
111
116
|
|
|
112
117
|
```text
|
|
113
|
-
TS2740: Type '{ verifyEmail: () => Rendered; }' is missing the following properties from type 'JanusMailTemplates<"en" | "fr" | "de">': welcome, resetPassword, signInCode, magicLink, and
|
|
118
|
+
TS2740: Type '{ verifyEmail: () => Rendered; }' is missing the following properties from type 'JanusMailTemplates<"en" | "fr" | "de">': welcome, resetPassword, signInCode, magicLink, and 7 more.
|
|
114
119
|
```
|
|
115
120
|
|
|
116
121
|
and in JavaScript it throws when `janusMail()` is called:
|
|
117
122
|
|
|
118
123
|
```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
|
|
124
|
+
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, newSignIn, welcome missing
|
|
120
125
|
```
|
|
121
126
|
|
|
122
127
|
A default template is typed for `'en' | 'fr'`, so reusing one in a wider set
|
|
@@ -129,4 +134,4 @@ Built-in locales for more languages are on the [roadmap](../roadmap.md).
|
|
|
129
134
|
|
|
130
135
|
- `@nxgt/mail`'s [locales guide](https://github.com/softistx/nxgt-mail/blob/develop/packages/mail/docs/guide/locales.md)
|
|
131
136
|
— `pickLocale` and `parseAcceptLanguage` in full.
|
|
132
|
-
- [Templates](templates.md) — writing the
|
|
137
|
+
- [Templates](templates.md) — writing the twelve 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 — the new sign-in's included — 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
|
|
@@ -311,10 +349,56 @@ when opening the session then fails.
|
|
|
311
349
|
codes; without it, `links.secureAccount()`. `recoveryCodes` is optional,
|
|
312
350
|
so a `links` written for 0.4 still compiles.
|
|
313
351
|
|
|
352
|
+
### `newSignIn(to, { device, time, location? })`
|
|
353
|
+
|
|
354
|
+
```ts
|
|
355
|
+
// From the sign-in's answer, with @nxgt/janus given devices: { keys } and the device cookie:
|
|
356
|
+
const signedIn = await auth.signIn({ email, password }, { device: cookie ?? null });
|
|
357
|
+
if (signedIn.newDevice) {
|
|
358
|
+
const { name, email, locale } = signedIn.user;
|
|
359
|
+
await mail.newSignIn(
|
|
360
|
+
{ name, email, locale },
|
|
361
|
+
{
|
|
362
|
+
device: describeDevice(request.headers.get('user-agent')), // yours: 'Firefox on macOS'
|
|
363
|
+
time: new Intl.DateTimeFormat(locale ?? 'en', {
|
|
364
|
+
dateStyle: 'long',
|
|
365
|
+
timeStyle: 'short',
|
|
366
|
+
timeZone: 'Europe/Paris', // the user's, when you keep it
|
|
367
|
+
}).format(signedIn.session.createdAt),
|
|
368
|
+
location: 'Lyon, France', // optional: from a geo-IP of yours
|
|
369
|
+
},
|
|
370
|
+
);
|
|
371
|
+
}
|
|
372
|
+
```
|
|
373
|
+
|
|
374
|
+
A notice for the account at `to.email`: it was signed in from a device the
|
|
375
|
+
user had not signed in from — which device, where and when — with a
|
|
376
|
+
**Secure my account** button to `links.secureAccount()`. Send it when a
|
|
377
|
+
`@nxgt/janus` 0.17 sign-in answers `newDevice: true`, or on the
|
|
378
|
+
`user.newDeviceSignedIn` event it sends then — the event names the user by
|
|
379
|
+
id alone, so read the name, the locale and the address from the user, and
|
|
380
|
+
it carries no request to describe the device from.
|
|
381
|
+
|
|
382
|
+
- **Every value is your text**, already in the recipient's locale: `device`
|
|
383
|
+
as you describe the request — "Firefox on macOS", "Firefox sur macOS" —
|
|
384
|
+
and `time` formatted in their time zone: "29 septembre 2026 à 09:12".
|
|
385
|
+
The session's `createdAt`, or the event's `occurredAt`, is when it
|
|
386
|
+
happened.
|
|
387
|
+
- **`location` is optional, and yours.** `@nxgt/janus` sees no IP: a
|
|
388
|
+
location comes from a geo-IP of your own, if you have one. Left out, the
|
|
389
|
+
e-mail shows `—` — an em dash, which reads the same in English and
|
|
390
|
+
French — since every line of the summary is always there. `null` is a
|
|
391
|
+
compile error, and in JavaScript `janusMail.newSignIn: location must be a
|
|
392
|
+
string`: spread it in only when you have one,
|
|
393
|
+
`...(city === null ? {} : { location: city })`.
|
|
394
|
+
- **The text part** gives each line of the summary as its label, then its
|
|
395
|
+
value on the next line — `Device`, then `Firefox on macOS` — where every
|
|
396
|
+
other paragraph is one line.
|
|
397
|
+
|
|
314
398
|
## The expiry
|
|
315
399
|
|
|
316
|
-
`verifyEmail`, `resetPassword`, `signInCode` and `
|
|
317
|
-
the link or the code lasts — "This link expires in 1 hour.", "Ce code expire dans
|
|
400
|
+
`verifyEmail`, `resetPassword`, `signInCode`, `magicLink` and `stepUp` say
|
|
401
|
+
how long the link or the code lasts — "This link expires in 1 hour.", "Ce code expire dans
|
|
318
402
|
10 minutes." The text is `expiresIn`, one of the template's variables,
|
|
319
403
|
derived at send time from the flow's `issued.expiresAt`:
|
|
320
404
|
|
|
@@ -328,7 +412,7 @@ derived at send time from the flow's `issued.expiresAt`:
|
|
|
328
412
|
`unitDisplay: 'long'`, in the recipient's locale: "3 hours", "3 heures".
|
|
329
413
|
|
|
330
414
|
With `@nxgt/janus`'s defaults, a verification link says "1 day", a reset
|
|
331
|
-
link "1 hour" and a sign-in code "10 minutes".
|
|
415
|
+
link "1 hour", and a sign-in code and a step-up's code "10 minutes".
|
|
332
416
|
|
|
333
417
|
The time is `janusMail({ clock })`'s — pass the clock `janus({ clock })` was
|
|
334
418
|
given, a `fixedClock` in tests — else the system clock:
|
|
@@ -363,10 +447,12 @@ a `TypeError`, and nothing reaches the mailer: revive the date with
|
|
|
363
447
|
| `resetPassword` | `issued.email` | the `email` passed to `request` |
|
|
364
448
|
| `signInCode` | `issued.email` | the `email` passed to `request` |
|
|
365
449
|
| `magicLink` | `issued.email` | the `email` passed to `request` |
|
|
450
|
+
| `stepUp` | `issued.email` | an address the client sent |
|
|
366
451
|
| `passwordChanged` | `to.email` | — |
|
|
367
452
|
| `emailChanged` | `to.formerEmail` | `to.newEmail` |
|
|
368
453
|
| `twoFactorEnabled`, `twoFactorDisabled` | `to.email` | — |
|
|
369
454
|
| `recoveryCodeUsed` | `to.email` | — |
|
|
455
|
+
| `newSignIn` | `to.email` | — |
|
|
370
456
|
| `welcome` | `to.email` | — |
|
|
371
457
|
|
|
372
458
|
`issued.email` is the address the user's record holds, as they registered
|
|
@@ -386,7 +472,7 @@ defines no error class and wraps nothing:
|
|
|
386
472
|
| --- | --- | --- | --- |
|
|
387
473
|
| `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
474
|
| `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 |
|
|
475
|
+
| `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
476
|
| `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
477
|
|
|
392
478
|
`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 twelve, 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 eleven others stay the defaults.
|
|
26
26
|
|
|
27
27
|
## What a template is
|
|
28
28
|
|
|
@@ -63,19 +63,22 @@ 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)` |
|
|
69
70
|
| `twoFactorDisabled` | `brand`, `name`, `link` | `mail.twoFactorDisabled(to)` |
|
|
70
71
|
| `recoveryCodeUsed` | `brand`, `name`, `when`, `recoveryCodesLeft`, `link` | `mail.recoveryCodeUsed(to, { when, recoveryCodesLeft })` |
|
|
72
|
+
| `newSignIn` | `brand`, `name`, `device`, `location`, `time`, `link` | `mail.newSignIn(to, { device, time, location? })` |
|
|
71
73
|
| `welcome` | `brand`, `name`, `link` | `mail.welcome(to)` |
|
|
72
74
|
|
|
73
75
|
Every one also gets `locale`, one of `locales`. Every value is a string:
|
|
74
76
|
`brand` from the options, `name` from the recipient, `link` from `links` —
|
|
75
|
-
`links.secureAccount()` for the
|
|
77
|
+
`links.secureAccount()` for the notices other than `recoveryCodeUsed` and
|
|
78
|
+
for `stepUp`, `links.recoveryCodes()` (else
|
|
76
79
|
`links.secureAccount()`) for `recoveryCodeUsed`, `links.getStarted()` for
|
|
77
|
-
`welcome` — `code`, `newEmail` and `
|
|
78
|
-
call, and `expiresIn` derived from the flow's `expiresAt` in the locale, or
|
|
80
|
+
`welcome` — `code`, `newEmail`, `when`, `device` and `time` from the
|
|
81
|
+
call, `location` from the call or `—` without one, and `expiresIn` derived from the flow's `expiresAt` in the locale, or
|
|
79
82
|
the send's own `expiresIn` ([Sending](sending.md#the-expiry)).
|
|
80
83
|
|
|
81
84
|
`recoveryCodesLeft` is a **sentence**, not the count: "You have 9 recovery
|
|
@@ -95,8 +98,8 @@ templates: {
|
|
|
95
98
|
// await mail.recoveryCodeUsed(to, { when, recoveryCodesLeft: `Sie haben noch ${left} Wiederherstellungscodes.` });
|
|
96
99
|
```
|
|
97
100
|
|
|
98
|
-
`signInCode`
|
|
99
|
-
and the
|
|
101
|
+
`signInCode` and `stepUp` are **never given the challenge**: it is not in
|
|
102
|
+
their variables, and the methods never read it. An override states the expiry from
|
|
100
103
|
`expiresIn`, already formatted:
|
|
101
104
|
|
|
102
105
|
```ts
|
|
@@ -125,11 +128,12 @@ methods live on its prototype and are refused — see
|
|
|
125
128
|
[troubleshooting](../troubleshooting.md#janusmail-templatesname-is-not-an-own-enumerable-property--pass-a-plain-object-as--name-variables--rendered-).
|
|
126
129
|
|
|
127
130
|
`templates` is `Partial` while `locales` stays within `en` and `fr`: give
|
|
128
|
-
any of the
|
|
129
|
-
another locale, it takes **all
|
|
131
|
+
any of the twelve, and the defaults render the rest. Once `locales` holds
|
|
132
|
+
another locale, it takes **all twelve** — `stepUp` included, since 0.8, and
|
|
133
|
+
`newSignIn`, since 0.9 — see
|
|
130
134
|
[Adding a locale](locales.md#adding-a-locale).
|
|
131
135
|
|
|
132
|
-
`mail.templates` answers the
|
|
136
|
+
`mail.templates` answers the twelve in use — yours, and the defaults for the
|
|
133
137
|
rest — frozen:
|
|
134
138
|
|
|
135
139
|
```ts
|
|
@@ -151,7 +155,7 @@ const { subject, html, text } = await defaults.resetPassword({
|
|
|
151
155
|
});
|
|
152
156
|
```
|
|
153
157
|
|
|
154
|
-
It answers the
|
|
158
|
+
It answers the twelve defaults, typed for `'en' | 'fr'`, without a mailer:
|
|
155
159
|
to preview an e-mail, to wrap one — add a line to the default text, say — or
|
|
156
160
|
to send one through something other than `janusMail()`. A default reused for
|
|
157
161
|
a wider set of locales is a compile error: it could not render the one it
|
|
@@ -177,7 +181,7 @@ them renders: that is when the package's `mails/` folder is read, once.
|
|
|
177
181
|
## What the defaults say
|
|
178
182
|
|
|
179
183
|
The defaults are [`@nxgt/mail-presets`](https://www.npmjs.com/package/@nxgt/mail-presets)
|
|
180
|
-
1.0
|
|
184
|
+
1.1.0, built with a neutral grey theme and no logo:
|
|
181
185
|
|
|
182
186
|
| Template | Built from | Subject (`en`) | Subject (`fr`) |
|
|
183
187
|
| --- | --- | --- | --- |
|
|
@@ -185,25 +189,35 @@ The defaults are [`@nxgt/mail-presets`](https://www.npmjs.com/package/@nxgt/mail
|
|
|
185
189
|
| `resetPassword` | `reset-password` | Reset your password | Réinitialisez votre mot de passe |
|
|
186
190
|
| `signInCode` | `sign-in-code` | Your sign-in code: `042817` | Votre code de connexion : `042817` |
|
|
187
191
|
| `magicLink` | `magic-link` | Your sign-in link | Votre lien de connexion |
|
|
192
|
+
| `stepUp` | `confirm-action` | Your confirmation code | Votre code de confirmation |
|
|
188
193
|
| `passwordChanged` | `password-changed` | Your password was changed | Votre mot de passe a été modifié |
|
|
189
194
|
| `emailChanged` | `email-changed` | Your e-mail address was changed | Votre adresse e-mail a été modifiée |
|
|
190
195
|
| `twoFactorEnabled` | `two-factor-enabled` | Two-factor authentication was turned on | L'authentification à deux facteurs a été activée |
|
|
191
196
|
| `twoFactorDisabled` | `two-factor-disabled` | Two-factor authentication was turned off | L'authentification à deux facteurs a été désactivée |
|
|
192
197
|
| `recoveryCodeUsed` | `recovery-code-used` | A recovery code was used on your account | Un code de récupération a été utilisé sur votre compte |
|
|
198
|
+
| `newSignIn` | `new-sign-in` | New sign-in to your account | Nouvelle connexion à votre compte |
|
|
193
199
|
| `welcome` | `welcome` | Welcome, `Ada` | Bienvenue, `Ada` |
|
|
194
200
|
|
|
195
201
|
Each has a header and a footer with the brand, a heading, a greeting by name
|
|
196
202
|
(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
|
|
198
|
-
*if this was not you, secure your account now*. `
|
|
203
|
+
client that shows no button, and a text part. The notices other than `recoveryCodeUsed` add a warning:
|
|
204
|
+
*if this was not you, secure your account now*. `stepUp` shows its code as
|
|
205
|
+
the sign-in code does, names no action — *someone, we hope you, asked to do
|
|
206
|
+
something sensitive on your account* — and warns not to share the code if
|
|
207
|
+
it was not the user, its button saying **Secure my account**. `recoveryCodeUsed` says it
|
|
199
208
|
too, after a warning banner — *a recovery code was used on your account at
|
|
200
209
|
`when`* — the codes left, and a line on generating new ones; its button says
|
|
201
|
-
**Secure my account**. `
|
|
210
|
+
**Secure my account**. `newSignIn` says the account *was signed in from a
|
|
211
|
+
device we had not seen*, then a summary of three lines — **Device**,
|
|
212
|
+
**Location** and **Time**, *Appareil*, *Lieu* and *Heure* in French — then
|
|
213
|
+
*if this was you, there is nothing to do; if this was not you, secure your
|
|
214
|
+
account now*, its button saying **Secure my account**; in the text part each
|
|
215
|
+
line of the summary is its label, then its value on the next line. `welcome` has the brand in
|
|
202
216
|
its heading — "Welcome to Acme" — and the recipient's name in its subject,
|
|
203
217
|
filled at send time like the body; its button says **Get started**.
|
|
204
218
|
|
|
205
|
-
`verifyEmail`, `resetPassword`, `signInCode` and `
|
|
206
|
-
long the link or the code lasts: "This link expires in 1 hour.", "Ce code expire dans
|
|
219
|
+
`verifyEmail`, `resetPassword`, `signInCode`, `magicLink` and `stepUp` also
|
|
220
|
+
say how long the link or the code lasts: "This link expires in 1 hour.", "Ce code expire dans
|
|
207
221
|
10 minutes." The text part keeps each paragraph on one line.
|
|
208
222
|
|
|
209
223
|
**The HTML follows dark mode.** Each HTML part declares
|
|
@@ -215,7 +229,7 @@ is near-black (`#27272a`) with light text in light mode, and near-white
|
|
|
215
229
|
(`#fafafa`) with near-black text (`#18181b`) in dark mode — 17:1 against the
|
|
216
230
|
dark card, where the light primary would be 1.2:1. The page behind the card
|
|
217
231
|
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
|
|
232
|
+
card, which its border outlines. The code's box — the sign-in code's and the step-up's — is a light grey
|
|
219
233
|
(`#f1f5f9`) with near-black code (`#020918`) in light mode, and a slate a
|
|
220
234
|
step above the dark card (`#1e293b`) with light code (`#cbd5e1`) in dark
|
|
221
235
|
mode, 9.85:1. The muted text — the footer, and the closing *if you did not
|
package/docs/roadmap.md
CHANGED
|
@@ -9,9 +9,8 @@ Nothing between releases.
|
|
|
9
9
|
|
|
10
10
|
## Next
|
|
11
11
|
|
|
12
|
-
- **The
|
|
13
|
-
`@nxgt/
|
|
14
|
-
sign-in from a new device, an invitation to a user type.
|
|
12
|
+
- **The invitation** — `invitation` from `@nxgt/mail-presets`, once
|
|
13
|
+
`@nxgt/janus` has a flow that sends it: an invitation to a user type.
|
|
15
14
|
- **The other presets of `@nxgt/mail-presets` 0.4.0** — `account-deleted`,
|
|
16
15
|
after a user is deleted, once `@nxgt/janus` has a deletion a link can
|
|
17
16
|
undo; and `invitation-accepted`, once it has invitations.
|
|
@@ -40,13 +39,33 @@ Nothing between releases.
|
|
|
40
39
|
its flows answer what to send, and this package is called with that answer.
|
|
41
40
|
An application that sends its own way needs neither this package nor a
|
|
42
41
|
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.
|
|
42
|
+
- **The challenge in an e-mail** — a sign-in code's or a step-up's e-mail
|
|
43
|
+
carries the code and never the challenge, not even through an override.
|
|
45
44
|
|
|
46
45
|
## Shipped
|
|
47
46
|
|
|
48
47
|
Newest first; from the first release on, the package's CHANGELOG holds every one.
|
|
49
48
|
|
|
49
|
+
- **The new sign-in notice, v0.9.0.** `newSignIn(to, { device, time,
|
|
50
|
+
location? })`, built from `@nxgt/mail-presets` 1.1.0's `new-sign-in` in
|
|
51
|
+
English and French — "New sign-in to your account", which device, where
|
|
52
|
+
and when, and a **Secure my account** button to `links.secureAccount()`:
|
|
53
|
+
send it when an `@nxgt/janus` 0.17 sign-in answers `newDevice: true`, or
|
|
54
|
+
on `user.newDeviceSignedIn`. Every value is your text in the recipient's
|
|
55
|
+
locale; `@nxgt/janus` sees no IP, so `location` is yours, and `—` without
|
|
56
|
+
one. Twelve e-mails now, and twelve templates to pass for a locale beyond
|
|
57
|
+
`en` and `fr`.
|
|
58
|
+
|
|
59
|
+
- **The step-up's code, v0.8.0.** `stepUp(issued, to, options?)`, built
|
|
60
|
+
from `@nxgt/mail-presets` 1.1.0's `confirm-action` in English and French:
|
|
61
|
+
send what `@nxgt/janus`'s `auth.stepUp.request(user)` answered, once
|
|
62
|
+
narrowed to `via: 'email'`, to `issued.email` — "Your confirmation code",
|
|
63
|
+
the code, how long it lasts, and a **Secure my account** button to
|
|
64
|
+
`links.secureAccount()` for a user who asked for nothing. A step-up
|
|
65
|
+
confirmed with the user's app sends nothing, and is refused. Eleven
|
|
66
|
+
e-mails now, and eleven templates to pass for a locale beyond `en` and
|
|
67
|
+
`fr`.
|
|
68
|
+
|
|
50
69
|
- **The sign-in link, v0.7.0.** `magicLink(issued, to?, options?)`, built
|
|
51
70
|
from `@nxgt/mail-presets` 1.0.1's `magic-link` in English and French:
|
|
52
71
|
send what `@nxgt/janus` 0.15's `auth.magicLink.request(email)` answered,
|
|
@@ -111,23 +130,3 @@ Newest first; from the first release on, the package's CHANGELOG holds every one
|
|
|
111
130
|
linking to `links.secureAccount()`: send them on `@nxgt/janus` 0.9's
|
|
112
131
|
`user.secondFactorEnabled` and `user.secondFactorDisabled` events. Seven
|
|
113
132
|
e-mails now, and seven templates to pass for a locale beyond `en` and `fr`.
|
|
114
|
-
- **Dark mode, v0.3.0.** The e-mails follow the reader's dark mode in
|
|
115
|
-
every client that supports it, Gmail excepted: a dark page, a dark card and
|
|
116
|
-
light text. Built from `@nxgt/mail-ui` 0.4.0, `@nxgt/mail-presets` 0.4.0
|
|
117
|
-
and `@nxgt/mail-i18n` 0.5.0; the text parts, subjects and variables are
|
|
118
|
-
unchanged, the manifest is still format 1, and the `@nxgt/mail` peer stays
|
|
119
|
-
`>=0.1.0 <1`.
|
|
120
|
-
|
|
121
|
-
- **The expiry in the e-mail, v0.2.0.** The verification, reset and sign-in
|
|
122
|
-
code e-mails say how long the link or code lasts — "1 hour", "1 heure" —
|
|
123
|
-
derived from the flow's `expiresAt` in the recipient's locale, measured
|
|
124
|
-
against `janusMail({ clock })` (the clock given to `janus()`), or given per
|
|
125
|
-
send as `{ expiresIn }`. Two breaks: a template called directly takes
|
|
126
|
-
`expiresIn`, and `signInCode` reads `issued.expiresAt`. Built from
|
|
127
|
-
`@nxgt/mail-presets` 0.2.0, whose text parts keep each paragraph on one
|
|
128
|
-
line.
|
|
129
|
-
|
|
130
|
-
- **The first release, v0.1.0.** The five e-mails of `@nxgt/janus`'s flows,
|
|
131
|
-
in English and French, over any `@nxgt/mail` transport: `janusMail()` and
|
|
132
|
-
`janusTemplates()`. Built with Maizzle when the package is built, shipped
|
|
133
|
-
in `mails/`, and only filled in at send time, every value escaped.
|
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, newSignIn, welcome`](#janusmail-templates-has-no-template-name--name-one-of-verifyemail-resetpassword-signincode-magiclink-stepup-passwordchanged-emailchanged-twofactorenabled-twofactordisabled-recoverycodeused-newsignin-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,10 +69,14 @@ 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
|
+
- [`TS2741: Property 'newSignIn' is missing in type '{ … }' but required in type 'JanusMailTemplates<…>'.`](#ts2741-property-newsignin-is-missing-in-type----but-required-in-type-janusmailtemplates)
|
|
74
|
+
- [`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
75
|
- [`TS2322: Type '"de"' is not assignable to type '"en" | "fr"'.`](#ts2322-type-de-is-not-assignable-to-type-en--fr)
|
|
72
76
|
- [`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
77
|
- [`TS2322: Type 'number | null' is not assignable to type 'string | number'.`](#ts2322-type-number--null-is-not-assignable-to-type-string--number)
|
|
74
78
|
- [`TS2322: Type 'Date' is not assignable to type 'string'.`](#ts2322-type-date-is-not-assignable-to-type-string)
|
|
79
|
+
- [`TS2322: Type 'string | null' is not assignable to type 'string'.`, on `location`](#ts2322-type-string--null-is-not-assignable-to-type-string-on-location) — or `… to type 'string | undefined'.`
|
|
75
80
|
|
|
76
81
|
---
|
|
77
82
|
|
|
@@ -196,9 +201,9 @@ A `fallbackLocale` the list does not hold — `'de'` with the default
|
|
|
196
201
|
|
|
197
202
|
`templates` given as a list or a function. Key each template by its name.
|
|
198
203
|
|
|
199
|
-
### `janusMail: templates has no template <name> — name one of verifyEmail, resetPassword, signInCode, magicLink, passwordChanged, emailChanged, twoFactorEnabled, twoFactorDisabled, recoveryCodeUsed, welcome`
|
|
204
|
+
### `janusMail: templates has no template <name> — name one of verifyEmail, resetPassword, signInCode, magicLink, stepUp, passwordChanged, emailChanged, twoFactorEnabled, twoFactorDisabled, recoveryCodeUsed, newSignIn, welcome`
|
|
200
205
|
|
|
201
|
-
A key that is not one of the
|
|
206
|
+
A key that is not one of the twelve — `invitation`, or the e-mail's file name
|
|
202
207
|
`'verify-email'` rather than the template's, `verifyEmail`. Other e-mails are
|
|
203
208
|
on the [roadmap](roadmap.md); send them with `@nxgt/mail` directly meanwhile.
|
|
204
209
|
|
|
@@ -232,7 +237,7 @@ as it is: fields are own properties.
|
|
|
232
237
|
### `janusMail: the default templates are built in en and fr only — with another locale in locales, pass every template in templates; <names> missing`
|
|
233
238
|
|
|
234
239
|
`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
|
|
240
|
+
out: the defaults could not render them in that locale. Pass all twelve — see
|
|
236
241
|
[Adding a locale](guide/locales.md#adding-a-locale) — or drop the locale.
|
|
237
242
|
The message lists the templates missing.
|
|
238
243
|
|
|
@@ -267,6 +272,26 @@ const mail = janusMail({
|
|
|
267
272
|
|
|
268
273
|
Sending again fails again: it is a bug, not an outage.
|
|
269
274
|
|
|
275
|
+
### `janusMail.stepUp: via must be 'email' — a step-up confirmed with the user's app sends no e-mail`
|
|
276
|
+
|
|
277
|
+
A `TypeError` from `mail.stepUp`, before anything is rendered or sent: it
|
|
278
|
+
was given what `auth.stepUp.request(user)` answered for a user whose second
|
|
279
|
+
factor is active — `via: 'secondFactor'`, with no code and no address — or
|
|
280
|
+
something with no `via` at all, such as a sign-in code's answer. A step-up
|
|
281
|
+
confirmed with the user's app sends nothing: ask for the code their app
|
|
282
|
+
shows. Check `via` first:
|
|
283
|
+
|
|
284
|
+
```ts
|
|
285
|
+
const issued = await auth.stepUp.request(current.user);
|
|
286
|
+
if (issued.via === 'email') {
|
|
287
|
+
await mail.stepUp(issued, { name: current.user.name, locale: current.user.locale });
|
|
288
|
+
}
|
|
289
|
+
return Response.json({ challenge: issued.challenge, via: issued.via });
|
|
290
|
+
```
|
|
291
|
+
|
|
292
|
+
In TypeScript the unchecked call does not compile — see
|
|
293
|
+
[`TS2345: Argument of type 'StepUpByEmail<…> | StepUpByApp<…>' …`](#ts2345-argument-of-type-stepupbyemail--stepupbyapp-is-not-assignable-to-parameter-of-type-pickstepupbyemailunknown-).
|
|
294
|
+
|
|
270
295
|
### `janusMail.<method>: <field> must be a string`
|
|
271
296
|
|
|
272
297
|
A `TypeError` from a call, naming the method and the field it could not read
|
|
@@ -286,7 +311,9 @@ send it with `signInCode`, or ask `auth.magicLink.request` for a link.
|
|
|
286
311
|
|
|
287
312
|
`issued` needs `token` and `email` (`code` and `email` for `signInCode`), and
|
|
288
313
|
the recipient its `name` — a user whose schema has no name passes one
|
|
289
|
-
anyway: `{ name: user.email }`. `janusMail.
|
|
314
|
+
anyway: `{ name: user.email }`. `janusMail.stepUp: name must be a string`
|
|
315
|
+
is `stepUp` called without the recipient: unlike `signInCode`, its e-mail
|
|
316
|
+
greets the user by name. `janusMail.verifyEmail: expiresIn must be a
|
|
290
317
|
string` is the third argument's `expiresIn` given as something else — a
|
|
291
318
|
number of seconds: pass the text to show, `{ expiresIn: '1 hour' }`, or
|
|
292
319
|
leave it out.
|
|
@@ -315,6 +342,14 @@ if (event.type === 'user.emailChanged' && event.formerEmail != null) {
|
|
|
315
342
|
}
|
|
316
343
|
```
|
|
317
344
|
|
|
345
|
+
`janusMail.newSignIn: device must be a string` and `janusMail.newSignIn:
|
|
346
|
+
time must be a string` are the second argument missing, or its `time` given
|
|
347
|
+
as the session's `Date`: pass the device as you describe it and the time as
|
|
348
|
+
text. `janusMail.newSignIn: location must be a string` is a location lookup's
|
|
349
|
+
`null`: leave `location` out, and the e-mail shows `—`. `janusMail.newSignIn:
|
|
350
|
+
email must be a string` is the `user.newDeviceSignedIn` event itself given
|
|
351
|
+
as the recipient — read the user first.
|
|
352
|
+
|
|
318
353
|
`janusMail.recoveryCodeUsed: when must be a string` is the second argument
|
|
319
354
|
missing, or its `when` given as the event's `Date`: pass the text to show,
|
|
320
355
|
formatted in the recipient's locale and time zone —
|
|
@@ -322,8 +357,8 @@ formatted in the recipient's locale and time zone —
|
|
|
322
357
|
|
|
323
358
|
### `janusMail.<method>: expiresAt must be a Date`
|
|
324
359
|
|
|
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
|
|
360
|
+
A `TypeError` from `verifyEmail`, `resetPassword`, `signInCode`,
|
|
361
|
+
`magicLink` or `stepUp`: the e-mail says how long its link or code lasts, from `issued.expiresAt`, and that is
|
|
327
362
|
not a valid `Date`. The usual cause is a flow's answer that went through
|
|
328
363
|
JSON — a job queue, a cache — where a `Date` becomes a string. Nothing is
|
|
329
364
|
sent. Revive it when the job runs, or say the expiry yourself:
|
|
@@ -363,8 +398,8 @@ await mail.recoveryCodeUsed(to, { when, recoveryCodesLeft: `Sie haben noch ${lef
|
|
|
363
398
|
|
|
364
399
|
### `janusMail.<method>: clock.now() must answer a Date`
|
|
365
400
|
|
|
366
|
-
A `TypeError` from `verifyEmail`, `resetPassword`, `signInCode
|
|
367
|
-
`magicLink`: the `clock` given to `janusMail()` has a `now` that answered something else — a
|
|
401
|
+
A `TypeError` from `verifyEmail`, `resetPassword`, `signInCode`,
|
|
402
|
+
`magicLink` or `stepUp`: the `clock` given to `janusMail()` has a `now` that answered something else — a
|
|
368
403
|
number from `Date.now()`, a string, an invalid `Date`. Nothing is sent.
|
|
369
404
|
`clock` is `@nxgt/janus`'s `Clock`: pass the one `janus({ clock })` was
|
|
370
405
|
given, or `{ now: () => new Date() }`.
|
|
@@ -497,7 +532,7 @@ avoids the read altogether. (In this repository: run `bun run build`.)
|
|
|
497
532
|
|
|
498
533
|
The default templates read `mails/` with `node:fs`, through
|
|
499
534
|
`@nxgt/mail/renderer`: Node, Bun or Deno only. On an edge runtime, pass all
|
|
500
|
-
|
|
535
|
+
twelve templates yourself; an inlined build is on the [roadmap](roadmap.md).
|
|
501
536
|
|
|
502
537
|
### An e-mail stays light in dark mode
|
|
503
538
|
|
|
@@ -630,8 +665,9 @@ whole.
|
|
|
630
665
|
### `TS2739: Type '{ … }' is missing the following properties from type 'JanusMailTemplates<…>'`
|
|
631
666
|
|
|
632
667
|
`locales` holds a locale beyond `en` and `fr`, and `templates` does not give
|
|
633
|
-
all
|
|
634
|
-
`…, and
|
|
668
|
+
all twelve. With five or more missing, the code is `TS2740` and the list ends
|
|
669
|
+
`…, and <n> more.` — `…, and 7 more.` for a `templates` holding one
|
|
670
|
+
template. See [Adding a locale](guide/locales.md#adding-a-locale).
|
|
635
671
|
|
|
636
672
|
### `TS2741: Property 'recoveryCodeUsed' is missing in type '{ … }' but required in type 'JanusMailTemplates<…>'.`
|
|
637
673
|
|
|
@@ -651,6 +687,42 @@ Add `magicLink` — it is given `brand`, `link` and `expiresIn` — even if you
|
|
|
651
687
|
never send it; in JavaScript the same `templates` is
|
|
652
688
|
[`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
689
|
|
|
690
|
+
### `TS2741: Property 'stepUp' is missing in type '{ … }' but required in type 'JanusMailTemplates<…>'.`
|
|
691
|
+
|
|
692
|
+
`templates` written before 0.8, with a locale beyond `en` and `fr`: since
|
|
693
|
+
0.8.0 there are eleven templates, and with such a locale every one is
|
|
694
|
+
yours. Add `stepUp` — it is given `brand`, `name`, `code`, `expiresIn` and
|
|
695
|
+
`link` — even if you never send it; in JavaScript the same `templates` is
|
|
696
|
+
[`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).
|
|
697
|
+
See [Templates](guide/templates.md#what-each-template-is-given).
|
|
698
|
+
|
|
699
|
+
### `TS2741: Property 'newSignIn' is missing in type '{ … }' but required in type 'JanusMailTemplates<…>'.`
|
|
700
|
+
|
|
701
|
+
`templates` written before 0.9, with a locale beyond `en` and `fr`: since
|
|
702
|
+
0.9.0 there are twelve templates, and with such a locale every one is
|
|
703
|
+
yours. Add `newSignIn` — it is given `brand`, `name`, `device`, `location`,
|
|
704
|
+
`time` and `link` — even if you never send it; in JavaScript the same
|
|
705
|
+
`templates` is
|
|
706
|
+
[`janusMail: the default templates are built in en and fr only — …; newSignIn missing`](#janusmail-the-default-templates-are-built-in-en-and-fr-only--with-another-locale-in-locales-pass-every-template-in-templates-names-missing).
|
|
707
|
+
See [Templates](guide/templates.md#what-each-template-is-given).
|
|
708
|
+
|
|
709
|
+
### `TS2345: Argument of type 'StepUpByEmail<…> | StepUpByApp<…>' is not assignable to parameter of type 'Pick<StepUpByEmail<unknown>, …>'.`
|
|
710
|
+
|
|
711
|
+
What `auth.stepUp.request(user)` answered, passed to `mail.stepUp` without
|
|
712
|
+
checking `via`, on a user type that may have a second factor. The error
|
|
713
|
+
goes on: `Type 'StepUpByApp<…>' is missing the following properties …`,
|
|
714
|
+
naming `code` and `email` — a user whose factor is active confirms with their app, and
|
|
715
|
+
there is nothing to send. Narrow it first:
|
|
716
|
+
|
|
717
|
+
```ts
|
|
718
|
+
const issued = await auth.stepUp.request(current.user);
|
|
719
|
+
if (issued.via === 'email') await mail.stepUp(issued, { name: current.user.name });
|
|
720
|
+
```
|
|
721
|
+
|
|
722
|
+
A sign-in code's answer given to `stepUp` reads `Property 'via' is missing
|
|
723
|
+
in type 'IssuedCode<…>'`: a sign-in code signs in, it confirms no action —
|
|
724
|
+
send it with `signInCode`.
|
|
725
|
+
|
|
654
726
|
### `TS2322: Type '"de"' is not assignable to type '"en" | "fr"'.`
|
|
655
727
|
|
|
656
728
|
A `fallbackLocale` outside `locales`. Add it to `locales` — with every
|
|
@@ -681,10 +753,24 @@ has no codes to count; check it first, as in
|
|
|
681
753
|
|
|
682
754
|
### `TS2322: Type 'Date' is not assignable to type 'string'.`
|
|
683
755
|
|
|
684
|
-
On `when`: the event's `occurredAt` passed as it is
|
|
685
|
-
|
|
756
|
+
On `when`: the event's `occurredAt` passed as it is — or, on `newSignIn`'s
|
|
757
|
+
`time`, the session's `createdAt`. Both are the text the e-mail shows —
|
|
758
|
+
format the date in the recipient's locale and time zone first:
|
|
686
759
|
|
|
687
760
|
```ts
|
|
688
761
|
const when = new Intl.DateTimeFormat(user.locale, { dateStyle: 'long', timeStyle: 'short', timeZone: 'Europe/Paris' })
|
|
689
762
|
.format(event.occurredAt);
|
|
690
763
|
```
|
|
764
|
+
|
|
765
|
+
### `TS2322: Type 'string | null' is not assignable to type 'string'.`, on `location`
|
|
766
|
+
|
|
767
|
+
Also, without `exactOptionalPropertyTypes` in your tsconfig:
|
|
768
|
+
`Type 'string | null' is not assignable to type 'string | undefined'.`
|
|
769
|
+
|
|
770
|
+
On `newSignIn`'s `location`: a geo-IP lookup's answer, `null` when it found
|
|
771
|
+
nothing, passed as it is. `location` is optional: leave it out, and the
|
|
772
|
+
e-mail shows `—`:
|
|
773
|
+
|
|
774
|
+
```ts
|
|
775
|
+
await mail.newSignIn(to, { device, time, ...(city === null ? {} : { location: city }) });
|
|
776
|
+
```
|