@ideem/zsm-client-sdk
Version:
ZSM makes 2FA easy and invisible for everyone, all the time, using advanced cryptography like MPC to establish cryptographic proof of the origin of any transaction or login attempt, while eliminating opportunities for social engineering. ZSM has no relian
409 lines (364 loc) • 22.9 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); }
async unenroll(user) { return await this.callWorkerFunction('unenroll', 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.webauthnDelete = this.webauthnDelete.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); }
async webauthnDelete(user) { return await this.callWorkerFunction('webauthnDelete', 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){
let tzO = new Date().getTimezoneOffset() * 60000;
GLOBAL.getAllLogs = async (showCSV) => {
let res = await zsmClient.callWorkerFunction("getLogs");
let {logs, timeOrigin} = JSON.parse(res);
let timeDifferential = performance.timeOrigin - timeOrigin;
console.log('timeDifferential :', timeDifferential);
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) => {
try{
return new Date((+ISOStamp - tzO)).toISOString().slice(11,-1);
}catch(e){
console.error('Error parsing ISOStamp:', ISOStamp, e);
return ISOStamp;
}
}
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': '#509',
'WORKER': '#009',
'RELPARTYSERVER': '#076',
'CRYPTOSERVER': '#700',
'RELPARTY': '#076',
'CRYPTO': '#700',
};
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 nameElements = item.name.replace(/[\[\]]/g, '').split(/(SERVER)?-/);
let threadName = (nameElements[0] + spacePadding).slice(0, 14);
let processName = (nameElements[1] ? nameElements[1] : item.name + 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 };