@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.
- package/dist/esm/base.d.ts +32 -0
- package/dist/esm/base.js +122 -54
- package/dist/esm/base.js.map +1 -1
- package/dist/esm/client/base.d.ts +36 -2
- package/dist/esm/client/base.js.map +1 -1
- package/dist/esm/client/index.d.ts +40 -1
- package/dist/esm/client/index.js +164 -7
- package/dist/esm/client/index.js.map +1 -1
- package/dist/esm/client/types.d.ts +68 -1
- package/dist/esm/client/types.js.map +1 -1
- package/dist/esm/errors.d.ts +6 -0
- package/dist/esm/errors.js +18 -0
- package/dist/esm/errors.js.map +1 -1
- package/dist/esm/index.d.ts +1 -1
- package/dist/esm/index.js +1 -1
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/session/manager.d.ts +1 -0
- package/dist/esm/session/manager.js.map +1 -1
- package/dist/esm/signer.d.ts +3 -1
- package/dist/esm/signer.js.map +1 -1
- package/dist/esm/types.d.ts +8 -0
- package/dist/esm/types.js +5 -0
- package/dist/esm/types.js.map +1 -1
- package/dist/esm/utils/parseMfaError.d.ts +2 -0
- package/dist/esm/utils/parseMfaError.js +15 -0
- package/dist/esm/utils/parseMfaError.js.map +1 -0
- package/dist/esm/version.d.ts +1 -1
- package/dist/esm/version.js +1 -1
- package/dist/esm/version.js.map +1 -1
- package/dist/types/base.d.ts +32 -0
- package/dist/types/base.d.ts.map +1 -1
- package/dist/types/client/base.d.ts +36 -2
- package/dist/types/client/base.d.ts.map +1 -1
- package/dist/types/client/index.d.ts +40 -1
- package/dist/types/client/index.d.ts.map +1 -1
- package/dist/types/client/types.d.ts +68 -1
- package/dist/types/client/types.d.ts.map +1 -1
- package/dist/types/errors.d.ts +6 -0
- package/dist/types/errors.d.ts.map +1 -1
- package/dist/types/index.d.ts +1 -1
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/session/manager.d.ts +1 -0
- package/dist/types/session/manager.d.ts.map +1 -1
- package/dist/types/signer.d.ts +3 -1
- package/dist/types/signer.d.ts.map +1 -1
- package/dist/types/types.d.ts +8 -0
- package/dist/types/types.d.ts.map +1 -1
- package/dist/types/utils/parseMfaError.d.ts +3 -0
- package/dist/types/utils/parseMfaError.d.ts.map +1 -0
- package/dist/types/version.d.ts +1 -1
- package/dist/types/version.d.ts.map +1 -1
- package/package.json +4 -4
- package/src/base.ts +171 -60
- package/src/client/base.ts +43 -1
- package/src/client/index.ts +174 -7
- package/src/client/types.ts +78 -1
- package/src/errors.ts +11 -1
- package/src/index.ts +5 -1
- package/src/session/manager.ts +5 -1
- package/src/signer.ts +6 -1
- package/src/types.ts +9 -0
- package/src/utils/parseMfaError.ts +15 -0
- 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 {
|
|
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 ("
|
|
773
|
-
|
|
774
|
-
|
|
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
|
-
|
|
792
|
-
|
|
793
|
-
|
|
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
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
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
|
-
|
|
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
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
});
|
|
845
|
+
this.sessionManager.setTemporarySession({
|
|
846
|
+
orgId,
|
|
847
|
+
isNewUser,
|
|
848
|
+
isMfaRequired,
|
|
849
|
+
});
|
|
830
850
|
|
|
831
|
-
|
|
832
|
-
|
|
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
|
-
|
|
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 {
|
package/src/client/base.ts
CHANGED
|
@@ -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;
|
package/src/client/index.ts
CHANGED
|
@@ -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
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
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
|
/**
|
package/src/client/types.ts
CHANGED
|
@@ -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: {
|
|
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
|
+
};
|