@arcblock/did-connect-service 4.1.61 → 4.1.63

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 (72) hide show
  1. package/dist/_generated/asset-bytes.js +1 -1
  2. package/dist/_generated/asset-manifest.js +2 -2
  3. package/dist/assets/{admin-core.bcfa7ce2.js → admin-core.bbd8d53c.js} +963 -194
  4. package/dist/assets/{admin.bd6c819b.css → admin.7b66e78d.css} +160 -7
  5. package/dist/blocklet-service-rpc.js +4 -4
  6. package/dist/blocklet-service-rpc.js.map +1 -1
  7. package/dist/handler-errors.d.ts +14 -8
  8. package/dist/handler-errors.d.ts.map +1 -1
  9. package/dist/handler-errors.js +15 -8
  10. package/dist/handler-errors.js.map +1 -1
  11. package/dist/handlers/account-merge-handler.d.ts +32 -5
  12. package/dist/handlers/account-merge-handler.d.ts.map +1 -1
  13. package/dist/handlers/account-merge-handler.js +155 -19
  14. package/dist/handlers/account-merge-handler.js.map +1 -1
  15. package/dist/handlers/auth-handler.d.ts.map +1 -1
  16. package/dist/handlers/auth-handler.js +11 -6
  17. package/dist/handlers/auth-handler.js.map +1 -1
  18. package/dist/handlers/did-connect-handler.d.ts +2 -2
  19. package/dist/handlers/did-connect-handler.d.ts.map +1 -1
  20. package/dist/handlers/did-connect-handler.js +9 -14
  21. package/dist/handlers/did-connect-handler.js.map +1 -1
  22. package/dist/handlers/email-login-handler.d.ts.map +1 -1
  23. package/dist/handlers/email-login-handler.js +15 -10
  24. package/dist/handlers/email-login-handler.js.map +1 -1
  25. package/dist/handlers/oauth-handler.d.ts +2 -0
  26. package/dist/handlers/oauth-handler.d.ts.map +1 -1
  27. package/dist/handlers/oauth-handler.js +71 -22
  28. package/dist/handlers/oauth-handler.js.map +1 -1
  29. package/dist/handlers/passkey-handler.d.ts +8 -2
  30. package/dist/handlers/passkey-handler.d.ts.map +1 -1
  31. package/dist/handlers/passkey-handler.js +27 -18
  32. package/dist/handlers/passkey-handler.js.map +1 -1
  33. package/dist/handlers/team-handler.js +2 -2
  34. package/dist/handlers/team-handler.js.map +1 -1
  35. package/dist/http/security-headers.d.ts +38 -0
  36. package/dist/http/security-headers.d.ts.map +1 -0
  37. package/dist/http/security-headers.js +67 -0
  38. package/dist/http/security-headers.js.map +1 -0
  39. package/dist/identity/session-epoch.d.ts +11 -0
  40. package/dist/identity/session-epoch.d.ts.map +1 -1
  41. package/dist/identity/session-epoch.js +12 -0
  42. package/dist/identity/session-epoch.js.map +1 -1
  43. package/dist/pages/admin/i18n.d.ts.map +1 -1
  44. package/dist/pages/admin/i18n.js +271 -47
  45. package/dist/pages/admin/i18n.js.map +1 -1
  46. package/dist/pages/admin/merge-confirm-view.d.ts +75 -18
  47. package/dist/pages/admin/merge-confirm-view.d.ts.map +1 -1
  48. package/dist/pages/admin/merge-confirm-view.js +304 -72
  49. package/dist/pages/admin/merge-confirm-view.js.map +1 -1
  50. package/dist/pages/admin/merge-method-picker.d.ts +34 -3
  51. package/dist/pages/admin/merge-method-picker.d.ts.map +1 -1
  52. package/dist/pages/admin/merge-method-picker.js +188 -14
  53. package/dist/pages/admin/merge-method-picker.js.map +1 -1
  54. package/dist/pages/admin/styles.d.ts.map +1 -1
  55. package/dist/pages/admin/styles.js +160 -7
  56. package/dist/pages/admin/styles.js.map +1 -1
  57. package/dist/pages/admin/tab-profile-accounts.d.ts.map +1 -1
  58. package/dist/pages/admin/tab-profile-accounts.js +490 -114
  59. package/dist/pages/admin/tab-profile-accounts.js.map +1 -1
  60. package/dist/pages/oauth-callback-page.d.ts +14 -0
  61. package/dist/pages/oauth-callback-page.d.ts.map +1 -1
  62. package/dist/pages/oauth-callback-page.js +35 -3
  63. package/dist/pages/oauth-callback-page.js.map +1 -1
  64. package/dist/store/account-merge.d.ts +139 -3
  65. package/dist/store/account-merge.d.ts.map +1 -1
  66. package/dist/store/account-merge.js +381 -11
  67. package/dist/store/account-merge.js.map +1 -1
  68. package/dist/store/d1-store.d.ts +20 -3
  69. package/dist/store/d1-store.d.ts.map +1 -1
  70. package/dist/store/d1-store.js +37 -26
  71. package/dist/store/d1-store.js.map +1 -1
  72. package/package.json +3 -3
@@ -3,8 +3,8 @@
3
3
  * Grouped chip layout: bound accounts as chips, unbound as dashed add-chips.
4
4
  */
5
5
  import { ICON_DID_WALLET, ICON_EMAIL, ICON_PASSKEY, OAUTH_ICONS, } from "@arcblock/did-connect-core/ui";
6
- import { renderMergeConfirmBodyHtml, toMergeConfirmViewModel } from "./merge-confirm-view.js";
7
- import { renderMergeMethodPickerHtml } from "./merge-method-picker.js";
6
+ import { renderMergeConfirmBodyHtml, renderMergeDoneBodyHtml, toMergeConfirmViewModel, } from "./merge-confirm-view.js";
7
+ import { formatMergeCandidateWhen, partitionMergeMethods, renderMergeMethodPickerHtml, renderMergePromptHtml, } from "./merge-method-picker.js";
8
8
  /** Providers hidden from the Connected Accounts UI. */
