@account-kit/signer 4.61.0 → 4.62.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.
package/src/base.ts CHANGED
@@ -114,6 +114,10 @@ type GetUserParams =
114
114
  value: string;
115
115
  };
116
116
 
117
+ type VerificationParams = {
118
+ verificationCode: string;
119
+ };
120
+
117
121
  /**
118
122
  * Base abstract class for Alchemy Signer, providing authentication and session management for smart accounts.
119
123
  * Implements the `SmartAccountAuthenticator` interface and handles various signer events.
@@ -834,24 +838,59 @@ export abstract class BaseAlchemySigner<TClient extends BaseSignerClient>
834
838
  )(params);
835
839
  }
836
840
 
837
- /*
841
+ /**
838
842
  * Sets the email for the authenticated user, allowing them to login with that
839
843
  * email.
840
844
  *
841
- * You must contact Alchemy to enable this feature for your team, as there are
842
- * important security considerations. In particular, you must not call this
843
- * without first validating that the user owns this email account.
845
+ * @deprecated You must contact Alchemy to enable this feature for your team,
846
+ * as there are important security considerations. In particular, you must not
847
+ * call this without first validating that the user owns this email account.
848
+ * It is recommended to now use the email verification flow instead.
844
849
  *
845
850
  * @param {string} email The email to set for the user
846
- * @returns {Promise<void>} A promise that resolves when the email is set
851
+ * @returns {Promise<string>} A promise that resolves to the updated email address
847
852
  * @throws {NotAuthenticatedError} If the user is not authenticated
848
853
  */
849
- setEmail: (email: string) => Promise<void> = SignerLogger.profiled(
850
- "BaseAlchemySigner.setEmail",
851
- async (email) => {
852
- return this.inner.setEmail(email);
853
- },
854
- );
854
+ setEmail(email: string): Promise<string>;
855
+
856
+ /**
857
+ * Uses a verification code to update a user's email, allowing them to login
858
+ * with that email. `sendVerificationCode` should be called first to obtain
859
+ * the code.
860
+ *
861
+ * @param {VerificationParams} params An object containing the verification code
862
+ * @param {string} params.verificationCode The OTP verification code
863
+ * @returns {Promise<string>} A promise that resolves to the updated email address
864
+ * @throws {NotAuthenticatedError} If the user is not authenticated
865
+ */
866
+ setEmail(params: VerificationParams): Promise<string>;
867
+
868
+ /**
869
+ * Implementation for setEmail method.
870
+ *
871
+ * @param {string | VerificationParams} params An object containing the verificationCode (or simply an email for legacy usage)
872
+ * @returns {Promise<void>} A promise that resolves when the email is set
873
+ */
874
+ async setEmail(params: string | VerificationParams): Promise<string> {
875
+ return SignerLogger.profiled(
876
+ "BaseAlchemySigner.setEmail",
877
+ async (params: string | { verificationCode: string }) => {
878
+ if (typeof params === "string") {
879
+ // Deprecated usage for backwards compatibility.
880
+ const email = params;
881
+ return await this.inner.setEmail(email);
882
+ }
883
+ const { otpId } = this.store.getState();
884
+ if (!otpId) {
885
+ throw new Error("Missing OTP ID");
886
+ }
887
+ return await this.inner.setEmail({
888
+ id: otpId,
889
+ code: params.verificationCode,
890
+ });
891
+ },
892
+ )(params);
893
+ }
855
894
 
