@voltro/plugin-auth 0.71.1 → 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/index.d.ts CHANGED
@@ -1,7 +1,9 @@
1
1
  import { AuthStrategy } from '@voltro/protocol';
2
2
  import { ConnectionCredential } from '@voltro/protocol';
3
+ import { DataStore } from '@voltro/database';
3
4
  import { Effect } from 'effect';
4
5
  import { KeyedSecret } from '@voltro/protocol/session';
6
+ import { MembershipsSource } from '@voltro/protocol';
5
7
  import { SessionSecrets } from '@voltro/protocol/session';
6
8
  import { SqlClient } from '@effect/sql/SqlClient';
7
9
  import { Subject } from '@voltro/protocol';
@@ -120,6 +122,8 @@ export declare interface AuthConfig {
120
122
  readonly emailVerification?: EmailVerificationConfig;
121
123
  }
122
124
 
125
+ export declare const authMemberships: (store: UserStore) => MembershipsSource;
126
+
123
127
  /**
124
128
  * The auth plugin instance also carries a pre-wired session
125
129
  * `AuthStrategy` (keyed verify + the SAME revocation cache the HTTP
@@ -423,7 +427,7 @@ export declare const guardSubject: (user: UserRecord, config: AuthConfig) => Eff
423
427
  * a winning redemption goes on to write the membership. Reversed, two
424
428
  * concurrent accepts would both write.
425
429
  */
426
- export declare const handleAcceptInvitation: (input: AcceptInvitationInput, store: UserStore, deps?: InvitationDeps) => Effect.Effect<HandlerResultShape_2>;
430
+ export declare const handleAcceptInvitation: (input: AcceptInvitationInput, store: IdentityStore & MembershipStore & InvitationStore, deps?: InvitationDeps) => Effect.Effect<HandlerResultShape_2>;
427
431
 
428
432
  /**
429
433
  * Accept an invitation by CREATING the account it was addressed to — the
@@ -441,7 +445,7 @@ export declare const handleAcceptInvitation: (input: AcceptInvitationInput, stor
441
445
  * caller to sign in and use {@link handleAcceptInvitation} — creating a second
442
446
  * account for one address is not a fallback, it is a duplicate identity.
443
447
  */
444
- export declare const handleAcceptInvitationWithSignUp: (input: AcceptInvitationWithSignUpInput, store: UserStore, deps: InvitationDeps & {
448
+ export declare const handleAcceptInvitationWithSignUp: (input: AcceptInvitationWithSignUpInput, store: IdentityStore & MembershipStore & InvitationStore, deps: InvitationDeps & {
445
449
  /** Hash a plaintext password. Injected so this module does not import the
446
450
  * password module's Effect error channel into the invitation surface. */
447
451
  readonly hashPassword: (plaintext: string) => Effect.Effect<string, unknown>;
@@ -458,7 +462,7 @@ export declare const handleAcceptInvitationWithSignUp: (input: AcceptInvitationW
458
462
  * email) is revoked first, so the address never holds two live tokens and
459
463
  * "resend" cannot be used to accumulate them.
460
464
  */
461
- export declare const handleCreateInvitation: (input: CreateInvitationInput, store: UserStore, deps: InvitationDeps) => Effect.Effect<HandlerResultShape_2>;
465
+ export declare const handleCreateInvitation: (input: CreateInvitationInput, store: MembershipStore & InvitationStore, deps: InvitationDeps) => Effect.Effect<HandlerResultShape_2>;
462
466
 
463
467
  /**
464
468
  * Issue a CSRF token. Returns the token in the JSON body AND sets a
@@ -474,7 +478,7 @@ export declare const handleCsrf: (config: AuthConfig) => HandlerResult;
474
478
  * Issues NO session — see the header. The response says whether the address is
475
479
  * now verified, and the client sends the user to sign in.
476
480
  */
477
- export declare const handleEmailVerifyConfirm: (input: EmailVerifyConfirmInput, store: UserStore) => Effect.Effect<HandlerResultShape>;
481
+ export declare const handleEmailVerifyConfirm: (input: EmailVerifyConfirmInput, store: IdentityStore & TokenStore) => Effect.Effect<HandlerResultShape>;
478
482
 
479
483
  /**
480
484
  * Request (or re-request) a verification link.
@@ -485,9 +489,9 @@ export declare const handleEmailVerifyConfirm: (input: EmailVerifyConfirmInput,
485
489
  * are registered, and (through the cooldown) whether one was recently active.
486
490
  * An already-verified user is also answered 202 and sent nothing.
487
491
  */
488
- export declare const handleEmailVerifyRequest: (input: EmailVerifyRequestInput, store: UserStore, deps: EmailVerificationDeps) => Effect.Effect<HandlerResultShape>;
492
+ export declare const handleEmailVerifyRequest: (input: EmailVerifyRequestInput, store: IdentityStore & TokenStore, deps: EmailVerificationDeps) => Effect.Effect<HandlerResultShape>;
489
493
 
490
- export declare const handleImpersonationStart: (input: ImpersonationStartInput, store: UserStore, deps: ImpersonationDeps) => Effect.Effect<HandlerResultShape_3>;
494
+ export declare const handleImpersonationStart: (input: ImpersonationStartInput, store: IdentityStore & ImpersonationStore, deps: ImpersonationDeps) => Effect.Effect<HandlerResultShape_3>;
491
495
 
492
496
  /**
493
497
  * End an impersonation and put the impersonator back in their own identity.
@@ -507,42 +511,42 @@ export declare const handleImpersonationStart: (input: ImpersonationStartInput,
507
511
  * meanwhile, the response clears the cookie — being signed out is the correct
508
512
  * outcome, and it is strictly better than staying signed in as somebody else.
509
513
  */
510
- export declare const handleImpersonationStop: (input: ImpersonationStopInput, store: UserStore, deps: ImpersonationDeps) => Effect.Effect<HandlerResultShape_3>;
514
+ export declare const handleImpersonationStop: (input: ImpersonationStopInput, store: IdentityStore & SessionStore & ImpersonationStore, deps: ImpersonationDeps) => Effect.Effect<HandlerResultShape_3>;
511
515
 
512
516
  export declare const handleListInvitations: (input: {
513
517
  readonly callerUserId: string;
514
518
  readonly tenantId: string;
515
- }, store: UserStore, deps: InvitationDeps) => Effect.Effect<HandlerResultShape_2>;
519
+ }, store: MembershipStore & InvitationStore, deps: InvitationDeps) => Effect.Effect<HandlerResultShape_2>;
516
520
 
517
521
  export declare const handleListMemberships: (input: {
518
522
  readonly userId: string;
519
- }, store: UserStore) => Effect.Effect<HandlerResult>;
523
+ }, store: MembershipStore) => Effect.Effect<HandlerResult>;
520
524
 
