@nxgt/janus-mail 0.7.1 → 0.9.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
@@ -2,8 +2,8 @@
2
2
 
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
- sign-in link, password changed, e-mail changed, two-factor authentication turned on or
6
- off, a recovery code used, and a welcome to a new user**, in English and
5
+ sign-in link, a step-up's confirmation code, password changed, e-mail changed, two-factor authentication turned on or
6
+ off, a recovery code used, a sign-in from a new device, and a welcome to a new user**, in English and
7
7
  French, with your brand
8
8
  in them. `@nxgt/janus` sends no e-mail — its flows answer what to send — and
9
9
  this package turns that answer into an e-mail and hands it to any
@@ -48,7 +48,7 @@ bun add -d typescript
48
48
 
49
49
  Three peers, all required: `@nxgt/mail` (0.1 or later, below 2 — 1.0 and
50
50
  its transports included), which defines the `Mailer` port and the errors;
51
- `@nxgt/janus` (0.15), whose flows' answers the methods take — types only,
51
+ `@nxgt/janus` (0.17), whose flows' answers the methods take — types only,
52
52
  nothing of it is loaded; and `typescript` (6). **No Maizzle, no Vue, no
53
53
  Tailwind**: they run at this package's build, not in yours.
54
54
 
@@ -65,12 +65,12 @@ needs Node `^22.22.3`, `^24.15.0` or `>=26`, for Maizzle — see
65
65
 
66
66
  | Export | What it is |
67
67
  | --- | --- |
68
- | `janusMail(options)` | The e-mails, over your mailer: `verifyEmail`, `resetPassword`, `signInCode`, `magicLink`, `passwordChanged`, `emailChanged`, `twoFactorEnabled`, `twoFactorDisabled`, `recoveryCodeUsed`, `welcome`, and `templates` and `locales`. Options: `mailer`, `from`, `replyTo?`, `brand`, `links` (`verifyEmail`, `resetPassword`, `secureAccount`, `getStarted`, and `recoveryCodes?` and `magicLink?`), `locales?`, `fallbackLocale?`, `templates?`, `clock?`. A wrong option is a bare `TypeError`, thrown here |
69
- | `janusTemplates()` | The ten default templates alone, each `(variables & { locale }) => Rendered` |
68
+ | `janusMail(options)` | The e-mails, over your mailer: `verifyEmail`, `resetPassword`, `signInCode`, `magicLink`, `stepUp`, `passwordChanged`, `emailChanged`, `twoFactorEnabled`, `twoFactorDisabled`, `recoveryCodeUsed`, `newSignIn`, `welcome`, and `templates` and `locales`. Options: `mailer`, `from`, `replyTo?`, `brand`, `links` (`verifyEmail`, `resetPassword`, `secureAccount`, `getStarted`, and `recoveryCodes?` and `magicLink?`), `locales?`, `fallbackLocale?`, `templates?`, `clock?`. A wrong option is a bare `TypeError`, thrown here |
69
+ | `janusTemplates()` | The twelve default templates alone, each `(variables & { locale }) => Rendered` |
70
70
  | `JanusMail<L>`, `JanusMailOptions<L>` | What `janusMail()` answers and takes, for the locales `L` |
71
- | `JanusMailTemplate<V, L>`, `JanusMailTemplates<L>`, `JanusMailTemplateName` | One template, the ten of them, and their names |
72
- | `JanusMailVariables` | What each template is given: `brand`, and `name`, `link`, `code`, `expiresIn`, `newEmail`, `when` or `recoveryCodesLeft` as its e-mail needs |
73
- | `JanusMailSendOptions` | The third argument of `verifyEmail`, `resetPassword`, `signInCode` and `magicLink`: `{ expiresIn? }`, the expiry as text, over the one derived |
71
+ | `JanusMailTemplate<V, L>`, `JanusMailTemplates<L>`, `JanusMailTemplateName` | One template, the twelve of them, and their names |
72
+ | `JanusMailVariables` | What each template is given: `brand`, and `name`, `link`, `code`, `expiresIn`, `newEmail`, `when`, `recoveryCodesLeft`, or `device`, `time` and `location`, as its e-mail needs |
73
+ | `JanusMailSendOptions` | The third argument of `verifyEmail`, `resetPassword`, `signInCode`, `magicLink` and `stepUp`: `{ expiresIn? }`, the expiry as text, over the one derived |
74
74
  | `JanusMailLocale` | `'en' \| 'fr'`: the locales the defaults are built in |
