(function () { function init() { const root = document.getElementById('qortal-personal-root'); if (!root) { return false; } if (root.dataset.initialized === 'true') { return true; } root.dataset.initialized = 'true'; const userMappingsUrl = root.dataset.userMappingsUrl; const userCreateWalletUrl = root.dataset.userCreateWalletUrl; const userBackupWalletUrl = root.dataset.userBackupWalletUrl; const userImportSeedLinkUrl = root.dataset.userImportSeedLinkUrl; const userImportBackupLinkUrl = root.dataset.userImportBackupLinkUrl; const userRecoveryStatusUrl = root.dataset.userRecoveryStatusUrl; const userRecoveryEnrollUrl = root.dataset.userRecoveryEnrollUrl; const userRecoveryDisableUrl = root.dataset.userRecoveryDisableUrl; const userRecoveryRevealUrl = root.dataset.userRecoveryRevealUrl; const userRecoveryCompleteEmergencyUrl = root.dataset.userRecoveryCompleteEmergencyUrl; const userUnlinkMappingUrl = root.dataset.userUnlinkMappingUrl; const userQappsPrefsUrl = root.dataset.userQappsPrefsUrl; const userQappsRulesUrl = root.dataset.userQappsRulesUrl; const userQappsRuleRevokeUrl = root.dataset.userQappsRuleRevokeUrl; const sovereignMode = root.dataset.sovereignMode === 'full_qortal' ? 'full_qortal' : 'powered'; const isPoweredMode = sovereignMode === 'powered'; const modeCopy = isPoweredMode ? { title: 'NuQloud Account Link', help: 'Import an existing account by seed phrase or backup JSON to link it with your profile.', } : { title: 'Qortal Account Link', help: 'Import an existing Qortal account by seed phrase or backup JSON to link it with your Nextcloud profile.', }; const createWalletButton = document.getElementById('qortal-create-wallet-button'); const createPasswordEl = document.getElementById('qortal-create-password'); const createKdfEl = document.getElementById('qortal-create-kdf'); const createResultEl = document.getElementById('qortal-create-wallet-result'); const backupWalletSelectEl = document.getElementById('qortal-backup-wallet-select'); const backupWalletPasswordEl = document.getElementById('qortal-backup-wallet-password'); const backupReencryptToggleEl = document.getElementById('qortal-backup-reencrypt-toggle'); const backupReencryptFieldsEl = document.getElementById('qortal-backup-reencrypt-fields'); const backupNewPasswordEl = document.getElementById('qortal-backup-new-password'); const backupNewPasswordConfirmEl = document.getElementById('qortal-backup-new-password-confirm'); const backupDownloadEl = document.getElementById('qortal-backup-download'); const backupSaveEl = document.getElementById('qortal-backup-save'); const backupWalletButton = document.getElementById('qortal-backup-wallet-button'); const backupResultEl = document.getElementById('qortal-backup-result'); const recoveryWalletSelectEl = document.getElementById('qortal-recovery-wallet-select'); const recoveryCurrentPasswordEl = document.getElementById('qortal-recovery-current-password'); const recoveryPassphraseEl = document.getElementById('qortal-recovery-passphrase'); const recoveryPassphraseConfirmEl = document.getElementById('qortal-recovery-passphrase-confirm'); const recoveryEnableManagedEl = document.getElementById('qortal-recovery-enable-managed'); const recoveryEnableEmergencyLoginEl = document.getElementById('qortal-recovery-enable-emergency-login'); const recoverySaveButton = document.getElementById('qortal-recovery-save-button'); const recoveryDisableButton = document.getElementById('qortal-recovery-disable-button'); const recoveryAdminCodeEl = document.getElementById('qortal-recovery-admin-code'); const recoveryRevealButton = document.getElementById('qortal-recovery-reveal-button'); const recoveryCompleteEmergencyButton = document.getElementById('qortal-recovery-complete-emergency-button'); const recoveryStatusEl = document.getElementById('qortal-recovery-status'); const recoveryResultEl = document.getElementById('qortal-recovery-result'); const importButton = document.getElementById('qortal-import-link'); const importBackupButton = document.getElementById('qortal-import-backup-link'); const refreshMappingsButton = document.getElementById('qortal-refresh-user-mappings'); const seedPhraseEl = document.getElementById('qortal-seed-phrase'); const walletPasswordEl = document.getElementById('qortal-wallet-password'); const backupFileEl = document.getElementById('qortal-backup-file'); const backupPasswordEl = document.getElementById('qortal-backup-password'); const feedbackEl = document.getElementById('qortal-personal-feedback'); const successEl = document.getElementById('qortal-personal-success'); const resultEl = document.getElementById('qortal-personal-result'); const mappingsBody = document.getElementById('qortal-user-mappings-body'); const defaultApprovalModeEl = document.getElementById('qortal-default-approval-mode'); const defaultApprovalTempMinutesEl = document.getElementById('qortal-default-approval-temp-minutes'); const defaultApprovalUnlock10MinEl = document.getElementById('qortal-default-approval-unlock-10-min'); const defaultUnlockSession20MinEl = document.getElementById('qortal-default-unlock-session-20-min'); const defaultTalkBridgeAutoApproveSendChatMessageEl = document.getElementById('qortal-default-talk-bridge-auto-approve-send-chat-message'); const highRiskConfirmationEnabledEl = document.getElementById('qortal-high-risk-confirmation-enabled'); const saveApprovalDefaultsButton = document.getElementById('qortal-save-approval-defaults'); const resetApprovalDefaultsButton = document.getElementById('qortal-reset-approval-defaults'); const approvalDefaultsResultEl = document.getElementById('qortal-approval-defaults-result'); const refreshApprovalRulesButton = document.getElementById('qortal-refresh-approval-rules'); const approvalRulesBody = document.getElementById('qortal-user-approval-rules-body'); const approvalRulesResultEl = document.getElementById('qortal-approval-rules-result'); const titleEl = document.getElementById('qortal-personal-title'); const helpEl = document.getElementById('qortal-personal-help'); const fullOnlySections = root.querySelectorAll('.qortal-full-only'); if ( !userMappingsUrl || !userCreateWalletUrl || !userBackupWalletUrl || !userImportSeedLinkUrl || !userImportBackupLinkUrl || !userRecoveryStatusUrl || !userRecoveryEnrollUrl || !userRecoveryDisableUrl || !userRecoveryRevealUrl || !userRecoveryCompleteEmergencyUrl || !userUnlinkMappingUrl || !userQappsPrefsUrl || !userQappsRulesUrl || !userQappsRuleRevokeUrl || !createWalletButton || !createPasswordEl || !createKdfEl || !createResultEl || !backupWalletSelectEl || !backupWalletPasswordEl || !backupReencryptToggleEl || !backupReencryptFieldsEl || !backupNewPasswordEl || !backupNewPasswordConfirmEl || !backupDownloadEl || !backupSaveEl || !backupWalletButton || !backupResultEl || !recoveryWalletSelectEl || !recoveryCurrentPasswordEl || !recoveryPassphraseEl || !recoveryPassphraseConfirmEl || !recoveryEnableManagedEl || !recoveryEnableEmergencyLoginEl || !recoverySaveButton || !recoveryDisableButton || !recoveryAdminCodeEl || !recoveryRevealButton || !recoveryCompleteEmergencyButton || !recoveryStatusEl || !recoveryResultEl || !importButton || !importBackupButton || !refreshMappingsButton || !seedPhraseEl || !walletPasswordEl || !backupFileEl || !backupPasswordEl || !feedbackEl || !successEl || !resultEl || !mappingsBody || !defaultApprovalModeEl || !defaultApprovalTempMinutesEl || !defaultApprovalUnlock10MinEl || !defaultUnlockSession20MinEl || !defaultTalkBridgeAutoApproveSendChatMessageEl || !highRiskConfirmationEnabledEl || !saveApprovalDefaultsButton || !resetApprovalDefaultsButton || !approvalDefaultsResultEl || !refreshApprovalRulesButton || !approvalRulesBody || !approvalRulesResultEl ) { return false; } const headers = { requesttoken: OC.requestToken, }; let availableWalletMappings = []; let preferredBackupWalletId = ''; function applyModeCopy() { if (titleEl) { titleEl.textContent = modeCopy.title; } if (helpEl) { helpEl.textContent = modeCopy.help; } if (isPoweredMode) { fullOnlySections.forEach(function (section) { if (section && section.classList) { section.classList.add('qortal-hidden'); } }); } root.setAttribute('data-mode-active', sovereignMode); } function setFeedback(message, isError) { feedbackEl.textContent = message; feedbackEl.classList.toggle('error', Boolean(isError)); feedbackEl.classList.toggle('success', !isError && message.length > 0); } function setSuccess(message) { successEl.textContent = message || ''; } function setApprovalDefaultsResult(message, isError) { approvalDefaultsResultEl.textContent = message || ''; approvalDefaultsResultEl.classList.toggle('error', Boolean(isError)); } function setApprovalRulesResult(message, isError) { approvalRulesResultEl.textContent = message || ''; approvalRulesResultEl.classList.toggle('error', Boolean(isError)); } function normalizeApprovalTempMinutes(value) { const parsed = Number(String(value || '').trim()); if (!Number.isFinite(parsed)) { return 120; } const allowed = [60, 120, 180, 360, 720, 1440, 7200]; const normalizedValue = Math.floor(parsed); return allowed.includes(normalizedValue) ? normalizedValue : 120; } function applyApprovalDefaultsFromDataset() { const unlockedMinutes = normalizeApprovalTempMinutes(root.dataset.userQappsApprovalTempMinutes || '120'); defaultApprovalModeEl.value = String(unlockedMinutes); defaultApprovalTempMinutesEl.value = String(unlockedMinutes); defaultApprovalUnlock10MinEl.checked = root.dataset.userQappsApprovalUnlock10Min === '1'; defaultUnlockSession20MinEl.checked = root.dataset.userQappsUnlockSession20Min === '1'; defaultTalkBridgeAutoApproveSendChatMessageEl.checked = root.dataset.userTalkBridgeAutoApproveSendChatMessage === '1'; highRiskConfirmationEnabledEl.checked = root.dataset.userQappsHighRiskConfirmationEnabled !== '0'; setApprovalDefaultsResult('Current unlock window loaded. Save changes to apply for future prompts.', false); } async function requestJson(url, options) { const response = await fetch(url, options); const payload = await response.json(); if (!response.ok) { throw new Error(payload.error || 'Request failed'); } return payload; } function assertBrokerOk(payload, fallbackError) { if (payload && payload.ok === false) { throw new Error(payload.error || fallbackError); } } function extractBackupObject(payload) { let candidate = payload; for (let i = 0; i < 4; i++) { if (candidate && typeof candidate === 'object' && candidate.data && typeof candidate.data === 'object') { candidate = candidate.data; continue; } if (candidate && typeof candidate === 'object' && candidate.backup !== undefined) { candidate = candidate.backup; if (typeof candidate === 'string') { try { candidate = JSON.parse(candidate); } catch (_error) { candidate = null; } } continue; } break; } return candidate && typeof candidate === 'object' ? candidate : null; } function formatTimestamp(value) { if (!value) { return ''; } try { return new Date(value).toLocaleString(); } catch (error) { return String(value); } } function formatApprovalMode(value) { const mode = String(value || '').trim().toLowerCase(); if (mode === 'persistent') { return 'Always allow'; } if (mode === 'session') { return 'Allow for session'; } return mode || '-'; } function downloadJsonFile(filename, data) { const blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' }); const url = URL.createObjectURL(blob); const anchor = document.createElement('a'); anchor.href = url; anchor.download = filename; document.body.appendChild(anchor); anchor.click(); anchor.remove(); URL.revokeObjectURL(url); } function truncateText(value, maxLength) { const text = String(value || ''); if (text.length <= maxLength) { return text; } return text.slice(0, Math.max(0, maxLength - 1)) + '…'; } function resolveMappingDisplayName(mapping) { if (!mapping || typeof mapping !== 'object') { return ''; } const primaryName = String( mapping.primaryName || mapping.registeredName || mapping.name || mapping.qortalName || '' ).trim(); if (primaryName) { return primaryName; } const address = String(mapping.qortalAddress || '').trim(); return address; } function getBackupWalletOptions() { const options = []; const seenWalletIds = new Set(); availableWalletMappings.forEach(function (mapping) { if (!mapping || typeof mapping !== 'object') { return; } const walletId = String(mapping.walletId || '').trim(); if (!walletId || seenWalletIds.has(walletId)) { return; } seenWalletIds.add(walletId); const address = String(mapping.qortalAddress || '').trim(); const status = String(mapping.status || '').trim(); const displayName = resolveMappingDisplayName(mapping); const displayAddress = address ? truncateText(address, 18) : ''; const labelBase = displayName || (displayAddress || 'linked account'); const labelWithAddress = displayName && displayAddress && displayName !== address ? (labelBase + ' • ' + displayAddress) : labelBase; options.push({ walletId: walletId, address: address, label: labelWithAddress + (status ? ' • ' + status : ''), }); }); return options; } function refreshBackupWalletSelect() { const options = getBackupWalletOptions(); backupWalletSelectEl.innerHTML = ''; if (options.length === 0) { const emptyOption = document.createElement('option'); emptyOption.value = ''; emptyOption.textContent = 'No linked accounts available'; backupWalletSelectEl.appendChild(emptyOption); return options; } options.forEach(function (entry) { const optionEl = document.createElement('option'); optionEl.value = entry.walletId; optionEl.textContent = entry.label; if (preferredBackupWalletId && preferredBackupWalletId === entry.walletId) { optionEl.selected = true; } backupWalletSelectEl.appendChild(optionEl); }); if (!preferredBackupWalletId) { backupWalletSelectEl.selectedIndex = 0; preferredBackupWalletId = backupWalletSelectEl.value; return options; } if (backupWalletSelectEl.selectedIndex < 0) { backupWalletSelectEl.selectedIndex = 0; preferredBackupWalletId = backupWalletSelectEl.value; } return options; } function refreshRecoveryWalletSelect() { const options = getBackupWalletOptions(); const previousSelectedWalletId = String(recoveryWalletSelectEl.value || '').trim(); recoveryWalletSelectEl.innerHTML = ''; if (options.length === 0) { const emptyOption = document.createElement('option'); emptyOption.value = ''; emptyOption.textContent = 'No linked accounts available'; recoveryWalletSelectEl.appendChild(emptyOption); return; } options.forEach(function (entry) { const optionEl = document.createElement('option'); optionEl.value = entry.walletId; optionEl.textContent = entry.label; if (previousSelectedWalletId && previousSelectedWalletId === entry.walletId) { optionEl.selected = true; } recoveryWalletSelectEl.appendChild(optionEl); }); if (recoveryWalletSelectEl.selectedIndex < 0) { recoveryWalletSelectEl.selectedIndex = 0; } } function setRecoveryResult(message, isError) { recoveryResultEl.textContent = message || ''; recoveryResultEl.classList.toggle('error', Boolean(isError)); } function renderRecoveryStatus(status) { const data = status && typeof status === 'object' ? status : {}; const managedEnabled = Boolean(data.managedEnabled); const emergencyLoginEnabled = Boolean(data.emergencyLoginEnabled); const lockActive = Boolean(data.lockActive); const emergencyRequired = Boolean(data.emergencyRequired); const emergencyActive = Boolean(data.emergencyActive); const walletId = String(data.walletId || '').trim(); const updatedAt = String(data.updatedAt || '').trim(); const lockedUntil = String(data.lockedUntil || '').trim(); const emergencyUntil = String(data.emergencyUntil || '').trim(); recoveryEnableManagedEl.checked = managedEnabled; recoveryEnableEmergencyLoginEl.checked = emergencyLoginEnabled; if (walletId) { const option = Array.from(recoveryWalletSelectEl.options).find(function (opt) { return String(opt.value || '').trim() === walletId; }); if (option) { recoveryWalletSelectEl.value = walletId; } } const lines = []; lines.push(managedEnabled ? 'Managed recovery is enabled.' : 'Managed recovery is disabled.'); if (updatedAt) { lines.push('Escrow updated: ' + formatTimestamp(updatedAt)); } if (lockActive && lockedUntil) { lines.push('Recovery locked until: ' + formatTimestamp(lockedUntil)); } if (emergencyRequired) { lines.push('Emergency reset active. Replace backup password now.'); } else if (emergencyActive && emergencyUntil) { lines.push('Emergency window active until: ' + formatTimestamp(emergencyUntil)); } recoveryStatusEl.textContent = lines.join(' '); } function syncBackupReencryptFields() { const showFields = backupReencryptToggleEl.checked; backupReencryptFieldsEl.classList.toggle('qortal-hidden', !showFields); if (!showFields) { backupNewPasswordEl.value = ''; backupNewPasswordConfirmEl.value = ''; } } function renderMappings(payload) { const mappings = payload && payload.ok && payload.data && Array.isArray(payload.data.mappings) ? payload.data.mappings : []; availableWalletMappings = mappings; const previousSelectedWalletId = String(backupWalletSelectEl.value || '').trim(); if (!preferredBackupWalletId && previousSelectedWalletId) { preferredBackupWalletId = previousSelectedWalletId; } refreshBackupWalletSelect(); refreshRecoveryWalletSelect(); mappingsBody.innerHTML = ''; if (mappings.length === 0) { mappingsBody.innerHTML = 'No linked Qortal accounts yet.'; return; } mappings.forEach(function (mapping) { const row = document.createElement('tr'); const addressCell = document.createElement('td'); const addressCode = document.createElement('code'); addressCode.textContent = mapping.qortalAddress || ''; addressCell.appendChild(addressCode); const walletCell = document.createElement('td'); const walletCode = document.createElement('code'); walletCode.textContent = mapping.walletId || ''; walletCell.appendChild(walletCode); const statusCell = document.createElement('td'); statusCell.textContent = mapping.status || ''; const updatedCell = document.createElement('td'); updatedCell.textContent = formatTimestamp(mapping.updatedAt); const actionCell = document.createElement('td'); const unlinkButton = document.createElement('button'); unlinkButton.className = 'button'; unlinkButton.textContent = 'Remove'; unlinkButton.addEventListener('click', async function () { try { await unlinkMapping(mapping.qortalAddress); setFeedback('Linked account removed.', false); } catch (error) { setFeedback(error.message || 'Failed to remove linked account', true); } }); actionCell.appendChild(unlinkButton); row.appendChild(addressCell); row.appendChild(walletCell); row.appendChild(statusCell); row.appendChild(updatedCell); row.appendChild(actionCell); mappingsBody.appendChild(row); }); } function renderApprovalRules(payload) { const data = payload && payload.data && typeof payload.data === 'object' ? payload.data : payload; const rules = data && Array.isArray(data.rules) ? data.rules : []; approvalRulesBody.innerHTML = ''; if (rules.length === 0) { approvalRulesBody.innerHTML = 'No active approval rules.'; return; } rules.forEach(function (rule) { const row = document.createElement('tr'); const modeCell = document.createElement('td'); modeCell.textContent = formatApprovalMode(rule.mode); const appCell = document.createElement('td'); appCell.textContent = rule.app || '-'; const scopeCell = document.createElement('td'); const scopeCode = document.createElement('code'); scopeCode.textContent = rule.scope || ''; scopeCell.appendChild(scopeCode); const requestTypeCell = document.createElement('td'); requestTypeCell.textContent = rule.requestType || '-'; const updatedCell = document.createElement('td'); updatedCell.textContent = formatTimestamp(rule.updatedAt); const actionCell = document.createElement('td'); const revokeButton = document.createElement('button'); revokeButton.className = 'button'; revokeButton.textContent = 'Remove'; revokeButton.addEventListener('click', async function () { try { await revokeApprovalRule(rule); setApprovalRulesResult('Rule removed.', false); } catch (error) { setApprovalRulesResult(error.message || 'Failed to remove rule', true); } }); actionCell.appendChild(revokeButton); row.appendChild(modeCell); row.appendChild(appCell); row.appendChild(scopeCell); row.appendChild(requestTypeCell); row.appendChild(updatedCell); row.appendChild(actionCell); approvalRulesBody.appendChild(row); }); } async function refreshMappings() { const payload = await requestJson(userMappingsUrl, { method: 'GET', headers, }); assertBrokerOk(payload, 'Failed to fetch linked accounts'); renderMappings(payload); } async function loadRecoveryStatus() { const payload = await requestJson(userRecoveryStatusUrl, { method: 'GET', headers, }); const data = payload && payload.data ? payload.data : payload; renderRecoveryStatus(data); return data; } async function saveRecoverySettings() { const walletId = String(recoveryWalletSelectEl.value || '').trim(); const currentBackupPassword = recoveryCurrentPasswordEl.value.trim(); const recoveryPassphrase = recoveryPassphraseEl.value.trim(); const recoveryPassphraseConfirm = recoveryPassphraseConfirmEl.value.trim(); const managedEnabled = recoveryEnableManagedEl.checked; const emergencyLoginEnabled = recoveryEnableEmergencyLoginEl.checked; if (!managedEnabled) { throw new Error('Enable managed recovery to save settings'); } if (!walletId) { throw new Error('Select a linked account'); } if (!currentBackupPassword) { throw new Error('Current backup password is required'); } if (!recoveryPassphrase || !recoveryPassphraseConfirm) { throw new Error('Recovery passphrase and confirmation are required'); } if (recoveryPassphrase !== recoveryPassphraseConfirm) { throw new Error('Recovery passphrase fields must match'); } if (recoveryPassphrase.length < 12) { throw new Error('Recovery passphrase must be at least 12 characters'); } const body = new URLSearchParams({ walletId, currentBackupPassword, recoveryPassphrase, recoveryPassphraseConfirm, managedEnabled: managedEnabled ? '1' : '', emergencyLoginEnabled: emergencyLoginEnabled ? '1' : '', }); const payload = await requestJson(userRecoveryEnrollUrl, { method: 'POST', headers, body, }); const data = payload && payload.data ? payload.data : payload; renderRecoveryStatus(data); recoveryCurrentPasswordEl.value = ''; recoveryPassphraseEl.value = ''; recoveryPassphraseConfirmEl.value = ''; setRecoveryResult('Recovery settings saved.', false); } async function disableRecoverySettings() { const payload = await requestJson(userRecoveryDisableUrl, { method: 'POST', headers, body: new URLSearchParams({ disable: '1' }), }); const data = payload && payload.data ? payload.data : payload; renderRecoveryStatus(data); setRecoveryResult('Managed recovery disabled.', false); } async function revealRecoveryPassword() { const recoveryPassphrase = recoveryPassphraseEl.value.trim(); const adminCode = recoveryAdminCodeEl.value.trim(); if (!recoveryPassphrase) { throw new Error('Recovery passphrase is required'); } if (!adminCode) { throw new Error('Admin recovery code is required'); } const body = new URLSearchParams({ recoveryPassphrase, adminCode, }); const payload = await requestJson(userRecoveryRevealUrl, { method: 'POST', headers, body, }); const data = payload && payload.data ? payload.data : payload; const backupPassword = data && data.backupPassword ? String(data.backupPassword) : ''; if (!backupPassword) { throw new Error('Recovery returned no password'); } if (data && data.recoveryStatus) { renderRecoveryStatus(data.recoveryStatus); } setRecoveryResult('Recovered backup password: ' + backupPassword, false); recoveryAdminCodeEl.value = ''; } async function completeEmergencyRecovery() { const payload = await requestJson(userRecoveryCompleteEmergencyUrl, { method: 'POST', headers, body: new URLSearchParams({ complete: '1' }), }); const data = payload && payload.data ? payload.data : payload; renderRecoveryStatus(data); setRecoveryResult('Emergency reset status cleared.', false); } async function refreshApprovalRules() { const payload = await requestJson(userQappsRulesUrl, { method: 'GET', headers, }); assertBrokerOk(payload, 'Failed to fetch approval rules'); renderApprovalRules(payload); } async function importAndLink() { const seedPhrase = seedPhraseEl.value.trim(); const password = walletPasswordEl.value.trim(); if (!seedPhrase || !password) { throw new Error('Seed phrase and account password are required'); } setFeedback('Importing account and linking profile...', false); setSuccess(''); const body = new URLSearchParams({ seedPhrase, password, }); const payload = await requestJson(userImportSeedLinkUrl, { method: 'POST', headers, body, }); assertBrokerOk(payload, 'Import/link failed'); resultEl.textContent = JSON.stringify(payload, null, 2); seedPhraseEl.value = ''; walletPasswordEl.value = ''; await refreshMappings(); setFeedback('Account imported and linked.', false); setSuccess('Account linked. You can now authenticate to Nextcloud using your Qortal account.'); } async function createWallet() { if (createWalletButton.disabled) { return; } const password = createPasswordEl.value.trim(); const kdfThreads = createKdfEl.value.trim(); if (!password) { throw new Error('Account password is required'); } createWalletButton.disabled = true; setFeedback('Creating account...', false); setSuccess(''); const body = new URLSearchParams({ password, kdfThreads: kdfThreads || '', }); try { const payload = await requestJson(userCreateWalletUrl, { method: 'POST', headers, body, }); assertBrokerOk(payload, 'Account creation failed'); createResultEl.textContent = JSON.stringify(payload, null, 2); const wallet = payload.wallet || payload.data || payload; if (wallet && wallet.walletId) { preferredBackupWalletId = String(wallet.walletId).trim(); } if (!backupWalletPasswordEl.value) { backupWalletPasswordEl.value = password; } await refreshMappings(); setFeedback('Account created and linked.', false); setSuccess('Account created. You can now authenticate to Nextcloud using your Qortal account.'); } finally { createWalletButton.disabled = false; } } async function backupWallet() { const walletId = backupWalletSelectEl.value.trim(); const password = backupWalletPasswordEl.value.trim(); const reEncryptEnabled = backupReencryptToggleEl.checked; const reEncryptPassword = reEncryptEnabled ? backupNewPasswordEl.value.trim() : ''; const newPassword = reEncryptEnabled ? backupNewPasswordConfirmEl.value.trim() : ''; const download = backupDownloadEl.checked; const saveToFiles = backupSaveEl.checked; if (!walletId) { throw new Error('Linked account is required'); } if (!password) { throw new Error('Current backup password is required'); } if (reEncryptEnabled && !reEncryptPassword) { throw new Error('Enter a new backup password or disable re-encrypt'); } if (reEncryptEnabled && !newPassword) { throw new Error('Confirm the new backup password'); } if (reEncryptEnabled && reEncryptPassword !== newPassword) { throw new Error('New backup password fields must match'); } if (!download && !saveToFiles) { throw new Error('Select at least one backup option'); } setFeedback('Creating account backup...', false); setSuccess(''); const body = new URLSearchParams({ walletId, password, saveToFiles: saveToFiles ? '1' : '', }); if (reEncryptEnabled) { body.set('reEncryptPassword', reEncryptPassword); body.set('newPassword', newPassword); } const payload = await requestJson(userBackupWalletUrl, { method: 'POST', headers, body, }); assertBrokerOk(payload, 'Backup failed'); backupResultEl.textContent = JSON.stringify(payload, null, 2); const backupData = extractBackupObject(payload); if (download && backupData) { const filename = `qortal-account-${walletId}.json`; downloadJsonFile(filename, backupData); } if (saveToFiles && payload.savedPath) { setSuccess(`Backup saved to Nextcloud Files: ${payload.savedPath}`); } setFeedback('Backup completed.', false); } async function importBackupAndLink() { const password = backupPasswordEl.value.trim(); if (!password) { throw new Error('Backup password is required'); } if (!backupFileEl.files || backupFileEl.files.length === 0) { throw new Error('Backup JSON file is required'); } const file = backupFileEl.files[0]; const text = await file.text(); let parsedBackup; try { parsedBackup = JSON.parse(text); } catch (error) { throw new Error('Backup file is not valid JSON'); } if (!parsedBackup || typeof parsedBackup !== 'object') { throw new Error('Backup file must contain a JSON object'); } const backupData = extractBackupObject(parsedBackup); if (!backupData) { throw new Error('Backup file does not contain a valid backup object'); } setFeedback('Importing backup and linking account...', false); setSuccess(''); const body = new URLSearchParams({ backupJson: JSON.stringify(backupData), password, }); const payload = await requestJson(userImportBackupLinkUrl, { method: 'POST', headers, body, }); assertBrokerOk(payload, 'Import/link failed'); resultEl.textContent = JSON.stringify(payload, null, 2); backupFileEl.value = ''; backupPasswordEl.value = ''; await refreshMappings(); setFeedback('Backup imported and linked.', false); setSuccess('Account linked. You can now authenticate to Nextcloud using your Qortal account.'); } async function unlinkMapping(qortalAddress) { if (!qortalAddress) { throw new Error('qortalAddress is required'); } const body = new URLSearchParams({ qortalAddress: qortalAddress, }); const payload = await requestJson(userUnlinkMappingUrl, { method: 'POST', headers, body, }); assertBrokerOk(payload, 'Unlink failed'); resultEl.textContent = JSON.stringify(payload, null, 2); await refreshMappings(); } async function revokeApprovalRule(rule) { const walletId = rule && typeof rule.walletId === 'string' ? rule.walletId.trim() : ''; const scope = rule && typeof rule.scope === 'string' ? rule.scope.trim() : ''; const app = rule && typeof rule.app === 'string' ? rule.app.trim() : ''; const requestType = rule && typeof rule.requestType === 'string' ? rule.requestType.trim() : ''; if (!walletId || !scope) { throw new Error('Invalid rule payload'); } const body = new URLSearchParams({ walletId, scope, app, requestType, }); const payload = await requestJson(userQappsRuleRevokeUrl, { method: 'POST', headers, body, }); assertBrokerOk(payload, 'Failed to revoke rule'); await refreshApprovalRules(); } async function saveApprovalDefaults() { const unlockWindowMinutes = normalizeApprovalTempMinutes(defaultApprovalModeEl.value); defaultApprovalTempMinutesEl.value = String(unlockWindowMinutes); const body = new URLSearchParams({ approvalMode: 'once', approvalTempMinutes: String(unlockWindowMinutes), talkBridgeAutoApproveSendChatMessage: defaultTalkBridgeAutoApproveSendChatMessageEl.checked ? '1' : '', highRiskConfirmationEnabled: highRiskConfirmationEnabledEl.checked ? '1' : '0', }); const payload = await requestJson(userQappsPrefsUrl, { method: 'POST', headers, body, }); assertBrokerOk(payload, 'Failed to save unlock window'); resultEl.textContent = JSON.stringify(payload, null, 2); const saved = payload && payload.data ? payload.data : payload; root.dataset.userQappsApprovalMode = 'once'; root.dataset.userQappsApprovalTempMinutes = String(normalizeApprovalTempMinutes(saved && saved.approvalTempMinutes ? saved.approvalTempMinutes : '120')); defaultApprovalModeEl.value = root.dataset.userQappsApprovalTempMinutes; defaultApprovalTempMinutesEl.value = root.dataset.userQappsApprovalTempMinutes; root.dataset.userQappsApprovalUnlock10Min = '0'; root.dataset.userQappsUnlockSession20Min = '0'; root.dataset.userTalkBridgeAutoApproveSendChatMessage = saved && saved.talkBridgeAutoApproveSendChatMessage ? '1' : '0'; root.dataset.userQappsHighRiskConfirmationEnabled = !saved || saved.highRiskConfirmationEnabled !== false ? '1' : '0'; applyApprovalDefaultsFromDataset(); setApprovalDefaultsResult('Unlock window saved.', false); } importButton.addEventListener('click', async function () { try { await importAndLink(); } catch (error) { setFeedback(error.message || 'Import/link failed', true); } }); refreshMappingsButton.addEventListener('click', async function () { try { await refreshMappings(); setFeedback('Linked accounts refreshed.', false); } catch (error) { setFeedback(error.message || 'Failed to refresh linked accounts', true); } }); importBackupButton.addEventListener('click', async function () { try { await importBackupAndLink(); } catch (error) { setFeedback(error.message || 'Backup import/link failed', true); } }); createWalletButton.addEventListener('click', async function () { try { await createWallet(); } catch (error) { setFeedback(error.message || 'Account creation failed', true); } }); backupWalletButton.addEventListener('click', async function () { try { await backupWallet(); } catch (error) { setFeedback(error.message || 'Backup failed', true); } }); backupWalletSelectEl.addEventListener('change', function () { preferredBackupWalletId = String(backupWalletSelectEl.value || '').trim(); }); backupReencryptToggleEl.addEventListener('change', function () { syncBackupReencryptFields(); }); recoverySaveButton.addEventListener('click', async function () { try { await saveRecoverySettings(); } catch (error) { setRecoveryResult(error.message || 'Failed to save recovery settings', true); } }); recoveryDisableButton.addEventListener('click', async function () { try { await disableRecoverySettings(); } catch (error) { setRecoveryResult(error.message || 'Failed to disable recovery settings', true); } }); recoveryRevealButton.addEventListener('click', async function () { try { await revealRecoveryPassword(); } catch (error) { setRecoveryResult(error.message || 'Failed to reveal backup password', true); } }); recoveryCompleteEmergencyButton.addEventListener('click', async function () { try { await completeEmergencyRecovery(); } catch (error) { setRecoveryResult(error.message || 'Failed to complete emergency recovery', true); } }); saveApprovalDefaultsButton.addEventListener('click', async function () { try { await saveApprovalDefaults(); } catch (error) { setApprovalDefaultsResult(error.message || 'Failed to save unlock window', true); } }); resetApprovalDefaultsButton.addEventListener('click', function () { defaultApprovalModeEl.value = '120'; defaultApprovalTempMinutesEl.value = '120'; defaultApprovalUnlock10MinEl.checked = false; defaultUnlockSession20MinEl.checked = false; defaultTalkBridgeAutoApproveSendChatMessageEl.checked = false; setApprovalDefaultsResult('Default unlock window restored in the form. Click Save Unlock Window to apply.', false); }); defaultApprovalModeEl.addEventListener('change', function () { defaultApprovalTempMinutesEl.value = String(normalizeApprovalTempMinutes(defaultApprovalModeEl.value)); }); refreshApprovalRulesButton.addEventListener('click', async function () { try { await refreshApprovalRules(); setApprovalRulesResult('Approval rules refreshed.', false); } catch (error) { setApprovalRulesResult(error.message || 'Failed to refresh approval rules', true); } }); [createPasswordEl, createKdfEl].forEach(function (el) { el.addEventListener('keydown', function (event) { if (event.key === 'Enter') { event.preventDefault(); } }); }); applyApprovalDefaultsFromDataset(); applyModeCopy(); syncBackupReencryptFields(); refreshMappings().catch(function (error) { setFeedback(error.message || 'Initial linked account load failed', true); }); loadRecoveryStatus().catch(function (error) { setRecoveryResult(error.message || 'Failed to load recovery status', true); }); if (!isPoweredMode) { refreshApprovalRules().catch(function (error) { setApprovalRulesResult(error.message || 'Initial approval rules load failed', true); }); } return true; } if (!init()) { const observer = new MutationObserver(function () { if (init()) { observer.disconnect(); } }); if (document.body) { observer.observe(document.body, { childList: true, subtree: true }); } } })();