@account-kit/signer 4.0.0-alpha.0

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 (121) hide show
  1. package/LICENSE +21 -0
  2. package/dist/cjs/base.d.ts +37 -0
  3. package/dist/cjs/base.js +292 -0
  4. package/dist/cjs/base.js.map +1 -0
  5. package/dist/cjs/client/base.d.ts +230 -0
  6. package/dist/cjs/client/base.js +298 -0
  7. package/dist/cjs/client/base.js.map +1 -0
  8. package/dist/cjs/client/index.d.ts +146 -0
  9. package/dist/cjs/client/index.js +260 -0
  10. package/dist/cjs/client/index.js.map +1 -0
  11. package/dist/cjs/client/types.d.ts +106 -0
  12. package/dist/cjs/client/types.js +3 -0
  13. package/dist/cjs/client/types.js.map +1 -0
  14. package/dist/cjs/errors.d.ts +4 -0
  15. package/dist/cjs/errors.js +16 -0
  16. package/dist/cjs/errors.js.map +1 -0
  17. package/dist/cjs/index.d.ts +8 -0
  18. package/dist/cjs/index.js +14 -0
  19. package/dist/cjs/index.js.map +1 -0
  20. package/dist/cjs/package.json +1 -0
  21. package/dist/cjs/session/manager.d.ts +45 -0
  22. package/dist/cjs/session/manager.js +230 -0
  23. package/dist/cjs/session/manager.js.map +1 -0
  24. package/dist/cjs/session/types.d.ts +16 -0
  25. package/dist/cjs/session/types.js +3 -0
  26. package/dist/cjs/session/types.js.map +1 -0
  27. package/dist/cjs/signer.d.ts +262 -0
  28. package/dist/cjs/signer.js +34 -0
  29. package/dist/cjs/signer.js.map +1 -0
  30. package/dist/cjs/types.d.ts +14 -0
  31. package/dist/cjs/types.js +12 -0
  32. package/dist/cjs/types.js.map +1 -0
  33. package/dist/cjs/utils/base64UrlEncode.d.ts +1 -0
  34. package/dist/cjs/utils/base64UrlEncode.js +12 -0
  35. package/dist/cjs/utils/base64UrlEncode.js.map +1 -0
  36. package/dist/cjs/utils/generateRandomBuffer.d.ts +1 -0
  37. package/dist/cjs/utils/generateRandomBuffer.js +10 -0
  38. package/dist/cjs/utils/generateRandomBuffer.js.map +1 -0
  39. package/dist/cjs/version.d.ts +1 -0
  40. package/dist/cjs/version.js +5 -0
  41. package/dist/cjs/version.js.map +1 -0
  42. package/dist/esm/base.d.ts +37 -0
  43. package/dist/esm/base.js +288 -0
  44. package/dist/esm/base.js.map +1 -0
  45. package/dist/esm/client/base.d.ts +230 -0
  46. package/dist/esm/client/base.js +291 -0
  47. package/dist/esm/client/base.js.map +1 -0
  48. package/dist/esm/client/index.d.ts +146 -0
  49. package/dist/esm/client/index.js +256 -0
  50. package/dist/esm/client/index.js.map +1 -0
  51. package/dist/esm/client/types.d.ts +106 -0
  52. package/dist/esm/client/types.js +2 -0
  53. package/dist/esm/client/types.js.map +1 -0
  54. package/dist/esm/errors.d.ts +4 -0
  55. package/dist/esm/errors.js +12 -0
  56. package/dist/esm/errors.js.map +1 -0
  57. package/dist/esm/index.d.ts +8 -0
  58. package/dist/esm/index.js +6 -0
  59. package/dist/esm/index.js.map +1 -0
  60. package/dist/esm/package.json +1 -0
  61. package/dist/esm/session/manager.d.ts +45 -0
  62. package/dist/esm/session/manager.js +223 -0
  63. package/dist/esm/session/manager.js.map +1 -0
  64. package/dist/esm/session/types.d.ts +16 -0
  65. package/dist/esm/session/types.js +2 -0
  66. package/dist/esm/session/types.js.map +1 -0
  67. package/dist/esm/signer.d.ts +262 -0
  68. package/dist/esm/signer.js +30 -0
  69. package/dist/esm/signer.js.map +1 -0
  70. package/dist/esm/types.d.ts +14 -0
  71. package/dist/esm/types.js +9 -0
  72. package/dist/esm/types.js.map +1 -0
  73. package/dist/esm/utils/base64UrlEncode.d.ts +1 -0
  74. package/dist/esm/utils/base64UrlEncode.js +8 -0
  75. package/dist/esm/utils/base64UrlEncode.js.map +1 -0
  76. package/dist/esm/utils/generateRandomBuffer.d.ts +1 -0
  77. package/dist/esm/utils/generateRandomBuffer.js +6 -0
  78. package/dist/esm/utils/generateRandomBuffer.js.map +1 -0
  79. package/dist/esm/version.d.ts +1 -0
  80. package/dist/esm/version.js +2 -0
  81. package/dist/esm/version.js.map +1 -0
  82. package/dist/types/base.d.ts +89 -0
  83. package/dist/types/base.d.ts.map +1 -0
  84. package/dist/types/client/base.d.ts +246 -0
  85. package/dist/types/client/base.d.ts.map +1 -0
  86. package/dist/types/client/index.d.ts +151 -0
  87. package/dist/types/client/index.d.ts.map +1 -0
  88. package/dist/types/client/types.d.ts +107 -0
  89. package/dist/types/client/types.d.ts.map +1 -0
  90. package/dist/types/errors.d.ts +5 -0
  91. package/dist/types/errors.d.ts.map +1 -0
  92. package/dist/types/index.d.ts +9 -0
  93. package/dist/types/index.d.ts.map +1 -0
  94. package/dist/types/session/manager.d.ts +46 -0
  95. package/dist/types/session/manager.d.ts.map +1 -0
  96. package/dist/types/session/types.d.ts +17 -0
  97. package/dist/types/session/types.d.ts.map +1 -0
  98. package/dist/types/signer.d.ts +269 -0
  99. package/dist/types/signer.d.ts.map +1 -0
  100. package/dist/types/types.d.ts +15 -0
  101. package/dist/types/types.d.ts.map +1 -0
  102. package/dist/types/utils/base64UrlEncode.d.ts +2 -0
  103. package/dist/types/utils/base64UrlEncode.d.ts.map +1 -0
  104. package/dist/types/utils/generateRandomBuffer.d.ts +2 -0
  105. package/dist/types/utils/generateRandomBuffer.d.ts.map +1 -0
  106. package/dist/types/version.d.ts +2 -0
  107. package/dist/types/version.d.ts.map +1 -0
  108. package/package.json +79 -0
  109. package/src/base.ts +386 -0
  110. package/src/client/base.ts +399 -0
  111. package/src/client/index.ts +267 -0
  112. package/src/client/types.ts +121 -0
  113. package/src/errors.ts +15 -0
  114. package/src/index.ts +10 -0
  115. package/src/session/manager.ts +249 -0
  116. package/src/session/types.ts +16 -0
  117. package/src/signer.ts +55 -0
  118. package/src/types.ts +17 -0
  119. package/src/utils/base64UrlEncode.ts +7 -0
  120. package/src/utils/generateRandomBuffer.ts +5 -0
  121. package/src/version.ts +3 -0
