@nxgt/janus 0.13.0 → 0.14.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.
Files changed (66) hide show
  1. package/README.md +56 -15
  2. package/dist/auth/config/index.d.ts +1 -1
  3. package/dist/auth/config/index.d.ts.map +1 -1
  4. package/dist/auth/config/janus-config.d.ts +23 -0
  5. package/dist/auth/config/janus-config.d.ts.map +1 -1
  6. package/dist/auth/config/resolve-config.d.ts.map +1 -1
  7. package/dist/auth/config/resolve-sign-in.d.ts +5 -0
  8. package/dist/auth/config/resolve-sign-in.d.ts.map +1 -0
  9. package/dist/auth/config/resolved-config.d.ts +5 -0
  10. package/dist/auth/config/resolved-config.d.ts.map +1 -1
  11. package/dist/auth/derived-keys.d.ts +9 -0
  12. package/dist/auth/derived-keys.d.ts.map +1 -0
  13. package/dist/auth/events.d.ts +19 -4
  14. package/dist/auth/events.d.ts.map +1 -1
  15. package/dist/auth/index.d.ts +1 -1
  16. package/dist/auth/index.d.ts.map +1 -1
  17. package/dist/auth/one-time/index.d.ts +3 -1
  18. package/dist/auth/one-time/index.d.ts.map +1 -1
  19. package/dist/auth/one-time/window.d.ts +50 -0
  20. package/dist/auth/one-time/window.d.ts.map +1 -0
  21. package/dist/auth/password-written.d.ts +7 -0
  22. package/dist/auth/password-written.d.ts.map +1 -1
  23. package/dist/auth/second-factor/app-code-attempts.d.ts +5 -8
  24. package/dist/auth/second-factor/app-code-attempts.d.ts.map +1 -1
  25. package/dist/auth/second-factor/challenge.d.ts.map +1 -1
  26. package/dist/auth/second-factor/recovery-codes.d.ts.map +1 -1
  27. package/dist/auth/second-factor/recovery.d.ts.map +1 -1
  28. package/dist/auth/sign-in-attempts.d.ts +43 -0
  29. package/dist/auth/sign-in-attempts.d.ts.map +1 -0
  30. package/dist/auth/types/password.d.ts +8 -0
  31. package/dist/auth/types/password.d.ts.map +1 -1
  32. package/dist/auth/users/fields-patch.d.ts +8 -2
  33. package/dist/auth/users/fields-patch.d.ts.map +1 -1
  34. package/dist/auth/users/record-flows.d.ts.map +1 -1
  35. package/dist/auth/users/sign-in.d.ts +2 -2
  36. package/dist/auth/users/sign-in.d.ts.map +1 -1
  37. package/dist/auth/users/update.d.ts +12 -0
  38. package/dist/auth/users/update.d.ts.map +1 -0
  39. package/dist/chunks/{index-km7ffctx.js → index-6c9fbtv2.js} +2 -2
  40. package/dist/chunks/{index-rd2rddvv.js → index-baazjyvc.js} +2 -2
  41. package/dist/chunks/{index-j2gd8fry.js → index-hnwggyxs.js} +3 -2
  42. package/dist/chunks/{index-j2gd8fry.js.map → index-hnwggyxs.js.map} +3 -3
  43. package/dist/chunks/{index-jp2kka6d.js → index-kj3st57z.js} +2 -2
  44. package/dist/conformance/index.js +3 -3
  45. package/dist/errors/janus-error/base.d.ts +1 -0
  46. package/dist/errors/janus-error/base.d.ts.map +1 -1
  47. package/dist/errors/janus-error/codes.d.ts +3 -1
  48. package/dist/errors/janus-error/codes.d.ts.map +1 -1
  49. package/dist/errors/janus-error/options.d.ts +9 -2
  50. package/dist/errors/janus-error/options.d.ts.map +1 -1
  51. package/dist/index.js +226 -86
  52. package/dist/index.js.map +20 -15
  53. package/dist/permissions/index.js +3 -3
  54. package/docs/README.md +2 -2
  55. package/docs/guide/errors.md +10 -2
  56. package/docs/guide/events.md +73 -12
  57. package/docs/guide/passwords.md +115 -17
  58. package/docs/guide/second-factor.md +4 -2
  59. package/docs/guide/users.md +11 -6
  60. package/docs/guide/vocabulary.md +1 -1
  61. package/docs/roadmap.md +27 -18
  62. package/docs/troubleshooting.md +55 -2
  63. package/package.json +1 -1
  64. /package/dist/chunks/{index-km7ffctx.js.map → index-6c9fbtv2.js.map} +0 -0
  65. /package/dist/chunks/{index-rd2rddvv.js.map → index-baazjyvc.js.map} +0 -0
  66. /package/dist/chunks/{index-jp2kka6d.js.map → index-kj3st57z.js.map} +0 -0
