@account-kit/signer 4.16.0 → 4.16.1-alpha.3

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 (63) hide show
  1. package/dist/esm/base.d.ts +32 -0
  2. package/dist/esm/base.js +122 -54
  3. package/dist/esm/base.js.map +1 -1
  4. package/dist/esm/client/base.d.ts +36 -2
  5. package/dist/esm/client/base.js.map +1 -1
  6. package/dist/esm/client/index.d.ts +40 -1
  7. package/dist/esm/client/index.js +164 -7
  8. package/dist/esm/client/index.js.map +1 -1
  9. package/dist/esm/client/types.d.ts +68 -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 +1 -0
  18. package/dist/esm/session/manager.js.map +1 -1
  19. package/dist/esm/signer.d.ts +3 -1
  20. package/dist/esm/signer.js.map +1 -1
  21. package/dist/esm/types.d.ts +8 -0
  22. package/dist/esm/types.js +5 -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 +32 -0
  31. package/dist/types/base.d.ts.map +1 -1
  32. package/dist/types/client/base.d.ts +36 -2
  33. package/dist/types/client/base.d.ts.map +1 -1
  34. package/dist/types/client/index.d.ts +40 -1
  35. package/dist/types/client/index.d.ts.map +1 -1
  36. package/dist/types/client/types.d.ts +68 -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 +1 -0
  43. package/dist/types/session/manager.d.ts.map +1 -1
  44. package/dist/types/signer.d.ts +3 -1
  45. package/dist/types/signer.d.ts.map +1 -1
  46. package/dist/types/types.d.ts +8 -0
  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/dist/types/version.d.ts.map +1 -1
  52. package/package.json +4 -4
  53. package/src/base.ts +171 -60
  54. package/src/client/base.ts +43 -1
  55. package/src/client/index.ts +174 -7
  56. package/src/client/types.ts +78 -1
  57. package/src/errors.ts +11 -1
  58. package/src/index.ts +5 -1
  59. package/src/session/manager.ts +5 -1
  60. package/src/signer.ts +6 -1
  61. package/src/types.ts +9 -0
  62. package/src/utils/parseMfaError.ts +15 -0
  63. package/src/version.ts +1 -1
package/src/base.ts CHANGED
@@ -21,7 +21,14 @@ import type { Mutate, StoreApi } from "zustand";
21
21
  import { subscribeWithSelector } from "zustand/middleware";
22
22
  import { createStore } from "zustand/vanilla";
23
23
  import type { BaseSignerClient } from "./client/base";
24
- import type { OauthConfig, OauthParams, User } from "./client/types";
24
+ import type {
25
+ EmailType,
26
+ MfaFactor,
27
+ OauthConfig,
28
+ OauthParams,
29
+ User,
30
+ VerifyMfaParams,
31
+ } from "./client/types";
25
32
  import { NotAuthenticatedError } from "./errors.js";
26
33
  import { SignerLogger } from "./metrics.js";
27
34
  import {
@@ -51,6 +58,10 @@ type AlchemySignerStore = {
51
58
  error: ErrorInfo | null;
52
59
  otpId?: string;
53
60
  isNewUser?: boolean;
61
+ mfaStatus: {
62
+ mfaRequired: boolean;
63
+ mfaFactorId?: string;
64
+ };
54
65
  };
55
66
 
56
67
  type UnpackedSignature = {
@@ -99,6 +110,10 @@ export abstract class BaseAlchemySigner<TClient extends BaseSignerClient>
99
110
  user: null,
100
111
  status: AlchemySignerStatus.INITIALIZING,
101
112
  error: initialError ?? null,
113
+ mfaStatus: {
114
+ mfaRequired: false,
115
+ mfaFactorId: undefined,
116
+ },
102
117
  } satisfies AlchemySignerStore)
103
118
  )
104
119
  );
@@ -171,6 +186,13 @@ export abstract class BaseAlchemySigner<TClient extends BaseSignerClient>
171
186
  },
172
187
  { fireImmediately: true }
173
188
  );
189
+ case "mfaStatusChanged":
190
+ return this.store.subscribe(
191
+ ({ mfaStatus }) => mfaStatus,
192
+ (mfaStatus) =>
193
+ (listener as AlchemySignerEvents["mfaStatusChanged"])(mfaStatus),
194
+ { fireImmediately: true }
195
+ );
174
196
  default:
