@account-kit/signer 4.31.2 → 4.33.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 (46) hide show
  1. package/dist/esm/base.d.ts +11 -8
  2. package/dist/esm/base.js +141 -38
  3. package/dist/esm/base.js.map +1 -1
  4. package/dist/esm/client/base.d.ts +132 -9
  5. package/dist/esm/client/base.js +34 -4
  6. package/dist/esm/client/base.js.map +1 -1
  7. package/dist/esm/client/index.d.ts +36 -14
  8. package/dist/esm/client/index.js +36 -18
  9. package/dist/esm/client/index.js.map +1 -1
  10. package/dist/esm/client/types.d.ts +19 -0
  11. package/dist/esm/client/types.js.map +1 -1
  12. package/dist/esm/signer.d.ts +88 -33
  13. package/dist/esm/signer.js +28 -3
  14. package/dist/esm/signer.js.map +1 -1
  15. package/dist/esm/solanaSigner.d.ts +3 -3
  16. package/dist/esm/solanaSigner.js +1 -1
  17. package/dist/esm/solanaSigner.js.map +1 -1
  18. package/dist/esm/types.d.ts +1 -0
  19. package/dist/esm/types.js.map +1 -1
  20. package/dist/esm/version.d.ts +1 -1
  21. package/dist/esm/version.js +1 -1
  22. package/dist/esm/version.js.map +1 -1
  23. package/dist/types/base.d.ts +11 -8
  24. package/dist/types/base.d.ts.map +1 -1
  25. package/dist/types/client/base.d.ts +132 -9
  26. package/dist/types/client/base.d.ts.map +1 -1
  27. package/dist/types/client/index.d.ts +36 -14
  28. package/dist/types/client/index.d.ts.map +1 -1
  29. package/dist/types/client/types.d.ts +19 -0
  30. package/dist/types/client/types.d.ts.map +1 -1
  31. package/dist/types/signer.d.ts +88 -33
  32. package/dist/types/signer.d.ts.map +1 -1
  33. package/dist/types/solanaSigner.d.ts +3 -3
  34. package/dist/types/solanaSigner.d.ts.map +1 -1
  35. package/dist/types/types.d.ts +1 -0
  36. package/dist/types/types.d.ts.map +1 -1
  37. package/dist/types/version.d.ts +1 -1
  38. package/package.json +6 -7
  39. package/src/base.ts +191 -63
  40. package/src/client/base.ts +36 -7
  41. package/src/client/index.ts +41 -18
  42. package/src/client/types.ts +21 -0
  43. package/src/signer.ts +36 -3
  44. package/src/solanaSigner.ts +4 -4
  45. package/src/types.ts +1 -0
  46. package/src/version.ts +1 -1
