UNPKG

@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
// 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 };