@ibgib/web-gib 0.0.61 → 0.0.62

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (59) hide show
  1. package/dist/AUTO-GENERATED-version.d.mts +1 -1
  2. package/dist/AUTO-GENERATED-version.mjs +1 -1
  3. package/dist/helpers.web.d.mts +3 -1
  4. package/dist/helpers.web.d.mts.map +1 -1
  5. package/dist/helpers.web.mjs +17 -1
  6. package/dist/helpers.web.mjs.map +1 -1
  7. package/dist/identity/device-delegate-helper.d.mts +48 -0
  8. package/dist/identity/device-delegate-helper.d.mts.map +1 -1
  9. package/dist/identity/device-delegate-helper.mjs +86 -2
  10. package/dist/identity/device-delegate-helper.mjs.map +1 -1
  11. package/dist/identity/device-delegate.respec.mjs +6 -6
  12. package/dist/identity/device-delegate.respec.mjs.map +1 -1
  13. package/dist/identity/passkey-helper.d.mts +72 -0
  14. package/dist/identity/passkey-helper.d.mts.map +1 -0
  15. package/dist/identity/passkey-helper.mjs +237 -0
  16. package/dist/identity/passkey-helper.mjs.map +1 -0
  17. package/dist/identity/passkey.respec.d.mts +2 -0
  18. package/dist/identity/passkey.respec.d.mts.map +1 -0
  19. package/dist/identity/passkey.respec.mjs +169 -0
  20. package/dist/identity/passkey.respec.mjs.map +1 -0
  21. package/dist/index.d.mts +1 -0
  22. package/dist/index.d.mts.map +1 -1
  23. package/dist/index.mjs +1 -0
  24. package/dist/index.mjs.map +1 -1
  25. package/dist/ui/component/identity/add-device/add-device.d.mts.map +1 -1
  26. package/dist/ui/component/identity/add-device/add-device.mjs +1 -0
  27. package/dist/ui/component/identity/add-device/add-device.mjs.map +1 -1
  28. package/dist/ui/component/identity/keystone-creator/keystone-creator.d.mts +18 -1
  29. package/dist/ui/component/identity/keystone-creator/keystone-creator.d.mts.map +1 -1
  30. package/dist/ui/component/identity/keystone-creator/keystone-creator.html +42 -3
  31. package/dist/ui/component/identity/keystone-creator/keystone-creator.mjs +510 -74
  32. package/dist/ui/component/identity/keystone-creator/keystone-creator.mjs.map +1 -1
  33. package/dist/ui/component/identity/keystone-details/keystone-details.d.mts +16 -1
  34. package/dist/ui/component/identity/keystone-details/keystone-details.d.mts.map +1 -1
  35. package/dist/ui/component/identity/keystone-details/keystone-details.html +38 -0
  36. package/dist/ui/component/identity/keystone-details/keystone-details.mjs +946 -122
  37. package/dist/ui/component/identity/keystone-details/keystone-details.mjs.map +1 -1
  38. package/dist/ui/component/identity/remove-device/remove-device.d.mts.map +1 -1
  39. package/dist/ui/component/identity/remove-device/remove-device.mjs +1 -0
  40. package/dist/ui/component/identity/remove-device/remove-device.mjs.map +1 -1
  41. package/dist/ui/ui-helpers.d.mts +3 -0
  42. package/dist/ui/ui-helpers.d.mts.map +1 -1
  43. package/dist/ui/ui-helpers.mjs +110 -6
  44. package/dist/ui/ui-helpers.mjs.map +1 -1
  45. package/package.json +2 -2
  46. package/src/AUTO-GENERATED-version.mts +1 -1
  47. package/src/helpers.web.mts +22 -2
  48. package/src/identity/device-delegate-helper.mts +135 -2
  49. package/src/identity/device-delegate.respec.mts +6 -6
  50. package/src/identity/passkey-helper.mts +304 -0
  51. package/src/identity/passkey.respec.mts +194 -0
  52. package/src/index.mts +1 -0
  53. package/src/ui/component/identity/add-device/add-device.mts +1 -0
  54. package/src/ui/component/identity/keystone-creator/keystone-creator.html +42 -3
  55. package/src/ui/component/identity/keystone-creator/keystone-creator.mts +551 -77
  56. package/src/ui/component/identity/keystone-details/keystone-details.html +38 -0
  57. package/src/ui/component/identity/keystone-details/keystone-details.mts +1011 -131
  58. package/src/ui/component/identity/remove-device/remove-device.mts +1 -0
  59. package/src/ui/ui-helpers.mts +118 -5
