@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
@@ -190,12 +190,46 @@ function setPlatform(adapter) {
190
190
  function getPlatform() {
191
191
  return _platform;
192
192
  }
193
+ // ---------------------------------------------------------------------------
194
+ // Mobile / Android detection helpers
195
+ // ---------------------------------------------------------------------------
196
+ /**
197
+ * Detect mobile device even when Chrome "Request Desktop Site" is active.
198
+ *
199
+ * Desktop mode on Android strips "Android" and "Mobile" from the UA, leaving
200
+ * something like "Linux x86_64 …". The fallback detects this by looking for
201
+ * touch capability + small viewport + a UA that doesn't belong to a known
202
+ * desktop OS (ChromeOS, Windows, macOS).
203
+ */
204
+ function detectMobile() {
205
+ const ua = _platform.getUserAgent();
206
+ if (/Android|iPhone|iPad|iPod/i.test(ua))
207
+ return true;
208
+ if (typeof navigator !== 'undefined' && typeof window !== 'undefined') {
209
+ const hasTouch = navigator.maxTouchPoints > 0;
210
+ // Macs don't have touch screens, so "Macintosh" + touch = iOS desktop mode
211
+ // (also catches iPadOS 13+ which always reports as Macintosh)
212
+ if (/Macintosh/i.test(ua) && hasTouch)
213
+ return true;
214
+ // Android desktop mode: Linux UA + touch + small viewport, excluding
215
+ // known desktop OSes (ChromeOS, Windows, macOS)
216
+ const isSmallViewport = window.innerWidth <= 1024;
217
+ const isDesktopOS = /CrOS|Windows|Macintosh/i.test(ua);
218
+ if (hasTouch && isSmallViewport && !isDesktopOS)
219
+ return true;
220
+ }
221
+ return false;
222
+ }
193
223
 
194
224
  /**
195
- * OffchainAuthProvider wraps a real auth provider (e.g., Phantom) for the poofnet environment.
225
+ * OffchainAuthProvider wraps a real auth provider (Turnkey, Phantom, ...) for the
226
+ * poofnet environment.
196
227
  *
197
- * signMessage shows a custom confirmation modal and then delegates to the wrapped
198
- * provider for a real detached signature over the exact offchain transaction message.
228
+ * Poofnet has no chain, but its onchain writes are still signed: the worker
229
+ * issues an unsendable approval message and verifies the wallet's signature
230
+ * before committing. So `signTransaction` and `signMessage` go straight to the
231
+ * wrapped wallet, whose own prompt is the only one the user sees. Only a raw
232
+ * submit - which needs a real cluster - is refused here.
199
233
  */
200
234
  class OffchainAuthProvider {
201
235
  constructor(wrappedProvider, config = {}) {
@@ -230,10 +264,7 @@ class OffchainAuthProvider {
230
264
  return null;
231
265
  }
232
266
  async signTransaction(tx) {
233
- if (getPlatform().hasDOM) {
234
- await this.showUnsupportedTransactionModal();
235
- }
236
- throw new Error('Poofnet does not support real Solana transactions. Deploy your project to mainnet to use this feature.');
267
+ return this.wrappedProvider.signTransaction(tx);
237
268
  }
238
269
  /**
239
270
  * Sign and submit transaction - not supported in poofnet environment.
@@ -258,21 +289,11 @@ class OffchainAuthProvider {
258
289
  }
259
290
  await this.wrappedProvider.exportWallet(address);
260
291
  }
261
- // ============ signMessage delegates to wrapped provider ============
262
292
  async signMessage(message) {
263
293
  var _a, _b, _c, _d, _e, _f;
264
294
  (_b = (_a = this.config).onSigningStart) === null || _b === void 0 ? void 0 : _b.call(_a);
265
295
  try {
266
- // On RN (no DOM), auto-confirm — there's no modal to show.
267
- // On web, show the confirmation modal.
268
- if (getPlatform().hasDOM) {
269
- const confirmed = await this.showTransactionModal(message);
270
- if (!confirmed) {
271
- throw new Error("Transaction rejected by user");
272
- }
273
- }
274
296
  const signature = await this.wrappedProvider.signMessage(message);
275
- // Callback: signing complete
276
297
  (_d = (_c = this.config).onSigningComplete) === null || _d === void 0 ? void 0 : _d.call(_c, signature);
277
298
  return signature;
278
299
  }
@@ -363,99 +384,6 @@ class OffchainAuthProvider {
363
384
  document.addEventListener("keydown", escHandler);
364
385
  });
365
386
  }
366
- async showTransactionModal(message) {
367
- return new Promise((resolve) => {
368
- // Parse the transaction message for display
369
- let parsed = {};
370
- try {
371
- parsed = JSON.parse(message);
372
- }
373
- catch (_a) {
374
- // If not JSON, just show raw message
375
- }
376
- // Create modal container. Captured in this closure so every teardown
377
- // path acts on exactly this prompt, never on a newer one that may have
378
- // replaced a shared field.
379
- const container = document.createElement("div");
380
- container.id = "poofnet-tx-modal";
381
- container.appendChild(this.buildTransactionModalContent(parsed, message));
382
- document.body.appendChild(container);
383
- // Add styles
384
- const style = document.createElement("style");
385
- style.textContent = this.getModalStyles();
386
- container.appendChild(style);
387
- // Mark all OTHER body children as inert to disable focus traps from
388
- // other modals, recording only the ones this modal changed (skipping
389
- // any already-inert) so cleanup restores exactly what we set and never
390
- // re-enables app-owned inert regions.
391
- const selfInerted = [];
392
- document.body.childNodes.forEach((child) => {
393
- if (child !== container &&
394
- child instanceof HTMLElement &&
395
- !child.hasAttribute("inert")) {
396
- child.setAttribute("inert", "");
397
- selfInerted.push(child);
398
- }
399
- });
400
- // Animate in
401
- requestAnimationFrame(() => {
402
- const overlay = container.querySelector(".poofnet-modal-overlay");
403
- const content = container.querySelector(".poofnet-modal-content");
404
- if (overlay)
405
- overlay.style.opacity = "1";
406
- if (content) {
407
- content.style.opacity = "1";
408
- content.style.transform = "translateY(0)";
409
- }
410
- });
411
- // Single idempotent teardown for every settlement path: removes the key
412
- // handler, restores only self-added inert, and removes exactly this
413
- // captured container after the exit animation.
414
- let settled = false;
415
- const escHandler = (e) => {
416
- if (e.key === "Escape")
417
- settle(false);
418
- };
419
- const cleanup = () => {
420
- if (settled)
421
- return;
422
- settled = true;
423
- document.removeEventListener("keydown", escHandler);
424
- for (const el of selfInerted)
425
- el.removeAttribute("inert");
426
- const overlay = container.querySelector(".poofnet-modal-overlay");
427
- const content = container.querySelector(".poofnet-modal-content");
428
- if (overlay)
429
- overlay.style.opacity = "0";
430
- if (content) {
431
- content.style.opacity = "0";
432
- content.style.transform = "translateY(20px)";
433
- }
434
- setTimeout(() => container.remove(), 200);
435
- };
436
- const settle = (confirmed) => {
437
- cleanup();
438
- resolve(confirmed);
439
- };
440
- // Handle confirm
441
- const confirmBtn = container.querySelector("#poofnet-confirm-btn");
442
- confirmBtn === null || confirmBtn === void 0 ? void 0 : confirmBtn.addEventListener("click", () => settle(true));
443
- // Handle cancel
444
- const cancelBtn = container.querySelector("#poofnet-cancel-btn");
445
- cancelBtn === null || cancelBtn === void 0 ? void 0 : cancelBtn.addEventListener("click", () => settle(false));
446
- // Handle close button
447
- const closeBtn = container.querySelector("#poofnet-close-btn");
448
- closeBtn === null || closeBtn === void 0 ? void 0 : closeBtn.addEventListener("click", () => settle(false));
449
- // Handle overlay click
450
- const overlay = container.querySelector(".poofnet-modal-overlay");
451
- overlay === null || overlay === void 0 ? void 0 : overlay.addEventListener("click", (e) => {
452
- if (e.target === overlay)
453
- settle(false);
454
- });
455
- // Handle escape key
456
- document.addEventListener("keydown", escHandler);
457
- });
458
- }
459
387
  createElement(tag, className, text) {
460
388
  const el = document.createElement(tag);
461
389
  if (className)
@@ -510,113 +438,6 @@ class OffchainAuthProvider {
510
438
  footer.appendChild(close);
511
439
  return overlay;
512
440
  }
513
- formatPath(path) {
514
- const parts = path.split("/").filter(Boolean);
515
- if (parts.length >= 2) {
516
- return {
517
- collection: parts[0],
518
- documentId: parts[parts.length - 1],
519
- };
520
- }
521
- return { collection: parts[0] || path, documentId: null };
522
- }
523
- formatFieldValue(value) {
524
- if (value === null || value === undefined)
525
- return "null";
526
- if (typeof value === "string") {
527
- return value.length > 30
528
- ? `"${value.slice(0, 30)}..."`
529
- : `"${value}"`;
530
- }
531
- if (typeof value === "number" || typeof value === "boolean")
532
- return String(value);
533
- if (Array.isArray(value))
534
- return `[${value.length} items]`;
535
- if (typeof value === "object")
536
- return `{${Object.keys(value).length} fields}`;
537
- return String(value);
538
- }
539
- getActionDescription(type, collection, isNew) {
540
- if (type === "delete")
541
- return `Delete from ${collection}`;
542
- return isNew ? `Create in ${collection}` : `Update ${collection}`;
543
- }
544
- buildTransactionModalContent(parsed, rawMessage) {
545
- const instructions = parsed.instructions || [];
546
- // Group instructions by type for summary
547
- const sets = instructions.filter((i) => i.type === "set");
548
- const deletes = instructions.filter((i) => i.type === "delete");
549
- // Create summary text
550
- let summaryText = "";
551
- if (sets.length > 0 && deletes.length > 0) {
552
- summaryText = `${sets.length} update${sets.length > 1 ? "s" : ""}, ${deletes.length} delete${deletes.length > 1 ? "s" : ""}`;
553
- }
554
- else if (sets.length > 0) {
555
- summaryText = `${sets.length} update${sets.length > 1 ? "s" : ""}`;
556
- }
557
- else if (deletes.length > 0) {
558
- summaryText = `${deletes.length} delete${deletes.length > 1 ? "s" : ""}`;
559
- }
560
- const { overlay, body, footer } = this.buildModalShell("🔐", "Confirm Transaction");
561
- if (instructions.length > 0) {
562
- if (summaryText) {
563
- this.appendTextElement(body, "div", "poofnet-summary", summaryText);
564
- }
565
- const list = this.createElement("div", "poofnet-instructions-list");
566
- for (const inst of instructions) {
567
- list.appendChild(this.buildInstructionElement(inst));
568
- }
569
- body.appendChild(list);
570
- }
571
- else {
572
- const section = this.createElement("div", "poofnet-section");
573
- this.appendTextElement(section, "h3", "poofnet-section-title", "Message");
574
- const raw = this.createElement("pre", "poofnet-raw-message", rawMessage.slice(0, 200) + (rawMessage.length > 200 ? "..." : ""));
575
- section.appendChild(raw);
576
- body.appendChild(section);
577
- }
578
- body.appendChild(this.buildInfoBox("This is a simulated transaction. No real fees will be charged."));
579
- const cancel = this.createElement("button", "poofnet-btn poofnet-btn-cancel", "Cancel");
580
- cancel.id = "poofnet-cancel-btn";
581
- cancel.type = "button";
582
- footer.appendChild(cancel);
583
- const confirm = this.createElement("button", "poofnet-btn poofnet-btn-confirm", "Confirm");
584
- confirm.id = "poofnet-confirm-btn";
585
- confirm.type = "button";
586
- footer.appendChild(confirm);
587
- return overlay;
588
- }
589
- buildInstructionElement(inst) {
590
- const { collection, documentId } = this.formatPath(inst.path);
591
- const isDelete = inst.type === "delete";
592
- const card = this.createElement("div", `poofnet-instruction ${isDelete ? "poofnet-action-delete" : "poofnet-action-set"}`);
593
- const header = this.createElement("div", "poofnet-instruction-header");
594
- this.appendTextElement(header, "span", "poofnet-instruction-icon", isDelete ? "🗑️" : "✏️");
595
- this.appendTextElement(header, "span", "poofnet-instruction-action", isDelete ? "Delete" : "Save");
596
- this.appendTextElement(header, "span", "poofnet-instruction-collection", collection);
597
- card.appendChild(header);
598
- if (documentId) {
599
- this.appendTextElement(card, "div", "poofnet-instruction-id", `ID: ${documentId}`);
600
- }
601
- if (inst.data && !isDelete) {
602
- const fieldKeys = Object.keys(inst.data).filter((key) => !key.startsWith("_"));
603
- const visibleFields = fieldKeys.slice(0, 4);
604
- if (visibleFields.length > 0) {
605
- const fields = this.createElement("div", "poofnet-fields");
606
- for (const key of visibleFields) {
607
- const row = this.createElement("div", "poofnet-field");
608
- this.appendTextElement(row, "span", "poofnet-field-key", `${key}:`);
609
- this.appendTextElement(row, "span", "poofnet-field-value", this.formatFieldValue(inst.data[key]));
610
- fields.appendChild(row);
611
- }
612
- if (fieldKeys.length > 4) {
613
- this.appendTextElement(fields, "div", "poofnet-field-more", `+${fieldKeys.length - 4} more fields`);
614
- }
615
- card.appendChild(fields);
616
- }
617
- }
618
- return card;
619
- }
620
441
  getModalStyles() {
621
442
  return `
622
443
  /* Ensure inert elements remain visible while being non-interactive */
@@ -713,130 +534,6 @@ class OffchainAuthProvider {
713
534
  flex: 1;
714
535
  }
715
536
 
716
- .poofnet-summary {
717
- font-size: 13px;
718
- color: #6b7280;
719
- margin-bottom: 12px;
720
- text-align: center;
721
- }
722
-
723
- .poofnet-section {
724
- margin-bottom: 16px;
725
- }
726
-
727
- .poofnet-section-title {
728
- font-size: 12px;
729
- font-weight: 600;
730
- color: #666;
731
- text-transform: uppercase;
732
- letter-spacing: 0.5px;
733
- margin: 0 0 8px;
734
- }
735
-
736
- .poofnet-instructions-list {
737
- display: flex;
738
- flex-direction: column;
739
- gap: 10px;
740
- }
741
-
742
- .poofnet-instruction {
743
- background: #f9fafb;
744
- border-radius: 10px;
745
- padding: 12px;
746
- border-left: 3px solid #8b5cf6;
747
- }
748
-
749
- .poofnet-instruction.poofnet-action-delete {
750
- border-left-color: #ef4444;
751
- }
752
-
753
- .poofnet-instruction-header {
754
- display: flex;
755
- align-items: center;
756
- gap: 8px;
757
- margin-bottom: 4px;
758
- }
759
-
760
- .poofnet-instruction-icon {
761
- font-size: 16px;
762
- flex-shrink: 0;
763
- }
764
-
765
- .poofnet-instruction-action {
766
- font-size: 13px;
767
- font-weight: 600;
768
- color: #8b5cf6;
769
- }
770
-
771
- .poofnet-action-delete .poofnet-instruction-action {
772
- color: #ef4444;
773
- }
774
-
775
- .poofnet-instruction-collection {
776
- font-size: 13px;
777
- font-weight: 500;
778
- color: #374151;
779
- }
780
-
781
- .poofnet-instruction-id {
782
- font-size: 11px;
783
- color: #9ca3af;
784
- font-family: monospace;
785
- margin-bottom: 8px;
786
- word-break: break-all;
787
- }
788
-
789
- .poofnet-fields {
790
- background: white;
791
- border-radius: 6px;
792
- padding: 8px 10px;
793
- margin-top: 8px;
794
- }
795
-
796
- .poofnet-field {
797
- display: flex;
798
- gap: 8px;
799
- padding: 4px 0;
800
- font-size: 12px;
801
- border-bottom: 1px solid #f3f4f6;
802
- }
803
-
804
- .poofnet-field:last-child {
805
- border-bottom: none;
806
- }
807
-
808
- .poofnet-field-key {
809
- color: #6b7280;
810
- font-weight: 500;
811
- flex-shrink: 0;
812
- }
813
-
814
- .poofnet-field-value {
815
- color: #374151;
816
- word-break: break-all;
817
- }
818
-
819
- .poofnet-field-more {
820
- font-size: 11px;
821
- color: #9ca3af;
822
- padding-top: 4px;
823
- font-style: italic;
824
- }
825
-
826
- .poofnet-raw-message {
827
- background: #f9fafb;
828
- border-radius: 8px;
829
- padding: 12px;
830
- font-size: 11px;
831
- font-family: monospace;
832
- color: #4b5563;
833
- white-space: pre-wrap;
834
- word-break: break-all;
835
- margin: 0;
836
- max-height: 120px;
837
- overflow-y: auto;
838
- }
839
-
840
537
  .poofnet-info-box {
841
538
  display: flex;
842
539
  align-items: center;
@@ -885,15 +582,6 @@ class OffchainAuthProvider {
885
582
  .poofnet-btn-cancel:hover {
886
583
  background: #e5e7eb;
887
584
  }
888
-
889
- .poofnet-btn-confirm {
890
- background: #8b5cf6;
891
- color: white;
892
- }
893
-
894
- .poofnet-btn-confirm:hover {
895
- background: #7c3aed;
896
- }
897
585
  `;
898
586
  }
899
587
  }
@@ -5358,9 +5046,18 @@ const OIDC_NO_WALLET = 'This session has no Turnkey wallet. Sign in again to pro
5358
5046
  // same tab (redirect + popup, or two popups) each own a distinct record, so a second
5359
5047
  // attempt cannot clobber the first's verifier/state/redirectUri.
5360
5048
  const PKCE_STORE_PREFIX = 'bounded.oidc.pkce';
5049
+ const STATE_PREFIX = 'bounded.';
5050
+ const LOGIN_TIMEOUT_MS = 10 * 60000;
5051
+ const CALLBACK_PARAMS = ['code', 'state', 'error', 'error_description', 'iss'];
5361
5052
  function pkceKey(state) {
5362
5053
  return `${PKCE_STORE_PREFIX}.${state}`;
5363
5054
  }
5055
+ function removeAttempt(state) {
5056
+ try {
5057
+ sessionStorage.removeItem(pkceKey(state));
5058
+ }
5059
+ catch ( /* Storage may have been disabled mid-login. */_a) { /* Storage may have been disabled mid-login. */ }
5060
+ }
5364
5061
  const ISSUER_SESSION_KIND_KEY = 'bounded_issuer_session_kind';
5365
5062
  // Active session store (Web on web, RN on React Native) — selected by config,
5366
5063
  // not by `typeof window` (truthy on RN). OIDC redirect/popup is web-only, but the
@@ -5469,6 +5166,165 @@ async function makePkce() {
5469
5166
  function randomState() {
5470
5167
  return b64url(getPlatform().getRandomBytes(16));
5471
5168
  }
5169
+ let pendingCompletion;
5170
+ let loginReturnError;
5171
+ /** The last OAuth return failure in this initialization, or null. Initialization
5172
+ * remains usable so the UI can display this error and offer a fresh sign-in. */
5173
+ function getLoginReturnError() {
5174
+ return (loginReturnError === null || loginReturnError === void 0 ? void 0 : loginReturnError.generation) === getInitializationGeneration() ? loginReturnError.error : null;
5175
+ }
5176
+ // A hosted page can contain both an app bundle and the platform widget's SDK.
5177
+ // They share one code exchange, while adopting the session in their own registries.
5178
+ const COMPLETIONS = Symbol.for('bounded.oidc.completions');
5179
+ function cleanCallbackUrl(url) {
5180
+ const clean = new URL(url);
5181
+ for (const key of CALLBACK_PARAMS)
5182
+ clean.searchParams.delete(key);
5183
+ return clean;
5184
+ }
5185
+ async function validateAttempt(pkce, callback) {
5186
+ const cfg = await getConfig();
5187
+ if (!pkce || typeof pkce.verifier !== 'string' || !pkce.verifier ||
5188
+ pkce.appId !== cfg.appId || pkce.issuer !== await issuerBase()) {
5189
+ throw new Error('Login belongs to a different app or auth server. Start sign-in again.');
5190
+ }
5191
+ if (!Number.isFinite(pkce.createdAt) || Date.now() - pkce.createdAt > LOGIN_TIMEOUT_MS || pkce.createdAt > Date.now()) {
5192
+ throw new Error('Login expired. Start sign-in again.');
5193
+ }
5194
+ if (callback && (cleanCallbackUrl(callback).href !== cleanCallbackUrl(new URL(pkce.redirectUri)).href ||
5195
+ (callback.searchParams.has('iss') && callback.searchParams.get('iss') !== pkce.issuer))) {
5196
+ throw new Error('Login returned to a different address or from a different auth server. Start sign-in again.');
5197
+ }
5198
+ }
5199
+ // The receiver acknowledges only after checking origin, window and state. An
5200
+ // absent/disconnected opener must never leave a popup claiming success.
5201
+ async function handOffPopup(url, state) {
5202
+ const cfg = await getConfig();
5203
+ const issuer = await issuerBase();
5204
+ const opener = window.opener;
5205
+ if (!opener || opener === window || opener.closed) {
5206
+ throw new Error('The original sign-in window is unavailable. Close this window and sign in again in the original tab.');
5207
+ }
5208
+ return new Promise((resolve, reject) => {
5209
+ const cleanup = () => { clearTimeout(timer); window.removeEventListener('message', onMessage); };
5210
+ const onMessage = (event) => {
5211
+ var _a;
5212
+ if (event.source !== opener || event.origin !== url.origin ||
5213
+ ((_a = event.data) === null || _a === void 0 ? void 0 : _a.type) !== 'bounded-oidc-accepted' || event.data.state !== state)
5214
+ return;
5215
+ cleanup();
5216
+ try {
5217
+ window.close();
5218
+ }
5219
+ catch ( /* The opener also closes its own popup handle. */_b) { /* The opener also closes its own popup handle. */ }
5220
+ resolve();
5221
+ };
5222
+ const timer = setTimeout(() => {
5223
+ cleanup();
5224
+ reject(new Error('Could not reach the original sign-in tab. Close this window and start sign-in again.'));
5225
+ }, 10000);
5226
+ window.addEventListener('message', onMessage);
5227
+ try {
5228
+ opener.postMessage({ type: 'bounded-oidc-code', state, code: url.searchParams.get('code'),
5229
+ error: url.searchParams.get('error'), errorDescription: url.searchParams.get('error_description'),
5230
+ callbackUrl: url.href, appId: cfg.appId, issuer }, url.origin);
5231
+ }
5232
+ catch (error) {
5233
+ cleanup();
5234
+ reject(error);
5235
+ }
5236
+ });
5237
+ }
5238
+ async function finishLoginReturn(url, state) {
5239
+ try {
5240
+ if (!/^bounded\.(popup|redirect)\.[A-Za-z0-9_-]{22}$/.test(state)) {
5241
+ throw new Error('Invalid login state. Start sign-in again.');
5242
+ }
5243
+ if (state.startsWith('bounded.popup.')) {
5244
+ await handOffPopup(url, state);
5245
+ return { handled: true, user: null };
5246
+ }
5247
+ const raw = sessionStorage.getItem(pkceKey(state));
5248
+ if (!raw)
5249
+ throw new Error('Missing login attempt. Start sign-in again in this tab.');
5250
+ const pkce = JSON.parse(raw);
5251
+ if (pkce.state !== state)
5252
+ throw new Error('OAuth state mismatch. Start sign-in again.');
5253
+ await validateAttempt(pkce, url);
5254
+ const error = url.searchParams.get('error');
5255
+ if (error)
5256
+ throw new Error(url.searchParams.get('error_description') || `Login failed: ${error}`);
5257
+ const user = await exchangeCode(url.searchParams.get('code'), pkce);
5258
+ return { handled: true, user, idToken: sessionManager().getIdToken() };
5259
+ }
5260
+ finally {
5261
+ removeAttempt(state);
5262
+ const clean = cleanCallbackUrl(url);
5263
+ if (window.location.href === url.href) {
5264
+ window.history.replaceState(window.history.state, document.title, clean.pathname + clean.search + clean.hash);
5265
+ }
5266
+ }
5267
+ }
5268
+ /** Internal initialization step. Unrelated OAuth callbacks are left untouched. */
5269
+ function completePendingLogin() {
5270
+ if (!hasDOM$2())
5271
+ return Promise.resolve({ handled: false, user: null });
5272
+ const url = new URL(window.location.href);
5273
+ const state = url.searchParams.get('state');
5274
+ if (!(state === null || state === void 0 ? void 0 : state.startsWith(STATE_PREFIX)) || (!url.searchParams.get('code') && !url.searchParams.get('error'))) {
5275
+ return Promise.resolve({ handled: false, user: null });
5276
+ }
5277
+ const generation = getInitializationGeneration();
5278
+ if ((pendingCompletion === null || pendingCompletion === void 0 ? void 0 : pendingCompletion.href) === url.href && pendingCompletion.generation === generation)
5279
+ return pendingCompletion.promise;
5280
+ const promise = finishSharedLoginReturn(url, state).catch((error) => {
5281
+ loginReturnError = { generation, error: error instanceof Error ? error : new Error(String(error)) };
5282
+ throw loginReturnError.error;
5283
+ }).finally(() => {
5284
+ if ((pendingCompletion === null || pendingCompletion === void 0 ? void 0 : pendingCompletion.promise) === promise)
5285
+ pendingCompletion = undefined;
5286
+ });
5287
+ pendingCompletion = { href: url.href, generation, promise };
5288
+ return promise;
5289
+ }
5290
+ async function finishSharedLoginReturn(url, state) {
5291
+ var _a;
5292
+ var _b;
5293
+ const cfg = await getConfig();
5294
+ const issuer = await issuerBase();
5295
+ if (state.startsWith('bounded.redirect.')) {
5296
+ const raw = sessionStorage.getItem(pkceKey(state));
5297
+ // A second SDK configured for another app/issuer on this page does not own
5298
+ // this return. Leave its URL and proof available to the initiating app.
5299
+ try {
5300
+ const attempt = raw && JSON.parse(raw);
5301
+ if (attempt && (attempt.appId !== cfg.appId || attempt.issuer !== issuer))
5302
+ return { handled: false, user: null };
5303
+ }
5304
+ catch ( /* The owning completion reports malformed attempt data. */_c) { /* The owning completion reports malformed attempt data. */ }
5305
+ }
5306
+ const shared = (_a = (_b = window)[COMPLETIONS]) !== null && _a !== void 0 ? _a : (_b[COMPLETIONS] = new Map());
5307
+ const key = JSON.stringify([url.href, cfg.appId, issuer]);
5308
+ const existing = shared.get(key);
5309
+ if (!existing) {
5310
+ const completion = finishLoginReturn(url, state);
5311
+ shared.set(key, completion);
5312
+ return completion;
5313
+ }
5314
+ const generation = getInitializationGeneration();
5315
+ const result = await existing;
5316
+ if (result.user) {
5317
+ if (!result.idToken || sessionManager().getIdToken() !== result.idToken) {
5318
+ throw new Error('The completed login session has changed. Start sign-in again.');
5319
+ }
5320
+ await adoptSessionProvider({ provider: oidcProviderSingleton, method: 'email', configKey: 'direct:oidc',
5321
+ user: Object.assign(Object.assign({}, result.user), { provider: oidcProviderSingleton }), beforeCommit: () => {
5322
+ if (generation !== getInitializationGeneration())
5323
+ throw new Error('Login was superseded by SDK reinitialization. Start sign-in again.');
5324
+ } });
5325
+ }
5326
+ return result;
5327
+ }
5472
5328
  function normalizeMethod(value) {
5473
5329
  const normalized = String(value || '').trim().toLowerCase();
5474
5330
  return /^[a-z0-9_-]{1,40}$/.test(normalized) ? normalized : null;
@@ -5511,7 +5367,8 @@ function appendHostedLoginHints(params, opts) {
5511
5367
  // native (expo-web-browser) path. The caller decides where the PkceState lives
5512
5368
  // (web → sessionStorage across the full-page redirect; native → in-memory across
5513
5369
  // the awaited browser session).
5514
- async function buildAuthorize(opts) {
5370
+ async function buildAuthorize(opts, mode = 'redirect') {
5371
+ loginReturnError = undefined;
5515
5372
  const cfg = await getConfig();
5516
5373
  if (!cfg.appId)
5517
5374
  throw new Error('appId is not configured (init the SDK with your appId)');
@@ -5520,15 +5377,23 @@ async function buildAuthorize(opts) {
5520
5377
  const redirectUri = opts.redirectUri || (hasDOM$2() ? window.location.origin + window.location.pathname : undefined);
5521
5378
  if (!redirectUri)
5522
5379
  throw new Error('redirectUri is required on React Native — pass the https universal link the app owner registered.');
5380
+ if (hasDOM$2()) {
5381
+ const redirect = new URL(redirectUri);
5382
+ if (redirect.origin !== window.location.origin || redirect.hash || CALLBACK_PARAMS.some((key) => redirect.searchParams.has(key))) {
5383
+ throw new Error('redirectUri must be on this app origin, without a fragment or OAuth callback parameters.');
5384
+ }
5385
+ }
5523
5386
  const { verifier, challenge } = await makePkce();
5524
- const state = randomState();
5387
+ const state = `${STATE_PREFIX}${mode}.${randomState()}`;
5525
5388
  const base = await issuerBase();
5526
5389
  const params = new URLSearchParams({
5527
5390
  client_id: cfg.appId, redirect_uri: redirectUri, response_type: 'code', scope: 'openid profile email',
5528
5391
  code_challenge: challenge, code_challenge_method: 'S256', state,
5529
5392
  });
5530
5393
  appendHostedLoginHints(params, opts);
5531
- return { url: `${base}/api/auth/oauth2/authorize?${params.toString()}`, pkce: { verifier, state, redirectUri } };
5394
+ return { url: `${base}/api/auth/oauth2/authorize?${params.toString()}`, pkce: {
5395
+ verifier, state, redirectUri, appId: cfg.appId, issuer: base, createdAt: Date.now(),
5396
+ } };
5532
5397
  }
5533
5398
  // Are we on React Native (no DOM)? OIDC dispatch uses this to choose the native
5534
5399
  // expo-web-browser driver vs. the web full-page-redirect/popup driver.
@@ -5577,8 +5442,7 @@ function parseCallbackParams(callbackUrl) {
5577
5442
  }
5578
5443
  // Full native login: open the hosted page in the system auth session, await the
5579
5444
  // universal-link callback, verify state, and run the SAME PKCE code→token exchange
5580
- // as web. Returns the signed-in User (native completes inline — there's no separate
5581
- // completeLoginFromRedirect() round-trip the way the web full-page redirect needs).
5445
+ // as web. Returns the signed-in User inline; web redirects complete on the next init().
5582
5446
  async function runNativeLogin(opts) {
5583
5447
  var _a, _b;
5584
5448
  const { url, pkce } = await buildAuthorize(opts);
@@ -5605,7 +5469,7 @@ async function runNativeLogin(opts) {
5605
5469
  if (!returnedState || returnedState !== pkce.state) {
5606
5470
  throw new Error('OAuth state mismatch — possible CSRF; aborting');
5607
5471
  }
5608
- return await exchangeCode(code, pkce.verifier, pkce.redirectUri);
5472
+ return await exchangeCode(code, pkce);
5609
5473
  }
5610
5474
  finally {
5611
5475
  setAuthLoading(false);
@@ -5614,7 +5478,7 @@ async function runNativeLogin(opts) {
5614
5478
  /** Start hosted login (Google / Apple / email / hosted phone). Works on WEB and REACT NATIVE.
5615
5479
  *
5616
5480
  * WEB: navigates the whole page to the hosted Bounded login and resolves (void) as the page
5617
- * unloads; on return to `redirectUri?code=...` you call completeLoginFromRedirect().
5481
+ * unloads; init() completes the return to redirectUri automatically.
5618
5482
  *
5619
5483
  * REACT NATIVE: opens the hosted page via expo-web-browser, awaits the https universal-link
5620
5484
  * callback, runs the PKCE exchange INLINE, and resolves with the signed-in User. No
@@ -5632,63 +5496,32 @@ async function loginWithRedirect(opts) {
5632
5496
  // is per-tab and cleared on close — appropriate for a short-lived auth handshake. Keyed by
5633
5497
  // this attempt's own state so a second concurrent login cannot overwrite it.
5634
5498
  sessionStorage.setItem(pkceKey(pkce.state), JSON.stringify(pkce));
5635
- setAuthLoading(true); // reflect "signing in…" right up to navigation; the return page's
5636
- window.location.href = url; // completeLoginFromRedirect() keeps it true until the user resolves.
5637
- }
5638
- /** Finish login on the redirect_uri page (WEB ONLY): exchange ?code= for a token (PKCE-verified),
5639
- * store the session, and return the User. Idempotent: returns null if there's no ?code= in the URL.
5640
- *
5641
- * On REACT NATIVE this returns null and is a no-op: native login completes INLINE inside
5642
- * loginWithRedirect() (which resolves with the User), so there is no separate redirect
5643
- * page to finish from. Calling it on RN is harmless. */
5644
- async function completeLoginFromRedirect() {
5645
- if (!hasDOM$2())
5646
- return null;
5647
- const url = new URL(window.location.href);
5648
- const code = url.searchParams.get('code');
5649
- const returnedState = url.searchParams.get('state');
5650
- if (!code)
5651
- return null;
5652
- // POPUP path: if we're the redirect_uri page running INSIDE the loginWithPopup() window,
5653
- // hand the code to the opener and close — no separate completeLoginInPopup() call needed.
5654
- // The opener is the same app/origin, so post to our own origin (the opener's message
5655
- // handler checks ev.origin === redirect_uri origin). One call now finishes BOTH flows.
5656
- if (window.opener && window.opener !== window) {
5657
- try {
5658
- window.opener.postMessage({ type: 'bounded-oidc-code', code, state: returnedState }, window.location.origin);
5659
- }
5660
- catch ( /* ignore */_a) { /* ignore */ }
5661
- try {
5662
- window.close();
5663
- }
5664
- catch ( /* ignore */_b) { /* ignore */ }
5665
- return null;
5666
- }
5499
+ // This promise represents dispatch, not authentication on the next page.
5500
+ // Browsers can cancel navigation (for example at beforeunload) without firing
5501
+ // pageshow. Never leave this document's SDK loading after dispatch settles.
5667
5502
  setAuthLoading(true);
5668
5503
  try {
5669
- // Read back the exact record THIS attempt wrote, keyed by the state the issuer
5670
- // returned, so a concurrent login's record is never consumed by mistake.
5671
- const raw = returnedState ? sessionStorage.getItem(pkceKey(returnedState)) : null;
5672
- if (!raw)
5673
- throw new Error('Missing PKCE state — start login with loginWithRedirect()');
5674
- const pkce = JSON.parse(raw);
5675
- if (!returnedState || returnedState !== pkce.state)
5676
- throw new Error('OAuth state mismatch — possible CSRF; aborting');
5677
- const user = await exchangeCode(code, pkce.verifier, pkce.redirectUri);
5678
- sessionStorage.removeItem(pkceKey(returnedState));
5679
- // Strip code/state from the URL so a reload doesn't re-exchange (codes are single-use anyway).
5680
- url.searchParams.delete('code');
5681
- url.searchParams.delete('state');
5682
- window.history.replaceState({}, document.title, url.pathname + url.search + url.hash);
5683
- return user;
5504
+ window.location.href = url;
5505
+ }
5506
+ catch (error) {
5507
+ removeAttempt(pkce.state);
5508
+ throw error;
5684
5509
  }
5685
5510
  finally {
5686
5511
  setAuthLoading(false);
5687
5512
  }
5688
5513
  }
5514
+ /** Optional explicit completion. Normal apps only await init(), which already finishes login.
5515
+ * Concurrent calls share one exchange. Returns null when no Bounded return is present. */
5516
+ async function completeLoginFromRedirect() {
5517
+ const result = await completePendingLogin();
5518
+ const error = getLoginReturnError();
5519
+ if (error)
5520
+ throw error;
5521
+ return result.user;
5522
+ }
5689
5523
  /** Popup variant: open the hosted login in a popup; resolves with the User once the popup's
5690
- * redirect_uri page posts the code back. The redirect_uri page just calls the usual
5691
- * completeLoginFromRedirect() (which auto-detects the popup) — no separate call needed. */
5524
+ * redirect_uri page initializes the SDK, which hands the code back automatically. */
5692
5525
  async function loginWithPopup(opts) {
5693
5526
  if (!hasDOM$2())
5694
5527
  throw new Error('loginWithPopup is web-only — on React Native use loginWithRedirect()');
@@ -5707,7 +5540,7 @@ async function loginWithPopup(opts) {
5707
5540
  throw new Error('Popup blocked — use loginWithRedirect() instead');
5708
5541
  let authorization;
5709
5542
  try {
5710
- authorization = await buildAuthorize(opts);
5543
+ authorization = await buildAuthorize(opts, 'popup');
5711
5544
  }
5712
5545
  catch (error) {
5713
5546
  try {
@@ -5742,6 +5575,7 @@ async function loginWithPopup(opts) {
5742
5575
  popup.location.href = url;
5743
5576
  }
5744
5577
  catch (_d) {
5578
+ removeAttempt(pkce.state);
5745
5579
  try {
5746
5580
  popup.close();
5747
5581
  }
@@ -5757,17 +5591,21 @@ async function loginWithPopup(opts) {
5757
5591
  let settled = false;
5758
5592
  let timer;
5759
5593
  let navigationTimer;
5594
+ let completionTimer;
5760
5595
  const cleanup = () => {
5761
5596
  settled = true;
5762
5597
  clearInterval(timer);
5763
5598
  clearTimeout(navigationTimer);
5599
+ clearTimeout(completionTimer);
5764
5600
  window.removeEventListener('message', onMsg);
5601
+ removeAttempt(pkce.state);
5765
5602
  };
5766
5603
  const onMsg = async (ev) => {
5767
- if (ev.origin !== expectedOrigin)
5768
- return; // only trust the redirect_uri origin
5604
+ if (settled || ev.origin !== expectedOrigin || ev.source !== popup)
5605
+ return;
5769
5606
  const d = ev.data;
5770
- if (!d || d.type !== 'bounded-oidc-code' || typeof d.code !== 'string')
5607
+ if (!d || d.type !== 'bounded-oidc-code' ||
5608
+ (typeof d.code !== 'string' && typeof d.error !== 'string'))
5771
5609
  return;
5772
5610
  // Validate state BEFORE tearing down this listener, against the record THIS
5773
5611
  // attempt captured in the closure (`pkce`), not a shared sessionStorage reread.
@@ -5775,10 +5613,15 @@ async function loginWithPopup(opts) {
5775
5613
  // stay registered for our own code, instead of cleaning up on another's message.
5776
5614
  if (d.state !== pkce.state)
5777
5615
  return;
5616
+ if (d.appId !== pkce.appId || d.issuer !== pkce.issuer)
5617
+ return;
5778
5618
  cleanup();
5779
5619
  try {
5780
- const user = await exchangeCode(d.code, pkce.verifier, pkce.redirectUri);
5781
- sessionStorage.removeItem(pkceKey(pkce.state));
5620
+ await validateAttempt(pkce, new URL(d.callbackUrl));
5621
+ popup.postMessage({ type: 'bounded-oidc-accepted', state: pkce.state }, expectedOrigin);
5622
+ if (d.error)
5623
+ throw new Error(typeof d.errorDescription === 'string' ? d.errorDescription : `Login failed: ${d.error}`);
5624
+ const user = await exchangeCode(d.code, pkce);
5782
5625
  try {
5783
5626
  popup.close();
5784
5627
  }
@@ -5786,10 +5629,22 @@ async function loginWithPopup(opts) {
5786
5629
  resolve(user);
5787
5630
  }
5788
5631
  catch (e) {
5632
+ try {
5633
+ popup.close();
5634
+ }
5635
+ catch ( /* ignore */_b) { /* ignore */ }
5789
5636
  reject(e);
5790
5637
  }
5791
5638
  };
5792
5639
  window.addEventListener('message', onMsg);
5640
+ completionTimer = setTimeout(() => {
5641
+ cleanup();
5642
+ try {
5643
+ popup.close();
5644
+ }
5645
+ catch ( /* ignore */_a) { /* ignore */ }
5646
+ reject(new Error('Login expired or its popup disconnected. Start sign-in again.'));
5647
+ }, LOGIN_TIMEOUT_MS);
5793
5648
  // Some browser/extension combinations can return a real popup handle but
5794
5649
  // strand the window at about:blank instead of navigating to the hosted
5795
5650
  // authorize URL. The old code then waited forever because the popup was
@@ -5847,25 +5702,6 @@ async function loginWithPopup(opts) {
5847
5702
  setAuthLoading(false);
5848
5703
  }
5849
5704
  }
5850
- /** Call this on the redirect_uri page when login was opened via loginWithPopup(): posts the
5851
- * code back to the opener and closes. (For the redirect variant use completeLoginFromRedirect.) */
5852
- function completeLoginInPopup(openerOrigin) {
5853
- if (!hasDOM$2() || !window.opener)
5854
- return;
5855
- const url = new URL(window.location.href);
5856
- const code = url.searchParams.get('code');
5857
- const state = url.searchParams.get('state');
5858
- if (!code)
5859
- return;
5860
- try {
5861
- window.opener.postMessage({ type: 'bounded-oidc-code', code, state }, openerOrigin);
5862
- }
5863
- catch ( /* ignore */_a) { /* ignore */ }
5864
- try {
5865
- window.close();
5866
- }
5867
- catch ( /* ignore */_b) { /* ignore */ }
5868
- }
5869
5705
  // Establish a Bounded session from a server-minted inline-session response (the same
5870
5706
  // { idToken, accessToken, refreshToken } shape as /session/refresh). Used by the
5871
5707
  // Turnkey-native login path (authMode:"turnkey"), which mints the session server-side
@@ -5908,14 +5744,20 @@ async function inlineAuthEndpoint() {
5908
5744
  const cfg = await getConfig();
5909
5745
  return { base: await issuerBase(), appId: cfg.appId };
5910
5746
  }
5911
- async function exchangeCode(code, verifier, redirectUri) {
5912
- const cfg = await getConfig();
5913
- const base = await issuerBase();
5747
+ async function exchangeCode(code, pkce) {
5748
+ const generation = getInitializationGeneration();
5749
+ const beforeCommit = () => {
5750
+ if (generation !== getInitializationGeneration())
5751
+ throw new Error('Login was superseded by SDK reinitialization. Start sign-in again.');
5752
+ };
5753
+ await validateAttempt(pkce);
5754
+ beforeCommit();
5755
+ const base = pkce.issuer;
5914
5756
  // OIDC token endpoint: application/x-www-form-urlencoded (the standard). Public client (no
5915
5757
  // secret) authenticated by PKCE. The code is single-use + short-TTL on the server.
5916
5758
  const body = new URLSearchParams({
5917
- grant_type: 'authorization_code', code, redirect_uri: redirectUri,
5918
- client_id: cfg.appId, code_verifier: verifier,
5759
+ grant_type: 'authorization_code', code, redirect_uri: pkce.redirectUri,
5760
+ client_id: pkce.appId, code_verifier: pkce.verifier,
5919
5761
  });
5920
5762
  let res;
5921
5763
  try {
@@ -5939,10 +5781,20 @@ async function exchangeCode(code, verifier, redirectUri) {
5939
5781
  const accessToken = tokenField(data, 'access_token', 'accessToken') || idToken;
5940
5782
  const refreshToken = boundedRefreshTokenField(data);
5941
5783
  const user = userFromIdToken(idToken);
5942
- await sessionManager().storeSession(String(user.address || ''), accessToken, idToken, refreshToken, base);
5784
+ // Reinitialization while the request was in flight must not install this
5785
+ // app's result under a different app's session manager/configuration.
5786
+ beforeCommit();
5787
+ const rollback = await sessionManager().storeSession(String(user.address || ''), accessToken, idToken, refreshToken, base, beforeCommit);
5943
5788
  // Adopt on EVERY session surface (see establishInlineSession) - hosted
5944
5789
  // popup/redirect completions all land here, so this closes their gap too.
5945
- await adoptSessionProvider({ provider: oidcProviderSingleton, method: 'email', configKey: 'direct:oidc', user });
5790
+ try {
5791
+ await adoptSessionProvider({ provider: oidcProviderSingleton, method: 'email', configKey: 'direct:oidc', user, beforeCommit });
5792
+ }
5793
+ catch (error) {
5794
+ if (typeof rollback === 'function')
5795
+ await rollback();
5796
+ throw error;
5797
+ }
5946
5798
  // OIDC login does establish the issuer's Better Auth browser cookie. Record
5947
5799
  // that fact separately from the provider name so logout clears the cookie
5948
5800
  // and returns through the registered app origin. Stamped AFTER the adoption
@@ -6308,7 +6160,7 @@ async function getAuthProvider(config) {
6308
6160
  // manager as legacy email sessions. Human login should be started
6309
6161
  // with loginWithRedirect(); this provider is kept as the restore and
6310
6162
  // logout surface for those sessions.
6311
- currentAuthProvider = new (await import('./email-auth-provider-BJXeBUwp.mjs')).EmailAuthProvider();
6163
+ currentAuthProvider = new (await import('./email-auth-provider-sA_9RTRl.mjs')).EmailAuthProvider();
6312
6164
  break;
6313
6165
  case "phantom": {
6314
6166
  // First-class Solana WALLET LOGIN (the poof/TaroBase-style "connect
@@ -6350,7 +6202,7 @@ async function getAuthProvider(config) {
6350
6202
  // (bounded-login-widget.ts), so registration never counts as
6351
6203
  // evidence the user has a wallet.
6352
6204
  await ensureSolanaMobileWalletRegistered(config, configuredNetwork);
6353
- const { InjectedWalletProvider } = await import('./injected-wallet-provider-DR8xVudg.mjs');
6205
+ const { InjectedWalletProvider } = await import('./injected-wallet-provider-BDCLkdR_.mjs');
6354
6206
  currentAuthProvider = new InjectedWalletProvider(resolveInjectedWalletConfig(config), rpcUrl, configuredNetwork);
6355
6207
  break;
6356
6208
  }
@@ -6363,7 +6215,7 @@ async function getAuthProvider(config) {
6363
6215
  // Selecting authMethod:'evm-wallet' IS the opt-in — pass evmWalletConfig
6364
6216
  // to point at a specific wallet or set chainId/domain.
6365
6217
  const evmWalletConfig = ((_a = config.evmWalletConfig) !== null && _a !== void 0 ? _a : {});
6366
- const { InjectedEvmWalletProvider } = await import('./injected-evm-wallet-provider-B3Mtkdnu.mjs');
6218
+ const { InjectedEvmWalletProvider } = await import('./injected-evm-wallet-provider-BuDISWSq.mjs');
6367
6219
  currentAuthProvider = new InjectedEvmWalletProvider(evmWalletConfig);
6368
6220
  break;
6369
6221
  }
@@ -6388,7 +6240,7 @@ async function getAuthProvider(config) {
6388
6240
  break;
6389
6241
  case "guest":
6390
6242
  // Anonymous auth: a device-local ed25519 keypair signs the challenge.
6391
- currentAuthProvider = new (await import('./guest-auth-provider-CUo6sgop.mjs')).GuestAuthProvider();
6243
+ currentAuthProvider = new (await import('./guest-auth-provider-1mrlNF2G.mjs')).GuestAuthProvider();
6392
6244
  break;
6393
6245
  }
6394
6246
  if (!currentAuthProvider) {
@@ -6450,7 +6302,7 @@ async function signInAnonymously() {
6450
6302
  // Lazy-load the guest provider so its ed25519/web3 dependencies stay out of
6451
6303
  // the main bundle for data-only apps. Specifier is a static string literal
6452
6304
  // so bundle-splitting can see it.
6453
- const provider = new (await import('./guest-auth-provider-CUo6sgop.mjs')).GuestAuthProvider();
6305
+ const provider = new (await import('./guest-auth-provider-1mrlNF2G.mjs')).GuestAuthProvider();
6454
6306
  setAuthLoading(true); // reflect "signing in…" while the guest keypair session is minted
6455
6307
  // adoptSessionProvider awaits only getConfig() (already resolved here) and then
6456
6308
  // runs every write - provider/method/config/marker/user - in one synchronous
@@ -6621,7 +6473,7 @@ function ensureSharedWalletLoginReady() {
6621
6473
  if (!sharedWalletReady) {
6622
6474
  sharedWalletReady = (async () => {
6623
6475
  await getConfig();
6624
- await import('./injected-wallet-provider-DR8xVudg.mjs');
6476
+ await import('./injected-wallet-provider-BDCLkdR_.mjs');
6625
6477
  })().catch((error) => {
6626
6478
  sharedWalletReady = null;
6627
6479
  throw error;
@@ -6724,7 +6576,7 @@ async function loginWithWallet(options) {
6724
6576
  await ensureSharedWalletLoginReady();
6725
6577
  else
6726
6578
  await ensureWalletLoginReady();
6727
- const { InjectedWalletProvider } = await import('./injected-wallet-provider-DR8xVudg.mjs');
6579
+ const { InjectedWalletProvider } = await import('./injected-wallet-provider-BDCLkdR_.mjs');
6728
6580
  // Keep the RAW wallet provider: the offchain wrapper forwards only a fixed
6729
6581
  // set of methods, so cleanup below must speak to the provider that actually
6730
6582
  // holds the config rather than to whatever is wrapping it.
@@ -6977,13 +6829,14 @@ var index = /*#__PURE__*/Object.freeze({
6977
6829
  WalletConfigError: WalletConfigError,
6978
6830
  clearIncompatibleSession: clearIncompatibleSession,
6979
6831
  completeLoginFromRedirect: completeLoginFromRedirect,
6980
- completeLoginInPopup: completeLoginInPopup,
6832
+ completePendingLogin: completePendingLogin,
6981
6833
  ensureSharedWalletLoginReady: ensureSharedWalletLoginReady,
6982
6834
  ensureWalletLoginReady: ensureWalletLoginReady,
6983
6835
  getAuthProvider: getAuthProvider,
6984
6836
  getConfiguredSolanaNetwork: getConfiguredSolanaNetwork,
6985
6837
  getCurrentAuthMethod: getCurrentAuthMethod,
6986
6838
  getInitConfig: getInitConfig,
6839
+ getLoginReturnError: getLoginReturnError,
6987
6840
  isEmailRequired: isEmailRequired,
6988
6841
  isWalletLoginDisabled: isWalletLoginDisabled,
6989
6842
  isWalletLoginEnabled: isWalletLoginEnabled,
@@ -7060,6 +6913,10 @@ let authLoadingListeners = [];
7060
6913
  let initCompleted = false;
7061
6914
  let isAuthLoading = false;
7062
6915
  let authReadyPromise = null;
6916
+ let initializingConfig = null;
6917
+ let initializationGeneration = 0;
6918
+ /** Internal guard for a login result arriving after the app reconfigured the SDK. */
6919
+ function getInitializationGeneration() { return initializationGeneration; }
7063
6920
  // This file acts as a middleware for the global state of the SDK
7064
6921
  // This mostly involves setting up the AuthProvider and managing the current user
7065
6922
  async function initialize(newConfig) {
@@ -7085,6 +6942,19 @@ async function initialize(newConfig) {
7085
6942
  authProviderInstance = await getAuthProvider(newConfig);
7086
6943
  // Initialize config with auth provider
7087
6944
  await init$1(Object.assign(Object.assign({}, newConfig), { authProvider: authProviderInstance }));
6945
+ // A return establishes its own provider/session, or hands off a popup.
6946
+ // Restoring the old provider afterwards would overwrite the new login.
6947
+ try {
6948
+ if ((await completePendingLogin()).handled) {
6949
+ initCompleted = true;
6950
+ return;
6951
+ }
6952
+ }
6953
+ catch (_a) {
6954
+ // A cancelled/failed return is a login failure, not a broken SDK.
6955
+ // The completion layer keeps it in getLoginReturnError(); restore
6956
+ // any existing session and leave sign-in/readiness usable for retry.
6957
+ }
7088
6958
  // Now that core is initialized, update the auth method on the config
7089
6959
  const authMethod = getCurrentAuthMethod();
7090
6960
  if (authMethod) {
@@ -7116,7 +6986,42 @@ async function initialize(newConfig) {
7116
6986
  setAuthLoading(false);
7117
6987
  }
7118
6988
  }
6989
+ // Compare data options by value, but preserve identity for callbacks and class
6990
+ // instances such as providers. Comparing only app/endpoints would silently drop
6991
+ // a concurrent change to wallet policy or provider configuration.
6992
+ function sameConfigValue(a, b, seen = new WeakMap()) {
6993
+ var _a;
6994
+ if (Object.is(a, b))
6995
+ return true;
6996
+ if (!a || !b || typeof a !== 'object' || typeof b !== 'object')
6997
+ return false;
6998
+ if (Array.isArray(a) !== Array.isArray(b))
6999
+ return false;
7000
+ if (Array.isArray(a) && a.length !== b.length)
7001
+ return false;
7002
+ if (!Array.isArray(a)) {
7003
+ for (const value of [a, b]) {
7004
+ const prototype = Object.getPrototypeOf(value);
7005
+ if (prototype !== null && ((_a = prototype.constructor) === null || _a === void 0 ? void 0 : _a.name) !== 'Object')
7006
+ return false;
7007
+ }
7008
+ }
7009
+ if (seen.has(a))
7010
+ return seen.get(a) === b;
7011
+ seen.set(a, b);
7012
+ const left = Object.keys(a), right = Object.keys(b);
7013
+ return left.length === right.length && left.every((key) => Object.prototype.hasOwnProperty.call(b, key)
7014
+ && sameConfigValue(a[key], b[key], seen));
7015
+ }
7119
7016
  function init(newConfig) {
7017
+ if (initializingConfig) {
7018
+ if (sameConfigValue(initializingConfig, newConfig)) {
7019
+ return authReadyPromise;
7020
+ }
7021
+ return Promise.reject(new Error('SDK initialization is in progress. Await init() before changing its configuration.'));
7022
+ }
7023
+ initializingConfig = Object.assign({}, newConfig);
7024
+ initializationGeneration++;
7120
7025
  let resolveReady;
7121
7026
  let rejectReady;
7122
7027
  const ready = new Promise((resolve, reject) => {
@@ -7127,7 +7032,7 @@ function init(newConfig) {
7127
7032
  // auth-loading listener to call waitForAuthReady() synchronously when init()
7128
7033
  // announces that session restoration has begun.
7129
7034
  authReadyPromise = ready;
7130
- void initialize(newConfig).then(resolveReady, rejectReady);
7035
+ void initialize(initializingConfig).then(() => { initializingConfig = null; resolveReady(); }, (error) => { initializingConfig = null; rejectReady(error); });
7131
7036
  return ready;
7132
7037
  }
7133
7038
  /**
@@ -7854,7 +7759,7 @@ function runOtpFrame(run) {
7854
7759
  // Backpack) via loginWithWallet(), run ENTIRELY on the app page (injected
7855
7760
  // providers only exist there, never inside a cross-origin frame).
7856
7761
  //
7857
- // Redirect stays available as the popup-blocked fallback (loginWithRedirect).
7762
+ // Mobile uses a full-page redirect; desktop keeps a popup with redirect fallback.
7858
7763
  // The same modal is reused for Turnkey wallet signing, so login -> first
7859
7764
  // signature is one continuous surface.
7860
7765
  // Palette + type ladder mirror the hosted auth page (login-page.ts): dark editorial
@@ -8097,6 +8002,16 @@ async function prepareAndOpen(opts, pending) {
8097
8002
  var _a, _b, _c, _d, _e, _f;
8098
8003
  // Imported lazily to avoid a static cycle with the auth index barrel.
8099
8004
  const authApi = await untilCancelled(Promise.resolve().then(function () { return index; }), pending);
8005
+ const hostedLogin = (options) => {
8006
+ if (detectMobile())
8007
+ return authApi.loginWithRedirect(options);
8008
+ // Invoke immediately inside the click handler to retain popup activation.
8009
+ return authApi.loginWithPopup(options).catch((error) => {
8010
+ if (shouldFallbackToRedirect(error))
8011
+ return authApi.loginWithRedirect(options);
8012
+ throw error;
8013
+ });
8014
+ };
8100
8015
  // BEFORE any side effect. Registering the mobile wallet from an empty config
8101
8016
  // would take the chainless mainnet default, and that registration cannot be
8102
8017
  // withdrawn - so opening the widget before init() would pin the page to the
@@ -8190,6 +8105,7 @@ async function prepareAndOpen(opts, pending) {
8190
8105
  // asserts rather than fixing a bug - which is exactly why it is worth keeping.
8191
8106
  pendingOpens.delete(pending);
8192
8107
  return new Promise((resolve, reject) => {
8108
+ var _a;
8193
8109
  // Per-open ownership generation (DS3-0402): a later open bumps the modal's
8194
8110
  // generation, so this open's token stops matching and all of its callbacks
8195
8111
  // (backdrop, success, cancel, and the async view swap) no-op instead of
@@ -8408,23 +8324,20 @@ async function prepareAndOpen(opts, pending) {
8408
8324
  loginHint: input.value,
8409
8325
  redirectUri: opts.redirectUri,
8410
8326
  };
8411
- authApi
8327
+ hostedLogin(emailOpts)
8412
8328
  // A hosted login that RESOLVES minted a real session, so it settles
8413
8329
  // the card even if another lane has since taken it - dropping it
8414
8330
  // would leave the app signed in behind a widget that says it is not.
8415
8331
  // Only the failure path belongs to one attempt's UI.
8416
- .loginWithPopup(emailOpts)
8417
- .then((u) => { settleAttempt(attempt); succeed(u); })
8418
- .catch((err) => {
8419
- // Popup unavailable: same-tab redirect instead. It resolves with
8420
- // nothing when it is about to navigate, so the only thing that
8421
- // comes back here is a REJECTION - which must still reach the
8422
- // handler below, or the lane stays locked with nothing running.
8423
- if (shouldFallbackToRedirect(err)) {
8424
- return authApi.loginWithRedirect(emailOpts).then((u) => { if (u)
8425
- succeed(u); });
8332
+ .then((u) => {
8333
+ if (u) {
8334
+ settleAttempt(attempt);
8335
+ succeed(u);
8336
+ }
8337
+ else if (settleAttempt(attempt)) {
8338
+ submit.disabled = false;
8339
+ submit.textContent = "Continue";
8426
8340
  }
8427
- throw err;
8428
8341
  })
8429
8342
  .catch((err) => {
8430
8343
  if (!settleAttempt(attempt))
@@ -8461,15 +8374,16 @@ async function prepareAndOpen(opts, pending) {
8461
8374
  }
8462
8375
  setBusy(b, "Opening secure sign-in\u2026");
8463
8376
  const socialOpts = { provider: method, redirectUri: opts.redirectUri };
8464
- authApi
8465
- .loginWithPopup(socialOpts)
8466
- .then((u) => { settleAttempt(attempt); succeed(u); })
8467
- .catch((err) => {
8468
- if (shouldFallbackToRedirect(err)) {
8469
- return authApi.loginWithRedirect(socialOpts).then((u) => { if (u)
8470
- succeed(u); });
8377
+ hostedLogin(socialOpts)
8378
+ .then((u) => {
8379
+ if (u) {
8380
+ settleAttempt(attempt);
8381
+ succeed(u);
8382
+ }
8383
+ else if (settleAttempt(attempt)) {
8384
+ b.disabled = false;
8385
+ setLabel(b, method);
8471
8386
  }
8472
- throw err;
8473
8387
  })
8474
8388
  .catch((err) => {
8475
8389
  if (!settleAttempt(attempt))
@@ -8733,7 +8647,7 @@ async function prepareAndOpen(opts, pending) {
8733
8647
  fitPanelToContent(m);
8734
8648
  });
8735
8649
  };
8736
- swap(renderChoose());
8650
+ swap(renderChoose((_a = authApi.getLoginReturnError()) === null || _a === void 0 ? void 0 : _a.message));
8737
8651
  showModal(m);
8738
8652
  });
8739
8653
  }
@@ -11027,5 +10941,5 @@ async function connectServiceSession(ticket, options) {
11027
10941
  }
11028
10942
  }
11029
10943
 
11030
- export { onrampStatus as $, getAuthProvider as A, signInAnonymously as B, loginWithPrivy as C, loginWithWallet as D, ensureWalletLoginReady as E, ensureSharedWalletLoginReady as F, preloadWalletLogin as G, loginWithRedirect as H, completeLoginFromRedirect as I, loginWithPopup as J, completeLoginInPopup as K, openBoundedWidget as L, ensureSignedIn as M, useAuth as N, useQuery as O, useBuildRun as P, apps as Q, signSolanaMessageViaTurnkey as R, getOrCreateTurnkeyWallet as S, openTurnkeyKeyExport as T, getTurnkeyWalletSessionState as U, getTurnkeyWalletRecoveryUrl as V, WalletConfigError as W, startTurnkeyEmailLogin as X, getIdToken as Y, onramp as Z, onrampQuote as _, signMessageWithTurnkey as a, setPlatform as a0, OffchainAuthProvider as a1, PrivyExpoProvider as a2, requestEip6963Providers as a3, connectBroadcast as a4, connectServiceSession as a5, builds as a6, BuildsApiError as a7, mediaAgentTools as a8, NON_TERMINAL_STATES as a9, TERMINAL_STATES as aa, RUN_STATES as ab, PARK_REASONS as ac, FAILURE_REASONS as ad, BUILD_REVIEW_CAPABILITY_SCHEMA as ae, BUILD_REVIEW_CAPABILITY_REFRESH_SCHEMA as af, BUILD_DELEGATED_REVIEW_CAPABILITY_SCHEMA as ag, signTransactionWithTurnkey as b, signTransactionV1WithTurnkey as c, signAndSubmitTransactionWithTurnkey as d, bs58 as e, defaultInjectedProvider as f, getPlatform as g, hasTurnkeyWallet as h, firstStandardWalletProvider as i, setAuthLoading as j, bufferExports as k, setStoredAuthMethod as l, confirmAndCheckTransaction as m, normalizeSolanaRpcUrl as n, defaultInjectedEvmProvider as o, init as p, getCurrentUser as q, resolveSolanaRpcUrl as r, setCurrentUser as s, transactionHasSignature as t, onAuthStateChanged as u, onAuthLoadingChanged as v, getAuthLoading as w, waitForAuthReady as x, login as y, logout as z };
11031
- //# sourceMappingURL=index-CaZx7sd8.mjs.map
10944
+ export { onrampStatus as $, getAuthProvider as A, signInAnonymously as B, loginWithPrivy as C, loginWithWallet as D, ensureWalletLoginReady as E, ensureSharedWalletLoginReady as F, preloadWalletLogin as G, loginWithRedirect as H, completeLoginFromRedirect as I, getLoginReturnError as J, loginWithPopup as K, openBoundedWidget as L, ensureSignedIn as M, useAuth as N, useQuery as O, useBuildRun as P, apps as Q, signSolanaMessageViaTurnkey as R, getOrCreateTurnkeyWallet as S, openTurnkeyKeyExport as T, getTurnkeyWalletSessionState as U, getTurnkeyWalletRecoveryUrl as V, WalletConfigError as W, startTurnkeyEmailLogin as X, getIdToken as Y, onramp as Z, onrampQuote as _, signMessageWithTurnkey as a, setPlatform as a0, OffchainAuthProvider as a1, PrivyExpoProvider as a2, requestEip6963Providers as a3, connectBroadcast as a4, connectServiceSession as a5, builds as a6, BuildsApiError as a7, mediaAgentTools as a8, NON_TERMINAL_STATES as a9, TERMINAL_STATES as aa, RUN_STATES as ab, PARK_REASONS as ac, FAILURE_REASONS as ad, BUILD_REVIEW_CAPABILITY_SCHEMA as ae, BUILD_REVIEW_CAPABILITY_REFRESH_SCHEMA as af, BUILD_DELEGATED_REVIEW_CAPABILITY_SCHEMA as ag, signTransactionWithTurnkey as b, signTransactionV1WithTurnkey as c, signAndSubmitTransactionWithTurnkey as d, bs58 as e, defaultInjectedProvider as f, getPlatform as g, hasTurnkeyWallet as h, firstStandardWalletProvider as i, setAuthLoading as j, bufferExports as k, setStoredAuthMethod as l, confirmAndCheckTransaction as m, normalizeSolanaRpcUrl as n, defaultInjectedEvmProvider as o, init as p, getCurrentUser as q, resolveSolanaRpcUrl as r, setCurrentUser as s, transactionHasSignature as t, onAuthStateChanged as u, onAuthLoadingChanged as v, getAuthLoading as w, waitForAuthReady as x, login as y, logout as z };
10945
+ //# sourceMappingURL=index-Bu6vnwxM.mjs.map