dcp-client
Version:
Core libraries for accessing DCP network
421 lines (378 loc) • 16.8 kB
JavaScript
/**
* @file dcp-client.js
* Interface/loader for web browser consumers of the dcp-client bundle.
*
* Once this script has been loaded in a vanilla-web environment, the dcp and dcpConfig
* symbols should be available to any scripts below this one in the DOM tree. This
* script is also aware of the following attributes on its own SCRIPT tag:
* - onload: code to run when completely loaded; dcpConfig will be defined.
* - onready: same as onload, but dcp is also defined.
* - scheduler: set the URL for the scheduler, also used to locate dcpConfig
* - bundle: set the URL for the bundle to load, default is the one in this directory
* @author Wes Garland, wes@kingsds.network
* @date Aug 2019
*/
;
(function namespaceIIFE() {
console.log(`%c
_____ _____ ___________ _
/ ___|_ _| _ | ___ \\ | |
\\ \`--. | | | | | | |_/ / | |
\`--. \\ | | | | | | __/ |_|
/\\__/ / | | \\ \\_/ / | _
\\____/ \\_/ \\___/\\_| |_|
%c
The console is a browser feature intended for developers. If somebody told you to paste something here it may be a scam and your information could be stolen. Help us keep security in mind and keep your data safe.
~ DCP Team
https://distributive.network/`, "font-weight: bold; font-size: 1.2em; color: #00a473;", "font-size: 1.2em;");
var _dcpConfig = typeof dcpConfig === 'object' ? dcpConfig : undefined;
{
let allScripts = document.getElementsByTagName('SCRIPT');
let thisScript = allScripts[allScripts.length - 1];
let thisScriptURL = new URL(thisScript.src)
let schedulerURL;
let dcpConfigHref = thisScript.getAttribute('dcpConfig') /* camel case is wrong+deprecated */
|| thisScript.getAttribute('dcp-config');
let configScript;
/* kebab to camel case */
function k2cCase(kebab)
{
return kebab.replace(/(-)([a-z])/g, (ignore, dash, char) => char.toUpperCase());
}
if (_dcpConfig && _dcpConfig.scheduler && _dcpConfig.scheduler.location && _dcpConfig.scheduler.location.href)
schedulerURL = new URL(_dcpConfig.scheduler.location.href);
else if (thisScript.getAttribute('scheduler'))
schedulerURL = new URL(thisScript.getAttribute('scheduler'));
if (!dcpConfigHref) {
if (schedulerURL)
dcpConfigHref = schedulerURL.origin + schedulerURL.pathname + 'etc/dcp-config.js' + (schedulerURL.search || thisScriptURL.search);
else
dcpConfigHref = thisScriptURL.origin + thisScriptURL.pathname.replace(/\/dcp-client\/dcp-client.js$/, '/etc/dcp-config.js') + thisScriptURL.search;
}
/**
* <script src="dcp-client" dcp-config.scheduler.task-distributor.setting="string:foo">
* Supported "types":
* - json:
* - url:
* - string:
* - number:
* - true
* - false
*/
for (let attrib of thisScript.attributes)
{
let node;
let { name, value } = attrib;
if (!name.startsWith('dcp-config.'))
continue;
if (value.startsWith('string:'))
value = value.slice(7);
else if (value.startsWith('number:'))
value = Number(value.slice(7));
else if (value.startsWith('json:'))
value = JSON.parse(value.slice(5));
else if (value === 'true')
value = true;
else if (value === 'false')
value = false;
else if (value.startsWith('url:'))
value = new URL(value.slice(4));
else
{
console.error(`dcp-client: invalid value for attribute ${name}`);
continue;
}
let edge;
let path = name.split('.');
path.shift();
if (!_dcpConfig)
_dcpConfig = {};
node = _dcpConfig;
while (path.length > 1)
{
edge = k2cCase(path.shift());
if (!node.hasOwnProperty(edge))
node[edge] = {};
node = node[edge];
}
edge = k2cCase(path.shift());
node[edge] = value;
}
/** Load dcp-config.js from scheduler, and merge with running dcpConfig */
function loadConfig()
{
configScript = document.createElement('SCRIPT');
configScript.setAttribute('type', 'text/javascript');
configScript.setAttribute('src', dcpConfigHref);
configScript.setAttribute('id', '_dcp_config');
if (!thisScript.id)
thisScript.id = '_dcp_client_loader';
let html = configScript.outerHTML;
/* If we know about an alternate scheduler location - by any means but usually script attribute -
* add it into our local configuration delta; generate this delta as-needed.
*/
if (schedulerURL)
{
if (!_dcpConfig)
_dcpConfig = {};
if (!_dcpConfig.scheduler)
_dcpConfig.scheduler = {};
_dcpConfig.scheduler.location = schedulerURL;
}
/* Preserve the config delta so that it can be merged on top of the remote config, before the
* bundle is completely initialized. The global dcpConfig is replaced by this new script.
*/
if (_dcpConfig)
{
thisScript.mergeConfig = mergeConfig;
html += `<script>document.getElementById('${thisScript.id}').mergeConfig();</scr` + 'ipt>';
}
document.write(html);
configScript.onerror = (function(e) {
alert('Error DCP-1001: Could not load or parse scheduler configuration from URL ("' + configScript.getAttribute('src') + '")');
console.error('dcpConfig load error: ', e);
}).toString();
}
function mergeConfig()
{
const mergedConf = leafMerge(dcpConfig, _dcpConfig);
Object.assign(dcpConfig, mergedConf);
function leafMerge() /* lifted from dcp-client obj-merge.js c32e780fae88071df1bb4aebe3282220d518260e */
{
var target = {};
for (let i=0; i < arguments.length; i++)
{
let neo = arguments[i];
if (neo === undefined)
continue;
for (let prop in neo)
{
if (!neo.hasOwnProperty(prop))
continue;
if (typeof neo[prop] === 'object' && neo[prop] !== null && !Array.isArray(neo[prop]) && ['Function','Object'].includes(neo[prop].constructor.name))
target[prop] = leafMerge(target[prop], neo[prop]);
else
target[prop] = neo[prop];
}
}
return target;
}
}
/**
* Utility code for loading content relative to the Distributive CDN or Auth subsystems, using
* information about their locations gleaned from dcpConfig. Since the browser is unable to preload
* content based on these tags, page load will be slightly slower than using hard-coded tags. Note
* also that loading scripts this way has may cause them to load after the DOMContentLoadeded event
* has fired; caveat elitor.
*
* Examples:
* 1. load stylesheet relative to CDN
* <link type="text/css" type="stylesheet" dcp-cdn-href="/path/to/dcp-style.css">
* 2. load javascript relative to auth host
* <script dcp-auth-src="/path/to/dcp-script.js"></script>
*/
function initMagicAttribs()
{
if (true
&& thisScript.getAttribute('disable-magic-attribs')
&& thisScript.getAttribute('disable-magic-attribs') !== 'false')
return;
function resolveWithQueryString(dcpUrl, urlTail)
{
const components = urlTail.split('?');
const pathname = components[0];
const queryString = components[1] ? ('?' + components[1]) : '';
return String(dcpUrl.resolve(pathname)) + queryString;
}
function scanForTags()
{
if (scanForTags.busy)
return;
scanForTags.busy = true;
try
{
const magic = { cdn: 'cdn', auth: 'pxAuth' }; /* xlate attrib fragment to conf prop */
for (const service in magic)
{
const srcAttribName = `dcp-${service}-src`;
const hrefAttribName = `dcp-${service}-href`;
const scripts = document.querySelectorAll(`SCRIPT[${srcAttribName}]`);
const location = dcpConfig[magic[service]].location;
for (const script of scripts)
{
const src = script.getAttribute(srcAttribName);
script.removeAttribute(srcAttribName);
script.src = resolveWithQueryString(location, src);
}
const links = document.querySelectorAll(`LINK[${hrefAttribName}]`);
for (const link of links)
{
const href = link.getAttribute(hrefAttribName);
link.removeAttribute(hrefAttribName);
link.href = resolveWithQueryString(location, href);
}
}
}
catch (error)
{
console.error(error);
throw error;
}
finally
{
scanForTags.busy = false;
}
} /* scanForTags */
document.addEventListener('readystatechange', scanForTags);
/* Watch DOM for mutations, initialize new magic relative attrib tags as they appear. */
function setupMutationObserver()
{
function handleMutation(mutationList, observer)
{
for (const mutation of mutationList)
{
if (true
&& mutation.type === 'childList'
&& mutation.target.querySelectorAll('LINK:not([href]), SCRIPT:not([src])').length)
{
scanForTags();
break;
}
}
}
scanForTags(); /* handle mutations that were missed to due startup races */
const observer = new MutationObserver(handleMutation);
observer.observe(document.head, { childList: true, subtree: true });
if (document.body)
observer.observe(document.body, { childList: true, subtree: true });
else
window.addEventListener('DOMContentLoaded', () => observer.observe(document.body, { childList: true, subtree: true }));
}
}
/**
* This function is never run directly; it is stringified and emitted in a
* SCRIPT tag that is injected into the document. As such, it cannot close
* over any non-global variables.
*/
async function bundleReadyIIFE() {
const bundleScript = document.getElementById("_dcp_client_bundle");
const ready = bundleScript.getAttribute('onready');
const dcp = bundleScript.exports;
const KVIN = new dcp.kvin.KVIN();
const thisScript = document.currentScript;
const ihc = (true
&& thisScript.hasAttribute('inherit-config')
&& thisScript.getAttribute('inherit-config') !== 'false'
&& (window !== top || window.opener));
KVIN.userCtors.dcpUrl$$DcpURL = dcp['dcp-url'].DcpURL;
KVIN.userCtors.dcpEth$$Address = dcp.wallet.Address;
if (typeof module !== 'undefined' && typeof module.declare !== 'undefined')
require('/internal/dcp/cjs2-shim').init(bundleScript.exports); /* CommonJS Modules/2.0d8 environment (BravoJS, NobleJS) */
else
window.dcp = dcp; /* vanilla JS */
/** Let protocol know where we got out config from, so origin can be reasoned about vis a vis security */
if (ihc)
dcp.protocol.setSchedulerConfigLocation_fromScript((window.opener || top).document.getElementById("_dcp_config"));
else
dcp.protocol.setSchedulerConfigLocation_fromScript(document.getElementById("_dcp_config"));
/**
* Slide baked-in config underneath the remote config to provide default values, unless inheriting
* config, in which case we deep-clone to get correct constructors for this context etc
*/
if (!ihc)
{
dcpConfig = dcp['dcp-config'] = dcp.utils.leafMerge(KVIN.unmarshal(dcp['dcp-default-config']), dcp['dcp-config']);
/**
* Transform instances of Address-like values into Addresses. Necessary since
* the config can't access the Address class before the bundle is loaded.
*/
dcp.wallet.Address.patchUp(dcpConfig);
dcp['dcp-url'].patchup(dcpConfig);
}
else
{
const parent = window.opener || top;
dcpConfig = dcp['dcp-config'] = KVIN.unmarshal(await parent.dcp.kvin.marshalAsync(parent.dcpConfig));
}
if (ready)
window.setTimeout(function bundleReadyFire() { let indirectEval=eval; indirectEval(ready) }, 0);
window.dispatchEvent(new CustomEvent('dcpclientbundleready', { detail: { dcp, KVIN } })); /* fires as soon as bundle has been parsed */
window.dispatchEvent(new CustomEvent('dcpclientready', { detail: dcp })); /* fires as soon as bundle has been parsed and dcp-config is ready */
}
/* Load dcp-client bundle from the same location as this module, extract the exports
* from it, and attach them to the global dcp object.
*
* @param {boolean} ihc true if we should inject the inherit-config attribute onto the
* bundle's script element
*/
function loadBundle(ihc) {
var bundleScript = document.createElement('SCRIPT');
var bundleSrc = thisScript.getAttribute("bundle") || (thisScript.src.replace('/dcp-client.js', '/dist/dcp-client-bundle.js'));
bundleScript.setAttribute('type', 'text/javascript');
bundleScript.setAttribute('src', bundleSrc);
bundleScript.setAttribute('id', '_dcp_client_bundle');
bundleScript.setAttribute('dcp-env', 'vanilla-web');
bundleScript.setAttribute('onerror', `alert('Error DCP-1002: Could not load dcp-client bundle from URL ("${bundleSrc}")')`);
bundleScript.setAttribute('onload', thisScript.getAttribute('onload'));
thisScript.removeAttribute('onload');
bundleScript.setAttribute('onready', thisScript.getAttribute('onready'));
thisScript.removeAttribute('onready');
document.write(bundleScript.outerHTML);
document.write(`<script id='_dcp_bundleReadyIIFE' inherit-config="${ihc}">/* bundleReadyIIFE */;(${bundleReadyIIFE})()</scr` + `ipt>`);
bundleScript = document.getElementById('_dcp_client_bundle');
if (bundleScript)
bundleScript.onerror = function(e) {
console.error('Bundle load error:', e);
bundleScript.removeAttribute('onready');
};
}
/* Load the default CSS. This is enough to make the modals, etc, work without affecting the user
* program appearance. If desired, a full DCP style can be loaded from the Distributive CDN; i.e.
* <link rel="stylesheet" type="text/css" dcp-cdn-href="/css/dcp-style.css">
*/
function loadCSS () {
var href = thisScript.getAttribute('load-css');
if (href === 'false')
return;
href = thisScriptURL.origin + thisScriptURL.pathname
.replace(/\/dcp-client\/dcp-client.js$/, '/dcp-client/assets/dcp-client.css');
const head = document.getElementsByTagName('head')[0];
let styleLink = document.createElement('link');
styleLink.rel = 'stylesheet';
styleLink.href = href;
head.appendChild(styleLink);
}
/**
* Determine if an inherit attribute has been set on an element IFF we are in a heritable context;
* eg the document has been loaded into a popup or frame with the same origin.
*/
function hasInheritAttribute(element, suffix)
{
return (true
&& element.hasAttribute(`inherit-${suffix}`)
&& element.getAttribute(`inherit-${suffix}`) !== 'false'
&& (window !== top || window.opener)
&& window.location.origin == (window.opener || top).location.origin);
}
const ihc = hasInheritAttribute(thisScript, 'config');
if (!ihc)
loadConfig();
else
window.dcpConfig = (window.opener || top).dcpConfig; /* temp plumbing, re-written in bundleReadyIIFE */
initMagicAttribs();
loadCSS();
if (hasInheritAttribute(thisScript, 'identity'))
{
window.addEventListener('dcpclientbundleready', ev => {
const { dcp, KVIN } = ev.detail;
const inheritedIdentity = (window.opener || top).dcp.identity.get();
dcp.identity.set(inheritedIdentity);
});
}
/* warning: bundle inheritance has context problems; avoid using this feature for non-trivial tasks /wg sep 2025 */
if (hasInheritAttribute(thisScript, 'bundle'))
window.dcp = window.opener ? window.opener.dcp : top.dcp;
else
loadBundle(ihc);
}
}) /* namespaceIIFE */();