@account-kit/signer 4.0.0-beta.2 → 4.0.0-beta.4

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 (57) hide show
  1. package/dist/esm/base.d.ts +33 -1
  2. package/dist/esm/base.js +81 -7
  3. package/dist/esm/base.js.map +1 -1
  4. package/dist/esm/client/base.d.ts +28 -5
  5. package/dist/esm/client/base.js +25 -1
  6. package/dist/esm/client/base.js.map +1 -1
  7. package/dist/esm/client/index.d.ts +78 -5
  8. package/dist/esm/client/index.js +211 -6
  9. package/dist/esm/client/index.js.map +1 -1
  10. package/dist/esm/client/types.d.ts +25 -0
  11. package/dist/esm/client/types.js.map +1 -1
  12. package/dist/esm/oauth.d.ts +18 -0
  13. package/dist/esm/oauth.js +29 -0
  14. package/dist/esm/oauth.js.map +1 -0
  15. package/dist/esm/session/manager.d.ts +3 -2
  16. package/dist/esm/session/manager.js +28 -15
  17. package/dist/esm/session/manager.js.map +1 -1
  18. package/dist/esm/session/types.d.ts +1 -1
  19. package/dist/esm/session/types.js.map +1 -1
  20. package/dist/esm/signer.d.ts +39 -7
  21. package/dist/esm/signer.js.map +1 -1
  22. package/dist/esm/utils/typeAssertions.d.ts +1 -0
  23. package/dist/esm/utils/typeAssertions.js +4 -0
  24. package/dist/esm/utils/typeAssertions.js.map +1 -0
  25. package/dist/esm/version.d.ts +1 -1
  26. package/dist/esm/version.js +1 -1
  27. package/dist/esm/version.js.map +1 -1
  28. package/dist/types/base.d.ts +33 -1
  29. package/dist/types/base.d.ts.map +1 -1
  30. package/dist/types/client/base.d.ts +28 -5
  31. package/dist/types/client/base.d.ts.map +1 -1
  32. package/dist/types/client/index.d.ts +78 -5
  33. package/dist/types/client/index.d.ts.map +1 -1
  34. package/dist/types/client/types.d.ts +25 -0
  35. package/dist/types/client/types.d.ts.map +1 -1
  36. package/dist/types/oauth.d.ts +19 -0
  37. package/dist/types/oauth.d.ts.map +1 -0
  38. package/dist/types/session/manager.d.ts +3 -2
  39. package/dist/types/session/manager.d.ts.map +1 -1
  40. package/dist/types/session/types.d.ts +1 -1
  41. package/dist/types/session/types.d.ts.map +1 -1
  42. package/dist/types/signer.d.ts +39 -7
  43. package/dist/types/signer.d.ts.map +1 -1
  44. package/dist/types/utils/typeAssertions.d.ts +2 -0
  45. package/dist/types/utils/typeAssertions.d.ts.map +1 -0
  46. package/dist/types/version.d.ts +1 -1
  47. package/package.json +3 -3
  48. package/src/base.ts +80 -10
  49. package/src/client/base.ts +31 -3
  50. package/src/client/index.ts +244 -12
  51. package/src/client/types.ts +26 -0
  52. package/src/oauth.ts +38 -0
  53. package/src/session/manager.ts +45 -19
  54. package/src/session/types.ts +1 -1
  55. package/src/signer.ts +15 -1
  56. package/src/utils/typeAssertions.ts +3 -0
  57. package/src/version.ts +1 -1
@@ -10,12 +10,15 @@ import type {
10
10
  CreateAccountParams,
11
11
  EmailAuthParams,
12
12
  GetWebAuthnAttestationResult,
13
+ OauthConfig,
14
+ OauthParams,
13
15
  SignerBody,
14
16
  SignerResponse,
15
17
  SignerRoutes,
16
18
  SignupResponse,
17
19
  User,
18
20
  } from "./types.js";
21
+ import { assertNever } from "../utils/typeAssertions.js";
19
22
 
