@ibgib/web-gib 0.0.60 → 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 (89) 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.d.mts.map +1 -1
  4. package/dist/helpers.mjs +1 -0
  5. package/dist/helpers.mjs.map +1 -1
  6. package/dist/helpers.web.d.mts +3 -1
  7. package/dist/helpers.web.d.mts.map +1 -1
  8. package/dist/helpers.web.mjs +17 -1
  9. package/dist/helpers.web.mjs.map +1 -1
  10. package/dist/identity/device-delegate-helper.d.mts +91 -0
  11. package/dist/identity/device-delegate-helper.d.mts.map +1 -0
  12. package/dist/identity/device-delegate-helper.mjs +190 -0
  13. package/dist/identity/device-delegate-helper.mjs.map +1 -0
  14. package/dist/identity/device-delegate.respec.d.mts +2 -0
  15. package/dist/identity/device-delegate.respec.d.mts.map +1 -0
  16. package/dist/identity/device-delegate.respec.mjs +106 -0
  17. package/dist/identity/device-delegate.respec.mjs.map +1 -0
  18. package/dist/identity/keystone-sync-service.d.mts +18 -0
  19. package/dist/identity/keystone-sync-service.d.mts.map +1 -1
  20. package/dist/identity/keystone-sync-service.mjs +77 -1
  21. package/dist/identity/keystone-sync-service.mjs.map +1 -1
  22. package/dist/identity/passkey-helper.d.mts +72 -0
  23. package/dist/identity/passkey-helper.d.mts.map +1 -0
  24. package/dist/identity/passkey-helper.mjs +237 -0
  25. package/dist/identity/passkey-helper.mjs.map +1 -0
  26. package/dist/identity/passkey.respec.d.mts +2 -0
  27. package/dist/identity/passkey.respec.d.mts.map +1 -0
  28. package/dist/identity/passkey.respec.mjs +169 -0
  29. package/dist/identity/passkey.respec.mjs.map +1 -0
  30. package/dist/index.d.mts +2 -1
  31. package/dist/index.d.mts.map +1 -1
  32. package/dist/index.mjs +2 -2
  33. package/dist/index.mjs.map +1 -1
  34. package/dist/storage/storage-helpers.web.d.mts +9 -0
  35. package/dist/storage/storage-helpers.web.d.mts.map +1 -1
  36. package/dist/storage/storage-helpers.web.mjs +42 -0
  37. package/dist/storage/storage-helpers.web.mjs.map +1 -1
  38. package/dist/ui/component/identity/add-device/add-device.d.mts.map +1 -1
  39. package/dist/ui/component/identity/add-device/add-device.mjs +31 -43
  40. package/dist/ui/component/identity/add-device/add-device.mjs.map +1 -1
  41. package/dist/ui/component/identity/keystone-creator/keystone-creator.d.mts +18 -1
  42. package/dist/ui/component/identity/keystone-creator/keystone-creator.d.mts.map +1 -1
  43. package/dist/ui/component/identity/keystone-creator/keystone-creator.html +42 -3
  44. package/dist/ui/component/identity/keystone-creator/keystone-creator.mjs +524 -104
  45. package/dist/ui/component/identity/keystone-creator/keystone-creator.mjs.map +1 -1
  46. package/dist/ui/component/identity/keystone-details/keystone-details.d.mts +19 -1
  47. package/dist/ui/component/identity/keystone-details/keystone-details.d.mts.map +1 -1
  48. package/dist/ui/component/identity/keystone-details/keystone-details.html +38 -0
  49. package/dist/ui/component/identity/keystone-details/keystone-details.mjs +1217 -132
  50. package/dist/ui/component/identity/keystone-details/keystone-details.mjs.map +1 -1
  51. package/dist/ui/component/identity/remove-device/remove-device.d.mts.map +1 -1
  52. package/dist/ui/component/identity/remove-device/remove-device.mjs +1 -0
  53. package/dist/ui/component/identity/remove-device/remove-device.mjs.map +1 -1
  54. package/dist/ui/ui-constants.d.mts +1 -0
  55. package/dist/ui/ui-constants.d.mts.map +1 -1
  56. package/dist/ui/ui-constants.mjs +1 -0
  57. package/dist/ui/ui-constants.mjs.map +1 -1
  58. package/dist/ui/ui-helpers.d.mts +3 -0
  59. package/dist/ui/ui-helpers.d.mts.map +1 -1
  60. package/dist/ui/ui-helpers.mjs +110 -6
  61. package/dist/ui/ui-helpers.mjs.map +1 -1
  62. package/dist/ui/ui-types.d.mts +5 -0
  63. package/dist/ui/ui-types.d.mts.map +1 -1
  64. package/package.json +2 -2
  65. package/src/AUTO-GENERATED-version.mts +1 -1
  66. package/src/helpers.mts +1 -0
  67. package/src/helpers.web.mts +22 -2
  68. package/src/identity/custodian-delegate-helper.mts.bak +7 -0
  69. package/src/identity/device-delegate-helper.mts +268 -0
  70. package/src/identity/device-delegate.respec.mts +125 -0
  71. package/src/identity/keystone-sync-service.mts +95 -2
  72. package/src/identity/passkey-helper.mts +304 -0
  73. package/src/identity/passkey.respec.mts +194 -0
  74. package/src/index.mts +2 -2
  75. package/src/storage/storage-helpers.web.mts +47 -0
  76. package/src/ui/component/identity/add-device/add-device.mts +37 -48
  77. package/src/ui/component/identity/keystone-creator/keystone-creator.html +42 -3
  78. package/src/ui/component/identity/keystone-creator/keystone-creator.mts +568 -111
  79. package/src/ui/component/identity/keystone-details/keystone-details.html +38 -0
  80. package/src/ui/component/identity/keystone-details/keystone-details.mts +1290 -142
  81. package/src/ui/component/identity/remove-device/remove-device.mts +1 -0
  82. package/src/ui/ui-constants.mts +1 -0
  83. package/src/ui/ui-helpers.mts +118 -5
  84. package/src/ui/ui-types.mts +6 -0
  85. package/dist/identity/custodian-delegate-helper.d.mts +0 -22
  86. package/dist/identity/custodian-delegate-helper.d.mts.map +0 -1
  87. package/dist/identity/custodian-delegate-helper.mjs +0 -67
  88. package/dist/identity/custodian-delegate-helper.mjs.map +0 -1
  89. package/src/identity/custodian-delegate-helper.mts +0 -86
@@ -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,13 +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 } from "../../../../identity/sso/sso-helpers.mjs";
18
+ import { isSsoProviderLinked, getKeystoneCapabilities, isDelegateExpired, isCustodialDelegate } from "@ibgib/core-gib/dist/keystone/delegate/delegate-helpers.mjs";
19
+ import { generatePoolChallenges } from "@ibgib/core-gib/dist/keystone/keystone-helpers.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";
19
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";
20
26
  const logalot = false;
