import { ComponentMetaspaceScope } from "../../component-types.mjs"; import thisCss from "./add-device.css"; import thisHtml from "./add-device.html"; import commonComponentStylesCss from "../../common-component-styles.css"; import { delay, extractErrorMsg, } 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 { getIbGibAddr } from "@ibgib/ts-gib/dist/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 { KEYSTONE_USERNAME_REGEXP, KEYSTONE_VERB_SYNC, KEYSTONE_VERB_CONNECT } from "@ibgib/core-gib/dist/keystone/keystone-constants.mjs"; import { deriveDelegateSecret, deriveDeviceDelegateSecret, createSyncDelegateKeystone } from "@ibgib/core-gib/dist/keystone/delegate/delegate-helpers.mjs"; import { getTimelinesGroupedByTjp, toDto, getTjpAddr } from "@ibgib/core-gib/dist/common/other/ibgib-helper.mjs"; import { updateSpecialIndex } from "@ibgib/core-gib/dist/timeline/timeline-api.mjs"; import { validateEmail } from "@ibgib/core-gib/dist/keystone/keystone-helpers.mjs"; import { FlatIbGibGraph } from "@ibgib/core-gib/dist/common/other/graph-types.mjs"; import { getApiBridge } from "../../../../api/ibgib-api-bridge.mjs"; import { getContextModeService } from "../../../../context/context-mode-service.mjs"; import { IbGibDynamicComponentMetaBase, IbGibFormInstanceBase, } from "../../ibgib-dynamic-component-bases.mjs"; import { ElementsBase, IbGibDynamicComponentInstance, IbGibDynamicComponentInstanceInitOpts, } from "../../component-types.mjs"; import { getComponentCtorArg } from "../../../../app-bootstrap/init-orchestration.mjs"; import { EVENT_IBGIB_IDENTITY_REQUEST_CHANGE, EVENT_IBGIB_UI_MESSAGE } from "../../../ui-constants.mjs"; import { generateDefaultDeviceTag } from "../../../../util/device-helper.mjs"; import { executeSsoOAuthPopup } from "../../../../identity/sso-popup-helper.mjs"; import { setDeviceDelegateSecret } from "../../../../identity/device-delegate-helper.mjs"; export const ADD_DEVICE_COMPONENT_NAME = 'ibgib-add-device'; /** * Metadata for the Add Device / Pairing component. */ export class AddDeviceComponentMeta extends IbGibDynamicComponentMetaBase { protected lc: string = `[AddDeviceComponentMeta]`; routeRegExp?: RegExp = new RegExp(`^${ADD_DEVICE_COMPONENT_NAME}$`); componentName = ADD_DEVICE_COMPONENT_NAME; constructor() { super(getComponentCtorArg()); if (!customElements.get(this.componentName)) { customElements.define(this.componentName, AddDeviceComponentInstance); } } async createInstance({ path, ibGibAddr }: { path: string; ibGibAddr: IbGibAddr; }): Promise { const component = document.createElement(this.componentName) as AddDeviceComponentInstance; await component.initialize({ ibGibAddr, meta: this, html: thisHtml, css: [commonComponentStylesCss, thisCss], }); return component; } } interface AddDeviceElements extends ElementsBase { containerEl: HTMLDivElement; titleEl: HTMLHeadingElement; subtitleEl: HTMLParagraphElement; surfaceEntryEl: HTMLDivElement; btnGoogleSso: HTMLButtonElement; btnGithubSso: HTMLButtonElement; inputIdentifier: HTMLInputElement; btnContinueEmail: HTMLButtonElement; sectionCodeVerification: HTMLDivElement; inputCode: HTMLInputElement; btnVerifyCode: HTMLButtonElement; sectionSovereignPassword: HTMLDivElement; inputPassword: HTMLInputElement; btnAuthorizePassword: HTMLButtonElement; inputDeviceTag: HTMLInputElement; checkboxMakeActive: HTMLInputElement; statusArea: HTMLDivElement; statusText: HTMLParagraphElement; resultAreaEl: HTMLDivElement; delegateAddrEl: HTMLElement; } /** * Concrete implementation of the Add Device / Pairing component. */ export class AddDeviceComponentInstance extends IbGibFormInstanceBase implements IbGibDynamicComponentInstance { /** Identity is device-scoped and mode-agnostic: always use the persistent metaspace. */ protected override metaspaceScope: ComponentMetaspaceScope = ComponentMetaspaceScope.persistent; protected lc: string = `[AddDeviceComponentInstance]`; // Staged state across progressive surfaces private _stagedEmail?: string; private _stagedPrimaryTjpAddr?: string; private _stagedPrimaryKeystone?: KeystoneIbGib_V1; private _stagedPrimaryGraph?: FlatIbGibGraph; private _stagedSiteUsername?: string; constructor() { super(); } override async initialize(opts: IbGibDynamicComponentInstanceInitOpts): Promise { await super.initialize(opts); } override async created(): Promise { const shadow = this.shadowRoot!; const elements: AddDeviceElements = {} as AddDeviceElements; elements.containerEl = shadow.getElementById('container') as HTMLDivElement; elements.titleEl = shadow.getElementById('title') as HTMLHeadingElement; elements.subtitleEl = shadow.getElementById('subtitle') as HTMLParagraphElement; elements.surfaceEntryEl = shadow.getElementById('surface-entry') as HTMLDivElement; elements.btnGoogleSso = shadow.getElementById('btn-google-sso') as HTMLButtonElement; elements.btnGithubSso = shadow.getElementById('btn-github-sso') as HTMLButtonElement; elements.inputIdentifier = shadow.getElementById('input-identifier') as HTMLInputElement; elements.btnContinueEmail = shadow.getElementById('btn-continue-email') as HTMLButtonElement; elements.sectionCodeVerification = shadow.getElementById('section-code-verification') as HTMLDivElement; elements.inputCode = shadow.getElementById('input-code') as HTMLInputElement; elements.btnVerifyCode = shadow.getElementById('btn-verify-code') as HTMLButtonElement; elements.sectionSovereignPassword = shadow.getElementById('section-sovereign-password') as HTMLDivElement; elements.inputPassword = shadow.getElementById('input-password') as HTMLInputElement; elements.btnAuthorizePassword = shadow.getElementById('btn-authorize-password') as HTMLButtonElement; elements.inputDeviceTag = shadow.getElementById('input-device-tag') as HTMLInputElement; elements.checkboxMakeActive = shadow.getElementById('checkbox-make-active') as HTMLInputElement; elements.statusArea = shadow.getElementById('status-area') as HTMLDivElement; elements.statusText = shadow.getElementById('status-text') as HTMLParagraphElement; elements.resultAreaEl = shadow.getElementById('result-area') as HTMLDivElement; elements.delegateAddrEl = shadow.getElementById('delegate-addr') as HTMLElement; elements.contentEl = elements.containerEl; this.elements = elements; // Auto-generate clean, friendly default device tag if (elements.inputDeviceTag && !elements.inputDeviceTag.value) { elements.inputDeviceTag.value = generateDefaultDeviceTag(); } this.initHandlers(); await this.renderUI(); } override async disconnected(): Promise { // cleanup if needed } private initHandlers() { const elements = this.elements!; elements.btnGoogleSso?.addEventListener('click', () => this.handleSsoClick('google')); elements.btnGithubSso?.addEventListener('click', () => this.handleSsoClick('github')); elements.btnContinueEmail?.addEventListener('click', () => this.handleContinueEmail()); elements.btnVerifyCode?.addEventListener('click', () => this.handleVerifyCode()); elements.btnAuthorizePassword?.addEventListener('click', () => this.handleAuthorizePassword()); this.shadowRoot!.getElementById('btn-switch-create-account')?.addEventListener('click', (e) => { e.preventDefault(); window.dispatchEvent(new CustomEvent(EVENT_IBGIB_UI_MESSAGE, { detail: { action: 'show-keystone-creator' }, bubbles: true, composed: true })); }); elements.inputIdentifier?.addEventListener('input', () => { elements.inputIdentifier.classList.remove('invalid'); }); elements.inputCode?.addEventListener('input', () => { elements.inputCode.classList.remove('invalid'); }); elements.inputPassword?.addEventListener('input', () => { elements.inputPassword.classList.remove('invalid'); }); elements.inputDeviceTag?.addEventListener('input', () => { elements.inputDeviceTag.classList.remove('invalid'); }); } private getDeviceTag(): string { const elements = this.elements!; const raw = elements.inputDeviceTag?.value?.trim(); if (raw && KEYSTONE_USERNAME_REGEXP.test(raw)) { return raw; } const generated = generateDefaultDeviceTag(); if (elements.inputDeviceTag) { elements.inputDeviceTag.value = generated; } return generated; } // ========================================================================= // Flow 1: OAuth2 SSO Sign In (Google / GitHub) // ========================================================================= protected async handleSsoClick(providerId: 'google' | 'github') { const lc = `${this.lc}[${this.handleSsoClick.name}]`; const providerName = providerId === 'google' ? 'Google' : 'GitHub'; this.setStatus(`Opening ${providerName} sign-in window...`, 'info'); this.showBusy({ isBusy: true, title: 'Sign In via SSO', msg: `Opening ${providerName} authorization window...\nPlease complete sign-in in the popup window.`, animationEmoji: '🌐' }); try { const resPopup = await executeSsoOAuthPopup({ providerId }); if (!resPopup.success || !resPopup.code) { this.setStatus(resPopup.message || `${providerName} sign-in cancelled.`, 'error'); return; } this.setStatus(`Validating ${providerName} authorization...`, 'info'); this.showBusy({ isBusy: true, title: 'Exchanging SSO Code...', msg: `Validating ${providerName} authorization with server...`, animationEmoji: '🔐' }); const apiBridge = getApiBridge(); const resLogin = await apiBridge.postSsoLogin({ code: resPopup.code, providerId, redirectUri: resPopup.redirectUri! }); if (!resLogin.success || !resLogin.primaryTjpAddr || !resLogin.custodianReqs) { this.setStatus(resLogin.message || `${providerName} login failed.`, 'error'); return; } const { primaryTjpAddr, custodianReqs, primaryKeystone } = resLogin; const email = resLogin.userInfo?.email || ''; const username = email ? email.split('@')[0] : 'user'; await this.completeCustodialDelegateOnboarding({ primaryTjpAddr, custodianReqs, fallbackPrimaryKeystone: primaryKeystone, fallbackPrimaryGraph: resLogin.primaryGraph, email, username, }); } catch (error) { const emsg = extractErrorMsg(error); console.error(`${lc} SSO login error: ${emsg}`, error); this.setStatus(`Error: ${emsg}`, 'error'); } finally { this.showBusy({ isBusy: false }); } } // ========================================================================= // Flow 2: Email Sign In (2FA OTP Gate) // ========================================================================= protected async handleContinueEmail() { const lc = `${this.lc}[${this.handleContinueEmail.name}]`; const elements = this.elements!; const identifier = elements.inputIdentifier?.value?.trim(); if (!identifier) { elements.inputIdentifier.classList.add('invalid'); this.setStatus('Please enter your email or username.', 'error'); return; } const isEmail = identifier.includes('@'); if (isEmail && !validateEmail(identifier)) { elements.inputIdentifier.classList.add('invalid'); this.setStatus('Please enter a valid email address.', 'error'); return; } else if (!isEmail && (!KEYSTONE_USERNAME_REGEXP.test(identifier) || /\s/.test(identifier))) { elements.inputIdentifier.classList.add('invalid'); this.setStatus('Please enter a valid username without spaces.', 'error'); return; } elements.btnContinueEmail.disabled = true; elements.btnContinueEmail.textContent = 'Checking...'; this.setStatus('Checking identity on server...', 'info'); this.showBusy({ isBusy: true, title: 'Locating Identity...', msg: 'Locating account on server...', animationEmoji: '🔍' }); try { // 1. Look up primary identity keystone for this identifier (email or siteUsername) const resLookup = await getApiBridge().lookupPrimaryKeystone({ identifier }); if (!resLookup.success || !resLookup.tjpAddr) { this.setStatus(`No account found for '${identifier}'. Please check the username or email, or click "Create Account" below.`, 'error'); return; } this._stagedPrimaryTjpAddr = resLookup.tjpAddr; this._stagedPrimaryKeystone = resLookup.primaryKeystoneIbGib as KeystoneIbGib_V1 | undefined; this._stagedSiteUsername = resLookup.siteUsername; const primaryKeystone = resLookup.primaryKeystoneIbGib as KeystoneIbGib_V1 | undefined; const emails = primaryKeystone?.data?.emails; const resolvedEmail = (emails && emails.length > 0) ? emails[0] : (isEmail ? identifier : undefined); this._stagedEmail = resolvedEmail; // If account has an email address registered, dispatch 6-digit verification code if (resolvedEmail) { this.setStatus('Dispatching 6-digit verification code...', 'info'); this.showBusy({ isBusy: true, title: 'Sending Code...', msg: `Dispatching 6-digit verification code to ${resolvedEmail}...`, animationEmoji: '✉️' }); const resCode = await getApiBridge().postVerificationCode({ email: resolvedEmail, primaryTjpAddr: this._stagedPrimaryTjpAddr }); if (!resCode.success) { this.setStatus(resCode.message || 'Failed to dispatch verification code.', 'error'); return; } // Transition UI to 6-digit PIN input prompt elements.sectionCodeVerification.classList.remove('hidden'); elements.sectionCodeVerification.scrollIntoView({ behavior: 'smooth' }); this.setStatus(`Verification code sent to ${resolvedEmail}! Enter the 6-digit code above to continue.`, 'info'); elements.inputCode?.focus(); } else { // Sovereign password-based account with no email registered: // Fetch cryptographic graph directly and proceed directly to master password entry! this.setStatus('Retrieving identity graph from server...', 'info'); this.showBusy({ isBusy: true, title: 'Loading Identity Graph...', msg: 'Fetching cryptographic identity timeline from server...', animationEmoji: '📥' }); const resGraph = await getApiBridge().getKeystoneGraph(this._stagedPrimaryTjpAddr); if (!resGraph.success || !resGraph.graph) { this.setStatus('Failed to retrieve identity graph from server.', 'error'); return; } this._stagedPrimaryGraph = resGraph.graph; const metaspace = getContextModeService().getPersistentMetaspace(); const space = await metaspace.getLocalUserSpace({}); if (space) { const graphIbGibs = Object.values(resGraph.graph); 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 }); } } const domainTimeline = timelinesMap[this._stagedPrimaryTjpAddr] || timelinesMap[Object.keys(timelinesMap)[0]]; if (domainTimeline && domainTimeline.length > 0) { this._stagedPrimaryKeystone = domainTimeline[domainTimeline.length - 1] as KeystoneIbGib_V1; } } // Reveal Sovereign Master Password prompt directly elements.sectionSovereignPassword.classList.remove('hidden'); elements.sectionSovereignPassword.scrollIntoView({ behavior: 'smooth' }); this.setStatus('Identity found! Please enter your master password to authorize this device.', 'info'); elements.inputPassword?.focus(); } } catch (error) { const emsg = extractErrorMsg(error); console.error(`${lc} ${emsg}`, error); this.setStatus(`Error: ${emsg}`, 'error'); } finally { elements.btnContinueEmail.disabled = false; elements.btnContinueEmail.textContent = 'Continue with Email or Username'; this.showBusy({ isBusy: false }); } } protected async handleVerifyCode() { const lc = `${this.lc}[${this.handleVerifyCode.name}]`; const elements = this.elements!; const code = elements.inputCode?.value?.trim(); const email = this._stagedEmail; const primaryTjpAddr = this._stagedPrimaryTjpAddr; if (!email || !primaryTjpAddr) { this.setStatus('Please enter your email and request a code first.', 'error'); return; } if (!code || code.length !== 6) { elements.inputCode.classList.add('invalid'); this.setStatus('Please enter a 6-digit verification code.', 'error'); return; } elements.btnVerifyCode.disabled = true; elements.btnVerifyCode.textContent = 'Verifying...'; this.setStatus('Verifying 6-digit code...', 'info'); this.showBusy({ isBusy: true, title: 'Verifying Code...', msg: 'Validating verification code with server...', animationEmoji: '🔐' }); try { // First, attempt Custodian Genesis / Sign-In resolution const resGenesis = await getApiBridge().postCustodianGenesis({ email, code }); if (resGenesis.success && resGenesis.primaryTjpAddr && resGenesis.custodianReqs) { // ------------------------------------------------------------- // Path A: Custodial Account (100% Passwordless) // ------------------------------------------------------------- const { primaryTjpAddr, custodianReqs, primaryKeystone, primaryGraph } = resGenesis; const username = email ? email.split('@')[0] : 'user'; await this.completeCustodialDelegateOnboarding({ primaryTjpAddr, custodianReqs, fallbackPrimaryKeystone: primaryKeystone, fallbackPrimaryGraph: primaryGraph, email, username, }); return; } // ----------------------------------------------------------------- // Path B: Sovereign Account (Fetch Verified Graph, Reveal Password Prompt) // ----------------------------------------------------------------- const resVerify = await getApiBridge().postVerifyCode({ email, primaryTjpAddr, code }); if (!resVerify.success) { this.setStatus(resVerify.message || 'Verification failed. Please check the code.', 'error'); return; } this.setStatus('Retrieving identity graph from server...', 'info'); this.showBusy({ isBusy: true, title: 'Loading Identity Graph...', msg: 'Fetching cryptographic identity timeline from server...', animationEmoji: '📥' }); const resGraph = await getApiBridge().getKeystoneGraph(primaryTjpAddr); if (!resGraph.success || !resGraph.graph) { this.setStatus('Failed to retrieve identity graph from server.', 'error'); return; } this._stagedPrimaryGraph = resGraph.graph; const metaspace = getContextModeService().getPersistentMetaspace(); const space = await metaspace.getLocalUserSpace({}); if (space) { const graphIbGibs = Object.values(resGraph.graph); 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 }); } } const domainTimeline = timelinesMap[primaryTjpAddr] || timelinesMap[Object.keys(timelinesMap)[0]]; if (domainTimeline && domainTimeline.length > 0) { this._stagedPrimaryKeystone = domainTimeline[domainTimeline.length - 1] as KeystoneIbGib_V1; } } // Hide OTP section, reveal Sovereign Master Password prompt elements.sectionCodeVerification.classList.add('hidden'); elements.sectionSovereignPassword.classList.remove('hidden'); elements.sectionSovereignPassword.scrollIntoView({ behavior: 'smooth' }); this.setStatus('2FA verified! Please enter your master password to authorize this device.', 'info'); elements.inputPassword?.focus(); } catch (error) { const emsg = extractErrorMsg(error); console.error(`${lc} ${emsg}`, error); this.setStatus(`Error: ${emsg}`, 'error'); } finally { elements.btnVerifyCode.disabled = false; elements.btnVerifyCode.textContent = 'Verify Code'; this.showBusy({ isBusy: false }); } } // ========================================================================= // DRY Custodial Delegation Helper (Shared by SSO & Email OTP) // ========================================================================= private async completeCustodialDelegateOnboarding({ primaryTjpAddr, custodianReqs, fallbackPrimaryKeystone, fallbackPrimaryGraph, email, username, }: { primaryTjpAddr: string; custodianReqs: any; fallbackPrimaryKeystone?: any; fallbackPrimaryGraph?: FlatIbGibGraph; email: string; username?: string; }): Promise { const lc = `${this.lc}[${this.completeCustodialDelegateOnboarding.name}]`; const deviceTag = this.getDeviceTag(); this.setStatus('Deriving local device delegate keys...', 'info'); this.showBusy({ isBusy: true, title: 'Creating Sync Delegate...', msg: `Generating cryptographic sync delegate keys for ${deviceTag}...`, animationEmoji: '🔑' }); const metaspace = getContextModeService().getPersistentMetaspace(); const space = await metaspace.getLocalUserSpace({ lock: false }); if (!space) { this.setStatus('Could not access local user space.', 'error'); return; } const delegateSecret = await deriveDeviceDelegateSecret({ primaryTjpAddr, deviceTag }); const userAlias = username || (email ? email.split('@')[0] : 'user'); const custodianSyncProfileName = process.env.KEYSTONE_PROFILE_CUSTODIAN_SYNC; if (!custodianSyncProfileName || !custodianSyncProfileName.trim()) { throw new Error('KEYSTONE_PROFILE_CUSTODIAN_SYNC environment variable is missing. Expected valid profile name like "sync-profile.dev" or "sync-profile.prod". (E: genuuid)'); } const delegateKeystone = await createSyncDelegateKeystone({ profileName: custodianSyncProfileName.trim() as any, username: userAlias, deviceTag, primaryTjpAddr, delegateSecret, custodianReqs, additionalFrameDetails: { client: 'space-gib-web' }, metaspace, space, }); const delegateTjpAddr = getTjpAddr({ ibGib: delegateKeystone }) || getIbGibAddr({ ibGib: delegateKeystone }); await setDeviceDelegateSecret({ delegateTjpAddr, delegateSecret }); this.setStatus('Registering device delegate with custodian server...', 'info'); this.showBusy({ isBusy: true, title: 'Registering Delegate...', msg: 'Registering local delegate with custodian server...', animationEmoji: '🔄' }); const resRegister = await getApiBridge().postCustodianRegisterDelegate({ delegateKeystone }); if (!resRegister.success) { this.setStatus(resRegister.message || 'Custodian delegate registration failed.', 'error'); return; } // Finalize onboarding into local space & special index await this.finalizeDeviceOnboarding({ primaryKeystone: resRegister.evolvedPrimaryKeystone || fallbackPrimaryKeystone, primaryGraph: resRegister.primaryGraph || fallbackPrimaryGraph, delegateKeystone, primaryTjpAddr, delegateSecret, makeActive: this.elements?.checkboxMakeActive?.checked ?? true }); } // ========================================================================= // Flow 3: Sovereign Master Password Unlock & Local Evolution // ========================================================================= protected async handleAuthorizePassword() { const lc = `${this.lc}[${this.handleAuthorizePassword.name}]`; const elements = this.elements!; const masterSecret = elements.inputPassword?.value?.trim(); const primaryKeystone = this._stagedPrimaryKeystone; const primaryTjpAddr = this._stagedPrimaryTjpAddr; if (!masterSecret) { elements.inputPassword.classList.add('invalid'); this.setStatus('Please enter your master password.', 'error'); return; } if (!primaryKeystone || !primaryTjpAddr) { this.setStatus('Missing staged primary identity context. Please restart sign-in.', 'error'); return; } elements.btnAuthorizePassword.disabled = true; elements.btnAuthorizePassword.textContent = 'Authorizing...'; this.setStatus('Verifying master password locally...', 'info'); this.showBusy({ isBusy: true, title: 'Verifying Password...', msg: 'Verifying cryptographic password locally in browser...', animationEmoji: '🔐' }); try { const keystoneService = new KeystoneService_V1(); const isValid = await keystoneService.verifySigningSecret({ keystoneIbGib: primaryKeystone, signingSecret: masterSecret, poolId: 'manage' }); if (!isValid) { elements.inputPassword.classList.add('invalid'); this.setStatus('Incorrect master password. Please try again.', 'error'); return; } const deviceTag = this.getDeviceTag(); const primaryUsername = primaryKeystone.data?.frameDetails?.username || 'user'; this.setStatus(`Generating local sync delegate for ${deviceTag}...`, 'info'); this.showBusy({ isBusy: true, title: 'Generating Keys...', msg: `Generating cryptographic delegate keys for ${deviceTag}...`, animationEmoji: '🔑' }); const metaspace = getContextModeService().getPersistentMetaspace(); const space = await metaspace.getLocalUserSpace({}); if (!space) { throw new Error('No default space found in metaspace.'); } const delegateSecret = await deriveDelegateSecret({ masterSecret }); const sovereignSyncProfileName = process.env.KEYSTONE_PROFILE_SOVEREIGN_SYNC; if (!sovereignSyncProfileName || !sovereignSyncProfileName.trim()) { throw new Error('KEYSTONE_PROFILE_SOVEREIGN_SYNC environment variable is missing. Expected valid profile name like "sync-profile.dev" or "sync-profile.prod". (E: 0cd848b379d89ecd030192c1f9201926)'); } const delegateKeystone = await createSyncDelegateKeystone({ profileName: sovereignSyncProfileName.trim() as any, username: primaryUsername, deviceTag, primaryTjpAddr, delegateSecret, additionalFrameDetails: { client: 'space-gib-web' }, metaspace, space, }); const delegateTjpAddr = getTjpAddr({ ibGib: delegateKeystone }) || getIbGibAddr({ ibGib: delegateKeystone }); await setDeviceDelegateSecret({ delegateTjpAddr, delegateSecret }); this.setStatus('Evolving primary identity to register delegate...', 'info'); this.showBusy({ isBusy: true, title: 'Registering Device...', msg: 'Evolving primary identity to register new device delegate...', animationEmoji: '⚙️' }); const evolvedParentKeystone = await keystoneService.registerDelegate({ parentKeystone: primaryKeystone, delegateKeystone, masterSecret, signingPoolId: 'manage', allowedVerbs: [KEYSTONE_VERB_SYNC, KEYSTONE_VERB_CONNECT], metaspace, space, deviceName: deviceTag }); this.setStatus('Publishing evolved identity to server...', 'info'); this.showBusy({ isBusy: true, title: 'Syncing to Server...', msg: 'Publishing evolved identity and delegate to server...', animationEmoji: '⚡' }); const resEvolve = await getApiBridge().putEvolveKeystone({ domainAddr: primaryTjpAddr, keystoneIbGib: evolvedParentKeystone, relatedKeystoneIbGibs: [delegateKeystone] }); if (!resEvolve.success) { console.warn(`${lc} Warning syncing delegate evolution to server: ${resEvolve.message}`); } await this.finalizeDeviceOnboarding({ primaryKeystone: evolvedParentKeystone, primaryGraph: this._stagedPrimaryGraph, delegateKeystone, primaryTjpAddr, masterSecret, delegateSecret, makeActive: elements.checkboxMakeActive?.checked ?? true }); } catch (error) { const emsg = extractErrorMsg(error); console.error(`${lc} ${emsg}`, error); this.setStatus(`Error: ${emsg}`, 'error'); } finally { elements.btnAuthorizePassword.disabled = false; elements.btnAuthorizePassword.textContent = 'Authorize & Pair Device'; this.showBusy({ isBusy: false }); } } // ========================================================================= // Finalize Device Onboarding & Navigation // ========================================================================= private async finalizeDeviceOnboarding({ primaryKeystone, primaryGraph, delegateKeystone, primaryTjpAddr, masterSecret, delegateSecret, makeActive = true, }: { primaryKeystone?: any; primaryGraph?: FlatIbGibGraph; delegateKeystone: any; primaryTjpAddr: string; masterSecret?: string; delegateSecret?: string; makeActive?: boolean; }): Promise { const lc = `${this.lc}[${this.finalizeDeviceOnboarding.name}]`; const humanEyeDelay = 500; const metaspace = getContextModeService().getPersistentMetaspace(); if (!metaspace) { throw new Error('Metaspace not found'); } const space = await metaspace.getLocalUserSpace({}); if (!space) { throw new Error('Local user space not found'); } // 1. Put primary frames and delegate into local space if (primaryGraph) { const graphIbGibs = Object.values(primaryGraph) as IbGib_V1[]; 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 if (primaryKeystone) { await metaspace.put({ ibGibs: [primaryKeystone], space }); await metaspace.registerNewIbGib({ ibGib: primaryKeystone, space }); } await metaspace.put({ ibGibs: [delegateKeystone], space }); await metaspace.registerNewIbGib({ ibGib: delegateKeystone, space }); // 2. Register in keystones special index const ibGibsToRegister: IbGib_V1[] = [toDto({ ibGib: delegateKeystone })!]; if (primaryKeystone) { ibGibsToRegister.unshift(toDto({ ibGib: primaryKeystone })!); } await updateSpecialIndex({ type: 'keystones', rel8nInfos: [{ rel8nName: 'keystone', ibGibs: ibGibsToRegister }], metaspace, space }); // 3. Make active if checked if (makeActive) { window.dispatchEvent(new CustomEvent(EVENT_IBGIB_IDENTITY_REQUEST_CHANGE, { detail: { activeIdentityAddr: primaryTjpAddr, siteUsername: this._stagedSiteUsername }, bubbles: true, composed: true })); } // 4. Update UI results const delegateAddr = getIbGibAddr({ ibGib: delegateKeystone }); if (this.elements?.delegateAddrEl) { this.elements.delegateAddrEl.textContent = delegateAddr; } if (this.elements?.resultAreaEl) { this.elements.resultAreaEl.classList.remove('collapsed'); } const successMsg = 'Device paired and identity activated successfully!'; this.setStatus(successMsg, 'success'); this.showBusy({ isBusy: true, title: 'Success!', msg: successMsg, animationEmoji: '✅' }); await delay(humanEyeDelay); // 4.5 Sanitize password input fields from DOM if (this.elements?.inputPassword) { this.elements.inputPassword.value = ''; } // 5. Dispatch UI navigation message to finalize pairing window.dispatchEvent(new CustomEvent(EVENT_IBGIB_UI_MESSAGE, { detail: { action: 'identity-onboarded', addr: primaryTjpAddr, siteUsername: this._stagedSiteUsername }, bubbles: true, composed: true })); this.showBusy({ isBusy: false }); } protected override async renderUI(): Promise { // No complex rendering needed } override async handleContextUpdated(): Promise { await this.renderUI(); } }