@@ -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,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
27
  import { promptForSecret } from "../../../../helpers.web.mjs";
28
- import { isSsoProviderLinked, getKeystoneCapabilities, isDelegateExpired } from "@ibgib/core-gib/dist/keystone/delegate/delegate-helpers.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,10 @@ 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
+ passkeyStatusMsgEl: HTMLDivElement;
92
103
  historicalWarningBannerEl: HTMLDivElement;
93
104
  containerEl: HTMLDivElement;
94
105
  ssoLinkCardEl: HTMLDivElement;
@@ -101,6 +112,10 @@ interface KeystoneDetailsElements extends ElementsBase {
101
112
  btnChangePasswordEl: HTMLButtonElement;
102
113
  changePasswordStatusEl: HTMLDivElement;
103
114
  changePasswordCardEl: HTMLDivElement;
115
+ sectionAddPasswordOtpEl: HTMLDivElement;
116
+ inputAddPasswordOtpEl: HTMLInputElement;
117
+ btnResendAddPasswordOtpEl: HTMLButtonElement;
118
+ descAddPasswordOtpEl: HTMLParagraphElement;
104
119
  }
105
120
 
106
121
  export class KeystoneDetailsComponentInstance
@@ -120,6 +135,7 @@ export class KeystoneDetailsComponentInstance
120
135
  private _onIdentityChanged: ((e: any) => void) | null = null;
121
136
  private keystoneCache: Map<string, { username: string, description: string, isPrimary: boolean, n: number }> = new Map();
122
137
  private aggregatedDetails?: any;
138
+ private resendCooldownInterval: any = null;
123
139
 
124
140
  constructor() {
125
141
  super();
@@ -142,6 +158,39 @@ export class KeystoneDetailsComponentInstance
142
158
  }
143
159
  }
144
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
+
145
194
  private async getKeystoneDetails(addr: string): Promise<{ username: string, description: string, isPrimary: boolean, n: number }> {
146
195
  const cached = this.keystoneCache.get(addr);
147
196
  if (cached) return cached;
@@ -208,6 +257,10 @@ export class KeystoneDetailsComponentInstance
208
257
  this.elements.rawKeystoneJsonEl = shadow.getElementById('raw-keystone-json') as HTMLPreElement;
209
258
  this.elements.delegatesCardEl = shadow.getElementById('delegates-card') as HTMLDivElement;
210
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;
211
264
  this.elements.containerEl = shadow.getElementById('container') as HTMLDivElement;
212
265
  this.elements.historicalWarningBannerEl = shadow.getElementById('historical-warning-banner') as HTMLDivElement;
213
266
  this.elements.ssoLinkCardEl = shadow.getElementById('sso-link-card') as HTMLDivElement;
@@ -220,6 +273,10 @@ export class KeystoneDetailsComponentInstance
220
273
  this.elements.btnChangePasswordEl = shadow.getElementById('btn-change-password') as HTMLButtonElement;
221
274
  this.elements.changePasswordStatusEl = shadow.getElementById('change-password-status') as HTMLDivElement;
222
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;
223
280
 
224
281
  // ElementBase requires contentEl
225
282
  this.elements.contentEl = shadow.getElementById('identity-details-view') as HTMLDivElement;
@@ -245,12 +302,6 @@ export class KeystoneDetailsComponentInstance
245
302
  }
246
303
  }
247
304
 
