@ibgib/web-gib 0.0.61 → 0.0.63

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (66) hide show
  1. package/dist/AUTO-GENERATED-version.d.mts +1 -1
  2. package/dist/AUTO-GENERATED-version.mjs +1 -1
  3. package/dist/helpers.web.d.mts +20 -3
  4. package/dist/helpers.web.d.mts.map +1 -1
  5. package/dist/helpers.web.mjs +84 -3
  6. package/dist/helpers.web.mjs.map +1 -1
  7. package/dist/identity/device-delegate-helper.d.mts +48 -0
  8. package/dist/identity/device-delegate-helper.d.mts.map +1 -1
  9. package/dist/identity/device-delegate-helper.mjs +86 -2
  10. package/dist/identity/device-delegate-helper.mjs.map +1 -1
  11. package/dist/identity/device-delegate.respec.mjs +6 -6
  12. package/dist/identity/device-delegate.respec.mjs.map +1 -1
  13. package/dist/identity/passkey-helper.d.mts +77 -0
  14. package/dist/identity/passkey-helper.d.mts.map +1 -0
  15. package/dist/identity/passkey-helper.mjs +258 -0
  16. package/dist/identity/passkey-helper.mjs.map +1 -0
  17. package/dist/identity/passkey.respec.d.mts +2 -0
  18. package/dist/identity/passkey.respec.d.mts.map +1 -0
  19. package/dist/identity/passkey.respec.mjs +169 -0
  20. package/dist/identity/passkey.respec.mjs.map +1 -0
  21. package/dist/index.d.mts +1 -0
  22. package/dist/index.d.mts.map +1 -1
  23. package/dist/index.mjs +1 -0
  24. package/dist/index.mjs.map +1 -1
  25. package/dist/ui/component/identity/add-device/add-device.d.mts.map +1 -1
  26. package/dist/ui/component/identity/add-device/add-device.mjs +1 -0
  27. package/dist/ui/component/identity/add-device/add-device.mjs.map +1 -1
  28. package/dist/ui/component/identity/keystone-creator/keystone-creator.css +26 -0
  29. package/dist/ui/component/identity/keystone-creator/keystone-creator.d.mts +18 -1
  30. package/dist/ui/component/identity/keystone-creator/keystone-creator.d.mts.map +1 -1
  31. package/dist/ui/component/identity/keystone-creator/keystone-creator.html +60 -4
  32. package/dist/ui/component/identity/keystone-creator/keystone-creator.mjs +518 -74
  33. package/dist/ui/component/identity/keystone-creator/keystone-creator.mjs.map +1 -1
  34. package/dist/ui/component/identity/keystone-details/keystone-details.d.mts +28 -1
  35. package/dist/ui/component/identity/keystone-details/keystone-details.d.mts.map +1 -1
  36. package/dist/ui/component/identity/keystone-details/keystone-details.html +46 -0
  37. package/dist/ui/component/identity/keystone-details/keystone-details.mjs +1146 -208
  38. package/dist/ui/component/identity/keystone-details/keystone-details.mjs.map +1 -1
  39. package/dist/ui/component/identity/remove-device/remove-device.d.mts.map +1 -1
  40. package/dist/ui/component/identity/remove-device/remove-device.mjs +1 -0
  41. package/dist/ui/component/identity/remove-device/remove-device.mjs.map +1 -1
  42. package/dist/ui/ui-helpers.d.mts +3 -0
  43. package/dist/ui/ui-helpers.d.mts.map +1 -1
  44. package/dist/ui/ui-helpers.mjs +116 -7
  45. package/dist/ui/ui-helpers.mjs.map +1 -1
  46. package/package.json +2 -2
  47. package/src/AUTO-GENERATED-version.mts +1 -1
  48. package/src/helpers.web.mts +117 -7
  49. package/src/identity/device-delegate-helper.mts +135 -2
  50. package/src/identity/device-delegate.respec.mts +6 -6
  51. package/src/identity/passkey-helper.mts +337 -0
  52. package/src/identity/passkey.respec.mts +194 -0
  53. package/src/index.mts +1 -0
  54. package/src/ui/component/identity/add-device/add-device.mts +1 -0
  55. package/src/ui/component/identity/keystone-creator/keystone-creator.css +26 -0
  56. package/src/ui/component/identity/keystone-creator/keystone-creator.html +60 -4
  57. package/src/ui/component/identity/keystone-creator/keystone-creator.mts +561 -77
  58. package/src/ui/component/identity/keystone-details/keystone-details.html +46 -0
  59. package/src/ui/component/identity/keystone-details/keystone-details.mts +1239 -224
  60. package/src/ui/component/identity/remove-device/remove-device.mts +1 -0
  61. package/src/ui/ui-helpers.mts +123 -6
  62. package/tools/auto-generated-agent-skills/skills/ibgib-create-app-server/SKILL.md +50 -0
  63. package/src/identity/custodian-delegate-helper.mts.bak +0 -7
  64. package/src/identity/sso/sso-config-helper.mts.bak +0 -52
  65. package/src/identity/sso/sso-custodian-service.mts.bak +0 -566
  66. package/src/identity/sso/sso-custodian-service.respec.mts.bak +0 -298
@@ -1,7 +1,7 @@
1
1
  import thisCss from "./keystone-details.css";
2
2
  import thisHtml from "./keystone-details.html";
3
3
  import commonComponentStylesCss from "../../common-component-styles.css";
4
- import { extractErrorMsg, pretty } from "@ibgib/helper-gib/dist/helpers/utils-helper.mjs";
4
+ import { extractErrorMsg, pretty, getSaferSubstring, HashAlgorithm, delay } from "@ibgib/helper-gib/dist/helpers/utils-helper.mjs";
5
5
  import { getIbAndGib, getIbGibAddr } from "@ibgib/ts-gib/dist/helper.mjs";
6
6
  import { getGibInfo } from "@ibgib/ts-gib/dist/V1/transforms/transform-helper.mjs";
7
7
  import { KeystoneService_V1 } from "@ibgib/core-gib/dist/keystone/keystone-service-v1.mjs";
@@ -10,15 +10,19 @@ import { EVENT_IBGIB_IDENTITY_REQUEST_CHANGE, EVENT_IBGIB_IDENTITY_CHANGED } fro
10
10
  import { getApiBridge } from "../../../../api/ibgib-api-bridge.mjs";
11
11
  import { getKeystoneSyncService } from "../../../../identity/keystone-sync-service.mjs";
12
12
  import { updateSpecialIndex } from "@ibgib/core-gib/dist/timeline/timeline-api.mjs";
13
- import { getTjpAddr, toDto } from "@ibgib/core-gib/dist/common/other/ibgib-helper.mjs";
13
+ import { getTjpAddr, toDto, getTimelinesGroupedByTjp } from "@ibgib/core-gib/dist/common/other/ibgib-helper.mjs";
14
14
  import { parseSpaceIb } from "@ibgib/core-gib/dist/witness/space/space-helper.mjs";
15
15
  import { executeSsoOAuthPopup } from "../../../../identity/sso-popup-helper.mjs";
16
16
  import { getComponentCtorArg } from "../../../../app-bootstrap/init-orchestration.mjs";
17
- import { promptForSecret } from "../../../../helpers.web.mjs";
18
- import { isSsoProviderLinked, getKeystoneCapabilities, isDelegateExpired } from "@ibgib/core-gib/dist/keystone/delegate/delegate-helpers.mjs";
17
+ import { promptForAuthoritySecret } from "../../../../helpers.web.mjs";
18
+ import { isSsoProviderLinked, getKeystoneCapabilities, isDelegateExpired, isCustodialDelegate } from "@ibgib/core-gib/dist/keystone/delegate/delegate-helpers.mjs";
19
19
  import { generatePoolChallenges } from "@ibgib/core-gib/dist/keystone/keystone-helpers.mjs";
20
- import { POOL_ID_MANAGE } from "@ibgib/core-gib/dist/keystone/keystone-constants.mjs";
20
+ import { POOL_ID_MANAGE, KEYSTONE_VERB_SYNC, KEYSTONE_VERB_CONNECT, KEYSTONE_VERB_MANAGE } from "@ibgib/core-gib/dist/keystone/keystone-constants.mjs";
21
+ import { generateDefaultDeviceTag } from "../../../../util/device-helper.mjs";
21
22
  import { getGlobalMetaspace_waitIfNeeded } from "../../../../helpers.mjs";
23
+ import { createSovereignDeviceSyncDelegate, createOAuth2DeviceSyncDelegate } from "../../../../identity/device-delegate-helper.mjs";
24
+ import { isPasskeyPrfSupported, registerPasskeyCredential, evaluatePasskeyPrf, createPasskeyDeviceSyncDelegate, bufferToHex } from "../../../../identity/passkey-helper.mjs";
25
+ import { KeystoneChallengeType, KeystoneReplenishStrategy } from "@ibgib/core-gib/dist/keystone/keystone-types.mjs";
22
26
  const logalot = false;
23
27
  export const KEYSTONE_DETAILS_COMPONENT_NAME = 'ibgib-keystone-details';
24
28
  export class KeystoneDetailsComponentMeta extends IbGibDynamicComponentMetaBase {
@@ -54,6 +58,7 @@ export class KeystoneDetailsComponentInstance extends IbGibDynamicComponentInsta
54
58
  _onIdentityChanged = null;
55
59
  keystoneCache = new Map();
56
60
  aggregatedDetails;
61
+ resendCooldownInterval = null;
57
62
  constructor() {
58
63
  super();
59
64
  }
@@ -77,6 +82,39 @@ export class KeystoneDetailsComponentInstance extends IbGibDynamicComponentInsta
77
82
  }
78
83
  }
79
84
  }
85
+ async disconnected() {
86
+ if (this._onIdentityChanged) {
87
+ window.removeEventListener(EVENT_IBGIB_IDENTITY_CHANGED, this._onIdentityChanged);
88
+ }
89
+ if (this.resendCooldownInterval) {
90
+ clearInterval(this.resendCooldownInterval);
91
+ this.resendCooldownInterval = null;
92
+ }
93
+ }
94
+ startResendCooldownTimer(durationSecs = 30) {
95
+ const btn = this.elements?.btnResendAddPasswordOtpEl;
96
+ if (!btn)
97
+ return;
98
+ if (this.resendCooldownInterval) {
99
+ clearInterval(this.resendCooldownInterval);
100
+ this.resendCooldownInterval = null;
101
+ }
102
+ let remaining = durationSecs;
103
+ btn.disabled = true;
104
+ btn.textContent = `Resend Code (${remaining}s)`;
105
+ this.resendCooldownInterval = setInterval(() => {
106
+ remaining -= 1;
107
+ if (remaining <= 0) {
108
+ clearInterval(this.resendCooldownInterval);
109
+ this.resendCooldownInterval = null;
110
+ btn.disabled = false;
111
+ btn.textContent = "Resend Code";
112
+ }
113
+ else {
114
+ btn.textContent = `Resend Code (${remaining}s)`;
115
+ }
116
+ }, 1000);
117
+ }
80
118
  async getKeystoneDetails(addr) {
81
119
  const cached = this.keystoneCache.get(addr);
82
120
  if (cached)
@@ -142,6 +180,11 @@ export class KeystoneDetailsComponentInstance extends IbGibDynamicComponentInsta
142
180
  this.elements.rawKeystoneJsonEl = shadow.getElementById('raw-keystone-json');
143
181
  this.elements.delegatesCardEl = shadow.getElementById('delegates-card');
144
182
  this.elements.delegatesListEl = shadow.getElementById('delegates-list');
183
+ this.elements.passkeysCardEl = shadow.getElementById('passkeys-card');
184
+ this.elements.passkeysListEl = shadow.getElementById('passkeys-list');
185
+ this.elements.btnAddPasskeyEl = shadow.getElementById('btn-add-passkey');
186
+ this.elements.selectPasskeyAttachmentEl = shadow.getElementById('select-passkey-attachment');
187
+ this.elements.passkeyStatusMsgEl = shadow.getElementById('passkey-status-msg');
145
188
  this.elements.containerEl = shadow.getElementById('container');
146
189
  this.elements.historicalWarningBannerEl = shadow.getElementById('historical-warning-banner');
147
190
  this.elements.ssoLinkCardEl = shadow.getElementById('sso-link-card');
@@ -154,6 +197,10 @@ export class KeystoneDetailsComponentInstance extends IbGibDynamicComponentInsta
154
197
  this.elements.btnChangePasswordEl = shadow.getElementById('btn-change-password');
155
198
  this.elements.changePasswordStatusEl = shadow.getElementById('change-password-status');
156
199
  this.elements.changePasswordCardEl = shadow.getElementById('change-password-card');
200
+ this.elements.sectionAddPasswordOtpEl = shadow.getElementById('section-add-password-otp');
201
+ this.elements.inputAddPasswordOtpEl = shadow.getElementById('input-add-password-otp');
202
+ this.elements.btnResendAddPasswordOtpEl = shadow.getElementById('btn-resend-add-password-otp');
203
+ this.elements.descAddPasswordOtpEl = shadow.getElementById('desc-add-password-otp');
157
204
  // ElementBase requires contentEl
158
205
  this.elements.contentEl = shadow.getElementById('identity-details-view');
159
206
  this.initHandlers();
@@ -178,11 +225,6 @@ export class KeystoneDetailsComponentInstance extends IbGibDynamicComponentInsta
178
225
  }