21
27
  export const KEYSTONE_DETAILS_COMPONENT_NAME = 'ibgib-keystone-details';
22
28
  export class KeystoneDetailsComponentMeta extends IbGibDynamicComponentMetaBase {
@@ -51,6 +57,8 @@ export class KeystoneDetailsComponentInstance extends IbGibDynamicComponentInsta
51
57
  isHistoricalFrame = false;
52
58
  _onIdentityChanged = null;
53
59
  keystoneCache = new Map();
60
+ aggregatedDetails;
61
+ resendCooldownInterval = null;
54
62
  constructor() {
55
63
  super();
56
64
  }
@@ -74,6 +82,39 @@ export class KeystoneDetailsComponentInstance extends IbGibDynamicComponentInsta
74
82
  }
75
83
  }
76
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
+ }
77
118
  async getKeystoneDetails(addr) {
78
119
  const cached = this.keystoneCache.get(addr);
79
120
  if (cached)
@@ -139,6 +180,10 @@ export class KeystoneDetailsComponentInstance extends IbGibDynamicComponentInsta
139
180
  this.elements.rawKeystoneJsonEl = shadow.getElementById('raw-keystone-json');
140
181
  this.elements.delegatesCardEl = shadow.getElementById('delegates-card');
141
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');
142
187
  this.elements.containerEl = shadow.getElementById('container');
143
188
  this.elements.historicalWarningBannerEl = shadow.getElementById('historical-warning-banner');
144
189
  this.elements.ssoLinkCardEl = shadow.getElementById('sso-link-card');
@@ -151,6 +196,10 @@ export class KeystoneDetailsComponentInstance extends IbGibDynamicComponentInsta
151
196
  this.elements.btnChangePasswordEl = shadow.getElementById('btn-change-password');
152
197
  this.elements.changePasswordStatusEl = shadow.getElementById('change-password-status');
153
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');
154
203
  // ElementBase requires contentEl
155
204
  this.elements.contentEl = shadow.getElementById('identity-details-view');
156
205
  this.initHandlers();
@@ -175,11 +224,6 @@ export class KeystoneDetailsComponentInstance extends IbGibDynamicComponentInsta
175
224
  }
176
225
  }
177
226
  }
178
- async disconnected() {
179
- if (this._onIdentityChanged) {
180
- window.removeEventListener(EVENT_IBGIB_IDENTITY_CHANGED, this._onIdentityChanged);
181
- }
182
- }
183
227
  initHandlers() {
184
228
  // Setup raw JSON accordion toggle
185
229
  this.elements.rawAccordionHeaderEl.addEventListener('click', () => this.toggleAccordion());
@@ -188,9 +232,11 @@ export class KeystoneDetailsComponentInstance extends IbGibDynamicComponentInsta
188
232
  this.elements.btnSetActiveEl.addEventListener('click', () => this.handleSetActiveIdentity());
189
233
  this.elements.btnCheckUpdatesEl.addEventListener('click', () => this.handleCheckForUpdates());
190
234
  this.elements.btnUpdateIdentityEl.addEventListener('click', () => this.handleUpdateIdentity());
235
+ this.elements.btnAddPasskeyEl?.addEventListener('click', () => this.handleAddPasskey());
191
236
  this.elements.btnLinkGoogleEl.addEventListener('click', () => this.handleSSOClick('google'));
192
237
  this.elements.btnLinkGithubEl.addEventListener('click', () => this.handleSSOClick('github'));
193
238
  this.elements.btnChangePasswordEl.addEventListener('click', () => this.handleChangePassword());
239
+ this.elements.btnResendAddPasswordOtpEl?.addEventListener('click', () => this.handleResendAddPasswordOtp());
194
240
  }
195
241
  toggleAccordion() {
196
242
  const header = this.elements.rawAccordionHeaderEl;
@@ -394,34 +440,112 @@ export class KeystoneDetailsComponentInstance extends IbGibDynamicComponentInsta
394
440
  this.elements.btnSetActiveEl.disabled = isTimelineActive;
395
441
  this.elements.btnLinkGoogleEl.classList.remove('disabled');
396
442
  this.elements.btnLinkGithubEl.classList.remove('disabled');
397
- // Check if already linked
398
- const isGoogleLinked = isSsoProviderLinked({ keystone: this.ibGib, providerId: 'google' });
399
- const isGithubLinked = isSsoProviderLinked({ keystone: this.ibGib, providerId: 'github' });
443
+ // Check if origin provider or linked
444
+ const originProvider = this.aggregatedDetails?.custodianProvider || this.ibGib?.data?.frameDetails?.custodianProvider;
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;
400
454
  // Google Card States
401
455
  const googleCard = this.elements.btnLinkGoogleEl;
402
456
  const googleLabel = googleCard.querySelector('.toggle-state-text');
403
457
  if (isGoogleLinked) {
458
+ if (!caps) {
459
+ throw new Error(`(UNEXPECTED) isGoogleLinked but capabilities falsy? (E: cc62e8357b66860dd2cb8148c833b726)`);
460
+ }
404
461
  googleCard.classList.remove('unlinked');
405
462
  googleCard.classList.add('linked');
406
- googleLabel.textContent = "Google Linked ✅";
463
+ const isOrigin = originProvider === 'google';
464
+ if (isOrigin && !caps.isSovereign) {
465
+ googleLabel.textContent = "Google (Primary Account) 🔒";
466
+ googleCard.title = "Cannot unlink primary account without first adding a Master Password below.";
467
+ }
468
+ else if (isOrigin) {
469
+ googleLabel.textContent = "Google (Primary Account) ✅";
470
+ googleCard.title = "Click to unlink Google provider (requires Master Password).";
471
+ }
472
+ else {
473
+ googleLabel.textContent = "Google Linked ✅";
474
+ googleCard.title = "Click to unlink Google provider.";
475
+ }
407
476
  }
408
477
  else {
409
478
  googleCard.classList.remove('linked');
410
479
  googleCard.classList.add('unlinked');
411
480
  googleLabel.textContent = "Unlinked";
481
+ googleCard.title = "Click to link Google account.";
412
482
  }
413
483
  // GitHub Card States
414
484
  const githubCard = this.elements.btnLinkGithubEl;
415
485
  const githubLabel = githubCard.querySelector('.toggle-state-text');
416
486
  if (isGithubLinked) {
487
+ if (!caps) {
488
+ throw new Error(`(UNEXPECTED) isGithubLinked but capabilities falsy? (E: e94688210538e0e82878088873a46826)`);
489
+ }
417
490
  githubCard.classList.remove('unlinked');
418
491
  githubCard.classList.add('linked');
419
- githubLabel.textContent = "GitHub Linked ✅";
492
+ const isOrigin = originProvider === 'github';
493
+ if (isOrigin && !caps.isSovereign) {
494
+ githubLabel.textContent = "GitHub (Primary Account) 🔒";
495
+ githubCard.title = "Cannot unlink primary account without first adding a Master Password below.";
496
+ }
497
+ else if (isOrigin) {
498
+ githubLabel.textContent = "GitHub (Primary Account) ✅";
499
+ githubCard.title = "Click to unlink GitHub provider (requires Master Password).";
500
+ }
501
+ else {
502
+ githubLabel.textContent = "GitHub Linked ✅";
503
+ githubCard.title = "Click to unlink GitHub provider.";
504
+ }
420
505
  }
421
506
  else {
422
507
  githubCard.classList.remove('linked');
423
508
  githubCard.classList.add('unlinked');
424
509
  githubLabel.textContent = "Unlinked";
510
+ githubCard.title = "Click to link GitHub account.";
511
+ }
512
+ }
513
+ }
514
+ updatePasswordCardView() {
515
+ const caps = !!this.ibGib ?
516
+ getKeystoneCapabilities({ keystone: this.ibGib, aggregatedDetails: this.aggregatedDetails }) :
517
+ false;
518
+ const cardTitleEl = this.elements?.changePasswordCardEl?.querySelector('h3');
519
+ const cardDescEl = this.elements?.changePasswordCardEl?.querySelector('.description');
520
+ const currentPasswordGroup = this.elements?.inputCurrentPasswordEl?.closest('.form-group');
521
+ const btnChangePassword = this.elements?.btnChangePasswordEl;
522
+ const isCustodialOnly = !!caps ?
523
+ caps.isCustodial && !caps.isSovereign :
524
+ false;
525
+ if (isCustodialOnly) {
526
+ if (cardTitleEl)
527
+ cardTitleEl.textContent = "Add Master Password";
528
+ if (cardDescEl)
529
+ cardDescEl.textContent = "Establish sovereign ownership by adding a master password. This enables direct, independent cryptographic management without relying solely on custodian authentication.";
530
+ if (currentPasswordGroup)
531
+ currentPasswordGroup.style.display = 'none';
532
+ if (btnChangePassword)
533
+ btnChangePassword.textContent = "Add Master Password";
534
+ }
535
+ else {
536
+ if (cardTitleEl)
537
+ cardTitleEl.textContent = "Change Password";
538
+ if (cardDescEl)
539
+ cardDescEl.textContent = "Rotate your local password that drives this identity. This will cryptographically evolve your primary identity and all registered user-owned delegate keystones to the new password.";
540
+ if (currentPasswordGroup)
541
+ currentPasswordGroup.style.display = '';
542
+ if (btnChangePassword)
543
+ btnChangePassword.textContent = "Update Password";
544
+ }
545
+ if (this.elements?.sectionAddPasswordOtpEl) {
546
+ this.elements.sectionAddPasswordOtpEl.classList.add('hidden');
547
+ if (this.elements.inputAddPasswordOtpEl) {
548
+ this.elements.inputAddPasswordOtpEl.value = '';
425
549
  }
426
550
  }
427
551
  }
