@ibgib/web-gib 0.0.61 → 0.0.62

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 (59) hide show
  1. package/dist/AUTO-GENERATED-version.d.mts +1 -1
  2. package/dist/AUTO-GENERATED-version.mjs +1 -1
  3. package/dist/helpers.web.d.mts +3 -1
  4. package/dist/helpers.web.d.mts.map +1 -1
  5. package/dist/helpers.web.mjs +17 -1
  6. package/dist/helpers.web.mjs.map +1 -1
  7. package/dist/identity/device-delegate-helper.d.mts +48 -0
  8. package/dist/identity/device-delegate-helper.d.mts.map +1 -1
  9. package/dist/identity/device-delegate-helper.mjs +86 -2
  10. package/dist/identity/device-delegate-helper.mjs.map +1 -1
  11. package/dist/identity/device-delegate.respec.mjs +6 -6
  12. package/dist/identity/device-delegate.respec.mjs.map +1 -1
  13. package/dist/identity/passkey-helper.d.mts +72 -0
  14. package/dist/identity/passkey-helper.d.mts.map +1 -0
  15. package/dist/identity/passkey-helper.mjs +237 -0
  16. package/dist/identity/passkey-helper.mjs.map +1 -0
  17. package/dist/identity/passkey.respec.d.mts +2 -0
  18. package/dist/identity/passkey.respec.d.mts.map +1 -0
  19. package/dist/identity/passkey.respec.mjs +169 -0
  20. package/dist/identity/passkey.respec.mjs.map +1 -0
  21. package/dist/index.d.mts +1 -0
  22. package/dist/index.d.mts.map +1 -1
  23. package/dist/index.mjs +1 -0
  24. package/dist/index.mjs.map +1 -1
  25. package/dist/ui/component/identity/add-device/add-device.d.mts.map +1 -1
  26. package/dist/ui/component/identity/add-device/add-device.mjs +1 -0
  27. package/dist/ui/component/identity/add-device/add-device.mjs.map +1 -1
  28. package/dist/ui/component/identity/keystone-creator/keystone-creator.d.mts +18 -1
  29. package/dist/ui/component/identity/keystone-creator/keystone-creator.d.mts.map +1 -1
  30. package/dist/ui/component/identity/keystone-creator/keystone-creator.html +42 -3
  31. package/dist/ui/component/identity/keystone-creator/keystone-creator.mjs +510 -74
  32. package/dist/ui/component/identity/keystone-creator/keystone-creator.mjs.map +1 -1
  33. package/dist/ui/component/identity/keystone-details/keystone-details.d.mts +16 -1
  34. package/dist/ui/component/identity/keystone-details/keystone-details.d.mts.map +1 -1
  35. package/dist/ui/component/identity/keystone-details/keystone-details.html +38 -0
  36. package/dist/ui/component/identity/keystone-details/keystone-details.mjs +946 -122
  37. package/dist/ui/component/identity/keystone-details/keystone-details.mjs.map +1 -1
  38. package/dist/ui/component/identity/remove-device/remove-device.d.mts.map +1 -1
  39. package/dist/ui/component/identity/remove-device/remove-device.mjs +1 -0
  40. package/dist/ui/component/identity/remove-device/remove-device.mjs.map +1 -1
  41. package/dist/ui/ui-helpers.d.mts +3 -0
  42. package/dist/ui/ui-helpers.d.mts.map +1 -1
  43. package/dist/ui/ui-helpers.mjs +110 -6
  44. package/dist/ui/ui-helpers.mjs.map +1 -1
  45. package/package.json +2 -2
  46. package/src/AUTO-GENERATED-version.mts +1 -1
  47. package/src/helpers.web.mts +22 -2
  48. package/src/identity/device-delegate-helper.mts +135 -2
  49. package/src/identity/device-delegate.respec.mts +6 -6
  50. package/src/identity/passkey-helper.mts +304 -0
  51. package/src/identity/passkey.respec.mts +194 -0
  52. package/src/index.mts +1 -0
  53. package/src/ui/component/identity/add-device/add-device.mts +1 -0
  54. package/src/ui/component/identity/keystone-creator/keystone-creator.html +42 -3
  55. package/src/ui/component/identity/keystone-creator/keystone-creator.mts +551 -77
  56. package/src/ui/component/identity/keystone-details/keystone-details.html +38 -0
  57. package/src/ui/component/identity/keystone-details/keystone-details.mts +1011 -131
  58. package/src/ui/component/identity/remove-device/remove-device.mts +1 -0
  59. package/src/ui/ui-helpers.mts +118 -5
@@ -1,7 +1,7 @@
1
1
  import thisCss from "./keystone-details.css";
2
2
  import thisHtml from "./keystone-details.html";
3
3
  import commonComponentStylesCss from "../../common-component-styles.css";
4
- import { extractErrorMsg, pretty } from "@ibgib/helper-gib/dist/helpers/utils-helper.mjs";
4
+ import { extractErrorMsg, pretty, getSaferSubstring, HashAlgorithm } from "@ibgib/helper-gib/dist/helpers/utils-helper.mjs";
5
5
  import { getIbAndGib, getIbGibAddr } from "@ibgib/ts-gib/dist/helper.mjs";
6
6
  import { getGibInfo } from "@ibgib/ts-gib/dist/V1/transforms/transform-helper.mjs";
7
7
  import { KeystoneService_V1 } from "@ibgib/core-gib/dist/keystone/keystone-service-v1.mjs";
@@ -10,15 +10,19 @@ import { EVENT_IBGIB_IDENTITY_REQUEST_CHANGE, EVENT_IBGIB_IDENTITY_CHANGED } fro
10
10
  import { getApiBridge } from "../../../../api/ibgib-api-bridge.mjs";
11
11
  import { getKeystoneSyncService } from "../../../../identity/keystone-sync-service.mjs";
12
12
  import { updateSpecialIndex } from "@ibgib/core-gib/dist/timeline/timeline-api.mjs";
13
- import { getTjpAddr, toDto } from "@ibgib/core-gib/dist/common/other/ibgib-helper.mjs";
13
+ import { getTjpAddr, toDto, getTimelinesGroupedByTjp } from "@ibgib/core-gib/dist/common/other/ibgib-helper.mjs";
14
14
  import { parseSpaceIb } from "@ibgib/core-gib/dist/witness/space/space-helper.mjs";
15
15
  import { executeSsoOAuthPopup } from "../../../../identity/sso-popup-helper.mjs";
16
16
  import { getComponentCtorArg } from "../../../../app-bootstrap/init-orchestration.mjs";
17
17
  import { promptForSecret } from "../../../../helpers.web.mjs";
18
- import { isSsoProviderLinked, getKeystoneCapabilities, isDelegateExpired } from "@ibgib/core-gib/dist/keystone/delegate/delegate-helpers.mjs";
18
+ import { isSsoProviderLinked, getKeystoneCapabilities, isDelegateExpired, isCustodialDelegate } from "@ibgib/core-gib/dist/keystone/delegate/delegate-helpers.mjs";
19
19
  import { generatePoolChallenges } from "@ibgib/core-gib/dist/keystone/keystone-helpers.mjs";
20
- import { POOL_ID_MANAGE } from "@ibgib/core-gib/dist/keystone/keystone-constants.mjs";
20
+ import { POOL_ID_MANAGE, KEYSTONE_VERB_SYNC, KEYSTONE_VERB_CONNECT, KEYSTONE_VERB_MANAGE } from "@ibgib/core-gib/dist/keystone/keystone-constants.mjs";
21
+ import { generateDefaultDeviceTag } from "../../../../util/device-helper.mjs";
21
22
  import { getGlobalMetaspace_waitIfNeeded } from "../../../../helpers.mjs";
23
+ import { createSovereignDeviceSyncDelegate, createOAuth2DeviceSyncDelegate } from "../../../../identity/device-delegate-helper.mjs";
24
+ import { isPasskeyPrfSupported, registerPasskeyCredential, evaluatePasskeyPrf, bufferToHex } from "../../../../identity/passkey-helper.mjs";
25
+ import { KeystoneChallengeType, KeystoneReplenishStrategy } from "@ibgib/core-gib/dist/keystone/keystone-types.mjs";
22
26
  const logalot = false;
23
27
  export const KEYSTONE_DETAILS_COMPONENT_NAME = 'ibgib-keystone-details';
24
28
  export class KeystoneDetailsComponentMeta extends IbGibDynamicComponentMetaBase {
@@ -54,6 +58,7 @@ export class KeystoneDetailsComponentInstance extends IbGibDynamicComponentInsta
54
58
  _onIdentityChanged = null;
55
59
  keystoneCache = new Map();
56
60
  aggregatedDetails;
61
+ resendCooldownInterval = null;
57
62
  constructor() {
58
63
  super();
59
64
  }
@@ -77,6 +82,39 @@ export class KeystoneDetailsComponentInstance extends IbGibDynamicComponentInsta
77
82
  }
78
83
  }
79
84
  }
85
+ async disconnected() {
86
+ if (this._onIdentityChanged) {
87
+ window.removeEventListener(EVENT_IBGIB_IDENTITY_CHANGED, this._onIdentityChanged);
88
+ }
89
+ if (this.resendCooldownInterval) {
90
+ clearInterval(this.resendCooldownInterval);
91
+ this.resendCooldownInterval = null;
92
+ }
93
+ }
94
+ startResendCooldownTimer(durationSecs = 30) {
95
+ const btn = this.elements?.btnResendAddPasswordOtpEl;
96
+ if (!btn)
97
+ return;
98
+ if (this.resendCooldownInterval) {
99
+ clearInterval(this.resendCooldownInterval);
100
+ this.resendCooldownInterval = null;
101
+ }
102
+ let remaining = durationSecs;
103
+ btn.disabled = true;
104
+ btn.textContent = `Resend Code (${remaining}s)`;
105
+ this.resendCooldownInterval = setInterval(() => {
106
+ remaining -= 1;
107
+ if (remaining <= 0) {
108
+ clearInterval(this.resendCooldownInterval);
109
+ this.resendCooldownInterval = null;
110
+ btn.disabled = false;
111
+ btn.textContent = "Resend Code";
112
+ }
113
+ else {
114
+ btn.textContent = `Resend Code (${remaining}s)`;
115
+ }
116
+ }, 1000);
117
+ }
80
118
  async getKeystoneDetails(addr) {
81
119
  const cached = this.keystoneCache.get(addr);
82
120
  if (cached)
@@ -142,6 +180,10 @@ export class KeystoneDetailsComponentInstance extends IbGibDynamicComponentInsta
142
180
  this.elements.rawKeystoneJsonEl = shadow.getElementById('raw-keystone-json');
143
181
  this.elements.delegatesCardEl = shadow.getElementById('delegates-card');
144
182
  this.elements.delegatesListEl = shadow.getElementById('delegates-list');
183
+ this.elements.passkeysCardEl = shadow.getElementById('passkeys-card');
184
+ this.elements.passkeysListEl = shadow.getElementById('passkeys-list');
185
+ this.elements.btnAddPasskeyEl = shadow.getElementById('btn-add-passkey');
186
+ this.elements.passkeyStatusMsgEl = shadow.getElementById('passkey-status-msg');
145
187
  this.elements.containerEl = shadow.getElementById('container');
146
188
  this.elements.historicalWarningBannerEl = shadow.getElementById('historical-warning-banner');
147
189
  this.elements.ssoLinkCardEl = shadow.getElementById('sso-link-card');
@@ -154,6 +196,10 @@ export class KeystoneDetailsComponentInstance extends IbGibDynamicComponentInsta
154
196
  this.elements.btnChangePasswordEl = shadow.getElementById('btn-change-password');
155
197
  this.elements.changePasswordStatusEl = shadow.getElementById('change-password-status');
156
198
  this.elements.changePasswordCardEl = shadow.getElementById('change-password-card');
199
+ this.elements.sectionAddPasswordOtpEl = shadow.getElementById('section-add-password-otp');
200
+ this.elements.inputAddPasswordOtpEl = shadow.getElementById('input-add-password-otp');
201
+ this.elements.btnResendAddPasswordOtpEl = shadow.getElementById('btn-resend-add-password-otp');
202
+ this.elements.descAddPasswordOtpEl = shadow.getElementById('desc-add-password-otp');
157
203
  // ElementBase requires contentEl
158
204
  this.elements.contentEl = shadow.getElementById('identity-details-view');
159
205
  this.initHandlers();
@@ -178,11 +224,6 @@ export class KeystoneDetailsComponentInstance extends IbGibDynamicComponentInsta
178
224
  }