179
226
  }
180
227
  }
181
- async disconnected() {
182
- if (this._onIdentityChanged) {
183
- window.removeEventListener(EVENT_IBGIB_IDENTITY_CHANGED, this._onIdentityChanged);
184
- }
185
- }
186
228
  initHandlers() {
187
229
  // Setup raw JSON accordion toggle
188
230
  this.elements.rawAccordionHeaderEl.addEventListener('click', () => this.toggleAccordion());
@@ -191,9 +233,11 @@ export class KeystoneDetailsComponentInstance extends IbGibDynamicComponentInsta
191
233
  this.elements.btnSetActiveEl.addEventListener('click', () => this.handleSetActiveIdentity());
192
234
  this.elements.btnCheckUpdatesEl.addEventListener('click', () => this.handleCheckForUpdates());
193
235
  this.elements.btnUpdateIdentityEl.addEventListener('click', () => this.handleUpdateIdentity());
236
+ this.elements.btnAddPasskeyEl?.addEventListener('click', () => this.handleAddPasskey());
194
237
  this.elements.btnLinkGoogleEl.addEventListener('click', () => this.handleSSOClick('google'));
195
238
  this.elements.btnLinkGithubEl.addEventListener('click', () => this.handleSSOClick('github'));
196
239
  this.elements.btnChangePasswordEl.addEventListener('click', () => this.handleChangePassword());
240
+ this.elements.btnResendAddPasswordOtpEl?.addEventListener('click', () => this.handleResendAddPasswordOtp());
197
241
  }
