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
JavaScript
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