20
23
  export interface BaseSignerClientParams {
21
24
  stamper: TurnkeyClient["stamper"];
@@ -39,6 +42,7 @@ export abstract class BaseSignerClient<TExportWalletParams = unknown> {
39
42
  protected turnkeyClient: TurnkeyClient;
40
43
  protected rootOrg: string;
41
44
  protected eventEmitter: EventEmitter<AlchemySignerClientEvents>;
45
+ protected oauthConfig: OauthConfig | undefined;
42
46
 
43
47
  /**
44
48
  * Create a new instance of the Alchemy Signer client
@@ -47,7 +51,6 @@ export abstract class BaseSignerClient<TExportWalletParams = unknown> {
47
51
  */
48
52
  constructor(params: BaseSignerClientParams) {
49
53
  const { stamper, connection, rootOrgId } = params;
50
-
51
54
  this.rootOrg = rootOrgId ?? "24c1acf5-810f-41e0-a503-d5d13fa8e830";
52
55
  this.eventEmitter = new EventEmitter<AlchemySignerClientEvents>();
53
56
  this.connectionConfig = ConnectionConfigSchema.parse(connection);
@@ -57,6 +60,16 @@ export abstract class BaseSignerClient<TExportWalletParams = unknown> {
57
60
  );
58
61
  }
59
62
 
63
+ /**
64
+ * Asynchronously fetches and sets the OAuth configuration.
65
+ *
66
+ * @returns {Promise<OauthConfig>} A promise that resolves to the OAuth configuration
67
+ */
68
+ public initOauth = async (): Promise<OauthConfig> => {
69
+ this.oauthConfig = await this.getOauthConfig();
70
+ return this.oauthConfig;
71
+ };
72
+
60
73
  protected get user() {
61
74
  return this._user;
62
75
  }
@@ -92,11 +105,14 @@ export abstract class BaseSignerClient<TExportWalletParams = unknown> {
92
105
  exportStamper: ExportWalletStamper;
93
106
  exportAs: "SEED_PHRASE" | "PRIVATE_KEY";
94
107
  }): Promise<boolean> {
95
- switch (params.exportAs) {
108
+ const { exportAs } = params;
109
+ switch (exportAs) {
96
110
  case "PRIVATE_KEY":
97
111
  return this.exportAsPrivateKey(params.exportStamper);
98
112
  case "SEED_PHRASE":
99
113
  return this.exportAsSeedPhrase(params.exportStamper);
114
+ default:
115
+ assertNever(exportAs, `Unknown export mode: ${exportAs}`);
100
116
  }
101
117
  }
102
118
 
@@ -110,17 +126,28 @@ export abstract class BaseSignerClient<TExportWalletParams = unknown> {
110
126
  params: Omit<EmailAuthParams, "targetPublicKey">
111
127
  ): Promise<{ orgId: string }>;
112
128
 
113
- public abstract completeEmailAuth(params: {
129
+ public abstract completeAuthWithBundle(params: {
114
130
  bundle: string;
115
131
  orgId: string;
132
+ connectedEventName: keyof AlchemySignerClientEvents;
116
133
  }): Promise<User>;
117
134
 
135
+ public abstract oauthWithRedirect(
136
+ args: Extract<OauthParams, { mode: "redirect" }>
137
+ ): Promise<never>;
138
+
139
+ public abstract oauthWithPopup(
140
+ args: Extract<OauthParams, { mode: "popup" }>
141
+ ): Promise<User>;
142
+
118
143
  public abstract disconnect(): Promise<void>;
119
144
 
120
145
  public abstract exportWallet(params: TExportWalletParams): Promise<boolean>;
121
146
 
122
147
  public abstract lookupUserWithPasskey(user?: User): Promise<User>;
123
148
 
149
+ protected abstract getOauthConfig(): Promise<OauthConfig>;
150
+
124
151
  protected abstract getWebAuthnAttestation(
125
152
  options: CredentialCreationOptions,
126
153
  userDetails?: { username: string }
@@ -310,6 +337,7 @@ export abstract class BaseSignerClient<TExportWalletParams = unknown> {
310
337
  body: SignerBody<R>
311
338
  ): Promise<SignerResponse<R>> => {
312
339
  const url = this.connectionConfig.rpcUrl ?? "https://api.g.alchemy.com";
340
+
313
341
  const basePath = "/signer";
314
342
 
315
343
  const headers = new Headers();
@@ -7,12 +7,19 @@ import { base64UrlEncode } from "../utils/base64UrlEncode.js";
7
7
  import { generateRandomBuffer } from "../utils/generateRandomBuffer.js";
8
8
  import { BaseSignerClient } from "./base.js";
9
9
  import type {
10
+ AlchemySignerClientEvents,
10
11
  CreateAccountParams,
11
12
  CredentialCreationOptionOverrides,
12
13
  EmailAuthParams,
13
14
  ExportWalletParams,
15
+ OauthConfig,
16
+ OauthParams,
14
17
  User,
15
18
  } from "./types.js";
19
+ import { getDefaultScopeAndClaims, getOauthNonce } from "../oauth.js";
20
+ import type { AuthParams, OauthMode } from "../signer.js";
21
+
22
+ const CHECK_CLOSE_INTERVAL = 500;
16
23
 
17
24
  export const AlchemySignerClientParamsSchema = z.object({
18
25
  connection: ConnectionConfigSchema,
@@ -25,12 +32,27 @@ export const AlchemySignerClientParamsSchema = z.object({
25
32
  .string()
26
33
  .optional()
27
34
  .default("24c1acf5-810f-41e0-a503-d5d13fa8e830"),
35
+ oauthCallbackUrl: z
36
+ .string()
37
+ .optional()
38
+ .default("https://signer.alchemy.com/callback"),
39
+ enablePopupOauth: z.boolean().optional().default(false),
28
40
  });
29
41
 
30
42
  export type AlchemySignerClientParams = z.input<
31
43
  typeof AlchemySignerClientParamsSchema
32
44
  >;
33
45
 
46
+ type OauthState = {
47
+ authProviderId: string;
48
+ isCustomProvider?: boolean;
49
+ requestKey: string;
50
+ turnkeyPublicKey: string;
51
+ expirationSeconds?: number;
52
+ redirectUrl?: string;
53
+ openerOrigin?: string;
54
+ };
55
+
34
56
  /**
35
57
  * A lower level client used by the AlchemySigner used to communicate with
36
58
  * Alchemy's signer service.
@@ -38,6 +60,7 @@ export type AlchemySignerClientParams = z.input<
38
60
  export class AlchemySignerWebClient extends BaseSignerClient<ExportWalletParams> {
39
61
  private iframeStamper: IframeStamper;
40
62
  private webauthnStamper: WebauthnStamper;
63
+ oauthCallbackUrl: string;
41
64
  iframeContainerId: string;
42
65
 
43
66
  /**
@@ -64,7 +87,7 @@ export class AlchemySignerWebClient extends BaseSignerClient<ExportWalletParams>
64
87
  * @param {string} params.rootOrgId The root organization ID
65
88
  */
66
89
  constructor(params: AlchemySignerClientParams) {
67
- const { connection, iframeConfig, rpId, rootOrgId } =
90
+ const { connection, iframeConfig, rpId, rootOrgId, oauthCallbackUrl } =
68
91
  AlchemySignerClientParamsSchema.parse(params);
69
92
 
70
93
  const iframeStamper = new IframeStamper({
@@ -85,6 +108,8 @@ export class AlchemySignerWebClient extends BaseSignerClient<ExportWalletParams>
85
108
  this.webauthnStamper = new WebauthnStamper({
86
109
  rpId: rpId ?? window.location.hostname,
87
110
  });
111
+
112
+ this.oauthCallbackUrl = oauthCallbackUrl;
88
113
  }
89
114
 
90
115
  /**
@@ -109,7 +134,7 @@ export class AlchemySignerWebClient extends BaseSignerClient<ExportWalletParams>
109
134
  * @param {CreateAccountParams} params The parameters for creating an account, including the type (email or passkey) and additional details.
110
135
  * @returns {Promise<SignupResponse>} A promise that resolves with the response object containing the account creation result.
111
136
  */
112
- createAccount = async (params: CreateAccountParams) => {
137
+ public override createAccount = async (params: CreateAccountParams) => {
113
138
  this.eventEmitter.emit("authenticating");
114
139
  if (params.type === "email") {
115
140
  const { email, expirationSeconds } = params;
@@ -174,7 +199,7 @@ export class AlchemySignerWebClient extends BaseSignerClient<ExportWalletParams>
174
199
  * @param {Omit<EmailAuthParams, "targetPublicKey">} params The parameters for email authentication, excluding the target public key
175
200
  * @returns {Promise<any>} The response from the authentication request
176
201
  */
177
- public initEmailAuth = async (
202
+ public override initEmailAuth = async (
178
203
  params: Omit<EmailAuthParams, "targetPublicKey">
179
204
  ) => {
180
205
  this.eventEmitter.emit("authenticating");
@@ -190,7 +215,7 @@ export class AlchemySignerWebClient extends BaseSignerClient<ExportWalletParams>
190
215
  };
191
216
 
192
217
  /**
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.
218
+ * Completes 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.
194
219
  *
195
220
  * @example
196
221
  * ```ts
@@ -205,19 +230,21 @@ export class AlchemySignerWebClient extends BaseSignerClient<ExportWalletParams>
205
230
  * },
206
231
  * });
207
232
  *
208
- * const account = await client.completeEmailAuth({ orgId: "user-org-id", bundle: "bundle-from-email" });
233
+ * const account = await client.completeAuthWithBundle({ orgId: "user-org-id", bundle: "bundle-from-email", connectedEventName: "connectedEmail" });
209
234
  * ```
210
235
  *
211
236
  * @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
237
  * @returns {Promise<User>} A promise that resolves to the authenticated user information
213
238
  */
214
- public completeEmailAuth = async ({
239
+ public override completeAuthWithBundle = async ({
215
240
  bundle,
216
241
  orgId,
242
+ connectedEventName,
217
243
  }: {
218
244
  bundle: string;
219
245
  orgId: string;
220
- }) => {
246
+ connectedEventName: keyof AlchemySignerClientEvents;
247
+ }): Promise<User> => {
221
248
  this.eventEmitter.emit("authenticating");
222
249
  await this.initIframeStamper();
223
250
 
@@ -228,7 +255,7 @@ export class AlchemySignerWebClient extends BaseSignerClient<ExportWalletParams>
228
255
  }
229
256
 
230
257
  const user = await this.whoami(orgId);
231
- this.eventEmitter.emit("connectedEmail", user, bundle);
258
+ this.eventEmitter.emit(connectedEventName, user, bundle);
232
259
 
233
260
  return user;
234
261
  };
@@ -255,7 +282,9 @@ export class AlchemySignerWebClient extends BaseSignerClient<ExportWalletParams>
255
282
  * @param {User} [user] An optional user object to authenticate
256
283
  * @returns {Promise<User>} A promise that resolves to the authenticated user object
257
284
  */
258
- public lookupUserWithPasskey = async (user: User | undefined = undefined) => {
285
+ public override lookupUserWithPasskey = async (
286
+ user: User | undefined = undefined
287
+ ) => {
259
288
  this.eventEmitter.emit("authenticating");
260
289
  await this.initWebauthnStamper(user);
261
290
  if (user) {
@@ -297,7 +326,7 @@ export class AlchemySignerWebClient extends BaseSignerClient<ExportWalletParams>
297
326
  * @param {string} [config.iframeElementId] Optional ID for the iframe element
298
327
  * @returns {Promise<void>} A promise that resolves when the export process is complete
299
328
  */
300
- public exportWallet = async ({
329
+ public override exportWallet = async ({
301
330
  iframeContainerId,
302
331
  iframeElementId = "turnkey-export-iframe",
303
332
  }: ExportWalletParams) => {
@@ -340,11 +369,187 @@ export class AlchemySignerWebClient extends BaseSignerClient<ExportWalletParams>
340
369
  * const account = await client.disconnect();
341
370
  * ```
342
371
  */
343
- public disconnect = async () => {
372
+ public override disconnect = async () => {
344
373
  this.user = undefined;
345
374
  this.iframeStamper.clear();
346
375
  };
347
376
 
377
+ /**
378
+ * 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.
379
+ *
380
+ * @example
381
+ * ```ts
382
+ * import { AlchemySignerWebClient } from "@account-kit/signer";
383
+ *
384
+ * const client = new AlchemySignerWebClient({
385
+ * connection: {
386
+ * apiKey: "your-api-key",
387
+ * },
388
+ * iframeConfig: {
389
+ * iframeContainerId: "signer-iframe-container",
390
+ * },
391
+ * });
392
+ *
393
+ * await client.oauthWithRedirect({
394
+ * type: "oauth",
395
+ * authProviderId: "google",
396
+ * mode: "redirect",
397
+ * redirectUrl: "/",
398
+ * });
399
+ * ```
400
+ *
401
+ * @param {Extract<AuthParams, { type: "oauth"; mode: "redirect" }>} args The arguments required to obtain the OAuth provider URL
402
+ * @returns {Promise<never>} A promise that will never resolve, only reject if the redirection fails
403
+ */
404
+ public override oauthWithRedirect = async (
405
+ args: Extract<AuthParams, { type: "oauth"; mode: "redirect" }>
406
+ ): Promise<never> => {
407
+ const providerUrl = await this.getOauthProviderUrl(args);
408
+ window.location.href = providerUrl;
409
+ return new Promise((_, reject) =>
410
+ setTimeout(() => reject("Failed to redirect to OAuth provider"), 1000)
411
+ );
412
+ };
413
+
414
+ /**
415
+ * Initiates an OAuth authentication flow in a popup window and returns the authenticated user.
416
+ *
417
+ * @example
418
+ * ```ts
419
+ * import { AlchemySignerWebClient } from "@account-kit/signer";
420
+ *
421
+ * const client = new AlchemySignerWebClient({
422
+ * connection: {
423
+ * apiKey: "your-api-key",
424
+ * },
425
+ * iframeConfig: {
426
+ * iframeContainerId: "signer-iframe-container",
427
+ * },
428
+ * });
429
+ *
430
+ * const user = await client.oauthWithPopup({
431
+ * type: "oauth",
432
+ * authProviderId: "google",
433
+ * mode: "popup"
434
+ * });
435
+ * ```
436
+ *
437
+ * @param {Extract<AuthParams, { type: "oauth"; mode: "popup" }>} args The authentication parameters specifying OAuth type and popup mode
438
+ * @returns {Promise<User>} A promise that resolves to a `User` object containing the authenticated user information
439
+ */
440
+ public override oauthWithPopup = async (
441
+ args: Extract<AuthParams, { type: "oauth"; mode: "popup" }>
442
+ ): Promise<User> => {
443
+ const providerUrl = await this.getOauthProviderUrl(args);
444
+ const popup = window.open(
445
+ providerUrl,
446
+ "_blank",
447
+ "popup,width=500,height=600"
448
+ );
449
+ return new Promise((resolve, reject) => {
450
+ const handleMessage = (event: MessageEvent) => {
451
+ if (!event.data) {
452
+ return;
453
+ }
454
+ const { alchemyBundle: bundle, alchemyOrgId: orgId } = event.data;
455
+ if (bundle && orgId) {
456
+ cleanup();
457
+ this.completeAuthWithBundle({
458
+ bundle,
459
+ orgId,
460
+ connectedEventName: "connectedOauth",
461
+ }).then(resolve, reject);
462
+ }
463
+ };
464
+
465
+ window.addEventListener("message", handleMessage);
466
+
467
+ const checkCloseIntervalId = setInterval(() => {
468
+ if (popup?.closed) {
469
+ cleanup();
470
+ reject(new Error("Oauth cancelled"));
471
+ }
472
+ }, CHECK_CLOSE_INTERVAL);
473
+
474
+ const cleanup = () => {
475
+ window.removeEventListener("message", handleMessage);
476
+ clearInterval(checkCloseIntervalId);
477
+ };
478
+ });
479
+ };
480
+
481
+ private getOauthProviderUrl = async (args: OauthParams): Promise<string> => {
482
+ const {
483
+ authProviderId,
484
+ isCustomProvider,
485
+ scope: providedScope,
486
+ claims: providedClaims,
487
+ mode,
488
+ redirectUrl,
489
+ expirationSeconds,
490
+ } = args;
491
+ const { codeChallenge, requestKey, authProviders } =
492
+ await this.getOauthConfigForMode(mode);
493
+ const authProvider = authProviders.find(
494
+ (provider) =>
495
+ provider.id === authProviderId &&
496
+ !!provider.isCustomProvider === !!isCustomProvider
497
+ );
498
+ if (!authProvider) {
499
+ throw new Error(`No auth provider found with id ${authProviderId}`);
500
+ }
501
+ let scope: string;
502
+ let claims: string | undefined;
503
+ if (providedScope) {
504
+ scope = providedScope;
505
+ claims = providedClaims;
506
+ } else {
507
+ if (isCustomProvider) {
508
+ throw new Error("scope must be provided for a custom provider");
509
+ }
510
+ const scopeAndClaims = getDefaultScopeAndClaims(authProviderId);
511
+ if (!scopeAndClaims) {
512
+ throw new Error(
513
+ `Default scope not known for provider ${authProviderId}`
514
+ );
515
+ }
516
+ ({ scope, claims } = scopeAndClaims);
517
+ }
518
+ const { authEndpoint, clientId } = authProvider;
519
+ const turnkeyPublicKey = await this.initIframeStamper();
520
+ const nonce = getOauthNonce(turnkeyPublicKey);
521
+ const stateObject: OauthState = {
522
+ authProviderId,
523
+ isCustomProvider,
524
+ requestKey,
525
+ turnkeyPublicKey,
526
+ expirationSeconds,
527
+ redirectUrl:
528
+ mode === "redirect" ? resolveRelativeUrl(redirectUrl) : undefined,
529
+ openerOrigin: mode === "popup" ? window.location.origin : undefined,
530
+ };
531
+ const state = base64UrlEncode(
532
+ new TextEncoder().encode(JSON.stringify(stateObject))
533
+ );
534
+ const authUrl = new URL(authEndpoint);
535
+ const params: Record<string, string> = {
536
+ redirect_uri: this.oauthCallbackUrl,
537
+ response_type: "code",
538
+ scope,
539
+ state,
540
+ code_challenge: codeChallenge,
541
+ code_challenge_method: "S256",
542
+ prompt: "select_account",
543
+ client_id: clientId,
544
+ nonce,
545
+ };
546
+ if (claims) {
547
+ params.claims = claims;
548
+ }
549
+ authUrl.search = new URLSearchParams(params).toString();
550
+ return authUrl.toString();
551
+ };
552
+
348
553
  private initIframeStamper = async () => {
349
554
  if (!this.iframeStamper.publicKey()) {
350
555
  await this.iframeStamper.init();
@@ -369,7 +574,7 @@ export class AlchemySignerWebClient extends BaseSignerClient<ExportWalletParams>
369
574
  }
370
575
  };
371
576
 
372
- protected getWebAuthnAttestation = async (
577
+ protected override getWebAuthnAttestation = async (
373
578
  options?: CredentialCreationOptionOverrides,
374
579
  userDetails: { username: string } = {
375
580
  username: this.user?.email ?? "anonymous",
@@ -423,4 +628,31 @@ export class AlchemySignerWebClient extends BaseSignerClient<ExportWalletParams>
423
628
 
424
629
  return { challenge, authenticatorUserId, attestation };
425
630
  };
631
+
632
+ protected override getOauthConfig = async (): Promise<OauthConfig> => {
633
+ const publicKey = await this.initIframeStamper();
634
+ const nonce = getOauthNonce(publicKey);
635
+ return this.request("/v1/prepare-oauth", { nonce });
636
+ };
637
+
638
+ private getOauthConfigForMode = async (
639
+ mode: OauthMode
640
+ ): Promise<OauthConfig> => {
641
+ if (this.oauthConfig) {
642
+ return this.oauthConfig;
643
+ } else if (mode === "redirect") {
644
+ return this.initOauth();
645
+ } else {
646
+ throw new Error(
647
+ "enablePopupOauth must be set in configuration or signer.preparePopupOauth must be called before using popup-based OAuth login"
648
+ );
649
+ }
650
+ };
651
+ }
652
+
653
+ function resolveRelativeUrl(url: string): string {
654
+ // Funny trick.
655
+ const a = document.createElement("a");
656
+ a.href = url;
657
+ return a.href;
426
658
  }
@@ -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"]>;
@@ -46,12 +47,29 @@ export type EmailAuthParams = {
46
47
  redirectParams?: URLSearchParams;
47
48
  };
48
49
 
50
+ export type OauthParams = Extract<AuthParams, { type: "oauth" }> & {
51
+ expirationSeconds?: number;
52
+ };
53
+
49
54
  export type SignupResponse = {
50
55
  orgId: string;
51
56
  userId?: string;
52
57
  address?: Address;
53
58
  };
54
59
 
60
+ export type OauthConfig = {
61
+ codeChallenge: string;
62
+ requestKey: string;
63
+ authProviders: AuthProviderConfig[];
64
+ };
65
+
66
+ export type AuthProviderConfig = {
67
+ id: string;
68
+ isCustomProvider?: boolean;
69
+ clientId: string;
70
+ authEndpoint: string;
71
+ };
72
+
55
73
  export type SignerRoutes = SignerEndpoints[number]["Route"];
56
74
  export type SignerBody<T extends SignerRoutes> = Extract<
57
75
  SignerEndpoints[number],
@@ -106,6 +124,13 @@ export type SignerEndpoints = [
106
124
  Response: {
107
125
  signature: Hex;
108
126
  };
127
+ },
128
+ {
129
+ Route: "/v1/prepare-oauth";
130
+ Body: {
131
+ nonce: string;
132
+ };
133
+ Response: OauthConfig;
109
134
  }
110
135
  ];
111
136
 
@@ -114,6 +139,7 @@ export type AlchemySignerClientEvents = {
114
139
  authenticating(): void;
115
140
  connectedEmail(user: User, bundle: string): void;
116
141
  connectedPasskey(user: User): void;
142
+ connectedOauth(user: User, bundle: string): void;
117
143
  disconnected(): void;
118
144
  };
119
145
 
package/src/oauth.ts ADDED
@@ -0,0 +1,38 @@
1
+ import { sha256 } from "viem";
2
+
3
+ /**
4
+ * Turnkey requires the nonce in the id token to be in this format.
5
+ *
6
+ * @param {string} turnkeyPublicKey key from a Turnkey iframe
7
+ * @returns {string} nonce to be used in OIDC
8
+ */
9
+ export function getOauthNonce(turnkeyPublicKey: string): string {
10
+ return sha256(new TextEncoder().encode(turnkeyPublicKey)).slice(2);
11
+ }
12
+
13
+ export type ScopeAndClaims = {
14
+ scope: string;
15
+ claims?: string;
16
+ };
17
+
18
+ const DEFAULT_SCOPE_AND_CLAIMS: Record<string, ScopeAndClaims> = {
19
+ google: { scope: "openid email" },
20
+ apple: { scope: "openid email" },
21
+ facebook: { scope: "openid email" },
22
+ twitch: {
23
+ scope: "openid user:read:email",
24
+ claims: JSON.stringify({ id_token: { email: null } }),
25
+ },
26
+ };
27
+
28
+ /**
29
+ * Returns the default scope and claims when using a known auth provider
30
+ *
31
+ * @param {string} knownAuthProviderId id of a known auth provider, e.g. "google"
32
+ * @returns {ScopeAndClaims | undefined} default scope and claims
33
+ */
34
+ export function getDefaultScopeAndClaims(
35
+ knownAuthProviderId: string
36
+ ): ScopeAndClaims | undefined {
37
+ return DEFAULT_SCOPE_AND_CLAIMS[knownAuthProviderId];
38
+ }
@@ -9,6 +9,7 @@ import { createStore, type Mutate, type StoreApi } from "zustand/vanilla";
9
9
  import type { BaseSignerClient } from "../client/base";
10
10
  import type { User } from "../client/types";
11
11
  import type { Session, SessionManagerEvents } from "./types";
12
+ import { assertNever } from "../utils/typeAssertions.js";
12
13
 
13
14
  export const DEFAULT_SESSION_MS = 15 * 60 * 1000; // 15 minutes
14
15
 
@@ -82,11 +83,17 @@ export class SessionManager {
82
83
  }
83
84
 
84
85
  switch (existingSession.type) {
85
- case "email": {
86
+ case "email":
87
+ case "oauth": {
88
+ const connectedEventName =
89
+ existingSession.type === "email"
90
+ ? "connectedEmail"
91
+ : "connectedOauth";
86
92
  const result = await this.client
87
- .completeEmailAuth({
93
+ .completeAuthWithBundle({
88
94
  bundle: existingSession.bundle,
89
95
  orgId: existingSession.user.orgId,
96
+ connectedEventName,
90
97
  })
91
98
  .catch((e) => {
92
99
  console.warn("Failed to load user from session", e);
@@ -108,7 +115,10 @@ export class SessionManager {
108
115
  return this.client.lookupUserWithPasskey(existingSession.user);
109
116
  }
110
117
  default:
111
- throw new Error("Unknown session type");
118
+ assertNever(
119
+ existingSession,
120
+ `Unknown session type: ${(existingSession as any).type}`
121
+ );
112
122
  }
113
123
  };
114
124
 
@@ -168,7 +178,7 @@ export class SessionManager {
168
178
 
169
179
  private setSession = (
170
180
  session:
171
- | Omit<Extract<Session, { type: "email" }>, "expirationDateMs">
181
+ | Omit<Extract<Session, { type: "email" | "oauth" }>, "expirationDateMs">
172
182
  | Omit<Extract<Session, { type: "passkey" }>, "expirationDateMs">
173
183
  ) => {
174
184
  this.store.setState({
@@ -211,21 +221,9 @@ export class SessionManager {
211
221
 
212
222
  this.client.on("disconnected", () => this.clearSession());
213
223
 
214
- this.client.on("connectedEmail", (user, bundle) => {
215
- const existingSession = this.getSession();
216
- if (
217
- existingSession != null &&
218
- existingSession.type === "email" &&
219
- existingSession.user.userId === user.userId &&
220
- // if the bundle is different, then we've refreshed the session
221
- // so we need to reset the session
222
- existingSession.bundle === bundle
223
- ) {
224
- return;
225
- }
226
-
227
- this.setSession({ type: "email", user, bundle });
228
- });
224
+ this.client.on("connectedEmail", (user, bundle) =>
225
+ this.setSessionWithUserAndBundle({ type: "email", user, bundle })
226
+ );
229
227
 
230
228
  this.client.on("connectedPasskey", (user) => {
231
229
  const existingSession = this.getSession();
@@ -240,10 +238,38 @@ export class SessionManager {
240
238
  this.setSession({ type: "passkey", user });
241
239
  });
242
240
 
241
+ this.client.on("connectedOauth", (user, bundle) =>
242
+ this.setSessionWithUserAndBundle({ type: "oauth", user, bundle })
243
+ );
244
+
243
245
  // sync local state if persisted state has changed from another tab
244
246
  window.addEventListener("focus", () => {
245
247
  this.store.persist.rehydrate();
246
248
  this.initialize();
247
249
  });
248
250
  };
251
+
252
+ private setSessionWithUserAndBundle = ({
253
+ type,
254
+ user,
255
+ bundle,
256
+ }: {
257
+ type: "email" | "oauth";
258
+ user: User;
259
+ bundle: string;
260
+ }) => {
261
+ const existingSession = this.getSession();
262
+ if (
263
+ existingSession != null &&
264
+ existingSession.type === type &&
265
+ existingSession.user.userId === user.userId &&
266
+ // if the bundle is different, then we've refreshed the session
267
+ // so we need to reset the session
268
+ existingSession.bundle === bundle
269
+ ) {
270
+ return;
271
+ }
272
+
273
+ this.setSession({ type, user, bundle });
274
+ };
249
275
  }
@@ -2,7 +2,7 @@ import type { User } from "../client/types";
2
2
 
3
3
  export type Session =
4
4
  | {
5
- type: "email";
5
+ type: "email" | "oauth";
6
6
  bundle: string;
7
7
  expirationDateMs: number;
8
8
  user: User;