@@ -484,6 +608,7 @@ export class KeystoneDetailsComponentInstance extends IbGibDynamicComponentInsta
484
608
  metaspace,
485
609
  space
486
610
  });
611
+ this.aggregatedDetails = aggregated;
487
612
  this.elements.identityAggrDetailsEl.textContent = aggregated ? pretty(aggregated) : '{}';
488
613
  // Update Username and Description fields
489
614
  this.elements.identityNameEl.textContent = aggregated?.username || aggregated?.profile?.name || aggregated?.name || '-';
@@ -491,18 +616,21 @@ export class KeystoneDetailsComponentInstance extends IbGibDynamicComponentInsta
491
616
  }
492
617
  catch (err) {
493
618
  console.warn(`${lc} Error aggregating details: ${extractErrorMsg(err)}`);
619
+ this.aggregatedDetails = undefined;
494
620
  this.elements.identityAggrDetailsEl.textContent = '{}';
495
621
  this.elements.identityNameEl.textContent = '-';
496
622
  this.elements.identityDescriptionEl.textContent = '-';
497
623
  }
498
624
  }
499
625
  else {
626
+ this.aggregatedDetails = undefined;
500
627
  this.elements.identityAggrDetailsEl.textContent = '{}';
501
628
  this.elements.identityNameEl.textContent = '-';
502
629
  this.elements.identityDescriptionEl.textContent = '-';
503
630
  }
504
631
  }
