@voltro/plugin-auth 0.71.1 → 0.73.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/CHANGELOG.md +468 -0
- package/THIRD-PARTY-NOTICES.md +29 -1
- package/dist/index.d.ts +260 -176
- package/dist/index.js +184 -131
- package/dist/{plugin-CPcpSSf9.js → plugin-DFQ8N52Q.js} +236 -232
- package/dist/plugin.d.ts +179 -142
- package/dist/plugin.js +1 -1
- package/dist/strategy.d.ts +179 -142
- package/package.json +3 -3
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
523
|
+
}, store: MembershipStore) => Effect.Effect<HandlerResult>;
|
|
520
524
|
|
|
521
|
-
export declare const handleListSessions: (input: ListSessionsInput, store:
|
|
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:
|
|
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:
|
|
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:
|
|
541
|
+
export declare const handleMfaEnrollStart: (input: MfaEnrollStartInput, store: IdentityStore & MfaStore) => Effect.Effect<HandlerResult>;
|
|
538
542
|
|
|
539
|
-
export declare const handleMfaEnrollVerify: (input: MfaEnrollVerifyInput, store:
|
|
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:
|
|
547
|
+
export declare const handleMfaRegenerateRecoveryCodes: (input: MfaRegenerateRecoveryCodesInput, store: IdentityStore & MfaStore) => Effect.Effect<HandlerResult>;
|
|
544
548
|
|
|
545
|
-
export declare const handleMfaUnenroll: (input: MfaUnenrollInput, store:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
594
|
+
}, store: InvitationStore, deps?: InvitationDeps) => Effect.Effect<HandlerResultShape_2>;
|
|
591
595
|
|
|
592
|
-
export declare const handleRevokeAllOtherSessions: (input: RevokeOtherSessionsInput, store:
|
|
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:
|
|
602
|
+
}, store: MembershipStore & InvitationStore, deps: InvitationDeps) => Effect.Effect<HandlerResultShape_2>;
|
|
599
603
|
|
|
600
|
-
export declare const handleRevokeSession: (input: RevokeSessionInput, store:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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
|
-
|
|
1799
|
-
|
|
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). */
|