@bounded-sh/client 0.0.103 → 0.0.105

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 (56) hide show
  1. package/README.md +15 -3
  2. package/dist/auth/index.d.ts +1 -1
  3. package/dist/auth/oidc-auth.d.ts +17 -13
  4. package/dist/auth/providers/offchain-auth-provider.d.ts +7 -9
  5. package/dist/{email-auth-provider-Cr3I4Po5.js → email-auth-provider-CB5ZBjJL.js} +2 -2
  6. package/dist/{email-auth-provider-Cr3I4Po5.js.map → email-auth-provider-CB5ZBjJL.js.map} +1 -1
  7. package/dist/{email-auth-provider-DCZ93FYg.js → email-auth-provider-Czo-xM6n.js} +2 -2
  8. package/dist/{email-auth-provider-DCZ93FYg.js.map → email-auth-provider-Czo-xM6n.js.map} +1 -1
  9. package/dist/{email-auth-provider-Ct3YA68i.mjs → email-auth-provider-D9SQu9c1.mjs} +2 -2
  10. package/dist/{email-auth-provider-Ct3YA68i.mjs.map → email-auth-provider-D9SQu9c1.mjs.map} +1 -1
  11. package/dist/{email-auth-provider-BJXeBUwp.mjs → email-auth-provider-sA_9RTRl.mjs} +2 -2
  12. package/dist/{email-auth-provider-BJXeBUwp.mjs.map → email-auth-provider-sA_9RTRl.mjs.map} +1 -1
  13. package/dist/global.d.ts +2 -0
  14. package/dist/{guest-auth-provider-CUo6sgop.mjs → guest-auth-provider-1mrlNF2G.mjs} +2 -2
  15. package/dist/{guest-auth-provider-CUo6sgop.mjs.map → guest-auth-provider-1mrlNF2G.mjs.map} +1 -1
  16. package/dist/{guest-auth-provider--Sqlucfr.js → guest-auth-provider-DJ5nCGjq.js} +2 -2
  17. package/dist/{guest-auth-provider--Sqlucfr.js.map → guest-auth-provider-DJ5nCGjq.js.map} +1 -1
  18. package/dist/{guest-auth-provider-CPj-w7_R.js → guest-auth-provider-DXNCZ0R6.js} +2 -2
  19. package/dist/{guest-auth-provider-CPj-w7_R.js.map → guest-auth-provider-DXNCZ0R6.js.map} +1 -1
  20. package/dist/{guest-auth-provider-DMtDTZeA.mjs → guest-auth-provider-DsFTB844.mjs} +2 -2
  21. package/dist/{guest-auth-provider-DMtDTZeA.mjs.map → guest-auth-provider-DsFTB844.mjs.map} +1 -1
  22. package/dist/{index-CaZx7sd8.mjs → index-Bu6vnwxM.mjs} +385 -471
  23. package/dist/index-Bu6vnwxM.mjs.map +1 -0
  24. package/dist/{index-Pyb2SLgv.js → index-yb-jFt5B.js} +385 -471
  25. package/dist/index-yb-jFt5B.js.map +1 -0
  26. package/dist/index.d.ts +1 -1
  27. package/dist/index.js +2 -2
  28. package/dist/index.mjs +1 -1
  29. package/dist/{index.native-3T0IcOed.mjs → index.native-BSOqaKIz.mjs} +321 -447
  30. package/dist/index.native-BSOqaKIz.mjs.map +1 -0
  31. package/dist/{index.native-PGrxu7RO.js → index.native-BpcNwWQX.js} +321 -447
  32. package/dist/index.native-BpcNwWQX.js.map +1 -0
  33. package/dist/index.native.d.ts +1 -1
  34. package/dist/index.native.js +2 -2
  35. package/dist/index.native.mjs +1 -1
  36. package/dist/{injected-evm-wallet-provider-B3Mtkdnu.mjs → injected-evm-wallet-provider-BuDISWSq.mjs} +2 -2
  37. package/dist/{injected-evm-wallet-provider-B3Mtkdnu.mjs.map → injected-evm-wallet-provider-BuDISWSq.mjs.map} +1 -1
  38. package/dist/{injected-evm-wallet-provider-U0XJu5HM.js → injected-evm-wallet-provider-D7tQrDaO.js} +2 -2
  39. package/dist/{injected-evm-wallet-provider-U0XJu5HM.js.map → injected-evm-wallet-provider-D7tQrDaO.js.map} +1 -1
  40. package/dist/{injected-evm-wallet-provider-C8rBnyO1.mjs → injected-evm-wallet-provider-Xb2G-ZGI.mjs} +2 -2
  41. package/dist/{injected-evm-wallet-provider-C8rBnyO1.mjs.map → injected-evm-wallet-provider-Xb2G-ZGI.mjs.map} +1 -1
  42. package/dist/{injected-evm-wallet-provider-BNu6Z-iN.js → injected-evm-wallet-provider-_UNpc_hM.js} +2 -2
  43. package/dist/{injected-evm-wallet-provider-BNu6Z-iN.js.map → injected-evm-wallet-provider-_UNpc_hM.js.map} +1 -1
  44. package/dist/{injected-wallet-provider-DR8xVudg.mjs → injected-wallet-provider-BDCLkdR_.mjs} +2 -2
  45. package/dist/{injected-wallet-provider-DR8xVudg.mjs.map → injected-wallet-provider-BDCLkdR_.mjs.map} +1 -1
  46. package/dist/{injected-wallet-provider-29eTkM-A.js → injected-wallet-provider-C7Q0haGY.js} +2 -2
  47. package/dist/{injected-wallet-provider-29eTkM-A.js.map → injected-wallet-provider-C7Q0haGY.js.map} +1 -1
  48. package/dist/{injected-wallet-provider-Cv9RELO6.js → injected-wallet-provider-CFwJYxh2.js} +2 -2
  49. package/dist/{injected-wallet-provider-Cv9RELO6.js.map → injected-wallet-provider-CFwJYxh2.js.map} +1 -1
  50. package/dist/{injected-wallet-provider-BnmYbm79.mjs → injected-wallet-provider-DZOwvKgd.mjs} +2 -2
  51. package/dist/{injected-wallet-provider-BnmYbm79.mjs.map → injected-wallet-provider-DZOwvKgd.mjs.map} +1 -1
  52. package/package.json +2 -2
  53. package/dist/index-CaZx7sd8.mjs.map +0 -1
  54. package/dist/index-Pyb2SLgv.js.map +0 -1
  55. package/dist/index.native-3T0IcOed.mjs.map +0 -1
  56. package/dist/index.native-PGrxu7RO.js.map +0 -1