521
- export declare const handleListSessions: (input: ListSessionsInput, store: UserStore) => Effect.Effect<HandlerResult>;
525
+ export declare const handleListSessions: (input: ListSessionsInput, store: SessionStore) => Effect.Effect<HandlerResult>;
522
526
 
523
527
  /**
524
528
  * Consume a magic-link token: validate (single-use, unexpired) and, on
525
529
  * success, issue a session for the bound user. Writes a session row +
526
530
  * loads memberships, same as password sign-in.
527
531
  */
528
- export declare const handleMagicLinkConsume: (input: MagicLinkConsumeInput, store: UserStore, config: AuthConfig) => Effect.Effect<HandlerResult>;
532
+ export declare const handleMagicLinkConsume: (input: MagicLinkConsumeInput, store: IdentityStore & MembershipStore & SessionStore & TokenStore & LockoutStore, config: AuthConfig) => Effect.Effect<HandlerResult>;
529
533
 
530
534
  /**
531
535
  * Request a magic-link. Always returns 202 regardless of whether the
532
536
  * email exists (no account-enumeration). When the user exists we mint a
533
537
  * single-use token, persist its hash, and call `config.sendEmail`.
534
538
  */
535
- export declare const handleMagicLinkRequest: (input: MagicLinkRequestInput, store: UserStore, config: AuthConfig) => Effect.Effect<HandlerResult>;
539
+ export declare const handleMagicLinkRequest: (input: MagicLinkRequestInput, store: IdentityStore & TokenStore, config: AuthConfig) => Effect.Effect<HandlerResult>;
536
540
 
537
- export declare const handleMfaEnrollStart: (input: MfaEnrollStartInput, store: UserStore) => Effect.Effect<HandlerResult>;
541
+ export declare const handleMfaEnrollStart: (input: MfaEnrollStartInput, store: IdentityStore & MfaStore) => Effect.Effect<HandlerResult>;
538
542
 
539
- export declare const handleMfaEnrollVerify: (input: MfaEnrollVerifyInput, store: UserStore) => Effect.Effect<HandlerResult>;
543
+ export declare const handleMfaEnrollVerify: (input: MfaEnrollVerifyInput, store: IdentityStore & MfaStore) => Effect.Effect<HandlerResult>;
540
544
 
541
545
  /** Regenerate the user's recovery codes — wipes the prior set and returns
542
546
  * a fresh plaintext batch (shown once). Only valid for an enrolled user. */
543
- export declare const handleMfaRegenerateRecoveryCodes: (input: MfaRegenerateRecoveryCodesInput, store: UserStore) => Effect.Effect<HandlerResult>;
547
+ export declare const handleMfaRegenerateRecoveryCodes: (input: MfaRegenerateRecoveryCodesInput, store: IdentityStore & MfaStore) => Effect.Effect<HandlerResult>;
544
548
 
545
- export declare const handleMfaUnenroll: (input: MfaUnenrollInput, store: UserStore) => Effect.Effect<HandlerResult>;
549
+ export declare const handleMfaUnenroll: (input: MfaUnenrollInput, store: IdentityStore & MfaStore) => Effect.Effect<HandlerResult>;
546
550
 
547
551
  /**
548
552
  * Complete an MFA sign-in: redeem the single-use pending token, verify the
@@ -553,29 +557,29 @@ export declare const handleMfaUnenroll: (input: MfaUnenrollInput, store: UserSto
553
557
  * consumed on redemption regardless, so a guessed code can't be retried
554
558
  * against the same challenge.
555
559
  */
556
- export declare const handleMfaVerify: (input: MfaVerifyInput, store: UserStore, config: AuthConfig) => Effect.Effect<HandlerResult>;
560
+ export declare const handleMfaVerify: (input: MfaVerifyInput, store: IdentityStore & MfaStore & MembershipStore & SessionStore & TokenStore & LockoutStore, config: AuthConfig) => Effect.Effect<HandlerResult>;
557
561
 
558
562
  /** Step 3: mint assertion options for `navigator.credentials.get`. */
559
- export declare const handlePasskeyAssertOptions: (input: PasskeyAssertOptionsInput, store: UserStore, challenges: ChallengeStore, pk: PasskeyConfig) => Effect.Effect<HandlerResult>;
563
+ export declare const handlePasskeyAssertOptions: (input: PasskeyAssertOptionsInput, store: PasskeyStore, challenges: ChallengeStore, pk: PasskeyConfig) => Effect.Effect<HandlerResult>;
560
564
 
561
565
  /** Step 4: verify the assertion (signature + strictly-increasing counter)
562
566
  * and issue a session. */
563
- export declare const handlePasskeyAssertVerify: (input: PasskeyAssertVerifyInput, store: UserStore, challenges: ChallengeStore, pk: PasskeyConfig, config: AuthConfig) => Effect.Effect<HandlerResult>;
567
+ export declare const handlePasskeyAssertVerify: (input: PasskeyAssertVerifyInput, store: IdentityStore & MembershipStore & SessionStore & PasskeyStore & LockoutStore, challenges: ChallengeStore, pk: PasskeyConfig, config: AuthConfig) => Effect.Effect<HandlerResult>;
564
568
 
565
569
  /** Step 1: mint registration options for `navigator.credentials.create`. */
566
- export declare const handlePasskeyRegisterOptions: (input: PasskeyRegisterOptionsInput, store: UserStore, challenges: ChallengeStore, pk: PasskeyConfig) => Effect.Effect<HandlerResult>;
570
+ export declare const handlePasskeyRegisterOptions: (input: PasskeyRegisterOptionsInput, store: IdentityStore & PasskeyStore, challenges: ChallengeStore, pk: PasskeyConfig) => Effect.Effect<HandlerResult>;
567
571
 
568
572
  /** Step 2: verify the registration and persist the credential. */