179
225
  }
180
226
  }
181
- async disconnected() {
182
- if (this._onIdentityChanged) {
183
- window.removeEventListener(EVENT_IBGIB_IDENTITY_CHANGED, this._onIdentityChanged);
184
- }
185
- }
186
227
  initHandlers() {
187
228
  // Setup raw JSON accordion toggle
188
229
  this.elements.rawAccordionHeaderEl.addEventListener('click', () => this.toggleAccordion());
@@ -191,9 +232,11 @@ export class KeystoneDetailsComponentInstance extends IbGibDynamicComponentInsta
191
232
  this.elements.btnSetActiveEl.addEventListener('click', () => this.handleSetActiveIdentity());
192
233
  this.elements.btnCheckUpdatesEl.addEventListener('click', () => this.handleCheckForUpdates());
193
234
  this.elements.btnUpdateIdentityEl.addEventListener('click', () => this.handleUpdateIdentity());
235
+ this.elements.btnAddPasskeyEl?.addEventListener('click', () => this.handleAddPasskey());
194
236
  this.elements.btnLinkGoogleEl.addEventListener('click', () => this.handleSSOClick('google'));
195
237
  this.elements.btnLinkGithubEl.addEventListener('click', () => this.handleSSOClick('github'));
196
238
  this.elements.btnChangePasswordEl.addEventListener('click', () => this.handleChangePassword());
239
+ this.elements.btnResendAddPasswordOtpEl?.addEventListener('click', () => this.handleResendAddPasswordOtp());
197
240
  }
