@rhinestone/1auth 0.1.0 → 0.1.2

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/index.mjs CHANGED
@@ -1,5 +1,6 @@
1
1
  import {
2
2
  buildTransactionReview,
3
+ createOneAuthProvider,
3
4
  createPasskeyProvider,
4
5
  encodeWebAuthnSignature,
5
6
  getAllSupportedChainsAndTokens,
@@ -18,7 +19,7 @@ import {
18
19
  isTestnet,
19
20
  isTokenAddressSupported,
20
21
  resolveTokenAddress
21
- } from "./chunk-UXYKIMGZ.mjs";
22
+ } from "./chunk-TACK3LJN.mjs";
22
23
 
23
24
  // src/client.ts
24
25
  import { parseUnits, hashTypedData } from "viem";
@@ -27,10 +28,13 @@ var POPUP_HEIGHT = 600;
27
28
  var DEFAULT_EMBED_WIDTH = "400px";
28
29
  var DEFAULT_EMBED_HEIGHT = "500px";
29
30
  var MODAL_WIDTH = 360;
30
- var PasskeyProviderClient = class {
31
+ var DEFAULT_PROVIDER_URL = "https://passkey.1auth.box";
32
+ var OneAuthClient = class {
31
33
  constructor(config) {
32
- this.config = config;
33
- this.theme = config.theme || {};
34
+ const providerUrl = config.providerUrl || DEFAULT_PROVIDER_URL;
35
+ const dialogUrl = config.dialogUrl || providerUrl;
36
+ this.config = { ...config, providerUrl, dialogUrl };
37
+ this.theme = this.config.theme || {};
34
38
  }
35
39
  /**
36
40
  * Update the theme configuration at runtime
@@ -92,9 +96,7 @@ var PasskeyProviderClient = class {
92
96
  const response = await fetch(
93
97
  `${this.config.providerUrl}/api/intent/status/${intentId}`,
94
98
  {
95
- headers: {
96
- "x-client-id": this.config.clientId
97
- }
99
+ headers: this.config.clientId ? { "x-client-id": this.config.clientId } : {}
98
100
  }
99
101
  );
100
102
  if (response.ok) {
@@ -113,16 +115,16 @@ var PasskeyProviderClient = class {
113
115
  return void 0;
114
116
  }
115
117
  /**
116
- * Show Porto-style "Get started" auth dialog (combines sign in + sign up)
117
- * This is the recommended method for authentication - shows a modal overlay
118
- * with both sign in and create account options.
118
+ * Open the auth dialog (sign in + sign up).
119
119
  */
120
120
  async authWithModal(options) {
121
121
  const dialogUrl = this.getDialogUrl();
122
122
  const params = new URLSearchParams({
123
- clientId: this.config.clientId,
124
123
  mode: "iframe"
125
124
  });
125
+ if (this.config.clientId) {
126
+ params.set("clientId", this.config.clientId);
127
+ }
126
128
  if (options?.username) {
127
129
  params.set("username", options.username);
128
130
  }
@@ -138,6 +140,55 @@ var PasskeyProviderClient = class {
138
140
  const { dialog, iframe, cleanup } = this.createModalDialog(url);
139
141
  return this.waitForModalAuthResponse(dialog, iframe, cleanup);
140
142
  }
143
+ /**
144
+ * Open the connect dialog (lightweight connection without passkey auth).
145
+ *
146
+ * This method shows a simple connection confirmation dialog that doesn't
147
+ * require a passkey signature. Users can optionally enable "auto-connect"
148
+ * to skip this dialog in the future.
149
+ *
150
+ * If the user has never connected before, this will return action: "switch"
151
+ * to indicate that the full auth modal should be opened instead.
152
+ *
153
+ * @example
154
+ * ```typescript
155
+ * const result = await client.connectWithModal();
156
+ *
157
+ * if (result.success) {
158
+ * console.log('Connected as:', result.username);
159
+ * } else if (result.action === 'switch') {
160
+ * // User needs to sign in first
161
+ * const authResult = await client.authWithModal();
162
+ * }
163
+ * ```
164
+ */
165
+ async connectWithModal(options) {
166
+ const dialogUrl = this.getDialogUrl();
167
+ const params = new URLSearchParams({
168
+ mode: "iframe"
169
+ });
170
+ if (this.config.clientId) {
171
+ params.set("clientId", this.config.clientId);
172
+ }
173
+ const themeParams = this.getThemeParams(options?.theme);
174
+ if (themeParams) {
175
+ const themeParsed = new URLSearchParams(themeParams);
176
+ themeParsed.forEach((value, key) => params.set(key, value));
177
+ }
178
+ const url = `${dialogUrl}/dialog/connect?${params.toString()}`;
179
+ const { dialog, iframe, cleanup } = this.createModalDialog(url);
180
+ const ready = await this.waitForDialogReady(dialog, iframe, cleanup, {
181
+ mode: "iframe"
182
+ });
183
+ if (!ready) {
184
+ return {
185
+ success: false,
186
+ action: "cancel",
187
+ error: { code: "USER_CANCELLED", message: "Connection was cancelled" }
188
+ };
189
+ }
190
+ return this.waitForConnectResponse(dialog, iframe, cleanup);
191
+ }
141
192
  /**
142
193
  * Authenticate a user with an optional challenge to sign.
143
194
  *
@@ -173,9 +224,11 @@ var PasskeyProviderClient = class {
173
224
  async authenticate(options) {
174
225
  const dialogUrl = this.getDialogUrl();
175
226
  const params = new URLSearchParams({
176
- clientId: this.config.clientId,
177
227
  mode: "iframe"
178
228
  });
229
+ if (this.config.clientId) {
230
+ params.set("clientId", this.config.clientId);
231
+ }
179
232
  if (options?.challenge) {
180
233
  params.set("challenge", options.challenge);
181
234
  }
@@ -189,28 +242,30 @@ var PasskeyProviderClient = class {
189
242
  return this.waitForAuthenticateResponse(dialog, iframe, cleanup);
190
243
  }
191
244
  /**
192
- * Show signing in a modal overlay (Porto-style iframe dialog)
245
+ * Show signing in a modal overlay (iframe dialog)
193
246
  */
194
247
  async signWithModal(options) {
195
248
  const dialogUrl = this.getDialogUrl();
196
249
  const themeParams = this.getThemeParams(options?.theme);
197
250
  const signingUrl = `${dialogUrl}/dialog/sign?mode=iframe${themeParams ? `&${themeParams}` : ""}`;
198
251
  const { dialog, iframe, cleanup } = this.createModalDialog(signingUrl);
199
- const dialogOrigin = this.getDialogOrigin();
200
- await new Promise((resolve) => {
201
- iframe.onload = () => {
202
- iframe.contentWindow?.postMessage({
203
- type: "PASSKEY_INIT",
204
- mode: "iframe",
205
- challenge: options.challenge,
206
- username: options.username,
207
- description: options.description,
208
- transaction: options.transaction,
209
- metadata: options.metadata
210
- }, dialogOrigin);
211
- resolve();
212
- };
252
+ const ready = await this.waitForDialogReady(dialog, iframe, cleanup, {
253
+ mode: "iframe",
254
+ challenge: options.challenge,
255
+ username: options.username,
256
+ description: options.description,
257
+ transaction: options.transaction,
258
+ metadata: options.metadata
213
259
  });
260
+ if (!ready) {
261
+ return {
262
+ success: false,
263
+ error: {
264
+ code: "USER_REJECTED",
265
+ message: "User closed the dialog"
266
+ }
267
+ };
268
+ }
214
269
  return this.waitForSigningResponse(dialog, iframe, cleanup);
215
270
  }
216
271
  /**
@@ -243,10 +298,24 @@ var PasskeyProviderClient = class {
243
298
  * ```
244
299
  */
245
300
  async sendIntent(options) {
246
- const signedIntent = options.signedIntent;
301
+ const signedIntent = options.signedIntent ? {
302
+ ...options.signedIntent,
303
+ merchantId: options.signedIntent.merchantId || options.signedIntent.developerId
304
+ } : void 0;
247
305
  const username = signedIntent?.username || options.username;
248
306
  const targetChain = signedIntent?.targetChain || options.targetChain;
249
307
  const calls = signedIntent?.calls || options.calls;
308
+ if (signedIntent && !signedIntent.merchantId) {
309
+ return {
310
+ success: false,
311
+ intentId: "",
312
+ status: "failed",
313
+ error: {
314
+ code: "INVALID_OPTIONS",
315
+ message: "Signed intent requires developerId (clientId)"
316
+ }
317
+ };
318
+ }
250
319
  if (!username && !signedIntent?.accountAddress) {
251
320
  return {
252
321
  success: false,
@@ -269,16 +338,21 @@ var PasskeyProviderClient = class {
269
338
  }
270
339
  };
271
340
  }
341
+ const serializedTokenRequests = options.tokenRequests?.map((r) => ({
342
+ token: r.token,
343
+ amount: r.amount.toString()
344
+ }));
272
345
  let prepareResponse;
346
+ const requestBody = signedIntent || {
347
+ username: options.username,
348
+ targetChain: options.targetChain,
349
+ calls: options.calls,
350
+ tokenRequests: serializedTokenRequests,
351
+ sourceAssets: options.sourceAssets,
352
+ sourceChainId: options.sourceChainId,
353
+ ...this.config.clientId && { clientId: this.config.clientId }
354
+ };
273
355
  try {
274
- const requestBody = signedIntent || {
275
- username: options.username,
276
- targetChain: options.targetChain,
277
- calls: options.calls,
278
- tokenRequests: options.tokenRequests,
279
- sourceAssets: options.sourceAssets,
280
- clientId: this.config.clientId
281
- };
282
356
  const response = await fetch(`${this.config.providerUrl}/api/intent/prepare`, {
283
357
  method: "POST",
284
358
  headers: {
@@ -315,112 +389,190 @@ var PasskeyProviderClient = class {
315
389
  };
316
390
  }
317
391
  const dialogUrl = this.getDialogUrl();
318
- const signingUrl = `${dialogUrl}/dialog/sign?mode=iframe`;
392
+ const themeParams = this.getThemeParams();
393
+ const signingUrl = `${dialogUrl}/dialog/sign?mode=iframe${themeParams ? `&${themeParams}` : ""}`;
319
394
  const { dialog, iframe, cleanup } = this.createModalDialog(signingUrl);
320
395
  const dialogOrigin = this.getDialogOrigin();
321
- await new Promise((resolve) => {
322
- const handleReady = (event) => {
323
- if (event.origin !== dialogOrigin) return;
324
- if (event.data?.type === "PASSKEY_READY") {
325
- window.removeEventListener("message", handleReady);
326
- iframe.contentWindow?.postMessage({
327
- type: "PASSKEY_INIT",
328
- mode: "iframe",
329
- calls,
330
- chainId: targetChain,
331
- transaction: prepareResponse.transaction,
332
- challenge: prepareResponse.challenge,
333
- username,
334
- accountAddress: prepareResponse.accountAddress,
335
- intentId: prepareResponse.intentId
336
- }, dialogOrigin);
337
- resolve();
338
- }
339
- };
340
- window.addEventListener("message", handleReady);
396
+ const ready = await this.waitForDialogReady(dialog, iframe, cleanup, {
397
+ mode: "iframe",
398
+ calls,
399
+ chainId: targetChain,
400
+ transaction: prepareResponse.transaction,
401
+ challenge: prepareResponse.challenge,
402
+ username,
403
+ accountAddress: prepareResponse.accountAddress,
404
+ originMessages: prepareResponse.originMessages,
405
+ tokenRequests: serializedTokenRequests,
406
+ expiresAt: prepareResponse.expiresAt,
407
+ userId: prepareResponse.userId,
408
+ intentOp: prepareResponse.intentOp
341
409
  });
342
- const signingResult = await this.waitForSigningResponse(dialog, iframe, cleanup);
410
+ if (!ready) {
411
+ return {
412
+ success: false,
413
+ intentId: "",
414
+ status: "failed",
415
+ error: { code: "USER_CANCELLED", message: "User closed the dialog" }
416
+ };
417
+ }
418
+ const signingResult = await this.waitForSigningWithRefresh(
419
+ dialog,
420
+ iframe,
421
+ cleanup,
422
+ dialogOrigin,
423
+ // Refresh callback - called when dialog requests a quote refresh
424
+ async () => {
425
+ console.log("[SDK] Dialog requested quote refresh, re-preparing intent");
426
+ try {
427
+ const refreshResponse = await fetch(`${this.config.providerUrl}/api/intent/prepare`, {
428
+ method: "POST",
429
+ headers: { "Content-Type": "application/json" },
430
+ body: JSON.stringify(requestBody),
431
+ credentials: "include"
432
+ });
433
+ if (!refreshResponse.ok) {
434
+ console.error("[SDK] Quote refresh failed:", await refreshResponse.text());
435
+ return null;
436
+ }
437
+ const refreshedData = await refreshResponse.json();
438
+ prepareResponse = refreshedData;
439
+ return {
440
+ intentOp: refreshedData.intentOp,
441
+ expiresAt: refreshedData.expiresAt,
442
+ challenge: refreshedData.challenge,
443
+ originMessages: refreshedData.originMessages,
444
+ transaction: refreshedData.transaction
445
+ };
446
+ } catch (error) {
447
+ console.error("[SDK] Quote refresh error:", error);
448
+ return null;
449
+ }
450
+ }
451
+ );
343
452
  if (!signingResult.success) {
344
453
  return {
345
454
  success: false,
346
- intentId: prepareResponse.intentId,
455
+ intentId: "",
456
+ // No intentId yet - signing was cancelled before execute
347
457
  status: "failed",
348
458
  error: signingResult.error
349
459
  };
350
460
  }
461
+ const dialogExecutedIntent = "intentId" in signingResult && signingResult.intentId;
351
462
  let executeResponse;
352
- try {
353
- const response = await fetch(`${this.config.providerUrl}/api/intent/execute`, {
354
- method: "POST",
355
- headers: {
356
- "Content-Type": "application/json"
357
- },
358
- body: JSON.stringify({
359
- intentId: prepareResponse.intentId,
360
- signature: signingResult.signature,
361
- passkey: signingResult.passkey
362
- // Include passkey info for signature encoding
363
- })
364
- });
365
- if (!response.ok) {
366
- const errorData = await response.json().catch(() => ({}));
463
+ if (dialogExecutedIntent) {
464
+ executeResponse = {
465
+ success: true,
466
+ intentId: signingResult.intentId,
467
+ status: "pending"
468
+ };
469
+ } else {
470
+ try {
471
+ const response = await fetch(`${this.config.providerUrl}/api/intent/execute`, {
472
+ method: "POST",
473
+ headers: {
474
+ "Content-Type": "application/json"
475
+ },
476
+ body: JSON.stringify({
477
+ // Data from prepare response (no intentId yet - created on execute)
478
+ intentOp: prepareResponse.intentOp,
479
+ userId: prepareResponse.userId,
480
+ targetChain: prepareResponse.targetChain,
481
+ calls: prepareResponse.calls,
482
+ expiresAt: prepareResponse.expiresAt,
483
+ // Signature from dialog
484
+ signature: signingResult.signature,
485
+ passkey: signingResult.passkey
486
+ // Include passkey info for signature encoding
487
+ })
488
+ });
489
+ if (!response.ok) {
490
+ const errorData = await response.json().catch(() => ({}));
491
+ this.sendTransactionStatus(iframe, "failed");
492
+ await this.waitForDialogClose(dialog, cleanup);
493
+ return {
494
+ success: false,
495
+ intentId: "",
496
+ // No intentId - execute failed before creation
497
+ status: "failed",
498
+ error: {
499
+ code: "EXECUTE_FAILED",
500
+ message: errorData.error || "Failed to execute intent"
501
+ }
502
+ };
503
+ }
504
+ executeResponse = await response.json();
505
+ } catch (error) {
367
506
  this.sendTransactionStatus(iframe, "failed");
368
507
  await this.waitForDialogClose(dialog, cleanup);
369
508
  return {
370
509
  success: false,
371
- intentId: prepareResponse.intentId,
510
+ intentId: "",
511
+ // No intentId - network error before creation
372
512
  status: "failed",
373
513
  error: {
374
- code: "EXECUTE_FAILED",
375
- message: errorData.error || "Failed to execute intent"
514
+ code: "NETWORK_ERROR",
515
+ message: error instanceof Error ? error.message : "Network error"
376
516
  }
377
517
  };
378
518
  }
379
- executeResponse = await response.json();
380
- } catch (error) {
381
- this.sendTransactionStatus(iframe, "failed");
382
- await this.waitForDialogClose(dialog, cleanup);
383
- return {
384
- success: false,
385
- intentId: prepareResponse.intentId,
386
- status: "failed",
387
- error: {
388
- code: "NETWORK_ERROR",
389
- message: error instanceof Error ? error.message : "Network error"
390
- }
391
- };
392
519
  }
393
- const closeOn = options.closeOn || "preconfirmed";
394
- const acceptPreconfirmations = closeOn !== "completed";
395
520
  let finalStatus = executeResponse.status;
396
521
  let finalTxHash = executeResponse.transactionHash;
397
522
  if (finalStatus === "pending") {
398
- this.sendTransactionStatus(iframe, "processing");
399
- try {
400
- const waitResponse = await fetch(
401
- `${this.config.providerUrl}/api/intent/wait/${prepareResponse.intentId}?preconfirm=${acceptPreconfirmations}`,
402
- {
403
- headers: {
404
- "x-client-id": this.config.clientId
523
+ this.sendTransactionStatus(iframe, "pending");
524
+ const maxAttempts = 120;
525
+ const pollIntervalMs = 1500;
526
+ let lastStatus = "pending";
527
+ for (let attempt = 0; attempt < maxAttempts; attempt++) {
528
+ try {
529
+ const statusResponse = await fetch(
530
+ `${this.config.providerUrl}/api/intent/status/${executeResponse.intentId}`,
531
+ {
532
+ method: "GET",
533
+ headers: this.config.clientId ? { "x-client-id": this.config.clientId } : {}
534
+ }
535
+ );
536
+ if (statusResponse.ok) {
537
+ const statusResult = await statusResponse.json();
538
+ finalStatus = statusResult.status;
539
+ finalTxHash = statusResult.transactionHash;
540
+ if (finalStatus !== lastStatus) {
541
+ this.sendTransactionStatus(iframe, finalStatus, finalTxHash);
542
+ lastStatus = finalStatus;
543
+ }
544
+ const closeOn2 = options.closeOn || "preconfirmed";
545
+ const successStatuses2 = {
546
+ claimed: ["claimed", "preconfirmed", "filled", "completed"],
547
+ preconfirmed: ["preconfirmed", "filled", "completed"],
548
+ filled: ["filled", "completed"],
549
+ completed: ["completed"]
550
+ };
551
+ const isTerminal = finalStatus === "failed" || finalStatus === "expired";
552
+ const isSuccess = successStatuses2[closeOn2]?.includes(finalStatus) ?? false;
553
+ if (isTerminal || isSuccess) {
554
+ break;
405
555
  }
406
556
  }
407
- );
408
- if (waitResponse.ok) {
409
- const waitResult = await waitResponse.json();
410
- finalStatus = waitResult.status === "preconfirmed" || waitResult.status === "completed" ? "completed" : waitResult.status;
411
- finalTxHash = waitResult.transactionHash;
412
- } else {
413
- console.error("Wait endpoint failed:", await waitResponse.text());
557
+ } catch (pollError) {
558
+ console.error("Failed to poll intent status:", pollError);
414
559
  }
415
- } catch (waitError) {
416
- console.error("Failed to wait for intent:", waitError);
560
+ await new Promise((resolve) => setTimeout(resolve, pollIntervalMs));
417
561
  }
418
562
  }
419
- const displayStatus = finalStatus === "completed" ? "confirmed" : finalStatus;
563
+ const closeOn = options.closeOn || "preconfirmed";
564
+ const successStatuses = {
565
+ claimed: ["claimed", "preconfirmed", "filled", "completed"],
566
+ preconfirmed: ["preconfirmed", "filled", "completed"],
567
+ filled: ["filled", "completed"],
568
+ completed: ["completed"]
569
+ };
570
+ const isSuccessStatus = successStatuses[closeOn]?.includes(finalStatus) ?? false;
571
+ const displayStatus = isSuccessStatus ? "confirmed" : finalStatus;
420
572
  this.sendTransactionStatus(iframe, displayStatus, finalTxHash);
421
573
  await this.waitForDialogClose(dialog, cleanup);
422
574
  if (options.waitForHash && !finalTxHash) {
423
- const hash = await this.waitForTransactionHash(prepareResponse.intentId, {
575
+ const hash = await this.waitForTransactionHash(executeResponse.intentId, {
424
576
  timeoutMs: options.hashTimeoutMs,
425
577
  intervalMs: options.hashIntervalMs
426
578
  });
@@ -431,7 +583,7 @@ var PasskeyProviderClient = class {
431
583
  finalStatus = "failed";
432
584
  return {
433
585
  success: false,
434
- intentId: prepareResponse.intentId,
586
+ intentId: executeResponse.intentId,
435
587
  status: finalStatus,
436
588
  transactionHash: finalTxHash,
437
589
  operationId: executeResponse.operationId,
@@ -443,14 +595,204 @@ var PasskeyProviderClient = class {
443
595
  }
444
596
  }
445
597
  return {
446
- success: finalStatus === "completed",
447
- intentId: prepareResponse.intentId,
598
+ success: isSuccessStatus,
599
+ intentId: executeResponse.intentId,
448
600
  status: finalStatus,
449
601
  transactionHash: finalTxHash,
450
602
  operationId: executeResponse.operationId,
451
603
  error: executeResponse.error
452
604
  };
453
605
  }
606
+ /**
607
+ * Send a batch of intents for multi-chain execution with a single passkey tap.
608
+ *
609
+ * This method prepares multiple intents, shows a paginated review,
610
+ * and signs all intents with a single passkey tap via a shared merkle tree.
611
+ *
612
+ * @example
613
+ * ```typescript
614
+ * const result = await client.sendBatchIntent({
615
+ * username: 'alice',
616
+ * intents: [
617
+ * {
618
+ * targetChain: 8453, // Base
619
+ * calls: [{ to: '0x...', data: '0x...', label: 'Swap on Base' }],
620
+ * },
621
+ * {
622
+ * targetChain: 42161, // Arbitrum
623
+ * calls: [{ to: '0x...', data: '0x...', label: 'Mint on Arbitrum' }],
624
+ * },
625
+ * ],
626
+ * });
627
+ *
628
+ * if (result.success) {
629
+ * console.log(`${result.successCount} intents submitted`);
630
+ * }
631
+ * ```
632
+ */
633
+ async sendBatchIntent(options) {
634
+ if (!options.username) {
635
+ return {
636
+ success: false,
637
+ results: [],
638
+ successCount: 0,
639
+ failureCount: 0
640
+ };
641
+ }
642
+ if (!options.intents?.length) {
643
+ return {
644
+ success: false,
645
+ results: [],
646
+ successCount: 0,
647
+ failureCount: 0
648
+ };
649
+ }
650
+ const serializedIntents = options.intents.map((intent) => ({
651
+ targetChain: intent.targetChain,
652
+ calls: intent.calls,
653
+ tokenRequests: intent.tokenRequests?.map((r) => ({
654
+ token: r.token,
655
+ amount: r.amount.toString()
656
+ })),
657
+ sourceAssets: intent.sourceAssets,
658
+ sourceChainId: intent.sourceChainId
659
+ }));
660
+ const requestBody = {
661
+ username: options.username,
662
+ intents: serializedIntents,
663
+ ...this.config.clientId && { clientId: this.config.clientId }
664
+ };
665
+ let prepareResponse;
666
+ try {
667
+ const response = await fetch(`${this.config.providerUrl}/api/intent/batch-prepare`, {
668
+ method: "POST",
669
+ headers: { "Content-Type": "application/json" },
670
+ body: JSON.stringify(requestBody)
671
+ });
672
+ if (!response.ok) {
673
+ const errorData = await response.json().catch(() => ({}));
674
+ const errorMessage = errorData.error || "Failed to prepare batch intent";
675
+ if (errorMessage.includes("User not found")) {
676
+ localStorage.removeItem("1auth-user");
677
+ }
678
+ return {
679
+ success: false,
680
+ results: [],
681
+ successCount: 0,
682
+ failureCount: 0
683
+ };
684
+ }
685
+ prepareResponse = await response.json();
686
+ } catch {
687
+ return {
688
+ success: false,
689
+ results: [],
690
+ successCount: 0,
691
+ failureCount: 0
692
+ };
693
+ }
694
+ const dialogUrl = this.getDialogUrl();
695
+ const themeParams = this.getThemeParams();
696
+ const signingUrl = `${dialogUrl}/dialog/sign?mode=iframe${themeParams ? `&${themeParams}` : ""}`;
697
+ const { dialog, iframe, cleanup } = this.createModalDialog(signingUrl);
698
+ const dialogOrigin = this.getDialogOrigin();
699
+ const ready = await this.waitForDialogReady(dialog, iframe, cleanup, {
700
+ mode: "iframe",
701
+ batchMode: true,
702
+ batchIntents: prepareResponse.intents,
703
+ challenge: prepareResponse.challenge,
704
+ username: options.username,
705
+ accountAddress: prepareResponse.accountAddress,
706
+ userId: prepareResponse.userId,
707
+ expiresAt: prepareResponse.expiresAt
708
+ });
709
+ if (!ready) {
710
+ return {
711
+ success: false,
712
+ results: [],
713
+ successCount: 0,
714
+ failureCount: 0
715
+ };
716
+ }
717
+ const batchResult = await new Promise((resolve) => {
718
+ const handleMessage = async (event) => {
719
+ if (event.origin !== dialogOrigin) return;
720
+ const message = event.data;
721
+ if (message?.type === "PASSKEY_REFRESH_QUOTE") {
722
+ console.log("[SDK] Batch dialog requested quote refresh, re-preparing all intents");
723
+ try {
724
+ const refreshResponse = await fetch(`${this.config.providerUrl}/api/intent/batch-prepare`, {
725
+ method: "POST",
726
+ headers: { "Content-Type": "application/json" },
727
+ body: JSON.stringify(requestBody)
728
+ });
729
+ if (refreshResponse.ok) {
730
+ const refreshed = await refreshResponse.json();
731
+ prepareResponse = refreshed;
732
+ iframe.contentWindow?.postMessage({
733
+ type: "PASSKEY_REFRESH_COMPLETE",
734
+ batchIntents: refreshed.intents,
735
+ challenge: refreshed.challenge,
736
+ expiresAt: refreshed.expiresAt
737
+ }, dialogOrigin);
738
+ } else {
739
+ iframe.contentWindow?.postMessage({
740
+ type: "PASSKEY_REFRESH_ERROR",
741
+ error: "Failed to refresh batch quotes"
742
+ }, dialogOrigin);
743
+ }
744
+ } catch {
745
+ iframe.contentWindow?.postMessage({
746
+ type: "PASSKEY_REFRESH_ERROR",
747
+ error: "Failed to refresh batch quotes"
748
+ }, dialogOrigin);
749
+ }
750
+ return;
751
+ }
752
+ if (message?.type === "PASSKEY_SIGNING_RESULT") {
753
+ window.removeEventListener("message", handleMessage);
754
+ if (message.success && message.data?.batchResults) {
755
+ const rawResults = message.data.batchResults;
756
+ const results = rawResults.map((r) => ({
757
+ index: r.index,
758
+ success: r.success ?? r.status !== "FAILED",
759
+ intentId: r.intentId || r.operationId || "",
760
+ status: r.status === "FAILED" ? "failed" : "pending",
761
+ error: r.error ? { code: "EXECUTE_FAILED", message: r.error } : void 0
762
+ }));
763
+ const successCount = results.filter((r) => r.success).length;
764
+ await this.waitForDialogClose(dialog, cleanup);
765
+ resolve({
766
+ success: successCount === results.length,
767
+ results,
768
+ successCount,
769
+ failureCount: results.length - successCount
770
+ });
771
+ } else {
772
+ cleanup();
773
+ resolve({
774
+ success: false,
775
+ results: [],
776
+ successCount: 0,
777
+ failureCount: 0
778
+ });
779
+ }
780
+ }
781
+ if (message?.type === "PASSKEY_CLOSE") {
782
+ window.removeEventListener("message", handleMessage);
783
+ cleanup();
784
+ resolve({
785
+ success: false,
786
+ results: [],
787
+ successCount: 0,
788
+ failureCount: 0
789
+ });
790
+ }
791
+ };
792
+ window.addEventListener("message", handleMessage);
793
+ });
794
+ return batchResult;
795
+ }
454
796
  /**
455
797
  * Send transaction status to the dialog iframe
456
798
  */
@@ -527,7 +869,77 @@ var PasskeyProviderClient = class {
527
869
  const payload = message?.data;
528
870
  if (message?.type === "PASSKEY_SIGNING_RESULT") {
529
871
  window.removeEventListener("message", handleMessage);
530
- if (message.success && payload?.signature) {
872
+ if (message.success && payload?.intentId) {
873
+ resolve({
874
+ success: true,
875
+ intentId: payload.intentId
876
+ });
877
+ } else if (message.success && payload?.signature) {
878
+ resolve({
879
+ success: true,
880
+ signature: payload.signature,
881
+ passkey: payload.passkey,
882
+ signedHash: payload.signedHash
883
+ });
884
+ } else {
885
+ resolve({
886
+ success: false,
887
+ error: message.error || {
888
+ code: "SIGNING_FAILED",
889
+ message: "Signing failed"
890
+ }
891
+ });
892
+ }
893
+ } else if (message?.type === "PASSKEY_CLOSE") {
894
+ window.removeEventListener("message", handleMessage);
895
+ cleanup();
896
+ resolve({
897
+ success: false,
898
+ error: {
899
+ code: "USER_REJECTED",
900
+ message: "User closed the dialog"
901
+ }
902
+ });
903
+ }
904
+ };
905
+ window.addEventListener("message", handleMessage);
906
+ });
907
+ }
908
+ /**
909
+ * Wait for signing result with auto-refresh support
910
+ * This method handles both signing results and quote refresh requests from the dialog
911
+ */
912
+ waitForSigningWithRefresh(dialog, iframe, cleanup, dialogOrigin, onRefresh) {
913
+ console.log("[SDK] waitForSigningWithRefresh, expecting origin:", dialogOrigin);
914
+ return new Promise((resolve) => {
915
+ const handleMessage = async (event) => {
916
+ if (event.origin !== dialogOrigin) return;
917
+ const message = event.data;
918
+ if (message?.type === "PASSKEY_REFRESH_QUOTE") {
919
+ console.log("[SDK] Received quote refresh request from dialog");
920
+ const refreshedData = await onRefresh();
921
+ if (refreshedData) {
922
+ iframe.contentWindow?.postMessage({
923
+ type: "PASSKEY_REFRESH_COMPLETE",
924
+ ...refreshedData
925
+ }, dialogOrigin);
926
+ } else {
927
+ iframe.contentWindow?.postMessage({
928
+ type: "PASSKEY_REFRESH_ERROR",
929
+ error: "Failed to refresh quote"
930
+ }, dialogOrigin);
931
+ }
932
+ return;
933
+ }
934
+ const payload = message?.data;
935
+ if (message?.type === "PASSKEY_SIGNING_RESULT") {
936
+ window.removeEventListener("message", handleMessage);
937
+ if (message.success && payload?.intentId) {
938
+ resolve({
939
+ success: true,
940
+ intentId: payload.intentId
941
+ });
942
+ } else if (message.success && payload?.signature) {
531
943
  resolve({
532
944
  success: true,
533
945
  signature: payload.signature,
@@ -593,9 +1005,7 @@ var PasskeyProviderClient = class {
593
1005
  const response = await fetch(
594
1006
  `${this.config.providerUrl}/api/intent/status/${intentId}`,
595
1007
  {
596
- headers: {
597
- "x-client-id": this.config.clientId
598
- }
1008
+ headers: this.config.clientId ? { "x-client-id": this.config.clientId } : {}
599
1009
  }
600
1010
  );
601
1011
  if (!response.ok) {
@@ -630,6 +1040,43 @@ var PasskeyProviderClient = class {
630
1040
  };
631
1041
  }
632
1042
  }
1043
+ /**
1044
+ * Get the history of intents for the authenticated user.
1045
+ *
1046
+ * Requires an active session (user must be logged in).
1047
+ *
1048
+ * @example
1049
+ * ```typescript
1050
+ * // Get recent intents
1051
+ * const history = await client.getIntentHistory({ limit: 10 });
1052
+ *
1053
+ * // Filter by status
1054
+ * const pending = await client.getIntentHistory({ status: 'pending' });
1055
+ *
1056
+ * // Filter by date range
1057
+ * const lastWeek = await client.getIntentHistory({
1058
+ * from: new Date(Date.now() - 7 * 24 * 60 * 60 * 1000).toISOString(),
1059
+ * });
1060
+ * ```
1061
+ */
1062
+ async getIntentHistory(options) {
1063
+ const queryParams = new URLSearchParams();
1064
+ if (options?.limit) queryParams.set("limit", String(options.limit));
1065
+ if (options?.offset) queryParams.set("offset", String(options.offset));
1066
+ if (options?.status) queryParams.set("status", options.status);
1067
+ if (options?.from) queryParams.set("from", options.from);
1068
+ if (options?.to) queryParams.set("to", options.to);
1069
+ const url = `${this.config.providerUrl}/api/intent/history${queryParams.toString() ? `?${queryParams}` : ""}`;
1070
+ const response = await fetch(url, {
1071
+ headers: this.config.clientId ? { "x-client-id": this.config.clientId } : {},
1072
+ credentials: "include"
1073
+ });
1074
+ if (!response.ok) {
1075
+ const errorData = await response.json().catch(() => ({}));
1076
+ throw new Error(errorData.error || "Failed to get intent history");
1077
+ }
1078
+ return response.json();
1079
+ }
633
1080
  /**
634
1081
  * Send a swap intent through the Rhinestone orchestrator
635
1082
  *
@@ -692,17 +1139,21 @@ var PasskeyProviderClient = class {
692
1139
  };
693
1140
  }
694
1141
  };
695
- const fromTokenResult = resolveToken(options.fromToken, "fromToken");
696
- if (!fromTokenResult.address) {
697
- return {
698
- success: false,
699
- intentId: "",
700
- status: "failed",
701
- error: {
702
- code: "INVALID_TOKEN",
703
- message: fromTokenResult.error || `Unknown fromToken: ${options.fromToken}`
704
- }
705
- };
1142
+ let fromTokenAddress;
1143
+ if (options.fromToken) {
1144
+ const fromTokenResult = resolveToken(options.fromToken, "fromToken");
1145
+ if (!fromTokenResult.address) {
1146
+ return {
1147
+ success: false,
1148
+ intentId: "",
1149
+ status: "failed",
1150
+ error: {
1151
+ code: "INVALID_TOKEN",
1152
+ message: fromTokenResult.error || `Unknown fromToken: ${options.fromToken}`
1153
+ }
1154
+ };
1155
+ }
1156
+ fromTokenAddress = fromTokenResult.address;
706
1157
  }
707
1158
  const toTokenResult = resolveToken(options.toToken, "toToken");
708
1159
  if (!toTokenResult.address) {
@@ -716,18 +1167,17 @@ var PasskeyProviderClient = class {
716
1167
  }
717
1168
  };
718
1169
  }
719
- const fromTokenAddress = fromTokenResult.address;
720
1170
  const toTokenAddress = toTokenResult.address;
721
1171
  console.log("[SDK sendSwap] Token resolution:", {
722
- fromToken: options.fromToken,
723
- fromTokenAddress,
1172
+ fromToken: options.fromToken ?? "Any",
1173
+ fromTokenAddress: fromTokenAddress ?? "orchestrator picks",
724
1174
  toToken: options.toToken,
725
1175
  toTokenAddress,
726
1176
  targetChain: options.targetChain
727
1177
  });
728
1178
  const formatTokenLabel = (token, fallback) => {
729
1179
  if (!token.startsWith("0x")) {
730
- return token.toUpperCase();
1180
+ return token;
731
1181
  }
732
1182
  try {
733
1183
  return getTokenSymbol(token, options.targetChain);
@@ -735,15 +1185,11 @@ var PasskeyProviderClient = class {
735
1185
  return fallback;
736
1186
  }
737
1187
  };
738
- const fromSymbol = formatTokenLabel(
739
- options.fromToken,
740
- `${options.fromToken.slice(0, 6)}...${options.fromToken.slice(-4)}`
741
- );
742
1188
  const toSymbol = formatTokenLabel(
743
1189
  options.toToken,
744
1190
  `${options.toToken.slice(0, 6)}...${options.toToken.slice(-4)}`
745
1191
  );
746
- const isFromNativeEth = fromTokenAddress === "0x0000000000000000000000000000000000000000";
1192
+ const isFromNativeEth = fromTokenAddress ? fromTokenAddress === "0x0000000000000000000000000000000000000000" : false;
747
1193
  const isToNativeEth = toTokenAddress === "0x0000000000000000000000000000000000000000";
748
1194
  const KNOWN_DECIMALS = {
749
1195
  ETH: 18,
@@ -753,31 +1199,33 @@ var PasskeyProviderClient = class {
753
1199
  USDT0: 6
754
1200
  };
755
1201
  const getDecimals = (symbol, chainId) => {
756
- const upperSymbol = symbol.toUpperCase();
757
1202
  try {
758
- const decimals = getTokenDecimals(upperSymbol, chainId);
759
- console.log(`[SDK] getTokenDecimals(${upperSymbol}, ${chainId}) = ${decimals}`);
1203
+ const match = getSupportedTokens(chainId).find(
1204
+ (t) => t.symbol.toUpperCase() === symbol.toUpperCase()
1205
+ );
1206
+ if (match) {
1207
+ console.log(`[SDK] getTokenDecimals(${match.symbol}, ${chainId}) = ${match.decimals}`);
1208
+ return match.decimals;
1209
+ }
1210
+ const decimals = getTokenDecimals(symbol, chainId);
1211
+ console.log(`[SDK] getTokenDecimals(${symbol}, ${chainId}) = ${decimals}`);
760
1212
  return decimals;
761
1213
  } catch (e) {
762
- console.warn(`[SDK] getTokenDecimals failed for ${upperSymbol}, using fallback`, e);
1214
+ const upperSymbol = symbol.toUpperCase();
1215
+ console.warn(`[SDK] getTokenDecimals failed for ${symbol}, using fallback`, e);
763
1216
  return KNOWN_DECIMALS[upperSymbol] ?? 18;
764
1217
  }
765
1218
  };
766
- const fromDecimals = getDecimals(options.fromToken, options.targetChain);
767
1219
  const toDecimals = getDecimals(options.toToken, options.targetChain);
768
- const isBridge = options.fromToken.toUpperCase() === options.toToken.toUpperCase();
769
- let tokenRequests;
770
- if (!isToNativeEth) {
771
- tokenRequests = [{
772
- token: toTokenAddress,
773
- amount: parseUnits(options.amount, toDecimals).toString()
774
- }];
775
- }
1220
+ const isBridge = options.fromToken ? options.fromToken.toUpperCase() === options.toToken.toUpperCase() : false;
1221
+ const tokenRequests = [{
1222
+ token: toTokenAddress,
1223
+ amount: parseUnits(options.amount, toDecimals)
1224
+ }];
776
1225
  console.log("[SDK sendSwap] Building intent:", {
777
1226
  isBridge,
778
1227
  isFromNativeEth,
779
1228
  isToNativeEth,
780
- fromDecimals,
781
1229
  toDecimals,
782
1230
  tokenRequests
783
1231
  });
@@ -788,15 +1236,20 @@ var PasskeyProviderClient = class {
788
1236
  {
789
1237
  // Minimal call - just signals to orchestrator we want the tokenRequests delivered
790
1238
  to: toTokenAddress,
791
- value: "0"
1239
+ value: "0",
1240
+ // SDK provides labels so dialog shows "Buy ETH" not "Send ETH / To: 0x000..."
1241
+ label: `Buy ${toSymbol}`,
1242
+ sublabel: `${options.amount} ${toSymbol}`
792
1243
  }
793
1244
  ],
794
1245
  // Request specific output tokens - this is what actually matters for swaps
795
1246
  tokenRequests,
796
1247
  // Constrain orchestrator to use only the fromToken as input
797
1248
  // This ensures the swap uses the correct source token
798
- // Pass the symbol (not address) so orchestrator can resolve per-chain
799
- sourceAssets: options.sourceAssets || [options.fromToken.toUpperCase()],
1249
+ // Use canonical symbol casing from registry (e.g. "MockUSD" not "MOCKUSD")
1250
+ sourceAssets: options.sourceAssets || (options.fromToken ? [options.fromToken] : void 0),
1251
+ // Pass source chain ID so orchestrator knows which chain to look for tokens on
1252
+ sourceChainId: options.sourceChainId,
800
1253
  closeOn: options.closeOn || "preconfirmed",
801
1254
  waitForHash: options.waitForHash,
802
1255
  hashTimeoutMs: options.hashTimeoutMs,
@@ -805,7 +1258,7 @@ var PasskeyProviderClient = class {
805
1258
  return {
806
1259
  ...result,
807
1260
  quote: result.success ? {
808
- fromToken: fromTokenAddress,
1261
+ fromToken: fromTokenAddress ?? options.fromToken,
809
1262
  toToken: toTokenAddress,
810
1263
  amountIn: options.amount,
811
1264
  amountOut: "",
@@ -847,26 +1300,23 @@ var PasskeyProviderClient = class {
847
1300
  const themeParams = this.getThemeParams(options?.theme);
848
1301
  const signingUrl = `${dialogUrl}/dialog/sign?mode=iframe${themeParams ? `&${themeParams}` : ""}`;
849
1302
  const { dialog, iframe, cleanup } = this.createModalDialog(signingUrl);
850
- const dialogOrigin = this.getDialogOrigin();
851
- await new Promise((resolve) => {
852
- const handleReady = (event) => {
853
- if (event.origin !== dialogOrigin) return;
854
- if (event.data?.type === "PASSKEY_READY") {
855
- window.removeEventListener("message", handleReady);
856
- iframe.contentWindow?.postMessage({
857
- type: "PASSKEY_INIT",
858
- mode: "iframe",
859
- message: options.message,
860
- challenge: options.challenge || options.message,
861
- username: options.username,
862
- description: options.description,
863
- metadata: options.metadata
864
- }, dialogOrigin);
865
- resolve();
1303
+ const ready = await this.waitForDialogReady(dialog, iframe, cleanup, {
1304
+ mode: "iframe",
1305
+ message: options.message,
1306
+ challenge: options.challenge || options.message,
1307
+ username: options.username,
1308
+ description: options.description,
1309
+ metadata: options.metadata
1310
+ });
1311
+ if (!ready) {
1312
+ return {
1313
+ success: false,
1314
+ error: {
1315
+ code: "USER_REJECTED",
1316
+ message: "User closed the dialog"
866
1317
  }
867
1318
  };
868
- window.addEventListener("message", handleReady);
869
- });
1319
+ }
870
1320
  const signingResult = await this.waitForSigningResponse(dialog, iframe, cleanup);
871
1321
  cleanup();
872
1322
  if (signingResult.success) {
@@ -935,31 +1385,28 @@ var PasskeyProviderClient = class {
935
1385
  const themeParams = this.getThemeParams(options?.theme);
936
1386
  const signingUrl = `${dialogUrl}/dialog/sign?mode=iframe${themeParams ? `&${themeParams}` : ""}`;
937
1387
  const { dialog, iframe, cleanup } = this.createModalDialog(signingUrl);
938
- const dialogOrigin = this.getDialogOrigin();
939
- await new Promise((resolve) => {
940
- const handleReady = (event) => {
941
- if (event.origin !== dialogOrigin) return;
942
- if (event.data?.type === "PASSKEY_READY") {
943
- window.removeEventListener("message", handleReady);
944
- iframe.contentWindow?.postMessage({
945
- type: "PASSKEY_INIT",
946
- mode: "iframe",
947
- signingMode: "typedData",
948
- typedData: {
949
- domain: options.domain,
950
- types: options.types,
951
- primaryType: options.primaryType,
952
- message: options.message
953
- },
954
- challenge: signedHash,
955
- username: options.username,
956
- description: options.description
957
- }, dialogOrigin);
958
- resolve();
1388
+ const ready = await this.waitForDialogReady(dialog, iframe, cleanup, {
1389
+ mode: "iframe",
1390
+ signingMode: "typedData",
1391
+ typedData: {
1392
+ domain: options.domain,
1393
+ types: options.types,
1394
+ primaryType: options.primaryType,
1395
+ message: options.message
1396
+ },
1397
+ challenge: signedHash,
1398
+ username: options.username,
1399
+ description: options.description
1400
+ });
1401
+ if (!ready) {
1402
+ return {
1403
+ success: false,
1404
+ error: {
1405
+ code: "USER_REJECTED",
1406
+ message: "User closed the dialog"
959
1407
  }
960
1408
  };
961
- window.addEventListener("message", handleReady);
962
- });
1409
+ }
963
1410
  const signingResult = await this.waitForSigningResponse(dialog, iframe, cleanup);
964
1411
  cleanup();
965
1412
  if (signingResult.success) {
@@ -1113,9 +1560,7 @@ var PasskeyProviderClient = class {
1113
1560
  const response = await fetch(
1114
1561
  `${this.config.providerUrl}/api/users/${encodeURIComponent(username)}/passkeys`,
1115
1562
  {
1116
- headers: {
1117
- "x-client-id": this.config.clientId
1118
- }
1563
+ headers: this.config.clientId ? { "x-client-id": this.config.clientId } : {}
1119
1564
  }
1120
1565
  );
1121
1566
  if (!response.ok) {
@@ -1134,7 +1579,7 @@ var PasskeyProviderClient = class {
1134
1579
  "Content-Type": "application/json"
1135
1580
  },
1136
1581
  body: JSON.stringify({
1137
- clientId: this.config.clientId,
1582
+ ...this.config.clientId && { clientId: this.config.clientId },
1138
1583
  username: options.username,
1139
1584
  challenge: options.challenge,
1140
1585
  description: options.description,
@@ -1161,7 +1606,45 @@ var PasskeyProviderClient = class {
1161
1606
  );
1162
1607
  }
1163
1608
  /**
1164
- * Create a modal dialog with an iframe inside (Porto-style overlay)
1609
+ * Wait for the dialog iframe to signal ready, then send init data.
1610
+ * Also handles early close (X button, escape, backdrop) during the ready phase.
1611
+ * Returns true if dialog is ready, false if it was closed before becoming ready.
1612
+ */
1613
+ waitForDialogReady(dialog, iframe, cleanup, initMessage) {
1614
+ const dialogOrigin = this.getDialogOrigin();
1615
+ return new Promise((resolve) => {
1616
+ let settled = false;
1617
+ const teardown = () => {
1618
+ if (settled) return;
1619
+ settled = true;
1620
+ window.removeEventListener("message", handleMessage);
1621
+ dialog.removeEventListener("close", handleClose);
1622
+ };
1623
+ const handleMessage = (event) => {
1624
+ if (event.origin !== dialogOrigin) return;
1625
+ if (event.data?.type === "PASSKEY_READY") {
1626
+ teardown();
1627
+ iframe.contentWindow?.postMessage({
1628
+ type: "PASSKEY_INIT",
1629
+ ...initMessage
1630
+ }, dialogOrigin);
1631
+ resolve(true);
1632
+ } else if (event.data?.type === "PASSKEY_CLOSE") {
1633
+ teardown();
1634
+ cleanup();
1635
+ resolve(false);
1636
+ }
1637
+ };
1638
+ const handleClose = () => {
1639
+ teardown();
1640
+ resolve(false);
1641
+ };
1642
+ window.addEventListener("message", handleMessage);
1643
+ dialog.addEventListener("close", handleClose);
1644
+ });
1645
+ }
1646
+ /**
1647
+ * Create a modal dialog with an iframe inside.
1165
1648
  */
1166
1649
  createModalDialog(url) {
1167
1650
  const dialogUrl = this.getDialogUrl();
@@ -1304,12 +1787,24 @@ var PasskeyProviderClient = class {
1304
1787
  };
1305
1788
  return { dialog, iframe, cleanup };
1306
1789
  }
1307
- waitForModalAuthResponse(_dialog, _iframe, cleanup) {
1790
+ waitForModalAuthResponse(_dialog, iframe, cleanup) {
1308
1791
  const dialogOrigin = this.getDialogOrigin();
1309
1792
  return new Promise((resolve) => {
1793
+ let dialogReady = false;
1310
1794
  const handleMessage = (event) => {
1311
1795
  if (event.origin !== dialogOrigin) return;
1312
1796
  const data = event.data;
1797
+ if (data?.type === "PASSKEY_READY") {
1798
+ dialogReady = true;
1799
+ iframe.contentWindow?.postMessage({
1800
+ type: "PASSKEY_INIT",
1801
+ mode: "iframe"
1802
+ }, dialogOrigin);
1803
+ return;
1804
+ }
1805
+ if (!dialogReady && data?.type === "PASSKEY_CLOSE") {
1806
+ return;
1807
+ }
1313
1808
  if (data?.type === "PASSKEY_LOGIN_RESULT") {
1314
1809
  window.removeEventListener("message", handleMessage);
1315
1810
  cleanup();
@@ -1339,6 +1834,11 @@ var PasskeyProviderClient = class {
1339
1834
  error: data.error
1340
1835
  });
1341
1836
  }
1837
+ } else if (data?.type === "PASSKEY_RETRY_POPUP") {
1838
+ window.removeEventListener("message", handleMessage);
1839
+ cleanup();
1840
+ const popupUrl = data.data?.url?.replace("mode=iframe", "mode=popup") || `${this.getDialogUrl()}/dialog/auth?mode=popup${this.config.clientId ? `&clientId=${this.config.clientId}` : ""}`;
1841
+ this.waitForPopupAuthResponse(popupUrl).then(resolve);
1342
1842
  } else if (data?.type === "PASSKEY_CLOSE") {
1343
1843
  window.removeEventListener("message", handleMessage);
1344
1844
  cleanup();
@@ -1354,6 +1854,77 @@ var PasskeyProviderClient = class {
1354
1854
  window.addEventListener("message", handleMessage);
1355
1855
  });
1356
1856
  }
1857
+ /**
1858
+ * Open a popup for auth and wait for the result.
1859
+ * Used when iframe mode fails (e.g., due to password manager interference).
1860
+ */
1861
+ waitForPopupAuthResponse(url) {
1862
+ const dialogOrigin = this.getDialogOrigin();
1863
+ const popup = this.openPopup(url);
1864
+ return new Promise((resolve) => {
1865
+ const pollTimer = setInterval(() => {
1866
+ if (popup?.closed) {
1867
+ clearInterval(pollTimer);
1868
+ window.removeEventListener("message", handleMessage);
1869
+ resolve({
1870
+ success: false,
1871
+ error: {
1872
+ code: "USER_CANCELLED",
1873
+ message: "Authentication was cancelled"
1874
+ }
1875
+ });
1876
+ }
1877
+ }, 500);
1878
+ const handleMessage = (event) => {
1879
+ if (event.origin !== dialogOrigin) return;
1880
+ const data = event.data;
1881
+ if (data?.type === "PASSKEY_LOGIN_RESULT") {
1882
+ clearInterval(pollTimer);
1883
+ window.removeEventListener("message", handleMessage);
1884
+ popup?.close();
1885
+ if (data.success) {
1886
+ resolve({
1887
+ success: true,
1888
+ username: data.data?.username,
1889
+ user: data.data?.user
1890
+ });
1891
+ } else {
1892
+ resolve({
1893
+ success: false,
1894
+ error: data.error
1895
+ });
1896
+ }
1897
+ } else if (data?.type === "PASSKEY_REGISTER_RESULT") {
1898
+ clearInterval(pollTimer);
1899
+ window.removeEventListener("message", handleMessage);
1900
+ popup?.close();
1901
+ if (data.success) {
1902
+ resolve({
1903
+ success: true,
1904
+ username: data.data?.username
1905
+ });
1906
+ } else {
1907
+ resolve({
1908
+ success: false,
1909
+ error: data.error
1910
+ });
1911
+ }
1912
+ } else if (data?.type === "PASSKEY_CLOSE") {
1913
+ clearInterval(pollTimer);
1914
+ window.removeEventListener("message", handleMessage);
1915
+ popup?.close();
1916
+ resolve({
1917
+ success: false,
1918
+ error: {
1919
+ code: "USER_CANCELLED",
1920
+ message: "Authentication was cancelled"
1921
+ }
1922
+ });
1923
+ }
1924
+ };
1925
+ window.addEventListener("message", handleMessage);
1926
+ });
1927
+ }
1357
1928
  waitForAuthenticateResponse(_dialog, _iframe, cleanup) {
1358
1929
  const dialogOrigin = this.getDialogOrigin();
1359
1930
  return new Promise((resolve) => {
@@ -1393,6 +1964,44 @@ var PasskeyProviderClient = class {
1393
1964
  window.addEventListener("message", handleMessage);
1394
1965
  });
1395
1966
  }
1967
+ waitForConnectResponse(_dialog, _iframe, cleanup) {
1968
+ const dialogOrigin = this.getDialogOrigin();
1969
+ return new Promise((resolve) => {
1970
+ const handleMessage = (event) => {
1971
+ if (event.origin !== dialogOrigin) return;
1972
+ const data = event.data;
1973
+ if (data?.type === "PASSKEY_CONNECT_RESULT") {
1974
+ window.removeEventListener("message", handleMessage);
1975
+ cleanup();
1976
+ if (data.success) {
1977
+ resolve({
1978
+ success: true,
1979
+ username: data.data?.username,
1980
+ autoConnected: data.data?.autoConnected
1981
+ });
1982
+ } else {
1983
+ resolve({
1984
+ success: false,
1985
+ action: data.data?.action,
1986
+ error: data.error
1987
+ });
1988
+ }
1989
+ } else if (data?.type === "PASSKEY_CLOSE") {
1990
+ window.removeEventListener("message", handleMessage);
1991
+ cleanup();
1992
+ resolve({
1993
+ success: false,
1994
+ action: "cancel",
1995
+ error: {
1996
+ code: "USER_CANCELLED",
1997
+ message: "Connection was cancelled"
1998
+ }
1999
+ });
2000
+ }
2001
+ };
2002
+ window.addEventListener("message", handleMessage);
2003
+ });
2004
+ }
1396
2005
  waitForModalSigningResponse(requestId, _dialog, _iframe, cleanup) {
1397
2006
  const dialogOrigin = this.getDialogOrigin();
1398
2007
  return new Promise((resolve) => {
@@ -1481,9 +2090,7 @@ var PasskeyProviderClient = class {
1481
2090
  const response = await fetch(
1482
2091
  `${this.config.providerUrl}/api/sign/request/${requestId}`,
1483
2092
  {
1484
- headers: {
1485
- "x-client-id": this.config.clientId
1486
- }
2093
+ headers: this.config.clientId ? { "x-client-id": this.config.clientId } : {}
1487
2094
  }
1488
2095
  );
1489
2096
  if (!response.ok) {
@@ -1601,7 +2208,7 @@ import {
1601
2208
  } from "viem";
1602
2209
  import { toAccount as toAccount2 } from "viem/accounts";
1603
2210
  function createPasskeyWalletClient(config) {
1604
- const provider = new PasskeyProviderClient({
2211
+ const provider = new OneAuthClient({
1605
2212
  providerUrl: config.providerUrl,
1606
2213
  clientId: config.clientId,
1607
2214
  dialogUrl: config.dialogUrl
@@ -1625,6 +2232,9 @@ function createPasskeyWalletClient(config) {
1625
2232
  if (!result.success) {
1626
2233
  throw new Error(result.error?.message || "Signing failed");
1627
2234
  }
2235
+ if (!result.signature) {
2236
+ throw new Error("No signature received");
2237
+ }
1628
2238
  return encodeWebAuthnSignature(result.signature);
1629
2239
  };
1630
2240
  const signTransactionImpl = async (transaction) => {
@@ -1645,6 +2255,9 @@ function createPasskeyWalletClient(config) {
1645
2255
  if (!result.success) {
1646
2256
  throw new Error(result.error?.message || "Signing failed");
1647
2257
  }
2258
+ if (!result.signature) {
2259
+ throw new Error("No signature received");
2260
+ }
1648
2261
  return encodeWebAuthnSignature(result.signature);
1649
2262
  };
1650
2263
  const signTypedDataImpl = async (typedData) => {
@@ -1666,8 +2279,35 @@ function createPasskeyWalletClient(config) {
1666
2279
  if (!result.success) {
1667
2280
  throw new Error(result.error?.message || "Signing failed");
1668
2281
  }
2282
+ if (!result.signature) {
2283
+ throw new Error("No signature received");
2284
+ }
1669
2285
  return encodeWebAuthnSignature(result.signature);
1670
2286
  };
2287
+ const buildIntentPayload = async (calls, targetChainOverride) => {
2288
+ const targetChain = targetChainOverride ?? config.chain.id;
2289
+ const intentCalls = calls.map((call) => ({
2290
+ to: call.to,
2291
+ data: call.data || "0x",
2292
+ value: call.value !== void 0 ? call.value.toString() : "0",
2293
+ label: call.label,
2294
+ sublabel: call.sublabel
2295
+ }));
2296
+ if (config.signIntent) {
2297
+ const signedIntent = await config.signIntent({
2298
+ username: config.username,
2299
+ accountAddress: config.accountAddress,
2300
+ targetChain,
2301
+ calls: intentCalls
2302
+ });
2303
+ return { signedIntent };
2304
+ }
2305
+ return {
2306
+ username: config.username,
2307
+ targetChain,
2308
+ calls: intentCalls
2309
+ };
2310
+ };
1671
2311
  const account = toAccount2({
1672
2312
  address: config.accountAddress,
1673
2313
  signMessage: ({ message }) => signMessageImpl(message),
@@ -1684,24 +2324,18 @@ function createPasskeyWalletClient(config) {
1684
2324
  * Send a single transaction via intent flow
1685
2325
  */
1686
2326
  async sendTransaction(transaction) {
2327
+ const targetChain = typeof transaction.chainId === "number" ? transaction.chainId : transaction.chain?.id;
1687
2328
  const calls = [
1688
2329
  {
1689
2330
  to: transaction.to,
1690
- data: transaction.data,
2331
+ data: transaction.data || "0x",
1691
2332
  value: transaction.value
1692
2333
  }
1693
2334
  ];
1694
2335
  const closeOn = config.waitForHash ?? true ? "completed" : "preconfirmed";
2336
+ const intentPayload = await buildIntentPayload(calls, targetChain);
1695
2337
  const result = await provider.sendIntent({
1696
- username: config.username,
1697
- targetChain: config.chain.id,
1698
- calls: calls.map((call) => ({
1699
- to: call.to,
1700
- data: call.data,
1701
- value: call.value ? call.value.toString() : "0",
1702
- label: call.label,
1703
- sublabel: call.sublabel
1704
- })),
2338
+ ...intentPayload,
1705
2339
  closeOn,
1706
2340
  waitForHash: config.waitForHash ?? true,
1707
2341
  hashTimeoutMs: config.hashTimeoutMs,
@@ -1716,18 +2350,12 @@ function createPasskeyWalletClient(config) {
1716
2350
  * Send multiple calls as a single batched transaction
1717
2351
  */
1718
2352
  async sendCalls(params) {
1719
- const { calls } = params;
2353
+ const { calls, chainId: targetChain, tokenRequests } = params;
1720
2354
  const closeOn = config.waitForHash ?? true ? "completed" : "preconfirmed";
2355
+ const intentPayload = await buildIntentPayload(calls, targetChain);
1721
2356
  const result = await provider.sendIntent({
1722
- username: config.username,
1723
- targetChain: config.chain.id,
1724
- calls: calls.map((call) => ({
1725
- to: call.to,
1726
- data: call.data,
1727
- value: call.value ? call.value.toString() : "0",
1728
- label: call.label,
1729
- sublabel: call.sublabel
1730
- })),
2357
+ ...intentPayload,
2358
+ tokenRequests,
1731
2359
  closeOn,
1732
2360
  waitForHash: config.waitForHash ?? true,
1733
2361
  hashTimeoutMs: config.hashTimeoutMs,
@@ -2171,9 +2799,10 @@ function BatchQueueWidget({ onSignAll }) {
2171
2799
 
2172
2800
  // src/verify.ts
2173
2801
  import { keccak256, toBytes } from "viem";
2174
- var PASSKEY_MESSAGE_PREFIX = "Passkey Signed Message:\n";
2802
+ var ETHEREUM_MESSAGE_PREFIX = "Ethereum Signed Message:\n";
2803
+ var PASSKEY_MESSAGE_PREFIX = ETHEREUM_MESSAGE_PREFIX;
2175
2804
  function hashMessage2(message) {
2176
- const prefixed = PASSKEY_MESSAGE_PREFIX + message.length.toString() + message;
2805
+ const prefixed = ETHEREUM_MESSAGE_PREFIX + message.length.toString() + message;
2177
2806
  return keccak256(toBytes(prefixed));
2178
2807
  }
2179
2808
  function verifyMessageHash(message, signedHash) {
@@ -2184,8 +2813,11 @@ function verifyMessageHash(message, signedHash) {
2184
2813
  export {
2185
2814
  BatchQueueProvider,
2186
2815
  BatchQueueWidget,
2816
+ ETHEREUM_MESSAGE_PREFIX,
2817
+ OneAuthClient,
2187
2818
  PASSKEY_MESSAGE_PREFIX,
2188
- PasskeyProviderClient,
2819
+ OneAuthClient as PasskeyProviderClient,
2820
+ createOneAuthProvider,
2189
2821
  createPasskeyAccount,
2190
2822
  createPasskeyProvider,
2191
2823
  createPasskeyWalletClient,