Skip to content

Commit 325b967

Browse files
committed
allow dynamic plugin loading; no more exceptions
fix npm run scripts fix plugin loading fix custom route installation fix inconsistent log messages
1 parent a47a0da commit 325b967

File tree

14 files changed

+970
-440
lines changed

14 files changed

+970
-440
lines changed

package-lock.json

Lines changed: 31 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@
3535
"passport": "^0.6.0",
3636
"passport-jwt": "^4.0.1",
3737
"passport-strategy": "^1.0.0",
38+
"path": "^0.12.7",
3839
"yaml": "^2.3.2",
3940
"zod": "^3.22.2"
4041
},

src/library/plugin/pluginLoader.ts

Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
import * as path from 'path';
2+
import { IMinAuthPlugin, IMinAuthPluginFactory } from './pluginType';
3+
import * as R from 'fp-ts/Record';
4+
import { pipe } from 'fp-ts/function';
5+
import { TaskEither } from 'fp-ts/TaskEither';
6+
import * as T from 'fp-ts/Task';
7+
import * as TE from 'fp-ts/TaskEither';
8+
import * as E from 'fp-ts/Either';
9+
import * as Str from 'fp-ts/string';
10+
import * as S from 'fp-ts/Semigroup';
11+
import { z } from 'zod';
12+
import env from 'env-var';
13+
import { existsSync } from 'fs';
14+
import fs from 'fs/promises';
15+
import {
16+
fromFailablePromise,
17+
liftZodParseResult
18+
} from '../../utils/TaskEither';
19+
20+
export const configurationSchema = z.object({
21+
pluginDir: z.string().optional(),
22+
plugins: z.record(
23+
z.object({
24+
path: z.string().optional(),
25+
config: z.unknown()
26+
})
27+
)
28+
});
29+
30+
export type Configuration = z.infer<typeof configurationSchema>;
31+
32+
export const _readConfiguration =
33+
<T>(parseConfiguration: (s: string) => z.SafeParseReturnType<T, T>) =>
34+
(cfgPath?: string): TaskEither<string, T> =>
35+
pipe(
36+
TE.Do,
37+
TE.bind('finalCfgPath', () =>
38+
TE.fromIOEither(() => {
39+
const finalCfgPath = path.resolve(
40+
cfgPath ??
41+
env.get('MINAUTH_CONFIG').default('config.yaml').asString()
42+
);
43+
44+
console.log(`reading configuration from ${finalCfgPath}`);
45+
46+
return existsSync(finalCfgPath)
47+
? E.right(finalCfgPath)
48+
: E.left('configuration file does not exists');
49+
})
50+
),
51+
TE.bind('cfgFileContent', ({ finalCfgPath }) =>
52+
fromFailablePromise<string>(() => fs.readFile(finalCfgPath, 'utf-8'))
53+
),
54+
TE.chain(({ cfgFileContent }) =>
55+
liftZodParseResult(parseConfiguration(cfgFileContent))
56+
)
57+
);
58+
59+
export const readConfiguration = _readConfiguration(
60+
configurationSchema.safeParse
61+
);
62+
63+
//
64+
65+
export type UntypedPlugin = IMinAuthPlugin<unknown, unknown>;
66+
67+
export type UntypedPluginFactory = IMinAuthPluginFactory<
68+
UntypedPlugin,
69+
unknown,
70+
unknown,
71+
unknown
72+
>;
73+
74+
export type UntypedPluginModule = { default: UntypedPluginFactory };
75+
76+
const importPluginModule = (
77+
pluginModulePath: string
78+
): TaskEither<string, UntypedPluginModule> =>
79+
fromFailablePromise(() => import(pluginModulePath));
80+
81+
const validatePluginCfg = (
82+
cfg: unknown,
83+
factory: UntypedPluginFactory
84+
): TaskEither<string, unknown> =>
85+
liftZodParseResult(factory.configurationSchema.safeParse(cfg));
86+
87+
const initializePlugin = (
88+
pluginModulePath: string,
89+
pluginCfg: unknown
90+
): TaskEither<string, UntypedPlugin> =>
91+
pipe(
92+
TE.Do,
93+
TE.bind('pluginModule', () => importPluginModule(pluginModulePath)),
94+
TE.let('pluginFactory', ({ pluginModule }) => pluginModule.default),
95+
TE.bind('typedPluginCfg', ({ pluginFactory }) =>
96+
validatePluginCfg(pluginCfg, pluginFactory)
97+
),
98+
TE.chain(({ pluginFactory, typedPluginCfg }) =>
99+
pluginFactory.initialize(typedPluginCfg)
100+
)
101+
);
102+
103+
export const initializePlugins = (
104+
cfg: Configuration
105+
): TaskEither<string, Record<string, UntypedPlugin>> => {
106+
const resolvePluginModulePath =
107+
(name: string, optionalPath?: string) => () => {
108+
const dir =
109+
cfg.pluginDir === undefined
110+
? process.cwd()
111+
: path.resolve(cfg.pluginDir);
112+
113+
return optionalPath === undefined
114+
? path.join(dir, name)
115+
: path.resolve(optionalPath);
116+
};
117+
118+
const resolveModulePathAndInitializePlugin = (
119+
pluginName: string,
120+
pluginCfg: {
121+
path?: string | undefined;
122+
config?: unknown;
123+
}
124+
): TaskEither<string, UntypedPlugin> =>
125+
pipe(
126+
TE.fromIO(resolvePluginModulePath(pluginName, pluginCfg.path)),
127+
TE.chain((modulePath: string) =>
128+
initializePlugin(modulePath, pluginCfg.config ?? {})
129+
)
130+
);
131+
132+
const Applicative = TE.getApplicativeTaskValidation(
133+
T.ApplyPar,
134+
pipe(Str.Semigroup, S.intercalate(', '))
135+
);
136+
137+
return R.traverseWithIndex(Applicative)(resolveModulePathAndInitializePlugin)(
138+
cfg.plugins
139+
);
140+
};

