@nxgt/janus 0.5.0 → 0.6.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 +70 -15
- package/dist/auth/config.d.ts +3 -0
- package/dist/auth/config.d.ts.map +1 -1
- package/dist/auth/context.d.ts +6 -0
- package/dist/auth/context.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/one-time.d.ts +59 -6
- package/dist/auth/one-time.d.ts.map +1 -1
- package/dist/auth/second-factor/challenge.d.ts +0 -6
- package/dist/auth/second-factor/challenge.d.ts.map +1 -1
- package/dist/auth/second-factor/factor.d.ts +1 -4
- package/dist/auth/second-factor/factor.d.ts.map +1 -1
- package/dist/auth/second-factor/flows.d.ts +7 -3
- package/dist/auth/second-factor/flows.d.ts.map +1 -1
- package/dist/auth/second-factor/lifecycle.d.ts.map +1 -1
- package/dist/auth/sign-in-code.d.ts +17 -0
- package/dist/auth/sign-in-code.d.ts.map +1 -0
- package/dist/auth/types.d.ts +50 -1
- package/dist/auth/types.d.ts.map +1 -1
- package/dist/auth/users.d.ts +2 -2
- package/dist/auth/users.d.ts.map +1 -1
- package/dist/index.js +115 -35
- package/dist/index.js.map +12 -11
- package/docs/README.md +1 -0
- package/docs/guide/email-flows.md +4 -0
- package/docs/guide/errors.md +3 -3
- package/docs/guide/second-factor.md +4 -2
- package/docs/guide/sign-in-code.md +471 -0
- package/docs/guide/users.md +2 -1
- package/docs/guide/vocabulary.md +6 -6
- package/docs/roadmap.md +16 -9
- package/docs/troubleshooting.md +164 -10
- package/package.json +1 -1
package/docs/troubleshooting.md
CHANGED
|
@@ -74,6 +74,13 @@ How the messages are shaped:
|
|
|
74
74
|
- [`<call>: the <type> type does not sign in with a password, so it has no second factor`](#call-the-type-type-does-not-sign-in-with-a-password-so-it-has-no-second-factor)
|
|
75
75
|
- `TOKEN_*`, `USER_INACTIVE` and `VERSION_CONFLICT` from `secondFactor.confirm`: in their entries above.
|
|
76
76
|
|
|
77
|
+
**Sign-in codes**
|
|
78
|
+
- [`TOKEN_STALE` — `<call>: the code was sent to an e-mail the user no longer has`](#token_stale--call-the-code-was-sent-to-an-e-mail-the-user-no-longer-has)
|
|
79
|
+
- [The code from an earlier e-mail is refused with `CODE_INVALID`](#the-code-from-an-earlier-e-mail-is-refused-with-code_invalid)
|
|
80
|
+
- [`signInCode.request` answers `null` for a user who exists](#signincoderequest-answers-null-for-a-user-who-exists)
|
|
81
|
+
- [`TS2339: Property 'signInCode' does not exist on type 'TypeApi<…>'.`](#ts2339-property-signincode-does-not-exist-on-type-typeapi)
|
|
82
|
+
- `CODE_INVALID`, `TOKEN_UNKNOWN`, `TOKEN_SPENT`, `TOKEN_EXPIRED`, `USER_INACTIVE` and `VERSION_CONFLICT` from `signInCode.confirm`, and `TS2339` on its `token`: in their entries above.
|
|
83
|
+
|
|
77
84
|
**Permissions**
|
|
78
85
|
- [`PERMISSION_DEPTH` — `can: checking <type>#<permission> crossed more than <n> relations without an answer`](#permission_depth--can-checking-typepermission-crossed-more-than-n-relations-without-an-answer)
|
|
79
86
|
- [`permissions: this model was not made by defineModel() …`](#permissions-this-model-was-not-made-by-definemodel--pass-what-definemodel-answered)
|
|
@@ -291,7 +298,7 @@ Also `janus: "<name>" cannot name a user type — janus() answers a method of th
|
|
|
291
298
|
|
|
292
299
|
### `janus: session.lifespan: "<value>" is not a duration; write a number followed by ms, s, m, h or d — for example "15m" or "720h"`
|
|
293
300
|
|
|
294
|
-
The same for `session.renewAfter`, `tokens.verifyEmail`, `tokens.resetPassword` and `secondFactor.challenge`. Also `<option>: a duration must be above zero` and `<option>: a duration in milliseconds must be a finite number above zero`.
|
|
301
|
+
The same for `session.renewAfter`, `tokens.verifyEmail`, `tokens.resetPassword`, `tokens.signInCode` and `secondFactor.challenge`. Also `<option>: a duration must be above zero` and `<option>: a duration in milliseconds must be a finite number above zero`.
|
|
295
302
|
|
|
296
303
|
**When:** `janus({...})`.
|
|
297
304
|
**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.
|
|
@@ -492,8 +499,8 @@ janus({ ..., hasher: scryptHasher(), verifiers: [bcryptVerifier] }); // a Passwo
|
|
|
492
499
|
|
|
493
500
|
### `USER_INACTIVE` — `<call>: the user is inactive`
|
|
494
501
|
|
|
495
|
-
**When:** `signIn`, with the **right** password, for a user set inactive. Also `secondFactor.confirm`, for a user set inactive after `signIn` asked for a code.
|
|
496
|
-
**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. On `
|
|
502
|
+
**When:** `signIn`, with the **right** password, for a user set inactive. Also `secondFactor.confirm`, for a user set inactive after `signIn` asked for a code, and `signInCode.confirm`, for a user set inactive after the code was sent.
|
|
503
|
+
**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.
|
|
497
504
|
**Fix:** answer 403, or reactivate, then sign in again: `await auth.setActive(user, true)`.
|
|
498
505
|
|
|
499
506
|
### `TOKEN_UNKNOWN`, `TOKEN_SPENT`, `TOKEN_EXPIRED`
|
|
@@ -526,6 +533,28 @@ code. To give slower visitors more time:
|
|
|
526
533
|
janus({ ..., secondFactor: { issuer: 'Acme', keys, challenge: '10m' } });
|
|
527
534
|
```
|
|
528
535
|
|
|
536
|
+
**On `signInCode.confirm`**, the messages name the challenge
|
|
537
|
+
`signInCode.request` answered: `signInCode.confirm: no such challenge`,
|
|
538
|
+
`signInCode.confirm: the challenge was already used`,
|
|
539
|
+
`signInCode.confirm: the challenge has expired`.
|
|
540
|
+
|
|
541
|
+
**When:** `signInCode.confirm(challenge, code)`.
|
|
542
|
+
**Why:** a challenge lives ten minutes and takes five codes. It is spent by
|
|
543
|
+
the code that signs the user in, by the fifth wrong code, and by a refusal
|
|
544
|
+
that ends it (`TOKEN_STALE`, `USER_INACTIVE`). `TOKEN_UNKNOWN` also covers a
|
|
545
|
+
challenge whose user was deleted, one confirmed through another user type's
|
|
546
|
+
`signInCode`, the decoy challenge of a `request` that answered `null`, and
|
|
547
|
+
the two arguments swapped. Another type's `confirm` compares no code and
|
|
548
|
+
spends nothing, but it has already cost one of the challenge's five
|
|
549
|
+
attempts: the attempt is counted before the type is known. The challenge is
|
|
550
|
+
left for its own type, with one attempt fewer.
|
|
551
|
+
**Fix:** answer 400 and offer to send a new code. To give slower inboxes
|
|
552
|
+
more time:
|
|
553
|
+
|
|
554
|
+
```ts
|
|
555
|
+
janus({ ..., tokens: { signInCode: '15m' } });
|
|
556
|
+
```
|
|
557
|
+
|
|
529
558
|
### `TOKEN_STALE` — `<call>: the token was sent to an e-mail the user no longer has`
|
|
530
559
|
|
|
531
560
|
**When:** `verifyEmail.confirm` or `resetPassword.confirm`, after the user changed their e-mail.
|
|
@@ -582,7 +611,8 @@ const page = await access.list(user, 'view', 'record', {
|
|
|
582
611
|
|
|
583
612
|
## Second factor
|
|
584
613
|
|
|
585
|
-
Every entry here needs `janus({ ..., secondFactor })
|
|
614
|
+
Every entry here needs `janus({ ..., secondFactor })`, but for the
|
|
615
|
+
`signInCode.confirm` paragraph of `CODE_INVALID`. The messages start with
|
|
586
616
|
`secondFactor.enroll`, `secondFactor.activate`, `secondFactor.confirm` or
|
|
587
617
|
`signIn` — prefixed by the type with several user types:
|
|
588
618
|
`staff.secondFactor.confirm: …`. `secondFactor.confirm` also rejects with
|
|
@@ -595,7 +625,7 @@ each of those entries has a paragraph for it.
|
|
|
595
625
|
|
|
596
626
|
The same for `session` and `user`.
|
|
597
627
|
|
|
598
|
-
**When:** `tsc`, wherever `signIn`'s answer is read, once `janus()` is given a `secondFactor
|
|
628
|
+
**When:** `tsc`, wherever `signIn`'s answer is read, once `janus()` is given a `secondFactor` — and `signInCode.confirm`'s, on a user type with a password.
|
|
599
629
|
**Why:** `signIn` then answers one of two shapes: `{ status: 'signedIn', user, session, token }`, or `{ status: 'secondFactor', challenge, expiresAt }` for a user whose second factor is active — the password alone opens no session for them. Without `secondFactor`, `signIn` still answers a session.
|
|
600
630
|
**Fix:** switch on `status`:
|
|
601
631
|
|
|
@@ -613,11 +643,25 @@ const signedIn = await auth.secondFactor.confirm(challenge, code); // { status:
|
|
|
613
643
|
|
|
614
644
|
### `CODE_INVALID` — `<call>: the code does not match, or was already used`
|
|
615
645
|
|
|
616
|
-
`TokenError
|
|
646
|
+
`TokenError`. The same message answers three calls; there is a paragraph
|
|
647
|
+
for each below.
|
|
648
|
+
|
|
649
|
+
**When:** `secondFactor.activate`, `secondFactor.confirm` or
|
|
650
|
+
`signInCode.confirm`, with a code that does not match.
|
|
651
|
+
|
|
652
|
+
**`secondFactor.activate(user, code)`**
|
|
653
|
+
**Why:** the code is wrong, or it was accepted before: a code is accepted
|
|
654
|
+
once. There is no challenge here: no `attemptsLeft`, and nothing is spent.
|
|
655
|
+
**Fix:** let the user try the next code the app shows. If it is the one on
|
|
656
|
+
screen, see [the next entry](#the-code-the-authenticator-app-shows-is-refused-with-code_invalid).
|
|
617
657
|
|
|
618
|
-
|
|
619
|
-
**Why:** the code is wrong, or it was accepted before
|
|
620
|
-
|
|
658
|
+
**`secondFactor.confirm(challenge, code)`**
|
|
659
|
+
**Why:** the code is wrong, or it was accepted before. Every call costs one of
|
|
660
|
+
the challenge's five attempts, counted before anything is checked. The error
|
|
661
|
+
carries `attemptsLeft`, what remains; at `0`, the challenge is spent and the
|
|
662
|
+
next `confirm` is `TOKEN_SPENT`.
|
|
663
|
+
**Fix:** answer 401 with `attemptsLeft`, and send the visitor back to sign in
|
|
664
|
+
once it is `0`:
|
|
621
665
|
|
|
622
666
|
```ts
|
|
623
667
|
import { TokenError } from '@nxgt/janus';
|
|
@@ -632,7 +676,34 @@ try {
|
|
|
632
676
|
}
|
|
633
677
|
```
|
|
634
678
|
|
|
635
|
-
|
|
679
|
+
**`signInCode.confirm(challenge, code)`** — the e-mailed code.
|
|
680
|
+
**Why:** the code is not the one sent with this challenge, or is not six
|
|
681
|
+
digits — a space, a dash, a code pasted with its label. The error carries
|
|
682
|
+
`attemptsLeft` and the `userId` of the user the code was sent to. Every call
|
|
683
|
+
costs one of the challenge's five attempts, counted before the code is
|
|
684
|
+
compared — a call through another user type's `signInCode` too, although it
|
|
685
|
+
answers `TOKEN_UNKNOWN`. At `0` the challenge is spent, and the next
|
|
686
|
+
`confirm` is `TOKEN_SPENT`, even with the right code. Codes sent at once past
|
|
687
|
+
the fifth attempt are all refused, the right one included.
|
|
688
|
+
**Fix:** answer 401 with `attemptsLeft`, and request a new code once it is
|
|
689
|
+
`0`. Strip what the visitor may have typed around the digits before calling
|
|
690
|
+
`confirm`:
|
|
691
|
+
|
|
692
|
+
```ts
|
|
693
|
+
import { TokenError } from '@nxgt/janus';
|
|
694
|
+
|
|
695
|
+
try {
|
|
696
|
+
return await auth.signInCode.confirm(challenge, code.replace(/\D/g, ''));
|
|
697
|
+
} catch (error) {
|
|
698
|
+
if (error instanceof TokenError && error.code === 'CODE_INVALID') {
|
|
699
|
+
return Response.json({ code: error.code, attemptsLeft: error.attemptsLeft }, { status: 401 });
|
|
700
|
+
}
|
|
701
|
+
throw error;
|
|
702
|
+
}
|
|
703
|
+
```
|
|
704
|
+
|
|
705
|
+
If the visitor typed the code from the e-mail correctly, see
|
|
706
|
+
[the code from an earlier e-mail](#the-code-from-an-earlier-e-mail-is-refused-with-code_invalid).
|
|
636
707
|
|
|
637
708
|
### The code the authenticator app shows is refused with `CODE_INVALID`
|
|
638
709
|
|
|
@@ -741,6 +812,89 @@ A `TypeError`.
|
|
|
741
812
|
|
|
742
813
|
---
|
|
743
814
|
|
|
815
|
+
## Sign-in codes
|
|
816
|
+
|
|
817
|
+
The messages start with `signInCode.confirm` — prefixed by the type with
|
|
818
|
+
several user types: `patient.signInCode.confirm: …`. `signInCode.request`
|
|
819
|
+
throws nothing but `STORE_FAILED`: an e-mail it cannot sign in is `null`.
|
|
820
|
+
`signInCode.confirm` also rejects with
|
|
821
|
+
[`CODE_INVALID`](#code_invalid--call-the-code-does-not-match-or-was-already-used),
|
|
822
|
+
[`TOKEN_UNKNOWN`, `TOKEN_SPENT`, `TOKEN_EXPIRED`](#token_unknown-token_spent-token_expired),
|
|
823
|
+
[`USER_INACTIVE`](#user_inactive--call-the-user-is-inactive) and
|
|
824
|
+
[`VERSION_CONFLICT`](#version_conflict--call-expected-version-n-found-m);
|
|
825
|
+
each of those entries has a paragraph for it.
|
|
826
|
+
[The sign-in code guide](guide/sign-in-code.md) has the whole flow.
|
|
827
|
+
|
|
828
|
+
### `TOKEN_STALE` — `<call>: the code was sent to an e-mail the user no longer has`
|
|
829
|
+
|
|
830
|
+
`TokenError`, carrying the `userId`.
|
|
831
|
+
|
|
832
|
+
**When:** `signInCode.confirm`, after the user's e-mail was changed — by
|
|
833
|
+
`update`, or a patch naming the e-mail field — since the code was sent.
|
|
834
|
+
**Why:** the code proves an address, and signing in with it would mark as
|
|
835
|
+
verified an address the user no longer holds. The challenge is spent.
|
|
836
|
+
**Fix:** answer 400, and request a new code: it goes to the current address.
|
|
837
|
+
|
|
838
|
+
```ts
|
|
839
|
+
const issued = await auth.signInCode.request(currentEmail);
|
|
840
|
+
```
|
|
841
|
+
|
|
842
|
+
### The code from an earlier e-mail is refused with `CODE_INVALID`
|
|
843
|
+
|
|
844
|
+
**When:** the visitor asked for a code twice — pressed "send again", or
|
|
845
|
+
opened the form in two tabs — and typed the code of the first e-mail.
|
|
846
|
+
**Why:** every `request` issues a new code **with its own challenge**, and a
|
|
847
|
+
code is checked against the challenge it was sent with. The cookie or the
|
|
848
|
+
form field now holds the second challenge, so the first code does not
|
|
849
|
+
match it — and costs an attempt.
|
|
850
|
+
**Fix:** tell the visitor that only the last code sent works, and put the
|
|
851
|
+
time it was sent in the e-mail's subject or text so they can tell the
|
|
852
|
+
e-mails apart. The earlier challenge is not revoked: it still expires on its
|
|
853
|
+
own.
|
|
854
|
+
|
|
855
|
+
### `signInCode.request` answers `null` for a user who exists
|
|
856
|
+
|
|
857
|
+
**When:** `signInCode.request(email)`, for an address you can see in the
|
|
858
|
+
database.
|
|
859
|
+
**Why**, in the order to check:
|
|
860
|
+
|
|
861
|
+
1. **The user is inactive.** An inactive user gets no code, and the answer
|
|
862
|
+
is the same as for nobody.
|
|
863
|
+
2. **It is another user type.** `clinic.patient.signInCode.request` looks
|
|
864
|
+
among patients only; the same address may hold a user of another type.
|
|
865
|
+
3. **The address is not the type's e-mail field.** A type whose `email`
|
|
866
|
+
option names `contact` is looked up by `contact`. A login that looks like
|
|
867
|
+
an e-mail — a `username` of `ada@example.com` — is not an e-mail, and is
|
|
868
|
+
`null` too.
|
|
869
|
+
|
|
870
|
+
The e-mail is trimmed and lowercased before the lookup, so case and
|
|
871
|
+
surrounding spaces are never the cause.
|
|
872
|
+
**Fix:** `setActive(user, true)`; call the right type's `signInCode`; name
|
|
873
|
+
the field with `email: 'contact'`. Do not tell the visitor which case it
|
|
874
|
+
was — the route answers the same page either way.
|
|
875
|
+
|
|
876
|
+
### `TS2339: Property 'signInCode' does not exist on type 'TypeApi<…>'.`
|
|
877
|
+
|
|
878
|
+
Also `Property 'signInCode' does not exist on type 'Janus<…>'.`, with the
|
|
879
|
+
single-type form, `janus({ user })`.
|
|
880
|
+
|
|
881
|
+
**When:** `tsc`, on `auth.signInCode` or `clinic.<type>.signInCode`.
|
|
882
|
+
**Why:** the user type has no e-mail: no field called `email`, and no
|
|
883
|
+
`email` option naming one. A code has nowhere to be sent, so the flow is
|
|
884
|
+
absent from the type.
|
|
885
|
+
**Fix:** name the field that holds the e-mail:
|
|
886
|
+
|
|
887
|
+
```ts
|
|
888
|
+
janus({
|
|
889
|
+
users: {
|
|
890
|
+
staff: { schema: Staff, password: { login: 'username' }, email: 'workEmail' },
|
|
891
|
+
},
|
|
892
|
+
...
|
|
893
|
+
});
|
|
894
|
+
```
|
|
895
|
+
|
|
896
|
+
---
|
|
897
|
+
|
|
744
898
|
## Permissions
|
|
745
899
|
|
|
746
900
|
### `PERMISSION_DEPTH` — `can: checking <type>#<permission> crossed more than <n> relations without an answer`
|