@nxgt/janus 0.17.1 → 0.18.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 +72 -18
- 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/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/chunks/{index-kj3st57z.js → index-4khsm7mw.js} +2 -2
- package/dist/chunks/{index-6c9fbtv2.js → index-cntabs9d.js} +2 -2
- package/dist/chunks/{index-hnwggyxs.js → index-kppkcbn5.js} +52 -49
- package/dist/chunks/{index-hnwggyxs.js.map → index-kppkcbn5.js.map} +4 -4
- package/dist/chunks/{index-baazjyvc.js → index-kvjj5xkp.js} +2 -2
- package/dist/conformance/index.js +3 -3
- 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 +118 -29
- package/dist/index.js.map +18 -15
- package/dist/permissions/index.js +40 -29
- package/dist/permissions/index.js.map +6 -6
- package/docs/README.md +1 -1
- 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 +1 -0
- package/docs/roadmap.md +14 -1
- package/docs/troubleshooting.md +88 -3
- 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-kj3st57z.js.map → index-4khsm7mw.js.map} +0 -0
- /package/dist/chunks/{index-6c9fbtv2.js.map → index-cntabs9d.js.map} +0 -0
- /package/dist/chunks/{index-baazjyvc.js.map → index-kvjj5xkp.js.map} +0 -0
|
@@ -114,7 +114,11 @@ readonly verifyEmail: {
|
|
|
114
114
|
};
|
|
115
115
|
```
|
|
116
116
|
|
|
117
|
-
`send` issues a token for the user's **current** e-mail.
|
|
117
|
+
`send` issues a token for the user's **current** e-mail. It is throttled
|
|
118
|
+
**per user**, five per 10-minute window: past it, `MailThrottledError`
|
|
119
|
+
(`MAIL_THROTTLED`, with `userId`) and no token. The application calls it for a
|
|
120
|
+
user it knows, so an unauthenticated loop on the address cannot spend the
|
|
121
|
+
count, and a user who changes e-mail buys no extra sends. `confirm` redeems it
|
|
118
122
|
and sets `emailVerified`. A token sent to an e-mail the user has since changed
|
|
119
123
|
is `TOKEN_STALE`: confirming it would verify an address nobody holds any more.
|
|
120
124
|
The address is checked again on the very record the write replaces, so an
|
|
@@ -165,9 +169,15 @@ if (first !== null && second !== null) {
|
|
|
165
169
|
|
|
166
170
|
Requests that arrive at once cannot each keep a link: each spends the
|
|
167
171
|
others' once it issued its own, so at most one survives — sometimes none, and
|
|
168
|
-
the visitor asks again.
|
|
169
|
-
codes: each one sends an e-mail and cancels the link before it, so
|
|
170
|
-
|
|
172
|
+
the visitor asks again. **`request` is throttled per address**, as for
|
|
173
|
+
sign-in codes: each one sends an e-mail and cancels the link before it, so
|
|
174
|
+
past five requests for one address in a 10-minute window it throws
|
|
175
|
+
`MailThrottledError` (`MAIL_THROTTLED`) and issues nothing; the link last sent
|
|
176
|
+
still works. An address nobody holds is counted and refused alike, and under
|
|
177
|
+
the limit still answers `null`. Somebody who knows an address can fill its
|
|
178
|
+
inbox with at most five reset e-mails a window, but cannot shut the reset:
|
|
179
|
+
the last one sent still works. Limit the route per client yourself too, and
|
|
180
|
+
see [the mail throttle](#requests-that-send-e-mail-are-throttled).
|
|
171
181
|
|
|
172
182
|
**Writing a password spends every reset link still live.** A `confirm`,
|
|
173
183
|
`changePassword` and `setPassword` each do, so a link sent before the
|
|
@@ -222,6 +232,156 @@ export async function resetPassword(request: Request): Promise<Response> {
|
|
|
222
232
|
}
|
|
223
233
|
```
|
|
224
234
|
|
|
235
|
+
## Requests that send e-mail are throttled
|
|
236
|
+
|
|
237
|
+
**Every request that hands out something to e-mail is counted**, on by default:
|
|
238
|
+
five per 10 minutes, per flow. Past the limit the request throws
|
|
239
|
+
`MailThrottledError` (`MAIL_THROTTLED`, 429) with `retryAfter` — the whole
|
|
240
|
+
seconds to the end of the window, at least 1 — and **a refused request spends,
|
|
241
|
+
invalidates and rotates nothing**: no token, code or challenge is minted, and
|
|
242
|
+
nothing earlier is spent, so **the last link or code sent still confirms until
|
|
243
|
+
it expires**, whoever asked for it. The count comes before anything is issued,
|
|
244
|
+
and the earlier tokens are spent only after a new one is issued.
|
|
245
|
+
|
|
246
|
+
```ts
|
|
247
|
+
import { MailThrottledError } from '@nxgt/janus';
|
|
248
|
+
|
|
249
|
+
try {
|
|
250
|
+
await auth.resetPassword.request(email);
|
|
251
|
+
} catch (error) {
|
|
252
|
+
if (error instanceof MailThrottledError) {
|
|
253
|
+
// 429 and Retry-After: error.retryAfter — safe to show, it reveals nothing about accounts.
|
|
254
|
+
// Tell the visitor: "Check your inbox: the last e-mail we sent still works.
|
|
255
|
+
// You can ask for a new one in N minutes."
|
|
256
|
+
}
|
|
257
|
+
throw error;
|
|
258
|
+
}
|
|
259
|
+
```
|
|
260
|
+
|
|
261
|
+
| Flow | Counted per | Why |
|
|
262
|
+
| --- | --- | --- |
|
|
263
|
+
| `magicLink.request(email)` | address | Anybody can call it with any address. Counted before the address is looked up, so an address nobody holds is refused like a registered one |
|
|
264
|
+
| `signInCode.request(email)` | address | The same |
|
|
265
|
+
| `resetPassword.request(email)` | address | The same |
|
|
266
|
+
| `verifyEmail.send(user)` | user | The application calls it for a user it knows: an unauthenticated loop on the address cannot spend this count, so it never blocks the user's own verification, and a user changing e-mail buys no extra sends |
|
|
267
|
+
| `stepUp.request(user)`, when it e-mails a code (`via: 'email'`) | user | The same. A step-up confirmed with the app (`via: 'secondFactor'`) sends nothing and is not counted |
|
|
268
|
+
|
|
269
|
+
**Keep the window no longer than the shortest token lifetime you set.** A
|
|
270
|
+
refused request spends nothing, but the last link or code sent still has to be
|
|
271
|
+
alive. With `tokens.signInCode: '5m'` and the 10-minute window, a code can lapse
|
|
272
|
+
while the visitor is still refused. `janus()` warns for it, once per call, with
|
|
273
|
+
`process.emitWarning` and the code `JANUS_THROTTLE_WINDOW`, naming each flow
|
|
274
|
+
whose lifetime is shorter than the window:
|
|
275
|
+
|
|
276
|
+
```
|
|
277
|
+
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.
|
|
278
|
+
```
|
|
279
|
+
|
|
280
|
+
Shorten `mail.throttle.window`, or raise the lifetime. Nothing is refused, so
|
|
281
|
+
a window you chose on purpose only costs the warning.
|
|
282
|
+
|
|
283
|
+
**Each flow counts on its own**: a loop on `magicLink.request` never shuts
|
|
284
|
+
`signInCode.request`. The address is normalised (trimmed, lowercased) and is
|
|
285
|
+
the same count whatever the user type.
|
|
286
|
+
|
|
287
|
+
| Option | Type | Default | Effect |
|
|
288
|
+
| --- | --- | --- | --- |
|
|
289
|
+
| `mail.throttle.attempts` | whole number above zero | `5` | Requests per flow, per address or user, per window |
|
|
290
|
+
| `mail.throttle.window` | `Duration` | `'10m'` | How long a window lasts |
|
|
291
|
+
| `mail.throttle` | `false` | | Counts nothing: limit those requests yourself |
|
|
292
|
+
|
|
293
|
+
```ts
|
|
294
|
+
janus({ user, store, mail: { throttle: { attempts: 3, window: '1h' } } });
|
|
295
|
+
janus({ user, store, mail: { throttle: false } });
|
|
296
|
+
```
|
|
297
|
+
|
|
298
|
+
```ts
|
|
299
|
+
export interface MailConfig {
|
|
300
|
+
readonly throttle?: MailThrottleConfig | false;
|
|
301
|
+
}
|
|
302
|
+
export interface MailThrottleConfig {
|
|
303
|
+
readonly attempts?: number;
|
|
304
|
+
readonly window?: Duration;
|
|
305
|
+
}
|
|
306
|
+
```
|
|
307
|
+
|
|
308
|
+
Five per ten minutes is no longer than the shortest default token lifetime
|
|
309
|
+
(`signInCode` and `stepUp` `'10m'`; `magicLink` `'15m'`, `resetPassword` `'1h'`,
|
|
310
|
+
`verifyEmail` `'24h'`), so under the defaults the last link or code sent —
|
|
311
|
+
issued inside the window — is still live for the whole refusal. It leaves room
|
|
312
|
+
for a visitor who asks again, and holds a loop to thirty e-mails an hour per
|
|
313
|
+
flow and address. A wrong value is refused when `janus()` is called, with
|
|
314
|
+
a `TypeError`: `janus: mail must be an object — { throttle }`, `janus:
|
|
315
|
+
mail.throttle must be { attempts, window }, or false to count nothing`,
|
|
316
|
+
`janus: mail.throttle.attempts must be a whole number above zero`, or a
|
|
317
|
+
`janus: mail.throttle.window` duration message.
|
|
318
|
+
|
|
319
|
+
**Fixed windows, and nothing locks.** The window is a fixed slice of the
|
|
320
|
+
clock, as the sign-in throttle's. Requests at once are counted in one store
|
|
321
|
+
write each: of twenty at once, exactly five are issued. The next window
|
|
322
|
+
answers again. A refused request is counted too but cannot extend the wait.
|
|
323
|
+
|
|
324
|
+
**Where the counts live.** In the tokens store, as `secondFactor` tokens named
|
|
325
|
+
by a keyed hash of the flow and the address or user id: no port change, no new
|
|
326
|
+
infrastructure, and never the address. That is one row per flow per address or
|
|
327
|
+
user per window. On PostgreSQL, `collectExpired()` does not collect lapsed
|
|
328
|
+
ones: schedule the delete, as for the sign-in throttle. A flushed or evicting
|
|
329
|
+
Redis forgets the counts, and a tokens store that cannot count throws
|
|
330
|
+
`STORE_FAILED` with nothing issued: it fails closed.
|
|
331
|
+
|
|
332
|
+
**What it does not do.**
|
|
333
|
+
|
|
334
|
+
- **It cannot lock somebody out.** Somebody who asks for an address in a loop
|
|
335
|
+
can fill its inbox with at most five e-mails per window per flow, but the
|
|
336
|
+
last e-mail they caused to be sent was a real e-mail to the real owner and
|
|
337
|
+
still works until it expires. The password sign-in is untouched.
|
|
338
|
+
- **A lifetime shorter than the window breaks that.** With
|
|
339
|
+
`tokens.signInCode: '5m'` and the default window, the last code can expire
|
|
340
|
+
before the window ends and the visitor has no live code until it does. Keep
|
|
341
|
+
`mail.throttle.window` no longer than the shortest of the `tokens.*`
|
|
342
|
+
lifetimes you use.
|
|
343
|
+
- **It is not per client.** `janus` never sees IP addresses, so one client
|
|
344
|
+
asking for many addresses is not counted. Add a per-IP ceiling in front of
|
|
345
|
+
the routes: `@nxgt/redis` (0.5.0 and later) has rate limits for it.
|
|
346
|
+
|
|
347
|
+
```ts
|
|
348
|
+
import { bindRateLimit, defineRateLimit, GuardError } from '@nxgt/redis';
|
|
349
|
+
|
|
350
|
+
export const mailRequests = defineRateLimit({
|
|
351
|
+
name: 'mail-requests',
|
|
352
|
+
key: (p: { ip: string }) => p.ip,
|
|
353
|
+
limit: 20,
|
|
354
|
+
per: 3_600_000,
|
|
355
|
+
});
|
|
356
|
+
const perIp = bindRateLimit(client, mailRequests); // or redis.limits.<name> when wired
|
|
357
|
+
|
|
358
|
+
try {
|
|
359
|
+
await perIp.enforce({ ip }); // before auth.magicLink.request(email)
|
|
360
|
+
} catch (error) {
|
|
361
|
+
if (error instanceof GuardError && error.code === 'RATE_LIMITED') {
|
|
362
|
+
// error.retryAfter is in MILLISECONDS: Math.ceil(error.retryAfter / 1000) for Retry-After
|
|
363
|
+
}
|
|
364
|
+
throw error;
|
|
365
|
+
}
|
|
366
|
+
```
|
|
367
|
+
- **A test suite** that requests more than five of one flow for one address or
|
|
368
|
+
user over a `fixedClock` is throttled: advance the clock past `retryAfter`,
|
|
369
|
+
or wire `mail: { throttle: false }`.
|
|
370
|
+
|
|
371
|
+
**Not throttled**, because they follow a write or a sign-in, which are bounded
|
|
372
|
+
elsewhere (the [sign-in throttle](passwords.md#password-guessing-is-throttled)
|
|
373
|
+
bounds sign-ins), not a request to mail an address:
|
|
374
|
+
|
|
375
|
+
- `update` changing the e-mail: an application write. Its notice to the former
|
|
376
|
+
address is sent from the `user.emailChanged` event.
|
|
377
|
+
- The notices sent from [events](events.md): a new device, a password changed,
|
|
378
|
+
a recovery code used, and the like.
|
|
379
|
+
|
|
380
|
+
`@nxgt/janus-hono` and `@nxgt/janus-graphql` answer `MAIL_THROTTLED` with 429,
|
|
381
|
+
`retryAfter` in the body (or the extensions) and a `Retry-After` header;
|
|
382
|
+
`@nxgt/janus-telemetry` writes a `janus.mail.throttled` warning with
|
|
383
|
+
`janus.mail.flow` and `janus.mail.retryAfter`, never the address.
|
|
384
|
+
|
|
225
385
|
## What a token refusal means
|
|
226
386
|
|
|
227
387
|
| Code | When |
|
package/docs/guide/errors.md
CHANGED
|
@@ -17,11 +17,11 @@ export function toResponse(error: unknown): Response {
|
|
|
17
17
|
|
|
18
18
|
`statusOf(code)` is the table under [The codes](#the-codes), exported so every
|
|
19
19
|
integration answers a code the same way — `@nxgt/janus-hono`'s `statusOf` is
|
|
20
|
-
this one. It answers `JanusErrorStatus`, a union of the
|
|
21
|
-
give (`400 | 401 | 403 | 404 | 409 | 500 | 501 | 503`), so a framework whose
|
|
20
|
+
this one. It answers `JanusErrorStatus`, a union of the nine statuses it can
|
|
21
|
+
give (`400 | 401 | 403 | 404 | 409 | 429 | 500 | 501 | 503`), so a framework whose
|
|
22
22
|
response takes a narrower type than `number` accepts it as it is.
|
|
23
23
|
|
|
24
|
-
`JanusErrorCode` is a union of
|
|
24
|
+
`JanusErrorCode` is a union of twenty-one string literals, and `statusOf` is a
|
|
25
25
|
`switch` over it that is exhaustive: when a code is added, it stops compiling
|
|
26
26
|
instead of answering `undefined` — and so does a `switch` of your own.
|
|
27
27
|
|
|
@@ -56,6 +56,7 @@ that wired the library, so no handler needs to tell it apart.
|
|
|
56
56
|
| `PASSWORD_TOO_SHORT` | `CredentialError` | 400 | Below `password.minLength` | `minLength` — never the password |
|
|
57
57
|
| `CREDENTIALS_INVALID` | `CredentialError` | 401 | Unknown login, no password, or the wrong one — **one code for the three** — and any password, the right one included, at a login past its attempts in the window — see [throttling](passwords.md#password-guessing-is-throttled) | `reason`, for your logs only; `retryAfter`, the seconds until the next window, when throttled |
|
|
58
58
|
| `HASH_UNSUPPORTED` | `CredentialError` | 400 | A stored hash no wired hasher reads | `hashPrefix` — never the hash |
|
|
59
|
+
| `MAIL_THROTTLED` | `MailThrottledError` | 429 | A request that sends e-mail was made too often for one address or one user in the window — see [the mail throttle](email-flows.md#requests-that-send-e-mail-are-throttled). **Nothing was issued, spent or rotated**: the last link or code sent still works | `retryAfter`, the whole seconds to the end of the window, at least 1; `userType`; `userId` only for the per-user flows. No `reason` |
|
|
59
60
|
| `USER_INACTIVE` | `UserInactiveError` | 403 | Deactivated; told only to someone who gave the right password, the right code, or a live sign-in link | `userId` |
|
|
60
61
|
| `STEP_UP_REQUIRED` | `StepUpRequiredError` | 403 | `assertFresh`: the session proved who it is `maxAge` ago or more. Not a denial of the action — ask for a [step-up](step-up.md), then send the request again | `userId` |
|
|
61
62
|
| `TOKEN_UNKNOWN`, `TOKEN_SPENT`, `TOKEN_EXPIRED`, `TOKEN_STALE` | `TokenError` | 400 | See [e-mail flows](email-flows.md#what-a-token-refusal-means). For a second factor's challenge: sign in again; for a sign-in code's: request a new code — see [sign-in codes](sign-in-code.md#what-confirm-refuses); for a sign-in link: ask for a new link — see [sign-in links](magic-link.md#what-confirm-refuses) | `userId` on `TOKEN_STALE` |
|
|
@@ -104,6 +105,14 @@ async function signIn(email: string, password: string): Promise<Response> {
|
|
|
104
105
|
`Retry-After` header: the login is throttled, and the right password is
|
|
105
106
|
refused too until then. It says nothing of whether the login exists — an
|
|
106
107
|
unknown one is throttled alike.
|
|
108
|
+
- **`MAIL_THROTTLED`'s `retryAfter`** belongs in the body and a `Retry-After`
|
|
109
|
+
header with a 429. Unlike `reason`, it is **safe to show the visitor**: an
|
|
110
|
+
address nobody holds is counted and refused alike, so it reveals nothing
|
|
111
|
+
about accounts. Its message names the call and whether the address or the
|
|
112
|
+
user was counted: `magicLink.request: too many e-mails asked for this
|
|
113
|
+
address — the last one sent still works; use it, or wait for the next window`,
|
|
114
|
+
or `… for this user …`. Tell the visitor to use the last e-mail they received,
|
|
115
|
+
or to wait `retryAfter` seconds.
|
|
107
116
|
- **`CODE_INVALID`'s `attemptsLeft`** belongs in the body — the form can say
|
|
108
117
|
how many attempts are left. `0` means the challenge is spent: send the visitor
|
|
109
118
|
back to the password.
|
package/docs/guide/magic-link.md
CHANGED
|
@@ -90,9 +90,45 @@ once cannot each keep a link: at most one survives, sometimes none, and the
|
|
|
90
90
|
visitor asks again. A link and a [sign-in code](sign-in-code.md) are
|
|
91
91
|
separate kinds: asking for one leaves the other live.
|
|
92
92
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
93
|
+
**`request` is throttled per address**: every call sends an e-mail and
|
|
94
|
+
cancels the link before it, so past five requests for one address in a
|
|
95
|
+
10-minute window, `request` throws `MailThrottledError` (`MAIL_THROTTLED`)
|
|
96
|
+
and issues nothing: **a refused request spends and rotates nothing, so the
|
|
97
|
+
link last sent still works** until it expires, even when somebody else's
|
|
98
|
+
request caused it. The address is counted
|
|
99
|
+
before anything is looked up, so an address nobody holds is counted and
|
|
100
|
+
refused alike, and under the limit it still answers `null`. Sign-in codes and
|
|
101
|
+
password resets count on their own: a loop on this route never shuts them.
|
|
102
|
+
Answer it with a 429 and `Retry-After`:
|
|
103
|
+
|
|
104
|
+
```ts
|
|
105
|
+
import { MailThrottledError } from '@nxgt/janus';
|
|
106
|
+
|
|
107
|
+
try {
|
|
108
|
+
const issued = await auth.magicLink.request(email);
|
|
109
|
+
// …send the e-mail from `issued`, whether or not there is one to send
|
|
110
|
+
} catch (error) {
|
|
111
|
+
if (error instanceof MailThrottledError) {
|
|
112
|
+
return Response.json(
|
|
113
|
+
{
|
|
114
|
+
error: 'too many e-mails — the last one we sent still works; use it, or wait',
|
|
115
|
+
retryAfter: error.retryAfter,
|
|
116
|
+
},
|
|
117
|
+
{ status: 429, headers: { 'retry-after': String(error.retryAfter) } },
|
|
118
|
+
);
|
|
119
|
+
}
|
|
120
|
+
throw error;
|
|
121
|
+
}
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
Tell the visitor to use the last e-mail they received, or to wait
|
|
125
|
+
`retryAfter` seconds: "Check your inbox: the last e-mail we sent still works.
|
|
126
|
+
You can ask for a new one in N minutes." The throttle is per address, not per
|
|
127
|
+
client: `janus` never sees IP addresses, so one client asking for many
|
|
128
|
+
addresses is not counted. Add a per-IP ceiling with `@nxgt/redis` rate limits
|
|
129
|
+
([example](email-flows.md#requests-that-send-e-mail-are-throttled)). See
|
|
130
|
+
[the mail throttle](email-flows.md#requests-that-send-e-mail-are-throttled)
|
|
131
|
+
for the options.
|
|
96
132
|
|
|
97
133
|
## Building the link
|
|
98
134
|
|
|
@@ -332,8 +368,8 @@ has each message with its cause.
|
|
|
332
368
|
for sign-in codes.
|
|
333
369
|
- **Signing out everywhere.** It revokes sessions; a link opens a new one.
|
|
334
370
|
- **The sign-in throttle.** `signIn.throttle` counts passwords tried at one
|
|
335
|
-
login; a link is not a password, and its token cannot be guessed.
|
|
336
|
-
|
|
371
|
+
login; a link is not a password, and its token cannot be guessed. The
|
|
372
|
+
[mail throttle](#requesting-a-link) counts its requests instead.
|
|
337
373
|
|
|
338
374
|
What does: its own redemption, a newer `request`, its fifteen minutes, an
|
|
339
375
|
e-mail changed since (`TOKEN_STALE`), the user deactivated
|
package/docs/guide/passwords.md
CHANGED
|
@@ -234,7 +234,8 @@ The throttle counts per login. It does not see:
|
|
|
234
234
|
- `changePassword`, which compares the current password too: limit it per
|
|
235
235
|
user.
|
|
236
236
|
- `resetPassword.request`, `signInCode.request` and `magicLink.request`,
|
|
237
|
-
which send e-mail:
|
|
237
|
+
which send e-mail: [the mail throttle](email-flows.md#requests-that-send-e-mail-are-throttled)
|
|
238
|
+
counts them per address, but not per client. Limit them per client.
|
|
238
239
|
|
|
239
240
|
```ts
|
|
240
241
|
if (!(await limiter.consume(`sign-in:${clientAddress}`))) {
|
|
@@ -130,9 +130,9 @@ patient observer; that limit is stated rather than denied.
|
|
|
130
130
|
|
|
131
131
|
Every `request` issues a **new** code with its own challenge, and spends the
|
|
132
132
|
earlier ones: only the last code sent works — see
|
|
133
|
-
[attempts](#attempts).
|
|
134
|
-
|
|
135
|
-
|
|
133
|
+
[attempts](#attempts). How often a code is asked for is limited per address by [the mail
|
|
134
|
+
throttle](email-flows.md#requests-that-send-e-mail-are-throttled); limit the
|
|
135
|
+
route per client yourself too.
|
|
136
136
|
|
|
137
137
|
## Sending the code by e-mail
|
|
138
138
|
|
|
@@ -252,10 +252,46 @@ Requests that arrive at once cannot each keep a code: each spends the others'
|
|
|
252
252
|
once it issued its own, so at most one survives — sometimes none, and the
|
|
253
253
|
visitor asks again. Guesses therefore never run against two live challenges.
|
|
254
254
|
|
|
255
|
-
|
|
256
|
-
`request`, and each one sends an e-mail and cancels the code before it
|
|
257
|
-
|
|
258
|
-
|
|
255
|
+
**`request` is throttled per address.** A new challenge takes only a new
|
|
256
|
+
`request`, and each one sends an e-mail and cancels the code before it, so
|
|
257
|
+
past five requests for one address in a 10-minute window `request` throws
|
|
258
|
+
`MailThrottledError` (`MAIL_THROTTLED`) and issues nothing — no challenge is
|
|
259
|
+
minted, a refused request spends and rotates nothing, and the code last sent
|
|
260
|
+
still works until it expires, even when somebody else's request caused it. The address is counted before
|
|
261
|
+
anything is looked up, so an address nobody holds is counted and refused
|
|
262
|
+
alike; under the limit it still answers `null`. This flow counts on its own:
|
|
263
|
+
a loop on `magicLink.request` never shuts it. Answer it with a 429 and
|
|
264
|
+
`Retry-After`:
|
|
265
|
+
|
|
266
|
+
```ts
|
|
267
|
+
import { MailThrottledError } from '@nxgt/janus';
|
|
268
|
+
|
|
269
|
+
try {
|
|
270
|
+
const issued = await auth.signInCode.request(email);
|
|
271
|
+
// …send the e-mail from `issued`, whether or not there is one to send
|
|
272
|
+
} catch (error) {
|
|
273
|
+
if (error instanceof MailThrottledError) {
|
|
274
|
+
return Response.json(
|
|
275
|
+
{
|
|
276
|
+
error: 'too many e-mails — the last one we sent still works; use it, or wait',
|
|
277
|
+
retryAfter: error.retryAfter,
|
|
278
|
+
},
|
|
279
|
+
{ status: 429, headers: { 'retry-after': String(error.retryAfter) } },
|
|
280
|
+
);
|
|
281
|
+
}
|
|
282
|
+
throw error;
|
|
283
|
+
}
|
|
284
|
+
```
|
|
285
|
+
|
|
286
|
+
With several user types the message is prefixed with the type, as in
|
|
287
|
+
`patient.signInCode.request: too many e-mails asked for this address — the last
|
|
288
|
+
one sent still works; use it, or wait for the next window`. Somebody who knows
|
|
289
|
+
an address cannot shut its e-mailed sign-in: the last code they caused to be
|
|
290
|
+
sent still works, and they can only fill the inbox, five per window. Tell the
|
|
291
|
+
visitor to use the last e-mail they received, or to wait `retryAfter` seconds.
|
|
292
|
+
`janus` never sees IP addresses, so one client asking for many addresses is
|
|
293
|
+
not counted: add a per-IP ceiling with `@nxgt/redis` rate limits
|
|
294
|
+
([example](email-flows.md#requests-that-send-e-mail-are-throttled)).
|
|
259
295
|
|
|
260
296
|
### Lifetime
|
|
261
297
|
|
|
@@ -267,6 +303,9 @@ arrive and be read; a longer window is a longer life for a stolen challenge.
|
|
|
267
303
|
janus({ ..., tokens: { signInCode: '15m' } });
|
|
268
304
|
```
|
|
269
305
|
|
|
306
|
+
Keep the mail window (`mail.throttle.window`, `'10m'` by default) no longer
|
|
307
|
+
than the lifetime: with `tokens.signInCode: '5m'` the last code can expire
|
|
308
|
+
before the window ends, and the visitor has none live until it does.
|
|
270
309
|
### The e-mail is verified
|
|
271
310
|
|
|
272
311
|
A user whose `emailVerified` was `false` has it `true` once `confirm`
|
package/docs/guide/step-up.md
CHANGED
|
@@ -181,13 +181,44 @@ counted **per user**, five per 15-minute window, in the same count as
|
|
|
181
181
|
challenge still gets five guesses per window, not five per challenge. The
|
|
182
182
|
app's code accepted is spent, as at a sign-in: it confirms nothing else.
|
|
183
183
|
|
|
184
|
-
|
|
185
|
-
five guesses per challenge, and a new challenge takes only a new
|
|
186
|
-
A stolen session could ask again and again — each request also
|
|
187
|
-
e-mail and cancels the code before it.
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
the
|
|
184
|
+
**`request` is throttled per user, when it e-mails.** An e-mailed code has no
|
|
185
|
+
such window: five guesses per challenge, and a new challenge takes only a new
|
|
186
|
+
`request`. A stolen session could ask again and again — each request also
|
|
187
|
+
sends an e-mail and cancels the code before it. So `stepUp.request` counts
|
|
188
|
+
the user, five per 10-minute window (`mail: { throttle }`), and past it throws
|
|
189
|
+
`MailThrottledError` (`MAIL_THROTTLED`, with `userId`) and issues nothing: a
|
|
190
|
+
refused request spends nothing, so the code last sent still works until it
|
|
191
|
+
expires. Tell the user to use the last e-mail they received, or to wait
|
|
192
|
+
`retryAfter` seconds.
|
|
193
|
+
It counts **only** `via: 'email'`: a step-up confirmed with the app
|
|
194
|
+
(`via: 'secondFactor'`) sends nothing and is not counted.
|
|
195
|
+
|
|
196
|
+
It is per user rather than per address because the application calls it for a
|
|
197
|
+
user it knows: nobody without their session can spend the count, so an
|
|
198
|
+
unauthenticated loop on their address never blocks their own step-up, and a
|
|
199
|
+
user who changes e-mail buys no extra sends. Answer it with a 429 and
|
|
200
|
+
`Retry-After`:
|
|
201
|
+
|
|
202
|
+
```ts
|
|
203
|
+
import { MailThrottledError } from '@nxgt/janus';
|
|
204
|
+
|
|
205
|
+
try {
|
|
206
|
+
const issued = await auth.stepUp.request(current.user);
|
|
207
|
+
// …send the e-mail from `issued`, whether or not there is one to send
|
|
208
|
+
} catch (error) {
|
|
209
|
+
if (error instanceof MailThrottledError) {
|
|
210
|
+
return Response.json(
|
|
211
|
+
{ error: 'too many e-mails', retryAfter: error.retryAfter },
|
|
212
|
+
{ status: 429, headers: { 'retry-after': String(error.retryAfter) } },
|
|
213
|
+
);
|
|
214
|
+
}
|
|
215
|
+
throw error;
|
|
216
|
+
}
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
The `janus.stepUp.asked` event of `@nxgt/janus-telemetry` is the signal to
|
|
220
|
+
alert on; it also writes a `janus.mail.throttled` warning when the throttle
|
|
221
|
+
refuses. See [the mail throttle](email-flows.md#requests-that-send-e-mail-are-throttled).
|
|
191
222
|
|
|
192
223
|
A step-up and a sign-in code are **two kinds of token**: a sign-in code's
|
|
193
224
|
challenge confirms no action, and a step-up's code signs nobody in.
|
package/docs/guide/users.md
CHANGED
|
@@ -98,6 +98,7 @@ that cuts an emoji in half, say.
|
|
|
98
98
|
| `tokens.magicLink` | `Duration` | `'15m'` | How long an e-mailed sign-in link lives. See [sign-in links](magic-link.md) |
|
|
99
99
|
| `tokens.stepUp` | `Duration` | `'10m'` | How long a step-up's challenge — and its e-mailed code — lives. See [step-up](step-up.md) |
|
|
100
100
|
| `signIn.throttle` | `{ attempts?, window? }` or `false` | `{ attempts: 10, window: '15m' }` | Passwords one login may try per window, the right one included, before `signIn` answers `CREDENTIALS_INVALID` with `retryAfter` until the window ends. `false` counts nothing. See [passwords](passwords.md#password-guessing-is-throttled) |
|
|
101
|
+
| `mail.throttle` | `{ attempts?, window? }` or `false` | `{ attempts: 5, window: '10m' }` | E-mails one address (`magicLink`, `signInCode`, `resetPassword` requests) or one user (`verifyEmail.send`, an e-mailed `stepUp.request`) may ask for per flow and window, before the request throws `MAIL_THROTTLED` with `retryAfter` and issues nothing (the last link or code sent still works). Keep `window` no longer than the shortest `tokens.*` lifetime you use. `false` counts nothing. See [e-mail flows](email-flows.md#requests-that-send-e-mail-are-throttled) |
|
|
101
102
|
| `secondFactor` | `{ issuer, keys, challenge? }` | none | A TOTP second factor for every type with a password. Changes what `signIn` answers — see [the second factor](second-factor.md#configuration) |
|
|
102
103
|
| `devices` | `{ keys }` | none | The keys device tokens are signed with, to tell a sign-in from a new device. Without it, no sign-in may be given a `device` — see [devices](devices.md#wiring) |
|
|
103
104
|
| `events` | `UserEventListener` | none | Called with every user event — `user.created`, `user.deleted`, … — after the write, awaited. See [user events](events.md) |
|
package/docs/roadmap.md
CHANGED
|
@@ -16,7 +16,8 @@ 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
|
|
|
22
23
|
- **More official adapters** — the ports are cut where atomicity is not
|
|
@@ -70,6 +71,18 @@ Nothing yet.
|
|
|
70
71
|
The last ten, newest first, each with the version it came in. Everything
|
|
71
72
|
before is in the [CHANGELOG](../CHANGELOG.md).
|
|
72
73
|
|
|
74
|
+
- **Requests that send e-mail are throttled, v0.18.0** —
|
|
75
|
+
`janus({ mail: { throttle: { attempts, window } } })`, five per ten
|
|
76
|
+
minutes by default and `false` to turn it off. `magicLink.request`,
|
|
77
|
+
`signInCode.request` and `resetPassword.request` count per address;
|
|
78
|
+
`verifyEmail.send` and an e-mailed `stepUp.request` count per user; each
|
|
79
|
+
flow on its own. Past the limit a request throws `MailThrottledError`
|
|
80
|
+
(`MAIL_THROTTLED`, 429) with `retryAfter` and issues, spends and rotates
|
|
81
|
+
nothing: the last link or code sent still works, so a loop on an address
|
|
82
|
+
cannot lock its owner out.
|
|
83
|
+
The window is no longer than the shortest default token lifetime. The counts
|
|
84
|
+
live in the tokens store, with no port change. `MailConfig` and
|
|
85
|
+
`MailThrottleConfig` are exported.
|
|
73
86
|
- **A sign-in from a new device, v0.17.0** — `janus({ devices: { keys } })`,
|
|
74
87
|
and `{ device }` as the last argument of `signUp`, `signIn`,
|
|
75
88
|
`secondFactor.confirm`, `secondFactor.recover`, `signInCode.confirm` and
|
package/docs/troubleshooting.md
CHANGED
|
@@ -42,6 +42,8 @@ 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**
|
|
@@ -54,6 +56,7 @@ How the messages are shaped:
|
|
|
54
56
|
- [`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
57
|
- [`CREDENTIALS_INVALID` — `<call>: the login and the password do not match`](#credentials_invalid--call-the-login-and-the-password-do-not-match)
|
|
56
58
|
- [`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)
|
|
59
|
+
- [`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
60
|
- [`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
61
|
- [`HASH_UNSUPPORTED` — `<call>: no wired verifier claims the prefix "<prefix>"`](#hash_unsupported--call-no-wired-verifier-claims-the-prefix-prefix)
|
|
59
62
|
- [`scryptHasher: the stored hash has the $scrypt$ prefix and not its format`](#scrypthasher-the-stored-hash-has-the-scrypt-prefix-and-not-its-format)
|
|
@@ -465,6 +468,39 @@ zero`, and a `janus: signIn.throttle.window` that is not a duration.
|
|
|
465
468
|
**Fix:** `signIn: { throttle: { attempts: 10, window: '15m' } }` — the
|
|
466
469
|
defaults — or `signIn: { throttle: false }` to count nothing.
|
|
467
470
|
|
|
471
|
+
### `janus: mail.throttle` wiring messages
|
|
472
|
+
|
|
473
|
+
`TypeError`, at `janus()`: `janus: mail must be an object — { throttle }`,
|
|
474
|
+
`janus: mail.throttle must be { attempts, window }, or false to count
|
|
475
|
+
nothing`, `janus: mail.throttle.attempts must be a whole number above zero`,
|
|
476
|
+
and a `janus: mail.throttle.window` that is not a duration.
|
|
477
|
+
**Fix:** `mail: { throttle: { attempts: 5, window: '10m' } }` — the defaults —
|
|
478
|
+
or `mail: { throttle: false }` to count nothing.
|
|
479
|
+
|
|
480
|
+
### `janus: tokens.<flow> is 5m, shorter than mail.throttle.window, 10m …`
|
|
481
|
+
|
|
482
|
+
A warning, not an error: `janus()` returns, and emits through
|
|
483
|
+
`process.emitWarning` with the code `JANUS_THROTTLE_WINDOW`, once per call. The
|
|
484
|
+
text, here for `tokens.signInCode: '5m'` with the default window:
|
|
485
|
+
|
|
486
|
+
```
|
|
487
|
+
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.
|
|
488
|
+
```
|
|
489
|
+
|
|
490
|
+
With several flows shorter than the window, one warning lists each
|
|
491
|
+
(`tokens.magicLink is 2m, … ; tokens.stepUp is 30s, …`). **Why:** a request the
|
|
492
|
+
mail throttle refuses spends nothing, so the last link or code sent still works,
|
|
493
|
+
but only until it expires; with a lifetime shorter than the window it can lapse
|
|
494
|
+
while the visitor is still refused. **Fix:** make the window no longer than the
|
|
495
|
+
shortest lifetime, or raise the lifetime:
|
|
496
|
+
|
|
497
|
+
```ts
|
|
498
|
+
janus({ user, store, tokens: { signInCode: '5m' }, mail: { throttle: { window: '5m' } } });
|
|
499
|
+
```
|
|
500
|
+
|
|
501
|
+
A runtime without `process.emitWarning` stays silent. Silence it with
|
|
502
|
+
`node --disable-warning=JANUS_THROTTLE_WINDOW`.
|
|
503
|
+
|
|
468
504
|
### Other `janus:` wiring messages
|
|
469
505
|
|
|
470
506
|
| Message | Fix |
|
|
@@ -641,6 +677,52 @@ if (error instanceof JanusError && error.retryAfter !== undefined) {
|
|
|
641
677
|
janus({ ..., signIn: { throttle: { attempts: 20, window: '15m' } } });
|
|
642
678
|
```
|
|
643
679
|
|
|
680
|
+
### `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`
|
|
681
|
+
|
|
682
|
+
`MailThrottledError`, with `retryAfter` and `userType`. The per-user flows say
|
|
683
|
+
`… too many e-mails asked for this user — the last one sent still works; use
|
|
684
|
+
it, or wait for the next window` and carry `userId`.
|
|
685
|
+
|
|
686
|
+
**When:** `magicLink.request`, `signInCode.request` or
|
|
687
|
+
`resetPassword.request` (per address), or `verifyEmail.send` and an e-mailed
|
|
688
|
+
`stepUp.request` (per user), past five requests of that flow in the current
|
|
689
|
+
10-minute window — whether or not anybody holds the address. `<call>` is the
|
|
690
|
+
call, such as `magicLink.request` or `patient.signInCode.request`.
|
|
691
|
+
**Why:** requests that send e-mail are throttled, on by default, so a loop
|
|
692
|
+
cannot fill an inbox or cancel its owner's link or code. Each flow counts on
|
|
693
|
+
its own, in fixed windows of the clock, by the store. **A refused request
|
|
694
|
+
spends, invalidates and rotates nothing**: no token, code or challenge is
|
|
695
|
+
issued, and the last link or code sent — by anyone's request, an attacker's
|
|
696
|
+
included — still confirms until it expires, so a loop on a victim's address
|
|
697
|
+
cannot lock them out. Nothing locks: the next window answers again. In tests with `fixedClock`, the window never
|
|
698
|
+
ends.
|
|
699
|
+
**Fix:** answer 429 with a `Retry-After` header, and tell the visitor to use
|
|
700
|
+
the last e-mail they received (it still works), or to wait `retryAfter`
|
|
701
|
+
seconds: "Check your inbox: the last e-mail we sent still works. You can ask for
|
|
702
|
+
a new one in N minutes." It reveals nothing about accounts, so it is safe to
|
|
703
|
+
show.
|
|
704
|
+
In tests, advance the clock past `retryAfter`, or wire `mail: { throttle:
|
|
705
|
+
false }`. If nobody you know asked five times, somebody is looping on the
|
|
706
|
+
route: `janus` never sees IP addresses, so limit it per client, which the
|
|
707
|
+
throttle does not — with `@nxgt/redis` rate limits (`defineRateLimit`,
|
|
708
|
+
`bindRateLimit`; `enforce()` throws `GuardError` `RATE_LIMITED` whose
|
|
709
|
+
`retryAfter` is in milliseconds), as in the
|
|
710
|
+
[e-mail flows guide](guide/email-flows.md#requests-that-send-e-mail-are-throttled).
|
|
711
|
+
If you set a token lifetime shorter than the window (`tokens.signInCode: '5m'`
|
|
712
|
+
with the default), the last code can expire before the window ends: keep
|
|
713
|
+
`mail.throttle.window` no longer than the shortest `tokens.*` you use.
|
|
714
|
+
|
|
715
|
+
```ts
|
|
716
|
+
import { MailThrottledError } from '@nxgt/janus';
|
|
717
|
+
|
|
718
|
+
if (error instanceof MailThrottledError) {
|
|
719
|
+
const headers = { 'retry-after': String(error.retryAfter) };
|
|
720
|
+
return Response.json({ code: error.code, retryAfter: error.retryAfter }, { status: 429, headers });
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
janus({ ..., mail: { throttle: { attempts: 10, window: '10m' } } });
|
|
724
|
+
```
|
|
725
|
+
|
|
644
726
|
### `STORE_FAILED` — `<call>: the store dropped the attempts it had just stored`
|
|
645
727
|
|
|
646
728
|
`StoreFailure`, with `slot: 'tokens'` and `operation: 'countAttempt'`.
|
|
@@ -1153,7 +1235,9 @@ A `TypeError`.
|
|
|
1153
1235
|
|
|
1154
1236
|
The messages start with `signInCode.confirm` — prefixed by the type with
|
|
1155
1237
|
several user types: `patient.signInCode.confirm: …`. `signInCode.request`
|
|
1156
|
-
throws nothing but `STORE_FAILED
|
|
1238
|
+
throws nothing but `STORE_FAILED` and
|
|
1239
|
+
[`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):
|
|
1240
|
+
an e-mail it cannot sign in is `null`.
|
|
1157
1241
|
`signInCode.confirm` also rejects with
|
|
1158
1242
|
[`CODE_INVALID`](#code_invalid--call-the-code-does-not-match-or-was-already-used),
|
|
1159
1243
|
[`TOKEN_UNKNOWN`, `TOKEN_SPENT`, `TOKEN_EXPIRED`](#token_unknown-token_spent-token_expired),
|
|
@@ -1236,8 +1320,9 @@ janus({
|
|
|
1236
1320
|
|
|
1237
1321
|
`magicLink.confirm`'s messages start with `magicLink.confirm` — prefixed by
|
|
1238
1322
|
the type with several user types: `patient.magicLink.confirm: …`.
|
|
1239
|
-
`magicLink.request` throws nothing but `STORE_FAILED
|
|
1240
|
-
|
|
1323
|
+
`magicLink.request` throws nothing but `STORE_FAILED` and
|
|
1324
|
+
[`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):
|
|
1325
|
+
an e-mail it cannot sign in is `null`. `magicLink.confirm` rejects with
|
|
1241
1326
|
[`TOKEN_UNKNOWN`, `TOKEN_SPENT`, `TOKEN_EXPIRED`](#token_unknown-token_spent-token_expired),
|
|
1242
1327
|
[`TOKEN_STALE`](#token_stale--call-the-token-was-sent-to-an-e-mail-the-user-no-longer-has),
|
|
1243
1328
|
[`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
|
|
File without changes
|