@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
@@ -1,19 +1,27 @@
1
- import { ConnectionConfigSchema } from "@aa-sdk/core";
1
+ import { BaseError, ConnectionConfigSchema } from "@aa-sdk/core";
2
2
  import { getWebAuthnAttestation } from "@turnkey/http";
3
3
  import { IframeStamper } from "@turnkey/iframe-stamper";
4
4
  import { WebauthnStamper } from "@turnkey/webauthn-stamper";
5
5
  import { z } from "zod";
6
+ import { getDefaultScopeAndClaims, getOauthNonce } from "../oauth.js";
7
+ import type { AuthParams, OauthMode } from "../signer.js";
6
8
  import { base64UrlEncode } from "../utils/base64UrlEncode.js";
7
9
  import { generateRandomBuffer } from "../utils/generateRandomBuffer.js";
8
10
  import { BaseSignerClient } from "./base.js";
9
11
  import type {
12
+ AlchemySignerClientEvents,
13
+ AuthenticatingEventMetadata,
10
14
  CreateAccountParams,
11
15
  CredentialCreationOptionOverrides,
12
16
  EmailAuthParams,
13
17
  ExportWalletParams,
18
+ OauthConfig,
19
+ OauthParams,
14
20
  User,
15
21
  } from "./types.js";
16
22
 
