@voltro/plugin-auth 0.71.0 → 0.72.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/plugin.d.ts CHANGED
@@ -204,6 +204,26 @@ declare interface GrantRoleContext {
204
204
  readonly tenantId: string;
205
205
  }
206
206
 
207
+ /** Identity — who a user is. The only role an app with its own identity table has to write itself; everything else composes from the shipped store. */
208
+ declare interface IdentityStore {
209
+ readonly findByEmail: (email: string) => Effect.Effect<UserRecord | null>;
210
+ readonly findById: (id: string) => Effect.Effect<UserRecord | null>;
211
+ readonly insert: (user: Omit<UserRecord, 'createdAt'>) => Effect.Effect<UserRecord, UserAlreadyExistsError>;
212
+ /** Replace a user's stored password hash. Used by password-reset AND
213
+ * by rehash-on-verify (silently upgrading an under-cost hash on a
214
+ * successful sign-in). */
215
+ readonly updatePassword: (userId: string, newHash: string) => Effect.Effect<UserRecord, UserNotFoundError>;
216
+ /**
217
+ * Stamp `emailVerifiedAt` — the address has been proven.
218
+ *
219
+ * IDEMPOTENT and non-regressing: a user already verified keeps their
220
+ * ORIGINAL timestamp. "When was this address first proven" is the question
221
+ * the column answers, and re-stamping it on every later magic-link login
222
+ * would quietly turn it into "when did they last click something".
223
+ */
224
+ readonly markEmailVerified: (userId: string) => Effect.Effect<UserRecord, UserNotFoundError>;
225
+ }
226
+
207
227
  declare interface ImpersonationAuditEvent {
208
228
  readonly kind: ImpersonationAuditKind;
209
229
  readonly at: Date;
@@ -298,6 +318,17 @@ declare interface ImpersonationGrantRecord {
298
318
  readonly userAgent?: string | null;
299
319
  }
300
320
 
321
+ /** Impersonation grants. */
322
+ declare interface ImpersonationStore {
323
+ /** Record an impersonation grant when the impersonated session is minted. */
324
+ readonly insertImpersonationGrant: (grant: Omit<ImpersonationGrantRecord, 'startedAt' | 'endedAt' | 'endedReason'>) => Effect.Effect<ImpersonationGrantRecord>;
325
+ /** The grant for an impersonated session, or null. The stop path's lookup. */
326
+ readonly findImpersonationGrantBySession: (sessionId: string) => Effect.Effect<ImpersonationGrantRecord | null>;
327
+ /** Close a grant. Only an OPEN grant is closed (`endedAt IS NULL`), so a
328
+ * double stop is a no-op rather than a rewritten end time. */
329
+ readonly endImpersonationGrant: (sessionId: string, endedReason: string, now: Date) => Effect.Effect<boolean>;
330
+ }
331
+
301
332
  /**
302
333
  * A tenant invitation — the grant that turns into a `memberships` row when
303
334
  * redeemed. See `invitationsTable` for why `role` lives here and not in the
@@ -355,6 +386,38 @@ declare interface InvitationsConfig {
355
386
  readonly canGrantRole?: (ctx: GrantRoleContext) => boolean;
356
387
  }
357
388
 
389
+ /** Tenant invitations. */
390
+ declare interface InvitationStore {
391
+ /** Persist an invitation (token already hashed). */
392
+ readonly insertInvitation: (invitation: Omit<InvitationRecord, 'acceptedAt' | 'acceptedByUserId' | 'revokedAt' | 'revokedByUserId' | 'createdAt'>) => Effect.Effect<InvitationRecord>;
393
+ /** Look an invitation up by token hash WITHOUT redeeming it — the read the
394
+ * "what am I being invited to" preview needs. Returns the row whatever its
395
+ * state; the caller decides what an expired or revoked one means. */
396
+ readonly findInvitationByHash: (tokenHash: string) => Effect.Effect<InvitationRecord | null>;
397
+ /**
398
+ * Atomically redeem an invitation: returns the row only when it exists, is
399
+ * unaccepted, unrevoked and unexpired — and stamps `acceptedAt` +
400
+ * `acceptedByUserId` in the same step. Null otherwise.
401
+ *
402
+ * The single-use guard, and it has to be ONE statement: two clicks on the
403
+ * same emailed link race, and a read-then-write would let both through and
404
+ * write the membership twice.
405
+ */
406
+ readonly acceptInvitation: (tokenHash: string, acceptedByUserId: string, now: Date) => Effect.Effect<InvitationRecord | null>;
407
+ /** Withdraw a PENDING invitation, scoped to its tenant so an admin of one
408
+ * tenant cannot revoke another's by id. Returns whether a row changed. */
409
+ readonly revokeInvitation: (invitationId: string, tenantId: string, revokedByUserId: string, now: Date) => Effect.Effect<boolean>;
410
+ /** Every invitation for a tenant, newest first. The admin list. */
411
+ readonly listInvitations: (tenantId: string) => Effect.Effect<ReadonlyArray<InvitationRecord>>;
412
+ /** How many invitations for this tenant are still redeemable — the bound
413
+ * `maxPending` enforces. */
414
+ readonly countPendingInvitations: (tenantId: string, now: Date) => Effect.Effect<number>;
415
+ /** Revoke every pending invitation for (tenant, email). Called before
416
+ * issuing a fresh one so re-inviting supersedes rather than accumulates —
417
+ * otherwise every resend leaves a live token behind. Returns the count. */
418
+ readonly supersedePendingInvitations: (tenantId: string, email: string, revokedByUserId: string, now: Date) => Effect.Effect<number>;
419
+ }
420
+
358
421
  /**
359
422
  * Brute-force lockout policy, resolved from `AuthConfig.lockout` and passed
360
423
  * INTO the store on each failure so the store itself stays policy-free.
@@ -375,6 +438,20 @@ declare interface LockoutState {
375
438
  readonly retryAfterSeconds: number;
376
439
  }
377
440
 
441
+ /** Brute-force lockout, keyed by normalized email. */
442
+ declare interface LockoutStore {
443
+ /** Is this email currently locked out? Reads only — never mutates. Returns
444
+ * the remaining cooldown so the handler can surface a retry-after. */
445
+ readonly isLockedOut: (email: string, now: number) => Effect.Effect<LockoutState>;
446
+ /** Record one failed credential attempt for `email`. Opens a fresh window
447
+ * when the previous one (or a prior lock) has expired, otherwise increments;
448
+ * sets the lock once `failedCount` reaches `policy.maxAttempts`. Called only
449
+ * for a NOT-currently-locked email (the handler checks `isLockedOut` first). */
450
+ readonly recordLoginFailure: (email: string, now: number, policy: LockoutPolicy) => Effect.Effect<void>;
451
+ /** Clear the counter for `email` on a completed login. */
452
+ readonly clearLoginFailures: (email: string) => Effect.Effect<void>;
453
+ }
454
+
378
455
  /** One tenant a user belongs to. The active tenant lives on the Subject;
379
456
  * the full set drives the switch-tenant menu + the switch-tenant guard. */
380
457
  declare interface MembershipRecord {
@@ -384,6 +461,21 @@ declare interface MembershipRecord {
384
461
  readonly joinedAt: Date;
385
462
  }
386
463
 
464
+ /** Memberships + switch-tenant. */
465
+ declare interface MembershipStore {
466
+ /** Every tenant the user belongs to. */
467
+ readonly listMemberships: (userId: string) => Effect.Effect<ReadonlyArray<MembershipRecord>>;
468
+ /** Idempotent grant: inserts the [userId, tenantId] membership or
469
+ * updates its role if it already exists. The DB-level composite
470
+ * UNIQUE on `[userId, tenantId]` (see `membershipsTable`) is the
471
+ * source of truth; this upsert converges to it. */
472
+ readonly addMembership: (membership: Omit<MembershipRecord, 'joinedAt'>) => Effect.Effect<MembershipRecord>;
473
+ /** Resolve the role for a [userId, tenantId] pair, or null when the
474
+ * user is NOT a member. The switch-tenant handler calls this to
475
+ * validate the requested tenant before rebinding the connection. */
476
+ readonly membershipRole: (userId: string, tenantId: string) => Effect.Effect<string | null>;
477
+ }
478
+
387
479
  /** MFA (TOTP) enrolment config. */
388
480
  export declare interface MfaConfig {
389
481
  /** Issuer label shown in the user's authenticator app (typically your
@@ -391,6 +483,31 @@ export declare interface MfaConfig {
391
483
  readonly issuer: string;
392
484
  }
393
485
 
486
+ /** TOTP enrolment + recovery codes. */
487
+ declare interface MfaStore {
488
+ /** Stash a freshly-generated TOTP secret on the user. Called by
489
+ * the MFA enrolment handler before the user verifies the first
490
+ * code. */
491
+ readonly setMfaSecret: (userId: string, secret: string) => Effect.Effect<UserRecord, UserNotFoundError>;
492
+ /** Mark MFA as fully enrolled (called after the first successful
493
+ * TOTP verify completes the enrolment ceremony). */
494
+ readonly markMfaEnrolled: (userId: string) => Effect.Effect<UserRecord, UserNotFoundError>;
495
+ /** Wipe the secret + enrolledAt. Used by /account/security's
496
+ * "remove MFA" action. */
497
+ readonly clearMfa: (userId: string) => Effect.Effect<UserRecord, UserNotFoundError>;
498
+ /** Replace the user's recovery codes with a fresh (already hashed) set.
499
+ * Called at enrolment / regeneration — wipes any prior codes so the
500
+ * displayed set is the only valid one. */
501
+ readonly replaceRecoveryCodes: (userId: string, codeHashes: ReadonlyArray<string>) => Effect.Effect<void>;
502
+ /** Atomically redeem one recovery code by its hash: returns true only
503
+ * when an unconsumed row for `userId` matched — and marks it consumed
504
+ * in the same step (single-use). false otherwise. */
505
+ readonly consumeRecoveryCode: (userId: string, codeHash: string) => Effect.Effect<boolean>;
506
+ /** Count the user's remaining (unconsumed) recovery codes — surfaced so
507
+ * the UI can warn when the user is running low. */
508
+ readonly countRecoveryCodes: (userId: string) => Effect.Effect<number>;
509
+ }
510
+
394
511
  declare interface PasskeyConfig {
395
512
  /** Relying-party id — the registrable domain (e.g. `example.com`). */
396
513
  readonly rpId: string;
@@ -412,6 +529,21 @@ declare interface PasskeyRecord {
412
529
  readonly lastUsedAt: Date | null;
413
530
  }
414
531
 
532
+ /** Passkeys (WebAuthn). */
533
+ declare interface PasskeyStore {
534
+ readonly listPasskeys: (userId: string) => Effect.Effect<ReadonlyArray<PasskeyRecord>>;
535
+ readonly findPasskey: (credentialId: string) => Effect.Effect<PasskeyRecord | null>;
536
+ readonly insertPasskey: (passkey: Omit<PasskeyRecord, 'createdAt' | 'lastUsedAt'>) => Effect.Effect<PasskeyRecord>;
537
+ /** Atomically bump the stored signature counter, guarding against a
538
+ * cloned authenticator (WebAuthn §6.1.1): the update only applies when
539
+ * `newCounter` strictly exceeds the stored counter. Returns whether a
540
+ * row was advanced. `false` with an unchanged stored counter is the
541
+ * clone/replay signal the assertion handler rejects on — the check and
542
+ * the write are ONE atomic statement at the store, so two replicas
543
+ * racing the same counter can't both succeed. */
544
+ readonly advancePasskeyCounter: (credentialId: string, newCounter: number) => Effect.Effect<boolean>;
545
+ }
546
+
415
547
  /**
416
548
  * Injected rebinder — the app passes `bindConnectionCredential` from
417
549
  * `@voltro/runtime` so the plugin stays runtime-agnostic.
@@ -469,6 +601,29 @@ declare interface SessionRevocationOptions {
469
601
  readonly now?: () => number;
470
602
  }
471
603
 
604
+ /** Server-side sessions — enumeration + revocation. */
605
+ declare interface SessionStore {
606
+ /** Active sessions for a user (most recent first). */
607
+ readonly listSessions: (userId: string) => Effect.Effect<ReadonlyArray<SessionRecord>>;
608
+ /** Record a session on sign-in. */
609
+ readonly insertSession: (session: Omit<SessionRecord, 'createdAt' | 'lastSeenAt'>) => Effect.Effect<SessionRecord>;
610
+ /** Look up one session row by id. The request-time revocation check
611
+ * (`makeSessionRevocationChecker`) calls this — a missing row means
612
+ * the session was revoked. */
613
+ readonly findSession: (sessionId: string) => Effect.Effect<SessionRecord | null>;
614
+ /** Slide a session row forward: bump `expiresAt` + `lastSeenAt`. Called
615
+ * when the sliding-window renewal re-issues the cookie, so the row
616
+ * outlives the renewed cookie and the revocation check keeps passing. */
617
+ readonly touchSession: (sessionId: string, expiresAt: Date) => Effect.Effect<void>;
618
+ /** Revoke a single session — only if it belongs to `userId` (so a
619
+ * caller can't revoke another user's session by id-guessing). Returns
620
+ * whether a row was removed. */
621
+ readonly revokeSession: (userId: string, sessionId: string) => Effect.Effect<boolean>;
622
+ /** Revoke every session for the user EXCEPT `keepSessionId` — the
623
+ * "sign out other devices" action. Returns the count removed. */
624
+ readonly revokeAllOtherSessions: (userId: string, keepSessionId: string) => Effect.Effect<number>;
625
+ }
626
+
472
627
  /**
473
628
  * A post-authentication subject guard: given the authenticated user,
474
629
  * decide whether the login may proceed. Runs after the credential check,
@@ -500,6 +655,28 @@ declare interface TokenRecord {
500
655
  readonly createdAt: Date;
501
656
  }
502
657
 
658
+ /** Single-use tokens: magic link, password reset, MFA pending, email verification. */
659
+ declare interface TokenStore {
660
+ /** Persist a single-use token (already hashed). */
661
+ readonly insertToken: (token: Omit<TokenRecord, 'consumedAt' | 'createdAt'>) => Effect.Effect<TokenRecord>;
662
+ /** Atomically redeem a token by its hash + purpose: returns the row
663
+ * only when it exists, matches the purpose, is unexpired, and was not
664
+ * already consumed — and marks it consumed in the same step. Returns
665
+ * null otherwise (single-use guard). */
666
+ readonly consumeToken: (tokenHash: string, purpose: TokenPurpose) => Effect.Effect<TokenRecord | null>;
667
+ /**
668
+ * The most recently CREATED token of this purpose for the user, consumed or
669
+ * not — or null when there is none.
670
+ *
671
+ * Exists for the resend cooldown, which is the only bound on an endpoint
672
+ * that sends mail to an address supplied by an unauthenticated caller. Note
673
+ * it deliberately does NOT filter on `consumedAt`: the cost being rate-
674
+ * limited is the EMAIL, and a token that was minted and then redeemed cost
675
+ * exactly as much to send as one still pending.
676
+ */
677
+ readonly latestToken: (userId: string, purpose: TokenPurpose) => Effect.Effect<TokenRecord | null>;
678
+ }
679
+
503
680
  declare class UserAlreadyExistsError extends UserAlreadyExistsError_base<{
504
681
  readonly email: string;
505
682
  }> {
@@ -569,148 +746,8 @@ declare interface UserRecord {
569
746
  readonly deactivatedAt?: Date | null;
570
747
  }
571
748
 
572
- declare interface UserStore {
573
- readonly findByEmail: (email: string) => Effect.Effect<UserRecord | null>;
574
- readonly findById: (id: string) => Effect.Effect<UserRecord | null>;
575
- readonly insert: (user: Omit<UserRecord, 'createdAt'>) => Effect.Effect<UserRecord, UserAlreadyExistsError>;
576
- /** Replace a user's stored password hash. Used by password-reset AND
577
- * by rehash-on-verify (silently upgrading an under-cost hash on a
578
- * successful sign-in). */
579
- readonly updatePassword: (userId: string, newHash: string) => Effect.Effect<UserRecord, UserNotFoundError>;
580
- /** Stash a freshly-generated TOTP secret on the user. Called by
581
- * the MFA enrolment handler before the user verifies the first
582
- * code. */
583
- readonly setMfaSecret: (userId: string, secret: string) => Effect.Effect<UserRecord, UserNotFoundError>;
584
- /** Mark MFA as fully enrolled (called after the first successful
585
- * TOTP verify completes the enrolment ceremony). */
586
- readonly markMfaEnrolled: (userId: string) => Effect.Effect<UserRecord, UserNotFoundError>;
587
- /** Wipe the secret + enrolledAt. Used by /account/security's
588
- * "remove MFA" action. */
589
- readonly clearMfa: (userId: string) => Effect.Effect<UserRecord, UserNotFoundError>;
590
- /**
591
- * Stamp `emailVerifiedAt` — the address has been proven.
592
- *
593
- * IDEMPOTENT and non-regressing: a user already verified keeps their
594
- * ORIGINAL timestamp. "When was this address first proven" is the question
595
- * the column answers, and re-stamping it on every later magic-link login
596
- * would quietly turn it into "when did they last click something".
597
- */
598
- readonly markEmailVerified: (userId: string) => Effect.Effect<UserRecord, UserNotFoundError>;
599
- /** Every tenant the user belongs to. */
600
- readonly listMemberships: (userId: string) => Effect.Effect<ReadonlyArray<MembershipRecord>>;
601
- /** Idempotent grant: inserts the [userId, tenantId] membership or
602
- * updates its role if it already exists. The DB-level composite
603
- * UNIQUE on `[userId, tenantId]` (see `membershipsTable`) is the
604
- * source of truth; this upsert converges to it. */
605
- readonly addMembership: (membership: Omit<MembershipRecord, 'joinedAt'>) => Effect.Effect<MembershipRecord>;
606
- /** Resolve the role for a [userId, tenantId] pair, or null when the
607
- * user is NOT a member. The switch-tenant handler calls this to
608
- * validate the requested tenant before rebinding the connection. */
609
- readonly membershipRole: (userId: string, tenantId: string) => Effect.Effect<string | null>;
610
- /** Active sessions for a user (most recent first). */
611
- readonly listSessions: (userId: string) => Effect.Effect<ReadonlyArray<SessionRecord>>;
612
- /** Record a session on sign-in. */
613
- readonly insertSession: (session: Omit<SessionRecord, 'createdAt' | 'lastSeenAt'>) => Effect.Effect<SessionRecord>;
614
- /** Look up one session row by id. The request-time revocation check
615
- * (`makeSessionRevocationChecker`) calls this — a missing row means
616
- * the session was revoked. */
617
- readonly findSession: (sessionId: string) => Effect.Effect<SessionRecord | null>;
618
- /** Slide a session row forward: bump `expiresAt` + `lastSeenAt`. Called
619
- * when the sliding-window renewal re-issues the cookie, so the row
620
- * outlives the renewed cookie and the revocation check keeps passing. */
621
- readonly touchSession: (sessionId: string, expiresAt: Date) => Effect.Effect<void>;
622
- /** Revoke a single session — only if it belongs to `userId` (so a
623
- * caller can't revoke another user's session by id-guessing). Returns
624
- * whether a row was removed. */
625
- readonly revokeSession: (userId: string, sessionId: string) => Effect.Effect<boolean>;
626
- /** Revoke every session for the user EXCEPT `keepSessionId` — the
627
- * "sign out other devices" action. Returns the count removed. */
628
- readonly revokeAllOtherSessions: (userId: string, keepSessionId: string) => Effect.Effect<number>;
629
- /** Persist a single-use token (already hashed). */
630
- readonly insertToken: (token: Omit<TokenRecord, 'consumedAt' | 'createdAt'>) => Effect.Effect<TokenRecord>;
631
- /** Atomically redeem a token by its hash + purpose: returns the row
632
- * only when it exists, matches the purpose, is unexpired, and was not
633
- * already consumed — and marks it consumed in the same step. Returns
634
- * null otherwise (single-use guard). */
635
- readonly consumeToken: (tokenHash: string, purpose: TokenPurpose) => Effect.Effect<TokenRecord | null>;
636
- /**
637
- * The most recently CREATED token of this purpose for the user, consumed or
638
- * not — or null when there is none.
639
- *
640
- * Exists for the resend cooldown, which is the only bound on an endpoint
641
- * that sends mail to an address supplied by an unauthenticated caller. Note
642
- * it deliberately does NOT filter on `consumedAt`: the cost being rate-
643
- * limited is the EMAIL, and a token that was minted and then redeemed cost
644
- * exactly as much to send as one still pending.
645
- */
646
- readonly latestToken: (userId: string, purpose: TokenPurpose) => Effect.Effect<TokenRecord | null>;
647
- /** Persist an invitation (token already hashed). */
648
- readonly insertInvitation: (invitation: Omit<InvitationRecord, 'acceptedAt' | 'acceptedByUserId' | 'revokedAt' | 'revokedByUserId' | 'createdAt'>) => Effect.Effect<InvitationRecord>;
649
- /** Look an invitation up by token hash WITHOUT redeeming it — the read the
650
- * "what am I being invited to" preview needs. Returns the row whatever its
651
- * state; the caller decides what an expired or revoked one means. */
652
- readonly findInvitationByHash: (tokenHash: string) => Effect.Effect<InvitationRecord | null>;
653
- /**
654
- * Atomically redeem an invitation: returns the row only when it exists, is
655
- * unaccepted, unrevoked and unexpired — and stamps `acceptedAt` +
656
- * `acceptedByUserId` in the same step. Null otherwise.
657
- *
658
- * The single-use guard, and it has to be ONE statement: two clicks on the
659
- * same emailed link race, and a read-then-write would let both through and
660
- * write the membership twice.
661
- */
662
- readonly acceptInvitation: (tokenHash: string, acceptedByUserId: string, now: Date) => Effect.Effect<InvitationRecord | null>;
663
- /** Withdraw a PENDING invitation, scoped to its tenant so an admin of one
664
- * tenant cannot revoke another's by id. Returns whether a row changed. */
665
- readonly revokeInvitation: (invitationId: string, tenantId: string, revokedByUserId: string, now: Date) => Effect.Effect<boolean>;
666
- /** Every invitation for a tenant, newest first. The admin list. */
667
- readonly listInvitations: (tenantId: string) => Effect.Effect<ReadonlyArray<InvitationRecord>>;
668
- /** How many invitations for this tenant are still redeemable — the bound
669
- * `maxPending` enforces. */
670
- readonly countPendingInvitations: (tenantId: string, now: Date) => Effect.Effect<number>;
671
- /** Revoke every pending invitation for (tenant, email). Called before
672
- * issuing a fresh one so re-inviting supersedes rather than accumulates —
673
- * otherwise every resend leaves a live token behind. Returns the count. */
674
- readonly supersedePendingInvitations: (tenantId: string, email: string, revokedByUserId: string, now: Date) => Effect.Effect<number>;
675
- /** Record an impersonation grant when the impersonated session is minted. */
676
- readonly insertImpersonationGrant: (grant: Omit<ImpersonationGrantRecord, 'startedAt' | 'endedAt' | 'endedReason'>) => Effect.Effect<ImpersonationGrantRecord>;
677
- /** The grant for an impersonated session, or null. The stop path's lookup. */
678
- readonly findImpersonationGrantBySession: (sessionId: string) => Effect.Effect<ImpersonationGrantRecord | null>;
679
- /** Close a grant. Only an OPEN grant is closed (`endedAt IS NULL`), so a
680
- * double stop is a no-op rather than a rewritten end time. */
681
- readonly endImpersonationGrant: (sessionId: string, endedReason: string, now: Date) => Effect.Effect<boolean>;
682
- /** Replace the user's recovery codes with a fresh (already hashed) set.
683
- * Called at enrolment / regeneration — wipes any prior codes so the
684
- * displayed set is the only valid one. */
685
- readonly replaceRecoveryCodes: (userId: string, codeHashes: ReadonlyArray<string>) => Effect.Effect<void>;
686
- /** Atomically redeem one recovery code by its hash: returns true only
687
- * when an unconsumed row for `userId` matched — and marks it consumed
688
- * in the same step (single-use). false otherwise. */
689
- readonly consumeRecoveryCode: (userId: string, codeHash: string) => Effect.Effect<boolean>;
690
- /** Count the user's remaining (unconsumed) recovery codes — surfaced so
691
- * the UI can warn when the user is running low. */
692
- readonly countRecoveryCodes: (userId: string) => Effect.Effect<number>;
693
- readonly listPasskeys: (userId: string) => Effect.Effect<ReadonlyArray<PasskeyRecord>>;
694
- readonly findPasskey: (credentialId: string) => Effect.Effect<PasskeyRecord | null>;
695
- readonly insertPasskey: (passkey: Omit<PasskeyRecord, 'createdAt' | 'lastUsedAt'>) => Effect.Effect<PasskeyRecord>;
696
- /** Atomically bump the stored signature counter, guarding against a
697
- * cloned authenticator (WebAuthn §6.1.1): the update only applies when
698
- * `newCounter` strictly exceeds the stored counter. Returns whether a
699
- * row was advanced. `false` with an unchanged stored counter is the
700
- * clone/replay signal the assertion handler rejects on — the check and
701
- * the write are ONE atomic statement at the store, so two replicas
702
- * racing the same counter can't both succeed. */
703
- readonly advancePasskeyCounter: (credentialId: string, newCounter: number) => Effect.Effect<boolean>;
704
- /** Is this email currently locked out? Reads only — never mutates. Returns
705
- * the remaining cooldown so the handler can surface a retry-after. */
706
- readonly isLockedOut: (email: string, now: number) => Effect.Effect<LockoutState>;
707
- /** Record one failed credential attempt for `email`. Opens a fresh window
708
- * when the previous one (or a prior lock) has expired, otherwise increments;
709
- * sets the lock once `failedCount` reaches `policy.maxAttempts`. Called only
710
- * for a NOT-currently-locked email (the handler checks `isLockedOut` first). */
711
- readonly recordLoginFailure: (email: string, now: number, policy: LockoutPolicy) => Effect.Effect<void>;
712
- /** Clear the counter for `email` on a completed login. */
713
- readonly clearLoginFailures: (email: string) => Effect.Effect<void>;
749
+ /** Every role at once — what the shipped stores return and what `authRoutesPlugin` takes. */
750
+ declare interface UserStore extends IdentityStore, MfaStore, MembershipStore, SessionStore, TokenStore, InvitationStore, ImpersonationStore, PasskeyStore, LockoutStore {
714
751
  }
715
752
 
716
753
  export { }
package/dist/plugin.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import "./password.js";
2
2
  import "./session.js";
3
- import { n as e, t } from "./plugin-CPcpSSf9.js";
3
+ import { n as e, t } from "./plugin-DFQ8N52Q.js";
4
4
  import "./csrf.js";
5
5
  export { t as authRoutesPlugin, e as getAuthSessionStrategy };