@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/README.md
CHANGED
|
@@ -29,8 +29,8 @@ The e-mails are built with [Maizzle](https://maizzle.com) when **this
|
|
|
29
29
|
package** is built — from
|
|
30
30
|
[`@nxgt/mail-presets`](https://www.npmjs.com/package/@nxgt/mail-presets),
|
|
31
31
|
CSS inlined for mail clients — and shipped as HTML and text. At send time
|
|
32
|
-
they are only filled in: your brand, the recipient's name, your links,
|
|
33
|
-
value escaped. No template engine and no Maizzle run in your server.
|
|
32
|
+
they are only filled in: your brand, the recipient's name, your links, how
|
|
33
|
+
long the link or code lasts, every value escaped. No template engine and no Maizzle run in your server.
|
|
34
34
|
|
|
35
35
|
> **0.x.** A minor version may still change the surface; the changelog says how.
|
|
36
36
|
|
|
@@ -54,11 +54,12 @@ runtime. Like `@nxgt/janus`, it expects `"moduleResolution": "bundler"`.
|
|
|
54
54
|
|
|
55
55
|
| Export | What it is |
|
|
56
56
|
| --- | --- |
|
|
57
|
-
| `janusMail(options)` | The e-mails, over your mailer: `verifyEmail`, `resetPassword`, `signInCode`, `passwordChanged`, `emailChanged`, and `templates` and `locales`. Options: `mailer`, `from`, `replyTo?`, `brand`, `links` (`verifyEmail`, `resetPassword`, `secureAccount`), `locales?`, `fallbackLocale?`, `templates?`. A wrong option is a bare `TypeError`, thrown here |
|
|
57
|
+
| `janusMail(options)` | The e-mails, over your mailer: `verifyEmail`, `resetPassword`, `signInCode`, `passwordChanged`, `emailChanged`, and `templates` and `locales`. Options: `mailer`, `from`, `replyTo?`, `brand`, `links` (`verifyEmail`, `resetPassword`, `secureAccount`), `locales?`, `fallbackLocale?`, `templates?`, `clock?`. A wrong option is a bare `TypeError`, thrown here |
|
|
58
58
|
| `janusTemplates()` | The five default templates alone, each `(variables & { locale }) => Rendered` |
|
|
59
59
|
| `JanusMail<L>`, `JanusMailOptions<L>` | What `janusMail()` answers and takes, for the locales `L` |
|
|
60
60
|
| `JanusMailTemplate<V, L>`, `JanusMailTemplates<L>`, `JanusMailTemplateName` | One template, the five of them, and their names |
|
|
61
|
-
| `JanusMailVariables` | What each template is given: `brand`, and `name`, `link`, `code` or `newEmail` as its e-mail needs |
|
|
61
|
+
| `JanusMailVariables` | What each template is given: `brand`, and `name`, `link`, `code`, `expiresIn` or `newEmail` as its e-mail needs |
|
|
62
|
+
| `JanusMailSendOptions` | The third argument of `verifyEmail`, `resetPassword` and `signInCode`: `{ expiresIn? }`, the expiry as text, over the one derived |
|
|
62
63
|
| `JanusMailLocale` | `'en' \| 'fr'`: the locales the defaults are built in |
|
|
63
64
|
| `JanusMailLinks`, `Recipient` | The `links` option; who an e-mail is for — `{ name, locale? }` |
|
|
64
65
|
|
|
@@ -68,12 +69,21 @@ class.
|
|
|
68
69
|
|
|
69
70
|
| Method | Sends | To |
|
|
70
71
|
| --- | --- | --- |
|
|
71
|
-
| `verifyEmail(issued, to)` | `issued` from `auth.verifyEmail.send(user)`; a link from `links.verifyEmail(issued.token)
|
|
72
|
-
| `resetPassword(issued, to)` | `issued` from `auth.resetPassword.request(email)`, checked for `null`; a link from `links.resetPassword(issued.token)
|
|
73
|
-
| `signInCode(issued, to?)` | `issued.code` from `auth.signInCode.request(email)
|
|
72
|
+
| `verifyEmail(issued, to, options?)` | `issued` from `auth.verifyEmail.send(user)`; a link from `links.verifyEmail(issued.token)`, and how long it lasts | `issued.email` |
|
|
73
|
+
| `resetPassword(issued, to, options?)` | `issued` from `auth.resetPassword.request(email)`, checked for `null`; a link from `links.resetPassword(issued.token)`, and how long it lasts | `issued.email` |
|
|
74
|
+
| `signInCode(issued, to?, options?)` | `issued.code` from `auth.signInCode.request(email)`, and how long it lasts — **never the challenge** | `issued.email` |
|
|
74
75
|
| `passwordChanged(to)` | A notice, with `links.secureAccount()` | `to.email` |
|
|
75
76
|
| `emailChanged(to)` | A notice naming `to.newEmail`, with `links.secureAccount()` | `to.formerEmail` |
|
|
76
77
|
|
|
78
|
+
The three e-mails of a link or a code say how long it lasts — "This link
|
|
79
|
+
expires in 1 hour.", "Ce lien expire dans 1 heure." — from the flow's
|
|
80
|
+
`issued.expiresAt`: the time left at send time, rounded to the minute, then
|
|
81
|
+
down to the largest whole unit (days, hours or minutes), formatted with
|
|
82
|
+
`Intl.NumberFormat` in the recipient's locale. The time is read from
|
|
83
|
+
`janusMail({ clock })` — give it the clock you gave `janus({ clock })` —
|
|
84
|
+
else the system clock. Pass `{ expiresIn }` to say it yourself, as plain
|
|
85
|
+
text in the recipient's language.
|
|
86
|
+
|
|
77
87
|
## Usage
|
|
78
88
|
|
|
79
89
|
The examples use `mail` from above, and an `auth` whose user schema has a
|
|
@@ -110,6 +120,12 @@ if (issued !== null) {
|
|
|
110
120
|
}
|
|
111
121
|
```
|
|
112
122
|
|
|
123
|
+
### Saying the expiry yourself
|
|
124
|
+
|
|
125
|
+
```ts
|
|
126
|
+
await mail.verifyEmail(issued, { name: user.name, locale: 'fr' }, { expiresIn: '24 heures' });
|
|
127
|
+
```
|
|
128
|
+
|
|
113
129
|
### Telling a user their password changed
|
|
114
130
|
|
|
115
131
|
```ts
|
|
@@ -192,9 +208,28 @@ templates is a compile error, and a `TypeError` in JavaScript.
|
|
|
192
208
|
body and the footer, escaped: no logo, no link, no markup. For those, replace
|
|
193
209
|
the templates.
|
|
194
210
|
|
|
195
|
-
**
|
|
196
|
-
|
|
197
|
-
|
|
211
|
+
**Send the flow's answer as it came: `expiresAt` must be a `Date`.** The
|
|
212
|
+
expiry is derived from `issued.expiresAt` at send time. A flow's answer that
|
|
213
|
+
went through JSON — a job queue — holds a string there, which is a compile
|
|
214
|
+
error and, in JavaScript, a `TypeError` (`expiresAt must be a Date`); revive
|
|
215
|
+
it with `new Date(...)`, or pass `{ expiresIn }`. An `expiresAt` already past
|
|
216
|
+
is a `TypeError` too: the link would not work, so nothing is sent.
|
|
217
|
+
|
|
218
|
+
**Give `janusMail()` the clock you gave `janus()`.** The time left is
|
|
219
|
+
measured against `clock`, the system clock by default. Tests that run
|
|
220
|
+
`janus({ clock: fixedClock(...) })` issue an `expiresAt` in the clock's time:
|
|
221
|
+
without the same `clock` here, a clock set in the past makes every send an
|
|
222
|
+
`expiresAt is past` `TypeError`, and one set ahead a wrong duration.
|
|
223
|
+
|
|
224
|
+
**Locales are BCP 47 tags: `fr-CA`, never `fr_CA`.** The expiry is
|
|
225
|
+
formatted by `Intl` in the locale picked, so a locale `Intl` refuses is a
|
|
226
|
+
`TypeError` from `janusMail()`:
|
|
227
|
+
[`janusMail: locales must be BCP 47 language tags, as 'fr-CA'`](docs/troubleshooting.md#janusmail-locales-must-be-bcp-47-language-tags-as-fr-ca).
|
|
228
|
+
|
|
229
|
+
**The expiry is formatted by the runtime's `Intl`.** A locale the runtime has
|
|
230
|
+
no data for is formatted in its default language, and French puts a no-break
|
|
231
|
+
space between the number and some units, as CLDR says. For a wording of your
|
|
232
|
+
own, pass `{ expiresIn }`.
|
|
198
233
|
|
|
199
234
|
**A failed send is `MAIL_FAILED`; answer it as an outage.** In an HTTP
|
|
200
235
|
handler, a `MailFailure` is a `503` — nothing is known to have been sent —
|
|
@@ -224,10 +259,16 @@ The symptoms and fixes are in [troubleshooting](docs/troubleshooting.md).
|
|
|
224
259
|
|
|
225
260
|
## Type safety, counted
|
|
226
261
|
|
|
227
|
-
**Twenty plausible mistakes, twenty refused at compile time.**
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
262
|
+
**Twenty-five plausible mistakes, twenty-five refused at compile time.**
|
|
263
|
+
Three files hold one `@ts-expect-error` per mistake, beside the calls that
|
|
264
|
+
must keep compiling:
|
|
265
|
+
[`test/types/send-refusals.ts`](test/types/send-refusals.ts) the eight of a
|
|
266
|
+
send, 1 to 8;
|
|
267
|
+
[`test/types/option-refusals.ts`](test/types/option-refusals.ts) the twelve
|
|
268
|
+
of `janusMail()`'s options, 9 to 20, beside adding a language with every
|
|
269
|
+
template; and
|
|
270
|
+
[`test/types/expiry-refusals.ts`](test/types/expiry-refusals.ts) the five of
|
|
271
|
+
the expiry and the clock, 21 to 25:
|
|
231
272
|
|
|
232
273
|
1. A sign-in code given to `verifyEmail`: it has no token.
|
|
233
274
|
2. A one-time token given to `signInCode`: it has no code.
|
|
@@ -249,9 +290,14 @@ among them adding a language with every template:
|
|
|
249
290
|
18. A template read from `mail.templates` that does not exist.
|
|
250
291
|
19. A link computed asynchronously.
|
|
251
292
|
20. A link answered as a `URL` object rather than its `href`.
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
293
|
+
21. A sign-in code without its `expiresAt`.
|
|
294
|
+
22. A flow's answer that went through JSON: `expiresAt` a string.
|
|
295
|
+
23. `expiresIn` given as a number of seconds rather than the text to show.
|
|
296
|
+
24. `expiresIn` given with the recipient rather than as the send's option.
|
|
297
|
+
25. `clock` given as a function rather than `@nxgt/janus`'s `Clock`.
|
|
298
|
+
|
|
299
|
+
In JavaScript, 19 to 23 are a `TypeError` at send time instead, naming the
|
|
300
|
+
call or the field, and 25 one from `janusMail()`.
|
|
255
301
|
|
|
256
302
|
[`test/types/variables.ts`](test/types/variables.ts) also holds
|
|
257
303
|
`JanusMailVariables` equal to the variables of the build: an e-mail that
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { ResolvedOptions } from './options';
|
|
2
|
+
import type { JanusMail } from './types';
|
|
3
|
+
type ExpiringMails = Pick<JanusMail<string>, 'verifyEmail' | 'resetPassword' | 'signInCode'>;
|
|
4
|
+
/** `verifyEmail`, `resetPassword` and `signInCode`, over the resolved options. */
|
|
5
|
+
export declare function expiringMails(options: ResolvedOptions): ExpiringMails;
|
|
6
|
+
export {};
|
|
7
|
+
//# sourceMappingURL=expiring-mails.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"expiring-mails.d.ts","sourceRoot":"","sources":["../src/expiring-mails.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,WAAW,CAAC;AAEjD,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,SAAS,CAAC;AAEzC,KAAK,aAAa,GAAG,IAAI,CACxB,SAAS,CAAC,MAAM,CAAC,EACjB,aAAa,GAAG,eAAe,GAAG,YAAY,CAC9C,CAAC;AAoBF,kFAAkF;AAClF,wBAAgB,aAAa,CAAC,OAAO,EAAE,eAAe,GAAG,aAAa,CAqBrE"}
|
package/dist/expiry.d.ts
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `expiresIn`: how long the link or the code of an e-mail stays valid, as
|
|
3
|
+
* the text the e-mail shows — "1 hour", "1 heure". Derived from the
|
|
4
|
+
* `expiresAt` the flow answered and the locale the recipient gets, unless the
|
|
5
|
+
* send passes its own.
|
|
6
|
+
*/
|
|
7
|
+
import type { Clock } from '@nxgt/janus';
|
|
8
|
+
/**
|
|
9
|
+
* A duration in the locale, in the largest unit it holds at least once:
|
|
10
|
+
* rounded to the minute first — the flow set `expiresAt` a moment before the
|
|
11
|
+
* send, so an hour is 59 minutes and 59 seconds by then — and then down to a
|
|
12
|
+
* whole unit: 90 minutes is "1 hour", 36 hours "1 day". At 30 seconds or
|
|
13
|
+
* more, the e-mail never promises more than half a minute beyond what is
|
|
14
|
+
* left; under that, it says "1 minute".
|
|
15
|
+
*
|
|
16
|
+
* ```ts
|
|
17
|
+
* formatExpiry(3_600_000, 'fr'); // '1 heure'
|
|
18
|
+
* ```
|
|
19
|
+
*/
|
|
20
|
+
export declare function formatExpiry(ms: number, locale: string): string;
|
|
21
|
+
/**
|
|
22
|
+
* The `expiresIn` of one send: the one passed, as is, or the time left
|
|
23
|
+
* until the flow's `expiresAt` by `clock` — the one given to `janus()`, when
|
|
24
|
+
* it was given to `janusMail()` too — in `locale`. A `TypeError` naming the method
|
|
25
|
+
* when `expiresAt` is not a valid `Date` — a flow's answer that went through
|
|
26
|
+
* JSON holds a string there — or is already past.
|
|
27
|
+
*/
|
|
28
|
+
export declare function expiresInFor(method: string, issued: unknown, locale: string, options: unknown, clock: Clock): string;
|
|
29
|
+
//# sourceMappingURL=expiry.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"expiry.d.ts","sourceRoot":"","sources":["../src/expiry.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,aAAa,CAAC;AAMzC;;;;;;;;;;;GAWG;AACH,wBAAgB,YAAY,CAAC,EAAE,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,MAAM,CAa/D;AAcD;;;;;;GAMG;AACH,wBAAgB,YAAY,CAC3B,MAAM,EAAE,MAAM,EACd,MAAM,EAAE,OAAO,EACf,MAAM,EAAE,MAAM,EACd,OAAO,EAAE,OAAO,EAChB,KAAK,EAAE,KAAK,GACV,MAAM,CAqBR"}
|
package/dist/generated/mail.d.ts
CHANGED
|
@@ -13,15 +13,18 @@ export interface MailEmails {
|
|
|
13
13
|
};
|
|
14
14
|
"reset-password": {
|
|
15
15
|
readonly brand: string | number;
|
|
16
|
+
readonly expiresIn: string | number;
|
|
16
17
|
readonly link: string;
|
|
17
18
|
readonly name: string | number;
|
|
18
19
|
};
|
|
19
20
|
"sign-in-code": {
|
|
20
21
|
readonly brand: string | number;
|
|
21
22
|
readonly code: string | number;
|
|
23
|
+
readonly expiresIn: string | number;
|
|
22
24
|
};
|
|
23
25
|
"verify-email": {
|
|
24
26
|
readonly brand: string | number;
|
|
27
|
+
readonly expiresIn: string | number;
|
|
25
28
|
readonly link: string;
|
|
26
29
|
readonly name: string | number;
|
|
27
30
|
};
|
|
@@ -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,IAAI,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAAA;KAAE,CAAC;
|
|
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,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;CAChJ"}
|
package/dist/index.d.ts
CHANGED
|
@@ -16,5 +16,5 @@
|
|
|
16
16
|
*/
|
|
17
17
|
export { janusMail } from './janus-mail';
|
|
18
18
|
export { janusTemplates } from './templates';
|
|
19
|
-
export type { JanusMail, JanusMailLinks, JanusMailLocale, JanusMailOptions, JanusMailTemplate, JanusMailTemplateName, JanusMailTemplates, JanusMailVariables, Recipient, } from './types';
|
|
19
|
+
export type { JanusMail, JanusMailLinks, JanusMailLocale, JanusMailOptions, JanusMailSendOptions, JanusMailTemplate, JanusMailTemplateName, JanusMailTemplates, JanusMailVariables, Recipient, } from './types';
|
|
20
20
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AACH,OAAO,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AACzC,OAAO,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAC7C,YAAY,EACX,SAAS,EACT,cAAc,EACd,eAAe,EACf,gBAAgB,EAChB,iBAAiB,EACjB,qBAAqB,EACrB,kBAAkB,EAClB,kBAAkB,EAClB,SAAS,GACT,MAAM,SAAS,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AACH,OAAO,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AACzC,OAAO,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAC7C,YAAY,EACX,SAAS,EACT,cAAc,EACd,eAAe,EACf,gBAAgB,EAChB,oBAAoB,EACpB,iBAAiB,EACjB,qBAAqB,EACrB,kBAAkB,EAClB,kBAAkB,EAClB,SAAS,GACT,MAAM,SAAS,CAAC"}
|
package/dist/index.js
CHANGED
|
@@ -1,5 +1,128 @@
|
|
|
1
|
-
// src/
|
|
1
|
+
// src/expiry.ts
|
|
2
|
+
var MINUTE_MS = 60000;
|
|
3
|
+
var MINUTES_IN_HOUR = 60;
|
|
4
|
+
var MINUTES_IN_DAY = 24 * MINUTES_IN_HOUR;
|
|
5
|
+
function formatExpiry(ms, locale) {
|
|
6
|
+
const minutes = Math.max(1, Math.round(ms / MINUTE_MS));
|
|
7
|
+
const [value, unit] = minutes >= MINUTES_IN_DAY ? [Math.floor(minutes / MINUTES_IN_DAY), "day"] : minutes >= MINUTES_IN_HOUR ? [Math.floor(minutes / MINUTES_IN_HOUR), "hour"] : [minutes, "minute"];
|
|
8
|
+
return new Intl.NumberFormat(locale, {
|
|
9
|
+
style: "unit",
|
|
10
|
+
unit,
|
|
11
|
+
unitDisplay: "long"
|
|
12
|
+
}).format(value);
|
|
13
|
+
}
|
|
14
|
+
function passedExpiresIn(method, options) {
|
|
15
|
+
const read = typeof options === "object" && options !== null ? options.expiresIn : undefined;
|
|
16
|
+
if (read !== undefined && typeof read !== "string") {
|
|
17
|
+
throw new TypeError(`janusMail.${method}: expiresIn must be a string`);
|
|
18
|
+
}
|
|
19
|
+
return read;
|
|
20
|
+
}
|
|
21
|
+
function expiresInFor(method, issued, locale, options, clock) {
|
|
22
|
+
const passed = passedExpiresIn(method, options);
|
|
23
|
+
if (passed !== undefined)
|
|
24
|
+
return passed;
|
|
25
|
+
const expiresAt = typeof issued === "object" && issued !== null ? issued.expiresAt : undefined;
|
|
26
|
+
if (!(expiresAt instanceof Date) || Number.isNaN(expiresAt.getTime())) {
|
|
27
|
+
throw new TypeError(`janusMail.${method}: expiresAt must be a Date`);
|
|
28
|
+
}
|
|
29
|
+
const now = clock.now();
|
|
30
|
+
if (!(now instanceof Date) || Number.isNaN(now.getTime())) {
|
|
31
|
+
throw new TypeError(`janusMail.${method}: clock.now() must answer a Date`);
|
|
32
|
+
}
|
|
33
|
+
const left = expiresAt.getTime() - now.getTime();
|
|
34
|
+
if (left <= 0) {
|
|
35
|
+
throw new TypeError(`janusMail.${method}: expiresAt is past — the link or code would not work`);
|
|
36
|
+
}
|
|
37
|
+
return formatExpiry(left, locale);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// src/send.ts
|
|
2
41
|
import { pickLocale } from "@nxgt/mail";
|
|
42
|
+
function field(method, value, key) {
|
|
43
|
+
const read = typeof value === "object" && value !== null ? value[key] : undefined;
|
|
44
|
+
if (typeof read !== "string") {
|
|
45
|
+
throw new TypeError(`janusMail.${method}: ${key} must be a string`);
|
|
46
|
+
}
|
|
47
|
+
return read;
|
|
48
|
+
}
|
|
49
|
+
function link(method, value, call) {
|
|
50
|
+
if (typeof value !== "string") {
|
|
51
|
+
throw new TypeError(`janusMail.${method}: links.${call} must answer a string`);
|
|
52
|
+
}
|
|
53
|
+
return value;
|
|
54
|
+
}
|
|
55
|
+
function localeFor(options, to) {
|
|
56
|
+
const wanted = typeof to === "object" && to !== null ? to.locale : undefined;
|
|
57
|
+
return pickLocale(wanted, options.locales, options.fallbackLocale);
|
|
58
|
+
}
|
|
59
|
+
async function send(options, name, address, variables) {
|
|
60
|
+
const template = options.templates[name];
|
|
61
|
+
const { subject, html, text } = await template(variables);
|
|
62
|
+
const replyTo = options.replyTo === undefined ? {} : { replyTo: options.replyTo };
|
|
63
|
+
return options.mailer.send({
|
|
64
|
+
to: address,
|
|
65
|
+
from: options.from,
|
|
66
|
+
...replyTo,
|
|
67
|
+
subject,
|
|
68
|
+
html,
|
|
69
|
+
text
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// src/expiring-mails.ts
|
|
74
|
+
function linkMail(options, name) {
|
|
75
|
+
return async (issued, to, sendOptions) => {
|
|
76
|
+
const token = field(name, issued, "token");
|
|
77
|
+
const locale = localeFor(options, to);
|
|
78
|
+
return send(options, name, field(name, issued, "email"), {
|
|
79
|
+
brand: options.brand,
|
|
80
|
+
name: field(name, to, "name"),
|
|
81
|
+
link: link(name, options.links[name](token), `${name}(token)`),
|
|
82
|
+
expiresIn: expiresInFor(name, issued, locale, sendOptions, options.clock),
|
|
83
|
+
locale
|
|
84
|
+
});
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
function expiringMails(options) {
|
|
88
|
+
return {
|
|
89
|
+
verifyEmail: linkMail(options, "verifyEmail"),
|
|
90
|
+
resetPassword: linkMail(options, "resetPassword"),
|
|
91
|
+
async signInCode(issued, to, sendOptions) {
|
|
92
|
+
const locale = localeFor(options, to);
|
|
93
|
+
return send(options, "signInCode", field("signInCode", issued, "email"), {
|
|
94
|
+
brand: options.brand,
|
|
95
|
+
code: field("signInCode", issued, "code"),
|
|
96
|
+
expiresIn: expiresInFor("signInCode", issued, locale, sendOptions, options.clock),
|
|
97
|
+
locale
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// src/notice-mails.ts
|
|
104
|
+
function noticeMails(options) {
|
|
105
|
+
const { brand, links } = options;
|
|
106
|
+
return {
|
|
107
|
+
async passwordChanged(to) {
|
|
108
|
+
return send(options, "passwordChanged", field("passwordChanged", to, "email"), {
|
|
109
|
+
brand,
|
|
110
|
+
name: field("passwordChanged", to, "name"),
|
|
111
|
+
link: link("passwordChanged", links.secureAccount(), "secureAccount()"),
|
|
112
|
+
locale: localeFor(options, to)
|
|
113
|
+
});
|
|
114
|
+
},
|
|
115
|
+
async emailChanged(to) {
|
|
116
|
+
return send(options, "emailChanged", field("emailChanged", to, "formerEmail"), {
|
|
117
|
+
brand,
|
|
118
|
+
name: field("emailChanged", to, "name"),
|
|
119
|
+
link: link("emailChanged", links.secureAccount(), "secureAccount()"),
|
|
120
|
+
newEmail: field("emailChanged", to, "newEmail"),
|
|
121
|
+
locale: localeFor(options, to)
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
};
|
|
125
|
+
}
|
|
3
126
|
|
|
4
127
|
// src/generated/locales.ts
|
|
5
128
|
var LOCALES = ["en", "fr"];
|
|
@@ -17,9 +140,9 @@ function defaultRenderer() {
|
|
|
17
140
|
// src/templates.ts
|
|
18
141
|
function janusTemplates() {
|
|
19
142
|
return Object.freeze({
|
|
20
|
-
verifyEmail: ({ brand, name, link, locale }) => defaultRenderer().render("verify-email", { brand, name, link }, { locale }),
|
|
21
|
-
resetPassword: ({ brand, name, link, locale }) => defaultRenderer().render("reset-password", { brand, name, link }, { locale }),
|
|
22
|
-
signInCode: ({ brand, code, locale }) => defaultRenderer().render("sign-in-code", { brand, code }, { locale }),
|
|
143
|
+
verifyEmail: ({ brand, name, link, expiresIn, locale }) => defaultRenderer().render("verify-email", { brand, name, link, expiresIn }, { locale }),
|
|
144
|
+
resetPassword: ({ brand, name, link, expiresIn, locale }) => defaultRenderer().render("reset-password", { brand, name, link, expiresIn }, { locale }),
|
|
145
|
+
signInCode: ({ brand, code, expiresIn, locale }) => defaultRenderer().render("sign-in-code", { brand, code, expiresIn }, { locale }),
|
|
23
146
|
passwordChanged: ({ brand, name, link, locale }) => defaultRenderer().render("password-changed", { brand, name, link }, { locale }),
|
|
24
147
|
emailChanged: ({ brand, name, link, newEmail, locale }) => defaultRenderer().render("email-changed", { brand, name, link, newEmail }, { locale })
|
|
25
148
|
});
|
|
@@ -34,6 +157,7 @@ var TEMPLATE_NAMES = [
|
|
|
34
157
|
"emailChanged"
|
|
35
158
|
];
|
|
36
159
|
var LINK_NAMES = ["verifyEmail", "resetPassword", "secureAccount"];
|
|
160
|
+
var SYSTEM_CLOCK = Object.freeze({ now: () => new Date });
|
|
37
161
|
function isObject(value) {
|
|
38
162
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
39
163
|
}
|
|
@@ -68,12 +192,31 @@ function checkSending(options) {
|
|
|
68
192
|
}
|
|
69
193
|
}
|
|
70
194
|
}
|
|
195
|
+
function resolveClock(clock) {
|
|
196
|
+
if (clock === undefined)
|
|
197
|
+
return SYSTEM_CLOCK;
|
|
198
|
+
if (!isObject(clock) || typeof clock.now !== "function") {
|
|
199
|
+
refuse("clock must be a Clock — an object with a now function");
|
|
200
|
+
}
|
|
201
|
+
return Object.freeze({ now: clock.now.bind(clock) });
|
|
202
|
+
}
|
|
203
|
+
function isLanguageTag(locale) {
|
|
204
|
+
try {
|
|
205
|
+
Intl.getCanonicalLocales(locale);
|
|
206
|
+
return true;
|
|
207
|
+
} catch {
|
|
208
|
+
return false;
|
|
209
|
+
}
|
|
210
|
+
}
|
|
71
211
|
function resolveLocales(options) {
|
|
72
212
|
const wanted = options.locales ?? LOCALES;
|
|
73
213
|
if (!Array.isArray(wanted) || wanted.length === 0 || !wanted.every((locale) => typeof locale === "string" && locale !== "")) {
|
|
74
214
|
refuse("locales must list at least one locale, as ['en', 'fr']");
|
|
75
215
|
}
|
|
76
216
|
const locales = Object.freeze([...wanted]);
|
|
217
|
+
if (!locales.every(isLanguageTag)) {
|
|
218
|
+
refuse("locales must be BCP 47 language tags, as 'fr-CA'");
|
|
219
|
+
}
|
|
77
220
|
if (new Set(locales).size !== locales.length) {
|
|
78
221
|
refuse("locales holds the same locale twice");
|
|
79
222
|
}
|
|
@@ -134,92 +277,17 @@ function resolveOptions(options) {
|
|
|
134
277
|
links: frozenLinks(options.links),
|
|
135
278
|
locales,
|
|
136
279
|
fallbackLocale,
|
|
137
|
-
templates: resolveTemplates(options, locales)
|
|
280
|
+
templates: resolveTemplates(options, locales),
|
|
281
|
+
clock: resolveClock(options.clock)
|
|
138
282
|
});
|
|
139
283
|
}
|
|
140
284
|
|
|
141
285
|
// src/janus-mail.ts
|
|
142
|
-
function field(method, value, key) {
|
|
143
|
-
const read = typeof value === "object" && value !== null ? value[key] : undefined;
|
|
144
|
-
if (typeof read !== "string") {
|
|
145
|
-
throw new TypeError(`janusMail.${method}: ${key} must be a string`);
|
|
146
|
-
}
|
|
147
|
-
return read;
|
|
148
|
-
}
|
|
149
|
-
function link(method, value, call) {
|
|
150
|
-
if (typeof value !== "string") {
|
|
151
|
-
throw new TypeError(`janusMail.${method}: links.${call} must answer a string`);
|
|
152
|
-
}
|
|
153
|
-
return value;
|
|
154
|
-
}
|
|
155
|
-
function localeFor(options, to) {
|
|
156
|
-
const wanted = typeof to === "object" && to !== null ? to.locale : undefined;
|
|
157
|
-
return pickLocale(wanted, options.locales, options.fallbackLocale);
|
|
158
|
-
}
|
|
159
|
-
async function send(options, name, address, variables) {
|
|
160
|
-
const template = options.templates[name];
|
|
161
|
-
const { subject, html, text } = await template(variables);
|
|
162
|
-
const replyTo = options.replyTo === undefined ? {} : { replyTo: options.replyTo };
|
|
163
|
-
return options.mailer.send({
|
|
164
|
-
to: address,
|
|
165
|
-
from: options.from,
|
|
166
|
-
...replyTo,
|
|
167
|
-
subject,
|
|
168
|
-
html,
|
|
169
|
-
text
|
|
170
|
-
});
|
|
171
|
-
}
|
|
172
|
-
function methods(options) {
|
|
173
|
-
const { brand, links } = options;
|
|
174
|
-
return {
|
|
175
|
-
async verifyEmail(issued, to) {
|
|
176
|
-
const token = field("verifyEmail", issued, "token");
|
|
177
|
-
return send(options, "verifyEmail", field("verifyEmail", issued, "email"), {
|
|
178
|
-
brand,
|
|
179
|
-
name: field("verifyEmail", to, "name"),
|
|
180
|
-
link: link("verifyEmail", links.verifyEmail(token), "verifyEmail(token)"),
|
|
181
|
-
locale: localeFor(options, to)
|
|
182
|
-
});
|
|
183
|
-
},
|
|
184
|
-
async resetPassword(issued, to) {
|
|
185
|
-
const token = field("resetPassword", issued, "token");
|
|
186
|
-
return send(options, "resetPassword", field("resetPassword", issued, "email"), {
|
|
187
|
-
brand,
|
|
188
|
-
name: field("resetPassword", to, "name"),
|
|
189
|
-
link: link("resetPassword", links.resetPassword(token), "resetPassword(token)"),
|
|
190
|
-
locale: localeFor(options, to)
|
|
191
|
-
});
|
|
192
|
-
},
|
|
193
|
-
async signInCode(issued, to) {
|
|
194
|
-
return send(options, "signInCode", field("signInCode", issued, "email"), {
|
|
195
|
-
brand,
|
|
196
|
-
code: field("signInCode", issued, "code"),
|
|
197
|
-
locale: localeFor(options, to)
|
|
198
|
-
});
|
|
199
|
-
},
|
|
200
|
-
async passwordChanged(to) {
|
|
201
|
-
return send(options, "passwordChanged", field("passwordChanged", to, "email"), {
|
|
202
|
-
brand,
|
|
203
|
-
name: field("passwordChanged", to, "name"),
|
|
204
|
-
link: link("passwordChanged", links.secureAccount(), "secureAccount()"),
|
|
205
|
-
locale: localeFor(options, to)
|
|
206
|
-
});
|
|
207
|
-
},
|
|
208
|
-
async emailChanged(to) {
|
|
209
|
-
return send(options, "emailChanged", field("emailChanged", to, "formerEmail"), {
|
|
210
|
-
brand,
|
|
211
|
-
name: field("emailChanged", to, "name"),
|
|
212
|
-
link: link("emailChanged", links.secureAccount(), "secureAccount()"),
|
|
213
|
-
newEmail: field("emailChanged", to, "newEmail"),
|
|
214
|
-
locale: localeFor(options, to)
|
|
215
|
-
});
|
|
216
|
-
}
|
|
217
|
-
};
|
|
218
|
-
}
|
|
219
286
|
function janusMail(options) {
|
|
220
287
|
const resolved = resolveOptions(options);
|
|
221
288
|
const mail = Object.freeze({
|
|
222
|
-
...
|
|
289
|
+
...expiringMails(resolved),
|
|
290
|
+
...noticeMails(resolved),
|
|
223
291
|
templates: resolved.templates,
|
|
224
292
|
locales: resolved.locales
|
|
225
293
|
});
|
|
@@ -230,5 +298,5 @@ export {
|
|
|
230
298
|
janusTemplates
|
|
231
299
|
};
|
|
232
300
|
|
|
233
|
-
//# debugId=
|
|
301
|
+
//# debugId=B823BF1AC1618A7C64756E2164756E21
|
|
234
302
|
//# sourceMappingURL=index.js.map
|