569
- export declare const handlePasskeyRegisterVerify: (input: PasskeyRegisterVerifyInput, store: UserStore, challenges: ChallengeStore, pk: PasskeyConfig) => Effect.Effect<HandlerResult>;
573
+ export declare const handlePasskeyRegisterVerify: (input: PasskeyRegisterVerifyInput, store: IdentityStore & PasskeyStore, challenges: ChallengeStore, pk: PasskeyConfig) => Effect.Effect<HandlerResult>;
570
574
 
571
575
  /** Confirm a password reset: validate the token, set the new (hashed)
572
576
  * password, and revoke ALL existing sessions for the user (a reset
573
577
  * invalidates every prior login). */
574
- export declare const handlePasswordResetConfirm: (input: PasswordResetConfirmInput, store: UserStore) => Effect.Effect<HandlerResult>;
578
+ export declare const handlePasswordResetConfirm: (input: PasswordResetConfirmInput, store: IdentityStore & SessionStore & TokenStore) => Effect.Effect<HandlerResult>;
575
579
 
576
580
  /** Request a password reset. Uniform 202 (no enumeration); mints + emails
577
581
  * a single-use reset token when the user exists. */
578
- export declare const handlePasswordResetRequest: (input: PasswordResetRequestInput, store: UserStore, config: AuthConfig) => Effect.Effect<HandlerResult>;
582
+ export declare const handlePasswordResetRequest: (input: PasswordResetRequestInput, store: IdentityStore & TokenStore, config: AuthConfig) => Effect.Effect<HandlerResult>;
579
583
 
580
584
  /**
581
585
  * What is behind this link, without redeeming it — the accept page's read.
@@ -587,17 +591,17 @@ export declare const handlePasswordResetRequest: (input: PasswordResetRequestInp
587
591
  */
588
592
  export declare const handlePreviewInvitation: (input: {
589
593
  readonly token: string;
590
- }, store: UserStore, deps?: InvitationDeps) => Effect.Effect<HandlerResultShape_2>;
594
+ }, store: InvitationStore, deps?: InvitationDeps) => Effect.Effect<HandlerResultShape_2>;
591
595
 
592
- export declare const handleRevokeAllOtherSessions: (input: RevokeOtherSessionsInput, store: UserStore) => Effect.Effect<HandlerResult>;
596
+ export declare const handleRevokeAllOtherSessions: (input: RevokeOtherSessionsInput, store: SessionStore) => Effect.Effect<HandlerResult>;
593
597
 
594
598
  export declare const handleRevokeInvitation: (input: {
595
599
  readonly callerUserId: string;
596
600
  readonly tenantId: string;
597
601
  readonly invitationId: string;
598
- }, store: UserStore, deps: InvitationDeps) => Effect.Effect<HandlerResultShape_2>;
602
+ }, store: MembershipStore & InvitationStore, deps: InvitationDeps) => Effect.Effect<HandlerResultShape_2>;
599
603
 
600
- export declare const handleRevokeSession: (input: RevokeSessionInput, store: UserStore) => Effect.Effect<HandlerResult>;
604
+ export declare const handleRevokeSession: (input: RevokeSessionInput, store: SessionStore) => Effect.Effect<HandlerResult>;
601
605
 
