@nxgt/janus 0.12.0 → 0.14.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (73) hide show
  1. package/README.md +62 -13
  2. package/dist/auth/config/index.d.ts +1 -1
  3. package/dist/auth/config/index.d.ts.map +1 -1
  4. package/dist/auth/config/janus-config.d.ts +23 -0
  5. package/dist/auth/config/janus-config.d.ts.map +1 -1
  6. package/dist/auth/config/resolve-config.d.ts.map +1 -1
  7. package/dist/auth/config/resolve-sign-in.d.ts +5 -0
  8. package/dist/auth/config/resolve-sign-in.d.ts.map +1 -0
  9. package/dist/auth/config/resolved-config.d.ts +5 -0
  10. package/dist/auth/config/resolved-config.d.ts.map +1 -1
  11. package/dist/auth/derived-keys.d.ts +9 -0
  12. package/dist/auth/derived-keys.d.ts.map +1 -0
  13. package/dist/auth/email-flows/reset-password.d.ts.map +1 -1
  14. package/dist/auth/events.d.ts +19 -4
  15. package/dist/auth/events.d.ts.map +1 -1
  16. package/dist/auth/index.d.ts +1 -1
  17. package/dist/auth/index.d.ts.map +1 -1
  18. package/dist/auth/one-time/index.d.ts +3 -1
  19. package/dist/auth/one-time/index.d.ts.map +1 -1
  20. package/dist/auth/one-time/window.d.ts +50 -0
  21. package/dist/auth/one-time/window.d.ts.map +1 -0
  22. package/dist/auth/password-written.d.ts +28 -5
  23. package/dist/auth/password-written.d.ts.map +1 -1
  24. package/dist/auth/port/types/tokens.d.ts +3 -2
  25. package/dist/auth/port/types/tokens.d.ts.map +1 -1
  26. package/dist/auth/second-factor/app-code-attempts.d.ts +5 -8
  27. package/dist/auth/second-factor/app-code-attempts.d.ts.map +1 -1
  28. package/dist/auth/second-factor/challenge.d.ts.map +1 -1
  29. package/dist/auth/second-factor/recovery-codes.d.ts.map +1 -1
  30. package/dist/auth/second-factor/recovery.d.ts.map +1 -1
  31. package/dist/auth/sign-in-attempts.d.ts +43 -0
  32. package/dist/auth/sign-in-attempts.d.ts.map +1 -0
  33. package/dist/auth/types/email-flows.d.ts +6 -3
  34. package/dist/auth/types/email-flows.d.ts.map +1 -1
  35. package/dist/auth/types/password.d.ts +8 -0
  36. package/dist/auth/types/password.d.ts.map +1 -1
  37. package/dist/auth/users/fields-patch.d.ts +8 -2
  38. package/dist/auth/users/fields-patch.d.ts.map +1 -1
  39. package/dist/auth/users/record-flows.d.ts.map +1 -1
  40. package/dist/auth/users/sign-in.d.ts +2 -2
  41. package/dist/auth/users/sign-in.d.ts.map +1 -1
  42. package/dist/auth/users/update.d.ts +12 -0
  43. package/dist/auth/users/update.d.ts.map +1 -0
  44. package/dist/chunks/{index-km7ffctx.js → index-6c9fbtv2.js} +2 -2
  45. package/dist/chunks/{index-rd2rddvv.js → index-baazjyvc.js} +2 -2
  46. package/dist/chunks/{index-j2gd8fry.js → index-hnwggyxs.js} +3 -2
  47. package/dist/chunks/{index-j2gd8fry.js.map → index-hnwggyxs.js.map} +3 -3
  48. package/dist/chunks/{index-jp2kka6d.js → index-kj3st57z.js} +2 -2
  49. package/dist/conformance/index.js +3 -3
  50. package/dist/errors/janus-error/base.d.ts +1 -0
  51. package/dist/errors/janus-error/base.d.ts.map +1 -1
  52. package/dist/errors/janus-error/codes.d.ts +3 -1
  53. package/dist/errors/janus-error/codes.d.ts.map +1 -1
  54. package/dist/errors/janus-error/options.d.ts +9 -2
  55. package/dist/errors/janus-error/options.d.ts.map +1 -1
  56. package/dist/index.js +234 -89
  57. package/dist/index.js.map +21 -16
  58. package/dist/permissions/index.js +3 -3
  59. package/docs/README.md +2 -2
  60. package/docs/guide/adapters.md +3 -2
  61. package/docs/guide/email-flows.md +40 -1
  62. package/docs/guide/errors.md +10 -2
  63. package/docs/guide/events.md +74 -13
  64. package/docs/guide/passwords.md +123 -3
  65. package/docs/guide/second-factor.md +4 -2
  66. package/docs/guide/users.md +16 -7
  67. package/docs/guide/vocabulary.md +2 -2
  68. package/docs/roadmap.md +34 -26
  69. package/docs/troubleshooting.md +66 -3
  70. package/package.json +1 -1
  71. /package/dist/chunks/{index-km7ffctx.js.map → index-6c9fbtv2.js.map} +0 -0
  72. /package/dist/chunks/{index-rd2rddvv.js.map → index-baazjyvc.js.map} +0 -0
  73. /package/dist/chunks/{index-jp2kka6d.js.map → index-kj3st57z.js.map} +0 -0
package/README.md CHANGED
@@ -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 and a wrong one |
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 |
@@ -384,6 +384,20 @@ else reaches the store and is asynchronous.
384
384
  `findUser` and `getUser` across types, `cookie.serialize(token, session)` and
385
385
  `cookie.clear()` — `HttpOnly; SameSite=Lax; Secure` unless you say otherwise
386
386
  — and `collectExpired`.
