@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
@@ -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 } 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,14 +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
-
27
26
  import { getComponentCtorArg } from "../../../../app-bootstrap/init-orchestration.mjs";
28
27
  import { promptForSecret } from "../../../../helpers.web.mjs";
29
- import { isSsoProviderLinked } from "../../../../identity/sso/sso-helpers.mjs";
28
+ import { isSsoProviderLinked, getKeystoneCapabilities, isDelegateExpired, deriveDelegateSecret, createSyncDelegateKeystone, isCustodialDelegate } from "@ibgib/core-gib/dist/keystone/delegate/delegate-helpers.mjs";
29
+ import { generatePoolChallenges } from "@ibgib/core-gib/dist/keystone/keystone-helpers.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";
30
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";
31
39
 
32
40
  const logalot = false;
33
41
 
@@ -88,6 +96,10 @@ interface KeystoneDetailsElements extends ElementsBase {
88
96
  rawKeystoneJsonEl: HTMLPreElement;
89
97
  delegatesCardEl: HTMLDivElement;
90
98
  delegatesListEl: HTMLTableSectionElement;
99
+ passkeysCardEl: HTMLDivElement;
100
+ passkeysListEl: HTMLTableSectionElement;
101
+ btnAddPasskeyEl: HTMLButtonElement;
102
+ passkeyStatusMsgEl: HTMLDivElement;
91
103
  historicalWarningBannerEl: HTMLDivElement;
92
104
  containerEl: HTMLDivElement;
93
105
  ssoLinkCardEl: HTMLDivElement;
@@ -100,6 +112,10 @@ interface KeystoneDetailsElements extends ElementsBase {
100
112
  btnChangePasswordEl: HTMLButtonElement;
101
113
  changePasswordStatusEl: HTMLDivElement;
102
114
  changePasswordCardEl: HTMLDivElement;
115
+ sectionAddPasswordOtpEl: HTMLDivElement;
116
+ inputAddPasswordOtpEl: HTMLInputElement;
117
+ btnResendAddPasswordOtpEl: HTMLButtonElement;
118
+ descAddPasswordOtpEl: HTMLParagraphElement;
103
119
  }
104
120
 
105
121
  export class KeystoneDetailsComponentInstance
@@ -118,6 +134,8 @@ export class KeystoneDetailsComponentInstance
118
134
  private isHistoricalFrame: boolean = false;
119
135
  private _onIdentityChanged: ((e: any) => void) | null = null;
120
136
  private keystoneCache: Map<string, { username: string, description: string, isPrimary: boolean, n: number }> = new Map();
137
+ private aggregatedDetails?: any;
138
+ private resendCooldownInterval: any = null;
121
139
 
122
140
  constructor() {
123
141
  super();
@@ -140,6 +158,39 @@ export class KeystoneDetailsComponentInstance
140
158
  }
141
159
  }
142
160
 
161
+ override async disconnected(): Promise<void> {
162
+ if (this._onIdentityChanged) {
163
+ window.removeEventListener(EVENT_IBGIB_IDENTITY_CHANGED, this._onIdentityChanged);
164
+ }
165
+ if (this.resendCooldownInterval) {
166
+ clearInterval(this.resendCooldownInterval);
167
+ this.resendCooldownInterval = null;
168
+ }
169
+ }
170
+
171
+ private startResendCooldownTimer(durationSecs: number = 30): void {
172
+ const btn = this.elements?.btnResendAddPasswordOtpEl;
173
+ if (!btn) return;
174
+ if (this.resendCooldownInterval) {
175
+ clearInterval(this.resendCooldownInterval);
176
+ this.resendCooldownInterval = null;
177
+ }
178
+ let remaining = durationSecs;
179
+ btn.disabled = true;
180
+ btn.textContent = `Resend Code (${remaining}s)`;
181
+ this.resendCooldownInterval = setInterval(() => {
182
+ remaining -= 1;
183
+ if (remaining <= 0) {
184
+ clearInterval(this.resendCooldownInterval);
185
+ this.resendCooldownInterval = null;
186
+ btn.disabled = false;
187
+ btn.textContent = "Resend Code";
188
+ } else {
189
+ btn.textContent = `Resend Code (${remaining}s)`;
190
+ }
191
+ }, 1000);
192
+ }
193
+
143
194
  private async getKeystoneDetails(addr: string): Promise<{ username: string, description: string, isPrimary: boolean, n: number }> {
144
195
  const cached = this.keystoneCache.get(addr);
145
196
  if (cached) return cached;
@@ -206,6 +257,10 @@ export class KeystoneDetailsComponentInstance
206
257
  this.elements.rawKeystoneJsonEl = shadow.getElementById('raw-keystone-json') as HTMLPreElement;
207
258
  this.elements.delegatesCardEl = shadow.getElementById('delegates-card') as HTMLDivElement;
208
259
  this.elements.delegatesListEl = shadow.getElementById('delegates-list') as HTMLTableSectionElement;
260
+ this.elements.passkeysCardEl = shadow.getElementById('passkeys-card') as HTMLDivElement;
261
+ this.elements.passkeysListEl = shadow.getElementById('passkeys-list') as HTMLTableSectionElement;
262
+ this.elements.btnAddPasskeyEl = shadow.getElementById('btn-add-passkey') as HTMLButtonElement;
263
+ this.elements.passkeyStatusMsgEl = shadow.getElementById('passkey-status-msg') as HTMLDivElement;
209
264
  this.elements.containerEl = shadow.getElementById('container') as HTMLDivElement;
210
265
  this.elements.historicalWarningBannerEl = shadow.getElementById('historical-warning-banner') as HTMLDivElement;
211
266
  this.elements.ssoLinkCardEl = shadow.getElementById('sso-link-card') as HTMLDivElement;
@@ -218,6 +273,10 @@ export class KeystoneDetailsComponentInstance
218
273
  this.elements.btnChangePasswordEl = shadow.getElementById('btn-change-password') as HTMLButtonElement;
219
274
  this.elements.changePasswordStatusEl = shadow.getElementById('change-password-status') as HTMLDivElement;
220
275
  this.elements.changePasswordCardEl = shadow.getElementById('change-password-card') as HTMLDivElement;
276
+ this.elements.sectionAddPasswordOtpEl = shadow.getElementById('section-add-password-otp') as HTMLDivElement;
277
+ this.elements.inputAddPasswordOtpEl = shadow.getElementById('input-add-password-otp') as HTMLInputElement;
278
+ this.elements.btnResendAddPasswordOtpEl = shadow.getElementById('btn-resend-add-password-otp') as HTMLButtonElement;
279
+ this.elements.descAddPasswordOtpEl = shadow.getElementById('desc-add-password-otp') as HTMLParagraphElement;
221
280
 
222
281
  // ElementBase requires contentEl
223
282
  this.elements.contentEl = shadow.getElementById('identity-details-view') as HTMLDivElement;
@@ -243,12 +302,6 @@ export class KeystoneDetailsComponentInstance
243
302
  }
244
303
  }
245
304
 
246
- override async disconnected(): Promise<void> {
247
- if (this._onIdentityChanged) {
248
- window.removeEventListener(EVENT_IBGIB_IDENTITY_CHANGED, this._onIdentityChanged);
249
- }
250
- }
251
-
252
305
  private initHandlers() {
253
306
  // Setup raw JSON accordion toggle
254
307
  this.elements!.rawAccordionHeaderEl.addEventListener('click', () => this.toggleAccordion());
@@ -258,9 +311,11 @@ export class KeystoneDetailsComponentInstance
258
311
  this.elements!.btnSetActiveEl.addEventListener('click', () => this.handleSetActiveIdentity());
259
312
  this.elements!.btnCheckUpdatesEl.addEventListener('click', () => this.handleCheckForUpdates());
260
313
  this.elements!.btnUpdateIdentityEl.addEventListener('click', () => this.handleUpdateIdentity());
314
+ this.elements!.btnAddPasskeyEl?.addEventListener('click', () => this.handleAddPasskey());
261
315
  this.elements!.btnLinkGoogleEl.addEventListener('click', () => this.handleSSOClick('google'));
262
316
  this.elements!.btnLinkGithubEl.addEventListener('click', () => this.handleSSOClick('github'));
263
317
  this.elements!.btnChangePasswordEl.addEventListener('click', () => this.handleChangePassword());
318
+ this.elements!.btnResendAddPasswordOtpEl?.addEventListener('click', () => this.handleResendAddPasswordOtp());
264
319
  }
265
320
 
