@ibgib/web-gib 0.0.60 → 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.d.mts.map +1 -1
- package/dist/helpers.mjs +1 -0
- package/dist/helpers.mjs.map +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 +91 -0
- package/dist/identity/device-delegate-helper.d.mts.map +1 -0
- package/dist/identity/device-delegate-helper.mjs +190 -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/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 +2 -1
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +2 -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 +31 -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 +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 +524 -104
- package/dist/ui/component/identity/keystone-creator/keystone-creator.mjs.map +1 -1
- package/dist/ui/component/identity/keystone-details/keystone-details.d.mts +19 -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 +1217 -132
- 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-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-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/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/helpers.web.mts +22 -2
- package/src/identity/custodian-delegate-helper.mts.bak +7 -0
- package/src/identity/device-delegate-helper.mts +268 -0
- package/src/identity/device-delegate.respec.mts +125 -0
- package/src/identity/keystone-sync-service.mts +95 -2
- package/src/identity/passkey-helper.mts +304 -0
- package/src/identity/passkey.respec.mts +194 -0
- package/src/index.mts +2 -2
- package/src/storage/storage-helpers.web.mts +47 -0
- package/src/ui/component/identity/add-device/add-device.mts +37 -48
- package/src/ui/component/identity/keystone-creator/keystone-creator.html +42 -3
- package/src/ui/component/identity/keystone-creator/keystone-creator.mts +568 -111
- package/src/ui/component/identity/keystone-details/keystone-details.html +38 -0
- package/src/ui/component/identity/keystone-details/keystone-details.mts +1290 -142
- package/src/ui/component/identity/remove-device/remove-device.mts +1 -0
- package/src/ui/ui-constants.mts +1 -0
- package/src/ui/ui-helpers.mts +118 -5
- 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
|
@@ -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,14 +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
|
-
|
|
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, deriveDelegateSecret, createSyncDelegateKeystone, isCustodialDelegate } 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, 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";
|
|
30
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";
|
|
31
39
|
|
|
32
40
|
const logalot = false;
|
|
33
41
|
|
|
@@ -88,6 +96,10 @@ interface KeystoneDetailsElements extends ElementsBase {
|
|
|
88
96
|
rawKeystoneJsonEl: HTMLPreElement;
|
|
89
97
|
delegatesCardEl: HTMLDivElement;
|
|
90
98
|
delegatesListEl: HTMLTableSectionElement;
|
|
99
|
+
passkeysCardEl: HTMLDivElement;
|
|
100
|
+
passkeysListEl: HTMLTableSectionElement;
|
|
101
|
+
btnAddPasskeyEl: HTMLButtonElement;
|
|
102
|
+
passkeyStatusMsgEl: HTMLDivElement;
|
|
91
103
|
historicalWarningBannerEl: HTMLDivElement;
|
|
92
104
|
containerEl: HTMLDivElement;
|
|
93
105
|
ssoLinkCardEl: HTMLDivElement;
|
|
@@ -100,6 +112,10 @@ interface KeystoneDetailsElements extends ElementsBase {
|
|
|
100
112
|
btnChangePasswordEl: HTMLButtonElement;
|
|
101
113
|
changePasswordStatusEl: HTMLDivElement;
|
|
102
114
|
changePasswordCardEl: HTMLDivElement;
|
|
115
|
+
sectionAddPasswordOtpEl: HTMLDivElement;
|
|
116
|
+
inputAddPasswordOtpEl: HTMLInputElement;
|
|
117
|
+
btnResendAddPasswordOtpEl: HTMLButtonElement;
|
|
118
|
+
descAddPasswordOtpEl: HTMLParagraphElement;
|
|
103
119
|
}
|
|
104
120
|
|
|
105
121
|
export class KeystoneDetailsComponentInstance
|
|
@@ -118,6 +134,8 @@ export class KeystoneDetailsComponentInstance
|
|
|
118
134
|
private isHistoricalFrame: boolean = false;
|
|
119
135
|
private _onIdentityChanged: ((e: any) => void) | null = null;
|
|
120
136
|
private keystoneCache: Map<string, { username: string, description: string, isPrimary: boolean, n: number }> = new Map();
|
|
137
|
+
private aggregatedDetails?: any;
|
|
138
|
+
private resendCooldownInterval: any = null;
|
|
121
139
|
|
|
122
140
|
constructor() {
|
|
123
141
|
super();
|
|
@@ -140,6 +158,39 @@ export class KeystoneDetailsComponentInstance
|
|
|
140
158
|
}
|
|
141
159
|
}
|
|
142
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
|
+
|
|
143
194
|
private async getKeystoneDetails(addr: string): Promise<{ username: string, description: string, isPrimary: boolean, n: number }> {
|
|
144
195
|
const cached = this.keystoneCache.get(addr);
|
|
145
196
|
if (cached) return cached;
|
|
@@ -206,6 +257,10 @@ export class KeystoneDetailsComponentInstance
|
|
|
206
257
|
this.elements.rawKeystoneJsonEl = shadow.getElementById('raw-keystone-json') as HTMLPreElement;
|
|
207
258
|
this.elements.delegatesCardEl = shadow.getElementById('delegates-card') as HTMLDivElement;
|
|
208
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;
|
|
209
264
|
this.elements.containerEl = shadow.getElementById('container') as HTMLDivElement;
|
|
210
265
|
this.elements.historicalWarningBannerEl = shadow.getElementById('historical-warning-banner') as HTMLDivElement;
|
|
211
266
|
this.elements.ssoLinkCardEl = shadow.getElementById('sso-link-card') as HTMLDivElement;
|
|
@@ -218,6 +273,10 @@ export class KeystoneDetailsComponentInstance
|
|
|
218
273
|
this.elements.btnChangePasswordEl = shadow.getElementById('btn-change-password') as HTMLButtonElement;
|
|
219
274
|
this.elements.changePasswordStatusEl = shadow.getElementById('change-password-status') as HTMLDivElement;
|
|
220
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;
|
|
221
280
|
|
|
222
281
|
// ElementBase requires contentEl
|
|
223
282
|
this.elements.contentEl = shadow.getElementById('identity-details-view') as HTMLDivElement;
|
|
@@ -243,12 +302,6 @@ export class KeystoneDetailsComponentInstance
|
|
|
243
302
|
}
|
|
244
303
|
}
|
|
245
304
|
|
|
246
|
-
override async disconnected(): Promise<void> {
|
|
247
|
-
if (this._onIdentityChanged) {
|
|
248
|
-
window.removeEventListener(EVENT_IBGIB_IDENTITY_CHANGED, this._onIdentityChanged);
|
|
249
|
-
}
|
|
250
|
-
}
|
|
251
|
-
|
|
252
305
|
private initHandlers() {
|
|
253
306
|
// Setup raw JSON accordion toggle
|
|
254
307
|
this.elements!.rawAccordionHeaderEl.addEventListener('click', () => this.toggleAccordion());
|
|
@@ -258,9 +311,11 @@ export class KeystoneDetailsComponentInstance
|
|
|
258
311
|
this.elements!.btnSetActiveEl.addEventListener('click', () => this.handleSetActiveIdentity());
|
|
259
312
|
this.elements!.btnCheckUpdatesEl.addEventListener('click', () => this.handleCheckForUpdates());
|
|
260
313
|
this.elements!.btnUpdateIdentityEl.addEventListener('click', () => this.handleUpdateIdentity());
|
|
314
|
+
this.elements!.btnAddPasskeyEl?.addEventListener('click', () => this.handleAddPasskey());
|
|
261
315
|
this.elements!.btnLinkGoogleEl.addEventListener('click', () => this.handleSSOClick('google'));
|
|
262
316
|
this.elements!.btnLinkGithubEl.addEventListener('click', () => this.handleSSOClick('github'));
|
|
263
317
|
this.elements!.btnChangePasswordEl.addEventListener('click', () => this.handleChangePassword());
|
|
318
|
+
this.elements!.btnResendAddPasswordOtpEl?.addEventListener('click', () => this.handleResendAddPasswordOtp());
|
|
264
319
|
}
|
|
265
320
|
|
|
266
321
|
private toggleAccordion() {
|
|
@@ -454,34 +509,99 @@ export class KeystoneDetailsComponentInstance
|
|
|
454
509
|
this.elements.btnLinkGoogleEl.classList.remove('disabled');
|
|
455
510
|
this.elements.btnLinkGithubEl.classList.remove('disabled');
|
|
456
511
|
|
|
457
|
-
// Check if
|
|
458
|
-
const
|
|
459
|
-
const
|
|
512
|
+
// Check if origin provider or linked
|
|
513
|
+
const originProvider = this.aggregatedDetails?.custodianProvider || this.ibGib?.data?.frameDetails?.custodianProvider;
|
|
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;
|
|
520
|
+
|
|
521
|
+
const caps = !!this.ibGib ?
|
|
522
|
+
getKeystoneCapabilities({ keystone: this.ibGib, aggregatedDetails: this.aggregatedDetails }) :
|
|
523
|
+
undefined;
|
|
460
524
|
|
|
461
525
|
// Google Card States
|
|
462
526
|
const googleCard = this.elements.btnLinkGoogleEl;
|
|
463
527
|
const googleLabel = googleCard.querySelector('.toggle-state-text')!;
|
|
464
528
|
if (isGoogleLinked) {
|
|
529
|
+
if (!caps) { throw new Error(`(UNEXPECTED) isGoogleLinked but capabilities falsy? (E: cc62e8357b66860dd2cb8148c833b726)`); }
|
|
465
530
|
googleCard.classList.remove('unlinked');
|
|
466
531
|
googleCard.classList.add('linked');
|
|
467
|
-
|
|
532
|
+
const isOrigin = originProvider === 'google';
|
|
533
|
+
if (isOrigin && !caps.isSovereign) {
|
|
534
|
+
googleLabel.textContent = "Google (Primary Account) 🔒";
|
|
535
|
+
googleCard.title = "Cannot unlink primary account without first adding a Master Password below.";
|
|
536
|
+
} else if (isOrigin) {
|
|
537
|
+
googleLabel.textContent = "Google (Primary Account) ✅";
|
|
538
|
+
googleCard.title = "Click to unlink Google provider (requires Master Password).";
|
|
539
|
+
} else {
|
|
540
|
+
googleLabel.textContent = "Google Linked ✅";
|
|
541
|
+
googleCard.title = "Click to unlink Google provider.";
|
|
542
|
+
}
|
|
468
543
|
} else {
|
|
469
544
|
googleCard.classList.remove('linked');
|
|
470
545
|
googleCard.classList.add('unlinked');
|
|
471
546
|
googleLabel.textContent = "Unlinked";
|
|
547
|
+
googleCard.title = "Click to link Google account.";
|
|
472
548
|
}
|
|
473
549
|
|
|
474
550
|
// GitHub Card States
|
|
475
551
|
const githubCard = this.elements.btnLinkGithubEl;
|
|
476
552
|
const githubLabel = githubCard.querySelector('.toggle-state-text')!;
|
|
477
553
|
if (isGithubLinked) {
|
|
554
|
+
if (!caps) { throw new Error(`(UNEXPECTED) isGithubLinked but capabilities falsy? (E: e94688210538e0e82878088873a46826)`); }
|
|
478
555
|
githubCard.classList.remove('unlinked');
|
|
479
556
|
githubCard.classList.add('linked');
|
|
480
|
-
|
|
557
|
+
const isOrigin = originProvider === 'github';
|
|
558
|
+
if (isOrigin && !caps.isSovereign) {
|
|
559
|
+
githubLabel.textContent = "GitHub (Primary Account) 🔒";
|
|
560
|
+
githubCard.title = "Cannot unlink primary account without first adding a Master Password below.";
|
|
561
|
+
} else if (isOrigin) {
|
|
562
|
+
githubLabel.textContent = "GitHub (Primary Account) ✅";
|
|
563
|
+
githubCard.title = "Click to unlink GitHub provider (requires Master Password).";
|
|
564
|
+
} else {
|
|
565
|
+
githubLabel.textContent = "GitHub Linked ✅";
|
|
566
|
+
githubCard.title = "Click to unlink GitHub provider.";
|
|
567
|
+
}
|
|
481
568
|
} else {
|
|
482
569
|
githubCard.classList.remove('linked');
|
|
483
570
|
githubCard.classList.add('unlinked');
|
|
484
571
|
githubLabel.textContent = "Unlinked";
|
|
572
|
+
githubCard.title = "Click to link GitHub account.";
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
private updatePasswordCardView(): void {
|
|
578
|
+
const caps = !!this.ibGib ?
|
|
579
|
+
getKeystoneCapabilities({ keystone: this.ibGib, aggregatedDetails: this.aggregatedDetails }) :
|
|
580
|
+
false;
|
|
581
|
+
const cardTitleEl = this.elements?.changePasswordCardEl?.querySelector('h3');
|
|
582
|
+
const cardDescEl = this.elements?.changePasswordCardEl?.querySelector('.description');
|
|
583
|
+
const currentPasswordGroup = this.elements?.inputCurrentPasswordEl?.closest('.form-group') as HTMLElement | null;
|
|
584
|
+
const btnChangePassword = this.elements?.btnChangePasswordEl;
|
|
585
|
+
|
|
586
|
+
const isCustodialOnly = !!caps ?
|
|
587
|
+
caps.isCustodial && !caps.isSovereign :
|
|
588
|
+
false;
|
|
589
|
+
if (isCustodialOnly) {
|
|
590
|
+
if (cardTitleEl) cardTitleEl.textContent = "Add Master Password";
|
|
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.";
|
|
592
|
+
if (currentPasswordGroup) currentPasswordGroup.style.display = 'none';
|
|
593
|
+
if (btnChangePassword) btnChangePassword.textContent = "Add Master Password";
|
|
594
|
+
} else {
|
|
595
|
+
if (cardTitleEl) cardTitleEl.textContent = "Change Password";
|
|
596
|
+
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.";
|
|
597
|
+
if (currentPasswordGroup) currentPasswordGroup.style.display = '';
|
|
598
|
+
if (btnChangePassword) btnChangePassword.textContent = "Update Password";
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
if (this.elements?.sectionAddPasswordOtpEl) {
|
|
602
|
+
this.elements.sectionAddPasswordOtpEl.classList.add('hidden');
|
|
603
|
+
if (this.elements.inputAddPasswordOtpEl) {
|
|
604
|
+
this.elements.inputAddPasswordOtpEl.value = '';
|
|
485
605
|
}
|
|
486
606
|
}
|
|
487
607
|
}
|
|
@@ -549,6 +669,7 @@ export class KeystoneDetailsComponentInstance
|
|
|
549
669
|
space
|
|
550
670
|
});
|
|
551
671
|
|
|
672
|
+
this.aggregatedDetails = aggregated;
|
|
552
673
|
this.elements.identityAggrDetailsEl.textContent = aggregated ? pretty(aggregated) : '{}';
|
|
553
674
|
|
|
554
675
|
// Update Username and Description fields
|
|
@@ -556,16 +677,19 @@ export class KeystoneDetailsComponentInstance
|
|
|
556
677
|
this.elements.identityDescriptionEl.textContent = aggregated?.description || aggregated?.profile?.description || '-';
|
|
557
678
|
} catch (err) {
|
|
558
679
|
console.warn(`${lc} Error aggregating details: ${extractErrorMsg(err)}`);
|
|
680
|
+
this.aggregatedDetails = undefined;
|
|
559
681
|
this.elements.identityAggrDetailsEl.textContent = '{}';
|
|
560
682
|
this.elements.identityNameEl.textContent = '-';
|
|
561
683
|
this.elements.identityDescriptionEl.textContent = '-';
|
|
562
684
|
}
|
|
563
685
|
} else {
|
|
686
|
+
this.aggregatedDetails = undefined;
|
|
564
687
|
this.elements.identityAggrDetailsEl.textContent = '{}';
|
|
565
688
|
this.elements.identityNameEl.textContent = '-';
|
|
566
689
|
this.elements.identityDescriptionEl.textContent = '-';
|
|
567
690
|
}
|
|
568
691
|
} else {
|
|
692
|
+
this.aggregatedDetails = undefined;
|
|
569
693
|
this.elements.identityAggrDetailsEl.textContent = '{}';
|
|
570
694
|
this.elements.identityNameEl.textContent = '-';
|
|
571
695
|
this.elements.identityDescriptionEl.textContent = '-';
|
|
@@ -591,15 +715,19 @@ export class KeystoneDetailsComponentInstance
|
|
|
591
715
|
// Render Challenge Pools
|
|
592
716
|
this.renderChallengePools(keystone.data?.challengePools || []);
|
|
593
717
|
|
|
594
|
-
// 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)
|
|
595
719
|
const details = await this.getKeystoneDetails(this.ibGibAddr);
|
|
596
720
|
if (details.isPrimary) {
|
|
597
721
|
this.elements.delegatesCardEl.classList.remove('hidden');
|
|
598
722
|
await this.renderDelegates(keystone.data?.delegates);
|
|
723
|
+
this.elements.passkeysCardEl.classList.remove('hidden');
|
|
724
|
+
await this.renderPasskeys(this.aggregatedDetails?.passkeys);
|
|
599
725
|
this.elements.ssoLinkCardEl.classList.remove('hidden');
|
|
600
726
|
this.elements.changePasswordCardEl.classList.remove('hidden');
|
|
727
|
+
this.updatePasswordCardView();
|
|
601
728
|
} else {
|
|
602
729
|
this.elements.delegatesCardEl.classList.add('hidden');
|
|
730
|
+
this.elements.passkeysCardEl.classList.add('hidden');
|
|
603
731
|
this.elements.ssoLinkCardEl.classList.add('hidden');
|
|
604
732
|
this.elements.changePasswordCardEl.classList.add('hidden');
|
|
605
733
|
}
|
|
@@ -615,6 +743,7 @@ export class KeystoneDetailsComponentInstance
|
|
|
615
743
|
|
|
616
744
|
private clearActiveDetails() {
|
|
617
745
|
if (!this.elements) { return; }
|
|
746
|
+
this.aggregatedDetails = undefined;
|
|
618
747
|
this.isHistoricalFrame = false;
|
|
619
748
|
if (this.elements.historicalWarningBannerEl) {
|
|
620
749
|
this.elements.historicalWarningBannerEl.classList.add('hidden');
|
|
@@ -633,6 +762,15 @@ export class KeystoneDetailsComponentInstance
|
|
|
633
762
|
if (this.elements.delegatesListEl) {
|
|
634
763
|
this.elements.delegatesListEl.innerHTML = '';
|
|
635
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
|
+
}
|
|
636
774
|
}
|
|
637
775
|
|
|
638
776
|
private renderChallengePools(pools: any[]) {
|
|
@@ -804,43 +942,71 @@ export class KeystoneDetailsComponentInstance
|
|
|
804
942
|
}
|
|
805
943
|
|
|
806
944
|
const metaspace = await getGlobalMetaspace_waitIfNeeded();
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
945
|
+
if (!metaspace) {
|
|
946
|
+
throw new Error(`(UNEXPECTED) metaspace falsy? (E: 8a9b0c1d2e3f4a5b6c7d8e9f0a1b2c3e)`);
|
|
947
|
+
}
|
|
948
|
+
const space = await metaspace.getLocalUserSpace({ lock: false });
|
|
949
|
+
if (!space) {
|
|
950
|
+
throw new Error(`(UNEXPECTED) space falsy? (E: 9b0c1d2e3f4a5b6c7d8e9f0a1b2c3d4f)`);
|
|
813
951
|
}
|
|
952
|
+
const localSpaceId = space.ib ? parseSpaceIb({ spaceIb: space.ib }).spaceId : undefined;
|
|
814
953
|
|
|
815
954
|
for (const [delegateTjpAddr, info] of Object.entries(delegates)) {
|
|
816
955
|
const tr = document.createElement('tr');
|
|
817
956
|
|
|
818
|
-
const deviceName = info.deviceName || info.details?.deviceTag || info.details?.name || 'Device Delegate';
|
|
957
|
+
const deviceName = info.deviceName || (info as any).details?.deviceTag || (info as any).details?.name || 'Device Delegate';
|
|
819
958
|
const tdName = document.createElement('td');
|
|
820
959
|
|
|
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';
|
|
960
|
+
const delegateSpaceId = info.spaceId || (info as any).details?.spaceId;
|
|
961
|
+
|
|
962
|
+
// Check if delegate is present in local space (via spaceId match or direct presence check)
|
|
963
|
+
let isThisDevice = false;
|
|
964
|
+
if (localSpaceId && delegateSpaceId === localSpaceId) {
|
|
965
|
+
isThisDevice = true;
|
|
966
|
+
} else {
|
|
967
|
+
try {
|
|
968
|
+
const resGet = await metaspace.get({ addrs: [delegateTjpAddr], space });
|
|
969
|
+
isThisDevice = !!(resGet.success && resGet.ibGibs && resGet.ibGibs.length > 0);
|
|
970
|
+
} catch {
|
|
971
|
+
isThisDevice = false;
|
|
841
972
|
}
|
|
842
973
|
}
|
|
843
974
|
|
|
975
|
+
const badgeEl = document.createElement('span');
|
|
976
|
+
badgeEl.style.marginLeft = '0.5rem';
|
|
977
|
+
badgeEl.style.padding = '0.15rem 0.4rem';
|
|
978
|
+
badgeEl.style.borderRadius = '4px';
|
|
979
|
+
badgeEl.style.fontSize = '0.75rem';
|
|
980
|
+
badgeEl.style.fontWeight = 'bold';
|
|
981
|
+
|
|
982
|
+
if (isThisDevice) {
|
|
983
|
+
badgeEl.textContent = 'This Device';
|
|
984
|
+
badgeEl.className = 'badge badge-this-device';
|
|
985
|
+
badgeEl.style.backgroundColor = '#1b5e20';
|
|
986
|
+
badgeEl.style.color = '#a5d6a7';
|
|
987
|
+
} else {
|
|
988
|
+
badgeEl.textContent = 'Remote Device';
|
|
989
|
+
badgeEl.className = 'badge badge-remote-device';
|
|
990
|
+
badgeEl.style.backgroundColor = '#0d47a1';
|
|
991
|
+
badgeEl.style.color = '#90caf9';
|
|
992
|
+
}
|
|
993
|
+
|
|
994
|
+
// Check expiration
|
|
995
|
+
const isExpired = isDelegateExpired({ delegateInfo: info });
|
|
996
|
+
let expiredBadgeEl: HTMLSpanElement | null = null;
|
|
997
|
+
if (isExpired) {
|
|
998
|
+
expiredBadgeEl = document.createElement('span');
|
|
999
|
+
expiredBadgeEl.textContent = 'Expired';
|
|
1000
|
+
expiredBadgeEl.className = 'badge badge-expired';
|
|
1001
|
+
expiredBadgeEl.style.marginLeft = '0.3rem';
|
|
1002
|
+
expiredBadgeEl.style.padding = '0.15rem 0.4rem';
|
|
1003
|
+
expiredBadgeEl.style.borderRadius = '4px';
|
|
1004
|
+
expiredBadgeEl.style.fontSize = '0.75rem';
|
|
1005
|
+
expiredBadgeEl.style.fontWeight = 'bold';
|
|
1006
|
+
expiredBadgeEl.style.backgroundColor = '#b71c1c';
|
|
1007
|
+
expiredBadgeEl.style.color = '#ef9a9a';
|
|
1008
|
+
}
|
|
1009
|
+
|
|
844
1010
|
const applyNameAndBadge = (displayName: string, hoverText?: string) => {
|
|
845
1011
|
tdName.innerHTML = '';
|
|
846
1012
|
const textSpan = document.createElement('span');
|
|
@@ -849,6 +1015,9 @@ export class KeystoneDetailsComponentInstance
|
|
|
849
1015
|
if (badgeEl) {
|
|
850
1016
|
tdName.appendChild(badgeEl);
|
|
851
1017
|
}
|
|
1018
|
+
if (expiredBadgeEl) {
|
|
1019
|
+
tdName.appendChild(expiredBadgeEl);
|
|
1020
|
+
}
|
|
852
1021
|
tdName.title = hoverText || displayName;
|
|
853
1022
|
};
|
|
854
1023
|
|
|
@@ -863,7 +1032,7 @@ export class KeystoneDetailsComponentInstance
|
|
|
863
1032
|
tdVerbs.textContent = info.allowedVerbs ? info.allowedVerbs.join(', ') : '-';
|
|
864
1033
|
tdVerbs.title = tdVerbs.textContent;
|
|
865
1034
|
|
|
866
|
-
const isRemote =
|
|
1035
|
+
const isRemote = !isThisDevice;
|
|
867
1036
|
|
|
868
1037
|
const tdActions = document.createElement('td');
|
|
869
1038
|
const btnView = document.createElement('button');
|
|
@@ -874,20 +1043,20 @@ export class KeystoneDetailsComponentInstance
|
|
|
874
1043
|
if (isRemote) {
|
|
875
1044
|
this.showRemoteDelegateModal(info);
|
|
876
1045
|
} else {
|
|
877
|
-
if (info.delegateAddr) {
|
|
1046
|
+
if (info.delegateAddr || delegateTjpAddr) {
|
|
878
1047
|
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
|
-
}
|
|
1048
|
+
try {
|
|
1049
|
+
const keystoneService = new KeystoneService_V1();
|
|
1050
|
+
const latestKeystone = await keystoneService.getLatestKeystone({
|
|
1051
|
+
addr: delegateTjpAddr,
|
|
1052
|
+
metaspace,
|
|
1053
|
+
space
|
|
1054
|
+
});
|
|
1055
|
+
if (latestKeystone) {
|
|
1056
|
+
addrToLoad = getIbGibAddr({ ibGib: latestKeystone });
|
|
890
1057
|
}
|
|
1058
|
+
} catch (err) {
|
|
1059
|
+
console.warn(`${lc} Error resolving latest delegate keystone: ${extractErrorMsg(err)}`);
|
|
891
1060
|
}
|
|
892
1061
|
this.dispatchEvent(new CustomEvent('ibgib-view-keystone-details', {
|
|
893
1062
|
detail: { addr: addrToLoad },
|
|
@@ -924,8 +1093,9 @@ export class KeystoneDetailsComponentInstance
|
|
|
924
1093
|
let masterSecret: string;
|
|
925
1094
|
try {
|
|
926
1095
|
masterSecret = await promptForSecret({
|
|
927
|
-
msg: `Enter Master Password to revoke delegate '${deviceName}':`,
|
|
928
|
-
confirm: false
|
|
1096
|
+
msg: `Enter Master Password or touch security key to revoke delegate '${deviceName}':`,
|
|
1097
|
+
confirm: false,
|
|
1098
|
+
passkeys: this.aggregatedDetails?.passkeys,
|
|
929
1099
|
});
|
|
930
1100
|
} catch (err) {
|
|
931
1101
|
// User cancelled prompt
|
|
@@ -942,11 +1112,35 @@ export class KeystoneDetailsComponentInstance
|
|
|
942
1112
|
|
|
943
1113
|
const keystoneSvc = new KeystoneService_V1();
|
|
944
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
|
+
|
|
945
1138
|
// Unregister delegate from parent keystone
|
|
946
1139
|
const evolvedParent = await keystoneSvc.unregisterDelegate({
|
|
947
1140
|
parentKeystone: this.ibGib!,
|
|
948
1141
|
delegateTjpAddr,
|
|
949
1142
|
masterSecret,
|
|
1143
|
+
signingPoolId: matchedPoolId,
|
|
950
1144
|
metaspace,
|
|
951
1145
|
space
|
|
952
1146
|
});
|
|
@@ -1000,6 +1194,477 @@ export class KeystoneDetailsComponentInstance
|
|
|
1000
1194
|
}
|
|
1001
1195
|
}
|
|
1002
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
|
+
|
|
1003
1668
|
override async handleContextUpdated(): Promise<void> {
|
|
1004
1669
|
const lc = `${this.lc}[${this.handleContextUpdated.name}]`;
|
|
1005
1670
|
try {
|
|
@@ -1039,10 +1704,26 @@ export class KeystoneDetailsComponentInstance
|
|
|
1039
1704
|
const lc = `${this.lc}[${this.handleSSOLink.name}]`;
|
|
1040
1705
|
this.setSSOStatus(`Linking ${providerId}...`, "info");
|
|
1041
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
|
+
|
|
1042
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
|
+
|
|
1043
1724
|
const resPopup = await executeSsoOAuthPopup({
|
|
1044
1725
|
providerId,
|
|
1045
|
-
parentTjpAddr:
|
|
1726
|
+
parentTjpAddr: primaryTjpAddr
|
|
1046
1727
|
});
|
|
1047
1728
|
|
|
1048
1729
|
if (!resPopup.success || !resPopup.code) {
|
|
@@ -1054,6 +1735,8 @@ export class KeystoneDetailsComponentInstance
|
|
|
1054
1735
|
} catch (error) {
|
|
1055
1736
|
console.error(`${lc} SSO link initiation failed: ${extractErrorMsg(error)}`);
|
|
1056
1737
|
this.setSSOStatus(`Link failed: ${extractErrorMsg(error)}`, "error");
|
|
1738
|
+
} finally {
|
|
1739
|
+
this.showBusy({ isBusy: false });
|
|
1057
1740
|
}
|
|
1058
1741
|
}
|
|
1059
1742
|
|
|
@@ -1066,7 +1749,14 @@ export class KeystoneDetailsComponentInstance
|
|
|
1066
1749
|
|
|
1067
1750
|
protected async handleSSOLinkCallback(code: string, stateObj: any, redirectUri: string) {
|
|
1068
1751
|
const lc = `${this.lc}[${this.handleSSOLinkCallback.name}]`;
|
|
1069
|
-
|
|
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
|
+
});
|
|
1070
1760
|
|
|
1071
1761
|
try {
|
|
1072
1762
|
// 1. Exchange OAuth code for custodian public pool config
|
|
@@ -1094,6 +1784,7 @@ export class KeystoneDetailsComponentInstance
|
|
|
1094
1784
|
const custodianPool = linkData.custodianPool;
|
|
1095
1785
|
|
|
1096
1786
|
// 2. Prompt user for master secret and verify it
|
|
1787
|
+
this.showBusy({ isBusy: false });
|
|
1097
1788
|
const keystoneService = new KeystoneService_V1();
|
|
1098
1789
|
let masterSecret: string | undefined;
|
|
1099
1790
|
let isCorrect = false;
|
|
@@ -1137,6 +1828,12 @@ export class KeystoneDetailsComponentInstance
|
|
|
1137
1828
|
return;
|
|
1138
1829
|
}
|
|
1139
1830
|
|
|
1831
|
+
this.showBusy({
|
|
1832
|
+
isBusy: true,
|
|
1833
|
+
title: 'Evolving Identity...',
|
|
1834
|
+
msg: `Installing ${providerName} custodian pool and signing evolution...`,
|
|
1835
|
+
animationEmoji: '📝'
|
|
1836
|
+
});
|
|
1140
1837
|
this.setSSOStatus("Evolving identity locally and generating cryptographic signatures...", "info");
|
|
1141
1838
|
|
|
1142
1839
|
const metaspace = await getGlobalMetaspace_waitIfNeeded();
|
|
@@ -1144,7 +1841,7 @@ export class KeystoneDetailsComponentInstance
|
|
|
1144
1841
|
const space = await metaspace.getLocalUserSpace({ lock: false });
|
|
1145
1842
|
if (!space) { throw new Error("Local space not found"); }
|
|
1146
1843
|
|
|
1147
|
-
// 3. Evolve parent keystone to include the custodian pool
|
|
1844
|
+
// 3. Evolve parent keystone to include the custodian pool (n -> n+1)
|
|
1148
1845
|
const evolvedParent = await keystoneService.addPools({
|
|
1149
1846
|
latestKeystone: this.ibGib!,
|
|
1150
1847
|
masterSecret: masterSecret!,
|
|
@@ -1153,24 +1850,64 @@ export class KeystoneDetailsComponentInstance
|
|
|
1153
1850
|
space
|
|
1154
1851
|
});
|
|
1155
1852
|
|
|
1156
|
-
//
|
|
1157
|
-
|
|
1158
|
-
const syncRes = await getApiBridge().putEvolveKeystone({
|
|
1853
|
+
// Sync evolved parent (n+1) to server first
|
|
1854
|
+
const syncRes1 = await getApiBridge().putEvolveKeystone({
|
|
1159
1855
|
domainAddr: stateObj.parentTjpAddr,
|
|
1160
1856
|
keystoneIbGib: evolvedParent,
|
|
1161
|
-
relatedKeystoneIbGibs: []
|
|
1857
|
+
relatedKeystoneIbGibs: []
|
|
1162
1858
|
});
|
|
1163
|
-
if (!
|
|
1164
|
-
throw new Error(`Server synchronization rejected: ${
|
|
1859
|
+
if (!syncRes1.success) {
|
|
1860
|
+
throw new Error(`Server synchronization of linked pool rejected: ${syncRes1.message}`);
|
|
1165
1861
|
}
|
|
1166
1862
|
|
|
1167
|
-
//
|
|
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
|
|
1168
1905
|
await updateSpecialIndex({
|
|
1169
1906
|
type: "keystones",
|
|
1170
1907
|
rel8nInfos: [
|
|
1171
1908
|
{
|
|
1172
1909
|
rel8nName: "keystone",
|
|
1173
|
-
ibGibs: [toDto({ ibGib:
|
|
1910
|
+
ibGibs: [toDto({ ibGib: parentWithDelegate }), toDto({ ibGib: delegateKeystone })]
|
|
1174
1911
|
}
|
|
1175
1912
|
],
|
|
1176
1913
|
metaspace,
|
|
@@ -1178,17 +1915,28 @@ export class KeystoneDetailsComponentInstance
|
|
|
1178
1915
|
});
|
|
1179
1916
|
|
|
1180
1917
|
this.setSSOStatus(`Successfully linked with ${stateObj.providerId}!`, "success");
|
|
1918
|
+
this.ibGibAddr = getIbGibAddr({ ibGib: parentWithDelegate });
|
|
1919
|
+
this.staticIbGib = parentWithDelegate;
|
|
1181
1920
|
await this.loadIbGib({ getLatest: false });
|
|
1182
1921
|
|
|
1183
1922
|
} catch (error) {
|
|
1184
1923
|
console.error(`${lc} SSO callback processing failed: ${extractErrorMsg(error)}`);
|
|
1185
1924
|
this.setSSOStatus(`Link failed: ${extractErrorMsg(error)}`, "error");
|
|
1925
|
+
} finally {
|
|
1926
|
+
this.showBusy({ isBusy: false });
|
|
1186
1927
|
}
|
|
1187
1928
|
}
|
|
1188
1929
|
|
|
1189
1930
|
protected async handleSSOClick(providerId: 'google' | 'github') {
|
|
1190
|
-
const isLinked =
|
|
1931
|
+
const isLinked = !!this.ibGib ?
|
|
1932
|
+
isSsoProviderLinked({ keystone: this.ibGib, aggregatedDetails: this.aggregatedDetails, providerId }) :
|
|
1933
|
+
false;
|
|
1191
1934
|
if (isLinked) {
|
|
1935
|
+
const caps = getKeystoneCapabilities({ keystone: this.ibGib!, aggregatedDetails: this.aggregatedDetails });
|
|
1936
|
+
if (!caps.isSovereign) {
|
|
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");
|
|
1938
|
+
return;
|
|
1939
|
+
}
|
|
1192
1940
|
await this.handleSSOUnlink(providerId);
|
|
1193
1941
|
} else {
|
|
1194
1942
|
await this.handleSSOLink(providerId);
|
|
@@ -1197,7 +1945,8 @@ export class KeystoneDetailsComponentInstance
|
|
|
1197
1945
|
|
|
1198
1946
|
protected async handleSSOUnlink(providerId: 'google' | 'github') {
|
|
1199
1947
|
const lc = `${this.lc}[${this.handleSSOUnlink.name}]`;
|
|
1200
|
-
|
|
1948
|
+
const providerName = providerId === 'google' ? 'Google' : providerId === 'github' ? 'GitHub' : providerId;
|
|
1949
|
+
this.setSSOStatus(`Unlinking ${providerName}...`, "info");
|
|
1201
1950
|
|
|
1202
1951
|
try {
|
|
1203
1952
|
// 1. Prompt user for password
|
|
@@ -1210,11 +1959,12 @@ export class KeystoneDetailsComponentInstance
|
|
|
1210
1959
|
while (tries < maxTries && !isCorrect) {
|
|
1211
1960
|
try {
|
|
1212
1961
|
const promptMsg = tries === 0
|
|
1213
|
-
? `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.`
|
|
1214
1963
|
: `Incorrect password. Please try again (${maxTries - tries} tries remaining):`;
|
|
1215
1964
|
masterSecret = await promptForSecret({
|
|
1216
1965
|
msg: promptMsg,
|
|
1217
|
-
confirm: false
|
|
1966
|
+
confirm: false,
|
|
1967
|
+
passkeys: this.aggregatedDetails?.passkeys,
|
|
1218
1968
|
});
|
|
1219
1969
|
} catch (error) {
|
|
1220
1970
|
masterSecret = undefined;
|
|
@@ -1225,11 +1975,10 @@ export class KeystoneDetailsComponentInstance
|
|
|
1225
1975
|
break;
|
|
1226
1976
|
}
|
|
1227
1977
|
|
|
1228
|
-
// Verify secret using verifySigningSecret
|
|
1978
|
+
// Verify secret using verifySigningSecret
|
|
1229
1979
|
isCorrect = await keystoneService.verifySigningSecret({
|
|
1230
1980
|
keystoneIbGib: this.ibGib!,
|
|
1231
|
-
signingSecret: masterSecret
|
|
1232
|
-
poolId: 'manage'
|
|
1981
|
+
signingSecret: masterSecret
|
|
1233
1982
|
});
|
|
1234
1983
|
|
|
1235
1984
|
if (!isCorrect) {
|
|
@@ -1242,6 +1991,12 @@ export class KeystoneDetailsComponentInstance
|
|
|
1242
1991
|
return;
|
|
1243
1992
|
}
|
|
1244
1993
|
|
|
1994
|
+
this.showBusy({
|
|
1995
|
+
isBusy: true,
|
|
1996
|
+
title: 'Unlinking Provider...',
|
|
1997
|
+
msg: `Removing ${providerName} custodian pool and delegate keys...`,
|
|
1998
|
+
animationEmoji: '🔓'
|
|
1999
|
+
});
|
|
1245
2000
|
this.setSSOStatus("Evolving identity locally and removing custodian pool...", "info");
|
|
1246
2001
|
|
|
1247
2002
|
const metaspace = await getGlobalMetaspace_waitIfNeeded();
|
|
@@ -1251,7 +2006,7 @@ export class KeystoneDetailsComponentInstance
|
|
|
1251
2006
|
|
|
1252
2007
|
// 2. Evolve parent keystone to remove the custodian pool
|
|
1253
2008
|
const targetPoolId = `custodian-manage-${providerId}`;
|
|
1254
|
-
|
|
2009
|
+
let evolvedParent = await keystoneService.removePools({
|
|
1255
2010
|
latestKeystone: this.ibGib!,
|
|
1256
2011
|
masterSecret: masterSecret!,
|
|
1257
2012
|
poolIds: [targetPoolId],
|
|
@@ -1259,10 +2014,24 @@ export class KeystoneDetailsComponentInstance
|
|
|
1259
2014
|
space
|
|
1260
2015
|
});
|
|
1261
2016
|
|
|
1262
|
-
|
|
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
|
+
});
|
|
1263
2032
|
this.setSSOStatus("Publishing evolved identity keystone to server...", "info");
|
|
1264
|
-
|
|
1265
|
-
domainAddr:
|
|
2033
|
+
let syncRes = await getApiBridge().putEvolveKeystone({
|
|
2034
|
+
domainAddr: primaryTjpAddr,
|
|
1266
2035
|
keystoneIbGib: evolvedParent,
|
|
1267
2036
|
relatedKeystoneIbGibs: [] // No outward delegate timelines
|
|
1268
2037
|
});
|
|
@@ -1270,6 +2039,38 @@ export class KeystoneDetailsComponentInstance
|
|
|
1270
2039
|
throw new Error(`Server synchronization rejected: ${syncRes.message}`);
|
|
1271
2040
|
}
|
|
1272
2041
|
|
|
2042
|
+
// 4. Unregister any corresponding outward delegate for this provider if present
|
|
2043
|
+
const delegatesMap = evolvedParent.data?.delegates || this.ibGib?.data?.delegates;
|
|
2044
|
+
if (delegatesMap) {
|
|
2045
|
+
for (const [delegateTjpAddr, info] of Object.entries(delegatesMap)) {
|
|
2046
|
+
if (isCustodialDelegate({ delegateInfo: info, providerId })) {
|
|
2047
|
+
evolvedParent = await keystoneService.unregisterDelegate({
|
|
2048
|
+
parentKeystone: evolvedParent,
|
|
2049
|
+
delegateTjpAddr,
|
|
2050
|
+
masterSecret: masterSecret!,
|
|
2051
|
+
signingPoolId: 'manage',
|
|
2052
|
+
metaspace,
|
|
2053
|
+
space,
|
|
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
|
+
}
|
|
2070
|
+
}
|
|
2071
|
+
}
|
|
2072
|
+
}
|
|
2073
|
+
|
|
1273
2074
|
// 4. Update index locally
|
|
1274
2075
|
await updateSpecialIndex({
|
|
1275
2076
|
type: "keystones",
|
|
@@ -1283,15 +2084,21 @@ export class KeystoneDetailsComponentInstance
|
|
|
1283
2084
|
space
|
|
1284
2085
|
});
|
|
1285
2086
|
|
|
1286
|
-
this.setSSOStatus(`Successfully unlinked ${
|
|
1287
|
-
|
|
2087
|
+
this.setSSOStatus(`Successfully unlinked ${providerName}!`, "success");
|
|
2088
|
+
this.ibGibAddr = getIbGibAddr({ ibGib: evolvedParent });
|
|
2089
|
+
this.staticIbGib = evolvedParent;
|
|
2090
|
+
await this.loadIbGib({ getLatest: false });
|
|
1288
2091
|
|
|
1289
2092
|
} catch (error) {
|
|
1290
|
-
console.error(`${lc}
|
|
1291
|
-
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 });
|
|
1292
2097
|
}
|
|
1293
2098
|
}
|
|
1294
2099
|
|
|
2100
|
+
|
|
2101
|
+
|
|
1295
2102
|
private setChangePasswordStatus(msg: string, type: 'info' | 'success' | 'error') {
|
|
1296
2103
|
const el = this.elements!.changePasswordStatusEl;
|
|
1297
2104
|
el.textContent = msg;
|
|
@@ -1299,9 +2106,380 @@ export class KeystoneDetailsComponentInstance
|
|
|
1299
2106
|
el.classList.remove('hidden');
|
|
1300
2107
|
}
|
|
1301
2108
|
|
|
2109
|
+
protected async handleAddMasterPassword(): Promise<void> {
|
|
2110
|
+
const lc = `${this.lc}[${this.handleAddMasterPassword.name}]`;
|
|
2111
|
+
this.setChangePasswordStatus("Validating entries...", "info");
|
|
2112
|
+
|
|
2113
|
+
try {
|
|
2114
|
+
const newPassword = this.elements!.inputNewPasswordEl.value;
|
|
2115
|
+
const confirmPassword = this.elements!.inputConfirmPasswordEl.value;
|
|
2116
|
+
|
|
2117
|
+
if (!newPassword) {
|
|
2118
|
+
this.setChangePasswordStatus("Master password is required.", "error");
|
|
2119
|
+
return;
|
|
2120
|
+
}
|
|
2121
|
+
if (newPassword !== confirmPassword) {
|
|
2122
|
+
this.setChangePasswordStatus("Master password and confirmation do not match.", "error");
|
|
2123
|
+
return;
|
|
2124
|
+
}
|
|
2125
|
+
|
|
2126
|
+
const initialManagePool = this.ibGib?.data?.challengePools?.find(p => p.id === POOL_ID_MANAGE);
|
|
2127
|
+
if (!initialManagePool) {
|
|
2128
|
+
this.setChangePasswordStatus("Primary keystone missing active manage challenge pool.", "error");
|
|
2129
|
+
return;
|
|
2130
|
+
}
|
|
2131
|
+
|
|
2132
|
+
this.showBusy({
|
|
2133
|
+
isBusy: true,
|
|
2134
|
+
title: 'Elevating Identity...',
|
|
2135
|
+
msg: 'Generating cryptographic sovereign challenge pool...',
|
|
2136
|
+
animationEmoji: '🔑'
|
|
2137
|
+
});
|
|
2138
|
+
this.setChangePasswordStatus("Generating sovereign challenge pool...", "info");
|
|
2139
|
+
const newChallenges = await generatePoolChallenges({
|
|
2140
|
+
config: initialManagePool.config,
|
|
2141
|
+
masterSecret: newPassword,
|
|
2142
|
+
});
|
|
2143
|
+
const newManagePool = {
|
|
2144
|
+
id: POOL_ID_MANAGE,
|
|
2145
|
+
config: initialManagePool.config,
|
|
2146
|
+
challenges: newChallenges,
|
|
2147
|
+
};
|
|
2148
|
+
|
|
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;
|
|
2160
|
+
|
|
2161
|
+
let requestBody: any = {
|
|
2162
|
+
primaryTjpAddr,
|
|
2163
|
+
newManagePool,
|
|
2164
|
+
};
|
|
2165
|
+
|
|
2166
|
+
if (custodianProvider === 'google' || custodianProvider === 'github') {
|
|
2167
|
+
this.showBusy({
|
|
2168
|
+
isBusy: true,
|
|
2169
|
+
title: 'Re-authenticating with Provider...',
|
|
2170
|
+
msg: `Please complete authentication in the ${custodianProvider} popup window...`,
|
|
2171
|
+
animationEmoji: '🔒'
|
|
2172
|
+
});
|
|
2173
|
+
this.setChangePasswordStatus(`Please authenticate in the popup with ${custodianProvider} to verify ownership...`, "info");
|
|
2174
|
+
const resPopup = await executeSsoOAuthPopup({
|
|
2175
|
+
providerId: custodianProvider,
|
|
2176
|
+
parentTjpAddr: primaryTjpAddr,
|
|
2177
|
+
});
|
|
2178
|
+
|
|
2179
|
+
if (!resPopup.success || !resPopup.code) {
|
|
2180
|
+
this.showBusy({ isBusy: false });
|
|
2181
|
+
this.setChangePasswordStatus(resPopup.message || "Re-authentication cancelled.", "error");
|
|
2182
|
+
return;
|
|
2183
|
+
}
|
|
2184
|
+
|
|
2185
|
+
requestBody.providerId = custodianProvider;
|
|
2186
|
+
requestBody.code = resPopup.code;
|
|
2187
|
+
requestBody.redirectUri = resPopup.redirectUri;
|
|
2188
|
+
} else {
|
|
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");
|
|
2197
|
+
return;
|
|
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
|
+
|
|
2252
|
+
requestBody.email = email;
|
|
2253
|
+
requestBody.code = otpCode;
|
|
2254
|
+
}
|
|
2255
|
+
|
|
2256
|
+
this.showBusy({
|
|
2257
|
+
isBusy: true,
|
|
2258
|
+
title: 'Updating Keystone Timeline...',
|
|
2259
|
+
msg: 'Publishing sovereign master password and evolving identity DAG...',
|
|
2260
|
+
animationEmoji: '⏳'
|
|
2261
|
+
});
|
|
2262
|
+
this.setChangePasswordStatus("Elevating identity to sovereign master password on server...", "info");
|
|
2263
|
+
const response = await fetch('/api/identity/custodian/add-master-password', {
|
|
2264
|
+
method: 'POST',
|
|
2265
|
+
headers: { 'Content-Type': 'application/json' },
|
|
2266
|
+
body: JSON.stringify(requestBody),
|
|
2267
|
+
});
|
|
2268
|
+
|
|
2269
|
+
const resJson = await response.json();
|
|
2270
|
+
if (!response.ok || !resJson.success) {
|
|
2271
|
+
throw new Error(resJson.error || resJson.message || `Server error (${response.status})`);
|
|
2272
|
+
}
|
|
2273
|
+
|
|
2274
|
+
const evolvedParent = resJson.primaryKeystone;
|
|
2275
|
+
|
|
2276
|
+
// Save to local user space & metaspace
|
|
2277
|
+
const metaspace = await getGlobalMetaspace_waitIfNeeded();
|
|
2278
|
+
if (!metaspace) { throw new Error("Global metaspace not available"); }
|
|
2279
|
+
const space = await metaspace.getLocalUserSpace({ lock: false });
|
|
2280
|
+
if (!space) { throw new Error("Local user space not available"); }
|
|
2281
|
+
|
|
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
|
+
|
|
2360
|
+
await updateSpecialIndex({
|
|
2361
|
+
type: "keystones",
|
|
2362
|
+
rel8nInfos: [
|
|
2363
|
+
{
|
|
2364
|
+
rel8nName: "keystone",
|
|
2365
|
+
ibGibs: [toDto({ ibGib: parentWithUserDelegate }), toDto({ ibGib: userSyncDelegate })]
|
|
2366
|
+
}
|
|
2367
|
+
],
|
|
2368
|
+
metaspace,
|
|
2369
|
+
space
|
|
2370
|
+
});
|
|
2371
|
+
|
|
2372
|
+
const evolvedAddr = getIbGibAddr({ ibGib: parentWithUserDelegate });
|
|
2373
|
+
|
|
2374
|
+
// Dispatch EVENT_IBGIB_IDENTITY_CHANGED
|
|
2375
|
+
window.dispatchEvent(new CustomEvent(EVENT_IBGIB_IDENTITY_CHANGED, {
|
|
2376
|
+
detail: { activeIdentityAddr: evolvedAddr },
|
|
2377
|
+
bubbles: true,
|
|
2378
|
+
composed: true
|
|
2379
|
+
}));
|
|
2380
|
+
|
|
2381
|
+
this.setChangePasswordStatus("Successfully added Master Password! Identity is now sovereign.", "success");
|
|
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
|
+
|
|
2394
|
+
// Refresh the view
|
|
2395
|
+
this.ibGibAddr = evolvedAddr;
|
|
2396
|
+
this.staticIbGib = parentWithUserDelegate;
|
|
2397
|
+
await this.loadIbGib({ getLatest: true });
|
|
2398
|
+
|
|
2399
|
+
// Clear inputs
|
|
2400
|
+
this.elements!.inputNewPasswordEl.value = '';
|
|
2401
|
+
this.elements!.inputConfirmPasswordEl.value = '';
|
|
2402
|
+
|
|
2403
|
+
} catch (error) {
|
|
2404
|
+
console.error(`${lc} Elevation failed: ${extractErrorMsg(error)}`);
|
|
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
|
+
}
|
|
2410
|
+
} finally {
|
|
2411
|
+
this.showBusy({ isBusy: false });
|
|
2412
|
+
}
|
|
2413
|
+
}
|
|
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
|
+
|
|
1302
2463
|
protected async handleChangePassword(): Promise<void> {
|
|
1303
2464
|
const lc = `${this.lc}[${this.handleChangePassword.name}]`;
|
|
2465
|
+
|
|
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;
|
|
2471
|
+
if (isCustodialOnly) {
|
|
2472
|
+
await this.handleAddMasterPassword();
|
|
2473
|
+
return;
|
|
2474
|
+
}
|
|
2475
|
+
|
|
1304
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
|
+
});
|
|
1305
2483
|
|
|
1306
2484
|
try {
|
|
1307
2485
|
const currentPassword = this.elements!.inputCurrentPasswordEl.value;
|
|
@@ -1334,6 +2512,12 @@ export class KeystoneDetailsComponentInstance
|
|
|
1334
2512
|
return;
|
|
1335
2513
|
}
|
|
1336
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
|
+
});
|
|
1337
2521
|
this.setChangePasswordStatus("Evolving primary identity keystone...", "info");
|
|
1338
2522
|
|
|
1339
2523
|
const metaspace = await getGlobalMetaspace_waitIfNeeded();
|
|
@@ -1341,8 +2525,7 @@ export class KeystoneDetailsComponentInstance
|
|
|
1341
2525
|
const space = await metaspace.getLocalUserSpace({ lock: false });
|
|
1342
2526
|
if (!space) { throw new Error(`(UNEXPECTED) couldn't get default local space? (E: 2fc1281c28351fd378a1d7f88eaa0526)`); }
|
|
1343
2527
|
|
|
1344
|
-
// 2. Rotate parent identity
|
|
1345
|
-
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
|
|
1346
2529
|
const { newKeystone: evolvedParent, newDelegates: evolvedDelegates } =
|
|
1347
2530
|
await keystoneService.changePassword({
|
|
1348
2531
|
keystoneIbGib: this.ibGib!,
|
|
@@ -1352,62 +2535,22 @@ export class KeystoneDetailsComponentInstance
|
|
|
1352
2535
|
metaspace,
|
|
1353
2536
|
space
|
|
1354
2537
|
});
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
this.
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
// for (const delegateInfo of Object.values(delegates)) {
|
|
1364
|
-
// const delegateAddr = delegateInfo.delegateAddr;
|
|
1365
|
-
// const delegateKeystone = await keystoneService.getLatestKeystone({
|
|
1366
|
-
// addr: delegateAddr,
|
|
1367
|
-
// metaspace,
|
|
1368
|
-
// space,
|
|
1369
|
-
// });
|
|
1370
|
-
|
|
1371
|
-
// // Identify user-owned challenge pools in the delegate that match the old password
|
|
1372
|
-
// const poolsToRotate: string[] = [];
|
|
1373
|
-
// for (const pool of delegateKeystone.data?.challengePools ?? []) {
|
|
1374
|
-
// if (!pool.isForeign) {
|
|
1375
|
-
// /**
|
|
1376
|
-
// * WARNING: There is an extreme edge case that if two pools
|
|
1377
|
-
// * happen to be owned by different parties but share the
|
|
1378
|
-
// * same secret, this will change the wrong pool's secret
|
|
1379
|
-
// * also. For V1, this shouldn't matter. But we need a
|
|
1380
|
-
// * better way of tracking pool ownership for V2 (or even
|
|
1381
|
-
// * V1 if we have multi-party support). (DO NOT REMOVE
|
|
1382
|
-
// * THIS COMMENT UNTIL THIS IS ADDRESSED)
|
|
1383
|
-
// */
|
|
1384
|
-
// const isPoolMatch = await keystoneService.verifySigningSecret({
|
|
1385
|
-
// keystoneIbGib: delegateKeystone,
|
|
1386
|
-
// signingSecret: currentPassword,
|
|
1387
|
-
// poolId: pool.id
|
|
1388
|
-
// });
|
|
1389
|
-
// if (isPoolMatch) { poolsToRotate.push(pool.id); }
|
|
1390
|
-
// }
|
|
1391
|
-
// }
|
|
1392
|
-
|
|
1393
|
-
// if (poolsToRotate.length > 0) {
|
|
1394
|
-
// console.log(`${lc} Rotating pools in delegate ${delegateInfo.delegateTjpAddr}: ${poolsToRotate.join(', ')}`);
|
|
1395
|
-
// const {newKeystone: evolvedDelegate} = await keystoneService.changePassword({
|
|
1396
|
-
// keystoneIbGib: delegateKeystone,
|
|
1397
|
-
// newMasterSecret: newPassword,
|
|
1398
|
-
// signingSecret: currentPassword,
|
|
1399
|
-
// signingPoolId: 'manage',
|
|
1400
|
-
// poolIds: poolsToRotate,
|
|
1401
|
-
// metaspace,
|
|
1402
|
-
// space
|
|
1403
|
-
// });
|
|
1404
|
-
// evolvedDelegates.push(evolvedDelegate);
|
|
1405
|
-
// }
|
|
1406
|
-
// }
|
|
1407
|
-
|
|
1408
|
-
// 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
|
+
});
|
|
1409
2546
|
this.setChangePasswordStatus("Syncing evolved keystones to server...", "info");
|
|
1410
|
-
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! });
|
|
1411
2554
|
|
|
1412
2555
|
const syncRes = await getApiBridge().putEvolveKeystone({
|
|
1413
2556
|
domainAddr: parentTjpAddr,
|
|
@@ -1418,7 +2561,7 @@ export class KeystoneDetailsComponentInstance
|
|
|
1418
2561
|
throw new Error(`Server synchronization rejected: ${syncRes.message}`);
|
|
1419
2562
|
}
|
|
1420
2563
|
|
|
1421
|
-
//
|
|
2564
|
+
// 4. Update index locally
|
|
1422
2565
|
this.setChangePasswordStatus("Updating local indexes...", "info");
|
|
1423
2566
|
const newKeystoneIbGibs = [toDto({ ibGib: evolvedParent })];
|
|
1424
2567
|
if (evolvedDelegates) {
|
|
@@ -1445,11 +2588,16 @@ export class KeystoneDetailsComponentInstance
|
|
|
1445
2588
|
this.elements!.inputConfirmPasswordEl.value = "";
|
|
1446
2589
|
|
|
1447
2590
|
this.setChangePasswordStatus("Password updated successfully!", "success");
|
|
2591
|
+
this.ibGibAddr = getIbGibAddr({ ibGib: evolvedParent });
|
|
2592
|
+
this.staticIbGib = evolvedParent;
|
|
1448
2593
|
await this.loadIbGib({ getLatest: true });
|
|
1449
2594
|
|
|
1450
2595
|
} catch (error) {
|
|
2596
|
+
debugger; // error in handleChangePassword
|
|
1451
2597
|
console.error(`${lc} Password rotation failed: ${extractErrorMsg(error)}`);
|
|
1452
2598
|
this.setChangePasswordStatus(`Update failed: ${extractErrorMsg(error)}`, "error");
|
|
2599
|
+
} finally {
|
|
2600
|
+
this.showBusy({ isBusy: false });
|
|
1453
2601
|
}
|
|
1454
2602
|
}
|
|
1455
2603
|
}
|