@nxgt/janus-mail 0.3.2 → 0.4.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 -15
- package/dist/generated/mail.d.ts +5 -0
- package/dist/generated/mail.d.ts.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +19 -15
- package/dist/index.js.map +6 -6
- package/dist/janus-mail.d.ts +1 -0
- package/dist/janus-mail.d.ts.map +1 -1
- package/dist/notice-mails.d.ts +7 -5
- package/dist/notice-mails.d.ts.map +1 -1
- package/dist/options.d.ts.map +1 -1
- package/dist/template-types.d.ts +79 -0
- package/dist/template-types.d.ts.map +1 -0
- package/dist/templates.d.ts +1 -1
- package/dist/templates.d.ts.map +1 -1
- package/dist/types.d.ts +17 -67
- package/dist/types.d.ts.map +1 -1
- package/docs/README.md +4 -3
- package/docs/guide/building.md +7 -5
- package/docs/guide/locales.md +4 -3
- package/docs/guide/sending.md +80 -38
- package/docs/guide/templates.md +26 -14
- package/docs/roadmap.md +19 -4
- package/docs/troubleshooting.md +79 -22
- package/mails/en/email-changed.html +9 -9
- package/mails/en/password-changed.html +9 -9
- package/mails/en/reset-password.html +9 -9
- package/mails/en/sign-in-code.html +9 -9
- package/mails/en/two-factor-disabled.html +9 -9
- package/mails/en/two-factor-enabled.html +9 -9
- package/mails/en/verify-email.html +9 -9
- package/mails/en/welcome.html +50 -0
- package/mails/en/welcome.txt +19 -0
- package/mails/fr/email-changed.html +9 -9
- package/mails/fr/password-changed.html +9 -9
- package/mails/fr/reset-password.html +9 -9
- package/mails/fr/sign-in-code.html +9 -9
- package/mails/fr/two-factor-disabled.html +9 -9
- package/mails/fr/two-factor-enabled.html +9 -9
- package/mails/fr/verify-email.html +9 -9
- package/mails/fr/welcome.html +50 -0
- package/mails/fr/welcome.txt +19 -0
- package/mails/mail-manifest.json +24 -0
- package/package.json +7 -6
package/dist/types.d.ts
CHANGED
|
@@ -4,75 +4,11 @@
|
|
|
4
4
|
* once, in `janusMail()`.
|
|
5
5
|
*/
|
|
6
6
|
import type { Clock, IssuedCode, IssuedToken } from '@nxgt/janus';
|
|
7
|
-
import type { Address, Mailer,
|
|
7
|
+
import type { Address, Mailer, SentMail, WantedLocales } from '@nxgt/mail';
|
|
8
8
|
import type { JanusMailLocale } from './generated/locales';
|
|
9
|
+
import type { JanusMailTemplates } from './template-types';
|
|
9
10
|
export type { JanusMailLocale } from './generated/locales';
|
|
10
|
-
|
|
11
|
-
* What each e-mail's template is given, by template name. Every value is
|
|
12
|
-
* text: `brand` is the name `janusMail({ brand })` was given, `name` the
|
|
13
|
-
* recipient's, `link` an absolute URL from `links`, and `expiresIn` how long
|
|
14
|
-
* the link or the code stays valid, in the recipient's locale — "1 hour",
|
|
15
|
-
* "1 heure" — or the send's own `expiresIn`.
|
|
16
|
-
*
|
|
17
|
-
* Held equal to the build's variables by `test/types/variables.ts`: an e-mail
|
|
18
|
-
* of the build that gains or loses a variable fails the typecheck there.
|
|
19
|
-
*/
|
|
20
|
-
export interface JanusMailVariables {
|
|
21
|
-
readonly verifyEmail: {
|
|
22
|
-
readonly brand: string;
|
|
23
|
-
readonly name: string;
|
|
24
|
-
readonly link: string;
|
|
25
|
-
readonly expiresIn: string;
|
|
26
|
-
};
|
|
27
|
-
readonly resetPassword: {
|
|
28
|
-
readonly brand: string;
|
|
29
|
-
readonly name: string;
|
|
30
|
-
readonly link: string;
|
|
31
|
-
readonly expiresIn: string;
|
|
32
|
-
};
|
|
33
|
-
/** No `challenge`, ever: it is the visitor's secret, and never goes in an e-mail. */
|
|
34
|
-
readonly signInCode: {
|
|
35
|
-
readonly brand: string;
|
|
36
|
-
readonly code: string;
|
|
37
|
-
readonly expiresIn: string;
|
|
38
|
-
};
|
|
39
|
-
readonly passwordChanged: {
|
|
40
|
-
readonly brand: string;
|
|
41
|
-
readonly name: string;
|
|
42
|
-
readonly link: string;
|
|
43
|
-
};
|
|
44
|
-
readonly emailChanged: {
|
|
45
|
-
readonly brand: string;
|
|
46
|
-
readonly name: string;
|
|
47
|
-
readonly link: string;
|
|
48
|
-
readonly newEmail: string;
|
|
49
|
-
};
|
|
50
|
-
readonly twoFactorEnabled: {
|
|
51
|
-
readonly brand: string;
|
|
52
|
-
readonly name: string;
|
|
53
|
-
readonly link: string;
|
|
54
|
-
};
|
|
55
|
-
readonly twoFactorDisabled: {
|
|
56
|
-
readonly brand: string;
|
|
57
|
-
readonly name: string;
|
|
58
|
-
readonly link: string;
|
|
59
|
-
};
|
|
60
|
-
}
|
|
61
|
-
/** The name of one of the seven templates: `verifyEmail`, `resetPassword`, … */
|
|
62
|
-
export type JanusMailTemplateName = keyof JanusMailVariables;
|
|
63
|
-
/**
|
|
64
|
-
* One e-mail: its variables and the locale picked for the recipient, in; its
|
|
65
|
-
* `subject`, `html` and `text`, out — escaping is then the function's job. A
|
|
66
|
-
* default, from the prebuilt e-mails, or yours: React Email, a string, or
|
|
67
|
-
* another Maizzle build.
|
|
68
|
-
*/
|
|
69
|
-
export type JanusMailTemplate<V, L extends string = JanusMailLocale> = (variables: V & {
|
|
70
|
-
readonly locale: L;
|
|
71
|
-
}) => Rendered | PromiseLike<Rendered>;
|
|
72
|
-
/** The seven templates, each for the locales `L`. */
|
|
73
|
-
export type JanusMailTemplates<L extends string = JanusMailLocale> = {
|
|
74
|
-
readonly [K in JanusMailTemplateName]: JanusMailTemplate<JanusMailVariables[K], L>;
|
|
75
|
-
};
|
|
11
|
+
export type { JanusMailTemplate, JanusMailTemplateName, JanusMailTemplates, JanusMailVariables, } from './template-types';
|
|
76
12
|
/**
|
|
77
13
|
* Who an e-mail is for, besides the address: the name it greets, and the
|
|
78
14
|
* locales they want, most wanted first — their stored locale, then their
|
|
@@ -105,6 +41,12 @@ export interface JanusMailLinks {
|
|
|
105
41
|
* and `twoFactorDisabled` link to it.
|
|
106
42
|
*/
|
|
107
43
|
readonly secureAccount: () => string;
|
|
44
|
+
/**
|
|
45
|
+
* Where a new user starts — your application's home, or its sign-in page
|
|
46
|
+
* for an account someone else created. `welcome` links to it, from its
|
|
47
|
+
* "Get started" button.
|
|
48
|
+
*/
|
|
49
|
+
readonly getStarted: () => string;
|
|
108
50
|
}
|
|
109
51
|
/** The options every locale set shares. */
|
|
110
52
|
interface JanusMailBaseOptions<L extends string> {
|
|
@@ -196,6 +138,14 @@ export interface JanusMail<L extends string = JanusMailLocale> {
|
|
|
196
138
|
twoFactorDisabled(to: Recipient & {
|
|
197
139
|
readonly email: string;
|
|
198
140
|
}): Promise<SentMail>;
|
|
141
|
+
/**
|
|
142
|
+
* Welcomes `to.email` to the brand, with a link to get started — on the
|
|
143
|
+
* `user.created` event, which `auth.create` and `auth.signUp` send once
|
|
144
|
+
* the user is inserted.
|
|
145
|
+
*/
|
|
146
|
+
welcome(to: Recipient & {
|
|
147
|
+
readonly email: string;
|
|
148
|
+
}): Promise<SentMail>;
|
|
199
149
|
/** The templates in use: the defaults, with `templates` over them. */
|
|
200
150
|
readonly templates: JanusMailTemplates<L>;
|
|
201
151
|
/** The locales sent in. */
|
package/dist/types.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AACH,OAAO,KAAK,EAAE,KAAK,EAAE,UAAU,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAClE,OAAO,KAAK,
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"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"}
|
package/docs/README.md
CHANGED
|
@@ -5,11 +5,11 @@ the detail.
|
|
|
5
5
|
|
|
6
6
|
| Page | Read it when |
|
|
7
7
|
| --- | --- |
|
|
8
|
-
| [Sending](guide/sending.md) | You are wiring `janusMail()` into your routes: the options
|
|
8
|
+
| [Sending](guide/sending.md) | You are wiring `janusMail()` into your routes: the options, `clock` among them; each method, the welcome on `user.created` among them; the expiry an e-mail states and how to set it, where each e-mail goes, what a failure looks like, retrying and tracing the mailer, and a test |
|
|
9
9
|
| [Templates](guide/templates.md) | You want an e-mail of your own: what a template takes and answers, a partial override, `janusTemplates()`, and what the defaults say |
|
|
10
10
|
| [Locales](guide/locales.md) | You send in more than one language: how the locale is picked, `fallbackLocale`, fewer locales, and adding one |
|
|
11
11
|
| [Building](guide/building.md) | You work on this package: the Maizzle project in `mail/`, `mails/`, the generated files, and why none of it reaches a consumer's build |
|
|
12
|
-
| [Troubleshooting](troubleshooting.md) | A `TypeError` at wiring or at a call, a `MailRefused`, a `MailFailure`,
|
|
12
|
+
| [Troubleshooting](troubleshooting.md) | A `TypeError` at wiring or at a call, a `MailRefused`, a `MailFailure`, a compile error, or an e-mail that reads wrong in dark or light mode |
|
|
13
13
|
| [Roadmap](roadmap.md) | You want to know what is coming, and what is deliberately not planned |
|
|
14
14
|
|
|
15
15
|
## Words
|
|
@@ -23,13 +23,14 @@ They mean the same here. These pages add:
|
|
|
23
23
|
|
|
24
24
|
| Word | Means | Not |
|
|
25
25
|
| --- | --- | --- |
|
|
26
|
-
| **e-mail** | One of the
|
|
26
|
+
| **e-mail** | One of the eight this package sends: `verify-email`, `reset-password`, `sign-in-code`, `password-changed`, `email-changed`, `two-factor-enabled`, `two-factor-disabled`, `welcome` — by the name of its built file | "mail", "message" — a *message* is what a mailer sends, addressed |
|
|
27
27
|
| **template** | The function that renders one e-mail: `(variables & { locale }) => Rendered`. Named in camelCase, after the method that sends it: `verifyEmail`, `signInCode` | "renderer" — the renderer is `@nxgt/mail`'s, and reads the build |
|
|
28
28
|
| **default template** | A template of `janusTemplates()`: the prebuilt e-mail, filled by the renderer | "preset" — a preset is `@nxgt/mail-presets`' source, which the build compiles |
|
|
29
29
|
| **override** | A template of yours, passed in `templates`, used instead of the default | "custom template" |
|
|
30
30
|
| **issued** | What a flow answered: an `IssuedToken` from `verifyEmail.send` or `resetPassword.request`, an `IssuedCode` from `signInCode.request` | "result", "payload" |
|
|
31
31
|
| **recipient** | Who an e-mail is for besides the address: a `Recipient`, `{ name, locale? }` | "user" — a user is a record of `@nxgt/janus`; "to" is the address |
|
|
32
32
|
| **notice** | An e-mail that tells, and asks for nothing: `passwordChanged`, `emailChanged`, `twoFactorEnabled`, `twoFactorDisabled`. It links to `links.secureAccount()` | "alert", "notification" |
|
|
33
|
+
| **welcome** | The e-mail sent once a user was created, on `@nxgt/janus`'s `user.created`: `welcome`. It links to `links.getStarted()` — not a notice, since it warns of nothing | "onboarding", "greeting" |
|
|
33
34
|
| **expiry** | How long a link or a code lasts, as the e-mail says it: `expiresIn`, derived from the flow's `expiresAt` in the recipient's locale — "1 hour", "1 heure" — or passed per send | "TTL", "lifetime" — the `tokens` TTL is `@nxgt/janus`'s setting, which sets `expiresAt` |
|
|
34
35
|
| **brand** | The name the e-mails show, `janusMail({ brand })`: text, filled at send time | "logo", "company" |
|
|
35
36
|
| **the build** | The e-mails as `scripts/build-mail.ts` writes them to `mails/`: HTML and text per locale, and the manifest | "dist" — `mails/` is not `dist/` |
|
package/docs/guide/building.md
CHANGED
|
@@ -43,7 +43,7 @@ packages/janus-mail/
|
|
|
43
43
|
`src/generated/mail.ts` (`rendererTypes`), from the build.
|
|
44
44
|
3. `scripts/build-mail.ts` reads the manifest and **fails unless its
|
|
45
45
|
format is one every `@nxgt/mail` the peer admits reads** — see below —
|
|
46
|
-
and **unless exactly the
|
|
46
|
+
and **unless exactly the eight e-mails were built**: a ninth from a new
|
|
47
47
|
`@nxgt/mail-presets`, or one missing, stops the build.
|
|
48
48
|
4. It writes `src/generated/locales.ts` from the manifest's locales, only
|
|
49
49
|
when its content changed.
|
|
@@ -53,10 +53,12 @@ Then `../../build.ts` builds `dist/`, as for every package.
|
|
|
53
53
|
## The manifest's format keeps the peer honest
|
|
54
54
|
|
|
55
55
|
The package peers `@nxgt/mail` at `>=0.1.0 <1`, so the build it ships must
|
|
56
|
-
be readable by `@nxgt/mail` 0.1.0
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
`@nxgt/mail`
|
|
56
|
+
be readable by `@nxgt/mail` 0.1.0 — the peer's floor, which the package's
|
|
57
|
+
specs and its typecheck run on in the Floors job, on every CI run. The
|
|
58
|
+
manifest says which format it is in — `formatVersion`, its first key,
|
|
59
|
+
`MANIFEST_FORMAT` of the `@nxgt/mail-i18n` that built it — and within 0.x a
|
|
60
|
+
renderer reads every format up to its own.
|
|
61
|
+
`@nxgt/mail` 0.1.0 through 0.9.0 read format 1, and a manifest without the
|
|
60
62
|
field is format 1.
|
|
61
63
|
|
|
62
64
|
`formatProblem` in `scripts/build-mail.ts` (spec'd beside it) fails the build
|
package/docs/guide/locales.md
CHANGED
|
@@ -85,6 +85,7 @@ const templates: JanusMailTemplates<Locale> = {
|
|
|
85
85
|
emailChanged: ({ name, link, newEmail, locale }) => render('email-changed', locale, { name, link, newEmail }),
|
|
86
86
|
twoFactorEnabled: ({ name, link, locale }) => render('two-factor-enabled', locale, { name, link }),
|
|
87
87
|
twoFactorDisabled: ({ name, link, locale }) => render('two-factor-disabled', locale, { name, link }),
|
|
88
|
+
welcome: ({ name, link, locale }) => render('welcome', locale, { name, link }),
|
|
88
89
|
};
|
|
89
90
|
|
|
90
91
|
const mail = janusMail({ mailer, from, brand: 'Acme', links, locales: ['en', 'fr', 'de'], templates });
|
|
@@ -99,13 +100,13 @@ With only some templates, the call does not compile — the error lands on
|
|
|
99
100
|
`templates`:
|
|
100
101
|
|
|
101
102
|
```text
|
|
102
|
-
TS2739: Type '{ verifyEmail: () => Rendered; }' is missing the following properties from type 'JanusMailTemplates<"en" | "fr" | "de">': resetPassword, signInCode, passwordChanged, emailChanged, twoFactorEnabled, twoFactorDisabled
|
|
103
|
+
TS2739: Type '{ verifyEmail: () => Rendered; }' is missing the following properties from type 'JanusMailTemplates<"en" | "fr" | "de">': resetPassword, signInCode, passwordChanged, emailChanged, twoFactorEnabled, twoFactorDisabled, welcome
|
|
103
104
|
```
|
|
104
105
|
|
|
105
106
|
and in JavaScript it throws when `janusMail()` is called:
|
|
106
107
|
|
|
107
108
|
```text
|
|
108
|
-
TypeError: janusMail: the default templates are built in en and fr only — with another locale in locales, pass every template in templates; resetPassword, signInCode, passwordChanged, emailChanged, twoFactorEnabled, twoFactorDisabled missing
|
|
109
|
+
TypeError: janusMail: the default templates are built in en and fr only — with another locale in locales, pass every template in templates; resetPassword, signInCode, passwordChanged, emailChanged, twoFactorEnabled, twoFactorDisabled, welcome missing
|
|
109
110
|
```
|
|
110
111
|
|
|
111
112
|
A default template is typed for `'en' | 'fr'`, so reusing one in a wider set
|
|
@@ -118,4 +119,4 @@ Built-in locales for more languages are on the [roadmap](../roadmap.md).
|
|
|
118
119
|
|
|
119
120
|
- `@nxgt/mail`'s [locales guide](https://github.com/softistx/nxgt-mail/blob/develop/packages/mail/docs/guide/locales.md)
|
|
120
121
|
— `pickLocale` and `parseAcceptLanguage` in full.
|
|
121
|
-
- [Templates](templates.md) — writing the
|
|
122
|
+
- [Templates](templates.md) — writing the eight for a new locale.
|
package/docs/guide/sending.md
CHANGED
|
@@ -26,6 +26,7 @@ export const mail = janusMail({
|
|
|
26
26
|
verifyEmail: (token) => `https://acme.example/verify?token=${encodeURIComponent(token)}`,
|
|
27
27
|
resetPassword: (token) => `https://acme.example/reset?token=${encodeURIComponent(token)}`,
|
|
28
28
|
secureAccount: () => 'https://acme.example/account/security',
|
|
29
|
+
getStarted: () => 'https://acme.example/',
|
|
29
30
|
},
|
|
30
31
|
});
|
|
31
32
|
```
|
|
@@ -45,6 +46,7 @@ time one is sent, and kept.
|
|
|
45
46
|
| `links.verifyEmail` | `(token) => string` | required | The page that confirms an address, given the one-time token |
|
|
46
47
|
| `links.resetPassword` | `(token) => string` | required | The page that sets a new password, given the one-time token |
|
|
47
48
|
| `links.secureAccount` | `() => string` | required | Where a user who made no change secures their account: the notices link to it |
|
|
49
|
+
| `links.getStarted` | `() => string` | required | Where a new user starts — your home page, or your sign-in page for an account someone else created: the welcome's **Get started** button links to it |
|
|
48
50
|
| `locales` | `readonly L[]` | `['en', 'fr']` | The locales sent in — see [Locales](locales.md) |
|
|
49
51
|
| `fallbackLocale` | one of `locales` | `'en'`, else the first of `locales` | The locale when the recipient wants none of `locales` |
|
|
50
52
|
| `templates` | `Partial<JanusMailTemplates<L>>` | none | Your own templates, over the defaults — see [Templates](templates.md) |
|
|
@@ -134,6 +136,68 @@ since whoever changed it already controls the new one. It names `newEmail`,
|
|
|
134
136
|
and links to `links.secureAccount()`. Read the former address before the
|
|
135
137
|
update: the user `update` answers already holds the new one.
|
|
136
138
|
|
|
139
|
+
### `twoFactorEnabled(to)` and `twoFactorDisabled(to)`
|
|
140
|
+
|
|
141
|
+
```ts
|
|
142
|
+
// On the user events @nxgt/janus sends once the write landed:
|
|
143
|
+
const auth = janus({
|
|
144
|
+
...config,
|
|
145
|
+
async events(event) {
|
|
146
|
+
if (event.type !== 'user.secondFactorEnabled' && event.type !== 'user.secondFactorDisabled') return;
|
|
147
|
+
const user = await auth.get(event.userId);
|
|
148
|
+
const to = { name: user.name, locale: user.locale, email: user.email };
|
|
149
|
+
await (event.type === 'user.secondFactorEnabled' ? mail.twoFactorEnabled(to) : mail.twoFactorDisabled(to));
|
|
150
|
+
},
|
|
151
|
+
});
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
Two notices: two-factor authentication was turned on, or off, for the
|
|
155
|
+
account at `to.email`, each with a link to `links.secureAccount()` — the
|
|
156
|
+
user's security settings — for a user who did not make the change. Send
|
|
157
|
+
`twoFactorEnabled` on the `user.secondFactorEnabled` event, which
|
|
158
|
+
`auth.secondFactor.activate` sends once the factor is active, and
|
|
159
|
+
`twoFactorDisabled` on `user.secondFactorDisabled`, which
|
|
160
|
+
`auth.secondFactor.disable` sends only when it removed an active factor: a
|
|
161
|
+
`disable` on a user who had none tells nobody anything. The event names the
|
|
162
|
+
user by id alone, so read the name, the locale and the address from the
|
|
163
|
+
user. Calling them right after `activate` or `disable` answered works as
|
|
164
|
+
well — without the events, check `hasSecondFactor` before the `disable`.
|
|
165
|
+
|
|
166
|
+
### `welcome(to)`
|
|
167
|
+
|
|
168
|
+
```ts
|
|
169
|
+
// On the user event @nxgt/janus sends once the user is inserted:
|
|
170
|
+
const auth = janus({
|
|
171
|
+
...config,
|
|
172
|
+
async events(event) {
|
|
173
|
+
if (event.type !== 'user.created') return;
|
|
174
|
+
const user = await auth.get(event.userId);
|
|
175
|
+
await mail.welcome({ name: user.name, locale: user.locale, email: user.email });
|
|
176
|
+
},
|
|
177
|
+
});
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
Welcomes the new user at `to.email` — "Welcome, Ada", "Bienvenue, Ada", the
|
|
181
|
+
name in the subject too — with a **Get started** button that links to
|
|
182
|
+
`links.getStarted()`: your home page, or your sign-in page when the account
|
|
183
|
+
was created for the user rather than by them. Send it on the `user.created`
|
|
184
|
+
event, which `auth.signUp` and `auth.create` send once the user is inserted —
|
|
185
|
+
even when `signUp`'s session then fails to open — and never for a sign-up
|
|
186
|
+
refused. The event names the user by id alone, so read the name, the locale
|
|
187
|
+
and the address from the user. With several user types, check
|
|
188
|
+
`event.userType` first: a type with no e-mail has nobody to welcome.
|
|
189
|
+
|
|
190
|
+
- **It goes out before the address is verified.** A sign-up with a mistyped
|
|
191
|
+
address welcomes whoever holds it, and with open sign-up anyone can make
|
|
192
|
+
your brand send "Welcome, <any name>" to any address. To welcome proven addresses only, send
|
|
193
|
+
it on `user.emailVerified` instead, sent once the address is proven —
|
|
194
|
+
and again after an address changed and was proven anew, so welcome only
|
|
195
|
+
a user you have not welcomed before.
|
|
196
|
+
- **A failed welcome fails no sign-up.** `janus()` awaits the listener, but a
|
|
197
|
+
listener that throws is a `JANUS_EVENT_FAILED` warning: the user exists,
|
|
198
|
+
and `signUp` answers as it would have. For a welcome that must arrive, put
|
|
199
|
+
the event in a queue that retries a `MAIL_FAILED`, and send from there.
|
|
200
|
+
|
|
137
201
|
## The expiry
|
|
138
202
|
|
|
139
203
|
`verifyEmail`, `resetPassword` and `signInCode` say how long the link or the
|
|
@@ -178,33 +242,6 @@ through JSON on its way to a queue holds a string — or is already past, is
|
|
|
178
242
|
a `TypeError`, and nothing reaches the mailer: revive the date with
|
|
179
243
|
`new Date(issued.expiresAt)` when the job runs, or pass `expiresIn`.
|
|
180
244
|
|
|
181
|
-
### `twoFactorEnabled(to)` and `twoFactorDisabled(to)`
|
|
182
|
-
|
|
183
|
-
```ts
|
|
184
|
-
// On the user events @nxgt/janus sends once the write landed:
|
|
185
|
-
const auth = janus({
|
|
186
|
-
...config,
|
|
187
|
-
async events(event) {
|
|
188
|
-
if (event.type !== 'user.secondFactorEnabled' && event.type !== 'user.secondFactorDisabled') return;
|
|
189
|
-
const user = await auth.get(event.userId);
|
|
190
|
-
const to = { name: user.name, locale: user.locale, email: user.email };
|
|
191
|
-
await (event.type === 'user.secondFactorEnabled' ? mail.twoFactorEnabled(to) : mail.twoFactorDisabled(to));
|
|
192
|
-
},
|
|
193
|
-
});
|
|
194
|
-
```
|
|
195
|
-
|
|
196
|
-
Two notices: two-factor authentication was turned on, or off, for the
|
|
197
|
-
account at `to.email`, each with a link to `links.secureAccount()` — the
|
|
198
|
-
user's security settings — for a user who did not make the change. Send
|
|
199
|
-
`twoFactorEnabled` on the `user.secondFactorEnabled` event, which
|
|
200
|
-
`auth.secondFactor.activate` sends once the factor is active, and
|
|
201
|
-
`twoFactorDisabled` on `user.secondFactorDisabled`, which
|
|
202
|
-
`auth.secondFactor.disable` sends only when it removed an active factor: a
|
|
203
|
-
`disable` on a user who had none tells nobody anything. The event names the
|
|
204
|
-
user by id alone, so read the name, the locale and the address from the
|
|
205
|
-
user. Calling them right after `activate` or `disable` answered works as
|
|
206
|
-
well — without the events, check `hasSecondFactor` before the `disable`.
|
|
207
|
-
|
|
208
245
|
## Where each e-mail goes
|
|
209
246
|
|
|
210
247
|
| Method | To | Never to |
|
|
@@ -215,6 +252,7 @@ well — without the events, check `hasSecondFactor` before the `disable`.
|
|
|
215
252
|
| `passwordChanged` | `to.email` | — |
|
|
216
253
|
| `emailChanged` | `to.formerEmail` | `to.newEmail` |
|
|
217
254
|
| `twoFactorEnabled`, `twoFactorDisabled` | `to.email` | — |
|
|
255
|
+
| `welcome` | `to.email` | — |
|
|
218
256
|
|
|
219
257
|
`issued.email` is the address the user's record holds, as they registered
|
|
220
258
|
it; what the visitor typed may differ in case or spacing. The recipient is a
|
|
@@ -268,10 +306,11 @@ retries a `MAIL_FAILED`, rather than to a promise nobody awaits.
|
|
|
268
306
|
|
|
269
307
|
### Retrying and tracing the mailer
|
|
270
308
|
|
|
271
|
-
|
|
272
|
-
`
|
|
273
|
-
key for every attempt, and never retries a `MailRefused`;
|
|
274
|
-
from `@nxgt/mail/telemetry`
|
|
309
|
+
Wrap the mailer before you hand it to `janusMail()`: `withRetry`, from
|
|
310
|
+
`@nxgt/mail` 0.8, retries a `MailFailure` with backoff, reusing one
|
|
311
|
+
idempotency key for every attempt, and never retries a `MailRefused`;
|
|
312
|
+
`withMailTelemetry`, from `@nxgt/mail/telemetry` 0.9 (`withTelemetry` on
|
|
313
|
+
0.8), opens a `mail.send` span per send. That entry
|
|
275
314
|
needs `@opentelemetry/api` installed (an optional peer, loaded by it alone);
|
|
276
315
|
with no OpenTelemetry SDK registered, its spans are no-ops. Put telemetry on the outside, so one
|
|
277
316
|
send is one span:
|
|
@@ -279,7 +318,7 @@ send is one span:
|
|
|
279
318
|
```ts
|
|
280
319
|
import { janusMail } from '@nxgt/janus-mail';
|
|
281
320
|
import { withRetry } from '@nxgt/mail';
|
|
282
|
-
import {
|
|
321
|
+
import { withMailTelemetry } from '@nxgt/mail/telemetry';
|
|
283
322
|
import { createSmtpMailer } from '@nxgt/mail-smtp';
|
|
284
323
|
|
|
285
324
|
export const mail = janusMail({
|
|
@@ -290,12 +329,14 @@ export const mail = janusMail({
|
|
|
290
329
|
});
|
|
291
330
|
```
|
|
292
331
|
|
|
293
|
-
**
|
|
294
|
-
|
|
295
|
-
telemetry to a block.
|
|
296
|
-
|
|
297
|
-
`@nxgt/janus-telemetry`'s spans.
|
|
298
|
-
|
|
332
|
+
**Two telemetries, unrelated.** `@nxgt/telemetry` — which
|
|
333
|
+
`@nxgt/janus-telemetry` peers — exports `withTelemetry(telemetry, fn)`,
|
|
334
|
+
which scopes a telemetry to a block. `mail.send` is recorded through
|
|
335
|
+
OpenTelemetry, not `@nxgt/telemetry`, so it does not nest under
|
|
336
|
+
`@nxgt/janus-telemetry`'s spans. `@nxgt/mail` 0.9 renamed its decorator
|
|
337
|
+
`withMailTelemetry` so the two no longer share a name; on 0.8 it is
|
|
338
|
+
`withTelemetry`, still exported by 0.9 as a deprecated alias until 1.0 —
|
|
339
|
+
import it under another name there when a module uses both.
|
|
299
340
|
|
|
300
341
|
**SMTP ignores the idempotency key**, so a retry after an ambiguous SMTP
|
|
301
342
|
timeout can deliver an e-mail twice. If a duplicate sign-in code or reset
|
|
@@ -322,6 +363,7 @@ const links = {
|
|
|
322
363
|
verifyEmail: (token: string) => `https://acme.example/verify?token=${token}`,
|
|
323
364
|
resetPassword: (token: string) => `https://acme.example/reset?token=${token}`,
|
|
324
365
|
secureAccount: () => 'https://acme.example/account/security',
|
|
366
|
+
getStarted: () => 'https://acme.example/',
|
|
325
367
|
};
|
|
326
368
|
|
|
327
369
|
it('sends the code, and never the challenge', async () => {
|
package/docs/guide/templates.md
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# Templates
|
|
2
2
|
|
|
3
3
|
This page is for changing what an e-mail says or looks like: what a template
|
|
4
|
-
is given and answers, replacing some of the
|
|
4
|
+
is given and answers, replacing some of the eight, using the defaults on their
|
|
5
5
|
own, and what the defaults say.
|
|
6
6
|
|
|
7
7
|
```ts
|
|
@@ -22,7 +22,7 @@ const mail = janusMail({
|
|
|
22
22
|
});
|
|
23
23
|
```
|
|
24
24
|
|
|
25
|
-
The sign-in code e-mail is now yours; the
|
|
25
|
+
The sign-in code e-mail is now yours; the seven others stay the defaults.
|
|
26
26
|
|
|
27
27
|
## What a template is
|
|
28
28
|
|
|
@@ -66,10 +66,12 @@ e-mail.
|
|
|
66
66
|
| `emailChanged` | `brand`, `name`, `link`, `newEmail` | `mail.emailChanged(to)` |
|
|
67
67
|
| `twoFactorEnabled` | `brand`, `name`, `link` | `mail.twoFactorEnabled(to)` |
|
|
68
68
|
| `twoFactorDisabled` | `brand`, `name`, `link` | `mail.twoFactorDisabled(to)` |
|
|
69
|
+
| `welcome` | `brand`, `name`, `link` | `mail.welcome(to)` |
|
|
69
70
|
|
|
70
71
|
Every one also gets `locale`, one of `locales`. Every value is a string:
|
|
71
72
|
`brand` from the options, `name` from the recipient, `link` from `links` —
|
|
72
|
-
`links.secureAccount()` for the four notices
|
|
73
|
+
`links.secureAccount()` for the four notices, `links.getStarted()` for
|
|
74
|
+
`welcome` — `code` and `newEmail` from the
|
|
73
75
|
call, and `expiresIn` derived from the flow's `expiresAt` in the locale, or
|
|
74
76
|
the send's own `expiresIn` ([Sending](sending.md#the-expiry)).
|
|
75
77
|
|
|
@@ -103,11 +105,11 @@ methods live on its prototype and are refused — see
|
|
|
103
105
|
[troubleshooting](../troubleshooting.md#janusmail-templatesname-is-not-an-own-enumerable-property--pass-a-plain-object-as--name-variables--rendered-).
|
|
104
106
|
|
|
105
107
|
`templates` is `Partial` while `locales` stays within `en` and `fr`: give
|
|
106
|
-
any of the
|
|
107
|
-
another locale, it takes **all
|
|
108
|
+
any of the eight, and the defaults render the rest. Once `locales` holds
|
|
109
|
+
another locale, it takes **all eight** — see
|
|
108
110
|
[Adding a locale](locales.md#adding-a-locale).
|
|
109
111
|
|
|
110
|
-
`mail.templates` answers the
|
|
112
|
+
`mail.templates` answers the eight in use — yours, and the defaults for the
|
|
111
113
|
rest — frozen:
|
|
112
114
|
|
|
113
115
|
```ts
|
|
@@ -129,7 +131,7 @@ const { subject, html, text } = await defaults.resetPassword({
|
|
|
129
131
|
});
|
|
130
132
|
```
|
|
131
133
|
|
|
132
|
-
It answers the
|
|
134
|
+
It answers the eight defaults, typed for `'en' | 'fr'`, without a mailer:
|
|
133
135
|
to preview an e-mail, to wrap one — add a line to the default text, say — or
|
|
134
136
|
to send one through something other than `janusMail()`. A default reused for
|
|
135
137
|
a wider set of locales is a compile error: it could not render the one it
|
|
@@ -155,7 +157,7 @@ them renders: that is when the package's `mails/` folder is read, once.
|
|
|
155
157
|
## What the defaults say
|
|
156
158
|
|
|
157
159
|
The defaults are [`@nxgt/mail-presets`](https://www.npmjs.com/package/@nxgt/mail-presets)
|
|
158
|
-
0.4.
|
|
160
|
+
0.4.3, built with a neutral grey theme and no logo:
|
|
159
161
|
|
|
160
162
|
| Template | Built from | Subject (`en`) | Subject (`fr`) |
|
|
161
163
|
| --- | --- | --- | --- |
|
|
@@ -166,11 +168,14 @@ The defaults are [`@nxgt/mail-presets`](https://www.npmjs.com/package/@nxgt/mail
|
|
|
166
168
|
| `emailChanged` | `email-changed` | Your e-mail address was changed | Votre adresse e-mail a été modifiée |
|
|
167
169
|
| `twoFactorEnabled` | `two-factor-enabled` | Two-factor authentication was turned on | L'authentification à deux facteurs a été activée |
|
|
168
170
|
| `twoFactorDisabled` | `two-factor-disabled` | Two-factor authentication was turned off | L'authentification à deux facteurs a été désactivée |
|
|
171
|
+
| `welcome` | `welcome` | Welcome, `Ada` | Bienvenue, `Ada` |
|
|
169
172
|
|
|
170
173
|
Each has a header and a footer with the brand, a heading, a greeting by name
|
|
171
174
|
(but `signInCode`), a button with its link and the link again in text for a
|
|
172
175
|
client that shows no button, and a text part. The four notices add a warning:
|
|
173
|
-
*if this was not you, secure your account now*.
|
|
176
|
+
*if this was not you, secure your account now*. `welcome` has the brand in
|
|
177
|
+
its heading — "Welcome to Acme" — and the recipient's name in its subject,
|
|
178
|
+
filled at send time like the body; its button says **Get started**.
|
|
174
179
|
|
|
175
180
|
`verifyEmail`, `resetPassword` and `signInCode` also say how long the link
|
|
176
181
|
or the code lasts: "This link expires in 1 hour.", "Ce code expire dans
|
|
@@ -186,11 +191,18 @@ is near-black (`#27272a`) with light text in light mode, and near-white
|
|
|
186
191
|
dark card, where the light primary would be 1.2:1. The page behind the card
|
|
187
192
|
is 5% of the light primary over the dark background, a shade darker than the
|
|
188
193
|
card, which its border outlines. The sign-in code's box is a light grey
|
|
189
|
-
(`#f1f5f9`)
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
+
(`#f1f5f9`) with near-black code (`#020918`) in light mode, and a slate a
|
|
195
|
+
step above the dark card (`#1e293b`) with light code (`#cbd5e1`) in dark
|
|
196
|
+
mode, 9.85:1. The muted text — the footer, and the closing *if you did not
|
|
197
|
+
ask for this* — is a slate grey (`#5f718a`) in light mode, 4.53:1 on the
|
|
198
|
+
page, and turns `#cbd5e1` in dark mode, 12.01:1 on the dark card and
|
|
199
|
+
13.31:1 on the page. The notices' alerts keep their light ground and grey
|
|
200
|
+
text in both modes, 4.56:1 at the least. Every
|
|
201
|
+
text of the defaults reads at 4.5:1 or more in both modes, but the link
|
|
202
|
+
under the button, which `@nxgt/mail-ui` colours with its info blue in
|
|
203
|
+
both: 6.78:1 on the dark card, 2.63:1 on the light one, and no theme token
|
|
204
|
+
moves it without breaking the other mode. Nothing is passed for any of it,
|
|
205
|
+
the text part has no colours, and a template of your own gets none of it. `@nxgt/mail-ui`'s
|
|
194
206
|
[Dark mode](https://github.com/softistx/nxgt-mail/blob/develop/packages/mail-ui/docs/guide/dark-mode.md)
|
|
195
207
|
guide has the technique, client by client.
|
|
196
208
|
|
package/docs/roadmap.md
CHANGED
|
@@ -9,10 +9,9 @@ Nothing between releases.
|
|
|
9
9
|
|
|
10
10
|
## Next
|
|
11
11
|
|
|
12
|
-
- **The other presets** — `magic-link`, `new-sign-in
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
an invitation to a user type.
|
|
12
|
+
- **The other presets** — `magic-link`, `new-sign-in` and `invitation` from
|
|
13
|
+
`@nxgt/mail-presets`, each once `@nxgt/janus` has a flow that sends it: a
|
|
14
|
+
sign-in link, a sign-in from a new device, an invitation to a user type.
|
|
16
15
|
- **The other presets of `@nxgt/mail-presets` 0.4.0** — `account-deleted`,
|
|
17
16
|
after a user is deleted, once `@nxgt/janus` has a deletion a link can
|
|
18
17
|
undo; and `invitation-accepted`, once it has invitations.
|
|
@@ -48,6 +47,22 @@ Nothing between releases.
|
|
|
48
47
|
|
|
49
48
|
Newest first; from the first release on, the package's CHANGELOG holds every one.
|
|
50
49
|
|
|
50
|
+
- **Muted text at 4.5:1 or more in both modes, v0.4.1.** Under dark mode the
|
|
51
|
+
sign-in code's box is a slate a step above the dark card with light code on
|
|
52
|
+
it, 9.85:1, and the muted text turns light, 13.31:1 on the page and
|
|
53
|
+
12.01:1 on the card; in light mode
|
|
54
|
+
the muted grey is a shade darker, 4.53:1 on the page and 4.56:1 on the
|
|
55
|
+
notices' alerts at the least. Built from `@nxgt/mail-ui` 0.7.0 and
|
|
56
|
+
`@nxgt/mail-presets` 0.4.3; the text parts, subjects and variables are
|
|
57
|
+
unchanged, the manifest is still format 1, and the `@nxgt/mail` peer stays
|
|
58
|
+
`>=0.1.0 <1`.
|
|
59
|
+
- **The welcome e-mail, v0.4.0.** `welcome(to)`, built from
|
|
60
|
+
`@nxgt/mail-presets` 0.4.2's `welcome` in English and French — "Welcome,
|
|
61
|
+
Ada" — with a **Get started** button linking to the new
|
|
62
|
+
`links.getStarted()`: send it on `@nxgt/janus`'s `user.created` event.
|
|
63
|
+
`links` now requires `getStarted`, and a locale beyond `en` and `fr` takes
|
|
64
|
+
eight templates.
|
|
65
|
+
|
|
51
66
|
- **A sign-in code box that is not a light slab in dark mode, v0.3.2.** Under
|
|
52
67
|
dark mode the code's box is a mid slate, 6.95:1 against the dark card, with
|
|
53
68
|
the near-black code at 7.76:1 on it, and the page behind the card is darker
|