@account-kit/signer 4.0.0-beta.1 → 4.0.0-beta.10

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 (77) hide show
  1. package/dist/esm/base.d.ts +42 -5
  2. package/dist/esm/base.js +186 -29
  3. package/dist/esm/base.js.map +1 -1
  4. package/dist/esm/client/base.d.ts +22 -4
  5. package/dist/esm/client/base.js +36 -2
  6. package/dist/esm/client/base.js.map +1 -1
  7. package/dist/esm/client/index.d.ts +108 -7
  8. package/dist/esm/client/index.js +282 -14
  9. package/dist/esm/client/index.js.map +1 -1
  10. package/dist/esm/client/types.d.ts +31 -1
  11. package/dist/esm/client/types.js.map +1 -1
  12. package/dist/esm/index.d.ts +1 -1
  13. package/dist/esm/index.js +1 -1
  14. package/dist/esm/index.js.map +1 -1
  15. package/dist/esm/metrics.d.ts +17 -0
  16. package/dist/esm/metrics.js +7 -0
  17. package/dist/esm/metrics.js.map +1 -0
  18. package/dist/esm/oauth.d.ts +19 -0
  19. package/dist/esm/oauth.js +26 -0
  20. package/dist/esm/oauth.js.map +1 -0
  21. package/dist/esm/session/manager.d.ts +3 -2
  22. package/dist/esm/session/manager.js +29 -15
  23. package/dist/esm/session/manager.js.map +1 -1
  24. package/dist/esm/session/types.d.ts +1 -1
  25. package/dist/esm/session/types.js.map +1 -1
  26. package/dist/esm/signer.d.ts +52 -7
  27. package/dist/esm/signer.js +46 -3
  28. package/dist/esm/signer.js.map +1 -1
  29. package/dist/esm/types.d.ts +8 -1
  30. package/dist/esm/types.js +3 -1
  31. package/dist/esm/types.js.map +1 -1
  32. package/dist/esm/utils/typeAssertions.d.ts +1 -0
  33. package/dist/esm/utils/typeAssertions.js +4 -0
  34. package/dist/esm/utils/typeAssertions.js.map +1 -0
  35. package/dist/esm/version.d.ts +1 -1
  36. package/dist/esm/version.js +1 -1
  37. package/dist/esm/version.js.map +1 -1
  38. package/dist/types/base.d.ts +42 -5
  39. package/dist/types/base.d.ts.map +1 -1
  40. package/dist/types/client/base.d.ts +22 -4
  41. package/dist/types/client/base.d.ts.map +1 -1
  42. package/dist/types/client/index.d.ts +108 -7
  43. package/dist/types/client/index.d.ts.map +1 -1
  44. package/dist/types/client/types.d.ts +31 -1
  45. package/dist/types/client/types.d.ts.map +1 -1
  46. package/dist/types/index.d.ts +1 -1
  47. package/dist/types/index.d.ts.map +1 -1
  48. package/dist/types/metrics.d.ts +18 -0
  49. package/dist/types/metrics.d.ts.map +1 -0
  50. package/dist/types/oauth.d.ts +20 -0
  51. package/dist/types/oauth.d.ts.map +1 -0
  52. package/dist/types/session/manager.d.ts +3 -2
  53. package/dist/types/session/manager.d.ts.map +1 -1
  54. package/dist/types/session/types.d.ts +1 -1
  55. package/dist/types/session/types.d.ts.map +1 -1
  56. package/dist/types/signer.d.ts +52 -7
  57. package/dist/types/signer.d.ts.map +1 -1
  58. package/dist/types/types.d.ts +8 -1
  59. package/dist/types/types.d.ts.map +1 -1
  60. package/dist/types/utils/typeAssertions.d.ts +2 -0
  61. package/dist/types/utils/typeAssertions.d.ts.map +1 -0
  62. package/dist/types/version.d.ts +1 -1
  63. package/dist/types/version.d.ts.map +1 -1
  64. package/package.json +6 -5
  65. package/src/base.ts +260 -65
  66. package/src/client/base.ts +49 -4
  67. package/src/client/index.ts +317 -20
  68. package/src/client/types.ts +33 -1
  69. package/src/index.ts +5 -1
  70. package/src/metrics.ts +23 -0
  71. package/src/oauth.ts +36 -0
  72. package/src/session/manager.ts +46 -19
  73. package/src/session/types.ts +1 -1
  74. package/src/signer.ts +91 -4
  75. package/src/types.ts +9 -1
  76. package/src/utils/typeAssertions.ts +3 -0
  77. package/src/version.ts +1 -1
