@voltro/plugin-auth 0.23.0 → 0.25.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/dist/index.d.ts CHANGED
@@ -80,6 +80,20 @@ export declare interface AuthConfig {
80
80
  * but any concern (unverified email, suspended tenant, …) can hook here.
81
81
  * Absent / empty ⇒ every authenticated user proceeds unchanged. */
82
82
  readonly subjectGuards?: ReadonlyArray<SubjectGuard>;
83
+ /** Brute-force lockout. After `maxAttempts` failed credential attempts
84
+ * (wrong password OR wrong MFA code) within `windowSeconds`, the account —
85
+ * keyed by EMAIL, so an unknown address locks exactly like a real one and
86
+ * the lock can't be used as an existence oracle — is refused with a 429 for
87
+ * `lockSeconds`; a completed login clears the counter. ON by default (a
88
+ * security default, not opt-in); raise `maxAttempts` very high to disable. */
89
+ readonly lockout?: {
90
+ readonly maxAttempts?: number;
91
+ readonly windowSeconds?: number;
92
+ readonly lockSeconds?: number;
93
+ };
94
+ /** Injectable clock (epoch-ms) for the lockout counter. Default `Date.now`;
95
+ * tests pass a controllable one to exercise window/lock expiry. */
96
+ readonly now?: () => number;
83
97
  }
84
98
 
85
99
  /**
@@ -405,6 +419,26 @@ declare interface ListSessionsInput {
405
419
  readonly userId: string;
406
420
  }
407
421
 
422
+ /**
423
+ * Brute-force lockout policy, resolved from `AuthConfig.lockout` and passed
424
+ * INTO the store on each failure so the store itself stays policy-free.
425
+ * Times in milliseconds; `now` everywhere is epoch-ms so tests can inject a
426
+ * clock. Kept keyed by EMAIL (not user id) on purpose — a locked account must
427
+ * not read differently from an unknown one, or the lock becomes an existence
428
+ * oracle. See `loginAttemptsTable`.
429
+ */
430
+ declare interface LockoutPolicy {
431
+ readonly maxAttempts: number;
432
+ readonly windowMs: number;
433
+ readonly lockMs: number;
434
+ }
435
+
436
+ declare interface LockoutState {
437
+ readonly locked: boolean;
438
+ /** Seconds until the lock lifts; 0 when not locked. */
439
+ readonly retryAfterSeconds: number;
440
+ }
441
+
408
442
  /** Default magic-link lifetime — short, since it's an inbox round-trip. */
409
443
  export declare const MAGIC_LINK_TTL_S: number;
410
444
 
@@ -1092,6 +1126,16 @@ export declare interface UserStore {
1092
1126
  * the write are ONE atomic statement at the store, so two replicas
1093
1127
  * racing the same counter can't both succeed. */
1094
1128
  readonly advancePasskeyCounter: (credentialId: string, newCounter: number) => Effect.Effect<boolean>;
1129
+ /** Is this email currently locked out? Reads only — never mutates. Returns
1130
+ * the remaining cooldown so the handler can surface a retry-after. */
1131
+ readonly isLockedOut: (email: string, now: number) => Effect.Effect<LockoutState>;
1132
+ /** Record one failed credential attempt for `email`. Opens a fresh window
1133
+ * when the previous one (or a prior lock) has expired, otherwise increments;
1134
+ * sets the lock once `failedCount` reaches `policy.maxAttempts`. Called only
1135
+ * for a NOT-currently-locked email (the handler checks `isLockedOut` first). */
1136
+ readonly recordLoginFailure: (email: string, now: number, policy: LockoutPolicy) => Effect.Effect<void>;
1137
+ /** Clear the counter for `email` on a completed login. */
1138
+ readonly clearLoginFailures: (email: string) => Effect.Effect<void>;
1095
1139
  }
1096
1140
 
1097
1141
  /** Verify a passkey assertion (sign-in). */