75
75
  | `JanusMailLinks`, `Recipient` | The `links` option; who an e-mail is for — `{ name, locale? }` |
76
76
 
@@ -88,14 +88,16 @@ class. To retry a `MailFailure` or trace each send, wrap the mailer you pass:
88
88
  | `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` |
89
89
  | `signInCode(issued, to?, options?)` | `issued.code` from `auth.signInCode.request(email)`, and how long it lasts — **never the challenge** | `issued.email` |
90
90
  | `magicLink(issued, to?, options?)` | `issued` from `auth.magicLink.request(email)`, checked for `null`; a link from `links.magicLink(issued.token)`, and how long it lasts. A `TypeError` without `links.magicLink` | `issued.email` |
91
+ | `stepUp(issued, to, options?)` | `issued.code` from `auth.stepUp.request(user)`, narrowed to `via: 'email'`, and how long it lasts, greeting `to.name`, with `links.secureAccount()` — **never the challenge**. A `TypeError` for `via: 'secondFactor'` | `issued.email` |
91
92
  | `passwordChanged(to)` | A notice, with `links.secureAccount()` | `to.email` |
92
93
  | `emailChanged(to)` | A notice naming `to.newEmail`, with `links.secureAccount()` | `to.formerEmail` |
93
94
  | `twoFactorEnabled(to)` | A notice: two-factor authentication was turned on, with `links.secureAccount()` | `to.email` |
94
95
  | `twoFactorDisabled(to)` | A notice: two-factor authentication was turned off, with `links.secureAccount()` | `to.email` |
95
96
  | `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` |
97
+ | `newSignIn(to, { device, time, location? })` | A notice: the account was signed in from a new device — which device, `time`, and `location`, `—` when left out — with `links.secureAccount()` | `to.email` |
96
98
  | `welcome(to)` | A welcome to a new user — "Welcome, Ada" — with `links.getStarted()` | `to.email` |
97
99
 
98
- The four e-mails of a link or a code say how long it lasts — "This link
100
+ The five e-mails of a link or a code say how long it lasts — "This link
99
101
  expires in 1 hour.", "Ce lien expire dans 1 heure." — from the flow's
100
102
  `issued.expiresAt`: the time left at send time, rounded to the minute, then
101
103
  down to the largest whole unit (days, hours or minutes), formatted with
