@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.
@@ -1,6 +1,6 @@
1
1
  import { hashPassword as e, verifyPassword as t, verifyPasswordWithRehash as n } from "./password.js";
2
2
  import { clearSessionCookie as r, issueSession as i, readSessionKeyed as a, resolveSessionSecret as o, resolveSessionSecrets as s, sessionSecretsOf as c } from "./session.js";
3
- import { a as l, l as u, o as d, s as f, t as p } from "./strategy-D345j4df.js";
3
+ import { a as l, l as u, o as d, s as f, t as p } from "./strategy-Ce4FlGi4.js";
4
4
  import { hashToken as m, mintToken as h } from "./tokens.js";
5
5
  import { CSRF_COOKIE_NAME as g, CSRF_HEADER_NAME as _, issueCsrfToken as v, verifyCsrf as ee } from "./csrf.js";
6
6
  import { generateChallenge as y, verifyAssertion as b, verifyRegistration as te } from "./webauthn.js";
@@ -32,39 +32,43 @@ var ce = { ok: !0 }, C = (e, t) => S.gen(function* () {
32
32
  }, me = (e) => {
33
33
  let t = Buffer.alloc(8);
34
34
  return t.writeBigUInt64BE(BigInt(e), 0), t;
35
- }, O = (e, t) => {
35
+ }, he = (e, t) => {
36
36
  let n = ne(ue, Buffer.from(e));
37
37
  n.update(me(t));
38
38
  let r = n.digest(), i = r[r.length - 1] & 15, a = (r[i] & 127) << 24 | (r[i + 1] & 255) << 16 | (r[i + 2] & 255) << 8 | r[i + 3] & 255;
39
39
  return String(a % 10 ** T).padStart(T, "0");
40
- }, k = () => fe(x(le)), A = (e) => `otpauth://totp/${encodeURIComponent(`${e.issuer}:${e.accountName}`)}?${new URLSearchParams({
40
+ }, O = () => fe(x(le)), k = (e) => `otpauth://totp/${encodeURIComponent(`${e.issuer}:${e.accountName}`)}?${new URLSearchParams({
41
41
  secret: e.secret,
42
42
  issuer: e.issuer,
43
43
  algorithm: "SHA1",
44
44
  digits: String(T),
45
45
  period: String(w)
46
- }).toString()}`, he = (e, t = Date.now()) => {
46
+ }).toString()}`, ge = (e, t = Date.now()) => {
47
47
  let n = pe(e);
48
- return n ? O(n, Math.floor(t / 1e3 / w)) : "000000";
49
- }, j = (e, t, n = Date.now()) => S.gen(function* () {
48
+ return n ? he(n, Math.floor(t / 1e3 / w)) : "000000";
49
+ }, A = (e, t, n = Date.now()) => S.gen(function* () {
50
50
  let r = pe(e);
51
51
  if (!r) return yield* S.fail(new E({ reason: "malformedSecret" }));
52
52
  let i = t.replace(/\s+/g, "");
53
53
  if (!/^[0-9]{6}$/.test(i)) return yield* S.fail(new E({ reason: "invalidCode" }));
54
54
  let a = Math.floor(n / 1e3 / w), o = Buffer.from(i);
55
55
  for (let e = -1; e <= de; e++) {
56
- let t = O(r, a + e), n = Buffer.from(t);
56
+ let t = he(r, a + e), n = Buffer.from(t);
57
57
  if (n.length === o.length && re(n, o)) return !0;
58
58
  }
59
59
  return yield* S.fail(new E({ reason: "invalidCode" }));
60
- }), M = (e = 10) => {
60
+ }), j = (e = 10) => {
61
61
  let t = [];
62
62
  for (let n = 0; n < e; n++) {
63
63
  let e = fe(x(16)).slice(0, 25).match(/.{1,5}/g) ?? [];
64
64
  t.push(e.join("-"));
65
65
  }
66
66
  return t;
67
- }, N = (e) => e.secrets ?? (e.secret === void 0 ? s() : c(e.secret)), P = (e) => e.secret ?? o(), F = (e, t, n = {}) => ({
67
+ }, _e = 5, ve = 900, ye = 900, M = (e) => ({
68
+ maxAttempts: e.lockout?.maxAttempts ?? _e,
69
+ windowMs: (e.lockout?.windowSeconds ?? ve) * 1e3,
70
+ lockMs: (e.lockout?.lockSeconds ?? ye) * 1e3
71
+ }), N = (e) => e.secrets ?? (e.secret === void 0 ? s() : c(e.secret)), P = (e) => e.secret ?? o(), F = (e, t, n = {}) => ({
68
72
  status: e,
69
73
  body: JSON.stringify(t),
70
74
  contentType: "application/json; charset=utf-8",
@@ -82,6 +86,7 @@ var ce = { ok: !0 }, C = (e, t) => S.gen(function* () {
82
86
  sessionId: t
83
87
  }
84
88
  } : e, z = (e, t, n, r = {}) => S.gen(function* () {
89
+ yield* t.clearLoginFailures(e.email);
85
90
  let a = yield* t.listMemberships(e.id), o = l("sess");
86
91
  yield* t.insertSession({
87
92
  id: o,
@@ -108,16 +113,21 @@ var ce = { ok: !0 }, C = (e, t) => S.gen(function* () {
108
113
  error: r.code,
109
114
  message: r.message
110
115
  });
111
- }), ge = 300, V = (r, i, a) => S.gen(function* () {
116
+ }), be = 300, V = (r, i, a) => S.gen(function* () {
112
117
  if (!r.email || !r.password) return F(400, { error: "email and password required" });
113
- let o = yield* i.findByEmail(r.email), s = o?.passwordHash, c = typeof s == "string" && s.length > 0 ? yield* n(r.password, s) : yield* S.gen(function* () {
118
+ let o = yield* i.findByEmail(r.email), s = (a.now ?? Date.now)(), c = yield* i.isLockedOut(r.email, s);
119
+ if (c.locked) return F(429, {
120
+ error: "account_locked",
121
+ retryAfterSeconds: c.retryAfterSeconds
122
+ });
123
+ let u = o?.passwordHash, d = typeof u == "string" && u.length > 0 ? yield* n(r.password, u) : yield* S.gen(function* () {
114
124
  let n = yield* e("decoy-not-a-real-password").pipe(S.orElseSucceed(() => ""));
115
125
  return yield* t(r.password, n), { valid: !1 };
116
126
  });
117
- if (!o || !c.valid) return F(401, { error: "invalid credentials" });
118
- let u = yield* B(o, a);
119
- if (u) return u;
120
- if (c.rehash && (yield* i.updatePassword(o.id, c.rehash).pipe(S.ignore)), o.mfaEnrolledAt) {
127
+ if (!o || !d.valid) return yield* i.recordLoginFailure(r.email, s, M(a)), F(401, { error: "invalid credentials" });
128
+ let f = yield* B(o, a);
129
+ if (f) return f;
130
+ if (d.rehash && (yield* i.updatePassword(o.id, d.rehash).pipe(S.ignore)), o.mfaEnrolledAt) {
121
131
  let e = h("mfa-pending", 300);
122
132
  return yield* i.insertToken({
123
133
  id: l("tok"),
@@ -132,14 +142,14 @@ var ce = { ok: !0 }, C = (e, t) => S.gen(function* () {
132
142
  expiresInSeconds: 300
133
143
  });
134
144
  }
135
- let { setCookie: d, subject: f } = yield* z(o, i, a, {
145
+ let { setCookie: p, subject: m } = yield* z(o, i, a, {
136
146
  ipAddress: r.ipAddress ?? null,
137
147
  userAgent: r.userAgent ?? null
138
148
  });
139
- return r.redirectAfter ? I(a.successRedirect ?? "/", { setCookie: d }) : F(200, {
149
+ return r.redirectAfter ? I(a.successRedirect ?? "/", { setCookie: p }) : F(200, {
140
150
  ok: !0,
141
- subject: f
142
- }, { setCookie: d });
151
+ subject: m
152
+ }, { setCookie: p });
143
153
  }), H = (e, t, n) => S.gen(function* () {
144
154
  if (!e.pendingToken) return F(400, { error: "pending token required" });
145
155
  if (!e.code && !e.recoveryCode) return F(400, { error: "code or recoveryCode required" });
@@ -147,17 +157,22 @@ var ce = { ok: !0 }, C = (e, t) => S.gen(function* () {
147
157
  if (!r) return F(401, { error: "invalid or expired challenge" });
148
158
  let i = yield* t.findById(r.userId);
149
159
  if (!i || !i.mfaEnrolledAt || !i.mfaSecret) return F(401, { error: "mfa not enrolled" });
150
- let a = yield* B(i, n);
151
- if (a) return a;
152
- if (!(e.recoveryCode ? yield* t.consumeRecoveryCode(i.id, m(e.recoveryCode.trim())) : yield* j(i.mfaSecret, e.code ?? "").pipe(S.map(() => !0), S.catchTag("TotpVerifyError", () => S.succeed(!1))))) return F(401, { error: "invalid_code" });
153
- let { setCookie: o, subject: s } = yield* z(i, t, n, {
160
+ let a = (n.now ?? Date.now)(), o = yield* t.isLockedOut(i.email, a);
161
+ if (o.locked) return F(429, {
162
+ error: "account_locked",
163
+ retryAfterSeconds: o.retryAfterSeconds
164
+ });
165
+ let s = yield* B(i, n);
166
+ if (s) return s;
167
+ if (!(e.recoveryCode ? yield* t.consumeRecoveryCode(i.id, m(e.recoveryCode.trim())) : yield* A(i.mfaSecret, e.code ?? "").pipe(S.map(() => !0), S.catchTag("TotpVerifyError", () => S.succeed(!1))))) return yield* t.recordLoginFailure(i.email, a, M(n)), F(401, { error: "invalid_code" });
168
+ let { setCookie: c, subject: l } = yield* z(i, t, n, {
154
169
  ipAddress: e.ipAddress ?? null,
155
170
  userAgent: e.userAgent ?? null
156
171
  });
157
- return e.redirectAfter ? I(n.successRedirect ?? "/", { setCookie: o }) : F(200, {
172
+ return e.redirectAfter ? I(n.successRedirect ?? "/", { setCookie: c }) : F(200, {
158
173
  ok: !0,
159
- subject: s
160
- }, { setCookie: o });
174
+ subject: l
175
+ }, { setCookie: c });
161
176
  }), U = (t, n, r) => S.gen(function* () {
162
177
  if (!t.email || !t.password) return F(400, { error: "email and password required" });
163
178
  if (t.password.length < 8) return F(400, { error: "password must be at least 8 characters" });
@@ -205,11 +220,11 @@ var ce = { ok: !0 }, C = (e, t) => S.gen(function* () {
205
220
  }), G = (e, t) => S.gen(function* () {
206
221
  let n = yield* t.findById(e.userId);
207
222
  if (!n) return F(404, { error: "user_not_found" });
208
- let r = k();
223
+ let r = O();
209
224
  return yield* t.setMfaSecret(n.id, r).pipe(S.catchAll(() => S.succeed(n))), F(200, {
210
225
  ok: !0,
211
226
  secret: r,
212
- otpauthUrl: A({
227
+ otpauthUrl: k({
213
228
  issuer: e.issuer,
214
229
  accountName: e.accountName,
215
230
  secret: r
@@ -220,9 +235,9 @@ var ce = { ok: !0 }, C = (e, t) => S.gen(function* () {
220
235
  if (!n) return F(404, { error: "user_not_found" });
221
236
  if (!n.mfaSecret) return F(400, { error: "no_enrollment_in_progress" });
222
237
  if (n.mfaEnrolledAt) return F(409, { error: "already_enrolled" });
223
- if (!(yield* j(n.mfaSecret, e.code).pipe(S.map(() => !0), S.catchTag("TotpVerifyError", () => S.succeed(!1))))) return F(401, { error: "invalid_code" });
238
+ if (!(yield* A(n.mfaSecret, e.code).pipe(S.map(() => !0), S.catchTag("TotpVerifyError", () => S.succeed(!1))))) return F(401, { error: "invalid_code" });
224
239
  yield* t.markMfaEnrolled(n.id).pipe(S.catchAll(() => S.succeed(n)));
225
- let r = M();
240
+ let r = j();
226
241
  return yield* t.replaceRecoveryCodes(n.id, r.map((e) => m(e))).pipe(S.ignore), F(200, {
227
242
  ok: !0,
228
243
  enrolled: !0,
@@ -232,18 +247,18 @@ var ce = { ok: !0 }, C = (e, t) => S.gen(function* () {
232
247
  let n = yield* t.findById(e.userId);
233
248
  if (!n) return F(404, { error: "user_not_found" });
234
249
  if (!n.mfaEnrolledAt) return F(400, { error: "mfa_not_enrolled" });
235
- let r = M();
250
+ let r = j();
236
251
  return yield* t.replaceRecoveryCodes(n.id, r.map((e) => m(e))).pipe(S.ignore), F(200, {
237
252
  ok: !0,
238
253
  recoveryCodes: r
239
254
  });
240
- }), _e = (e, t) => S.gen(function* () {
255
+ }), xe = (e, t) => S.gen(function* () {
241
256
  let n = yield* t.findById(e.userId);
242
257
  return n ? (yield* t.clearMfa(n.id).pipe(S.catchAll(() => S.succeed(n))), yield* t.replaceRecoveryCodes(n.id, []).pipe(S.ignore), F(200, {
243
258
  ok: !0,
244
259
  enrolled: !1
245
260
  })) : F(404, { error: "user_not_found" });
246
- }), ve = (e) => {
261
+ }), Se = (e) => {
247
262
  let t = v(P(e)), n = ae(g, t, {
248
263
  httpOnly: !1,
249
264
  sameSite: "lax",
@@ -252,7 +267,7 @@ var ce = { ok: !0 }, C = (e, t) => S.gen(function* () {
252
267
  path: "/"
253
268
  });
254
269
  return F(200, { csrfToken: t }, { setCookie: n });
255
- }, ye = (e, t, n) => `${e}${t}${t.includes("?") ? "&" : "?"}token=${encodeURIComponent(n)}`, be = (e, t, n) => S.gen(function* () {
270
+ }, Ce = (e, t, n) => `${e}${t}${t.includes("?") ? "&" : "?"}token=${encodeURIComponent(n)}`, we = (e, t, n) => S.gen(function* () {
256
271
  if (!e.email) return F(400, { error: "email required" });
257
272
  let r = yield* t.findByEmail(e.email);
258
273
  if (r) {
@@ -264,7 +279,7 @@ var ce = { ok: !0 }, C = (e, t) => S.gen(function* () {
264
279
  purpose: "magic-link",
265
280
  expiresAt: i.expiresAt
266
281
  }).pipe(S.ignore);
267
- let a = ye(n.appBaseUrl ?? "", e.callbackPath ?? "/auth/magic-link/callback", i.token);
282
+ let a = Ce(n.appBaseUrl ?? "", e.callbackPath ?? "/auth/magic-link/callback", i.token);
268
283
  n.sendEmail && (yield* S.tryPromise(() => n.sendEmail({
269
284
  to: r.email,
270
285
  kind: "magic-link",
@@ -275,7 +290,7 @@ var ce = { ok: !0 }, C = (e, t) => S.gen(function* () {
275
290
  })).pipe(S.ignore));
276
291
  }
277
292
  return F(202, { ok: !0 });
278
- }), xe = (e, t, n) => S.gen(function* () {
293
+ }), Te = (e, t, n) => S.gen(function* () {
279
294
  if (!e.token) return F(400, { error: "token required" });
280
295
  let r = yield* t.consumeToken(m(e.token), "magic-link");
281
296
  if (!r) return F(401, { error: "invalid or expired token" });
@@ -301,7 +316,7 @@ var ce = { ok: !0 }, C = (e, t) => S.gen(function* () {
301
316
  ok: !0,
302
317
  subject: u
303
318
  }, { setCookie: d.setCookie });
304
- }), Se = (e, t, n) => S.gen(function* () {
319
+ }), Ee = (e, t, n) => S.gen(function* () {
305
320
  if (!e.email) return F(400, { error: "email required" });
306
321
  let r = yield* t.findByEmail(e.email);
307
322
  if (r) {
@@ -313,7 +328,7 @@ var ce = { ok: !0 }, C = (e, t) => S.gen(function* () {
313
328
  purpose: "password-reset",
314
329
  expiresAt: i.expiresAt
315
330
  }).pipe(S.ignore);
316
- let a = ye(n.appBaseUrl ?? "", e.callbackPath ?? "/auth/password-reset/callback", i.token);
331
+ let a = Ce(n.appBaseUrl ?? "", e.callbackPath ?? "/auth/password-reset/callback", i.token);
317
332
  n.sendEmail && (yield* S.tryPromise(() => n.sendEmail({
318
333
  to: r.email,
319
334
  kind: "password-reset",
@@ -324,14 +339,14 @@ var ce = { ok: !0 }, C = (e, t) => S.gen(function* () {
324
339
  })).pipe(S.ignore));
325
340
  }
326
341
  return F(202, { ok: !0 });
327
- }), Ce = (t, n) => S.gen(function* () {
342
+ }), De = (t, n) => S.gen(function* () {
328
343
  if (!t.token || !t.newPassword) return F(400, { error: "token and newPassword required" });
329
344
  if (t.newPassword.length < 8) return F(400, { error: "password must be at least 8 characters" });
330
345
  let r = yield* n.consumeToken(m(t.token), "password-reset");
331
346
  if (!r) return F(401, { error: "invalid or expired token" });
332
347
  let i = yield* e(t.newPassword).pipe(S.orElseSucceed(() => ""));
333
348
  return i ? (yield* n.updatePassword(r.userId, i).pipe(S.ignore), yield* n.revokeAllOtherSessions(r.userId, "__none__").pipe(S.ignore), F(200, { ok: !0 })) : F(500, { error: "hash_failed" });
334
- }), we = (e, t) => S.gen(function* () {
349
+ }), Oe = (e, t) => S.gen(function* () {
335
350
  return F(200, { sessions: (yield* t.listSessions(e.userId)).map((e) => ({
336
351
  id: e.id,
337
352
  tenantId: e.tenantId,
@@ -341,17 +356,17 @@ var ce = { ok: !0 }, C = (e, t) => S.gen(function* () {
341
356
  lastSeenAt: e.lastSeenAt.toISOString(),
342
357
  expiresAt: e.expiresAt.toISOString()
343
358
  })) });
344
- }), Te = (e, t) => S.gen(function* () {
359
+ }), ke = (e, t) => S.gen(function* () {
345
360
  return (yield* t.revokeSession(e.userId, e.sessionId)) ? F(200, {
346
361
  ok: !0,
347
362
  revoked: 1
348
363
  }) : F(404, { error: "session_not_found" });
349
- }), Ee = (e, t) => S.gen(function* () {
364
+ }), Ae = (e, t) => S.gen(function* () {
350
365
  return F(200, {
351
366
  ok: !0,
352
367
  revoked: yield* t.revokeAllOtherSessions(e.userId, e.keepSessionId)
353
368
  });
354
- }), De = (e, t, n, r) => S.gen(function* () {
369
+ }), je = (e, t, n, r) => S.gen(function* () {
355
370
  let a = yield* t.findById(e.userId);
356
371
  if (!a) return F(404, { error: "user_not_found" });
357
372
  let o = yield* t.membershipRole(e.userId, e.targetTenantId);
@@ -373,7 +388,7 @@ var ce = { ok: !0 }, C = (e, t) => S.gen(function* () {
373
388
  role: o,
374
389
  subject: u
375
390
  }, { setCookie: d.setCookie });
376
- }), Oe = (e, t) => S.gen(function* () {
391
+ }), Me = (e, t) => S.gen(function* () {
377
392
  return F(200, { memberships: (yield* t.listMemberships(e.userId)).map((e) => ({
378
393
  tenantId: e.tenantId,
379
394
  role: e.role,
@@ -384,7 +399,7 @@ var ce = { ok: !0 }, C = (e, t) => S.gen(function* () {
384
399
  body: JSON.stringify(t),
385
400
  contentType: "application/json; charset=utf-8",
386
401
  ...n
387
- }), ke = 3600 * 24 * 7, Ae = () => {
402
+ }), Ne = 3600 * 24 * 7, Pe = () => {
388
403
  let e = /* @__PURE__ */ new Map();
389
404
  return {
390
405
  put: (t, n, r) => S.sync(() => {
@@ -398,7 +413,7 @@ var ce = { ok: !0 }, C = (e, t) => S.gen(function* () {
398
413
  return e.delete(t), !n || n.expiresAt < Date.now() ? null : n.challenge;
399
414
  })
400
415
  };
401
- }, je = (e) => ({
416
+ }, Fe = (e) => ({
402
417
  put: (t, n, r) => S.gen(function* () {
403
418
  let i = /* @__PURE__ */ new Date(), a = new Date(i.getTime() + r * 1e3);
404
419
  (yield* e`
@@ -418,7 +433,7 @@ var ce = { ok: !0 }, C = (e, t) => S.gen(function* () {
418
433
  `)[0];
419
434
  return n && n.expiresAt.getTime() > Date.now() ? n.challenge : null;
420
435
  }).pipe(S.orElseSucceed(() => null))
421
- }), Y = 300, Me = (e, t, n, r) => S.gen(function* () {
436
+ }), Y = 300, Ie = (e, t, n, r) => S.gen(function* () {
422
437
  if (!(yield* t.findById(e.userId))) return J(404, { error: "user_not_found" });
423
438
  let i = y();
424
439
  yield* n.put(`${e.userId}:register`, i, Y);
@@ -451,7 +466,7 @@ var ce = { ok: !0 }, C = (e, t) => S.gen(function* () {
451
466
  },
452
467
  timeout: Y * 1e3
453
468
  });
454
- }), Ne = (e, t, n, r) => S.gen(function* () {
469
+ }), Le = (e, t, n, r) => S.gen(function* () {
455
470
  if (!(yield* t.findById(e.userId))) return J(404, { error: "user_not_found" });
456
471
  let i = yield* n.take(`${e.userId}:register`);
457
472
  if (!i) return J(400, { error: "no_pending_challenge" });
@@ -473,7 +488,7 @@ var ce = { ok: !0 }, C = (e, t) => S.gen(function* () {
473
488
  ok: !0,
474
489
  credentialId: a.result.credentialId
475
490
  })) : J(400, { error: a.error });
476
- }), Pe = (e, t, n, r) => S.gen(function* () {
491
+ }), Re = (e, t, n, r) => S.gen(function* () {
477
492
  let i = y(), a = e.userId ? `${e.userId}:assert` : "*:assert";
478
493
  yield* n.put(a, i, Y);
479
494
  let o = e.userId ? (yield* t.listPasskeys(e.userId)).map((e) => ({
@@ -487,7 +502,7 @@ var ce = { ok: !0 }, C = (e, t) => S.gen(function* () {
487
502
  userVerification: "preferred",
488
503
  timeout: Y * 1e3
489
504
  });
490
- }), Fe = (e, t, n, r, a) => S.gen(function* () {
505
+ }), ze = (e, t, n, r, a) => S.gen(function* () {
491
506
  let o = yield* t.findPasskey(e.credentialId);
492
507
  if (!o) return J(401, { error: "unknown_credential" });
493
508
  let s = e.userId ? `${e.userId}:assert` : "*:assert", c = yield* n.take(s);
@@ -513,7 +528,7 @@ var ce = { ok: !0 }, C = (e, t) => S.gen(function* () {
513
528
  id: h,
514
529
  userId: d.id,
515
530
  tenantId: d.tenantId,
516
- expiresAt: new Date(Date.now() + ke * 1e3),
531
+ expiresAt: new Date(Date.now() + Ne * 1e3),
517
532
  ipAddress: null,
518
533
  userAgent: null
519
534
  }).pipe(S.ignore);
@@ -524,7 +539,7 @@ var ce = { ok: !0 }, C = (e, t) => S.gen(function* () {
524
539
  sessionId: h
525
540
  }
526
541
  } : g, v = i(_, N(a), {
527
- ttlSeconds: ke,
542
+ ttlSeconds: Ne,
528
543
  ...a.cookieDomain ? { domain: a.cookieDomain } : {},
529
544
  secure: a.cookieSecure ?? !0
530
545
  });
@@ -532,10 +547,10 @@ var ce = { ok: !0 }, C = (e, t) => S.gen(function* () {
532
547
  ok: !0,
533
548
  subject: _
534
549
  }, { setCookie: v.setCookie });
535
- }), Ie = (e) => {
536
- for (let t of e) if (Le(t)) return t.auth.sessionStrategy;
550
+ }), Be = (e) => {
551
+ for (let t of e) if (Ve(t)) return t.auth.sessionStrategy;
537
552
  return null;
538
- }, Le = (e) => typeof e.auth == "object" && e.auth !== null && typeof e.auth.sessionStrategy == "object", Re = "/auth", ze = (e) => {
553
+ }, Ve = (e) => typeof e.auth == "object" && e.auth !== null && typeof e.auth.sessionStrategy == "object", He = "/auth", Ue = (e) => {
539
554
  let t = Buffer.from(e.rawBody).toString("utf8");
540
555
  if (!t) return {};
541
556
  if ((e.headers["content-type"] ?? "").includes("application/x-www-form-urlencoded")) {
@@ -561,8 +576,8 @@ var ce = { ok: !0 }, C = (e, t) => S.gen(function* () {
561
576
  status: e,
562
577
  body: JSON.stringify(t),
563
578
  contentType: "application/json; charset=utf-8"
564
- }), Q = (e) => typeof e == "string" ? e : void 0, $ = (e, t) => typeof t == "string" && t.length > 0 ? { [e]: t } : {}, Be = (e) => {
565
- let t = e.prefix ?? Re, n = e.store, r = e.challengeStore ?? Ae(), o = {
579
+ }), Q = (e) => typeof e == "string" ? e : void 0, $ = (e, t) => typeof t == "string" && t.length > 0 ? { [e]: t } : {}, We = (e) => {
580
+ let t = e.prefix ?? He, n = e.store, r = e.challengeStore ?? Pe(), o = {
566
581
  ...e.secret === void 0 ? {} : { secret: e.secret },
567
582
  ...e.secrets === void 0 ? {} : { secrets: e.secrets },
568
583
  defaultTenantId: e.defaultTenantId,
@@ -571,7 +586,9 @@ var ce = { ok: !0 }, C = (e, t) => S.gen(function* () {
571
586
  ...e.successRedirect === void 0 ? {} : { successRedirect: e.successRedirect },
572
587
  ...e.sendEmail === void 0 ? {} : { sendEmail: e.sendEmail },
573
588
  ...e.appBaseUrl === void 0 ? {} : { appBaseUrl: e.appBaseUrl },
574
- ...e.subjectGuards === void 0 ? {} : { subjectGuards: e.subjectGuards }
589
+ ...e.subjectGuards === void 0 ? {} : { subjectGuards: e.subjectGuards },
590
+ ...e.lockout === void 0 ? {} : { lockout: e.lockout },
591
+ ...e.now === void 0 ? {} : { now: e.now }
575
592
  }, s = u(n, e.sessionRevocation ?? {}), c = async (e) => {
576
593
  let t = N(o), r = a(e.headers.cookie, t);
577
594
  if (!r) return null;
@@ -610,8 +627,8 @@ var ce = { ok: !0 }, C = (e, t) => S.gen(function* () {
610
627
  method: "*",
611
628
  path: t,
612
629
  handle: async (i) => {
613
- let a = i.method === "GET" ? {} : ze(i);
614
- if (i.method === "GET" && m(i, "/csrf")) return X(ve(o));
630
+ let a = i.method === "GET" ? {} : Ue(i);
631
+ if (i.method === "GET" && m(i, "/csrf")) return X(Se(o));
615
632
  if (i.method === "POST" && m(i, "/sign-in")) return h(V({
616
633
  email: Q(a.email) ?? "",
617
634
  password: Q(a.password) ?? "",
@@ -629,19 +646,19 @@ var ce = { ok: !0 }, C = (e, t) => S.gen(function* () {
629
646
  let e = await c(i);
630
647
  return e && e.subject.type === "user" && e.sessionId && (await S.runPromise(n.revokeSession(e.subject.id, e.sessionId)), s.invalidate(e.sessionId)), X(W(o));
631
648
  }
632
- if (i.method === "POST" && m(i, "/magic-link")) return h(be({
649
+ if (i.method === "POST" && m(i, "/magic-link")) return h(we({
633
650
  email: Q(a.email) ?? "",
634
651
  ...$("callbackPath", a.callbackPath)
635
652
  }, n, o));
636
- if (i.method === "POST" && m(i, "/magic-link/callback")) return h(xe({
653
+ if (i.method === "POST" && m(i, "/magic-link/callback")) return h(Te({
637
654
  token: Q(a.token) ?? "",
638
655
  ...a.redirectAfter === void 0 ? {} : { redirectAfter: !!a.redirectAfter }
639
656
  }, n, o));
640
- if (i.method === "POST" && m(i, "/password-reset")) return h(Se({
657
+ if (i.method === "POST" && m(i, "/password-reset")) return h(Ee({
641
658
  email: Q(a.email) ?? "",
642
659
  ...$("callbackPath", a.callbackPath)
643
660
  }, n, o));
644
- if (i.method === "POST" && m(i, "/password-reset/confirm")) return h(Ce({
661
+ if (i.method === "POST" && m(i, "/password-reset/confirm")) return h(De({
645
662
  token: Q(a.token) ?? "",
646
663
  newPassword: Q(a.newPassword) ?? ""
647
664
  }, n));
@@ -653,8 +670,8 @@ var ce = { ok: !0 }, C = (e, t) => S.gen(function* () {
653
670
  ...i.headers["x-forwarded-for"] ? { ipAddress: i.headers["x-forwarded-for"] } : {},
654
671
  ...i.headers["user-agent"] ? { userAgent: i.headers["user-agent"] } : {}
655
672
  }, n, o));
656
- if (e.passkey && i.method === "POST" && m(i, "/passkey/assert/options")) return h(Pe({ ...$("userId", a.userId) }, n, r, e.passkey));
657
- if (e.passkey && i.method === "POST" && m(i, "/passkey/assert/verify")) return h(Fe({
673
+ if (e.passkey && i.method === "POST" && m(i, "/passkey/assert/options")) return h(Re({ ...$("userId", a.userId) }, n, r, e.passkey));
674
+ if (e.passkey && i.method === "POST" && m(i, "/passkey/assert/verify")) return h(ze({
658
675
  credentialId: Q(a.credentialId) ?? "",
659
676
  clientDataJSON: Q(a.clientDataJSON) ?? "",
660
677
  authenticatorData: Q(a.authenticatorData) ?? "",
@@ -672,11 +689,11 @@ var ce = { ok: !0 }, C = (e, t) => S.gen(function* () {
672
689
  }
673
690
  if (i.method === "GET" && m(i, "/sessions")) {
674
691
  let e = p();
675
- return "status" in e ? e : _(we({ userId: e.userId }, n));
692
+ return "status" in e ? e : _(Oe({ userId: e.userId }, n));
676
693
  }
677
694
  if (i.method === "GET" && m(i, "/memberships")) {
678
695
  let e = p();
679
- return "status" in e ? e : _(Oe({ userId: e.userId }, n));
696
+ return "status" in e ? e : _(Me({ userId: e.userId }, n));
680
697
  }
681
698
  if (i.method === "POST" && m(i, "/sessions/revoke")) {
682
699
  let e = p();
@@ -684,21 +701,21 @@ var ce = { ok: !0 }, C = (e, t) => S.gen(function* () {
684
701
  let t = g();
685
702
  if (t) return t;
686
703
  let r = Q(a.sessionId) ?? "";
687
- return _(Te({
704
+ return _(ke({
688
705
  userId: e.userId,
689
706
  sessionId: r
690
707
  }, n)).then((e) => (r && s.invalidate(r), e));
691
708
  }
692
709
  if (i.method === "POST" && m(i, "/sessions/revoke-others")) {
693
710
  let e = p();
694
- return "status" in e ? e : g() || _(Ee({
711
+ return "status" in e ? e : g() || _(Ae({
695
712
  userId: e.userId,
696
713
  keepSessionId: u?.sessionId ?? "__none__"
697
714
  }, n));
698
715
  }
699
716
  if (i.method === "POST" && m(i, "/switch-tenant")) {
700
717
  let t = p();
701
- return "status" in t ? t : g() || h(De({
718
+ return "status" in t ? t : g() || h(je({
702
719
  userId: t.userId,
703
720
  targetTenantId: Q(a.targetTenantId) ?? "",
704
721
  ...typeof a.clientId == "number" ? { clientId: a.clientId } : {},
@@ -727,7 +744,7 @@ var ce = { ok: !0 }, C = (e, t) => S.gen(function* () {
727
744
  }
728
745
  if (e.mfa && i.method === "POST" && m(i, "/mfa/unenroll")) {
729
746
  let e = p();
730
- return "status" in e ? e : g() || _(_e({ userId: e.userId }, n));
747
+ return "status" in e ? e : g() || _(xe({ userId: e.userId }, n));
731
748
  }
732
749
  if (e.mfa && i.method === "POST" && m(i, "/mfa/recovery-codes/regenerate")) {
733
750
  let e = p();
@@ -735,14 +752,14 @@ var ce = { ok: !0 }, C = (e, t) => S.gen(function* () {
735
752
  }
736
753
  if (e.passkey && i.method === "POST" && m(i, "/passkey/register/options")) {
737
754
  let t = p();
738
- return "status" in t ? t : g() || _(Me({
755
+ return "status" in t ? t : g() || _(Ie({
739
756
  userId: t.userId,
740
757
  userName: Q(a.userName) ?? t.userId
741
758
  }, n, r, e.passkey));
742
759
  }
743
760
  if (e.passkey && i.method === "POST" && m(i, "/passkey/register/verify")) {
744
761
  let t = p();
745
- return "status" in t ? t : g() || _(Ne({
762
+ return "status" in t ? t : g() || _(Le({
746
763
  userId: t.userId,
747
764
  credentialId: Q(a.credentialId) ?? "",
748
765
  clientDataJSON: Q(a.clientDataJSON) ?? "",
@@ -756,7 +773,7 @@ var ce = { ok: !0 }, C = (e, t) => S.gen(function* () {
756
773
  name: e.name ? `@voltro/plugin-auth#${e.name}` : "@voltro/plugin-auth",
757
774
  description: "Auth routes — mounts the full auth surface (password, magic-link, password-reset, CSRF, session probe/list/revoke, memberships, switch-tenant, passkey ceremonies) under /auth on the framework listener, with keyed secret rotation + request-time revocation.",
758
775
  permissions: [],
759
- declaredEnv: Ve,
776
+ declaredEnv: Ge,
760
777
  httpRoutes: [v],
761
778
  onActivate: (n) => S.sync(() => {
762
779
  n.logger.info("auth routes active", {
@@ -776,7 +793,7 @@ var ce = { ok: !0 }, C = (e, t) => S.gen(function* () {
776
793
  revocation: s
777
794
  }) }
778
795
  };
779
- }, Ve = [
796
+ }, Ge = [
780
797
  {
781
798
  name: "VOLTRO_SESSION_SECRET_PREVIOUS",
782
799
  required: !1,
@@ -800,4 +817,4 @@ var ce = { ok: !0 }, C = (e, t) => S.gen(function* () {
800
817
  }
801
818
  ];
802
819
  //#endregion
803
- export { z as A, Se as C, W as D, V as E, A as F, j as I, C as L, M, he as N, U as O, k as P, Ce as S, Te as T, G as _, Fe as a, _e as b, Ae as c, B as d, ve as f, be as g, xe as h, Pe as i, E as j, De as k, ge as l, we as m, Ie as n, Me as o, Oe as p, je as r, Ne as s, Be as t, N as u, K as v, Ee as w, H as x, q as y };
820
+ export { z as A, Ee as C, W as D, V as E, k as F, A as I, C as L, j as M, ge as N, U as O, O as P, De as S, ke as T, G as _, ze as a, xe as b, Pe as c, B as d, Se as f, we as g, Te as h, Re as i, E as j, je as k, be as l, Oe as m, Be as n, Ie as o, Me as p, Fe as r, Le as s, We as t, N as u, K as v, Ae as w, H as x, q as y };
package/dist/plugin.d.ts CHANGED
@@ -49,6 +49,20 @@ declare interface AuthConfig {
49
49
  * but any concern (unverified email, suspended tenant, …) can hook here.
50
50
  * Absent / empty ⇒ every authenticated user proceeds unchanged. */
51
51
  readonly subjectGuards?: ReadonlyArray<SubjectGuard>;
52
+ /** Brute-force lockout. After `maxAttempts` failed credential attempts
53
+ * (wrong password OR wrong MFA code) within `windowSeconds`, the account —
54
+ * keyed by EMAIL, so an unknown address locks exactly like a real one and
55
+ * the lock can't be used as an existence oracle — is refused with a 429 for
56
+ * `lockSeconds`; a completed login clears the counter. ON by default (a
57
+ * security default, not opt-in); raise `maxAttempts` very high to disable. */
58
+ readonly lockout?: {
59
+ readonly maxAttempts?: number;
60
+ readonly windowSeconds?: number;
61
+ readonly lockSeconds?: number;
62
+ };
63
+ /** Injectable clock (epoch-ms) for the lockout counter. Default `Date.now`;
64
+ * tests pass a controllable one to exercise window/lock expiry. */
65
+ readonly now?: () => number;
52
66
  }
53
67
 
54
68
  /**
@@ -108,6 +122,26 @@ declare interface ChallengeStore {
108
122
  */
109
123
  export declare const getAuthSessionStrategy: (plugins: ReadonlyArray<VoltroPlugin>) => AuthStrategy | null;
110
124
 
125
+ /**
126
+ * Brute-force lockout policy, resolved from `AuthConfig.lockout` and passed
127
+ * INTO the store on each failure so the store itself stays policy-free.
128
+ * Times in milliseconds; `now` everywhere is epoch-ms so tests can inject a
129
+ * clock. Kept keyed by EMAIL (not user id) on purpose — a locked account must
130
+ * not read differently from an unknown one, or the lock becomes an existence
131
+ * oracle. See `loginAttemptsTable`.
132
+ */
133
+ declare interface LockoutPolicy {
134
+ readonly maxAttempts: number;
135
+ readonly windowMs: number;
136
+ readonly lockMs: number;
137
+ }
138
+
139
+ declare interface LockoutState {
140
+ readonly locked: boolean;
141
+ /** Seconds until the lock lifts; 0 when not locked. */
142
+ readonly retryAfterSeconds: number;
143
+ }
144
+
111
145
  /** One tenant a user belongs to. The active tenant lives on the Subject;
112
146
  * the full set drives the switch-tenant menu + the switch-tenant guard. */
113
147
  declare interface MembershipRecord {
@@ -396,6 +430,16 @@ declare interface UserStore {
396
430
  * the write are ONE atomic statement at the store, so two replicas
397
431
  * racing the same counter can't both succeed. */
398
432
  readonly advancePasskeyCounter: (credentialId: string, newCounter: number) => Effect.Effect<boolean>;
433
+ /** Is this email currently locked out? Reads only — never mutates. Returns
434
+ * the remaining cooldown so the handler can surface a retry-after. */
435
+ readonly isLockedOut: (email: string, now: number) => Effect.Effect<LockoutState>;
436
+ /** Record one failed credential attempt for `email`. Opens a fresh window
437
+ * when the previous one (or a prior lock) has expired, otherwise increments;
438
+ * sets the lock once `failedCount` reaches `policy.maxAttempts`. Called only
439
+ * for a NOT-currently-locked email (the handler checks `isLockedOut` first). */
440
+ readonly recordLoginFailure: (email: string, now: number, policy: LockoutPolicy) => Effect.Effect<void>;
441
+ /** Clear the counter for `email` on a completed login. */
442
+ readonly clearLoginFailures: (email: string) => Effect.Effect<void>;
399
443
  }
400
444
 
401
445
  export { }
package/dist/plugin.js CHANGED
@@ -1,4 +1,4 @@
1
1
  import "./session.js";
2
- import { n as e, t } from "./plugin-HgTMFY3m.js";
2
+ import { n as e, t } from "./plugin-ZXs8LFn0.js";
3
3
  import "./csrf.js";
4
4
  export { t as authRoutesPlugin, e as getAuthSessionStrategy };
package/dist/schema.d.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  import { TableLike } from '@voltro/database';
2
2
 
3
3
  /** Tables the plugin contributes, as an array. */
4
- export declare const authTables: readonly [TableLike, TableLike, TableLike, TableLike, TableLike, TableLike, TableLike];
4
+ export declare const authTables: readonly [TableLike, TableLike, TableLike, TableLike, TableLike, TableLike, TableLike, TableLike];
5
5
 
6
6
  /**
7
7
  * Auth-token table. Single-use, hashed, expiring tokens for the
@@ -12,6 +12,16 @@ export declare const authTables: readonly [TableLike, TableLike, TableLike, Tabl
12
12
  */
13
13
  export declare const authTokensTable: TableLike;
14
14
 
15
+ /**
16
+ * Login-attempt counters for brute-force lockout, keyed by normalized
17
+ * email — NOT by user id. Tracking the EMAIL (whether or not a user row
18
+ * exists) is what keeps a locked account from leaking its existence: an
19
+ * unknown email that has been hammered locks exactly like a real one, so
20
+ * the refusal is uniform. `handleSignIn` records a failure on every wrong
21
+ * credential and clears the row on a completed login.
22
+ */
23
+ export declare const loginAttemptsTable: TableLike;
24
+
15
25
  /**
16
26
  * Memberships table. A user belongs to MANY tenants; each row is one
17
27
  * membership with a `role`. The active tenant lives on the Subject, but
package/dist/schema.js CHANGED
@@ -53,14 +53,23 @@ var a = n("users", {
53
53
  challenge: r(),
54
54
  expiresAt: i(),
55
55
  createdAt: i().default("now")
56
- }), f = [
56
+ }), f = n("loginAttempts", {
57
+ id: e(),
58
+ email: r().unique(),
59
+ failedCount: t().default(0),
60
+ windowStartedAt: i().nullable(),
61
+ lockedUntil: i().nullable(),
62
+ createdAt: i().default("now"),
63
+ updatedAt: i().default("now").onUpdate("now")
64
+ }), p = [
57
65
  a,
58
66
  o,
59
67
  s,
60
68
  c,
61
69
  l,
62
70
  u,
63
- d
71
+ d,
72
+ f
64
73
  ];
65
74
  //#endregion
66
- export { f as authTables, c as authTokensTable, s as membershipsTable, d as passkeyChallengesTable, l as passkeysTable, u as recoveryCodesTable, o as sessionsTable, a as usersTable };
75
+ export { p as authTables, c as authTokensTable, f as loginAttemptsTable, s as membershipsTable, d as passkeyChallengesTable, l as passkeysTable, u as recoveryCodesTable, o as sessionsTable, a as usersTable };