@ibgib/web-gib 0.0.60 → 0.0.62
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/AUTO-GENERATED-version.d.mts +1 -1
- package/dist/AUTO-GENERATED-version.mjs +1 -1
- package/dist/helpers.d.mts.map +1 -1
- package/dist/helpers.mjs +1 -0
- package/dist/helpers.mjs.map +1 -1
- package/dist/helpers.web.d.mts +3 -1
- package/dist/helpers.web.d.mts.map +1 -1
- package/dist/helpers.web.mjs +17 -1
- package/dist/helpers.web.mjs.map +1 -1
- package/dist/identity/device-delegate-helper.d.mts +91 -0
- package/dist/identity/device-delegate-helper.d.mts.map +1 -0
- package/dist/identity/device-delegate-helper.mjs +190 -0
- package/dist/identity/device-delegate-helper.mjs.map +1 -0
- package/dist/identity/device-delegate.respec.d.mts +2 -0
- package/dist/identity/device-delegate.respec.d.mts.map +1 -0
- package/dist/identity/device-delegate.respec.mjs +106 -0
- package/dist/identity/device-delegate.respec.mjs.map +1 -0
- package/dist/identity/keystone-sync-service.d.mts +18 -0
- package/dist/identity/keystone-sync-service.d.mts.map +1 -1
- package/dist/identity/keystone-sync-service.mjs +77 -1
- package/dist/identity/keystone-sync-service.mjs.map +1 -1
- package/dist/identity/passkey-helper.d.mts +72 -0
- package/dist/identity/passkey-helper.d.mts.map +1 -0
- package/dist/identity/passkey-helper.mjs +237 -0
- package/dist/identity/passkey-helper.mjs.map +1 -0
- package/dist/identity/passkey.respec.d.mts +2 -0
- package/dist/identity/passkey.respec.d.mts.map +1 -0
- package/dist/identity/passkey.respec.mjs +169 -0
- package/dist/identity/passkey.respec.mjs.map +1 -0
- package/dist/index.d.mts +2 -1
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +2 -2
- package/dist/index.mjs.map +1 -1
- package/dist/storage/storage-helpers.web.d.mts +9 -0
- package/dist/storage/storage-helpers.web.d.mts.map +1 -1
- package/dist/storage/storage-helpers.web.mjs +42 -0
- package/dist/storage/storage-helpers.web.mjs.map +1 -1
- package/dist/ui/component/identity/add-device/add-device.d.mts.map +1 -1
- package/dist/ui/component/identity/add-device/add-device.mjs +31 -43
- package/dist/ui/component/identity/add-device/add-device.mjs.map +1 -1
- package/dist/ui/component/identity/keystone-creator/keystone-creator.d.mts +18 -1
- package/dist/ui/component/identity/keystone-creator/keystone-creator.d.mts.map +1 -1
- package/dist/ui/component/identity/keystone-creator/keystone-creator.html +42 -3
- package/dist/ui/component/identity/keystone-creator/keystone-creator.mjs +524 -104
- package/dist/ui/component/identity/keystone-creator/keystone-creator.mjs.map +1 -1
- package/dist/ui/component/identity/keystone-details/keystone-details.d.mts +19 -1
- package/dist/ui/component/identity/keystone-details/keystone-details.d.mts.map +1 -1
- package/dist/ui/component/identity/keystone-details/keystone-details.html +38 -0
- package/dist/ui/component/identity/keystone-details/keystone-details.mjs +1217 -132
- package/dist/ui/component/identity/keystone-details/keystone-details.mjs.map +1 -1
- package/dist/ui/component/identity/remove-device/remove-device.d.mts.map +1 -1
- package/dist/ui/component/identity/remove-device/remove-device.mjs +1 -0
- package/dist/ui/component/identity/remove-device/remove-device.mjs.map +1 -1
- package/dist/ui/ui-constants.d.mts +1 -0
- package/dist/ui/ui-constants.d.mts.map +1 -1
- package/dist/ui/ui-constants.mjs +1 -0
- package/dist/ui/ui-constants.mjs.map +1 -1
- package/dist/ui/ui-helpers.d.mts +3 -0
- package/dist/ui/ui-helpers.d.mts.map +1 -1
- package/dist/ui/ui-helpers.mjs +110 -6
- package/dist/ui/ui-helpers.mjs.map +1 -1
- package/dist/ui/ui-types.d.mts +5 -0
- package/dist/ui/ui-types.d.mts.map +1 -1
- package/package.json +2 -2
- package/src/AUTO-GENERATED-version.mts +1 -1
- package/src/helpers.mts +1 -0
- package/src/helpers.web.mts +22 -2
- package/src/identity/custodian-delegate-helper.mts.bak +7 -0
- package/src/identity/device-delegate-helper.mts +268 -0
- package/src/identity/device-delegate.respec.mts +125 -0
- package/src/identity/keystone-sync-service.mts +95 -2
- package/src/identity/passkey-helper.mts +304 -0
- package/src/identity/passkey.respec.mts +194 -0
- package/src/index.mts +2 -2
- package/src/storage/storage-helpers.web.mts +47 -0
- package/src/ui/component/identity/add-device/add-device.mts +37 -48
- package/src/ui/component/identity/keystone-creator/keystone-creator.html +42 -3
- package/src/ui/component/identity/keystone-creator/keystone-creator.mts +568 -111
- package/src/ui/component/identity/keystone-details/keystone-details.html +38 -0
- package/src/ui/component/identity/keystone-details/keystone-details.mts +1290 -142
- package/src/ui/component/identity/remove-device/remove-device.mts +1 -0
- package/src/ui/ui-constants.mts +1 -0
- package/src/ui/ui-helpers.mts +118 -5
- package/src/ui/ui-types.mts +6 -0
- package/dist/identity/custodian-delegate-helper.d.mts +0 -22
- package/dist/identity/custodian-delegate-helper.d.mts.map +0 -1
- package/dist/identity/custodian-delegate-helper.mjs +0 -67
- package/dist/identity/custodian-delegate-helper.mjs.map +0 -1
- package/src/identity/custodian-delegate-helper.mts +0 -86
|
@@ -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 } 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,13 +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
17
|
import { promptForSecret } from "../../../../helpers.web.mjs";
|
|
18
|
-
import { isSsoProviderLinked } from "
|
|
18
|
+
import { isSsoProviderLinked, getKeystoneCapabilities, isDelegateExpired, isCustodialDelegate } from "@ibgib/core-gib/dist/keystone/delegate/delegate-helpers.mjs";
|
|
19
|
+
import { generatePoolChallenges } from "@ibgib/core-gib/dist/keystone/keystone-helpers.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";
|
|
19
22
|
import { getGlobalMetaspace_waitIfNeeded } from "../../../../helpers.mjs";
|
|
23
|
+
import { createSovereignDeviceSyncDelegate, createOAuth2DeviceSyncDelegate } from "../../../../identity/device-delegate-helper.mjs";
|
|
24
|
+
import { isPasskeyPrfSupported, registerPasskeyCredential, evaluatePasskeyPrf, bufferToHex } from "../../../../identity/passkey-helper.mjs";
|
|
25
|
+
import { KeystoneChallengeType, KeystoneReplenishStrategy } from "@ibgib/core-gib/dist/keystone/keystone-types.mjs";
|
|
20
26
|
const logalot = false;
|
|
21
27
|
export const KEYSTONE_DETAILS_COMPONENT_NAME = 'ibgib-keystone-details';
|
|
22
28
|
export class KeystoneDetailsComponentMeta extends IbGibDynamicComponentMetaBase {
|
|
@@ -51,6 +57,8 @@ export class KeystoneDetailsComponentInstance extends IbGibDynamicComponentInsta
|
|
|
51
57
|
isHistoricalFrame = false;
|
|
52
58
|
_onIdentityChanged = null;
|
|
53
59
|
keystoneCache = new Map();
|
|
60
|
+
aggregatedDetails;
|
|
61
|
+
resendCooldownInterval = null;
|
|
54
62
|
constructor() {
|
|
55
63
|
super();
|
|
56
64
|
}
|
|
@@ -74,6 +82,39 @@ export class KeystoneDetailsComponentInstance extends IbGibDynamicComponentInsta
|
|
|
74
82
|
}
|
|
75
83
|
}
|
|
76
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
|
+
}
|
|
77
118
|
async getKeystoneDetails(addr) {
|
|
78
119
|
const cached = this.keystoneCache.get(addr);
|
|
79
120
|
if (cached)
|
|
@@ -139,6 +180,10 @@ export class KeystoneDetailsComponentInstance extends IbGibDynamicComponentInsta
|
|
|
139
180
|
this.elements.rawKeystoneJsonEl = shadow.getElementById('raw-keystone-json');
|
|
140
181
|
this.elements.delegatesCardEl = shadow.getElementById('delegates-card');
|
|
141
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.passkeyStatusMsgEl = shadow.getElementById('passkey-status-msg');
|
|
142
187
|
this.elements.containerEl = shadow.getElementById('container');
|
|
143
188
|
this.elements.historicalWarningBannerEl = shadow.getElementById('historical-warning-banner');
|
|
144
189
|
this.elements.ssoLinkCardEl = shadow.getElementById('sso-link-card');
|
|
@@ -151,6 +196,10 @@ export class KeystoneDetailsComponentInstance extends IbGibDynamicComponentInsta
|
|
|
151
196
|
this.elements.btnChangePasswordEl = shadow.getElementById('btn-change-password');
|
|
152
197
|
this.elements.changePasswordStatusEl = shadow.getElementById('change-password-status');
|
|
153
198
|
this.elements.changePasswordCardEl = shadow.getElementById('change-password-card');
|
|
199
|
+
this.elements.sectionAddPasswordOtpEl = shadow.getElementById('section-add-password-otp');
|
|
200
|
+
this.elements.inputAddPasswordOtpEl = shadow.getElementById('input-add-password-otp');
|
|
201
|
+
this.elements.btnResendAddPasswordOtpEl = shadow.getElementById('btn-resend-add-password-otp');
|
|
202
|
+
this.elements.descAddPasswordOtpEl = shadow.getElementById('desc-add-password-otp');
|
|
154
203
|
// ElementBase requires contentEl
|
|
155
204
|
this.elements.contentEl = shadow.getElementById('identity-details-view');
|
|
156
205
|
this.initHandlers();
|
|
@@ -175,11 +224,6 @@ export class KeystoneDetailsComponentInstance extends IbGibDynamicComponentInsta
|
|
|
175
224
|
}
|
|
176
225
|
}
|
|
177
226
|
}
|
|
178
|
-
async disconnected() {
|
|
179
|
-
if (this._onIdentityChanged) {
|
|
180
|
-
window.removeEventListener(EVENT_IBGIB_IDENTITY_CHANGED, this._onIdentityChanged);
|
|
181
|
-
}
|
|
182
|
-
}
|
|
183
227
|
initHandlers() {
|
|
184
228
|
// Setup raw JSON accordion toggle
|
|
185
229
|
this.elements.rawAccordionHeaderEl.addEventListener('click', () => this.toggleAccordion());
|
|
@@ -188,9 +232,11 @@ export class KeystoneDetailsComponentInstance extends IbGibDynamicComponentInsta
|
|
|
188
232
|
this.elements.btnSetActiveEl.addEventListener('click', () => this.handleSetActiveIdentity());
|
|
189
233
|
this.elements.btnCheckUpdatesEl.addEventListener('click', () => this.handleCheckForUpdates());
|
|
190
234
|
this.elements.btnUpdateIdentityEl.addEventListener('click', () => this.handleUpdateIdentity());
|
|
235
|
+
this.elements.btnAddPasskeyEl?.addEventListener('click', () => this.handleAddPasskey());
|
|
191
236
|
this.elements.btnLinkGoogleEl.addEventListener('click', () => this.handleSSOClick('google'));
|
|
192
237
|
this.elements.btnLinkGithubEl.addEventListener('click', () => this.handleSSOClick('github'));
|
|
193
238
|
this.elements.btnChangePasswordEl.addEventListener('click', () => this.handleChangePassword());
|
|
239
|
+
this.elements.btnResendAddPasswordOtpEl?.addEventListener('click', () => this.handleResendAddPasswordOtp());
|
|
194
240
|
}
|
|
195
241
|
toggleAccordion() {
|
|
196
242
|
const header = this.elements.rawAccordionHeaderEl;
|
|
@@ -394,34 +440,112 @@ export class KeystoneDetailsComponentInstance extends IbGibDynamicComponentInsta
|
|
|
394
440
|
this.elements.btnSetActiveEl.disabled = isTimelineActive;
|
|
395
441
|
this.elements.btnLinkGoogleEl.classList.remove('disabled');
|
|
396
442
|
this.elements.btnLinkGithubEl.classList.remove('disabled');
|
|
397
|
-
// Check if
|
|
398
|
-
const
|
|
399
|
-
const
|
|
443
|
+
// Check if origin provider or linked
|
|
444
|
+
const originProvider = this.aggregatedDetails?.custodianProvider || this.ibGib?.data?.frameDetails?.custodianProvider;
|
|
445
|
+
const isGoogleLinked = !!this.ibGib ?
|
|
446
|
+
isSsoProviderLinked({ keystone: this.ibGib, aggregatedDetails: this.aggregatedDetails, providerId: 'google' }) :
|
|
447
|
+
false;
|
|
448
|
+
const isGithubLinked = !!this.ibGib ?
|
|
449
|
+
isSsoProviderLinked({ keystone: this.ibGib, aggregatedDetails: this.aggregatedDetails, providerId: 'github' }) :
|
|
450
|
+
false;
|
|
451
|
+
const caps = !!this.ibGib ?
|
|
452
|
+
getKeystoneCapabilities({ keystone: this.ibGib, aggregatedDetails: this.aggregatedDetails }) :
|
|
453
|
+
undefined;
|
|
400
454
|
// Google Card States
|
|
401
455
|
const googleCard = this.elements.btnLinkGoogleEl;
|
|
402
456
|
const googleLabel = googleCard.querySelector('.toggle-state-text');
|
|
403
457
|
if (isGoogleLinked) {
|
|
458
|
+
if (!caps) {
|
|
459
|
+
throw new Error(`(UNEXPECTED) isGoogleLinked but capabilities falsy? (E: cc62e8357b66860dd2cb8148c833b726)`);
|
|
460
|
+
}
|
|
404
461
|
googleCard.classList.remove('unlinked');
|
|
405
462
|
googleCard.classList.add('linked');
|
|
406
|
-
|
|
463
|
+
const isOrigin = originProvider === 'google';
|
|
464
|
+
if (isOrigin && !caps.isSovereign) {
|
|
465
|
+
googleLabel.textContent = "Google (Primary Account) 🔒";
|
|
466
|
+
googleCard.title = "Cannot unlink primary account without first adding a Master Password below.";
|
|
467
|
+
}
|
|
468
|
+
else if (isOrigin) {
|
|
469
|
+
googleLabel.textContent = "Google (Primary Account) ✅";
|
|
470
|
+
googleCard.title = "Click to unlink Google provider (requires Master Password).";
|
|
471
|
+
}
|
|
472
|
+
else {
|
|
473
|
+
googleLabel.textContent = "Google Linked ✅";
|
|
474
|
+
googleCard.title = "Click to unlink Google provider.";
|
|
475
|
+
}
|
|
407
476
|
}
|
|
408
477
|
else {
|
|
409
478
|
googleCard.classList.remove('linked');
|
|
410
479
|
googleCard.classList.add('unlinked');
|
|
411
480
|
googleLabel.textContent = "Unlinked";
|
|
481
|
+
googleCard.title = "Click to link Google account.";
|
|
412
482
|
}
|
|
413
483
|
// GitHub Card States
|
|
414
484
|
const githubCard = this.elements.btnLinkGithubEl;
|
|
415
485
|
const githubLabel = githubCard.querySelector('.toggle-state-text');
|
|
416
486
|
if (isGithubLinked) {
|
|
487
|
+
if (!caps) {
|
|
488
|
+
throw new Error(`(UNEXPECTED) isGithubLinked but capabilities falsy? (E: e94688210538e0e82878088873a46826)`);
|
|
489
|
+
}
|
|
417
490
|
githubCard.classList.remove('unlinked');
|
|
418
491
|
githubCard.classList.add('linked');
|
|
419
|
-
|
|
492
|
+
const isOrigin = originProvider === 'github';
|
|
493
|
+
if (isOrigin && !caps.isSovereign) {
|
|
494
|
+
githubLabel.textContent = "GitHub (Primary Account) 🔒";
|
|
495
|
+
githubCard.title = "Cannot unlink primary account without first adding a Master Password below.";
|
|
496
|
+
}
|
|
497
|
+
else if (isOrigin) {
|
|
498
|
+
githubLabel.textContent = "GitHub (Primary Account) ✅";
|
|
499
|
+
githubCard.title = "Click to unlink GitHub provider (requires Master Password).";
|
|
500
|
+
}
|
|
501
|
+
else {
|
|
502
|
+
githubLabel.textContent = "GitHub Linked ✅";
|
|
503
|
+
githubCard.title = "Click to unlink GitHub provider.";
|
|
504
|
+
}
|
|
420
505
|
}
|
|
421
506
|
else {
|
|
422
507
|
githubCard.classList.remove('linked');
|
|
423
508
|
githubCard.classList.add('unlinked');
|
|
424
509
|
githubLabel.textContent = "Unlinked";
|
|
510
|
+
githubCard.title = "Click to link GitHub account.";
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
updatePasswordCardView() {
|
|
515
|
+
const caps = !!this.ibGib ?
|
|
516
|
+
getKeystoneCapabilities({ keystone: this.ibGib, aggregatedDetails: this.aggregatedDetails }) :
|
|
517
|
+
false;
|
|
518
|
+
const cardTitleEl = this.elements?.changePasswordCardEl?.querySelector('h3');
|
|
519
|
+
const cardDescEl = this.elements?.changePasswordCardEl?.querySelector('.description');
|
|
520
|
+
const currentPasswordGroup = this.elements?.inputCurrentPasswordEl?.closest('.form-group');
|
|
521
|
+
const btnChangePassword = this.elements?.btnChangePasswordEl;
|
|
522
|
+
const isCustodialOnly = !!caps ?
|
|
523
|
+
caps.isCustodial && !caps.isSovereign :
|
|
524
|
+
false;
|
|
525
|
+
if (isCustodialOnly) {
|
|
526
|
+
if (cardTitleEl)
|
|
527
|
+
cardTitleEl.textContent = "Add Master Password";
|
|
528
|
+
if (cardDescEl)
|
|
529
|
+
cardDescEl.textContent = "Establish sovereign ownership by adding a master password. This enables direct, independent cryptographic management without relying solely on custodian authentication.";
|
|
530
|
+
if (currentPasswordGroup)
|
|
531
|
+
currentPasswordGroup.style.display = 'none';
|
|
532
|
+
if (btnChangePassword)
|
|
533
|
+
btnChangePassword.textContent = "Add Master Password";
|
|
534
|
+
}
|
|
535
|
+
else {
|
|
536
|
+
if (cardTitleEl)
|
|
537
|
+
cardTitleEl.textContent = "Change Password";
|
|
538
|
+
if (cardDescEl)
|
|
539
|
+
cardDescEl.textContent = "Rotate your local password that drives this identity. This will cryptographically evolve your primary identity and all registered user-owned delegate keystones to the new password.";
|
|
540
|
+
if (currentPasswordGroup)
|
|
541
|
+
currentPasswordGroup.style.display = '';
|
|
542
|
+
if (btnChangePassword)
|
|
543
|
+
btnChangePassword.textContent = "Update Password";
|
|
544
|
+
}
|
|
545
|
+
if (this.elements?.sectionAddPasswordOtpEl) {
|
|
546
|
+
this.elements.sectionAddPasswordOtpEl.classList.add('hidden');
|
|
547
|
+
if (this.elements.inputAddPasswordOtpEl) {
|
|
548
|
+
this.elements.inputAddPasswordOtpEl.value = '';
|
|
425
549
|
}
|
|
426
550
|
}
|
|
427
551
|
}
|
|
@@ -484,6 +608,7 @@ export class KeystoneDetailsComponentInstance extends IbGibDynamicComponentInsta
|
|
|
484
608
|
metaspace,
|
|
485
609
|
space
|
|
486
610
|
});
|
|
611
|
+
this.aggregatedDetails = aggregated;
|
|
487
612
|
this.elements.identityAggrDetailsEl.textContent = aggregated ? pretty(aggregated) : '{}';
|
|
488
613
|
// Update Username and Description fields
|
|
489
614
|
this.elements.identityNameEl.textContent = aggregated?.username || aggregated?.profile?.name || aggregated?.name || '-';
|
|
@@ -491,18 +616,21 @@ export class KeystoneDetailsComponentInstance extends IbGibDynamicComponentInsta
|
|
|
491
616
|
}
|
|
492
617
|
catch (err) {
|
|
493
618
|
console.warn(`${lc} Error aggregating details: ${extractErrorMsg(err)}`);
|
|
619
|
+
this.aggregatedDetails = undefined;
|
|
494
620
|
this.elements.identityAggrDetailsEl.textContent = '{}';
|
|
495
621
|
this.elements.identityNameEl.textContent = '-';
|
|
496
622
|
this.elements.identityDescriptionEl.textContent = '-';
|
|
497
623
|
}
|
|
498
624
|
}
|
|
499
625
|
else {
|
|
626
|
+
this.aggregatedDetails = undefined;
|
|
500
627
|
this.elements.identityAggrDetailsEl.textContent = '{}';
|
|
501
628
|
this.elements.identityNameEl.textContent = '-';
|
|
502
629
|
this.elements.identityDescriptionEl.textContent = '-';
|
|
503
630
|
}
|
|
504
631
|
}
|
|
505
632
|
else {
|
|
633
|
+
this.aggregatedDetails = undefined;
|
|
506
634
|
this.elements.identityAggrDetailsEl.textContent = '{}';
|
|
507
635
|
this.elements.identityNameEl.textContent = '-';
|
|
508
636
|
this.elements.identityDescriptionEl.textContent = '-';
|
|
@@ -525,16 +653,20 @@ export class KeystoneDetailsComponentInstance extends IbGibDynamicComponentInsta
|
|
|
525
653
|
this.elements.rawKeystoneJsonEl.textContent = pretty(keystone);
|
|
526
654
|
// Render Challenge Pools
|
|
527
655
|
this.renderChallengePools(keystone.data?.challengePools || []);
|
|
528
|
-
// Render Delegates, SSO link, and Password Change cards (only if this is a primary keystone, hide them for delegate/session keystones)
|
|
656
|
+
// Render Delegates, Passkeys, SSO link, and Password Change cards (only if this is a primary keystone, hide them for delegate/session keystones)
|
|
529
657
|
const details = await this.getKeystoneDetails(this.ibGibAddr);
|
|
530
658
|
if (details.isPrimary) {
|
|
531
659
|
this.elements.delegatesCardEl.classList.remove('hidden');
|
|
532
660
|
await this.renderDelegates(keystone.data?.delegates);
|
|
661
|
+
this.elements.passkeysCardEl.classList.remove('hidden');
|
|
662
|
+
await this.renderPasskeys(this.aggregatedDetails?.passkeys);
|
|
533
663
|
this.elements.ssoLinkCardEl.classList.remove('hidden');
|
|
534
664
|
this.elements.changePasswordCardEl.classList.remove('hidden');
|
|
665
|
+
this.updatePasswordCardView();
|
|
535
666
|
}
|
|
536
667
|
else {
|
|
537
668
|
this.elements.delegatesCardEl.classList.add('hidden');
|
|
669
|
+
this.elements.passkeysCardEl.classList.add('hidden');
|
|
538
670
|
this.elements.ssoLinkCardEl.classList.add('hidden');
|
|
539
671
|
this.elements.changePasswordCardEl.classList.add('hidden');
|
|
540
672
|
}
|
|
@@ -554,6 +686,7 @@ export class KeystoneDetailsComponentInstance extends IbGibDynamicComponentInsta
|
|
|
554
686
|
if (!this.elements) {
|
|
555
687
|
return;
|
|
556
688
|
}
|
|
689
|
+
this.aggregatedDetails = undefined;
|
|
557
690
|
this.isHistoricalFrame = false;
|
|
558
691
|
if (this.elements.historicalWarningBannerEl) {
|
|
559
692
|
this.elements.historicalWarningBannerEl.classList.add('hidden');
|
|
@@ -572,6 +705,15 @@ export class KeystoneDetailsComponentInstance extends IbGibDynamicComponentInsta
|
|
|
572
705
|
if (this.elements.delegatesListEl) {
|
|
573
706
|
this.elements.delegatesListEl.innerHTML = '';
|
|
574
707
|
}
|
|
708
|
+
if (this.elements.passkeysListEl) {
|
|
709
|
+
this.elements.passkeysListEl.innerHTML = '';
|
|
710
|
+
}
|
|
711
|
+
if (this.elements.sectionAddPasswordOtpEl) {
|
|
712
|
+
this.elements.sectionAddPasswordOtpEl.classList.add('hidden');
|
|
713
|
+
if (this.elements.inputAddPasswordOtpEl) {
|
|
714
|
+
this.elements.inputAddPasswordOtpEl.value = '';
|
|
715
|
+
}
|
|
716
|
+
}
|
|
575
717
|
}
|
|
576
718
|
renderChallengePools(pools) {
|
|
577
719
|
if (!this.elements) {
|
|
@@ -730,39 +872,66 @@ export class KeystoneDetailsComponentInstance extends IbGibDynamicComponentInsta
|
|
|
730
872
|
return;
|
|
731
873
|
}
|
|
732
874
|
const metaspace = await getGlobalMetaspace_waitIfNeeded();
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
const space = await metaspace.getLocalUserSpace({ lock: false });
|
|
736
|
-
if (space?.ib) {
|
|
737
|
-
localSpaceId = parseSpaceIb({ spaceIb: space.ib }).spaceId;
|
|
738
|
-
}
|
|
875
|
+
if (!metaspace) {
|
|
876
|
+
throw new Error(`(UNEXPECTED) metaspace falsy? (E: 8a9b0c1d2e3f4a5b6c7d8e9f0a1b2c3e)`);
|
|
739
877
|
}
|
|
878
|
+
const space = await metaspace.getLocalUserSpace({ lock: false });
|
|
879
|
+
if (!space) {
|
|
880
|
+
throw new Error(`(UNEXPECTED) space falsy? (E: 9b0c1d2e3f4a5b6c7d8e9f0a1b2c3d4f)`);
|
|
881
|
+
}
|
|
882
|
+
const localSpaceId = space.ib ? parseSpaceIb({ spaceIb: space.ib }).spaceId : undefined;
|
|
740
883
|
for (const [delegateTjpAddr, info] of Object.entries(delegates)) {
|
|
741
884
|
const tr = document.createElement('tr');
|
|
742
885
|
const deviceName = info.deviceName || info.details?.deviceTag || info.details?.name || 'Device Delegate';
|
|
743
886
|
const tdName = document.createElement('td');
|
|
744
887
|
const delegateSpaceId = info.spaceId || info.details?.spaceId;
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
badgeEl.textContent = 'This Device';
|
|
755
|
-
badgeEl.className = 'badge badge-this-device';
|
|
756
|
-
badgeEl.style.backgroundColor = '#1b5e20';
|
|
757
|
-
badgeEl.style.color = '#a5d6a7';
|
|
888
|
+
// Check if delegate is present in local space (via spaceId match or direct presence check)
|
|
889
|
+
let isThisDevice = false;
|
|
890
|
+
if (localSpaceId && delegateSpaceId === localSpaceId) {
|
|
891
|
+
isThisDevice = true;
|
|
892
|
+
}
|
|
893
|
+
else {
|
|
894
|
+
try {
|
|
895
|
+
const resGet = await metaspace.get({ addrs: [delegateTjpAddr], space });
|
|
896
|
+
isThisDevice = !!(resGet.success && resGet.ibGibs && resGet.ibGibs.length > 0);
|
|
758
897
|
}
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
badgeEl.className = 'badge badge-remote-device';
|
|
762
|
-
badgeEl.style.backgroundColor = '#0d47a1';
|
|
763
|
-
badgeEl.style.color = '#90caf9';
|
|
898
|
+
catch {
|
|
899
|
+
isThisDevice = false;
|
|
764
900
|
}
|
|
765
901
|
}
|
|
902
|
+
const badgeEl = document.createElement('span');
|
|
903
|
+
badgeEl.style.marginLeft = '0.5rem';
|
|
904
|
+
badgeEl.style.padding = '0.15rem 0.4rem';
|
|
905
|
+
badgeEl.style.borderRadius = '4px';
|
|
906
|
+
badgeEl.style.fontSize = '0.75rem';
|
|
907
|
+
badgeEl.style.fontWeight = 'bold';
|
|
908
|
+
if (isThisDevice) {
|
|
909
|
+
badgeEl.textContent = 'This Device';
|
|
910
|
+
badgeEl.className = 'badge badge-this-device';
|
|
911
|
+
badgeEl.style.backgroundColor = '#1b5e20';
|
|
912
|
+
badgeEl.style.color = '#a5d6a7';
|
|
913
|
+
}
|
|
914
|
+
else {
|
|
915
|
+
badgeEl.textContent = 'Remote Device';
|
|
916
|
+
badgeEl.className = 'badge badge-remote-device';
|
|
917
|
+
badgeEl.style.backgroundColor = '#0d47a1';
|
|
918
|
+
badgeEl.style.color = '#90caf9';
|
|
919
|
+
}
|
|
920
|
+
// Check expiration
|
|
921
|
+
const isExpired = isDelegateExpired({ delegateInfo: info });
|
|
922
|
+
let expiredBadgeEl = null;
|
|
923
|
+
if (isExpired) {
|
|
924
|
+
expiredBadgeEl = document.createElement('span');
|
|
925
|
+
expiredBadgeEl.textContent = 'Expired';
|
|
926
|
+
expiredBadgeEl.className = 'badge badge-expired';
|
|
927
|
+
expiredBadgeEl.style.marginLeft = '0.3rem';
|
|
928
|
+
expiredBadgeEl.style.padding = '0.15rem 0.4rem';
|
|
929
|
+
expiredBadgeEl.style.borderRadius = '4px';
|
|
930
|
+
expiredBadgeEl.style.fontSize = '0.75rem';
|
|
931
|
+
expiredBadgeEl.style.fontWeight = 'bold';
|
|
932
|
+
expiredBadgeEl.style.backgroundColor = '#b71c1c';
|
|
933
|
+
expiredBadgeEl.style.color = '#ef9a9a';
|
|
934
|
+
}
|
|
766
935
|
const applyNameAndBadge = (displayName, hoverText) => {
|
|
767
936
|
tdName.innerHTML = '';
|
|
768
937
|
const textSpan = document.createElement('span');
|
|
@@ -771,6 +940,9 @@ export class KeystoneDetailsComponentInstance extends IbGibDynamicComponentInsta
|
|
|
771
940
|
if (badgeEl) {
|
|
772
941
|
tdName.appendChild(badgeEl);
|
|
773
942
|
}
|
|
943
|
+
if (expiredBadgeEl) {
|
|
944
|
+
tdName.appendChild(expiredBadgeEl);
|
|
945
|
+
}
|
|
774
946
|
tdName.title = hoverText || displayName;
|
|
775
947
|
};
|
|
776
948
|
const displayName = `💻 ${deviceName}`;
|
|
@@ -781,7 +953,7 @@ export class KeystoneDetailsComponentInstance extends IbGibDynamicComponentInsta
|
|
|
781
953
|
const tdVerbs = document.createElement('td');
|
|
782
954
|
tdVerbs.textContent = info.allowedVerbs ? info.allowedVerbs.join(', ') : '-';
|
|
783
955
|
tdVerbs.title = tdVerbs.textContent;
|
|
784
|
-
const isRemote =
|
|
956
|
+
const isRemote = !isThisDevice;
|
|
785
957
|
const tdActions = document.createElement('td');
|
|
786
958
|
const btnView = document.createElement('button');
|
|
787
959
|
btnView.className = 'action-btn secondary small';
|
|
@@ -792,22 +964,22 @@ export class KeystoneDetailsComponentInstance extends IbGibDynamicComponentInsta
|
|
|
792
964
|
this.showRemoteDelegateModal(info);
|
|
793
965
|
}
|
|
794
966
|
else {
|
|
795
|
-
if (info.delegateAddr) {
|
|
967
|
+
if (info.delegateAddr || delegateTjpAddr) {
|
|
796
968
|
let addrToLoad = info.delegateAddr;
|
|
797
|
-
|
|
798
|
-
const
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
}
|
|
806
|
-
catch (err) {
|
|
807
|
-
console.warn(`${lc} Error resolving latest delegate address: ${extractErrorMsg(err)}`);
|
|
808
|
-
}
|
|
969
|
+
try {
|
|
970
|
+
const keystoneService = new KeystoneService_V1();
|
|
971
|
+
const latestKeystone = await keystoneService.getLatestKeystone({
|
|
972
|
+
addr: delegateTjpAddr,
|
|
973
|
+
metaspace,
|
|
974
|
+
space
|
|
975
|
+
});
|
|
976
|
+
if (latestKeystone) {
|
|
977
|
+
addrToLoad = getIbGibAddr({ ibGib: latestKeystone });
|
|
809
978
|
}
|
|
810
979
|
}
|
|
980
|
+
catch (err) {
|
|
981
|
+
console.warn(`${lc} Error resolving latest delegate keystone: ${extractErrorMsg(err)}`);
|
|
982
|
+
}
|
|
811
983
|
this.dispatchEvent(new CustomEvent('ibgib-view-keystone-details', {
|
|
812
984
|
detail: { addr: addrToLoad },
|
|
813
985
|
bubbles: true,
|
|
@@ -841,8 +1013,9 @@ export class KeystoneDetailsComponentInstance extends IbGibDynamicComponentInsta
|
|
|
841
1013
|
let masterSecret;
|
|
842
1014
|
try {
|
|
843
1015
|
masterSecret = await promptForSecret({
|
|
844
|
-
msg: `Enter Master Password to revoke delegate '${deviceName}':`,
|
|
845
|
-
confirm: false
|
|
1016
|
+
msg: `Enter Master Password or touch security key to revoke delegate '${deviceName}':`,
|
|
1017
|
+
confirm: false,
|
|
1018
|
+
passkeys: this.aggregatedDetails?.passkeys,
|
|
846
1019
|
});
|
|
847
1020
|
}
|
|
848
1021
|
catch (err) {
|
|
@@ -861,11 +1034,30 @@ export class KeystoneDetailsComponentInstance extends IbGibDynamicComponentInsta
|
|
|
861
1034
|
throw new Error("Local space not found");
|
|
862
1035
|
}
|
|
863
1036
|
const keystoneSvc = new KeystoneService_V1();
|
|
1037
|
+
// Resolve which manage pool matches the provided masterSecret
|
|
1038
|
+
let matchedPoolId;
|
|
1039
|
+
const candidatePools = this.ibGib.data?.challengePools.filter(p => p.id === 'manage' || p.id.startsWith('passkey-manage-') || (p.config.allowedVerbs && p.config.allowedVerbs.includes(KEYSTONE_VERB_MANAGE))) || [];
|
|
1040
|
+
for (const pool of candidatePools) {
|
|
1041
|
+
const ok = await keystoneSvc.verifySigningSecret({
|
|
1042
|
+
keystoneIbGib: this.ibGib,
|
|
1043
|
+
signingSecret: masterSecret,
|
|
1044
|
+
poolId: pool.id
|
|
1045
|
+
});
|
|
1046
|
+
if (ok) {
|
|
1047
|
+
matchedPoolId = pool.id;
|
|
1048
|
+
break;
|
|
1049
|
+
}
|
|
1050
|
+
}
|
|
1051
|
+
if (!matchedPoolId) {
|
|
1052
|
+
this.setSyncStatus("Failed to authorize revocation (invalid secret/passkey).", "error");
|
|
1053
|
+
return;
|
|
1054
|
+
}
|
|
864
1055
|
// Unregister delegate from parent keystone
|
|
865
1056
|
const evolvedParent = await keystoneSvc.unregisterDelegate({
|
|
866
1057
|
parentKeystone: this.ibGib,
|
|
867
1058
|
delegateTjpAddr,
|
|
868
1059
|
masterSecret,
|
|
1060
|
+
signingPoolId: matchedPoolId,
|
|
869
1061
|
metaspace,
|
|
870
1062
|
space
|
|
871
1063
|
});
|
|
@@ -911,6 +1103,452 @@ export class KeystoneDetailsComponentInstance extends IbGibDynamicComponentInsta
|
|
|
911
1103
|
this.setSyncStatus(`Revocation failed: ${extractErrorMsg(error)}`, "error");
|
|
912
1104
|
}
|
|
913
1105
|
}
|
|
1106
|
+
setPasskeyStatus(msg, type) {
|
|
1107
|
+
const statusEl = this.elements?.passkeyStatusMsgEl;
|
|
1108
|
+
if (!statusEl)
|
|
1109
|
+
return;
|
|
1110
|
+
statusEl.textContent = msg;
|
|
1111
|
+
statusEl.className = `status-msg ${type}`;
|
|
1112
|
+
statusEl.classList.remove('hidden');
|
|
1113
|
+
}
|
|
1114
|
+
async renderPasskeys(passkeys) {
|
|
1115
|
+
if (!this.elements) {
|
|
1116
|
+
return;
|
|
1117
|
+
}
|
|
1118
|
+
const listEl = this.elements.passkeysListEl;
|
|
1119
|
+
listEl.innerHTML = '';
|
|
1120
|
+
const caps = !!this.ibGib ?
|
|
1121
|
+
getKeystoneCapabilities({ keystone: this.ibGib, aggregatedDetails: this.aggregatedDetails }) :
|
|
1122
|
+
undefined;
|
|
1123
|
+
// Update button text depending on whether the account is custodial-only (elevation) or sovereign/hybrid
|
|
1124
|
+
const isCustodialOnly = !!caps ? caps.isCustodial && !caps.isSovereign : false;
|
|
1125
|
+
if (this.elements.btnAddPasskeyEl) {
|
|
1126
|
+
this.elements.btnAddPasskeyEl.textContent = isCustodialOnly
|
|
1127
|
+
? "🛡️ Add Passkey (Passwordless Sovereign Elevation)"
|
|
1128
|
+
: "🛡️ Register New Passkey / Security Key";
|
|
1129
|
+
}
|
|
1130
|
+
const validPasskeyEntries = Object.entries(passkeys || {}).filter(([_, entry]) => entry !== null && entry !== undefined);
|
|
1131
|
+
if (validPasskeyEntries.length === 0) {
|
|
1132
|
+
listEl.innerHTML = `
|
|
1133
|
+
<tr>
|
|
1134
|
+
<td colspan="4" style="text-align: center; color: var(--clr-text-secondary, #b3b3b3); padding: 1rem;">
|
|
1135
|
+
No hardware passkeys or biometrics registered.
|
|
1136
|
+
</td>
|
|
1137
|
+
</tr>
|
|
1138
|
+
`;
|
|
1139
|
+
return;
|
|
1140
|
+
}
|
|
1141
|
+
for (const [credId, entry] of validPasskeyEntries) {
|
|
1142
|
+
const row = document.createElement('tr');
|
|
1143
|
+
const createdText = entry.createdUTC ? new Date(entry.createdUTC).toLocaleString() : 'N/A';
|
|
1144
|
+
const nameText = entry.name || 'Hardware Key';
|
|
1145
|
+
// Format transport badges
|
|
1146
|
+
let transportBadgesHtml = '';
|
|
1147
|
+
if (entry.transports && entry.transports.length > 0) {
|
|
1148
|
+
transportBadgesHtml = entry.transports.map(t => {
|
|
1149
|
+
let icon = '🔑';
|
|
1150
|
+
let label = t;
|
|
1151
|
+
if (t === 'internal') {
|
|
1152
|
+
icon = '💻';
|
|
1153
|
+
label = 'Touch ID / Built-in';
|
|
1154
|
+
}
|
|
1155
|
+
else if (t === 'usb') {
|
|
1156
|
+
icon = '🔑';
|
|
1157
|
+
label = 'USB Key';
|
|
1158
|
+
}
|
|
1159
|
+
else if (t === 'nfc') {
|
|
1160
|
+
icon = '📶';
|
|
1161
|
+
label = 'NFC';
|
|
1162
|
+
}
|
|
1163
|
+
else if (t === 'ble' || t === 'hybrid') {
|
|
1164
|
+
icon = '📲';
|
|
1165
|
+
label = 'Mobile / Bluetooth';
|
|
1166
|
+
}
|
|
1167
|
+
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>`;
|
|
1168
|
+
}).join(' ');
|
|
1169
|
+
}
|
|
1170
|
+
else {
|
|
1171
|
+
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>`;
|
|
1172
|
+
}
|
|
1173
|
+
row.innerHTML = `
|
|
1174
|
+
<td>
|
|
1175
|
+
<div style="font-weight: 500;">${nameText}</div>
|
|
1176
|
+
<code style="font-size: 0.75rem; color: var(--clr-text-secondary, #b3b3b3); word-break: break-all;">${credId.substring(0, 16)}...</code>
|
|
1177
|
+
</td>
|
|
1178
|
+
<td>${transportBadgesHtml}</td>
|
|
1179
|
+
<td style="font-size: 0.85rem; color: var(--clr-text-secondary, #b3b3b3);">${createdText}</td>
|
|
1180
|
+
<td>
|
|
1181
|
+
<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);">
|
|
1182
|
+
Remove
|
|
1183
|
+
</button>
|
|
1184
|
+
</td>
|
|
1185
|
+
`;
|
|
1186
|
+
const btnRemove = row.querySelector('.btn-remove-passkey');
|
|
1187
|
+
btnRemove?.addEventListener('click', () => this.handleRemovePasskey(credId));
|
|
1188
|
+
listEl.appendChild(row);
|
|
1189
|
+
}
|
|
1190
|
+
}
|
|
1191
|
+
async handleAddPasskey() {
|
|
1192
|
+
const lc = `${this.lc}[${this.handleAddPasskey.name}]`;
|
|
1193
|
+
if (!this.ibGib || !this.ibGibAddr) {
|
|
1194
|
+
return;
|
|
1195
|
+
}
|
|
1196
|
+
try {
|
|
1197
|
+
const caps = getKeystoneCapabilities({ keystone: this.ibGib, aggregatedDetails: this.aggregatedDetails });
|
|
1198
|
+
const isCustodialOnly = caps.isCustodial && !caps.isSovereign;
|
|
1199
|
+
const supported = await isPasskeyPrfSupported();
|
|
1200
|
+
if (!supported) {
|
|
1201
|
+
this.setPasskeyStatus("WebAuthn PRF extension is not supported in this browser environment.", "error");
|
|
1202
|
+
return;
|
|
1203
|
+
}
|
|
1204
|
+
this.showBusy({
|
|
1205
|
+
isBusy: true,
|
|
1206
|
+
title: 'Touch Biometrics or Security Key',
|
|
1207
|
+
msg: 'Please touch your fingerprint sensor, Face ID, or insert your USB security key when prompted...',
|
|
1208
|
+
animationEmoji: '🛡️'
|
|
1209
|
+
});
|
|
1210
|
+
const username = this.aggregatedDetails?.username || this.ibGib.data?.frameDetails?.username || 'user';
|
|
1211
|
+
const rawSalt = crypto.getRandomValues(new Uint8Array(32));
|
|
1212
|
+
const saltHex = bufferToHex(rawSalt.buffer);
|
|
1213
|
+
const regResult = await registerPasskeyCredential({
|
|
1214
|
+
username,
|
|
1215
|
+
displayName: username,
|
|
1216
|
+
salt: saltHex
|
|
1217
|
+
});
|
|
1218
|
+
const { credentialId, transports } = regResult;
|
|
1219
|
+
let passkeySecret = regResult.passkeySecret;
|
|
1220
|
+
if (!passkeySecret) {
|
|
1221
|
+
const prfResult = await evaluatePasskeyPrf({
|
|
1222
|
+
credentialId,
|
|
1223
|
+
salt: saltHex,
|
|
1224
|
+
transports
|
|
1225
|
+
});
|
|
1226
|
+
passkeySecret = prfResult.passkeySecret;
|
|
1227
|
+
}
|
|
1228
|
+
const defaultDeviceTag = generateDefaultDeviceTag();
|
|
1229
|
+
const passkeyName = defaultDeviceTag;
|
|
1230
|
+
const passkeyTag = getSaferSubstring({ text: passkeyName, length: 12, keepLiterals: ['-'] }) || 'key';
|
|
1231
|
+
const poolId = `passkey-manage-${passkeyTag}`;
|
|
1232
|
+
const passkeyEntry = {
|
|
1233
|
+
credentialId,
|
|
1234
|
+
poolId,
|
|
1235
|
+
name: passkeyName,
|
|
1236
|
+
salt: saltHex,
|
|
1237
|
+
createdUTC: new Date().toISOString(),
|
|
1238
|
+
...(transports && transports.length > 0 ? { transports } : {})
|
|
1239
|
+
};
|
|
1240
|
+
const metaspace = await getGlobalMetaspace_waitIfNeeded();
|
|
1241
|
+
if (!metaspace)
|
|
1242
|
+
throw new Error("Metaspace not found");
|
|
1243
|
+
const space = await metaspace.getLocalUserSpace({ lock: false });
|
|
1244
|
+
if (!space)
|
|
1245
|
+
throw new Error("Local space not found");
|
|
1246
|
+
const keystoneService = new KeystoneService_V1();
|
|
1247
|
+
let evolvedKeystone;
|
|
1248
|
+
if (isCustodialOnly) {
|
|
1249
|
+
// Elevate custodial account with sovereign passkey
|
|
1250
|
+
this.showBusy({
|
|
1251
|
+
isBusy: true,
|
|
1252
|
+
title: 'Elevating Account Ownership...',
|
|
1253
|
+
msg: 'Establishing sovereign passkey pool on identity...',
|
|
1254
|
+
animationEmoji: '⚡'
|
|
1255
|
+
});
|
|
1256
|
+
// Generate new manage pool configuration using passkeySecret
|
|
1257
|
+
const poolConfig = {
|
|
1258
|
+
id: poolId,
|
|
1259
|
+
type: KeystoneChallengeType.hash_reveal_v1,
|
|
1260
|
+
salt: poolId,
|
|
1261
|
+
algo: HashAlgorithm.sha_256,
|
|
1262
|
+
rounds: 1,
|
|
1263
|
+
allowedVerbs: [KEYSTONE_VERB_MANAGE, KEYSTONE_VERB_SYNC, KEYSTONE_VERB_CONNECT],
|
|
1264
|
+
behavior: {
|
|
1265
|
+
size: 10,
|
|
1266
|
+
replenish: KeystoneReplenishStrategy.topUp,
|
|
1267
|
+
selectSequentially: 1,
|
|
1268
|
+
selectRandomly: 0,
|
|
1269
|
+
targetBindingCount: 0,
|
|
1270
|
+
}
|
|
1271
|
+
};
|
|
1272
|
+
const challenges = await generatePoolChallenges({
|
|
1273
|
+
config: poolConfig,
|
|
1274
|
+
masterSecret: passkeySecret
|
|
1275
|
+
});
|
|
1276
|
+
const newPool = {
|
|
1277
|
+
id: poolId,
|
|
1278
|
+
config: poolConfig,
|
|
1279
|
+
challenges
|
|
1280
|
+
};
|
|
1281
|
+
evolvedKeystone = await keystoneService.addPools({
|
|
1282
|
+
latestKeystone: this.ibGib,
|
|
1283
|
+
masterSecret: passkeySecret,
|
|
1284
|
+
newPools: [newPool],
|
|
1285
|
+
frameDetails: {
|
|
1286
|
+
passkeys: {
|
|
1287
|
+
[credentialId]: passkeyEntry
|
|
1288
|
+
}
|
|
1289
|
+
},
|
|
1290
|
+
dataToPatch: {
|
|
1291
|
+
credentialIds: [...(this.ibGib.data?.credentialIds || []), credentialId]
|
|
1292
|
+
},
|
|
1293
|
+
metaspace,
|
|
1294
|
+
space
|
|
1295
|
+
});
|
|
1296
|
+
}
|
|
1297
|
+
else {
|
|
1298
|
+
// Secondary passkey registration on existing sovereign identity:
|
|
1299
|
+
// Prompt user to authorize with current master password
|
|
1300
|
+
let authSecret;
|
|
1301
|
+
let isCorrect = false;
|
|
1302
|
+
let tries = 0;
|
|
1303
|
+
const maxTries = 5;
|
|
1304
|
+
while (tries < maxTries && !isCorrect) {
|
|
1305
|
+
try {
|
|
1306
|
+
const promptMsg = tries === 0
|
|
1307
|
+
? 'Enter your current Master Secret (Password) or touch security key to authorize adding this passkey:'
|
|
1308
|
+
: `Incorrect password. Please try again (${maxTries - tries} tries remaining):`;
|
|
1309
|
+
authSecret = await promptForSecret({
|
|
1310
|
+
msg: promptMsg,
|
|
1311
|
+
confirm: false,
|
|
1312
|
+
passkeys: this.aggregatedDetails?.passkeys,
|
|
1313
|
+
});
|
|
1314
|
+
}
|
|
1315
|
+
catch (err) {
|
|
1316
|
+
authSecret = undefined;
|
|
1317
|
+
break;
|
|
1318
|
+
}
|
|
1319
|
+
if (!authSecret)
|
|
1320
|
+
break;
|
|
1321
|
+
// Verify secret against manage/passkey pools
|
|
1322
|
+
isCorrect = await keystoneService.verifySigningSecret({
|
|
1323
|
+
keystoneIbGib: this.ibGib,
|
|
1324
|
+
signingSecret: authSecret
|
|
1325
|
+
});
|
|
1326
|
+
if (!isCorrect)
|
|
1327
|
+
tries++;
|
|
1328
|
+
}
|
|
1329
|
+
if (!isCorrect || !authSecret) {
|
|
1330
|
+
this.setPasskeyStatus("Registration cancelled (authorization required).", "error");
|
|
1331
|
+
return;
|
|
1332
|
+
}
|
|
1333
|
+
this.showBusy({
|
|
1334
|
+
isBusy: true,
|
|
1335
|
+
title: 'Registering Passkey...',
|
|
1336
|
+
msg: 'Adding cryptographic challenge pool and evolving identity...',
|
|
1337
|
+
animationEmoji: '🛡️'
|
|
1338
|
+
});
|
|
1339
|
+
const poolConfig = {
|
|
1340
|
+
id: poolId,
|
|
1341
|
+
type: KeystoneChallengeType.hash_reveal_v1,
|
|
1342
|
+
salt: poolId,
|
|
1343
|
+
algo: HashAlgorithm.sha_256,
|
|
1344
|
+
rounds: 1,
|
|
1345
|
+
allowedVerbs: [KEYSTONE_VERB_MANAGE, KEYSTONE_VERB_SYNC, KEYSTONE_VERB_CONNECT],
|
|
1346
|
+
behavior: {
|
|
1347
|
+
size: 10,
|
|
1348
|
+
replenish: KeystoneReplenishStrategy.topUp,
|
|
1349
|
+
selectSequentially: 1,
|
|
1350
|
+
selectRandomly: 0,
|
|
1351
|
+
targetBindingCount: 0,
|
|
1352
|
+
}
|
|
1353
|
+
};
|
|
1354
|
+
const challenges = await generatePoolChallenges({
|
|
1355
|
+
config: poolConfig,
|
|
1356
|
+
masterSecret: passkeySecret
|
|
1357
|
+
});
|
|
1358
|
+
const newPool = {
|
|
1359
|
+
id: poolId,
|
|
1360
|
+
config: poolConfig,
|
|
1361
|
+
challenges
|
|
1362
|
+
};
|
|
1363
|
+
evolvedKeystone = await keystoneService.addPools({
|
|
1364
|
+
latestKeystone: this.ibGib,
|
|
1365
|
+
masterSecret: authSecret,
|
|
1366
|
+
newPools: [newPool],
|
|
1367
|
+
frameDetails: {
|
|
1368
|
+
passkeys: {
|
|
1369
|
+
[credentialId]: passkeyEntry
|
|
1370
|
+
}
|
|
1371
|
+
},
|
|
1372
|
+
dataToPatch: {
|
|
1373
|
+
credentialIds: [...(this.ibGib.data?.credentialIds || []), credentialId]
|
|
1374
|
+
},
|
|
1375
|
+
metaspace,
|
|
1376
|
+
space
|
|
1377
|
+
});
|
|
1378
|
+
}
|
|
1379
|
+
// Sync evolved keystone to server
|
|
1380
|
+
this.showBusy({
|
|
1381
|
+
isBusy: true,
|
|
1382
|
+
title: 'Syncing to Server...',
|
|
1383
|
+
msg: 'Publishing evolved identity keystone to server...',
|
|
1384
|
+
animationEmoji: '⚡'
|
|
1385
|
+
});
|
|
1386
|
+
const primaryTjpAddr = getTjpAddr({ ibGib: evolvedKeystone, defaultIfNone: 'incomingAddr' });
|
|
1387
|
+
const resSync = await getApiBridge().putEvolveKeystone({
|
|
1388
|
+
domainAddr: primaryTjpAddr,
|
|
1389
|
+
keystoneIbGib: evolvedKeystone,
|
|
1390
|
+
relatedKeystoneIbGibs: []
|
|
1391
|
+
});
|
|
1392
|
+
if (!resSync.success) {
|
|
1393
|
+
console.warn(`${lc} Server sync note: ${resSync.message}`);
|
|
1394
|
+
}
|
|
1395
|
+
// Update Special Index
|
|
1396
|
+
await updateSpecialIndex({
|
|
1397
|
+
type: "keystones",
|
|
1398
|
+
rel8nInfos: [
|
|
1399
|
+
{
|
|
1400
|
+
rel8nName: "keystone",
|
|
1401
|
+
ibGibs: [toDto({ ibGib: evolvedKeystone })]
|
|
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 isCorrect = false;
|
|
1431
|
+
let tries = 0;
|
|
1432
|
+
const maxTries = 5;
|
|
1433
|
+
while (tries < maxTries && !isCorrect) {
|
|
1434
|
+
try {
|
|
1435
|
+
const promptMsg = tries === 0
|
|
1436
|
+
? `Are you sure you want to remove "${passkeyName}"? Enter your Master Secret (Password) or touch security key to authorize:`
|
|
1437
|
+
: `Incorrect password. Please try again (${maxTries - tries} tries remaining):`;
|
|
1438
|
+
masterSecret = await promptForSecret({
|
|
1439
|
+
msg: promptMsg,
|
|
1440
|
+
confirm: false,
|
|
1441
|
+
passkeys: this.aggregatedDetails?.passkeys,
|
|
1442
|
+
});
|
|
1443
|
+
}
|
|
1444
|
+
catch (err) {
|
|
1445
|
+
masterSecret = undefined;
|
|
1446
|
+
break;
|
|
1447
|
+
}
|
|
1448
|
+
if (!masterSecret)
|
|
1449
|
+
break;
|
|
1450
|
+
isCorrect = await keystoneService.verifySigningSecret({
|
|
1451
|
+
keystoneIbGib: this.ibGib,
|
|
1452
|
+
signingSecret: masterSecret
|
|
1453
|
+
});
|
|
1454
|
+
if (!isCorrect)
|
|
1455
|
+
tries++;
|
|
1456
|
+
}
|
|
1457
|
+
if (!isCorrect || !masterSecret) {
|
|
1458
|
+
this.setPasskeyStatus("Removal cancelled or unauthorized.", "error");
|
|
1459
|
+
return;
|
|
1460
|
+
}
|
|
1461
|
+
this.showBusy({
|
|
1462
|
+
isBusy: true,
|
|
1463
|
+
title: 'Revoking Passkey...',
|
|
1464
|
+
msg: 'Tombstoning passkey record and removing challenge pool...',
|
|
1465
|
+
animationEmoji: '🗑️'
|
|
1466
|
+
});
|
|
1467
|
+
const metaspace = await getGlobalMetaspace_waitIfNeeded();
|
|
1468
|
+
if (!metaspace)
|
|
1469
|
+
throw new Error("Metaspace not found");
|
|
1470
|
+
const space = await metaspace.getLocalUserSpace({ lock: false });
|
|
1471
|
+
if (!space)
|
|
1472
|
+
throw new Error("Local space not found");
|
|
1473
|
+
const poolIdToRemove = passkeyEntry?.poolId || `passkey-manage-${credentialId.substring(0, 8)}`;
|
|
1474
|
+
const poolExists = this.ibGib.data?.challengePools.some(p => p.id === poolIdToRemove);
|
|
1475
|
+
const filteredCredentialIds = (this.ibGib.data?.credentialIds || []).filter(id => id !== credentialId);
|
|
1476
|
+
let evolvedKeystone;
|
|
1477
|
+
if (poolExists) {
|
|
1478
|
+
evolvedKeystone = await keystoneService.removePools({
|
|
1479
|
+
latestKeystone: this.ibGib,
|
|
1480
|
+
masterSecret,
|
|
1481
|
+
poolIds: [poolIdToRemove],
|
|
1482
|
+
frameDetails: {
|
|
1483
|
+
passkeys: {
|
|
1484
|
+
[credentialId]: null
|
|
1485
|
+
}
|
|
1486
|
+
},
|
|
1487
|
+
dataToPatch: {
|
|
1488
|
+
credentialIds: filteredCredentialIds
|
|
1489
|
+
},
|
|
1490
|
+
metaspace,
|
|
1491
|
+
space
|
|
1492
|
+
});
|
|
1493
|
+
}
|
|
1494
|
+
else {
|
|
1495
|
+
evolvedKeystone = await keystoneService.sign({
|
|
1496
|
+
latestKeystone: this.ibGib,
|
|
1497
|
+
masterSecret,
|
|
1498
|
+
claim: {
|
|
1499
|
+
verb: POOL_ID_MANAGE,
|
|
1500
|
+
target: getIbGibAddr({ ibGib: this.ibGib })
|
|
1501
|
+
},
|
|
1502
|
+
frameDetails: {
|
|
1503
|
+
passkeys: {
|
|
1504
|
+
[credentialId]: null
|
|
1505
|
+
}
|
|
1506
|
+
},
|
|
1507
|
+
dataToPatch: {
|
|
1508
|
+
credentialIds: filteredCredentialIds
|
|
1509
|
+
},
|
|
1510
|
+
metaspace,
|
|
1511
|
+
space
|
|
1512
|
+
});
|
|
1513
|
+
}
|
|
1514
|
+
// Sync to server
|
|
1515
|
+
this.showBusy({
|
|
1516
|
+
isBusy: true,
|
|
1517
|
+
title: 'Syncing to Server...',
|
|
1518
|
+
msg: 'Publishing updated keystone to server...',
|
|
1519
|
+
animationEmoji: '⚡'
|
|
1520
|
+
});
|
|
1521
|
+
const primaryTjpAddr = getTjpAddr({ ibGib: evolvedKeystone, defaultIfNone: 'incomingAddr' });
|
|
1522
|
+
await getApiBridge().putEvolveKeystone({
|
|
1523
|
+
domainAddr: primaryTjpAddr,
|
|
1524
|
+
keystoneIbGib: evolvedKeystone,
|
|
1525
|
+
relatedKeystoneIbGibs: []
|
|
1526
|
+
});
|
|
1527
|
+
// Update Special Index
|
|
1528
|
+
await updateSpecialIndex({
|
|
1529
|
+
type: "keystones",
|
|
1530
|
+
rel8nInfos: [
|
|
1531
|
+
{
|
|
1532
|
+
rel8nName: "keystone",
|
|
1533
|
+
ibGibs: [toDto({ ibGib: evolvedKeystone })]
|
|
1534
|
+
}
|
|
1535
|
+
],
|
|
1536
|
+
metaspace,
|
|
1537
|
+
space
|
|
1538
|
+
});
|
|
1539
|
+
this.setPasskeyStatus(`Passkey "${passkeyName}" successfully removed.`, "success");
|
|
1540
|
+
this.ibGibAddr = getIbGibAddr({ ibGib: evolvedKeystone });
|
|
1541
|
+
this.staticIbGib = evolvedKeystone;
|
|
1542
|
+
await this.loadIbGib({ getLatest: false });
|
|
1543
|
+
}
|
|
1544
|
+
catch (error) {
|
|
1545
|
+
console.error(`${lc} ${extractErrorMsg(error)}`, error);
|
|
1546
|
+
this.setPasskeyStatus(`Failed to remove passkey: ${extractErrorMsg(error)}`, "error");
|
|
1547
|
+
}
|
|
1548
|
+
finally {
|
|
1549
|
+
this.showBusy({ isBusy: false });
|
|
1550
|
+
}
|
|
1551
|
+
}
|
|
914
1552
|
async handleContextUpdated() {
|
|
915
1553
|
const lc = `${this.lc}[${this.handleContextUpdated.name}]`;
|
|
916
1554
|
try {
|
|
@@ -955,10 +1593,24 @@ export class KeystoneDetailsComponentInstance extends IbGibDynamicComponentInsta
|
|
|
955
1593
|
async handleSSOLink(providerId) {
|
|
956
1594
|
const lc = `${this.lc}[${this.handleSSOLink.name}]`;
|
|
957
1595
|
this.setSSOStatus(`Linking ${providerId}...`, "info");
|
|
1596
|
+
const providerName = providerId === 'google' ? 'Google' : providerId === 'github' ? 'GitHub' : providerId;
|
|
1597
|
+
this.setSSOStatus(`Opening ${providerName} SSO authorization window...`, "info");
|
|
1598
|
+
this.showBusy({
|
|
1599
|
+
isBusy: true,
|
|
1600
|
+
title: `Connecting to ${providerName}...`,
|
|
1601
|
+
msg: `Opening ${providerName} SSO authorization window...\nPlease complete authorization in the popup window.`,
|
|
1602
|
+
animationEmoji: '🌐'
|
|
1603
|
+
});
|
|
958
1604
|
try {
|
|
1605
|
+
const primaryTjpAddr = (this.ibGib ? getTjpAddr({ ibGib: this.ibGib }) : undefined)
|
|
1606
|
+
|| (this.ibGibAddr ? (() => {
|
|
1607
|
+
const { ib } = getIbAndGib({ ibGibAddr: this.ibGibAddr });
|
|
1608
|
+
const { tjpGib } = getGibInfo({ ibGibAddr: this.ibGibAddr });
|
|
1609
|
+
return tjpGib && ib ? `${ib}^${tjpGib}` : this.ibGibAddr;
|
|
1610
|
+
})() : undefined);
|
|
959
1611
|
const resPopup = await executeSsoOAuthPopup({
|
|
960
1612
|
providerId,
|
|
961
|
-
parentTjpAddr:
|
|
1613
|
+
parentTjpAddr: primaryTjpAddr
|
|
962
1614
|
});
|
|
963
1615
|
if (!resPopup.success || !resPopup.code) {
|
|
964
1616
|
this.setSSOStatus(resPopup.message || `Linking cancelled.`, "error");
|
|
@@ -970,6 +1622,9 @@ export class KeystoneDetailsComponentInstance extends IbGibDynamicComponentInsta
|
|
|
970
1622
|
console.error(`${lc} SSO link initiation failed: ${extractErrorMsg(error)}`);
|
|
971
1623
|
this.setSSOStatus(`Link failed: ${extractErrorMsg(error)}`, "error");
|
|
972
1624
|
}
|
|
1625
|
+
finally {
|
|
1626
|
+
this.showBusy({ isBusy: false });
|
|
1627
|
+
}
|
|
973
1628
|
}
|
|
974
1629
|
setSSOStatus(msg, type) {
|
|
975
1630
|
const statusEl = this.elements.ssoStatusMsgEl;
|
|
@@ -979,7 +1634,14 @@ export class KeystoneDetailsComponentInstance extends IbGibDynamicComponentInsta
|
|
|
979
1634
|
}
|
|
980
1635
|
async handleSSOLinkCallback(code, stateObj, redirectUri) {
|
|
981
1636
|
const lc = `${this.lc}[${this.handleSSOLinkCallback.name}]`;
|
|
982
|
-
|
|
1637
|
+
const providerName = stateObj.providerId === 'google' ? 'Google' : stateObj.providerId === 'github' ? 'GitHub' : stateObj.providerId;
|
|
1638
|
+
this.setSSOStatus(`Retrieving challenge pool configuration from server...`, "info");
|
|
1639
|
+
this.showBusy({
|
|
1640
|
+
isBusy: true,
|
|
1641
|
+
title: `Linking ${providerName}...`,
|
|
1642
|
+
msg: `Validating ${providerName} authorization with server...`,
|
|
1643
|
+
animationEmoji: '🔐'
|
|
1644
|
+
});
|
|
983
1645
|
try {
|
|
984
1646
|
// 1. Exchange OAuth code for custodian public pool config
|
|
985
1647
|
const res = await fetch('/api/identity/sso/link', {
|
|
@@ -1003,6 +1665,7 @@ export class KeystoneDetailsComponentInstance extends IbGibDynamicComponentInsta
|
|
|
1003
1665
|
}
|
|
1004
1666
|
const custodianPool = linkData.custodianPool;
|
|
1005
1667
|
// 2. Prompt user for master secret and verify it
|
|
1668
|
+
this.showBusy({ isBusy: false });
|
|
1006
1669
|
const keystoneService = new KeystoneService_V1();
|
|
1007
1670
|
let masterSecret;
|
|
1008
1671
|
let isCorrect = false;
|
|
@@ -1043,6 +1706,12 @@ export class KeystoneDetailsComponentInstance extends IbGibDynamicComponentInsta
|
|
|
1043
1706
|
this.setSSOStatus("Link cancelled or failed (incorrect password).", "error");
|
|
1044
1707
|
return;
|
|
1045
1708
|
}
|
|
1709
|
+
this.showBusy({
|
|
1710
|
+
isBusy: true,
|
|
1711
|
+
title: 'Evolving Identity...',
|
|
1712
|
+
msg: `Installing ${providerName} custodian pool and signing evolution...`,
|
|
1713
|
+
animationEmoji: '📝'
|
|
1714
|
+
});
|
|
1046
1715
|
this.setSSOStatus("Evolving identity locally and generating cryptographic signatures...", "info");
|
|
1047
1716
|
const metaspace = await getGlobalMetaspace_waitIfNeeded();
|
|
1048
1717
|
if (!metaspace)
|
|
@@ -1051,7 +1720,7 @@ export class KeystoneDetailsComponentInstance extends IbGibDynamicComponentInsta
|
|
|
1051
1720
|
if (!space) {
|
|
1052
1721
|
throw new Error("Local space not found");
|
|
1053
1722
|
}
|
|
1054
|
-
// 3. Evolve parent keystone to include the custodian pool
|
|
1723
|
+
// 3. Evolve parent keystone to include the custodian pool (n -> n+1)
|
|
1055
1724
|
const evolvedParent = await keystoneService.addPools({
|
|
1056
1725
|
latestKeystone: this.ibGib,
|
|
1057
1726
|
masterSecret: masterSecret,
|
|
@@ -1059,39 +1728,87 @@ export class KeystoneDetailsComponentInstance extends IbGibDynamicComponentInsta
|
|
|
1059
1728
|
metaspace,
|
|
1060
1729
|
space
|
|
1061
1730
|
});
|
|
1062
|
-
//
|
|
1063
|
-
|
|
1064
|
-
const syncRes = await getApiBridge().putEvolveKeystone({
|
|
1731
|
+
// Sync evolved parent (n+1) to server first
|
|
1732
|
+
const syncRes1 = await getApiBridge().putEvolveKeystone({
|
|
1065
1733
|
domainAddr: stateObj.parentTjpAddr,
|
|
1066
1734
|
keystoneIbGib: evolvedParent,
|
|
1067
|
-
relatedKeystoneIbGibs: []
|
|
1735
|
+
relatedKeystoneIbGibs: []
|
|
1068
1736
|
});
|
|
1069
|
-
if (!
|
|
1070
|
-
throw new Error(`Server synchronization rejected: ${
|
|
1737
|
+
if (!syncRes1.success) {
|
|
1738
|
+
throw new Error(`Server synchronization of linked pool rejected: ${syncRes1.message}`);
|
|
1739
|
+
}
|
|
1740
|
+
// 4. Create and register OAuth2 sync delegate for this provider (n+1 -> n+2)
|
|
1741
|
+
const username = this.aggregatedDetails?.username || this.ibGib?.data?.username || 'user';
|
|
1742
|
+
const { delegateKeystone, deviceTag } = await createOAuth2DeviceSyncDelegate({
|
|
1743
|
+
providerId: stateObj.providerId,
|
|
1744
|
+
username,
|
|
1745
|
+
primaryTjpAddr: stateObj.parentTjpAddr,
|
|
1746
|
+
custodianReqs: linkData.custodianReqs,
|
|
1747
|
+
metaspace,
|
|
1748
|
+
space,
|
|
1749
|
+
});
|
|
1750
|
+
const parentWithDelegate = await keystoneService.registerDelegate({
|
|
1751
|
+
parentKeystone: evolvedParent,
|
|
1752
|
+
delegateKeystone,
|
|
1753
|
+
masterSecret: masterSecret,
|
|
1754
|
+
signingPoolId: 'manage',
|
|
1755
|
+
metaspace,
|
|
1756
|
+
space,
|
|
1757
|
+
allowedVerbs: [KEYSTONE_VERB_SYNC, KEYSTONE_VERB_CONNECT],
|
|
1758
|
+
deviceName: deviceTag,
|
|
1759
|
+
});
|
|
1760
|
+
await metaspace.put({ ibGibs: [delegateKeystone, parentWithDelegate], space });
|
|
1761
|
+
// 5. Sync parent with registered delegate (n+2) to server
|
|
1762
|
+
this.showBusy({
|
|
1763
|
+
isBusy: true,
|
|
1764
|
+
title: 'Syncing to Server...',
|
|
1765
|
+
msg: 'Publishing linked identity and delegate to server...',
|
|
1766
|
+
animationEmoji: '⚡'
|
|
1767
|
+
});
|
|
1768
|
+
this.setSSOStatus("Publishing evolved identity keystone to server...", "info");
|
|
1769
|
+
const syncRes2 = await getApiBridge().putEvolveKeystone({
|
|
1770
|
+
domainAddr: stateObj.parentTjpAddr,
|
|
1771
|
+
keystoneIbGib: parentWithDelegate,
|
|
1772
|
+
relatedKeystoneIbGibs: [delegateKeystone]
|
|
1773
|
+
});
|
|
1774
|
+
if (!syncRes2.success) {
|
|
1775
|
+
throw new Error(`Server synchronization of delegate rejected: ${syncRes2.message}`);
|
|
1071
1776
|
}
|
|
1072
|
-
//
|
|
1777
|
+
// 6. Update index locally
|
|
1073
1778
|
await updateSpecialIndex({
|
|
1074
1779
|
type: "keystones",
|
|
1075
1780
|
rel8nInfos: [
|
|
1076
1781
|
{
|
|
1077
1782
|
rel8nName: "keystone",
|
|
1078
|
-
ibGibs: [toDto({ ibGib:
|
|
1783
|
+
ibGibs: [toDto({ ibGib: parentWithDelegate }), toDto({ ibGib: delegateKeystone })]
|
|
1079
1784
|
}
|
|
1080
1785
|
],
|
|
1081
1786
|
metaspace,
|
|
1082
1787
|
space
|
|
1083
1788
|
});
|
|
1084
1789
|
this.setSSOStatus(`Successfully linked with ${stateObj.providerId}!`, "success");
|
|
1790
|
+
this.ibGibAddr = getIbGibAddr({ ibGib: parentWithDelegate });
|
|
1791
|
+
this.staticIbGib = parentWithDelegate;
|
|
1085
1792
|
await this.loadIbGib({ getLatest: false });
|
|
1086
1793
|
}
|
|
1087
1794
|
catch (error) {
|
|
1088
1795
|
console.error(`${lc} SSO callback processing failed: ${extractErrorMsg(error)}`);
|
|
1089
1796
|
this.setSSOStatus(`Link failed: ${extractErrorMsg(error)}`, "error");
|
|
1090
1797
|
}
|
|
1798
|
+
finally {
|
|
1799
|
+
this.showBusy({ isBusy: false });
|
|
1800
|
+
}
|
|
1091
1801
|
}
|
|
1092
1802
|
async handleSSOClick(providerId) {
|
|
1093
|
-
const isLinked =
|
|
1803
|
+
const isLinked = !!this.ibGib ?
|
|
1804
|
+
isSsoProviderLinked({ keystone: this.ibGib, aggregatedDetails: this.aggregatedDetails, providerId }) :
|
|
1805
|
+
false;
|
|
1094
1806
|
if (isLinked) {
|
|
1807
|
+
const caps = getKeystoneCapabilities({ keystone: this.ibGib, aggregatedDetails: this.aggregatedDetails });
|
|
1808
|
+
if (!caps.isSovereign) {
|
|
1809
|
+
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");
|
|
1810
|
+
return;
|
|
1811
|
+
}
|
|
1095
1812
|
await this.handleSSOUnlink(providerId);
|
|
1096
1813
|
}
|
|
1097
1814
|
else {
|
|
@@ -1100,7 +1817,8 @@ export class KeystoneDetailsComponentInstance extends IbGibDynamicComponentInsta
|
|
|
1100
1817
|
}
|
|
1101
1818
|
async handleSSOUnlink(providerId) {
|
|
1102
1819
|
const lc = `${this.lc}[${this.handleSSOUnlink.name}]`;
|
|
1103
|
-
|
|
1820
|
+
const providerName = providerId === 'google' ? 'Google' : providerId === 'github' ? 'GitHub' : providerId;
|
|
1821
|
+
this.setSSOStatus(`Unlinking ${providerName}...`, "info");
|
|
1104
1822
|
try {
|
|
1105
1823
|
// 1. Prompt user for password
|
|
1106
1824
|
const keystoneService = new KeystoneService_V1();
|
|
@@ -1111,11 +1829,12 @@ export class KeystoneDetailsComponentInstance extends IbGibDynamicComponentInsta
|
|
|
1111
1829
|
while (tries < maxTries && !isCorrect) {
|
|
1112
1830
|
try {
|
|
1113
1831
|
const promptMsg = tries === 0
|
|
1114
|
-
? `Are you sure you want to unlink your ${
|
|
1832
|
+
? `Are you sure you want to unlink your ${providerName} account? If so, enter your local Master Secret (Password) or touch security key to authorize. Otherwise, click Cancel.`
|
|
1115
1833
|
: `Incorrect password. Please try again (${maxTries - tries} tries remaining):`;
|
|
1116
1834
|
masterSecret = await promptForSecret({
|
|
1117
1835
|
msg: promptMsg,
|
|
1118
|
-
confirm: false
|
|
1836
|
+
confirm: false,
|
|
1837
|
+
passkeys: this.aggregatedDetails?.passkeys,
|
|
1119
1838
|
});
|
|
1120
1839
|
}
|
|
1121
1840
|
catch (error) {
|
|
@@ -1125,11 +1844,10 @@ export class KeystoneDetailsComponentInstance extends IbGibDynamicComponentInsta
|
|
|
1125
1844
|
if (!masterSecret) {
|
|
1126
1845
|
break;
|
|
1127
1846
|
}
|
|
1128
|
-
// Verify secret using verifySigningSecret
|
|
1847
|
+
// Verify secret using verifySigningSecret
|
|
1129
1848
|
isCorrect = await keystoneService.verifySigningSecret({
|
|
1130
1849
|
keystoneIbGib: this.ibGib,
|
|
1131
|
-
signingSecret: masterSecret
|
|
1132
|
-
poolId: 'manage'
|
|
1850
|
+
signingSecret: masterSecret
|
|
1133
1851
|
});
|
|
1134
1852
|
if (!isCorrect) {
|
|
1135
1853
|
tries++;
|
|
@@ -1139,6 +1857,12 @@ export class KeystoneDetailsComponentInstance extends IbGibDynamicComponentInsta
|
|
|
1139
1857
|
this.setSSOStatus("Unlink cancelled or failed (incorrect password).", "error");
|
|
1140
1858
|
return;
|
|
1141
1859
|
}
|
|
1860
|
+
this.showBusy({
|
|
1861
|
+
isBusy: true,
|
|
1862
|
+
title: 'Unlinking Provider...',
|
|
1863
|
+
msg: `Removing ${providerName} custodian pool and delegate keys...`,
|
|
1864
|
+
animationEmoji: '🔓'
|
|
1865
|
+
});
|
|
1142
1866
|
this.setSSOStatus("Evolving identity locally and removing custodian pool...", "info");
|
|
1143
1867
|
const metaspace = await getGlobalMetaspace_waitIfNeeded();
|
|
1144
1868
|
if (!metaspace)
|
|
@@ -1149,23 +1873,66 @@ export class KeystoneDetailsComponentInstance extends IbGibDynamicComponentInsta
|
|
|
1149
1873
|
}
|
|
1150
1874
|
// 2. Evolve parent keystone to remove the custodian pool
|
|
1151
1875
|
const targetPoolId = `custodian-manage-${providerId}`;
|
|
1152
|
-
|
|
1876
|
+
let evolvedParent = await keystoneService.removePools({
|
|
1153
1877
|
latestKeystone: this.ibGib,
|
|
1154
1878
|
masterSecret: masterSecret,
|
|
1155
1879
|
poolIds: [targetPoolId],
|
|
1156
1880
|
metaspace,
|
|
1157
1881
|
space
|
|
1158
1882
|
});
|
|
1159
|
-
|
|
1883
|
+
const primaryTjpAddr = (this.ibGib ? getTjpAddr({ ibGib: this.ibGib }) : undefined)
|
|
1884
|
+
|| (this.ibGibAddr ? (() => {
|
|
1885
|
+
const { ib } = getIbAndGib({ ibGibAddr: this.ibGibAddr });
|
|
1886
|
+
const { tjpGib } = getGibInfo({ ibGibAddr: this.ibGibAddr });
|
|
1887
|
+
return tjpGib && ib ? `${ib}^${tjpGib}` : this.ibGibAddr;
|
|
1888
|
+
})() : undefined)
|
|
1889
|
+
|| getIbGibAddr({ ibGib: this.ibGib });
|
|
1890
|
+
// 3. Sync pool removal frame to server
|
|
1891
|
+
this.showBusy({
|
|
1892
|
+
isBusy: true,
|
|
1893
|
+
title: 'Syncing to Server...',
|
|
1894
|
+
msg: `Publishing ${providerName} pool removal to server...`,
|
|
1895
|
+
animationEmoji: '⚡'
|
|
1896
|
+
});
|
|
1160
1897
|
this.setSSOStatus("Publishing evolved identity keystone to server...", "info");
|
|
1161
|
-
|
|
1162
|
-
domainAddr:
|
|
1898
|
+
let syncRes = await getApiBridge().putEvolveKeystone({
|
|
1899
|
+
domainAddr: primaryTjpAddr,
|
|
1163
1900
|
keystoneIbGib: evolvedParent,
|
|
1164
1901
|
relatedKeystoneIbGibs: [] // No outward delegate timelines
|
|
1165
1902
|
});
|
|
1166
1903
|
if (!syncRes.success) {
|
|
1167
1904
|
throw new Error(`Server synchronization rejected: ${syncRes.message}`);
|
|
1168
1905
|
}
|
|
1906
|
+
// 4. Unregister any corresponding outward delegate for this provider if present
|
|
1907
|
+
const delegatesMap = evolvedParent.data?.delegates || this.ibGib?.data?.delegates;
|
|
1908
|
+
if (delegatesMap) {
|
|
1909
|
+
for (const [delegateTjpAddr, info] of Object.entries(delegatesMap)) {
|
|
1910
|
+
if (isCustodialDelegate({ delegateInfo: info, providerId })) {
|
|
1911
|
+
evolvedParent = await keystoneService.unregisterDelegate({
|
|
1912
|
+
parentKeystone: evolvedParent,
|
|
1913
|
+
delegateTjpAddr,
|
|
1914
|
+
masterSecret: masterSecret,
|
|
1915
|
+
signingPoolId: 'manage',
|
|
1916
|
+
metaspace,
|
|
1917
|
+
space,
|
|
1918
|
+
});
|
|
1919
|
+
this.showBusy({
|
|
1920
|
+
isBusy: true,
|
|
1921
|
+
title: 'Syncing to Server...',
|
|
1922
|
+
msg: `Publishing ${providerName} delegate unregistration to server...`,
|
|
1923
|
+
animationEmoji: '⚡'
|
|
1924
|
+
});
|
|
1925
|
+
syncRes = await getApiBridge().putEvolveKeystone({
|
|
1926
|
+
domainAddr: primaryTjpAddr,
|
|
1927
|
+
keystoneIbGib: evolvedParent,
|
|
1928
|
+
relatedKeystoneIbGibs: []
|
|
1929
|
+
});
|
|
1930
|
+
if (!syncRes.success) {
|
|
1931
|
+
throw new Error(`Server synchronization rejected: ${syncRes.message}`);
|
|
1932
|
+
}
|
|
1933
|
+
}
|
|
1934
|
+
}
|
|
1935
|
+
}
|
|
1169
1936
|
// 4. Update index locally
|
|
1170
1937
|
await updateSpecialIndex({
|
|
1171
1938
|
type: "keystones",
|
|
@@ -1178,12 +1945,17 @@ export class KeystoneDetailsComponentInstance extends IbGibDynamicComponentInsta
|
|
|
1178
1945
|
metaspace,
|
|
1179
1946
|
space
|
|
1180
1947
|
});
|
|
1181
|
-
this.setSSOStatus(`Successfully unlinked ${
|
|
1182
|
-
|
|
1948
|
+
this.setSSOStatus(`Successfully unlinked ${providerName}!`, "success");
|
|
1949
|
+
this.ibGibAddr = getIbGibAddr({ ibGib: evolvedParent });
|
|
1950
|
+
this.staticIbGib = evolvedParent;
|
|
1951
|
+
await this.loadIbGib({ getLatest: false });
|
|
1183
1952
|
}
|
|
1184
1953
|
catch (error) {
|
|
1185
|
-
console.error(`${lc}
|
|
1186
|
-
this.setSSOStatus(`
|
|
1954
|
+
console.error(`${lc} ${extractErrorMsg(error)}`, error);
|
|
1955
|
+
this.setSSOStatus(`Failed to unlink ${providerName}: ${extractErrorMsg(error)}`, "error");
|
|
1956
|
+
}
|
|
1957
|
+
finally {
|
|
1958
|
+
this.showBusy({ isBusy: false });
|
|
1187
1959
|
}
|
|
1188
1960
|
}
|
|
1189
1961
|
setChangePasswordStatus(msg, type) {
|
|
@@ -1192,9 +1964,346 @@ export class KeystoneDetailsComponentInstance extends IbGibDynamicComponentInsta
|
|
|
1192
1964
|
el.className = `status-msg ${type}`;
|
|
1193
1965
|
el.classList.remove('hidden');
|
|
1194
1966
|
}
|
|
1967
|
+
async handleAddMasterPassword() {
|
|
1968
|
+
const lc = `${this.lc}[${this.handleAddMasterPassword.name}]`;
|
|
1969
|
+
this.setChangePasswordStatus("Validating entries...", "info");
|
|
1970
|
+
try {
|
|
1971
|
+
const newPassword = this.elements.inputNewPasswordEl.value;
|
|
1972
|
+
const confirmPassword = this.elements.inputConfirmPasswordEl.value;
|
|
1973
|
+
if (!newPassword) {
|
|
1974
|
+
this.setChangePasswordStatus("Master password is required.", "error");
|
|
1975
|
+
return;
|
|
1976
|
+
}
|
|
1977
|
+
if (newPassword !== confirmPassword) {
|
|
1978
|
+
this.setChangePasswordStatus("Master password and confirmation do not match.", "error");
|
|
1979
|
+
return;
|
|
1980
|
+
}
|
|
1981
|
+
const initialManagePool = this.ibGib?.data?.challengePools?.find(p => p.id === POOL_ID_MANAGE);
|
|
1982
|
+
if (!initialManagePool) {
|
|
1983
|
+
this.setChangePasswordStatus("Primary keystone missing active manage challenge pool.", "error");
|
|
1984
|
+
return;
|
|
1985
|
+
}
|
|
1986
|
+
this.showBusy({
|
|
1987
|
+
isBusy: true,
|
|
1988
|
+
title: 'Elevating Identity...',
|
|
1989
|
+
msg: 'Generating cryptographic sovereign challenge pool...',
|
|
1990
|
+
animationEmoji: '🔑'
|
|
1991
|
+
});
|
|
1992
|
+
this.setChangePasswordStatus("Generating sovereign challenge pool...", "info");
|
|
1993
|
+
const newChallenges = await generatePoolChallenges({
|
|
1994
|
+
config: initialManagePool.config,
|
|
1995
|
+
masterSecret: newPassword,
|
|
1996
|
+
});
|
|
1997
|
+
const newManagePool = {
|
|
1998
|
+
id: POOL_ID_MANAGE,
|
|
1999
|
+
config: initialManagePool.config,
|
|
2000
|
+
challenges: newChallenges,
|
|
2001
|
+
};
|
|
2002
|
+
const primaryTjpAddr = (this.ibGib ? getTjpAddr({ ibGib: this.ibGib }) : undefined)
|
|
2003
|
+
|| (this.ibGibAddr ? (() => {
|
|
2004
|
+
const { ib } = getIbAndGib({ ibGibAddr: this.ibGibAddr });
|
|
2005
|
+
const { tjpGib } = getGibInfo({ ibGibAddr: this.ibGibAddr });
|
|
2006
|
+
return tjpGib && ib ? `${ib}^${tjpGib}` : this.ibGibAddr;
|
|
2007
|
+
})() : undefined)
|
|
2008
|
+
|| getIbGibAddr({ ibGib: this.ibGib });
|
|
2009
|
+
const custodianProvider = this.aggregatedDetails?.custodianProvider
|
|
2010
|
+
|| this.ibGib?.data?.frameDetails?.custodianProvider
|
|
2011
|
+
|| this.ibGib?.data?.custodianProvider;
|
|
2012
|
+
let requestBody = {
|
|
2013
|
+
primaryTjpAddr,
|
|
2014
|
+
newManagePool,
|
|
2015
|
+
};
|
|
2016
|
+
if (custodianProvider === 'google' || custodianProvider === 'github') {
|
|
2017
|
+
this.showBusy({
|
|
2018
|
+
isBusy: true,
|
|
2019
|
+
title: 'Re-authenticating with Provider...',
|
|
2020
|
+
msg: `Please complete authentication in the ${custodianProvider} popup window...`,
|
|
2021
|
+
animationEmoji: '🔒'
|
|
2022
|
+
});
|
|
2023
|
+
this.setChangePasswordStatus(`Please authenticate in the popup with ${custodianProvider} to verify ownership...`, "info");
|
|
2024
|
+
const resPopup = await executeSsoOAuthPopup({
|
|
2025
|
+
providerId: custodianProvider,
|
|
2026
|
+
parentTjpAddr: primaryTjpAddr,
|
|
2027
|
+
});
|
|
2028
|
+
if (!resPopup.success || !resPopup.code) {
|
|
2029
|
+
this.showBusy({ isBusy: false });
|
|
2030
|
+
this.setChangePasswordStatus(resPopup.message || "Re-authentication cancelled.", "error");
|
|
2031
|
+
return;
|
|
2032
|
+
}
|
|
2033
|
+
requestBody.providerId = custodianProvider;
|
|
2034
|
+
requestBody.code = resPopup.code;
|
|
2035
|
+
requestBody.redirectUri = resPopup.redirectUri;
|
|
2036
|
+
}
|
|
2037
|
+
else {
|
|
2038
|
+
// Email OTP flow
|
|
2039
|
+
const email = this.aggregatedDetails?.emails?.[0]
|
|
2040
|
+
|| this.ibGib?.data?.emails?.[0]
|
|
2041
|
+
|| this.ibGib?.data?.frameDetails?.emails?.[0];
|
|
2042
|
+
if (!email) {
|
|
2043
|
+
this.showBusy({ isBusy: false });
|
|
2044
|
+
this.setChangePasswordStatus("No email address found for this identity to send verification code.", "error");
|
|
2045
|
+
return;
|
|
2046
|
+
}
|
|
2047
|
+
const otpSection = this.elements?.sectionAddPasswordOtpEl;
|
|
2048
|
+
const otpInput = this.elements?.inputAddPasswordOtpEl;
|
|
2049
|
+
const isOtpVisible = otpSection && !otpSection.classList.contains('hidden');
|
|
2050
|
+
// Step 1: If OTP section is not visible yet, send code and show the OTP input inline in the card
|
|
2051
|
+
if (!isOtpVisible) {
|
|
2052
|
+
this.showBusy({
|
|
2053
|
+
isBusy: true,
|
|
2054
|
+
title: 'Sending Verification Code...',
|
|
2055
|
+
msg: `Sending 6-digit verification code to ${email}...`,
|
|
2056
|
+
animationEmoji: '📧'
|
|
2057
|
+
});
|
|
2058
|
+
this.setChangePasswordStatus(`Sending verification code to ${email}...`, "info");
|
|
2059
|
+
const resCode = await getApiBridge().postVerificationCode({
|
|
2060
|
+
email,
|
|
2061
|
+
primaryTjpAddr,
|
|
2062
|
+
});
|
|
2063
|
+
this.showBusy({ isBusy: false });
|
|
2064
|
+
if (!resCode.success) {
|
|
2065
|
+
this.setChangePasswordStatus(resCode.message || "Failed to send verification code.", "error");
|
|
2066
|
+
return;
|
|
2067
|
+
}
|
|
2068
|
+
if (otpSection) {
|
|
2069
|
+
otpSection.classList.remove('hidden');
|
|
2070
|
+
}
|
|
2071
|
+
if (this.elements?.descAddPasswordOtpEl) {
|
|
2072
|
+
this.elements.descAddPasswordOtpEl.textContent = `A 6-digit verification code was sent to ${email}. Enter it below to establish sovereign ownership:`;
|
|
2073
|
+
}
|
|
2074
|
+
if (this.elements?.btnChangePasswordEl) {
|
|
2075
|
+
this.elements.btnChangePasswordEl.textContent = "Verify Code & Add Master Password";
|
|
2076
|
+
}
|
|
2077
|
+
this.setChangePasswordStatus(`✉️ 6-digit verification code sent to ${email}. Enter the code and click "Verify Code & Add Master Password".`, "info");
|
|
2078
|
+
this.startResendCooldownTimer(30);
|
|
2079
|
+
if (otpInput) {
|
|
2080
|
+
otpInput.value = '';
|
|
2081
|
+
otpInput.focus();
|
|
2082
|
+
}
|
|
2083
|
+
return;
|
|
2084
|
+
}
|
|
2085
|
+
// Step 2: OTP section is visible - validate entered code
|
|
2086
|
+
const otpCode = (otpInput?.value || '').trim();
|
|
2087
|
+
if (!otpCode || otpCode.length < 6) {
|
|
2088
|
+
this.setChangePasswordStatus("Please enter the complete 6-digit verification code sent to your email.", "error");
|
|
2089
|
+
if (otpInput) {
|
|
2090
|
+
otpInput.focus();
|
|
2091
|
+
}
|
|
2092
|
+
return;
|
|
2093
|
+
}
|
|
2094
|
+
requestBody.email = email;
|
|
2095
|
+
requestBody.code = otpCode;
|
|
2096
|
+
}
|
|
2097
|
+
this.showBusy({
|
|
2098
|
+
isBusy: true,
|
|
2099
|
+
title: 'Updating Keystone Timeline...',
|
|
2100
|
+
msg: 'Publishing sovereign master password and evolving identity DAG...',
|
|
2101
|
+
animationEmoji: '⏳'
|
|
2102
|
+
});
|
|
2103
|
+
this.setChangePasswordStatus("Elevating identity to sovereign master password on server...", "info");
|
|
2104
|
+
const response = await fetch('/api/identity/custodian/add-master-password', {
|
|
2105
|
+
method: 'POST',
|
|
2106
|
+
headers: { 'Content-Type': 'application/json' },
|
|
2107
|
+
body: JSON.stringify(requestBody),
|
|
2108
|
+
});
|
|
2109
|
+
const resJson = await response.json();
|
|
2110
|
+
if (!response.ok || !resJson.success) {
|
|
2111
|
+
throw new Error(resJson.error || resJson.message || `Server error (${response.status})`);
|
|
2112
|
+
}
|
|
2113
|
+
const evolvedParent = resJson.primaryKeystone;
|
|
2114
|
+
// Save to local user space & metaspace
|
|
2115
|
+
const metaspace = await getGlobalMetaspace_waitIfNeeded();
|
|
2116
|
+
if (!metaspace) {
|
|
2117
|
+
throw new Error("Global metaspace not available");
|
|
2118
|
+
}
|
|
2119
|
+
const space = await metaspace.getLocalUserSpace({ lock: false });
|
|
2120
|
+
if (!space) {
|
|
2121
|
+
throw new Error("Local user space not available");
|
|
2122
|
+
}
|
|
2123
|
+
if (resJson.primaryGraph) {
|
|
2124
|
+
const graphIbGibs = Object.values(resJson.primaryGraph);
|
|
2125
|
+
await metaspace.put({ ibGibs: graphIbGibs, space });
|
|
2126
|
+
const timelinesMap = getTimelinesGroupedByTjp({ ibGibs: graphIbGibs });
|
|
2127
|
+
for (const tjpKey of Object.keys(timelinesMap)) {
|
|
2128
|
+
for (const frame of timelinesMap[tjpKey]) {
|
|
2129
|
+
await metaspace.registerNewIbGib({ ibGib: frame, space });
|
|
2130
|
+
}
|
|
2131
|
+
}
|
|
2132
|
+
}
|
|
2133
|
+
else {
|
|
2134
|
+
await metaspace.put({ ibGibs: [evolvedParent], space });
|
|
2135
|
+
await metaspace.registerNewIbGib({ ibGib: evolvedParent, space });
|
|
2136
|
+
}
|
|
2137
|
+
// 1. Create a password-based sync delegate for this device
|
|
2138
|
+
this.showBusy({
|
|
2139
|
+
isBusy: true,
|
|
2140
|
+
title: 'Creating Sovereign Sync Delegate...',
|
|
2141
|
+
msg: 'Deriving local device sync delegate keys from Master Password...',
|
|
2142
|
+
animationEmoji: '🔑'
|
|
2143
|
+
});
|
|
2144
|
+
const rawUsername = this.aggregatedDetails?.username
|
|
2145
|
+
|| this.ibGib?.data?.username
|
|
2146
|
+
|| this.aggregatedDetails?.emails?.[0]?.split('@')[0]
|
|
2147
|
+
|| this.ibGib?.data?.emails?.[0]?.split('@')[0]
|
|
2148
|
+
|| 'user';
|
|
2149
|
+
const { delegateKeystone: userSyncDelegate, deviceTag } = await createSovereignDeviceSyncDelegate({
|
|
2150
|
+
username: rawUsername,
|
|
2151
|
+
primaryTjpAddr,
|
|
2152
|
+
masterSecret: newPassword,
|
|
2153
|
+
metaspace,
|
|
2154
|
+
space,
|
|
2155
|
+
});
|
|
2156
|
+
// 2. Register the new user sync delegate on the evolved parent keystone
|
|
2157
|
+
this.showBusy({
|
|
2158
|
+
isBusy: true,
|
|
2159
|
+
title: 'Registering Sovereign Delegate...',
|
|
2160
|
+
msg: 'Registering user sync delegate on primary keystone...',
|
|
2161
|
+
animationEmoji: '📝'
|
|
2162
|
+
});
|
|
2163
|
+
const keystoneService = new KeystoneService_V1();
|
|
2164
|
+
const parentWithUserDelegate = await keystoneService.registerDelegate({
|
|
2165
|
+
parentKeystone: evolvedParent,
|
|
2166
|
+
delegateKeystone: userSyncDelegate,
|
|
2167
|
+
masterSecret: newPassword,
|
|
2168
|
+
signingPoolId: 'manage',
|
|
2169
|
+
metaspace,
|
|
2170
|
+
space,
|
|
2171
|
+
allowedVerbs: [KEYSTONE_VERB_SYNC, KEYSTONE_VERB_CONNECT],
|
|
2172
|
+
deviceName: deviceTag
|
|
2173
|
+
});
|
|
2174
|
+
// 3. Post evolved keystone with new delegate to server
|
|
2175
|
+
this.showBusy({
|
|
2176
|
+
isBusy: true,
|
|
2177
|
+
title: 'Syncing to Server...',
|
|
2178
|
+
msg: 'Publishing updated domain identity and sovereign delegate to server...',
|
|
2179
|
+
animationEmoji: '⚡'
|
|
2180
|
+
});
|
|
2181
|
+
const resSync = await getApiBridge().putEvolveKeystone({
|
|
2182
|
+
domainAddr: primaryTjpAddr,
|
|
2183
|
+
keystoneIbGib: parentWithUserDelegate,
|
|
2184
|
+
relatedKeystoneIbGibs: [userSyncDelegate]
|
|
2185
|
+
});
|
|
2186
|
+
if (!resSync.success) {
|
|
2187
|
+
console.warn(`${lc} Warning syncing user delegate to server: ${resSync.message}`);
|
|
2188
|
+
}
|
|
2189
|
+
// 4. Save parentWithUserDelegate and userSyncDelegate to local space & metaspace
|
|
2190
|
+
await metaspace.put({ ibGibs: [parentWithUserDelegate, userSyncDelegate], space });
|
|
2191
|
+
await metaspace.registerNewIbGib({ ibGib: parentWithUserDelegate, space });
|
|
2192
|
+
await metaspace.registerNewIbGib({ ibGib: userSyncDelegate, space });
|
|
2193
|
+
await updateSpecialIndex({
|
|
2194
|
+
type: "keystones",
|
|
2195
|
+
rel8nInfos: [
|
|
2196
|
+
{
|
|
2197
|
+
rel8nName: "keystone",
|
|
2198
|
+
ibGibs: [toDto({ ibGib: parentWithUserDelegate }), toDto({ ibGib: userSyncDelegate })]
|
|
2199
|
+
}
|
|
2200
|
+
],
|
|
2201
|
+
metaspace,
|
|
2202
|
+
space
|
|
2203
|
+
});
|
|
2204
|
+
const evolvedAddr = getIbGibAddr({ ibGib: parentWithUserDelegate });
|
|
2205
|
+
// Dispatch EVENT_IBGIB_IDENTITY_CHANGED
|
|
2206
|
+
window.dispatchEvent(new CustomEvent(EVENT_IBGIB_IDENTITY_CHANGED, {
|
|
2207
|
+
detail: { activeIdentityAddr: evolvedAddr },
|
|
2208
|
+
bubbles: true,
|
|
2209
|
+
composed: true
|
|
2210
|
+
}));
|
|
2211
|
+
this.setChangePasswordStatus("Successfully added Master Password! Identity is now sovereign.", "success");
|
|
2212
|
+
// Reset OTP section and button text
|
|
2213
|
+
if (this.elements?.sectionAddPasswordOtpEl) {
|
|
2214
|
+
this.elements.sectionAddPasswordOtpEl.classList.add('hidden');
|
|
2215
|
+
if (this.elements.inputAddPasswordOtpEl) {
|
|
2216
|
+
this.elements.inputAddPasswordOtpEl.value = '';
|
|
2217
|
+
}
|
|
2218
|
+
}
|
|
2219
|
+
if (this.elements?.btnChangePasswordEl) {
|
|
2220
|
+
this.elements.btnChangePasswordEl.textContent = "Update Password";
|
|
2221
|
+
}
|
|
2222
|
+
// Refresh the view
|
|
2223
|
+
this.ibGibAddr = evolvedAddr;
|
|
2224
|
+
this.staticIbGib = parentWithUserDelegate;
|
|
2225
|
+
await this.loadIbGib({ getLatest: true });
|
|
2226
|
+
// Clear inputs
|
|
2227
|
+
this.elements.inputNewPasswordEl.value = '';
|
|
2228
|
+
this.elements.inputConfirmPasswordEl.value = '';
|
|
2229
|
+
}
|
|
2230
|
+
catch (error) {
|
|
2231
|
+
console.error(`${lc} Elevation failed: ${extractErrorMsg(error)}`);
|
|
2232
|
+
this.setChangePasswordStatus(`Failed to add master password: ${extractErrorMsg(error)}`, "error");
|
|
2233
|
+
if (this.elements?.inputAddPasswordOtpEl && !this.elements.sectionAddPasswordOtpEl?.classList.contains('hidden')) {
|
|
2234
|
+
this.elements.inputAddPasswordOtpEl.select();
|
|
2235
|
+
this.elements.inputAddPasswordOtpEl.focus();
|
|
2236
|
+
}
|
|
2237
|
+
}
|
|
2238
|
+
finally {
|
|
2239
|
+
this.showBusy({ isBusy: false });
|
|
2240
|
+
}
|
|
2241
|
+
}
|
|
2242
|
+
async handleResendAddPasswordOtp() {
|
|
2243
|
+
const lc = `${this.lc}[${this.handleResendAddPasswordOtp.name}]`;
|
|
2244
|
+
const email = this.aggregatedDetails?.emails?.[0]
|
|
2245
|
+
|| this.ibGib?.data?.emails?.[0]
|
|
2246
|
+
|| this.ibGib?.data?.frameDetails?.emails?.[0];
|
|
2247
|
+
if (!email) {
|
|
2248
|
+
this.setChangePasswordStatus("No email address found for this identity to send verification code.", "error");
|
|
2249
|
+
return;
|
|
2250
|
+
}
|
|
2251
|
+
const primaryTjpAddr = (this.ibGib ? getTjpAddr({ ibGib: this.ibGib }) : undefined)
|
|
2252
|
+
|| (this.ibGibAddr ? (() => {
|
|
2253
|
+
const { ib } = getIbAndGib({ ibGibAddr: this.ibGibAddr });
|
|
2254
|
+
const { tjpGib } = getGibInfo({ ibGibAddr: this.ibGibAddr });
|
|
2255
|
+
return tjpGib && ib ? `${ib}^${tjpGib}` : this.ibGibAddr;
|
|
2256
|
+
})() : undefined)
|
|
2257
|
+
|| getIbGibAddr({ ibGib: this.ibGib });
|
|
2258
|
+
const btn = this.elements.btnResendAddPasswordOtpEl;
|
|
2259
|
+
if (btn) {
|
|
2260
|
+
btn.disabled = true;
|
|
2261
|
+
btn.textContent = "Sending...";
|
|
2262
|
+
}
|
|
2263
|
+
this.setChangePasswordStatus(`Resending verification code to ${email}...`, "info");
|
|
2264
|
+
try {
|
|
2265
|
+
const resCode = await getApiBridge().postVerificationCode({ email, primaryTjpAddr });
|
|
2266
|
+
if (resCode.success) {
|
|
2267
|
+
this.setChangePasswordStatus(`✉️ Fresh 6-digit verification code sent to ${email}!`, "success");
|
|
2268
|
+
this.startResendCooldownTimer(30);
|
|
2269
|
+
if (this.elements?.inputAddPasswordOtpEl) {
|
|
2270
|
+
this.elements.inputAddPasswordOtpEl.value = '';
|
|
2271
|
+
this.elements.inputAddPasswordOtpEl.focus();
|
|
2272
|
+
}
|
|
2273
|
+
}
|
|
2274
|
+
else {
|
|
2275
|
+
this.setChangePasswordStatus(resCode.message || "Failed to resend verification code.", "error");
|
|
2276
|
+
if (btn) {
|
|
2277
|
+
btn.disabled = false;
|
|
2278
|
+
btn.textContent = "Resend Code";
|
|
2279
|
+
}
|
|
2280
|
+
}
|
|
2281
|
+
}
|
|
2282
|
+
catch (err) {
|
|
2283
|
+
this.setChangePasswordStatus(`Error resending verification code: ${extractErrorMsg(err)}`, "error");
|
|
2284
|
+
if (btn) {
|
|
2285
|
+
btn.disabled = false;
|
|
2286
|
+
btn.textContent = "Resend Code";
|
|
2287
|
+
}
|
|
2288
|
+
}
|
|
2289
|
+
}
|
|
1195
2290
|
async handleChangePassword() {
|
|
1196
2291
|
const lc = `${this.lc}[${this.handleChangePassword.name}]`;
|
|
2292
|
+
const caps = !!this.ibGib ?
|
|
2293
|
+
getKeystoneCapabilities({ keystone: this.ibGib, aggregatedDetails: this.aggregatedDetails }) :
|
|
2294
|
+
undefined;
|
|
2295
|
+
const isCustodialOnly = !!caps ? caps.isCustodial && !caps.isSovereign : false;
|
|
2296
|
+
if (isCustodialOnly) {
|
|
2297
|
+
await this.handleAddMasterPassword();
|
|
2298
|
+
return;
|
|
2299
|
+
}
|
|
1197
2300
|
this.setChangePasswordStatus("Validating entries...", "info");
|
|
2301
|
+
this.showBusy({
|
|
2302
|
+
isBusy: true,
|
|
2303
|
+
title: 'Changing Master Password...',
|
|
2304
|
+
msg: 'Validating current password...',
|
|
2305
|
+
animationEmoji: '🔐'
|
|
2306
|
+
});
|
|
1198
2307
|
try {
|
|
1199
2308
|
const currentPassword = this.elements.inputCurrentPasswordEl.value;
|
|
1200
2309
|
const newPassword = this.elements.inputNewPasswordEl.value;
|
|
@@ -1222,6 +2331,12 @@ export class KeystoneDetailsComponentInstance extends IbGibDynamicComponentInsta
|
|
|
1222
2331
|
this.setChangePasswordStatus("Incorrect current password.", "error");
|
|
1223
2332
|
return;
|
|
1224
2333
|
}
|
|
2334
|
+
this.showBusy({
|
|
2335
|
+
isBusy: true,
|
|
2336
|
+
title: 'Evolving Master Password...',
|
|
2337
|
+
msg: 'Generating new challenge proofs and rotating local user delegates...',
|
|
2338
|
+
animationEmoji: '⚙️'
|
|
2339
|
+
});
|
|
1225
2340
|
this.setChangePasswordStatus("Evolving primary identity keystone...", "info");
|
|
1226
2341
|
const metaspace = await getGlobalMetaspace_waitIfNeeded();
|
|
1227
2342
|
if (!metaspace) {
|
|
@@ -1231,8 +2346,7 @@ export class KeystoneDetailsComponentInstance extends IbGibDynamicComponentInsta
|
|
|
1231
2346
|
if (!space) {
|
|
1232
2347
|
throw new Error(`(UNEXPECTED) couldn't get default local space? (E: 2fc1281c28351fd378a1d7f88eaa0526)`);
|
|
1233
2348
|
}
|
|
1234
|
-
// 2. Rotate parent identity
|
|
1235
|
-
debugger; // walk through this...looking for delegate failure // hmm...I thought we got change password going?
|
|
2349
|
+
// 2. Rotate parent identity and sovereign sync delegates
|
|
1236
2350
|
const { newKeystone: evolvedParent, newDelegates: evolvedDelegates } = await keystoneService.changePassword({
|
|
1237
2351
|
keystoneIbGib: this.ibGib,
|
|
1238
2352
|
newMasterSecret: newPassword,
|
|
@@ -1241,56 +2355,21 @@ export class KeystoneDetailsComponentInstance extends IbGibDynamicComponentInsta
|
|
|
1241
2355
|
metaspace,
|
|
1242
2356
|
space
|
|
1243
2357
|
});
|
|
1244
|
-
|
|
1245
|
-
this.
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
// const delegateKeystone = await keystoneService.getLatestKeystone({
|
|
1252
|
-
// addr: delegateAddr,
|
|
1253
|
-
// metaspace,
|
|
1254
|
-
// space,
|
|
1255
|
-
// });
|
|
1256
|
-
// // Identify user-owned challenge pools in the delegate that match the old password
|
|
1257
|
-
// const poolsToRotate: string[] = [];
|
|
1258
|
-
// for (const pool of delegateKeystone.data?.challengePools ?? []) {
|
|
1259
|
-
// if (!pool.isForeign) {
|
|
1260
|
-
// /**
|
|
1261
|
-
// * WARNING: There is an extreme edge case that if two pools
|
|
1262
|
-
// * happen to be owned by different parties but share the
|
|
1263
|
-
// * same secret, this will change the wrong pool's secret
|
|
1264
|
-
// * also. For V1, this shouldn't matter. But we need a
|
|
1265
|
-
// * better way of tracking pool ownership for V2 (or even
|
|
1266
|
-
// * V1 if we have multi-party support). (DO NOT REMOVE
|
|
1267
|
-
// * THIS COMMENT UNTIL THIS IS ADDRESSED)
|
|
1268
|
-
// */
|
|
1269
|
-
// const isPoolMatch = await keystoneService.verifySigningSecret({
|
|
1270
|
-
// keystoneIbGib: delegateKeystone,
|
|
1271
|
-
// signingSecret: currentPassword,
|
|
1272
|
-
// poolId: pool.id
|
|
1273
|
-
// });
|
|
1274
|
-
// if (isPoolMatch) { poolsToRotate.push(pool.id); }
|
|
1275
|
-
// }
|
|
1276
|
-
// }
|
|
1277
|
-
// if (poolsToRotate.length > 0) {
|
|
1278
|
-
// console.log(`${lc} Rotating pools in delegate ${delegateInfo.delegateTjpAddr}: ${poolsToRotate.join(', ')}`);
|
|
1279
|
-
// const {newKeystone: evolvedDelegate} = await keystoneService.changePassword({
|
|
1280
|
-
// keystoneIbGib: delegateKeystone,
|
|
1281
|
-
// newMasterSecret: newPassword,
|
|
1282
|
-
// signingSecret: currentPassword,
|
|
1283
|
-
// signingPoolId: 'manage',
|
|
1284
|
-
// poolIds: poolsToRotate,
|
|
1285
|
-
// metaspace,
|
|
1286
|
-
// space
|
|
1287
|
-
// });
|
|
1288
|
-
// evolvedDelegates.push(evolvedDelegate);
|
|
1289
|
-
// }
|
|
1290
|
-
// }
|
|
1291
|
-
// 4. Sync evolved parent and all evolved delegate keystones atomically
|
|
2358
|
+
// 3. Sync evolved parent and all evolved delegate keystones atomically
|
|
2359
|
+
this.showBusy({
|
|
2360
|
+
isBusy: true,
|
|
2361
|
+
title: 'Syncing to Server...',
|
|
2362
|
+
msg: 'Publishing rotated identity and delegates to server...',
|
|
2363
|
+
animationEmoji: '⚡'
|
|
2364
|
+
});
|
|
1292
2365
|
this.setChangePasswordStatus("Syncing evolved keystones to server...", "info");
|
|
1293
|
-
const parentTjpAddr =
|
|
2366
|
+
const parentTjpAddr = (this.ibGib ? getTjpAddr({ ibGib: this.ibGib }) : undefined)
|
|
2367
|
+
|| (this.ibGibAddr ? (() => {
|
|
2368
|
+
const { ib } = getIbAndGib({ ibGibAddr: this.ibGibAddr });
|
|
2369
|
+
const { tjpGib } = getGibInfo({ ibGibAddr: this.ibGibAddr });
|
|
2370
|
+
return tjpGib && ib ? `${ib}^${tjpGib}` : this.ibGibAddr;
|
|
2371
|
+
})() : undefined)
|
|
2372
|
+
|| getIbGibAddr({ ibGib: this.ibGib });
|
|
1294
2373
|
const syncRes = await getApiBridge().putEvolveKeystone({
|
|
1295
2374
|
domainAddr: parentTjpAddr,
|
|
1296
2375
|
keystoneIbGib: evolvedParent,
|
|
@@ -1299,7 +2378,7 @@ export class KeystoneDetailsComponentInstance extends IbGibDynamicComponentInsta
|
|
|
1299
2378
|
if (!syncRes.success) {
|
|
1300
2379
|
throw new Error(`Server synchronization rejected: ${syncRes.message}`);
|
|
1301
2380
|
}
|
|
1302
|
-
//
|
|
2381
|
+
// 4. Update index locally
|
|
1303
2382
|
this.setChangePasswordStatus("Updating local indexes...", "info");
|
|
1304
2383
|
const newKeystoneIbGibs = [toDto({ ibGib: evolvedParent })];
|
|
1305
2384
|
if (evolvedDelegates) {
|
|
@@ -1324,12 +2403,18 @@ export class KeystoneDetailsComponentInstance extends IbGibDynamicComponentInsta
|
|
|
1324
2403
|
this.elements.inputNewPasswordEl.value = "";
|
|
1325
2404
|
this.elements.inputConfirmPasswordEl.value = "";
|
|
1326
2405
|
this.setChangePasswordStatus("Password updated successfully!", "success");
|
|
2406
|
+
this.ibGibAddr = getIbGibAddr({ ibGib: evolvedParent });
|
|
2407
|
+
this.staticIbGib = evolvedParent;
|
|
1327
2408
|
await this.loadIbGib({ getLatest: true });
|
|
1328
2409
|
}
|
|
1329
2410
|
catch (error) {
|
|
2411
|
+
debugger; // error in handleChangePassword
|
|
1330
2412
|
console.error(`${lc} Password rotation failed: ${extractErrorMsg(error)}`);
|
|
1331
2413
|
this.setChangePasswordStatus(`Update failed: ${extractErrorMsg(error)}`, "error");
|
|
1332
2414
|
}
|
|
2415
|
+
finally {
|
|
2416
|
+
this.showBusy({ isBusy: false });
|
|
2417
|
+
}
|
|
1333
2418
|
}
|
|
1334
2419
|
}
|
|
1335
2420
|
//# sourceMappingURL=keystone-details.mjs.map
|