UNPKG

minauth

Version:

A TypeScript library for building authentication systems on top of the Mina blockchain and other zero-knowledge proofs solutions.

167 lines 9.71 kB
import env from 'env-var'; import * as E from 'fp-ts/lib/Either.js'; import * as RTE from 'fp-ts/lib/ReaderTaskEither.js'; import * as R from 'fp-ts/lib/Record.js'; import * as S from 'fp-ts/lib/Semigroup.js'; import * as T from 'fp-ts/lib/Task.js'; import * as TE from 'fp-ts/lib/TaskEither.js'; import { pipe } from 'fp-ts/lib/function.js'; import * as Str from 'fp-ts/lib/string.js'; import { existsSync } from 'fs'; import fs from 'fs/promises'; import * as path from 'path'; import { z } from 'zod'; import { fpInterfaceTag, tsInterfaceTag } from '../plugin/interfacekind.js'; import { tsToFpMinAuthPluginFactory } from '../plugin/plugintype.js'; import { tryCatchIO, useLogger } from '../utils/fp/readertaskeither.js'; import { fromFailablePromise, liftZodParseResult } from '../utils/fp/taskeither.js'; import { askActivePlugins, askLogger, askPluginInstance, tapAndLogError } from './pluginruntime.js'; /** * Configuration schema for the plugin loader */ export const configurationSchema = z.object({ /** Directory where to look for plugins */ pluginDir: z.string().optional(), /** Plugins to load along with their configuration */ plugins: z.record(z.object({ /** Path to the plugin module */ path: z.string().optional(), /** Configuration for the plugin */ config: z.unknown() })) }); /** * Read the configuration from a file with custom parser */ export const _readConfiguration = (logger) => (parseConfiguration) => (cfgPath) => pipe(TE.Do, TE.bind('finalCfgPath', () => TE.fromIOEither(() => { const finalCfgPath = path.resolve(cfgPath ?? env .get('MINAUTH_CONFIG') .default('minauth-config.json') .asString()); logger.debug({ finalCfgPath }); return existsSync(finalCfgPath) ? E.right(finalCfgPath) : E.left('configuration file does not exist'); })), TE.bind('cfgFileContent', ({ finalCfgPath }) => fromFailablePromise(() => fs.readFile(finalCfgPath, 'utf-8'))), TE.chain(({ cfgFileContent }) => liftZodParseResult(parseConfiguration(JSON.parse(cfgFileContent))))); /** Read the configuration from a file * The file path can be passed from $MINAUTH_CONFIG env var and * defaults to `config.yaml` in the current working directory */ export const readConfiguration = (logger) => _readConfiguration(logger)(configurationSchema.safeParse); const importPluginModule = (pluginModulePath) => fromFailablePromise(async () => { return import(pluginModulePath); }); const validatePluginCfg = (cfg, factory) => TE.fromEither(factory.configurationDec.decode(cfg)); /** * Perform a dynamic import of the plugin module and initialize the plugin. */ const initializePlugin = (pluginModulePath, pluginCfg, logger) => pipe(TE.Do, TE.bind('pluginModule', () => importPluginModule(pluginModulePath)), TE.let('rawPluginFactory', ({ pluginModule }) => pluginModule.default), TE.bind('pluginFactory', ({ rawPluginFactory }) => rawPluginFactory.__interface_tag === fpInterfaceTag ? TE.right(rawPluginFactory) : rawPluginFactory.__interface_tag === tsInterfaceTag ? TE.right(tsToFpMinAuthPluginFactory(rawPluginFactory)) : // TODO This check should be moved to `importPluginModule` TE.left('invalid plugin module')), TE.bind('typedPluginCfg', ({ pluginFactory }) => validatePluginCfg(pluginCfg, pluginFactory)), TE.bind('pluginInstance', ({ pluginFactory, typedPluginCfg }) => pluginFactory.initialize(typedPluginCfg, logger)), TE.tapIO(() => () => logger.info('Plugin initialized')), TE.map(({ pluginFactory, pluginInstance }) => { return { __interface_tag: 'fp', // NOTE: non-properties are not getting copied using `...pluginInstance` // So we do it manually. verifyAndGetOutput: (i) => pluginInstance.verifyAndGetOutput(i), checkOutputValidity: (o) => pluginInstance.checkOutputValidity(o), customRoutes: pluginInstance.customRoutes, inputDecoder: pluginFactory.inputDecoder, outputEncDec: pluginFactory.outputEncDec }; })); /** * Given a logger and plugin configuration, initialize the plugins. * Provided plugins will be transformed to the functional style interface, * to used as such by the library. * Initializes `PluginRuntimeEnv` */ export const initializePlugins = ( // the root logger of the hierarchy rootLogger) => (cfg) => { const resolvePluginModulePath = (name, optionalPath) => () => { const dir = cfg.pluginDir === undefined ? process.cwd() : path.resolve(cfg.pluginDir); return optionalPath === undefined ? path.join(dir, name, 'plugin.js') : path.resolve(optionalPath); }; const resolveModulePathAndInitializePlugin = (initLogger, pluginsLogger) => (pluginName, pluginCfg) => pipe(TE.Do, TE.bind('modulePath', () => TE.fromIO(resolvePluginModulePath(pluginName, pluginCfg.path))), TE.let( // The configuration object passed to the `initialize` function of the plugin factory 'pluginConfig', () => pluginCfg.config ?? {}), TE.tapIO(({ modulePath, pluginConfig }) => () => { initLogger.info(`loading plugin ${pluginName}`); initLogger.debug({ pluginName, modulePath, pluginConfig }); }), TE.bind('pluginLogger', () => TE.fromIO(() => pluginsLogger.getSubLogger({ name: `${pluginName}` }))), TE.chain(({ modulePath, pluginConfig, pluginLogger }) => initializePlugin(modulePath, pluginConfig, pluginLogger)), TE.mapLeft( // TODO: more structural error? (err) => `error while initializing plugin ${pluginName}: ${err}`), TE.tapError((err) => TE.fromIO(() => initLogger.error(`unable to initialize plugin ${pluginName}`, err)))); const Applicative = TE.getApplicativeTaskValidation(T.ApplySeq, pipe(Str.Semigroup, S.intercalate(', '))); const mkLogger = (name) => TE.fromIO(() => rootLogger.getSubLogger({ name })); return pipe(TE.Do, // A dedicated logger for the plugin initilization process TE.bind('initLogger', () => mkLogger('pluginLoader')), // Plugin runtime "manages" plugins and knows nothing about // what happens in each plugin, used to log events happen inside the // PluginRuntime monad. TE.bind('pluginsLogger', () => mkLogger('activePlugins')), // Plugins should use the provided logger to log the events // that happen inside the plugin, like the detail of the verification process. TE.bind('runtimeLogger', () => mkLogger('pluginRuntime')), // Initialize each plugin TE.bind('plugins', ({ initLogger, pluginsLogger }) => R.traverseWithIndex(Applicative)(resolveModulePathAndInitializePlugin(initLogger, pluginsLogger))(cfg.plugins)), // lift results TE.map(({ plugins, runtimeLogger }) => { return { logger: runtimeLogger, plugins }; })); }; /** * Install custom routes for a plugin into the express app. * The routes are installed under `/plugins/${pluginName}`. * and come from `pluginInstance.customRoutes`. * The routes are meant for plugin / prover communication. */ const installPluginsCustomRoutes = (app) => (pluginName, pluginInstance) => pipe(askLogger(), RTE.chain((logger) => tryCatchIO(() => app.use(`/plugins/${pluginName}`, (req, _, next) => { logger.debug('handling plugin custom route', { pluginName, method: req.method, path: req.path }); next(); }, pluginInstance.customRoutes), (reason) => `unable to install custom route: ${reason}`)), RTE.asUnit); /** * Install custom routes for all the active plugins. * The routes are installed under `/plugins/${pluginName}`. * and come from `pluginInstance.customRoutes`. * The routes are meant for plugin / prover communication. */ export const installCustomRoutes = (app) => pipe(askActivePlugins(), RTE.chain(R.traverseWithIndex(RTE.ApplicativeSeq)(installPluginsCustomRoutes(app))), RTE.asUnit, tapAndLogError('failed to install custom routes')); /** * Verify proof with given plugin and return its output. */ export const verifyProof = (input, pluginName) => pipe(RTE.Do, RTE.tap(() => useLogger((logger) => { logger.info(`verifying input with ${pluginName}. Input: ${JSON.stringify(input, null, 2)}`); })), RTE.bind('pluginInstance', () => askPluginInstance(pluginName)), // Use the plugin to extract the output. The plugin is also responsible // for checking the legitimacy of the public inputs. RTE.bind('typedInput', ({ pluginInstance }) => RTE.fromEither(pluginInstance.inputDecoder.decode(input))), RTE.bind('output', ({ typedInput, pluginInstance }) => RTE.fromTaskEither(pluginInstance.verifyAndGetOutput(typedInput))), RTE.map(({ pluginInstance, output }) => pluginInstance.outputEncDec.encode(output)), tapAndLogError(`unable to verify proof using plugin ${pluginName}`)); /** Validate the output of a plugin within the active plugin runtime. * @param pluginName The name of on of active plugins to use for validation. * @param output The encoded plugin output. * @returns The validation result. */ export const validateOutput = (pluginName, /** The encoded plugin output */ output) => pipe(RTE.Do, RTE.tap(() => useLogger((logger) => { logger.info(`validating output using plugin ${pluginName}`); logger.debug({ pluginName, output }); })), RTE.bind('pluginInstance', () => askPluginInstance(pluginName)), RTE.bind('typedOutput', ({ pluginInstance }) => RTE.fromEither(pluginInstance.outputEncDec.decode(output))), RTE.chain(({ typedOutput, pluginInstance }) => RTE.fromTaskEither(pluginInstance.checkOutputValidity(typedOutput))), tapAndLogError(`unable to validate proof using plugin ${pluginName}`)); //# sourceMappingURL=plugin-fp-api.js.map