248
- override async disconnected(): Promise<void> {
249
- if (this._onIdentityChanged) {
250
- window.removeEventListener(EVENT_IBGIB_IDENTITY_CHANGED, this._onIdentityChanged);
251
- }
252
- }
253
-
254
305
  private initHandlers() {
255
306
  // Setup raw JSON accordion toggle
256
307
  this.elements!.rawAccordionHeaderEl.addEventListener('click', () => this.toggleAccordion());
@@ -260,9 +311,11 @@ export class KeystoneDetailsComponentInstance
260
311
  this.elements!.btnSetActiveEl.addEventListener('click', () => this.handleSetActiveIdentity());
261
312
  this.elements!.btnCheckUpdatesEl.addEventListener('click', () => this.handleCheckForUpdates());
262
313
  this.elements!.btnUpdateIdentityEl.addEventListener('click', () => this.handleUpdateIdentity());
314
+ this.elements!.btnAddPasskeyEl?.addEventListener('click', () => this.handleAddPasskey());
263
315
  this.elements!.btnLinkGoogleEl.addEventListener('click', () => this.handleSSOClick('google'));
264
316
  this.elements!.btnLinkGithubEl.addEventListener('click', () => this.handleSSOClick('github'));
265
317
  this.elements!.btnChangePasswordEl.addEventListener('click', () => this.handleChangePassword());
318
+ this.elements!.btnResendAddPasswordOtpEl?.addEventListener('click', () => this.handleResendAddPasswordOtp());
266
319
  }
267
320
 
