@luckystack/login 0.5.1 → 0.6.1

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
@@ -305,23 +305,104 @@ declare const registerSessionSanitizer: (sanitizer: SessionSanitizer) => void;
305
305
  /** Read the active sanitizer (or `null` when none is registered). */
306
306
  declare const getSessionSanitizer: () => SessionSanitizer | null;
307
307
 
308
+ interface UserAdapterCreateInput {
309
+ email: string;
310
+ provider: string;
311
+ name?: string | null;
312
+ password?: string | null;
313
+ avatar?: string | null;
314
+ avatarFallback?: string | null;
315
+ language?: string | null;
316
+ }
317
+ interface UserRecord extends BaseSessionLayout {
318
+ password?: string | null;
319
+ provider?: string | null;
320
+ /** Most recent successful login. Mirrors the optional `lastLogin` column on User. */
321
+ lastLogin?: Date | null;
322
+ /**
323
+ * 2FA (ADR 0024): whether this user enrolled a second factor. All three 2FA
324
+ * fields are OPTIONAL so pre-0.6 schemas/adapters keep working unchanged —
325
+ * an adapter that never returns them simply has no 2FA-capable users.
326
+ */
327
+ twoFactorEnabled?: boolean | null;
328
+ /**
329
+ * 2FA: the TOTP shared secret (base32; `gcm:`-prefixed when encrypted at
330
+ * rest via TOTP_ENCRYPTION_KEY). NEVER reaches the client — stripped by
331
+ * `sanitizeUserForSession` before any session persist/broadcast.
332
+ */
333
+ totpSecret?: string | null;
334
+ /** 2FA: sha256 hashes of the UNUSED one-time recovery codes (also stripped). */
335
+ recoveryCodes?: string[] | null;
336
+ }
337
+ interface UserAdapter {
338
+ findByEmail: (params: {
339
+ email: string;
340
+ provider: string;
341
+ }) => Promise<UserRecord | null>;
342
+ /**
343
+ * Look up a user by email IRRESPECTIVE of provider. Required only when
344
+ * `auth.providerAccountStrategy === 'unified'` — the framework resolves
345
+ * accounts by email alone so the same address maps to one User across
346
+ * providers. Optional so existing custom adapters keep compiling; when a
347
+ * project sets `'unified'` but its adapter omits this method, the framework
348
+ * logs a one-time warning and falls back to provider-scoped lookup.
349
+ */
350
+ findByEmailAnyProvider?: (params: {
351
+ email: string;
352
+ }) => Promise<UserRecord | null>;
353
+ findById: (id: string) => Promise<UserRecord | null>;
354
+ create: (input: UserAdapterCreateInput) => Promise<UserRecord>;
355
+ update: (id: string, patch: Partial<UserRecord>) => Promise<UserRecord>;
356
+ /**
357
+ * Hard-delete a user. Account-deletion flows (`deleteAccount_v1`) call this
358
+ * instead of reaching past the adapter to `prisma.user.delete`, so a custom
359
+ * adapter / non-Prisma store / soft-delete strategy stays in control. To
360
+ * implement a soft delete, ignore the hard delete here and instead expose it
361
+ * through `update({ deletedAt: new Date() })` from your route.
362
+ *
363
+ * Optional so existing custom adapters keep compiling; the default Prisma
364
+ * adapter implements it. Consumers calling account deletion against a custom
365
+ * adapter that omits `delete` get a clear "adapter does not support delete"
366
+ * failure rather than a silent Prisma bypass.
367
+ */
368
+ delete?: (id: string) => Promise<void>;
369
+ }
370
+ declare const registerUserAdapter: (adapter: UserAdapter) => UserAdapter;
371
+ declare const isUserAdapterRegistered: () => boolean;
372
+ declare const defaultPrismaUserAdapter: () => UserAdapter;
373
+ declare const getUserAdapter: () => UserAdapter;
374
+
308
375
  interface LoginOrRegisterParams {
309
376
  email?: string;
310
377
  password?: string;
311
378
  name?: string;
312
379
  confirmPassword?: string;
313
380
  }
381
+ type TwoFactorMethod = 'totp' | 'email-code' | 'recovery-code';
314
382
  interface CredentialsLoginSuccess {
315
383
  status: true;
316
384
  reason: string;
317
385
  newToken: string;
318
386
  session: BaseSessionLayout;
387
+ requiresTwoFactor?: undefined;
388
+ }
389
+ interface CredentialsLoginChallenge {
390
+ status: true;
391
+ reason: string;
392
+ requiresTwoFactor: true;
393
+ challengeToken: string;
394
+ twoFactorMethods: TwoFactorMethod[];
395
+ newToken?: undefined;
396
+ session?: undefined;
319
397
  }