package/src/base.ts CHANGED
@@ -4,10 +4,14 @@ import {
4
4
  hashTypedData,
5
5
  keccak256,
6
6
  serializeTransaction,
7
- type CustomSource,
7
+ type GetTransactionType,
8
8
  type Hex,
9
+ type IsNarrowable,
9
10
  type LocalAccount,
11
+ type SerializeTransactionFn,
10
12
  type SignableMessage,
13
+ type TransactionSerializable,
14
+ type TransactionSerialized,
11
15
  type TypedData,
12
16
  type TypedDataDefinition,
13
17
  } from "viem";
@@ -16,8 +20,9 @@ import type { Mutate, StoreApi } from "zustand";
16
20
  import { subscribeWithSelector } from "zustand/middleware";
17
21
  import { createStore } from "zustand/vanilla";
18
22
  import type { BaseSignerClient } from "./client/base";
19
- import type { User } from "./client/types";
23
+ import type { OauthConfig, OauthParams, User } from "./client/types";
20
24
  import { NotAuthenticatedError } from "./errors.js";
25
+ import { SignerLogger } from "./metrics.js";
21
26
  import {
22
27
  SessionManager,
23
28
  type SessionManagerParams,
@@ -27,16 +32,20 @@ import {
27
32
  AlchemySignerStatus,
28
33
  type AlchemySignerEvent,
29
34
  type AlchemySignerEvents,
35
+ type ErrorInfo,
30
36
  } from "./types.js";
37
+ import { assertNever } from "./utils/typeAssertions.js";
31
38
 
32
39
  export interface BaseAlchemySignerParams<TClient extends BaseSignerClient> {
33
40
  client: TClient;
34
41
  sessionConfig?: Omit<SessionManagerParams, "client">;
42
+ initialError?: ErrorInfo;
35
43
  }
36
44
 
37
45
  type AlchemySignerStore = {
38
46
  user: User | null;
39
47
  status: AlchemySignerStatus;
48
+ error: ErrorInfo | null;
40
49
  };
41
50
 
42
51
  type InternalStore = Mutate<
@@ -64,8 +73,13 @@ export abstract class BaseAlchemySigner<TClient extends BaseSignerClient>
64
73
  * @param {BaseAlchemySignerParams<TClient>} param0 Object containing the client and session configuration
65
74
  * @param {TClient} param0.client The client instance to be used internally
66
75
  * @param {SessionConfig} param0.sessionConfig Configuration for managing sessions
76
+ * @param {ErrorInfo | undefined} param0.initialError Error already present on the signer when initialized, if any
67
77
  */
68
- constructor({ client, sessionConfig }: BaseAlchemySignerParams<TClient>) {
78
+ constructor({
79
+ client,
80
+ sessionConfig,
81
+ initialError,
82
+ }: BaseAlchemySignerParams<TClient>) {
69
83
  this.inner = client;
70
84
  this.store = createStore(
71
85
  subscribeWithSelector(
@@ -73,6 +87,7 @@ export abstract class BaseAlchemySigner<TClient extends BaseSignerClient>
73
87
  ({
74
88
  user: null,
75
89
  status: AlchemySignerStatus.INITIALIZING,
90
+ error: initialError ?? null,
76
91
  } satisfies AlchemySignerStore)
77
92
  )
78
93
  );
@@ -83,15 +98,6 @@ export abstract class BaseAlchemySigner<TClient extends BaseSignerClient>
83
98
  ...sessionConfig,
84
99
  client: this.inner,
85
100
  });
86
- this.store = createStore(
87
- subscribeWithSelector(
88
- () =>
89
- ({
90
- user: null,
91
- status: AlchemySignerStatus.INITIALIZING,
92
- } satisfies AlchemySignerStore)
93
- )
94
- );
95
101
  // register listeners first
96
102
  this.registerListeners();
97
103
  // then initialize so that we can catch those events
@@ -137,11 +143,51 @@ export abstract class BaseAlchemySigner<TClient extends BaseSignerClient>
137
143
  listener as AlchemySignerEvents["statusChanged"],
138
144
  { fireImmediately: true }
139
145
  );