9
9
  const HIDDEN_PROVIDERS = new Set(["facebook", "auth0-legacy"]);
10
10
  /** Build a JS-safe icon map to embed in inline <script>. */
@@ -40,11 +40,16 @@ var __connectedAccounts = [];
40
40
  var __oauthProviders = [];
41
41
  var __accountMergeOAuthCleanup = null;
42
42
  var __mergePromptDid = "";
43
+ var __mergePromptCandidates = [];
43
44
  var __providerIcons = ${buildIconMapJSON()};
44
45
  var __hiddenProviders = ${JSON.stringify([...HIDDEN_PROVIDERS])};
45
46
  ${toMergeConfirmViewModel.toString()}
46
47
  ${renderMergeConfirmBodyHtml.toString()}
48
+ ${renderMergeDoneBodyHtml.toString()}
47
49
  ${renderMergeMethodPickerHtml.toString()}
50
+ ${formatMergeCandidateWhen.toString()}
51
+ ${partitionMergeMethods.toString()}
52
+ ${renderMergePromptHtml.toString()}
48
53
 
49
54
  async function loadConnectedAccounts() {
50
55
  var card = document.getElementById("connected-accounts-card");
@@ -179,7 +184,7 @@ function renderConnectedAccounts() {
179
184
  html += '<div class="ca-section">';
180
185
  html += '<div class="ca-section-label">' + __t("profile.mergeAccount") + '</div>';
181
186
  html += '<p class="ca-merge-hint" style="margin:0 0 8px;color:var(--text-secondary);font-size:13px;">' + __t("profile.mergeAccountHint") + '</p>';
182
- html += '<button class="btn btn-secondary" onclick="startAccountMerge()">' + __t("profile.mergeChooseMethod") + '</button>';
187
+ html += '<button class="btn btn-secondary" onclick="startAccountMerge()">' + __t("profile.mergeStart") + '</button>';
183
188
  html += '</div>';
184
189
 
185
190
  list.innerHTML = html;
@@ -510,17 +515,29 @@ function cleanupAccountMergeOAuth() {
510
515
  if (__accountMergeOAuthCleanup) __accountMergeOAuthCleanup();
511
516
  }
512
517
 
518
+ function mergeChallengeLooksExpired(error) {
519
+ return error === "Invalid or expired merge challenge"
520
+ || error === "合并验证无效或已过期"
521
+ || error === "合併驗證無效或已過期"
522
+ || error === "統合チャレンジが無効か、期限切れです";
523
+ }
524
+
525
+ function emailCodeAlreadySent(error) {
526
+ return error === "Code already sent, please wait"
527
+ || error === "验证码已发送,请稍候"
528
+ || error === "驗證碼已寄出,請稍候"
529
+ || error === "確認コードは送信済みです。しばらくお待ちください";
530
+ }
531
+
513
532
  async function proveAccountMergeWithWallet(mergeChallenge) {
514
533
  var tokenData;
515
534
  try {
516
- tokenData = await api("GET", "/../did/merge-proof/token");
535
+ tokenData = await api("GET", "/../did/merge-proof/token", undefined, { silent: true });
517
536
  } catch (e) {
518
- showToast(__t("profile.mergeFailed"), "error");
519
- return null;
537
+ return { ok: false, error: __t("profile.mergeFailed") };
520
538
  }
521
539
  if (!tokenData || !tokenData.token) {
522
- showToast(__t("profile.mergeFailed"), "error");
523
- return null;
540
+ return { ok: false, error: (tokenData && tokenData.error) || __t("profile.mergeFailed") };
524
541
  }
525
542
 
526
543
  var token = tokenData.token;
@@ -596,17 +613,16 @@ async function proveAccountMergeWithWallet(mergeChallenge) {
596
613
  var proved = await api("POST", "/../did/connect/complete", {
597
614
  token: token,
598
615
  mergeChallenge: mergeChallenge,
599
- });
616
+ }, { silent: true });
600
617
  if (!proved || !proved.mergeIntent) {
601
- statusEl.textContent = (proved && proved.error) || __t("profile.mergeFailed");
602
- polling = false;
618
+ finish(proved || { ok: false, error: __t("profile.mergeFailed") });
603
619
  return;
604
620
  }
605
621
  finish(proved);
606
622
  } else if (status.status === "error") {
607
623
  clearInterval(pollTimer);
608
624
  pollTimer = null;
609
- statusEl.textContent = status.error || __t("profile.mergeFailed");
625
+ finish({ ok: false, error: status.error || __t("profile.mergeFailed") });
610
626
  }
611
627
  } catch (e) { /* ignore poll errors */ }
612
628
  polling = false;
@@ -616,14 +632,12 @@ async function proveAccountMergeWithWallet(mergeChallenge) {
616
632
 
617
633
  async function proveAccountMergeWithPasskey(mergeChallenge) {
618
634
  if (typeof PublicKeyCredential === "undefined" || !navigator.credentials || !navigator.credentials.get) {
619
- showToast(__t("profile.mergePasskeyUnavailable"), "error");
620
- return null;
635
+ return { ok: false, error: __t("profile.mergePasskeyUnavailable") };
621
636
  }
622
- var opts = await api("GET", "/../passkey/auth");
637
+ var opts = await api("GET", "/../passkey/auth", undefined, { silent: true });
623
638
  var publicKey = opts.authentication || opts;
624
- if (!opts.challengeId || !publicKey || !publicKey.challenge) {
625
- showToast((opts && opts.error) || __t("profile.mergeFailed"), "error");
626
- return null;
639
+ if (!opts || opts.ok === false || !opts.challengeId || !publicKey || !publicKey.challenge) {
640
+ return { ok: false, error: (opts && opts.error) || __t("profile.mergeFailed") };
627
641
  }
628
642
  publicKey.challenge = base64urlToBuffer(publicKey.challenge);
629
643
  if (publicKey.allowCredentials) {
@@ -636,7 +650,7 @@ async function proveAccountMergeWithPasskey(mergeChallenge) {
636
650
  credential = await navigator.credentials.get({ publicKey: publicKey });
637
651
  } catch (e) {
638
652
  if (!e || e.name !== "NotAllowedError") {
639
- showToast((e && e.message) || __t("profile.mergeFailed"), "error");
653
+ return { ok: false, error: (e && e.message) || __t("profile.mergeFailed") };
640
654
  }
641
655
  return null;
642
656
  }
@@ -656,7 +670,7 @@ async function proveAccountMergeWithPasskey(mergeChallenge) {
656
670
  },
657
671
  },
658
672
  mergeChallenge: mergeChallenge,
659
- });
673
+ }, { silent: true });
660
674
  }
