Files

6940 lines
211 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
(function () {
const root = document.getElementById("qortal-qapps-root");
if (!root) {
return;
}
let qapps = [];
const normalizeQappIconMode = function (value) {
const normalized = String(value || "")
.trim()
.toLowerCase();
if (normalized === "custom" || normalized === "name") {
return normalized;
}
return "auto";
};
try {
const parsed = JSON.parse(root.dataset.qapps || "[]");
if (Array.isArray(parsed)) {
qapps = parsed
.filter(function (entry) {
return (
entry &&
typeof entry === "object" &&
typeof entry.address === "string" &&
entry.address.trim() !== ""
);
})
.map(function (entry) {
return {
name: typeof entry.name === "string" ? entry.name.trim() : "",
address: String(entry.address || "").trim(),
description:
typeof entry.description === "string"
? entry.description.trim()
: "",
iconMode: normalizeQappIconMode(entry.iconMode),
iconUrl:
typeof entry.iconUrl === "string" ? entry.iconUrl.trim() : "",
iconFromName:
typeof entry.iconFromName === "string"
? entry.iconFromName.trim()
: typeof entry.iconSourceName === "string"
? entry.iconSourceName.trim()
: "",
};
});
}
} 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 embedMode =
root.dataset.embedMode === "1" || urlParams.get("embed") === "1";
const embedChromeless =
embedMode &&
(urlParams.get("chromeless") === "1" ||
urlParams.get("embed_chrome") === "0");
const embedLockQapp = embedMode && urlParams.get("lock_qapp") === "1";
const embedRequestedQapp = normalizeBrowserAddress(
decodeURIComponent(urlParams.get("qapp") || urlParams.get("app") || "")
);
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 advancedUnlockSettingsEnabled =
root.dataset.qappsAdvancedUnlockSettingsEnabled === "1";
const defaultApprovalMode = (function () {
if (!advancedUnlockSettingsEnabled) {
return "app_always";
}
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 || "120").trim()
);
if (!Number.isFinite(parsed)) {
return 120;
}
const value = Math.floor(parsed);
const allowed = [60, 120, 180, 360, 720, 1440, 7200];
return allowed.includes(value) ? value : 120;
})();
const defaultApprovalUnlock10Min =
advancedUnlockSettingsEnabled &&
root.dataset.qappsDefaultApprovalUnlock10Min === "1";
const defaultUnlockSession20Min =
advancedUnlockSettingsEnabled &&
root.dataset.qappsDefaultUnlockSession20Min === "1";
const userMappingsUrl = root.dataset.userMappingsUrl || "";
const nextcloudPublicUrl = root.dataset.nextcloudPublicUrl || "";
const settingsPath =
root.dataset.settingsPath || "/settings/user/qortal_integration";
const sovereignMode =
root.dataset.sovereignMode === "full_qortal" ? "full_qortal" : "powered";
const isPoweredMode = sovereignMode === "powered";
const effectiveDebugEnabled = !isPoweredMode && debugEnabled;
const modeCopy = isPoweredMode
? {
title: "NuQloud Apps",
help: "Launch approved NuQloud Apps from inside Nextcloud.",
emptyNote: "App access is not enabled yet. Ask an admin to enable it.",
browserTitle: "Full NuQloud Browser",
browserNote:
"Open the full NuQloud browser for unrestricted app access.",
warningNote:
"No linked wallet is available for your account yet. You can browse apps, but authenticated actions will not work until you create or attach a wallet.",
settingsButton: "Open Integration Settings",
nameRequiredTitle: "Distributed Identity Setup Needed",
nameRequiredContinue: "Continue in View-Only Mode",
listTitle: "Available NuQloud Apps",
listNote:
"Launch approved NuQloud Apps from this list, or use Full Browser mode for unrestricted navigation.",
listEmpty: "No apps available.",
nodeMissingAlert:
"Node or gateway URL is not configured. Ask an admin to configure rendering.",
launchDisabledTitle: "Node or gateway URL is not configured.",
browserName: "NuQloud Browser",
requestEndpointMissing: "Apps request endpoint is not configured",
approvalTitle: advancedUnlockSettingsEnabled
? "Authorize Request"
: "Continue in NuQloud App",
approvalNote: advancedUnlockSettingsEnabled
? "Choose how this request should be handled. Broker/API session auth is automatic while you are logged into Nextcloud."
: "Continue with this app action. Enter your account password only if your account is currently locked.",
unlockTitle: advancedUnlockSettingsEnabled
? "Unlock Wallet"
: "Unlock to Continue",
unlockNote: advancedUnlockSettingsEnabled
? "Your wallet is locked. Enter your wallet password to continue."
: "Enter your account password to continue in this app.",
loadingLabel: "Loading from decentralized network...",
loadingFromQdn: "Loading from decentralized network...",
accountLabel: "NuQloud decentralized account",
unitLabel: "publish credits",
costLabel: "125 publish credits",
createAccountStep: "Use the 'Create Account' button on the dashboard.",
fundingStep:
"Request initial publish credits from your admins or CHD, or purchase publish credits on the dashboard.",
nameStep:
"Use 125 publish credits to register your distributed identity name on the network.",
prereqOutro:
"Then you will have full access to distributed authentication, communications, backups, and data features.",
mobileSwitchNote:
'Open "{name}" here? This will replace your current app in this view.',
mobileLeaveNote:
'Leave your current app and open "{name}" here? Tap OK to open anyway or Cancel to stay.',
}
: {
title: "Q-Apps",
help: "Launch approved Q-Apps from inside Nextcloud.",
emptyNote:
"Q-Apps access is not enabled yet. Ask an admin to enable it.",
browserTitle: "Full Qortal Browser",
browserNote:
"Open the full Qortal browser for unrestricted Q-App access.",
warningNote:
"No Qortal account is linked to your Nextcloud user yet. You can browse Q-Apps, but authentication-required actions will not work until you create or attach a Qortal account.",
settingsButton: "Open Qortal Settings",
nameRequiredTitle: "Distributed Identity Setup Needed",
nameRequiredContinue: "Continue in View-Only Mode",
listTitle: "Available Q-Apps",
listNote:
"Launch approved Q-Apps from this list, or use Full Browser mode for unrestricted navigation.",
listEmpty: "No Q-Apps available.",
nodeMissingAlert:
"Qortal node or gateway URL is not configured. Ask an admin to configure rendering.",
launchDisabledTitle: "Qortal node or gateway URL is not configured.",
browserName: "Qortal Browser",
requestEndpointMissing: "Q-Apps request endpoint is not configured",
approvalTitle: advancedUnlockSettingsEnabled
? "Authorize Qortal Request"
: "Continue in Q-App",
approvalNote: advancedUnlockSettingsEnabled
? "Choose how this Qortal request should be handled. Broker/API session auth is automatic while you are logged into Nextcloud."
: "Continue with this Q-App action. Enter your wallet password only if your wallet is currently locked.",
unlockTitle: advancedUnlockSettingsEnabled
? "Unlock Qortal Wallet"
: "Unlock to Continue",
unlockNote: advancedUnlockSettingsEnabled
? "Your wallet is locked. Enter your wallet password to continue."
: "Enter your wallet password to continue in this app.",
loadingLabel: "Loading from QDN...",
loadingFromQdn: "Loading from QDN...",
accountLabel: "Qortal account",
unitLabel: "QORT",
costLabel: "1.25 QORT",
createAccountStep: "Use the 'Create Account' button on the dashboard.",
fundingStep:
"Request initial QORT from your admins/CHD, or purchase publish credits/QORT on the dashboard.",
nameStep:
"Use 1.25 QORT to register your distributed identity name on the network.",
prereqOutro:
"Then you will have full access to distributed authentication, communications, backups, and QDN data features.",
mobileSwitchNote:
'Open "{name}" here? This will replace your current app in this view.',
mobileLeaveNote:
'Leave your current app and open "{name}" here? Tap OK to open anyway or Cancel to stay.',
};
const emptyCard = document.getElementById("qortal-qapps-empty");
const titleEl = document.getElementById("qortal-qapps-title");
const helpEl = document.getElementById("qortal-qapps-help");
const emptyNoteEl = document.getElementById("qortal-qapps-empty-note");
const browserCard = document.getElementById("qortal-qapps-browser");
const browserTitleEl = document.getElementById("qortal-qapps-browser-title");
const browserNoteEl = document.getElementById("qortal-qapps-browser-note");
const browserAddressEl = document.getElementById(
"qortal-qapps-browser-address"
);
const browserButton = document.getElementById("qortal-qapps-open-browser");
const listCard = document.getElementById("qortal-qapps-list");
const listTitleEl = document.getElementById("qortal-qapps-list-title");
const listNoteEl = document.getElementById("qortal-qapps-list-note");
const galleryEl = document.getElementById("qortal-qapps-gallery");
const listBody = document.getElementById("qortal-qapps-body");
const warningCard = document.getElementById("qortal-qapps-warning");
const warningNoteEl = document.getElementById("qortal-qapps-warning-note");
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 viewerNodeStats = document.getElementById(
"qortal-qapps-viewer-node-stats"
);
const viewerPeersPill = document.getElementById(
"qortal-qapps-viewer-peers-pill"
);
const viewerPeersValue = document.getElementById(
"qortal-qapps-viewer-peers-value"
);
const viewerDataPeersPill = document.getElementById(
"qortal-qapps-viewer-data-peers-pill"
);
const viewerDataPeersValue = document.getElementById(
"qortal-qapps-viewer-data-peers-value"
);
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 viewerMobileRefresh = document.getElementById("qortal-qapps-mobile-refresh");
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 debugResizeHandle = document.getElementById(
"qortal-qapps-debug-resize-handle"
);
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 approvalTitleEl = document.getElementById(
"qortal-qapps-approval-title"
);
const approvalNoteEl = document.getElementById("qortal-qapps-approval-note");
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 unlockTitleEl = document.getElementById("qortal-qapps-unlock-title");
const unlockNoteEl = document.getElementById("qortal-qapps-unlock-note");
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 nameRequiredTitleEl = document.getElementById(
"qortal-qapps-name-required-title"
);
const nameRequiredNote1El = document.getElementById(
"qortal-qapps-name-required-note-1"
);
const nameRequiredStepsEl = document.getElementById(
"qortal-qapps-name-required-steps"
);
const nameRequiredStep1El = document.getElementById(
"qortal-qapps-name-required-step-1"
);
const nameRequiredStep2El = document.getElementById(
"qortal-qapps-name-required-step-2"
);
const nameRequiredStep3El = document.getElementById(
"qortal-qapps-name-required-step-3"
);
const nameRequiredNote2El = document.getElementById(
"qortal-qapps-name-required-note-2"
);
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 switchModal = document.getElementById("qortal-qapps-switch-modal");
const switchModalNoteEl = document.getElementById(
"qortal-qapps-switch-modal-note"
);
const switchModalOpenHereButton = document.getElementById(
"qortal-qapps-switch-open-here"
);
const switchModalOpenNewTabButton = document.getElementById(
"qortal-qapps-switch-open-new-tab"
);
const switchModalCancelButton = document.getElementById(
"qortal-qapps-switch-cancel"
);
const debugPanelDockStorageKey = "qortal_qapps_debug_dock";
const debugPanelHeightStorageKey = "qortal_qapps_debug_height";
const debugPanelDefaultHeight = 280;
const debugPanelMinHeight = 160;
const debugPanelCompactRatio = 0.25;
const debugPanelPeekHeight = 18;
let debugPanelExpandedHeight = debugPanelDefaultHeight;
let debugPanelResizeObserver = null;
let debugDockHoverSuppressed = false;
if (embedMode) {
root.classList.add("qortal-embed-mode");
if (embedChromeless) {
root.classList.add("qortal-embed-chromeless");
}
}
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: isPoweredMode
? "Downloading from decentralized network..."
: "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 temporaryApprovalStorageKey = "qortal_qapps_temp_rules_v1";
const temporaryApprovalRules = loadTemporaryApprovalRules();
let qdnLoadingRetryTimer = null;
let qdnLoadingRetryAttempts = 0;
let qdnLoadingStatusFailures = 0;
let qdnLoadingLastSnapshot = "";
let currentQdnResourceTarget = null;
let viewerNodeStatusPollTimer = null;
let viewerNodeStatusLastFetchAt = 0;
let switchModalResolver = null;
const nameLookupCacheTtlMs = 60 * 1000;
let nameLookupCache = {
address: "",
hasName: null,
checkedAt: 0,
};
let balanceLookupCache = {
address: "",
balanceQort: null,
checkedAt: 0,
};
function applyModeCopy() {
if (titleEl) {
titleEl.textContent = modeCopy.title;
}
if (helpEl) {
helpEl.textContent = modeCopy.help;
}
if (emptyNoteEl) {
emptyNoteEl.textContent = modeCopy.emptyNote;
}
if (browserTitleEl) {
browserTitleEl.textContent = modeCopy.browserTitle;
}
if (browserNoteEl) {
browserNoteEl.textContent = modeCopy.browserNote;
}
if (warningNoteEl) {
warningNoteEl.textContent = modeCopy.warningNote;
}
if (settingsLink) {
settingsLink.textContent = modeCopy.settingsButton;
}
if (nameRequiredTitleEl) {
nameRequiredTitleEl.textContent = modeCopy.nameRequiredTitle;
}
if (nameRequiredContinue) {
nameRequiredContinue.textContent = modeCopy.nameRequiredContinue;
}
if (listTitleEl) {
listTitleEl.textContent = modeCopy.listTitle;
}
if (listNoteEl) {
listNoteEl.textContent = modeCopy.listNote;
}
if (approvalTitleEl) {
approvalTitleEl.textContent = modeCopy.approvalTitle;
}
if (approvalNoteEl) {
approvalNoteEl.textContent = modeCopy.approvalNote;
}
if (unlockTitleEl) {
unlockTitleEl.textContent = modeCopy.unlockTitle;
}
if (unlockNoteEl) {
unlockNoteEl.textContent = modeCopy.unlockNote;
}
root.setAttribute("data-mode-active", sovereignMode);
}
function approvalConfirmLabel(showUnlockFields) {
if (advancedUnlockSettingsEnabled) {
return "Approve";
}
return showUnlockFields ? "Unlock and Continue" : "Continue";
}
function approvalBusyLabel(showUnlockFields) {
if (advancedUnlockSettingsEnabled) {
return "Approving...";
}
return showUnlockFields ? "Unlocking..." : "Continuing...";
}
function unlockConfirmLabel() {
return "Unlock";
}
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 setViewerNodeStatPillState(element, state) {
if (!element) {
return;
}
element.classList.remove(
"qortal-node-stat-ok",
"qortal-node-stat-warn",
"qortal-node-stat-error"
);
if (state === "ok") {
element.classList.add("qortal-node-stat-ok");
} else if (state === "warn") {
element.classList.add("qortal-node-stat-warn");
} else if (state === "error") {
element.classList.add("qortal-node-stat-error");
}
}
function setViewerNodeStatusDisplay(peers, dataPeers, unavailable) {
if (!viewerNodeStats) {
return;
}
if (viewerPeersValue) {
viewerPeersValue.textContent = unavailable
? "!"
: peers !== null
? String(peers)
: "—";
}
if (viewerDataPeersValue) {
viewerDataPeersValue.textContent = unavailable
? "!"
: dataPeers !== null
? String(dataPeers)
: "—";
}
if (unavailable) {
setViewerNodeStatPillState(viewerPeersPill, "error");
setViewerNodeStatPillState(viewerDataPeersPill, "error");
return;
}
setViewerNodeStatPillState(
viewerPeersPill,
peers === null ? null : peers < 3 ? "error" : "ok"
);
setViewerNodeStatPillState(
viewerDataPeersPill,
dataPeers === null ? null : dataPeers < 3 ? "error" : "ok"
);
}
function findNodeStatusField(source, keys, depth) {
if (!source || typeof source !== "object") {
return null;
}
if (!Array.isArray(keys) || keys.length === 0) {
return null;
}
const limit = typeof depth === "number" && depth > 0 ? depth : 3;
const queue = [{ value: source, level: 0 }];
const seen = new Set();
while (queue.length > 0) {
const next = queue.shift();
if (!next || !next.value || typeof next.value !== "object") {
continue;
}
if (seen.has(next.value)) {
continue;
}
seen.add(next.value);
for (let i = 0; i < keys.length; i += 1) {
const key = keys[i];
if (Object.prototype.hasOwnProperty.call(next.value, key)) {
return next.value[key];
}
}
if (next.level >= limit) {
continue;
}
Object.keys(next.value).forEach(function (childKey) {
const child = next.value[childKey];
if (child && typeof child === "object") {
queue.push({ value: child, level: next.level + 1 });
}
});
}
return null;
}
function parseNodeStatusInt(value) {
const parsed = coerceFiniteNumber(value);
if (parsed === null) {
return null;
}
return Math.max(0, Math.floor(parsed));
}
function stopViewerNodeStatusPolling() {
if (viewerNodeStatusPollTimer) {
window.clearInterval(viewerNodeStatusPollTimer);
viewerNodeStatusPollTimer = null;
}
}
async function refreshViewerNodeStatus(force) {
if (!viewerCard || viewerCard.classList.contains("qortal-hidden")) {
return;
}
const now = Date.now();
if (
!force &&
viewerNodeStatusLastFetchAt &&
now - viewerNodeStatusLastFetchAt < 8000
) {
return;
}
viewerNodeStatusLastFetchAt = now;
try {
const result = await postBrokerQortalRequest("GET_NODE_STATUS", {});
const peers = parseNodeStatusInt(
findNodeStatusField(
result,
[
"peers",
"peerCount",
"numberOfConnections",
"connectedPeers",
"connections",
],
4
)
);
const dataPeers = parseNodeStatusInt(
findNodeStatusField(
result,
[
"numberOfDataConnections",
"dataPeers",
"dataPeerCount",
"numberOfDataPeers",
"connectedDataPeers",
],
4
)
);
setViewerNodeStatusDisplay(peers, dataPeers, false);
} catch (_error) {
setViewerNodeStatusDisplay(null, null, true);
}
}
function startViewerNodeStatusPolling() {
if (!viewerNodeStats) {
return;
}
stopViewerNodeStatusPolling();
refreshViewerNodeStatus(true).catch(function () {
// ignored
});
viewerNodeStatusPollTimer = window.setInterval(function () {
refreshViewerNodeStatus(false).catch(function () {
// ignored
});
}, 20000);
}
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 modeCopy.loadingFromQdn;
}
function isTerminalQdnStatus(status) {
const normalizedStatus = String(status || "")
.trim()
.toUpperCase();
return (
normalizedStatus === "NOT_PUBLISHED" ||
normalizedStatus === "UNSUPPORTED" ||
normalizedStatus === "BLOCKED"
);
}
function updateQdnLoadingProgress(
status,
percentLoaded,
localChunkCount,
totalChunkCount,
description
) {
const normalizedStatus = String(status || "").toUpperCase();
if (normalizedStatus === "READY") {
clearQdnLoadingProgress();
return;
}
const localCount = coerceFiniteNumber(localChunkCount);
const totalCount = coerceFiniteNumber(totalChunkCount);
// Some QDN status responses expose per-chunk percent (0..100) without aggregate totals,
// which causes visible 0/100 flicker while multiple chunks download. In that case,
// prefer an overall "loading/chunks" indicator instead of a percent bar.
const shouldUsePercent =
normalizedStatus === "READY" || (totalCount !== null && totalCount > 0);
const boundedPercent = shouldUsePercent
? coerceFiniteNumber(percentLoaded)
: null;
const bounded =
boundedPercent !== null
? Math.max(0, Math.min(100, boundedPercent))
: null;
if (!viewerLoadingProgress) {
return;
}
let toolbarLabel = modeCopy.loadingLabel;
if (bounded !== null) {
toolbarLabel = "Loading " + bounded.toFixed(1) + "%";
}
if (localCount !== null && String(toolbarLabel).indexOf("%") === -1) {
toolbarLabel += " (" + localCount + " 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 = localCount;
const total = totalCount;
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;
let terminalMissingResult = null;
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;
}
const normalizedStatus = String(parsed.status || "").toUpperCase();
if (
normalizedStatus === "NOT_PUBLISHED" ||
normalizedStatus === "UNSUPPORTED" ||
normalizedStatus === "BLOCKED"
) {
// Deep links inside a Q-App can look like an "identifier" on first probe candidate.
// Keep trying alternate status URLs (especially without identifier) before concluding missing.
if (!terminalMissingResult) {
terminalMissingResult = {
ok: true,
status: normalizedStatus,
description: parsed.description || "",
percentLoaded: coerceFiniteNumber(parsed.percentLoaded),
localChunkCount: coerceFiniteNumber(parsed.localChunkCount),
totalChunkCount: coerceFiniteNumber(parsed.totalChunkCount),
url,
};
}
continue;
}
return {
ok: true,
status: normalizedStatus,
description: parsed.description || "",
percentLoaded: coerceFiniteNumber(parsed.percentLoaded),
localChunkCount: coerceFiniteNumber(parsed.localChunkCount),
totalChunkCount: coerceFiniteNumber(parsed.totalChunkCount),
url,
};
} catch (_error) {
// Try the next candidate URL.
}
}
if (terminalMissingResult) {
return terminalMissingResult;
}
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) {
if (isTerminalQdnStatus(prefetchedStatusResult.status)) {
clearQdnLoadingRetry("content_loaded_terminal_status");
qdnLoadingRetryAttempts = 0;
return;
}
updateQdnLoadingProgress(
prefetchedStatusResult.status,
prefetchedStatusResult.percentLoaded,
prefetchedStatusResult.localChunkCount,
prefetchedStatusResult.totalChunkCount,
prefetchedStatusResult.description
);
if (prefetchedStatusResult.status === "READY") {
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") {
if (detection.detected) {
logDebug(
"info",
"QDN resource ready while loading interstitial is visible, refreshing viewer",
{
attempt: qdnLoadingRetryAttempts,
}
);
reloadViewerFrame();
} else {
clearQdnLoadingRetry("content_loaded_ready");
qdnLoadingRetryAttempts = 0;
}
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;
}
const spendingRequestTypes = new Set([
"SIGN_TRANSACTION",
"REGISTER_NAME",
"UPDATE_NAME",
"SELL_NAME",
"CANCEL_SELL_NAME",
"BUY_NAME",
"SEND_COIN",
"SEND_COIN_MULTIPLE",
"DEPLOY_AT",
"CREATE_POLL",
"VOTE_ON_POLL",
"CREATE_TRADE_BUY_ORDER",
"CANCEL_TRADE_BUY_ORDER",
"CREATE_ASSET_ORDER",
"CANCEL_ASSET_ORDER",
"TRANSFER_ASSET",
"PUBLISH_QDN_RESOURCE",
"PUBLISH_MULTIPLE_QDN_RESOURCES",
"ADD_LIST_ITEMS",
"DELETE_LIST_ITEM",
]);
function isLikelySpendingRequestType(requestType) {
const normalized = String(requestType || "")
.trim()
.toUpperCase();
if (!normalized) {
return false;
}
if (spendingRequestTypes.has(normalized)) {
return true;
}
if (normalized.startsWith("PUBLISH_") || normalized.startsWith("SEND_")) {
return true;
}
if (normalized.includes("TRADE") || normalized.includes("BUY_ORDER")) {
return true;
}
return false;
}
function isHighRiskConfirmationRequired(error, json) {
if (error === "high_risk_confirmation_required") {
return true;
}
return Boolean(
json &&
typeof json.error === "string" &&
json.error === "high_risk_confirmation_required"
);
}
function highRiskConfirmationMessage(requestType, details) {
const info = details && typeof details === "object" ? details : {};
const lines = [
"NuQloud blocked this request for an extra safety check.",
"",
"Action: " + String(info.requestType || requestType || "Q-App request"),
];
if (info.reason) {
lines.push("Reason: " + String(info.reason));
}
if (info.amount) {
lines.push("Amount: " + String(info.amount));
}
if (info.recipient) {
lines.push("Recipient: " + String(info.recipient));
}
if (info.txCount) {
lines.push("Estimated transactions: " + String(info.txCount));
}
if (info.feeCredits) {
lines.push(
"Estimated publish fees: " +
String(info.feeCredits) +
" credits" +
(info.feeQort ? " (" + String(info.feeQort) + " QORT)" : "")
);
}
lines.push("", "Continue only if you expected this request.");
return lines.join("\n");
}
async function postQappsRequestWithHighRiskConfirmation(
requestType,
payload,
headers
) {
let workingPayload = Object.assign({}, payload || {});
const postOnce = async function () {
const response = await fetchWithRetry(
qappsRequestUrl,
{
method: "POST",
headers,
body: JSON.stringify({
requestType,
payload: workingPayload,
}),
},
2,
250
);
const json = await response.json();
return { response, json };
};
let outcome = await postOnce();
if (
!outcome.response.ok ||
outcome.json.ok === false ||
outcome.json.error
) {
const errorMessage = outcome.json.error || "qortal_request_failed";
if (isHighRiskConfirmationRequired(errorMessage, outcome.json)) {
const details =
outcome.json && outcome.json.details ? outcome.json.details : {};
const token =
details && typeof details.confirmationToken === "string"
? details.confirmationToken
: "";
if (!token) {
return {
response: outcome.response,
json: { error: "high_risk_confirmation_unavailable", details },
payload: workingPayload,
};
}
const accepted = window.confirm(
highRiskConfirmationMessage(requestType, details)
);
if (!accepted) {
return {
response: outcome.response,
json: { error: "high_risk_confirmation_cancelled", details },
payload: workingPayload,
};
}
workingPayload = Object.assign({}, workingPayload, {
highRiskConfirmationToken: token,
});
outcome = await postOnce();
}
}
return {
response: outcome.response,
json: outcome.json,
payload: workingPayload,
};
}
function extractSignedTransactionPayload(value, depth) {
const currentDepth = typeof depth === "number" ? depth : 0;
if (currentDepth > 6 || value === null || value === undefined) {
return "";
}
if (typeof value === "string") {
const trimmed = value.trim();
if (!trimmed) {
return "";
}
if (
(trimmed[0] === "{" || trimmed[0] === "[") &&
trimmed.length <= 500000
) {
try {
const parsed = JSON.parse(trimmed);
const parsedValue = extractSignedTransactionPayload(
parsed,
currentDepth + 1
);
if (parsedValue) {
return parsedValue;
}
} catch (_error) {
// Ignore JSON parse failure and treat as plain payload.
}
}
return trimmed;
}
if (Array.isArray(value)) {
for (let i = 0; i < value.length; i += 1) {
const candidate = extractSignedTransactionPayload(
value[i],
currentDepth + 1
);
if (candidate) {
return candidate;
}
}
return "";
}
if (typeof value !== "object") {
return "";
}
const candidateKeys = [
"signedTransaction",
"signedTransactionBytes",
"transactionBytes",
"signedBytes",
"bytes",
"data",
"result",
"payload",
"transaction",
];
for (let i = 0; i < candidateKeys.length; i += 1) {
const key = candidateKeys[i];
if (!Object.prototype.hasOwnProperty.call(value, key)) {
continue;
}
const candidate = extractSignedTransactionPayload(
value[key],
currentDepth + 1
);
if (candidate) {
return candidate;
}
}
for (const key in value) {
if (!Object.prototype.hasOwnProperty.call(value, key)) {
continue;
}
const nested = extractSignedTransactionPayload(
value[key],
currentDepth + 1
);
if (nested) {
return nested;
}
}
return "";
}
function normalizeQappsResultPayload(requestType, resultPayload) {
if (
requestType === "GET_PRIMARY_NAME" &&
typeof resultPayload !== "string"
) {
if (Array.isArray(resultPayload)) {
return resultPayload.length > 0 ? resultPayload[0] : "";
}
if (resultPayload && typeof resultPayload === "object") {
return (
resultPayload.name ||
resultPayload.primaryName ||
(resultPayload.data && resultPayload.data.name) ||
""
);
}
if (resultPayload === null || resultPayload === undefined) {
return "";
}
}
if (requestType === "SIGN_TRANSACTION") {
const signedPayload = extractSignedTransactionPayload(resultPayload, 0);
if (signedPayload) {
return signedPayload;
}
}
return resultPayload;
}
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 (!effectiveDebugEnabled || !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;
}
function getViewportHeight() {
return (
(window.visualViewport && window.visualViewport.height) ||
window.innerHeight ||
document.documentElement.clientHeight ||
0
);
}
function clampDebugPanelHeight(value) {
const numeric = Math.round(Number(value));
if (!Number.isFinite(numeric)) {
return debugPanelDefaultHeight;
}
return Math.max(debugPanelMinHeight, numeric);
}
function readStoredDebugPanelHeight() {
if (!window.localStorage) {
return debugPanelExpandedHeight;
}
const parsed = Number(
window.localStorage.getItem(debugPanelHeightStorageKey)
);
return clampDebugPanelHeight(parsed);
}
function applyDebugPanelSizing() {
if (!debugCard) {
return;
}
const compactHeight = Math.max(
120,
Math.round(debugPanelExpandedHeight * debugPanelCompactRatio)
);
debugCard.style.setProperty(
"--qortal-debug-panel-height",
debugPanelExpandedHeight + "px"
);
debugCard.style.setProperty(
"--qortal-debug-panel-compact-height",
compactHeight + "px"
);
debugCard.style.setProperty(
"--qortal-debug-panel-peek-height",
debugPanelPeekHeight + "px"
);
}
function setDebugPanelExpandedHeight(nextHeight, persist) {
debugPanelExpandedHeight = clampDebugPanelHeight(nextHeight);
applyDebugPanelSizing();
if (persist && window.localStorage) {
window.localStorage.setItem(
debugPanelHeightStorageKey,
String(debugPanelExpandedHeight)
);
}
}
function setDebugDockHidden(hidden, persist) {
if (!debugCard) {
return;
}
debugCard.classList.toggle("qortal-debug-docked-hidden", Boolean(hidden));
if (!hidden) {
releaseDebugDockHoverSuppression();
}
if (persist && window.localStorage) {
window.localStorage.setItem(debugPanelDockStorageKey, hidden ? "1" : "0");
}
updateDebugPanelControls();
}
function suppressDebugDockHover() {
if (!debugCard) {
return;
}
debugDockHoverSuppressed = true;
debugCard.classList.add("qortal-debug-dock-suppressed");
}
function releaseDebugDockHoverSuppression() {
if (!debugCard) {
return;
}
debugDockHoverSuppressed = false;
debugCard.classList.remove("qortal-debug-dock-suppressed");
}
function syncDebugPanelState() {
if (!debugCard || !effectiveDebugEnabled) {
return;
}
setDebugPanelExpandedHeight(readStoredDebugPanelHeight(), false);
setDebugDockHidden(
window.localStorage &&
window.localStorage.getItem(debugPanelDockStorageKey) === "1",
false
);
updateDebugPanelControls();
}
function updateDebugPanelControls() {
if (!debugCard) {
return;
}
if (debugMinimize) {
const isMinimized = debugCard.classList.contains(
"qortal-debug-minimized"
);
const minimizeLabel = isMinimized
? "Expand debug panel"
: "Minimize debug panel";
debugMinimize.textContent = "";
debugMinimize.setAttribute("title", minimizeLabel);
debugMinimize.setAttribute("aria-label", minimizeLabel);
debugMinimize.setAttribute("aria-pressed", isMinimized ? "true" : "false");
}
if (debugFloat) {
const isDockHidden = debugCard.classList.contains(
"qortal-debug-docked-hidden"
);
const dockLabel = isDockHidden
? "Keep debug panel open"
: "Auto-hide debug panel";
debugFloat.textContent = isDockHidden ? "⇧" : "⤓";
debugFloat.setAttribute("title", dockLabel);
debugFloat.setAttribute("aria-label", dockLabel);
debugFloat.setAttribute("aria-pressed", isDockHidden ? "true" : "false");
}
}
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();
let fromQortalScheme = false;
let suffix = "";
if (/^https?:\/\//i.test(path)) {
return appendThemeParams(path);
}
if (path.toLowerCase().startsWith("qortal://")) {
fromQortalScheme = true;
path = path.slice("qortal://".length);
}
if (fromQortalScheme) {
const queryIndex = path.indexOf("?");
const hashIndex = path.indexOf("#");
let splitIndex = -1;
if (queryIndex >= 0 && hashIndex >= 0) {
splitIndex = Math.min(queryIndex, hashIndex);
} else if (queryIndex >= 0) {
splitIndex = queryIndex;
} else if (hashIndex >= 0) {
splitIndex = hashIndex;
}
if (splitIndex >= 0) {
suffix = path.slice(splitIndex);
path = path.slice(0, splitIndex);
}
}
path = path.replace(/^\/+/, "");
if (ensureTrailingSlash && path && !path.endsWith("/")) {
let shouldAppendSlash = true;
if (fromQortalScheme) {
const basePath = stripQueryAndHash(path);
const segments = basePath.split("/").filter(function (segment) {
return Boolean(segment);
});
// Preserve SPA/app sub-routes like qortal://APP/Q-Assets/assets (no forced trailing slash).
// Only force a slash for root-style QDN addresses (service + name only).
if (segments.length > 2) {
shouldAppendSlash = false;
}
}
if (shouldAppendSlash) {
path += "/";
}
}
const upstreamPath = toUpstreamProxyPath(path, true);
if (gatewayProxyTemplate) {
if (!viewerRendererConfigured) {
return "";
}
return appendThemeParams(buildProxyUrl(upstreamPath) + suffix);
}
const directBase = nodeUrl
? nodeUrl.replace(/\/$/, "")
: gatewayUrl.replace(/\/$/, "");
if (!directBase) {
return "";
}
const base = directBase.replace(/\/$/, "");
return appendThemeParams(base + "/" + upstreamPath + suffix);
}
function normalizeBrowserAddress(address) {
const raw = String(address || "").trim();
if (!raw) {
return "";
}
if (!raw.toLowerCase().startsWith("qortal://")) {
return raw;
}
const splitIndex = (function () {
const queryIndex = raw.indexOf("?");
const hashIndex = raw.indexOf("#");
if (queryIndex === -1) {
return hashIndex;
}
if (hashIndex === -1) {
return queryIndex;
}
return Math.min(queryIndex, hashIndex);
})();
const base = splitIndex >= 0 ? raw.slice(0, splitIndex) : raw;
const suffix = splitIndex >= 0 ? raw.slice(splitIndex) : "";
const afterScheme = base.slice("qortal://".length).replace(/^\/+/, "");
if (!afterScheme) {
return raw;
}
// Browser convenience: qortal://Name -> qortal://WEBSITE/Name
if (!afterScheme.includes("/")) {
return "qortal://WEBSITE/" + afterScheme;
}
const segments = afterScheme
.split("/")
.filter(function (segment) {
return Boolean(segment);
})
.map(safeDecodeURIComponent);
let changed = true;
while (changed) {
changed = false;
if (
segments.length >= 5 &&
String(segments[2] || "").toLowerCase() === "apps" &&
String(segments[3] || "").toLowerCase() === "qortal_integration" &&
String(segments[4] || "").toLowerCase() === "gateway"
) {
segments.splice(2, 3);
changed = true;
continue;
}
if (
segments.length >= 5 &&
String(segments[2] || "").toLowerCase() === "render" &&
String(segments[3] || "") === String(segments[0] || "") &&
String(segments[4] || "") === String(segments[1] || "")
) {
segments.splice(2, 3);
changed = true;
continue;
}
}
return "qortal://" + segments.join("/") + suffix;
}
function detectCloudTheme() {
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 String(dataTheme).toLowerCase().includes("dark")
? "dark"
: "light";
}
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 detectCloudLanguage() {
return (
(document.documentElement && document.documentElement.lang) ||
navigator.language ||
""
);
}
function applyEmbedThemeParams(address) {
const normalized = normalizeBrowserAddress(address || "");
if (!normalized.toLowerCase().startsWith("qortal://")) {
return normalized;
}
const hashParts = normalized.split("#");
const withoutHash = hashParts[0] || "";
const hash = hashParts.length > 1 ? "#" + hashParts.slice(1).join("#") : "";
const queryIndex = withoutHash.indexOf("?");
const base =
queryIndex >= 0 ? withoutHash.slice(0, queryIndex) : withoutHash;
const query = queryIndex >= 0 ? withoutHash.slice(queryIndex + 1) : "";
const params = new URLSearchParams(query);
params.set("theme", detectCloudTheme());
const lang = detectCloudLanguage();
if (lang) {
params.set("lang", lang);
}
const nextQuery = params.toString();
return base + (nextQuery ? "?" + nextQuery : "") + hash;
}
function extractQappTargetKey(address) {
const normalized = normalizeBrowserAddress(address || "");
if (!normalized.toLowerCase().startsWith("qortal://")) {
return "";
}
const withoutScheme = normalized.slice("qortal://".length);
const withoutQuery = withoutScheme.split("#")[0].split("?")[0];
const segments = withoutQuery.split("/").filter(function (segment) {
return Boolean(segment);
});
if (segments.length < 2) {
return "";
}
return (
String(segments[0]).toUpperCase() +
"/" +
String(segments[1]).toUpperCase()
);
}
function isEmbedLockedToDifferentQapp(address) {
if (!embedLockQapp || !embedRequestedQapp) {
return false;
}
const lockedKey = extractQappTargetKey(embedRequestedQapp);
const candidateKey = extractQappTargetKey(address);
if (!lockedKey || !candidateKey) {
return false;
}
return lockedKey !== candidateKey;
}
function splitPathAndSuffix(value) {
const raw = String(value || "").trim();
if (!raw) {
return { path: "", suffix: "" };
}
const queryIndex = raw.indexOf("?");
const hashIndex = raw.indexOf("#");
let splitIndex = -1;
if (queryIndex >= 0 && hashIndex >= 0) {
splitIndex = Math.min(queryIndex, hashIndex);
} else if (queryIndex >= 0) {
splitIndex = queryIndex;
} else if (hashIndex >= 0) {
splitIndex = hashIndex;
}
if (splitIndex < 0) {
return { path: raw, suffix: "" };
}
return {
path: raw.slice(0, splitIndex),
suffix: raw.slice(splitIndex),
};
}
function parseCurrentQappBaseAddress() {
const source = normalizeBrowserAddress(
currentViewerAddress || currentQappAddress || ""
);
if (!source || !source.toLowerCase().startsWith("qortal://")) {
return null;
}
const rawPath = stripQueryAndHash(source.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;
}
return {
service: String(segments[0] || "")
.trim()
.toUpperCase(),
name: String(segments[1] || "").trim(),
};
}
function resolveRelativeAddressForCurrentQapp(value) {
const split = splitPathAndSuffix(value);
let path = String(split.path || "").trim();
if (
!path ||
/^https?:\/\//i.test(path) ||
path.toLowerCase().startsWith("qortal://")
) {
return "";
}
path = path.replace(/^\/+/, "");
if (!path) {
return "";
}
const gatewayPrefix = "apps/qortal_integration/gateway/";
const lowerPath = path.toLowerCase();
if (lowerPath.startsWith(gatewayPrefix)) {
path = path.slice(gatewayPrefix.length);
} else if (lowerPath.startsWith("qortal_integration/gateway/")) {
path = path.slice("qortal_integration/gateway/".length);
} else {
const embeddedGatewayIndex = lowerPath.indexOf("/" + gatewayPrefix);
if (embeddedGatewayIndex >= 0) {
path = path.slice(embeddedGatewayIndex + gatewayPrefix.length + 1);
}
}
if (!path) {
return "";
}
if (path.toLowerCase().startsWith("render/")) {
return normalizeBrowserAddress(
"qortal://" + path.slice("render/".length) + split.suffix
);
}
const segments = path
.split("/")
.filter(function (segment) {
return Boolean(segment);
})
.map(safeDecodeURIComponent);
if (segments.length < 1) {
return "";
}
if (
segments.length >= 2 &&
/^[A-Z0-9_]+$/.test(String(segments[0] || ""))
) {
return normalizeBrowserAddress(
"qortal://" + segments.join("/") + split.suffix
);
}
const base = parseCurrentQappBaseAddress();
if (!base || !base.service || !base.name) {
return "";
}
return normalizeBrowserAddress(
"qortal://" +
base.service +
"/" +
base.name +
"/" +
segments.join("/") +
split.suffix
);
}
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;
}
const suffix = splitPathAndSuffix(normalized).suffix || "";
if (service === "APP" || service === "WEBSITE") {
return "qortal://" + service + "/" + name + suffix;
}
return "qortal://" + segments.join("/") + suffix;
}
function buildReloadSequence(address) {
const normalized = normalizeBrowserAddress(address || "");
if (!normalized) {
return null;
}
const split = splitPathAndSuffix(normalized);
const fullAddress = normalizeBrowserAddress(split.path + (split.suffix || ""));
const baseAddress = deriveSafeReloadAddress(split.path);
const finalAddress = deriveSafeReloadAddress(fullAddress);
const needsTwoStep = Boolean(split.suffix) && Boolean(baseAddress) && baseAddress !== finalAddress;
return {
fullAddress,
baseAddress: baseAddress || finalAddress || fullAddress,
finalAddress: finalAddress || fullAddress,
needsTwoStep,
};
}
let pendingReloadFinalAddress = "";
let pendingReloadFallbackAddress = "";
let pendingReloadTimeout = null;
let launchRetryAddress = "";
let launchRetryCount = 0;
const launchRetryMax = 1;
function clearPendingReloadSequence() {
pendingReloadFinalAddress = "";
pendingReloadFallbackAddress = "";
if (pendingReloadTimeout) {
window.clearTimeout(pendingReloadTimeout);
pendingReloadTimeout = null;
}
}
function startViewerReloadSequence(address) {
const sequence = buildReloadSequence(address);
if (!sequence) {
return false;
}
clearPendingReloadSequence();
if (!sequence.needsTwoStep) {
openAddress(sequence.finalAddress, modeCopy.browserName);
return true;
}
pendingReloadFinalAddress = sequence.finalAddress;
pendingReloadFallbackAddress = sequence.baseAddress;
openAddress(sequence.baseAddress, modeCopy.browserName);
pendingReloadTimeout = window.setTimeout(function () {
if (!pendingReloadFinalAddress) {
return;
}
const finalTarget = pendingReloadFinalAddress;
clearPendingReloadSequence();
openAddress(finalTarget, modeCopy.browserName);
}, 300);
return true;
}
function isGatewayProxyViewerUrl(url) {
const value = String(url || "").toLowerCase();
return (
value.indexOf("/apps/qortal_integration/gateway/") !== -1 ||
value.indexOf("/index.php/apps/qortal_integration/gateway/") !== -1
);
}
function scheduleOneTimeLaunchRetry(reason) {
const candidate = normalizeBrowserAddress(
currentViewerAddress || currentQappAddress || ""
);
if (!candidate) {
return false;
}
if (launchRetryAddress !== candidate) {
launchRetryAddress = candidate;
launchRetryCount = 0;
}
if (launchRetryCount >= launchRetryMax) {
return false;
}
launchRetryCount += 1;
logDebug("warn", "Retrying initial Q-App load once", {
reason: reason || "initial_load_failure",
address: candidate,
retryCount: launchRetryCount,
});
window.setTimeout(function () {
openAddress(candidate, modeCopy.browserName);
}, 500);
return true;
}
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);
} else {
const resolvedRelative = resolveRelativeAddressForCurrentQapp(
"/" + upstreamPath + parsed.search + parsed.hash
);
if (resolvedRelative) {
return resolvedRelative;
}
}
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("/")) {
const parsedFromViewer = parseQortalAddressFromViewerUrl(
window.location.origin + trimmed
);
if (parsedFromViewer) {
return parsedFromViewer;
}
const resolvedRelative = resolveRelativeAddressForCurrentQapp(trimmed);
if (resolvedRelative) {
return resolvedRelative;
}
return normalizeBrowserAddress(
"qortal://" + trimmed.replace(/^\/+/, "")
);
}
if (/^[a-z][a-z0-9+.-]*:\/\//i.test(trimmed)) {
const parsed = parseQortalAddressFromViewerUrl(trimmed);
if (parsed) {
return parsed;
}
} else {
const resolvedRelative = resolveRelativeAddressForCurrentQapp(trimmed);
if (resolvedRelative) {
return resolvedRelative;
}
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 routeQortalProtocolTarget(rawTarget, label) {
const normalizedTarget = normalizeBrowserAddress(
String(rawTarget || "").trim()
);
if (
!normalizedTarget ||
!normalizedTarget.toLowerCase().startsWith("qortal://")
) {
return false;
}
openAddress(normalizedTarget, label || modeCopy.browserName);
return true;
}
function installViewerNavigationInterceptors() {
if (!viewerFrame || !viewerFrame.contentWindow) {
return;
}
const frameWindow = viewerFrame.contentWindow;
let frameDocument = null;
try {
frameDocument = frameWindow.document;
} catch (_error) {
return;
}
if (!frameDocument) {
return;
}
if (!frameWindow.__qortalNativeOpenInterceptorInstalled) {
const originalOpen =
typeof frameWindow.open === "function"
? frameWindow.open.bind(frameWindow)
: null;
frameWindow.open = function (url, target, features) {
if (routeQortalProtocolTarget(url, modeCopy.browserName)) {
logDebug("info", "Iframe window.open routed to integrated viewer", {
url: String(url || ""),
target: typeof target === "string" ? target : "",
});
return frameWindow;
}
if (originalOpen) {
return originalOpen(url, target, features);
}
return null;
};
frameWindow.__qortalNativeOpenInterceptorInstalled = true;
}
if (frameDocument.__qortalNativeLinkInterceptorInstalled) {
return;
}
frameDocument.addEventListener(
"click",
function (event) {
const target =
event.target instanceof Element
? event.target.closest("a[href]")
: null;
if (!target) {
return;
}
const href = target.getAttribute("href") || "";
if (
!href ||
!String(href).trim().toLowerCase().startsWith("qortal://")
) {
return;
}
event.preventDefault();
event.stopPropagation();
routeQortalProtocolTarget(
href,
target.textContent || modeCopy.browserName
);
},
true
);
frameDocument.__qortalNativeLinkInterceptorInstalled = true;
}
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() {
return buildDashboardHighlightUrl(["register-name"]);
}
function buildDashboardHighlightUrl(highlights) {
const base = (
nextcloudPublicUrl ||
window.location.origin +
(typeof OC !== "undefined" && OC.webroot ? OC.webroot : "")
).replace(/\/$/, "");
const validHighlights = Array.isArray(highlights)
? highlights
.map(function (entry) {
return String(entry || "")
.trim()
.toLowerCase();
})
.filter(function (entry) {
return entry !== "";
})
: [];
const query =
validHighlights.length > 0
? "?highlight=" + encodeURIComponent(validHighlights.join(","))
: "";
return base + "/apps/qortal_integration/account" + query;
}
function extractBalanceAmount(balance) {
const pickAmount = function (value) {
if (value === undefined || value === null || value === "") {
return undefined;
}
if (typeof value === "number" || typeof value === "string") {
return value;
}
if (!value || typeof value !== "object") {
return undefined;
}
return (
value.balance ??
value.confirmedBalance ??
value.confirmed ??
value.availableBalance ??
value.amount ??
value.value ??
undefined
);
};
if (Array.isArray(balance)) {
for (let i = 0; i < balance.length; i += 1) {
const amount = pickAmount(balance[i]);
if (amount !== undefined) {
return amount;
}
}
return undefined;
}
return pickAmount(balance);
}
function parseBalanceQort(value) {
const amount = extractBalanceAmount(value);
if (amount === undefined || amount === null || amount === "") {
return null;
}
const parsed = Number(amount);
return Number.isFinite(parsed) ? parsed : null;
}
async function fetchBalanceQort(address) {
const normalizedAddress = String(address || "").trim();
if (!normalizedAddress) {
return null;
}
const now = Date.now();
if (
balanceLookupCache.address === normalizedAddress &&
typeof balanceLookupCache.balanceQort === "number" &&
now - balanceLookupCache.checkedAt <= nameLookupCacheTtlMs
) {
return balanceLookupCache.balanceQort;
}
let balanceQort = null;
try {
balanceQort = parseBalanceQort(
await postBrokerQortalRequest("GET_WALLET_BALANCE", {
coin: "QORT",
address: normalizedAddress,
})
);
} catch (_error) {
balanceQort = null;
}
if (balanceQort === null) {
try {
balanceQort = parseBalanceQort(
await postBrokerQortalRequest("GET_BALANCE", {
address: normalizedAddress,
})
);
} catch (_error) {
balanceQort = null;
}
}
balanceLookupCache = {
address: normalizedAddress,
balanceQort: balanceQort,
checkedAt: now,
};
return balanceQort;
}
function buildIdentityPrerequisiteState(
hasWallet,
hasEnoughBalance,
hasName
) {
if (!hasWallet) {
return {
title: modeCopy.nameRequiredTitle,
intro:
"You have not yet created your " +
modeCopy.accountLabel +
". To use distributed authentication, communications, and data inside apps, you must create your account first.",
steps: [
modeCopy.createAccountStep,
modeCopy.fundingStep,
modeCopy.nameStep,
],
outro: modeCopy.prereqOutro,
highlights: ["create-account"],
};
}
if (!hasName && hasEnoughBalance === false) {
return {
title: modeCopy.nameRequiredTitle,
intro:
"Your account is created, but you do not yet have enough " +
modeCopy.unitLabel +
" to register a distributed identity name.",
steps: [modeCopy.fundingStep, modeCopy.nameStep],
outro: modeCopy.prereqOutro,
highlights: ["balance", "purchases", "register-name"],
};
}
if (!hasName) {
return {
title: modeCopy.nameRequiredTitle,
intro:
"Your account is created, but you do not yet have a registered distributed identity name.",
steps: [
"Open the dashboard and go to the name registration section.",
modeCopy.nameStep,
],
outro: modeCopy.prereqOutro,
highlights: ["register-name"],
};
}
return null;
}
function isTransientFetchError(error) {
const message =
error && error.message ? String(error.message) : String(error || "");
const normalized = message.toLowerCase();
return (
normalized.includes("failed to fetch") ||
normalized.includes("networkerror")
);
}
function sleep(ms) {
return new Promise(function (resolve) {
window.setTimeout(resolve, ms);
});
}
async function fetchWithRetry(url, options, attempts, delayMs) {
const maxAttempts = Number.isFinite(attempts)
? Math.max(1, Math.floor(attempts))
: 1;
const retryDelay = Number.isFinite(delayMs)
? Math.max(0, Math.floor(delayMs))
: 0;
let lastError = null;
for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
try {
return await fetch(url, options);
} catch (error) {
lastError = error;
if (!isTransientFetchError(error) || attempt >= maxAttempts) {
throw error;
}
await sleep(retryDelay);
}
}
throw lastError || new Error("fetch_failed");
}
async function postBrokerQortalRequest(requestType, payload) {
if (!qappsRequestUrl) {
throw new Error(modeCopy.requestEndpointMissing);
}
const headers = { "Content-Type": "application/json" };
if (typeof OC !== "undefined" && OC.requestToken) {
headers.requesttoken = OC.requestToken;
}
const outcome = await postQappsRequestWithHighRiskConfirmation(
requestType,
payload || {},
headers
);
const response = outcome.response;
const json = outcome.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(state) {
return new Promise(function (resolve) {
if (
!nameRequiredModal ||
!nameRequiredDashboardLink ||
!nameRequiredContinue ||
!nameRequiredCancel
) {
resolve("continue");
return;
}
const promptState = state && typeof state === "object" ? state : null;
const highlights =
promptState && Array.isArray(promptState.highlights)
? promptState.highlights
: ["register-name"];
const dashboardUrl = buildDashboardHighlightUrl(highlights);
if (nameRequiredTitleEl) {
nameRequiredTitleEl.textContent =
promptState && promptState.title
? promptState.title
: modeCopy.nameRequiredTitle;
}
if (nameRequiredNote1El) {
nameRequiredNote1El.textContent =
promptState && promptState.intro ? promptState.intro : "";
}
if (nameRequiredStep1El) {
nameRequiredStep1El.textContent =
promptState && promptState.steps && promptState.steps[0]
? promptState.steps[0]
: "";
nameRequiredStep1El.classList.toggle(
"qortal-hidden",
!nameRequiredStep1El.textContent
);
}
if (nameRequiredStep2El) {
nameRequiredStep2El.textContent =
promptState && promptState.steps && promptState.steps[1]
? promptState.steps[1]
: "";
nameRequiredStep2El.classList.toggle(
"qortal-hidden",
!nameRequiredStep2El.textContent
);
}
if (nameRequiredStep3El) {
nameRequiredStep3El.textContent =
promptState && promptState.steps && promptState.steps[2]
? promptState.steps[2]
: "";
nameRequiredStep3El.classList.toggle(
"qortal-hidden",
!nameRequiredStep3El.textContent
);
}
if (nameRequiredStepsEl) {
nameRequiredStepsEl.classList.toggle(
"qortal-hidden",
!promptState ||
!Array.isArray(promptState.steps) ||
promptState.steps.length === 0
);
}
if (nameRequiredNote2El) {
nameRequiredNote2El.textContent =
promptState && promptState.outro ? promptState.outro : "";
}
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) {
const decisionWithoutAccount = await showRegisteredNameRequiredModal(
buildIdentityPrerequisiteState(false, false, false)
);
if (decisionWithoutAccount === "dashboard") {
window.location.href = buildDashboardHighlightUrl(["create-account"]);
return false;
}
return decisionWithoutAccount === "continue";
}
const hasName = await hasRegisteredName(effectiveAddress);
if (hasName) {
return true;
}
const balanceQort = await fetchBalanceQort(effectiveAddress);
const hasEnoughBalance =
typeof balanceQort === "number" ? balanceQort >= 1.25 : null;
const decision = await showRegisteredNameRequiredModal(
buildIdentityPrerequisiteState(true, hasEnoughBalance, false)
);
if (decision === "dashboard") {
const highlights =
hasEnoughBalance === false
? ["balance", "purchases", "register-name"]
: ["register-name"];
window.location.href = buildDashboardHighlightUrl(highlights);
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" : "";
}
if (isOpen) {
startViewerNodeStatusPolling();
} else {
stopViewerNodeStatusPolling();
clearPendingReloadSequence();
}
}
function extractAppNameFromQortalAddress(address) {
const raw = String(address || "").trim();
if (!raw.toLowerCase().startsWith("qortal://")) {
return "";
}
const trimmed = raw.slice("qortal://".length).replace(/^\/+/, "");
if (!trimmed) {
return "";
}
const parts = trimmed.split("/").filter(Boolean);
if (!parts.length) {
return "";
}
const first = String(parts[0] || "").toUpperCase();
if ((first === "APP" || first === "WEBSITE") && parts.length > 1) {
return String(parts[1] || "").trim();
}
return String(parts[0] || "").trim();
}
function resolveQappIcon(entry) {
if (!entry || typeof entry !== "object") {
return "";
}
const iconMode = normalizeQappIconMode(entry.iconMode);
const iconUrl = String(entry.iconUrl || "").trim();
if (iconMode === "custom" && iconUrl) {
if (iconUrl.toLowerCase().startsWith("qortal://")) {
const iconPath = iconUrl.slice("qortal://".length).replace(/^\/+/, "");
const proxied = openGatewayPath(iconPath);
return proxied || "";
}
return iconUrl;
}
const appName =
iconMode === "name"
? String(entry.iconFromName || "").trim()
: extractAppNameFromQortalAddress(entry.address || "");
if (!appName) {
return "";
}
const thumbPath = "arbitrary/THUMBNAIL/" + appName + "/qortal_avatar";
return openGatewayPath(thumbPath);
}
function isViewerShowingApp() {
return Boolean(
viewerCard &&
!viewerCard.classList.contains("qortal-hidden") &&
currentQappAddress
);
}
function shouldPromptAppSwitch(targetAddress) {
if (!isViewerShowingApp()) {
return false;
}
const normalizedTarget = normalizeBrowserAddress(targetAddress || "");
const normalizedCurrent = normalizeBrowserAddress(
currentViewerAddress || currentQappAddress || ""
);
if (!normalizedTarget || !normalizedCurrent) {
return false;
}
const toIdentity = function (value) {
const raw = String(value || "")
.trim()
.replace(/^qortal:\/\//i, "")
.replace(/^\/+/, "");
const segments = raw.split(/[/?#]/)[0].split("/").filter(Boolean);
if (segments.length < 2) {
return raw.toLowerCase();
}
return (
String(segments[0] || "") +
"/" +
String(segments[1] || "")
).toLowerCase();
};
if (toIdentity(normalizedTarget) === toIdentity(normalizedCurrent)) {
return false;
}
return normalizedTarget.toLowerCase() !== normalizedCurrent.toLowerCase();
}
function closeSwitchModal(choice) {
if (switchModal) {
switchModal.classList.add("qortal-hidden");
}
if (switchModalResolver) {
const resolve = switchModalResolver;
switchModalResolver = null;
resolve(choice || "cancel");
}
}
function isMobileSwitchPromptMode() {
const viewportWidth =
(window.visualViewport && window.visualViewport.width) ||
window.innerWidth ||
document.documentElement.clientWidth ||
0;
return Boolean(viewportWidth && viewportWidth <= 780);
}
function openSwitchModal(targetAddress, targetName, mode) {
if (
!switchModal ||
!switchModalOpenHereButton ||
!switchModalOpenNewTabButton ||
!switchModalCancelButton
) {
return Promise.resolve("same");
}
if (switchModalResolver) {
const priorResolve = switchModalResolver;
switchModalResolver = null;
priorResolve("cancel");
}
if (switchModalNoteEl) {
const displayName = String(
targetName ||
extractAppNameFromQortalAddress(targetAddress) ||
targetAddress ||
"selected destination"
);
if (mode === 'leave') {
switchModalNoteEl.textContent =
'Opening "' +
displayName +
'" here will close your current app in this tab.';
} else {
switchModalNoteEl.textContent =
'Opening "' +
displayName +
'" here will replace your current app in this tab.';
}
}
if (isMobileSwitchPromptMode()) {
const displayName = String(
targetName ||
extractAppNameFromQortalAddress(targetAddress) ||
targetAddress ||
"selected destination"
);
const template =
mode === "leave"
? modeCopy.mobileLeaveNote
: modeCopy.mobileSwitchNote;
const message = String(template || "")
.replace("{name}", displayName)
.trim();
setMobileMenuOpen(false, false);
return Promise.resolve(window.confirm(message) ? "same" : "cancel");
}
switchModal.classList.remove("qortal-hidden");
return new Promise(function (resolve) {
switchModalResolver = resolve;
});
}
function buildQappsPageUrl(address) {
const normalizedAddress = normalizeBrowserAddress(address || "");
if (!normalizedAddress) {
return "";
}
const url = new URL(window.location.href);
url.searchParams.set("qapp", normalizedAddress);
return url.toString();
}
async function openAddress(address, name) {
if (!address || !viewerCard || !viewerFrame || !viewerAddress) {
return;
}
const allowedToOpen = await ensureRegisteredNameBeforeOpening();
if (!allowedToOpen) {
return;
}
const relativeResolvedAddress =
resolveRelativeAddressForCurrentQapp(address);
let normalizedAddress = normalizeBrowserAddress(
relativeResolvedAddress || address
);
if (embedMode) {
normalizedAddress = applyEmbedThemeParams(normalizedAddress);
}
if (isEmbedLockedToDifferentQapp(normalizedAddress)) {
const pageUrl = buildQappsPageUrl(normalizedAddress);
if (pageUrl) {
window.open(pageUrl, "_blank", "noopener");
}
return;
}
const resolved = resolveGatewayAddress(normalizedAddress, true);
if (!resolved) {
window.alert(modeCopy.nodeMissingAlert);
return;
}
currentQappAddress = normalizedAddress;
currentQdnResourceTarget = parseQdnResourceTarget(normalizedAddress);
logDebug("info", "Opening Q-App", { address: normalizedAddress, resolved });
if (launchRetryAddress !== normalizedAddress) {
launchRetryAddress = normalizedAddress;
launchRetryCount = 0;
}
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");
}
viewerCard.classList.remove("qortal-hidden");
// Keep the viewer visible before assigning src to avoid lazy/hidden first-load edge cases.
try {
viewerFrame.setAttribute("loading", "eager");
} catch (_error) {
// Ignore unsupported attribute updates.
}
viewerFrame.src = resolved;
setViewerOpen(true);
setMobileMenuOpen(false, false);
if (debugCard && effectiveDebugEnabled) {
syncDebugPanelState();
}
resizeViewerFrame();
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;
clearPendingReloadSequence();
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
}
};
}
const handleViewerReload = function () {
const preferredAddress =
currentViewerAddress || currentQappAddress || "";
if (
preferredAddress &&
preferredAddress.toLowerCase().startsWith("qortal://")
) {
logDebug("info", "Reload routed through safe sequence", {
currentViewerAddress: currentViewerAddress || "",
currentQappAddress: currentQappAddress || "",
reloadAddress: preferredAddress,
});
startViewerReloadSequence(preferredAddress);
return;
}
try {
if (viewerFrame.contentWindow) {
viewerFrame.contentWindow.location.reload();
}
} catch (_err) {
// ignore cross-origin issues
}
};
if (viewerReload) {
viewerReload.onclick = handleViewerReload;
}
if (viewerMobileRefresh) {
viewerMobileRefresh.onclick = handleViewerReload;
}
}
async function openAddressWithSwitchPrompt(address, name) {
if (!address) {
return;
}
const normalizedTarget = normalizeBrowserAddress(address);
if (!shouldPromptAppSwitch(normalizedTarget)) {
await openAddress(normalizedTarget, name);
return;
}
const choice = await openSwitchModal(normalizedTarget, name);
if (choice === "newtab") {
const pageUrl = buildQappsPageUrl(normalizedTarget);
if (pageUrl) {
window.open(pageUrl, "_blank", "noopener");
}
return;
}
if (choice === "same") {
await openAddress(normalizedTarget, name);
}
}
function resizeViewerFrame() {
if (!viewerFrameWrap) {
return;
}
const rect = viewerFrameWrap.getBoundingClientRect();
const rootRect = root.getBoundingClientRect();
const viewportHeight = getViewportHeight();
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 safeAreaBottom = isMobile
? Math.max(0, window.innerHeight - viewportHeight) + 2
: 8;
const padding = isMobile ? safeAreaBottom : 8;
const debugHeight =
debugCard && !debugCard.classList.contains("qortal-hidden")
? Math.ceil(debugCard.getBoundingClientRect().height)
: 0;
let nextHeight = Math.floor(
viewportHeight - rect.top - padding - debugHeight
);
if (
root.classList.contains(viewerOpenClass) &&
Number.isFinite(rootRect.bottom)
) {
const containerPadding = isMobile ? 4 : 8;
const containerBoundHeight = Math.floor(
rootRect.bottom - rect.top - containerPadding - debugHeight
);
if (Number.isFinite(containerBoundHeight) && containerBoundHeight > 0) {
nextHeight = Math.min(nextHeight, containerBoundHeight);
}
}
if (isMobile) {
nextHeight = Math.min(
nextHeight,
Math.floor(viewportHeight - rect.top - safeAreaBottom - debugHeight)
);
}
nextHeight = Math.max(0, nextHeight);
viewerFrameWrap.style.height = nextHeight + "px";
}
function renderList() {
if (galleryEl) {
galleryEl.innerHTML = "";
}
if (!listBody) {
return;
}
listBody.innerHTML = "";
if (!enabled || qapps.length === 0) {
listBody.innerHTML =
'<tr><td colspan="4">' + modeCopy.listEmpty + "</td></tr>";
return;
}
qapps.forEach(function (entry) {
if (galleryEl) {
const cardLink = document.createElement("a");
cardLink.className = "qortal-qapp-card";
cardLink.href = "#";
cardLink.title = entry.name || entry.address || "";
cardLink.setAttribute(
"aria-label",
(entry.name || entry.address || "Q-App") + " (open app)"
);
cardLink.addEventListener("click", function (event) {
event.preventDefault();
openAddressWithSwitchPrompt(entry.address || "", entry.name || "");
});
const iconUrl = resolveQappIcon(entry);
if (iconUrl) {
const image = document.createElement("img");
image.className = "qortal-qapp-card__image";
image.loading = "lazy";
image.alt = entry.name || "Q-App";
image.src = iconUrl;
image.onerror = function () {
image.remove();
if (!cardLink.querySelector(".qortal-qapp-card__fallback")) {
const fallback = document.createElement("div");
fallback.className = "qortal-qapp-card__fallback";
fallback.textContent = entry.name || entry.address || "Q-App";
cardLink.appendChild(fallback);
}
};
cardLink.appendChild(image);
} else {
const fallback = document.createElement("div");
fallback.className = "qortal-qapp-card__fallback";
fallback.textContent = entry.name || entry.address || "Q-App";
cardLink.appendChild(fallback);
}
const title = document.createElement("div");
title.className = "qortal-qapp-card__title";
title.textContent = entry.name || entry.address || "Q-App";
cardLink.appendChild(title);
const description = document.createElement("div");
description.className = "qortal-qapp-card__desc";
description.textContent = entry.description || "Open app";
cardLink.appendChild(description);
galleryEl.appendChild(cardLink);
}
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 = modeCopy.launchDisabledTitle;
}
launchButton.addEventListener("click", function () {
openAddressWithSwitchPrompt(entry.address || "", entry.name || "");
});
actionCell.appendChild(launchButton);
row.appendChild(nameCell);
row.appendChild(addressCell);
row.appendChild(descCell);
row.appendChild(actionCell);
listBody.appendChild(row);
});
}
applyModeCopy();
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 () {
openAddressWithSwitchPrompt(browserAddress, modeCopy.browserName);
});
}
} 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();
}
if (switchModalOpenHereButton) {
switchModalOpenHereButton.addEventListener("click", function () {
closeSwitchModal("same");
});
}
if (switchModalOpenNewTabButton) {
switchModalOpenNewTabButton.addEventListener("click", function () {
closeSwitchModal("newtab");
});
}
if (switchModalCancelButton) {
switchModalCancelButton.addEventListener("click", function () {
closeSwitchModal("cancel");
});
}
if (switchModal) {
switchModal.addEventListener("click", function (event) {
if (
event.target === switchModal ||
event.target.classList.contains("qortal-modal__backdrop")
) {
closeSwitchModal("cancel");
}
});
}
document.addEventListener("keydown", function (event) {
if (
event.key === "Escape" &&
switchModal &&
!switchModal.classList.contains("qortal-hidden")
) {
closeSwitchModal("cancel");
}
});
function extractQappAddressFromHref(href) {
const rawHref = String(href || "").trim();
if (!rawHref) {
return "";
}
let parsed;
try {
parsed = new URL(rawHref, window.location.origin);
} catch (_error) {
return "";
}
if (!/\/apps\/qortal_integration\/qapps/i.test(parsed.pathname)) {
return "";
}
const qapp = parsed.searchParams.get("qapp");
if (!qapp) {
return "";
}
return normalizeBrowserAddress(qapp);
}
function shouldPromptNavigationAway(target) {
if (!target || !isViewerShowingApp()) {
return false;
}
if (target.hasAttribute('download')) {
return false;
}
const targetAttr = String(target.getAttribute('target') || '').trim().toLowerCase();
if (targetAttr === '_blank') {
return false;
}
const href = String(target.getAttribute('href') || '').trim();
if (!href || href === '#' || href.toLowerCase().startsWith('javascript:')) {
return false;
}
const qappAddress = extractQappAddressFromHref(href);
if (qappAddress) {
return shouldPromptAppSwitch(qappAddress);
}
let absoluteUrl = '';
try {
absoluteUrl = new URL(href, window.location.origin).toString();
} catch (_error) {
return false;
}
const currentUrl = new URL(window.location.href);
if (absoluteUrl === currentUrl.toString()) {
return false;
}
return true;
}
document.addEventListener("click", function (event) {
if (
event.defaultPrevented ||
event.button !== 0 ||
event.metaKey ||
event.ctrlKey ||
event.shiftKey ||
event.altKey
) {
return;
}
const target =
event.target instanceof Element ? event.target.closest("a[href]") : null;
if (!target || !shouldPromptNavigationAway(target)) {
return;
}
const href = String(target.getAttribute("href") || "");
const targetAddress = extractQappAddressFromHref(href);
event.preventDefault();
const absoluteUrl = new URL(href, window.location.origin).toString();
const label =
target.textContent ||
target.getAttribute('aria-label') ||
target.getAttribute('title') ||
targetAddress ||
absoluteUrl;
openSwitchModal(
targetAddress || absoluteUrl,
label,
targetAddress ? 'switch' : 'leave'
).then(function (choice) {
if (choice === "newtab") {
window.open(absoluteUrl, "_blank", "noopener");
return;
}
if (choice === "same") {
window.location.href = absoluteUrl;
}
});
});
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,
};
balanceLookupCache = {
address: "",
balanceQort: 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 || "");
return;
}
const normalizedTarget = normalizeBrowserAddress(target);
if (normalizedTarget.toLowerCase().startsWith("qortal://")) {
openAddress(normalizedTarget, modeCopy.browserName);
}
}
renderList();
Promise.resolve(refreshUserMapping()).finally(function () {
openFromQuery();
});
if (debugCard) {
debugCard.classList.toggle("qortal-hidden", !effectiveDebugEnabled);
}
if (debugCard && effectiveDebugEnabled) {
syncDebugPanelState();
resizeViewerFrame();
}
if (debugCard && effectiveDebugEnabled) {
debugCard.addEventListener("pointerleave", releaseDebugDockHoverSuppression);
debugCard.addEventListener("mouseleave", releaseDebugDockHoverSuppression);
}
if (debugCard && effectiveDebugEnabled && debugResizeHandle) {
debugResizeHandle.addEventListener("pointerdown", function (event) {
if (event.button !== 0 || debugCard.classList.contains("qortal-debug-docked-hidden")) {
return;
}
if (debugCard.classList.contains("qortal-debug-minimized")) {
debugCard.classList.remove("qortal-debug-minimized");
updateDebugPanelControls();
}
event.preventDefault();
const startY = event.clientY;
const startHeight = debugPanelExpandedHeight;
const onPointerMove = function (moveEvent) {
const delta = startY - moveEvent.clientY;
setDebugPanelExpandedHeight(startHeight + delta, false);
resizeViewerFrame();
};
const onPointerEnd = function () {
document.removeEventListener("pointermove", onPointerMove);
document.removeEventListener("pointerup", onPointerEnd);
document.removeEventListener("pointercancel", onPointerEnd);
setDebugPanelExpandedHeight(debugPanelExpandedHeight, true);
resizeViewerFrame();
};
document.addEventListener("pointermove", onPointerMove);
document.addEventListener("pointerup", onPointerEnd);
document.addEventListener("pointercancel", onPointerEnd);
});
}
if (debugFloat) {
debugFloat.addEventListener("click", function (event) {
if (!debugCard || !effectiveDebugEnabled) {
return;
}
if (event.detail > 0) {
suppressDebugDockHover();
}
const isDockHidden = debugCard.classList.contains(
"qortal-debug-docked-hidden"
);
setDebugDockHidden(!isDockHidden, true);
if (isDockHidden) {
debugCard.classList.remove("qortal-debug-minimized");
releaseDebugDockHoverSuppression();
updateDebugPanelControls();
}
if (event.currentTarget instanceof HTMLElement) {
event.currentTarget.blur();
}
resizeViewerFrame();
});
}
if (debugMinimize) {
debugMinimize.addEventListener("click", function () {
if (!debugCard || !effectiveDebugEnabled) {
return;
}
if (debugCard.classList.contains("qortal-debug-docked-hidden")) {
setDebugDockHidden(false, true);
}
const isMinimized = debugCard.classList.toggle("qortal-debug-minimized");
debugMinimize.setAttribute("aria-pressed", isMinimized ? "true" : "false");
updateDebugPanelControls();
resizeViewerFrame();
});
}
if (debugCard && typeof ResizeObserver === "function" && effectiveDebugEnabled) {
debugPanelResizeObserver = new ResizeObserver(function () {
resizeViewerFrame();
});
debugPanelResizeObserver.observe(debugCard);
}
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, modeCopy.browserName);
});
viewerAddressInput.addEventListener("keydown", function (event) {
if (event.key === "Enter") {
event.preventDefault();
viewerAddressGo.click();
}
});
}
if (viewerFrame) {
viewerFrame.addEventListener("load", async function () {
installViewerNavigationInterceptors();
syncViewerAddressFromFrame();
if (pendingReloadFinalAddress) {
const finalTarget = pendingReloadFinalAddress;
clearPendingReloadSequence();
window.setTimeout(function () {
openAddress(finalTarget, modeCopy.browserName);
}, 0);
return;
}
if (!viewerCard || viewerCard.classList.contains("qortal-hidden")) {
clearQdnLoadingRetry("viewer_hidden");
return;
}
const frameSrc = String(viewerFrame.src || "");
const proxyFrame = isGatewayProxyViewerUrl(frameSrc);
const detection = detectQdnLoadingInterstitial();
if (!detection.inspectable) {
if (proxyFrame && scheduleOneTimeLaunchRetry("uninspectable_initial_load")) {
return;
}
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 && isTerminalQdnStatus(statusResult.status)) {
launchRetryCount = 0;
clearQdnLoadingRetry("content_loaded_terminal_status");
qdnLoadingRetryAttempts = 0;
return;
}
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") {
launchRetryCount = 0;
clearQdnLoadingRetry("content_loaded_ready");
qdnLoadingRetryAttempts = 0;
return;
}
}
launchRetryCount = 0;
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 isFileLikeValue(value) {
if (!value) {
return false;
}
if (typeof File !== "undefined" && value instanceof File) {
return true;
}
if (typeof Blob !== "undefined" && value instanceof Blob) {
return true;
}
return false;
}
function uint8ArrayToBase64(bytes) {
const chunkSize = 0x8000;
let binary = "";
for (let index = 0; index < bytes.length; index += chunkSize) {
const chunk = bytes.subarray(
index,
Math.min(index + chunkSize, bytes.length)
);
binary += String.fromCharCode.apply(null, chunk);
}
return window.btoa(binary);
}
async function fileLikeToBase64(value) {
if (!isFileLikeValue(value) || typeof value.arrayBuffer !== "function") {
throw new Error("Unsupported file payload for publish request");
}
const arrayBuffer = await value.arrayBuffer();
return uint8ArrayToBase64(new Uint8Array(arrayBuffer));
}
function pickPayloadBase64Value(value) {
if (!value || typeof value !== "object") {
return "";
}
const candidates = [value.base64, value.data64, value.data];
for (let index = 0; index < candidates.length; index += 1) {
const candidate = candidates[index];
if (typeof candidate === "string" && candidate.trim() !== "") {
return candidate.trim();
}
}
return "";
}
function unwrapSingleArrayValue(value) {
if (!Array.isArray(value)) {
return { value: value, multiple: false };
}
if (value.length === 0) {
return { value: null, multiple: false };
}
if (value.length > 1) {
return { value: value, multiple: true };
}
return { value: value[0], multiple: false };
}
async function normalizeFileUploadPayload(item, contextLabel) {
if (!item || typeof item !== "object") {
return item;
}
const normalized = Object.assign({}, item);
const uploadType = String(normalized.uploadType || "")
.trim()
.toLowerCase();
const hasFileHints =
Object.prototype.hasOwnProperty.call(normalized, "file") ||
Object.prototype.hasOwnProperty.call(normalized, "filePath");
if (uploadType !== "file" && !hasFileHints) {
return normalized;
}
const fileUnwrapped = unwrapSingleArrayValue(normalized.file);
const filePathUnwrapped = unwrapSingleArrayValue(normalized.filePath);
if (fileUnwrapped.multiple || filePathUnwrapped.multiple) {
throw new Error(
contextLabel +
" only supports one file per resource when uploadType=file"
);
}
const fileValue = fileUnwrapped.value;
const filePathValue = filePathUnwrapped.value;
const hasInlineBase64 = pickPayloadBase64Value(normalized) !== "";
const markedBase64 =
normalized.isBase64 === true ||
String(normalized.encoding || "")
.trim()
.toLowerCase() === "base64";
if (hasInlineBase64) {
const base64Data = pickPayloadBase64Value(normalized);
normalized.data = base64Data;
normalized.data64 = base64Data;
normalized.base64 = base64Data;
normalized.uploadType = "base64";
delete normalized.file;
delete normalized.filePath;
return normalized;
}
if (
markedBase64 &&
typeof fileValue === "string" &&
fileValue.trim() !== ""
) {
const base64Data = fileValue.trim();
normalized.data = base64Data;
normalized.data64 = base64Data;
normalized.base64 = base64Data;
normalized.uploadType = "base64";
delete normalized.file;
delete normalized.filePath;
return normalized;
}
const candidateFile = isFileLikeValue(fileValue)
? fileValue
: isFileLikeValue(filePathValue)
? filePathValue
: null;
if (candidateFile) {
const base64Data = await fileLikeToBase64(candidateFile);
normalized.data = base64Data;
normalized.data64 = base64Data;
normalized.base64 = base64Data;
normalized.uploadType = "base64";
delete normalized.file;
delete normalized.filePath;
return normalized;
}
if (typeof filePathValue === "string" && filePathValue.trim() !== "") {
normalized.filePath = filePathValue.trim();
delete normalized.file;
return normalized;
}
if (typeof fileValue === "string" && fileValue.trim() !== "") {
if (markedBase64) {
const base64Data = fileValue.trim();
normalized.data = base64Data;
normalized.data64 = base64Data;
normalized.base64 = base64Data;
normalized.uploadType = "base64";
delete normalized.file;
delete normalized.filePath;
return normalized;
}
normalized.filePath = fileValue.trim();
delete normalized.file;
return normalized;
}
throw new Error(
contextLabel + " is missing supported file data for uploadType=file"
);
}
async function normalizeCryptoEncryptPayload(item, contextLabel) {
if (!item || typeof item !== "object") {
return item;
}
const normalized = Object.assign({}, item);
const fileUnwrapped = unwrapSingleArrayValue(normalized.file);
const blobUnwrapped = unwrapSingleArrayValue(normalized.blob);
if (fileUnwrapped.multiple || blobUnwrapped.multiple) {
throw new Error(contextLabel + " only supports one file payload");
}
const fileValue = fileUnwrapped.value;
const blobValue = blobUnwrapped.value;
const candidateFile = isFileLikeValue(fileValue)
? fileValue
: isFileLikeValue(blobValue)
? blobValue
: null;
if (candidateFile) {
const base64Data = await fileLikeToBase64(candidateFile);
if (!base64Data) {
throw new Error(contextLabel + " requires non-empty file data");
}
normalized.data = base64Data;
normalized.data64 = base64Data;
normalized.base64 = base64Data;
delete normalized.file;
delete normalized.blob;
delete normalized.filePath;
return normalized;
}
const markedBase64 =
normalized.isBase64 === true ||
String(normalized.encoding || "")
.trim()
.toLowerCase() === "base64";
if (
markedBase64 &&
typeof fileValue === "string" &&
fileValue.trim() !== ""
) {
const base64Data = fileValue.trim();
normalized.data = base64Data;
normalized.data64 = base64Data;
normalized.base64 = base64Data;
delete normalized.file;
delete normalized.blob;
delete normalized.filePath;
}
return normalized;
}
function normalizeBridgePayload(requestType, payload) {
if (!payload || typeof payload !== "object") {
return {};
}
const normalized = Object.assign({}, payload);
const isEmptyValue = function (value) {
return value === undefined || value === null || value === "";
};
const firstNonEmpty = function (values) {
for (let i = 0; i < values.length; i += 1) {
const value = values[i];
if (!isEmptyValue(value)) {
return value;
}
}
return "";
};
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 normalizedName = firstNonEmpty([
normalized.name,
normalized.registeredName,
normalized.registered_name,
]);
if (!isEmptyValue(normalizedName)) {
normalized.name = normalizedName;
if (isEmptyValue(normalized.registeredName)) {
normalized.registeredName = normalizedName;
}
}
const normalizedService = firstNonEmpty([
normalized.service,
normalized.serviceName,
normalized.service_name,
normalized.type,
]);
if (!isEmptyValue(normalizedService)) {
normalized.service = normalizedService;
}
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 resourceName = firstNonEmpty([
resourceCopy.name,
resourceCopy.registeredName,
resourceCopy.registered_name,
]);
if (!isEmptyValue(resourceName)) {
resourceCopy.name = resourceName;
if (isEmptyValue(resourceCopy.registeredName)) {
resourceCopy.registeredName = resourceName;
}
}
const resourceService = firstNonEmpty([
resourceCopy.service,
resourceCopy.serviceName,
resourceCopy.service_name,
resourceCopy.type,
]);
if (!isEmptyValue(resourceService)) {
resourceCopy.service = resourceService;
}
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_DATA_WITH_SHARING_KEY",
"DECRYPT_DATA_WITH_SHARING_KEY",
"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;
}
async function normalizeBridgePayloadAsync(requestType, payload) {
const normalized = normalizeBridgePayload(requestType, payload);
if (!normalized || typeof normalized !== "object") {
return normalized;
}
if (requestType === "PUBLISH_QDN_RESOURCE") {
return await normalizeFileUploadPayload(
normalized,
"PUBLISH_QDN_RESOURCE"
);
}
if (
requestType === "PUBLISH_MULTIPLE_QDN_RESOURCES" &&
Array.isArray(normalized.resources)
) {
const normalizedResources = [];
for (let index = 0; index < normalized.resources.length; index += 1) {
const resource = normalized.resources[index];
const normalizedResource = await normalizeFileUploadPayload(
resource,
"PUBLISH_MULTIPLE_QDN_RESOURCES resource " + index
);
normalizedResources.push(normalizedResource);
}
normalized.resources = normalizedResources;
}
const cryptoEncryptActions = new Set([
"ENCRYPT_DATA",
"ENCRYPT_DATA_WITH_SHARING_KEY",
"ENCRYPT_QORTAL_GROUP_DATA",
]);
if (cryptoEncryptActions.has(requestType)) {
return await normalizeCryptoEncryptPayload(normalized, requestType);
}
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 120;
}
const valueInt = Math.floor(parsed);
const allowed = [60, 120, 180, 360, 720, 1440, 7200];
return allowed.includes(valueInt) ? valueInt : 120;
}
function sanitizeTemporaryApprovalRules(entries) {
if (!Array.isArray(entries)) {
return [];
}
return entries
.filter(function (entry) {
return (
entry &&
typeof entry === "object" &&
typeof entry.requestType === "string" &&
typeof entry.scope === "string" &&
typeof entry.app === "string"
);
})
.map(function (entry) {
return {
requestType: String(entry.requestType || "").trim(),
scope: String(entry.scope || "").trim(),
app: String(entry.app || "").trim(),
expiresAt: Number(entry.expiresAt || 0),
};
})
.filter(function (entry) {
return (
entry.requestType !== "" &&
entry.scope !== "" &&
Number.isFinite(entry.expiresAt) &&
entry.expiresAt > Date.now()
);
});
}
function loadTemporaryApprovalRules() {
if (!window.sessionStorage) {
return [];
}
try {
return sanitizeTemporaryApprovalRules(
JSON.parse(
window.sessionStorage.getItem(temporaryApprovalStorageKey) || "[]"
)
);
} catch (_error) {
return [];
}
}
function persistTemporaryApprovalRules() {
if (!window.sessionStorage) {
return;
}
try {
window.sessionStorage.setItem(
temporaryApprovalStorageKey,
JSON.stringify(sanitizeTemporaryApprovalRules(temporaryApprovalRules))
);
} catch (_error) {
// ignore storage failures
}
}
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;
persistTemporaryApprovalRules();
return;
}
}
temporaryApprovalRules.push({
requestType: normalizedRequestType,
scope: normalizedScope,
app: normalizedApp,
expiresAt,
});
persistTemporaryApprovalRules();
}
function findTemporaryApprovalRule(requestType, scope, app) {
const normalizedRequestType = String(requestType || "").trim();
const normalizedScope = String(scope || "").trim();
const normalizedApp = String(app || "").trim();
const now = Date.now();
let scopeAgnosticMatch = null;
let mutated = false;
for (let i = temporaryApprovalRules.length - 1; i >= 0; i -= 1) {
const candidate = temporaryApprovalRules[i];
if (!candidate || candidate.expiresAt <= now) {
temporaryApprovalRules.splice(i, 1);
mutated = true;
continue;
}
if (candidate.requestType !== normalizedRequestType) {
continue;
}
if (candidate.app !== normalizedApp) {
continue;
}
if (candidate.scope === normalizedScope) {
return candidate;
}
if (
!scopeAgnosticMatch ||
candidate.expiresAt > scopeAgnosticMatch.expiresAt
) {
scopeAgnosticMatch = candidate;
}
}
if (mutated) {
persistTemporaryApprovalRules();
}
return scopeAgnosticMatch;
}
function approvalModeLabel(mode) {
switch (normalizeApprovalMode(mode)) {
case "type_minutes":
return "Allow this request type for X minutes in this browser tab";
case "type_always":
return "Always allow this request type for this app/scope";
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 (approvalConfirm) {
approvalConfirm.textContent = approvalConfirmLabel(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 blurActiveElement() {
if (document.activeElement && typeof document.activeElement.blur === "function") {
try {
document.activeElement.blur();
} catch (_error) {
// Ignore focus cleanup failures.
}
}
}
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 = advancedUnlockSettingsEnabled
? normalizeApprovalMode(defaultApprovalMode)
: "app_always";
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();
if (advancedUnlockSettingsEnabled) {
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";
}
} else if (approvalTempMinutesEl) {
approvalTempMinutesEl.disabled = true;
}
showApprovalError("");
blurActiveElement();
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 = advancedUnlockSettingsEnabled
? document.querySelector('input[name="qortal-approval-mode"]:checked')
: null;
const mode = advancedUnlockSettingsEnabled
? normalizeApprovalMode(choice ? choice.value : "once")
: "app_always";
const unlockFieldsVisible =
approvalUnlockFieldsEl &&
!approvalUnlockFieldsEl.classList.contains("qortal-hidden");
const passwordValue = approvalPassword
? approvalPassword.value.trim()
: "";
const ttlSeconds = unlockFieldsVisible
? (advancedUnlockSettingsEnabled
? (approvalTtl && approvalTtl.checked ? 3600 : undefined)
: 3600)
: 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: advancedUnlockSettingsEnabled
? normalizeApprovalMinutes(
approvalTempMinutesEl
? approvalTempMinutesEl.value
: defaultApprovalTempMinutes
)
: 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 = "";
showUnlockError("");
blurActiveElement();
unlockModal.classList.remove("qortal-hidden");
unlockConfirm.textContent = unlockConfirmLabel();
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 = defaultApprovalTempMinutes * 60;
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 = !advancedUnlockSettingsEnabled;
}
const modeInputs = document.querySelectorAll(
'input[name="qortal-approval-mode"]'
);
modeInputs.forEach(function (input) {
input.onchange = null;
});
if (approvalConfirm) {
approvalConfirm.disabled = false;
approvalConfirm.textContent = approvalConfirmLabel(false);
}
if (approvalCancel) {
approvalCancel.disabled = false;
}
}
function closeUnlockModal() {
if (!unlockModal) {
return;
}
unlockModal.classList.add("qortal-hidden");
showUnlockError("");
if (unlockConfirm) {
unlockConfirm.disabled = false;
unlockConfirm.textContent = unlockConfirmLabel();
}
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 fetchWithRetry(
qappsApproveUrl,
{
method: "POST",
headers,
body: JSON.stringify({
walletId,
scope,
app: currentQappAddress,
requestType,
}),
},
2,
250
);
} 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 outcome = await postQappsRequestWithHighRiskConfirmation(
requestType,
retryPayload,
headers
);
return { retryResponse: outcome.response, retryJson: outcome.json };
};
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 = normalizeQappsResultPayload(
requestType,
retryJson.data !== undefined ? retryJson.data : retryJson
);
return { ok: true, result: retryResult };
}
return { ok: false, error: "approval_failed" };
}
async function autoApproveRequest(requestType, payload, approvalContext, headers, walletId) {
const effectiveWalletId = walletId || (approvalContext && approvalContext.walletId) || currentWalletId;
if (!effectiveWalletId) {
throw new Error("walletId is not linked");
}
const safeContext = Object.assign({}, approvalContext || {}, {
walletId: effectiveWalletId,
scope: normalizeApprovalScope(
approvalContext && approvalContext.scope ? approvalContext.scope : "",
requestType
),
});
let token;
try {
token = await approveOnce(requestType, payload, safeContext);
} catch (error) {
const message =
error && error.message ? error.message : String(error || "approval_failed");
if (!message.includes("wallet_locked")) {
throw error;
}
markWalletLocked();
await ensureWalletUnlocked(effectiveWalletId, requestType);
token = await approveOnce(requestType, payload, safeContext);
}
return await runApprovedRequestWithFallbacks(
requestType,
payload,
token,
safeContext,
headers,
{ action: "once", password: "", ttlSeconds: undefined },
effectiveWalletId
);
}
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 fetchWithRetry(
qappsPermissionsUrl,
{
method: "POST",
headers,
body: JSON.stringify(permissionPayload),
},
2,
250
);
} 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 outcome = await postQappsRequestWithHighRiskConfirmation(
requestType,
payload,
headers
);
const retryResponse = outcome.response;
const retryJson = outcome.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 = normalizeQappsResultPayload(
requestType,
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 tryAutoApproveNonSpendingRequest(
requestType,
payload,
approvalCtx,
headers
) {
if (isLikelySpendingRequestType(requestType)) {
return { handled: false };
}
const walletId =
approvalCtx && approvalCtx.walletId
? approvalCtx.walletId
: currentWalletId;
if (!walletId) {
return { handled: false };
}
const normalizedScope = normalizeApprovalScope(
approvalCtx && approvalCtx.scope ? approvalCtx.scope : "",
requestType
);
const normalizedContext = Object.assign({}, approvalCtx || {}, {
walletId,
scope: normalizedScope,
});
try {
await refreshWalletLockState(walletId);
if (walletLockState.state === "locked") {
await ensureWalletUnlocked(walletId, requestType);
}
await setPermission(
"type_always",
requestType,
normalizedScope,
walletId
);
const permissionOutcome = await runPermissionRequestWithFallbacks(
"type_always",
requestType,
payload,
headers,
{ action: "type_always", password: "", ttlSeconds: undefined },
walletId,
normalizedContext
);
if (permissionOutcome && permissionOutcome.ok) {
activeApprovalPolicyMode = "type_always";
logDebug("info", "Auto-approved non-spending request type", {
requestType,
scope: normalizedScope,
walletId,
});
return { handled: true, ok: true, result: permissionOutcome.result };
}
logDebug(
"warn",
"Auto-approve non-spending request failed; falling back to manual",
{
requestType,
error:
permissionOutcome && permissionOutcome.error
? permissionOutcome.error
: "approval_failed",
}
);
return { handled: false };
} catch (error) {
const message =
error && error.message
? error.message
: String(error || "approval_failed");
if (message.includes("wallet_locked")) {
return { handled: true, ok: false, error: "wallet_locked" };
}
logDebug(
"warn",
"Auto-approve non-spending request threw; falling back to manual",
{
requestType,
error: message,
}
);
return { handled: false };
}
}
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 fetchWithRetry(
qappsUnlockUrl,
{
method: "POST",
headers,
body: JSON.stringify({
walletId,
password,
...(ttlSeconds ? { ttlSeconds } : {}),
}),
},
2,
250
);
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 = unlockConfirmLabel();
}
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;
let payload = {};
try {
payload = await normalizeBridgePayloadAsync(
requestType,
buildBridgePayload(message)
);
} catch (normalizeError) {
const normalizeMessage =
normalizeError && normalizeError.message
? normalizeError.message
: "Invalid publish payload";
if (hasPort && event.ports[0]) {
postBridgeError(event.ports[0], normalizeMessage);
} else {
logDebug("error", "Payload normalization failed", {
requestType,
error: normalizeMessage,
});
}
return;
}
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 =
getNavigationAddressFromPayload(payload) ||
(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, modeCopy.browserName);
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, modeCopy.requestEndpointMissing);
return;
}
const headers = { "Content-Type": "application/json" };
if (typeof OC !== "undefined" && OC.requestToken) {
headers.requesttoken = OC.requestToken;
}
logDebug("info", "Broker qortalRequest start", { requestType, payload });
let requestOutcome = await postQappsRequestWithHighRiskConfirmation(
requestType,
payload,
headers
);
payload = requestOutcome.payload;
const response = requestOutcome.response;
const json = requestOutcome.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
);
requestOutcome = await postQappsRequestWithHighRiskConfirmation(
requestType,
payload,
headers
);
payload = requestOutcome.payload;
const retryResponse = requestOutcome.response;
const retryJson = requestOutcome.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,
});
closeUnlockModal();
await refreshWalletLockState(
retryApprovalContext.walletId || currentWalletId
);
const autoOutcome = await autoApproveRequest(
requestType,
payload,
retryApprovalContext,
headers,
retryApprovalContext.walletId || currentWalletId
);
if (!autoOutcome.ok) {
showApprovalError(autoOutcome.error || "approval_failed");
postBridgeError(
port,
autoOutcome.error || "qortal_request_failed"
);
closeUnlockModal();
return;
}
logDebug("info", "Broker qortalRequest ok", {
requestType,
result: autoOutcome.result,
});
port.postMessage({
result: autoOutcome.result,
error: null,
});
closeUnlockModal();
return;
}
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 = unlockConfirmLabel();
}
if (unlockCancel) {
unlockCancel.disabled = false;
}
closeUnlockModal();
return;
}
}
if (
typeof errorMessage === "string" &&
errorMessage.includes("approval_required")
) {
logDebug("info", "Approval required", {
requestType,
context: approvalContext,
});
await refreshWalletLockState(
approvalContext.walletId || currentWalletId
);
try {
const autoOutcome = await autoApproveRequest(
requestType,
payload,
approvalContext,
headers,
approvalContext.walletId || currentWalletId
);
if (!autoOutcome.ok) {
showApprovalError(autoOutcome.error || "approval_failed");
postBridgeError(
port,
autoOutcome.error || "qortal_request_failed"
);
return;
}
logDebug("info", "Broker qortalRequest ok", {
requestType,
result: autoOutcome.result,
});
port.postMessage({ result: autoOutcome.result, error: null });
return;
} catch (approvalError) {
showApprovalError(approvalError.message || "approval_failed");
logDebug("error", "Approval failed", {
requestType,
error: approvalError.message || "approval_failed",
});
postBridgeError(port, approvalError.message || "approval_failed");
return;
}
}
logDebug("error", "Broker qortalRequest failed", {
requestType,
error: json.error || "qortal_request_failed",
});
postBridgeError(port, json.error || "qortal_request_failed");
return;
}
let resultPayload = normalizeQappsResultPayload(
requestType,
json.data !== undefined ? json.data : json
);
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;
}
function resolveSaveFileTarget(payload) {
const raw =
payload && typeof payload === "object"
? payload.saveTarget ||
payload.target ||
payload.destination ||
payload.targetType ||
payload.saveTo
: "";
const normalized = String(raw || "")
.trim()
.toLowerCase();
if (!normalized) {
return "browser-local";
}
if (
normalized === "browser" ||
normalized === "browser-local" ||
normalized === "local" ||
normalized === "download" ||
normalized === "device" ||
normalized === "user-device" ||
normalized === "auto"
) {
return "browser-local";
}
if (
normalized === "daemon" ||
normalized === "daemon-local" ||
normalized === "server" ||
normalized === "server-local"
) {
return "daemon-local";
}
if (
normalized === "files" ||
normalized === "nextcloud" ||
normalized === "nextcloud-files" ||
normalized === "cloud-files"
) {
return "nextcloud-files";
}
return normalized;
}
function resolveSaveFileName(payload) {
const defaultName = "download_" + Date.now() + ".bin";
if (!payload || typeof payload !== "object") {
return defaultName;
}
const candidates = [
payload.filename,
payload.fileName,
payload.name,
payload.downloadName,
payload.filePath,
payload.path,
payload.savePath,
payload.location,
];
for (let i = 0; i < candidates.length; i += 1) {
const candidate = candidates[i];
if (typeof candidate !== "string") {
continue;
}
const trimmed = candidate.trim();
if (!trimmed) {
continue;
}
const basename = trimmed.replace(/\\/g, "/").split("/").pop();
const safe = String(basename || "")
.trim()
.replace(/[\u0000-\u001f<>:"|?*]+/g, "_");
if (safe) {
return safe;
}
}
return defaultName;
}
function decodeBase64ToUint8Array(base64Value) {
const normalized = String(base64Value || "").trim();
if (!normalized) {
return new Uint8Array(0);
}
try {
const binary = window.atob(normalized);
const bytes = new Uint8Array(binary.length);
for (let i = 0; i < binary.length; i += 1) {
bytes[i] = binary.charCodeAt(i) & 0xff;
}
return bytes;
} catch (_error) {
throw new Error("Invalid base64 payload for SAVE_FILE");
}
}
function encodeUtf8Bytes(text) {
const normalized = String(text === undefined || text === null ? "" : text);
if (typeof TextEncoder !== "undefined") {
return new TextEncoder().encode(normalized);
}
// Fallback for very old browsers: preserve byte values via encodeURIComponent.
const encoded = unescape(encodeURIComponent(normalized));
const bytes = new Uint8Array(encoded.length);
for (let i = 0; i < encoded.length; i += 1) {
bytes[i] = encoded.charCodeAt(i) & 0xff;
}
return bytes;
}
function decodeSaveFilePayload(payload) {
const contentType =
String(
(payload &&
(payload.mimeType || payload.contentType || payload.type)) ||
"application/octet-stream"
).trim() || "application/octet-stream";
const encodingHint = String(
(payload && (payload.encoding || payload.dataEncoding)) || ""
)
.trim()
.toLowerCase();
const explicitBase64 =
payload &&
(payload.data64 ||
payload.base64 ||
payload.blob64 ||
payload.base64Data);
if (explicitBase64 !== undefined && explicitBase64 !== null) {
const bytes = decodeBase64ToUint8Array(explicitBase64);
return { bytes: bytes, contentType: contentType, encoding: "base64" };
}
const explicitText =
payload &&
(payload.text ||
payload.stringData ||
payload.string ||
payload.plainText);
if (explicitText !== undefined && explicitText !== null) {
const bytes = encodeUtf8Bytes(explicitText);
return { bytes: bytes, contentType: contentType, encoding: "utf8" };
}
const genericData =
payload &&
(payload.data !== undefined
? payload.data
: payload.blob !== undefined
? payload.blob
: payload.contents);
if (genericData === undefined || genericData === null) {
throw new Error("SAVE_FILE requires text, base64/data64, or data");
}
if (genericData instanceof ArrayBuffer) {
return {
bytes: new Uint8Array(genericData),
contentType: contentType,
encoding: "arraybuffer",
};
}
if (ArrayBuffer.isView(genericData)) {
return {
bytes: new Uint8Array(
genericData.buffer,
genericData.byteOffset,
genericData.byteLength
),
contentType: contentType,
encoding: "typedarray",
};
}
if (Array.isArray(genericData)) {
return {
bytes: new Uint8Array(
genericData.map(function (value) {
return Number(value) & 0xff;
})
),
contentType: contentType,
encoding: "bytes",
};
}
if (
encodingHint === "utf8" ||
encodingHint === "utf-8" ||
encodingHint === "text" ||
encodingHint === "string"
) {
return {
bytes: encodeUtf8Bytes(genericData),
contentType: contentType,
encoding: "utf8",
};
}
if (typeof genericData === "string") {
return {
bytes: decodeBase64ToUint8Array(genericData),
contentType: contentType,
encoding: "base64",
};
}
throw new Error("Unsupported SAVE_FILE payload data format");
}
function triggerBrowserDownload(blob, fileName) {
const blobUrl = window.URL.createObjectURL(blob);
try {
const link = document.createElement("a");
link.href = blobUrl;
link.download = fileName;
link.rel = "noopener";
link.style.display = "none";
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
} finally {
window.setTimeout(function () {
window.URL.revokeObjectURL(blobUrl);
}, 1000);
}
}
async function saveFileToBrowser(payload) {
const fileName = resolveSaveFileName(payload);
const decoded = decodeSaveFilePayload(
payload && typeof payload === "object" ? payload : {}
);
const bytes =
decoded.bytes instanceof Uint8Array
? decoded.bytes
: new Uint8Array(decoded.bytes || []);
const blob = new Blob([bytes], {
type: decoded.contentType || "application/octet-stream",
});
const preferPicker =
!payload || payload.useBrowserDownload === true
? false
: typeof window.showSaveFilePicker === "function";
if (preferPicker) {
try {
const handle = await window.showSaveFilePicker({
suggestedName: fileName,
types: [
{
description: "Downloaded file",
accept: {
[decoded.contentType || "application/octet-stream"]: [
"." + fileName.split(".").pop(),
],
},
},
],
});
const writable = await handle.createWritable();
await writable.write(blob);
await writable.close();
return {
saved: true,
target: "browser-local",
method: "file-system-access",
fileName: fileName,
bytesWritten: bytes.byteLength,
contentType: decoded.contentType,
encoding: decoded.encoding,
};
} catch (error) {
if (
error &&
(error.name === "AbortError" ||
/abort/i.test(String(error.message || "")))
) {
throw new Error("SAVE_FILE canceled");
}
// Fall through to browser download prompt.
}
}
triggerBrowserDownload(blob, fileName);
return {
saved: true,
target: "browser-local",
method: "download",
fileName: fileName,
bytesWritten: bytes.byteLength,
contentType: decoded.contentType,
encoding: decoded.encoding,
};
}
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 };
}
case "SAVE_FILE": {
const saveTarget = resolveSaveFileTarget(payload);
if (saveTarget === "daemon-local") {
return { handled: false };
}
if (saveTarget === "nextcloud-files") {
return {
handled: true,
error: "Nextcloud Files SAVE_FILE target is not available yet",
};
}
const result = await saveFileToBrowser(payload || {});
return { handled: true, data: result };
}
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 };
}
})();