602
606
  export declare interface HandlerResult {
603
607
  readonly status: number;
@@ -648,7 +652,7 @@ declare interface HandlerResultShape_3 {
648
652
  * for the device list + revocation, and (c) rehashes the stored password
649
653
  * when it's below the current scrypt cost (rehash-on-verify).
650
654
  */
651
- export declare const handleSignIn: (input: SignInInput, store: UserStore, config: AuthConfig) => Effect.Effect<HandlerResult>;
655
+ export declare const handleSignIn: (input: SignInInput, store: IdentityStore & MembershipStore & SessionStore & TokenStore & LockoutStore, config: AuthConfig) => Effect.Effect<HandlerResult>;
652
656
 
653
657
  /**
654
658
  * Sign-out handler. Clears the session cookie. Synchronous so it
@@ -666,7 +670,7 @@ export declare const handleSignOut: (config: AuthConfig) => HandlerResult;
666
670
  * via the Effect's error channel — callers either map them to 5xx or
667
671
  * pattern-match for finer-grained responses.
668
672
  */
669
- export declare const handleSignUp: (input: SignUpInput, store: UserStore, config: AuthConfig) => Effect.Effect<HandlerResult>;
673
+ export declare const handleSignUp: (input: SignUpInput, store: IdentityStore & MembershipStore & SessionStore & TokenStore & LockoutStore, config: AuthConfig) => Effect.Effect<HandlerResult>;
670
674
 
671
675
  /**
672
676
  * Switch the active tenant. Validates the user is actually a member of
@@ -680,7 +684,13 @@ export declare const handleSignUp: (input: SignUpInput, store: UserStore, config
680
684
  * nothing re-scopes them, and nothing ever did — the `onBindConnectionSubject`
681
685
  * listener that claimed to had no subscribers in the entire framework.
682
686
  */
683
- export declare const handleSwitchTenant: (input: SwitchTenantInput, store: UserStore, config: AuthConfig, rebind?: RebindConnection) => Effect.Effect<HandlerResult>;
687
+ export declare const handleSwitchTenant: (input: SwitchTenantInput, store: IdentityStore & MembershipStore, config: AuthConfig, rebind?: RebindConnection,
688
+ /** The app's DataStore, when the plugin has been bound to one: the switch
689
+ * is then ALSO recorded as the framework's tenant selection
690
+ * (`_voltro_tenant_selections`), so the cookie and the selection never
691
+ * disagree — a request this user makes with any OTHER credential (an IdP
692
+ * JWT, an API key) resolves in the tenant they switched to as well. */
693
+ dataStore?: DataStore) => Effect.Effect<HandlerResult>;
684
694
 
685
695
  /**
686
696
  * Hash a password. Returns a self-describing string that
@@ -694,6 +704,56 @@ export declare const hashPassword: (plaintext: string) => Effect.Effect<string,
694
704
  * brute-force surface that would need a slow KDF. */
695
705
  export declare const hashToken: (plaintext: string) => string;
696
706
 
707
+ /** The identity columns the store reads and writes, by their record name. */
708
+ export declare type IdentityColumn = 'id' | 'email' | 'passwordHash' | 'tenantId' | 'createdAt' | 'updatedAt' | 'mfaSecret' | 'mfaEnrolledAt' | 'emailVerifiedAt' | 'deactivatedAt';
709
+
710
+ /** 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. */
711
+ export declare interface IdentityStore {
712
+ readonly findByEmail: (email: string) => Effect.Effect<UserRecord | null>;
713
+ readonly findById: (id: string) => Effect.Effect<UserRecord | null>;
714
+ readonly insert: (user: Omit<UserRecord, 'createdAt'>) => Effect.Effect<UserRecord, UserAlreadyExistsError>;
715
+ /** Replace a user's stored password hash. Used by password-reset AND
716
+ * by rehash-on-verify (silently upgrading an under-cost hash on a
717
+ * successful sign-in). */
718
+ readonly updatePassword: (userId: string, newHash: string) => Effect.Effect<UserRecord, UserNotFoundError>;
719
+ /**
720
+ * Stamp `emailVerifiedAt` — the address has been proven.
721
+ *
722
+ * IDEMPOTENT and non-regressing: a user already verified keeps their
723
+ * ORIGINAL timestamp. "When was this address first proven" is the question
724
+ * the column answers, and re-stamping it on every later magic-link login
725
+ * would quietly turn it into "when did they last click something".
726
+ */
727
+ readonly markEmailVerified: (userId: string) => Effect.Effect<UserRecord, UserNotFoundError>;
728
+ }
729
+
730
+ export declare interface IdentityTableMapping {
731
+ /** The table (default `users`). Unqualified or `schema.table`. */
732
+ readonly table?: string;
733
+ /**
734
+ * Record name → column name. Unmapped columns keep their record name.
735
+ * `id`, `email`, `tenantId` and `createdAt` must exist. The others may be
736
+ * `false` — "my table has no such column": reads answer null, and a write
737
+ * that exists only to set that column (`updatePassword` → `passwordHash`,
738
+ * the MFA methods → `mfaSecret` / `mfaEnrolledAt`, `markEmailVerified` →
739
+ * `emailVerifiedAt`) dies naming the mapping. `updatedAt: false` merely
740
+ * omits the stamp; `deactivatedAt` is `false` by default because the
741
+ * shipped table has no such column — map it to read the app's own.
742
+ */
743
+ readonly columns?: {
744
+ readonly id?: string;
745
+ readonly email?: string;
746
+ readonly tenantId?: string;
747
+ readonly createdAt?: string;
748
+ readonly passwordHash?: string | false;
749
+ readonly updatedAt?: string | false;
750
+ readonly mfaSecret?: string | false;
751
+ readonly mfaEnrolledAt?: string | false;
752
+ readonly emailVerifiedAt?: string | false;
753
+ readonly deactivatedAt?: string | false;
754
+ };
755
+ }
756
+
697
757
  /** Default grant lifetime — 15 minutes. A support interaction, not a shift. */
698
758
  export declare const IMPERSONATION_DEFAULT_DURATION_S: number;
699
759
 
@@ -905,6 +965,17 @@ export declare interface ImpersonationStopInput {
905
965
  readonly sessionId: string;
906
966
  }
907
967
 
968
+ /** Impersonation grants. */
969
+ export declare interface ImpersonationStore {
970
+ /** Record an impersonation grant when the impersonated session is minted. */
971
+ readonly insertImpersonationGrant: (grant: Omit<ImpersonationGrantRecord, 'startedAt' | 'endedAt' | 'endedReason'>) => Effect.Effect<ImpersonationGrantRecord>;
972
+ /** The grant for an impersonated session, or null. The stop path's lookup. */
973
+ readonly findImpersonationGrantBySession: (sessionId: string) => Effect.Effect<ImpersonationGrantRecord | null>;
974
+ /** Close a grant. Only an OPEN grant is closed (`endedAt IS NULL`), so a
975
+ * double stop is a no-op rather than a rewritten end time. */
976
+ readonly endImpersonationGrant: (sessionId: string, endedReason: string, now: Date) => Effect.Effect<boolean>;
977
+ }
978
+
908
979
  /** Default cap on live invitations per tenant. */
909
980
  export declare const INVITATION_MAX_PENDING = 500;
910
981
 
@@ -986,6 +1057,38 @@ export declare interface InvitationsConfig {
986
1057
  readonly canGrantRole?: (ctx: GrantRoleContext) => boolean;
987
1058
  }
988
1059
 
1060
+ /** Tenant invitations. */
1061
+ export declare interface InvitationStore {
1062
+ /** Persist an invitation (token already hashed). */
1063
+ readonly insertInvitation: (invitation: Omit<InvitationRecord, 'acceptedAt' | 'acceptedByUserId' | 'revokedAt' | 'revokedByUserId' | 'createdAt'>) => Effect.Effect<InvitationRecord>;
1064
+ /** Look an invitation up by token hash WITHOUT redeeming it — the read the
1065
+ * "what am I being invited to" preview needs. Returns the row whatever its
1066
+ * state; the caller decides what an expired or revoked one means. */
1067
+ readonly findInvitationByHash: (tokenHash: string) => Effect.Effect<InvitationRecord | null>;
1068
+ /**
1069
+ * Atomically redeem an invitation: returns the row only when it exists, is
1070
+ * unaccepted, unrevoked and unexpired — and stamps `acceptedAt` +
1071
+ * `acceptedByUserId` in the same step. Null otherwise.
1072
+ *
1073
+ * The single-use guard, and it has to be ONE statement: two clicks on the
1074
+ * same emailed link race, and a read-then-write would let both through and
1075
+ * write the membership twice.
1076
+ */
1077
+ readonly acceptInvitation: (tokenHash: string, acceptedByUserId: string, now: Date) => Effect.Effect<InvitationRecord | null>;
1078
+ /** Withdraw a PENDING invitation, scoped to its tenant so an admin of one
1079
+ * tenant cannot revoke another's by id. Returns whether a row changed. */
1080
+ readonly revokeInvitation: (invitationId: string, tenantId: string, revokedByUserId: string, now: Date) => Effect.Effect<boolean>;
1081
+ /** Every invitation for a tenant, newest first. The admin list. */
1082
+ readonly listInvitations: (tenantId: string) => Effect.Effect<ReadonlyArray<InvitationRecord>>;
1083
+ /** How many invitations for this tenant are still redeemable — the bound
1084
+ * `maxPending` enforces. */
1085
+ readonly countPendingInvitations: (tenantId: string, now: Date) => Effect.Effect<number>;
1086
+ /** Revoke every pending invitation for (tenant, email). Called before
1087
+ * issuing a fresh one so re-inviting supersedes rather than accumulates —
1088
+ * otherwise every resend leaves a live token behind. Returns the count. */
1089
+ readonly supersedePendingInvitations: (tenantId: string, email: string, revokedByUserId: string, now: Date) => Effect.Effect<number>;
1090
+ }
1091
+
989
1092
  /** Verify a token's own HMAC (signature integrity), independent of the
990
1093
  * cookie/header match. */
991
1094
  export declare const isCsrfTokenWellFormed: (token: string, secret: string) => boolean;
@@ -1054,7 +1157,7 @@ export declare interface IssueSessionOptions {
1054
1157
  * this body — which is exactly how a mark added in one place ends up missing
1055
1158
  * on two of five login paths, discoverable only by logging in the right way.
1056
1159
  */
1057
- export declare const issueUserSession: (user: UserRecord, store: UserStore, config: AuthConfig, meta?: IssueUserSessionOptions) => Effect.Effect<{
1160
+ export declare const issueUserSession: (user: UserRecord, store: MembershipStore & SessionStore & LockoutStore, config: AuthConfig, meta?: IssueUserSessionOptions) => Effect.Effect<{
1058
1161
  readonly setCookie: string;
1059
1162
  readonly subject: ReturnType<typeof subjectFromUser>;
1060
1163
  }>;
@@ -1124,6 +1227,20 @@ declare interface LockoutState {
1124
1227
  readonly retryAfterSeconds: number;
1125
1228
  }
1126
1229
 
1230
+ /** Brute-force lockout, keyed by normalized email. */
1231
+ export declare interface LockoutStore {
1232
+ /** Is this email currently locked out? Reads only — never mutates. Returns
1233
+ * the remaining cooldown so the handler can surface a retry-after. */
1234
+ readonly isLockedOut: (email: string, now: number) => Effect.Effect<LockoutState>;
1235
+ /** Record one failed credential attempt for `email`. Opens a fresh window
1236
+ * when the previous one (or a prior lock) has expired, otherwise increments;
1237
+ * sets the lock once `failedCount` reaches `policy.maxAttempts`. Called only
1238
+ * for a NOT-currently-locked email (the handler checks `isLockedOut` first). */
1239
+ readonly recordLoginFailure: (email: string, now: number, policy: LockoutPolicy) => Effect.Effect<void>;
1240
+ /** Clear the counter for `email` on a completed login. */
1241
+ readonly clearLoginFailures: (email: string) => Effect.Effect<void>;
1242
+ }
1243
+
1127
1244
  /** Default magic-link lifetime — short, since it's an inbox round-trip. */
1128
1245
  export declare const MAGIC_LINK_TTL_S: number;
1129
1246
 
@@ -1179,6 +1296,21 @@ export declare interface MembershipRecord {
1179
1296
  readonly joinedAt: Date;
1180
1297
  }
1181
1298
 
1299
+ /** Memberships + switch-tenant. */
1300
+ export declare interface MembershipStore {
1301
+ /** Every tenant the user belongs to. */
1302
+ readonly listMemberships: (userId: string) => Effect.Effect<ReadonlyArray<MembershipRecord>>;
1303
+ /** Idempotent grant: inserts the [userId, tenantId] membership or
1304
+ * updates its role if it already exists. The DB-level composite
1305
+ * UNIQUE on `[userId, tenantId]` (see `membershipsTable`) is the
1306
+ * source of truth; this upsert converges to it. */
1307
+ readonly addMembership: (membership: Omit<MembershipRecord, 'joinedAt'>) => Effect.Effect<MembershipRecord>;
1308
+ /** Resolve the role for a [userId, tenantId] pair, or null when the
1309
+ * user is NOT a member. The switch-tenant handler calls this to
1310
+ * validate the requested tenant before rebinding the connection. */
1311
+ readonly membershipRole: (userId: string, tenantId: string) => Effect.Effect<string | null>;
1312
+ }
1313
+
1182
1314
  /** In-memory challenge store. Single-node only — a challenge minted on one
1183
1315
  * replica is invisible to another, so this is the DEV / single-node
1184
1316
  * default. Multi-replica deployments pass `dataStoreChallengeStore`. */
@@ -1227,6 +1359,31 @@ export declare interface MfaRegenerateRecoveryCodesInput {
1227
1359
  readonly userId: string;
1228
1360
  }
1229
1361
 
1362
+ /** TOTP enrolment + recovery codes. */
1363
+ export declare interface MfaStore {
1364
+ /** Stash a freshly-generated TOTP secret on the user. Called by
1365
+ * the MFA enrolment handler before the user verifies the first
1366
+ * code. */
1367
+ readonly setMfaSecret: (userId: string, secret: string) => Effect.Effect<UserRecord, UserNotFoundError>;
1368
+ /** Mark MFA as fully enrolled (called after the first successful
1369
+ * TOTP verify completes the enrolment ceremony). */
1370
+ readonly markMfaEnrolled: (userId: string) => Effect.Effect<UserRecord, UserNotFoundError>;
1371
+ /** Wipe the secret + enrolledAt. Used by /account/security's
1372
+ * "remove MFA" action. */
1373
+ readonly clearMfa: (userId: string) => Effect.Effect<UserRecord, UserNotFoundError>;
1374
+ /** Replace the user's recovery codes with a fresh (already hashed) set.
1375
+ * Called at enrolment / regeneration — wipes any prior codes so the
1376
+ * displayed set is the only valid one. */
1377
+ readonly replaceRecoveryCodes: (userId: string, codeHashes: ReadonlyArray<string>) => Effect.Effect<void>;
1378
+ /** Atomically redeem one recovery code by its hash: returns true only
1379
+ * when an unconsumed row for `userId` matched — and marks it consumed
1380
+ * in the same step (single-use). false otherwise. */
1381
+ readonly consumeRecoveryCode: (userId: string, codeHash: string) => Effect.Effect<boolean>;
1382
+ /** Count the user's remaining (unconsumed) recovery codes — surfaced so
1383
+ * the UI can warn when the user is running low. */
1384
+ readonly countRecoveryCodes: (userId: string) => Effect.Effect<number>;
1385
+ }
1386
+
1230
1387
  export declare interface MfaUnenrollInput {
1231
1388
  readonly userId: string;
1232
1389
  }
@@ -1352,6 +1509,21 @@ declare interface PasskeyRegisterVerifyInput {
1352
1509
  readonly transports?: string;
1353
1510
  }
1354
1511
 
1512
+ /** Passkeys (WebAuthn). */
1513
+ export declare interface PasskeyStore {
1514
+ readonly listPasskeys: (userId: string) => Effect.Effect<ReadonlyArray<PasskeyRecord>>;
1515
+ readonly findPasskey: (credentialId: string) => Effect.Effect<PasskeyRecord | null>;
1516
+ readonly insertPasskey: (passkey: Omit<PasskeyRecord, 'createdAt' | 'lastUsedAt'>) => Effect.Effect<PasskeyRecord>;
1517
+ /** Atomically bump the stored signature counter, guarding against a
1518
+ * cloned authenticator (WebAuthn §6.1.1): the update only applies when
1519
+ * `newCounter` strictly exceeds the stored counter. Returns whether a
1520
+ * row was advanced. `false` with an unchanged stored counter is the
1521
+ * clone/replay signal the assertion handler rejects on — the check and
1522
+ * the write are ONE atomic statement at the store, so two replicas
1523
+ * racing the same counter can't both succeed. */
1524
+ readonly advancePasskeyCounter: (credentialId: string, newCounter: number) => Effect.Effect<boolean>;
1525
+ }
1526
+
1355
1527
  /** Default password-reset lifetime — a bit longer. */
1356
1528
  export declare const PASSWORD_RESET_TTL_S: number;
1357
1529
 
@@ -1386,13 +1558,20 @@ declare interface PasswordResetRequestInput {
1386
1558
 
1387
1559
  /**
1388
1560
  * Build a UserStore that reads + writes the auth tables via the provided
1389
- * `SqlClient`. The caller owns the client's lifecycle.
1561
+ * `SqlClient`. The caller owns the client's lifecycle. `options.identity`
1562
+ * points the identity role at an existing table (see `IdentityTableMapping`).
1390
1563
  *
1391
1564
  * Find* flatten database-error channels to `never` (we treat connection
1392
1565
  * issues as "not found" — surfacing lets a brute-forcer tell flaky DB
1393
1566
  * from missing user). `insert` surfaces `UserAlreadyExistsError`.
1394
1567
  */
1395
- export declare const postgresUserStore: (sql: SqlClient) => UserStore;
1568
+ export declare const postgresUserStore: (sql: SqlClient, options?: PostgresUserStoreOptions) => UserStore;
1569
+
1570
+ export declare interface PostgresUserStoreOptions {
1571
+ /** Map the identity role onto an existing table. Every other role stays on
1572
+ * the plugin's own tables. */
1573
+ readonly identity?: IdentityTableMapping;
1574
+ }
1396
1575
 
1397
1576
  /**
1398
1577
  * Read + verify the session cookie from a Cookie header string.
@@ -1517,7 +1696,7 @@ export declare interface SendEmailInput {
1517
1696
  * suppressed it or no transport is wired. The caller must NOT surface that
1518
1697
  * distinction to an unauthenticated client (see `handleEmailVerifyRequest`).
1519
1698
  */
1520
- export declare const sendEmailVerification: (user: UserRecord, store: UserStore, deps: EmailVerificationDeps) => Effect.Effect<boolean>;
1699
+ export declare const sendEmailVerification: (user: UserRecord, store: TokenStore, deps: EmailVerificationDeps) => Effect.Effect<boolean>;
1521
1700
 
1522
1701
  export declare const SESSION_COOKIE_NAME: "voltro:session";
1523
1702
 
@@ -1589,6 +1768,29 @@ export { SessionSecrets }
1589
1768
  */
1590
1769
  export declare const sessionSecretsOf: (secret: SessionSecretInput) => SessionSecrets;
1591
1770
 
1771
+ /** Server-side sessions — enumeration + revocation. */
1772
+ export declare interface SessionStore {
1773
+ /** Active sessions for a user (most recent first). */
1774
+ readonly listSessions: (userId: string) => Effect.Effect<ReadonlyArray<SessionRecord>>;
1775
+ /** Record a session on sign-in. */
1776
+ readonly insertSession: (session: Omit<SessionRecord, 'createdAt' | 'lastSeenAt'>) => Effect.Effect<SessionRecord>;
1777
+ /** Look up one session row by id. The request-time revocation check
1778
+ * (`makeSessionRevocationChecker`) calls this — a missing row means
1779
+ * the session was revoked. */
1780
+ readonly findSession: (sessionId: string) => Effect.Effect<SessionRecord | null>;
1781
+ /** Slide a session row forward: bump `expiresAt` + `lastSeenAt`. Called
1782
+ * when the sliding-window renewal re-issues the cookie, so the row
1783
+ * outlives the renewed cookie and the revocation check keeps passing. */
1784
+ readonly touchSession: (sessionId: string, expiresAt: Date) => Effect.Effect<void>;
1785
+ /** Revoke a single session — only if it belongs to `userId` (so a
1786
+ * caller can't revoke another user's session by id-guessing). Returns
1787
+ * whether a row was removed. */
1788
+ readonly revokeSession: (userId: string, sessionId: string) => Effect.Effect<boolean>;
1789
+ /** Revoke every session for the user EXCEPT `keepSessionId` — the
1790
+ * "sign out other devices" action. Returns the count removed. */
1791
+ readonly revokeAllOtherSessions: (userId: string, keepSessionId: string) => Effect.Effect<number>;
1792
+ }
1793
+
1592
1794
  export declare interface SignInInput {
1593
1795
  readonly email: string;
1594
1796
  readonly password: string;
@@ -1707,6 +1909,28 @@ export declare interface TokenRecord {
1707
1909
  readonly createdAt: Date;
1708
1910
  }
1709
1911
 
1912
+ /** Single-use tokens: magic link, password reset, MFA pending, email verification. */
1913
+ export declare interface TokenStore {
1914
+ /** Persist a single-use token (already hashed). */
1915
+ readonly insertToken: (token: Omit<TokenRecord, 'consumedAt' | 'createdAt'>) => Effect.Effect<TokenRecord>;
1916
+ /** Atomically redeem a token by its hash + purpose: returns the row
1917
+ * only when it exists, matches the purpose, is unexpired, and was not
1918
+ * already consumed — and marks it consumed in the same step. Returns
1919
+ * null otherwise (single-use guard). */
1920
+ readonly consumeToken: (tokenHash: string, purpose: TokenPurpose) => Effect.Effect<TokenRecord | null>;
1921
+ /**
1922
+ * The most recently CREATED token of this purpose for the user, consumed or
1923
+ * not — or null when there is none.
1924
+ *
1925
+ * Exists for the resend cooldown, which is the only bound on an endpoint
1926
+ * that sends mail to an address supplied by an unauthenticated caller. Note
1927
+ * it deliberately does NOT filter on `consumedAt`: the cost being rate-
1928
+ * limited is the EMAIL, and a token that was minted and then redeemed cost
1929
+ * exactly as much to send as one still pending.
1930
+ */
1931
+ readonly latestToken: (userId: string, purpose: TokenPurpose) => Effect.Effect<TokenRecord | null>;
1932
+ }
1933
+
1710
1934
  export declare class TotpVerifyError extends TotpVerifyError_base<{
1711
1935
  readonly reason: 'invalidCode' | 'malformedSecret' | 'replay';
1712
1936
  }> {
@@ -1795,148 +2019,8 @@ export declare interface UserRecord {
1795
2019
  readonly deactivatedAt?: Date | null;
1796
2020
  }
1797
2021
 
1798
- export declare interface UserStore {
1799
- readonly findByEmail: (email: string) => Effect.Effect<UserRecord | null>;
1800
- readonly findById: (id: string) => Effect.Effect<UserRecord | null>;
1801
- readonly insert: (user: Omit<UserRecord, 'createdAt'>) => Effect.Effect<UserRecord, UserAlreadyExistsError>;
1802
- /** Replace a user's stored password hash. Used by password-reset AND
1803
- * by rehash-on-verify (silently upgrading an under-cost hash on a
1804
- * successful sign-in). */
1805
- readonly updatePassword: (userId: string, newHash: string) => Effect.Effect<UserRecord, UserNotFoundError>;
1806
- /** Stash a freshly-generated TOTP secret on the user. Called by
1807
- * the MFA enrolment handler before the user verifies the first
1808
- * code. */
1809
- readonly setMfaSecret: (userId: string, secret: string) => Effect.Effect<UserRecord, UserNotFoundError>;
1810
- /** Mark MFA as fully enrolled (called after the first successful
1811
- * TOTP verify completes the enrolment ceremony). */
1812
- readonly markMfaEnrolled: (userId: string) => Effect.Effect<UserRecord, UserNotFoundError>;
1813
- /** Wipe the secret + enrolledAt. Used by /account/security's
1814
- * "remove MFA" action. */
1815
- readonly clearMfa: (userId: string) => Effect.Effect<UserRecord, UserNotFoundError>;
1816
- /**
1817
- * Stamp `emailVerifiedAt` — the address has been proven.
1818
- *
1819
- * IDEMPOTENT and non-regressing: a user already verified keeps their
1820
- * ORIGINAL timestamp. "When was this address first proven" is the question
1821
- * the column answers, and re-stamping it on every later magic-link login
1822
- * would quietly turn it into "when did they last click something".
1823
- */
1824
- readonly markEmailVerified: (userId: string) => Effect.Effect<UserRecord, UserNotFoundError>;
1825
- /** Every tenant the user belongs to. */
1826
- readonly listMemberships: (userId: string) => Effect.Effect<ReadonlyArray<MembershipRecord>>;
1827
- /** Idempotent grant: inserts the [userId, tenantId] membership or
1828
- * updates its role if it already exists. The DB-level composite
1829
- * UNIQUE on `[userId, tenantId]` (see `membershipsTable`) is the
1830
- * source of truth; this upsert converges to it. */
1831
- readonly addMembership: (membership: Omit<MembershipRecord, 'joinedAt'>) => Effect.Effect<MembershipRecord>;
1832
- /** Resolve the role for a [userId, tenantId] pair, or null when the
1833
- * user is NOT a member. The switch-tenant handler calls this to
1834
- * validate the requested tenant before rebinding the connection. */
1835
- readonly membershipRole: (userId: string, tenantId: string) => Effect.Effect<string | null>;
1836
- /** Active sessions for a user (most recent first). */
1837
- readonly listSessions: (userId: string) => Effect.Effect<ReadonlyArray<SessionRecord>>;
1838
- /** Record a session on sign-in. */
1839
- readonly insertSession: (session: Omit<SessionRecord, 'createdAt' | 'lastSeenAt'>) => Effect.Effect<SessionRecord>;
1840
- /** Look up one session row by id. The request-time revocation check
1841
- * (`makeSessionRevocationChecker`) calls this — a missing row means
1842
- * the session was revoked. */
1843
- readonly findSession: (sessionId: string) => Effect.Effect<SessionRecord | null>;
1844
- /** Slide a session row forward: bump `expiresAt` + `lastSeenAt`. Called
1845
- * when the sliding-window renewal re-issues the cookie, so the row
1846
- * outlives the renewed cookie and the revocation check keeps passing. */
1847
- readonly touchSession: (sessionId: string, expiresAt: Date) => Effect.Effect<void>;
1848
- /** Revoke a single session — only if it belongs to `userId` (so a
1849
- * caller can't revoke another user's session by id-guessing). Returns
1850
- * whether a row was removed. */
1851
- readonly revokeSession: (userId: string, sessionId: string) => Effect.Effect<boolean>;
1852
- /** Revoke every session for the user EXCEPT `keepSessionId` — the
1853
- * "sign out other devices" action. Returns the count removed. */
1854
- readonly revokeAllOtherSessions: (userId: string, keepSessionId: string) => Effect.Effect<number>;
1855
- /** Persist a single-use token (already hashed). */
1856
- readonly insertToken: (token: Omit<TokenRecord, 'consumedAt' | 'createdAt'>) => Effect.Effect<TokenRecord>;
1857
- /** Atomically redeem a token by its hash + purpose: returns the row
1858
- * only when it exists, matches the purpose, is unexpired, and was not
1859
- * already consumed — and marks it consumed in the same step. Returns
1860
- * null otherwise (single-use guard). */
1861
- readonly consumeToken: (tokenHash: string, purpose: TokenPurpose) => Effect.Effect<TokenRecord | null>;
1862
- /**
1863
- * The most recently CREATED token of this purpose for the user, consumed or
1864
- * not — or null when there is none.
1865
- *
1866
- * Exists for the resend cooldown, which is the only bound on an endpoint
1867
- * that sends mail to an address supplied by an unauthenticated caller. Note
1868
- * it deliberately does NOT filter on `consumedAt`: the cost being rate-
1869
- * limited is the EMAIL, and a token that was minted and then redeemed cost
1870
- * exactly as much to send as one still pending.
1871
- */
1872
- readonly latestToken: (userId: string, purpose: TokenPurpose) => Effect.Effect<TokenRecord | null>;
1873
- /** Persist an invitation (token already hashed). */
1874
- readonly insertInvitation: (invitation: Omit<InvitationRecord, 'acceptedAt' | 'acceptedByUserId' | 'revokedAt' | 'revokedByUserId' | 'createdAt'>) => Effect.Effect<InvitationRecord>;
1875
- /** Look an invitation up by token hash WITHOUT redeeming it — the read the
1876
- * "what am I being invited to" preview needs. Returns the row whatever its
1877
- * state; the caller decides what an expired or revoked one means. */
1878
- readonly findInvitationByHash: (tokenHash: string) => Effect.Effect<InvitationRecord | null>;
1879
- /**
1880
- * Atomically redeem an invitation: returns the row only when it exists, is
1881
- * unaccepted, unrevoked and unexpired — and stamps `acceptedAt` +
1882
- * `acceptedByUserId` in the same step. Null otherwise.
1883
- *
1884
- * The single-use guard, and it has to be ONE statement: two clicks on the
1885
- * same emailed link race, and a read-then-write would let both through and
1886
- * write the membership twice.
1887
- */
1888
- readonly acceptInvitation: (tokenHash: string, acceptedByUserId: string, now: Date) => Effect.Effect<InvitationRecord | null>;
1889
- /** Withdraw a PENDING invitation, scoped to its tenant so an admin of one
1890
- * tenant cannot revoke another's by id. Returns whether a row changed. */
1891
- readonly revokeInvitation: (invitationId: string, tenantId: string, revokedByUserId: string, now: Date) => Effect.Effect<boolean>;
1892
- /** Every invitation for a tenant, newest first. The admin list. */
1893
- readonly listInvitations: (tenantId: string) => Effect.Effect<ReadonlyArray<InvitationRecord>>;
1894
- /** How many invitations for this tenant are still redeemable — the bound
1895
- * `maxPending` enforces. */
1896
- readonly countPendingInvitations: (tenantId: string, now: Date) => Effect.Effect<number>;
1897
- /** Revoke every pending invitation for (tenant, email). Called before
1898
- * issuing a fresh one so re-inviting supersedes rather than accumulates —
1899
- * otherwise every resend leaves a live token behind. Returns the count. */
1900
- readonly supersedePendingInvitations: (tenantId: string, email: string, revokedByUserId: string, now: Date) => Effect.Effect<number>;
1901
- /** Record an impersonation grant when the impersonated session is minted. */
1902
- readonly insertImpersonationGrant: (grant: Omit<ImpersonationGrantRecord, 'startedAt' | 'endedAt' | 'endedReason'>) => Effect.Effect<ImpersonationGrantRecord>;
1903
- /** The grant for an impersonated session, or null. The stop path's lookup. */
1904
- readonly findImpersonationGrantBySession: (sessionId: string) => Effect.Effect<ImpersonationGrantRecord | null>;
1905
- /** Close a grant. Only an OPEN grant is closed (`endedAt IS NULL`), so a
1906
- * double stop is a no-op rather than a rewritten end time. */
1907
- readonly endImpersonationGrant: (sessionId: string, endedReason: string, now: Date) => Effect.Effect<boolean>;
1908
- /** Replace the user's recovery codes with a fresh (already hashed) set.
1909
- * Called at enrolment / regeneration — wipes any prior codes so the
1910
- * displayed set is the only valid one. */
1911
- readonly replaceRecoveryCodes: (userId: string, codeHashes: ReadonlyArray<string>) => Effect.Effect<void>;
1912
- /** Atomically redeem one recovery code by its hash: returns true only
1913
- * when an unconsumed row for `userId` matched — and marks it consumed
1914
- * in the same step (single-use). false otherwise. */
1915
- readonly consumeRecoveryCode: (userId: string, codeHash: string) => Effect.Effect<boolean>;
1916
- /** Count the user's remaining (unconsumed) recovery codes — surfaced so
1917
- * the UI can warn when the user is running low. */
1918
- readonly countRecoveryCodes: (userId: string) => Effect.Effect<number>;
1919
- readonly listPasskeys: (userId: string) => Effect.Effect<ReadonlyArray<PasskeyRecord>>;
1920
- readonly findPasskey: (credentialId: string) => Effect.Effect<PasskeyRecord | null>;
1921
- readonly insertPasskey: (passkey: Omit<PasskeyRecord, 'createdAt' | 'lastUsedAt'>) => Effect.Effect<PasskeyRecord>;
1922
- /** Atomically bump the stored signature counter, guarding against a
1923
- * cloned authenticator (WebAuthn §6.1.1): the update only applies when
1924
- * `newCounter` strictly exceeds the stored counter. Returns whether a
1925
- * row was advanced. `false` with an unchanged stored counter is the
1926
- * clone/replay signal the assertion handler rejects on — the check and
1927
- * the write are ONE atomic statement at the store, so two replicas
1928
- * racing the same counter can't both succeed. */
1929
- readonly advancePasskeyCounter: (credentialId: string, newCounter: number) => Effect.Effect<boolean>;
1930
- /** Is this email currently locked out? Reads only — never mutates. Returns
1931
- * the remaining cooldown so the handler can surface a retry-after. */
1932
- readonly isLockedOut: (email: string, now: number) => Effect.Effect<LockoutState>;
1933
- /** Record one failed credential attempt for `email`. Opens a fresh window
1934
- * when the previous one (or a prior lock) has expired, otherwise increments;
1935
- * sets the lock once `failedCount` reaches `policy.maxAttempts`. Called only
1936
- * for a NOT-currently-locked email (the handler checks `isLockedOut` first). */
1937
- readonly recordLoginFailure: (email: string, now: number, policy: LockoutPolicy) => Effect.Effect<void>;
1938
- /** Clear the counter for `email` on a completed login. */
1939
- readonly clearLoginFailures: (email: string) => Effect.Effect<void>;
2022
+ /** Every role at once — what the shipped stores return and what `authRoutesPlugin` takes. */
2023
+ export declare interface UserStore extends IdentityStore, MfaStore, MembershipStore, SessionStore, TokenStore, InvitationStore, ImpersonationStore, PasskeyStore, LockoutStore {
1940
2024
  }
1941
2025
 
1942
2026
  /** Verify a passkey assertion (sign-in). */