661
675
 
662
676
  function proveAccountMergeWithOAuth(provider, mergeChallenge) {
@@ -666,15 +680,23 @@ function proveAccountMergeWithOAuth(provider, mergeChallenge) {
666
680
  return;
667
681
  }
668
682
  cleanupAccountMergeOAuth();
683
+ var mergeResultPrefix = "connect.mergeOAuthResult.";
684
+ try {
685
+ var staleKeys = [];
686
+ for (var s = 0; s < localStorage.length; s++) {
687
+ var staleKey = localStorage.key(s);
688
+ if (staleKey && staleKey.indexOf(mergeResultPrefix) === 0) staleKeys.push(staleKey);
689
+ }
690
+ for (var t = 0; t < staleKeys.length; t++) localStorage.removeItem(staleKeys[t]);
691
+ } catch (e0) {}
669
692
  var returnUrl = encodeURIComponent(window.location.href);
670
- var url = window.__BASE_PATH + "/../oauth/" + encodeURIComponent(provider) + "/login?returnUrl=" + returnUrl;
693
+ var url = window.__BASE_PATH + "/../oauth/" + encodeURIComponent(provider) + "/login?returnUrl=" + returnUrl + "&mergeChallenge=" + encodeURIComponent(mergeChallenge);
671
694
  var redirectUri = location.origin + "/.well-known/service/oauth/callback/" + encodeURIComponent(provider);
672
695
  var w = 500, h = 600;
673
696
  var left = (screen.width - w) / 2, top = (screen.height - h) / 2;
674
697
  var popup = window.open(url, "oauth_merge", "width=" + w + ",height=" + h + ",left=" + left + ",top=" + top);
675
698
  if (!popup) {
676
- showToast(__t("common.popupBlocked"), "error");
677
- resolve(null);
699
+ resolve({ ok: false, error: __t("common.popupBlocked") });
678
700
  return;
679
701
  }
680
702
  var settled = false;
@@ -685,6 +707,7 @@ function proveAccountMergeWithOAuth(provider, mergeChallenge) {
685
707
  }, 5 * 60 * 1000);
686
708
  function cleanup() {
687
709
  window.removeEventListener("message", onMessage);
710
+ window.removeEventListener("storage", onStorage);
688
711
  clearTimeout(abandonTimer);
689
712
  if (__accountMergeOAuthCleanup === cancel) __accountMergeOAuthCleanup = null;
690
713
  }
@@ -694,6 +717,37 @@ function proveAccountMergeWithOAuth(provider, mergeChallenge) {
694
717
  cleanup();
695
718
  resolve(value);
696
719
  }
720
+ function parseMergeOAuthResult(raw) {
721
+ if (!raw) return null;
722
+ try {
723
+ var parsed = JSON.parse(raw);
724
+ if (!parsed || typeof parsed !== "object") return { ok: false, error: __t("profile.mergeFailed") };
725
+ return parsed;
726
+ } catch (e1) {
727
+ return { ok: false, error: __t("profile.mergeFailed") };
728
+ }
729
+ }
730
+ function readMergeOAuthResultOnce() {
731
+ try {
732
+ for (var i = 0; i < localStorage.length; i++) {
733
+ var key = localStorage.key(i);
734
+ if (!key || key.indexOf(mergeResultPrefix) !== 0) continue;
735
+ var raw = localStorage.getItem(key);
736
+ try { localStorage.removeItem(key); } catch (e2) {}
737
+ return parseMergeOAuthResult(raw);
738
+ }
739
+ } catch (e3) {}
740
+ return null;
741
+ }
742
+ function onStorage(event) {
743
+ if (settled || !event || !event.key || event.key.indexOf(mergeResultPrefix) !== 0) return;
744
+ var raw = event.newValue;
745
+ try { localStorage.removeItem(event.key); } catch (e4) {}
746
+ var payload = parseMergeOAuthResult(raw);
747
+ if (!payload) return;
748
+ try { popup.close(); } catch (e5) {}
749
+ finish(payload);
750
+ }
697
751
  async function onMessage(event) {
698
752
  if (event.origin !== location.origin) return;
699
753
  if (!event.data || event.data.type !== "authorization_response") return;
@@ -711,23 +765,28 @@ function proveAccountMergeWithOAuth(provider, mergeChallenge) {
711
765
  code: resp.code,
712
766
  redirectUri: redirectUri,
713
767
  mergeChallenge: mergeChallenge,
714
- });
768
+ }, { silent: true });
715
769
  finish(proved);
716
770
  } catch (e) {
717
- showToast((e && e.message) || __t("profile.mergeFailed"), "error");
718
- finish(null);
771
+ finish({ ok: false, error: (e && e.message) || __t("profile.mergeFailed") });
719
772
  }
720
773
  }
721
774
  window.addEventListener("message", onMessage);
775
+ window.addEventListener("storage", onStorage);
722
776
  function cancel() {
723
777
  try { popup.close(); } catch (e) {}
724
778
  finish(null);
725
779
  }
726
780
  __accountMergeOAuthCleanup = cancel;
781
+ var existing = readMergeOAuthResultOnce();
782
+ if (existing) {
783
+ try { popup.close(); } catch (e6) {}
784
+ finish(existing);
785
+ }
727
786
  });
728
787
  }
729
788
 
