@nxgt/janus-mail 0.5.2 → 0.7.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 +74 -25
- package/dist/expiring-mails.d.ts +2 -2
- package/dist/expiring-mails.d.ts.map +1 -1
- package/dist/generated/mail.d.ts +5 -0
- package/dist/generated/mail.d.ts.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.js +22 -3
- package/dist/index.js.map +6 -6
- package/dist/links.d.ts.map +1 -1
- package/dist/options.d.ts.map +1 -1
- package/dist/template-types.d.ts +8 -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 +18 -1
- 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 -6
- package/docs/guide/sending.md +69 -11
- package/docs/guide/templates.md +10 -8
- package/docs/roadmap.md +18 -2
- package/docs/troubleshooting.md +79 -17
- package/mails/en/magic-link.html +51 -0
- package/mails/en/magic-link.txt +21 -0
- package/mails/fr/magic-link.html +51 -0
- package/mails/fr/magic-link.txt +21 -0
- package/mails/mail-manifest.json +24 -0
- package/package.json +4 -4
package/dist/types.d.ts
CHANGED
|
@@ -54,6 +54,15 @@ export interface JanusMailLinks {
|
|
|
54
54
|
* the codes usually are.
|
|
55
55
|
*/
|
|
56
56
|
readonly recoveryCodes?: () => string;
|
|
57
|
+
/**
|
|
58
|
+
* The page a sign-in link opens, given its one-time token —
|
|
59
|
+
* `magicLink` links to it. **Optional**, for an application that sends no
|
|
60
|
+
* sign-in link; without it, `magicLink` throws a `TypeError` before
|
|
61
|
+
* anything is rendered. That page spends nothing: its button posts the
|
|
62
|
+
* token to the route that calls `auth.magicLink.confirm`, so a mail
|
|
63
|
+
* scanner that opens the link does not sign in for the user.
|
|
64
|
+
*/
|
|
65
|
+
readonly magicLink?: (token: string) => string;
|
|
57
66
|
}
|
|
58
67
|
/** The options every locale set shares. */
|
|
59
68
|
interface JanusMailBaseOptions<L extends string> {
|
|
@@ -78,7 +87,7 @@ interface JanusMailBaseOptions<L extends string> {
|
|
|
78
87
|
readonly clock?: Clock;
|
|
79
88
|
}
|
|
80
89
|
/**
|
|
81
|
-
* What one send of `verifyEmail`, `resetPassword` or `
|
|
90
|
+
* What one send of `verifyEmail`, `resetPassword`, `signInCode` or `magicLink` may pass
|
|
82
91
|
* besides the flow's answer and the recipient.
|
|
83
92
|
*/
|
|
84
93
|
export interface JanusMailSendOptions {
|
|
@@ -120,6 +129,14 @@ export interface JanusMail<L extends string = JanusMailLocale> {
|
|
|
120
129
|
* the e-mail.
|
|
121
130
|
*/
|
|
122
131
|
signInCode(issued: Pick<IssuedCode<unknown>, 'code' | 'email' | 'expiresAt'>, to?: Pick<Recipient, 'locale'>, options?: JanusMailSendOptions): Promise<SentMail>;
|
|
132
|
+
/**
|
|
133
|
+
* The sign-in link `auth.magicLink.request(email)` answered, once checked
|
|
134
|
+
* for `null`, to `issued.email` — its button and its fallback link built
|
|
135
|
+
* by `links.magicLink(issued.token)`, saying how long it stays valid.
|
|
136
|
+
* Greets nobody by name, as a sign-in code does not. A `TypeError`, before
|
|
137
|
+
* anything is rendered, when `janusMail()` was given no `links.magicLink`.
|
|
138
|
+
*/
|
|
139
|
+
magicLink(issued: IssuedToken, to?: Pick<Recipient, 'locale'>, options?: JanusMailSendOptions): Promise<SentMail>;
|
|
123
140
|
/** Tells `to.email` their password was changed, with a link to secure the account. */
|
|
124
141
|
passwordChanged(to: Recipient & {
|
|
125
142
|
readonly email: string;
|
package/dist/types.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AACH,OAAO,KAAK,EAAE,KAAK,EAAE,UAAU,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAClE,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,EAAE,QAAQ,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAC3E,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,qBAAqB,CAAC;AAC3D,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,kBAAkB,CAAC;AAE3D,YAAY,EAAE,eAAe,EAAE,MAAM,qBAAqB,CAAC;AAC3D,YAAY,EACX,iBAAiB,EACjB,qBAAqB,EACrB,kBAAkB,EAClB,kBAAkB,GAClB,MAAM,kBAAkB,CAAC;AAE1B;;;;;GAKG;AACH,MAAM,WAAW,SAAS;IACzB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,MAAM,CAAC,EAAE,aAAa,CAAC;CAChC;AAED;;;;;;;;;;GAUG;AACH,MAAM,WAAW,cAAc;IAC9B,6HAA6H;IAC7H,QAAQ,CAAC,WAAW,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,MAAM,CAAC;IAChD,mEAAmE;IACnE,QAAQ,CAAC,aAAa,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,MAAM,CAAC;IAClD;;;;OAIG;IACH,QAAQ,CAAC,aAAa,EAAE,MAAM,MAAM,CAAC;IACrC;;;;OAIG;IACH,QAAQ,CAAC,UAAU,EAAE,MAAM,MAAM,CAAC;IAClC;;;;;OAKG;IACH,QAAQ,CAAC,aAAa,CAAC,EAAE,MAAM,MAAM,CAAC;
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"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"}
|
package/docs/README.md
CHANGED
|
@@ -5,7 +5,7 @@ the detail.
|
|
|
5
5
|
|
|
6
6
|
| Page | Read it when |
|
|
7
7
|
| --- | --- |
|
|
8
|
-
| [Sending](guide/sending.md) | You are wiring `janusMail()` into your routes: the options, `clock` among them; each method, the welcome on `user.created
|
|
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 |
|
|
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 ten this package sends: `verify-email`, `reset-password`, `sign-in-code`, `magic-link`, `password-changed`, `email-changed`, `two-factor-enabled`, `two-factor-disabled`, `recovery-code-used`, `welcome` — by the name of its built file | "mail", "message" — a *message* is what a mailer sends, addressed |
|
|
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` or `
|
|
30
|
+
| **issued** | What a flow answered: an `IssuedToken` from `verifyEmail.send`, `resetPassword.request` or `magicLink.request`, an `IssuedCode` from `signInCode.request` | "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 ten e-mails were built**: an eleventh 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
|
@@ -81,6 +81,7 @@ const templates: JanusMailTemplates<Locale> = {
|
|
|
81
81
|
verifyEmail: ({ name, link, expiresIn, locale }) => render('verify-email', locale, { name, link, expiresIn }),
|
|
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
|
+
magicLink: ({ link, expiresIn, locale }) => render('magic-link', locale, { link, expiresIn }),
|
|
84
85
|
passwordChanged: ({ name, link, locale }) => render('password-changed', locale, { name, link }),
|
|
85
86
|
emailChanged: ({ name, link, newEmail, locale }) => render('email-changed', locale, { name, link, newEmail }),
|
|
86
87
|
twoFactorEnabled: ({ name, link, locale }) => render('two-factor-enabled', locale, { name, link }),
|
|
@@ -100,22 +101,22 @@ mail.locales; // readonly ('en' | 'fr' | 'de')[]
|
|
|
100
101
|
plural of the codes left in `en` and `fr` only, so a German recipient's
|
|
101
102
|
`mail.recoveryCodeUsed` takes the sentence as text — "Sie haben noch 3
|
|
102
103
|
Wiederherstellungscodes." — and a count is a `TypeError` there.
|
|
103
|
-
`recoveryCodeUsed` is the ninth template, since 0.5.0
|
|
104
|
-
|
|
105
|
-
`fr
|
|
104
|
+
`recoveryCodeUsed` is the ninth template, since 0.5.0, and `magicLink` the
|
|
105
|
+
tenth, since 0.7.0: a `templates` written for an earlier version no longer
|
|
106
|
+
compiles with a locale beyond `en` and `fr` — add the one it lacks. A
|
|
106
107
|
renderer over a Maizzle build of your own, with
|
|
107
108
|
`@nxgt/mail-presets` and a `locales/de.json`, is the closest to the defaults.
|
|
108
109
|
With only some templates, the call does not compile — the error lands on
|
|
109
110
|
`templates`:
|
|
110
111
|
|
|
111
112
|
```text
|
|
112
|
-
TS2740: Type '{ verifyEmail: () => Rendered; }' is missing the following properties from type 'JanusMailTemplates<"en" | "fr" | "de">': resetPassword, signInCode,
|
|
113
|
+
TS2740: Type '{ verifyEmail: () => Rendered; }' is missing the following properties from type 'JanusMailTemplates<"en" | "fr" | "de">': welcome, resetPassword, signInCode, magicLink, and 5 more.
|
|
113
114
|
```
|
|
114
115
|
|
|
115
116
|
and in JavaScript it throws when `janusMail()` is called:
|
|
116
117
|
|
|
117
118
|
```text
|
|
118
|
-
TypeError: janusMail: the default templates are built in en and fr only — with another locale in locales, pass every template in templates; resetPassword, signInCode, passwordChanged, emailChanged, twoFactorEnabled, twoFactorDisabled, recoveryCodeUsed, welcome missing
|
|
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
|
|
119
120
|
```
|
|
120
121
|
|
|
121
122
|
A default template is typed for `'en' | 'fr'`, so reusing one in a wider set
|
|
@@ -128,4 +129,4 @@ Built-in locales for more languages are on the [roadmap](../roadmap.md).
|
|
|
128
129
|
|
|
129
130
|
- `@nxgt/mail`'s [locales guide](https://github.com/softistx/nxgt-mail/blob/develop/packages/mail/docs/guide/locales.md)
|
|
130
131
|
— `pickLocale` and `parseAcceptLanguage` in full.
|
|
131
|
-
- [Templates](templates.md) — writing the
|
|
132
|
+
- [Templates](templates.md) — writing the ten for a new locale.
|
package/docs/guide/sending.md
CHANGED
|
@@ -48,6 +48,7 @@ time one is sent, and kept.
|
|
|
48
48
|
| `links.resetPassword` | `(token) => string` | required | The page that sets a new password, given the one-time token |
|
|
49
49
|
| `links.secureAccount` | `() => string` | required | Where a user who made no change secures their account: the notices link to it |
|
|
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
|
+
| `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` |
|
|
51
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 |
|
|
52
53
|
| `locales` | `readonly L[]` | `['en', 'fr']` | The locales sent in — see [Locales](locales.md) |
|
|
53
54
|
| `fallbackLocale` | one of `locales` | `'en'`, else the first of `locales` | The locale when the recipient wants none of `locales` |
|
|
@@ -114,29 +115,85 @@ challenge is the visitor's secret, and neither the default template nor an
|
|
|
114
115
|
override is ever given it. `to` is optional — the e-mail greets nobody by
|
|
115
116
|
name — and only its `locale` is read.
|
|
116
117
|
|
|
118
|
+
### `magicLink(issued, to?, options?)`
|
|
119
|
+
|
|
120
|
+
```ts
|
|
121
|
+
const issued = await auth.magicLink.request(email); // IssuedToken & { user } | null
|
|
122
|
+
if (issued !== null) await mail.magicLink(issued, { locale: issued.user.locale });
|
|
123
|
+
return new Response(null, { status: 202 }); // the same answer either way
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
`request` answers `null` for an address nobody holds, and for an inactive
|
|
127
|
+
user; the compiler refuses `null` here. Sent to `issued.email`, with a
|
|
128
|
+
**Sign in** button to `links.magicLink(issued.token)` and how long it lasts —
|
|
129
|
+
"This link expires in 15 minutes." `to` is optional — the e-mail greets
|
|
130
|
+
nobody by name — and only its `locale` is read.
|
|
131
|
+
|
|
132
|
+
- **`links.magicLink` is required to send it.** It is optional in
|
|
133
|
+
`janusMail()`, for an application that sends no sign-in link, and without
|
|
134
|
+
it `magicLink` throws a `TypeError` —
|
|
135
|
+
`janusMail.magicLink: links.magicLink is missing — …` — before anything is
|
|
136
|
+
rendered. There is no fallback: only your page can carry the token.
|
|
137
|
+
- **Link to a page, never to the route that signs in.** Mail scanners open
|
|
138
|
+
every link in an e-mail; one that signs in on `GET` signs in for the
|
|
139
|
+
scanner, and the user's click answers `TOKEN_SPENT`. The page's button
|
|
140
|
+
posts the token to the route calling `auth.magicLink.confirm` —
|
|
141
|
+
[`@nxgt/janus`'s sign-in link guide](https://github.com/softistx/nxgt-janus/blob/develop/packages/janus/docs/guide/magic-link.md)
|
|
142
|
+
has it, and [`@nxgt/janus-hono`'s routes](https://github.com/softistx/nxgt-janus/blob/develop/packages/janus-hono/docs/guide/routes.md#a-link-sent-by-e-mail)
|
|
143
|
+
the same on Hono.
|
|
144
|
+
- **A sign-in code is not a link.** Its answer has a `challenge`, the
|
|
145
|
+
visitor's secret, and no `token`: the compiler refuses it here, and
|
|
146
|
+
JavaScript gets `janusMail.magicLink: token must be a string`.
|
|
147
|
+
|
|
117
148
|
### `passwordChanged(to)`
|
|
118
149
|
|
|
119
150
|
```ts
|
|
120
|
-
|
|
121
|
-
|
|
151
|
+
// On the user event @nxgt/janus 0.14 sends once the password is written:
|
|
152
|
+
const auth = janus({
|
|
153
|
+
...config,
|
|
154
|
+
async events(event) {
|
|
155
|
+
if (event.type !== 'user.passwordChanged') return;
|
|
156
|
+
const user = await auth.get(event.userId);
|
|
157
|
+
await mail.passwordChanged({ name: user.name, locale: user.locale, email: user.email });
|
|
158
|
+
},
|
|
159
|
+
});
|
|
122
160
|
```
|
|
123
161
|
|
|
124
162
|
A notice: the password of the account at `to.email` changed, with a link to
|
|
125
|
-
`links.secureAccount()` for a user who did not change it. Send it
|
|
126
|
-
`
|
|
163
|
+
`links.secureAccount()` for a user who did not change it. Send it on the
|
|
164
|
+
`user.passwordChanged` event, which `changePassword` and `setPassword` send.
|
|
165
|
+
A reset sends `user.passwordReset` instead, never both: to tell a reset too,
|
|
166
|
+
send the same notice on that type — `event.type === 'user.passwordChanged'
|
|
167
|
+
|| event.type === 'user.passwordReset'`. The event names the user by id
|
|
168
|
+
alone, so read the name, the locale and the address from the user.
|
|
169
|
+
`setPassword` sends it for a user's first password too — an account created
|
|
170
|
+
without one — so the notice then tells them a password was set on their
|
|
171
|
+
account, which is worth knowing all the same.
|
|
127
172
|
|
|
128
173
|
### `emailChanged(to)`
|
|
129
174
|
|
|
130
175
|
```ts
|
|
131
|
-
|
|
132
|
-
const
|
|
133
|
-
|
|
176
|
+
// On the user event @nxgt/janus 0.14 sends when an update changed the e-mail:
|
|
177
|
+
const auth = janus({
|
|
178
|
+
...config,
|
|
179
|
+
async events(event) {
|
|
180
|
+
if (event.type !== 'user.emailChanged' || event.formerEmail == null) return;
|
|
181
|
+
const user = await auth.get(event.userId);
|
|
182
|
+
await mail.emailChanged({ name: user.name, locale: user.locale, formerEmail: event.formerEmail, newEmail: user.email });
|
|
183
|
+
},
|
|
184
|
+
});
|
|
134
185
|
```
|
|
135
186
|
|
|
136
187
|
Sent to **`formerEmail`**: the owner of the old address is the one to warn,
|
|
137
188
|
since whoever changed it already controls the new one. It names `newEmail`,
|
|
138
|
-
and links to `links.secureAccount()`.
|
|
139
|
-
|
|
189
|
+
and links to `links.secureAccount()`. The `user.emailChanged` event is the
|
|
190
|
+
one that carries an address — `event.formerEmail`, the e-mail before the
|
|
191
|
+
update — since the user read back already holds the new one. It is `null`
|
|
192
|
+
for a user who had no e-mail before: there is no former inbox to tell, and
|
|
193
|
+
the type refuses it unchecked. The example assumes a required e-mail: with
|
|
194
|
+
an optional one, `update` also sends the event when it removes the address,
|
|
195
|
+
and the user read back has no `newEmail` to name — check `user.email` first,
|
|
196
|
+
and tell the former address your own way or not at all.
|
|
140
197
|
|
|
141
198
|
### `twoFactorEnabled(to)` and `twoFactorDisabled(to)`
|
|
142
199
|
|
|
@@ -250,8 +307,8 @@ when opening the session then fails.
|
|
|
250
307
|
|
|
251
308
|
## The expiry
|
|
252
309
|
|
|
253
|
-
`verifyEmail`, `resetPassword` and `
|
|
254
|
-
code lasts — "This link expires in 1 hour.", "Ce code expire dans
|
|
310
|
+
`verifyEmail`, `resetPassword`, `signInCode` and `magicLink` say how long
|
|
311
|
+
the link or the code lasts — "This link expires in 1 hour.", "Ce code expire dans
|
|
255
312
|
10 minutes." The text is `expiresIn`, one of the template's variables,
|
|
256
313
|
derived at send time from the flow's `issued.expiresAt`:
|
|
257
314
|
|
|
@@ -299,6 +356,7 @@ a `TypeError`, and nothing reaches the mailer: revive the date with
|
|
|
299
356
|
| `verifyEmail` | `issued.email` | what the visitor typed |
|
|
300
357
|
| `resetPassword` | `issued.email` | the `email` passed to `request` |
|
|
301
358
|
| `signInCode` | `issued.email` | the `email` passed to `request` |
|
|
359
|
+
| `magicLink` | `issued.email` | the `email` passed to `request` |
|
|
302
360
|
| `passwordChanged` | `to.email` | — |
|
|
303
361
|
| `emailChanged` | `to.formerEmail` | `to.newEmail` |
|
|
304
362
|
| `twoFactorEnabled`, `twoFactorDisabled` | `to.email` | — |
|
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 ten, using the defaults on their
|
|
5
5
|
own, and what the defaults say.
|
|
6
6
|
|
|
7
7
|
```ts
|
|
@@ -62,6 +62,7 @@ e-mail.
|
|
|
62
62
|
| `verifyEmail` | `brand`, `name`, `link`, `expiresIn` | `mail.verifyEmail(issued, to, options?)` |
|
|
63
63
|
| `resetPassword` | `brand`, `name`, `link`, `expiresIn` | `mail.resetPassword(issued, to, options?)` |
|
|
64
64
|
| `signInCode` | `brand`, `code`, `expiresIn` | `mail.signInCode(issued, to?, options?)` |
|
|
65
|
+
| `magicLink` | `brand`, `link`, `expiresIn` | `mail.magicLink(issued, to?, options?)` |
|
|
65
66
|
| `passwordChanged` | `brand`, `name`, `link` | `mail.passwordChanged(to)` |
|
|
66
67
|
| `emailChanged` | `brand`, `name`, `link`, `newEmail` | `mail.emailChanged(to)` |
|
|
67
68
|
| `twoFactorEnabled` | `brand`, `name`, `link` | `mail.twoFactorEnabled(to)` |
|
|
@@ -124,11 +125,11 @@ methods live on its prototype and are refused — see
|
|
|
124
125
|
[troubleshooting](../troubleshooting.md#janusmail-templatesname-is-not-an-own-enumerable-property--pass-a-plain-object-as--name-variables--rendered-).
|
|
125
126
|
|
|
126
127
|
`templates` is `Partial` while `locales` stays within `en` and `fr`: give
|
|
127
|
-
any of the
|
|
128
|
-
another locale, it takes **all
|
|
128
|
+
any of the ten, and the defaults render the rest. Once `locales` holds
|
|
129
|
+
another locale, it takes **all ten** — see
|
|
129
130
|
[Adding a locale](locales.md#adding-a-locale).
|
|
130
131
|
|
|
131
|
-
`mail.templates` answers the
|
|
132
|
+
`mail.templates` answers the ten in use — yours, and the defaults for the
|
|
132
133
|
rest — frozen:
|
|
133
134
|
|
|
134
135
|
```ts
|
|
@@ -150,7 +151,7 @@ const { subject, html, text } = await defaults.resetPassword({
|
|
|
150
151
|
});
|
|
151
152
|
```
|
|
152
153
|
|
|
153
|
-
It answers the
|
|
154
|
+
It answers the ten defaults, typed for `'en' | 'fr'`, without a mailer:
|
|
154
155
|
to preview an e-mail, to wrap one — add a line to the default text, say — or
|
|
155
156
|
to send one through something other than `janusMail()`. A default reused for
|
|
156
157
|
a wider set of locales is a compile error: it could not render the one it
|
|
@@ -183,6 +184,7 @@ The defaults are [`@nxgt/mail-presets`](https://www.npmjs.com/package/@nxgt/mail
|
|
|
183
184
|
| `verifyEmail` | `verify-email` | Confirm your e-mail address | Confirmez votre adresse e-mail |
|
|
184
185
|
| `resetPassword` | `reset-password` | Reset your password | Réinitialisez votre mot de passe |
|
|
185
186
|
| `signInCode` | `sign-in-code` | Your sign-in code: `042817` | Votre code de connexion : `042817` |
|
|
187
|
+
| `magicLink` | `magic-link` | Your sign-in link | Votre lien de connexion |
|
|
186
188
|
| `passwordChanged` | `password-changed` | Your password was changed | Votre mot de passe a été modifié |
|
|
187
189
|
| `emailChanged` | `email-changed` | Your e-mail address was changed | Votre adresse e-mail a été modifiée |
|
|
188
190
|
| `twoFactorEnabled` | `two-factor-enabled` | Two-factor authentication was turned on | L'authentification à deux facteurs a été activée |
|
|
@@ -191,7 +193,7 @@ The defaults are [`@nxgt/mail-presets`](https://www.npmjs.com/package/@nxgt/mail
|
|
|
191
193
|
| `welcome` | `welcome` | Welcome, `Ada` | Bienvenue, `Ada` |
|
|
192
194
|
|
|
193
195
|
Each has a header and a footer with the brand, a heading, a greeting by name
|
|
194
|
-
(but `signInCode`), a button with its link and the link again in text for a
|
|
196
|
+
(but `signInCode` and `magicLink`), a button with its link and the link again in text for a
|
|
195
197
|
client that shows no button, and a text part. The other four notices add a warning:
|
|
196
198
|
*if this was not you, secure your account now*. `recoveryCodeUsed` says it
|
|
197
199
|
too, after a warning banner — *a recovery code was used on your account at
|
|
@@ -200,8 +202,8 @@ too, after a warning banner — *a recovery code was used on your account at
|
|
|
200
202
|
its heading — "Welcome to Acme" — and the recipient's name in its subject,
|
|
201
203
|
filled at send time like the body; its button says **Get started**.
|
|
202
204
|
|
|
203
|
-
`verifyEmail`, `resetPassword` and `
|
|
204
|
-
or the code lasts: "This link expires in 1 hour.", "Ce code expire dans
|
|
205
|
+
`verifyEmail`, `resetPassword`, `signInCode` and `magicLink` also say how
|
|
206
|
+
long the link or the code lasts: "This link expires in 1 hour.", "Ce code expire dans
|
|
205
207
|
10 minutes." The text part keeps each paragraph on one line.
|
|
206
208
|
|
|
207
209
|
**The HTML follows dark mode.** Each HTML part declares
|
package/docs/roadmap.md
CHANGED
|
@@ -9,9 +9,9 @@ Nothing between releases.
|
|
|
9
9
|
|
|
10
10
|
## Next
|
|
11
11
|
|
|
12
|
-
- **The other presets** — `
|
|
12
|
+
- **The other presets** — `new-sign-in` and `invitation` from
|
|
13
13
|
`@nxgt/mail-presets`, each once `@nxgt/janus` has a flow that sends it: a
|
|
14
|
-
sign-in
|
|
14
|
+
sign-in from a new device, an invitation to a user type.
|
|
15
15
|
- **The other presets of `@nxgt/mail-presets` 0.4.0** — `account-deleted`,
|
|
16
16
|
after a user is deleted, once `@nxgt/janus` has a deletion a link can
|
|
17
17
|
undo; and `invitation-accepted`, once it has invitations.
|
|
@@ -47,6 +47,22 @@ Nothing between releases.
|
|
|
47
47
|
|
|
48
48
|
Newest first; from the first release on, the package's CHANGELOG holds every one.
|
|
49
49
|
|
|
50
|
+
- **The sign-in link, v0.7.0.** `magicLink(issued, to?, options?)`, built
|
|
51
|
+
from `@nxgt/mail-presets` 1.0.1's `magic-link` in English and French:
|
|
52
|
+
send what `@nxgt/janus` 0.15's `auth.magicLink.request(email)` answered,
|
|
53
|
+
to `issued.email`, its button built by the new, optional
|
|
54
|
+
`links.magicLink(token)` and its expiry read from `issued.expiresAt` —
|
|
55
|
+
"This link expires in 15 minutes." Without `links.magicLink`, the send is
|
|
56
|
+
a `TypeError`. Ten e-mails now, and ten templates to pass for a locale
|
|
57
|
+
beyond `en` and `fr`.
|
|
58
|
+
|
|
59
|
+
- **The change notices on their events, v0.6.0.** `passwordChanged` and
|
|
60
|
+
`emailChanged` are sent from `@nxgt/janus` 0.14's `user.passwordChanged`
|
|
61
|
+
and `user.emailChanged` events, as the two-factor notices are, rather than
|
|
62
|
+
after the call: whoever changed the password or the e-mail, the user is
|
|
63
|
+
told. `emailChanged` goes to the event's `formerEmail`, the inbox the
|
|
64
|
+
account just left, and the compiler refuses it unchecked for `null`.
|
|
65
|
+
|
|
50
66
|
- **The recovery code notice, and a readable link in both modes, v0.5.0.**
|
|
51
67
|
`recoveryCodeUsed(to, { when, recoveryCodesLeft })`, built from
|
|
52
68
|
`@nxgt/mail-presets` 1.0.0's `recovery-code-used` in English and French:
|
package/docs/troubleshooting.md
CHANGED
|
@@ -28,18 +28,20 @@ How the messages are shaped:
|
|
|
28
28
|
- [`janusMail: links must be an object, as { verifyEmail, resetPassword, secureAccount, getStarted }`](#janusmail-links-must-be-an-object-as--verifyemail-resetpassword-secureaccount-getstarted-)
|
|
29
29
|
- [`janusMail: links.<name> must be a function`](#janusmail-linksname-must-be-a-function)
|
|
30
30
|
- [`janusMail: links.recoveryCodes must be a function, or left out`](#janusmail-linksrecoverycodes-must-be-a-function-or-left-out)
|
|
31
|
+
- [`janusMail: links.magicLink must be a function, or left out`](#janusmail-linksmagiclink-must-be-a-function-or-left-out)
|
|
31
32
|
- [`janusMail: locales must list at least one locale, as ['en', 'fr']`](#janusmail-locales-must-list-at-least-one-locale-as-en-fr)
|
|
32
33
|
- [`janusMail: locales must be BCP 47 language tags, as 'fr-CA'`](#janusmail-locales-must-be-bcp-47-language-tags-as-fr-ca)
|
|
33
34
|
- [`janusMail: locales holds the same locale twice`](#janusmail-locales-holds-the-same-locale-twice)
|
|
34
35
|
- [`janusMail: fallbackLocale must be one of locales`](#janusmail-fallbacklocale-must-be-one-of-locales)
|
|
35
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-)
|
|
36
|
-
- [`janusMail: templates has no template <name> — name one of verifyEmail, resetPassword, signInCode, passwordChanged, emailChanged, twoFactorEnabled, twoFactorDisabled, recoveryCodeUsed, welcome`](#janusmail-templates-has-no-template-name--name-one-of-verifyemail-resetpassword-signincode-passwordchanged-emailchanged-twofactorenabled-twofactordisabled-recoverycodeused-welcome)
|
|
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
38
|
- [`janusMail: templates.<name> must be a function`](#janusmail-templatesname-must-be-a-function)
|
|
38
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-)
|
|
39
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)
|
|
40
41
|
- [`janusMail: the default templates are built in en and fr only — with another locale in locales, pass every template in templates; <names> missing`](#janusmail-the-default-templates-are-built-in-en-and-fr-only--with-another-locale-in-locales-pass-every-template-in-templates-names-missing)
|
|
41
42
|
|
|
42
43
|
**Sending**
|
|
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)
|
|
43
45
|
- [`janusMail.<method>: <field> must be a string`](#janusmailmethod-field-must-be-a-string)
|
|
44
46
|
- [`janusMail.<method>: expiresAt must be a Date`](#janusmailmethod-expiresat-must-be-a-date)
|
|
45
47
|
- [`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)
|
|
@@ -65,6 +67,7 @@ How the messages are shaped:
|
|
|
65
67
|
- [`TS2345: … Types of property 'expiresAt' are incompatible. Type 'string' is not assignable to type 'Date'.`](#ts2345--types-of-property-expiresat-are-incompatible-type-string-is-not-assignable-to-type-date)
|
|
66
68
|
- [`TS2739: Type '{ … }' is missing the following properties from type 'JanusMailTemplates<…>'`](#ts2739-type----is-missing-the-following-properties-from-type-janusmailtemplates)
|
|
67
69
|
- [`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
|
+
- [`TS2741: Property 'magicLink' is missing in type '{ … }' but required in type 'JanusMailTemplates<…>'.`](#ts2741-property-magiclink-is-missing-in-type----but-required-in-type-janusmailtemplates)
|
|
68
71
|
- [`TS2322: Type '"de"' is not assignable to type '"en" | "fr"'.`](#ts2322-type-de-is-not-assignable-to-type-en--fr)
|
|
69
72
|
- [`TS2741: Property 'getStarted' is missing in type '{ … }' but required in type 'JanusMailLinks'.`](#ts2741-property-getstarted-is-missing-in-type----but-required-in-type-janusmaillinks)
|
|
70
73
|
- [`TS2322: Type 'number | null' is not assignable to type 'string | number'.`](#ts2322-type-number--null-is-not-assignable-to-type-string--number)
|
|
@@ -114,7 +117,8 @@ janusMail({ mailer, from, brand: 'Acme', links });
|
|
|
114
117
|
### `janusMail: links must be an object, as { verifyEmail, resetPassword, secureAccount, getStarted }`
|
|
115
118
|
|
|
116
119
|
No `links`. Four are required: the notices link to `secureAccount`, and
|
|
117
|
-
the welcome to `getStarted`.
|
|
120
|
+
the welcome to `getStarted`. Two more, `recoveryCodes` and `magicLink`, are
|
|
121
|
+
optional.
|
|
118
122
|
|
|
119
123
|
### `janusMail: links.<name> must be a function`
|
|
120
124
|
|
|
@@ -148,6 +152,18 @@ regenerates their codes:
|
|
|
148
152
|
links: { …, recoveryCodes: () => 'https://acme.example/account/recovery-codes' },
|
|
149
153
|
```
|
|
150
154
|
|
|
155
|
+
### `janusMail: links.magicLink must be a function, or left out`
|
|
156
|
+
|
|
157
|
+
`links.magicLink` given as a URL, or as anything but a function. It is
|
|
158
|
+
optional — an application that sends no sign-in link leaves it out — but
|
|
159
|
+
when given it takes the link's one-time token and answers **a page of
|
|
160
|
+
yours that spends nothing**, whose button posts the token to the route
|
|
161
|
+
calling `auth.magicLink.confirm`:
|
|
162
|
+
|
|
163
|
+
```ts
|
|
164
|
+
links: { …, magicLink: (token) => `https://acme.example/sign-in/link?token=${token}` },
|
|
165
|
+
```
|
|
166
|
+
|
|
151
167
|
### `janusMail: locales must list at least one locale, as ['en', 'fr']`
|
|
152
168
|
|
|
153
169
|
`locales: []`, `locales: 'en'`, or a list holding something that is not a
|
|
@@ -180,9 +196,9 @@ A `fallbackLocale` the list does not hold — `'de'` with the default
|
|
|
180
196
|
|
|
181
197
|
`templates` given as a list or a function. Key each template by its name.
|
|
182
198
|
|
|
183
|
-
### `janusMail: templates has no template <name> — name one of verifyEmail, resetPassword, signInCode, passwordChanged, emailChanged, twoFactorEnabled, twoFactorDisabled, recoveryCodeUsed, welcome`
|
|
199
|
+
### `janusMail: templates has no template <name> — name one of verifyEmail, resetPassword, signInCode, magicLink, passwordChanged, emailChanged, twoFactorEnabled, twoFactorDisabled, recoveryCodeUsed, welcome`
|
|
184
200
|
|
|
185
|
-
A key that is not one of the
|
|
201
|
+
A key that is not one of the ten — `newSignIn`, or the e-mail's file name
|
|
186
202
|
`'verify-email'` rather than the template's, `verifyEmail`. Other e-mails are
|
|
187
203
|
on the [roadmap](roadmap.md); send them with `@nxgt/mail` directly meanwhile.
|
|
188
204
|
|
|
@@ -216,7 +232,7 @@ as it is: fields are own properties.
|
|
|
216
232
|
### `janusMail: the default templates are built in en and fr only — with another locale in locales, pass every template in templates; <names> missing`
|
|
217
233
|
|
|
218
234
|
`locales` holds a locale beyond `en` and `fr`, and `templates` leaves some
|
|
219
|
-
out: the defaults could not render them in that locale. Pass all
|
|
235
|
+
out: the defaults could not render them in that locale. Pass all ten — see
|
|
220
236
|
[Adding a locale](guide/locales.md#adding-a-locale) — or drop the locale.
|
|
221
237
|
The message lists the templates missing.
|
|
222
238
|
|
|
@@ -234,6 +250,23 @@ janusMail({ mailer, from, brand: 'Acme', links, clock });
|
|
|
234
250
|
|
|
235
251
|
## Sending
|
|
236
252
|
|
|
253
|
+
### `janusMail.magicLink: links.magicLink is missing — pass it to janusMail({ links }) to send sign-in links`
|
|
254
|
+
|
|
255
|
+
A `TypeError` from `mail.magicLink`, before anything is rendered or sent:
|
|
256
|
+
`janusMail()` was given no `links.magicLink`. It is optional, so
|
|
257
|
+
`janusMail()` accepts `links` without it — written before 0.7, or for an
|
|
258
|
+
application that sent no sign-in link — and there is no page to fall back
|
|
259
|
+
on: only yours can carry the token. Add it:
|
|
260
|
+
|
|
261
|
+
```ts
|
|
262
|
+
const mail = janusMail({
|
|
263
|
+
…,
|
|
264
|
+
links: { …, magicLink: (token) => `https://acme.example/sign-in/link?token=${token}` },
|
|
265
|
+
});
|
|
266
|
+
```
|
|
267
|
+
|
|
268
|
+
Sending again fails again: it is a bug, not an outage.
|
|
269
|
+
|
|
237
270
|
### `janusMail.<method>: <field> must be a string`
|
|
238
271
|
|
|
239
272
|
A `TypeError` from a call, naming the method and the field it could not read
|
|
@@ -247,6 +280,10 @@ const issued = await auth.resetPassword.request(email);
|
|
|
247
280
|
if (issued !== null) await mail.resetPassword(issued, { name: issued.user.name });
|
|
248
281
|
```
|
|
249
282
|
|
|
283
|
+
`janusMail.magicLink: token must be a string` is a sign-in code's answer
|
|
284
|
+
given to `magicLink` — it has a `challenge`, never mailed, and no token:
|
|
285
|
+
send it with `signInCode`, or ask `auth.magicLink.request` for a link.
|
|
286
|
+
|
|
250
287
|
`issued` needs `token` and `email` (`code` and `email` for `signInCode`), and
|
|
251
288
|
the recipient its `name` — a user whose schema has no name passes one
|
|
252
289
|
anyway: `{ name: user.email }`. `janusMail.verifyEmail: expiresIn must be a
|
|
@@ -255,14 +292,29 @@ number of seconds: pass the text to show, `{ expiresIn: '1 hour' }`, or
|
|
|
255
292
|
leave it out.
|
|
256
293
|
|
|
257
294
|
`janusMail.twoFactorEnabled: email must be a string` (or `twoFactorDisabled`,
|
|
258
|
-
`recoveryCodeUsed` or `welcome`) is the user event itself
|
|
259
|
-
e-mail: an event names the user by id alone. Read the user first:
|
|
295
|
+
`passwordChanged`, `recoveryCodeUsed` or `welcome`) is the user event itself
|
|
296
|
+
given to the e-mail: an event names the user by id alone. Read the user first:
|
|
260
297
|
|
|
261
298
|
```ts
|
|
262
299
|
const user = await auth.get(event.userId);
|
|
263
300
|
await mail.twoFactorDisabled({ name: user.name, locale: user.locale, email: user.email });
|
|
264
301
|
```
|
|
265
302
|
|
|
303
|
+
On `user.emailChanged`, `janusMail.emailChanged: name must be a string` is
|
|
304
|
+
the event itself given to `emailChanged`, and `janusMail.emailChanged:
|
|
305
|
+
formerEmail must be a string` is `event.formerEmail` passed without its
|
|
306
|
+
check — it is `null` for a user who had no e-mail before. `janusMail.emailChanged:
|
|
307
|
+
newEmail must be a string` is an update that removed the e-mail: there is no
|
|
308
|
+
new address to name. Check both, and read the rest from the user:
|
|
309
|
+
|
|
310
|
+
```ts
|
|
311
|
+
if (event.type === 'user.emailChanged' && event.formerEmail != null) {
|
|
312
|
+
const user = await auth.get(event.userId);
|
|
313
|
+
if (user.email === undefined) return; // removed: tell the former address your own way
|
|
314
|
+
await mail.emailChanged({ name: user.name, locale: user.locale, formerEmail: event.formerEmail, newEmail: user.email });
|
|
315
|
+
}
|
|
316
|
+
```
|
|
317
|
+
|
|
266
318
|
`janusMail.recoveryCodeUsed: when must be a string` is the second argument
|
|
267
319
|
missing, or its `when` given as the event's `Date`: pass the text to show,
|
|
268
320
|
formatted in the recipient's locale and time zone —
|
|
@@ -270,8 +322,8 @@ formatted in the recipient's locale and time zone —
|
|
|
270
322
|
|
|
271
323
|
### `janusMail.<method>: expiresAt must be a Date`
|
|
272
324
|
|
|
273
|
-
A `TypeError` from `verifyEmail`, `resetPassword
|
|
274
|
-
says how long its link or code lasts, from `issued.expiresAt`, and that is
|
|
325
|
+
A `TypeError` from `verifyEmail`, `resetPassword`, `signInCode` or
|
|
326
|
+
`magicLink`: the e-mail says how long its link or code lasts, from `issued.expiresAt`, and that is
|
|
275
327
|
not a valid `Date`. The usual cause is a flow's answer that went through
|
|
276
328
|
JSON — a job queue, a cache — where a `Date` becomes a string. Nothing is
|
|
277
329
|
sent. Revive it when the job runs, or say the expiry yourself:
|
|
@@ -311,8 +363,8 @@ await mail.recoveryCodeUsed(to, { when, recoveryCodesLeft: `Sie haben noch ${lef
|
|
|
311
363
|
|
|
312
364
|
### `janusMail.<method>: clock.now() must answer a Date`
|
|
313
365
|
|
|
314
|
-
A `TypeError` from `verifyEmail`, `resetPassword
|
|
315
|
-
`clock` given to `janusMail()` has a `now` that answered something else — a
|
|
366
|
+
A `TypeError` from `verifyEmail`, `resetPassword`, `signInCode` or
|
|
367
|
+
`magicLink`: the `clock` given to `janusMail()` has a `now` that answered something else — a
|
|
316
368
|
number from `Date.now()`, a string, an invalid `Date`. Nothing is sent.
|
|
317
369
|
`clock` is `@nxgt/janus`'s `Clock`: pass the one `janus({ clock })` was
|
|
318
370
|
given, or `{ now: () => new Date() }`.
|
|
@@ -369,7 +421,7 @@ A `TypeError` from a call, before anything is rendered or sent
|
|
|
369
421
|
must answer a string`, `janusMail.emailChanged: links.secureAccount() must
|
|
370
422
|
answer a string`, `janusMail.welcome: links.getStarted() must answer a
|
|
371
423
|
string`, `janusMail.recoveryCodeUsed: links.recoveryCodes() must answer a
|
|
372
|
-
string`. The `links` function answered something other than a
|
|
424
|
+
string`, `janusMail.magicLink: links.magicLink(token) must answer a string`. The `links` function answered something other than a
|
|
373
425
|
string: a `URL` object, `undefined`, or a promise — an `async` function.
|
|
374
426
|
Answer `url.href`, and compute the link synchronously:
|
|
375
427
|
|
|
@@ -445,7 +497,7 @@ avoids the read altogether. (In this repository: run `bun run build`.)
|
|
|
445
497
|
|
|
446
498
|
The default templates read `mails/` with `node:fs`, through
|
|
447
499
|
`@nxgt/mail/renderer`: Node, Bun or Deno only. On an edge runtime, pass all
|
|
448
|
-
|
|
500
|
+
ten templates yourself; an inlined build is on the [roadmap](roadmap.md).
|
|
449
501
|
|
|
450
502
|
### An e-mail stays light in dark mode
|
|
451
503
|
|
|
@@ -561,7 +613,9 @@ if (issued !== null) await mail.resetPassword(issued, { name: issued.user.name }
|
|
|
561
613
|
return new Response(null, { status: 202 });
|
|
562
614
|
```
|
|
563
615
|
|
|
564
|
-
|
|
616
|
+
`auth.magicLink.request(email)`'s answer, passed to `mail.magicLink`
|
|
617
|
+
unchecked, reads the same. The same holds for `signInCode.request`, whose
|
|
618
|
+
answer passed unchecked reads
|
|
565
619
|
`Argument of type 'IssuedCode<…> | null' is not assignable to parameter of
|
|
566
620
|
type 'Pick<IssuedCode<unknown>, "email" | "code" | "expiresAt">'`.
|
|
567
621
|
|
|
@@ -576,19 +630,27 @@ whole.
|
|
|
576
630
|
### `TS2739: Type '{ … }' is missing the following properties from type 'JanusMailTemplates<…>'`
|
|
577
631
|
|
|
578
632
|
`locales` holds a locale beyond `en` and `fr`, and `templates` does not give
|
|
579
|
-
all
|
|
580
|
-
`…, and
|
|
633
|
+
all ten. With five or more missing, the code is `TS2740` and the list ends
|
|
634
|
+
`…, and 5 more.` See [Adding a locale](guide/locales.md#adding-a-locale).
|
|
581
635
|
|
|
582
636
|
### `TS2741: Property 'recoveryCodeUsed' is missing in type '{ … }' but required in type 'JanusMailTemplates<…>'.`
|
|
583
637
|
|
|
584
638
|
`templates` written for 0.4, with a locale beyond `en` and `fr`: since
|
|
585
|
-
0.5.0
|
|
639
|
+
0.5.0 `recoveryCodeUsed` is one of the templates, and with such a locale every one is yours.
|
|
586
640
|
Add `recoveryCodeUsed` — it is given `brand`, `name`, `when`,
|
|
587
641
|
`recoveryCodesLeft` (a sentence) and `link` — even if you never send it; in
|
|
588
642
|
JavaScript the same `templates` is
|
|
589
643
|
[`janusMail: the default templates are built in en and fr only — …; recoveryCodeUsed missing`](#janusmail-the-default-templates-are-built-in-en-and-fr-only--with-another-locale-in-locales-pass-every-template-in-templates-names-missing).
|
|
590
644
|
See [Templates](guide/templates.md#what-each-template-is-given).
|
|
591
645
|
|
|
646
|
+
### `TS2741: Property 'magicLink' is missing in type '{ … }' but required in type 'JanusMailTemplates<…>'.`
|
|
647
|
+
|
|
648
|
+
`templates` written before 0.7, with a locale beyond `en` and `fr`: since
|
|
649
|
+
0.7.0 there are ten templates, and with such a locale every one is yours.
|
|
650
|
+
Add `magicLink` — it is given `brand`, `link` and `expiresIn` — even if you
|
|
651
|
+
never send it; in JavaScript the same `templates` is
|
|
652
|
+
[`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
|
+
|
|
592
654
|
### `TS2322: Type '"de"' is not assignable to type '"en" | "fr"'.`
|
|
593
655
|
|
|
594
656
|
A `fallbackLocale` outside `locales`. Add it to `locales` — with every
|