@ibgib/web-gib 0.0.61 → 0.0.63

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (66) 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 +20 -3
  4. package/dist/helpers.web.d.mts.map +1 -1
  5. package/dist/helpers.web.mjs +84 -3
  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 +77 -0
  14. package/dist/identity/passkey-helper.d.mts.map +1 -0
  15. package/dist/identity/passkey-helper.mjs +258 -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.css +26 -0
  29. package/dist/ui/component/identity/keystone-creator/keystone-creator.d.mts +18 -1
  30. package/dist/ui/component/identity/keystone-creator/keystone-creator.d.mts.map +1 -1
  31. package/dist/ui/component/identity/keystone-creator/keystone-creator.html +60 -4
  32. package/dist/ui/component/identity/keystone-creator/keystone-creator.mjs +518 -74
  33. package/dist/ui/component/identity/keystone-creator/keystone-creator.mjs.map +1 -1
  34. package/dist/ui/component/identity/keystone-details/keystone-details.d.mts +28 -1
  35. package/dist/ui/component/identity/keystone-details/keystone-details.d.mts.map +1 -1
  36. package/dist/ui/component/identity/keystone-details/keystone-details.html +46 -0
  37. package/dist/ui/component/identity/keystone-details/keystone-details.mjs +1146 -208
  38. package/dist/ui/component/identity/keystone-details/keystone-details.mjs.map +1 -1
  39. package/dist/ui/component/identity/remove-device/remove-device.d.mts.map +1 -1
  40. package/dist/ui/component/identity/remove-device/remove-device.mjs +1 -0
  41. package/dist/ui/component/identity/remove-device/remove-device.mjs.map +1 -1
  42. package/dist/ui/ui-helpers.d.mts +3 -0
  43. package/dist/ui/ui-helpers.d.mts.map +1 -1
  44. package/dist/ui/ui-helpers.mjs +116 -7
  45. package/dist/ui/ui-helpers.mjs.map +1 -1
  46. package/package.json +2 -2
  47. package/src/AUTO-GENERATED-version.mts +1 -1
  48. package/src/helpers.web.mts +117 -7
  49. package/src/identity/device-delegate-helper.mts +135 -2
  50. package/src/identity/device-delegate.respec.mts +6 -6
  51. package/src/identity/passkey-helper.mts +337 -0
  52. package/src/identity/passkey.respec.mts +194 -0
  53. package/src/index.mts +1 -0
  54. package/src/ui/component/identity/add-device/add-device.mts +1 -0
  55. package/src/ui/component/identity/keystone-creator/keystone-creator.css +26 -0
  56. package/src/ui/component/identity/keystone-creator/keystone-creator.html +60 -4
  57. package/src/ui/component/identity/keystone-creator/keystone-creator.mts +561 -77
  58. package/src/ui/component/identity/keystone-details/keystone-details.html +46 -0
  59. package/src/ui/component/identity/keystone-details/keystone-details.mts +1239 -224
  60. package/src/ui/component/identity/remove-device/remove-device.mts +1 -0
  61. package/src/ui/ui-helpers.mts +123 -6
  62. package/tools/auto-generated-agent-skills/skills/ibgib-create-app-server/SKILL.md +50 -0
  63. package/src/identity/custodian-delegate-helper.mts.bak +0 -7
  64. package/src/identity/sso/sso-config-helper.mts.bak +0 -52
  65. package/src/identity/sso/sso-custodian-service.mts.bak +0 -566
  66. package/src/identity/sso/sso-custodian-service.respec.mts.bak +0 -298
@@ -2,7 +2,7 @@ import thisCss from "./keystone-details.css";
2
2
  import thisHtml from "./keystone-details.html";
3
3
  import commonComponentStylesCss from "../../common-component-styles.css";
4
4
 
5
- import { extractErrorMsg, pretty } from "@ibgib/helper-gib/dist/helpers/utils-helper.mjs";
5
+ import { extractErrorMsg, pretty, getSaferSubstring, HashAlgorithm, delay } from "@ibgib/helper-gib/dist/helpers/utils-helper.mjs";
6
6
  import { IbGibAddr } from "@ibgib/ts-gib/dist/types.mjs";
7
7
  import { IbGib_V1 } from "@ibgib/ts-gib/dist/V1/types.mjs";
8
8
  import { getIbAndGib, getIbGibAddr } from "@ibgib/ts-gib/dist/helper.mjs";
@@ -20,15 +20,22 @@ import { EVENT_IBGIB_IDENTITY_REQUEST_CHANGE, EVENT_IBGIB_IDENTITY_CHANGED } fro
20
20
  import { getApiBridge } from "../../../../api/ibgib-api-bridge.mjs";
21
21
  import { getKeystoneSyncService } from "../../../../identity/keystone-sync-service.mjs";
22
22
  import { updateSpecialIndex } from "@ibgib/core-gib/dist/timeline/timeline-api.mjs";
23
- import { getTjpAddr, toDto } from "@ibgib/core-gib/dist/common/other/ibgib-helper.mjs";
23
+ import { getTjpAddr, toDto, getTimelinesGroupedByTjp } from "@ibgib/core-gib/dist/common/other/ibgib-helper.mjs";
24
24
  import { parseSpaceIb } from "@ibgib/core-gib/dist/witness/space/space-helper.mjs";
25
25
  import { executeSsoOAuthPopup } from "../../../../identity/sso-popup-helper.mjs";
26
26
  import { getComponentCtorArg } from "../../../../app-bootstrap/init-orchestration.mjs";
27
- import { promptForSecret } from "../../../../helpers.web.mjs";
28
- import { isSsoProviderLinked, getKeystoneCapabilities, isDelegateExpired } from "@ibgib/core-gib/dist/keystone/delegate/delegate-helpers.mjs";
27
+ import { promptForSecret, promptForAuthoritySecret } from "../../../../helpers.web.mjs";
28
+ import { isSsoProviderLinked, getKeystoneCapabilities, isDelegateExpired, deriveDelegateSecret, createSyncDelegateKeystone, isCustodialDelegate } from "@ibgib/core-gib/dist/keystone/delegate/delegate-helpers.mjs";
29
29
  import { generatePoolChallenges } from "@ibgib/core-gib/dist/keystone/keystone-helpers.mjs";
30
- import { POOL_ID_MANAGE } from "@ibgib/core-gib/dist/keystone/keystone-constants.mjs";
30
+ import { POOL_ID_MANAGE, KEYSTONE_VERB_SYNC, KEYSTONE_VERB_CONNECT, KEYSTONE_VERB_MANAGE } from "@ibgib/core-gib/dist/keystone/keystone-constants.mjs";
31
+ import { generateDefaultDeviceTag } from "../../../../util/device-helper.mjs";
31
32
  import { getGlobalMetaspace_waitIfNeeded } from "../../../../helpers.mjs";
33
+ import { createSovereignDeviceSyncDelegate, createOAuth2DeviceSyncDelegate, createEmailDeviceSyncDelegate } from "../../../../identity/device-delegate-helper.mjs";
34
+ import { isPasskeyPrfSupported, registerPasskeyCredential, evaluatePasskeyPrf, createPasskeyDeviceSyncDelegate, bufferToHex } from "../../../../identity/passkey-helper.mjs";
35
+ import {
36
+ KeystonePasskeyEntry, KeystoneChallengePool, KeystonePoolConfig_HashV1,
37
+ KeystoneChallengeType, KeystoneReplenishStrategy
38
+ } from "@ibgib/core-gib/dist/keystone/keystone-types.mjs";
32
39
 
33
40
  const logalot = false;
34
41
 
@@ -89,6 +96,11 @@ interface KeystoneDetailsElements extends ElementsBase {
89
96
  rawKeystoneJsonEl: HTMLPreElement;
90
97
  delegatesCardEl: HTMLDivElement;
91
98
  delegatesListEl: HTMLTableSectionElement;
99
+ passkeysCardEl: HTMLDivElement;
100
+ passkeysListEl: HTMLTableSectionElement;
101
+ btnAddPasskeyEl: HTMLButtonElement;
102
+ selectPasskeyAttachmentEl?: HTMLSelectElement;
103
+ passkeyStatusMsgEl: HTMLDivElement;
92
104
  historicalWarningBannerEl: HTMLDivElement;
93
105
  containerEl: HTMLDivElement;
94
106
  ssoLinkCardEl: HTMLDivElement;
@@ -101,6 +113,10 @@ interface KeystoneDetailsElements extends ElementsBase {
101
113
  btnChangePasswordEl: HTMLButtonElement;
102
114
  changePasswordStatusEl: HTMLDivElement;
103
115
  changePasswordCardEl: HTMLDivElement;
116
+ sectionAddPasswordOtpEl: HTMLDivElement;
117
+ inputAddPasswordOtpEl: HTMLInputElement;
118
+ btnResendAddPasswordOtpEl: HTMLButtonElement;
119
+ descAddPasswordOtpEl: HTMLParagraphElement;
104
120
  }
105
121
 
106
122
  export class KeystoneDetailsComponentInstance
@@ -120,6 +136,7 @@ export class KeystoneDetailsComponentInstance
120
136
  private _onIdentityChanged: ((e: any) => void) | null = null;
121
137
  private keystoneCache: Map<string, { username: string, description: string, isPrimary: boolean, n: number }> = new Map();
122
138
  private aggregatedDetails?: any;
139
+ private resendCooldownInterval: any = null;
123
140
 
124
141
  constructor() {
125
142
  super();
@@ -142,6 +159,39 @@ export class KeystoneDetailsComponentInstance
142
159
  }
143
160
  }
144
161
 
162
+ override async disconnected(): Promise<void> {
163
+ if (this._onIdentityChanged) {
164
+ window.removeEventListener(EVENT_IBGIB_IDENTITY_CHANGED, this._onIdentityChanged);
165
+ }
166
+ if (this.resendCooldownInterval) {
167
+ clearInterval(this.resendCooldownInterval);
168
+ this.resendCooldownInterval = null;
169
+ }
170
+ }
171
+
172
+ private startResendCooldownTimer(durationSecs: number = 30): void {
173
+ const btn = this.elements?.btnResendAddPasswordOtpEl;
174
+ if (!btn) return;
175
+ if (this.resendCooldownInterval) {
176
+ clearInterval(this.resendCooldownInterval);
177
+ this.resendCooldownInterval = null;
178
+ }
179
+ let remaining = durationSecs;
180
+ btn.disabled = true;
181
+ btn.textContent = `Resend Code (${remaining}s)`;
182
+ this.resendCooldownInterval = setInterval(() => {
183
+ remaining -= 1;
184
+ if (remaining <= 0) {
185
+ clearInterval(this.resendCooldownInterval);
186
+ this.resendCooldownInterval = null;
187
+ btn.disabled = false;
188
+ btn.textContent = "Resend Code";
189
+ } else {
190
+ btn.textContent = `Resend Code (${remaining}s)`;
191
+ }
192
+ }, 1000);
193
+ }
194
+
145
195
  private async getKeystoneDetails(addr: string): Promise<{ username: string, description: string, isPrimary: boolean, n: number }> {
146
196
  const cached = this.keystoneCache.get(addr);
147
197
  if (cached) return cached;
@@ -208,6 +258,11 @@ export class KeystoneDetailsComponentInstance
208
258
  this.elements.rawKeystoneJsonEl = shadow.getElementById('raw-keystone-json') as HTMLPreElement;
209
259
  this.elements.delegatesCardEl = shadow.getElementById('delegates-card') as HTMLDivElement;
210
260
  this.elements.delegatesListEl = shadow.getElementById('delegates-list') as HTMLTableSectionElement;
261
+ this.elements.passkeysCardEl = shadow.getElementById('passkeys-card') as HTMLDivElement;
262
+ this.elements.passkeysListEl = shadow.getElementById('passkeys-list') as HTMLTableSectionElement;
263
+ this.elements.btnAddPasskeyEl = shadow.getElementById('btn-add-passkey') as HTMLButtonElement;
264
+ this.elements.selectPasskeyAttachmentEl = shadow.getElementById('select-passkey-attachment') as HTMLSelectElement;
265
+ this.elements.passkeyStatusMsgEl = shadow.getElementById('passkey-status-msg') as HTMLDivElement;
211
266
  this.elements.containerEl = shadow.getElementById('container') as HTMLDivElement;
212
267
  this.elements.historicalWarningBannerEl = shadow.getElementById('historical-warning-banner') as HTMLDivElement;
213
268
  this.elements.ssoLinkCardEl = shadow.getElementById('sso-link-card') as HTMLDivElement;
@@ -220,6 +275,10 @@ export class KeystoneDetailsComponentInstance
220
275
  this.elements.btnChangePasswordEl = shadow.getElementById('btn-change-password') as HTMLButtonElement;
221
276
  this.elements.changePasswordStatusEl = shadow.getElementById('change-password-status') as HTMLDivElement;
222
277
  this.elements.changePasswordCardEl = shadow.getElementById('change-password-card') as HTMLDivElement;
278
+ this.elements.sectionAddPasswordOtpEl = shadow.getElementById('section-add-password-otp') as HTMLDivElement;
279
+ this.elements.inputAddPasswordOtpEl = shadow.getElementById('input-add-password-otp') as HTMLInputElement;
280
+ this.elements.btnResendAddPasswordOtpEl = shadow.getElementById('btn-resend-add-password-otp') as HTMLButtonElement;
281
+ this.elements.descAddPasswordOtpEl = shadow.getElementById('desc-add-password-otp') as HTMLParagraphElement;
223
282
 
224
283
  // ElementBase requires contentEl
225
284
  this.elements.contentEl = shadow.getElementById('identity-details-view') as HTMLDivElement;
@@ -245,12 +304,6 @@ export class KeystoneDetailsComponentInstance
245
304
  }