266
321
  private toggleAccordion() {
@@ -454,34 +509,99 @@ export class KeystoneDetailsComponentInstance
454
509
  this.elements.btnLinkGoogleEl.classList.remove('disabled');
455
510
  this.elements.btnLinkGithubEl.classList.remove('disabled');
456
511
 
457
- // Check if already linked
458
- const isGoogleLinked = isSsoProviderLinked({ keystone: this.ibGib, providerId: 'google' });
459
- const isGithubLinked = isSsoProviderLinked({ keystone: this.ibGib, providerId: 'github' });
512
+ // Check if origin provider or linked
513
+ const originProvider = this.aggregatedDetails?.custodianProvider || this.ibGib?.data?.frameDetails?.custodianProvider;
514
+ const isGoogleLinked = !!this.ibGib ?
515
+ isSsoProviderLinked({ keystone: this.ibGib, aggregatedDetails: this.aggregatedDetails, providerId: 'google' }) :
516
+ false;
517
+ const isGithubLinked = !!this.ibGib ?
518
+ isSsoProviderLinked({ keystone: this.ibGib, aggregatedDetails: this.aggregatedDetails, providerId: 'github' }) :
519
+ false;
520
+
521
+ const caps = !!this.ibGib ?
522
+ getKeystoneCapabilities({ keystone: this.ibGib, aggregatedDetails: this.aggregatedDetails }) :
523
+ undefined;
460
524
 
461
525
  // Google Card States
462
526
  const googleCard = this.elements.btnLinkGoogleEl;
463
527
  const googleLabel = googleCard.querySelector('.toggle-state-text')!;
464
528
  if (isGoogleLinked) {
529
+ if (!caps) { throw new Error(`(UNEXPECTED) isGoogleLinked but capabilities falsy? (E: cc62e8357b66860dd2cb8148c833b726)`); }
465
530
  googleCard.classList.remove('unlinked');
466
531
  googleCard.classList.add('linked');
467
- googleLabel.textContent = "Google Linked ✅";
532
+ const isOrigin = originProvider === 'google';
533
+ if (isOrigin && !caps.isSovereign) {
534
+ googleLabel.textContent = "Google (Primary Account) 🔒";
535
+ googleCard.title = "Cannot unlink primary account without first adding a Master Password below.";
536
+ } else if (isOrigin) {
537
+ googleLabel.textContent = "Google (Primary Account) ✅";
538
+ googleCard.title = "Click to unlink Google provider (requires Master Password).";
539
+ } else {
540
+ googleLabel.textContent = "Google Linked ✅";
541
+ googleCard.title = "Click to unlink Google provider.";
542
+ }
468
543
  } else {
469
544
  googleCard.classList.remove('linked');
470
545
  googleCard.classList.add('unlinked');
471
546
  googleLabel.textContent = "Unlinked";
547
+ googleCard.title = "Click to link Google account.";
472
548
  }
473
549
 
474
550
  // GitHub Card States
475
551
  const githubCard = this.elements.btnLinkGithubEl;
476
552
  const githubLabel = githubCard.querySelector('.toggle-state-text')!;
477
553
  if (isGithubLinked) {
554
+ if (!caps) { throw new Error(`(UNEXPECTED) isGithubLinked but capabilities falsy? (E: e94688210538e0e82878088873a46826)`); }
478
555
  githubCard.classList.remove('unlinked');
479
556
  githubCard.classList.add('linked');
480
- githubLabel.textContent = "GitHub Linked ✅";
557
+ const isOrigin = originProvider === 'github';
558
+ if (isOrigin && !caps.isSovereign) {
559
+ githubLabel.textContent = "GitHub (Primary Account) 🔒";
560
+ githubCard.title = "Cannot unlink primary account without first adding a Master Password below.";
561
+ } else if (isOrigin) {
562
+ githubLabel.textContent = "GitHub (Primary Account) ✅";
563
+ githubCard.title = "Click to unlink GitHub provider (requires Master Password).";
564
+ } else {
565
+ githubLabel.textContent = "GitHub Linked ✅";
566
+ githubCard.title = "Click to unlink GitHub provider.";
567
+ }
481
568
  } else {
482
569
  githubCard.classList.remove('linked');
483
570
  githubCard.classList.add('unlinked');
484
571
  githubLabel.textContent = "Unlinked";
572
+ githubCard.title = "Click to link GitHub account.";
573
+ }
574
+ }
575
+ }
576
+
577
+ private updatePasswordCardView(): void {
578
+ const caps = !!this.ibGib ?
579
+ getKeystoneCapabilities({ keystone: this.ibGib, aggregatedDetails: this.aggregatedDetails }) :
580
+ false;
581
+ const cardTitleEl = this.elements?.changePasswordCardEl?.querySelector('h3');
582
+ const cardDescEl = this.elements?.changePasswordCardEl?.querySelector('.description');
583
+ const currentPasswordGroup = this.elements?.inputCurrentPasswordEl?.closest('.form-group') as HTMLElement | null;
584
+ const btnChangePassword = this.elements?.btnChangePasswordEl;
585
+
586
+ const isCustodialOnly = !!caps ?
587
+ caps.isCustodial && !caps.isSovereign :
588
+ false;
589
+ if (isCustodialOnly) {
590
+ if (cardTitleEl) cardTitleEl.textContent = "Add Master Password";
591
+ if (cardDescEl) cardDescEl.textContent = "Establish sovereign ownership by adding a master password. This enables direct, independent cryptographic management without relying solely on custodian authentication.";
592
+ if (currentPasswordGroup) currentPasswordGroup.style.display = 'none';
593
+ if (btnChangePassword) btnChangePassword.textContent = "Add Master Password";
594
+ } else {
595
+ if (cardTitleEl) cardTitleEl.textContent = "Change Password";
596
+ if (cardDescEl) 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.";
597
+ if (currentPasswordGroup) currentPasswordGroup.style.display = '';
598
+ if (btnChangePassword) btnChangePassword.textContent = "Update Password";
599
+ }
600
+
601
+ if (this.elements?.sectionAddPasswordOtpEl) {
602
+ this.elements.sectionAddPasswordOtpEl.classList.add('hidden');
603
+ if (this.elements.inputAddPasswordOtpEl) {
604
+ this.elements.inputAddPasswordOtpEl.value = '';
485
605
  }
486
606
  }
487
607
  }
@@ -549,6 +669,7 @@ export class KeystoneDetailsComponentInstance
549
669
  space
550
670
  });
551
671
 
672
+ this.aggregatedDetails = aggregated;
552
673
  this.elements.identityAggrDetailsEl.textContent = aggregated ? pretty(aggregated) : '{}';
553
674
 
554
675
  // Update Username and Description fields
@@ -556,16 +677,19 @@ export class KeystoneDetailsComponentInstance
556
677
  this.elements.identityDescriptionEl.textContent = aggregated?.description || aggregated?.profile?.description || '-';
557
678
  } catch (err) {
558
679
  console.warn(`${lc} Error aggregating details: ${extractErrorMsg(err)}`);
680
+ this.aggregatedDetails = undefined;
559
681
  this.elements.identityAggrDetailsEl.textContent = '{}';
560
682
  this.elements.identityNameEl.textContent = '-';
561
683
  this.elements.identityDescriptionEl.textContent = '-';
562
684
  }
563
685
  } else {
686
+ this.aggregatedDetails = undefined;
564
687
  this.elements.identityAggrDetailsEl.textContent = '{}';
565
688
  this.elements.identityNameEl.textContent = '-';
566
689
  this.elements.identityDescriptionEl.textContent = '-';
567
690
  }
