@ibgib/web-gib 0.0.61 → 0.0.62
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/AUTO-GENERATED-version.d.mts +1 -1
- package/dist/AUTO-GENERATED-version.mjs +1 -1
- package/dist/helpers.web.d.mts +3 -1
- package/dist/helpers.web.d.mts.map +1 -1
- package/dist/helpers.web.mjs +17 -1
- package/dist/helpers.web.mjs.map +1 -1
- package/dist/identity/device-delegate-helper.d.mts +48 -0
- package/dist/identity/device-delegate-helper.d.mts.map +1 -1
- package/dist/identity/device-delegate-helper.mjs +86 -2
- package/dist/identity/device-delegate-helper.mjs.map +1 -1
- package/dist/identity/device-delegate.respec.mjs +6 -6
- package/dist/identity/device-delegate.respec.mjs.map +1 -1
- package/dist/identity/passkey-helper.d.mts +72 -0
- package/dist/identity/passkey-helper.d.mts.map +1 -0
- package/dist/identity/passkey-helper.mjs +237 -0
- package/dist/identity/passkey-helper.mjs.map +1 -0
- package/dist/identity/passkey.respec.d.mts +2 -0
- package/dist/identity/passkey.respec.d.mts.map +1 -0
- package/dist/identity/passkey.respec.mjs +169 -0
- package/dist/identity/passkey.respec.mjs.map +1 -0
- package/dist/index.d.mts +1 -0
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +1 -0
- package/dist/index.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 +1 -0
- package/dist/ui/component/identity/add-device/add-device.mjs.map +1 -1
- package/dist/ui/component/identity/keystone-creator/keystone-creator.d.mts +18 -1
- package/dist/ui/component/identity/keystone-creator/keystone-creator.d.mts.map +1 -1
- package/dist/ui/component/identity/keystone-creator/keystone-creator.html +42 -3
- package/dist/ui/component/identity/keystone-creator/keystone-creator.mjs +510 -74
- package/dist/ui/component/identity/keystone-creator/keystone-creator.mjs.map +1 -1
- package/dist/ui/component/identity/keystone-details/keystone-details.d.mts +16 -1
- package/dist/ui/component/identity/keystone-details/keystone-details.d.mts.map +1 -1
- package/dist/ui/component/identity/keystone-details/keystone-details.html +38 -0
- package/dist/ui/component/identity/keystone-details/keystone-details.mjs +946 -122
- package/dist/ui/component/identity/keystone-details/keystone-details.mjs.map +1 -1
- package/dist/ui/component/identity/remove-device/remove-device.d.mts.map +1 -1
- package/dist/ui/component/identity/remove-device/remove-device.mjs +1 -0
- package/dist/ui/component/identity/remove-device/remove-device.mjs.map +1 -1
- package/dist/ui/ui-helpers.d.mts +3 -0
- package/dist/ui/ui-helpers.d.mts.map +1 -1
- package/dist/ui/ui-helpers.mjs +110 -6
- package/dist/ui/ui-helpers.mjs.map +1 -1
- package/package.json +2 -2
- package/src/AUTO-GENERATED-version.mts +1 -1
- package/src/helpers.web.mts +22 -2
- package/src/identity/device-delegate-helper.mts +135 -2
- package/src/identity/device-delegate.respec.mts +6 -6
- package/src/identity/passkey-helper.mts +304 -0
- package/src/identity/passkey.respec.mts +194 -0
- package/src/index.mts +1 -0
- package/src/ui/component/identity/add-device/add-device.mts +1 -0
- package/src/ui/component/identity/keystone-creator/keystone-creator.html +42 -3
- package/src/ui/component/identity/keystone-creator/keystone-creator.mts +551 -77
- package/src/ui/component/identity/keystone-details/keystone-details.html +38 -0
- package/src/ui/component/identity/keystone-details/keystone-details.mts +1011 -131
- package/src/ui/component/identity/remove-device/remove-device.mts +1 -0
- package/src/ui/ui-helpers.mts +118 -5
|
@@ -2,7 +2,7 @@ import thisCss from "./keystone-details.css";
|
|
|
2
2
|
import thisHtml from "./keystone-details.html";
|
|
3
3
|
import commonComponentStylesCss from "../../common-component-styles.css";
|
|
4
4
|
|
|
5
|
-
import { extractErrorMsg, pretty } from "@ibgib/helper-gib/dist/helpers/utils-helper.mjs";
|
|
5
|
+
import { extractErrorMsg, pretty, getSaferSubstring, HashAlgorithm } from "@ibgib/helper-gib/dist/helpers/utils-helper.mjs";
|
|
6
6
|
import { IbGibAddr } from "@ibgib/ts-gib/dist/types.mjs";
|
|
7
7
|
import { IbGib_V1 } from "@ibgib/ts-gib/dist/V1/types.mjs";
|
|
8
8
|
import { getIbAndGib, getIbGibAddr } from "@ibgib/ts-gib/dist/helper.mjs";
|
|
@@ -20,15 +20,22 @@ import { EVENT_IBGIB_IDENTITY_REQUEST_CHANGE, EVENT_IBGIB_IDENTITY_CHANGED } fro
|
|
|
20
20
|
import { getApiBridge } from "../../../../api/ibgib-api-bridge.mjs";
|
|
21
21
|
import { getKeystoneSyncService } from "../../../../identity/keystone-sync-service.mjs";
|
|
22
22
|
import { updateSpecialIndex } from "@ibgib/core-gib/dist/timeline/timeline-api.mjs";
|
|
23
|
-
import { getTjpAddr, toDto } from "@ibgib/core-gib/dist/common/other/ibgib-helper.mjs";
|
|
23
|
+
import { getTjpAddr, toDto, getTimelinesGroupedByTjp } from "@ibgib/core-gib/dist/common/other/ibgib-helper.mjs";
|
|
24
24
|
import { parseSpaceIb } from "@ibgib/core-gib/dist/witness/space/space-helper.mjs";
|
|
25
25
|
import { executeSsoOAuthPopup } from "../../../../identity/sso-popup-helper.mjs";
|
|
26
26
|
import { getComponentCtorArg } from "../../../../app-bootstrap/init-orchestration.mjs";
|
|
27
27
|
import { promptForSecret } from "../../../../helpers.web.mjs";
|
|
28
|
-
import { isSsoProviderLinked, getKeystoneCapabilities, isDelegateExpired } from "@ibgib/core-gib/dist/keystone/delegate/delegate-helpers.mjs";
|
|
28
|
+
import { isSsoProviderLinked, getKeystoneCapabilities, isDelegateExpired, deriveDelegateSecret, createSyncDelegateKeystone, isCustodialDelegate } from "@ibgib/core-gib/dist/keystone/delegate/delegate-helpers.mjs";
|
|
29
29
|
import { generatePoolChallenges } from "@ibgib/core-gib/dist/keystone/keystone-helpers.mjs";
|
|
30
|
-
import { POOL_ID_MANAGE } from "@ibgib/core-gib/dist/keystone/keystone-constants.mjs";
|
|
30
|
+
import { POOL_ID_MANAGE, KEYSTONE_VERB_SYNC, KEYSTONE_VERB_CONNECT, KEYSTONE_VERB_MANAGE } from "@ibgib/core-gib/dist/keystone/keystone-constants.mjs";
|
|
31
|
+
import { generateDefaultDeviceTag } from "../../../../util/device-helper.mjs";
|
|
31
32
|
import { getGlobalMetaspace_waitIfNeeded } from "../../../../helpers.mjs";
|
|
33
|
+
import { createSovereignDeviceSyncDelegate, createOAuth2DeviceSyncDelegate, createEmailDeviceSyncDelegate } from "../../../../identity/device-delegate-helper.mjs";
|
|
34
|
+
import { isPasskeyPrfSupported, registerPasskeyCredential, evaluatePasskeyPrf, createPasskeyDeviceSyncDelegate, bufferToHex } from "../../../../identity/passkey-helper.mjs";
|
|
35
|
+
import {
|
|
36
|
+
KeystonePasskeyEntry, KeystoneChallengePool, KeystonePoolConfig_HashV1,
|
|
37
|
+
KeystoneChallengeType, KeystoneReplenishStrategy
|
|
38
|
+
} from "@ibgib/core-gib/dist/keystone/keystone-types.mjs";
|
|
32
39
|
|
|
33
40
|
const logalot = false;
|
|
34
41
|
|
|
@@ -89,6 +96,10 @@ interface KeystoneDetailsElements extends ElementsBase {
|
|
|
89
96
|
rawKeystoneJsonEl: HTMLPreElement;
|
|
90
97
|
delegatesCardEl: HTMLDivElement;
|
|
91
98
|
delegatesListEl: HTMLTableSectionElement;
|
|
99
|
+
passkeysCardEl: HTMLDivElement;
|
|
100
|
+
passkeysListEl: HTMLTableSectionElement;
|
|
101
|
+
btnAddPasskeyEl: HTMLButtonElement;
|
|
102
|
+
passkeyStatusMsgEl: HTMLDivElement;
|
|
92
103
|
historicalWarningBannerEl: HTMLDivElement;
|
|
93
104
|
containerEl: HTMLDivElement;
|
|
94
105
|
ssoLinkCardEl: HTMLDivElement;
|
|
@@ -101,6 +112,10 @@ interface KeystoneDetailsElements extends ElementsBase {
|
|
|
101
112
|
btnChangePasswordEl: HTMLButtonElement;
|
|
102
113
|
changePasswordStatusEl: HTMLDivElement;
|
|
103
114
|
changePasswordCardEl: HTMLDivElement;
|
|
115
|
+
sectionAddPasswordOtpEl: HTMLDivElement;
|
|
116
|
+
inputAddPasswordOtpEl: HTMLInputElement;
|
|
117
|
+
btnResendAddPasswordOtpEl: HTMLButtonElement;
|
|
118
|
+
descAddPasswordOtpEl: HTMLParagraphElement;
|
|
104
119
|
}
|
|
105
120
|
|
|
106
121
|
export class KeystoneDetailsComponentInstance
|
|
@@ -120,6 +135,7 @@ export class KeystoneDetailsComponentInstance
|
|
|
120
135
|
private _onIdentityChanged: ((e: any) => void) | null = null;
|
|
121
136
|
private keystoneCache: Map<string, { username: string, description: string, isPrimary: boolean, n: number }> = new Map();
|
|
122
137
|
private aggregatedDetails?: any;
|
|
138
|
+
private resendCooldownInterval: any = null;
|
|
123
139
|
|
|
124
140
|
constructor() {
|
|
125
141
|
super();
|
|
@@ -142,6 +158,39 @@ export class KeystoneDetailsComponentInstance
|
|
|
142
158
|
}
|
|
143
159
|
}
|
|
144
160
|
|
|
161
|
+
override async disconnected(): Promise<void> {
|
|
162
|
+
if (this._onIdentityChanged) {
|
|
163
|
+
window.removeEventListener(EVENT_IBGIB_IDENTITY_CHANGED, this._onIdentityChanged);
|
|
164
|
+
}
|
|
165
|
+
if (this.resendCooldownInterval) {
|
|
166
|
+
clearInterval(this.resendCooldownInterval);
|
|
167
|
+
this.resendCooldownInterval = null;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
private startResendCooldownTimer(durationSecs: number = 30): void {
|
|
172
|
+
const btn = this.elements?.btnResendAddPasswordOtpEl;
|
|
173
|
+
if (!btn) return;
|
|
174
|
+
if (this.resendCooldownInterval) {
|
|
175
|
+
clearInterval(this.resendCooldownInterval);
|
|
176
|
+
this.resendCooldownInterval = null;
|
|
177
|
+
}
|
|
178
|
+
let remaining = durationSecs;
|
|
179
|
+
btn.disabled = true;
|
|
180
|
+
btn.textContent = `Resend Code (${remaining}s)`;
|
|
181
|
+
this.resendCooldownInterval = setInterval(() => {
|
|
182
|
+
remaining -= 1;
|
|
183
|
+
if (remaining <= 0) {
|
|
184
|
+
clearInterval(this.resendCooldownInterval);
|
|
185
|
+
this.resendCooldownInterval = null;
|
|
186
|
+
btn.disabled = false;
|
|
187
|
+
btn.textContent = "Resend Code";
|
|
188
|
+
} else {
|
|
189
|
+
btn.textContent = `Resend Code (${remaining}s)`;
|
|
190
|
+
}
|
|
191
|
+
}, 1000);
|
|
192
|
+
}
|
|
193
|
+
|
|
145
194
|
private async getKeystoneDetails(addr: string): Promise<{ username: string, description: string, isPrimary: boolean, n: number }> {
|
|
146
195
|
const cached = this.keystoneCache.get(addr);
|
|
147
196
|
if (cached) return cached;
|
|
@@ -208,6 +257,10 @@ export class KeystoneDetailsComponentInstance
|
|
|
208
257
|
this.elements.rawKeystoneJsonEl = shadow.getElementById('raw-keystone-json') as HTMLPreElement;
|
|
209
258
|
this.elements.delegatesCardEl = shadow.getElementById('delegates-card') as HTMLDivElement;
|
|
210
259
|
this.elements.delegatesListEl = shadow.getElementById('delegates-list') as HTMLTableSectionElement;
|
|
260
|
+
this.elements.passkeysCardEl = shadow.getElementById('passkeys-card') as HTMLDivElement;
|
|
261
|
+
this.elements.passkeysListEl = shadow.getElementById('passkeys-list') as HTMLTableSectionElement;
|
|
262
|
+
this.elements.btnAddPasskeyEl = shadow.getElementById('btn-add-passkey') as HTMLButtonElement;
|
|
263
|
+
this.elements.passkeyStatusMsgEl = shadow.getElementById('passkey-status-msg') as HTMLDivElement;
|
|
211
264
|
this.elements.containerEl = shadow.getElementById('container') as HTMLDivElement;
|
|
212
265
|
this.elements.historicalWarningBannerEl = shadow.getElementById('historical-warning-banner') as HTMLDivElement;
|
|
213
266
|
this.elements.ssoLinkCardEl = shadow.getElementById('sso-link-card') as HTMLDivElement;
|
|
@@ -220,6 +273,10 @@ export class KeystoneDetailsComponentInstance
|
|
|
220
273
|
this.elements.btnChangePasswordEl = shadow.getElementById('btn-change-password') as HTMLButtonElement;
|
|
221
274
|
this.elements.changePasswordStatusEl = shadow.getElementById('change-password-status') as HTMLDivElement;
|
|
222
275
|
this.elements.changePasswordCardEl = shadow.getElementById('change-password-card') as HTMLDivElement;
|
|
276
|
+
this.elements.sectionAddPasswordOtpEl = shadow.getElementById('section-add-password-otp') as HTMLDivElement;
|
|
277
|
+
this.elements.inputAddPasswordOtpEl = shadow.getElementById('input-add-password-otp') as HTMLInputElement;
|
|
278
|
+
this.elements.btnResendAddPasswordOtpEl = shadow.getElementById('btn-resend-add-password-otp') as HTMLButtonElement;
|
|
279
|
+
this.elements.descAddPasswordOtpEl = shadow.getElementById('desc-add-password-otp') as HTMLParagraphElement;
|
|
223
280
|
|
|
224
281
|
// ElementBase requires contentEl
|
|
225
282
|
this.elements.contentEl = shadow.getElementById('identity-details-view') as HTMLDivElement;
|
|
@@ -245,12 +302,6 @@ export class KeystoneDetailsComponentInstance
|
|
|
245
302
|
}
|
|
246
303
|
}
|
|
247
304
|
|
|
248
|
-
override async disconnected(): Promise<void> {
|
|
249
|
-
if (this._onIdentityChanged) {
|
|
250
|
-
window.removeEventListener(EVENT_IBGIB_IDENTITY_CHANGED, this._onIdentityChanged);
|
|
251
|
-
}
|
|
252
|
-
}
|
|
253
|
-
|
|
254
305
|
private initHandlers() {
|
|
255
306
|
// Setup raw JSON accordion toggle
|
|
256
307
|
this.elements!.rawAccordionHeaderEl.addEventListener('click', () => this.toggleAccordion());
|
|
@@ -260,9 +311,11 @@ export class KeystoneDetailsComponentInstance
|
|
|
260
311
|
this.elements!.btnSetActiveEl.addEventListener('click', () => this.handleSetActiveIdentity());
|
|
261
312
|
this.elements!.btnCheckUpdatesEl.addEventListener('click', () => this.handleCheckForUpdates());
|
|
262
313
|
this.elements!.btnUpdateIdentityEl.addEventListener('click', () => this.handleUpdateIdentity());
|
|
314
|
+
this.elements!.btnAddPasskeyEl?.addEventListener('click', () => this.handleAddPasskey());
|
|
263
315
|
this.elements!.btnLinkGoogleEl.addEventListener('click', () => this.handleSSOClick('google'));
|
|
264
316
|
this.elements!.btnLinkGithubEl.addEventListener('click', () => this.handleSSOClick('github'));
|
|
265
317
|
this.elements!.btnChangePasswordEl.addEventListener('click', () => this.handleChangePassword());
|
|
318
|
+
this.elements!.btnResendAddPasswordOtpEl?.addEventListener('click', () => this.handleResendAddPasswordOtp());
|
|
266
319
|
}
|
|
267
320
|
|
|
268
321
|
private toggleAccordion() {
|
|
@@ -458,15 +511,22 @@ export class KeystoneDetailsComponentInstance
|
|
|
458
511
|
|
|
459
512
|
// Check if origin provider or linked
|
|
460
513
|
const originProvider = this.aggregatedDetails?.custodianProvider || this.ibGib?.data?.frameDetails?.custodianProvider;
|
|
461
|
-
const isGoogleLinked =
|
|
462
|
-
|
|
514
|
+
const isGoogleLinked = !!this.ibGib ?
|
|
515
|
+
isSsoProviderLinked({ keystone: this.ibGib, aggregatedDetails: this.aggregatedDetails, providerId: 'google' }) :
|
|
516
|
+
false;
|
|
517
|
+
const isGithubLinked = !!this.ibGib ?
|
|
518
|
+
isSsoProviderLinked({ keystone: this.ibGib, aggregatedDetails: this.aggregatedDetails, providerId: 'github' }) :
|
|
519
|
+
false;
|
|
463
520
|
|
|
464
|
-
const caps =
|
|
521
|
+
const caps = !!this.ibGib ?
|
|
522
|
+
getKeystoneCapabilities({ keystone: this.ibGib, aggregatedDetails: this.aggregatedDetails }) :
|
|
523
|
+
undefined;
|
|
465
524
|
|
|
466
525
|
// Google Card States
|
|
467
526
|
const googleCard = this.elements.btnLinkGoogleEl;
|
|
468
527
|
const googleLabel = googleCard.querySelector('.toggle-state-text')!;
|
|
469
528
|
if (isGoogleLinked) {
|
|
529
|
+
if (!caps) { throw new Error(`(UNEXPECTED) isGoogleLinked but capabilities falsy? (E: cc62e8357b66860dd2cb8148c833b726)`); }
|
|
470
530
|
googleCard.classList.remove('unlinked');
|
|
471
531
|
googleCard.classList.add('linked');
|
|
472
532
|
const isOrigin = originProvider === 'google';
|
|
@@ -491,6 +551,7 @@ export class KeystoneDetailsComponentInstance
|
|
|
491
551
|
const githubCard = this.elements.btnLinkGithubEl;
|
|
492
552
|
const githubLabel = githubCard.querySelector('.toggle-state-text')!;
|
|
493
553
|
if (isGithubLinked) {
|
|
554
|
+
if (!caps) { throw new Error(`(UNEXPECTED) isGithubLinked but capabilities falsy? (E: e94688210538e0e82878088873a46826)`); }
|
|
494
555
|
githubCard.classList.remove('unlinked');
|
|
495
556
|
githubCard.classList.add('linked');
|
|
496
557
|
const isOrigin = originProvider === 'github';
|
|
@@ -514,13 +575,17 @@ export class KeystoneDetailsComponentInstance
|
|
|
514
575
|
}
|
|
515
576
|
|
|
516
577
|
private updatePasswordCardView(): void {
|
|
517
|
-
const caps =
|
|
578
|
+
const caps = !!this.ibGib ?
|
|
579
|
+
getKeystoneCapabilities({ keystone: this.ibGib, aggregatedDetails: this.aggregatedDetails }) :
|
|
580
|
+
false;
|
|
518
581
|
const cardTitleEl = this.elements?.changePasswordCardEl?.querySelector('h3');
|
|
519
582
|
const cardDescEl = this.elements?.changePasswordCardEl?.querySelector('.description');
|
|
520
583
|
const currentPasswordGroup = this.elements?.inputCurrentPasswordEl?.closest('.form-group') as HTMLElement | null;
|
|
521
584
|
const btnChangePassword = this.elements?.btnChangePasswordEl;
|
|
522
585
|
|
|
523
|
-
const isCustodialOnly = caps
|
|
586
|
+
const isCustodialOnly = !!caps ?
|
|
587
|
+
caps.isCustodial && !caps.isSovereign :
|
|
588
|
+
false;
|
|
524
589
|
if (isCustodialOnly) {
|
|
525
590
|
if (cardTitleEl) cardTitleEl.textContent = "Add Master Password";
|
|
526
591
|
if (cardDescEl) cardDescEl.textContent = "Establish sovereign ownership by adding a master password. This enables direct, independent cryptographic management without relying solely on custodian authentication.";
|
|
@@ -532,6 +597,13 @@ export class KeystoneDetailsComponentInstance
|
|
|
532
597
|
if (currentPasswordGroup) currentPasswordGroup.style.display = '';
|
|
533
598
|
if (btnChangePassword) btnChangePassword.textContent = "Update Password";
|
|
534
599
|
}
|
|
600
|
+
|
|
601
|
+
if (this.elements?.sectionAddPasswordOtpEl) {
|
|
602
|
+
this.elements.sectionAddPasswordOtpEl.classList.add('hidden');
|
|
603
|
+
if (this.elements.inputAddPasswordOtpEl) {
|
|
604
|
+
this.elements.inputAddPasswordOtpEl.value = '';
|
|
605
|
+
}
|
|
606
|
+
}
|
|
535
607
|
}
|
|
536
608
|
|
|
537
609
|
private async updateFrameDetailsView(): Promise<void> {
|
|
@@ -643,16 +715,19 @@ export class KeystoneDetailsComponentInstance
|
|
|
643
715
|
// Render Challenge Pools
|
|
644
716
|
this.renderChallengePools(keystone.data?.challengePools || []);
|
|
645
717
|
|
|
646
|
-
// Render Delegates, SSO link, and Password Change cards (only if this is a primary keystone, hide them for delegate/session keystones)
|
|
718
|
+
// Render Delegates, Passkeys, SSO link, and Password Change cards (only if this is a primary keystone, hide them for delegate/session keystones)
|
|
647
719
|
const details = await this.getKeystoneDetails(this.ibGibAddr);
|
|
648
720
|
if (details.isPrimary) {
|
|
649
721
|
this.elements.delegatesCardEl.classList.remove('hidden');
|
|
650
722
|
await this.renderDelegates(keystone.data?.delegates);
|
|
723
|
+
this.elements.passkeysCardEl.classList.remove('hidden');
|
|
724
|
+
await this.renderPasskeys(this.aggregatedDetails?.passkeys);
|
|
651
725
|
this.elements.ssoLinkCardEl.classList.remove('hidden');
|
|
652
726
|
this.elements.changePasswordCardEl.classList.remove('hidden');
|
|
653
727
|
this.updatePasswordCardView();
|
|
654
728
|
} else {
|
|
655
729
|
this.elements.delegatesCardEl.classList.add('hidden');
|
|
730
|
+
this.elements.passkeysCardEl.classList.add('hidden');
|
|
656
731
|
this.elements.ssoLinkCardEl.classList.add('hidden');
|
|
657
732
|
this.elements.changePasswordCardEl.classList.add('hidden');
|
|
658
733
|
}
|
|
@@ -687,6 +762,15 @@ export class KeystoneDetailsComponentInstance
|
|
|
687
762
|
if (this.elements.delegatesListEl) {
|
|
688
763
|
this.elements.delegatesListEl.innerHTML = '';
|
|
689
764
|
}
|
|
765
|
+
if (this.elements.passkeysListEl) {
|
|
766
|
+
this.elements.passkeysListEl.innerHTML = '';
|
|
767
|
+
}
|
|
768
|
+
if (this.elements.sectionAddPasswordOtpEl) {
|
|
769
|
+
this.elements.sectionAddPasswordOtpEl.classList.add('hidden');
|
|
770
|
+
if (this.elements.inputAddPasswordOtpEl) {
|
|
771
|
+
this.elements.inputAddPasswordOtpEl.value = '';
|
|
772
|
+
}
|
|
773
|
+
}
|
|
690
774
|
}
|
|
691
775
|
|
|
692
776
|
private renderChallengePools(pools: any[]) {
|
|
@@ -1009,8 +1093,9 @@ export class KeystoneDetailsComponentInstance
|
|
|
1009
1093
|
let masterSecret: string;
|
|
1010
1094
|
try {
|
|
1011
1095
|
masterSecret = await promptForSecret({
|
|
1012
|
-
msg: `Enter Master Password to revoke delegate '${deviceName}':`,
|
|
1013
|
-
confirm: false
|
|
1096
|
+
msg: `Enter Master Password or touch security key to revoke delegate '${deviceName}':`,
|
|
1097
|
+
confirm: false,
|
|
1098
|
+
passkeys: this.aggregatedDetails?.passkeys,
|
|
1014
1099
|
});
|
|
1015
1100
|
} catch (err) {
|
|
1016
1101
|
// User cancelled prompt
|
|
@@ -1027,11 +1112,35 @@ export class KeystoneDetailsComponentInstance
|
|
|
1027
1112
|
|
|
1028
1113
|
const keystoneSvc = new KeystoneService_V1();
|
|
1029
1114
|
|
|
1115
|
+
// Resolve which manage pool matches the provided masterSecret
|
|
1116
|
+
let matchedPoolId: string | undefined;
|
|
1117
|
+
const candidatePools = this.ibGib!.data?.challengePools.filter(p =>
|
|
1118
|
+
p.id === 'manage' || p.id.startsWith('passkey-manage-') || (p.config.allowedVerbs && p.config.allowedVerbs.includes(KEYSTONE_VERB_MANAGE))
|
|
1119
|
+
) || [];
|
|
1120
|
+
|
|
1121
|
+
for (const pool of candidatePools) {
|
|
1122
|
+
const ok = await keystoneSvc.verifySigningSecret({
|
|
1123
|
+
keystoneIbGib: this.ibGib!,
|
|
1124
|
+
signingSecret: masterSecret,
|
|
1125
|
+
poolId: pool.id
|
|
1126
|
+
});
|
|
1127
|
+
if (ok) {
|
|
1128
|
+
matchedPoolId = pool.id;
|
|
1129
|
+
break;
|
|
1130
|
+
}
|
|
1131
|
+
}
|
|
1132
|
+
|
|
1133
|
+
if (!matchedPoolId) {
|
|
1134
|
+
this.setSyncStatus("Failed to authorize revocation (invalid secret/passkey).", "error");
|
|
1135
|
+
return;
|
|
1136
|
+
}
|
|
1137
|
+
|
|
1030
1138
|
// Unregister delegate from parent keystone
|
|
1031
1139
|
const evolvedParent = await keystoneSvc.unregisterDelegate({
|
|
1032
1140
|
parentKeystone: this.ibGib!,
|
|
1033
1141
|
delegateTjpAddr,
|
|
1034
1142
|
masterSecret,
|
|
1143
|
+
signingPoolId: matchedPoolId,
|
|
1035
1144
|
metaspace,
|
|
1036
1145
|
space
|
|
1037
1146
|
});
|
|
@@ -1085,6 +1194,477 @@ export class KeystoneDetailsComponentInstance
|
|
|
1085
1194
|
}
|
|
1086
1195
|
}
|
|
1087
1196
|
|
|
1197
|
+
private setPasskeyStatus(msg: string, type: 'info' | 'success' | 'error') {
|
|
1198
|
+
const statusEl = this.elements?.passkeyStatusMsgEl;
|
|
1199
|
+
if (!statusEl) return;
|
|
1200
|
+
statusEl.textContent = msg;
|
|
1201
|
+
statusEl.className = `status-msg ${type}`;
|
|
1202
|
+
statusEl.classList.remove('hidden');
|
|
1203
|
+
}
|
|
1204
|
+
|
|
1205
|
+
private async renderPasskeys(passkeys: Record<string, KeystonePasskeyEntry> | undefined) {
|
|
1206
|
+
if (!this.elements) { return; }
|
|
1207
|
+
const listEl = this.elements.passkeysListEl;
|
|
1208
|
+
listEl.innerHTML = '';
|
|
1209
|
+
|
|
1210
|
+
const caps = !!this.ibGib ?
|
|
1211
|
+
getKeystoneCapabilities({ keystone: this.ibGib, aggregatedDetails: this.aggregatedDetails }) :
|
|
1212
|
+
undefined;
|
|
1213
|
+
|
|
1214
|
+
// Update button text depending on whether the account is custodial-only (elevation) or sovereign/hybrid
|
|
1215
|
+
const isCustodialOnly = !!caps ? caps.isCustodial && !caps.isSovereign : false;
|
|
1216
|
+
if (this.elements.btnAddPasskeyEl) {
|
|
1217
|
+
this.elements.btnAddPasskeyEl.textContent = isCustodialOnly
|
|
1218
|
+
? "🛡️ Add Passkey (Passwordless Sovereign Elevation)"
|
|
1219
|
+
: "🛡️ Register New Passkey / Security Key";
|
|
1220
|
+
}
|
|
1221
|
+
|
|
1222
|
+
const validPasskeyEntries = Object.entries(passkeys || {}).filter(([_, entry]) => entry !== null && entry !== undefined);
|
|
1223
|
+
|
|
1224
|
+
if (validPasskeyEntries.length === 0) {
|
|
1225
|
+
listEl.innerHTML = `
|
|
1226
|
+
<tr>
|
|
1227
|
+
<td colspan="4" style="text-align: center; color: var(--clr-text-secondary, #b3b3b3); padding: 1rem;">
|
|
1228
|
+
No hardware passkeys or biometrics registered.
|
|
1229
|
+
</td>
|
|
1230
|
+
</tr>
|
|
1231
|
+
`;
|
|
1232
|
+
return;
|
|
1233
|
+
}
|
|
1234
|
+
|
|
1235
|
+
for (const [credId, entry] of validPasskeyEntries) {
|
|
1236
|
+
const row = document.createElement('tr');
|
|
1237
|
+
const createdText = entry.createdUTC ? new Date(entry.createdUTC).toLocaleString() : 'N/A';
|
|
1238
|
+
const nameText = entry.name || 'Hardware Key';
|
|
1239
|
+
|
|
1240
|
+
// Format transport badges
|
|
1241
|
+
let transportBadgesHtml = '';
|
|
1242
|
+
if (entry.transports && entry.transports.length > 0) {
|
|
1243
|
+
transportBadgesHtml = entry.transports.map(t => {
|
|
1244
|
+
let icon = '🔑';
|
|
1245
|
+
let label: string = t;
|
|
1246
|
+
if (t === 'internal') { icon = '💻'; label = 'Touch ID / Built-in'; }
|
|
1247
|
+
else if (t === 'usb') { icon = '🔑'; label = 'USB Key'; }
|
|
1248
|
+
else if (t === 'nfc') { icon = '📶'; label = 'NFC'; }
|
|
1249
|
+
else if (t === 'ble' || t === 'hybrid') { icon = '📲'; label = 'Mobile / Bluetooth'; }
|
|
1250
|
+
return `<span class="badge badge-remote-device" style="background-color: #1b5e20; color: #a5d6a7; font-size: 0.75rem; padding: 0.2rem 0.5rem; border-radius: 4px; font-weight: bold; margin-right: 0.3rem;">${icon} ${label}</span>`;
|
|
1251
|
+
}).join(' ');
|
|
1252
|
+
} else {
|
|
1253
|
+
transportBadgesHtml = `<span class="badge badge-remote-device" style="background-color: #2e7d32; color: #c8e6c9; font-size: 0.75rem; padding: 0.2rem 0.5rem; border-radius: 4px; font-weight: bold;">🛡️ Resident Passkey</span>`;
|
|
1254
|
+
}
|
|
1255
|
+
|
|
1256
|
+
row.innerHTML = `
|
|
1257
|
+
<td>
|
|
1258
|
+
<div style="font-weight: 500;">${nameText}</div>
|
|
1259
|
+
<code style="font-size: 0.75rem; color: var(--clr-text-secondary, #b3b3b3); word-break: break-all;">${credId.substring(0, 16)}...</code>
|
|
1260
|
+
</td>
|
|
1261
|
+
<td>${transportBadgesHtml}</td>
|
|
1262
|
+
<td style="font-size: 0.85rem; color: var(--clr-text-secondary, #b3b3b3);">${createdText}</td>
|
|
1263
|
+
<td>
|
|
1264
|
+
<button class="action-btn secondary btn-remove-passkey" style="font-size: 0.8rem; padding: 0.3rem 0.6rem; background: rgba(239, 83, 80, 0.1); color: #ef5350; border: 1px solid rgba(239, 83, 80, 0.3);">
|
|
1265
|
+
Remove
|
|
1266
|
+
</button>
|
|
1267
|
+
</td>
|
|
1268
|
+
`;
|
|
1269
|
+
|
|
1270
|
+
const btnRemove = row.querySelector('.btn-remove-passkey') as HTMLButtonElement;
|
|
1271
|
+
btnRemove?.addEventListener('click', () => this.handleRemovePasskey(credId));
|
|
1272
|
+
|
|
1273
|
+
listEl.appendChild(row);
|
|
1274
|
+
}
|
|
1275
|
+
}
|
|
1276
|
+
|
|
1277
|
+
protected async handleAddPasskey(): Promise<void> {
|
|
1278
|
+
const lc = `${this.lc}[${this.handleAddPasskey.name}]`;
|
|
1279
|
+
if (!this.ibGib || !this.ibGibAddr) { return; }
|
|
1280
|
+
|
|
1281
|
+
try {
|
|
1282
|
+
const caps = getKeystoneCapabilities({ keystone: this.ibGib, aggregatedDetails: this.aggregatedDetails });
|
|
1283
|
+
const isCustodialOnly = caps.isCustodial && !caps.isSovereign;
|
|
1284
|
+
|
|
1285
|
+
const supported = await isPasskeyPrfSupported();
|
|
1286
|
+
if (!supported) {
|
|
1287
|
+
this.setPasskeyStatus("WebAuthn PRF extension is not supported in this browser environment.", "error");
|
|
1288
|
+
return;
|
|
1289
|
+
}
|
|
1290
|
+
|
|
1291
|
+
this.showBusy({
|
|
1292
|
+
isBusy: true,
|
|
1293
|
+
title: 'Touch Biometrics or Security Key',
|
|
1294
|
+
msg: 'Please touch your fingerprint sensor, Face ID, or insert your USB security key when prompted...',
|
|
1295
|
+
animationEmoji: '🛡️'
|
|
1296
|
+
});
|
|
1297
|
+
|
|
1298
|
+
const username = this.aggregatedDetails?.username || this.ibGib.data?.frameDetails?.username || 'user';
|
|
1299
|
+
const rawSalt = crypto.getRandomValues(new Uint8Array(32));
|
|
1300
|
+
const saltHex = bufferToHex(rawSalt.buffer as ArrayBuffer);
|
|
1301
|
+
|
|
1302
|
+
const regResult = await registerPasskeyCredential({
|
|
1303
|
+
username,
|
|
1304
|
+
displayName: username,
|
|
1305
|
+
salt: saltHex
|
|
1306
|
+
});
|
|
1307
|
+
const { credentialId, transports } = regResult;
|
|
1308
|
+
|
|
1309
|
+
let passkeySecret = regResult.passkeySecret;
|
|
1310
|
+
if (!passkeySecret) {
|
|
1311
|
+
const prfResult = await evaluatePasskeyPrf({
|
|
1312
|
+
credentialId,
|
|
1313
|
+
salt: saltHex,
|
|
1314
|
+
transports
|
|
1315
|
+
});
|
|
1316
|
+
passkeySecret = prfResult.passkeySecret;
|
|
1317
|
+
}
|
|
1318
|
+
|
|
1319
|
+
const defaultDeviceTag = generateDefaultDeviceTag();
|
|
1320
|
+
const passkeyName = defaultDeviceTag;
|
|
1321
|
+
const passkeyTag = getSaferSubstring({ text: passkeyName, length: 12, keepLiterals: ['-'] }) || 'key';
|
|
1322
|
+
const poolId = `passkey-manage-${passkeyTag}`;
|
|
1323
|
+
|
|
1324
|
+
const passkeyEntry: KeystonePasskeyEntry = {
|
|
1325
|
+
credentialId,
|
|
1326
|
+
poolId,
|
|
1327
|
+
name: passkeyName,
|
|
1328
|
+
salt: saltHex,
|
|
1329
|
+
createdUTC: new Date().toISOString(),
|
|
1330
|
+
...(transports && transports.length > 0 ? { transports } : {})
|
|
1331
|
+
};
|
|
1332
|
+
|
|
1333
|
+
const metaspace = await getGlobalMetaspace_waitIfNeeded();
|
|
1334
|
+
if (!metaspace) throw new Error("Metaspace not found");
|
|
1335
|
+
const space = await metaspace.getLocalUserSpace({ lock: false });
|
|
1336
|
+
if (!space) throw new Error("Local space not found");
|
|
1337
|
+
|
|
1338
|
+
const keystoneService = new KeystoneService_V1();
|
|
1339
|
+
let evolvedKeystone: KeystoneIbGib_V1;
|
|
1340
|
+
|
|
1341
|
+
if (isCustodialOnly) {
|
|
1342
|
+
// Elevate custodial account with sovereign passkey
|
|
1343
|
+
this.showBusy({
|
|
1344
|
+
isBusy: true,
|
|
1345
|
+
title: 'Elevating Account Ownership...',
|
|
1346
|
+
msg: 'Establishing sovereign passkey pool on identity...',
|
|
1347
|
+
animationEmoji: '⚡'
|
|
1348
|
+
});
|
|
1349
|
+
|
|
1350
|
+
// Generate new manage pool configuration using passkeySecret
|
|
1351
|
+
const poolConfig: KeystonePoolConfig_HashV1 = {
|
|
1352
|
+
id: poolId,
|
|
1353
|
+
type: KeystoneChallengeType.hash_reveal_v1,
|
|
1354
|
+
salt: poolId,
|
|
1355
|
+
algo: HashAlgorithm.sha_256,
|
|
1356
|
+
rounds: 1,
|
|
1357
|
+
allowedVerbs: [KEYSTONE_VERB_MANAGE, KEYSTONE_VERB_SYNC, KEYSTONE_VERB_CONNECT],
|
|
1358
|
+
behavior: {
|
|
1359
|
+
size: 10,
|
|
1360
|
+
replenish: KeystoneReplenishStrategy.topUp,
|
|
1361
|
+
selectSequentially: 1,
|
|
1362
|
+
selectRandomly: 0,
|
|
1363
|
+
targetBindingCount: 0,
|
|
1364
|
+
}
|
|
1365
|
+
};
|
|
1366
|
+
|
|
1367
|
+
const challenges = await generatePoolChallenges({
|
|
1368
|
+
config: poolConfig,
|
|
1369
|
+
masterSecret: passkeySecret
|
|
1370
|
+
});
|
|
1371
|
+
|
|
1372
|
+
const newPool: KeystoneChallengePool = {
|
|
1373
|
+
id: poolId,
|
|
1374
|
+
config: poolConfig,
|
|
1375
|
+
challenges
|
|
1376
|
+
};
|
|
1377
|
+
|
|
1378
|
+
evolvedKeystone = await keystoneService.addPools({
|
|
1379
|
+
latestKeystone: this.ibGib,
|
|
1380
|
+
masterSecret: passkeySecret,
|
|
1381
|
+
newPools: [newPool],
|
|
1382
|
+
frameDetails: {
|
|
1383
|
+
passkeys: {
|
|
1384
|
+
[credentialId]: passkeyEntry
|
|
1385
|
+
}
|
|
1386
|
+
},
|
|
1387
|
+
dataToPatch: {
|
|
1388
|
+
credentialIds: [...(this.ibGib.data?.credentialIds || []), credentialId]
|
|
1389
|
+
},
|
|
1390
|
+
metaspace,
|
|
1391
|
+
space
|
|
1392
|
+
});
|
|
1393
|
+
} else {
|
|
1394
|
+
// Secondary passkey registration on existing sovereign identity:
|
|
1395
|
+
// Prompt user to authorize with current master password
|
|
1396
|
+
let authSecret: string | undefined;
|
|
1397
|
+
let isCorrect = false;
|
|
1398
|
+
let tries = 0;
|
|
1399
|
+
const maxTries = 5;
|
|
1400
|
+
|
|
1401
|
+
while (tries < maxTries && !isCorrect) {
|
|
1402
|
+
try {
|
|
1403
|
+
const promptMsg = tries === 0
|
|
1404
|
+
? 'Enter your current Master Secret (Password) or touch security key to authorize adding this passkey:'
|
|
1405
|
+
: `Incorrect password. Please try again (${maxTries - tries} tries remaining):`;
|
|
1406
|
+
authSecret = await promptForSecret({
|
|
1407
|
+
msg: promptMsg,
|
|
1408
|
+
confirm: false,
|
|
1409
|
+
passkeys: this.aggregatedDetails?.passkeys,
|
|
1410
|
+
});
|
|
1411
|
+
} catch (err) {
|
|
1412
|
+
authSecret = undefined;
|
|
1413
|
+
break;
|
|
1414
|
+
}
|
|
1415
|
+
|
|
1416
|
+
if (!authSecret) break;
|
|
1417
|
+
|
|
1418
|
+
// Verify secret against manage/passkey pools
|
|
1419
|
+
isCorrect = await keystoneService.verifySigningSecret({
|
|
1420
|
+
keystoneIbGib: this.ibGib,
|
|
1421
|
+
signingSecret: authSecret
|
|
1422
|
+
});
|
|
1423
|
+
|
|
1424
|
+
if (!isCorrect) tries++;
|
|
1425
|
+
}
|
|
1426
|
+
|
|
1427
|
+
if (!isCorrect || !authSecret) {
|
|
1428
|
+
this.setPasskeyStatus("Registration cancelled (authorization required).", "error");
|
|
1429
|
+
return;
|
|
1430
|
+
}
|
|
1431
|
+
|
|
1432
|
+
this.showBusy({
|
|
1433
|
+
isBusy: true,
|
|
1434
|
+
title: 'Registering Passkey...',
|
|
1435
|
+
msg: 'Adding cryptographic challenge pool and evolving identity...',
|
|
1436
|
+
animationEmoji: '🛡️'
|
|
1437
|
+
});
|
|
1438
|
+
|
|
1439
|
+
const poolConfig: KeystonePoolConfig_HashV1 = {
|
|
1440
|
+
id: poolId,
|
|
1441
|
+
type: KeystoneChallengeType.hash_reveal_v1,
|
|
1442
|
+
salt: poolId,
|
|
1443
|
+
algo: HashAlgorithm.sha_256,
|
|
1444
|
+
rounds: 1,
|
|
1445
|
+
allowedVerbs: [KEYSTONE_VERB_MANAGE, KEYSTONE_VERB_SYNC, KEYSTONE_VERB_CONNECT],
|
|
1446
|
+
behavior: {
|
|
1447
|
+
size: 10,
|
|
1448
|
+
replenish: KeystoneReplenishStrategy.topUp,
|
|
1449
|
+
selectSequentially: 1,
|
|
1450
|
+
selectRandomly: 0,
|
|
1451
|
+
targetBindingCount: 0,
|
|
1452
|
+
}
|
|
1453
|
+
};
|
|
1454
|
+
|
|
1455
|
+
const challenges = await generatePoolChallenges({
|
|
1456
|
+
config: poolConfig,
|
|
1457
|
+
masterSecret: passkeySecret
|
|
1458
|
+
});
|
|
1459
|
+
|
|
1460
|
+
const newPool: KeystoneChallengePool = {
|
|
1461
|
+
id: poolId,
|
|
1462
|
+
config: poolConfig,
|
|
1463
|
+
challenges
|
|
1464
|
+
};
|
|
1465
|
+
|
|
1466
|
+
evolvedKeystone = await keystoneService.addPools({
|
|
1467
|
+
latestKeystone: this.ibGib,
|
|
1468
|
+
masterSecret: authSecret,
|
|
1469
|
+
newPools: [newPool],
|
|
1470
|
+
frameDetails: {
|
|
1471
|
+
passkeys: {
|
|
1472
|
+
[credentialId]: passkeyEntry
|
|
1473
|
+
}
|
|
1474
|
+
},
|
|
1475
|
+
dataToPatch: {
|
|
1476
|
+
credentialIds: [...(this.ibGib.data?.credentialIds || []), credentialId]
|
|
1477
|
+
},
|
|
1478
|
+
metaspace,
|
|
1479
|
+
space
|
|
1480
|
+
});
|
|
1481
|
+
}
|
|
1482
|
+
|
|
1483
|
+
// Sync evolved keystone to server
|
|
1484
|
+
this.showBusy({
|
|
1485
|
+
isBusy: true,
|
|
1486
|
+
title: 'Syncing to Server...',
|
|
1487
|
+
msg: 'Publishing evolved identity keystone to server...',
|
|
1488
|
+
animationEmoji: '⚡'
|
|
1489
|
+
});
|
|
1490
|
+
|
|
1491
|
+
const primaryTjpAddr = getTjpAddr({ ibGib: evolvedKeystone, defaultIfNone: 'incomingAddr' })!;
|
|
1492
|
+
const resSync = await getApiBridge().putEvolveKeystone({
|
|
1493
|
+
domainAddr: primaryTjpAddr,
|
|
1494
|
+
keystoneIbGib: evolvedKeystone,
|
|
1495
|
+
relatedKeystoneIbGibs: []
|
|
1496
|
+
});
|
|
1497
|
+
|
|
1498
|
+
if (!resSync.success) {
|
|
1499
|
+
console.warn(`${lc} Server sync note: ${resSync.message}`);
|
|
1500
|
+
}
|
|
1501
|
+
|
|
1502
|
+
// Update Special Index
|
|
1503
|
+
await updateSpecialIndex({
|
|
1504
|
+
type: "keystones",
|
|
1505
|
+
rel8nInfos: [
|
|
1506
|
+
{
|
|
1507
|
+
rel8nName: "keystone",
|
|
1508
|
+
ibGibs: [toDto({ ibGib: evolvedKeystone })]
|
|
1509
|
+
}
|
|
1510
|
+
],
|
|
1511
|
+
metaspace,
|
|
1512
|
+
space
|
|
1513
|
+
});
|
|
1514
|
+
|
|
1515
|
+
this.setPasskeyStatus("Passkey successfully registered and bound to identity!", "success");
|
|
1516
|
+
this.ibGibAddr = getIbGibAddr({ ibGib: evolvedKeystone });
|
|
1517
|
+
this.staticIbGib = evolvedKeystone;
|
|
1518
|
+
await this.loadIbGib({ getLatest: false });
|
|
1519
|
+
|
|
1520
|
+
} catch (error) {
|
|
1521
|
+
console.error(`${lc} ${extractErrorMsg(error)}`, error);
|
|
1522
|
+
this.setPasskeyStatus(`Failed to register passkey: ${extractErrorMsg(error)}`, "error");
|
|
1523
|
+
} finally {
|
|
1524
|
+
this.showBusy({ isBusy: false });
|
|
1525
|
+
}
|
|
1526
|
+
}
|
|
1527
|
+
|
|
1528
|
+
protected async handleRemovePasskey(credentialId: string): Promise<void> {
|
|
1529
|
+
const lc = `${this.lc}[${this.handleRemovePasskey.name}]`;
|
|
1530
|
+
if (!this.ibGib || !this.ibGibAddr) { return; }
|
|
1531
|
+
|
|
1532
|
+
const passkeyEntry = this.aggregatedDetails?.passkeys?.[credentialId];
|
|
1533
|
+
const passkeyName = passkeyEntry?.name || 'this passkey';
|
|
1534
|
+
|
|
1535
|
+
try {
|
|
1536
|
+
const keystoneService = new KeystoneService_V1();
|
|
1537
|
+
let masterSecret: string | undefined;
|
|
1538
|
+
let isCorrect = false;
|
|
1539
|
+
let tries = 0;
|
|
1540
|
+
const maxTries = 5;
|
|
1541
|
+
|
|
1542
|
+
while (tries < maxTries && !isCorrect) {
|
|
1543
|
+
try {
|
|
1544
|
+
const promptMsg = tries === 0
|
|
1545
|
+
? `Are you sure you want to remove "${passkeyName}"? Enter your Master Secret (Password) or touch security key to authorize:`
|
|
1546
|
+
: `Incorrect password. Please try again (${maxTries - tries} tries remaining):`;
|
|
1547
|
+
masterSecret = await promptForSecret({
|
|
1548
|
+
msg: promptMsg,
|
|
1549
|
+
confirm: false,
|
|
1550
|
+
passkeys: this.aggregatedDetails?.passkeys,
|
|
1551
|
+
});
|
|
1552
|
+
} catch (err) {
|
|
1553
|
+
masterSecret = undefined;
|
|
1554
|
+
break;
|
|
1555
|
+
}
|
|
1556
|
+
|
|
1557
|
+
if (!masterSecret) break;
|
|
1558
|
+
|
|
1559
|
+
isCorrect = await keystoneService.verifySigningSecret({
|
|
1560
|
+
keystoneIbGib: this.ibGib,
|
|
1561
|
+
signingSecret: masterSecret
|
|
1562
|
+
});
|
|
1563
|
+
|
|
1564
|
+
if (!isCorrect) tries++;
|
|
1565
|
+
}
|
|
1566
|
+
|
|
1567
|
+
if (!isCorrect || !masterSecret) {
|
|
1568
|
+
this.setPasskeyStatus("Removal cancelled or unauthorized.", "error");
|
|
1569
|
+
return;
|
|
1570
|
+
}
|
|
1571
|
+
|
|
1572
|
+
this.showBusy({
|
|
1573
|
+
isBusy: true,
|
|
1574
|
+
title: 'Revoking Passkey...',
|
|
1575
|
+
msg: 'Tombstoning passkey record and removing challenge pool...',
|
|
1576
|
+
animationEmoji: '🗑️'
|
|
1577
|
+
});
|
|
1578
|
+
|
|
1579
|
+
const metaspace = await getGlobalMetaspace_waitIfNeeded();
|
|
1580
|
+
if (!metaspace) throw new Error("Metaspace not found");
|
|
1581
|
+
const space = await metaspace.getLocalUserSpace({ lock: false });
|
|
1582
|
+
if (!space) throw new Error("Local space not found");
|
|
1583
|
+
|
|
1584
|
+
const poolIdToRemove = passkeyEntry?.poolId || `passkey-manage-${credentialId.substring(0, 8)}`;
|
|
1585
|
+
const poolExists = this.ibGib.data?.challengePools.some(p => p.id === poolIdToRemove);
|
|
1586
|
+
|
|
1587
|
+
const filteredCredentialIds = (this.ibGib.data?.credentialIds || []).filter(id => id !== credentialId);
|
|
1588
|
+
|
|
1589
|
+
let evolvedKeystone: KeystoneIbGib_V1;
|
|
1590
|
+
if (poolExists) {
|
|
1591
|
+
evolvedKeystone = await keystoneService.removePools({
|
|
1592
|
+
latestKeystone: this.ibGib,
|
|
1593
|
+
masterSecret,
|
|
1594
|
+
poolIds: [poolIdToRemove],
|
|
1595
|
+
frameDetails: {
|
|
1596
|
+
passkeys: {
|
|
1597
|
+
[credentialId]: null
|
|
1598
|
+
}
|
|
1599
|
+
},
|
|
1600
|
+
dataToPatch: {
|
|
1601
|
+
credentialIds: filteredCredentialIds
|
|
1602
|
+
},
|
|
1603
|
+
metaspace,
|
|
1604
|
+
space
|
|
1605
|
+
});
|
|
1606
|
+
} else {
|
|
1607
|
+
evolvedKeystone = await keystoneService.sign({
|
|
1608
|
+
latestKeystone: this.ibGib,
|
|
1609
|
+
masterSecret,
|
|
1610
|
+
claim: {
|
|
1611
|
+
verb: POOL_ID_MANAGE,
|
|
1612
|
+
target: getIbGibAddr({ ibGib: this.ibGib })
|
|
1613
|
+
},
|
|
1614
|
+
frameDetails: {
|
|
1615
|
+
passkeys: {
|
|
1616
|
+
[credentialId]: null
|
|
1617
|
+
}
|
|
1618
|
+
},
|
|
1619
|
+
dataToPatch: {
|
|
1620
|
+
credentialIds: filteredCredentialIds
|
|
1621
|
+
},
|
|
1622
|
+
metaspace,
|
|
1623
|
+
space
|
|
1624
|
+
});
|
|
1625
|
+
}
|
|
1626
|
+
|
|
1627
|
+
// Sync to server
|
|
1628
|
+
this.showBusy({
|
|
1629
|
+
isBusy: true,
|
|
1630
|
+
title: 'Syncing to Server...',
|
|
1631
|
+
msg: 'Publishing updated keystone to server...',
|
|
1632
|
+
animationEmoji: '⚡'
|
|
1633
|
+
});
|
|
1634
|
+
|
|
1635
|
+
const primaryTjpAddr = getTjpAddr({ ibGib: evolvedKeystone, defaultIfNone: 'incomingAddr' })!;
|
|
1636
|
+
await getApiBridge().putEvolveKeystone({
|
|
1637
|
+
domainAddr: primaryTjpAddr,
|
|
1638
|
+
keystoneIbGib: evolvedKeystone,
|
|
1639
|
+
relatedKeystoneIbGibs: []
|
|
1640
|
+
});
|
|
1641
|
+
|
|
1642
|
+
// Update Special Index
|
|
1643
|
+
await updateSpecialIndex({
|
|
1644
|
+
type: "keystones",
|
|
1645
|
+
rel8nInfos: [
|
|
1646
|
+
{
|
|
1647
|
+
rel8nName: "keystone",
|
|
1648
|
+
ibGibs: [toDto({ ibGib: evolvedKeystone })]
|
|
1649
|
+
}
|
|
1650
|
+
],
|
|
1651
|
+
metaspace,
|
|
1652
|
+
space
|
|
1653
|
+
});
|
|
1654
|
+
|
|
1655
|
+
this.setPasskeyStatus(`Passkey "${passkeyName}" successfully removed.`, "success");
|
|
1656
|
+
this.ibGibAddr = getIbGibAddr({ ibGib: evolvedKeystone });
|
|
1657
|
+
this.staticIbGib = evolvedKeystone;
|
|
1658
|
+
await this.loadIbGib({ getLatest: false });
|
|
1659
|
+
|
|
1660
|
+
} catch (error) {
|
|
1661
|
+
console.error(`${lc} ${extractErrorMsg(error)}`, error);
|
|
1662
|
+
this.setPasskeyStatus(`Failed to remove passkey: ${extractErrorMsg(error)}`, "error");
|
|
1663
|
+
} finally {
|
|
1664
|
+
this.showBusy({ isBusy: false });
|
|
1665
|
+
}
|
|
1666
|
+
}
|
|
1667
|
+
|
|
1088
1668
|
override async handleContextUpdated(): Promise<void> {
|
|
1089
1669
|
const lc = `${this.lc}[${this.handleContextUpdated.name}]`;
|
|
1090
1670
|
try {
|
|
@@ -1124,10 +1704,26 @@ export class KeystoneDetailsComponentInstance
|
|
|
1124
1704
|
const lc = `${this.lc}[${this.handleSSOLink.name}]`;
|
|
1125
1705
|
this.setSSOStatus(`Linking ${providerId}...`, "info");
|
|
1126
1706
|
|
|
1707
|
+
const providerName = providerId === 'google' ? 'Google' : providerId === 'github' ? 'GitHub' : providerId;
|
|
1708
|
+
this.setSSOStatus(`Opening ${providerName} SSO authorization window...`, "info");
|
|
1709
|
+
this.showBusy({
|
|
1710
|
+
isBusy: true,
|
|
1711
|
+
title: `Connecting to ${providerName}...`,
|
|
1712
|
+
msg: `Opening ${providerName} SSO authorization window...\nPlease complete authorization in the popup window.`,
|
|
1713
|
+
animationEmoji: '🌐'
|
|
1714
|
+
});
|
|
1715
|
+
|
|
1127
1716
|
try {
|
|
1717
|
+
const primaryTjpAddr = (this.ibGib ? getTjpAddr({ ibGib: this.ibGib }) : undefined)
|
|
1718
|
+
|| (this.ibGibAddr ? (() => {
|
|
1719
|
+
const { ib } = getIbAndGib({ ibGibAddr: this.ibGibAddr });
|
|
1720
|
+
const { tjpGib } = getGibInfo({ ibGibAddr: this.ibGibAddr });
|
|
1721
|
+
return tjpGib && ib ? `${ib}^${tjpGib}` : this.ibGibAddr;
|
|
1722
|
+
})() : undefined);
|
|
1723
|
+
|
|
1128
1724
|
const resPopup = await executeSsoOAuthPopup({
|
|
1129
1725
|
providerId,
|
|
1130
|
-
parentTjpAddr:
|
|
1726
|
+
parentTjpAddr: primaryTjpAddr
|
|
1131
1727
|
});
|
|
1132
1728
|
|
|
1133
1729
|
if (!resPopup.success || !resPopup.code) {
|
|
@@ -1139,6 +1735,8 @@ export class KeystoneDetailsComponentInstance
|
|
|
1139
1735
|
} catch (error) {
|
|
1140
1736
|
console.error(`${lc} SSO link initiation failed: ${extractErrorMsg(error)}`);
|
|
1141
1737
|
this.setSSOStatus(`Link failed: ${extractErrorMsg(error)}`, "error");
|
|
1738
|
+
} finally {
|
|
1739
|
+
this.showBusy({ isBusy: false });
|
|
1142
1740
|
}
|
|
1143
1741
|
}
|
|
1144
1742
|
|
|
@@ -1151,7 +1749,14 @@ export class KeystoneDetailsComponentInstance
|
|
|
1151
1749
|
|
|
1152
1750
|
protected async handleSSOLinkCallback(code: string, stateObj: any, redirectUri: string) {
|
|
1153
1751
|
const lc = `${this.lc}[${this.handleSSOLinkCallback.name}]`;
|
|
1154
|
-
|
|
1752
|
+
const providerName = stateObj.providerId === 'google' ? 'Google' : stateObj.providerId === 'github' ? 'GitHub' : stateObj.providerId;
|
|
1753
|
+
this.setSSOStatus(`Retrieving challenge pool configuration from server...`, "info");
|
|
1754
|
+
this.showBusy({
|
|
1755
|
+
isBusy: true,
|
|
1756
|
+
title: `Linking ${providerName}...`,
|
|
1757
|
+
msg: `Validating ${providerName} authorization with server...`,
|
|
1758
|
+
animationEmoji: '🔐'
|
|
1759
|
+
});
|
|
1155
1760
|
|
|
1156
1761
|
try {
|
|
1157
1762
|
// 1. Exchange OAuth code for custodian public pool config
|
|
@@ -1179,6 +1784,7 @@ export class KeystoneDetailsComponentInstance
|
|
|
1179
1784
|
const custodianPool = linkData.custodianPool;
|
|
1180
1785
|
|
|
1181
1786
|
// 2. Prompt user for master secret and verify it
|
|
1787
|
+
this.showBusy({ isBusy: false });
|
|
1182
1788
|
const keystoneService = new KeystoneService_V1();
|
|
1183
1789
|
let masterSecret: string | undefined;
|
|
1184
1790
|
let isCorrect = false;
|
|
@@ -1222,6 +1828,12 @@ export class KeystoneDetailsComponentInstance
|
|
|
1222
1828
|
return;
|
|
1223
1829
|
}
|
|
1224
1830
|
|
|
1831
|
+
this.showBusy({
|
|
1832
|
+
isBusy: true,
|
|
1833
|
+
title: 'Evolving Identity...',
|
|
1834
|
+
msg: `Installing ${providerName} custodian pool and signing evolution...`,
|
|
1835
|
+
animationEmoji: '📝'
|
|
1836
|
+
});
|
|
1225
1837
|
this.setSSOStatus("Evolving identity locally and generating cryptographic signatures...", "info");
|
|
1226
1838
|
|
|
1227
1839
|
const metaspace = await getGlobalMetaspace_waitIfNeeded();
|
|
@@ -1229,7 +1841,7 @@ export class KeystoneDetailsComponentInstance
|
|
|
1229
1841
|
const space = await metaspace.getLocalUserSpace({ lock: false });
|
|
1230
1842
|
if (!space) { throw new Error("Local space not found"); }
|
|
1231
1843
|
|
|
1232
|
-
// 3. Evolve parent keystone to include the custodian pool
|
|
1844
|
+
// 3. Evolve parent keystone to include the custodian pool (n -> n+1)
|
|
1233
1845
|
const evolvedParent = await keystoneService.addPools({
|
|
1234
1846
|
latestKeystone: this.ibGib!,
|
|
1235
1847
|
masterSecret: masterSecret!,
|
|
@@ -1238,24 +1850,64 @@ export class KeystoneDetailsComponentInstance
|
|
|
1238
1850
|
space
|
|
1239
1851
|
});
|
|
1240
1852
|
|
|
1241
|
-
//
|
|
1242
|
-
|
|
1243
|
-
const syncRes = await getApiBridge().putEvolveKeystone({
|
|
1853
|
+
// Sync evolved parent (n+1) to server first
|
|
1854
|
+
const syncRes1 = await getApiBridge().putEvolveKeystone({
|
|
1244
1855
|
domainAddr: stateObj.parentTjpAddr,
|
|
1245
1856
|
keystoneIbGib: evolvedParent,
|
|
1246
|
-
relatedKeystoneIbGibs: []
|
|
1857
|
+
relatedKeystoneIbGibs: []
|
|
1247
1858
|
});
|
|
1248
|
-
if (!
|
|
1249
|
-
throw new Error(`Server synchronization rejected: ${
|
|
1859
|
+
if (!syncRes1.success) {
|
|
1860
|
+
throw new Error(`Server synchronization of linked pool rejected: ${syncRes1.message}`);
|
|
1250
1861
|
}
|
|
1251
1862
|
|
|
1252
|
-
//
|
|
1863
|
+
// 4. Create and register OAuth2 sync delegate for this provider (n+1 -> n+2)
|
|
1864
|
+
const username = this.aggregatedDetails?.username || this.ibGib?.data?.username || 'user';
|
|
1865
|
+
const { delegateKeystone, deviceTag } = await createOAuth2DeviceSyncDelegate({
|
|
1866
|
+
providerId: stateObj.providerId,
|
|
1867
|
+
username,
|
|
1868
|
+
primaryTjpAddr: stateObj.parentTjpAddr,
|
|
1869
|
+
custodianReqs: linkData.custodianReqs,
|
|
1870
|
+
metaspace,
|
|
1871
|
+
space,
|
|
1872
|
+
});
|
|
1873
|
+
|
|
1874
|
+
const parentWithDelegate = await keystoneService.registerDelegate({
|
|
1875
|
+
parentKeystone: evolvedParent,
|
|
1876
|
+
delegateKeystone,
|
|
1877
|
+
masterSecret: masterSecret!,
|
|
1878
|
+
signingPoolId: 'manage',
|
|
1879
|
+
metaspace,
|
|
1880
|
+
space,
|
|
1881
|
+
allowedVerbs: [KEYSTONE_VERB_SYNC, KEYSTONE_VERB_CONNECT],
|
|
1882
|
+
deviceName: deviceTag,
|
|
1883
|
+
});
|
|
1884
|
+
|
|
1885
|
+
await metaspace.put({ ibGibs: [delegateKeystone, parentWithDelegate], space });
|
|
1886
|
+
|
|
1887
|
+
// 5. Sync parent with registered delegate (n+2) to server
|
|
1888
|
+
this.showBusy({
|
|
1889
|
+
isBusy: true,
|
|
1890
|
+
title: 'Syncing to Server...',
|
|
1891
|
+
msg: 'Publishing linked identity and delegate to server...',
|
|
1892
|
+
animationEmoji: '⚡'
|
|
1893
|
+
});
|
|
1894
|
+
this.setSSOStatus("Publishing evolved identity keystone to server...", "info");
|
|
1895
|
+
const syncRes2 = await getApiBridge().putEvolveKeystone({
|
|
1896
|
+
domainAddr: stateObj.parentTjpAddr,
|
|
1897
|
+
keystoneIbGib: parentWithDelegate,
|
|
1898
|
+
relatedKeystoneIbGibs: [delegateKeystone]
|
|
1899
|
+
});
|
|
1900
|
+
if (!syncRes2.success) {
|
|
1901
|
+
throw new Error(`Server synchronization of delegate rejected: ${syncRes2.message}`);
|
|
1902
|
+
}
|
|
1903
|
+
|
|
1904
|
+
// 6. Update index locally
|
|
1253
1905
|
await updateSpecialIndex({
|
|
1254
1906
|
type: "keystones",
|
|
1255
1907
|
rel8nInfos: [
|
|
1256
1908
|
{
|
|
1257
1909
|
rel8nName: "keystone",
|
|
1258
|
-
ibGibs: [toDto({ ibGib:
|
|
1910
|
+
ibGibs: [toDto({ ibGib: parentWithDelegate }), toDto({ ibGib: delegateKeystone })]
|
|
1259
1911
|
}
|
|
1260
1912
|
],
|
|
1261
1913
|
metaspace,
|
|
@@ -1263,18 +1915,24 @@ export class KeystoneDetailsComponentInstance
|
|
|
1263
1915
|
});
|
|
1264
1916
|
|
|
1265
1917
|
this.setSSOStatus(`Successfully linked with ${stateObj.providerId}!`, "success");
|
|
1918
|
+
this.ibGibAddr = getIbGibAddr({ ibGib: parentWithDelegate });
|
|
1919
|
+
this.staticIbGib = parentWithDelegate;
|
|
1266
1920
|
await this.loadIbGib({ getLatest: false });
|
|
1267
1921
|
|
|
1268
1922
|
} catch (error) {
|
|
1269
1923
|
console.error(`${lc} SSO callback processing failed: ${extractErrorMsg(error)}`);
|
|
1270
1924
|
this.setSSOStatus(`Link failed: ${extractErrorMsg(error)}`, "error");
|
|
1925
|
+
} finally {
|
|
1926
|
+
this.showBusy({ isBusy: false });
|
|
1271
1927
|
}
|
|
1272
1928
|
}
|
|
1273
1929
|
|
|
1274
1930
|
protected async handleSSOClick(providerId: 'google' | 'github') {
|
|
1275
|
-
const isLinked =
|
|
1931
|
+
const isLinked = !!this.ibGib ?
|
|
1932
|
+
isSsoProviderLinked({ keystone: this.ibGib, aggregatedDetails: this.aggregatedDetails, providerId }) :
|
|
1933
|
+
false;
|
|
1276
1934
|
if (isLinked) {
|
|
1277
|
-
const caps = getKeystoneCapabilities({ keystone: this.ibGib
|
|
1935
|
+
const caps = getKeystoneCapabilities({ keystone: this.ibGib!, aggregatedDetails: this.aggregatedDetails });
|
|
1278
1936
|
if (!caps.isSovereign) {
|
|
1279
1937
|
this.setSSOStatus(`Cannot unlink ${providerId}: This is your only identity recovery and management method. Please add a Master Password first in the Password card below to enable sovereign ownership before unlinking.`, "error");
|
|
1280
1938
|
return;
|
|
@@ -1287,7 +1945,8 @@ export class KeystoneDetailsComponentInstance
|
|
|
1287
1945
|
|
|
1288
1946
|
protected async handleSSOUnlink(providerId: 'google' | 'github') {
|
|
1289
1947
|
const lc = `${this.lc}[${this.handleSSOUnlink.name}]`;
|
|
1290
|
-
|
|
1948
|
+
const providerName = providerId === 'google' ? 'Google' : providerId === 'github' ? 'GitHub' : providerId;
|
|
1949
|
+
this.setSSOStatus(`Unlinking ${providerName}...`, "info");
|
|
1291
1950
|
|
|
1292
1951
|
try {
|
|
1293
1952
|
// 1. Prompt user for password
|
|
@@ -1300,11 +1959,12 @@ export class KeystoneDetailsComponentInstance
|
|
|
1300
1959
|
while (tries < maxTries && !isCorrect) {
|
|
1301
1960
|
try {
|
|
1302
1961
|
const promptMsg = tries === 0
|
|
1303
|
-
? `Are you sure you want to unlink your ${
|
|
1962
|
+
? `Are you sure you want to unlink your ${providerName} account? If so, enter your local Master Secret (Password) or touch security key to authorize. Otherwise, click Cancel.`
|
|
1304
1963
|
: `Incorrect password. Please try again (${maxTries - tries} tries remaining):`;
|
|
1305
1964
|
masterSecret = await promptForSecret({
|
|
1306
1965
|
msg: promptMsg,
|
|
1307
|
-
confirm: false
|
|
1966
|
+
confirm: false,
|
|
1967
|
+
passkeys: this.aggregatedDetails?.passkeys,
|
|
1308
1968
|
});
|
|
1309
1969
|
} catch (error) {
|
|
1310
1970
|
masterSecret = undefined;
|
|
@@ -1315,11 +1975,10 @@ export class KeystoneDetailsComponentInstance
|
|
|
1315
1975
|
break;
|
|
1316
1976
|
}
|
|
1317
1977
|
|
|
1318
|
-
// Verify secret using verifySigningSecret
|
|
1978
|
+
// Verify secret using verifySigningSecret
|
|
1319
1979
|
isCorrect = await keystoneService.verifySigningSecret({
|
|
1320
1980
|
keystoneIbGib: this.ibGib!,
|
|
1321
|
-
signingSecret: masterSecret
|
|
1322
|
-
poolId: 'manage'
|
|
1981
|
+
signingSecret: masterSecret
|
|
1323
1982
|
});
|
|
1324
1983
|
|
|
1325
1984
|
if (!isCorrect) {
|
|
@@ -1332,6 +1991,12 @@ export class KeystoneDetailsComponentInstance
|
|
|
1332
1991
|
return;
|
|
1333
1992
|
}
|
|
1334
1993
|
|
|
1994
|
+
this.showBusy({
|
|
1995
|
+
isBusy: true,
|
|
1996
|
+
title: 'Unlinking Provider...',
|
|
1997
|
+
msg: `Removing ${providerName} custodian pool and delegate keys...`,
|
|
1998
|
+
animationEmoji: '🔓'
|
|
1999
|
+
});
|
|
1335
2000
|
this.setSSOStatus("Evolving identity locally and removing custodian pool...", "info");
|
|
1336
2001
|
|
|
1337
2002
|
const metaspace = await getGlobalMetaspace_waitIfNeeded();
|
|
@@ -1349,33 +2014,63 @@ export class KeystoneDetailsComponentInstance
|
|
|
1349
2014
|
space
|
|
1350
2015
|
});
|
|
1351
2016
|
|
|
1352
|
-
|
|
1353
|
-
|
|
2017
|
+
const primaryTjpAddr = (this.ibGib ? getTjpAddr({ ibGib: this.ibGib }) : undefined)
|
|
2018
|
+
|| (this.ibGibAddr ? (() => {
|
|
2019
|
+
const { ib } = getIbAndGib({ ibGibAddr: this.ibGibAddr });
|
|
2020
|
+
const { tjpGib } = getGibInfo({ ibGibAddr: this.ibGibAddr });
|
|
2021
|
+
return tjpGib && ib ? `${ib}^${tjpGib}` : this.ibGibAddr;
|
|
2022
|
+
})() : undefined)
|
|
2023
|
+
|| getIbGibAddr({ ibGib: this.ibGib! });
|
|
2024
|
+
|
|
2025
|
+
// 3. Sync pool removal frame to server
|
|
2026
|
+
this.showBusy({
|
|
2027
|
+
isBusy: true,
|
|
2028
|
+
title: 'Syncing to Server...',
|
|
2029
|
+
msg: `Publishing ${providerName} pool removal to server...`,
|
|
2030
|
+
animationEmoji: '⚡'
|
|
2031
|
+
});
|
|
2032
|
+
this.setSSOStatus("Publishing evolved identity keystone to server...", "info");
|
|
2033
|
+
let syncRes = await getApiBridge().putEvolveKeystone({
|
|
2034
|
+
domainAddr: primaryTjpAddr,
|
|
2035
|
+
keystoneIbGib: evolvedParent,
|
|
2036
|
+
relatedKeystoneIbGibs: [] // No outward delegate timelines
|
|
2037
|
+
});
|
|
2038
|
+
if (!syncRes.success) {
|
|
2039
|
+
throw new Error(`Server synchronization rejected: ${syncRes.message}`);
|
|
2040
|
+
}
|
|
2041
|
+
|
|
2042
|
+
// 4. Unregister any corresponding outward delegate for this provider if present
|
|
2043
|
+
const delegatesMap = evolvedParent.data?.delegates || this.ibGib?.data?.delegates;
|
|
1354
2044
|
if (delegatesMap) {
|
|
1355
2045
|
for (const [delegateTjpAddr, info] of Object.entries(delegatesMap)) {
|
|
1356
|
-
if (info
|
|
2046
|
+
if (isCustodialDelegate({ delegateInfo: info, providerId })) {
|
|
1357
2047
|
evolvedParent = await keystoneService.unregisterDelegate({
|
|
1358
2048
|
parentKeystone: evolvedParent,
|
|
1359
2049
|
delegateTjpAddr,
|
|
1360
2050
|
masterSecret: masterSecret!,
|
|
2051
|
+
signingPoolId: 'manage',
|
|
1361
2052
|
metaspace,
|
|
1362
2053
|
space,
|
|
1363
2054
|
});
|
|
2055
|
+
|
|
2056
|
+
this.showBusy({
|
|
2057
|
+
isBusy: true,
|
|
2058
|
+
title: 'Syncing to Server...',
|
|
2059
|
+
msg: `Publishing ${providerName} delegate unregistration to server...`,
|
|
2060
|
+
animationEmoji: '⚡'
|
|
2061
|
+
});
|
|
2062
|
+
syncRes = await getApiBridge().putEvolveKeystone({
|
|
2063
|
+
domainAddr: primaryTjpAddr,
|
|
2064
|
+
keystoneIbGib: evolvedParent,
|
|
2065
|
+
relatedKeystoneIbGibs: []
|
|
2066
|
+
});
|
|
2067
|
+
if (!syncRes.success) {
|
|
2068
|
+
throw new Error(`Server synchronization rejected: ${syncRes.message}`);
|
|
2069
|
+
}
|
|
1364
2070
|
}
|
|
1365
2071
|
}
|
|
1366
2072
|
}
|
|
1367
2073
|
|
|
1368
|
-
// 3. Sync evolved parent keystone back to server
|
|
1369
|
-
this.setSSOStatus("Publishing evolved identity keystone to server...", "info");
|
|
1370
|
-
const syncRes = await getApiBridge().putEvolveKeystone({
|
|
1371
|
-
domainAddr: getIbGibAddr({ ibGib: this.ibGib! }),
|
|
1372
|
-
keystoneIbGib: evolvedParent,
|
|
1373
|
-
relatedKeystoneIbGibs: [] // No outward delegate timelines
|
|
1374
|
-
});
|
|
1375
|
-
if (!syncRes.success) {
|
|
1376
|
-
throw new Error(`Server synchronization rejected: ${syncRes.message}`);
|
|
1377
|
-
}
|
|
1378
|
-
|
|
1379
2074
|
// 4. Update index locally
|
|
1380
2075
|
await updateSpecialIndex({
|
|
1381
2076
|
type: "keystones",
|
|
@@ -1389,15 +2084,21 @@ export class KeystoneDetailsComponentInstance
|
|
|
1389
2084
|
space
|
|
1390
2085
|
});
|
|
1391
2086
|
|
|
1392
|
-
this.setSSOStatus(`Successfully unlinked ${
|
|
1393
|
-
|
|
2087
|
+
this.setSSOStatus(`Successfully unlinked ${providerName}!`, "success");
|
|
2088
|
+
this.ibGibAddr = getIbGibAddr({ ibGib: evolvedParent });
|
|
2089
|
+
this.staticIbGib = evolvedParent;
|
|
2090
|
+
await this.loadIbGib({ getLatest: false });
|
|
1394
2091
|
|
|
1395
2092
|
} catch (error) {
|
|
1396
|
-
console.error(`${lc}
|
|
1397
|
-
this.setSSOStatus(`
|
|
2093
|
+
console.error(`${lc} ${extractErrorMsg(error)}`, error);
|
|
2094
|
+
this.setSSOStatus(`Failed to unlink ${providerName}: ${extractErrorMsg(error)}`, "error");
|
|
2095
|
+
} finally {
|
|
2096
|
+
this.showBusy({ isBusy: false });
|
|
1398
2097
|
}
|
|
1399
2098
|
}
|
|
1400
2099
|
|
|
2100
|
+
|
|
2101
|
+
|
|
1401
2102
|
private setChangePasswordStatus(msg: string, type: 'info' | 'success' | 'error') {
|
|
1402
2103
|
const el = this.elements!.changePasswordStatusEl;
|
|
1403
2104
|
el.textContent = msg;
|
|
@@ -1445,8 +2146,17 @@ export class KeystoneDetailsComponentInstance
|
|
|
1445
2146
|
challenges: newChallenges,
|
|
1446
2147
|
};
|
|
1447
2148
|
|
|
1448
|
-
const primaryTjpAddr =
|
|
1449
|
-
|
|
2149
|
+
const primaryTjpAddr = (this.ibGib ? getTjpAddr({ ibGib: this.ibGib }) : undefined)
|
|
2150
|
+
|| (this.ibGibAddr ? (() => {
|
|
2151
|
+
const { ib } = getIbAndGib({ ibGibAddr: this.ibGibAddr });
|
|
2152
|
+
const { tjpGib } = getGibInfo({ ibGibAddr: this.ibGibAddr });
|
|
2153
|
+
return tjpGib && ib ? `${ib}^${tjpGib}` : this.ibGibAddr;
|
|
2154
|
+
})() : undefined)
|
|
2155
|
+
|| getIbGibAddr({ ibGib: this.ibGib! });
|
|
2156
|
+
|
|
2157
|
+
const custodianProvider = this.aggregatedDetails?.custodianProvider
|
|
2158
|
+
|| this.ibGib?.data?.frameDetails?.custodianProvider
|
|
2159
|
+
|| (this.ibGib?.data as any)?.custodianProvider;
|
|
1450
2160
|
|
|
1451
2161
|
let requestBody: any = {
|
|
1452
2162
|
primaryTjpAddr,
|
|
@@ -1476,17 +2186,69 @@ export class KeystoneDetailsComponentInstance
|
|
|
1476
2186
|
requestBody.code = resPopup.code;
|
|
1477
2187
|
requestBody.redirectUri = resPopup.redirectUri;
|
|
1478
2188
|
} else {
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
this.setChangePasswordStatus("Verification code required.", "error");
|
|
2189
|
+
// Email OTP flow
|
|
2190
|
+
const email = this.aggregatedDetails?.emails?.[0]
|
|
2191
|
+
|| this.ibGib?.data?.emails?.[0]
|
|
2192
|
+
|| this.ibGib?.data?.frameDetails?.emails?.[0];
|
|
2193
|
+
|
|
2194
|
+
if (!email) {
|
|
2195
|
+
this.showBusy({ isBusy: false });
|
|
2196
|
+
this.setChangePasswordStatus("No email address found for this identity to send verification code.", "error");
|
|
1488
2197
|
return;
|
|
1489
2198
|
}
|
|
2199
|
+
|
|
2200
|
+
const otpSection = this.elements?.sectionAddPasswordOtpEl;
|
|
2201
|
+
const otpInput = this.elements?.inputAddPasswordOtpEl;
|
|
2202
|
+
const isOtpVisible = otpSection && !otpSection.classList.contains('hidden');
|
|
2203
|
+
|
|
2204
|
+
// Step 1: If OTP section is not visible yet, send code and show the OTP input inline in the card
|
|
2205
|
+
if (!isOtpVisible) {
|
|
2206
|
+
this.showBusy({
|
|
2207
|
+
isBusy: true,
|
|
2208
|
+
title: 'Sending Verification Code...',
|
|
2209
|
+
msg: `Sending 6-digit verification code to ${email}...`,
|
|
2210
|
+
animationEmoji: '📧'
|
|
2211
|
+
});
|
|
2212
|
+
this.setChangePasswordStatus(`Sending verification code to ${email}...`, "info");
|
|
2213
|
+
|
|
2214
|
+
const resCode = await getApiBridge().postVerificationCode({
|
|
2215
|
+
email,
|
|
2216
|
+
primaryTjpAddr,
|
|
2217
|
+
});
|
|
2218
|
+
|
|
2219
|
+
this.showBusy({ isBusy: false });
|
|
2220
|
+
|
|
2221
|
+
if (!resCode.success) {
|
|
2222
|
+
this.setChangePasswordStatus(resCode.message || "Failed to send verification code.", "error");
|
|
2223
|
+
return;
|
|
2224
|
+
}
|
|
2225
|
+
|
|
2226
|
+
if (otpSection) {
|
|
2227
|
+
otpSection.classList.remove('hidden');
|
|
2228
|
+
}
|
|
2229
|
+
if (this.elements?.descAddPasswordOtpEl) {
|
|
2230
|
+
this.elements.descAddPasswordOtpEl.textContent = `A 6-digit verification code was sent to ${email}. Enter it below to establish sovereign ownership:`;
|
|
2231
|
+
}
|
|
2232
|
+
if (this.elements?.btnChangePasswordEl) {
|
|
2233
|
+
this.elements.btnChangePasswordEl.textContent = "Verify Code & Add Master Password";
|
|
2234
|
+
}
|
|
2235
|
+
this.setChangePasswordStatus(`✉️ 6-digit verification code sent to ${email}. Enter the code and click "Verify Code & Add Master Password".`, "info");
|
|
2236
|
+
this.startResendCooldownTimer(30);
|
|
2237
|
+
if (otpInput) {
|
|
2238
|
+
otpInput.value = '';
|
|
2239
|
+
otpInput.focus();
|
|
2240
|
+
}
|
|
2241
|
+
return;
|
|
2242
|
+
}
|
|
2243
|
+
|
|
2244
|
+
// Step 2: OTP section is visible - validate entered code
|
|
2245
|
+
const otpCode = (otpInput?.value || '').trim();
|
|
2246
|
+
if (!otpCode || otpCode.length < 6) {
|
|
2247
|
+
this.setChangePasswordStatus("Please enter the complete 6-digit verification code sent to your email.", "error");
|
|
2248
|
+
if (otpInput) { otpInput.focus(); }
|
|
2249
|
+
return;
|
|
2250
|
+
}
|
|
2251
|
+
|
|
1490
2252
|
requestBody.email = email;
|
|
1491
2253
|
requestBody.code = otpCode;
|
|
1492
2254
|
}
|
|
@@ -1517,21 +2279,97 @@ export class KeystoneDetailsComponentInstance
|
|
|
1517
2279
|
const space = await metaspace.getLocalUserSpace({ lock: false });
|
|
1518
2280
|
if (!space) { throw new Error("Local user space not available"); }
|
|
1519
2281
|
|
|
1520
|
-
|
|
1521
|
-
|
|
2282
|
+
if (resJson.primaryGraph) {
|
|
2283
|
+
const graphIbGibs = Object.values(resJson.primaryGraph) as any[];
|
|
2284
|
+
await metaspace.put({ ibGibs: graphIbGibs, space });
|
|
2285
|
+
const timelinesMap = getTimelinesGroupedByTjp({ ibGibs: graphIbGibs });
|
|
2286
|
+
for (const tjpKey of Object.keys(timelinesMap)) {
|
|
2287
|
+
for (const frame of timelinesMap[tjpKey]) {
|
|
2288
|
+
await metaspace.registerNewIbGib({ ibGib: frame, space });
|
|
2289
|
+
}
|
|
2290
|
+
}
|
|
2291
|
+
} else {
|
|
2292
|
+
await metaspace.put({ ibGibs: [evolvedParent], space });
|
|
2293
|
+
await metaspace.registerNewIbGib({ ibGib: evolvedParent, space });
|
|
2294
|
+
}
|
|
2295
|
+
|
|
2296
|
+
// 1. Create a password-based sync delegate for this device
|
|
2297
|
+
this.showBusy({
|
|
2298
|
+
isBusy: true,
|
|
2299
|
+
title: 'Creating Sovereign Sync Delegate...',
|
|
2300
|
+
msg: 'Deriving local device sync delegate keys from Master Password...',
|
|
2301
|
+
animationEmoji: '🔑'
|
|
2302
|
+
});
|
|
2303
|
+
|
|
2304
|
+
const rawUsername = this.aggregatedDetails?.username
|
|
2305
|
+
|| this.ibGib?.data?.username
|
|
2306
|
+
|| this.aggregatedDetails?.emails?.[0]?.split('@')[0]
|
|
2307
|
+
|| this.ibGib?.data?.emails?.[0]?.split('@')[0]
|
|
2308
|
+
|| 'user';
|
|
2309
|
+
|
|
2310
|
+
const { delegateKeystone: userSyncDelegate, deviceTag } = await createSovereignDeviceSyncDelegate({
|
|
2311
|
+
username: rawUsername,
|
|
2312
|
+
primaryTjpAddr,
|
|
2313
|
+
masterSecret: newPassword,
|
|
2314
|
+
metaspace,
|
|
2315
|
+
space,
|
|
2316
|
+
});
|
|
2317
|
+
|
|
2318
|
+
// 2. Register the new user sync delegate on the evolved parent keystone
|
|
2319
|
+
this.showBusy({
|
|
2320
|
+
isBusy: true,
|
|
2321
|
+
title: 'Registering Sovereign Delegate...',
|
|
2322
|
+
msg: 'Registering user sync delegate on primary keystone...',
|
|
2323
|
+
animationEmoji: '📝'
|
|
2324
|
+
});
|
|
2325
|
+
|
|
2326
|
+
const keystoneService = new KeystoneService_V1();
|
|
2327
|
+
const parentWithUserDelegate = await keystoneService.registerDelegate({
|
|
2328
|
+
parentKeystone: evolvedParent,
|
|
2329
|
+
delegateKeystone: userSyncDelegate,
|
|
2330
|
+
masterSecret: newPassword,
|
|
2331
|
+
signingPoolId: 'manage',
|
|
2332
|
+
metaspace,
|
|
2333
|
+
space,
|
|
2334
|
+
allowedVerbs: [KEYSTONE_VERB_SYNC, KEYSTONE_VERB_CONNECT],
|
|
2335
|
+
deviceName: deviceTag
|
|
2336
|
+
});
|
|
2337
|
+
|
|
2338
|
+
// 3. Post evolved keystone with new delegate to server
|
|
2339
|
+
this.showBusy({
|
|
2340
|
+
isBusy: true,
|
|
2341
|
+
title: 'Syncing to Server...',
|
|
2342
|
+
msg: 'Publishing updated domain identity and sovereign delegate to server...',
|
|
2343
|
+
animationEmoji: '⚡'
|
|
2344
|
+
});
|
|
2345
|
+
|
|
2346
|
+
const resSync = await getApiBridge().putEvolveKeystone({
|
|
2347
|
+
domainAddr: primaryTjpAddr,
|
|
2348
|
+
keystoneIbGib: parentWithUserDelegate,
|
|
2349
|
+
relatedKeystoneIbGibs: [userSyncDelegate]
|
|
2350
|
+
});
|
|
2351
|
+
if (!resSync.success) {
|
|
2352
|
+
console.warn(`${lc} Warning syncing user delegate to server: ${resSync.message}`);
|
|
2353
|
+
}
|
|
2354
|
+
|
|
2355
|
+
// 4. Save parentWithUserDelegate and userSyncDelegate to local space & metaspace
|
|
2356
|
+
await metaspace.put({ ibGibs: [parentWithUserDelegate, userSyncDelegate], space });
|
|
2357
|
+
await metaspace.registerNewIbGib({ ibGib: parentWithUserDelegate, space });
|
|
2358
|
+
await metaspace.registerNewIbGib({ ibGib: userSyncDelegate, space });
|
|
2359
|
+
|
|
1522
2360
|
await updateSpecialIndex({
|
|
1523
2361
|
type: "keystones",
|
|
1524
2362
|
rel8nInfos: [
|
|
1525
2363
|
{
|
|
1526
2364
|
rel8nName: "keystone",
|
|
1527
|
-
ibGibs: [toDto({ ibGib:
|
|
2365
|
+
ibGibs: [toDto({ ibGib: parentWithUserDelegate }), toDto({ ibGib: userSyncDelegate })]
|
|
1528
2366
|
}
|
|
1529
2367
|
],
|
|
1530
2368
|
metaspace,
|
|
1531
2369
|
space
|
|
1532
2370
|
});
|
|
1533
2371
|
|
|
1534
|
-
const evolvedAddr = getIbGibAddr({ ibGib:
|
|
2372
|
+
const evolvedAddr = getIbGibAddr({ ibGib: parentWithUserDelegate });
|
|
1535
2373
|
|
|
1536
2374
|
// Dispatch EVENT_IBGIB_IDENTITY_CHANGED
|
|
1537
2375
|
window.dispatchEvent(new CustomEvent(EVENT_IBGIB_IDENTITY_CHANGED, {
|
|
@@ -1542,9 +2380,20 @@ export class KeystoneDetailsComponentInstance
|
|
|
1542
2380
|
|
|
1543
2381
|
this.setChangePasswordStatus("Successfully added Master Password! Identity is now sovereign.", "success");
|
|
1544
2382
|
|
|
2383
|
+
// Reset OTP section and button text
|
|
2384
|
+
if (this.elements?.sectionAddPasswordOtpEl) {
|
|
2385
|
+
this.elements.sectionAddPasswordOtpEl.classList.add('hidden');
|
|
2386
|
+
if (this.elements.inputAddPasswordOtpEl) {
|
|
2387
|
+
this.elements.inputAddPasswordOtpEl.value = '';
|
|
2388
|
+
}
|
|
2389
|
+
}
|
|
2390
|
+
if (this.elements?.btnChangePasswordEl) {
|
|
2391
|
+
this.elements.btnChangePasswordEl.textContent = "Update Password";
|
|
2392
|
+
}
|
|
2393
|
+
|
|
1545
2394
|
// Refresh the view
|
|
1546
2395
|
this.ibGibAddr = evolvedAddr;
|
|
1547
|
-
this.staticIbGib =
|
|
2396
|
+
this.staticIbGib = parentWithUserDelegate;
|
|
1548
2397
|
await this.loadIbGib({ getLatest: true });
|
|
1549
2398
|
|
|
1550
2399
|
// Clear inputs
|
|
@@ -1554,22 +2403,83 @@ export class KeystoneDetailsComponentInstance
|
|
|
1554
2403
|
} catch (error) {
|
|
1555
2404
|
console.error(`${lc} Elevation failed: ${extractErrorMsg(error)}`);
|
|
1556
2405
|
this.setChangePasswordStatus(`Failed to add master password: ${extractErrorMsg(error)}`, "error");
|
|
2406
|
+
if (this.elements?.inputAddPasswordOtpEl && !this.elements.sectionAddPasswordOtpEl?.classList.contains('hidden')) {
|
|
2407
|
+
this.elements.inputAddPasswordOtpEl.select();
|
|
2408
|
+
this.elements.inputAddPasswordOtpEl.focus();
|
|
2409
|
+
}
|
|
1557
2410
|
} finally {
|
|
1558
2411
|
this.showBusy({ isBusy: false });
|
|
1559
2412
|
}
|
|
1560
2413
|
}
|
|
1561
2414
|
|
|
2415
|
+
protected async handleResendAddPasswordOtp(): Promise<void> {
|
|
2416
|
+
const lc = `${this.lc}[${this.handleResendAddPasswordOtp.name}]`;
|
|
2417
|
+
const email = this.aggregatedDetails?.emails?.[0]
|
|
2418
|
+
|| this.ibGib?.data?.emails?.[0]
|
|
2419
|
+
|| this.ibGib?.data?.frameDetails?.emails?.[0];
|
|
2420
|
+
if (!email) {
|
|
2421
|
+
this.setChangePasswordStatus("No email address found for this identity to send verification code.", "error");
|
|
2422
|
+
return;
|
|
2423
|
+
}
|
|
2424
|
+
const primaryTjpAddr = (this.ibGib ? getTjpAddr({ ibGib: this.ibGib }) : undefined)
|
|
2425
|
+
|| (this.ibGibAddr ? (() => {
|
|
2426
|
+
const { ib } = getIbAndGib({ ibGibAddr: this.ibGibAddr });
|
|
2427
|
+
const { tjpGib } = getGibInfo({ ibGibAddr: this.ibGibAddr });
|
|
2428
|
+
return tjpGib && ib ? `${ib}^${tjpGib}` : this.ibGibAddr;
|
|
2429
|
+
})() : undefined)
|
|
2430
|
+
|| getIbGibAddr({ ibGib: this.ibGib! });
|
|
2431
|
+
|
|
2432
|
+
const btn = this.elements!.btnResendAddPasswordOtpEl;
|
|
2433
|
+
if (btn) {
|
|
2434
|
+
btn.disabled = true;
|
|
2435
|
+
btn.textContent = "Sending...";
|
|
2436
|
+
}
|
|
2437
|
+
this.setChangePasswordStatus(`Resending verification code to ${email}...`, "info");
|
|
2438
|
+
try {
|
|
2439
|
+
const resCode = await getApiBridge().postVerificationCode({ email, primaryTjpAddr });
|
|
2440
|
+
if (resCode.success) {
|
|
2441
|
+
this.setChangePasswordStatus(`✉️ Fresh 6-digit verification code sent to ${email}!`, "success");
|
|
2442
|
+
this.startResendCooldownTimer(30);
|
|
2443
|
+
if (this.elements?.inputAddPasswordOtpEl) {
|
|
2444
|
+
this.elements.inputAddPasswordOtpEl.value = '';
|
|
2445
|
+
this.elements.inputAddPasswordOtpEl.focus();
|
|
2446
|
+
}
|
|
2447
|
+
} else {
|
|
2448
|
+
this.setChangePasswordStatus(resCode.message || "Failed to resend verification code.", "error");
|
|
2449
|
+
if (btn) {
|
|
2450
|
+
btn.disabled = false;
|
|
2451
|
+
btn.textContent = "Resend Code";
|
|
2452
|
+
}
|
|
2453
|
+
}
|
|
2454
|
+
} catch (err) {
|
|
2455
|
+
this.setChangePasswordStatus(`Error resending verification code: ${extractErrorMsg(err)}`, "error");
|
|
2456
|
+
if (btn) {
|
|
2457
|
+
btn.disabled = false;
|
|
2458
|
+
btn.textContent = "Resend Code";
|
|
2459
|
+
}
|
|
2460
|
+
}
|
|
2461
|
+
}
|
|
2462
|
+
|
|
1562
2463
|
protected async handleChangePassword(): Promise<void> {
|
|
1563
2464
|
const lc = `${this.lc}[${this.handleChangePassword.name}]`;
|
|
1564
2465
|
|
|
1565
|
-
const caps =
|
|
1566
|
-
|
|
2466
|
+
const caps = !!this.ibGib ?
|
|
2467
|
+
getKeystoneCapabilities({ keystone: this.ibGib, aggregatedDetails: this.aggregatedDetails }) :
|
|
2468
|
+
undefined;
|
|
2469
|
+
|
|
2470
|
+
const isCustodialOnly = !!caps ? caps.isCustodial && !caps.isSovereign : false;
|
|
1567
2471
|
if (isCustodialOnly) {
|
|
1568
2472
|
await this.handleAddMasterPassword();
|
|
1569
2473
|
return;
|
|
1570
2474
|
}
|
|
1571
2475
|
|
|
1572
2476
|
this.setChangePasswordStatus("Validating entries...", "info");
|
|
2477
|
+
this.showBusy({
|
|
2478
|
+
isBusy: true,
|
|
2479
|
+
title: 'Changing Master Password...',
|
|
2480
|
+
msg: 'Validating current password...',
|
|
2481
|
+
animationEmoji: '🔐'
|
|
2482
|
+
});
|
|
1573
2483
|
|
|
1574
2484
|
try {
|
|
1575
2485
|
const currentPassword = this.elements!.inputCurrentPasswordEl.value;
|
|
@@ -1602,6 +2512,12 @@ export class KeystoneDetailsComponentInstance
|
|
|
1602
2512
|
return;
|
|
1603
2513
|
}
|
|
1604
2514
|
|
|
2515
|
+
this.showBusy({
|
|
2516
|
+
isBusy: true,
|
|
2517
|
+
title: 'Evolving Master Password...',
|
|
2518
|
+
msg: 'Generating new challenge proofs and rotating local user delegates...',
|
|
2519
|
+
animationEmoji: '⚙️'
|
|
2520
|
+
});
|
|
1605
2521
|
this.setChangePasswordStatus("Evolving primary identity keystone...", "info");
|
|
1606
2522
|
|
|
1607
2523
|
const metaspace = await getGlobalMetaspace_waitIfNeeded();
|
|
@@ -1609,8 +2525,7 @@ export class KeystoneDetailsComponentInstance
|
|
|
1609
2525
|
const space = await metaspace.getLocalUserSpace({ lock: false });
|
|
1610
2526
|
if (!space) { throw new Error(`(UNEXPECTED) couldn't get default local space? (E: 2fc1281c28351fd378a1d7f88eaa0526)`); }
|
|
1611
2527
|
|
|
1612
|
-
// 2. Rotate parent identity
|
|
1613
|
-
debugger; // walk through this...looking for delegate failure // hmm...I thought we got change password going?
|
|
2528
|
+
// 2. Rotate parent identity and sovereign sync delegates
|
|
1614
2529
|
const { newKeystone: evolvedParent, newDelegates: evolvedDelegates } =
|
|
1615
2530
|
await keystoneService.changePassword({
|
|
1616
2531
|
keystoneIbGib: this.ibGib!,
|
|
@@ -1620,62 +2535,22 @@ export class KeystoneDetailsComponentInstance
|
|
|
1620
2535
|
metaspace,
|
|
1621
2536
|
space
|
|
1622
2537
|
});
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
this.
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
|
|
1631
|
-
// for (const delegateInfo of Object.values(delegates)) {
|
|
1632
|
-
// const delegateAddr = delegateInfo.delegateAddr;
|
|
1633
|
-
// const delegateKeystone = await keystoneService.getLatestKeystone({
|
|
1634
|
-
// addr: delegateAddr,
|
|
1635
|
-
// metaspace,
|
|
1636
|
-
// space,
|
|
1637
|
-
// });
|
|
1638
|
-
|
|
1639
|
-
// // Identify user-owned challenge pools in the delegate that match the old password
|
|
1640
|
-
// const poolsToRotate: string[] = [];
|
|
1641
|
-
// for (const pool of delegateKeystone.data?.challengePools ?? []) {
|
|
1642
|
-
// if (!pool.isForeign) {
|
|
1643
|
-
// /**
|
|
1644
|
-
// * WARNING: There is an extreme edge case that if two pools
|
|
1645
|
-
// * happen to be owned by different parties but share the
|
|
1646
|
-
// * same secret, this will change the wrong pool's secret
|
|
1647
|
-
// * also. For V1, this shouldn't matter. But we need a
|
|
1648
|
-
// * better way of tracking pool ownership for V2 (or even
|
|
1649
|
-
// * V1 if we have multi-party support). (DO NOT REMOVE
|
|
1650
|
-
// * THIS COMMENT UNTIL THIS IS ADDRESSED)
|
|
1651
|
-
// */
|
|
1652
|
-
// const isPoolMatch = await keystoneService.verifySigningSecret({
|
|
1653
|
-
// keystoneIbGib: delegateKeystone,
|
|
1654
|
-
// signingSecret: currentPassword,
|
|
1655
|
-
// poolId: pool.id
|
|
1656
|
-
// });
|
|
1657
|
-
// if (isPoolMatch) { poolsToRotate.push(pool.id); }
|
|
1658
|
-
// }
|
|
1659
|
-
// }
|
|
1660
|
-
|
|
1661
|
-
// if (poolsToRotate.length > 0) {
|
|
1662
|
-
// console.log(`${lc} Rotating pools in delegate ${delegateInfo.delegateTjpAddr}: ${poolsToRotate.join(', ')}`);
|
|
1663
|
-
// const {newKeystone: evolvedDelegate} = await keystoneService.changePassword({
|
|
1664
|
-
// keystoneIbGib: delegateKeystone,
|
|
1665
|
-
// newMasterSecret: newPassword,
|
|
1666
|
-
// signingSecret: currentPassword,
|
|
1667
|
-
// signingPoolId: 'manage',
|
|
1668
|
-
// poolIds: poolsToRotate,
|
|
1669
|
-
// metaspace,
|
|
1670
|
-
// space
|
|
1671
|
-
// });
|
|
1672
|
-
// evolvedDelegates.push(evolvedDelegate);
|
|
1673
|
-
// }
|
|
1674
|
-
// }
|
|
1675
|
-
|
|
1676
|
-
// 4. Sync evolved parent and all evolved delegate keystones atomically
|
|
2538
|
+
|
|
2539
|
+
// 3. Sync evolved parent and all evolved delegate keystones atomically
|
|
2540
|
+
this.showBusy({
|
|
2541
|
+
isBusy: true,
|
|
2542
|
+
title: 'Syncing to Server...',
|
|
2543
|
+
msg: 'Publishing rotated identity and delegates to server...',
|
|
2544
|
+
animationEmoji: '⚡'
|
|
2545
|
+
});
|
|
1677
2546
|
this.setChangePasswordStatus("Syncing evolved keystones to server...", "info");
|
|
1678
|
-
const parentTjpAddr =
|
|
2547
|
+
const parentTjpAddr = (this.ibGib ? getTjpAddr({ ibGib: this.ibGib }) : undefined)
|
|
2548
|
+
|| (this.ibGibAddr ? (() => {
|
|
2549
|
+
const { ib } = getIbAndGib({ ibGibAddr: this.ibGibAddr });
|
|
2550
|
+
const { tjpGib } = getGibInfo({ ibGibAddr: this.ibGibAddr });
|
|
2551
|
+
return tjpGib && ib ? `${ib}^${tjpGib}` : this.ibGibAddr;
|
|
2552
|
+
})() : undefined)
|
|
2553
|
+
|| getIbGibAddr({ ibGib: this.ibGib! });
|
|
1679
2554
|
|
|
1680
2555
|
const syncRes = await getApiBridge().putEvolveKeystone({
|
|
1681
2556
|
domainAddr: parentTjpAddr,
|
|
@@ -1686,7 +2561,7 @@ export class KeystoneDetailsComponentInstance
|
|
|
1686
2561
|
throw new Error(`Server synchronization rejected: ${syncRes.message}`);
|
|
1687
2562
|
}
|
|
1688
2563
|
|
|
1689
|
-
//
|
|
2564
|
+
// 4. Update index locally
|
|
1690
2565
|
this.setChangePasswordStatus("Updating local indexes...", "info");
|
|
1691
2566
|
const newKeystoneIbGibs = [toDto({ ibGib: evolvedParent })];
|
|
1692
2567
|
if (evolvedDelegates) {
|
|
@@ -1713,11 +2588,16 @@ export class KeystoneDetailsComponentInstance
|
|
|
1713
2588
|
this.elements!.inputConfirmPasswordEl.value = "";
|
|
1714
2589
|
|
|
1715
2590
|
this.setChangePasswordStatus("Password updated successfully!", "success");
|
|
2591
|
+
this.ibGibAddr = getIbGibAddr({ ibGib: evolvedParent });
|
|
2592
|
+
this.staticIbGib = evolvedParent;
|
|
1716
2593
|
await this.loadIbGib({ getLatest: true });
|
|
1717
2594
|
|
|
1718
2595
|
} catch (error) {
|
|
2596
|
+
debugger; // error in handleChangePassword
|
|
1719
2597
|
console.error(`${lc} Password rotation failed: ${extractErrorMsg(error)}`);
|
|
1720
2598
|
this.setChangePasswordStatus(`Update failed: ${extractErrorMsg(error)}`, "error");
|
|
2599
|
+
} finally {
|
|
2600
|
+
this.showBusy({ isBusy: false });
|
|
1721
2601
|
}
|
|
1722
2602
|
}
|
|
1723
2603
|
}
|