package/src/base.ts CHANGED
@@ -1,4 +1,8 @@
1
- import { takeBytes, type SmartAccountAuthenticator } from "@aa-sdk/core";
1
+ import {
2
+ takeBytes,
3
+ type SmartAccountAuthenticator,
4
+ type AuthorizationRequest,
5
+ } from "@aa-sdk/core";
2
6
  import {
3
7
  hashMessage,
4
8
  hashTypedData,
@@ -10,28 +14,29 @@ import {
10
14
  type LocalAccount,
11
15
  type SerializeTransactionFn,
12
16
  type SignableMessage,
17
+ type SignedAuthorization,
13
18
  type TransactionSerializable,
14
19
  type TransactionSerialized,
15
20
  type TypedData,
16
21
  type TypedDataDefinition,
17
22
  } from "viem";
18
23
  import { toAccount } from "viem/accounts";
19
- import { hashAuthorization, type Authorization } from "viem/experimental";
20
24
  import type { Mutate, StoreApi } from "zustand";
21
25
  import { subscribeWithSelector } from "zustand/middleware";
22
26
  import { createStore } from "zustand/vanilla";
23
27
  import type { BaseSignerClient } from "./client/base";
24
- import type {
25
- EmailType,
26
- MfaFactor,
27
- OauthConfig,
28
- OauthParams,
29
- User,
30
- VerifyMfaParams,
31
- AddMfaParams,
32
- AddMfaResult,
33
- RemoveMfaParams,
34
- } from "./client/types";
28
+ import {
29
+ type EmailType,
30
+ type MfaFactor,
31
+ type OauthConfig,
32
+ type OauthParams,
33
+ type User,
34
+ type VerifyMfaParams,
35
+ type AddMfaParams,
36
+ type AddMfaResult,
37
+ type RemoveMfaParams,
38
+ type AuthLinkingPrompt,
39
+ } from "./client/types.js";
35
40
  import { NotAuthenticatedError } from "./errors.js";
36
41
  import { SignerLogger } from "./metrics.js";
37
42
  import {
@@ -49,11 +54,13 @@ import {
49
54
  type ValidateMultiFactorsArgs,
50
55
  } from "./types.js";
51
56
  import { assertNever } from "./utils/typeAssertions.js";
57
+ import { hashAuthorization } from "viem/utils";
52
58
 
53
59
  export interface BaseAlchemySignerParams<TClient extends BaseSignerClient> {
54
60
  client: TClient;
55
61
  sessionConfig?: Omit<SessionManagerParams, "client">;
56
62
  initialError?: ErrorInfo;
63
+ initialAuthLinkingPrompt?: AuthLinkingPrompt;
57
64
  }
58
65
 
59
66
  type AlchemySignerStore = {
@@ -67,6 +74,11 @@ type AlchemySignerStore = {
67
74
  mfaFactorId?: string;
68
75
  encryptedPayload?: string;
69
76
  };
77
+ authLinkingStatus?: {
78
+ email: string;
79
+ providerName: string;
80
+ idToken: string;
81
+ };
70
82
  };
71
83
 
72
84
  type UnpackedSignature = {
@@ -115,6 +127,7 @@ export abstract class BaseAlchemySigner<TClient extends BaseSignerClient>
115
127
  client,
116
128
  sessionConfig,
117
129
  initialError,
130
+ initialAuthLinkingPrompt,
118
131
  }: BaseAlchemySignerParams<TClient>) {
119
132
  this.inner = client;
120
133
  this.store = createStore(
@@ -143,6 +156,9 @@ export abstract class BaseAlchemySigner<TClient extends BaseSignerClient>
143
156
  // then initialize so that we can catch those events
144
157
  this.sessionManager.initialize();
145
158
  this.config = this.fetchConfig();
159
+ if (initialAuthLinkingPrompt) {
160
+ this.setAuthLinkingPrompt(initialAuthLinkingPrompt);
161
+ }
146
162
  }
147
163
 
148
164
  /**
@@ -161,52 +177,64 @@ export abstract class BaseAlchemySigner<TClient extends BaseSignerClient>
161
177
  // is fired. In the Client and SessionManager we use EventEmitter because it's easier to handle internally
162
178
  switch (event) {
163
179
  case "connected":
164
- return this.store.subscribe(
180
+ return subscribeWithDelayedFireImmediately(
181
+ this.store,
165
182
  ({ status }) => status,
166
183
  (status) =>
167
184
  status === AlchemySignerStatus.CONNECTED &&
168
185
  (listener as AlchemySignerEvents["connected"])(
169
186
  this.store.getState().user!
170
- ),
171
- { fireImmediately: true }
187
+ )
172
188
  );
173
189
  case "disconnected":
174
- return this.store.subscribe(
190
+ return subscribeWithDelayedFireImmediately(
191
+ this.store,
175
192
  ({ status }) => status,
176
193
  (status) =>
177
194
  status === AlchemySignerStatus.DISCONNECTED &&
178
- (listener as AlchemySignerEvents["disconnected"])(),
179
- { fireImmediately: true }
195
+ (listener as AlchemySignerEvents["disconnected"])()
180
196
  );
181
197
  case "statusChanged":
182
- return this.store.subscribe(
198
+ return subscribeWithDelayedFireImmediately(
199
+ this.store,
183
200
  ({ status }) => status,
184
- listener as AlchemySignerEvents["statusChanged"],
185
- { fireImmediately: true }
201
+ listener as AlchemySignerEvents["statusChanged"]
186
202
  );
187
203
  case "errorChanged":
188
- return this.store.subscribe(
204
+ return subscribeWithDelayedFireImmediately(
205
+ this.store,
189
206
  ({ error }) => error,
190
207
  (error) =>
191
208
  (listener as AlchemySignerEvents["errorChanged"])(
192
209
  error ?? undefined
193
- ),
194
- { fireImmediately: true }
210
+ )
195
211
  );
196
212
  case "newUserSignup":
197
- return this.store.subscribe(
213
+ return subscribeWithDelayedFireImmediately(
214
+ this.store,
198
215
  ({ isNewUser }) => isNewUser,
199
216
  (isNewUser) => {
200
217
  if (isNewUser) (listener as AlchemySignerEvents["newUserSignup"])();
201
- },
202
- { fireImmediately: true }
218
+ }
203
219
  );
204
220
  case "mfaStatusChanged":
205
- return this.store.subscribe(
221
+ return subscribeWithDelayedFireImmediately(
222
+ this.store,
206
223
  ({ mfaStatus }) => mfaStatus,
207
224
  (mfaStatus) =>
208
- (listener as AlchemySignerEvents["mfaStatusChanged"])(mfaStatus),
209
- { fireImmediately: true }
225
+ (listener as AlchemySignerEvents["mfaStatusChanged"])(mfaStatus)
226
+ );
227
+ case "emailAuthLinkingRequired":
228
+ return subscribeWithDelayedFireImmediately(
229
+ this.store,
230
+ ({ authLinkingStatus }) => authLinkingStatus,
231
+ (authLinkingStatus) => {
232
+ if (authLinkingStatus) {
233
+ (listener as AlchemySignerEvents["emailAuthLinkingRequired"])(
234
+ authLinkingStatus.email
235
+ );
236
+ }
237
+ }
210
238
  );
211
239
  default:
212
240
  assertNever(event, `Unknown event type ${event}`);
@@ -600,12 +628,12 @@ export abstract class BaseAlchemySigner<TClient extends BaseSignerClient>
600
628
  * });
601
629
  * ```
602
630
  *
603
- * @param {Authorization<number, false>} unsignedAuthorization the authorization to be signed
604
- * @returns {Promise<Authorization<number, true>> | undefined} a promise that resolves to the authorization with the signature
631
+ * @param {AuthorizationRequest<number>} unsignedAuthorization the authorization to be signed
632
+ * @returns {Promise<SignedAuthorization<number>> | undefined} a promise that resolves to the authorization with the signature
605
633
  */
606
634
  signAuthorization: (
607
- unsignedAuthorization: Authorization<number, false>
608
- ) => Promise<Authorization<number, true>> = SignerLogger.profiled(
635
+ unsignedAuthorization: AuthorizationRequest<number>
636
+ ) => Promise<SignedAuthorization<number>> = SignerLogger.profiled(
609
637
  "BaseAlchemySigner.signAuthorization",
610
638
  async (unsignedAuthorization) => {
611
639
  const hashedAuthorization = hashAuthorization(unsignedAuthorization);
@@ -613,7 +641,14 @@ export abstract class BaseAlchemySigner<TClient extends BaseSignerClient>
613
641
  hashedAuthorization
614
642
  );
615
643
  const signature = this.unpackSignRawMessageBytes(signedAuthorizationHex);
616
- return { ...unsignedAuthorization, ...signature };
644
+ const { address, contractAddress, ...unsignedAuthorizationRest } =
645
+ unsignedAuthorization;
646
+
647
+ return {
648
+ ...unsignedAuthorizationRest,
649
+ ...signature,
650
+ address: address ?? contractAddress,
651
+ };
617
652
  }
618
653
  );
619
654
 
@@ -855,25 +890,19 @@ export abstract class BaseAlchemySigner<TClient extends BaseSignerClient>
855
890
  params.redirectParams
856
891
  );
