@nxgt/janus 0.19.0 → 0.20.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 (48) hide show
  1. package/README.md +117 -7
  2. package/dist/auth/email-flows/reset-password.d.ts.map +1 -1
  3. package/dist/auth/events.d.ts +3 -3
  4. package/dist/auth/index.d.ts +1 -1
  5. package/dist/auth/index.d.ts.map +1 -1
  6. package/dist/auth/magic-link.d.ts +3 -0
  7. package/dist/auth/magic-link.d.ts.map +1 -1
  8. package/dist/auth/one-time/issue.d.ts +6 -0
  9. package/dist/auth/one-time/issue.d.ts.map +1 -1
  10. package/dist/auth/prepared.d.ts +21 -0
  11. package/dist/auth/prepared.d.ts.map +1 -0
  12. package/dist/auth/proven-sign-in.d.ts +22 -0
  13. package/dist/auth/proven-sign-in.d.ts.map +1 -0
  14. package/dist/auth/sign-in-code.d.ts +4 -0
  15. package/dist/auth/sign-in-code.d.ts.map +1 -1
  16. package/dist/auth/types/email-flows.d.ts +18 -0
  17. package/dist/auth/types/email-flows.d.ts.map +1 -1
  18. package/dist/auth/types/index.d.ts +2 -0
  19. package/dist/auth/types/index.d.ts.map +1 -1
  20. package/dist/auth/types/janus.d.ts +7 -5
  21. package/dist/auth/types/janus.d.ts.map +1 -1
  22. package/dist/auth/types/magic-link.d.ts +11 -0
  23. package/dist/auth/types/magic-link.d.ts.map +1 -1
  24. package/dist/auth/types/prepared.d.ts +41 -0
  25. package/dist/auth/types/prepared.d.ts.map +1 -0
  26. package/dist/auth/types/proven-sign-in.d.ts +43 -0
  27. package/dist/auth/types/proven-sign-in.d.ts.map +1 -0
  28. package/dist/auth/types/sign-in.d.ts +2 -2
  29. package/dist/auth/users/any-type-api.d.ts +2 -2
  30. package/dist/auth/users/any-type-api.d.ts.map +1 -1
  31. package/dist/auth/users/type-api.d.ts +2 -2
  32. package/dist/auth/users/type-api.d.ts.map +1 -1
  33. package/dist/index.js +92 -17
  34. package/dist/index.js.map +11 -9
  35. package/docs/README.md +1 -1
  36. package/docs/guide/devices.md +4 -0
  37. package/docs/guide/email-flows.md +105 -0
  38. package/docs/guide/errors.md +2 -2
  39. package/docs/guide/events.md +2 -2
  40. package/docs/guide/magic-link.md +49 -2
  41. package/docs/guide/passwords.md +5 -2
  42. package/docs/guide/second-factor.md +1 -1
  43. package/docs/guide/sessions.md +94 -1
  44. package/docs/guide/sign-in-code.md +54 -2
  45. package/docs/guide/users.md +1 -0
  46. package/docs/roadmap.md +17 -26
  47. package/docs/troubleshooting.md +81 -5
  48. package/package.json +1 -1
@@ -9,8 +9,9 @@ How the messages are shaped:
9
9
  `resetPassword.confirm: …`, `can: …`. With several user types, that call is
10
10
  prefixed by the type: `staff.signIn: …`. Below, `<call>` stands for it.
11
11
  - **A `TypeError` is a wiring mistake**: it comes from how the application was
12
- put together — a configuration, a store, a model — and never from a request.
13
- Fix the code; no handler should answer one.
12
+ put together — a configuration, a store, a model, a prepared request sent
13
+ twice — and never from a request. Fix the code; no handler should answer
14
+ one.
14
15
  - **A `JanusError` is a refusal at call time**, on a value that could have come
15
16
  from a request. It carries a `code` you can `switch` on; the heading of each
16
17
  entry below names it.
