@ibgib/web-gib 0.0.61 → 0.0.63
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/AUTO-GENERATED-version.d.mts +1 -1
- package/dist/AUTO-GENERATED-version.mjs +1 -1
- package/dist/helpers.web.d.mts +20 -3
- package/dist/helpers.web.d.mts.map +1 -1
- package/dist/helpers.web.mjs +84 -3
- package/dist/helpers.web.mjs.map +1 -1
- package/dist/identity/device-delegate-helper.d.mts +48 -0
- package/dist/identity/device-delegate-helper.d.mts.map +1 -1
- package/dist/identity/device-delegate-helper.mjs +86 -2
- package/dist/identity/device-delegate-helper.mjs.map +1 -1
- package/dist/identity/device-delegate.respec.mjs +6 -6
- package/dist/identity/device-delegate.respec.mjs.map +1 -1
- package/dist/identity/passkey-helper.d.mts +77 -0
- package/dist/identity/passkey-helper.d.mts.map +1 -0
- package/dist/identity/passkey-helper.mjs +258 -0
- package/dist/identity/passkey-helper.mjs.map +1 -0
- package/dist/identity/passkey.respec.d.mts +2 -0
- package/dist/identity/passkey.respec.d.mts.map +1 -0
- package/dist/identity/passkey.respec.mjs +169 -0
- package/dist/identity/passkey.respec.mjs.map +1 -0
- package/dist/index.d.mts +1 -0
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +1 -0
- package/dist/index.mjs.map +1 -1
- package/dist/ui/component/identity/add-device/add-device.d.mts.map +1 -1
- package/dist/ui/component/identity/add-device/add-device.mjs +1 -0
- package/dist/ui/component/identity/add-device/add-device.mjs.map +1 -1
- package/dist/ui/component/identity/keystone-creator/keystone-creator.css +26 -0
- 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 +60 -4
- package/dist/ui/component/identity/keystone-creator/keystone-creator.mjs +518 -74
- package/dist/ui/component/identity/keystone-creator/keystone-creator.mjs.map +1 -1
- package/dist/ui/component/identity/keystone-details/keystone-details.d.mts +28 -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 +46 -0
- package/dist/ui/component/identity/keystone-details/keystone-details.mjs +1146 -208
- package/dist/ui/component/identity/keystone-details/keystone-details.mjs.map +1 -1
- package/dist/ui/component/identity/remove-device/remove-device.d.mts.map +1 -1
- package/dist/ui/component/identity/remove-device/remove-device.mjs +1 -0
- package/dist/ui/component/identity/remove-device/remove-device.mjs.map +1 -1
- package/dist/ui/ui-helpers.d.mts +3 -0
- package/dist/ui/ui-helpers.d.mts.map +1 -1
- package/dist/ui/ui-helpers.mjs +116 -7
- package/dist/ui/ui-helpers.mjs.map +1 -1
- package/package.json +2 -2
- package/src/AUTO-GENERATED-version.mts +1 -1
- package/src/helpers.web.mts +117 -7
- package/src/identity/device-delegate-helper.mts +135 -2
- package/src/identity/device-delegate.respec.mts +6 -6
- package/src/identity/passkey-helper.mts +337 -0
- package/src/identity/passkey.respec.mts +194 -0
- package/src/index.mts +1 -0
- package/src/ui/component/identity/add-device/add-device.mts +1 -0
- package/src/ui/component/identity/keystone-creator/keystone-creator.css +26 -0
- package/src/ui/component/identity/keystone-creator/keystone-creator.html +60 -4
- package/src/ui/component/identity/keystone-creator/keystone-creator.mts +561 -77
- package/src/ui/component/identity/keystone-details/keystone-details.html +46 -0
- package/src/ui/component/identity/keystone-details/keystone-details.mts +1239 -224
- package/src/ui/component/identity/remove-device/remove-device.mts +1 -0
- package/src/ui/ui-helpers.mts +123 -6
- package/tools/auto-generated-agent-skills/skills/ibgib-create-app-server/SKILL.md +50 -0
- package/src/identity/custodian-delegate-helper.mts.bak +0 -7
- package/src/identity/sso/sso-config-helper.mts.bak +0 -52
- package/src/identity/sso/sso-custodian-service.mts.bak +0 -566
- package/src/identity/sso/sso-custodian-service.respec.mts.bak +0 -298
|
@@ -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, delay } from "@ibgib/helper-gib/dist/helpers/utils-helper.mjs";
|
|
6
6
|
import { IbGibAddr } from "@ibgib/ts-gib/dist/types.mjs";
|
|
7
7
|
import { IbGib_V1 } from "@ibgib/ts-gib/dist/V1/types.mjs";
|
|
8
8
|
import { getIbAndGib, getIbGibAddr } from "@ibgib/ts-gib/dist/helper.mjs";
|
|
@@ -20,15 +20,22 @@ import { EVENT_IBGIB_IDENTITY_REQUEST_CHANGE, EVENT_IBGIB_IDENTITY_CHANGED } fro
|
|
|
20
20
|
import { getApiBridge } from "../../../../api/ibgib-api-bridge.mjs";
|
|
21
21
|
import { getKeystoneSyncService } from "../../../../identity/keystone-sync-service.mjs";
|
|
22
22
|
import { updateSpecialIndex } from "@ibgib/core-gib/dist/timeline/timeline-api.mjs";
|
|
23
|
-
import { getTjpAddr, toDto } from "@ibgib/core-gib/dist/common/other/ibgib-helper.mjs";
|
|
23
|
+
import { getTjpAddr, toDto, getTimelinesGroupedByTjp } from "@ibgib/core-gib/dist/common/other/ibgib-helper.mjs";
|
|
24
24
|
import { parseSpaceIb } from "@ibgib/core-gib/dist/witness/space/space-helper.mjs";
|
|
25
25
|
import { executeSsoOAuthPopup } from "../../../../identity/sso-popup-helper.mjs";
|
|
26
26
|
import { getComponentCtorArg } from "../../../../app-bootstrap/init-orchestration.mjs";
|
|
27
|
-
import { promptForSecret } from "../../../../helpers.web.mjs";
|
|
28
|
-
import { isSsoProviderLinked, getKeystoneCapabilities, isDelegateExpired } from "@ibgib/core-gib/dist/keystone/delegate/delegate-helpers.mjs";
|
|
27
|
+
import { promptForSecret, promptForAuthoritySecret } from "../../../../helpers.web.mjs";
|
|
28
|
+
import { isSsoProviderLinked, getKeystoneCapabilities, isDelegateExpired, deriveDelegateSecret, createSyncDelegateKeystone, isCustodialDelegate } from "@ibgib/core-gib/dist/keystone/delegate/delegate-helpers.mjs";
|
|
29
29
|
import { generatePoolChallenges } from "@ibgib/core-gib/dist/keystone/keystone-helpers.mjs";
|
|
30
|
-
import { POOL_ID_MANAGE } from "@ibgib/core-gib/dist/keystone/keystone-constants.mjs";
|
|
30
|
+
import { POOL_ID_MANAGE, KEYSTONE_VERB_SYNC, KEYSTONE_VERB_CONNECT, KEYSTONE_VERB_MANAGE } from "@ibgib/core-gib/dist/keystone/keystone-constants.mjs";
|
|
31
|
+
import { generateDefaultDeviceTag } from "../../../../util/device-helper.mjs";
|
|
31
32
|
import { getGlobalMetaspace_waitIfNeeded } from "../../../../helpers.mjs";
|
|
33
|
+
import { createSovereignDeviceSyncDelegate, createOAuth2DeviceSyncDelegate, createEmailDeviceSyncDelegate } from "../../../../identity/device-delegate-helper.mjs";
|
|
34
|
+
import { isPasskeyPrfSupported, registerPasskeyCredential, evaluatePasskeyPrf, createPasskeyDeviceSyncDelegate, bufferToHex } from "../../../../identity/passkey-helper.mjs";
|
|
35
|
+
import {
|
|
36
|
+
KeystonePasskeyEntry, KeystoneChallengePool, KeystonePoolConfig_HashV1,
|
|
37
|
+
KeystoneChallengeType, KeystoneReplenishStrategy
|
|
38
|
+
} from "@ibgib/core-gib/dist/keystone/keystone-types.mjs";
|
|
32
39
|
|
|
33
40
|
const logalot = false;
|
|
34
41
|
|
|
@@ -89,6 +96,11 @@ interface KeystoneDetailsElements extends ElementsBase {
|
|
|
89
96
|
rawKeystoneJsonEl: HTMLPreElement;
|
|
90
97
|
delegatesCardEl: HTMLDivElement;
|
|
91
98
|
delegatesListEl: HTMLTableSectionElement;
|
|
99
|
+
passkeysCardEl: HTMLDivElement;
|
|
100
|
+
passkeysListEl: HTMLTableSectionElement;
|
|
101
|
+
btnAddPasskeyEl: HTMLButtonElement;
|
|
102
|
+
selectPasskeyAttachmentEl?: HTMLSelectElement;
|
|
103
|
+
passkeyStatusMsgEl: HTMLDivElement;
|
|
92
104
|
historicalWarningBannerEl: HTMLDivElement;
|
|
93
105
|
containerEl: HTMLDivElement;
|
|
94
106
|
ssoLinkCardEl: HTMLDivElement;
|
|
@@ -101,6 +113,10 @@ interface KeystoneDetailsElements extends ElementsBase {
|
|
|
101
113
|
btnChangePasswordEl: HTMLButtonElement;
|
|
102
114
|
changePasswordStatusEl: HTMLDivElement;
|
|
103
115
|
changePasswordCardEl: HTMLDivElement;
|
|
116
|
+
sectionAddPasswordOtpEl: HTMLDivElement;
|
|
117
|
+
inputAddPasswordOtpEl: HTMLInputElement;
|
|
118
|
+
btnResendAddPasswordOtpEl: HTMLButtonElement;
|
|
119
|
+
descAddPasswordOtpEl: HTMLParagraphElement;
|
|
104
120
|
}
|
|
105
121
|
|
|
106
122
|
export class KeystoneDetailsComponentInstance
|
|
@@ -120,6 +136,7 @@ export class KeystoneDetailsComponentInstance
|
|
|
120
136
|
private _onIdentityChanged: ((e: any) => void) | null = null;
|
|
121
137
|
private keystoneCache: Map<string, { username: string, description: string, isPrimary: boolean, n: number }> = new Map();
|
|
122
138
|
private aggregatedDetails?: any;
|
|
139
|
+
private resendCooldownInterval: any = null;
|
|
123
140
|
|
|
124
141
|
constructor() {
|
|
125
142
|
super();
|
|
@@ -142,6 +159,39 @@ export class KeystoneDetailsComponentInstance
|
|
|
142
159
|
}
|
|
143
160
|
}
|
|
144
161
|
|
|
162
|
+
override async disconnected(): Promise<void> {
|
|
163
|
+
if (this._onIdentityChanged) {
|
|
164
|
+
window.removeEventListener(EVENT_IBGIB_IDENTITY_CHANGED, this._onIdentityChanged);
|
|
165
|
+
}
|
|
166
|
+
if (this.resendCooldownInterval) {
|
|
167
|
+
clearInterval(this.resendCooldownInterval);
|
|
168
|
+
this.resendCooldownInterval = null;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
private startResendCooldownTimer(durationSecs: number = 30): void {
|
|
173
|
+
const btn = this.elements?.btnResendAddPasswordOtpEl;
|
|
174
|
+
if (!btn) return;
|
|
175
|
+
if (this.resendCooldownInterval) {
|
|
176
|
+
clearInterval(this.resendCooldownInterval);
|
|
177
|
+
this.resendCooldownInterval = null;
|
|
178
|
+
}
|
|
179
|
+
let remaining = durationSecs;
|
|
180
|
+
btn.disabled = true;
|
|
181
|
+
btn.textContent = `Resend Code (${remaining}s)`;
|
|
182
|
+
this.resendCooldownInterval = setInterval(() => {
|
|
183
|
+
remaining -= 1;
|
|
184
|
+
if (remaining <= 0) {
|
|
185
|
+
clearInterval(this.resendCooldownInterval);
|
|
186
|
+
this.resendCooldownInterval = null;
|
|
187
|
+
btn.disabled = false;
|
|
188
|
+
btn.textContent = "Resend Code";
|
|
189
|
+
} else {
|
|
190
|
+
btn.textContent = `Resend Code (${remaining}s)`;
|
|
191
|
+
}
|
|
192
|
+
}, 1000);
|
|
193
|
+
}
|
|
194
|
+
|
|
145
195
|
private async getKeystoneDetails(addr: string): Promise<{ username: string, description: string, isPrimary: boolean, n: number }> {
|
|
146
196
|
const cached = this.keystoneCache.get(addr);
|
|
147
197
|
if (cached) return cached;
|
|
@@ -208,6 +258,11 @@ export class KeystoneDetailsComponentInstance
|
|
|
208
258
|
this.elements.rawKeystoneJsonEl = shadow.getElementById('raw-keystone-json') as HTMLPreElement;
|
|
209
259
|
this.elements.delegatesCardEl = shadow.getElementById('delegates-card') as HTMLDivElement;
|
|
210
260
|
this.elements.delegatesListEl = shadow.getElementById('delegates-list') as HTMLTableSectionElement;
|
|
261
|
+
this.elements.passkeysCardEl = shadow.getElementById('passkeys-card') as HTMLDivElement;
|
|
262
|
+
this.elements.passkeysListEl = shadow.getElementById('passkeys-list') as HTMLTableSectionElement;
|
|
263
|
+
this.elements.btnAddPasskeyEl = shadow.getElementById('btn-add-passkey') as HTMLButtonElement;
|
|
264
|
+
this.elements.selectPasskeyAttachmentEl = shadow.getElementById('select-passkey-attachment') as HTMLSelectElement;
|
|
265
|
+
this.elements.passkeyStatusMsgEl = shadow.getElementById('passkey-status-msg') as HTMLDivElement;
|
|
211
266
|
this.elements.containerEl = shadow.getElementById('container') as HTMLDivElement;
|
|
212
267
|
this.elements.historicalWarningBannerEl = shadow.getElementById('historical-warning-banner') as HTMLDivElement;
|
|
213
268
|
this.elements.ssoLinkCardEl = shadow.getElementById('sso-link-card') as HTMLDivElement;
|
|
@@ -220,6 +275,10 @@ export class KeystoneDetailsComponentInstance
|
|
|
220
275
|
this.elements.btnChangePasswordEl = shadow.getElementById('btn-change-password') as HTMLButtonElement;
|
|
221
276
|
this.elements.changePasswordStatusEl = shadow.getElementById('change-password-status') as HTMLDivElement;
|
|
222
277
|
this.elements.changePasswordCardEl = shadow.getElementById('change-password-card') as HTMLDivElement;
|
|
278
|
+
this.elements.sectionAddPasswordOtpEl = shadow.getElementById('section-add-password-otp') as HTMLDivElement;
|
|
279
|
+
this.elements.inputAddPasswordOtpEl = shadow.getElementById('input-add-password-otp') as HTMLInputElement;
|
|
280
|
+
this.elements.btnResendAddPasswordOtpEl = shadow.getElementById('btn-resend-add-password-otp') as HTMLButtonElement;
|
|
281
|
+
this.elements.descAddPasswordOtpEl = shadow.getElementById('desc-add-password-otp') as HTMLParagraphElement;
|
|
223
282
|
|
|
224
283
|
// ElementBase requires contentEl
|
|
225
284
|
this.elements.contentEl = shadow.getElementById('identity-details-view') as HTMLDivElement;
|
|
@@ -245,12 +304,6 @@ export class KeystoneDetailsComponentInstance
|
|
|
245
304
|
}
|
|
246
305
|
}
|
|
247
306
|
|
|
248
|
-
override async disconnected(): Promise<void> {
|
|
249
|
-
if (this._onIdentityChanged) {
|
|
250
|
-
window.removeEventListener(EVENT_IBGIB_IDENTITY_CHANGED, this._onIdentityChanged);
|
|
251
|
-
}
|
|
252
|
-
}
|
|
253
|
-
|
|
254
307
|
private initHandlers() {
|
|
255
308
|
// Setup raw JSON accordion toggle
|
|
256
309
|
this.elements!.rawAccordionHeaderEl.addEventListener('click', () => this.toggleAccordion());
|
|
@@ -260,9 +313,11 @@ export class KeystoneDetailsComponentInstance
|
|
|
260
313
|
this.elements!.btnSetActiveEl.addEventListener('click', () => this.handleSetActiveIdentity());
|
|
261
314
|
this.elements!.btnCheckUpdatesEl.addEventListener('click', () => this.handleCheckForUpdates());
|
|
262
315
|
this.elements!.btnUpdateIdentityEl.addEventListener('click', () => this.handleUpdateIdentity());
|
|
316
|
+
this.elements!.btnAddPasskeyEl?.addEventListener('click', () => this.handleAddPasskey());
|
|
263
317
|
this.elements!.btnLinkGoogleEl.addEventListener('click', () => this.handleSSOClick('google'));
|
|
264
318
|
this.elements!.btnLinkGithubEl.addEventListener('click', () => this.handleSSOClick('github'));
|
|
265
319
|
this.elements!.btnChangePasswordEl.addEventListener('click', () => this.handleChangePassword());
|
|
320
|
+
this.elements!.btnResendAddPasswordOtpEl?.addEventListener('click', () => this.handleResendAddPasswordOtp());
|
|
266
321
|
}
|
|
267
322
|
|
|
268
323
|
private toggleAccordion() {
|
|
@@ -458,15 +513,22 @@ export class KeystoneDetailsComponentInstance
|
|
|
458
513
|
|
|
459
514
|
// Check if origin provider or linked
|
|
460
515
|
const originProvider = this.aggregatedDetails?.custodianProvider || this.ibGib?.data?.frameDetails?.custodianProvider;
|
|
461
|
-
const isGoogleLinked =
|
|
462
|
-
|
|
516
|
+
const isGoogleLinked = !!this.ibGib ?
|
|
517
|
+
isSsoProviderLinked({ keystone: this.ibGib, aggregatedDetails: this.aggregatedDetails, providerId: 'google' }) :
|
|
518
|
+
false;
|
|
519
|
+
const isGithubLinked = !!this.ibGib ?
|
|
520
|
+
isSsoProviderLinked({ keystone: this.ibGib, aggregatedDetails: this.aggregatedDetails, providerId: 'github' }) :
|
|
521
|
+
false;
|
|
463
522
|
|
|
464
|
-
const caps =
|
|
523
|
+
const caps = !!this.ibGib ?
|
|
524
|
+
getKeystoneCapabilities({ keystone: this.ibGib, aggregatedDetails: this.aggregatedDetails }) :
|
|
525
|
+
undefined;
|
|
465
526
|
|
|
466
527
|
// Google Card States
|
|
467
528
|
const googleCard = this.elements.btnLinkGoogleEl;
|
|
468
529
|
const googleLabel = googleCard.querySelector('.toggle-state-text')!;
|
|
469
530
|
if (isGoogleLinked) {
|
|
531
|
+
if (!caps) { throw new Error(`(UNEXPECTED) isGoogleLinked but capabilities falsy? (E: cc62e8357b66860dd2cb8148c833b726)`); }
|
|
470
532
|
googleCard.classList.remove('unlinked');
|
|
471
533
|
googleCard.classList.add('linked');
|
|
472
534
|
const isOrigin = originProvider === 'google';
|
|
@@ -491,6 +553,7 @@ export class KeystoneDetailsComponentInstance
|
|
|
491
553
|
const githubCard = this.elements.btnLinkGithubEl;
|
|
492
554
|
const githubLabel = githubCard.querySelector('.toggle-state-text')!;
|
|
493
555
|
if (isGithubLinked) {
|
|
556
|
+
if (!caps) { throw new Error(`(UNEXPECTED) isGithubLinked but capabilities falsy? (E: e94688210538e0e82878088873a46826)`); }
|
|
494
557
|
githubCard.classList.remove('unlinked');
|
|
495
558
|
githubCard.classList.add('linked');
|
|
496
559
|
const isOrigin = originProvider === 'github';
|
|
@@ -514,16 +577,18 @@ export class KeystoneDetailsComponentInstance
|
|
|
514
577
|
}
|
|
515
578
|
|
|
516
579
|
private updatePasswordCardView(): void {
|
|
517
|
-
const caps =
|
|
580
|
+
const caps = !!this.ibGib ?
|
|
581
|
+
getKeystoneCapabilities({ keystone: this.ibGib, aggregatedDetails: this.aggregatedDetails }) :
|
|
582
|
+
undefined;
|
|
518
583
|
const cardTitleEl = this.elements?.changePasswordCardEl?.querySelector('h3');
|
|
519
584
|
const cardDescEl = this.elements?.changePasswordCardEl?.querySelector('.description');
|
|
520
585
|
const currentPasswordGroup = this.elements?.inputCurrentPasswordEl?.closest('.form-group') as HTMLElement | null;
|
|
521
586
|
const btnChangePassword = this.elements?.btnChangePasswordEl;
|
|
522
587
|
|
|
523
|
-
const
|
|
524
|
-
if (
|
|
588
|
+
const hasPassword = !!caps?.hasPassword;
|
|
589
|
+
if (!hasPassword) {
|
|
525
590
|
if (cardTitleEl) cardTitleEl.textContent = "Add Master Password";
|
|
526
|
-
if (cardDescEl) cardDescEl.textContent = "Establish
|
|
591
|
+
if (cardDescEl) cardDescEl.textContent = "Establish a master password for this identity. This enables sovereign password authentication alongside your passkeys and linked providers.";
|
|
527
592
|
if (currentPasswordGroup) currentPasswordGroup.style.display = 'none';
|
|
528
593
|
if (btnChangePassword) btnChangePassword.textContent = "Add Master Password";
|
|
529
594
|
} else {
|
|
@@ -532,6 +597,13 @@ export class KeystoneDetailsComponentInstance
|
|
|
532
597
|
if (currentPasswordGroup) currentPasswordGroup.style.display = '';
|
|
533
598
|
if (btnChangePassword) btnChangePassword.textContent = "Update Password";
|
|
534
599
|
}
|
|
600
|
+
|
|
601
|
+
if (this.elements?.sectionAddPasswordOtpEl) {
|
|
602
|
+
this.elements.sectionAddPasswordOtpEl.classList.add('hidden');
|
|
603
|
+
if (this.elements.inputAddPasswordOtpEl) {
|
|
604
|
+
this.elements.inputAddPasswordOtpEl.value = '';
|
|
605
|
+
}
|
|
606
|
+
}
|
|
535
607
|
}
|
|
536
608
|
|
|
537
609
|
private async updateFrameDetailsView(): Promise<void> {
|
|
@@ -643,16 +715,19 @@ export class KeystoneDetailsComponentInstance
|
|
|
643
715
|
// Render Challenge Pools
|
|
644
716
|
this.renderChallengePools(keystone.data?.challengePools || []);
|
|
645
717
|
|
|
646
|
-
// Render Delegates, SSO link, and Password Change cards (only if this is a primary keystone, hide them for delegate/session keystones)
|
|
718
|
+
// Render Delegates, Passkeys, SSO link, and Password Change cards (only if this is a primary keystone, hide them for delegate/session keystones)
|
|
647
719
|
const details = await this.getKeystoneDetails(this.ibGibAddr);
|
|
648
720
|
if (details.isPrimary) {
|
|
649
721
|
this.elements.delegatesCardEl.classList.remove('hidden');
|
|
650
722
|
await this.renderDelegates(keystone.data?.delegates);
|
|
723
|
+
this.elements.passkeysCardEl.classList.remove('hidden');
|
|
724
|
+
await this.renderPasskeys(this.aggregatedDetails?.passkeys);
|
|
651
725
|
this.elements.ssoLinkCardEl.classList.remove('hidden');
|
|
652
726
|
this.elements.changePasswordCardEl.classList.remove('hidden');
|
|
653
727
|
this.updatePasswordCardView();
|
|
654
728
|
} else {
|
|
655
729
|
this.elements.delegatesCardEl.classList.add('hidden');
|
|
730
|
+
this.elements.passkeysCardEl.classList.add('hidden');
|
|
656
731
|
this.elements.ssoLinkCardEl.classList.add('hidden');
|
|
657
732
|
this.elements.changePasswordCardEl.classList.add('hidden');
|
|
658
733
|
}
|
|
@@ -687,6 +762,15 @@ export class KeystoneDetailsComponentInstance
|
|
|
687
762
|
if (this.elements.delegatesListEl) {
|
|
688
763
|
this.elements.delegatesListEl.innerHTML = '';
|
|
689
764
|
}
|
|
765
|
+
if (this.elements.passkeysListEl) {
|
|
766
|
+
this.elements.passkeysListEl.innerHTML = '';
|
|
767
|
+
}
|
|
768
|
+
if (this.elements.sectionAddPasswordOtpEl) {
|
|
769
|
+
this.elements.sectionAddPasswordOtpEl.classList.add('hidden');
|
|
770
|
+
if (this.elements.inputAddPasswordOtpEl) {
|
|
771
|
+
this.elements.inputAddPasswordOtpEl.value = '';
|
|
772
|
+
}
|
|
773
|
+
}
|
|
690
774
|
}
|
|
691
775
|
|
|
692
776
|
private renderChallengePools(pools: any[]) {
|
|
@@ -1007,11 +1091,17 @@ export class KeystoneDetailsComponentInstance
|
|
|
1007
1091
|
const lc = `${this.lc}[${this.handleRevokeDelegate.name}]`;
|
|
1008
1092
|
try {
|
|
1009
1093
|
let masterSecret: string;
|
|
1094
|
+
let matchedPoolId: string;
|
|
1010
1095
|
try {
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1096
|
+
const authResult = await promptForAuthoritySecret({
|
|
1097
|
+
keystone: this.ibGib,
|
|
1098
|
+
aggregatedDetails: this.aggregatedDetails,
|
|
1099
|
+
title: 'Revoke Delegate',
|
|
1100
|
+
msg: `Enter Master Password or touch security key to revoke delegate '${deviceName}':`,
|
|
1101
|
+
verb: KEYSTONE_VERB_MANAGE
|
|
1014
1102
|
});
|
|
1103
|
+
masterSecret = authResult.secret;
|
|
1104
|
+
matchedPoolId = authResult.poolId;
|
|
1015
1105
|
} catch (err) {
|
|
1016
1106
|
// User cancelled prompt
|
|
1017
1107
|
return;
|
|
@@ -1032,6 +1122,7 @@ export class KeystoneDetailsComponentInstance
|
|
|
1032
1122
|
parentKeystone: this.ibGib!,
|
|
1033
1123
|
delegateTjpAddr,
|
|
1034
1124
|
masterSecret,
|
|
1125
|
+
signingPoolId: matchedPoolId,
|
|
1035
1126
|
metaspace,
|
|
1036
1127
|
space
|
|
1037
1128
|
});
|
|
@@ -1085,6 +1176,480 @@ export class KeystoneDetailsComponentInstance
|
|
|
1085
1176
|
}
|
|
1086
1177
|
}
|
|
1087
1178
|
|
|
1179
|
+
private setPasskeyStatus(msg: string, type: 'info' | 'success' | 'error') {
|
|
1180
|
+
const statusEl = this.elements?.passkeyStatusMsgEl;
|
|
1181
|
+
if (!statusEl) return;
|
|
1182
|
+
statusEl.textContent = msg;
|
|
1183
|
+
statusEl.className = `status-msg ${type}`;
|
|
1184
|
+
statusEl.classList.remove('hidden');
|
|
1185
|
+
}
|
|
1186
|
+
|
|
1187
|
+
private async renderPasskeys(passkeys: Record<string, KeystonePasskeyEntry> | undefined) {
|
|
1188
|
+
if (!this.elements) { return; }
|
|
1189
|
+
const listEl = this.elements.passkeysListEl;
|
|
1190
|
+
listEl.innerHTML = '';
|
|
1191
|
+
|
|
1192
|
+
const caps = !!this.ibGib ?
|
|
1193
|
+
getKeystoneCapabilities({ keystone: this.ibGib, aggregatedDetails: this.aggregatedDetails }) :
|
|
1194
|
+
undefined;
|
|
1195
|
+
|
|
1196
|
+
// Update button text depending on whether the account is custodial-only (elevation) or sovereign/hybrid
|
|
1197
|
+
const isCustodialOnly = !!caps ? caps.isCustodial && !caps.isSovereign : false;
|
|
1198
|
+
if (this.elements.btnAddPasskeyEl) {
|
|
1199
|
+
this.elements.btnAddPasskeyEl.textContent = isCustodialOnly
|
|
1200
|
+
? "🛡️ Add Passkey (Passwordless Sovereign Elevation)"
|
|
1201
|
+
: "🛡️ Register New Passkey / Security Key";
|
|
1202
|
+
}
|
|
1203
|
+
|
|
1204
|
+
const validPasskeyEntries = Object.entries(passkeys || {}).filter(([_, entry]) => entry !== null && entry !== undefined);
|
|
1205
|
+
|
|
1206
|
+
if (validPasskeyEntries.length === 0) {
|
|
1207
|
+
listEl.innerHTML = `
|
|
1208
|
+
<tr>
|
|
1209
|
+
<td colspan="4" style="text-align: center; color: var(--clr-text-secondary, #b3b3b3); padding: 1rem;">
|
|
1210
|
+
No hardware passkeys or biometrics registered.
|
|
1211
|
+
</td>
|
|
1212
|
+
</tr>
|
|
1213
|
+
`;
|
|
1214
|
+
return;
|
|
1215
|
+
}
|
|
1216
|
+
|
|
1217
|
+
for (const [credId, entry] of validPasskeyEntries) {
|
|
1218
|
+
const row = document.createElement('tr');
|
|
1219
|
+
const createdText = entry.createdUTC ? new Date(entry.createdUTC).toLocaleString() : 'N/A';
|
|
1220
|
+
const nameText = entry.name || 'Hardware Key';
|
|
1221
|
+
|
|
1222
|
+
// Format transport badges
|
|
1223
|
+
let transportBadgesHtml = '';
|
|
1224
|
+
if (entry.transports && entry.transports.length > 0) {
|
|
1225
|
+
transportBadgesHtml = entry.transports.map(t => {
|
|
1226
|
+
let icon = '🔑';
|
|
1227
|
+
let label: string = t;
|
|
1228
|
+
if (t === 'internal') { icon = '💻'; label = 'Touch ID / Built-in'; }
|
|
1229
|
+
else if (t === 'usb') { icon = '🔑'; label = 'USB Key'; }
|
|
1230
|
+
else if (t === 'nfc') { icon = '📶'; label = 'NFC'; }
|
|
1231
|
+
else if (t === 'ble' || t === 'hybrid') { icon = '📲'; label = 'Mobile / Bluetooth'; }
|
|
1232
|
+
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>`;
|
|
1233
|
+
}).join(' ');
|
|
1234
|
+
} else {
|
|
1235
|
+
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>`;
|
|
1236
|
+
}
|
|
1237
|
+
|
|
1238
|
+
row.innerHTML = `
|
|
1239
|
+
<td>
|
|
1240
|
+
<div style="font-weight: 500;">${nameText}</div>
|
|
1241
|
+
<code style="font-size: 0.75rem; color: var(--clr-text-secondary, #b3b3b3); word-break: break-all;">${credId.substring(0, 16)}...</code>
|
|
1242
|
+
</td>
|
|
1243
|
+
<td>${transportBadgesHtml}</td>
|
|
1244
|
+
<td style="font-size: 0.85rem; color: var(--clr-text-secondary, #b3b3b3);">${createdText}</td>
|
|
1245
|
+
<td>
|
|
1246
|
+
<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);">
|
|
1247
|
+
Remove
|
|
1248
|
+
</button>
|
|
1249
|
+
</td>
|
|
1250
|
+
`;
|
|
1251
|
+
|
|
1252
|
+
const btnRemove = row.querySelector('.btn-remove-passkey') as HTMLButtonElement;
|
|
1253
|
+
btnRemove?.addEventListener('click', () => this.handleRemovePasskey(credId));
|
|
1254
|
+
|
|
1255
|
+
listEl.appendChild(row);
|
|
1256
|
+
}
|
|
1257
|
+
}
|
|
1258
|
+
|
|
1259
|
+
protected async handleAddPasskey(): Promise<void> {
|
|
1260
|
+
const lc = `${this.lc}[${this.handleAddPasskey.name}]`;
|
|
1261
|
+
if (!this.ibGib || !this.ibGibAddr) { return; }
|
|
1262
|
+
|
|
1263
|
+
try {
|
|
1264
|
+
const caps = getKeystoneCapabilities({ keystone: this.ibGib, aggregatedDetails: this.aggregatedDetails });
|
|
1265
|
+
const isCustodialOnly = caps.isCustodial && !caps.isSovereign;
|
|
1266
|
+
|
|
1267
|
+
const supported = await isPasskeyPrfSupported();
|
|
1268
|
+
if (!supported) {
|
|
1269
|
+
this.setPasskeyStatus("WebAuthn PRF extension is not supported in this browser environment.", "error");
|
|
1270
|
+
return;
|
|
1271
|
+
}
|
|
1272
|
+
|
|
1273
|
+
this.showBusy({
|
|
1274
|
+
isBusy: true,
|
|
1275
|
+
title: 'Touch Biometrics or Security Key',
|
|
1276
|
+
msg: 'Please touch your fingerprint sensor, Face ID, or insert your USB security key when prompted...',
|
|
1277
|
+
animationEmoji: '🛡️'
|
|
1278
|
+
});
|
|
1279
|
+
|
|
1280
|
+
const username = this.aggregatedDetails?.username || this.ibGib.data?.frameDetails?.username || 'user';
|
|
1281
|
+
const rawSalt = crypto.getRandomValues(new Uint8Array(32));
|
|
1282
|
+
const saltHex = bufferToHex(rawSalt.buffer as ArrayBuffer);
|
|
1283
|
+
|
|
1284
|
+
const attachmentVal = this.elements?.selectPasskeyAttachmentEl?.value;
|
|
1285
|
+
const authenticatorAttachment: AuthenticatorAttachment | undefined =
|
|
1286
|
+
(attachmentVal === 'cross-platform' || attachmentVal === 'platform')
|
|
1287
|
+
? attachmentVal
|
|
1288
|
+
: undefined;
|
|
1289
|
+
|
|
1290
|
+
const regResult = await registerPasskeyCredential({
|
|
1291
|
+
username,
|
|
1292
|
+
displayName: username,
|
|
1293
|
+
salt: saltHex,
|
|
1294
|
+
userVerification: 'preferred',
|
|
1295
|
+
authenticatorAttachment,
|
|
1296
|
+
});
|
|
1297
|
+
const { credentialId, transports } = regResult;
|
|
1298
|
+
|
|
1299
|
+
let passkeySecret = regResult.passkeySecret;
|
|
1300
|
+
if (!passkeySecret) {
|
|
1301
|
+
const prfResult = await evaluatePasskeyPrf({
|
|
1302
|
+
credentialId,
|
|
1303
|
+
salt: saltHex,
|
|
1304
|
+
transports,
|
|
1305
|
+
userVerification: 'preferred'
|
|
1306
|
+
});
|
|
1307
|
+
passkeySecret = prfResult.passkeySecret;
|
|
1308
|
+
}
|
|
1309
|
+
|
|
1310
|
+
const defaultDeviceTag = generateDefaultDeviceTag();
|
|
1311
|
+
const passkeyName = defaultDeviceTag;
|
|
1312
|
+
const passkeyTag = getSaferSubstring({ text: passkeyName, length: 12, keepLiterals: ['-'] }) || 'key';
|
|
1313
|
+
const poolId = `passkey-manage-${passkeyTag}`;
|
|
1314
|
+
|
|
1315
|
+
const passkeyEntry: KeystonePasskeyEntry = {
|
|
1316
|
+
credentialId,
|
|
1317
|
+
poolId,
|
|
1318
|
+
name: passkeyName,
|
|
1319
|
+
salt: saltHex,
|
|
1320
|
+
createdUTC: new Date().toISOString(),
|
|
1321
|
+
...(transports && transports.length > 0 ? { transports } : {})
|
|
1322
|
+
};
|
|
1323
|
+
|
|
1324
|
+
const metaspace = await getGlobalMetaspace_waitIfNeeded();
|
|
1325
|
+
if (!metaspace) throw new Error("Metaspace not found");
|
|
1326
|
+
const space = await metaspace.getLocalUserSpace({ lock: false });
|
|
1327
|
+
if (!space) throw new Error("Local space not found");
|
|
1328
|
+
|
|
1329
|
+
const keystoneService = new KeystoneService_V1();
|
|
1330
|
+
let evolvedKeystone: KeystoneIbGib_V1;
|
|
1331
|
+
|
|
1332
|
+
const poolConfig: KeystonePoolConfig_HashV1 = {
|
|
1333
|
+
id: poolId,
|
|
1334
|
+
type: KeystoneChallengeType.hash_reveal_v1,
|
|
1335
|
+
salt: poolId,
|
|
1336
|
+
algo: HashAlgorithm.sha_256,
|
|
1337
|
+
rounds: 1,
|
|
1338
|
+
allowedVerbs: [KEYSTONE_VERB_MANAGE, KEYSTONE_VERB_SYNC, KEYSTONE_VERB_CONNECT],
|
|
1339
|
+
behavior: {
|
|
1340
|
+
size: 10,
|
|
1341
|
+
replenish: KeystoneReplenishStrategy.topUp,
|
|
1342
|
+
selectSequentially: 1,
|
|
1343
|
+
selectRandomly: 0,
|
|
1344
|
+
targetBindingCount: 0,
|
|
1345
|
+
}
|
|
1346
|
+
};
|
|
1347
|
+
|
|
1348
|
+
const challenges = await generatePoolChallenges({
|
|
1349
|
+
config: poolConfig,
|
|
1350
|
+
masterSecret: passkeySecret
|
|
1351
|
+
});
|
|
1352
|
+
|
|
1353
|
+
const newPool: KeystoneChallengePool = {
|
|
1354
|
+
id: poolId,
|
|
1355
|
+
config: poolConfig,
|
|
1356
|
+
challenges
|
|
1357
|
+
};
|
|
1358
|
+
|
|
1359
|
+
let authSecret: string | undefined;
|
|
1360
|
+
let signingPoolId: string | undefined;
|
|
1361
|
+
|
|
1362
|
+
if (isCustodialOnly) {
|
|
1363
|
+
// Elevate custodial account with sovereign passkey
|
|
1364
|
+
this.showBusy({
|
|
1365
|
+
isBusy: true,
|
|
1366
|
+
title: 'Elevating Account Ownership...',
|
|
1367
|
+
msg: 'Establishing sovereign passkey pool on identity...',
|
|
1368
|
+
animationEmoji: '⚡'
|
|
1369
|
+
});
|
|
1370
|
+
|
|
1371
|
+
authSecret = passkeySecret;
|
|
1372
|
+
signingPoolId = poolId;
|
|
1373
|
+
|
|
1374
|
+
evolvedKeystone = await keystoneService.addPools({
|
|
1375
|
+
latestKeystone: this.ibGib,
|
|
1376
|
+
masterSecret: authSecret,
|
|
1377
|
+
signingPoolId,
|
|
1378
|
+
newPools: [newPool],
|
|
1379
|
+
frameDetails: {
|
|
1380
|
+
passkeys: {
|
|
1381
|
+
[credentialId]: passkeyEntry
|
|
1382
|
+
}
|
|
1383
|
+
},
|
|
1384
|
+
dataToPatch: {
|
|
1385
|
+
credentialIds: [...(this.ibGib.data?.credentialIds || []), credentialId]
|
|
1386
|
+
},
|
|
1387
|
+
metaspace,
|
|
1388
|
+
space
|
|
1389
|
+
});
|
|
1390
|
+
} else {
|
|
1391
|
+
// Secondary passkey registration on existing sovereign identity:
|
|
1392
|
+
// Prompt user to authorize with current master password or passkey
|
|
1393
|
+
try {
|
|
1394
|
+
const authResult = await promptForAuthoritySecret({
|
|
1395
|
+
keystone: this.ibGib,
|
|
1396
|
+
aggregatedDetails: this.aggregatedDetails,
|
|
1397
|
+
title: 'Authorize New Passkey',
|
|
1398
|
+
msg: 'Enter Master Password or touch security key to authorize adding this passkey:',
|
|
1399
|
+
verb: KEYSTONE_VERB_MANAGE
|
|
1400
|
+
});
|
|
1401
|
+
authSecret = authResult.secret;
|
|
1402
|
+
signingPoolId = authResult.poolId;
|
|
1403
|
+
} catch (err) {
|
|
1404
|
+
this.setPasskeyStatus("Registration cancelled (authorization required).", "error");
|
|
1405
|
+
return;
|
|
1406
|
+
}
|
|
1407
|
+
|
|
1408
|
+
this.showBusy({
|
|
1409
|
+
isBusy: true,
|
|
1410
|
+
title: 'Registering Passkey...',
|
|
1411
|
+
msg: 'Adding cryptographic challenge pool and evolving identity...',
|
|
1412
|
+
animationEmoji: '🛡️'
|
|
1413
|
+
});
|
|
1414
|
+
|
|
1415
|
+
evolvedKeystone = await keystoneService.addPools({
|
|
1416
|
+
latestKeystone: this.ibGib,
|
|
1417
|
+
masterSecret: authSecret,
|
|
1418
|
+
signingPoolId,
|
|
1419
|
+
newPools: [newPool],
|
|
1420
|
+
frameDetails: {
|
|
1421
|
+
passkeys: {
|
|
1422
|
+
[credentialId]: passkeyEntry
|
|
1423
|
+
}
|
|
1424
|
+
},
|
|
1425
|
+
dataToPatch: {
|
|
1426
|
+
credentialIds: [...(this.ibGib.data?.credentialIds || []), credentialId]
|
|
1427
|
+
},
|
|
1428
|
+
metaspace,
|
|
1429
|
+
space
|
|
1430
|
+
});
|
|
1431
|
+
|
|
1432
|
+
// Sync pool addition frame to server
|
|
1433
|
+
const primaryTjpAddr = getTjpAddr({ ibGib: evolvedKeystone, defaultIfNone: 'incomingAddr' })!;
|
|
1434
|
+
const resSyncPool = await getApiBridge().putEvolveKeystone({
|
|
1435
|
+
domainAddr: primaryTjpAddr,
|
|
1436
|
+
keystoneIbGib: evolvedKeystone,
|
|
1437
|
+
relatedKeystoneIbGibs: []
|
|
1438
|
+
});
|
|
1439
|
+
if (!resSyncPool.success) {
|
|
1440
|
+
throw new Error(`Server synchronization of passkey pool failed: ${resSyncPool.message}`);
|
|
1441
|
+
}
|
|
1442
|
+
}
|
|
1443
|
+
|
|
1444
|
+
// Construct outward device sync delegate for this passkey
|
|
1445
|
+
this.showBusy({
|
|
1446
|
+
isBusy: true,
|
|
1447
|
+
title: 'Creating Sync Delegate...',
|
|
1448
|
+
msg: 'Constructing outward passkey device sync delegate...',
|
|
1449
|
+
animationEmoji: '📱'
|
|
1450
|
+
});
|
|
1451
|
+
|
|
1452
|
+
const primaryTjpAddr = getTjpAddr({ ibGib: evolvedKeystone, defaultIfNone: 'incomingAddr' })!;
|
|
1453
|
+
const { delegateKeystone, deviceTag } = await createPasskeyDeviceSyncDelegate({
|
|
1454
|
+
username,
|
|
1455
|
+
primaryTjpAddr,
|
|
1456
|
+
passkeySecret,
|
|
1457
|
+
transports,
|
|
1458
|
+
metaspace,
|
|
1459
|
+
space
|
|
1460
|
+
});
|
|
1461
|
+
|
|
1462
|
+
// Register outward sync delegate on primary keystone
|
|
1463
|
+
this.showBusy({
|
|
1464
|
+
isBusy: true,
|
|
1465
|
+
title: 'Registering Delegate...',
|
|
1466
|
+
msg: 'Registering outward passkey delegate on primary identity...',
|
|
1467
|
+
animationEmoji: '📝'
|
|
1468
|
+
});
|
|
1469
|
+
|
|
1470
|
+
const delegateSigningPoolId = isCustodialOnly ? poolId : signingPoolId!;
|
|
1471
|
+
const delegateAuthSecret = isCustodialOnly ? passkeySecret : authSecret!;
|
|
1472
|
+
|
|
1473
|
+
evolvedKeystone = await keystoneService.registerDelegate({
|
|
1474
|
+
parentKeystone: evolvedKeystone,
|
|
1475
|
+
delegateKeystone,
|
|
1476
|
+
masterSecret: delegateAuthSecret,
|
|
1477
|
+
signingPoolId: delegateSigningPoolId,
|
|
1478
|
+
metaspace,
|
|
1479
|
+
space,
|
|
1480
|
+
allowedVerbs: [KEYSTONE_VERB_SYNC, KEYSTONE_VERB_CONNECT],
|
|
1481
|
+
deviceName: deviceTag
|
|
1482
|
+
});
|
|
1483
|
+
|
|
1484
|
+
// Sync evolved keystone and delegate to server
|
|
1485
|
+
this.showBusy({
|
|
1486
|
+
isBusy: true,
|
|
1487
|
+
title: 'Syncing to Server...',
|
|
1488
|
+
msg: 'Publishing evolved identity keystone and delegate to server...',
|
|
1489
|
+
animationEmoji: '⚡'
|
|
1490
|
+
});
|
|
1491
|
+
|
|
1492
|
+
const resSync = await getApiBridge().putEvolveKeystone({
|
|
1493
|
+
domainAddr: primaryTjpAddr,
|
|
1494
|
+
keystoneIbGib: evolvedKeystone,
|
|
1495
|
+
relatedKeystoneIbGibs: [delegateKeystone]
|
|
1496
|
+
});
|
|
1497
|
+
|
|
1498
|
+
if (!resSync.success) {
|
|
1499
|
+
throw new Error(`Server synchronization of delegate failed: ${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 }), toDto({ ibGib: delegateKeystone })]
|
|
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 signingPoolId: string | undefined;
|
|
1539
|
+
|
|
1540
|
+
try {
|
|
1541
|
+
const authResult = await promptForAuthoritySecret({
|
|
1542
|
+
keystone: this.ibGib,
|
|
1543
|
+
aggregatedDetails: this.aggregatedDetails,
|
|
1544
|
+
title: 'Authorize Removal',
|
|
1545
|
+
msg: `Are you sure you want to remove "${passkeyName}"? Enter your Master Secret (Password) or touch security key to authorize:`,
|
|
1546
|
+
verb: KEYSTONE_VERB_MANAGE
|
|
1547
|
+
});
|
|
1548
|
+
masterSecret = authResult.secret;
|
|
1549
|
+
signingPoolId = authResult.poolId;
|
|
1550
|
+
} catch (err) {
|
|
1551
|
+
this.setPasskeyStatus("Removal cancelled or unauthorized.", "error");
|
|
1552
|
+
return;
|
|
1553
|
+
}
|
|
1554
|
+
|
|
1555
|
+
this.showBusy({
|
|
1556
|
+
isBusy: true,
|
|
1557
|
+
title: 'Revoking Passkey...',
|
|
1558
|
+
msg: 'Tombstoning passkey record and removing challenge pool...',
|
|
1559
|
+
animationEmoji: '🗑️'
|
|
1560
|
+
});
|
|
1561
|
+
|
|
1562
|
+
const metaspace = await getGlobalMetaspace_waitIfNeeded();
|
|
1563
|
+
if (!metaspace) throw new Error("Metaspace not found");
|
|
1564
|
+
const space = await metaspace.getLocalUserSpace({ lock: false });
|
|
1565
|
+
if (!space) throw new Error("Local space not found");
|
|
1566
|
+
|
|
1567
|
+
const poolIdToRemove = passkeyEntry?.poolId || `passkey-manage-${credentialId.substring(0, 8)}`;
|
|
1568
|
+
const poolExists = this.ibGib.data?.challengePools.some(p => p.id === poolIdToRemove);
|
|
1569
|
+
|
|
1570
|
+
const filteredCredentialIds = (this.ibGib.data?.credentialIds || []).filter(id => id !== credentialId);
|
|
1571
|
+
|
|
1572
|
+
let evolvedKeystone: KeystoneIbGib_V1;
|
|
1573
|
+
if (poolExists) {
|
|
1574
|
+
evolvedKeystone = await keystoneService.removePools({
|
|
1575
|
+
latestKeystone: this.ibGib,
|
|
1576
|
+
masterSecret: masterSecret!,
|
|
1577
|
+
signingPoolId: signingPoolId!,
|
|
1578
|
+
poolIds: [poolIdToRemove],
|
|
1579
|
+
frameDetails: {
|
|
1580
|
+
passkeys: {
|
|
1581
|
+
[credentialId]: null
|
|
1582
|
+
}
|
|
1583
|
+
},
|
|
1584
|
+
dataToPatch: {
|
|
1585
|
+
credentialIds: filteredCredentialIds
|
|
1586
|
+
},
|
|
1587
|
+
metaspace,
|
|
1588
|
+
space
|
|
1589
|
+
});
|
|
1590
|
+
} else {
|
|
1591
|
+
// If pool doesn't exist, just update frameDetails
|
|
1592
|
+
evolvedKeystone = await keystoneService.sign({
|
|
1593
|
+
latestKeystone: this.ibGib,
|
|
1594
|
+
masterSecret: masterSecret!,
|
|
1595
|
+
poolId: signingPoolId,
|
|
1596
|
+
claim: {
|
|
1597
|
+
target: getIbGibAddr({ ibGib: this.ibGib })
|
|
1598
|
+
},
|
|
1599
|
+
frameDetails: {
|
|
1600
|
+
passkeys: {
|
|
1601
|
+
[credentialId]: null
|
|
1602
|
+
}
|
|
1603
|
+
},
|
|
1604
|
+
dataToPatch: {
|
|
1605
|
+
credentialIds: filteredCredentialIds
|
|
1606
|
+
},
|
|
1607
|
+
metaspace,
|
|
1608
|
+
space
|
|
1609
|
+
});
|
|
1610
|
+
}
|
|
1611
|
+
|
|
1612
|
+
// Sync to server
|
|
1613
|
+
this.showBusy({
|
|
1614
|
+
isBusy: true,
|
|
1615
|
+
title: 'Syncing to Server...',
|
|
1616
|
+
msg: 'Publishing updated keystone to server...',
|
|
1617
|
+
animationEmoji: '⚡'
|
|
1618
|
+
});
|
|
1619
|
+
|
|
1620
|
+
const primaryTjpAddr = getTjpAddr({ ibGib: evolvedKeystone, defaultIfNone: 'incomingAddr' })!;
|
|
1621
|
+
await getApiBridge().putEvolveKeystone({
|
|
1622
|
+
domainAddr: primaryTjpAddr,
|
|
1623
|
+
keystoneIbGib: evolvedKeystone,
|
|
1624
|
+
relatedKeystoneIbGibs: []
|
|
1625
|
+
});
|
|
1626
|
+
|
|
1627
|
+
// Update Special Index
|
|
1628
|
+
await updateSpecialIndex({
|
|
1629
|
+
type: "keystones",
|
|
1630
|
+
rel8nInfos: [
|
|
1631
|
+
{
|
|
1632
|
+
rel8nName: "keystone",
|
|
1633
|
+
ibGibs: [toDto({ ibGib: evolvedKeystone })]
|
|
1634
|
+
}
|
|
1635
|
+
],
|
|
1636
|
+
metaspace,
|
|
1637
|
+
space
|
|
1638
|
+
});
|
|
1639
|
+
|
|
1640
|
+
this.setPasskeyStatus(`Passkey "${passkeyName}" successfully removed.`, "success");
|
|
1641
|
+
this.ibGibAddr = getIbGibAddr({ ibGib: evolvedKeystone });
|
|
1642
|
+
this.staticIbGib = evolvedKeystone;
|
|
1643
|
+
await this.loadIbGib({ getLatest: false });
|
|
1644
|
+
|
|
1645
|
+
} catch (error) {
|
|
1646
|
+
console.error(`${lc} ${extractErrorMsg(error)}`, error);
|
|
1647
|
+
this.setPasskeyStatus(`Failed to remove passkey: ${extractErrorMsg(error)}`, "error");
|
|
1648
|
+
} finally {
|
|
1649
|
+
this.showBusy({ isBusy: false });
|
|
1650
|
+
}
|
|
1651
|
+
}
|
|
1652
|
+
|
|
1088
1653
|
override async handleContextUpdated(): Promise<void> {
|
|
1089
1654
|
const lc = `${this.lc}[${this.handleContextUpdated.name}]`;
|
|
1090
1655
|
try {
|
|
@@ -1124,10 +1689,26 @@ export class KeystoneDetailsComponentInstance
|
|
|
1124
1689
|
const lc = `${this.lc}[${this.handleSSOLink.name}]`;
|
|
1125
1690
|
this.setSSOStatus(`Linking ${providerId}...`, "info");
|
|
1126
1691
|
|
|
1692
|
+
const providerName = providerId === 'google' ? 'Google' : providerId === 'github' ? 'GitHub' : providerId;
|
|
1693
|
+
this.setSSOStatus(`Opening ${providerName} SSO authorization window...`, "info");
|
|
1694
|
+
this.showBusy({
|
|
1695
|
+
isBusy: true,
|
|
1696
|
+
title: `Connecting to ${providerName}...`,
|
|
1697
|
+
msg: `Opening ${providerName} SSO authorization window...\nPlease complete authorization in the popup window.`,
|
|
1698
|
+
animationEmoji: '🌐'
|
|
1699
|
+
});
|
|
1700
|
+
|
|
1127
1701
|
try {
|
|
1702
|
+
const primaryTjpAddr = (this.ibGib ? getTjpAddr({ ibGib: this.ibGib }) : undefined)
|
|
1703
|
+
|| (this.ibGibAddr ? (() => {
|
|
1704
|
+
const { ib } = getIbAndGib({ ibGibAddr: this.ibGibAddr });
|
|
1705
|
+
const { tjpGib } = getGibInfo({ ibGibAddr: this.ibGibAddr });
|
|
1706
|
+
return tjpGib && ib ? `${ib}^${tjpGib}` : this.ibGibAddr;
|
|
1707
|
+
})() : undefined);
|
|
1708
|
+
|
|
1128
1709
|
const resPopup = await executeSsoOAuthPopup({
|
|
1129
1710
|
providerId,
|
|
1130
|
-
parentTjpAddr:
|
|
1711
|
+
parentTjpAddr: primaryTjpAddr
|
|
1131
1712
|
});
|
|
1132
1713
|
|
|
1133
1714
|
if (!resPopup.success || !resPopup.code) {
|
|
@@ -1139,6 +1720,8 @@ export class KeystoneDetailsComponentInstance
|
|
|
1139
1720
|
} catch (error) {
|
|
1140
1721
|
console.error(`${lc} SSO link initiation failed: ${extractErrorMsg(error)}`);
|
|
1141
1722
|
this.setSSOStatus(`Link failed: ${extractErrorMsg(error)}`, "error");
|
|
1723
|
+
} finally {
|
|
1724
|
+
this.showBusy({ isBusy: false });
|
|
1142
1725
|
}
|
|
1143
1726
|
}
|
|
1144
1727
|
|
|
@@ -1151,7 +1734,14 @@ export class KeystoneDetailsComponentInstance
|
|
|
1151
1734
|
|
|
1152
1735
|
protected async handleSSOLinkCallback(code: string, stateObj: any, redirectUri: string) {
|
|
1153
1736
|
const lc = `${this.lc}[${this.handleSSOLinkCallback.name}]`;
|
|
1154
|
-
|
|
1737
|
+
const providerName = stateObj.providerId === 'google' ? 'Google' : stateObj.providerId === 'github' ? 'GitHub' : stateObj.providerId;
|
|
1738
|
+
this.setSSOStatus(`Retrieving challenge pool configuration from server...`, "info");
|
|
1739
|
+
this.showBusy({
|
|
1740
|
+
isBusy: true,
|
|
1741
|
+
title: `Linking ${providerName}...`,
|
|
1742
|
+
msg: `Validating ${providerName} authorization with server...`,
|
|
1743
|
+
animationEmoji: '🔐'
|
|
1744
|
+
});
|
|
1155
1745
|
|
|
1156
1746
|
try {
|
|
1157
1747
|
// 1. Exchange OAuth code for custodian public pool config
|
|
@@ -1178,50 +1768,32 @@ export class KeystoneDetailsComponentInstance
|
|
|
1178
1768
|
|
|
1179
1769
|
const custodianPool = linkData.custodianPool;
|
|
1180
1770
|
|
|
1181
|
-
// 2. Prompt user for
|
|
1771
|
+
// 2. Prompt user for authorization secret
|
|
1182
1772
|
const keystoneService = new KeystoneService_V1();
|
|
1183
1773
|
let masterSecret: string | undefined;
|
|
1184
|
-
let
|
|
1185
|
-
let tries = 0;
|
|
1186
|
-
const maxTries = 8;
|
|
1187
|
-
|
|
1188
|
-
while (tries < maxTries && !isCorrect) {
|
|
1189
|
-
try {
|
|
1190
|
-
const promptMsg = tries === 0
|
|
1191
|
-
? "Link successful! Enter your local Master Secret (Password) to sign and register the custodian pool"
|
|
1192
|
-
: `Incorrect password. Please try again (${maxTries - tries} tries remaining):`;
|
|
1193
|
-
masterSecret = await promptForSecret({
|
|
1194
|
-
msg: promptMsg,
|
|
1195
|
-
confirm: false
|
|
1196
|
-
});
|
|
1197
|
-
} catch (error) {
|
|
1198
|
-
// cancelled
|
|
1199
|
-
if (logalot) { console.log(`${lc}[promptForSecret] user cancelled (I: 911ff1fb0234949228481968e9087626)`); }
|
|
1200
|
-
masterSecret = undefined;
|
|
1201
|
-
break;
|
|
1202
|
-
}
|
|
1203
|
-
|
|
1204
|
-
if (!masterSecret) {
|
|
1205
|
-
break;
|
|
1206
|
-
}
|
|
1774
|
+
let signingPoolId: string | undefined;
|
|
1207
1775
|
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1776
|
+
try {
|
|
1777
|
+
const authResult = await promptForAuthoritySecret({
|
|
1778
|
+
keystone: this.ibGib,
|
|
1779
|
+
aggregatedDetails: this.aggregatedDetails,
|
|
1780
|
+
title: `Link ${providerName}`,
|
|
1781
|
+
msg: `Enter Master Password or touch security key to authorize linking ${providerName}:`,
|
|
1782
|
+
verb: KEYSTONE_VERB_MANAGE
|
|
1213
1783
|
});
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
}
|
|
1219
|
-
|
|
1220
|
-
if (!isCorrect) {
|
|
1221
|
-
this.setSSOStatus("Link cancelled or failed (incorrect password).", "error");
|
|
1784
|
+
masterSecret = authResult.secret;
|
|
1785
|
+
signingPoolId = authResult.poolId;
|
|
1786
|
+
} catch (error) {
|
|
1787
|
+
this.setSSOStatus("Link cancelled or failed.", "error");
|
|
1222
1788
|
return;
|
|
1223
1789
|
}
|
|
1224
1790
|
|
|
1791
|
+
this.showBusy({
|
|
1792
|
+
isBusy: true,
|
|
1793
|
+
title: 'Evolving Identity...',
|
|
1794
|
+
msg: `Installing ${providerName} custodian pool and signing evolution...`,
|
|
1795
|
+
animationEmoji: '📝'
|
|
1796
|
+
});
|
|
1225
1797
|
this.setSSOStatus("Evolving identity locally and generating cryptographic signatures...", "info");
|
|
1226
1798
|
|
|
1227
1799
|
const metaspace = await getGlobalMetaspace_waitIfNeeded();
|
|
@@ -1229,33 +1801,74 @@ export class KeystoneDetailsComponentInstance
|
|
|
1229
1801
|
const space = await metaspace.getLocalUserSpace({ lock: false });
|
|
1230
1802
|
if (!space) { throw new Error("Local space not found"); }
|
|
1231
1803
|
|
|
1232
|
-
// 3. Evolve parent keystone to include the custodian pool
|
|
1804
|
+
// 3. Evolve parent keystone to include the custodian pool (n -> n+1)
|
|
1233
1805
|
const evolvedParent = await keystoneService.addPools({
|
|
1234
1806
|
latestKeystone: this.ibGib!,
|
|
1235
1807
|
masterSecret: masterSecret!,
|
|
1808
|
+
signingPoolId: signingPoolId!,
|
|
1236
1809
|
newPools: [custodianPool],
|
|
1237
1810
|
metaspace,
|
|
1238
1811
|
space
|
|
1239
1812
|
});
|
|
1240
1813
|
|
|
1241
|
-
//
|
|
1242
|
-
|
|
1243
|
-
const syncRes = await getApiBridge().putEvolveKeystone({
|
|
1814
|
+
// Sync evolved parent (n+1) to server first
|
|
1815
|
+
const syncRes1 = await getApiBridge().putEvolveKeystone({
|
|
1244
1816
|
domainAddr: stateObj.parentTjpAddr,
|
|
1245
1817
|
keystoneIbGib: evolvedParent,
|
|
1246
|
-
relatedKeystoneIbGibs: []
|
|
1818
|
+
relatedKeystoneIbGibs: []
|
|
1247
1819
|
});
|
|
1248
|
-
if (!
|
|
1249
|
-
throw new Error(`Server synchronization rejected: ${
|
|
1820
|
+
if (!syncRes1.success) {
|
|
1821
|
+
throw new Error(`Server synchronization of linked pool rejected: ${syncRes1.message}`);
|
|
1250
1822
|
}
|
|
1251
1823
|
|
|
1252
|
-
//
|
|
1824
|
+
// 4. Create and register OAuth2 sync delegate for this provider (n+1 -> n+2)
|
|
1825
|
+
const username = this.aggregatedDetails?.username || this.ibGib?.data?.username || 'user';
|
|
1826
|
+
const { delegateKeystone, deviceTag } = await createOAuth2DeviceSyncDelegate({
|
|
1827
|
+
providerId: stateObj.providerId,
|
|
1828
|
+
username,
|
|
1829
|
+
primaryTjpAddr: stateObj.parentTjpAddr,
|
|
1830
|
+
custodianReqs: linkData.custodianReqs,
|
|
1831
|
+
metaspace,
|
|
1832
|
+
space,
|
|
1833
|
+
});
|
|
1834
|
+
|
|
1835
|
+
const parentWithDelegate = await keystoneService.registerDelegate({
|
|
1836
|
+
parentKeystone: evolvedParent,
|
|
1837
|
+
delegateKeystone,
|
|
1838
|
+
masterSecret: masterSecret!,
|
|
1839
|
+
signingPoolId: signingPoolId!,
|
|
1840
|
+
metaspace,
|
|
1841
|
+
space,
|
|
1842
|
+
allowedVerbs: [KEYSTONE_VERB_SYNC, KEYSTONE_VERB_CONNECT],
|
|
1843
|
+
deviceName: deviceTag,
|
|
1844
|
+
});
|
|
1845
|
+
|
|
1846
|
+
await metaspace.put({ ibGibs: [delegateKeystone, parentWithDelegate], space });
|
|
1847
|
+
|
|
1848
|
+
// 5. Sync parent with registered delegate (n+2) to server
|
|
1849
|
+
this.showBusy({
|
|
1850
|
+
isBusy: true,
|
|
1851
|
+
title: 'Syncing to Server...',
|
|
1852
|
+
msg: 'Publishing linked identity and delegate to server...',
|
|
1853
|
+
animationEmoji: '⚡'
|
|
1854
|
+
});
|
|
1855
|
+
this.setSSOStatus("Publishing evolved identity keystone to server...", "info");
|
|
1856
|
+
const syncRes2 = await getApiBridge().putEvolveKeystone({
|
|
1857
|
+
domainAddr: stateObj.parentTjpAddr,
|
|
1858
|
+
keystoneIbGib: parentWithDelegate,
|
|
1859
|
+
relatedKeystoneIbGibs: [delegateKeystone]
|
|
1860
|
+
});
|
|
1861
|
+
if (!syncRes2.success) {
|
|
1862
|
+
throw new Error(`Server synchronization of delegate rejected: ${syncRes2.message}`);
|
|
1863
|
+
}
|
|
1864
|
+
|
|
1865
|
+
// 6. Update index locally
|
|
1253
1866
|
await updateSpecialIndex({
|
|
1254
1867
|
type: "keystones",
|
|
1255
1868
|
rel8nInfos: [
|
|
1256
1869
|
{
|
|
1257
1870
|
rel8nName: "keystone",
|
|
1258
|
-
ibGibs: [toDto({ ibGib:
|
|
1871
|
+
ibGibs: [toDto({ ibGib: parentWithDelegate }), toDto({ ibGib: delegateKeystone })]
|
|
1259
1872
|
}
|
|
1260
1873
|
],
|
|
1261
1874
|
metaspace,
|
|
@@ -1263,18 +1876,24 @@ export class KeystoneDetailsComponentInstance
|
|
|
1263
1876
|
});
|
|
1264
1877
|
|
|
1265
1878
|
this.setSSOStatus(`Successfully linked with ${stateObj.providerId}!`, "success");
|
|
1879
|
+
this.ibGibAddr = getIbGibAddr({ ibGib: parentWithDelegate });
|
|
1880
|
+
this.staticIbGib = parentWithDelegate;
|
|
1266
1881
|
await this.loadIbGib({ getLatest: false });
|
|
1267
1882
|
|
|
1268
1883
|
} catch (error) {
|
|
1269
1884
|
console.error(`${lc} SSO callback processing failed: ${extractErrorMsg(error)}`);
|
|
1270
1885
|
this.setSSOStatus(`Link failed: ${extractErrorMsg(error)}`, "error");
|
|
1886
|
+
} finally {
|
|
1887
|
+
this.showBusy({ isBusy: false });
|
|
1271
1888
|
}
|
|
1272
1889
|
}
|
|
1273
1890
|
|
|
1274
1891
|
protected async handleSSOClick(providerId: 'google' | 'github') {
|
|
1275
|
-
const isLinked =
|
|
1892
|
+
const isLinked = !!this.ibGib ?
|
|
1893
|
+
isSsoProviderLinked({ keystone: this.ibGib, aggregatedDetails: this.aggregatedDetails, providerId }) :
|
|
1894
|
+
false;
|
|
1276
1895
|
if (isLinked) {
|
|
1277
|
-
const caps = getKeystoneCapabilities({ keystone: this.ibGib
|
|
1896
|
+
const caps = getKeystoneCapabilities({ keystone: this.ibGib!, aggregatedDetails: this.aggregatedDetails });
|
|
1278
1897
|
if (!caps.isSovereign) {
|
|
1279
1898
|
this.setSSOStatus(`Cannot unlink ${providerId}: This is your only identity recovery and management method. Please add a Master Password first in the Password card below to enable sovereign ownership before unlinking.`, "error");
|
|
1280
1899
|
return;
|
|
@@ -1287,51 +1906,36 @@ export class KeystoneDetailsComponentInstance
|
|
|
1287
1906
|
|
|
1288
1907
|
protected async handleSSOUnlink(providerId: 'google' | 'github') {
|
|
1289
1908
|
const lc = `${this.lc}[${this.handleSSOUnlink.name}]`;
|
|
1290
|
-
|
|
1909
|
+
const providerName = providerId === 'google' ? 'Google' : providerId === 'github' ? 'GitHub' : providerId;
|
|
1910
|
+
this.setSSOStatus(`Unlinking ${providerName}...`, "info");
|
|
1291
1911
|
|
|
1292
1912
|
try {
|
|
1293
|
-
// 1. Prompt user for
|
|
1913
|
+
// 1. Prompt user for authorization secret
|
|
1294
1914
|
const keystoneService = new KeystoneService_V1();
|
|
1295
1915
|
let masterSecret: string | undefined;
|
|
1296
|
-
let
|
|
1297
|
-
let tries = 0;
|
|
1298
|
-
const maxTries = 8;
|
|
1916
|
+
let signingPoolId: string | undefined;
|
|
1299
1917
|
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
confirm: false
|
|
1308
|
-
});
|
|
1309
|
-
} catch (error) {
|
|
1310
|
-
masterSecret = undefined;
|
|
1311
|
-
break;
|
|
1312
|
-
}
|
|
1313
|
-
|
|
1314
|
-
if (!masterSecret) {
|
|
1315
|
-
break;
|
|
1316
|
-
}
|
|
1317
|
-
|
|
1318
|
-
// Verify secret using verifySigningSecret (requires manage poolId)
|
|
1319
|
-
isCorrect = await keystoneService.verifySigningSecret({
|
|
1320
|
-
keystoneIbGib: this.ibGib!,
|
|
1321
|
-
signingSecret: masterSecret,
|
|
1322
|
-
poolId: 'manage'
|
|
1918
|
+
try {
|
|
1919
|
+
const authResult = await promptForAuthoritySecret({
|
|
1920
|
+
keystone: this.ibGib,
|
|
1921
|
+
aggregatedDetails: this.aggregatedDetails,
|
|
1922
|
+
title: `Unlink ${providerName}`,
|
|
1923
|
+
msg: `Are you sure you want to unlink your ${providerName} account? If so, enter your Master Password or touch security key to authorize.`,
|
|
1924
|
+
verb: KEYSTONE_VERB_MANAGE
|
|
1323
1925
|
});
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
}
|
|
1329
|
-
|
|
1330
|
-
if (!isCorrect) {
|
|
1331
|
-
this.setSSOStatus("Unlink cancelled or failed (incorrect password).", "error");
|
|
1926
|
+
masterSecret = authResult.secret;
|
|
1927
|
+
signingPoolId = authResult.poolId;
|
|
1928
|
+
} catch (error) {
|
|
1929
|
+
this.setSSOStatus("Unlink cancelled or failed.", "error");
|
|
1332
1930
|
return;
|
|
1333
1931
|
}
|
|
1334
1932
|
|
|
1933
|
+
this.showBusy({
|
|
1934
|
+
isBusy: true,
|
|
1935
|
+
title: 'Unlinking Provider...',
|
|
1936
|
+
msg: `Removing ${providerName} custodian pool and delegate keys...`,
|
|
1937
|
+
animationEmoji: '🔓'
|
|
1938
|
+
});
|
|
1335
1939
|
this.setSSOStatus("Evolving identity locally and removing custodian pool...", "info");
|
|
1336
1940
|
|
|
1337
1941
|
const metaspace = await getGlobalMetaspace_waitIfNeeded();
|
|
@@ -1344,38 +1948,69 @@ export class KeystoneDetailsComponentInstance
|
|
|
1344
1948
|
let evolvedParent = await keystoneService.removePools({
|
|
1345
1949
|
latestKeystone: this.ibGib!,
|
|
1346
1950
|
masterSecret: masterSecret!,
|
|
1951
|
+
signingPoolId: signingPoolId!,
|
|
1347
1952
|
poolIds: [targetPoolId],
|
|
1348
1953
|
metaspace,
|
|
1349
1954
|
space
|
|
1350
1955
|
});
|
|
1351
1956
|
|
|
1352
|
-
|
|
1353
|
-
|
|
1957
|
+
const primaryTjpAddr = (this.ibGib ? getTjpAddr({ ibGib: this.ibGib }) : undefined)
|
|
1958
|
+
|| (this.ibGibAddr ? (() => {
|
|
1959
|
+
const { ib } = getIbAndGib({ ibGibAddr: this.ibGibAddr });
|
|
1960
|
+
const { tjpGib } = getGibInfo({ ibGibAddr: this.ibGibAddr });
|
|
1961
|
+
return tjpGib && ib ? `${ib}^${tjpGib}` : this.ibGibAddr;
|
|
1962
|
+
})() : undefined)
|
|
1963
|
+
|| getIbGibAddr({ ibGib: this.ibGib! });
|
|
1964
|
+
|
|
1965
|
+
// 3. Sync pool removal frame to server
|
|
1966
|
+
this.showBusy({
|
|
1967
|
+
isBusy: true,
|
|
1968
|
+
title: 'Syncing to Server...',
|
|
1969
|
+
msg: `Publishing ${providerName} pool removal to server...`,
|
|
1970
|
+
animationEmoji: '⚡'
|
|
1971
|
+
});
|
|
1972
|
+
this.setSSOStatus("Publishing evolved identity keystone to server...", "info");
|
|
1973
|
+
let syncRes = await getApiBridge().putEvolveKeystone({
|
|
1974
|
+
domainAddr: primaryTjpAddr,
|
|
1975
|
+
keystoneIbGib: evolvedParent,
|
|
1976
|
+
relatedKeystoneIbGibs: [] // No outward delegate timelines
|
|
1977
|
+
});
|
|
1978
|
+
if (!syncRes.success) {
|
|
1979
|
+
throw new Error(`Server synchronization rejected: ${syncRes.message}`);
|
|
1980
|
+
}
|
|
1981
|
+
|
|
1982
|
+
// 4. Unregister any corresponding outward delegate for this provider if present
|
|
1983
|
+
const delegatesMap = evolvedParent.data?.delegates || this.ibGib?.data?.delegates;
|
|
1354
1984
|
if (delegatesMap) {
|
|
1355
1985
|
for (const [delegateTjpAddr, info] of Object.entries(delegatesMap)) {
|
|
1356
|
-
if (info
|
|
1986
|
+
if (isCustodialDelegate({ delegateInfo: info, providerId })) {
|
|
1357
1987
|
evolvedParent = await keystoneService.unregisterDelegate({
|
|
1358
1988
|
parentKeystone: evolvedParent,
|
|
1359
1989
|
delegateTjpAddr,
|
|
1360
1990
|
masterSecret: masterSecret!,
|
|
1991
|
+
signingPoolId: 'manage',
|
|
1361
1992
|
metaspace,
|
|
1362
1993
|
space,
|
|
1363
1994
|
});
|
|
1995
|
+
|
|
1996
|
+
this.showBusy({
|
|
1997
|
+
isBusy: true,
|
|
1998
|
+
title: 'Syncing to Server...',
|
|
1999
|
+
msg: `Publishing ${providerName} delegate unregistration to server...`,
|
|
2000
|
+
animationEmoji: '⚡'
|
|
2001
|
+
});
|
|
2002
|
+
syncRes = await getApiBridge().putEvolveKeystone({
|
|
2003
|
+
domainAddr: primaryTjpAddr,
|
|
2004
|
+
keystoneIbGib: evolvedParent,
|
|
2005
|
+
relatedKeystoneIbGibs: []
|
|
2006
|
+
});
|
|
2007
|
+
if (!syncRes.success) {
|
|
2008
|
+
throw new Error(`Server synchronization rejected: ${syncRes.message}`);
|
|
2009
|
+
}
|
|
1364
2010
|
}
|
|
1365
2011
|
}
|
|
1366
2012
|
}
|
|
1367
2013
|
|
|
1368
|
-
// 3. Sync evolved parent keystone back to server
|
|
1369
|
-
this.setSSOStatus("Publishing evolved identity keystone to server...", "info");
|
|
1370
|
-
const syncRes = await getApiBridge().putEvolveKeystone({
|
|
1371
|
-
domainAddr: getIbGibAddr({ ibGib: this.ibGib! }),
|
|
1372
|
-
keystoneIbGib: evolvedParent,
|
|
1373
|
-
relatedKeystoneIbGibs: [] // No outward delegate timelines
|
|
1374
|
-
});
|
|
1375
|
-
if (!syncRes.success) {
|
|
1376
|
-
throw new Error(`Server synchronization rejected: ${syncRes.message}`);
|
|
1377
|
-
}
|
|
1378
|
-
|
|
1379
2014
|
// 4. Update index locally
|
|
1380
2015
|
await updateSpecialIndex({
|
|
1381
2016
|
type: "keystones",
|
|
@@ -1389,15 +2024,21 @@ export class KeystoneDetailsComponentInstance
|
|
|
1389
2024
|
space
|
|
1390
2025
|
});
|
|
1391
2026
|
|
|
1392
|
-
this.setSSOStatus(`Successfully unlinked ${
|
|
1393
|
-
|
|
2027
|
+
this.setSSOStatus(`Successfully unlinked ${providerName}!`, "success");
|
|
2028
|
+
this.ibGibAddr = getIbGibAddr({ ibGib: evolvedParent });
|
|
2029
|
+
this.staticIbGib = evolvedParent;
|
|
2030
|
+
await this.loadIbGib({ getLatest: false });
|
|
1394
2031
|
|
|
1395
2032
|
} catch (error) {
|
|
1396
|
-
console.error(`${lc}
|
|
1397
|
-
this.setSSOStatus(`
|
|
2033
|
+
console.error(`${lc} ${extractErrorMsg(error)}`, error);
|
|
2034
|
+
this.setSSOStatus(`Failed to unlink ${providerName}: ${extractErrorMsg(error)}`, "error");
|
|
2035
|
+
} finally {
|
|
2036
|
+
this.showBusy({ isBusy: false });
|
|
1398
2037
|
}
|
|
1399
2038
|
}
|
|
1400
2039
|
|
|
2040
|
+
|
|
2041
|
+
|
|
1401
2042
|
private setChangePasswordStatus(msg: string, type: 'info' | 'success' | 'error') {
|
|
1402
2043
|
const el = this.elements!.changePasswordStatusEl;
|
|
1403
2044
|
el.textContent = msg;
|
|
@@ -1445,8 +2086,17 @@ export class KeystoneDetailsComponentInstance
|
|
|
1445
2086
|
challenges: newChallenges,
|
|
1446
2087
|
};
|
|
1447
2088
|
|
|
1448
|
-
const primaryTjpAddr =
|
|
1449
|
-
|
|
2089
|
+
const primaryTjpAddr = (this.ibGib ? getTjpAddr({ ibGib: this.ibGib }) : undefined)
|
|
2090
|
+
|| (this.ibGibAddr ? (() => {
|
|
2091
|
+
const { ib } = getIbAndGib({ ibGibAddr: this.ibGibAddr });
|
|
2092
|
+
const { tjpGib } = getGibInfo({ ibGibAddr: this.ibGibAddr });
|
|
2093
|
+
return tjpGib && ib ? `${ib}^${tjpGib}` : this.ibGibAddr;
|
|
2094
|
+
})() : undefined)
|
|
2095
|
+
|| getIbGibAddr({ ibGib: this.ibGib! });
|
|
2096
|
+
|
|
2097
|
+
const custodianProvider = this.aggregatedDetails?.custodianProvider
|
|
2098
|
+
|| this.ibGib?.data?.frameDetails?.custodianProvider
|
|
2099
|
+
|| (this.ibGib?.data as any)?.custodianProvider;
|
|
1450
2100
|
|
|
1451
2101
|
let requestBody: any = {
|
|
1452
2102
|
primaryTjpAddr,
|
|
@@ -1476,17 +2126,69 @@ export class KeystoneDetailsComponentInstance
|
|
|
1476
2126
|
requestBody.code = resPopup.code;
|
|
1477
2127
|
requestBody.redirectUri = resPopup.redirectUri;
|
|
1478
2128
|
} else {
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
this.setChangePasswordStatus("Verification code required.", "error");
|
|
2129
|
+
// Email OTP flow
|
|
2130
|
+
const email = this.aggregatedDetails?.emails?.[0]
|
|
2131
|
+
|| this.ibGib?.data?.emails?.[0]
|
|
2132
|
+
|| this.ibGib?.data?.frameDetails?.emails?.[0];
|
|
2133
|
+
|
|
2134
|
+
if (!email) {
|
|
2135
|
+
this.showBusy({ isBusy: false });
|
|
2136
|
+
this.setChangePasswordStatus("No email address found for this identity to send verification code.", "error");
|
|
1488
2137
|
return;
|
|
1489
2138
|
}
|
|
2139
|
+
|
|
2140
|
+
const otpSection = this.elements?.sectionAddPasswordOtpEl;
|
|
2141
|
+
const otpInput = this.elements?.inputAddPasswordOtpEl;
|
|
2142
|
+
const isOtpVisible = otpSection && !otpSection.classList.contains('hidden');
|
|
2143
|
+
|
|
2144
|
+
// Step 1: If OTP section is not visible yet, send code and show the OTP input inline in the card
|
|
2145
|
+
if (!isOtpVisible) {
|
|
2146
|
+
this.showBusy({
|
|
2147
|
+
isBusy: true,
|
|
2148
|
+
title: 'Sending Verification Code...',
|
|
2149
|
+
msg: `Sending 6-digit verification code to ${email}...`,
|
|
2150
|
+
animationEmoji: '📧'
|
|
2151
|
+
});
|
|
2152
|
+
this.setChangePasswordStatus(`Sending verification code to ${email}...`, "info");
|
|
2153
|
+
|
|
2154
|
+
const resCode = await getApiBridge().postVerificationCode({
|
|
2155
|
+
email,
|
|
2156
|
+
primaryTjpAddr,
|
|
2157
|
+
});
|
|
2158
|
+
|
|
2159
|
+
this.showBusy({ isBusy: false });
|
|
2160
|
+
|
|
2161
|
+
if (!resCode.success) {
|
|
2162
|
+
this.setChangePasswordStatus(resCode.message || "Failed to send verification code.", "error");
|
|
2163
|
+
return;
|
|
2164
|
+
}
|
|
2165
|
+
|
|
2166
|
+
if (otpSection) {
|
|
2167
|
+
otpSection.classList.remove('hidden');
|
|
2168
|
+
}
|
|
2169
|
+
if (this.elements?.descAddPasswordOtpEl) {
|
|
2170
|
+
this.elements.descAddPasswordOtpEl.textContent = `A 6-digit verification code was sent to ${email}. Enter it below to establish sovereign ownership:`;
|
|
2171
|
+
}
|
|
2172
|
+
if (this.elements?.btnChangePasswordEl) {
|
|
2173
|
+
this.elements.btnChangePasswordEl.textContent = "Verify Code & Add Master Password";
|
|
2174
|
+
}
|
|
2175
|
+
this.setChangePasswordStatus(`✉️ 6-digit verification code sent to ${email}. Enter the code and click "Verify Code & Add Master Password".`, "info");
|
|
2176
|
+
this.startResendCooldownTimer(30);
|
|
2177
|
+
if (otpInput) {
|
|
2178
|
+
otpInput.value = '';
|
|
2179
|
+
otpInput.focus();
|
|
2180
|
+
}
|
|
2181
|
+
return;
|
|
2182
|
+
}
|
|
2183
|
+
|
|
2184
|
+
// Step 2: OTP section is visible - validate entered code
|
|
2185
|
+
const otpCode = (otpInput?.value || '').trim();
|
|
2186
|
+
if (!otpCode || otpCode.length < 6) {
|
|
2187
|
+
this.setChangePasswordStatus("Please enter the complete 6-digit verification code sent to your email.", "error");
|
|
2188
|
+
if (otpInput) { otpInput.focus(); }
|
|
2189
|
+
return;
|
|
2190
|
+
}
|
|
2191
|
+
|
|
1490
2192
|
requestBody.email = email;
|
|
1491
2193
|
requestBody.code = otpCode;
|
|
1492
2194
|
}
|
|
@@ -1517,42 +2219,371 @@ export class KeystoneDetailsComponentInstance
|
|
|
1517
2219
|
const space = await metaspace.getLocalUserSpace({ lock: false });
|
|
1518
2220
|
if (!space) { throw new Error("Local user space not available"); }
|
|
1519
2221
|
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
{
|
|
1526
|
-
|
|
1527
|
-
ibGibs: [toDto({ ibGib: evolvedParent })]
|
|
2222
|
+
if (resJson.primaryGraph) {
|
|
2223
|
+
const graphIbGibs = Object.values(resJson.primaryGraph) as any[];
|
|
2224
|
+
await metaspace.put({ ibGibs: graphIbGibs, space });
|
|
2225
|
+
const timelinesMap = getTimelinesGroupedByTjp({ ibGibs: graphIbGibs });
|
|
2226
|
+
for (const tjpKey of Object.keys(timelinesMap)) {
|
|
2227
|
+
for (const frame of timelinesMap[tjpKey]) {
|
|
2228
|
+
await metaspace.registerNewIbGib({ ibGib: frame, space });
|
|
1528
2229
|
}
|
|
1529
|
-
|
|
2230
|
+
}
|
|
2231
|
+
} else {
|
|
2232
|
+
await metaspace.put({ ibGibs: [evolvedParent], space });
|
|
2233
|
+
await metaspace.registerNewIbGib({ ibGib: evolvedParent, space });
|
|
2234
|
+
}
|
|
2235
|
+
|
|
2236
|
+
await this.registerAndSyncSovereignPasswordDelegate({
|
|
2237
|
+
evolvedParent,
|
|
2238
|
+
newPassword,
|
|
2239
|
+
primaryTjpAddr,
|
|
1530
2240
|
metaspace,
|
|
1531
2241
|
space
|
|
1532
2242
|
});
|
|
1533
2243
|
|
|
1534
|
-
|
|
2244
|
+
} catch (error) {
|
|
2245
|
+
console.error(`${lc} Elevation failed: ${extractErrorMsg(error)}`);
|
|
2246
|
+
this.setChangePasswordStatus(`Failed to add master password: ${extractErrorMsg(error)}`, "error");
|
|
2247
|
+
if (this.elements?.inputAddPasswordOtpEl && !this.elements.sectionAddPasswordOtpEl?.classList.contains('hidden')) {
|
|
2248
|
+
this.elements.inputAddPasswordOtpEl.select();
|
|
2249
|
+
this.elements.inputAddPasswordOtpEl.focus();
|
|
2250
|
+
}
|
|
2251
|
+
} finally {
|
|
2252
|
+
this.showBusy({ isBusy: false });
|
|
2253
|
+
}
|
|
2254
|
+
}
|
|
1535
2255
|
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
2256
|
+
protected async handleResendAddPasswordOtp(): Promise<void> {
|
|
2257
|
+
const lc = `${this.lc}[${this.handleResendAddPasswordOtp.name}]`;
|
|
2258
|
+
const email = this.aggregatedDetails?.emails?.[0]
|
|
2259
|
+
|| this.ibGib?.data?.emails?.[0]
|
|
2260
|
+
|| this.ibGib?.data?.frameDetails?.emails?.[0];
|
|
2261
|
+
if (!email) {
|
|
2262
|
+
this.setChangePasswordStatus("No email address found for this identity to send verification code.", "error");
|
|
2263
|
+
return;
|
|
2264
|
+
}
|
|
2265
|
+
const primaryTjpAddr = (this.ibGib ? getTjpAddr({ ibGib: this.ibGib }) : undefined)
|
|
2266
|
+
|| (this.ibGibAddr ? (() => {
|
|
2267
|
+
const { ib } = getIbAndGib({ ibGibAddr: this.ibGibAddr });
|
|
2268
|
+
const { tjpGib } = getGibInfo({ ibGibAddr: this.ibGibAddr });
|
|
2269
|
+
return tjpGib && ib ? `${ib}^${tjpGib}` : this.ibGibAddr;
|
|
2270
|
+
})() : undefined)
|
|
2271
|
+
|| getIbGibAddr({ ibGib: this.ibGib! });
|
|
2272
|
+
|
|
2273
|
+
const btn = this.elements!.btnResendAddPasswordOtpEl;
|
|
2274
|
+
if (btn) {
|
|
2275
|
+
btn.disabled = true;
|
|
2276
|
+
btn.textContent = "Sending...";
|
|
2277
|
+
}
|
|
2278
|
+
this.setChangePasswordStatus(`Resending verification code to ${email}...`, "info");
|
|
2279
|
+
try {
|
|
2280
|
+
const resCode = await getApiBridge().postVerificationCode({ email, primaryTjpAddr });
|
|
2281
|
+
if (resCode.success) {
|
|
2282
|
+
this.setChangePasswordStatus(`✉️ Fresh 6-digit verification code sent to ${email}!`, "success");
|
|
2283
|
+
this.startResendCooldownTimer(30);
|
|
2284
|
+
if (this.elements?.inputAddPasswordOtpEl) {
|
|
2285
|
+
this.elements.inputAddPasswordOtpEl.value = '';
|
|
2286
|
+
this.elements.inputAddPasswordOtpEl.focus();
|
|
2287
|
+
}
|
|
2288
|
+
} else {
|
|
2289
|
+
this.setChangePasswordStatus(resCode.message || "Failed to resend verification code.", "error");
|
|
2290
|
+
if (btn) {
|
|
2291
|
+
btn.disabled = false;
|
|
2292
|
+
btn.textContent = "Resend Code";
|
|
2293
|
+
}
|
|
2294
|
+
}
|
|
2295
|
+
} catch (err) {
|
|
2296
|
+
this.setChangePasswordStatus(`Error resending verification code: ${extractErrorMsg(err)}`, "error");
|
|
2297
|
+
if (btn) {
|
|
2298
|
+
btn.disabled = false;
|
|
2299
|
+
btn.textContent = "Resend Code";
|
|
2300
|
+
}
|
|
2301
|
+
}
|
|
2302
|
+
}
|
|
1542
2303
|
|
|
1543
|
-
|
|
2304
|
+
/**
|
|
2305
|
+
* Common DRY post-elevation workflow: creates and registers the password-based
|
|
2306
|
+
* device sync delegate, synchronizes the evolved identity with the delegate to the server,
|
|
2307
|
+
* updates the local space and special index, and dispatches the identity change event.
|
|
2308
|
+
*/
|
|
2309
|
+
private async registerAndSyncSovereignPasswordDelegate({
|
|
2310
|
+
evolvedParent,
|
|
2311
|
+
newPassword,
|
|
2312
|
+
primaryTjpAddr,
|
|
2313
|
+
metaspace,
|
|
2314
|
+
space,
|
|
2315
|
+
}: {
|
|
2316
|
+
evolvedParent: KeystoneIbGib_V1;
|
|
2317
|
+
newPassword: string;
|
|
2318
|
+
primaryTjpAddr: string;
|
|
2319
|
+
metaspace: any;
|
|
2320
|
+
space: any;
|
|
2321
|
+
}): Promise<KeystoneIbGib_V1> {
|
|
2322
|
+
const lc = `${this.lc}[${this.registerAndSyncSovereignPasswordDelegate.name}]`;
|
|
2323
|
+
|
|
2324
|
+
// 1. Create a password-based sync delegate for this device
|
|
2325
|
+
this.showBusy({
|
|
2326
|
+
isBusy: true,
|
|
2327
|
+
title: 'Creating Sovereign Sync Delegate...',
|
|
2328
|
+
msg: 'Deriving local device sync delegate keys from Master Password...',
|
|
2329
|
+
animationEmoji: '🔑'
|
|
2330
|
+
});
|
|
2331
|
+
await delay(50);
|
|
2332
|
+
|
|
2333
|
+
const rawUsername = this.aggregatedDetails?.username
|
|
2334
|
+
|| this.ibGib?.data?.username
|
|
2335
|
+
|| this.aggregatedDetails?.emails?.[0]?.split('@')[0]
|
|
2336
|
+
|| this.ibGib?.data?.emails?.[0]?.split('@')[0]
|
|
2337
|
+
|| 'user';
|
|
2338
|
+
|
|
2339
|
+
const { delegateKeystone: userSyncDelegate, deviceTag } = await createSovereignDeviceSyncDelegate({
|
|
2340
|
+
username: rawUsername,
|
|
2341
|
+
primaryTjpAddr,
|
|
2342
|
+
masterSecret: newPassword,
|
|
2343
|
+
metaspace,
|
|
2344
|
+
space,
|
|
2345
|
+
});
|
|
1544
2346
|
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
2347
|
+
// 2. Register the new user sync delegate on the evolved parent keystone
|
|
2348
|
+
this.showBusy({
|
|
2349
|
+
isBusy: true,
|
|
2350
|
+
title: 'Registering Sovereign Delegate...',
|
|
2351
|
+
msg: 'Registering user sync delegate on primary keystone...',
|
|
2352
|
+
animationEmoji: '📝'
|
|
2353
|
+
});
|
|
2354
|
+
await delay(50);
|
|
2355
|
+
|
|
2356
|
+
const keystoneService = new KeystoneService_V1();
|
|
2357
|
+
const parentWithUserDelegate = await keystoneService.registerDelegate({
|
|
2358
|
+
parentKeystone: evolvedParent,
|
|
2359
|
+
delegateKeystone: userSyncDelegate,
|
|
2360
|
+
masterSecret: newPassword,
|
|
2361
|
+
signingPoolId: POOL_ID_MANAGE,
|
|
2362
|
+
metaspace,
|
|
2363
|
+
space,
|
|
2364
|
+
allowedVerbs: [KEYSTONE_VERB_SYNC, KEYSTONE_VERB_CONNECT],
|
|
2365
|
+
deviceName: deviceTag
|
|
2366
|
+
});
|
|
1549
2367
|
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
2368
|
+
// 3. Post evolved keystone with new delegate to server
|
|
2369
|
+
this.showBusy({
|
|
2370
|
+
isBusy: true,
|
|
2371
|
+
title: 'Syncing to Server...',
|
|
2372
|
+
msg: 'Publishing updated domain identity and sovereign delegate to server...',
|
|
2373
|
+
animationEmoji: '⚡'
|
|
2374
|
+
});
|
|
2375
|
+
|
|
2376
|
+
const resSync = await getApiBridge().putEvolveKeystone({
|
|
2377
|
+
domainAddr: primaryTjpAddr,
|
|
2378
|
+
keystoneIbGib: parentWithUserDelegate,
|
|
2379
|
+
relatedKeystoneIbGibs: [userSyncDelegate]
|
|
2380
|
+
});
|
|
2381
|
+
if (!resSync.success) {
|
|
2382
|
+
throw new Error(`Server sync of password delegate failed: ${resSync.message}`);
|
|
2383
|
+
}
|
|
2384
|
+
|
|
2385
|
+
// 4. Save parentWithUserDelegate and userSyncDelegate to local space & metaspace
|
|
2386
|
+
await metaspace.put({ ibGibs: [parentWithUserDelegate, userSyncDelegate], space });
|
|
2387
|
+
await metaspace.registerNewIbGib({ ibGib: parentWithUserDelegate, space });
|
|
2388
|
+
await metaspace.registerNewIbGib({ ibGib: userSyncDelegate, space });
|
|
2389
|
+
|
|
2390
|
+
await updateSpecialIndex({
|
|
2391
|
+
type: "keystones",
|
|
2392
|
+
rel8nInfos: [
|
|
2393
|
+
{
|
|
2394
|
+
rel8nName: "keystone",
|
|
2395
|
+
ibGibs: [toDto({ ibGib: parentWithUserDelegate }), toDto({ ibGib: userSyncDelegate })]
|
|
2396
|
+
}
|
|
2397
|
+
],
|
|
2398
|
+
metaspace,
|
|
2399
|
+
space
|
|
2400
|
+
});
|
|
2401
|
+
|
|
2402
|
+
const evolvedAddr = getIbGibAddr({ ibGib: parentWithUserDelegate });
|
|
2403
|
+
|
|
2404
|
+
// Dispatch EVENT_IBGIB_IDENTITY_CHANGED
|
|
2405
|
+
window.dispatchEvent(new CustomEvent(EVENT_IBGIB_IDENTITY_CHANGED, {
|
|
2406
|
+
detail: { activeIdentityAddr: evolvedAddr },
|
|
2407
|
+
bubbles: true,
|
|
2408
|
+
composed: true
|
|
2409
|
+
}));
|
|
2410
|
+
|
|
2411
|
+
this.setChangePasswordStatus("Successfully added Master Password! Identity is now sovereign.", "success");
|
|
2412
|
+
|
|
2413
|
+
// Reset OTP section and button text
|
|
2414
|
+
if (this.elements?.sectionAddPasswordOtpEl) {
|
|
2415
|
+
this.elements.sectionAddPasswordOtpEl.classList.add('hidden');
|
|
2416
|
+
if (this.elements.inputAddPasswordOtpEl) {
|
|
2417
|
+
this.elements.inputAddPasswordOtpEl.value = '';
|
|
2418
|
+
}
|
|
2419
|
+
}
|
|
2420
|
+
if (this.elements?.btnChangePasswordEl) {
|
|
2421
|
+
this.elements.btnChangePasswordEl.textContent = "Update Password";
|
|
2422
|
+
}
|
|
2423
|
+
|
|
2424
|
+
// Refresh the view
|
|
2425
|
+
this.ibGibAddr = evolvedAddr;
|
|
2426
|
+
this.staticIbGib = parentWithUserDelegate;
|
|
2427
|
+
await this.loadIbGib({ getLatest: true });
|
|
2428
|
+
|
|
2429
|
+
// Clear inputs
|
|
2430
|
+
this.elements!.inputNewPasswordEl.value = '';
|
|
2431
|
+
this.elements!.inputConfirmPasswordEl.value = '';
|
|
2432
|
+
|
|
2433
|
+
return parentWithUserDelegate;
|
|
2434
|
+
}
|
|
2435
|
+
|
|
2436
|
+
/**
|
|
2437
|
+
* Handles adding a master password to a sovereign identity that was created with passkeys only.
|
|
2438
|
+
* Authorizes via passkey biometric/hardware assertion and cryptographically adds the manage pool.
|
|
2439
|
+
*/
|
|
2440
|
+
protected async handleAddPasswordToPasskeyIdentity(): Promise<void> {
|
|
2441
|
+
const lc = `${this.lc}[${this.handleAddPasswordToPasskeyIdentity.name}]`;
|
|
2442
|
+
this.setChangePasswordStatus("Validating entries...", "info");
|
|
2443
|
+
|
|
2444
|
+
try {
|
|
2445
|
+
const newPassword = this.elements!.inputNewPasswordEl.value;
|
|
2446
|
+
const confirmPassword = this.elements!.inputConfirmPasswordEl.value;
|
|
2447
|
+
|
|
2448
|
+
if (!newPassword) {
|
|
2449
|
+
this.setChangePasswordStatus("Master password is required.", "error");
|
|
2450
|
+
return;
|
|
2451
|
+
}
|
|
2452
|
+
if (newPassword !== confirmPassword) {
|
|
2453
|
+
this.setChangePasswordStatus("Master password and confirmation do not match.", "error");
|
|
2454
|
+
return;
|
|
2455
|
+
}
|
|
2456
|
+
|
|
2457
|
+
const passkeysMap = this.aggregatedDetails?.passkeys
|
|
2458
|
+
|| (this.ibGib?.data as any)?.frameDetails?.passkeys
|
|
2459
|
+
|| (this.ibGib?.data as any)?.passkeys
|
|
2460
|
+
|| {};
|
|
2461
|
+
const passkeysList: KeystonePasskeyEntry[] = Array.isArray(passkeysMap)
|
|
2462
|
+
? passkeysMap
|
|
2463
|
+
: Object.values(passkeysMap);
|
|
2464
|
+
|
|
2465
|
+
const passkeyPool = this.ibGib?.data?.challengePools?.find(p => p.id.startsWith('passkey-manage-'));
|
|
2466
|
+
if (!passkeyPool) {
|
|
2467
|
+
this.setChangePasswordStatus("No active passkey manage pool found on this identity.", "error");
|
|
2468
|
+
return;
|
|
2469
|
+
}
|
|
2470
|
+
|
|
2471
|
+
let passkeySecret: string | undefined;
|
|
2472
|
+
let signingPoolId: string | undefined;
|
|
2473
|
+
|
|
2474
|
+
try {
|
|
2475
|
+
const authResult = await promptForAuthoritySecret({
|
|
2476
|
+
keystone: this.ibGib,
|
|
2477
|
+
aggregatedDetails: this.aggregatedDetails,
|
|
2478
|
+
title: 'Authorize Master Password',
|
|
2479
|
+
msg: 'Please touch your fingerprint sensor, Face ID, or security key to authorize adding a Master Password...',
|
|
2480
|
+
verb: KEYSTONE_VERB_MANAGE
|
|
2481
|
+
});
|
|
2482
|
+
passkeySecret = authResult.secret;
|
|
2483
|
+
signingPoolId = authResult.poolId;
|
|
2484
|
+
} catch (err) {
|
|
2485
|
+
this.setChangePasswordStatus("Authorization cancelled.", "error");
|
|
2486
|
+
return;
|
|
2487
|
+
}
|
|
2488
|
+
|
|
2489
|
+
this.showBusy({
|
|
2490
|
+
isBusy: true,
|
|
2491
|
+
title: 'Adding Master Password...',
|
|
2492
|
+
msg: 'Compiling cryptographic master password pool and evolving identity...',
|
|
2493
|
+
animationEmoji: '🔑'
|
|
2494
|
+
});
|
|
2495
|
+
await delay(50);
|
|
2496
|
+
this.setChangePasswordStatus("Generating sovereign master password challenge pool...", "info");
|
|
2497
|
+
|
|
2498
|
+
const metaspace = await getGlobalMetaspace_waitIfNeeded();
|
|
2499
|
+
if (!metaspace) throw new Error("Global metaspace not available");
|
|
2500
|
+
const space = await metaspace.getLocalUserSpace({ lock: false });
|
|
2501
|
+
if (!space) throw new Error("Local user space not available");
|
|
2502
|
+
|
|
2503
|
+
const poolConfig: KeystonePoolConfig_HashV1 = {
|
|
2504
|
+
id: POOL_ID_MANAGE,
|
|
2505
|
+
type: KeystoneChallengeType.hash_reveal_v1,
|
|
2506
|
+
salt: POOL_ID_MANAGE,
|
|
2507
|
+
algo: HashAlgorithm.sha_256,
|
|
2508
|
+
rounds: 1,
|
|
2509
|
+
allowedVerbs: [KEYSTONE_VERB_MANAGE, KEYSTONE_VERB_SYNC, KEYSTONE_VERB_CONNECT],
|
|
2510
|
+
behavior: {
|
|
2511
|
+
size: 10,
|
|
2512
|
+
replenish: KeystoneReplenishStrategy.topUp,
|
|
2513
|
+
selectSequentially: 1,
|
|
2514
|
+
selectRandomly: 0,
|
|
2515
|
+
targetBindingCount: 0,
|
|
2516
|
+
}
|
|
2517
|
+
};
|
|
2518
|
+
|
|
2519
|
+
const newChallenges = await generatePoolChallenges({
|
|
2520
|
+
config: poolConfig,
|
|
2521
|
+
masterSecret: newPassword,
|
|
2522
|
+
});
|
|
2523
|
+
|
|
2524
|
+
const newManagePool = {
|
|
2525
|
+
id: POOL_ID_MANAGE,
|
|
2526
|
+
config: poolConfig,
|
|
2527
|
+
challenges: newChallenges,
|
|
2528
|
+
};
|
|
2529
|
+
|
|
2530
|
+
const primaryTjpAddr = (this.ibGib ? getTjpAddr({ ibGib: this.ibGib }) : undefined)
|
|
2531
|
+
|| (this.ibGibAddr ? (() => {
|
|
2532
|
+
const { ib } = getIbAndGib({ ibGibAddr: this.ibGibAddr });
|
|
2533
|
+
const { tjpGib } = getGibInfo({ ibGibAddr: this.ibGibAddr });
|
|
2534
|
+
return tjpGib && ib ? `${ib}^${tjpGib}` : this.ibGibAddr;
|
|
2535
|
+
})() : undefined)
|
|
2536
|
+
|| getIbGibAddr({ ibGib: this.ibGib! });
|
|
2537
|
+
|
|
2538
|
+
const keystoneService = new KeystoneService_V1();
|
|
2539
|
+
const evolvedParent = await keystoneService.sign({
|
|
2540
|
+
latestKeystone: this.ibGib!,
|
|
2541
|
+
claim: {
|
|
2542
|
+
target: primaryTjpAddr,
|
|
2543
|
+
verb: KEYSTONE_VERB_MANAGE
|
|
2544
|
+
},
|
|
2545
|
+
masterSecret: passkeySecret,
|
|
2546
|
+
poolId: signingPoolId,
|
|
2547
|
+
dataToPatch: {
|
|
2548
|
+
challengePools: [...(this.ibGib!.data!.challengePools || []), newManagePool]
|
|
2549
|
+
},
|
|
2550
|
+
metaspace,
|
|
2551
|
+
space
|
|
2552
|
+
});
|
|
2553
|
+
|
|
2554
|
+
// Sync pool addition to server
|
|
2555
|
+
this.showBusy({
|
|
2556
|
+
isBusy: true,
|
|
2557
|
+
title: 'Syncing to Server...',
|
|
2558
|
+
msg: 'Publishing updated identity to server...',
|
|
2559
|
+
animationEmoji: '⚡'
|
|
2560
|
+
});
|
|
2561
|
+
|
|
2562
|
+
const resSync = await getApiBridge().putEvolveKeystone({
|
|
2563
|
+
domainAddr: primaryTjpAddr,
|
|
2564
|
+
keystoneIbGib: evolvedParent,
|
|
2565
|
+
relatedKeystoneIbGibs: []
|
|
2566
|
+
});
|
|
2567
|
+
|
|
2568
|
+
if (!resSync.success) {
|
|
2569
|
+
throw new Error(`Server sync failed: ${resSync.message}`);
|
|
2570
|
+
}
|
|
2571
|
+
|
|
2572
|
+
// Save locally
|
|
2573
|
+
await metaspace.put({ ibGibs: [evolvedParent], space });
|
|
2574
|
+
await metaspace.registerNewIbGib({ ibGib: evolvedParent, space });
|
|
2575
|
+
|
|
2576
|
+
// Delegate registration & final sync via DRY helper
|
|
2577
|
+
await this.registerAndSyncSovereignPasswordDelegate({
|
|
2578
|
+
evolvedParent,
|
|
2579
|
+
newPassword,
|
|
2580
|
+
primaryTjpAddr,
|
|
2581
|
+
metaspace,
|
|
2582
|
+
space
|
|
2583
|
+
});
|
|
1553
2584
|
|
|
1554
2585
|
} catch (error) {
|
|
1555
|
-
console.error(`${lc}
|
|
2586
|
+
console.error(`${lc} Failed to add password to passkey identity: ${extractErrorMsg(error)}`, error);
|
|
1556
2587
|
this.setChangePasswordStatus(`Failed to add master password: ${extractErrorMsg(error)}`, "error");
|
|
1557
2588
|
} finally {
|
|
1558
2589
|
this.showBusy({ isBusy: false });
|
|
@@ -1562,14 +2593,28 @@ export class KeystoneDetailsComponentInstance
|
|
|
1562
2593
|
protected async handleChangePassword(): Promise<void> {
|
|
1563
2594
|
const lc = `${this.lc}[${this.handleChangePassword.name}]`;
|
|
1564
2595
|
|
|
1565
|
-
const caps =
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
|
|
2596
|
+
const caps = !!this.ibGib ?
|
|
2597
|
+
getKeystoneCapabilities({ keystone: this.ibGib, aggregatedDetails: this.aggregatedDetails }) :
|
|
2598
|
+
undefined;
|
|
2599
|
+
|
|
2600
|
+
const hasPassword = !!caps?.hasPassword;
|
|
2601
|
+
if (!hasPassword) {
|
|
2602
|
+
const isCustodialOnly = !!caps ? caps.isCustodial && !caps.isSovereign : false;
|
|
2603
|
+
if (isCustodialOnly) {
|
|
2604
|
+
await this.handleAddMasterPassword();
|
|
2605
|
+
} else {
|
|
2606
|
+
await this.handleAddPasswordToPasskeyIdentity();
|
|
2607
|
+
}
|
|
1569
2608
|
return;
|
|
1570
2609
|
}
|
|
1571
2610
|
|
|
1572
2611
|
this.setChangePasswordStatus("Validating entries...", "info");
|
|
2612
|
+
this.showBusy({
|
|
2613
|
+
isBusy: true,
|
|
2614
|
+
title: 'Changing Master Password...',
|
|
2615
|
+
msg: 'Validating current password...',
|
|
2616
|
+
animationEmoji: '🔐'
|
|
2617
|
+
});
|
|
1573
2618
|
|
|
1574
2619
|
try {
|
|
1575
2620
|
const currentPassword = this.elements!.inputCurrentPasswordEl.value;
|
|
@@ -1602,6 +2647,12 @@ export class KeystoneDetailsComponentInstance
|
|
|
1602
2647
|
return;
|
|
1603
2648
|
}
|
|
1604
2649
|
|
|
2650
|
+
this.showBusy({
|
|
2651
|
+
isBusy: true,
|
|
2652
|
+
title: 'Evolving Master Password...',
|
|
2653
|
+
msg: 'Generating new challenge proofs and rotating local user delegates...',
|
|
2654
|
+
animationEmoji: '⚙️'
|
|
2655
|
+
});
|
|
1605
2656
|
this.setChangePasswordStatus("Evolving primary identity keystone...", "info");
|
|
1606
2657
|
|
|
1607
2658
|
const metaspace = await getGlobalMetaspace_waitIfNeeded();
|
|
@@ -1609,8 +2660,7 @@ export class KeystoneDetailsComponentInstance
|
|
|
1609
2660
|
const space = await metaspace.getLocalUserSpace({ lock: false });
|
|
1610
2661
|
if (!space) { throw new Error(`(UNEXPECTED) couldn't get default local space? (E: 2fc1281c28351fd378a1d7f88eaa0526)`); }
|
|
1611
2662
|
|
|
1612
|
-
// 2. Rotate parent identity
|
|
1613
|
-
debugger; // walk through this...looking for delegate failure // hmm...I thought we got change password going?
|
|
2663
|
+
// 2. Rotate parent identity and sovereign sync delegates
|
|
1614
2664
|
const { newKeystone: evolvedParent, newDelegates: evolvedDelegates } =
|
|
1615
2665
|
await keystoneService.changePassword({
|
|
1616
2666
|
keystoneIbGib: this.ibGib!,
|
|
@@ -1620,62 +2670,22 @@ export class KeystoneDetailsComponentInstance
|
|
|
1620
2670
|
metaspace,
|
|
1621
2671
|
space
|
|
1622
2672
|
});
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
this.
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
|
|
1631
|
-
// for (const delegateInfo of Object.values(delegates)) {
|
|
1632
|
-
// const delegateAddr = delegateInfo.delegateAddr;
|
|
1633
|
-
// const delegateKeystone = await keystoneService.getLatestKeystone({
|
|
1634
|
-
// addr: delegateAddr,
|
|
1635
|
-
// metaspace,
|
|
1636
|
-
// space,
|
|
1637
|
-
// });
|
|
1638
|
-
|
|
1639
|
-
// // Identify user-owned challenge pools in the delegate that match the old password
|
|
1640
|
-
// const poolsToRotate: string[] = [];
|
|
1641
|
-
// for (const pool of delegateKeystone.data?.challengePools ?? []) {
|
|
1642
|
-
// if (!pool.isForeign) {
|
|
1643
|
-
// /**
|
|
1644
|
-
// * WARNING: There is an extreme edge case that if two pools
|
|
1645
|
-
// * happen to be owned by different parties but share the
|
|
1646
|
-
// * same secret, this will change the wrong pool's secret
|
|
1647
|
-
// * also. For V1, this shouldn't matter. But we need a
|
|
1648
|
-
// * better way of tracking pool ownership for V2 (or even
|
|
1649
|
-
// * V1 if we have multi-party support). (DO NOT REMOVE
|
|
1650
|
-
// * THIS COMMENT UNTIL THIS IS ADDRESSED)
|
|
1651
|
-
// */
|
|
1652
|
-
// const isPoolMatch = await keystoneService.verifySigningSecret({
|
|
1653
|
-
// keystoneIbGib: delegateKeystone,
|
|
1654
|
-
// signingSecret: currentPassword,
|
|
1655
|
-
// poolId: pool.id
|
|
1656
|
-
// });
|
|
1657
|
-
// if (isPoolMatch) { poolsToRotate.push(pool.id); }
|
|
1658
|
-
// }
|
|
1659
|
-
// }
|
|
1660
|
-
|
|
1661
|
-
// if (poolsToRotate.length > 0) {
|
|
1662
|
-
// console.log(`${lc} Rotating pools in delegate ${delegateInfo.delegateTjpAddr}: ${poolsToRotate.join(', ')}`);
|
|
1663
|
-
// const {newKeystone: evolvedDelegate} = await keystoneService.changePassword({
|
|
1664
|
-
// keystoneIbGib: delegateKeystone,
|
|
1665
|
-
// newMasterSecret: newPassword,
|
|
1666
|
-
// signingSecret: currentPassword,
|
|
1667
|
-
// signingPoolId: 'manage',
|
|
1668
|
-
// poolIds: poolsToRotate,
|
|
1669
|
-
// metaspace,
|
|
1670
|
-
// space
|
|
1671
|
-
// });
|
|
1672
|
-
// evolvedDelegates.push(evolvedDelegate);
|
|
1673
|
-
// }
|
|
1674
|
-
// }
|
|
1675
|
-
|
|
1676
|
-
// 4. Sync evolved parent and all evolved delegate keystones atomically
|
|
2673
|
+
|
|
2674
|
+
// 3. Sync evolved parent and all evolved delegate keystones atomically
|
|
2675
|
+
this.showBusy({
|
|
2676
|
+
isBusy: true,
|
|
2677
|
+
title: 'Syncing to Server...',
|
|
2678
|
+
msg: 'Publishing rotated identity and delegates to server...',
|
|
2679
|
+
animationEmoji: '⚡'
|
|
2680
|
+
});
|
|
1677
2681
|
this.setChangePasswordStatus("Syncing evolved keystones to server...", "info");
|
|
1678
|
-
const parentTjpAddr =
|
|
2682
|
+
const parentTjpAddr = (this.ibGib ? getTjpAddr({ ibGib: this.ibGib }) : undefined)
|
|
2683
|
+
|| (this.ibGibAddr ? (() => {
|
|
2684
|
+
const { ib } = getIbAndGib({ ibGibAddr: this.ibGibAddr });
|
|
2685
|
+
const { tjpGib } = getGibInfo({ ibGibAddr: this.ibGibAddr });
|
|
2686
|
+
return tjpGib && ib ? `${ib}^${tjpGib}` : this.ibGibAddr;
|
|
2687
|
+
})() : undefined)
|
|
2688
|
+
|| getIbGibAddr({ ibGib: this.ibGib! });
|
|
1679
2689
|
|
|
1680
2690
|
const syncRes = await getApiBridge().putEvolveKeystone({
|
|
1681
2691
|
domainAddr: parentTjpAddr,
|
|
@@ -1686,7 +2696,7 @@ export class KeystoneDetailsComponentInstance
|
|
|
1686
2696
|
throw new Error(`Server synchronization rejected: ${syncRes.message}`);
|
|
1687
2697
|
}
|
|
1688
2698
|
|
|
1689
|
-
//
|
|
2699
|
+
// 4. Update index locally
|
|
1690
2700
|
this.setChangePasswordStatus("Updating local indexes...", "info");
|
|
1691
2701
|
const newKeystoneIbGibs = [toDto({ ibGib: evolvedParent })];
|
|
1692
2702
|
if (evolvedDelegates) {
|
|
@@ -1713,11 +2723,16 @@ export class KeystoneDetailsComponentInstance
|
|
|
1713
2723
|
this.elements!.inputConfirmPasswordEl.value = "";
|
|
1714
2724
|
|
|
1715
2725
|
this.setChangePasswordStatus("Password updated successfully!", "success");
|
|
2726
|
+
this.ibGibAddr = getIbGibAddr({ ibGib: evolvedParent });
|
|
2727
|
+
this.staticIbGib = evolvedParent;
|
|
1716
2728
|
await this.loadIbGib({ getLatest: true });
|
|
1717
2729
|
|
|
1718
2730
|
} catch (error) {
|
|
2731
|
+
debugger; // error in handleChangePassword
|
|
1719
2732
|
console.error(`${lc} Password rotation failed: ${extractErrorMsg(error)}`);
|
|
1720
2733
|
this.setChangePasswordStatus(`Update failed: ${extractErrorMsg(error)}`, "error");
|
|
2734
|
+
} finally {
|
|
2735
|
+
this.showBusy({ isBusy: false });
|
|
1721
2736
|
}
|
|
1722
2737
|
}
|
|
1723
2738
|
}
|