@nxgt/janus 0.17.2 → 0.19.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 +107 -27
- package/dist/auth/config/index.d.ts +1 -1
- package/dist/auth/config/index.d.ts.map +1 -1
- package/dist/auth/config/janus-config.d.ts +31 -0
- package/dist/auth/config/janus-config.d.ts.map +1 -1
- package/dist/auth/config/resolve-config.d.ts.map +1 -1
- package/dist/auth/config/resolve-throttle.d.ts +30 -0
- package/dist/auth/config/resolve-throttle.d.ts.map +1 -0
- package/dist/auth/config/resolved-config.d.ts +5 -0
- package/dist/auth/config/resolved-config.d.ts.map +1 -1
- package/dist/auth/config/warn-throttle-window.d.ts +14 -0
- package/dist/auth/config/warn-throttle-window.d.ts.map +1 -0
- package/dist/auth/counter-key.d.ts +19 -0
- package/dist/auth/counter-key.d.ts.map +1 -0
- package/dist/auth/email-flows/reset-password.d.ts.map +1 -1
- package/dist/auth/email-flows/verify-email.d.ts.map +1 -1
- package/dist/auth/index.d.ts +1 -1
- package/dist/auth/index.d.ts.map +1 -1
- package/dist/auth/janus.d.ts.map +1 -1
- package/dist/auth/magic-link.d.ts.map +1 -1
- package/dist/auth/mail-requests.d.ts +51 -0
- package/dist/auth/mail-requests.d.ts.map +1 -0
- package/dist/auth/port/assert-stores.d.ts.map +1 -1
- package/dist/auth/port/memory/users.d.ts.map +1 -1
- package/dist/auth/port/types/stores.d.ts +5 -0
- package/dist/auth/port/types/stores.d.ts.map +1 -1
- package/dist/auth/port/types/users.d.ts +17 -0
- package/dist/auth/port/types/users.d.ts.map +1 -1
- package/dist/auth/sign-in-attempts.d.ts +3 -6
- package/dist/auth/sign-in-attempts.d.ts.map +1 -1
- package/dist/auth/sign-in-code.d.ts +3 -2
- package/dist/auth/sign-in-code.d.ts.map +1 -1
- package/dist/auth/step-up/request.d.ts.map +1 -1
- package/dist/auth/types/email-flows.d.ts +16 -6
- package/dist/auth/types/email-flows.d.ts.map +1 -1
- package/dist/auth/types/magic-link.d.ts +4 -2
- package/dist/auth/types/magic-link.d.ts.map +1 -1
- package/dist/auth/types/step-up.d.ts +4 -1
- package/dist/auth/types/step-up.d.ts.map +1 -1
- package/dist/auth/types/user-type.d.ts +15 -0
- package/dist/auth/types/user-type.d.ts.map +1 -1
- package/dist/auth/users/find-many.d.ts +29 -0
- package/dist/auth/users/find-many.d.ts.map +1 -0
- package/dist/auth/users/record-flows.d.ts.map +1 -1
- package/dist/chunks/{index-6fxq0kzf.js → index-4khsm7mw.js} +2 -2
- package/dist/chunks/{index-7xe5x3eg.js → index-6bjfv4xv.js} +9 -3
- package/dist/chunks/{index-7xe5x3eg.js.map → index-6bjfv4xv.js.map} +3 -3
- package/dist/chunks/{index-k136102s.js → index-cntabs9d.js} +2 -2
- package/dist/chunks/{index-ztng6018.js → index-kppkcbn5.js} +10 -3
- package/dist/chunks/{index-ztng6018.js.map → index-kppkcbn5.js.map} +4 -4
- package/dist/conformance/cases/outage.d.ts.map +1 -1
- package/dist/conformance/cases/users/batch.d.ts +8 -0
- package/dist/conformance/cases/users/batch.d.ts.map +1 -0
- package/dist/conformance/cases/users/index.d.ts.map +1 -1
- package/dist/conformance/describe.d.ts +2 -1
- package/dist/conformance/describe.d.ts.map +1 -1
- package/dist/conformance/index.d.ts +1 -1
- package/dist/conformance/index.d.ts.map +1 -1
- package/dist/conformance/index.js +106 -46
- package/dist/conformance/index.js.map +7 -6
- package/dist/conformance/types.d.ts +10 -2
- package/dist/conformance/types.d.ts.map +1 -1
- package/dist/errors/janus-error/codes.d.ts +10 -0
- package/dist/errors/janus-error/codes.d.ts.map +1 -1
- package/dist/errors/janus-error/index.d.ts +1 -1
- package/dist/errors/janus-error/index.d.ts.map +1 -1
- package/dist/errors/janus-error/options.d.ts +4 -3
- package/dist/errors/janus-error/options.d.ts.map +1 -1
- package/dist/errors/janus-error/refusals.d.ts +8 -0
- package/dist/errors/janus-error/refusals.d.ts.map +1 -1
- package/dist/errors/janus-error/status.d.ts +1 -1
- package/dist/errors/janus-error/status.d.ts.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +171 -30
- package/dist/index.js.map +18 -14
- package/dist/permissions/index.js +3 -3
- package/docs/README.md +1 -1
- package/docs/guide/adapters.md +44 -4
- package/docs/guide/email-flows.md +164 -4
- package/docs/guide/errors.md +12 -3
- package/docs/guide/magic-link.md +41 -5
- package/docs/guide/passwords.md +2 -1
- package/docs/guide/sign-in-code.md +46 -7
- package/docs/guide/step-up.md +38 -7
- package/docs/guide/users.md +49 -0
- package/docs/roadmap.md +26 -15
- package/docs/troubleshooting.md +127 -5
- package/package.json +1 -1
- package/dist/auth/config/resolve-sign-in.d.ts +0 -5
- package/dist/auth/config/resolve-sign-in.d.ts.map +0 -1
- /package/dist/chunks/{index-6fxq0kzf.js.map → index-4khsm7mw.js.map} +0 -0
- /package/dist/chunks/{index-k136102s.js.map → index-cntabs9d.js.map} +0 -0
package/docs/roadmap.md
CHANGED
|
@@ -16,9 +16,15 @@ Nothing yet.
|
|
|
16
16
|
- **Collecting lapsed one-time tokens** — `collectExpired()` deletes lapsed
|
|
17
17
|
sessions only. On a store with no TTL (PostgreSQL), lapsed tokens stay
|
|
18
18
|
until you delete them, and the sign-in throttle adds one per login tried
|
|
19
|
-
per window
|
|
19
|
+
per window, and the mail throttle one per flow per address or user per
|
|
20
|
+
window. A port method to collect them, as sessions are, would let the
|
|
20
21
|
core do it.
|
|
21
22
|
|
|
23
|
+
- **A batch read across types** — `findMany` reads one user type, as `find`
|
|
24
|
+
does. The shared `findUser(id)` has no batch counterpart yet; one on the
|
|
25
|
+
instance, answering a union narrowed by `user.type`, would serve a list that
|
|
26
|
+
mixes types.
|
|
27
|
+
|
|
22
28
|
- **More official adapters** — the ports are cut where atomicity is not
|
|
23
29
|
required, so users, sessions and permission tuples can each live in the
|
|
24
30
|
database that suits them. MongoDB is the first adapter
|
|
@@ -70,6 +76,25 @@ Nothing yet.
|
|
|
70
76
|
The last ten, newest first, each with the version it came in. Everything
|
|
71
77
|
before is in the [CHANGELOG](../CHANGELOG.md).
|
|
72
78
|
|
|
79
|
+
- **Several users by id in one query, v0.18.0** — `auth.<type>.findMany(ids)`
|
|
80
|
+
answers the type's users in the order of `ids`, each once, and leaves out an
|
|
81
|
+
id nobody of that type holds — for a list of rows that would otherwise make
|
|
82
|
+
one `find` per row and fill the connection pool. The users store gains an
|
|
83
|
+
optional `findUsers`, implemented by the reference store,
|
|
84
|
+
`@nxgt/janus-drizzle` 0.7.0 and `@nxgt/janus-mongo` 0.8.0; a store without it
|
|
85
|
+
is read with `findUser`, ten at a time, so third-party adapters keep working.
|
|
86
|
+
- **Requests that send e-mail are throttled, v0.18.0** —
|
|
87
|
+
`janus({ mail: { throttle: { attempts, window } } })`, five per ten
|
|
88
|
+
minutes by default and `false` to turn it off. `magicLink.request`,
|
|
89
|
+
`signInCode.request` and `resetPassword.request` count per address;
|
|
90
|
+
`verifyEmail.send` and an e-mailed `stepUp.request` count per user; each
|
|
91
|
+
flow on its own. Past the limit a request throws `MailThrottledError`
|
|
92
|
+
(`MAIL_THROTTLED`, 429) with `retryAfter` and issues, spends and rotates
|
|
93
|
+
nothing: the last link or code sent still works, so a loop on an address
|
|
94
|
+
cannot lock its owner out.
|
|
95
|
+
The window is no longer than the shortest default token lifetime. The counts
|
|
96
|
+
live in the tokens store, with no port change. `MailConfig` and
|
|
97
|
+
`MailThrottleConfig` are exported.
|
|
73
98
|
- **A sign-in from a new device, v0.17.0** — `janus({ devices: { keys } })`,
|
|
74
99
|
and `{ device }` as the last argument of `signUp`, `signIn`,
|
|
75
100
|
`secondFactor.confirm`, `secondFactor.recover`, `signInCode.confirm` and
|
|
@@ -140,20 +165,6 @@ before is in the [CHANGELOG](../CHANGELOG.md).
|
|
|
140
165
|
user only — or a security settings page; `null` for a user with no active
|
|
141
166
|
factor. The listener can now tell the user how many codes remain, as
|
|
142
167
|
`@nxgt/janus-mail`'s `recoveryCodeUsed` e-mail does.
|
|
143
|
-
- **Recovery codes, v0.10.0** — a user who loses their authenticator app
|
|
144
|
-
still signs in, with no operator resetting the account.
|
|
145
|
-
`secondFactor.activate` answers `{ user, recoveryCodes }`: ten single-use
|
|
146
|
-
codes, shown once and stored only as keyed hashes (breaking: read `.user`).
|
|
147
|
-
`secondFactor.recover(challenge, code)` redeems a sign-in's challenge with
|
|
148
|
-
one and answers the session with `recoveryCodesLeft`;
|
|
149
|
-
`secondFactor.regenerateRecoveryCodes(user, code)` replaces them all on a
|
|
150
|
-
fresh code from the app, and counts its attempts itself — five per user
|
|
151
|
-
per 15-minute window, in the store — so a stolen session cannot guess the
|
|
152
|
-
code, and the route needs no limiter of its own. Two user events,
|
|
153
|
-
`user.recoveryCodesRegenerated` and `user.recoveryCodeUsed`. For adapters:
|
|
154
|
-
`SecondFactorRecord.recoveryCodes`,
|
|
155
|
-
stored by the users stores of `@nxgt/janus-drizzle` and
|
|
156
|
-
`@nxgt/janus-mongo`, with conformance cases.
|
|
157
168
|
- **Sending the e-mails, `@nxgt/janus-mail` v0.1.0** — a package of its
|
|
158
169
|
own, built on the `@nxgt/mail` toolkit: `janusMail({ mailer, from, brand,
|
|
159
170
|
links })` takes what each flow answers —
|
package/docs/troubleshooting.md
CHANGED
|
@@ -42,11 +42,16 @@ How the messages are shaped:
|
|
|
42
42
|
- [`janus: devices.keys: …`](#janus-deviceskeys-expected-at-least-one-key---id-key--the-first-seals)
|
|
43
43
|
- [`janus: events must be a function that takes a user event …`](#janus-events-must-be-a-function-that-takes-a-user-event--webhooks---from-nxgtjanus-webhooks-or-your-own)
|
|
44
44
|
- [`janus: signIn.throttle …` wiring messages](#janus-signinthrottle-wiring-messages)
|
|
45
|
+
- [`janus: mail.throttle …` wiring messages](#janus-mailthrottle-wiring-messages)
|
|
46
|
+
- [`janus: tokens.<flow> is 5m, shorter than mail.throttle.window, 10m …`](#janus-tokensflow-is-5m-shorter-than-mailthrottlewindow-10m-)
|
|
45
47
|
- [Other `janus:` wiring messages](#other-janus-wiring-messages)
|
|
46
48
|
|
|
47
49
|
**Users, sessions and tokens**
|
|
48
50
|
- [`STORE_FAILED` — `<slot>.<method>: the store could not answer`](#store_failed--slotmethod-the-store-could-not-answer)
|
|
49
51
|
- [`STORE_FAILED` — `<slot>.<method> answered undefined …`](#store_failed--slotmethod-answered-undefined-an-absence-is-null-so-this-store-forgot-to-answer)
|
|
52
|
+
- [`STORE_FAILED` — `users.findUsers answered no list …`](#store_failed--usersfindusers-answered-no-list-an-absence-is-a-shorter-list-so-this-store-forgot-to-answer)
|
|
53
|
+
- [`<call>: ids must be an array of user ids`](#call-ids-must-be-an-array-of-user-ids)
|
|
54
|
+
- [`findMany` answers fewer users than ids](#findmany-answers-fewer-users-than-ids)
|
|
50
55
|
- [`NOT_FOUND` — `<call>: no <type> has this id`](#not_found--call-no-type-has-this-id)
|
|
51
56
|
- [`LOGIN_TAKEN` — `<call>: the login is taken by another <type>`](#login_taken--call-the-login-is-taken-by-another-type)
|
|
52
57
|
- [`VERSION_CONFLICT` — `<call>: expected version <n>, found <m>`](#version_conflict--call-expected-version-n-found-m)
|
|
@@ -54,6 +59,7 @@ How the messages are shaped:
|
|
|
54
59
|
- [`PASSWORD_TOO_SHORT` — `<call>: the password is shorter than the policy's <n> characters`](#password_too_short--call-the-password-is-shorter-than-the-policys-n-characters)
|
|
55
60
|
- [`CREDENTIALS_INVALID` — `<call>: the login and the password do not match`](#credentials_invalid--call-the-login-and-the-password-do-not-match)
|
|
56
61
|
- [`CREDENTIALS_INVALID` — `<call>: too many passwords tried at this login …`](#credentials_invalid--call-too-many-passwords-tried-at-this-login--wait-for-the-next-window)
|
|
62
|
+
- [`MAIL_THROTTLED` — `<call>: too many e-mails asked for this address …`, or `… for this user …`](#mail_throttled--call-too-many-e-mails-asked-for-this-address--the-last-one-sent-still-works-use-it-or-wait-for-the-next-window)
|
|
57
63
|
- [`STORE_FAILED` — `<call>: the store dropped the attempts it had just stored`](#store_failed--call-the-store-dropped-the-attempts-it-had-just-stored)
|
|
58
64
|
- [`HASH_UNSUPPORTED` — `<call>: no wired verifier claims the prefix "<prefix>"`](#hash_unsupported--call-no-wired-verifier-claims-the-prefix-prefix)
|
|
59
65
|
- [`scryptHasher: the stored hash has the $scrypt$ prefix and not its format`](#scrypthasher-the-stored-hash-has-the-scrypt-prefix-and-not-its-format)
|
|
@@ -280,10 +286,10 @@ janus({ ..., hasher: scryptHasher(), verifiers: [legacyBcrypt] }); // not [scryp
|
|
|
280
286
|
|
|
281
287
|
### `janus: store.<slot> has no method <method>, which the port requires`
|
|
282
288
|
|
|
283
|
-
Also: `janus: store.<slot> is missing`, `janus: store must be an object with users, sessions and tokens`, `janus: store.sessions.deleteExpiredSessions must be a function or absent`.
|
|
289
|
+
Also: `janus: store.<slot> is missing`, `janus: store must be an object with users, sessions and tokens`, `janus: store.sessions.deleteExpiredSessions must be a function or absent`, `janus: store.users.findUsers must be a function or absent`.
|
|
284
290
|
|
|
285
291
|
**When:** `janus({...})`, from JavaScript or with a store typed loosely. TypeScript refuses a partial store at compile time and names the method.
|
|
286
|
-
**Why:** `store` is `{ users, sessions, tokens }`, and each slot must answer every method of the port. `deleteExpiredSessions`
|
|
292
|
+
**Why:** `store` is `{ users, sessions, tokens }`, and each slot must answer every method of the port. Two methods are optional — `sessions.deleteExpiredSessions` and `users.findUsers` — and each is absent, or a function: a value in its place (`findUsers: true`, a property copied from a config) is a wiring mistake, not an absent capability.
|
|
287
293
|
An adapter written for an earlier `@nxgt/janus` reports the method a later
|
|
288
294
|
release added to the port: `store.tokens has no method countAttempt` for one
|
|
289
295
|
written against 0.3 (the method came in 0.4), and
|
|
@@ -465,6 +471,39 @@ zero`, and a `janus: signIn.throttle.window` that is not a duration.
|
|
|
465
471
|
**Fix:** `signIn: { throttle: { attempts: 10, window: '15m' } }` — the
|
|
466
472
|
defaults — or `signIn: { throttle: false }` to count nothing.
|
|
467
473
|
|
|
474
|
+
### `janus: mail.throttle` wiring messages
|
|
475
|
+
|
|
476
|
+
`TypeError`, at `janus()`: `janus: mail must be an object — { throttle }`,
|
|
477
|
+
`janus: mail.throttle must be { attempts, window }, or false to count
|
|
478
|
+
nothing`, `janus: mail.throttle.attempts must be a whole number above zero`,
|
|
479
|
+
and a `janus: mail.throttle.window` that is not a duration.
|
|
480
|
+
**Fix:** `mail: { throttle: { attempts: 5, window: '10m' } }` — the defaults —
|
|
481
|
+
or `mail: { throttle: false }` to count nothing.
|
|
482
|
+
|
|
483
|
+
### `janus: tokens.<flow> is 5m, shorter than mail.throttle.window, 10m …`
|
|
484
|
+
|
|
485
|
+
A warning, not an error: `janus()` returns, and emits through
|
|
486
|
+
`process.emitWarning` with the code `JANUS_THROTTLE_WINDOW`, once per call. The
|
|
487
|
+
text, here for `tokens.signInCode: '5m'` with the default window:
|
|
488
|
+
|
|
489
|
+
```
|
|
490
|
+
janus: tokens.signInCode is 5m, shorter than mail.throttle.window, 10m — past the limit the last token sent can expire before the refusal ends. Set mail.throttle.window to 5m or less, or raise tokens.signInCode to 10m or more.
|
|
491
|
+
```
|
|
492
|
+
|
|
493
|
+
With several flows shorter than the window, one warning lists each
|
|
494
|
+
(`tokens.magicLink is 2m, … ; tokens.stepUp is 30s, …`). **Why:** a request the
|
|
495
|
+
mail throttle refuses spends nothing, so the last link or code sent still works,
|
|
496
|
+
but only until it expires; with a lifetime shorter than the window it can lapse
|
|
497
|
+
while the visitor is still refused. **Fix:** make the window no longer than the
|
|
498
|
+
shortest lifetime, or raise the lifetime:
|
|
499
|
+
|
|
500
|
+
```ts
|
|
501
|
+
janus({ user, store, tokens: { signInCode: '5m' }, mail: { throttle: { window: '5m' } } });
|
|
502
|
+
```
|
|
503
|
+
|
|
504
|
+
A runtime without `process.emitWarning` stays silent. Silence it with
|
|
505
|
+
`node --disable-warning=JANUS_THROTTLE_WINDOW`.
|
|
506
|
+
|
|
468
507
|
### Other `janus:` wiring messages
|
|
469
508
|
|
|
470
509
|
| Message | Fix |
|
|
@@ -507,6 +546,40 @@ try {
|
|
|
507
546
|
**Why:** a method that can find nothing must answer `null`. `undefined` is also what a missing `return` produces, so it is treated as a bug in the store, not as "not found".
|
|
508
547
|
**Fix:** `return doc ?? null;` — and run `@nxgt/janus/conformance` against the store.
|
|
509
548
|
|
|
549
|
+
### `STORE_FAILED` — `users.findUsers answered no list: an absence is a shorter list, so this store forgot to answer`
|
|
550
|
+
|
|
551
|
+
**When:** `findMany`, over a users store you wrote that implements the optional `findUsers`.
|
|
552
|
+
**Why:** `findUsers` answered `null`, `undefined` or something else than an array. An id nobody holds is left out of the list, and nobody at all is `[]`; anything else is a store that forgot to answer, so it fails rather than read as "nobody".
|
|
553
|
+
**Fix:** answer the rows you found, mapped to records — or leave `findUsers` out, and `findMany` reads with `findUser`:
|
|
554
|
+
|
|
555
|
+
```ts
|
|
556
|
+
findUsers: async (ids) => (await db.users.where('id', 'in', ids)).map(toUser), // [] when none
|
|
557
|
+
```
|
|
558
|
+
|
|
559
|
+
### `<call>: ids must be an array of user ids`
|
|
560
|
+
|
|
561
|
+
A bare `TypeError` — `findMany: …`, or `staff.findMany: …` with several user types.
|
|
562
|
+
|
|
563
|
+
**When:** `findMany` was given something other than an array: one id, a `Set`, a comma-separated string.
|
|
564
|
+
**Why:** `findMany` takes a list; one id is `find`. Only your code passes the wrong shape, and TypeScript refuses it.
|
|
565
|
+
**Fix:** pass an array, or use `find` for one id:
|
|
566
|
+
|
|
567
|
+
```ts
|
|
568
|
+
await auth.staff.findMany([...selectedIds]); // from a Set
|
|
569
|
+
await auth.staff.find(id); // one id
|
|
570
|
+
```
|
|
571
|
+
|
|
572
|
+
### `findMany` answers fewer users than ids
|
|
573
|
+
|
|
574
|
+
**When:** always, when an id is malformed, unknown, held by a user of another type, or repeated.
|
|
575
|
+
**Why:** `findMany` leaves out what `find` answers `null` for, and answers a repeated id once — it never puts `null` in a place. Reading `users[i]` as the user of `ids[i]` then shows one row's account on another.
|
|
576
|
+
**Fix:** match by id:
|
|
577
|
+
|
|
578
|
+
```ts
|
|
579
|
+
const byId = new Map((await auth.staff.findMany(ids)).map((user) => [user.id, user]));
|
|
580
|
+
const member = byId.get(row.staffId); // undefined: nobody of this type holds it
|
|
581
|
+
```
|
|
582
|
+
|
|
510
583
|
### `NOT_FOUND` — `<call>: no <type> has this id`
|
|
511
584
|
|
|
512
585
|
`NotFoundError`, from `get`, `getUser`, `update`, `setActive`, `setPassword`, `verifyEmail.send`, `secondFactor.recoveryCodesLeft`. Also `<call>: the user has no e-mail`.
|
|
@@ -641,6 +714,52 @@ if (error instanceof JanusError && error.retryAfter !== undefined) {
|
|
|
641
714
|
janus({ ..., signIn: { throttle: { attempts: 20, window: '15m' } } });
|
|
642
715
|
```
|
|
643
716
|
|
|
717
|
+
### `MAIL_THROTTLED` — `<call>: too many e-mails asked for this address — the last one sent still works; use it, or wait for the next window`
|
|
718
|
+
|
|
719
|
+
`MailThrottledError`, with `retryAfter` and `userType`. The per-user flows say
|
|
720
|
+
`… too many e-mails asked for this user — the last one sent still works; use
|
|
721
|
+
it, or wait for the next window` and carry `userId`.
|
|
722
|
+
|
|
723
|
+
**When:** `magicLink.request`, `signInCode.request` or
|
|
724
|
+
`resetPassword.request` (per address), or `verifyEmail.send` and an e-mailed
|
|
725
|
+
`stepUp.request` (per user), past five requests of that flow in the current
|
|
726
|
+
10-minute window — whether or not anybody holds the address. `<call>` is the
|
|
727
|
+
call, such as `magicLink.request` or `patient.signInCode.request`.
|
|
728
|
+
**Why:** requests that send e-mail are throttled, on by default, so a loop
|
|
729
|
+
cannot fill an inbox or cancel its owner's link or code. Each flow counts on
|
|
730
|
+
its own, in fixed windows of the clock, by the store. **A refused request
|
|
731
|
+
spends, invalidates and rotates nothing**: no token, code or challenge is
|
|
732
|
+
issued, and the last link or code sent — by anyone's request, an attacker's
|
|
733
|
+
included — still confirms until it expires, so a loop on a victim's address
|
|
734
|
+
cannot lock them out. Nothing locks: the next window answers again. In tests with `fixedClock`, the window never
|
|
735
|
+
ends.
|
|
736
|
+
**Fix:** answer 429 with a `Retry-After` header, and tell the visitor to use
|
|
737
|
+
the last e-mail they received (it still works), or to wait `retryAfter`
|
|
738
|
+
seconds: "Check your inbox: the last e-mail we sent still works. You can ask for
|
|
739
|
+
a new one in N minutes." It reveals nothing about accounts, so it is safe to
|
|
740
|
+
show.
|
|
741
|
+
In tests, advance the clock past `retryAfter`, or wire `mail: { throttle:
|
|
742
|
+
false }`. If nobody you know asked five times, somebody is looping on the
|
|
743
|
+
route: `janus` never sees IP addresses, so limit it per client, which the
|
|
744
|
+
throttle does not — with `@nxgt/redis` rate limits (`defineRateLimit`,
|
|
745
|
+
`bindRateLimit`; `enforce()` throws `GuardError` `RATE_LIMITED` whose
|
|
746
|
+
`retryAfter` is in milliseconds), as in the
|
|
747
|
+
[e-mail flows guide](guide/email-flows.md#requests-that-send-e-mail-are-throttled).
|
|
748
|
+
If you set a token lifetime shorter than the window (`tokens.signInCode: '5m'`
|
|
749
|
+
with the default), the last code can expire before the window ends: keep
|
|
750
|
+
`mail.throttle.window` no longer than the shortest `tokens.*` you use.
|
|
751
|
+
|
|
752
|
+
```ts
|
|
753
|
+
import { MailThrottledError } from '@nxgt/janus';
|
|
754
|
+
|
|
755
|
+
if (error instanceof MailThrottledError) {
|
|
756
|
+
const headers = { 'retry-after': String(error.retryAfter) };
|
|
757
|
+
return Response.json({ code: error.code, retryAfter: error.retryAfter }, { status: 429, headers });
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
janus({ ..., mail: { throttle: { attempts: 10, window: '10m' } } });
|
|
761
|
+
```
|
|
762
|
+
|
|
644
763
|
### `STORE_FAILED` — `<call>: the store dropped the attempts it had just stored`
|
|
645
764
|
|
|
646
765
|
`StoreFailure`, with `slot: 'tokens'` and `operation: 'countAttempt'`.
|
|
@@ -1153,7 +1272,9 @@ A `TypeError`.
|
|
|
1153
1272
|
|
|
1154
1273
|
The messages start with `signInCode.confirm` — prefixed by the type with
|
|
1155
1274
|
several user types: `patient.signInCode.confirm: …`. `signInCode.request`
|
|
1156
|
-
throws nothing but `STORE_FAILED
|
|
1275
|
+
throws nothing but `STORE_FAILED` and
|
|
1276
|
+
[`MAIL_THROTTLED`](#mail_throttled--call-too-many-e-mails-asked-for-this-address--the-last-one-sent-still-works-use-it-or-wait-for-the-next-window):
|
|
1277
|
+
an e-mail it cannot sign in is `null`.
|
|
1157
1278
|
`signInCode.confirm` also rejects with
|
|
1158
1279
|
[`CODE_INVALID`](#code_invalid--call-the-code-does-not-match-or-was-already-used),
|
|
1159
1280
|
[`TOKEN_UNKNOWN`, `TOKEN_SPENT`, `TOKEN_EXPIRED`](#token_unknown-token_spent-token_expired),
|
|
@@ -1236,8 +1357,9 @@ janus({
|
|
|
1236
1357
|
|
|
1237
1358
|
`magicLink.confirm`'s messages start with `magicLink.confirm` — prefixed by
|
|
1238
1359
|
the type with several user types: `patient.magicLink.confirm: …`.
|
|
1239
|
-
`magicLink.request` throws nothing but `STORE_FAILED
|
|
1240
|
-
|
|
1360
|
+
`magicLink.request` throws nothing but `STORE_FAILED` and
|
|
1361
|
+
[`MAIL_THROTTLED`](#mail_throttled--call-too-many-e-mails-asked-for-this-address--the-last-one-sent-still-works-use-it-or-wait-for-the-next-window):
|
|
1362
|
+
an e-mail it cannot sign in is `null`. `magicLink.confirm` rejects with
|
|
1241
1363
|
[`TOKEN_UNKNOWN`, `TOKEN_SPENT`, `TOKEN_EXPIRED`](#token_unknown-token_spent-token_expired),
|
|
1242
1364
|
[`TOKEN_STALE`](#token_stale--call-the-token-was-sent-to-an-e-mail-the-user-no-longer-has),
|
|
1243
1365
|
[`USER_INACTIVE`](#user_inactive--call-the-user-is-inactive) and
|
package/package.json
CHANGED
|
@@ -1,5 +0,0 @@
|
|
|
1
|
-
/** Resolves `signIn.throttle`: on unless `false`, ten passwords per login per fifteen minutes. */
|
|
2
|
-
import type { SignInConfig } from './janus-config';
|
|
3
|
-
import type { ResolvedConfig } from './resolved-config';
|
|
4
|
-
export declare function resolveSignInThrottle(config: SignInConfig | undefined, where: string): ResolvedConfig['signInThrottle'];
|
|
5
|
-
//# sourceMappingURL=resolve-sign-in.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"resolve-sign-in.d.ts","sourceRoot":"","sources":["../../../src/auth/config/resolve-sign-in.ts"],"names":[],"mappings":"AAAA,kGAAkG;AAGlG,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAC;AACnD,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,mBAAmB,CAAC;AAExD,wBAAgB,qBAAqB,CACpC,MAAM,EAAE,YAAY,GAAG,SAAS,EAChC,KAAK,EAAE,MAAM,GACX,cAAc,CAAC,gBAAgB,CAAC,CAoBlC"}
|
|
File without changes
|
|
File without changes
|