@nxgt/janus 0.5.0 → 0.7.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 +116 -23
  2. package/dist/auth/config.d.ts +3 -0
  3. package/dist/auth/config.d.ts.map +1 -1
  4. package/dist/auth/context.d.ts +12 -0
  5. package/dist/auth/context.d.ts.map +1 -1
  6. package/dist/auth/email-flows.d.ts +11 -0
  7. package/dist/auth/email-flows.d.ts.map +1 -0
  8. package/dist/auth/index.d.ts +1 -1
  9. package/dist/auth/index.d.ts.map +1 -1
  10. package/dist/auth/one-time.d.ts +66 -6
  11. package/dist/auth/one-time.d.ts.map +1 -1
  12. package/dist/auth/password-written.d.ts +24 -0
  13. package/dist/auth/password-written.d.ts.map +1 -0
  14. package/dist/auth/port/assert-stores.d.ts.map +1 -1
  15. package/dist/auth/port/types.d.ts +23 -1
  16. package/dist/auth/port/types.d.ts.map +1 -1
  17. package/dist/auth/second-factor/challenge.d.ts +0 -6
  18. package/dist/auth/second-factor/challenge.d.ts.map +1 -1
  19. package/dist/auth/second-factor/factor.d.ts +1 -4
  20. package/dist/auth/second-factor/factor.d.ts.map +1 -1
  21. package/dist/auth/second-factor/flows.d.ts +7 -3
  22. package/dist/auth/second-factor/flows.d.ts.map +1 -1
  23. package/dist/auth/second-factor/lifecycle.d.ts.map +1 -1
  24. package/dist/auth/sign-in-code.d.ts +20 -0
  25. package/dist/auth/sign-in-code.d.ts.map +1 -0
  26. package/dist/auth/types.d.ts +56 -1
  27. package/dist/auth/types.d.ts.map +1 -1
  28. package/dist/auth/users.d.ts +2 -2
  29. package/dist/auth/users.d.ts.map +1 -1
  30. package/dist/chunks/{index-gbwn2tts.js → index-c0jajeda.js} +12 -2
  31. package/dist/chunks/{index-gbwn2tts.js.map → index-c0jajeda.js.map} +3 -3
  32. package/dist/conformance/cases/outage.d.ts.map +1 -1
  33. package/dist/conformance/cases/tokens.d.ts.map +1 -1
  34. package/dist/conformance/index.js +73 -2
  35. package/dist/conformance/index.js.map +4 -4
  36. package/dist/index.js +253 -116
  37. package/dist/index.js.map +15 -12
  38. package/docs/README.md +2 -1
  39. package/docs/guide/adapters.md +56 -3
  40. package/docs/guide/email-flows.md +11 -1
  41. package/docs/guide/errors.md +3 -3
  42. package/docs/guide/second-factor.md +31 -4
  43. package/docs/guide/sign-in-code.md +489 -0
  44. package/docs/guide/users.md +5 -4
  45. package/docs/guide/vocabulary.md +7 -6
  46. package/docs/roadmap.md +28 -13
  47. package/docs/troubleshooting.md +193 -28
  48. package/package.json +1 -1
@@ -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)
@@ -225,9 +232,11 @@ Also: `janus: store.<slot> is missing`, `janus: store must be an object with use
225
232
 
226
233
  **When:** `janus({...})`, from JavaScript or with a store typed loosely. TypeScript refuses a partial store at compile time and names the method.
227
234
  **Why:** `store` is `{ users, sessions, tokens }`, and each slot must answer every method of the port. `deleteExpiredSessions` is the one optional method: absent, or a function.
228
- An adapter written against `@nxgt/janus` 0.3 reports
229
- `store.tokens has no method countAttempt` until it implements the method 0.4
230
- added.
235
+ An adapter written for an earlier `@nxgt/janus` reports the method a later
236
+ release added to the port: `store.tokens has no method countAttempt` for one
237
+ written against 0.3 (the method came in 0.4), and
238
+ `store.tokens has no method spendUserTokens` for one written against 0.6 (the
239
+ method came in 0.7).
231
240
 
232
241
  **Fix:** pass the three stores, whole:
233
242
 
@@ -237,17 +246,17 @@ import { createMemoryStores, janus } from '@nxgt/janus';
237
246
  janus({ ..., store: createMemoryStores() });
238
247
  ```