src/library/plugin/pluginType.ts

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { RequestHandler } from 'express';
2+
import { TaskEither } from 'fp-ts/lib/TaskEither';
23
import { JsonProof } from 'o1js';
34
import z from 'zod';
45

@@ -10,7 +11,7 @@ export interface IMinAuthPlugin<PublicInputArgs, Output> {
1011
verifyAndGetOutput(
1112
publicInputArgs: PublicInputArgs,
1213
serializedProof: JsonProof
13-
): Promise<Output>;
14+
): TaskEither<string, Output>;
1415

1516
// The schema of the arguments for fetching public inputs.
1617
readonly publicInputArgsSchema: z.ZodType<PublicInputArgs>;
@@ -38,7 +39,7 @@ export interface IMinAuthPluginFactory<
3839
> {
3940
// Initialize the plugin given the configuration. The underlying zk program is
4041
// typically compiled here.
41-
initialize(cfg: Configuration): Promise<T>;
42+
initialize(cfg: Configuration): TaskEither<string, T>;
4243

4344
readonly configurationSchema: z.ZodType<Configuration>;
4445
}
@@ -49,9 +50,9 @@ export interface IMinAuthProver<PublicInputArgs, PublicInput, PrivateInput> {
4950
prove(
5051
publicInput: PublicInput,
5152
secretInput: PrivateInput
52-
): Promise<JsonProof>;
53+
): TaskEither<string, JsonProof>;
5354

54-
fetchPublicInputs(args: PublicInputArgs): Promise<PublicInput>;
55+
fetchPublicInputs(args: PublicInputArgs): TaskEither<string, PublicInput>;
5556
}
5657

5758
export interface IMinAuthProverFactory<
@@ -61,5 +62,5 @@ export interface IMinAuthProverFactory<
6162
PublicInput,
6263
PrivateInput
6364
> {
64-
initialize(cfg: Configuration): Promise<T>;
65+
initialize(cfg: Configuration): TaskEither<string, T>;
6566
}

src/library/plugin/utils.ts

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
import { TaskEither } from 'fp-ts/lib/TaskEither';
2+
import { UntypedPlugin } from './pluginLoader';
3+
import * as expressCore from 'express-serve-static-core';
4+
import {
5+
fromFailableIO,
6+
fromFailablePromise,
7+
liftZodParseResult
8+
} from '../../utils/TaskEither';
9+
import * as R from 'fp-ts/Record';
10+
import * as TE from 'fp-ts/TaskEither';
11+
import { RequestHandler } from 'express';
12+
import { pipe } from 'fp-ts/lib/function';
13+
import { JsonProof, verify } from 'o1js';
14+
15+
type ActivePlugins = Record<string, UntypedPlugin>;
16+
17+
export const installCustomRoutes =
18+
(activePlugins: ActivePlugins) =>
19+
(app: expressCore.Express): TaskEither<string, void> =>
20+
pipe(
21+
R.traverseWithIndex(TE.ApplicativeSeq)(
22+
(pluginName, pluginInstance: UntypedPlugin) =>
23+
R.traverseWithIndex(TE.ApplicativeSeq)(
24+
(path, handler: RequestHandler) =>
25+
fromFailableIO(
26+
() => app.use(`/plugins/${pluginName}/${path}`, handler),
27+
`failed to install custom route ${path} for plugin ${pluginName}`
28+
)
29+
)(pluginInstance.customRoutes)
30+
)(activePlugins),
31+
TE.map(() => {})
32+
);
33+
34+
export const verifyProof =
35+
(activePlugins: ActivePlugins) =>
36+
(
37+
proof: JsonProof,
38+
publicInputArgs: unknown,
39+
pluginName: string
40+
): TaskEither<string, unknown> =>
41+
pipe(
42+
TE.Do,
43+
TE.tap(() =>
44+
TE.fromIO(() =>
45+
console.info(`verifying proof using plugin ${pluginName}`)
46+
)
47+
),
48+
TE.bind('pluginInstance', () =>
49+
TE.fromOption(() => `plugin ${pluginName} not found`)(
50+
R.lookup(pluginName)(activePlugins)
51+
)
52+
),
53+
// Step 1: check that the proof was generated using a certain verification key.
54+
TE.tap(({ pluginInstance }) =>
55+
pipe(
56+
fromFailablePromise(
57+
() => verify(proof, pluginInstance.verificationKey),
58+
'unable to verify proof'
59+
),
60+
TE.tap((valid) =>
61+
valid ? TE.right(undefined) : TE.left('invalid proof')
62+
)
63+
)
64+
),
65+
// Step 2: use the plugin to extract the output. The plugin is also responsible
66+
// for checking the legitimacy of the public inputs.
67+
TE.bind('typedPublicInputArgs', ({ pluginInstance }) =>
68+
liftZodParseResult(
69+
pluginInstance.publicInputArgsSchema.safeParse(publicInputArgs)
70+
)
71+
),
72+
TE.chain(({ typedPublicInputArgs, pluginInstance }) =>
73+
pluginInstance.verifyAndGetOutput(typedPublicInputArgs, proof)
74+
)
75+
);

0 commit comments

Comments
 (0)