320
398
  interface CredentialsLoginFailure {
321
399
  status: false;
322
400
  reason: string;
401
+ newToken?: undefined;
402
+ session?: undefined;
403
+ requiresTwoFactor?: undefined;
323
404
  }
324
- type CredentialsLoginResult = CredentialsLoginSuccess | CredentialsLoginFailure;
405
+ type CredentialsLoginResult = CredentialsLoginSuccess | CredentialsLoginChallenge | CredentialsLoginFailure;
325
406
  declare const OAUTH_STATE_COOKIE_NAME = "ls-oauth-state";
326
407
  interface CreateOAuthStateResult {
327
408
  /** Opaque `state` param put on the authorize URL + echoed back on callback. */
@@ -351,6 +432,16 @@ declare const registerWithCredentials: ({ email, password, name, confirmPassword
351
432
  }, options?: {
352
433
  supersedeToken?: string;
353
434
  }) => Promise<CredentialsLoginResult>;
435
+ type TwoFactorGate = (user: UserRecord, context: {
436
+ requesterIp?: string;
437
+ }) => Promise<CredentialsLoginChallenge | null>;
438
+ declare const registerTwoFactorGate: (gate: TwoFactorGate) => void;
439
+ declare const finalizeLogin: (user: UserRecord, options: {
440
+ provider: string;
441
+ email?: string;
442
+ supersedeToken?: string;
443
+ requesterIp?: string;
444
+ }) => Promise<CredentialsLoginResult>;
354
445
  declare const loginWithCredentialsCore: ({ email, password }: {
355
446
  email: string;
356
447
  password: string;
@@ -698,58 +789,153 @@ declare const registerOAuthProviders: (providers: OAuthProvider[]) => OAuthProvi
698
789
  */
699
790
  declare const registerOAuthProviderFactory: (factory: () => OAuthProvider[]) => void;
700
791
 
701
- interface UserAdapterCreateInput {
702
- email: string;
703
- provider: string;
704
- name?: string | null;
705
- password?: string | null;
706
- avatar?: string | null;
707
- avatarFallback?: string | null;
708
- language?: string | null;
709
- }
710
- interface UserRecord extends BaseSessionLayout {
711
- password?: string | null;
712
- provider?: string | null;
713
- /** Most recent successful login. Mirrors the optional `lastLogin` column on User. */
714
- lastLogin?: Date | null;
792
+ declare const availableTwoFactorMethods: (user: UserRecord) => TwoFactorMethod[];
793
+ declare const createTwoFactorChallengeIfRequired: (user: UserRecord, _context: {
794
+ requesterIp?: string;
795
+ }) => Promise<CredentialsLoginChallenge | null>;
796
+ interface VerifyTwoFactorInput {
797
+ challengeToken: string;
798
+ code: string;
799
+ /** Defaults to 'totp'; the client sends what the user picked. */
800
+ method?: TwoFactorMethod;
801
+ supersedeToken?: string;
802
+ requesterIp?: string;
715
803
  }
716
- interface UserAdapter {
717
- findByEmail: (params: {
718
- email: string;
719
- provider: string;
720
- }) => Promise<UserRecord | null>;
804
+ declare const verifyTwoFactorChallenge: (input: VerifyTwoFactorInput) => Promise<CredentialsLoginResult>;
805
+ declare const requestTwoFactorEmailCode: (challengeToken: string) => Promise<{
806
+ ok: boolean;
807
+ reason?: string;
808
+ }>;
809
+ type TotpEnrollmentStart = {
810
+ ok: true;
811
+ secret: string;
812
+ otpauthUri: string;
813
+ } | {
814
+ ok: false;
815
+ reason: string;
816
+ };
817
+ /**
818
+ * Step 1: mint a secret and park it as PENDING (Redis, 10 min) — the user
819
+ * record is untouched until the user proves the app works via
820
+ * `confirmTotpEnrollment`. Re-calling replaces the pending secret. Refused for
821
+ * an already-enrolled user / a disabled feature (see enrollmentBlockedReason).
822
+ */
823
+ declare const beginTotpEnrollment: (user: UserRecord) => Promise<TotpEnrollmentStart>;
824
+ type ConfirmTotpEnrollmentResult = {
825
+ ok: true;
826
+ recoveryCodes: string[];
827
+ } | {
828
+ ok: false;
829
+ reason: string;
830
+ };
831
+ /**
832
+ * Step 2: the user enters the first code from their app. On success the
833
+ * secret + recovery-code hashes land on the user record and 2FA is ON.
834
+ * The RAW recovery codes are returned exactly once — show + let them save.
835
+ */
836
+ declare const confirmTotpEnrollment: (user: UserRecord, code: string) => Promise<ConfirmTotpEnrollmentResult>;
837
+ /**
838
+ * Disable 2FA. Requires a currently-valid TOTP code OR an unused recovery
839
+ * code, so a hijacked (but 2FA-less) session can't silently strip the factor.
840
+ */
841
+ declare const disableTwoFactor: (user: UserRecord, code: string) => Promise<{
842
+ ok: boolean;
843
+ reason?: string;
844
+ }>;
845
+ /** Replace the recovery-code set (requires a valid current TOTP code). */
846
+ declare const regenerateRecoveryCodes: (user: UserRecord, code: string) => Promise<ConfirmTotpEnrollmentResult>;
847
+
848
+ declare const base32Encode: (input: Buffer) => string;
849
+ declare const base32Decode: (input: string) => Buffer | null;
850
+ declare const hotp: (key: Buffer, counter: number, digits?: number) => string;
851
+ interface TotpVerifyInput {
852
+ /** base32-encoded shared secret (as provisioned to the authenticator app). */
853
+ secret: string;
854
+ /** The user-entered code (digits; surrounding whitespace tolerated). */
855
+ code: string;
721
856
  /**
722
- * Look up a user by email IRRESPECTIVE of provider. Required only when
723
- * `auth.providerAccountStrategy === 'unified'` — the framework resolves
724
- * accounts by email alone so the same address maps to one User across
725
- * providers. Optional so existing custom adapters keep compiling; when a
726
- * project sets `'unified'` but its adapter omits this method, the framework
727
- * logs a one-time warning and falls back to provider-scoped lookup.
857
+ * Accepted clock drift in 30s steps on EITHER side of "now" (default 1 —
858
+ * i.e. the previous, current and next code are accepted; RFC 6238 §5.2
859
+ * recommends at most one backward step).
728
860
  */
729
- findByEmailAnyProvider?: (params: {
730
- email: string;
731
- }) => Promise<UserRecord | null>;
732
- findById: (id: string) => Promise<UserRecord | null>;
733
- create: (input: UserAdapterCreateInput) => Promise<UserRecord>;
734
- update: (id: string, patch: Partial<UserRecord>) => Promise<UserRecord>;
861
+ window?: number;
862
+ /** Injection point for tests; defaults to the real clock. */
863
+ timestampMs?: number;
864
+ }
865
+ interface TotpVerifyResult {
866
+ valid: boolean;
735
867
  /**
736
- * Hard-delete a user. Account-deletion flows (`deleteAccount_v1`) call this
737
- * instead of reaching past the adapter to `prisma.user.delete`, so a custom
738
- * adapter / non-Prisma store / soft-delete strategy stays in control. To
739
- * implement a soft delete, ignore the hard delete here and instead expose it
740
- * through `update({ deletedAt: new Date() })` from your route.
741
- *
742
- * Optional so existing custom adapters keep compiling; the default Prisma
743
- * adapter implements it. Consumers calling account deletion against a custom
744
- * adapter that omits `delete` get a clear "adapter does not support delete"
745
- * failure rather than a silent Prisma bypass.
868
+ * The timestep the code matched (only when valid). Persist the highest
869
+ * accepted value per user and refuse `timestep <= lastAccepted` to make
870
+ * every code single-use (replay protection across the drift window).
746
871
  */
747
- delete?: (id: string) => Promise<void>;
872
+ timestep: number | null;
873
+ }
874
+ declare const verifyTotp: ({ secret, code, window, timestampMs }: TotpVerifyInput) => TotpVerifyResult;
875
+ /** 20 random bytes (RFC 4226 §4 minimum: 128 bits; 160 recommended), base32. */
876
+ declare const generateTotpSecret: () => string;
877
+ interface OtpauthUriInput {
878
+ /** base32 secret from generateTotpSecret(). */
879
+ secret: string;
880
+ /** What the authenticator app shows as the account (usually the user's email). */
881
+ accountName: string;
882
+ /** App/brand name shown above the account (defaults to the project title). */
883
+ issuer: string;
884
+ }
885
+ declare const buildOtpauthUri: ({ secret, accountName, issuer }: OtpauthUriInput) => string;
886
+
887
+ interface RequestEmailLoginCodeInput {
888
+ email: string;
889
+ requesterIp?: string;
748
890
  }
749
- declare const registerUserAdapter: (adapter: UserAdapter) => UserAdapter;
750
- declare const isUserAdapterRegistered: () => boolean;
751
- declare const defaultPrismaUserAdapter: () => UserAdapter;
752
- declare const getUserAdapter: () => UserAdapter;
891
+ type RequestEmailLoginCodeResult = {
892
+ ok: true;
893
+ } | {
894
+ ok: false;
895
+ reason: string;
896
+ };
897
+ declare const requestEmailLoginCode: ({ email, requesterIp }: RequestEmailLoginCodeInput) => Promise<RequestEmailLoginCodeResult>;
898
+ interface VerifyEmailLoginCodeInput {
899
+ email: string;
900
+ code: string;
901
+ supersedeToken?: string;
902
+ requesterIp?: string;
903
+ }
904
+ declare const verifyEmailLoginCode: (input: VerifyEmailLoginCodeInput) => Promise<CredentialsLoginResult>;
905
+
906
+ type EmailOtpPurpose = 'login' | '2fa';
907
+ declare const generateNumericCode: (digits: number) => string;
908
+ interface IssueEmailCodeInput {
909
+ purpose: EmailOtpPurpose;
910
+ /** Email address (login) or userId (2fa). */
911
+ identity: string;
912
+ ttlSeconds: number;
913
+ digits: number;
914
+ }
915
+ /**
916
+ * Mint + store a fresh code for (purpose, identity). Any previously active
917
+ * code for the same slot is replaced, and its attempt counter reset. Returns
918
+ * the RAW code exactly once — the caller emails it; only sha256(code) is
919
+ * stored (hash-at-rest, ADR 0010 spirit).
920
+ */
921
+ declare const issueEmailCode: ({ purpose, identity, ttlSeconds, digits }: IssueEmailCodeInput) => Promise<string>;
922
+ type EmailCodeVerdict = 'valid' | 'invalid' | 'expired' | 'locked';
923
+ interface VerifyEmailCodeInput {
924
+ purpose: EmailOtpPurpose;
925
+ identity: string;
926
+ code: string;
927
+ /** Wrong-code attempts before the code is burned (counter is server-side). */
928
+ maxAttempts: number;
929
+ }
930
+ /**
931
+ * Verify + consume. Verdicts: 'valid' (code matched, slot consumed),
932
+ * 'invalid' (wrong code, attempt recorded), 'locked' (attempt budget spent —
933
+ * the active code is burned so guessing can't continue), 'expired' (no active
934
+ * code: TTL passed, never issued, or already consumed).
935
+ */
936
+ declare const verifyEmailCode: ({ purpose, identity, code, maxAttempts }: VerifyEmailCodeInput) => Promise<EmailCodeVerdict>;
937
+ /** Drop any active code + counter for the slot (e.g. after a completed login). */
938
+ declare const clearEmailCode: (purpose: EmailOtpPurpose, identity: string) => Promise<void>;
753
939
 
754
940
  interface PostLoginRedirectInput {
755
941
  userId: string;
@@ -761,4 +947,4 @@ type PostLoginRedirectResolver = (input: PostLoginRedirectInput) => string | Pro
761
947
  declare const registerPostLoginRedirect: (resolver: PostLoginRedirectResolver) => PostLoginRedirectResolver;
762
948
  declare const getPostLoginRedirect: () => PostLoginRedirectResolver | null;
763
949
 
764
- export { type CreateOAuthStateResult, type CredentialsLoginFailure, type CredentialsLoginResult, type CredentialsLoginSuccess, type CredentialsProvider, type EmailChangePayload, type FullOAuthProvider, type LoginFailedPayload, OAUTH_STATE_COOKIE_NAME, type OAuthCallbackResult, type OAuthProvider, type OAuthUserData, type PasswordChangedPayload, PasswordPolicyError, type PasswordResetCompletedPayload, type PasswordResetRequestedPayload, type PostAccountDeletePayload, type PostEmailChangeRequestedPayload, type PostEmailChangedPayload, type PostLoginPayload, type PostLoginRedirectInput, type PostLoginRedirectResolver, type PostLogoutPayload, type PostRegisterPayload, type PostSessionCreatePayload, type PostSessionDeletePayload, type PreAccountDeletePayload, type PreEmailChangePayload, type PreLoginPayload, type PreLogoutPayload, type PrePasswordChangedPayload, type PrePasswordResetCompletedPayload, type PreRegisterPayload, type PreSessionCreatePayload, type PreSessionDeletePayload, type SessionAdapter, type SessionSanitizer, type UserAdapter, type UserAdapterCreateInput, type UserRecord, activeUsersKeyFor, asOAuthUserData, clearAuthFailures, consumeEmailChangeToken, consumePasswordResetToken, createEmailChangeToken, createOAuthState, createPasswordResetToken, credentialsProvider, defaultPrismaUserAdapter, deleteSession, discordProvider, facebookProvider, getAllSessions, getOAuthProviders, getPostLoginRedirect, getSession, getSessionAdapter, getSessionSanitizer, getUserAdapter, githubProvider, googleProvider, isAccountLocked, isFullOAuthProvider, isUserAdapterRegistered, loginCallback, loginWithCredentials, loginWithCredentialsCore, logout, microsoftProvider, recordAuthFailure, redisSessionAdapter, registerOAuthProviderFactory, registerOAuthProviders, registerPostLoginRedirect, registerSessionAdapter, registerSessionSanitizer, registerUserAdapter, registerWithCredentials, revokeUserSessions, saveSession, sendEmailChangeConfirmation, sendPasswordResetEmail, sessionKeyFor, updatePasswordHash, validatePassword, verifyPassword };
950
+ export { type ConfirmTotpEnrollmentResult, type CreateOAuthStateResult, type CredentialsLoginChallenge, type CredentialsLoginFailure, type CredentialsLoginResult, type CredentialsLoginSuccess, type CredentialsProvider, type EmailChangePayload, type EmailCodeVerdict, type EmailOtpPurpose, type FullOAuthProvider, type LoginFailedPayload, OAUTH_STATE_COOKIE_NAME, type OAuthCallbackResult, type OAuthProvider, type OAuthUserData, type PasswordChangedPayload, PasswordPolicyError, type PasswordResetCompletedPayload, type PasswordResetRequestedPayload, type PostAccountDeletePayload, type PostEmailChangeRequestedPayload, type PostEmailChangedPayload, type PostLoginPayload, type PostLoginRedirectInput, type PostLoginRedirectResolver, type PostLogoutPayload, type PostRegisterPayload, type PostSessionCreatePayload, type PostSessionDeletePayload, type PreAccountDeletePayload, type PreEmailChangePayload, type PreLoginPayload, type PreLogoutPayload, type PrePasswordChangedPayload, type PrePasswordResetCompletedPayload, type PreRegisterPayload, type PreSessionCreatePayload, type PreSessionDeletePayload, type RequestEmailLoginCodeInput, type RequestEmailLoginCodeResult, type SessionAdapter, type SessionSanitizer, type TotpEnrollmentStart, type TwoFactorGate, type TwoFactorMethod, type UserAdapter, type UserAdapterCreateInput, type UserRecord, type VerifyEmailLoginCodeInput, type VerifyTwoFactorInput, activeUsersKeyFor, asOAuthUserData, availableTwoFactorMethods, base32Decode, base32Encode, beginTotpEnrollment, buildOtpauthUri, clearAuthFailures, clearEmailCode, confirmTotpEnrollment, consumeEmailChangeToken, consumePasswordResetToken, createEmailChangeToken, createOAuthState, createPasswordResetToken, createTwoFactorChallengeIfRequired, credentialsProvider, defaultPrismaUserAdapter, deleteSession, disableTwoFactor, discordProvider, facebookProvider, finalizeLogin, generateNumericCode, generateTotpSecret, getAllSessions, getOAuthProviders, getPostLoginRedirect, getSession, getSessionAdapter, getSessionSanitizer, getUserAdapter, githubProvider, googleProvider, hotp, isAccountLocked, isFullOAuthProvider, isUserAdapterRegistered, issueEmailCode, loginCallback, loginWithCredentials, loginWithCredentialsCore, logout, microsoftProvider, recordAuthFailure, redisSessionAdapter, regenerateRecoveryCodes, registerOAuthProviderFactory, registerOAuthProviders, registerPostLoginRedirect, registerSessionAdapter, registerSessionSanitizer, registerTwoFactorGate, registerUserAdapter, registerWithCredentials, requestEmailLoginCode, requestTwoFactorEmailCode, revokeUserSessions, saveSession, sendEmailChangeConfirmation, sendPasswordResetEmail, sessionKeyFor, updatePasswordHash, validatePassword, verifyEmailCode, verifyEmailLoginCode, verifyPassword, verifyTotp, verifyTwoFactorChallenge };