@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
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import thisCss from "./keystone-details.css";
|
|
2
2
|
import thisHtml from "./keystone-details.html";
|
|
3
3
|
import commonComponentStylesCss from "../../common-component-styles.css";
|
|
4
|
-
import { extractErrorMsg, pretty } from "@ibgib/helper-gib/dist/helpers/utils-helper.mjs";
|
|
4
|
+
import { extractErrorMsg, pretty, getSaferSubstring, HashAlgorithm, delay } from "@ibgib/helper-gib/dist/helpers/utils-helper.mjs";
|
|
5
5
|
import { getIbAndGib, getIbGibAddr } from "@ibgib/ts-gib/dist/helper.mjs";
|
|
6
6
|
import { getGibInfo } from "@ibgib/ts-gib/dist/V1/transforms/transform-helper.mjs";
|
|
7
7
|
import { KeystoneService_V1 } from "@ibgib/core-gib/dist/keystone/keystone-service-v1.mjs";
|
|
@@ -10,15 +10,19 @@ import { EVENT_IBGIB_IDENTITY_REQUEST_CHANGE, EVENT_IBGIB_IDENTITY_CHANGED } fro
|
|
|
10
10
|
import { getApiBridge } from "../../../../api/ibgib-api-bridge.mjs";
|
|
11
11
|
import { getKeystoneSyncService } from "../../../../identity/keystone-sync-service.mjs";
|
|
12
12
|
import { updateSpecialIndex } from "@ibgib/core-gib/dist/timeline/timeline-api.mjs";
|
|
13
|
-
import { getTjpAddr, toDto } from "@ibgib/core-gib/dist/common/other/ibgib-helper.mjs";
|
|
13
|
+
import { getTjpAddr, toDto, getTimelinesGroupedByTjp } from "@ibgib/core-gib/dist/common/other/ibgib-helper.mjs";
|
|
14
14
|
import { parseSpaceIb } from "@ibgib/core-gib/dist/witness/space/space-helper.mjs";
|
|
15
15
|
import { executeSsoOAuthPopup } from "../../../../identity/sso-popup-helper.mjs";
|
|
16
16
|
import { getComponentCtorArg } from "../../../../app-bootstrap/init-orchestration.mjs";
|
|
17
|
-
import {
|
|
18
|
-
import { isSsoProviderLinked, getKeystoneCapabilities, isDelegateExpired } from "@ibgib/core-gib/dist/keystone/delegate/delegate-helpers.mjs";
|
|
17
|
+
import { promptForAuthoritySecret } from "../../../../helpers.web.mjs";
|
|
18
|
+
import { isSsoProviderLinked, getKeystoneCapabilities, isDelegateExpired, isCustodialDelegate } from "@ibgib/core-gib/dist/keystone/delegate/delegate-helpers.mjs";
|
|
19
19
|
import { generatePoolChallenges } from "@ibgib/core-gib/dist/keystone/keystone-helpers.mjs";
|
|
20
|
-
import { POOL_ID_MANAGE } from "@ibgib/core-gib/dist/keystone/keystone-constants.mjs";
|
|
20
|
+
import { POOL_ID_MANAGE, KEYSTONE_VERB_SYNC, KEYSTONE_VERB_CONNECT, KEYSTONE_VERB_MANAGE } from "@ibgib/core-gib/dist/keystone/keystone-constants.mjs";
|
|
21
|
+
import { generateDefaultDeviceTag } from "../../../../util/device-helper.mjs";
|
|
21
22
|
import { getGlobalMetaspace_waitIfNeeded } from "../../../../helpers.mjs";
|
|
23
|
+
import { createSovereignDeviceSyncDelegate, createOAuth2DeviceSyncDelegate } from "../../../../identity/device-delegate-helper.mjs";
|
|
24
|
+
import { isPasskeyPrfSupported, registerPasskeyCredential, evaluatePasskeyPrf, createPasskeyDeviceSyncDelegate, bufferToHex } from "../../../../identity/passkey-helper.mjs";
|
|
25
|
+
import { KeystoneChallengeType, KeystoneReplenishStrategy } from "@ibgib/core-gib/dist/keystone/keystone-types.mjs";
|
|
22
26
|
const logalot = false;
|
|
23
27
|
export const KEYSTONE_DETAILS_COMPONENT_NAME = 'ibgib-keystone-details';
|
|
24
28
|
export class KeystoneDetailsComponentMeta extends IbGibDynamicComponentMetaBase {
|
|
@@ -54,6 +58,7 @@ export class KeystoneDetailsComponentInstance extends IbGibDynamicComponentInsta
|
|
|
54
58
|
_onIdentityChanged = null;
|
|
55
59
|
keystoneCache = new Map();
|
|
56
60
|
aggregatedDetails;
|
|
61
|
+
resendCooldownInterval = null;
|
|
57
62
|
constructor() {
|
|
58
63
|
super();
|
|
59
64
|
}
|
|
@@ -77,6 +82,39 @@ export class KeystoneDetailsComponentInstance extends IbGibDynamicComponentInsta
|
|
|
77
82
|
}
|
|
78
83
|
}
|
|
79
84
|
}
|
|
85
|
+
async disconnected() {
|
|
86
|
+
if (this._onIdentityChanged) {
|
|
87
|
+
window.removeEventListener(EVENT_IBGIB_IDENTITY_CHANGED, this._onIdentityChanged);
|
|
88
|
+
}
|
|
89
|
+
if (this.resendCooldownInterval) {
|
|
90
|
+
clearInterval(this.resendCooldownInterval);
|
|
91
|
+
this.resendCooldownInterval = null;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
startResendCooldownTimer(durationSecs = 30) {
|
|
95
|
+
const btn = this.elements?.btnResendAddPasswordOtpEl;
|
|
96
|
+
if (!btn)
|
|
97
|
+
return;
|
|
98
|
+
if (this.resendCooldownInterval) {
|
|
99
|
+
clearInterval(this.resendCooldownInterval);
|
|
100
|
+
this.resendCooldownInterval = null;
|
|
101
|
+
}
|
|
102
|
+
let remaining = durationSecs;
|
|
103
|
+
btn.disabled = true;
|
|
104
|
+
btn.textContent = `Resend Code (${remaining}s)`;
|
|
105
|
+
this.resendCooldownInterval = setInterval(() => {
|
|
106
|
+
remaining -= 1;
|
|
107
|
+
if (remaining <= 0) {
|
|
108
|
+
clearInterval(this.resendCooldownInterval);
|
|
109
|
+
this.resendCooldownInterval = null;
|
|
110
|
+
btn.disabled = false;
|
|
111
|
+
btn.textContent = "Resend Code";
|
|
112
|
+
}
|
|
113
|
+
else {
|
|
114
|
+
btn.textContent = `Resend Code (${remaining}s)`;
|
|
115
|
+
}
|
|
116
|
+
}, 1000);
|
|
117
|
+
}
|
|
80
118
|
async getKeystoneDetails(addr) {
|
|
81
119
|
const cached = this.keystoneCache.get(addr);
|
|
82
120
|
if (cached)
|
|
@@ -142,6 +180,11 @@ export class KeystoneDetailsComponentInstance extends IbGibDynamicComponentInsta
|
|
|
142
180
|
this.elements.rawKeystoneJsonEl = shadow.getElementById('raw-keystone-json');
|
|
143
181
|
this.elements.delegatesCardEl = shadow.getElementById('delegates-card');
|
|
144
182
|
this.elements.delegatesListEl = shadow.getElementById('delegates-list');
|
|
183
|
+
this.elements.passkeysCardEl = shadow.getElementById('passkeys-card');
|
|
184
|
+
this.elements.passkeysListEl = shadow.getElementById('passkeys-list');
|
|
185
|
+
this.elements.btnAddPasskeyEl = shadow.getElementById('btn-add-passkey');
|
|
186
|
+
this.elements.selectPasskeyAttachmentEl = shadow.getElementById('select-passkey-attachment');
|
|
187
|
+
this.elements.passkeyStatusMsgEl = shadow.getElementById('passkey-status-msg');
|
|
145
188
|
this.elements.containerEl = shadow.getElementById('container');
|
|
146
189
|
this.elements.historicalWarningBannerEl = shadow.getElementById('historical-warning-banner');
|
|
147
190
|
this.elements.ssoLinkCardEl = shadow.getElementById('sso-link-card');
|
|
@@ -154,6 +197,10 @@ export class KeystoneDetailsComponentInstance extends IbGibDynamicComponentInsta
|
|
|
154
197
|
this.elements.btnChangePasswordEl = shadow.getElementById('btn-change-password');
|
|
155
198
|
this.elements.changePasswordStatusEl = shadow.getElementById('change-password-status');
|
|
156
199
|
this.elements.changePasswordCardEl = shadow.getElementById('change-password-card');
|
|
200
|
+
this.elements.sectionAddPasswordOtpEl = shadow.getElementById('section-add-password-otp');
|
|
201
|
+
this.elements.inputAddPasswordOtpEl = shadow.getElementById('input-add-password-otp');
|
|
202
|
+
this.elements.btnResendAddPasswordOtpEl = shadow.getElementById('btn-resend-add-password-otp');
|
|
203
|
+
this.elements.descAddPasswordOtpEl = shadow.getElementById('desc-add-password-otp');
|
|
157
204
|
// ElementBase requires contentEl
|
|
158
205
|
this.elements.contentEl = shadow.getElementById('identity-details-view');
|
|
159
206
|
this.initHandlers();
|
|
@@ -178,11 +225,6 @@ export class KeystoneDetailsComponentInstance extends IbGibDynamicComponentInsta
|
|
|
178
225
|
}
|
|
179
226
|
}
|
|
180
227
|
}
|
|
181
|
-
async disconnected() {
|
|
182
|
-
if (this._onIdentityChanged) {
|
|
183
|
-
window.removeEventListener(EVENT_IBGIB_IDENTITY_CHANGED, this._onIdentityChanged);
|
|
184
|
-
}
|
|
185
|
-
}
|
|
186
228
|
initHandlers() {
|
|
187
229
|
// Setup raw JSON accordion toggle
|
|
188
230
|
this.elements.rawAccordionHeaderEl.addEventListener('click', () => this.toggleAccordion());
|
|
@@ -191,9 +233,11 @@ export class KeystoneDetailsComponentInstance extends IbGibDynamicComponentInsta
|
|
|
191
233
|
this.elements.btnSetActiveEl.addEventListener('click', () => this.handleSetActiveIdentity());
|
|
192
234
|
this.elements.btnCheckUpdatesEl.addEventListener('click', () => this.handleCheckForUpdates());
|
|
193
235
|
this.elements.btnUpdateIdentityEl.addEventListener('click', () => this.handleUpdateIdentity());
|
|
236
|
+
this.elements.btnAddPasskeyEl?.addEventListener('click', () => this.handleAddPasskey());
|
|
194
237
|
this.elements.btnLinkGoogleEl.addEventListener('click', () => this.handleSSOClick('google'));
|
|
195
238
|
this.elements.btnLinkGithubEl.addEventListener('click', () => this.handleSSOClick('github'));
|
|
196
239
|
this.elements.btnChangePasswordEl.addEventListener('click', () => this.handleChangePassword());
|
|
240
|
+
this.elements.btnResendAddPasswordOtpEl?.addEventListener('click', () => this.handleResendAddPasswordOtp());
|
|
197
241
|
}
|
|
198
242
|
toggleAccordion() {
|
|
199
243
|
const header = this.elements.rawAccordionHeaderEl;
|
|
@@ -399,13 +443,22 @@ export class KeystoneDetailsComponentInstance extends IbGibDynamicComponentInsta
|
|
|
399
443
|
this.elements.btnLinkGithubEl.classList.remove('disabled');
|
|
400
444
|
// Check if origin provider or linked
|
|
401
445
|
const originProvider = this.aggregatedDetails?.custodianProvider || this.ibGib?.data?.frameDetails?.custodianProvider;
|
|
402
|
-
const isGoogleLinked =
|
|
403
|
-
|
|
404
|
-
|
|
446
|
+
const isGoogleLinked = !!this.ibGib ?
|
|
447
|
+
isSsoProviderLinked({ keystone: this.ibGib, aggregatedDetails: this.aggregatedDetails, providerId: 'google' }) :
|
|
448
|
+
false;
|
|
449
|
+
const isGithubLinked = !!this.ibGib ?
|
|
450
|
+
isSsoProviderLinked({ keystone: this.ibGib, aggregatedDetails: this.aggregatedDetails, providerId: 'github' }) :
|
|
451
|
+
false;
|
|
452
|
+
const caps = !!this.ibGib ?
|
|
453
|
+
getKeystoneCapabilities({ keystone: this.ibGib, aggregatedDetails: this.aggregatedDetails }) :
|
|
454
|
+
undefined;
|
|
405
455
|
// Google Card States
|
|
406
456
|
const googleCard = this.elements.btnLinkGoogleEl;
|
|
407
457
|
const googleLabel = googleCard.querySelector('.toggle-state-text');
|
|
408
458
|
if (isGoogleLinked) {
|
|
459
|
+
if (!caps) {
|
|
460
|
+
throw new Error(`(UNEXPECTED) isGoogleLinked but capabilities falsy? (E: cc62e8357b66860dd2cb8148c833b726)`);
|
|
461
|
+
}
|
|
409
462
|
googleCard.classList.remove('unlinked');
|
|
410
463
|
googleCard.classList.add('linked');
|
|
411
464
|
const isOrigin = originProvider === 'google';
|
|
@@ -432,6 +485,9 @@ export class KeystoneDetailsComponentInstance extends IbGibDynamicComponentInsta
|
|
|
432
485
|
const githubCard = this.elements.btnLinkGithubEl;
|
|
433
486
|
const githubLabel = githubCard.querySelector('.toggle-state-text');
|
|
434
487
|
if (isGithubLinked) {
|
|
488
|
+
if (!caps) {
|
|
489
|
+
throw new Error(`(UNEXPECTED) isGithubLinked but capabilities falsy? (E: e94688210538e0e82878088873a46826)`);
|
|
490
|
+
}
|
|
435
491
|
githubCard.classList.remove('unlinked');
|
|
436
492
|
githubCard.classList.add('linked');
|
|
437
493
|
const isOrigin = originProvider === 'github';
|
|
@@ -457,17 +513,19 @@ export class KeystoneDetailsComponentInstance extends IbGibDynamicComponentInsta
|
|
|
457
513
|
}
|
|
458
514
|
}
|
|
459
515
|
updatePasswordCardView() {
|
|
460
|
-
const caps =
|
|
516
|
+
const caps = !!this.ibGib ?
|
|
517
|
+
getKeystoneCapabilities({ keystone: this.ibGib, aggregatedDetails: this.aggregatedDetails }) :
|
|
518
|
+
undefined;
|
|
461
519
|
const cardTitleEl = this.elements?.changePasswordCardEl?.querySelector('h3');
|
|
462
520
|
const cardDescEl = this.elements?.changePasswordCardEl?.querySelector('.description');
|
|
463
521
|
const currentPasswordGroup = this.elements?.inputCurrentPasswordEl?.closest('.form-group');
|
|
464
522
|
const btnChangePassword = this.elements?.btnChangePasswordEl;
|
|
465
|
-
const
|
|
466
|
-
if (
|
|
523
|
+
const hasPassword = !!caps?.hasPassword;
|
|
524
|
+
if (!hasPassword) {
|
|
467
525
|
if (cardTitleEl)
|
|
468
526
|
cardTitleEl.textContent = "Add Master Password";
|
|
469
527
|
if (cardDescEl)
|
|
470
|
-
cardDescEl.textContent = "Establish
|
|
528
|
+
cardDescEl.textContent = "Establish a master password for this identity. This enables sovereign password authentication alongside your passkeys and linked providers.";
|
|
471
529
|
if (currentPasswordGroup)
|
|
472
530
|
currentPasswordGroup.style.display = 'none';
|
|
473
531
|
if (btnChangePassword)
|
|
@@ -483,6 +541,12 @@ export class KeystoneDetailsComponentInstance extends IbGibDynamicComponentInsta
|
|
|
483
541
|
if (btnChangePassword)
|
|
484
542
|
btnChangePassword.textContent = "Update Password";
|
|
485
543
|
}
|
|
544
|
+
if (this.elements?.sectionAddPasswordOtpEl) {
|
|
545
|
+
this.elements.sectionAddPasswordOtpEl.classList.add('hidden');
|
|
546
|
+
if (this.elements.inputAddPasswordOtpEl) {
|
|
547
|
+
this.elements.inputAddPasswordOtpEl.value = '';
|
|
548
|
+
}
|
|
549
|
+
}
|
|
486
550
|
}
|
|
487
551
|
async updateFrameDetailsView() {
|
|
488
552
|
const lc = `${this.lc}[${this.updateFrameDetailsView.name}]`;
|
|
@@ -588,17 +652,20 @@ export class KeystoneDetailsComponentInstance extends IbGibDynamicComponentInsta
|
|
|
588
652
|
this.elements.rawKeystoneJsonEl.textContent = pretty(keystone);
|
|
589
653
|
// Render Challenge Pools
|
|
590
654
|
this.renderChallengePools(keystone.data?.challengePools || []);
|
|
591
|
-
// Render Delegates, SSO link, and Password Change cards (only if this is a primary keystone, hide them for delegate/session keystones)
|
|
655
|
+
// Render Delegates, Passkeys, SSO link, and Password Change cards (only if this is a primary keystone, hide them for delegate/session keystones)
|
|
592
656
|
const details = await this.getKeystoneDetails(this.ibGibAddr);
|
|
593
657
|
if (details.isPrimary) {
|
|
594
658
|
this.elements.delegatesCardEl.classList.remove('hidden');
|
|
595
659
|
await this.renderDelegates(keystone.data?.delegates);
|
|
660
|
+
this.elements.passkeysCardEl.classList.remove('hidden');
|
|
661
|
+
await this.renderPasskeys(this.aggregatedDetails?.passkeys);
|
|
596
662
|
this.elements.ssoLinkCardEl.classList.remove('hidden');
|
|
597
663
|
this.elements.changePasswordCardEl.classList.remove('hidden');
|
|
598
664
|
this.updatePasswordCardView();
|
|
599
665
|
}
|
|
600
666
|
else {
|
|
601
667
|
this.elements.delegatesCardEl.classList.add('hidden');
|
|
668
|
+
this.elements.passkeysCardEl.classList.add('hidden');
|
|
602
669
|
this.elements.ssoLinkCardEl.classList.add('hidden');
|
|
603
670
|
this.elements.changePasswordCardEl.classList.add('hidden');
|
|
604
671
|
}
|
|
@@ -637,6 +704,15 @@ export class KeystoneDetailsComponentInstance extends IbGibDynamicComponentInsta
|
|
|
637
704
|
if (this.elements.delegatesListEl) {
|
|
638
705
|
this.elements.delegatesListEl.innerHTML = '';
|
|
639
706
|
}
|
|
707
|
+
if (this.elements.passkeysListEl) {
|
|
708
|
+
this.elements.passkeysListEl.innerHTML = '';
|
|
709
|
+
}
|
|
710
|
+
if (this.elements.sectionAddPasswordOtpEl) {
|
|
711
|
+
this.elements.sectionAddPasswordOtpEl.classList.add('hidden');
|
|
712
|
+
if (this.elements.inputAddPasswordOtpEl) {
|
|
713
|
+
this.elements.inputAddPasswordOtpEl.value = '';
|
|
714
|
+
}
|
|
715
|
+
}
|
|
640
716
|
}
|
|
641
717
|
renderChallengePools(pools) {
|
|
642
718
|
if (!this.elements) {
|
|
@@ -934,11 +1010,17 @@ export class KeystoneDetailsComponentInstance extends IbGibDynamicComponentInsta
|
|
|
934
1010
|
const lc = `${this.lc}[${this.handleRevokeDelegate.name}]`;
|
|
935
1011
|
try {
|
|
936
1012
|
let masterSecret;
|
|
1013
|
+
let matchedPoolId;
|
|
937
1014
|
try {
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
1015
|
+
const authResult = await promptForAuthoritySecret({
|
|
1016
|
+
keystone: this.ibGib,
|
|
1017
|
+
aggregatedDetails: this.aggregatedDetails,
|
|
1018
|
+
title: 'Revoke Delegate',
|
|
1019
|
+
msg: `Enter Master Password or touch security key to revoke delegate '${deviceName}':`,
|
|
1020
|
+
verb: KEYSTONE_VERB_MANAGE
|
|
941
1021
|
});
|
|
1022
|
+
masterSecret = authResult.secret;
|
|
1023
|
+
matchedPoolId = authResult.poolId;
|
|
942
1024
|
}
|
|
943
1025
|
catch (err) {
|
|
944
1026
|
// User cancelled prompt
|
|
@@ -961,6 +1043,7 @@ export class KeystoneDetailsComponentInstance extends IbGibDynamicComponentInsta
|
|
|
961
1043
|
parentKeystone: this.ibGib,
|
|
962
1044
|
delegateTjpAddr,
|
|
963
1045
|
masterSecret,
|
|
1046
|
+
signingPoolId: matchedPoolId,
|
|
964
1047
|
metaspace,
|
|
965
1048
|
space
|
|
966
1049
|
});
|
|
@@ -1006,6 +1089,453 @@ export class KeystoneDetailsComponentInstance extends IbGibDynamicComponentInsta
|
|
|
1006
1089
|
this.setSyncStatus(`Revocation failed: ${extractErrorMsg(error)}`, "error");
|
|
1007
1090
|
}
|
|
1008
1091
|
}
|
|
1092
|
+
setPasskeyStatus(msg, type) {
|
|
1093
|
+
const statusEl = this.elements?.passkeyStatusMsgEl;
|
|
1094
|
+
if (!statusEl)
|
|
1095
|
+
return;
|
|
1096
|
+
statusEl.textContent = msg;
|
|
1097
|
+
statusEl.className = `status-msg ${type}`;
|
|
1098
|
+
statusEl.classList.remove('hidden');
|
|
1099
|
+
}
|
|
1100
|
+
async renderPasskeys(passkeys) {
|
|
1101
|
+
if (!this.elements) {
|
|
1102
|
+
return;
|
|
1103
|
+
}
|
|
1104
|
+
const listEl = this.elements.passkeysListEl;
|
|
1105
|
+
listEl.innerHTML = '';
|
|
1106
|
+
const caps = !!this.ibGib ?
|
|
1107
|
+
getKeystoneCapabilities({ keystone: this.ibGib, aggregatedDetails: this.aggregatedDetails }) :
|
|
1108
|
+
undefined;
|
|
1109
|
+
// Update button text depending on whether the account is custodial-only (elevation) or sovereign/hybrid
|
|
1110
|
+
const isCustodialOnly = !!caps ? caps.isCustodial && !caps.isSovereign : false;
|
|
1111
|
+
if (this.elements.btnAddPasskeyEl) {
|
|
1112
|
+
this.elements.btnAddPasskeyEl.textContent = isCustodialOnly
|
|
1113
|
+
? "🛡️ Add Passkey (Passwordless Sovereign Elevation)"
|
|
1114
|
+
: "🛡️ Register New Passkey / Security Key";
|
|
1115
|
+
}
|
|
1116
|
+
const validPasskeyEntries = Object.entries(passkeys || {}).filter(([_, entry]) => entry !== null && entry !== undefined);
|
|
1117
|
+
if (validPasskeyEntries.length === 0) {
|
|
1118
|
+
listEl.innerHTML = `
|
|
1119
|
+
<tr>
|
|
1120
|
+
<td colspan="4" style="text-align: center; color: var(--clr-text-secondary, #b3b3b3); padding: 1rem;">
|
|
1121
|
+
No hardware passkeys or biometrics registered.
|
|
1122
|
+
</td>
|
|
1123
|
+
</tr>
|
|
1124
|
+
`;
|
|
1125
|
+
return;
|
|
1126
|
+
}
|
|
1127
|
+
for (const [credId, entry] of validPasskeyEntries) {
|
|
1128
|
+
const row = document.createElement('tr');
|
|
1129
|
+
const createdText = entry.createdUTC ? new Date(entry.createdUTC).toLocaleString() : 'N/A';
|
|
1130
|
+
const nameText = entry.name || 'Hardware Key';
|
|
1131
|
+
// Format transport badges
|
|
1132
|
+
let transportBadgesHtml = '';
|
|
1133
|
+
if (entry.transports && entry.transports.length > 0) {
|
|
1134
|
+
transportBadgesHtml = entry.transports.map(t => {
|
|
1135
|
+
let icon = '🔑';
|
|
1136
|
+
let label = t;
|
|
1137
|
+
if (t === 'internal') {
|
|
1138
|
+
icon = '💻';
|
|
1139
|
+
label = 'Touch ID / Built-in';
|
|
1140
|
+
}
|
|
1141
|
+
else if (t === 'usb') {
|
|
1142
|
+
icon = '🔑';
|
|
1143
|
+
label = 'USB Key';
|
|
1144
|
+
}
|
|
1145
|
+
else if (t === 'nfc') {
|
|
1146
|
+
icon = '📶';
|
|
1147
|
+
label = 'NFC';
|
|
1148
|
+
}
|
|
1149
|
+
else if (t === 'ble' || t === 'hybrid') {
|
|
1150
|
+
icon = '📲';
|
|
1151
|
+
label = 'Mobile / Bluetooth';
|
|
1152
|
+
}
|
|
1153
|
+
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>`;
|
|
1154
|
+
}).join(' ');
|
|
1155
|
+
}
|
|
1156
|
+
else {
|
|
1157
|
+
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>`;
|
|
1158
|
+
}
|
|
1159
|
+
row.innerHTML = `
|
|
1160
|
+
<td>
|
|
1161
|
+
<div style="font-weight: 500;">${nameText}</div>
|
|
1162
|
+
<code style="font-size: 0.75rem; color: var(--clr-text-secondary, #b3b3b3); word-break: break-all;">${credId.substring(0, 16)}...</code>
|
|
1163
|
+
</td>
|
|
1164
|
+
<td>${transportBadgesHtml}</td>
|
|
1165
|
+
<td style="font-size: 0.85rem; color: var(--clr-text-secondary, #b3b3b3);">${createdText}</td>
|
|
1166
|
+
<td>
|
|
1167
|
+
<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);">
|
|
1168
|
+
Remove
|
|
1169
|
+
</button>
|
|
1170
|
+
</td>
|
|
1171
|
+
`;
|
|
1172
|
+
const btnRemove = row.querySelector('.btn-remove-passkey');
|
|
1173
|
+
btnRemove?.addEventListener('click', () => this.handleRemovePasskey(credId));
|
|
1174
|
+
listEl.appendChild(row);
|
|
1175
|
+
}
|
|
1176
|
+
}
|
|
1177
|
+
async handleAddPasskey() {
|
|
1178
|
+
const lc = `${this.lc}[${this.handleAddPasskey.name}]`;
|
|
1179
|
+
if (!this.ibGib || !this.ibGibAddr) {
|
|
1180
|
+
return;
|
|
1181
|
+
}
|
|
1182
|
+
try {
|
|
1183
|
+
const caps = getKeystoneCapabilities({ keystone: this.ibGib, aggregatedDetails: this.aggregatedDetails });
|
|
1184
|
+
const isCustodialOnly = caps.isCustodial && !caps.isSovereign;
|
|
1185
|
+
const supported = await isPasskeyPrfSupported();
|
|
1186
|
+
if (!supported) {
|
|
1187
|
+
this.setPasskeyStatus("WebAuthn PRF extension is not supported in this browser environment.", "error");
|
|
1188
|
+
return;
|
|
1189
|
+
}
|
|
1190
|
+
this.showBusy({
|
|
1191
|
+
isBusy: true,
|
|
1192
|
+
title: 'Touch Biometrics or Security Key',
|
|
1193
|
+
msg: 'Please touch your fingerprint sensor, Face ID, or insert your USB security key when prompted...',
|
|
1194
|
+
animationEmoji: '🛡️'
|
|
1195
|
+
});
|
|
1196
|
+
const username = this.aggregatedDetails?.username || this.ibGib.data?.frameDetails?.username || 'user';
|
|
1197
|
+
const rawSalt = crypto.getRandomValues(new Uint8Array(32));
|
|
1198
|
+
const saltHex = bufferToHex(rawSalt.buffer);
|
|
1199
|
+
const attachmentVal = this.elements?.selectPasskeyAttachmentEl?.value;
|
|
1200
|
+
const authenticatorAttachment = (attachmentVal === 'cross-platform' || attachmentVal === 'platform')
|
|
1201
|
+
? attachmentVal
|
|
1202
|
+
: undefined;
|
|
1203
|
+
const regResult = await registerPasskeyCredential({
|
|
1204
|
+
username,
|
|
1205
|
+
displayName: username,
|
|
1206
|
+
salt: saltHex,
|
|
1207
|
+
userVerification: 'preferred',
|
|
1208
|
+
authenticatorAttachment,
|
|
1209
|
+
});
|
|
1210
|
+
const { credentialId, transports } = regResult;
|
|
1211
|
+
let passkeySecret = regResult.passkeySecret;
|
|
1212
|
+
if (!passkeySecret) {
|
|
1213
|
+
const prfResult = await evaluatePasskeyPrf({
|
|
1214
|
+
credentialId,
|
|
1215
|
+
salt: saltHex,
|
|
1216
|
+
transports,
|
|
1217
|
+
userVerification: 'preferred'
|
|
1218
|
+
});
|
|
1219
|
+
passkeySecret = prfResult.passkeySecret;
|
|
1220
|
+
}
|
|
1221
|
+
const defaultDeviceTag = generateDefaultDeviceTag();
|
|
1222
|
+
const passkeyName = defaultDeviceTag;
|
|
1223
|
+
const passkeyTag = getSaferSubstring({ text: passkeyName, length: 12, keepLiterals: ['-'] }) || 'key';
|
|
1224
|
+
const poolId = `passkey-manage-${passkeyTag}`;
|
|
1225
|
+
const passkeyEntry = {
|
|
1226
|
+
credentialId,
|
|
1227
|
+
poolId,
|
|
1228
|
+
name: passkeyName,
|
|
1229
|
+
salt: saltHex,
|
|
1230
|
+
createdUTC: new Date().toISOString(),
|
|
1231
|
+
...(transports && transports.length > 0 ? { transports } : {})
|
|
1232
|
+
};
|
|
1233
|
+
const metaspace = await getGlobalMetaspace_waitIfNeeded();
|
|
1234
|
+
if (!metaspace)
|
|
1235
|
+
throw new Error("Metaspace not found");
|
|
1236
|
+
const space = await metaspace.getLocalUserSpace({ lock: false });
|
|
1237
|
+
if (!space)
|
|
1238
|
+
throw new Error("Local space not found");
|
|
1239
|
+
const keystoneService = new KeystoneService_V1();
|
|
1240
|
+
let evolvedKeystone;
|
|
1241
|
+
const poolConfig = {
|
|
1242
|
+
id: poolId,
|
|
1243
|
+
type: KeystoneChallengeType.hash_reveal_v1,
|
|
1244
|
+
salt: poolId,
|
|
1245
|
+
algo: HashAlgorithm.sha_256,
|
|
1246
|
+
rounds: 1,
|
|
1247
|
+
allowedVerbs: [KEYSTONE_VERB_MANAGE, KEYSTONE_VERB_SYNC, KEYSTONE_VERB_CONNECT],
|
|
1248
|
+
behavior: {
|
|
1249
|
+
size: 10,
|
|
1250
|
+
replenish: KeystoneReplenishStrategy.topUp,
|
|
1251
|
+
selectSequentially: 1,
|
|
1252
|
+
selectRandomly: 0,
|
|
1253
|
+
targetBindingCount: 0,
|
|
1254
|
+
}
|
|
1255
|
+
};
|
|
1256
|
+
const challenges = await generatePoolChallenges({
|
|
1257
|
+
config: poolConfig,
|
|
1258
|
+
masterSecret: passkeySecret
|
|
1259
|
+
});
|
|
1260
|
+
const newPool = {
|
|
1261
|
+
id: poolId,
|
|
1262
|
+
config: poolConfig,
|
|
1263
|
+
challenges
|
|
1264
|
+
};
|
|
1265
|
+
let authSecret;
|
|
1266
|
+
let signingPoolId;
|
|
1267
|
+
if (isCustodialOnly) {
|
|
1268
|
+
// Elevate custodial account with sovereign passkey
|
|
1269
|
+
this.showBusy({
|
|
1270
|
+
isBusy: true,
|
|
1271
|
+
title: 'Elevating Account Ownership...',
|
|
1272
|
+
msg: 'Establishing sovereign passkey pool on identity...',
|
|
1273
|
+
animationEmoji: '⚡'
|
|
1274
|
+
});
|
|
1275
|
+
authSecret = passkeySecret;
|
|
1276
|
+
signingPoolId = poolId;
|
|
1277
|
+
evolvedKeystone = await keystoneService.addPools({
|
|
1278
|
+
latestKeystone: this.ibGib,
|
|
1279
|
+
masterSecret: authSecret,
|
|
1280
|
+
signingPoolId,
|
|
1281
|
+
newPools: [newPool],
|
|
1282
|
+
frameDetails: {
|
|
1283
|
+
passkeys: {
|
|
1284
|
+
[credentialId]: passkeyEntry
|
|
1285
|
+
}
|
|
1286
|
+
},
|
|
1287
|
+
dataToPatch: {
|
|
1288
|
+
credentialIds: [...(this.ibGib.data?.credentialIds || []), credentialId]
|
|
1289
|
+
},
|
|
1290
|
+
metaspace,
|
|
1291
|
+
space
|
|
1292
|
+
});
|
|
1293
|
+
}
|
|
1294
|
+
else {
|
|
1295
|
+
// Secondary passkey registration on existing sovereign identity:
|
|
1296
|
+
// Prompt user to authorize with current master password or passkey
|
|
1297
|
+
try {
|
|
1298
|
+
const authResult = await promptForAuthoritySecret({
|
|
1299
|
+
keystone: this.ibGib,
|
|
1300
|
+
aggregatedDetails: this.aggregatedDetails,
|
|
1301
|
+
title: 'Authorize New Passkey',
|
|
1302
|
+
msg: 'Enter Master Password or touch security key to authorize adding this passkey:',
|
|
1303
|
+
verb: KEYSTONE_VERB_MANAGE
|
|
1304
|
+
});
|
|
1305
|
+
authSecret = authResult.secret;
|
|
1306
|
+
signingPoolId = authResult.poolId;
|
|
1307
|
+
}
|
|
1308
|
+
catch (err) {
|
|
1309
|
+
this.setPasskeyStatus("Registration cancelled (authorization required).", "error");
|
|
1310
|
+
return;
|
|
1311
|
+
}
|
|
1312
|
+
this.showBusy({
|
|
1313
|
+
isBusy: true,
|
|
1314
|
+
title: 'Registering Passkey...',
|
|
1315
|
+
msg: 'Adding cryptographic challenge pool and evolving identity...',
|
|
1316
|
+
animationEmoji: '🛡️'
|
|
1317
|
+
});
|
|
1318
|
+
evolvedKeystone = await keystoneService.addPools({
|
|
1319
|
+
latestKeystone: this.ibGib,
|
|
1320
|
+
masterSecret: authSecret,
|
|
1321
|
+
signingPoolId,
|
|
1322
|
+
newPools: [newPool],
|
|
1323
|
+
frameDetails: {
|
|
1324
|
+
passkeys: {
|
|
1325
|
+
[credentialId]: passkeyEntry
|
|
1326
|
+
}
|
|
1327
|
+
},
|
|
1328
|
+
dataToPatch: {
|
|
1329
|
+
credentialIds: [...(this.ibGib.data?.credentialIds || []), credentialId]
|
|
1330
|
+
},
|
|
1331
|
+
metaspace,
|
|
1332
|
+
space
|
|
1333
|
+
});
|
|
1334
|
+
// Sync pool addition frame to server
|
|
1335
|
+
const primaryTjpAddr = getTjpAddr({ ibGib: evolvedKeystone, defaultIfNone: 'incomingAddr' });
|
|
1336
|
+
const resSyncPool = await getApiBridge().putEvolveKeystone({
|
|
1337
|
+
domainAddr: primaryTjpAddr,
|
|
1338
|
+
keystoneIbGib: evolvedKeystone,
|
|
1339
|
+
relatedKeystoneIbGibs: []
|
|
1340
|
+
});
|
|
1341
|
+
if (!resSyncPool.success) {
|
|
1342
|
+
throw new Error(`Server synchronization of passkey pool failed: ${resSyncPool.message}`);
|
|
1343
|
+
}
|
|
1344
|
+
}
|
|
1345
|
+
// Construct outward device sync delegate for this passkey
|
|
1346
|
+
this.showBusy({
|
|
1347
|
+
isBusy: true,
|
|
1348
|
+
title: 'Creating Sync Delegate...',
|
|
1349
|
+
msg: 'Constructing outward passkey device sync delegate...',
|
|
1350
|
+
animationEmoji: '📱'
|
|
1351
|
+
});
|
|
1352
|
+
const primaryTjpAddr = getTjpAddr({ ibGib: evolvedKeystone, defaultIfNone: 'incomingAddr' });
|
|
1353
|
+
const { delegateKeystone, deviceTag } = await createPasskeyDeviceSyncDelegate({
|
|
1354
|
+
username,
|
|
1355
|
+
primaryTjpAddr,
|
|
1356
|
+
passkeySecret,
|
|
1357
|
+
transports,
|
|
1358
|
+
metaspace,
|
|
1359
|
+
space
|
|
1360
|
+
});
|
|
1361
|
+
// Register outward sync delegate on primary keystone
|
|
1362
|
+
this.showBusy({
|
|
1363
|
+
isBusy: true,
|
|
1364
|
+
title: 'Registering Delegate...',
|
|
1365
|
+
msg: 'Registering outward passkey delegate on primary identity...',
|
|
1366
|
+
animationEmoji: '📝'
|
|
1367
|
+
});
|
|
1368
|
+
const delegateSigningPoolId = isCustodialOnly ? poolId : signingPoolId;
|
|
1369
|
+
const delegateAuthSecret = isCustodialOnly ? passkeySecret : authSecret;
|
|
1370
|
+
evolvedKeystone = await keystoneService.registerDelegate({
|
|
1371
|
+
parentKeystone: evolvedKeystone,
|
|
1372
|
+
delegateKeystone,
|
|
1373
|
+
masterSecret: delegateAuthSecret,
|
|
1374
|
+
signingPoolId: delegateSigningPoolId,
|
|
1375
|
+
metaspace,
|
|
1376
|
+
space,
|
|
1377
|
+
allowedVerbs: [KEYSTONE_VERB_SYNC, KEYSTONE_VERB_CONNECT],
|
|
1378
|
+
deviceName: deviceTag
|
|
1379
|
+
});
|
|
1380
|
+
// Sync evolved keystone and delegate to server
|
|
1381
|
+
this.showBusy({
|
|
1382
|
+
isBusy: true,
|
|
1383
|
+
title: 'Syncing to Server...',
|
|
1384
|
+
msg: 'Publishing evolved identity keystone and delegate to server...',
|
|
1385
|
+
animationEmoji: '⚡'
|
|
1386
|
+
});
|
|
1387
|
+
const resSync = await getApiBridge().putEvolveKeystone({
|
|
1388
|
+
domainAddr: primaryTjpAddr,
|
|
1389
|
+
keystoneIbGib: evolvedKeystone,
|
|
1390
|
+
relatedKeystoneIbGibs: [delegateKeystone]
|
|
1391
|
+
});
|
|
1392
|
+
if (!resSync.success) {
|
|
1393
|
+
throw new Error(`Server synchronization of delegate failed: ${resSync.message}`);
|
|
1394
|
+
}
|
|
1395
|
+
// Update Special Index
|
|
1396
|
+
await updateSpecialIndex({
|
|
1397
|
+
type: "keystones",
|
|
1398
|
+
rel8nInfos: [
|
|
1399
|
+
{
|
|
1400
|
+
rel8nName: "keystone",
|
|
1401
|
+
ibGibs: [toDto({ ibGib: evolvedKeystone }), toDto({ ibGib: delegateKeystone })]
|
|
1402
|
+
}
|
|
1403
|
+
],
|
|
1404
|
+
metaspace,
|
|
1405
|
+
space
|
|
1406
|
+
});
|
|
1407
|
+
this.setPasskeyStatus("Passkey successfully registered and bound to identity!", "success");
|
|
1408
|
+
this.ibGibAddr = getIbGibAddr({ ibGib: evolvedKeystone });
|
|
1409
|
+
this.staticIbGib = evolvedKeystone;
|
|
1410
|
+
await this.loadIbGib({ getLatest: false });
|
|
1411
|
+
}
|
|
1412
|
+
catch (error) {
|
|
1413
|
+
console.error(`${lc} ${extractErrorMsg(error)}`, error);
|
|
1414
|
+
this.setPasskeyStatus(`Failed to register passkey: ${extractErrorMsg(error)}`, "error");
|
|
1415
|
+
}
|
|
1416
|
+
finally {
|
|
1417
|
+
this.showBusy({ isBusy: false });
|
|
1418
|
+
}
|
|
1419
|
+
}
|
|
1420
|
+
async handleRemovePasskey(credentialId) {
|
|
1421
|
+
const lc = `${this.lc}[${this.handleRemovePasskey.name}]`;
|
|
1422
|
+
if (!this.ibGib || !this.ibGibAddr) {
|
|
1423
|
+
return;
|
|
1424
|
+
}
|
|
1425
|
+
const passkeyEntry = this.aggregatedDetails?.passkeys?.[credentialId];
|
|
1426
|
+
const passkeyName = passkeyEntry?.name || 'this passkey';
|
|
1427
|
+
try {
|
|
1428
|
+
const keystoneService = new KeystoneService_V1();
|
|
1429
|
+
let masterSecret;
|
|
1430
|
+
let signingPoolId;
|
|
1431
|
+
try {
|
|
1432
|
+
const authResult = await promptForAuthoritySecret({
|
|
1433
|
+
keystone: this.ibGib,
|
|
1434
|
+
aggregatedDetails: this.aggregatedDetails,
|
|
1435
|
+
title: 'Authorize Removal',
|
|
1436
|
+
msg: `Are you sure you want to remove "${passkeyName}"? Enter your Master Secret (Password) or touch security key to authorize:`,
|
|
1437
|
+
verb: KEYSTONE_VERB_MANAGE
|
|
1438
|
+
});
|
|
1439
|
+
masterSecret = authResult.secret;
|
|
1440
|
+
signingPoolId = authResult.poolId;
|
|
1441
|
+
}
|
|
1442
|
+
catch (err) {
|
|
1443
|
+
this.setPasskeyStatus("Removal cancelled or unauthorized.", "error");
|
|
1444
|
+
return;
|
|
1445
|
+
}
|
|
1446
|
+
this.showBusy({
|
|
1447
|
+
isBusy: true,
|
|
1448
|
+
title: 'Revoking Passkey...',
|
|
1449
|
+
msg: 'Tombstoning passkey record and removing challenge pool...',
|
|
1450
|
+
animationEmoji: '🗑️'
|
|
1451
|
+
});
|
|
1452
|
+
const metaspace = await getGlobalMetaspace_waitIfNeeded();
|
|
1453
|
+
if (!metaspace)
|
|
1454
|
+
throw new Error("Metaspace not found");
|
|
1455
|
+
const space = await metaspace.getLocalUserSpace({ lock: false });
|
|
1456
|
+
if (!space)
|
|
1457
|
+
throw new Error("Local space not found");
|
|
1458
|
+
const poolIdToRemove = passkeyEntry?.poolId || `passkey-manage-${credentialId.substring(0, 8)}`;
|
|
1459
|
+
const poolExists = this.ibGib.data?.challengePools.some(p => p.id === poolIdToRemove);
|
|
1460
|
+
const filteredCredentialIds = (this.ibGib.data?.credentialIds || []).filter(id => id !== credentialId);
|
|
1461
|
+
let evolvedKeystone;
|
|
1462
|
+
if (poolExists) {
|
|
1463
|
+
evolvedKeystone = await keystoneService.removePools({
|
|
1464
|
+
latestKeystone: this.ibGib,
|
|
1465
|
+
masterSecret: masterSecret,
|
|
1466
|
+
signingPoolId: signingPoolId,
|
|
1467
|
+
poolIds: [poolIdToRemove],
|
|
1468
|
+
frameDetails: {
|
|
1469
|
+
passkeys: {
|
|
1470
|
+
[credentialId]: null
|
|
1471
|
+
}
|
|
1472
|
+
},
|
|
1473
|
+
dataToPatch: {
|
|
1474
|
+
credentialIds: filteredCredentialIds
|
|
1475
|
+
},
|
|
1476
|
+
metaspace,
|
|
1477
|
+
space
|
|
1478
|
+
});
|
|
1479
|
+
}
|
|
1480
|
+
else {
|
|
1481
|
+
// If pool doesn't exist, just update frameDetails
|
|
1482
|
+
evolvedKeystone = await keystoneService.sign({
|
|
1483
|
+
latestKeystone: this.ibGib,
|
|
1484
|
+
masterSecret: masterSecret,
|
|
1485
|
+
poolId: signingPoolId,
|
|
1486
|
+
claim: {
|
|
1487
|
+
target: getIbGibAddr({ ibGib: this.ibGib })
|
|
1488
|
+
},
|
|
1489
|
+
frameDetails: {
|
|
1490
|
+
passkeys: {
|
|
1491
|
+
[credentialId]: null
|
|
1492
|
+
}
|
|
1493
|
+
},
|
|
1494
|
+
dataToPatch: {
|
|
1495
|
+
credentialIds: filteredCredentialIds
|
|
1496
|
+
},
|
|
1497
|
+
metaspace,
|
|
1498
|
+
space
|
|
1499
|
+
});
|
|
1500
|
+
}
|
|
1501
|
+
// Sync to server
|
|
1502
|
+
this.showBusy({
|
|
1503
|
+
isBusy: true,
|
|
1504
|
+
title: 'Syncing to Server...',
|
|
1505
|
+
msg: 'Publishing updated keystone to server...',
|
|
1506
|
+
animationEmoji: '⚡'
|
|
1507
|
+
});
|
|
1508
|
+
const primaryTjpAddr = getTjpAddr({ ibGib: evolvedKeystone, defaultIfNone: 'incomingAddr' });
|
|
1509
|
+
await getApiBridge().putEvolveKeystone({
|
|
1510
|
+
domainAddr: primaryTjpAddr,
|
|
1511
|
+
keystoneIbGib: evolvedKeystone,
|
|
1512
|
+
relatedKeystoneIbGibs: []
|
|
1513
|
+
});
|
|
1514
|
+
// Update Special Index
|
|
1515
|
+
await updateSpecialIndex({
|
|
1516
|
+
type: "keystones",
|
|
1517
|
+
rel8nInfos: [
|
|
1518
|
+
{
|
|
1519
|
+
rel8nName: "keystone",
|
|
1520
|
+
ibGibs: [toDto({ ibGib: evolvedKeystone })]
|
|
1521
|
+
}
|
|
1522
|
+
],
|
|
1523
|
+
metaspace,
|
|
1524
|
+
space
|
|
1525
|
+
});
|
|
1526
|
+
this.setPasskeyStatus(`Passkey "${passkeyName}" successfully removed.`, "success");
|
|
1527
|
+
this.ibGibAddr = getIbGibAddr({ ibGib: evolvedKeystone });
|
|
1528
|
+
this.staticIbGib = evolvedKeystone;
|
|
1529
|
+
await this.loadIbGib({ getLatest: false });
|
|
1530
|
+
}
|
|
1531
|
+
catch (error) {
|
|
1532
|
+
console.error(`${lc} ${extractErrorMsg(error)}`, error);
|
|
1533
|
+
this.setPasskeyStatus(`Failed to remove passkey: ${extractErrorMsg(error)}`, "error");
|
|
1534
|
+
}
|
|
1535
|
+
finally {
|
|
1536
|
+
this.showBusy({ isBusy: false });
|
|
1537
|
+
}
|
|
1538
|
+
}
|
|
1009
1539
|
async handleContextUpdated() {
|
|
1010
1540
|
const lc = `${this.lc}[${this.handleContextUpdated.name}]`;
|
|
1011
1541
|
try {
|
|
@@ -1050,10 +1580,24 @@ export class KeystoneDetailsComponentInstance extends IbGibDynamicComponentInsta
|
|
|
1050
1580
|
async handleSSOLink(providerId) {
|
|
1051
1581
|
const lc = `${this.lc}[${this.handleSSOLink.name}]`;
|
|
1052
1582
|
this.setSSOStatus(`Linking ${providerId}...`, "info");
|
|
1583
|
+
const providerName = providerId === 'google' ? 'Google' : providerId === 'github' ? 'GitHub' : providerId;
|
|
1584
|
+
this.setSSOStatus(`Opening ${providerName} SSO authorization window...`, "info");
|
|
1585
|
+
this.showBusy({
|
|
1586
|
+
isBusy: true,
|
|
1587
|
+
title: `Connecting to ${providerName}...`,
|
|
1588
|
+
msg: `Opening ${providerName} SSO authorization window...\nPlease complete authorization in the popup window.`,
|
|
1589
|
+
animationEmoji: '🌐'
|
|
1590
|
+
});
|
|
1053
1591
|
try {
|
|
1592
|
+
const primaryTjpAddr = (this.ibGib ? getTjpAddr({ ibGib: this.ibGib }) : undefined)
|
|
1593
|
+
|| (this.ibGibAddr ? (() => {
|
|
1594
|
+
const { ib } = getIbAndGib({ ibGibAddr: this.ibGibAddr });
|
|
1595
|
+
const { tjpGib } = getGibInfo({ ibGibAddr: this.ibGibAddr });
|
|
1596
|
+
return tjpGib && ib ? `${ib}^${tjpGib}` : this.ibGibAddr;
|
|
1597
|
+
})() : undefined);
|
|
1054
1598
|
const resPopup = await executeSsoOAuthPopup({
|
|
1055
1599
|
providerId,
|
|
1056
|
-
parentTjpAddr:
|
|
1600
|
+
parentTjpAddr: primaryTjpAddr
|
|
1057
1601
|
});
|
|
1058
1602
|
if (!resPopup.success || !resPopup.code) {
|
|
1059
1603
|
this.setSSOStatus(resPopup.message || `Linking cancelled.`, "error");
|
|
@@ -1065,6 +1609,9 @@ export class KeystoneDetailsComponentInstance extends IbGibDynamicComponentInsta
|
|
|
1065
1609
|
console.error(`${lc} SSO link initiation failed: ${extractErrorMsg(error)}`);
|
|
1066
1610
|
this.setSSOStatus(`Link failed: ${extractErrorMsg(error)}`, "error");
|
|
1067
1611
|
}
|
|
1612
|
+
finally {
|
|
1613
|
+
this.showBusy({ isBusy: false });
|
|
1614
|
+
}
|
|
1068
1615
|
}
|
|
1069
1616
|
setSSOStatus(msg, type) {
|
|
1070
1617
|
const statusEl = this.elements.ssoStatusMsgEl;
|
|
@@ -1074,7 +1621,14 @@ export class KeystoneDetailsComponentInstance extends IbGibDynamicComponentInsta
|
|
|
1074
1621
|
}
|
|
1075
1622
|
async handleSSOLinkCallback(code, stateObj, redirectUri) {
|
|
1076
1623
|
const lc = `${this.lc}[${this.handleSSOLinkCallback.name}]`;
|
|
1077
|
-
|
|
1624
|
+
const providerName = stateObj.providerId === 'google' ? 'Google' : stateObj.providerId === 'github' ? 'GitHub' : stateObj.providerId;
|
|
1625
|
+
this.setSSOStatus(`Retrieving challenge pool configuration from server...`, "info");
|
|
1626
|
+
this.showBusy({
|
|
1627
|
+
isBusy: true,
|
|
1628
|
+
title: `Linking ${providerName}...`,
|
|
1629
|
+
msg: `Validating ${providerName} authorization with server...`,
|
|
1630
|
+
animationEmoji: '🔐'
|
|
1631
|
+
});
|
|
1078
1632
|
try {
|
|
1079
1633
|
// 1. Exchange OAuth code for custodian public pool config
|
|
1080
1634
|
const res = await fetch('/api/identity/sso/link', {
|
|
@@ -1097,47 +1651,31 @@ export class KeystoneDetailsComponentInstance extends IbGibDynamicComponentInsta
|
|
|
1097
1651
|
throw new Error(`Failed to retrieve custodian challenge pool: ${linkData.message || 'unknown error'}`);
|
|
1098
1652
|
}
|
|
1099
1653
|
const custodianPool = linkData.custodianPool;
|
|
1100
|
-
// 2. Prompt user for
|
|
1654
|
+
// 2. Prompt user for authorization secret
|
|
1101
1655
|
const keystoneService = new KeystoneService_V1();
|
|
1102
1656
|
let masterSecret;
|
|
1103
|
-
let
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
masterSecret = await promptForSecret({
|
|
1112
|
-
msg: promptMsg,
|
|
1113
|
-
confirm: false
|
|
1114
|
-
});
|
|
1115
|
-
}
|
|
1116
|
-
catch (error) {
|
|
1117
|
-
// cancelled
|
|
1118
|
-
if (logalot) {
|
|
1119
|
-
console.log(`${lc}[promptForSecret] user cancelled (I: 911ff1fb0234949228481968e9087626)`);
|
|
1120
|
-
}
|
|
1121
|
-
masterSecret = undefined;
|
|
1122
|
-
break;
|
|
1123
|
-
}
|
|
1124
|
-
if (!masterSecret) {
|
|
1125
|
-
break;
|
|
1126
|
-
}
|
|
1127
|
-
// Verify secret using verifySigningSecret (requires manage poolId)
|
|
1128
|
-
isCorrect = await keystoneService.verifySigningSecret({
|
|
1129
|
-
keystoneIbGib: this.ibGib,
|
|
1130
|
-
signingSecret: masterSecret,
|
|
1131
|
-
poolId: 'manage'
|
|
1657
|
+
let signingPoolId;
|
|
1658
|
+
try {
|
|
1659
|
+
const authResult = await promptForAuthoritySecret({
|
|
1660
|
+
keystone: this.ibGib,
|
|
1661
|
+
aggregatedDetails: this.aggregatedDetails,
|
|
1662
|
+
title: `Link ${providerName}`,
|
|
1663
|
+
msg: `Enter Master Password or touch security key to authorize linking ${providerName}:`,
|
|
1664
|
+
verb: KEYSTONE_VERB_MANAGE
|
|
1132
1665
|
});
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
}
|
|
1666
|
+
masterSecret = authResult.secret;
|
|
1667
|
+
signingPoolId = authResult.poolId;
|
|
1136
1668
|
}
|
|
1137
|
-
|
|
1138
|
-
this.setSSOStatus("Link cancelled or failed
|
|
1669
|
+
catch (error) {
|
|
1670
|
+
this.setSSOStatus("Link cancelled or failed.", "error");
|
|
1139
1671
|
return;
|
|
1140
1672
|
}
|
|
1673
|
+
this.showBusy({
|
|
1674
|
+
isBusy: true,
|
|
1675
|
+
title: 'Evolving Identity...',
|
|
1676
|
+
msg: `Installing ${providerName} custodian pool and signing evolution...`,
|
|
1677
|
+
animationEmoji: '📝'
|
|
1678
|
+
});
|
|
1141
1679
|
this.setSSOStatus("Evolving identity locally and generating cryptographic signatures...", "info");
|
|
1142
1680
|
const metaspace = await getGlobalMetaspace_waitIfNeeded();
|
|
1143
1681
|
if (!metaspace)
|
|
@@ -1146,46 +1684,90 @@ export class KeystoneDetailsComponentInstance extends IbGibDynamicComponentInsta
|
|
|
1146
1684
|
if (!space) {
|
|
1147
1685
|
throw new Error("Local space not found");
|
|
1148
1686
|
}
|
|
1149
|
-
// 3. Evolve parent keystone to include the custodian pool
|
|
1687
|
+
// 3. Evolve parent keystone to include the custodian pool (n -> n+1)
|
|
1150
1688
|
const evolvedParent = await keystoneService.addPools({
|
|
1151
1689
|
latestKeystone: this.ibGib,
|
|
1152
1690
|
masterSecret: masterSecret,
|
|
1691
|
+
signingPoolId: signingPoolId,
|
|
1153
1692
|
newPools: [custodianPool],
|
|
1154
1693
|
metaspace,
|
|
1155
1694
|
space
|
|
1156
1695
|
});
|
|
1157
|
-
//
|
|
1158
|
-
|
|
1159
|
-
const syncRes = await getApiBridge().putEvolveKeystone({
|
|
1696
|
+
// Sync evolved parent (n+1) to server first
|
|
1697
|
+
const syncRes1 = await getApiBridge().putEvolveKeystone({
|
|
1160
1698
|
domainAddr: stateObj.parentTjpAddr,
|
|
1161
1699
|
keystoneIbGib: evolvedParent,
|
|
1162
|
-
relatedKeystoneIbGibs: []
|
|
1700
|
+
relatedKeystoneIbGibs: []
|
|
1163
1701
|
});
|
|
1164
|
-
if (!
|
|
1165
|
-
throw new Error(`Server synchronization rejected: ${
|
|
1702
|
+
if (!syncRes1.success) {
|
|
1703
|
+
throw new Error(`Server synchronization of linked pool rejected: ${syncRes1.message}`);
|
|
1704
|
+
}
|
|
1705
|
+
// 4. Create and register OAuth2 sync delegate for this provider (n+1 -> n+2)
|
|
1706
|
+
const username = this.aggregatedDetails?.username || this.ibGib?.data?.username || 'user';
|
|
1707
|
+
const { delegateKeystone, deviceTag } = await createOAuth2DeviceSyncDelegate({
|
|
1708
|
+
providerId: stateObj.providerId,
|
|
1709
|
+
username,
|
|
1710
|
+
primaryTjpAddr: stateObj.parentTjpAddr,
|
|
1711
|
+
custodianReqs: linkData.custodianReqs,
|
|
1712
|
+
metaspace,
|
|
1713
|
+
space,
|
|
1714
|
+
});
|
|
1715
|
+
const parentWithDelegate = await keystoneService.registerDelegate({
|
|
1716
|
+
parentKeystone: evolvedParent,
|
|
1717
|
+
delegateKeystone,
|
|
1718
|
+
masterSecret: masterSecret,
|
|
1719
|
+
signingPoolId: signingPoolId,
|
|
1720
|
+
metaspace,
|
|
1721
|
+
space,
|
|
1722
|
+
allowedVerbs: [KEYSTONE_VERB_SYNC, KEYSTONE_VERB_CONNECT],
|
|
1723
|
+
deviceName: deviceTag,
|
|
1724
|
+
});
|
|
1725
|
+
await metaspace.put({ ibGibs: [delegateKeystone, parentWithDelegate], space });
|
|
1726
|
+
// 5. Sync parent with registered delegate (n+2) to server
|
|
1727
|
+
this.showBusy({
|
|
1728
|
+
isBusy: true,
|
|
1729
|
+
title: 'Syncing to Server...',
|
|
1730
|
+
msg: 'Publishing linked identity and delegate to server...',
|
|
1731
|
+
animationEmoji: '⚡'
|
|
1732
|
+
});
|
|
1733
|
+
this.setSSOStatus("Publishing evolved identity keystone to server...", "info");
|
|
1734
|
+
const syncRes2 = await getApiBridge().putEvolveKeystone({
|
|
1735
|
+
domainAddr: stateObj.parentTjpAddr,
|
|
1736
|
+
keystoneIbGib: parentWithDelegate,
|
|
1737
|
+
relatedKeystoneIbGibs: [delegateKeystone]
|
|
1738
|
+
});
|
|
1739
|
+
if (!syncRes2.success) {
|
|
1740
|
+
throw new Error(`Server synchronization of delegate rejected: ${syncRes2.message}`);
|
|
1166
1741
|
}
|
|
1167
|
-
//
|
|
1742
|
+
// 6. Update index locally
|
|
1168
1743
|
await updateSpecialIndex({
|
|
1169
1744
|
type: "keystones",
|
|
1170
1745
|
rel8nInfos: [
|
|
1171
1746
|
{
|
|
1172
1747
|
rel8nName: "keystone",
|
|
1173
|
-
ibGibs: [toDto({ ibGib:
|
|
1748
|
+
ibGibs: [toDto({ ibGib: parentWithDelegate }), toDto({ ibGib: delegateKeystone })]
|
|
1174
1749
|
}
|
|
1175
1750
|
],
|
|
1176
1751
|
metaspace,
|
|
1177
1752
|
space
|
|
1178
1753
|
});
|
|
1179
1754
|
this.setSSOStatus(`Successfully linked with ${stateObj.providerId}!`, "success");
|
|
1755
|
+
this.ibGibAddr = getIbGibAddr({ ibGib: parentWithDelegate });
|
|
1756
|
+
this.staticIbGib = parentWithDelegate;
|
|
1180
1757
|
await this.loadIbGib({ getLatest: false });
|
|
1181
1758
|
}
|
|
1182
1759
|
catch (error) {
|
|
1183
1760
|
console.error(`${lc} SSO callback processing failed: ${extractErrorMsg(error)}`);
|
|
1184
1761
|
this.setSSOStatus(`Link failed: ${extractErrorMsg(error)}`, "error");
|
|
1185
1762
|
}
|
|
1763
|
+
finally {
|
|
1764
|
+
this.showBusy({ isBusy: false });
|
|
1765
|
+
}
|
|
1186
1766
|
}
|
|
1187
1767
|
async handleSSOClick(providerId) {
|
|
1188
|
-
const isLinked =
|
|
1768
|
+
const isLinked = !!this.ibGib ?
|
|
1769
|
+
isSsoProviderLinked({ keystone: this.ibGib, aggregatedDetails: this.aggregatedDetails, providerId }) :
|
|
1770
|
+
false;
|
|
1189
1771
|
if (isLinked) {
|
|
1190
1772
|
const caps = getKeystoneCapabilities({ keystone: this.ibGib, aggregatedDetails: this.aggregatedDetails });
|
|
1191
1773
|
if (!caps.isSovereign) {
|
|
@@ -1200,45 +1782,34 @@ export class KeystoneDetailsComponentInstance extends IbGibDynamicComponentInsta
|
|
|
1200
1782
|
}
|
|
1201
1783
|
async handleSSOUnlink(providerId) {
|
|
1202
1784
|
const lc = `${this.lc}[${this.handleSSOUnlink.name}]`;
|
|
1203
|
-
|
|
1785
|
+
const providerName = providerId === 'google' ? 'Google' : providerId === 'github' ? 'GitHub' : providerId;
|
|
1786
|
+
this.setSSOStatus(`Unlinking ${providerName}...`, "info");
|
|
1204
1787
|
try {
|
|
1205
|
-
// 1. Prompt user for
|
|
1788
|
+
// 1. Prompt user for authorization secret
|
|
1206
1789
|
const keystoneService = new KeystoneService_V1();
|
|
1207
1790
|
let masterSecret;
|
|
1208
|
-
let
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
masterSecret = await promptForSecret({
|
|
1217
|
-
msg: promptMsg,
|
|
1218
|
-
confirm: false
|
|
1219
|
-
});
|
|
1220
|
-
}
|
|
1221
|
-
catch (error) {
|
|
1222
|
-
masterSecret = undefined;
|
|
1223
|
-
break;
|
|
1224
|
-
}
|
|
1225
|
-
if (!masterSecret) {
|
|
1226
|
-
break;
|
|
1227
|
-
}
|
|
1228
|
-
// Verify secret using verifySigningSecret (requires manage poolId)
|
|
1229
|
-
isCorrect = await keystoneService.verifySigningSecret({
|
|
1230
|
-
keystoneIbGib: this.ibGib,
|
|
1231
|
-
signingSecret: masterSecret,
|
|
1232
|
-
poolId: 'manage'
|
|
1791
|
+
let signingPoolId;
|
|
1792
|
+
try {
|
|
1793
|
+
const authResult = await promptForAuthoritySecret({
|
|
1794
|
+
keystone: this.ibGib,
|
|
1795
|
+
aggregatedDetails: this.aggregatedDetails,
|
|
1796
|
+
title: `Unlink ${providerName}`,
|
|
1797
|
+
msg: `Are you sure you want to unlink your ${providerName} account? If so, enter your Master Password or touch security key to authorize.`,
|
|
1798
|
+
verb: KEYSTONE_VERB_MANAGE
|
|
1233
1799
|
});
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
}
|
|
1800
|
+
masterSecret = authResult.secret;
|
|
1801
|
+
signingPoolId = authResult.poolId;
|
|
1237
1802
|
}
|
|
1238
|
-
|
|
1239
|
-
this.setSSOStatus("Unlink cancelled or failed
|
|
1803
|
+
catch (error) {
|
|
1804
|
+
this.setSSOStatus("Unlink cancelled or failed.", "error");
|
|
1240
1805
|
return;
|
|
1241
1806
|
}
|
|
1807
|
+
this.showBusy({
|
|
1808
|
+
isBusy: true,
|
|
1809
|
+
title: 'Unlinking Provider...',
|
|
1810
|
+
msg: `Removing ${providerName} custodian pool and delegate keys...`,
|
|
1811
|
+
animationEmoji: '🔓'
|
|
1812
|
+
});
|
|
1242
1813
|
this.setSSOStatus("Evolving identity locally and removing custodian pool...", "info");
|
|
1243
1814
|
const metaspace = await getGlobalMetaspace_waitIfNeeded();
|
|
1244
1815
|
if (!metaspace)
|
|
@@ -1252,35 +1823,64 @@ export class KeystoneDetailsComponentInstance extends IbGibDynamicComponentInsta
|
|
|
1252
1823
|
let evolvedParent = await keystoneService.removePools({
|
|
1253
1824
|
latestKeystone: this.ibGib,
|
|
1254
1825
|
masterSecret: masterSecret,
|
|
1826
|
+
signingPoolId: signingPoolId,
|
|
1255
1827
|
poolIds: [targetPoolId],
|
|
1256
1828
|
metaspace,
|
|
1257
1829
|
space
|
|
1258
1830
|
});
|
|
1259
|
-
|
|
1260
|
-
|
|
1831
|
+
const primaryTjpAddr = (this.ibGib ? getTjpAddr({ ibGib: this.ibGib }) : undefined)
|
|
1832
|
+
|| (this.ibGibAddr ? (() => {
|
|
1833
|
+
const { ib } = getIbAndGib({ ibGibAddr: this.ibGibAddr });
|
|
1834
|
+
const { tjpGib } = getGibInfo({ ibGibAddr: this.ibGibAddr });
|
|
1835
|
+
return tjpGib && ib ? `${ib}^${tjpGib}` : this.ibGibAddr;
|
|
1836
|
+
})() : undefined)
|
|
1837
|
+
|| getIbGibAddr({ ibGib: this.ibGib });
|
|
1838
|
+
// 3. Sync pool removal frame to server
|
|
1839
|
+
this.showBusy({
|
|
1840
|
+
isBusy: true,
|
|
1841
|
+
title: 'Syncing to Server...',
|
|
1842
|
+
msg: `Publishing ${providerName} pool removal to server...`,
|
|
1843
|
+
animationEmoji: '⚡'
|
|
1844
|
+
});
|
|
1845
|
+
this.setSSOStatus("Publishing evolved identity keystone to server...", "info");
|
|
1846
|
+
let syncRes = await getApiBridge().putEvolveKeystone({
|
|
1847
|
+
domainAddr: primaryTjpAddr,
|
|
1848
|
+
keystoneIbGib: evolvedParent,
|
|
1849
|
+
relatedKeystoneIbGibs: [] // No outward delegate timelines
|
|
1850
|
+
});
|
|
1851
|
+
if (!syncRes.success) {
|
|
1852
|
+
throw new Error(`Server synchronization rejected: ${syncRes.message}`);
|
|
1853
|
+
}
|
|
1854
|
+
// 4. Unregister any corresponding outward delegate for this provider if present
|
|
1855
|
+
const delegatesMap = evolvedParent.data?.delegates || this.ibGib?.data?.delegates;
|
|
1261
1856
|
if (delegatesMap) {
|
|
1262
1857
|
for (const [delegateTjpAddr, info] of Object.entries(delegatesMap)) {
|
|
1263
|
-
if (info
|
|
1858
|
+
if (isCustodialDelegate({ delegateInfo: info, providerId })) {
|
|
1264
1859
|
evolvedParent = await keystoneService.unregisterDelegate({
|
|
1265
1860
|
parentKeystone: evolvedParent,
|
|
1266
1861
|
delegateTjpAddr,
|
|
1267
1862
|
masterSecret: masterSecret,
|
|
1863
|
+
signingPoolId: 'manage',
|
|
1268
1864
|
metaspace,
|
|
1269
1865
|
space,
|
|
1270
1866
|
});
|
|
1867
|
+
this.showBusy({
|
|
1868
|
+
isBusy: true,
|
|
1869
|
+
title: 'Syncing to Server...',
|
|
1870
|
+
msg: `Publishing ${providerName} delegate unregistration to server...`,
|
|
1871
|
+
animationEmoji: '⚡'
|
|
1872
|
+
});
|
|
1873
|
+
syncRes = await getApiBridge().putEvolveKeystone({
|
|
1874
|
+
domainAddr: primaryTjpAddr,
|
|
1875
|
+
keystoneIbGib: evolvedParent,
|
|
1876
|
+
relatedKeystoneIbGibs: []
|
|
1877
|
+
});
|
|
1878
|
+
if (!syncRes.success) {
|
|
1879
|
+
throw new Error(`Server synchronization rejected: ${syncRes.message}`);
|
|
1880
|
+
}
|
|
1271
1881
|
}
|
|
1272
1882
|
}
|
|
1273
1883
|
}
|
|
1274
|
-
// 3. Sync evolved parent keystone back to server
|
|
1275
|
-
this.setSSOStatus("Publishing evolved identity keystone to server...", "info");
|
|
1276
|
-
const syncRes = await getApiBridge().putEvolveKeystone({
|
|
1277
|
-
domainAddr: getIbGibAddr({ ibGib: this.ibGib }),
|
|
1278
|
-
keystoneIbGib: evolvedParent,
|
|
1279
|
-
relatedKeystoneIbGibs: [] // No outward delegate timelines
|
|
1280
|
-
});
|
|
1281
|
-
if (!syncRes.success) {
|
|
1282
|
-
throw new Error(`Server synchronization rejected: ${syncRes.message}`);
|
|
1283
|
-
}
|
|
1284
1884
|
// 4. Update index locally
|
|
1285
1885
|
await updateSpecialIndex({
|
|
1286
1886
|
type: "keystones",
|
|
@@ -1293,12 +1893,17 @@ export class KeystoneDetailsComponentInstance extends IbGibDynamicComponentInsta
|
|
|
1293
1893
|
metaspace,
|
|
1294
1894
|
space
|
|
1295
1895
|
});
|
|
1296
|
-
this.setSSOStatus(`Successfully unlinked ${
|
|
1297
|
-
|
|
1896
|
+
this.setSSOStatus(`Successfully unlinked ${providerName}!`, "success");
|
|
1897
|
+
this.ibGibAddr = getIbGibAddr({ ibGib: evolvedParent });
|
|
1898
|
+
this.staticIbGib = evolvedParent;
|
|
1899
|
+
await this.loadIbGib({ getLatest: false });
|
|
1298
1900
|
}
|
|
1299
1901
|
catch (error) {
|
|
1300
|
-
console.error(`${lc}
|
|
1301
|
-
this.setSSOStatus(`
|
|
1902
|
+
console.error(`${lc} ${extractErrorMsg(error)}`, error);
|
|
1903
|
+
this.setSSOStatus(`Failed to unlink ${providerName}: ${extractErrorMsg(error)}`, "error");
|
|
1904
|
+
}
|
|
1905
|
+
finally {
|
|
1906
|
+
this.showBusy({ isBusy: false });
|
|
1302
1907
|
}
|
|
1303
1908
|
}
|
|
1304
1909
|
setChangePasswordStatus(msg, type) {
|
|
@@ -1342,8 +1947,16 @@ export class KeystoneDetailsComponentInstance extends IbGibDynamicComponentInsta
|
|
|
1342
1947
|
config: initialManagePool.config,
|
|
1343
1948
|
challenges: newChallenges,
|
|
1344
1949
|
};
|
|
1345
|
-
const primaryTjpAddr =
|
|
1346
|
-
|
|
1950
|
+
const primaryTjpAddr = (this.ibGib ? getTjpAddr({ ibGib: this.ibGib }) : undefined)
|
|
1951
|
+
|| (this.ibGibAddr ? (() => {
|
|
1952
|
+
const { ib } = getIbAndGib({ ibGibAddr: this.ibGibAddr });
|
|
1953
|
+
const { tjpGib } = getGibInfo({ ibGibAddr: this.ibGibAddr });
|
|
1954
|
+
return tjpGib && ib ? `${ib}^${tjpGib}` : this.ibGibAddr;
|
|
1955
|
+
})() : undefined)
|
|
1956
|
+
|| getIbGibAddr({ ibGib: this.ibGib });
|
|
1957
|
+
const custodianProvider = this.aggregatedDetails?.custodianProvider
|
|
1958
|
+
|| this.ibGib?.data?.frameDetails?.custodianProvider
|
|
1959
|
+
|| this.ibGib?.data?.custodianProvider;
|
|
1347
1960
|
let requestBody = {
|
|
1348
1961
|
primaryTjpAddr,
|
|
1349
1962
|
newManagePool,
|
|
@@ -1370,15 +1983,60 @@ export class KeystoneDetailsComponentInstance extends IbGibDynamicComponentInsta
|
|
|
1370
1983
|
requestBody.redirectUri = resPopup.redirectUri;
|
|
1371
1984
|
}
|
|
1372
1985
|
else {
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1986
|
+
// Email OTP flow
|
|
1987
|
+
const email = this.aggregatedDetails?.emails?.[0]
|
|
1988
|
+
|| this.ibGib?.data?.emails?.[0]
|
|
1989
|
+
|| this.ibGib?.data?.frameDetails?.emails?.[0];
|
|
1990
|
+
if (!email) {
|
|
1991
|
+
this.showBusy({ isBusy: false });
|
|
1992
|
+
this.setChangePasswordStatus("No email address found for this identity to send verification code.", "error");
|
|
1993
|
+
return;
|
|
1994
|
+
}
|
|
1995
|
+
const otpSection = this.elements?.sectionAddPasswordOtpEl;
|
|
1996
|
+
const otpInput = this.elements?.inputAddPasswordOtpEl;
|
|
1997
|
+
const isOtpVisible = otpSection && !otpSection.classList.contains('hidden');
|
|
1998
|
+
// Step 1: If OTP section is not visible yet, send code and show the OTP input inline in the card
|
|
1999
|
+
if (!isOtpVisible) {
|
|
2000
|
+
this.showBusy({
|
|
2001
|
+
isBusy: true,
|
|
2002
|
+
title: 'Sending Verification Code...',
|
|
2003
|
+
msg: `Sending 6-digit verification code to ${email}...`,
|
|
2004
|
+
animationEmoji: '📧'
|
|
2005
|
+
});
|
|
2006
|
+
this.setChangePasswordStatus(`Sending verification code to ${email}...`, "info");
|
|
2007
|
+
const resCode = await getApiBridge().postVerificationCode({
|
|
2008
|
+
email,
|
|
2009
|
+
primaryTjpAddr,
|
|
2010
|
+
});
|
|
2011
|
+
this.showBusy({ isBusy: false });
|
|
2012
|
+
if (!resCode.success) {
|
|
2013
|
+
this.setChangePasswordStatus(resCode.message || "Failed to send verification code.", "error");
|
|
2014
|
+
return;
|
|
2015
|
+
}
|
|
2016
|
+
if (otpSection) {
|
|
2017
|
+
otpSection.classList.remove('hidden');
|
|
2018
|
+
}
|
|
2019
|
+
if (this.elements?.descAddPasswordOtpEl) {
|
|
2020
|
+
this.elements.descAddPasswordOtpEl.textContent = `A 6-digit verification code was sent to ${email}. Enter it below to establish sovereign ownership:`;
|
|
2021
|
+
}
|
|
2022
|
+
if (this.elements?.btnChangePasswordEl) {
|
|
2023
|
+
this.elements.btnChangePasswordEl.textContent = "Verify Code & Add Master Password";
|
|
2024
|
+
}
|
|
2025
|
+
this.setChangePasswordStatus(`✉️ 6-digit verification code sent to ${email}. Enter the code and click "Verify Code & Add Master Password".`, "info");
|
|
2026
|
+
this.startResendCooldownTimer(30);
|
|
2027
|
+
if (otpInput) {
|
|
2028
|
+
otpInput.value = '';
|
|
2029
|
+
otpInput.focus();
|
|
2030
|
+
}
|
|
2031
|
+
return;
|
|
2032
|
+
}
|
|
2033
|
+
// Step 2: OTP section is visible - validate entered code
|
|
2034
|
+
const otpCode = (otpInput?.value || '').trim();
|
|
2035
|
+
if (!otpCode || otpCode.length < 6) {
|
|
2036
|
+
this.setChangePasswordStatus("Please enter the complete 6-digit verification code sent to your email.", "error");
|
|
2037
|
+
if (otpInput) {
|
|
2038
|
+
otpInput.focus();
|
|
2039
|
+
}
|
|
1382
2040
|
return;
|
|
1383
2041
|
}
|
|
1384
2042
|
requestBody.email = email;
|
|
@@ -1410,38 +2068,328 @@ export class KeystoneDetailsComponentInstance extends IbGibDynamicComponentInsta
|
|
|
1410
2068
|
if (!space) {
|
|
1411
2069
|
throw new Error("Local user space not available");
|
|
1412
2070
|
}
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
{
|
|
1419
|
-
|
|
1420
|
-
ibGibs: [toDto({ ibGib: evolvedParent })]
|
|
2071
|
+
if (resJson.primaryGraph) {
|
|
2072
|
+
const graphIbGibs = Object.values(resJson.primaryGraph);
|
|
2073
|
+
await metaspace.put({ ibGibs: graphIbGibs, space });
|
|
2074
|
+
const timelinesMap = getTimelinesGroupedByTjp({ ibGibs: graphIbGibs });
|
|
2075
|
+
for (const tjpKey of Object.keys(timelinesMap)) {
|
|
2076
|
+
for (const frame of timelinesMap[tjpKey]) {
|
|
2077
|
+
await metaspace.registerNewIbGib({ ibGib: frame, space });
|
|
1421
2078
|
}
|
|
1422
|
-
|
|
2079
|
+
}
|
|
2080
|
+
}
|
|
2081
|
+
else {
|
|
2082
|
+
await metaspace.put({ ibGibs: [evolvedParent], space });
|
|
2083
|
+
await metaspace.registerNewIbGib({ ibGib: evolvedParent, space });
|
|
2084
|
+
}
|
|
2085
|
+
await this.registerAndSyncSovereignPasswordDelegate({
|
|
2086
|
+
evolvedParent,
|
|
2087
|
+
newPassword,
|
|
2088
|
+
primaryTjpAddr,
|
|
1423
2089
|
metaspace,
|
|
1424
2090
|
space
|
|
1425
2091
|
});
|
|
1426
|
-
const evolvedAddr = getIbGibAddr({ ibGib: evolvedParent });
|
|
1427
|
-
// Dispatch EVENT_IBGIB_IDENTITY_CHANGED
|
|
1428
|
-
window.dispatchEvent(new CustomEvent(EVENT_IBGIB_IDENTITY_CHANGED, {
|
|
1429
|
-
detail: { activeIdentityAddr: evolvedAddr },
|
|
1430
|
-
bubbles: true,
|
|
1431
|
-
composed: true
|
|
1432
|
-
}));
|
|
1433
|
-
this.setChangePasswordStatus("Successfully added Master Password! Identity is now sovereign.", "success");
|
|
1434
|
-
// Refresh the view
|
|
1435
|
-
this.ibGibAddr = evolvedAddr;
|
|
1436
|
-
this.staticIbGib = evolvedParent;
|
|
1437
|
-
await this.loadIbGib({ getLatest: true });
|
|
1438
|
-
// Clear inputs
|
|
1439
|
-
this.elements.inputNewPasswordEl.value = '';
|
|
1440
|
-
this.elements.inputConfirmPasswordEl.value = '';
|
|
1441
2092
|
}
|
|
1442
2093
|
catch (error) {
|
|
1443
2094
|
console.error(`${lc} Elevation failed: ${extractErrorMsg(error)}`);
|
|
1444
2095
|
this.setChangePasswordStatus(`Failed to add master password: ${extractErrorMsg(error)}`, "error");
|
|
2096
|
+
if (this.elements?.inputAddPasswordOtpEl && !this.elements.sectionAddPasswordOtpEl?.classList.contains('hidden')) {
|
|
2097
|
+
this.elements.inputAddPasswordOtpEl.select();
|
|
2098
|
+
this.elements.inputAddPasswordOtpEl.focus();
|
|
2099
|
+
}
|
|
2100
|
+
}
|
|
2101
|
+
finally {
|
|
2102
|
+
this.showBusy({ isBusy: false });
|
|
2103
|
+
}
|
|
2104
|
+
}
|
|
2105
|
+
async handleResendAddPasswordOtp() {
|
|
2106
|
+
const lc = `${this.lc}[${this.handleResendAddPasswordOtp.name}]`;
|
|
2107
|
+
const email = this.aggregatedDetails?.emails?.[0]
|
|
2108
|
+
|| this.ibGib?.data?.emails?.[0]
|
|
2109
|
+
|| this.ibGib?.data?.frameDetails?.emails?.[0];
|
|
2110
|
+
if (!email) {
|
|
2111
|
+
this.setChangePasswordStatus("No email address found for this identity to send verification code.", "error");
|
|
2112
|
+
return;
|
|
2113
|
+
}
|
|
2114
|
+
const primaryTjpAddr = (this.ibGib ? getTjpAddr({ ibGib: this.ibGib }) : undefined)
|
|
2115
|
+
|| (this.ibGibAddr ? (() => {
|
|
2116
|
+
const { ib } = getIbAndGib({ ibGibAddr: this.ibGibAddr });
|
|
2117
|
+
const { tjpGib } = getGibInfo({ ibGibAddr: this.ibGibAddr });
|
|
2118
|
+
return tjpGib && ib ? `${ib}^${tjpGib}` : this.ibGibAddr;
|
|
2119
|
+
})() : undefined)
|
|
2120
|
+
|| getIbGibAddr({ ibGib: this.ibGib });
|
|
2121
|
+
const btn = this.elements.btnResendAddPasswordOtpEl;
|
|
2122
|
+
if (btn) {
|
|
2123
|
+
btn.disabled = true;
|
|
2124
|
+
btn.textContent = "Sending...";
|
|
2125
|
+
}
|
|
2126
|
+
this.setChangePasswordStatus(`Resending verification code to ${email}...`, "info");
|
|
2127
|
+
try {
|
|
2128
|
+
const resCode = await getApiBridge().postVerificationCode({ email, primaryTjpAddr });
|
|
2129
|
+
if (resCode.success) {
|
|
2130
|
+
this.setChangePasswordStatus(`✉️ Fresh 6-digit verification code sent to ${email}!`, "success");
|
|
2131
|
+
this.startResendCooldownTimer(30);
|
|
2132
|
+
if (this.elements?.inputAddPasswordOtpEl) {
|
|
2133
|
+
this.elements.inputAddPasswordOtpEl.value = '';
|
|
2134
|
+
this.elements.inputAddPasswordOtpEl.focus();
|
|
2135
|
+
}
|
|
2136
|
+
}
|
|
2137
|
+
else {
|
|
2138
|
+
this.setChangePasswordStatus(resCode.message || "Failed to resend verification code.", "error");
|
|
2139
|
+
if (btn) {
|
|
2140
|
+
btn.disabled = false;
|
|
2141
|
+
btn.textContent = "Resend Code";
|
|
2142
|
+
}
|
|
2143
|
+
}
|
|
2144
|
+
}
|
|
2145
|
+
catch (err) {
|
|
2146
|
+
this.setChangePasswordStatus(`Error resending verification code: ${extractErrorMsg(err)}`, "error");
|
|
2147
|
+
if (btn) {
|
|
2148
|
+
btn.disabled = false;
|
|
2149
|
+
btn.textContent = "Resend Code";
|
|
2150
|
+
}
|
|
2151
|
+
}
|
|
2152
|
+
}
|
|
2153
|
+
/**
|
|
2154
|
+
* Common DRY post-elevation workflow: creates and registers the password-based
|
|
2155
|
+
* device sync delegate, synchronizes the evolved identity with the delegate to the server,
|
|
2156
|
+
* updates the local space and special index, and dispatches the identity change event.
|
|
2157
|
+
*/
|
|
2158
|
+
async registerAndSyncSovereignPasswordDelegate({ evolvedParent, newPassword, primaryTjpAddr, metaspace, space, }) {
|
|
2159
|
+
const lc = `${this.lc}[${this.registerAndSyncSovereignPasswordDelegate.name}]`;
|
|
2160
|
+
// 1. Create a password-based sync delegate for this device
|
|
2161
|
+
this.showBusy({
|
|
2162
|
+
isBusy: true,
|
|
2163
|
+
title: 'Creating Sovereign Sync Delegate...',
|
|
2164
|
+
msg: 'Deriving local device sync delegate keys from Master Password...',
|
|
2165
|
+
animationEmoji: '🔑'
|
|
2166
|
+
});
|
|
2167
|
+
await delay(50);
|
|
2168
|
+
const rawUsername = this.aggregatedDetails?.username
|
|
2169
|
+
|| this.ibGib?.data?.username
|
|
2170
|
+
|| this.aggregatedDetails?.emails?.[0]?.split('@')[0]
|
|
2171
|
+
|| this.ibGib?.data?.emails?.[0]?.split('@')[0]
|
|
2172
|
+
|| 'user';
|
|
2173
|
+
const { delegateKeystone: userSyncDelegate, deviceTag } = await createSovereignDeviceSyncDelegate({
|
|
2174
|
+
username: rawUsername,
|
|
2175
|
+
primaryTjpAddr,
|
|
2176
|
+
masterSecret: newPassword,
|
|
2177
|
+
metaspace,
|
|
2178
|
+
space,
|
|
2179
|
+
});
|
|
2180
|
+
// 2. Register the new user sync delegate on the evolved parent keystone
|
|
2181
|
+
this.showBusy({
|
|
2182
|
+
isBusy: true,
|
|
2183
|
+
title: 'Registering Sovereign Delegate...',
|
|
2184
|
+
msg: 'Registering user sync delegate on primary keystone...',
|
|
2185
|
+
animationEmoji: '📝'
|
|
2186
|
+
});
|
|
2187
|
+
await delay(50);
|
|
2188
|
+
const keystoneService = new KeystoneService_V1();
|
|
2189
|
+
const parentWithUserDelegate = await keystoneService.registerDelegate({
|
|
2190
|
+
parentKeystone: evolvedParent,
|
|
2191
|
+
delegateKeystone: userSyncDelegate,
|
|
2192
|
+
masterSecret: newPassword,
|
|
2193
|
+
signingPoolId: POOL_ID_MANAGE,
|
|
2194
|
+
metaspace,
|
|
2195
|
+
space,
|
|
2196
|
+
allowedVerbs: [KEYSTONE_VERB_SYNC, KEYSTONE_VERB_CONNECT],
|
|
2197
|
+
deviceName: deviceTag
|
|
2198
|
+
});
|
|
2199
|
+
// 3. Post evolved keystone with new delegate to server
|
|
2200
|
+
this.showBusy({
|
|
2201
|
+
isBusy: true,
|
|
2202
|
+
title: 'Syncing to Server...',
|
|
2203
|
+
msg: 'Publishing updated domain identity and sovereign delegate to server...',
|
|
2204
|
+
animationEmoji: '⚡'
|
|
2205
|
+
});
|
|
2206
|
+
const resSync = await getApiBridge().putEvolveKeystone({
|
|
2207
|
+
domainAddr: primaryTjpAddr,
|
|
2208
|
+
keystoneIbGib: parentWithUserDelegate,
|
|
2209
|
+
relatedKeystoneIbGibs: [userSyncDelegate]
|
|
2210
|
+
});
|
|
2211
|
+
if (!resSync.success) {
|
|
2212
|
+
throw new Error(`Server sync of password delegate failed: ${resSync.message}`);
|
|
2213
|
+
}
|
|
2214
|
+
// 4. Save parentWithUserDelegate and userSyncDelegate to local space & metaspace
|
|
2215
|
+
await metaspace.put({ ibGibs: [parentWithUserDelegate, userSyncDelegate], space });
|
|
2216
|
+
await metaspace.registerNewIbGib({ ibGib: parentWithUserDelegate, space });
|
|
2217
|
+
await metaspace.registerNewIbGib({ ibGib: userSyncDelegate, space });
|
|
2218
|
+
await updateSpecialIndex({
|
|
2219
|
+
type: "keystones",
|
|
2220
|
+
rel8nInfos: [
|
|
2221
|
+
{
|
|
2222
|
+
rel8nName: "keystone",
|
|
2223
|
+
ibGibs: [toDto({ ibGib: parentWithUserDelegate }), toDto({ ibGib: userSyncDelegate })]
|
|
2224
|
+
}
|
|
2225
|
+
],
|
|
2226
|
+
metaspace,
|
|
2227
|
+
space
|
|
2228
|
+
});
|
|
2229
|
+
const evolvedAddr = getIbGibAddr({ ibGib: parentWithUserDelegate });
|
|
2230
|
+
// Dispatch EVENT_IBGIB_IDENTITY_CHANGED
|
|
2231
|
+
window.dispatchEvent(new CustomEvent(EVENT_IBGIB_IDENTITY_CHANGED, {
|
|
2232
|
+
detail: { activeIdentityAddr: evolvedAddr },
|
|
2233
|
+
bubbles: true,
|
|
2234
|
+
composed: true
|
|
2235
|
+
}));
|
|
2236
|
+
this.setChangePasswordStatus("Successfully added Master Password! Identity is now sovereign.", "success");
|
|
2237
|
+
// Reset OTP section and button text
|
|
2238
|
+
if (this.elements?.sectionAddPasswordOtpEl) {
|
|
2239
|
+
this.elements.sectionAddPasswordOtpEl.classList.add('hidden');
|
|
2240
|
+
if (this.elements.inputAddPasswordOtpEl) {
|
|
2241
|
+
this.elements.inputAddPasswordOtpEl.value = '';
|
|
2242
|
+
}
|
|
2243
|
+
}
|
|
2244
|
+
if (this.elements?.btnChangePasswordEl) {
|
|
2245
|
+
this.elements.btnChangePasswordEl.textContent = "Update Password";
|
|
2246
|
+
}
|
|
2247
|
+
// Refresh the view
|
|
2248
|
+
this.ibGibAddr = evolvedAddr;
|
|
2249
|
+
this.staticIbGib = parentWithUserDelegate;
|
|
2250
|
+
await this.loadIbGib({ getLatest: true });
|
|
2251
|
+
// Clear inputs
|
|
2252
|
+
this.elements.inputNewPasswordEl.value = '';
|
|
2253
|
+
this.elements.inputConfirmPasswordEl.value = '';
|
|
2254
|
+
return parentWithUserDelegate;
|
|
2255
|
+
}
|
|
2256
|
+
/**
|
|
2257
|
+
* Handles adding a master password to a sovereign identity that was created with passkeys only.
|
|
2258
|
+
* Authorizes via passkey biometric/hardware assertion and cryptographically adds the manage pool.
|
|
2259
|
+
*/
|
|
2260
|
+
async handleAddPasswordToPasskeyIdentity() {
|
|
2261
|
+
const lc = `${this.lc}[${this.handleAddPasswordToPasskeyIdentity.name}]`;
|
|
2262
|
+
this.setChangePasswordStatus("Validating entries...", "info");
|
|
2263
|
+
try {
|
|
2264
|
+
const newPassword = this.elements.inputNewPasswordEl.value;
|
|
2265
|
+
const confirmPassword = this.elements.inputConfirmPasswordEl.value;
|
|
2266
|
+
if (!newPassword) {
|
|
2267
|
+
this.setChangePasswordStatus("Master password is required.", "error");
|
|
2268
|
+
return;
|
|
2269
|
+
}
|
|
2270
|
+
if (newPassword !== confirmPassword) {
|
|
2271
|
+
this.setChangePasswordStatus("Master password and confirmation do not match.", "error");
|
|
2272
|
+
return;
|
|
2273
|
+
}
|
|
2274
|
+
const passkeysMap = this.aggregatedDetails?.passkeys
|
|
2275
|
+
|| this.ibGib?.data?.frameDetails?.passkeys
|
|
2276
|
+
|| this.ibGib?.data?.passkeys
|
|
2277
|
+
|| {};
|
|
2278
|
+
const passkeysList = Array.isArray(passkeysMap)
|
|
2279
|
+
? passkeysMap
|
|
2280
|
+
: Object.values(passkeysMap);
|
|
2281
|
+
const passkeyPool = this.ibGib?.data?.challengePools?.find(p => p.id.startsWith('passkey-manage-'));
|
|
2282
|
+
if (!passkeyPool) {
|
|
2283
|
+
this.setChangePasswordStatus("No active passkey manage pool found on this identity.", "error");
|
|
2284
|
+
return;
|
|
2285
|
+
}
|
|
2286
|
+
let passkeySecret;
|
|
2287
|
+
let signingPoolId;
|
|
2288
|
+
try {
|
|
2289
|
+
const authResult = await promptForAuthoritySecret({
|
|
2290
|
+
keystone: this.ibGib,
|
|
2291
|
+
aggregatedDetails: this.aggregatedDetails,
|
|
2292
|
+
title: 'Authorize Master Password',
|
|
2293
|
+
msg: 'Please touch your fingerprint sensor, Face ID, or security key to authorize adding a Master Password...',
|
|
2294
|
+
verb: KEYSTONE_VERB_MANAGE
|
|
2295
|
+
});
|
|
2296
|
+
passkeySecret = authResult.secret;
|
|
2297
|
+
signingPoolId = authResult.poolId;
|
|
2298
|
+
}
|
|
2299
|
+
catch (err) {
|
|
2300
|
+
this.setChangePasswordStatus("Authorization cancelled.", "error");
|
|
2301
|
+
return;
|
|
2302
|
+
}
|
|
2303
|
+
this.showBusy({
|
|
2304
|
+
isBusy: true,
|
|
2305
|
+
title: 'Adding Master Password...',
|
|
2306
|
+
msg: 'Compiling cryptographic master password pool and evolving identity...',
|
|
2307
|
+
animationEmoji: '🔑'
|
|
2308
|
+
});
|
|
2309
|
+
await delay(50);
|
|
2310
|
+
this.setChangePasswordStatus("Generating sovereign master password challenge pool...", "info");
|
|
2311
|
+
const metaspace = await getGlobalMetaspace_waitIfNeeded();
|
|
2312
|
+
if (!metaspace)
|
|
2313
|
+
throw new Error("Global metaspace not available");
|
|
2314
|
+
const space = await metaspace.getLocalUserSpace({ lock: false });
|
|
2315
|
+
if (!space)
|
|
2316
|
+
throw new Error("Local user space not available");
|
|
2317
|
+
const poolConfig = {
|
|
2318
|
+
id: POOL_ID_MANAGE,
|
|
2319
|
+
type: KeystoneChallengeType.hash_reveal_v1,
|
|
2320
|
+
salt: POOL_ID_MANAGE,
|
|
2321
|
+
algo: HashAlgorithm.sha_256,
|
|
2322
|
+
rounds: 1,
|
|
2323
|
+
allowedVerbs: [KEYSTONE_VERB_MANAGE, KEYSTONE_VERB_SYNC, KEYSTONE_VERB_CONNECT],
|
|
2324
|
+
behavior: {
|
|
2325
|
+
size: 10,
|
|
2326
|
+
replenish: KeystoneReplenishStrategy.topUp,
|
|
2327
|
+
selectSequentially: 1,
|
|
2328
|
+
selectRandomly: 0,
|
|
2329
|
+
targetBindingCount: 0,
|
|
2330
|
+
}
|
|
2331
|
+
};
|
|
2332
|
+
const newChallenges = await generatePoolChallenges({
|
|
2333
|
+
config: poolConfig,
|
|
2334
|
+
masterSecret: newPassword,
|
|
2335
|
+
});
|
|
2336
|
+
const newManagePool = {
|
|
2337
|
+
id: POOL_ID_MANAGE,
|
|
2338
|
+
config: poolConfig,
|
|
2339
|
+
challenges: newChallenges,
|
|
2340
|
+
};
|
|
2341
|
+
const primaryTjpAddr = (this.ibGib ? getTjpAddr({ ibGib: this.ibGib }) : undefined)
|
|
2342
|
+
|| (this.ibGibAddr ? (() => {
|
|
2343
|
+
const { ib } = getIbAndGib({ ibGibAddr: this.ibGibAddr });
|
|
2344
|
+
const { tjpGib } = getGibInfo({ ibGibAddr: this.ibGibAddr });
|
|
2345
|
+
return tjpGib && ib ? `${ib}^${tjpGib}` : this.ibGibAddr;
|
|
2346
|
+
})() : undefined)
|
|
2347
|
+
|| getIbGibAddr({ ibGib: this.ibGib });
|
|
2348
|
+
const keystoneService = new KeystoneService_V1();
|
|
2349
|
+
const evolvedParent = await keystoneService.sign({
|
|
2350
|
+
latestKeystone: this.ibGib,
|
|
2351
|
+
claim: {
|
|
2352
|
+
target: primaryTjpAddr,
|
|
2353
|
+
verb: KEYSTONE_VERB_MANAGE
|
|
2354
|
+
},
|
|
2355
|
+
masterSecret: passkeySecret,
|
|
2356
|
+
poolId: signingPoolId,
|
|
2357
|
+
dataToPatch: {
|
|
2358
|
+
challengePools: [...(this.ibGib.data.challengePools || []), newManagePool]
|
|
2359
|
+
},
|
|
2360
|
+
metaspace,
|
|
2361
|
+
space
|
|
2362
|
+
});
|
|
2363
|
+
// Sync pool addition to server
|
|
2364
|
+
this.showBusy({
|
|
2365
|
+
isBusy: true,
|
|
2366
|
+
title: 'Syncing to Server...',
|
|
2367
|
+
msg: 'Publishing updated identity to server...',
|
|
2368
|
+
animationEmoji: '⚡'
|
|
2369
|
+
});
|
|
2370
|
+
const resSync = await getApiBridge().putEvolveKeystone({
|
|
2371
|
+
domainAddr: primaryTjpAddr,
|
|
2372
|
+
keystoneIbGib: evolvedParent,
|
|
2373
|
+
relatedKeystoneIbGibs: []
|
|
2374
|
+
});
|
|
2375
|
+
if (!resSync.success) {
|
|
2376
|
+
throw new Error(`Server sync failed: ${resSync.message}`);
|
|
2377
|
+
}
|
|
2378
|
+
// Save locally
|
|
2379
|
+
await metaspace.put({ ibGibs: [evolvedParent], space });
|
|
2380
|
+
await metaspace.registerNewIbGib({ ibGib: evolvedParent, space });
|
|
2381
|
+
// Delegate registration & final sync via DRY helper
|
|
2382
|
+
await this.registerAndSyncSovereignPasswordDelegate({
|
|
2383
|
+
evolvedParent,
|
|
2384
|
+
newPassword,
|
|
2385
|
+
primaryTjpAddr,
|
|
2386
|
+
metaspace,
|
|
2387
|
+
space
|
|
2388
|
+
});
|
|
2389
|
+
}
|
|
2390
|
+
catch (error) {
|
|
2391
|
+
console.error(`${lc} Failed to add password to passkey identity: ${extractErrorMsg(error)}`, error);
|
|
2392
|
+
this.setChangePasswordStatus(`Failed to add master password: ${extractErrorMsg(error)}`, "error");
|
|
1445
2393
|
}
|
|
1446
2394
|
finally {
|
|
1447
2395
|
this.showBusy({ isBusy: false });
|
|
@@ -1449,13 +2397,27 @@ export class KeystoneDetailsComponentInstance extends IbGibDynamicComponentInsta
|
|
|
1449
2397
|
}
|
|
1450
2398
|
async handleChangePassword() {
|
|
1451
2399
|
const lc = `${this.lc}[${this.handleChangePassword.name}]`;
|
|
1452
|
-
const caps =
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
2400
|
+
const caps = !!this.ibGib ?
|
|
2401
|
+
getKeystoneCapabilities({ keystone: this.ibGib, aggregatedDetails: this.aggregatedDetails }) :
|
|
2402
|
+
undefined;
|
|
2403
|
+
const hasPassword = !!caps?.hasPassword;
|
|
2404
|
+
if (!hasPassword) {
|
|
2405
|
+
const isCustodialOnly = !!caps ? caps.isCustodial && !caps.isSovereign : false;
|
|
2406
|
+
if (isCustodialOnly) {
|
|
2407
|
+
await this.handleAddMasterPassword();
|
|
2408
|
+
}
|
|
2409
|
+
else {
|
|
2410
|
+
await this.handleAddPasswordToPasskeyIdentity();
|
|
2411
|
+
}
|
|
1456
2412
|
return;
|
|
1457
2413
|
}
|
|
1458
2414
|
this.setChangePasswordStatus("Validating entries...", "info");
|
|
2415
|
+
this.showBusy({
|
|
2416
|
+
isBusy: true,
|
|
2417
|
+
title: 'Changing Master Password...',
|
|
2418
|
+
msg: 'Validating current password...',
|
|
2419
|
+
animationEmoji: '🔐'
|
|
2420
|
+
});
|
|
1459
2421
|
try {
|
|
1460
2422
|
const currentPassword = this.elements.inputCurrentPasswordEl.value;
|
|
1461
2423
|
const newPassword = this.elements.inputNewPasswordEl.value;
|
|
@@ -1483,6 +2445,12 @@ export class KeystoneDetailsComponentInstance extends IbGibDynamicComponentInsta
|
|
|
1483
2445
|
this.setChangePasswordStatus("Incorrect current password.", "error");
|
|
1484
2446
|
return;
|
|
1485
2447
|
}
|
|
2448
|
+
this.showBusy({
|
|
2449
|
+
isBusy: true,
|
|
2450
|
+
title: 'Evolving Master Password...',
|
|
2451
|
+
msg: 'Generating new challenge proofs and rotating local user delegates...',
|
|
2452
|
+
animationEmoji: '⚙️'
|
|
2453
|
+
});
|
|
1486
2454
|
this.setChangePasswordStatus("Evolving primary identity keystone...", "info");
|
|
1487
2455
|
const metaspace = await getGlobalMetaspace_waitIfNeeded();
|
|
1488
2456
|
if (!metaspace) {
|
|
@@ -1492,8 +2460,7 @@ export class KeystoneDetailsComponentInstance extends IbGibDynamicComponentInsta
|
|
|
1492
2460
|
if (!space) {
|
|
1493
2461
|
throw new Error(`(UNEXPECTED) couldn't get default local space? (E: 2fc1281c28351fd378a1d7f88eaa0526)`);
|
|
1494
2462
|
}
|
|
1495
|
-
// 2. Rotate parent identity
|
|
1496
|
-
debugger; // walk through this...looking for delegate failure // hmm...I thought we got change password going?
|
|
2463
|
+
// 2. Rotate parent identity and sovereign sync delegates
|
|
1497
2464
|
const { newKeystone: evolvedParent, newDelegates: evolvedDelegates } = await keystoneService.changePassword({
|
|
1498
2465
|
keystoneIbGib: this.ibGib,
|
|
1499
2466
|
newMasterSecret: newPassword,
|
|
@@ -1502,56 +2469,21 @@ export class KeystoneDetailsComponentInstance extends IbGibDynamicComponentInsta
|
|
|
1502
2469
|
metaspace,
|
|
1503
2470
|
space
|
|
1504
2471
|
});
|
|
1505
|
-
|
|
1506
|
-
this.
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
// const delegateKeystone = await keystoneService.getLatestKeystone({
|
|
1513
|
-
// addr: delegateAddr,
|
|
1514
|
-
// metaspace,
|
|
1515
|
-
// space,
|
|
1516
|
-
// });
|
|
1517
|
-
// // Identify user-owned challenge pools in the delegate that match the old password
|
|
1518
|
-
// const poolsToRotate: string[] = [];
|
|
1519
|
-
// for (const pool of delegateKeystone.data?.challengePools ?? []) {
|
|
1520
|
-
// if (!pool.isForeign) {
|
|
1521
|
-
// /**
|
|
1522
|
-
// * WARNING: There is an extreme edge case that if two pools
|
|
1523
|
-
// * happen to be owned by different parties but share the
|
|
1524
|
-
// * same secret, this will change the wrong pool's secret
|
|
1525
|
-
// * also. For V1, this shouldn't matter. But we need a
|
|
1526
|
-
// * better way of tracking pool ownership for V2 (or even
|
|
1527
|
-
// * V1 if we have multi-party support). (DO NOT REMOVE
|
|
1528
|
-
// * THIS COMMENT UNTIL THIS IS ADDRESSED)
|
|
1529
|
-
// */
|
|
1530
|
-
// const isPoolMatch = await keystoneService.verifySigningSecret({
|
|
1531
|
-
// keystoneIbGib: delegateKeystone,
|
|
1532
|
-
// signingSecret: currentPassword,
|
|
1533
|
-
// poolId: pool.id
|
|
1534
|
-
// });
|
|
1535
|
-
// if (isPoolMatch) { poolsToRotate.push(pool.id); }
|
|
1536
|
-
// }
|
|
1537
|
-
// }
|
|
1538
|
-
// if (poolsToRotate.length > 0) {
|
|
1539
|
-
// console.log(`${lc} Rotating pools in delegate ${delegateInfo.delegateTjpAddr}: ${poolsToRotate.join(', ')}`);
|
|
1540
|
-
// const {newKeystone: evolvedDelegate} = await keystoneService.changePassword({
|
|
1541
|
-
// keystoneIbGib: delegateKeystone,
|
|
1542
|
-
// newMasterSecret: newPassword,
|
|
1543
|
-
// signingSecret: currentPassword,
|
|
1544
|
-
// signingPoolId: 'manage',
|
|
1545
|
-
// poolIds: poolsToRotate,
|
|
1546
|
-
// metaspace,
|
|
1547
|
-
// space
|
|
1548
|
-
// });
|
|
1549
|
-
// evolvedDelegates.push(evolvedDelegate);
|
|
1550
|
-
// }
|
|
1551
|
-
// }
|
|
1552
|
-
// 4. Sync evolved parent and all evolved delegate keystones atomically
|
|
2472
|
+
// 3. Sync evolved parent and all evolved delegate keystones atomically
|
|
2473
|
+
this.showBusy({
|
|
2474
|
+
isBusy: true,
|
|
2475
|
+
title: 'Syncing to Server...',
|
|
2476
|
+
msg: 'Publishing rotated identity and delegates to server...',
|
|
2477
|
+
animationEmoji: '⚡'
|
|
2478
|
+
});
|
|
1553
2479
|
this.setChangePasswordStatus("Syncing evolved keystones to server...", "info");
|
|
1554
|
-
const parentTjpAddr =
|
|
2480
|
+
const parentTjpAddr = (this.ibGib ? getTjpAddr({ ibGib: this.ibGib }) : undefined)
|
|
2481
|
+
|| (this.ibGibAddr ? (() => {
|
|
2482
|
+
const { ib } = getIbAndGib({ ibGibAddr: this.ibGibAddr });
|
|
2483
|
+
const { tjpGib } = getGibInfo({ ibGibAddr: this.ibGibAddr });
|
|
2484
|
+
return tjpGib && ib ? `${ib}^${tjpGib}` : this.ibGibAddr;
|
|
2485
|
+
})() : undefined)
|
|
2486
|
+
|| getIbGibAddr({ ibGib: this.ibGib });
|
|
1555
2487
|
const syncRes = await getApiBridge().putEvolveKeystone({
|
|
1556
2488
|
domainAddr: parentTjpAddr,
|
|
1557
2489
|
keystoneIbGib: evolvedParent,
|
|
@@ -1560,7 +2492,7 @@ export class KeystoneDetailsComponentInstance extends IbGibDynamicComponentInsta
|
|
|
1560
2492
|
if (!syncRes.success) {
|
|
1561
2493
|
throw new Error(`Server synchronization rejected: ${syncRes.message}`);
|
|
1562
2494
|
}
|
|
1563
|
-
//
|
|
2495
|
+
// 4. Update index locally
|
|
1564
2496
|
this.setChangePasswordStatus("Updating local indexes...", "info");
|
|
1565
2497
|
const newKeystoneIbGibs = [toDto({ ibGib: evolvedParent })];
|
|
1566
2498
|
if (evolvedDelegates) {
|
|
@@ -1585,12 +2517,18 @@ export class KeystoneDetailsComponentInstance extends IbGibDynamicComponentInsta
|
|
|
1585
2517
|
this.elements.inputNewPasswordEl.value = "";
|
|
1586
2518
|
this.elements.inputConfirmPasswordEl.value = "";
|
|
1587
2519
|
this.setChangePasswordStatus("Password updated successfully!", "success");
|
|
2520
|
+
this.ibGibAddr = getIbGibAddr({ ibGib: evolvedParent });
|
|
2521
|
+
this.staticIbGib = evolvedParent;
|
|
1588
2522
|
await this.loadIbGib({ getLatest: true });
|
|
1589
2523
|
}
|
|
1590
2524
|
catch (error) {
|
|
2525
|
+
debugger; // error in handleChangePassword
|
|
1591
2526
|
console.error(`${lc} Password rotation failed: ${extractErrorMsg(error)}`);
|
|
1592
2527
|
this.setChangePasswordStatus(`Update failed: ${extractErrorMsg(error)}`, "error");
|
|
1593
2528
|
}
|
|
2529
|
+
finally {
|
|
2530
|
+
this.showBusy({ isBusy: false });
|
|
2531
|
+
}
|
|
1594
2532
|
}
|
|
1595
2533
|
}
|
|
1596
2534
|
//# sourceMappingURL=keystone-details.mjs.map
|