@ibgib/space-gib 0.0.7 → 0.0.9

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.
@@ -19,11 +19,13 @@ import {
19
19
  import { EVENT_IBGIB_IDENTITY_REQUEST_CHANGE, EVENT_IBGIB_IDENTITY_CHANGED } from "@ibgib/web-gib/dist/ui/ui-constants.mjs";
20
20
  import { spaceGibApiBridge } from "../../api/space-gib-api-bridge.mjs";
21
21
  import { updateSpecialIndex } from "@ibgib/core-gib/dist/timeline/timeline-api.mjs";
22
- import { toDto } from "@ibgib/core-gib/dist/common/other/ibgib-helper.mjs";
22
+ import { getTjpAddr, toDto } from "@ibgib/core-gib/dist/common/other/ibgib-helper.mjs";
23
23
 
24
24
  import { GLOBAL_LOG_A_LOT, APP_CONFIG, } from "../../constants.mjs";
25
25
  import { getComponentCtorArg, getIbGibGlobalThis_SpaceGib } from "../../helpers.web.mjs";
26
26
  import { devLog } from "../../dev-tools.mjs";
27
+ import { promptForSecret } from "@ibgib/web-gib/dist/helpers.web.mjs";
28
+ import { isSsoProviderLinked } from "@ibgib/web-gib/dist/identity/sso/sso-helpers.mjs";
27
29
 
28
30
  const logalot = GLOBAL_LOG_A_LOT || true;
29
31
 
@@ -84,9 +86,15 @@ interface KeystoneDetailsElements extends ElementsBase {
84
86
  historicalWarningBannerEl: HTMLDivElement;
85
87
  containerEl: HTMLDivElement;
86
88
  ssoLinkCardEl: HTMLDivElement;
87
- btnLinkGoogleEl: HTMLButtonElement;
88
- btnLinkGithubEl: HTMLButtonElement;
89
+ btnLinkGoogleEl: HTMLDivElement;
90
+ btnLinkGithubEl: HTMLDivElement;
89
91
  ssoStatusMsgEl: HTMLDivElement;
92
+ inputCurrentPassphraseEl: HTMLInputElement;
93
+ inputNewPassphraseEl: HTMLInputElement;
94
+ inputConfirmPassphraseEl: HTMLInputElement;
95
+ btnChangePassphraseEl: HTMLButtonElement;
96
+ changePassphraseStatusEl: HTMLDivElement;
97
+ changePassphraseCardEl: HTMLDivElement;
90
98
  }
91
99
 
92
100
  export class KeystoneDetailsComponentInstance
@@ -190,9 +198,15 @@ export class KeystoneDetailsComponentInstance
190
198
  this.elements.containerEl = shadow.getElementById('container') as HTMLDivElement;
191
199
  this.elements.historicalWarningBannerEl = shadow.getElementById('historical-warning-banner') as HTMLDivElement;
192
200
  this.elements.ssoLinkCardEl = shadow.getElementById('sso-link-card') as HTMLDivElement;
193
- this.elements.btnLinkGoogleEl = shadow.getElementById('btn-link-google') as HTMLButtonElement;
194
- this.elements.btnLinkGithubEl = shadow.getElementById('btn-link-github') as HTMLButtonElement;
201
+ this.elements.btnLinkGoogleEl = shadow.getElementById('btn-link-google') as HTMLDivElement;
202
+ this.elements.btnLinkGithubEl = shadow.getElementById('btn-link-github') as HTMLDivElement;
195
203
  this.elements.ssoStatusMsgEl = shadow.getElementById('sso-status-msg') as HTMLDivElement;
204
+ this.elements.inputCurrentPassphraseEl = shadow.getElementById('input-current-passphrase') as HTMLInputElement;
205
+ this.elements.inputNewPassphraseEl = shadow.getElementById('input-new-passphrase') as HTMLInputElement;
206
+ this.elements.inputConfirmPassphraseEl = shadow.getElementById('input-confirm-passphrase') as HTMLInputElement;
207
+ this.elements.btnChangePassphraseEl = shadow.getElementById('btn-change-passphrase') as HTMLButtonElement;
208
+ this.elements.changePassphraseStatusEl = shadow.getElementById('change-passphrase-status') as HTMLDivElement;
209
+ this.elements.changePassphraseCardEl = shadow.getElementById('change-passphrase-card') as HTMLDivElement;
196
210
 
197
211
  // ElementBase requires contentEl
198
212
  this.elements.contentEl = shadow.getElementById('identity-details-view') as HTMLDivElement;
@@ -229,8 +243,9 @@ export class KeystoneDetailsComponentInstance
229
243
  // Buttons
230
244
  this.elements!.btnVerifyChainEl.addEventListener('click', () => this.handleVerifyIdentityChain());
231
245
  this.elements!.btnSetActiveEl.addEventListener('click', () => this.handleSetActiveIdentity());
232
- this.elements!.btnLinkGoogleEl.addEventListener('click', () => this.handleSSOLink('google'));
233
- this.elements!.btnLinkGithubEl.addEventListener('click', () => this.handleSSOLink('github'));
246
+ this.elements!.btnLinkGoogleEl.addEventListener('click', () => this.handleSSOClick('google'));
247
+ this.elements!.btnLinkGithubEl.addEventListener('click', () => this.handleSSOClick('github'));
248
+ this.elements!.btnChangePassphraseEl.addEventListener('click', () => this.handleChangePassphrase());
234
249
  }
