@voltro/plugin-auth 0.22.1 → 0.24.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.
@@ -35,9 +35,9 @@ var s = 3e4, c = 1e4, l = (e, t = {}) => {
35
35
  kind: "typeid",
36
36
  prefix: e
37
37
  }, e), p = (e = []) => {
38
- let t = /* @__PURE__ */ new Map(), r = /* @__PURE__ */ new Map(), i = [], a = [], o = [], s = [], c = [];
38
+ let t = /* @__PURE__ */ new Map(), r = /* @__PURE__ */ new Map(), i = [], a = [], o = [], s = [], c = [], l = /* @__PURE__ */ new Map();
39
39
  for (let n of e) t.set(n.id, n), r.set(n.email.toLowerCase(), n);
40
- let l = (e, i) => n.gen(function* () {
40
+ let p = (e, i) => n.gen(function* () {
41
41
  let a = t.get(e);
42
42
  if (!a) return yield* n.fail(new d({ userId: e }));
43
43
  let o = i(a);
@@ -56,20 +56,20 @@ var s = 3e4, c = 1e4, l = (e, t = {}) => {
56
56
  };
57
57
  return t.set(a.id, a), r.set(i, a), a;
58
58
  }),
59
- updatePassword: (e, t) => l(e, (e) => ({
59
+ updatePassword: (e, t) => p(e, (e) => ({
60
60
  ...e,
61
61
  passwordHash: t
62
62
  })),
63
- setMfaSecret: (e, t) => l(e, (e) => ({
63
+ setMfaSecret: (e, t) => p(e, (e) => ({
64
64
  ...e,
65
65
  mfaSecret: t,
66
66
  mfaEnrolledAt: null
67
67
  })),
68
- markMfaEnrolled: (e) => l(e, (e) => ({
68
+ markMfaEnrolled: (e) => p(e, (e) => ({
69
69
  ...e,
70
70
  mfaEnrolledAt: /* @__PURE__ */ new Date()
71
71
  })),
72
- clearMfa: (e) => l(e, (e) => ({
72
+ clearMfa: (e) => p(e, (e) => ({
73
73
  ...e,
74
74
  mfaSecret: null,
75
75
  mfaEnrolledAt: null
@@ -173,6 +173,27 @@ var s = 3e4, c = 1e4, l = (e, t = {}) => {
173
173
  counter: t,
174
174
  lastUsedAt: /* @__PURE__ */ new Date()
175
175
  }, !0);
176
+ }),
177
+ isLockedOut: (e, t) => n.sync(() => {
178
+ let n = l.get(e.toLowerCase());
179
+ return n?.lockedUntil != null && t < n.lockedUntil ? {
180
+ locked: !0,
181
+ retryAfterSeconds: Math.ceil((n.lockedUntil - t) / 1e3)
182
+ } : {
183
+ locked: !1,
184
+ retryAfterSeconds: 0
185
+ };
186
+ }),
187
+ recordLoginFailure: (e, t, r) => n.sync(() => {
188
+ let n = e.toLowerCase(), i = l.get(n), a = i?.windowStartedAt == null || t - i.windowStartedAt >= r.windowMs, o = i?.lockedUntil != null && t >= i.lockedUntil, s = !i || a || o, c = s ? 1 : i.failedCount + 1, u = s ? t : i.windowStartedAt, d = c >= r.maxAttempts ? t + r.lockMs : null;
189
+ l.set(n, {
190
+ failedCount: c,
191
+ windowStartedAt: u,
192
+ lockedUntil: d
193
+ });
194
+ }),
195
+ clearLoginFailures: (e) => n.sync(() => {
196
+ l.delete(e.toLowerCase());
176
197
  })
177
198
  };
178
199
  }, m = (e, t = {}) => {
@@ -4,6 +4,26 @@ import { SessionSecrets } from '@voltro/protocol/session';
4
4
  import { VoidIfEmpty } from 'effect/Types';
5
5
  import { YieldableError } from 'effect/Cause';
6
6
 
7
+ /**
8
+ * Brute-force lockout policy, resolved from `AuthConfig.lockout` and passed
9
+ * INTO the store on each failure so the store itself stays policy-free.
10
+ * Times in milliseconds; `now` everywhere is epoch-ms so tests can inject a
11
+ * clock. Kept keyed by EMAIL (not user id) on purpose — a locked account must
12
+ * not read differently from an unknown one, or the lock becomes an existence
13
+ * oracle. See `loginAttemptsTable`.
14
+ */
15
+ declare interface LockoutPolicy {
16
+ readonly maxAttempts: number;
17
+ readonly windowMs: number;
18
+ readonly lockMs: number;
19
+ }
20
+
21
+ declare interface LockoutState {
22
+ readonly locked: boolean;
23
+ /** Seconds until the lock lifts; 0 when not locked. */
24
+ readonly retryAfterSeconds: number;
25
+ }
26
+
7
27
  /** One tenant a user belongs to. The active tenant lives on the Subject;
8
28
  * the full set drives the switch-tenant menu + the switch-tenant guard. */
9
29
  declare interface MembershipRecord {
@@ -217,6 +237,16 @@ declare interface UserStore {
217
237
  * the write are ONE atomic statement at the store, so two replicas
218
238
  * racing the same counter can't both succeed. */
219
239
  readonly advancePasskeyCounter: (credentialId: string, newCounter: number) => Effect.Effect<boolean>;
240
+ /** Is this email currently locked out? Reads only — never mutates. Returns
241
+ * the remaining cooldown so the handler can surface a retry-after. */
242
+ readonly isLockedOut: (email: string, now: number) => Effect.Effect<LockoutState>;
243
+ /** Record one failed credential attempt for `email`. Opens a fresh window
244
+ * when the previous one (or a prior lock) has expired, otherwise increments;
245
+ * sets the lock once `failedCount` reaches `policy.maxAttempts`. Called only
246
+ * for a NOT-currently-locked email (the handler checks `isLockedOut` first). */
247
+ readonly recordLoginFailure: (email: string, now: number, policy: LockoutPolicy) => Effect.Effect<void>;
248
+ /** Clear the counter for `email` on a completed login. */
249
+ readonly clearLoginFailures: (email: string) => Effect.Effect<void>;
220
250
  }
221
251
 
222
252
  export declare const voltroPasswordStrategy: (options?: VoltroPasswordStrategyOptions) => AuthStrategy;
package/dist/strategy.js CHANGED
@@ -1,3 +1,3 @@
1
1
  import "./session.js";
2
- import { t as e } from "./strategy-D345j4df.js";
2
+ import { t as e } from "./strategy-Ce4FlGi4.js";
3
3
  export { e as voltroPasswordStrategy };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@voltro/plugin-auth",
3
- "version": "0.22.1",
3
+ "version": "0.24.0",
4
4
  "description": "Authentication primitives: password hashing, session creation, schema mixin + tables. Pairs with the `auth` app template for the UI; both independently usable. Server-side only (uses node:crypto).",
5
5
  "keywords": [
6
6
  "voltro",
@@ -78,8 +78,8 @@
78
78
  },
79
79
  "dependencies": {
80
80
  "@effect/sql": "^0.52.0",
81
- "@voltro/database": "0.22.1",
82
- "@voltro/protocol": "0.22.1"
81
+ "@voltro/database": "0.24.0",
82
+ "@voltro/protocol": "0.24.0"
83
83
  },
84
84
  "peerDependencies": {
85
85
  "effect": "^3.22.0",