505
632
  else {
633
+ this.aggregatedDetails = undefined;
506
634
  this.elements.identityAggrDetailsEl.textContent = '{}';
507
635
  this.elements.identityNameEl.textContent = '-';
508
636
  this.elements.identityDescriptionEl.textContent = '-';
@@ -525,16 +653,20 @@ export class KeystoneDetailsComponentInstance extends IbGibDynamicComponentInsta
525
653
  this.elements.rawKeystoneJsonEl.textContent = pretty(keystone);
526
654
  // Render Challenge Pools
527
655
  this.renderChallengePools(keystone.data?.challengePools || []);
528
- // 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)
529
657
  const details = await this.getKeystoneDetails(this.ibGibAddr);
530
658
  if (details.isPrimary) {
531
659
  this.elements.delegatesCardEl.classList.remove('hidden');
532
660
  await this.renderDelegates(keystone.data?.delegates);
661
+ this.elements.passkeysCardEl.classList.remove('hidden');
662
+ await this.renderPasskeys(this.aggregatedDetails?.passkeys);
533
663
  this.elements.ssoLinkCardEl.classList.remove('hidden');
534
664
  this.elements.changePasswordCardEl.classList.remove('hidden');
665
+ this.updatePasswordCardView();
535
666
  }
536
667
  else {
537
668
  this.elements.delegatesCardEl.classList.add('hidden');
669
+ this.elements.passkeysCardEl.classList.add('hidden');
538
670
  this.elements.ssoLinkCardEl.classList.add('hidden');
539
671
  this.elements.changePasswordCardEl.classList.add('hidden');
540
672
  }
@@ -554,6 +686,7 @@ export class KeystoneDetailsComponentInstance extends IbGibDynamicComponentInsta
554
686
  if (!this.elements) {
555
687
  return;
556
688
  }
689
+ this.aggregatedDetails = undefined;
557
690
  this.isHistoricalFrame = false;
558
691
  if (this.elements.historicalWarningBannerEl) {
559
692
  this.elements.historicalWarningBannerEl.classList.add('hidden');
@@ -572,6 +705,15 @@ export class KeystoneDetailsComponentInstance extends IbGibDynamicComponentInsta
572
705
  if (this.elements.delegatesListEl) {
573
706
  this.elements.delegatesListEl.innerHTML = '';
574
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
+ }
575
717
  }
576
718
  renderChallengePools(pools) {
577
719
  if (!this.elements) {
@@ -730,39 +872,66 @@ export class KeystoneDetailsComponentInstance extends IbGibDynamicComponentInsta
730
872
  return;
731
873
  }
732
874
  const metaspace = await getGlobalMetaspace_waitIfNeeded();
733
- let localSpaceId;
734
- if (metaspace) {
735
- const space = await metaspace.getLocalUserSpace({ lock: false });
736
- if (space?.ib) {
737
- localSpaceId = parseSpaceIb({ spaceIb: space.ib }).spaceId;
738
- }
875
+ if (!metaspace) {
876
+ throw new Error(`(UNEXPECTED) metaspace falsy? (E: 8a9b0c1d2e3f4a5b6c7d8e9f0a1b2c3e)`);
739
877
  }
878
+ const space = await metaspace.getLocalUserSpace({ lock: false });
879
+ if (!space) {
880
+ throw new Error(`(UNEXPECTED) space falsy? (E: 9b0c1d2e3f4a5b6c7d8e9f0a1b2c3d4f)`);
881
+ }
882
+ const localSpaceId = space.ib ? parseSpaceIb({ spaceIb: space.ib }).spaceId : undefined;
740
883
  for (const [delegateTjpAddr, info] of Object.entries(delegates)) {
741
884
  const tr = document.createElement('tr');
742
885
  const deviceName = info.deviceName || info.details?.deviceTag || info.details?.name || 'Device Delegate';
743
886
  const tdName = document.createElement('td');
744
887
  const delegateSpaceId = info.spaceId || info.details?.spaceId;
745
- let badgeEl = null;
746
- if (delegateSpaceId) {
747
- badgeEl = document.createElement('span');
748
- badgeEl.style.marginLeft = '0.5rem';
749
- badgeEl.style.padding = '0.15rem 0.4rem';
750
- badgeEl.style.borderRadius = '4px';
751
- badgeEl.style.fontSize = '0.75rem';
752
- badgeEl.style.fontWeight = 'bold';
753
- if (localSpaceId && delegateSpaceId === localSpaceId) {
754
- badgeEl.textContent = 'This Device';
755
- badgeEl.className = 'badge badge-this-device';
756
- badgeEl.style.backgroundColor = '#1b5e20';
757
- badgeEl.style.color = '#a5d6a7';
888
+ // Check if delegate is present in local space (via spaceId match or direct presence check)
889
+ let isThisDevice = false;
890
+ if (localSpaceId && delegateSpaceId === localSpaceId) {
891
+ isThisDevice = true;
892
+ }
893
+ else {
894
+ try {
895
+ const resGet = await metaspace.get({ addrs: [delegateTjpAddr], space });
896
+ isThisDevice = !!(resGet.success && resGet.ibGibs && resGet.ibGibs.length > 0);
758
897
  }
759
- else {
760
- badgeEl.textContent = 'Remote Device';
761
- badgeEl.className = 'badge badge-remote-device';
762
- badgeEl.style.backgroundColor = '#0d47a1';
763
- badgeEl.style.color = '#90caf9';
898
+ catch {
899
+ isThisDevice = false;
764
900
  }
765
901
  }
902
+ const badgeEl = document.createElement('span');
903
+ badgeEl.style.marginLeft = '0.5rem';
904
+ badgeEl.style.padding = '0.15rem 0.4rem';
905
+ badgeEl.style.borderRadius = '4px';
906
+ badgeEl.style.fontSize = '0.75rem';
907
+ badgeEl.style.fontWeight = 'bold';
908
+ if (isThisDevice) {
909
+ badgeEl.textContent = 'This Device';
910
+ badgeEl.className = 'badge badge-this-device';
911
+ badgeEl.style.backgroundColor = '#1b5e20';
912
+ badgeEl.style.color = '#a5d6a7';
913
+ }
914
+ else {
915
+ badgeEl.textContent = 'Remote Device';
916
+ badgeEl.className = 'badge badge-remote-device';
917
+ badgeEl.style.backgroundColor = '#0d47a1';
918
+ badgeEl.style.color = '#90caf9';
919
+ }
920
+ // Check expiration
921
+ const isExpired = isDelegateExpired({ delegateInfo: info });
922
+ let expiredBadgeEl = null;
923
+ if (isExpired) {
924
+ expiredBadgeEl = document.createElement('span');
925
+ expiredBadgeEl.textContent = 'Expired';
926
+ expiredBadgeEl.className = 'badge badge-expired';
927
+ expiredBadgeEl.style.marginLeft = '0.3rem';
928
+ expiredBadgeEl.style.padding = '0.15rem 0.4rem';
929
+ expiredBadgeEl.style.borderRadius = '4px';
930
+ expiredBadgeEl.style.fontSize = '0.75rem';
931
+ expiredBadgeEl.style.fontWeight = 'bold';
932
+ expiredBadgeEl.style.backgroundColor = '#b71c1c';
933
+ expiredBadgeEl.style.color = '#ef9a9a';
934
+ }
766
935
  const applyNameAndBadge = (displayName, hoverText) => {
767
936
  tdName.innerHTML = '';
768
937
  const textSpan = document.createElement('span');
@@ -771,6 +940,9 @@ export class KeystoneDetailsComponentInstance extends IbGibDynamicComponentInsta
771
940
  if (badgeEl) {
772
941
  tdName.appendChild(badgeEl);
773
942
  }
943
+ if (expiredBadgeEl) {
944
+ tdName.appendChild(expiredBadgeEl);
945
+ }
774
946
  tdName.title = hoverText || displayName;
775
947
  };
776
948
  const displayName = `💻 ${deviceName}`;
@@ -781,7 +953,7 @@ export class KeystoneDetailsComponentInstance extends IbGibDynamicComponentInsta
781
953
  const tdVerbs = document.createElement('td');
782
954
  tdVerbs.textContent = info.allowedVerbs ? info.allowedVerbs.join(', ') : '-';
783
955
  tdVerbs.title = tdVerbs.textContent;
784
- const isRemote = !!(delegateSpaceId && localSpaceId && delegateSpaceId !== localSpaceId);
956
+ const isRemote = !isThisDevice;
785
957
  const tdActions = document.createElement('td');
786
958
  const btnView = document.createElement('button');
787
959
  btnView.className = 'action-btn secondary small';
@@ -792,22 +964,22 @@ export class KeystoneDetailsComponentInstance extends IbGibDynamicComponentInsta
792
964
  this.showRemoteDelegateModal(info);
793
965
  }
794
966
  else {
795
- if (info.delegateAddr) {
967
+ if (info.delegateAddr || delegateTjpAddr) {
796
968
  let addrToLoad = info.delegateAddr;
797
- if (metaspace) {
798
- const space = await metaspace.getLocalUserSpace({ lock: false });
799
- if (space) {
800
- try {
801
- const latest = await metaspace.getLatestAddr({ addr: delegateTjpAddr, space });
802
- if (latest) {
803
- addrToLoad = latest;
804
- }
805
- }
806
- catch (err) {
807
- console.warn(`${lc} Error resolving latest delegate address: ${extractErrorMsg(err)}`);
808
- }
969
+ try {
970
+ const keystoneService = new KeystoneService_V1();
971
+ const latestKeystone = await keystoneService.getLatestKeystone({
972
+ addr: delegateTjpAddr,
973
+ metaspace,
974
+ space
975
+ });
976
+ if (latestKeystone) {
977
+ addrToLoad = getIbGibAddr({ ibGib: latestKeystone });
809
978
  }
810
979
  }
980
+ catch (err) {
981
+ console.warn(`${lc} Error resolving latest delegate keystone: ${extractErrorMsg(err)}`);
982
+ }
811
983
  this.dispatchEvent(new CustomEvent('ibgib-view-keystone-details', {
812
984
  detail: { addr: addrToLoad },
813
985
  bubbles: true,
@@ -841,8 +1013,9 @@ export class KeystoneDetailsComponentInstance extends IbGibDynamicComponentInsta
841
1013
  let masterSecret;
842
1014
  try {
843
1015
  masterSecret = await promptForSecret({
844
- msg: `Enter Master Password to revoke delegate '${deviceName}':`,
845
- confirm: false
1016
+ msg: `Enter Master Password or touch security key to revoke delegate '${deviceName}':`,
1017
+ confirm: false,
1018
+ passkeys: this.aggregatedDetails?.passkeys,
846
1019
  });
847
1020
  }
848
1021
  catch (err) {
@@ -861,11 +1034,30 @@ export class KeystoneDetailsComponentInstance extends IbGibDynamicComponentInsta
861
1034
  throw new Error("Local space not found");
862
1035
  }
863
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
+ }
864
1055
  // Unregister delegate from parent keystone
865
1056
  const evolvedParent = await keystoneSvc.unregisterDelegate({
866
1057
  parentKeystone: this.ibGib,
867
1058
  delegateTjpAddr,
868
1059
  masterSecret,
1060
+ signingPoolId: matchedPoolId,
869
1061
  metaspace,
870
1062
  space
871
1063
  });
@@ -911,6 +1103,452 @@ export class KeystoneDetailsComponentInstance extends IbGibDynamicComponentInsta
911
1103
  this.setSyncStatus(`Revocation failed: ${extractErrorMsg(error)}`, "error");
912
1104
  }
913
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
+ }
914
1552
  async handleContextUpdated() {
915
1553
  const lc = `${this.lc}[${this.handleContextUpdated.name}]`;
916
1554
  try {
@@ -955,10 +1593,24 @@ export class KeystoneDetailsComponentInstance extends IbGibDynamicComponentInsta
955
1593
  async handleSSOLink(providerId) {
956
1594
  const lc = `${this.lc}[${this.handleSSOLink.name}]`;
957
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
+ });
958
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);
959
1611
  const resPopup = await executeSsoOAuthPopup({
960
1612
  providerId,
961
- parentTjpAddr: this.ibGibAddr
1613
+ parentTjpAddr: primaryTjpAddr
962
1614
  });
963
1615
  if (!resPopup.success || !resPopup.code) {
964
1616
  this.setSSOStatus(resPopup.message || `Linking cancelled.`, "error");
@@ -970,6 +1622,9 @@ export class KeystoneDetailsComponentInstance extends IbGibDynamicComponentInsta
970
1622
  console.error(`${lc} SSO link initiation failed: ${extractErrorMsg(error)}`);
971
1623
  this.setSSOStatus(`Link failed: ${extractErrorMsg(error)}`, "error");
972
1624
  }
1625
+ finally {
1626
+ this.showBusy({ isBusy: false });
1627
+ }
973
1628
  }
974
1629
  setSSOStatus(msg, type) {
975
1630
  const statusEl = this.elements.ssoStatusMsgEl;
@@ -979,7 +1634,14 @@ export class KeystoneDetailsComponentInstance extends IbGibDynamicComponentInsta
979
1634
  }
980
1635
  async handleSSOLinkCallback(code, stateObj, redirectUri) {
981
1636
  const lc = `${this.lc}[${this.handleSSOLinkCallback.name}]`;
982
- 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
+ });
983
1645
  try {
984
1646
  // 1. Exchange OAuth code for custodian public pool config
985
1647
  const res = await fetch('/api/identity/sso/link', {
@@ -1003,6 +1665,7 @@ export class KeystoneDetailsComponentInstance extends IbGibDynamicComponentInsta
1003
1665
  }
1004
1666
  const custodianPool = linkData.custodianPool;
1005
1667
  // 2. Prompt user for master secret and verify it
1668
+ this.showBusy({ isBusy: false });
1006
1669
  const keystoneService = new KeystoneService_V1();
1007
1670
  let masterSecret;
1008
1671
  let isCorrect = false;
@@ -1043,6 +1706,12 @@ export class KeystoneDetailsComponentInstance extends IbGibDynamicComponentInsta
1043
1706
  this.setSSOStatus("Link cancelled or failed (incorrect password).", "error");
1044
1707
  return;
1045
1708
  }
1709
+ this.showBusy({
1710
+ isBusy: true,
1711
+ title: 'Evolving Identity...',
1712
+ msg: `Installing ${providerName} custodian pool and signing evolution...`,
1713
+ animationEmoji: '📝'
1714
+ });
1046
1715
  this.setSSOStatus("Evolving identity locally and generating cryptographic signatures...", "info");
1047
1716
  const metaspace = await getGlobalMetaspace_waitIfNeeded();
1048
1717
  if (!metaspace)
@@ -1051,7 +1720,7 @@ export class KeystoneDetailsComponentInstance extends IbGibDynamicComponentInsta
1051
1720
  if (!space) {
1052
1721
  throw new Error("Local space not found");
1053
1722
  }
1054
- // 3. Evolve parent keystone to include the custodian pool
1723
+ // 3. Evolve parent keystone to include the custodian pool (n -> n+1)
1055
1724
  const evolvedParent = await keystoneService.addPools({
1056
1725
  latestKeystone: this.ibGib,
1057
1726
  masterSecret: masterSecret,
@@ -1059,39 +1728,87 @@ export class KeystoneDetailsComponentInstance extends IbGibDynamicComponentInsta
1059
1728
  metaspace,
1060
1729
  space
1061
1730
  });
1062
- // 4. Sync evolved parent keystone back to server
1063
- this.setSSOStatus("Publishing evolved identity keystone to server...", "info");
1064
- const syncRes = await getApiBridge().putEvolveKeystone({
1731
+ // Sync evolved parent (n+1) to server first
1732
+ const syncRes1 = await getApiBridge().putEvolveKeystone({
1065
1733
  domainAddr: stateObj.parentTjpAddr,
1066
1734
  keystoneIbGib: evolvedParent,
1067
- relatedKeystoneIbGibs: [] // No outward delegate timelines
1735
+ relatedKeystoneIbGibs: []
1068
1736
  });
1069
- if (!syncRes.success) {
1070
- 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}`);
1071
1776
  }
1072
- // 5. Update index locally
1777
+ // 6. Update index locally
1073
1778
  await updateSpecialIndex({
1074
1779
  type: "keystones",
1075
1780
  rel8nInfos: [
1076
1781
  {
1077
1782
  rel8nName: "keystone",
1078
- ibGibs: [toDto({ ibGib: evolvedParent })]
1783
+ ibGibs: [toDto({ ibGib: parentWithDelegate }), toDto({ ibGib: delegateKeystone })]
1079
1784
  }
1080
1785
  ],
1081
1786
  metaspace,
1082
1787
  space
1083
1788
  });
1084
1789
  this.setSSOStatus(`Successfully linked with ${stateObj.providerId}!`, "success");
1790
+ this.ibGibAddr = getIbGibAddr({ ibGib: parentWithDelegate });
1791
+ this.staticIbGib = parentWithDelegate;
1085
1792
  await this.loadIbGib({ getLatest: false });
1086
1793
  }
1087
1794
  catch (error) {
1088
1795
  console.error(`${lc} SSO callback processing failed: ${extractErrorMsg(error)}`);
1089
1796
  this.setSSOStatus(`Link failed: ${extractErrorMsg(error)}`, "error");
1090
1797
  }
1798
+ finally {
1799
+ this.showBusy({ isBusy: false });
1800
+ }
1091
1801
  }
1092
1802
  async handleSSOClick(providerId) {
1093
- const isLinked = isSsoProviderLinked({ keystone: this.ibGib, providerId });
1803
+ const isLinked = !!this.ibGib ?
1804
+ isSsoProviderLinked({ keystone: this.ibGib, aggregatedDetails: this.aggregatedDetails, providerId }) :
1805
+ false;
1094
1806
  if (isLinked) {
1807
+ const caps = getKeystoneCapabilities({ keystone: this.ibGib, aggregatedDetails: this.aggregatedDetails });
1808
+ if (!caps.isSovereign) {
1809
+ this.setSSOStatus(`Cannot unlink ${providerId}: This is your only identity recovery and management method. Please add a Master Password first in the Password card below to enable sovereign ownership before unlinking.`, "error");
1810
+ return;
1811
+ }
1095
1812
  await this.handleSSOUnlink(providerId);
1096
1813
  }
1097
1814
  else {
@@ -1100,7 +1817,8 @@ export class KeystoneDetailsComponentInstance extends IbGibDynamicComponentInsta
1100
1817
  }
1101
1818
  async handleSSOUnlink(providerId) {
1102
1819
  const lc = `${this.lc}[${this.handleSSOUnlink.name}]`;
1103
- this.setSSOStatus(`Unlinking ${providerId}...`, "info");
1820
+ const providerName = providerId === 'google' ? 'Google' : providerId === 'github' ? 'GitHub' : providerId;
1821
+ this.setSSOStatus(`Unlinking ${providerName}...`, "info");
1104
1822
  try {
1105
1823
  // 1. Prompt user for password
1106
1824
  const keystoneService = new KeystoneService_V1();
@@ -1111,11 +1829,12 @@ export class KeystoneDetailsComponentInstance extends IbGibDynamicComponentInsta
1111
1829
  while (tries < maxTries && !isCorrect) {
1112
1830
  try {
1113
1831
  const promptMsg = tries === 0
1114
- ? `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.`
1115
1833
  : `Incorrect password. Please try again (${maxTries - tries} tries remaining):`;
1116
1834
  masterSecret = await promptForSecret({
1117
1835
  msg: promptMsg,
1118
- confirm: false
1836
+ confirm: false,
1837
+ passkeys: this.aggregatedDetails?.passkeys,
1119
1838
  });
1120
1839
  }
1121
1840
  catch (error) {
@@ -1125,11 +1844,10 @@ export class KeystoneDetailsComponentInstance extends IbGibDynamicComponentInsta
1125
1844
  if (!masterSecret) {
1126
1845
  break;
1127
1846
  }
1128
- // Verify secret using verifySigningSecret (requires manage poolId)
1847
+ // Verify secret using verifySigningSecret
1129
1848
  isCorrect = await keystoneService.verifySigningSecret({
1130
1849
  keystoneIbGib: this.ibGib,
1131
- signingSecret: masterSecret,
1132
- poolId: 'manage'
1850
+ signingSecret: masterSecret
1133
1851
  });
1134
1852
  if (!isCorrect) {
1135
1853
  tries++;
@@ -1139,6 +1857,12 @@ export class KeystoneDetailsComponentInstance extends IbGibDynamicComponentInsta
1139
1857
  this.setSSOStatus("Unlink cancelled or failed (incorrect password).", "error");
1140
1858
  return;
1141
1859
  }
1860
+ this.showBusy({
1861
+ isBusy: true,
1862
+ title: 'Unlinking Provider...',
1863
+ msg: `Removing ${providerName} custodian pool and delegate keys...`,
1864
+ animationEmoji: '🔓'
1865
+ });
1142
1866
  this.setSSOStatus("Evolving identity locally and removing custodian pool...", "info");
1143
1867
  const metaspace = await getGlobalMetaspace_waitIfNeeded();
1144
1868
  if (!metaspace)
@@ -1149,23 +1873,66 @@ export class KeystoneDetailsComponentInstance extends IbGibDynamicComponentInsta
1149
1873
  }
1150
1874
  // 2. Evolve parent keystone to remove the custodian pool
1151
1875
  const targetPoolId = `custodian-manage-${providerId}`;
1152
- const evolvedParent = await keystoneService.removePools({
1876
+ let evolvedParent = await keystoneService.removePools({
1153
1877
  latestKeystone: this.ibGib,
1154
1878
  masterSecret: masterSecret,
1155
1879
  poolIds: [targetPoolId],
1156
1880
  metaspace,
1157
1881
  space
1158
1882
  });
1159
- // 3. Sync evolved parent keystone back to server
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
+ });
1160
1897
  this.setSSOStatus("Publishing evolved identity keystone to server...", "info");
1161
- const syncRes = await getApiBridge().putEvolveKeystone({
1162
- domainAddr: getIbGibAddr({ ibGib: this.ibGib }),
1898
+ let syncRes = await getApiBridge().putEvolveKeystone({
1899
+ domainAddr: primaryTjpAddr,
1163
1900
  keystoneIbGib: evolvedParent,
1164
1901
  relatedKeystoneIbGibs: [] // No outward delegate timelines
1165
1902
  });
1166
1903
  if (!syncRes.success) {
1167
1904
  throw new Error(`Server synchronization rejected: ${syncRes.message}`);
1168
1905
  }
1906
+ // 4. Unregister any corresponding outward delegate for this provider if present
1907
+ const delegatesMap = evolvedParent.data?.delegates || this.ibGib?.data?.delegates;
1908
+ if (delegatesMap) {
1909
+ for (const [delegateTjpAddr, info] of Object.entries(delegatesMap)) {
1910
+ if (isCustodialDelegate({ delegateInfo: info, providerId })) {
1911
+ evolvedParent = await keystoneService.unregisterDelegate({
1912
+ parentKeystone: evolvedParent,
1913
+ delegateTjpAddr,
1914
+ masterSecret: masterSecret,
1915
+ signingPoolId: 'manage',
1916
+ metaspace,
1917
+ space,
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
+ }
1933
+ }
1934
+ }
1935
+ }
1169
1936
  // 4. Update index locally
1170
1937
  await updateSpecialIndex({
1171
1938
  type: "keystones",
@@ -1178,12 +1945,17 @@ export class KeystoneDetailsComponentInstance extends IbGibDynamicComponentInsta
1178
1945
  metaspace,
1179
1946
  space
1180
1947
  });
1181
- this.setSSOStatus(`Successfully unlinked ${providerId}!`, "success");
1182
- 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 });
1183
1952
  }
1184
1953
  catch (error) {
1185
- console.error(`${lc} SSO unlink failed: ${extractErrorMsg(error)}`);
1186
- 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 });
1187
1959
  }
1188
1960
  }
1189
1961
  setChangePasswordStatus(msg, type) {
@@ -1192,9 +1964,346 @@ export class KeystoneDetailsComponentInstance extends IbGibDynamicComponentInsta
1192
1964
  el.className = `status-msg ${type}`;
1193
1965
  el.classList.remove('hidden');
1194
1966
  }
1967
+ async handleAddMasterPassword() {
1968
+ const lc = `${this.lc}[${this.handleAddMasterPassword.name}]`;
1969
+ this.setChangePasswordStatus("Validating entries...", "info");
1970
+ try {
1971
+ const newPassword = this.elements.inputNewPasswordEl.value;
1972
+ const confirmPassword = this.elements.inputConfirmPasswordEl.value;
1973
+ if (!newPassword) {
1974
+ this.setChangePasswordStatus("Master password is required.", "error");
1975
+ return;
1976
+ }
1977
+ if (newPassword !== confirmPassword) {
1978
+ this.setChangePasswordStatus("Master password and confirmation do not match.", "error");
1979
+ return;
1980
+ }
1981
+ const initialManagePool = this.ibGib?.data?.challengePools?.find(p => p.id === POOL_ID_MANAGE);
1982
+ if (!initialManagePool) {
1983
+ this.setChangePasswordStatus("Primary keystone missing active manage challenge pool.", "error");
1984
+ return;
1985
+ }
1986
+ this.showBusy({
1987
+ isBusy: true,
1988
+ title: 'Elevating Identity...',
1989
+ msg: 'Generating cryptographic sovereign challenge pool...',
1990
+ animationEmoji: '🔑'
1991
+ });
1992
+ this.setChangePasswordStatus("Generating sovereign challenge pool...", "info");
1993
+ const newChallenges = await generatePoolChallenges({
1994
+ config: initialManagePool.config,
1995
+ masterSecret: newPassword,
1996
+ });
1997
+ const newManagePool = {
1998
+ id: POOL_ID_MANAGE,
1999
+ config: initialManagePool.config,
2000
+ challenges: newChallenges,
2001
+ };
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;
2012
+ let requestBody = {
2013
+ primaryTjpAddr,
2014
+ newManagePool,
2015
+ };
2016
+ if (custodianProvider === 'google' || custodianProvider === 'github') {
2017
+ this.showBusy({
2018
+ isBusy: true,
2019
+ title: 'Re-authenticating with Provider...',
2020
+ msg: `Please complete authentication in the ${custodianProvider} popup window...`,
2021
+ animationEmoji: '🔒'
2022
+ });
2023
+ this.setChangePasswordStatus(`Please authenticate in the popup with ${custodianProvider} to verify ownership...`, "info");
2024
+ const resPopup = await executeSsoOAuthPopup({
2025
+ providerId: custodianProvider,
2026
+ parentTjpAddr: primaryTjpAddr,
2027
+ });
2028
+ if (!resPopup.success || !resPopup.code) {
2029
+ this.showBusy({ isBusy: false });
2030
+ this.setChangePasswordStatus(resPopup.message || "Re-authentication cancelled.", "error");
2031
+ return;
2032
+ }
2033
+ requestBody.providerId = custodianProvider;
2034
+ requestBody.code = resPopup.code;
2035
+ requestBody.redirectUri = resPopup.redirectUri;
2036
+ }
2037
+ else {
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
+ }
2092
+ return;
2093
+ }
2094
+ requestBody.email = email;
2095
+ requestBody.code = otpCode;
2096
+ }
2097
+ this.showBusy({
2098
+ isBusy: true,
2099
+ title: 'Updating Keystone Timeline...',
2100
+ msg: 'Publishing sovereign master password and evolving identity DAG...',
2101
+ animationEmoji: '⏳'
2102
+ });
2103
+ this.setChangePasswordStatus("Elevating identity to sovereign master password on server...", "info");
2104
+ const response = await fetch('/api/identity/custodian/add-master-password', {
2105
+ method: 'POST',
2106
+ headers: { 'Content-Type': 'application/json' },
2107
+ body: JSON.stringify(requestBody),
2108
+ });
2109
+ const resJson = await response.json();
2110
+ if (!response.ok || !resJson.success) {
2111
+ throw new Error(resJson.error || resJson.message || `Server error (${response.status})`);
2112
+ }
2113
+ const evolvedParent = resJson.primaryKeystone;
2114
+ // Save to local user space & metaspace
2115
+ const metaspace = await getGlobalMetaspace_waitIfNeeded();
2116
+ if (!metaspace) {
2117
+ throw new Error("Global metaspace not available");
2118
+ }
2119
+ const space = await metaspace.getLocalUserSpace({ lock: false });
2120
+ if (!space) {
2121
+ throw new Error("Local user space not available");
2122
+ }
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 });
2193
+ await updateSpecialIndex({
2194
+ type: "keystones",
2195
+ rel8nInfos: [
2196
+ {
2197
+ rel8nName: "keystone",
2198
+ ibGibs: [toDto({ ibGib: parentWithUserDelegate }), toDto({ ibGib: userSyncDelegate })]
2199
+ }
2200
+ ],
2201
+ metaspace,
2202
+ space
2203
+ });
2204
+ const evolvedAddr = getIbGibAddr({ ibGib: parentWithUserDelegate });
2205
+ // Dispatch EVENT_IBGIB_IDENTITY_CHANGED
2206
+ window.dispatchEvent(new CustomEvent(EVENT_IBGIB_IDENTITY_CHANGED, {
2207
+ detail: { activeIdentityAddr: evolvedAddr },
2208
+ bubbles: true,
2209
+ composed: true
2210
+ }));
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
+ }
2222
+ // Refresh the view
2223
+ this.ibGibAddr = evolvedAddr;
2224
+ this.staticIbGib = parentWithUserDelegate;
2225
+ await this.loadIbGib({ getLatest: true });
2226
+ // Clear inputs
2227
+ this.elements.inputNewPasswordEl.value = '';
2228
+ this.elements.inputConfirmPasswordEl.value = '';
2229
+ }
2230
+ catch (error) {
2231
+ console.error(`${lc} Elevation failed: ${extractErrorMsg(error)}`);
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
+ }
2237
+ }
2238
+ finally {
2239
+ this.showBusy({ isBusy: false });
2240
+ }
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
+ }
1195
2290
  async handleChangePassword() {
1196
2291
  const lc = `${this.lc}[${this.handleChangePassword.name}]`;
2292
+ const caps = !!this.ibGib ?
2293
+ getKeystoneCapabilities({ keystone: this.ibGib, aggregatedDetails: this.aggregatedDetails }) :
2294
+ undefined;
2295
+ const isCustodialOnly = !!caps ? caps.isCustodial && !caps.isSovereign : false;
2296
+ if (isCustodialOnly) {
2297
+ await this.handleAddMasterPassword();
2298
+ return;
2299
+ }
1197
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
+ });
1198
2307
  try {
1199
2308
  const currentPassword = this.elements.inputCurrentPasswordEl.value;
1200
2309
  const newPassword = this.elements.inputNewPasswordEl.value;
@@ -1222,6 +2331,12 @@ export class KeystoneDetailsComponentInstance extends IbGibDynamicComponentInsta
1222
2331
  this.setChangePasswordStatus("Incorrect current password.", "error");
1223
2332
  return;
1224
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
+ });
1225
2340
  this.setChangePasswordStatus("Evolving primary identity keystone...", "info");
1226
2341
  const metaspace = await getGlobalMetaspace_waitIfNeeded();
1227
2342
  if (!metaspace) {
@@ -1231,8 +2346,7 @@ export class KeystoneDetailsComponentInstance extends IbGibDynamicComponentInsta
1231
2346
  if (!space) {
1232
2347
  throw new Error(`(UNEXPECTED) couldn't get default local space? (E: 2fc1281c28351fd378a1d7f88eaa0526)`);
1233
2348
  }
1234
- // 2. Rotate parent identity
1235
- 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
1236
2350
  const { newKeystone: evolvedParent, newDelegates: evolvedDelegates } = await keystoneService.changePassword({
1237
2351
  keystoneIbGib: this.ibGib,
1238
2352
  newMasterSecret: newPassword,
@@ -1241,56 +2355,21 @@ export class KeystoneDetailsComponentInstance extends IbGibDynamicComponentInsta
1241
2355
  metaspace,
1242
2356
  space
1243
2357
  });
1244
- debugger; // look at evolvedDelegates
1245
- this.setChangePasswordStatus("Resolving and rotating delegate keystones...", "info");
1246
- // // 3. Evolve registered delegate keystones (preserving custodian/foreign delegate secrets)
1247
- // const delegates = this.ibGib!.data?.delegates || {};
1248
- // const evolvedDelegates: KeystoneIbGib_V1[] = [];
1249
- // for (const delegateInfo of Object.values(delegates)) {
1250
- // const delegateAddr = delegateInfo.delegateAddr;
1251
- // const delegateKeystone = await keystoneService.getLatestKeystone({
1252
- // addr: delegateAddr,
1253
- // metaspace,
1254
- // space,
1255
- // });
1256
- // // Identify user-owned challenge pools in the delegate that match the old password
1257
- // const poolsToRotate: string[] = [];
1258
- // for (const pool of delegateKeystone.data?.challengePools ?? []) {
1259
- // if (!pool.isForeign) {
1260
- // /**
1261
- // * WARNING: There is an extreme edge case that if two pools
1262
- // * happen to be owned by different parties but share the
1263
- // * same secret, this will change the wrong pool's secret
1264
- // * also. For V1, this shouldn't matter. But we need a
1265
- // * better way of tracking pool ownership for V2 (or even
1266
- // * V1 if we have multi-party support). (DO NOT REMOVE
1267
- // * THIS COMMENT UNTIL THIS IS ADDRESSED)
1268
- // */
1269
- // const isPoolMatch = await keystoneService.verifySigningSecret({
1270
- // keystoneIbGib: delegateKeystone,
1271
- // signingSecret: currentPassword,
1272
- // poolId: pool.id
1273
- // });
1274
- // if (isPoolMatch) { poolsToRotate.push(pool.id); }
1275
- // }
1276
- // }
1277
- // if (poolsToRotate.length > 0) {
1278
- // console.log(`${lc} Rotating pools in delegate ${delegateInfo.delegateTjpAddr}: ${poolsToRotate.join(', ')}`);
1279
- // const {newKeystone: evolvedDelegate} = await keystoneService.changePassword({
1280
- // keystoneIbGib: delegateKeystone,
1281
- // newMasterSecret: newPassword,
1282
- // signingSecret: currentPassword,
1283
- // signingPoolId: 'manage',
1284
- // poolIds: poolsToRotate,
1285
- // metaspace,
1286
- // space
1287
- // });
1288
- // evolvedDelegates.push(evolvedDelegate);
1289
- // }
1290
- // }
1291
- // 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
+ });
1292
2365
  this.setChangePasswordStatus("Syncing evolved keystones to server...", "info");
1293
- 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 });
1294
2373
  const syncRes = await getApiBridge().putEvolveKeystone({
1295
2374
  domainAddr: parentTjpAddr,
1296
2375
  keystoneIbGib: evolvedParent,
@@ -1299,7 +2378,7 @@ export class KeystoneDetailsComponentInstance extends IbGibDynamicComponentInsta
1299
2378
  if (!syncRes.success) {
1300
2379
  throw new Error(`Server synchronization rejected: ${syncRes.message}`);
1301
2380
  }
1302
- // 5. Update index locally
2381
+ // 4. Update index locally
1303
2382
  this.setChangePasswordStatus("Updating local indexes...", "info");
1304
2383
  const newKeystoneIbGibs = [toDto({ ibGib: evolvedParent })];
1305
2384
  if (evolvedDelegates) {
@@ -1324,12 +2403,18 @@ export class KeystoneDetailsComponentInstance extends IbGibDynamicComponentInsta
1324
2403
  this.elements.inputNewPasswordEl.value = "";
1325
2404
  this.elements.inputConfirmPasswordEl.value = "";
1326
2405
  this.setChangePasswordStatus("Password updated successfully!", "success");
2406
+ this.ibGibAddr = getIbGibAddr({ ibGib: evolvedParent });
2407
+ this.staticIbGib = evolvedParent;
1327
2408
  await this.loadIbGib({ getLatest: true });
1328
2409
  }
1329
2410
  catch (error) {
2411
+ debugger; // error in handleChangePassword
1330
2412
  console.error(`${lc} Password rotation failed: ${extractErrorMsg(error)}`);
1331
2413
  this.setChangePasswordStatus(`Update failed: ${extractErrorMsg(error)}`, "error");
1332
2414
  }
2415
+ finally {
2416
+ this.showBusy({ isBusy: false });
2417
+ }
1333
2418
  }
1334
2419
  }
1335
2420
  //# sourceMappingURL=keystone-details.mjs.map