@nxgt/janus-mail 0.5.1 → 0.6.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 CHANGED
@@ -47,7 +47,7 @@ bun add -d typescript
47
47
 
48
48
  Three peers, all required: `@nxgt/mail` (0.1 or later, below 2 — 1.0 and
49
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,
50
+ `@nxgt/janus` (0.14), whose flows' answers the methods take — types only,
51
51
  nothing of it is loaded; and `typescript` (6). **No Maizzle, no Vue, no
52
52
  Tailwind**: they run at this package's build, not in yours.
53
53
 
@@ -144,21 +144,35 @@ if (issued !== null) {
144
144
  await mail.verifyEmail(issued, { name: user.name, locale: 'fr' }, { expiresIn: '24 heures' });
145
145
  ```
146
146
 
147
- ### Telling a user their password changed
147
+ ### Telling a user their password or e-mail changed
148
148
 
149
- ```ts
150
- const changed = await auth.changePassword(user, { current, next });
151
- await mail.passwordChanged({ name: changed.name, locale: changed.locale, email: changed.email });
152
- ```
153
-
154
- ### Telling a user their e-mail changed
149
+ `@nxgt/janus` 0.14 sends `user.passwordChanged` once `changePassword` or
150
+ `setPassword` wrote the password, and `user.emailChanged` once `update`
151
+ changed the e-mail. The e-mail change is told to the **former** address — the
152
+ new one belongs to whoever changed it — which the event carries as
153
+ `formerEmail`, `null` for a user who had none:
155
154
 
156
155
  ```ts
157
- const before = user.email;
158
- const updated = await auth.update(user, { email: next });
159
- await mail.emailChanged({ name: updated.name, locale: updated.locale, formerEmail: before, newEmail: updated.email });
156
+ const auth = janus({
157
+ ...config,
158
+ async events(event) {
159
+ if (event.type === 'user.passwordChanged') {
160
+ const user = await auth.get(event.userId);
161
+ await mail.passwordChanged({ name: user.name, locale: user.locale, email: user.email });
162
+ }
163
+ if (event.type === 'user.emailChanged' && event.formerEmail != null) {
164
+ const user = await auth.get(event.userId);
165
+ await mail.emailChanged({ name: user.name, locale: user.locale, formerEmail: event.formerEmail, newEmail: user.email });
166
+ }
167
+ },
168
+ });
160
169
  ```
161
170
 
171
+ A reset sends `user.passwordReset`, not `user.passwordChanged`: add that type
172
+ to the first test to tell a reset too. The examples assume a required e-mail;
173
+ with an optional one, an update that removes it has no `newEmail` to name —
174
+ see [Sending](docs/guide/sending.md#emailchangedto).
175
+
162
176
  ### Telling a user their second factor was turned on or off
163
177
 
164
178
  `@nxgt/janus` sends `user.secondFactorEnabled` once `secondFactor.activate`
@@ -380,8 +394,8 @@ The symptoms and fixes are in [troubleshooting](docs/troubleshooting.md).
380
394
 
381
395
  ## Type safety, counted
382
396
 
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
397
+ **Thirty-six plausible mistakes, thirty-six refused at compile time.**
398
+ Six files hold one `@ts-expect-error` per mistake, beside the calls that
385
399
  must keep compiling:
386
400
  [`test/types/send-refusals.ts`](test/types/send-refusals.ts) the eight of a
387
401
  send, 1 to 8;
@@ -391,9 +405,11 @@ template;
391
405
  [`test/types/expiry-refusals.ts`](test/types/expiry-refusals.ts) the five of
392
406
  the expiry and the clock, 21 to 25;
393
407
  [`test/types/notice-refusals.ts`](test/types/notice-refusals.ts) the four of
394
- the two-factor notices and the welcome, 26 to 29; and
408
+ the two-factor notices and the welcome, 26 to 29;
395
409
  [`test/types/recovery-refusals.ts`](test/types/recovery-refusals.ts) the
396
- five of the recovery code notice, 30 to 34:
410
+ five of the recovery code notice, 30 to 34; and
411
+ [`test/types/change-refusals.ts`](test/types/change-refusals.ts) the two of
412
+ the e-mail change notice sent on its event, 35 and 36:
397
413
 
398
414
  1. A sign-in code given to `verifyEmail`: it has no token.
399
415
  2. A one-time token given to `signInCode`: it has no code.
@@ -429,8 +445,10 @@ five of the recovery code notice, 30 to 34:
429
445
  32. The count as `recoveryCodesLeft()` answered it, not checked for `null` first.
430
446
  33. `when` given as the event's `Date` rather than the text in the recipient's locale and time zone.
431
447
  34. `links.recoveryCodes` given as a URL rather than a function.
448
+ 35. `user.emailChanged`'s `formerEmail` given to `emailChanged` unchecked: it is `null` for a user who had none.
449
+ 36. The `user.emailChanged` event itself given to `emailChanged`: it has the former address, but neither a name nor the new one.
432
450
 
433
- In JavaScript, 19 to 23, 26 to 28 and 30 to 33 are a `TypeError` at send
451
+ In JavaScript, 19 to 23, 26 to 28, 30 to 33, 35 and 36 are a `TypeError` at send
434
452
  time instead, naming the call or the field, and 25, 29 and 34 one from
435
453
  `janusMail()`.
436
454
 
package/docs/README.md CHANGED
@@ -5,7 +5,7 @@ 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, `clock` among them; each method, the welcome on `user.created` and the recovery code notice on `user.recoveryCodeUsed` 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 |
8
+ | [Sending](guide/sending.md) | You are wiring `janusMail()` into your routes: the options, `clock` among them; each method, the welcome on `user.created`, the recovery code notice on `user.recoveryCodeUsed` and the change notices on `user.passwordChanged` and `user.emailChanged` 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 |
@@ -117,26 +117,52 @@ name — and only its `locale` is read.
117
117
  ### `passwordChanged(to)`
118
118
 
119
119
  ```ts
120
- const changed = await auth.changePassword(user, { current, next });
121
- await mail.passwordChanged({ name: changed.name, locale: changed.locale, email: changed.email });
120
+ // On the user event @nxgt/janus 0.14 sends once the password is written:
121
+ const auth = janus({
122
+ ...config,
123
+ async events(event) {
124
+ if (event.type !== 'user.passwordChanged') return;
125
+ const user = await auth.get(event.userId);
126
+ await mail.passwordChanged({ name: user.name, locale: user.locale, email: user.email });
127
+ },
128
+ });
122
129
  ```
123
130
 
124
131
  A notice: the password of the account at `to.email` changed, with a link to
125
- `links.secureAccount()` for a user who did not change it. Send it after
126
- `changePassword`, `setPassword` and `resetPassword.confirm`.
132
+ `links.secureAccount()` for a user who did not change it. Send it on the
133
+ `user.passwordChanged` event, which `changePassword` and `setPassword` send.
134
+ A reset sends `user.passwordReset` instead, never both: to tell a reset too,
135
+ send the same notice on that type — `event.type === 'user.passwordChanged'
136
+ || event.type === 'user.passwordReset'`. The event names the user by id
137
+ alone, so read the name, the locale and the address from the user.
138
+ `setPassword` sends it for a user's first password too — an account created
139
+ without one — so the notice then tells them a password was set on their
140
+ account, which is worth knowing all the same.
127
141
 
128
142
  ### `emailChanged(to)`
129
143
 
130
144
  ```ts
131
- const formerEmail = user.email;
132
- const updated = await auth.update(user, { email: next });
133
- await mail.emailChanged({ name: updated.name, locale: updated.locale, formerEmail, newEmail: updated.email });
145
+ // On the user event @nxgt/janus 0.14 sends when an update changed the e-mail:
146
+ const auth = janus({
147
+ ...config,
148
+ async events(event) {
149
+ if (event.type !== 'user.emailChanged' || event.formerEmail == null) return;
150
+ const user = await auth.get(event.userId);
151
+ await mail.emailChanged({ name: user.name, locale: user.locale, formerEmail: event.formerEmail, newEmail: user.email });
152
+ },
153
+ });
134
154
  ```
135
155
 
136
156
  Sent to **`formerEmail`**: the owner of the old address is the one to warn,
137
157
  since whoever changed it already controls the new one. It names `newEmail`,
138
- and links to `links.secureAccount()`. Read the former address before the
139
- update: the user `update` answers already holds the new one.
158
+ and links to `links.secureAccount()`. The `user.emailChanged` event is the
159
+ one that carries an address — `event.formerEmail`, the e-mail before the
160
+ update — since the user read back already holds the new one. It is `null`
161
+ for a user who had no e-mail before: there is no former inbox to tell, and
162
+ the type refuses it unchecked. The example assumes a required e-mail: with
163
+ an optional one, `update` also sends the event when it removes the address,
164
+ and the user read back has no `newEmail` to name — check `user.email` first,
165
+ and tell the former address your own way or not at all.
140
166
 
141
167
  ### `twoFactorEnabled(to)` and `twoFactorDisabled(to)`
142
168
 
package/docs/roadmap.md CHANGED
@@ -47,6 +47,13 @@ Nothing between releases.
47
47
 
48
48
  Newest first; from the first release on, the package's CHANGELOG holds every one.
49
49
 
50
+ - **The change notices on their events, v0.6.0.** `passwordChanged` and
51
+ `emailChanged` are sent from `@nxgt/janus` 0.14's `user.passwordChanged`
52
+ and `user.emailChanged` events, as the two-factor notices are, rather than
53
+ after the call: whoever changed the password or the e-mail, the user is
54
+ told. `emailChanged` goes to the event's `formerEmail`, the inbox the
55
+ account just left, and the compiler refuses it unchecked for `null`.
56
+
50
57
  - **The recovery code notice, and a readable link in both modes, v0.5.0.**
51
58
  `recoveryCodeUsed(to, { when, recoveryCodesLeft })`, built from
52
59
  `@nxgt/mail-presets` 1.0.0's `recovery-code-used` in English and French:
@@ -255,14 +255,29 @@ number of seconds: pass the text to show, `{ expiresIn: '1 hour' }`, or
255
255
  leave it out.
256
256
 
257
257
  `janusMail.twoFactorEnabled: email must be a string` (or `twoFactorDisabled`,
258
- `recoveryCodeUsed` or `welcome`) is the user event itself given to the
259
- e-mail: an event names the user by id alone. Read the user first:
258
+ `passwordChanged`, `recoveryCodeUsed` or `welcome`) is the user event itself
259
+ given to the e-mail: an event names the user by id alone. Read the user first:
260
260
 
261
261
  ```ts
262
262
  const user = await auth.get(event.userId);
263
263
  await mail.twoFactorDisabled({ name: user.name, locale: user.locale, email: user.email });
264
264
  ```
265
265
 
266
+ On `user.emailChanged`, `janusMail.emailChanged: name must be a string` is
267
+ the event itself given to `emailChanged`, and `janusMail.emailChanged:
268
+ formerEmail must be a string` is `event.formerEmail` passed without its
269
+ check — it is `null` for a user who had no e-mail before. `janusMail.emailChanged:
270
+ newEmail must be a string` is an update that removed the e-mail: there is no
271
+ new address to name. Check both, and read the rest from the user:
272
+
273
+ ```ts
274
+ if (event.type === 'user.emailChanged' && event.formerEmail != null) {
275
+ const user = await auth.get(event.userId);
276
+ if (user.email === undefined) return; // removed: tell the former address your own way
277
+ await mail.emailChanged({ name: user.name, locale: user.locale, formerEmail: event.formerEmail, newEmail: user.email });
278
+ }
279
+ ```
280
+
266
281
  `janusMail.recoveryCodeUsed: when must be a string` is the second argument
267
282
  missing, or its `when` given as the event's `Date`: pass the text to show,
268
283
  formatted in the recipient's locale and time zone —
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nxgt/janus-mail",
3
- "version": "0.5.1",
3
+ "version": "0.6.0",
4
4
  "description": "The e-mails of @nxgt/janus's flows — e-mail verification, password reset, sign-in code, password changed, e-mail changed, two-factor enabled and disabled, recovery code used, welcome — rendered in English and French from a prebuilt Maizzle build, with your brand, sent through any @nxgt/mail transport",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -54,7 +54,7 @@
54
54
  "@maizzle/framework": "6.1.7",
55
55
  "@maizzle/tailwindcss": "1.5.6",
56
56
  "@formatjs/icu-messageformat-parser": "3.5.20",
57
- "@nxgt/janus": "^0.12.0",
57
+ "@nxgt/janus": "^0.14.0",
58
58
  "@nxgt/mail": "1.0.1",
59
59
  "@nxgt/mail-config": "1.0.1",
60
60
  "@nxgt/mail-i18n": "1.0.1",
@@ -66,7 +66,7 @@
66
66
  "zod": "^4.6.5"
67
67
  },
68
68
  "peerDependencies": {
69
- "@nxgt/janus": "^0.12.0",
69
+ "@nxgt/janus": "^0.14.0",
70
70
  "@nxgt/mail": ">=0.1.0 <2",
71
71
  "typescript": "^6.0.3"
72
72
  }