857
892
 
858
- this.sessionManager.setTemporarySession({
859
- orgId,
860
- isNewUser,
861
- });
893
+ this.setAwaitingEmailAuth({ orgId, otpId, isNewUser });
862
894
 
863
- this.store.setState({
864
- status: AlchemySignerStatus.AWAITING_EMAIL_AUTH,
865
- otpId,
866
- error: null,
867
- });
895
+ // Clear the auth linking status if the email has changed. This would mean
896
+ // that the previously initiated social login is not associated with the
897
+ // email which is now being used to login.
898
+ const { authLinkingStatus } = this.store.getState();
899
+ if (authLinkingStatus && authLinkingStatus.email !== params.email) {
900
+ this.store.setState({ authLinkingStatus: undefined });
901
+ }
868
902
 
869
903
  // We wait for the session manager to emit a connected event if
870
904
  // cross tab sessions are permitted
871
- return new Promise<User>((resolve) => {
872
- const removeListener = this.sessionManager.on("connected", (session) => {
873
- resolve(session.user);
874
- removeListener();
875
- });
876
- });
905
+ return this.waitForConnected();
877
906
  };
878
907
 
879
908
  private authenticateWithPasskey = async (
@@ -919,15 +948,20 @@ export abstract class BaseAlchemySigner<TClient extends BaseSignerClient>
919
948
  private authenticateWithOauth = async (
920
949
  args: Extract<AuthParams, { type: "oauth" }>
921
950
  ): Promise<User> => {
951
+ this.store.setState({ authLinkingStatus: undefined });
922
952
  const params: OauthParams = {
923
953
  ...args,
924
954
  expirationSeconds: this.getExpirationSeconds(),
925
955
  };
926
956
  if (params.mode === "redirect") {
927
957
  return this.inner.oauthWithRedirect(params);
928
- } else {
929
- return this.inner.oauthWithPopup(params);
930
958
  }
959
+ const result = await this.inner.oauthWithPopup(params);
960
+ if (!isAuthLinkingPrompt(result)) {
961
+ return result;
962
+ }
963
+ this.setAuthLinkingPrompt(result);
964
+ return this.waitForConnected();
931
965
  };
932
966
 
933
967
  private authenticateWithOtp = async (
@@ -953,16 +987,7 @@ export abstract class BaseAlchemySigner<TClient extends BaseSignerClient>
953
987
 
954
988
  if (response.mfaRequired) {
955
989
  this.handleMfaRequired(response.encryptedPayload, response.multiFactors);
956
-
957
- return new Promise<User>((resolve) => {
958
- const removeListener = this.sessionManager.on(
959
- "connected",
960
- (session) => {
961
- resolve(session.user);
962
- removeListener();
963
- }
964
- );
965
- });
990
+ return this.waitForConnected();
966
991
  }
967
992
 
968
993
  const user = await this.inner.completeAuthWithBundle({
@@ -980,9 +1005,39 @@ export abstract class BaseAlchemySigner<TClient extends BaseSignerClient>
980
1005
  });
981
1006
  }
982
1007
 
1008
+ const { authLinkingStatus } = this.store.getState();
1009
+ if (authLinkingStatus) {
1010
+ (async () => {
1011
+ this.inner.addOauthProvider({
1012
+ providerName: authLinkingStatus.providerName,
1013
+ oidcToken: authLinkingStatus.idToken,
1014
+ });
1015
+ })();
1016
+ }
1017
+
983
1018
  return user;
984
1019
  };
