@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/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 `signInCode` may pass
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;
@@ -1 +1 @@
1
- {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"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"}
package/docs/README.md CHANGED
@@ -5,7 +5,7 @@ the detail.
5
5
 
6
6
  | Page | Read it when |
7
7
  | --- | --- |
8
- | [Sending](guide/sending.md) | You are wiring `janusMail()` into your routes: the options, `clock` among them; each method, the welcome on `user.created` and the recovery code notice on `user.recoveryCodeUsed` among them; the expiry an e-mail states and how to set it, where each e-mail goes, what a failure looks like, retrying and tracing the mailer, and a test |
8
+ | [Sending](guide/sending.md) | You are wiring `janusMail()` into your routes: the options, `clock` among them; each method, the 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 nine this package sends: `verify-email`, `reset-password`, `sign-in-code`, `password-changed`, `email-changed`, `two-factor-enabled`, `two-factor-disabled`, `recovery-code-used`, `welcome` — by the name of its built file | "mail", "message" — a *message* is what a mailer sends, addressed |
26
+ | **e-mail** | One of the 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 `resetPassword.request`, an `IssuedCode` from `signInCode.request` | "result", "payload" |
30
+ | **issued** | What a flow answered: an `IssuedToken` from `verifyEmail.send`, `resetPassword.request` or `magicLink.request`, an `IssuedCode` from `signInCode.request` | "result", "payload" |
31
31
  | **recipient** | Who an e-mail is for besides the address: a `Recipient`, `{ name, locale? }` | "user" — a user is a record of `@nxgt/janus`; "to" is the address |
32
32
  | **notice** | An e-mail that tells, and asks for nothing: `passwordChanged`, `emailChanged`, `twoFactorEnabled`, `twoFactorDisabled`, `recoveryCodeUsed`. It links to `links.secureAccount()` — `recoveryCodeUsed` to `links.recoveryCodes()` when given | "alert", "notification" |
33
33
  | **welcome** | The e-mail sent once a user was created, on `@nxgt/janus`'s `user.created`: `welcome`. It links to `links.getStarted()` — not a notice, since it warns of nothing | "onboarding", "greeting" |
@@ -55,7 +55,7 @@ packages/janus-mail/
55
55
  `src/generated/mail.ts` (`rendererTypes`), from the build.
56
56
  3. `scripts/build-mail.ts` reads the manifest and **fails unless its
57
57
  format is one every `@nxgt/mail` the peer admits reads** — see below —
58
- and **unless exactly the nine e-mails were built**: a tenth from a new
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
@@ -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: a `templates` of
104
- eight, written for 0.4, no longer compiles with a locale beyond `en` and
105
- `fr`. A
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, passwordChanged, emailChanged, and 4 more.
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 nine for a new locale.
132
+ - [Templates](templates.md) — writing the ten for a new locale.
@@ -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
- const changed = await auth.changePassword(user, { current, next });
121
- await mail.passwordChanged({ name: changed.name, locale: changed.locale, email: changed.email });
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 after
126
- `changePassword`, `setPassword` and `resetPassword.confirm`.
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
- const formerEmail = user.email;
132
- const updated = await auth.update(user, { email: next });
133
- await mail.emailChanged({ name: updated.name, locale: updated.locale, formerEmail, newEmail: updated.email });
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()`. Read the former address before the
139
- update: the user `update` answers already holds the new one.
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 `signInCode` say how long the link or the
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` | — |
@@ -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 nine, using the defaults on their
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 nine, and the defaults render the rest. Once `locales` holds
128
- another locale, it takes **all nine** — see
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 nine in use — yours, and the defaults for 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 nine defaults, typed for `'en' | 'fr'`, without a mailer:
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 `signInCode` also say how long the link
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** — `magic-link`, `new-sign-in` and `invitation` from
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 link, a sign-in from a new device, an invitation to a user type.
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:
@@ -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`. A fifth, `recoveryCodes`, is optional.
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 nine — `magicLink`, or the e-mail's file name
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 nine — see
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 given to the
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` or `signInCode`: the e-mail
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` or `signInCode`: the
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
- nine templates yourself; an inlined build is on the [roadmap](roadmap.md).
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
- The same holds for `signInCode.request`, whose answer passed unchecked reads
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 nine. With five or more missing, the code is `TS2740` and the list ends
580
- `…, and 4 more.` See [Adding a locale](guide/locales.md#adding-a-locale).
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 there are nine templates, and with such a locale every one is yours.
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