@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.
- package/dist/client/bootstrap.mjs +1 -1
- package/dist/client/chunk-ACIGPCQW.mjs +2407 -0
- package/dist/client/chunk-ACIGPCQW.mjs.map +7 -0
- package/dist/client/chunk-QRMTJE6M.mjs +42 -0
- package/dist/client/chunk-QRMTJE6M.mjs.map +7 -0
- package/dist/client/index.mjs +1 -1
- package/dist/client/script.mjs +1 -1
- package/dist/server/server.mjs +5655 -5268
- package/dist/server/server.mjs.map +4 -4
- package/package.json +4 -4
- package/src/client/AUTO-GENERATED-version.mts +1 -1
- package/src/client/api/space-gib-api-bridge.mts +37 -3
- package/src/client/components/keystone-creator/keystone-creator.mts +5 -5
- package/src/client/components/keystone-details/keystone-details.css +158 -19
- package/src/client/components/keystone-details/keystone-details.html +57 -5
- package/src/client/components/keystone-details/keystone-details.mts +417 -25
- package/src/client/dev-tools/common.mts +4 -4
- package/src/server/serve-gib/handlers/api/keystone/keystone-evolve.handler.mts +45 -3
- package/src/server/serve-gib/handlers/api/keystone/sso-link.handler.mts +25 -4
- package/src/server/serve-gib/handlers/api/keystone/sso-login.handler.mts +4 -3
- package/dist/client/chunk-RXFIBFRK.mjs +0 -42
- package/dist/client/chunk-RXFIBFRK.mjs.map +0 -7
- package/dist/client/chunk-ULFNE26Y.mjs +0 -2216
- package/dist/client/chunk-ULFNE26Y.mjs.map +0 -7
|
@@ -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:
|
|
88
|
-
btnLinkGithubEl:
|
|
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
|
|
194
|
-
this.elements.btnLinkGithubEl = shadow.getElementById('btn-link-github') as
|
|
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.
|
|
233
|
-
this.elements!.btnLinkGithubEl.addEventListener('click', () => this.
|
|
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
|
-
|
|
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 ${
|
|
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
|
|
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:
|
|
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
|
|
766
|
-
const
|
|
767
|
-
|
|
768
|
-
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
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
|
});
|