@nxgt/janus-mail 0.1.0 → 0.2.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.
- package/README.md +63 -17
- package/dist/expiring-mails.d.ts +7 -0
- package/dist/expiring-mails.d.ts.map +1 -0
- package/dist/expiry.d.ts +29 -0
- package/dist/expiry.d.ts.map +1 -0
- package/dist/generated/mail.d.ts +3 -0
- package/dist/generated/mail.d.ts.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +152 -84
- package/dist/index.js.map +10 -6
- package/dist/janus-mail.d.ts +6 -0
- package/dist/janus-mail.d.ts.map +1 -1
- package/dist/notice-mails.d.ts +12 -0
- package/dist/notice-mails.d.ts.map +1 -0
- package/dist/options.d.ts +2 -0
- package/dist/options.d.ts.map +1 -1
- package/dist/send.d.ts +27 -0
- package/dist/send.d.ts.map +1 -0
- package/dist/templates.d.ts.map +1 -1
- package/dist/types.d.ts +36 -7
- package/dist/types.d.ts.map +1 -1
- package/docs/README.md +2 -1
- package/docs/guide/building.md +23 -1
- package/docs/guide/locales.md +10 -4
- package/docs/guide/sending.md +65 -11
- package/docs/guide/templates.md +16 -14
- package/docs/roadmap.md +9 -2
- package/docs/troubleshooting.md +98 -4
- package/mails/en/reset-password.html +2 -2
- package/mails/en/reset-password.txt +3 -2
- package/mails/en/sign-in-code.html +1 -1
- package/mails/en/sign-in-code.txt +2 -0
- package/mails/en/verify-email.html +2 -2
- package/mails/en/verify-email.txt +3 -2
- package/mails/fr/reset-password.html +2 -2
- package/mails/fr/reset-password.txt +3 -2
- package/mails/fr/sign-in-code.html +1 -1
- package/mails/fr/sign-in-code.txt +2 -0
- package/mails/fr/verify-email.html +2 -2
- package/mails/fr/verify-email.txt +3 -2
- package/mails/mail-manifest.json +4 -1
- package/package.json +6 -6
package/docs/guide/sending.md
CHANGED
|
@@ -48,6 +48,7 @@ time one is sent, and kept.
|
|
|
48
48
|
| `locales` | `readonly L[]` | `['en', 'fr']` | The locales sent in — see [Locales](locales.md) |
|
|
49
49
|
| `fallbackLocale` | one of `locales` | `'en'`, else the first of `locales` | The locale when the recipient wants none of `locales` |
|
|
50
50
|
| `templates` | `Partial<JanusMailTemplates<L>>` | none | Your own templates, over the defaults — see [Templates](templates.md) |
|
|
51
|
+
| `clock` | `Clock` from `@nxgt/janus` | the system clock | What the time left until a flow's `expiresAt` is measured against — pass the one `janus({ clock })` was given; see [The expiry](#the-expiry) |
|
|
51
52
|
|
|
52
53
|
Every link is called at send time, and must answer an absolute `http:`,
|
|
53
54
|
`https:` or `mailto:` URL: a default template refuses anything else with
|
|
@@ -72,16 +73,17 @@ option and never its value — every message is in
|
|
|
72
73
|
Each method renders its e-mail with its template, hands it to the mailer,
|
|
73
74
|
and answers the mailer's `SentMail` (`{ messageId }`).
|
|
74
75
|
|
|
75
|
-
### `verifyEmail(issued, to)`
|
|
76
|
+
### `verifyEmail(issued, to, options?)`
|
|
76
77
|
|
|
77
78
|
```ts
|
|
78
79
|
const issued = await auth.verifyEmail.send(user); // IssuedToken: { token, email, expiresAt }
|
|
79
80
|
await mail.verifyEmail(issued, { name: user.name, locale: user.locale });
|
|
80
81
|
```
|
|
81
82
|
|
|
82
|
-
Sent to `issued.email`, with the link `links.verifyEmail(issued.token)
|
|
83
|
+
Sent to `issued.email`, with the link `links.verifyEmail(issued.token)` and
|
|
84
|
+
how long it lasts — see [The expiry](#the-expiry).
|
|
83
85
|
|
|
84
|
-
### `resetPassword(issued, to)`
|
|
86
|
+
### `resetPassword(issued, to, options?)`
|
|
85
87
|
|
|
86
88
|
```ts
|
|
87
89
|
const issued = await auth.resetPassword.request(email); // (IssuedToken & { user }) | null
|
|
@@ -92,16 +94,18 @@ if (issued !== null) {
|
|
|
92
94
|
|
|
93
95
|
`request` answers `null` for an address nobody holds, and the compiler
|
|
94
96
|
refuses `null` here: check it first, and answer the visitor the same either
|
|
95
|
-
way. Sent to `issued.email`, with `links.resetPassword(issued.token)
|
|
97
|
+
way. Sent to `issued.email`, with `links.resetPassword(issued.token)` and how
|
|
98
|
+
long it lasts.
|
|
96
99
|
|
|
97
|
-
### `signInCode(issued, to?)`
|
|
100
|
+
### `signInCode(issued, to?, options?)`
|
|
98
101
|
|
|
99
102
|
```ts
|
|
100
103
|
const issued = await auth.signInCode.request(email); // IssuedCode: { code, challenge, email, expiresAt, user } | null
|
|
101
104
|
if (issued !== null) await mail.signInCode(issued, { locale: issued.user.locale });
|
|
102
105
|
```
|
|
103
106
|
|
|
104
|
-
It reads `issued.code` and `issued.
|
|
107
|
+
It reads `issued.code`, `issued.email` and `issued.expiresAt`, **and nothing
|
|
108
|
+
else**: the
|
|
105
109
|
challenge is the visitor's secret, and neither the default template nor an
|
|
106
110
|
override is ever given it. `to` is optional — the e-mail greets nobody by
|
|
107
111
|
name — and only its `locale` is read.
|
|
@@ -130,6 +134,50 @@ since whoever changed it already controls the new one. It names `newEmail`,
|
|
|
130
134
|
and links to `links.secureAccount()`. Read the former address before the
|
|
131
135
|
update: the user `update` answers already holds the new one.
|
|
132
136
|
|
|
137
|
+
## The expiry
|
|
138
|
+
|
|
139
|
+
`verifyEmail`, `resetPassword` and `signInCode` say how long the link or the
|
|
140
|
+
code lasts — "This link expires in 1 hour.", "Ce code expire dans
|
|
141
|
+
10 minutes." The text is `expiresIn`, one of the template's variables,
|
|
142
|
+
derived at send time from the flow's `issued.expiresAt`:
|
|
143
|
+
|
|
144
|
+
1. the time left until `expiresAt`, rounded to the minute — the flow set it a
|
|
145
|
+
moment ago, so an hour is still "1 hour";
|
|
146
|
+
2. then **down** to the largest whole unit it holds — days, hours or minutes
|
|
147
|
+
— so, at 30 seconds or more, the e-mail never promises more than half a
|
|
148
|
+
minute beyond what is left; under that, it says "1 minute". 90 minutes is
|
|
149
|
+
"1 hour", 36 hours "1 day";
|
|
150
|
+
3. formatted by `Intl.NumberFormat` with `style: 'unit'` and
|
|
151
|
+
`unitDisplay: 'long'`, in the recipient's locale: "3 hours", "3 heures".
|
|
152
|
+
|
|
153
|
+
With `@nxgt/janus`'s defaults, a verification link says "1 day", a reset
|
|
154
|
+
link "1 hour" and a sign-in code "10 minutes".
|
|
155
|
+
|
|
156
|
+
The time is `janusMail({ clock })`'s — pass the clock `janus({ clock })` was
|
|
157
|
+
given, a `fixedClock` in tests — else the system clock:
|
|
158
|
+
|
|
159
|
+
```ts
|
|
160
|
+
import { fixedClock } from '@nxgt/janus';
|
|
161
|
+
|
|
162
|
+
const clock = fixedClock(Date.UTC(2026, 0, 1));
|
|
163
|
+
const auth = janus({ /* … */ clock });
|
|
164
|
+
const mail = janusMail({ mailer, from, brand: 'Acme', links, clock });
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
To say it yourself — another wording, or a locale the runtime's `Intl` has no
|
|
168
|
+
data for — pass `expiresIn` as the third argument, plain text already in the
|
|
169
|
+
recipient's language. `expiresAt` is then not read:
|
|
170
|
+
|
|
171
|
+
```ts
|
|
172
|
+
await mail.verifyEmail(issued, { name: user.name, locale: 'fr' }, { expiresIn: '24 heures' });
|
|
173
|
+
await mail.signInCode(issued, undefined, { expiresIn: 'ten minutes' });
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
A send whose `expiresAt` is not a valid `Date` — a flow's answer that went
|
|
177
|
+
through JSON on its way to a queue holds a string — or is already past, is
|
|
178
|
+
a `TypeError`, and nothing reaches the mailer: revive the date with
|
|
179
|
+
`new Date(issued.expiresAt)` when the job runs, or pass `expiresIn`.
|
|
180
|
+
|
|
133
181
|
## Where each e-mail goes
|
|
134
182
|
|
|
135
183
|
| Method | To | Never to |
|
|
@@ -157,7 +205,7 @@ defines no error class and wraps nothing:
|
|
|
157
205
|
| --- | --- | --- | --- |
|
|
158
206
|
| `MailFailure` (`MAIL_FAILED`) | the mailer | The transport could not hand the e-mail over. Nothing is known to have been sent | A `503`, or a retry from a queue |
|
|
159
207
|
| `MailRefused` (`MAIL_REFUSED`) | the renderer, or the mailer | The e-mail itself is wrong: a link that is not `http:`, `https:` or `mailto:`, an address that is not one | A bug to fix; sending it again fails again |
|
|
160
|
-
| `TypeError` | this package, or the renderer | A call without the value a flow answered — `janusMail.resetPassword: token must be a string` — or, in JavaScript, a link that is not a string (a compile error in TypeScript) — `janusMail.verifyEmail: links.verifyEmail(token) must answer a string` | A bug to fix |
|
|
208
|
+
| `TypeError` | this package, or the renderer | A call without the value a flow answered — `janusMail.resetPassword: token must be a string` — or, in JavaScript, a link that is not a string (a compile error in TypeScript) — `janusMail.verifyEmail: links.verifyEmail(token) must answer a string`. For [the expiry](#the-expiry): `janusMail.<method>: expiresAt must be a Date`, `janusMail.<method>: expiresAt is past — the link or code would not work`, `janusMail.<method>: expiresIn must be a string`, `janusMail.<method>: clock.now() must answer a Date` | A bug to fix — for a past `expiresAt`, issue a new token or code and send that |
|
|
161
209
|
| `Error` from `createMailRenderer` | the renderer | `mails/` is missing where the package runs: a bundler inlined `@nxgt/janus-mail`, or a deploy kept `dist/` only | Keep `@nxgt/janus-mail` external to your bundle and deploy its `mails/` with it; see [troubleshooting](../troubleshooting.md#createmailrenderer-mailsmail-manifestjson-cannot-be-read--run-maizzle-build-and-deploy-its-output-folder) |
|
|
162
210
|
|
|
163
211
|
`instanceof` holds against the classes of your own `@nxgt/mail`, since it is
|
|
@@ -197,9 +245,14 @@ fail:
|
|
|
197
245
|
|
|
198
246
|
```ts
|
|
199
247
|
import { expect, it } from 'bun:test';
|
|
248
|
+
import { fixedClock } from '@nxgt/janus';
|
|
200
249
|
import { createMemoryMailer, MailFailure } from '@nxgt/mail';
|
|
201
250
|
import { janusMail } from '@nxgt/janus-mail';
|
|
202
251
|
|
|
252
|
+
// The clock given to janus({ clock }) too: the codes it issues expire ten minutes on.
|
|
253
|
+
const clock = fixedClock(Date.UTC(2026, 0, 1, 9));
|
|
254
|
+
const expiresAt = new Date(Date.UTC(2026, 0, 1, 9, 10));
|
|
255
|
+
|
|
203
256
|
const links = {
|
|
204
257
|
verifyEmail: (token: string) => `https://acme.example/verify?token=${token}`,
|
|
205
258
|
resetPassword: (token: string) => `https://acme.example/reset?token=${token}`,
|
|
@@ -208,24 +261,25 @@ const links = {
|
|
|
208
261
|
|
|
209
262
|
it('sends the code, and never the challenge', async () => {
|
|
210
263
|
const mailer = createMemoryMailer();
|
|
211
|
-
const mail = janusMail({ mailer, from: 'noreply@acme.example', brand: 'Acme', links });
|
|
212
|
-
const issued = { code: '042817', challenge: 'secret', email: 'ada@example.com' };
|
|
264
|
+
const mail = janusMail({ mailer, from: 'noreply@acme.example', brand: 'Acme', links, clock });
|
|
265
|
+
const issued = { code: '042817', challenge: 'secret', email: 'ada@example.com', expiresAt };
|
|
213
266
|
|
|
214
267
|
await mail.signInCode(issued, { locale: 'fr-CA' });
|
|
215
268
|
|
|
216
269
|
const [sent] = mailer.sent;
|
|
217
270
|
expect(sent?.to).toBe('ada@example.com');
|
|
218
271
|
expect(sent?.subject).toBe('Votre code de connexion : 042817');
|
|
272
|
+
expect(sent?.text).toContain('10 minutes.');
|
|
219
273
|
expect(`${sent?.html}${sent?.text}`).not.toContain('secret');
|
|
220
274
|
});
|
|
221
275
|
|
|
222
276
|
it('says so when the mailer is down', async () => {
|
|
223
277
|
const mailer = createMemoryMailer();
|
|
224
278
|
mailer.failNext();
|
|
225
|
-
const mail = janusMail({ mailer, from: 'noreply@acme.example', brand: 'Acme', links });
|
|
279
|
+
const mail = janusMail({ mailer, from: 'noreply@acme.example', brand: 'Acme', links, clock });
|
|
226
280
|
|
|
227
281
|
const error = await mail
|
|
228
|
-
.signInCode({ code: '042817', email: 'ada@example.com' })
|
|
282
|
+
.signInCode({ code: '042817', email: 'ada@example.com', expiresAt })
|
|
229
283
|
.then(() => null, (e: unknown) => e); // settled where it is created
|
|
230
284
|
|
|
231
285
|
expect(error).toBeInstanceOf(MailFailure);
|
package/docs/guide/templates.md
CHANGED
|
@@ -59,30 +59,30 @@ e-mail.
|
|
|
59
59
|
|
|
60
60
|
| Template | Variables | Sent by |
|
|
61
61
|
| --- | --- | --- |
|
|
62
|
-
| `verifyEmail` | `brand`, `name`, `link` | `mail.verifyEmail(issued, to)` |
|
|
63
|
-
| `resetPassword` | `brand`, `name`, `link` | `mail.resetPassword(issued, to)` |
|
|
64
|
-
| `signInCode` | `brand`, `code` | `mail.signInCode(issued, to?)` |
|
|
62
|
+
| `verifyEmail` | `brand`, `name`, `link`, `expiresIn` | `mail.verifyEmail(issued, to, options?)` |
|
|
63
|
+
| `resetPassword` | `brand`, `name`, `link`, `expiresIn` | `mail.resetPassword(issued, to, options?)` |
|
|
64
|
+
| `signInCode` | `brand`, `code`, `expiresIn` | `mail.signInCode(issued, to?, options?)` |
|
|
65
65
|
| `passwordChanged` | `brand`, `name`, `link` | `mail.passwordChanged(to)` |
|
|
66
66
|
| `emailChanged` | `brand`, `name`, `link`, `newEmail` | `mail.emailChanged(to)` |
|
|
67
67
|
|
|
68
68
|
Every one also gets `locale`, one of `locales`. Every value is a string:
|
|
69
69
|
`brand` from the options, `name` from the recipient, `link` from `links` —
|
|
70
70
|
`links.secureAccount()` for the two notices — `code` and `newEmail` from the
|
|
71
|
-
call
|
|
71
|
+
call, and `expiresIn` derived from the flow's `expiresAt` in the locale, or
|
|
72
|
+
the send's own `expiresIn` ([Sending](sending.md#the-expiry)).
|
|
72
73
|
|
|
73
74
|
`signInCode` is **never given the challenge**: it is not in its variables,
|
|
74
|
-
and the method never reads it.
|
|
75
|
-
|
|
75
|
+
and the method never reads it. An override states the expiry from
|
|
76
|
+
`expiresIn`, already formatted:
|
|
76
77
|
|
|
77
78
|
```ts
|
|
78
79
|
const mail = janusMail({
|
|
79
80
|
mailer, from, brand: 'Acme', links,
|
|
80
81
|
templates: {
|
|
81
|
-
|
|
82
|
-
signInCode: ({ code, locale }) =>
|
|
82
|
+
signInCode: ({ code, expiresIn, locale }) =>
|
|
83
83
|
locale === 'fr'
|
|
84
|
-
? { subject: `Code : ${code}`, html: `<p>${code} — valable
|
|
85
|
-
: { subject: `Code: ${code}`, html: `<p>${code} — valid for
|
|
84
|
+
? { subject: `Code : ${code}`, html: `<p>${code} — valable ${expiresIn}.</p>`, text: `${code} — valable ${expiresIn}.` }
|
|
85
|
+
: { subject: `Code: ${code}`, html: `<p>${code} — valid for ${expiresIn}.</p>`, text: `${code} — valid for ${expiresIn}.` },
|
|
86
86
|
},
|
|
87
87
|
});
|
|
88
88
|
```
|
|
@@ -109,7 +109,7 @@ another locale, it takes **all five** — see
|
|
|
109
109
|
rest — frozen:
|
|
110
110
|
|
|
111
111
|
```ts
|
|
112
|
-
const rendered = await mail.templates.verifyEmail({ brand: 'Acme', name: 'Ada', link, locale: 'fr' });
|
|
112
|
+
const rendered = await mail.templates.verifyEmail({ brand: 'Acme', name: 'Ada', link, expiresIn: '1 heure', locale: 'fr' });
|
|
113
113
|
```
|
|
114
114
|
|
|
115
115
|
## The defaults alone — `janusTemplates()`
|
|
@@ -122,6 +122,7 @@ const { subject, html, text } = await defaults.resetPassword({
|
|
|
122
122
|
brand: 'Acme',
|
|
123
123
|
name: 'Ada',
|
|
124
124
|
link: 'https://acme.example/reset?token=abc',
|
|
125
|
+
expiresIn: '1 heure',
|
|
125
126
|
locale: 'fr',
|
|
126
127
|
});
|
|
127
128
|
```
|
|
@@ -152,7 +153,7 @@ them renders: that is when the package's `mails/` folder is read, once.
|
|
|
152
153
|
## What the defaults say
|
|
153
154
|
|
|
154
155
|
The defaults are [`@nxgt/mail-presets`](https://www.npmjs.com/package/@nxgt/mail-presets)
|
|
155
|
-
0.
|
|
156
|
+
0.3.0, built with a neutral grey theme and no logo:
|
|
156
157
|
|
|
157
158
|
| Template | Built from | Subject (`en`) | Subject (`fr`) |
|
|
158
159
|
| --- | --- | --- | --- |
|
|
@@ -167,8 +168,9 @@ Each has a header and a footer with the brand, a heading, a greeting by name
|
|
|
167
168
|
client that shows no button, and a text part. The two notices add a warning:
|
|
168
169
|
*if this was not you, secure your account now*.
|
|
169
170
|
|
|
170
|
-
|
|
171
|
-
|
|
171
|
+
`verifyEmail`, `resetPassword` and `signInCode` also say how long the link
|
|
172
|
+
or the code lasts: "This link expires in 1 hour.", "Ce code expire dans
|
|
173
|
+
10 minutes." The text part keeps each paragraph on one line.
|
|
172
174
|
|
|
173
175
|
**The brand is text.** It is written in the header, the body and the footer,
|
|
174
176
|
escaped — no logo and no link, which would need absolute URLs known when the
|
package/docs/roadmap.md
CHANGED
|
@@ -13,8 +13,6 @@ Nothing between releases.
|
|
|
13
13
|
`invitation` from `@nxgt/mail-presets`, each once `@nxgt/janus` has a flow
|
|
14
14
|
that sends it: a sign-in link, a sign-in from a new device, a user created,
|
|
15
15
|
an invitation to a user type.
|
|
16
|
-
- **The expiry in the e-mail** — "the link expires in 24 hours", from the
|
|
17
|
-
flow's `expiresAt`, once the presets' messages take it.
|
|
18
16
|
|
|
19
17
|
## Later
|
|
20
18
|
|
|
@@ -46,6 +44,15 @@ Nothing between releases.
|
|
|
46
44
|
|
|
47
45
|
Newest first; from the first release on, the package's CHANGELOG holds every one.
|
|
48
46
|
|
|
47
|
+
- **The expiry in the e-mail, v0.2.0.** The verification, reset and sign-in
|
|
48
|
+
code e-mails say how long the link or code lasts — "1 hour", "1 heure" —
|
|
49
|
+
derived from the flow's `expiresAt` in the recipient's locale, measured
|
|
50
|
+
against `janusMail({ clock })` (the clock given to `janus()`), or given per
|
|
51
|
+
send as `{ expiresIn }`. Two breaks: a template called directly takes
|
|
52
|
+
`expiresIn`, and `signInCode` reads `issued.expiresAt`. Built from
|
|
53
|
+
`@nxgt/mail-presets` 0.2.0, whose text parts keep each paragraph on one
|
|
54
|
+
line.
|
|
55
|
+
|
|
49
56
|
- **The first release, v0.1.0.** The five e-mails of `@nxgt/janus`'s flows,
|
|
50
57
|
in English and French, over any `@nxgt/mail` transport: `janusMail()` and
|
|
51
58
|
`janusTemplates()`. Built with Maizzle when the package is built, shipped
|
package/docs/troubleshooting.md
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# Troubleshooting `@nxgt/janus-mail`
|
|
2
2
|
|
|
3
|
-
Each entry is headed by the text you see: a message, an error `code`,
|
|
4
|
-
compiler error. Search this page for the words of your message.
|
|
3
|
+
Each entry is headed by the text you see: a message, an error `code`, a
|
|
4
|
+
compiler error — or, where nothing is thrown, what the e-mail says. Search this page for the words of your message.
|
|
5
5
|
|
|
6
6
|
How the messages are shaped:
|
|
7
7
|
|
|
@@ -28,16 +28,22 @@ How the messages are shaped:
|
|
|
28
28
|
- [`janusMail: links must be an object, as { verifyEmail, resetPassword, secureAccount }`](#janusmail-links-must-be-an-object-as--verifyemail-resetpassword-secureaccount-)
|
|
29
29
|
- [`janusMail: links.<name> must be a function`](#janusmail-linksname-must-be-a-function)
|
|
30
30
|
- [`janusMail: locales must list at least one locale, as ['en', 'fr']`](#janusmail-locales-must-list-at-least-one-locale-as-en-fr)
|
|
31
|
+
- [`janusMail: locales must be BCP 47 language tags, as 'fr-CA'`](#janusmail-locales-must-be-bcp-47-language-tags-as-fr-ca)
|
|
31
32
|
- [`janusMail: locales holds the same locale twice`](#janusmail-locales-holds-the-same-locale-twice)
|
|
32
33
|
- [`janusMail: fallbackLocale must be one of locales`](#janusmail-fallbacklocale-must-be-one-of-locales)
|
|
33
34
|
- [`janusMail: templates must be an object of functions, as { verifyEmail: (variables) => rendered }`](#janusmail-templates-must-be-an-object-of-functions-as--verifyemail-variables--rendered-)
|
|
34
35
|
- [`janusMail: templates has no template <name> — name one of verifyEmail, resetPassword, signInCode, passwordChanged, emailChanged`](#janusmail-templates-has-no-template-name--name-one-of-verifyemail-resetpassword-signincode-passwordchanged-emailchanged)
|
|
35
36
|
- [`janusMail: templates.<name> must be a function`](#janusmail-templatesname-must-be-a-function)
|
|
36
37
|
- [`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-)
|
|
38
|
+
- [`janusMail: clock must be a Clock — an object with a now function`](#janusmail-clock-must-be-a-clock--an-object-with-a-now-function)
|
|
37
39
|
- [`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)
|
|
38
40
|
|
|
39
41
|
**Sending**
|
|
40
42
|
- [`janusMail.<method>: <field> must be a string`](#janusmailmethod-field-must-be-a-string)
|
|
43
|
+
- [`janusMail.<method>: expiresAt must be a Date`](#janusmailmethod-expiresat-must-be-a-date)
|
|
44
|
+
- [`janusMail.<method>: clock.now() must answer a Date`](#janusmailmethod-clocknow-must-answer-a-date)
|
|
45
|
+
- [`janusMail.<method>: expiresAt is past — the link or code would not work`](#janusmailmethod-expiresat-is-past--the-link-or-code-would-not-work)
|
|
46
|
+
- [`This link expires in 1 hour.` — in the wrong language, or shorter than the `tokens` TTL](#this-link-expires-in-1-hour--in-the-wrong-language-or-shorter-than-the-tokens-ttl)
|
|
41
47
|
- [`MAIL_REFUSED` — `render: <email>: link must be an http:, https: or mailto: URL`](#mail_refused--render-email-link-must-be-an-http-https-or-mailto-url)
|
|
42
48
|
- [`janusMail.<method>: links.<call> must answer a string`](#janusmailmethod-linkscall-must-answer-a-string)
|
|
43
49
|
- [`render: <email>: link must be a string or a finite number`](#render-email-link-must-be-a-string-or-a-finite-number)
|
|
@@ -48,6 +54,7 @@ How the messages are shaped:
|
|
|
48
54
|
|
|
49
55
|
**Compile errors**
|
|
50
56
|
- [`TS2345: Argument of type '(IssuedToken & { user: … }) | null' is not assignable to parameter of type 'IssuedToken'.`](#ts2345-argument-of-type-issuedtoken---user----null-is-not-assignable-to-parameter-of-type-issuedtoken)
|
|
57
|
+
- [`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)
|
|
51
58
|
- [`TS2739: Type '{ … }' is missing the following properties from type 'JanusMailTemplates<…>'`](#ts2739-type----is-missing-the-following-properties-from-type-janusmailtemplates)
|
|
52
59
|
- [`TS2322: Type '"de"' is not assignable to type '"en" | "fr"'.`](#ts2322-type-de-is-not-assignable-to-type-en--fr)
|
|
53
60
|
|
|
@@ -115,6 +122,19 @@ links: {
|
|
|
115
122
|
`locales: []`, `locales: 'en'`, or a list holding something that is not a
|
|
116
123
|
non-empty string. Leave it out for `['en', 'fr']`.
|
|
117
124
|
|
|
125
|
+
### `janusMail: locales must be BCP 47 language tags, as 'fr-CA'`
|
|
126
|
+
|
|
127
|
+
A `TypeError` when `janusMail()` is called: a locale in `locales` is one
|
|
128
|
+
`Intl` refuses — `de_DE` with an underscore, a grandfathered `i-klingon`, a
|
|
129
|
+
bare private-use `x-…`. The expiry is formatted by `Intl` in the locale
|
|
130
|
+
picked, so a locale it cannot parse would fail at the first send; it is
|
|
131
|
+
refused here instead. Write a tag `Intl` takes — `de-DE`, not `de_DE`; `tlh`,
|
|
132
|
+
not `i-klingon`:
|
|
133
|
+
|
|
134
|
+
```ts
|
|
135
|
+
janusMail({ mailer, from, brand: 'Acme', links, locales: ['en', 'fr', 'de-DE'], templates });
|
|
136
|
+
```
|
|
137
|
+
|
|
118
138
|
### `janusMail: locales holds the same locale twice`
|
|
119
139
|
|
|
120
140
|
`['en', 'en']`. List each once.
|
|
@@ -169,6 +189,18 @@ out: the defaults could not render them in that locale. Pass all five — see
|
|
|
169
189
|
[Adding a locale](guide/locales.md#adding-a-locale) — or drop the locale.
|
|
170
190
|
The message lists the templates missing.
|
|
171
191
|
|
|
192
|
+
### `janusMail: clock must be a Clock — an object with a now function`
|
|
193
|
+
|
|
194
|
+
`clock` is `@nxgt/janus`'s `Clock`, `{ now(): Date }` — pass the one given to
|
|
195
|
+
`janus({ clock })`, not `Date.now` or `() => new Date()`:
|
|
196
|
+
|
|
197
|
+
```ts
|
|
198
|
+
import { fixedClock } from '@nxgt/janus';
|
|
199
|
+
|
|
200
|
+
const clock = fixedClock(Date.UTC(2026, 0, 1));
|
|
201
|
+
janusMail({ mailer, from, brand: 'Acme', links, clock });
|
|
202
|
+
```
|
|
203
|
+
|
|
172
204
|
## Sending
|
|
173
205
|
|
|
174
206
|
### `janusMail.<method>: <field> must be a string`
|
|
@@ -186,7 +218,61 @@ if (issued !== null) await mail.resetPassword(issued, { name: issued.user.name }
|
|
|
186
218
|
|
|
187
219
|
`issued` needs `token` and `email` (`code` and `email` for `signInCode`), and
|
|
188
220
|
the recipient its `name` — a user whose schema has no name passes one
|
|
189
|
-
anyway: `{ name: user.email }`.
|
|
221
|
+
anyway: `{ name: user.email }`. `janusMail.verifyEmail: expiresIn must be a
|
|
222
|
+
string` is the third argument's `expiresIn` given as something else — a
|
|
223
|
+
number of seconds: pass the text to show, `{ expiresIn: '1 hour' }`, or
|
|
224
|
+
leave it out.
|
|
225
|
+
|
|
226
|
+
### `janusMail.<method>: expiresAt must be a Date`
|
|
227
|
+
|
|
228
|
+
A `TypeError` from `verifyEmail`, `resetPassword` or `signInCode`: the e-mail
|
|
229
|
+
says how long its link or code lasts, from `issued.expiresAt`, and that is
|
|
230
|
+
not a valid `Date`. The usual cause is a flow's answer that went through
|
|
231
|
+
JSON — a job queue, a cache — where a `Date` becomes a string. Nothing is
|
|
232
|
+
sent. Revive it when the job runs, or say the expiry yourself:
|
|
233
|
+
|
|
234
|
+
```ts
|
|
235
|
+
const issued = { ...job.issued, expiresAt: new Date(job.issued.expiresAt) };
|
|
236
|
+
await mail.verifyEmail(issued, { name: job.name });
|
|
237
|
+
// or
|
|
238
|
+
await mail.verifyEmail(job.issued, { name: job.name }, { expiresIn: '1 day' });
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
### `janusMail.<method>: clock.now() must answer a Date`
|
|
242
|
+
|
|
243
|
+
A `TypeError` from `verifyEmail`, `resetPassword` or `signInCode`: the
|
|
244
|
+
`clock` given to `janusMail()` has a `now` that answered something else — a
|
|
245
|
+
number from `Date.now()`, a string, an invalid `Date`. Nothing is sent.
|
|
246
|
+
`clock` is `@nxgt/janus`'s `Clock`: pass the one `janus({ clock })` was
|
|
247
|
+
given, or `{ now: () => new Date() }`.
|
|
248
|
+
|
|
249
|
+
### `janusMail.<method>: expiresAt is past — the link or code would not work`
|
|
250
|
+
|
|
251
|
+
A `TypeError`: the send came after the link or the code expired — a queue
|
|
252
|
+
that waited too long, an `expiresAt` from another record, or, in tests,
|
|
253
|
+
`janus({ clock: fixedClock(...) })` without the same `clock` given to
|
|
254
|
+
`janusMail()`, so a date in the clock's past is measured against today. Nothing is
|
|
255
|
+
sent: an e-mail whose link fails helps nobody. Issue a new one
|
|
256
|
+
(`auth.verifyEmail.send(user)`, `auth.signInCode.request(email)`) and send
|
|
257
|
+
that; if a queue can hold a send that long, lengthen the `tokens` TTL in
|
|
258
|
+
`janus({ tokens })`.
|
|
259
|
+
|
|
260
|
+
### `This link expires in 1 hour.` — in the wrong language, or shorter than the `tokens` TTL
|
|
261
|
+
|
|
262
|
+
`expiresIn` is formatted by the runtime's `Intl.NumberFormat`, in the locale
|
|
263
|
+
picked for the recipient. A locale the runtime has no data for — a Node
|
|
264
|
+
built with `small-icu`, a locale of your own — is formatted in the
|
|
265
|
+
runtime's default language. The time left is measured against
|
|
266
|
+
`janusMail({ clock })`, rounded to the minute, then **down** to the largest
|
|
267
|
+
whole unit: 90 minutes is "1 hour", 36 hours "1 day".
|
|
268
|
+
For another wording, pass it:
|
|
269
|
+
|
|
270
|
+
```ts
|
|
271
|
+
await mail.resetPassword(issued, { name, locale: 'de' }, { expiresIn: '90 Minuten' });
|
|
272
|
+
```
|
|
273
|
+
|
|
274
|
+
French puts a no-break space between the number and some units (`1 heure`,
|
|
275
|
+
per CLDR): compare with `\s` in a test, not a plain space.
|
|
190
276
|
|
|
191
277
|
### `MAIL_REFUSED` — `render: <email>: link must be an http:, https: or mailto: URL`
|
|
192
278
|
|
|
@@ -300,7 +386,15 @@ return new Response(null, { status: 202 });
|
|
|
300
386
|
|
|
301
387
|
The same holds for `signInCode.request`, whose answer passed unchecked reads
|
|
302
388
|
`Argument of type 'IssuedCode<…> | null' is not assignable to parameter of
|
|
303
|
-
type 'Pick<IssuedCode<unknown>, "email" | "code">'`.
|
|
389
|
+
type 'Pick<IssuedCode<unknown>, "email" | "code" | "expiresAt">'`.
|
|
390
|
+
|
|
391
|
+
### `TS2345: … Types of property 'expiresAt' are incompatible. Type 'string' is not assignable to type 'Date'.`
|
|
392
|
+
|
|
393
|
+
A flow's answer that went through JSON, typed as it came out. Revive the
|
|
394
|
+
date — `{ ...issued, expiresAt: new Date(issued.expiresAt) }` — or pass the
|
|
395
|
+
expiry as text in the third argument. A sign-in code built by hand without
|
|
396
|
+
`expiresAt` reads `Property 'expiresAt' is missing`: pass the flow's answer
|
|
397
|
+
whole.
|
|
304
398
|
|
|
305
399
|
### `TS2739: Type '{ … }' is missing the following properties from type 'JanusMailTemplates<…>'`
|
|
306
400
|
|
|
@@ -28,8 +28,8 @@ font-weight:600;color:#020918;color:lab(2.35721% .367686 -8.51797);">Reset your
|
|
|
28
28
|
<!--[if mso]><i style="mso-font-width:114%;mso-text-raise:19px;" hidden> </i>
|
|
29
29
|
<![endif]--> <span style="mso-text-raise:10px;">Choose a new password</span>
|
|
30
30
|
<!--[if mso]><i style="mso-font-width:114%;" hidden> ​</i>
|
|
31
|
-
<![endif]--></a></div><div role="separator" style="line-height:24px;">‍</div><p style="margin:0 0 16px;font-size:12px;line-height:16px;color:#62748e;color:lab(48.0876% -2.03595 -16.5814);">If the button does not work, open this link: <a href="{{ link }}" style="color:#54a2ff;color:lab(65.0361% -1.42065 -56.9802);text-decoration:underline;">{{
|
|
32
|
-
ignore this e-mail.</p>
|
|
31
|
+
<![endif]--></a></div><div role="separator" style="line-height:24px;">‍</div><p 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 style="margin:0 0 16px;font-size:12px;line-height:16px;color:#62748e;color:lab(48.0876% -2.03595 -16.5814);">If the button does not work, open this link: <a href="{{ link }}" style="color:#54a2ff;color:lab(65.0361% -1.42065 -56.9802);text-decoration:underline;">{{
|
|
32
|
+
link }}</a></p><p style="margin:0 0 16px;font-size:10px;color:#62748e;color:lab(48.0876% -2.03595 -16.5814);">If you did not ask for this, you can ignore this e-mail.</p>
|
|
33
33
|
</td></tr><tr><td style="padding-left:24px;padding-right:24px;padding-top:24px;text-align:center;font-size:12px;line-height:16px;color:#62748e;color:lab(48.0876% -2.03595 -16.5814);"><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>
|
|
34
34
|
</td></tr></table></div>
|
|
35
35
|
<!--[if mso]>
|
|
@@ -12,10 +12,11 @@ Choose a new password
|
|
|
12
12
|
|
|
13
13
|
{{ link }}
|
|
14
14
|
|
|
15
|
+
This link expires in {{ expiresIn }}.
|
|
16
|
+
|
|
15
17
|
If the button does not work, open this link: {{ link }}
|
|
16
18
|
|
|
17
|
-
If you did not ask for this, you can
|
|
18
|
-
ignore this e-mail.
|
|
19
|
+
If you did not ask for this, you can ignore this e-mail.
|
|
19
20
|
|
|
20
21
|
You received this e-mail because you have an account with {{ brand }}.
|
|
21
22
|
|
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
</td></tr><tr><td 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,0,0,.1);"><h1 style="margin:0 0 16px;font-size:24px;line-height:32px;
|
|
26
26
|
font-weight:600;color:#020918;color:lab(2.35721% .367686 -8.51797);">Your sign-in code</h1><p style="margin:0 0 16px;font-size:14px;line-height:20px;color:#020918;color:lab(2.35721% .367686 -8.51797);">Enter this code to sign in to {{ brand }}. It works once.</p>
|
|
27
27
|
<table role="presentation" cellpadding="0" cellspacing="0" style="margin-bottom:16px;width:100%;"><tr><td style="border-radius:10px;background-color:#f1f5f9;background-color:lab(96.286% -.852436 -2.46847);padding:16px 24px;text-align:center;font-family:ui-monospace,Menlo,Consolas,monospace;font-size:30px;line-height:36px;font-weight:600;letter-spacing:.3em;color:#020918;color:lab(2.35721% .367686 -8.51797);">{{ code }}
|
|
28
|
-
</td></tr></table><p style="margin:0 0 16px;font-size:10px;color:#62748e;color:lab(48.0876% -2.03595 -16.5814);">If you did not try to sign in, you can ignore this e-mail: no one can sign in without the code.</p>
|
|
28
|
+
</td></tr></table><p style="margin:0 0 16px;font-size:12px;line-height:16px;color:#020918;color:lab(2.35721% .367686 -8.51797);">This code expires in {{ expiresIn }}.</p><p style="margin:0 0 16px;font-size:10px;color:#62748e;color:lab(48.0876% -2.03595 -16.5814);">If you did not try to sign in, you can ignore this e-mail: no one can sign in without the code.</p>
|
|
29
29
|
</td></tr><tr><td style="padding-left:24px;padding-right:24px;padding-top:24px;text-align:center;font-size:12px;line-height:16px;color:#62748e;color:lab(48.0876% -2.03595 -16.5814);"><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>
|
|
30
30
|
</td></tr></table></div>
|
|
31
31
|
<!--[if mso]>
|
|
@@ -8,6 +8,8 @@ Enter this code to sign in to {{ brand }}. It works once.
|
|
|
8
8
|
|
|
9
9
|
{{ code }}
|
|
10
10
|
|
|
11
|
+
This code expires in {{ expiresIn }}.
|
|
12
|
+
|
|
11
13
|
If you did not try to sign in, you can ignore this e-mail: no one can sign in without the code.
|
|
12
14
|
|
|
13
15
|
You received this e-mail because you have an account with {{ brand }}.
|
|
@@ -28,8 +28,8 @@ background-color:#27272a;padding:10px 16px;font-family:ui-sans-serif,system-ui,-
|
|
|
28
28
|
<!--[if mso]><i style="mso-font-width:114%;mso-text-raise:19px;" hidden> </i>
|
|
29
29
|
<![endif]--> <span style="mso-text-raise:10px;">Confirm my address</span>
|
|
30
30
|
<!--[if mso]><i style="mso-font-width:114%;" hidden> ​</i>
|
|
31
|
-
<![endif]--></a></div><div role="separator" style="line-height:24px;">‍</div><p style="margin:0 0 16px;font-size:12px;line-height:16px;color:#62748e;color:lab(48.0876% -2.03595 -16.5814);">If the button does not work, open this link: <a href="{{ link }}" style="color:#54a2ff;color:lab(65.0361% -1.42065 -56.9802);text-decoration:underline;">{{
|
|
32
|
-
ignore this e-mail.</p>
|
|
31
|
+
<![endif]--></a></div><div role="separator" style="line-height:24px;">‍</div><p 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 style="margin:0 0 16px;font-size:12px;line-height:16px;color:#62748e;color:lab(48.0876% -2.03595 -16.5814);">If the button does not work, open this link: <a href="{{ link }}" style="color:#54a2ff;color:lab(65.0361% -1.42065 -56.9802);text-decoration:underline;">{{
|
|
32
|
+
link }}</a></p><p style="margin:0 0 16px;font-size:10px;color:#62748e;color:lab(48.0876% -2.03595 -16.5814);">If you did not ask for this, you can ignore this e-mail.</p>
|
|
33
33
|
</td></tr><tr><td style="padding-left:24px;padding-right:24px;padding-top:24px;text-align:center;font-size:12px;line-height:16px;color:#62748e;color:lab(48.0876% -2.03595 -16.5814);"><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>
|
|
34
34
|
</td></tr></table></div>
|
|
35
35
|
<!--[if mso]>
|
|
@@ -12,10 +12,11 @@ Confirm my address
|
|
|
12
12
|
|
|
13
13
|
{{ link }}
|
|
14
14
|
|
|
15
|
+
This link expires in {{ expiresIn }}.
|
|
16
|
+
|
|
15
17
|
If the button does not work, open this link: {{ link }}
|
|
16
18
|
|
|
17
|
-
If you did not ask for this, you can
|
|
18
|
-
ignore this e-mail.
|
|
19
|
+
If you did not ask for this, you can ignore this e-mail.
|
|
19
20
|
|
|
20
21
|
You received this e-mail because you have an account with {{ brand }}.
|
|
21
22
|
|
|
@@ -27,8 +27,8 @@ style="display:inline-block;border-radius:9999px;background-color:#27272a;paddin
|
|
|
27
27
|
<!--[if mso]><i style="mso-font-width:114%;mso-text-raise:19px;" hidden> </i>
|
|
28
28
|
<![endif]--> <span style="mso-text-raise:10px;">Choisir un nouveau mot de passe</span>
|
|
29
29
|
<!--[if mso]><i style="mso-font-width:114%;" hidden> ​</i>
|
|
30
|
-
<![endif]--></a></div><div role="separator" style="line-height:24px;">‍</div><p style="margin:0 0 16px;font-size:12px;line-height:16px;color:#62748e;color:lab(48.0876% -2.03595 -16.5814);">Si le bouton ne fonctionne pas, ouvrez ce lien : <a href="{{ link }}" style="color:#54a2ff;color:lab(65.0361% -1.42065 -56.9802);text-decoration:
|
|
31
|
-
de cette demande, vous pouvez ignorer cet e-mail.</p>
|
|
30
|
+
<![endif]--></a></div><div role="separator" style="line-height:24px;">‍</div><p style="margin:0 0 16px;font-size:12px;line-height:16px;color:#020918;color:lab(2.35721% .367686 -8.51797);">Ce lien expire dans {{ expiresIn }}.</p><p style="margin:0 0 16px;font-size:12px;line-height:16px;color:#62748e;color:lab(48.0876% -2.03595 -16.5814);">Si le bouton ne fonctionne pas, ouvrez ce lien : <a href="{{ link }}" style="color:#54a2ff;color:lab(65.0361% -1.42065 -56.9802);text-decoration:
|
|
31
|
+
underline;">{{ link }}</a></p><p style="margin:0 0 16px;font-size:10px;color:#62748e;color:lab(48.0876% -2.03595 -16.5814);">Si vous n'êtes pas à l'origine de cette demande, vous pouvez ignorer cet e-mail.</p>
|
|
32
32
|
</td></tr><tr><td style="padding-left:24px;padding-right:24px;padding-top:24px;text-align:center;font-size:12px;line-height:16px;color:#62748e;color:lab(48.0876% -2.03595 -16.5814);"><p style="margin:0 0 8px;">Vous recevez cet e-mail parce que vous avez un compte chez {{ brand }}.</p><p style="margin:0;">{{ brand }}</p>
|
|
33
33
|
</td></tr></table></div>
|
|
34
34
|
<!--[if mso]>
|
|
@@ -12,10 +12,11 @@ Choisir un nouveau mot de passe
|
|
|
12
12
|
|
|
13
13
|
{{ link }}
|
|
14
14
|
|
|
15
|
+
Ce lien expire dans {{ expiresIn }}.
|
|
16
|
+
|
|
15
17
|
Si le bouton ne fonctionne pas, ouvrez ce lien : {{ link }}
|
|
16
18
|
|
|
17
|
-
Si vous n'êtes pas à l'origine
|
|
18
|
-
de cette demande, vous pouvez ignorer cet e-mail.
|
|
19
|
+
Si vous n'êtes pas à l'origine de cette demande, vous pouvez ignorer cet e-mail.
|
|
19
20
|
|
|
20
21
|
Vous recevez cet e-mail parce que vous avez un compte chez {{ brand }}.
|
|
21
22
|
|
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
</td></tr><tr><td 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,0,0,.1);"><h1 style="margin:0 0 16px;font-size:24px;line-height:32px;
|
|
26
26
|
font-weight:600;color:#020918;color:lab(2.35721% .367686 -8.51797);">Votre code de connexion</h1><p style="margin:0 0 16px;font-size:14px;line-height:20px;color:#020918;color:lab(2.35721% .367686 -8.51797);">Saisissez ce code pour vous connecter à {{ brand }}. Il ne fonctionne qu'une fois.</p>
|
|
27
27
|
<table role="presentation" cellpadding="0" cellspacing="0" style="margin-bottom:16px;width:100%;"><tr><td style="border-radius:10px;background-color:#f1f5f9;background-color:lab(96.286% -.852436 -2.46847);padding:16px 24px;text-align:center;font-family:ui-monospace,Menlo,Consolas,monospace;font-size:30px;line-height:36px;font-weight:600;letter-spacing:.3em;color:#020918;color:lab(2.35721% .367686 -8.51797);">{{ code }}
|
|
28
|
-
</td></tr></table><p style="margin:0 0 16px;font-size:10px;color:#62748e;color:lab(48.0876% -2.03595 -16.5814);">Si vous n'avez pas essayé de vous connecter, vous pouvez ignorer cet e-mail : personne ne peut se connecter sans ce code.</p>
|
|
28
|
+
</td></tr></table><p style="margin:0 0 16px;font-size:12px;line-height:16px;color:#020918;color:lab(2.35721% .367686 -8.51797);">Ce code expire dans {{ expiresIn }}.</p><p style="margin:0 0 16px;font-size:10px;color:#62748e;color:lab(48.0876% -2.03595 -16.5814);">Si vous n'avez pas essayé de vous connecter, vous pouvez ignorer cet e-mail : personne ne peut se connecter sans ce code.</p>
|
|
29
29
|
</td></tr><tr><td style="padding-left:24px;padding-right:24px;padding-top:24px;text-align:center;font-size:12px;line-height:16px;color:#62748e;color:lab(48.0876% -2.03595 -16.5814);"><p style="margin:0 0 8px;">Vous recevez cet e-mail parce que vous avez un compte chez {{ brand }}.</p><p style="margin:0;">{{ brand }}</p>
|
|
30
30
|
</td></tr></table></div>
|
|
31
31
|
<!--[if mso]>
|
|
@@ -8,6 +8,8 @@ Saisissez ce code pour vous connecter à {{ brand }}. Il ne fonctionne qu'une fo
|
|
|
8
8
|
|
|
9
9
|
{{ code }}
|
|
10
10
|
|
|
11
|
+
Ce code expire dans {{ expiresIn }}.
|
|
12
|
+
|
|
11
13
|
Si vous n'avez pas essayé de vous connecter, vous pouvez ignorer cet e-mail : personne ne peut se connecter sans ce code.
|
|
12
14
|
|
|
13
15
|
Vous recevez cet e-mail parce que vous avez un compte chez {{ brand }}.
|
|
@@ -28,8 +28,8 @@ border-radius:9999px;background-color:#27272a;padding:10px 16px;font-family:ui-s
|
|
|
28
28
|
<!--[if mso]><i style="mso-font-width:114%;mso-text-raise:19px;" hidden> </i>
|
|
29
29
|
<![endif]--> <span style="mso-text-raise:10px;">Confirmer mon adresse</span>
|
|
30
30
|
<!--[if mso]><i style="mso-font-width:114%;" hidden> ​</i>
|
|
31
|
-
<![endif]--></a></div><div role="separator" style="line-height:24px;">‍</div><p style="margin:0 0 16px;font-size:12px;line-height:16px;color:#62748e;color:lab(48.0876% -2.03595 -16.5814);">Si le bouton ne fonctionne pas, ouvrez ce lien : <a href="{{ link }}" style="color:#54a2ff;color:lab(65.0361% -1.42065 -56.9802);text-decoration:
|
|
32
|
-
de cette demande, vous pouvez ignorer cet e-mail.</p>
|
|
31
|
+
<![endif]--></a></div><div role="separator" style="line-height:24px;">‍</div><p style="margin:0 0 16px;font-size:12px;line-height:16px;color:#020918;color:lab(2.35721% .367686 -8.51797);">Ce lien expire dans {{ expiresIn }}.</p><p style="margin:0 0 16px;font-size:12px;line-height:16px;color:#62748e;color:lab(48.0876% -2.03595 -16.5814);">Si le bouton ne fonctionne pas, ouvrez ce lien : <a href="{{ link }}" style="color:#54a2ff;color:lab(65.0361% -1.42065 -56.9802);text-decoration:
|
|
32
|
+
underline;">{{ link }}</a></p><p style="margin:0 0 16px;font-size:10px;color:#62748e;color:lab(48.0876% -2.03595 -16.5814);">Si vous n'êtes pas à l'origine de cette demande, vous pouvez ignorer cet e-mail.</p>
|
|
33
33
|
</td></tr><tr><td style="padding-left:24px;padding-right:24px;padding-top:24px;text-align:center;font-size:12px;line-height:16px;color:#62748e;color:lab(48.0876% -2.03595 -16.5814);"><p style="margin:0 0 8px;">Vous recevez cet e-mail parce que vous avez un compte chez {{ brand }}.</p><p style="margin:0;">{{ brand }}</p>
|
|
34
34
|
</td></tr></table></div>
|
|
35
35
|
<!--[if mso]>
|
|
@@ -12,10 +12,11 @@ Confirmer mon adresse
|
|
|
12
12
|
|
|
13
13
|
{{ link }}
|
|
14
14
|
|
|
15
|
+
Ce lien expire dans {{ expiresIn }}.
|
|
16
|
+
|
|
15
17
|
Si le bouton ne fonctionne pas, ouvrez ce lien : {{ link }}
|
|
16
18
|
|
|
17
|
-
Si vous n'êtes pas à l'origine
|
|
18
|
-
de cette demande, vous pouvez ignorer cet e-mail.
|
|
19
|
+
Si vous n'êtes pas à l'origine de cette demande, vous pouvez ignorer cet e-mail.
|
|
19
20
|
|
|
20
21
|
Vous recevez cet e-mail parce que vous avez un compte chez {{ brand }}.
|
|
21
22
|
|
package/mails/mail-manifest.json
CHANGED
|
@@ -58,6 +58,7 @@
|
|
|
58
58
|
"reset-password": {
|
|
59
59
|
"variables": [
|
|
60
60
|
"brand",
|
|
61
|
+
"expiresIn",
|
|
61
62
|
"link",
|
|
62
63
|
"name"
|
|
63
64
|
],
|
|
@@ -82,7 +83,8 @@
|
|
|
82
83
|
"sign-in-code": {
|
|
83
84
|
"variables": [
|
|
84
85
|
"brand",
|
|
85
|
-
"code"
|
|
86
|
+
"code",
|
|
87
|
+
"expiresIn"
|
|
86
88
|
],
|
|
87
89
|
"urlVariables": [],
|
|
88
90
|
"subject": {
|
|
@@ -103,6 +105,7 @@
|
|
|
103
105
|
"verify-email": {
|
|
104
106
|
"variables": [
|
|
105
107
|
"brand",
|
|
108
|
+
"expiresIn",
|
|
106
109
|
"link",
|
|
107
110
|
"name"
|
|
108
111
|
],
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nxgt/janus-mail",
|
|
3
|
-
"version": "0.1
|
|
3
|
+
"version": "0.2.1",
|
|
4
4
|
"description": "The e-mails of @nxgt/janus's flows — e-mail verification, password reset, sign-in code, password changed, e-mail changed — rendered in English and French from a prebuilt Maizzle build, with your brand, sent through any @nxgt/mail transport",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -54,11 +54,11 @@
|
|
|
54
54
|
"@maizzle/framework": "6.1.7",
|
|
55
55
|
"@maizzle/tailwindcss": "1.5.6",
|
|
56
56
|
"@nxgt/janus": "^0.8.8",
|
|
57
|
-
"@nxgt/mail": "0.
|
|
58
|
-
"@nxgt/mail-config": "0.2.
|
|
59
|
-
"@nxgt/mail-i18n": "0.
|
|
60
|
-
"@nxgt/mail-presets": "0.
|
|
61
|
-
"@nxgt/mail-ui": "0.
|
|
57
|
+
"@nxgt/mail": "0.6.0",
|
|
58
|
+
"@nxgt/mail-config": "0.2.1",
|
|
59
|
+
"@nxgt/mail-i18n": "0.4.0",
|
|
60
|
+
"@nxgt/mail-presets": "0.3.0",
|
|
61
|
+
"@nxgt/mail-ui": "0.3.0",
|
|
62
62
|
"@types/bun": "^1.4.2",
|
|
63
63
|
"vue": "3.5.43",
|
|
64
64
|
"vue-tsc": "^3.3.11"
|