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