@nxgt/janus-mail 0.6.0 → 0.7.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1 +1 @@
1
- {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"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"}
package/docs/README.md CHANGED
@@ -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,6 +115,36 @@ 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
@@ -130,8 +161,10 @@ const auth = janus({
130
161
 
131
162
  A notice: the password of the account at `to.email` changed, with a link to
132
163
  `links.secureAccount()` for a user who did not change it. Send it on the
133
- `user.passwordChanged` event, which `changePassword` and `setPassword` send.
134
- A reset sends `user.passwordReset` instead, never both: to tell a reset too,
164
+ `user.passwordChanged` event, which `changePassword` and `setPassword` send —
165
+ and, from `@nxgt/janus` 0.16, `signInCode.confirm` and `magicLink.confirm`
166
+ when their first proof of the e-mail dropped a password someone may have set
167
+ without holding the inbox. A reset sends `user.passwordReset` instead, never both: to tell a reset too,
135
168
  send the same notice on that type — `event.type === 'user.passwordChanged'
136
169
  || event.type === 'user.passwordReset'`. The event names the user by id
137
170
  alone, so read the name, the locale and the address from the user.
@@ -179,17 +212,21 @@ const auth = janus({
179
212
  });
180
213
  ```
181
214
 
182
- Two notices: two-factor authentication was turned on, or off, for the
183
- account at `to.email`, each with a link to `links.secureAccount()` — the
184
- user's security settings — for a user who did not make the change. Send
215
+ Two notices: two-factor authentication was turned on, or off, for the account
216
+ at `to.email`, each with a link to `links.secureAccount()` — the user's
217
+ security settings — for a user who did not make the change. Send
185
218
  `twoFactorEnabled` on the `user.secondFactorEnabled` event, which
186
219
  `auth.secondFactor.activate` sends once the factor is active, and
187
220
  `twoFactorDisabled` on `user.secondFactorDisabled`, which
188
221
  `auth.secondFactor.disable` sends only when it removed an active factor: a
189
- `disable` on a user who had none tells nobody anything. The event names the
190
- user by id alone, so read the name, the locale and the address from the
191
- user. Calling them right after `activate` or `disable` answered works as
192
- well — without the events, check `hasSecondFactor` before the `disable`.
222
+ `disable` on a user who had none tells nobody anything. From `@nxgt/janus`
223
+ 0.16, `signInCode.confirm` and `magicLink.confirm` send it too when their
224
+ first proof of the e-mail removed a factor someone may have enrolled without
225
+ holding the inbox — after `user.passwordChanged`, so the inbox's owner gets
226
+ both notices. The event names the user by id alone, so read the name, the
227
+ locale and the address from the user. Calling them right after `activate` or
228
+ `disable` answered works as well — without the events, check `hasSecondFactor`
229
+ before the `disable`.
193
230
 
194
231
  ### `welcome(to)`
195
232
 
@@ -276,8 +313,8 @@ when opening the session then fails.
276
313
 
277
314
  ## The expiry
278
315
 
279
- `verifyEmail`, `resetPassword` and `signInCode` say how long the link or the
280
- code lasts — "This link expires in 1 hour.", "Ce code expire dans
316
+ `verifyEmail`, `resetPassword`, `signInCode` and `magicLink` say how long
317
+ the link or the code lasts — "This link expires in 1 hour.", "Ce code expire dans
281
318
  10 minutes." The text is `expiresIn`, one of the template's variables,
282
319
  derived at send time from the flow's `issued.expiresAt`:
283
320
 
@@ -325,6 +362,7 @@ a `TypeError`, and nothing reaches the mailer: revive the date with
325
362
  | `verifyEmail` | `issued.email` | what the visitor typed |
326
363
  | `resetPassword` | `issued.email` | the `email` passed to `request` |
327
364
  | `signInCode` | `issued.email` | the `email` passed to `request` |
365
+ | `magicLink` | `issued.email` | the `email` passed to `request` |
328
366
  | `passwordChanged` | `to.email` | — |
329
367
  | `emailChanged` | `to.formerEmail` | `to.newEmail` |
330
368
  | `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,15 @@ 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
+
50
59
  - **The change notices on their events, v0.6.0.** `passwordChanged` and
51
60
  `emailChanged` are sent from `@nxgt/janus` 0.14's `user.passwordChanged`
52
61
  and `user.emailChanged` events, as the two-factor notices are, rather than
@@ -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
@@ -285,8 +322,8 @@ formatted in the recipient's locale and time zone —
285
322
 
286
323
  ### `janusMail.<method>: expiresAt must be a Date`
287
324
 
288
- A `TypeError` from `verifyEmail`, `resetPassword` or `signInCode`: the e-mail
289
- 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
290
327
  not a valid `Date`. The usual cause is a flow's answer that went through
291
328
  JSON — a job queue, a cache — where a `Date` becomes a string. Nothing is
292
329
  sent. Revive it when the job runs, or say the expiry yourself:
@@ -326,8 +363,8 @@ await mail.recoveryCodeUsed(to, { when, recoveryCodesLeft: `Sie haben noch ${lef
326
363
 
327
364
  ### `janusMail.<method>: clock.now() must answer a Date`
328
365
 
329
- A `TypeError` from `verifyEmail`, `resetPassword` or `signInCode`: the
330
- `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
331
368
  number from `Date.now()`, a string, an invalid `Date`. Nothing is sent.
332
369
  `clock` is `@nxgt/janus`'s `Clock`: pass the one `janus({ clock })` was
333
370
  given, or `{ now: () => new Date() }`.
@@ -384,7 +421,7 @@ A `TypeError` from a call, before anything is rendered or sent
384
421
  must answer a string`, `janusMail.emailChanged: links.secureAccount() must
385
422
  answer a string`, `janusMail.welcome: links.getStarted() must answer a
386
423
  string`, `janusMail.recoveryCodeUsed: links.recoveryCodes() must answer a
387
- 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
388
425
  string: a `URL` object, `undefined`, or a promise — an `async` function.
389
426
  Answer `url.href`, and compute the link synchronously:
390
427
 
@@ -460,7 +497,7 @@ avoids the read altogether. (In this repository: run `bun run build`.)
460
497
 
461
498
  The default templates read `mails/` with `node:fs`, through
462
499
  `@nxgt/mail/renderer`: Node, Bun or Deno only. On an edge runtime, pass all
463
- 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).
464
501
 
465
502
  ### An e-mail stays light in dark mode
466
503
 
@@ -576,7 +613,9 @@ if (issued !== null) await mail.resetPassword(issued, { name: issued.user.name }
576
613
  return new Response(null, { status: 202 });
577
614
  ```
578
615
 
579
- 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
580
619
  `Argument of type 'IssuedCode<…> | null' is not assignable to parameter of
581
620
  type 'Pick<IssuedCode<unknown>, "email" | "code" | "expiresAt">'`.
582
621
 
@@ -591,19 +630,27 @@ whole.
591
630
  ### `TS2739: Type '{ … }' is missing the following properties from type 'JanusMailTemplates<…>'`
592
631
 
593
632
  `locales` holds a locale beyond `en` and `fr`, and `templates` does not give
594
- all nine. With five or more missing, the code is `TS2740` and the list ends
595
- `…, 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).
596
635
 
597
636
  ### `TS2741: Property 'recoveryCodeUsed' is missing in type '{ … }' but required in type 'JanusMailTemplates<…>'.`
598
637
 
599
638
  `templates` written for 0.4, with a locale beyond `en` and `fr`: since
600
- 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.
601
640
  Add `recoveryCodeUsed` — it is given `brand`, `name`, `when`,
602
641
  `recoveryCodesLeft` (a sentence) and `link` — even if you never send it; in
603
642
  JavaScript the same `templates` is
604
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).
605
644
  See [Templates](guide/templates.md#what-each-template-is-given).
606
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
+
607
654
  ### `TS2322: Type '"de"' is not assignable to type '"en" | "fr"'.`
608
655
 
609
656
  A `fallbackLocale` outside `locales`. Add it to `locales` — with every
@@ -0,0 +1,51 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en" dir="ltr" xmlns:v="urn:schemas-microsoft-com:vml" xmlns:o="urn:schemas-microsoft-com:office:office" style="color-scheme:light dark;">
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <meta name="x-apple-disable-message-reformatting">
6
+ <meta name="viewport" content="width=device-width, initial-scale=1">
7
+ <meta name="color-scheme" content="light dark">
8
+ <meta name="supported-color-schemes" content="light dark">
9
+ <style>[data-ogsc] .nx-dark-bg-paper{background-color:#040a19!important;}[data-ogsb] .nx-dark-bg-paper{background-color:#040a19!important;}[data-ogsc] .nx-dark-bg-card{background-color:#0f172b!important;background-color:lab(7.78673% 1.82345 -15.0537)!important;}[data-ogsb] .nx-dark-bg-card{background-color:#0f172b!important;background-color:lab(7.78673% 1.82345 -15.0537)!important;}[data-ogsc] .nx-dark-bg-background{background-color:#020918!important;background-color:lab(2.35721% .367686
10
+ -8.51797)!important;}[data-ogsb] .nx-dark-bg-background{background-color:#020918!important;background-color:lab(2.35721% .367686 -8.51797)!important;}[data-ogsc] .nx-dark-text-foreground{color:#f8fafc!important;color:lab(98.1434% -.369519 -1.05966)!important;}[data-ogsb] .nx-dark-text-foreground{color:#f8fafc!important;color:lab(98.1434% -.369519 -1.05966)!important;}[data-ogsc] .nx-dark-text-foreground-25{color:#404551!important;}[data-ogsb] .nx-dark-text-foreground-25{color:#404551!important;}
11
+ [data-ogsc] .nx-dark-text-card-foreground{color:#f8fafc!important;color:lab(98.1434% -.369519 -1.05966)!important;}[data-ogsb] .nx-dark-text-card-foreground{color:#f8fafc!important;color:lab(98.1434% -.369519 -1.05966)!important;}[data-ogsc] .nx-dark-border-border{border-color:#1c212f!important;}[data-ogsb] .nx-dark-border-border{border-color:#1c212f!important;}[data-ogsc] .nx-dark-bg-muted{background-color:#1e293b!important;}[data-ogsb] .nx-dark-bg-muted{background-color:#1e293b!important;}
12
+ [data-ogsc] .nx-dark-text-muted-foreground{color:#cbd5e1!important;}[data-ogsb] .nx-dark-text-muted-foreground{color:#cbd5e1!important;}[data-ogsc] .nx-dark-border-muted-foreground{border-color:#cbd5e1!important;}[data-ogsb] .nx-dark-border-muted-foreground{border-color:#cbd5e1!important;}[data-ogsc] .nx-dark-border-muted{border-color:#1e293b!important;}[data-ogsb] .nx-dark-border-muted{border-color:#1e293b!important;}[data-ogsc] .nx-dark-bg-primary{background-color:#fafafa!important;}
13
+ [data-ogsb] .nx-dark-bg-primary{background-color:#fafafa!important;}[data-ogsc] .nx-dark-text-primary{color:#fafafa!important;}[data-ogsb] .nx-dark-text-primary{color:#fafafa!important;}[data-ogsc] .nx-dark-text-primary-foreground{color:#18181b!important;}[data-ogsb] .nx-dark-text-primary-foreground{color:#18181b!important;}[data-ogsc] .nx-dark-text-info{color:#93c5fd!important;}[data-ogsb] .nx-dark-text-info{color:#93c5fd!important;}[data-ogsc] .nx-dark-border-primary{border-color:#fafafa
14
+ !important;}[data-ogsb] .nx-dark-border-primary{border-color:#fafafa!important;}[data-ogsc] .nx-dark-bg-primary-15{background-color:#272d3a!important;}[data-ogsb] .nx-dark-bg-primary-15{background-color:#272d3a!important;}[data-ogsc] .nx-dark-bg-primary-20{background-color:#343945!important;}[data-ogsb] .nx-dark-bg-primary-20{background-color:#343945!important;}[data-ogsc] .nx-dark-border-primary-40{border-color:#656972!important;}[data-ogsb] .nx-dark-border-primary-40{border-color:#656972
15
+ !important;}[data-ogsc] .nx-dark-border-primary-50{border-color:#7e8189!important;}[data-ogsb] .nx-dark-border-primary-50{border-color:#7e8189!important;}[data-ogsc] .nx-light-only{display:none!important;}[data-ogsb] .nx-light-only{display:none!important;}[data-ogsc] .nx-dark-only{display:inline-block!important;}[data-ogsb] .nx-dark-only{display:inline-block!important;}
16
+ @media (prefers-color-scheme:dark){html{--lightningcss-light:;--lightningcss-dark:initial;}.nx-dark-bg-paper{background-color:#040a19!important;}.nx-dark-bg-card{background-color:#0f172b!important;background-color:lab(7.78673% 1.82345 -15.0537)!important;}.nx-dark-text-foreground{color:#f8fafc!important;color:lab(98.1434% -.369519 -1.05966)!important;}.nx-dark-text-card-foreground{color:#f8fafc!important;color:lab(98.1434% -.369519 -1.05966)!important;}.nx-dark-border-border{border-color:#1c212f
17
+ !important;}.nx-dark-text-muted-foreground{color:#cbd5e1!important;}.nx-dark-bg-primary{background-color:#fafafa!important;}.nx-dark-text-primary-foreground{color:#18181b!important;}.nx-dark-text-info{color:#93c5fd!important;} }
18
+ </style>
19
+ </head>
20
+ <body xml:lang="en" dir="ltr" style="margin:0;padding:0;width:100%;height:100%;word-break:break-word;background-color:#f4f4f4;" class="nx-dark-bg-paper"><div style="display:none;">One click to sign in.&#8199;&#65279;&#847; &#8199;&#65279;&#847; &#8199;&#65279;&#847; &#8199;&#65279;&#847; &#8199;&#65279;&#847; &#8199;&#65279;&#847; &#8199;&#65279;&#847; &#8199;&#65279;&#847; &#8199;&#65279;&#847; &#8199;&#65279;&#847; &#8199;&#65279;&#847; &#8199;&#65279;&#847; &#8199;&#65279;&#847; &#8199;&#65279;&#847;
21
+ &#8199;&#65279;&#847; &#8199;&#65279;&#847; &#8199;&#65279;&#847; &#8199;&#65279;&#847; &#8199;&#65279;&#847; &#8199;&#65279;&#847; &#8199;&#65279;&#847; &#8199;&#65279;&#847; &#8199;&#65279;&#847; &#8199;&#65279;&#847; &#8199;&#65279;&#847; &#8199;&#65279;&#847; &#8199;&#65279;&#847; &#8199;&#65279;&#847; &#8199;&#65279;&#847; &#8199;&#65279;&#847; &#8199;&#65279;&#847; &#8199;&#65279;&#847; &#8199;&#65279;&#847; &#8199;&#65279;&#847; &#8199;&#65279;&#847; &#8199;&#65279;&#847; &#8199;&#65279;&#847;
22
+ &#8199;&#65279;&#847; &#8199;&#65279;&#847; &#8199;&#65279;&#847; &#8199;&#65279;&#847; &#8199;&#65279;&#847; &#8199;&#65279;&#847; &#8199;&#65279;&#847; &#8199;&#65279;&#847; &#8199;&#65279;&#847; &#8199;&#65279;&#847; &#8199;&#65279;&#847; &#8199;&#65279;&#847; &#8199;&#65279;&#847; &#8199;&#65279;&#847; &#8199;&#65279;&#847; &#8199;&#65279;&#847; &#8199;&#65279;&#847; &#8199;&#65279;&#847; &#8199;&#65279;&#847; &#8199;&#65279;&#847; &#8199;&#65279;&#847; &#8199;&#65279;&#847; &#8199;&#65279;&#847;
23
+ &#8199;&#65279;&#847; &#8199;&#65279;&#847; &#8199;&#65279;&#847; &#8199;&#65279;&#847; &#8199;&#65279;&#847; &#8199;&#65279;&#847; &#8199;&#65279;&#847; &#8199;&#65279;&#847; &#8199;&#65279;&#847; &#8199;&#65279;&#847; &#8199;&#65279;&#847; &#8199;&#65279;&#847; &#8199;&#65279;&#847; &#8199;&#65279;&#847; &#8199;&#65279;&#847; &#8199;&#65279;&#847; &#8199;&#65279;&#847; &#8199;&#65279;&#847; &#8199;&#65279;&#847; &#8199;&#65279;&#847; &#8199;&#65279;&#847; &#8199;&#65279;&#847; &#8199;&#65279;&#847;
24
+ &#8199;&#65279;&#847; &#8199;&#65279;&#847; &#8199;&#65279;&#847; &#8199;&#65279;&#847; &#8199;&#65279;&#847; &#8199;&#65279;&#847; &#8199;&#65279;&#847; &#8199;&#65279;&#847; &#8199;&#65279;&#847; &#8199;&#65279;&#847; &#8199;&#65279;&#847; &#8199;&#65279;&#847; &#8199;&#65279;&#847; &#8199;&#65279;&#847; &#8199;&#65279;&#847; &#8199;&#65279;&#847; &#8199;&#65279;&#847; &#8199;&#65279;&#847; &#8199;&#65279;&#847; &#8199;&#65279;&#847; &#8199;&#65279;&#847; &#8199;&#65279;&#847; &#8199;&#65279;&#847;
25
+ &#8199;&#65279;&#847; &#8199;&#65279;&#847; &#8199;&#65279;&#847; &#8199;&#65279;&#847; &#8199;&#65279;&#847; &#8199;&#65279;&#847; &#8199;&#65279;&#847; &#8199;&#65279;&#847; &#8199;&#65279;&#847; &#8199;&#65279;&#847; &#8199;&#65279;&#847; &#8199;&#65279;&#847; &#8199;&#65279;&#847; &#8199;&#65279;&#847; &#8199;&#65279;&#847; &#8199;&#65279;&#847; &#8199;&#65279;&#847; &#8199;&#65279;&#847; &#8199;&#65279;&#847; &#8199;&#65279;&#847; &#8199;&#65279;&#847; &#8199;&#65279;&#847; &#8199;&#65279;&#847;
26
+ &#8199;&#65279;&#847; &#8199;&#65279;&#847; &#8199;&#65279;&#847; &#8199;&#65279;&#847; &#8199;&#65279;&#847; &#8199;&#65279;&#847; &#8199;&#65279;&#847; &#8199;&#65279;&#847; &#8199;&#65279;&#847; &#8199;&#65279;&#847; &#8199;&#65279;&#847; &#8199;&#65279;&#847; &#8199;&#65279;&#847; &#8199;&#65279;&#847; &#8199;&#65279;&#847; &#8199;&#65279;&#847; &#8199;&#65279;&#847; &#8199;&#65279;&#847; &#8199;&#65279;&#847; &#8199;&#65279;&#847; &#8199;&#65279;&#847; &#8199;&#65279;&#847; &#8199;&#65279;&#847;
27
+ &#8199;&#65279;&#847; &#8199;&#65279;&#847; &#8199;&#65279;&#847; &#8199;&#65279;&#847; &#8199;&#65279;&#847; &#8199;&#65279;&#847; &#8199;&#65279;&#847; &#8199;&#65279;&#847; &#8199;&#65279;&#847; &#8199;&#65279;&#847; &#8199;&#65279;&#847; &#8199;&#65279;&#847; &#8199;&#65279;&#847; &#8199;&#65279;&#847; &#8199;&#65279;&#847; &#8199;&#65279;&#847; &#8199;&#65279;&#847; &#8199;&#65279;&#847; &#8199;&#65279;&#847; &#8199;&#65279;&#847; &#8199;&#65279;&#847; &#8199;&#65279;&#847; &#8199;&#65279;&#847;
28
+ &#8199;&#65279;&#847; &#8199;&#65279;&#847; &#8199;&#65279;&#847; &#8199;&#65279;&#847; &nbsp;</div><span style="display:none">
29
+ <!--[if mso]><xml><o:OfficeDocumentSettings><o:PixelsPerInch>96</o:PixelsPerInch></o:OfficeDocumentSettings><w:WordDocument><w:DontUseAdvancedTypographyReadingMail/></w:WordDocument></xml>
30
+ <![endif]--></span><div role="article" aria-roledescription="email" lang="en" dir="ltr" style="font-size:medium;font-size:max(16px,1rem)">
31
+ <table class="nx-dark-bg-paper" role="presentation" cellpadding="0" cellspacing="0" dir="ltr" style="width:100%;background-color:#f4f4f4;font-family:ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont, 'Segoe UI',Roboto, 'Noto Sans',Ubuntu,Cantarell, 'Helvetica Neue',sans-serif;"><tr><td align="center" style="padding:32px 16px;">
32
+ <!--[if mso]>
33
+ <table role="none" cellpadding="0" cellspacing="0" style="width:600px" align="center"><tr><td>
34
+ <![endif]--><div style="margin:0 auto;max-width:600px;">
35
+ <table role="presentation" cellpadding="0" cellspacing="0" dir="ltr" style="width:100%;"><tr><td style="padding-bottom:24px;text-align:center;"><span class="nx-dark-text-foreground" style="font-size:18px;line-height:28px;font-weight:600;color:#020918;color:lab(2.35721% .367686 -8.51797);">{{ brand }}</span>
36
+ </td></tr><tr><td class="nx-dark-border-border nx-dark-bg-card nx-dark-text-card-foreground" style="border-radius:14px;border-color:#e2e8f0;border-style:solid;border-width:1px;border-color:lab(91.7353% -.998765 -4.76968);background-color:#fff;background-color:lab(100% 0 0);padding:32px;font-size:14px;line-height:20px;color:#020618;color:lab(1.76974% 1.32743 -9.28855);box-shadow:0 0 rgba(0,0,0,0),0 0 rgba(0,0,0,0),0 0 rgba(0,0,0,0),0 0 rgba(0,0,0,0),0 1px 3px rgba(0,0,0,.1),0 1px 2px -1px rgba(0,
37
+ 0,0,.1);"><h1 class="nx-dark-text-foreground" style="margin:0 0 16px;font-size:24px;line-height:32px;font-weight:600;color:#020918;color:lab(2.35721% .367686 -8.51797);">Sign in to {{ brand }}</h1><p class="nx-dark-text-foreground" style="margin:0 0 16px;font-size:14px;line-height:20px;color:#020918;color:lab(2.35721% .367686 -8.51797);">Click the button below to sign in. The link works once.</p><div><a style="display:inline-block;border-radius:9999px;background-color:#27272a;padding:10px 16px;
38
+ font-family:ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont, 'Segoe UI',Roboto, 'Noto Sans',Ubuntu,Cantarell, 'Helvetica Neue',sans-serif;font-size:14px;line-height:16px;font-weight:500;color:#f8fafc;color:lab(98.1434% -.369519 -1.05966);text-decoration:none;" href="{{ link }}" class="nx-dark-bg-primary nx-dark-text-primary-foreground">
39
+ <!--[if mso]><i style="mso-font-width:114%;mso-text-raise:19px;" hidden>&emsp;</i>
40
+ <![endif]--> <span style="mso-text-raise:10px;">Sign in</span>
41
+ <!--[if mso]><i style="mso-font-width:114%;" hidden>&emsp;&#8203;</i>
42
+ <![endif]--></a></div><div role="separator" style="line-height:24px;">&zwj;</div><p class="nx-dark-text-foreground" style="margin:0 0 16px;font-size:12px;line-height:16px;color:#020918;color:lab(2.35721% .367686 -8.51797);">This link expires in {{ expiresIn }}.</p><p class="nx-dark-text-foreground" style="margin:0 0 16px;font-size:12px;line-height:16px;color:#5f718a;">If the button does not work, open this link: <a href="{{ link }}" class="nx-dark-text-info" style="color:#1d4ed8;text-decoration:
43
+ underline;">{{ link }}</a></p><p class="nx-dark-text-muted-foreground" style="margin:0 0 16px;font-size:10px;color:#5f718a;">If you did not ask for this, you can ignore this e-mail.</p>
44
+ </td></tr><tr><td class="nx-dark-text-muted-foreground" style="padding-left:24px;padding-right:24px;padding-top:24px;text-align:center;font-size:12px;line-height:16px;color:#5f718a;"><p style="margin:0 0 8px;">You received this e-mail because you have an account with {{ brand }}.</p><p style="margin:0;">{{ brand }}</p>
45
+ </td></tr></table></div>
46
+ <!--[if mso]>
47
+ </td></tr></table>
48
+ <![endif]-->
49
+ </td></tr></table></div>
50
+ </body>
51
+ </html>
@@ -0,0 +1,21 @@
1
+ One click to sign in.
2
+
3
+ {{ brand }}
4
+
5
+ Sign in to {{ brand }}
6
+
7
+ Click the button below to sign in. The link works once.
8
+
9
+ Sign in
10
+
11
+ {{ link }}
12
+
13
+ This link expires in {{ expiresIn }}.
14
+
15
+ If the button does not work, open this link: {{ link }}
16
+
17
+ If you did not ask for this, you can ignore this e-mail.
18
+
19
+ You received this e-mail because you have an account with {{ brand }}.
20
+
21
+ {{ brand }}