387
+ - **Password guessing is throttled**, on by default: past ten passwords
388
+ tried at one login in a 15-minute window, `signIn` refuses every one — the
389
+ right password included — with `CREDENTIALS_INVALID`, `reason:
390
+ 'throttled'` and `retryAfter`, the seconds until the next window. Nothing
391
+ locks, a login nobody holds is counted alike, and a password sign-in that
392
+ opens a session — after its second factor, when one is active — starts
393
+ the count again. `signIn: { throttle: { attempts, window } }`
394
+ changes it, `signIn: { throttle: false }` turns it off; a store that
395
+ cannot count throws `STORE_FAILED`
396
+ ([passwords](docs/guide/passwords.md#password-guessing-is-throttled)).
397
+
398
+ ```ts
399
+ janus({ user, password: { login: 'email' }, store, hasher, signIn: { throttle: { attempts: 5, window: '1h' } } });
400
+ ```
387
401
  - **Sessions** last `'7d'` and slide: `authenticate` renews one once `renewAfter`
388
402
  (`'1d'`) has passed, writing at most once per period, and says so with
389
403
  `renewed`. The token is handed back once; the store only holds its `sha256`.
@@ -687,34 +701,45 @@ await auth.signUp({ email, password }); // the listener has the event before thi
687
701
  | `user.created` | `create`, `signUp` |
688
702
  | `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 |
689
703
  | `user.passwordReset` | `resetPassword.confirm` |
704
+ | `user.passwordChanged` | `changePassword`, `setPassword` — never a reset, which is `user.passwordReset` alone |
705
+ | `user.emailChanged` | `update`, when it changed the e-mail — carrying `formerEmail`, the address before (`null` for none) |
690
706
  | `user.secondFactorEnabled` | `secondFactor.activate`, once the factor is active — not `enroll`, which leaves it waiting |
691
707
  | `user.secondFactorDisabled` | `secondFactor.disable`, when it removed an active factor — never for a user who had none, or one still waiting |
692
708
  | `user.recoveryCodesRegenerated` | `secondFactor.regenerateRecoveryCodes` — not `activate`, whose codes come with `user.secondFactorEnabled` |
693
709
  | `user.recoveryCodeUsed` | `secondFactor.recover`, once the recovery code is spent: a sign-in without the user's phone |
694
710
  | `user.deleted` | `delete`, once — a replay that deletes nobody sends nothing |
695
711
 
696
- - **The user is named by id, and nothing else**: no login, no e-mail, no
697
- field, no password, no token. Whoever receives the event reads the rest
698
- from where it is kept, if they may.
712
+ - **The user is named by id, and nothing else**: no login, no field, no
713
+ password, no token. Whoever receives the event reads the rest from where it
714
+ is kept, if they may. One exception: `user.emailChanged` carries
715
+ `formerEmail`, which nothing keeps once the write landed, so a notice can
716
+ reach the inbox the account just left. `@nxgt/janus-webhooks` never posts
717
+ it.
699
718
  - **Each event has an `id` of its own**, a UUIDv7: the key to deliver it once.
700
719
  - **The listener runs after the write, and is awaited** before the
701
720
  flow answers, so a durable queue has the event by then. `occurredAt` is the
702
721
  write's own time. A refused flow sends nothing.
703
722
  - **Typed**: `events` is a `UserEventListener`; `UserEventType` is the closed
704
- union of the eight types, so a `switch` on `event.type` is exhaustive — and
705
- a new type, like the two recovery-code ones in 0.10, breaks it until handled.
723
+ union of the ten types, so a `switch` on `event.type` is exhaustive — and
724
+ a new type, like the two recovery-code ones in 0.10 or the two change ones
725
+ in 0.14, breaks it until handled.
706
726
  - **A listener that throws fails no flow** — the write happened. It is a
707
727
  `JANUS_EVENT_FAILED` warning naming the event's type, its id and the user's
708
728
  id, never the failure's message.
709
729
 
710
- To tell the user their second factor was turned on or off, or that a
711
- recovery code was used, send
730
+ To tell the user their password or e-mail changed, their second factor was
731
+ turned on or off, or that a recovery code was used, send
712
732
  [`@nxgt/janus-mail`](https://www.npmjs.com/package/@nxgt/janus-mail)'s notice
713
733
  from the listener — or from a queue it feeds:
714
734
 
715
735
  ```ts
716
736
  // `mail` is janusMail({ … }), and the user schema holds a name and a locale.
717
737
  async events(event) {
738
+ if (event.type === 'user.emailChanged' && event.formerEmail != null) {
739
+ const user = await auth.get(event.userId);
740
+ // To the former address: the new one belongs to whoever changed it.
741
+ await mail.emailChanged({ name: user.name, locale: user.locale, formerEmail: event.formerEmail, newEmail: user.email });
742
+ }
718
743
  if (event.type === 'user.secondFactorDisabled') {
719
744
  const user = await auth.get(event.userId);
720
745
  await mail.twoFactorDisabled({ name: user.name, locale: user.locale, email: user.email });
@@ -733,8 +758,8 @@ async events(event) {
733
758
 
734
759
  To post them as signed webhooks:
735
760
  [`@nxgt/janus-webhooks`](https://www.npmjs.com/package/@nxgt/janus-webhooks).
736
- [The user events guide](docs/guide/events.md) has the listener, the eight
737
- types, mail for a recovery code used, what a failure costs, and a test.
761
+ [The user events guide](docs/guide/events.md) has the listener, the ten
762
+ types, mail for a recovery code used or a password or e-mail changed, what a failure costs, and a test.
738
763
 
739
764
  ### Permissions — `@nxgt/janus/permissions`
740
765
 
@@ -1025,6 +1050,30 @@ waiting on its second factor is spent with them — as it is by `setPassword`
1025
1050
  and `changePassword`; what the visitor does next is
1026
1051
  your policy. A password refused for its length does not spend the token.
1027
1052
 
1053
+ **Only the last reset link works, and only until the password changes.** A
1054
+ `resetPassword.request` spends the user's earlier links, and any password
1055
+ written — by a link, `changePassword` or `setPassword` — spends every link
1056
+ still live, so an older e-mail's link answers `TOKEN_SPENT`. Tell the visitor
1057
+ to use the latest e-mail, and rate-limit `resetPassword.request` per address:
1058
+ each request cancels the link before it.
1059
+
1060
+ **`signIn` throttles each login, not each client.** Ten passwords per login
1061
+ per 15 minutes, then `CREDENTIALS_INVALID` with `retryAfter` until the window
1062
+ ends — the right password too, so tell the visitor to wait `retryAfter`
1063
+ seconds rather than that the password is wrong. `CredentialRefusal` gained
1064
+ `'throttled'`: an exhaustive `switch` over `error.reason` must handle it. A
1065
+ test that tries more than ten wrong passwords at one login over a
1066
+ `fixedClock` is throttled too: advance the clock past `retryAfter`, or wire
1067
+ `signIn: { throttle: false }`. **Somebody who knows a login can keep its
1068
+ password sign-in shut**, ten tries a window; a sign-in code, when you wire them, still opens it.
1069
+ On PostgreSQL, delete lapsed tokens on a schedule: every login tried adds a
1070
+ row per window. One password tried against
1071
+ many logins is not counted: rate-limit `signIn` per client address yourself
1072
+ ([passwords](docs/guide/passwords.md#what-you-still-limit-yourself)). The
1073
+ counts live in the tokens store: a flushed or evicting Redis forgets them,
1074
+ and a tokens store that cannot answer fails every password sign-in with
1075
+ `STORE_FAILED`.
1076
+
1028
1077
  **A sign-in can move a user's `version`.** Rewriting a stale hash is a write. A
1029
1078
  user object read before that sign-in, and then passed as `ifVersion`, gets
1030
1079
  `VERSION_CONFLICT`. That is the conflict doing its job: read the user again.
@@ -1102,7 +1151,7 @@ that sends one, since it is awaited: queue the event and return.
1102
1151
 
1103
1152
  ## Type safety, counted
1104
1153
 
1105
- **One hundred and twenty-nine plausible mistakes, one hundred and twenty-nine refused at compile time — and
1154
+ **One hundred and thirty-seven plausible mistakes, one hundred and thirty-seven refused at compile time — and
1106
1155
  two gaps, named.**
1107
1156
 
1108
1157
  The lists are typechecked and never run, with one `@ts-expect-error` per
@@ -1110,9 +1159,9 @@ mistake beside the shapes that must keep compiling. One is a single file:
1110
1159
  `test/types/refusals.ts` (fifteen, on the shared vocabulary). The other three
1111
1160
  are folders with one file per behaviour: `test/types/port/` (twenty-four, on
1112
1161
  the identity stores' port, from the point of view of the person implementing
1113
- it), `test/types/auth/` (forty-two, on `janus()`, from the point of view of
1162
+ it), `test/types/auth/` (fifty, on `janus()`, from the point of view of
1114
1163
  the application — twelve of them on the second factor, three on sign-in codes,
1115
- three on user events, four on step-ups) and `test/types/permissions/` (forty-eight, on the
1164
+ five on user events, four on step-ups, six on the sign-in throttle) and `test/types/permissions/` (forty-eight, on the
1116
1165
  permission model and the questions asked of it). The rule comes from
1117
1166
  `nxgt-data`, and so does the reason to distrust the claim without the files:
1118
1167
  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"}
@@ -51,6 +51,13 @@ interface SharedConfig {
51
51
  /** How long a step-up's challenge waits, e-mailed or not. `'10m'` when absent. */
52
52
  readonly stepUp?: Duration;
53
53
  };
54
+ /**
55
+ * How `signIn` answers password guessing. **On by default**: past ten
56
+ * passwords tried at one login in a 15-minute window, `signIn` answers
57
+ * `CREDENTIALS_INVALID` with `retryAfter` — the right password included —
58
+ * until the next window. Nothing locks: the next window signs in.
59
+ */
60
+ readonly signIn?: SignInConfig;
54
61
  /**
55
62
  * A TOTP second factor, for every user type with a password. Absent, no
56
63
  * `secondFactor` flows exist and `signIn` answers a session directly.
@@ -66,6 +73,22 @@ interface SharedConfig {
66
73
  */
67
74
  readonly events?: UserEventListener;
68
75
  }
76
+ /** What `signIn` does with password guessing. */
77
+ export interface SignInConfig {
78
+ /**
79
+ * The passwords one login may try per window. `{ attempts: 10, window:
80
+ * '15m' }` when absent; `false` counts nothing — rate-limit `signIn`
81
+ * yourself then.
82
+ */
83
+ readonly throttle?: SignInThrottleConfig | false;
84
+ }
85
+ /** How many passwords one login may try, and per how long. */
86
+ export interface SignInThrottleConfig {
87
+ /** Passwords tried per login and per window, the right one included. `10` when absent. */
88
+ readonly attempts?: number;
89
+ /** How long one window lasts. `'15m'` when absent. */
90
+ readonly window?: Duration;
91
+ }
69
92
  /** What a TOTP second factor needs: a name for the app, and the keys that seal. */
70
93
  export interface SecondFactorConfig {
71
94
  /** 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":"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"}
@@ -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;AAMxB,OAAO,KAAK,EAAE,cAAc,EAAgB,MAAM,mBAAmB,CAAC;AAGtE,qFAAqF;AACrF,wBAAgB,aAAa,CAC5B,MAAM,EAAE,WAAW,EACnB,KAAK,EAAE,MAAM,GACX,cAAc,CAuBhB"}
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"}
@@ -27,6 +27,11 @@ export interface ResolvedConfig {
27
27
  readonly signInCode: number;
28
28
  readonly stepUp: number;
29
29
  };
30
+ /** `null` when `signIn.throttle` is `false`. */
31
+ readonly signInThrottle: {
32
+ readonly attempts: number;
33
+ readonly windowMs: number;
34
+ } | null;
30
35
  readonly secondFactor: {
31
36
  readonly issuer: string;
32
37
  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,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"}
@@ -1 +1 @@
1
- {"version":3,"file":"reset-password.d.ts","sourceRoot":"","sources":["../../../src/auth/email-flows/reset-password.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,KAAK,EAAE,EAAE,EAAE,MAAM,OAAO,CAAC;AAChC,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,WAAW,CAAC;AAC9C,OAAO,EACN,KAAK,OAAO,EACZ,KAAK,OAAO,EAOZ,MAAM,YAAY,CAAC;AAIpB,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,UAAU,CAAC;AAGjD,KAAK,aAAa,GAAG,gBAAgB,CAAC,OAAO,CAAC,CAAC,eAAe,CAAC,CAAC;AAEhE;;;GAGG;AACH,wBAAgB,kBAAkB,CACjC,OAAO,EAAE,OAAO,EAChB,IAAI,EAAE,YAAY,EAClB,EAAE,EAAE,EAAE,GACJ,aAAa,CA4Bf"}
1
+ {"version":3,"file":"reset-password.d.ts","sourceRoot":"","sources":["../../../src/auth/email-flows/reset-password.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,KAAK,EAAE,EAAE,EAAE,MAAM,OAAO,CAAC;AAChC,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,WAAW,CAAC;AAC9C,OAAO,EACN,KAAK,OAAO,EACZ,KAAK,OAAO,EAOZ,MAAM,YAAY,CAAC;AAKpB,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,UAAU,CAAC;AAGjD,KAAK,aAAa,GAAG,gBAAgB,CAAC,OAAO,CAAC,CAAC,eAAe,CAAC,CAAC;AAEhE;;;GAGG;AACH,wBAAgB,kBAAkB,CACjC,OAAO,EAAE,OAAO,EAChB,IAAI,EAAE,YAAY,EAClB,EAAE,EAAE,EAAE,GACJ,aAAa,CAqCf"}
@@ -8,6 +8,11 @@ import type { Context } from './context';
8
8
  * `resetPassword.confirm` or the code of `signInCode.confirm`, which prove
9
9
  * the e-mail too; never for an e-mail already verified;
10
10
  * - `user.passwordReset` — by `resetPassword.confirm`;
11
+ * - `user.passwordChanged` — by `changePassword` and `setPassword`; never by a
12
+ * reset, which is `user.passwordReset` alone;
13
+ * - `user.emailChanged` — by `update`, when it changed the e-mail — compared
14
+ * normalised, the same test that makes the new one unverified. The one
15
+ * event that carries more than the user: `formerEmail`;
11
16
  * - `user.secondFactorEnabled` — by `secondFactor.activate`, once the factor
12
17
  * is active: not by `enroll`, which leaves it waiting for its first code;
13
18
  * - `user.secondFactorDisabled` — by `secondFactor.disable`, when it removed
@@ -19,11 +24,12 @@ import type { Context } from './context';
19
24
  * spent: a sign-in without the user's phone;
20
25
  * - `user.deleted` — by `delete`, once; a replay that finds nobody is none.
21
26
  */
22
- export type UserEventType = 'user.created' | 'user.emailVerified' | 'user.passwordReset' | 'user.secondFactorEnabled' | 'user.secondFactorDisabled' | 'user.recoveryCodesRegenerated' | 'user.recoveryCodeUsed' | 'user.deleted';
27
+ export type UserEventType = 'user.created' | 'user.emailVerified' | 'user.passwordReset' | 'user.passwordChanged' | 'user.emailChanged' | 'user.secondFactorEnabled' | 'user.secondFactorDisabled' | 'user.recoveryCodesRegenerated' | 'user.recoveryCodeUsed' | 'user.deleted';
23
28
  /**
24
29
  * A user event: **the user named by id, and nothing else** — no login, no
25
- * e-mail, no field, no password, no token. Whoever receives it reads the rest
26
- * from where it is kept, if they may.
30
+ * field, no password, no token. Whoever receives it reads the rest from where
31
+ * it is kept, if they may. One exception, `formerEmail` on
32
+ * `user.emailChanged`: once the write landed, the old address is kept nowhere.
27
33
  */
28
34
  export interface UserEvent {
29
35
  /** A UUIDv7 minted for this event: the key to deliver it once. */
@@ -36,7 +42,16 @@ export interface UserEvent {
36
42
  readonly occurredAt: Date;
37
43
  readonly userId: Id;
38
44
  readonly userType: string;
45
+ /**
46
+ * On `user.emailChanged` only: the address the user had before, as it was
47
+ * stored — `null` when they had none. So a notice can reach the inbox the
48
+ * account was just taken from; the new address is on the user. Absent
49
+ * from every other type, and from an event a webhook delivered.
50
+ */
51
+ readonly formerEmail?: string | null;
39
52
  }
53
+ /** What an event carries beyond the user, on the one type that does. */
54
+ export type EventExtras = Pick<UserEvent, 'formerEmail'>;
40
55
  /**
41
56
  * What `janus({ events })` takes: called once per event, **after** the write
42
57
  * landed and before the flow answers — awaited, so a queue that stores the
@@ -57,5 +72,5 @@ export declare function resolveEvents(events: unknown, where: string): UserEvent
57
72
  export declare function emit(context: Context, type: UserEventType, user: {
58
73
  readonly id: Id;
59
74
  readonly type: string;
60
- }, occurredAt: Date): Promise<void>;
75
+ }, occurredAt: Date, extras?: EventExtras): Promise<void>;
61
76
  //# 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;;;;;;;;;;;;;;;;;;GAkBG;AACH,MAAM,MAAM,aAAa,GACtB,cAAc,GACd,oBAAoB,GACpB,oBAAoB,GACpB,0BAA0B,GAC1B,2BAA2B,GAC3B,+BAA+B,GAC/B,uBAAuB,GACvB,cAAc,CAAC;AAElB;;;;GAIG;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;CAC1B;AAED;;;;;;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,GACd,OAAO,CAAC,IAAI,CAAC,CAoBf"}
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;;;;;;;;;;;;;;;;;;;;;;;GAuBG;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"}
@@ -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';
@@ -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,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"}
@@ -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`) and the code it may carry (`codes.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;;;;;;;GAOG;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"}
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"}
@@ -0,0 +1,50 @@
1
+ /**
2
+ * Attempts counted by the store in fixed windows of the clock — what bounds
3
+ * the codes tried at an app outside a sign-in (`second-factor/app-code-attempts.ts`)
4
+ * and the passwords tried at one login (`../sign-in-attempts.ts`).
5
+ *
6
+ * **No port of its own.** The count is a one-time token of kind
7
+ * `secondFactor`, counted by `countAttempt` like a challenge's: one
8
+ * conditional write per attempt, never read then written, so twenty guesses
9
+ * at once are twenty distinct counts. Its secret is never given out: each
10
+ * caller names it by a keyed hash of what the count is for, so it cannot be
11
+ * redeemed as a challenge.
12
+ *
13
+ * A token once spent counts nothing more. So the count is a **chain** of
14
+ * links, and what a spent link means is the caller's to say: `carry`, and the
15
+ * next link carries on from the attempts it holds — a password written, which
16
+ * spends every `secondFactor` token of the user, buys no guess; `restart`,
17
+ * and the next link starts from zero — a sign-in that succeeded spends the
18
+ * link it counted on, and the login's count starts again.
19
+ */
20
+ import type { Context } from '../context';
21
+ /**
22
+ * More links than this in one window, for `carry`: more passwords written
23
+ * than guesses, and the count is past any limit.
24
+ */
25
+ export declare const MAX_LINKS = 32;
26
+ /** What {@link countInWindow} counts, and how. */
27
+ export interface AttemptWindow {
28
+ /** How long one window lasts, in milliseconds. */
29
+ readonly windowMs: number;
30
+ /** What a spent link means: its attempts carried on, or a count started again. */
31
+ readonly spent: 'carry' | 'restart';
32
+ /** The `userId` each link is stored under. */
33
+ readonly userId: string;
34
+ /** A link's token hash in one window: never given out, and never a secret. */
35
+ readonly linkHash: (window: number, link: number) => string;
36
+ /** What to throw when a link is gone right after it was stored, or the links never end. */
37
+ readonly vanished: () => Error;
38
+ }
39
+ /** One attempt, counted. */
40
+ export interface CountedAttempt {
41
+ /** The attempts of this window, this one included. */
42
+ readonly attempts: number;
43
+ /** The link it was counted on — what spending restarts — or `null` past the last one. */
44
+ readonly link: string | null;
45
+ /** When the window ends, in milliseconds since the epoch. */
46
+ readonly endsAt: number;
47
+ }
48
+ /** Counts one attempt in the current window, before anything is compared. */
49
+ export declare function countInWindow(context: Context, counting: AttemptWindow): Promise<CountedAttempt>;
50
+ //# sourceMappingURL=window.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"window.d.ts","sourceRoot":"","sources":["../../../src/auth/one-time/window.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAEH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,YAAY,CAAC;AAG1C;;;GAGG;AACH,eAAO,MAAM,SAAS,KAAK,CAAC;AAQ5B,kDAAkD;AAClD,MAAM,WAAW,aAAa;IAC7B,kDAAkD;IAClD,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,kFAAkF;IAClF,QAAQ,CAAC,KAAK,EAAE,OAAO,GAAG,SAAS,CAAC;IACpC,8CAA8C;IAC9C,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,8EAA8E;IAC9E,QAAQ,CAAC,QAAQ,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,KAAK,MAAM,CAAC;IAC5D,2FAA2F;IAC3F,QAAQ,CAAC,QAAQ,EAAE,MAAM,KAAK,CAAC;CAC/B;AAED,4BAA4B;AAC5B,MAAM,WAAW,cAAc;IAC9B,sDAAsD;IACtD,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,yFAAyF;IACzF,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IAC7B,6DAA6D;IAC7D,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;CACxB;AAUD,6EAA6E;AAC7E,wBAAsB,aAAa,CAClC,OAAO,EAAE,OAAO,EAChB,QAAQ,EAAE,aAAa,GACrB,OAAO,CAAC,cAAc,CAAC,CAazB"}
@@ -4,12 +4,35 @@ import { type AnyUser, type Context } from './context';
4
4
  import type { UserRecord } from './port/types';
5
5
  import type { SignInResult } from './types';
6
6
  /**
7
- * What writing a password ends besides: every second-factor challenge of the
8
- * user still open, so a sign-in started with the old password cannot be
9
- * finished. Called **after** the write — the other half of
10
- * {@link heldByPassword}, which reads **after** it issued.
7
+ * What writing a password ends besides. A reset, a change and a set each call
8
+ * it **after** the write, and a failure here throws — it is never ignored:
9
+ *
10
+ * - The user's reset links still unspent. A link answers a request to
11
+ * replace the password; once it is replaced, an older link would replace
12
+ * it again, without knowing the new one.
13
+ * - Every second-factor challenge still open, so a sign-in started with the
14
+ * old password cannot be finished — the other half of
15
+ * {@link heldByPassword}, which reads **after** it issued.
16
+ *
17
+ * Both spends are attempted even when one fails, so a store failing one kind
18
+ * still spends the other; the first failure is then thrown.
19
+ *
20
+ * After the write, not before: a link issued while the password is written
21
+ * is spent too, where one issued between an earlier spend and the write
22
+ * would survive. So an outage here leaves the password written and the
23
+ * older links live, and the caller is told, with `STORE_FAILED`; the next
24
+ * `resetPassword.request` spends them. Sign-in codes and step-ups are left
25
+ * alone: the password proves neither, and a step-up belongs to a session,
26
+ * which a reset revokes.
27
+ */
28
+ export declare function endWhatThePasswordOpened(context: Context, userId: Id): Promise<void>;
29
+ /**
30
+ * What a change and a set do once the password is written: end what the
31
+ * old one opened, then send `user.passwordChanged` — from a `finally`, so an
32
+ * outage ending them still reports the write, which a retry would not make
33
+ * again. A reset sends `user.passwordReset` instead, from its own flow.
11
34
  */
12
- export declare function endSignInsWaiting(context: Context, userId: Id): Promise<void>;
35
+ export declare function passwordChanged(context: Context, written: UserRecord): Promise<void>;
13
36
  /**
14
37
  * Whether the password a sign-in verified is still the user's, once the
15
38
  * sign-in answered. A password written meanwhile ends the sign-in: its
@@ -1 +1 @@
1
- {"version":3,"file":"password-written.d.ts","sourceRoot":"","sources":["../../src/auth/password-written.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,EAAE,EAAE,MAAM,WAAW,CAAC;AACpC,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,UAAU,CAAC;AAC7C,OAAO,EACN,KAAK,OAAO,EACZ,KAAK,OAAO,EAGZ,MAAM,WAAW,CAAC;AAEnB,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AAC/C,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AAE5C;;;;;GAKG;AACH,wBAAsB,iBAAiB,CACtC,OAAO,EAAE,OAAO,EAChB,MAAM,EAAE,EAAE,GACR,OAAO,CAAC,IAAI,CAAC,CAMf;AAED;;;;;;;;;GASG;AACH,wBAAsB,cAAc,CACnC,OAAO,EAAE,OAAO,EAChB,IAAI,EAAE,YAAY,EAClB,QAAQ,EAAE,UAAU,EACpB,QAAQ,EAAE,MAAM,EAChB,MAAM,EAAE,YAAY,CAAC,OAAO,CAAC,EAC7B,KAAK,EAAE,MAAM,GACX,OAAO,CAAC,OAAO,CAAC,CAkBlB"}
1
+ {"version":3,"file":"password-written.d.ts","sourceRoot":"","sources":["../../src/auth/password-written.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,EAAE,EAAE,MAAM,WAAW,CAAC;AACpC,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,UAAU,CAAC;AAC7C,OAAO,EACN,KAAK,OAAO,EACZ,KAAK,OAAO,EAGZ,MAAM,WAAW,CAAC;AAGnB,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AAC/C,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AAE5C;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,wBAAsB,wBAAwB,CAC7C,OAAO,EAAE,OAAO,EAChB,MAAM,EAAE,EAAE,GACR,OAAO,CAAC,IAAI,CAAC,CAWf;AAED;;;;;GAKG;AACH,wBAAsB,eAAe,CACpC,OAAO,EAAE,OAAO,EAChB,OAAO,EAAE,UAAU,GACjB,OAAO,CAAC,IAAI,CAAC,CAMf;AAED;;;;;;;;;GASG;AACH,wBAAsB,cAAc,CACnC,OAAO,EAAE,OAAO,EAChB,IAAI,EAAE,YAAY,EAClB,QAAQ,EAAE,UAAU,EACpB,QAAQ,EAAE,MAAM,EAChB,MAAM,EAAE,YAAY,CAAC,OAAO,CAAC,EAC7B,KAAK,EAAE,MAAM,GACX,OAAO,CAAC,OAAO,CAAC,CAkBlB"}
@@ -104,8 +104,9 @@ export interface TokenStore {
104
104
  * Spends every **unspent** token of one user and one `kind` at `at` —
105
105
  * but the one whose hash is `except`, when given — and answers how many
106
106
  * it spent. What issuing a sign-in code calls, sparing the code it just
107
- * issued, so only the last code sent works; and what writing a password
108
- * calls, so no second-factor challenge opened with the old one survives.
107
+ * issued, so only the last code sent works — and so for a reset link and
108
+ * a step-up; and what writing a password calls, so no reset link and no
109
+ * second-factor challenge opened with the old one survives.
109
110
  *
110
111
  * - A spent token keeps its `spentAt`: it never changes once set.
111
112
  * - A token of another `kind`, or of another user, is not touched.
@@ -1 +1 @@
1
- {"version":3,"file":"tokens.d.ts","sourceRoot":"","sources":["../../../../src/auth/port/types/tokens.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,KAAK,EAAE,EAAE,EAAE,MAAM,iBAAiB,CAAC;AAE1C;;;;;;;;;;;;;;GAcG;AACH,MAAM,MAAM,SAAS,GAClB,aAAa,GACb,eAAe,GACf,cAAc,GACd,YAAY,GACZ,QAAQ,CAAC;AAEZ,yEAAyE;AACzE,MAAM,WAAW,WAAW;IAC3B,oEAAoE;IACpE,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,IAAI,EAAE,SAAS,CAAC;IACzB,QAAQ,CAAC,MAAM,EAAE,EAAE,CAAC;IACpB;;;OAGG;IACH,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB;;;;;;OAMG;IACH,QAAQ,CAAC,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;IACjC;;;;OAIG;IACH,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,SAAS,EAAE,IAAI,CAAC;IACzB,mDAAmD;IACnD,QAAQ,CAAC,OAAO,EAAE,IAAI,GAAG,IAAI,CAAC;IAC9B,QAAQ,CAAC,SAAS,EAAE,IAAI,CAAC;CACzB;AAED,kDAAkD;AAClD,MAAM,WAAW,UAAU;IAC1B;;;OAGG;IACH,WAAW,CAAC,MAAM,EAAE,WAAW,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAEhD;;;;;;;;;;;;;;;;;;OAkBG;IACH,YAAY,CACX,SAAS,EAAE,MAAM,EACjB,IAAI,EAAE,SAAS,EACf,EAAE,EAAE,IAAI,GACN,OAAO,CAAC,WAAW,GAAG,IAAI,CAAC,CAAC;IAE/B;;;;;;;;;;;;;;;;;;;OAmBG;IACH,YAAY,CAAC,SAAS,EAAE,MAAM,EAAE,IAAI,EAAE,SAAS,GAAG,OAAO,CAAC,WAAW,GAAG,IAAI,CAAC,CAAC;IAE9E;;;;;;;;;;;;;;;;OAgBG;IACH,eAAe,CACd,MAAM,EAAE,EAAE,EACV,IAAI,EAAE,SAAS,EACf,EAAE,EAAE,IAAI,EACR,MAAM,CAAC,EAAE,MAAM,GACb,OAAO,CAAC,MAAM,CAAC,CAAC;IAEnB;;;;OAIG;IACH,gBAAgB,CAAC,MAAM,EAAE,EAAE,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;CAC9C"}
1
+ {"version":3,"file":"tokens.d.ts","sourceRoot":"","sources":["../../../../src/auth/port/types/tokens.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,KAAK,EAAE,EAAE,EAAE,MAAM,iBAAiB,CAAC;AAE1C;;;;;;;;;;;;;;GAcG;AACH,MAAM,MAAM,SAAS,GAClB,aAAa,GACb,eAAe,GACf,cAAc,GACd,YAAY,GACZ,QAAQ,CAAC;AAEZ,yEAAyE;AACzE,MAAM,WAAW,WAAW;IAC3B,oEAAoE;IACpE,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,IAAI,EAAE,SAAS,CAAC;IACzB,QAAQ,CAAC,MAAM,EAAE,EAAE,CAAC;IACpB;;;OAGG;IACH,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB;;;;;;OAMG;IACH,QAAQ,CAAC,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;IACjC;;;;OAIG;IACH,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,SAAS,EAAE,IAAI,CAAC;IACzB,mDAAmD;IACnD,QAAQ,CAAC,OAAO,EAAE,IAAI,GAAG,IAAI,CAAC;IAC9B,QAAQ,CAAC,SAAS,EAAE,IAAI,CAAC;CACzB;AAED,kDAAkD;AAClD,MAAM,WAAW,UAAU;IAC1B;;;OAGG;IACH,WAAW,CAAC,MAAM,EAAE,WAAW,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAEhD;;;;;;;;;;;;;;;;;;OAkBG;IACH,YAAY,CACX,SAAS,EAAE,MAAM,EACjB,IAAI,EAAE,SAAS,EACf,EAAE,EAAE,IAAI,GACN,OAAO,CAAC,WAAW,GAAG,IAAI,CAAC,CAAC;IAE/B;;;;;;;;;;;;;;;;;;;OAmBG;IACH,YAAY,CAAC,SAAS,EAAE,MAAM,EAAE,IAAI,EAAE,SAAS,GAAG,OAAO,CAAC,WAAW,GAAG,IAAI,CAAC,CAAC;IAE9E;;;;;;;;;;;;;;;;;OAiBG;IACH,eAAe,CACd,MAAM,EAAE,EAAE,EACV,IAAI,EAAE,SAAS,EACf,EAAE,EAAE,IAAI,EACR,MAAM,CAAC,EAAE,MAAM,GACb,OAAO,CAAC,MAAM,CAAC,CAAC;IAEnB;;;;OAIG;IACH,gBAAgB,CAAC,MAAM,EAAE,EAAE,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;CAC9C"}
@@ -8,11 +8,9 @@ import type { Sealer } from '../sealing';
8
8
  * per window, so a stolen session cannot guess the app's code at leisure,
9
9
  * nor buy more guesses by switching from one to the other.
10
10
  *
11
- * **No port of its own.** The count is a one-time token of kind
12
- * `secondFactor`, counted by `countAttempt` like a challenge's: one
13
- * conditional write per attempt, never read then written. Its secret is
14
- * never given out; it is a keyed hash of what the count is for, so nobody
15
- * without the sealing keys can name it — nor redeem it as a challenge.
11
+ * Counted in an attempt window (`../one-time/window.ts`): a `secondFactor`
12
+ * token named by a keyed hash of what the count is for, so nobody without
13
+ * the sealing keys can name it — nor redeem it as a challenge.
16
14
  *
17
15
  * What it is for decides when it starts again: the **window**, a fixed
18
16
  * {@link APP_CODE_WINDOW_MS} slice of the clock, and the factor's
@@ -21,9 +19,8 @@ import type { Sealer } from '../sealing';
21
19
  * app.
22
20
  *
23
21
  * Writing a password spends every `secondFactor` token of the user, this one
24
- * included, and a spent token counts nothing more. So the count is a chain:
25
- * a spent link keeps the attempts it holds, and the next one carries on
26
- * from them — a password written between guesses buys none.
22
+ * included. So a spent link **carries** its attempts on to the next — a
23
+ * password written between guesses buys none.
27
24
  */
28
25
  /** How long the attempts of one window last: fifteen minutes. */
29
26
  export declare const APP_CODE_WINDOW_MS: number;
@@ -1 +1 @@
1
- {"version":3,"file":"app-code-attempts.d.ts","sourceRoot":"","sources":["../../../src/auth/second-factor/app-code-attempts.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,YAAY,CAAC;AAE1C,OAAO,KAAK,EAAe,UAAU,EAAE,MAAM,eAAe,CAAC;AAC7D,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,YAAY,CAAC;AAGzC;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AAEH,iEAAiE;AACjE,eAAO,MAAM,kBAAkB,QAAc,CAAC;AA8C9C;;;;;GAKG;AACH,wBAAsB,mBAAmB,CACxC,OAAO,EAAE,OAAO,EAChB,MAAM,EAAE,MAAM,EACd,MAAM,EAAE,UAAU,EAClB,KAAK,EAAE,MAAM,GACX,OAAO,CAAC,MAAM,CAAC,CAgBjB"}
1
+ {"version":3,"file":"app-code-attempts.d.ts","sourceRoot":"","sources":["../../../src/auth/second-factor/app-code-attempts.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,YAAY,CAAC;AAG1C,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,eAAe,CAAC;AAChD,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,YAAY,CAAC;AAGzC;;;;;;;;;;;;;;;;;;;;GAoBG;AAEH,iEAAiE;AACjE,eAAO,MAAM,kBAAkB,QAAc,CAAC;AA6B9C;;;;;GAKG;AACH,wBAAsB,mBAAmB,CACxC,OAAO,EAAE,OAAO,EAChB,MAAM,EAAE,MAAM,EACd,MAAM,EAAE,UAAU,EAClB,KAAK,EAAE,MAAM,GACX,OAAO,CAAC,MAAM,CAAC,CA+BjB"}
@@ -1 +1 @@
1
- {"version":3,"file":"challenge.d.ts","sourceRoot":"","sources":["../../../src/auth/second-factor/challenge.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,EAAE,EAAE,MAAM,OAAO,CAAC;AAChC,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,WAAW,CAAC;AAC9C,OAAO,KAAK,EAAE,OAAO,EAAE,OAAO,EAAE,MAAM,YAAY,CAAC;AAEnD,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,eAAe,CAAC;AAEhD,OAAO,KAAK,EAAE,oBAAoB,EAAE,QAAQ,EAAE,MAAM,UAAU,CAAC;AAI/D;;;;GAIG;AACH,wBAAgB,cAAc,CAAC,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,YAAY,EAAE,EAAE,EAAE,EAAE;kBAGhE,UAAU,SACX,MAAM,GACX,OAAO,CAAC,oBAAoB,CAAC;uBAqBP,MAAM,QAAQ,MAAM,GAAG,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAC,CAAC;EAI3E"}
1
+ {"version":3,"file":"challenge.d.ts","sourceRoot":"","sources":["../../../src/auth/second-factor/challenge.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,EAAE,EAAE,MAAM,OAAO,CAAC;AAChC,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,WAAW,CAAC;AAC9C,OAAO,KAAK,EAAE,OAAO,EAAE,OAAO,EAAE,MAAM,YAAY,CAAC;AAEnD,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,eAAe,CAAC;AAGhD,OAAO,KAAK,EAAE,oBAAoB,EAAE,QAAQ,EAAE,MAAM,UAAU,CAAC;AAI/D;;;;GAIG;AACH,wBAAgB,cAAc,CAAC,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,YAAY,EAAE,EAAE,EAAE,EAAE;kBAGhE,UAAU,SACX,MAAM,GACX,OAAO,CAAC,oBAAoB,CAAC;uBAqBP,MAAM,QAAQ,MAAM,GAAG,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAC,CAAC;EAI3E"}
@@ -1 +1 @@
1
- {"version":3,"file":"recovery-codes.d.ts","sourceRoot":"","sources":["../../../src/auth/second-factor/recovery-codes.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,YAAY,CAAC;AAEzC;;;;;;;;;;;;GAYG;AAEH,sEAAsE;AACtE,eAAO,MAAM,cAAc,KAAK,CAAC;AASjC,kEAAkE;AAClE,wBAAgB,iBAAiB,IAAI,MAAM,EAAE,CAQ5C;AAED;;;;GAIG;AACH,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAO7D;AAwBD,wEAAwE;AACxE,wBAAgB,gBAAgB,CAC/B,MAAM,EAAE,MAAM,EACd,MAAM,EAAE,MAAM,EACd,IAAI,EAAE,MAAM,GACV,MAAM,CAIR;AAED;;;;;;;GAOG;AACH,wBAAgB,gBAAgB,CAC/B,MAAM,EAAE,MAAM,EACd,MAAM,EAAE,MAAM,EACd,MAAM,EAAE,SAAS,MAAM,EAAE,EACzB,KAAK,EAAE,MAAM,EACb,KAAK,EAAE,MAAM,GACX,MAAM,CA6BR"}
1
+ {"version":3,"file":"recovery-codes.d.ts","sourceRoot":"","sources":["../../../src/auth/second-factor/recovery-codes.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,YAAY,CAAC;AAEzC;;;;;;;;;;;;GAYG;AAEH,sEAAsE;AACtE,eAAO,MAAM,cAAc,KAAK,CAAC;AASjC,kEAAkE;AAClE,wBAAgB,iBAAiB,IAAI,MAAM,EAAE,CAQ5C;AAED;;;;GAIG;AACH,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAO7D;AASD,wEAAwE;AACxE,wBAAgB,gBAAgB,CAC/B,MAAM,EAAE,MAAM,EACd,MAAM,EAAE,MAAM,EACd,IAAI,EAAE,MAAM,GACV,MAAM,CAIR;AAED;;;;;;;GAOG;AACH,wBAAgB,gBAAgB,CAC/B,MAAM,EAAE,MAAM,EACd,MAAM,EAAE,MAAM,EACd,MAAM,EAAE,SAAS,MAAM,EAAE,EACzB,KAAK,EAAE,MAAM,EACb,KAAK,EAAE,MAAM,GACX,MAAM,CA6BR"}
@@ -1 +1 @@
1
- {"version":3,"file":"recovery.d.ts","sourceRoot":"","sources":["../../../src/auth/second-factor/recovery.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,EAAE,EAAE,MAAM,OAAO,CAAC;AAChC,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,WAAW,CAAC;AAC9C,OAAO,KAAK,EAAE,OAAO,EAAE,OAAO,EAAE,MAAM,YAAY,CAAC;AAGnD,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,UAAU,CAAC;AAIhD;;;;;;;;;GASG;AACH,wBAAsB,eAAe,CACpC,OAAO,EAAE,OAAO,EAChB,IAAI,EAAE,YAAY,EAClB,SAAS,EAAE,MAAM,EACjB,IAAI,EAAE,MAAM,EACZ,EAAE,EAAE,EAAE,GACJ,OAAO,CAAC,eAAe,CAAC,OAAO,CAAC,CAAC,CAmCnC"}
1
+ {"version":3,"file":"recovery.d.ts","sourceRoot":"","sources":["../../../src/auth/second-factor/recovery.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,EAAE,EAAE,MAAM,OAAO,CAAC;AAChC,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,WAAW,CAAC;AAC9C,OAAO,KAAK,EAAE,OAAO,EAAE,OAAO,EAAE,MAAM,YAAY,CAAC;AAInD,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,UAAU,CAAC;AAIhD;;;;;;;;;GASG;AACH,wBAAsB,eAAe,CACpC,OAAO,EAAE,OAAO,EAChB,IAAI,EAAE,YAAY,EAClB,SAAS,EAAE,MAAM,EACjB,IAAI,EAAE,MAAM,EACZ,EAAE,EAAE,EAAE,GACJ,OAAO,CAAC,eAAe,CAAC,OAAO,CAAC,CAAC,CAoCnC"}
@@ -0,0 +1,43 @@
1
+ /**
2
+ * The passwords tried at one login, counted by the store per window —
3
+ * `janus({ signIn: { throttle } })`, ten per fifteen minutes by default.
4
+ * Past the last one, `signIn` answers `CREDENTIALS_INVALID` with
5
+ * `retryAfter`, the right password included, until the window ends. **Nothing
6
+ * locks** past a window — but somebody who keeps trying a login keeps its
7
+ * password sign-in shut, window after window: a per-client limit is the
8
+ * application's, and a sign-in code still opens the account.
9
+ *
10
+ * Counted per login, **known or not**: an unknown login is counted as a
11
+ * registered one is, so the throttle does not tell them apart. Counted
12
+ * before anything is looked up or compared, so of twenty guesses at once,
13
+ * exactly as many as the limit are compared.
14
+ *
15
+ * Counted in an attempt window (`one-time/window.ts`), in `secondFactor`
16
+ * tokens stored under no user: the `userId` of each is a keyed hash of the
17
+ * login, never the login. The key is derived from the sealing keys when
18
+ * `secondFactor` is configured; without them it is a fixed one, and the hash
19
+ * only keeps the login out of plain sight — safe because a `secondFactor`
20
+ * token is redeemed only by the second factor's flows, which do not exist
21
+ * without the keys. A sign-in that opens a session spends the link it
22
+ * counts on, and the login's count **restarts** from zero.
23
+ *
24
+ * A store that cannot count throws `STORE_FAILED`: never a refusal, and
25
+ * never a sign-in let through uncounted.
26
+ */
27
+ import type { ResolvedType } from './config';
28
+ import type { Context } from './context';
29
+ import type { UserRecord } from './port/types';
30
+ /**
31
+ * Counts one password tried at `login` — already normalised — before it is
32
+ * looked up, and refuses past the last one: the right password included.
33
+ */
34
+ export declare function countSignInAttempt(context: Context, type: ResolvedType, login: string, where: string): Promise<void>;
35
+ /**
36
+ * Starts the user's count again, once a sign-in opens a session: after the
37
+ * password alone, or after the second factor's code — never after the
38
+ * password of a user whose second factor is still to come, so knowing the
39
+ * password buys no more challenges. It counts one attempt on the current
40
+ * link, and spends it: the next one counts from zero.
41
+ */
42
+ export declare function restartSignInCount(context: Context, type: ResolvedType, record: UserRecord, where: string): Promise<void>;
43
+ //# sourceMappingURL=sign-in-attempts.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sign-in-attempts.d.ts","sourceRoot":"","sources":["../../src/auth/sign-in-attempts.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AAGH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,UAAU,CAAC;AAC7C,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAGzC,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AA8C/C;;;GAGG;AACH,wBAAsB,kBAAkB,CACvC,OAAO,EAAE,OAAO,EAChB,IAAI,EAAE,YAAY,EAClB,KAAK,EAAE,MAAM,EACb,KAAK,EAAE,MAAM,GACX,OAAO,CAAC,IAAI,CAAC,CAgBf;AAED;;;;;;GAMG;AACH,wBAAsB,kBAAkB,CACvC,OAAO,EAAE,OAAO,EAChB,IAAI,EAAE,YAAY,EAClB,MAAM,EAAE,UAAU,EAClB,KAAK,EAAE,MAAM,GACX,OAAO,CAAC,IAAI,CAAC,CAWf"}