268
321
  private toggleAccordion() {
@@ -458,15 +511,22 @@ export class KeystoneDetailsComponentInstance
458
511
 
459
512
  // Check if origin provider or linked
460
513
  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' });
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;
463
520
 
464
- const caps = getKeystoneCapabilities({ keystone: this.ibGib, aggregatedDetails: this.aggregatedDetails });
521
+ const caps = !!this.ibGib ?
522
+ getKeystoneCapabilities({ keystone: this.ibGib, aggregatedDetails: this.aggregatedDetails }) :
523
+ undefined;
465
524
 
466
525
  // Google Card States
467
526
  const googleCard = this.elements.btnLinkGoogleEl;
468
527
  const googleLabel = googleCard.querySelector('.toggle-state-text')!;
469
528
  if (isGoogleLinked) {
529
+ if (!caps) { throw new Error(`(UNEXPECTED) isGoogleLinked but capabilities falsy? (E: cc62e8357b66860dd2cb8148c833b726)`); }
470
530
  googleCard.classList.remove('unlinked');
471
531
  googleCard.classList.add('linked');
472
532
  const isOrigin = originProvider === 'google';
@@ -491,6 +551,7 @@ export class KeystoneDetailsComponentInstance
491
551
  const githubCard = this.elements.btnLinkGithubEl;
492
552
  const githubLabel = githubCard.querySelector('.toggle-state-text')!;
493
553
  if (isGithubLinked) {
554
+ if (!caps) { throw new Error(`(UNEXPECTED) isGithubLinked but capabilities falsy? (E: e94688210538e0e82878088873a46826)`); }
494
555
  githubCard.classList.remove('unlinked');
495
556
  githubCard.classList.add('linked');
496
557
  const isOrigin = originProvider === 'github';
@@ -514,13 +575,17 @@ export class KeystoneDetailsComponentInstance
514
575
  }
515
576
 
516
577
  private updatePasswordCardView(): void {
517
- const caps = getKeystoneCapabilities({ keystone: this.ibGib, aggregatedDetails: this.aggregatedDetails });
578
+ const caps = !!this.ibGib ?
579
+ getKeystoneCapabilities({ keystone: this.ibGib, aggregatedDetails: this.aggregatedDetails }) :
580
+ false;
518
581
  const cardTitleEl = this.elements?.changePasswordCardEl?.querySelector('h3');
519
582
  const cardDescEl = this.elements?.changePasswordCardEl?.querySelector('.description');
520
583
  const currentPasswordGroup = this.elements?.inputCurrentPasswordEl?.closest('.form-group') as HTMLElement | null;
521
584
  const btnChangePassword = this.elements?.btnChangePasswordEl;
522
585
 
523
- const isCustodialOnly = caps.isCustodial && !caps.isSovereign;
586
+ const isCustodialOnly = !!caps ?
587
+ caps.isCustodial && !caps.isSovereign :
588
+ false;
524
589
  if (isCustodialOnly) {
525
590
  if (cardTitleEl) cardTitleEl.textContent = "Add Master Password";
526
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.";
@@ -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[]) {
@@ -1009,8 +1093,9 @@ export class KeystoneDetailsComponentInstance
1009
1093
  let masterSecret: string;
1010
1094
  try {
1011
1095
  masterSecret = await promptForSecret({
1012
- msg: `Enter Master Password to revoke delegate '${deviceName}':`,
1013
- confirm: false
1096
+ msg: `Enter Master Password or touch security key to revoke delegate '${deviceName}':`,
1097
+ confirm: false,
1098
+ passkeys: this.aggregatedDetails?.passkeys,
1014
1099
  });
1015
1100
  } catch (err) {
1016
1101
  // User cancelled prompt
@@ -1027,11 +1112,35 @@ export class KeystoneDetailsComponentInstance
1027
1112
 
1028
1113
  const keystoneSvc = new KeystoneService_V1();
1029
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
+
1030
1138
  // Unregister delegate from parent keystone
1031
1139
  const evolvedParent = await keystoneSvc.unregisterDelegate({
1032
1140
  parentKeystone: this.ibGib!,
1033
1141
  delegateTjpAddr,
1034
1142
  masterSecret,
1143
+ signingPoolId: matchedPoolId,
1035
1144
  metaspace,
1036
1145
  space
1037
1146
  });
@@ -1085,6 +1194,477 @@ export class KeystoneDetailsComponentInstance
1085
1194
  }
1086
1195
  }
1087
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
+
1088
1668
  override async handleContextUpdated(): Promise<void> {
1089
1669
  const lc = `${this.lc}[${this.handleContextUpdated.name}]`;
1090
1670
  try {
@@ -1124,10 +1704,26 @@ export class KeystoneDetailsComponentInstance
1124
1704
  const lc = `${this.lc}[${this.handleSSOLink.name}]`;
1125
1705
  this.setSSOStatus(`Linking ${providerId}...`, "info");
1126
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
+
1127
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
+
1128
1724
  const resPopup = await executeSsoOAuthPopup({
1129
1725
  providerId,
1130
- parentTjpAddr: this.ibGibAddr
1726
+ parentTjpAddr: primaryTjpAddr
1131
1727
  });
1132
1728
 
1133
1729
  if (!resPopup.success || !resPopup.code) {
@@ -1139,6 +1735,8 @@ export class KeystoneDetailsComponentInstance
1139
1735
  } catch (error) {
1140
1736
  console.error(`${lc} SSO link initiation failed: ${extractErrorMsg(error)}`);
1141
1737
  this.setSSOStatus(`Link failed: ${extractErrorMsg(error)}`, "error");
1738
+ } finally {
1739
+ this.showBusy({ isBusy: false });
1142
1740
  }
1143
1741
  }
1144
1742
 
@@ -1151,7 +1749,14 @@ export class KeystoneDetailsComponentInstance
1151
1749
 
1152
1750
  protected async handleSSOLinkCallback(code: string, stateObj: any, redirectUri: string) {
1153
1751
  const lc = `${this.lc}[${this.handleSSOLinkCallback.name}]`;
1154
- 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
+ });
1155
1760
 
1156
1761
  try {
1157
1762
  // 1. Exchange OAuth code for custodian public pool config
@@ -1179,6 +1784,7 @@ export class KeystoneDetailsComponentInstance
1179
1784
  const custodianPool = linkData.custodianPool;
1180
1785
 
1181
1786
  // 2. Prompt user for master secret and verify it
1787
+ this.showBusy({ isBusy: false });
1182
1788
  const keystoneService = new KeystoneService_V1();
1183
1789
  let masterSecret: string | undefined;
1184
1790
  let isCorrect = false;
@@ -1222,6 +1828,12 @@ export class KeystoneDetailsComponentInstance
1222
1828
  return;
1223
1829
  }
1224
1830
 
1831
+ this.showBusy({
1832
+ isBusy: true,
1833
+ title: 'Evolving Identity...',
1834
+ msg: `Installing ${providerName} custodian pool and signing evolution...`,
1835
+ animationEmoji: '📝'
1836
+ });
1225
1837
  this.setSSOStatus("Evolving identity locally and generating cryptographic signatures...", "info");
1226
1838
 
1227
1839
  const metaspace = await getGlobalMetaspace_waitIfNeeded();
@@ -1229,7 +1841,7 @@ export class KeystoneDetailsComponentInstance
1229
1841
  const space = await metaspace.getLocalUserSpace({ lock: false });
1230
1842
  if (!space) { throw new Error("Local space not found"); }
1231
1843
 
1232
- // 3. Evolve parent keystone to include the custodian pool
1844
+ // 3. Evolve parent keystone to include the custodian pool (n -> n+1)
1233
1845
  const evolvedParent = await keystoneService.addPools({
1234
1846
  latestKeystone: this.ibGib!,
1235
1847
  masterSecret: masterSecret!,
@@ -1238,24 +1850,64 @@ export class KeystoneDetailsComponentInstance
1238
1850
  space
1239
1851
  });
1240
1852
 
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({
1853
+ // Sync evolved parent (n+1) to server first
1854
+ const syncRes1 = await getApiBridge().putEvolveKeystone({
1244
1855
  domainAddr: stateObj.parentTjpAddr,
1245
1856
  keystoneIbGib: evolvedParent,
1246
- relatedKeystoneIbGibs: [] // No outward delegate timelines
1857
+ relatedKeystoneIbGibs: []
1247
1858
  });
1248
- if (!syncRes.success) {
1249
- throw new Error(`Server synchronization rejected: ${syncRes.message}`);
1859
+ if (!syncRes1.success) {
1860
+ throw new Error(`Server synchronization of linked pool rejected: ${syncRes1.message}`);
1250
1861
  }
1251
1862
 
1252
- // 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
1253
1905
  await updateSpecialIndex({
1254
1906
  type: "keystones",
1255
1907
  rel8nInfos: [
1256
1908
  {
1257
1909
  rel8nName: "keystone",
1258
- ibGibs: [toDto({ ibGib: evolvedParent })]
1910
+ ibGibs: [toDto({ ibGib: parentWithDelegate }), toDto({ ibGib: delegateKeystone })]
1259
1911
  }
1260
1912
  ],
1261
1913
  metaspace,
@@ -1263,18 +1915,24 @@ export class KeystoneDetailsComponentInstance
1263
1915
  });
1264
1916
 
1265
1917
  this.setSSOStatus(`Successfully linked with ${stateObj.providerId}!`, "success");
1918
+ this.ibGibAddr = getIbGibAddr({ ibGib: parentWithDelegate });
1919
+ this.staticIbGib = parentWithDelegate;
1266
1920
  await this.loadIbGib({ getLatest: false });
1267
1921
 
1268
1922
  } catch (error) {
1269
1923
  console.error(`${lc} SSO callback processing failed: ${extractErrorMsg(error)}`);
1270
1924
  this.setSSOStatus(`Link failed: ${extractErrorMsg(error)}`, "error");
1925
+ } finally {
1926
+ this.showBusy({ isBusy: false });
1271
1927
  }
1272
1928
  }
1273
1929
 
1274
1930
  protected async handleSSOClick(providerId: 'google' | 'github') {
1275
- const isLinked = isSsoProviderLinked({ keystone: this.ibGib, aggregatedDetails: this.aggregatedDetails, providerId });
1931
+ const isLinked = !!this.ibGib ?
1932
+ isSsoProviderLinked({ keystone: this.ibGib, aggregatedDetails: this.aggregatedDetails, providerId }) :
1933
+ false;
1276
1934
  if (isLinked) {
1277
- const caps = getKeystoneCapabilities({ keystone: this.ibGib, aggregatedDetails: this.aggregatedDetails });
1935
+ const caps = getKeystoneCapabilities({ keystone: this.ibGib!, aggregatedDetails: this.aggregatedDetails });
1278
1936
  if (!caps.isSovereign) {
1279
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");
1280
1938
  return;
@@ -1287,7 +1945,8 @@ export class KeystoneDetailsComponentInstance
1287
1945
 
1288
1946
  protected async handleSSOUnlink(providerId: 'google' | 'github') {
1289
1947
  const lc = `${this.lc}[${this.handleSSOUnlink.name}]`;
1290
- this.setSSOStatus(`Unlinking ${providerId}...`, "info");
1948
+ const providerName = providerId === 'google' ? 'Google' : providerId === 'github' ? 'GitHub' : providerId;
1949
+ this.setSSOStatus(`Unlinking ${providerName}...`, "info");
1291
1950
 
1292
1951
  try {
1293
1952
  // 1. Prompt user for password
@@ -1300,11 +1959,12 @@ export class KeystoneDetailsComponentInstance
1300
1959
  while (tries < maxTries && !isCorrect) {
1301
1960
  try {
1302
1961
  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.`
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.`
1304
1963
  : `Incorrect password. Please try again (${maxTries - tries} tries remaining):`;
1305
1964
  masterSecret = await promptForSecret({
1306
1965
  msg: promptMsg,
1307
- confirm: false
1966
+ confirm: false,
1967
+ passkeys: this.aggregatedDetails?.passkeys,
1308
1968
  });
1309
1969
  } catch (error) {
1310
1970
  masterSecret = undefined;
@@ -1315,11 +1975,10 @@ export class KeystoneDetailsComponentInstance
1315
1975
  break;
1316
1976
  }
1317
1977
 
1318
- // Verify secret using verifySigningSecret (requires manage poolId)
1978
+ // Verify secret using verifySigningSecret
1319
1979
  isCorrect = await keystoneService.verifySigningSecret({
1320
1980
  keystoneIbGib: this.ibGib!,
1321
- signingSecret: masterSecret,
1322
- poolId: 'manage'
1981
+ signingSecret: masterSecret
1323
1982
  });
1324
1983
 
1325
1984
  if (!isCorrect) {
@@ -1332,6 +1991,12 @@ export class KeystoneDetailsComponentInstance
1332
1991
  return;
1333
1992
  }
1334
1993
 
1994
+ this.showBusy({
1995
+ isBusy: true,
1996
+ title: 'Unlinking Provider...',
1997
+ msg: `Removing ${providerName} custodian pool and delegate keys...`,
1998
+ animationEmoji: '🔓'
1999
+ });
1335
2000
  this.setSSOStatus("Evolving identity locally and removing custodian pool...", "info");
1336
2001
 
1337
2002
  const metaspace = await getGlobalMetaspace_waitIfNeeded();
@@ -1349,33 +2014,63 @@ export class KeystoneDetailsComponentInstance
1349
2014
  space
1350
2015
  });
1351
2016
 
1352
- // Unregister any corresponding outward delegate for this provider if present
1353
- const delegatesMap = this.ibGib?.data?.delegates;
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
+ });
2032
+ this.setSSOStatus("Publishing evolved identity keystone to server...", "info");
2033
+ let syncRes = await getApiBridge().putEvolveKeystone({
2034
+ domainAddr: primaryTjpAddr,
2035
+ keystoneIbGib: evolvedParent,
2036
+ relatedKeystoneIbGibs: [] // No outward delegate timelines
2037
+ });
2038
+ if (!syncRes.success) {
2039
+ throw new Error(`Server synchronization rejected: ${syncRes.message}`);
2040
+ }
2041
+
2042
+ // 4. Unregister any corresponding outward delegate for this provider if present
2043
+ const delegatesMap = evolvedParent.data?.delegates || this.ibGib?.data?.delegates;
1354
2044
  if (delegatesMap) {
1355
2045
  for (const [delegateTjpAddr, info] of Object.entries(delegatesMap)) {
1356
- if (info.deviceName?.includes(providerId)) {
2046
+ if (isCustodialDelegate({ delegateInfo: info, providerId })) {
1357
2047
  evolvedParent = await keystoneService.unregisterDelegate({
1358
2048
  parentKeystone: evolvedParent,
1359
2049
  delegateTjpAddr,
1360
2050
  masterSecret: masterSecret!,
2051
+ signingPoolId: 'manage',
1361
2052
  metaspace,
1362
2053
  space,
1363
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
+ }
1364
2070
  }
1365
2071
  }
1366
2072
  }
1367
2073
 
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
2074
  // 4. Update index locally
1380
2075
  await updateSpecialIndex({
1381
2076
  type: "keystones",
@@ -1389,15 +2084,21 @@ export class KeystoneDetailsComponentInstance
1389
2084
  space
1390
2085
  });
1391
2086
 
1392
- this.setSSOStatus(`Successfully unlinked ${providerId}!`, "success");
1393
- 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 });
1394
2091
 
1395
2092
  } catch (error) {
1396
- console.error(`${lc} SSO unlink failed: ${extractErrorMsg(error)}`);
1397
- 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 });
1398
2097
  }
1399
2098
  }
1400
2099
 
2100
+
2101
+
1401
2102
  private setChangePasswordStatus(msg: string, type: 'info' | 'success' | 'error') {
1402
2103
  const el = this.elements!.changePasswordStatusEl;
1403
2104
  el.textContent = msg;
@@ -1445,8 +2146,17 @@ export class KeystoneDetailsComponentInstance
1445
2146
  challenges: newChallenges,
1446
2147
  };
1447
2148
 
1448
- const primaryTjpAddr = getTjpAddr({ ibGib: this.ibGib! }) || getIbGibAddr({ ibGib: this.ibGib! });
1449
- const custodianProvider = this.aggregatedDetails?.custodianProvider || 'google';
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;
1450
2160
 
1451
2161
  let requestBody: any = {
1452
2162
  primaryTjpAddr,
@@ -1476,17 +2186,69 @@ export class KeystoneDetailsComponentInstance
1476
2186
  requestBody.code = resPopup.code;
1477
2187
  requestBody.redirectUri = resPopup.redirectUri;
1478
2188
  } 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");
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");
1488
2197
  return;
1489
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
+
1490
2252
  requestBody.email = email;
1491
2253
  requestBody.code = otpCode;
1492
2254
  }
@@ -1517,21 +2279,97 @@ export class KeystoneDetailsComponentInstance
1517
2279
  const space = await metaspace.getLocalUserSpace({ lock: false });
1518
2280
  if (!space) { throw new Error("Local user space not available"); }
1519
2281
 
1520
- await metaspace.put({ ibGibs: [evolvedParent], space });
1521
- await metaspace.registerNewIbGib({ ibGib: evolvedParent, space });
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
+
1522
2360
  await updateSpecialIndex({
1523
2361
  type: "keystones",
1524
2362
  rel8nInfos: [
1525
2363
  {
1526
2364
  rel8nName: "keystone",
1527
- ibGibs: [toDto({ ibGib: evolvedParent })]
2365
+ ibGibs: [toDto({ ibGib: parentWithUserDelegate }), toDto({ ibGib: userSyncDelegate })]
1528
2366
  }
1529
2367
  ],
1530
2368
  metaspace,
1531
2369
  space
1532
2370
  });
1533
2371
 
1534
- const evolvedAddr = getIbGibAddr({ ibGib: evolvedParent });
2372
+ const evolvedAddr = getIbGibAddr({ ibGib: parentWithUserDelegate });
1535
2373
 
1536
2374
  // Dispatch EVENT_IBGIB_IDENTITY_CHANGED
1537
2375
  window.dispatchEvent(new CustomEvent(EVENT_IBGIB_IDENTITY_CHANGED, {
@@ -1542,9 +2380,20 @@ export class KeystoneDetailsComponentInstance
1542
2380
 
1543
2381
  this.setChangePasswordStatus("Successfully added Master Password! Identity is now sovereign.", "success");
1544
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
+
1545
2394
  // Refresh the view
1546
2395
  this.ibGibAddr = evolvedAddr;
1547
- this.staticIbGib = evolvedParent;
2396
+ this.staticIbGib = parentWithUserDelegate;
1548
2397
  await this.loadIbGib({ getLatest: true });
1549
2398
 
1550
2399
  // Clear inputs
@@ -1554,22 +2403,83 @@ export class KeystoneDetailsComponentInstance
1554
2403
  } catch (error) {
1555
2404
  console.error(`${lc} Elevation failed: ${extractErrorMsg(error)}`);
1556
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
+ }
1557
2410
  } finally {
1558
2411
  this.showBusy({ isBusy: false });
1559
2412
  }
1560
2413
  }
1561
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
+
1562
2463
  protected async handleChangePassword(): Promise<void> {
1563
2464
  const lc = `${this.lc}[${this.handleChangePassword.name}]`;
1564
2465
 
1565
- const caps = getKeystoneCapabilities({ keystone: this.ibGib, aggregatedDetails: this.aggregatedDetails });
1566
- const isCustodialOnly = caps.isCustodial && !caps.isSovereign;
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;
1567
2471
  if (isCustodialOnly) {
1568
2472
  await this.handleAddMasterPassword();
1569
2473
  return;
1570
2474
  }
1571
2475
 
1572
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
+ });
1573
2483
 
