@account-kit/signer 4.22.0 → 4.23.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.
Files changed (62) hide show
  1. package/dist/esm/base.d.ts +183 -2
  2. package/dist/esm/base.js +352 -55
  3. package/dist/esm/base.js.map +1 -1
  4. package/dist/esm/client/base.d.ts +46 -5
  5. package/dist/esm/client/base.js.map +1 -1
  6. package/dist/esm/client/index.d.ts +51 -4
  7. package/dist/esm/client/index.js +201 -9
  8. package/dist/esm/client/index.js.map +1 -1
  9. package/dist/esm/client/types.d.ts +102 -1
  10. package/dist/esm/client/types.js.map +1 -1
  11. package/dist/esm/errors.d.ts +6 -0
  12. package/dist/esm/errors.js +18 -0
  13. package/dist/esm/errors.js.map +1 -1
  14. package/dist/esm/index.d.ts +1 -1
  15. package/dist/esm/index.js +1 -1
  16. package/dist/esm/index.js.map +1 -1
  17. package/dist/esm/session/manager.d.ts +2 -0
  18. package/dist/esm/session/manager.js.map +1 -1
  19. package/dist/esm/signer.d.ts +4 -2
  20. package/dist/esm/signer.js.map +1 -1
  21. package/dist/esm/types.d.ts +15 -1
  22. package/dist/esm/types.js +6 -0
  23. package/dist/esm/types.js.map +1 -1
  24. package/dist/esm/utils/parseMfaError.d.ts +2 -0
  25. package/dist/esm/utils/parseMfaError.js +15 -0
  26. package/dist/esm/utils/parseMfaError.js.map +1 -0
  27. package/dist/esm/version.d.ts +1 -1
  28. package/dist/esm/version.js +1 -1
  29. package/dist/esm/version.js.map +1 -1
  30. package/dist/types/base.d.ts +183 -2
  31. package/dist/types/base.d.ts.map +1 -1
  32. package/dist/types/client/base.d.ts +46 -5
  33. package/dist/types/client/base.d.ts.map +1 -1
  34. package/dist/types/client/index.d.ts +51 -4
  35. package/dist/types/client/index.d.ts.map +1 -1
  36. package/dist/types/client/types.d.ts +102 -1
  37. package/dist/types/client/types.d.ts.map +1 -1
  38. package/dist/types/errors.d.ts +6 -0
  39. package/dist/types/errors.d.ts.map +1 -1
  40. package/dist/types/index.d.ts +1 -1
  41. package/dist/types/index.d.ts.map +1 -1
  42. package/dist/types/session/manager.d.ts +2 -0
  43. package/dist/types/session/manager.d.ts.map +1 -1
  44. package/dist/types/signer.d.ts +4 -2
  45. package/dist/types/signer.d.ts.map +1 -1
  46. package/dist/types/types.d.ts +15 -1
  47. package/dist/types/types.d.ts.map +1 -1
  48. package/dist/types/utils/parseMfaError.d.ts +3 -0
  49. package/dist/types/utils/parseMfaError.d.ts.map +1 -0
  50. package/dist/types/version.d.ts +1 -1
  51. package/package.json +4 -4
  52. package/src/base.ts +421 -62
  53. package/src/client/base.ts +56 -2
  54. package/src/client/index.ts +225 -10
  55. package/src/client/types.ts +112 -1
  56. package/src/errors.ts +11 -1
  57. package/src/index.ts +5 -1
  58. package/src/session/manager.ts +6 -1
  59. package/src/signer.ts +6 -1
  60. package/src/types.ts +16 -0
  61. package/src/utils/parseMfaError.ts +15 -0
  62. package/src/version.ts +1 -1
@@ -2,11 +2,11 @@ import { type SmartAccountAuthenticator } from "@aa-sdk/core";
2
2
  import { type GetTransactionType, type Hex, type IsNarrowable, type LocalAccount, type SerializeTransactionFn, type SignableMessage, type TransactionSerializable, type TransactionSerialized, type TypedData, type TypedDataDefinition } from "viem";