235
250
 
236
251
  private toggleAccordion() {
@@ -294,14 +309,23 @@ export class KeystoneDetailsComponentInstance
294
309
  throw new Error(`(UNEXPECTED) space falsy and we couldn't get default local user space from metaspace? (E: fa298edcecbda889cfd7b228fbd57c26)`);
295
310
  }
296
311
 
297
- const resGet = await metaspace.get({ addrs: [this.ibGibAddr], space });
312
+ let loadAddr = this.ibGibAddr;
313
+ if (opts?.getLatest) {
314
+ const latest = await metaspace.getLatestAddr({ addr: this.ibGibAddr, space });
315
+ if (latest) {
316
+ loadAddr = latest;
317
+ this.ibGibAddr = latest;
318
+ }
319
+ }
320
+
321
+ const resGet = await metaspace.get({ addrs: [loadAddr], space });
298
322
 
299
323
  if (resGet.success && resGet.ibGibs && resGet.ibGibs.length === 1) {
300
324
  this.staticIbGib = resGet.ibGibs[0] as KeystoneIbGib_V1;
301
325
  } else {
302
326
  const resIbGib = resGet.rawResultIbGib as any;
303
327
  const addrsNotFound = resIbGib?.data?.addrsNotFound ?? 'unknown';
304
- throw new Error(`couldn't find address ${this.ibGibAddr}. addrsNotFound: ${addrsNotFound}? resGet.errorMsg: ${resGet.errorMsg} (E: e0e5c9b7de2837bc902fa88390cd9f26)`);
328
+ throw new Error(`couldn't find address ${loadAddr}. addrsNotFound: ${addrsNotFound}? resGet.errorMsg: ${resGet.errorMsg} (E: e0e5c9b7de2837bc902fa88390cd9f26)`);
305
329
  }
306
330
 
307
331
  await this.updateFrameDetailsView();
@@ -348,9 +372,43 @@ export class KeystoneDetailsComponentInstance
348
372
  if (this.isHistoricalFrame) {
349
373
  this.elements.btnSetActiveEl.disabled = true;
350
374
  this.elements.btnVerifyChainEl.disabled = true;
375
+ this.elements.btnLinkGoogleEl.classList.add('disabled');
376
+ this.elements.btnLinkGithubEl.classList.add('disabled');
351
377
  } else {
352
378
  this.elements.btnVerifyChainEl.disabled = false;
353
379
  this.elements.btnSetActiveEl.disabled = isTimelineActive;
380
+ this.elements.btnLinkGoogleEl.classList.remove('disabled');
381
+ this.elements.btnLinkGithubEl.classList.remove('disabled');
382
+
383
+ // Check if already linked
384
+ const isGoogleLinked = isSsoProviderLinked({ keystone: this.ibGib, providerId: 'google' });
385
+ const isGithubLinked = isSsoProviderLinked({ keystone: this.ibGib, providerId: 'github' });
386
+
387
+ // Google Card States
388
+ const googleCard = this.elements.btnLinkGoogleEl;
389
+ const googleLabel = googleCard.querySelector('.toggle-state-text')!;
390
+ if (isGoogleLinked) {
391
+ googleCard.classList.remove('unlinked');
392
+ googleCard.classList.add('linked');
393
+ googleLabel.textContent = "Google Linked ✅";
394
+ } else {
395
+ googleCard.classList.remove('linked');
396
+ googleCard.classList.add('unlinked');
397
+ googleLabel.textContent = "Unlinked";
398
+ }
399
+
400
+ // GitHub Card States
401
+ const githubCard = this.elements.btnLinkGithubEl;
402
+ const githubLabel = githubCard.querySelector('.toggle-state-text')!;
403
+ if (isGithubLinked) {
404
+ githubCard.classList.remove('unlinked');
405
+ githubCard.classList.add('linked');
406
+ githubLabel.textContent = "GitHub Linked ✅";
407
+ } else {
408
+ githubCard.classList.remove('linked');
409
+ githubCard.classList.add('unlinked');
410
+ githubLabel.textContent = "Unlinked";
411
+ }
354
412
  }