246
305
  }
247
306
 
248
- override async disconnected(): Promise<void> {
249
- if (this._onIdentityChanged) {
250
- window.removeEventListener(EVENT_IBGIB_IDENTITY_CHANGED, this._onIdentityChanged);
251
- }
252
- }
253
-
254
307
  private initHandlers() {
255
308
  // Setup raw JSON accordion toggle
256
309
  this.elements!.rawAccordionHeaderEl.addEventListener('click', () => this.toggleAccordion());
@@ -260,9 +313,11 @@ export class KeystoneDetailsComponentInstance
260
313
  this.elements!.btnSetActiveEl.addEventListener('click', () => this.handleSetActiveIdentity());
261
314
  this.elements!.btnCheckUpdatesEl.addEventListener('click', () => this.handleCheckForUpdates());
262
315
  this.elements!.btnUpdateIdentityEl.addEventListener('click', () => this.handleUpdateIdentity());
316
+ this.elements!.btnAddPasskeyEl?.addEventListener('click', () => this.handleAddPasskey());
263
317
  this.elements!.btnLinkGoogleEl.addEventListener('click', () => this.handleSSOClick('google'));
264
318
  this.elements!.btnLinkGithubEl.addEventListener('click', () => this.handleSSOClick('github'));
265
319
  this.elements!.btnChangePasswordEl.addEventListener('click', () => this.handleChangePassword());
320
+ this.elements!.btnResendAddPasswordOtpEl?.addEventListener('click', () => this.handleResendAddPasswordOtp());
266
321
  }
267
322
 