985
1020
 
1021
+ private setAwaitingEmailAuth = ({
1022
+ orgId,
1023
+ otpId,
1024
+ isNewUser,
1025
+ }: {
1026
+ orgId: string;
1027
+ otpId?: string;
1028
+ isNewUser?: boolean;
1029
+ }): void => {
1030
+ this.sessionManager.setTemporarySession({
1031
+ orgId,
1032
+ isNewUser,
1033
+ });
1034
+ this.store.setState({
1035
+ status: AlchemySignerStatus.AWAITING_EMAIL_AUTH,
1036
+ otpId,
1037
+ error: null,
1038
+ });
1039
+ };
1040
+
986
1041
  private handleOauthReturn = ({
987
1042
  bundle,
988
1043
  orgId,
@@ -1407,6 +1462,30 @@ export abstract class BaseAlchemySigner<TClient extends BaseSignerClient>
1407
1462
  protected fetchConfig = async (): Promise<SignerConfig> => {
1408
1463
  return this.inner.request("/v1/signer-config", {});
1409
1464
  };
1465
+
1466
+ private setAuthLinkingPrompt = (prompt: AuthLinkingPrompt) => {
1467
+ this.setAwaitingEmailAuth({
1468
+ orgId: prompt.orgId,
1469
+ otpId: prompt.otpId,
1470
+ isNewUser: false,
1471
+ });
1472
+ this.store.setState({
1473
+ authLinkingStatus: {
1474
+ email: prompt.email,
1475
+ providerName: prompt.providerName,
1476
+ idToken: prompt.idToken,
1477
+ },
1478
+ });
1479
+ };
1480
+
1481
+ private waitForConnected = (): Promise<User> => {
1482
+ return new Promise<User>((resolve) => {
1483
+ const removeListener = this.sessionManager.on("connected", (session) => {
1484
+ resolve(session.user);
1485
+ removeListener();
1486
+ });
1487
+ });
1488
+ };
1410
1489
  }
1411
1490
 
1412
1491
  function toErrorInfo(error: unknown): ErrorInfo {
@@ -1414,3 +1493,52 @@ function toErrorInfo(error: unknown): ErrorInfo {
1414
1493
  ? { name: error.name, message: error.message }
1415
1494
  : { name: "Error", message: "Unknown error" };
1416
1495
  }
1496
+
1497
+ // eslint-disable-next-line jsdoc/require-param, jsdoc/require-returns
1498
+ /**
1499
+ * Zustand's `fireImmediately` option calls the listener before
1500
+ * `store.subscribe` has returned, which breaks listeners which call
1501
+ * unsubscribe, e.g.
1502
+ *
1503
+ * ```ts
1504
+ * const unsubscribe = store.subscribe(
1505
+ * selector,
1506
+ * (update) => {
1507
+ * handleUpdate(update);
1508
+ * unsubscribe();
1509
+ * },
1510
+ * { fireImmediately: true },
1511
+ * )
1512
+ * ```
1513
+ *
1514
+ * since `unsubscribe` is still undefined at the time the listener is called. To
1515
+ * prevent this, if the listener triggers before `subscribe` has returned, delay
1516
+ * the callback to a later run of the event loop.
1517
+ */
1518
+ function subscribeWithDelayedFireImmediately<T>(
1519
+ store: InternalStore,
1520
+ selector: (state: AlchemySignerStore) => T,
1521
+ listener: (selectedState: T, previousSelectedState: T) => void
1522
+ ): () => void {
1523
+ let subscribeHasReturned = false;
1524
+ const unsubscribe = store.subscribe(
1525
+ selector,
1526
+ (...args) => {
1527
+ if (subscribeHasReturned) {
1528
+ listener(...args);
1529
+ } else {
1530
+ setTimeout(() => listener(...args), 0);
1531
+ }
1532
+ },
1533
+ { fireImmediately: true }
1534
+ );
1535
+ subscribeHasReturned = true;
1536
+ return unsubscribe;
1537
+ }
1538
+
1539
+ function isAuthLinkingPrompt(result: unknown): result is AuthLinkingPrompt {
1540
+ return (
1541
+ (result as AuthLinkingPrompt)?.status ===
1542
+ "ACCOUNT_LINKING_CONFIRMATION_REQUIRED"
1543
+ );
1544
+ }
@@ -34,6 +34,8 @@ import type {
34
34
  VerifyMfaParams,
35
35
  SubmitOtpCodeResponse,
36
36
  ValidateMultiFactorsParams,
37
+ AuthLinkingPrompt,
38
+ AddOauthProviderParams,
37
39
  } from "./types.js";
38
40
  import { VERSION } from "../version.js";
39
41
 
@@ -99,13 +101,13 @@ export abstract class BaseSignerClient<TExportWalletParams = unknown> {
99
101
  }
100
102
 
101
103
  protected set user(user: User | undefined) {
102
- if (user && !this._user) {
104
+ const previousUser = this._user;
105
+ this._user = user;
106
+ if (user && !previousUser) {
103
107
  this.eventEmitter.emit("connected", user);
104
- } else if (!user && this._user) {
108
+ } else if (!user && previousUser) {
105
109
  this.eventEmitter.emit("disconnected");
106
110
  }
107
-
108
- this._user = user;
109
111
  }
110
112
 
111
113
  /**
@@ -160,11 +162,11 @@ export abstract class BaseSignerClient<TExportWalletParams = unknown> {
160
162
 
161
163
  public abstract oauthWithRedirect(
162
164
  args: Extract<OauthParams, { mode: "redirect" }>
163
- ): Promise<User | never>;
165
+ ): Promise<User>;
164
166
 
165
167
  public abstract oauthWithPopup(
166
168
  args: Extract<OauthParams, { mode: "popup" }>
167
- ): Promise<User>;
169
+ ): Promise<User | AuthLinkingPrompt>;
168
170
 
169
171
  public abstract submitOtpCode(
170
172
  args: Omit<OtpParams, "targetPublicKey">
@@ -266,6 +268,32 @@ export abstract class BaseSignerClient<TExportWalletParams = unknown> {
266
268
  };
267
269
  };
268
270
 
271
+ /**
272
+ * Adds an OAuth provider for the authenticated user using the provided parameters. Throws an error if the user is not authenticated.
273
+ *
274
+ * @param {AddOauthProviderParams} params The parameters for adding an OAuth provider, including `providerName` and `oidcToken`.
275
+ * @throws {NotAuthenticatedError} Throws if the user is not authenticated.
276
+ * @returns {Promise<void>} A Promise that resolves when the OAuth provider is added.
277
+ */
278
+ public addOauthProvider = async (
279
+ params: AddOauthProviderParams
280
+ ): Promise<void> => {
281
+ if (!this.user) {
282
+ throw new NotAuthenticatedError();
283
+ }
284
+ const { providerName, oidcToken } = params;
285
+ const stampedRequest = await this.turnkeyClient.stampCreateOauthProviders({
286
+ type: "ACTIVITY_TYPE_CREATE_OAUTH_PROVIDERS",
287
+ timestampMs: Date.now().toString(),
288
+ organizationId: this.user.orgId,
289
+ parameters: {
290
+ userId: this.user.userId,
291
+ oauthProviders: [{ providerName, oidcToken }],
292
+ },
293
+ });
294
+ await this.request("/v1/add-oauth-provider", { stampedRequest });
295
+ };
296
+
269
297
  /**
270
298
  * Retrieves the current user or fetches the user information if not already available.
271
299
  *
@@ -374,7 +402,7 @@ export abstract class BaseSignerClient<TExportWalletParams = unknown> {
374
402
  throw new Error("User must be authenticated to create api key");
375
403
  }
376
404
  const resp = await this.turnkeyClient.createApiKeys({
377
- type: "ACTIVITY_TYPE_CREATE_API_KEYS",
405
+ type: "ACTIVITY_TYPE_CREATE_API_KEYS_V2",
378
406
  timestampMs: new Date().getTime().toString(),
379
407
  organizationId: this.user.orgId,
380
408
  parameters: {
@@ -382,6 +410,7 @@ export abstract class BaseSignerClient<TExportWalletParams = unknown> {
382
410
  {
383
411
  apiKeyName: params.name,
384
412
  publicKey: params.publicKey,
413
+ curveType: "API_KEY_CURVE_P256",
385
414
  expirationSeconds: params.expirationSec.toString(),
386
415
  },
387
416
  ],
@@ -18,6 +18,7 @@ import type {
18
18
  OtpParams,
19
19
  User,
20
20
  SubmitOtpCodeResponse,
21
+ AuthLinkingPrompt,
21
22
  } from "./types.js";
22
23
  import { MfaRequiredError } from "../errors.js";
23
24
  import { parseMfaError } from "../utils/parseMfaError.js";
@@ -531,7 +532,7 @@ export class AlchemySignerWebClient extends BaseSignerClient<ExportWalletParams>
531
532
  */
532
533
  public override oauthWithPopup = async (
533
534
  args: Extract<AuthParams, { type: "oauth"; mode: "popup" }>
534
- ): Promise<User> => {
535
+ ): Promise<User | AuthLinkingPrompt> => {
535
536
  const turnkeyPublicKey = await this.initIframeStamper();
536
537
  const oauthParams = args;
537
538
  const providerUrl = await this.getOauthProviderUrl({
@@ -551,33 +552,55 @@ export class AlchemySignerWebClient extends BaseSignerClient<ExportWalletParams>
551
552
  return;
552
553
  }
553
554
  const {
555
+ alchemyStatus: status,
554
556
  alchemyBundle: bundle,
555
557
  alchemyOrgId: orgId,
556
558
  alchemyIdToken: idToken,
557
559
  alchemyIsSignup: isSignup,
558
560
  alchemyError,
561
+ alchemyOtpId: otpId,
562
+ alchemyEmail: email,
563
+ alchemyAuthProvider: providerName,
559
564
  } = event.data;
560
- if (bundle && orgId && idToken) {
561
- cleanup();
562
- popup?.close();
563
- this.completeAuthWithBundle({
564
- bundle,
565
- orgId,
566
- connectedEventName: "connectedOauth",
567
- idToken,
568
- authenticatingType: "oauth",
569
- }).then((user) => {
570
- if (isSignup) {
571
- eventEmitter.emit("newUserSignup");
572
- }
573
-
574
- resolve(user);
575
- }, reject);
576
- } else if (alchemyError) {
565
+ if (alchemyError) {
577
566
  cleanup();
578
567
  popup?.close();
579
568
  reject(new OauthFailedError(alchemyError));
580
569
  }
570
+ if (!status) {
571
+ // This message isn't meant for us.
572
+ return;
573
+ }
574
+ cleanup();
575
+ popup?.close();
576
+ switch (status) {
577
+ case "SUCCESS":
578
+ this.completeAuthWithBundle({
579
+ bundle,
580
+ orgId,
581
+ connectedEventName: "connectedOauth",
582
+ idToken,
583
+ authenticatingType: "oauth",
584
+ }).then((user) => {
585
+ if (isSignup) {
586
+ eventEmitter.emit("newUserSignup");
587
+ }
588
+ resolve(user);
589
+ }, reject);
590
+ break;
591
+ case "ACCOUNT_LINKING_CONFIRMATION_REQUIRED":
592
+ resolve({
593
+ status,
594
+ idToken,
595
+ email,
596
+ providerName,
597
+ otpId,
598
+ orgId,
599
+ } satisfies AuthLinkingPrompt);
600
+ break;
601
+ default:
602
+ reject(new Error(`Unknown status: ${status}`));
603
+ }
581
604
  };
582
605
 
583
606
  window.addEventListener("message", handleMessage);
@@ -171,6 +171,13 @@ export type SignerEndpoints = [
171
171
  signature: Hex;
172
172
  };
173
173
  },
174
+ {
175
+ Route: "/v1/add-oauth-provider";
176
+ Body: {
177
+ stampedRequest: TSignedRequest;
178
+ };
179
+ Response: void;
180
+ },
174
181
  {
175
182
  Route: "/v1/prepare-oauth";
176
183
  Body: {
@@ -262,6 +269,15 @@ export type GetWebAuthnAttestationResult = {
262
269
  authenticatorUserId: ArrayBuffer;
263
270
  };
264
271
 
272
+ export type AuthLinkingPrompt = {
273
+ status: "ACCOUNT_LINKING_CONFIRMATION_REQUIRED";
274
+ idToken: string;
275
+ email: string;
276
+ providerName: string;
277
+ otpId: string;
278
+ orgId: string;
279
+ };
280
+
265
281
  export type OauthState = {
266
282
  authProviderId: string;
267
283
  isCustomProvider?: boolean;
@@ -324,6 +340,11 @@ export type SubmitOtpCodeResponse =
324
340
  | { bundle: string; mfaRequired: false }
325
341
  | { mfaRequired: true; encryptedPayload: string; multiFactors: MfaFactor[] };
326
342
 
343
+ export type AddOauthProviderParams = {
344
+ providerName: string;
345
+ oidcToken: string;
346
+ };
347
+
327
348
  export type experimental_CreateApiKeyParams = {
328
349
  name: string;
329
350
  publicKey: string;
package/src/signer.ts CHANGED
@@ -5,6 +5,7 @@ import {
5
5
  AlchemySignerWebClient,
6
6
  } from "./client/index.js";
7
7
  import type {
8
+ AuthLinkingPrompt,
8
9
  CredentialCreationOptionOverrides,
9
10
  VerifyMfaParams,
10
11
  } from "./client/types.js";
@@ -141,20 +142,28 @@ export class AlchemyWebSigner extends BaseAlchemySigner<AlchemySignerWebClient>
141
142
  emailBundle: "bundle",
142
143
  // We don't need this, but we still want to remove it from the URL.
143
144
  emailOrgId: "orgId",
145
+ status: "alchemy-status",
144
146
  oauthBundle: "alchemy-bundle",
145
147
  oauthOrgId: "alchemy-org-id",
146
- oauthError: "alchemy-error",
147
148
  idToken: "alchemy-id-token",
148
149
  isSignup: "aa-is-signup",
150
+ otpId: "alchemy-otp-id",
151
+ email: "alchemy-email",
152
+ authProvider: "alchemy-auth-provider",
153
+ oauthError: "alchemy-error",
149
154
  };
150
155
 
151
156
  const {
152
157
  emailBundle,
158
+ status,
153
159
  oauthBundle,
154
160
  oauthOrgId,
155
- oauthError,
156
161
  idToken,
157
162
  isSignup,
163
+ otpId,
164
+ email,
165
+ authProvider,
166
+ oauthError,
158
167
  } = getAndRemoveQueryParams(qpStructure);
159
168
 
160
169
  if (!AlchemyWebSigner.replaceStateFilterInstalled) {
@@ -167,7 +176,31 @@ export class AlchemyWebSigner extends BaseAlchemySigner<AlchemySignerWebClient>
167
176
  ? { name: "OauthError", message: oauthError }
168
177
  : undefined;
169
178
 
170
- super({ client, sessionConfig, initialError });
179
+ const initialAuthLinkingPrompt: AuthLinkingPrompt | undefined = (() => {
180
+ if (status !== "ACCOUNT_LINKING_CONFIRMATION_REQUIRED") {
181
+ return undefined;
182
+ }
183
+ if (
184
+ idToken == null ||
185
+ email == null ||
186
+ authProvider == null ||
187
+ otpId == null ||
188
+ oauthOrgId == null
189
+ ) {
190
+ console.error("Missing required query params for auth linking prompt");
191
+ return undefined;
192
+ }
193
+ return {
194
+ status,
195
+ idToken,
196
+ email,
197
+ providerName: authProvider,
198
+ otpId,
199
+ orgId: oauthOrgId,
200
+ };
201
+ })();
202
+
203
+ super({ client, sessionConfig, initialError, initialAuthLinkingPrompt });
171
204
 
172
205
  const isNewUser = isSignup === "true";
173
206