23
+ const CHECK_CLOSE_INTERVAL = 500;
24
+
17
25
  export const AlchemySignerClientParamsSchema = z.object({
18
26
  connection: ConnectionConfigSchema,
19
27
  iframeConfig: z.object({
@@ -25,12 +33,27 @@ export const AlchemySignerClientParamsSchema = z.object({
25
33
  .string()
26
34
  .optional()
27
35
  .default("24c1acf5-810f-41e0-a503-d5d13fa8e830"),
36
+ oauthCallbackUrl: z
37
+ .string()
38
+ .optional()
39
+ .default("https://signer.alchemy.com/callback"),
40
+ enablePopupOauth: z.boolean().optional().default(false),
28
41
  });
29
42
 
30
43
  export type AlchemySignerClientParams = z.input<
31
44
  typeof AlchemySignerClientParamsSchema
32
45
  >;
33
46
 
47
+ type OauthState = {
48
+ authProviderId: string;
49
+ isCustomProvider?: boolean;
50
+ requestKey: string;
51
+ turnkeyPublicKey: string;
52
+ expirationSeconds?: number;
53
+ redirectUrl?: string;
54
+ openerOrigin?: string;
55
+ };
56
+
34
57
  /**
35
58
  * A lower level client used by the AlchemySigner used to communicate with
36
59
  * Alchemy's signer service.
@@ -38,6 +61,7 @@ export type AlchemySignerClientParams = z.input<
38
61
  export class AlchemySignerWebClient extends BaseSignerClient<ExportWalletParams> {
39
62
  private iframeStamper: IframeStamper;
40
63
  private webauthnStamper: WebauthnStamper;
64
+ oauthCallbackUrl: string;
41
65
  iframeContainerId: string;
42
66
 
43
67
  /**
@@ -64,7 +88,7 @@ export class AlchemySignerWebClient extends BaseSignerClient<ExportWalletParams>
64
88
  * @param {string} params.rootOrgId The root organization ID
65
89
  */
66
90
  constructor(params: AlchemySignerClientParams) {
67
- const { connection, iframeConfig, rpId, rootOrgId } =
91
+ const { connection, iframeConfig, rpId, rootOrgId, oauthCallbackUrl } =
68
92
  AlchemySignerClientParamsSchema.parse(params);
69
93
 
70
94
  const iframeStamper = new IframeStamper({
@@ -85,6 +109,8 @@ export class AlchemySignerWebClient extends BaseSignerClient<ExportWalletParams>
85
109
  this.webauthnStamper = new WebauthnStamper({
86
110
  rpId: rpId ?? window.location.hostname,
87
111
  });
112
+
113
+ this.oauthCallbackUrl = oauthCallbackUrl;
88
114
  }
89
115
 
90
116
  /**
@@ -109,9 +135,9 @@ export class AlchemySignerWebClient extends BaseSignerClient<ExportWalletParams>
109
135
  * @param {CreateAccountParams} params The parameters for creating an account, including the type (email or passkey) and additional details.
110
136
  * @returns {Promise<SignupResponse>} A promise that resolves with the response object containing the account creation result.
111
137
  */
112
- createAccount = async (params: CreateAccountParams) => {
113
- this.eventEmitter.emit("authenticating");
138
+ public override createAccount = async (params: CreateAccountParams) => {
114
139
  if (params.type === "email") {
140
+ this.eventEmitter.emit("authenticating", { type: "email" });
115
141
  const { email, expirationSeconds } = params;
116
142
  const publicKey = await this.initIframeStamper();
117
143
 
@@ -125,6 +151,7 @@ export class AlchemySignerWebClient extends BaseSignerClient<ExportWalletParams>
125
151
  return response;
126
152
  }
127
153
 
154
+ this.eventEmitter.emit("authenticating", { type: "passkey" });
128
155
  // Passkey account creation flow
129
156
  const { attestation, challenge } = await this.getWebAuthnAttestation(
130
157
  params.creationOpts,
@@ -174,10 +201,10 @@ export class AlchemySignerWebClient extends BaseSignerClient<ExportWalletParams>
174
201
  * @param {Omit<EmailAuthParams, "targetPublicKey">} params The parameters for email authentication, excluding the target public key
175
202
  * @returns {Promise<any>} The response from the authentication request
176
203
  */
177
- public initEmailAuth = async (
204
+ public override initEmailAuth = async (
178
205
  params: Omit<EmailAuthParams, "targetPublicKey">
179
206
  ) => {
180
- this.eventEmitter.emit("authenticating");
207
+ this.eventEmitter.emit("authenticating", { type: "email" });
181
208
  const { email, expirationSeconds } = params;
182
209
  const publicKey = await this.initIframeStamper();
183
210
 
@@ -190,7 +217,9 @@ export class AlchemySignerWebClient extends BaseSignerClient<ExportWalletParams>
190
217
  };
191
218
 
192
219
  /**
193
- * Completes email auth for the user by injecting a credential bundle and retrieving the user information based on the provided organization ID. Emits events during the process.
220
+ * Completes auth for the user by injecting a credential bundle and retrieving
221
+ * the user information based on the provided organization ID. Emits events
222
+ * during the process.
194
223
  *
195
224
  * @example
196
225
  * ```ts
@@ -205,20 +234,31 @@ export class AlchemySignerWebClient extends BaseSignerClient<ExportWalletParams>
205
234
  * },
206
235
  * });
207
236
  *
208
- * const account = await client.completeEmailAuth({ orgId: "user-org-id", bundle: "bundle-from-email" });
237
+ * const account = await client.completeAuthWithBundle({ orgId: "user-org-id", bundle: "bundle-from-email", connectedEventName: "connectedEmail" });
209
238
  * ```
210
239
  *
211
- * @param {{ bundle: string; orgId: string }} config The configuration object for the authentication function containing the credential bundle to inject and the organization id associated with the user
212
- * @returns {Promise<User>} A promise that resolves to the authenticated user information
240
+ * @param {{ bundle: string; orgId: string, connectedEventName: keyof AlchemySignerClientEvents, idToken?: string }} config
241
+ * The configuration object for the authentication function containing the
242
+ * credential bundle to inject and the organization id associated with the
243
+ * user, as well as the event to be emitted on success and optionally an OIDC
244
+ * ID token with extra user information
245
+ * @returns {Promise<User>} A promise that resolves to the authenticated user
246
+ * information
213
247
  */
214
- public completeEmailAuth = async ({
248
+ public override completeAuthWithBundle = async ({
215
249
  bundle,
216
250
  orgId,
251
+ connectedEventName,
252
+ idToken,
253
+ authenticatingType,
217
254
  }: {
218
255
  bundle: string;
219
256
  orgId: string;
220
- }) => {
221
- this.eventEmitter.emit("authenticating");
257
+ connectedEventName: keyof AlchemySignerClientEvents;
258
+ authenticatingType: AuthenticatingEventMetadata["type"];
259
+ idToken?: string;
260
+ }): Promise<User> => {
261
+ this.eventEmitter.emit("authenticating", { type: authenticatingType });
222
262
  await this.initIframeStamper();
223
263
 
224
264
  const result = await this.iframeStamper.injectCredentialBundle(bundle);
@@ -227,8 +267,9 @@ export class AlchemySignerWebClient extends BaseSignerClient<ExportWalletParams>
227
267
  throw new Error("Failed to inject credential bundle");
228
268
  }
229
269
 
230
- const user = await this.whoami(orgId);
231
- this.eventEmitter.emit("connectedEmail", user, bundle);
270
+ const user = await this.whoami(orgId, idToken);
271
+
272
+ this.eventEmitter.emit(connectedEventName, user, bundle);
232
273
 
233
274
  return user;
234
275
  };
@@ -255,8 +296,10 @@ export class AlchemySignerWebClient extends BaseSignerClient<ExportWalletParams>
255
296
  * @param {User} [user] An optional user object to authenticate
256
297
  * @returns {Promise<User>} A promise that resolves to the authenticated user object
257
298
  */
258
- public lookupUserWithPasskey = async (user: User | undefined = undefined) => {
259
- this.eventEmitter.emit("authenticating");
299
+ public override lookupUserWithPasskey = async (
300
+ user: User | undefined = undefined
301
+ ) => {
302
+ this.eventEmitter.emit("authenticating", { type: "passkey" });
260
303
  await this.initWebauthnStamper(user);
261
304
  if (user) {
262
305
  this.user = user;
@@ -297,7 +340,7 @@ export class AlchemySignerWebClient extends BaseSignerClient<ExportWalletParams>
297
340
  * @param {string} [config.iframeElementId] Optional ID for the iframe element
298
341
  * @returns {Promise<void>} A promise that resolves when the export process is complete
299
342
  */
300
- public exportWallet = async ({
343
+ public override exportWallet = async ({
301
344
  iframeContainerId,
302
345
  iframeElementId = "turnkey-export-iframe",
303
346
  }: ExportWalletParams) => {
@@ -340,9 +383,202 @@ export class AlchemySignerWebClient extends BaseSignerClient<ExportWalletParams>
340
383
  * const account = await client.disconnect();
341
384
  * ```
342
385
  */
343
- public disconnect = async () => {
386
+ public override disconnect = async () => {
344
387
  this.user = undefined;
345
388
  this.iframeStamper.clear();
389
+ await this.iframeStamper.init();
390
+ };
391
+
392
+ /**
393
+ * Redirects the user to the OAuth provider URL based on the provided arguments. This function will always reject after 1 second if the redirection does not occur.
394
+ *
395
+ * @example
396
+ * ```ts
397
+ * import { AlchemySignerWebClient } from "@account-kit/signer";
398
+ *
399
+ * const client = new AlchemySignerWebClient({
400
+ * connection: {
401
+ * apiKey: "your-api-key",
402
+ * },
403
+ * iframeConfig: {
404
+ * iframeContainerId: "signer-iframe-container",
405
+ * },
406
+ * });
407
+ *
408
+ * await client.oauthWithRedirect({
409
+ * type: "oauth",
410
+ * authProviderId: "google",
411
+ * mode: "redirect",
412
+ * redirectUrl: "/",
413
+ * });
414
+ * ```
415
+ *
416
+ * @param {Extract<AuthParams, { type: "oauth"; mode: "redirect" }>} args The arguments required to obtain the OAuth provider URL
417
+ * @returns {Promise<never>} A promise that will never resolve, only reject if the redirection fails
418
+ */
419
+ public override oauthWithRedirect = async (
420
+ args: Extract<AuthParams, { type: "oauth"; mode: "redirect" }>
421
+ ): Promise<never> => {
422
+ const providerUrl = await this.getOauthProviderUrl(args);
423
+ window.location.href = providerUrl;
424
+ return new Promise((_, reject) =>
425
+ setTimeout(() => reject("Failed to redirect to OAuth provider"), 1000)
426
+ );
427
+ };
428
+
429
+ /**
430
+ * Initiates an OAuth authentication flow in a popup window and returns the authenticated user.
431
+ *
432
+ * @example
433
+ * ```ts
434
+ * import { AlchemySignerWebClient } from "@account-kit/signer";
435
+ *
436
+ * const client = new AlchemySignerWebClient({
437
+ * connection: {
438
+ * apiKey: "your-api-key",
439
+ * },
440
+ * iframeConfig: {
441
+ * iframeContainerId: "signer-iframe-container",
442
+ * },
443
+ * });
444
+ *
445
+ * const user = await client.oauthWithPopup({
446
+ * type: "oauth",
447
+ * authProviderId: "google",
448
+ * mode: "popup"
449
+ * });
450
+ * ```
451
+ *
452
+ * @param {Extract<AuthParams, { type: "oauth"; mode: "popup" }>} args The authentication parameters specifying OAuth type and popup mode
453
+ * @returns {Promise<User>} A promise that resolves to a `User` object containing the authenticated user information
454
+ */
455
+ public override oauthWithPopup = async (
456
+ args: Extract<AuthParams, { type: "oauth"; mode: "popup" }>
457
+ ): Promise<User> => {
458
+ const providerUrl = await this.getOauthProviderUrl(args);
459
+ const popup = window.open(
460
+ providerUrl,
461
+ "_blank",
462
+ "popup,width=500,height=600"
463
+ );
464
+ return new Promise((resolve, reject) => {
465
+ const handleMessage = (event: MessageEvent) => {
466
+ if (!event.data) {
467
+ return;
468
+ }
469
+ const {
470
+ alchemyBundle: bundle,
471
+ alchemyOrgId: orgId,
472
+ alchemyIdToken: idToken,
473
+ alchemyError,
474
+ } = event.data;
475
+ if (bundle && orgId && idToken) {
476
+ cleanup();
477
+ popup?.close();
478
+ this.completeAuthWithBundle({
479
+ bundle,
480
+ orgId,
481
+ connectedEventName: "connectedOauth",
482
+ idToken,
483
+ authenticatingType: "oauth",
484
+ }).then(resolve, reject);
485
+ } else if (alchemyError) {
486
+ cleanup();
487
+ popup?.close();
488
+ reject(new OauthFailedError(alchemyError));
489
+ }
490
+ };
491
+
492
+ window.addEventListener("message", handleMessage);
493
+
494
+ const checkCloseIntervalId = setInterval(() => {
495
+ if (popup?.closed) {
496
+ cleanup();
497
+ reject(new OauthCancelledError());
498
+ }
499
+ }, CHECK_CLOSE_INTERVAL);
500
+
501
+ const cleanup = () => {
502
+ window.removeEventListener("message", handleMessage);
503
+ clearInterval(checkCloseIntervalId);
504
+ };
505
+ });
506
+ };
507
+
508
+ private getOauthProviderUrl = async (args: OauthParams): Promise<string> => {
509
+ const {
510
+ authProviderId,
511
+ isCustomProvider,
512
+ auth0Connection,
513
+ scope: providedScope,
514
+ claims: providedClaims,
515
+ mode,
516
+ redirectUrl,
517
+ expirationSeconds,
518
+ } = args;
519
+ const { codeChallenge, requestKey, authProviders } =
520
+ await this.getOauthConfigForMode(mode);
521
+ const authProvider = authProviders.find(
522
+ (provider) =>
523
+ provider.id === authProviderId &&
524
+ !!provider.isCustomProvider === !!isCustomProvider
525
+ );
526
+ if (!authProvider) {
527
+ throw new Error(`No auth provider found with id ${authProviderId}`);
528
+ }
529
+ let scope: string;
530
+ let claims: string | undefined;
531
+ if (providedScope) {
532
+ scope = addOpenIdIfAbsent(providedScope);
533
+ claims = providedClaims;
534
+ } else {
535
+ if (isCustomProvider) {
536
+ throw new Error("scope must be provided for a custom provider");
537
+ }
538
+ const scopeAndClaims = getDefaultScopeAndClaims(authProviderId);
539
+ if (!scopeAndClaims) {
540
+ throw new Error(
541
+ `Default scope not known for provider ${authProviderId}`
542
+ );
543
+ }
544
+ ({ scope, claims } = scopeAndClaims);
545
+ }
546
+ const { authEndpoint, clientId } = authProvider;
547
+ const turnkeyPublicKey = await this.initIframeStamper();
548
+ const nonce = getOauthNonce(turnkeyPublicKey);
549
+ const stateObject: OauthState = {
550
+ authProviderId,
551
+ isCustomProvider,
552
+ requestKey,
553
+ turnkeyPublicKey,
554
+ expirationSeconds,
555
+ redirectUrl:
556
+ mode === "redirect" ? resolveRelativeUrl(redirectUrl) : undefined,
557
+ openerOrigin: mode === "popup" ? window.location.origin : undefined,
558
+ };
559
+ const state = base64UrlEncode(
560
+ new TextEncoder().encode(JSON.stringify(stateObject))
561
+ );
562
+ const authUrl = new URL(authEndpoint);
563
+ const params: Record<string, string> = {
564
+ redirect_uri: this.oauthCallbackUrl,
565
+ response_type: "code",
566
+ scope,
567
+ state,
568
+ code_challenge: codeChallenge,
569
+ code_challenge_method: "S256",
570
+ prompt: "select_account",
571
+ client_id: clientId,
572
+ nonce,
573
+ };
574
+ if (claims) {
575
+ params.claims = claims;
576
+ }
577
+ if (auth0Connection) {
578
+ params.connection = auth0Connection;
579
+ }
580
+ authUrl.search = new URLSearchParams(params).toString();
581
+ return authUrl.toString();
346
582
  };
347
583
 
348
584
  private initIframeStamper = async () => {
@@ -369,7 +605,7 @@ export class AlchemySignerWebClient extends BaseSignerClient<ExportWalletParams>
369
605
  }
370
606
  };
371
607
 
372
- protected getWebAuthnAttestation = async (
608
+ protected override getWebAuthnAttestation = async (
373
609
  options?: CredentialCreationOptionOverrides,
374
610
  userDetails: { username: string } = {
375
611
  username: this.user?.email ?? "anonymous",
@@ -423,4 +659,65 @@ export class AlchemySignerWebClient extends BaseSignerClient<ExportWalletParams>
423
659
 
424
660
  return { challenge, authenticatorUserId, attestation };
425
661
  };
662
+
663
+ protected override getOauthConfig = async (): Promise<OauthConfig> => {
664
+ const publicKey = await this.initIframeStamper();
665
+ const nonce = getOauthNonce(publicKey);
666
+ return this.request("/v1/prepare-oauth", { nonce });
667
+ };
668
+
669
+ private getOauthConfigForMode = async (
670
+ mode: OauthMode
671
+ ): Promise<OauthConfig> => {
672
+ if (this.oauthConfig) {
673
+ return this.oauthConfig;
674
+ } else if (mode === "redirect") {
675
+ return this.initOauth();
676
+ } else {
677
+ throw new Error(
678
+ "enablePopupOauth must be set in configuration or signer.preparePopupOauth must be called before using popup-based OAuth login"
679
+ );
680
+ }
681
+ };
682
+ }
683
+
684
+ function resolveRelativeUrl(url: string): string {
685
+ // Funny trick.
686
+ const a = document.createElement("a");
687
+ a.href = url;
688
+ return a.href;
689
+ }
690
+
691
+ /**
692
+ * "openid" is a required scope in the OIDC protocol. Insert it if the user
693
+ * forgot.
694
+ *
695
+ * @param {string} scope scope param which may be missing "openid"
696
+ * @returns {string} scope which most definitely contains "openid"
697
+ */
698
+ function addOpenIdIfAbsent(scope: string): string {
699
+ return scope.match(/\bopenid\b/) ? scope : `openid ${scope}`;
700
+ }
701
+
702
+ /**
703
+ * This error is thrown when the OAuth flow is cancelled because the auth popup
704
+ * window was closed.
705
+ */
706
+ export class OauthCancelledError extends BaseError {
707
+ override name = "OauthCancelledError";
708
+
709
+ /**
710
+ * Constructor for initializing an error indicating that the OAuth flow was
711
+ * cancelled.
712
+ */
713
+ constructor() {
714
+ super("OAuth cancelled");
715
+ }
716
+ }
717
+
718
+ /**
719
+ * This error is thrown when an error occurs during the OAuth login flow.
720
+ */
721
+ export class OauthFailedError extends BaseError {
722
+ override name = "OauthFailedError";
426
723
  }
@@ -1,6 +1,7 @@
1
1
  import type { Address } from "@aa-sdk/core";
2
2
  import type { TSignedRequest, getWebAuthnAttestation } from "@turnkey/http";
3
3
  import type { Hex } from "viem";
4
+ import type { AuthParams } from "../signer";
4
5
 
5
6
  export type CredentialCreationOptionOverrides = {
6
7
  publicKey?: Partial<CredentialCreationOptions["publicKey"]>;
@@ -13,6 +14,8 @@ export type User = {
13
14
  userId: string;
14
15
  address: Address;
15
16
  credentialId?: string;
17
+ idToken?: string;
18
+ claims?: Record<string, unknown>;
16
19
  };
17
20
  // [!endregion User]
18
21
 
@@ -46,12 +49,29 @@ export type EmailAuthParams = {
46
49
  redirectParams?: URLSearchParams;
47
50
  };
48
51
 
52
+ export type OauthParams = Extract<AuthParams, { type: "oauth" }> & {
53
+ expirationSeconds?: number;
54
+ };
55
+
49
56
  export type SignupResponse = {
50
57
  orgId: string;
51
58
  userId?: string;
52
59
  address?: Address;
53
60
  };
54
61
 
62
+ export type OauthConfig = {
63
+ codeChallenge: string;
64
+ requestKey: string;
65
+ authProviders: AuthProviderConfig[];
66
+ };
67
+
68
+ export type AuthProviderConfig = {
69
+ id: string;
70
+ isCustomProvider?: boolean;
71
+ clientId: string;
72
+ authEndpoint: string;
73
+ };
74
+
55
75
  export type SignerRoutes = SignerEndpoints[number]["Route"];
56
76
  export type SignerBody<T extends SignerRoutes> = Extract<
57
77
  SignerEndpoints[number],
@@ -106,14 +126,26 @@ export type SignerEndpoints = [
106
126
  Response: {
107
127
  signature: Hex;
108
128
  };
129
+ },
130
+ {
131
+ Route: "/v1/prepare-oauth";
132
+ Body: {
133
+ nonce: string;
134
+ };
135
+ Response: OauthConfig;
109
136
  }
110
137
  ];
111
138
 
139
+ export type AuthenticatingEventMetadata = {
140
+ type: "email" | "passkey" | "oauth";
141
+ };
142
+
112
143
  export type AlchemySignerClientEvents = {
113
144
  connected(user: User): void;
114
- authenticating(): void;
145
+ authenticating(data: AuthenticatingEventMetadata): void;
115
146
  connectedEmail(user: User, bundle: string): void;
116
147
  connectedPasskey(user: User): void;
148
+ connectedOauth(user: User, bundle: string): void;
117
149
  disconnected(): void;
118
150
  };
119
151
 
package/src/index.ts CHANGED
@@ -1,6 +1,10 @@
1
1
  export { BaseAlchemySigner } from "./base.js";
2
2
  export { BaseSignerClient } from "./client/base.js";
3
- export { AlchemySignerWebClient } from "./client/index.js";
3
+ export {
4
+ AlchemySignerWebClient,
5
+ OauthCancelledError,
6
+ OauthFailedError,
7
+ } from "./client/index.js";
4
8
  export type * from "./client/types.js";
5
9
  export { DEFAULT_SESSION_MS } from "./session/manager.js";
6
10
  export type * from "./signer.js";
package/src/metrics.ts ADDED
@@ -0,0 +1,23 @@
1
+ import { createLogger } from "@account-kit/logging";
2
+ import { VERSION } from "./version.js";
3
+
4
+ export type SignerEventsSchema = [
5
+ {
6
+ EventName: "signer_authnticate";
7
+ EventData:
8
+ | {
9
+ authType: "email" | "passkey_anon" | "passkey_email" | "oauthReturn";
10
+ provider?: never;
11
+ }
12
+ | { authType: "oauth"; provider: string };
13
+ },
14
+ {
15
+ EventName: "signer_sign_message";
16
+ EventData: undefined;
17
+ }
18
+ ];
19
+
20
+ export const SignerLogger = createLogger<SignerEventsSchema>({
21
+ package: "@account-kit/signer",
22
+ version: VERSION,
23
+ });
package/src/oauth.ts ADDED
@@ -0,0 +1,36 @@
1
+ import { sha256 } from "viem";
2
+ import type { KnownAuthProvider } from "./signer";
3
+
4
+ /**
5
+ * Turnkey requires the nonce in the id token to be in this format.
6
+ *
7
+ * @param {string} turnkeyPublicKey key from a Turnkey iframe
8
+ * @returns {string} nonce to be used in OIDC
9
+ */
10
+ export function getOauthNonce(turnkeyPublicKey: string): string {
11
+ return sha256(new TextEncoder().encode(turnkeyPublicKey)).slice(2);
12
+ }
13
+
14
+ export type ScopeAndClaims = {
15
+ scope: string;
16
+ claims?: string;
17
+ };
18
+
19
+ const DEFAULT_SCOPE_AND_CLAIMS: Record<KnownAuthProvider, ScopeAndClaims> = {
20
+ google: { scope: "openid email" },
21
+ apple: { scope: "openid email" },
22
+ facebook: { scope: "openid email" },
23
+ auth0: { scope: "openid email" },
24
+ };
25
+
26
+ /**
27
+ * Returns the default scope and claims when using a known auth provider
28
+ *
29
+ * @param {string} knownAuthProviderId id of a known auth provider, e.g. "google"
30
+ * @returns {ScopeAndClaims | undefined} default scope and claims
31
+ */
32
+ export function getDefaultScopeAndClaims(
33
+ knownAuthProviderId: KnownAuthProvider
34
+ ): ScopeAndClaims | undefined {
35
+ return DEFAULT_SCOPE_AND_CLAIMS[knownAuthProviderId];
36
+ }