198
241
  toggleAccordion() {
199
242
  const header = this.elements.rawAccordionHeaderEl;
@@ -399,13 +442,22 @@ export class KeystoneDetailsComponentInstance extends IbGibDynamicComponentInsta
399
442
  this.elements.btnLinkGithubEl.classList.remove('disabled');
400
443
  // Check if origin provider or linked
401
444
  const originProvider = this.aggregatedDetails?.custodianProvider || this.ibGib?.data?.frameDetails?.custodianProvider;
402
- const isGoogleLinked = isSsoProviderLinked({ keystone: this.ibGib, aggregatedDetails: this.aggregatedDetails, providerId: 'google' });
403
- const isGithubLinked = isSsoProviderLinked({ keystone: this.ibGib, aggregatedDetails: this.aggregatedDetails, providerId: 'github' });
404
- const caps = getKeystoneCapabilities({ keystone: this.ibGib, aggregatedDetails: this.aggregatedDetails });
445
+ const isGoogleLinked = !!this.ibGib ?
446
+ isSsoProviderLinked({ keystone: this.ibGib, aggregatedDetails: this.aggregatedDetails, providerId: 'google' }) :
447
+ false;
448
+ const isGithubLinked = !!this.ibGib ?
449
+ isSsoProviderLinked({ keystone: this.ibGib, aggregatedDetails: this.aggregatedDetails, providerId: 'github' }) :
450
+ false;
451
+ const caps = !!this.ibGib ?
452
+ getKeystoneCapabilities({ keystone: this.ibGib, aggregatedDetails: this.aggregatedDetails }) :
453
+ undefined;
405
454
  // Google Card States
406
455
  const googleCard = this.elements.btnLinkGoogleEl;
407
456
  const googleLabel = googleCard.querySelector('.toggle-state-text');
408
457
  if (isGoogleLinked) {
458
+ if (!caps) {
459
+ throw new Error(`(UNEXPECTED) isGoogleLinked but capabilities falsy? (E: cc62e8357b66860dd2cb8148c833b726)`);
460
+ }
409
461
  googleCard.classList.remove('unlinked');
410
462
  googleCard.classList.add('linked');
411
463
  const isOrigin = originProvider === 'google';
@@ -432,6 +484,9 @@ export class KeystoneDetailsComponentInstance extends IbGibDynamicComponentInsta
432
484
  const githubCard = this.elements.btnLinkGithubEl;
433
485
  const githubLabel = githubCard.querySelector('.toggle-state-text');
434
486
  if (isGithubLinked) {
487
+ if (!caps) {
488
+ throw new Error(`(UNEXPECTED) isGithubLinked but capabilities falsy? (E: e94688210538e0e82878088873a46826)`);
489
+ }
435
490
  githubCard.classList.remove('unlinked');
436
491
  githubCard.classList.add('linked');
437
492
  const isOrigin = originProvider === 'github';
@@ -457,12 +512,16 @@ export class KeystoneDetailsComponentInstance extends IbGibDynamicComponentInsta
457
512
  }
458
513
  }
459
514
  updatePasswordCardView() {
460
- const caps = getKeystoneCapabilities({ keystone: this.ibGib, aggregatedDetails: this.aggregatedDetails });
515
+ const caps = !!this.ibGib ?
516
+ getKeystoneCapabilities({ keystone: this.ibGib, aggregatedDetails: this.aggregatedDetails }) :
517
+ false;
461
518
  const cardTitleEl = this.elements?.changePasswordCardEl?.querySelector('h3');
462
519
  const cardDescEl = this.elements?.changePasswordCardEl?.querySelector('.description');
463
520
  const currentPasswordGroup = this.elements?.inputCurrentPasswordEl?.closest('.form-group');
464
521
  const btnChangePassword = this.elements?.btnChangePasswordEl;
465
- const isCustodialOnly = caps.isCustodial && !caps.isSovereign;
522
+ const isCustodialOnly = !!caps ?
523
+ caps.isCustodial && !caps.isSovereign :
524
+ false;
466
525
  if (isCustodialOnly) {
467
526
  if (cardTitleEl)
468
527
  cardTitleEl.textContent = "Add Master Password";
@@ -483,6 +542,12 @@ export class KeystoneDetailsComponentInstance extends IbGibDynamicComponentInsta
483
542
  if (btnChangePassword)
484
543
  btnChangePassword.textContent = "Update Password";
485
544
  }
545
+ if (this.elements?.sectionAddPasswordOtpEl) {
546
+ this.elements.sectionAddPasswordOtpEl.classList.add('hidden');
547
+ if (this.elements.inputAddPasswordOtpEl) {
548
+ this.elements.inputAddPasswordOtpEl.value = '';
549
+ }
550
+ }
486
551
  }
487
552
  async updateFrameDetailsView() {
488
553
  const lc = `${this.lc}[${this.updateFrameDetailsView.name}]`;
@@ -588,17 +653,20 @@ export class KeystoneDetailsComponentInstance extends IbGibDynamicComponentInsta
588
653
  this.elements.rawKeystoneJsonEl.textContent = pretty(keystone);
589
654
  // Render Challenge Pools
590
655
  this.renderChallengePools(keystone.data?.challengePools || []);
591
- // Render Delegates, SSO link, and Password Change cards (only if this is a primary keystone, hide them for delegate/session keystones)
656
+ // Render Delegates, Passkeys, SSO link, and Password Change cards (only if this is a primary keystone, hide them for delegate/session keystones)
592
657
  const details = await this.getKeystoneDetails(this.ibGibAddr);
593
658
  if (details.isPrimary) {
594
659
  this.elements.delegatesCardEl.classList.remove('hidden');
595
660
  await this.renderDelegates(keystone.data?.delegates);
661
+ this.elements.passkeysCardEl.classList.remove('hidden');
662
+ await this.renderPasskeys(this.aggregatedDetails?.passkeys);
596
663
  this.elements.ssoLinkCardEl.classList.remove('hidden');
597
664
  this.elements.changePasswordCardEl.classList.remove('hidden');
598
665
  this.updatePasswordCardView();
599
666
  }
600
667
  else {
601
668
  this.elements.delegatesCardEl.classList.add('hidden');
669
+ this.elements.passkeysCardEl.classList.add('hidden');
602
670
  this.elements.ssoLinkCardEl.classList.add('hidden');
603
671
  this.elements.changePasswordCardEl.classList.add('hidden');
604
672
  }
@@ -637,6 +705,15 @@ export class KeystoneDetailsComponentInstance extends IbGibDynamicComponentInsta
637
705
  if (this.elements.delegatesListEl) {
638
706
  this.elements.delegatesListEl.innerHTML = '';
639
707
  }
708
+ if (this.elements.passkeysListEl) {
709
+ this.elements.passkeysListEl.innerHTML = '';
710
+ }
711
+ if (this.elements.sectionAddPasswordOtpEl) {
712
+ this.elements.sectionAddPasswordOtpEl.classList.add('hidden');
713
+ if (this.elements.inputAddPasswordOtpEl) {
714
+ this.elements.inputAddPasswordOtpEl.value = '';
715
+ }
716
+ }
640
717
  }
641
718
  renderChallengePools(pools) {
642
719
  if (!this.elements) {
@@ -936,8 +1013,9 @@ export class KeystoneDetailsComponentInstance extends IbGibDynamicComponentInsta
936
1013
  let masterSecret;
937
1014
  try {
938
1015
  masterSecret = await promptForSecret({
939
- msg: `Enter Master Password to revoke delegate '${deviceName}':`,
940
- confirm: false
1016
+ msg: `Enter Master Password or touch security key to revoke delegate '${deviceName}':`,
1017
+ confirm: false,
1018
+ passkeys: this.aggregatedDetails?.passkeys,
941
1019
  });
942
1020
  }
943
1021
  catch (err) {
@@ -956,11 +1034,30 @@ export class KeystoneDetailsComponentInstance extends IbGibDynamicComponentInsta
956
1034
  throw new Error("Local space not found");
957
1035
  }
958
1036
  const keystoneSvc = new KeystoneService_V1();
1037
+ // Resolve which manage pool matches the provided masterSecret
1038
+ let matchedPoolId;
1039
+ const candidatePools = this.ibGib.data?.challengePools.filter(p => p.id === 'manage' || p.id.startsWith('passkey-manage-') || (p.config.allowedVerbs && p.config.allowedVerbs.includes(KEYSTONE_VERB_MANAGE))) || [];
1040
+ for (const pool of candidatePools) {
1041
+ const ok = await keystoneSvc.verifySigningSecret({
1042
+ keystoneIbGib: this.ibGib,
1043
+ signingSecret: masterSecret,
1044
+ poolId: pool.id
1045
+ });
1046
+ if (ok) {
1047
+ matchedPoolId = pool.id;
1048
+ break;
1049
+ }
1050
+ }
1051
+ if (!matchedPoolId) {
1052
+ this.setSyncStatus("Failed to authorize revocation (invalid secret/passkey).", "error");
1053
+ return;
1054
+ }
959
1055
  // Unregister delegate from parent keystone
960
1056
  const evolvedParent = await keystoneSvc.unregisterDelegate({
961
1057
  parentKeystone: this.ibGib,
962
1058
  delegateTjpAddr,
963
1059
  masterSecret,
1060
+ signingPoolId: matchedPoolId,
964
1061
  metaspace,
965
1062
  space
966
1063
  });
@@ -1006,6 +1103,452 @@ export class KeystoneDetailsComponentInstance extends IbGibDynamicComponentInsta
1006
1103
  this.setSyncStatus(`Revocation failed: ${extractErrorMsg(error)}`, "error");
1007
1104
  }
1008
1105
  }
1106
+ setPasskeyStatus(msg, type) {
1107
+ const statusEl = this.elements?.passkeyStatusMsgEl;
1108
+ if (!statusEl)
1109
+ return;
1110
+ statusEl.textContent = msg;
1111
+ statusEl.className = `status-msg ${type}`;
1112
+ statusEl.classList.remove('hidden');
1113
+ }
1114
+ async renderPasskeys(passkeys) {
1115
+ if (!this.elements) {
1116
+ return;
1117
+ }
1118
+ const listEl = this.elements.passkeysListEl;
1119
+ listEl.innerHTML = '';
1120
+ const caps = !!this.ibGib ?
1121
+ getKeystoneCapabilities({ keystone: this.ibGib, aggregatedDetails: this.aggregatedDetails }) :
1122
+ undefined;
1123
+ // Update button text depending on whether the account is custodial-only (elevation) or sovereign/hybrid
1124
+ const isCustodialOnly = !!caps ? caps.isCustodial && !caps.isSovereign : false;
1125
+ if (this.elements.btnAddPasskeyEl) {
1126
+ this.elements.btnAddPasskeyEl.textContent = isCustodialOnly
1127
+ ? "🛡️ Add Passkey (Passwordless Sovereign Elevation)"
1128
+ : "🛡️ Register New Passkey / Security Key";
1129
+ }
1130
+ const validPasskeyEntries = Object.entries(passkeys || {}).filter(([_, entry]) => entry !== null && entry !== undefined);
1131
+ if (validPasskeyEntries.length === 0) {
1132
+ listEl.innerHTML = `
1133
+ <tr>
1134
+ <td colspan="4" style="text-align: center; color: var(--clr-text-secondary, #b3b3b3); padding: 1rem;">
1135
+ No hardware passkeys or biometrics registered.
1136
+ </td>
1137
+ </tr>
1138
+ `;
1139
+ return;
1140
+ }
1141
+ for (const [credId, entry] of validPasskeyEntries) {
1142
+ const row = document.createElement('tr');
1143
+ const createdText = entry.createdUTC ? new Date(entry.createdUTC).toLocaleString() : 'N/A';
1144
+ const nameText = entry.name || 'Hardware Key';
1145
+ // Format transport badges
1146
+ let transportBadgesHtml = '';
1147
+ if (entry.transports && entry.transports.length > 0) {
1148
+ transportBadgesHtml = entry.transports.map(t => {
1149
+ let icon = '🔑';
1150
+ let label = t;
1151
+ if (t === 'internal') {
1152
+ icon = '💻';
1153
+ label = 'Touch ID / Built-in';
1154
+ }
1155
+ else if (t === 'usb') {
1156
+ icon = '🔑';
1157
+ label = 'USB Key';
1158
+ }
1159
+ else if (t === 'nfc') {
1160
+ icon = '📶';
1161
+ label = 'NFC';
1162
+ }
1163
+ else if (t === 'ble' || t === 'hybrid') {
1164
+ icon = '📲';
1165
+ label = 'Mobile / Bluetooth';
1166
+ }
1167
+ return `<span class="badge badge-remote-device" style="background-color: #1b5e20; color: #a5d6a7; font-size: 0.75rem; padding: 0.2rem 0.5rem; border-radius: 4px; font-weight: bold; margin-right: 0.3rem;">${icon} ${label}</span>`;
1168
+ }).join(' ');
1169
+ }
1170
+ else {
1171
+ transportBadgesHtml = `<span class="badge badge-remote-device" style="background-color: #2e7d32; color: #c8e6c9; font-size: 0.75rem; padding: 0.2rem 0.5rem; border-radius: 4px; font-weight: bold;">🛡️ Resident Passkey</span>`;
1172
+ }
1173
+ row.innerHTML = `
1174
+ <td>
1175
+ <div style="font-weight: 500;">${nameText}</div>
1176
+ <code style="font-size: 0.75rem; color: var(--clr-text-secondary, #b3b3b3); word-break: break-all;">${credId.substring(0, 16)}...</code>
1177
+ </td>
1178
+ <td>${transportBadgesHtml}</td>
1179
+ <td style="font-size: 0.85rem; color: var(--clr-text-secondary, #b3b3b3);">${createdText}</td>
1180
+ <td>
1181
+ <button class="action-btn secondary btn-remove-passkey" style="font-size: 0.8rem; padding: 0.3rem 0.6rem; background: rgba(239, 83, 80, 0.1); color: #ef5350; border: 1px solid rgba(239, 83, 80, 0.3);">
1182
+ Remove
1183
+ </button>
1184
+ </td>
1185
+ `;
1186
+ const btnRemove = row.querySelector('.btn-remove-passkey');
1187
+ btnRemove?.addEventListener('click', () => this.handleRemovePasskey(credId));
1188
+ listEl.appendChild(row);
1189
+ }
1190
+ }
1191
+ async handleAddPasskey() {
1192
+ const lc = `${this.lc}[${this.handleAddPasskey.name}]`;
1193
+ if (!this.ibGib || !this.ibGibAddr) {
1194
+ return;
1195
+ }
1196
+ try {
1197
+ const caps = getKeystoneCapabilities({ keystone: this.ibGib, aggregatedDetails: this.aggregatedDetails });
1198
+ const isCustodialOnly = caps.isCustodial && !caps.isSovereign;
1199
+ const supported = await isPasskeyPrfSupported();
1200
+ if (!supported) {
1201
+ this.setPasskeyStatus("WebAuthn PRF extension is not supported in this browser environment.", "error");
1202
+ return;
1203
+ }
1204
+ this.showBusy({
1205
+ isBusy: true,
1206
+ title: 'Touch Biometrics or Security Key',
1207
+ msg: 'Please touch your fingerprint sensor, Face ID, or insert your USB security key when prompted...',
1208
+ animationEmoji: '🛡️'
1209
+ });
1210
+ const username = this.aggregatedDetails?.username || this.ibGib.data?.frameDetails?.username || 'user';
1211
+ const rawSalt = crypto.getRandomValues(new Uint8Array(32));
1212
+ const saltHex = bufferToHex(rawSalt.buffer);
1213
+ const regResult = await registerPasskeyCredential({
1214
+ username,
1215
+ displayName: username,
1216
+ salt: saltHex
1217
+ });
1218
+ const { credentialId, transports } = regResult;
1219
+ let passkeySecret = regResult.passkeySecret;
1220
+ if (!passkeySecret) {
1221
+ const prfResult = await evaluatePasskeyPrf({
1222
+ credentialId,
1223
+ salt: saltHex,
1224
+ transports
1225
+ });
1226
+ passkeySecret = prfResult.passkeySecret;
1227
+ }
1228
+ const defaultDeviceTag = generateDefaultDeviceTag();
1229
+ const passkeyName = defaultDeviceTag;
1230
+ const passkeyTag = getSaferSubstring({ text: passkeyName, length: 12, keepLiterals: ['-'] }) || 'key';
1231
+ const poolId = `passkey-manage-${passkeyTag}`;
1232
+ const passkeyEntry = {
1233
+ credentialId,
1234
+ poolId,
1235
+ name: passkeyName,
1236
+ salt: saltHex,
1237
+ createdUTC: new Date().toISOString(),
1238
+ ...(transports && transports.length > 0 ? { transports } : {})
1239
+ };
1240
+ const metaspace = await getGlobalMetaspace_waitIfNeeded();
1241
+ if (!metaspace)
1242
+ throw new Error("Metaspace not found");
1243
+ const space = await metaspace.getLocalUserSpace({ lock: false });
1244
+ if (!space)
1245
+ throw new Error("Local space not found");
1246
+ const keystoneService = new KeystoneService_V1();
1247
+ let evolvedKeystone;
1248
+ if (isCustodialOnly) {
1249
+ // Elevate custodial account with sovereign passkey
1250
+ this.showBusy({
1251
+ isBusy: true,
1252
+ title: 'Elevating Account Ownership...',
1253
+ msg: 'Establishing sovereign passkey pool on identity...',
1254
+ animationEmoji: '⚡'
1255
+ });
1256
+ // Generate new manage pool configuration using passkeySecret
1257
+ const poolConfig = {
1258
+ id: poolId,
1259
+ type: KeystoneChallengeType.hash_reveal_v1,
1260
+ salt: poolId,
1261
+ algo: HashAlgorithm.sha_256,
1262
+ rounds: 1,
1263
+ allowedVerbs: [KEYSTONE_VERB_MANAGE, KEYSTONE_VERB_SYNC, KEYSTONE_VERB_CONNECT],
1264
+ behavior: {
1265
+ size: 10,
1266
+ replenish: KeystoneReplenishStrategy.topUp,
1267
+ selectSequentially: 1,
1268
+ selectRandomly: 0,
1269
+ targetBindingCount: 0,
1270
+ }
1271
+ };
1272
+ const challenges = await generatePoolChallenges({
1273
+ config: poolConfig,
1274
+ masterSecret: passkeySecret
1275
+ });
1276
+ const newPool = {
1277
+ id: poolId,
1278
+ config: poolConfig,
1279
+ challenges
1280
+ };
1281
+ evolvedKeystone = await keystoneService.addPools({
1282
+ latestKeystone: this.ibGib,
1283
+ masterSecret: passkeySecret,
1284
+ newPools: [newPool],
1285
+ frameDetails: {
1286
+ passkeys: {
1287
+ [credentialId]: passkeyEntry
1288
+ }
1289
+ },
1290
+ dataToPatch: {
1291
+ credentialIds: [...(this.ibGib.data?.credentialIds || []), credentialId]
1292
+ },
1293
+ metaspace,
1294
+ space
1295
+ });
1296
+ }
1297
+ else {
1298
+ // Secondary passkey registration on existing sovereign identity:
1299
+ // Prompt user to authorize with current master password
1300
+ let authSecret;
1301
+ let isCorrect = false;
1302
+ let tries = 0;
1303
+ const maxTries = 5;
1304
+ while (tries < maxTries && !isCorrect) {
1305
+ try {
1306
+ const promptMsg = tries === 0
1307
+ ? 'Enter your current Master Secret (Password) or touch security key to authorize adding this passkey:'
1308
+ : `Incorrect password. Please try again (${maxTries - tries} tries remaining):`;
1309
+ authSecret = await promptForSecret({
1310
+ msg: promptMsg,
1311
+ confirm: false,
1312
+ passkeys: this.aggregatedDetails?.passkeys,
1313
+ });
1314
+ }
1315
+ catch (err) {
1316
+ authSecret = undefined;
1317
+ break;
1318
+ }
1319
+ if (!authSecret)
1320
+ break;
1321
+ // Verify secret against manage/passkey pools
1322
+ isCorrect = await keystoneService.verifySigningSecret({
1323
+ keystoneIbGib: this.ibGib,
1324
+ signingSecret: authSecret
1325
+ });
1326
+ if (!isCorrect)
1327
+ tries++;
1328
+ }
1329
+ if (!isCorrect || !authSecret) {
1330
+ this.setPasskeyStatus("Registration cancelled (authorization required).", "error");
1331
+ return;
1332
+ }
1333
+ this.showBusy({
1334
+ isBusy: true,
1335
+ title: 'Registering Passkey...',
1336
+ msg: 'Adding cryptographic challenge pool and evolving identity...',
1337
+ animationEmoji: '🛡️'
1338
+ });
1339
+ const poolConfig = {
1340
+ id: poolId,
1341
+ type: KeystoneChallengeType.hash_reveal_v1,
1342
+ salt: poolId,
1343
+ algo: HashAlgorithm.sha_256,
1344
+ rounds: 1,
1345
+ allowedVerbs: [KEYSTONE_VERB_MANAGE, KEYSTONE_VERB_SYNC, KEYSTONE_VERB_CONNECT],
1346
+ behavior: {
1347
+ size: 10,
1348
+ replenish: KeystoneReplenishStrategy.topUp,
1349
+ selectSequentially: 1,
1350
+ selectRandomly: 0,
1351
+ targetBindingCount: 0,
1352
+ }
1353
+ };
1354
+ const challenges = await generatePoolChallenges({
1355
+ config: poolConfig,
1356
+ masterSecret: passkeySecret
1357
+ });
1358
+ const newPool = {
1359
+ id: poolId,
1360
+ config: poolConfig,
1361
+ challenges
1362
+ };
1363
+ evolvedKeystone = await keystoneService.addPools({
1364
+ latestKeystone: this.ibGib,
1365
+ masterSecret: authSecret,
1366
+ newPools: [newPool],
1367
+ frameDetails: {
1368
+ passkeys: {
1369
+ [credentialId]: passkeyEntry
1370
+ }
1371
+ },
1372
+ dataToPatch: {
1373
+ credentialIds: [...(this.ibGib.data?.credentialIds || []), credentialId]
1374
+ },
1375
+ metaspace,
1376
+ space
1377
+ });
1378
+ }
1379
+ // Sync evolved keystone to server
1380
+ this.showBusy({
1381
+ isBusy: true,
1382
+ title: 'Syncing to Server...',
1383
+ msg: 'Publishing evolved identity keystone to server...',
1384
+ animationEmoji: '⚡'
1385
+ });
1386
+ const primaryTjpAddr = getTjpAddr({ ibGib: evolvedKeystone, defaultIfNone: 'incomingAddr' });
1387
+ const resSync = await getApiBridge().putEvolveKeystone({
1388
+ domainAddr: primaryTjpAddr,
1389
+ keystoneIbGib: evolvedKeystone,
1390
+ relatedKeystoneIbGibs: []
1391
+ });
1392
+ if (!resSync.success) {
1393
+ console.warn(`${lc} Server sync note: ${resSync.message}`);
1394
+ }
1395
+ // Update Special Index
1396
+ await updateSpecialIndex({
1397
+ type: "keystones",
1398
+ rel8nInfos: [
1399
+ {
1400
+ rel8nName: "keystone",
1401
+ ibGibs: [toDto({ ibGib: evolvedKeystone })]
1402
+ }
1403
+ ],
1404
+ metaspace,
1405
+ space
1406
+ });
1407
+ this.setPasskeyStatus("Passkey successfully registered and bound to identity!", "success");
1408
+ this.ibGibAddr = getIbGibAddr({ ibGib: evolvedKeystone });
1409
+ this.staticIbGib = evolvedKeystone;
1410
+ await this.loadIbGib({ getLatest: false });
1411
+ }
1412
+ catch (error) {
1413
+ console.error(`${lc} ${extractErrorMsg(error)}`, error);
1414
+ this.setPasskeyStatus(`Failed to register passkey: ${extractErrorMsg(error)}`, "error");
1415
+ }
1416
+ finally {
1417
+ this.showBusy({ isBusy: false });
1418
+ }
1419
+ }
1420
+ async handleRemovePasskey(credentialId) {
1421
+ const lc = `${this.lc}[${this.handleRemovePasskey.name}]`;
1422
+ if (!this.ibGib || !this.ibGibAddr) {
1423
+ return;
1424
+ }
1425
+ const passkeyEntry = this.aggregatedDetails?.passkeys?.[credentialId];
1426
+ const passkeyName = passkeyEntry?.name || 'this passkey';
1427
+ try {
1428
+ const keystoneService = new KeystoneService_V1();
1429
+ let masterSecret;
1430
+ let isCorrect = false;
1431
+ let tries = 0;
1432
+ const maxTries = 5;
1433
+ while (tries < maxTries && !isCorrect) {
1434
+ try {
1435
+ const promptMsg = tries === 0
1436
+ ? `Are you sure you want to remove "${passkeyName}"? Enter your Master Secret (Password) or touch security key to authorize:`
1437
+ : `Incorrect password. Please try again (${maxTries - tries} tries remaining):`;
1438
+ masterSecret = await promptForSecret({
1439
+ msg: promptMsg,
1440
+ confirm: false,
1441
+ passkeys: this.aggregatedDetails?.passkeys,
1442
+ });
1443
+ }
1444
+ catch (err) {
1445
+ masterSecret = undefined;
1446
+ break;
1447
+ }
1448
+ if (!masterSecret)
1449
+ break;
1450
+ isCorrect = await keystoneService.verifySigningSecret({
1451
+ keystoneIbGib: this.ibGib,
1452
+ signingSecret: masterSecret
1453
+ });
1454
+ if (!isCorrect)
1455
+ tries++;
1456
+ }
1457
+ if (!isCorrect || !masterSecret) {
1458
+ this.setPasskeyStatus("Removal cancelled or unauthorized.", "error");
1459
+ return;
1460
+ }
1461
+ this.showBusy({
1462
+ isBusy: true,
1463
+ title: 'Revoking Passkey...',
1464
+ msg: 'Tombstoning passkey record and removing challenge pool...',
1465
+ animationEmoji: '🗑️'
1466
+ });
1467
+ const metaspace = await getGlobalMetaspace_waitIfNeeded();
1468
+ if (!metaspace)
1469
+ throw new Error("Metaspace not found");
1470
+ const space = await metaspace.getLocalUserSpace({ lock: false });
1471
+ if (!space)
1472
+ throw new Error("Local space not found");
1473
+ const poolIdToRemove = passkeyEntry?.poolId || `passkey-manage-${credentialId.substring(0, 8)}`;
1474
+ const poolExists = this.ibGib.data?.challengePools.some(p => p.id === poolIdToRemove);
1475
+ const filteredCredentialIds = (this.ibGib.data?.credentialIds || []).filter(id => id !== credentialId);
1476
+ let evolvedKeystone;
1477
+ if (poolExists) {
1478
+ evolvedKeystone = await keystoneService.removePools({
1479
+ latestKeystone: this.ibGib,
1480
+ masterSecret,
1481
+ poolIds: [poolIdToRemove],
1482
+ frameDetails: {
1483
+ passkeys: {
1484
+ [credentialId]: null
1485
+ }
1486
+ },
1487
+ dataToPatch: {
1488
+ credentialIds: filteredCredentialIds
1489
+ },
1490
+ metaspace,
1491
+ space
1492
+ });
1493
+ }
1494
+ else {
1495
+ evolvedKeystone = await keystoneService.sign({
1496
+ latestKeystone: this.ibGib,
1497
+ masterSecret,
1498
+ claim: {
1499
+ verb: POOL_ID_MANAGE,
1500
+ target: getIbGibAddr({ ibGib: this.ibGib })
1501
+ },
1502
+ frameDetails: {
1503
+ passkeys: {
1504
+ [credentialId]: null
1505
+ }
1506
+ },
1507
+ dataToPatch: {
1508
+ credentialIds: filteredCredentialIds
1509
+ },
1510
+ metaspace,
1511
+ space
1512
+ });
1513
+ }
1514
+ // Sync to server
1515
+ this.showBusy({
1516
+ isBusy: true,
1517
+ title: 'Syncing to Server...',
1518
+ msg: 'Publishing updated keystone to server...',
1519
+ animationEmoji: '⚡'
1520
+ });
1521
+ const primaryTjpAddr = getTjpAddr({ ibGib: evolvedKeystone, defaultIfNone: 'incomingAddr' });
1522
+ await getApiBridge().putEvolveKeystone({
1523
+ domainAddr: primaryTjpAddr,
1524
+ keystoneIbGib: evolvedKeystone,
1525
+ relatedKeystoneIbGibs: []
1526
+ });
1527
+ // Update Special Index
1528
+ await updateSpecialIndex({
1529
+ type: "keystones",
1530
+ rel8nInfos: [
1531
+ {
1532
+ rel8nName: "keystone",
1533
+ ibGibs: [toDto({ ibGib: evolvedKeystone })]
1534
+ }
1535
+ ],
1536
+ metaspace,
1537
+ space
1538
+ });
1539
+ this.setPasskeyStatus(`Passkey "${passkeyName}" successfully removed.`, "success");
1540
+ this.ibGibAddr = getIbGibAddr({ ibGib: evolvedKeystone });
1541
+ this.staticIbGib = evolvedKeystone;
1542
+ await this.loadIbGib({ getLatest: false });
1543
+ }
1544
+ catch (error) {
1545
+ console.error(`${lc} ${extractErrorMsg(error)}`, error);
1546
+ this.setPasskeyStatus(`Failed to remove passkey: ${extractErrorMsg(error)}`, "error");
1547
+ }
1548
+ finally {
1549
+ this.showBusy({ isBusy: false });
1550
+ }
1551
+ }
1009
1552
  async handleContextUpdated() {
1010
1553
  const lc = `${this.lc}[${this.handleContextUpdated.name}]`;
1011
1554
  try {
@@ -1050,10 +1593,24 @@ export class KeystoneDetailsComponentInstance extends IbGibDynamicComponentInsta
1050
1593
  async handleSSOLink(providerId) {
1051
1594
  const lc = `${this.lc}[${this.handleSSOLink.name}]`;
1052
1595
  this.setSSOStatus(`Linking ${providerId}...`, "info");
1596
+ const providerName = providerId === 'google' ? 'Google' : providerId === 'github' ? 'GitHub' : providerId;
1597
+ this.setSSOStatus(`Opening ${providerName} SSO authorization window...`, "info");
1598
+ this.showBusy({
1599
+ isBusy: true,
1600
+ title: `Connecting to ${providerName}...`,
1601
+ msg: `Opening ${providerName} SSO authorization window...\nPlease complete authorization in the popup window.`,
1602
+ animationEmoji: '🌐'
1603
+ });
1053
1604
  try {
1605
+ const primaryTjpAddr = (this.ibGib ? getTjpAddr({ ibGib: this.ibGib }) : undefined)
1606
+ || (this.ibGibAddr ? (() => {
1607
+ const { ib } = getIbAndGib({ ibGibAddr: this.ibGibAddr });
1608
+ const { tjpGib } = getGibInfo({ ibGibAddr: this.ibGibAddr });
1609
+ return tjpGib && ib ? `${ib}^${tjpGib}` : this.ibGibAddr;
1610
+ })() : undefined);
1054
1611
  const resPopup = await executeSsoOAuthPopup({
1055
1612
  providerId,
1056
- parentTjpAddr: this.ibGibAddr
1613
+ parentTjpAddr: primaryTjpAddr
1057
1614
  });
1058
1615
  if (!resPopup.success || !resPopup.code) {
1059
1616
  this.setSSOStatus(resPopup.message || `Linking cancelled.`, "error");
@@ -1065,6 +1622,9 @@ export class KeystoneDetailsComponentInstance extends IbGibDynamicComponentInsta
1065
1622
  console.error(`${lc} SSO link initiation failed: ${extractErrorMsg(error)}`);
1066
1623
  this.setSSOStatus(`Link failed: ${extractErrorMsg(error)}`, "error");
1067
1624
  }
1625
+ finally {
1626
+ this.showBusy({ isBusy: false });
1627
+ }
1068
1628
  }
1069
1629
  setSSOStatus(msg, type) {
1070
1630
  const statusEl = this.elements.ssoStatusMsgEl;
@@ -1074,7 +1634,14 @@ export class KeystoneDetailsComponentInstance extends IbGibDynamicComponentInsta
1074
1634
  }
1075
1635
  async handleSSOLinkCallback(code, stateObj, redirectUri) {
1076
1636
  const lc = `${this.lc}[${this.handleSSOLinkCallback.name}]`;
1077
- this.setSSOStatus("Retrieving challenge pool configuration from server...", "info");
1637
+ const providerName = stateObj.providerId === 'google' ? 'Google' : stateObj.providerId === 'github' ? 'GitHub' : stateObj.providerId;
1638
+ this.setSSOStatus(`Retrieving challenge pool configuration from server...`, "info");
1639
+ this.showBusy({
1640
+ isBusy: true,
1641
+ title: `Linking ${providerName}...`,
1642
+ msg: `Validating ${providerName} authorization with server...`,
1643
+ animationEmoji: '🔐'
1644
+ });
1078
1645
  try {
1079
1646
  // 1. Exchange OAuth code for custodian public pool config
1080
1647
  const res = await fetch('/api/identity/sso/link', {
@@ -1098,6 +1665,7 @@ export class KeystoneDetailsComponentInstance extends IbGibDynamicComponentInsta
1098
1665
  }
1099
1666
  const custodianPool = linkData.custodianPool;
1100
1667
  // 2. Prompt user for master secret and verify it
1668
+ this.showBusy({ isBusy: false });
1101
1669
  const keystoneService = new KeystoneService_V1();
1102
1670
  let masterSecret;
1103
1671
  let isCorrect = false;
@@ -1138,6 +1706,12 @@ export class KeystoneDetailsComponentInstance extends IbGibDynamicComponentInsta
1138
1706
  this.setSSOStatus("Link cancelled or failed (incorrect password).", "error");
1139
1707
  return;
1140
1708
  }
1709
+ this.showBusy({
1710
+ isBusy: true,
1711
+ title: 'Evolving Identity...',
1712
+ msg: `Installing ${providerName} custodian pool and signing evolution...`,
1713
+ animationEmoji: '📝'
1714
+ });
1141
1715
  this.setSSOStatus("Evolving identity locally and generating cryptographic signatures...", "info");
1142
1716
  const metaspace = await getGlobalMetaspace_waitIfNeeded();
1143
1717
  if (!metaspace)
@@ -1146,7 +1720,7 @@ export class KeystoneDetailsComponentInstance extends IbGibDynamicComponentInsta
1146
1720
  if (!space) {
1147
1721
  throw new Error("Local space not found");
1148
1722
  }
1149
- // 3. Evolve parent keystone to include the custodian pool
1723
+ // 3. Evolve parent keystone to include the custodian pool (n -> n+1)
1150
1724
  const evolvedParent = await keystoneService.addPools({
1151
1725
  latestKeystone: this.ibGib,
1152
1726
  masterSecret: masterSecret,
@@ -1154,38 +1728,81 @@ export class KeystoneDetailsComponentInstance extends IbGibDynamicComponentInsta
1154
1728
  metaspace,
1155
1729
  space
1156
1730
  });
1157
- // 4. Sync evolved parent keystone back to server
1158
- this.setSSOStatus("Publishing evolved identity keystone to server...", "info");
1159
- const syncRes = await getApiBridge().putEvolveKeystone({
1731
+ // Sync evolved parent (n+1) to server first
1732
+ const syncRes1 = await getApiBridge().putEvolveKeystone({
1160
1733
  domainAddr: stateObj.parentTjpAddr,
1161
1734
  keystoneIbGib: evolvedParent,
1162
- relatedKeystoneIbGibs: [] // No outward delegate timelines
1735
+ relatedKeystoneIbGibs: []
1163
1736
  });
1164
- if (!syncRes.success) {
1165
- throw new Error(`Server synchronization rejected: ${syncRes.message}`);
1737
+ if (!syncRes1.success) {
1738
+ throw new Error(`Server synchronization of linked pool rejected: ${syncRes1.message}`);
1739
+ }
1740
+ // 4. Create and register OAuth2 sync delegate for this provider (n+1 -> n+2)
1741
+ const username = this.aggregatedDetails?.username || this.ibGib?.data?.username || 'user';
1742
+ const { delegateKeystone, deviceTag } = await createOAuth2DeviceSyncDelegate({
1743
+ providerId: stateObj.providerId,
1744
+ username,
1745
+ primaryTjpAddr: stateObj.parentTjpAddr,
1746
+ custodianReqs: linkData.custodianReqs,
1747
+ metaspace,
1748
+ space,
1749
+ });
1750
+ const parentWithDelegate = await keystoneService.registerDelegate({
1751
+ parentKeystone: evolvedParent,
1752
+ delegateKeystone,
1753
+ masterSecret: masterSecret,
1754
+ signingPoolId: 'manage',
1755
+ metaspace,
1756
+ space,
1757
+ allowedVerbs: [KEYSTONE_VERB_SYNC, KEYSTONE_VERB_CONNECT],
1758
+ deviceName: deviceTag,
1759
+ });
1760
+ await metaspace.put({ ibGibs: [delegateKeystone, parentWithDelegate], space });
1761
+ // 5. Sync parent with registered delegate (n+2) to server
1762
+ this.showBusy({
1763
+ isBusy: true,
1764
+ title: 'Syncing to Server...',
1765
+ msg: 'Publishing linked identity and delegate to server...',
1766
+ animationEmoji: '⚡'
1767
+ });
1768
+ this.setSSOStatus("Publishing evolved identity keystone to server...", "info");
1769
+ const syncRes2 = await getApiBridge().putEvolveKeystone({
1770
+ domainAddr: stateObj.parentTjpAddr,
1771
+ keystoneIbGib: parentWithDelegate,
1772
+ relatedKeystoneIbGibs: [delegateKeystone]
1773
+ });
1774
+ if (!syncRes2.success) {
1775
+ throw new Error(`Server synchronization of delegate rejected: ${syncRes2.message}`);
1166
1776
  }
1167
- // 5. Update index locally
1777
+ // 6. Update index locally
1168
1778
  await updateSpecialIndex({
1169
1779
  type: "keystones",
1170
1780
  rel8nInfos: [
1171
1781
  {
1172
1782
  rel8nName: "keystone",
1173
- ibGibs: [toDto({ ibGib: evolvedParent })]
1783
+ ibGibs: [toDto({ ibGib: parentWithDelegate }), toDto({ ibGib: delegateKeystone })]
1174
1784
  }
1175
1785
  ],
1176
1786
  metaspace,
1177
1787
  space
1178
1788
  });
1179
1789
  this.setSSOStatus(`Successfully linked with ${stateObj.providerId}!`, "success");
1790
+ this.ibGibAddr = getIbGibAddr({ ibGib: parentWithDelegate });
1791
+ this.staticIbGib = parentWithDelegate;
1180
1792
  await this.loadIbGib({ getLatest: false });
1181
1793
  }
1182
1794
  catch (error) {
1183
1795
  console.error(`${lc} SSO callback processing failed: ${extractErrorMsg(error)}`);
1184
1796
  this.setSSOStatus(`Link failed: ${extractErrorMsg(error)}`, "error");
1185
1797
  }
1798
+ finally {
1799
+ this.showBusy({ isBusy: false });
1800
+ }
1186
1801
  }
1187
1802
  async handleSSOClick(providerId) {
1188
- const isLinked = isSsoProviderLinked({ keystone: this.ibGib, aggregatedDetails: this.aggregatedDetails, providerId });
1803
+ const isLinked = !!this.ibGib ?
1804
+ isSsoProviderLinked({ keystone: this.ibGib, aggregatedDetails: this.aggregatedDetails, providerId }) :
1805
+ false;
1189
1806
  if (isLinked) {
1190
1807
  const caps = getKeystoneCapabilities({ keystone: this.ibGib, aggregatedDetails: this.aggregatedDetails });
1191
1808
  if (!caps.isSovereign) {
@@ -1200,7 +1817,8 @@ export class KeystoneDetailsComponentInstance extends IbGibDynamicComponentInsta
1200
1817
  }
1201
1818
  async handleSSOUnlink(providerId) {
1202
1819
  const lc = `${this.lc}[${this.handleSSOUnlink.name}]`;
1203
- this.setSSOStatus(`Unlinking ${providerId}...`, "info");
1820
+ const providerName = providerId === 'google' ? 'Google' : providerId === 'github' ? 'GitHub' : providerId;
1821
+ this.setSSOStatus(`Unlinking ${providerName}...`, "info");
1204
1822
  try {
1205
1823
  // 1. Prompt user for password
1206
1824
  const keystoneService = new KeystoneService_V1();
@@ -1211,11 +1829,12 @@ export class KeystoneDetailsComponentInstance extends IbGibDynamicComponentInsta
1211
1829
  while (tries < maxTries && !isCorrect) {
1212
1830
  try {
1213
1831
  const promptMsg = tries === 0
1214
- ? `Are you sure you want to unlink your ${providerId} account? If so, enter your local Master Secret (Password) to authorize. Otherwise, click Cancel.`
1832
+ ? `Are you sure you want to unlink your ${providerName} account? If so, enter your local Master Secret (Password) or touch security key to authorize. Otherwise, click Cancel.`
1215
1833
  : `Incorrect password. Please try again (${maxTries - tries} tries remaining):`;
1216
1834
  masterSecret = await promptForSecret({
1217
1835
  msg: promptMsg,
1218
- confirm: false
1836
+ confirm: false,
1837
+ passkeys: this.aggregatedDetails?.passkeys,
1219
1838
  });
1220
1839
  }
1221
1840
  catch (error) {
@@ -1225,11 +1844,10 @@ export class KeystoneDetailsComponentInstance extends IbGibDynamicComponentInsta
1225
1844
  if (!masterSecret) {
1226
1845
  break;
1227
1846
  }
1228
- // Verify secret using verifySigningSecret (requires manage poolId)
1847
+ // Verify secret using verifySigningSecret
1229
1848
  isCorrect = await keystoneService.verifySigningSecret({
1230
1849
  keystoneIbGib: this.ibGib,
1231
- signingSecret: masterSecret,
1232
- poolId: 'manage'
1850
+ signingSecret: masterSecret
1233
1851
  });
1234
1852
  if (!isCorrect) {
1235
1853
  tries++;
@@ -1239,6 +1857,12 @@ export class KeystoneDetailsComponentInstance extends IbGibDynamicComponentInsta
1239
1857
  this.setSSOStatus("Unlink cancelled or failed (incorrect password).", "error");
1240
1858
  return;
1241
1859
  }
1860
+ this.showBusy({
1861
+ isBusy: true,
1862
+ title: 'Unlinking Provider...',
1863
+ msg: `Removing ${providerName} custodian pool and delegate keys...`,
1864
+ animationEmoji: '🔓'
1865
+ });
1242
1866
  this.setSSOStatus("Evolving identity locally and removing custodian pool...", "info");
1243
1867
  const metaspace = await getGlobalMetaspace_waitIfNeeded();
1244
1868
  if (!metaspace)
@@ -1256,31 +1880,59 @@ export class KeystoneDetailsComponentInstance extends IbGibDynamicComponentInsta
1256
1880
  metaspace,
1257
1881
  space
1258
1882
  });
1259
- // Unregister any corresponding outward delegate for this provider if present
1260
- const delegatesMap = this.ibGib?.data?.delegates;
1883
+ const primaryTjpAddr = (this.ibGib ? getTjpAddr({ ibGib: this.ibGib }) : undefined)
1884
+ || (this.ibGibAddr ? (() => {
1885
+ const { ib } = getIbAndGib({ ibGibAddr: this.ibGibAddr });
1886
+ const { tjpGib } = getGibInfo({ ibGibAddr: this.ibGibAddr });
1887
+ return tjpGib && ib ? `${ib}^${tjpGib}` : this.ibGibAddr;
1888
+ })() : undefined)
1889
+ || getIbGibAddr({ ibGib: this.ibGib });
1890
+ // 3. Sync pool removal frame to server
1891
+ this.showBusy({
1892
+ isBusy: true,
1893
+ title: 'Syncing to Server...',
1894
+ msg: `Publishing ${providerName} pool removal to server...`,
1895
+ animationEmoji: '⚡'
1896
+ });
1897
+ this.setSSOStatus("Publishing evolved identity keystone to server...", "info");
1898
+ let syncRes = await getApiBridge().putEvolveKeystone({
1899
+ domainAddr: primaryTjpAddr,
1900
+ keystoneIbGib: evolvedParent,
1901
+ relatedKeystoneIbGibs: [] // No outward delegate timelines
1902
+ });
1903
+ if (!syncRes.success) {
1904
+ throw new Error(`Server synchronization rejected: ${syncRes.message}`);
1905
+ }
1906
+ // 4. Unregister any corresponding outward delegate for this provider if present
1907
+ const delegatesMap = evolvedParent.data?.delegates || this.ibGib?.data?.delegates;
1261
1908
  if (delegatesMap) {
1262
1909
  for (const [delegateTjpAddr, info] of Object.entries(delegatesMap)) {
1263
- if (info.deviceName?.includes(providerId)) {
1910
+ if (isCustodialDelegate({ delegateInfo: info, providerId })) {
1264
1911
  evolvedParent = await keystoneService.unregisterDelegate({
1265
1912
  parentKeystone: evolvedParent,
1266
1913
  delegateTjpAddr,
1267
1914
  masterSecret: masterSecret,
1915
+ signingPoolId: 'manage',
1268
1916
  metaspace,
1269
1917
  space,
1270
1918
  });
1919
+ this.showBusy({
1920
+ isBusy: true,
1921
+ title: 'Syncing to Server...',
1922
+ msg: `Publishing ${providerName} delegate unregistration to server...`,
1923
+ animationEmoji: '⚡'
1924
+ });
1925
+ syncRes = await getApiBridge().putEvolveKeystone({
1926
+ domainAddr: primaryTjpAddr,
1927
+ keystoneIbGib: evolvedParent,
1928
+ relatedKeystoneIbGibs: []
1929
+ });
1930
+ if (!syncRes.success) {
1931
+ throw new Error(`Server synchronization rejected: ${syncRes.message}`);
1932
+ }
1271
1933
  }
1272
1934
  }
1273
1935
  }
1274
- // 3. Sync evolved parent keystone back to server
1275
- this.setSSOStatus("Publishing evolved identity keystone to server...", "info");
1276
- const syncRes = await getApiBridge().putEvolveKeystone({
1277
- domainAddr: getIbGibAddr({ ibGib: this.ibGib }),
1278
- keystoneIbGib: evolvedParent,
1279
- relatedKeystoneIbGibs: [] // No outward delegate timelines
1280
- });
1281
- if (!syncRes.success) {
1282
- throw new Error(`Server synchronization rejected: ${syncRes.message}`);
1283
- }
1284
1936
  // 4. Update index locally
1285
1937
  await updateSpecialIndex({
1286
1938
  type: "keystones",
@@ -1293,12 +1945,17 @@ export class KeystoneDetailsComponentInstance extends IbGibDynamicComponentInsta
1293
1945
  metaspace,
1294
1946
  space
1295
1947
  });
1296
- this.setSSOStatus(`Successfully unlinked ${providerId}!`, "success");
1297
- await this.loadIbGib({ getLatest: true });
1948
+ this.setSSOStatus(`Successfully unlinked ${providerName}!`, "success");
1949
+ this.ibGibAddr = getIbGibAddr({ ibGib: evolvedParent });
1950
+ this.staticIbGib = evolvedParent;
1951
+ await this.loadIbGib({ getLatest: false });
1298
1952
  }
1299
1953
  catch (error) {
1300
- console.error(`${lc} SSO unlink failed: ${extractErrorMsg(error)}`);
1301
- this.setSSOStatus(`Unlink failed: ${extractErrorMsg(error)}`, "error");
1954
+ console.error(`${lc} ${extractErrorMsg(error)}`, error);
1955
+ this.setSSOStatus(`Failed to unlink ${providerName}: ${extractErrorMsg(error)}`, "error");
1956
+ }
1957
+ finally {
1958
+ this.showBusy({ isBusy: false });
1302
1959
  }
1303
1960
  }
1304
1961
  setChangePasswordStatus(msg, type) {
@@ -1342,8 +1999,16 @@ export class KeystoneDetailsComponentInstance extends IbGibDynamicComponentInsta
1342
1999
  config: initialManagePool.config,
1343
2000
  challenges: newChallenges,
1344
2001
  };
1345
- const primaryTjpAddr = getTjpAddr({ ibGib: this.ibGib }) || getIbGibAddr({ ibGib: this.ibGib });
1346
- const custodianProvider = this.aggregatedDetails?.custodianProvider || 'google';
2002
+ const primaryTjpAddr = (this.ibGib ? getTjpAddr({ ibGib: this.ibGib }) : undefined)
2003
+ || (this.ibGibAddr ? (() => {
2004
+ const { ib } = getIbAndGib({ ibGibAddr: this.ibGibAddr });
2005
+ const { tjpGib } = getGibInfo({ ibGibAddr: this.ibGibAddr });
2006
+ return tjpGib && ib ? `${ib}^${tjpGib}` : this.ibGibAddr;
2007
+ })() : undefined)
2008
+ || getIbGibAddr({ ibGib: this.ibGib });
2009
+ const custodianProvider = this.aggregatedDetails?.custodianProvider
2010
+ || this.ibGib?.data?.frameDetails?.custodianProvider
2011
+ || this.ibGib?.data?.custodianProvider;
1347
2012
  let requestBody = {
1348
2013
  primaryTjpAddr,
1349
2014
  newManagePool,
@@ -1370,15 +2035,60 @@ export class KeystoneDetailsComponentInstance extends IbGibDynamicComponentInsta
1370
2035
  requestBody.redirectUri = resPopup.redirectUri;
1371
2036
  }
1372
2037
  else {
1373
- this.showBusy({ isBusy: false });
1374
- // Email OTP fallback
1375
- const email = this.aggregatedDetails?.emails?.[0] || this.ibGib?.data?.emails?.[0];
1376
- const otpCode = await promptForSecret({
1377
- msg: `Enter 6-digit verification code sent to ${email}:`,
1378
- confirm: false,
1379
- });
1380
- if (!otpCode) {
1381
- this.setChangePasswordStatus("Verification code required.", "error");
2038
+ // Email OTP flow
2039
+ const email = this.aggregatedDetails?.emails?.[0]
2040
+ || this.ibGib?.data?.emails?.[0]
2041
+ || this.ibGib?.data?.frameDetails?.emails?.[0];
2042
+ if (!email) {
2043
+ this.showBusy({ isBusy: false });
2044
+ this.setChangePasswordStatus("No email address found for this identity to send verification code.", "error");
2045
+ return;
2046
+ }
2047
+ const otpSection = this.elements?.sectionAddPasswordOtpEl;
2048
+ const otpInput = this.elements?.inputAddPasswordOtpEl;
2049
+ const isOtpVisible = otpSection && !otpSection.classList.contains('hidden');
2050
+ // Step 1: If OTP section is not visible yet, send code and show the OTP input inline in the card
2051
+ if (!isOtpVisible) {
2052
+ this.showBusy({
2053
+ isBusy: true,
2054
+ title: 'Sending Verification Code...',
2055
+ msg: `Sending 6-digit verification code to ${email}...`,
2056
+ animationEmoji: '📧'
2057
+ });
2058
+ this.setChangePasswordStatus(`Sending verification code to ${email}...`, "info");
2059
+ const resCode = await getApiBridge().postVerificationCode({
2060
+ email,
2061
+ primaryTjpAddr,
2062
+ });
2063
+ this.showBusy({ isBusy: false });
2064
+ if (!resCode.success) {
2065
+ this.setChangePasswordStatus(resCode.message || "Failed to send verification code.", "error");
2066
+ return;
2067
+ }
2068
+ if (otpSection) {
2069
+ otpSection.classList.remove('hidden');
2070
+ }
2071
+ if (this.elements?.descAddPasswordOtpEl) {
2072
+ this.elements.descAddPasswordOtpEl.textContent = `A 6-digit verification code was sent to ${email}. Enter it below to establish sovereign ownership:`;
2073
+ }
2074
+ if (this.elements?.btnChangePasswordEl) {
2075
+ this.elements.btnChangePasswordEl.textContent = "Verify Code & Add Master Password";
2076
+ }
2077
+ this.setChangePasswordStatus(`✉️ 6-digit verification code sent to ${email}. Enter the code and click "Verify Code & Add Master Password".`, "info");
2078
+ this.startResendCooldownTimer(30);
2079
+ if (otpInput) {
2080
+ otpInput.value = '';
2081
+ otpInput.focus();
2082
+ }
2083
+ return;
2084
+ }
2085
+ // Step 2: OTP section is visible - validate entered code
2086
+ const otpCode = (otpInput?.value || '').trim();
2087
+ if (!otpCode || otpCode.length < 6) {
2088
+ this.setChangePasswordStatus("Please enter the complete 6-digit verification code sent to your email.", "error");
2089
+ if (otpInput) {
2090
+ otpInput.focus();
2091
+ }
1382
2092
  return;
1383
2093
  }
1384
2094
  requestBody.email = email;
@@ -1410,20 +2120,88 @@ export class KeystoneDetailsComponentInstance extends IbGibDynamicComponentInsta
1410
2120
  if (!space) {
1411
2121
  throw new Error("Local user space not available");
1412
2122
  }
1413
- await metaspace.put({ ibGibs: [evolvedParent], space });
1414
- await metaspace.registerNewIbGib({ ibGib: evolvedParent, space });
2123
+ if (resJson.primaryGraph) {
2124
+ const graphIbGibs = Object.values(resJson.primaryGraph);
2125
+ await metaspace.put({ ibGibs: graphIbGibs, space });
2126
+ const timelinesMap = getTimelinesGroupedByTjp({ ibGibs: graphIbGibs });
2127
+ for (const tjpKey of Object.keys(timelinesMap)) {
2128
+ for (const frame of timelinesMap[tjpKey]) {
2129
+ await metaspace.registerNewIbGib({ ibGib: frame, space });
2130
+ }
2131
+ }
2132
+ }
2133
+ else {
2134
+ await metaspace.put({ ibGibs: [evolvedParent], space });
2135
+ await metaspace.registerNewIbGib({ ibGib: evolvedParent, space });
2136
+ }
2137
+ // 1. Create a password-based sync delegate for this device
2138
+ this.showBusy({
2139
+ isBusy: true,
2140
+ title: 'Creating Sovereign Sync Delegate...',
2141
+ msg: 'Deriving local device sync delegate keys from Master Password...',
2142
+ animationEmoji: '🔑'
2143
+ });
2144
+ const rawUsername = this.aggregatedDetails?.username
2145
+ || this.ibGib?.data?.username
2146
+ || this.aggregatedDetails?.emails?.[0]?.split('@')[0]
2147
+ || this.ibGib?.data?.emails?.[0]?.split('@')[0]
2148
+ || 'user';
2149
+ const { delegateKeystone: userSyncDelegate, deviceTag } = await createSovereignDeviceSyncDelegate({
2150
+ username: rawUsername,
2151
+ primaryTjpAddr,
2152
+ masterSecret: newPassword,
2153
+ metaspace,
2154
+ space,
2155
+ });
2156
+ // 2. Register the new user sync delegate on the evolved parent keystone
2157
+ this.showBusy({
2158
+ isBusy: true,
2159
+ title: 'Registering Sovereign Delegate...',
2160
+ msg: 'Registering user sync delegate on primary keystone...',
2161
+ animationEmoji: '📝'
2162
+ });
2163
+ const keystoneService = new KeystoneService_V1();
2164
+ const parentWithUserDelegate = await keystoneService.registerDelegate({
2165
+ parentKeystone: evolvedParent,
2166
+ delegateKeystone: userSyncDelegate,
2167
+ masterSecret: newPassword,
2168
+ signingPoolId: 'manage',
2169
+ metaspace,
2170
+ space,
2171
+ allowedVerbs: [KEYSTONE_VERB_SYNC, KEYSTONE_VERB_CONNECT],
2172
+ deviceName: deviceTag
2173
+ });
2174
+ // 3. Post evolved keystone with new delegate to server
2175
+ this.showBusy({
2176
+ isBusy: true,
2177
+ title: 'Syncing to Server...',
2178
+ msg: 'Publishing updated domain identity and sovereign delegate to server...',
2179
+ animationEmoji: '⚡'
2180
+ });
2181
+ const resSync = await getApiBridge().putEvolveKeystone({
2182
+ domainAddr: primaryTjpAddr,
2183
+ keystoneIbGib: parentWithUserDelegate,
2184
+ relatedKeystoneIbGibs: [userSyncDelegate]
2185
+ });
2186
+ if (!resSync.success) {
2187
+ console.warn(`${lc} Warning syncing user delegate to server: ${resSync.message}`);
2188
+ }
2189
+ // 4. Save parentWithUserDelegate and userSyncDelegate to local space & metaspace
2190
+ await metaspace.put({ ibGibs: [parentWithUserDelegate, userSyncDelegate], space });
2191
+ await metaspace.registerNewIbGib({ ibGib: parentWithUserDelegate, space });
2192
+ await metaspace.registerNewIbGib({ ibGib: userSyncDelegate, space });
1415
2193
  await updateSpecialIndex({
1416
2194
  type: "keystones",
1417
2195
  rel8nInfos: [
1418
2196
  {
1419
2197
  rel8nName: "keystone",
1420
- ibGibs: [toDto({ ibGib: evolvedParent })]
2198
+ ibGibs: [toDto({ ibGib: parentWithUserDelegate }), toDto({ ibGib: userSyncDelegate })]
1421
2199
  }
1422
2200
  ],
1423
2201
  metaspace,
1424
2202
  space
1425
2203
  });
1426
- const evolvedAddr = getIbGibAddr({ ibGib: evolvedParent });
2204
+ const evolvedAddr = getIbGibAddr({ ibGib: parentWithUserDelegate });
1427
2205
  // Dispatch EVENT_IBGIB_IDENTITY_CHANGED
1428
2206
  window.dispatchEvent(new CustomEvent(EVENT_IBGIB_IDENTITY_CHANGED, {
1429
2207
  detail: { activeIdentityAddr: evolvedAddr },
@@ -1431,9 +2209,19 @@ export class KeystoneDetailsComponentInstance extends IbGibDynamicComponentInsta
1431
2209
  composed: true
1432
2210
  }));
1433
2211
  this.setChangePasswordStatus("Successfully added Master Password! Identity is now sovereign.", "success");
2212
+ // Reset OTP section and button text
2213
+ if (this.elements?.sectionAddPasswordOtpEl) {
2214
+ this.elements.sectionAddPasswordOtpEl.classList.add('hidden');
2215
+ if (this.elements.inputAddPasswordOtpEl) {
2216
+ this.elements.inputAddPasswordOtpEl.value = '';
2217
+ }
2218
+ }
2219
+ if (this.elements?.btnChangePasswordEl) {
2220
+ this.elements.btnChangePasswordEl.textContent = "Update Password";
2221
+ }
1434
2222
  // Refresh the view
1435
2223
  this.ibGibAddr = evolvedAddr;
1436
- this.staticIbGib = evolvedParent;
2224
+ this.staticIbGib = parentWithUserDelegate;
1437
2225
  await this.loadIbGib({ getLatest: true });
1438
2226
  // Clear inputs
1439
2227
  this.elements.inputNewPasswordEl.value = '';
@@ -1442,20 +2230,80 @@ export class KeystoneDetailsComponentInstance extends IbGibDynamicComponentInsta
1442
2230
  catch (error) {
1443
2231
  console.error(`${lc} Elevation failed: ${extractErrorMsg(error)}`);
1444
2232
  this.setChangePasswordStatus(`Failed to add master password: ${extractErrorMsg(error)}`, "error");
2233
+ if (this.elements?.inputAddPasswordOtpEl && !this.elements.sectionAddPasswordOtpEl?.classList.contains('hidden')) {
2234
+ this.elements.inputAddPasswordOtpEl.select();
2235
+ this.elements.inputAddPasswordOtpEl.focus();
2236
+ }
1445
2237
  }
1446
2238
  finally {
1447
2239
  this.showBusy({ isBusy: false });
1448
2240
  }
1449
2241
  }
2242
+ async handleResendAddPasswordOtp() {
2243
+ const lc = `${this.lc}[${this.handleResendAddPasswordOtp.name}]`;
2244
+ const email = this.aggregatedDetails?.emails?.[0]
2245
+ || this.ibGib?.data?.emails?.[0]
2246
+ || this.ibGib?.data?.frameDetails?.emails?.[0];
2247
+ if (!email) {
2248
+ this.setChangePasswordStatus("No email address found for this identity to send verification code.", "error");
2249
+ return;
2250
+ }
2251
+ const primaryTjpAddr = (this.ibGib ? getTjpAddr({ ibGib: this.ibGib }) : undefined)
2252
+ || (this.ibGibAddr ? (() => {
2253
+ const { ib } = getIbAndGib({ ibGibAddr: this.ibGibAddr });
2254
+ const { tjpGib } = getGibInfo({ ibGibAddr: this.ibGibAddr });
2255
+ return tjpGib && ib ? `${ib}^${tjpGib}` : this.ibGibAddr;
2256
+ })() : undefined)
2257
+ || getIbGibAddr({ ibGib: this.ibGib });
2258
+ const btn = this.elements.btnResendAddPasswordOtpEl;
2259
+ if (btn) {
2260
+ btn.disabled = true;
2261
+ btn.textContent = "Sending...";
2262
+ }
2263
+ this.setChangePasswordStatus(`Resending verification code to ${email}...`, "info");
2264
+ try {
2265
+ const resCode = await getApiBridge().postVerificationCode({ email, primaryTjpAddr });
2266
+ if (resCode.success) {
2267
+ this.setChangePasswordStatus(`✉️ Fresh 6-digit verification code sent to ${email}!`, "success");
2268
+ this.startResendCooldownTimer(30);
2269
+ if (this.elements?.inputAddPasswordOtpEl) {
2270
+ this.elements.inputAddPasswordOtpEl.value = '';
2271
+ this.elements.inputAddPasswordOtpEl.focus();
2272
+ }
2273
+ }
2274
+ else {
2275
+ this.setChangePasswordStatus(resCode.message || "Failed to resend verification code.", "error");
2276
+ if (btn) {
2277
+ btn.disabled = false;
2278
+ btn.textContent = "Resend Code";
2279
+ }
2280
+ }
2281
+ }
2282
+ catch (err) {
2283
+ this.setChangePasswordStatus(`Error resending verification code: ${extractErrorMsg(err)}`, "error");
2284
+ if (btn) {
2285
+ btn.disabled = false;
2286
+ btn.textContent = "Resend Code";
2287
+ }
2288
+ }
2289
+ }
1450
2290
  async handleChangePassword() {
1451
2291
  const lc = `${this.lc}[${this.handleChangePassword.name}]`;
1452
- const caps = getKeystoneCapabilities({ keystone: this.ibGib, aggregatedDetails: this.aggregatedDetails });
1453
- const isCustodialOnly = caps.isCustodial && !caps.isSovereign;
2292
+ const caps = !!this.ibGib ?
2293
+ getKeystoneCapabilities({ keystone: this.ibGib, aggregatedDetails: this.aggregatedDetails }) :
2294
+ undefined;
2295
+ const isCustodialOnly = !!caps ? caps.isCustodial && !caps.isSovereign : false;
1454
2296
  if (isCustodialOnly) {
1455
2297
  await this.handleAddMasterPassword();
1456
2298
  return;
1457
2299
  }
1458
2300
  this.setChangePasswordStatus("Validating entries...", "info");
2301
+ this.showBusy({
2302
+ isBusy: true,
2303
+ title: 'Changing Master Password...',
2304
+ msg: 'Validating current password...',
2305
+ animationEmoji: '🔐'
2306
+ });
1459
2307
  try {
1460
2308
  const currentPassword = this.elements.inputCurrentPasswordEl.value;
1461
2309
  const newPassword = this.elements.inputNewPasswordEl.value;
@@ -1483,6 +2331,12 @@ export class KeystoneDetailsComponentInstance extends IbGibDynamicComponentInsta
1483
2331
  this.setChangePasswordStatus("Incorrect current password.", "error");
1484
2332
  return;
1485
2333
  }
2334
+ this.showBusy({
2335
+ isBusy: true,
2336
+ title: 'Evolving Master Password...',
2337
+ msg: 'Generating new challenge proofs and rotating local user delegates...',
2338
+ animationEmoji: '⚙️'
2339
+ });
1486
2340
  this.setChangePasswordStatus("Evolving primary identity keystone...", "info");
1487
2341
  const metaspace = await getGlobalMetaspace_waitIfNeeded();
1488
2342
  if (!metaspace) {
@@ -1492,8 +2346,7 @@ export class KeystoneDetailsComponentInstance extends IbGibDynamicComponentInsta
1492
2346
  if (!space) {
1493
2347
  throw new Error(`(UNEXPECTED) couldn't get default local space? (E: 2fc1281c28351fd378a1d7f88eaa0526)`);
1494
2348
  }
1495
- // 2. Rotate parent identity
1496
- debugger; // walk through this...looking for delegate failure // hmm...I thought we got change password going?
2349
+ // 2. Rotate parent identity and sovereign sync delegates
1497
2350
  const { newKeystone: evolvedParent, newDelegates: evolvedDelegates } = await keystoneService.changePassword({
1498
2351
  keystoneIbGib: this.ibGib,
1499
2352
  newMasterSecret: newPassword,
@@ -1502,56 +2355,21 @@ export class KeystoneDetailsComponentInstance extends IbGibDynamicComponentInsta
1502
2355
  metaspace,
1503
2356
  space
1504
2357
  });
1505
- debugger; // look at evolvedDelegates
1506
- this.setChangePasswordStatus("Resolving and rotating delegate keystones...", "info");
1507
- // // 3. Evolve registered delegate keystones (preserving custodian/foreign delegate secrets)
1508
- // const delegates = this.ibGib!.data?.delegates || {};
1509
- // const evolvedDelegates: KeystoneIbGib_V1[] = [];
1510
- // for (const delegateInfo of Object.values(delegates)) {
1511
- // const delegateAddr = delegateInfo.delegateAddr;
1512
- // const delegateKeystone = await keystoneService.getLatestKeystone({
1513
- // addr: delegateAddr,
1514
- // metaspace,
1515
- // space,
1516
- // });
1517
- // // Identify user-owned challenge pools in the delegate that match the old password
1518
- // const poolsToRotate: string[] = [];
1519
- // for (const pool of delegateKeystone.data?.challengePools ?? []) {
1520
- // if (!pool.isForeign) {
1521
- // /**
1522
- // * WARNING: There is an extreme edge case that if two pools
1523
- // * happen to be owned by different parties but share the
1524
- // * same secret, this will change the wrong pool's secret
1525
- // * also. For V1, this shouldn't matter. But we need a
1526
- // * better way of tracking pool ownership for V2 (or even
1527
- // * V1 if we have multi-party support). (DO NOT REMOVE
1528
- // * THIS COMMENT UNTIL THIS IS ADDRESSED)
1529
- // */
1530
- // const isPoolMatch = await keystoneService.verifySigningSecret({
1531
- // keystoneIbGib: delegateKeystone,
1532
- // signingSecret: currentPassword,
1533
- // poolId: pool.id
1534
- // });
1535
- // if (isPoolMatch) { poolsToRotate.push(pool.id); }
1536
- // }
1537
- // }
1538
- // if (poolsToRotate.length > 0) {
1539
- // console.log(`${lc} Rotating pools in delegate ${delegateInfo.delegateTjpAddr}: ${poolsToRotate.join(', ')}`);
1540
- // const {newKeystone: evolvedDelegate} = await keystoneService.changePassword({
1541
- // keystoneIbGib: delegateKeystone,
1542
- // newMasterSecret: newPassword,
1543
- // signingSecret: currentPassword,
1544
- // signingPoolId: 'manage',
1545
- // poolIds: poolsToRotate,
1546
- // metaspace,
1547
- // space
1548
- // });
1549
- // evolvedDelegates.push(evolvedDelegate);
1550
- // }
1551
- // }
1552
- // 4. Sync evolved parent and all evolved delegate keystones atomically
2358
+ // 3. Sync evolved parent and all evolved delegate keystones atomically
2359
+ this.showBusy({
2360
+ isBusy: true,
2361
+ title: 'Syncing to Server...',
2362
+ msg: 'Publishing rotated identity and delegates to server...',
2363
+ animationEmoji: '⚡'
2364
+ });
1553
2365
  this.setChangePasswordStatus("Syncing evolved keystones to server...", "info");
1554
- const parentTjpAddr = getIbGibAddr({ ibGib: this.ibGib }); // is this.ibGib the tjp always? or is it the latest in the keystone line? Does it matter?
2366
+ const parentTjpAddr = (this.ibGib ? getTjpAddr({ ibGib: this.ibGib }) : undefined)
2367
+ || (this.ibGibAddr ? (() => {
2368
+ const { ib } = getIbAndGib({ ibGibAddr: this.ibGibAddr });
2369
+ const { tjpGib } = getGibInfo({ ibGibAddr: this.ibGibAddr });
2370
+ return tjpGib && ib ? `${ib}^${tjpGib}` : this.ibGibAddr;
2371
+ })() : undefined)
2372
+ || getIbGibAddr({ ibGib: this.ibGib });
1555
2373
  const syncRes = await getApiBridge().putEvolveKeystone({
1556
2374
  domainAddr: parentTjpAddr,
1557
2375
  keystoneIbGib: evolvedParent,
@@ -1560,7 +2378,7 @@ export class KeystoneDetailsComponentInstance extends IbGibDynamicComponentInsta
1560
2378
  if (!syncRes.success) {
1561
2379
  throw new Error(`Server synchronization rejected: ${syncRes.message}`);
1562
2380
  }
1563
- // 5. Update index locally
2381
+ // 4. Update index locally
1564
2382
  this.setChangePasswordStatus("Updating local indexes...", "info");
1565
2383
  const newKeystoneIbGibs = [toDto({ ibGib: evolvedParent })];
1566
2384
  if (evolvedDelegates) {
@@ -1585,12 +2403,18 @@ export class KeystoneDetailsComponentInstance extends IbGibDynamicComponentInsta
1585
2403
  this.elements.inputNewPasswordEl.value = "";
1586
2404
  this.elements.inputConfirmPasswordEl.value = "";
1587
2405
  this.setChangePasswordStatus("Password updated successfully!", "success");
2406
+ this.ibGibAddr = getIbGibAddr({ ibGib: evolvedParent });
2407
+ this.staticIbGib = evolvedParent;
1588
2408
  await this.loadIbGib({ getLatest: true });
1589
2409
  }
1590
2410
  catch (error) {
2411
+ debugger; // error in handleChangePassword
1591
2412
  console.error(`${lc} Password rotation failed: ${extractErrorMsg(error)}`);
1592
2413
  this.setChangePasswordStatus(`Update failed: ${extractErrorMsg(error)}`, "error");
1593
2414
  }
2415
+ finally {
2416
+ this.showBusy({ isBusy: false });
2417
+ }
1594
2418
  }
1595
2419
  }
1596
2420
  //# sourceMappingURL=keystone-details.mjs.map