568
691
  } else {
692
+ this.aggregatedDetails = undefined;
569
693
  this.elements.identityAggrDetailsEl.textContent = '{}';
570
694
  this.elements.identityNameEl.textContent = '-';
571
695
  this.elements.identityDescriptionEl.textContent = '-';
@@ -591,15 +715,19 @@ export class KeystoneDetailsComponentInstance
591
715
  // Render Challenge Pools
592
716
  this.renderChallengePools(keystone.data?.challengePools || []);
593
717
 
594
- // 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)
595
719
  const details = await this.getKeystoneDetails(this.ibGibAddr);
596
720
  if (details.isPrimary) {
597
721
  this.elements.delegatesCardEl.classList.remove('hidden');
598
722
  await this.renderDelegates(keystone.data?.delegates);
723
+ this.elements.passkeysCardEl.classList.remove('hidden');
724
+ await this.renderPasskeys(this.aggregatedDetails?.passkeys);
599
725
  this.elements.ssoLinkCardEl.classList.remove('hidden');
600
726
  this.elements.changePasswordCardEl.classList.remove('hidden');
727
+ this.updatePasswordCardView();
601
728
  } else {
602
729
  this.elements.delegatesCardEl.classList.add('hidden');
730
+ this.elements.passkeysCardEl.classList.add('hidden');
603
731
  this.elements.ssoLinkCardEl.classList.add('hidden');
604
732
  this.elements.changePasswordCardEl.classList.add('hidden');
605
733
  }
@@ -615,6 +743,7 @@ export class KeystoneDetailsComponentInstance
615
743
 
616
744
  private clearActiveDetails() {
617
745
  if (!this.elements) { return; }
746
+ this.aggregatedDetails = undefined;
618
747
  this.isHistoricalFrame = false;
619
748
  if (this.elements.historicalWarningBannerEl) {
620
749
  this.elements.historicalWarningBannerEl.classList.add('hidden');
@@ -633,6 +762,15 @@ export class KeystoneDetailsComponentInstance
633
762
  if (this.elements.delegatesListEl) {
634
763
  this.elements.delegatesListEl.innerHTML = '';
635
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
+ }
636
774
  }
637
775
 
638
776
  private renderChallengePools(pools: any[]) {
@@ -804,43 +942,71 @@ export class KeystoneDetailsComponentInstance
804
942
  }
805
943
 
806
944
  const metaspace = await getGlobalMetaspace_waitIfNeeded();
807
- let localSpaceId: string | undefined;
808
- if (metaspace) {
809
- const space = await metaspace.getLocalUserSpace({ lock: false });
810
- if (space?.ib) {
811
- localSpaceId = parseSpaceIb({ spaceIb: space.ib }).spaceId;
812
- }
945
+ if (!metaspace) {
946
+ throw new Error(`(UNEXPECTED) metaspace falsy? (E: 8a9b0c1d2e3f4a5b6c7d8e9f0a1b2c3e)`);
947
+ }
948
+ const space = await metaspace.getLocalUserSpace({ lock: false });
949
+ if (!space) {
950
+ throw new Error(`(UNEXPECTED) space falsy? (E: 9b0c1d2e3f4a5b6c7d8e9f0a1b2c3d4f)`);
813
951
  }
952
+ const localSpaceId = space.ib ? parseSpaceIb({ spaceIb: space.ib }).spaceId : undefined;
814
953
 
815
954
  for (const [delegateTjpAddr, info] of Object.entries(delegates)) {
816
955
  const tr = document.createElement('tr');
817
956
 
818
- const deviceName = info.deviceName || info.details?.deviceTag || info.details?.name || 'Device Delegate';
957
+ const deviceName = info.deviceName || (info as any).details?.deviceTag || (info as any).details?.name || 'Device Delegate';
819
958
  const tdName = document.createElement('td');
820
959
 
821
- const delegateSpaceId = info.spaceId || info.details?.spaceId;
822
- let badgeEl: HTMLSpanElement | null = null;
823
-
824
- if (delegateSpaceId) {
825
- badgeEl = document.createElement('span');
826
- badgeEl.style.marginLeft = '0.5rem';
827
- badgeEl.style.padding = '0.15rem 0.4rem';
828
- badgeEl.style.borderRadius = '4px';
829
- badgeEl.style.fontSize = '0.75rem';
830
- badgeEl.style.fontWeight = 'bold';
831
- if (localSpaceId && delegateSpaceId === localSpaceId) {
832
- badgeEl.textContent = 'This Device';
833
- badgeEl.className = 'badge badge-this-device';
834
- badgeEl.style.backgroundColor = '#1b5e20';
835
- badgeEl.style.color = '#a5d6a7';
836
- } else {
837
- badgeEl.textContent = 'Remote Device';
838
- badgeEl.className = 'badge badge-remote-device';
839
- badgeEl.style.backgroundColor = '#0d47a1';
840
- badgeEl.style.color = '#90caf9';
960
+ const delegateSpaceId = info.spaceId || (info as any).details?.spaceId;
961
+
962
+ // Check if delegate is present in local space (via spaceId match or direct presence check)
963
+ let isThisDevice = false;
964
+ if (localSpaceId && delegateSpaceId === localSpaceId) {
965
+ isThisDevice = true;
966
+ } else {
967
+ try {
968
+ const resGet = await metaspace.get({ addrs: [delegateTjpAddr], space });
969
+ isThisDevice = !!(resGet.success && resGet.ibGibs && resGet.ibGibs.length > 0);
970
+ } catch {
971
+ isThisDevice = false;
841
972
  }
842
973
  }
843
974
 
975
+ const badgeEl = document.createElement('span');
976
+ badgeEl.style.marginLeft = '0.5rem';
977
+ badgeEl.style.padding = '0.15rem 0.4rem';
978
+ badgeEl.style.borderRadius = '4px';
979
+ badgeEl.style.fontSize = '0.75rem';
980
+ badgeEl.style.fontWeight = 'bold';
981
+
982
+ if (isThisDevice) {
983
+ badgeEl.textContent = 'This Device';
984
+ badgeEl.className = 'badge badge-this-device';
985
+ badgeEl.style.backgroundColor = '#1b5e20';
986
+ badgeEl.style.color = '#a5d6a7';
987
+ } else {
988
+ badgeEl.textContent = 'Remote Device';
989
+ badgeEl.className = 'badge badge-remote-device';
990
+ badgeEl.style.backgroundColor = '#0d47a1';
991
+ badgeEl.style.color = '#90caf9';
992
+ }
993
+
994
+ // Check expiration
995
+ const isExpired = isDelegateExpired({ delegateInfo: info });
996
+ let expiredBadgeEl: HTMLSpanElement | null = null;
997
+ if (isExpired) {
998
+ expiredBadgeEl = document.createElement('span');
999
+ expiredBadgeEl.textContent = 'Expired';
1000
+ expiredBadgeEl.className = 'badge badge-expired';
1001
+ expiredBadgeEl.style.marginLeft = '0.3rem';
1002
+ expiredBadgeEl.style.padding = '0.15rem 0.4rem';
1003
+ expiredBadgeEl.style.borderRadius = '4px';
1004
+ expiredBadgeEl.style.fontSize = '0.75rem';
1005
+ expiredBadgeEl.style.fontWeight = 'bold';
1006
+ expiredBadgeEl.style.backgroundColor = '#b71c1c';
1007
+ expiredBadgeEl.style.color = '#ef9a9a';
1008
+ }
1009
+
844
1010
  const applyNameAndBadge = (displayName: string, hoverText?: string) => {
845
1011
  tdName.innerHTML = '';
846
1012
  const textSpan = document.createElement('span');
@@ -849,6 +1015,9 @@ export class KeystoneDetailsComponentInstance
849
1015
  if (badgeEl) {
850
1016
  tdName.appendChild(badgeEl);
851
1017
  }
1018
+ if (expiredBadgeEl) {
1019
+ tdName.appendChild(expiredBadgeEl);
1020
+ }
852
1021
  tdName.title = hoverText || displayName;
853
1022
  };
854
1023
 
@@ -863,7 +1032,7 @@ export class KeystoneDetailsComponentInstance
863
1032
  tdVerbs.textContent = info.allowedVerbs ? info.allowedVerbs.join(', ') : '-';
864
1033
  tdVerbs.title = tdVerbs.textContent;
865
1034
 
866
- const isRemote = !!(delegateSpaceId && localSpaceId && delegateSpaceId !== localSpaceId);
1035
+ const isRemote = !isThisDevice;
867
1036
 
868
1037
  const tdActions = document.createElement('td');
869
1038
  const btnView = document.createElement('button');
@@ -874,20 +1043,20 @@ export class KeystoneDetailsComponentInstance
874
1043
  if (isRemote) {
875
1044
  this.showRemoteDelegateModal(info);
876
1045
  } else {
877
- if (info.delegateAddr) {
1046
+ if (info.delegateAddr || delegateTjpAddr) {
878
1047
  let addrToLoad = info.delegateAddr;
879
- if (metaspace) {
880
- const space = await metaspace.getLocalUserSpace({ lock: false });
881
- if (space) {
882
- try {
883
- const latest = await metaspace.getLatestAddr({ addr: delegateTjpAddr, space });
884
- if (latest) {
885
- addrToLoad = latest;
886
- }
887
- } catch (err) {
888
- console.warn(`${lc} Error resolving latest delegate address: ${extractErrorMsg(err)}`);
889
- }
1048
+ try {
1049
+ const keystoneService = new KeystoneService_V1();
1050
+ const latestKeystone = await keystoneService.getLatestKeystone({
1051
+ addr: delegateTjpAddr,
1052
+ metaspace,
1053
+ space
1054
+ });
1055
+ if (latestKeystone) {
1056
+ addrToLoad = getIbGibAddr({ ibGib: latestKeystone });
890
1057
  }
1058
+ } catch (err) {
1059
+ console.warn(`${lc} Error resolving latest delegate keystone: ${extractErrorMsg(err)}`);
891
1060
  }
892
1061
  this.dispatchEvent(new CustomEvent('ibgib-view-keystone-details', {
893
1062
  detail: { addr: addrToLoad },
@@ -924,8 +1093,9 @@ export class KeystoneDetailsComponentInstance
924
1093
  let masterSecret: string;
925
1094
  try {
926
1095
  masterSecret = await promptForSecret({
927
- msg: `Enter Master Password to revoke delegate '${deviceName}':`,
928
- confirm: false
1096
+ msg: `Enter Master Password or touch security key to revoke delegate '${deviceName}':`,
1097
+ confirm: false,
1098
+ passkeys: this.aggregatedDetails?.passkeys,
929
1099
  });
930
1100
  } catch (err) {
931
1101
  // User cancelled prompt
@@ -942,11 +1112,35 @@ export class KeystoneDetailsComponentInstance
942
1112
 
943
1113
  const keystoneSvc = new KeystoneService_V1();
944
1114
 
1115
+ // Resolve which manage pool matches the provided masterSecret
1116
+ let matchedPoolId: string | undefined;
1117
+ const candidatePools = this.ibGib!.data?.challengePools.filter(p =>
1118
+ p.id === 'manage' || p.id.startsWith('passkey-manage-') || (p.config.allowedVerbs && p.config.allowedVerbs.includes(KEYSTONE_VERB_MANAGE))
1119
+ ) || [];
1120
+
1121
+ for (const pool of candidatePools) {
1122
+ const ok = await keystoneSvc.verifySigningSecret({
1123
+ keystoneIbGib: this.ibGib!,
1124
+ signingSecret: masterSecret,
1125
+ poolId: pool.id
1126
+ });
1127
+ if (ok) {
1128
+ matchedPoolId = pool.id;
1129
+ break;
1130
+ }
1131
+ }
1132
+
1133
+ if (!matchedPoolId) {
1134
+ this.setSyncStatus("Failed to authorize revocation (invalid secret/passkey).", "error");
1135
+ return;
1136
+ }
1137
+
945
1138
  // Unregister delegate from parent keystone
946
1139
  const evolvedParent = await keystoneSvc.unregisterDelegate({
947
1140
  parentKeystone: this.ibGib!,
948
1141
  delegateTjpAddr,
949
1142
  masterSecret,
1143
+ signingPoolId: matchedPoolId,
950
1144
  metaspace,
951
1145
  space
952
1146
  });
@@ -1000,6 +1194,477 @@ export class KeystoneDetailsComponentInstance
1000
1194
  }
1001
1195
  }
1002
1196
 
1197
+ private setPasskeyStatus(msg: string, type: 'info' | 'success' | 'error') {
1198
+ const statusEl = this.elements?.passkeyStatusMsgEl;
1199
+ if (!statusEl) return;
1200
+ statusEl.textContent = msg;
1201
+ statusEl.className = `status-msg ${type}`;
1202
+ statusEl.classList.remove('hidden');
1203
+ }
1204
+
1205
+ private async renderPasskeys(passkeys: Record<string, KeystonePasskeyEntry> | undefined) {
1206
+ if (!this.elements) { return; }
1207
+ const listEl = this.elements.passkeysListEl;
1208
+ listEl.innerHTML = '';
1209
+
1210
+ const caps = !!this.ibGib ?
1211
+ getKeystoneCapabilities({ keystone: this.ibGib, aggregatedDetails: this.aggregatedDetails }) :
1212
+ undefined;
1213
+
1214
+ // Update button text depending on whether the account is custodial-only (elevation) or sovereign/hybrid
1215
+ const isCustodialOnly = !!caps ? caps.isCustodial && !caps.isSovereign : false;
1216
+ if (this.elements.btnAddPasskeyEl) {
1217
+ this.elements.btnAddPasskeyEl.textContent = isCustodialOnly
1218
+ ? "🛡️ Add Passkey (Passwordless Sovereign Elevation)"
1219
+ : "🛡️ Register New Passkey / Security Key";
1220
+ }
1221
+
1222
+ const validPasskeyEntries = Object.entries(passkeys || {}).filter(([_, entry]) => entry !== null && entry !== undefined);
1223
+
1224
+ if (validPasskeyEntries.length === 0) {
1225
+ listEl.innerHTML = `
1226
+ <tr>
1227
+ <td colspan="4" style="text-align: center; color: var(--clr-text-secondary, #b3b3b3); padding: 1rem;">
1228
+ No hardware passkeys or biometrics registered.
1229
+ </td>
1230
+ </tr>
1231
+ `;
1232
+ return;
1233
+ }
1234
+
1235
+ for (const [credId, entry] of validPasskeyEntries) {
1236
+ const row = document.createElement('tr');
1237
+ const createdText = entry.createdUTC ? new Date(entry.createdUTC).toLocaleString() : 'N/A';
1238
+ const nameText = entry.name || 'Hardware Key';
1239
+
1240
+ // Format transport badges
1241
+ let transportBadgesHtml = '';
1242
+ if (entry.transports && entry.transports.length > 0) {
1243
+ transportBadgesHtml = entry.transports.map(t => {
1244
+ let icon = '🔑';
1245
+ let label: string = t;
1246
+ if (t === 'internal') { icon = '💻'; label = 'Touch ID / Built-in'; }
1247
+ else if (t === 'usb') { icon = '🔑'; label = 'USB Key'; }
1248
+ else if (t === 'nfc') { icon = '📶'; label = 'NFC'; }
1249
+ else if (t === 'ble' || t === 'hybrid') { icon = '📲'; label = 'Mobile / Bluetooth'; }
1250
+ 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>`;
1251
+ }).join(' ');
1252
+ } else {
1253
+ 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>`;
1254
+ }
1255
+
1256
+ row.innerHTML = `
1257
+ <td>
1258
+ <div style="font-weight: 500;">${nameText}</div>
1259
+ <code style="font-size: 0.75rem; color: var(--clr-text-secondary, #b3b3b3); word-break: break-all;">${credId.substring(0, 16)}...</code>
1260
+ </td>
1261
+ <td>${transportBadgesHtml}</td>
1262
+ <td style="font-size: 0.85rem; color: var(--clr-text-secondary, #b3b3b3);">${createdText}</td>
1263
+ <td>
1264
+ <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);">
1265
+ Remove
1266
+ </button>
1267
+ </td>
1268
+ `;
1269
+
1270
+ const btnRemove = row.querySelector('.btn-remove-passkey') as HTMLButtonElement;
1271
+ btnRemove?.addEventListener('click', () => this.handleRemovePasskey(credId));
1272
+
1273
+ listEl.appendChild(row);
1274
+ }
1275
+ }
1276
+
1277
+ protected async handleAddPasskey(): Promise<void> {
1278
+ const lc = `${this.lc}[${this.handleAddPasskey.name}]`;
1279
+ if (!this.ibGib || !this.ibGibAddr) { return; }
1280
+
1281
+ try {
1282
+ const caps = getKeystoneCapabilities({ keystone: this.ibGib, aggregatedDetails: this.aggregatedDetails });
1283
+ const isCustodialOnly = caps.isCustodial && !caps.isSovereign;
1284
+
1285
+ const supported = await isPasskeyPrfSupported();
1286
+ if (!supported) {
1287
+ this.setPasskeyStatus("WebAuthn PRF extension is not supported in this browser environment.", "error");
1288
+ return;
1289
+ }
1290
+
1291
+ this.showBusy({
1292
+ isBusy: true,
1293
+ title: 'Touch Biometrics or Security Key',
1294
+ msg: 'Please touch your fingerprint sensor, Face ID, or insert your USB security key when prompted...',
1295
+ animationEmoji: '🛡️'
1296
+ });
1297
+
1298
+ const username = this.aggregatedDetails?.username || this.ibGib.data?.frameDetails?.username || 'user';
1299
+ const rawSalt = crypto.getRandomValues(new Uint8Array(32));
1300
+ const saltHex = bufferToHex(rawSalt.buffer as ArrayBuffer);
1301
+
1302
+ const regResult = await registerPasskeyCredential({
1303
+ username,
1304
+ displayName: username,
1305
+ salt: saltHex
1306
+ });
1307
+ const { credentialId, transports } = regResult;
1308
+
1309
+ let passkeySecret = regResult.passkeySecret;
1310
+ if (!passkeySecret) {
1311
+ const prfResult = await evaluatePasskeyPrf({
1312
+ credentialId,
1313
+ salt: saltHex,
1314
+ transports
1315
+ });
1316
+ passkeySecret = prfResult.passkeySecret;
1317
+ }
1318
+
1319
+ const defaultDeviceTag = generateDefaultDeviceTag();
1320
+ const passkeyName = defaultDeviceTag;
1321
+ const passkeyTag = getSaferSubstring({ text: passkeyName, length: 12, keepLiterals: ['-'] }) || 'key';
1322
+ const poolId = `passkey-manage-${passkeyTag}`;
1323
+
1324
+ const passkeyEntry: KeystonePasskeyEntry = {
1325
+ credentialId,
1326
+ poolId,
1327
+ name: passkeyName,
1328
+ salt: saltHex,
1329
+ createdUTC: new Date().toISOString(),
1330
+ ...(transports && transports.length > 0 ? { transports } : {})
1331
+ };
1332
+
1333
+ const metaspace = await getGlobalMetaspace_waitIfNeeded();
1334
+ if (!metaspace) throw new Error("Metaspace not found");
1335
+ const space = await metaspace.getLocalUserSpace({ lock: false });
1336
+ if (!space) throw new Error("Local space not found");
1337
+
1338
+ const keystoneService = new KeystoneService_V1();
1339
+ let evolvedKeystone: KeystoneIbGib_V1;
1340
+
1341
+ if (isCustodialOnly) {
1342
+ // Elevate custodial account with sovereign passkey
1343
+ this.showBusy({
1344
+ isBusy: true,
1345
+ title: 'Elevating Account Ownership...',
1346
+ msg: 'Establishing sovereign passkey pool on identity...',
1347
+ animationEmoji: '⚡'
1348
+ });
1349
+
1350
+ // Generate new manage pool configuration using passkeySecret
1351
+ const poolConfig: KeystonePoolConfig_HashV1 = {
1352
+ id: poolId,
1353
+ type: KeystoneChallengeType.hash_reveal_v1,
1354
+ salt: poolId,
1355
+ algo: HashAlgorithm.sha_256,
1356
+ rounds: 1,
1357
+ allowedVerbs: [KEYSTONE_VERB_MANAGE, KEYSTONE_VERB_SYNC, KEYSTONE_VERB_CONNECT],
1358
+ behavior: {
1359
+ size: 10,
1360
+ replenish: KeystoneReplenishStrategy.topUp,
1361
+ selectSequentially: 1,
1362
+ selectRandomly: 0,
1363
+ targetBindingCount: 0,
1364
+ }
1365
+ };
1366
+
1367
+ const challenges = await generatePoolChallenges({
1368
+ config: poolConfig,
1369
+ masterSecret: passkeySecret
1370
+ });
1371
+
1372
+ const newPool: KeystoneChallengePool = {
1373
+ id: poolId,
1374
+ config: poolConfig,
1375
+ challenges
1376
+ };
1377
+
1378
+ evolvedKeystone = await keystoneService.addPools({
1379
+ latestKeystone: this.ibGib,
1380
+ masterSecret: passkeySecret,
1381
+ newPools: [newPool],
1382
+ frameDetails: {
1383
+ passkeys: {
1384
+ [credentialId]: passkeyEntry
1385
+ }
1386
+ },
1387
+ dataToPatch: {
1388
+ credentialIds: [...(this.ibGib.data?.credentialIds || []), credentialId]
1389
+ },
1390
+ metaspace,
1391
+ space
1392
+ });
1393
+ } else {
1394
+ // Secondary passkey registration on existing sovereign identity:
1395
+ // Prompt user to authorize with current master password
1396
+ let authSecret: string | undefined;
1397
+ let isCorrect = false;
1398
+ let tries = 0;
1399
+ const maxTries = 5;
1400
+
1401
+ while (tries < maxTries && !isCorrect) {
1402
+ try {
1403
+ const promptMsg = tries === 0
1404
+ ? 'Enter your current Master Secret (Password) or touch security key to authorize adding this passkey:'
1405
+ : `Incorrect password. Please try again (${maxTries - tries} tries remaining):`;
1406
+ authSecret = await promptForSecret({
1407
+ msg: promptMsg,
1408
+ confirm: false,
1409
+ passkeys: this.aggregatedDetails?.passkeys,
1410
+ });
1411
+ } catch (err) {
1412
+ authSecret = undefined;
1413
+ break;
1414
+ }
1415
+
1416
+ if (!authSecret) break;
1417
+
1418
+ // Verify secret against manage/passkey pools
1419
+ isCorrect = await keystoneService.verifySigningSecret({
1420
+ keystoneIbGib: this.ibGib,
1421
+ signingSecret: authSecret
1422
+ });
1423
+
1424
+ if (!isCorrect) tries++;
1425
+ }
1426
+
1427
+ if (!isCorrect || !authSecret) {
1428
+ this.setPasskeyStatus("Registration cancelled (authorization required).", "error");
1429
+ return;
1430
+ }
1431
+
1432
+ this.showBusy({
1433
+ isBusy: true,
1434
+ title: 'Registering Passkey...',
1435
+ msg: 'Adding cryptographic challenge pool and evolving identity...',
1436
+ animationEmoji: '🛡️'
1437
+ });
1438
+
1439
+ const poolConfig: KeystonePoolConfig_HashV1 = {
1440
+ id: poolId,
1441
+ type: KeystoneChallengeType.hash_reveal_v1,
1442
+ salt: poolId,
1443
+ algo: HashAlgorithm.sha_256,
1444
+ rounds: 1,
1445
+ allowedVerbs: [KEYSTONE_VERB_MANAGE, KEYSTONE_VERB_SYNC, KEYSTONE_VERB_CONNECT],
1446
+ behavior: {
1447
+ size: 10,
1448
+ replenish: KeystoneReplenishStrategy.topUp,
1449
+ selectSequentially: 1,
1450
+ selectRandomly: 0,
1451
+ targetBindingCount: 0,
1452
+ }
1453
+ };
1454
+
1455
+ const challenges = await generatePoolChallenges({
1456
+ config: poolConfig,
1457
+ masterSecret: passkeySecret
1458
+ });
1459
+
1460
+ const newPool: KeystoneChallengePool = {
1461
+ id: poolId,
1462
+ config: poolConfig,
1463
+ challenges
1464
+ };
1465
+
1466
+ evolvedKeystone = await keystoneService.addPools({
1467
+ latestKeystone: this.ibGib,
1468
+ masterSecret: authSecret,
1469
+ newPools: [newPool],
1470
+ frameDetails: {
1471
+ passkeys: {
1472
+ [credentialId]: passkeyEntry
1473
+ }
1474
+ },
1475
+ dataToPatch: {
1476
+ credentialIds: [...(this.ibGib.data?.credentialIds || []), credentialId]
1477
+ },
1478
+ metaspace,
1479
+ space
1480
+ });
1481
+ }
1482
+
1483
+ // Sync evolved keystone to server
1484
+ this.showBusy({
1485
+ isBusy: true,
1486
+ title: 'Syncing to Server...',
1487
+ msg: 'Publishing evolved identity keystone to server...',
1488
+ animationEmoji: '⚡'
1489
+ });
1490
+
1491
+ const primaryTjpAddr = getTjpAddr({ ibGib: evolvedKeystone, defaultIfNone: 'incomingAddr' })!;
1492
+ const resSync = await getApiBridge().putEvolveKeystone({
1493
+ domainAddr: primaryTjpAddr,
1494
+ keystoneIbGib: evolvedKeystone,
1495
+ relatedKeystoneIbGibs: []
1496
+ });
1497
+
1498
+ if (!resSync.success) {
1499
+ console.warn(`${lc} Server sync note: ${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 })]
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 isCorrect = false;
1539
+ let tries = 0;
1540
+ const maxTries = 5;
1541
+
1542
+ while (tries < maxTries && !isCorrect) {
1543
+ try {
1544
+ const promptMsg = tries === 0
1545
+ ? `Are you sure you want to remove "${passkeyName}"? Enter your Master Secret (Password) or touch security key to authorize:`
1546
+ : `Incorrect password. Please try again (${maxTries - tries} tries remaining):`;
1547
+ masterSecret = await promptForSecret({
1548
+ msg: promptMsg,
1549
+ confirm: false,
1550
+ passkeys: this.aggregatedDetails?.passkeys,
1551
+ });
1552
+ } catch (err) {
1553
+ masterSecret = undefined;
1554
+ break;
1555
+ }
1556
+
1557
+ if (!masterSecret) break;
1558
+
1559
+ isCorrect = await keystoneService.verifySigningSecret({
1560
+ keystoneIbGib: this.ibGib,
1561
+ signingSecret: masterSecret
1562
+ });
1563
+
1564
+ if (!isCorrect) tries++;
1565
+ }
1566
+
1567
+ if (!isCorrect || !masterSecret) {
1568
+ this.setPasskeyStatus("Removal cancelled or unauthorized.", "error");
1569
+ return;
1570
+ }
1571
+
1572
+ this.showBusy({
1573
+ isBusy: true,
1574
+ title: 'Revoking Passkey...',
1575
+ msg: 'Tombstoning passkey record and removing challenge pool...',
1576
+ animationEmoji: '🗑️'
1577
+ });
1578
+
1579
+ const metaspace = await getGlobalMetaspace_waitIfNeeded();
1580
+ if (!metaspace) throw new Error("Metaspace not found");
1581
+ const space = await metaspace.getLocalUserSpace({ lock: false });
1582
+ if (!space) throw new Error("Local space not found");
1583
+
1584
+ const poolIdToRemove = passkeyEntry?.poolId || `passkey-manage-${credentialId.substring(0, 8)}`;
1585
+ const poolExists = this.ibGib.data?.challengePools.some(p => p.id === poolIdToRemove);
1586
+
1587
+ const filteredCredentialIds = (this.ibGib.data?.credentialIds || []).filter(id => id !== credentialId);
1588
+
1589
+ let evolvedKeystone: KeystoneIbGib_V1;
1590
+ if (poolExists) {
1591
+ evolvedKeystone = await keystoneService.removePools({
1592
+ latestKeystone: this.ibGib,
1593
+ masterSecret,
1594
+ poolIds: [poolIdToRemove],
1595
+ frameDetails: {
1596
+ passkeys: {
1597
+ [credentialId]: null
1598
+ }
1599
+ },
1600
+ dataToPatch: {
1601
+ credentialIds: filteredCredentialIds
1602
+ },
1603
+ metaspace,
1604
+ space
1605
+ });
1606
+ } else {
1607
+ evolvedKeystone = await keystoneService.sign({
1608
+ latestKeystone: this.ibGib,
1609
+ masterSecret,
1610
+ claim: {
1611
+ verb: POOL_ID_MANAGE,
1612
+ target: getIbGibAddr({ ibGib: this.ibGib })
1613
+ },
1614
+ frameDetails: {
1615
+ passkeys: {
1616
+ [credentialId]: null
1617
+ }
1618
+ },
1619
+ dataToPatch: {
1620
+ credentialIds: filteredCredentialIds
1621
+ },
1622
+ metaspace,
1623
+ space
1624
+ });
1625
+ }
1626
+
1627
+ // Sync to server
1628
+ this.showBusy({
1629
+ isBusy: true,
1630
+ title: 'Syncing to Server...',
1631
+ msg: 'Publishing updated keystone to server...',
1632
+ animationEmoji: '⚡'
1633
+ });
1634
+
1635
+ const primaryTjpAddr = getTjpAddr({ ibGib: evolvedKeystone, defaultIfNone: 'incomingAddr' })!;
1636
+ await getApiBridge().putEvolveKeystone({
1637
+ domainAddr: primaryTjpAddr,
1638
+ keystoneIbGib: evolvedKeystone,
1639
+ relatedKeystoneIbGibs: []
1640
+ });
1641
+
1642
+ // Update Special Index
1643
+ await updateSpecialIndex({
1644
+ type: "keystones",
1645
+ rel8nInfos: [
1646
+ {
1647
+ rel8nName: "keystone",
1648
+ ibGibs: [toDto({ ibGib: evolvedKeystone })]
1649
+ }
1650
+ ],
1651
+ metaspace,
1652
+ space
1653
+ });
1654
+
1655
+ this.setPasskeyStatus(`Passkey "${passkeyName}" successfully removed.`, "success");
1656
+ this.ibGibAddr = getIbGibAddr({ ibGib: evolvedKeystone });
1657
+ this.staticIbGib = evolvedKeystone;
1658
+ await this.loadIbGib({ getLatest: false });
1659
+
1660
+ } catch (error) {
1661
+ console.error(`${lc} ${extractErrorMsg(error)}`, error);
1662
+ this.setPasskeyStatus(`Failed to remove passkey: ${extractErrorMsg(error)}`, "error");
1663
+ } finally {
1664
+ this.showBusy({ isBusy: false });
1665
+ }
1666
+ }
1667
+
1003
1668
  override async handleContextUpdated(): Promise<void> {
1004
1669
  const lc = `${this.lc}[${this.handleContextUpdated.name}]`;
1005
1670
  try {
@@ -1039,10 +1704,26 @@ export class KeystoneDetailsComponentInstance
1039
1704
  const lc = `${this.lc}[${this.handleSSOLink.name}]`;
1040
1705
  this.setSSOStatus(`Linking ${providerId}...`, "info");
1041
1706
 
1707
+ const providerName = providerId === 'google' ? 'Google' : providerId === 'github' ? 'GitHub' : providerId;
1708
+ this.setSSOStatus(`Opening ${providerName} SSO authorization window...`, "info");
1709
+ this.showBusy({
1710
+ isBusy: true,
1711
+ title: `Connecting to ${providerName}...`,
1712
+ msg: `Opening ${providerName} SSO authorization window...\nPlease complete authorization in the popup window.`,
1713
+ animationEmoji: '🌐'
1714
+ });
1715
+
1042
1716
  try {
1717
+ const primaryTjpAddr = (this.ibGib ? getTjpAddr({ ibGib: this.ibGib }) : undefined)
1718
+ || (this.ibGibAddr ? (() => {
1719
+ const { ib } = getIbAndGib({ ibGibAddr: this.ibGibAddr });
1720
+ const { tjpGib } = getGibInfo({ ibGibAddr: this.ibGibAddr });
1721
+ return tjpGib && ib ? `${ib}^${tjpGib}` : this.ibGibAddr;
1722
+ })() : undefined);
1723
+
1043
1724
  const resPopup = await executeSsoOAuthPopup({
1044
1725
  providerId,
1045
- parentTjpAddr: this.ibGibAddr
1726
+ parentTjpAddr: primaryTjpAddr
1046
1727
  });
1047
1728
 
1048
1729
  if (!resPopup.success || !resPopup.code) {
@@ -1054,6 +1735,8 @@ export class KeystoneDetailsComponentInstance
1054
1735
  } catch (error) {
1055
1736
  console.error(`${lc} SSO link initiation failed: ${extractErrorMsg(error)}`);
1056
1737
  this.setSSOStatus(`Link failed: ${extractErrorMsg(error)}`, "error");
1738
+ } finally {
1739
+ this.showBusy({ isBusy: false });
1057
1740
  }
1058
1741
  }
1059
1742
 
@@ -1066,7 +1749,14 @@ export class KeystoneDetailsComponentInstance
1066
1749
 
1067
1750
  protected async handleSSOLinkCallback(code: string, stateObj: any, redirectUri: string) {
1068
1751
  const lc = `${this.lc}[${this.handleSSOLinkCallback.name}]`;
1069
- this.setSSOStatus("Retrieving challenge pool configuration from server...", "info");
1752
+ const providerName = stateObj.providerId === 'google' ? 'Google' : stateObj.providerId === 'github' ? 'GitHub' : stateObj.providerId;
1753
+ this.setSSOStatus(`Retrieving challenge pool configuration from server...`, "info");
1754
+ this.showBusy({
1755
+ isBusy: true,
1756
+ title: `Linking ${providerName}...`,
1757
+ msg: `Validating ${providerName} authorization with server...`,
1758
+ animationEmoji: '🔐'
1759
+ });
1070
1760
 
1071
1761
  try {
1072
1762
  // 1. Exchange OAuth code for custodian public pool config
@@ -1094,6 +1784,7 @@ export class KeystoneDetailsComponentInstance
1094
1784
  const custodianPool = linkData.custodianPool;
1095
1785
 
1096
1786
  // 2. Prompt user for master secret and verify it
1787
+ this.showBusy({ isBusy: false });
1097
1788
  const keystoneService = new KeystoneService_V1();
1098
1789
  let masterSecret: string | undefined;
1099
1790
  let isCorrect = false;
@@ -1137,6 +1828,12 @@ export class KeystoneDetailsComponentInstance
1137
1828
  return;
1138
1829
  }
1139
1830
 
1831
+ this.showBusy({
1832
+ isBusy: true,
1833
+ title: 'Evolving Identity...',
1834
+ msg: `Installing ${providerName} custodian pool and signing evolution...`,
1835
+ animationEmoji: '📝'
1836
+ });
1140
1837
  this.setSSOStatus("Evolving identity locally and generating cryptographic signatures...", "info");
1141
1838
 
1142
1839
  const metaspace = await getGlobalMetaspace_waitIfNeeded();
@@ -1144,7 +1841,7 @@ export class KeystoneDetailsComponentInstance
1144
1841
  const space = await metaspace.getLocalUserSpace({ lock: false });
1145
1842
  if (!space) { throw new Error("Local space not found"); }
1146
1843
 
1147
- // 3. Evolve parent keystone to include the custodian pool
1844
+ // 3. Evolve parent keystone to include the custodian pool (n -> n+1)
1148
1845
  const evolvedParent = await keystoneService.addPools({
1149
1846
  latestKeystone: this.ibGib!,
1150
1847
  masterSecret: masterSecret!,
@@ -1153,24 +1850,64 @@ export class KeystoneDetailsComponentInstance
1153
1850
  space
1154
1851
  });
1155
1852
 
1156
- // 4. Sync evolved parent keystone back to server
1157
- this.setSSOStatus("Publishing evolved identity keystone to server...", "info");
1158
- const syncRes = await getApiBridge().putEvolveKeystone({
1853
+ // Sync evolved parent (n+1) to server first
1854
+ const syncRes1 = await getApiBridge().putEvolveKeystone({
1159
1855
  domainAddr: stateObj.parentTjpAddr,
1160
1856
  keystoneIbGib: evolvedParent,
1161
- relatedKeystoneIbGibs: [] // No outward delegate timelines
1857
+ relatedKeystoneIbGibs: []
1162
1858
  });
1163
- if (!syncRes.success) {
1164
- throw new Error(`Server synchronization rejected: ${syncRes.message}`);
1859
+ if (!syncRes1.success) {
1860
+ throw new Error(`Server synchronization of linked pool rejected: ${syncRes1.message}`);
1165
1861
  }
1166
1862
 
1167
- // 5. Update index locally
1863
+ // 4. Create and register OAuth2 sync delegate for this provider (n+1 -> n+2)
1864
+ const username = this.aggregatedDetails?.username || this.ibGib?.data?.username || 'user';
1865
+ const { delegateKeystone, deviceTag } = await createOAuth2DeviceSyncDelegate({
1866
+ providerId: stateObj.providerId,
1867
+ username,
1868
+ primaryTjpAddr: stateObj.parentTjpAddr,
1869
+ custodianReqs: linkData.custodianReqs,
1870
+ metaspace,
1871
+ space,
1872
+ });
1873
+
1874
+ const parentWithDelegate = await keystoneService.registerDelegate({
1875
+ parentKeystone: evolvedParent,
1876
+ delegateKeystone,
1877
+ masterSecret: masterSecret!,
1878
+ signingPoolId: 'manage',
1879
+ metaspace,
1880
+ space,
1881
+ allowedVerbs: [KEYSTONE_VERB_SYNC, KEYSTONE_VERB_CONNECT],
1882
+ deviceName: deviceTag,
1883
+ });
1884
+
1885
+ await metaspace.put({ ibGibs: [delegateKeystone, parentWithDelegate], space });
1886
+
1887
+ // 5. Sync parent with registered delegate (n+2) to server
1888
+ this.showBusy({
1889
+ isBusy: true,
1890
+ title: 'Syncing to Server...',
1891
+ msg: 'Publishing linked identity and delegate to server...',
1892
+ animationEmoji: '⚡'
1893
+ });
1894
+ this.setSSOStatus("Publishing evolved identity keystone to server...", "info");
1895
+ const syncRes2 = await getApiBridge().putEvolveKeystone({
1896
+ domainAddr: stateObj.parentTjpAddr,
1897
+ keystoneIbGib: parentWithDelegate,
1898
+ relatedKeystoneIbGibs: [delegateKeystone]
1899
+ });
1900
+ if (!syncRes2.success) {
1901
+ throw new Error(`Server synchronization of delegate rejected: ${syncRes2.message}`);
1902
+ }
1903
+
1904
+ // 6. Update index locally
1168
1905
  await updateSpecialIndex({
1169
1906
  type: "keystones",
1170
1907
  rel8nInfos: [
1171
1908
  {
1172
1909
  rel8nName: "keystone",
1173
- ibGibs: [toDto({ ibGib: evolvedParent })]
1910
+ ibGibs: [toDto({ ibGib: parentWithDelegate }), toDto({ ibGib: delegateKeystone })]
1174
1911
  }
1175
1912
  ],
1176
1913
  metaspace,
@@ -1178,17 +1915,28 @@ export class KeystoneDetailsComponentInstance
1178
1915
  });
1179
1916
 
1180
1917
  this.setSSOStatus(`Successfully linked with ${stateObj.providerId}!`, "success");
1918
+ this.ibGibAddr = getIbGibAddr({ ibGib: parentWithDelegate });
1919
+ this.staticIbGib = parentWithDelegate;
1181
1920
  await this.loadIbGib({ getLatest: false });
1182
1921
 
1183
1922
  } catch (error) {
1184
1923
  console.error(`${lc} SSO callback processing failed: ${extractErrorMsg(error)}`);
1185
1924
  this.setSSOStatus(`Link failed: ${extractErrorMsg(error)}`, "error");
1925
+ } finally {
1926
+ this.showBusy({ isBusy: false });
1186
1927
  }
1187
1928
  }
1188
1929
 
1189
1930
  protected async handleSSOClick(providerId: 'google' | 'github') {
1190
- const isLinked = isSsoProviderLinked({ keystone: this.ibGib, providerId });
1931
+ const isLinked = !!this.ibGib ?
1932
+ isSsoProviderLinked({ keystone: this.ibGib, aggregatedDetails: this.aggregatedDetails, providerId }) :
1933
+ false;
1191
1934
  if (isLinked) {
1935
+ const caps = getKeystoneCapabilities({ keystone: this.ibGib!, aggregatedDetails: this.aggregatedDetails });
1936
+ if (!caps.isSovereign) {
1937
+ 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");
1938
+ return;
1939
+ }
1192
1940
  await this.handleSSOUnlink(providerId);
1193
1941
  } else {
1194
1942
  await this.handleSSOLink(providerId);
@@ -1197,7 +1945,8 @@ export class KeystoneDetailsComponentInstance
1197
1945
 
1198
1946
  protected async handleSSOUnlink(providerId: 'google' | 'github') {
1199
1947
  const lc = `${this.lc}[${this.handleSSOUnlink.name}]`;
1200
- this.setSSOStatus(`Unlinking ${providerId}...`, "info");
1948
+ const providerName = providerId === 'google' ? 'Google' : providerId === 'github' ? 'GitHub' : providerId;
1949
+ this.setSSOStatus(`Unlinking ${providerName}...`, "info");
1201
1950
 
1202
1951
  try {
1203
1952
  // 1. Prompt user for password
@@ -1210,11 +1959,12 @@ export class KeystoneDetailsComponentInstance
1210
1959
  while (tries < maxTries && !isCorrect) {
1211
1960
  try {
1212
1961
  const promptMsg = tries === 0
1213
- ? `Are you sure you want to unlink your ${providerId} account? If so, enter your local Master Secret (Password) to authorize. Otherwise, click Cancel.`
1962
+ ? `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.`
1214
1963
  : `Incorrect password. Please try again (${maxTries - tries} tries remaining):`;
1215
1964
  masterSecret = await promptForSecret({
1216
1965
  msg: promptMsg,
1217
- confirm: false
1966
+ confirm: false,
1967
+ passkeys: this.aggregatedDetails?.passkeys,
1218
1968
  });
1219
1969
  } catch (error) {
1220
1970
  masterSecret = undefined;
@@ -1225,11 +1975,10 @@ export class KeystoneDetailsComponentInstance
1225
1975
  break;
1226
1976
  }
1227
1977
 
1228
- // Verify secret using verifySigningSecret (requires manage poolId)
1978
+ // Verify secret using verifySigningSecret
1229
1979
  isCorrect = await keystoneService.verifySigningSecret({
1230
1980
  keystoneIbGib: this.ibGib!,
1231
- signingSecret: masterSecret,
1232
- poolId: 'manage'
1981
+ signingSecret: masterSecret
1233
1982
  });
1234
1983
 
1235
1984
  if (!isCorrect) {
@@ -1242,6 +1991,12 @@ export class KeystoneDetailsComponentInstance
1242
1991
  return;
1243
1992
  }
1244
1993
 
1994
+ this.showBusy({
1995
+ isBusy: true,
1996
+ title: 'Unlinking Provider...',
1997
+ msg: `Removing ${providerName} custodian pool and delegate keys...`,
1998
+ animationEmoji: '🔓'
1999
+ });
1245
2000
  this.setSSOStatus("Evolving identity locally and removing custodian pool...", "info");
1246
2001
 
1247
2002
  const metaspace = await getGlobalMetaspace_waitIfNeeded();
@@ -1251,7 +2006,7 @@ export class KeystoneDetailsComponentInstance
1251
2006
 
1252
2007
  // 2. Evolve parent keystone to remove the custodian pool
1253
2008
  const targetPoolId = `custodian-manage-${providerId}`;
1254
- const evolvedParent = await keystoneService.removePools({
2009
+ let evolvedParent = await keystoneService.removePools({
1255
2010
  latestKeystone: this.ibGib!,
1256
2011
  masterSecret: masterSecret!,
1257
2012
  poolIds: [targetPoolId],
@@ -1259,10 +2014,24 @@ export class KeystoneDetailsComponentInstance
1259
2014
  space
1260
2015
  });
1261
2016
 
1262
- // 3. Sync evolved parent keystone back to server
2017
+ const primaryTjpAddr = (this.ibGib ? getTjpAddr({ ibGib: this.ibGib }) : undefined)
2018
+ || (this.ibGibAddr ? (() => {
2019
+ const { ib } = getIbAndGib({ ibGibAddr: this.ibGibAddr });
2020
+ const { tjpGib } = getGibInfo({ ibGibAddr: this.ibGibAddr });
2021
+ return tjpGib && ib ? `${ib}^${tjpGib}` : this.ibGibAddr;
2022
+ })() : undefined)
2023
+ || getIbGibAddr({ ibGib: this.ibGib! });
2024
+
2025
+ // 3. Sync pool removal frame to server
2026
+ this.showBusy({
2027
+ isBusy: true,
2028
+ title: 'Syncing to Server...',
2029
+ msg: `Publishing ${providerName} pool removal to server...`,
2030
+ animationEmoji: '⚡'
2031
+ });
1263
2032
  this.setSSOStatus("Publishing evolved identity keystone to server...", "info");
1264
- const syncRes = await getApiBridge().putEvolveKeystone({
1265
- domainAddr: getIbGibAddr({ ibGib: this.ibGib! }),
2033
+ let syncRes = await getApiBridge().putEvolveKeystone({
2034
+ domainAddr: primaryTjpAddr,
1266
2035
  keystoneIbGib: evolvedParent,
1267
2036
  relatedKeystoneIbGibs: [] // No outward delegate timelines
1268
2037
  });
@@ -1270,6 +2039,38 @@ export class KeystoneDetailsComponentInstance
1270
2039
  throw new Error(`Server synchronization rejected: ${syncRes.message}`);
1271
2040
  }
1272
2041
 
2042
+ // 4. Unregister any corresponding outward delegate for this provider if present
2043
+ const delegatesMap = evolvedParent.data?.delegates || this.ibGib?.data?.delegates;
2044
+ if (delegatesMap) {
2045
+ for (const [delegateTjpAddr, info] of Object.entries(delegatesMap)) {
2046
+ if (isCustodialDelegate({ delegateInfo: info, providerId })) {
2047
+ evolvedParent = await keystoneService.unregisterDelegate({
2048
+ parentKeystone: evolvedParent,
2049
+ delegateTjpAddr,
2050
+ masterSecret: masterSecret!,
2051
+ signingPoolId: 'manage',
2052
+ metaspace,
2053
+ space,
2054
+ });
2055
+
2056
+ this.showBusy({
2057
+ isBusy: true,
2058
+ title: 'Syncing to Server...',
2059
+ msg: `Publishing ${providerName} delegate unregistration to server...`,
2060
+ animationEmoji: '⚡'
2061
+ });
2062
+ syncRes = await getApiBridge().putEvolveKeystone({
2063
+ domainAddr: primaryTjpAddr,
2064
+ keystoneIbGib: evolvedParent,
2065
+ relatedKeystoneIbGibs: []
2066
+ });
2067
+ if (!syncRes.success) {
2068
+ throw new Error(`Server synchronization rejected: ${syncRes.message}`);
2069
+ }
2070
+ }
2071
+ }
2072
+ }
2073
+
1273
2074
  // 4. Update index locally
1274
2075
  await updateSpecialIndex({
1275
2076
  type: "keystones",
@@ -1283,15 +2084,21 @@ export class KeystoneDetailsComponentInstance
1283
2084
  space
1284
2085
  });
1285
2086
 
1286
- this.setSSOStatus(`Successfully unlinked ${providerId}!`, "success");
1287
- await this.loadIbGib({ getLatest: true });
2087
+ this.setSSOStatus(`Successfully unlinked ${providerName}!`, "success");
2088
+ this.ibGibAddr = getIbGibAddr({ ibGib: evolvedParent });
2089
+ this.staticIbGib = evolvedParent;
2090
+ await this.loadIbGib({ getLatest: false });
1288
2091
 
1289
2092
  } catch (error) {
1290
- console.error(`${lc} SSO unlink failed: ${extractErrorMsg(error)}`);
1291
- this.setSSOStatus(`Unlink failed: ${extractErrorMsg(error)}`, "error");
2093
+ console.error(`${lc} ${extractErrorMsg(error)}`, error);
2094
+ this.setSSOStatus(`Failed to unlink ${providerName}: ${extractErrorMsg(error)}`, "error");
2095
+ } finally {
2096
+ this.showBusy({ isBusy: false });
1292
2097
  }
1293
2098
  }
1294
2099
 
2100
+
2101
+
1295
2102
  private setChangePasswordStatus(msg: string, type: 'info' | 'success' | 'error') {
1296
2103
  const el = this.elements!.changePasswordStatusEl;
1297
2104
  el.textContent = msg;
@@ -1299,9 +2106,380 @@ export class KeystoneDetailsComponentInstance
1299
2106
  el.classList.remove('hidden');
1300
2107
  }
1301
2108
 
2109
+ protected async handleAddMasterPassword(): Promise<void> {
2110
+ const lc = `${this.lc}[${this.handleAddMasterPassword.name}]`;
2111
+ this.setChangePasswordStatus("Validating entries...", "info");
2112
+
2113
+ try {
2114
+ const newPassword = this.elements!.inputNewPasswordEl.value;
2115
+ const confirmPassword = this.elements!.inputConfirmPasswordEl.value;
2116
+
2117
+ if (!newPassword) {
2118
+ this.setChangePasswordStatus("Master password is required.", "error");
2119
+ return;
2120
+ }
2121
+ if (newPassword !== confirmPassword) {
2122
+ this.setChangePasswordStatus("Master password and confirmation do not match.", "error");
2123
+ return;
2124
+ }
2125
+
2126
+ const initialManagePool = this.ibGib?.data?.challengePools?.find(p => p.id === POOL_ID_MANAGE);
2127
+ if (!initialManagePool) {
2128
+ this.setChangePasswordStatus("Primary keystone missing active manage challenge pool.", "error");
2129
+ return;
2130
+ }
2131
+
2132
+ this.showBusy({
2133
+ isBusy: true,
2134
+ title: 'Elevating Identity...',
2135
+ msg: 'Generating cryptographic sovereign challenge pool...',
2136
+ animationEmoji: '🔑'
2137
+ });
2138
+ this.setChangePasswordStatus("Generating sovereign challenge pool...", "info");
2139
+ const newChallenges = await generatePoolChallenges({
2140
+ config: initialManagePool.config,
2141
+ masterSecret: newPassword,
2142
+ });
2143
+ const newManagePool = {
2144
+ id: POOL_ID_MANAGE,
2145
+ config: initialManagePool.config,
2146
+ challenges: newChallenges,
2147
+ };
2148
+
2149
+ const primaryTjpAddr = (this.ibGib ? getTjpAddr({ ibGib: this.ibGib }) : undefined)
2150
+ || (this.ibGibAddr ? (() => {
2151
+ const { ib } = getIbAndGib({ ibGibAddr: this.ibGibAddr });
2152
+ const { tjpGib } = getGibInfo({ ibGibAddr: this.ibGibAddr });
2153
+ return tjpGib && ib ? `${ib}^${tjpGib}` : this.ibGibAddr;
2154
+ })() : undefined)
2155
+ || getIbGibAddr({ ibGib: this.ibGib! });
2156
+
2157
+ const custodianProvider = this.aggregatedDetails?.custodianProvider
2158
+ || this.ibGib?.data?.frameDetails?.custodianProvider
2159
+ || (this.ibGib?.data as any)?.custodianProvider;
2160
+
2161
+ let requestBody: any = {
2162
+ primaryTjpAddr,
2163
+ newManagePool,
2164
+ };
2165
+
2166
+ if (custodianProvider === 'google' || custodianProvider === 'github') {
2167
+ this.showBusy({
2168
+ isBusy: true,
2169
+ title: 'Re-authenticating with Provider...',
2170
+ msg: `Please complete authentication in the ${custodianProvider} popup window...`,
2171
+ animationEmoji: '🔒'
2172
+ });
2173
+ this.setChangePasswordStatus(`Please authenticate in the popup with ${custodianProvider} to verify ownership...`, "info");
2174
+ const resPopup = await executeSsoOAuthPopup({
2175
+ providerId: custodianProvider,
2176
+ parentTjpAddr: primaryTjpAddr,
2177
+ });
2178
+
2179
+ if (!resPopup.success || !resPopup.code) {
2180
+ this.showBusy({ isBusy: false });
2181
+ this.setChangePasswordStatus(resPopup.message || "Re-authentication cancelled.", "error");
2182
+ return;
2183
+ }
2184
+
2185
+ requestBody.providerId = custodianProvider;
2186
+ requestBody.code = resPopup.code;
2187
+ requestBody.redirectUri = resPopup.redirectUri;
2188
+ } else {
2189
+ // Email OTP flow
2190
+ const email = this.aggregatedDetails?.emails?.[0]
2191
+ || this.ibGib?.data?.emails?.[0]
2192
+ || this.ibGib?.data?.frameDetails?.emails?.[0];
2193
+
2194
+ if (!email) {
2195
+ this.showBusy({ isBusy: false });
2196
+ this.setChangePasswordStatus("No email address found for this identity to send verification code.", "error");
2197
+ return;
2198
+ }
2199
+
2200
+ const otpSection = this.elements?.sectionAddPasswordOtpEl;
2201
+ const otpInput = this.elements?.inputAddPasswordOtpEl;
2202
+ const isOtpVisible = otpSection && !otpSection.classList.contains('hidden');
2203
+
2204
+ // Step 1: If OTP section is not visible yet, send code and show the OTP input inline in the card
2205
+ if (!isOtpVisible) {
2206
+ this.showBusy({
2207
+ isBusy: true,
2208
+ title: 'Sending Verification Code...',
2209
+ msg: `Sending 6-digit verification code to ${email}...`,
2210
+ animationEmoji: '📧'
2211
+ });
2212
+ this.setChangePasswordStatus(`Sending verification code to ${email}...`, "info");
2213
+
2214
+ const resCode = await getApiBridge().postVerificationCode({
2215
+ email,
2216
+ primaryTjpAddr,
2217
+ });
2218
+
2219
+ this.showBusy({ isBusy: false });
2220
+
2221
+ if (!resCode.success) {
2222
+ this.setChangePasswordStatus(resCode.message || "Failed to send verification code.", "error");
2223
+ return;
2224
+ }
2225
+
2226
+ if (otpSection) {
2227
+ otpSection.classList.remove('hidden');
2228
+ }
2229
+ if (this.elements?.descAddPasswordOtpEl) {
2230
+ this.elements.descAddPasswordOtpEl.textContent = `A 6-digit verification code was sent to ${email}. Enter it below to establish sovereign ownership:`;
2231
+ }
2232
+ if (this.elements?.btnChangePasswordEl) {
2233
+ this.elements.btnChangePasswordEl.textContent = "Verify Code & Add Master Password";
2234
+ }
2235
+ this.setChangePasswordStatus(`✉️ 6-digit verification code sent to ${email}. Enter the code and click "Verify Code & Add Master Password".`, "info");
2236
+ this.startResendCooldownTimer(30);
2237
+ if (otpInput) {
2238
+ otpInput.value = '';
2239
+ otpInput.focus();
2240
+ }
2241
+ return;
2242
+ }
2243
+
2244
+ // Step 2: OTP section is visible - validate entered code
2245
+ const otpCode = (otpInput?.value || '').trim();
2246
+ if (!otpCode || otpCode.length < 6) {
2247
+ this.setChangePasswordStatus("Please enter the complete 6-digit verification code sent to your email.", "error");
2248
+ if (otpInput) { otpInput.focus(); }
2249
+ return;
2250
+ }
2251
+
2252
+ requestBody.email = email;
2253
+ requestBody.code = otpCode;
2254
+ }
2255
+
2256
+ this.showBusy({
2257
+ isBusy: true,
2258
+ title: 'Updating Keystone Timeline...',
2259
+ msg: 'Publishing sovereign master password and evolving identity DAG...',
2260
+ animationEmoji: '⏳'
2261
+ });
2262
+ this.setChangePasswordStatus("Elevating identity to sovereign master password on server...", "info");
2263
+ const response = await fetch('/api/identity/custodian/add-master-password', {
2264
+ method: 'POST',
2265
+ headers: { 'Content-Type': 'application/json' },
2266
+ body: JSON.stringify(requestBody),
2267
+ });
2268
+
2269
+ const resJson = await response.json();
2270
+ if (!response.ok || !resJson.success) {
2271
+ throw new Error(resJson.error || resJson.message || `Server error (${response.status})`);
2272
+ }
2273
+
2274
+ const evolvedParent = resJson.primaryKeystone;
2275
+
2276
+ // Save to local user space & metaspace
2277
+ const metaspace = await getGlobalMetaspace_waitIfNeeded();
2278
+ if (!metaspace) { throw new Error("Global metaspace not available"); }
2279
+ const space = await metaspace.getLocalUserSpace({ lock: false });
2280
+ if (!space) { throw new Error("Local user space not available"); }
2281
+
2282
+ if (resJson.primaryGraph) {
2283
+ const graphIbGibs = Object.values(resJson.primaryGraph) as any[];
2284
+ await metaspace.put({ ibGibs: graphIbGibs, space });
2285
+ const timelinesMap = getTimelinesGroupedByTjp({ ibGibs: graphIbGibs });
2286
+ for (const tjpKey of Object.keys(timelinesMap)) {
2287
+ for (const frame of timelinesMap[tjpKey]) {
2288
+ await metaspace.registerNewIbGib({ ibGib: frame, space });
2289
+ }
2290
+ }
2291
+ } else {
2292
+ await metaspace.put({ ibGibs: [evolvedParent], space });
2293
+ await metaspace.registerNewIbGib({ ibGib: evolvedParent, space });
2294
+ }
2295
+
2296
+ // 1. Create a password-based sync delegate for this device
2297
+ this.showBusy({
2298
+ isBusy: true,
2299
+ title: 'Creating Sovereign Sync Delegate...',
2300
+ msg: 'Deriving local device sync delegate keys from Master Password...',
2301
+ animationEmoji: '🔑'
2302
+ });
2303
+
2304
+ const rawUsername = this.aggregatedDetails?.username
2305
+ || this.ibGib?.data?.username
2306
+ || this.aggregatedDetails?.emails?.[0]?.split('@')[0]
2307
+ || this.ibGib?.data?.emails?.[0]?.split('@')[0]
2308
+ || 'user';
2309
+
2310
+ const { delegateKeystone: userSyncDelegate, deviceTag } = await createSovereignDeviceSyncDelegate({
2311
+ username: rawUsername,
2312
+ primaryTjpAddr,
2313
+ masterSecret: newPassword,
2314
+ metaspace,
2315
+ space,
2316
+ });
2317
+
2318
+ // 2. Register the new user sync delegate on the evolved parent keystone
2319
+ this.showBusy({
2320
+ isBusy: true,
2321
+ title: 'Registering Sovereign Delegate...',
2322
+ msg: 'Registering user sync delegate on primary keystone...',
2323
+ animationEmoji: '📝'
2324
+ });
2325
+
2326
+ const keystoneService = new KeystoneService_V1();
2327
+ const parentWithUserDelegate = await keystoneService.registerDelegate({
2328
+ parentKeystone: evolvedParent,
2329
+ delegateKeystone: userSyncDelegate,
2330
+ masterSecret: newPassword,
2331
+ signingPoolId: 'manage',
2332
+ metaspace,
2333
+ space,
2334
+ allowedVerbs: [KEYSTONE_VERB_SYNC, KEYSTONE_VERB_CONNECT],
2335
+ deviceName: deviceTag
2336
+ });
2337
+
2338
+ // 3. Post evolved keystone with new delegate to server
2339
+ this.showBusy({
2340
+ isBusy: true,
2341
+ title: 'Syncing to Server...',
2342
+ msg: 'Publishing updated domain identity and sovereign delegate to server...',
2343
+ animationEmoji: '⚡'
2344
+ });
2345
+
2346
+ const resSync = await getApiBridge().putEvolveKeystone({
2347
+ domainAddr: primaryTjpAddr,
2348
+ keystoneIbGib: parentWithUserDelegate,
2349
+ relatedKeystoneIbGibs: [userSyncDelegate]
2350
+ });
2351
+ if (!resSync.success) {
2352
+ console.warn(`${lc} Warning syncing user delegate to server: ${resSync.message}`);
2353
+ }
2354
+
2355
+ // 4. Save parentWithUserDelegate and userSyncDelegate to local space & metaspace
2356
+ await metaspace.put({ ibGibs: [parentWithUserDelegate, userSyncDelegate], space });
2357
+ await metaspace.registerNewIbGib({ ibGib: parentWithUserDelegate, space });
2358
+ await metaspace.registerNewIbGib({ ibGib: userSyncDelegate, space });
2359
+
2360
+ await updateSpecialIndex({
2361
+ type: "keystones",
2362
+ rel8nInfos: [
2363
+ {
2364
+ rel8nName: "keystone",
2365
+ ibGibs: [toDto({ ibGib: parentWithUserDelegate }), toDto({ ibGib: userSyncDelegate })]
2366
+ }
2367
+ ],
2368
+ metaspace,
2369
+ space
2370
+ });
2371
+
2372
+ const evolvedAddr = getIbGibAddr({ ibGib: parentWithUserDelegate });
2373
+
2374
+ // Dispatch EVENT_IBGIB_IDENTITY_CHANGED
2375
+ window.dispatchEvent(new CustomEvent(EVENT_IBGIB_IDENTITY_CHANGED, {
2376
+ detail: { activeIdentityAddr: evolvedAddr },
2377
+ bubbles: true,
2378
+ composed: true
2379
+ }));
2380
+
2381
+ this.setChangePasswordStatus("Successfully added Master Password! Identity is now sovereign.", "success");
2382
+
2383
+ // Reset OTP section and button text
2384
+ if (this.elements?.sectionAddPasswordOtpEl) {
2385
+ this.elements.sectionAddPasswordOtpEl.classList.add('hidden');
2386
+ if (this.elements.inputAddPasswordOtpEl) {
2387
+ this.elements.inputAddPasswordOtpEl.value = '';
2388
+ }
2389
+ }
2390
+ if (this.elements?.btnChangePasswordEl) {
2391
+ this.elements.btnChangePasswordEl.textContent = "Update Password";
2392
+ }
2393
+
2394
+ // Refresh the view
2395
+ this.ibGibAddr = evolvedAddr;
2396
+ this.staticIbGib = parentWithUserDelegate;
2397
+ await this.loadIbGib({ getLatest: true });
2398
+
2399
+ // Clear inputs
2400
+ this.elements!.inputNewPasswordEl.value = '';
2401
+ this.elements!.inputConfirmPasswordEl.value = '';
2402
+
2403
+ } catch (error) {
2404
+ console.error(`${lc} Elevation failed: ${extractErrorMsg(error)}`);
2405
+ this.setChangePasswordStatus(`Failed to add master password: ${extractErrorMsg(error)}`, "error");
2406
+ if (this.elements?.inputAddPasswordOtpEl && !this.elements.sectionAddPasswordOtpEl?.classList.contains('hidden')) {
2407
+ this.elements.inputAddPasswordOtpEl.select();
2408
+ this.elements.inputAddPasswordOtpEl.focus();
2409
+ }
2410
+ } finally {
2411
+ this.showBusy({ isBusy: false });
2412
+ }
2413
+ }
2414
+
2415
+ protected async handleResendAddPasswordOtp(): Promise<void> {
2416
+ const lc = `${this.lc}[${this.handleResendAddPasswordOtp.name}]`;
2417
+ const email = this.aggregatedDetails?.emails?.[0]
2418
+ || this.ibGib?.data?.emails?.[0]
2419
+ || this.ibGib?.data?.frameDetails?.emails?.[0];
2420
+ if (!email) {
2421
+ this.setChangePasswordStatus("No email address found for this identity to send verification code.", "error");
2422
+ return;
2423
+ }
2424
+ const primaryTjpAddr = (this.ibGib ? getTjpAddr({ ibGib: this.ibGib }) : undefined)
2425
+ || (this.ibGibAddr ? (() => {
2426
+ const { ib } = getIbAndGib({ ibGibAddr: this.ibGibAddr });
2427
+ const { tjpGib } = getGibInfo({ ibGibAddr: this.ibGibAddr });
2428
+ return tjpGib && ib ? `${ib}^${tjpGib}` : this.ibGibAddr;
2429
+ })() : undefined)
2430
+ || getIbGibAddr({ ibGib: this.ibGib! });
2431
+
2432
+ const btn = this.elements!.btnResendAddPasswordOtpEl;
2433
+ if (btn) {
2434
+ btn.disabled = true;
2435
+ btn.textContent = "Sending...";
2436
+ }
2437
+ this.setChangePasswordStatus(`Resending verification code to ${email}...`, "info");
2438
+ try {
2439
+ const resCode = await getApiBridge().postVerificationCode({ email, primaryTjpAddr });
2440
+ if (resCode.success) {
2441
+ this.setChangePasswordStatus(`✉️ Fresh 6-digit verification code sent to ${email}!`, "success");
2442
+ this.startResendCooldownTimer(30);
2443
+ if (this.elements?.inputAddPasswordOtpEl) {
2444
+ this.elements.inputAddPasswordOtpEl.value = '';
2445
+ this.elements.inputAddPasswordOtpEl.focus();
2446
+ }
2447
+ } else {
2448
+ this.setChangePasswordStatus(resCode.message || "Failed to resend verification code.", "error");
2449
+ if (btn) {
2450
+ btn.disabled = false;
2451
+ btn.textContent = "Resend Code";
2452
+ }
2453
+ }
2454
+ } catch (err) {
2455
+ this.setChangePasswordStatus(`Error resending verification code: ${extractErrorMsg(err)}`, "error");
2456
+ if (btn) {
2457
+ btn.disabled = false;
2458
+ btn.textContent = "Resend Code";
2459
+ }
2460
+ }
2461
+ }
2462
+
1302
2463
  protected async handleChangePassword(): Promise<void> {
1303
2464
  const lc = `${this.lc}[${this.handleChangePassword.name}]`;
2465
+
2466
+ const caps = !!this.ibGib ?
2467
+ getKeystoneCapabilities({ keystone: this.ibGib, aggregatedDetails: this.aggregatedDetails }) :
2468
+ undefined;
2469
+
2470
+ const isCustodialOnly = !!caps ? caps.isCustodial && !caps.isSovereign : false;
2471
+ if (isCustodialOnly) {
2472
+ await this.handleAddMasterPassword();
2473
+ return;
2474
+ }
2475
+
1304
2476
  this.setChangePasswordStatus("Validating entries...", "info");
2477
+ this.showBusy({
2478
+ isBusy: true,
2479
+ title: 'Changing Master Password...',
2480
+ msg: 'Validating current password...',
2481
+ animationEmoji: '🔐'
2482
+ });
1305
2483
 
1306
2484
  try {
1307
2485
  const currentPassword = this.elements!.inputCurrentPasswordEl.value;
@@ -1334,6 +2512,12 @@ export class KeystoneDetailsComponentInstance
1334
2512
  return;
1335
2513
  }
1336
2514
 
2515
+ this.showBusy({
2516
+ isBusy: true,
2517
+ title: 'Evolving Master Password...',
2518
+ msg: 'Generating new challenge proofs and rotating local user delegates...',
2519
+ animationEmoji: '⚙️'
2520
+ });
1337
2521
  this.setChangePasswordStatus("Evolving primary identity keystone...", "info");
1338
2522
 
1339
2523
  const metaspace = await getGlobalMetaspace_waitIfNeeded();
@@ -1341,8 +2525,7 @@ export class KeystoneDetailsComponentInstance
1341
2525
  const space = await metaspace.getLocalUserSpace({ lock: false });
1342
2526
  if (!space) { throw new Error(`(UNEXPECTED) couldn't get default local space? (E: 2fc1281c28351fd378a1d7f88eaa0526)`); }
1343
2527
 
1344
- // 2. Rotate parent identity
1345
- debugger; // walk through this...looking for delegate failure // hmm...I thought we got change password going?
2528
+ // 2. Rotate parent identity and sovereign sync delegates
1346
2529
  const { newKeystone: evolvedParent, newDelegates: evolvedDelegates } =
1347
2530
  await keystoneService.changePassword({
1348
2531
  keystoneIbGib: this.ibGib!,
@@ -1352,62 +2535,22 @@ export class KeystoneDetailsComponentInstance
1352
2535
  metaspace,
1353
2536
  space
1354
2537
  });
1355
- debugger; // look at evolvedDelegates
1356
-
1357
- this.setChangePasswordStatus("Resolving and rotating delegate keystones...", "info");
1358
-
1359
- // // 3. Evolve registered delegate keystones (preserving custodian/foreign delegate secrets)
1360
- // const delegates = this.ibGib!.data?.delegates || {};
1361
- // const evolvedDelegates: KeystoneIbGib_V1[] = [];
1362
-
1363
- // for (const delegateInfo of Object.values(delegates)) {
1364
- // const delegateAddr = delegateInfo.delegateAddr;
1365
- // const delegateKeystone = await keystoneService.getLatestKeystone({
1366
- // addr: delegateAddr,
1367
- // metaspace,
1368
- // space,
1369
- // });
1370
-
1371
- // // Identify user-owned challenge pools in the delegate that match the old password
1372
- // const poolsToRotate: string[] = [];
1373
- // for (const pool of delegateKeystone.data?.challengePools ?? []) {
1374
- // if (!pool.isForeign) {
1375
- // /**
1376
- // * WARNING: There is an extreme edge case that if two pools
1377
- // * happen to be owned by different parties but share the
1378
- // * same secret, this will change the wrong pool's secret
1379
- // * also. For V1, this shouldn't matter. But we need a
1380
- // * better way of tracking pool ownership for V2 (or even
1381
- // * V1 if we have multi-party support). (DO NOT REMOVE
1382
- // * THIS COMMENT UNTIL THIS IS ADDRESSED)
1383
- // */
1384
- // const isPoolMatch = await keystoneService.verifySigningSecret({
1385
- // keystoneIbGib: delegateKeystone,
1386
- // signingSecret: currentPassword,
1387
- // poolId: pool.id
1388
- // });
1389
- // if (isPoolMatch) { poolsToRotate.push(pool.id); }
1390
- // }
1391
- // }
1392
-
1393
- // if (poolsToRotate.length > 0) {
1394
- // console.log(`${lc} Rotating pools in delegate ${delegateInfo.delegateTjpAddr}: ${poolsToRotate.join(', ')}`);
1395
- // const {newKeystone: evolvedDelegate} = await keystoneService.changePassword({
1396
- // keystoneIbGib: delegateKeystone,
1397
- // newMasterSecret: newPassword,
1398
- // signingSecret: currentPassword,
1399
- // signingPoolId: 'manage',
1400
- // poolIds: poolsToRotate,
1401
- // metaspace,
1402
- // space
1403
- // });
1404
- // evolvedDelegates.push(evolvedDelegate);
1405
- // }
1406
- // }
1407
-
1408
- // 4. Sync evolved parent and all evolved delegate keystones atomically
2538
+
2539
+ // 3. Sync evolved parent and all evolved delegate keystones atomically
2540
+ this.showBusy({
2541
+ isBusy: true,
2542
+ title: 'Syncing to Server...',
2543
+ msg: 'Publishing rotated identity and delegates to server...',
2544
+ animationEmoji: '⚡'
2545
+ });
1409
2546
  this.setChangePasswordStatus("Syncing evolved keystones to server...", "info");
1410
- const parentTjpAddr = getIbGibAddr({ ibGib: this.ibGib! }); // is this.ibGib the tjp always? or is it the latest in the keystone line? Does it matter?
2547
+ const parentTjpAddr = (this.ibGib ? getTjpAddr({ ibGib: this.ibGib }) : undefined)
2548
+ || (this.ibGibAddr ? (() => {
2549
+ const { ib } = getIbAndGib({ ibGibAddr: this.ibGibAddr });
2550
+ const { tjpGib } = getGibInfo({ ibGibAddr: this.ibGibAddr });
2551
+ return tjpGib && ib ? `${ib}^${tjpGib}` : this.ibGibAddr;
2552
+ })() : undefined)
2553
+ || getIbGibAddr({ ibGib: this.ibGib! });
1411
2554
 
1412
2555
  const syncRes = await getApiBridge().putEvolveKeystone({
1413
2556
  domainAddr: parentTjpAddr,
@@ -1418,7 +2561,7 @@ export class KeystoneDetailsComponentInstance
1418
2561
  throw new Error(`Server synchronization rejected: ${syncRes.message}`);
1419
2562
  }
1420
2563
 
1421
- // 5. Update index locally
2564
+ // 4. Update index locally
1422
2565
  this.setChangePasswordStatus("Updating local indexes...", "info");
1423
2566
  const newKeystoneIbGibs = [toDto({ ibGib: evolvedParent })];
1424
2567
  if (evolvedDelegates) {
@@ -1445,11 +2588,16 @@ export class KeystoneDetailsComponentInstance
1445
2588
  this.elements!.inputConfirmPasswordEl.value = "";
1446
2589
 
1447
2590
  this.setChangePasswordStatus("Password updated successfully!", "success");
2591
+ this.ibGibAddr = getIbGibAddr({ ibGib: evolvedParent });
2592
+ this.staticIbGib = evolvedParent;
1448
2593
  await this.loadIbGib({ getLatest: true });
1449
2594
 
1450
2595
  } catch (error) {
2596
+ debugger; // error in handleChangePassword
1451
2597
  console.error(`${lc} Password rotation failed: ${extractErrorMsg(error)}`);
1452
2598
  this.setChangePasswordStatus(`Update failed: ${extractErrorMsg(error)}`, "error");
2599
+ } finally {
2600
+ this.showBusy({ isBusy: false });
1453
2601
  }
1454
2602
  }
1455
2603
  }