239
248
 
240
- For `countAttempt`, upgrade the published adapter to the release that
241
- implements it — `@nxgt/janus-drizzle` 0.2, `@nxgt/janus-mongo` 0.3,
242
- `@nxgt/janus-redis` 0.2:
249
+ Upgrade the published adapter to the release that implements both —
250
+ `@nxgt/janus-drizzle` 0.3, `@nxgt/janus-mongo` 0.4, `@nxgt/janus-redis` 0.3:
243
251
 
244
252
  ```bash
245
- bun add @nxgt/janus@^0.4 @nxgt/janus-drizzle@^0.2 # or @nxgt/janus-mongo@^0.3, @nxgt/janus-redis@^0.2
253
+ bun add @nxgt/janus@^0.7 @nxgt/janus-drizzle@^0.3 # or @nxgt/janus-mongo@^0.4, @nxgt/janus-redis@^0.3
246
254
  ```
247
255
 
248
- Your own adapter implements it as
249
- [`TokenStore.countAttempt`](guide/adapters.md#tokenstorecountattempt) sets
250
- out, then runs the conformance suite.
256
+ Your own adapter implements them as
257
+ [`TokenStore.countAttempt`](guide/adapters.md#tokenstorecountattempt) and
258
+ [`TokenStore.spendUserTokens`](guide/adapters.md#tokenstorespendusertokens)
259
+ set out, then runs the conformance suite.
251
260
 
252
261
  ### `janus: relations must be a relation store — relations.deleteEntity is missing`
253
262
 
@@ -291,7 +300,7 @@ Also `janus: "<name>" cannot name a user type — janus() answers a method of th
291
300
 
292
301
  ### `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
302
 
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`.
303
+ 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
304
 
296
305
  **When:** `janus({...})`.
297
306
  **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.
@@ -466,7 +475,7 @@ if (error instanceof UserInvalidError) {
466
475
  Also `changePassword: the current password does not match`.
467
476
 
468
477
  **When:** `signIn`, `changePassword`.
469
- **Why:** no user holds the login, the user has no password, or the password is wrong — **one code for the three**. `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.
478
+ **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.
470
479
  **Fix:** answer 401 with the same body whatever the reason:
471
480
 
472
481
  ```ts
@@ -492,8 +501,8 @@ janus({ ..., hasher: scryptHasher(), verifiers: [bcryptVerifier] }); // a Passwo
492
501
 
493
502
  ### `USER_INACTIVE` — `<call>: the user is inactive`
494
503
 
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 `secondFactor.confirm`, the challenge is spent: reactivating the user does not revive it.
504
+ **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.
505
+ **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
506
  **Fix:** answer 403, or reactivate, then sign in again: `await auth.setActive(user, true)`.
498
507
 
499
508
  ### `TOKEN_UNKNOWN`, `TOKEN_SPENT`, `TOKEN_EXPIRED`
@@ -515,10 +524,14 @@ answered: `secondFactor.confirm: no such challenge`,
515
524
 
516
525
  **When:** `secondFactor.confirm(challenge, code)`.
517
526
  **Why:** a challenge lives five minutes and takes five codes. It is spent by
518
- the code that opens the session, by the fifth wrong code, and by a refusal
519
- that ends it (`USER_INACTIVE`, `SECOND_FACTOR_NOT_ENROLLED`). `TOKEN_UNKNOWN`
520
- also covers a challenge whose user was deleted, and a challenge passed where a
521
- code was expected — the two arguments swapped.
527
+ the code that opens the session, by the fifth wrong code, by a refusal
528
+ that ends it (`USER_INACTIVE`, `SECOND_FACTOR_NOT_ENROLLED`), and by a
529
+ password written — `resetPassword.confirm`, `setPassword`, `changePassword` —
530
+ which ends every sign-in left waiting on its code.
531
+ `TOKEN_UNKNOWN` also covers a challenge whose user was deleted, one
532
+ confirmed through another user type's `secondFactor`, and a challenge passed
533
+ where a code was expected — the two arguments swapped. Another type's
534
+ `confirm` still costs an attempt, and the fifth spends the challenge.
522
535
  **Fix:** answer 400 and send the visitor back to sign in, which asks for a new
523
536
  code. To give slower visitors more time:
524
537
 
@@ -526,10 +539,37 @@ code. To give slower visitors more time:
526
539
  janus({ ..., secondFactor: { issuer: 'Acme', keys, challenge: '10m' } });
527
540
  ```
528
541
 
542
+ **On `signInCode.confirm`**, the messages name the challenge
543
+ `signInCode.request` answered: `signInCode.confirm: no such challenge`,
544
+ `signInCode.confirm: the challenge was already used`,
545
+ `signInCode.confirm: the challenge has expired`.
546
+
547
+ **When:** `signInCode.confirm(challenge, code)`.
548
+ **Why:** a challenge lives ten minutes and takes five codes. It is spent by
549
+ the code that signs the user in, by the fifth wrong code, by a refusal
550
+ that ends it (`TOKEN_STALE`, `USER_INACTIVE`), and by the next `request` for
551
+ the same user: only the last code sent works, so a visitor who asked twice
552
+ and typed the first code gets `TOKEN_SPENT` — and when two requests race,
553
+ even the last code can be spent: at most one survives, sometimes none. `TOKEN_UNKNOWN` also covers a
554
+ challenge whose user was deleted, one confirmed through another user type's
555
+ `signInCode`, the decoy challenge of a `request` that answered `null`, and
556
+ the two arguments swapped. Another type's `confirm` compares no code, but it
557
+ has already cost one of the challenge's five
558
+ attempts: the attempt is counted before the type is known. The challenge is
559
+ left for its own type, with one attempt fewer — and the fifth such call
560
+ spends it, as a fifth wrong code would.
561
+ **Fix:** answer 400 and offer to send a new code — and tell the visitor to
562
+ use the latest e-mail. To give slower inboxes
563
+ more time:
564
+
565
+ ```ts
566
+ janus({ ..., tokens: { signInCode: '15m' } });
567
+ ```
568
+
529
569
  ### `TOKEN_STALE` — `<call>: the token was sent to an e-mail the user no longer has`
530
570
 
531
- **When:** `verifyEmail.confirm` or `resetPassword.confirm`, after the user changed their e-mail.
532
- **Why:** confirming it would verify an address nobody holds any more. The token is spent.
571
+ **When:** `verifyEmail.confirm` or `resetPassword.confirm`, after the user changed their e-mail — even while the link was being redeemed: the address is checked again on the very record the write replaces.
572
+ **Why:** confirming it would verify an address nobody holds any more, or reset a password through one. The token is spent, and nothing is written.
533
573
  **Fix:** send a new token to the current address: `await auth.verifyEmail.send(user)`.
534
574
 
535
575
  ### `INVALID_CURSOR` — `<call>: this cursor was not minted by this store, or was minted for another ordering (<n> characters)`
@@ -582,7 +622,8 @@ const page = await access.list(user, 'view', 'record', {
582
622
 
583
623
  ## Second factor
584
624
 
585
- Every entry here needs `janus({ ..., secondFactor })`. The messages start with
625
+ Every entry here needs `janus({ ..., secondFactor })`, but for the
626
+ `signInCode.confirm` paragraph of `CODE_INVALID`. The messages start with
586
627
  `secondFactor.enroll`, `secondFactor.activate`, `secondFactor.confirm` or
587
628
  `signIn` — prefixed by the type with several user types:
588
629
  `staff.secondFactor.confirm: …`. `secondFactor.confirm` also rejects with
@@ -595,8 +636,8 @@ each of those entries has a paragraph for it.
595
636
 
596
637
  The same for `session` and `user`.
597
638
 
598
- **When:** `tsc`, wherever `signIn`'s answer is read, once `janus()` is given a `secondFactor`.
599
- **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.
639
+ **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.
640
+ **Why:** `signIn` then answers one of two shapes: `{ status: 'signedIn', user, session, token }`, or `{ status: 'secondFactor', challenge, expiresAt, userId }` for a user whose second factor is active — the password alone opens no session for them. Without `secondFactor`, `signIn` still answers a session.
600
641
  **Fix:** switch on `status`:
601
642
 
602
643
  ```ts
@@ -613,11 +654,25 @@ const signedIn = await auth.secondFactor.confirm(challenge, code); // { status:
613
654
 
614
655
  ### `CODE_INVALID` — `<call>: the code does not match, or was already used`
615
656
 
616
- `TokenError`, carrying `attemptsLeft` on `secondFactor.confirm`.
657
+ `TokenError`. The same message answers three calls; there is a paragraph
658
+ for each below.
659
+
660
+ **When:** `secondFactor.activate`, `secondFactor.confirm` or
661
+ `signInCode.confirm`, with a code that does not match.
662
+
663
+ **`secondFactor.activate(user, code)`**
664
+ **Why:** the code is wrong, or it was accepted before: a code is accepted
665
+ once. There is no challenge here: no `attemptsLeft`, and nothing is spent.
666
+ **Fix:** let the user try the next code the app shows. If it is the one on
667
+ screen, see [the next entry](#the-code-the-authenticator-app-shows-is-refused-with-code_invalid).
617
668
 
618
- **When:** `secondFactor.activate(user, code)` or `secondFactor.confirm(challenge, code)`.
619
- **Why:** the code is wrong, or it was accepted before: a code is accepted once. On `confirm`, every call costs one of the challenge's five attempts, counted before anything is checked. `attemptsLeft` is what remains; at `0`, the challenge is spent and the next `confirm` is `TOKEN_SPENT`. On `activate` there is no challenge, no `attemptsLeft`, and nothing is spent: the user tries the next code.
620
- **Fix:** answer 401 with `attemptsLeft`, and send the visitor back to sign in once it is `0`:
669
+ **`secondFactor.confirm(challenge, code)`**
670
+ **Why:** the code is wrong, or it was accepted before. Every call costs one of
671
+ the challenge's five attempts, counted before anything is checked. The error
672
+ carries `attemptsLeft`, what remains; at `0`, the challenge is spent and the
673
+ next `confirm` is `TOKEN_SPENT`.
674
+ **Fix:** answer 401 with `attemptsLeft`, and send the visitor back to sign in
675
+ once it is `0`:
621
676
 
622
677
  ```ts
623
678
  import { TokenError } from '@nxgt/janus';
@@ -632,7 +687,34 @@ try {
632
687
  }
633
688
  ```
634
689
 
635
- If the code the user typed is the one their app shows, see the next entry.
690
+ **`signInCode.confirm(challenge, code)`** — the e-mailed code.
691
+ **Why:** the code is not the one sent with this challenge, or is not six
692
+ digits — a space, a dash, a code pasted with its label. The error carries
693
+ `attemptsLeft` and the `userId` of the user the code was sent to. Every call
694
+ costs one of the challenge's five attempts, counted before the code is
695
+ compared — a call through another user type's `signInCode` too, although it
696
+ answers `TOKEN_UNKNOWN`. At `0` the challenge is spent, and the next
697
+ `confirm` is `TOKEN_SPENT`, even with the right code. Codes sent at once past
698
+ the fifth attempt are all refused, the right one included.
699
+ **Fix:** answer 401 with `attemptsLeft`, and request a new code once it is
700
+ `0`. Strip what the visitor may have typed around the digits before calling
701
+ `confirm`:
702
+
703
+ ```ts
704
+ import { TokenError } from '@nxgt/janus';
705
+
706
+ try {
707
+ return await auth.signInCode.confirm(challenge, code.replace(/\D/g, ''));
708
+ } catch (error) {
709
+ if (error instanceof TokenError && error.code === 'CODE_INVALID') {
710
+ return Response.json({ code: error.code, attemptsLeft: error.attemptsLeft }, { status: 401 });
711
+ }
712
+ throw error;
713
+ }
714
+ ```
715
+
716
+ If the visitor typed the code from the e-mail correctly, see
717
+ [the code from an earlier e-mail](#the-code-from-an-earlier-e-mail-is-refused-with-code_invalid).
636
718
 
637
719
  ### The code the authenticator app shows is refused with `CODE_INVALID`
638
720
 
@@ -741,6 +823,89 @@ A `TypeError`.
741
823
 
742
824
  ---
743
825
 
826
+ ## Sign-in codes
827
+
828
+ The messages start with `signInCode.confirm` — prefixed by the type with
829
+ several user types: `patient.signInCode.confirm: …`. `signInCode.request`
830
+ throws nothing but `STORE_FAILED`: an e-mail it cannot sign in is `null`.
831
+ `signInCode.confirm` also rejects with
832
+ [`CODE_INVALID`](#code_invalid--call-the-code-does-not-match-or-was-already-used),
833
+ [`TOKEN_UNKNOWN`, `TOKEN_SPENT`, `TOKEN_EXPIRED`](#token_unknown-token_spent-token_expired),
834
+ [`USER_INACTIVE`](#user_inactive--call-the-user-is-inactive) and
835
+ [`VERSION_CONFLICT`](#version_conflict--call-expected-version-n-found-m);
836
+ each of those entries has a paragraph for it.
837
+ [The sign-in code guide](guide/sign-in-code.md) has the whole flow.
838
+
839
+ ### `TOKEN_STALE` — `<call>: the code was sent to an e-mail the user no longer has`
840
+
841
+ `TokenError`, carrying the `userId`.
842
+
843
+ **When:** `signInCode.confirm`, after the user's e-mail was changed — by
844
+ `update`, or a patch naming the e-mail field — since the code was sent.
845
+ **Why:** the code proves an address, and signing in with it would mark as
846
+ verified an address the user no longer holds. The challenge is spent.
847
+ **Fix:** answer 400, and request a new code: it goes to the current address.
848
+
849
+ ```ts
850
+ const issued = await auth.signInCode.request(currentEmail);
851
+ ```
852
+
853
+ ### The code from an earlier e-mail is refused with `CODE_INVALID`
854
+
855
+ **When:** the visitor asked for a code twice — pressed "send again", or
856
+ opened the form in two tabs — and typed the code of the first e-mail.
857
+ **Why:** every `request` issues a new code **with its own challenge**, and a
858
+ code is checked against the challenge it was sent with. The cookie or the
859
+ form field now holds the second challenge, so the first code does not
860
+ match it — and costs an attempt.
861
+ **Fix:** tell the visitor that only the last code sent works, and put the
862
+ time it was sent in the e-mail's subject or text so they can tell the
863
+ e-mails apart. The earlier challenge is spent by the new `request`: the
864
+ first code, even with its own challenge, answers `TOKEN_SPENT`.
865
+
866
+ ### `signInCode.request` answers `null` for a user who exists
867
+
868
+ **When:** `signInCode.request(email)`, for an address you can see in the
869
+ database.
870
+ **Why**, in the order to check:
871
+
872
+ 1. **The user is inactive.** An inactive user gets no code, and the answer
873
+ is the same as for nobody.
874
+ 2. **It is another user type.** `clinic.patient.signInCode.request` looks
875
+ among patients only; the same address may hold a user of another type.
876
+ 3. **The address is not the type's e-mail field.** A type whose `email`
877
+ option names `contact` is looked up by `contact`. A login that looks like
878
+ an e-mail — a `username` of `ada@example.com` — is not an e-mail, and is
879
+ `null` too.
880
+
881
+ The e-mail is trimmed and lowercased before the lookup, so case and
882
+ surrounding spaces are never the cause.
883
+ **Fix:** `setActive(user, true)`; call the right type's `signInCode`; name
884
+ the field with `email: 'contact'`. Do not tell the visitor which case it
885
+ was — the route answers the same page either way.
886
+
887
+ ### `TS2339: Property 'signInCode' does not exist on type 'TypeApi<…>'.`
888
+
889
+ Also `Property 'signInCode' does not exist on type 'Janus<…>'.`, with the
890
+ single-type form, `janus({ user })`.
891
+
892
+ **When:** `tsc`, on `auth.signInCode` or `clinic.<type>.signInCode`.
893
+ **Why:** the user type has no e-mail: no field called `email`, and no
894
+ `email` option naming one. A code has nowhere to be sent, so the flow is
895
+ absent from the type.
896
+ **Fix:** name the field that holds the e-mail:
897
+
898
+ ```ts
899
+ janus({
900
+ users: {
901
+ staff: { schema: Staff, password: { login: 'username' }, email: 'workEmail' },
902
+ },
903
+ ...
904
+ });
905
+ ```
906
+
907
+ ---
908
+
744
909
  ## Permissions
745
910
 
746
911
  ### `PERMISSION_DEPTH` — `can: checking <type>#<permission> crossed more than <n> relations without an answer`
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nxgt/janus",
3
- "version": "0.5.0",
3
+ "version": "0.7.0",
4
4
  "description": "Embeddable, type-safe identities and permissions: bring your own database",
5
5
  "license": "MIT",
6
6
  "type": "module",