856
895
  /**
857
896
  * Removes the email for the authenticated user, disallowing them from login with that email.
@@ -862,7 +901,65 @@ export abstract class BaseAlchemySigner<TClient extends BaseSignerClient>
862
901
  removeEmail: () => Promise<void> = SignerLogger.profiled(
863
902
  "BaseAlchemySigner.removeEmail",
864
903
  async () => {
865
- return this.inner.removeEmail();
904
+ await this.inner.removeEmail();
905
+ },
906
+ );
907
+
908
+ /**
909
+ * Sets the phone number for the authenticated user, allowing them to login with that
910
+ * phone number. `sendVerificationCode` should be called first to obtain the code.
911
+ *
912
+ * @param {VerificationParams} params An object containing the verification code
913
+ * @param {string} params.verificationCode The OTP verification code
914
+ * @returns {Promise<void>} A promise that resolves when the phone number is set
915
+ * @throws {NotAuthenticatedError} If the user is not authenticated
916
+ */
917
+ setPhoneNumber: (params: VerificationParams) => Promise<void> =
918
+ SignerLogger.profiled(
919
+ "BaseAlchemySigner.setPhoneNumber",
920
+ async ({ verificationCode }) => {
921
+ const { otpId } = this.store.getState();
922
+ if (!otpId) {
923
+ throw new Error("Missing OTP ID");
924
+ }
925
+ await this.inner.setPhoneNumber({
926
+ id: otpId,
927
+ code: verificationCode,
928
+ });
929
+ },
930
+ );
931
+
932
+ /**
933
+ * Removes the phone number for the authenticated user, disallowing them from login with that phone number.
934
+ *
935
+ * @returns {Promise<void>} A promise that resolves when the phone number is removed
936
+ * @throws {NotAuthenticatedError} If the user is not authenticated
937
+ */
938
+ removePhoneNumber: () => Promise<void> = SignerLogger.profiled(
939
+ "BaseAlchemySigner.removePhoneNumber",
940
+ async () => {
941
+ await this.inner.removePhoneNumber();
942
+ },
943
+ );
944
+
945
+ /**
946
+ * Initiates an OTP (One-Time Password) verification process for a user contact.
947
+ * Use this method before calling `setEmail` with verification code to verify ownership of the email address.
948
+ *
949
+ * @param {"email" | "sms"} type The type of OTP to send, either "email" or "sms"
950
+ * @param {string} contact The email address or phone number to send the OTP to
951
+ * @returns {Promise<{ otpId: string }>} A promise that resolves to an object containing the OTP ID
952
+ * @throws {NotAuthenticatedError} If the user is not authenticated
953
+ */
954
+ sendVerificationCode: (
955
+ type: "email" | "sms",
956
+ contact: string,
957
+ ) => Promise<{ otpId: string }> = SignerLogger.profiled(
958
+ "BaseAlchemySigner.sendVerificationCode",
959
+ async (type, contact) => {
960
+ const { otpId } = await this.inner.initOtp(type, contact);
961
+ this.store.setState({ otpId });
962
+ return { otpId };
866
963
  },
867
964
  );
868
965
 
@@ -49,6 +49,7 @@ import type {
49
49
  IdTokenOnly,
50
50
  AuthMethods,
51
51
  SmsAuthParams,
52
+ VerificationOtp,
52
53
  } from "./types.js";
53
54
  import { VERSION } from "../version.js";
54
55
  import { secp256k1 } from "@noble/curves/secp256k1";
@@ -291,53 +292,178 @@ export abstract class BaseSignerClient<
291
292
  * Sets the email for the authenticated user, allowing them to login with that
292
293
  * email.
293
294
  *
294
- * You must contact Alchemy to enable this feature for your team, as there are
295
- * important security considerations. In particular, you must not call this
296
- * without first validating that the user owns this email account.
295
+ * @deprecated You must contact Alchemy to enable this feature for your team,
296
+ * as there are important security considerations. In particular, you must not
297
+ * call this without first validating that the user owns this email account.
298
+ * Recommended to use the email verification flow instead.
297
299
  *
298
300
  * @param {string} email The email to set for the user
299
- * @returns {Promise<void>} A promise that resolves when the email is set
301
+ * @returns {Promise<void>} A promise that resolves to the updated email
300
302
  * @throws {NotAuthenticatedError} If the user is not authenticated
301
303
  */
