@ibgib/web-gib 0.0.60 → 0.0.61
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/AUTO-GENERATED-version.d.mts +1 -1
- package/dist/AUTO-GENERATED-version.mjs +1 -1
- package/dist/helpers.d.mts.map +1 -1
- package/dist/helpers.mjs +1 -0
- package/dist/helpers.mjs.map +1 -1
- package/dist/identity/device-delegate-helper.d.mts +43 -0
- package/dist/identity/device-delegate-helper.d.mts.map +1 -0
- package/dist/identity/device-delegate-helper.mjs +106 -0
- package/dist/identity/device-delegate-helper.mjs.map +1 -0
- package/dist/identity/device-delegate.respec.d.mts +2 -0
- package/dist/identity/device-delegate.respec.d.mts.map +1 -0
- package/dist/identity/device-delegate.respec.mjs +106 -0
- package/dist/identity/device-delegate.respec.mjs.map +1 -0
- package/dist/identity/keystone-sync-service.d.mts +18 -0
- package/dist/identity/keystone-sync-service.d.mts.map +1 -1
- package/dist/identity/keystone-sync-service.mjs +77 -1
- package/dist/identity/keystone-sync-service.mjs.map +1 -1
- package/dist/index.d.mts +1 -1
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +1 -2
- package/dist/index.mjs.map +1 -1
- package/dist/storage/storage-helpers.web.d.mts +9 -0
- package/dist/storage/storage-helpers.web.d.mts.map +1 -1
- package/dist/storage/storage-helpers.web.mjs +42 -0
- package/dist/storage/storage-helpers.web.mjs.map +1 -1
- package/dist/ui/component/identity/add-device/add-device.d.mts.map +1 -1
- package/dist/ui/component/identity/add-device/add-device.mjs +30 -43
- package/dist/ui/component/identity/add-device/add-device.mjs.map +1 -1
- package/dist/ui/component/identity/keystone-creator/keystone-creator.d.mts.map +1 -1
- package/dist/ui/component/identity/keystone-creator/keystone-creator.mjs +41 -57
- package/dist/ui/component/identity/keystone-creator/keystone-creator.mjs.map +1 -1
- package/dist/ui/component/identity/keystone-details/keystone-details.d.mts +3 -0
- package/dist/ui/component/identity/keystone-details/keystone-details.d.mts.map +1 -1
- package/dist/ui/component/identity/keystone-details/keystone-details.mjs +307 -46
- package/dist/ui/component/identity/keystone-details/keystone-details.mjs.map +1 -1
- package/dist/ui/ui-constants.d.mts +1 -0
- package/dist/ui/ui-constants.d.mts.map +1 -1
- package/dist/ui/ui-constants.mjs +1 -0
- package/dist/ui/ui-constants.mjs.map +1 -1
- package/dist/ui/ui-types.d.mts +5 -0
- package/dist/ui/ui-types.d.mts.map +1 -1
- package/package.json +2 -2
- package/src/AUTO-GENERATED-version.mts +1 -1
- package/src/helpers.mts +1 -0
- package/src/identity/custodian-delegate-helper.mts.bak +7 -0
- package/src/identity/device-delegate-helper.mts +135 -0
- package/src/identity/device-delegate.respec.mts +125 -0
- package/src/identity/keystone-sync-service.mts +95 -2
- package/src/index.mts +1 -2
- package/src/storage/storage-helpers.web.mts +47 -0
- package/src/ui/component/identity/add-device/add-device.mts +36 -48
- package/src/ui/component/identity/keystone-creator/keystone-creator.mts +48 -65
- package/src/ui/component/identity/keystone-details/keystone-details.mts +317 -49
- package/src/ui/ui-constants.mts +1 -0
- package/src/ui/ui-types.mts +6 -0
- package/dist/identity/custodian-delegate-helper.d.mts +0 -22
- package/dist/identity/custodian-delegate-helper.d.mts.map +0 -1
- package/dist/identity/custodian-delegate-helper.mjs +0 -67
- package/dist/identity/custodian-delegate-helper.mjs.map +0 -1
- package/src/identity/custodian-delegate-helper.mts +0 -86
|
@@ -23,10 +23,11 @@ import { updateSpecialIndex } from "@ibgib/core-gib/dist/timeline/timeline-api.m
|
|
|
23
23
|
import { getTjpAddr, toDto } from "@ibgib/core-gib/dist/common/other/ibgib-helper.mjs";
|
|
24
24
|
import { parseSpaceIb } from "@ibgib/core-gib/dist/witness/space/space-helper.mjs";
|
|
25
25
|
import { executeSsoOAuthPopup } from "../../../../identity/sso-popup-helper.mjs";
|
|
26
|
-
|
|
27
26
|
import { getComponentCtorArg } from "../../../../app-bootstrap/init-orchestration.mjs";
|
|
28
27
|
import { promptForSecret } from "../../../../helpers.web.mjs";
|
|
29
|
-
import { isSsoProviderLinked } from "
|
|
28
|
+
import { isSsoProviderLinked, getKeystoneCapabilities, isDelegateExpired } from "@ibgib/core-gib/dist/keystone/delegate/delegate-helpers.mjs";
|
|
29
|
+
import { generatePoolChallenges } from "@ibgib/core-gib/dist/keystone/keystone-helpers.mjs";
|
|
30
|
+
import { POOL_ID_MANAGE } from "@ibgib/core-gib/dist/keystone/keystone-constants.mjs";
|
|
30
31
|
import { getGlobalMetaspace_waitIfNeeded } from "../../../../helpers.mjs";
|
|
31
32
|
|
|
32
33
|
const logalot = false;
|
|
@@ -118,6 +119,7 @@ export class KeystoneDetailsComponentInstance
|
|
|
118
119
|
private isHistoricalFrame: boolean = false;
|
|
119
120
|
private _onIdentityChanged: ((e: any) => void) | null = null;
|
|
120
121
|
private keystoneCache: Map<string, { username: string, description: string, isPrimary: boolean, n: number }> = new Map();
|
|
122
|
+
private aggregatedDetails?: any;
|
|
121
123
|
|
|
122
124
|
constructor() {
|
|
123
125
|
super();
|
|
@@ -454,9 +456,12 @@ export class KeystoneDetailsComponentInstance
|
|
|
454
456
|
this.elements.btnLinkGoogleEl.classList.remove('disabled');
|
|
455
457
|
this.elements.btnLinkGithubEl.classList.remove('disabled');
|
|
456
458
|
|
|
457
|
-
// Check if
|
|
458
|
-
const
|
|
459
|
-
const
|
|
459
|
+
// Check if origin provider or linked
|
|
460
|
+
const originProvider = this.aggregatedDetails?.custodianProvider || this.ibGib?.data?.frameDetails?.custodianProvider;
|
|
461
|
+
const isGoogleLinked = isSsoProviderLinked({ keystone: this.ibGib, aggregatedDetails: this.aggregatedDetails, providerId: 'google' });
|
|
462
|
+
const isGithubLinked = isSsoProviderLinked({ keystone: this.ibGib, aggregatedDetails: this.aggregatedDetails, providerId: 'github' });
|
|
463
|
+
|
|
464
|
+
const caps = getKeystoneCapabilities({ keystone: this.ibGib, aggregatedDetails: this.aggregatedDetails });
|
|
460
465
|
|
|
461
466
|
// Google Card States
|
|
462
467
|
const googleCard = this.elements.btnLinkGoogleEl;
|
|
@@ -464,11 +469,22 @@ export class KeystoneDetailsComponentInstance
|
|
|
464
469
|
if (isGoogleLinked) {
|
|
465
470
|
googleCard.classList.remove('unlinked');
|
|
466
471
|
googleCard.classList.add('linked');
|
|
467
|
-
|
|
472
|
+
const isOrigin = originProvider === 'google';
|
|
473
|
+
if (isOrigin && !caps.isSovereign) {
|
|
474
|
+
googleLabel.textContent = "Google (Primary Account) 🔒";
|
|
475
|
+
googleCard.title = "Cannot unlink primary account without first adding a Master Password below.";
|
|
476
|
+
} else if (isOrigin) {
|
|
477
|
+
googleLabel.textContent = "Google (Primary Account) ✅";
|
|
478
|
+
googleCard.title = "Click to unlink Google provider (requires Master Password).";
|
|
479
|
+
} else {
|
|
480
|
+
googleLabel.textContent = "Google Linked ✅";
|
|
481
|
+
googleCard.title = "Click to unlink Google provider.";
|
|
482
|
+
}
|
|
468
483
|
} else {
|
|
469
484
|
googleCard.classList.remove('linked');
|
|
470
485
|
googleCard.classList.add('unlinked');
|
|
471
486
|
googleLabel.textContent = "Unlinked";
|
|
487
|
+
googleCard.title = "Click to link Google account.";
|
|
472
488
|
}
|
|
473
489
|
|
|
474
490
|
// GitHub Card States
|
|
@@ -477,15 +493,47 @@ export class KeystoneDetailsComponentInstance
|
|
|
477
493
|
if (isGithubLinked) {
|
|
478
494
|
githubCard.classList.remove('unlinked');
|
|
479
495
|
githubCard.classList.add('linked');
|
|
480
|
-
|
|
496
|
+
const isOrigin = originProvider === 'github';
|
|
497
|
+
if (isOrigin && !caps.isSovereign) {
|
|
498
|
+
githubLabel.textContent = "GitHub (Primary Account) 🔒";
|
|
499
|
+
githubCard.title = "Cannot unlink primary account without first adding a Master Password below.";
|
|
500
|
+
} else if (isOrigin) {
|
|
501
|
+
githubLabel.textContent = "GitHub (Primary Account) ✅";
|
|
502
|
+
githubCard.title = "Click to unlink GitHub provider (requires Master Password).";
|
|
503
|
+
} else {
|
|
504
|
+
githubLabel.textContent = "GitHub Linked ✅";
|
|
505
|
+
githubCard.title = "Click to unlink GitHub provider.";
|
|
506
|
+
}
|
|
481
507
|
} else {
|
|
482
508
|
githubCard.classList.remove('linked');
|
|
483
509
|
githubCard.classList.add('unlinked');
|
|
484
510
|
githubLabel.textContent = "Unlinked";
|
|
511
|
+
githubCard.title = "Click to link GitHub account.";
|
|
485
512
|
}
|
|
486
513
|
}
|
|
487
514
|
}
|
|
488
515
|
|
|
516
|
+
private updatePasswordCardView(): void {
|
|
517
|
+
const caps = getKeystoneCapabilities({ keystone: this.ibGib, aggregatedDetails: this.aggregatedDetails });
|
|
518
|
+
const cardTitleEl = this.elements?.changePasswordCardEl?.querySelector('h3');
|
|
519
|
+
const cardDescEl = this.elements?.changePasswordCardEl?.querySelector('.description');
|
|
520
|
+
const currentPasswordGroup = this.elements?.inputCurrentPasswordEl?.closest('.form-group') as HTMLElement | null;
|
|
521
|
+
const btnChangePassword = this.elements?.btnChangePasswordEl;
|
|
522
|
+
|
|
523
|
+
const isCustodialOnly = caps.isCustodial && !caps.isSovereign;
|
|
524
|
+
if (isCustodialOnly) {
|
|
525
|
+
if (cardTitleEl) cardTitleEl.textContent = "Add Master Password";
|
|
526
|
+
if (cardDescEl) cardDescEl.textContent = "Establish sovereign ownership by adding a master password. This enables direct, independent cryptographic management without relying solely on custodian authentication.";
|
|
527
|
+
if (currentPasswordGroup) currentPasswordGroup.style.display = 'none';
|
|
528
|
+
if (btnChangePassword) btnChangePassword.textContent = "Add Master Password";
|
|
529
|
+
} else {
|
|
530
|
+
if (cardTitleEl) cardTitleEl.textContent = "Change Password";
|
|
531
|
+
if (cardDescEl) cardDescEl.textContent = "Rotate your local password that drives this identity. This will cryptographically evolve your primary identity and all registered user-owned delegate keystones to the new password.";
|
|
532
|
+
if (currentPasswordGroup) currentPasswordGroup.style.display = '';
|
|
533
|
+
if (btnChangePassword) btnChangePassword.textContent = "Update Password";
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
|
|
489
537
|
private async updateFrameDetailsView(): Promise<void> {
|
|
490
538
|
const lc = `${this.lc}[${this.updateFrameDetailsView.name}]`;
|
|
491
539
|
try {
|
|
@@ -549,6 +597,7 @@ export class KeystoneDetailsComponentInstance
|
|
|
549
597
|
space
|
|
550
598
|
});
|
|
551
599
|
|
|
600
|
+
this.aggregatedDetails = aggregated;
|
|
552
601
|
this.elements.identityAggrDetailsEl.textContent = aggregated ? pretty(aggregated) : '{}';
|
|
553
602
|
|
|
554
603
|
// Update Username and Description fields
|
|
@@ -556,16 +605,19 @@ export class KeystoneDetailsComponentInstance
|
|
|
556
605
|
this.elements.identityDescriptionEl.textContent = aggregated?.description || aggregated?.profile?.description || '-';
|
|
557
606
|
} catch (err) {
|
|
558
607
|
console.warn(`${lc} Error aggregating details: ${extractErrorMsg(err)}`);
|
|
608
|
+
this.aggregatedDetails = undefined;
|
|
559
609
|
this.elements.identityAggrDetailsEl.textContent = '{}';
|
|
560
610
|
this.elements.identityNameEl.textContent = '-';
|
|
561
611
|
this.elements.identityDescriptionEl.textContent = '-';
|
|
562
612
|
}
|
|
563
613
|
} else {
|
|
614
|
+
this.aggregatedDetails = undefined;
|
|
564
615
|
this.elements.identityAggrDetailsEl.textContent = '{}';
|
|
565
616
|
this.elements.identityNameEl.textContent = '-';
|
|
566
617
|
this.elements.identityDescriptionEl.textContent = '-';
|
|
567
618
|
}
|
|
568
619
|
} else {
|
|
620
|
+
this.aggregatedDetails = undefined;
|
|
569
621
|
this.elements.identityAggrDetailsEl.textContent = '{}';
|
|
570
622
|
this.elements.identityNameEl.textContent = '-';
|
|
571
623
|
this.elements.identityDescriptionEl.textContent = '-';
|
|
@@ -598,6 +650,7 @@ export class KeystoneDetailsComponentInstance
|
|
|
598
650
|
await this.renderDelegates(keystone.data?.delegates);
|
|
599
651
|
this.elements.ssoLinkCardEl.classList.remove('hidden');
|
|
600
652
|
this.elements.changePasswordCardEl.classList.remove('hidden');
|
|
653
|
+
this.updatePasswordCardView();
|
|
601
654
|
} else {
|
|
602
655
|
this.elements.delegatesCardEl.classList.add('hidden');
|
|
603
656
|
this.elements.ssoLinkCardEl.classList.add('hidden');
|
|
@@ -615,6 +668,7 @@ export class KeystoneDetailsComponentInstance
|
|
|
615
668
|
|
|
616
669
|
private clearActiveDetails() {
|
|
617
670
|
if (!this.elements) { return; }
|
|
671
|
+
this.aggregatedDetails = undefined;
|
|
618
672
|
this.isHistoricalFrame = false;
|
|
619
673
|
if (this.elements.historicalWarningBannerEl) {
|
|
620
674
|
this.elements.historicalWarningBannerEl.classList.add('hidden');
|
|
@@ -804,43 +858,71 @@ export class KeystoneDetailsComponentInstance
|
|
|
804
858
|
}
|
|
805
859
|
|
|
806
860
|
const metaspace = await getGlobalMetaspace_waitIfNeeded();
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
const space = await metaspace.getLocalUserSpace({ lock: false });
|
|
810
|
-
if (space?.ib) {
|
|
811
|
-
localSpaceId = parseSpaceIb({ spaceIb: space.ib }).spaceId;
|
|
812
|
-
}
|
|
861
|
+
if (!metaspace) {
|
|
862
|
+
throw new Error(`(UNEXPECTED) metaspace falsy? (E: 8a9b0c1d2e3f4a5b6c7d8e9f0a1b2c3e)`);
|
|
813
863
|
}
|
|
864
|
+
const space = await metaspace.getLocalUserSpace({ lock: false });
|
|
865
|
+
if (!space) {
|
|
866
|
+
throw new Error(`(UNEXPECTED) space falsy? (E: 9b0c1d2e3f4a5b6c7d8e9f0a1b2c3d4f)`);
|
|
867
|
+
}
|
|
868
|
+
const localSpaceId = space.ib ? parseSpaceIb({ spaceIb: space.ib }).spaceId : undefined;
|
|
814
869
|
|
|
815
870
|
for (const [delegateTjpAddr, info] of Object.entries(delegates)) {
|
|
816
871
|
const tr = document.createElement('tr');
|
|
817
872
|
|
|
818
|
-
const deviceName = info.deviceName || info.details?.deviceTag || info.details?.name || 'Device Delegate';
|
|
873
|
+
const deviceName = info.deviceName || (info as any).details?.deviceTag || (info as any).details?.name || 'Device Delegate';
|
|
819
874
|
const tdName = document.createElement('td');
|
|
820
875
|
|
|
821
|
-
const delegateSpaceId = info.spaceId || info.details?.spaceId;
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
badgeEl.className = 'badge badge-this-device';
|
|
834
|
-
badgeEl.style.backgroundColor = '#1b5e20';
|
|
835
|
-
badgeEl.style.color = '#a5d6a7';
|
|
836
|
-
} else {
|
|
837
|
-
badgeEl.textContent = 'Remote Device';
|
|
838
|
-
badgeEl.className = 'badge badge-remote-device';
|
|
839
|
-
badgeEl.style.backgroundColor = '#0d47a1';
|
|
840
|
-
badgeEl.style.color = '#90caf9';
|
|
876
|
+
const delegateSpaceId = info.spaceId || (info as any).details?.spaceId;
|
|
877
|
+
|
|
878
|
+
// Check if delegate is present in local space (via spaceId match or direct presence check)
|
|
879
|
+
let isThisDevice = false;
|
|
880
|
+
if (localSpaceId && delegateSpaceId === localSpaceId) {
|
|
881
|
+
isThisDevice = true;
|
|
882
|
+
} else {
|
|
883
|
+
try {
|
|
884
|
+
const resGet = await metaspace.get({ addrs: [delegateTjpAddr], space });
|
|
885
|
+
isThisDevice = !!(resGet.success && resGet.ibGibs && resGet.ibGibs.length > 0);
|
|
886
|
+
} catch {
|
|
887
|
+
isThisDevice = false;
|
|
841
888
|
}
|
|
842
889
|
}
|
|
843
890
|
|
|
891
|
+
const badgeEl = document.createElement('span');
|
|
892
|
+
badgeEl.style.marginLeft = '0.5rem';
|
|
893
|
+
badgeEl.style.padding = '0.15rem 0.4rem';
|
|
894
|
+
badgeEl.style.borderRadius = '4px';
|
|
895
|
+
badgeEl.style.fontSize = '0.75rem';
|
|
896
|
+
badgeEl.style.fontWeight = 'bold';
|
|
897
|
+
|
|
898
|
+
if (isThisDevice) {
|
|
899
|
+
badgeEl.textContent = 'This Device';
|
|
900
|
+
badgeEl.className = 'badge badge-this-device';
|
|
901
|
+
badgeEl.style.backgroundColor = '#1b5e20';
|
|
902
|
+
badgeEl.style.color = '#a5d6a7';
|
|
903
|
+
} else {
|
|
904
|
+
badgeEl.textContent = 'Remote Device';
|
|
905
|
+
badgeEl.className = 'badge badge-remote-device';
|
|
906
|
+
badgeEl.style.backgroundColor = '#0d47a1';
|
|
907
|
+
badgeEl.style.color = '#90caf9';
|
|
908
|
+
}
|
|
909
|
+
|
|
910
|
+
// Check expiration
|
|
911
|
+
const isExpired = isDelegateExpired({ delegateInfo: info });
|
|
912
|
+
let expiredBadgeEl: HTMLSpanElement | null = null;
|
|
913
|
+
if (isExpired) {
|
|
914
|
+
expiredBadgeEl = document.createElement('span');
|
|
915
|
+
expiredBadgeEl.textContent = 'Expired';
|
|
916
|
+
expiredBadgeEl.className = 'badge badge-expired';
|
|
917
|
+
expiredBadgeEl.style.marginLeft = '0.3rem';
|
|
918
|
+
expiredBadgeEl.style.padding = '0.15rem 0.4rem';
|
|
919
|
+
expiredBadgeEl.style.borderRadius = '4px';
|
|
920
|
+
expiredBadgeEl.style.fontSize = '0.75rem';
|
|
921
|
+
expiredBadgeEl.style.fontWeight = 'bold';
|
|
922
|
+
expiredBadgeEl.style.backgroundColor = '#b71c1c';
|
|
923
|
+
expiredBadgeEl.style.color = '#ef9a9a';
|
|
924
|
+
}
|
|
925
|
+
|
|
844
926
|
const applyNameAndBadge = (displayName: string, hoverText?: string) => {
|
|
845
927
|
tdName.innerHTML = '';
|
|
846
928
|
const textSpan = document.createElement('span');
|
|
@@ -849,6 +931,9 @@ export class KeystoneDetailsComponentInstance
|
|
|
849
931
|
if (badgeEl) {
|
|
850
932
|
tdName.appendChild(badgeEl);
|
|
851
933
|
}
|
|
934
|
+
if (expiredBadgeEl) {
|
|
935
|
+
tdName.appendChild(expiredBadgeEl);
|
|
936
|
+
}
|
|
852
937
|
tdName.title = hoverText || displayName;
|
|
853
938
|
};
|
|
854
939
|
|
|
@@ -863,7 +948,7 @@ export class KeystoneDetailsComponentInstance
|
|
|
863
948
|
tdVerbs.textContent = info.allowedVerbs ? info.allowedVerbs.join(', ') : '-';
|
|
864
949
|
tdVerbs.title = tdVerbs.textContent;
|
|
865
950
|
|
|
866
|
-
const isRemote =
|
|
951
|
+
const isRemote = !isThisDevice;
|
|
867
952
|
|
|
868
953
|
const tdActions = document.createElement('td');
|
|
869
954
|
const btnView = document.createElement('button');
|
|
@@ -874,20 +959,20 @@ export class KeystoneDetailsComponentInstance
|
|
|
874
959
|
if (isRemote) {
|
|
875
960
|
this.showRemoteDelegateModal(info);
|
|
876
961
|
} else {
|
|
877
|
-
if (info.delegateAddr) {
|
|
962
|
+
if (info.delegateAddr || delegateTjpAddr) {
|
|
878
963
|
let addrToLoad = info.delegateAddr;
|
|
879
|
-
|
|
880
|
-
const
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
console.warn(`${lc} Error resolving latest delegate address: ${extractErrorMsg(err)}`);
|
|
889
|
-
}
|
|
964
|
+
try {
|
|
965
|
+
const keystoneService = new KeystoneService_V1();
|
|
966
|
+
const latestKeystone = await keystoneService.getLatestKeystone({
|
|
967
|
+
addr: delegateTjpAddr,
|
|
968
|
+
metaspace,
|
|
969
|
+
space
|
|
970
|
+
});
|
|
971
|
+
if (latestKeystone) {
|
|
972
|
+
addrToLoad = getIbGibAddr({ ibGib: latestKeystone });
|
|
890
973
|
}
|
|
974
|
+
} catch (err) {
|
|
975
|
+
console.warn(`${lc} Error resolving latest delegate keystone: ${extractErrorMsg(err)}`);
|
|
891
976
|
}
|
|
892
977
|
this.dispatchEvent(new CustomEvent('ibgib-view-keystone-details', {
|
|
893
978
|
detail: { addr: addrToLoad },
|
|
@@ -1187,8 +1272,13 @@ export class KeystoneDetailsComponentInstance
|
|
|
1187
1272
|
}
|
|
1188
1273
|
|
|
1189
1274
|
protected async handleSSOClick(providerId: 'google' | 'github') {
|
|
1190
|
-
const isLinked = isSsoProviderLinked({ keystone: this.ibGib, providerId });
|
|
1275
|
+
const isLinked = isSsoProviderLinked({ keystone: this.ibGib, aggregatedDetails: this.aggregatedDetails, providerId });
|
|
1191
1276
|
if (isLinked) {
|
|
1277
|
+
const caps = getKeystoneCapabilities({ keystone: this.ibGib, aggregatedDetails: this.aggregatedDetails });
|
|
1278
|
+
if (!caps.isSovereign) {
|
|
1279
|
+
this.setSSOStatus(`Cannot unlink ${providerId}: This is your only identity recovery and management method. Please add a Master Password first in the Password card below to enable sovereign ownership before unlinking.`, "error");
|
|
1280
|
+
return;
|
|
1281
|
+
}
|
|
1192
1282
|
await this.handleSSOUnlink(providerId);
|
|
1193
1283
|
} else {
|
|
1194
1284
|
await this.handleSSOLink(providerId);
|
|
@@ -1251,7 +1341,7 @@ export class KeystoneDetailsComponentInstance
|
|
|
1251
1341
|
|
|
1252
1342
|
// 2. Evolve parent keystone to remove the custodian pool
|
|
1253
1343
|
const targetPoolId = `custodian-manage-${providerId}`;
|
|
1254
|
-
|
|
1344
|
+
let evolvedParent = await keystoneService.removePools({
|
|
1255
1345
|
latestKeystone: this.ibGib!,
|
|
1256
1346
|
masterSecret: masterSecret!,
|
|
1257
1347
|
poolIds: [targetPoolId],
|
|
@@ -1259,6 +1349,22 @@ export class KeystoneDetailsComponentInstance
|
|
|
1259
1349
|
space
|
|
1260
1350
|
});
|
|
1261
1351
|
|
|
1352
|
+
// Unregister any corresponding outward delegate for this provider if present
|
|
1353
|
+
const delegatesMap = this.ibGib?.data?.delegates;
|
|
1354
|
+
if (delegatesMap) {
|
|
1355
|
+
for (const [delegateTjpAddr, info] of Object.entries(delegatesMap)) {
|
|
1356
|
+
if (info.deviceName?.includes(providerId)) {
|
|
1357
|
+
evolvedParent = await keystoneService.unregisterDelegate({
|
|
1358
|
+
parentKeystone: evolvedParent,
|
|
1359
|
+
delegateTjpAddr,
|
|
1360
|
+
masterSecret: masterSecret!,
|
|
1361
|
+
metaspace,
|
|
1362
|
+
space,
|
|
1363
|
+
});
|
|
1364
|
+
}
|
|
1365
|
+
}
|
|
1366
|
+
}
|
|
1367
|
+
|
|
1262
1368
|
// 3. Sync evolved parent keystone back to server
|
|
1263
1369
|
this.setSSOStatus("Publishing evolved identity keystone to server...", "info");
|
|
1264
1370
|
const syncRes = await getApiBridge().putEvolveKeystone({
|
|
@@ -1299,8 +1405,170 @@ export class KeystoneDetailsComponentInstance
|
|
|
1299
1405
|
el.classList.remove('hidden');
|
|
1300
1406
|
}
|
|
1301
1407
|
|
|
1408
|
+
protected async handleAddMasterPassword(): Promise<void> {
|
|
1409
|
+
const lc = `${this.lc}[${this.handleAddMasterPassword.name}]`;
|
|
1410
|
+
this.setChangePasswordStatus("Validating entries...", "info");
|
|
1411
|
+
|
|
1412
|
+
try {
|
|
1413
|
+
const newPassword = this.elements!.inputNewPasswordEl.value;
|
|
1414
|
+
const confirmPassword = this.elements!.inputConfirmPasswordEl.value;
|
|
1415
|
+
|
|
1416
|
+
if (!newPassword) {
|
|
1417
|
+
this.setChangePasswordStatus("Master password is required.", "error");
|
|
1418
|
+
return;
|
|
1419
|
+
}
|
|
1420
|
+
if (newPassword !== confirmPassword) {
|
|
1421
|
+
this.setChangePasswordStatus("Master password and confirmation do not match.", "error");
|
|
1422
|
+
return;
|
|
1423
|
+
}
|
|
1424
|
+
|
|
1425
|
+
const initialManagePool = this.ibGib?.data?.challengePools?.find(p => p.id === POOL_ID_MANAGE);
|
|
1426
|
+
if (!initialManagePool) {
|
|
1427
|
+
this.setChangePasswordStatus("Primary keystone missing active manage challenge pool.", "error");
|
|
1428
|
+
return;
|
|
1429
|
+
}
|
|
1430
|
+
|
|
1431
|
+
this.showBusy({
|
|
1432
|
+
isBusy: true,
|
|
1433
|
+
title: 'Elevating Identity...',
|
|
1434
|
+
msg: 'Generating cryptographic sovereign challenge pool...',
|
|
1435
|
+
animationEmoji: '🔑'
|
|
1436
|
+
});
|
|
1437
|
+
this.setChangePasswordStatus("Generating sovereign challenge pool...", "info");
|
|
1438
|
+
const newChallenges = await generatePoolChallenges({
|
|
1439
|
+
config: initialManagePool.config,
|
|
1440
|
+
masterSecret: newPassword,
|
|
1441
|
+
});
|
|
1442
|
+
const newManagePool = {
|
|
1443
|
+
id: POOL_ID_MANAGE,
|
|
1444
|
+
config: initialManagePool.config,
|
|
1445
|
+
challenges: newChallenges,
|
|
1446
|
+
};
|
|
1447
|
+
|
|
1448
|
+
const primaryTjpAddr = getTjpAddr({ ibGib: this.ibGib! }) || getIbGibAddr({ ibGib: this.ibGib! });
|
|
1449
|
+
const custodianProvider = this.aggregatedDetails?.custodianProvider || 'google';
|
|
1450
|
+
|
|
1451
|
+
let requestBody: any = {
|
|
1452
|
+
primaryTjpAddr,
|
|
1453
|
+
newManagePool,
|
|
1454
|
+
};
|
|
1455
|
+
|
|
1456
|
+
if (custodianProvider === 'google' || custodianProvider === 'github') {
|
|
1457
|
+
this.showBusy({
|
|
1458
|
+
isBusy: true,
|
|
1459
|
+
title: 'Re-authenticating with Provider...',
|
|
1460
|
+
msg: `Please complete authentication in the ${custodianProvider} popup window...`,
|
|
1461
|
+
animationEmoji: '🔒'
|
|
1462
|
+
});
|
|
1463
|
+
this.setChangePasswordStatus(`Please authenticate in the popup with ${custodianProvider} to verify ownership...`, "info");
|
|
1464
|
+
const resPopup = await executeSsoOAuthPopup({
|
|
1465
|
+
providerId: custodianProvider,
|
|
1466
|
+
parentTjpAddr: primaryTjpAddr,
|
|
1467
|
+
});
|
|
1468
|
+
|
|
1469
|
+
if (!resPopup.success || !resPopup.code) {
|
|
1470
|
+
this.showBusy({ isBusy: false });
|
|
1471
|
+
this.setChangePasswordStatus(resPopup.message || "Re-authentication cancelled.", "error");
|
|
1472
|
+
return;
|
|
1473
|
+
}
|
|
1474
|
+
|
|
1475
|
+
requestBody.providerId = custodianProvider;
|
|
1476
|
+
requestBody.code = resPopup.code;
|
|
1477
|
+
requestBody.redirectUri = resPopup.redirectUri;
|
|
1478
|
+
} else {
|
|
1479
|
+
this.showBusy({ isBusy: false });
|
|
1480
|
+
// Email OTP fallback
|
|
1481
|
+
const email = this.aggregatedDetails?.emails?.[0] || this.ibGib?.data?.emails?.[0];
|
|
1482
|
+
const otpCode = await promptForSecret({
|
|
1483
|
+
msg: `Enter 6-digit verification code sent to ${email}:`,
|
|
1484
|
+
confirm: false,
|
|
1485
|
+
});
|
|
1486
|
+
if (!otpCode) {
|
|
1487
|
+
this.setChangePasswordStatus("Verification code required.", "error");
|
|
1488
|
+
return;
|
|
1489
|
+
}
|
|
1490
|
+
requestBody.email = email;
|
|
1491
|
+
requestBody.code = otpCode;
|
|
1492
|
+
}
|
|
1493
|
+
|
|
1494
|
+
this.showBusy({
|
|
1495
|
+
isBusy: true,
|
|
1496
|
+
title: 'Updating Keystone Timeline...',
|
|
1497
|
+
msg: 'Publishing sovereign master password and evolving identity DAG...',
|
|
1498
|
+
animationEmoji: '⏳'
|
|
1499
|
+
});
|
|
1500
|
+
this.setChangePasswordStatus("Elevating identity to sovereign master password on server...", "info");
|
|
1501
|
+
const response = await fetch('/api/identity/custodian/add-master-password', {
|
|
1502
|
+
method: 'POST',
|
|
1503
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1504
|
+
body: JSON.stringify(requestBody),
|
|
1505
|
+
});
|
|
1506
|
+
|
|
1507
|
+
const resJson = await response.json();
|
|
1508
|
+
if (!response.ok || !resJson.success) {
|
|
1509
|
+
throw new Error(resJson.error || resJson.message || `Server error (${response.status})`);
|
|
1510
|
+
}
|
|
1511
|
+
|
|
1512
|
+
const evolvedParent = resJson.primaryKeystone;
|
|
1513
|
+
|
|
1514
|
+
// Save to local user space & metaspace
|
|
1515
|
+
const metaspace = await getGlobalMetaspace_waitIfNeeded();
|
|
1516
|
+
if (!metaspace) { throw new Error("Global metaspace not available"); }
|
|
1517
|
+
const space = await metaspace.getLocalUserSpace({ lock: false });
|
|
1518
|
+
if (!space) { throw new Error("Local user space not available"); }
|
|
1519
|
+
|
|
1520
|
+
await metaspace.put({ ibGibs: [evolvedParent], space });
|
|
1521
|
+
await metaspace.registerNewIbGib({ ibGib: evolvedParent, space });
|
|
1522
|
+
await updateSpecialIndex({
|
|
1523
|
+
type: "keystones",
|
|
1524
|
+
rel8nInfos: [
|
|
1525
|
+
{
|
|
1526
|
+
rel8nName: "keystone",
|
|
1527
|
+
ibGibs: [toDto({ ibGib: evolvedParent })]
|
|
1528
|
+
}
|
|
1529
|
+
],
|
|
1530
|
+
metaspace,
|
|
1531
|
+
space
|
|
1532
|
+
});
|
|
1533
|
+
|
|
1534
|
+
const evolvedAddr = getIbGibAddr({ ibGib: evolvedParent });
|
|
1535
|
+
|
|
1536
|
+
// Dispatch EVENT_IBGIB_IDENTITY_CHANGED
|
|
1537
|
+
window.dispatchEvent(new CustomEvent(EVENT_IBGIB_IDENTITY_CHANGED, {
|
|
1538
|
+
detail: { activeIdentityAddr: evolvedAddr },
|
|
1539
|
+
bubbles: true,
|
|
1540
|
+
composed: true
|
|
1541
|
+
}));
|
|
1542
|
+
|
|
1543
|
+
this.setChangePasswordStatus("Successfully added Master Password! Identity is now sovereign.", "success");
|
|
1544
|
+
|
|
1545
|
+
// Refresh the view
|
|
1546
|
+
this.ibGibAddr = evolvedAddr;
|
|
1547
|
+
this.staticIbGib = evolvedParent;
|
|
1548
|
+
await this.loadIbGib({ getLatest: true });
|
|
1549
|
+
|
|
1550
|
+
// Clear inputs
|
|
1551
|
+
this.elements!.inputNewPasswordEl.value = '';
|
|
1552
|
+
this.elements!.inputConfirmPasswordEl.value = '';
|
|
1553
|
+
|
|
1554
|
+
} catch (error) {
|
|
1555
|
+
console.error(`${lc} Elevation failed: ${extractErrorMsg(error)}`);
|
|
1556
|
+
this.setChangePasswordStatus(`Failed to add master password: ${extractErrorMsg(error)}`, "error");
|
|
1557
|
+
} finally {
|
|
1558
|
+
this.showBusy({ isBusy: false });
|
|
1559
|
+
}
|
|
1560
|
+
}
|
|
1561
|
+
|
|
1302
1562
|
protected async handleChangePassword(): Promise<void> {
|
|
1303
1563
|
const lc = `${this.lc}[${this.handleChangePassword.name}]`;
|
|
1564
|
+
|
|
1565
|
+
const caps = getKeystoneCapabilities({ keystone: this.ibGib, aggregatedDetails: this.aggregatedDetails });
|
|
1566
|
+
const isCustodialOnly = caps.isCustodial && !caps.isSovereign;
|
|
1567
|
+
if (isCustodialOnly) {
|
|
1568
|
+
await this.handleAddMasterPassword();
|
|
1569
|
+
return;
|
|
1570
|
+
}
|
|
1571
|
+
|
|
1304
1572
|
this.setChangePasswordStatus("Validating entries...", "info");
|
|
1305
1573
|
|
|
1306
1574
|
try {
|
package/src/ui/ui-constants.mts
CHANGED
|
@@ -107,6 +107,7 @@ export const EVENT_IBGIB_UI_MESSAGE = 'ibgib-ui-message';
|
|
|
107
107
|
export const EVENT_IBGIB_IDENTITY_REQUEST_CHANGE = 'ibgib-identity-request-change';
|
|
108
108
|
export const EVENT_IBGIB_IDENTITY_CHANGED = 'ibgib-identity-changed';
|
|
109
109
|
export const EVENT_IBGIB_IDENTITY_UPDATE_AVAILABLE = 'ibgib-identity-update-available';
|
|
110
|
+
export const EVENT_IBGIB_IDENTITY_RENEWAL_REQUIRED = 'ibgib-identity-renewal-required';
|
|
110
111
|
// #endregion Custom Events
|
|
111
112
|
|
|
112
113
|
|
package/src/ui/ui-types.mts
CHANGED
|
@@ -17,3 +17,9 @@ export interface EventIbGibIdentityUpdateAvailableDetail {
|
|
|
17
17
|
localN: number;
|
|
18
18
|
serverLatestAddr: string;
|
|
19
19
|
}
|
|
20
|
+
|
|
21
|
+
export interface EventIbGibIdentityRenewalRequiredDetail {
|
|
22
|
+
activePrimaryAddr: string;
|
|
23
|
+
primaryTjpAddr: string;
|
|
24
|
+
reason: 'expired' | 'missing_secret';
|
|
25
|
+
}
|
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @module identity/custodian-delegate-helper
|
|
3
|
-
*
|
|
4
|
-
* Client-side helper functions for deriving and storing local cryptographic delegate secrets
|
|
5
|
-
* for full-custodian / passwordless identities.
|
|
6
|
-
*/
|
|
7
|
-
/**
|
|
8
|
-
* Retrieves or generates a high-entropy local delegate secret for a passwordless custodian identity.
|
|
9
|
-
* Stored locally in `localStorage` keyed by `primaryTjpAddr`.
|
|
10
|
-
*
|
|
11
|
-
* @param primaryTjpAddr The primary identity keystone TJP address (`ib^gib`).
|
|
12
|
-
*/
|
|
13
|
-
export declare function deriveLocalCustodianDelegateSecret({ primaryTjpAddr, }: {
|
|
14
|
-
primaryTjpAddr: string;
|
|
15
|
-
}): Promise<string>;
|
|
16
|
-
/**
|
|
17
|
-
* Removes the stored local delegate secret for a custodian identity.
|
|
18
|
-
*/
|
|
19
|
-
export declare function clearLocalCustodianDelegateSecret({ primaryTjpAddr, }: {
|
|
20
|
-
primaryTjpAddr: string;
|
|
21
|
-
}): void;
|
|
22
|
-
//# sourceMappingURL=custodian-delegate-helper.d.mts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"custodian-delegate-helper.d.mts","sourceRoot":"","sources":["../../src/identity/custodian-delegate-helper.mts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAQH;;;;;GAKG;AACH,wBAAsB,kCAAkC,CAAC,EACrD,cAAc,GACjB,EAAE;IACC,cAAc,EAAE,MAAM,CAAC;CAC1B,GAAG,OAAO,CAAC,MAAM,CAAC,CAiDlB;AAED;;GAEG;AACH,wBAAgB,iCAAiC,CAAC,EAC9C,cAAc,GACjB,EAAE;IACC,cAAc,EAAE,MAAM,CAAC;CAC1B,GAAG,IAAI,CAIP"}
|
|
@@ -1,67 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @module identity/custodian-delegate-helper
|
|
3
|
-
*
|
|
4
|
-
* Client-side helper functions for deriving and storing local cryptographic delegate secrets
|
|
5
|
-
* for full-custodian / passwordless identities.
|
|
6
|
-
*/
|
|
7
|
-
import { getUUID, hash, HashAlgorithm, extractErrorMsg } from "@ibgib/helper-gib/dist/helpers/utils-helper.mjs";
|
|
8
|
-
import { storageGet, storagePut } from "../storage/storage-helpers.web.mjs";
|
|
9
|
-
import { getGlobalDbName, getGlobalStoreName } from "../helpers.web.mjs";
|
|
10
|
-
const LOCAL_STORAGE_PREFIX = 'ibgib_custodian_delegate_secret_';
|
|
11
|
-
/**
|
|
12
|
-
* Retrieves or generates a high-entropy local delegate secret for a passwordless custodian identity.
|
|
13
|
-
* Stored locally in `localStorage` keyed by `primaryTjpAddr`.
|
|
14
|
-
*
|
|
15
|
-
* @param primaryTjpAddr The primary identity keystone TJP address (`ib^gib`).
|
|
16
|
-
*/
|
|
17
|
-
export async function deriveLocalCustodianDelegateSecret({ primaryTjpAddr, }) {
|
|
18
|
-
const lc = `[${deriveLocalCustodianDelegateSecret.name}]`;
|
|
19
|
-
try {
|
|
20
|
-
if (!primaryTjpAddr) {
|
|
21
|
-
throw new Error('primaryTjpAddr is required to derive local custodian delegate secret (E: a1b2c3d4e5f67890123456789a000101)');
|
|
22
|
-
}
|
|
23
|
-
const key = `${LOCAL_STORAGE_PREFIX}${primaryTjpAddr}`;
|
|
24
|
-
const dbName = getGlobalDbName();
|
|
25
|
-
const storeName = getGlobalStoreName();
|
|
26
|
-
// debugger; // what are the dbName and storeName?
|
|
27
|
-
const existingSecret = await storageGet({ dbName, storeName, key });
|
|
28
|
-
if (existingSecret) {
|
|
29
|
-
return existingSecret;
|
|
30
|
-
}
|
|
31
|
-
// Generate high-entropy 512-bit SHA hash secret from fresh UUIDs
|
|
32
|
-
const rawEntropy = await getUUID(32);
|
|
33
|
-
const newSecret = await hash({
|
|
34
|
-
s: rawEntropy,
|
|
35
|
-
algorithm: HashAlgorithm.sha_512
|
|
36
|
-
});
|
|
37
|
-
await storagePut({ dbName, storeName, key, value: newSecret });
|
|
38
|
-
return newSecret;
|
|
39
|
-
// const existingSecret = localStorage.getItem(key);
|
|
40
|
-
// if (existingSecret) {
|
|
41
|
-
// return existingSecret;
|
|
42
|
-
// }
|
|
43
|
-
// // Generate high-entropy 512-bit SHA hash secret from fresh UUIDs
|
|
44
|
-
// const rawEntropy = `${await getUUID()}-${await getUUID()}-${await getUUID()}-${Date.now()}`;
|
|
45
|
-
// const newSecret = await hash({
|
|
46
|
-
// s: rawEntropy,
|
|
47
|
-
// algorithm: HashAlgorithm.sha_512
|
|
48
|
-
// });
|
|
49
|
-
// localStorage.setItem(key, newSecret);
|
|
50
|
-
// return newSecret;
|
|
51
|
-
}
|
|
52
|
-
catch (error) {
|
|
53
|
-
console.error(`${lc} ${extractErrorMsg(error)}`);
|
|
54
|
-
throw error;
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
/**
|
|
58
|
-
* Removes the stored local delegate secret for a custodian identity.
|
|
59
|
-
*/
|
|
60
|
-
export function clearLocalCustodianDelegateSecret({ primaryTjpAddr, }) {
|
|
61
|
-
if (!primaryTjpAddr) {
|
|
62
|
-
return;
|
|
63
|
-
}
|
|
64
|
-
const key = `${LOCAL_STORAGE_PREFIX}${primaryTjpAddr}`;
|
|
65
|
-
localStorage.removeItem(key);
|
|
66
|
-
}
|
|
67
|
-
//# sourceMappingURL=custodian-delegate-helper.mjs.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"custodian-delegate-helper.mjs","sourceRoot":"","sources":["../../src/identity/custodian-delegate-helper.mts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,aAAa,EAAE,eAAe,EAAE,MAAM,iDAAiD,CAAC;AAChH,OAAO,EAAE,UAAU,EAAE,UAAU,EAAE,MAAM,oCAAoC,CAAC;AAC5E,OAAO,EAAE,eAAe,EAAE,kBAAkB,EAAE,MAAM,oBAAoB,CAAC;AAEzE,MAAM,oBAAoB,GAAG,kCAAkC,CAAC;AAEhE;;;;;GAKG;AACH,MAAM,CAAC,KAAK,UAAU,kCAAkC,CAAC,EACrD,cAAc,GAGjB;IACG,MAAM,EAAE,GAAG,IAAI,kCAAkC,CAAC,IAAI,GAAG,CAAC;IAC1D,IAAI,CAAC;QACD,IAAI,CAAC,cAAc,EAAE,CAAC;YAClB,MAAM,IAAI,KAAK,CAAC,4GAA4G,CAAC,CAAC;QAClI,CAAC;QAED,MAAM,GAAG,GAAG,GAAG,oBAAoB,GAAG,cAAc,EAAE,CAAC;QAEvD,MAAM,MAAM,GAAG,eAAe,EAAE,CAAC;QACjC,MAAM,SAAS,GAAG,kBAAkB,EAAE,CAAC;QAEvC,kDAAkD;QAGlD,MAAM,cAAc,GAAG,MAAM,UAAU,CAAC,EAAE,MAAM,EAAE,SAAS,EAAE,GAAG,EAAE,CAAC,CAAC;QACpE,IAAI,cAAc,EAAE,CAAC;YACjB,OAAO,cAAc,CAAC;QAC1B,CAAC;QAED,iEAAiE;QACjE,MAAM,UAAU,GAAG,MAAM,OAAO,CAAC,EAAE,CAAC,CAAC;QACrC,MAAM,SAAS,GAAG,MAAM,IAAI,CAAC;YACzB,CAAC,EAAE,UAAU;YACb,SAAS,EAAE,aAAa,CAAC,OAAO;SACnC,CAAC,CAAC;QACH,MAAM,UAAU,CAAC,EAAE,MAAM,EAAE,SAAS,EAAE,GAAG,EAAE,KAAK,EAAE,SAAS,EAAE,CAAC,CAAC;QAE/D,OAAO,SAAS,CAAC;QAGjB,oDAAoD;QACpD,wBAAwB;QACxB,6BAA6B;QAC7B,IAAI;QAEJ,oEAAoE;QACpE,+FAA+F;QAC/F,iCAAiC;QACjC,qBAAqB;QACrB,uCAAuC;QACvC,MAAM;QAEN,wCAAwC;QACxC,oBAAoB;IACxB,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACb,OAAO,CAAC,KAAK,CAAC,GAAG,EAAE,IAAI,eAAe,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC;QACjD,MAAM,KAAK,CAAC;IAChB,CAAC;AACL,CAAC;AAED;;GAEG;AACH,MAAM,UAAU,iCAAiC,CAAC,EAC9C,cAAc,GAGjB;IACG,IAAI,CAAC,cAAc,EAAE,CAAC;QAAC,OAAO;IAAC,CAAC;IAChC,MAAM,GAAG,GAAG,GAAG,oBAAoB,GAAG,cAAc,EAAE,CAAC;IACvD,YAAY,CAAC,UAAU,CAAC,GAAG,CAAC,CAAC;AACjC,CAAC"}
|