Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -345,6 +345,7 @@ linkStyle default opacity:0.5
geolocation_controller --> base_controller;
geolocation_controller --> controller_utils;
geolocation_controller --> messenger;
json_rpc_engine --> messenger;
json_rpc_middleware_stream --> json_rpc_engine;
keyring_controller --> base_controller;
keyring_controller --> messenger;
Expand Down
1 change: 1 addition & 0 deletions packages/json-rpc-engine/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@
"test:watch": "NODE_OPTIONS=--experimental-vm-modules jest --watch"
},
"dependencies": {
"@metamask/messenger": "^1.1.1",
"@metamask/rpc-errors": "^7.0.2",
"@metamask/safe-event-emitter": "^3.0.0",
"@metamask/utils": "^11.9.0",
Expand Down
56 changes: 56 additions & 0 deletions packages/json-rpc-engine/src/v2/createMethodMiddleware.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import { Messenger, MOCK_ANY_NAMESPACE } from '@metamask/messenger';

import { makeRequest } from '../../tests/utils';
import {
createMethodMiddleware,
MethodHandler,
} from './createMethodMiddleware';
import { JsonRpcEngineV2 } from './JsonRpcEngineV2';

type TestAction = {
type: 'Example:TestAction';
handler: () => Promise<string>;
};

const getValueA = {
hookNames: { testHook: true },
implementation: ({ hooks }): Promise<string> => hooks.testHook(),
} satisfies MethodHandler<{ testHook: () => Promise<string> }>;

const getValueB = {
actionNames: ['Example:TestAction'],
implementation: ({ messenger }): Promise<string> =>
messenger.call('Example:TestAction'),
} satisfies MethodHandler<never, TestAction>;

const messenger = new Messenger<string, TestAction>({
namespace: MOCK_ANY_NAMESPACE,
});

messenger.registerActionHandler('Example:TestAction', async () => 'B');

const middleware = createMethodMiddleware({
handlers: { getValueA, getValueB },
hooks: { testHook: async () => 'A' },
messenger,
});

const engine = JsonRpcEngineV2.create({ middleware: [middleware] });

describe('createMethodMiddleware', () => {
it('passes in the requested hooks', async () => {
const result = await engine.handle(makeRequest({ method: 'getValueA' }));
expect(result).toBe('A');
});

it('passes in a delegated messenger', async () => {
const result = await engine.handle(makeRequest({ method: 'getValueB' }));
expect(result).toBe('B');
});

it('skips unrecognized methods', async () => {
await expect(
engine.handle(makeRequest({ method: 'getValueC' })),
).rejects.toThrow('Nothing ended request');
});
});
143 changes: 143 additions & 0 deletions packages/json-rpc-engine/src/v2/createMethodMiddleware.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
import { ActionConstraint, Messenger } from '@metamask/messenger';

import { JsonRpcMiddleware, Next } from './JsonRpcEngineV2';
import { ContextConstraint } from './MiddlewareContext';
import { Json, JsonRpcParams, JsonRpcRequest } from './utils';

type HandlerActions<Handler> =
Handler extends MethodHandler<Record<string, unknown>, infer Actions> ? Actions : never;

type HandlerHooks<Handler> =
Handler extends MethodHandler<infer Hooks> ? Hooks : never;

export type MethodHandlerImplementation<
Hooks extends Record<string, unknown> = Record<string, never>,
MessengerActions extends ActionConstraint = never,
Parameters extends JsonRpcParams = JsonRpcParams,
Result extends Json = Json,
Context extends ContextConstraint = ContextConstraint,
> = (options: {
request: Readonly<JsonRpcRequest<Parameters>>;
context: Context;
next: Next<JsonRpcRequest>;
messenger: Messenger<string, MessengerActions>;
hooks: Hooks;
}) => Promise<Result> | Result;

export type MethodHandler<
Hooks extends Record<string, unknown> = Record<string, unknown>,
MessengerActions extends ActionConstraint = never,
Parameters extends JsonRpcParams = JsonRpcParams,
Result extends Json = Json,
Context extends ContextConstraint = ContextConstraint,
> = {
implementation: MethodHandlerImplementation<
Hooks,
MessengerActions,
Parameters,
Result,
Context
>;
hookNames?: { [Key in keyof Hooks]: true };
actionNames?: MessengerActions['type'][];
};

export type CreateMethodMiddlewareOptions<
Handlers extends Record<string, MethodHandler>,
> = {
handlers: Handlers;
messenger: Messenger<string, HandlerActions<Handlers[keyof Handlers]>>;
hooks: HandlerHooks<Handlers[keyof Handlers]>;
};

type ResolvedHandler<
Hooks extends Record<string, unknown> = Record<string, unknown>,
MessengerActions extends ActionConstraint = never,
Parameters extends JsonRpcParams = JsonRpcParams,
Result extends Json = Json,
Context extends ContextConstraint = ContextConstraint,
> = {
implementation: MethodHandlerImplementation<
Hooks,
MessengerActions,
Parameters,
Result,
Context
>;
hooks: Record<string, unknown>;
messenger: Messenger<string, MessengerActions>;
};

export function createMethodMiddleware<
Handlers extends Record<string, MethodHandler>,
Context extends ContextConstraint,
>(
options: CreateMethodMiddlewareOptions<Handlers>,
): JsonRpcMiddleware<JsonRpcRequest, Json, Context> {
const { messenger: rootMessenger, hooks: allHooks } = options;

const handlers = Object.entries(options.handlers).reduce<
Record<string, ResolvedHandler>
>((accumulator, [handlerName, handler]) => {
const handlerHooks = selectHooks(allHooks, handler.hookNames) ?? {};
const handlerMessenger = new Messenger({
namespace: handlerName,
parent: rootMessenger,
});

rootMessenger.delegate({
actions: handler.actionNames ?? [],
messenger: handlerMessenger,
});

accumulator[handlerName] = {
implementation: handler.implementation,
hooks: handlerHooks,
messenger: handlerMessenger,
};
return accumulator;
}, {});

return ({ request, context, next }) => {
const handler = handlers[request.method];
if (handler === undefined) {
return next();
}

const { implementation, hooks, messenger } = handler;

return implementation({ request, context, next, hooks, messenger });
};
}

/**
* Returns the subset of the specified `hooks` that are included in the
* `hookNames` object. This is a Principle of Least Authority (POLA) measure
* to ensure that each RPC method implementation only has access to the
* API "hooks" it needs to do its job.
*
* @param hooks - The hooks to select from.
* @param hookNames - The names of the hooks to select.
* @returns The selected hooks.
* @template Hooks - The hooks to select from.
* @template HookName - The names of the hooks to select.
*/
export function selectHooks<
Hooks extends Record<string, unknown>,
HookName extends keyof Hooks,
>(
hooks: Hooks,
hookNames?: Record<HookName, true>,
): Pick<Hooks, HookName> | undefined {
if (hookNames) {
return Object.keys(hookNames).reduce<Partial<Pick<Hooks, HookName>>>(
(hookSubset, _hookName) => {
const hookName = _hookName as HookName;
hookSubset[hookName] = hooks[hookName];
return hookSubset;
},
{},
) as Pick<Hooks, HookName>;
}
return undefined;
}
2 changes: 2 additions & 0 deletions packages/json-rpc-engine/src/v2/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,12 @@ describe('@metamask/json-rpc-engine/v2', () => {
"JsonRpcServer",
"MiddlewareContext",
"asLegacyMiddleware",
"createMethodMiddleware",
"createScaffoldMiddleware",
"getUniqueId",
"isNotification",
"isRequest",
"selectHooks",
]
`);
});
Expand Down
1 change: 1 addition & 0 deletions packages/json-rpc-engine/src/v2/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
export { asLegacyMiddleware } from './asLegacyMiddleware';
export { getUniqueId } from '../getUniqueId';
export { selectHooks, createMethodMiddleware } from './createMethodMiddleware';
export { createScaffoldMiddleware } from './createScaffoldMiddleware';
export { JsonRpcEngineV2 } from './JsonRpcEngineV2';
export type {
Expand Down
5 changes: 5 additions & 0 deletions packages/json-rpc-engine/tsconfig.build.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,10 @@
"outDir": "./dist",
"rootDir": "./src"
},
"references": [
{
"path": "../messenger/tsconfig.build.json"
}
],
"include": ["../../types", "./src"]
}
5 changes: 5 additions & 0 deletions packages/json-rpc-engine/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,5 +9,10 @@
"noUncheckedIndexedAccess": true,
"target": "es2020"
},
"references": [
{
"path": "../messenger/tsconfig.json"
}
],
"include": ["../../types", "../../tests", "./src", "./tests"]
}
1 change: 1 addition & 0 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -4106,6 +4106,7 @@ __metadata:
"@lavamoat/allow-scripts": "npm:^3.0.4"
"@lavamoat/preinstall-always-fail": "npm:^2.1.0"
"@metamask/auto-changelog": "npm:^6.1.0"
"@metamask/messenger": "npm:^1.1.1"
"@metamask/rpc-errors": "npm:^7.0.2"
"@metamask/safe-event-emitter": "npm:^3.0.0"
"@metamask/utils": "npm:^11.9.0"
Expand Down
Loading