@@ -40,6 +40,7 @@ How the messages are shaped:
40
40
  - [`janus: secondFactor.keys: the key "<id>" is not 32 bytes in base64 …`](#janus-secondfactorkeys-the-key-id-is-not-32-bytes-in-base64--make-one-with-openssl-rand--base64-32)
41
41
  - [`"hasSecondFactor" is a field janus sets itself; rename it`](#hassecondfactor-is-a-field-janus-sets-itself-rename-it)
42
42
  - [`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)
43
+ - [`janus: signIn.throttle …` wiring messages](#janus-signinthrottle-wiring-messages)
43
44
  - [Other `janus:` wiring messages](#other-janus-wiring-messages)
44
45
 
45
46
  **Users, sessions and tokens**
@@ -51,6 +52,8 @@ How the messages are shaped:
51
52
  - [`USER_INVALID` — `<call>: the fields do not match the <type> schema …`](#user_invalid--call-the-fields-do-not-match-the-type-schema-n-issues-at-paths)
52
53
  - [`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)
53
54
  - [`CREDENTIALS_INVALID` — `<call>: the login and the password do not match`](#credentials_invalid--call-the-login-and-the-password-do-not-match)
55
+ - [`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)
56
+ - [`STORE_FAILED` — `<call>: the store dropped the attempts it had just stored`](#store_failed--call-the-store-dropped-the-attempts-it-had-just-stored)
54
57
  - [`HASH_UNSUPPORTED` — `<call>: no wired verifier claims the prefix "<prefix>"`](#hash_unsupported--call-no-wired-verifier-claims-the-prefix-prefix)
55
58
  - [`scryptHasher: the stored hash has the $scrypt$ prefix and not its format`](#scrypthasher-the-stored-hash-has-the-scrypt-prefix-and-not-its-format)
56
59
  - [`USER_INACTIVE` — `<call>: the user is inactive`](#user_inactive--call-the-user-is-inactive)
@@ -328,7 +331,7 @@ Also `janus: "<name>" cannot name a user type — janus() answers a method of th
328
331
 
329
332
  ### `janus: session.lifespan: "<value>" is not a duration; write a number followed by ms, s, m, h or d — for example "15m" or "720h"`
330
333
 
331
- The same for `session.renewAfter`, `tokens.verifyEmail`, `tokens.resetPassword`, `tokens.signInCode`, `tokens.stepUp` and `secondFactor.challenge` — and, at call time rather than in `janus()`, `assertFresh: maxAge: "<value>" is not a duration; …`, from a `maxAge` written wrong. Also `<option>: a duration must be above zero` and `<option>: a duration in milliseconds must be a finite number above zero`.
334
+ The same for `session.renewAfter`, `tokens.verifyEmail`, `tokens.resetPassword`, `tokens.signInCode`, `tokens.stepUp`, `secondFactor.challenge` and `signIn.throttle.window` — and, at call time rather than in `janus()`, `assertFresh: maxAge: "<value>" is not a duration; …`, from a `maxAge` written wrong. Also `<option>: a duration must be above zero` and `<option>: a duration in milliseconds must be a finite number above zero`.
332
335
 
333
336
  **When:** `janus({...})`.
334
337
  **Why:** a duration is a number of milliseconds, or a number followed by one unit. `'30 m'` compiles — TypeScript's `${number}` accepts the space — and is refused here.
@@ -403,6 +406,15 @@ janus({
403
406
  });
404
407
  ```
405
408
 
409
+ ### `janus: signIn.throttle` wiring messages
410
+
411
+ `TypeError`, at `janus()`: `janus: signIn must be an object — { throttle }`,
412
+ `janus: signIn.throttle must be { attempts, window }, or false to count
413
+ nothing`, `janus: signIn.throttle.attempts must be a whole number above
414
+ zero`, and a `janus: signIn.throttle.window` that is not a duration.
415
+ **Fix:** `signIn: { throttle: { attempts: 10, window: '15m' } }` — the
416
+ defaults — or `signIn: { throttle: false }` to count nothing.
417
+
406
418
  ### Other `janus:` wiring messages
407
419
 
408
420
  | Message | Fix |
@@ -532,7 +544,7 @@ if (error instanceof UserInvalidError) {
532
544
  Also `changePassword: the current password does not match`.
533
545
 
534
546
  **When:** `signIn`, `changePassword`.
535
- **Why:** no user holds the login, the user has no password, or the password is wrong — **one code for the three**. Also a sign-in that verified a password written over while it ran (`reason: 'wrongPassword'`): its session is revoked, or its challenge spent, before the refusal. `error.reason` (`unknownLogin`, `noPassword`, `wrongPassword`) tells them apart for your logs and your rate limiter. A login holding a NUL character or a lone surrogate is `unknownLogin`: no user can hold one.
547
+ **Why:** no user holds the login, the user has no password, or the password is wrong — **one code for the three**. Also a sign-in that verified a password written over while it ran (`reason: 'wrongPassword'`): its session is revoked, or its challenge spent, before the refusal. `error.reason` (`unknownLogin`, `noPassword`, `wrongPassword`) tells them apart for your logs and your rate limiter; a login past its attempts has its own message, below. A login holding a NUL character or a lone surrogate is `unknownLogin`: no user can hold one.
536
548
  **Fix:** answer 401 with the same body whatever the reason:
537
549
 
538
550
  ```ts
@@ -540,6 +552,47 @@ Also `changePassword: the current password does not match`.
540
552
  return new Response('Wrong e-mail or password', { status: 401 });
541
553
  ```
542
554
 
555
+ ### `CREDENTIALS_INVALID` — `<call>: too many passwords tried at this login — wait for the next window`
556
+
557
+ `CredentialError`, with `reason: 'throttled'` and `retryAfter`.
558
+
559
+ **When:** `signIn`, after ten passwords were tried at this login in the
560
+ current 15-minute window — whatever this password is, **the right one
561
+ included**, and whether or not anybody holds the login. Nothing is compared.
562
+ **Why:** `signIn` throttles password guessing per login, on by default:
563
+ counted by the store, in fixed windows of the clock, shared by every process
564
+ over the same store. Nothing locks: the next window signs in, and a sign-in
565
+ that succeeds starts the count again. In tests with `fixedClock`, the window
566
+ never ends: advance the clock past `retryAfter`, or wire
567
+ `signIn: { throttle: false }`.
568
+ **Fix:** tell the visitor to wait `retryAfter` seconds — not that the
569
+ password is wrong — and answer 401, as `@nxgt/janus-hono`'s `janusErrors()`
570
+ does, with a `Retry-After` header. If nobody you know tried ten passwords,
571
+ somebody is guessing: look for `janus.signIn.throttled` in your logs. To
572
+ change the limit or turn it off:
573
+
574
+ ```ts
575
+ if (error instanceof JanusError && error.retryAfter !== undefined) {
576
+ const headers = { 'retry-after': String(error.retryAfter) };
577
+ return Response.json({ code: error.code, retryAfter: error.retryAfter }, { status: 401, headers });
578
+ }
579
+
580
+ janus({ ..., signIn: { throttle: { attempts: 20, window: '15m' } } });
581
+ ```
582
+
583
+ ### `STORE_FAILED` — `<call>: the store dropped the attempts it had just stored`
584
+
585
+ `StoreFailure`, with `slot: 'tokens'` and `operation: 'countAttempt'`.
586
+
587
+ **When:** `signIn`, when the tokens store answers `countAttempt` with `null`
588
+ for the count it stored an instant before.
589
+ **Why:** the store lost it — a Redis evicting keys under memory pressure, a
590
+ flush between the two calls, or an adapter whose `countAttempt` does not
591
+ find what `insertToken` wrote. The throttle cannot count, so the sign-in
592
+ fails rather than pass uncounted.
593
+ **Fix:** answer 503. Run the conformance suite against the adapter, and give
594
+ a Redis tokens store `maxmemory-policy noeviction`.
595
+
543
596
  ### `HASH_UNSUPPORTED` — `<call>: no wired verifier claims the prefix "<prefix>"`
544
597
 
545
598
  **When:** `signIn` or `changePassword`, for a user whose stored hash was written by another system — typically after an import.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nxgt/janus",
3
- "version": "0.13.0",
3
+ "version": "0.14.0",
4
4
  "description": "Embeddable, type-safe identities and permissions: bring your own database",
5
5
  "license": "MIT",
6
6
  "type": "module",