1574
2484
  try {
1575
2485
  const currentPassword = this.elements!.inputCurrentPasswordEl.value;
@@ -1602,6 +2512,12 @@ export class KeystoneDetailsComponentInstance
1602
2512
  return;
1603
2513
  }
1604
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
+ });
1605
2521
  this.setChangePasswordStatus("Evolving primary identity keystone...", "info");
1606
2522
 
1607
2523
  const metaspace = await getGlobalMetaspace_waitIfNeeded();
@@ -1609,8 +2525,7 @@ export class KeystoneDetailsComponentInstance
1609
2525
  const space = await metaspace.getLocalUserSpace({ lock: false });
1610
2526
  if (!space) { throw new Error(`(UNEXPECTED) couldn't get default local space? (E: 2fc1281c28351fd378a1d7f88eaa0526)`); }
1611
2527
 
1612
- // 2. Rotate parent identity
1613
- 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
1614
2529
  const { newKeystone: evolvedParent, newDelegates: evolvedDelegates } =
1615
2530
  await keystoneService.changePassword({
1616
2531
  keystoneIbGib: this.ibGib!,
@@ -1620,62 +2535,22 @@ export class KeystoneDetailsComponentInstance
1620
2535
  metaspace,
1621
2536
  space
1622
2537
  });
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
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
+ });
1677
2546
  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?
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! });
1679
2554
 