355
413
  }
356
414
 
@@ -459,15 +517,17 @@ export class KeystoneDetailsComponentInstance
459
517
  // Render Challenge Pools
460
518
  this.renderChallengePools(keystone.data?.challengePools || []);
461
519
 
462
- // Render Delegates & SSO link card (only if this is a primary keystone, hide delegates table & SSO card for delegate/session keystones)
520
+ // Render Delegates, SSO link, and Passphrase Change cards (only if this is a primary keystone, hide them for delegate/session keystones)
463
521
  const details = await this.getKeystoneDetails(this.ibGibAddr);
464
522
  if (details.isPrimary) {
465
523
  this.elements.delegatesCardEl.classList.remove('hidden');
466
524
  await this.renderDelegates(keystone.data?.delegates);
467
525
  this.elements.ssoLinkCardEl.classList.remove('hidden');
526
+ this.elements.changePassphraseCardEl.classList.remove('hidden');
468
527
  } else {
469
528
  this.elements.delegatesCardEl.classList.add('hidden');
470
529
  this.elements.ssoLinkCardEl.classList.add('hidden');
530
+ this.elements.changePassphraseCardEl.classList.add('hidden');
471
531
  }
472
532
 
473
533
  this.updateActiveButtonState();
@@ -597,10 +657,26 @@ export class KeystoneDetailsComponentInstance
597
657
  const btnView = document.createElement('button');
598
658
  btnView.className = 'action-btn secondary small';
599
659
  btnView.textContent = 'View Details';
