import { ComponentMetaspaceScope } from "../../component-types.mjs"; import thisCss from "./keystone-details.css"; import thisHtml from "./keystone-details.html"; import commonComponentStylesCss from "../../common-component-styles.css"; import { extractErrorMsg, pretty, getSaferSubstring, HashAlgorithm, delay } from "@ibgib/helper-gib/dist/helpers/utils-helper.mjs"; import { IbGibAddr } from "@ibgib/ts-gib/dist/types.mjs"; import { IbGib_V1 } from "@ibgib/ts-gib/dist/V1/types.mjs"; import { getIbAndGib, getIbGibAddr } from "@ibgib/ts-gib/dist/helper.mjs"; import { getGibInfo } from "@ibgib/ts-gib/dist/V1/transforms/transform-helper.mjs"; import { KeystoneService_V1 } from "@ibgib/core-gib/dist/keystone/keystone-service-v1.mjs"; import { KeystoneIbGib_V1 } from "@ibgib/core-gib/dist/keystone/keystone-types.mjs"; import { IbGibDynamicComponentMetaBase, IbGibDynamicComponentInstanceBase, } from "../../ibgib-dynamic-component-bases.mjs"; import { ElementsBase, IbGibDynamicComponentInstance, IbGibDynamicComponentInstanceInitOpts, } from "../../component-types.mjs"; import { EVENT_IBGIB_IDENTITY_REQUEST_CHANGE, EVENT_IBGIB_IDENTITY_CHANGED } from "../../../ui-constants.mjs"; import { getApiBridge } from "../../../../api/ibgib-api-bridge.mjs"; import { getKeystoneSyncService } from "../../../../identity/keystone-sync-service.mjs"; import { updateSpecialIndex } from "@ibgib/core-gib/dist/timeline/timeline-api.mjs"; import { getTjpAddr, toDto, getTimelinesGroupedByTjp } from "@ibgib/core-gib/dist/common/other/ibgib-helper.mjs"; import { parseSpaceIb } from "@ibgib/core-gib/dist/witness/space/space-helper.mjs"; import { executeSsoOAuthPopup } from "../../../../identity/sso-popup-helper.mjs"; import { getComponentCtorArg } from "../../../../app-bootstrap/init-orchestration.mjs"; import { promptForSecret, promptForAuthoritySecret } from "../../../../helpers.web.mjs"; import { isSsoProviderLinked, getKeystoneCapabilities, isDelegateExpired, deriveDelegateSecret, createSyncDelegateKeystone, isCustodialDelegate } from "@ibgib/core-gib/dist/keystone/delegate/delegate-helpers.mjs"; import { generatePoolChallenges, verifyPoolSecret } from "@ibgib/core-gib/dist/keystone/keystone-helpers.mjs"; import { POOL_ID_MANAGE, KEYSTONE_VERB_SYNC, KEYSTONE_VERB_CONNECT, KEYSTONE_VERB_MANAGE } from "@ibgib/core-gib/dist/keystone/keystone-constants.mjs"; import { generateDefaultDeviceTag } from "../../../../util/device-helper.mjs"; import { getContextModeService } from "../../../../context/context-mode-service.mjs"; import { createSovereignDeviceSyncDelegate, createOAuth2DeviceSyncDelegate, createEmailDeviceSyncDelegate, setDeviceDelegateSecret, getDeviceDelegateSecret } from "../../../../identity/device-delegate-helper.mjs"; import { isPasskeyPrfSupported, registerPasskeyCredential, evaluatePasskeyPrf, createPasskeyDeviceSyncDelegate, bufferToHex } from "../../../../identity/passkey-helper.mjs"; import { KeystonePasskeyEntry, KeystoneChallengePool, KeystonePoolConfig_HashV1, KeystoneChallengeType, KeystoneReplenishStrategy } from "@ibgib/core-gib/dist/keystone/keystone-types.mjs"; const logalot = false; export const KEYSTONE_DETAILS_COMPONENT_NAME = 'ibgib-keystone-details'; export class KeystoneDetailsComponentMeta extends IbGibDynamicComponentMetaBase { protected override lc: string = `[KeystoneDetailsComponentMeta]`; routeRegExp?: RegExp = new RegExp(`^${KEYSTONE_DETAILS_COMPONENT_NAME}$`); componentName = KEYSTONE_DETAILS_COMPONENT_NAME; constructor() { super(getComponentCtorArg()); if (!customElements.get(this.componentName)) { customElements.define(this.componentName, KeystoneDetailsComponentInstance); } } async createInstance({ path, ibGibAddr }: { path: string; ibGibAddr: IbGibAddr; }): Promise { const lc = `${this.lc}[${this.createInstance.name}]`; const component = document.createElement(this.componentName) as KeystoneDetailsComponentInstance; await component.initialize({ ibGibAddr, meta: this, html: thisHtml, css: [commonComponentStylesCss, thisCss], }); return component; } } interface KeystoneDetailsElements extends ElementsBase { identityDetailsViewEl: HTMLDivElement; identityAddrEl: HTMLElement; identityGenEl: HTMLElement; identityTimestampEl: HTMLElement; identityUuidEl: HTMLElement; identityNameEl: HTMLElement; identityDescriptionEl: HTMLElement; identityFrameDetailsEl: HTMLPreElement; identityAggrDetailsEl: HTMLPreElement; poolsContainerEl: HTMLDivElement; btnVerifyChainEl: HTMLButtonElement; btnSetActiveEl: HTMLButtonElement; btnCheckUpdatesEl: HTMLButtonElement; btnUpdateIdentityEl: HTMLButtonElement; syncStatusMsgEl: HTMLDivElement; verificationStatusEl: HTMLDivElement; rawAccordionHeaderEl: HTMLDivElement; rawAccordionContentEl: HTMLDivElement; rawKeystoneJsonEl: HTMLPreElement; delegatesCardEl: HTMLDivElement; delegatesListEl: HTMLTableSectionElement; delegatesAlertsEl?: HTMLDivElement; passkeysCardEl: HTMLDivElement; passkeysListEl: HTMLTableSectionElement; btnAddPasskeyEl: HTMLButtonElement; selectPasskeyAttachmentEl?: HTMLSelectElement; passkeyStatusMsgEl: HTMLDivElement; historicalWarningBannerEl: HTMLDivElement; containerEl: HTMLDivElement; ssoLinkCardEl: HTMLDivElement; btnLinkGoogleEl: HTMLDivElement; btnLinkGithubEl: HTMLDivElement; ssoStatusMsgEl: HTMLDivElement; inputCurrentPasswordEl: HTMLInputElement; inputNewPasswordEl: HTMLInputElement; inputConfirmPasswordEl: HTMLInputElement; btnChangePasswordEl: HTMLButtonElement; changePasswordStatusEl: HTMLDivElement; changePasswordCardEl: HTMLDivElement; sectionAddPasswordOtpEl: HTMLDivElement; inputAddPasswordOtpEl: HTMLInputElement; btnResendAddPasswordOtpEl: HTMLButtonElement; descAddPasswordOtpEl: HTMLParagraphElement; } export class KeystoneDetailsComponentInstance extends IbGibDynamicComponentInstanceBase implements IbGibDynamicComponentInstance { /** Identity is device-scoped and mode-agnostic: always use the persistent metaspace. */ protected override metaspaceScope: ComponentMetaspaceScope = ComponentMetaspaceScope.persistent; protected override lc: string = `[KeystoneDetailsComponentInstance]`; private staticIbGib: KeystoneIbGib_V1 | undefined; override get ibGib(): KeystoneIbGib_V1 | undefined { return this.staticIbGib; } private globalActiveAddr: string | null = null; private isHistoricalFrame: boolean = false; private _onIdentityChanged: ((e: any) => void) | null = null; private keystoneCache: Map = new Map(); private aggregatedDetails?: any; private resendCooldownInterval: any = null; constructor() { super(); } override async initialize(opts: IbGibDynamicComponentInstanceInitOpts): Promise { const lc = `${this.lc}[${this.initialize.name}]`; try { if (logalot) { console.log(`${lc} starting... (I: 4c9328643a18e56b56e57292c87bd226)`); } await super.initialize(opts); // await this.loadIbGib({ getLatest: false }); await this.loadIbGib({ getLatest: false }); } catch (error) { console.error(`${lc} ${extractErrorMsg(error)}`); throw error; } finally { if (logalot) { console.log(`${lc} complete.`); } } } override async disconnected(): Promise { if (this._onIdentityChanged) { window.removeEventListener(EVENT_IBGIB_IDENTITY_CHANGED, this._onIdentityChanged); } if (this.resendCooldownInterval) { clearInterval(this.resendCooldownInterval); this.resendCooldownInterval = null; } } private startResendCooldownTimer(durationSecs: number = 30): void { const btn = this.elements?.btnResendAddPasswordOtpEl; if (!btn) return; if (this.resendCooldownInterval) { clearInterval(this.resendCooldownInterval); this.resendCooldownInterval = null; } let remaining = durationSecs; btn.disabled = true; btn.textContent = `Resend Code (${remaining}s)`; this.resendCooldownInterval = setInterval(() => { remaining -= 1; if (remaining <= 0) { clearInterval(this.resendCooldownInterval); this.resendCooldownInterval = null; btn.disabled = false; btn.textContent = "Resend Code"; } else { btn.textContent = `Resend Code (${remaining}s)`; } }, 1000); } private async getKeystoneDetails(addr: string): Promise<{ username: string, description: string, isPrimary: boolean, n: number }> { const cached = this.keystoneCache.get(addr); if (cached) return cached; const metaspace = getContextModeService().getPersistentMetaspace(); if (!metaspace) return { username: '', description: '', isPrimary: false, n: 0 }; const space = await metaspace.getLocalUserSpace({ lock: false }); if (!space) return { username: '', description: '', isPrimary: false, n: 0 }; try { const keystoneSvc = new KeystoneService_V1(); const info = await keystoneSvc.getKeystoneCommonInfo({ addr, metaspace, space }); if (info) { const details = { username: info.name, description: info.description, isPrimary: info.isPrimary, n: info.n }; this.keystoneCache.set(addr, details); return details; } else { console.warn(`[getKeystoneDetails] Keystone common info not found in local space for addr: ${addr}`); } } catch (err) { console.warn(`Error getting details for keystone ${addr}: ${extractErrorMsg(err)}`); } return { username: '', description: '', isPrimary: false, n: 0 }; } override async created(): Promise { const lc = `${this.lc}[${this.created.name}]`; try { if (logalot) { console.log(`${lc} starting... (I: 0b2c6debcdac9db9b7928aa8695ed826)`); } this.elements = {} as KeystoneDetailsElements; const shadow = this.shadowRoot!; this.elements.identityDetailsViewEl = shadow.getElementById('identity-details-view') as HTMLDivElement; this.elements.identityAddrEl = shadow.getElementById('identity-addr') as HTMLElement; this.elements.identityGenEl = shadow.getElementById('identity-gen') as HTMLSpanElement; this.elements.identityTimestampEl = shadow.getElementById('identity-timestamp') as HTMLSpanElement; this.elements.identityUuidEl = shadow.getElementById('identity-uuid') as HTMLSpanElement; this.elements.identityNameEl = shadow.getElementById('identity-name') as HTMLElement; this.elements.identityDescriptionEl = shadow.getElementById('identity-description') as HTMLElement; this.elements.identityFrameDetailsEl = shadow.getElementById('identity-frame-details') as HTMLPreElement; this.elements.identityAggrDetailsEl = shadow.getElementById('identity-aggr-details') as HTMLPreElement; this.elements.poolsContainerEl = shadow.getElementById('pools-container') as HTMLDivElement; this.elements.btnVerifyChainEl = shadow.getElementById('btn-verify-chain') as HTMLButtonElement; this.elements.btnSetActiveEl = shadow.getElementById('btn-set-active') as HTMLButtonElement; this.elements.btnCheckUpdatesEl = shadow.getElementById('btn-check-updates') as HTMLButtonElement; this.elements.btnUpdateIdentityEl = shadow.getElementById('btn-update-identity') as HTMLButtonElement; this.elements.syncStatusMsgEl = shadow.getElementById('sync-status-msg') as HTMLDivElement; this.elements.verificationStatusEl = shadow.getElementById('verification-status') as HTMLDivElement; this.elements.rawAccordionHeaderEl = shadow.getElementById('raw-accordion-header') as HTMLDivElement; this.elements.rawAccordionContentEl = shadow.getElementById('raw-accordion-content') as HTMLDivElement; this.elements.rawKeystoneJsonEl = shadow.getElementById('raw-keystone-json') as HTMLPreElement; this.elements.delegatesCardEl = shadow.getElementById('delegates-card') as HTMLDivElement; this.elements.delegatesListEl = shadow.getElementById('delegates-list') as HTMLTableSectionElement; this.elements.delegatesAlertsEl = shadow.getElementById('delegates-alerts') as HTMLDivElement; this.elements.passkeysCardEl = shadow.getElementById('passkeys-card') as HTMLDivElement; this.elements.passkeysListEl = shadow.getElementById('passkeys-list') as HTMLTableSectionElement; this.elements.btnAddPasskeyEl = shadow.getElementById('btn-add-passkey') as HTMLButtonElement; this.elements.selectPasskeyAttachmentEl = shadow.getElementById('select-passkey-attachment') as HTMLSelectElement; this.elements.passkeyStatusMsgEl = shadow.getElementById('passkey-status-msg') as HTMLDivElement; this.elements.containerEl = shadow.getElementById('container') as HTMLDivElement; this.elements.historicalWarningBannerEl = shadow.getElementById('historical-warning-banner') as HTMLDivElement; this.elements.ssoLinkCardEl = shadow.getElementById('sso-link-card') as HTMLDivElement; this.elements.btnLinkGoogleEl = shadow.getElementById('btn-link-google') as HTMLDivElement; this.elements.btnLinkGithubEl = shadow.getElementById('btn-link-github') as HTMLDivElement; this.elements.ssoStatusMsgEl = shadow.getElementById('sso-status-msg') as HTMLDivElement; this.elements.inputCurrentPasswordEl = shadow.getElementById('input-current-password') as HTMLInputElement; this.elements.inputNewPasswordEl = shadow.getElementById('input-new-password') as HTMLInputElement; this.elements.inputConfirmPasswordEl = shadow.getElementById('input-confirm-password') as HTMLInputElement; this.elements.btnChangePasswordEl = shadow.getElementById('btn-change-password') as HTMLButtonElement; this.elements.changePasswordStatusEl = shadow.getElementById('change-password-status') as HTMLDivElement; this.elements.changePasswordCardEl = shadow.getElementById('change-password-card') as HTMLDivElement; this.elements.sectionAddPasswordOtpEl = shadow.getElementById('section-add-password-otp') as HTMLDivElement; this.elements.inputAddPasswordOtpEl = shadow.getElementById('input-add-password-otp') as HTMLInputElement; this.elements.btnResendAddPasswordOtpEl = shadow.getElementById('btn-resend-add-password-otp') as HTMLButtonElement; this.elements.descAddPasswordOtpEl = shadow.getElementById('desc-add-password-otp') as HTMLParagraphElement; // ElementBase requires contentEl this.elements.contentEl = shadow.getElementById('identity-details-view') as HTMLDivElement; this.initHandlers(); // Subscribe to global active identity changes this._onIdentityChanged = async (e: any) => { this.globalActiveAddr = e.detail?.activeIdentityAddr || null; this.keystoneCache.clear(); await this.updateFrameDetailsView(); this.updateActiveButtonState(); }; window.addEventListener(EVENT_IBGIB_IDENTITY_CHANGED, this._onIdentityChanged); await this.resolveActiveIdentity(); await this.loadIbGib({ getLatest: false }); } catch (error) { console.error(`${lc} ${extractErrorMsg(error)}`); throw error; } finally { if (logalot) { console.log(`${lc} complete.`); } } } private initHandlers() { // Setup raw JSON accordion toggle this.elements!.rawAccordionHeaderEl.addEventListener('click', () => this.toggleAccordion()); // Buttons this.elements!.btnVerifyChainEl.addEventListener('click', () => this.handleVerifyIdentityChain()); this.elements!.btnSetActiveEl.addEventListener('click', () => this.handleSetActiveIdentity()); this.elements!.btnCheckUpdatesEl.addEventListener('click', () => this.handleCheckForUpdates()); this.elements!.btnUpdateIdentityEl.addEventListener('click', () => this.handleUpdateIdentity()); this.elements!.btnAddPasskeyEl?.addEventListener('click', () => this.handleAddPasskey()); this.elements!.btnLinkGoogleEl.addEventListener('click', () => this.handleSSOClick('google')); this.elements!.btnLinkGithubEl.addEventListener('click', () => this.handleSSOClick('github')); this.elements!.btnChangePasswordEl.addEventListener('click', () => this.handleChangePassword()); this.elements!.btnResendAddPasswordOtpEl?.addEventListener('click', () => this.handleResendAddPasswordOtp()); } private toggleAccordion() { const header = this.elements!.rawAccordionHeaderEl; const content = this.elements!.rawAccordionContentEl; header.classList.toggle('expanded'); content.classList.toggle('expanded'); } private setSyncStatus(msg: string, type: 'info' | 'success' | 'error') { const statusEl = this.elements!.syncStatusMsgEl; statusEl.textContent = msg; statusEl.className = `status-msg ${type}`; statusEl.classList.remove('hidden'); } protected async handleCheckForUpdates(): Promise { const lc = `${this.lc}[${this.handleCheckForUpdates.name}]`; if (!this.ibGibAddr) { return; } this.setSyncStatus("Checking server for keystone updates...", "info"); try { const metaspace = getContextModeService().getPersistentMetaspace(); const space = metaspace ? await metaspace.getLocalUserSpace({ lock: false }) : undefined; const syncSvc = getKeystoneSyncService(); const res = await syncSvc.checkForKeystoneUpdate({ activePrimaryAddr: this.ibGibAddr, metaspace, space }); if (res.hasUpdate) { this.setSyncStatus(`Update Available (Server n=${res.serverN}, Local n=${res.localN})`, "info"); } else { this.setSyncStatus(`Up to Date (n=${res.localN})`, "success"); } } catch (error) { console.error(`${lc} ${extractErrorMsg(error)}`); this.setSyncStatus(`Check failed: ${extractErrorMsg(error)}`, "error"); } } protected async handleUpdateIdentity(): Promise { const lc = `${this.lc}[${this.handleUpdateIdentity.name}]`; if (!this.ibGibAddr) { return; } this.setSyncStatus("Updating identity keystone from server...", "info"); try { const metaspace = getContextModeService().getPersistentMetaspace(); const space = metaspace ? await metaspace.getLocalUserSpace({ lock: false }) : undefined; const syncSvc = getKeystoneSyncService(); const updatedFrame = await syncSvc.updateActiveKeystone({ activePrimaryAddr: this.ibGibAddr, metaspace, space }); if (updatedFrame) { const newAddr = getIbGibAddr({ ibGib: updatedFrame }); this.ibGibAddr = newAddr; this.staticIbGib = updatedFrame; this.setSyncStatus(`Identity updated successfully to n=${updatedFrame.data?.n ?? 0}`, "success"); await this.loadIbGib({ getLatest: true }); } else { this.setSyncStatus("Identity is already up to date.", "info"); } } catch (error) { console.error(`${lc} ${extractErrorMsg(error)}`); this.setSyncStatus(`Update failed: ${extractErrorMsg(error)}`, "error"); } } protected async handleVerifyIdentityChain() { const lc = `${this.lc}[${this.handleVerifyIdentityChain.name}]`; console.log(`${lc} Verifying identity keystone timeline...`); this.setVerificationStatus("Verification request sent...", "info"); try { this.setVerificationStatus("Verification complete. Root Keystone is verified (Genesis phase 1).", "success"); } catch (error) { this.setVerificationStatus(`Verification failed: ${extractErrorMsg(error)}`, "error"); } } protected async handleSetActiveIdentity() { if (!this.ibGibAddr) { return; } window.dispatchEvent(new CustomEvent(EVENT_IBGIB_IDENTITY_REQUEST_CHANGE, { detail: { activeIdentityAddr: this.ibGibAddr }, bubbles: true, composed: true })); } private setVerificationStatus(msg: string, type: 'info' | 'success' | 'error') { const statusEl = this.elements!.verificationStatusEl; statusEl.textContent = msg; statusEl.className = `status-msg ${type}`; statusEl.classList.remove('hidden'); } private async resolveActiveIdentity() { const lc = `${this.lc}[resolveActiveIdentity]`; try { const domainIdentity = getContextModeService().getPersistentIdentity()?.domainIdentity; this.globalActiveAddr = domainIdentity ? getIbGibAddr({ ibGib: domainIdentity }) : null; } catch (error) { console.error(`${lc} ${extractErrorMsg(error)}`); this.globalActiveAddr = null; } } override async loadIbGib(opts?: { getLatest?: boolean }): Promise { const lc = `${this.lc}[${this.loadIbGib.name}]`; try { if (logalot) { console.log(`${lc} starting... (I: f0f7e8d52cbdc37bec844b2875996e26)`); } if (!this.ibGibAddr) { return; } const metaspace = getContextModeService().getPersistentMetaspace(); if (!metaspace) { throw new Error(`(UNEXPECTED) metaspace falsy? (E: a9864dbbaefc1082ce7bd228ebd67c26)`); } let space = await metaspace.getLocalUserSpace({ lock: false }); if (!space) { throw new Error(`(UNEXPECTED) space falsy and we couldn't get default local user space from metaspace? (E: fa298edcecbda889cfd7b228fbd57c26)`); } let loadAddr = this.ibGibAddr; if (opts?.getLatest) { const latest = await metaspace.getLatestAddr({ addr: this.ibGibAddr, space }); if (latest) { loadAddr = latest; this.ibGibAddr = latest; } } const resGet = await metaspace.get({ addrs: [loadAddr], space }); if (resGet.success && resGet.ibGibs && resGet.ibGibs.length === 1) { this.staticIbGib = resGet.ibGibs[0] as KeystoneIbGib_V1; } else { const resIbGib = resGet.rawResultIbGib as any; const addrsNotFound = resIbGib?.data?.addrsNotFound ?? 'unknown'; throw new Error(`couldn't find address ${loadAddr}. addrsNotFound: ${addrsNotFound}? resGet.errorMsg: ${resGet.errorMsg} (E: e0e5c9b7de2837bc902fa88390cd9f26)`); } await this.updateFrameDetailsView(); } catch (error) { console.error(`${lc} Registered identity keystone address ${this.ibGibAddr} not found in local browser space: ${extractErrorMsg(error)} (E: genuuid)`); throw error; } finally { if (logalot) { console.log(`${lc} complete.`); } } } protected override async renderUI(): Promise { const lc = `${this.lc}[${this.renderUI.name}]`; try { if (logalot) { console.log(`${lc} starting... (I: c8aa086352d8079708e00f78b6ac0826)`); } await super.renderUI(); // await this.updateFrameDetailsView(); } catch (error) { console.error(`${lc} ${extractErrorMsg(error)}`); throw error; } finally { if (logalot) { console.log(`${lc} complete.`); } } } private updateActiveButtonState() { if (!this.elements) { return; } const tjpGib = getGibInfo({ ibGibAddr: this.ibGibAddr }).tjpGib ?? getIbAndGib({ ibGibAddr: this.ibGibAddr }).gib; const activeTjpGib = this.globalActiveAddr ? (getGibInfo({ ibGibAddr: this.globalActiveAddr }).tjpGib ?? getIbAndGib({ ibGibAddr: this.globalActiveAddr }).gib) : null; const isTimelineActive = !!(this.globalActiveAddr && tjpGib === activeTjpGib); if (isTimelineActive) { this.elements.btnSetActiveEl.textContent = "Active Identity"; this.elements.btnSetActiveEl.classList.add('active-primary'); } else { this.elements.btnSetActiveEl.textContent = "Set as Active Identity"; this.elements.btnSetActiveEl.classList.remove('active-primary'); } // Disable buttons if historical if (this.isHistoricalFrame) { this.elements.btnSetActiveEl.disabled = true; this.elements.btnVerifyChainEl.disabled = true; this.elements.btnLinkGoogleEl.classList.add('disabled'); this.elements.btnLinkGithubEl.classList.add('disabled'); } else { this.elements.btnVerifyChainEl.disabled = false; this.elements.btnSetActiveEl.disabled = isTimelineActive; this.elements.btnLinkGoogleEl.classList.remove('disabled'); this.elements.btnLinkGithubEl.classList.remove('disabled'); // Check if origin provider or linked const originProvider = this.aggregatedDetails?.custodianProvider || this.ibGib?.data?.frameDetails?.custodianProvider; const isGoogleLinked = !!this.ibGib ? isSsoProviderLinked({ keystone: this.ibGib, aggregatedDetails: this.aggregatedDetails, providerId: 'google' }) : false; const isGithubLinked = !!this.ibGib ? isSsoProviderLinked({ keystone: this.ibGib, aggregatedDetails: this.aggregatedDetails, providerId: 'github' }) : false; const caps = !!this.ibGib ? getKeystoneCapabilities({ keystone: this.ibGib, aggregatedDetails: this.aggregatedDetails }) : undefined; // Google Card States const googleCard = this.elements.btnLinkGoogleEl; const googleLabel = googleCard.querySelector('.toggle-state-text')!; if (isGoogleLinked) { if (!caps) { throw new Error(`(UNEXPECTED) isGoogleLinked but capabilities falsy? (E: cc62e8357b66860dd2cb8148c833b726)`); } googleCard.classList.remove('unlinked'); googleCard.classList.add('linked'); const isOrigin = originProvider === 'google'; if (isOrigin && !caps.isSovereign) { googleLabel.textContent = "Google (Primary Account) 🔒"; googleCard.title = "Cannot unlink primary account without first adding a Master Password below."; } else if (isOrigin) { googleLabel.textContent = "Google (Primary Account) ✅"; googleCard.title = "Click to unlink Google provider (requires Master Password)."; } else { googleLabel.textContent = "Google Linked ✅"; googleCard.title = "Click to unlink Google provider."; } } else { googleCard.classList.remove('linked'); googleCard.classList.add('unlinked'); googleLabel.textContent = "Unlinked"; googleCard.title = "Click to link Google account."; } // GitHub Card States const githubCard = this.elements.btnLinkGithubEl; const githubLabel = githubCard.querySelector('.toggle-state-text')!; if (isGithubLinked) { if (!caps) { throw new Error(`(UNEXPECTED) isGithubLinked but capabilities falsy? (E: e94688210538e0e82878088873a46826)`); } githubCard.classList.remove('unlinked'); githubCard.classList.add('linked'); const isOrigin = originProvider === 'github'; if (isOrigin && !caps.isSovereign) { githubLabel.textContent = "GitHub (Primary Account) 🔒"; githubCard.title = "Cannot unlink primary account without first adding a Master Password below."; } else if (isOrigin) { githubLabel.textContent = "GitHub (Primary Account) ✅"; githubCard.title = "Click to unlink GitHub provider (requires Master Password)."; } else { githubLabel.textContent = "GitHub Linked ✅"; githubCard.title = "Click to unlink GitHub provider."; } } else { githubCard.classList.remove('linked'); githubCard.classList.add('unlinked'); githubLabel.textContent = "Unlinked"; githubCard.title = "Click to link GitHub account."; } } } private updatePasswordCardView(): void { const caps = !!this.ibGib ? getKeystoneCapabilities({ keystone: this.ibGib, aggregatedDetails: this.aggregatedDetails }) : undefined; const cardTitleEl = this.elements?.changePasswordCardEl?.querySelector('h3'); const cardDescEl = this.elements?.changePasswordCardEl?.querySelector('.description'); const currentPasswordGroup = this.elements?.inputCurrentPasswordEl?.closest('.form-group') as HTMLElement | null; const btnChangePassword = this.elements?.btnChangePasswordEl; const hasPassword = !!caps?.hasPassword; if (!hasPassword) { if (cardTitleEl) cardTitleEl.textContent = "Add Master Password"; if (cardDescEl) cardDescEl.textContent = "Establish a master password for this identity. This enables sovereign password authentication alongside your passkeys and linked providers."; if (currentPasswordGroup) currentPasswordGroup.style.display = 'none'; if (btnChangePassword) btnChangePassword.textContent = "Add Master Password"; } else { if (cardTitleEl) cardTitleEl.textContent = "Change Password"; if (cardDescEl) cardDescEl.textContent = "Rotate your local password that drives this identity. This will cryptographically evolve your primary identity and all registered user-owned delegate keystones to the new password."; if (currentPasswordGroup) currentPasswordGroup.style.display = ''; if (btnChangePassword) btnChangePassword.textContent = "Update Password"; } if (this.elements?.sectionAddPasswordOtpEl) { this.elements.sectionAddPasswordOtpEl.classList.add('hidden'); if (this.elements.inputAddPasswordOtpEl) { this.elements.inputAddPasswordOtpEl.value = ''; } } } private async updateFrameDetailsView(): Promise { const lc = `${this.lc}[${this.updateFrameDetailsView.name}]`; try { if (logalot) { console.log(`${lc} starting... (I: 1fa9e66d2e78fb100874573e2db71d26)`); } if (!this.elements) { return; } const keystone = this.ibGib; if (!keystone) { this.clearActiveDetails(); return; } const keystoneAddr = this.ibGibAddr; // Update Verifiable Address this.elements.identityAddrEl.textContent = keystoneAddr; // Update Generation this.elements.identityGenEl.textContent = keystone.data?.n !== undefined ? String(keystone.data.n) : '0 (Genesis)'; // Update Timestamp const timestampStr = keystone.data?.timestamp; let formattedTimestamp = 'N/A'; if (timestampStr) { const parsed = parseInt(timestampStr); if (!isNaN(parsed)) { formattedTimestamp = new Date(parsed).toLocaleString(); } else { formattedTimestamp = new Date(timestampStr).toLocaleString(); } } this.elements.identityTimestampEl.textContent = formattedTimestamp; // Update UUID this.elements.identityUuidEl.textContent = keystone.data?.uuid || 'N/A'; // Show Frame Details const frameDetails = keystone.data?.frameDetails; this.elements.identityFrameDetailsEl.textContent = frameDetails ? pretty(frameDetails) : '{}'; // Get and Show Aggregated details up to the scrolled-to frame let isHistorical = false; const metaspace = getContextModeService().getPersistentMetaspace(); if (metaspace) { const space = await metaspace.getLocalUserSpace({ lock: false }); if (space) { try { const latestAddr = await metaspace.getLatestAddr({ addr: this.ibGibAddr, space }); if (latestAddr && latestAddr !== this.ibGibAddr) { isHistorical = true; } } catch (err) { console.warn(`${lc} Error checking latest address: ${extractErrorMsg(err)}`); } try { const keystoneSvc = new KeystoneService_V1(); const aggregated = await keystoneSvc.getAggregateDetails({ latestKeystone: keystone, metaspace, space }); this.aggregatedDetails = aggregated; this.elements.identityAggrDetailsEl.textContent = aggregated ? pretty(aggregated) : '{}'; // Update Username and Description fields this.elements.identityNameEl.textContent = aggregated?.username || aggregated?.profile?.name || aggregated?.name || '-'; this.elements.identityDescriptionEl.textContent = aggregated?.description || aggregated?.profile?.description || '-'; } catch (err) { console.warn(`${lc} Error aggregating details: ${extractErrorMsg(err)}`); this.aggregatedDetails = undefined; this.elements.identityAggrDetailsEl.textContent = '{}'; this.elements.identityNameEl.textContent = '-'; this.elements.identityDescriptionEl.textContent = '-'; } } else { this.aggregatedDetails = undefined; this.elements.identityAggrDetailsEl.textContent = '{}'; this.elements.identityNameEl.textContent = '-'; this.elements.identityDescriptionEl.textContent = '-'; } } else { this.aggregatedDetails = undefined; this.elements.identityAggrDetailsEl.textContent = '{}'; this.elements.identityNameEl.textContent = '-'; this.elements.identityDescriptionEl.textContent = '-'; } // Update historical warning state this.isHistoricalFrame = isHistorical; const banner = this.elements.historicalWarningBannerEl; const container = this.elements.containerEl; if (banner && container) { if (isHistorical) { banner.classList.remove('hidden'); container.classList.add('is-historical'); } else { banner.classList.add('hidden'); container.classList.remove('is-historical'); } } // Render Raw Timeline JSON this.elements.rawKeystoneJsonEl.textContent = pretty(keystone); // Render Challenge Pools this.renderChallengePools(keystone.data?.challengePools || []); // Render Delegates, Passkeys, SSO link, and Password Change cards (only if this is a primary keystone, hide them for delegate/session keystones) const details = await this.getKeystoneDetails(this.ibGibAddr); if (details.isPrimary) { this.elements.delegatesCardEl.classList.remove('hidden'); await this.renderDelegates(keystone.data?.delegates); this.elements.passkeysCardEl.classList.remove('hidden'); await this.renderPasskeys(this.aggregatedDetails?.passkeys); this.elements.ssoLinkCardEl.classList.remove('hidden'); this.elements.changePasswordCardEl.classList.remove('hidden'); this.updatePasswordCardView(); } else { this.elements.delegatesCardEl.classList.add('hidden'); this.elements.passkeysCardEl.classList.add('hidden'); this.elements.ssoLinkCardEl.classList.add('hidden'); this.elements.changePasswordCardEl.classList.add('hidden'); } this.updateActiveButtonState(); } catch (error) { console.error(`${lc} ${extractErrorMsg(error)}`); throw error; } finally { if (logalot) { console.log(`${lc} complete.`); } } } private clearActiveDetails() { if (!this.elements) { return; } this.aggregatedDetails = undefined; this.isHistoricalFrame = false; if (this.elements.historicalWarningBannerEl) { this.elements.historicalWarningBannerEl.classList.add('hidden'); } if (this.elements.containerEl) { this.elements.containerEl.classList.remove('is-historical'); } this.elements.identityGenEl.textContent = '-'; this.elements.identityTimestampEl.textContent = '-'; this.elements.identityUuidEl.textContent = '-'; this.elements.identityNameEl.textContent = '-'; this.elements.identityDescriptionEl.textContent = '-'; this.elements.identityFrameDetailsEl.textContent = '{}'; this.elements.identityAggrDetailsEl.textContent = '{}'; this.elements.poolsContainerEl.innerHTML = ''; if (this.elements.delegatesListEl) { this.elements.delegatesListEl.innerHTML = ''; } if (this.elements.passkeysListEl) { this.elements.passkeysListEl.innerHTML = ''; } if (this.elements.sectionAddPasswordOtpEl) { this.elements.sectionAddPasswordOtpEl.classList.add('hidden'); if (this.elements.inputAddPasswordOtpEl) { this.elements.inputAddPasswordOtpEl.value = ''; } } } private renderChallengePools(pools: any[]) { if (!this.elements) { return; } const container = this.elements.poolsContainerEl; container.innerHTML = ''; if (!pools || pools.length === 0) { container.innerHTML = `

No challenge pools found in this keystone.

`; return; } for (const pool of pools) { const card = document.createElement('div'); card.className = 'pool-card'; const config = pool.config || {}; const behavior = config.behavior || {}; const allowedVerbs = config.allowedVerbs || []; const activeCount = pool.challenges ? Object.keys(pool.challenges).length : 0; const isForeign = !!pool.isForeign; // Generate HTML for the challenges list const challenges = pool.challenges || {}; const challengeListHtml = Object.entries(challenges).map(([id, challenge]: [string, any]) => { const truncatedHash = challenge.hash && challenge.hash.length > 20 ? challenge.hash.substring(0, 10) + '...' + challenge.hash.substring(challenge.hash.length - 8) : challenge.hash || 'N/A'; return `
${id} ${truncatedHash}
`; }).join(''); card.innerHTML = `
${pool.id || 'Unnamed Pool'} ${isForeign ? 'Foreign' : 'Native'}
${allowedVerbs.length > 0 ? allowedVerbs.join(', ') : 'Any'}
FIFO: ${behavior.selectSequentially ?? 0}, Rand: ${behavior.selectRandomly ?? 0}
Challenges (${activeCount})
${challengeListHtml || '

No challenges active.

'}
`; container.appendChild(card); } } private showRemoteDelegateModal(info: any) { const shadow = this.shadowRoot!; let modalOverlay = shadow.getElementById('remote-delegate-modal-overlay'); if (modalOverlay) { modalOverlay.remove(); } modalOverlay = document.createElement('div'); modalOverlay.id = 'remote-delegate-modal-overlay'; modalOverlay.className = 'modal-overlay'; modalOverlay.style.cssText = ` position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0, 0, 0, 0.7); display: flex; align-items: center; justify-content: center; z-index: 9999; `; const modalContent = document.createElement('div'); modalContent.className = 'modal-card card'; modalContent.style.cssText = ` background: var(--clr-bg-card, #1e1e1e); color: var(--clr-text-primary, #ffffff); padding: 1.5rem; border-radius: 8px; max-width: 550px; width: 90%; box-shadow: 0 4px 20px rgba(0, 0, 0, 0.5); font-family: inherit; `; const deviceNameText = info.deviceName || info.details?.deviceTag || info.details?.name || 'Device Delegate'; const nameText = info.username || info.name || info.details?.name || info.details?.username || 'Remote Delegate'; const descText = info.description || info.details?.description || 'N/A'; const spaceIdText = info.spaceId || info.details?.spaceId || 'N/A'; const allowedVerbsText = info.allowedVerbs ? info.allowedVerbs.join(', ') : 'None'; const tjpAddrText = info.delegateTjpAddr || 'N/A'; modalContent.innerHTML = `

Remote Delegate Metadata Remote Device

This delegate origin is a remote device. Full timeline history stays isolated on the origin device unless explicitly synced.

Device Name
💻 ${deviceNameText}
Name
${nameText}
Description
${descText}
Space ID
${spaceIdText}
Allowed Verbs
${allowedVerbsText}
TJP Address
${tjpAddrText}
`; modalOverlay.appendChild(modalContent); shadow.appendChild(modalOverlay); const closeBtn = modalContent.querySelector('#close-remote-modal-btn'); closeBtn?.addEventListener('click', () => { modalOverlay?.remove(); }); modalOverlay.addEventListener('click', (e) => { if (e.target === modalOverlay) { modalOverlay?.remove(); } }); } private async renderDelegates(delegates?: Record) { const lc = `${this.lc}[${this.renderDelegates.name}]`; if (!this.elements) { return; } const listEl = this.elements.delegatesListEl; listEl.innerHTML = ''; if (this.elements.delegatesAlertsEl) { this.elements.delegatesAlertsEl.innerHTML = ''; } delegates = delegates ?? this.ibGib?.data?.delegates; if (!delegates || Object.keys(delegates).length === 0) { listEl.innerHTML = ` No registered delegates. `; return; } const metaspace = getContextModeService().getPersistentMetaspace(); if (!metaspace) { throw new Error(`(UNEXPECTED) metaspace falsy? (E: 6bcd78498138b11368f07fb824252526)`); } const space = await metaspace.getLocalUserSpace({ lock: false }); if (!space) { throw new Error(`(UNEXPECTED) space falsy? (E: 0f163cb6e4d8730f956508388e341826)`); } const localSpaceId = space.ib ? parseSpaceIb({ spaceIb: space.ib }).spaceId : undefined; const unhealthyDelegates: Array<{ delegateTjpAddr: string; deviceName: string; info: any; kind: 'sovereign' | 'passkey' | 'custodial'; }> = []; for (const [delegateTjpAddr, info] of Object.entries(delegates)) { const tr = document.createElement('tr'); const deviceName = info.deviceName || (info as any).details?.deviceTag || (info as any).details?.name || 'Device Delegate'; const tdName = document.createElement('td'); const delegateSpaceId = info.spaceId || (info as any).details?.spaceId; // Check if delegate is present in local space (via spaceId match or direct presence check) let isThisDevice = false; if (localSpaceId && delegateSpaceId === localSpaceId) { isThisDevice = true; } else { try { const resGet = await metaspace.get({ addrs: [delegateTjpAddr], space }); isThisDevice = !!(resGet.success && resGet.ibGibs && resGet.ibGibs.length > 0); } catch { isThisDevice = false; } } const badgeEl = document.createElement('span'); badgeEl.style.marginLeft = '0.5rem'; badgeEl.style.padding = '0.15rem 0.4rem'; badgeEl.style.borderRadius = '4px'; badgeEl.style.fontSize = '0.75rem'; badgeEl.style.fontWeight = 'bold'; if (isThisDevice) { badgeEl.textContent = 'This Device'; badgeEl.className = 'badge badge-this-device'; badgeEl.style.backgroundColor = '#1b5e20'; badgeEl.style.color = '#a5d6a7'; } else { badgeEl.textContent = 'Remote Device'; badgeEl.className = 'badge badge-remote-device'; badgeEl.style.backgroundColor = '#0d47a1'; badgeEl.style.color = '#90caf9'; } // Check expiration const isExpired = isDelegateExpired({ delegateInfo: info }); let expiredBadgeEl: HTMLSpanElement | null = null; if (isExpired) { expiredBadgeEl = document.createElement('span'); expiredBadgeEl.textContent = 'Expired'; expiredBadgeEl.className = 'badge badge-expired'; expiredBadgeEl.style.marginLeft = '0.3rem'; expiredBadgeEl.style.padding = '0.15rem 0.4rem'; expiredBadgeEl.style.borderRadius = '4px'; expiredBadgeEl.style.fontSize = '0.75rem'; expiredBadgeEl.style.fontWeight = 'bold'; expiredBadgeEl.style.backgroundColor = '#b71c1c'; expiredBadgeEl.style.color = '#ef9a9a'; } // Check secret health for local device delegates let secretHealthBadgeEl: HTMLSpanElement | null = null; let isSecretValid = false; let delegateKind: 'sovereign' | 'passkey' | 'custodial' = 'sovereign'; if (isCustodialDelegate({ delegateInfo: info })) { delegateKind = 'custodial'; } else if ( deviceName.toLowerCase().startsWith('passkey-') || (info as any).details?.credentialId || (info as any).details?.transport ) { delegateKind = 'passkey'; } else { delegateKind = 'sovereign'; } if (isThisDevice) { const storedSecret = await getDeviceDelegateSecret({ delegateTjpAddr }); if (storedSecret) { try { const keystoneService = new KeystoneService_V1(); const latestDelegate = await keystoneService.getLatestKeystone({ addr: delegateTjpAddr, metaspace, space }); if (latestDelegate?.data?.challengePools) { const syncPool = latestDelegate.data.challengePools.find((p: any) => p.allowedVerbs?.includes(KEYSTONE_VERB_SYNC) || p.id === KEYSTONE_VERB_SYNC || p.id === 'sync' ); if (syncPool) { isSecretValid = await verifyPoolSecret({ pool: syncPool, signingSecret: storedSecret }); } } } catch (err) { console.warn(`${lc} Error verifying delegate pool secret for ${delegateTjpAddr}: ${extractErrorMsg(err)}`); isSecretValid = false; } } secretHealthBadgeEl = document.createElement('span'); secretHealthBadgeEl.style.marginLeft = '0.3rem'; secretHealthBadgeEl.style.padding = '0.15rem 0.4rem'; secretHealthBadgeEl.style.borderRadius = '4px'; secretHealthBadgeEl.style.fontSize = '0.75rem'; secretHealthBadgeEl.style.fontWeight = 'bold'; if (isSecretValid) { secretHealthBadgeEl.textContent = '✅ Ready'; secretHealthBadgeEl.className = 'badge badge-ready'; secretHealthBadgeEl.style.backgroundColor = '#1b5e20'; secretHealthBadgeEl.style.color = '#a5d6a7'; } else { secretHealthBadgeEl.textContent = '⚠️ Needs Secret Update'; secretHealthBadgeEl.className = 'badge badge-needs-update'; secretHealthBadgeEl.style.backgroundColor = '#e65100'; secretHealthBadgeEl.style.color = '#ffe0b2'; unhealthyDelegates.push({ delegateTjpAddr, deviceName, info, kind: delegateKind, }); } } const applyNameAndBadge = (displayName: string, hoverText?: string) => { tdName.innerHTML = ''; const textSpan = document.createElement('span'); textSpan.textContent = displayName; tdName.appendChild(textSpan); if (badgeEl) { tdName.appendChild(badgeEl); } if (expiredBadgeEl) { tdName.appendChild(expiredBadgeEl); } if (secretHealthBadgeEl) { tdName.appendChild(secretHealthBadgeEl); } tdName.title = hoverText || displayName; }; const displayName = `💻 ${deviceName}`; applyNameAndBadge(displayName, `TJP: ${delegateTjpAddr}\nDevice: ${deviceName}`); const tdTjp = document.createElement('td'); tdTjp.textContent = delegateTjpAddr; tdTjp.title = delegateTjpAddr; const tdVerbs = document.createElement('td'); tdVerbs.textContent = info.allowedVerbs ? info.allowedVerbs.join(', ') : '-'; tdVerbs.title = tdVerbs.textContent; const isRemote = !isThisDevice; const tdActions = document.createElement('td'); const btnView = document.createElement('button'); btnView.className = 'action-btn secondary small'; btnView.textContent = 'View Details'; btnView.addEventListener('click', async () => { const lc = `${this.lc}[renderDelegates.click]`; if (isRemote) { this.showRemoteDelegateModal(info); } else { if (info.delegateAddr || delegateTjpAddr) { let addrToLoad = info.delegateAddr; try { const keystoneService = new KeystoneService_V1(); const latestKeystone = await keystoneService.getLatestKeystone({ addr: delegateTjpAddr, metaspace, space }); if (latestKeystone) { addrToLoad = getIbGibAddr({ ibGib: latestKeystone }); } } catch (err) { console.warn(`${lc} Error resolving latest delegate keystone: ${extractErrorMsg(err)}`); } this.dispatchEvent(new CustomEvent('ibgib-view-keystone-details', { detail: { addr: addrToLoad }, bubbles: true, composed: true })); } else { this.showRemoteDelegateModal(info); } } }); tdActions.appendChild(btnView); if (isThisDevice && !isSecretValid) { const btnUpdate = document.createElement('button'); btnUpdate.className = 'action-btn warning small'; btnUpdate.style.marginLeft = '0.4rem'; btnUpdate.textContent = '🔑 Update Secret'; btnUpdate.title = 'Enter password/credentials to update the stored delegate secret for this device'; btnUpdate.addEventListener('click', async () => { await this.handleUpdateDelegateSecret({ delegateTjpAddr, info, deviceName, kind: delegateKind }); }); tdActions.appendChild(btnUpdate); } const btnRevoke = document.createElement('button'); btnRevoke.className = 'action-btn danger small'; btnRevoke.style.marginLeft = '0.4rem'; btnRevoke.textContent = 'Revoke'; btnRevoke.addEventListener('click', async () => { await this.handleRevokeDelegate(delegateTjpAddr, deviceName); }); tdActions.appendChild(btnRevoke); tr.appendChild(tdName); tr.appendChild(tdTjp); tr.appendChild(tdVerbs); tr.appendChild(tdActions); listEl.appendChild(tr); } // Render delegates alerts container let alertsContainer = this.elements.delegatesAlertsEl; if (!alertsContainer) { alertsContainer = this.shadowRoot?.querySelector('#delegates-alerts') as HTMLDivElement; } if (alertsContainer) { alertsContainer.innerHTML = ''; for (const item of unhealthyDelegates) { const alertBox = document.createElement('div'); alertBox.className = 'delegates-alert-box'; let msgTitle = `Sync Secret Needs Update: ${item.deviceName}`; let msgBody = ''; if (item.kind === 'sovereign') { msgBody = 'Your password was changed on another device (or this device\'s stored secret is missing or outdated). Enter your current master password on this device to reactivate background synchronization.'; } else if (item.kind === 'passkey') { msgBody = 'This passkey delegate secret has not been derived on this device. Touch your security key or biometric authenticator to derive the local secret and reactivate background synchronization.'; } else { msgBody = 'Your custodial session credentials need to be re-authenticated to update the local sync delegate secret.'; } alertBox.innerHTML = `
⚠️ ${msgTitle}
${msgBody}
`; const btnRow = document.createElement('div'); btnRow.style.marginTop = '0.5rem'; const btnFix = document.createElement('button'); btnFix.className = 'action-btn warning small'; btnFix.textContent = '🔑 Update Secret'; btnFix.addEventListener('click', async () => { await this.handleUpdateDelegateSecret({ delegateTjpAddr: item.delegateTjpAddr, info: item.info, deviceName: item.deviceName, kind: item.kind }); }); btnRow.appendChild(btnFix); alertBox.appendChild(btnRow); alertsContainer.appendChild(alertBox); } } } protected async handleUpdateDelegateSecret({ delegateTjpAddr, info, deviceName, kind }: { delegateTjpAddr: string; info: any; deviceName: string; kind: 'sovereign' | 'passkey' | 'custodial'; }): Promise { const lc = `${this.lc}[${this.handleUpdateDelegateSecret.name}]`; try { const metaspace = getContextModeService().getPersistentMetaspace(); if (!metaspace) throw new Error("Metaspace not found"); const space = await metaspace.getLocalUserSpace({ lock: false }); if (!space) throw new Error("Local space not found"); const keystoneService = new KeystoneService_V1(); const latestDelegate = await keystoneService.getLatestKeystone({ addr: delegateTjpAddr, metaspace, space }); if (!latestDelegate) { throw new Error(`Could not find latest delegate keystone for ${delegateTjpAddr}`); } const syncPool = latestDelegate.data?.challengePools?.find((p: any) => p.allowedVerbs?.includes(KEYSTONE_VERB_SYNC) || p.id === KEYSTONE_VERB_SYNC || p.id === 'sync' ); if (!syncPool) { throw new Error(`Delegate keystone has no sync challenge pool configured.`); } let candidateSecret: string | undefined; if (kind === 'sovereign') { const enteredPassword = await promptForSecret({ msg: `Enter your current master password to re-authenticate '${deviceName}':`, confirm: false, }); if (!enteredPassword) { return; } this.showBusy({ isBusy: true, title: 'Verifying Secret...', msg: 'Verifying password against delegate sync challenge pool...', animationEmoji: '🔐' }); candidateSecret = await deriveDelegateSecret({ masterSecret: enteredPassword }); } else if (kind === 'passkey') { const credentialId = (info as any).details?.credentialId || this.ibGib?.data?.credentialIds?.[0]; if (!credentialId) { throw new Error('No credential ID associated with this passkey delegate.'); } this.showBusy({ isBusy: true, title: 'Touch Security Key...', msg: 'Touch your security key or authenticate to derive sync secret...', animationEmoji: '🔑' }); const prfRes = await evaluatePasskeyPrf({ credentialId, salt: delegateTjpAddr, }); candidateSecret = prfRes.passkeySecret; } else { throw new Error(`Custodial re-authentication for '${deviceName}' is not yet supported in this dialog.`); } if (!candidateSecret) { throw new Error('No secret was derived.'); } const isValid = await verifyPoolSecret({ pool: syncPool, signingSecret: candidateSecret }); if (!isValid) { throw new Error('Authentication failed: Secret does not match the delegate sync challenge pool. Please check your password.'); } // Persist valid secret to local IndexedDB (zero DAG mutation!) await setDeviceDelegateSecret({ delegateTjpAddr, delegateSecret: candidateSecret }); this.showBusy({ isBusy: true, title: 'Success!', msg: `Delegate sync secret updated successfully for ${deviceName}!`, animationEmoji: '✅' }); await delay(1000); // Re-render delegates await this.renderDelegates(); } catch (error) { console.error(`${lc} ${extractErrorMsg(error)}`, error); alert(`Secret update failed: ${extractErrorMsg(error)}`); } finally { this.showBusy({ isBusy: false }); } } protected async handleRevokeDelegate(delegateTjpAddr: string, deviceName: string): Promise { const lc = `${this.lc}[${this.handleRevokeDelegate.name}]`; try { let masterSecret: string; let matchedPoolId: string; try { const authResult = await promptForAuthoritySecret({ keystone: this.ibGib, aggregatedDetails: this.aggregatedDetails, title: 'Revoke Delegate', msg: `Enter Master Password or touch security key to revoke delegate '${deviceName}':`, verb: KEYSTONE_VERB_MANAGE }); masterSecret = authResult.secret; matchedPoolId = authResult.poolId; } catch (err) { // User cancelled prompt return; } if (!masterSecret) { return; } this.setSyncStatus("Revoking delegate device...", "info"); const metaspace = getContextModeService().getPersistentMetaspace(); if (!metaspace) throw new Error("Metaspace not found"); const space = await metaspace.getLocalUserSpace({ lock: false }); if (!space) { throw new Error("Local space not found"); } const keystoneSvc = new KeystoneService_V1(); // Unregister delegate from parent keystone const evolvedParent = await keystoneSvc.unregisterDelegate({ parentKeystone: this.ibGib!, delegateTjpAddr, masterSecret, signingPoolId: matchedPoolId, metaspace, space }); // Put evolved parent keystone to server via putEvolveKeystone const parentTjpAddr = getTjpAddr({ ibGib: this.ibGib! }) || getIbGibAddr({ ibGib: this.ibGib! }); const syncRes = await getApiBridge().putEvolveKeystone({ domainAddr: parentTjpAddr, keystoneIbGib: evolvedParent, relatedKeystoneIbGibs: [] }); if (!syncRes.success) { throw new Error(`Server rejected delegate revocation: ${syncRes.message}`); } // Save evolved parent to space and update local index await metaspace.put({ ibGibs: [evolvedParent], space }); await metaspace.registerNewIbGib({ ibGib: evolvedParent, space }); await updateSpecialIndex({ type: "keystones", rel8nInfos: [ { rel8nName: "keystone", ibGibs: [toDto({ ibGib: evolvedParent })] } ], metaspace, space }); const evolvedAddr = getIbGibAddr({ ibGib: evolvedParent }); // Dispatch EVENT_IBGIB_IDENTITY_CHANGED window.dispatchEvent(new CustomEvent(EVENT_IBGIB_IDENTITY_CHANGED, { detail: { activeIdentityAddr: evolvedAddr }, bubbles: true, composed: true })); this.setSyncStatus(`Successfully revoked device '${deviceName}'!`, "success"); // Refresh the view this.ibGibAddr = evolvedAddr; this.staticIbGib = evolvedParent; await this.loadIbGib({ getLatest: true }); } catch (error) { console.error(`${lc} Delegate revocation failed: ${extractErrorMsg(error)}`); this.setSyncStatus(`Revocation failed: ${extractErrorMsg(error)}`, "error"); } } private setPasskeyStatus(msg: string, type: 'info' | 'success' | 'error') { const statusEl = this.elements?.passkeyStatusMsgEl; if (!statusEl) return; statusEl.textContent = msg; statusEl.className = `status-msg ${type}`; statusEl.classList.remove('hidden'); } private async renderPasskeys(passkeys: Record | undefined) { if (!this.elements) { return; } const listEl = this.elements.passkeysListEl; listEl.innerHTML = ''; const caps = !!this.ibGib ? getKeystoneCapabilities({ keystone: this.ibGib, aggregatedDetails: this.aggregatedDetails }) : undefined; // Update button text depending on whether the account is custodial-only (elevation) or sovereign/hybrid const isCustodialOnly = !!caps ? caps.isCustodial && !caps.isSovereign : false; if (this.elements.btnAddPasskeyEl) { this.elements.btnAddPasskeyEl.textContent = isCustodialOnly ? "🛡️ Add Passkey (Passwordless Sovereign Elevation)" : "🛡️ Register New Passkey / Security Key"; } const validPasskeyEntries = Object.entries(passkeys || {}).filter(([_, entry]) => entry !== null && entry !== undefined); if (validPasskeyEntries.length === 0) { listEl.innerHTML = ` No hardware passkeys or biometrics registered. `; return; } for (const [credId, entry] of validPasskeyEntries) { const row = document.createElement('tr'); const createdText = entry.createdUTC ? new Date(entry.createdUTC).toLocaleString() : 'N/A'; const nameText = entry.name || 'Hardware Key'; // Format transport badges let transportBadgesHtml = ''; if (entry.transports && entry.transports.length > 0) { transportBadgesHtml = entry.transports.map(t => { let icon = '🔑'; let label: string = t; if (t === 'internal') { icon = '💻'; label = 'Touch ID / Built-in'; } else if (t === 'usb') { icon = '🔑'; label = 'USB Key'; } else if (t === 'nfc') { icon = '📶'; label = 'NFC'; } else if (t === 'ble' || t === 'hybrid') { icon = '📲'; label = 'Mobile / Bluetooth'; } return `${icon} ${label}`; }).join(' '); } else { transportBadgesHtml = `🛡️ Resident Passkey`; } row.innerHTML = `
${nameText}
${credId.substring(0, 16)}... ${transportBadgesHtml} ${createdText} `; const btnRemove = row.querySelector('.btn-remove-passkey') as HTMLButtonElement; btnRemove?.addEventListener('click', () => this.handleRemovePasskey(credId)); listEl.appendChild(row); } } protected async handleAddPasskey(): Promise { const lc = `${this.lc}[${this.handleAddPasskey.name}]`; if (!this.ibGib || !this.ibGibAddr) { return; } try { const caps = getKeystoneCapabilities({ keystone: this.ibGib, aggregatedDetails: this.aggregatedDetails }); const isCustodialOnly = caps.isCustodial && !caps.isSovereign; const supported = await isPasskeyPrfSupported(); if (!supported) { this.setPasskeyStatus("WebAuthn PRF extension is not supported in this browser environment.", "error"); return; } this.showBusy({ isBusy: true, title: 'Touch Biometrics or Security Key', msg: 'Please touch your fingerprint sensor, Face ID, or insert your USB security key when prompted...', animationEmoji: '🛡️' }); const username = this.aggregatedDetails?.username || this.ibGib.data?.frameDetails?.username || 'user'; const rawSalt = crypto.getRandomValues(new Uint8Array(32)); const saltHex = bufferToHex(rawSalt.buffer as ArrayBuffer); const attachmentVal = this.elements?.selectPasskeyAttachmentEl?.value; const authenticatorAttachment: AuthenticatorAttachment | undefined = (attachmentVal === 'cross-platform' || attachmentVal === 'platform') ? attachmentVal : undefined; const regResult = await registerPasskeyCredential({ username, displayName: username, salt: saltHex, userVerification: 'preferred', authenticatorAttachment, }); const { credentialId, transports } = regResult; let passkeySecret = regResult.passkeySecret; if (!passkeySecret) { const prfResult = await evaluatePasskeyPrf({ credentialId, salt: saltHex, transports, userVerification: 'preferred' }); passkeySecret = prfResult.passkeySecret; } const defaultDeviceTag = generateDefaultDeviceTag(); const passkeyName = defaultDeviceTag; const passkeyTag = getSaferSubstring({ text: passkeyName, length: 12, keepLiterals: ['-'] }) || 'key'; const poolId = `passkey-manage-${passkeyTag}`; const passkeyEntry: KeystonePasskeyEntry = { credentialId, poolId, name: passkeyName, salt: saltHex, createdUTC: new Date().toISOString(), ...(transports && transports.length > 0 ? { transports } : {}) }; const metaspace = getContextModeService().getPersistentMetaspace(); if (!metaspace) throw new Error("Metaspace not found"); const space = await metaspace.getLocalUserSpace({ lock: false }); if (!space) throw new Error("Local space not found"); const keystoneService = new KeystoneService_V1(); let evolvedKeystone: KeystoneIbGib_V1; const poolConfig: KeystonePoolConfig_HashV1 = { id: poolId, type: KeystoneChallengeType.hash_reveal_v1, salt: poolId, algo: HashAlgorithm.sha_256, rounds: 1, allowedVerbs: [KEYSTONE_VERB_MANAGE, KEYSTONE_VERB_SYNC, KEYSTONE_VERB_CONNECT], behavior: { size: 10, replenish: KeystoneReplenishStrategy.topUp, selectSequentially: 1, selectRandomly: 0, targetBindingCount: 0, } }; const challenges = await generatePoolChallenges({ config: poolConfig, masterSecret: passkeySecret }); const newPool: KeystoneChallengePool = { id: poolId, config: poolConfig, challenges }; let authSecret: string | undefined; let signingPoolId: string | undefined; if (isCustodialOnly) { // Elevate custodial account with sovereign passkey this.showBusy({ isBusy: true, title: 'Elevating Account Ownership...', msg: 'Establishing sovereign passkey pool on identity...', animationEmoji: '⚡' }); authSecret = passkeySecret; signingPoolId = poolId; evolvedKeystone = await keystoneService.addPools({ latestKeystone: this.ibGib, masterSecret: authSecret, signingPoolId, newPools: [newPool], frameDetails: { passkeys: { [credentialId]: passkeyEntry } }, dataToPatch: { credentialIds: [...(this.ibGib.data?.credentialIds || []), credentialId] }, metaspace, space }); } else { // Secondary passkey registration on existing sovereign identity: // Prompt user to authorize with current master password or passkey try { const authResult = await promptForAuthoritySecret({ keystone: this.ibGib, aggregatedDetails: this.aggregatedDetails, title: 'Authorize New Passkey', msg: 'Enter Master Password or touch security key to authorize adding this passkey:', verb: KEYSTONE_VERB_MANAGE }); authSecret = authResult.secret; signingPoolId = authResult.poolId; } catch (err) { this.setPasskeyStatus("Registration cancelled (authorization required).", "error"); return; } this.showBusy({ isBusy: true, title: 'Registering Passkey...', msg: 'Adding cryptographic challenge pool and evolving identity...', animationEmoji: '🛡️' }); evolvedKeystone = await keystoneService.addPools({ latestKeystone: this.ibGib, masterSecret: authSecret, signingPoolId, newPools: [newPool], frameDetails: { passkeys: { [credentialId]: passkeyEntry } }, dataToPatch: { credentialIds: [...(this.ibGib.data?.credentialIds || []), credentialId] }, metaspace, space }); // Sync pool addition frame to server const primaryTjpAddr = getTjpAddr({ ibGib: evolvedKeystone, defaultIfNone: 'incomingAddr' })!; const resSyncPool = await getApiBridge().putEvolveKeystone({ domainAddr: primaryTjpAddr, keystoneIbGib: evolvedKeystone, relatedKeystoneIbGibs: [] }); if (!resSyncPool.success) { throw new Error(`Server synchronization of passkey pool failed: ${resSyncPool.message}`); } } // Construct outward device sync delegate for this passkey this.showBusy({ isBusy: true, title: 'Creating Sync Delegate...', msg: 'Constructing outward passkey device sync delegate...', animationEmoji: '📱' }); const primaryTjpAddr = getTjpAddr({ ibGib: evolvedKeystone, defaultIfNone: 'incomingAddr' })!; const { delegateKeystone, deviceTag } = await createPasskeyDeviceSyncDelegate({ username, primaryTjpAddr, passkeySecret, transports, metaspace, space }); // Register outward sync delegate on primary keystone this.showBusy({ isBusy: true, title: 'Registering Delegate...', msg: 'Registering outward passkey delegate on primary identity...', animationEmoji: '📝' }); const delegateSigningPoolId = isCustodialOnly ? poolId : signingPoolId!; const delegateAuthSecret = isCustodialOnly ? passkeySecret : authSecret!; evolvedKeystone = await keystoneService.registerDelegate({ parentKeystone: evolvedKeystone, delegateKeystone, masterSecret: delegateAuthSecret, signingPoolId: delegateSigningPoolId, metaspace, space, allowedVerbs: [KEYSTONE_VERB_SYNC, KEYSTONE_VERB_CONNECT], deviceName: deviceTag }); // Sync evolved keystone and delegate to server this.showBusy({ isBusy: true, title: 'Syncing to Server...', msg: 'Publishing evolved identity keystone and delegate to server...', animationEmoji: '⚡' }); const resSync = await getApiBridge().putEvolveKeystone({ domainAddr: primaryTjpAddr, keystoneIbGib: evolvedKeystone, relatedKeystoneIbGibs: [delegateKeystone] }); if (!resSync.success) { throw new Error(`Server synchronization of delegate failed: ${resSync.message}`); } // Update Special Index await updateSpecialIndex({ type: "keystones", rel8nInfos: [ { rel8nName: "keystone", ibGibs: [toDto({ ibGib: evolvedKeystone }), toDto({ ibGib: delegateKeystone })] } ], metaspace, space }); this.setPasskeyStatus("Passkey successfully registered and bound to identity!", "success"); this.ibGibAddr = getIbGibAddr({ ibGib: evolvedKeystone }); this.staticIbGib = evolvedKeystone; await this.loadIbGib({ getLatest: false }); } catch (error) { console.error(`${lc} ${extractErrorMsg(error)}`, error); this.setPasskeyStatus(`Failed to register passkey: ${extractErrorMsg(error)}`, "error"); } finally { this.showBusy({ isBusy: false }); } } protected async handleRemovePasskey(credentialId: string): Promise { const lc = `${this.lc}[${this.handleRemovePasskey.name}]`; if (!this.ibGib || !this.ibGibAddr) { return; } const passkeyEntry = this.aggregatedDetails?.passkeys?.[credentialId]; const passkeyName = passkeyEntry?.name || 'this passkey'; try { const keystoneService = new KeystoneService_V1(); let masterSecret: string | undefined; let signingPoolId: string | undefined; try { const authResult = await promptForAuthoritySecret({ keystone: this.ibGib, aggregatedDetails: this.aggregatedDetails, title: 'Authorize Removal', msg: `Are you sure you want to remove "${passkeyName}"? Enter your Master Secret (Password) or touch security key to authorize:`, verb: KEYSTONE_VERB_MANAGE }); masterSecret = authResult.secret; signingPoolId = authResult.poolId; } catch (err) { this.setPasskeyStatus("Removal cancelled or unauthorized.", "error"); return; } this.showBusy({ isBusy: true, title: 'Revoking Passkey...', msg: 'Tombstoning passkey record and removing challenge pool...', animationEmoji: '🗑️' }); const metaspace = getContextModeService().getPersistentMetaspace(); if (!metaspace) throw new Error("Metaspace not found"); const space = await metaspace.getLocalUserSpace({ lock: false }); if (!space) throw new Error("Local space not found"); const poolIdToRemove = passkeyEntry?.poolId || `passkey-manage-${credentialId.substring(0, 8)}`; const poolExists = this.ibGib.data?.challengePools.some(p => p.id === poolIdToRemove); const filteredCredentialIds = (this.ibGib.data?.credentialIds || []).filter(id => id !== credentialId); let evolvedKeystone: KeystoneIbGib_V1; if (poolExists) { evolvedKeystone = await keystoneService.removePools({ latestKeystone: this.ibGib, masterSecret: masterSecret!, signingPoolId: signingPoolId!, poolIds: [poolIdToRemove], frameDetails: { passkeys: { [credentialId]: null } }, dataToPatch: { credentialIds: filteredCredentialIds }, metaspace, space }); } else { // If pool doesn't exist, just update frameDetails evolvedKeystone = await keystoneService.sign({ latestKeystone: this.ibGib, masterSecret: masterSecret!, poolId: signingPoolId, claim: { target: getIbGibAddr({ ibGib: this.ibGib }) }, frameDetails: { passkeys: { [credentialId]: null } }, dataToPatch: { credentialIds: filteredCredentialIds }, metaspace, space }); } // Sync to server this.showBusy({ isBusy: true, title: 'Syncing to Server...', msg: 'Publishing updated keystone to server...', animationEmoji: '⚡' }); const primaryTjpAddr = getTjpAddr({ ibGib: evolvedKeystone, defaultIfNone: 'incomingAddr' })!; await getApiBridge().putEvolveKeystone({ domainAddr: primaryTjpAddr, keystoneIbGib: evolvedKeystone, relatedKeystoneIbGibs: [] }); // Update Special Index await updateSpecialIndex({ type: "keystones", rel8nInfos: [ { rel8nName: "keystone", ibGibs: [toDto({ ibGib: evolvedKeystone })] } ], metaspace, space }); this.setPasskeyStatus(`Passkey "${passkeyName}" successfully removed.`, "success"); this.ibGibAddr = getIbGibAddr({ ibGib: evolvedKeystone }); this.staticIbGib = evolvedKeystone; await this.loadIbGib({ getLatest: false }); } catch (error) { console.error(`${lc} ${extractErrorMsg(error)}`, error); this.setPasskeyStatus(`Failed to remove passkey: ${extractErrorMsg(error)}`, "error"); } finally { this.showBusy({ isBusy: false }); } } override async handleContextUpdated(): Promise { const lc = `${this.lc}[${this.handleContextUpdated.name}]`; try { if (logalot) { console.log(`${lc} starting... (I: 243a88c1d4689c779bbbf2a4abd5da26)`); } // don't do anything when the context is updated? // is this hit? no. leave this as a no-op console.warn(`${lc} this component currently designed expecting that this does not hit (W: 95dc018b87027287e860de0826146826)`); await super.handleContextUpdated(); } catch (error) { console.error(`${lc} ${extractErrorMsg(error)}`); throw error; } finally { if (logalot) { console.log(`${lc} complete.`); } } } protected override async handleNewContextChild({ childIbGib }: { childIbGib: IbGib_V1; }): Promise { const lc = `${this.lc}[${this.handleNewContextChild.name}]`; try { if (logalot) { console.log(`${lc} starting... (I: b04dc63842f871658840233485659726)`); } // is this hit? no await super.handleNewContextChild({ childIbGib }); console.warn(`${lc} this component currently designed expecting that this does not hit (W: 95dc018b87027287e860de0826146826)`); } catch (error) { console.error(`${lc} ${extractErrorMsg(error)}`); throw error; } finally { if (logalot) { console.log(`${lc} complete.`); } } } protected async handleSSOLink(providerId: 'google' | 'github') { const lc = `${this.lc}[${this.handleSSOLink.name}]`; this.setSSOStatus(`Linking ${providerId}...`, "info"); const providerName = providerId === 'google' ? 'Google' : providerId === 'github' ? 'GitHub' : providerId; this.setSSOStatus(`Opening ${providerName} SSO authorization window...`, "info"); this.showBusy({ isBusy: true, title: `Connecting to ${providerName}...`, msg: `Opening ${providerName} SSO authorization window...\nPlease complete authorization in the popup window.`, animationEmoji: '🌐' }); try { const primaryTjpAddr = (this.ibGib ? getTjpAddr({ ibGib: this.ibGib }) : undefined) || (this.ibGibAddr ? (() => { const { ib } = getIbAndGib({ ibGibAddr: this.ibGibAddr }); const { tjpGib } = getGibInfo({ ibGibAddr: this.ibGibAddr }); return tjpGib && ib ? `${ib}^${tjpGib}` : this.ibGibAddr; })() : undefined); const resPopup = await executeSsoOAuthPopup({ providerId, parentTjpAddr: primaryTjpAddr }); if (!resPopup.success || !resPopup.code) { this.setSSOStatus(resPopup.message || `Linking cancelled.`, "error"); return; } await this.handleSSOLinkCallback(resPopup.code, resPopup.stateObj, resPopup.redirectUri!); } catch (error) { console.error(`${lc} SSO link initiation failed: ${extractErrorMsg(error)}`); this.setSSOStatus(`Link failed: ${extractErrorMsg(error)}`, "error"); } finally { this.showBusy({ isBusy: false }); } } private setSSOStatus(msg: string, type: 'info' | 'success' | 'error') { const statusEl = this.elements!.ssoStatusMsgEl; statusEl.textContent = msg; statusEl.className = `status-msg ${type}`; statusEl.classList.remove('hidden'); } protected async handleSSOLinkCallback(code: string, stateObj: any, redirectUri: string) { const lc = `${this.lc}[${this.handleSSOLinkCallback.name}]`; const providerName = stateObj.providerId === 'google' ? 'Google' : stateObj.providerId === 'github' ? 'GitHub' : stateObj.providerId; this.setSSOStatus(`Retrieving challenge pool configuration from server...`, "info"); this.showBusy({ isBusy: true, title: `Linking ${providerName}...`, msg: `Validating ${providerName} authorization with server...`, animationEmoji: '🔐' }); try { // 1. Exchange OAuth code for custodian public pool config const res = await fetch('/api/identity/sso/link', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ code, providerId: stateObj.providerId, parentTjpAddr: stateObj.parentTjpAddr, userNonce: stateObj.userNonce, redirectUri }) }); if (!res.ok) { const errBody = await res.json().catch(() => ({})); throw new Error(`Server link exchange failed: ${errBody.message || res.statusText}`); } const linkData = await res.json(); if (!linkData.success || !linkData.custodianPool) { throw new Error(`Failed to retrieve custodian challenge pool: ${linkData.message || 'unknown error'}`); } const custodianPool = linkData.custodianPool; // 2. Prompt user for authorization secret const keystoneService = new KeystoneService_V1(); let masterSecret: string | undefined; let signingPoolId: string | undefined; try { const authResult = await promptForAuthoritySecret({ keystone: this.ibGib, aggregatedDetails: this.aggregatedDetails, title: `Link ${providerName}`, msg: `Enter Master Password or touch security key to authorize linking ${providerName}:`, verb: KEYSTONE_VERB_MANAGE }); masterSecret = authResult.secret; signingPoolId = authResult.poolId; } catch (error) { this.setSSOStatus("Link cancelled or failed.", "error"); return; } this.showBusy({ isBusy: true, title: 'Evolving Identity...', msg: `Installing ${providerName} custodian pool and signing evolution...`, animationEmoji: '📝' }); this.setSSOStatus("Evolving identity locally and generating cryptographic signatures...", "info"); const metaspace = getContextModeService().getPersistentMetaspace(); if (!metaspace) throw new Error("Metaspace not found"); const space = await metaspace.getLocalUserSpace({ lock: false }); if (!space) { throw new Error("Local space not found"); } // 3. Evolve parent keystone to include the custodian pool (n -> n+1) const evolvedParent = await keystoneService.addPools({ latestKeystone: this.ibGib!, masterSecret: masterSecret!, signingPoolId: signingPoolId!, newPools: [custodianPool], metaspace, space }); // Sync evolved parent (n+1) to server first const syncRes1 = await getApiBridge().putEvolveKeystone({ domainAddr: stateObj.parentTjpAddr, keystoneIbGib: evolvedParent, relatedKeystoneIbGibs: [] }); if (!syncRes1.success) { throw new Error(`Server synchronization of linked pool rejected: ${syncRes1.message}`); } // 4. Create and register OAuth2 sync delegate for this provider (n+1 -> n+2) const username = this.aggregatedDetails?.username || this.ibGib?.data?.username || 'user'; const { delegateKeystone, deviceTag } = await createOAuth2DeviceSyncDelegate({ providerId: stateObj.providerId, username, primaryTjpAddr: stateObj.parentTjpAddr, custodianReqs: linkData.custodianReqs, metaspace, space, }); const parentWithDelegate = await keystoneService.registerDelegate({ parentKeystone: evolvedParent, delegateKeystone, masterSecret: masterSecret!, signingPoolId: signingPoolId!, metaspace, space, allowedVerbs: [KEYSTONE_VERB_SYNC, KEYSTONE_VERB_CONNECT], deviceName: deviceTag, }); await metaspace.put({ ibGibs: [delegateKeystone, parentWithDelegate], space }); // 5. Sync parent with registered delegate (n+2) to server this.showBusy({ isBusy: true, title: 'Syncing to Server...', msg: 'Publishing linked identity and delegate to server...', animationEmoji: '⚡' }); this.setSSOStatus("Publishing evolved identity keystone to server...", "info"); const syncRes2 = await getApiBridge().putEvolveKeystone({ domainAddr: stateObj.parentTjpAddr, keystoneIbGib: parentWithDelegate, relatedKeystoneIbGibs: [delegateKeystone] }); if (!syncRes2.success) { throw new Error(`Server synchronization of delegate rejected: ${syncRes2.message}`); } // 6. Update index locally await updateSpecialIndex({ type: "keystones", rel8nInfos: [ { rel8nName: "keystone", ibGibs: [toDto({ ibGib: parentWithDelegate }), toDto({ ibGib: delegateKeystone })] } ], metaspace, space }); this.setSSOStatus(`Successfully linked with ${stateObj.providerId}!`, "success"); this.ibGibAddr = getIbGibAddr({ ibGib: parentWithDelegate }); this.staticIbGib = parentWithDelegate; await this.loadIbGib({ getLatest: false }); } catch (error) { console.error(`${lc} SSO callback processing failed: ${extractErrorMsg(error)}`); this.setSSOStatus(`Link failed: ${extractErrorMsg(error)}`, "error"); } finally { this.showBusy({ isBusy: false }); } } protected async handleSSOClick(providerId: 'google' | 'github') { const isLinked = !!this.ibGib ? isSsoProviderLinked({ keystone: this.ibGib, aggregatedDetails: this.aggregatedDetails, providerId }) : false; if (isLinked) { const caps = getKeystoneCapabilities({ keystone: this.ibGib!, aggregatedDetails: this.aggregatedDetails }); if (!caps.isSovereign) { 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"); return; } await this.handleSSOUnlink(providerId); } else { await this.handleSSOLink(providerId); } } protected async handleSSOUnlink(providerId: 'google' | 'github') { const lc = `${this.lc}[${this.handleSSOUnlink.name}]`; const providerName = providerId === 'google' ? 'Google' : providerId === 'github' ? 'GitHub' : providerId; this.setSSOStatus(`Unlinking ${providerName}...`, "info"); try { // 1. Prompt user for authorization secret const keystoneService = new KeystoneService_V1(); let masterSecret: string | undefined; let signingPoolId: string | undefined; try { const authResult = await promptForAuthoritySecret({ keystone: this.ibGib, aggregatedDetails: this.aggregatedDetails, title: `Unlink ${providerName}`, msg: `Are you sure you want to unlink your ${providerName} account? If so, enter your Master Password or touch security key to authorize.`, verb: KEYSTONE_VERB_MANAGE }); masterSecret = authResult.secret; signingPoolId = authResult.poolId; } catch (error) { this.setSSOStatus("Unlink cancelled or failed.", "error"); return; } this.showBusy({ isBusy: true, title: 'Unlinking Provider...', msg: `Removing ${providerName} custodian pool and delegate keys...`, animationEmoji: '🔓' }); this.setSSOStatus("Evolving identity locally and removing custodian pool...", "info"); const metaspace = getContextModeService().getPersistentMetaspace(); if (!metaspace) throw new Error("Metaspace not found"); const space = await metaspace.getLocalUserSpace({ lock: false }); if (!space) { throw new Error("Local space not found"); } // 2. Evolve parent keystone to remove the custodian pool const targetPoolId = `custodian-manage-${providerId}`; let evolvedParent = await keystoneService.removePools({ latestKeystone: this.ibGib!, masterSecret: masterSecret!, signingPoolId: signingPoolId!, poolIds: [targetPoolId], metaspace, space }); const primaryTjpAddr = (this.ibGib ? getTjpAddr({ ibGib: this.ibGib }) : undefined) || (this.ibGibAddr ? (() => { const { ib } = getIbAndGib({ ibGibAddr: this.ibGibAddr }); const { tjpGib } = getGibInfo({ ibGibAddr: this.ibGibAddr }); return tjpGib && ib ? `${ib}^${tjpGib}` : this.ibGibAddr; })() : undefined) || getIbGibAddr({ ibGib: this.ibGib! }); // 3. Sync pool removal frame to server this.showBusy({ isBusy: true, title: 'Syncing to Server...', msg: `Publishing ${providerName} pool removal to server...`, animationEmoji: '⚡' }); this.setSSOStatus("Publishing evolved identity keystone to server...", "info"); let syncRes = await getApiBridge().putEvolveKeystone({ domainAddr: primaryTjpAddr, keystoneIbGib: evolvedParent, relatedKeystoneIbGibs: [] // No outward delegate timelines }); if (!syncRes.success) { throw new Error(`Server synchronization rejected: ${syncRes.message}`); } // 4. Unregister any corresponding outward delegate for this provider if present const delegatesMap = evolvedParent.data?.delegates || this.ibGib?.data?.delegates; if (delegatesMap) { for (const [delegateTjpAddr, info] of Object.entries(delegatesMap)) { if (isCustodialDelegate({ delegateInfo: info, providerId })) { evolvedParent = await keystoneService.unregisterDelegate({ parentKeystone: evolvedParent, delegateTjpAddr, masterSecret: masterSecret!, signingPoolId: 'manage', metaspace, space, }); this.showBusy({ isBusy: true, title: 'Syncing to Server...', msg: `Publishing ${providerName} delegate unregistration to server...`, animationEmoji: '⚡' }); syncRes = await getApiBridge().putEvolveKeystone({ domainAddr: primaryTjpAddr, keystoneIbGib: evolvedParent, relatedKeystoneIbGibs: [] }); if (!syncRes.success) { throw new Error(`Server synchronization rejected: ${syncRes.message}`); } } } } // 4. Update index locally await updateSpecialIndex({ type: "keystones", rel8nInfos: [ { rel8nName: "keystone", ibGibs: [toDto({ ibGib: evolvedParent })] } ], metaspace, space }); this.setSSOStatus(`Successfully unlinked ${providerName}!`, "success"); this.ibGibAddr = getIbGibAddr({ ibGib: evolvedParent }); this.staticIbGib = evolvedParent; await this.loadIbGib({ getLatest: false }); } catch (error) { console.error(`${lc} ${extractErrorMsg(error)}`, error); this.setSSOStatus(`Failed to unlink ${providerName}: ${extractErrorMsg(error)}`, "error"); } finally { this.showBusy({ isBusy: false }); } } private setChangePasswordStatus(msg: string, type: 'info' | 'success' | 'error') { const el = this.elements!.changePasswordStatusEl; el.textContent = msg; el.className = `status-msg ${type}`; el.classList.remove('hidden'); } protected async handleAddMasterPassword(): Promise { const lc = `${this.lc}[${this.handleAddMasterPassword.name}]`; this.setChangePasswordStatus("Validating entries...", "info"); try { const newPassword = this.elements!.inputNewPasswordEl.value; const confirmPassword = this.elements!.inputConfirmPasswordEl.value; if (!newPassword) { this.setChangePasswordStatus("Master password is required.", "error"); return; } if (newPassword !== confirmPassword) { this.setChangePasswordStatus("Master password and confirmation do not match.", "error"); return; } const initialManagePool = this.ibGib?.data?.challengePools?.find(p => p.id === POOL_ID_MANAGE); if (!initialManagePool) { this.setChangePasswordStatus("Primary keystone missing active manage challenge pool.", "error"); return; } this.showBusy({ isBusy: true, title: 'Elevating Identity...', msg: 'Generating cryptographic sovereign challenge pool...', animationEmoji: '🔑' }); this.setChangePasswordStatus("Generating sovereign challenge pool...", "info"); const newChallenges = await generatePoolChallenges({ config: initialManagePool.config, masterSecret: newPassword, }); const newManagePool = { id: POOL_ID_MANAGE, config: initialManagePool.config, challenges: newChallenges, }; const primaryTjpAddr = (this.ibGib ? getTjpAddr({ ibGib: this.ibGib }) : undefined) || (this.ibGibAddr ? (() => { const { ib } = getIbAndGib({ ibGibAddr: this.ibGibAddr }); const { tjpGib } = getGibInfo({ ibGibAddr: this.ibGibAddr }); return tjpGib && ib ? `${ib}^${tjpGib}` : this.ibGibAddr; })() : undefined) || getIbGibAddr({ ibGib: this.ibGib! }); const custodianProvider = this.aggregatedDetails?.custodianProvider || this.ibGib?.data?.frameDetails?.custodianProvider || (this.ibGib?.data as any)?.custodianProvider; let requestBody: any = { primaryTjpAddr, newManagePool, }; if (custodianProvider === 'google' || custodianProvider === 'github') { this.showBusy({ isBusy: true, title: 'Re-authenticating with Provider...', msg: `Please complete authentication in the ${custodianProvider} popup window...`, animationEmoji: '🔒' }); this.setChangePasswordStatus(`Please authenticate in the popup with ${custodianProvider} to verify ownership...`, "info"); const resPopup = await executeSsoOAuthPopup({ providerId: custodianProvider, parentTjpAddr: primaryTjpAddr, }); if (!resPopup.success || !resPopup.code) { this.showBusy({ isBusy: false }); this.setChangePasswordStatus(resPopup.message || "Re-authentication cancelled.", "error"); return; } requestBody.providerId = custodianProvider; requestBody.code = resPopup.code; requestBody.redirectUri = resPopup.redirectUri; } else { // Email OTP flow const email = this.aggregatedDetails?.emails?.[0] || this.ibGib?.data?.emails?.[0] || this.ibGib?.data?.frameDetails?.emails?.[0]; if (!email) { this.showBusy({ isBusy: false }); this.setChangePasswordStatus("No email address found for this identity to send verification code.", "error"); return; } const otpSection = this.elements?.sectionAddPasswordOtpEl; const otpInput = this.elements?.inputAddPasswordOtpEl; const isOtpVisible = otpSection && !otpSection.classList.contains('hidden'); // Step 1: If OTP section is not visible yet, send code and show the OTP input inline in the card if (!isOtpVisible) { this.showBusy({ isBusy: true, title: 'Sending Verification Code...', msg: `Sending 6-digit verification code to ${email}...`, animationEmoji: '📧' }); this.setChangePasswordStatus(`Sending verification code to ${email}...`, "info"); const resCode = await getApiBridge().postVerificationCode({ email, primaryTjpAddr, }); this.showBusy({ isBusy: false }); if (!resCode.success) { this.setChangePasswordStatus(resCode.message || "Failed to send verification code.", "error"); return; } if (otpSection) { otpSection.classList.remove('hidden'); } if (this.elements?.descAddPasswordOtpEl) { this.elements.descAddPasswordOtpEl.textContent = `A 6-digit verification code was sent to ${email}. Enter it below to establish sovereign ownership:`; } if (this.elements?.btnChangePasswordEl) { this.elements.btnChangePasswordEl.textContent = "Verify Code & Add Master Password"; } this.setChangePasswordStatus(`✉️ 6-digit verification code sent to ${email}. Enter the code and click "Verify Code & Add Master Password".`, "info"); this.startResendCooldownTimer(30); if (otpInput) { otpInput.value = ''; otpInput.focus(); } return; } // Step 2: OTP section is visible - validate entered code const otpCode = (otpInput?.value || '').trim(); if (!otpCode || otpCode.length < 6) { this.setChangePasswordStatus("Please enter the complete 6-digit verification code sent to your email.", "error"); if (otpInput) { otpInput.focus(); } return; } requestBody.email = email; requestBody.code = otpCode; } this.showBusy({ isBusy: true, title: 'Updating Keystone Timeline...', msg: 'Publishing sovereign master password and evolving identity DAG...', animationEmoji: '⏳' }); this.setChangePasswordStatus("Elevating identity to sovereign master password on server...", "info"); const response = await fetch('/api/identity/custodian/add-master-password', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(requestBody), }); const resJson = await response.json(); if (!response.ok || !resJson.success) { throw new Error(resJson.error || resJson.message || `Server error (${response.status})`); } const evolvedParent = resJson.primaryKeystone; // Save to local user space & metaspace const metaspace = getContextModeService().getPersistentMetaspace(); if (!metaspace) { throw new Error("Global metaspace not available"); } const space = await metaspace.getLocalUserSpace({ lock: false }); if (!space) { throw new Error("Local user space not available"); } if (resJson.primaryGraph) { const graphIbGibs = Object.values(resJson.primaryGraph) as any[]; await metaspace.put({ ibGibs: graphIbGibs, space }); const timelinesMap = getTimelinesGroupedByTjp({ ibGibs: graphIbGibs }); for (const tjpKey of Object.keys(timelinesMap)) { for (const frame of timelinesMap[tjpKey]) { await metaspace.registerNewIbGib({ ibGib: frame, space }); } } } else { await metaspace.put({ ibGibs: [evolvedParent], space }); await metaspace.registerNewIbGib({ ibGib: evolvedParent, space }); } await this.registerAndSyncSovereignPasswordDelegate({ evolvedParent, newPassword, primaryTjpAddr, metaspace, space }); } catch (error) { console.error(`${lc} Elevation failed: ${extractErrorMsg(error)}`); this.setChangePasswordStatus(`Failed to add master password: ${extractErrorMsg(error)}`, "error"); if (this.elements?.inputAddPasswordOtpEl && !this.elements.sectionAddPasswordOtpEl?.classList.contains('hidden')) { this.elements.inputAddPasswordOtpEl.select(); this.elements.inputAddPasswordOtpEl.focus(); } } finally { this.showBusy({ isBusy: false }); } } protected async handleResendAddPasswordOtp(): Promise { const lc = `${this.lc}[${this.handleResendAddPasswordOtp.name}]`; const email = this.aggregatedDetails?.emails?.[0] || this.ibGib?.data?.emails?.[0] || this.ibGib?.data?.frameDetails?.emails?.[0]; if (!email) { this.setChangePasswordStatus("No email address found for this identity to send verification code.", "error"); return; } const primaryTjpAddr = (this.ibGib ? getTjpAddr({ ibGib: this.ibGib }) : undefined) || (this.ibGibAddr ? (() => { const { ib } = getIbAndGib({ ibGibAddr: this.ibGibAddr }); const { tjpGib } = getGibInfo({ ibGibAddr: this.ibGibAddr }); return tjpGib && ib ? `${ib}^${tjpGib}` : this.ibGibAddr; })() : undefined) || getIbGibAddr({ ibGib: this.ibGib! }); const btn = this.elements!.btnResendAddPasswordOtpEl; if (btn) { btn.disabled = true; btn.textContent = "Sending..."; } this.setChangePasswordStatus(`Resending verification code to ${email}...`, "info"); try { const resCode = await getApiBridge().postVerificationCode({ email, primaryTjpAddr }); if (resCode.success) { this.setChangePasswordStatus(`✉️ Fresh 6-digit verification code sent to ${email}!`, "success"); this.startResendCooldownTimer(30); if (this.elements?.inputAddPasswordOtpEl) { this.elements.inputAddPasswordOtpEl.value = ''; this.elements.inputAddPasswordOtpEl.focus(); } } else { this.setChangePasswordStatus(resCode.message || "Failed to resend verification code.", "error"); if (btn) { btn.disabled = false; btn.textContent = "Resend Code"; } } } catch (err) { this.setChangePasswordStatus(`Error resending verification code: ${extractErrorMsg(err)}`, "error"); if (btn) { btn.disabled = false; btn.textContent = "Resend Code"; } } } /** * Common DRY post-elevation workflow: creates and registers the password-based * device sync delegate, synchronizes the evolved identity with the delegate to the server, * updates the local space and special index, and dispatches the identity change event. */ private async registerAndSyncSovereignPasswordDelegate({ evolvedParent, newPassword, primaryTjpAddr, metaspace, space, }: { evolvedParent: KeystoneIbGib_V1; newPassword: string; primaryTjpAddr: string; metaspace: any; space: any; }): Promise { const lc = `${this.lc}[${this.registerAndSyncSovereignPasswordDelegate.name}]`; // 1. Create a password-based sync delegate for this device this.showBusy({ isBusy: true, title: 'Creating Sovereign Sync Delegate...', msg: 'Deriving local device sync delegate keys from Master Password...', animationEmoji: '🔑' }); await delay(50); const rawUsername = this.aggregatedDetails?.username || this.ibGib?.data?.username || this.aggregatedDetails?.emails?.[0]?.split('@')[0] || this.ibGib?.data?.emails?.[0]?.split('@')[0] || 'user'; const { delegateKeystone: userSyncDelegate, deviceTag } = await createSovereignDeviceSyncDelegate({ username: rawUsername, primaryTjpAddr, masterSecret: newPassword, metaspace, space, }); // 2. Register the new user sync delegate on the evolved parent keystone this.showBusy({ isBusy: true, title: 'Registering Sovereign Delegate...', msg: 'Registering user sync delegate on primary keystone...', animationEmoji: '📝' }); await delay(50); const keystoneService = new KeystoneService_V1(); const parentWithUserDelegate = await keystoneService.registerDelegate({ parentKeystone: evolvedParent, delegateKeystone: userSyncDelegate, masterSecret: newPassword, signingPoolId: POOL_ID_MANAGE, metaspace, space, allowedVerbs: [KEYSTONE_VERB_SYNC, KEYSTONE_VERB_CONNECT], deviceName: deviceTag }); // 3. Post evolved keystone with new delegate to server this.showBusy({ isBusy: true, title: 'Syncing to Server...', msg: 'Publishing updated domain identity and sovereign delegate to server...', animationEmoji: '⚡' }); const resSync = await getApiBridge().putEvolveKeystone({ domainAddr: primaryTjpAddr, keystoneIbGib: parentWithUserDelegate, relatedKeystoneIbGibs: [userSyncDelegate] }); if (!resSync.success) { throw new Error(`Server sync of password delegate failed: ${resSync.message}`); } // 4. Save parentWithUserDelegate and userSyncDelegate to local space & metaspace await metaspace.put({ ibGibs: [parentWithUserDelegate, userSyncDelegate], space }); await metaspace.registerNewIbGib({ ibGib: parentWithUserDelegate, space }); await metaspace.registerNewIbGib({ ibGib: userSyncDelegate, space }); await updateSpecialIndex({ type: "keystones", rel8nInfos: [ { rel8nName: "keystone", ibGibs: [toDto({ ibGib: parentWithUserDelegate }), toDto({ ibGib: userSyncDelegate })] } ], metaspace, space }); const evolvedAddr = getIbGibAddr({ ibGib: parentWithUserDelegate }); // Dispatch EVENT_IBGIB_IDENTITY_CHANGED window.dispatchEvent(new CustomEvent(EVENT_IBGIB_IDENTITY_CHANGED, { detail: { activeIdentityAddr: evolvedAddr }, bubbles: true, composed: true })); this.setChangePasswordStatus("Successfully added Master Password! Identity is now sovereign.", "success"); // Reset OTP section and button text if (this.elements?.sectionAddPasswordOtpEl) { this.elements.sectionAddPasswordOtpEl.classList.add('hidden'); if (this.elements.inputAddPasswordOtpEl) { this.elements.inputAddPasswordOtpEl.value = ''; } } if (this.elements?.btnChangePasswordEl) { this.elements.btnChangePasswordEl.textContent = "Update Password"; } // Refresh the view this.ibGibAddr = evolvedAddr; this.staticIbGib = parentWithUserDelegate; await this.loadIbGib({ getLatest: true }); // Clear inputs this.elements!.inputNewPasswordEl.value = ''; this.elements!.inputConfirmPasswordEl.value = ''; return parentWithUserDelegate; } /** * Handles adding a master password to a sovereign identity that was created with passkeys only. * Authorizes via passkey biometric/hardware assertion and cryptographically adds the manage pool. */ protected async handleAddPasswordToPasskeyIdentity(): Promise { const lc = `${this.lc}[${this.handleAddPasswordToPasskeyIdentity.name}]`; this.setChangePasswordStatus("Validating entries...", "info"); try { const newPassword = this.elements!.inputNewPasswordEl.value; const confirmPassword = this.elements!.inputConfirmPasswordEl.value; if (!newPassword) { this.setChangePasswordStatus("Master password is required.", "error"); return; } if (newPassword !== confirmPassword) { this.setChangePasswordStatus("Master password and confirmation do not match.", "error"); return; } const passkeysMap = this.aggregatedDetails?.passkeys || (this.ibGib?.data as any)?.frameDetails?.passkeys || (this.ibGib?.data as any)?.passkeys || {}; const passkeysList: KeystonePasskeyEntry[] = Array.isArray(passkeysMap) ? passkeysMap : Object.values(passkeysMap); const passkeyPool = this.ibGib?.data?.challengePools?.find(p => p.id.startsWith('passkey-manage-')); if (!passkeyPool) { this.setChangePasswordStatus("No active passkey manage pool found on this identity.", "error"); return; } let passkeySecret: string | undefined; let signingPoolId: string | undefined; try { const authResult = await promptForAuthoritySecret({ keystone: this.ibGib, aggregatedDetails: this.aggregatedDetails, title: 'Authorize Master Password', msg: 'Please touch your fingerprint sensor, Face ID, or security key to authorize adding a Master Password...', verb: KEYSTONE_VERB_MANAGE }); passkeySecret = authResult.secret; signingPoolId = authResult.poolId; } catch (err) { this.setChangePasswordStatus("Authorization cancelled.", "error"); return; } this.showBusy({ isBusy: true, title: 'Adding Master Password...', msg: 'Compiling cryptographic master password pool and evolving identity...', animationEmoji: '🔑' }); await delay(50); this.setChangePasswordStatus("Generating sovereign master password challenge pool...", "info"); const metaspace = getContextModeService().getPersistentMetaspace(); if (!metaspace) throw new Error("Global metaspace not available"); const space = await metaspace.getLocalUserSpace({ lock: false }); if (!space) throw new Error("Local user space not available"); const poolConfig: KeystonePoolConfig_HashV1 = { id: POOL_ID_MANAGE, type: KeystoneChallengeType.hash_reveal_v1, salt: POOL_ID_MANAGE, algo: HashAlgorithm.sha_256, rounds: 1, allowedVerbs: [KEYSTONE_VERB_MANAGE, KEYSTONE_VERB_SYNC, KEYSTONE_VERB_CONNECT], behavior: { size: 10, replenish: KeystoneReplenishStrategy.topUp, selectSequentially: 1, selectRandomly: 0, targetBindingCount: 0, } }; const newChallenges = await generatePoolChallenges({ config: poolConfig, masterSecret: newPassword, }); const newManagePool = { id: POOL_ID_MANAGE, config: poolConfig, challenges: newChallenges, }; const primaryTjpAddr = (this.ibGib ? getTjpAddr({ ibGib: this.ibGib }) : undefined) || (this.ibGibAddr ? (() => { const { ib } = getIbAndGib({ ibGibAddr: this.ibGibAddr }); const { tjpGib } = getGibInfo({ ibGibAddr: this.ibGibAddr }); return tjpGib && ib ? `${ib}^${tjpGib}` : this.ibGibAddr; })() : undefined) || getIbGibAddr({ ibGib: this.ibGib! }); const keystoneService = new KeystoneService_V1(); const evolvedParent = await keystoneService.sign({ latestKeystone: this.ibGib!, claim: { target: primaryTjpAddr, verb: KEYSTONE_VERB_MANAGE }, masterSecret: passkeySecret, poolId: signingPoolId, dataToPatch: { challengePools: [...(this.ibGib!.data!.challengePools || []), newManagePool] }, metaspace, space }); // Sync pool addition to server this.showBusy({ isBusy: true, title: 'Syncing to Server...', msg: 'Publishing updated identity to server...', animationEmoji: '⚡' }); const resSync = await getApiBridge().putEvolveKeystone({ domainAddr: primaryTjpAddr, keystoneIbGib: evolvedParent, relatedKeystoneIbGibs: [] }); if (!resSync.success) { throw new Error(`Server sync failed: ${resSync.message}`); } // Save locally await metaspace.put({ ibGibs: [evolvedParent], space }); await metaspace.registerNewIbGib({ ibGib: evolvedParent, space }); // Delegate registration & final sync via DRY helper await this.registerAndSyncSovereignPasswordDelegate({ evolvedParent, newPassword, primaryTjpAddr, metaspace, space }); } catch (error) { console.error(`${lc} Failed to add password to passkey identity: ${extractErrorMsg(error)}`, error); this.setChangePasswordStatus(`Failed to add master password: ${extractErrorMsg(error)}`, "error"); } finally { this.showBusy({ isBusy: false }); } } protected async handleChangePassword(): Promise { const lc = `${this.lc}[${this.handleChangePassword.name}]`; const caps = !!this.ibGib ? getKeystoneCapabilities({ keystone: this.ibGib, aggregatedDetails: this.aggregatedDetails }) : undefined; const hasPassword = !!caps?.hasPassword; if (!hasPassword) { const isCustodialOnly = !!caps ? caps.isCustodial && !caps.isSovereign : false; if (isCustodialOnly) { await this.handleAddMasterPassword(); } else { await this.handleAddPasswordToPasskeyIdentity(); } return; } this.setChangePasswordStatus("Validating entries...", "info"); this.showBusy({ isBusy: true, title: 'Changing Master Password...', msg: 'Validating current password...', animationEmoji: '🔐' }); try { const currentPassword = this.elements!.inputCurrentPasswordEl.value; const newPassword = this.elements!.inputNewPasswordEl.value; const confirmPassword = this.elements!.inputConfirmPasswordEl.value; if (!currentPassword) { this.setChangePasswordStatus("Current password is required.", "error"); return; } if (!newPassword) { this.setChangePasswordStatus("New password is required.", "error"); return; } if (newPassword !== confirmPassword) { this.setChangePasswordStatus("New password and confirmation do not match.", "error"); return; } const keystoneService = new KeystoneService_V1(); // 1. Verify current password against parent manage pool const isMatch = await keystoneService.verifySigningSecret({ keystoneIbGib: this.ibGib!, signingSecret: currentPassword, poolId: 'manage' }); if (!isMatch) { this.setChangePasswordStatus("Incorrect current password.", "error"); return; } this.showBusy({ isBusy: true, title: 'Evolving Master Password...', msg: 'Generating new challenge proofs and rotating local user delegates...', animationEmoji: '⚙️' }); this.setChangePasswordStatus("Evolving primary identity keystone...", "info"); const metaspace = getContextModeService().getPersistentMetaspace(); if (!metaspace) { throw new Error(`(UNEXPECTED) metaspace falsy? (E: 95e3c8374b0f088f339f5c5a8700f826)`); } const space = await metaspace.getLocalUserSpace({ lock: false }); if (!space) { throw new Error(`(UNEXPECTED) couldn't get default local space? (E: 2fc1281c28351fd378a1d7f88eaa0526)`); } // 2. Rotate parent identity and sovereign sync delegates const { newKeystone: evolvedParent, newDelegates: evolvedDelegates } = await keystoneService.changePassword({ keystoneIbGib: this.ibGib!, newMasterSecret: newPassword, signingSecret: currentPassword, signingPoolId: 'manage', metaspace, space }); // 3. Sync evolved parent and all evolved delegate keystones atomically this.showBusy({ isBusy: true, title: 'Syncing to Server...', msg: 'Publishing rotated identity and delegates to server...', animationEmoji: '⚡' }); this.setChangePasswordStatus("Syncing evolved keystones to server...", "info"); const parentTjpAddr = (this.ibGib ? getTjpAddr({ ibGib: this.ibGib }) : undefined) || (this.ibGibAddr ? (() => { const { ib } = getIbAndGib({ ibGibAddr: this.ibGibAddr }); const { tjpGib } = getGibInfo({ ibGibAddr: this.ibGibAddr }); return tjpGib && ib ? `${ib}^${tjpGib}` : this.ibGibAddr; })() : undefined) || getIbGibAddr({ ibGib: this.ibGib! }); const syncRes = await getApiBridge().putEvolveKeystone({ domainAddr: parentTjpAddr, keystoneIbGib: evolvedParent, relatedKeystoneIbGibs: evolvedDelegates, }); if (!syncRes.success) { throw new Error(`Server synchronization rejected: ${syncRes.message}`); } // 4. Update index locally this.setChangePasswordStatus("Updating local indexes...", "info"); const newKeystoneIbGibs = [toDto({ ibGib: evolvedParent })]; if (evolvedDelegates) { evolvedDelegates.forEach(d => { newKeystoneIbGibs.push(toDto({ ibGib: d })); }); } const rel8nInfos = [ { rel8nName: "keystone", ibGibs: newKeystoneIbGibs, } ]; await updateSpecialIndex({ type: "keystones", rel8nInfos, metaspace, space }); // 5. Update device delegate secret in IndexedDB for all local sovereign delegates const newDelegateSecret = await deriveDelegateSecret({ masterSecret: newPassword }); const allDelegates = evolvedParent.data?.delegates || {}; for (const [delegateTjpAddr, dInfo] of Object.entries(allDelegates)) { if (!isCustodialDelegate({ delegateInfo: dInfo })) { const resGet = await metaspace.get({ addrs: [delegateTjpAddr], space }); if (resGet.success && resGet.ibGibs && resGet.ibGibs.length > 0) { await setDeviceDelegateSecret({ delegateTjpAddr, delegateSecret: newDelegateSecret }); } } } // Clear inputs this.elements!.inputCurrentPasswordEl.value = ""; this.elements!.inputNewPasswordEl.value = ""; this.elements!.inputConfirmPasswordEl.value = ""; this.setChangePasswordStatus("Password updated successfully!", "success"); this.ibGibAddr = getIbGibAddr({ ibGib: evolvedParent }); this.staticIbGib = evolvedParent; await this.loadIbGib({ getLatest: true }); } catch (error) { debugger; // error in handleChangePassword console.error(`${lc} Password rotation failed: ${extractErrorMsg(error)}`); this.setChangePasswordStatus(`Update failed: ${extractErrorMsg(error)}`, "error"); } finally { this.showBusy({ isBusy: false }); } } }