1680
2555
  const syncRes = await getApiBridge().putEvolveKeystone({
1681
2556
  domainAddr: parentTjpAddr,
@@ -1686,7 +2561,7 @@ export class KeystoneDetailsComponentInstance
1686
2561
  throw new Error(`Server synchronization rejected: ${syncRes.message}`);
1687
2562
  }
1688
2563
 
1689
- // 5. Update index locally
2564
+ // 4. Update index locally
1690
2565
  this.setChangePasswordStatus("Updating local indexes...", "info");
1691
2566
  const newKeystoneIbGibs = [toDto({ ibGib: evolvedParent })];
1692
2567
  if (evolvedDelegates) {
@@ -1713,11 +2588,16 @@ export class KeystoneDetailsComponentInstance
1713
2588
  this.elements!.inputConfirmPasswordEl.value = "";
1714
2589
 
1715
2590
  this.setChangePasswordStatus("Password updated successfully!", "success");
2591
+ this.ibGibAddr = getIbGibAddr({ ibGib: evolvedParent });
2592
+ this.staticIbGib = evolvedParent;
1716
2593
  await this.loadIbGib({ getLatest: true });
1717
2594
 
1718
2595
  } catch (error) {
2596
+ debugger; // error in handleChangePassword
1719
2597
  console.error(`${lc} Password rotation failed: ${extractErrorMsg(error)}`);
1720
2598
  this.setChangePasswordStatus(`Update failed: ${extractErrorMsg(error)}`, "error");
2599
+ } finally {
2600
+ this.showBusy({ isBusy: false });
1721
2601
  }
1722
2602
  }
1723
2603
  }