package/dist/index.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import { PasswordEmptyError as e, PasswordHashError as t, hashPassword as n, needsRehash as r, parseScryptParams as i, verifyPassword as a, verifyPasswordWithRehash as o } from "./password.js";
2
- import { SESSION_COOKIE_NAME as s, clearSessionCookie as ee, issueSession as te, readSession as c, readSessionKeyed as l, resolveSessionSecret as u, resolveSessionSecrets as d, sessionSecretsOf as f } from "./session.js";
3
- import { a as p, c as m, i as h, l as g, n as _, o as v, r as y, s as b, t as x } from "./strategy-D345j4df.js";
4
- import { A as S, C, D as w, E as T, F as E, I as D, L as O, M as k, N as A, O as j, P as M, S as N, T as P, _ as F, a as I, b as L, c as R, d as z, f as B, g as V, h as H, i as U, j as ne, k as W, l as G, m as K, n as q, o as J, p as Y, r as X, s as Z, t as re, u as ie, v as ae, w as oe, x as se, y as ce } from "./plugin-HgTMFY3m.js";
2
+ import { SESSION_COOKIE_NAME as s, clearSessionCookie as c, issueSession as l, readSession as u, readSessionKeyed as d, resolveSessionSecret as f, resolveSessionSecrets as p, sessionSecretsOf as m } from "./session.js";
3
+ import { a as h, c as g, i as _, l as v, n as y, o as b, r as x, s as S, t as C } from "./strategy-Ce4FlGi4.js";
4
+ import { A as w, C as T, D as E, E as D, F as O, I as k, L as A, M as ee, N as te, O as j, P as M, S as N, T as P, _ as F, a as I, b as L, c as R, d as z, f as B, g as V, h as H, i as U, j as W, k as G, l as K, m as q, n as J, o as Y, p as X, r as ne, s as Z, t as re, u as ie, v as ae, w as oe, x as se, y as ce } from "./plugin-ZXs8LFn0.js";
5
5
  import { MAGIC_LINK_TTL_S as le, PASSWORD_RESET_TTL_S as ue, hashToken as de, mintToken as fe } from "./tokens.js";
6
6
  import { CSRF_COOKIE_NAME as pe, CSRF_HEADER_NAME as me, isCsrfTokenWellFormed as Q, issueCsrfToken as he, verifyCsrf as ge } from "./csrf.js";
7
7
  import { generateChallenge as _e, verifyAssertion as ve, verifyRegistration as ye } from "./webauthn.js";