198
242
  toggleAccordion() {
199
243
  const header = this.elements.rawAccordionHeaderEl;
@@ -399,13 +443,22 @@ export class KeystoneDetailsComponentInstance extends IbGibDynamicComponentInsta
399
443
  this.elements.btnLinkGithubEl.classList.remove('disabled');
400
444
  // Check if origin provider or linked
401
445
  const originProvider = this.aggregatedDetails?.custodianProvider || this.ibGib?.data?.frameDetails?.custodianProvider;
402
- const isGoogleLinked = isSsoProviderLinked({ keystone: this.ibGib, aggregatedDetails: this.aggregatedDetails, providerId: 'google' });
403
- const isGithubLinked = isSsoProviderLinked({ keystone: this.ibGib, aggregatedDetails: this.aggregatedDetails, providerId: 'github' });
404
- const caps = getKeystoneCapabilities({ keystone: this.ibGib, aggregatedDetails: this.aggregatedDetails });
446
+ const isGoogleLinked = !!this.ibGib ?
447
+ isSsoProviderLinked({ keystone: this.ibGib, aggregatedDetails: this.aggregatedDetails, providerId: 'google' }) :
448
+ false;
449
+ const isGithubLinked = !!this.ibGib ?
450
+ isSsoProviderLinked({ keystone: this.ibGib, aggregatedDetails: this.aggregatedDetails, providerId: 'github' }) :
451
+ false;
452
+ const caps = !!this.ibGib ?
453
+ getKeystoneCapabilities({ keystone: this.ibGib, aggregatedDetails: this.aggregatedDetails }) :
454
+ undefined;
405
455
  // Google Card States
406
456
  const googleCard = this.elements.btnLinkGoogleEl;
407
457
  const googleLabel = googleCard.querySelector('.toggle-state-text');
408
458
  if (isGoogleLinked) {
459
+ if (!caps) {
460
+ throw new Error(`(UNEXPECTED) isGoogleLinked but capabilities falsy? (E: cc62e8357b66860dd2cb8148c833b726)`);
461
+ }
409
462
  googleCard.classList.remove('unlinked');
410
463
  googleCard.classList.add('linked');
411
464
  const isOrigin = originProvider === 'google';
@@ -432,6 +485,9 @@ export class KeystoneDetailsComponentInstance extends IbGibDynamicComponentInsta
432
485
  const githubCard = this.elements.btnLinkGithubEl;
433
486
  const githubLabel = githubCard.querySelector('.toggle-state-text');
434
487
  if (isGithubLinked) {
488
+ if (!caps) {
489
+ throw new Error(`(UNEXPECTED) isGithubLinked but capabilities falsy? (E: e94688210538e0e82878088873a46826)`);
490
+ }
435
491
  githubCard.classList.remove('unlinked');
436
492
  githubCard.classList.add('linked');
437
493
  const isOrigin = originProvider === 'github';
@@ -457,17 +513,19 @@ export class KeystoneDetailsComponentInstance extends IbGibDynamicComponentInsta
457
513
  }
458
514
  }
459
515
  updatePasswordCardView() {
460
- const caps = getKeystoneCapabilities({ keystone: this.ibGib, aggregatedDetails: this.aggregatedDetails });
516
+ const caps = !!this.ibGib ?
517
+ getKeystoneCapabilities({ keystone: this.ibGib, aggregatedDetails: this.aggregatedDetails }) :
518
+ undefined;
461
519
  const cardTitleEl = this.elements?.changePasswordCardEl?.querySelector('h3');
462
520
  const cardDescEl = this.elements?.changePasswordCardEl?.querySelector('.description');
463
521
  const currentPasswordGroup = this.elements?.inputCurrentPasswordEl?.closest('.form-group');
464
522
  const btnChangePassword = this.elements?.btnChangePasswordEl;
465
- const isCustodialOnly = caps.isCustodial && !caps.isSovereign;
466
- if (isCustodialOnly) {
523
+ const hasPassword = !!caps?.hasPassword;
524
+ if (!hasPassword) {
467
525
  if (cardTitleEl)
468
526
  cardTitleEl.textContent = "Add Master Password";
469
527
  if (cardDescEl)
470
- cardDescEl.textContent = "Establish sovereign ownership by adding a master password. This enables direct, independent cryptographic management without relying solely on custodian authentication.";
528
+ cardDescEl.textContent = "Establish a master password for this identity. This enables sovereign password authentication alongside your passkeys and linked providers.";
471
529
  if (currentPasswordGroup)
472
530
  currentPasswordGroup.style.display = 'none';
473
531
  if (btnChangePassword)
@@ -483,6 +541,12 @@ export class KeystoneDetailsComponentInstance extends IbGibDynamicComponentInsta
483
541
  if (btnChangePassword)
484
542
  btnChangePassword.textContent = "Update Password";
485
543
  }
544
+ if (this.elements?.sectionAddPasswordOtpEl) {
545
+ this.elements.sectionAddPasswordOtpEl.classList.add('hidden');
546
+ if (this.elements.inputAddPasswordOtpEl) {
547
+ this.elements.inputAddPasswordOtpEl.value = '';
548
+ }
549
+ }
486
550
  }
487
551
  async updateFrameDetailsView() {
488
552
  const lc = `${this.lc}[${this.updateFrameDetailsView.name}]`;
@@ -588,17 +652,20 @@ export class KeystoneDetailsComponentInstance extends IbGibDynamicComponentInsta
588
652
  this.elements.rawKeystoneJsonEl.textContent = pretty(keystone);
589
653
  // Render Challenge Pools
590
654
  this.renderChallengePools(keystone.data?.challengePools || []);
591
- // Render Delegates, SSO link, and Password Change cards (only if this is a primary keystone, hide them for delegate/session keystones)
655
+ // Render Delegates, Passkeys, SSO link, and Password Change cards (only if this is a primary keystone, hide them for delegate/session keystones)
592
656
  const details = await this.getKeystoneDetails(this.ibGibAddr);
593
657
  if (details.isPrimary) {
594
658
  this.elements.delegatesCardEl.classList.remove('hidden');
595
659
  await this.renderDelegates(keystone.data?.delegates);
660
+ this.elements.passkeysCardEl.classList.remove('hidden');
661
+ await this.renderPasskeys(this.aggregatedDetails?.passkeys);
596
662
  this.elements.ssoLinkCardEl.classList.remove('hidden');
597
663
  this.elements.changePasswordCardEl.classList.remove('hidden');
598
664
  this.updatePasswordCardView();
599
665
  }
600
666
  else {
601
667
  this.elements.delegatesCardEl.classList.add('hidden');
668
+ this.elements.passkeysCardEl.classList.add('hidden');
602
669
  this.elements.ssoLinkCardEl.classList.add('hidden');
603
670
  this.elements.changePasswordCardEl.classList.add('hidden');
604
671
  }
@@ -637,6 +704,15 @@ export class KeystoneDetailsComponentInstance extends IbGibDynamicComponentInsta
637
704
  if (this.elements.delegatesListEl) {
638
705
  this.elements.delegatesListEl.innerHTML = '';
639
706
  }
707
+ if (this.elements.passkeysListEl) {
708
+ this.elements.passkeysListEl.innerHTML = '';
709
+ }
710
+ if (this.elements.sectionAddPasswordOtpEl) {
711
+ this.elements.sectionAddPasswordOtpEl.classList.add('hidden');
712
+ if (this.elements.inputAddPasswordOtpEl) {
713
+ this.elements.inputAddPasswordOtpEl.value = '';
714
+ }
715
+ }
640
716
  }
641
717
  renderChallengePools(pools) {
642
718
  if (!this.elements) {
@@ -934,11 +1010,17 @@ export class KeystoneDetailsComponentInstance extends IbGibDynamicComponentInsta
934
1010
  const lc = `${this.lc}[${this.handleRevokeDelegate.name}]`;
935
1011
  try {
936
1012
  let masterSecret;
1013
+ let matchedPoolId;
937
1014
  try {
938
- masterSecret = await promptForSecret({
939
- msg: `Enter Master Password to revoke delegate '${deviceName}':`,
940
- confirm: false
1015
+ const authResult = await promptForAuthoritySecret({
1016
+ keystone: this.ibGib,
1017
+ aggregatedDetails: this.aggregatedDetails,
1018
+ title: 'Revoke Delegate',
1019
+ msg: `Enter Master Password or touch security key to revoke delegate '${deviceName}':`,
1020
+ verb: KEYSTONE_VERB_MANAGE
941
1021
  });
1022
+ masterSecret = authResult.secret;
1023
+ matchedPoolId = authResult.poolId;
942
1024
  }
943
1025
  catch (err) {
944
1026
  // User cancelled prompt
@@ -961,6 +1043,7 @@ export class KeystoneDetailsComponentInstance extends IbGibDynamicComponentInsta
961
1043
  parentKeystone: this.ibGib,
962
1044
  delegateTjpAddr,
963
1045
  masterSecret,
1046
+ signingPoolId: matchedPoolId,
964
1047
  metaspace,
965
1048
  space
966
1049
  });
@@ -1006,6 +1089,453 @@ export class KeystoneDetailsComponentInstance extends IbGibDynamicComponentInsta
1006
1089
  this.setSyncStatus(`Revocation failed: ${extractErrorMsg(error)}`, "error");
1007
1090
  }
1008
1091
  }
1092
+ setPasskeyStatus(msg, type) {
1093
+ const statusEl = this.elements?.passkeyStatusMsgEl;
1094
+ if (!statusEl)
1095
+ return;
1096
+ statusEl.textContent = msg;
1097
+ statusEl.className = `status-msg ${type}`;
1098
+ statusEl.classList.remove('hidden');
1099
+ }
1100
+ async renderPasskeys(passkeys) {
1101
+ if (!this.elements) {
1102
+ return;
1103
+ }
1104
+ const listEl = this.elements.passkeysListEl;
1105
+ listEl.innerHTML = '';
1106
+ const caps = !!this.ibGib ?
1107
+ getKeystoneCapabilities({ keystone: this.ibGib, aggregatedDetails: this.aggregatedDetails }) :
1108
+ undefined;
1109
+ // Update button text depending on whether the account is custodial-only (elevation) or sovereign/hybrid
1110
+ const isCustodialOnly = !!caps ? caps.isCustodial && !caps.isSovereign : false;
1111
+ if (this.elements.btnAddPasskeyEl) {
1112
+ this.elements.btnAddPasskeyEl.textContent = isCustodialOnly
1113
+ ? "🛡️ Add Passkey (Passwordless Sovereign Elevation)"
1114
+ : "🛡️ Register New Passkey / Security Key";
1115
+ }
1116
+ const validPasskeyEntries = Object.entries(passkeys || {}).filter(([_, entry]) => entry !== null && entry !== undefined);
1117
+ if (validPasskeyEntries.length === 0) {
1118
+ listEl.innerHTML = `
1119
+ <tr>
1120
+ <td colspan="4" style="text-align: center; color: var(--clr-text-secondary, #b3b3b3); padding: 1rem;">
1121
+ No hardware passkeys or biometrics registered.
1122
+ </td>
1123
+ </tr>
1124
+ `;
1125
+ return;
1126
+ }
1127
+ for (const [credId, entry] of validPasskeyEntries) {
1128
+ const row = document.createElement('tr');
1129
+ const createdText = entry.createdUTC ? new Date(entry.createdUTC).toLocaleString() : 'N/A';
1130
+ const nameText = entry.name || 'Hardware Key';
1131
+ // Format transport badges
1132
+ let transportBadgesHtml = '';
1133
+ if (entry.transports && entry.transports.length > 0) {
1134
+ transportBadgesHtml = entry.transports.map(t => {
1135
+ let icon = '🔑';
1136
+ let label = t;
1137
+ if (t === 'internal') {
1138
+ icon = '💻';
1139
+ label = 'Touch ID / Built-in';
1140
+ }
1141
+ else if (t === 'usb') {
1142
+ icon = '🔑';
1143
+ label = 'USB Key';
1144
+ }
1145
+ else if (t === 'nfc') {
1146
+ icon = '📶';
1147
+ label = 'NFC';
1148
+ }
1149
+ else if (t === 'ble' || t === 'hybrid') {
1150
+ icon = '📲';
1151
+ label = 'Mobile / Bluetooth';
1152
+ }
1153
+ 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>`;
1154
+ }).join(' ');
1155
+ }
1156
+ else {
1157
+ 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>`;
1158
+ }
1159
+ row.innerHTML = `
1160
+ <td>
1161
+ <div style="font-weight: 500;">${nameText}</div>
1162
+ <code style="font-size: 0.75rem; color: var(--clr-text-secondary, #b3b3b3); word-break: break-all;">${credId.substring(0, 16)}...</code>
1163
+ </td>
1164
+ <td>${transportBadgesHtml}</td>
1165
+ <td style="font-size: 0.85rem; color: var(--clr-text-secondary, #b3b3b3);">${createdText}</td>
1166
+ <td>
1167
+ <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);">
1168
+ Remove
1169
+ </button>
1170
+ </td>
1171
+ `;
1172
+ const btnRemove = row.querySelector('.btn-remove-passkey');
1173
+ btnRemove?.addEventListener('click', () => this.handleRemovePasskey(credId));
1174
+ listEl.appendChild(row);
1175
+ }
1176
+ }
1177
+ async handleAddPasskey() {
1178
+ const lc = `${this.lc}[${this.handleAddPasskey.name}]`;
1179
+ if (!this.ibGib || !this.ibGibAddr) {
1180
+ return;
1181
+ }
1182
+ try {
1183
+ const caps = getKeystoneCapabilities({ keystone: this.ibGib, aggregatedDetails: this.aggregatedDetails });
1184
+ const isCustodialOnly = caps.isCustodial && !caps.isSovereign;
1185
+ const supported = await isPasskeyPrfSupported();
1186
+ if (!supported) {
1187
+ this.setPasskeyStatus("WebAuthn PRF extension is not supported in this browser environment.", "error");
1188
+ return;
1189
+ }
1190
+ this.showBusy({
1191
+ isBusy: true,
1192
+ title: 'Touch Biometrics or Security Key',
1193
+ msg: 'Please touch your fingerprint sensor, Face ID, or insert your USB security key when prompted...',
1194
+ animationEmoji: '🛡️'
1195
+ });
1196
+ const username = this.aggregatedDetails?.username || this.ibGib.data?.frameDetails?.username || 'user';
1197
+ const rawSalt = crypto.getRandomValues(new Uint8Array(32));
1198
+ const saltHex = bufferToHex(rawSalt.buffer);
1199
+ const attachmentVal = this.elements?.selectPasskeyAttachmentEl?.value;
1200
+ const authenticatorAttachment = (attachmentVal === 'cross-platform' || attachmentVal === 'platform')
1201
+ ? attachmentVal
1202
+ : undefined;
1203
+ const regResult = await registerPasskeyCredential({
1204
+ username,
1205
+ displayName: username,
1206
+ salt: saltHex,
1207
+ userVerification: 'preferred',
1208
+ authenticatorAttachment,
1209
+ });
1210
+ const { credentialId, transports } = regResult;
1211
+ let passkeySecret = regResult.passkeySecret;
1212
+ if (!passkeySecret) {
1213
+ const prfResult = await evaluatePasskeyPrf({
1214
+ credentialId,
1215
+ salt: saltHex,
1216
+ transports,
1217
+ userVerification: 'preferred'
1218
+ });
1219
+ passkeySecret = prfResult.passkeySecret;
1220
+ }
1221
+ const defaultDeviceTag = generateDefaultDeviceTag();
1222
+ const passkeyName = defaultDeviceTag;
1223
+ const passkeyTag = getSaferSubstring({ text: passkeyName, length: 12, keepLiterals: ['-'] }) || 'key';
1224
+ const poolId = `passkey-manage-${passkeyTag}`;
1225
+ const passkeyEntry = {
1226
+ credentialId,
1227
+ poolId,
1228
+ name: passkeyName,
1229
+ salt: saltHex,
1230
+ createdUTC: new Date().toISOString(),
1231
+ ...(transports && transports.length > 0 ? { transports } : {})
1232
+ };
1233
+ const metaspace = await getGlobalMetaspace_waitIfNeeded();
1234
+ if (!metaspace)
1235
+ throw new Error("Metaspace not found");
1236
+ const space = await metaspace.getLocalUserSpace({ lock: false });
1237
+ if (!space)
1238
+ throw new Error("Local space not found");
1239
+ const keystoneService = new KeystoneService_V1();
1240
+ let evolvedKeystone;
1241
+ const poolConfig = {
1242
+ id: poolId,
1243
+ type: KeystoneChallengeType.hash_reveal_v1,
1244
+ salt: poolId,
1245
+ algo: HashAlgorithm.sha_256,
1246
+ rounds: 1,
1247
+ allowedVerbs: [KEYSTONE_VERB_MANAGE, KEYSTONE_VERB_SYNC, KEYSTONE_VERB_CONNECT],
1248
+ behavior: {
1249
+ size: 10,
1250
+ replenish: KeystoneReplenishStrategy.topUp,
1251
+ selectSequentially: 1,
1252
+ selectRandomly: 0,
1253
+ targetBindingCount: 0,
1254
+ }
1255
+ };
1256
+ const challenges = await generatePoolChallenges({
1257
+ config: poolConfig,
1258
+ masterSecret: passkeySecret
1259
+ });
1260
+ const newPool = {
1261
+ id: poolId,
1262
+ config: poolConfig,
1263
+ challenges
1264
+ };
1265
+ let authSecret;
1266
+ let signingPoolId;
1267
+ if (isCustodialOnly) {
1268
+ // Elevate custodial account with sovereign passkey
1269
+ this.showBusy({
1270
+ isBusy: true,
1271
+ title: 'Elevating Account Ownership...',
1272
+ msg: 'Establishing sovereign passkey pool on identity...',
1273
+ animationEmoji: '⚡'
1274
+ });
1275
+ authSecret = passkeySecret;
1276
+ signingPoolId = poolId;
1277
+ evolvedKeystone = await keystoneService.addPools({
1278
+ latestKeystone: this.ibGib,
1279
+ masterSecret: authSecret,
1280
+ signingPoolId,
1281
+ newPools: [newPool],
1282
+ frameDetails: {
1283
+ passkeys: {
1284
+ [credentialId]: passkeyEntry
1285
+ }
1286
+ },
1287
+ dataToPatch: {
1288
+ credentialIds: [...(this.ibGib.data?.credentialIds || []), credentialId]
1289
+ },
1290
+ metaspace,
1291
+ space
1292
+ });
1293
+ }
1294
+ else {
1295
+ // Secondary passkey registration on existing sovereign identity:
1296
+ // Prompt user to authorize with current master password or passkey
1297
+ try {
1298
+ const authResult = await promptForAuthoritySecret({
1299
+ keystone: this.ibGib,
1300
+ aggregatedDetails: this.aggregatedDetails,
1301
+ title: 'Authorize New Passkey',
1302
+ msg: 'Enter Master Password or touch security key to authorize adding this passkey:',
1303
+ verb: KEYSTONE_VERB_MANAGE
1304
+ });
1305
+ authSecret = authResult.secret;
1306
+ signingPoolId = authResult.poolId;
1307
+ }
1308
+ catch (err) {
1309
+ this.setPasskeyStatus("Registration cancelled (authorization required).", "error");
1310
+ return;
1311
+ }
1312
+ this.showBusy({
1313
+ isBusy: true,
1314
+ title: 'Registering Passkey...',
1315
+ msg: 'Adding cryptographic challenge pool and evolving identity...',
1316
+ animationEmoji: '🛡️'
1317
+ });
1318
+ evolvedKeystone = await keystoneService.addPools({
1319
+ latestKeystone: this.ibGib,
1320
+ masterSecret: authSecret,
1321
+ signingPoolId,
1322
+ newPools: [newPool],
1323
+ frameDetails: {
1324
+ passkeys: {
1325
+ [credentialId]: passkeyEntry
1326
+ }
1327
+ },
1328
+ dataToPatch: {
1329
+ credentialIds: [...(this.ibGib.data?.credentialIds || []), credentialId]
1330
+ },
1331
+ metaspace,
1332
+ space
1333
+ });
1334
+ // Sync pool addition frame to server
1335
+ const primaryTjpAddr = getTjpAddr({ ibGib: evolvedKeystone, defaultIfNone: 'incomingAddr' });
1336
+ const resSyncPool = await getApiBridge().putEvolveKeystone({
1337
+ domainAddr: primaryTjpAddr,
1338
+ keystoneIbGib: evolvedKeystone,
1339
+ relatedKeystoneIbGibs: []
1340
+ });
1341
+ if (!resSyncPool.success) {
1342
+ throw new Error(`Server synchronization of passkey pool failed: ${resSyncPool.message}`);
1343
+ }
1344
+ }
1345
+ // Construct outward device sync delegate for this passkey
1346
+ this.showBusy({
1347
+ isBusy: true,
1348
+ title: 'Creating Sync Delegate...',
1349
+ msg: 'Constructing outward passkey device sync delegate...',
1350
+ animationEmoji: '📱'
1351
+ });
1352
+ const primaryTjpAddr = getTjpAddr({ ibGib: evolvedKeystone, defaultIfNone: 'incomingAddr' });
1353
+ const { delegateKeystone, deviceTag } = await createPasskeyDeviceSyncDelegate({
1354
+ username,
1355
+ primaryTjpAddr,
1356
+ passkeySecret,
1357
+ transports,
1358
+ metaspace,
1359
+ space
1360
+ });
1361
+ // Register outward sync delegate on primary keystone
1362
+ this.showBusy({
1363
+ isBusy: true,
1364
+ title: 'Registering Delegate...',
1365
+ msg: 'Registering outward passkey delegate on primary identity...',
1366
+ animationEmoji: '📝'
1367
+ });
1368
+ const delegateSigningPoolId = isCustodialOnly ? poolId : signingPoolId;
1369
+ const delegateAuthSecret = isCustodialOnly ? passkeySecret : authSecret;
1370
+ evolvedKeystone = await keystoneService.registerDelegate({
1371
+ parentKeystone: evolvedKeystone,
1372
+ delegateKeystone,
1373
+ masterSecret: delegateAuthSecret,
1374
+ signingPoolId: delegateSigningPoolId,
1375
+ metaspace,
1376
+ space,
1377
+ allowedVerbs: [KEYSTONE_VERB_SYNC, KEYSTONE_VERB_CONNECT],
1378
+ deviceName: deviceTag
1379
+ });
1380
+ // Sync evolved keystone and delegate to server
1381
+ this.showBusy({
1382
+ isBusy: true,
1383
+ title: 'Syncing to Server...',
1384
+ msg: 'Publishing evolved identity keystone and delegate to server...',
1385
+ animationEmoji: '⚡'
1386
+ });
1387
+ const resSync = await getApiBridge().putEvolveKeystone({
1388
+ domainAddr: primaryTjpAddr,
1389
+ keystoneIbGib: evolvedKeystone,
1390
+ relatedKeystoneIbGibs: [delegateKeystone]
1391
+ });
1392
+ if (!resSync.success) {
1393
+ throw new Error(`Server synchronization of delegate failed: ${resSync.message}`);
1394
+ }
1395
+ // Update Special Index
1396
+ await updateSpecialIndex({
1397
+ type: "keystones",
1398
+ rel8nInfos: [
1399
+ {
1400
+ rel8nName: "keystone",
1401
+ ibGibs: [toDto({ ibGib: evolvedKeystone }), toDto({ ibGib: delegateKeystone })]
1402
+ }
1403
+ ],
1404
+ metaspace,
1405
+ space
1406
+ });
1407
+ this.setPasskeyStatus("Passkey successfully registered and bound to identity!", "success");
1408
+ this.ibGibAddr = getIbGibAddr({ ibGib: evolvedKeystone });
1409
+ this.staticIbGib = evolvedKeystone;
1410
+ await this.loadIbGib({ getLatest: false });
1411
+ }
1412
+ catch (error) {
1413
+ console.error(`${lc} ${extractErrorMsg(error)}`, error);
1414
+ this.setPasskeyStatus(`Failed to register passkey: ${extractErrorMsg(error)}`, "error");
1415
+ }
1416
+ finally {
1417
+ this.showBusy({ isBusy: false });
1418
+ }
1419
+ }
1420
+ async handleRemovePasskey(credentialId) {
1421
+ const lc = `${this.lc}[${this.handleRemovePasskey.name}]`;
1422
+ if (!this.ibGib || !this.ibGibAddr) {
1423
+ return;
1424
+ }
1425
+ const passkeyEntry = this.aggregatedDetails?.passkeys?.[credentialId];
1426
+ const passkeyName = passkeyEntry?.name || 'this passkey';
1427
+ try {
1428
+ const keystoneService = new KeystoneService_V1();
1429
+ let masterSecret;
1430
+ let signingPoolId;
1431
+ try {
1432
+ const authResult = await promptForAuthoritySecret({
1433
+ keystone: this.ibGib,
1434
+ aggregatedDetails: this.aggregatedDetails,
1435
+ title: 'Authorize Removal',
1436
+ msg: `Are you sure you want to remove "${passkeyName}"? Enter your Master Secret (Password) or touch security key to authorize:`,
1437
+ verb: KEYSTONE_VERB_MANAGE
1438
+ });
1439
+ masterSecret = authResult.secret;
1440
+ signingPoolId = authResult.poolId;
1441
+ }
1442
+ catch (err) {
1443
+ this.setPasskeyStatus("Removal cancelled or unauthorized.", "error");
1444
+ return;
1445
+ }
1446
+ this.showBusy({
1447
+ isBusy: true,
1448
+ title: 'Revoking Passkey...',
1449
+ msg: 'Tombstoning passkey record and removing challenge pool...',
1450
+ animationEmoji: '🗑️'
1451
+ });
1452
+ const metaspace = await getGlobalMetaspace_waitIfNeeded();
1453
+ if (!metaspace)
1454
+ throw new Error("Metaspace not found");
1455
+ const space = await metaspace.getLocalUserSpace({ lock: false });
1456
+ if (!space)
1457
+ throw new Error("Local space not found");
1458
+ const poolIdToRemove = passkeyEntry?.poolId || `passkey-manage-${credentialId.substring(0, 8)}`;
1459
+ const poolExists = this.ibGib.data?.challengePools.some(p => p.id === poolIdToRemove);
1460
+ const filteredCredentialIds = (this.ibGib.data?.credentialIds || []).filter(id => id !== credentialId);
1461
+ let evolvedKeystone;
1462
+ if (poolExists) {
1463
+ evolvedKeystone = await keystoneService.removePools({
1464
+ latestKeystone: this.ibGib,
1465
+ masterSecret: masterSecret,
1466
+ signingPoolId: signingPoolId,
1467
+ poolIds: [poolIdToRemove],
1468
+ frameDetails: {
1469
+ passkeys: {
1470
+ [credentialId]: null
1471
+ }
1472
+ },
1473
+ dataToPatch: {
1474
+ credentialIds: filteredCredentialIds
1475
+ },
1476
+ metaspace,
1477
+ space
1478
+ });
1479
+ }
1480
+ else {
1481
+ // If pool doesn't exist, just update frameDetails
1482
+ evolvedKeystone = await keystoneService.sign({
1483
+ latestKeystone: this.ibGib,
1484
+ masterSecret: masterSecret,
1485
+ poolId: signingPoolId,
1486
+ claim: {
1487
+ target: getIbGibAddr({ ibGib: this.ibGib })
1488
+ },
1489
+ frameDetails: {
1490
+ passkeys: {
1491
+ [credentialId]: null
1492
+ }
1493
+ },
1494
+ dataToPatch: {
1495
+ credentialIds: filteredCredentialIds
1496
+ },
1497
+ metaspace,
1498
+ space
1499
+ });
1500
+ }
1501
+ // Sync to server
1502
+ this.showBusy({
1503
+ isBusy: true,
1504
+ title: 'Syncing to Server...',
1505
+ msg: 'Publishing updated keystone to server...',
1506
+ animationEmoji: '⚡'
1507
+ });
1508
+ const primaryTjpAddr = getTjpAddr({ ibGib: evolvedKeystone, defaultIfNone: 'incomingAddr' });
1509
+ await getApiBridge().putEvolveKeystone({
1510
+ domainAddr: primaryTjpAddr,
1511
+ keystoneIbGib: evolvedKeystone,
1512
+ relatedKeystoneIbGibs: []
1513
+ });
1514
+ // Update Special Index
1515
+ await updateSpecialIndex({
1516
+ type: "keystones",
1517
+ rel8nInfos: [
1518
+ {
1519
+ rel8nName: "keystone",
1520
+ ibGibs: [toDto({ ibGib: evolvedKeystone })]
1521
+ }
1522
+ ],
1523
+ metaspace,
1524
+ space
1525
+ });
1526
+ this.setPasskeyStatus(`Passkey "${passkeyName}" successfully removed.`, "success");
1527
+ this.ibGibAddr = getIbGibAddr({ ibGib: evolvedKeystone });
1528
+ this.staticIbGib = evolvedKeystone;
1529
+ await this.loadIbGib({ getLatest: false });
1530
+ }
1531
+ catch (error) {
1532
+ console.error(`${lc} ${extractErrorMsg(error)}`, error);
1533
+ this.setPasskeyStatus(`Failed to remove passkey: ${extractErrorMsg(error)}`, "error");
1534
+ }
1535
+ finally {
1536
+ this.showBusy({ isBusy: false });
1537
+ }
1538
+ }
1009
1539
  async handleContextUpdated() {
1010
1540
  const lc = `${this.lc}[${this.handleContextUpdated.name}]`;
1011
1541
  try {
@@ -1050,10 +1580,24 @@ export class KeystoneDetailsComponentInstance extends IbGibDynamicComponentInsta
1050
1580
  async handleSSOLink(providerId) {
1051
1581
  const lc = `${this.lc}[${this.handleSSOLink.name}]`;
1052
1582
  this.setSSOStatus(`Linking ${providerId}...`, "info");
1583
+ const providerName = providerId === 'google' ? 'Google' : providerId === 'github' ? 'GitHub' : providerId;
1584
+ this.setSSOStatus(`Opening ${providerName} SSO authorization window...`, "info");
1585
+ this.showBusy({
1586
+ isBusy: true,
1587
+ title: `Connecting to ${providerName}...`,
1588
+ msg: `Opening ${providerName} SSO authorization window...\nPlease complete authorization in the popup window.`,
1589
+ animationEmoji: '🌐'
1590
+ });
1053
1591
  try {
1592
+ const primaryTjpAddr = (this.ibGib ? getTjpAddr({ ibGib: this.ibGib }) : undefined)
1593
+ || (this.ibGibAddr ? (() => {
1594
+ const { ib } = getIbAndGib({ ibGibAddr: this.ibGibAddr });
1595
+ const { tjpGib } = getGibInfo({ ibGibAddr: this.ibGibAddr });
1596
+ return tjpGib && ib ? `${ib}^${tjpGib}` : this.ibGibAddr;
1597
+ })() : undefined);
1054
1598
  const resPopup = await executeSsoOAuthPopup({
1055
1599
  providerId,
1056
- parentTjpAddr: this.ibGibAddr
1600
+ parentTjpAddr: primaryTjpAddr
1057
1601
  });
1058
1602
  if (!resPopup.success || !resPopup.code) {
1059
1603
  this.setSSOStatus(resPopup.message || `Linking cancelled.`, "error");
@@ -1065,6 +1609,9 @@ export class KeystoneDetailsComponentInstance extends IbGibDynamicComponentInsta
1065
1609
  console.error(`${lc} SSO link initiation failed: ${extractErrorMsg(error)}`);
1066
1610
  this.setSSOStatus(`Link failed: ${extractErrorMsg(error)}`, "error");
1067
1611
  }
1612
+ finally {
1613
+ this.showBusy({ isBusy: false });
1614
+ }
1068
1615
  }
1069
1616
  setSSOStatus(msg, type) {
1070
1617
  const statusEl = this.elements.ssoStatusMsgEl;
@@ -1074,7 +1621,14 @@ export class KeystoneDetailsComponentInstance extends IbGibDynamicComponentInsta
1074
1621
  }
1075
1622
  async handleSSOLinkCallback(code, stateObj, redirectUri) {
1076
1623
  const lc = `${this.lc}[${this.handleSSOLinkCallback.name}]`;
1077
- this.setSSOStatus("Retrieving challenge pool configuration from server...", "info");
1624
+ const providerName = stateObj.providerId === 'google' ? 'Google' : stateObj.providerId === 'github' ? 'GitHub' : stateObj.providerId;
1625
+ this.setSSOStatus(`Retrieving challenge pool configuration from server...`, "info");
1626
+ this.showBusy({
1627
+ isBusy: true,
1628
+ title: `Linking ${providerName}...`,
1629
+ msg: `Validating ${providerName} authorization with server...`,
1630
+ animationEmoji: '🔐'
1631
+ });
1078
1632
  try {
1079
1633
  // 1. Exchange OAuth code for custodian public pool config
1080
1634
  const res = await fetch('/api/identity/sso/link', {
@@ -1097,47 +1651,31 @@ export class KeystoneDetailsComponentInstance extends IbGibDynamicComponentInsta
1097
1651
  throw new Error(`Failed to retrieve custodian challenge pool: ${linkData.message || 'unknown error'}`);
1098
1652
  }
1099
1653
  const custodianPool = linkData.custodianPool;
1100
- // 2. Prompt user for master secret and verify it
1654
+ // 2. Prompt user for authorization secret
1101
1655
  const keystoneService = new KeystoneService_V1();
1102
1656
  let masterSecret;
1103
- let isCorrect = false;
1104
- let tries = 0;
1105
- const maxTries = 8;
1106
- while (tries < maxTries && !isCorrect) {
1107
- try {
1108
- const promptMsg = tries === 0
1109
- ? "Link successful! Enter your local Master Secret (Password) to sign and register the custodian pool"
1110
- : `Incorrect password. Please try again (${maxTries - tries} tries remaining):`;
1111
- masterSecret = await promptForSecret({
1112
- msg: promptMsg,
1113
- confirm: false
1114
- });
1115
- }
1116
- catch (error) {
1117
- // cancelled
1118
- if (logalot) {
1119
- console.log(`${lc}[promptForSecret] user cancelled (I: 911ff1fb0234949228481968e9087626)`);
1120
- }
1121
- masterSecret = undefined;
1122
- break;
1123
- }
1124
- if (!masterSecret) {
1125
- break;
1126
- }
1127
- // Verify secret using verifySigningSecret (requires manage poolId)
1128
- isCorrect = await keystoneService.verifySigningSecret({
1129
- keystoneIbGib: this.ibGib,
1130
- signingSecret: masterSecret,
1131
- poolId: 'manage'
1657
+ let signingPoolId;
1658
+ try {
1659
+ const authResult = await promptForAuthoritySecret({
1660
+ keystone: this.ibGib,
1661
+ aggregatedDetails: this.aggregatedDetails,
1662
+ title: `Link ${providerName}`,
1663
+ msg: `Enter Master Password or touch security key to authorize linking ${providerName}:`,
1664
+ verb: KEYSTONE_VERB_MANAGE
1132
1665
  });
1133
- if (!isCorrect) {
1134
- tries++;
1135
- }
1666
+ masterSecret = authResult.secret;
1667
+ signingPoolId = authResult.poolId;
1136
1668
  }
1137
- if (!isCorrect) {
1138
- this.setSSOStatus("Link cancelled or failed (incorrect password).", "error");
1669
+ catch (error) {
1670
+ this.setSSOStatus("Link cancelled or failed.", "error");
1139
1671
  return;
1140
1672
  }
1673
+ this.showBusy({
1674
+ isBusy: true,
1675
+ title: 'Evolving Identity...',
1676
+ msg: `Installing ${providerName} custodian pool and signing evolution...`,
1677
+ animationEmoji: '📝'
1678
+ });
1141
1679
  this.setSSOStatus("Evolving identity locally and generating cryptographic signatures...", "info");
1142
1680
  const metaspace = await getGlobalMetaspace_waitIfNeeded();
1143
1681
  if (!metaspace)
@@ -1146,46 +1684,90 @@ export class KeystoneDetailsComponentInstance extends IbGibDynamicComponentInsta
1146
1684
  if (!space) {
1147
1685
  throw new Error("Local space not found");
1148
1686
  }
1149
- // 3. Evolve parent keystone to include the custodian pool
1687
+ // 3. Evolve parent keystone to include the custodian pool (n -> n+1)
1150
1688
  const evolvedParent = await keystoneService.addPools({
1151
1689
  latestKeystone: this.ibGib,
1152
1690
  masterSecret: masterSecret,
1691
+ signingPoolId: signingPoolId,
1153
1692
  newPools: [custodianPool],
1154
1693
  metaspace,
1155
1694
  space
1156
1695
  });
1157
- // 4. Sync evolved parent keystone back to server
1158
- this.setSSOStatus("Publishing evolved identity keystone to server...", "info");
1159
- const syncRes = await getApiBridge().putEvolveKeystone({
1696
+ // Sync evolved parent (n+1) to server first
1697
+ const syncRes1 = await getApiBridge().putEvolveKeystone({
1160
1698
  domainAddr: stateObj.parentTjpAddr,
1161
1699
  keystoneIbGib: evolvedParent,
1162
- relatedKeystoneIbGibs: [] // No outward delegate timelines
1700
+ relatedKeystoneIbGibs: []
1163
1701
  });
1164
- if (!syncRes.success) {
1165
- throw new Error(`Server synchronization rejected: ${syncRes.message}`);
1702
+ if (!syncRes1.success) {
1703
+ throw new Error(`Server synchronization of linked pool rejected: ${syncRes1.message}`);
1704
+ }
1705
+ // 4. Create and register OAuth2 sync delegate for this provider (n+1 -> n+2)
1706
+ const username = this.aggregatedDetails?.username || this.ibGib?.data?.username || 'user';
1707
+ const { delegateKeystone, deviceTag } = await createOAuth2DeviceSyncDelegate({
1708
+ providerId: stateObj.providerId,
1709
+ username,
1710
+ primaryTjpAddr: stateObj.parentTjpAddr,
1711
+ custodianReqs: linkData.custodianReqs,
1712
+ metaspace,
1713
+ space,
1714
+ });
1715
+ const parentWithDelegate = await keystoneService.registerDelegate({
1716
+ parentKeystone: evolvedParent,
1717
+ delegateKeystone,
1718
+ masterSecret: masterSecret,
1719
+ signingPoolId: signingPoolId,
1720
+ metaspace,
1721
+ space,
1722
+ allowedVerbs: [KEYSTONE_VERB_SYNC, KEYSTONE_VERB_CONNECT],
1723
+ deviceName: deviceTag,
1724
+ });
1725
+ await metaspace.put({ ibGibs: [delegateKeystone, parentWithDelegate], space });
1726
+ // 5. Sync parent with registered delegate (n+2) to server
1727
+ this.showBusy({
1728
+ isBusy: true,
1729
+ title: 'Syncing to Server...',
1730
+ msg: 'Publishing linked identity and delegate to server...',
1731
+ animationEmoji: '⚡'
1732
+ });
1733
+ this.setSSOStatus("Publishing evolved identity keystone to server...", "info");
1734
+ const syncRes2 = await getApiBridge().putEvolveKeystone({
1735
+ domainAddr: stateObj.parentTjpAddr,
1736
+ keystoneIbGib: parentWithDelegate,
1737
+ relatedKeystoneIbGibs: [delegateKeystone]
1738
+ });
1739
+ if (!syncRes2.success) {
1740
+ throw new Error(`Server synchronization of delegate rejected: ${syncRes2.message}`);
1166
1741
  }
1167
- // 5. Update index locally
1742
+ // 6. Update index locally
1168
1743
  await updateSpecialIndex({
1169
1744
  type: "keystones",
1170
1745
  rel8nInfos: [
1171
1746
  {
1172
1747
  rel8nName: "keystone",
1173
- ibGibs: [toDto({ ibGib: evolvedParent })]
1748
+ ibGibs: [toDto({ ibGib: parentWithDelegate }), toDto({ ibGib: delegateKeystone })]
1174
1749
  }
1175
1750
  ],
1176
1751
  metaspace,
1177
1752
  space
1178
1753
  });
1179
1754
  this.setSSOStatus(`Successfully linked with ${stateObj.providerId}!`, "success");
1755
+ this.ibGibAddr = getIbGibAddr({ ibGib: parentWithDelegate });
1756
+ this.staticIbGib = parentWithDelegate;
1180
1757
  await this.loadIbGib({ getLatest: false });
1181
1758
  }
1182
1759
  catch (error) {
1183
1760
  console.error(`${lc} SSO callback processing failed: ${extractErrorMsg(error)}`);
1184
1761
  this.setSSOStatus(`Link failed: ${extractErrorMsg(error)}`, "error");
1185
1762
  }
1763
+ finally {
1764
+ this.showBusy({ isBusy: false });
1765
+ }
1186
1766
  }
1187
1767
  async handleSSOClick(providerId) {
1188
- const isLinked = isSsoProviderLinked({ keystone: this.ibGib, aggregatedDetails: this.aggregatedDetails, providerId });
1768
+ const isLinked = !!this.ibGib ?
1769
+ isSsoProviderLinked({ keystone: this.ibGib, aggregatedDetails: this.aggregatedDetails, providerId }) :
1770
+ false;
1189
1771
  if (isLinked) {
1190
1772
  const caps = getKeystoneCapabilities({ keystone: this.ibGib, aggregatedDetails: this.aggregatedDetails });
1191
1773
  if (!caps.isSovereign) {
@@ -1200,45 +1782,34 @@ export class KeystoneDetailsComponentInstance extends IbGibDynamicComponentInsta
1200
1782
  }
1201
1783
  async handleSSOUnlink(providerId) {
1202
1784
  const lc = `${this.lc}[${this.handleSSOUnlink.name}]`;
1203
- this.setSSOStatus(`Unlinking ${providerId}...`, "info");
1785
+ const providerName = providerId === 'google' ? 'Google' : providerId === 'github' ? 'GitHub' : providerId;
1786
+ this.setSSOStatus(`Unlinking ${providerName}...`, "info");
1204
1787
  try {
1205
- // 1. Prompt user for password
1788
+ // 1. Prompt user for authorization secret
1206
1789
  const keystoneService = new KeystoneService_V1();
1207
1790
  let masterSecret;
1208
- let isCorrect = false;
1209
- let tries = 0;
1210
- const maxTries = 8;
1211
- while (tries < maxTries && !isCorrect) {
1212
- try {
1213
- const promptMsg = tries === 0
1214
- ? `Are you sure you want to unlink your ${providerId} account? If so, enter your local Master Secret (Password) to authorize. Otherwise, click Cancel.`
1215
- : `Incorrect password. Please try again (${maxTries - tries} tries remaining):`;
1216
- masterSecret = await promptForSecret({
1217
- msg: promptMsg,
1218
- confirm: false
1219
- });
1220
- }
1221
- catch (error) {
1222
- masterSecret = undefined;
1223
- break;
1224
- }
1225
- if (!masterSecret) {
1226
- break;
1227
- }
1228
- // Verify secret using verifySigningSecret (requires manage poolId)
1229
- isCorrect = await keystoneService.verifySigningSecret({
1230
- keystoneIbGib: this.ibGib,
1231
- signingSecret: masterSecret,
1232
- poolId: 'manage'
1791
+ let signingPoolId;
1792
+ try {
1793
+ const authResult = await promptForAuthoritySecret({
1794
+ keystone: this.ibGib,
1795
+ aggregatedDetails: this.aggregatedDetails,
1796
+ title: `Unlink ${providerName}`,
1797
+ msg: `Are you sure you want to unlink your ${providerName} account? If so, enter your Master Password or touch security key to authorize.`,
1798
+ verb: KEYSTONE_VERB_MANAGE
1233
1799
  });
1234
- if (!isCorrect) {
1235
- tries++;
1236
- }
1800
+ masterSecret = authResult.secret;
1801
+ signingPoolId = authResult.poolId;
1237
1802
  }
1238
- if (!isCorrect) {
1239
- this.setSSOStatus("Unlink cancelled or failed (incorrect password).", "error");
1803
+ catch (error) {
1804
+ this.setSSOStatus("Unlink cancelled or failed.", "error");
1240
1805
  return;
1241
1806
  }
1807
+ this.showBusy({
1808
+ isBusy: true,
1809
+ title: 'Unlinking Provider...',
1810
+ msg: `Removing ${providerName} custodian pool and delegate keys...`,
1811
+ animationEmoji: '🔓'
1812
+ });
1242
1813
  this.setSSOStatus("Evolving identity locally and removing custodian pool...", "info");
1243
1814
  const metaspace = await getGlobalMetaspace_waitIfNeeded();
1244
1815
  if (!metaspace)
@@ -1252,35 +1823,64 @@ export class KeystoneDetailsComponentInstance extends IbGibDynamicComponentInsta
1252
1823
  let evolvedParent = await keystoneService.removePools({
1253
1824
  latestKeystone: this.ibGib,
1254
1825
  masterSecret: masterSecret,
1826
+ signingPoolId: signingPoolId,
1255
1827
  poolIds: [targetPoolId],
1256
1828
  metaspace,
1257
1829
  space
1258
1830
  });
1259
- // Unregister any corresponding outward delegate for this provider if present
1260
- const delegatesMap = this.ibGib?.data?.delegates;
1831
+ const primaryTjpAddr = (this.ibGib ? getTjpAddr({ ibGib: this.ibGib }) : undefined)
1832
+ || (this.ibGibAddr ? (() => {
1833
+ const { ib } = getIbAndGib({ ibGibAddr: this.ibGibAddr });
1834
+ const { tjpGib } = getGibInfo({ ibGibAddr: this.ibGibAddr });
1835
+ return tjpGib && ib ? `${ib}^${tjpGib}` : this.ibGibAddr;
1836
+ })() : undefined)
1837
+ || getIbGibAddr({ ibGib: this.ibGib });
1838
+ // 3. Sync pool removal frame to server
1839
+ this.showBusy({
1840
+ isBusy: true,
1841
+ title: 'Syncing to Server...',
1842
+ msg: `Publishing ${providerName} pool removal to server...`,
1843
+ animationEmoji: '⚡'
1844
+ });
1845
+ this.setSSOStatus("Publishing evolved identity keystone to server...", "info");
1846
+ let syncRes = await getApiBridge().putEvolveKeystone({
1847
+ domainAddr: primaryTjpAddr,
1848
+ keystoneIbGib: evolvedParent,
1849
+ relatedKeystoneIbGibs: [] // No outward delegate timelines
1850
+ });
1851
+ if (!syncRes.success) {
1852
+ throw new Error(`Server synchronization rejected: ${syncRes.message}`);
1853
+ }
1854
+ // 4. Unregister any corresponding outward delegate for this provider if present
1855
+ const delegatesMap = evolvedParent.data?.delegates || this.ibGib?.data?.delegates;
1261
1856
  if (delegatesMap) {
1262
1857
  for (const [delegateTjpAddr, info] of Object.entries(delegatesMap)) {
1263
- if (info.deviceName?.includes(providerId)) {
1858
+ if (isCustodialDelegate({ delegateInfo: info, providerId })) {
1264
1859
  evolvedParent = await keystoneService.unregisterDelegate({
1265
1860
  parentKeystone: evolvedParent,
1266
1861
  delegateTjpAddr,
1267
1862
  masterSecret: masterSecret,
1863
+ signingPoolId: 'manage',
1268
1864
  metaspace,
1269
1865
  space,
1270
1866
  });
1867
+ this.showBusy({
1868
+ isBusy: true,
1869
+ title: 'Syncing to Server...',
1870
+ msg: `Publishing ${providerName} delegate unregistration to server...`,
1871
+ animationEmoji: '⚡'
1872
+ });
1873
+ syncRes = await getApiBridge().putEvolveKeystone({
1874
+ domainAddr: primaryTjpAddr,
1875
+ keystoneIbGib: evolvedParent,
1876
+ relatedKeystoneIbGibs: []
1877
+ });
1878
+ if (!syncRes.success) {
1879
+ throw new Error(`Server synchronization rejected: ${syncRes.message}`);
1880
+ }
1271
1881
  }
1272
1882
  }
1273
1883
  }
1274
- // 3. Sync evolved parent keystone back to server
1275
- this.setSSOStatus("Publishing evolved identity keystone to server...", "info");
1276
- const syncRes = await getApiBridge().putEvolveKeystone({
1277
- domainAddr: getIbGibAddr({ ibGib: this.ibGib }),
1278
- keystoneIbGib: evolvedParent,
1279
- relatedKeystoneIbGibs: [] // No outward delegate timelines
1280
- });
1281
- if (!syncRes.success) {
1282
- throw new Error(`Server synchronization rejected: ${syncRes.message}`);
1283
- }
1284
1884
  // 4. Update index locally
1285
1885
  await updateSpecialIndex({
1286
1886
  type: "keystones",
@@ -1293,12 +1893,17 @@ export class KeystoneDetailsComponentInstance extends IbGibDynamicComponentInsta
1293
1893
  metaspace,
1294
1894
  space
1295
1895
  });
1296
- this.setSSOStatus(`Successfully unlinked ${providerId}!`, "success");
1297
- await this.loadIbGib({ getLatest: true });
1896
+ this.setSSOStatus(`Successfully unlinked ${providerName}!`, "success");
1897
+ this.ibGibAddr = getIbGibAddr({ ibGib: evolvedParent });
1898
+ this.staticIbGib = evolvedParent;
1899
+ await this.loadIbGib({ getLatest: false });
1298
1900
  }
1299
1901
  catch (error) {
1300
- console.error(`${lc} SSO unlink failed: ${extractErrorMsg(error)}`);
1301
- this.setSSOStatus(`Unlink failed: ${extractErrorMsg(error)}`, "error");
1902
+ console.error(`${lc} ${extractErrorMsg(error)}`, error);
1903
+ this.setSSOStatus(`Failed to unlink ${providerName}: ${extractErrorMsg(error)}`, "error");
1904
+ }
1905
+ finally {
1906
+ this.showBusy({ isBusy: false });
1302
1907
  }
1303
1908
  }
1304
1909
  setChangePasswordStatus(msg, type) {
@@ -1342,8 +1947,16 @@ export class KeystoneDetailsComponentInstance extends IbGibDynamicComponentInsta
1342
1947
  config: initialManagePool.config,
1343
1948
  challenges: newChallenges,
1344
1949
  };
1345
- const primaryTjpAddr = getTjpAddr({ ibGib: this.ibGib }) || getIbGibAddr({ ibGib: this.ibGib });
1346
- const custodianProvider = this.aggregatedDetails?.custodianProvider || 'google';
1950
+ const primaryTjpAddr = (this.ibGib ? getTjpAddr({ ibGib: this.ibGib }) : undefined)
1951
+ || (this.ibGibAddr ? (() => {
1952
+ const { ib } = getIbAndGib({ ibGibAddr: this.ibGibAddr });
1953
+ const { tjpGib } = getGibInfo({ ibGibAddr: this.ibGibAddr });
1954
+ return tjpGib && ib ? `${ib}^${tjpGib}` : this.ibGibAddr;
1955
+ })() : undefined)
1956
+ || getIbGibAddr({ ibGib: this.ibGib });
1957
+ const custodianProvider = this.aggregatedDetails?.custodianProvider
1958
+ || this.ibGib?.data?.frameDetails?.custodianProvider
1959
+ || this.ibGib?.data?.custodianProvider;
1347
1960
  let requestBody = {
1348
1961
  primaryTjpAddr,
1349
1962
  newManagePool,
@@ -1370,15 +1983,60 @@ export class KeystoneDetailsComponentInstance extends IbGibDynamicComponentInsta
1370
1983
  requestBody.redirectUri = resPopup.redirectUri;
1371
1984
  }
1372
1985
  else {
1373
- this.showBusy({ isBusy: false });
1374
- // Email OTP fallback
1375
- const email = this.aggregatedDetails?.emails?.[0] || this.ibGib?.data?.emails?.[0];
1376
- const otpCode = await promptForSecret({
1377
- msg: `Enter 6-digit verification code sent to ${email}:`,
1378
- confirm: false,
1379
- });
1380
- if (!otpCode) {
1381
- this.setChangePasswordStatus("Verification code required.", "error");
1986
+ // Email OTP flow
1987
+ const email = this.aggregatedDetails?.emails?.[0]
1988
+ || this.ibGib?.data?.emails?.[0]
1989
+ || this.ibGib?.data?.frameDetails?.emails?.[0];
1990
+ if (!email) {
1991
+ this.showBusy({ isBusy: false });
1992
+ this.setChangePasswordStatus("No email address found for this identity to send verification code.", "error");
1993
+ return;
1994
+ }
1995
+ const otpSection = this.elements?.sectionAddPasswordOtpEl;
1996
+ const otpInput = this.elements?.inputAddPasswordOtpEl;
1997
+ const isOtpVisible = otpSection && !otpSection.classList.contains('hidden');
1998
+ // Step 1: If OTP section is not visible yet, send code and show the OTP input inline in the card
1999
+ if (!isOtpVisible) {
2000
+ this.showBusy({
2001
+ isBusy: true,
2002
+ title: 'Sending Verification Code...',
2003
+ msg: `Sending 6-digit verification code to ${email}...`,
2004
+ animationEmoji: '📧'
2005
+ });
2006
+ this.setChangePasswordStatus(`Sending verification code to ${email}...`, "info");
2007
+ const resCode = await getApiBridge().postVerificationCode({
2008
+ email,
2009
+ primaryTjpAddr,
2010
+ });
2011
+ this.showBusy({ isBusy: false });
2012
+ if (!resCode.success) {
2013
+ this.setChangePasswordStatus(resCode.message || "Failed to send verification code.", "error");
2014
+ return;
2015
+ }
2016
+ if (otpSection) {
2017
+ otpSection.classList.remove('hidden');
2018
+ }
2019
+ if (this.elements?.descAddPasswordOtpEl) {
2020
+ this.elements.descAddPasswordOtpEl.textContent = `A 6-digit verification code was sent to ${email}. Enter it below to establish sovereign ownership:`;
2021
+ }
2022
+ if (this.elements?.btnChangePasswordEl) {
2023
+ this.elements.btnChangePasswordEl.textContent = "Verify Code & Add Master Password";
2024
+ }
2025
+ this.setChangePasswordStatus(`✉️ 6-digit verification code sent to ${email}. Enter the code and click "Verify Code & Add Master Password".`, "info");
2026
+ this.startResendCooldownTimer(30);
2027
+ if (otpInput) {
2028
+ otpInput.value = '';
2029
+ otpInput.focus();
2030
+ }
2031
+ return;
2032
+ }
2033
+ // Step 2: OTP section is visible - validate entered code
2034
+ const otpCode = (otpInput?.value || '').trim();
2035
+ if (!otpCode || otpCode.length < 6) {
2036
+ this.setChangePasswordStatus("Please enter the complete 6-digit verification code sent to your email.", "error");
2037
+ if (otpInput) {
2038
+ otpInput.focus();
2039
+ }
1382
2040
  return;
1383
2041
  }
1384
2042
  requestBody.email = email;
@@ -1410,38 +2068,328 @@ export class KeystoneDetailsComponentInstance extends IbGibDynamicComponentInsta
1410
2068
  if (!space) {
1411
2069
  throw new Error("Local user space not available");
1412
2070
  }
1413
- await metaspace.put({ ibGibs: [evolvedParent], space });
1414
- await metaspace.registerNewIbGib({ ibGib: evolvedParent, space });
1415
- await updateSpecialIndex({
1416
- type: "keystones",
1417
- rel8nInfos: [
1418
- {
1419
- rel8nName: "keystone",
1420
- ibGibs: [toDto({ ibGib: evolvedParent })]
2071
+ if (resJson.primaryGraph) {
2072
+ const graphIbGibs = Object.values(resJson.primaryGraph);
2073
+ await metaspace.put({ ibGibs: graphIbGibs, space });
2074
+ const timelinesMap = getTimelinesGroupedByTjp({ ibGibs: graphIbGibs });
2075
+ for (const tjpKey of Object.keys(timelinesMap)) {
2076
+ for (const frame of timelinesMap[tjpKey]) {
2077
+ await metaspace.registerNewIbGib({ ibGib: frame, space });
1421
2078
  }
1422
- ],
2079
+ }
2080
+ }
2081
+ else {
2082
+ await metaspace.put({ ibGibs: [evolvedParent], space });
2083
+ await metaspace.registerNewIbGib({ ibGib: evolvedParent, space });
2084
+ }
2085
+ await this.registerAndSyncSovereignPasswordDelegate({
2086
+ evolvedParent,
2087
+ newPassword,
2088
+ primaryTjpAddr,
1423
2089
  metaspace,
1424
2090
  space
1425
2091
  });
1426
- const evolvedAddr = getIbGibAddr({ ibGib: evolvedParent });
1427
- // Dispatch EVENT_IBGIB_IDENTITY_CHANGED
1428
- window.dispatchEvent(new CustomEvent(EVENT_IBGIB_IDENTITY_CHANGED, {
1429
- detail: { activeIdentityAddr: evolvedAddr },
1430
- bubbles: true,
1431
- composed: true
1432
- }));
1433
- this.setChangePasswordStatus("Successfully added Master Password! Identity is now sovereign.", "success");
1434
- // Refresh the view
1435
- this.ibGibAddr = evolvedAddr;
1436
- this.staticIbGib = evolvedParent;
1437
- await this.loadIbGib({ getLatest: true });
1438
- // Clear inputs
1439
- this.elements.inputNewPasswordEl.value = '';
1440
- this.elements.inputConfirmPasswordEl.value = '';
1441
2092
  }
1442
2093
  catch (error) {
1443
2094
  console.error(`${lc} Elevation failed: ${extractErrorMsg(error)}`);
1444
2095
  this.setChangePasswordStatus(`Failed to add master password: ${extractErrorMsg(error)}`, "error");
2096
+ if (this.elements?.inputAddPasswordOtpEl && !this.elements.sectionAddPasswordOtpEl?.classList.contains('hidden')) {
2097
+ this.elements.inputAddPasswordOtpEl.select();
2098
+ this.elements.inputAddPasswordOtpEl.focus();
2099
+ }
2100
+ }
2101
+ finally {
2102
+ this.showBusy({ isBusy: false });
2103
+ }
2104
+ }
2105
+ async handleResendAddPasswordOtp() {
2106
+ const lc = `${this.lc}[${this.handleResendAddPasswordOtp.name}]`;
2107
+ const email = this.aggregatedDetails?.emails?.[0]
2108
+ || this.ibGib?.data?.emails?.[0]
2109
+ || this.ibGib?.data?.frameDetails?.emails?.[0];
2110
+ if (!email) {
2111
+ this.setChangePasswordStatus("No email address found for this identity to send verification code.", "error");
2112
+ return;
2113
+ }
2114
+ const primaryTjpAddr = (this.ibGib ? getTjpAddr({ ibGib: this.ibGib }) : undefined)
2115
+ || (this.ibGibAddr ? (() => {
2116
+ const { ib } = getIbAndGib({ ibGibAddr: this.ibGibAddr });
2117
+ const { tjpGib } = getGibInfo({ ibGibAddr: this.ibGibAddr });
2118
+ return tjpGib && ib ? `${ib}^${tjpGib}` : this.ibGibAddr;
2119
+ })() : undefined)
2120
+ || getIbGibAddr({ ibGib: this.ibGib });
2121
+ const btn = this.elements.btnResendAddPasswordOtpEl;
2122
+ if (btn) {
2123
+ btn.disabled = true;
2124
+ btn.textContent = "Sending...";
2125
+ }
2126
+ this.setChangePasswordStatus(`Resending verification code to ${email}...`, "info");
2127
+ try {
2128
+ const resCode = await getApiBridge().postVerificationCode({ email, primaryTjpAddr });
2129
+ if (resCode.success) {
2130
+ this.setChangePasswordStatus(`✉️ Fresh 6-digit verification code sent to ${email}!`, "success");
2131
+ this.startResendCooldownTimer(30);
2132
+ if (this.elements?.inputAddPasswordOtpEl) {
2133
+ this.elements.inputAddPasswordOtpEl.value = '';
2134
+ this.elements.inputAddPasswordOtpEl.focus();
2135
+ }
2136
+ }
2137
+ else {
2138
+ this.setChangePasswordStatus(resCode.message || "Failed to resend verification code.", "error");
2139
+ if (btn) {
2140
+ btn.disabled = false;
2141
+ btn.textContent = "Resend Code";
2142
+ }
2143
+ }
2144
+ }
2145
+ catch (err) {
2146
+ this.setChangePasswordStatus(`Error resending verification code: ${extractErrorMsg(err)}`, "error");
2147
+ if (btn) {
2148
+ btn.disabled = false;
2149
+ btn.textContent = "Resend Code";
2150
+ }
2151
+ }
2152
+ }
2153
+ /**
2154
+ * Common DRY post-elevation workflow: creates and registers the password-based
2155
+ * device sync delegate, synchronizes the evolved identity with the delegate to the server,
2156
+ * updates the local space and special index, and dispatches the identity change event.
2157
+ */
2158
+ async registerAndSyncSovereignPasswordDelegate({ evolvedParent, newPassword, primaryTjpAddr, metaspace, space, }) {
2159
+ const lc = `${this.lc}[${this.registerAndSyncSovereignPasswordDelegate.name}]`;
2160
+ // 1. Create a password-based sync delegate for this device
2161
+ this.showBusy({
2162
+ isBusy: true,
2163
+ title: 'Creating Sovereign Sync Delegate...',
2164
+ msg: 'Deriving local device sync delegate keys from Master Password...',
2165
+ animationEmoji: '🔑'
2166
+ });
2167
+ await delay(50);
2168
+ const rawUsername = this.aggregatedDetails?.username
2169
+ || this.ibGib?.data?.username
2170
+ || this.aggregatedDetails?.emails?.[0]?.split('@')[0]
2171
+ || this.ibGib?.data?.emails?.[0]?.split('@')[0]
2172
+ || 'user';
2173
+ const { delegateKeystone: userSyncDelegate, deviceTag } = await createSovereignDeviceSyncDelegate({
2174
+ username: rawUsername,
2175
+ primaryTjpAddr,
2176
+ masterSecret: newPassword,
2177
+ metaspace,
2178
+ space,
2179
+ });
2180
+ // 2. Register the new user sync delegate on the evolved parent keystone
2181
+ this.showBusy({
2182
+ isBusy: true,
2183
+ title: 'Registering Sovereign Delegate...',
2184
+ msg: 'Registering user sync delegate on primary keystone...',
2185
+ animationEmoji: '📝'
2186
+ });
2187
+ await delay(50);
2188
+ const keystoneService = new KeystoneService_V1();
2189
+ const parentWithUserDelegate = await keystoneService.registerDelegate({
2190
+ parentKeystone: evolvedParent,
2191
+ delegateKeystone: userSyncDelegate,
2192
+ masterSecret: newPassword,
2193
+ signingPoolId: POOL_ID_MANAGE,
2194
+ metaspace,
2195
+ space,
2196
+ allowedVerbs: [KEYSTONE_VERB_SYNC, KEYSTONE_VERB_CONNECT],
2197
+ deviceName: deviceTag
2198
+ });
2199
+ // 3. Post evolved keystone with new delegate to server
2200
+ this.showBusy({
2201
+ isBusy: true,
2202
+ title: 'Syncing to Server...',
2203
+ msg: 'Publishing updated domain identity and sovereign delegate to server...',
2204
+ animationEmoji: '⚡'
2205
+ });
2206
+ const resSync = await getApiBridge().putEvolveKeystone({
2207
+ domainAddr: primaryTjpAddr,
2208
+ keystoneIbGib: parentWithUserDelegate,
2209
+ relatedKeystoneIbGibs: [userSyncDelegate]
2210
+ });
2211
+ if (!resSync.success) {
2212
+ throw new Error(`Server sync of password delegate failed: ${resSync.message}`);
2213
+ }
2214
+ // 4. Save parentWithUserDelegate and userSyncDelegate to local space & metaspace
2215
+ await metaspace.put({ ibGibs: [parentWithUserDelegate, userSyncDelegate], space });
2216
+ await metaspace.registerNewIbGib({ ibGib: parentWithUserDelegate, space });
2217
+ await metaspace.registerNewIbGib({ ibGib: userSyncDelegate, space });
2218
+ await updateSpecialIndex({
2219
+ type: "keystones",
2220
+ rel8nInfos: [
2221
+ {
2222
+ rel8nName: "keystone",
2223
+ ibGibs: [toDto({ ibGib: parentWithUserDelegate }), toDto({ ibGib: userSyncDelegate })]
2224
+ }
2225
+ ],
2226
+ metaspace,
2227
+ space
2228
+ });
2229
+ const evolvedAddr = getIbGibAddr({ ibGib: parentWithUserDelegate });
2230
+ // Dispatch EVENT_IBGIB_IDENTITY_CHANGED
2231
+ window.dispatchEvent(new CustomEvent(EVENT_IBGIB_IDENTITY_CHANGED, {
2232
+ detail: { activeIdentityAddr: evolvedAddr },
2233
+ bubbles: true,
2234
+ composed: true
2235
+ }));
2236
+ this.setChangePasswordStatus("Successfully added Master Password! Identity is now sovereign.", "success");
2237
+ // Reset OTP section and button text
2238
+ if (this.elements?.sectionAddPasswordOtpEl) {
2239
+ this.elements.sectionAddPasswordOtpEl.classList.add('hidden');
2240
+ if (this.elements.inputAddPasswordOtpEl) {
2241
+ this.elements.inputAddPasswordOtpEl.value = '';
2242
+ }
2243
+ }
2244
+ if (this.elements?.btnChangePasswordEl) {
2245
+ this.elements.btnChangePasswordEl.textContent = "Update Password";
2246
+ }
2247
+ // Refresh the view
2248
+ this.ibGibAddr = evolvedAddr;
2249
+ this.staticIbGib = parentWithUserDelegate;
2250
+ await this.loadIbGib({ getLatest: true });
2251
+ // Clear inputs
2252
+ this.elements.inputNewPasswordEl.value = '';
2253
+ this.elements.inputConfirmPasswordEl.value = '';
2254
+ return parentWithUserDelegate;
2255
+ }
2256
+ /**
2257
+ * Handles adding a master password to a sovereign identity that was created with passkeys only.
2258
+ * Authorizes via passkey biometric/hardware assertion and cryptographically adds the manage pool.
2259
+ */
2260
+ async handleAddPasswordToPasskeyIdentity() {
2261
+ const lc = `${this.lc}[${this.handleAddPasswordToPasskeyIdentity.name}]`;
2262
+ this.setChangePasswordStatus("Validating entries...", "info");
2263
+ try {
2264
+ const newPassword = this.elements.inputNewPasswordEl.value;
2265
+ const confirmPassword = this.elements.inputConfirmPasswordEl.value;
2266
+ if (!newPassword) {
2267
+ this.setChangePasswordStatus("Master password is required.", "error");
2268
+ return;
2269
+ }
2270
+ if (newPassword !== confirmPassword) {
2271
+ this.setChangePasswordStatus("Master password and confirmation do not match.", "error");
2272
+ return;
2273
+ }
2274
+ const passkeysMap = this.aggregatedDetails?.passkeys
2275
+ || this.ibGib?.data?.frameDetails?.passkeys
2276
+ || this.ibGib?.data?.passkeys
2277
+ || {};
2278
+ const passkeysList = Array.isArray(passkeysMap)
2279
+ ? passkeysMap
2280
+ : Object.values(passkeysMap);
2281
+ const passkeyPool = this.ibGib?.data?.challengePools?.find(p => p.id.startsWith('passkey-manage-'));
2282
+ if (!passkeyPool) {
2283
+ this.setChangePasswordStatus("No active passkey manage pool found on this identity.", "error");
2284
+ return;
2285
+ }
2286
+ let passkeySecret;
2287
+ let signingPoolId;
2288
+ try {
2289
+ const authResult = await promptForAuthoritySecret({
2290
+ keystone: this.ibGib,
2291
+ aggregatedDetails: this.aggregatedDetails,
2292
+ title: 'Authorize Master Password',
2293
+ msg: 'Please touch your fingerprint sensor, Face ID, or security key to authorize adding a Master Password...',
2294
+ verb: KEYSTONE_VERB_MANAGE
2295
+ });
2296
+ passkeySecret = authResult.secret;
2297
+ signingPoolId = authResult.poolId;
2298
+ }
2299
+ catch (err) {
2300
+ this.setChangePasswordStatus("Authorization cancelled.", "error");
2301
+ return;
2302
+ }
2303
+ this.showBusy({
2304
+ isBusy: true,
2305
+ title: 'Adding Master Password...',
2306
+ msg: 'Compiling cryptographic master password pool and evolving identity...',
2307
+ animationEmoji: '🔑'
2308
+ });
2309
+ await delay(50);
2310
+ this.setChangePasswordStatus("Generating sovereign master password challenge pool...", "info");
2311
+ const metaspace = await getGlobalMetaspace_waitIfNeeded();
2312
+ if (!metaspace)
2313
+ throw new Error("Global metaspace not available");
2314
+ const space = await metaspace.getLocalUserSpace({ lock: false });
2315
+ if (!space)
2316
+ throw new Error("Local user space not available");
2317
+ const poolConfig = {
2318
+ id: POOL_ID_MANAGE,
2319
+ type: KeystoneChallengeType.hash_reveal_v1,
2320
+ salt: POOL_ID_MANAGE,
2321
+ algo: HashAlgorithm.sha_256,
2322
+ rounds: 1,
2323
+ allowedVerbs: [KEYSTONE_VERB_MANAGE, KEYSTONE_VERB_SYNC, KEYSTONE_VERB_CONNECT],
2324
+ behavior: {
2325
+ size: 10,
2326
+ replenish: KeystoneReplenishStrategy.topUp,
2327
+ selectSequentially: 1,
2328
+ selectRandomly: 0,
2329
+ targetBindingCount: 0,
2330
+ }
2331
+ };
2332
+ const newChallenges = await generatePoolChallenges({
2333
+ config: poolConfig,
2334
+ masterSecret: newPassword,
2335
+ });
2336
+ const newManagePool = {
2337
+ id: POOL_ID_MANAGE,
2338
+ config: poolConfig,
2339
+ challenges: newChallenges,
2340
+ };
2341
+ const primaryTjpAddr = (this.ibGib ? getTjpAddr({ ibGib: this.ibGib }) : undefined)
2342
+ || (this.ibGibAddr ? (() => {
2343
+ const { ib } = getIbAndGib({ ibGibAddr: this.ibGibAddr });
2344
+ const { tjpGib } = getGibInfo({ ibGibAddr: this.ibGibAddr });
2345
+ return tjpGib && ib ? `${ib}^${tjpGib}` : this.ibGibAddr;
2346
+ })() : undefined)
2347
+ || getIbGibAddr({ ibGib: this.ibGib });
2348
+ const keystoneService = new KeystoneService_V1();
2349
+ const evolvedParent = await keystoneService.sign({
2350
+ latestKeystone: this.ibGib,
2351
+ claim: {
2352
+ target: primaryTjpAddr,
2353
+ verb: KEYSTONE_VERB_MANAGE
2354
+ },
2355
+ masterSecret: passkeySecret,
2356
+ poolId: signingPoolId,
2357
+ dataToPatch: {
2358
+ challengePools: [...(this.ibGib.data.challengePools || []), newManagePool]
2359
+ },
2360
+ metaspace,
2361
+ space
2362
+ });
2363
+ // Sync pool addition to server
2364
+ this.showBusy({
2365
+ isBusy: true,
2366
+ title: 'Syncing to Server...',
2367
+ msg: 'Publishing updated identity to server...',
2368
+ animationEmoji: '⚡'
2369
+ });
2370
+ const resSync = await getApiBridge().putEvolveKeystone({
2371
+ domainAddr: primaryTjpAddr,
2372
+ keystoneIbGib: evolvedParent,
2373
+ relatedKeystoneIbGibs: []
2374
+ });
2375
+ if (!resSync.success) {
2376
+ throw new Error(`Server sync failed: ${resSync.message}`);
2377
+ }
2378
+ // Save locally
2379
+ await metaspace.put({ ibGibs: [evolvedParent], space });
2380
+ await metaspace.registerNewIbGib({ ibGib: evolvedParent, space });
2381
+ // Delegate registration & final sync via DRY helper
2382
+ await this.registerAndSyncSovereignPasswordDelegate({
2383
+ evolvedParent,
2384
+ newPassword,
2385
+ primaryTjpAddr,
2386
+ metaspace,
2387
+ space
2388
+ });
2389
+ }
2390
+ catch (error) {
2391
+ console.error(`${lc} Failed to add password to passkey identity: ${extractErrorMsg(error)}`, error);
2392
+ this.setChangePasswordStatus(`Failed to add master password: ${extractErrorMsg(error)}`, "error");
1445
2393
  }
1446
2394
  finally {
1447
2395
  this.showBusy({ isBusy: false });
@@ -1449,13 +2397,27 @@ export class KeystoneDetailsComponentInstance extends IbGibDynamicComponentInsta
1449
2397
  }
1450
2398
  async handleChangePassword() {
1451
2399
  const lc = `${this.lc}[${this.handleChangePassword.name}]`;
1452
- const caps = getKeystoneCapabilities({ keystone: this.ibGib, aggregatedDetails: this.aggregatedDetails });
1453
- const isCustodialOnly = caps.isCustodial && !caps.isSovereign;
1454
- if (isCustodialOnly) {
1455
- await this.handleAddMasterPassword();
2400
+ const caps = !!this.ibGib ?
2401
+ getKeystoneCapabilities({ keystone: this.ibGib, aggregatedDetails: this.aggregatedDetails }) :
2402
+ undefined;
2403
+ const hasPassword = !!caps?.hasPassword;
2404
+ if (!hasPassword) {
2405
+ const isCustodialOnly = !!caps ? caps.isCustodial && !caps.isSovereign : false;
2406
+ if (isCustodialOnly) {
2407
+ await this.handleAddMasterPassword();
2408
+ }
2409
+ else {
2410
+ await this.handleAddPasswordToPasskeyIdentity();
2411
+ }
1456
2412
  return;
1457
2413
  }
1458
2414
  this.setChangePasswordStatus("Validating entries...", "info");
2415
+ this.showBusy({
2416
+ isBusy: true,
2417
+ title: 'Changing Master Password...',
2418
+ msg: 'Validating current password...',
2419
+ animationEmoji: '🔐'
2420
+ });
1459
2421
  try {
1460
2422
  const currentPassword = this.elements.inputCurrentPasswordEl.value;
1461
2423
  const newPassword = this.elements.inputNewPasswordEl.value;
@@ -1483,6 +2445,12 @@ export class KeystoneDetailsComponentInstance extends IbGibDynamicComponentInsta
1483
2445
  this.setChangePasswordStatus("Incorrect current password.", "error");
1484
2446
  return;
1485
2447
  }
2448
+ this.showBusy({
2449
+ isBusy: true,
2450
+ title: 'Evolving Master Password...',
2451
+ msg: 'Generating new challenge proofs and rotating local user delegates...',
2452
+ animationEmoji: '⚙️'
2453
+ });
1486
2454
  this.setChangePasswordStatus("Evolving primary identity keystone...", "info");
1487
2455
  const metaspace = await getGlobalMetaspace_waitIfNeeded();
1488
2456
  if (!metaspace) {
@@ -1492,8 +2460,7 @@ export class KeystoneDetailsComponentInstance extends IbGibDynamicComponentInsta
1492
2460
  if (!space) {
1493
2461
  throw new Error(`(UNEXPECTED) couldn't get default local space? (E: 2fc1281c28351fd378a1d7f88eaa0526)`);
1494
2462
  }
1495
- // 2. Rotate parent identity
1496
- debugger; // walk through this...looking for delegate failure // hmm...I thought we got change password going?
2463
+ // 2. Rotate parent identity and sovereign sync delegates
1497
2464
  const { newKeystone: evolvedParent, newDelegates: evolvedDelegates } = await keystoneService.changePassword({
1498
2465
  keystoneIbGib: this.ibGib,
1499
2466
  newMasterSecret: newPassword,
@@ -1502,56 +2469,21 @@ export class KeystoneDetailsComponentInstance extends IbGibDynamicComponentInsta
1502
2469
  metaspace,
1503
2470
  space
1504
2471
  });
1505
- debugger; // look at evolvedDelegates
1506
- this.setChangePasswordStatus("Resolving and rotating delegate keystones...", "info");
1507
- // // 3. Evolve registered delegate keystones (preserving custodian/foreign delegate secrets)
1508
- // const delegates = this.ibGib!.data?.delegates || {};
1509
- // const evolvedDelegates: KeystoneIbGib_V1[] = [];
1510
- // for (const delegateInfo of Object.values(delegates)) {
1511
- // const delegateAddr = delegateInfo.delegateAddr;
1512
- // const delegateKeystone = await keystoneService.getLatestKeystone({
1513
- // addr: delegateAddr,
1514
- // metaspace,
1515
- // space,
1516
- // });
1517
- // // Identify user-owned challenge pools in the delegate that match the old password
1518
- // const poolsToRotate: string[] = [];
1519
- // for (const pool of delegateKeystone.data?.challengePools ?? []) {
1520
- // if (!pool.isForeign) {
1521
- // /**
1522
- // * WARNING: There is an extreme edge case that if two pools
1523
- // * happen to be owned by different parties but share the
1524
- // * same secret, this will change the wrong pool's secret
1525
- // * also. For V1, this shouldn't matter. But we need a
1526
- // * better way of tracking pool ownership for V2 (or even
1527
- // * V1 if we have multi-party support). (DO NOT REMOVE
1528
- // * THIS COMMENT UNTIL THIS IS ADDRESSED)
1529
- // */
1530
- // const isPoolMatch = await keystoneService.verifySigningSecret({
1531
- // keystoneIbGib: delegateKeystone,
1532
- // signingSecret: currentPassword,
1533
- // poolId: pool.id
1534
- // });
1535
- // if (isPoolMatch) { poolsToRotate.push(pool.id); }
1536
- // }
1537
- // }
1538
- // if (poolsToRotate.length > 0) {
1539
- // console.log(`${lc} Rotating pools in delegate ${delegateInfo.delegateTjpAddr}: ${poolsToRotate.join(', ')}`);
1540
- // const {newKeystone: evolvedDelegate} = await keystoneService.changePassword({
1541
- // keystoneIbGib: delegateKeystone,
1542
- // newMasterSecret: newPassword,
1543
- // signingSecret: currentPassword,
1544
- // signingPoolId: 'manage',
1545
- // poolIds: poolsToRotate,
1546
- // metaspace,
1547
- // space
1548
- // });
1549
- // evolvedDelegates.push(evolvedDelegate);
1550
- // }
1551
- // }
1552
- // 4. Sync evolved parent and all evolved delegate keystones atomically
2472
+ // 3. Sync evolved parent and all evolved delegate keystones atomically
2473
+ this.showBusy({
2474
+ isBusy: true,
2475
+ title: 'Syncing to Server...',
2476
+ msg: 'Publishing rotated identity and delegates to server...',
2477
+ animationEmoji: '⚡'
2478
+ });
1553
2479
  this.setChangePasswordStatus("Syncing evolved keystones to server...", "info");
1554
- const parentTjpAddr = getIbGibAddr({ ibGib: this.ibGib }); // is this.ibGib the tjp always? or is it the latest in the keystone line? Does it matter?
2480
+ const parentTjpAddr = (this.ibGib ? getTjpAddr({ ibGib: this.ibGib }) : undefined)
2481
+ || (this.ibGibAddr ? (() => {
2482
+ const { ib } = getIbAndGib({ ibGibAddr: this.ibGibAddr });
2483
+ const { tjpGib } = getGibInfo({ ibGibAddr: this.ibGibAddr });
2484
+ return tjpGib && ib ? `${ib}^${tjpGib}` : this.ibGibAddr;
2485
+ })() : undefined)
2486
+ || getIbGibAddr({ ibGib: this.ibGib });
1555
2487
  const syncRes = await getApiBridge().putEvolveKeystone({
1556
2488
  domainAddr: parentTjpAddr,
1557
2489
  keystoneIbGib: evolvedParent,
@@ -1560,7 +2492,7 @@ export class KeystoneDetailsComponentInstance extends IbGibDynamicComponentInsta
1560
2492
  if (!syncRes.success) {
1561
2493
  throw new Error(`Server synchronization rejected: ${syncRes.message}`);
1562
2494
  }
1563
- // 5. Update index locally
2495
+ // 4. Update index locally
1564
2496
  this.setChangePasswordStatus("Updating local indexes...", "info");
1565
2497
  const newKeystoneIbGibs = [toDto({ ibGib: evolvedParent })];
1566
2498
  if (evolvedDelegates) {
@@ -1585,12 +2517,18 @@ export class KeystoneDetailsComponentInstance extends IbGibDynamicComponentInsta
1585
2517
  this.elements.inputNewPasswordEl.value = "";
1586
2518
  this.elements.inputConfirmPasswordEl.value = "";
1587
2519
  this.setChangePasswordStatus("Password updated successfully!", "success");
2520
+ this.ibGibAddr = getIbGibAddr({ ibGib: evolvedParent });
2521
+ this.staticIbGib = evolvedParent;
1588
2522
  await this.loadIbGib({ getLatest: true });
1589
2523
  }
1590
2524
  catch (error) {
2525
+ debugger; // error in handleChangePassword
1591
2526
  console.error(`${lc} Password rotation failed: ${extractErrorMsg(error)}`);
1592
2527
  this.setChangePasswordStatus(`Update failed: ${extractErrorMsg(error)}`, "error");
1593
2528
  }
2529
+ finally {
2530
+ this.showBusy({ isBusy: false });
2531
+ }
1594
2532
  }
1595
2533
  }
1596
2534
  //# sourceMappingURL=keystone-details.mjs.map