@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.
- package/dist/esm/base.d.ts +33 -1
- package/dist/esm/base.js +81 -7
- package/dist/esm/base.js.map +1 -1
- package/dist/esm/client/base.d.ts +28 -5
- package/dist/esm/client/base.js +25 -1
- package/dist/esm/client/base.js.map +1 -1
- package/dist/esm/client/index.d.ts +78 -5
- package/dist/esm/client/index.js +211 -6
- package/dist/esm/client/index.js.map +1 -1
- package/dist/esm/client/types.d.ts +25 -0
- package/dist/esm/client/types.js.map +1 -1
- package/dist/esm/oauth.d.ts +18 -0
- package/dist/esm/oauth.js +29 -0
- package/dist/esm/oauth.js.map +1 -0
- package/dist/esm/session/manager.d.ts +3 -2
- package/dist/esm/session/manager.js +28 -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 +39 -7
- package/dist/esm/signer.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 +33 -1
- package/dist/types/base.d.ts.map +1 -1
- package/dist/types/client/base.d.ts +28 -5
- package/dist/types/client/base.d.ts.map +1 -1
- package/dist/types/client/index.d.ts +78 -5
- package/dist/types/client/index.d.ts.map +1 -1
- package/dist/types/client/types.d.ts +25 -0
- package/dist/types/client/types.d.ts.map +1 -1
- package/dist/types/oauth.d.ts +19 -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 +39 -7
- package/dist/types/signer.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/package.json +3 -3
- package/src/base.ts +80 -10
- package/src/client/base.ts +31 -3
- package/src/client/index.ts +244 -12
- package/src/client/types.ts +26 -0
- package/src/oauth.ts +38 -0
- package/src/session/manager.ts +45 -19
- package/src/session/types.ts +1 -1
- package/src/signer.ts +15 -1
- package/src/utils/typeAssertions.ts +3 -0
- package/src/version.ts +1 -1
package/src/client/base.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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();
|
package/src/client/index.ts
CHANGED
|
@@ -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
|
|
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.
|
|
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
|
|
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(
|
|
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 (
|
|
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
|
}
|
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"]>;
|
|
@@ -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
|
+
}
|
package/src/session/manager.ts
CHANGED
|
@@ -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
|
-
.
|
|
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
|
-
|
|
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
|
-
|
|
216
|
-
|
|
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
|
}
|