UNPKG

@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

112 lines (94 loc) 6.88 kB
class PluginManager { #sdkClassMap = new Map(); // Map of SDK class names to their implementations #classCorpus = new Map(); // Map of plugin identities to their class maps #status = 'INITIALIZED'; // Indicates the current status of the plugin manager (INITIALIZED, SEEDED, FROZEN) /** * Returns whether the plugin manager is frozen. * @returns {boolean} True if the plugin manager is frozen, false otherwise. */ get isFrozen() { return this.#status === 'FROZEN'; } /** * @name classes * @description Retrieves the SDK class associated with the given class description. * @param {string} classDesc The name of the class to retrieve. * @returns {Function} The SDK class associated with the given name. * @throws {Error} If the classDesc is not provided * @throws {Error} If no class is registered for the given name. */ classes (classDesc) { if(this.#status === 'INITIALIZED') throw new Error('[zsmPluginManager] :: classes :: collection accessor cannot be referenced before SOME classes are registered!'); if(!classDesc || typeof classDesc !== 'string') throw new Error(`[zsmPluginManager] :: classes :: collection accessor requires a valid class description (received ${typeof classDesc}: ${classDesc})!`); classDesc = classDesc.toUpperCase(); if(!this.isFrozen){ console.warn('[zsmPluginManager] :: Freezing the plugin manager as the first class has been preemptively requested. No further plugins can be registered after this point.\nIf you feel you received this message unexpectedly, please ensure all plugins are imported before initializing (`new UMFAClient()` or `new FIDO2Client()`) a client.'); this.freezePlugins(); } if(!this.#sdkClassMap.has(classDesc)) throw new Error(`[zsmPluginManager] :: classes :: No SDK class registered for: ${classDesc}`); return this.#sdkClassMap.get(classDesc); } /** * @name seedClassMap * @description Seeds the class map with initial values. * @param {Object<string, Object>} registeredObj The key-value pairs to seed the class map with. * @returns {Map<string, Function>} A Map containing the seeded class definitions */ seedClassMap(registeredObj) { const constructedMap = new Map(); for (const [classDesc, ctor] of Object.entries(registeredObj.classDefs || {})) { if(ctor) constructedMap.set(classDesc.toUpperCase(), ctor); } return constructedMap; } /** * @name registerPlugin * @description Registers a plugin with the plugin manager. * @param {string} pluginIdentity The unique identity of the plugin. * @param {Object<string, Function>} classObj The class map for the plugin. * @throws {Error} Throws an error if the plugin manager is frozen. */ registerPlugin(pluginIdentity, classObj){ if (this.isFrozen) throw new Error('[zsmPluginManager] :: registerPlugin :: Cannot register plugin: PlugInManager is frozen!\nThis typically occurs when a plugin is imported after a client - UMFAClient or FIDO2Client - has already been initialized.\nMake sure to import all plugins before initializing (`new UMFAClient()` or `new FIDO2Client()`) a client.'); if (!pluginIdentity || typeof classObj !== 'object' || classObj == null) throw new Error('[zsmPluginManager] :: registerPlugin :: pluginIdentity and class map object required'); this.#status = 'SEEDED'; this.#classCorpus.set(pluginIdentity.toUpperCase(), classObj); console.info(`[zsmPluginManager] :: PluginManager :: Registered plugin "${pluginIdentity}".`); return true; } /** * @name freezePlugins * @description Freezes the plugin manager, preventing further modifications. */ freezePlugins(){ if (this.isFrozen) return; // If the plugin manager is already frozen, bail. // Below is the plug-in precedence resolution logic. As new plugins are registered, they may override core classes. // As new plugins are DEVELOPED, they may introduce new classes or modify existing ones, and their extension/override logic must be declared below. // [CORE PACKAGE] Default all of the active classes to the "core" classes (set below this declaration) const core = this.#classCorpus.get('CORE'); if (!core) throw new Error('[zsmPluginManager] :: freezePlugins :: Core not registered before freeze'); const constructedMap = this.seedClassMap(core); // returns Map // [PASSKEYS+ PLUGIN PACKAGE] If the "PASSKEYS+" plugin is registered, supercede the core classes with the passkeys classes if(this.#classCorpus.has('PASSKEYS+')) { const passkeysMap = this.#classCorpus.get('PASSKEYS+'); if (passkeysMap?.classDefs) for (const [k,v] of Object.entries(passkeysMap.classDefs)) constructedMap.set(k.toUpperCase(), v); } // Apply class decorators (wrappers) AFTER overlays, if present in any of the registrations const postConstructDecorators = [...this.#classCorpus.values()].filter(entry => entry.mode === 'DECORATOR'); if (postConstructDecorators.length) { for (const modifier of postConstructDecorators) { let { classDesc, postModFn } = modifier; if (typeof postModFn !== 'function') throw new Error('[zsmPluginManager] :: freezePlugins :: Decorator must contain a valid function (received ' + typeof postModFn + ')'); const baseCtor = constructedMap.get(classDesc.toUpperCase()); if (typeof baseCtor !== 'function') throw new Error(`[zsmPluginManager] :: freezePlugins :: Decorator targets unknown class "${classDesc}"`); const decorated = postModFn(baseCtor); if (typeof decorated !== 'function') throw new Error('[zsmPluginManager] :: freezePlugins :: Decorator must return a constructor (received ' + typeof decorated + ')'); constructedMap.set(classDesc.toUpperCase(), decorated); } } this.#sdkClassMap = constructedMap; this.#status = 'FROZEN'; } } const pluginManager = new PluginManager() export { pluginManager as zsmPluginManager };