b54a3139c7
Includes QWB, Qortal Web, and Q-Shops Q-Apps with shared packages and build scripts. Co-authored-by: Cursor <cursoragent@cursor.com>
862 lines
36 KiB
HTML
862 lines
36 KiB
HTML
<!doctype html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8" />
|
|
<script>
|
|
(function () {
|
|
/* Set `<base href>` to the **name root** of this WEBSITE (not the sub-page slug),
|
|
* so relative asset URLs (`./assets/...`) always resolve to the QDN-served name root.
|
|
* Otherwise visitors on `/render/WEBSITE/<name>/blog/` would 404 fetching
|
|
* `/render/WEBSITE/<name>/blog/assets/...`. */
|
|
try {
|
|
var p = location.pathname || '/';
|
|
var m = /^(\/render\/(?:WEBSITE|APP)\/[^\/]+)(\/.*)?$/i.exec(p);
|
|
var base;
|
|
if (m) {
|
|
base = m[1] + '/';
|
|
} else if (/\.html?$/i.test(p)) {
|
|
base = p.replace(/\/[^/]+$/, '/');
|
|
} else if (p !== '/' && !p.endsWith('/')) {
|
|
base = p + '/';
|
|
} else {
|
|
base = p;
|
|
}
|
|
if (base && base.charAt(0) === '/') {
|
|
var b = document.createElement('base');
|
|
b.href = base;
|
|
document.head.insertBefore(b, document.head.firstChild);
|
|
}
|
|
} catch (e) {
|
|
/* */
|
|
}
|
|
})();
|
|
</script>
|
|
<script>
|
|
(function () {
|
|
/**
|
|
* Strip the duplicate-name segment (`/render/WEBSITE/<name>/<name>/<slug>`) Hub used to
|
|
* inject in copy-link URLs. We compute the canonical pathname FROM scratch on each
|
|
* tick — never cache it — so the SPA's own `history.pushState` calls (hash routing for
|
|
* sub-pages: `/<name>/#<slug>`) don't get clobbered by an outdated canonical.
|
|
*
|
|
* The expected canonical pathname is always `/render/WEBSITE/<name>/<firstSegOrNothing>/`
|
|
* — at most ONE non-name segment after the name. Any deeper / duplicated segments are
|
|
* collapsed back to that shape. Hash and search are preserved.
|
|
*/
|
|
function computeCanonicalPathname() {
|
|
var p = location.pathname || '/';
|
|
var m = /^(\/render\/(?:WEBSITE|APP)\/)([^\/]+)(\/.*)?$/i.exec(p);
|
|
if (!m) return null;
|
|
var prefix = m[1];
|
|
var name = m[2];
|
|
var rest = (m[3] || '').replace(/^\/+/, '').replace(/\/+$/, '');
|
|
var firstSeg = rest ? rest.split('/')[0] : '';
|
|
/** If the very first segment matches the name (Hub's bug), drop it and use the next. */
|
|
if (firstSeg && decodeURIComponent(firstSeg).toLowerCase() === decodeURIComponent(name).toLowerCase()) {
|
|
var afterDup = rest.slice(firstSeg.length).replace(/^\/+/, '');
|
|
firstSeg = afterDup ? afterDup.split('/')[0] : '';
|
|
}
|
|
return prefix + name + '/' + (firstSeg ? firstSeg + '/' : '');
|
|
}
|
|
|
|
function pinUrlToCanonical() {
|
|
try {
|
|
var canonical = computeCanonicalPathname();
|
|
if (!canonical) return;
|
|
if (location.pathname !== canonical) {
|
|
window.history.replaceState(
|
|
{},
|
|
'',
|
|
canonical + (location.search || '') + (location.hash || ''),
|
|
);
|
|
}
|
|
} catch (e) {
|
|
/* */
|
|
}
|
|
}
|
|
|
|
/** Run once now so the duplicated-name URL is normalised before React mounts. */
|
|
pinUrlToCanonical();
|
|
|
|
window.addEventListener('popstate', pinUrlToCanonical);
|
|
/**
|
|
* Belt-and-suspenders: a slow timer catches any URL change that bypassed our handlers
|
|
* (e.g. an extension that overrides history.pushState). The recompute happens fresh each
|
|
* time so SPA hash navigation (which only changes `location.hash`) is never overwritten.
|
|
*/
|
|
try {
|
|
setInterval(pinUrlToCanonical, 1500);
|
|
} catch (e) {
|
|
/* */
|
|
}
|
|
|
|
/**
|
|
* Block in-iframe relative-path navigation (e.g. user-authored `<a href="/Blog">`) that
|
|
* would push the iframe to a sub-path the Hub then mis-copies. Allow same-page anchor
|
|
* links (`#section`) and anything with a real scheme (qortal:, http:, https:, mailto:).
|
|
*/
|
|
document.addEventListener(
|
|
'click',
|
|
function (ev) {
|
|
try {
|
|
var a = ev.target && ev.target.closest ? ev.target.closest('a[href]') : null;
|
|
if (!a) return;
|
|
var href = a.getAttribute('href') || '';
|
|
if (!href) return;
|
|
if (href.charAt(0) === '#') return;
|
|
if (/^(qortal:|https?:|mailto:|tel:|data:)/i.test(href)) return;
|
|
/* Plain-text relative URL — never let it navigate the Q-App iframe. */
|
|
ev.preventDefault();
|
|
ev.stopPropagation();
|
|
} catch (e) {
|
|
/* */
|
|
}
|
|
},
|
|
true,
|
|
);
|
|
})();
|
|
</script>
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover" />
|
|
<title>Website</title>
|
|
<link rel="icon" href="./favicon.svg" type="image/svg+xml" />
|
|
</head>
|
|
<body>
|
|
<div id="root">
|
|
<p
|
|
class="qwb-fallback"
|
|
style="padding: 2rem; font: 14px system-ui, sans-serif; color: #7a8a7e; margin: 0"
|
|
>
|
|
Loading…
|
|
</p>
|
|
</div>
|
|
<script>
|
|
(function () {
|
|
var shown = false;
|
|
function show(html) {
|
|
if (shown) return;
|
|
var r = document.getElementById('root');
|
|
if (!r || !r.querySelector('.qwb-fallback')) return;
|
|
shown = true;
|
|
r.innerHTML =
|
|
'<div class="qwb-fallback" style="padding:1.5rem;font:14px/1.5 system-ui,sans-serif;color:#c8a090;max-width:36rem">' +
|
|
html +
|
|
'</div>';
|
|
}
|
|
window.addEventListener(
|
|
'error',
|
|
function (ev) {
|
|
var t = ev && ev.target;
|
|
if (!t || t.tagName !== 'SCRIPT') return;
|
|
var a = t.getAttribute && t.getAttribute('src');
|
|
var s = a || t.src || '';
|
|
if (s) {
|
|
show(
|
|
'Could not load: ' +
|
|
s +
|
|
'<p style="margin:12px 0 0;font-size:13px;line-height:1.45;color:inherit">Often the <code>assets</code> folder is not next to this file, or the site was exported before a full build. Run <code>npm run build</code>, open this app from the Hub with <code>index.html</code> at the zip root, then use <strong>Publish Website</strong> or <strong>Export .zip</strong> again.</p>',
|
|
);
|
|
}
|
|
},
|
|
true,
|
|
);
|
|
setTimeout(function () {
|
|
if (shown) return;
|
|
var r = document.getElementById('root');
|
|
if (!r || !r.querySelector('.qwb-fallback')) return;
|
|
show('If this persists, the published bundle may be missing assets.');
|
|
}, 15000);
|
|
})();
|
|
</script>
|
|
<script id="sn-embed-config" type="application/json"></script>
|
|
<script>
|
|
(function () {
|
|
/* Never return early: Hub may inject `qortalRequest` without `qortalRequestWithNoTimeout`, which
|
|
* leaves Q-Mail / publish racing a short timeout. We always install long helpers below. */
|
|
|
|
function qs(obj) {
|
|
var parts = [];
|
|
for (var k in obj) {
|
|
if (!Object.prototype.hasOwnProperty.call(obj, k)) continue;
|
|
var v = obj[k];
|
|
if (v == null) continue;
|
|
parts.push(encodeURIComponent(k) + '=' + encodeURIComponent(String(v)));
|
|
}
|
|
return parts.length ? '?' + parts.join('&') : '';
|
|
}
|
|
function getJson(url, timeoutMs) {
|
|
var ctl = typeof AbortController === 'function' ? new AbortController() : null;
|
|
var timer = setTimeout(function () { try { ctl && ctl.abort(); } catch (e) {} }, timeoutMs || 4000);
|
|
return fetch(url, { headers: { accept: 'application/json' }, signal: ctl ? ctl.signal : undefined })
|
|
.then(function (r) {
|
|
if (!r.ok) throw new Error('HTTP ' + r.status + ' for ' + url);
|
|
return r.text().then(function (t) {
|
|
var s = (t || '').trim();
|
|
if (!(s.charAt(0) === '{' || s.charAt(0) === '[')) {
|
|
throw new Error('Non-JSON response from ' + url + ' (got ' + s.slice(0, 40) + '…)');
|
|
}
|
|
return JSON.parse(s);
|
|
});
|
|
})
|
|
.finally(function () { clearTimeout(timer); });
|
|
}
|
|
function getText(url) {
|
|
return fetch(url).then(function (r) {
|
|
if (!r.ok) throw new Error('HTTP ' + r.status + ' for ' + url);
|
|
return r.text();
|
|
});
|
|
}
|
|
|
|
/** Arbitrary path segments: encodeURIComponent leaves ' ( ) ! * literal — REST 404s (e.g. MA's). */
|
|
function encodeQdnSeg(seg) {
|
|
return encodeURIComponent(String(seg == null ? '' : seg)).replace(/[!'()*]/g, function (ch) {
|
|
var c = ch.charCodeAt(0);
|
|
var h = c.toString(16).toUpperCase();
|
|
return '%' + (h.length < 2 ? '0' + h : h);
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Hub stores the node API key in extension storage; only the Hub shell window handles
|
|
* `{ type: 'backgroundMessage' }` (see Qortal-Hub `setupMessageListener`). This iframe must
|
|
* post that envelope to `parent`, not forward it as a qortalRequest action.
|
|
*/
|
|
function findHubSendMessage() {
|
|
var w = window.parent;
|
|
var hops = 0;
|
|
while (w && w !== window && hops < 16) {
|
|
try {
|
|
if (typeof w.sendMessage === 'function') return w.sendMessage.bind(w);
|
|
} catch (e) {}
|
|
try {
|
|
w = w.parent;
|
|
} catch (e2) {
|
|
break;
|
|
}
|
|
hops++;
|
|
}
|
|
try {
|
|
if (window.top && window.top !== window && typeof window.top.sendMessage === 'function') {
|
|
return window.top.sendMessage.bind(window.top);
|
|
}
|
|
} catch (e3) {}
|
|
return null;
|
|
}
|
|
|
|
function postHubBackgroundMessage(msg, targetOrigin) {
|
|
targetOrigin = targetOrigin || window.location.origin;
|
|
var seen = [];
|
|
var w = window;
|
|
var hops = 0;
|
|
function mark(win) {
|
|
for (var i = 0; i < seen.length; i++) if (seen[i] === win) return false;
|
|
seen.push(win);
|
|
return true;
|
|
}
|
|
function post(win) {
|
|
try {
|
|
win.postMessage(msg, targetOrigin);
|
|
} catch (e) {
|
|
try {
|
|
win.postMessage(msg, '*');
|
|
} catch (e2) {}
|
|
}
|
|
}
|
|
while (w && hops < 16) {
|
|
if (w !== window && mark(w)) post(w);
|
|
try {
|
|
if (!w.parent || w.parent === w) break;
|
|
w = w.parent;
|
|
} catch (e2) {
|
|
break;
|
|
}
|
|
hops++;
|
|
}
|
|
try {
|
|
if (window.top && window.top !== window && mark(window.top)) post(window.top);
|
|
} catch (e4) {}
|
|
}
|
|
|
|
function relayHubBackground(action, payload, timeoutMs) {
|
|
timeoutMs = timeoutMs || 20000;
|
|
return new Promise(function (resolve, reject) {
|
|
var send = findHubSendMessage();
|
|
if (send) {
|
|
try {
|
|
console.log('[qwb bg -> Hub.sendMessage]', action);
|
|
} catch (e) {}
|
|
Promise.resolve(send(action, payload || {}, timeoutMs))
|
|
.then(function (res) {
|
|
if (res && typeof res === 'object' && res.error) {
|
|
var errMsg =
|
|
typeof res.error === 'string'
|
|
? res.error
|
|
: res.message || JSON.stringify(res.error);
|
|
reject(new Error(errMsg));
|
|
return;
|
|
}
|
|
try {
|
|
console.log('[qwb bg <- Hub.sendMessage]', action, res);
|
|
} catch (e2) {}
|
|
resolve(res);
|
|
})
|
|
.catch(reject);
|
|
return;
|
|
}
|
|
if (!window.parent || window.parent === window) {
|
|
try {
|
|
if (!window.top || window.top === window) {
|
|
reject(new Error('Private app preview requires Qortal Hub (no parent window)'));
|
|
return;
|
|
}
|
|
} catch (e5) {
|
|
reject(new Error('Private app preview requires Qortal Hub (no parent window)'));
|
|
return;
|
|
}
|
|
}
|
|
var requestId = 'qwb-bg-' + Date.now() + '-' + Math.random().toString(36).slice(2, 9);
|
|
var done = false;
|
|
var onMsg = function (ev) {
|
|
var d = ev.data;
|
|
if (!d || d.type !== 'backgroundMessageResponse' || d.requestId !== requestId) return;
|
|
done = true;
|
|
window.removeEventListener('message', onMsg);
|
|
clearTimeout(tId);
|
|
try {
|
|
console.log('[qwb bg <- postMessage]', action, d);
|
|
} catch (e) {}
|
|
if (d.error) reject(new Error(String(d.error)));
|
|
else resolve(d.payload);
|
|
};
|
|
window.addEventListener('message', onMsg);
|
|
var msg = { type: 'backgroundMessage', action: action, requestId: requestId, payload: payload || {} };
|
|
try {
|
|
console.log('[qwb bg -> Hub postMessage]', action, requestId);
|
|
} catch (e) {}
|
|
try {
|
|
var targetOrigin = window.location.origin;
|
|
try {
|
|
if (window.top && window.top !== window && window.top.location.origin) {
|
|
targetOrigin = window.top.location.origin;
|
|
}
|
|
} catch (eOrigin) {
|
|
/* cross-origin top */
|
|
}
|
|
postHubBackgroundMessage(msg, targetOrigin);
|
|
} catch (e2) {
|
|
done = true;
|
|
window.removeEventListener('message', onMsg);
|
|
reject(e2);
|
|
return;
|
|
}
|
|
var tId = setTimeout(function () {
|
|
if (done) return;
|
|
done = true;
|
|
window.removeEventListener('message', onMsg);
|
|
reject(
|
|
new Error(
|
|
'Hub ' +
|
|
action +
|
|
' timed out after ' +
|
|
Math.round(timeoutMs / 1000) +
|
|
's (open this site inside Qortal Hub with API key configured in Settings → API)',
|
|
),
|
|
);
|
|
}, timeoutMs);
|
|
});
|
|
}
|
|
|
|
function readSessionHubApiKey() {
|
|
try {
|
|
var raw = sessionStorage.getItem('qwb_hub_node_api');
|
|
if (!raw) return null;
|
|
var o = JSON.parse(raw);
|
|
var url = o && o.url ? String(o.url).replace(/\/$/, '') : '';
|
|
var apikey = o && (o.apikey || o.apiKey) ? String(o.apikey || o.apiKey).trim() : '';
|
|
if (url && apikey) return { url: url, apikey: apikey };
|
|
} catch (e) {}
|
|
return null;
|
|
}
|
|
|
|
function hubCoreUrlWithApiKey(path, apiKeyObj) {
|
|
var p = path.charAt(0) === '/' ? path : '/' + path;
|
|
if (!apiKeyObj || !apiKeyObj.url || !apiKeyObj.apikey) return p;
|
|
var base = String(apiKeyObj.url).replace(/\/$/, '');
|
|
var full = base + p;
|
|
var sep = full.indexOf('?') >= 0 ? '&' : '?';
|
|
return full + sep + 'apiKey=' + encodeURIComponent(String(apiKeyObj.apikey));
|
|
}
|
|
|
|
function localHandler(req) {
|
|
var a = req && req.action;
|
|
if (a === 'GET_ACCOUNT_NAMES' && req.address) {
|
|
return getJson('/names/address/' + encodeURIComponent(req.address) + qs({ limit: req.limit, offset: req.offset, reverse: req.reverse }))
|
|
.catch(function () { return null; });
|
|
}
|
|
if (a === 'GET_NAME_DATA' && req.name) {
|
|
return getJson('/names/' + encodeURIComponent(req.name))
|
|
.catch(function () { return null; });
|
|
}
|
|
if (a === 'GET_ACCOUNT_DATA' && req.address) {
|
|
return getJson('/addresses/' + encodeURIComponent(req.address))
|
|
.catch(function () { return null; });
|
|
}
|
|
if (a === 'SEARCH_NAMES' && req.query) {
|
|
return getJson('/names/search' + qs({ query: req.query, prefix: req.prefix, limit: req.limit, offset: req.offset, reverse: req.reverse }))
|
|
.catch(function () { return null; });
|
|
}
|
|
if (a === 'LIST_QDN_RESOURCES' || a === 'SEARCH_QDN_RESOURCES') {
|
|
var base = a === 'SEARCH_QDN_RESOURCES' ? '/arbitrary/resources/search' : '/arbitrary/resources';
|
|
var o = {
|
|
service: req.service,
|
|
name: req.name,
|
|
identifier: req.identifier,
|
|
query: req.query,
|
|
title: req.title,
|
|
description: req.description,
|
|
prefix: req.prefix,
|
|
exactmatchnames: req.exactmatchnames,
|
|
default: req.default,
|
|
mode: req.mode,
|
|
minlevel: req.minlevel,
|
|
namefilter: req.namefilter,
|
|
followedonly: req.followedonly != null ? req.followedonly : req.followedOnly,
|
|
excludeblocked: req.excludeblocked != null ? req.excludeblocked : req.excludeBlocked,
|
|
includemetadata: req.includemetadata != null ? req.includemetadata : req.includeMetadata,
|
|
includestatus: req.includestatus != null ? req.includestatus : req.includeStatus,
|
|
before: req.before,
|
|
after: req.after,
|
|
limit: req.limit,
|
|
offset: req.offset,
|
|
reverse: req.reverse,
|
|
};
|
|
var url = base + qs(o);
|
|
if (Array.isArray(req.names)) {
|
|
req.names.forEach(function (x) {
|
|
url = url + '&name=' + encodeURIComponent(String(x));
|
|
});
|
|
}
|
|
if (Array.isArray(req.keywords)) {
|
|
req.keywords.forEach(function (x) {
|
|
url = url + '&keywords=' + encodeURIComponent(String(x));
|
|
});
|
|
}
|
|
return getJson(url);
|
|
}
|
|
if (a === 'GET_QDN_RESOURCE_STATUS' && req.service && req.name) {
|
|
return getJson(
|
|
'/arbitrary/resource/status/' +
|
|
encodeQdnSeg(req.service) +
|
|
'/' +
|
|
encodeQdnSeg(req.name) +
|
|
(req.identifier ? '/' + encodeQdnSeg(req.identifier) : ''),
|
|
);
|
|
}
|
|
if (a === 'GET_QDN_RESOURCE_URL' && req.service && req.name) {
|
|
var svcUrl = String(req.service).toUpperCase();
|
|
var u = '/arbitrary/' + encodeQdnSeg(req.service) + '/' + encodeQdnSeg(req.name);
|
|
if (req.identifier) u += '/' + encodeQdnSeg(req.identifier);
|
|
var pRaw = req.path != null ? String(req.path) : '';
|
|
var webLike = svcUrl === 'WEBSITE' || svcUrl === 'APP';
|
|
if (pRaw && webLike) {
|
|
u += pRaw.charAt(0) === '/' ? pRaw : '/' + pRaw;
|
|
}
|
|
var fpRaw = req.filepath != null && req.filepath !== '' ? String(req.filepath) : '';
|
|
if (!webLike && pRaw && !fpRaw) {
|
|
fpRaw = pRaw;
|
|
}
|
|
if (fpRaw) {
|
|
u +=
|
|
u.indexOf('?') >= 0
|
|
? '&filepath=' + encodeURIComponent(fpRaw)
|
|
: '?filepath=' + encodeURIComponent(fpRaw);
|
|
}
|
|
return Promise.resolve(u);
|
|
}
|
|
if (a === 'FETCH_QDN_RESOURCE' && req.service && req.name) {
|
|
/**
|
|
* IMPORTANT: do **NOT** intercept FETCH_QDN_RESOURCE locally for IMAGE / VIDEO / AUDIO
|
|
* or whenever `encoding: 'base64'` is requested. Plain `fetch().text()` mangles binary
|
|
* bytes into a garbled UTF-8 string, which then can't be base64-decoded — that's why
|
|
* the Q-Shop delivery map appears blank even though `qortalRequest` is being called.
|
|
* The Hub's native `qortalRequest` knows how to return real base64 for binary services.
|
|
* We only intercept locally for textual services (JSON / DOCUMENT) where reading as
|
|
* text is correct, and even then only when no `encoding` was requested.
|
|
*/
|
|
var svc = String(req.service).toUpperCase();
|
|
var binaryService = svc === 'IMAGE' || svc === 'VIDEO' || svc === 'AUDIO' || svc === 'FILE' || svc === 'THUMBNAIL';
|
|
var wantsBase64 = req.encoding && String(req.encoding).toLowerCase() === 'base64';
|
|
/** DOCUMENT/JSON base64 ciphertext (e.g. Hub private apps) is ASCII on the wire — REST is reliable; bridge FETCH often stalls. */
|
|
var restBase64Text =
|
|
wantsBase64 &&
|
|
(svc === 'DOCUMENT' || svc === 'DOCUMENT_PRIVATE' || svc === 'JSON');
|
|
if (binaryService || (wantsBase64 && !restBase64Text)) {
|
|
return null; // fall through to forwardToParent → Hub native qortalRequest
|
|
}
|
|
var url = '/arbitrary/' + encodeQdnSeg(req.service) + '/' + encodeQdnSeg(req.name);
|
|
if (req.identifier) url += '/' + encodeQdnSeg(req.identifier);
|
|
if (req.filepath) {
|
|
url += (url.indexOf('?') >= 0 ? '&' : '?') + 'filepath=' + encodeURIComponent(req.filepath);
|
|
} else if (wantsBase64) {
|
|
url += (url.indexOf('?') >= 0 ? '&' : '?') + 'encoding=base64';
|
|
}
|
|
return getText(url)
|
|
.then(function (text) {
|
|
var t = typeof text === 'string' ? text : String(text);
|
|
var s = t.trim();
|
|
if (s.length > 1 && (s.charAt(0) === '{' || s.charAt(0) === '[')) {
|
|
try { return JSON.parse(s); } catch (e) { /* not JSON */ }
|
|
}
|
|
return t;
|
|
})
|
|
.catch(function (e) {
|
|
try { console.log('[qwb FETCH rest fail → Hub]', req, e && e.message); } catch (ee) {}
|
|
return forwardToParent(req);
|
|
});
|
|
}
|
|
if (a === 'QWB_GET_HUB_CORE_API_KEY') {
|
|
var sess = readSessionHubApiKey();
|
|
if (sess) return Promise.resolve(sess);
|
|
return Promise.reject(
|
|
new Error(
|
|
'Node API key not set for this site session — use the form in the private app embed (from Hub Settings → API).',
|
|
),
|
|
);
|
|
}
|
|
if (a === 'QWB_BUILD_PRIVATE_APP_PREVIEW' && req.appName && req.zipBase64) {
|
|
function postPreview(apiKey) {
|
|
var path = '/arbitrary/APP/' + encodeQdnSeg(req.appName) + '/zip?preview=true';
|
|
var postUrl = hubCoreUrlWithApiKey(path, apiKey);
|
|
return fetch(postUrl, {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'text/plain' },
|
|
body: String(req.zipBase64),
|
|
credentials: 'include',
|
|
cache: 'no-store',
|
|
}).then(function (r) {
|
|
if (!r.ok) throw new Error('HTTP ' + r.status);
|
|
return r.text();
|
|
}).then(function (previewPath) {
|
|
previewPath = (previewPath || '').trim();
|
|
if (!previewPath) throw new Error('Empty preview path from node');
|
|
if (/^https?:\/\//i.test(previewPath)) {
|
|
if (apiKey && apiKey.apikey) {
|
|
try {
|
|
var u = new URL(previewPath);
|
|
u.searchParams.set('apiKey', String(apiKey.apikey));
|
|
return u.href;
|
|
} catch (e4) {
|
|
return previewPath;
|
|
}
|
|
}
|
|
return previewPath;
|
|
}
|
|
var rel = previewPath.charAt(0) === '/' ? previewPath : '/' + previewPath;
|
|
return hubCoreUrlWithApiKey(rel, apiKey);
|
|
});
|
|
}
|
|
var sessKey = readSessionHubApiKey();
|
|
if (sessKey) return postPreview(sessKey);
|
|
return relayHubBackground('getApiKey', {}, 20000)
|
|
.then(function (apiKey) {
|
|
if (apiKey && apiKey.url && apiKey.apikey) {
|
|
return postPreview(apiKey).catch(function () {
|
|
return postPreview(null);
|
|
});
|
|
}
|
|
return postPreview(null);
|
|
})
|
|
.catch(function () {
|
|
return postPreview(null);
|
|
});
|
|
}
|
|
return null;
|
|
}
|
|
|
|
window.__qwbBridgeLog = window.__qwbBridgeLog || [];
|
|
/** Align with Qortal q-apps.js `getDefaultTimeout` — Q-Mail / QDN publish must not abort at 60s. */
|
|
function qwbStubForwardTimeoutMs(action) {
|
|
if (!action) return 60000;
|
|
switch (action) {
|
|
case 'GET_USER_ACCOUNT':
|
|
case 'SAVE_FILE':
|
|
case 'SIGN_TRANSACTION':
|
|
case 'DECRYPT_DATA':
|
|
return 60 * 60 * 1000;
|
|
case 'SEARCH_QDN_RESOURCES':
|
|
return 30 * 1000;
|
|
case 'FETCH_QDN_RESOURCE':
|
|
return 60 * 1000;
|
|
case 'PUBLISH_QDN_RESOURCE':
|
|
case 'PUBLISH_MULTIPLE_QDN_RESOURCES':
|
|
return 60 * 60 * 1000;
|
|
case 'SEND_CHAT_MESSAGE':
|
|
return 60 * 1000;
|
|
case 'CREATE_TRADE_BUY_ORDER':
|
|
case 'CREATE_TRADE_SELL_ORDER':
|
|
case 'CANCEL_TRADE_SELL_ORDER':
|
|
case 'VOTE_ON_POLL':
|
|
case 'CREATE_POLL':
|
|
case 'JOIN_GROUP':
|
|
case 'DEPLOY_AT':
|
|
case 'SEND_COIN':
|
|
case 'TRANSFER_ASSET':
|
|
case 'SEND_PAYMENT':
|
|
return 5 * 60 * 1000;
|
|
case 'GET_WALLET_BALANCE':
|
|
return 2 * 60 * 1000;
|
|
case 'GET_NAME_DATA':
|
|
case 'GET_ACCOUNT_DATA':
|
|
case 'GET_ACCOUNT_NAMES':
|
|
return 5 * 60 * 1000;
|
|
case 'QWB_BUILD_PRIVATE_APP_PREVIEW':
|
|
return 15 * 1000;
|
|
case 'QWB_GET_HUB_CORE_API_KEY':
|
|
return 500;
|
|
default:
|
|
return 5 * 60 * 1000;
|
|
}
|
|
}
|
|
function forwardToParent(request, timeoutMs) {
|
|
return new Promise(function (resolve, reject) {
|
|
var channel = new MessageChannel();
|
|
var done = false;
|
|
var t0 = Date.now();
|
|
var waitMs =
|
|
timeoutMs != null && timeoutMs > 0 ? timeoutMs : qwbStubForwardTimeoutMs(request && request.action);
|
|
channel.port1.onmessage = function (ev) {
|
|
if (done) return;
|
|
done = true;
|
|
try { channel.port1.close(); } catch (e) {}
|
|
var d = ev && ev.data;
|
|
try {
|
|
window.__qwbBridgeLog.push({ t: Date.now() - t0, action: request && request.action, raw: d });
|
|
console.log('[qwb bridge <-]', request && request.action, d);
|
|
} catch (e) { /* */ }
|
|
if (d && typeof d === 'object' && !Array.isArray(d) && ('result' in d || 'error' in d)) {
|
|
var err = d.error;
|
|
if (err) {
|
|
var msg;
|
|
if (typeof err === 'string') msg = err;
|
|
else if (err && typeof err === 'object') msg = err.message || err.error || JSON.stringify(err);
|
|
else msg = String(err);
|
|
reject(new Error(msg));
|
|
return;
|
|
}
|
|
d = d.result;
|
|
} else if (d && typeof d === 'object' && !Array.isArray(d)) {
|
|
if ('data' in d && d.data !== undefined && !('address' in d && 'publicKey' in d)) d = d.data;
|
|
else if ('response' in d && d.response !== undefined) d = d.response;
|
|
}
|
|
if (d && d.error) {
|
|
reject(new Error(typeof d.error === 'string' ? d.error : JSON.stringify(d.error)));
|
|
} else {
|
|
resolve(d);
|
|
}
|
|
};
|
|
channel.port1.onmessageerror = function () {
|
|
if (done) return;
|
|
done = true;
|
|
reject(new Error('qortalRequest MessageChannel error'));
|
|
};
|
|
try {
|
|
var payload = Object.assign({}, request || {}, { requestedHandler: 'UI' });
|
|
try { console.log('[qwb bridge ->]', request && request.action, payload); } catch (e) { /* */ }
|
|
// Full public/qortal-bridge.js uses window.postMessage to the same document (it installs the listener).
|
|
// This minimal stub has no in-page handler: in a Hub iframe, forward to `parent` (allowed cross-origin).
|
|
if (window.parent && window.parent !== window) {
|
|
window.parent.postMessage(payload, '*', [channel.port2]);
|
|
} else {
|
|
window.postMessage(payload, '*', [channel.port2]);
|
|
}
|
|
} catch (e) {
|
|
if (done) return;
|
|
done = true;
|
|
reject(e);
|
|
return;
|
|
}
|
|
setTimeout(function () {
|
|
if (done) return;
|
|
done = true;
|
|
try { channel.port1.close(); } catch (e) {}
|
|
reject(new Error('qortalRequest timed out'));
|
|
}, waitMs);
|
|
});
|
|
}
|
|
|
|
try {
|
|
window.__QWB_QORTAL_BRIDGE_STUB__ = true;
|
|
} catch (e) {}
|
|
|
|
var QWB_LOCAL_ONLY_ACTIONS = {
|
|
QWB_GET_HUB_CORE_API_KEY: 1,
|
|
QWB_BUILD_PRIVATE_APP_PREVIEW: 1,
|
|
};
|
|
|
|
function qwbLocalOrForward(request, forwardTimeoutMs) {
|
|
var tm =
|
|
forwardTimeoutMs != null && forwardTimeoutMs > 0
|
|
? forwardTimeoutMs
|
|
: qwbStubForwardTimeoutMs(request && request.action);
|
|
var action = request && request.action;
|
|
var localOnly = !!(action && QWB_LOCAL_ONLY_ACTIONS[action]);
|
|
var local = null;
|
|
try {
|
|
local = localHandler(request);
|
|
} catch (e) {
|
|
if (localOnly) return Promise.reject(e);
|
|
local = null;
|
|
}
|
|
if (!local) {
|
|
if (localOnly) {
|
|
return Promise.reject(
|
|
new Error('QWB action not handled locally: ' + String(action || 'unknown')),
|
|
);
|
|
}
|
|
return forwardToParent(request, tm);
|
|
}
|
|
return Promise.resolve(local)
|
|
.then(function (result) {
|
|
if (result == null) {
|
|
if (localOnly) {
|
|
throw new Error(
|
|
action === 'QWB_GET_HUB_CORE_API_KEY'
|
|
? 'Could not read Hub API key — open Settings → API in Qortal Hub, then reload.'
|
|
: 'Could not build private app preview on this node.',
|
|
);
|
|
}
|
|
return forwardToParent(request, tm);
|
|
}
|
|
return result;
|
|
})
|
|
.catch(function (err) {
|
|
if (localOnly) throw err;
|
|
return forwardToParent(request, tm);
|
|
});
|
|
}
|
|
|
|
if (typeof window.qortalRequest !== 'function') {
|
|
window.qortalRequest = function (request) {
|
|
return qwbLocalOrForward(request, null);
|
|
};
|
|
}
|
|
|
|
if (typeof window.qortalRequestWithNoTimeout !== 'function') {
|
|
window.qortalRequestWithNoTimeout = function (request, effectiveTimeoutMs) {
|
|
var ms =
|
|
effectiveTimeoutMs != null && effectiveTimeoutMs > 0
|
|
? effectiveTimeoutMs
|
|
: qwbStubForwardTimeoutMs(request && request.action);
|
|
return qwbLocalOrForward(request, ms);
|
|
};
|
|
}
|
|
|
|
if (typeof window.qortalRequestWithTimeout !== 'function') {
|
|
window.qortalRequestWithTimeout = function (req, ms) {
|
|
var outer = ms != null && ms > 0 ? ms : qwbStubForwardTimeoutMs(req && req.action);
|
|
return qwbLocalOrForward(req, outer);
|
|
};
|
|
}
|
|
})();
|
|
</script>
|
|
<script>
|
|
(function () {
|
|
/**
|
|
* Embedded Q-App iframes use the same stub as this page: they postMessage the Hub via
|
|
* their *parent* — but for them the parent is this WEBSITE, not the Hub, so the port never
|
|
* reached a wallet. When this document already has a Hub bridge (user authenticated
|
|
* the site), we fulfill those child requests and reply on the MessageChannel.
|
|
*/
|
|
function isQAppChildFrameSource(src) {
|
|
if (!src || src === window) return false;
|
|
try {
|
|
var ifr = document.getElementsByTagName('iframe');
|
|
for (var i = 0; i < ifr.length; i++) {
|
|
if (ifr[i].contentWindow === src) return true;
|
|
}
|
|
} catch (e) {
|
|
return false;
|
|
}
|
|
return false;
|
|
}
|
|
/**
|
|
* Actions a CHILD Q-App embed must NOT be allowed to forward to the Hub: anything that
|
|
* mutates Hub's tab-level state (address bar, copy-link source, displayed-resource
|
|
* notification). Without this filter, an embedded Q-App (e.g. a Q-Blog iframe inside
|
|
* the website) calls `qortalRequest({ action: 'QDN_RESOURCE_DISPLAYED', service: 'APP',
|
|
* name: 'Q-Blog', identifier: 'default', path: '/Biohackers Corner/Blog' })` and Hub
|
|
* overwrites its address bar with the embed's URL — producing the duplicated-name
|
|
* `qortal://WEBSITE/<name>/<name>/<slug>` "Copy link" output the user reported.
|
|
*
|
|
* The embedded Q-App still runs normally (Hub-bridge calls for fetching data, signing,
|
|
* etc. are forwarded as before); we only swallow the resource-display notification with
|
|
* a synthetic success reply so the child doesn't await forever or surface an error.
|
|
*/
|
|
var BLOCKED_CHILD_ACTIONS = {
|
|
QDN_RESOURCE_DISPLAYED: 1,
|
|
QDN_RESOURCE_HIDDEN: 1,
|
|
/** Defensive: same family of "tell the Hub I'm now showing X" notifications. */
|
|
LINK_TO_QDN_RESOURCE: 1,
|
|
};
|
|
|
|
window.addEventListener('message', function (ev) {
|
|
if (!ev || !ev.data) return;
|
|
if (!ev.ports || !ev.ports[0]) return;
|
|
if (typeof ev.data.action !== 'string' || !ev.data.action) return;
|
|
if (!isQAppChildFrameSource(ev.source)) return;
|
|
var port = ev.ports[0];
|
|
function replyRes(res) {
|
|
try {
|
|
port.postMessage({ result: res, error: null });
|
|
} catch (e) {}
|
|
}
|
|
function replyErr(err) {
|
|
var msg;
|
|
if (err == null) msg = 'Unknown error';
|
|
else if (typeof err === 'string') msg = err;
|
|
else if (err && typeof err === 'object' && 'message' in err) msg = String((err).message);
|
|
else msg = String(err);
|
|
try {
|
|
port.postMessage({ result: null, error: { message: msg } });
|
|
} catch (e) {}
|
|
}
|
|
/**
|
|
* Block child-iframe attempts to claim Hub's address bar. Reply "ok" so the embed's
|
|
* `qortalRequest` Promise resolves (the embed doesn't actually need the Hub-side
|
|
* effect for anything to render correctly — it's purely a notification).
|
|
*/
|
|
if (BLOCKED_CHILD_ACTIONS[ev.data.action]) {
|
|
try {
|
|
console.log('[qwb bridge ←✗] swallowed child action (would clobber Hub address bar):', ev.data.action, ev.data);
|
|
} catch (e) {}
|
|
replyRes({ ok: true });
|
|
return;
|
|
}
|
|
if (typeof window.qortalRequest === 'function') {
|
|
var clean = {};
|
|
for (var k in ev.data) {
|
|
if (Object.prototype.hasOwnProperty.call(ev.data, k) && k !== 'requestedHandler') clean[k] = ev.data[k];
|
|
}
|
|
var run =
|
|
typeof window.qortalRequestWithNoTimeout === 'function'
|
|
? window.qortalRequestWithNoTimeout(clean)
|
|
: window.qortalRequest(clean);
|
|
run.then(replyRes, function (e) { replyErr(e); });
|
|
return;
|
|
}
|
|
if (window.top && window.top !== window) {
|
|
try {
|
|
window.top.postMessage(ev.data, '*', [port]);
|
|
} catch (e) {
|
|
replyErr(e);
|
|
}
|
|
return;
|
|
}
|
|
replyErr('No Qortal bridge on this page for embedded Q-App');
|
|
});
|
|
})();
|
|
</script>
|
|
<script src="./zip-store.js"></script>
|
|
<script type="module" src="./src/publicSiteEntry.tsx"></script>
|
|
</body>
|
|
</html>
|