302
- public setEmail = async (email: string): Promise<void> => {
303
- if (!email) {
304
- throw new Error(
305
- "Email must not be empty. Use removeEmail() to remove email auth.",
306
- );
304
+ public setEmail(email: string): Promise<string>;
305
+
306
+ /**
307
+ * Sets the email for the authenticated user, allowing them to login with that
308
+ * email. Must be called after calling `initOtp` with the email.
309
+ *
310
+ * @param {VerificationOtp} otp The OTP verification object including the OTP ID and OTP code
311
+ * @returns {Promise<void>} A promise that resolves to the updated email
312
+ * @throws {NotAuthenticatedError} If the user is not authenticated
313
+ */
314
+ public setEmail(otp: VerificationOtp): Promise<string>;
315
+
316
+ /**
317
+ * Implementation for setEmail method with optional OTP verification.
318
+ *
319
+ * @param {string | VerificationOtp} params An OTP object containing the OTP ID & OTP code (or an email address for legacy usage)
320
+ * @returns {Promise<void>} A promise that resolves to the updated email address
321
+ */
322
+ public async setEmail(params: string | VerificationOtp): Promise<string> {
323
+ if (typeof params === "string") {
324
+ // Legacy use, requires team flag.
325
+ const contact = params;
326
+ if (!contact) {
327
+ throw new Error(
328
+ "Email must not be empty. Use removeEmail() to remove email auth.",
329
+ );
330
+ }
331
+ await this.updateEmail(contact);
332
+ return contact;
307
333
  }
308
- await this.updateEmail(email);
309
- };
334
+
335
+ const { verificationToken } = await this.request("/v1/verify-otp", {
336
+ otpId: params.id,
337
+ otpCode: params.code,
338
+ });
339
+ const { contact } = jwtDecode<{ contact: string }>(verificationToken);
340
+ await this.updateEmail(contact, verificationToken);
341
+ return contact;
342
+ }
310
343
 
311
344
  /**
312
345
  * Removes the email for the authenticated user, disallowing them from login with that email.
313
346
  *
314
- * @returns {Promise<void>} A promise that resolves when the email is removed
347
+ * @returns {Promise<string>} A promise that resolves when the email is removed
315
348
  * @throws {NotAuthenticatedError} If the user is not authenticated
316
349
  */
317
350
  public removeEmail = async (): Promise<void> => {
318
- // This is a hack to remove the email for the user. Turnkey does not
319
- // support clearing the email once set, so we set it to a known
320
- // inaccessible address instead.
321
- await this.updateEmail("not.enabled@example.invalid");
351
+ await this.updateEmail("");
322
352
  };
323
353
 
324
- private updateEmail = async (email: string): Promise<void> => {
354
+ private updateEmail = async (
355
+ email: string,
356
+ verificationToken?: string,
357
+ ): Promise<void> => {
325
358
  if (!this.user) {
326
359
  throw new NotAuthenticatedError();
327
360
  }
328
- const stampedRequest = await this.turnkeyClient.stampUpdateUser({
329
- type: "ACTIVITY_TYPE_UPDATE_USER",
361
+
362
+ // Unverified use is legacy & requires team flag.
363
+ const isUnverified = email && !verificationToken;
364
+
365
+ const stampedRequest = isUnverified
366
+ ? await this.turnkeyClient.stampUpdateUser({
367
+ type: "ACTIVITY_TYPE_UPDATE_USER",
368
+ timestampMs: Date.now().toString(),
369
+ organizationId: this.user.orgId,
370
+ parameters: {
371
+ userId: this.user.userId,
372
+ userEmail: email,
373
+ },
374
+ })
375
+ : await this.turnkeyClient.stampUpdateUserEmail({
376
+ type: "ACTIVITY_TYPE_UPDATE_USER_EMAIL",
377
+ timestampMs: Date.now().toString(),
378
+ organizationId: this.user.orgId,
379
+ parameters: {
380
+ userId: this.user.userId,
381
+ userEmail: email,
382
+ verificationToken,
383
+ },
384
+ });
385
+
386
+ await this.request("/v1/update-email-auth", {
387
+ stampedRequest,
388
+ });
389
+ this.user = {
390
+ ...this.user,
391
+ email: email || undefined,
392
+ };
393
+ };
394
+
395
+ /**
396
+ * Updates the phone number for the authenticated user, allowing them to login with that
397
+ * phone number. Must be called after calling `initOtp` with the phone number.
398
+ *
399
+ * @param {VerificationOtp} otp The OTP object including the OTP ID and OTP code
400
+ * @returns {Promise<void>} A promise that resolves when the phone number is set
401
+ * @throws {NotAuthenticatedError} If the user is not authenticated
402
+ */
403
+ public setPhoneNumber = async (otp: VerificationOtp): Promise<void> => {
404
+ const { verificationToken } = await this.request("/v1/verify-otp", {
405
+ otpId: otp.id,
406
+ otpCode: otp.code,
407
+ });
408
+ const { contact } = jwtDecode<{ contact: string }>(verificationToken);
409
+ await this.updatePhoneNumber(contact, verificationToken);
410
+ };
411
+
412
+ /**
413
+ * Removes the phone number for the authenticated user, disallowing them from login with that phone number.
414
+ *
415
+ * @returns {Promise<void>} A promise that resolves when the phone number is removed
416
+ * @throws {NotAuthenticatedError} If the user is not authenticated
417
+ */
418
+ public removePhoneNumber = async (): Promise<void> => {
419
+ await this.updatePhoneNumber("");
420
+ };
421
+
422
+ private updatePhoneNumber = async (
423
+ phone: string,
424
+ verificationToken?: string,
425
+ ): Promise<void> => {
426
+ if (!this.user) {
427
+ throw new NotAuthenticatedError();
428
+ }
429
+ if (phone.trim() && !verificationToken) {
430
+ throw new Error("Verification token is required to change phone number.");
431
+ }
432
+ const stampedRequest = await this.turnkeyClient.stampUpdateUserPhoneNumber({
433
+ type: "ACTIVITY_TYPE_UPDATE_USER_PHONE_NUMBER",
330
434
  timestampMs: Date.now().toString(),
331
435
  organizationId: this.user.orgId,
332
436
  parameters: {
333
437
  userId: this.user.userId,
334
- userEmail: email,
335
- userTagIds: [],
438
+ userPhoneNumber: phone,
439
+ verificationToken,
336
440
  },
337
441
  });
338
- await this.request("/v1/update-email-auth", {
442
+ await this.request("/v1/update-phone-auth", {
339
443
  stampedRequest,
340
444
  });
445
+ this.user = {
446
+ ...this.user,
447
+ phone: phone || undefined,
448
+ };
449
+ };
450
+
451
+ /**
452
+ * Initiates an OTP (One-Time Password) verification process for a user contact.
453
+ *
454
+ * @param {("email" | "sms")} type - The type of OTP to send, either "email" or "sms"
455
+ * @param {string} contact - The email address or phone number to send the OTP to
456
+ * @returns {Promise<{ otpId: string }>} A promise that resolves to an object containing the OTP ID
457
+ * @throws {NotAuthenticatedError} When no user is currently authenticated
458
+ */
459
+ public initOtp = async (
460
+ type: "email" | "sms",
461
+ contact: string,
462
+ ): Promise<{ otpId: string }> => {
463
+ return await this.request("/v1/init-otp", {
464
+ otpType: type === "email" ? "OTP_TYPE_EMAIL" : "OTP_TYPE_SMS",
465
+ contact,
466
+ });
341
467
  };
342
468
 
343
469
  /**
@@ -761,7 +887,9 @@ export abstract class BaseSignerClient<
761
887
 
762
888
  return {
763
889
  stampHeaderName: "X-Stamp",
764
- stampHeaderValue: base64UrlEncode(Buffer.from(JSON.stringify(stamp))),
890
+ stampHeaderValue: base64UrlEncode(
891
+ Buffer.from(JSON.stringify(stamp)).buffer,
892
+ ),
765
893
  };
766
894
  },
767
895
  });
@@ -1230,7 +1358,7 @@ export abstract class BaseSignerClient<
1230
1358
  fetchIdTokenOnly: oauthParams.fetchIdTokenOnly,
1231
1359
  };
1232
1360
  const state = base64UrlEncode(
1233
- new TextEncoder().encode(JSON.stringify(stateObject)),
1361
+ new TextEncoder().encode(JSON.stringify(stateObject)).buffer,
1234
1362
  );
1235
1363
  const authUrl = new URL(authEndpoint);
1236
1364
  const params: Record<string, string> = {
@@ -7,6 +7,15 @@ import type {
7
7
  import type { Hex } from "viem";
8
8
  import type { AuthParams } from "../signer";
9
9
 
10
+ // [!region VerificationOtp]
11
+ export type VerificationOtp = {
12
+ /** The OTP ID returned from initOtp */
13
+ id: string;
14
+ /** The OTP code received by the user */
15
+ code: string;
16
+ };
17
+ // [!endregion VerificationOtp]
18
+
10
19
  export type CredentialCreationOptionOverrides = {
11
20
  publicKey?: Partial<CredentialCreationOptions["publicKey"]>;
12
21
  } & Pick<CredentialCreationOptions, "signal">;
@@ -14,6 +23,7 @@ export type CredentialCreationOptionOverrides = {
14
23
  // [!region User]
15
24
  export type User = {
16
25
  email?: string;
26
+ phone?: string;
17
27
  orgId: string;
18
28
  userId: string;
19
29
  address: Address;
@@ -197,6 +207,26 @@ export type SignerEndpoints = [
197
207
  orgId: string | null;
198
208
  };
199
209
  },
210
+ {
211
+ Route: "/v1/init-otp";
212
+ Body: {
213
+ contact: string;
214
+ otpType: "OTP_TYPE_SMS" | "OTP_TYPE_EMAIL";
215
+ };
216
+ Response: {
217
+ otpId: string;
218
+ };
219
+ },
220
+ {
221
+ Route: "/v1/verify-otp";
222
+ Body: {
223
+ otpId: string;
224
+ otpCode: string;
225
+ };
226
+ Response: {
227
+ verificationToken: string;
228
+ };
229
+ },
200
230
  {
201
231
  Route: "/v1/sign-payload";
202
232
  Body: {
@@ -213,6 +243,13 @@ export type SignerEndpoints = [
213
243
  };
214
244
  Response: void;
215
245
  },
246
+ {
247
+ Route: "/v1/update-phone-auth";
248
+ Body: {
249
+ stampedRequest: TSignedRequest;
250
+ };
251
+ Response: void;
252
+ },
216
253
  {
217
254
  Route: "/v1/add-oauth-provider";
218
255
  Body: {
@@ -512,6 +549,7 @@ export type AddOauthProviderParams = {
512
549
 
513
550
  export type AuthMethods = {
514
551
  email?: string;
552
+ phone?: string;
515
553
  oauthProviders: OauthProviderInfo[];
516
554
  passkeys: PasskeyInfo[];
517
555
  };
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.61.0";
3
+ export const VERSION = "4.62.0";