175
197
  assertNever(event, `Unknown event type ${event}`);
176
198
  }
@@ -580,6 +602,39 @@ export abstract class BaseAlchemySigner<TClient extends BaseSignerClient>
580
602
  }
581
603
  );
582
604
 
605
+ /**
606
+ * Gets the current MFA status
607
+ *
608
+ * @example
609
+ * ```ts
610
+ * import { AlchemyWebSigner } from "@account-kit/signer";
611
+ *
612
+ * const signer = new AlchemyWebSigner({
613
+ * client: {
614
+ * connection: {
615
+ * rpcUrl: "/api/rpc",
616
+ * },
617
+ * iframeConfig: {
618
+ * iframeContainerId: "alchemy-signer-iframe-container",
619
+ * },
620
+ * },
621
+ * });
622
+ *
623
+ * const mfaStatus = signer.getMfaStatus();
624
+ * if (mfaStatus === AlchemyMfaStatus.REQUIRED) {
625
+ * // Handle MFA requirement
626
+ * }
627
+ * ```
628
+ *
629
+ * @returns {{ mfaRequired: boolean; mfaFactorId?: string }} The current MFA status
630
+ */
631
+ getMfaStatus = (): {
632
+ mfaRequired: boolean;
633
+ mfaFactorId?: string;
634
+ } => {
635
+ return this.store.getState().mfaStatus;
636
+ };
637
+
583
638
  private unpackSignRawMessageBytes = (
584
639
  hex: `0x${string}`
585
640
  ): UnpackedSignature => {
@@ -769,70 +824,48 @@ export abstract class BaseAlchemySigner<TClient extends BaseSignerClient>
769
824
  private authenticateWithEmail = async (
770
825
  params: Extract<AuthParams, { type: "email" }>
771
826
  ): Promise<User> => {
772
- if ("email" in params) {
773
- const existingUser = await this.getUser(params.email);
774
- const expirationSeconds = this.getExpirationSeconds();
775
-
776
- const { orgId, otpId } = existingUser
777
- ? await this.inner.initEmailAuth({
778
- email: params.email,
779
- emailMode: params.emailMode,
780
- expirationSeconds,
781
- redirectParams: params.redirectParams,
782
- })
783
- : await this.inner.createAccount({
784
- type: "email",
785
- email: params.email,
786
- emailMode: params.emailMode,
787
- expirationSeconds,
788
- redirectParams: params.redirectParams,
789
- });
827
+ if ("bundle" in params) {
828
+ return this.completeEmailAuth(params);
829
+ }
790
830
 
791
- this.sessionManager.setTemporarySession({
792
- orgId,
793
- isNewUser: !existingUser,
794
- });
795
- this.store.setState({
796
- status: AlchemySignerStatus.AWAITING_EMAIL_AUTH,
797
- otpId,
798
- error: null,
799
- });
831
+ if (!("email" in params)) {
832
+ throw new Error("Email is required");
833
+ }
800
834
 
801
- // We wait for the session manager to emit a connected event if
802
- // cross tab sessions are permitted
803
- return new Promise<User>((resolve) => {
804
- const removeListener = this.sessionManager.on(
805
- "connected",
806
- (session) => {
807
- resolve(session.user);
808
- removeListener();
809
- }
810
- );
811
- });
812
- } else {
813
- const temporarySession = params.orgId
814
- ? { orgId: params.orgId }
815
- : this.sessionManager.getTemporarySession();
835
+ const { orgId, otpId, multiFactors, isNewUser } =
836
+ await this.initOrCreateEmailUser(
837
+ params.email,
838
+ params.emailMode ?? "otp",
839
+ params.multiFactors,
840
+ params.redirectParams
841
+ );
816
842
 
817
- if (!temporarySession) {
818
- this.store.setState({
819
- status: AlchemySignerStatus.DISCONNECTED,
820
- });
821
- throw new Error("Could not find email auth init session!");
822
- }
843
+ const isMfaRequired = multiFactors ? multiFactors?.length > 0 : false;
823
844
 
824
- const user = await this.inner.completeAuthWithBundle({
825
- bundle: params.bundle,
826
- orgId: temporarySession.orgId,
827
- connectedEventName: "connectedEmail",
828
- authenticatingType: "email",
829
- });
845
+ this.sessionManager.setTemporarySession({
846
+ orgId,
847
+ isNewUser,
848
+ isMfaRequired,
849
+ });
830
850
 
831
- // fire new user event
832
- this.emitNewUserEvent(params.isNewUser);
851
+ this.store.setState({
852
+ status: AlchemySignerStatus.AWAITING_EMAIL_AUTH,
853
+ otpId,
854
+ error: null,
855
+ mfaStatus: {
856
+ mfaRequired: isMfaRequired,
857
+ mfaFactorId: multiFactors?.[0]?.multiFactorId,
858
+ },
859
+ });
833
860
 
834
- return user;
835
- }
861
+ // We wait for the session manager to emit a connected event if
862
+ // cross tab sessions are permitted
863
+ return new Promise<User>((resolve) => {
864
+ const removeListener = this.sessionManager.on("connected", (session) => {
865
+ resolve(session.user);
866
+ removeListener();
867
+ });
868
+ });
836
869
  };
837
870
 
838
871
  private authenticateWithPasskey = async (
@@ -893,7 +926,7 @@ export abstract class BaseAlchemySigner<TClient extends BaseSignerClient>
893
926
  args: Extract<AuthParams, { type: "otp" }>
894
927
  ): Promise<User> => {
895
928
  const tempSession = this.sessionManager.getTemporarySession();
896
- const { orgId, isNewUser } = tempSession ?? {};
929
+ const { orgId, isNewUser, isMfaRequired } = tempSession ?? {};
897
930
  const { otpId } = this.store.getState();
898
931
  if (!orgId) {
899
932
  throw new Error("orgId not found in session");
@@ -901,11 +934,16 @@ export abstract class BaseAlchemySigner<TClient extends BaseSignerClient>
901
934
  if (!otpId) {
902
935
  throw new Error("otpId not found in session");
903
936
  }
937
+ if (isMfaRequired && !args.multiFactors) {
938
+ throw new Error(`MFA is required.`);
939
+ }
940
+
904
941
  const { bundle } = await this.inner.submitOtpCode({
905
942
  orgId,
906
943
  otpId,
907
944
  otpCode: args.otpCode,
908
945
  expirationSeconds: this.getExpirationSeconds(),
946
+ multiFactors: args.multiFactors,
909
947
  });
910
948
  const user = await this.inner.completeAuthWithBundle({
911
949
  bundle,
@@ -1011,6 +1049,79 @@ export abstract class BaseAlchemySigner<TClient extends BaseSignerClient>
1011
1049
  // assumes that if isNewUser is undefined it is a returning user
1012
1050
  if (isNewUser) this.store.setState({ isNewUser });
1013
1051
  };
1052
+
1053
+ private async initOrCreateEmailUser(
1054
+ email: string,
1055
+ emailMode: EmailType,
1056
+ multiFactors?: VerifyMfaParams[],
1057
+ redirectParams?: URLSearchParams
1058
+ ): Promise<{
1059
+ orgId: string;
1060
+ otpId?: string;
1061
+ multiFactors?: MfaFactor[];
1062
+ isNewUser: boolean;
1063
+ }> {
1064
+ const existingUser = await this.getUser(email);
1065
+ const expirationSeconds = this.getExpirationSeconds();
1066
+
1067
+ if (existingUser) {
1068
+ const {
1069
+ orgId,
1070
+ otpId,
1071
+ multiFactors: mfaFactors,
1072
+ } = await this.inner.initEmailAuth({
1073
+ email: email,
1074
+ emailMode: emailMode,
1075
+ expirationSeconds,
1076
+ redirectParams: redirectParams,
1077
+ multiFactors,
1078
+ });
1079
+ return {
1080
+ orgId,
1081
+ otpId,
1082
+ multiFactors: mfaFactors,
1083
+ isNewUser: false,
1084
+ };
1085
+ }
1086
+
1087
+ const { orgId, otpId } = await this.inner.createAccount({
1088
+ type: "email",
1089
+ email,
1090
+ emailMode,
1091
+ expirationSeconds,
1092
+ redirectParams,
1093
+ });
1094
+ return {
1095
+ orgId,
1096
+ otpId,
1097
+ isNewUser: true,
1098
+ };
1099
+ }
1100
+
1101
+ private async completeEmailAuth(
1102
+ params: Extract<AuthParams, { type: "email"; bundle: string }>
1103
+ ): Promise<User> {
1104
+ const temporarySession = params.orgId
1105
+ ? { orgId: params.orgId }
1106
+ : this.sessionManager.getTemporarySession();
1107
+
1108
+ if (!temporarySession) {
1109
+ this.store.setState({ status: AlchemySignerStatus.DISCONNECTED });
1110
+ throw new Error("Could not find email auth init session!");
1111
+ }
1112
+
1113
+ const user = await this.inner.completeAuthWithBundle({
1114
+ bundle: params.bundle,
1115
+ orgId: temporarySession.orgId,
1116
+ connectedEventName: "connectedEmail",
1117
+ authenticatingType: "email",
1118
+ });
1119
+
1120
+ // fire new user event
1121
+ this.emitNewUserEvent(params.isNewUser);
1122
+
1123
+ return user;
1124
+ }
1014
1125
  }
1015
1126
 
1016
1127
  function toErrorInfo(error: unknown): ErrorInfo {
@@ -14,9 +14,13 @@ import type {
14
14
  AlchemySignerClientEvents,
15
15
  AuthenticatingEventMetadata,
16
16
  CreateAccountParams,
17
+ RemoveMfaParams,
17
18
  EmailAuthParams,
19
+ EnableMfaParams,
20
+ EnableMfaResult,
18
21
  GetOauthProviderUrlArgs,
19
22
  GetWebAuthnAttestationResult,
23
+ MfaFactor,
20
24
  OauthConfig,
21
25
  OauthParams,
22
26
  OauthState,
@@ -26,6 +30,7 @@ import type {
26
30
  SignerRoutes,
27
31
  SignupResponse,
28
32
  User,
33
+ VerifyMfaParams,
29
34
  } from "./types.js";
30
35
 
31
36
  export interface BaseSignerClientParams {
@@ -131,7 +136,44 @@ export abstract class BaseSignerClient<TExportWalletParams = unknown> {
131
136
 
132
137
  public abstract initEmailAuth(
133
138
  params: Omit<EmailAuthParams, "targetPublicKey">
134
- ): Promise<{ orgId: string; otpId?: string }>;
139
+ ): Promise<{ orgId: string; otpId?: string; multiFactors?: MfaFactor[] }>;
140
+
141
+ /**
142
+ * Retrieves the list of MFA factors configured for the current user.
143
+ *
144
+ * @returns {Promise<{ multiFactors: Array<MfaFactor> }>} A promise that resolves to an array of configured MFA factors
145
+ */
146
+ public abstract getMfaFactors(): Promise<{
147
+ multiFactors: MfaFactor[];
148
+ }>;
149
+
150
+ /**
151
+ * Initiates the setup of a new MFA factor for the current user. Mfa will need to be verified before it is active.
152
+ *
153
+ * @param {EnableMfaParams} params The parameters required to enable a new MFA factor
154
+ * @returns {Promise<EnableMfaResult>} A promise that resolves to the factor setup information
155
+ */
156
+ public abstract addMfa(params: EnableMfaParams): Promise<EnableMfaResult>;
157
+
158
+ /**
159
+ * Verifies a newly created MFA factor to complete the setup process.
160
+ *
161
+ * @param {VerifyMfaParams} params The parameters required to verify the MFA factor
162
+ * @returns {Promise<{ multiFactors: MfaFactor[] }>} A promise that resolves to the updated list of MFA factors
163
+ */
164
+ public abstract verifyMfa(params: VerifyMfaParams): Promise<{
165
+ multiFactors: MfaFactor[];
166
+ }>;
167
+
168
+ /**
169
+ * Removes existing MFA factors by ID or factor type.
170
+ *
171
+ * @param {RemoveMfaParams} params The parameters specifying which factors to disable
172
+ * @returns {Promise<{ multiFactors: MfaFactor[] }>} A promise that resolves to the updated list of MFA factors
173
+ */
174
+ public abstract removeMfa(params: RemoveMfaParams): Promise<{
175
+ multiFactors: MfaFactor[];
176
+ }>;
135
177
 
136
178
  public abstract completeAuthWithBundle(params: {
137
179
  bundle: string;
@@ -17,9 +17,23 @@ import type {
17
17
  OauthConfig,
18
18
  OtpParams,
19
19
  User,
20
+ MfaFactor,
21
+ EnableMfaParams,
22
+ EnableMfaResult,
23
+ VerifyMfaParams,
24
+ RemoveMfaParams,
20
25
  } from "./types.js";
26
+ import { MfaRequiredError, NotAuthenticatedError } from "../errors.js";
27
+ import { parseMfaError } from "../utils/parseMfaError.js";
21
28
 
22
29
  const CHECK_CLOSE_INTERVAL = 500;
30
+ const MFA_PAYLOAD = {
31
+ GET: "get_mfa",
32
+ ADD: "add_mfa",
33
+ DELETE: "delete_mfas",
34
+ VERIFY: "verify_mfa",
35
+ LIST: "list_mfas",
36
+ };
23
37
 
24
38
  export const AlchemySignerClientParamsSchema = z.object({
25
39
  connection: ConnectionConfigSchema,
@@ -198,13 +212,25 @@ export class AlchemySignerWebClient extends BaseSignerClient<ExportWalletParams>
198
212
  const { email, emailMode, expirationSeconds } = params;
199
213
  const publicKey = await this.initIframeStamper();
200
214
 
201
- return this.request("/v1/auth", {
202
- email,
203
- emailMode,
204
- targetPublicKey: publicKey,
205
- expirationSeconds,
206
- redirectParams: params.redirectParams?.toString(),
207
- });
215
+ try {
216
+ return await this.request("/v1/auth", {
217
+ email,
218
+ emailMode,
219
+ targetPublicKey: publicKey,
220
+ expirationSeconds,
221
+ redirectParams: params.redirectParams?.toString(),
222
+ multiFactors: params.multiFactors,
223
+ });
224
+ } catch (error) {
225
+ const multiFactors = parseMfaError(error);
226
+
227
+ // If MFA is required, and emailMode is Magic Link, the user must submit mfa with the request or
228
+ // the the server will return an error with the required mfa factors.
229
+ if (multiFactors) {
230
+ throw new MfaRequiredError(multiFactors);
231
+ }
232
+ throw error;
233
+ }
208
234
  };
209
235
 
210
236
  /**
@@ -242,6 +268,12 @@ export class AlchemySignerWebClient extends BaseSignerClient<ExportWalletParams>
242
268
  ...args,
243
269
  targetPublicKey,
244
270
  });
271
+
272
+ if (!credentialBundle) {
273
+ throw new Error(
274
+ "Failed to submit OTP code. Check if multiFactor is required."
275
+ );
276
+ }
245
277
  return { bundle: credentialBundle };
246
278
  }
247
279
 
@@ -670,6 +702,141 @@ export class AlchemySignerWebClient extends BaseSignerClient<ExportWalletParams>
670
702
  const nonce = this.getOauthNonce(publicKey);
671
703
  return this.request("/v1/prepare-oauth", { nonce });
672
704
  };
705
+
706
+ /**
707
+ * Retrieves the list of MFA factors configured for the current user.
708
+ *
709
+ * @returns {Promise<{ multiFactors: MfaFactor[] }>} A promise that resolves to an array of configured MFA factors
710
+ * @throws {NotAuthenticatedError} If no user is authenticated
711
+ */
712
+ public override getMfaFactors = async (): Promise<{
713
+ multiFactors: MfaFactor[];
714
+ }> => {
715
+ if (!this.user) {
716
+ throw new NotAuthenticatedError();
717
+ }
718
+
719
+ const stampedRequest = await this.turnkeyClient.stampSignRawPayload({
720
+ organizationId: this.user.orgId,
721
+ type: "ACTIVITY_TYPE_SIGN_RAW_PAYLOAD_V2",
722
+ timestampMs: Date.now().toString(),
723
+ parameters: {
724
+ encoding: "PAYLOAD_ENCODING_HEXADECIMAL",
725
+ hashFunction: "HASH_FUNCTION_NO_OP",
726
+ payload: MFA_PAYLOAD.LIST,
727
+ signWith: this.user.address,
728
+ },
729
+ });
730
+
731
+ return this.request("/v1/auth-list-multi-factors", {
732
+ stampedRequest,
733
+ });
734
+ };
735
+
736
+ /**
737
+ * Initiates the setup of a new MFA factor for the current user. Mfa will need to be verified before it is active.
738
+ *
739
+ * @param {EnableMfaParams} params The parameters required to enable a new MFA factor
740
+ * @returns {Promise<EnableMfaResult>} A promise that resolves to the factor setup information
741
+ * @throws {NotAuthenticatedError} If no user is authenticated
742
+ * @throws {Error} If an unsupported factor type is provided
743
+ */
744
+ public override addMfa = async (
745
+ params: EnableMfaParams
746
+ ): Promise<EnableMfaResult> => {
747
+ if (!this.user) {
748
+ throw new NotAuthenticatedError();
749
+ }
750
+
751
+ const stampedRequest = await this.turnkeyClient.stampSignRawPayload({
752
+ organizationId: this.user.orgId,
753
+ type: "ACTIVITY_TYPE_SIGN_RAW_PAYLOAD_V2",
754
+ timestampMs: Date.now().toString(),
755
+ parameters: {
756
+ encoding: "PAYLOAD_ENCODING_HEXADECIMAL",
757
+ hashFunction: "HASH_FUNCTION_NO_OP",
758
+ payload: MFA_PAYLOAD.ADD,
759
+ signWith: this.user.address,
760
+ },
761
+ });
762
+
763
+ switch (params.multiFactorType) {
764
+ case "totp":
765
+ return this.request("/v1/auth-request-multi-factor", {
766
+ stampedRequest,
767
+ multiFactorType: params.multiFactorType,
768
+ });
769
+ default:
770
+ throw new Error(
771
+ `Unsupported MFA factor type: ${params.multiFactorType}`
772
+ );
773
+ }
774
+ };
775
+
776
+ /**
777
+ * Verifies a newly created MFA factor to complete the setup process.
778
+ *
779
+ * @param {VerifyMfaParams} params The parameters required to verify the MFA factor
780
+ * @returns {Promise<{ multiFactors: MfaFactor[] }>} A promise that resolves to the updated list of MFA factors
781
+ * @throws {NotAuthenticatedError} If no user is authenticated
782
+ */
783
+ public override verifyMfa = async (
784
+ params: VerifyMfaParams
785
+ ): Promise<{ multiFactors: MfaFactor[] }> => {
786
+ if (!this.user) {
787
+ throw new NotAuthenticatedError();
788
+ }
789
+
790
+ const stampedRequest = await this.turnkeyClient.stampSignRawPayload({
791
+ organizationId: this.user.orgId,
792
+ type: "ACTIVITY_TYPE_SIGN_RAW_PAYLOAD_V2",
793
+ timestampMs: Date.now().toString(),
794
+ parameters: {
795
+ encoding: "PAYLOAD_ENCODING_HEXADECIMAL",
796
+ hashFunction: "HASH_FUNCTION_NO_OP",
797
+ payload: MFA_PAYLOAD.VERIFY,
798
+ signWith: this.user.address,
799
+ },
800
+ });
801
+
802
+ return this.request("/v1/auth-verify-multi-factor", {
803
+ stampedRequest,
804
+ multiFactorId: params.multiFactorId,
805
+ multiFactorCode: params.multiFactorCode,
806
+ });
807
+ };
808
+
809
+ /**
810
+ * Removes existing MFA factors by ID.
811
+ *
812
+ * @param {RemoveMfaParams} params The parameters specifying which factors to disable
813
+ * @returns {Promise<{ multiFactors: MfaFactor[] }>} A promise that resolves to the updated list of MFA factors
814
+ * @throws {NotAuthenticatedError} If no user is authenticated
815
+ */
816
+ public override removeMfa = async (
817
+ params: RemoveMfaParams
818
+ ): Promise<{ multiFactors: MfaFactor[] }> => {
819
+ if (!this.user) {
820
+ throw new NotAuthenticatedError();
821
+ }
822
+
823
+ const stampedRequest = await this.turnkeyClient.stampSignRawPayload({
824
+ organizationId: this.user.orgId,
825
+ type: "ACTIVITY_TYPE_SIGN_RAW_PAYLOAD_V2",
826
+ timestampMs: Date.now().toString(),
827
+ parameters: {
828
+ encoding: "PAYLOAD_ENCODING_HEXADECIMAL",
829
+ hashFunction: "HASH_FUNCTION_NO_OP",
830
+ payload: MFA_PAYLOAD.DELETE,
831
+ signWith: this.user.address,
832
+ },
833
+ });
834
+
835
+ return this.request("/v1/auth-delete-multi-factors", {
836
+ stampedRequest,
837
+ multiFactorIds: params.multiFactorIds,
838
+ });
839
+ };
673
840
  }
674
841
 
675
842
  /**
@@ -52,6 +52,7 @@ export type EmailAuthParams = {
52
52
  expirationSeconds?: number;
53
53
  targetPublicKey: string;
54
54
  redirectParams?: URLSearchParams;
55
+ multiFactors?: VerifyMfaParams[];
55
56
  };
56
57
 
57
58
  export type OauthParams = Extract<AuthParams, { type: "oauth" }> & {
@@ -64,6 +65,7 @@ export type OtpParams = {
64
65
  otpCode: string;
65
66
  targetPublicKey: string;
66
67
  expirationSeconds?: number;
68
+ multiFactors?: VerifyMfaParams[];
67
69
  };
68
70
 
69
71
  export type SignupResponse = {
@@ -122,10 +124,12 @@ export type SignerEndpoints = [
122
124
  Route: "/v1/auth";
123
125
  Body: Omit<EmailAuthParams, "redirectParams"> & {
124
126
  redirectParams?: string;
127
+ multiFactors?: VerifyMfaParams[];
125
128
  };
126
129
  Response: {
127
130
  orgId: string;
128
131
  otpId?: string;
132
+ multiFactors?: MfaFactor[];
129
133
  };
130
134
  },
131
135
  {
@@ -156,7 +160,45 @@ export type SignerEndpoints = [
156
160
  {
157
161
  Route: "/v1/otp";
158
162
  Body: OtpParams;
159
- Response: { credentialBundle: string };
163
+ Response: {
164
+ credentialBundle: string | null;
165
+ };
166
+ },
167
+ {
168
+ Route: "/v1/auth-list-multi-factors";
169
+ Body: {
170
+ stampedRequest: TSignedRequest;
171
+ };
172
+ Response: {
173
+ multiFactors: MfaFactor[];
174
+ };
175
+ },
176
+ {
177
+ Route: "/v1/auth-delete-multi-factors";
178
+ Body: {
179
+ stampedRequest: TSignedRequest;
180
+ multiFactorIds: string[];
181
+ };
182
+ Response: {
183
+ multiFactors: MfaFactor[];
184
+ };
185
+ },
186
+ {
187
+ Route: "/v1/auth-request-multi-factor";
188
+ Body: {
189
+ stampedRequest: TSignedRequest;
190
+ multiFactorType: MultiFactorType;
191
+ };
192
+ Response: EnableMfaResult;
193
+ },
194
+ {
195
+ Route: "/v1/auth-verify-multi-factor";
196
+ Body: VerifyMfaParams & {
197
+ stampedRequest: TSignedRequest;
198
+ };
199
+ Response: {
200
+ multiFactors: MfaFactor[];
201
+ };
160
202
  }
161
203
  ];
162
204
 
@@ -200,3 +242,38 @@ export type GetOauthProviderUrlArgs = {
200
242
  oauthConfig?: OauthConfig;
201
243
  usesRelativeUrl?: boolean;
202
244
  };
245
+
246
+ export type MfaFactor = {
247
+ multiFactorId: string;
248
+ multiFactorType: string;
249
+ };
250
+
251
+ type MultiFactorType = "totp";
252
+
253
+ export type EnableMfaParams = {
254
+ multiFactorType: MultiFactorType;
255
+ };
256
+
257
+ export type EnableMfaResult = {
258
+ multiFactorType: MultiFactorType;
259
+ multiFactorId: string;
260
+ multiFactorTotpUrl: string;
261
+ };
262
+
263
+ export type VerifyMfaParams = {
264
+ multiFactorId: string;
265
+ multiFactorCode: string;
266
+ };
267
+
268
+ export type RemoveMfaParams = {
269
+ multiFactorIds: string[];
270
+ };
271
+
272
+ export type MfaChallenge = {
273
+ multiFactorId: string;
274
+ multiFactorChallenge:
275
+ | {
276
+ code: string;
277
+ }
278
+ | Record<string, any>;
279
+ };