600
- btnView.addEventListener('click', () => {
660
+ btnView.addEventListener('click', async () => {
661
+ const lc = `${this.lc}[renderDelegates.click]`;
601
662
  if (info.delegateAddr) {
663
+ let addrToLoad = info.delegateAddr;
664
+ const metaspace = getIbGibGlobalThis_SpaceGib(APP_CONFIG).metaspace;
665
+ if (metaspace) {
666
+ const space = await metaspace.getLocalUserSpace({ lock: false });
667
+ if (space) {
668
+ try {
669
+ const latest = await metaspace.getLatestAddr({ addr: delegateTjpAddr, space });
670
+ if (latest) {
671
+ addrToLoad = latest;
672
+ }
673
+ } catch (err) {
674
+ console.warn(`${lc} Error resolving latest delegate address: ${extractErrorMsg(err)}`);
675
+ }
676
+ }
677
+ }
602
678
  this.dispatchEvent(new CustomEvent('ibgib-view-keystone-details', {
603
- detail: { addr: info.delegateAddr },
679
+ detail: { addr: addrToLoad },
604
680
  bubbles: true,
605
681
  composed: true
606
682
  }));
@@ -687,7 +763,7 @@ export class KeystoneDetailsComponentInstance
687
763
 
688
764
  // 2. Generate a secure, high-entropy user nonce
689
765
  const userNonce = (Math.random().toString(36).substring(2) + Math.random().toString(36).substring(2)).substring(0, 32);
690
-
766
+
691
767
  // 3. Construct state payload
692
768
  const stateObj = {
693
769
  providerId,
@@ -710,16 +786,30 @@ export class KeystoneDetailsComponentInstance
710
786
  }
711
787
 
712
788
  // 6. Listen for callback from pop-up
789
+ let checkClosedInterval: any;
713
790
  const messageListener = async (event: MessageEvent) => {
714
791
  if (event.origin !== window.location.origin) return;
715
792
  const callbackData = event.data;
716
793
  if (callbackData && callbackData.type === 'sso-oauth-callback') {
717
794
  window.removeEventListener('message', messageListener);
795
+ clearInterval(checkClosedInterval);
718
796
  await this.handleSSOLinkCallback(callbackData.code, stateObj, providerConfig.redirectUri);
719
797
  }
720
798
  };
721
799
  window.addEventListener('message', messageListener);
722
800
 
801
+ // 7. Poll for manual closure of the popup window
802
+ checkClosedInterval = setInterval(() => {
803
+ if (popup.closed) {
804
+ clearInterval(checkClosedInterval);
805
+ window.removeEventListener('message', messageListener);
806
+ const statusEl = this.elements!.ssoStatusMsgEl;
807
+ if (statusEl.textContent && statusEl.textContent.startsWith('Linking')) {
808
+ this.setSSOStatus('Linking cancelled (window closed).', 'error');
809
+ }
810
+ }
811
+ }, 500);
812
+
723
813
  } catch (error) {
724
814
  console.error(`${lc} SSO link initiation failed: ${extractErrorMsg(error)}`);
725
815
  this.setSSOStatus(`Link failed: ${extractErrorMsg(error)}`, "error");
@@ -762,10 +852,47 @@ export class KeystoneDetailsComponentInstance
762
852
 
763
853
  const custodianPool = linkData.custodianPool;
764
854
 
765
- // 2. Prompt user for master secret to evolve locally
766
- const masterSecret = prompt("Link successful! Enter your local Master Secret (Passphrase) to sign and register the custodian pool:");
767
- if (!masterSecret) {
768
- this.setSSOStatus("Link cancelled by user.", "info");
855
+ // 2. Prompt user for master secret and verify it
856
+ const keystoneService = new KeystoneService_V1();
857
+ let masterSecret: string | undefined;
858
+ let isCorrect = false;
859
+ let tries = 0;
860
+ const maxTries = 8;
861
+
862
+ while (tries < maxTries && !isCorrect) {
863
+ try {
864
+ const promptMsg = tries === 0
865
+ ? "Link successful! Enter your local Master Secret (Passphrase) to sign and register the custodian pool"
866
+ : `Incorrect passphrase. Please try again (${maxTries - tries} tries remaining):`;
867
+ masterSecret = await promptForSecret({
868
+ msg: promptMsg,
869
+ confirm: false
870
+ });
871
+ } catch (error) {
872
+ // cancelled
873
+ if (logalot) { console.log(`${lc}[promptForSecret] user cancelled (I: 911ff1fb0234949228481968e9087626)`); }
874
+ masterSecret = undefined;
875
+ break;
876
+ }
877
+
878
+ if (!masterSecret) {
879
+ break;
880
+ }
881
+
882
+ // Verify secret using verifySigningSecret (requires manage poolId)
883
+ isCorrect = await keystoneService.verifySigningSecret({
884
+ keystoneIbGib: this.ibGib!,
885
+ signingSecret: masterSecret,
886
+ poolId: 'manage'
887
+ });
888
+
889
+ if (!isCorrect) {
890
+ tries++;
891
+ }
892
+ }
893
+
894
+ if (!isCorrect) {
895
+ this.setSSOStatus("Link cancelled or failed (incorrect passphrase).", "error");
769
896
  return;
770
897
  }
771
898
 
@@ -774,13 +901,12 @@ export class KeystoneDetailsComponentInstance
774
901
  const metaspace = getIbGibGlobalThis_SpaceGib(APP_CONFIG).metaspace;
775
902
  if (!metaspace) throw new Error("Metaspace not found");
776
903
  const space = await metaspace.getLocalUserSpace({ lock: false });
777
- if (!space) throw new Error("Local space not found");
904
+ if (!space) { throw new Error("Local space not found"); }
778
905
 
779
906
  // 3. Evolve parent keystone to include the custodian pool
780
- const keystoneService = new KeystoneService_V1();
781
907
  const evolvedParent = await keystoneService.addPools({
782
908
  latestKeystone: this.ibGib!,
783
- masterSecret,
909
+ masterSecret: masterSecret!,
784
910
  newPools: [custodianPool],
785
911
  metaspace,
786
912
  space
@@ -788,11 +914,11 @@ export class KeystoneDetailsComponentInstance
788
914
 
789
915
  // 4. Sync evolved parent keystone back to server
790
916
  this.setSSOStatus("Publishing evolved identity keystone to server...", "info");
791
- const syncRes = await spaceGibApiBridge.putEvolveKeystone(
792
- stateObj.parentTjpAddr,
793
- evolvedParent,
794
- [] // No outward delegate timelines
795
- );
917
+ const syncRes = await spaceGibApiBridge.putEvolveKeystone({
918
+ domainAddr: stateObj.parentTjpAddr,
919
+ keystoneIbGib: evolvedParent,
920
+ relatedKeystoneIbGibs: [] // No outward delegate timelines
921
+ });
796
922
  if (!syncRes.success) {
797
923
  throw new Error(`Server synchronization rejected: ${syncRes.message}`);
798
924
  }
@@ -818,4 +944,270 @@ export class KeystoneDetailsComponentInstance
818
944
  this.setSSOStatus(`Link failed: ${extractErrorMsg(error)}`, "error");
819
945
  }
820
946
  }
947
+
948
+ protected async handleSSOClick(providerId: 'google' | 'github') {
949
+ const isLinked = isSsoProviderLinked({ keystone: this.ibGib, providerId });
950
+ if (isLinked) {
951
+ await this.handleSSOUnlink(providerId);
952
+ } else {
953
+ await this.handleSSOLink(providerId);
954
+ }
955
+ }
956
+
957
+ protected async handleSSOUnlink(providerId: 'google' | 'github') {
958
+ const lc = `${this.lc}[${this.handleSSOUnlink.name}]`;
959
+ this.setSSOStatus(`Unlinking ${providerId}...`, "info");
960
+
961
+ try {
962
+ // 1. Prompt user for passphrase
963
+ const keystoneService = new KeystoneService_V1();
964
+ let masterSecret: string | undefined;
965
+ let isCorrect = false;
966
+ let tries = 0;
967
+ const maxTries = 8;
968
+
969
+ while (tries < maxTries && !isCorrect) {
970
+ try {
971
+ const promptMsg = tries === 0
972
+ ? `Are you sure you want to unlink your ${providerId} account? If so, enter your local Master Secret (Passphrase) to authorize. Otherwise, click Cancel.`
973
+ : `Incorrect passphrase. Please try again (${maxTries - tries} tries remaining):`;
974
+ masterSecret = await promptForSecret({
975
+ msg: promptMsg,
976
+ confirm: false
977
+ });
978
+ } catch (error) {
979
+ masterSecret = undefined;
980
+ break;
981
+ }
982
+
983
+ if (!masterSecret) {
984
+ break;
985
+ }
986
+
987
+ // Verify secret using verifySigningSecret (requires manage poolId)
988
+ isCorrect = await keystoneService.verifySigningSecret({
989
+ keystoneIbGib: this.ibGib!,
990
+ signingSecret: masterSecret,
991
+ poolId: 'manage'
992
+ });
993
+
994
+ if (!isCorrect) {
995
+ tries++;
996
+ }
997
+ }
998
+
999
+ if (!isCorrect) {
1000
+ this.setSSOStatus("Unlink cancelled or failed (incorrect passphrase).", "error");
1001
+ return;
1002
+ }
1003
+
1004
+ this.setSSOStatus("Evolving identity locally and removing custodian pool...", "info");
1005
+
1006
+ const metaspace = getIbGibGlobalThis_SpaceGib(APP_CONFIG).metaspace;
1007
+ if (!metaspace) throw new Error("Metaspace not found");
1008
+ const space = await metaspace.getLocalUserSpace({ lock: false });
1009
+ if (!space) { throw new Error("Local space not found"); }
1010
+
1011
+ // 2. Evolve parent keystone to remove the custodian pool
1012
+ const targetPoolId = `custodian-manage-${providerId}`;
1013
+ const evolvedParent = await keystoneService.removePools({
1014
+ latestKeystone: this.ibGib!,
1015
+ masterSecret: masterSecret!,
1016
+ poolIds: [targetPoolId],
1017
+ metaspace,
1018
+ space
1019
+ });
1020
+
1021
+ // 3. Sync evolved parent keystone back to server
1022
+ this.setSSOStatus("Publishing evolved identity keystone to server...", "info");
1023
+ const syncRes = await spaceGibApiBridge.putEvolveKeystone({
1024
+ domainAddr: getIbGibAddr({ ibGib: this.ibGib! }),
1025
+ keystoneIbGib: evolvedParent,
1026
+ relatedKeystoneIbGibs: [] // No outward delegate timelines
1027
+ });
1028
+ if (!syncRes.success) {
1029
+ throw new Error(`Server synchronization rejected: ${syncRes.message}`);
1030
+ }
1031
+
1032
+ // 4. Update index locally
1033
+ await updateSpecialIndex({
1034
+ type: "keystones",
1035
+ rel8nInfos: [
1036
+ {
1037
+ rel8nName: "keystone",
1038
+ ibGibs: [toDto({ ibGib: evolvedParent })]
1039
+ }
1040
+ ],
1041
+ metaspace,
1042
+ space
1043
+ });
1044
+
1045
+ this.setSSOStatus(`Successfully unlinked ${providerId}!`, "success");
1046
+ await this.loadIbGib({ getLatest: true });
1047
+
1048
+ } catch (error) {
1049
+ console.error(`${lc} SSO unlink failed: ${extractErrorMsg(error)}`);
1050
+ this.setSSOStatus(`Unlink failed: ${extractErrorMsg(error)}`, "error");
1051
+ }
1052
+ }
1053
+
1054
+ private setChangePassphraseStatus(msg: string, type: 'info' | 'success' | 'error') {
1055
+ const el = this.elements!.changePassphraseStatusEl;
1056
+ el.textContent = msg;
1057
+ el.className = `status-msg ${type}`;
1058
+ el.classList.remove('hidden');
1059
+ }
1060
+
1061
+ protected async handleChangePassphrase(): Promise<void> {
1062
+ const lc = `${this.lc}[${this.handleChangePassphrase.name}]`;
1063
+ this.setChangePassphraseStatus("Validating entries...", "info");
1064
+
1065
+ try {
1066
+ const currentPassphrase = this.elements!.inputCurrentPassphraseEl.value;
1067
+ const newPassphrase = this.elements!.inputNewPassphraseEl.value;
1068
+ const confirmPassphrase = this.elements!.inputConfirmPassphraseEl.value;
1069
+
1070
+ if (!currentPassphrase) {
1071
+ this.setChangePassphraseStatus("Current passphrase is required.", "error");
1072
+ return;
1073
+ }
1074
+ if (!newPassphrase) {
1075
+ this.setChangePassphraseStatus("New passphrase is required.", "error");
1076
+ return;
1077
+ }
1078
+ if (newPassphrase !== confirmPassphrase) {
1079
+ this.setChangePassphraseStatus("New passphrase and confirmation do not match.", "error");
1080
+ return;
1081
+ }
1082
+
1083
+ const keystoneService = new KeystoneService_V1();
1084
+
1085
+ // 1. Verify current passphrase against parent manage pool
1086
+ const isMatch = await keystoneService.verifySigningSecret({
1087
+ keystoneIbGib: this.ibGib!,
1088
+ signingSecret: currentPassphrase,
1089
+ poolId: 'manage'
1090
+ });
1091
+ if (!isMatch) {
1092
+ this.setChangePassphraseStatus("Incorrect current passphrase.", "error");
1093
+ return;
1094
+ }
1095
+
1096
+ this.setChangePassphraseStatus("Evolving primary identity keystone...", "info");
1097
+
1098
+ const metaspace = getIbGibGlobalThis_SpaceGib(APP_CONFIG).metaspace;
1099
+ if (!metaspace) throw new Error("Metaspace not found");
1100
+ const space = await metaspace.getLocalUserSpace({ lock: false });
1101
+ if (!space) { throw new Error("Local space not found"); }
1102
+
1103
+ // 2. Rotate parent identity
1104
+ debugger; // walk through this...looking for delegate failure
1105
+ const { newKeystone: evolvedParent, newDelegates: evolvedDelegates } = await keystoneService.changePassword({
1106
+ keystoneIbGib: this.ibGib!,
1107
+ newMasterSecret: newPassphrase,
1108
+ signingSecret: currentPassphrase,
1109
+ signingPoolId: 'manage',
1110
+ metaspace,
1111
+ space
1112
+ });
1113
+ debugger; // look at evolvedDelegates
1114
+
1115
+ this.setChangePassphraseStatus("Resolving and rotating delegate keystones...", "info");
1116
+
1117
+ // // 3. Evolve registered delegate keystones (preserving custodian/foreign delegate secrets)
1118
+ // const delegates = this.ibGib!.data?.delegates || {};
1119
+ // const evolvedDelegates: KeystoneIbGib_V1[] = [];
1120
+
1121
+ // for (const delegateInfo of Object.values(delegates)) {
1122
+ // const delegateAddr = delegateInfo.delegateAddr;
1123
+ // const delegateKeystone = await keystoneService.getLatestKeystone({
1124
+ // addr: delegateAddr,
1125
+ // metaspace,
1126
+ // space,
1127
+ // });
1128
+
1129
+ // // Identify user-owned challenge pools in the delegate that match the old passphrase
1130
+ // const poolsToRotate: string[] = [];
1131
+ // for (const pool of delegateKeystone.data?.challengePools ?? []) {
1132
+ // if (!pool.isForeign) {
1133
+ // /**
1134
+ // * WARNING: There is an extreme edge case that if two pools
1135
+ // * happen to be owned by different parties but share the
1136
+ // * same secret, this will change the wrong pool's secret
1137
+ // * also. For V1, this shouldn't matter. But we need a
1138
+ // * better way of tracking pool ownership for V2 (or even
1139
+ // * V1 if we have multi-party support). (DO NOT REMOVE
1140
+ // * THIS COMMENT UNTIL THIS IS ADDRESSED)
1141
+ // */
1142
+ // const isPoolMatch = await keystoneService.verifySigningSecret({
1143
+ // keystoneIbGib: delegateKeystone,
1144
+ // signingSecret: currentPassphrase,
1145
+ // poolId: pool.id
1146
+ // });
1147
+ // if (isPoolMatch) { poolsToRotate.push(pool.id); }
1148
+ // }
1149
+ // }
1150
+
1151
+ // if (poolsToRotate.length > 0) {
1152
+ // console.log(`${lc} Rotating pools in delegate ${delegateInfo.delegateTjpAddr}: ${poolsToRotate.join(', ')}`);
1153
+ // const {newKeystone: evolvedDelegate} = await keystoneService.changePassword({
1154
+ // keystoneIbGib: delegateKeystone,
1155
+ // newMasterSecret: newPassphrase,
1156
+ // signingSecret: currentPassphrase,
1157
+ // signingPoolId: 'manage',
1158
+ // poolIds: poolsToRotate,
1159
+ // metaspace,
1160
+ // space
1161
+ // });
1162
+ // evolvedDelegates.push(evolvedDelegate);
1163
+ // }
1164
+ // }
1165
+
1166
+ // 4. Sync evolved parent and all evolved delegate keystones atomically
1167
+ this.setChangePassphraseStatus("Syncing evolved keystones to server...", "info");
1168
+ const parentTjp = getIbGibAddr({ ibGib: this.ibGib! }); // is this.ibGib the tjp always? or is it the latest in the keystone line? Does it matter?
1169
+
1170
+ const syncRes = await spaceGibApiBridge.putEvolveKeystone({
1171
+ domainAddr: parentTjp,
1172
+ keystoneIbGib: evolvedParent,
1173
+ relatedKeystoneIbGibs: evolvedDelegates,
1174
+ });
1175
+ if (!syncRes.success) {
1176
+ throw new Error(`Server synchronization rejected: ${syncRes.message}`);
1177
+ }
1178
+
1179
+ // 5. Update index locally
1180
+ this.setChangePassphraseStatus("Updating local indexes...", "info");
1181
+ const newKeystoneIbGibs = [toDto({ ibGib: evolvedParent })];
1182
+ if (evolvedDelegates) {
1183
+ evolvedDelegates.forEach(d => {
1184
+ newKeystoneIbGibs.push(toDto({ ibGib: d }));
1185
+ });
1186
+ }
1187
+ const rel8nInfos = [
1188
+ {
1189
+ rel8nName: "keystone",
1190
+ ibGibs: newKeystoneIbGibs,
1191
+ }
1192
+ ];
1193
+ await updateSpecialIndex({
1194
+ type: "keystones",
1195
+ rel8nInfos,
1196
+ metaspace,
1197
+ space
1198
+ });
1199
+
1200
+ // Clear inputs
1201
+ this.elements!.inputCurrentPassphraseEl.value = "";
1202
+ this.elements!.inputNewPassphraseEl.value = "";
1203
+ this.elements!.inputConfirmPassphraseEl.value = "";
1204
+
1205
+ this.setChangePassphraseStatus("Passphrase updated successfully!", "success");
1206
+ await this.loadIbGib({ getLatest: true });
1207
+
1208
+ } catch (error) {
1209
+ console.error(`${lc} Passphrase rotation failed: ${extractErrorMsg(error)}`);
1210
+ this.setChangePassphraseStatus(`Update failed: ${extractErrorMsg(error)}`, "error");
1211
+ }
1212
+ }
821
1213
  }
@@ -102,11 +102,11 @@ export async function createAndRegisterSyncDelegate({
102
102
  const domainAddr = getIbGibAddr({ ibGib: domainI });
103
103
 
104
104
  // 4. Sync evolved parent and delegate to server
105
- const resEvolve = await apiBridge.putEvolveKeystone(
105
+ const resEvolve = await apiBridge.putEvolveKeystone({
106
106
  domainAddr,
107
- evolvedDomainI,
108
- [delegateKeystone]
109
- );
107
+ keystoneIbGib: evolvedDomainI,
108
+ relatedKeystoneIbGibs: [delegateKeystone]
109
+ });
110
110
  if (!resEvolve.success) {
111
111
  throw new Error(`Server rejected delegate evolution: ${resEvolve.message}`);
112
112
  }
@@ -15,6 +15,8 @@ import { getIbGibAddr } from '@ibgib/ts-gib/dist/helper.mjs';
15
15
  import { IbGib_V1 } from '@ibgib/ts-gib/dist/V1/types.mjs';
16
16
  import { validateAndRegisterEvolveKeystone } from '@ibgib/core-gib/dist/sync/sync-peer/sync-peer-websocket/sync-peer-websocket-receiver/sync-websocket-peer-helpers.mjs';
17
17
  import { KeystoneIbGib_V1 } from '@ibgib/core-gib/dist/keystone/keystone-types.mjs';
18
+ import { loadSsoServerConfig } from '@ibgib/web-gib/dist/identity/sso/sso-config-helper.mjs';
19
+ import { SsoCustodianService } from '@ibgib/web-gib/dist/identity/sso/sso-custodian-service.mjs';
18
20
 
19
21
  import { GLOBAL_LOG_A_LOT } from '../../../constants.mjs';
20
22
  import { ServeGibHandlerWithMetaspaceBase } from '../../handler-base.mjs';
@@ -27,9 +29,19 @@ interface EvolveParams extends ParamsWithDomain {
27
29
  domainAddr: string;
28
30
  }
29
31
 
32
+ /**
33
+ * Body payload for the Keystone Evolve REST API request.
34
+ */
30
35
  interface EvolveBody {
36
+ /** The main evolved keystone frame. */
31
37
  keystoneIbGib: KeystoneIbGib_V1;
32
- relatedIbGibs?: IbGib_V1[];
38
+ /**
39
+ * Array of other keystone frames related to this evolution.
40
+ * Ephemeral session keystones for sync, or persistent delegate keystones for manage.
41
+ */
42
+ relatedKeystoneIbGibs?: IbGib_V1[];
43
+ /** Optional map of providerId to token to revoke during unlinking. */
44
+ revokeTokens?: Record<string, string>;
33
45
  }
34
46
 
35
47
  export class KeystoneEvolveHandler extends ServeGibHandlerWithMetaspaceBase<EvolveParams, EvolveBody> {
@@ -75,7 +87,7 @@ export class KeystoneEvolveHandler extends ServeGibHandlerWithMetaspaceBase<Evol
75
87
  return this.error(400, 'Invalid request JSON body: bodyStr does NOT parse. (E: 2f81c8c80a6884ff32b877671e92f326)');
76
88
  }
77
89
 
78
- const { keystoneIbGib, relatedIbGibs = [] } = parsed;
90
+ const { keystoneIbGib, relatedKeystoneIbGibs = [] } = parsed;
79
91
  if (!keystoneIbGib) {
80
92
  return this.error(400, 'Invalid request. Body must contain keystoneIbGib (E: e234647f79282982c8012fe8015f5826)');
81
93
  }
@@ -89,11 +101,41 @@ export class KeystoneEvolveHandler extends ServeGibHandlerWithMetaspaceBase<Evol
89
101
  await validateAndRegisterEvolveKeystone({
90
102
  domainAddr,
91
103
  keystoneIbGib,
92
- relatedIbGibs,
104
+ relatedKeystoneIbGibs,
93
105
  metaspace: reqCtx.metaspace,
94
106
  space
95
107
  });
96
108
 
109
+ // Post-Evolution Hook: Handle SSO provider unlinking
110
+ const claim = keystoneIbGib.data?.proofs?.[0]?.claim;
111
+ if (claim?.details?.type === 'remove-pool') {
112
+ const removeInfo = claim.details.info;
113
+ if (removeInfo && Array.isArray(removeInfo.remove)) {
114
+ // Lazy-load custodian service config
115
+ const config = loadSsoServerConfig();
116
+ const custodian = new SsoCustodianService(config);
117
+
118
+ for (const poolId of removeInfo.remove) {
119
+ if (poolId.startsWith('custodian-manage-')) {
120
+ const providerId = poolId.replace('custodian-manage-', '');
121
+ console.log(`${lc} Evolved keystone removed custodian pool: ${poolId}. Performing unlink cleanup for provider: ${providerId}`);
122
+
123
+ // Revoke token if supplied by the client
124
+ const token = parsed.revokeTokens?.[providerId];
125
+ if (token) {
126
+ try {
127
+ await custodian.revokeToken(providerId as any, token);
128
+ } catch (e) {
129
+ console.warn(`${lc} Failed to revoke token for provider ${providerId}: ${extractErrorMsg(e)}`);
130
+ }
131
+ } else {
132
+ console.log(`${lc} No OAuth token supplied by client for revocation of provider: ${providerId}`);
133
+ }
134
+ }
135
+ }
136
+ }
137
+ }
138
+
97
139
  return this.ok({
98
140
  message: `Successfully evolved domain keystone with session keystone`,
99
141
  });