@@ -36,147 +36,128 @@ var be = (e) => {
36
36
  ...t,
37
37
  email: n,
38
38
  createdAt: r
39
- })), $.catchAll((e) => be(e) === "23505" ? $.fail(new _({ email: n })) : $.die(e)));
39
+ })), $.catchAll((e) => be(e) === "23505" ? $.fail(new y({ email: n })) : $.die(e)));
40
40
  }), i = (t) => $.gen(function* () {
41
41
  return (yield* e`
42
42
  SELECT id, email, "passwordHash", "tenantId", "createdAt", "mfaSecret", "mfaEnrolledAt"
43
43
  FROM users WHERE id = ${t} LIMIT 1
44
- `)[0] || (yield* $.fail(new y({ userId: t })));
45
- }).pipe($.catchTag("SqlError", (e) => $.die(e)));
46
- return {
47
- findByEmail: t,
48
- findById: n,
49
- insert: r,
50
- updatePassword: (t, n) => $.gen(function* () {
51
- return yield* e`
44
+ `)[0] || (yield* $.fail(new x({ userId: t })));
45
+ }).pipe($.catchTag("SqlError", (e) => $.die(e))), a = (t, n) => $.gen(function* () {
46
+ return yield* e`
52
47
  UPDATE users SET "passwordHash" = ${n}, "updatedAt" = ${/* @__PURE__ */ new Date()}
53
48
  WHERE id = ${t}
54
49
  `.pipe($.catchTag("SqlError", (e) => $.die(e))), yield* i(t);
55
- }),
56
- setMfaSecret: (t, n) => $.gen(function* () {
57
- return yield* e`
50
+ }), o = (t, n) => $.gen(function* () {
51
+ return yield* e`
58
52
  UPDATE users
59
53
  SET "mfaSecret" = ${n}, "mfaEnrolledAt" = ${null}
60
54
  WHERE id = ${t}
61
55
  `.pipe($.catchTag("SqlError", (e) => $.die(e))), yield* i(t);
62
- }),
63
- markMfaEnrolled: (t) => $.gen(function* () {
64
- return yield* e`
56
+ }), s = (t) => $.gen(function* () {
57
+ return yield* e`
65
58
  UPDATE users
66
59
  SET "mfaEnrolledAt" = ${/* @__PURE__ */ new Date()}
67
60
  WHERE id = ${t}
68
61
  `.pipe($.catchTag("SqlError", (e) => $.die(e))), yield* i(t);
69
- }),
70
- clearMfa: (t) => $.gen(function* () {
71
- return yield* e`
62
+ }), c = (t) => $.gen(function* () {
63
+ return yield* e`
72
64
  UPDATE users
73
65
  SET "mfaSecret" = ${null}, "mfaEnrolledAt" = ${null}
74
66
  WHERE id = ${t}
75
67
  `.pipe($.catchTag("SqlError", (e) => $.die(e))), yield* i(t);
76
- }),
77
- listMemberships: (t) => $.gen(function* () {
78
- return (yield* e`
68
+ }), l = (t) => $.gen(function* () {
69
+ return (yield* e`
79
70
  SELECT "userId", "tenantId", role, "joinedAt"
80
71
  FROM memberships WHERE "userId" = ${t}
81
72
  ORDER BY "joinedAt" ASC
82
73
  `).map((e) => ({ ...e }));
83
- }).pipe($.orElseSucceed(() => [])),
84
- addMembership: (t) => $.gen(function* () {
85
- let n = /* @__PURE__ */ new Date();
86
- yield* e`
74
+ }).pipe($.orElseSucceed(() => [])), u = (t) => $.gen(function* () {
75
+ let n = /* @__PURE__ */ new Date();
76
+ yield* e`
87
77
  UPDATE memberships SET role = ${t.role}
88
78
  WHERE "userId" = ${t.userId} AND "tenantId" = ${t.tenantId}
89
79
  `;
90
- let r = yield* e`
80
+ let r = yield* e`
91
81
  SELECT "userId", "tenantId", role, "joinedAt"
92
82
  FROM memberships
93
83
  WHERE "userId" = ${t.userId} AND "tenantId" = ${t.tenantId}
94
84
  LIMIT 1
95
85
  `;
96
- return r[0] ? { ...r[0] } : (yield* e`
86
+ return r[0] ? { ...r[0] } : (yield* e`
97
87
  INSERT INTO memberships (id, "userId", "tenantId", role, "joinedAt")
98
- VALUES (${p("mem")}, ${t.userId}, ${t.tenantId}, ${t.role}, ${n})
88
+ VALUES (${h("mem")}, ${t.userId}, ${t.tenantId}, ${t.role}, ${n})
99
89
  `, {
100
- ...t,
101
- joinedAt: n
102
- });
103
- }).pipe($.catchAll(() => $.succeed({
104
90
  ...t,
105
- joinedAt: /* @__PURE__ */ new Date()
106
- }))),
107
- membershipRole: (t, n) => $.gen(function* () {
108
- return (yield* e`
91
+ joinedAt: n
92
+ });
93
+ }).pipe($.catchAll(() => $.succeed({
94
+ ...t,
95
+ joinedAt: /* @__PURE__ */ new Date()
96
+ }))), d = (t, n) => $.gen(function* () {
97
+ return (yield* e`
109
98
  SELECT role FROM memberships
110
99
  WHERE "userId" = ${t} AND "tenantId" = ${n} LIMIT 1
111
100
  `)[0]?.role ?? null;
112
- }).pipe($.orElseSucceed(() => null)),
113
- listSessions: (t) => $.gen(function* () {
114
- return (yield* e`
101
+ }).pipe($.orElseSucceed(() => null)), f = (t) => $.gen(function* () {
102
+ return (yield* e`
115
103
  SELECT id, "userId", "tenantId", "expiresAt", "ipAddress", "userAgent", "createdAt", "lastSeenAt"
116
104
  FROM sessions WHERE "userId" = ${t}
117
105
  ORDER BY "createdAt" DESC
118
106
  `).map((e) => ({ ...e }));
119
- }).pipe($.orElseSucceed(() => [])),
120
- insertSession: (t) => $.gen(function* () {
121
- let n = /* @__PURE__ */ new Date();
122
- return yield* e`
107
+ }).pipe($.orElseSucceed(() => [])), p = (t) => $.gen(function* () {
108
+ let n = /* @__PURE__ */ new Date();
109
+ return yield* e`
123
110
  INSERT INTO sessions (id, "userId", "tenantId", "expiresAt", "ipAddress", "userAgent", "createdAt", "lastSeenAt")
124
111
  VALUES (
125
112
  ${t.id}, ${t.userId}, ${t.tenantId}, ${t.expiresAt},
126
113
  ${t.ipAddress ?? null}, ${t.userAgent ?? null}, ${n}, ${n}
127
114
  )
128
115
  `, {
129
- ...t,
130
- createdAt: n,
131
- lastSeenAt: n
132
- };
133
- }).pipe($.catchAll(() => $.succeed({
134
116
  ...t,
135
- createdAt: /* @__PURE__ */ new Date(),
136
- lastSeenAt: /* @__PURE__ */ new Date()
137
- }))),
138
- findSession: (t) => $.gen(function* () {
139
- let n = yield* e`
117
+ createdAt: n,
118
+ lastSeenAt: n
119
+ };
120
+ }).pipe($.catchAll(() => $.succeed({
121
+ ...t,
122
+ createdAt: /* @__PURE__ */ new Date(),
123
+ lastSeenAt: /* @__PURE__ */ new Date()
124
+ }))), m = (t) => $.gen(function* () {
125
+ let n = yield* e`
140
126
  SELECT id, "userId", "tenantId", "expiresAt", "ipAddress", "userAgent", "createdAt", "lastSeenAt"
141
127
  FROM sessions WHERE id = ${t} LIMIT 1
142
128
  `;
143
- return n[0] ? { ...n[0] } : null;
144
- }).pipe($.orElseSucceed(() => null)),
145
- touchSession: (t, n) => $.gen(function* () {
146
- yield* e`
129
+ return n[0] ? { ...n[0] } : null;
130
+ }).pipe($.orElseSucceed(() => null)), g = (t, n) => $.gen(function* () {
131
+ yield* e`
147
132
  UPDATE sessions SET "expiresAt" = ${n}, "lastSeenAt" = ${/* @__PURE__ */ new Date()}
148
133
  WHERE id = ${t}
149
134
  `;
150
- }).pipe($.asVoid, $.orElseSucceed(() => void 0)),
151
- revokeSession: (t, n) => $.gen(function* () {
152
- return (yield* e`
135
+ }).pipe($.asVoid, $.orElseSucceed(() => void 0)), _ = (t, n) => $.gen(function* () {
136
+ return (yield* e`
153
137
  DELETE FROM sessions WHERE id = ${n} AND "userId" = ${t}
154
138
  RETURNING id
155
139
  `).length > 0;
156
- }).pipe($.orElseSucceed(() => !1)),
157
- revokeAllOtherSessions: (t, n) => $.gen(function* () {
158
- return (yield* e`
140
+ }).pipe($.orElseSucceed(() => !1)), v = (t, n) => $.gen(function* () {
141
+ return (yield* e`
159
142
  DELETE FROM sessions WHERE "userId" = ${t} AND id <> ${n}
160
143
  RETURNING id
161
144
  `).length;
162
- }).pipe($.orElseSucceed(() => 0)),
163
- insertToken: (t) => $.gen(function* () {
164
- let n = /* @__PURE__ */ new Date();
165
- return yield* e`
145
+ }).pipe($.orElseSucceed(() => 0)), b = (t) => $.gen(function* () {
146
+ let n = /* @__PURE__ */ new Date();
147
+ return yield* e`
166
148
  INSERT INTO "authTokens" (id, "tokenHash", "userId", purpose, "expiresAt", "consumedAt", "createdAt")
167
149
  VALUES (${t.id}, ${t.tokenHash}, ${t.userId}, ${t.purpose}, ${t.expiresAt}, ${null}, ${n})
168
150
  `, {
169
- ...t,
170
- consumedAt: null,
171
- createdAt: n
172
- };
173
- }).pipe($.catchAll(() => $.succeed({
174
151
  ...t,
175
152
  consumedAt: null,
176
- createdAt: /* @__PURE__ */ new Date()
177
- }))),
178
- consumeToken: (t, n) => $.gen(function* () {
179
- let r = yield* e`
153
+ createdAt: n
154
+ };
155
+ }).pipe($.catchAll(() => $.succeed({
156
+ ...t,
157
+ consumedAt: null,
158
+ createdAt: /* @__PURE__ */ new Date()
159
+ }))), S = (t, n) => $.gen(function* () {
160
+ let r = yield* e`
180
161
  UPDATE "authTokens"
181
162
  SET "consumedAt" = ${/* @__PURE__ */ new Date()}
182
163
  WHERE "tokenHash" = ${t}
@@ -185,72 +166,125 @@ var be = (e) => {
185
166
  AND "expiresAt" > ${/* @__PURE__ */ new Date()}
186
167
  RETURNING id, "tokenHash", "userId", purpose, "expiresAt", "consumedAt", "createdAt"
187
168
  `;
188
- return r[0] ? { ...r[0] } : null;
189
- }).pipe($.orElseSucceed(() => null)),
190
- replaceRecoveryCodes: (t, n) => $.gen(function* () {
191
- let r = /* @__PURE__ */ new Date();
192
- yield* e`DELETE FROM "recoveryCodes" WHERE "userId" = ${t}`;
193
- for (let i of n) yield* e`
194
- INSERT INTO "recoveryCodes" (id, "userId", "codeHash", "consumedAt", "createdAt")
195
- VALUES (${p("rc")}, ${t}, ${i}, ${null}, ${r})
196
- `;
197
- }).pipe($.asVoid, $.orElseSucceed(() => void 0)),
198
- consumeRecoveryCode: (t, n) => $.gen(function* () {
199
- return (yield* e`
200
- UPDATE "recoveryCodes"
201
- SET "consumedAt" = ${/* @__PURE__ */ new Date()}
202
- WHERE "userId" = ${t}
203
- AND "codeHash" = ${n}
204
- AND "consumedAt" IS NULL
205
- RETURNING id
206
- `).length > 0;
207
- }).pipe($.orElseSucceed(() => !1)),
208
- countRecoveryCodes: (t) => $.gen(function* () {
209
- return (yield* e`
210
- SELECT COUNT(*)::int AS n FROM "recoveryCodes"
211
- WHERE "userId" = ${t} AND "consumedAt" IS NULL
212
- `)[0]?.n ?? 0;
213
- }).pipe($.orElseSucceed(() => 0)),
214
- listPasskeys: (t) => $.gen(function* () {
215
- return (yield* e`
169
+ return r[0] ? { ...r[0] } : null;
170
+ }).pipe($.orElseSucceed(() => null)), C = (t) => $.gen(function* () {
171
+ return (yield* e`
216
172
  SELECT id, "credentialId", "userId", "publicKey", counter, transports, "createdAt", "lastUsedAt"
217
173
  FROM passkeys WHERE "userId" = ${t}
218
174
  `).map((e) => ({ ...e }));
219
- }).pipe($.orElseSucceed(() => [])),
220
- findPasskey: (t) => $.gen(function* () {
221
- let n = yield* e`
175
+ }).pipe($.orElseSucceed(() => [])), w = (t) => $.gen(function* () {
176
+ let n = yield* e`
222
177
  SELECT id, "credentialId", "userId", "publicKey", counter, transports, "createdAt", "lastUsedAt"
223
178
  FROM passkeys WHERE "credentialId" = ${t} LIMIT 1
224
179
  `;
225
- return n[0] ? { ...n[0] } : null;
226
- }).pipe($.orElseSucceed(() => null)),
227
- insertPasskey: (t) => $.gen(function* () {
228
- let n = /* @__PURE__ */ new Date();
229
- return yield* e`
180
+ return n[0] ? { ...n[0] } : null;
181
+ }).pipe($.orElseSucceed(() => null)), T = (t) => $.gen(function* () {
182
+ let n = /* @__PURE__ */ new Date();
183
+ return yield* e`
230
184
  INSERT INTO passkeys (id, "credentialId", "userId", "publicKey", counter, transports, "createdAt", "lastUsedAt")
231
185
  VALUES (
232
186
  ${t.id}, ${t.credentialId}, ${t.userId}, ${t.publicKey},
233
187
  ${t.counter}, ${t.transports ?? null}, ${n}, ${null}
234
188
  )
235
189
  `, {
236
- ...t,
237
- createdAt: n,
238
- lastUsedAt: null
239
- };
240
- }).pipe($.catchAll(() => $.succeed({
241
190
  ...t,
242
- createdAt: /* @__PURE__ */ new Date(),
191
+ createdAt: n,
243
192
  lastUsedAt: null
244
- }))),
245
- advancePasskeyCounter: (t, n) => $.gen(function* () {
246
- return (yield* e`
193
+ };
194
+ }).pipe($.catchAll(() => $.succeed({
195
+ ...t,
196
+ createdAt: /* @__PURE__ */ new Date(),
197
+ lastUsedAt: null
198
+ }))), E = (t, n) => $.gen(function* () {
199
+ return (yield* e`
247
200
  UPDATE passkeys
248
201
  SET counter = ${n}, "lastUsedAt" = ${/* @__PURE__ */ new Date()}
249
202
  WHERE "credentialId" = ${t}
250
203
  AND counter < ${n}
251
204
  RETURNING id
252
205
  `).length > 0;
253
- }).pipe($.orElseSucceed(() => !1))
206
+ }).pipe($.orElseSucceed(() => !1)), D = (t, n) => $.gen(function* () {
207
+ let r = /* @__PURE__ */ new Date();
208
+ yield* e`DELETE FROM "recoveryCodes" WHERE "userId" = ${t}`;
209
+ for (let i of n) yield* e`
210
+ INSERT INTO "recoveryCodes" (id, "userId", "codeHash", "consumedAt", "createdAt")
211
+ VALUES (${h("rc")}, ${t}, ${i}, ${null}, ${r})
212
+ `;
213
+ }).pipe($.asVoid, $.orElseSucceed(() => void 0)), O = (t, n) => $.gen(function* () {
214
+ return (yield* e`
215
+ UPDATE "recoveryCodes"
216
+ SET "consumedAt" = ${/* @__PURE__ */ new Date()}
217
+ WHERE "userId" = ${t}
218
+ AND "codeHash" = ${n}
219
+ AND "consumedAt" IS NULL
220
+ RETURNING id
221
+ `).length > 0;
222
+ }).pipe($.orElseSucceed(() => !1)), k = (t) => $.gen(function* () {
223
+ return (yield* e`
224
+ SELECT COUNT(*)::int AS n FROM "recoveryCodes"
225
+ WHERE "userId" = ${t} AND "consumedAt" IS NULL
226
+ `)[0]?.n ?? 0;
227
+ }).pipe($.orElseSucceed(() => 0)), A = (e, t) => $.catchAll((n) => $.logWarning(`[auth] lockout ${e} failed — brute-force protection is not counting`).pipe($.annotateLogs("cause", String(n)), $.as(t)));
228
+ return {
229
+ findByEmail: t,
230
+ findById: n,
231
+ insert: r,
232
+ updatePassword: a,
233
+ setMfaSecret: o,
234
+ markMfaEnrolled: s,
235
+ clearMfa: c,
236
+ listMemberships: l,
237
+ addMembership: u,
238
+ membershipRole: d,
239
+ listSessions: f,
240
+ insertSession: p,
241
+ findSession: m,
242
+ touchSession: g,
243
+ revokeSession: _,
244
+ revokeAllOtherSessions: v,
245
+ insertToken: b,
246
+ consumeToken: S,
247
+ replaceRecoveryCodes: D,
248
+ consumeRecoveryCode: O,
249
+ countRecoveryCodes: k,
250
+ listPasskeys: C,
251
+ findPasskey: w,
252
+ insertPasskey: T,
253
+ advancePasskeyCounter: E,
254
+ isLockedOut: (t, n) => $.gen(function* () {
255
+ let r = (yield* e`
256
+ SELECT "lockedUntil" FROM "loginAttempts"
257
+ WHERE email = ${t.toLowerCase()} LIMIT 1
258
+ `)[0]?.lockedUntil ?? null;
259
+ return r != null && n < r.getTime() ? {
260
+ locked: !0,
261
+ retryAfterSeconds: Math.ceil((r.getTime() - n) / 1e3)
262
+ } : {
263
+ locked: !1,
264
+ retryAfterSeconds: 0
265
+ };
266
+ }).pipe(A("read", {
267
+ locked: !1,
268
+ retryAfterSeconds: 0
269
+ })),
270
+ recordLoginFailure: (t, n, r) => $.gen(function* () {
271
+ let i = t.toLowerCase(), a = (yield* e`
272
+ SELECT "failedCount", "windowStartedAt", "lockedUntil"
273
+ FROM "loginAttempts" WHERE email = ${i} LIMIT 1
274
+ `)[0], o = a?.windowStartedAt?.getTime() ?? null, s = a?.lockedUntil?.getTime() ?? null, c = o == null || n - o >= r.windowMs, l = !a || c || s != null && n >= s, u = l ? 1 : a.failedCount + 1, d = l ? new Date(n) : new Date(o), f = u >= r.maxAttempts ? new Date(n + r.lockMs) : null, p = new Date(n);
275
+ yield* e`
276
+ INSERT INTO "loginAttempts" (id, email, "failedCount", "windowStartedAt", "lockedUntil", "createdAt", "updatedAt")
277
+ VALUES (${h("la")}, ${i}, ${u}, ${d}, ${f}, ${p}, ${p})
278
+ ON CONFLICT (email) DO UPDATE SET
279
+ "failedCount" = ${u},
280
+ "windowStartedAt" = ${d},
281
+ "lockedUntil" = ${f},
282
+ "updatedAt" = ${p}
283
+ `;
284
+ }).pipe($.asVoid, A("record", void 0)),
285
+ clearLoginFailures: (t) => $.gen(function* () {
286
+ yield* e`DELETE FROM "loginAttempts" WHERE email = ${t.toLowerCase()}`;
287
+ }).pipe($.asVoid, A("clear", void 0))
254
288
  };