@@ -156,6 +158,23 @@ scanners open every link in an e-mail, and one that signs in on `GET` signs
156
158
  in for the scanner. [`@nxgt/janus`'s sign-in link guide](https://github.com/softistx/nxgt-janus/blob/develop/packages/janus/docs/guide/magic-link.md)
157
159
  has that page.
158
160
 
161
+ ### Confirming a sensitive action — the step-up
162
+
163
+ ```ts
164
+ const issued = await auth.stepUp.request(current.user); // the user of the session
165
+ if (issued.via === 'email') {
166
+ await mail.stepUp(issued, { name: current.user.name, locale: current.user.locale }); // "Your confirmation code"
167
+ }
168
+ return Response.json({ challenge: issued.challenge, via: issued.via }); // the challenge stays with the user
169
+ ```
170
+
171
+ A user whose second factor is active confirms with their app
172
+ (`via: 'secondFactor'`): there is nothing to send. The e-mail names no
173
+ action, and its **Secure my account** button links to
174
+ `links.secureAccount()`, for a user who asked for nothing.
175
+ [`@nxgt/janus`'s step-up guide](https://github.com/softistx/nxgt-janus/blob/develop/packages/janus/docs/guide/step-up.md)
176
+ has the confirming route.
177
+
159
178
  ### Saying the expiry yourself
160
179
 
161
180
  ```ts
@@ -273,6 +292,45 @@ récupération." — or the sentence itself, as text. The button links to
273
292
  `links.recoveryCodes()`, where the user regenerates their codes, and to
274
293
  `links.secureAccount()` when you give no `recoveryCodes`.
275
294
 
295
+ ### Telling a user a new device signed in
296
+
297
+ `@nxgt/janus` 0.17, given `janus({ devices })` and a sign-in's `{ device }`,
298
+ answers `newDevice: true` for a device the user had not signed in from, and
299
+ sends `user.newDeviceSignedIn`. Send the notice from the sign-in's answer,
300
+ where the request is in hand to name the device:
301
+
302
+ ```ts
303
+ const signedIn = await auth.signIn({ email, password }, { device: cookie ?? null });
304
+ if (signedIn.newDevice) {
305
+ const { name, email, locale } = signedIn.user;
306
+ const time = new Intl.DateTimeFormat(locale, { dateStyle: 'long', timeStyle: 'short', timeZone: 'Europe/Paris' })
307
+ .format(signedIn.session.createdAt);
308
+ await mail.newSignIn({ name, email, locale }, { device: 'Firefox on macOS', time }); // "New sign-in to your account"
309
+ }
310
+ ```
311
+
312
+ or from the event, which names the user by id alone:
313
+
314
+ ```ts
315
+ const auth = janus({
316
+ ...config,
317
+ async events(event) {
318
+ if (event.type === 'user.newDeviceSignedIn') {
319
+ const user = await auth.get(event.userId);
320
+ const time = new Intl.DateTimeFormat(user.locale, { dateStyle: 'long', timeStyle: 'short', timeZone: 'Europe/Paris' })
321
+ .format(event.occurredAt);
322
+ await mail.newSignIn({ name: user.name, email: user.email, locale: user.locale }, { device: 'a new device', time });
323
+ }
324
+ },
325
+ });
326
+ ```
327
+
328
+ Every value is your text, in the recipient's locale: `device` described
329
+ from the request's `User-Agent`, `time` formatted in their time zone.
330
+ **`@nxgt/janus` sees no IP**, so `location` is yours to work out — from a
331
+ geo-IP of your own — and an e-mail without one shows `—` in its place. The
332
+ button links to `links.secureAccount()`.
333
+
276
334
  ### Replacing a template
277
335
 
278
336
  Any template can be your own function — React Email, a string, another
@@ -361,14 +419,27 @@ it.** An application that sends no sign-in link leaves it out; calling
361
419
  rendered or sent. Unlike `recoveryCodes`, it has no fallback: no other page
362
420
  can carry the token.
363
421
 
422
+ **Leave `location` out when you do not know it.** A geo-IP lookup that found
423
+ nothing answers `null`, which is a compile error here and, in JavaScript,
424
+ `janusMail.newSignIn: location must be a string`: spread it in only when it
425
+ is a string — `...(city === null ? {} : { location: city })` — and the
426
+ e-mail shows `—`.
427
+
428
+ **Check `via` before `stepUp`.** `auth.stepUp.request(user)` answers
429
+ `via: 'secondFactor'` — no code, no address — for a user whose second
430
+ factor is active. Passed un-narrowed it is a compile error; in JavaScript,
431
+ `janusMail.stepUp: via must be 'email' — …`, a `TypeError` before anything
432
+ is sent. Unlike `signInCode`, `stepUp` takes the recipient: it greets the
433
+ user by name.
434
+
364
435
  **Templates are own properties.** A class's methods live on its prototype
365
436
  and are refused with a `TypeError`; its fields (`signInCode = (variables) =>
366
437
  …`) pass.
367
438
 
368
439
  **A locale beyond `en` and `fr` needs every template.** The defaults are
369
- built in those two only, so `locales: ['en', 'fr', 'de']` without all ten
370
- templates — `recoveryCodeUsed` included, since 0.5, and `magicLink`, since
371
- 0.7 — is a compile error, and a `TypeError` in JavaScript.
440
+ built in those two only, so `locales: ['en', 'fr', 'de']` without all twelve
441
+ templates — `recoveryCodeUsed` included, since 0.5, `magicLink`, since
442
+ 0.7, `stepUp`, since 0.8, and `newSignIn`, since 0.9 — is a compile error, and a `TypeError` in JavaScript.
372
443
 
373
444
  **The brand is text only.** `brand: 'Acme'` is written in the header, the
374
445
  body and the footer, escaped: no logo, no link, no markup. For those, replace
@@ -425,8 +496,8 @@ The symptoms and fixes are in [troubleshooting](docs/troubleshooting.md).
425
496
 
426
497
  ## Type safety, counted
427
498
 
428
- **Forty plausible mistakes, forty refused at compile time.**
429
- Seven files hold one `@ts-expect-error` per mistake, beside the calls that
499
+ **Fifty plausible mistakes, fifty refused at compile time.**
500
+ Nine files hold one `@ts-expect-error` per mistake, beside the calls that
430
501
  must keep compiling:
431
502
  [`test/types/send-refusals.ts`](test/types/send-refusals.ts) the eight of a
432
503
  send, 1 to 8;
@@ -440,9 +511,14 @@ the two-factor notices and the welcome, 26 to 29;
440
511
  [`test/types/recovery-refusals.ts`](test/types/recovery-refusals.ts) the
441
512
  five of the recovery code notice, 30 to 34;
442
513
  [`test/types/change-refusals.ts`](test/types/change-refusals.ts) the two of
443
- the e-mail change notice sent on its event, 35 and 36; and
514
+ the e-mail change notice sent on its event, 35 and 36;
444
515
  [`test/types/magic-link-refusals.ts`](test/types/magic-link-refusals.ts) the
445
- four of the sign-in link, 37 to 40:
516
+ four of the sign-in link, 37 to 40;
517
+ [`test/types/step-up-refusals.ts`](test/types/step-up-refusals.ts) the six
518
+ of the step-up's code, 41 to 46, beside the names of the templates, held to
519
+ the twelve; and
520
+ [`test/types/new-sign-in-refusals.ts`](test/types/new-sign-in-refusals.ts)
521
+ the four of the new sign-in notice, 47 to 50:
446
522
 
447
523
  1. A sign-in code given to `verifyEmail`: it has no token.
448
524
  2. A one-time token given to `signInCode`: it has no code.
@@ -484,8 +560,18 @@ four of the sign-in link, 37 to 40:
484
560
  38. `magicLink.request`'s answer, not checked for `null` first.
485
561
  39. The token alone given to `magicLink`, rather than what the flow answered.
486
562
  40. `links.magicLink` computed asynchronously.
487
-
488
- In JavaScript, 19 to 23, 26 to 28, 30 to 33 and 35 to 40 are a `TypeError` at send
563
+ 41. `stepUp.request`'s answer not narrowed to `via: 'email'`: an app's step-up has nothing to send.
564
+ 42. A step-up confirmed with the user's app, narrowed to it, given to `stepUp`.
565
+ 43. A sign-in code given to `stepUp`: it signs in, it confirms no action.
566
+ 44. A one-time token given to `stepUp`: it has no code.
567
+ 45. `stepUp` without the recipient: its e-mail greets the user by name.
568
+ 46. The step-up's `user` given as the recipient when the user type has no name.
569
+ 47. The `user.newDeviceSignedIn` event itself given to `newSignIn`: it has neither a name nor an address.
570
+ 48. `newSignIn` without the device and the time.
571
+ 49. `time` given as the session's `Date` rather than the text in the recipient's locale and time zone.
572
+ 50. A location lookup's `null` passed as `location`, rather than leaving it out.
573
+
574
+ In JavaScript, 19 to 23, 26 to 28, 30 to 33 and 35 to 50 are a `TypeError` at send
489
575
  time instead, naming the call or the field, and 25, 29 and 34 one from
490
576
  `janusMail()`.
491
577
 
@@ -1,5 +1,12 @@
1
1
  /** The e-mails of the build, each with the variables it takes when it is sent. */
2
2
  export interface MailEmails {
3
+ "confirm-action": {
4
+ readonly brand: string | number;
5
+ readonly code: string | number;
6
+ readonly expiresIn: string | number;
7
+ readonly link: string;
8
+ readonly name: string | number;
9
+ };
3
10
  "email-changed": {
4
11
  readonly brand: string | number;
5
12
  readonly link: string;
@@ -11,6 +18,14 @@ export interface MailEmails {
11
18
  readonly expiresIn: string | number;
12
19
  readonly link: string;
13
20
  };
21
+ "new-sign-in": {
22
+ readonly brand: string | number;
23
+ readonly device: string | number;
24
+ readonly link: string;
25
+ readonly location: string | number;
26
+ readonly name: string | number;
27
+ readonly time: string | number;
28
+ };
14
29
  "password-changed": {
15
30
  readonly brand: string | number;
16
31
  readonly link: string;
@@ -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,YAAY,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,CAAA;KAAE,CAAC;IAC9G,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"}
1
+ {"version":3,"file":"mail.d.ts","sourceRoot":"","sources":["../../src/generated/mail.ts"],"names":[],"mappings":"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"}
package/dist/index.d.ts CHANGED
@@ -2,8 +2,9 @@
2
2
  * `@nxgt/janus-mail` — the e-mails of `@nxgt/janus`'s flows.
3
3
  *
4
4
  * `@nxgt/janus` sends no e-mail: its flows answer what to send — a one-time
5
- * token, a sign-in code, a sign-in link's token, the address — and this package turns that answer
6
- * into an e-mail and hands it to an `@nxgt/mail` transport. Ten e-mails, in
5
+ * token, a sign-in code, a sign-in link's token, a step-up's code, the
6
+ * address — and this package turns that answer into an e-mail and hands it
7
+ * to an `@nxgt/mail` transport. Twelve e-mails, in
7
8
  * English and French, prebuilt with Maizzle when this package is built, and
8
9
  * filled at send time with your brand, the recipient's name and your links.
9
10
  * Any of them can be replaced by a function of your own.
@@ -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,oBAAoB,EACpB,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;;;;;;;;;;;;;;;;GAgBG;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
@@ -117,6 +117,32 @@ function expiringMails(options) {
117
117
  };
118
118
  }
119
119
 
120
+ // src/new-sign-in-mail.ts
121
+ var METHOD = "newSignIn";
122
+ var NO_LOCATION = "—";
123
+ function locationOf(signIn) {
124
+ const given = typeof signIn === "object" && signIn !== null ? signIn.location : undefined;
125
+ if (given === undefined)
126
+ return NO_LOCATION;
127
+ return field(METHOD, signIn, "location");
128
+ }
129
+ function newSignInMail(options) {
130
+ const { brand, links } = options;
131
+ return {
132
+ async newSignIn(to, signIn) {
133
+ return send(options, METHOD, field(METHOD, to, "email"), {
134
+ brand,
135
+ name: field(METHOD, to, "name"),
136
+ device: field(METHOD, signIn, "device"),
137
+ location: locationOf(signIn),
138
+ time: field(METHOD, signIn, "time"),
139
+ link: link(METHOD, links.secureAccount(), "secureAccount()"),
140
+ locale: localeFor(options, to)
141
+ });
142
+ }
143
+ };
144
+ }
145
+
120
146
  // src/notice-mails.ts
121
147
  function noticeMails(options) {
122
148
  const { brand, links } = options;
@@ -198,24 +224,36 @@ function defaultRenderer() {
198
224
  }
199
225
 
200
226
  // src/templates.ts
227
+ var recoveryCodeUsed = ({
228
+ brand,
229
+ name,
230
+ when,
231
+ recoveryCodesLeft,
232
+ link,
233
+ locale
234
+ }) => defaultRenderer().render("recovery-code-used", { brand, name, when, recoveryCodesLeft, link }, { locale });
235
+ var newSignIn = ({
236
+ brand,
237
+ name,
238
+ device,
239
+ location,
240
+ time,
241
+ link,
242
+ locale
243
+ }) => defaultRenderer().render("new-sign-in", { brand, name, device, location, time, link }, { locale });
201
244
  function janusTemplates() {
202
245
  return Object.freeze({
203
246
  verifyEmail: ({ brand, name, link, expiresIn, locale }) => defaultRenderer().render("verify-email", { brand, name, link, expiresIn }, { locale }),
204
247
  resetPassword: ({ brand, name, link, expiresIn, locale }) => defaultRenderer().render("reset-password", { brand, name, link, expiresIn }, { locale }),
205
248
  signInCode: ({ brand, code, expiresIn, locale }) => defaultRenderer().render("sign-in-code", { brand, code, expiresIn }, { locale }),
206
249
  magicLink: ({ brand, link, expiresIn, locale }) => defaultRenderer().render("magic-link", { brand, link, expiresIn }, { locale }),
250
+ stepUp: ({ brand, name, code, expiresIn, link, locale }) => defaultRenderer().render("confirm-action", { brand, name, code, expiresIn, link }, { locale }),
207
251
  passwordChanged: ({ brand, name, link, locale }) => defaultRenderer().render("password-changed", { brand, name, link }, { locale }),
208
252
  emailChanged: ({ brand, name, link, newEmail, locale }) => defaultRenderer().render("email-changed", { brand, name, link, newEmail }, { locale }),
209
253
  twoFactorEnabled: ({ brand, name, link, locale }) => defaultRenderer().render("two-factor-enabled", { brand, name, link }, { locale }),
210
254
  twoFactorDisabled: ({ brand, name, link, locale }) => defaultRenderer().render("two-factor-disabled", { brand, name, link }, { locale }),
211
- recoveryCodeUsed: ({
212
- brand,
213
- name,
214
- when,
215
- recoveryCodesLeft,
216
- link,
217
- locale
218
- }) => defaultRenderer().render("recovery-code-used", { brand, name, when, recoveryCodesLeft, link }, { locale }),
255
+ recoveryCodeUsed,
256
+ newSignIn,
219
257
  welcome: ({ brand, name, link, locale }) => defaultRenderer().render("welcome", { brand, name, link }, { locale })
220
258
  });
221
259
  }
@@ -226,11 +264,13 @@ var TEMPLATE_NAMES = [
226
264
  "resetPassword",
227
265
  "signInCode",
228
266
  "magicLink",
267
+ "stepUp",
229
268
  "passwordChanged",
230
269
  "emailChanged",
231
270
  "twoFactorEnabled",
232
271
  "twoFactorDisabled",
233
272
  "recoveryCodeUsed",
273
+ "newSignIn",
234
274
  "welcome"
235
275
  ];
236
276
  var SYSTEM_CLOCK = Object.freeze({ now: () => new Date });
@@ -386,18 +426,18 @@ function codesLeftText(locale, count) {
386
426
  }
387
427
 
388
428
  // src/recovery-mail.ts
389
- var METHOD = "recoveryCodeUsed";
429
+ var METHOD2 = "recoveryCodeUsed";
390
430
  var isCount = (value) => typeof value === "number" && Number.isSafeInteger(value) && value >= 0;
391
431
  function codesLeft(used, locale) {
392
432
  const given = typeof used === "object" && used !== null ? used.recoveryCodesLeft : undefined;
393
433
  if (typeof given === "string")
394
434
  return given;
395
435
  if (!isCount(given)) {
396
- throw new TypeError(`janusMail.${METHOD}: recoveryCodesLeft must be a count — a whole number, 0 or more — or the sentence to show`);
436
+ throw new TypeError(`janusMail.${METHOD2}: recoveryCodesLeft must be a count — a whole number, 0 or more — or the sentence to show`);
397
437
  }
398
438
  const text = codesLeftText(locale, given);
399
439
  if (text === undefined) {
400
- throw new TypeError(`janusMail.${METHOD}: recoveryCodesLeft must be the sentence to show in a locale the default e-mails are not built in`);
440
+ throw new TypeError(`janusMail.${METHOD2}: recoveryCodesLeft must be the sentence to show in a locale the default e-mails are not built in`);
401
441
  }
402
442
  return text;
403
443
  }
@@ -406,12 +446,38 @@ function recoveryMail(options) {
406
446
  return {
407
447
  async recoveryCodeUsed(to, used) {
408
448
  const locale = localeFor(options, to);
409
- return send(options, METHOD, field(METHOD, to, "email"), {
449
+ return send(options, METHOD2, field(METHOD2, to, "email"), {
410
450
  brand,
411
- name: field(METHOD, to, "name"),
412
- when: field(METHOD, used, "when"),
451
+ name: field(METHOD2, to, "name"),
452
+ when: field(METHOD2, used, "when"),
413
453
  recoveryCodesLeft: codesLeft(used, locale),
414
- link: links.recoveryCodes === undefined ? link(METHOD, links.secureAccount(), "secureAccount()") : link(METHOD, links.recoveryCodes(), "recoveryCodes()"),
454
+ link: links.recoveryCodes === undefined ? link(METHOD2, links.secureAccount(), "secureAccount()") : link(METHOD2, links.recoveryCodes(), "recoveryCodes()"),
455
+ locale
456
+ });
457
+ }
458
+ };
459
+ }
460
+
461
+ // src/step-up-mail.ts
462
+ var METHOD3 = "stepUp";
463
+ function checkVia(issued) {
464
+ const via = typeof issued === "object" && issued !== null ? issued.via : undefined;
465
+ if (via !== "email") {
466
+ throw new TypeError(`janusMail.${METHOD3}: via must be 'email' — a step-up confirmed with the user's app sends no e-mail`);
467
+ }
468
+ }
469
+ function stepUpMail(options) {
470
+ const { brand, links } = options;
471
+ return {
472
+ async stepUp(issued, to, sendOptions) {
473
+ checkVia(issued);
474
+ const locale = localeFor(options, to);
475
+ return send(options, METHOD3, field(METHOD3, issued, "email"), {
476
+ brand,
477
+ name: field(METHOD3, to, "name"),
478
+ code: field(METHOD3, issued, "code"),
479
+ expiresIn: expiresInFor(METHOD3, issued, locale, sendOptions, options.clock),
480
+ link: link(METHOD3, links.secureAccount(), "secureAccount()"),
415
481
  locale
416
482
  });
417
483
  }
@@ -425,6 +491,8 @@ function janusMail(options) {
425
491
  ...expiringMails(resolved),
426
492
  ...noticeMails(resolved),
427
493
  ...recoveryMail(resolved),
494
+ ...newSignInMail(resolved),
495
+ ...stepUpMail(resolved),
428
496
  templates: resolved.templates,
429
497
  locales: resolved.locales
430
498
  });
@@ -435,5 +503,5 @@ export {
435
503
  janusTemplates
436
504
  };
437
505
 
438
- //# debugId=45A006F2B09A6D7D64756E2164756E21
506
+ //# debugId=F5414A38AA5F17CC64756E2164756E21
439
507
  //# sourceMappingURL=index.js.map