(function () { const root = document.getElementById('qortal-qapps-root'); if (!root) { return; } let qapps = []; try { const parsed = JSON.parse(root.dataset.qapps || '[]'); if (Array.isArray(parsed)) { qapps = parsed; } } catch (error) { qapps = []; } const enabled = root.dataset.qappsEnabled === '1'; const browserEnabled = root.dataset.qappsBrowserEnabled === '1'; const browserAddress = root.dataset.qappsBrowserAddress || ''; const urlParams = new URLSearchParams(window.location.search); const debugFromQuery = urlParams.get('debug') === '1' || urlParams.get('qapps_debug') === '1'; const debugFromStorage = window.localStorage && window.localStorage.getItem('qortal_qapps_debug') === '1'; const debugEnabled = root.dataset.qappsDebugEnabled === '1' || debugFromQuery || debugFromStorage; const nodeUrl = root.dataset.qortalNodeUrl || ''; const gatewayUrl = root.dataset.qortalGatewayUrl || ''; const gatewayProxyTemplate = root.dataset.gatewayProxyUrl || ''; const viewerRendererConfigured = Boolean(nodeUrl || gatewayUrl); const qappsRequestUrl = root.dataset.qappsRequestUrl || ''; const qappsApproveUrl = root.dataset.qappsApproveUrl || ''; const qappsPermissionsUrl = root.dataset.qappsPermissionsUrl || ''; const qappsUnlockUrl = root.dataset.qappsUnlockUrl || ''; const qappsUnlockStatusUrl = root.dataset.qappsUnlockStatusUrl || ''; const defaultApprovalMode = (function () { const rawMode = String(root.dataset.qappsDefaultApprovalMode || '').trim(); if (rawMode === 'session') { return 'type_minutes'; } if (rawMode === 'scope') { return 'type_always'; } if (rawMode === 'app') { return 'app_always'; } if (rawMode === 'type_minutes' || rawMode === 'type_always' || rawMode === 'app_always') { return rawMode; } return 'once'; })(); const defaultApprovalTempMinutes = (function () { const parsed = Number(String(root.dataset.qappsDefaultApprovalTempMinutes || '10').trim()); if (!Number.isFinite(parsed)) { return 10; } return Math.max(1, Math.min(Math.floor(parsed), 1440)); })(); const defaultApprovalUnlock10Min = root.dataset.qappsDefaultApprovalUnlock10Min === '1'; const defaultUnlockSession20Min = root.dataset.qappsDefaultUnlockSession20Min === '1'; const userMappingsUrl = root.dataset.userMappingsUrl || ''; const nextcloudPublicUrl = root.dataset.nextcloudPublicUrl || ''; const settingsPath = root.dataset.settingsPath || '/settings/user/qortal_integration'; const emptyCard = document.getElementById('qortal-qapps-empty'); const browserCard = document.getElementById('qortal-qapps-browser'); const browserAddressEl = document.getElementById('qortal-qapps-browser-address'); const browserButton = document.getElementById('qortal-qapps-open-browser'); const listCard = document.getElementById('qortal-qapps-list'); const listBody = document.getElementById('qortal-qapps-body'); const warningCard = document.getElementById('qortal-qapps-warning'); const settingsLink = document.getElementById('qortal-qapps-settings-link'); const viewerCard = document.getElementById('qortal-qapps-viewer'); const viewerAddress = document.getElementById('qortal-qapps-viewer-address'); const viewerAddressInputWrap = root.querySelector('.qortal-viewer-address-input'); const viewerAddressInput = document.getElementById('qortal-qapps-viewer-address-input'); const viewerAddressGo = document.getElementById('qortal-qapps-viewer-address-go'); const viewerLoadingProgress = document.getElementById('qortal-qapps-loading-progress'); const viewerLoadingOverlay = document.getElementById('qortal-qapps-loading-overlay'); const viewerLoadingStatus = document.getElementById('qortal-qapps-loading-status'); const viewerLoadingMeta = document.getElementById('qortal-qapps-loading-meta'); const viewerLoadingProgressBar = document.getElementById('qortal-qapps-loading-progress-bar'); const viewerFrame = document.getElementById('qortal-qapps-frame'); const viewerFrameWrap = document.getElementById('qortal-qapps-frame-wrap'); const viewerClose = document.getElementById('qortal-qapps-close'); const viewerOpenExternal = document.getElementById('qortal-qapps-open-external'); const viewerBack = document.getElementById('qortal-qapps-back'); const viewerReload = document.getElementById('qortal-qapps-reload'); const mobileMenuToggle = document.getElementById('qortal-qapps-mobile-menu'); const viewerMenuToggle = document.getElementById('qortal-qapps-menu'); const pageHeader = root.querySelector('.qortal-page-header'); const debugCard = document.getElementById('qortal-qapps-debug'); const debugBody = document.getElementById('qortal-qapps-debug-body'); const debugCopy = document.getElementById('qortal-qapps-debug-copy'); const debugClear = document.getElementById('qortal-qapps-debug-clear'); const debugFloat = document.getElementById('qortal-qapps-debug-float'); const debugMinimize = document.getElementById('qortal-qapps-debug-minimize'); const approvalModal = document.getElementById('qortal-qapps-approval'); const approvalAppEl = document.getElementById('qortal-approval-app'); const approvalActionEl = document.getElementById('qortal-approval-action'); const approvalTempMinutesEl = document.getElementById('qortal-approval-temp-minutes'); const approvalPassword = document.getElementById('qortal-approval-password'); const approvalTtl = document.getElementById('qortal-approval-ttl'); const approvalPolicyStatusEl = document.getElementById('qortal-approval-policy-status'); const approvalWalletStatusEl = document.getElementById('qortal-approval-wallet-status'); const approvalUnlockFieldsEl = document.getElementById('qortal-approval-unlock-fields'); const approvalConfirm = document.getElementById('qortal-approval-confirm'); const approvalCancel = document.getElementById('qortal-approval-cancel'); const approvalError = document.getElementById('qortal-approval-error'); const unlockModal = document.getElementById('qortal-qapps-unlock'); const unlockAppEl = document.getElementById('qortal-unlock-app'); const unlockActionEl = document.getElementById('qortal-unlock-action'); const unlockPassword = document.getElementById('qortal-unlock-password'); const unlockSession = document.getElementById('qortal-unlock-session'); const unlockConfirm = document.getElementById('qortal-unlock-confirm'); const unlockCancel = document.getElementById('qortal-unlock-cancel'); const unlockError = document.getElementById('qortal-unlock-error'); const nameRequiredModal = document.getElementById('qortal-qapps-name-required'); const nameRequiredDashboardLink = document.getElementById('qortal-qapps-name-required-dashboard'); const nameRequiredContinue = document.getElementById('qortal-qapps-name-required-continue'); const nameRequiredCancel = document.getElementById('qortal-qapps-name-required-cancel'); const debugEntries = []; const debugLimit = 200; const qdnLoadingRetryDelayMs = 4000; const qdnLoadingReloadFallbackDelayMs = 7000; const qdnLoadingRetryMaxAttempts = 240; const qdnLoadingMarkers = [ 'files are being retrieved from the qortal data network', 'this page will refresh automatically when the content becomes available', 'initializing', 'initialising' ]; const qdnStatusTitles = { NOT_STARTED: 'Preparing resource...', PUBLISHED: 'Published, download not started', DOWNLOADING: 'Downloading from QDN...', DOWNLOADED: 'Downloaded, preparing content...', BUILDING: 'Building content...', MISSING_DATA: 'Missing data, waiting for peers...', BUILD_FAILED: 'Build failed. Retrying...', NOT_PUBLISHED: 'Resource not found', UNSUPPORTED: 'Unsupported request', BLOCKED: 'Content is blocked', }; const qdnStatusNumericMap = { 1: 'PUBLISHED', 2: 'NOT_PUBLISHED', 3: 'DOWNLOADING', 4: 'DOWNLOADED', 5: 'BUILDING', 6: 'READY', 7: 'MISSING_DATA', 8: 'BUILD_FAILED', 9: 'UNSUPPORTED', 10: 'BLOCKED', }; const mobileMenuClass = 'qortal-mobile-menu-open'; const viewerOpenClass = 'qortal-viewer-open'; const approvalScopeAliases = { PUBLISH_MULTIPLE_QDN_RESOURCES: 'PUBLISH_QDN_RESOURCE', }; let currentWalletId = ''; let currentWalletAddress = ''; let currentQappAddress = ''; let currentViewerAddress = ''; let approvalContext = null; let activeApprovalPolicyMode = defaultApprovalMode; const walletLockState = { state: 'unknown', expiresAt: null, }; const temporaryApprovalRules = []; let qdnLoadingRetryTimer = null; let qdnLoadingRetryAttempts = 0; let qdnLoadingStatusFailures = 0; let qdnLoadingLastSnapshot = ''; let currentQdnResourceTarget = null; const nameLookupCacheTtlMs = 60 * 1000; let nameLookupCache = { address: '', hasName: null, checkedAt: 0, }; function clearQdnLoadingProgress() { if (viewerLoadingProgress) { viewerLoadingProgress.textContent = ''; viewerLoadingProgress.classList.add('qortal-hidden'); } if (viewerLoadingStatus) { viewerLoadingStatus.textContent = ''; } if (viewerLoadingMeta) { viewerLoadingMeta.textContent = ''; } if (viewerLoadingProgressBar) { viewerLoadingProgressBar.style.width = '0%'; } if (viewerLoadingOverlay) { viewerLoadingOverlay.classList.add('qortal-hidden'); } } function coerceFiniteNumber(value) { if (typeof value === 'number' && Number.isFinite(value)) { return value; } if (typeof value === 'string' && value.trim() !== '') { const parsed = Number(value); if (Number.isFinite(parsed)) { return parsed; } } return null; } function getQdnStatusLabel(status, description) { if (typeof description === 'string' && description.trim() !== '') { return description.trim(); } const normalizedStatus = String(status || '').trim().toUpperCase(); if (normalizedStatus && qdnStatusTitles[normalizedStatus]) { return qdnStatusTitles[normalizedStatus]; } return 'Loading from QDN...'; } function updateQdnLoadingProgress(status, percentLoaded, localChunkCount, totalChunkCount, description) { const normalizedStatus = String(status || '').toUpperCase(); if (normalizedStatus === 'READY') { clearQdnLoadingProgress(); return; } const boundedPercent = coerceFiniteNumber(percentLoaded); const bounded = boundedPercent !== null ? Math.max(0, Math.min(100, boundedPercent)) : null; if (!viewerLoadingProgress) { return; } let toolbarLabel = 'Loading...'; if (bounded !== null) { toolbarLabel = 'Loading ' + bounded.toFixed(1) + '%'; } if ( localChunkCount !== null && localChunkCount !== undefined && Number.isFinite(localChunkCount) && String(toolbarLabel).indexOf('%') === -1 ) { toolbarLabel += ' (' + localChunkCount + ' chunks)'; } viewerLoadingProgress.textContent = toolbarLabel; viewerLoadingProgress.classList.remove('qortal-hidden'); if (viewerLoadingOverlay) { viewerLoadingOverlay.classList.remove('qortal-hidden'); } if (viewerLoadingStatus) { const statusLabel = getQdnStatusLabel(normalizedStatus, description); viewerLoadingStatus.textContent = bounded !== null ? statusLabel + ' ' + bounded.toFixed(1) + '%' : statusLabel; } if (viewerLoadingProgressBar) { viewerLoadingProgressBar.style.width = (bounded !== null ? bounded : 0) + '%'; } if (viewerLoadingMeta) { const local = coerceFiniteNumber(localChunkCount); const total = coerceFiniteNumber(totalChunkCount); if (local !== null && total !== null && total > 0) { viewerLoadingMeta.textContent = 'Chunks ' + local + ' / ' + total; } else if (local !== null) { viewerLoadingMeta.textContent = local + ' chunks downloaded'; } else { viewerLoadingMeta.textContent = ''; } } } function clearQdnLoadingRetry(reason) { if (qdnLoadingRetryTimer) { window.clearTimeout(qdnLoadingRetryTimer); qdnLoadingRetryTimer = null; logDebug('info', 'QDN loading auto-retry stopped', { reason: reason || 'cleared', attempts: qdnLoadingRetryAttempts, }); } qdnLoadingStatusFailures = 0; qdnLoadingLastSnapshot = ''; clearQdnLoadingProgress(); } function safeDecodeURIComponent(value) { if (typeof value !== 'string') { return ''; } try { return decodeURIComponent(value); } catch (_error) { return value; } } function stripQueryAndHash(value) { return String(value || '').split('#')[0].split('?')[0]; } function parseQdnResourceTarget(address) { const normalized = normalizeBrowserAddress(address || ''); if (!normalized || !normalized.toLowerCase().startsWith('qortal://')) { return null; } const rawPath = stripQueryAndHash(normalized.slice('qortal://'.length)).replace(/^\/+/, ''); if (!rawPath) { return null; } const segments = rawPath .split('/') .filter(function (segment) { return Boolean(segment); }) .map(safeDecodeURIComponent); if (segments.length < 2) { return null; } const service = String(segments[0] || '').trim().toUpperCase(); const name = String(segments[1] || '').trim(); if (!service || !name) { return null; } const identifierCandidate = segments.length > 2 ? String(segments[2] || '').trim() : ''; return { service, name, identifierCandidate, }; } function buildQdnStatusUrls(target) { if (!target || !target.service || !target.name) { return []; } const encodeSegmentsLocally = !gatewayProxyTemplate; const paths = []; const urls = []; const pushPath = function (segments) { const encoded = segments .filter(function (segment) { return typeof segment === 'string' && segment.trim() !== ''; }) .map(function (segment) { const value = segment.trim(); return encodeSegmentsLocally ? encodeURIComponent(value) : value; }) .join('/'); if (!encoded || paths.includes(encoded)) { return; } paths.push(encoded); }; const pushUrl = function (baseUrl, withBuildParam) { if (!baseUrl) { return; } const built = withBuildParam ? baseUrl + (baseUrl.indexOf('?') === -1 ? '?' : '&') + 'build=true' : baseUrl; if (!urls.includes(built)) { urls.push(built); } }; const base = ['arbitrary', 'resource', 'status', target.service, target.name]; if (target.identifierCandidate) { pushPath(base.concat([target.identifierCandidate])); } pushPath(base); paths.forEach(function (path) { const baseUrl = openGatewayPath(path); // Prefer build=true (progress/status from loading flow), but fall back to plain status. pushUrl(baseUrl, true); pushUrl(baseUrl, false); }); return urls.filter(function (url) { return Boolean(url); }); } function isLikelyStatus404(result) { if (!result) { return false; } if (result.status !== 404) { return false; } const body = result.json; if (!body || typeof body !== 'object') { return true; } const message = (typeof body.message === 'string' ? body.message : '') || (typeof body.error === 'string' ? body.error : ''); return message.toLowerCase().includes('not found') || message === ''; } async function probeQdnResourceProperties(target) { if (!target || !target.service || !target.name) { return { ok: false }; } const identifier = target.identifierCandidate || 'default'; const path = `arbitrary/resource/properties/${target.service}/${target.name}/${identifier}`; const baseUrl = openGatewayPath(path); if (!baseUrl) { return { ok: false }; } try { const result = await fetchJsonWithTimeout(baseUrl + (baseUrl.indexOf('?') === -1 ? '?' : '&') + 'build=true', 4500); if (!result.ok) { return { ok: false }; } // properties endpoint is mostly used to trigger/build; return generic downloading state. return { ok: true, status: 'BUILDING', description: 'Initializing and building content...', percentLoaded: null, localChunkCount: null, totalChunkCount: null, url: baseUrl, }; } catch (_error) { return { ok: false }; } } async function probeQdnResourceStatus(target) { const urls = buildQdnStatusUrls(target); let sawStatus404 = false; for (let i = 0; i < urls.length; i += 1) { const url = urls[i]; try { const result = await fetchJsonWithTimeout(url, 4500); if (!result.ok || !result.json) { if (isLikelyStatus404(result)) { sawStatus404 = true; } continue; } const parsed = parseQdnStatusPayload(result.json); if (!parsed) { continue; } return { ok: true, status: String(parsed.status || '').toUpperCase(), description: parsed.description || '', percentLoaded: coerceFiniteNumber(parsed.percentLoaded), localChunkCount: coerceFiniteNumber(parsed.localChunkCount), totalChunkCount: coerceFiniteNumber(parsed.totalChunkCount), url, }; } catch (_error) { // Try the next candidate URL. } } if (sawStatus404) { logDebug('warn', 'QDN status endpoint returned 404 variants; falling back to properties probe', { service: target.service, name: target.name, identifier: target.identifierCandidate || '', }); const propertiesResult = await probeQdnResourceProperties(target); if (propertiesResult.ok) { return propertiesResult; } } return { ok: false }; } function parseQdnStatusCandidate(candidate) { if (!candidate || typeof candidate !== 'object') { return null; } const nestedStatus = candidate.status && typeof candidate.status === 'object' ? candidate.status : null; let statusCode = ''; const rawCandidates = [ candidate.id, candidate.status, nestedStatus ? nestedStatus.id : null, nestedStatus ? nestedStatus.status : null, nestedStatus ? nestedStatus.name : null, ]; for (let i = 0; i < rawCandidates.length; i += 1) { const value = rawCandidates[i]; if (typeof value === 'string' && value.trim() !== '') { statusCode = value.trim().toUpperCase(); break; } } if (!statusCode) { const numericValue = coerceFiniteNumber(candidate.status) || (nestedStatus ? coerceFiniteNumber(nestedStatus.value) : null); if (numericValue !== null && qdnStatusNumericMap[Math.floor(numericValue)]) { statusCode = qdnStatusNumericMap[Math.floor(numericValue)]; } } if (!statusCode) { return null; } let localChunkCount = coerceFiniteNumber(candidate.localChunkCount); if (localChunkCount === null && nestedStatus) { localChunkCount = coerceFiniteNumber(nestedStatus.localChunkCount); } let totalChunkCount = coerceFiniteNumber(candidate.totalChunkCount); if (totalChunkCount === null && nestedStatus) { totalChunkCount = coerceFiniteNumber(nestedStatus.totalChunkCount); } let percentLoaded = coerceFiniteNumber(candidate.percentLoaded); if (percentLoaded === null && nestedStatus) { percentLoaded = coerceFiniteNumber(nestedStatus.percentLoaded); } if (percentLoaded === null && localChunkCount !== null && totalChunkCount !== null && totalChunkCount > 0) { percentLoaded = (localChunkCount / totalChunkCount) * 100; } return { status: statusCode, description: (typeof candidate.description === 'string' && candidate.description.trim() !== '' ? candidate.description.trim() : '') || (nestedStatus && typeof nestedStatus.description === 'string' ? nestedStatus.description.trim() : '') || (typeof candidate.title === 'string' ? candidate.title.trim() : ''), percentLoaded, localChunkCount, totalChunkCount, }; } function parseQdnStatusPayload(payload) { const visited = new Set(); const queue = [payload]; let depth = 0; while (queue.length > 0 && depth < 16) { const candidate = queue.shift(); depth += 1; if (!candidate || typeof candidate !== 'object') { continue; } if (visited.has(candidate)) { continue; } visited.add(candidate); const parsed = parseQdnStatusCandidate(candidate); if (parsed) { return parsed; } if (candidate.data && typeof candidate.data === 'object') { queue.push(candidate.data); } if (candidate.result && typeof candidate.result === 'object') { queue.push(candidate.result); } if (candidate.status && typeof candidate.status === 'object') { queue.push(candidate.status); } } return null; } async function fetchJsonWithTimeout(url, timeoutMs) { const canAbort = typeof window.AbortController === 'function'; const controller = canAbort ? new window.AbortController() : null; let timeoutId = null; if (controller && timeoutMs > 0) { timeoutId = window.setTimeout(function () { controller.abort(); }, timeoutMs); } try { const response = await fetch(url, { method: 'GET', signal: controller ? controller.signal : undefined, }); const rawText = await response.text(); let json = null; if (rawText) { try { json = JSON.parse(rawText); } catch (_error) { json = null; } } return { ok: response.ok, status: response.status, json, }; } finally { if (timeoutId) { window.clearTimeout(timeoutId); } } } function detectQdnLoadingInterstitial() { if (!viewerFrame) { return { inspectable: false, detected: false }; } let doc = null; try { doc = viewerFrame.contentDocument; } catch (_error) { return { inspectable: false, detected: false }; } if (!doc || !doc.body) { return { inspectable: true, detected: false }; } const bodyText = (doc.body.innerText || doc.body.textContent || '').toLowerCase(); const titleText = String(doc.title || '').toLowerCase(); const combinedText = bodyText + '\n' + titleText; const detected = qdnLoadingMarkers.some(function (marker) { return combinedText.includes(marker); }); return { inspectable: true, detected }; } function reloadViewerFrame() { if (!viewerFrame) { return false; } try { if (viewerFrame.contentWindow) { viewerFrame.contentWindow.location.reload(); return true; } } catch (_error) { // fallback below } try { if (viewerFrame.src) { viewerFrame.src = viewerFrame.src; return true; } } catch (_error) { return false; } return false; } async function runQdnLoadingRetryTick() { if (qdnLoadingRetryAttempts >= qdnLoadingRetryMaxAttempts) { logDebug('warn', 'QDN loading auto-retry max attempts reached', { attempts: qdnLoadingRetryAttempts, }); return; } if (!viewerCard || viewerCard.classList.contains('qortal-hidden')) { clearQdnLoadingRetry('viewer_hidden'); return; } const detection = detectQdnLoadingInterstitial(); if (!detection.inspectable) { clearQdnLoadingRetry('cross_origin_or_uninspectable'); return; } let prefetchedStatusResult = null; if (!detection.detected && currentQdnResourceTarget) { prefetchedStatusResult = await probeQdnResourceStatus(currentQdnResourceTarget); if (prefetchedStatusResult.ok) { updateQdnLoadingProgress( prefetchedStatusResult.status, prefetchedStatusResult.percentLoaded, prefetchedStatusResult.localChunkCount, prefetchedStatusResult.totalChunkCount, prefetchedStatusResult.description ); if (prefetchedStatusResult.status === 'READY') { const shouldForceRefresh = qdnLoadingRetryAttempts > 0 || (viewerLoadingOverlay && !viewerLoadingOverlay.classList.contains('qortal-hidden')); if (shouldForceRefresh) { logDebug('info', 'QDN status ready after loading flow, forcing viewer refresh', { attempt: qdnLoadingRetryAttempts, }); clearQdnLoadingRetry('status_ready_refresh'); qdnLoadingRetryAttempts = 0; reloadViewerFrame(); return; } clearQdnLoadingRetry('content_loaded'); qdnLoadingRetryAttempts = 0; return; } } else { clearQdnLoadingRetry('content_loaded'); qdnLoadingRetryAttempts = 0; return; } } if (!detection.detected && !prefetchedStatusResult) { clearQdnLoadingRetry('content_loaded'); qdnLoadingRetryAttempts = 0; return; } updateQdnLoadingProgress('DOWNLOADING', null, null, null, ''); qdnLoadingRetryAttempts += 1; let shouldReload = true; let delayMs = qdnLoadingRetryDelayMs; if (currentQdnResourceTarget) { const statusResult = prefetchedStatusResult || await probeQdnResourceStatus(currentQdnResourceTarget); if (statusResult.ok) { qdnLoadingStatusFailures = 0; const statusSnapshot = statusResult.status + ':' + (statusResult.percentLoaded !== null ? statusResult.percentLoaded : '') + ':' + (statusResult.localChunkCount !== null ? statusResult.localChunkCount : ''); if ( statusSnapshot !== qdnLoadingLastSnapshot || qdnLoadingRetryAttempts % 8 === 0 ) { logDebug('info', 'QDN loading status', { attempt: qdnLoadingRetryAttempts, status: statusResult.status, percentLoaded: statusResult.percentLoaded, localChunkCount: statusResult.localChunkCount, totalChunkCount: statusResult.totalChunkCount, }); qdnLoadingLastSnapshot = statusSnapshot; } updateQdnLoadingProgress( statusResult.status, statusResult.percentLoaded, statusResult.localChunkCount, statusResult.totalChunkCount, statusResult.description ); if (statusResult.status === 'READY') { logDebug('info', 'QDN resource ready, refreshing viewer', { attempt: qdnLoadingRetryAttempts, }); reloadViewerFrame(); return; } shouldReload = false; } else { qdnLoadingStatusFailures += 1; delayMs = qdnLoadingReloadFallbackDelayMs; if (qdnLoadingStatusFailures <= 3) { shouldReload = false; logDebug('warn', 'QDN status probe failed, waiting before fallback reload', { attempt: qdnLoadingRetryAttempts, failures: qdnLoadingStatusFailures, }); } } } if (shouldReload) { logDebug('info', 'QDN loading detected, retrying load', { attempt: qdnLoadingRetryAttempts, delayMs, mode: currentQdnResourceTarget ? 'fallback_reload' : 'reload', }); reloadViewerFrame(); } scheduleQdnLoadingRetry(delayMs); } function scheduleQdnLoadingRetry(delayOverrideMs) { if (qdnLoadingRetryTimer) { return; } if (qdnLoadingRetryAttempts >= qdnLoadingRetryMaxAttempts) { logDebug('warn', 'QDN loading auto-retry max attempts reached', { attempts: qdnLoadingRetryAttempts, }); return; } const nextDelay = typeof delayOverrideMs === 'number' && Number.isFinite(delayOverrideMs) ? Math.max(1000, Math.floor(delayOverrideMs)) : qdnLoadingRetryDelayMs; qdnLoadingRetryTimer = window.setTimeout(function () { qdnLoadingRetryTimer = null; runQdnLoadingRetryTick().catch(function (error) { logDebug('warn', 'QDN loading retry tick failed', { error: error && error.message ? error.message : String(error), }); scheduleQdnLoadingRetry(qdnLoadingReloadFallbackDelayMs); }); }, nextDelay); } function fallbackApprovalScope(requestType) { return approvalScopeAliases[requestType] || requestType || ''; } function normalizeApprovalScope(scope, requestType) { const candidate = typeof scope === 'string' ? scope.trim() : ''; if (!candidate) { return fallbackApprovalScope(requestType); } if (requestType && approvalScopeAliases[requestType] && candidate === requestType) { return approvalScopeAliases[requestType]; } return candidate; } function sanitizeDebugValue(value) { if (!value || typeof value !== 'object') { return value; } if (Array.isArray(value)) { return value.map(sanitizeDebugValue); } const clone = {}; for (const key in value) { if (!Object.prototype.hasOwnProperty.call(value, key)) { continue; } const lower = key.toLowerCase(); if ( lower.includes('seed') || lower.includes('password') || lower.includes('secret') || lower.includes('backup') ) { clone[key] = '[redacted]'; continue; } clone[key] = sanitizeDebugValue(value[key]); } return clone; } function logDebug(level, message, details) { if (!debugEnabled || !debugBody) { return; } const timestamp = new Date().toISOString(); const entry = { time: timestamp, level, message, details: sanitizeDebugValue(details), }; debugEntries.push(entry); if (debugEntries.length > debugLimit) { debugEntries.shift(); } const lines = debugEntries.map(function (item) { const suffix = item.details !== undefined ? ' ' + JSON.stringify(item.details) : ''; const level = item.level.toUpperCase(); return '[' + item.time + '] ' + level + ': ' + item.message + suffix; }); debugBody.textContent = lines.join('\n'); debugBody.scrollTop = debugBody.scrollHeight; } if (debugFromQuery && window.localStorage) { window.localStorage.setItem('qortal_qapps_debug', '1'); } function detectTheme() { const docEl = document.documentElement; const body = document.body; const dataTheme = (docEl && docEl.dataset ? docEl.dataset.theme : '') || (body && body.dataset ? body.dataset.theme : ''); if (dataTheme) { return dataTheme; } const classList = (docEl && docEl.classList ? docEl.classList : body && body.classList ? body.classList : null); if (classList) { if (classList.contains('theme-dark') || classList.contains('dark') || classList.contains('theme--dark') || classList.contains('nc-dark')) { return 'dark'; } if (classList.contains('theme-light') || classList.contains('light') || classList.contains('theme--light') || classList.contains('nc-light')) { return 'light'; } } if (window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches) { return 'dark'; } return 'light'; } function detectLanguage() { return (document.documentElement && document.documentElement.lang) || navigator.language || ''; } function appendThemeParams(url) { if (!url) { return url; } try { const u = new URL(url, window.location.origin); if (!u.searchParams.has('theme')) { u.searchParams.set('theme', detectTheme()); } const lang = detectLanguage(); if (lang && !u.searchParams.has('lang')) { u.searchParams.set('lang', lang); } return u.toString(); } catch (error) { return url; } } function encodeGatewayPath(path) { return path .split('/') .map(function (segment) { return encodeURIComponent(segment); }) .join('/'); } function buildProxyUrl(path) { if (!gatewayProxyTemplate) { return ''; } if (gatewayProxyTemplate.includes('__PATH__')) { return gatewayProxyTemplate.replace('__PATH__', encodeGatewayPath(path)); } return gatewayProxyTemplate + encodeURIComponent(path); } function isUpstreamRootPath(path) { const normalized = String(path || '').replace(/^\/+/, ''); return /^(render|arbitrary|names|addresses|transactions|blocks|crosschain|groups|chat|lists|assets|at|admin|peers|utils|apps|payments|polls|stats|bootstrap|developer|websockets)(\/|$)/i.test(normalized); } function toUpstreamProxyPath(path, preferRender) { const normalized = String(path || '').replace(/^\/+/, ''); if (!normalized) { return ''; } if (preferRender && nodeUrl && !isUpstreamRootPath(normalized)) { return 'render/' + normalized; } return normalized; } function resolveGatewayAddress(address, ensureTrailingSlash) { if (!address) { return ''; } let path = address.trim(); if (/^https?:\/\//i.test(path)) { return appendThemeParams(path); } if (path.toLowerCase().startsWith('qortal://')) { path = path.slice('qortal://'.length); } path = path.replace(/^\/+/, ''); if (ensureTrailingSlash && path && !path.endsWith('/')) { path += '/'; } const upstreamPath = toUpstreamProxyPath(path, true); if (gatewayProxyTemplate) { if (!viewerRendererConfigured) { return ''; } return appendThemeParams(buildProxyUrl(upstreamPath)); } const directBase = nodeUrl ? nodeUrl.replace(/\/$/, '') : gatewayUrl.replace(/\/$/, ''); if (!directBase) { return ''; } const base = directBase.replace(/\/$/, ''); return appendThemeParams(base + '/' + upstreamPath); } function normalizeBrowserAddress(address) { const raw = String(address || '').trim(); if (!raw) { return ''; } if (!raw.toLowerCase().startsWith('qortal://')) { return raw; } const afterScheme = raw.slice('qortal://'.length).replace(/^\/+/, ''); if (!afterScheme) { return raw; } // Browser convenience: qortal://Name -> qortal://WEBSITE/Name if (!afterScheme.includes('/')) { return 'qortal://WEBSITE/' + afterScheme; } return raw; } function deriveSafeReloadAddress(address) { const normalized = normalizeBrowserAddress(address || ''); if (!normalized || !normalized.toLowerCase().startsWith('qortal://')) { return normalized; } const rawPath = stripQueryAndHash(normalized.slice('qortal://'.length)).replace(/^\/+/, ''); if (!rawPath) { return normalized; } const segments = rawPath .split('/') .filter(function (segment) { return Boolean(segment); }) .map(safeDecodeURIComponent); if (segments.length < 2) { return normalized; } const service = String(segments[0] || '').trim().toUpperCase(); const name = String(segments[1] || '').trim(); if (!service || !name) { return normalized; } if (service === 'APP') { return 'qortal://APP/' + name; } return normalized; } function setViewerAddressDisplay(address) { const value = String(address || '').trim(); currentViewerAddress = value; if (viewerAddress) { viewerAddress.textContent = value; } if (viewerAddressInput) { viewerAddressInput.value = value; } } function parseQortalAddressFromViewerUrl(url) { if (!url) { return ''; } try { const parsed = new URL(url, window.location.origin); const gatewayMarker = '/apps/qortal_integration/gateway/'; const markerIndex = parsed.pathname.indexOf(gatewayMarker); if (markerIndex < 0) { return ''; } let upstreamPath = parsed.pathname .slice(markerIndex + gatewayMarker.length) .replace(/^\/+/, ''); if (!upstreamPath) { return ''; } if (upstreamPath.toLowerCase().startsWith('render/')) { upstreamPath = upstreamPath.slice('render/'.length); } const decodedPath = upstreamPath .split('/') .map(function (segment) { return safeDecodeURIComponent(segment); }) .join('/'); return normalizeBrowserAddress('qortal://' + decodedPath + parsed.search + parsed.hash); } catch (_error) { return ''; } } function getNavigationAddressFromPayload(payload) { if (!payload || typeof payload !== 'object') { return ''; } const candidates = [ payload.qortalLink, payload.address, payload.url, payload.href, payload.path, payload.value, ]; for (let i = 0; i < candidates.length; i += 1) { const candidate = candidates[i]; if (typeof candidate !== 'string' || candidate.trim() === '') { continue; } const trimmed = candidate.trim(); if (trimmed.toLowerCase().startsWith('qortal://')) { return normalizeBrowserAddress(trimmed); } if (trimmed.startsWith('/')) { return normalizeBrowserAddress('qortal://' + trimmed.replace(/^\/+/, '')); } if (/^[a-z][a-z0-9+.-]*:\/\//i.test(trimmed)) { const parsed = parseQortalAddressFromViewerUrl(trimmed); if (parsed) { return parsed; } } else { return normalizeBrowserAddress('qortal://' + trimmed.replace(/^\/+/, '')); } } return ''; } function syncViewerAddressFromFrame() { if (!viewerFrame || !viewerFrame.contentWindow) { return ''; } try { const href = viewerFrame.contentWindow.location.href; const parsed = parseQortalAddressFromViewerUrl(href); if (parsed) { setViewerAddressDisplay(parsed); return parsed; } } catch (_error) { // ignore cross-origin access failures } return ''; } function copyText(value) { const text = String(value || '').trim(); if (!text) { return Promise.resolve(false); } if (navigator.clipboard && navigator.clipboard.writeText) { return navigator.clipboard.writeText(text).then(function () { return true; }).catch(function () { return false; }); } return Promise.resolve(false); } function getDashboardRegisterUrl() { const base = (nextcloudPublicUrl || (window.location.origin + (typeof OC !== 'undefined' && OC.webroot ? OC.webroot : ''))) .replace(/\/$/, ''); return base + '/apps/qortal_integration/account?highlight=register-name'; } async function postBrokerQortalRequest(requestType, payload) { if (!qappsRequestUrl) { throw new Error('Q-Apps request endpoint is not configured'); } const headers = { 'Content-Type': 'application/json' }; if (typeof OC !== 'undefined' && OC.requestToken) { headers.requesttoken = OC.requestToken; } const response = await fetch(qappsRequestUrl, { method: 'POST', headers, body: JSON.stringify({ requestType, payload: payload || {}, }), }); const json = await response.json(); if (!response.ok || !json || json.ok === false || json.error) { throw new Error((json && json.error) || 'qortal_request_failed'); } return json.data !== undefined ? json.data : json; } function parsePrimaryNameResult(resultPayload) { if (typeof resultPayload === 'string') { return resultPayload.trim(); } if (Array.isArray(resultPayload)) { return resultPayload.length > 0 ? parsePrimaryNameResult(resultPayload[0]) : ''; } if (resultPayload && typeof resultPayload === 'object') { return String( resultPayload.name || resultPayload.primaryName || (resultPayload.data && resultPayload.data.name) || '' ).trim(); } return ''; } async function hasRegisteredName(address) { const normalizedAddress = String(address || '').trim(); if (!normalizedAddress) { return true; } const now = Date.now(); if ( nameLookupCache.address === normalizedAddress && typeof nameLookupCache.hasName === 'boolean' && (now - nameLookupCache.checkedAt) <= nameLookupCacheTtlMs ) { return nameLookupCache.hasName; } let hasName = false; try { const primaryName = parsePrimaryNameResult( await postBrokerQortalRequest('GET_PRIMARY_NAME', { address: normalizedAddress }) ); if (primaryName) { hasName = true; } else { const namesResult = await postBrokerQortalRequest('GET_ACCOUNT_NAMES', { address: normalizedAddress }); if (Array.isArray(namesResult)) { hasName = namesResult.some(function (entry) { if (typeof entry === 'string') { return entry.trim() !== ''; } if (entry && typeof entry === 'object') { return String(entry.name || '').trim() !== ''; } return false; }); } } } catch (error) { logDebug('warn', 'Registered-name check failed; allowing access', { address: normalizedAddress, error: error && error.message ? error.message : String(error), }); hasName = true; } nameLookupCache = { address: normalizedAddress, hasName, checkedAt: now, }; return hasName; } function showRegisteredNameRequiredModal() { return new Promise(function (resolve) { if (!nameRequiredModal || !nameRequiredDashboardLink || !nameRequiredContinue || !nameRequiredCancel) { resolve('continue'); return; } const dashboardUrl = getDashboardRegisterUrl(); nameRequiredDashboardLink.href = dashboardUrl; nameRequiredModal.classList.remove('qortal-hidden'); const cleanup = function () { nameRequiredDashboardLink.onclick = null; nameRequiredContinue.onclick = null; nameRequiredCancel.onclick = null; nameRequiredModal.classList.add('qortal-hidden'); }; nameRequiredDashboardLink.onclick = function (event) { if (event) { event.preventDefault(); } cleanup(); resolve('dashboard'); }; nameRequiredContinue.onclick = function () { cleanup(); resolve('continue'); }; nameRequiredCancel.onclick = function () { cleanup(); resolve('cancel'); }; }); } async function ensureRegisteredNameBeforeOpening() { let effectiveAddress = String(currentWalletAddress || '').trim(); if (!effectiveAddress && currentWalletId && userMappingsUrl) { await refreshUserMapping(); effectiveAddress = String(currentWalletAddress || '').trim(); } if (!effectiveAddress) { return true; } const hasName = await hasRegisteredName(effectiveAddress); if (hasName) { return true; } const decision = await showRegisteredNameRequiredModal(); if (decision === 'dashboard') { window.location.href = getDashboardRegisterUrl(); return false; } return decision === 'continue'; } function setMobileMenuOpen(open, shouldResize) { const isOpen = Boolean(open); root.classList.toggle(mobileMenuClass, isOpen); if (mobileMenuToggle) { mobileMenuToggle.setAttribute('aria-expanded', isOpen ? 'true' : 'false'); } if (viewerMenuToggle) { viewerMenuToggle.setAttribute('aria-expanded', isOpen ? 'true' : 'false'); } if (shouldResize) { resizeViewerFrame(); } } function setViewerOpen(open) { const isOpen = Boolean(open); root.classList.toggle(viewerOpenClass, isOpen); if (pageHeader) { pageHeader.style.display = isOpen ? 'none' : ''; } } async function openAddress(address, name) { if (!address || !viewerCard || !viewerFrame || !viewerAddress) { return; } const allowedToOpen = await ensureRegisteredNameBeforeOpening(); if (!allowedToOpen) { return; } const normalizedAddress = normalizeBrowserAddress(address); const resolved = resolveGatewayAddress(normalizedAddress, true); if (!resolved) { window.alert('Qortal node or gateway URL is not configured. Ask an admin to configure rendering.'); return; } currentQappAddress = normalizedAddress; currentQdnResourceTarget = parseQdnResourceTarget(normalizedAddress); logDebug('info', 'Opening Q-App', { address: normalizedAddress, resolved }); clearQdnLoadingRetry('open_address'); qdnLoadingRetryAttempts = 0; qdnLoadingStatusFailures = 0; qdnLoadingLastSnapshot = ''; updateQdnLoadingProgress('NOT_STARTED', 0, null, null, ''); setViewerAddressDisplay(normalizedAddress); if (viewerAddressInputWrap) { viewerAddressInputWrap.classList.remove('qortal-hidden'); } if (viewerAddress) { viewerAddress.classList.remove('qortal-hidden'); } viewerFrame.src = resolved; viewerCard.classList.remove('qortal-hidden'); setViewerOpen(true); setMobileMenuOpen(false, false); resizeViewerFrame(); if (debugCard && debugEnabled) { const docked = window.localStorage && window.localStorage.getItem('qortal_qapps_debug_dock') === '1'; debugCard.classList.toggle('qortal-debug-floating', !docked); if (!debugCard.classList.contains('qortal-debug-minimized')) { debugCard.classList.remove('qortal-hidden'); } } if (viewerOpenExternal) { viewerOpenExternal.textContent = 'Copy link'; viewerOpenExternal.onclick = async function () { const fromFrame = syncViewerAddressFromFrame(); const linkToCopy = fromFrame || currentViewerAddress || currentQappAddress || normalizedAddress; const copied = await copyText(linkToCopy); viewerOpenExternal.textContent = copied ? 'Copied' : 'Copy failed'; window.setTimeout(function () { viewerOpenExternal.textContent = 'Copy link'; }, 1200); }; } if (viewerClose) { viewerClose.onclick = function () { clearQdnLoadingRetry('viewer_closed'); qdnLoadingRetryAttempts = 0; currentQdnResourceTarget = null; setViewerOpen(false); setMobileMenuOpen(false, false); viewerFrame.src = 'about:blank'; viewerCard.classList.add('qortal-hidden'); }; } if (viewerBack) { viewerBack.onclick = function () { try { if (viewerFrame.contentWindow) { viewerFrame.contentWindow.history.back(); } } catch (_err) { // ignore cross-origin issues } }; } if (viewerReload) { viewerReload.onclick = function () { const preferredAddress = deriveSafeReloadAddress(currentViewerAddress || currentQappAddress || ''); if (preferredAddress && preferredAddress.toLowerCase().startsWith('qortal://')) { logDebug('info', 'Reload routed to safe reopen', { currentViewerAddress: currentViewerAddress || '', currentQappAddress: currentQappAddress || '', reloadAddress: preferredAddress, }); openAddress(preferredAddress, 'Qortal Browser'); return; } try { if (viewerFrame.contentWindow) { viewerFrame.contentWindow.location.reload(); } } catch (_err) { // ignore cross-origin issues } }; } } function resizeViewerFrame() { if (!viewerFrameWrap) { return; } const rect = viewerFrameWrap.getBoundingClientRect(); const rootRect = root.getBoundingClientRect(); const viewportHeight = (window.visualViewport && window.visualViewport.height) || window.innerHeight || document.documentElement.clientHeight || 0; const viewportWidth = (window.visualViewport && window.visualViewport.width) || window.innerWidth || document.documentElement.clientWidth || 0; const isMobile = viewportWidth && viewportWidth <= 780; if (!isMobile && root.classList.contains(mobileMenuClass)) { setMobileMenuOpen(false, false); } const padding = isMobile ? 8 : 16; let nextHeight = Math.floor(viewportHeight - rect.top - padding); if (root.classList.contains(viewerOpenClass) && Number.isFinite(rootRect.bottom)) { const containerPadding = isMobile ? 4 : 8; const containerBoundHeight = Math.floor(rootRect.bottom - rect.top - containerPadding); if (Number.isFinite(containerBoundHeight) && containerBoundHeight > 0) { nextHeight = Math.min(nextHeight, containerBoundHeight); } } if (isMobile) { const maxMobile = Math.floor(viewportHeight * 0.85); nextHeight = Math.min(nextHeight, maxMobile); } const minHeight = isMobile ? Math.max(160, Math.floor(viewportHeight * 0.45)) : 360; nextHeight = Math.max(minHeight, nextHeight); viewerFrameWrap.style.height = nextHeight + 'px'; } function renderList() { if (!listBody) { return; } listBody.innerHTML = ''; if (!enabled || qapps.length === 0) { listBody.innerHTML = 'No Q-Apps available.'; return; } qapps.forEach(function (entry) { const row = document.createElement('tr'); const nameCell = document.createElement('td'); nameCell.textContent = entry.name || ''; const addressCell = document.createElement('td'); const addressCode = document.createElement('code'); addressCode.textContent = entry.address || ''; addressCell.appendChild(addressCode); const descCell = document.createElement('td'); descCell.textContent = entry.description || ''; const actionCell = document.createElement('td'); const launchButton = document.createElement('button'); launchButton.className = 'button'; launchButton.textContent = 'Launch'; launchButton.disabled = !viewerRendererConfigured; if (!viewerRendererConfigured) { launchButton.title = 'Qortal node or gateway URL is not configured.'; } launchButton.addEventListener('click', function () { openAddress(entry.address || '', entry.name || ''); }); actionCell.appendChild(launchButton); row.appendChild(nameCell); row.appendChild(addressCell); row.appendChild(descCell); row.appendChild(actionCell); listBody.appendChild(row); }); } if (emptyCard) { emptyCard.style.display = enabled || browserEnabled ? 'none' : 'block'; } if (browserCard) { if (browserEnabled && browserAddress) { browserCard.style.display = 'block'; if (browserAddressEl) { browserAddressEl.textContent = browserAddress; } if (browserButton) { browserButton.addEventListener('click', function () { openAddress(browserAddress, 'Qortal Browser'); }); } } else { browserCard.style.display = 'none'; } } if (listCard) { listCard.style.display = enabled ? 'block' : 'none'; } if (settingsLink) { const base = (nextcloudPublicUrl || (window.location.origin + (typeof OC !== 'undefined' && OC.webroot ? OC.webroot : ''))) .replace(/\/$/, ''); settingsLink.href = base + settingsPath; } if (nameRequiredDashboardLink) { nameRequiredDashboardLink.href = getDashboardRegisterUrl(); } async function refreshUserMapping() { if (!warningCard || !userMappingsUrl) { return; } try { const headers = {}; if (typeof OC !== 'undefined' && OC.requestToken) { headers.requesttoken = OC.requestToken; } const response = await fetch(userMappingsUrl, { method: 'GET', headers }); const payload = await response.json(); let mappings = []; if (payload && Array.isArray(payload.mappings)) { mappings = payload.mappings; } else if (payload && payload.data && Array.isArray(payload.data.mappings)) { mappings = payload.data.mappings; } let selected = null; const hasWallet = mappings.some(function (entry) { if (!entry || typeof entry !== 'object') { return false; } if (!entry.walletId) { return false; } if (entry.status && entry.status !== 'linked') { return false; } if (!selected) { selected = entry; } return true; }); if (!selected) { selected = mappings.find(function (entry) { return entry && entry.walletId; }); } if (!selected) { selected = mappings.find(function (entry) { return entry && entry.qortalAddress; }); } currentWalletId = selected && selected.walletId ? String(selected.walletId) : ''; currentWalletAddress = selected && selected.qortalAddress ? String(selected.qortalAddress) : ''; if (nameLookupCache.address !== currentWalletAddress) { nameLookupCache = { address: '', hasName: null, checkedAt: 0, }; } if (currentWalletId) { await refreshWalletLockState(currentWalletId); } else { markWalletUnknown(); } warningCard.classList.toggle('qortal-hidden', hasWallet); } catch (error) { warningCard.classList.add('qortal-hidden'); markWalletUnknown(); } } function openFromQuery() { const params = new URLSearchParams(window.location.search); const raw = params.get('qapp') || params.get('app'); if (!raw) { return; } const target = decodeURIComponent(raw); const match = qapps.find(function (entry) { return entry && (entry.address === target || entry.name === target); }); if (match) { openAddress(match.address || '', match.name || ''); } } renderList(); Promise.resolve(refreshUserMapping()).finally(function () { openFromQuery(); }); if (debugCard) { debugCard.classList.toggle('qortal-hidden', !debugEnabled); } if (debugCard && debugEnabled) { const docked = window.localStorage && window.localStorage.getItem('qortal_qapps_debug_dock') === '1'; debugCard.classList.toggle('qortal-debug-floating', !docked); if (debugFloat) { debugFloat.textContent = docked ? 'Float Panel' : 'Dock Panel'; debugFloat.addEventListener('click', function () { const isFloating = debugCard.classList.contains('qortal-debug-floating'); debugCard.classList.toggle('qortal-debug-floating', !isFloating); const nowDocked = isFloating; if (window.localStorage) { window.localStorage.setItem('qortal_qapps_debug_dock', nowDocked ? '1' : '0'); } debugFloat.textContent = nowDocked ? 'Float Panel' : 'Dock Panel'; }); } if (debugMinimize) { debugMinimize.textContent = debugCard.classList.contains('qortal-debug-minimized') ? 'Expand' : 'Minimize'; debugMinimize.addEventListener('click', function () { const isMinimized = debugCard.classList.toggle('qortal-debug-minimized'); debugMinimize.textContent = isMinimized ? 'Expand' : 'Minimize'; }); } } if (debugCopy) { debugCopy.addEventListener('click', function () { if (!debugBody) return; const text = debugBody.textContent || ''; if (!text) return; if (navigator.clipboard && navigator.clipboard.writeText) { navigator.clipboard.writeText(text).catch(function () { /* ignore */ }); } }); } if (debugClear) { debugClear.addEventListener('click', function () { debugEntries.length = 0; if (debugBody) { debugBody.textContent = 'Debug panel enabled.'; } }); } window.addEventListener('resize', resizeViewerFrame); if (window.visualViewport) { window.visualViewport.addEventListener('resize', resizeViewerFrame); } function toggleMobileMenu() { setMobileMenuOpen(!root.classList.contains(mobileMenuClass), true); } setViewerOpen(Boolean(viewerCard && !viewerCard.classList.contains('qortal-hidden'))); setMobileMenuOpen(root.classList.contains(mobileMenuClass), false); if (mobileMenuToggle) { mobileMenuToggle.addEventListener('click', toggleMobileMenu); } if (viewerMenuToggle) { viewerMenuToggle.addEventListener('click', toggleMobileMenu); } if (viewerAddressGo && viewerAddressInput) { viewerAddressGo.addEventListener('click', function () { const next = viewerAddressInput.value.trim(); if (!next) { return; } openAddress(next, 'Qortal Browser'); }); viewerAddressInput.addEventListener('keydown', function (event) { if (event.key === 'Enter') { event.preventDefault(); viewerAddressGo.click(); } }); } if (viewerFrame) { viewerFrame.addEventListener('load', async function () { syncViewerAddressFromFrame(); if (!viewerCard || viewerCard.classList.contains('qortal-hidden')) { clearQdnLoadingRetry('viewer_hidden'); return; } const detection = detectQdnLoadingInterstitial(); if (!detection.inspectable) { clearQdnLoadingRetry('cross_origin_or_uninspectable'); return; } if (detection.detected) { updateQdnLoadingProgress('DOWNLOADING', null, null, null, ''); scheduleQdnLoadingRetry(); return; } if (currentQdnResourceTarget) { const statusResult = await probeQdnResourceStatus(currentQdnResourceTarget); if (statusResult.ok && statusResult.status !== 'READY') { updateQdnLoadingProgress( statusResult.status, statusResult.percentLoaded, statusResult.localChunkCount, statusResult.totalChunkCount, statusResult.description ); scheduleQdnLoadingRetry(); return; } if (statusResult.ok && statusResult.status === 'READY') { const shouldForceRefresh = qdnLoadingRetryAttempts > 0 || (viewerLoadingOverlay && !viewerLoadingOverlay.classList.contains('qortal-hidden')); if (shouldForceRefresh) { logDebug('info', 'QDN status ready on frame load, forcing viewer refresh', { attempt: qdnLoadingRetryAttempts, }); clearQdnLoadingRetry('status_ready_refresh'); qdnLoadingRetryAttempts = 0; reloadViewerFrame(); return; } } } clearQdnLoadingRetry('content_loaded'); qdnLoadingRetryAttempts = 0; }); } function originFromUrl(url) { try { return new URL(url).origin; } catch (error) { return ''; } } const allowedOrigins = new Set(); const gatewayOrigin = originFromUrl(gatewayUrl); if (gatewayOrigin) { allowedOrigins.add(gatewayOrigin); } allowedOrigins.add(window.location.origin); function buildBridgePayload(message) { if (!message || typeof message !== 'object') { return {}; } if (message.payload && typeof message.payload === 'object') { return message.payload; } const copy = Object.assign({}, message); delete copy.action; delete copy.requestedHandler; delete copy.payload; delete copy.requestId; delete copy.isExtension; return copy; } function normalizeBridgePayload(requestType, payload) { if (!payload || typeof payload !== 'object') { return {}; } const normalized = Object.assign({}, payload); const pickBase64Value = function (value) { if (!value || typeof value !== 'object') { return ''; } if (typeof value.base64 === 'string' && value.base64.trim() !== '') { return value.base64; } if (typeof value.data64 === 'string' && value.data64.trim() !== '') { return value.data64; } return ''; }; const listActions = new Set(['GET_LIST_ITEMS', 'ADD_LIST_ITEMS', 'DELETE_LIST_ITEM']); if (listActions.has(requestType)) { if ( (normalized.listName === undefined || normalized.listName === null || normalized.listName === '') && normalized.list_name !== undefined && normalized.list_name !== null && normalized.list_name !== '' ) { normalized.listName = normalized.list_name; } } if (requestType === 'PUBLISH_QDN_RESOURCE') { const payloadBase64 = pickBase64Value(normalized); if ( (normalized.data === undefined || normalized.data === null || normalized.data === '') && payloadBase64 !== '' ) { normalized.data = payloadBase64; } if ( (normalized.uploadType === undefined || normalized.uploadType === null || normalized.uploadType === '') && payloadBase64 !== '' ) { normalized.uploadType = 'base64'; } } if (requestType === 'PUBLISH_MULTIPLE_QDN_RESOURCES' && Array.isArray(normalized.resources)) { normalized.resources = normalized.resources.map(function (resource) { if (!resource || typeof resource !== 'object') { return resource; } const resourceCopy = Object.assign({}, resource); const resourceBase64 = pickBase64Value(resourceCopy); if ( (resourceCopy.data === undefined || resourceCopy.data === null || resourceCopy.data === '') && resourceBase64 !== '' ) { resourceCopy.data = resourceBase64; } if ( (resourceCopy.uploadType === undefined || resourceCopy.uploadType === null || resourceCopy.uploadType === '') && resourceBase64 !== '' ) { resourceCopy.uploadType = 'base64'; } return resourceCopy; }); } const cryptoActions = new Set([ 'ENCRYPT_DATA', 'DECRYPT_DATA', 'ENCRYPT_QORTAL_GROUP_DATA', 'DECRYPT_QORTAL_GROUP_DATA' ]); if (cryptoActions.has(requestType)) { if ( (normalized.data === undefined || normalized.data === null || normalized.data === '') && typeof normalized.data64 === 'string' && normalized.data64.trim() !== '' ) { normalized.data = normalized.data64; } if ( (normalized.encryptedData === undefined || normalized.encryptedData === null || normalized.encryptedData === '') && typeof normalized.encryptedData64 === 'string' && normalized.encryptedData64.trim() !== '' ) { normalized.encryptedData = normalized.encryptedData64; } } return normalized; } function buildGatewayPath(payload) { if (!payload || typeof payload !== 'object') { return ''; } const service = payload.service || payload.type || 'WEBSITE'; const name = payload.name; if (!name) { return ''; } let path = String(service).replace(/^\/+/, '') + '/' + String(name).replace(/^\/+/, ''); if (payload.identifier) { path += '/' + String(payload.identifier).replace(/^\/+/, ''); } if (payload.path) { const extra = String(payload.path); path += extra.startsWith('/') ? extra : '/' + extra; } return path; } function openGatewayPath(path) { if (!path) { return ''; } const normalized = path.replace(/^\/+/, ''); const upstreamPath = toUpstreamProxyPath(normalized, true); if (!viewerRendererConfigured) { return ''; } if (gatewayProxyTemplate) { return buildProxyUrl(upstreamPath); } if (nodeUrl) { return nodeUrl.replace(/\/$/, '') + '/' + upstreamPath; } if (gatewayUrl) { return gatewayUrl.replace(/\/$/, '') + '/' + upstreamPath; } return ''; } function postBridgeError(port, errorMessage) { if (!port) { return; } port.postMessage({ result: null, error: { error: errorMessage, message: errorMessage, }, }); } function normalizeApprovalMode(mode) { const normalized = String(mode || '').trim(); if (normalized === 'session') { return 'type_minutes'; } if (normalized === 'scope') { return 'type_always'; } if (normalized === 'app') { return 'app_always'; } if (normalized === 'type_minutes' || normalized === 'type_always' || normalized === 'app_always') { return normalized; } return 'once'; } function normalizeApprovalMinutes(value) { const parsed = Number(String(value || '').trim()); if (!Number.isFinite(parsed)) { return defaultApprovalTempMinutes; } return Math.max(1, Math.min(Math.floor(parsed), 1440)); } function upsertTemporaryApprovalRule(entry) { const normalizedRequestType = String(entry.requestType || '').trim(); const normalizedScope = String(entry.scope || '').trim(); const normalizedApp = String(entry.app || '').trim(); if (!normalizedRequestType || !normalizedScope) { return; } const expiresAt = Number(entry.expiresAt || 0); if (!Number.isFinite(expiresAt) || expiresAt <= Date.now()) { return; } for (let i = temporaryApprovalRules.length - 1; i >= 0; i -= 1) { const candidate = temporaryApprovalRules[i]; if (!candidate || candidate.expiresAt <= Date.now()) { temporaryApprovalRules.splice(i, 1); continue; } if ( candidate.requestType === normalizedRequestType && candidate.scope === normalizedScope && candidate.app === normalizedApp ) { candidate.expiresAt = expiresAt; return; } } temporaryApprovalRules.push({ requestType: normalizedRequestType, scope: normalizedScope, app: normalizedApp, expiresAt, }); } function findTemporaryApprovalRule(requestType, scope, app) { const normalizedRequestType = String(requestType || '').trim(); const normalizedScope = String(scope || '').trim(); const normalizedApp = String(app || '').trim(); const now = Date.now(); for (let i = temporaryApprovalRules.length - 1; i >= 0; i -= 1) { const candidate = temporaryApprovalRules[i]; if (!candidate || candidate.expiresAt <= now) { temporaryApprovalRules.splice(i, 1); continue; } if (candidate.requestType !== normalizedRequestType) { continue; } if (candidate.scope !== normalizedScope) { continue; } if (candidate.app !== normalizedApp) { continue; } return candidate; } return null; } function approvalModeLabel(mode) { switch (normalizeApprovalMode(mode)) { case 'type_minutes': return 'Allow this request type for X minutes'; case 'type_always': return 'Always allow this request type'; case 'app_always': return 'Always allow all request types for this Q-App'; default: return 'Allow only this request'; } } function updateApprovalPolicyStatus(selectedMode) { if (!approvalPolicyStatusEl) { return; } approvalPolicyStatusEl.textContent = 'Policy currently active in this tab: ' + approvalModeLabel(activeApprovalPolicyMode) + '. This request: ' + approvalModeLabel(selectedMode); } function markWalletLocked() { walletLockState.state = 'locked'; walletLockState.expiresAt = null; if (approvalModal && !approvalModal.classList.contains('qortal-hidden')) { updateApprovalWalletStatus(); } } function markWalletUnknown() { walletLockState.state = 'unknown'; walletLockState.expiresAt = null; if (approvalModal && !approvalModal.classList.contains('qortal-hidden')) { updateApprovalWalletStatus(); } } function markWalletUnlocked(ttlSeconds) { walletLockState.state = 'unlocked'; if (typeof ttlSeconds === 'number' && Number.isFinite(ttlSeconds) && ttlSeconds > 0) { walletLockState.expiresAt = Date.now() + (ttlSeconds * 1000); if (approvalModal && !approvalModal.classList.contains('qortal-hidden')) { updateApprovalWalletStatus(); } return; } walletLockState.expiresAt = null; if (approvalModal && !approvalModal.classList.contains('qortal-hidden')) { updateApprovalWalletStatus(); } } function getWalletLockStatusPresentation() { if (walletLockState.state === 'locked') { return { text: 'Wallet lock: LOCKED (password required)', className: 'qortal-wallet-status-locked', showUnlockFields: true, }; } if (walletLockState.state === 'unlocked') { if (walletLockState.expiresAt && walletLockState.expiresAt <= Date.now()) { walletLockState.state = 'locked'; walletLockState.expiresAt = null; return { text: 'Wallet lock: LOCKED (previous unlock expired)', className: 'qortal-wallet-status-locked', showUnlockFields: true, }; } if (walletLockState.expiresAt && walletLockState.expiresAt > Date.now()) { const remainingMinutes = Math.max(1, Math.ceil((walletLockState.expiresAt - Date.now()) / 60000)); return { text: 'Wallet lock: UNLOCKED for about ' + remainingMinutes + ' min', className: 'qortal-wallet-status-ok', showUnlockFields: false, }; } return { text: 'Wallet lock: UNLOCKED', className: 'qortal-wallet-status-ok', showUnlockFields: false, }; } return { text: 'Wallet lock: unknown (password may be required)', className: 'qortal-wallet-status-unknown', showUnlockFields: false, }; } function updateApprovalWalletStatus() { const status = getWalletLockStatusPresentation(); if (approvalWalletStatusEl) { approvalWalletStatusEl.textContent = status.text; approvalWalletStatusEl.classList.remove( 'qortal-wallet-status-ok', 'qortal-wallet-status-locked', 'qortal-wallet-status-unknown' ); approvalWalletStatusEl.classList.add(status.className); } if (approvalUnlockFieldsEl) { approvalUnlockFieldsEl.classList.toggle('qortal-hidden', !status.showUnlockFields); } if (!status.showUnlockFields) { if (approvalPassword) { approvalPassword.value = ''; } if (approvalTtl) { approvalTtl.checked = defaultApprovalUnlock10Min; } } return status; } function parseUnlockStatus(value) { let candidate = value; for (let i = 0; i < 4; i += 1) { if (!candidate || typeof candidate !== 'object' || candidate.data === undefined || candidate.data === null) { break; } if (typeof candidate.data !== 'object') { break; } candidate = candidate.data; } if (!candidate || typeof candidate !== 'object' || typeof candidate.unlocked !== 'boolean') { return null; } const unlocked = candidate.unlocked; const rawExpires = candidate.expiresAt !== undefined ? candidate.expiresAt : candidate.expires_at; let expiresAtMs = null; if (typeof rawExpires === 'number' && Number.isFinite(rawExpires)) { if (rawExpires > 0) { expiresAtMs = rawExpires < 10_000_000_000 ? rawExpires * 1000 : rawExpires; } } else if (typeof rawExpires === 'string' && rawExpires.trim() !== '') { const trimmed = rawExpires.trim(); if (/^\d+$/.test(trimmed)) { const parsed = Number(trimmed); if (Number.isFinite(parsed) && parsed > 0) { expiresAtMs = parsed < 10_000_000_000 ? parsed * 1000 : parsed; } } else { const parsedDate = Date.parse(trimmed); if (!Number.isNaN(parsedDate) && parsedDate > 0) { expiresAtMs = parsedDate; } } } return { unlocked, expiresAtMs }; } async function refreshWalletLockState(walletId) { const id = String(walletId || '').trim(); if (!id || !qappsUnlockStatusUrl) { markWalletUnknown(); return false; } const headers = {}; if (typeof OC !== 'undefined' && OC.requestToken) { headers.requesttoken = OC.requestToken; } let response; try { const joiner = qappsUnlockStatusUrl.indexOf('?') === -1 ? '?' : '&'; response = await fetch( qappsUnlockStatusUrl + joiner + 'walletId=' + encodeURIComponent(id), { method: 'GET', headers } ); } catch (error) { logDebug('warn', 'Wallet unlock status fetch failed', { walletId: id, error: error && error.message ? error.message : String(error), }); markWalletUnknown(); return false; } let payload = null; try { payload = await response.json(); } catch (_error) { payload = null; } if (!response.ok || (payload && payload.error)) { logDebug('warn', 'Wallet unlock status request failed', { walletId: id, status: response.status, error: payload && payload.error ? payload.error : 'wallet_unlock_status_failed', }); markWalletUnknown(); return false; } const parsed = parseUnlockStatus(payload); if (!parsed) { markWalletUnknown(); return false; } if (!parsed.unlocked) { markWalletLocked(); return true; } if (parsed.expiresAtMs && parsed.expiresAtMs > Date.now()) { const ttlSeconds = Math.max(1, Math.floor((parsed.expiresAtMs - Date.now()) / 1000)); markWalletUnlocked(ttlSeconds); return true; } markWalletUnlocked(undefined); return true; } function showApprovalError(message) { if (!approvalError) { return; } approvalError.textContent = message; approvalError.classList.toggle('qortal-hidden', !message); } function showUnlockError(message) { if (!unlockError) { return; } unlockError.textContent = message; unlockError.classList.toggle('qortal-hidden', !message); } function showApprovalModal(context) { return new Promise(function (resolve) { if (!approvalModal || !approvalConfirm || !approvalCancel) { resolve({ action: 'cancel' }); return; } approvalContext = context || null; if (approvalAppEl) { approvalAppEl.textContent = (context && context.app) || currentQappAddress || 'Unknown'; } if (approvalActionEl) { const normalizedScope = normalizeApprovalScope( context && context.scope ? context.scope : '', context && context.requestType ? context.requestType : '' ); approvalActionEl.textContent = normalizedScope || (context && context.requestType) || 'Unknown'; } if (approvalPassword) { approvalPassword.value = ''; } if (approvalTtl) { approvalTtl.checked = defaultApprovalUnlock10Min; } const normalizedDefaultMode = normalizeApprovalMode(defaultApprovalMode); const defaultModeInput = document.querySelector( 'input[name="qortal-approval-mode"][value="' + normalizedDefaultMode + '"]' ); if (defaultModeInput) { defaultModeInput.checked = true; } else { const fallbackModeInput = document.querySelector('input[name="qortal-approval-mode"][value="once"]'); if (fallbackModeInput) { fallbackModeInput.checked = true; } } if (approvalTempMinutesEl) { approvalTempMinutesEl.value = String(defaultApprovalTempMinutes); } const walletStatus = updateApprovalWalletStatus(); updateApprovalPolicyStatus(normalizedDefaultMode); const modeInputs = document.querySelectorAll('input[name="qortal-approval-mode"]'); modeInputs.forEach(function (input) { input.onchange = function () { const selectedMode = normalizeApprovalMode(input.value || 'once'); updateApprovalPolicyStatus(selectedMode); if (approvalTempMinutesEl) { approvalTempMinutesEl.disabled = selectedMode !== 'type_minutes'; } }; }); if (approvalTempMinutesEl) { approvalTempMinutesEl.disabled = normalizedDefaultMode !== 'type_minutes'; } showApprovalError(''); approvalModal.classList.remove('qortal-hidden'); if (walletStatus.showUnlockFields && approvalPassword) { approvalPassword.focus(); approvalPassword.onkeydown = function (event) { if (event.key !== 'Enter') { return; } event.preventDefault(); if (approvalConfirm && !approvalConfirm.disabled) { approvalConfirm.click(); } }; } else if (approvalConfirm) { approvalConfirm.focus(); } logDebug('info', 'Approval modal shown', { requestType: context && context.requestType, app: context && context.app }); approvalConfirm.onclick = function () { const choice = document.querySelector('input[name="qortal-approval-mode"]:checked'); const mode = normalizeApprovalMode(choice ? choice.value : 'once'); const unlockFieldsVisible = approvalUnlockFieldsEl && !approvalUnlockFieldsEl.classList.contains('qortal-hidden'); const passwordValue = approvalPassword ? approvalPassword.value.trim() : ''; const ttlSeconds = unlockFieldsVisible && approvalTtl && approvalTtl.checked ? 600 : undefined; if (unlockFieldsVisible && !passwordValue) { showApprovalError('Wallet password is required because the wallet is locked.'); return; } logDebug('info', 'Approval confirm clicked', { mode }); resolve({ action: mode, password: unlockFieldsVisible ? passwordValue : '', ttlSeconds, tempMinutes: normalizeApprovalMinutes(approvalTempMinutesEl ? approvalTempMinutesEl.value : defaultApprovalTempMinutes), }); }; approvalCancel.onclick = function () { logDebug('info', 'Approval cancelled'); closeApprovalModal(); resolve({ action: 'cancel' }); }; }); } function showUnlockModal(context) { return new Promise(function (resolve) { if (!unlockModal || !unlockConfirm || !unlockCancel || !unlockPassword) { resolve({ action: 'cancel' }); return; } if (unlockAppEl) { unlockAppEl.textContent = (context && context.app) || currentQappAddress || 'Unknown'; } if (unlockActionEl) { unlockActionEl.textContent = (context && context.requestType) || 'Unknown'; } unlockPassword.value = ''; if (unlockSession) { unlockSession.checked = defaultUnlockSession20Min; } showUnlockError(''); unlockModal.classList.remove('qortal-hidden'); unlockPassword.focus(); unlockPassword.onkeydown = function (event) { if (event.key !== 'Enter') { return; } event.preventDefault(); if (unlockConfirm && !unlockConfirm.disabled) { unlockConfirm.click(); } }; unlockConfirm.onclick = function () { const password = unlockPassword.value.trim(); if (!password) { showUnlockError('Wallet password is required.'); return; } const ttlSeconds = unlockSession && unlockSession.checked ? 1200 : undefined; resolve({ action: 'unlock', password, ttlSeconds }); }; unlockCancel.onclick = function () { closeUnlockModal(); resolve({ action: 'cancel' }); }; }); } function closeApprovalModal() { if (!approvalModal) { return; } approvalModal.classList.add('qortal-hidden'); approvalContext = null; showApprovalError(''); if (approvalPassword) { approvalPassword.value = ''; approvalPassword.onkeydown = null; } if (approvalTtl) { approvalTtl.checked = defaultApprovalUnlock10Min; } if (approvalTempMinutesEl) { approvalTempMinutesEl.value = String(defaultApprovalTempMinutes); approvalTempMinutesEl.disabled = false; } const modeInputs = document.querySelectorAll('input[name="qortal-approval-mode"]'); modeInputs.forEach(function (input) { input.onchange = null; }); if (approvalConfirm) { approvalConfirm.disabled = false; approvalConfirm.textContent = 'Approve'; } if (approvalCancel) { approvalCancel.disabled = false; } } function closeUnlockModal() { if (!unlockModal) { return; } unlockModal.classList.add('qortal-hidden'); showUnlockError(''); if (unlockConfirm) { unlockConfirm.disabled = false; unlockConfirm.textContent = 'Unlock'; } if (unlockCancel) { unlockCancel.disabled = false; } if (unlockPassword) { unlockPassword.value = ''; unlockPassword.onkeydown = null; } } async function approveOnce(requestType, payload, context) { if (!qappsApproveUrl) { throw new Error('Approval endpoint is not configured'); } const walletId = context && context.walletId ? context.walletId : currentWalletId; const scope = normalizeApprovalScope(context && context.scope ? context.scope : '', requestType); if (!walletId) { throw new Error('walletId is not linked'); } if (!scope) { throw new Error('scope is missing'); } const headers = { 'Content-Type': 'application/json' }; if (typeof OC !== 'undefined' && OC.requestToken) { headers.requesttoken = OC.requestToken; } let response; try { response = await fetch(qappsApproveUrl, { method: 'POST', headers, body: JSON.stringify({ walletId, scope, app: currentQappAddress, requestType, }), }); } catch (error) { const message = error && error.message ? error.message : 'Failed to fetch'; throw new Error(`approval_fetch_failed (${scope}): ${message}`); } const json = await response.json(); if (!response.ok || json.ok === false || json.error) { throw new Error(json.error || 'approval_failed'); } const token = json.data && json.data.approvalToken ? json.data.approvalToken : json.approvalToken; if (!token) { throw new Error('approval_token_missing'); } return token; } async function runApprovedRequestWithFallbacks(requestType, payload, token, context, headers, decision, walletId) { const approvedScopes = new Set(); const initialScope = normalizeApprovalScope(context && context.scope ? context.scope : '', requestType); if (initialScope) { approvedScopes.add(initialScope); } const runRequest = async function (approvalToken) { const retryPayload = Object.assign({}, payload, { approvalToken }); const retryResponse = await fetch(qappsRequestUrl, { method: 'POST', headers, body: JSON.stringify({ requestType, payload: retryPayload, }), }); const retryJson = await retryResponse.json(); return { retryResponse, retryJson }; }; let attempt = 0; while (attempt < 4) { const { retryResponse, retryJson } = await runRequest(token); if (!retryResponse.ok || retryJson.ok === false || retryJson.error) { const retryError = retryJson.error || 'approval_failed'; logDebug('warn', 'Approval retry failed', { requestType, error: retryError }); if (typeof retryError === 'string' && retryError.includes('wallet_locked')) { markWalletLocked(); if (decision && decision.password) { await unlockWallet(walletId, decision.password, decision.ttlSeconds); } else { await ensureWalletUnlocked(walletId, requestType); } token = await approveOnce(requestType, payload, context); attempt += 1; continue; } if (typeof retryError === 'string' && retryError.includes('approval_required')) { const retryApprovalContext = extractApprovalContext(retryError, retryJson.details, requestType); const requiredScope = normalizeApprovalScope(retryApprovalContext.scope, requestType); if (requiredScope && !approvedScopes.has(requiredScope)) { logDebug('info', 'Supplemental approval required', { requestType, scope: requiredScope }); token = await approveOnce(requestType, payload, { scope: requiredScope, walletId: retryApprovalContext.walletId || walletId, }); approvedScopes.add(requiredScope); attempt += 1; continue; } } return { ok: false, error: retryError }; } const retryResult = retryJson.data !== undefined ? retryJson.data : retryJson; return { ok: true, result: retryResult }; } return { ok: false, error: 'approval_failed' }; } async function setPermission(mode, requestType, scopeOverride, walletIdOverride) { if (!qappsPermissionsUrl) { throw new Error('Permissions endpoint is not configured'); } const effectiveWalletId = walletIdOverride || currentWalletId; if (!effectiveWalletId) { throw new Error('walletId is not linked'); } const normalizedMode = normalizeApprovalMode(mode); const modeValue = 'persistent'; const contextScope = normalizeApprovalScope( scopeOverride || (approvalContext && approvalContext.scope ? approvalContext.scope : ''), requestType ); const scopeValue = contextScope || fallbackApprovalScope(requestType); const headers = { 'Content-Type': 'application/json' }; if (typeof OC !== 'undefined' && OC.requestToken) { headers.requesttoken = OC.requestToken; } async function postPermission(permissionPayload) { logDebug('info', 'Setting permission', { mode: permissionPayload.mode, scope: permissionPayload.scope, app: permissionPayload.app || '', requestType: permissionPayload.requestType || '', }); let response; try { response = await fetch(qappsPermissionsUrl, { method: 'POST', headers, body: JSON.stringify(permissionPayload), }); } catch (error) { const message = error && error.message ? error.message : 'Failed to fetch'; throw new Error('permission_fetch_failed: ' + message); } const text = await response.text(); let json = null; if (text) { try { json = JSON.parse(text); } catch (_error) { json = null; } } if (!response.ok) { const detail = json && json.error ? json.error : ('HTTP ' + response.status); throw new Error('permission_set_failed: ' + detail); } if (json && (json.ok === false || json.error)) { throw new Error(json.error || 'permission_set_failed'); } return json || { ok: true }; } if (normalizedMode === 'app_always') { if (!currentQappAddress) { throw new Error('app context is missing'); } const appWidePayload = { walletId: effectiveWalletId, scope: currentQappAddress, mode: modeValue, app: currentQappAddress, }; let appWideSet = false; let appWideError = null; try { await postPermission(appWidePayload); appWideSet = true; } catch (primaryError) { appWideError = primaryError; logDebug('warn', 'App-wide permission set failed, trying app+type fallback', { requestType, error: primaryError && primaryError.message ? primaryError.message : String(primaryError), }); } // Compatibility: some external-auth versions accept app-wide permissions // but still enforce request scope checks. Also set app+scope when available. if (scopeValue) { const fallbackPayload = { walletId: effectiveWalletId, scope: scopeValue, mode: modeValue, app: currentQappAddress, ...(requestType ? { requestType } : {}), }; try { return await postPermission(fallbackPayload); } catch (fallbackError) { if (appWideSet) { logDebug('warn', 'App+scope permission set failed after app-wide success', { requestType, scope: scopeValue, error: fallbackError && fallbackError.message ? fallbackError.message : String(fallbackError), }); return { ok: true }; } throw fallbackError; } } if (appWideSet) { return { ok: true }; } throw appWideError || new Error('scope is missing'); } if (normalizedMode === 'type_always') { if (!scopeValue) { throw new Error('scope is missing'); } return await postPermission({ walletId: effectiveWalletId, scope: scopeValue, mode: modeValue, requestType, }); } throw new Error('permission mode not supported'); } async function runPermissionRequestWithFallbacks(mode, requestType, payload, headers, decision, walletId, context) { const grantedScopes = new Set(); const initialScope = normalizeApprovalScope(context && context.scope ? context.scope : '', requestType); if (initialScope) { grantedScopes.add(initialScope); } let attempt = 0; while (attempt < 4) { const retryResponse = await fetch(qappsRequestUrl, { method: 'POST', headers, body: JSON.stringify({ requestType, payload, }), }); const retryJson = await retryResponse.json(); if (!retryResponse.ok || retryJson.ok === false || retryJson.error) { const retryError = retryJson.error || 'approval_failed'; logDebug('warn', 'Permission retry failed', { requestType, error: retryError }); if (typeof retryError === 'string' && retryError.includes('wallet_locked')) { markWalletLocked(); if (decision && decision.password) { await unlockWallet(walletId, decision.password, decision.ttlSeconds); } else { await ensureWalletUnlocked(walletId, requestType); } attempt += 1; continue; } if (typeof retryError === 'string' && retryError.includes('approval_required')) { const retryApprovalContext = extractApprovalContext(retryError, retryJson.details, requestType); const requiredScope = normalizeApprovalScope(retryApprovalContext.scope, requestType); if (requiredScope && !grantedScopes.has(requiredScope)) { await setPermission(mode, requestType, requiredScope, retryApprovalContext.walletId || walletId); grantedScopes.add(requiredScope); attempt += 1; continue; } } return { ok: false, error: retryError }; } const retryResult = retryJson.data !== undefined ? retryJson.data : retryJson; return { ok: true, result: retryResult }; } return { ok: false, error: 'approval_failed' }; } async function tryTemporaryApproval(requestType, payload, approvalCtx, headers) { const walletId = approvalCtx && approvalCtx.walletId ? approvalCtx.walletId : currentWalletId; const scope = normalizeApprovalScope(approvalCtx && approvalCtx.scope ? approvalCtx.scope : '', requestType); if (!walletId || !scope || !currentQappAddress) { return null; } const activeRule = findTemporaryApprovalRule(requestType, scope, currentQappAddress); if (!activeRule) { return null; } logDebug('info', 'Applying temporary approval rule', { requestType, scope, app: currentQappAddress, expiresAt: activeRule.expiresAt, }); try { const token = await approveOnce(requestType, payload, approvalCtx); const retryOutcome = await runApprovedRequestWithFallbacks( requestType, payload, token, approvalCtx, headers, { action: 'type_minutes', password: '', ttlSeconds: undefined }, walletId ); return retryOutcome; } catch (error) { logDebug('warn', 'Temporary approval auto-flow failed', { requestType, error: error && error.message ? error.message : String(error), }); return null; } } async function unlockWallet(walletId, password, ttlSeconds) { if (!qappsUnlockUrl) { throw new Error('Unlock endpoint is not configured'); } const headers = { 'Content-Type': 'application/json' }; if (typeof OC !== 'undefined' && OC.requestToken) { headers.requesttoken = OC.requestToken; } const response = await fetch(qappsUnlockUrl, { method: 'POST', headers, body: JSON.stringify({ walletId, password, ...(ttlSeconds ? { ttlSeconds } : {}), }), }); const json = await response.json(); if (!response.ok || json.ok === false || json.error) { markWalletLocked(); throw new Error(json.error || 'wallet_unlock_failed'); } markWalletUnlocked(typeof ttlSeconds === 'number' ? ttlSeconds : undefined); return json; } async function ensureWalletUnlocked(walletId, requestType) { const decision = await showUnlockModal({ requestType, app: currentQappAddress, walletId }); if (decision.action === 'cancel') { throw new Error('wallet_locked'); } if (unlockConfirm) { unlockConfirm.disabled = true; unlockConfirm.textContent = 'Unlocking...'; } if (unlockCancel) { unlockCancel.disabled = true; } try { await unlockWallet(walletId, decision.password, decision.ttlSeconds); closeUnlockModal(); } catch (unlockErr) { showUnlockError(unlockErr.message || 'wallet_unlock_failed'); if (unlockConfirm) { unlockConfirm.disabled = false; unlockConfirm.textContent = 'Unlock'; } if (unlockCancel) { unlockCancel.disabled = false; } throw unlockErr; } } function extractApprovalContext(errorMessage, details, requestType) { const context = { scope: '', walletId: '', }; function captureFromValue(value, depth) { if (depth > 3 || value === null || value === undefined) { return; } if (typeof value === 'string') { if (!context.scope) { const scopeMatch = value.match(/"scope"\s*:\s*"([^"]+)"/); if (scopeMatch && scopeMatch[1]) { context.scope = scopeMatch[1]; } } if (!context.walletId) { const walletMatch = value.match(/"walletId"\s*:\s*"([^"]+)"/); if (walletMatch && walletMatch[1]) { context.walletId = walletMatch[1]; } } const jsonStart = value.indexOf('{'); const jsonEnd = value.lastIndexOf('}'); if (jsonStart !== -1 && jsonEnd !== -1 && jsonEnd > jsonStart) { try { const parsed = JSON.parse(value.slice(jsonStart, jsonEnd + 1)); captureFromValue(parsed, depth + 1); } catch (_err) { // ignore parse errors } } return; } if (typeof value !== 'object') { return; } if (!context.scope && typeof value.scope === 'string') { context.scope = value.scope; } if (!context.walletId && typeof value.walletId === 'string') { context.walletId = value.walletId; } if (value.detail !== undefined) { captureFromValue(value.detail, depth + 1); } if (value.data !== undefined) { captureFromValue(value.data, depth + 1); } if (value.error !== undefined) { captureFromValue(value.error, depth + 1); } } captureFromValue(details, 0); captureFromValue(errorMessage, 0); context.scope = normalizeApprovalScope(context.scope, requestType); if (!context.walletId) { context.walletId = currentWalletId || ''; } return context; } function isWalletLockedError(errorMessage, details) { if (typeof errorMessage === 'string' && errorMessage.includes('wallet_locked')) { return true; } const detail = details && details.data && details.data.detail ? details.data.detail : null; if (detail && typeof detail === 'object' && detail.error === 'wallet_locked') { return true; } if (details && typeof details === 'object' && details.error === 'wallet_locked') { return true; } return false; } window.addEventListener('message', async function (event) { const message = event && event.data ? event.data : null; if (!message || typeof message.action !== 'string') { return; } const hasPort = Boolean(event && event.ports && event.ports[0]); if (viewerFrame && event.source && viewerFrame.contentWindow && event.source !== viewerFrame.contentWindow) { logDebug('warn', 'Ignored message from unknown source', { action: message.action }); return; } if (event.origin && !allowedOrigins.has(event.origin)) { logDebug('warn', 'Ignored message from disallowed origin', { origin: event.origin, action: message.action }); return; } const requestType = message.action; const payload = normalizeBridgePayload(requestType, buildBridgePayload(message)); if (requestType === 'SET_TAB' && message.requestedHandler === 'UI') { const path = buildGatewayPath(payload); const url = openGatewayPath(path); if (url) { logDebug('info', 'SET_TAB opening', { path, url }); window.open(url, '_blank', 'noopener'); } if (event.source && typeof event.source.postMessage === 'function') { event.source.postMessage( { action: 'SET_TAB_SUCCESS', requestedHandler: 'UI', payload: { name: payload.name, }, }, event.origin || '*' ); } return; } if (!hasPort && requestType === 'SET_TAB_SUCCESS') { return; } if (!hasPort) { if (requestType === 'NAVIGATION_HISTORY') { const addressFromPayload = getNavigationAddressFromPayload(payload); if (addressFromPayload) { setViewerAddressDisplay(addressFromPayload); logDebug('info', 'Navigation history update (no port)', { requestType, address: addressFromPayload }); } else { const frameAddress = syncViewerAddressFromFrame(); logDebug('info', 'Navigation history update (no port)', frameAddress ? { requestType, address: frameAddress } : { requestType }); } return; } logDebug('warn', 'Missing message port', { requestType }); return; } const port = event.ports[0]; if (!requestType) { postBridgeError(port, 'requestType is required'); return; } if (requestType === 'QDN_RESOURCE_DISPLAYED') { port.postMessage({ result: true, error: null }); return; } if (requestType === 'LINK_TO_QDN_RESOURCE') { const path = buildGatewayPath(payload); const url = openGatewayPath(path); if (url) { logDebug('info', 'LINK_TO_QDN_RESOURCE opening', { path, url }); window.open(url, '_blank', 'noopener'); } port.postMessage({ result: true, error: null }); return; } if (requestType === 'OPEN_NEW_TAB') { const qortalLink = (typeof payload.qortalLink === 'string' && payload.qortalLink.trim()) || (typeof payload.link === 'string' && payload.link.trim()) || (typeof payload.url === 'string' && payload.url.trim()) || (typeof payload.href === 'string' && payload.href.trim()) || ''; if (!qortalLink) { postBridgeError(port, 'qortalLink is required'); return; } logDebug('info', 'OPEN_NEW_TAB routed to integrated viewer', { qortalLink }); openAddress(qortalLink, 'Qortal Browser'); port.postMessage({ result: true, error: null }); return; } try { const gatewayResult = await handleGatewayRequest(requestType, payload); if (gatewayResult.handled) { if (gatewayResult.error) { logDebug('error', 'Gateway request failed', { requestType, error: gatewayResult.error }); postBridgeError(port, gatewayResult.error); } else { logDebug('info', 'Gateway request handled', { requestType, result: gatewayResult.data }); port.postMessage({ result: gatewayResult.data, error: null }); } return; } if (!qappsRequestUrl) { logDebug('error', 'Q-Apps request URL missing', { requestType }); postBridgeError(port, 'Q-Apps request endpoint is not configured'); return; } const headers = { 'Content-Type': 'application/json' }; if (typeof OC !== 'undefined' && OC.requestToken) { headers.requesttoken = OC.requestToken; } logDebug('info', 'Broker qortalRequest start', { requestType, payload }); const response = await fetch(qappsRequestUrl, { method: 'POST', headers, body: JSON.stringify({ requestType, payload, }), }); const json = await response.json(); if (!response.ok || json.ok === false || json.error) { const errorMessage = json.error || 'qortal_request_failed'; const approvalContext = extractApprovalContext(errorMessage, json.details, requestType); if (isWalletLockedError(errorMessage, json.details)) { markWalletLocked(); const walletId = approvalContext.walletId || payload.walletId || currentWalletId; logDebug('info', 'Wallet locked', { requestType, walletId }); const decision = await showUnlockModal({ requestType, app: currentQappAddress, walletId }); if (decision.action === 'cancel') { postBridgeError(port, 'wallet_locked'); closeUnlockModal(); return; } if (unlockConfirm) { unlockConfirm.disabled = true; unlockConfirm.textContent = 'Unlocking...'; } if (unlockCancel) { unlockCancel.disabled = true; } try { await unlockWallet(walletId, decision.password, decision.ttlSeconds); const retryResponse = await fetch(qappsRequestUrl, { method: 'POST', headers, body: JSON.stringify({ requestType, payload, }), }); const retryJson = await retryResponse.json(); if (!retryResponse.ok || retryJson.ok === false || retryJson.error) { const retryError = retryJson.error || 'qortal_request_failed'; if (typeof retryError === 'string' && retryError.includes('approval_required')) { const retryApprovalContext = extractApprovalContext(retryError, retryJson.details, requestType); logDebug('info', 'Approval required after unlock', { requestType }); const temporaryOutcome = await tryTemporaryApproval( requestType, payload, retryApprovalContext, headers ); if (temporaryOutcome && temporaryOutcome.ok) { logDebug('info', 'Broker qortalRequest ok', { requestType, result: temporaryOutcome.result }); activeApprovalPolicyMode = 'type_minutes'; port.postMessage({ result: temporaryOutcome.result, error: null }); closeUnlockModal(); return; } closeUnlockModal(); await refreshWalletLockState(retryApprovalContext.walletId || currentWalletId); const decisionAfterUnlock = await showApprovalModal({ requestType, payload, app: currentQappAddress, scope: retryApprovalContext.scope, walletId: retryApprovalContext.walletId, }); if (decisionAfterUnlock.action === 'cancel') { postBridgeError(port, 'approval_cancelled'); closeApprovalModal(); return; } if (approvalConfirm) { approvalConfirm.disabled = true; approvalConfirm.textContent = 'Approving...'; } if (approvalCancel) { approvalCancel.disabled = true; } try { if (decisionAfterUnlock.action === 'once' || decisionAfterUnlock.action === 'type_minutes') { if (decisionAfterUnlock.action === 'type_minutes') { const temporaryScope = normalizeApprovalScope(retryApprovalContext.scope, requestType); upsertTemporaryApprovalRule({ requestType, scope: temporaryScope, app: currentQappAddress, expiresAt: Date.now() + (decisionAfterUnlock.tempMinutes * 60 * 1000), }); } const token = await approveOnce(requestType, payload, retryApprovalContext); const retryOutcome = await runApprovedRequestWithFallbacks( requestType, payload, token, retryApprovalContext, headers, decisionAfterUnlock, retryApprovalContext.walletId || currentWalletId ); if (!retryOutcome.ok) { showApprovalError(retryOutcome.error || 'approval_failed'); postBridgeError(port, retryOutcome.error || 'qortal_request_failed'); closeApprovalModal(); return; } logDebug('info', 'Broker qortalRequest ok', { requestType, result: retryOutcome.result }); if (decisionAfterUnlock.action === 'type_minutes') { activeApprovalPolicyMode = 'type_minutes'; } port.postMessage({ result: retryOutcome.result, error: null }); closeApprovalModal(); return; } if ( decisionAfterUnlock.action === 'type_always' || decisionAfterUnlock.action === 'app_always' ) { const walletIdAfterUnlock = retryApprovalContext.walletId || currentWalletId; await setPermission(decisionAfterUnlock.action, requestType, retryApprovalContext.scope, walletIdAfterUnlock); const permissionOutcome = await runPermissionRequestWithFallbacks( decisionAfterUnlock.action, requestType, payload, headers, decisionAfterUnlock, walletIdAfterUnlock, retryApprovalContext ); if (!permissionOutcome.ok) { showApprovalError(permissionOutcome.error || 'approval_failed'); postBridgeError(port, permissionOutcome.error || 'qortal_request_failed'); closeApprovalModal(); return; } activeApprovalPolicyMode = decisionAfterUnlock.action; logDebug('info', 'Broker qortalRequest ok', { requestType, result: permissionOutcome.result }); port.postMessage({ result: permissionOutcome.result, error: null }); closeApprovalModal(); return; } } finally { if (approvalConfirm) { approvalConfirm.disabled = false; approvalConfirm.textContent = 'Approve'; } if (approvalCancel) { approvalCancel.disabled = false; } } } showUnlockError(retryError || 'wallet_unlock_failed'); postBridgeError(port, retryError || 'wallet_unlock_failed'); closeUnlockModal(); return; } const retryResult = retryJson.data !== undefined ? retryJson.data : retryJson; logDebug('info', 'Broker qortalRequest ok', { requestType, result: retryResult }); port.postMessage({ result: retryResult, error: null }); closeUnlockModal(); return; } catch (unlockErr) { showUnlockError(unlockErr.message || 'wallet_unlock_failed'); logDebug('error', 'Wallet unlock failed', { requestType, error: unlockErr.message || 'wallet_unlock_failed' }); postBridgeError(port, unlockErr.message || 'wallet_unlock_failed'); if (unlockConfirm) { unlockConfirm.disabled = false; unlockConfirm.textContent = 'Unlock'; } if (unlockCancel) { unlockCancel.disabled = false; } closeUnlockModal(); return; } } if (typeof errorMessage === 'string' && errorMessage.includes('approval_required')) { logDebug('info', 'Approval required', { requestType, context: approvalContext }); const temporaryOutcome = await tryTemporaryApproval(requestType, payload, approvalContext, headers); if (temporaryOutcome && temporaryOutcome.ok) { logDebug('info', 'Broker qortalRequest ok', { requestType, result: temporaryOutcome.result }); activeApprovalPolicyMode = 'type_minutes'; port.postMessage({ result: temporaryOutcome.result, error: null }); return; } await refreshWalletLockState(approvalContext.walletId || currentWalletId); const decision = await showApprovalModal({ requestType, payload, app: currentQappAddress, scope: approvalContext.scope, walletId: approvalContext.walletId, }); if (decision.action === 'cancel') { postBridgeError(port, 'approval_cancelled'); closeApprovalModal(); return; } if (approvalConfirm) { approvalConfirm.disabled = true; approvalConfirm.textContent = 'Approving...'; } if (approvalCancel) { approvalCancel.disabled = true; } try { const walletId = approvalContext.walletId || currentWalletId; if (decision.password) { await unlockWallet(walletId, decision.password, decision.ttlSeconds); } if (decision.action === 'once' || decision.action === 'type_minutes') { if (decision.action === 'type_minutes') { const temporaryScope = normalizeApprovalScope(approvalContext.scope, requestType); upsertTemporaryApprovalRule({ requestType, scope: temporaryScope, app: currentQappAddress, expiresAt: Date.now() + (decision.tempMinutes * 60 * 1000), }); } let token; try { token = await approveOnce(requestType, payload, approvalContext); } catch (error) { const msg = error && error.message ? error.message : String(error || 'approval_failed'); if (msg.includes('wallet_locked')) { markWalletLocked(); if (decision.password) { await unlockWallet(walletId, decision.password, decision.ttlSeconds); } else { await ensureWalletUnlocked(walletId, requestType); } token = await approveOnce(requestType, payload, approvalContext); } else { throw error; } } const retryOutcome = await runApprovedRequestWithFallbacks( requestType, payload, token, approvalContext, headers, decision, walletId ); if (!retryOutcome.ok) { showApprovalError(retryOutcome.error || 'approval_failed'); postBridgeError(port, retryOutcome.error || 'qortal_request_failed'); if (approvalConfirm) { approvalConfirm.disabled = false; approvalConfirm.textContent = 'Approve'; } if (approvalCancel) { approvalCancel.disabled = false; } closeApprovalModal(); return; } logDebug('info', 'Broker qortalRequest ok', { requestType, result: retryOutcome.result }); if (decision.action === 'type_minutes') { activeApprovalPolicyMode = 'type_minutes'; } port.postMessage({ result: retryOutcome.result, error: null }); closeApprovalModal(); return; } if (decision.action === 'type_always' || decision.action === 'app_always') { try { await setPermission(decision.action, requestType, approvalContext.scope, walletId); } catch (error) { const msg = error && error.message ? error.message : String(error || 'permission_set_failed'); if (msg.includes('wallet_locked')) { markWalletLocked(); if (decision.password) { await unlockWallet(walletId, decision.password, decision.ttlSeconds); } else { await ensureWalletUnlocked(walletId, requestType); } await setPermission(decision.action, requestType, approvalContext.scope, walletId); } else { throw error; } } const permissionOutcome = await runPermissionRequestWithFallbacks( decision.action, requestType, payload, headers, decision, walletId, approvalContext ); if (!permissionOutcome.ok) { showApprovalError(permissionOutcome.error || 'approval_failed'); postBridgeError(port, permissionOutcome.error || 'qortal_request_failed'); closeApprovalModal(); return; } activeApprovalPolicyMode = decision.action; logDebug('info', 'Broker qortalRequest ok', { requestType, result: permissionOutcome.result }); port.postMessage({ result: permissionOutcome.result, error: null }); closeApprovalModal(); return; } } catch (approvalError) { showApprovalError(approvalError.message || 'approval_failed'); logDebug('error', 'Approval failed', { requestType, error: approvalError.message || 'approval_failed' }); postBridgeError(port, approvalError.message || 'approval_failed'); if (approvalConfirm) { approvalConfirm.disabled = false; approvalConfirm.textContent = 'Approve'; } if (approvalCancel) { approvalCancel.disabled = false; } closeApprovalModal(); return; } } logDebug('error', 'Broker qortalRequest failed', { requestType, error: json.error || 'qortal_request_failed' }); postBridgeError(port, json.error || 'qortal_request_failed'); return; } let resultPayload = json.data !== undefined ? json.data : json; if (requestType === 'GET_PRIMARY_NAME' && typeof resultPayload !== 'string') { if (Array.isArray(resultPayload)) { resultPayload = resultPayload.length > 0 ? resultPayload[0] : ''; } else if (resultPayload && typeof resultPayload === 'object') { resultPayload = resultPayload.name || resultPayload.primaryName || (resultPayload.data && resultPayload.data.name) || ''; } else if (resultPayload === null || resultPayload === undefined) { resultPayload = ''; } } logDebug('info', 'Broker qortalRequest ok', { requestType, result: resultPayload }); port.postMessage({ result: resultPayload, error: null }); } catch (error) { logDebug('error', 'qortalRequest exception', { requestType, error: error.message || 'qortal_request_failed' }); postBridgeError(port, error.message || 'qortal_request_failed'); } }); function appendQuery(params, key, value) { if (value === undefined || value === null || value === '') { return; } if (Array.isArray(value)) { value.forEach(function (item) { appendQuery(params, key, item); }); return; } const normalized = typeof value === 'boolean' ? String(Boolean(value)) : String(value); params.append(key, normalized); } function buildGatewayApiUrl(path, params) { const base = openGatewayPath(path); if (!base) { return ''; } if (!params) { return base; } const qs = params.toString(); if (!qs) { return base; } return base + (base.includes('?') ? '&' : '?') + qs; } function parseGatewayResponse(text) { if (text === '') { return null; } try { return JSON.parse(text); } catch (error) { return text; } } async function fetchGateway(url) { logDebug('info', 'Gateway fetch', { url }); const response = await fetch(url, { method: 'GET', headers: { Accept: 'application/json,text/plain,*/*', }, }); const text = await response.text(); const parsed = parseGatewayResponse(text); if (!response.ok) { const message = parsed && parsed.error ? parsed.error : 'gateway_request_failed'; logDebug('error', 'Gateway fetch failed', { url, status: response.status, message }); throw new Error(message); } logDebug('info', 'Gateway fetch ok', { url, status: response.status }); return parsed; } async function handleGatewayRequest(requestType, payload) { const params = new URLSearchParams(); let path = ''; switch (requestType) { case 'IS_USING_PUBLIC_NODE': return { handled: true, data: false }; case 'GET_ACCOUNT_DATA': if (!payload.address) return { handled: true, error: 'address is required' }; path = `addresses/${payload.address}`; break; case 'GET_ACCOUNT_NAMES': if (!payload.address) return { handled: true, error: 'address is required' }; path = `names/address/${payload.address}`; break; case 'SEARCH_NAMES': path = 'names/search'; appendQuery(params, 'query', payload.query); appendQuery(params, 'prefix', payload.prefix); appendQuery(params, 'limit', payload.limit); appendQuery(params, 'offset', payload.offset); appendQuery(params, 'reverse', payload.reverse); break; case 'GET_NAME_DATA': if (!payload.name) return { handled: true, error: 'name is required' }; path = `names/${payload.name}`; break; case 'LIST_QDN_RESOURCES': path = 'arbitrary/resources'; appendQuery(params, 'service', payload.service); appendQuery(params, 'name', payload.name); appendQuery(params, 'identifier', payload.identifier); appendQuery(params, 'default', payload.default); appendQuery(params, 'includestatus', payload.includeStatus); appendQuery(params, 'includemetadata', payload.includeMetadata); appendQuery(params, 'namefilter', payload.nameListFilter); appendQuery(params, 'followedonly', payload.followedOnly); appendQuery(params, 'excludeblocked', payload.excludeBlocked); appendQuery(params, 'limit', payload.limit); appendQuery(params, 'offset', payload.offset); appendQuery(params, 'reverse', payload.reverse); break; case 'SEARCH_QDN_RESOURCES': path = 'arbitrary/resources/search'; appendQuery(params, 'service', payload.service); appendQuery(params, 'query', payload.query); appendQuery(params, 'identifier', payload.identifier); appendQuery(params, 'name', payload.name); appendQuery(params, 'name', payload.names); appendQuery(params, 'keywords', payload.keywords); appendQuery(params, 'title', payload.title); appendQuery(params, 'description', payload.description); appendQuery(params, 'prefix', payload.prefix); appendQuery(params, 'exactmatchnames', payload.exactMatchNames); appendQuery(params, 'default', payload.default); appendQuery(params, 'mode', payload.mode); appendQuery(params, 'minlevel', payload.minLevel); appendQuery(params, 'includestatus', payload.includeStatus); appendQuery(params, 'includemetadata', payload.includeMetadata); appendQuery(params, 'namefilter', payload.nameListFilter); appendQuery(params, 'followedonly', payload.followedOnly); appendQuery(params, 'excludeblocked', payload.excludeBlocked); appendQuery(params, 'before', payload.before); appendQuery(params, 'after', payload.after); appendQuery(params, 'limit', payload.limit); appendQuery(params, 'offset', payload.offset); appendQuery(params, 'reverse', payload.reverse); break; case 'FETCH_QDN_RESOURCE': if (!payload.service || !payload.name) return { handled: true, error: 'service and name are required' }; path = `arbitrary/${payload.service}/${payload.name}`; if (payload.identifier) { path += `/${payload.identifier}`; } appendQuery(params, 'filepath', payload.filepath); appendQuery(params, 'rebuild', payload.rebuild); appendQuery(params, 'encoding', payload.encoding); break; case 'GET_QDN_RESOURCE_STATUS': if (!payload.service || !payload.name) return { handled: true, error: 'service and name are required' }; path = `arbitrary/resource/status/${payload.service}/${payload.name}`; if (payload.identifier) { path += `/${payload.identifier}`; } appendQuery(params, 'build', payload.build); break; case 'GET_QDN_RESOURCE_METADATA': if (!payload.service || !payload.name) return { handled: true, error: 'service and name are required' }; path = `arbitrary/metadata/${payload.service}/${payload.name}/${payload.identifier || 'default'}`; break; case 'GET_QDN_RESOURCE_PROPERTIES': if (!payload.service || !payload.name) return { handled: true, error: 'service and name are required' }; path = `arbitrary/resource/properties/${payload.service}/${payload.name}/${payload.identifier || 'default'}`; break; case 'GET_QDN_RESOURCE_URL': if (!payload.service || !payload.name) return { handled: true, error: 'service and name are required' }; path = `arbitrary/resource/status/${payload.service}/${payload.name}`; if (payload.identifier) { path += `/${payload.identifier}`; } appendQuery(params, 'build', payload.build); { const statusUrl = buildGatewayApiUrl(path, params); if (!statusUrl) return { handled: true, error: 'Gateway URL is not configured' }; const status = await fetchGateway(statusUrl); const parsedStatus = parseQdnStatusPayload(status); const statusCode = parsedStatus ? parsedStatus.status : ''; const totalChunkCount = parsedStatus ? parsedStatus.totalChunkCount : coerceFiniteNumber(status && status.totalChunkCount); if ( !status || statusCode === 'NOT_PUBLISHED' || statusCode === 'UNSUPPORTED' || statusCode === 'BLOCKED' || (!statusCode && totalChunkCount !== null && totalChunkCount <= 0) ) { return { handled: true, error: 'Resource does not exist' }; } const resourcePath = `${payload.service}/${payload.name}` + (payload.identifier ? `/${payload.identifier}` : '') + (payload.path ? `/${String(payload.path).replace(/^\/+/, '')}` : ''); const url = openGatewayPath(resourcePath); return { handled: true, data: url }; } default: return { handled: false }; } const url = buildGatewayApiUrl(path, params); if (!url) { return { handled: true, error: 'Gateway URL is not configured' }; } const data = await fetchGateway(url); return { handled: true, data }; } })();