146
+ case "errorChanged":
147
+ return this.store.subscribe(
148
+ ({ error }) => error,
149
+ (error) =>
150
+ (listener as AlchemySignerEvents["errorChanged"])(
151
+ error ?? undefined
152
+ ),
153
+ { fireImmediately: true }
154
+ );
140
155
  default:
141
- throw new Error(`Uknown event type ${event}`);
156
+ assertNever(event, `Unknown event type ${event}`);
142
157
  }
143
158
  };
144
159
 
160
+ /**
161
+ * Prepares the config needed to use popup-based OAuth login. This must be
162
+ * called before calling `.authenticate` with params `{ type: "oauth", mode:
163
+ * "popup" }`, and is recommended to be called on page load.
164
+ *
165
+ * This method exists because browsers may prevent popups from opening unless
166
+ * triggered by user interaction, and so the OAuth config must already have
167
+ * been fetched at the time a user clicks a social login button.
168
+ *
169
+ * @example
170
+ * ```ts
171
+ * import { AlchemyWebSigner } from "@account-kit/signer";
172
+ *
173
+ * const signer = new AlchemyWebSigner({
174
+ * client: {
175
+ * connection: {
176
+ * rpcUrl: "/api/rpc",
177
+ * },
178
+ * iframeConfig: {
179
+ * iframeContainerId: "alchemy-signer-iframe-container",
180
+ * },
181
+ * },
182
+ * });
183
+ *
184
+ * await signer.preparePopupOauth();
185
+ * ```
186
+ * @returns {Promise<OauthConfig>} the config which must be loaded before
187
+ * using popup-based OAuth
188
+ */
189
+ preparePopupOauth = (): Promise<OauthConfig> => this.inner.initOauth();
190
+
145
191
  /**
146
192
  * Authenticate a user with either an email or a passkey and create a session for that user
147
193
  *
@@ -169,12 +215,70 @@ export abstract class BaseAlchemySigner<TClient extends BaseSignerClient>
169
215
  * @param {AuthParams} params - undefined if passkey login, otherwise an object with email and bundle to resolve
170
216
  * @returns {Promise<User>} the user that was authenticated
171
217
  */