@@ -211,12 +211,46 @@ function setPlatform(adapter) {
211
211
  function getPlatform() {
212
212
  return _platform;
213
213
  }
214
+ // ---------------------------------------------------------------------------
215
+ // Mobile / Android detection helpers
216
+ // ---------------------------------------------------------------------------
217
+ /**
218
+ * Detect mobile device even when Chrome "Request Desktop Site" is active.
219
+ *
220
+ * Desktop mode on Android strips "Android" and "Mobile" from the UA, leaving
221
+ * something like "Linux x86_64 …". The fallback detects this by looking for
222
+ * touch capability + small viewport + a UA that doesn't belong to a known
223
+ * desktop OS (ChromeOS, Windows, macOS).
224
+ */
225
+ function detectMobile() {
226
+ const ua = _platform.getUserAgent();
227
+ if (/Android|iPhone|iPad|iPod/i.test(ua))
228
+ return true;
229
+ if (typeof navigator !== 'undefined' && typeof window !== 'undefined') {
230
+ const hasTouch = navigator.maxTouchPoints > 0;
231
+ // Macs don't have touch screens, so "Macintosh" + touch = iOS desktop mode
232
+ // (also catches iPadOS 13+ which always reports as Macintosh)
233
+ if (/Macintosh/i.test(ua) && hasTouch)
234
+ return true;
235
+ // Android desktop mode: Linux UA + touch + small viewport, excluding
236
+ // known desktop OSes (ChromeOS, Windows, macOS)
237
+ const isSmallViewport = window.innerWidth <= 1024;
238
+ const isDesktopOS = /CrOS|Windows|Macintosh/i.test(ua);
239
+ if (hasTouch && isSmallViewport && !isDesktopOS)
240
+ return true;
241
+ }
242
+ return false;
243
+ }
214
244
 
215
245
  /**
216
- * OffchainAuthProvider wraps a real auth provider (e.g., Phantom) for the poofnet environment.
246
+ * OffchainAuthProvider wraps a real auth provider (Turnkey, Phantom, ...) for the
247
+ * poofnet environment.
217
248
  *
218
- * signMessage shows a custom confirmation modal and then delegates to the wrapped
219
- * provider for a real detached signature over the exact offchain transaction message.
249
+ * Poofnet has no chain, but its onchain writes are still signed: the worker
250
+ * issues an unsendable approval message and verifies the wallet's signature
251
+ * before committing. So `signTransaction` and `signMessage` go straight to the
252
+ * wrapped wallet, whose own prompt is the only one the user sees. Only a raw
253
+ * submit - which needs a real cluster - is refused here.
220
254
  */
221
255
  class OffchainAuthProvider {
222
256
  constructor(wrappedProvider, config = {}) {
@@ -251,10 +285,7 @@ class OffchainAuthProvider {
251
285
  return null;
252
286
  }
253
287
  async signTransaction(tx) {
254
- if (getPlatform().hasDOM) {
255
- await this.showUnsupportedTransactionModal();
256
- }
257
- throw new Error('Poofnet does not support real Solana transactions. Deploy your project to mainnet to use this feature.');
288
+ return this.wrappedProvider.signTransaction(tx);
258
289
  }
259
290
  /**
260
291
  * Sign and submit transaction - not supported in poofnet environment.
@@ -279,21 +310,11 @@ class OffchainAuthProvider {
279
310
  }
280
311
  await this.wrappedProvider.exportWallet(address);
281
312
  }
282
- // ============ signMessage delegates to wrapped provider ============
283
313
  async signMessage(message) {
284
314
  var _a, _b, _c, _d, _e, _f;
285
315
  (_b = (_a = this.config).onSigningStart) === null || _b === void 0 ? void 0 : _b.call(_a);
286
316
  try {
287
- // On RN (no DOM), auto-confirm — there's no modal to show.
288
- // On web, show the confirmation modal.
289
- if (getPlatform().hasDOM) {
290
- const confirmed = await this.showTransactionModal(message);
291
- if (!confirmed) {
292
- throw new Error("Transaction rejected by user");
293
- }
294
- }
295
317
  const signature = await this.wrappedProvider.signMessage(message);
296
- // Callback: signing complete
297
318
  (_d = (_c = this.config).onSigningComplete) === null || _d === void 0 ? void 0 : _d.call(_c, signature);
298
319
  return signature;
299
320
  }
@@ -384,99 +405,6 @@ class OffchainAuthProvider {
384
405
  document.addEventListener("keydown", escHandler);
385
406
  });
386
407
  }
387
- async showTransactionModal(message) {
388
- return new Promise((resolve) => {
389
- // Parse the transaction message for display
390
- let parsed = {};
391
- try {
392
- parsed = JSON.parse(message);
393
- }
394
- catch (_a) {
395
- // If not JSON, just show raw message
396
- }
397
- // Create modal container. Captured in this closure so every teardown
398
- // path acts on exactly this prompt, never on a newer one that may have
399
- // replaced a shared field.
400
- const container = document.createElement("div");
401
- container.id = "poofnet-tx-modal";
402
- container.appendChild(this.buildTransactionModalContent(parsed, message));
403
- document.body.appendChild(container);
404
- // Add styles
405
- const style = document.createElement("style");
406
- style.textContent = this.getModalStyles();
407
- container.appendChild(style);
408
- // Mark all OTHER body children as inert to disable focus traps from
409
- // other modals, recording only the ones this modal changed (skipping
410
- // any already-inert) so cleanup restores exactly what we set and never
411
- // re-enables app-owned inert regions.
412
- const selfInerted = [];
413
- document.body.childNodes.forEach((child) => {
414
- if (child !== container &&
415
- child instanceof HTMLElement &&
416
- !child.hasAttribute("inert")) {
417
- child.setAttribute("inert", "");
418
- selfInerted.push(child);
419
- }
420
- });
421
- // Animate in
422
- requestAnimationFrame(() => {
423
- const overlay = container.querySelector(".poofnet-modal-overlay");
424
- const content = container.querySelector(".poofnet-modal-content");
425
- if (overlay)
426
- overlay.style.opacity = "1";
427
- if (content) {
428
- content.style.opacity = "1";
429
- content.style.transform = "translateY(0)";
430
- }
431
- });
432
- // Single idempotent teardown for every settlement path: removes the key
433
- // handler, restores only self-added inert, and removes exactly this
434
- // captured container after the exit animation.
435
- let settled = false;
436
- const escHandler = (e) => {
437
- if (e.key === "Escape")
438
- settle(false);
439
- };
440
- const cleanup = () => {
441
- if (settled)
442
- return;
443
- settled = true;
444
- document.removeEventListener("keydown", escHandler);
445
- for (const el of selfInerted)
446
- el.removeAttribute("inert");
447
- const overlay = container.querySelector(".poofnet-modal-overlay");
448
- const content = container.querySelector(".poofnet-modal-content");
449
- if (overlay)
450
- overlay.style.opacity = "0";
451
- if (content) {
452
- content.style.opacity = "0";
453
- content.style.transform = "translateY(20px)";
454
- }
455
- setTimeout(() => container.remove(), 200);
456
- };
457
- const settle = (confirmed) => {
458
- cleanup();
459
- resolve(confirmed);
460
- };
461
- // Handle confirm
462
- const confirmBtn = container.querySelector("#poofnet-confirm-btn");
463
- confirmBtn === null || confirmBtn === void 0 ? void 0 : confirmBtn.addEventListener("click", () => settle(true));
464
- // Handle cancel
465
- const cancelBtn = container.querySelector("#poofnet-cancel-btn");
466
- cancelBtn === null || cancelBtn === void 0 ? void 0 : cancelBtn.addEventListener("click", () => settle(false));
467
- // Handle close button
468
- const closeBtn = container.querySelector("#poofnet-close-btn");
469
- closeBtn === null || closeBtn === void 0 ? void 0 : closeBtn.addEventListener("click", () => settle(false));
470
- // Handle overlay click
471
- const overlay = container.querySelector(".poofnet-modal-overlay");
472
- overlay === null || overlay === void 0 ? void 0 : overlay.addEventListener("click", (e) => {
473
- if (e.target === overlay)
474
- settle(false);
475
- });
476
- // Handle escape key
477
- document.addEventListener("keydown", escHandler);
478
- });
479
- }
480
408
  createElement(tag, className, text) {
481
409
  const el = document.createElement(tag);
482
410
  if (className)
@@ -531,113 +459,6 @@ class OffchainAuthProvider {
531
459
  footer.appendChild(close);
532
460
  return overlay;
533
461
  }
534
- formatPath(path) {
535
- const parts = path.split("/").filter(Boolean);
536
- if (parts.length >= 2) {
537
- return {
538
- collection: parts[0],
539
- documentId: parts[parts.length - 1],
540
- };
541
- }
542
- return { collection: parts[0] || path, documentId: null };
543
- }
544
- formatFieldValue(value) {
545
- if (value === null || value === undefined)
546
- return "null";
547
- if (typeof value === "string") {
548
- return value.length > 30
549
- ? `"${value.slice(0, 30)}..."`
550
- : `"${value}"`;
551
- }
552
- if (typeof value === "number" || typeof value === "boolean")
553
- return String(value);
554
- if (Array.isArray(value))
555
- return `[${value.length} items]`;
556
- if (typeof value === "object")
557
- return `{${Object.keys(value).length} fields}`;
558
- return String(value);
559
- }
560
- getActionDescription(type, collection, isNew) {
561
- if (type === "delete")
562
- return `Delete from ${collection}`;
563
- return isNew ? `Create in ${collection}` : `Update ${collection}`;
564
- }
565
- buildTransactionModalContent(parsed, rawMessage) {
566
- const instructions = parsed.instructions || [];
567
- // Group instructions by type for summary
568
- const sets = instructions.filter((i) => i.type === "set");
569
- const deletes = instructions.filter((i) => i.type === "delete");
570
- // Create summary text
571
- let summaryText = "";
572
- if (sets.length > 0 && deletes.length > 0) {
573
- summaryText = `${sets.length} update${sets.length > 1 ? "s" : ""}, ${deletes.length} delete${deletes.length > 1 ? "s" : ""}`;
574
- }
575
- else if (sets.length > 0) {
576
- summaryText = `${sets.length} update${sets.length > 1 ? "s" : ""}`;
577
- }
578
- else if (deletes.length > 0) {
579
- summaryText = `${deletes.length} delete${deletes.length > 1 ? "s" : ""}`;
580
- }
581
- const { overlay, body, footer } = this.buildModalShell("🔐", "Confirm Transaction");
582
- if (instructions.length > 0) {
583
- if (summaryText) {
584
- this.appendTextElement(body, "div", "poofnet-summary", summaryText);
585
- }
586
- const list = this.createElement("div", "poofnet-instructions-list");
587
- for (const inst of instructions) {
588
- list.appendChild(this.buildInstructionElement(inst));
589
- }
590
- body.appendChild(list);
591
- }
592
- else {
593
- const section = this.createElement("div", "poofnet-section");
594
- this.appendTextElement(section, "h3", "poofnet-section-title", "Message");
595
- const raw = this.createElement("pre", "poofnet-raw-message", rawMessage.slice(0, 200) + (rawMessage.length > 200 ? "..." : ""));
596
- section.appendChild(raw);
597
- body.appendChild(section);
598
- }
599
- body.appendChild(this.buildInfoBox("This is a simulated transaction. No real fees will be charged."));
600
- const cancel = this.createElement("button", "poofnet-btn poofnet-btn-cancel", "Cancel");
601
- cancel.id = "poofnet-cancel-btn";
602
- cancel.type = "button";
603
- footer.appendChild(cancel);
604
- const confirm = this.createElement("button", "poofnet-btn poofnet-btn-confirm", "Confirm");
605
- confirm.id = "poofnet-confirm-btn";
606
- confirm.type = "button";
607
- footer.appendChild(confirm);
608
- return overlay;
609
- }
610
- buildInstructionElement(inst) {
611
- const { collection, documentId } = this.formatPath(inst.path);
612
- const isDelete = inst.type === "delete";
613
- const card = this.createElement("div", `poofnet-instruction ${isDelete ? "poofnet-action-delete" : "poofnet-action-set"}`);
614
- const header = this.createElement("div", "poofnet-instruction-header");
615
- this.appendTextElement(header, "span", "poofnet-instruction-icon", isDelete ? "🗑️" : "✏️");
616
- this.appendTextElement(header, "span", "poofnet-instruction-action", isDelete ? "Delete" : "Save");
617
- this.appendTextElement(header, "span", "poofnet-instruction-collection", collection);
618
- card.appendChild(header);
619
- if (documentId) {
620
- this.appendTextElement(card, "div", "poofnet-instruction-id", `ID: ${documentId}`);
621
- }
622
- if (inst.data && !isDelete) {
623
- const fieldKeys = Object.keys(inst.data).filter((key) => !key.startsWith("_"));
624
- const visibleFields = fieldKeys.slice(0, 4);
625
- if (visibleFields.length > 0) {
626
- const fields = this.createElement("div", "poofnet-fields");
627
- for (const key of visibleFields) {
628
- const row = this.createElement("div", "poofnet-field");
629
- this.appendTextElement(row, "span", "poofnet-field-key", `${key}:`);
630
- this.appendTextElement(row, "span", "poofnet-field-value", this.formatFieldValue(inst.data[key]));
631
- fields.appendChild(row);
632
- }
633
- if (fieldKeys.length > 4) {
634
- this.appendTextElement(fields, "div", "poofnet-field-more", `+${fieldKeys.length - 4} more fields`);
635
- }
636
- card.appendChild(fields);
637
- }
638
- }
639
- return card;
640
- }
641
462
  getModalStyles() {
642
463
  return `
643
464
  /* Ensure inert elements remain visible while being non-interactive */
@@ -734,130 +555,6 @@ class OffchainAuthProvider {
734
555
  flex: 1;
735
556
  }
736
557
 
737
- .poofnet-summary {
738
- font-size: 13px;
739
- color: #6b7280;
740
- margin-bottom: 12px;
741
- text-align: center;
742
- }
743
-
744
- .poofnet-section {
745
- margin-bottom: 16px;
746
- }
747
-
748
- .poofnet-section-title {
749
- font-size: 12px;
750
- font-weight: 600;
751
- color: #666;
752
- text-transform: uppercase;
753
- letter-spacing: 0.5px;
754
- margin: 0 0 8px;
755
- }
756
-
757
- .poofnet-instructions-list {
758
- display: flex;
759
- flex-direction: column;
760
- gap: 10px;
761
- }
762
-
763
- .poofnet-instruction {
764
- background: #f9fafb;
765
- border-radius: 10px;
766
- padding: 12px;
767
- border-left: 3px solid #8b5cf6;
768
- }
769
-
770
- .poofnet-instruction.poofnet-action-delete {
771
- border-left-color: #ef4444;
772
- }
773
-
774
- .poofnet-instruction-header {
775
- display: flex;
776
- align-items: center;
777
- gap: 8px;
778
- margin-bottom: 4px;
779
- }
780
-
781
- .poofnet-instruction-icon {
782
- font-size: 16px;
783
- flex-shrink: 0;
784
- }
785
-
786
- .poofnet-instruction-action {
787
- font-size: 13px;
788
- font-weight: 600;
789
- color: #8b5cf6;
790
- }
791
-
792
- .poofnet-action-delete .poofnet-instruction-action {
793
- color: #ef4444;
794
- }
795
-
796
- .poofnet-instruction-collection {
797
- font-size: 13px;
798
- font-weight: 500;
799
- color: #374151;
800
- }
801
-
802
- .poofnet-instruction-id {
803
- font-size: 11px;
804
- color: #9ca3af;
805
- font-family: monospace;
806
- margin-bottom: 8px;
807
- word-break: break-all;
808
- }
809
-
810
- .poofnet-fields {
811
- background: white;
812
- border-radius: 6px;
813
- padding: 8px 10px;
814
- margin-top: 8px;
815
- }
816
-
817
- .poofnet-field {
818
- display: flex;
819
- gap: 8px;
820
- padding: 4px 0;
821
- font-size: 12px;
822
- border-bottom: 1px solid #f3f4f6;
823
- }
824
-
825
- .poofnet-field:last-child {
826
- border-bottom: none;
827
- }
828
-
829
- .poofnet-field-key {
830
- color: #6b7280;
831
- font-weight: 500;
832
- flex-shrink: 0;
833
- }
834
-
835
- .poofnet-field-value {
836
- color: #374151;
837
- word-break: break-all;
838
- }
839
-
840
- .poofnet-field-more {
841
- font-size: 11px;
842
- color: #9ca3af;
843
- padding-top: 4px;
844
- font-style: italic;
845
- }
846
-
847
- .poofnet-raw-message {
848
- background: #f9fafb;
849
- border-radius: 8px;
850
- padding: 12px;
851
- font-size: 11px;
852
- font-family: monospace;
853
- color: #4b5563;
854
- white-space: pre-wrap;
855
- word-break: break-all;
856
- margin: 0;
857
- max-height: 120px;
858
- overflow-y: auto;
859
- }
860
-
861
558
  .poofnet-info-box {
862
559
  display: flex;
863
560
  align-items: center;
@@ -906,15 +603,6 @@ class OffchainAuthProvider {
906
603
  .poofnet-btn-cancel:hover {
907
604
  background: #e5e7eb;
908
605
  }
909
-
910
- .poofnet-btn-confirm {
911
- background: #8b5cf6;
912
- color: white;
913
- }
914
-
915
- .poofnet-btn-confirm:hover {
916
- background: #7c3aed;
917
- }
918
606
  `;
919
607
  }
920
608
  }
@@ -5379,9 +5067,18 @@ const OIDC_NO_WALLET = 'This session has no Turnkey wallet. Sign in again to pro
5379
5067
  // same tab (redirect + popup, or two popups) each own a distinct record, so a second
5380
5068
  // attempt cannot clobber the first's verifier/state/redirectUri.
5381
5069
  const PKCE_STORE_PREFIX = 'bounded.oidc.pkce';
5070
+ const STATE_PREFIX = 'bounded.';
5071
+ const LOGIN_TIMEOUT_MS = 10 * 60000;
5072
+ const CALLBACK_PARAMS = ['code', 'state', 'error', 'error_description', 'iss'];
5382
5073
  function pkceKey(state) {
5383
5074
  return `${PKCE_STORE_PREFIX}.${state}`;
5384
5075
  }
5076
+ function removeAttempt(state) {
5077
+ try {
5078
+ sessionStorage.removeItem(pkceKey(state));
5079
+ }
5080
+ catch ( /* Storage may have been disabled mid-login. */_a) { /* Storage may have been disabled mid-login. */ }
5081
+ }
5385
5082
  const ISSUER_SESSION_KIND_KEY = 'bounded_issuer_session_kind';
5386
5083
  // Active session store (Web on web, RN on React Native) — selected by config,
5387
5084
  // not by `typeof window` (truthy on RN). OIDC redirect/popup is web-only, but the
@@ -5490,6 +5187,165 @@ async function makePkce() {
5490
5187
  function randomState() {
5491
5188
  return b64url(getPlatform().getRandomBytes(16));
5492
5189
  }
5190
+ let pendingCompletion;
5191
+ let loginReturnError;
5192
+ /** The last OAuth return failure in this initialization, or null. Initialization
5193
+ * remains usable so the UI can display this error and offer a fresh sign-in. */
5194
+ function getLoginReturnError() {
5195
+ return (loginReturnError === null || loginReturnError === void 0 ? void 0 : loginReturnError.generation) === getInitializationGeneration() ? loginReturnError.error : null;
5196
+ }
5197
+ // A hosted page can contain both an app bundle and the platform widget's SDK.
5198
+ // They share one code exchange, while adopting the session in their own registries.
5199
+ const COMPLETIONS = Symbol.for('bounded.oidc.completions');
5200
+ function cleanCallbackUrl(url) {
5201
+ const clean = new URL(url);
5202
+ for (const key of CALLBACK_PARAMS)
5203
+ clean.searchParams.delete(key);
5204
+ return clean;
5205
+ }
5206
+ async function validateAttempt(pkce, callback) {
5207
+ const cfg = await core.getConfig();
5208
+ if (!pkce || typeof pkce.verifier !== 'string' || !pkce.verifier ||
5209
+ pkce.appId !== cfg.appId || pkce.issuer !== await issuerBase()) {
5210
+ throw new Error('Login belongs to a different app or auth server. Start sign-in again.');
5211
+ }
5212
+ if (!Number.isFinite(pkce.createdAt) || Date.now() - pkce.createdAt > LOGIN_TIMEOUT_MS || pkce.createdAt > Date.now()) {
5213
+ throw new Error('Login expired. Start sign-in again.');
5214
+ }
5215
+ if (callback && (cleanCallbackUrl(callback).href !== cleanCallbackUrl(new URL(pkce.redirectUri)).href ||
5216
+ (callback.searchParams.has('iss') && callback.searchParams.get('iss') !== pkce.issuer))) {
5217
+ throw new Error('Login returned to a different address or from a different auth server. Start sign-in again.');
5218
+ }
5219
+ }
5220
+ // The receiver acknowledges only after checking origin, window and state. An
5221
+ // absent/disconnected opener must never leave a popup claiming success.
5222
+ async function handOffPopup(url, state) {
5223
+ const cfg = await core.getConfig();
5224
+ const issuer = await issuerBase();
5225
+ const opener = window.opener;
5226
+ if (!opener || opener === window || opener.closed) {
5227
+ throw new Error('The original sign-in window is unavailable. Close this window and sign in again in the original tab.');
5228
+ }
5229
+ return new Promise((resolve, reject) => {
5230
+ const cleanup = () => { clearTimeout(timer); window.removeEventListener('message', onMessage); };
5231
+ const onMessage = (event) => {
5232
+ var _a;
5233
+ if (event.source !== opener || event.origin !== url.origin ||
5234
+ ((_a = event.data) === null || _a === void 0 ? void 0 : _a.type) !== 'bounded-oidc-accepted' || event.data.state !== state)
5235
+ return;
5236
+ cleanup();
5237
+ try {
5238
+ window.close();
5239
+ }
5240
+ catch ( /* The opener also closes its own popup handle. */_b) { /* The opener also closes its own popup handle. */ }
5241
+ resolve();
5242
+ };
5243
+ const timer = setTimeout(() => {
5244
+ cleanup();
5245
+ reject(new Error('Could not reach the original sign-in tab. Close this window and start sign-in again.'));
5246
+ }, 10000);
5247
+ window.addEventListener('message', onMessage);
5248
+ try {
5249
+ opener.postMessage({ type: 'bounded-oidc-code', state, code: url.searchParams.get('code'),
5250
+ error: url.searchParams.get('error'), errorDescription: url.searchParams.get('error_description'),
5251
+ callbackUrl: url.href, appId: cfg.appId, issuer }, url.origin);
5252
+ }
5253
+ catch (error) {
5254
+ cleanup();
5255
+ reject(error);
5256
+ }
5257
+ });
5258
+ }
5259
+ async function finishLoginReturn(url, state) {
5260
+ try {
5261
+ if (!/^bounded\.(popup|redirect)\.[A-Za-z0-9_-]{22}$/.test(state)) {
5262
+ throw new Error('Invalid login state. Start sign-in again.');
5263
+ }
5264
+ if (state.startsWith('bounded.popup.')) {
5265
+ await handOffPopup(url, state);
5266
+ return { handled: true, user: null };
5267
+ }
5268
+ const raw = sessionStorage.getItem(pkceKey(state));
5269
+ if (!raw)
5270
+ throw new Error('Missing login attempt. Start sign-in again in this tab.');
5271
+ const pkce = JSON.parse(raw);
5272
+ if (pkce.state !== state)
5273
+ throw new Error('OAuth state mismatch. Start sign-in again.');
5274
+ await validateAttempt(pkce, url);
5275
+ const error = url.searchParams.get('error');
5276
+ if (error)
5277
+ throw new Error(url.searchParams.get('error_description') || `Login failed: ${error}`);
5278
+ const user = await exchangeCode(url.searchParams.get('code'), pkce);
5279
+ return { handled: true, user, idToken: sessionManager().getIdToken() };
5280
+ }
5281
+ finally {
5282
+ removeAttempt(state);
5283
+ const clean = cleanCallbackUrl(url);
5284
+ if (window.location.href === url.href) {
5285
+ window.history.replaceState(window.history.state, document.title, clean.pathname + clean.search + clean.hash);
5286
+ }
5287
+ }
5288
+ }
5289
+ /** Internal initialization step. Unrelated OAuth callbacks are left untouched. */
5290
+ function completePendingLogin() {
5291
+ if (!hasDOM$2())
5292
+ return Promise.resolve({ handled: false, user: null });
5293
+ const url = new URL(window.location.href);
5294
+ const state = url.searchParams.get('state');
5295
+ if (!(state === null || state === void 0 ? void 0 : state.startsWith(STATE_PREFIX)) || (!url.searchParams.get('code') && !url.searchParams.get('error'))) {
5296
+ return Promise.resolve({ handled: false, user: null });
5297
+ }
5298
+ const generation = getInitializationGeneration();
5299
+ if ((pendingCompletion === null || pendingCompletion === void 0 ? void 0 : pendingCompletion.href) === url.href && pendingCompletion.generation === generation)
5300
+ return pendingCompletion.promise;
5301
+ const promise = finishSharedLoginReturn(url, state).catch((error) => {
5302
+ loginReturnError = { generation, error: error instanceof Error ? error : new Error(String(error)) };
5303
+ throw loginReturnError.error;
5304
+ }).finally(() => {
5305
+ if ((pendingCompletion === null || pendingCompletion === void 0 ? void 0 : pendingCompletion.promise) === promise)
5306
+ pendingCompletion = undefined;
5307
+ });
5308
+ pendingCompletion = { href: url.href, generation, promise };
5309
+ return promise;
5310
+ }
5311
+ async function finishSharedLoginReturn(url, state) {
5312
+ var _a;
5313
+ var _b;
5314
+ const cfg = await core.getConfig();
5315
+ const issuer = await issuerBase();
5316
+ if (state.startsWith('bounded.redirect.')) {
5317
+ const raw = sessionStorage.getItem(pkceKey(state));
5318
+ // A second SDK configured for another app/issuer on this page does not own
5319
+ // this return. Leave its URL and proof available to the initiating app.
5320
+ try {
5321
+ const attempt = raw && JSON.parse(raw);
5322
+ if (attempt && (attempt.appId !== cfg.appId || attempt.issuer !== issuer))
5323
+ return { handled: false, user: null };
5324
+ }
5325
+ catch ( /* The owning completion reports malformed attempt data. */_c) { /* The owning completion reports malformed attempt data. */ }
5326
+ }
5327
+ const shared = (_a = (_b = window)[COMPLETIONS]) !== null && _a !== void 0 ? _a : (_b[COMPLETIONS] = new Map());
5328
+ const key = JSON.stringify([url.href, cfg.appId, issuer]);
5329
+ const existing = shared.get(key);
5330
+ if (!existing) {
5331
+ const completion = finishLoginReturn(url, state);
5332
+ shared.set(key, completion);
5333
+ return completion;
5334
+ }
5335
+ const generation = getInitializationGeneration();
5336
+ const result = await existing;
5337
+ if (result.user) {
5338
+ if (!result.idToken || sessionManager().getIdToken() !== result.idToken) {
5339
+ throw new Error('The completed login session has changed. Start sign-in again.');
5340
+ }
5341
+ await adoptSessionProvider({ provider: oidcProviderSingleton, method: 'email', configKey: 'direct:oidc',
5342
+ user: Object.assign(Object.assign({}, result.user), { provider: oidcProviderSingleton }), beforeCommit: () => {
5343
+ if (generation !== getInitializationGeneration())
5344
+ throw new Error('Login was superseded by SDK reinitialization. Start sign-in again.');
5345
+ } });
5346
+ }
5347
+ return result;
5348
+ }
5493
5349
  function normalizeMethod(value) {
5494
5350
  const normalized = String(value || '').trim().toLowerCase();
5495
5351
  return /^[a-z0-9_-]{1,40}$/.test(normalized) ? normalized : null;
@@ -5532,7 +5388,8 @@ function appendHostedLoginHints(params, opts) {
5532
5388
  // native (expo-web-browser) path. The caller decides where the PkceState lives
5533
5389
  // (web → sessionStorage across the full-page redirect; native → in-memory across
5534
5390
  // the awaited browser session).
5535
- async function buildAuthorize(opts) {
5391
+ async function buildAuthorize(opts, mode = 'redirect') {
5392
+ loginReturnError = undefined;
5536
5393
  const cfg = await core.getConfig();
5537
5394
  if (!cfg.appId)
5538
5395
  throw new Error('appId is not configured (init the SDK with your appId)');
@@ -5541,15 +5398,23 @@ async function buildAuthorize(opts) {
5541
5398
  const redirectUri = opts.redirectUri || (hasDOM$2() ? window.location.origin + window.location.pathname : undefined);
5542
5399
  if (!redirectUri)
5543
5400
  throw new Error('redirectUri is required on React Native — pass the https universal link the app owner registered.');
5401
+ if (hasDOM$2()) {
5402
+ const redirect = new URL(redirectUri);
5403
+ if (redirect.origin !== window.location.origin || redirect.hash || CALLBACK_PARAMS.some((key) => redirect.searchParams.has(key))) {
5404
+ throw new Error('redirectUri must be on this app origin, without a fragment or OAuth callback parameters.');
5405
+ }
5406
+ }
5544
5407
  const { verifier, challenge } = await makePkce();
5545
- const state = randomState();
5408
+ const state = `${STATE_PREFIX}${mode}.${randomState()}`;
5546
5409
  const base = await issuerBase();
5547
5410
  const params = new URLSearchParams({
5548
5411
  client_id: cfg.appId, redirect_uri: redirectUri, response_type: 'code', scope: 'openid profile email',
5549
5412
  code_challenge: challenge, code_challenge_method: 'S256', state,
5550
5413
  });
5551
5414
  appendHostedLoginHints(params, opts);
5552
- return { url: `${base}/api/auth/oauth2/authorize?${params.toString()}`, pkce: { verifier, state, redirectUri } };
5415
+ return { url: `${base}/api/auth/oauth2/authorize?${params.toString()}`, pkce: {
5416
+ verifier, state, redirectUri, appId: cfg.appId, issuer: base, createdAt: Date.now(),
5417
+ } };
5553
5418
  }
5554
5419
  // Are we on React Native (no DOM)? OIDC dispatch uses this to choose the native
5555
5420
  // expo-web-browser driver vs. the web full-page-redirect/popup driver.
@@ -5598,8 +5463,7 @@ function parseCallbackParams(callbackUrl) {
5598
5463
  }
5599
5464
  // Full native login: open the hosted page in the system auth session, await the
5600
5465
  // universal-link callback, verify state, and run the SAME PKCE code→token exchange
5601
- // as web. Returns the signed-in User (native completes inline — there's no separate
5602
- // completeLoginFromRedirect() round-trip the way the web full-page redirect needs).
5466
+ // as web. Returns the signed-in User inline; web redirects complete on the next init().
5603
5467
  async function runNativeLogin(opts) {
5604
5468
  var _a, _b;
5605
5469
  const { url, pkce } = await buildAuthorize(opts);
@@ -5626,7 +5490,7 @@ async function runNativeLogin(opts) {
5626
5490
  if (!returnedState || returnedState !== pkce.state) {
5627
5491
  throw new Error('OAuth state mismatch — possible CSRF; aborting');
5628
5492
  }
5629
- return await exchangeCode(code, pkce.verifier, pkce.redirectUri);
5493
+ return await exchangeCode(code, pkce);
5630
5494
  }
5631
5495
  finally {
5632
5496
  setAuthLoading(false);
@@ -5635,7 +5499,7 @@ async function runNativeLogin(opts) {
5635
5499
  /** Start hosted login (Google / Apple / email / hosted phone). Works on WEB and REACT NATIVE.
5636
5500
  *
5637
5501
  * WEB: navigates the whole page to the hosted Bounded login and resolves (void) as the page
5638
- * unloads; on return to `redirectUri?code=...` you call completeLoginFromRedirect().
5502
+ * unloads; init() completes the return to redirectUri automatically.
5639
5503
  *
5640
5504
  * REACT NATIVE: opens the hosted page via expo-web-browser, awaits the https universal-link
5641
5505
  * callback, runs the PKCE exchange INLINE, and resolves with the signed-in User. No
@@ -5653,63 +5517,32 @@ async function loginWithRedirect(opts) {
5653
5517
  // is per-tab and cleared on close — appropriate for a short-lived auth handshake. Keyed by
5654
5518
  // this attempt's own state so a second concurrent login cannot overwrite it.
5655
5519
  sessionStorage.setItem(pkceKey(pkce.state), JSON.stringify(pkce));
5656
- setAuthLoading(true); // reflect "signing in…" right up to navigation; the return page's
5657
- window.location.href = url; // completeLoginFromRedirect() keeps it true until the user resolves.
5658
- }
5659
- /** Finish login on the redirect_uri page (WEB ONLY): exchange ?code= for a token (PKCE-verified),
5660
- * store the session, and return the User. Idempotent: returns null if there's no ?code= in the URL.
5661
- *
5662
- * On REACT NATIVE this returns null and is a no-op: native login completes INLINE inside
5663
- * loginWithRedirect() (which resolves with the User), so there is no separate redirect
5664
- * page to finish from. Calling it on RN is harmless. */
5665
- async function completeLoginFromRedirect() {
5666
- if (!hasDOM$2())
5667
- return null;
5668
- const url = new URL(window.location.href);
5669
- const code = url.searchParams.get('code');
5670
- const returnedState = url.searchParams.get('state');
5671
- if (!code)
5672
- return null;
5673
- // POPUP path: if we're the redirect_uri page running INSIDE the loginWithPopup() window,
5674
- // hand the code to the opener and close — no separate completeLoginInPopup() call needed.
5675
- // The opener is the same app/origin, so post to our own origin (the opener's message
5676
- // handler checks ev.origin === redirect_uri origin). One call now finishes BOTH flows.
5677
- if (window.opener && window.opener !== window) {
5678
- try {
5679
- window.opener.postMessage({ type: 'bounded-oidc-code', code, state: returnedState }, window.location.origin);
5680
- }
5681
- catch ( /* ignore */_a) { /* ignore */ }
5682
- try {
5683
- window.close();
5684
- }
5685
- catch ( /* ignore */_b) { /* ignore */ }
5686
- return null;
5687
- }
5520
+ // This promise represents dispatch, not authentication on the next page.
5521
+ // Browsers can cancel navigation (for example at beforeunload) without firing
5522
+ // pageshow. Never leave this document's SDK loading after dispatch settles.
5688
5523
  setAuthLoading(true);
5689
5524
  try {
5690
- // Read back the exact record THIS attempt wrote, keyed by the state the issuer
5691
- // returned, so a concurrent login's record is never consumed by mistake.
5692
- const raw = returnedState ? sessionStorage.getItem(pkceKey(returnedState)) : null;
5693
- if (!raw)
5694
- throw new Error('Missing PKCE state — start login with loginWithRedirect()');
5695
- const pkce = JSON.parse(raw);
5696
- if (!returnedState || returnedState !== pkce.state)
5697
- throw new Error('OAuth state mismatch — possible CSRF; aborting');
5698
- const user = await exchangeCode(code, pkce.verifier, pkce.redirectUri);
5699
- sessionStorage.removeItem(pkceKey(returnedState));
5700
- // Strip code/state from the URL so a reload doesn't re-exchange (codes are single-use anyway).
5701
- url.searchParams.delete('code');
5702
- url.searchParams.delete('state');
5703
- window.history.replaceState({}, document.title, url.pathname + url.search + url.hash);
5704
- return user;
5525
+ window.location.href = url;
5526
+ }
5527
+ catch (error) {
5528
+ removeAttempt(pkce.state);
5529
+ throw error;
5705
5530
  }
5706
5531
  finally {
5707
5532
  setAuthLoading(false);
5708
5533
  }
5709
5534
  }
5535
+ /** Optional explicit completion. Normal apps only await init(), which already finishes login.
5536
+ * Concurrent calls share one exchange. Returns null when no Bounded return is present. */
5537
+ async function completeLoginFromRedirect() {
5538
+ const result = await completePendingLogin();
5539
+ const error = getLoginReturnError();
5540
+ if (error)
5541
+ throw error;
5542
+ return result.user;
5543
+ }
5710
5544
  /** Popup variant: open the hosted login in a popup; resolves with the User once the popup's
5711
- * redirect_uri page posts the code back. The redirect_uri page just calls the usual
5712
- * completeLoginFromRedirect() (which auto-detects the popup) — no separate call needed. */
5545
+ * redirect_uri page initializes the SDK, which hands the code back automatically. */
5713
5546
  async function loginWithPopup(opts) {
5714
5547
  if (!hasDOM$2())
5715
5548
  throw new Error('loginWithPopup is web-only — on React Native use loginWithRedirect()');
@@ -5728,7 +5561,7 @@ async function loginWithPopup(opts) {
5728
5561
  throw new Error('Popup blocked — use loginWithRedirect() instead');
5729
5562
  let authorization;
5730
5563
  try {
5731
- authorization = await buildAuthorize(opts);
5564
+ authorization = await buildAuthorize(opts, 'popup');
5732
5565
  }
5733
5566
  catch (error) {
5734
5567
  try {
@@ -5763,6 +5596,7 @@ async function loginWithPopup(opts) {
5763
5596
  popup.location.href = url;
5764
5597
  }
5765
5598
  catch (_d) {
5599
+ removeAttempt(pkce.state);
5766
5600
  try {
5767
5601
  popup.close();
5768
5602
  }
@@ -5778,17 +5612,21 @@ async function loginWithPopup(opts) {
5778
5612
  let settled = false;
5779
5613
  let timer;
5780
5614
  let navigationTimer;
5615
+ let completionTimer;
5781
5616
  const cleanup = () => {
5782
5617
  settled = true;
5783
5618
  clearInterval(timer);
5784
5619
  clearTimeout(navigationTimer);
5620
+ clearTimeout(completionTimer);
5785
5621
  window.removeEventListener('message', onMsg);
5622
+ removeAttempt(pkce.state);
5786
5623
  };
5787
5624
  const onMsg = async (ev) => {
5788
- if (ev.origin !== expectedOrigin)
5789
- return; // only trust the redirect_uri origin
5625
+ if (settled || ev.origin !== expectedOrigin || ev.source !== popup)
5626
+ return;
5790
5627
  const d = ev.data;
5791
- if (!d || d.type !== 'bounded-oidc-code' || typeof d.code !== 'string')
5628
+ if (!d || d.type !== 'bounded-oidc-code' ||
5629
+ (typeof d.code !== 'string' && typeof d.error !== 'string'))
5792
5630
  return;
5793
5631
  // Validate state BEFORE tearing down this listener, against the record THIS
5794
5632
  // attempt captured in the closure (`pkce`), not a shared sessionStorage reread.
@@ -5796,10 +5634,15 @@ async function loginWithPopup(opts) {
5796
5634
  // stay registered for our own code, instead of cleaning up on another's message.
5797
5635
  if (d.state !== pkce.state)
5798
5636
  return;
5637
+ if (d.appId !== pkce.appId || d.issuer !== pkce.issuer)
5638
+ return;
5799
5639
  cleanup();
5800
5640
  try {
5801
- const user = await exchangeCode(d.code, pkce.verifier, pkce.redirectUri);
5802
- sessionStorage.removeItem(pkceKey(pkce.state));
5641
+ await validateAttempt(pkce, new URL(d.callbackUrl));
5642
+ popup.postMessage({ type: 'bounded-oidc-accepted', state: pkce.state }, expectedOrigin);
5643
+ if (d.error)
5644
+ throw new Error(typeof d.errorDescription === 'string' ? d.errorDescription : `Login failed: ${d.error}`);
5645
+ const user = await exchangeCode(d.code, pkce);
5803
5646
  try {
5804
5647
  popup.close();
5805
5648
  }
@@ -5807,10 +5650,22 @@ async function loginWithPopup(opts) {
5807
5650
  resolve(user);
5808
5651
  }
5809
5652
  catch (e) {
5653
+ try {
5654
+ popup.close();
5655
+ }
5656
+ catch ( /* ignore */_b) { /* ignore */ }
5810
5657
  reject(e);
5811
5658
  }
5812
5659
  };
5813
5660
  window.addEventListener('message', onMsg);
5661
+ completionTimer = setTimeout(() => {
5662
+ cleanup();
5663
+ try {
5664
+ popup.close();
5665
+ }
5666
+ catch ( /* ignore */_a) { /* ignore */ }
5667
+ reject(new Error('Login expired or its popup disconnected. Start sign-in again.'));
5668
+ }, LOGIN_TIMEOUT_MS);
5814
5669
  // Some browser/extension combinations can return a real popup handle but
5815
5670
  // strand the window at about:blank instead of navigating to the hosted
5816
5671
  // authorize URL. The old code then waited forever because the popup was
@@ -5868,25 +5723,6 @@ async function loginWithPopup(opts) {
5868
5723
  setAuthLoading(false);
5869
5724
  }
5870
5725
  }
5871
- /** Call this on the redirect_uri page when login was opened via loginWithPopup(): posts the
5872
- * code back to the opener and closes. (For the redirect variant use completeLoginFromRedirect.) */
5873
- function completeLoginInPopup(openerOrigin) {
5874
- if (!hasDOM$2() || !window.opener)
5875
- return;
5876
- const url = new URL(window.location.href);
5877
- const code = url.searchParams.get('code');
5878
- const state = url.searchParams.get('state');
5879
- if (!code)
5880
- return;
5881
- try {
5882
- window.opener.postMessage({ type: 'bounded-oidc-code', code, state }, openerOrigin);
5883
- }
5884
- catch ( /* ignore */_a) { /* ignore */ }
5885
- try {
5886
- window.close();
5887
- }
5888
- catch ( /* ignore */_b) { /* ignore */ }
5889
- }
5890
5726
  // Establish a Bounded session from a server-minted inline-session response (the same
5891
5727
  // { idToken, accessToken, refreshToken } shape as /session/refresh). Used by the
5892
5728
  // Turnkey-native login path (authMode:"turnkey"), which mints the session server-side
@@ -5929,14 +5765,20 @@ async function inlineAuthEndpoint() {
5929
5765
  const cfg = await core.getConfig();
5930
5766
  return { base: await issuerBase(), appId: cfg.appId };
5931
5767
  }
5932
- async function exchangeCode(code, verifier, redirectUri) {
5933
- const cfg = await core.getConfig();
5934
- const base = await issuerBase();
5768
+ async function exchangeCode(code, pkce) {
5769
+ const generation = getInitializationGeneration();
5770
+ const beforeCommit = () => {
5771
+ if (generation !== getInitializationGeneration())
5772
+ throw new Error('Login was superseded by SDK reinitialization. Start sign-in again.');
5773
+ };
5774
+ await validateAttempt(pkce);
5775
+ beforeCommit();
5776
+ const base = pkce.issuer;
5935
5777
  // OIDC token endpoint: application/x-www-form-urlencoded (the standard). Public client (no
5936
5778
  // secret) authenticated by PKCE. The code is single-use + short-TTL on the server.
5937
5779
  const body = new URLSearchParams({
5938
- grant_type: 'authorization_code', code, redirect_uri: redirectUri,
5939
- client_id: cfg.appId, code_verifier: verifier,
5780
+ grant_type: 'authorization_code', code, redirect_uri: pkce.redirectUri,
5781
+ client_id: pkce.appId, code_verifier: pkce.verifier,
5940
5782
  });
5941
5783
  let res;
5942
5784
  try {
@@ -5960,10 +5802,20 @@ async function exchangeCode(code, verifier, redirectUri) {
5960
5802
  const accessToken = tokenField(data, 'access_token', 'accessToken') || idToken;
5961
5803
  const refreshToken = boundedRefreshTokenField(data);
5962
5804
  const user = userFromIdToken(idToken);
5963
- await sessionManager().storeSession(String(user.address || ''), accessToken, idToken, refreshToken, base);
5805
+ // Reinitialization while the request was in flight must not install this
5806
+ // app's result under a different app's session manager/configuration.
5807
+ beforeCommit();
5808
+ const rollback = await sessionManager().storeSession(String(user.address || ''), accessToken, idToken, refreshToken, base, beforeCommit);
5964
5809
  // Adopt on EVERY session surface (see establishInlineSession) - hosted
5965
5810
  // popup/redirect completions all land here, so this closes their gap too.
5966
- await adoptSessionProvider({ provider: oidcProviderSingleton, method: 'email', configKey: 'direct:oidc', user });
5811
+ try {
5812
+ await adoptSessionProvider({ provider: oidcProviderSingleton, method: 'email', configKey: 'direct:oidc', user, beforeCommit });
5813
+ }
5814
+ catch (error) {
5815
+ if (typeof rollback === 'function')
5816
+ await rollback();
5817
+ throw error;
5818
+ }
5967
5819
  // OIDC login does establish the issuer's Better Auth browser cookie. Record
5968
5820
  // that fact separately from the provider name so logout clears the cookie
5969
5821
  // and returns through the registered app origin. Stamped AFTER the adoption
@@ -6329,7 +6181,7 @@ async function getAuthProvider(config) {
6329
6181
  // manager as legacy email sessions. Human login should be started
6330
6182
  // with loginWithRedirect(); this provider is kept as the restore and
6331
6183
  // logout surface for those sessions.
6332
- currentAuthProvider = new (await Promise.resolve().then(function () { return require('./email-auth-provider-Cr3I4Po5.js'); })).EmailAuthProvider();
6184
+ currentAuthProvider = new (await Promise.resolve().then(function () { return require('./email-auth-provider-CB5ZBjJL.js'); })).EmailAuthProvider();
6333
6185
  break;
6334
6186
  case "phantom": {
6335
6187
  // First-class Solana WALLET LOGIN (the poof/TaroBase-style "connect
@@ -6371,7 +6223,7 @@ async function getAuthProvider(config) {
6371
6223
  // (bounded-login-widget.ts), so registration never counts as
6372
6224
  // evidence the user has a wallet.
6373
6225
  await ensureSolanaMobileWalletRegistered(config, configuredNetwork);
6374
- const { InjectedWalletProvider } = await Promise.resolve().then(function () { return require('./injected-wallet-provider-Cv9RELO6.js'); });
6226
+ const { InjectedWalletProvider } = await Promise.resolve().then(function () { return require('./injected-wallet-provider-CFwJYxh2.js'); });
6375
6227
  currentAuthProvider = new InjectedWalletProvider(resolveInjectedWalletConfig(config), rpcUrl, configuredNetwork);
6376
6228
  break;
6377
6229
  }
@@ -6384,7 +6236,7 @@ async function getAuthProvider(config) {
6384
6236
  // Selecting authMethod:'evm-wallet' IS the opt-in — pass evmWalletConfig
6385
6237
  // to point at a specific wallet or set chainId/domain.
6386
6238
  const evmWalletConfig = ((_a = config.evmWalletConfig) !== null && _a !== void 0 ? _a : {});
6387
- const { InjectedEvmWalletProvider } = await Promise.resolve().then(function () { return require('./injected-evm-wallet-provider-U0XJu5HM.js'); });
6239
+ const { InjectedEvmWalletProvider } = await Promise.resolve().then(function () { return require('./injected-evm-wallet-provider-D7tQrDaO.js'); });
6388
6240
  currentAuthProvider = new InjectedEvmWalletProvider(evmWalletConfig);
6389
6241
  break;
6390
6242
  }
@@ -6409,7 +6261,7 @@ async function getAuthProvider(config) {
6409
6261
  break;
6410
6262
  case "guest":
6411
6263
  // Anonymous auth: a device-local ed25519 keypair signs the challenge.
6412
- currentAuthProvider = new (await Promise.resolve().then(function () { return require('./guest-auth-provider-CPj-w7_R.js'); })).GuestAuthProvider();
6264
+ currentAuthProvider = new (await Promise.resolve().then(function () { return require('./guest-auth-provider-DXNCZ0R6.js'); })).GuestAuthProvider();
6413
6265
  break;
6414
6266
  }
6415
6267
  if (!currentAuthProvider) {
@@ -6471,7 +6323,7 @@ async function signInAnonymously() {
6471
6323
  // Lazy-load the guest provider so its ed25519/web3 dependencies stay out of
6472
6324
  // the main bundle for data-only apps. Specifier is a static string literal
6473
6325
  // so bundle-splitting can see it.
6474
- const provider = new (await Promise.resolve().then(function () { return require('./guest-auth-provider-CPj-w7_R.js'); })).GuestAuthProvider();
6326
+ const provider = new (await Promise.resolve().then(function () { return require('./guest-auth-provider-DXNCZ0R6.js'); })).GuestAuthProvider();
6475
6327
  setAuthLoading(true); // reflect "signing in…" while the guest keypair session is minted
6476
6328
  // adoptSessionProvider awaits only getConfig() (already resolved here) and then
6477
6329
  // runs every write - provider/method/config/marker/user - in one synchronous
@@ -6642,7 +6494,7 @@ function ensureSharedWalletLoginReady() {
6642
6494
  if (!sharedWalletReady) {
6643
6495
  sharedWalletReady = (async () => {
6644
6496
  await core.getConfig();
6645
- await Promise.resolve().then(function () { return require('./injected-wallet-provider-Cv9RELO6.js'); });
6497
+ await Promise.resolve().then(function () { return require('./injected-wallet-provider-CFwJYxh2.js'); });
6646
6498
  })().catch((error) => {
6647
6499
  sharedWalletReady = null;
6648
6500
  throw error;
@@ -6745,7 +6597,7 @@ async function loginWithWallet(options) {
6745
6597
  await ensureSharedWalletLoginReady();
6746
6598
  else
6747
6599
  await ensureWalletLoginReady();
6748
- const { InjectedWalletProvider } = await Promise.resolve().then(function () { return require('./injected-wallet-provider-Cv9RELO6.js'); });
6600
+ const { InjectedWalletProvider } = await Promise.resolve().then(function () { return require('./injected-wallet-provider-CFwJYxh2.js'); });
6749
6601
  // Keep the RAW wallet provider: the offchain wrapper forwards only a fixed
6750
6602
  // set of methods, so cleanup below must speak to the provider that actually
6751
6603
  // holds the config rather than to whatever is wrapping it.
@@ -6998,13 +6850,14 @@ var index = /*#__PURE__*/Object.freeze({
6998
6850
  WalletConfigError: WalletConfigError,
6999
6851
  clearIncompatibleSession: clearIncompatibleSession,
7000
6852
  completeLoginFromRedirect: completeLoginFromRedirect,
7001
- completeLoginInPopup: completeLoginInPopup,
6853
+ completePendingLogin: completePendingLogin,
7002
6854
  ensureSharedWalletLoginReady: ensureSharedWalletLoginReady,
7003
6855
  ensureWalletLoginReady: ensureWalletLoginReady,
7004
6856
  getAuthProvider: getAuthProvider,
7005
6857
  getConfiguredSolanaNetwork: getConfiguredSolanaNetwork,
7006
6858
  getCurrentAuthMethod: getCurrentAuthMethod,
7007
6859
  getInitConfig: getInitConfig,
6860
+ getLoginReturnError: getLoginReturnError,
7008
6861
  isEmailRequired: isEmailRequired,
7009
6862
  isWalletLoginDisabled: isWalletLoginDisabled,
7010
6863
  isWalletLoginEnabled: isWalletLoginEnabled,
@@ -7081,6 +6934,10 @@ let authLoadingListeners = [];
7081
6934
  let initCompleted = false;
7082
6935
  let isAuthLoading = false;
7083
6936
  let authReadyPromise = null;
6937
+ let initializingConfig = null;
6938
+ let initializationGeneration = 0;
6939
+ /** Internal guard for a login result arriving after the app reconfigured the SDK. */
6940
+ function getInitializationGeneration() { return initializationGeneration; }
7084
6941
  // This file acts as a middleware for the global state of the SDK
7085
6942
  // This mostly involves setting up the AuthProvider and managing the current user
7086
6943
  async function initialize(newConfig) {
@@ -7106,6 +6963,19 @@ async function initialize(newConfig) {
7106
6963
  authProviderInstance = await getAuthProvider(newConfig);
7107
6964
  // Initialize config with auth provider
7108
6965
  await core.init(Object.assign(Object.assign({}, newConfig), { authProvider: authProviderInstance }));
6966
+ // A return establishes its own provider/session, or hands off a popup.
6967
+ // Restoring the old provider afterwards would overwrite the new login.
6968
+ try {
6969
+ if ((await completePendingLogin()).handled) {
6970
+ initCompleted = true;
6971
+ return;
6972
+ }
6973
+ }
6974
+ catch (_a) {
6975
+ // A cancelled/failed return is a login failure, not a broken SDK.
6976
+ // The completion layer keeps it in getLoginReturnError(); restore
6977
+ // any existing session and leave sign-in/readiness usable for retry.
6978
+ }
7109
6979
  // Now that core is initialized, update the auth method on the config
7110
6980
  const authMethod = getCurrentAuthMethod();
7111
6981
  if (authMethod) {
@@ -7137,7 +7007,42 @@ async function initialize(newConfig) {
7137
7007
  setAuthLoading(false);
7138
7008
  }
7139
7009
  }
7010
+ // Compare data options by value, but preserve identity for callbacks and class
7011
+ // instances such as providers. Comparing only app/endpoints would silently drop
7012
+ // a concurrent change to wallet policy or provider configuration.
7013
+ function sameConfigValue(a, b, seen = new WeakMap()) {
7014
+ var _a;
7015
+ if (Object.is(a, b))
7016
+ return true;
7017
+ if (!a || !b || typeof a !== 'object' || typeof b !== 'object')
7018
+ return false;
7019
+ if (Array.isArray(a) !== Array.isArray(b))
7020
+ return false;
7021
+ if (Array.isArray(a) && a.length !== b.length)
7022
+ return false;
7023
+ if (!Array.isArray(a)) {
7024
+ for (const value of [a, b]) {
7025
+ const prototype = Object.getPrototypeOf(value);
7026
+ if (prototype !== null && ((_a = prototype.constructor) === null || _a === void 0 ? void 0 : _a.name) !== 'Object')
7027
+ return false;
7028
+ }
7029
+ }
7030
+ if (seen.has(a))
7031
+ return seen.get(a) === b;
7032
+ seen.set(a, b);
7033
+ const left = Object.keys(a), right = Object.keys(b);
7034
+ return left.length === right.length && left.every((key) => Object.prototype.hasOwnProperty.call(b, key)
7035
+ && sameConfigValue(a[key], b[key], seen));
7036
+ }
7140
7037
  function init(newConfig) {
7038
+ if (initializingConfig) {
7039
+ if (sameConfigValue(initializingConfig, newConfig)) {
7040
+ return authReadyPromise;
7041
+ }
7042
+ return Promise.reject(new Error('SDK initialization is in progress. Await init() before changing its configuration.'));
7043
+ }
7044
+ initializingConfig = Object.assign({}, newConfig);
7045
+ initializationGeneration++;
7141
7046
  let resolveReady;
7142
7047
  let rejectReady;
7143
7048
  const ready = new Promise((resolve, reject) => {
@@ -7148,7 +7053,7 @@ function init(newConfig) {
7148
7053
  // auth-loading listener to call waitForAuthReady() synchronously when init()
7149
7054
  // announces that session restoration has begun.
7150
7055
  authReadyPromise = ready;
7151
- void initialize(newConfig).then(resolveReady, rejectReady);
7056
+ void initialize(initializingConfig).then(() => { initializingConfig = null; resolveReady(); }, (error) => { initializingConfig = null; rejectReady(error); });
7152
7057
  return ready;
7153
7058
  }
7154
7059
  /**
@@ -7875,7 +7780,7 @@ function runOtpFrame(run) {
7875
7780
  // Backpack) via loginWithWallet(), run ENTIRELY on the app page (injected
7876
7781
  // providers only exist there, never inside a cross-origin frame).
7877
7782
  //
7878
- // Redirect stays available as the popup-blocked fallback (loginWithRedirect).
7783
+ // Mobile uses a full-page redirect; desktop keeps a popup with redirect fallback.
7879
7784
  // The same modal is reused for Turnkey wallet signing, so login -> first
7880
7785
  // signature is one continuous surface.
7881
7786
  // Palette + type ladder mirror the hosted auth page (login-page.ts): dark editorial
@@ -8118,6 +8023,16 @@ async function prepareAndOpen(opts, pending) {
8118
8023
  var _a, _b, _c, _d, _e, _f;
8119
8024
  // Imported lazily to avoid a static cycle with the auth index barrel.
8120
8025
  const authApi = await untilCancelled(Promise.resolve().then(function () { return index; }), pending);
8026
+ const hostedLogin = (options) => {
8027
+ if (detectMobile())
8028
+ return authApi.loginWithRedirect(options);
8029
+ // Invoke immediately inside the click handler to retain popup activation.
8030
+ return authApi.loginWithPopup(options).catch((error) => {
8031
+ if (shouldFallbackToRedirect(error))
8032
+ return authApi.loginWithRedirect(options);
8033
+ throw error;
8034
+ });
8035
+ };
8121
8036
  // BEFORE any side effect. Registering the mobile wallet from an empty config
8122
8037
  // would take the chainless mainnet default, and that registration cannot be
8123
8038
  // withdrawn - so opening the widget before init() would pin the page to the
@@ -8211,6 +8126,7 @@ async function prepareAndOpen(opts, pending) {
8211
8126
  // asserts rather than fixing a bug - which is exactly why it is worth keeping.
8212
8127
  pendingOpens.delete(pending);
8213
8128
  return new Promise((resolve, reject) => {
8129
+ var _a;
8214
8130
  // Per-open ownership generation (DS3-0402): a later open bumps the modal's
8215
8131
  // generation, so this open's token stops matching and all of its callbacks
8216
8132
  // (backdrop, success, cancel, and the async view swap) no-op instead of
@@ -8429,23 +8345,20 @@ async function prepareAndOpen(opts, pending) {
8429
8345
  loginHint: input.value,
8430
8346
  redirectUri: opts.redirectUri,
8431
8347
  };
8432
- authApi
8348
+ hostedLogin(emailOpts)
8433
8349
  // A hosted login that RESOLVES minted a real session, so it settles
8434
8350
  // the card even if another lane has since taken it - dropping it
8435
8351
  // would leave the app signed in behind a widget that says it is not.
8436
8352
  // Only the failure path belongs to one attempt's UI.
8437
- .loginWithPopup(emailOpts)
8438
- .then((u) => { settleAttempt(attempt); succeed(u); })
8439
- .catch((err) => {
8440
- // Popup unavailable: same-tab redirect instead. It resolves with
8441
- // nothing when it is about to navigate, so the only thing that
8442
- // comes back here is a REJECTION - which must still reach the
8443
- // handler below, or the lane stays locked with nothing running.
8444
- if (shouldFallbackToRedirect(err)) {
8445
- return authApi.loginWithRedirect(emailOpts).then((u) => { if (u)
8446
- succeed(u); });
8353
+ .then((u) => {
8354
+ if (u) {
8355
+ settleAttempt(attempt);
8356
+ succeed(u);
8357
+ }
8358
+ else if (settleAttempt(attempt)) {
8359
+ submit.disabled = false;
8360
+ submit.textContent = "Continue";
8447
8361
  }
8448
- throw err;
8449
8362
  })
8450
8363
  .catch((err) => {
8451
8364
  if (!settleAttempt(attempt))
@@ -8482,15 +8395,16 @@ async function prepareAndOpen(opts, pending) {
8482
8395
  }
8483
8396
  setBusy(b, "Opening secure sign-in\u2026");
8484
8397
  const socialOpts = { provider: method, redirectUri: opts.redirectUri };
8485
- authApi
8486
- .loginWithPopup(socialOpts)
8487
- .then((u) => { settleAttempt(attempt); succeed(u); })
8488
- .catch((err) => {
8489
- if (shouldFallbackToRedirect(err)) {
8490
- return authApi.loginWithRedirect(socialOpts).then((u) => { if (u)
8491
- succeed(u); });
8398
+ hostedLogin(socialOpts)
8399
+ .then((u) => {
8400
+ if (u) {
8401
+ settleAttempt(attempt);
8402
+ succeed(u);
8403
+ }
8404
+ else if (settleAttempt(attempt)) {
8405
+ b.disabled = false;
8406
+ setLabel(b, method);
8492
8407
  }
8493
- throw err;
8494
8408
  })
8495
8409
  .catch((err) => {
8496
8410
  if (!settleAttempt(attempt))
@@ -8754,7 +8668,7 @@ async function prepareAndOpen(opts, pending) {
8754
8668
  fitPanelToContent(m);
8755
8669
  });
8756
8670
  };
8757
- swap(renderChoose());
8671
+ swap(renderChoose((_a = authApi.getLoginReturnError()) === null || _a === void 0 ? void 0 : _a.message));
8758
8672
  showModal(m);
8759
8673
  });
8760
8674
  }
@@ -11065,7 +10979,6 @@ exports.bs58 = bs58;
11065
10979
  exports.bufferExports = bufferExports;
11066
10980
  exports.builds = builds;
11067
10981
  exports.completeLoginFromRedirect = completeLoginFromRedirect;
11068
- exports.completeLoginInPopup = completeLoginInPopup;
11069
10982
  exports.confirmAndCheckTransaction = confirmAndCheckTransaction;
11070
10983
  exports.connectBroadcast = connectBroadcast;
11071
10984
  exports.connectServiceSession = connectServiceSession;
@@ -11079,6 +10992,7 @@ exports.getAuthLoading = getAuthLoading;
11079
10992
  exports.getAuthProvider = getAuthProvider;
11080
10993
  exports.getCurrentUser = getCurrentUser;
11081
10994
  exports.getIdToken = getIdToken;
10995
+ exports.getLoginReturnError = getLoginReturnError;
11082
10996
  exports.getOrCreateTurnkeyWallet = getOrCreateTurnkeyWallet;
11083
10997
  exports.getPlatform = getPlatform;
11084
10998
  exports.getTurnkeyWalletRecoveryUrl = getTurnkeyWalletRecoveryUrl;
@@ -11119,4 +11033,4 @@ exports.useAuth = useAuth;
11119
11033
  exports.useBuildRun = useBuildRun;
11120
11034
  exports.useQuery = useQuery;
11121
11035
  exports.waitForAuthReady = waitForAuthReady;
11122
- //# sourceMappingURL=index-Pyb2SLgv.js.map
11036
+ //# sourceMappingURL=index-yb-jFt5B.js.map