268
323
  private toggleAccordion() {
@@ -458,15 +513,22 @@ export class KeystoneDetailsComponentInstance
458
513
 
459
514
  // Check if origin provider or linked
460
515
  const originProvider = this.aggregatedDetails?.custodianProvider || this.ibGib?.data?.frameDetails?.custodianProvider;
461
- const isGoogleLinked = isSsoProviderLinked({ keystone: this.ibGib, aggregatedDetails: this.aggregatedDetails, providerId: 'google' });
462
- const isGithubLinked = isSsoProviderLinked({ keystone: this.ibGib, aggregatedDetails: this.aggregatedDetails, providerId: 'github' });
516
+ const isGoogleLinked = !!this.ibGib ?
517
+ isSsoProviderLinked({ keystone: this.ibGib, aggregatedDetails: this.aggregatedDetails, providerId: 'google' }) :
518
+ false;
519
+ const isGithubLinked = !!this.ibGib ?
520
+ isSsoProviderLinked({ keystone: this.ibGib, aggregatedDetails: this.aggregatedDetails, providerId: 'github' }) :
521
+ false;
463
522
 
464
- const caps = getKeystoneCapabilities({ keystone: this.ibGib, aggregatedDetails: this.aggregatedDetails });
523
+ const caps = !!this.ibGib ?
524
+ getKeystoneCapabilities({ keystone: this.ibGib, aggregatedDetails: this.aggregatedDetails }) :
525
+ undefined;
465
526
 
466
527
  // Google Card States
467
528
  const googleCard = this.elements.btnLinkGoogleEl;
468
529
  const googleLabel = googleCard.querySelector('.toggle-state-text')!;
469
530
  if (isGoogleLinked) {
531
+ if (!caps) { throw new Error(`(UNEXPECTED) isGoogleLinked but capabilities falsy? (E: cc62e8357b66860dd2cb8148c833b726)`); }
470
532
  googleCard.classList.remove('unlinked');
471
533
  googleCard.classList.add('linked');
472
534
  const isOrigin = originProvider === 'google';
@@ -491,6 +553,7 @@ export class KeystoneDetailsComponentInstance
491
553
  const githubCard = this.elements.btnLinkGithubEl;
492
554
  const githubLabel = githubCard.querySelector('.toggle-state-text')!;
493
555
  if (isGithubLinked) {
556
+ if (!caps) { throw new Error(`(UNEXPECTED) isGithubLinked but capabilities falsy? (E: e94688210538e0e82878088873a46826)`); }
494
557
  githubCard.classList.remove('unlinked');
495
558
  githubCard.classList.add('linked');
496
559
  const isOrigin = originProvider === 'github';
@@ -514,16 +577,18 @@ export class KeystoneDetailsComponentInstance
514
577
  }
515
578
 
516
579
  private updatePasswordCardView(): void {
517
- const caps = getKeystoneCapabilities({ keystone: this.ibGib, aggregatedDetails: this.aggregatedDetails });
580
+ const caps = !!this.ibGib ?
581
+ getKeystoneCapabilities({ keystone: this.ibGib, aggregatedDetails: this.aggregatedDetails }) :
582
+ undefined;
518
583
  const cardTitleEl = this.elements?.changePasswordCardEl?.querySelector('h3');
519
584
  const cardDescEl = this.elements?.changePasswordCardEl?.querySelector('.description');
520
585
  const currentPasswordGroup = this.elements?.inputCurrentPasswordEl?.closest('.form-group') as HTMLElement | null;
521
586
  const btnChangePassword = this.elements?.btnChangePasswordEl;
522
587
 
523
- const isCustodialOnly = caps.isCustodial && !caps.isSovereign;
524
- if (isCustodialOnly) {
588
+ const hasPassword = !!caps?.hasPassword;
589
+ if (!hasPassword) {
525
590
  if (cardTitleEl) cardTitleEl.textContent = "Add Master Password";
526
- if (cardDescEl) cardDescEl.textContent = "Establish sovereign ownership by adding a master password. This enables direct, independent cryptographic management without relying solely on custodian authentication.";
591
+ if (cardDescEl) cardDescEl.textContent = "Establish a master password for this identity. This enables sovereign password authentication alongside your passkeys and linked providers.";
527
592
  if (currentPasswordGroup) currentPasswordGroup.style.display = 'none';
528
593
  if (btnChangePassword) btnChangePassword.textContent = "Add Master Password";
529
594
  } else {
@@ -532,6 +597,13 @@ export class KeystoneDetailsComponentInstance
532
597
  if (currentPasswordGroup) currentPasswordGroup.style.display = '';
533
598
  if (btnChangePassword) btnChangePassword.textContent = "Update Password";
534
599
  }
600
+
601
+ if (this.elements?.sectionAddPasswordOtpEl) {
602
+ this.elements.sectionAddPasswordOtpEl.classList.add('hidden');
603
+ if (this.elements.inputAddPasswordOtpEl) {
604
+ this.elements.inputAddPasswordOtpEl.value = '';
605
+ }
606
+ }
535
607
  }
536
608
 
537
609
  private async updateFrameDetailsView(): Promise<void> {
@@ -643,16 +715,19 @@ export class KeystoneDetailsComponentInstance
643
715
  // Render Challenge Pools
644
716
  this.renderChallengePools(keystone.data?.challengePools || []);
645
717
 
646
- // Render Delegates, SSO link, and Password Change cards (only if this is a primary keystone, hide them for delegate/session keystones)
718
+ // Render Delegates, Passkeys, SSO link, and Password Change cards (only if this is a primary keystone, hide them for delegate/session keystones)
647
719
  const details = await this.getKeystoneDetails(this.ibGibAddr);
648
720
  if (details.isPrimary) {
649
721
  this.elements.delegatesCardEl.classList.remove('hidden');
650
722
  await this.renderDelegates(keystone.data?.delegates);
723
+ this.elements.passkeysCardEl.classList.remove('hidden');
724
+ await this.renderPasskeys(this.aggregatedDetails?.passkeys);
651
725
  this.elements.ssoLinkCardEl.classList.remove('hidden');
652
726
  this.elements.changePasswordCardEl.classList.remove('hidden');
653
727
  this.updatePasswordCardView();
654
728
  } else {
655
729
  this.elements.delegatesCardEl.classList.add('hidden');
730
+ this.elements.passkeysCardEl.classList.add('hidden');
656
731
  this.elements.ssoLinkCardEl.classList.add('hidden');
657
732
  this.elements.changePasswordCardEl.classList.add('hidden');
658
733
  }
@@ -687,6 +762,15 @@ export class KeystoneDetailsComponentInstance
687
762
  if (this.elements.delegatesListEl) {
688
763
  this.elements.delegatesListEl.innerHTML = '';
689
764
  }
765
+ if (this.elements.passkeysListEl) {
766
+ this.elements.passkeysListEl.innerHTML = '';
767
+ }
768
+ if (this.elements.sectionAddPasswordOtpEl) {
769
+ this.elements.sectionAddPasswordOtpEl.classList.add('hidden');
770
+ if (this.elements.inputAddPasswordOtpEl) {
771
+ this.elements.inputAddPasswordOtpEl.value = '';
772
+ }
773
+ }
690
774
  }
691
775
 
692
776
  private renderChallengePools(pools: any[]) {
@@ -1007,11 +1091,17 @@ export class KeystoneDetailsComponentInstance
1007
1091
  const lc = `${this.lc}[${this.handleRevokeDelegate.name}]`;
1008
1092
  try {
1009
1093
  let masterSecret: string;
1094
+ let matchedPoolId: string;
1010
1095
  try {
1011
- masterSecret = await promptForSecret({
1012
- msg: `Enter Master Password to revoke delegate '${deviceName}':`,
1013
- confirm: false
1096
+ const authResult = await promptForAuthoritySecret({
1097
+ keystone: this.ibGib,
1098
+ aggregatedDetails: this.aggregatedDetails,
1099
+ title: 'Revoke Delegate',
1100
+ msg: `Enter Master Password or touch security key to revoke delegate '${deviceName}':`,
1101
+ verb: KEYSTONE_VERB_MANAGE
1014
1102
  });
1103
+ masterSecret = authResult.secret;
1104
+ matchedPoolId = authResult.poolId;
1015
1105
  } catch (err) {
1016
1106
  // User cancelled prompt
1017
1107
  return;
@@ -1032,6 +1122,7 @@ export class KeystoneDetailsComponentInstance
1032
1122
  parentKeystone: this.ibGib!,
1033
1123
  delegateTjpAddr,
1034
1124
  masterSecret,
1125
+ signingPoolId: matchedPoolId,
1035
1126
  metaspace,
1036
1127
  space
1037
1128
  });
@@ -1085,6 +1176,480 @@ export class KeystoneDetailsComponentInstance
1085
1176
  }
1086
1177
  }
1087
1178
 
1179
+ private setPasskeyStatus(msg: string, type: 'info' | 'success' | 'error') {
1180
+ const statusEl = this.elements?.passkeyStatusMsgEl;
1181
+ if (!statusEl) return;
1182
+ statusEl.textContent = msg;
1183
+ statusEl.className = `status-msg ${type}`;
1184
+ statusEl.classList.remove('hidden');
1185
+ }
1186
+
1187
+ private async renderPasskeys(passkeys: Record<string, KeystonePasskeyEntry> | undefined) {
1188
+ if (!this.elements) { return; }
1189
+ const listEl = this.elements.passkeysListEl;
1190
+ listEl.innerHTML = '';
1191
+
1192
+ const caps = !!this.ibGib ?
1193
+ getKeystoneCapabilities({ keystone: this.ibGib, aggregatedDetails: this.aggregatedDetails }) :
1194
+ undefined;
1195
+
1196
+ // Update button text depending on whether the account is custodial-only (elevation) or sovereign/hybrid
1197
+ const isCustodialOnly = !!caps ? caps.isCustodial && !caps.isSovereign : false;
1198
+ if (this.elements.btnAddPasskeyEl) {
1199
+ this.elements.btnAddPasskeyEl.textContent = isCustodialOnly
1200
+ ? "🛡️ Add Passkey (Passwordless Sovereign Elevation)"
1201
+ : "🛡️ Register New Passkey / Security Key";
1202
+ }
1203
+
1204
+ const validPasskeyEntries = Object.entries(passkeys || {}).filter(([_, entry]) => entry !== null && entry !== undefined);
1205
+
1206
+ if (validPasskeyEntries.length === 0) {
1207
+ listEl.innerHTML = `
1208
+ <tr>
1209
+ <td colspan="4" style="text-align: center; color: var(--clr-text-secondary, #b3b3b3); padding: 1rem;">
1210
+ No hardware passkeys or biometrics registered.
1211
+ </td>
1212
+ </tr>
1213
+ `;
1214
+ return;
1215
+ }
1216
+
1217
+ for (const [credId, entry] of validPasskeyEntries) {
1218
+ const row = document.createElement('tr');
1219
+ const createdText = entry.createdUTC ? new Date(entry.createdUTC).toLocaleString() : 'N/A';
1220
+ const nameText = entry.name || 'Hardware Key';
1221
+
1222
+ // Format transport badges
1223
+ let transportBadgesHtml = '';
1224
+ if (entry.transports && entry.transports.length > 0) {
1225
+ transportBadgesHtml = entry.transports.map(t => {
1226
+ let icon = '🔑';
1227
+ let label: string = t;
1228
+ if (t === 'internal') { icon = '💻'; label = 'Touch ID / Built-in'; }
1229
+ else if (t === 'usb') { icon = '🔑'; label = 'USB Key'; }
1230
+ else if (t === 'nfc') { icon = '📶'; label = 'NFC'; }
1231
+ else if (t === 'ble' || t === 'hybrid') { icon = '📲'; label = 'Mobile / Bluetooth'; }
1232
+ 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>`;
1233
+ }).join(' ');
1234
+ } else {
1235
+ 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>`;
1236
+ }
1237
+
1238
+ row.innerHTML = `
1239
+ <td>
1240
+ <div style="font-weight: 500;">${nameText}</div>
1241
+ <code style="font-size: 0.75rem; color: var(--clr-text-secondary, #b3b3b3); word-break: break-all;">${credId.substring(0, 16)}...</code>
1242
+ </td>
1243
+ <td>${transportBadgesHtml}</td>
1244
+ <td style="font-size: 0.85rem; color: var(--clr-text-secondary, #b3b3b3);">${createdText}</td>
1245
+ <td>
1246
+ <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);">
1247
+ Remove
1248
+ </button>
1249
+ </td>
1250
+ `;
1251
+
1252
+ const btnRemove = row.querySelector('.btn-remove-passkey') as HTMLButtonElement;
1253
+ btnRemove?.addEventListener('click', () => this.handleRemovePasskey(credId));
1254
+
1255
+ listEl.appendChild(row);
1256
+ }
1257
+ }
1258
+
1259
+ protected async handleAddPasskey(): Promise<void> {
1260
+ const lc = `${this.lc}[${this.handleAddPasskey.name}]`;
1261
+ if (!this.ibGib || !this.ibGibAddr) { return; }
1262
+
1263
+ try {
1264
+ const caps = getKeystoneCapabilities({ keystone: this.ibGib, aggregatedDetails: this.aggregatedDetails });
1265
+ const isCustodialOnly = caps.isCustodial && !caps.isSovereign;
1266
+
1267
+ const supported = await isPasskeyPrfSupported();
1268
+ if (!supported) {
1269
+ this.setPasskeyStatus("WebAuthn PRF extension is not supported in this browser environment.", "error");
1270
+ return;
1271
+ }
1272
+
1273
+ this.showBusy({
1274
+ isBusy: true,
1275
+ title: 'Touch Biometrics or Security Key',
1276
+ msg: 'Please touch your fingerprint sensor, Face ID, or insert your USB security key when prompted...',
1277
+ animationEmoji: '🛡️'
1278
+ });
1279
+
1280
+ const username = this.aggregatedDetails?.username || this.ibGib.data?.frameDetails?.username || 'user';
1281
+ const rawSalt = crypto.getRandomValues(new Uint8Array(32));
1282
+ const saltHex = bufferToHex(rawSalt.buffer as ArrayBuffer);
1283
+
1284
+ const attachmentVal = this.elements?.selectPasskeyAttachmentEl?.value;
1285
+ const authenticatorAttachment: AuthenticatorAttachment | undefined =
1286
+ (attachmentVal === 'cross-platform' || attachmentVal === 'platform')
1287
+ ? attachmentVal
1288
+ : undefined;
1289
+
1290
+ const regResult = await registerPasskeyCredential({
1291
+ username,
1292
+ displayName: username,
1293
+ salt: saltHex,
1294
+ userVerification: 'preferred',
1295
+ authenticatorAttachment,
1296
+ });
1297
+ const { credentialId, transports } = regResult;
1298
+
1299
+ let passkeySecret = regResult.passkeySecret;
1300
+ if (!passkeySecret) {
1301
+ const prfResult = await evaluatePasskeyPrf({
1302
+ credentialId,
1303
+ salt: saltHex,
1304
+ transports,
1305
+ userVerification: 'preferred'
1306
+ });
1307
+ passkeySecret = prfResult.passkeySecret;
1308
+ }
1309
+
1310
+ const defaultDeviceTag = generateDefaultDeviceTag();
1311
+ const passkeyName = defaultDeviceTag;
1312
+ const passkeyTag = getSaferSubstring({ text: passkeyName, length: 12, keepLiterals: ['-'] }) || 'key';
1313
+ const poolId = `passkey-manage-${passkeyTag}`;
1314
+
1315
+ const passkeyEntry: KeystonePasskeyEntry = {
1316
+ credentialId,
1317
+ poolId,
1318
+ name: passkeyName,
1319
+ salt: saltHex,
1320
+ createdUTC: new Date().toISOString(),
1321
+ ...(transports && transports.length > 0 ? { transports } : {})
1322
+ };
1323
+
1324
+ const metaspace = await getGlobalMetaspace_waitIfNeeded();
1325
+ if (!metaspace) throw new Error("Metaspace not found");
1326
+ const space = await metaspace.getLocalUserSpace({ lock: false });
1327
+ if (!space) throw new Error("Local space not found");
1328
+
1329
+ const keystoneService = new KeystoneService_V1();
1330
+ let evolvedKeystone: KeystoneIbGib_V1;
1331
+
1332
+ const poolConfig: KeystonePoolConfig_HashV1 = {
1333
+ id: poolId,
1334
+ type: KeystoneChallengeType.hash_reveal_v1,
1335
+ salt: poolId,
1336
+ algo: HashAlgorithm.sha_256,
1337
+ rounds: 1,
1338
+ allowedVerbs: [KEYSTONE_VERB_MANAGE, KEYSTONE_VERB_SYNC, KEYSTONE_VERB_CONNECT],
1339
+ behavior: {
1340
+ size: 10,
1341
+ replenish: KeystoneReplenishStrategy.topUp,
1342
+ selectSequentially: 1,
1343
+ selectRandomly: 0,
1344
+ targetBindingCount: 0,
1345
+ }
1346
+ };
1347
+
1348
+ const challenges = await generatePoolChallenges({
1349
+ config: poolConfig,
1350
+ masterSecret: passkeySecret
1351
+ });
1352
+
1353
+ const newPool: KeystoneChallengePool = {
1354
+ id: poolId,
1355
+ config: poolConfig,
1356
+ challenges
1357
+ };
1358
+
1359
+ let authSecret: string | undefined;
1360
+ let signingPoolId: string | undefined;
1361
+
1362
+ if (isCustodialOnly) {
1363
+ // Elevate custodial account with sovereign passkey
1364
+ this.showBusy({
1365
+ isBusy: true,
1366
+ title: 'Elevating Account Ownership...',
1367
+ msg: 'Establishing sovereign passkey pool on identity...',
1368
+ animationEmoji: '⚡'
1369
+ });
1370
+
1371
+ authSecret = passkeySecret;
1372
+ signingPoolId = poolId;
1373
+
1374
+ evolvedKeystone = await keystoneService.addPools({
1375
+ latestKeystone: this.ibGib,
1376
+ masterSecret: authSecret,
1377
+ signingPoolId,
1378
+ newPools: [newPool],
1379
+ frameDetails: {
1380
+ passkeys: {
1381
+ [credentialId]: passkeyEntry
1382
+ }
1383
+ },
1384
+ dataToPatch: {
1385
+ credentialIds: [...(this.ibGib.data?.credentialIds || []), credentialId]
1386
+ },
1387
+ metaspace,
1388
+ space
1389
+ });
1390
+ } else {
1391
+ // Secondary passkey registration on existing sovereign identity:
1392
+ // Prompt user to authorize with current master password or passkey
1393
+ try {
1394
+ const authResult = await promptForAuthoritySecret({
1395
+ keystone: this.ibGib,
1396
+ aggregatedDetails: this.aggregatedDetails,
1397
+ title: 'Authorize New Passkey',
1398
+ msg: 'Enter Master Password or touch security key to authorize adding this passkey:',
1399
+ verb: KEYSTONE_VERB_MANAGE
1400
+ });
1401
+ authSecret = authResult.secret;
1402
+ signingPoolId = authResult.poolId;
1403
+ } catch (err) {
1404
+ this.setPasskeyStatus("Registration cancelled (authorization required).", "error");
1405
+ return;
1406
+ }
1407
+
1408
+ this.showBusy({
1409
+ isBusy: true,
1410
+ title: 'Registering Passkey...',
1411
+ msg: 'Adding cryptographic challenge pool and evolving identity...',
1412
+ animationEmoji: '🛡️'
1413
+ });
1414
+
1415
+ evolvedKeystone = await keystoneService.addPools({
1416
+ latestKeystone: this.ibGib,
1417
+ masterSecret: authSecret,
1418
+ signingPoolId,
1419
+ newPools: [newPool],
1420
+ frameDetails: {
1421
+ passkeys: {
1422
+ [credentialId]: passkeyEntry
1423
+ }
1424
+ },
1425
+ dataToPatch: {
1426
+ credentialIds: [...(this.ibGib.data?.credentialIds || []), credentialId]
1427
+ },
1428
+ metaspace,
1429
+ space
1430
+ });
1431
+
1432
+ // Sync pool addition frame to server
1433
+ const primaryTjpAddr = getTjpAddr({ ibGib: evolvedKeystone, defaultIfNone: 'incomingAddr' })!;
1434
+ const resSyncPool = await getApiBridge().putEvolveKeystone({
1435
+ domainAddr: primaryTjpAddr,
1436
+ keystoneIbGib: evolvedKeystone,
1437
+ relatedKeystoneIbGibs: []
1438
+ });
1439
+ if (!resSyncPool.success) {
1440
+ throw new Error(`Server synchronization of passkey pool failed: ${resSyncPool.message}`);
1441
+ }
1442
+ }
1443
+
1444
+ // Construct outward device sync delegate for this passkey
1445
+ this.showBusy({
1446
+ isBusy: true,
1447
+ title: 'Creating Sync Delegate...',
1448
+ msg: 'Constructing outward passkey device sync delegate...',
1449
+ animationEmoji: '📱'
1450
+ });
1451
+
1452
+ const primaryTjpAddr = getTjpAddr({ ibGib: evolvedKeystone, defaultIfNone: 'incomingAddr' })!;
1453
+ const { delegateKeystone, deviceTag } = await createPasskeyDeviceSyncDelegate({
1454
+ username,
1455
+ primaryTjpAddr,
1456
+ passkeySecret,
1457
+ transports,
1458
+ metaspace,
1459
+ space
1460
+ });
1461
+
1462
+ // Register outward sync delegate on primary keystone
1463
+ this.showBusy({
1464
+ isBusy: true,
1465
+ title: 'Registering Delegate...',
1466
+ msg: 'Registering outward passkey delegate on primary identity...',
1467
+ animationEmoji: '📝'
1468
+ });
1469
+
1470
+ const delegateSigningPoolId = isCustodialOnly ? poolId : signingPoolId!;
1471
+ const delegateAuthSecret = isCustodialOnly ? passkeySecret : authSecret!;
1472
+
1473
+ evolvedKeystone = await keystoneService.registerDelegate({
1474
+ parentKeystone: evolvedKeystone,
1475
+ delegateKeystone,
1476
+ masterSecret: delegateAuthSecret,
1477
+ signingPoolId: delegateSigningPoolId,
1478
+ metaspace,
1479
+ space,
1480
+ allowedVerbs: [KEYSTONE_VERB_SYNC, KEYSTONE_VERB_CONNECT],
1481
+ deviceName: deviceTag
1482
+ });
1483
+
1484
+ // Sync evolved keystone and delegate to server
1485
+ this.showBusy({
1486
+ isBusy: true,
1487
+ title: 'Syncing to Server...',
1488
+ msg: 'Publishing evolved identity keystone and delegate to server...',
1489
+ animationEmoji: '⚡'
1490
+ });
1491
+
1492
+ const resSync = await getApiBridge().putEvolveKeystone({
1493
+ domainAddr: primaryTjpAddr,
1494
+ keystoneIbGib: evolvedKeystone,
1495
+ relatedKeystoneIbGibs: [delegateKeystone]
1496
+ });
1497
+
1498
+ if (!resSync.success) {
1499
+ throw new Error(`Server synchronization of delegate failed: ${resSync.message}`);
1500
+ }
1501
+
1502
+ // Update Special Index
1503
+ await updateSpecialIndex({
1504
+ type: "keystones",
1505
+ rel8nInfos: [
1506
+ {
1507
+ rel8nName: "keystone",
1508
+ ibGibs: [toDto({ ibGib: evolvedKeystone }), toDto({ ibGib: delegateKeystone })]
1509
+ }
1510
+ ],
1511
+ metaspace,
1512
+ space
1513
+ });
1514
+
1515
+ this.setPasskeyStatus("Passkey successfully registered and bound to identity!", "success");
1516
+ this.ibGibAddr = getIbGibAddr({ ibGib: evolvedKeystone });
1517
+ this.staticIbGib = evolvedKeystone;
1518
+ await this.loadIbGib({ getLatest: false });
1519
+
1520
+ } catch (error) {
1521
+ console.error(`${lc} ${extractErrorMsg(error)}`, error);
1522
+ this.setPasskeyStatus(`Failed to register passkey: ${extractErrorMsg(error)}`, "error");
1523
+ } finally {
1524
+ this.showBusy({ isBusy: false });
1525
+ }
1526
+ }
1527
+
1528
+ protected async handleRemovePasskey(credentialId: string): Promise<void> {
1529
+ const lc = `${this.lc}[${this.handleRemovePasskey.name}]`;
1530
+ if (!this.ibGib || !this.ibGibAddr) { return; }
1531
+
1532
+ const passkeyEntry = this.aggregatedDetails?.passkeys?.[credentialId];
1533
+ const passkeyName = passkeyEntry?.name || 'this passkey';
1534
+
1535
+ try {
1536
+ const keystoneService = new KeystoneService_V1();
1537
+ let masterSecret: string | undefined;
1538
+ let signingPoolId: string | undefined;
1539
+
1540
+ try {
1541
+ const authResult = await promptForAuthoritySecret({
1542
+ keystone: this.ibGib,
1543
+ aggregatedDetails: this.aggregatedDetails,
1544
+ title: 'Authorize Removal',
1545
+ msg: `Are you sure you want to remove "${passkeyName}"? Enter your Master Secret (Password) or touch security key to authorize:`,
1546
+ verb: KEYSTONE_VERB_MANAGE
1547
+ });
1548
+ masterSecret = authResult.secret;
1549
+ signingPoolId = authResult.poolId;
1550
+ } catch (err) {
1551
+ this.setPasskeyStatus("Removal cancelled or unauthorized.", "error");
1552
+ return;
1553
+ }
1554
+
1555
+ this.showBusy({
1556
+ isBusy: true,
1557
+ title: 'Revoking Passkey...',
1558
+ msg: 'Tombstoning passkey record and removing challenge pool...',
1559
+ animationEmoji: '🗑️'
1560
+ });
1561
+
1562
+ const metaspace = await getGlobalMetaspace_waitIfNeeded();
1563
+ if (!metaspace) throw new Error("Metaspace not found");
1564
+ const space = await metaspace.getLocalUserSpace({ lock: false });
1565
+ if (!space) throw new Error("Local space not found");
1566
+
1567
+ const poolIdToRemove = passkeyEntry?.poolId || `passkey-manage-${credentialId.substring(0, 8)}`;
1568
+ const poolExists = this.ibGib.data?.challengePools.some(p => p.id === poolIdToRemove);
1569
+
1570
+ const filteredCredentialIds = (this.ibGib.data?.credentialIds || []).filter(id => id !== credentialId);
1571
+
1572
+ let evolvedKeystone: KeystoneIbGib_V1;
1573
+ if (poolExists) {
1574
+ evolvedKeystone = await keystoneService.removePools({
1575
+ latestKeystone: this.ibGib,
1576
+ masterSecret: masterSecret!,
1577
+ signingPoolId: signingPoolId!,
1578
+ poolIds: [poolIdToRemove],
1579
+ frameDetails: {
1580
+ passkeys: {
1581
+ [credentialId]: null
1582
+ }
1583
+ },
1584
+ dataToPatch: {
1585
+ credentialIds: filteredCredentialIds
1586
+ },
1587
+ metaspace,
1588
+ space
1589
+ });
1590
+ } else {
1591
+ // If pool doesn't exist, just update frameDetails
1592
+ evolvedKeystone = await keystoneService.sign({
1593
+ latestKeystone: this.ibGib,
1594
+ masterSecret: masterSecret!,
1595
+ poolId: signingPoolId,
1596
+ claim: {
1597
+ target: getIbGibAddr({ ibGib: this.ibGib })
1598
+ },
1599
+ frameDetails: {
1600
+ passkeys: {
1601
+ [credentialId]: null
1602
+ }
1603
+ },
1604
+ dataToPatch: {
1605
+ credentialIds: filteredCredentialIds
1606
+ },
1607
+ metaspace,
1608
+ space
1609
+ });
1610
+ }
1611
+
1612
+ // Sync to server
1613
+ this.showBusy({
1614
+ isBusy: true,
1615
+ title: 'Syncing to Server...',
1616
+ msg: 'Publishing updated keystone to server...',
1617
+ animationEmoji: '⚡'
1618
+ });
1619
+
1620
+ const primaryTjpAddr = getTjpAddr({ ibGib: evolvedKeystone, defaultIfNone: 'incomingAddr' })!;
1621
+ await getApiBridge().putEvolveKeystone({
1622
+ domainAddr: primaryTjpAddr,
1623
+ keystoneIbGib: evolvedKeystone,
1624
+ relatedKeystoneIbGibs: []
1625
+ });
1626
+
1627
+ // Update Special Index
1628
+ await updateSpecialIndex({
1629
+ type: "keystones",
1630
+ rel8nInfos: [
1631
+ {
1632
+ rel8nName: "keystone",
1633
+ ibGibs: [toDto({ ibGib: evolvedKeystone })]
1634
+ }
1635
+ ],
1636
+ metaspace,
1637
+ space
1638
+ });
1639
+
1640
+ this.setPasskeyStatus(`Passkey "${passkeyName}" successfully removed.`, "success");
1641
+ this.ibGibAddr = getIbGibAddr({ ibGib: evolvedKeystone });
1642
+ this.staticIbGib = evolvedKeystone;
1643
+ await this.loadIbGib({ getLatest: false });
1644
+
1645
+ } catch (error) {
1646
+ console.error(`${lc} ${extractErrorMsg(error)}`, error);
1647
+ this.setPasskeyStatus(`Failed to remove passkey: ${extractErrorMsg(error)}`, "error");
1648
+ } finally {
1649
+ this.showBusy({ isBusy: false });
1650
+ }
1651
+ }
1652
+
1088
1653
  override async handleContextUpdated(): Promise<void> {
1089
1654
  const lc = `${this.lc}[${this.handleContextUpdated.name}]`;
1090
1655
  try {
@@ -1124,10 +1689,26 @@ export class KeystoneDetailsComponentInstance
1124
1689
  const lc = `${this.lc}[${this.handleSSOLink.name}]`;
1125
1690
  this.setSSOStatus(`Linking ${providerId}...`, "info");
1126
1691
 
1692
+ const providerName = providerId === 'google' ? 'Google' : providerId === 'github' ? 'GitHub' : providerId;
1693
+ this.setSSOStatus(`Opening ${providerName} SSO authorization window...`, "info");
1694
+ this.showBusy({
1695
+ isBusy: true,
1696
+ title: `Connecting to ${providerName}...`,
1697
+ msg: `Opening ${providerName} SSO authorization window...\nPlease complete authorization in the popup window.`,
1698
+ animationEmoji: '🌐'
1699
+ });
1700
+
1127
1701
  try {
1702
+ const primaryTjpAddr = (this.ibGib ? getTjpAddr({ ibGib: this.ibGib }) : undefined)
1703
+ || (this.ibGibAddr ? (() => {
1704
+ const { ib } = getIbAndGib({ ibGibAddr: this.ibGibAddr });
1705
+ const { tjpGib } = getGibInfo({ ibGibAddr: this.ibGibAddr });
1706
+ return tjpGib && ib ? `${ib}^${tjpGib}` : this.ibGibAddr;
1707
+ })() : undefined);
1708
+
1128
1709
  const resPopup = await executeSsoOAuthPopup({
1129
1710
  providerId,
1130
- parentTjpAddr: this.ibGibAddr
1711
+ parentTjpAddr: primaryTjpAddr
1131
1712
  });
1132
1713
 
1133
1714
  if (!resPopup.success || !resPopup.code) {
@@ -1139,6 +1720,8 @@ export class KeystoneDetailsComponentInstance
1139
1720
  } catch (error) {
1140
1721
  console.error(`${lc} SSO link initiation failed: ${extractErrorMsg(error)}`);
1141
1722
  this.setSSOStatus(`Link failed: ${extractErrorMsg(error)}`, "error");
1723
+ } finally {
1724
+ this.showBusy({ isBusy: false });
1142
1725
  }
1143
1726
  }
1144
1727
 
@@ -1151,7 +1734,14 @@ export class KeystoneDetailsComponentInstance
1151
1734
 
1152
1735
  protected async handleSSOLinkCallback(code: string, stateObj: any, redirectUri: string) {
1153
1736
  const lc = `${this.lc}[${this.handleSSOLinkCallback.name}]`;
1154
- this.setSSOStatus("Retrieving challenge pool configuration from server...", "info");
1737
+ const providerName = stateObj.providerId === 'google' ? 'Google' : stateObj.providerId === 'github' ? 'GitHub' : stateObj.providerId;
1738
+ this.setSSOStatus(`Retrieving challenge pool configuration from server...`, "info");
1739
+ this.showBusy({
1740
+ isBusy: true,
1741
+ title: `Linking ${providerName}...`,
1742
+ msg: `Validating ${providerName} authorization with server...`,
1743
+ animationEmoji: '🔐'
1744
+ });
1155
1745
 
1156
1746
  try {
1157
1747
  // 1. Exchange OAuth code for custodian public pool config
@@ -1178,50 +1768,32 @@ export class KeystoneDetailsComponentInstance
1178
1768
 
1179
1769
  const custodianPool = linkData.custodianPool;
1180
1770
 
1181
- // 2. Prompt user for master secret and verify it
1771
+ // 2. Prompt user for authorization secret
1182
1772
  const keystoneService = new KeystoneService_V1();
1183
1773
  let masterSecret: string | undefined;
1184
- let isCorrect = false;
1185
- let tries = 0;
1186
- const maxTries = 8;
1187
-
1188
- while (tries < maxTries && !isCorrect) {
1189
- try {
1190
- const promptMsg = tries === 0
1191
- ? "Link successful! Enter your local Master Secret (Password) to sign and register the custodian pool"
1192
- : `Incorrect password. Please try again (${maxTries - tries} tries remaining):`;
1193
- masterSecret = await promptForSecret({
1194
- msg: promptMsg,
1195
- confirm: false
1196
- });
1197
- } catch (error) {
1198
- // cancelled
1199
- if (logalot) { console.log(`${lc}[promptForSecret] user cancelled (I: 911ff1fb0234949228481968e9087626)`); }
1200
- masterSecret = undefined;
1201
- break;
1202
- }
1203
-
1204
- if (!masterSecret) {
1205
- break;
1206
- }
1774
+ let signingPoolId: string | undefined;
1207
1775
 
1208
- // Verify secret using verifySigningSecret (requires manage poolId)
1209
- isCorrect = await keystoneService.verifySigningSecret({
1210
- keystoneIbGib: this.ibGib!,
1211
- signingSecret: masterSecret,
1212
- poolId: 'manage'
1776
+ try {
1777
+ const authResult = await promptForAuthoritySecret({
1778
+ keystone: this.ibGib,
1779
+ aggregatedDetails: this.aggregatedDetails,
1780
+ title: `Link ${providerName}`,
1781
+ msg: `Enter Master Password or touch security key to authorize linking ${providerName}:`,
1782
+ verb: KEYSTONE_VERB_MANAGE
1213
1783
  });
1214
-
1215
- if (!isCorrect) {
1216
- tries++;
1217
- }
1218
- }
1219
-
1220
- if (!isCorrect) {
1221
- this.setSSOStatus("Link cancelled or failed (incorrect password).", "error");
1784
+ masterSecret = authResult.secret;
1785
+ signingPoolId = authResult.poolId;
1786
+ } catch (error) {
1787
+ this.setSSOStatus("Link cancelled or failed.", "error");
1222
1788
  return;
1223
1789
  }
1224
1790
 
1791
+ this.showBusy({
1792
+ isBusy: true,
1793
+ title: 'Evolving Identity...',
1794
+ msg: `Installing ${providerName} custodian pool and signing evolution...`,
1795
+ animationEmoji: '📝'
1796
+ });
1225
1797
  this.setSSOStatus("Evolving identity locally and generating cryptographic signatures...", "info");
1226
1798
 
1227
1799
  const metaspace = await getGlobalMetaspace_waitIfNeeded();
@@ -1229,33 +1801,74 @@ export class KeystoneDetailsComponentInstance
1229
1801
  const space = await metaspace.getLocalUserSpace({ lock: false });
1230
1802
  if (!space) { throw new Error("Local space not found"); }
1231
1803
 
1232
- // 3. Evolve parent keystone to include the custodian pool
1804
+ // 3. Evolve parent keystone to include the custodian pool (n -> n+1)
1233
1805
  const evolvedParent = await keystoneService.addPools({
1234
1806
  latestKeystone: this.ibGib!,
1235
1807
  masterSecret: masterSecret!,
1808
+ signingPoolId: signingPoolId!,
1236
1809
  newPools: [custodianPool],
1237
1810
  metaspace,
1238
1811
  space
1239
1812
  });
1240
1813
 
1241
- // 4. Sync evolved parent keystone back to server
1242
- this.setSSOStatus("Publishing evolved identity keystone to server...", "info");
1243
- const syncRes = await getApiBridge().putEvolveKeystone({
1814
+ // Sync evolved parent (n+1) to server first
1815
+ const syncRes1 = await getApiBridge().putEvolveKeystone({
1244
1816
  domainAddr: stateObj.parentTjpAddr,
1245
1817
  keystoneIbGib: evolvedParent,
1246
- relatedKeystoneIbGibs: [] // No outward delegate timelines
1818
+ relatedKeystoneIbGibs: []
1247
1819
  });
1248
- if (!syncRes.success) {
1249
- throw new Error(`Server synchronization rejected: ${syncRes.message}`);
1820
+ if (!syncRes1.success) {
1821
+ throw new Error(`Server synchronization of linked pool rejected: ${syncRes1.message}`);
1250
1822
  }
1251
1823
 
1252
- // 5. Update index locally
1824
+ // 4. Create and register OAuth2 sync delegate for this provider (n+1 -> n+2)
1825
+ const username = this.aggregatedDetails?.username || this.ibGib?.data?.username || 'user';
1826
+ const { delegateKeystone, deviceTag } = await createOAuth2DeviceSyncDelegate({
1827
+ providerId: stateObj.providerId,
1828
+ username,
1829
+ primaryTjpAddr: stateObj.parentTjpAddr,
1830
+ custodianReqs: linkData.custodianReqs,
1831
+ metaspace,
1832
+ space,
1833
+ });
1834
+
1835
+ const parentWithDelegate = await keystoneService.registerDelegate({
1836
+ parentKeystone: evolvedParent,
1837
+ delegateKeystone,
1838
+ masterSecret: masterSecret!,
1839
+ signingPoolId: signingPoolId!,
1840
+ metaspace,
1841
+ space,
1842
+ allowedVerbs: [KEYSTONE_VERB_SYNC, KEYSTONE_VERB_CONNECT],
1843
+ deviceName: deviceTag,
1844
+ });
1845
+
1846
+ await metaspace.put({ ibGibs: [delegateKeystone, parentWithDelegate], space });
1847
+
1848
+ // 5. Sync parent with registered delegate (n+2) to server
1849
+ this.showBusy({
1850
+ isBusy: true,
1851
+ title: 'Syncing to Server...',
1852
+ msg: 'Publishing linked identity and delegate to server...',
1853
+ animationEmoji: '⚡'
1854
+ });
1855
+ this.setSSOStatus("Publishing evolved identity keystone to server...", "info");
1856
+ const syncRes2 = await getApiBridge().putEvolveKeystone({
1857
+ domainAddr: stateObj.parentTjpAddr,
1858
+ keystoneIbGib: parentWithDelegate,
1859
+ relatedKeystoneIbGibs: [delegateKeystone]
1860
+ });
1861
+ if (!syncRes2.success) {
1862
+ throw new Error(`Server synchronization of delegate rejected: ${syncRes2.message}`);
1863
+ }
1864
+
1865
+ // 6. Update index locally
1253
1866
  await updateSpecialIndex({
1254
1867
  type: "keystones",
1255
1868
  rel8nInfos: [
1256
1869
  {
1257
1870
  rel8nName: "keystone",
1258
- ibGibs: [toDto({ ibGib: evolvedParent })]
1871
+ ibGibs: [toDto({ ibGib: parentWithDelegate }), toDto({ ibGib: delegateKeystone })]
1259
1872
  }
1260
1873
  ],
1261
1874
  metaspace,
@@ -1263,18 +1876,24 @@ export class KeystoneDetailsComponentInstance
1263
1876
  });
1264
1877
 
1265
1878
  this.setSSOStatus(`Successfully linked with ${stateObj.providerId}!`, "success");
1879
+ this.ibGibAddr = getIbGibAddr({ ibGib: parentWithDelegate });
1880
+ this.staticIbGib = parentWithDelegate;
1266
1881
  await this.loadIbGib({ getLatest: false });
1267
1882
 
1268
1883
  } catch (error) {
1269
1884
  console.error(`${lc} SSO callback processing failed: ${extractErrorMsg(error)}`);
1270
1885
  this.setSSOStatus(`Link failed: ${extractErrorMsg(error)}`, "error");
1886
+ } finally {
1887
+ this.showBusy({ isBusy: false });
1271
1888
  }
1272
1889
  }
1273
1890
 
1274
1891
  protected async handleSSOClick(providerId: 'google' | 'github') {
1275
- const isLinked = isSsoProviderLinked({ keystone: this.ibGib, aggregatedDetails: this.aggregatedDetails, providerId });
1892
+ const isLinked = !!this.ibGib ?
1893
+ isSsoProviderLinked({ keystone: this.ibGib, aggregatedDetails: this.aggregatedDetails, providerId }) :
1894
+ false;
1276
1895
  if (isLinked) {
1277
- const caps = getKeystoneCapabilities({ keystone: this.ibGib, aggregatedDetails: this.aggregatedDetails });
1896
+ const caps = getKeystoneCapabilities({ keystone: this.ibGib!, aggregatedDetails: this.aggregatedDetails });
1278
1897
  if (!caps.isSovereign) {
1279
1898
  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");
1280
1899
  return;
@@ -1287,51 +1906,36 @@ export class KeystoneDetailsComponentInstance
1287
1906
 
1288
1907
  protected async handleSSOUnlink(providerId: 'google' | 'github') {
1289
1908
  const lc = `${this.lc}[${this.handleSSOUnlink.name}]`;
1290
- this.setSSOStatus(`Unlinking ${providerId}...`, "info");
1909
+ const providerName = providerId === 'google' ? 'Google' : providerId === 'github' ? 'GitHub' : providerId;
1910
+ this.setSSOStatus(`Unlinking ${providerName}...`, "info");
1291
1911
 
1292
1912
  try {
1293
- // 1. Prompt user for password
1913
+ // 1. Prompt user for authorization secret
1294
1914
  const keystoneService = new KeystoneService_V1();
1295
1915
  let masterSecret: string | undefined;
1296
- let isCorrect = false;
1297
- let tries = 0;
1298
- const maxTries = 8;
1916
+ let signingPoolId: string | undefined;
1299
1917
 
1300
- while (tries < maxTries && !isCorrect) {
1301
- try {
1302
- const promptMsg = tries === 0
1303
- ? `Are you sure you want to unlink your ${providerId} account? If so, enter your local Master Secret (Password) to authorize. Otherwise, click Cancel.`
1304
- : `Incorrect password. Please try again (${maxTries - tries} tries remaining):`;
1305
- masterSecret = await promptForSecret({
1306
- msg: promptMsg,
1307
- confirm: false
1308
- });
1309
- } catch (error) {
1310
- masterSecret = undefined;
1311
- break;
1312
- }
1313
-
1314
- if (!masterSecret) {
1315
- break;
1316
- }
1317
-
1318
- // Verify secret using verifySigningSecret (requires manage poolId)
1319
- isCorrect = await keystoneService.verifySigningSecret({
1320
- keystoneIbGib: this.ibGib!,
1321
- signingSecret: masterSecret,
1322
- poolId: 'manage'
1918
+ try {
1919
+ const authResult = await promptForAuthoritySecret({
1920
+ keystone: this.ibGib,
1921
+ aggregatedDetails: this.aggregatedDetails,
1922
+ title: `Unlink ${providerName}`,
1923
+ msg: `Are you sure you want to unlink your ${providerName} account? If so, enter your Master Password or touch security key to authorize.`,
1924
+ verb: KEYSTONE_VERB_MANAGE
1323
1925
  });
1324
-
1325
- if (!isCorrect) {
1326
- tries++;
1327
- }
1328
- }
1329
-
1330
- if (!isCorrect) {
1331
- this.setSSOStatus("Unlink cancelled or failed (incorrect password).", "error");
1926
+ masterSecret = authResult.secret;
1927
+ signingPoolId = authResult.poolId;
1928
+ } catch (error) {
1929
+ this.setSSOStatus("Unlink cancelled or failed.", "error");
1332
1930
  return;
1333
1931
  }
1334
1932
 
1933
+ this.showBusy({
1934
+ isBusy: true,
1935
+ title: 'Unlinking Provider...',
1936
+ msg: `Removing ${providerName} custodian pool and delegate keys...`,
1937
+ animationEmoji: '🔓'
1938
+ });
1335
1939
  this.setSSOStatus("Evolving identity locally and removing custodian pool...", "info");
1336
1940
 
1337
1941
  const metaspace = await getGlobalMetaspace_waitIfNeeded();
@@ -1344,38 +1948,69 @@ export class KeystoneDetailsComponentInstance
1344
1948
  let evolvedParent = await keystoneService.removePools({
1345
1949
  latestKeystone: this.ibGib!,
1346
1950
  masterSecret: masterSecret!,
1951
+ signingPoolId: signingPoolId!,
1347
1952
  poolIds: [targetPoolId],
1348
1953
  metaspace,
1349
1954
  space
1350
1955
  });
1351
1956
 
1352
- // Unregister any corresponding outward delegate for this provider if present
1353
- const delegatesMap = this.ibGib?.data?.delegates;
1957
+ const primaryTjpAddr = (this.ibGib ? getTjpAddr({ ibGib: this.ibGib }) : undefined)
1958
+ || (this.ibGibAddr ? (() => {
1959
+ const { ib } = getIbAndGib({ ibGibAddr: this.ibGibAddr });
1960
+ const { tjpGib } = getGibInfo({ ibGibAddr: this.ibGibAddr });
1961
+ return tjpGib && ib ? `${ib}^${tjpGib}` : this.ibGibAddr;
1962
+ })() : undefined)
1963
+ || getIbGibAddr({ ibGib: this.ibGib! });
1964
+
1965
+ // 3. Sync pool removal frame to server
1966
+ this.showBusy({
1967
+ isBusy: true,
1968
+ title: 'Syncing to Server...',
1969
+ msg: `Publishing ${providerName} pool removal to server...`,
1970
+ animationEmoji: '⚡'
1971
+ });
1972
+ this.setSSOStatus("Publishing evolved identity keystone to server...", "info");
1973
+ let syncRes = await getApiBridge().putEvolveKeystone({
1974
+ domainAddr: primaryTjpAddr,
1975
+ keystoneIbGib: evolvedParent,
1976
+ relatedKeystoneIbGibs: [] // No outward delegate timelines
1977
+ });
1978
+ if (!syncRes.success) {
1979
+ throw new Error(`Server synchronization rejected: ${syncRes.message}`);
1980
+ }
1981
+
1982
+ // 4. Unregister any corresponding outward delegate for this provider if present
1983
+ const delegatesMap = evolvedParent.data?.delegates || this.ibGib?.data?.delegates;
1354
1984
  if (delegatesMap) {
1355
1985
  for (const [delegateTjpAddr, info] of Object.entries(delegatesMap)) {
1356
- if (info.deviceName?.includes(providerId)) {
1986
+ if (isCustodialDelegate({ delegateInfo: info, providerId })) {
1357
1987
  evolvedParent = await keystoneService.unregisterDelegate({
1358
1988
  parentKeystone: evolvedParent,
1359
1989
  delegateTjpAddr,
1360
1990
  masterSecret: masterSecret!,
1991
+ signingPoolId: 'manage',
1361
1992
  metaspace,
1362
1993
  space,
1363
1994
  });
1995
+
1996
+ this.showBusy({
1997
+ isBusy: true,
1998
+ title: 'Syncing to Server...',
1999
+ msg: `Publishing ${providerName} delegate unregistration to server...`,
2000
+ animationEmoji: '⚡'
2001
+ });
2002
+ syncRes = await getApiBridge().putEvolveKeystone({
2003
+ domainAddr: primaryTjpAddr,
2004
+ keystoneIbGib: evolvedParent,
2005
+ relatedKeystoneIbGibs: []
2006
+ });
2007
+ if (!syncRes.success) {
2008
+ throw new Error(`Server synchronization rejected: ${syncRes.message}`);
2009
+ }
1364
2010
  }
1365
2011
  }
1366
2012
  }
1367
2013
 
1368
- // 3. Sync evolved parent keystone back to server
1369
- this.setSSOStatus("Publishing evolved identity keystone to server...", "info");
1370
- const syncRes = await getApiBridge().putEvolveKeystone({
1371
- domainAddr: getIbGibAddr({ ibGib: this.ibGib! }),
1372
- keystoneIbGib: evolvedParent,
1373
- relatedKeystoneIbGibs: [] // No outward delegate timelines
1374
- });
1375
- if (!syncRes.success) {
1376
- throw new Error(`Server synchronization rejected: ${syncRes.message}`);
1377
- }
1378
-
1379
2014
  // 4. Update index locally
1380
2015
  await updateSpecialIndex({
1381
2016
  type: "keystones",
@@ -1389,15 +2024,21 @@ export class KeystoneDetailsComponentInstance
1389
2024
  space
1390
2025
  });
1391
2026
 
1392
- this.setSSOStatus(`Successfully unlinked ${providerId}!`, "success");
1393
- await this.loadIbGib({ getLatest: true });
2027
+ this.setSSOStatus(`Successfully unlinked ${providerName}!`, "success");
2028
+ this.ibGibAddr = getIbGibAddr({ ibGib: evolvedParent });
2029
+ this.staticIbGib = evolvedParent;
2030
+ await this.loadIbGib({ getLatest: false });
1394
2031
 
1395
2032
  } catch (error) {
1396
- console.error(`${lc} SSO unlink failed: ${extractErrorMsg(error)}`);
1397
- this.setSSOStatus(`Unlink failed: ${extractErrorMsg(error)}`, "error");
2033
+ console.error(`${lc} ${extractErrorMsg(error)}`, error);
2034
+ this.setSSOStatus(`Failed to unlink ${providerName}: ${extractErrorMsg(error)}`, "error");
2035
+ } finally {
2036
+ this.showBusy({ isBusy: false });
1398
2037
  }
1399
2038
  }
1400
2039
 
2040
+
2041
+
1401
2042
  private setChangePasswordStatus(msg: string, type: 'info' | 'success' | 'error') {
1402
2043
  const el = this.elements!.changePasswordStatusEl;
1403
2044
  el.textContent = msg;
@@ -1445,8 +2086,17 @@ export class KeystoneDetailsComponentInstance
1445
2086
  challenges: newChallenges,
1446
2087
  };
1447
2088
 
1448
- const primaryTjpAddr = getTjpAddr({ ibGib: this.ibGib! }) || getIbGibAddr({ ibGib: this.ibGib! });
1449
- const custodianProvider = this.aggregatedDetails?.custodianProvider || 'google';
2089
+ const primaryTjpAddr = (this.ibGib ? getTjpAddr({ ibGib: this.ibGib }) : undefined)
2090
+ || (this.ibGibAddr ? (() => {
2091
+ const { ib } = getIbAndGib({ ibGibAddr: this.ibGibAddr });
2092
+ const { tjpGib } = getGibInfo({ ibGibAddr: this.ibGibAddr });
2093
+ return tjpGib && ib ? `${ib}^${tjpGib}` : this.ibGibAddr;
2094
+ })() : undefined)
2095
+ || getIbGibAddr({ ibGib: this.ibGib! });
2096
+
2097
+ const custodianProvider = this.aggregatedDetails?.custodianProvider
2098
+ || this.ibGib?.data?.frameDetails?.custodianProvider
2099
+ || (this.ibGib?.data as any)?.custodianProvider;
1450
2100
 
1451
2101
  let requestBody: any = {
1452
2102
  primaryTjpAddr,
@@ -1476,17 +2126,69 @@ export class KeystoneDetailsComponentInstance
1476
2126
  requestBody.code = resPopup.code;
1477
2127
  requestBody.redirectUri = resPopup.redirectUri;
1478
2128
  } else {
1479
- this.showBusy({ isBusy: false });
1480
- // Email OTP fallback
1481
- const email = this.aggregatedDetails?.emails?.[0] || this.ibGib?.data?.emails?.[0];
1482
- const otpCode = await promptForSecret({
1483
- msg: `Enter 6-digit verification code sent to ${email}:`,
1484
- confirm: false,
1485
- });
1486
- if (!otpCode) {
1487
- this.setChangePasswordStatus("Verification code required.", "error");
2129
+ // Email OTP flow
2130
+ const email = this.aggregatedDetails?.emails?.[0]
2131
+ || this.ibGib?.data?.emails?.[0]
2132
+ || this.ibGib?.data?.frameDetails?.emails?.[0];
2133
+
2134
+ if (!email) {
2135
+ this.showBusy({ isBusy: false });
2136
+ this.setChangePasswordStatus("No email address found for this identity to send verification code.", "error");
1488
2137
  return;
1489
2138
  }
2139
+
2140
+ const otpSection = this.elements?.sectionAddPasswordOtpEl;
2141
+ const otpInput = this.elements?.inputAddPasswordOtpEl;
2142
+ const isOtpVisible = otpSection && !otpSection.classList.contains('hidden');
2143
+
2144
+ // Step 1: If OTP section is not visible yet, send code and show the OTP input inline in the card
2145
+ if (!isOtpVisible) {
2146
+ this.showBusy({
2147
+ isBusy: true,
2148
+ title: 'Sending Verification Code...',
2149
+ msg: `Sending 6-digit verification code to ${email}...`,
2150
+ animationEmoji: '📧'
2151
+ });
2152
+ this.setChangePasswordStatus(`Sending verification code to ${email}...`, "info");
2153
+
2154
+ const resCode = await getApiBridge().postVerificationCode({
2155
+ email,
2156
+ primaryTjpAddr,
2157
+ });
2158
+
2159
+ this.showBusy({ isBusy: false });
2160
+
2161
+ if (!resCode.success) {
2162
+ this.setChangePasswordStatus(resCode.message || "Failed to send verification code.", "error");
2163
+ return;
2164
+ }
2165
+
2166
+ if (otpSection) {
2167
+ otpSection.classList.remove('hidden');
2168
+ }
2169
+ if (this.elements?.descAddPasswordOtpEl) {
2170
+ this.elements.descAddPasswordOtpEl.textContent = `A 6-digit verification code was sent to ${email}. Enter it below to establish sovereign ownership:`;
2171
+ }
2172
+ if (this.elements?.btnChangePasswordEl) {
2173
+ this.elements.btnChangePasswordEl.textContent = "Verify Code & Add Master Password";
2174
+ }
2175
+ this.setChangePasswordStatus(`✉️ 6-digit verification code sent to ${email}. Enter the code and click "Verify Code & Add Master Password".`, "info");
2176
+ this.startResendCooldownTimer(30);
2177
+ if (otpInput) {
2178
+ otpInput.value = '';
2179
+ otpInput.focus();
2180
+ }
2181
+ return;
2182
+ }
2183
+
2184
+ // Step 2: OTP section is visible - validate entered code
2185
+ const otpCode = (otpInput?.value || '').trim();
2186
+ if (!otpCode || otpCode.length < 6) {
2187
+ this.setChangePasswordStatus("Please enter the complete 6-digit verification code sent to your email.", "error");
2188
+ if (otpInput) { otpInput.focus(); }
2189
+ return;
2190
+ }
2191
+
1490
2192
  requestBody.email = email;
1491
2193
  requestBody.code = otpCode;
1492
2194
  }
@@ -1517,42 +2219,371 @@ export class KeystoneDetailsComponentInstance
1517
2219
  const space = await metaspace.getLocalUserSpace({ lock: false });
1518
2220
  if (!space) { throw new Error("Local user space not available"); }
1519
2221
 
1520
- await metaspace.put({ ibGibs: [evolvedParent], space });
1521
- await metaspace.registerNewIbGib({ ibGib: evolvedParent, space });
1522
- await updateSpecialIndex({
1523
- type: "keystones",
1524
- rel8nInfos: [
1525
- {
1526
- rel8nName: "keystone",
1527
- ibGibs: [toDto({ ibGib: evolvedParent })]
2222
+ if (resJson.primaryGraph) {
2223
+ const graphIbGibs = Object.values(resJson.primaryGraph) as any[];
2224
+ await metaspace.put({ ibGibs: graphIbGibs, space });
2225
+ const timelinesMap = getTimelinesGroupedByTjp({ ibGibs: graphIbGibs });
2226
+ for (const tjpKey of Object.keys(timelinesMap)) {
2227
+ for (const frame of timelinesMap[tjpKey]) {
2228
+ await metaspace.registerNewIbGib({ ibGib: frame, space });
1528
2229
  }
1529
- ],
2230
+ }
2231
+ } else {
2232
+ await metaspace.put({ ibGibs: [evolvedParent], space });
2233
+ await metaspace.registerNewIbGib({ ibGib: evolvedParent, space });
2234
+ }
2235
+
2236
+ await this.registerAndSyncSovereignPasswordDelegate({
2237
+ evolvedParent,
2238
+ newPassword,
2239
+ primaryTjpAddr,
1530
2240
  metaspace,
1531
2241
  space
1532
2242
  });
1533
2243
 
1534
- const evolvedAddr = getIbGibAddr({ ibGib: evolvedParent });
2244
+ } catch (error) {
2245
+ console.error(`${lc} Elevation failed: ${extractErrorMsg(error)}`);
2246
+ this.setChangePasswordStatus(`Failed to add master password: ${extractErrorMsg(error)}`, "error");
2247
+ if (this.elements?.inputAddPasswordOtpEl && !this.elements.sectionAddPasswordOtpEl?.classList.contains('hidden')) {
2248
+ this.elements.inputAddPasswordOtpEl.select();
2249
+ this.elements.inputAddPasswordOtpEl.focus();
2250
+ }
2251
+ } finally {
2252
+ this.showBusy({ isBusy: false });
2253
+ }
2254
+ }
1535
2255
 
1536
- // Dispatch EVENT_IBGIB_IDENTITY_CHANGED
1537
- window.dispatchEvent(new CustomEvent(EVENT_IBGIB_IDENTITY_CHANGED, {
1538
- detail: { activeIdentityAddr: evolvedAddr },
1539
- bubbles: true,
1540
- composed: true
1541
- }));
2256
+ protected async handleResendAddPasswordOtp(): Promise<void> {
2257
+ const lc = `${this.lc}[${this.handleResendAddPasswordOtp.name}]`;
2258
+ const email = this.aggregatedDetails?.emails?.[0]
2259
+ || this.ibGib?.data?.emails?.[0]
2260
+ || this.ibGib?.data?.frameDetails?.emails?.[0];
2261
+ if (!email) {
2262
+ this.setChangePasswordStatus("No email address found for this identity to send verification code.", "error");
2263
+ return;
2264
+ }
2265
+ const primaryTjpAddr = (this.ibGib ? getTjpAddr({ ibGib: this.ibGib }) : undefined)
2266
+ || (this.ibGibAddr ? (() => {
2267
+ const { ib } = getIbAndGib({ ibGibAddr: this.ibGibAddr });
2268
+ const { tjpGib } = getGibInfo({ ibGibAddr: this.ibGibAddr });
2269
+ return tjpGib && ib ? `${ib}^${tjpGib}` : this.ibGibAddr;
2270
+ })() : undefined)
2271
+ || getIbGibAddr({ ibGib: this.ibGib! });
2272
+
2273
+ const btn = this.elements!.btnResendAddPasswordOtpEl;
2274
+ if (btn) {
2275
+ btn.disabled = true;
2276
+ btn.textContent = "Sending...";
2277
+ }
2278
+ this.setChangePasswordStatus(`Resending verification code to ${email}...`, "info");
2279
+ try {
2280
+ const resCode = await getApiBridge().postVerificationCode({ email, primaryTjpAddr });
2281
+ if (resCode.success) {
2282
+ this.setChangePasswordStatus(`✉️ Fresh 6-digit verification code sent to ${email}!`, "success");
2283
+ this.startResendCooldownTimer(30);
2284
+ if (this.elements?.inputAddPasswordOtpEl) {
2285
+ this.elements.inputAddPasswordOtpEl.value = '';
2286
+ this.elements.inputAddPasswordOtpEl.focus();
2287
+ }
2288
+ } else {
2289
+ this.setChangePasswordStatus(resCode.message || "Failed to resend verification code.", "error");
2290
+ if (btn) {
2291
+ btn.disabled = false;
2292
+ btn.textContent = "Resend Code";
2293
+ }
2294
+ }
2295
+ } catch (err) {
2296
+ this.setChangePasswordStatus(`Error resending verification code: ${extractErrorMsg(err)}`, "error");
2297
+ if (btn) {
2298
+ btn.disabled = false;
2299
+ btn.textContent = "Resend Code";
2300
+ }
2301
+ }
2302
+ }
1542
2303
 
1543
- this.setChangePasswordStatus("Successfully added Master Password! Identity is now sovereign.", "success");
2304
+ /**
2305
+ * Common DRY post-elevation workflow: creates and registers the password-based
2306
+ * device sync delegate, synchronizes the evolved identity with the delegate to the server,
2307
+ * updates the local space and special index, and dispatches the identity change event.
2308
+ */
2309
+ private async registerAndSyncSovereignPasswordDelegate({
2310
+ evolvedParent,
2311
+ newPassword,
2312
+ primaryTjpAddr,
2313
+ metaspace,
2314
+ space,
2315
+ }: {
2316
+ evolvedParent: KeystoneIbGib_V1;
2317
+ newPassword: string;
2318
+ primaryTjpAddr: string;
2319
+ metaspace: any;
2320
+ space: any;
2321
+ }): Promise<KeystoneIbGib_V1> {
2322
+ const lc = `${this.lc}[${this.registerAndSyncSovereignPasswordDelegate.name}]`;
2323
+
2324
+ // 1. Create a password-based sync delegate for this device
2325
+ this.showBusy({
2326
+ isBusy: true,
2327
+ title: 'Creating Sovereign Sync Delegate...',
2328
+ msg: 'Deriving local device sync delegate keys from Master Password...',
2329
+ animationEmoji: '🔑'
2330
+ });
2331
+ await delay(50);
2332
+
2333
+ const rawUsername = this.aggregatedDetails?.username
2334
+ || this.ibGib?.data?.username
2335
+ || this.aggregatedDetails?.emails?.[0]?.split('@')[0]
2336
+ || this.ibGib?.data?.emails?.[0]?.split('@')[0]
2337
+ || 'user';
2338
+
2339
+ const { delegateKeystone: userSyncDelegate, deviceTag } = await createSovereignDeviceSyncDelegate({
2340
+ username: rawUsername,
2341
+ primaryTjpAddr,
2342
+ masterSecret: newPassword,
2343
+ metaspace,
2344
+ space,
2345
+ });
1544
2346
 
1545
- // Refresh the view
1546
- this.ibGibAddr = evolvedAddr;
1547
- this.staticIbGib = evolvedParent;
1548
- await this.loadIbGib({ getLatest: true });
2347
+ // 2. Register the new user sync delegate on the evolved parent keystone
2348
+ this.showBusy({
2349
+ isBusy: true,
2350
+ title: 'Registering Sovereign Delegate...',
2351
+ msg: 'Registering user sync delegate on primary keystone...',
2352
+ animationEmoji: '📝'
2353
+ });
2354
+ await delay(50);
2355
+
2356
+ const keystoneService = new KeystoneService_V1();
2357
+ const parentWithUserDelegate = await keystoneService.registerDelegate({
2358
+ parentKeystone: evolvedParent,
2359
+ delegateKeystone: userSyncDelegate,
2360
+ masterSecret: newPassword,
2361
+ signingPoolId: POOL_ID_MANAGE,
2362
+ metaspace,
2363
+ space,
2364
+ allowedVerbs: [KEYSTONE_VERB_SYNC, KEYSTONE_VERB_CONNECT],
2365
+ deviceName: deviceTag
2366
+ });
1549
2367
 
1550
- // Clear inputs
1551
- this.elements!.inputNewPasswordEl.value = '';
1552
- this.elements!.inputConfirmPasswordEl.value = '';
2368
+ // 3. Post evolved keystone with new delegate to server
2369
+ this.showBusy({
2370
+ isBusy: true,
2371
+ title: 'Syncing to Server...',
2372
+ msg: 'Publishing updated domain identity and sovereign delegate to server...',
2373
+ animationEmoji: '⚡'
2374
+ });
2375
+
2376
+ const resSync = await getApiBridge().putEvolveKeystone({
2377
+ domainAddr: primaryTjpAddr,
2378
+ keystoneIbGib: parentWithUserDelegate,
2379
+ relatedKeystoneIbGibs: [userSyncDelegate]
2380
+ });
2381
+ if (!resSync.success) {
2382
+ throw new Error(`Server sync of password delegate failed: ${resSync.message}`);
2383
+ }
2384
+
2385
+ // 4. Save parentWithUserDelegate and userSyncDelegate to local space & metaspace
2386
+ await metaspace.put({ ibGibs: [parentWithUserDelegate, userSyncDelegate], space });
2387
+ await metaspace.registerNewIbGib({ ibGib: parentWithUserDelegate, space });
2388
+ await metaspace.registerNewIbGib({ ibGib: userSyncDelegate, space });
2389
+
2390
+ await updateSpecialIndex({
2391
+ type: "keystones",
2392
+ rel8nInfos: [
2393
+ {
2394
+ rel8nName: "keystone",
2395
+ ibGibs: [toDto({ ibGib: parentWithUserDelegate }), toDto({ ibGib: userSyncDelegate })]
2396
+ }
2397
+ ],
2398
+ metaspace,
2399
+ space
2400
+ });
2401
+
2402
+ const evolvedAddr = getIbGibAddr({ ibGib: parentWithUserDelegate });
2403
+
2404
+ // Dispatch EVENT_IBGIB_IDENTITY_CHANGED
2405
+ window.dispatchEvent(new CustomEvent(EVENT_IBGIB_IDENTITY_CHANGED, {
2406
+ detail: { activeIdentityAddr: evolvedAddr },
2407
+ bubbles: true,
2408
+ composed: true
2409
+ }));
2410
+
2411
+ this.setChangePasswordStatus("Successfully added Master Password! Identity is now sovereign.", "success");
2412
+
2413
+ // Reset OTP section and button text
2414
+ if (this.elements?.sectionAddPasswordOtpEl) {
2415
+ this.elements.sectionAddPasswordOtpEl.classList.add('hidden');
2416
+ if (this.elements.inputAddPasswordOtpEl) {
2417
+ this.elements.inputAddPasswordOtpEl.value = '';
2418
+ }
2419
+ }
2420
+ if (this.elements?.btnChangePasswordEl) {
2421
+ this.elements.btnChangePasswordEl.textContent = "Update Password";
2422
+ }
2423
+
2424
+ // Refresh the view
2425
+ this.ibGibAddr = evolvedAddr;
2426
+ this.staticIbGib = parentWithUserDelegate;
2427
+ await this.loadIbGib({ getLatest: true });
2428
+
2429
+ // Clear inputs
2430
+ this.elements!.inputNewPasswordEl.value = '';
2431
+ this.elements!.inputConfirmPasswordEl.value = '';
2432
+
2433
+ return parentWithUserDelegate;
2434
+ }
2435
+
2436
+ /**
2437
+ * Handles adding a master password to a sovereign identity that was created with passkeys only.
2438
+ * Authorizes via passkey biometric/hardware assertion and cryptographically adds the manage pool.
2439
+ */
2440
+ protected async handleAddPasswordToPasskeyIdentity(): Promise<void> {
2441
+ const lc = `${this.lc}[${this.handleAddPasswordToPasskeyIdentity.name}]`;
2442
+ this.setChangePasswordStatus("Validating entries...", "info");
2443
+
2444
+ try {
2445
+ const newPassword = this.elements!.inputNewPasswordEl.value;
2446
+ const confirmPassword = this.elements!.inputConfirmPasswordEl.value;
2447
+
2448
+ if (!newPassword) {
2449
+ this.setChangePasswordStatus("Master password is required.", "error");
2450
+ return;
2451
+ }
2452
+ if (newPassword !== confirmPassword) {
2453
+ this.setChangePasswordStatus("Master password and confirmation do not match.", "error");
2454
+ return;
2455
+ }
2456
+
2457
+ const passkeysMap = this.aggregatedDetails?.passkeys
2458
+ || (this.ibGib?.data as any)?.frameDetails?.passkeys
2459
+ || (this.ibGib?.data as any)?.passkeys
2460
+ || {};
2461
+ const passkeysList: KeystonePasskeyEntry[] = Array.isArray(passkeysMap)
2462
+ ? passkeysMap
2463
+ : Object.values(passkeysMap);
2464
+
2465
+ const passkeyPool = this.ibGib?.data?.challengePools?.find(p => p.id.startsWith('passkey-manage-'));
2466
+ if (!passkeyPool) {
2467
+ this.setChangePasswordStatus("No active passkey manage pool found on this identity.", "error");
2468
+ return;
2469
+ }
2470
+
2471
+ let passkeySecret: string | undefined;
2472
+ let signingPoolId: string | undefined;
2473
+
2474
+ try {
2475
+ const authResult = await promptForAuthoritySecret({
2476
+ keystone: this.ibGib,
2477
+ aggregatedDetails: this.aggregatedDetails,
2478
+ title: 'Authorize Master Password',
2479
+ msg: 'Please touch your fingerprint sensor, Face ID, or security key to authorize adding a Master Password...',
2480
+ verb: KEYSTONE_VERB_MANAGE
2481
+ });
2482
+ passkeySecret = authResult.secret;
2483
+ signingPoolId = authResult.poolId;
2484
+ } catch (err) {
2485
+ this.setChangePasswordStatus("Authorization cancelled.", "error");
2486
+ return;
2487
+ }
2488
+
2489
+ this.showBusy({
2490
+ isBusy: true,
2491
+ title: 'Adding Master Password...',
2492
+ msg: 'Compiling cryptographic master password pool and evolving identity...',
2493
+ animationEmoji: '🔑'
2494
+ });
2495
+ await delay(50);
2496
+ this.setChangePasswordStatus("Generating sovereign master password challenge pool...", "info");
2497
+
2498
+ const metaspace = await getGlobalMetaspace_waitIfNeeded();
2499
+ if (!metaspace) throw new Error("Global metaspace not available");
2500
+ const space = await metaspace.getLocalUserSpace({ lock: false });
2501
+ if (!space) throw new Error("Local user space not available");
2502
+
2503
+ const poolConfig: KeystonePoolConfig_HashV1 = {
2504
+ id: POOL_ID_MANAGE,
2505
+ type: KeystoneChallengeType.hash_reveal_v1,
2506
+ salt: POOL_ID_MANAGE,
2507
+ algo: HashAlgorithm.sha_256,
2508
+ rounds: 1,
2509
+ allowedVerbs: [KEYSTONE_VERB_MANAGE, KEYSTONE_VERB_SYNC, KEYSTONE_VERB_CONNECT],
2510
+ behavior: {
2511
+ size: 10,
2512
+ replenish: KeystoneReplenishStrategy.topUp,
2513
+ selectSequentially: 1,
2514
+ selectRandomly: 0,
2515
+ targetBindingCount: 0,
2516
+ }
2517
+ };
2518
+
2519
+ const newChallenges = await generatePoolChallenges({
2520
+ config: poolConfig,
2521
+ masterSecret: newPassword,
2522
+ });
2523
+
2524
+ const newManagePool = {
2525
+ id: POOL_ID_MANAGE,
2526
+ config: poolConfig,
2527
+ challenges: newChallenges,
2528
+ };
2529
+
2530
+ const primaryTjpAddr = (this.ibGib ? getTjpAddr({ ibGib: this.ibGib }) : undefined)
2531
+ || (this.ibGibAddr ? (() => {
2532
+ const { ib } = getIbAndGib({ ibGibAddr: this.ibGibAddr });
2533
+ const { tjpGib } = getGibInfo({ ibGibAddr: this.ibGibAddr });
2534
+ return tjpGib && ib ? `${ib}^${tjpGib}` : this.ibGibAddr;
2535
+ })() : undefined)
2536
+ || getIbGibAddr({ ibGib: this.ibGib! });
2537
+
2538
+ const keystoneService = new KeystoneService_V1();
2539
+ const evolvedParent = await keystoneService.sign({
2540
+ latestKeystone: this.ibGib!,
2541
+ claim: {
2542
+ target: primaryTjpAddr,
2543
+ verb: KEYSTONE_VERB_MANAGE
2544
+ },
2545
+ masterSecret: passkeySecret,
2546
+ poolId: signingPoolId,
2547
+ dataToPatch: {
2548
+ challengePools: [...(this.ibGib!.data!.challengePools || []), newManagePool]
2549
+ },
2550
+ metaspace,
2551
+ space
2552
+ });
2553
+
2554
+ // Sync pool addition to server
2555
+ this.showBusy({
2556
+ isBusy: true,
2557
+ title: 'Syncing to Server...',
2558
+ msg: 'Publishing updated identity to server...',
2559
+ animationEmoji: '⚡'
2560
+ });
2561
+
2562
+ const resSync = await getApiBridge().putEvolveKeystone({
2563
+ domainAddr: primaryTjpAddr,
2564
+ keystoneIbGib: evolvedParent,
2565
+ relatedKeystoneIbGibs: []
2566
+ });
2567
+
2568
+ if (!resSync.success) {
2569
+ throw new Error(`Server sync failed: ${resSync.message}`);
2570
+ }
2571
+
2572
+ // Save locally
2573
+ await metaspace.put({ ibGibs: [evolvedParent], space });
2574
+ await metaspace.registerNewIbGib({ ibGib: evolvedParent, space });
2575
+
2576
+ // Delegate registration & final sync via DRY helper
2577
+ await this.registerAndSyncSovereignPasswordDelegate({
2578
+ evolvedParent,
2579
+ newPassword,
2580
+ primaryTjpAddr,
2581
+ metaspace,
2582
+ space
2583
+ });
1553
2584
 
1554
2585
  } catch (error) {
1555
- console.error(`${lc} Elevation failed: ${extractErrorMsg(error)}`);
2586
+ console.error(`${lc} Failed to add password to passkey identity: ${extractErrorMsg(error)}`, error);
1556
2587
  this.setChangePasswordStatus(`Failed to add master password: ${extractErrorMsg(error)}`, "error");
1557
2588
  } finally {
1558
2589
  this.showBusy({ isBusy: false });
@@ -1562,14 +2593,28 @@ export class KeystoneDetailsComponentInstance
1562
2593
  protected async handleChangePassword(): Promise<void> {
1563
2594
  const lc = `${this.lc}[${this.handleChangePassword.name}]`;
1564
2595
 
1565
- const caps = getKeystoneCapabilities({ keystone: this.ibGib, aggregatedDetails: this.aggregatedDetails });
1566
- const isCustodialOnly = caps.isCustodial && !caps.isSovereign;
1567
- if (isCustodialOnly) {
1568
- await this.handleAddMasterPassword();
2596
+ const caps = !!this.ibGib ?
2597
+ getKeystoneCapabilities({ keystone: this.ibGib, aggregatedDetails: this.aggregatedDetails }) :
2598
+ undefined;
2599
+
2600
+ const hasPassword = !!caps?.hasPassword;
2601
+ if (!hasPassword) {
2602
+ const isCustodialOnly = !!caps ? caps.isCustodial && !caps.isSovereign : false;
2603
+ if (isCustodialOnly) {
2604
+ await this.handleAddMasterPassword();
2605
+ } else {
2606
+ await this.handleAddPasswordToPasskeyIdentity();
2607
+ }
1569
2608
  return;
1570
2609
  }
1571
2610
 
1572
2611
  this.setChangePasswordStatus("Validating entries...", "info");
2612
+ this.showBusy({
2613
+ isBusy: true,
2614
+ title: 'Changing Master Password...',
2615
+ msg: 'Validating current password...',
2616
+ animationEmoji: '🔐'
2617
+ });
1573
2618
 
1574
2619
  try {
1575
2620
  const currentPassword = this.elements!.inputCurrentPasswordEl.value;
@@ -1602,6 +2647,12 @@ export class KeystoneDetailsComponentInstance
1602
2647
  return;
1603
2648
  }
1604
2649
 
2650
+ this.showBusy({
2651
+ isBusy: true,
2652
+ title: 'Evolving Master Password...',
2653
+ msg: 'Generating new challenge proofs and rotating local user delegates...',
2654
+ animationEmoji: '⚙️'
2655
+ });
1605
2656
  this.setChangePasswordStatus("Evolving primary identity keystone...", "info");
1606
2657
 
1607
2658
  const metaspace = await getGlobalMetaspace_waitIfNeeded();
@@ -1609,8 +2660,7 @@ export class KeystoneDetailsComponentInstance
1609
2660
  const space = await metaspace.getLocalUserSpace({ lock: false });
1610
2661
  if (!space) { throw new Error(`(UNEXPECTED) couldn't get default local space? (E: 2fc1281c28351fd378a1d7f88eaa0526)`); }
1611
2662
 
1612
- // 2. Rotate parent identity
1613
- debugger; // walk through this...looking for delegate failure // hmm...I thought we got change password going?
2663
+ // 2. Rotate parent identity and sovereign sync delegates
1614
2664
  const { newKeystone: evolvedParent, newDelegates: evolvedDelegates } =
1615
2665
  await keystoneService.changePassword({
1616
2666
  keystoneIbGib: this.ibGib!,
@@ -1620,62 +2670,22 @@ export class KeystoneDetailsComponentInstance
1620
2670
  metaspace,
1621
2671
  space
1622
2672
  });
1623
- debugger; // look at evolvedDelegates
1624
-
1625
- this.setChangePasswordStatus("Resolving and rotating delegate keystones...", "info");
1626
-
1627
- // // 3. Evolve registered delegate keystones (preserving custodian/foreign delegate secrets)
1628
- // const delegates = this.ibGib!.data?.delegates || {};
1629
- // const evolvedDelegates: KeystoneIbGib_V1[] = [];
1630
-
1631
- // for (const delegateInfo of Object.values(delegates)) {
1632
- // const delegateAddr = delegateInfo.delegateAddr;
1633
- // const delegateKeystone = await keystoneService.getLatestKeystone({
1634
- // addr: delegateAddr,
1635
- // metaspace,
1636
- // space,
1637
- // });
1638
-
1639
- // // Identify user-owned challenge pools in the delegate that match the old password
1640
- // const poolsToRotate: string[] = [];
1641
- // for (const pool of delegateKeystone.data?.challengePools ?? []) {
1642
- // if (!pool.isForeign) {
1643
- // /**
1644
- // * WARNING: There is an extreme edge case that if two pools
1645
- // * happen to be owned by different parties but share the
1646
- // * same secret, this will change the wrong pool's secret
1647
- // * also. For V1, this shouldn't matter. But we need a
1648
- // * better way of tracking pool ownership for V2 (or even
1649
- // * V1 if we have multi-party support). (DO NOT REMOVE
1650
- // * THIS COMMENT UNTIL THIS IS ADDRESSED)
1651
- // */
1652
- // const isPoolMatch = await keystoneService.verifySigningSecret({
1653
- // keystoneIbGib: delegateKeystone,
1654
- // signingSecret: currentPassword,
1655
- // poolId: pool.id
1656
- // });
1657
- // if (isPoolMatch) { poolsToRotate.push(pool.id); }
1658
- // }
1659
- // }
1660
-
1661
- // if (poolsToRotate.length > 0) {
1662
- // console.log(`${lc} Rotating pools in delegate ${delegateInfo.delegateTjpAddr}: ${poolsToRotate.join(', ')}`);
1663
- // const {newKeystone: evolvedDelegate} = await keystoneService.changePassword({
1664
- // keystoneIbGib: delegateKeystone,
1665
- // newMasterSecret: newPassword,
1666
- // signingSecret: currentPassword,
1667
- // signingPoolId: 'manage',
1668
- // poolIds: poolsToRotate,
1669
- // metaspace,
1670
- // space
1671
- // });
1672
- // evolvedDelegates.push(evolvedDelegate);
1673
- // }
1674
- // }
1675
-
1676
- // 4. Sync evolved parent and all evolved delegate keystones atomically
2673
+
2674
+ // 3. Sync evolved parent and all evolved delegate keystones atomically
2675
+ this.showBusy({
2676
+ isBusy: true,
2677
+ title: 'Syncing to Server...',
2678
+ msg: 'Publishing rotated identity and delegates to server...',
2679
+ animationEmoji: '⚡'
2680
+ });
1677
2681
  this.setChangePasswordStatus("Syncing evolved keystones to server...", "info");
1678
- const parentTjpAddr = getIbGibAddr({ ibGib: this.ibGib! }); // is this.ibGib the tjp always? or is it the latest in the keystone line? Does it matter?
2682
+ const parentTjpAddr = (this.ibGib ? getTjpAddr({ ibGib: this.ibGib }) : undefined)
2683
+ || (this.ibGibAddr ? (() => {
2684
+ const { ib } = getIbAndGib({ ibGibAddr: this.ibGibAddr });
2685
+ const { tjpGib } = getGibInfo({ ibGibAddr: this.ibGibAddr });
2686
+ return tjpGib && ib ? `${ib}^${tjpGib}` : this.ibGibAddr;
2687
+ })() : undefined)
2688
+ || getIbGibAddr({ ibGib: this.ibGib! });
1679
2689
 
1680
2690
  const syncRes = await getApiBridge().putEvolveKeystone({
1681
2691
  domainAddr: parentTjpAddr,
@@ -1686,7 +2696,7 @@ export class KeystoneDetailsComponentInstance
1686
2696
  throw new Error(`Server synchronization rejected: ${syncRes.message}`);
1687
2697
  }
1688
2698
 
1689
- // 5. Update index locally
2699
+ // 4. Update index locally
1690
2700
  this.setChangePasswordStatus("Updating local indexes...", "info");
1691
2701
  const newKeystoneIbGibs = [toDto({ ibGib: evolvedParent })];
1692
2702
  if (evolvedDelegates) {
@@ -1713,11 +2723,16 @@ export class KeystoneDetailsComponentInstance
1713
2723
  this.elements!.inputConfirmPasswordEl.value = "";
1714
2724
 
1715
2725
  this.setChangePasswordStatus("Password updated successfully!", "success");
2726
+ this.ibGibAddr = getIbGibAddr({ ibGib: evolvedParent });
2727
+ this.staticIbGib = evolvedParent;
1716
2728
  await this.loadIbGib({ getLatest: true });
1717
2729
 
1718
2730
  } catch (error) {
2731
+ debugger; // error in handleChangePassword
1719
2732
  console.error(`${lc} Password rotation failed: ${extractErrorMsg(error)}`);
1720
2733
  this.setChangePasswordStatus(`Update failed: ${extractErrorMsg(error)}`, "error");
2734
+ } finally {
2735
+ this.showBusy({ isBusy: false });
1721
2736
  }
1722
2737
  }
1723
2738
  }