@nxgt/janus 0.13.0 → 0.15.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 +132 -25
- 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 +25 -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-sign-in.d.ts +5 -0
- package/dist/auth/config/resolve-sign-in.d.ts.map +1 -0
- package/dist/auth/config/resolved-config.d.ts +6 -0
- package/dist/auth/config/resolved-config.d.ts.map +1 -1
- package/dist/auth/derived-keys.d.ts +9 -0
- package/dist/auth/derived-keys.d.ts.map +1 -0
- package/dist/auth/email-sign-in.d.ts +40 -0
- package/dist/auth/email-sign-in.d.ts.map +1 -0
- package/dist/auth/events.d.ts +22 -6
- package/dist/auth/events.d.ts.map +1 -1
- package/dist/auth/index.d.ts +2 -2
- package/dist/auth/index.d.ts.map +1 -1
- package/dist/auth/magic-link.d.ts +22 -0
- package/dist/auth/magic-link.d.ts.map +1 -0
- package/dist/auth/one-time/index.d.ts +3 -1
- package/dist/auth/one-time/index.d.ts.map +1 -1
- package/dist/auth/one-time/window.d.ts +50 -0
- package/dist/auth/one-time/window.d.ts.map +1 -0
- package/dist/auth/password-written.d.ts +7 -0
- package/dist/auth/password-written.d.ts.map +1 -1
- package/dist/auth/port/types/tokens.d.ts +6 -3
- package/dist/auth/port/types/tokens.d.ts.map +1 -1
- package/dist/auth/second-factor/app-code-attempts.d.ts +5 -8
- package/dist/auth/second-factor/app-code-attempts.d.ts.map +1 -1
- package/dist/auth/second-factor/challenge.d.ts.map +1 -1
- package/dist/auth/second-factor/recovery-codes.d.ts.map +1 -1
- package/dist/auth/second-factor/recovery.d.ts.map +1 -1
- package/dist/auth/sign-in-attempts.d.ts +43 -0
- package/dist/auth/sign-in-attempts.d.ts.map +1 -0
- package/dist/auth/sign-in-code.d.ts +1 -3
- package/dist/auth/sign-in-code.d.ts.map +1 -1
- package/dist/auth/types/email-flows.d.ts +3 -3
- package/dist/auth/types/index.d.ts +1 -0
- package/dist/auth/types/index.d.ts.map +1 -1
- package/dist/auth/types/janus.d.ts +8 -1
- package/dist/auth/types/janus.d.ts.map +1 -1
- package/dist/auth/types/magic-link.d.ts +46 -0
- package/dist/auth/types/magic-link.d.ts.map +1 -0
- package/dist/auth/types/password.d.ts +8 -0
- package/dist/auth/types/password.d.ts.map +1 -1
- package/dist/auth/users/any-type-api.d.ts +2 -2
- package/dist/auth/users/any-type-api.d.ts.map +1 -1
- package/dist/auth/users/fields-patch.d.ts +8 -2
- package/dist/auth/users/fields-patch.d.ts.map +1 -1
- package/dist/auth/users/record-flows.d.ts.map +1 -1
- package/dist/auth/users/sign-in.d.ts +2 -2
- package/dist/auth/users/sign-in.d.ts.map +1 -1
- package/dist/auth/users/type-api.d.ts +2 -1
- package/dist/auth/users/type-api.d.ts.map +1 -1
- package/dist/auth/users/update.d.ts +12 -0
- package/dist/auth/users/update.d.ts.map +1 -0
- package/dist/chunks/{index-km7ffctx.js → index-6c9fbtv2.js} +2 -2
- package/dist/chunks/{index-rd2rddvv.js → index-baazjyvc.js} +2 -2
- package/dist/chunks/{index-j2gd8fry.js → index-hnwggyxs.js} +3 -2
- package/dist/chunks/{index-j2gd8fry.js.map → index-hnwggyxs.js.map} +3 -3
- package/dist/chunks/{index-jp2kka6d.js → index-kj3st57z.js} +2 -2
- package/dist/conformance/cases/tokens/kinds.d.ts +4 -1
- package/dist/conformance/cases/tokens/kinds.d.ts.map +1 -1
- package/dist/conformance/index.js +26 -4
- package/dist/conformance/index.js.map +3 -3
- package/dist/errors/janus-error/base.d.ts +1 -0
- package/dist/errors/janus-error/base.d.ts.map +1 -1
- package/dist/errors/janus-error/codes.d.ts +3 -1
- package/dist/errors/janus-error/codes.d.ts.map +1 -1
- package/dist/errors/janus-error/options.d.ts +9 -2
- package/dist/errors/janus-error/options.d.ts.map +1 -1
- package/dist/index.js +296 -101
- package/dist/index.js.map +24 -17
- package/dist/permissions/index.js +3 -3
- package/docs/README.md +4 -3
- package/docs/guide/adapters.md +11 -7
- package/docs/guide/email-flows.md +11 -7
- package/docs/guide/errors.md +12 -4
- package/docs/guide/events.md +75 -14
- package/docs/guide/magic-link.md +411 -0
- package/docs/guide/passwords.md +116 -17
- package/docs/guide/second-factor.md +6 -3
- package/docs/guide/sign-in-code.md +14 -6
- package/docs/guide/users.md +13 -7
- package/docs/guide/vocabulary.md +6 -5
- package/docs/roadmap.md +37 -23
- package/docs/troubleshooting.md +203 -9
- package/package.json +1 -1
- /package/dist/chunks/{index-km7ffctx.js.map → index-6c9fbtv2.js.map} +0 -0
- /package/dist/chunks/{index-rd2rddvv.js.map → index-baazjyvc.js.map} +0 -0
- /package/dist/chunks/{index-jp2kka6d.js.map → index-kj3st57z.js.map} +0 -0
package/README.md
CHANGED
|
@@ -46,7 +46,7 @@ a published entry point is a promise.
|
|
|
46
46
|
## Three ways to use it
|
|
47
47
|
|
|
48
48
|
Janus has two sides. **Identities** answers *who is this?* — users, their
|
|
49
|
-
logins and passwords, codes sent by e-mail, a TOTP second factor, sessions, one-time tokens. **Permissions** answers *may
|
|
49
|
+
logins and passwords, codes and links sent by e-mail, a TOTP second factor, sessions, one-time tokens. **Permissions** answers *may
|
|
50
50
|
they?* — a model, the tuples stored against it, and `can`. Each side is usable
|
|
51
51
|
alone, and neither loads the other's code: a spec reads the import graph of
|
|
52
52
|
each entry point and fails if one reaches into the other.
|
|
@@ -186,7 +186,7 @@ if (error instanceof JanusError) {
|
|
|
186
186
|
| `LOGIN_TAKEN`, `VERSION_CONFLICT` | 409 |
|
|
187
187
|
| `USER_INVALID` | 400, field by field from `issues` |
|
|
188
188
|
| `PASSWORD_TOO_SHORT`, `HASH_UNSUPPORTED` | 400 |
|
|
189
|
-
| `CREDENTIALS_INVALID` | 401 — one code for an unknown login, no password
|
|
189
|
+
| `CREDENTIALS_INVALID` | 401 — one code for an unknown login, no password, a wrong one and a login throttled for too many; `retryAfter`, when set, belongs in the body and a `Retry-After` header |
|
|
190
190
|
| `CODE_INVALID` | 401 — a one-time code that does not match: a second factor's, or one sent by e-mail; `attemptsLeft` from either `confirm` belongs in the body |
|
|
191
191
|
| `SECOND_FACTOR_NOT_ENROLLED`, `SECOND_FACTOR_ACTIVE` | 409 — the factor is not in the state the call needs |
|
|
192
192
|
| `USER_INACTIVE` | 403 |
|
|
@@ -324,6 +324,8 @@ await auth.resetPassword.request(email); // … | null
|
|
|
324
324
|
await auth.resetPassword.confirm(token, newPassword);
|
|
325
325
|
await auth.signInCode.request(email); // { code, challenge, email, expiresAt, user } | null
|
|
326
326
|
await auth.signInCode.confirm(challenge, code); // { status: 'signedIn', user, session, token }
|
|
327
|
+
await auth.magicLink.request(email); // { token, email, expiresAt, user } | null
|
|
328
|
+
await auth.magicLink.confirm(token); // { status: 'signedIn', user, session, token }
|
|
327
329
|
await auth.stepUp.request(user); // { via: 'email', code, challenge, email, expiresAt, user }
|
|
328
330
|
await auth.stepUp.confirm(request, challenge, code); // the request's session, authenticatedAt: now
|
|
329
331
|
|
|
@@ -366,8 +368,8 @@ else reaches the store and is asynchronous.
|
|
|
366
368
|
compile error on `login`, and the message lists the fields you could have
|
|
367
369
|
meant. It is normalised with `'lowercaseTrim'` unless you say otherwise.
|
|
368
370
|
- **`email`** defaults to the field named `email`. A type without one has no
|
|
369
|
-
`verifyEmail`, no `resetPassword` and no `
|
|
370
|
-
its type, not failing at run time. Changing the e-mail sets `emailVerified`
|
|
371
|
+
`verifyEmail`, no `resetPassword`, no `signInCode` and no `magicLink` — they
|
|
372
|
+
are absent from its type, not failing at run time. Changing the e-mail sets `emailVerified`
|
|
371
373
|
back to `false`.
|
|
372
374
|
- **Per type**: `create`, `find` (or `null`), `get` (or `NOT_FOUND`), `list`,
|
|
373
375
|
`update(user, patch)` — merged over the stored fields, then validated whole —
|
|
@@ -384,6 +386,20 @@ else reaches the store and is asynchronous.
|
|
|
384
386
|
`findUser` and `getUser` across types, `cookie.serialize(token, session)` and
|
|
385
387
|
`cookie.clear()` — `HttpOnly; SameSite=Lax; Secure` unless you say otherwise
|
|
386
388
|
— and `collectExpired`.
|
|
389
|
+
- **Password guessing is throttled**, on by default: past ten passwords
|
|
390
|
+
tried at one login in a 15-minute window, `signIn` refuses every one — the
|
|
391
|
+
right password included — with `CREDENTIALS_INVALID`, `reason:
|
|
392
|
+
'throttled'` and `retryAfter`, the seconds until the next window. Nothing
|
|
393
|
+
locks, a login nobody holds is counted alike, and a password sign-in that
|
|
394
|
+
opens a session — after its second factor, when one is active — starts
|
|
395
|
+
the count again. `signIn: { throttle: { attempts, window } }`
|
|
396
|
+
changes it, `signIn: { throttle: false }` turns it off; a store that
|
|
397
|
+
cannot count throws `STORE_FAILED`
|
|
398
|
+
([passwords](docs/guide/passwords.md#password-guessing-is-throttled)).
|
|
399
|
+
|
|
400
|
+
```ts
|
|
401
|
+
janus({ user, password: { login: 'email' }, store, hasher, signIn: { throttle: { attempts: 5, window: '1h' } } });
|
|
402
|
+
```
|
|
387
403
|
- **Sessions** last `'7d'` and slide: `authenticate` renews one once `renewAfter`
|
|
388
404
|
(`'1d'`) has passed, writing at most once per period, and says so with
|
|
389
405
|
`renewed`. The token is handed back once; the store only holds its `sha256`.
|
|
@@ -488,7 +504,11 @@ await sessions.reauthenticateSession(session.id, new Date()); // { …, authenti
|
|
|
488
504
|
```
|
|
489
505
|
|
|
490
506
|
A token of kind `stepUp` is that confirmation's challenge: kept apart from
|
|
491
|
-
`signInCode`, so neither is ever redeemed as the other.
|
|
507
|
+
`signInCode`, so neither is ever redeemed as the other. A token of kind
|
|
508
|
+
`magicLink`, new in 0.15, is a sign-in link's, kept apart from `signInCode`
|
|
509
|
+
the same way: a sign-in code's challenge is held by whoever asked for it,
|
|
510
|
+
and must never sign anyone in as a link. A store that lists the kinds — a
|
|
511
|
+
`CHECK`, a validator's enum — adds `magicLink`.
|
|
492
512
|
|
|
493
513
|
An adapter written against `@nxgt/janus` 0.3 does not compile against this
|
|
494
514
|
port until it implements `countAttempt`, nor one written against 0.6 until it
|
|
@@ -620,6 +640,43 @@ A six-digit code sent to the user's e-mail signs them in, with no password.
|
|
|
620
640
|
[The sign-in code guide](docs/guide/sign-in-code.md) has the request that
|
|
621
641
|
tells nobody who exists, the challenge in a cookie, every error, and a test.
|
|
622
642
|
|
|
643
|
+
### Sign-in links — `magicLink`
|
|
644
|
+
|
|
645
|
+
```ts
|
|
646
|
+
const issued = await auth.magicLink.request(email); // null for nobody, or an inactive user
|
|
647
|
+
if (issued !== null) {
|
|
648
|
+
const link = `https://app.example/sign-in/link?token=${issued.token}`; // a page of yours
|
|
649
|
+
await sendMail(issued.email, `Sign in: ${link}`); // the token in the e-mail, and nowhere else
|
|
650
|
+
}
|
|
651
|
+
// answer the same page either way: nothing reaches the visitor, so there is nothing to forge
|
|
652
|
+
|
|
653
|
+
// the page's button POSTs the token back — never confirm on the link's GET
|
|
654
|
+
const signedIn = await auth.magicLink.confirm(token); // { status: 'signedIn', user, session, token }
|
|
655
|
+
```
|
|
656
|
+
|
|
657
|
+
A link sent to the user's e-mail signs them in: a sign-in code with nothing
|
|
658
|
+
to type.
|
|
659
|
+
|
|
660
|
+
- **On every user type with an e-mail**, with a password or without one, as
|
|
661
|
+
`signInCode`. A type with no e-mail has no `magicLink` at all.
|
|
662
|
+
- **`request(email)` answers `null`** when nobody of this type holds that
|
|
663
|
+
e-mail, or the user is inactive — never say which. It answers a `token` of
|
|
664
|
+
32 random bytes, base64url, for a link; only its hash is stored. One link
|
|
665
|
+
is live per user: a new `request` spends the ones before. A link and a
|
|
666
|
+
code are separate: asking for one leaves the other live.
|
|
667
|
+
- **`confirm(token)`** spends the token in one conditional write — of two
|
|
668
|
+
confirmations at once, one signs in — marks the e-mail verified and opens
|
|
669
|
+
the session. A link lives `'15m'` (`tokens.magicLink`); there are no
|
|
670
|
+
attempts, since there is nothing to guess. `TOKEN_STALE`, `USER_INACTIVE`
|
|
671
|
+
and an active second factor are handled as for a code.
|
|
672
|
+
- **Not ended by a password write, nor limited by the sign-in throttle**:
|
|
673
|
+
the password proves nothing a link does, and the throttle counts
|
|
674
|
+
passwords. Rate-limit `request` per address and per client.
|
|
675
|
+
|
|
676
|
+
[The sign-in link guide](docs/guide/magic-link.md) has the page that
|
|
677
|
+
confirms from a `POST` so mail scanners spend nothing, every error, routes
|
|
678
|
+
and a test.
|
|
679
|
+
|
|
623
680
|
### Step-up — `stepUp` and `assertFresh`
|
|
624
681
|
|
|
625
682
|
```ts
|
|
@@ -685,36 +742,47 @@ await auth.signUp({ email, password }); // the listener has the event before thi
|
|
|
685
742
|
| `type` | Sent by |
|
|
686
743
|
| --- | --- |
|
|
687
744
|
| `user.created` | `create`, `signUp` |
|
|
688
|
-
| `user.emailVerified` | `verifyEmail.confirm`; `resetPassword.confirm` and `signInCode.confirm`, whose link or code proves the e-mail too — never for an e-mail already verified |
|
|
745
|
+
| `user.emailVerified` | `verifyEmail.confirm`; `resetPassword.confirm`, `magicLink.confirm` and `signInCode.confirm`, whose link or code proves the e-mail too — never for an e-mail already verified |
|
|
689
746
|
| `user.passwordReset` | `resetPassword.confirm` |
|
|
747
|
+
| `user.passwordChanged` | `changePassword`, `setPassword` — never a reset, which is `user.passwordReset` alone |
|
|
748
|
+
| `user.emailChanged` | `update`, when it changed the e-mail — carrying `formerEmail`, the address before (`null` for none) |
|
|
690
749
|
| `user.secondFactorEnabled` | `secondFactor.activate`, once the factor is active — not `enroll`, which leaves it waiting |
|
|
691
750
|
| `user.secondFactorDisabled` | `secondFactor.disable`, when it removed an active factor — never for a user who had none, or one still waiting |
|
|
692
751
|
| `user.recoveryCodesRegenerated` | `secondFactor.regenerateRecoveryCodes` — not `activate`, whose codes come with `user.secondFactorEnabled` |
|
|
693
752
|
| `user.recoveryCodeUsed` | `secondFactor.recover`, once the recovery code is spent: a sign-in without the user's phone |
|
|
694
753
|
| `user.deleted` | `delete`, once — a replay that deletes nobody sends nothing |
|
|
695
754
|
|
|
696
|
-
- **The user is named by id, and nothing else**: no login, no
|
|
697
|
-
|
|
698
|
-
|
|
755
|
+
- **The user is named by id, and nothing else**: no login, no field, no
|
|
756
|
+
password, no token. Whoever receives the event reads the rest from where it
|
|
757
|
+
is kept, if they may. One exception: `user.emailChanged` carries
|
|
758
|
+
`formerEmail`, which nothing keeps once the write landed, so a notice can
|
|
759
|
+
reach the inbox the account just left. `@nxgt/janus-webhooks` never posts
|
|
760
|
+
it.
|
|
699
761
|
- **Each event has an `id` of its own**, a UUIDv7: the key to deliver it once.
|
|
700
762
|
- **The listener runs after the write, and is awaited** before the
|
|
701
763
|
flow answers, so a durable queue has the event by then. `occurredAt` is the
|
|
702
764
|
write's own time. A refused flow sends nothing.
|
|
703
765
|
- **Typed**: `events` is a `UserEventListener`; `UserEventType` is the closed
|
|
704
|
-
union of the
|
|
705
|
-
a new type, like the two recovery-code ones in 0.10
|
|
766
|
+
union of the ten types, so a `switch` on `event.type` is exhaustive — and
|
|
767
|
+
a new type, like the two recovery-code ones in 0.10 or the two change ones
|
|
768
|
+
in 0.14, breaks it until handled.
|
|
706
769
|
- **A listener that throws fails no flow** — the write happened. It is a
|
|
707
770
|
`JANUS_EVENT_FAILED` warning naming the event's type, its id and the user's
|
|
708
771
|
id, never the failure's message.
|
|
709
772
|
|
|
710
|
-
To tell the user their
|
|
711
|
-
recovery code was used, send
|
|
773
|
+
To tell the user their password or e-mail changed, their second factor was
|
|
774
|
+
turned on or off, or that a recovery code was used, send
|
|
712
775
|
[`@nxgt/janus-mail`](https://www.npmjs.com/package/@nxgt/janus-mail)'s notice
|
|
713
776
|
from the listener — or from a queue it feeds:
|
|
714
777
|
|
|
715
778
|
```ts
|
|
716
779
|
// `mail` is janusMail({ … }), and the user schema holds a name and a locale.
|
|
717
780
|
async events(event) {
|
|
781
|
+
if (event.type === 'user.emailChanged' && event.formerEmail != null) {
|
|
782
|
+
const user = await auth.get(event.userId);
|
|
783
|
+
// To the former address: the new one belongs to whoever changed it.
|
|
784
|
+
await mail.emailChanged({ name: user.name, locale: user.locale, formerEmail: event.formerEmail, newEmail: user.email });
|
|
785
|
+
}
|
|
718
786
|
if (event.type === 'user.secondFactorDisabled') {
|
|
719
787
|
const user = await auth.get(event.userId);
|
|
720
788
|
await mail.twoFactorDisabled({ name: user.name, locale: user.locale, email: user.email });
|
|
@@ -733,8 +801,8 @@ async events(event) {
|
|
|
733
801
|
|
|
734
802
|
To post them as signed webhooks:
|
|
735
803
|
[`@nxgt/janus-webhooks`](https://www.npmjs.com/package/@nxgt/janus-webhooks).
|
|
736
|
-
[The user events guide](docs/guide/events.md) has the listener, the
|
|
737
|
-
types, mail for a recovery code used, what a failure costs, and a test.
|
|
804
|
+
[The user events guide](docs/guide/events.md) has the listener, the ten
|
|
805
|
+
types, mail for a recovery code used or a password or e-mail changed, what a failure costs, and a test.
|
|
738
806
|
|
|
739
807
|
### Permissions — `@nxgt/janus/permissions`
|
|
740
808
|
|
|
@@ -853,7 +921,7 @@ describeJanusStores({
|
|
|
853
921
|
});
|
|
854
922
|
```
|
|
855
923
|
|
|
856
|
-
There are
|
|
924
|
+
There are 55 cases. They cover:
|
|
857
925
|
- round-trip, byte for byte — including every edge character the core lets
|
|
858
926
|
through (control characters, U+FFFF, a surrogate pair);
|
|
859
927
|
- uniqueness, as a constraint: of twenty concurrent inserts of one login,
|
|
@@ -866,8 +934,9 @@ There are 54 cases. They cover:
|
|
|
866
934
|
and never brings back a revoked session, even one revoked at the same
|
|
867
935
|
moment;
|
|
868
936
|
- one-time tokens: a token of every kind the port names is stored, counted
|
|
869
|
-
and spent — a `CHECK` or an enum that forgot `stepUp`
|
|
870
|
-
|
|
937
|
+
and spent — a `CHECK` or an enum that forgot `stepUp` or `magicLink` fails
|
|
938
|
+
here — and neither a step-up nor a sign-in link is ever counted, spent nor
|
|
939
|
+
answered as a sign-in code; of twenty
|
|
871
940
|
concurrent redemptions, exactly one succeeds; of twenty concurrent
|
|
872
941
|
`countAttempt` calls, each answers a distinct count, and none is counted
|
|
873
942
|
once a racing redemption spent the token;
|
|
@@ -913,9 +982,9 @@ runner-less layer.
|
|
|
913
982
|
## Traps
|
|
914
983
|
|
|
915
984
|
**Once `secondFactor` is configured, switch on `signIn`'s `status`** — and
|
|
916
|
-
on `signInCode.confirm`'s. A user whose factor is active gets
|
|
985
|
+
on `signInCode.confirm`'s and `magicLink.confirm`'s. A user whose factor is active gets
|
|
917
986
|
`{ status: 'secondFactor', challenge, expiresAt, userId }`, with no `token`
|
|
918
|
-
and no `session`: an e-mailed code proves the e-mail, not the factor.
|
|
987
|
+
and no `session`: an e-mailed code or link proves the e-mail, not the factor.
|
|
919
988
|
`userId` is for your logs and rate limits — answer the visitor the
|
|
920
989
|
challenge alone.
|
|
921
990
|
`if (result.status === 'secondFactor') …` before anything reads them.
|
|
@@ -948,6 +1017,27 @@ address**: at most one code is live per user — a new `request` spends the one
|
|
|
948
1017
|
before — so without a limit anyone who knows an address can fill its inbox,
|
|
949
1018
|
or cancel its owner's code before they type it.
|
|
950
1019
|
|
|
1020
|
+
**Confirm a sign-in link from a `POST`, never from its `GET`.** Mail
|
|
1021
|
+
scanners open every link in an e-mail before the user does: a route that
|
|
1022
|
+
calls `magicLink.confirm` on the link's `GET` spends it for the scanner, and
|
|
1023
|
+
the user's click answers `TOKEN_SPENT`. Link to a page that spends nothing,
|
|
1024
|
+
whose button posts the token — and echo into that page only a token of the
|
|
1025
|
+
token's shape. Refuse that `POST` from another site — check its `Origin`,
|
|
1026
|
+
or `Sec-Fetch-Site: same-origin` — or any page can sign a visitor into an
|
|
1027
|
+
account whose link it holds. Whoever opens the link signs in, on the device
|
|
1028
|
+
that opened it: where the sign-in must complete in the browser that asked,
|
|
1029
|
+
send a code.
|
|
1030
|
+
|
|
1031
|
+
**A link or a code proves an e-mail someone else may have registered.**
|
|
1032
|
+
`signUp` does not wait for the address to be proved, and `signIn` does not
|
|
1033
|
+
ask whether it was: whoever registered the address with a password of theirs
|
|
1034
|
+
keeps that password, and their session, after its owner signs in by link or
|
|
1035
|
+
code and the e-mail turns verified. Where a password signs in before the
|
|
1036
|
+
e-mail is verified, require `verifyEmail` before `signIn` — or, on the
|
|
1037
|
+
`user.emailVerified` those confirmations send, sign the user out everywhere
|
|
1038
|
+
— the listener runs before the new session opens — and have them set a
|
|
1039
|
+
password of their own.
|
|
1040
|
+
|
|
951
1041
|
**Every `janus()` that signs users in needs the same `secondFactor`.** An
|
|
952
1042
|
instance without keys never signs in a user whose factor is active: `signIn`
|
|
953
1043
|
throws a `TypeError` rather than open a session on the password alone. Build
|
|
@@ -1030,8 +1120,24 @@ your policy. A password refused for its length does not spend the token.
|
|
|
1030
1120
|
written — by a link, `changePassword` or `setPassword` — spends every link
|
|
1031
1121
|
still live, so an older e-mail's link answers `TOKEN_SPENT`. Tell the visitor
|
|
1032
1122
|
to use the latest e-mail, and rate-limit `resetPassword.request` per address:
|
|
1033
|
-
each request cancels the link before it.
|
|
1034
|
-
|
|
1123
|
+
each request cancels the link before it.
|
|
1124
|
+
|
|
1125
|
+
**`signIn` throttles each login, not each client.** Ten passwords per login
|
|
1126
|
+
per 15 minutes, then `CREDENTIALS_INVALID` with `retryAfter` until the window
|
|
1127
|
+
ends — the right password too, so tell the visitor to wait `retryAfter`
|
|
1128
|
+
seconds rather than that the password is wrong. `CredentialRefusal` gained
|
|
1129
|
+
`'throttled'`: an exhaustive `switch` over `error.reason` must handle it. A
|
|
1130
|
+
test that tries more than ten wrong passwords at one login over a
|
|
1131
|
+
`fixedClock` is throttled too: advance the clock past `retryAfter`, or wire
|
|
1132
|
+
`signIn: { throttle: false }`. **Somebody who knows a login can keep its
|
|
1133
|
+
password sign-in shut**, ten tries a window; a sign-in code or link, when you wire them, still opens it — the throttle counts passwords, never those.
|
|
1134
|
+
On PostgreSQL, delete lapsed tokens on a schedule: every login tried adds a
|
|
1135
|
+
row per window. One password tried against
|
|
1136
|
+
many logins is not counted: rate-limit `signIn` per client address yourself
|
|
1137
|
+
([passwords](docs/guide/passwords.md#what-you-still-limit-yourself)). The
|
|
1138
|
+
counts live in the tokens store: a flushed or evicting Redis forgets them,
|
|
1139
|
+
and a tokens store that cannot answer fails every password sign-in with
|
|
1140
|
+
`STORE_FAILED`.
|
|
1035
1141
|
|
|
1036
1142
|
**A sign-in can move a user's `version`.** Rewriting a stale hash is a write. A
|
|
1037
1143
|
user object read before that sign-in, and then passed as `ifVersion`, gets
|
|
@@ -1110,7 +1216,7 @@ that sends one, since it is awaited: queue the event and return.
|
|
|
1110
1216
|
|
|
1111
1217
|
## Type safety, counted
|
|
1112
1218
|
|
|
1113
|
-
**One hundred and
|
|
1219
|
+
**One hundred and forty-one plausible mistakes, one hundred and forty-one refused at compile time — and
|
|
1114
1220
|
two gaps, named.**
|
|
1115
1221
|
|
|
1116
1222
|
The lists are typechecked and never run, with one `@ts-expect-error` per
|
|
@@ -1118,9 +1224,10 @@ mistake beside the shapes that must keep compiling. One is a single file:
|
|
|
1118
1224
|
`test/types/refusals.ts` (fifteen, on the shared vocabulary). The other three
|
|
1119
1225
|
are folders with one file per behaviour: `test/types/port/` (twenty-four, on
|
|
1120
1226
|
the identity stores' port, from the point of view of the person implementing
|
|
1121
|
-
it), `test/types/auth/` (
|
|
1227
|
+
it), `test/types/auth/` (fifty-four, on `janus()`, from the point of view of
|
|
1122
1228
|
the application — twelve of them on the second factor, three on sign-in codes,
|
|
1123
|
-
|
|
1229
|
+
five on user events, four on step-ups, six on the sign-in throttle, four on
|
|
1230
|
+
sign-in links) and `test/types/permissions/` (forty-eight, on the
|
|
1124
1231
|
permission model and the questions asked of it). The rule comes from
|
|
1125
1232
|
`nxgt-data`, and so does the reason to distrust the claim without the files:
|
|
1126
1233
|
when it was last measured on `@nxgt/mongo`, *seven of twelve plausible
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
* Gathered here from the files beside this one.
|
|
10
10
|
*/
|
|
11
11
|
export type { PasswordHasher } from './hasher';
|
|
12
|
-
export type { CookieConfig, JanusConfig, MultiTypeConfig, SecondFactorConfig, SingleTypeConfig, } from './janus-config';
|
|
12
|
+
export type { CookieConfig, JanusConfig, MultiTypeConfig, SecondFactorConfig, SignInConfig, SignInThrottleConfig, SingleTypeConfig, } from './janus-config';
|
|
13
13
|
export { type Normalize, normalizeEmail } from './normalize';
|
|
14
14
|
export type { PasswordConfig } from './password-config';
|
|
15
15
|
export { RESERVED_FIELDS, RESERVED_TYPES, SINGLE_TYPE } from './reserved';
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/auth/config/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,YAAY,EAAE,cAAc,EAAE,MAAM,UAAU,CAAC;AAC/C,YAAY,EACX,YAAY,EACZ,WAAW,EACX,eAAe,EACf,kBAAkB,EAClB,gBAAgB,GAChB,MAAM,gBAAgB,CAAC;AACxB,OAAO,EAAE,KAAK,SAAS,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAC7D,YAAY,EAAE,cAAc,EAAE,MAAM,mBAAmB,CAAC;AACxD,OAAO,EAAE,eAAe,EAAE,cAAc,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAC1E,OAAO,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAC;AACjD,YAAY,EAAE,cAAc,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAC;AACtE,YAAY,EACX,UAAU,EACV,aAAa,EACb,UAAU,EACV,cAAc,GACd,MAAM,aAAa,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/auth/config/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,YAAY,EAAE,cAAc,EAAE,MAAM,UAAU,CAAC;AAC/C,YAAY,EACX,YAAY,EACZ,WAAW,EACX,eAAe,EACf,kBAAkB,EAClB,YAAY,EACZ,oBAAoB,EACpB,gBAAgB,GAChB,MAAM,gBAAgB,CAAC;AACxB,OAAO,EAAE,KAAK,SAAS,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAC7D,YAAY,EAAE,cAAc,EAAE,MAAM,mBAAmB,CAAC;AACxD,OAAO,EAAE,eAAe,EAAE,cAAc,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAC1E,OAAO,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAC;AACjD,YAAY,EAAE,cAAc,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAC;AACtE,YAAY,EACX,UAAU,EACV,aAAa,EACb,UAAU,EACV,cAAc,GACd,MAAM,aAAa,CAAC"}
|
|
@@ -48,9 +48,18 @@ interface SharedConfig {
|
|
|
48
48
|
readonly resetPassword?: Duration;
|
|
49
49
|
/** How long an e-mailed sign-in code waits. `'10m'` when absent. */
|
|
50
50
|
readonly signInCode?: Duration;
|
|
51
|
+
/** How long an e-mailed sign-in link waits. `'15m'` when absent. */
|
|
52
|
+
readonly magicLink?: Duration;
|
|
51
53
|
/** How long a step-up's challenge waits, e-mailed or not. `'10m'` when absent. */
|
|
52
54
|
readonly stepUp?: Duration;
|
|
53
55
|
};
|
|
56
|
+
/**
|
|
57
|
+
* How `signIn` answers password guessing. **On by default**: past ten
|
|
58
|
+
* passwords tried at one login in a 15-minute window, `signIn` answers
|
|
59
|
+
* `CREDENTIALS_INVALID` with `retryAfter` — the right password included —
|
|
60
|
+
* until the next window. Nothing locks: the next window signs in.
|
|
61
|
+
*/
|
|
62
|
+
readonly signIn?: SignInConfig;
|
|
54
63
|
/**
|
|
55
64
|
* A TOTP second factor, for every user type with a password. Absent, no
|
|
56
65
|
* `secondFactor` flows exist and `signIn` answers a session directly.
|
|
@@ -66,6 +75,22 @@ interface SharedConfig {
|
|
|
66
75
|
*/
|
|
67
76
|
readonly events?: UserEventListener;
|
|
68
77
|
}
|
|
78
|
+
/** What `signIn` does with password guessing. */
|
|
79
|
+
export interface SignInConfig {
|
|
80
|
+
/**
|
|
81
|
+
* The passwords one login may try per window. `{ attempts: 10, window:
|
|
82
|
+
* '15m' }` when absent; `false` counts nothing — rate-limit `signIn`
|
|
83
|
+
* yourself then.
|
|
84
|
+
*/
|
|
85
|
+
readonly throttle?: SignInThrottleConfig | false;
|
|
86
|
+
}
|
|
87
|
+
/** How many passwords one login may try, and per how long. */
|
|
88
|
+
export interface SignInThrottleConfig {
|
|
89
|
+
/** Passwords tried per login and per window, the right one included. `10` when absent. */
|
|
90
|
+
readonly attempts?: number;
|
|
91
|
+
/** How long one window lasts. `'15m'` when absent. */
|
|
92
|
+
readonly window?: Duration;
|
|
93
|
+
}
|
|
69
94
|
/** What a TOTP second factor needs: a name for the app, and the keys that seal. */
|
|
70
95
|
export interface SecondFactorConfig {
|
|
71
96
|
/** Shown in the authenticator app beside the account: your product's name. */
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"janus-config.d.ts","sourceRoot":"","sources":["../../../src/auth/config/janus-config.ts"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"janus-config.d.ts","sourceRoot":"","sources":["../../../src/auth/config/janus-config.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,8BAA8B,CAAC;AAClE,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,kBAAkB,CAAC;AAC9C,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,qBAAqB,CAAC;AACpD,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,WAAW,CAAC;AACnD,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC;AACjD,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AAC7C,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,UAAU,CAAC;AAC/C,OAAO,KAAK,EAAE,UAAU,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAE9D,2DAA2D;AAC3D,MAAM,WAAW,YAAY;IAC5B,oFAAoF;IACpF,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC;IACzB,yBAAyB;IACzB,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC;IACvB,2BAA2B;IAC3B,QAAQ,CAAC,QAAQ,CAAC,EAAE,KAAK,GAAG,QAAQ,GAAG,MAAM,CAAC;IAC9C,0DAA0D;IAC1D,QAAQ,CAAC,MAAM,CAAC,EAAE,OAAO,CAAC;CAC1B;AAED,UAAU,YAAY;IACrB,iEAAiE;IACjE,QAAQ,CAAC,KAAK,EAAE,WAAW,CAAC;IAC5B;;;;OAIG;IACH,QAAQ,CAAC,SAAS,CAAC,EAAE,aAAa,CAAC;IACnC;;;;OAIG;IACH,QAAQ,CAAC,MAAM,CAAC,EAAE,cAAc,CAAC;IACjC,0EAA0E;IAC1E,QAAQ,CAAC,SAAS,CAAC,EAAE,SAAS,cAAc,EAAE,CAAC;IAC/C,QAAQ,CAAC,KAAK,CAAC,EAAE,KAAK,CAAC;IACvB,QAAQ,CAAC,MAAM,CAAC,EAAE,YAAY,CAAC;IAC/B,QAAQ,CAAC,MAAM,CAAC,EAAE;QACjB,2BAA2B;QAC3B,QAAQ,CAAC,WAAW,CAAC,EAAE,QAAQ,CAAC;QAChC,0BAA0B;QAC1B,QAAQ,CAAC,aAAa,CAAC,EAAE,QAAQ,CAAC;QAClC,oEAAoE;QACpE,QAAQ,CAAC,UAAU,CAAC,EAAE,QAAQ,CAAC;QAC/B,oEAAoE;QACpE,QAAQ,CAAC,SAAS,CAAC,EAAE,QAAQ,CAAC;QAC9B,kFAAkF;QAClF,QAAQ,CAAC,MAAM,CAAC,EAAE,QAAQ,CAAC;KAC3B,CAAC;IACF;;;;;OAKG;IACH,QAAQ,CAAC,MAAM,CAAC,EAAE,YAAY,CAAC;IAC/B;;;OAGG;IACH,QAAQ,CAAC,YAAY,CAAC,EAAE,kBAAkB,CAAC;IAC3C;;;;;;;OAOG;IACH,QAAQ,CAAC,MAAM,CAAC,EAAE,iBAAiB,CAAC;CACpC;AAED,iDAAiD;AACjD,MAAM,WAAW,YAAY;IAC5B;;;;OAIG;IACH,QAAQ,CAAC,QAAQ,CAAC,EAAE,oBAAoB,GAAG,KAAK,CAAC;CACjD;AAED,8DAA8D;AAC9D,MAAM,WAAW,oBAAoB;IACpC,0FAA0F;IAC1F,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAC3B,sDAAsD;IACtD,QAAQ,CAAC,MAAM,CAAC,EAAE,QAAQ,CAAC;CAC3B;AAED,mFAAmF;AACnF,MAAM,WAAW,kBAAkB;IAClC,8EAA8E;IAC9E,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB;;;;OAIG;IACH,QAAQ,CAAC,IAAI,EAAE,SAAS,CAAC,UAAU,EAAE,GAAG,UAAU,EAAE,CAAC,CAAC;IACtD,0EAA0E;IAC1E,QAAQ,CAAC,SAAS,CAAC,EAAE,QAAQ,CAAC;CAC9B;AAED,+DAA+D;AAC/D,MAAM,WAAW,gBAChB,SAAQ,YAAY,EACnB,IAAI,CAAC,cAAc,EAAE,QAAQ,CAAC;IAC/B,QAAQ,CAAC,IAAI,EAAE,UAAU,CAAC;IAC1B,QAAQ,CAAC,KAAK,CAAC,EAAE,KAAK,CAAC;CACvB;AAED,mEAAmE;AACnE,MAAM,WAAW,eAAgB,SAAQ,YAAY;IACpD,QAAQ,CAAC,KAAK,EAAE;QAAE,QAAQ,EAAE,IAAI,EAAE,MAAM,GAAG,cAAc,CAAA;KAAE,CAAC;IAC5D,QAAQ,CAAC,IAAI,CAAC,EAAE,KAAK,CAAC;IACtB,QAAQ,CAAC,QAAQ,CAAC,EAAE,KAAK,CAAC;IAC1B,QAAQ,CAAC,KAAK,CAAC,EAAE,KAAK,CAAC;IACvB,QAAQ,CAAC,OAAO,CAAC,EAAE,KAAK,CAAC;IACzB,QAAQ,CAAC,aAAa,CAAC,EAAE,KAAK,CAAC;CAC/B;AAED,MAAM,MAAM,WAAW,GAAG,gBAAgB,GAAG,eAAe,CAAC"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"resolve-config.d.ts","sourceRoot":"","sources":["../../../src/auth/config/resolve-config.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAGH,OAAO,KAAK,EACX,WAAW,EAGX,MAAM,gBAAgB,CAAC;
|
|
1
|
+
{"version":3,"file":"resolve-config.d.ts","sourceRoot":"","sources":["../../../src/auth/config/resolve-config.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAGH,OAAO,KAAK,EACX,WAAW,EAGX,MAAM,gBAAgB,CAAC;AAOxB,OAAO,KAAK,EAAE,cAAc,EAAgB,MAAM,mBAAmB,CAAC;AAGtE,qFAAqF;AACrF,wBAAgB,aAAa,CAC5B,MAAM,EAAE,WAAW,EACnB,KAAK,EAAE,MAAM,GACX,cAAc,CAwBhB"}
|
|
@@ -0,0 +1,5 @@
|
|
|
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
|
|
@@ -0,0 +1 @@
|
|
|
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"}
|
|
@@ -25,8 +25,14 @@ export interface ResolvedConfig {
|
|
|
25
25
|
readonly verifyEmail: number;
|
|
26
26
|
readonly resetPassword: number;
|
|
27
27
|
readonly signInCode: number;
|
|
28
|
+
readonly magicLink: number;
|
|
28
29
|
readonly stepUp: number;
|
|
29
30
|
};
|
|
31
|
+
/** `null` when `signIn.throttle` is `false`. */
|
|
32
|
+
readonly signInThrottle: {
|
|
33
|
+
readonly attempts: number;
|
|
34
|
+
readonly windowMs: number;
|
|
35
|
+
} | null;
|
|
30
36
|
readonly secondFactor: {
|
|
31
37
|
readonly issuer: string;
|
|
32
38
|
readonly sealer: Sealer;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"resolved-config.d.ts","sourceRoot":"","sources":["../../../src/auth/config/resolved-config.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,YAAY,CAAC;AACzC,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAE9C,oFAAoF;AACpF,MAAM,WAAW,YAAY;IAC5B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,MAAM,EAAE,UAAU,CAAC;IAC5B,QAAQ,CAAC,aAAa,EAAE,MAAM,CAAC;IAC/B,QAAQ,CAAC,QAAQ,EAAE;QAClB,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;QACvB,QAAQ,CAAC,SAAS,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,MAAM,CAAC;QAC9C,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;KAC3B,GAAG,IAAI,CAAC;IACT,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,YAAY,EAAE,MAAM,GAAG,IAAI,CAAC;CACrC;AAED,MAAM,WAAW,cAAc;IAC9B,QAAQ,CAAC,MAAM,EAAE,OAAO,CAAC;IACzB,QAAQ,CAAC,KAAK,EAAE,WAAW,CAAC,MAAM,EAAE,YAAY,CAAC,CAAC;IAClD,QAAQ,CAAC,UAAU,EAAE;QACpB,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;QAC7B,QAAQ,CAAC,aAAa,EAAE,MAAM,CAAC;QAC/B,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;QAC5B,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;KACxB,CAAC;IACF,QAAQ,CAAC,YAAY,EAAE;QACtB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;QACxB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;QACxB,QAAQ,CAAC,cAAc,EAAE,MAAM,CAAC;KAChC,GAAG,IAAI,CAAC;IACT,QAAQ,CAAC,MAAM,EAAE;QAChB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;QACtB,QAAQ,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;QAC/B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;QACtB,QAAQ,CAAC,QAAQ,EAAE,KAAK,GAAG,QAAQ,GAAG,MAAM,CAAC;QAC7C,QAAQ,CAAC,MAAM,EAAE,OAAO,CAAC;KACzB,CAAC;CACF"}
|
|
1
|
+
{"version":3,"file":"resolved-config.d.ts","sourceRoot":"","sources":["../../../src/auth/config/resolved-config.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,YAAY,CAAC;AACzC,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAE9C,oFAAoF;AACpF,MAAM,WAAW,YAAY;IAC5B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,MAAM,EAAE,UAAU,CAAC;IAC5B,QAAQ,CAAC,aAAa,EAAE,MAAM,CAAC;IAC/B,QAAQ,CAAC,QAAQ,EAAE;QAClB,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;QACvB,QAAQ,CAAC,SAAS,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,MAAM,CAAC;QAC9C,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;KAC3B,GAAG,IAAI,CAAC;IACT,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,YAAY,EAAE,MAAM,GAAG,IAAI,CAAC;CACrC;AAED,MAAM,WAAW,cAAc;IAC9B,QAAQ,CAAC,MAAM,EAAE,OAAO,CAAC;IACzB,QAAQ,CAAC,KAAK,EAAE,WAAW,CAAC,MAAM,EAAE,YAAY,CAAC,CAAC;IAClD,QAAQ,CAAC,UAAU,EAAE;QACpB,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;QAC7B,QAAQ,CAAC,aAAa,EAAE,MAAM,CAAC;QAC/B,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;QAC5B,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;QAC3B,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;KACxB,CAAC;IACF,gDAAgD;IAChD,QAAQ,CAAC,cAAc,EAAE;QACxB,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;QAC1B,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;KAC1B,GAAG,IAAI,CAAC;IACT,QAAQ,CAAC,YAAY,EAAE;QACtB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;QACxB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;QACxB,QAAQ,CAAC,cAAc,EAAE,MAAM,CAAC;KAChC,GAAG,IAAI,CAAC;IACT,QAAQ,CAAC,MAAM,EAAE;QAChB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;QACtB,QAAQ,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;QAC/B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;QACtB,QAAQ,CAAC,QAAQ,EAAE,KAAK,GAAG,QAAQ,GAAG,MAAM,CAAC;QAC7C,QAAQ,CAAC,MAAM,EAAE,OAAO,CAAC;KACzB,CAAC;CACF"}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { Sealer } from './sealing';
|
|
2
|
+
/**
|
|
3
|
+
* The key `keyId` lends `purpose` — the first key by default — derived once
|
|
4
|
+
* per key, or `undefined` when the sealer no longer holds that key.
|
|
5
|
+
*/
|
|
6
|
+
export declare function derivedKey(sealer: Sealer, purpose: string, keyId?: string): Buffer | undefined;
|
|
7
|
+
/** HMAC-SHA256 of `parts`, joined by a NUL — which no part may hold. */
|
|
8
|
+
export declare function keyedHash(key: Buffer, parts: readonly string[]): Buffer;
|
|
9
|
+
//# sourceMappingURL=derived-keys.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"derived-keys.d.ts","sourceRoot":"","sources":["../../src/auth/derived-keys.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,WAAW,CAAC;AAWxC;;;GAGG;AACH,wBAAgB,UAAU,CACzB,MAAM,EAAE,MAAM,EACd,OAAO,EAAE,MAAM,EACf,KAAK,GAAE,MAAwB,GAC7B,MAAM,GAAG,SAAS,CAepB;AAED,wEAAwE;AACxE,wBAAgB,SAAS,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,SAAS,MAAM,EAAE,GAAG,MAAM,CAEvE"}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What the two sign-ins by e-mail share — a code typed back
|
|
3
|
+
* (`sign-in-code.ts`) and a link followed (`magic-link.ts`): who may be sent
|
|
4
|
+
* one, keeping only the last one live, and how a redeemed one ends — the
|
|
5
|
+
* e-mail proved, then the same session or second-factor challenge a
|
|
6
|
+
* password opens.
|
|
7
|
+
*/
|
|
8
|
+
import type { ResolvedType } from './config';
|
|
9
|
+
import { type AnyUser, type Context } from './context';
|
|
10
|
+
import type { TokenKind, TokenRecord, UserRecord } from './port/types';
|
|
11
|
+
import type { SignInResult } from './types';
|
|
12
|
+
/** What a sign-in answers once the user proved who they are: `secondFactor.finish`. */
|
|
13
|
+
export type Finish = (record: UserRecord, where: string) => Promise<SignInResult<AnyUser>>;
|
|
14
|
+
/**
|
|
15
|
+
* The user of this type holding `email`, when they may be sent a sign-in:
|
|
16
|
+
* `null` for nobody, and for an inactive user — the same answer, so the
|
|
17
|
+
* caller cannot tell them apart either.
|
|
18
|
+
*/
|
|
19
|
+
export declare function signInHolder(context: Context, type: ResolvedType, email: string): Promise<UserRecord | null>;
|
|
20
|
+
/**
|
|
21
|
+
* Spends every other unspent token of this kind the user holds, sparing the
|
|
22
|
+
* one just issued. Issued first, spent after, so requests that race leave
|
|
23
|
+
* at most one live — maybe none, and the visitor asks again — never one each.
|
|
24
|
+
*/
|
|
25
|
+
export declare function keepOnlyLatest(context: Context, userId: string, kind: TokenKind, secret: string): Promise<void>;
|
|
26
|
+
/**
|
|
27
|
+
* Ends a sign-in by e-mail whose token is spent: refuses an e-mail changed
|
|
28
|
+
* since it was sent and an inactive user, proves the e-mail — it reached the
|
|
29
|
+
* inbox — and finishes as a password would.
|
|
30
|
+
*
|
|
31
|
+
* The proof is written under the version read, so an address changed since
|
|
32
|
+
* is not the one proved: a `VERSION_CONFLICT` then, with the token spent.
|
|
33
|
+
*/
|
|
34
|
+
export declare function finishEmailSignIn(context: Context, type: ResolvedType, finish: Finish, signIn: {
|
|
35
|
+
readonly user: UserRecord;
|
|
36
|
+
readonly token: TokenRecord;
|
|
37
|
+
readonly where: string;
|
|
38
|
+
readonly noun: 'token' | 'code';
|
|
39
|
+
}): Promise<SignInResult<AnyUser>>;
|
|
40
|
+
//# sourceMappingURL=email-sign-in.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"email-sign-in.d.ts","sourceRoot":"","sources":["../../src/auth/email-sign-in.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAGH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,UAAU,CAAC;AAC7C,OAAO,EACN,KAAK,OAAO,EACZ,KAAK,OAAO,EAGZ,MAAM,WAAW,CAAC;AAGnB,OAAO,KAAK,EAAE,SAAS,EAAE,WAAW,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AAEvE,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AAE5C,uFAAuF;AACvF,MAAM,MAAM,MAAM,GAAG,CACpB,MAAM,EAAE,UAAU,EAClB,KAAK,EAAE,MAAM,KACT,OAAO,CAAC,YAAY,CAAC,OAAO,CAAC,CAAC,CAAC;AAEpC;;;;GAIG;AACH,wBAAsB,YAAY,CACjC,OAAO,EAAE,OAAO,EAChB,IAAI,EAAE,YAAY,EAClB,KAAK,EAAE,MAAM,GACX,OAAO,CAAC,UAAU,GAAG,IAAI,CAAC,CAG5B;AAED;;;;GAIG;AACH,wBAAsB,cAAc,CACnC,OAAO,EAAE,OAAO,EAChB,MAAM,EAAE,MAAM,EACd,IAAI,EAAE,SAAS,EACf,MAAM,EAAE,MAAM,GACZ,OAAO,CAAC,IAAI,CAAC,CAOf;AAED;;;;;;;GAOG;AACH,wBAAsB,iBAAiB,CACtC,OAAO,EAAE,OAAO,EAChB,IAAI,EAAE,YAAY,EAClB,MAAM,EAAE,MAAM,EACd,MAAM,EAAE;IACP,QAAQ,CAAC,IAAI,EAAE,UAAU,CAAC;IAC1B,QAAQ,CAAC,KAAK,EAAE,WAAW,CAAC;IAC5B,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,IAAI,EAAE,OAAO,GAAG,MAAM,CAAC;CAChC,GACC,OAAO,CAAC,YAAY,CAAC,OAAO,CAAC,CAAC,CAqBhC"}
|
package/dist/auth/events.d.ts
CHANGED
|
@@ -5,9 +5,15 @@ import type { Context } from './context';
|
|
|
5
5
|
*
|
|
6
6
|
* - `user.created` — by `create` or `signUp`;
|
|
7
7
|
* - `user.emailVerified` — by `verifyEmail.confirm`, or by the link of
|
|
8
|
-
* `resetPassword.confirm` or
|
|
9
|
-
* the e-mail too; never for an e-mail
|
|
8
|
+
* `resetPassword.confirm` or `magicLink.confirm`, or the code of
|
|
9
|
+
* `signInCode.confirm`, which prove the e-mail too; never for an e-mail
|
|
10
|
+
* already verified;
|
|
10
11
|
* - `user.passwordReset` — by `resetPassword.confirm`;
|
|
12
|
+
* - `user.passwordChanged` — by `changePassword` and `setPassword`; never by a
|
|
13
|
+
* reset, which is `user.passwordReset` alone;
|
|
14
|
+
* - `user.emailChanged` — by `update`, when it changed the e-mail — compared
|
|
15
|
+
* normalised, the same test that makes the new one unverified. The one
|
|
16
|
+
* event that carries more than the user: `formerEmail`;
|
|
11
17
|
* - `user.secondFactorEnabled` — by `secondFactor.activate`, once the factor
|
|
12
18
|
* is active: not by `enroll`, which leaves it waiting for its first code;
|
|
13
19
|
* - `user.secondFactorDisabled` — by `secondFactor.disable`, when it removed
|
|
@@ -19,11 +25,12 @@ import type { Context } from './context';
|
|
|
19
25
|
* spent: a sign-in without the user's phone;
|
|
20
26
|
* - `user.deleted` — by `delete`, once; a replay that finds nobody is none.
|
|
21
27
|
*/
|
|
22
|
-
export type UserEventType = 'user.created' | 'user.emailVerified' | 'user.passwordReset' | 'user.secondFactorEnabled' | 'user.secondFactorDisabled' | 'user.recoveryCodesRegenerated' | 'user.recoveryCodeUsed' | 'user.deleted';
|
|
28
|
+
export type UserEventType = 'user.created' | 'user.emailVerified' | 'user.passwordReset' | 'user.passwordChanged' | 'user.emailChanged' | 'user.secondFactorEnabled' | 'user.secondFactorDisabled' | 'user.recoveryCodesRegenerated' | 'user.recoveryCodeUsed' | 'user.deleted';
|
|
23
29
|
/**
|
|
24
30
|
* A user event: **the user named by id, and nothing else** — no login, no
|
|
25
|
-
*
|
|
26
|
-
*
|
|
31
|
+
* field, no password, no token. Whoever receives it reads the rest from where
|
|
32
|
+
* it is kept, if they may. One exception, `formerEmail` on
|
|
33
|
+
* `user.emailChanged`: once the write landed, the old address is kept nowhere.
|
|
27
34
|
*/
|
|
28
35
|
export interface UserEvent {
|
|
29
36
|
/** A UUIDv7 minted for this event: the key to deliver it once. */
|
|
@@ -36,7 +43,16 @@ export interface UserEvent {
|
|
|
36
43
|
readonly occurredAt: Date;
|
|
37
44
|
readonly userId: Id;
|
|
38
45
|
readonly userType: string;
|
|
46
|
+
/**
|
|
47
|
+
* On `user.emailChanged` only: the address the user had before, as it was
|
|
48
|
+
* stored — `null` when they had none. So a notice can reach the inbox the
|
|
49
|
+
* account was just taken from; the new address is on the user. Absent
|
|
50
|
+
* from every other type, and from an event a webhook delivered.
|
|
51
|
+
*/
|
|
52
|
+
readonly formerEmail?: string | null;
|
|
39
53
|
}
|
|
54
|
+
/** What an event carries beyond the user, on the one type that does. */
|
|
55
|
+
export type EventExtras = Pick<UserEvent, 'formerEmail'>;
|
|
40
56
|
/**
|
|
41
57
|
* What `janus({ events })` takes: called once per event, **after** the write
|
|
42
58
|
* landed and before the flow answers — awaited, so a queue that stores the
|
|
@@ -57,5 +73,5 @@ export declare function resolveEvents(events: unknown, where: string): UserEvent
|
|
|
57
73
|
export declare function emit(context: Context, type: UserEventType, user: {
|
|
58
74
|
readonly id: Id;
|
|
59
75
|
readonly type: string;
|
|
60
|
-
}, occurredAt: Date): Promise<void>;
|
|
76
|
+
}, occurredAt: Date, extras?: EventExtras): Promise<void>;
|
|
61
77
|
//# sourceMappingURL=events.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"events.d.ts","sourceRoot":"","sources":["../../src/auth/events.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,EAAE,EAAU,MAAM,WAAW,CAAC;AAC5C,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAEzC
|
|
1
|
+
{"version":3,"file":"events.d.ts","sourceRoot":"","sources":["../../src/auth/events.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,EAAE,EAAU,MAAM,WAAW,CAAC;AAC5C,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAEzC;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,MAAM,MAAM,aAAa,GACtB,cAAc,GACd,oBAAoB,GACpB,oBAAoB,GACpB,sBAAsB,GACtB,mBAAmB,GACnB,0BAA0B,GAC1B,2BAA2B,GAC3B,+BAA+B,GAC/B,uBAAuB,GACvB,cAAc,CAAC;AAElB;;;;;GAKG;AACH,MAAM,WAAW,SAAS;IACzB,kEAAkE;IAClE,QAAQ,CAAC,EAAE,EAAE,EAAE,CAAC;IAChB,QAAQ,CAAC,IAAI,EAAE,aAAa,CAAC;IAC7B;;;OAGG;IACH,QAAQ,CAAC,UAAU,EAAE,IAAI,CAAC;IAC1B,QAAQ,CAAC,MAAM,EAAE,EAAE,CAAC;IACpB,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B;;;;;OAKG;IACH,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CACrC;AAED,wEAAwE;AACxE,MAAM,MAAM,WAAW,GAAG,IAAI,CAAC,SAAS,EAAE,aAAa,CAAC,CAAC;AAEzD;;;;;;GAMG;AACH,MAAM,MAAM,iBAAiB,GAAG,CAAC,KAAK,EAAE,SAAS,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;AAE3E,oFAAoF;AACpF,wBAAgB,aAAa,CAC5B,MAAM,EAAE,OAAO,EACf,KAAK,EAAE,MAAM,GACX,iBAAiB,GAAG,IAAI,CAQ1B;AAED;;;;;;GAMG;AACH,wBAAsB,IAAI,CACzB,OAAO,EAAE,OAAO,EAChB,IAAI,EAAE,aAAa,EACnB,IAAI,EAAE;IAAE,QAAQ,CAAC,EAAE,EAAE,EAAE,CAAC;IAAC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;CAAE,EAChD,UAAU,EAAE,IAAI,EAChB,MAAM,GAAE,WAAgB,GACtB,OAAO,CAAC,IAAI,CAAC,CAqBf"}
|
package/dist/auth/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export type { CookieConfig, FieldsJson, JanusConfig, MultiTypeConfig, Normalize, PasswordConfig, PasswordHasher, SecondFactorConfig, SessionConfig, SingleTypeConfig, UserSchema, UserTypeConfig, } from './config';
|
|
1
|
+
export type { CookieConfig, FieldsJson, JanusConfig, MultiTypeConfig, Normalize, PasswordConfig, PasswordHasher, SecondFactorConfig, SessionConfig, SignInConfig, SignInThrottleConfig, SingleTypeConfig, UserSchema, UserTypeConfig, } from './config';
|
|
2
2
|
export type { UserEvent, UserEventListener, UserEventType, } from './events';
|
|
3
3
|
export { bunHasher, scryptHasher } from './hashers';
|
|
4
4
|
export { janus } from './janus';
|
|
@@ -8,5 +8,5 @@ export type { JanusStores, Json, JsonObject, PasswordRecord, SecondFactorRecord,
|
|
|
8
8
|
export type { SealingKey } from './sealing';
|
|
9
9
|
export { assertFresh } from './sessions/fresh';
|
|
10
10
|
export type { StandardSchemaV1 } from './standard-schema';
|
|
11
|
-
export type { Authenticated, Checked, EmailOf, HeaderRecord, IssuedCode, IssuedToken, Janus, LoginOf, PasswordApi, RecoveredSignIn, RecoveryCodesIssued, RequestLike, RequiredStringKeys, ResetPasswordApi, SecondFactorApi, SecondFactorEnrolment, SecondFactorRequired, Session, SharedApi, SignedIn, SignInCodeApi, SignInResult, StepUpApi, StepUpByApp, StepUpByEmail, TypeApi, TypesOf, User, UserBase, UserOf, UserRef, UserTypeApi, VerifyEmailApi, WriteOptions, } from './types';
|
|
11
|
+
export type { Authenticated, Checked, EmailOf, HeaderRecord, IssuedCode, IssuedToken, Janus, LoginOf, MagicLinkApi, PasswordApi, RecoveredSignIn, RecoveryCodesIssued, RequestLike, RequiredStringKeys, ResetPasswordApi, SecondFactorApi, SecondFactorEnrolment, SecondFactorRequired, Session, SharedApi, SignedIn, SignInCodeApi, SignInResult, StepUpApi, StepUpByApp, StepUpByEmail, TypeApi, TypesOf, User, UserBase, UserOf, UserRef, UserTypeApi, VerifyEmailApi, WriteOptions, } from './types';
|
|
12
12
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/auth/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/auth/index.ts"],"names":[],"mappings":"AAAA,YAAY,EACX,YAAY,EACZ,UAAU,EACV,WAAW,EACX,eAAe,EACf,SAAS,EACT,cAAc,EACd,cAAc,EACd,kBAAkB,EAClB,aAAa,EACb,gBAAgB,EAChB,UAAU,EACV,cAAc,GACd,MAAM,UAAU,CAAC;AAClB,YAAY,EACX,SAAS,EACT,iBAAiB,EACjB,aAAa,GACb,MAAM,UAAU,CAAC;AAClB,OAAO,EAAE,SAAS,EAAE,YAAY,EAAE,MAAM,WAAW,CAAC;AACpD,OAAO,EAAE,KAAK,EAAE,MAAM,SAAS,CAAC;AAChC,OAAO,EAAE,YAAY,EAAE,MAAM,sBAAsB,CAAC;AACpD,OAAO,EAAE,kBAAkB,EAAE,MAAM,eAAe,CAAC;AACnD,YAAY,EACX,WAAW,EACX,IAAI,EACJ,UAAU,EACV,cAAc,EACd,kBAAkB,EAClB,SAAS,EACT,aAAa,EACb,YAAY,EACZ,iBAAiB,EACjB,SAAS,EACT,WAAW,EACX,UAAU,EACV,eAAe,EACf,SAAS,EACT,UAAU,EACV,SAAS,GACT,MAAM,cAAc,CAAC;AACtB,YAAY,EAAE,UAAU,EAAE,MAAM,WAAW,CAAC;AAC5C,OAAO,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAC;AAC/C,YAAY,EAAE,gBAAgB,EAAE,MAAM,mBAAmB,CAAC;AAC1D,YAAY,EACX,aAAa,EACb,OAAO,EACP,OAAO,EACP,YAAY,EACZ,UAAU,EACV,WAAW,EACX,KAAK,EACL,OAAO,EACP,WAAW,EACX,eAAe,EACf,mBAAmB,EACnB,WAAW,EACX,kBAAkB,EAClB,gBAAgB,EAChB,eAAe,EACf,qBAAqB,EACrB,oBAAoB,EACpB,OAAO,EACP,SAAS,EACT,QAAQ,EACR,aAAa,EACb,YAAY,EACZ,SAAS,EACT,WAAW,EACX,aAAa,EACb,OAAO,EACP,OAAO,EACP,IAAI,EACJ,QAAQ,EACR,MAAM,EACN,OAAO,EACP,WAAW,EACX,cAAc,EACd,YAAY,GACZ,MAAM,SAAS,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/auth/index.ts"],"names":[],"mappings":"AAAA,YAAY,EACX,YAAY,EACZ,UAAU,EACV,WAAW,EACX,eAAe,EACf,SAAS,EACT,cAAc,EACd,cAAc,EACd,kBAAkB,EAClB,aAAa,EACb,YAAY,EACZ,oBAAoB,EACpB,gBAAgB,EAChB,UAAU,EACV,cAAc,GACd,MAAM,UAAU,CAAC;AAClB,YAAY,EACX,SAAS,EACT,iBAAiB,EACjB,aAAa,GACb,MAAM,UAAU,CAAC;AAClB,OAAO,EAAE,SAAS,EAAE,YAAY,EAAE,MAAM,WAAW,CAAC;AACpD,OAAO,EAAE,KAAK,EAAE,MAAM,SAAS,CAAC;AAChC,OAAO,EAAE,YAAY,EAAE,MAAM,sBAAsB,CAAC;AACpD,OAAO,EAAE,kBAAkB,EAAE,MAAM,eAAe,CAAC;AACnD,YAAY,EACX,WAAW,EACX,IAAI,EACJ,UAAU,EACV,cAAc,EACd,kBAAkB,EAClB,SAAS,EACT,aAAa,EACb,YAAY,EACZ,iBAAiB,EACjB,SAAS,EACT,WAAW,EACX,UAAU,EACV,eAAe,EACf,SAAS,EACT,UAAU,EACV,SAAS,GACT,MAAM,cAAc,CAAC;AACtB,YAAY,EAAE,UAAU,EAAE,MAAM,WAAW,CAAC;AAC5C,OAAO,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAC;AAC/C,YAAY,EAAE,gBAAgB,EAAE,MAAM,mBAAmB,CAAC;AAC1D,YAAY,EACX,aAAa,EACb,OAAO,EACP,OAAO,EACP,YAAY,EACZ,UAAU,EACV,WAAW,EACX,KAAK,EACL,OAAO,EACP,YAAY,EACZ,WAAW,EACX,eAAe,EACf,mBAAmB,EACnB,WAAW,EACX,kBAAkB,EAClB,gBAAgB,EAChB,eAAe,EACf,qBAAqB,EACrB,oBAAoB,EACpB,OAAO,EACP,SAAS,EACT,QAAQ,EACR,aAAa,EACb,YAAY,EACZ,SAAS,EACT,WAAW,EACX,aAAa,EACb,OAAO,EACP,OAAO,EACP,IAAI,EACJ,QAAQ,EACR,MAAM,EACN,OAAO,EACP,WAAW,EACX,cAAc,EACd,YAAY,GACZ,MAAM,SAAS,CAAC"}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { At } from './at';
|
|
2
|
+
import type { ResolvedType } from './config';
|
|
3
|
+
import { type AnyUser, type Context } from './context';
|
|
4
|
+
import { type Finish } from './email-sign-in';
|
|
5
|
+
import type { MagicLinkApi, SignInResult } from './types';
|
|
6
|
+
/**
|
|
7
|
+
* Signing in with a link sent to the user's e-mail: no password, and the
|
|
8
|
+
* link proves the address — a sign-in code with nothing to type.
|
|
9
|
+
*
|
|
10
|
+
* The token is 32 random bytes, so there is nothing to guess and no attempt
|
|
11
|
+
* to count: redeeming it is one conditional write, and of two redemptions
|
|
12
|
+
* at once exactly one signs in. **At most one link is live per user**, as
|
|
13
|
+
* for a sign-in code: `request` spends every other once it issued its own.
|
|
14
|
+
* A token of its own kind, `magicLink`, so a sign-in code's challenge — which
|
|
15
|
+
* the visitor holds — is never a link, nor a link a challenge.
|
|
16
|
+
*
|
|
17
|
+
* Whoever opens the link signs in, in the browser that opened it: the
|
|
18
|
+
* application confirms it from a `POST`, never from the `GET` of the link,
|
|
19
|
+
* which a mail scanner follows too.
|
|
20
|
+
*/
|
|
21
|
+
export declare function magicLinkFlows(context: Context, type: ResolvedType, at: At, finish: Finish): MagicLinkApi<AnyUser, SignInResult<AnyUser>>['magicLink'];
|
|
22
|
+
//# sourceMappingURL=magic-link.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"magic-link.d.ts","sourceRoot":"","sources":["../../src/auth/magic-link.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,EAAE,EAAE,MAAM,MAAM,CAAC;AAC/B,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,UAAU,CAAC;AAC7C,OAAO,EAAE,KAAK,OAAO,EAAE,KAAK,OAAO,EAAU,MAAM,WAAW,CAAC;AAE/D,OAAO,EACN,KAAK,MAAM,EAIX,MAAM,iBAAiB,CAAC;AAEzB,OAAO,KAAK,EAAe,YAAY,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AAEvE;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,cAAc,CAC7B,OAAO,EAAE,OAAO,EAChB,IAAI,EAAE,YAAY,EAClB,EAAE,EAAE,EAAE,EACN,MAAM,EAAE,MAAM,GACZ,YAAY,CAAC,OAAO,EAAE,YAAY,CAAC,OAAO,CAAC,CAAC,CAAC,WAAW,CAAC,CAgB3D"}
|
|
@@ -2,7 +2,8 @@
|
|
|
2
2
|
* What every one-time token shares — an e-mail link, a sign-in code, a
|
|
3
3
|
* second-factor challenge — kept in one place so they cannot drift apart:
|
|
4
4
|
* issuing it (`issue.ts`), spending it (`spend.ts`), refusing it
|
|
5
|
-
* (`refusals.ts`)
|
|
5
|
+
* (`refusals.ts`), the code it may carry (`codes.ts`) and the attempts a
|
|
6
|
+
* window counts in tokens of its own (`window.ts`).
|
|
6
7
|
*
|
|
7
8
|
* Gathered here from the files beside this one.
|
|
8
9
|
*/
|
|
@@ -10,4 +11,5 @@ export { CODE_ATTEMPTS, codeInvalid, codeMatches, countCodeAttempt, hashCode, un
|
|
|
10
11
|
export { issueCode, issueOneTime } from './issue';
|
|
11
12
|
export { type OneTimeNoun, refuseStale, refuseUnusable, unknownOneTime, } from './refusals';
|
|
12
13
|
export { burnOneTime, spendOneTime } from './spend';
|
|
14
|
+
export { type AttemptWindow, type CountedAttempt, countInWindow, MAX_LINKS, } from './window';
|
|
13
15
|
//# sourceMappingURL=index.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/auth/one-time/index.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/auth/one-time/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,EACN,aAAa,EACb,WAAW,EACX,WAAW,EACX,gBAAgB,EAChB,QAAQ,EACR,gBAAgB,GAChB,MAAM,SAAS,CAAC;AACjB,OAAO,EAAE,SAAS,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AAClD,OAAO,EACN,KAAK,WAAW,EAChB,WAAW,EACX,cAAc,EACd,cAAc,GACd,MAAM,YAAY,CAAC;AACpB,OAAO,EAAE,WAAW,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AACpD,OAAO,EACN,KAAK,aAAa,EAClB,KAAK,cAAc,EACnB,aAAa,EACb,SAAS,GACT,MAAM,UAAU,CAAC"}
|