@ideem/zsm-client-sdk-legacy-support
Version:
A monolithic library exporting UMFAClient via Web-Worker, legacy-support edition.
393 lines (348 loc) • 22.2 kB
JavaScript
// Alias environment-specific global object
const GLOBAL = typeof globalThis !== "undefined" ? globalThis
: typeof self !== "undefined" ? self
: typeof window !== "undefined" ? window
: typeof global !== "undefined" ? global
: (() => { throw new Error("Unable to determine global scope"); })();
// Error class for worker errors
class WorkerError extends Error {
constructor(name, message, ...details) {
super(message);
this.name = name ? `Worker ${name} Error` : 'Worker Error';
if (details.length) this.details = details;
if (Error.captureStackTrace) {
Error.captureStackTrace(this, WorkerError);
} else {
this.stack = (new Error(message)).stack;
}
}
}
class ZSMClientBase {
constructor(config) {
this.config = config;
this.clientWorker = null;
this.timeoutTimer = null;
GLOBAL.zsmClient = null;
}
async init(clientType = 'UMFAClient', config = this.config) {
if(GLOBAL.clientWorker) GLOBAL.clientWorker.terminate();
const workerPath = GLOBAL?.workerPath ? GLOBAL.workerPath
: config?.workerPath ? config.workerPath
: './worker.js';
const pathResolver = (import.meta && import.meta?.resolve) ? import.meta?.resolve(workerPath)
: (import.meta && import.meta?.url) ? import.meta?.url.replace(/\/[^\/]+$/, '/' + workerPath)
: document?.currentScript ? document?.currentScript?.src?.replace(/\/[^\/]+$/, '/' + workerPath)
: workerPath;
GLOBAL.clientWorker = this.clientWorker = new Worker(new URL(pathResolver));
GLOBAL.zsmClient = this;
await this.callWorkerFunction("init", {config, clientType}, false);
return this;
}
async callWorkerFunction(functionName, data, acknowledgementExpected=true) {
return new Promise((resolve, reject) => {
this.clientWorker.postMessage({functionName, data });
if(acknowledgementExpected) this.timeoutTimer = GLOBAL.setTimeout(() => reject(new WorkerError('Transaction', 'Transaction timed out after 10 seconds!')), 10000);
this.clientWorker.onmessage = (event) => {
GLOBAL.clearTimeout(this.timeoutTimer);
let workerResponse = event.data;
if(GLOBAL.showWorkerTelemetry && (workerResponse.result || workerResponse.error)) {
console.groupCollapsed(`%cWorker posted to Main Thread a response to invocation:`, 'font-weight:bold; color:#F60;', functionName)
console.log(`%c Function Invoked:`, 'font-weight:100; color:#AAA;', workerResponse.functionName);
console.log(`%c Success Status:`, 'font-weight:100; color:#AAA;', workerResponse.success ? 'true' : 'false');
console.log(`%c Result value:`, 'font-weight:100; color:#AAA;', (workerResponse.success ? workerResponse.result : workerResponse.error));
console.log(`%c Response Object:`, 'font-weight:100; color:#AAA;', workerResponse);
console.groupEnd();
}
if (workerResponse instanceof Error || workerResponse.error || !workerResponse.success) {
return reject(workerResponse.error);
}
resolve(workerResponse.result);
};
this.clientWorker.onerror = (err) => { GLOBAL.clearTimeout(this.timeoutTimer); reject(new WorkerError('', err.message)); };
this.clientWorker.onmessageerror = (err) => { GLOBAL.clearTimeout(this.timeoutTimer); reject(new WorkerError('Message', err.message)); };
});
}
}
class UMFAClient extends ZSMClientBase {
constructor(config) {
super(config);
this.init = super.init.bind(this);
this.callWorkerFunction = super.callWorkerFunction.bind(this);
this.checkEnrollment = this.checkEnrollment.bind(this);
this.enroll = this.enroll.bind(this);
this.authenticate = this.authenticate.bind(this);
this.optimizedEnroll = this.optimizedEnroll.bind(this);
this.optimizedAuthenticate = this.optimizedAuthenticate.bind(this);
this.init('UMFAClient');
}
async checkEnrollment(user) { return await this.callWorkerFunction('checkEnrollment', user); }
async enroll(user) { return await this.callWorkerFunction('enroll', user); }
async authenticate(user) { return await this.callWorkerFunction('authenticate', user); }
async optimizedEnroll(user) { return await this.callWorkerFunction('optimizedEnroll', user); }
async optimizedAuthenticate(user) { return await this.callWorkerFunction('optimizedAuthenticate', user); }
}
class FIDO2Client extends ZSMClientBase {
constructor(config) {
super(config);
this.init = super.init.bind(this);
this.callWorkerFunction = super.callWorkerFunction.bind(this);
this.checkEnrollment = this.checkEnrollment.bind(this);
this.checkIdentity = this.checkIdentity.bind(this);
this.webauthnCreate = this.webauthnCreate.bind(this);
this.webauthnGet = this.webauthnGet.bind(this);
this.webauthnRetrieve = this.webauthnRetrieve.bind(this);
this.init('FIDO2Client');
}
async checkIdentity(user, auto=true) { return await this.callWorkerFunction('checkIdentity', [user, auto]); }
async checkEnrollment(user) { return await this.callWorkerFunction('checkEnrollment', user); }
async webauthnCreate(user) { return await this.callWorkerFunction('webauthnCreate', user); }
async webauthnGet(user) { return await this.callWorkerFunction('webauthnGet', user); }
async webauthnRetrieve(user) { return await this.callWorkerFunction('webauthnRetrieve', user); }
}
function amendPerformanceLoggingToClass(cls) {
GLOBAL.sessionStamp = new Date();
GLOBAL.sessionStamp = new Date(GLOBAL.sessionStamp.getTime() - (GLOBAL.sessionStamp.getTimezoneOffset() * 60000)).toISOString().slice(11, -5);
GLOBAL.exportCount = 0;
if(GLOBAL.logStack == null) {
GLOBAL.logStack = [];
GLOBAL.getLogs = () => GLOBAL.logStack.sort((a, b) => a.stamp - b.stamp);
GLOBAL.clearLogs = () => {GLOBAL.logStack = []; };
GLOBAL.showLogs = () => {
if(!GLOBAL.logStack || GLOBAL.logStack.length === 0) return console.info('There are no logs currently in the stack!');
const getTime = (ISOStamp) => new Date(ISOStamp).toISOString().split('T')[1].slice(0,-1);
(function parseLog(stack=[...GLOBAL.logStack], parentEndTime = Infinity) {
while (stack.length > 0 && stack[0].startTime < parentEndTime) {
let markS, markE, spanName, indE, duration, stamps;
markS = stack.shift();
spanName = markS.name.slice(0, -6);
indE = stack.findIndex(log => log.name === spanName + "-END");
if (indE !== -1) markE = stack.splice(indE, 1)[0];
duration = (markE != null) ? (markE.startTime - markS.startTime).toFixed(4) + 'ms' : 'ONGOING';
stamps = (markE != null) ? getTime(markS.stamp) + " - " + getTime(markE.stamp) : getTime(markS.stamp) + " - ...";
console.groupCollapsed(spanName.replace(/-/g, ' :: ') + ' :: ' + duration + " (" + stamps + ")");
parseLog(stack, markE.startTime);
console.groupEnd();
}
})()
}
GLOBAL.addPerfMarker = (tag) => {
const {name, startTime} = performance.mark(tag).toJSON();
GLOBAL.logStack.push({
type : "MAINTHREAD",
order : GLOBAL.logStack.length,
name,
absTime : startTime,
stamp : performance.timeOrigin + startTime
});
}
}
return class extends cls {
constructor(...args) {
super(...args);
Object.getOwnPropertyNames(this).forEach(propName => {
if (propName === "constructor" || typeof this[propName] !== "function") return;
const originalMethod = this[propName];
let markerTagPrefix = `[MAINTHREAD]-${cls.name}-${propName}`;
this[propName] = function(...args) {
if(propName === "callWorkerFunction") markerTagPrefix = `[MAINTHREAD]-${cls.name}-${propName}-${args[0]}`;
GLOBAL.addPerfMarker(markerTagPrefix + "-START");
const result = originalMethod.apply(this, args);
if (result && typeof result.then === 'function') {
return result.finally(() => GLOBAL.addPerfMarker(markerTagPrefix + "-END"));
} else {
GLOBAL.addPerfMarker(markerTagPrefix + "-END");
return result;
}
};
});
}
};
}
//
if(typeof UMFAClient !== "undefined" && UMFAClient instanceof Function) UMFAClient = amendPerformanceLoggingToClass(UMFAClient);
if(typeof FIDO2Client !== "undefined" && FIDO2Client instanceof Function) FIDO2Client = amendPerformanceLoggingToClass(FIDO2Client);
let masterLogs = [];
if(window){
GLOBAL.getAllLogs = async (showCSV) => {
let res = await zsmClient.callWorkerFunction("getLogs");
let {logs, timeOrigin} = JSON.parse(res);
let timeDifferential = performance.timeOrigin - timeOrigin;
if(!showCSV) {
console.groupCollapsed('RAW logs');
GLOBAL.logStack.forEach(mainRecord => console.info('mainRecord :', mainRecord));
logs.forEach(workerRecord => {
workerRecord.absTime = workerRecord.absTime - timeDifferential;
console.info('workerRecord :', workerRecord);
});
console.groupEnd();
}
let allLogs = [...GLOBAL.logStack, ...logs];
allLogs = allLogs.sort((a, b) => a.absTime - b.absTime);
allLogs = allLogs.map((log, i) => Object.assign(log, {stamp: log.absTime + performance.timeOrigin, order:i}));
if(!showCSV) {
console.groupCollapsed('ALL logs (Combined and Sorted)');
masterLogs = [...masterLogs, ...allLogs];
allLogs.forEach(log => console.info('log :', log));
console.groupEnd();
}
return allLogs;
};
GLOBAL.showLogs = async (showCSV=false) => {
const getTime = (ISOStamp) => new Date(ISOStamp).toISOString().split('T')[1].slice(0,-1);
const stripStartEnd = (name) => name.replace(/-(START|END)$/gi, '');
function recursivelyMoveChildLogsIntoStartNode(stack) {
let processedLogs = [];
while (stack.length > 0) {
let startingMarker = stack.shift();
if(/-END$/i.test(startingMarker.name)) {
continue;
}
let endingMarker = stack.findIndex(log => log.name === stripStartEnd(startingMarker.name) + '-END'),
dateTimeStamps = [],
stackSubset = [],
totalDuration = 0,
networkLatency = 0,
computeDuration = 0;
if(!~endingMarker) {
stackSubset = stack;
endingMarker = stack.length;
totalDuration = 'ONGOING';
dateTimeStamps.push(getTime(startingMarker.stamp), '...?');
}else{
stackSubset = stack.splice(0, endingMarker+1);
endingMarker = stackSubset[stackSubset.length-1];
totalDuration = endingMarker.absTime - startingMarker.absTime;
dateTimeStamps.push(getTime(startingMarker.stamp), getTime(endingMarker.stamp));
}
processedLogs.push(Object.assign(startingMarker, {
name: stripStartEnd(startingMarker.name),
children: recursivelyMoveChildLogsIntoStartNode(stackSubset),
totalDuration,
dateTimeStamps,
networkLatency,
computeDuration
}));
}
return processedLogs;
}
function recursivelyComputeCumulativeNetworkLatency(node) {
let nodeLatency = 0;
if(node.clientToServerLatency) nodeLatency += node.clientToServerLatency;
if(node.serverToClientLatency) nodeLatency += node.serverToClientLatency;
if (Array.isArray(node.children) && node.children.length > 0) {
node.children.forEach(child => {
nodeLatency += recursivelyComputeCumulativeNetworkLatency(child);
});
}
node.networkLatency = nodeLatency;
return nodeLatency;
}
function calculateComputeDurationForNode(node) {
if(!node.children) return node.computeDuration = totalDuration;
node.computeDuration = (node.children.reduce((acc,child)=>{
return acc - child.totalDuration;
}, node.totalDuration))
node.children.forEach(calculateComputeDurationForNode)
}
let exportableStack = [];
function recursivelyOutputLogsToConsole(stack, depth = 0) {
let outputColors={
'MAINTHREAD': '#3f0b8a',
'WORKER': '#00438c',
'SERVER': '#027264',
};
function formatNumberColumn(num, NaNValue='0.000') {
let opValue = isNaN(num) ? NaNValue : Math.abs(num).toFixed(3);
let lPadStr = ' '.repeat(12 - opValue.length);
return lPadStr + opValue;
}
stack.forEach(item => {
let spacePadding = ' '.repeat(100);
let exportableRecord = {}
let sequence = item.order;
let threadName = (item.name.replace(/^\[([A-Z]+)\]-.*$/, '$1') + spacePadding).slice(0, 14);
let processName = (item.name.replace(/^\[[A-Z]+\]-(.*)$/, '$1') + spacePadding).slice(0, 70-(depth*2));
let totalDuration = formatNumberColumn(item?.totalDuration, 'ONGOING');
let computeDuration = (/ONGOING/.test(totalDuration)) ? formatNumberColumn('ONGOING', 'ONGOING') : formatNumberColumn(item?.computeDuration);
let networkLatency = (/ONGOING/.test(totalDuration)) ? formatNumberColumn('ONGOING', 'ONGOING') : formatNumberColumn(item?.networkLatency);
let cumulativeCompute;
if(/ONGOING/.test(totalDuration)) cumulativeCompute = formatNumberColumn('ONGOING', 'ONGOING');
else {
if(totalDuration < (Math.abs(networkLatency) + Math.abs(computeDuration))) totalDuration = formatNumberColumn(Math.abs(networkLatency) + Math.abs(computeDuration));
cumulativeCompute = formatNumberColumn((totalDuration - networkLatency - computeDuration), 'ONGOING');
}
let indentionPads = depth > 0 ? ' '.repeat(depth-1) + ' ' : ' ';
let detailPads = depth > 0 ? ' '.repeat(depth-1) + ' '.repeat(threadName.length) + ' ' : ' ' + ' '.repeat(threadName.length);
let timeSpan = item?.dateTimeStamps ? (' ' + item.dateTimeStamps[0]) : 'N/A';
if(!showCSV) {
let outputLine = ` ${threadName}${indentionPads} ${processName} ${totalDuration} ${networkLatency} ${cumulativeCompute} ${computeDuration} ${timeSpan}`;
console.groupCollapsed(`%c${outputLine}`, `color: ${outputColors[item.type]}; font-weight: 500;`);
console.info(`%c${detailPads} Sequence: `, `color: ${outputColors[item.type]}; font-weight: 700;`, sequence);
console.info(`%c${detailPads} Thread Name: `, `color: ${outputColors[item.type]}; font-weight: 700;`, threadName.trim());
console.info(`%c${detailPads} Process Name: `, `color: ${outputColors[item.type]}; font-weight: 700;`, processName.trim());
console.info(`%c${detailPads} Abs. Performance Mark: `, `color: ${outputColors[item.type]}; font-weight: 700;`, (item?.absTime??'') + 'ms from timeOrigin');
console.info(`%c${detailPads} TimeStamp (Start): `, `color: ${outputColors[item.type]}; font-weight: 700;`, (item?.dateTimeStamps[0]??''));
console.info(`%c${detailPads} TimeStamp (End): `, `color: ${outputColors[item.type]}; font-weight: 700;`, (item?.dateTimeStamps[1]??''));
console.info(`%c${detailPads} Cumulative Tot. Duration: `, `color: ${outputColors[item.type]}; font-weight: 700;`, (/ONGOING/.test(totalDuration) ? 'ONGOING' : totalDuration.trim() + 'ms'));
console.info(`%c${detailPads} Cumulative Network Latency: `, `color: ${outputColors[item.type]}; font-weight: 700;`, (/ONGOING/.test(networkLatency) ? 'ONGOING' : networkLatency.trim() + 'ms'));
console.info(`%c${detailPads} Cumulative Compute Duration: `, `color: ${outputColors[item.type]}; font-weight: 700;`, (/ONGOING/.test(cumulativeCompute) ? 'ONGOING' : cumulativeCompute.trim() + 'ms'));
console.info(`%c${detailPads} Process Compute Duration: `, `color: ${outputColors[item.type]}; font-weight: 700;`, (/ONGOING/.test(computeDuration) ? 'ONGOING' : computeDuration.trim() + 'ms'));
console.info(`%c${detailPads} ================================================================`, `color: ${outputColors[item.type]}; font-weight: 700;`, "\n\n");
console.groupEnd();
} else {
exportableRecord = {
sequence : sequence,
threadName : threadName.trim(),
processName : processName.trim(),
absTime : (item?.absTime) ? (item.absTime.toFixed(5)) : '',
totalDuration : (/ONGOING/.test(totalDuration) ? 'ONGOING' : +(totalDuration.trim())),
networkLatency : (/ONGOING/.test(networkLatency) ? 'ONGOING' : +(networkLatency.trim())),
cumulativeCompute : (/ONGOING/.test(cumulativeCompute) ? 'ONGOING' : +(cumulativeCompute.trim())),
computeDuration : (/ONGOING/.test(computeDuration) ? 'ONGOING' : +(computeDuration.trim())),
startTime : (item?.dateTimeStamps[0]??''),
endTime : (item?.dateTimeStamps[1]??''),
spawnsChildren : item?.children ? item.children.length : 0
};
exportableStack.push(exportableRecord);
}
if(item.children.length > 0) recursivelyOutputLogsToConsole(item.children, depth + 1);
});
return exportableStack.length ? exportableStack : undefined;
}
let allLogs = await GLOBAL.getAllLogs(showCSV);
let processedStack = recursivelyMoveChildLogsIntoStartNode(allLogs);
processedStack.forEach(root => recursivelyComputeCumulativeNetworkLatency(root));
processedStack.forEach(root => calculateComputeDurationForNode(root));
if(!showCSV) console.info(`%c${' THREAD PROCESS NAME DURATION NETWORK TOT.COMP. COMPUTE START TIME'}`, 'color: #000; font-weight: 700;');
let exportableData = recursivelyOutputLogsToConsole(processedStack);
if(!showCSV || !exportableData) return;
let csvData = exportableData.flatMap((log, i) => {
let retVal = [Object.values(log)];
retVal[0][0] = i + 1;
retVal = ['"' + retVal[0].join('","') + '"'];
if(i === 0) retVal.unshift('"' + Object.keys(log).join('","') + '"');
return retVal;
});
try {
GLOBAL.exportCount++;
console.groupCollapsed(`CSV Data Exported as %c${GLOBAL.sessionStamp}-${(GLOBAL.exportCount)}.csv`, 'color: #F60; font-weight: 700;');
let blobdtMIME = new Blob([csvData.join('\n')], { type: "text/csv" });
let url = URL.createObjectURL(blobdtMIME)
let anchor = document.createElement("a")
anchor.id = "download"
anchor.setAttribute("download", `${GLOBAL.sessionStamp}-${(GLOBAL.exportCount)}.csv`);
anchor.href = url;
anchor.click();
console.info(`CSV Data:\n${csvData.join('\n')}\n...EOS`);
URL.revokeObjectURL(url);
GLOBAL.logStack = [];
zsmClient.callWorkerFunction("clearLogs", true);
console.groupEnd();
} catch (error) {
console.error('Error during download:', error);
console.groupEnd();
}
return exportableData;
}
}
export { UMFAClient, FIDO2Client };