@account-kit/signer 4.21.0 → 4.23.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.
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
@@ -17,9 +17,25 @@ import type {
17
17
  OauthConfig,
18
18
  OtpParams,
19
19
  User,
20
+ MfaFactor,
21
+ EnableMfaParams,
22
+ EnableMfaResult,
23
+ VerifyMfaParams,
24
+ RemoveMfaParams,
25
+ SubmitOtpCodeResponse,
26
+ ValidateMultiFactorsParams,
20
27
  } from "./types.js";
28
+ import { MfaRequiredError, NotAuthenticatedError } from "../errors.js";
29
+ import { parseMfaError } from "../utils/parseMfaError.js";
21
30
 
22
31
  const CHECK_CLOSE_INTERVAL = 500;
32
+ const MFA_PAYLOAD = {
33
+ GET: "get_mfa",
34
+ ADD: "add_mfa",
35
+ DELETE: "delete_mfas",
36
+ VERIFY: "verify_mfa",
37
+ LIST: "list_mfas",
38
+ };
23
39
 
24
40
  export const AlchemySignerClientParamsSchema = z.object({
25
41
  connection: ConnectionConfigSchema,
@@ -198,13 +214,25 @@ export class AlchemySignerWebClient extends BaseSignerClient<ExportWalletParams>
198
214
  const { email, emailMode, expirationSeconds } = params;
199
215
  const publicKey = await this.initIframeStamper();
200
216
 
201
- return this.request("/v1/auth", {
202
- email,
203
- emailMode,
204
- targetPublicKey: publicKey,
205
- expirationSeconds,
206
- redirectParams: params.redirectParams?.toString(),
207
- });
217
+ try {
218
+ return await this.request("/v1/auth", {
219
+ email,
220
+ emailMode,
221
+ targetPublicKey: publicKey,
222
+ expirationSeconds,
223
+ redirectParams: params.redirectParams?.toString(),
224
+ multiFactors: params.multiFactors,
225
+ });
226
+ } catch (error) {
227
+ const multiFactors = parseMfaError(error);
228
+
229
+ // If MFA is required, and emailMode is Magic Link, the user must submit mfa with the request or
230
+ // the the server will return an error with the required mfa factors.
231
+ if (multiFactors) {
232
+ throw new MfaRequiredError(multiFactors);
233
+ }
234
+ throw error;
235
+ }
208
236
  };
209
237
 
210
238
  /**
@@ -235,14 +263,38 @@ export class AlchemySignerWebClient extends BaseSignerClient<ExportWalletParams>
235
263
  */
236
264
  public override async submitOtpCode(
237
265
  args: Omit<OtpParams, "targetPublicKey">
238
- ): Promise<{ bundle: string }> {
266
+ ): Promise<SubmitOtpCodeResponse> {
239
267
  this.eventEmitter.emit("authenticating", { type: "otpVerify" });
240
268
  const targetPublicKey = await this.initIframeStamper();
241
- const { credentialBundle } = await this.request("/v1/otp", {
269
+ const response = await this.request("/v1/otp", {
242
270
  ...args,
243
271
  targetPublicKey,
244
272
  });
245
- return { bundle: credentialBundle };
273
+
274
+ if ("credentialBundle" in response && response.credentialBundle) {
275
+ return {
276
+ mfaRequired: false,
277
+ bundle: response.credentialBundle,
278
+ };
279
+ }
280
+
281
+ // If the server says "MFA_REQUIRED", pass that data back to the caller:
282
+ if (
283
+ response.status === "MFA_REQUIRED" &&
284
+ response.encryptedPayload &&
285
+ response.multiFactors
286
+ ) {
287
+ return {
288
+ mfaRequired: true,
289
+ encryptedPayload: response.encryptedPayload,
290
+ multiFactors: response.multiFactors,
291
+ };
292
+ }
293
+
294
+ // Otherwise, it's truly an error:
295
+ throw new Error(
296
+ "Failed to submit OTP code. Server did not return required fields."
297
+ );
246
298
  }
247
299
 
248
300
  /**
@@ -670,6 +722,169 @@ export class AlchemySignerWebClient extends BaseSignerClient<ExportWalletParams>
670
722
  const nonce = this.getOauthNonce(publicKey);
671
723
  return this.request("/v1/prepare-oauth", { nonce });
672
724
  };
725
+
726
+ /**
727
+ * Retrieves the list of MFA factors configured for the current user.
728
+ *
729
+ * @returns {Promise<{ multiFactors: MfaFactor[] }>} A promise that resolves to an array of configured MFA factors
730
+ * @throws {NotAuthenticatedError} If no user is authenticated
731
+ */
732
+ public override getMfaFactors = async (): Promise<{
733
+ multiFactors: MfaFactor[];
734
+ }> => {
735
+ if (!this.user) {
736
+ throw new NotAuthenticatedError();
737
+ }
738
+
739
+ const stampedRequest = await this.turnkeyClient.stampSignRawPayload({
740
+ organizationId: this.user.orgId,
741
+ type: "ACTIVITY_TYPE_SIGN_RAW_PAYLOAD_V2",
742
+ timestampMs: Date.now().toString(),
743
+ parameters: {
744
+ encoding: "PAYLOAD_ENCODING_HEXADECIMAL",
745
+ hashFunction: "HASH_FUNCTION_NO_OP",
746
+ payload: MFA_PAYLOAD.LIST,
747
+ signWith: this.user.address,
748
+ },
749
+ });
750
+
751
+ return this.request("/v1/auth-list-multi-factors", {
752
+ stampedRequest,
753
+ });
754
+ };
755
+
756
+ /**
757
+ * Initiates the setup of a new MFA factor for the current user. Mfa will need to be verified before it is active.
758
+ *
759
+ * @param {EnableMfaParams} params The parameters required to enable a new MFA factor
760
+ * @returns {Promise<EnableMfaResult>} A promise that resolves to the factor setup information
761
+ * @throws {NotAuthenticatedError} If no user is authenticated
762
+ * @throws {Error} If an unsupported factor type is provided
763
+ */
764
+ public override addMfa = async (
765
+ params: EnableMfaParams
766
+ ): Promise<EnableMfaResult> => {
767
+ if (!this.user) {
768
+ throw new NotAuthenticatedError();
769
+ }
770
+
771
+ const stampedRequest = await this.turnkeyClient.stampSignRawPayload({
772
+ organizationId: this.user.orgId,
773
+ type: "ACTIVITY_TYPE_SIGN_RAW_PAYLOAD_V2",
774
+ timestampMs: Date.now().toString(),
775
+ parameters: {
776
+ encoding: "PAYLOAD_ENCODING_HEXADECIMAL",
777
+ hashFunction: "HASH_FUNCTION_NO_OP",
778
+ payload: MFA_PAYLOAD.ADD,
779
+ signWith: this.user.address,
780
+ },
781
+ });
782
+
783
+ switch (params.multiFactorType) {
784
+ case "totp":
785
+ return this.request("/v1/auth-request-multi-factor", {
786
+ stampedRequest,
787
+ multiFactorType: params.multiFactorType,
788
+ });
789
+ default:
790
+ throw new Error(
791
+ `Unsupported MFA factor type: ${params.multiFactorType}`
792
+ );
793
+ }
794
+ };
795
+
796
+ /**
797
+ * Verifies a newly created MFA factor to complete the setup process.
798
+ *
799
+ * @param {VerifyMfaParams} params The parameters required to verify the MFA factor
800
+ * @returns {Promise<{ multiFactors: MfaFactor[] }>} A promise that resolves to the updated list of MFA factors
801
+ * @throws {NotAuthenticatedError} If no user is authenticated
802
+ */
803
+ public override verifyMfa = async (
804
+ params: VerifyMfaParams
805
+ ): Promise<{ multiFactors: MfaFactor[] }> => {
806
+ if (!this.user) {
807
+ throw new NotAuthenticatedError();
808
+ }
809
+
810
+ const stampedRequest = await this.turnkeyClient.stampSignRawPayload({
811
+ organizationId: this.user.orgId,
812
+ type: "ACTIVITY_TYPE_SIGN_RAW_PAYLOAD_V2",
813
+ timestampMs: Date.now().toString(),
814
+ parameters: {
815
+ encoding: "PAYLOAD_ENCODING_HEXADECIMAL",
816
+ hashFunction: "HASH_FUNCTION_NO_OP",
817
+ payload: MFA_PAYLOAD.VERIFY,
818
+ signWith: this.user.address,
819
+ },
820
+ });
821
+
822
+ return this.request("/v1/auth-verify-multi-factor", {
823
+ stampedRequest,
824
+ multiFactorId: params.multiFactorId,
825
+ multiFactorCode: params.multiFactorCode,
826
+ });
827
+ };
828
+
829
+ /**
830
+ * Removes existing MFA factors by ID.
831
+ *
832
+ * @param {RemoveMfaParams} params The parameters specifying which factors to disable
833
+ * @returns {Promise<{ multiFactors: MfaFactor[] }>} A promise that resolves to the updated list of MFA factors
834
+ * @throws {NotAuthenticatedError} If no user is authenticated
835
+ */
836
+ public override removeMfa = async (
837
+ params: RemoveMfaParams
838
+ ): Promise<{ multiFactors: MfaFactor[] }> => {
839
+ if (!this.user) {
840
+ throw new NotAuthenticatedError();
841
+ }
842
+
843
+ const stampedRequest = await this.turnkeyClient.stampSignRawPayload({
844
+ organizationId: this.user.orgId,
845
+ type: "ACTIVITY_TYPE_SIGN_RAW_PAYLOAD_V2",
846
+ timestampMs: Date.now().toString(),
847
+ parameters: {
848
+ encoding: "PAYLOAD_ENCODING_HEXADECIMAL",
849
+ hashFunction: "HASH_FUNCTION_NO_OP",
850
+ payload: MFA_PAYLOAD.DELETE,
851
+ signWith: this.user.address,
852
+ },
853
+ });
854
+
855
+ return this.request("/v1/auth-delete-multi-factors", {
856
+ stampedRequest,
857
+ multiFactorIds: params.multiFactorIds,
858
+ });
859
+ };
860
+
861
+ /**
862
+ * Validates multiple MFA factors using the provided encrypted payload and MFA codes.
863
+ *
864
+ * @param {ValidateMultiFactorsParams} params The validation parameters
865
+ * @returns {Promise<{ bundle: string }>} A promise that resolves to an object containing the credential bundle
866
+ * @throws {Error} If no credential bundle is returned from the server
867
+ */
868
+ public override async validateMultiFactors(
869
+ params: ValidateMultiFactorsParams
870
+ ): Promise<{ bundle: string }> {
871
+ // Send the encryptedPayload plus TOTP codes, etc:
872
+ const response = await this.request("/v1/auth-validate-multi-factors", {
873
+ encryptedPayload: params.encryptedPayload,
874
+ multiFactors: params.multiFactors,
875
+ });
876
+
877
+ // The server is expected to return the *decrypted* payload in `response.payload.credentialBundle`
878
+ if (!response.payload || !response.payload.credentialBundle) {
879
+ throw new Error(
880
+ "Request to validateMultiFactors did not return a credential bundle"
881
+ );
882
+ }
883
+
884
+ return {
885
+ bundle: response.payload.credentialBundle,
886
+ };
887
+ }
673
888
  }
674
889
 
675
890
  /**
@@ -54,6 +54,7 @@ export type EmailAuthParams = {
54
54
  expirationSeconds?: number;
55
55
  targetPublicKey: string;
56
56
  redirectParams?: URLSearchParams;
57
+ multiFactors?: VerifyMfaParams[];
57
58
  };
58
59
 
59
60
  export type OauthParams = Extract<AuthParams, { type: "oauth" }> & {
@@ -66,8 +67,20 @@ export type OtpParams = {
66
67
  otpCode: string;
67
68
  targetPublicKey: string;
68
69
  expirationSeconds?: number;
70
+ multiFactors?: VerifyMfaParams[];
69
71
  };
70
72
 
73
+ export type OtpResponse =
74
+ | {
75
+ status: "SUCCESS";
76
+ credentialBundle: string;
77
+ }
78
+ | {
79
+ status: "MFA_REQUIRED";
80
+ encryptedPayload: string;
81
+ multiFactors: MfaFactor[];
82
+ };
83
+
71
84
  export type SignupResponse = {
72
85
  orgId: string;
73
86
  userId?: string;
@@ -132,10 +145,12 @@ export type SignerEndpoints = [
132
145
  Route: "/v1/auth";
133
146
  Body: Omit<EmailAuthParams, "redirectParams"> & {
134
147
  redirectParams?: string;
148
+ multiFactors?: VerifyMfaParams[];
135
149
  };
136
150
  Response: {
137
151
  orgId: string;
138
152
  otpId?: string;
153
+ multiFactors?: MfaFactor[];
139
154
  };
140
155
  },
141
156
  {
@@ -166,12 +181,61 @@ export type SignerEndpoints = [
166
181
  {
167
182
  Route: "/v1/otp";
168
183
  Body: OtpParams;
169
- Response: { credentialBundle: string };
184
+ Response: OtpResponse;
185
+ },
186
+ {
187
+ Route: "/v1/auth-list-multi-factors";
188
+ Body: {
189
+ stampedRequest: TSignedRequest;
190
+ };
191
+ Response: {
192
+ multiFactors: MfaFactor[];
193
+ };
194
+ },
195
+ {
196
+ Route: "/v1/auth-delete-multi-factors";
197
+ Body: {
198
+ stampedRequest: TSignedRequest;
199
+ multiFactorIds: string[];
200
+ };
201
+ Response: {
202
+ multiFactors: MfaFactor[];
203
+ };
204
+ },
205
+ {
206
+ Route: "/v1/auth-request-multi-factor";
207
+ Body: {
208
+ stampedRequest: TSignedRequest;
209
+ multiFactorType: MultiFactorType;
210
+ };
211
+ Response: EnableMfaResult;
212
+ },
213
+ {
214
+ Route: "/v1/auth-verify-multi-factor";
215
+ Body: VerifyMfaParams & {
216
+ stampedRequest: TSignedRequest;
217
+ };
218
+ Response: {
219
+ multiFactors: MfaFactor[];
220
+ };
170
221
  },
171
222
  {
172
223
  Route: "/v1/signer-config";
173
224
  Body: {};
174
225
  Response: SignerConfig;
226
+ },
227
+ {
228
+ Route: "/v1/auth-validate-multi-factors";
229
+ Body: {
230
+ encryptedPayload: string;
231
+ multiFactors: VerifyMfaParams[];
232
+ };
233
+ Response: {
234
+ payload: {
235
+ credentialBundle?: string;
236
+ };
237
+ multiFactors: MfaFactor[];
238
+ };
175
239
  }
176
240
  ];
177
241
 
@@ -216,6 +280,53 @@ export type GetOauthProviderUrlArgs = {
216
280
  usesRelativeUrl?: boolean;
217
281
  };
218
282
 
283
+ export type MfaFactor = {
284
+ multiFactorId: string;
285
+ multiFactorType: string;
286
+ };
287
+
288
+ type MultiFactorType = "totp";
289
+
290
+ export type EnableMfaParams = {
291
+ multiFactorType: MultiFactorType;
292
+ };
293
+
294
+ export type EnableMfaResult = {
295
+ multiFactorType: MultiFactorType;
296
+ multiFactorId: string;
297
+ multiFactorTotpUrl: string;
298
+ };
299
+
300
+ export type VerifyMfaParams = {
301
+ multiFactorId: string;
302
+ multiFactorCode: string;
303
+ };
304
+
305
+ export type RemoveMfaParams = {
306
+ multiFactorIds: string[];
307
+ };
308
+
309
+ export type ValidateMultiFactorsParams = {
310
+ encryptedPayload: string;
311
+ multiFactors: Array<{
312
+ multiFactorId: string;
313
+ multiFactorCode: string;
314
+ }>;
315
+ };
316
+
317
+ export type MfaChallenge = {
318
+ multiFactorId: string;
319
+ multiFactorChallenge:
320
+ | {
321
+ code: string;
322
+ }
323
+ | Record<string, any>;
324
+ };
325
+
326
+ export type SubmitOtpCodeResponse =
327
+ | { bundle: string; mfaRequired: false }
328
+ | { mfaRequired: true; encryptedPayload: string; multiFactors: MfaFactor[] };
329
+
219
330
  export type experimental_CreateApiKeyParams = {
220
331
  name: string;
221
332
  publicKey: string;
package/src/errors.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { BaseError } from "@aa-sdk/core";
2
-
2
+ import type { MfaFactor } from "./client/types";
3
3
  export class NotAuthenticatedError extends BaseError {
4
4
  override name = "NotAuthenticatedError";
5
5
  constructor() {
@@ -23,3 +23,13 @@ export class OAuthProvidersError extends BaseError {
23
23
  });
24
24
  }
25
25
  }
26
+
27
+ export class MfaRequiredError extends BaseError {
28
+ override name = "MfaRequiredError";
29
+ public multiFactors: MfaFactor[];
30
+
31
+ constructor(multiFactors: MfaFactor[]) {
32
+ super("MFA is required for this user");
33
+ this.multiFactors = multiFactors;
34
+ }
35
+ }
package/src/index.ts CHANGED
@@ -6,7 +6,11 @@ export {
6
6
  OauthFailedError,
7
7
  } from "./client/index.js";
8
8
  export type * from "./client/types.js";
9
- export { NotAuthenticatedError, OAuthProvidersError } from "./errors.js";
9
+ export {
10
+ NotAuthenticatedError,
11
+ OAuthProvidersError,
12
+ MfaRequiredError,
13
+ } from "./errors.js";
10
14
  export {
11
15
  DEFAULT_SESSION_MS,
12
16
  SessionManagerParamsSchema,
@@ -43,7 +43,12 @@ type Store = Mutate<
43
43
  [["zustand/subscribeWithSelector", never], ["zustand/persist", SessionState]]
44
44
  >;
45
45
 
46
- type TemporarySession = { orgId: string; isNewUser?: boolean };
46
+ type TemporarySession = {
47
+ orgId: string;
48
+ isNewUser?: boolean;
49
+ encryptedPayload?: string;
50
+ mfaFactorId?: string;
51
+ };
47
52
 
48
53
  export class SessionManager {
49
54
  private sessionKey: string;
package/src/signer.ts CHANGED
@@ -4,7 +4,10 @@ import {
4
4
  AlchemySignerClientParamsSchema,
5
5
  AlchemySignerWebClient,
6
6
  } from "./client/index.js";
7
- import type { CredentialCreationOptionOverrides } from "./client/types.js";
7
+ import type {
8
+ CredentialCreationOptionOverrides,
9
+ VerifyMfaParams,
10
+ } from "./client/types.js";
8
11
  import { SessionManagerParamsSchema } from "./session/manager.js";
9
12
 
10
13
  export type AuthParams =
@@ -14,6 +17,7 @@ export type AuthParams =
14
17
  /** @deprecated This option will be overriden by dashboard settings. Please use the dashboard settings instead. This option will be removed in a future release. */
15
18
  emailMode?: "magicLink" | "otp";
16
19
  redirectParams?: URLSearchParams;
20
+ multiFactors?: VerifyMfaParams[];
17
21
  }
18
22
  | { type: "email"; bundle: string; orgId?: string; isNewUser?: boolean }
19
23
  | {
@@ -48,6 +52,7 @@ export type AuthParams =
48
52
  | {
49
53
  type: "otp";
50
54
  otpCode: string;
55
+ multiFactors?: VerifyMfaParams[];
51
56
  };
52
57
 
53
58
  export type OauthProviderConfig =
package/src/types.ts CHANGED
@@ -6,6 +6,11 @@ export type AlchemySignerEvents = {
6
6
  disconnected(): void;
7
7
  statusChanged(status: AlchemySignerStatus): void;
8
8
  errorChanged(error: ErrorInfo | undefined): void;
9
+ mfaStatusChanged(mfaStatus: {
10
+ mfaRequired: boolean;
11
+ mfaFactorId?: string;
12
+ encryptedPayload?: string;
13
+ }): void;
9
14
  };
10
15
 
11
16
  export type AlchemySignerEvent = keyof AlchemySignerEvents;
@@ -19,9 +24,20 @@ export enum AlchemySignerStatus {
19
24
  AUTHENTICATING_OAUTH = "AUTHENTICATING_OAUTH",
20
25
  AWAITING_EMAIL_AUTH = "AWAITING_EMAIL_AUTH",
21
26
  AWAITING_OTP_AUTH = "AWAITING_OTP_AUTH",
27
+ AWAITING_MFA_AUTH = "AWAITING_MFA_AUTH",
28
+ }
29
+
30
+ export enum AlchemyMfaStatus {
31
+ NOT_REQUIRED = "not_required",
32
+ REQUIRED = "required",
22
33
  }
23
34
 
24
35
  export interface ErrorInfo {
25
36
  name: string;
26
37
  message: string;
27
38
  }
39
+
40
+ export type ValidateMultiFactorsArgs = {
41
+ multiFactorId?: string;
42
+ multiFactorCode: string;
43
+ };
@@ -0,0 +1,15 @@
1
+ import type { MfaFactor } from "../client/types.js";
2
+
3
+ export function parseMfaError(error: unknown): MfaFactor[] | null {
4
+ if (error instanceof Error) {
5
+ try {
6
+ const parsed = JSON.parse(error.message);
7
+ if (parsed?.data?.multiFactors) {
8
+ return parsed.data.multiFactors;
9
+ }
10
+ } catch {
11
+ // ignore JSON parse failures
12
+ }
13
+ }
14
+ return null;
15
+ }
package/src/version.ts CHANGED
@@ -1,3 +1,3 @@
1
1
  // This file is autogenerated by inject-version.ts. Any changes will be
2
2
  // overwritten on commit!
3
- export const VERSION = "4.21.0";
3
+ export const VERSION = "4.23.0";