@account-kit/signer 4.0.0-beta.0 → 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.
- package/dist/esm/base.d.ts +43 -8
- package/dist/esm/base.js +186 -29
- package/dist/esm/base.js.map +1 -1
- package/dist/esm/client/base.d.ts +22 -4
- package/dist/esm/client/base.js +36 -2
- package/dist/esm/client/base.js.map +1 -1
- package/dist/esm/client/index.d.ts +108 -7
- package/dist/esm/client/index.js +282 -14
- package/dist/esm/client/index.js.map +1 -1
- package/dist/esm/client/types.d.ts +31 -1
- package/dist/esm/client/types.js.map +1 -1
- package/dist/esm/index.d.ts +1 -1
- package/dist/esm/index.js +1 -1
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/metrics.d.ts +17 -0
- package/dist/esm/metrics.js +7 -0
- package/dist/esm/metrics.js.map +1 -0
- package/dist/esm/oauth.d.ts +19 -0
- package/dist/esm/oauth.js +26 -0
- package/dist/esm/oauth.js.map +1 -0
- package/dist/esm/session/manager.d.ts +3 -2
- package/dist/esm/session/manager.js +29 -15
- package/dist/esm/session/manager.js.map +1 -1
- package/dist/esm/session/types.d.ts +1 -1
- package/dist/esm/session/types.js.map +1 -1
- package/dist/esm/signer.d.ts +52 -7
- package/dist/esm/signer.js +46 -3
- package/dist/esm/signer.js.map +1 -1
- package/dist/esm/types.d.ts +8 -1
- package/dist/esm/types.js +3 -1
- package/dist/esm/types.js.map +1 -1
- package/dist/esm/utils/typeAssertions.d.ts +1 -0
- package/dist/esm/utils/typeAssertions.js +4 -0
- package/dist/esm/utils/typeAssertions.js.map +1 -0
- package/dist/esm/version.d.ts +1 -1
- package/dist/esm/version.js +1 -1
- package/dist/esm/version.js.map +1 -1
- package/dist/types/base.d.ts +43 -8
- package/dist/types/base.d.ts.map +1 -1
- package/dist/types/client/base.d.ts +22 -4
- package/dist/types/client/base.d.ts.map +1 -1
- package/dist/types/client/index.d.ts +108 -7
- package/dist/types/client/index.d.ts.map +1 -1
- package/dist/types/client/types.d.ts +31 -1
- package/dist/types/client/types.d.ts.map +1 -1
- package/dist/types/index.d.ts +1 -1
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/metrics.d.ts +18 -0
- package/dist/types/metrics.d.ts.map +1 -0
- package/dist/types/oauth.d.ts +20 -0
- package/dist/types/oauth.d.ts.map +1 -0
- package/dist/types/session/manager.d.ts +3 -2
- package/dist/types/session/manager.d.ts.map +1 -1
- package/dist/types/session/types.d.ts +1 -1
- package/dist/types/session/types.d.ts.map +1 -1
- package/dist/types/signer.d.ts +52 -7
- package/dist/types/signer.d.ts.map +1 -1
- package/dist/types/types.d.ts +8 -1
- package/dist/types/types.d.ts.map +1 -1
- package/dist/types/utils/typeAssertions.d.ts +2 -0
- package/dist/types/utils/typeAssertions.d.ts.map +1 -0
- package/dist/types/version.d.ts +1 -1
- package/dist/types/version.d.ts.map +1 -1
- package/package.json +7 -10
- package/src/base.ts +261 -66
- package/src/client/base.ts +49 -4
- package/src/client/index.ts +317 -20
- package/src/client/types.ts +33 -1
- package/src/index.ts +5 -1
- package/src/metrics.ts +23 -0
- package/src/oauth.ts +36 -0
- package/src/session/manager.ts +46 -19
- package/src/session/types.ts +1 -1
- package/src/signer.ts +91 -4
- package/src/types.ts +9 -1
- package/src/utils/typeAssertions.ts +3 -0
- package/src/version.ts +1 -1
package/src/client/index.ts
CHANGED
|
@@ -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
|
|
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.
|
|
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
|
|
212
|
-
*
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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 (
|
|
259
|
-
|
|
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
|
}
|
package/src/client/types.ts
CHANGED
|
@@ -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 {
|
|
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
|
+
}
|