@@ -60,6 +61,9 @@ How the messages are shaped:
60
61
  - [`CREDENTIALS_INVALID` — `<call>: the login and the password do not match`](#credentials_invalid--call-the-login-and-the-password-do-not-match)
61
62
  - [`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
63
  - [`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)
64
+ - [Accepting an invitation answers `MAIL_THROTTLED`](#accepting-an-invitation-answers-mail_throttled)
65
+ - [`MAIL_THROTTLED` never reaches the visitor: `request` runs in the background](#mail_throttled-never-reaches-the-visitor-request-runs-in-the-background)
66
+ - [`<flow>.prepare(…).send: already called — …`](#flowpreparesend-already-called--a-prepared-request-sends-once-call-flowprepare-again-for-another)
63
67
  - [`STORE_FAILED` — `<call>: the store dropped the attempts it had just stored`](#store_failed--call-the-store-dropped-the-attempts-it-had-just-stored)
64
68
  - [`HASH_UNSUPPORTED` — `<call>: no wired verifier claims the prefix "<prefix>"`](#hash_unsupported--call-no-wired-verifier-claims-the-prefix-prefix)
65
69
  - [`scryptHasher: the stored hash has the $scrypt$ prefix and not its format`](#scrypthasher-the-stored-hash-has-the-scrypt-prefix-and-not-its-format)
@@ -582,10 +586,10 @@ const member = byId.get(row.staffId); // undefined: nobody of this type holds it
582
586
 
583
587
  ### `NOT_FOUND` — `<call>: no <type> has this id`
584
588
 
585
- `NotFoundError`, from `get`, `getUser`, `update`, `setActive`, `setPassword`, `verifyEmail.send`, `secondFactor.recoveryCodesLeft`. Also `<call>: the user has no e-mail`.
589
+ `NotFoundError`, from `get`, `getUser`, `update`, `setActive`, `setPassword`, `signInProven`, `verifyEmail.send`, `secondFactor.recoveryCodesLeft`. Also `<call>: the user has no e-mail`.
586
590
 
587
591
  **When:** the id names nobody, names a user of another type (`auth.staff.get(patientId)`), or is not an id at all.
588
- **Why:** `get*` calls turn an absence into `NOT_FOUND`; `find*` calls answer `null` instead.
592
+ **Why:** `get*` calls turn an absence into `NOT_FOUND`; `find*` calls answer `null` instead. `signInProven` is a `get`: a sign-in that found nobody throws, and on it `NOT_FOUND` means the proof you verified names a user deleted since, or you passed the id of another type's user — or a value that is not the proof's id at all.
589
593
  **Fix:** answer 404, or use `find` when absence is an ordinary outcome:
590
594
 
591
595
  ```ts
@@ -760,6 +764,78 @@ if (error instanceof MailThrottledError) {
760
764
  janus({ ..., mail: { throttle: { attempts: 10, window: '10m' } } });
761
765
  ```
762
766
 
767
+ ### Accepting an invitation answers `MAIL_THROTTLED`
768
+
769
+ **When:** the application signs an invitee in by calling
770
+ `magicLink.request(email)` and confirming the token at once, and somebody
771
+ asked for links to that address five times in the window — the default mail
772
+ throttle. The acceptance answers 429, and the message says the last link
773
+ sent still works, though the invitee was sent none.
774
+ **Why:** `request` is counted per address, whoever asks: that is what stops a
775
+ loop from filling an inbox. A sign-in your server already proved — the
776
+ invitation token it issued — does not need an e-mailed link, and should not
777
+ share its count.
778
+ **Fix:** call `signInProven` with the id from the invitation's record, after
779
+ verifying the token yourself. It counts nothing, sends nothing, spends no
780
+ link already sent, and answers what `signIn` answers.
781
+
782
+ ```ts
783
+ const invitation = await invitations.spend(hash(token), new Date()); // yours: once, unexpired
784
+ if (invitation === null) return new Response(null, { status: 404 });
785
+ const result = await auth.signInProven(invitation.userId, { device: deviceOf(request) });
786
+ ```
787
+
788
+ Never pass it an id the request carries: it checks no credential. See
789
+ [sessions](guide/sessions.md#signing-in-on-your-own-proof--signinproven).
790
+
791
+ ### `MAIL_THROTTLED` never reaches the visitor: `request` runs in the background
792
+
793
+ **When:** the application runs `magicLink.request`, `signInCode.request` or
794
+ `resetPassword.request` off the visitor's request — a queue, a promise it
795
+ does not await — so that the time of the answer does not tell whether the
796
+ address has an account. Past the limit, the refusal is thrown there, and
797
+ nobody tells the visitor to use the last e-mail they received.
798
+ **Why:** `request` counts, then looks the address up and issues: its
799
+ `MAIL_THROTTLED` comes from the same call whose time must be hidden.
800
+ **Fix:** call `prepare(email)` in the visitor's request — validated and
801
+ counted, nobody looked up, the same store calls for any address — and
802
+ answer its `MAIL_THROTTLED` with 429; call `send()` on what it answered in the
803
+ background. `send()` counts nothing and answers what `request` would have.
804
+
805
+ ```ts
806
+ const pending = await auth.magicLink.prepare(email); // MAIL_THROTTLED here, in the visitor's request
807
+ void pending.send().then(mailIt).catch(reportError); // looked up and issued off it
808
+ return new Response(null, { status: 202 });
809
+ ```
810
+
811
+ See [requests in two steps](guide/email-flows.md#requests-in-two-steps).
812
+
813
+ ### `<flow>.prepare(…).send: already called — a prepared request sends once; call <flow>.prepare again for another`
814
+
815
+ A `TypeError`, such as `magicLink.prepare(…).send: already called — a
816
+ prepared request sends once; call magicLink.prepare again for another`, or
817
+ `patient.signInCode.prepare(…)…` with several user types.
818
+ **When:** `send()` was called a second time on what one `prepare` answered —
819
+ after it issued, after it answered `null`, after it threw (`STORE_FAILED`),
820
+ or while the first call was still running.
821
+ **Why:** `prepare` counted one request against the mail throttle, and that
822
+ count pays for one issue. A `send()` that could run again would issue
823
+ uncounted, so a loop could mail an address past the limit; nothing a caller
824
+ passes skips the count. It is a bug in the calling code, never a value from
825
+ a request, so it is a `TypeError` with no `code`.
826
+ **Fix:** call `send()` once per `prepare`. To retry — after a failure, in a
827
+ queue that retries — `prepare` again, which counts again (and, for a sign-in
828
+ code, mints another challenge: give the visitor the new one):
829
+
830
+ ```ts
831
+ async function sendLink(email: string): Promise<void> {
832
+ const pending = await auth.magicLink.prepare(email); // one count…
833
+ const issued = await pending.send(); // …one issue
834
+ if (issued !== null) await mailer.send(issued.email, linkTo(issued.token));
835
+ }
836
+ // a retry calls sendLink again: never pending.send() twice
837
+ ```
838
+
763
839
  ### `STORE_FAILED` — `<call>: the store dropped the attempts it had just stored`
764
840
 
765
841
  `StoreFailure`, with `slot: 'tokens'` and `operation: 'countAttempt'`.
@@ -791,7 +867,7 @@ janus({ ..., hasher: scryptHasher(), verifiers: [bcryptVerifier] }); // a Passwo
791
867
 
792
868
  ### `USER_INACTIVE` — `<call>: the user is inactive`
793
869
 
794
- **When:** `signIn`, with the **right** password, for a user set inactive. Also `secondFactor.confirm` and `secondFactor.recover`, for a user set inactive after `signIn` asked for a code, `signInCode.confirm`, for a user set inactive after the code was sent, and `magicLink.confirm`, for one set inactive after the link was sent.
870
+ **When:** `signIn`, with the **right** password, for a user set inactive. Also `secondFactor.confirm` and `secondFactor.recover`, for a user set inactive after `signIn` asked for a code, `signInCode.confirm`, for a user set inactive after the code was sent, and `magicLink.confirm`, for one set inactive after the link was sent. And `signInProven`, for an inactive user: the application's proof does not reactivate anybody.
795
871
  **Why:** an inactive user keeps their record and password, and every sign-in is refused. It is checked after the password, so only somebody who knows the password learns the user is inactive — and after the code, so only somebody who read the e-mail does: `signInCode.request` answers `null` for an inactive user, as for nobody. On either `confirm`, the challenge is spent: reactivating the user does not revive it. `magicLink.confirm` checks after spending the link, which is spent too, and `magicLink.request` answers `null` for an inactive user.
796
872
  **Fix:** answer 403, or reactivate, then sign in again: `await auth.setActive(user, true)`.
797
873
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nxgt/janus",
3
- "version": "0.19.0",
3
+ "version": "0.20.0",
4
4
  "description": "Embeddable, type-safe identities and permissions: bring your own database",
5
5
  "license": "MIT",
6
6
  "type": "module",