@nxgt/janus-mail 0.4.1 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +78 -25
- package/dist/codes-left.d.ts +6 -0
- package/dist/codes-left.d.ts.map +1 -0
- package/dist/generated/codes-left.d.ts +3 -0
- package/dist/generated/codes-left.d.ts.map +1 -0
- package/dist/generated/mail.d.ts +7 -0
- package/dist/generated/mail.d.ts.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +130 -21
- package/dist/index.js.map +11 -6
- package/dist/janus-mail.d.ts +1 -0
- package/dist/janus-mail.d.ts.map +1 -1
- package/dist/links.d.ts +10 -0
- package/dist/links.d.ts.map +1 -0
- package/dist/options.d.ts.map +1 -1
- package/dist/recovery-mail.d.ts +5 -0
- package/dist/recovery-mail.d.ts.map +1 -0
- package/dist/refuse.d.ts +3 -0
- package/dist/refuse.d.ts.map +1 -0
- package/dist/template-types.d.ts +15 -2
- package/dist/template-types.d.ts.map +1 -1
- package/dist/templates.d.ts +1 -1
- package/dist/templates.d.ts.map +1 -1
- package/dist/types.d.ts +25 -0
- package/dist/types.d.ts.map +1 -1
- package/docs/README.md +4 -3
- package/docs/guide/building.md +58 -9
- package/docs/guide/locales.md +13 -4
- package/docs/guide/sending.md +61 -7
- package/docs/guide/templates.md +41 -16
- package/docs/roadmap.md +15 -1
- package/docs/troubleshooting.md +114 -21
- package/mails/en/email-changed.html +5 -5
- package/mails/en/password-changed.html +5 -5
- package/mails/en/recovery-code-used.html +53 -0
- package/mails/en/recovery-code-used.txt +23 -0
- package/mails/en/reset-password.html +6 -6
- package/mails/en/sign-in-code.html +3 -3
- package/mails/en/two-factor-disabled.html +5 -5
- package/mails/en/two-factor-enabled.html +5 -5
- package/mails/en/verify-email.html +6 -6
- package/mails/en/welcome.html +5 -5
- package/mails/fr/email-changed.html +5 -5
- package/mails/fr/password-changed.html +5 -5
- package/mails/fr/recovery-code-used.html +52 -0
- package/mails/fr/recovery-code-used.txt +23 -0
- package/mails/fr/reset-password.html +6 -6
- package/mails/fr/sign-in-code.html +3 -3
- package/mails/fr/two-factor-disabled.html +5 -5
- package/mails/fr/two-factor-enabled.html +5 -5
- package/mails/fr/verify-email.html +6 -6
- package/mails/fr/welcome.html +5 -5
- package/mails/mail-manifest.json +26 -0
- package/package.json +11 -10
package/README.md
CHANGED
|
@@ -3,7 +3,8 @@
|
|
|
3
3
|
The e-mails of [`@nxgt/janus`](https://www.npmjs.com/package/@nxgt/janus)'s
|
|
4
4
|
flows, ready to send: **e-mail verification, password reset, sign-in code,
|
|
5
5
|
password changed, e-mail changed, two-factor authentication turned on or
|
|
6
|
-
off, and a welcome to a new user**, in English and
|
|
6
|
+
off, a recovery code used, and a welcome to a new user**, in English and
|
|
7
|
+
French, with your brand
|
|
7
8
|
in them. `@nxgt/janus` sends no e-mail — its flows answer what to send — and
|
|
8
9
|
this package turns that answer into an e-mail and hands it to any
|
|
9
10
|
[`@nxgt/mail`](https://www.npmjs.com/package/@nxgt/mail) transport.
|
|
@@ -21,6 +22,7 @@ const mail = janusMail({
|
|
|
21
22
|
resetPassword: (token) => `https://acme.example/reset?token=${token}`,
|
|
22
23
|
secureAccount: () => 'https://acme.example/account/security',
|
|
23
24
|
getStarted: () => 'https://acme.example/',
|
|
25
|
+
recoveryCodes: () => 'https://acme.example/account/recovery-codes', // optional
|
|
24
26
|
},
|
|
25
27
|
});
|
|
26
28
|
|
|
@@ -43,9 +45,9 @@ bun add @nxgt/janus-mail @nxgt/mail @nxgt/janus
|
|
|
43
45
|
bun add -d typescript
|
|
44
46
|
```
|
|
45
47
|
|
|
46
|
-
Three peers, all required: `@nxgt/mail` (0.1 or later, below
|
|
47
|
-
transports
|
|
48
|
-
`@nxgt/janus` (0.
|
|
48
|
+
Three peers, all required: `@nxgt/mail` (0.1 or later, below 2 — 1.0 and
|
|
49
|
+
its transports included), which defines the `Mailer` port and the errors;
|
|
50
|
+
`@nxgt/janus` (0.11), whose flows' answers the methods take — types only,
|
|
49
51
|
nothing of it is loaded; and `typescript` (6). **No Maizzle, no Vue, no
|
|
50
52
|
Tailwind**: they run at this package's build, not in yours.
|
|
51
53
|
|
|
@@ -53,18 +55,20 @@ The `@nxgt/mail` floor is tested, not claimed: the package's specs and its
|
|
|
53
55
|
typecheck run on `@nxgt/mail` 0.1.0 as well, in the Floors job, on every CI
|
|
54
56
|
run.
|
|
55
57
|
|
|
56
|
-
It reads its prebuilt e-mails with `node:fs`: **Node
|
|
57
|
-
runtime.
|
|
58
|
+
It reads its prebuilt e-mails with `node:fs`: **Node (20 or later), Bun or
|
|
59
|
+
Deno**, not an edge runtime. Rebuilding the e-mails from this repository
|
|
60
|
+
needs Node `^22.22.3`, `^24.15.0` or `>=26`, for Maizzle — see
|
|
61
|
+
[Building](docs/guide/building.md); installing the package needs no build. Like `@nxgt/janus`, it expects `"moduleResolution": "bundler"`.
|
|
58
62
|
|
|
59
63
|
## API
|
|
60
64
|
|
|
61
65
|
| Export | What it is |
|
|
62
66
|
| --- | --- |
|
|
63
|
-
| `janusMail(options)` | The e-mails, over your mailer: `verifyEmail`, `resetPassword`, `signInCode`, `passwordChanged`, `emailChanged`, `twoFactorEnabled`, `twoFactorDisabled`, `welcome`, and `templates` and `locales`. Options: `mailer`, `from`, `replyTo?`, `brand`, `links` (`verifyEmail`, `resetPassword`, `secureAccount`, `getStarted`), `locales?`, `fallbackLocale?`, `templates?`, `clock?`. A wrong option is a bare `TypeError`, thrown here |
|
|
64
|
-
| `janusTemplates()` | The
|
|
67
|
+
| `janusMail(options)` | The e-mails, over your mailer: `verifyEmail`, `resetPassword`, `signInCode`, `passwordChanged`, `emailChanged`, `twoFactorEnabled`, `twoFactorDisabled`, `recoveryCodeUsed`, `welcome`, and `templates` and `locales`. Options: `mailer`, `from`, `replyTo?`, `brand`, `links` (`verifyEmail`, `resetPassword`, `secureAccount`, `getStarted`, and `recoveryCodes?`), `locales?`, `fallbackLocale?`, `templates?`, `clock?`. A wrong option is a bare `TypeError`, thrown here |
|
|
68
|
+
| `janusTemplates()` | The nine default templates alone, each `(variables & { locale }) => Rendered` |
|
|
65
69
|
| `JanusMail<L>`, `JanusMailOptions<L>` | What `janusMail()` answers and takes, for the locales `L` |
|
|
66
|
-
| `JanusMailTemplate<V, L>`, `JanusMailTemplates<L>`, `JanusMailTemplateName` | One template, the
|
|
67
|
-
| `JanusMailVariables` | What each template is given: `brand`, and `name`, `link`, `code`, `expiresIn` or `
|
|
70
|
+
| `JanusMailTemplate<V, L>`, `JanusMailTemplates<L>`, `JanusMailTemplateName` | One template, the nine of them, and their names |
|
|
71
|
+
| `JanusMailVariables` | What each template is given: `brand`, and `name`, `link`, `code`, `expiresIn`, `newEmail`, `when` or `recoveryCodesLeft` as its e-mail needs |
|
|
68
72
|
| `JanusMailSendOptions` | The third argument of `verifyEmail`, `resetPassword` and `signInCode`: `{ expiresIn? }`, the expiry as text, over the one derived |
|
|
69
73
|
| `JanusMailLocale` | `'en' \| 'fr'`: the locales the defaults are built in |
|
|
70
74
|
| `JanusMailLinks`, `Recipient` | The `links` option; who an e-mail is for — `{ name, locale? }` |
|
|
@@ -73,8 +77,8 @@ Each method answers the mailer's `SentMail` and **rejects with the mailer's
|
|
|
73
77
|
`MailFailure` or `MailRefused`, untouched**: this package defines no error
|
|
74
78
|
class. To retry a `MailFailure` or trace each send, wrap the mailer you pass:
|
|
75
79
|
`withMailTelemetry(withRetry(mailer), { transport })` — `withRetry` from
|
|
76
|
-
`@nxgt/mail` 0.8, `withMailTelemetry` from `@nxgt/mail/telemetry`
|
|
77
|
-
names it `withTelemetry`,
|
|
80
|
+
`@nxgt/mail` 0.8 or later, `withMailTelemetry` from `@nxgt/mail/telemetry`
|
|
81
|
+
0.9 or later (0.8 names it `withTelemetry`, which 1.0 removed); see
|
|
78
82
|
[Sending](docs/guide/sending.md#retrying-and-tracing-the-mailer).
|
|
79
83
|
|
|
80
84
|
| Method | Sends | To |
|
|
@@ -86,6 +90,7 @@ names it `withTelemetry`, a deprecated alias until 1.0); see
|
|
|
86
90
|
| `emailChanged(to)` | A notice naming `to.newEmail`, with `links.secureAccount()` | `to.formerEmail` |
|
|
87
91
|
| `twoFactorEnabled(to)` | A notice: two-factor authentication was turned on, with `links.secureAccount()` | `to.email` |
|
|
88
92
|
| `twoFactorDisabled(to)` | A notice: two-factor authentication was turned off, with `links.secureAccount()` | `to.email` |
|
|
93
|
+
| `recoveryCodeUsed(to, { when, recoveryCodesLeft })` | A notice: a recovery code was used, `when`, and how many are left — "You have 9 recovery codes left." — with `links.recoveryCodes()`, else `links.secureAccount()` | `to.email` |
|
|
89
94
|
| `welcome(to)` | A welcome to a new user — "Welcome, Ada" — with `links.getStarted()` | `to.email` |
|
|
90
95
|
|
|
91
96
|
The three e-mails of a link or a code say how long it lasts — "This link
|
|
@@ -200,6 +205,36 @@ out before the address is verified; to welcome proven addresses only, send
|
|
|
200
205
|
it on `user.emailVerified` instead — see
|
|
201
206
|
[Sending](docs/guide/sending.md#welcometo).
|
|
202
207
|
|
|
208
|
+
### Telling a user a recovery code was used
|
|
209
|
+
|
|
210
|
+
`@nxgt/janus` sends `user.recoveryCodeUsed` once `secondFactor.recover`
|
|
211
|
+
spent a code: a sign-in without the user's phone. The event names the user
|
|
212
|
+
by id only, so read the user, and the codes left with
|
|
213
|
+
`secondFactor.recoveryCodesLeft` (`@nxgt/janus` 0.11):
|
|
214
|
+
|
|
215
|
+
```ts
|
|
216
|
+
const auth = janus({
|
|
217
|
+
...config,
|
|
218
|
+
async events(event) {
|
|
219
|
+
if (event.type === 'user.recoveryCodeUsed') {
|
|
220
|
+
const user = await auth.get(event.userId);
|
|
221
|
+
const recoveryCodesLeft = await auth.secondFactor.recoveryCodesLeft(user);
|
|
222
|
+
if (recoveryCodesLeft === null) return; // the factor was turned off since
|
|
223
|
+
const when = new Intl.DateTimeFormat(user.locale, { dateStyle: 'long', timeStyle: 'short', timeZone: 'Europe/Paris' })
|
|
224
|
+
.format(event.occurredAt);
|
|
225
|
+
await mail.recoveryCodeUsed({ name: user.name, locale: user.locale, email: user.email }, { when, recoveryCodesLeft });
|
|
226
|
+
}
|
|
227
|
+
},
|
|
228
|
+
});
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
`when` is text: format it in the recipient's locale and time zone.
|
|
232
|
+
`recoveryCodesLeft` is the count, written as a sentence in the recipient's
|
|
233
|
+
locale — "You have 1 recovery code left.", "Il ne vous reste aucun code de
|
|
234
|
+
récupération." — or the sentence itself, as text. The button links to
|
|
235
|
+
`links.recoveryCodes()`, where the user regenerates their codes, and to
|
|
236
|
+
`links.secureAccount()` when you give no `recoveryCodes`.
|
|
237
|
+
|
|
203
238
|
### Replacing a template
|
|
204
239
|
|
|
205
240
|
Any template can be your own function — React Email, a string, another
|
|
@@ -245,10 +280,19 @@ writes that name in its subject — "Welcome, <anything>" — so on
|
|
|
245
280
|
`user.created` anyone can make your brand send it to any address. Once the
|
|
246
281
|
address is proven, only its owner receives it.
|
|
247
282
|
|
|
248
|
-
**Every link is required, `getStarted` included
|
|
249
|
-
has no `getStarted`: a compile error on `links`, and
|
|
250
|
-
`janusMail: links.getStarted must be a function`. Add it even
|
|
251
|
-
send the welcome.
|
|
283
|
+
**Every link is required, `getStarted` included — but `recoveryCodes`.**
|
|
284
|
+
`links` written for 0.3 has no `getStarted`: a compile error on `links`, and
|
|
285
|
+
in JavaScript `janusMail: links.getStarted must be a function`. Add it even
|
|
286
|
+
if you never send the welcome. `recoveryCodes` is optional: without it,
|
|
287
|
+
`recoveryCodeUsed` links to `secureAccount`.
|
|
288
|
+
|
|
289
|
+
**The recovery code event carries no count.** `user.recoveryCodeUsed` names
|
|
290
|
+
the user only; pass `recoveryCodesLeft` as
|
|
291
|
+
`auth.secondFactor.recoveryCodesLeft(user)` answers it, once checked for
|
|
292
|
+
`null` — a user whose factor was turned off since has no codes to count, and
|
|
293
|
+
`null` is a compile error here. With a locale the defaults are not built in,
|
|
294
|
+
pass the sentence rather than the count: the build has no plural for it, and
|
|
295
|
+
a count is a `TypeError` at send.
|
|
252
296
|
|
|
253
297
|
**`node:fs` means no edge runtime.** The default e-mails are read from the
|
|
254
298
|
package's `mails/` folder, on the first one sent. On an edge runtime, pass
|
|
@@ -277,8 +321,9 @@ and are refused with a `TypeError`; its fields (`signInCode = (variables) =>
|
|
|
277
321
|
…`) pass.
|
|
278
322
|
|
|
279
323
|
**A locale beyond `en` and `fr` needs every template.** The defaults are
|
|
280
|
-
built in those two only, so `locales: ['en', 'fr', 'de']` without all
|
|
281
|
-
templates is a compile error, and
|
|
324
|
+
built in those two only, so `locales: ['en', 'fr', 'de']` without all nine
|
|
325
|
+
templates — `recoveryCodeUsed` included, since 0.5 — is a compile error, and
|
|
326
|
+
a `TypeError` in JavaScript.
|
|
282
327
|
|
|
283
328
|
**The brand is text only.** `brand: 'Acme'` is written in the header, the
|
|
284
329
|
body and the footer, escaped: no logo, no link, no markup. For those, replace
|
|
@@ -335,8 +380,8 @@ The symptoms and fixes are in [troubleshooting](docs/troubleshooting.md).
|
|
|
335
380
|
|
|
336
381
|
## Type safety, counted
|
|
337
382
|
|
|
338
|
-
**
|
|
339
|
-
|
|
383
|
+
**Thirty-four plausible mistakes, thirty-four refused at compile time.**
|
|
384
|
+
Five files hold one `@ts-expect-error` per mistake, beside the calls that
|
|
340
385
|
must keep compiling:
|
|
341
386
|
[`test/types/send-refusals.ts`](test/types/send-refusals.ts) the eight of a
|
|
342
387
|
send, 1 to 8;
|
|
@@ -344,9 +389,11 @@ send, 1 to 8;
|
|
|
344
389
|
of `janusMail()`'s options, 9 to 20, beside adding a language with every
|
|
345
390
|
template;
|
|
346
391
|
[`test/types/expiry-refusals.ts`](test/types/expiry-refusals.ts) the five of
|
|
347
|
-
the expiry and the clock, 21 to 25;
|
|
392
|
+
the expiry and the clock, 21 to 25;
|
|
348
393
|
[`test/types/notice-refusals.ts`](test/types/notice-refusals.ts) the four of
|
|
349
|
-
the two-factor notices and the welcome, 26 to 29
|
|
394
|
+
the two-factor notices and the welcome, 26 to 29; and
|
|
395
|
+
[`test/types/recovery-refusals.ts`](test/types/recovery-refusals.ts) the
|
|
396
|
+
five of the recovery code notice, 30 to 34:
|
|
350
397
|
|
|
351
398
|
1. A sign-in code given to `verifyEmail`: it has no token.
|
|
352
399
|
2. A one-time token given to `signInCode`: it has no code.
|
|
@@ -377,9 +424,15 @@ the two-factor notices and the welcome, 26 to 29:
|
|
|
377
424
|
27. `twoFactorEnabled` given `emailChanged`'s `formerEmail` rather than `email`.
|
|
378
425
|
28. The `user.created` event itself given to `welcome`: it has neither a name nor an address.
|
|
379
426
|
29. `links` written before the welcome, without `getStarted`.
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
427
|
+
30. The `user.recoveryCodeUsed` event itself given to `recoveryCodeUsed`: it has neither a name nor an address.
|
|
428
|
+
31. `recoveryCodeUsed` without `when` and the count.
|
|
429
|
+
32. The count as `recoveryCodesLeft()` answered it, not checked for `null` first.
|
|
430
|
+
33. `when` given as the event's `Date` rather than the text in the recipient's locale and time zone.
|
|
431
|
+
34. `links.recoveryCodes` given as a URL rather than a function.
|
|
432
|
+
|
|
433
|
+
In JavaScript, 19 to 23, 26 to 28 and 30 to 33 are a `TypeError` at send
|
|
434
|
+
time instead, naming the call or the field, and 25, 29 and 34 one from
|
|
435
|
+
`janusMail()`.
|
|
383
436
|
|
|
384
437
|
[`test/types/variables.ts`](test/types/variables.ts) also holds
|
|
385
438
|
`JanusMailVariables` equal to the variables of the build: an e-mail that
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"codes-left.d.ts","sourceRoot":"","sources":["../src/codes-left.ts"],"names":[],"mappings":"AAcA;;;GAGG;AACH,wBAAgB,aAAa,CAC5B,MAAM,EAAE,MAAM,EACd,KAAK,EAAE,MAAM,GACX,MAAM,GAAG,SAAS,CASpB"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"codes-left.d.ts","sourceRoot":"","sources":["../../src/generated/codes-left.ts"],"names":[],"mappings":"AAGA,2GAA2G;AAC3G,eAAO,MAAM,UAAU,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,SAAS,CAAC,MAAM,GAAG,IAAI,CAAC,EAAE,CAAC,CAAC,CAAC,CA+BrG,CAAC"}
|
package/dist/generated/mail.d.ts
CHANGED
|
@@ -11,6 +11,13 @@ export interface MailEmails {
|
|
|
11
11
|
readonly link: string;
|
|
12
12
|
readonly name: string | number;
|
|
13
13
|
};
|
|
14
|
+
"recovery-code-used": {
|
|
15
|
+
readonly brand: string | number;
|
|
16
|
+
readonly link: string;
|
|
17
|
+
readonly name: string | number;
|
|
18
|
+
readonly recoveryCodesLeft: string | number;
|
|
19
|
+
readonly when: string | number;
|
|
20
|
+
};
|
|
14
21
|
"reset-password": {
|
|
15
22
|
readonly brand: string | number;
|
|
16
23
|
readonly expiresIn: string | number;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"mail.d.ts","sourceRoot":"","sources":["../../src/generated/mail.ts"],"names":[],"mappings":"AAGA,kFAAkF;AAClF,MAAM,WAAW,UAAU;IAC1B,eAAe,EAAE;QAAE,QAAQ,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAAC;QAAC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAAC;QAAC,QAAQ,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CAAA;KAAE,CAAC;IAChJ,kBAAkB,EAAE;QAAE,QAAQ,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAAC;QAAC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAAA;KAAE,CAAC;IAC/G,gBAAgB,EAAE;QAAE,QAAQ,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAAC;QAAC,QAAQ,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,CAAC;QAAC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAAA;KAAE,CAAC;IAClJ,cAAc,EAAE;QAAE,QAAQ,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAAC;QAAC,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAAC;QAAC,QAAQ,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,CAAA;KAAE,CAAC;IACzH,qBAAqB,EAAE;QAAE,QAAQ,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAAC;QAAC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAAA;KAAE,CAAC;IAClH,oBAAoB,EAAE;QAAE,QAAQ,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAAC;QAAC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAAA;KAAE,CAAC;IACjH,cAAc,EAAE;QAAE,QAAQ,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAAC;QAAC,QAAQ,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,CAAC;QAAC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAAA;KAAE,CAAC;IAChJ,SAAS,EAAE;QAAE,QAAQ,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAAC;QAAC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAAA;KAAE,CAAC;CACtG"}
|
|
1
|
+
{"version":3,"file":"mail.d.ts","sourceRoot":"","sources":["../../src/generated/mail.ts"],"names":[],"mappings":"AAGA,kFAAkF;AAClF,MAAM,WAAW,UAAU;IAC1B,eAAe,EAAE;QAAE,QAAQ,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAAC;QAAC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAAC;QAAC,QAAQ,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CAAA;KAAE,CAAC;IAChJ,kBAAkB,EAAE;QAAE,QAAQ,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAAC;QAAC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAAA;KAAE,CAAC;IAC/G,oBAAoB,EAAE;QAAE,QAAQ,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAAC;QAAC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAAC;QAAC,QAAQ,CAAC,iBAAiB,EAAE,MAAM,GAAG,MAAM,CAAC;QAAC,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAAA;KAAE,CAAC;IAC9L,gBAAgB,EAAE;QAAE,QAAQ,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAAC;QAAC,QAAQ,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,CAAC;QAAC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAAA;KAAE,CAAC;IAClJ,cAAc,EAAE;QAAE,QAAQ,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAAC;QAAC,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAAC;QAAC,QAAQ,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,CAAA;KAAE,CAAC;IACzH,qBAAqB,EAAE;QAAE,QAAQ,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAAC;QAAC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAAA;KAAE,CAAC;IAClH,oBAAoB,EAAE;QAAE,QAAQ,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAAC;QAAC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAAA;KAAE,CAAC;IACjH,cAAc,EAAE;QAAE,QAAQ,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAAC;QAAC,QAAQ,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,CAAC;QAAC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAAA;KAAE,CAAC;IAChJ,SAAS,EAAE;QAAE,QAAQ,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAAC;QAAC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAAA;KAAE,CAAC;CACtG"}
|
package/dist/index.d.ts
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
*
|
|
4
4
|
* `@nxgt/janus` sends no e-mail: its flows answer what to send — a one-time
|
|
5
5
|
* token, a sign-in code, the address — and this package turns that answer
|
|
6
|
-
* into an e-mail and hands it to an `@nxgt/mail` transport.
|
|
6
|
+
* into an e-mail and hands it to an `@nxgt/mail` transport. Nine e-mails, in
|
|
7
7
|
* English and French, prebuilt with Maizzle when this package is built, and
|
|
8
8
|
* filled at send time with your brand, the recipient's name and your links.
|
|
9
9
|
* Any of them can be replaced by a function of your own.
|
package/dist/index.js
CHANGED
|
@@ -129,6 +129,47 @@ function noticeMails(options) {
|
|
|
129
129
|
// src/generated/locales.ts
|
|
130
130
|
var LOCALES = ["en", "fr"];
|
|
131
131
|
|
|
132
|
+
// src/refuse.ts
|
|
133
|
+
function refuse(message) {
|
|
134
|
+
throw new TypeError(`janusMail: ${message}`);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// src/links.ts
|
|
138
|
+
var REQUIRED_LINKS = [
|
|
139
|
+
"verifyEmail",
|
|
140
|
+
"resetPassword",
|
|
141
|
+
"secureAccount",
|
|
142
|
+
"getStarted"
|
|
143
|
+
];
|
|
144
|
+
var OPTIONAL_LINKS = ["recoveryCodes"];
|
|
145
|
+
function checkLinks(links) {
|
|
146
|
+
if (typeof links !== "object" || links === null || Array.isArray(links)) {
|
|
147
|
+
refuse("links must be an object, as { verifyEmail, resetPassword, secureAccount, getStarted }");
|
|
148
|
+
}
|
|
149
|
+
const given = links;
|
|
150
|
+
for (const name of REQUIRED_LINKS) {
|
|
151
|
+
if (typeof given[name] !== "function") {
|
|
152
|
+
refuse(`links.${name} must be a function`);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
for (const name of OPTIONAL_LINKS) {
|
|
156
|
+
if (given[name] !== undefined && typeof given[name] !== "function") {
|
|
157
|
+
refuse(`links.${name} must be a function, or left out`);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
function frozenLinks(links) {
|
|
162
|
+
const given = links;
|
|
163
|
+
const names = [
|
|
164
|
+
...REQUIRED_LINKS,
|
|
165
|
+
...OPTIONAL_LINKS.filter((name) => given[name] !== undefined)
|
|
166
|
+
];
|
|
167
|
+
return Object.freeze(Object.fromEntries(names.map((name) => [
|
|
168
|
+
name,
|
|
169
|
+
given[name].bind(links)
|
|
170
|
+
])));
|
|
171
|
+
}
|
|
172
|
+
|
|
132
173
|
// src/mails.ts
|
|
133
174
|
import { fileURLToPath } from "node:url";
|
|
134
175
|
import { createMailRenderer } from "@nxgt/mail/renderer";
|
|
@@ -149,6 +190,14 @@ function janusTemplates() {
|
|
|
149
190
|
emailChanged: ({ brand, name, link, newEmail, locale }) => defaultRenderer().render("email-changed", { brand, name, link, newEmail }, { locale }),
|
|
150
191
|
twoFactorEnabled: ({ brand, name, link, locale }) => defaultRenderer().render("two-factor-enabled", { brand, name, link }, { locale }),
|
|
151
192
|
twoFactorDisabled: ({ brand, name, link, locale }) => defaultRenderer().render("two-factor-disabled", { brand, name, link }, { locale }),
|
|
193
|
+
recoveryCodeUsed: ({
|
|
194
|
+
brand,
|
|
195
|
+
name,
|
|
196
|
+
when,
|
|
197
|
+
recoveryCodesLeft,
|
|
198
|
+
link,
|
|
199
|
+
locale
|
|
200
|
+
}) => defaultRenderer().render("recovery-code-used", { brand, name, when, recoveryCodesLeft, link }, { locale }),
|
|
152
201
|
welcome: ({ brand, name, link, locale }) => defaultRenderer().render("welcome", { brand, name, link }, { locale })
|
|
153
202
|
});
|
|
154
203
|
}
|
|
@@ -162,14 +211,9 @@ var TEMPLATE_NAMES = [
|
|
|
162
211
|
"emailChanged",
|
|
163
212
|
"twoFactorEnabled",
|
|
164
213
|
"twoFactorDisabled",
|
|
214
|
+
"recoveryCodeUsed",
|
|
165
215
|
"welcome"
|
|
166
216
|
];
|
|
167
|
-
var LINK_NAMES = [
|
|
168
|
-
"verifyEmail",
|
|
169
|
-
"resetPassword",
|
|
170
|
-
"secureAccount",
|
|
171
|
-
"getStarted"
|
|
172
|
-
];
|
|
173
217
|
var SYSTEM_CLOCK = Object.freeze({ now: () => new Date });
|
|
174
218
|
function isObject(value) {
|
|
175
219
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
@@ -179,9 +223,6 @@ function isAddress(value) {
|
|
|
179
223
|
return value.trim() !== "";
|
|
180
224
|
return isObject(value) && typeof value.name === "string" && typeof value.address === "string" && value.address.trim() !== "";
|
|
181
225
|
}
|
|
182
|
-
function refuse(message) {
|
|
183
|
-
throw new TypeError(`janusMail: ${message}`);
|
|
184
|
-
}
|
|
185
226
|
function checkSending(options) {
|
|
186
227
|
const { mailer, from, replyTo, brand, links } = options;
|
|
187
228
|
if (!isObject(mailer) || typeof mailer.send !== "function") {
|
|
@@ -196,14 +237,7 @@ function checkSending(options) {
|
|
|
196
237
|
if (typeof brand !== "string" || brand.trim() === "") {
|
|
197
238
|
refuse("brand must be the name the e-mails show, as 'Acme'");
|
|
198
239
|
}
|
|
199
|
-
|
|
200
|
-
refuse("links must be an object, as { verifyEmail, resetPassword, secureAccount, getStarted }");
|
|
201
|
-
}
|
|
202
|
-
for (const name of LINK_NAMES) {
|
|
203
|
-
if (typeof links[name] !== "function") {
|
|
204
|
-
refuse(`links.${name} must be a function`);
|
|
205
|
-
}
|
|
206
|
-
}
|
|
240
|
+
checkLinks(links);
|
|
207
241
|
}
|
|
208
242
|
function resolveClock(clock) {
|
|
209
243
|
if (clock === undefined)
|
|
@@ -269,9 +303,6 @@ function resolveTemplates(options, locales) {
|
|
|
269
303
|
function frozenAddress(address) {
|
|
270
304
|
return typeof address === "string" ? address : Object.freeze({ name: address.name, address: address.address });
|
|
271
305
|
}
|
|
272
|
-
function frozenLinks(links) {
|
|
273
|
-
return Object.freeze(Object.fromEntries(LINK_NAMES.map((name) => [name, links[name].bind(links)])));
|
|
274
|
-
}
|
|
275
306
|
function resolveOptions(options) {
|
|
276
307
|
if (!isObject(options)) {
|
|
277
308
|
refuse("options must be an object, as { mailer, from, brand, links }");
|
|
@@ -291,12 +322,90 @@ function resolveOptions(options) {
|
|
|
291
322
|
});
|
|
292
323
|
}
|
|
293
324
|
|
|
325
|
+
// src/generated/codes-left.ts
|
|
326
|
+
var CODES_LEFT = {
|
|
327
|
+
en: {
|
|
328
|
+
"=0": [
|
|
329
|
+
"You have no recovery codes left."
|
|
330
|
+
],
|
|
331
|
+
one: [
|
|
332
|
+
"You have ",
|
|
333
|
+
null,
|
|
334
|
+
" recovery code left."
|
|
335
|
+
],
|
|
336
|
+
other: [
|
|
337
|
+
"You have ",
|
|
338
|
+
null,
|
|
339
|
+
" recovery codes left."
|
|
340
|
+
]
|
|
341
|
+
},
|
|
342
|
+
fr: {
|
|
343
|
+
"=0": [
|
|
344
|
+
"Il ne vous reste aucun code de récupération."
|
|
345
|
+
],
|
|
346
|
+
one: [
|
|
347
|
+
"Il vous reste ",
|
|
348
|
+
null,
|
|
349
|
+
" code de récupération."
|
|
350
|
+
],
|
|
351
|
+
other: [
|
|
352
|
+
"Il vous reste ",
|
|
353
|
+
null,
|
|
354
|
+
" codes de récupération."
|
|
355
|
+
]
|
|
356
|
+
}
|
|
357
|
+
};
|
|
358
|
+
|
|
359
|
+
// src/codes-left.ts
|
|
360
|
+
function codesLeftText(locale, count) {
|
|
361
|
+
if (!Object.hasOwn(CODES_LEFT, locale))
|
|
362
|
+
return;
|
|
363
|
+
const branches = CODES_LEFT[locale];
|
|
364
|
+
const branch = branches[`=${count}`] ?? branches[new Intl.PluralRules(locale).select(count)] ?? branches.other;
|
|
365
|
+
const digits = new Intl.NumberFormat(locale).format(count);
|
|
366
|
+
return (branch ?? []).map((part) => part ?? digits).join("");
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
// src/recovery-mail.ts
|
|
370
|
+
var METHOD = "recoveryCodeUsed";
|
|
371
|
+
var isCount = (value) => typeof value === "number" && Number.isSafeInteger(value) && value >= 0;
|
|
372
|
+
function codesLeft(used, locale) {
|
|
373
|
+
const given = typeof used === "object" && used !== null ? used.recoveryCodesLeft : undefined;
|
|
374
|
+
if (typeof given === "string")
|
|
375
|
+
return given;
|
|
376
|
+
if (!isCount(given)) {
|
|
377
|
+
throw new TypeError(`janusMail.${METHOD}: recoveryCodesLeft must be a count — a whole number, 0 or more — or the sentence to show`);
|
|
378
|
+
}
|
|
379
|
+
const text = codesLeftText(locale, given);
|
|
380
|
+
if (text === undefined) {
|
|
381
|
+
throw new TypeError(`janusMail.${METHOD}: recoveryCodesLeft must be the sentence to show in a locale the default e-mails are not built in`);
|
|
382
|
+
}
|
|
383
|
+
return text;
|
|
384
|
+
}
|
|
385
|
+
function recoveryMail(options) {
|
|
386
|
+
const { brand, links } = options;
|
|
387
|
+
return {
|
|
388
|
+
async recoveryCodeUsed(to, used) {
|
|
389
|
+
const locale = localeFor(options, to);
|
|
390
|
+
return send(options, METHOD, field(METHOD, to, "email"), {
|
|
391
|
+
brand,
|
|
392
|
+
name: field(METHOD, to, "name"),
|
|
393
|
+
when: field(METHOD, used, "when"),
|
|
394
|
+
recoveryCodesLeft: codesLeft(used, locale),
|
|
395
|
+
link: links.recoveryCodes === undefined ? link(METHOD, links.secureAccount(), "secureAccount()") : link(METHOD, links.recoveryCodes(), "recoveryCodes()"),
|
|
396
|
+
locale
|
|
397
|
+
});
|
|
398
|
+
}
|
|
399
|
+
};
|
|
400
|
+
}
|
|
401
|
+
|
|
294
402
|
// src/janus-mail.ts
|
|
295
403
|
function janusMail(options) {
|
|
296
404
|
const resolved = resolveOptions(options);
|
|
297
405
|
const mail = Object.freeze({
|
|
298
406
|
...expiringMails(resolved),
|
|
299
407
|
...noticeMails(resolved),
|
|
408
|
+
...recoveryMail(resolved),
|
|
300
409
|
templates: resolved.templates,
|
|
301
410
|
locales: resolved.locales
|
|
302
411
|
});
|
|
@@ -307,5 +416,5 @@ export {
|
|
|
307
416
|
janusTemplates
|
|
308
417
|
};
|
|
309
418
|
|
|
310
|
-
//# debugId=
|
|
419
|
+
//# debugId=E470EFFF013B3B0B64756E2164756E21
|
|
311
420
|
//# sourceMappingURL=index.js.map
|