import { ComponentMetaspaceScope } from "../../component-types.mjs"; import thisCss from "./keystone-creator.css"; import thisHtml from "./keystone-creator.html"; import commonComponentStylesCss from "../../common-component-styles.css"; import { delay, extractErrorMsg, getSaferSubstring } 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 { KeystoneProfileBuilder } from "@ibgib/core-gib/dist/keystone/policy/keystone-profile-builder.mjs"; import { updateSpecialIndex, } from "@ibgib/core-gib/dist/timeline/timeline-api.mjs"; import { toDto, getTjpAddr, getTimelinesGroupedByTjp } from "@ibgib/core-gib/dist/common/other/ibgib-helper.mjs"; import { FlatIbGibGraph } from "@ibgib/core-gib/dist/common/other/graph-types.mjs"; import { validateEmail } from "@ibgib/core-gib/dist/keystone/keystone-helpers.mjs"; import { KEYSTONE_USERNAME_REGEXP, KEYSTONE_DESCRIPTION_REGEXP, KEYSTONE_VERB_SYNC, KEYSTONE_VERB_CONNECT } from "@ibgib/core-gib/dist/keystone/keystone-constants.mjs"; import { KeystonePasskeyEntry } from "@ibgib/core-gib/dist/keystone/keystone-types.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 { getApiBridge } from "../../../../api/ibgib-api-bridge.mjs"; import { createSovereignDeviceSyncDelegate, createOAuth2DeviceSyncDelegate, createEmailDeviceSyncDelegate } from "../../../../identity/device-delegate-helper.mjs"; import { executeSsoOAuthPopup } from "../../../../identity/sso-popup-helper.mjs"; import { generateDefaultDeviceTag } from "../../../../util/device-helper.mjs"; import { registerPasskeyCredential, evaluatePasskeyPrf, createPasskeyDeviceSyncDelegate, bufferToHex } from "../../../../identity/passkey-helper.mjs"; import { EVENT_IBGIB_IDENTITY_REQUEST_CHANGE, EVENT_IBGIB_UI_MESSAGE } from "../../../ui-constants.mjs"; import { alertUser, promptForText, shadowRoot_getElementById } from "../../../../helpers.web.mjs"; export const KEYSTONE_CREATOR_COMPONENT_NAME = 'ibgib-keystone-creator'; /** * Metadata for the Keystone Creator component. */ export class KeystoneCreatorComponentMeta extends IbGibDynamicComponentMetaBase { protected lc: string = `[KeystoneCreatorComponentMeta]`; routeRegExp?: RegExp = new RegExp(`^${KEYSTONE_CREATOR_COMPONENT_NAME}$`); componentName = KEYSTONE_CREATOR_COMPONENT_NAME; constructor() { super(getComponentCtorArg()); if (!customElements.get(this.componentName)) { customElements.define(this.componentName, KeystoneCreatorComponentInstance); } } async createInstance({ path, ibGibAddr }: { path: string; ibGibAddr: IbGibAddr; }): Promise { const lc = `${this.lc}[${this.createInstance.name}]`; const component = document.createElement(this.componentName) as KeystoneCreatorComponentInstance; await component.initialize({ ibGibAddr, meta: this, html: thisHtml, css: [commonComponentStylesCss, thisCss], }); return component; } } /** * References to DOM elements within the component's shadow root. */ interface KeystoneCreatorElements extends ElementsBase { containerEl: HTMLDivElement; titleEl: HTMLHeadingElement; btnGenerateEl: HTMLButtonElement; resultAreaEl: HTMLDivElement; keystoneAddrEl: HTMLElement; // Shared Identity Profile inputUsername: HTMLInputElement; inputSiteUsername: HTMLInputElement; badgeSiteUsername: HTMLSpanElement; msgSiteUsername: HTMLParagraphElement; groupExceptionKey: HTMLDivElement; inputExceptionKey: HTMLInputElement; linkRequestException: HTMLAnchorElement; groupDescription: HTMLDivElement; inputDescription: HTMLInputElement; groupEmail: HTMLDivElement; inputEmail: HTMLInputElement; // Password-based specific inputSecret: HTMLInputElement; checkboxMakeActive: HTMLInputElement; // Quick specific btnGoogleSso: HTMLButtonElement; btnGithubSso: HTMLButtonElement; inputQuickEmail: HTMLInputElement; btnQuickSendCode: HTMLButtonElement; secQuickOtp: HTMLDivElement; inputQuickOtp: HTMLInputElement; btnQuickVerifyCode: HTMLButtonElement; btnQuickResendCode: HTMLButtonElement; // Passkey specific inputPasskeyName: HTMLInputElement; selectPasskeyAttachment: HTMLSelectElement; checkboxPasskeyMakeActive: HTMLInputElement; btnGeneratePasskey: HTMLButtonElement; // Status & Exception Request Modal statusArea: HTMLDivElement; statusText: HTMLParagraphElement; modalExceptionRequest: HTMLDivElement; btnCloseModalException: HTMLButtonElement; btnCancelExceptionRequest: HTMLButtonElement; formExceptionRequest: HTMLFormElement; inputReqSiteUsername: HTMLInputElement; inputReqApplicantName: HTMLInputElement; inputReqEmail: HTMLInputElement; inputReqProjectTitle: HTMLInputElement; inputReqReason: HTMLTextAreaElement; inputReqProfileUrl: HTMLInputElement; statusExceptionRequest: HTMLDivElement; btnSubmitExceptionRequest: HTMLButtonElement; } /** * Concrete implementation of the Keystone Creator component. */ export class KeystoneCreatorComponentInstance 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 = `[KeystoneCreatorComponentInstance]`; private hasExistingIdentities = false; private resendCooldownInterval: any = null; private userEditedSiteUsername = false; private siteUsernameDebounceTimer: any = null; private email: string = ''; constructor() { super(); } override async initialize(opts: IbGibDynamicComponentInstanceInitOpts): Promise { await super.initialize(opts); } override async disconnected(): Promise { if (this.resendCooldownInterval) { clearInterval(this.resendCooldownInterval); this.resendCooldownInterval = null; } if (this.siteUsernameDebounceTimer) { clearTimeout(this.siteUsernameDebounceTimer); this.siteUsernameDebounceTimer = null; } } private startResendCooldownTimer({ btnId, durationSecs = 30 }: { btnId: string; durationSecs?: number }): void { const btn = this.shadowRoot?.getElementById(btnId) as HTMLButtonElement | null; 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); } override async created(): Promise { this.elements = {} as KeystoneCreatorElements; this.elements.containerEl = this.shadowRoot!.getElementById('container') as HTMLDivElement; this.elements.titleEl = this.shadowRoot!.getElementById('title') as HTMLHeadingElement; this.elements.btnGenerateEl = this.shadowRoot!.getElementById('btn-generate') as HTMLButtonElement; this.elements.resultAreaEl = this.shadowRoot!.getElementById('result-area') as HTMLDivElement; this.elements.keystoneAddrEl = this.shadowRoot!.getElementById('keystone-addr') as HTMLElement; // Shared Identity Profile Elements this.elements.inputUsername = this.shadowRoot!.getElementById('input-username') as HTMLInputElement; this.elements.inputSiteUsername = this.shadowRoot!.getElementById('input-site-username') as HTMLInputElement; this.elements.badgeSiteUsername = this.shadowRoot!.getElementById('badge-site-username') as HTMLSpanElement; this.elements.msgSiteUsername = this.shadowRoot!.getElementById('msg-site-username') as HTMLParagraphElement; this.elements.groupExceptionKey = this.shadowRoot!.getElementById('group-exception-key') as HTMLDivElement; this.elements.inputExceptionKey = this.shadowRoot!.getElementById('input-exception-key') as HTMLInputElement; this.elements.linkRequestException = this.shadowRoot!.getElementById('link-request-exception') as HTMLAnchorElement; this.elements.groupDescription = this.shadowRoot!.getElementById('group-description') as HTMLDivElement; this.elements.inputDescription = this.shadowRoot!.getElementById('input-description') as HTMLInputElement; this.elements.groupEmail = this.shadowRoot!.getElementById('group-email') as HTMLDivElement; this.elements.inputEmail = this.shadowRoot!.getElementById('input-email') as HTMLInputElement; // Password Mode Specific this.elements.inputSecret = this.shadowRoot!.getElementById('input-secret') as HTMLInputElement; this.elements.checkboxMakeActive = this.shadowRoot!.getElementById('checkbox-make-active') as HTMLInputElement; // Quick Mode Specific this.elements.btnGoogleSso = this.shadowRoot!.getElementById('btn-google-sso') as HTMLButtonElement; this.elements.btnGithubSso = this.shadowRoot!.getElementById('btn-github-sso') as HTMLButtonElement; this.elements.inputQuickEmail = this.shadowRoot!.getElementById('input-quick-email') as HTMLInputElement; this.elements.btnQuickSendCode = this.shadowRoot!.getElementById('btn-quick-send-code') as HTMLButtonElement; this.elements.secQuickOtp = this.shadowRoot!.getElementById('section-quick-otp') as HTMLDivElement; this.elements.inputQuickOtp = this.shadowRoot!.getElementById('input-quick-otp') as HTMLInputElement; this.elements.btnQuickVerifyCode = this.shadowRoot!.getElementById('btn-quick-verify-code') as HTMLButtonElement; this.elements.btnQuickResendCode = this.shadowRoot!.getElementById('btn-quick-resend-code') as HTMLButtonElement; // Passkey Mode Specific this.elements.inputPasskeyName = this.shadowRoot!.getElementById('input-passkey-name') as HTMLInputElement; this.elements.selectPasskeyAttachment = this.shadowRoot!.getElementById('select-passkey-attachment') as HTMLSelectElement; this.elements.checkboxPasskeyMakeActive = this.shadowRoot!.getElementById('checkbox-passkey-make-active') as HTMLInputElement; this.elements.btnGeneratePasskey = this.shadowRoot!.getElementById('btn-generate-passkey') as HTMLButtonElement; // Status Area & Exception Request Modal this.elements.statusArea = this.shadowRoot!.getElementById('status-area') as HTMLDivElement; this.elements.statusText = this.shadowRoot!.getElementById('status-text') as HTMLParagraphElement; this.elements.modalExceptionRequest = this.shadowRoot!.getElementById('modal-exception-request') as HTMLDivElement; this.elements.btnCloseModalException = this.shadowRoot!.getElementById('btn-close-modal-exception') as HTMLButtonElement; this.elements.btnCancelExceptionRequest = this.shadowRoot!.getElementById('btn-cancel-exception-request') as HTMLButtonElement; this.elements.formExceptionRequest = this.shadowRoot!.getElementById('form-exception-request') as HTMLFormElement; this.elements.inputReqSiteUsername = this.shadowRoot!.getElementById('input-req-site-username') as HTMLInputElement; this.elements.inputReqApplicantName = this.shadowRoot!.getElementById('input-req-applicant-name') as HTMLInputElement; this.elements.inputReqEmail = this.shadowRoot!.getElementById('input-req-email') as HTMLInputElement; this.elements.inputReqProjectTitle = this.shadowRoot!.getElementById('input-req-project-title') as HTMLInputElement; this.elements.inputReqReason = this.shadowRoot!.getElementById('input-req-reason') as HTMLTextAreaElement; this.elements.inputReqProfileUrl = this.shadowRoot!.getElementById('input-req-profile-url') as HTMLInputElement; this.elements.statusExceptionRequest = this.shadowRoot!.getElementById('status-exception-request') as HTMLDivElement; this.elements.btnSubmitExceptionRequest = this.shadowRoot!.getElementById('btn-submit-exception-request') as HTMLButtonElement; // ElementBase requires contentEl this.elements.contentEl = this.elements.containerEl; this.initHandlers(); if (this.ibGibAddr && !this.ibGib) { await this.loadIbGib(); } await this.checkExistingIdentities(); await this.renderUI(); } private async checkExistingIdentities() { const lc = `${this.lc}[checkExistingIdentities]`; try { const metaspace = getContextModeService().getPersistentMetaspace(); const space = await metaspace.getLocalUserSpace({}); if (space) { const keystonesIndex = await metaspace.getSpecialIbGib({ type: "keystones", space, initialize: true, }); const registeredKeystones = keystonesIndex?.rel8ns?.keystone ?? []; this.hasExistingIdentities = registeredKeystones.length > 0; } } catch (error) { console.warn(`${lc} Error checking existing identities: ${extractErrorMsg(error)}`); this.hasExistingIdentities = false; } const checkboxes = [this.elements!.checkboxMakeActive, this.elements!.checkboxPasskeyMakeActive]; for (const checkbox of checkboxes) { if (checkbox) { if (!this.hasExistingIdentities) { checkbox.checked = true; checkbox.disabled = true; checkbox.title = "At least one active identity is required."; } else { checkbox.disabled = false; checkbox.title = ""; } } } } private initHandlers() { // Mode Tab Switching (Quick vs Passkey vs Password-based) const tabQuick = this.shadowRoot?.getElementById('tab-quick') as HTMLButtonElement | null; const tabPasskey = this.shadowRoot?.getElementById('tab-passkey') as HTMLButtonElement | null; const tabPassword = this.shadowRoot?.getElementById('tab-password') as HTMLButtonElement | null; const panelQuick = this.shadowRoot?.getElementById('panel-quick') as HTMLDivElement | null; const panelPasskey = this.shadowRoot?.getElementById('panel-passkey') as HTMLDivElement | null; const panelPassword = this.shadowRoot?.getElementById('panel-password') as HTMLDivElement | null; const subtitleQuick = this.shadowRoot?.getElementById('subtitle-quick') as HTMLParagraphElement | null; const subtitlePasskey = this.shadowRoot?.getElementById('subtitle-passkey') as HTMLParagraphElement | null; const subtitlePassword = this.shadowRoot?.getElementById('subtitle-password') as HTMLParagraphElement | null; const switchTab = (mode: 'quick' | 'passkey' | 'password') => { tabQuick?.classList.toggle('active', mode === 'quick'); tabPasskey?.classList.toggle('active', mode === 'passkey'); tabPassword?.classList.toggle('active', mode === 'password'); panelQuick?.classList.toggle('hidden', mode !== 'quick'); panelPasskey?.classList.toggle('hidden', mode !== 'passkey'); panelPassword?.classList.toggle('hidden', mode !== 'password'); subtitleQuick?.classList.toggle('hidden', mode !== 'quick'); subtitlePasskey?.classList.toggle('hidden', mode !== 'passkey'); subtitlePassword?.classList.toggle('hidden', mode !== 'password'); // Dynamic visibility for shared identity profile fields: // - Quick mode: hide description and primary email (uses Quick OTP email) // - Passkey & Password modes: show description and primary email this.elements?.groupDescription?.classList.toggle('hidden', mode === 'quick'); this.elements?.groupEmail?.classList.toggle('hidden', mode === 'quick'); // Sync email lock-step if (this.elements?.inputEmail) this.elements.inputEmail.value = this.email; if (this.elements?.inputQuickEmail) this.elements.inputQuickEmail.value = this.email; }; tabQuick?.addEventListener('click', () => switchTab('quick')); tabPasskey?.addEventListener('click', () => switchTab('passkey')); tabPassword?.addEventListener('click', () => switchTab('password')); const btnGoogleSso = this.shadowRoot?.getElementById('btn-google-sso') as HTMLButtonElement | null; btnGoogleSso?.addEventListener('click', () => this.handleSsoClick('google')); const btnGithubSso = this.shadowRoot?.getElementById('btn-github-sso') as HTMLButtonElement | null; btnGithubSso?.addEventListener('click', () => this.handleSsoClick('github')); // Email Sign-In Info Button const btnEmailInfo = this.shadowRoot?.getElementById('btn-email-info') as HTMLButtonElement | null; btnEmailInfo?.addEventListener('click', (e) => { e.preventDefault(); alertUser({ title: 'Custom Email Sign-In', msg: 'When you enter your email address, we dispatch a 6-digit one-time verification code to your inbox.\n\nEntering the code creates your cryptographic identity delegate on your device and signs you in seamlessly without needing a password!' }); }); // Quick Email Verification Code Handlers const btnQuickSendCode = this.shadowRoot?.getElementById('btn-quick-send-code') as HTMLButtonElement | null; const btnQuickVerifyCode = this.shadowRoot?.getElementById('btn-quick-verify-code') as HTMLButtonElement | null; const btnQuickResendCode = this.shadowRoot?.getElementById('btn-quick-resend-code') as HTMLButtonElement | null; btnQuickSendCode?.addEventListener('click', () => this.handleQuickSendCode()); btnQuickVerifyCode?.addEventListener('click', () => this.handleQuickVerifyCode()); btnQuickResendCode?.addEventListener('click', () => this.handleQuickResendCode()); this.elements!.btnGeneratePasskey?.addEventListener('click', () => this.handleGeneratePasskey()); this.elements!.btnGenerateEl.addEventListener('click', () => this.handleGenerate()); this.shadowRoot!.getElementById('btn-switch-sign-in')?.addEventListener('click', (e) => { e.preventDefault(); window.dispatchEvent(new CustomEvent(EVENT_IBGIB_UI_MESSAGE, { detail: { action: 'show-add-device' }, bubbles: true, composed: true })); }); // Lock-step email synchronization this.elements!.inputEmail?.addEventListener('input', () => { this.elements!.inputEmail.classList.remove('invalid'); this.email = this.elements!.inputEmail.value.trim(); if (this.elements!.inputQuickEmail) { this.elements!.inputQuickEmail.value = this.email; } }); this.elements!.inputQuickEmail?.addEventListener('input', () => { this.elements!.inputQuickEmail.classList.remove('invalid'); this.email = this.elements!.inputQuickEmail.value.trim(); if (this.elements!.inputEmail) { this.elements!.inputEmail.value = this.email; } }); const updateQuickActionButtonsState = () => { const hasUsername = !!this.elements?.inputUsername?.value?.trim(); const hasSiteUsername = !!this.elements?.inputSiteUsername?.value?.trim(); const isReady = hasUsername && hasSiteUsername; const disabledTitle = !isReady ? "Please enter a Username and Site Handle first." : ""; if (this.elements?.btnGoogleSso) { this.elements.btnGoogleSso.disabled = !isReady; this.elements.btnGoogleSso.title = disabledTitle; } if (this.elements?.btnGithubSso) { this.elements.btnGithubSso.disabled = !isReady; this.elements.btnGithubSso.title = disabledTitle; } if (this.elements?.btnQuickSendCode) { this.elements.btnQuickSendCode.disabled = !isReady; this.elements.btnQuickSendCode.title = disabledTitle; } }; // Initialize button disabled state updateQuickActionButtonsState(); // Clear invalid styling and handle auto-suggestion this.elements!.inputUsername?.addEventListener('input', () => { this.elements!.inputUsername.classList.remove('invalid'); if (!this.userEditedSiteUsername && this.elements!.inputSiteUsername) { const raw = this.elements!.inputUsername.value.trim(); const suggested = getSaferSubstring({ text: raw, keepLiterals: [], replaceMap: { ' ': '_' } }).toLowerCase(); this.elements!.inputSiteUsername.value = suggested; this.triggerSiteUsernameCheck(); } updateQuickActionButtonsState(); }); this.elements!.inputSiteUsername?.addEventListener('input', () => { this.userEditedSiteUsername = true; this.elements!.inputSiteUsername.classList.remove('invalid'); this.triggerSiteUsernameCheck(); updateQuickActionButtonsState(); }); this.elements!.inputExceptionKey?.addEventListener('input', () => { this.elements!.inputExceptionKey.classList.remove('invalid'); }); this.elements!.inputDescription?.addEventListener('input', () => { this.elements!.inputDescription.classList.remove('invalid'); }); this.elements!.inputSecret?.addEventListener('input', () => { this.elements!.inputSecret.classList.remove('invalid'); }); // Developer Program Exception Key Request Modal const modalException = this.elements!.modalExceptionRequest; const openModal = (defaultHandle: string, defaultEmail: string) => { if (!modalException) return; if (this.elements!.inputReqSiteUsername) this.elements!.inputReqSiteUsername.value = defaultHandle; if (this.elements!.inputReqEmail && defaultEmail) this.elements!.inputReqEmail.value = defaultEmail; if (this.elements!.statusExceptionRequest) this.elements!.statusExceptionRequest.textContent = ''; modalException.classList.remove('hidden'); }; const closeModal = () => { modalException?.classList.add('hidden'); }; this.elements!.linkRequestException?.addEventListener('click', (e) => { e.preventDefault(); const handle = this.elements!.inputSiteUsername?.value?.trim() || ''; const email = this.email; openModal(handle, email); }); this.elements!.btnCloseModalException?.addEventListener('click', () => closeModal()); this.elements!.btnCancelExceptionRequest?.addEventListener('click', () => closeModal()); this.elements!.formExceptionRequest?.addEventListener('submit', async (e) => { e.preventDefault(); const siteUsername = this.elements!.inputReqSiteUsername?.value?.trim(); const applicantName = this.elements!.inputReqApplicantName?.value?.trim(); const email = this.elements!.inputReqEmail?.value?.trim(); const projectTitle = this.elements!.inputReqProjectTitle?.value?.trim(); const reason = this.elements!.inputReqReason?.value?.trim(); const profileUrl = this.elements!.inputReqProfileUrl?.value?.trim(); if (!siteUsername || !applicantName || !email || !projectTitle || !reason) { if (this.elements!.statusExceptionRequest) { this.elements!.statusExceptionRequest.textContent = 'Please fill out all required fields.'; this.elements!.statusExceptionRequest.style.color = '#f38ba8'; } return; } if (!validateEmail(email)) { if (this.elements!.statusExceptionRequest) { this.elements!.statusExceptionRequest.textContent = 'Please enter a valid email address (e.g. user@example.com).'; this.elements!.statusExceptionRequest.style.color = '#f38ba8'; } return; } if (this.elements!.btnSubmitExceptionRequest) { this.elements!.btnSubmitExceptionRequest.disabled = true; this.elements!.btnSubmitExceptionRequest.textContent = 'Submitting...'; } try { const res = await getApiBridge().requestSiteUsernameException({ siteUsername, applicantName, email, projectTitle, reason, profileUrl: profileUrl || undefined }); if (res.success) { if (this.elements!.statusExceptionRequest) { this.elements!.statusExceptionRequest.textContent = '✓ Request submitted! Check your email for next steps.'; this.elements!.statusExceptionRequest.style.color = '#a6e3a1'; } setTimeout(() => closeModal(), 2000); } else { if (this.elements!.statusExceptionRequest) { this.elements!.statusExceptionRequest.textContent = res.message || 'Submission failed. Please try again.'; this.elements!.statusExceptionRequest.style.color = '#f38ba8'; } } } catch (err) { if (this.elements!.statusExceptionRequest) { this.elements!.statusExceptionRequest.textContent = extractErrorMsg(err); this.elements!.statusExceptionRequest.style.color = '#f38ba8'; } } finally { if (this.elements!.btnSubmitExceptionRequest) { this.elements!.btnSubmitExceptionRequest.disabled = false; this.elements!.btnSubmitExceptionRequest.textContent = 'Submit Request'; } } }); } private triggerSiteUsernameCheck(): void { const inputEl = this.elements!.inputSiteUsername; const badgeEl = this.elements!.badgeSiteUsername; const msgEl = this.elements!.msgSiteUsername; const groupExEl = this.elements!.groupExceptionKey; if (this.siteUsernameDebounceTimer) { clearTimeout(this.siteUsernameDebounceTimer); } this.siteUsernameDebounceTimer = setTimeout(async () => { if (!inputEl || !badgeEl || !msgEl || !groupExEl) return; await this.checkSiteUsernameAvailability({ siteUsername: inputEl.value, badgeEl, msgEl, groupExceptionKeyEl: groupExEl }); }, 300); } private async checkSiteUsernameAvailability({ siteUsername, badgeEl, msgEl, groupExceptionKeyEl }: { siteUsername: string; badgeEl: HTMLSpanElement; msgEl: HTMLParagraphElement; groupExceptionKeyEl: HTMLDivElement; }): Promise { const trimmed = siteUsername.trim(); if (!trimmed) { badgeEl.textContent = ''; badgeEl.className = 'handle-badge'; msgEl.textContent = ''; groupExceptionKeyEl.classList.add('hidden'); return false; } // Client-side pre-validation against KEYSTONE_USERNAME_REGEXP & whitespace if (!KEYSTONE_USERNAME_REGEXP.test(trimmed) || /\s/.test(trimmed)) { badgeEl.textContent = '✗ Invalid'; badgeEl.className = 'handle-badge unavailable'; msgEl.textContent = 'Site handle contains invalid characters. Only letters, numbers, _, -, and . are allowed (no spaces).'; groupExceptionKeyEl.classList.add('hidden'); return false; } badgeEl.textContent = 'Checking...'; badgeEl.className = 'handle-badge checking'; msgEl.textContent = ''; try { const res = await getApiBridge().checkSiteUsernameAvailability({ siteUsername: trimmed }); if (res.available) { if (res.requiresExceptionKey) { badgeEl.textContent = 'Exception Key Required'; badgeEl.className = 'handle-badge exception-needed'; msgEl.textContent = res.message || 'Short handles and reserved handles require an exception key.'; groupExceptionKeyEl.classList.remove('hidden'); return true; } else { badgeEl.textContent = '✓ Available'; badgeEl.className = 'handle-badge available'; msgEl.textContent = 'Handle is available!'; groupExceptionKeyEl.classList.add('hidden'); return true; } } else { badgeEl.textContent = '✗ Unavailable'; badgeEl.className = 'handle-badge unavailable'; if (res.reason === 'pending_reservation' && res.retryAfterMs) { const mins = Math.max(1, Math.ceil(res.retryAfterMs / 60000)); msgEl.textContent = `Temporarily reserved for verification. Resets in ~${mins} min${mins === 1 ? '' : 's'}.`; } else { msgEl.textContent = res.message || 'This handle is already taken.'; } groupExceptionKeyEl.classList.add('hidden'); return false; } } catch (error) { badgeEl.textContent = '⚠️ Check Failed'; badgeEl.className = 'handle-badge unavailable'; msgEl.textContent = extractErrorMsg(error); return false; } } private setButtonLoading(isLoading: boolean) { const btn = this.shadowRoot?.getElementById('btn-generate') as HTMLButtonElement | null; if (btn) { btn.disabled = isLoading; btn.textContent = isLoading ? 'Generating...' : 'Generate Identity Keystone'; } } /** * Triggered by Google / GitHub SSO buttons. * Opens OAuth popup, exchanges authorization code, builds local delegate keystone, * registers delegate on server, and finalizes onboarding. */ private async handleSsoClick(providerId: 'google' | 'github'): Promise { const lc = `${this.lc}[${this.handleSsoClick.name}]`; const providerName = providerId === 'google' ? 'Google' : providerId === 'github' ? 'GitHub' : providerId; const customUsername = (this.elements?.inputUsername?.value || '').trim(); const customSiteUsername = (this.elements?.inputSiteUsername?.value || '').trim(); const customExceptionKey = (this.elements?.inputExceptionKey?.value || '').trim(); if (!customUsername) { this.elements?.inputUsername?.classList.add('invalid'); this.elements?.inputUsername?.focus(); this.setStatus("Please enter a Username before continuing with SSO.", "error"); return; } if (!this.validateInputPattern({ input: this.elements!.inputUsername, pattern: KEYSTONE_USERNAME_REGEXP, errorMessage: "Username must be 1-63 characters (alphanumeric, underscores, hyphens, dots).", required: true })) { this.setStatus("Invalid username. Must be 1-63 characters (alphanumeric, _, -, .).", "error"); return; } if (!customSiteUsername) { this.elements?.inputSiteUsername?.classList.add('invalid'); this.elements?.inputSiteUsername?.focus(); this.setStatus("Please enter a Site Handle before continuing with SSO.", "error"); return; } if (!KEYSTONE_USERNAME_REGEXP.test(customSiteUsername) || /\s/.test(customSiteUsername)) { this.elements?.inputSiteUsername?.classList.add('invalid'); this.elements?.inputSiteUsername?.focus(); this.setStatus("Site handle must be 1-63 characters (alphanumeric, _, -, .) with no spaces.", "error"); return; } // Pre-check site username availability on server before opening popup this.showBusy({ isBusy: true, title: 'Checking Handle Availability...', msg: 'Checking server identity index...', animationEmoji: '🔍' }); const resSiteCheck = await getApiBridge().checkSiteUsernameAvailability({ siteUsername: customSiteUsername }); if (!resSiteCheck.available) { this.showBusy({ isBusy: false }); this.elements?.inputSiteUsername?.classList.add('invalid'); this.setStatus(resSiteCheck.message || "Site handle is already taken.", "error"); return; } if (resSiteCheck.requiresExceptionKey && !customExceptionKey) { this.showBusy({ isBusy: false }); this.elements?.groupExceptionKey?.classList.remove('hidden'); this.elements?.inputExceptionKey?.classList.add('invalid'); this.elements?.inputExceptionKey?.focus(); this.setStatus(resSiteCheck.message || "Short handles (≤ 6 chars) and reserved handles require an approved developer exception key.", "error"); return; } this.setStatus(`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 resPopup = await executeSsoOAuthPopup({ providerId }); if (!resPopup.success || !resPopup.code) { this.setStatus(resPopup.message || `${providerName} sign-in cancelled.`, 'error'); return; } this.setStatus(`Exchanging ${providerName} authorization code...`, 'info'); this.showBusy({ isBusy: true, title: 'Exchanging SSO Code...', msg: `Validating ${providerName} authorization code with custodian server...`, animationEmoji: '🔐' }); const apiBridge = getApiBridge(); const resLogin = await apiBridge.postSsoLogin({ code: resPopup.code, providerId, redirectUri: resPopup.redirectUri!, username: customUsername || undefined, siteUsername: customSiteUsername || undefined, siteUsernameExceptionKey: customExceptionKey || undefined, }); if (!resLogin.success || !resLogin.primaryTjpAddr || !resLogin.custodianReqs) { this.setStatus(resLogin.message || `${providerName} login failed.`, 'error'); return; } const { primaryTjpAddr, custodianReqs } = resLogin; this.setStatus('Deriving local device delegate keys...', 'info'); this.showBusy({ isBusy: true, title: 'Creating Sync Delegate...', msg: 'Generating cryptographic sync delegate keys...', 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 email = resLogin.userInfo?.email || ''; const rawUsername = customUsername || resLogin.userInfo?.name || (email ? email.split('@')[0] : 'user'); const username = getSaferSubstring({ text: rawUsername, length: 62, keepLiterals: [], replaceMap: { ' ': '_' } }).toLowerCase(); const { delegateKeystone, delegateSecret } = await createOAuth2DeviceSyncDelegate({ providerId, username, primaryTjpAddr, custodianReqs, metaspace, space, }); 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 apiBridge.postCustodianRegisterDelegate({ delegateKeystone }); if (!resRegister.success) { this.setStatus(resRegister.message || 'Custodian delegate registration failed.', 'error'); return; } // Finalize onboarding: update keystones index, activate identity, and navigate to identity manager await this.finalizeIdentityOnboarding({ primaryKeystone: resRegister.evolvedPrimaryKeystone || resLogin.primaryKeystone, primaryGraph: resRegister.primaryGraph || resLogin.primaryGraph, delegateKeystone, primaryTjpAddr, delegateSecret, makeActive: true, siteUsername: (this.elements!.inputSiteUsername?.value || '').trim() || (resLogin as any)?.siteUsername || undefined, }); } catch (error) { const emsg = extractErrorMsg(error); console.error(`${lc} ${emsg}`, error); this.setStatus(`SSO sign-in error: ${emsg}`, 'error'); } finally { this.showBusy({ isBusy: false }); } } /** * Triggered by the "Send 6-Digit Verification Code" button. * Validates quick email input and dispatches a 6-digit OTP code to the user's inbox. */ private async handleQuickSendCode(): Promise { const lc = `${this.lc}[${this.handleQuickSendCode.name}]`; const btnQuickSendCode = this.shadowRoot?.getElementById('btn-quick-send-code') as HTMLButtonElement | null; const inputQuickEmail = this.shadowRoot?.getElementById('input-quick-email') as HTMLInputElement | null; const secQuickOtp = this.shadowRoot?.getElementById('section-quick-otp') as HTMLDivElement | null; const username = (this.elements?.inputUsername?.value || '').trim(); const siteUsername = (this.elements?.inputSiteUsername?.value || '').trim(); const exceptionKey = (this.elements?.inputExceptionKey?.value || '').trim(); if (!username) { this.elements?.inputUsername?.classList.add('invalid'); this.elements?.inputUsername?.focus(); this.setStatus("Please enter a Username before requesting a verification code.", "error"); return; } if (!this.validateInputPattern({ input: this.elements!.inputUsername, pattern: KEYSTONE_USERNAME_REGEXP, errorMessage: "Username must be 1-63 characters (alphanumeric, underscores, hyphens, dots).", required: true })) { this.setStatus("Invalid username. Must be 1-63 characters (alphanumeric, _, -, .).", "error"); return; } if (!siteUsername) { this.elements?.inputSiteUsername?.classList.add('invalid'); this.elements?.inputSiteUsername?.focus(); this.setStatus("Please enter a Site Handle before requesting a verification code.", "error"); return; } if (!KEYSTONE_USERNAME_REGEXP.test(siteUsername) || /\s/.test(siteUsername)) { this.elements?.inputSiteUsername?.classList.add('invalid'); this.elements?.inputSiteUsername?.focus(); this.setStatus("Site handle must be 1-63 characters (alphanumeric, _, -, .) with no spaces.", "error"); return; } const email = (inputQuickEmail?.value || this.email || '').trim(); if (!email || !validateEmail(email)) { inputQuickEmail?.classList.add('invalid'); inputQuickEmail?.focus(); this.setStatus("Please enter a valid email address.", "error"); return; } // Pre-check site username availability on server before sending code this.showBusy({ isBusy: true, title: 'Checking Handle Availability...', msg: 'Checking server identity index...', animationEmoji: '🔍' }); const resSiteCheck = await getApiBridge().checkSiteUsernameAvailability({ siteUsername }); if (!resSiteCheck.available) { this.showBusy({ isBusy: false }); this.elements?.inputSiteUsername?.classList.add('invalid'); this.setStatus(resSiteCheck.message || "Site handle is already taken.", "error"); return; } if (resSiteCheck.requiresExceptionKey && !exceptionKey) { this.showBusy({ isBusy: false }); this.elements?.groupExceptionKey?.classList.remove('hidden'); this.elements?.inputExceptionKey?.classList.add('invalid'); this.elements?.inputExceptionKey?.focus(); this.setStatus(resSiteCheck.message || "Short handles (≤ 6 chars) and reserved handles require an approved developer exception key.", "error"); return; } this.showBusy({ isBusy: false }); if (btnQuickSendCode) { btnQuickSendCode.disabled = true; btnQuickSendCode.textContent = "Sending Verification Code..."; } this.setStatus("Requesting verification code...", "info"); try { const resCode = await getApiBridge().postVerificationCode({ email }); if (resCode.success) { secQuickOtp?.classList.remove('hidden'); this.setStatus("✉️ 6-digit verification code sent to your email!", "success"); this.startResendCooldownTimer({ btnId: 'btn-quick-resend-code', durationSecs: 30 }); } else { this.setStatus(resCode.message || "Failed to dispatch verification code.", "error"); } } catch (error) { const emsg = extractErrorMsg(error); console.error(`${lc} ${emsg}`); this.setStatus(`Failed to dispatch verification code: ${emsg}`, "error"); } finally { if (btnQuickSendCode) { btnQuickSendCode.disabled = false; btnQuickSendCode.textContent = "Send 6-Digit Verification Code"; } } } /** * Triggered by the "Resend Code" button in the quick email panel. */ private async handleQuickResendCode(): Promise { const lc = `${this.lc}[${this.handleQuickResendCode.name}]`; const btnQuickResendCode = this.shadowRoot?.getElementById('btn-quick-resend-code') as HTMLButtonElement | null; const inputQuickEmail = this.shadowRoot?.getElementById('input-quick-email') as HTMLInputElement | null; const inputQuickOtp = this.shadowRoot?.getElementById('input-quick-otp') as HTMLInputElement | null; const email = (inputQuickEmail?.value || '').trim(); if (!email || !validateEmail(email)) { this.setStatus("Please enter a valid email address.", "error"); return; } if (btnQuickResendCode) { btnQuickResendCode.disabled = true; btnQuickResendCode.textContent = "Sending..."; } this.setStatus("Resending verification code...", "info"); try { const resCode = await getApiBridge().postVerificationCode({ email }); if (resCode.success) { this.setStatus(`✉️ Fresh 6-digit verification code sent to ${email}!`, "success"); this.startResendCooldownTimer({ btnId: 'btn-quick-resend-code', durationSecs: 30 }); if (inputQuickOtp) { inputQuickOtp.value = ''; inputQuickOtp.focus(); } } else { this.setStatus(resCode.message || "Failed to resend verification code.", "error"); if (btnQuickResendCode) { btnQuickResendCode.disabled = false; btnQuickResendCode.textContent = "Resend Code"; } } } catch (error) { const emsg = extractErrorMsg(error); console.error(`${lc} ${emsg}`); this.setStatus(`Failed to resend verification code: ${emsg}`, "error"); if (btnQuickResendCode) { btnQuickResendCode.disabled = false; btnQuickResendCode.textContent = "Resend Code"; } } } /** * Triggered by the "Verify Code" button. * Submits 6-digit OTP code to custodian server, builds local sync delegate keystone, * registers delegate on server, and finalizes onboarding. */ private async handleQuickVerifyCode(): Promise { const lc = `${this.lc}[${this.handleQuickVerifyCode.name}]`; const inputQuickEmail = this.shadowRoot?.getElementById('input-quick-email') as HTMLInputElement | null; const inputQuickOtp = this.shadowRoot?.getElementById('input-quick-otp') as HTMLInputElement | null; const btnQuickVerifyCode = this.shadowRoot?.getElementById('btn-quick-verify-code') as HTMLButtonElement | null; const email = (inputQuickEmail?.value || '').trim(); const code = (inputQuickOtp?.value || '').trim(); if (!code || code.length !== 6 || !/^\d{6}$/.test(code)) { this.setStatus("Please enter a valid 6-digit verification code.", "error"); return; } const customUsername = (this.elements?.inputUsername?.value || '').trim(); const customSiteUsername = (this.elements?.inputSiteUsername?.value || '').trim(); const customExceptionKey = (this.elements?.inputExceptionKey?.value || '').trim(); if (!customUsername) { this.elements?.inputUsername?.classList.add('invalid'); this.setStatus("Please enter a Username first.", "error"); return; } if (!customSiteUsername) { this.elements?.inputSiteUsername?.classList.add('invalid'); this.setStatus("Please enter a Site Handle first.", "error"); return; } if (!KEYSTONE_USERNAME_REGEXP.test(customSiteUsername) || /\s/.test(customSiteUsername)) { this.elements?.inputSiteUsername?.classList.add('invalid'); this.setStatus("Site handle must be 1-63 characters (alphanumeric, _, -, .) with no spaces.", "error"); return; } if (btnQuickVerifyCode) { btnQuickVerifyCode.disabled = true; btnQuickVerifyCode.textContent = "Verifying..."; } this.setStatus("Verifying code & completing onboarding...", "info"); this.showBusy({ isBusy: true, title: 'Verifying Code...', msg: 'Great! Juust a moment...\n(Validating code, promoting server domain space, other security stuff...)', animationEmoji: '🔐' }); try { const resGenesis = await getApiBridge().postCustodianGenesis({ email, code, username: customUsername || undefined, siteUsername: customSiteUsername || undefined, siteUsernameExceptionKey: customExceptionKey || undefined, }); if (!resGenesis.success || !resGenesis.primaryTjpAddr || !resGenesis.custodianReqs) { this.setStatus(resGenesis.message || "Custodian verification failed.", "error"); return; } const primaryTjpAddr = resGenesis.primaryTjpAddr; const custodianReqs = resGenesis.custodianReqs; // Construct local delegate keystone this.setStatus("Constructing local sync delegate...", "info"); this.showBusy({ isBusy: true, title: 'Creating Sync Delegate...', msg: 'Generating cryptographic sync delegate keys...', animationEmoji: '🔑' }); const metaspace = getContextModeService().getPersistentMetaspace(); const space = await metaspace.getLocalUserSpace({ lock: false }); if (!space) { this.setStatus("Failed to access local user space.", "error"); return; } const { delegateKeystone, delegateSecret } = await createEmailDeviceSyncDelegate({ email, username: customUsername || undefined, primaryTjpAddr, custodianReqs, metaspace, space, }); // Register delegate with server 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 || "Failed to register delegate with custodian server.", "error"); return; } // Finalize onboarding: update keystones index, activate identity, and navigate to identity manager await this.finalizeIdentityOnboarding({ primaryKeystone: resRegister.evolvedPrimaryKeystone || resGenesis.primaryKeystone, primaryGraph: resRegister.primaryGraph || resGenesis.primaryGraph, delegateKeystone, primaryTjpAddr, delegateSecret, makeActive: true, siteUsername: (this.elements!.inputSiteUsername?.value || '').trim() || (resGenesis as any)?.siteUsername || undefined, }); } catch (error) { const emsg = extractErrorMsg(error); console.error(`${lc} ${emsg}`, error); this.setStatus(`Error: ${emsg}`, "error"); } finally { this.showBusy({ isBusy: false }); if (btnQuickVerifyCode) { btnQuickVerifyCode.disabled = false; btnQuickVerifyCode.textContent = "Verify Code"; } } } /** * Triggered by the "Create Identity with Passkey / Biometrics" button. * Orchestrates: WebAuthn Credential -> PRF Secret Evaluation -> Local Passkey Genesis -> Graph Collection -> Server Sync. */ private async handleGeneratePasskey() { const lc = `${this.lc}[${this.handleGeneratePasskey.name}]`; const btn = this.shadowRoot?.getElementById('btn-generate-passkey') as HTMLButtonElement | null; try { console.log(`${lc} starting passkey genesis orchestration...`); const humanEyeDelay = 500; this.setStatus("Initializing passkey genesis...", "info"); const inputUsername = this.elements!.inputUsername; const inputDesc = this.elements!.inputDescription; const inputEmail = this.elements!.inputEmail; const inputName = this.elements!.inputPasskeyName; const checkboxActive = this.elements!.checkboxPasskeyMakeActive; const username = (inputUsername?.value || '').trim(); if (inputUsername) inputUsername.value = username; if (!this.validateInputPattern({ input: inputUsername!, pattern: KEYSTONE_USERNAME_REGEXP, errorMessage: "Username must be 1-63 characters (alphanumeric, underscores, hyphens, dots).", required: true })) { this.setStatus("Invalid username. Must be 1-63 characters (alphanumeric, _, -, .).", "error"); return; } const description = (inputDesc?.value || '').trim(); if (inputDesc) inputDesc.value = description; if (inputDesc && !this.validateInputPattern({ input: inputDesc, pattern: KEYSTONE_DESCRIPTION_REGEXP, errorMessage: "Description must be 0-128 characters.", required: false })) { this.setStatus("Invalid description. Max 128 chars.", "error"); return; } const email = (inputEmail?.value || '').trim(); if (inputEmail) inputEmail.value = email; if (email && !validateEmail(email)) { inputEmail?.classList.add('invalid'); this.setStatus("Invalid email address format.", "error"); return; } const siteUsername = (this.elements!.inputSiteUsername?.value || '').trim(); if (this.elements!.inputSiteUsername) this.elements!.inputSiteUsername.value = siteUsername; const siteUsernameExceptionKey = (this.elements!.inputExceptionKey?.value || '').trim(); if (this.elements!.inputExceptionKey) this.elements!.inputExceptionKey.value = siteUsernameExceptionKey; if (siteUsername) { if (!KEYSTONE_USERNAME_REGEXP.test(siteUsername) || /\s/.test(siteUsername)) { this.elements!.inputSiteUsername?.classList.add('invalid'); this.setStatus("Site handle must be 1-63 characters (alphanumeric, _, -, .) with no spaces.", "error"); return; } } if (btn) { btn.disabled = true; btn.textContent = 'Awaiting Biometrics / Security Key...'; } // Pre-check site username availability on server if provided if (siteUsername) { this.showBusy({ isBusy: true, title: 'Checking Handle Availability...', msg: 'Checking server identity index...', animationEmoji: '🔍' }); const resSiteCheck = await getApiBridge().checkSiteUsernameAvailability({ siteUsername }); if (!resSiteCheck.available) { const msg = resSiteCheck.message || "Site handle is already taken."; this.setStatus(msg, "error"); this.showBusy({ isBusy: false }); if (btn) { btn.disabled = false; btn.textContent = '🛡️ Create Identity with Passkey / Biometrics'; } return; } if (resSiteCheck.requiresExceptionKey && !siteUsernameExceptionKey) { this.elements!.groupExceptionKey?.classList.remove('hidden'); this.elements!.inputExceptionKey?.classList.add('invalid'); this.setStatus(resSiteCheck.message || "Short handles (≤ 6 chars) and reserved handles require an approved exception key.", "error"); this.showBusy({ isBusy: false }); if (btn) { btn.disabled = false; btn.textContent = '🛡️ Create Identity with Passkey / Biometrics'; } return; } } // Pre-check email availability on server if email provided if (email) { this.showBusy({ isBusy: true, title: 'Checking Email Availability...', msg: 'Checking server identity index...', animationEmoji: '🔍' }); const resCheck = await getApiBridge().checkEmailAvailability({ email }); if (!resCheck.available) { const msg = resCheck.message || "Email/Identity is already registered."; this.setStatus(msg, "error"); this.showBusy({ isBusy: false }); if (btn) { btn.disabled = false; btn.textContent = '🛡️ Create Identity with Passkey / Biometrics'; } return; } } // 1. WebAuthn Passkey Registration & PRF Secret Evaluation 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 selectAttachment = this.shadowRoot?.querySelector('#select-passkey-attachment') as HTMLSelectElement | null; const attachmentVal = selectAttachment?.value; const authenticatorAttachment: AuthenticatorAttachment | undefined = (attachmentVal === 'cross-platform' || attachmentVal === 'platform') ? attachmentVal : undefined; const rawSalt = crypto.getRandomValues(new Uint8Array(32)); const saltHex = bufferToHex(rawSalt.buffer as ArrayBuffer); const regResult = await registerPasskeyCredential({ username, displayName: username, salt: saltHex, authenticatorAttachment, userVerification: 'preferred' }); const { credentialId, transports } = regResult; let passkeySecret = regResult.passkeySecret; if (!passkeySecret) { const prfResult = await evaluatePasskeyPrf({ credentialId, salt: saltHex, transports, userVerification: 'preferred' }); passkeySecret = prfResult.passkeySecret; } const passkeyName = (inputName?.value || '').trim() || generateDefaultDeviceTag(); 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 } : {}) }; this.showBusy({ isBusy: true, title: 'Generating Identity...', msg: 'Compiling sovereign passkey challenge pools...', animationEmoji: '⚙️' }); // 2. Prepare Sovereign Keystone Configs const sovereignPrimaryProfileName = process.env.KEYSTONE_PROFILE_SOVEREIGN_PRIMARY; if (!sovereignPrimaryProfileName || !sovereignPrimaryProfileName.trim()) { throw new Error('KEYSTONE_PROFILE_SOVEREIGN_PRIMARY environment variable is missing.'); } const builder = KeystoneProfileBuilder.buildKeystone(sovereignPrimaryProfileName.trim() as any) .withUsername(username) .withDescription(description); if (email) { builder.withEmails([email]); } builder.withDetails({ client: 'space-gib-web', role: 'domain-identity', passkeys: { [credentialId]: passkeyEntry } }); const configs = await builder.compileConfigs(); // Re-label manage pool to passkey pool id for (const cfg of configs) { if (cfg.id === 'manage') { cfg.id = poolId; } } // 3. Prepare Metaspace & Local Space const metaspace = getContextModeService().getPersistentMetaspace(); const space = await metaspace.getLocalUserSpace({ lock: false }); if (!space) { this.setStatus("Failed to access local user space.", "error"); return; } // 4. Perform Local Genesis this.showBusy({ isBusy: true, title: 'Generating Identity...', msg: 'Generating cryptographic domain identity keys...', animationEmoji: '🔑' }); const keystoneService = new KeystoneService_V1(); const keystoneIbGib = await keystoneService.genesis({ masterSecret: passkeySecret, emails: email ? [email] : undefined, credentialIds: [credentialId], configs, metaspace, space, frameDetails: builder.getFrameDetails(), isPrimary: true }); const primaryTjpAddr = getTjpAddr({ ibGib: keystoneIbGib, defaultIfNone: 'incomingAddr' })!; console.log(`${lc} passkey local genesis complete: ${primaryTjpAddr}`); await delay(humanEyeDelay); this.setStatus("Local genesis complete. Creating passkey sync delegate...", "info"); // 5. Create Passkey Device Sync Delegate this.showBusy({ isBusy: true, title: 'Creating Sync Delegate...', msg: 'Generating cryptographic sync delegate keys...', animationEmoji: '🔑' }); const { delegateKeystone: delegateKeystoneIbGib, deviceTag, delegateSecret } = await createPasskeyDeviceSyncDelegate({ username, primaryTjpAddr, passkeySecret, transports, metaspace, space, }); // 6. Register Delegate locally on primary keystone const evolvedParentKeystoneIbGib = await keystoneService.registerDelegate({ parentKeystone: keystoneIbGib, delegateKeystone: delegateKeystoneIbGib, masterSecret: passkeySecret, signingPoolId: poolId, allowedVerbs: [KEYSTONE_VERB_SYNC, KEYSTONE_VERB_CONNECT], deviceName: deviceTag, metaspace, space, }); // 7. Post to Server & Handle Verification this.setStatus("Syncing genesis keystone to server...", "info"); this.showBusy({ isBusy: true, title: 'Syncing to Server...', msg: 'Staging pending genesis keystone on server...', animationEmoji: '🔄' }); let currentSiteUsername = siteUsername; let currentExceptionKey = siteUsernameExceptionKey; let resSync = await getApiBridge().postGenesisKeystone(keystoneIbGib, { siteUsername: currentSiteUsername || undefined, siteUsernameExceptionKey: currentExceptionKey || undefined }); // Local-first 409 recovery hook via promptForText while (!resSync.success && resSync.statusCode === 409) { this.showBusy({ isBusy: false }); const promptMsg = resSync.message ? `${resSync.message}\n\nPlease choose a new unique site handle:` : `Handle '@${currentSiteUsername}' was just claimed. Please choose a new unique site handle:`; const newHandle = await promptForText({ title: 'Handle Already Claimed', msg: promptMsg, defaultValue: currentSiteUsername, cancelable: true }); if (!newHandle || !newHandle.trim()) { this.setStatus("Identity creation aborted: handle already claimed.", "error"); if (btn) { btn.disabled = false; btn.textContent = '🛡️ Create Identity with Passkey / Biometrics'; } return; } currentSiteUsername = newHandle.trim(); if (this.elements!.inputSiteUsername) { this.elements!.inputSiteUsername.value = currentSiteUsername; } this.showBusy({ isBusy: true, title: 'Syncing to Server...', msg: `Retrying server sync with handle '@${currentSiteUsername}'...`, animationEmoji: '🔄' }); resSync = await getApiBridge().postGenesisKeystone(keystoneIbGib, { siteUsername: currentSiteUsername, siteUsernameExceptionKey: currentExceptionKey || undefined }); } if (!resSync.success) { const msg = `Sync failed: ${resSync.message}`; console.error(`${lc} ${msg}`); this.setStatus(msg, "error"); this.showBusy({ isBusy: false }); if (btn) { btn.disabled = false; btn.textContent = '🛡️ Create Identity with Passkey / Biometrics'; } return; } const isPending = (resSync as any).pending || (resSync as any).statusCode === 202; if (isPending) { console.log(`${lc} Genesis keystone quarantined in pending staging area. Prompting for 6-digit verification code.`); this.showBusy({ isBusy: false }); this.setFormDisabled(true); if (!this.shadowRoot) { throw new Error(`(UNEXPECTED) this.shadowRoot falsy? (E: 6d316f5a2bf8e6a0286306468d35c826)`); } const secVerify = shadowRoot_getElementById(this.shadowRoot, 'section-code-verification'); const inputVerifyCode = shadowRoot_getElementById(this.shadowRoot, 'input-verify-code'); const btnSubmitCode = shadowRoot_getElementById(this.shadowRoot, 'btn-submit-code'); const btnResendCode = shadowRoot_getElementById(this.shadowRoot, 'btn-resend-code'); secVerify.classList.remove('hidden'); secVerify.scrollIntoView({ behavior: 'smooth', block: 'nearest' }); this.setStatus("✉️ Verification code sent to your email. Enter 6-digit code below:", "info"); this.startResendCooldownTimer({ btnId: 'btn-resend-code', durationSecs: 30 }); const onResendCode = async () => { btnResendCode.disabled = true; btnResendCode.textContent = "Sending..."; this.setStatus("Resending verification code to your email...", "info"); try { const resResend = await getApiBridge().postVerificationCode({ email, primaryTjpAddr }); if (resResend.success) { this.setStatus("✉️ Fresh 6-digit verification code sent to your email!", "success"); this.startResendCooldownTimer({ btnId: 'btn-resend-code', durationSecs: 30 }); inputVerifyCode.value = ''; inputVerifyCode.focus(); } else { this.setStatus(resResend.message || "Failed to resend verification code.", "error"); btnResendCode.disabled = false; btnResendCode.textContent = "Resend Code"; } } catch (error) { const emsg = extractErrorMsg(error); console.error(`${lc} ${emsg}`); this.setStatus(`Failed to resend verification code: ${emsg}`, "error"); btnResendCode.disabled = false; btnResendCode.textContent = "Resend Code"; } }; btnResendCode.addEventListener('click', onResendCode); await new Promise((resolve) => { const onSubmitCode = async () => { const code = inputVerifyCode.value.trim(); if (!code || code.length !== 6 || !/^\d{6}$/.test(code)) { this.setStatus("Please enter a valid 6-digit numeric verification code (e.g. 482901).", "error"); return; } btnSubmitCode.disabled = true; btnSubmitCode.textContent = "Verifying..."; this.showBusy({ isBusy: true, title: 'Verifying & Activating...', msg: 'Great! Juust a moment...\n(Validating code, promoting server domain space, other security stuff...)', animationEmoji: '🔐' }); const resVerify = await getApiBridge().postVerifyCode({ email, primaryTjpAddr, code }); if (!resVerify.success) { this.showBusy({ isBusy: false }); this.setStatus(resVerify.message || "Incorrect verification code.", "error"); btnSubmitCode.disabled = false; btnSubmitCode.textContent = "Verify Code"; return; } console.log(`${lc} Code verified & server domain promoted! Syncing delegate...`); this.showBusy({ isBusy: true, title: 'Syncing Delegate...', msg: 'Posting evolved domain identity and sync delegate to server...', animationEmoji: '⚡' }); btnSubmitCode.removeEventListener('click', onSubmitCode); secVerify.classList.add('hidden'); this.setFormDisabled(false); // Now that server domain space is promoted, post evolved keystone + delegate const resEvolve = await getApiBridge().putEvolveKeystone({ domainAddr: primaryTjpAddr, keystoneIbGib: evolvedParentKeystoneIbGib, relatedKeystoneIbGibs: [delegateKeystoneIbGib] }); if (!resEvolve.success) { console.warn(`${lc} Delegate sync warning after code verification: ${resEvolve.message}`); } resolve(); }; btnSubmitCode.addEventListener('click', onSubmitCode); }); } else { // Post evolved keystone + delegate keystone immediately this.setStatus("Syncing delegate registration to server...", "info"); this.showBusy({ isBusy: true, title: 'Syncing Delegate...', msg: 'Posting evolved domain identity and sync delegate to server...', animationEmoji: '🔄' }); const resEvolve = await getApiBridge().putEvolveKeystone({ domainAddr: primaryTjpAddr, keystoneIbGib: evolvedParentKeystoneIbGib, relatedKeystoneIbGibs: [delegateKeystoneIbGib] }); if (!resEvolve.success) { const msg = `Delegate registration sync failed: ${resEvolve.message}`; console.error(`${lc} ${msg}`); this.setStatus(msg, "error"); this.showBusy({ isBusy: false }); if (btn) { btn.disabled = false; btn.textContent = '🛡️ Create Identity with Passkey / Biometrics'; } return; } } // 8. Finalize Onboarding await this.finalizeIdentityOnboarding({ primaryKeystone: evolvedParentKeystoneIbGib, delegateKeystone: delegateKeystoneIbGib, primaryTjpAddr, masterSecret: passkeySecret, delegateSecret, makeActive: checkboxActive?.checked ?? true, siteUsername: currentSiteUsername || (this.elements!.inputSiteUsername?.value || '').trim() || undefined, }); } catch (error) { const emsg = extractErrorMsg(error); console.error(`${lc} ${emsg}`, error); this.setStatus(`Error: ${emsg}`, "error"); } finally { this.showBusy({ isBusy: false }); if (btn) { btn.disabled = false; btn.textContent = '🛡️ Create Identity with Passkey / Biometrics'; } } } /** * Triggered by the "Generate" button. * Orchestrates: Secret -> Local Genesis -> Graph Collection -> Server Sync. */ private async handleGenerate() { const lc = `${this.lc}[${this.handleGenerate.name}]`; try { console.log(`${lc} starting genesis orchestration...`); const humanEyeDelay = 500; this.setStatus("Initializing genesis...", "info"); const username = this.elements!.inputUsername.value.trim(); this.elements!.inputUsername.value = username; // Validate Username using base class validation plumbing if (!this.validateInputPattern({ input: this.elements!.inputUsername, pattern: KEYSTONE_USERNAME_REGEXP, errorMessage: "Username must be 1-63 characters (alphanumeric, underscores, hyphens, dots).", required: true })) { this.setStatus("Invalid username. Must be 1-63 characters (alphanumeric, _, -, .).", "error"); return; } const description = this.elements!.inputDescription.value.trim(); this.elements!.inputDescription.value = description; if (!this.validateInputPattern({ input: this.elements!.inputDescription, pattern: KEYSTONE_DESCRIPTION_REGEXP, errorMessage: "Description must be 0-128 characters and consist of English characters and common punctuation.", required: false })) { this.setStatus("Invalid description. Alphanumeric + common punctuation, max 128 chars.", "error"); return; } // Validate Primary Email (Optional) const email = this.elements!.inputEmail.value.trim(); this.elements!.inputEmail.value = email; if (email && !validateEmail(email)) { this.elements!.inputEmail.classList.add('invalid'); this.setStatus("Invalid email address format.", "error"); return; } const siteUsername = (this.elements!.inputSiteUsername?.value || '').trim(); if (this.elements!.inputSiteUsername) this.elements!.inputSiteUsername.value = siteUsername; const siteUsernameExceptionKey = (this.elements!.inputExceptionKey?.value || '').trim(); if (this.elements!.inputExceptionKey) this.elements!.inputExceptionKey.value = siteUsernameExceptionKey; if (!siteUsername) { this.elements!.inputSiteUsername?.classList.add('invalid'); this.elements!.inputSiteUsername?.focus(); this.setStatus("Site handle (@handle) is required.", "error"); return; } if (!KEYSTONE_USERNAME_REGEXP.test(siteUsername) || /\s/.test(siteUsername)) { this.elements!.inputSiteUsername?.classList.add('invalid'); this.setStatus("Site handle must be 1-63 characters (alphanumeric, _, -, .) with no spaces.", "error"); return; } const masterSecret = this.elements!.inputSecret.value.trim(); if (!masterSecret) { this.elements!.inputSecret.classList.add('invalid'); this.setStatus("Please enter a master secret.", "error"); return; } else { this.elements!.inputSecret.classList.remove('invalid'); } this.setButtonLoading(true); // Pre-check site username availability on server this.showBusy({ isBusy: true, title: 'Checking Handle Availability...', msg: 'Checking server identity index...', animationEmoji: '🔍' }); const resSiteCheck = await getApiBridge().checkSiteUsernameAvailability({ siteUsername }); if (!resSiteCheck.available) { const msg = resSiteCheck.message || "Site handle is already taken."; this.setStatus(msg, "error"); this.showBusy({ isBusy: false }); this.setButtonLoading(false); return; } if (resSiteCheck.requiresExceptionKey && !siteUsernameExceptionKey) { this.elements!.groupExceptionKey?.classList.remove('hidden'); this.elements!.inputExceptionKey?.classList.add('invalid'); this.setStatus(resSiteCheck.message || "Short handles (≤ 6 chars) and reserved handles require an approved exception key.", "error"); this.showBusy({ isBusy: false }); this.setButtonLoading(false); return; } this.showBusy({ isBusy: false }); // 0. Pre-check email availability on server before generating local keystone if (email) { this.showBusy({ isBusy: true, title: 'Checking Email Availability...', msg: 'Checking server identity index...', animationEmoji: '🔍' }); const resCheck = await getApiBridge().checkEmailAvailability({ email }); if (!resCheck.available) { const msg = resCheck.message || "Email/Identity is already registered."; console.error(`${lc} ${msg}`); this.setStatus(msg, "error"); this.showBusy({ isBusy: false }); this.setButtonLoading(false); return; } } this.showBusy({ isBusy: true, title: 'Generating Identity...', msg: 'Preparing configuration and policy options...', animationEmoji: '⚙️' }); // 1. Prepare Primary Keystone Configs const sovereignPrimaryProfileName = process.env.KEYSTONE_PROFILE_SOVEREIGN_PRIMARY; if (!sovereignPrimaryProfileName || !sovereignPrimaryProfileName.trim()) { throw new Error('KEYSTONE_PROFILE_SOVEREIGN_PRIMARY environment variable is missing. Expected valid profile name like "primary-profile.dev" or "primary-profile.prod". (E: genuuid)'); } console.log(`${lc} Using sovereign primary keystone policy profile '${sovereignPrimaryProfileName}' (I: 90123456789abcdef0123456789abcde)`); const builder = KeystoneProfileBuilder.buildKeystone(sovereignPrimaryProfileName.trim() as any) .withUsername(username) .withDescription(description); if (email) { builder.withEmails([email]); } builder.withDetails({ client: 'space-gib-web', role: 'domain-identity' }); const configs = await builder.compileConfigs(); // 2. Prepare Metaspace/Space const metaspace = getContextModeService().getPersistentMetaspace(); const space = await metaspace.getLocalUserSpace({ lock: false }); if (!space) { this.setStatus("Failed to access local user space.", "error"); return; } // 3. Perform Genesis (Local) this.showBusy({ isBusy: true, title: 'Generating Identity...', msg: 'Generating cryptographic domain identity keys...', animationEmoji: '🔑' }); const keystoneService = new KeystoneService_V1(); const keystoneIbGib = await keystoneService.genesis({ masterSecret, emails: email ? [email] : undefined, configs, metaspace, space, frameDetails: builder.getFrameDetails(), isPrimary: true }); const primaryTjpAddr = getTjpAddr({ ibGib: keystoneIbGib, defaultIfNone: 'incomingAddr' })!; console.log(`${lc} local genesis complete: ${primaryTjpAddr}`); await delay(humanEyeDelay); // for human eyes, not needed for functionality this.setStatus("Local genesis complete. Creating sync delegate...", "info"); console.log(`✓ Local genesis complete: ${primaryTjpAddr}`); // Automatically generate Sync Delegate Keystone this.showBusy({ isBusy: true, title: 'Creating Sync Delegate...', msg: 'Generating cryptographic sync delegate keys...', animationEmoji: '🔑' }); const { delegateKeystone: delegateKeystoneIbGib, deviceTag, delegateSecret } = await createSovereignDeviceSyncDelegate({ username, primaryTjpAddr, masterSecret, metaspace, space, }); const delegateAddr = getIbGibAddr({ ibGib: delegateKeystoneIbGib }); console.log(`${lc} sync delegate genesis complete: ${delegateAddr}`); await delay(humanEyeDelay); // Register delegate on parent this.setStatus("Registering delegate on parent...", "info"); this.showBusy({ isBusy: true, title: 'Registering Delegate...', msg: 'Evolving domain identity to register delegate...', animationEmoji: '📝' }); const evolvedParentKeystoneIbGib = await keystoneService.registerDelegate({ parentKeystone: keystoneIbGib, delegateKeystone: delegateKeystoneIbGib, masterSecret, signingPoolId: 'manage', metaspace, space, allowedVerbs: [KEYSTONE_VERB_SYNC, KEYSTONE_VERB_CONNECT], deviceName: deviceTag, }); const evolvedParentAddr = getIbGibAddr({ ibGib: evolvedParentKeystoneIbGib }); console.log(`${lc} delegate registered. evolved parent address: ${evolvedParentAddr}`); await delay(humanEyeDelay); // 4. Post to Server this.setStatus("Syncing genesis keystone to server...", "info"); this.showBusy({ isBusy: true, title: 'Syncing to Server...', msg: 'Staging pending genesis keystone on server...', animationEmoji: '🔄' }); let currentSiteUsername = siteUsername; let currentExceptionKey = siteUsernameExceptionKey; let resSync = await getApiBridge().postGenesisKeystone(keystoneIbGib, { siteUsername: currentSiteUsername || undefined, siteUsernameExceptionKey: currentExceptionKey || undefined }); // Local-first 409 recovery hook via promptForText while (!resSync.success && resSync.statusCode === 409) { this.showBusy({ isBusy: false }); const promptMsg = resSync.message ? `${resSync.message}\n\nPlease choose a new unique site handle:` : `Handle '@${currentSiteUsername}' was just claimed. Please choose a new unique site handle:`; const newHandle = await promptForText({ title: 'Handle Already Claimed', msg: promptMsg, defaultValue: currentSiteUsername, cancelable: true }); if (!newHandle || !newHandle.trim()) { this.setStatus("Identity creation aborted: handle already claimed.", "error"); this.setButtonLoading(false); return; } currentSiteUsername = newHandle.trim(); if (this.elements!.inputSiteUsername) { this.elements!.inputSiteUsername.value = currentSiteUsername; } this.showBusy({ isBusy: true, title: 'Syncing to Server...', msg: `Retrying server sync with handle '@${currentSiteUsername}'...`, animationEmoji: '🔄' }); resSync = await getApiBridge().postGenesisKeystone(keystoneIbGib, { siteUsername: currentSiteUsername, siteUsernameExceptionKey: currentExceptionKey || undefined }); } if (!resSync.success) { const msg = `Sync failed: ${resSync.message}`; console.error(`${lc} ${msg}`); this.setStatus(msg, "error"); this.showBusy({ isBusy: false }); this.setButtonLoading(false); return; } const isPending = (resSync as any).pending || (resSync as any).statusCode === 202; if (isPending) { console.log(`${lc} Genesis keystone quarantined in pending staging area. Prompting for 6-digit verification code.`); this.showBusy({ isBusy: false }); this.setFormDisabled(true); if (!this.shadowRoot) { throw new Error(`(UNEXPECTED) this.shadowRoot falsy? (E: 6d316f5a2bf8e6a0286306468d35c826)`); } const secVerify = shadowRoot_getElementById(this.shadowRoot, 'section-code-verification'); const inputVerifyCode = shadowRoot_getElementById(this.shadowRoot, 'input-verify-code'); const btnSubmitCode = shadowRoot_getElementById(this.shadowRoot, 'btn-submit-code'); const btnResendCode = shadowRoot_getElementById(this.shadowRoot, 'btn-resend-code'); secVerify.classList.remove('hidden'); secVerify.scrollIntoView({ behavior: 'smooth', block: 'nearest' }); this.setStatus("✉️ Verification code sent to your email. Enter 6-digit code below:", "info"); this.startResendCooldownTimer({ btnId: 'btn-resend-code', durationSecs: 30 }); const onResendCode = async () => { btnResendCode.disabled = true; btnResendCode.textContent = "Sending..."; this.setStatus("Resending verification code to your email...", "info"); try { const resResend = await getApiBridge().postVerificationCode({ email, primaryTjpAddr: primaryTjpAddr }); if (resResend.success) { this.setStatus("✉️ Fresh 6-digit verification code sent to your email!", "success"); this.startResendCooldownTimer({ btnId: 'btn-resend-code', durationSecs: 30 }); inputVerifyCode.value = ''; inputVerifyCode.focus(); } else { this.setStatus(resResend.message || "Failed to resend verification code.", "error"); btnResendCode.disabled = false; btnResendCode.textContent = "Resend Code"; } } catch (error) { const emsg = extractErrorMsg(error); console.error(`${lc} ${emsg}`); this.setStatus(`Failed to resend verification code: ${emsg}`, "error"); btnResendCode.disabled = false; btnResendCode.textContent = "Resend Code"; } }; btnResendCode.addEventListener('click', onResendCode); await new Promise((resolve, reject) => { const onSubmitCode = async () => { const code = inputVerifyCode.value.trim(); if (!code || code.length !== 6 || !/^\d{6}$/.test(code)) { this.setStatus("Please enter a valid 6-digit numeric verification code (e.g. 482901).", "error"); return; } btnSubmitCode.disabled = true; btnSubmitCode.textContent = "Verifying..."; this.showBusy({ isBusy: true, title: 'Verifying & Activating...', msg: 'Great! Juust a moment...\n(Validating code, promoting server domain space, other security stuff...)', animationEmoji: '🔐' }); const resVerify = await getApiBridge().postVerifyCode({ email, primaryTjpAddr: primaryTjpAddr, code }); if (!resVerify.success) { this.showBusy({ isBusy: false }); this.setStatus(resVerify.message || "Incorrect verification code.", "error"); btnSubmitCode.disabled = false; btnSubmitCode.textContent = "Verify Code"; return; } console.log(`${lc} Code verified & server domain promoted! Syncing delegate...`); this.showBusy({ isBusy: true, title: 'Syncing Delegate...', msg: 'Posting evolved domain identity and sync delegate to server...', animationEmoji: '⚡' }); btnSubmitCode.removeEventListener('click', onSubmitCode); secVerify.classList.add('hidden'); this.setFormDisabled(false); // Now that server domain space is promoted, post evolved keystone + delegate const resEvolve = await getApiBridge().putEvolveKeystone({ domainAddr: primaryTjpAddr, keystoneIbGib: evolvedParentKeystoneIbGib, relatedKeystoneIbGibs: [delegateKeystoneIbGib] }); if (!resEvolve.success) { console.warn(`${lc} Delegate sync warning after code verification: ${resEvolve.message}`); } resolve(); }; btnSubmitCode.addEventListener('click', onSubmitCode); }); } else { // Post evolved keystone + delegate keystone immediately (no email or pre-verified) this.setStatus("Syncing delegate registration to server...", "info"); this.showBusy({ isBusy: true, title: 'Syncing Delegate...', msg: 'Posting evolved domain identity and sync delegate to server...', animationEmoji: '🔄' }); const resEvolve = await getApiBridge().putEvolveKeystone({ domainAddr: primaryTjpAddr, keystoneIbGib: evolvedParentKeystoneIbGib, relatedKeystoneIbGibs: [delegateKeystoneIbGib] }); if (!resEvolve.success) { const msg = `Delegate registration sync failed: ${resEvolve.message}`; console.error(`${lc} ${msg}`); this.setStatus(msg, "error"); this.showBusy({ isBusy: false }); this.setButtonLoading(false); return; } } // 5. Finalize onboarding: update keystones index, activate identity, and navigate to identity manager await this.finalizeIdentityOnboarding({ primaryKeystone: evolvedParentKeystoneIbGib, delegateKeystone: delegateKeystoneIbGib, primaryTjpAddr: evolvedParentAddr, masterSecret, delegateSecret, makeActive: this.elements!.checkboxMakeActive?.checked ?? true, siteUsername: currentSiteUsername || (this.elements!.inputSiteUsername?.value || '').trim() || undefined, }); } catch (error) { const emsg = extractErrorMsg(error); console.error(`${lc} ${emsg}`, error); this.setStatus(`Error: ${emsg}`, "error"); } finally { this.showBusy({ isBusy: false, submitBtnSelector: '#btn-generate' }); this.setButtonLoading(false); } } /** * DRY helper to register keystones in the local keystones special index, * set the active identity, and navigate to the Identity Manager component. */ private async finalizeIdentityOnboarding({ primaryKeystone, primaryGraph, delegateKeystone, primaryTjpAddr, masterSecret, delegateSecret, makeActive = true, siteUsername, }: { primaryKeystone?: IbGib_V1; primaryGraph?: FlatIbGibGraph; delegateKeystone: IbGib_V1; primaryTjpAddr: string; masterSecret?: string; delegateSecret?: string; makeActive?: boolean; siteUsername?: string; }): Promise { const lc = `${this.lc}[${this.finalizeIdentityOnboarding.name}]`; const humanEyeDelay = 500; const metaspace = getContextModeService().getPersistentMetaspace(); if (!metaspace) { throw new Error(`(UNEXPECTED) metaspace falsy? Expected active global metaspace instance. (E: 2e37a8b6fe987449c5d8693a7d4a2827)`); } const space = await metaspace.getLocalUserSpace({}); if (!space) { throw new Error(`(UNEXPECTED) space falsy? Expected valid default local user space from metaspace. (E: 1e37a8b6fe987449c5d8693a7d4a2826)`); } // 0. Persist all frames of primary identity graph into local browser 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 }); } // 0.5 Verify primary keystone exists in local browser space via getLatestKeystone const keystoneService = new KeystoneService_V1(); try { await keystoneService.getLatestKeystone({ addr: primaryTjpAddr, metaspace, space }); } catch (error) { const emsg = `Primary identity keystone ${primaryTjpAddr} not found in local browser space! Error: ${extractErrorMsg(error)}`; console.error(`${lc} ${emsg} (E: genuuid)`); throw new Error(emsg); } // 1. Register with the local keystones special index this.showBusy({ isBusy: true, title: 'Registering Identity...', msg: 'Updating local identity indexes and scope settings...', animationEmoji: '💾' }); const ibGibsToRegister: IbGib_V1[] = [toDto({ ibGib: delegateKeystone })]; if (!primaryKeystone && !primaryGraph) { throw new Error(`(UNEXPECTED) primaryKeystone and primaryGraph both falsy? We should have gotten this back from the server. (E: e34cd882ac66633c75c53318a3b28826)`); } if (!delegateKeystone) { throw new Error(`(UNEXPECTED) delegateKeystone falsy? We should have made this locally and even registered it with the server. (E: 56b1c396269e15f008b7e298c71bc826)`); } if (primaryKeystone) { ibGibsToRegister.unshift(toDto({ ibGib: primaryKeystone })); } await updateSpecialIndex({ type: "keystones", rel8nInfos: [ { rel8nName: "keystone", ibGibs: ibGibsToRegister, }, ], metaspace, space, }); // Expose for dev tools debugging if available if (masterSecret) { (window as any).dev_domainI = primaryKeystone; (window as any).dev_domainIMasterSecret = masterSecret; (window as any).dev_syncDelegate = delegateKeystone; (window as any).dev_syncDelegateSecret = delegateSecret; } // 2. Request to make the new identity active if (makeActive) { window.dispatchEvent(new CustomEvent(EVENT_IBGIB_IDENTITY_REQUEST_CHANGE, { detail: { activeIdentityAddr: primaryTjpAddr, siteUsername: siteUsername ? siteUsername.replace(/^@/, '') : undefined, }, bubbles: true, composed: true })); } // 3. Display success status const successMsg = "Identity successfully created and activated!"; this.showBusy({ isBusy: true, title: 'Success!', msg: successMsg, animationEmoji: '✅' }); await delay(humanEyeDelay); if (this.elements?.keystoneAddrEl) { this.elements.keystoneAddrEl.textContent = primaryTjpAddr; } if (this.elements?.statusArea) { this.elements.statusArea.classList.remove('hidden'); } this.setStatus(successMsg, "success"); console.log(`${lc} ✓ ${successMsg}`); // 3.5 Sanitize secret input fields from DOM if (this.elements?.inputSecret) { this.elements.inputSecret.value = ''; } // 4. Dispatch UI navigation message to finalize onboarding window.dispatchEvent(new CustomEvent(EVENT_IBGIB_UI_MESSAGE, { detail: { action: 'identity-onboarded', addr: primaryTjpAddr, siteUsername }, bubbles: true, composed: true })); this.showBusy({ isBusy: false }); } private setFormDisabled(disabled: boolean): void { const root = this.shadowRoot!; const inputIds = [ 'input-username', 'input-description', 'input-email', 'input-secret', 'checkbox-make-active', 'btn-generate', 'btn-switch-sign-in', ]; for (const id of inputIds) { const el = root.getElementById(id) as HTMLInputElement | HTMLButtonElement | null; if (el) { el.disabled = disabled; if (disabled) { el.style.opacity = '0.5'; el.style.pointerEvents = 'none'; } else { el.style.opacity = '1'; el.style.pointerEvents = 'auto'; } } } } protected override async renderUI(): Promise { // No complex rendering needed for this simple creator component yet } override async handleContextUpdated(): Promise { await this.renderUI(); } }