3
3
  import { type Authorization } from "viem/experimental";
4
4
  import type { BaseSignerClient } from "./client/base";
5
- import type { OauthConfig, User } from "./client/types";
5
+ import type { MfaFactor, OauthConfig, User, VerifyMfaParams, EnableMfaParams, EnableMfaResult, RemoveMfaParams } from "./client/types";
6
6
  import { type SessionManagerParams } from "./session/manager.js";
7
7
  import type { AuthParams } from "./signer";
8
8
  import { SolanaSigner } from "./solanaSigner.js";
9
- import { type AlchemySignerEvents, type ErrorInfo } from "./types.js";
9
+ import { type AlchemySignerEvents, type ErrorInfo, type ValidateMultiFactorsArgs } from "./types.js";
10
10
  export interface BaseAlchemySignerParams<TClient extends BaseSignerClient> {
11
11
  client: TClient;
12
12
  sessionConfig?: Omit<SessionManagerParams, "client">;
@@ -281,6 +281,36 @@ export declare abstract class BaseAlchemySigner<TClient extends BaseSignerClient
281
281
  * @returns {Promise<Authorization<number, true>> | undefined} a promise that resolves to the authorization with the signature
282
282
  */
283
283
  signAuthorization: (unsignedAuthorization: Authorization<number, false>) => Promise<Authorization<number, true>>;
284
+ /**
285
+ * Gets the current MFA status
286
+ *
287
+ * @example
288
+ * ```ts
289
+ * import { AlchemyWebSigner } from "@account-kit/signer";
290
+ *
291
+ * const signer = new AlchemyWebSigner({
292
+ * client: {
293
+ * connection: {
294
+ * rpcUrl: "/api/rpc",
295
+ * },
296
+ * iframeConfig: {
297
+ * iframeContainerId: "alchemy-signer-iframe-container",
298
+ * },
299
+ * },
300
+ * });
301
+ *
302
+ * const mfaStatus = signer.getMfaStatus();
303
+ * if (mfaStatus === AlchemyMfaStatus.REQUIRED) {
304
+ * // Handle MFA requirement
305
+ * }
306
+ * ```
307
+ *
308
+ * @returns {{ mfaRequired: boolean; mfaFactorId?: string }} The current MFA status
309
+ */
310
+ getMfaStatus: () => {
311
+ mfaRequired: boolean;
312
+ mfaFactorId?: string;
313
+ };
284
314
  private unpackSignRawMessageBytes;
285
315
  /**
286
316
  * Unauthenticated call to look up a user's organizationId by email
@@ -421,9 +451,160 @@ export declare abstract class BaseAlchemySigner<TClient extends BaseSignerClient
421
451
  private authenticateWithOauth;
422
452
  private authenticateWithOtp;
423
453
  private handleOauthReturn;
454
+ private handleMfaRequired;
424
455
  private getExpirationSeconds;
425
456
  private registerListeners;
426
457
  private emitNewUserEvent;
458
+ private initOrCreateEmailUser;
459
+ private completeEmailAuth;
460
+ /**
461
+ * Retrieves the list of MFA factors configured for the current user.
462
+ *
463
+ * @example
464
+ * ```ts
465
+ * import { AlchemyWebSigner } from "@account-kit/signer";
466
+ *
467
+ * const signer = new AlchemyWebSigner({
468
+ * client: {
469
+ * connection: {
470
+ * rpcUrl: "/api/rpc",
471
+ * },
472
+ * iframeConfig: {
473
+ * iframeContainerId: "alchemy-signer-iframe-container",
474
+ * },
475
+ * },
476
+ * });
477
+ *
478
+ * const { multiFactors } = await signer.getMfaFactors();
479
+ * ```
480
+ *
481
+ * @throws {NotAuthenticatedError} If no user is authenticated
482
+ * @returns {Promise<{ multiFactors: Array<MfaFactor> }>} A promise that resolves to an array of configured MFA factors
483
+ */
484
+ getMfaFactors: () => Promise<{
485
+ multiFactors: MfaFactor[];
486
+ }>;
487
+ /**
488
+ * Initiates the setup of a new MFA factor for the current user.
489
+ * The factor will need to be verified using verifyMfa before it becomes active.
490
+ *
491
+ * @example
492
+ * ```ts
493
+ * import { AlchemyWebSigner } from "@account-kit/signer";
494
+ *
495
+ * const signer = new AlchemyWebSigner({
496
+ * client: {
497
+ * connection: {
498
+ * rpcUrl: "/api/rpc",
499
+ * },
500
+ * iframeConfig: {
501
+ * iframeContainerId: "alchemy-signer-iframe-container",
502
+ * },
503
+ * },
504
+ * });
505
+ *
506
+ * const result = await signer.addMfa({ multiFactorType: "totp" });
507
+ * // Result contains multiFactorTotpUrl to display as QR code
508
+ * ```
509
+ *
510
+ * @param {EnableMfaParams} params The parameters required to enable a new MFA factor
511
+ * @throws {NotAuthenticatedError} If no user is authenticated
512
+ * @returns {Promise<EnableMfaResult>} A promise that resolves to the factor setup information
513
+ */
514
+ addMfa: (params: EnableMfaParams) => Promise<EnableMfaResult>;
515
+ /**
516
+ * Verifies a newly created MFA factor to complete the setup process.
517
+ *
518
+ * @example
519
+ * ```ts
520
+ * import { AlchemyWebSigner } from "@account-kit/signer";
521
+ *
522
+ * const signer = new AlchemyWebSigner({
523
+ * client: {
524
+ * connection: {
525
+ * rpcUrl: "/api/rpc",
526
+ * },
527
+ * iframeConfig: {
528
+ * iframeContainerId: "alchemy-signer-iframe-container",
529
+ * },
530
+ * },
531
+ * });
532
+ *
533
+ * const result = await signer.verifyMfa({
534
+ * multiFactorId: "factor-id",
535
+ * multiFactorCode: "123456" // 6-digit code from authenticator app
536
+ * });
537
+ * ```
538
+ *
539
+ * @param {VerifyMfaParams} params The parameters required to verify the MFA factor
540
+ * @throws {NotAuthenticatedError} If no user is authenticated
541
+ * @returns {Promise<{ multiFactors: MfaFactor[] }>} A promise that resolves to the updated list of MFA factors
542
+ */
543
+ verifyMfa: (params: VerifyMfaParams) => Promise<{
544
+ multiFactors: MfaFactor[];
545
+ }>;
546
+ /**
547
+ * Removes existing MFA factors by their IDs.
548
+ *
549
+ * @example
550
+ * ```ts
551
+ * import { AlchemyWebSigner } from "@account-kit/signer";
552
+ *
553
+ * const signer = new AlchemyWebSigner({
554
+ * client: {
555
+ * connection: {
556
+ * rpcUrl: "/api/rpc",
557
+ * },
558
+ * iframeConfig: {
559
+ * iframeContainerId: "alchemy-signer-iframe-container",
560
+ * },
561
+ * },
562
+ * });
563
+ *
564
+ * const result = await signer.removeMfa({
565
+ * multiFactorIds: ["factor-id-1", "factor-id-2"]
566
+ * });
567
+ * ```
568
+ *
569
+ * @param {RemoveMfaParams} params The parameters specifying which factors to disable
570
+ * @throws {NotAuthenticatedError} If no user is authenticated
571
+ * @returns {Promise<{ multiFactors: MfaFactor[] }>} A promise that resolves to the updated list of MFA factors
572
+ */
573
+ removeMfa: (params: RemoveMfaParams) => Promise<{
574
+ multiFactors: MfaFactor[];
575
+ }>;
576
+ /**
577
+ * Validates MFA factors that were required during authentication.
578
+ * This function should be called after MFA is required and the user has provided their MFA code.
579
+ * It completes the authentication process by validating the MFA factors and completing the auth bundle.
580
+ *
581
+ * @example
582
+ * ```ts
583
+ * import { AlchemyWebSigner } from "@account-kit/signer";
584
+ *
585
+ * const signer = new AlchemyWebSigner({
586
+ * client: {
587
+ * connection: {
588
+ * rpcUrl: "/api/rpc",
589
+ * },
590
+ * iframeConfig: {
591
+ * iframeContainerId: "alchemy-signer-iframe-container",
592
+ * },
593
+ * },
594
+ * });
595
+ *
596
+ * // After MFA is required and user provides code
597
+ * const user = await signer.validateMultiFactors({
598
+ * multiFactorCode: "123456", // 6-digit code from authenticator app
599
+ * multiFactorId: "factor-id"
600
+ * });
601
+ * ```
602
+ *
603
+ * @param {ValidateMultiFactorsArgs} params - Parameters for validating MFA factors
604
+ * @throws {Error} If there is no pending MFA context or if orgId is not found
605
+ * @returns {Promise<User>} A promise that resolves to the authenticated user
606
+ */
607
+ validateMultiFactors(params: ValidateMultiFactorsArgs): Promise<User>;
427
608
  protected initConfig: () => Promise<SignerConfig>;
428
609
  /**
429
610
  * Returns the signer configuration while fetching it if it's not already initialized.
package/dist/esm/base.js CHANGED
@@ -87,6 +87,8 @@ export class BaseAlchemySigner {
87
87
  if (isNewUser)
88
88
  listener();
89
89
  }, { fireImmediately: true });
90
+ case "mfaStatusChanged":
91
+ return this.store.subscribe(({ mfaStatus }) => mfaStatus, (mfaStatus) => listener(mfaStatus), { fireImmediately: true });
90
92
  default:
91
93
  assertNever(event, `Unknown event type ${event}`);
92
94
  }
@@ -483,6 +485,40 @@ export class BaseAlchemySigner {
483
485
  return { ...unsignedAuthorization, ...signature };
484
486
  })
485
487
  });
488
+ /**
489
+ * Gets the current MFA status
490
+ *
491
+ * @example
492
+ * ```ts
493
+ * import { AlchemyWebSigner } from "@account-kit/signer";
494
+ *
495
+ * const signer = new AlchemyWebSigner({
496
+ * client: {
497
+ * connection: {
498
+ * rpcUrl: "/api/rpc",
499
+ * },
500
+ * iframeConfig: {
501
+ * iframeContainerId: "alchemy-signer-iframe-container",
502
+ * },
503
+ * },
504
+ * });
505
+ *
506
+ * const mfaStatus = signer.getMfaStatus();
507
+ * if (mfaStatus === AlchemyMfaStatus.REQUIRED) {
508
+ * // Handle MFA requirement
509
+ * }
510
+ * ```
511
+ *
512
+ * @returns {{ mfaRequired: boolean; mfaFactorId?: string }} The current MFA status
513
+ */
514
+ Object.defineProperty(this, "getMfaStatus", {
515
+ enumerable: true,
516
+ configurable: true,
517
+ writable: true,
518
+ value: () => {
519
+ return this.store.getState().mfaStatus;
520
+ }
521
+ });
486
522
  Object.defineProperty(this, "unpackSignRawMessageBytes", {
487
523
  enumerable: true,
488
524
  configurable: true,
@@ -691,61 +727,27 @@ export class BaseAlchemySigner {
691
727
  configurable: true,
692
728
  writable: true,
693
729
  value: async (params) => {
694
- if ("email" in params) {
695
- const existingUser = await this.getUser(params.email);
696
- const expirationSeconds = this.getExpirationSeconds();
697
- const { orgId, otpId } = existingUser
698
- ? await this.inner.initEmailAuth({
699
- email: params.email,
700
- emailMode: params.emailMode,
701
- expirationSeconds,
702
- redirectParams: params.redirectParams,
703
- })
704
- : await this.inner.createAccount({
705
- type: "email",
706
- email: params.email,
707
- emailMode: params.emailMode,
708
- expirationSeconds,
709
- redirectParams: params.redirectParams,
710
- });
711
- this.sessionManager.setTemporarySession({
712
- orgId,
713
- isNewUser: !existingUser,
714
- });
715
- this.store.setState({
716
- status: AlchemySignerStatus.AWAITING_EMAIL_AUTH,
717
- otpId,
718
- error: null,
719
- });
720
- // We wait for the session manager to emit a connected event if
721
- // cross tab sessions are permitted
722
- return new Promise((resolve) => {
723
- const removeListener = this.sessionManager.on("connected", (session) => {
724
- resolve(session.user);
725
- removeListener();
726
- });
727
- });
730
+ if ("bundle" in params) {
731
+ return this.completeEmailAuth(params);
728
732
  }
729
- else {
730
- const temporarySession = params.orgId
731
- ? { orgId: params.orgId }
732
- : this.sessionManager.getTemporarySession();
733
- if (!temporarySession) {
734
- this.store.setState({
735
- status: AlchemySignerStatus.DISCONNECTED,
736
- });
737
- throw new Error("Could not find email auth init session!");
738
- }
739
- const user = await this.inner.completeAuthWithBundle({
740
- bundle: params.bundle,
741
- orgId: temporarySession.orgId,
742
- connectedEventName: "connectedEmail",
743
- authenticatingType: "email",
733
+ const { orgId, otpId, isNewUser } = await this.initOrCreateEmailUser(params.email, params.emailMode, params.multiFactors, params.redirectParams);
734
+ this.sessionManager.setTemporarySession({
735
+ orgId,
736
+ isNewUser,
737
+ });
738
+ this.store.setState({
739
+ status: AlchemySignerStatus.AWAITING_EMAIL_AUTH,
740
+ otpId,
741
+ error: null,
742
+ });
743
+ // We wait for the session manager to emit a connected event if
744
+ // cross tab sessions are permitted
745
+ return new Promise((resolve) => {
746
+ const removeListener = this.sessionManager.on("connected", (session) => {
747
+ resolve(session.user);
748
+ removeListener();
744
749
  });
745
- // fire new user event
746
- this.emitNewUserEvent(params.isNewUser);
747
- return user;
748
- }
750
+ });
749
751
  }
750
752
  });
751
753
  Object.defineProperty(this, "authenticateWithPasskey", {
@@ -814,14 +816,24 @@ export class BaseAlchemySigner {
814
816
  if (!otpId) {
815
817
  throw new Error("otpId not found in session");
816
818
  }
817
- const { bundle } = await this.inner.submitOtpCode({
819
+ const response = await this.inner.submitOtpCode({
818
820
  orgId,
819
821
  otpId,
820
822
  otpCode: args.otpCode,
821
823
  expirationSeconds: this.getExpirationSeconds(),
824
+ multiFactors: args.multiFactors,
822
825
  });
826
+ if (response.mfaRequired) {
827
+ this.handleMfaRequired(response.encryptedPayload, response.multiFactors);
828
+ return new Promise((resolve) => {
829
+ const removeListener = this.sessionManager.on("connected", (session) => {
830
+ resolve(session.user);
831
+ removeListener();
832
+ });
833
+ });
834
+ }
823
835
  const user = await this.inner.completeAuthWithBundle({
824
- bundle,
836
+ bundle: response.bundle,
825
837
  orgId,
826
838
  connectedEventName: "connectedOtp",
827
839
  authenticatingType: "otp",
@@ -928,6 +940,144 @@ export class BaseAlchemySigner {
928
940
  this.store.setState({ isNewUser });
929
941
  }
930
942
  });
943
+ /**
944
+ * Retrieves the list of MFA factors configured for the current user.
945
+ *
946
+ * @example
947
+ * ```ts
948
+ * import { AlchemyWebSigner } from "@account-kit/signer";
949
+ *
950
+ * const signer = new AlchemyWebSigner({
951
+ * client: {
952
+ * connection: {
953
+ * rpcUrl: "/api/rpc",
954
+ * },
955
+ * iframeConfig: {
956
+ * iframeContainerId: "alchemy-signer-iframe-container",
957
+ * },
958
+ * },
959
+ * });
960
+ *
961
+ * const { multiFactors } = await signer.getMfaFactors();
962
+ * ```
963
+ *
964
+ * @throws {NotAuthenticatedError} If no user is authenticated
965
+ * @returns {Promise<{ multiFactors: Array<MfaFactor> }>} A promise that resolves to an array of configured MFA factors
966
+ */
967
+ Object.defineProperty(this, "getMfaFactors", {
968
+ enumerable: true,
969
+ configurable: true,
970
+ writable: true,
971
+ value: SignerLogger.profiled("BaseAlchemySigner.getMfaFactors", async () => {
972
+ return this.inner.getMfaFactors();
973
+ })
974
+ });
975
+ /**
976
+ * Initiates the setup of a new MFA factor for the current user.
977
+ * The factor will need to be verified using verifyMfa before it becomes active.
978
+ *
979
+ * @example
980
+ * ```ts
981
+ * import { AlchemyWebSigner } from "@account-kit/signer";
982
+ *
983
+ * const signer = new AlchemyWebSigner({
984
+ * client: {
985
+ * connection: {
986
+ * rpcUrl: "/api/rpc",
987
+ * },
988
+ * iframeConfig: {
989
+ * iframeContainerId: "alchemy-signer-iframe-container",
990
+ * },
991
+ * },
992
+ * });
993
+ *
994
+ * const result = await signer.addMfa({ multiFactorType: "totp" });
995
+ * // Result contains multiFactorTotpUrl to display as QR code
996
+ * ```
997
+ *
998
+ * @param {EnableMfaParams} params The parameters required to enable a new MFA factor
999
+ * @throws {NotAuthenticatedError} If no user is authenticated
1000
+ * @returns {Promise<EnableMfaResult>} A promise that resolves to the factor setup information
1001
+ */
1002
+ Object.defineProperty(this, "addMfa", {
1003
+ enumerable: true,
1004
+ configurable: true,
1005
+ writable: true,
1006
+ value: SignerLogger.profiled("BaseAlchemySigner.addMfa", async (params) => {
1007
+ return this.inner.addMfa(params);
1008
+ })
1009
+ });
1010
+ /**
1011
+ * Verifies a newly created MFA factor to complete the setup process.
1012
+ *
1013
+ * @example
1014
+ * ```ts
1015
+ * import { AlchemyWebSigner } from "@account-kit/signer";
1016
+ *
1017
+ * const signer = new AlchemyWebSigner({
1018
+ * client: {
1019
+ * connection: {
1020
+ * rpcUrl: "/api/rpc",
1021
+ * },
1022
+ * iframeConfig: {
1023
+ * iframeContainerId: "alchemy-signer-iframe-container",
1024
+ * },
1025
+ * },
1026
+ * });
1027
+ *
1028
+ * const result = await signer.verifyMfa({
1029
+ * multiFactorId: "factor-id",
1030
+ * multiFactorCode: "123456" // 6-digit code from authenticator app
1031
+ * });
1032
+ * ```
1033
+ *
1034
+ * @param {VerifyMfaParams} params The parameters required to verify the MFA factor
1035
+ * @throws {NotAuthenticatedError} If no user is authenticated
1036
+ * @returns {Promise<{ multiFactors: MfaFactor[] }>} A promise that resolves to the updated list of MFA factors
1037
+ */
1038
+ Object.defineProperty(this, "verifyMfa", {
1039
+ enumerable: true,
1040
+ configurable: true,
1041
+ writable: true,
1042
+ value: SignerLogger.profiled("BaseAlchemySigner.verifyMfa", async (params) => {
1043
+ return this.inner.verifyMfa(params);
1044
+ })
1045
+ });
1046
+ /**
1047
+ * Removes existing MFA factors by their IDs.
1048
+ *
1049
+ * @example
1050
+ * ```ts
1051
+ * import { AlchemyWebSigner } from "@account-kit/signer";
1052
+ *
1053
+ * const signer = new AlchemyWebSigner({
1054
+ * client: {
1055
+ * connection: {
1056
+ * rpcUrl: "/api/rpc",
1057
+ * },
1058
+ * iframeConfig: {
1059
+ * iframeContainerId: "alchemy-signer-iframe-container",
1060
+ * },
1061
+ * },
1062
+ * });
1063
+ *
1064
+ * const result = await signer.removeMfa({
1065
+ * multiFactorIds: ["factor-id-1", "factor-id-2"]
1066
+ * });
1067
+ * ```
1068
+ *
1069
+ * @param {RemoveMfaParams} params The parameters specifying which factors to disable
1070
+ * @throws {NotAuthenticatedError} If no user is authenticated
1071
+ * @returns {Promise<{ multiFactors: MfaFactor[] }>} A promise that resolves to the updated list of MFA factors
1072
+ */
1073
+ Object.defineProperty(this, "removeMfa", {
1074
+ enumerable: true,
1075
+ configurable: true,
1076
+ writable: true,
1077
+ value: SignerLogger.profiled("BaseAlchemySigner.removeMfa", async (params) => {
1078
+ return this.inner.removeMfa(params);
1079
+ })
1080
+ });
931
1081
  Object.defineProperty(this, "initConfig", {
932
1082
  enumerable: true,
933
1083
  configurable: true,
@@ -966,6 +1116,10 @@ export class BaseAlchemySigner {
966
1116
  user: null,
967
1117
  status: AlchemySignerStatus.INITIALIZING,
968
1118
  error: initialError ?? null,
1119
+ mfaStatus: {
1120
+ mfaRequired: false,
1121
+ mfaFactorId: undefined,
1122
+ },
969
1123
  })));
970
1124
  // NOTE: it's important that the session manager share a client
971
1125
  // with the signer. The SessionManager leverages the Signer's client
@@ -980,6 +1134,149 @@ export class BaseAlchemySigner {
980
1134
  this.sessionManager.initialize();
981
1135
  this.config = this.fetchConfig();
982
1136
  }
1137
+ handleMfaRequired(encryptedPayload, multiFactors) {
1138
+ // Store complete MFA context in the temporary session
1139
+ const tempSession = this.sessionManager.getTemporarySession();
1140
+ if (tempSession) {
1141
+ this.sessionManager.setTemporarySession({
1142
+ ...tempSession,
1143
+ encryptedPayload,
1144
+ mfaFactorId: multiFactors?.[0]?.multiFactorId,
1145
+ });
1146
+ }
1147
+ // Keep minimal state in the store for UI updates
1148
+ this.store.setState({
1149
+ status: AlchemySignerStatus.AWAITING_MFA_AUTH,
1150
+ error: null,
1151
+ mfaStatus: {
1152
+ mfaRequired: true,
1153
+ mfaFactorId: multiFactors?.[0]?.multiFactorId,
1154
+ },
1155
+ });
1156
+ }
1157
+ async initOrCreateEmailUser(email, emailMode, multiFactors, redirectParams) {
1158
+ const existingUser = await this.getUser(email);
1159
+ const expirationSeconds = this.getExpirationSeconds();
1160
+ if (existingUser) {
1161
+ const { orgId, otpId } = await this.inner.initEmailAuth({
1162
+ email: email,
1163
+ emailMode: emailMode,
1164
+ expirationSeconds,
1165
+ redirectParams: redirectParams,
1166
+ multiFactors,
1167
+ });
1168
+ return {
1169
+ orgId,
1170
+ otpId,
1171
+ isNewUser: false,
1172
+ };
1173
+ }
1174
+ const { orgId, otpId } = await this.inner.createAccount({
1175
+ type: "email",
1176
+ email,
1177
+ emailMode,
1178
+ expirationSeconds,
1179
+ redirectParams,
1180
+ });
1181
+ return {
1182
+ orgId,
1183
+ otpId,
1184
+ isNewUser: true,
1185
+ };
1186
+ }
1187
+ async completeEmailAuth(params) {
1188
+ const temporarySession = params.orgId
1189
+ ? { orgId: params.orgId }
1190
+ : this.sessionManager.getTemporarySession();
1191
+ if (!temporarySession) {
1192
+ this.store.setState({ status: AlchemySignerStatus.DISCONNECTED });
1193
+ throw new Error("Could not find email auth init session!");
1194
+ }
1195
+ const user = await this.inner.completeAuthWithBundle({
1196
+ bundle: params.bundle,
1197
+ orgId: temporarySession.orgId,
1198
+ connectedEventName: "connectedEmail",
1199
+ authenticatingType: "email",
1200
+ });
1201
+ // fire new user event
1202
+ this.emitNewUserEvent(params.isNewUser);
1203
+ return user;
1204
+ }
1205
+ /**
1206
+ * Validates MFA factors that were required during authentication.
1207
+ * This function should be called after MFA is required and the user has provided their MFA code.
1208
+ * It completes the authentication process by validating the MFA factors and completing the auth bundle.
1209
+ *
1210
+ * @example
1211
+ * ```ts
1212
+ * import { AlchemyWebSigner } from "@account-kit/signer";
1213
+ *
1214
+ * const signer = new AlchemyWebSigner({
1215
+ * client: {
1216
+ * connection: {
1217
+ * rpcUrl: "/api/rpc",
1218
+ * },
1219
+ * iframeConfig: {
1220
+ * iframeContainerId: "alchemy-signer-iframe-container",
1221
+ * },
1222
+ * },
1223
+ * });
1224
+ *
1225
+ * // After MFA is required and user provides code
1226
+ * const user = await signer.validateMultiFactors({
1227
+ * multiFactorCode: "123456", // 6-digit code from authenticator app
1228
+ * multiFactorId: "factor-id"
1229
+ * });
1230
+ * ```
1231
+ *
1232
+ * @param {ValidateMultiFactorsArgs} params - Parameters for validating MFA factors
1233
+ * @throws {Error} If there is no pending MFA context or if orgId is not found
1234
+ * @returns {Promise<User>} A promise that resolves to the authenticated user
1235
+ */
1236
+ async validateMultiFactors(params) {
1237
+ // Get MFA context from temporary session
1238
+ const tempSession = this.sessionManager.getTemporarySession();
1239
+ if (!tempSession?.orgId ||
1240
+ !tempSession.encryptedPayload ||
1241
+ !tempSession.mfaFactorId) {
1242
+ throw new Error("No pending MFA context found. Call submitOtpCode() first.");
1243
+ }
1244
+ if (params.multiFactorId &&
1245
+ tempSession.mfaFactorId !== params.multiFactorId) {
1246
+ throw new Error("MFA factor ID mismatch");
1247
+ }
1248
+ // Call the low-level client
1249
+ const { bundle } = await this.inner.validateMultiFactors({
1250
+ encryptedPayload: tempSession.encryptedPayload,
1251
+ multiFactors: [
1252
+ {
1253
+ multiFactorId: tempSession.mfaFactorId,
1254
+ multiFactorCode: params.multiFactorCode,
1255
+ },
1256
+ ],
1257
+ });
1258
+ // Complete the authentication
1259
+ const user = await this.inner.completeAuthWithBundle({
1260
+ bundle,
1261
+ orgId: tempSession.orgId,
1262
+ connectedEventName: "connectedOtp",
1263
+ authenticatingType: "otp",
1264
+ });
1265
+ // Remove MFA data from temporary session
1266
+ this.sessionManager.setTemporarySession({
1267
+ ...tempSession,
1268
+ encryptedPayload: undefined,
1269
+ mfaFactorId: undefined,
1270
+ });
1271
+ // Update UI state
1272
+ this.store.setState({
1273
+ mfaStatus: {
1274
+ mfaRequired: false,
1275
+ mfaFactorId: undefined,
1276
+ },
1277
+ });
1278
+ return user;
1279
+ }
983
1280
  }
984
1281
  function toErrorInfo(error) {
985
1282
  return error instanceof Error