730
- async function proveAccountMergeWithEmail(mergeChallenge) {
789
+ async function proveAccountMergeWithEmail(mergeChallenge, prefillEmail) {
731
790
  return new Promise(function(resolve) {
732
791
  var d = document.getElementById("email-merge-dialog");
733
792
  if (!d) {
@@ -744,6 +803,7 @@ async function proveAccountMergeWithEmail(mergeChallenge) {
744
803
  + '</div></div>'
745
804
  + '<div class="dialog-actions">'
746
805
  + '<button type="button" class="btn btn-secondary" id="email-merge-cancel">' + __t("common.cancel") + '</button>'
806
+ + '<button type="button" class="btn btn-secondary" id="email-merge-switch">' + __t("profile.mergeEmailSwitchMethod") + '</button>'
747
807
  + '<button type="button" class="btn btn-secondary" id="email-merge-send">' + __t("profile.mergeEmailSendCode") + '</button>'
748
808
  + '<button type="button" class="btn" id="email-merge-ok" disabled>' + __t("common.confirm") + '</button>'
749
809
  + '</div>';
@@ -755,31 +815,86 @@ async function proveAccountMergeWithEmail(mergeChallenge) {
755
815
  var okBtn = document.getElementById("email-merge-ok");
756
816
  var verifyId = null;
757
817
  var settled = false;
818
+ var activeChallenge = mergeChallenge;
819
+ var sentAt = 0;
820
+ var cooldownTimer = null;
821
+ var lastError = "";
758
822
  function finish(value) {
759
823
  if (settled) return;
760
824
  settled = true;
825
+ if (cooldownTimer) clearInterval(cooldownTimer);
826
+ cooldownTimer = null;
761
827
  if (d.open) d.close();
762
828
  resolve(value);
763
829
  }
830
+ function secondsLeft() {
831
+ if (!sentAt) return 0;
832
+ var left = 60000 - (Date.now() - sentAt);
833
+ if (left <= 0) return 0;
834
+ return Math.ceil(left / 1000);
835
+ }
836
+ function paintSendButton() {
837
+ var left = secondsLeft();
838
+ if (left > 0) {
839
+ sendBtn.disabled = true;
840
+ sendBtn.textContent = __t("profile.mergeEmailResendWait", { seconds: String(left) });
841
+ return;
842
+ }
843
+ sendBtn.disabled = false;
844
+ sendBtn.textContent = sentAt ? __t("profile.mergeEmailResend") : __t("profile.mergeEmailSendCode");
845
+ }
846
+ function startCooldown() {
847
+ if (cooldownTimer) clearInterval(cooldownTimer);
848
+ paintSendButton();
849
+ cooldownTimer = setInterval(function() {
850
+ paintSendButton();
851
+ if (secondsLeft() <= 0 && cooldownTimer) {
852
+ clearInterval(cooldownTimer);
853
+ cooldownTimer = null;
854
+ }
855
+ }, 1000);
856
+ }
857
+ function markCodeConsumed(message) {
858
+ codeEl.value = "";
859
+ codeEl.disabled = true;
860
+ okBtn.disabled = true;
861
+ lastError = message || "";
862
+ statusEl.textContent = lastError
863
+ ? lastError + " " + __t("profile.mergeEmailRequestNewCode")
864
+ : __t("profile.mergeEmailRequestNewCode");
865
+ if (secondsLeft() > 0) startCooldown();
866
+ else paintSendButton();
867
+ }
764
868
  document.getElementById("email-merge-cancel").onclick = function() { finish(null); };
869
+ document.getElementById("email-merge-switch").onclick = function() {
870
+ finish({ switchMethod: true, error: lastError, challenge: activeChallenge });
871
+ };
765
872
  d.onclose = function() { finish(null); };
766
873
  sendBtn.onclick = async function() {
767
874
  var email = (emailEl.value || "").trim();
768
875
  if (!email || email.indexOf("@") === -1) return;
769
876
  sendBtn.disabled = true;
770
877
  statusEl.textContent = __t("profile.emailSending");
771
- var sent = await api("POST", "/../email/sendCode", { email: email });
878
+ var sent = await api("POST", "/../email/sendCode", { email: email }, { silent: true });
772
879
  if (!sent || sent.id == null) {
773
- statusEl.textContent = (sent && sent.error) || __t("profile.emailSendFailed");
774
- sendBtn.disabled = false;
880
+ var sendError = (sent && sent.error) || __t("profile.emailSendFailed");
881
+ statusEl.textContent = sendError;
882
+ if (emailCodeAlreadySent(sendError)) {
883
+ if (!sentAt) sentAt = Date.now();
884
+ startCooldown();
885
+ } else {
886
+ sendBtn.disabled = false;
887
+ }
775
888
  return;
776
889
  }
777
890
  verifyId = sent.id;
891
+ sentAt = Date.now();
892
+ codeEl.value = "";
778
893
  codeEl.disabled = false;
779
894
  okBtn.disabled = false;
780
895
  codeEl.focus();
781
896
  statusEl.textContent = __t("profile.emailCodeSent");
782
- sendBtn.disabled = false;
897
+ startCooldown();
783
898
  };
784
899
  okBtn.onclick = async function() {
785
900
  var code = (codeEl.value || "").trim();
@@ -788,39 +903,58 @@ async function proveAccountMergeWithEmail(mergeChallenge) {
788
903
  var proved = await api("POST", "/../email/login", {
789
904
  code: code,
790
905
  id: verifyId,
791
- mergeChallenge: mergeChallenge,
792
- });
793
- if (!proved || !proved.mergeIntent) {
794
- statusEl.textContent = (proved && proved.error) || __t("profile.mergeFailed");
795
- okBtn.disabled = false;
906
+ mergeChallenge: activeChallenge,
907
+ }, { silent: true });
908
+ if (proved && proved.mergeIntent) {
909
+ finish(proved);
796
910
  return;
797
911
  }
798
- finish(proved);
912
+ var err = (proved && proved.error) || __t("profile.mergeFailed");
913
+ if (mergeChallengeLooksExpired(err)) {
914
+ var restarted = await api("POST", "/../account/merge/start", {}, { silent: true });
915
+ if (restarted && restarted.challenge) activeChallenge = restarted.challenge;
916
+ }
917
+ markCodeConsumed(err);
799
918
  };
919
+ if (typeof prefillEmail === "string" && prefillEmail) emailEl.value = prefillEmail;
800
920
  d.showModal();
801
921
  emailEl.focus();
802
922
  });
803
923
  }
804
924
 
805
- async function finalizeAccountMerge(proved) {
925
+ async function finalizeAccountMerge(proved, candidate) {
806
926
  if (!proved || !proved.mergeIntent) {
807
927
  showToast((proved && proved.error) || __t("profile.mergeFailed"), "error");
808
928
  return false;
809
929
  }
810
930
  var plan = await api("POST", "/../account/merge/dry-run", { mergeIntent: proved.mergeIntent });
811
- var ok = await confirmMergePlan(plan);
812
- if (!ok) {
931
+ if (!plan || plan.ok === false || !plan.accounts) return false;
932
+ // Server omits keep → session. The first paint must match recommendedKeep.
933
+ if ((plan.recommendedKeep === "session" || plan.recommendedKeep === "proved") && plan.keep !== plan.recommendedKeep) {
934
+ var next = plan.recommendedKeep;
935
+ var aligned = await api("POST", "/../account/merge/dry-run", { mergeIntent: proved.mergeIntent, keep: next }, { silent: true });
936
+ if (aligned && aligned.ok !== false && aligned.accounts) plan = aligned;
937
+ else showToast((aligned && aligned.error) || __t("profile.mergeFailed"), "error");
938
+ }
939
+ var candidateEmail = candidate && typeof candidate.email === "string" ? candidate.email : "";
940
+ plan.__hasCandidate = !!(candidate && typeof candidate === "object");
941
+ var confirmed = await confirmMergePlan(plan, proved.mergeIntent, candidateEmail);
942
+ if (!confirmed) {
813
943
  showToast(__t("profile.mergeCancelled"), "error");
814
944
  return false;
815
945
  }
816
- var committed = await api("POST", "/../account/merge/commit", { mergeIntent: proved.mergeIntent });
817
- if (committed && committed.ok) {
818
- showToast(__t("profile.mergeSuccess"), "success");
819
- loadConnectedAccounts();
820
- return true;
821
- }
822
- showToast((committed && committed.error) || __t("profile.mergeFailed"), "error");
823
- return false;
946
+ return true;
947
+ }
948
+
949
+ function mergeCandidateWhenLabel(iso) {
950
+ var lang = (document.documentElement && document.documentElement.lang) || "en";
951
+ return formatMergeCandidateWhen(iso, Date.now(), lang, __t);
952
+ }
953
+
954
+ function mergePromptIcon(provider) {
955
+ if (provider && __providerIcons[provider]) return getProviderIconSvg(provider);
956
+ var ch = provider ? provider.charAt(0).toUpperCase() : "?";
957
+ return '<span class="ca-merge-prompt-fallback">' + escapeHtml(ch) + '</span>';
824
958
  }
825
959
 
826
960
  function hideMergePrompt() {
@@ -831,10 +965,48 @@ function hideMergePrompt() {
831
965
  el.innerHTML = "";
832
966
  }
833
967
 
834
- function dismissMergePrompt() {
968
+ function paintMergePrompt(rows, hiddenProviders) {
969
+ var el = document.getElementById("merge-candidate-prompt");
970
+ if (!el) return;
971
+ if (!rows || !rows.length) {
972
+ hideMergePrompt();
973
+ return;
974
+ }
975
+ el.innerHTML = renderMergePromptHtml(rows[0].email || "", rows, __t, escapeHtml, mergePromptIcon, getProviderLabel, mergeCandidateWhenLabel, hiddenProviders || __hiddenProviders);
976
+ el.hidden = false;
977
+ el.style.display = "";
978
+ }
979
+
980
+ function startMergeCandidate(btn) {
981
+ var ref = btn && btn.getAttribute ? btn.getAttribute("data-merge-ref") : "";
982
+ var list = __mergePromptCandidates || [];
983
+ for (var i = 0; i < list.length; i++) {
984
+ if (list[i].ref === ref) {
985
+ startAccountMerge(list[i]);
986
+ return;
987
+ }
988
+ }
989
+ }
990
+
991
+ function hideMergeCandidate(btn) {
992
+ var ref = btn && btn.getAttribute ? btn.getAttribute("data-merge-ref") : "";
993
+ var did = __mergePromptDid || "";
994
+ if (did && ref) {
995
+ try { localStorage["connect.mergePrompt.notMine." + did + "." + ref] = "1"; } catch (e) {}
996
+ }
997
+ var next = [];
998
+ var list = __mergePromptCandidates || [];
999
+ for (var i = 0; i < list.length; i++) {
1000
+ if (list[i].ref !== ref) next.push(list[i]);
1001
+ }
1002
+ __mergePromptCandidates = next;
1003
+ paintMergePrompt(next, __hiddenProviders);
1004
+ }
1005
+
1006
+ function dismissMergePromptLater() {
835
1007
  var did = __mergePromptDid || "";
836
1008
  if (did) {
837
- try { localStorage["connect.mergePrompt.dismissed." + did] = "1"; } catch (e) {}
1009
+ try { sessionStorage["connect.mergePrompt.later." + did] = "1"; } catch (e) {}
838
1010
  }
839
1011
  hideMergePrompt();
840
1012
  }
@@ -842,74 +1014,117 @@ function dismissMergePrompt() {
842
1014
  async function maybeShowMergePrompt(did) {
843
1015
  hideMergePrompt();
844
1016
  __mergePromptDid = did || "";
1017
+ __mergePromptCandidates = [];
845
1018
  if (!did) return;
846
1019
  try {
847
- if (localStorage["connect.mergePrompt.dismissed." + did] === "1") return;
1020
+ if (sessionStorage["connect.mergePrompt.later." + did] === "1") return;
848
1021
  } catch (e) {}
849
1022
  var data = await api("GET", "/../account/merge/candidate", undefined, { silent: true });
850
- if (!data || data.mergeCandidate !== true) return;
1023
+ if (!data || data.mergeCandidate !== true || !data.candidates || !data.candidates.length) return;
851
1024
  try {
852
- if (localStorage["connect.mergePrompt.dismissed." + did] === "1") return;
853
- } catch (e) {}
1025
+ if (sessionStorage["connect.mergePrompt.later." + did] === "1") return;
1026
+ } catch (e2) {}
1027
+ var email = typeof data.email === "string" ? data.email : "";
1028
+ if (!email) return;
1029
+ var visible = [];
1030
+ for (var i = 0; i < data.candidates.length && visible.length < 5; i++) {
1031
+ var c = data.candidates[i];
1032
+ if (!c || typeof c.ref !== "string" || !c.ref) continue;
1033
+ try {
1034
+ if (localStorage["connect.mergePrompt.notMine." + did + "." + c.ref] === "1") continue;
1035
+ } catch (e3) {}
1036
+ visible.push({
1037
+ ref: c.ref,
1038
+ methods: Array.isArray(c.methods) ? c.methods : [],
1039
+ emailLogin: c.emailLogin === true,
1040
+ email: email,
1041
+ createdAt: typeof c.createdAt === "string" ? c.createdAt : "",
1042
+ lastLoginAt: typeof c.lastLoginAt === "string" ? c.lastLoginAt : "",
1043
+ });
1044
+ }
1045
+ if (!visible.length) return;
854
1046
  var el = document.getElementById("merge-candidate-prompt");
855
1047
  if (!el) return;
856
- el.innerHTML = '<div class="ca-merge-prompt-title">' + escapeHtml(__t("profile.mergePromptTitle")) + '</div>'
857
- + '<p class="ca-merge-prompt-body">' + escapeHtml(__t("profile.mergePromptBody")) + '</p>'
858
- + '<div class="ca-merge-prompt-actions">'
859
- + '<button type="button" class="btn" id="merge-candidate-prompt-action" onclick="startAccountMerge()">' + escapeHtml(__t("profile.mergePromptAction")) + '</button>'
860
- + '<button type="button" class="btn btn-secondary" id="merge-candidate-prompt-dismiss" onclick="dismissMergePrompt()">' + escapeHtml(__t("profile.mergePromptDismiss")) + '</button>'
861
- + '</div>';
862
- el.hidden = false;
863
- el.style.display = "";
1048
+ __mergePromptCandidates = visible;
1049
+ paintMergePrompt(visible, __hiddenProviders);
864
1050
  }
865
1051
 
866
- async function startAccountMerge() {
1052
+ async function startAccountMerge(candidate) {
867
1053
  cleanupAccountMergeOAuth();
868
- var start;
869
- try {
870
- start = await api("POST", "/../account/merge/start", {});
871
- } catch (e) {
872
- showToast((e && e.message) || __t("profile.mergeFailed"), "error");
873
- return;
874
- }
875
- if (!start || !start.challenge) {
876
- showToast(__t("profile.mergeFailed"), "error");
877
- return;
1054
+ var guided = null;
1055
+ if (candidate && typeof candidate.ref === "string" && candidate.ref) {
1056
+ guided = partitionMergeMethods(candidate, listMergeMethods(), __hiddenProviders);
878
1057
  }
879
-
880
1058
  var d = document.getElementById("account-merge-method-dialog");
881
1059
  if (!d) {
882
1060
  d = document.createElement("dialog");
883
1061
  d.id = "account-merge-method-dialog";
884
1062
  document.body.appendChild(d);
885
1063
  }
886
- d.innerHTML = renderMergeMethodPickerHtml(listMergeMethods(), __t, escapeHtml, getProviderIconSvg, getProviderLabel);
887
- document.getElementById("account-merge-method-cancel").onclick = function() { d.close(); };
888
- var buttons = d.querySelectorAll("[data-merge-method]");
889
- for (var b = 0; b < buttons.length; b++) {
890
- buttons[b].onclick = async function(event) {
891
- var button = event.currentTarget;
892
- var method = button.getAttribute("data-merge-method");
893
- var selectedProvider = button.getAttribute("data-merge-provider");
894
- d.close();
895
- try {
896
- var proved = null;
897
- if (method === "wallet") proved = await proveAccountMergeWithWallet(start.challenge);
898
- else if (method === "passkey") proved = await proveAccountMergeWithPasskey(start.challenge);
899
- else if (method === "oauth") proved = await proveAccountMergeWithOAuth(selectedProvider, start.challenge);
900
- else if (method === "email") proved = await proveAccountMergeWithEmail(start.challenge);
901
- if (proved) await finalizeAccountMerge(proved);
902
- } catch (e) {
903
- if (!e || e.name !== "NotAllowedError") {
904
- showToast((e && e.message) || __t("profile.mergeFailed"), "error");
1064
+ var challenge = null;
1065
+ function paint(errorText) {
1066
+ d.innerHTML = renderMergeMethodPickerHtml(guided ? [] : listMergeMethods(), __t, escapeHtml, getProviderIconSvg, getProviderLabel, errorText || "", guided);
1067
+ var cancel = document.getElementById("account-merge-method-cancel");
1068
+ if (cancel) cancel.onclick = function() { d.close(); };
1069
+ var buttons = d.querySelectorAll("[data-merge-method]");
1070
+ for (var b = 0; b < buttons.length; b++) {
1071
+ buttons[b].onclick = onPick;
1072
+ }
1073
+ }
1074
+ paint("");
1075
+ d.showModal();
1076
+
1077
+ async function ensureChallenge(force) {
1078
+ if (challenge && !force) return challenge;
1079
+ var start = await api("POST", "/../account/merge/start", {}, { silent: true });
1080
+ if (!start || !start.challenge) {
1081
+ paint((start && start.error) || __t("profile.mergeFailed"));
1082
+ return null;
1083
+ }
1084
+ challenge = start.challenge;
1085
+ return challenge;
1086
+ }
1087
+
1088
+ async function onPick(event) {
1089
+ var button = event.currentTarget;
1090
+ var method = button.getAttribute("data-merge-method");
1091
+ var selectedProvider = button.getAttribute("data-merge-provider");
1092
+ paint("");
1093
+ var active = await ensureChallenge(false);
1094
+ if (!active) return;
1095
+ try {
1096
+ var proved = null;
1097
+ if (method === "wallet") proved = await proveAccountMergeWithWallet(active);
1098
+ else if (method === "passkey") proved = await proveAccountMergeWithPasskey(active);
1099
+ else if (method === "oauth") proved = await proveAccountMergeWithOAuth(selectedProvider, active);
1100
+ else if (method === "email") proved = await proveAccountMergeWithEmail(active, button.getAttribute("data-merge-email") || "");
1101
+ if (proved && proved.switchMethod) {
1102
+ if (typeof proved.challenge === "string" && proved.challenge) challenge = proved.challenge;
1103
+ paint(proved.error || "");
1104
+ return;
1105
+ }
1106
+ if (proved && proved.mergeIntent) {
1107
+ if (d.open) d.close();
1108
+ await finalizeAccountMerge(proved, candidate);
1109
+ return;
1110
+ }
1111
+ if (proved && proved.error) {
1112
+ if (mergeChallengeLooksExpired(proved.error)) {
1113
+ challenge = null;
1114
+ await ensureChallenge(true);
905
1115
  }
1116
+ paint(proved.error);
1117
+ return;
906
1118
  }
907
- };
1119
+ } catch (e) {
1120
+ if (!e || e.name !== "NotAllowedError") {
1121
+ paint((e && e.message) || __t("profile.mergeFailed"));
1122
+ }
1123
+ }
908
1124
  }
909
- d.showModal();
910
1125
  }
911
1126
 
912
- function confirmMergePlan(plan) {
1127
+ function confirmMergePlan(plan, mergeIntent, candidateEmail) {
913
1128
  return new Promise(function(resolve) {
914
1129
  var d = document.getElementById("merge-confirm-dialog");
915
1130
  if (!d) {
@@ -918,32 +1133,193 @@ function confirmMergePlan(plan) {
918
1133
  d.className = "merge-confirm-dialog";
919
1134
  document.body.appendChild(d);
920
1135
  }
921
- d.innerHTML = '<div class="dialog-content">'
922
- + '<h3>' + escapeHtml(__t("profile.mergeConfirmTitle")) + '</h3>'
923
- + renderMergeConfirmBodyHtml(plan, __t, escapeHtml, getProviderLabel)
924
- + '</div>'
925
- + '<div class="dialog-actions">'
926
- + '<button class="btn btn-secondary" id="merge-confirm-cancel">' + escapeHtml(__t("common.cancel")) + '</button>'
927
- + '<button class="btn btn-danger" id="merge-confirm-ok">' + escapeHtml(__t("common.confirm")) + '</button>'
928
- + '</div>';
1136
+ var current = plan || {};
1137
+ var selectedKeep = current.keep === "proved" || current.keep === "session" ? current.keep : "session";
1138
+ // accounts.session.did stays the signed-in DID. intoDid follows keep.
1139
+ var sessionAccount = current.accounts && current.accounts.session;
1140
+ var sessionDid = sessionAccount && typeof sessionAccount.did === "string" ? sessionAccount.did : "";
1141
+ var hasCandidate = current.__hasCandidate === true;
1142
+ var loading = false;
1143
+ var generation = 0;
1144
+ var phase = "preview";
1145
+ var settled = false;
1146
+ var resultPayload = null;
929
1147
 
930
- var okBtn = document.getElementById("merge-confirm-ok");
931
- var cancelBtn = document.getElementById("merge-confirm-cancel");
932
- if (plan && plan.tooLarge) okBtn.disabled = true;
1148
+ function keptDid() {
1149
+ var side = selectedKeep === "proved" ? current.accounts && current.accounts.proved : current.accounts && current.accounts.session;
1150
+ if (side && typeof side.did === "string" && side.did) return side.did;
1151
+ return typeof current.intoDid === "string" ? current.intoDid : "";
1152
+ }
1153
+
1154
+ function submitLabel() {
1155
+ if (selectedKeep === "session") return __t("profile.mergeConfirmSubmitKeep");
1156
+ return __t("profile.mergeConfirmSubmitSwitch", { did: truncateDid(keptDid()) });
1157
+ }
1158
+
1159
+ function paint() {
1160
+ d.innerHTML = '<div class="dialog-content">'
1161
+ + '<h3 id="merge-confirm-title">' + escapeHtml(__t("profile.mergeConfirmTitle")) + '</h3>'
1162
+ + renderMergeConfirmBodyHtml(current, __t, escapeHtml, getProviderLabel, {
1163
+ selectedKeep: selectedKeep,
1164
+ hasCandidate: hasCandidate,
1165
+ candidateEmail: candidateEmail || "",
1166
+ shortDid: truncateDid,
1167
+ loading: loading
1168
+ })
1169
+ + '</div>'
1170
+ + '<div class="dialog-actions">'
1171
+ + '<button type="button" class="btn btn-secondary" id="merge-confirm-cancel">' + escapeHtml(__t("common.cancel")) + '</button>'
1172
+ + '<button type="button" class="btn btn-danger" id="merge-confirm-ok">' + escapeHtml(submitLabel()) + '</button>'
1173
+ + '</div>';
1174
+ var okBtn = document.getElementById("merge-confirm-ok");
1175
+ var cancelBtn = document.getElementById("merge-confirm-cancel");
1176
+ if (loading || current.tooLarge || phase !== "preview") okBtn.disabled = true;
1177
+ cancelBtn.onclick = function() { finish(false); };
1178
+ okBtn.onclick = onOk;
1179
+ var radios = d.querySelectorAll('input[name="merge-keep"]');
1180
+ for (var i = 0; i < radios.length; i++) radios[i].onchange = onKeepChange;
1181
+ var copies = d.querySelectorAll("[data-copy-did]");
1182
+ for (var c = 0; c < copies.length; c++) copies[c].onclick = onCopy;
1183
+ }
1184
+
1185
+ function onCopy(event) {
1186
+ if (event) {
1187
+ if (event.preventDefault) event.preventDefault();
1188
+ if (event.stopPropagation) event.stopPropagation();
1189
+ }
1190
+ var btn = event && event.currentTarget;
1191
+ var full = btn && btn.getAttribute ? btn.getAttribute("data-copy-did") : "";
1192
+ if (full) copyToClipboard(full);
1193
+ }
1194
+
1195
+ function onKeepChange(event) {
1196
+ if (phase !== "preview") return;
1197
+ var input = event && event.currentTarget;
1198
+ var next = input && input.value;
1199
+ if (next !== "session" && next !== "proved") return;
1200
+ if (next === selectedKeep) return;
1201
+ selectedKeep = next;
1202
+ loading = true;
1203
+ var token = ++generation;
1204
+ var okBtn = document.getElementById("merge-confirm-ok");
1205
+ if (okBtn) okBtn.disabled = true;
1206
+ setTimeout(function() {
1207
+ if (token !== generation || !loading || phase !== "preview") return;
1208
+ paint();
1209
+ }, 0);
1210
+ refreshKeep(next, token);
1211
+ }
1212
+
1213
+ async function refreshKeep(next, token) {
1214
+ var refreshed = await api("POST", "/../account/merge/dry-run", { mergeIntent: mergeIntent, keep: next }, { silent: true });
1215
+ if (token !== generation) return;
1216
+ loading = false;
1217
+ if (!refreshed || refreshed.ok === false || !refreshed.accounts) {
1218
+ showToast((refreshed && refreshed.error) || __t("profile.mergeFailed"), "error");
1219
+ selectedKeep = current.keep === "proved" || current.keep === "session" ? current.keep : "session";
1220
+ paint();
1221
+ return;
1222
+ }
1223
+ current = refreshed;
1224
+ selectedKeep = current.keep === "proved" || current.keep === "session" ? current.keep : next;
1225
+ paint();
1226
+ }
1227
+
1228
+ async function onOk() {
1229
+ if (phase !== "preview" || loading || current.tooLarge) return;
1230
+ var okBtn = document.getElementById("merge-confirm-ok");
1231
+ if (!okBtn || okBtn.disabled) return;
1232
+ okBtn.disabled = true;
1233
+ phase = "committing";
1234
+ generation += 1;
1235
+ var committed = await api("POST", "/../account/merge/commit", { mergeIntent: mergeIntent, keep: selectedKeep });
1236
+ if (settled) return;
1237
+ if (!committed || committed.ok === false || !committed.survivorDid) {
1238
+ phase = "preview";
1239
+ if (!d.open) {
1240
+ try { d.showModal(); } catch (err) {}
1241
+ }
1242
+ paint();
1243
+ return;
1244
+ }
1245
+ phase = "done";
1246
+ resultPayload = committed;
1247
+ showDone(committed);
1248
+ }
1249
+
1250
+ function showDone(committed) {
1251
+ var survivorDid = committed && typeof committed.survivorDid === "string" ? committed.survivorDid : "";
1252
+ d.innerHTML = '<div class="dialog-content">'
1253
+ + '<h3 id="merge-confirm-title">' + escapeHtml(__t("profile.mergeDoneTitle")) + '</h3>'
1254
+ + renderMergeDoneBodyHtml({
1255
+ survivorDid: survivorDid,
1256
+ survivorMethods: current.survivorMethods
1257
+ }, __t, escapeHtml, getProviderLabel, truncateDid)
1258
+ + '</div>'
1259
+ + '<div class="dialog-actions">'
1260
+ + '<button type="button" class="btn" id="merge-confirm-finish">' + escapeHtml(__t("profile.mergeDoneFinish")) + '</button>'
1261
+ + '</div>';
1262
+ var finishBtn = document.getElementById("merge-confirm-finish");
1263
+ finishBtn.onclick = function() { finishDone(survivorDid); };
1264
+ if (!d.open) {
1265
+ try { d.showModal(); } catch (err) {}
1266
+ }
1267
+ }
1268
+
1269
+ function finishDone(survivorDid) {
1270
+ if (settled) return;
1271
+ settled = true;
1272
+ d.oncancel = null;
1273
+ d.onclose = null;
1274
+ if (d.open) d.close();
1275
+ if (survivorDid && sessionDid && survivorDid !== sessionDid) location.reload();
1276
+ else loadConnectedAccounts();
1277
+ resolve(true);
1278
+ }
933
1279
 
934
- var settled = false;
935
1280
  function finish(result) {
936
1281
  if (settled) return;
1282
+ if (phase === "done") {
1283
+ finishDone(resultPayload && resultPayload.survivorDid ? resultPayload.survivorDid : "");
1284
+ return;
1285
+ }
1286
+ if (phase === "committing") {
1287
+ if (!d.open) {
1288
+ try { d.showModal(); } catch (err) {}
1289
+ }
1290
+ return;
1291
+ }
937
1292
  settled = true;
938
- okBtn.onclick = null;
939
- cancelBtn.onclick = null;
1293
+ d.oncancel = null;
940
1294
  d.onclose = null;
941
- d.close();
1295
+ if (d.open) d.close();
942
1296
  resolve(result);
943
1297
  }
944
- okBtn.onclick = function() { if (okBtn.disabled) return; finish(true); };
945
- cancelBtn.onclick = function() { finish(false); };
946
- d.onclose = function() { finish(false); };
1298
+
1299
+ d.oncancel = function(event) {
1300
+ if (event && event.preventDefault) event.preventDefault();
1301
+ if (phase === "committing") return;
1302
+ if (phase === "done") {
1303
+ finishDone(resultPayload && resultPayload.survivorDid ? resultPayload.survivorDid : "");
1304
+ return;
1305
+ }
1306
+ finish(false);
1307
+ };
1308
+ d.onclose = function() {
1309
+ if (settled) return;
1310
+ if (phase === "committing") {
1311
+ if (!d.open) {
1312
+ try { d.showModal(); } catch (err) {}
1313
+ }
1314
+ return;
1315
+ }
1316
+ if (phase === "done") {
1317
+ finishDone(resultPayload && resultPayload.survivorDid ? resultPayload.survivorDid : "");
1318
+ return;
1319
+ }
1320
+ finish(false);
1321
+ };
1322
+ paint();
947
1323
  d.showModal();
948
1324
  });
949
1325
  }