255
289
  }, Se = (e) => (t) => $.runPromise(e.send({
256
290
  to: t.to,
@@ -259,4 +293,4 @@ var be = (e) => {
259
293
  text: t.text
260
294
  }).pipe($.asVoid, $.orElseSucceed(() => void 0)));
261
295
  //#endregion
262
- export { pe as CSRF_COOKIE_NAME, me as CSRF_HEADER_NAME, le as MAGIC_LINK_TTL_S, G as MFA_PENDING_TTL_S, ue as PASSWORD_RESET_TTL_S, e as PasswordEmptyError, t as PasswordHashError, s as SESSION_COOKIE_NAME, ne as TotpVerifyError, _ as UserAlreadyExistsError, y as UserNotFoundError, re as authRoutesPlugin, ee as clearSessionCookie, ie as configSessionSecrets, X as dataStoreChallengeStore, _e as generateChallenge, k as generateRecoveryCodes, A as generateTotpCode, M as generateTotpSecret, q as getAuthSessionStrategy, z as guardSubject, B as handleCsrf, Y as handleListMemberships, K as handleListSessions, H as handleMagicLinkConsume, V as handleMagicLinkRequest, F as handleMfaEnrollStart, ae as handleMfaEnrollVerify, ce as handleMfaRegenerateRecoveryCodes, L as handleMfaUnenroll, se as handleMfaVerify, U as handlePasskeyAssertOptions, I as handlePasskeyAssertVerify, J as handlePasskeyRegisterOptions, Z as handlePasskeyRegisterVerify, N as handlePasswordResetConfirm, C as handlePasswordResetRequest, oe as handleRevokeAllOtherSessions, P as handleRevokeSession, T as handleSignIn, w as handleSignOut, j as handleSignUp, W as handleSwitchTenant, n as hashPassword, de as hashToken, Q as isCsrfTokenWellFormed, he as issueCsrfToken, te as issueSession, S as issueUserSession, Se as mailSender, g as makeSessionRevocationChecker, R as memoryChallengeStore, h as memoryUserStore, fe as mintToken, r as needsRehash, p as newAuthId, E as otpauthUrl, i as parseScryptParams, xe as postgresUserStore, c as readSession, l as readSessionKeyed, u as resolveSessionSecret, d as resolveSessionSecrets, O as runSubjectGuards, v as sessionIdOfSubject, f as sessionSecretsOf, b as subjectFromUser, m as subjectMemberships, ve as verifyAssertion, ge as verifyCsrf, a as verifyPassword, o as verifyPasswordWithRehash, ye as verifyRegistration, D as verifyTotpCode, x as voltroPasswordStrategy };
296
+ export { pe as CSRF_COOKIE_NAME, me as CSRF_HEADER_NAME, le as MAGIC_LINK_TTL_S, K as MFA_PENDING_TTL_S, ue as PASSWORD_RESET_TTL_S, e as PasswordEmptyError, t as PasswordHashError, s as SESSION_COOKIE_NAME, W as TotpVerifyError, y as UserAlreadyExistsError, x as UserNotFoundError, re as authRoutesPlugin, c as clearSessionCookie, ie as configSessionSecrets, ne as dataStoreChallengeStore, _e as generateChallenge, ee as generateRecoveryCodes, te as generateTotpCode, M as generateTotpSecret, J as getAuthSessionStrategy, z as guardSubject, B as handleCsrf, X as handleListMemberships, q as handleListSessions, H as handleMagicLinkConsume, V as handleMagicLinkRequest, F as handleMfaEnrollStart, ae as handleMfaEnrollVerify, ce as handleMfaRegenerateRecoveryCodes, L as handleMfaUnenroll, se as handleMfaVerify, U as handlePasskeyAssertOptions, I as handlePasskeyAssertVerify, Y as handlePasskeyRegisterOptions, Z as handlePasskeyRegisterVerify, N as handlePasswordResetConfirm, T as handlePasswordResetRequest, oe as handleRevokeAllOtherSessions, P as handleRevokeSession, D as handleSignIn, E as handleSignOut, j as handleSignUp, G as handleSwitchTenant, n as hashPassword, de as hashToken, Q as isCsrfTokenWellFormed, he as issueCsrfToken, l as issueSession, w as issueUserSession, Se as mailSender, v as makeSessionRevocationChecker, R as memoryChallengeStore, _ as memoryUserStore, fe as mintToken, r as needsRehash, h as newAuthId, O as otpauthUrl, i as parseScryptParams, xe as postgresUserStore, u as readSession, d as readSessionKeyed, f as resolveSessionSecret, p as resolveSessionSecrets, A as runSubjectGuards, b as sessionIdOfSubject, m as sessionSecretsOf, S as subjectFromUser, g as subjectMemberships, ve as verifyAssertion, ge as verifyCsrf, a as verifyPassword, o as verifyPasswordWithRehash, ye as verifyRegistration, k as verifyTotpCode, C as voltroPasswordStrategy };