172
- authenticate: (params: AuthParams) => Promise<User> = async (params) => {
173
- if (params.type === "email") {
174
- return this.authenticateWithEmail(params);
218
+ authenticate: (params: AuthParams) => Promise<User> = SignerLogger.profiled(
219
+ "BaseAlchemySigner.authenticate",
220
+ async (params) => {
221
+ const { type } = params;
222
+ const result = (() => {
223
+ switch (type) {
224
+ case "email":
225
+ return this.authenticateWithEmail(params);
226
+ case "passkey":
227
+ return this.authenticateWithPasskey(params);
228
+ case "oauth":
229
+ return this.authenticateWithOauth(params);
230
+ case "oauthReturn":
231
+ return this.handleOauthReturn(params);
232
+ default:
233
+ assertNever(type, `Unknown auth type: ${type}`);
234
+ }
235
+ })();
236
+
237
+ this.trackAuthenticateType(params);
238
+
239
+ return result.catch((error) => {
240
+ this.store.setState({ error: toErrorInfo(error) });
241
+ throw error;
242
+ });
243
+ }
244
+ );
245
+
246
+ private trackAuthenticateType = (params: AuthParams) => {
247
+ const { type } = params;
248
+ switch (type) {
249
+ case "email": {
250
+ // we just want to track the start of email auth
251
+ if ("bundle" in params) return;
252
+ SignerLogger.trackEvent({
253
+ name: "signer_authnticate",
254
+ data: { authType: "email" },
255
+ });
256
+ return;
257
+ }
258
+ case "passkey": {
259
+ const isAnon = !("email" in params) && params.createNew == null;
260
+ SignerLogger.trackEvent({
261
+ name: "signer_authnticate",
262
+ data: {
263
+ authType: isAnon ? "passkey_anon" : "passkey_email",
264
+ },
265
+ });
266
+ return;
267
+ }
268
+ case "oauth":
269
+ SignerLogger.trackEvent({
270
+ name: "signer_authnticate",
271
+ data: {
272
+ authType: "oauth",
273
+ provider: params.authProviderId,
274
+ },
275
+ });
276
+ break;
277
+ case "oauthReturn":
278
+ break;
279
+ default:
280
+ assertNever(type, `Unknown auth type: ${type}`);
175
281
  }
176
-
177
- return this.authenticateWithPasskey(params);
178
282
  };
179
283
 
180
284
  /**
@@ -245,11 +349,14 @@ export abstract class BaseAlchemySigner<TClient extends BaseSignerClient>
245
349
  *
246
350
  * @returns {Promise<string>} A promise that resolves to the address of the current user.
247
351
  */
248
- getAddress: () => Promise<`0x${string}`> = async () => {
249
- const { address } = await this.inner.whoami();
352
+ getAddress: () => Promise<`0x${string}`> = SignerLogger.profiled(
353
+ "BaseAlchemySigner.getAddress",
354
+ async () => {
355
+ const { address } = await this.inner.whoami();
250
356
 
251
- return address;
252
- };
357
+ return address;
358
+ }
359
+ );
253
360
 
254
361
  /**
255
362
  * Signs a raw message after hashing it.
@@ -275,13 +382,18 @@ export abstract class BaseAlchemySigner<TClient extends BaseSignerClient>
275
382
  * @param {string} msg the message to be hashed and then signed
276
383
  * @returns {Promise<string>} a promise that resolves to the signed message
277
384
  */
278
- signMessage: (msg: SignableMessage) => Promise<`0x${string}`> = async (
279
- msg
280
- ) => {
281
- const messageHash = hashMessage(msg);
385
+ signMessage: (msg: SignableMessage) => Promise<`0x${string}`> =
386
+ SignerLogger.profiled("BaseAlchemySigner.signMessage", async (msg) => {
387
+ const messageHash = hashMessage(msg);
282
388
 
283
- return this.inner.signRawMessage(messageHash);
284
- };
389
+ const result = await this.inner.signRawMessage(messageHash);
390
+
391
+ SignerLogger.trackEvent({
392
+ name: "signer_sign_message",
393
+ });
394
+
395
+ return result;
396
+ });
285
397
 
286
398
  /**
287
399
  * Signs a typed message by first hashing it and then signing the hashed message using the `signRawMessage` method.
@@ -317,11 +429,14 @@ export abstract class BaseAlchemySigner<TClient extends BaseSignerClient>
317
429
  TPrimaryType extends keyof TTypedData | "EIP712Domain" = keyof TTypedData
318
430
  >(
319
431
  params: TypedDataDefinition<TTypedData, TPrimaryType>
320
- ) => Promise<Hex> = async (params) => {
321
- const messageHash = hashTypedData(params);
432
+ ) => Promise<Hex> = SignerLogger.profiled(
433
+ "BaseAlchemySigner.signTypedData",
434
+ async (params) => {
435
+ const messageHash = hashTypedData(params);
322
436
 
323
- return this.inner.signRawMessage(messageHash);
324
- };
437
+ return this.inner.signRawMessage(messageHash);
438
+ }
439
+ );
325
440
 
326
441
  /**
327
442
  * Serializes a transaction, signs it with a raw message, and then returns the serialized transaction with the signature.
@@ -353,21 +468,41 @@ export abstract class BaseAlchemySigner<TClient extends BaseSignerClient>
353
468
  * @param {() => Hex} [args.serializer] an optional serializer function. If not provided, the default `serializeTransaction` function will be used
354
469
  * @returns {Promise<string>} a promise that resolves to the serialized transaction with the signature
355
470
  */
356
- signTransaction: CustomSource["signTransaction"] = async (tx, args) => {
357
- const serializeFn = args?.serializer ?? serializeTransaction;
358
- const serializedTx = serializeFn(tx);
359
- const signatureHex = await this.inner.signRawMessage(
360
- keccak256(serializedTx)
361
- );
471
+ signTransaction: <
472
+ serializer extends SerializeTransactionFn<TransactionSerializable> = SerializeTransactionFn<TransactionSerializable>,
473
+ transaction extends Parameters<serializer>[0] = Parameters<serializer>[0]
474
+ >(
475
+ transaction: transaction,
476
+ options?:
477
+ | {
478
+ serializer?: serializer | undefined;
479
+ }
480
+ | undefined
481
+ ) => Promise<
482
+ IsNarrowable<
483
+ TransactionSerialized<GetTransactionType<transaction>>,
484
+ Hex
485
+ > extends true
486
+ ? TransactionSerialized<GetTransactionType<transaction>>
487
+ : Hex
488
+ > = SignerLogger.profiled(
489
+ "BaseAlchemySigner.signTransaction",
490
+ async (tx, args) => {
491
+ const serializeFn = args?.serializer ?? serializeTransaction;
492
+ const serializedTx = serializeFn(tx);
493
+ const signatureHex = await this.inner.signRawMessage(
494
+ keccak256(serializedTx)
495
+ );
362
496
 
363
- const signature = {
364
- r: takeBytes(signatureHex, { count: 32 }),
365
- s: takeBytes(signatureHex, { count: 32, offset: 32 }),
366
- v: BigInt(takeBytes(signatureHex, { count: 1, offset: 64 })),
367
- };
497
+ const signature = {
498
+ r: takeBytes(signatureHex, { count: 32 }),
499
+ s: takeBytes(signatureHex, { count: 32, offset: 32 }),
500
+ v: BigInt(takeBytes(signatureHex, { count: 1, offset: 64 })),
501
+ };
368
502
 
369
- return serializeFn(tx, signature);
370
- };
503
+ return serializeFn(tx, signature);
504
+ }
505
+ );
371
506
 
372
507
  /**
373
508
  * Unauthenticated call to look up a user's organizationId by email
@@ -393,19 +528,18 @@ export abstract class BaseAlchemySigner<TClient extends BaseSignerClient>
393
528
  * @param {string} email the email to lookup
394
529
  * @returns {Promise<{orgId: string}>} the organization id for the user if they exist
395
530
  */
396
- getUser: (email: string) => Promise<{ orgId: string } | null> = async (
397
- email
398
- ) => {
399
- const result = await this.inner.lookupUserByEmail(email);
531
+ getUser: (email: string) => Promise<{ orgId: string } | null> =
532
+ SignerLogger.profiled("BaseAlchemySigner.getUser", async (email) => {
533
+ const result = await this.inner.lookupUserByEmail(email);
400
534
 
401
- if (result.orgId == null) {
402
- return null;
403
- }
535
+ if (result.orgId == null) {
536
+ return null;
537
+ }
404
538
 
405
- return {
406
- orgId: result.orgId,
407
- };
408
- };
539
+ return {
540
+ orgId: result.orgId,
541
+ };
542
+ });
409
543
 
410
544
  /**
411
545
  * Adds a passkey to the user's account
@@ -432,9 +566,9 @@ export abstract class BaseAlchemySigner<TClient extends BaseSignerClient>
432
566
  * @returns {Promise<string[]>} an array of the authenticator ids added to the user
433
567
  */
434
568
  addPasskey: (params?: CredentialCreationOptions) => Promise<string[]> =
435
- async (params) => {
569
+ SignerLogger.profiled("BaseAlchemySigner.addPasskey", async (params) => {
436
570
  return this.inner.addPasskey(params ?? {});
437
- };
571
+ });
438
572
 
439
573
  /**
440
574
  * Used to export the wallet for a given user
@@ -519,22 +653,28 @@ export abstract class BaseAlchemySigner<TClient extends BaseSignerClient>
519
653
  ): Promise<User> => {
520
654
  if ("email" in params) {
521
655
  const existingUser = await this.getUser(params.email);
656
+ const expirationSeconds = Math.floor(
657
+ this.sessionManager.expirationTimeMs / 1000
658
+ );
522
659
 
523
660
  const { orgId } = existingUser
524
661
  ? await this.inner.initEmailAuth({
525
662
  email: params.email,
526
- expirationSeconds: this.sessionManager.expirationTimeMs,
663
+ expirationSeconds,
527
664
  redirectParams: params.redirectParams,
528
665
  })
529
666
  : await this.inner.createAccount({
530
667
  type: "email",
531
668
  email: params.email,
532
- expirationSeconds: this.sessionManager.expirationTimeMs,
669
+ expirationSeconds,
533
670
  redirectParams: params.redirectParams,
534
671
  });
535
672
 
536
673
  this.sessionManager.setTemporarySession({ orgId });
537
- this.store.setState({ status: AlchemySignerStatus.AWAITING_EMAIL_AUTH });
674
+ this.store.setState({
675
+ status: AlchemySignerStatus.AWAITING_EMAIL_AUTH,
676
+ error: null,
677
+ });
538
678
 
539
679
  // We wait for the session manager to emit a connected event if
540
680
  // cross tab sessions are permitted
@@ -557,9 +697,11 @@ export abstract class BaseAlchemySigner<TClient extends BaseSignerClient>
557
697
  throw new Error("Could not find email auth init session!");
558
698
  }
559
699
 
560
- const user = await this.inner.completeEmailAuth({
700
+ const user = await this.inner.completeAuthWithBundle({
561
701
  bundle: params.bundle,
562
702
  orgId: temporarySession.orgId,
703
+ connectedEventName: "connectedEmail",
704
+ authenticatingType: "email",
563
705
  });
564
706
 
565
707
  return user;
@@ -568,7 +710,7 @@ export abstract class BaseAlchemySigner<TClient extends BaseSignerClient>
568
710
 
569
711
  private authenticateWithPasskey = async (
570
712
  args: Extract<AuthParams, { type: "passkey" }>
571
- ) => {
713
+ ): Promise<User> => {
572
714
  let user: User;
573
715
  const shouldCreateNew = async () => {
574
716
  if ("email" in args) {
@@ -604,11 +746,41 @@ export abstract class BaseAlchemySigner<TClient extends BaseSignerClient>
604
746
  return user;
605
747
  };
606
748
 
749
+ private authenticateWithOauth = async (
750
+ args: Extract<AuthParams, { type: "oauth" }>
751
+ ): Promise<User> => {
752
+ const params: OauthParams = {
753
+ ...args,
754
+ expirationSeconds: Math.floor(
755
+ this.sessionManager.expirationTimeMs / 1000
756
+ ),
757
+ };
758
+ if (params.mode === "redirect") {
759
+ return this.inner.oauthWithRedirect(params);
760
+ } else {
761
+ return this.inner.oauthWithPopup(params);
762
+ }
763
+ };
764
+
765
+ private handleOauthReturn = ({
766
+ bundle,
767
+ orgId,
768
+ idToken,
769
+ }: Extract<AuthParams, { type: "oauthReturn" }>): Promise<User> =>
770
+ this.inner.completeAuthWithBundle({
771
+ bundle,
772
+ orgId,
773
+ connectedEventName: "connectedOauth",
774
+ authenticatingType: "oauth",
775
+ idToken,
776
+ });
777
+
607
778
  private registerListeners = () => {
608
779
  this.sessionManager.on("connected", (session) => {
609
780
  this.store.setState({
610
781
  user: session.user,
611
782
  status: AlchemySignerStatus.CONNECTED,
783
+ error: null,
612
784
  });
613
785
  });
614
786
 
@@ -624,11 +796,34 @@ export abstract class BaseAlchemySigner<TClient extends BaseSignerClient>
624
796
  status: state.user
625
797
  ? AlchemySignerStatus.CONNECTED
626
798
  : AlchemySignerStatus.DISCONNECTED,
799
+ ...(state.user ? { error: null } : undefined),
627
800
  }));
628
801
  });
629
802
 
630
- this.inner.on("authenticating", () => {
631
- this.store.setState({ status: AlchemySignerStatus.AUTHENTICATING });
803
+ this.inner.on("authenticating", ({ type }) => {
804
+ const status = (() => {
805
+ switch (type) {
806
+ case "email":
807
+ return AlchemySignerStatus.AUTHENTICATING_EMAIL;
808
+ case "passkey":
809
+ return AlchemySignerStatus.AUTHENTICATING_PASSKEY;
810
+ case "oauth":
811
+ return AlchemySignerStatus.AUTHENTICATING_OAUTH;
812
+ default:
813
+ assertNever(type, "unhandled authenticating type");
814
+ }
815
+ })();
816
+
817
+ this.store.setState({
818
+ status,
819
+ error: null,
820
+ });
632
821
  });
633
822
  };
634
823
  }
824
+
825
+ function toErrorInfo(error: unknown): ErrorInfo {
826
+ return error instanceof Error
827
+ ? { name: error.name, message: error.message }
828
+ : { name: "Error", message: "Unknown error" };
829
+ }
@@ -1,15 +1,20 @@
1
1
  import { ConnectionConfigSchema, type ConnectionConfig } from "@aa-sdk/core";
2
2
  import { TurnkeyClient, type TSignedRequest } from "@turnkey/http";
3
3
  import EventEmitter from "eventemitter3";
4
+ import { jwtDecode } from "jwt-decode";
4
5
  import type { Hex } from "viem";
5
6
  import { NotAuthenticatedError } from "../errors.js";
6
7
  import { base64UrlEncode } from "../utils/base64UrlEncode.js";
8
+ import { assertNever } from "../utils/typeAssertions.js";
7
9
  import type {
8
10
  AlchemySignerClientEvent,
9
11
  AlchemySignerClientEvents,
12
+ AuthenticatingEventMetadata,
10
13
  CreateAccountParams,
11
14
  EmailAuthParams,
12
15
  GetWebAuthnAttestationResult,
16
+ OauthConfig,
17
+ OauthParams,
13
18
  SignerBody,
14
19
  SignerResponse,
15
20
  SignerRoutes,
@@ -39,6 +44,7 @@ export abstract class BaseSignerClient<TExportWalletParams = unknown> {
39
44
  protected turnkeyClient: TurnkeyClient;
40
45
  protected rootOrg: string;
41
46
  protected eventEmitter: EventEmitter<AlchemySignerClientEvents>;
47
+ protected oauthConfig: OauthConfig | undefined;
42
48
 
43
49
  /**
44
50
  * Create a new instance of the Alchemy Signer client
@@ -47,7 +53,6 @@ export abstract class BaseSignerClient<TExportWalletParams = unknown> {
47
53
  */
48
54
  constructor(params: BaseSignerClientParams) {
49
55
  const { stamper, connection, rootOrgId } = params;
50
-
51
56
  this.rootOrg = rootOrgId ?? "24c1acf5-810f-41e0-a503-d5d13fa8e830";
52
57
  this.eventEmitter = new EventEmitter<AlchemySignerClientEvents>();
53
58
  this.connectionConfig = ConnectionConfigSchema.parse(connection);
@@ -57,6 +62,16 @@ export abstract class BaseSignerClient<TExportWalletParams = unknown> {
57
62
  );
58
63
  }
59
64
 
65
+ /**
66
+ * Asynchronously fetches and sets the OAuth configuration.
67
+ *
68
+ * @returns {Promise<OauthConfig>} A promise that resolves to the OAuth configuration
69
+ */
70
+ public initOauth = async (): Promise<OauthConfig> => {
71
+ this.oauthConfig = await this.getOauthConfig();
72
+ return this.oauthConfig;
73
+ };
74
+
60
75
  protected get user() {
61
76
  return this._user;
62
77
  }
@@ -92,11 +107,14 @@ export abstract class BaseSignerClient<TExportWalletParams = unknown> {
92
107
  exportStamper: ExportWalletStamper;
93
108
  exportAs: "SEED_PHRASE" | "PRIVATE_KEY";
94
109
  }): Promise<boolean> {
95
- switch (params.exportAs) {
110
+ const { exportAs } = params;
111
+ switch (exportAs) {
96
112
  case "PRIVATE_KEY":
97
113
  return this.exportAsPrivateKey(params.exportStamper);
98
114
  case "SEED_PHRASE":
99
115
  return this.exportAsSeedPhrase(params.exportStamper);
116
+ default:
117
+ assertNever(exportAs, `Unknown export mode: ${exportAs}`);
100
118
  }
101
119
  }
102
120
 
@@ -110,17 +128,30 @@ export abstract class BaseSignerClient<TExportWalletParams = unknown> {
110
128
  params: Omit<EmailAuthParams, "targetPublicKey">
111
129
  ): Promise<{ orgId: string }>;
112
130
 
113
- public abstract completeEmailAuth(params: {
131
+ public abstract completeAuthWithBundle(params: {
114
132
  bundle: string;
115
133
  orgId: string;
134
+ connectedEventName: keyof AlchemySignerClientEvents;
135
+ authenticatingType: AuthenticatingEventMetadata["type"];
136
+ idToken?: string;
116
137
  }): Promise<User>;
117
138
 
139
+ public abstract oauthWithRedirect(
140
+ args: Extract<OauthParams, { mode: "redirect" }>
141
+ ): Promise<never>;
142
+
143
+ public abstract oauthWithPopup(
144
+ args: Extract<OauthParams, { mode: "popup" }>
145
+ ): Promise<User>;
146
+
118
147
  public abstract disconnect(): Promise<void>;
119
148
 
120
149
  public abstract exportWallet(params: TExportWalletParams): Promise<boolean>;
121
150
 
122
151
  public abstract lookupUserWithPasskey(user?: User): Promise<User>;
123
152
 
153
+ protected abstract getOauthConfig(): Promise<OauthConfig>;
154
+
124
155
  protected abstract getWebAuthnAttestation(
125
156
  options: CredentialCreationOptions,
126
157
  userDetails?: { username: string }
@@ -190,10 +221,14 @@ export abstract class BaseSignerClient<TExportWalletParams = unknown> {
190
221
  * Retrieves the current user or fetches the user information if not already available.
191
222
  *
192
223
  * @param {string} [orgId] optional organization ID, defaults to the user's organization ID
224
+ * @param {string} idToken an OIDC ID token containing additional user information
193
225
  * @returns {Promise<User>} A promise that resolves to the user object
194
226
  * @throws {Error} if no organization ID is provided when there is no current user
195
227
  */
196
- public whoami = async (orgId = this.user?.orgId): Promise<User> => {
228
+ public whoami = async (
229
+ orgId = this.user?.orgId,
230
+ idToken?: string
231
+ ): Promise<User> => {
197
232
  if (this.user) {
198
233
  return this.user;
199
234
  }
@@ -210,6 +245,15 @@ export abstract class BaseSignerClient<TExportWalletParams = unknown> {
210
245
  stampedRequest,
211
246
  });
212
247
 
248
+ if (idToken) {
249
+ const claims: Record<string, unknown> = jwtDecode(idToken);
250
+ user.idToken = idToken;
251
+ user.claims = claims;
252
+ if (typeof claims.email === "string") {
253
+ user.email = claims.email;
254
+ }
255
+ }
256
+
213
257
  const credentialId = (() => {
214
258
  try {
215
259
  return JSON.parse(stampedRequest?.stamp.stampHeaderValue)
@@ -310,6 +354,7 @@ export abstract class BaseSignerClient<TExportWalletParams = unknown> {
310
354
  body: SignerBody<R>
311
355
  ): Promise<SignerResponse<R>> => {
312
356
  const url = this.connectionConfig.rpcUrl ?? "https://api.g.alchemy.com";
357
+
313
358
  const basePath = "/signer";
314
359
 
315
360
  const headers = new Headers();