@@ -0,0 +1,399 @@
1
+ import { type ConnectionConfig, ConnectionConfigSchema } from "@aa-sdk/core";
2
+ import { TurnkeyClient } from "@turnkey/http";
3
+ import EventEmitter from "eventemitter3";
4
+ import type { Hex } from "viem";
5
+ import { NotAuthenticatedError } from "../errors.js";
6
+ import { base64UrlEncode } from "../utils/base64UrlEncode.js";
7
+ import type {
8
+ AlchemySignerClientEvent,
9
+ AlchemySignerClientEvents,
10
+ CreateAccountParams,
11
+ EmailAuthParams,
12
+ GetWebAuthnAttestationResult,
13
+ SignerBody,
14
+ SignerResponse,
15
+ SignerRoutes,
16
+ SignupResponse,
17
+ User,
18
+ } from "./types.js";
19
+
20
+ export interface BaseSignerClientParams {
21
+ stamper: TurnkeyClient["stamper"];
22
+ connection: ConnectionConfig;
23
+ rootOrgId?: string;
24
+ rpId?: string;
25
+ }
26
+
27
+ export type ExportWalletStamper = TurnkeyClient["stamper"] & {
28
+ injectWalletExportBundle(bundle: string): Promise<boolean>;
29
+ injectKeyExportBundle(bundle: string): Promise<boolean>;
30
+ publicKey(): string | null;
31
+ };
32
+
33
+ export abstract class BaseSignerClient<TExportWalletParams = unknown> {
34
+ private _user: User | undefined;
35
+ private connectionConfig: ConnectionConfig;
36
+ protected turnkeyClient: TurnkeyClient;
37
+ protected rootOrg: string;
38
+ protected eventEmitter: EventEmitter<AlchemySignerClientEvents>;
39
+
40
+ constructor(params: BaseSignerClientParams) {
41
+ const { stamper, connection, rootOrgId } = params;
42
+
43
+ this.rootOrg = rootOrgId ?? "24c1acf5-810f-41e0-a503-d5d13fa8e830";
44
+ this.eventEmitter = new EventEmitter<AlchemySignerClientEvents>();
45
+ this.connectionConfig = ConnectionConfigSchema.parse(connection);
46
+ this.turnkeyClient = new TurnkeyClient(
47
+ { baseUrl: "https://api.turnkey.com" },
48
+ stamper
49
+ );
50
+ }
51
+
52
+ protected get user() {
53
+ return this._user;
54
+ }
55
+
56
+ protected set user(user: User | undefined) {
57
+ if (user && !this._user) {
58
+ this.eventEmitter.emit("connected", user);
59
+ } else if (!user && this._user) {
60
+ this.eventEmitter.emit("disconnected");
61
+ }
62
+
63
+ this._user = user;
64
+ }
65
+
66
+ protected setStamper(stamper: TurnkeyClient["stamper"]) {
67
+ this.turnkeyClient.stamper = stamper;
68
+ }
69
+
70
+ protected exportWalletInner(params: {
71
+ exportStamper: ExportWalletStamper;
72
+ exportAs: "SEED_PHRASE" | "PRIVATE_KEY";
73
+ }): Promise<boolean> {
74
+ switch (params.exportAs) {
75
+ case "PRIVATE_KEY":
76
+ return this.exportAsPrivateKey(params.exportStamper);
77
+ case "SEED_PHRASE":
78
+ return this.exportAsSeedPhrase(params.exportStamper);
79
+ }
80
+ }
81
+
82
+ // #region ABSTRACT METHODS
83
+
84
+ public abstract createAccount(
85
+ params: CreateAccountParams
86
+ ): Promise<SignupResponse>;
87
+
88
+ public abstract initEmailAuth(
89
+ params: Omit<EmailAuthParams, "targetPublicKey">
90
+ ): Promise<{ orgId: string }>;
91
+
92
+ public abstract completeEmailAuth(params: {
93
+ bundle: string;
94
+ orgId: string;
95
+ }): Promise<User>;
96
+
97
+ public abstract disconnect(): Promise<void>;
98
+
99
+ public abstract exportWallet(params: TExportWalletParams): Promise<boolean>;
100
+
101
+ public abstract lookupUserWithPasskey(user?: User): Promise<User>;
102
+
103
+ protected abstract getWebAuthnAttestation(
104
+ options: CredentialCreationOptions,
105
+ userDetails?: { username: string }
106
+ ): Promise<GetWebAuthnAttestationResult>;
107
+
108
+ // #endregion
109
+
110
+ // #region PUBLIC METHODS
111
+
112
+ /**
113
+ * Listen to events emitted by the client
114
+ *
115
+ * @param event the event you want to listen to
116
+ * @param listener the callback function to execute when an event is fired
117
+ * @returns a function that will remove the listener when called
118
+ */
119
+ public on = <E extends AlchemySignerClientEvent>(
120
+ event: E,
121
+ listener: AlchemySignerClientEvents[E]
122
+ ) => {
123
+ this.eventEmitter.on(event, listener as any);
124
+
125
+ return () => this.eventEmitter.removeListener(event, listener as any);
126
+ };
127
+
128
+ public addPasskey = async (options: CredentialCreationOptions) => {
129
+ if (!this.user) {
130
+ throw new NotAuthenticatedError();
131
+ }
132
+ const { attestation, challenge } = await this.getWebAuthnAttestation(
133
+ options
134
+ );
135
+
136
+ const { activity } = await this.turnkeyClient.createAuthenticators({
137
+ type: "ACTIVITY_TYPE_CREATE_AUTHENTICATORS_V2",
138
+ timestampMs: Date.now().toString(),
139
+ organizationId: this.user.orgId,
140
+ parameters: {
141
+ userId: this.user.userId,
142
+ authenticators: [
143
+ {
144
+ attestation,
145
+ authenticatorName: `passkey-${Date.now().toString()}`,
146
+ challenge: base64UrlEncode(challenge),
147
+ },
148
+ ],
149
+ },
150
+ });
151
+
152
+ const { authenticatorIds } = await this.pollActivityCompletion(
153
+ activity,
154
+ this.user.orgId,
155
+ "createAuthenticatorsResult"
156
+ );
157
+
158
+ return authenticatorIds;
159
+ };
160
+
161
+ public whoami = async (orgId = this.user?.orgId): Promise<User> => {
162
+ if (this.user) {
163
+ return this.user;
164
+ }
165
+
166
+ if (!orgId) {
167
+ throw new Error("No orgId provided");
168
+ }
169
+
170
+ const stampedRequest = await this.turnkeyClient.stampGetWhoami({
171
+ organizationId: orgId,
172
+ });
173
+
174
+ const user = await this.request("/v1/whoami", {
175
+ stampedRequest,
176
+ });
177
+
178
+ const credentialId = (() => {
179
+ try {
180
+ return JSON.parse(stampedRequest?.stamp.stampHeaderValue)
181
+ .credentialId as string;
182
+ } catch (e) {
183
+ return undefined;
184
+ }
185
+ })();
186
+
187
+ this.user = {
188
+ ...user,
189
+ credentialId,
190
+ };
191
+
192
+ return this.user;
193
+ };
194
+
195
+ public lookupUserByEmail = async (email: string) => {
196
+ return this.request("/v1/lookup", { email });
197
+ };
198
+
199
+ /**
200
+ * This will sign a message with the user's private key, without doing any transformations on the message.
201
+ * For SignMessage or SignTypedData, the caller should hash the message before calling this method and pass
202
+ * that result here.
203
+ *
204
+ * @param msg the hex representation of the bytes to sign
205
+ * @returns the signature over the raw hex
206
+ */
207
+ public signRawMessage = async (msg: Hex) => {
208
+ if (!this.user) {
209
+ throw new NotAuthenticatedError();
210
+ }
211
+
212
+ const stampedRequest = await this.turnkeyClient.stampSignRawPayload({
213
+ organizationId: this.user.orgId,
214
+ type: "ACTIVITY_TYPE_SIGN_RAW_PAYLOAD_V2",
215
+ timestampMs: Date.now().toString(),
216
+ parameters: {
217
+ encoding: "PAYLOAD_ENCODING_HEXADECIMAL",
218
+ hashFunction: "HASH_FUNCTION_NO_OP",
219
+ payload: msg,
220
+ signWith: this.user.address,
221
+ },
222
+ });
223
+
224
+ const { signature } = await this.request("/v1/sign-payload", {
225
+ stampedRequest,
226
+ });
227
+
228
+ return signature;
229
+ };
230
+
231
+ public getUser = (): User | null => {
232
+ return this.user ?? null;
233
+ };
234
+
235
+ public request = async <R extends SignerRoutes>(
236
+ route: R,
237
+ body: SignerBody<R>
238
+ ): Promise<SignerResponse<R>> => {
239
+ const url = this.connectionConfig.rpcUrl ?? "https://api.g.alchemy.com";
240
+ const basePath = "/signer";
241
+
242
+ const headers = new Headers();
243
+ headers.append("Content-Type", "application/json");
244
+ if (this.connectionConfig.apiKey) {
245
+ headers.append("Authorization", `Bearer ${this.connectionConfig.apiKey}`);
246
+ } else if (this.connectionConfig.jwt) {
247
+ headers.append("Authorization", `Bearer ${this.connectionConfig.jwt}`);
248
+ }
249
+
250
+ const response = await fetch(`${url}${basePath}${route}`, {
251
+ method: "POST",
252
+ body: JSON.stringify(body),
253
+ headers,
254
+ });
255
+
256
+ if (!response.ok) {
257
+ throw new Error(await response.text());
258
+ }
259
+
260
+ const json = await response.json();
261
+
262
+ return json as SignerResponse<R>;
263
+ };
264
+
265
+ // #endregion
266
+
267
+ // #region PRIVATE METHODS
268
+ private exportAsSeedPhrase = async (stamper: ExportWalletStamper) => {
269
+ if (!this.user) {
270
+ throw new NotAuthenticatedError();
271
+ }
272
+
273
+ const { wallets } = await this.turnkeyClient.getWallets({
274
+ organizationId: this.user.orgId,
275
+ });
276
+
277
+ const walletAccounts = await Promise.all(
278
+ wallets.map(({ walletId }) =>
279
+ this.turnkeyClient.getWalletAccounts({
280
+ organizationId: this.user!.orgId,
281
+ walletId,
282
+ })
283
+ )
284
+ ).then((x) => x.flatMap((x) => x.accounts));
285
+
286
+ const walletAccount = walletAccounts.find(
287
+ (x) => x.address === this.user!.address
288
+ );
289
+
290
+ if (!walletAccount) {
291
+ throw new Error(
292
+ `Could not find wallet associated with ${this.user.address}`
293
+ );
294
+ }
295
+
296
+ const { activity } = await this.turnkeyClient.exportWallet({
297
+ organizationId: this.user.orgId,
298
+ type: "ACTIVITY_TYPE_EXPORT_WALLET",
299
+ timestampMs: Date.now().toString(),
300
+ parameters: {
301
+ walletId: walletAccount!.walletId,
302
+ targetPublicKey: stamper.publicKey()!,
303
+ },
304
+ });
305
+
306
+ const { exportBundle } = await this.pollActivityCompletion(
307
+ activity,
308
+ this.user.orgId,
309
+ "exportWalletResult"
310
+ );
311
+
312
+ const result = await stamper.injectWalletExportBundle(exportBundle);
313
+
314
+ if (!result) {
315
+ throw new Error("Failed to inject wallet export bundle");
316
+ }
317
+
318
+ return result;
319
+ };
320
+
321
+ private exportAsPrivateKey = async (stamper: ExportWalletStamper) => {
322
+ if (!this.user) {
323
+ throw new NotAuthenticatedError();
324
+ }
325
+
326
+ const { activity } = await this.turnkeyClient.exportWalletAccount({
327
+ organizationId: this.user.orgId,
328
+ type: "ACTIVITY_TYPE_EXPORT_WALLET_ACCOUNT",
329
+ timestampMs: Date.now().toString(),
330
+ parameters: {
331
+ address: this.user.address,
332
+ targetPublicKey: stamper.publicKey()!,
333
+ },
334
+ });
335
+
336
+ const { exportBundle } = await this.pollActivityCompletion(
337
+ activity,
338
+ this.user.orgId,
339
+ "exportWalletAccountResult"
340
+ );
341
+
342
+ const result = await stamper.injectKeyExportBundle(exportBundle);
343
+
344
+ if (!result) {
345
+ throw new Error("Failed to inject wallet export bundle");
346
+ }
347
+
348
+ return result;
349
+ };
350
+
351
+ protected pollActivityCompletion = async <
352
+ T extends keyof Awaited<
353
+ ReturnType<(typeof this.turnkeyClient)["getActivity"]>
354
+ >["activity"]["result"]
355
+ >(
356
+ activity: Awaited<
357
+ ReturnType<(typeof this.turnkeyClient)["getActivity"]>
358
+ >["activity"],
359
+ organizationId: string,
360
+ resultKey: T
361
+ ): Promise<
362
+ NonNullable<
363
+ Awaited<
364
+ ReturnType<(typeof this.turnkeyClient)["getActivity"]>
365
+ >["activity"]["result"][T]
366
+ >
367
+ > => {
368
+ if (activity.status === "ACTIVITY_STATUS_COMPLETED") {
369
+ return activity.result[resultKey]!;
370
+ }
371
+
372
+ const {
373
+ activity: { status, id, result },
374
+ } = await this.turnkeyClient.getActivity({
375
+ activityId: activity.id,
376
+ organizationId,
377
+ });
378
+
379
+ if (status === "ACTIVITY_STATUS_COMPLETED") {
380
+ return result[resultKey]!;
381
+ }
382
+
383
+ if (
384
+ status === "ACTIVITY_STATUS_FAILED" ||
385
+ status === "ACTIVITY_STATUS_REJECTED" ||
386
+ status === "ACTIVITY_STATUS_CONSENSUS_NEEDED"
387
+ ) {
388
+ throw new Error(
389
+ `Failed to get activity with with id ${id} (status: ${status})`
390
+ );
391
+ }
392
+
393
+ // TODO: add ability to configure this + add exponential backoff
394
+ await new Promise((resolve) => setTimeout(resolve, 500));
395
+
396
+ return this.pollActivityCompletion(activity, organizationId, resultKey);
397
+ };
398
+ // #endregion
399
+ }
@@ -0,0 +1,267 @@
1
+ import { ConnectionConfigSchema } from "@aa-sdk/core";
2
+ import { getWebAuthnAttestation } from "@turnkey/http";
3
+ import { IframeStamper } from "@turnkey/iframe-stamper";
4
+ import { WebauthnStamper } from "@turnkey/webauthn-stamper";
5
+ import { z } from "zod";
6
+ import { base64UrlEncode } from "../utils/base64UrlEncode.js";
7
+ import { generateRandomBuffer } from "../utils/generateRandomBuffer.js";
8
+ import { BaseSignerClient } from "./base.js";
9
+ import type {
10
+ CreateAccountParams,
11
+ CredentialCreationOptionOverrides,
12
+ EmailAuthParams,
13
+ ExportWalletParams,
14
+ User,
15
+ } from "./types.js";
16
+
17
+ export const AlchemySignerClientParamsSchema = z.object({
18
+ connection: ConnectionConfigSchema,
19
+ iframeConfig: z.object({
20
+ iframeElementId: z.string().default("turnkey-iframe"),
21
+ iframeContainerId: z.string(),
22
+ }),
23
+ rpId: z.string().optional(),
24
+ rootOrgId: z
25
+ .string()
26
+ .optional()
27
+ .default("24c1acf5-810f-41e0-a503-d5d13fa8e830"),
28
+ });
29
+
30
+ export type AlchemySignerClientParams = z.input<
31
+ typeof AlchemySignerClientParamsSchema
32
+ >;
33
+
34
+ /**
35
+ * A lower level client used by the AlchemySigner used to communicate with
36
+ * Alchemy's signer service.
37
+ */
38
+ export class AlchemySignerWebClient extends BaseSignerClient<ExportWalletParams> {
39
+ private iframeStamper: IframeStamper;
40
+ private webauthnStamper: WebauthnStamper;
41
+ iframeContainerId: string;
42
+
43
+ constructor(params: AlchemySignerClientParams) {
44
+ const { connection, iframeConfig, rpId, rootOrgId } =
45
+ AlchemySignerClientParamsSchema.parse(params);
46
+
47
+ const iframeStamper = new IframeStamper({
48
+ iframeElementId: iframeConfig.iframeElementId,
49
+ iframeUrl: "https://auth.turnkey.com",
50
+ iframeContainer: document.getElementById(iframeConfig.iframeContainerId),
51
+ });
52
+
53
+ super({
54
+ connection,
55
+ rootOrgId,
56
+ stamper: iframeStamper,
57
+ });
58
+
59
+ this.iframeStamper = iframeStamper;
60
+ this.iframeContainerId = iframeConfig.iframeContainerId;
61
+
62
+ this.webauthnStamper = new WebauthnStamper({
63
+ rpId: rpId ?? window.location.hostname,
64
+ });
65
+ }
66
+
67
+ public createAccount = async (params: CreateAccountParams) => {
68
+ this.eventEmitter.emit("authenticating");
69
+ if (params.type === "email") {
70
+ const { email, expirationSeconds } = params;
71
+ const publicKey = await this.initIframeStamper();
72
+
73
+ const response = await this.request("/v1/signup", {
74
+ email,
75
+ targetPublicKey: publicKey,
76
+ expirationSeconds,
77
+ redirectParams: params.redirectParams?.toString(),
78
+ });
79
+
80
+ return response;
81
+ }
82
+
83
+ // Passkey account creation flow
84
+ const { attestation, challenge } = await this.getWebAuthnAttestation(
85
+ params.creationOpts,
86
+ { username: params.username }
87
+ );
88
+
89
+ const result = await this.request("/v1/signup", {
90
+ passkey: {
91
+ challenge: base64UrlEncode(challenge),
92
+ attestation,
93
+ },
94
+ });
95
+
96
+ this.user = {
97
+ orgId: result.orgId,
98
+ address: result.address!,
99
+ userId: result.userId!,
100
+ credentialId: attestation.credentialId,
101
+ };
102
+ this.initWebauthnStamper(this.user);
103
+ this.eventEmitter.emit("connectedPasskey", this.user);
104
+
105
+ return result;
106
+ };
107
+
108
+ public initEmailAuth = async (
109
+ params: Omit<EmailAuthParams, "targetPublicKey">
110
+ ) => {
111
+ this.eventEmitter.emit("authenticating");
112
+ const { email, expirationSeconds } = params;
113
+ const publicKey = await this.initIframeStamper();
114
+
115
+ return this.request("/v1/auth", {
116
+ email,
117
+ targetPublicKey: publicKey,
118
+ expirationSeconds,
119
+ redirectParams: params.redirectParams?.toString(),
120
+ });
121
+ };
122
+
123
+ public completeEmailAuth = async ({
124
+ bundle,
125
+ orgId,
126
+ }: {
127
+ bundle: string;
128
+ orgId: string;
129
+ }) => {
130
+ this.eventEmitter.emit("authenticating");
131
+ await this.initIframeStamper();
132
+
133
+ const result = await this.iframeStamper.injectCredentialBundle(bundle);
134
+
135
+ if (!result) {
136
+ throw new Error("Failed to inject credential bundle");
137
+ }
138
+
139
+ const user = await this.whoami(orgId);
140
+ this.eventEmitter.emit("connectedEmail", user, bundle);
141
+
142
+ return user;
143
+ };
144
+
145
+ public lookupUserWithPasskey = async (user: User | undefined = undefined) => {
146
+ this.eventEmitter.emit("authenticating");
147
+ await this.initWebauthnStamper(user);
148
+ if (user) {
149
+ this.user = user;
150
+ return user;
151
+ }
152
+
153
+ const result = await this.whoami(this.rootOrg);
154
+ await this.initWebauthnStamper(result);
155
+ this.eventEmitter.emit("connectedPasskey", result);
156
+
157
+ return result;
158
+ };
159
+
160
+ public exportWallet = async ({
161
+ iframeContainerId,
162
+ iframeElementId = "turnkey-export-iframe",
163
+ }: ExportWalletParams) => {
164
+ const exportWalletIframeStamper = new IframeStamper({
165
+ iframeContainer: document.getElementById(iframeContainerId),
166
+ iframeElementId: iframeElementId,
167
+ iframeUrl: "https://export.turnkey.com",
168
+ });
169
+ await exportWalletIframeStamper.init();
170
+
171
+ if (this.turnkeyClient.stamper === this.iframeStamper) {
172
+ return this.exportWalletInner({
173
+ exportStamper: exportWalletIframeStamper,
174
+ exportAs: "SEED_PHRASE",
175
+ });
176
+ }
177
+
178
+ return this.exportWalletInner({
179
+ exportStamper: exportWalletIframeStamper,
180
+ exportAs: "PRIVATE_KEY",
181
+ });
182
+ };
183
+
184
+ public disconnect = async () => {
185
+ this.user = undefined;
186
+ this.iframeStamper.clear();
187
+ };
188
+
189
+ private initIframeStamper = async () => {
190
+ if (!this.iframeStamper.publicKey()) {
191
+ await this.iframeStamper.init();
192
+ }
193
+
194
+ this.setStamper(this.iframeStamper);
195
+
196
+ return this.iframeStamper.publicKey()!;
197
+ };
198
+
199
+ private initWebauthnStamper = async (user: User | undefined = this.user) => {
200
+ this.setStamper(this.webauthnStamper);
201
+ if (user && user.credentialId) {
202
+ // The goal here is to allow us to cache the allowed credential, but this doesn't work with hybrid transport :(
203
+ this.webauthnStamper.allowCredentials = [
204
+ {
205
+ id: Buffer.from(user.credentialId, "base64"),
206
+ type: "public-key",
207
+ transports: ["internal", "hybrid"],
208
+ },
209
+ ];
210
+ }
211
+ };
212
+
213
+ protected getWebAuthnAttestation = async (
214
+ options?: CredentialCreationOptionOverrides,
215
+ userDetails: { username: string } = {
216
+ username: this.user?.email ?? "anonymous",
217
+ }
218
+ ) => {
219
+ const challenge = generateRandomBuffer();
220
+ const authenticatorUserId = generateRandomBuffer();
221
+
222
+ const attestation = await getWebAuthnAttestation({
223
+ publicKey: {
224
+ ...options?.publicKey,
225
+ authenticatorSelection: {
226
+ residentKey: "preferred",
227
+ requireResidentKey: false,
228
+ userVerification: "preferred",
229
+ ...options?.publicKey?.authenticatorSelection,
230
+ },
231
+ challenge,
232
+ rp: {
233
+ id: window.location.hostname,
234
+ name: window.location.hostname,
235
+ ...options?.publicKey?.rp,
236
+ },
237
+ pubKeyCredParams: [
238
+ {
239
+ type: "public-key",
240
+ alg: -7,
241
+ },
242
+ {
243
+ type: "public-key",
244
+ alg: -257,
245
+ },
246
+ ],
247
+ user: {
248
+ id: authenticatorUserId,
249
+ name: userDetails.username,
250
+ displayName: userDetails.username,
251
+ ...options?.publicKey?.user,
252
+ },
253
+ },
254
+ signal: options?.signal,
255
+ });
256
+
257
+ // on iOS sometimes this is returned as empty or null, so handling that here
258
+ if (attestation.transports == null || attestation.transports.length === 0) {
259
+ attestation.transports = [
260
+ "AUTHENTICATOR_TRANSPORT_INTERNAL",
261
+ "AUTHENTICATOR_TRANSPORT_HYBRID",
262
+ ];
263
+ }
264
+
265
+ return { challenge, authenticatorUserId, attestation };
266
+ };
267
+ }