All files / kernel-rpc-methods/src RpcService.ts

100% Statements 17/17
100% Branches 2/2
100% Functions 8/8
100% Lines 17/17

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157                                                                                                      216x 216x                       527x 4x                                 527x 527x   527x 527x                       527x                         527x                         526x 526x   1x                                       525x   525x 525x 525x          
import { rpcErrors } from '@metamask/rpc-errors';
import type { Struct } from '@metamask/superstruct';
import { hasProperty } from '@metamask/utils';
import type { Json, JsonRpcParams } from '@metamask/utils';
 
import type { Handler } from './types.ts';
 
type ExtractHooks<Handlers> =
  // We only use this type to extract the hooks from the handlers,
  // so we can safely use `any` for the generic constraints.
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
  Handlers extends Handler<string, any, any, infer Hooks> ? Hooks : never;
 
type ExtractMethods<Handlers> =
  // We only use this type to extract the hooks from the handlers,
  // so we can safely use `any` for the generic constraints.
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
  Handlers extends Handler<infer Methods, any, any, any> ? Methods : never;
 
type HandlerRecord<
  Handlers extends Handler<
    string,
    JsonRpcParams,
    Json | void,
    Record<string, unknown>
  >,
> = Record<Handlers['method'], Handlers>;
 
/**
 * A registry for RPC method handlers that provides type-safe registration and execution.
 */
export class RpcService<
  // The class picks up its type from the `handlers` argument,
  // so using `any` in this constraint is safe.
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
  Handlers extends HandlerRecord<Handler<string, any, any, any>>,
> {
  readonly #handlers: Handlers;
 
  readonly #hooks: ExtractHooks<Handlers[keyof Handlers]>;
 
  /**
   * Create a new HandlerRegistry with the specified method handlers.
   *
   * @param handlers - A record mapping method names to their handler implementations.
   * @param hooks - The hooks to pass to the method implementation.
   */
  constructor(
    handlers: Handlers,
    hooks: ExtractHooks<Handlers[keyof Handlers]>,
  ) {
    this.#handlers = handlers;
    this.#hooks = hooks;
  }
 
  /**
   * Assert that a method is registered in this registry.
   *
   * @param method - The method name to check.
   * @throws If the method is not registered.
   */
  assertHasMethod(
    method: string,
  ): asserts method is ExtractMethods<Handlers[keyof Handlers]> {
    if (!this.#hasMethod(method as ExtractMethods<Handlers[keyof Handlers]>)) {
      throw rpcErrors.methodNotFound();
    }
  }
 
  /**
   * Execute a method with the provided parameters. Only the hooks specified in the
   * handler's `hooks` array will be passed to the implementation.
   *
   * @param method - The method name to execute.
   * @param params - The parameters to pass to the method implementation.
   * @returns The result of the method execution.
   * @throws If the parameters are invalid.
   */
  async execute<Method extends ExtractMethods<Handlers[keyof Handlers]>>(
    method: Method,
    params: unknown,
  ): Promise<ReturnType<Handlers[Method]['implementation']>> {
    const handler = this.#getHandler(method);
    assertParams(params, handler.params);
 
    const expectedHooks = selectHooks(this.#hooks, handler.hooks);
    return await handler.implementation(expectedHooks, params);
  }
 
  /**
   * Check if a method is registered in this registry.
   *
   * @param method - The method name to check.
   * @returns Whether the method is registered.
   */
  #hasMethod<Method extends ExtractMethods<Handlers[keyof Handlers]>>(
    method: Method,
  ): boolean {
    return hasProperty(this.#handlers, method);
  }
 
  /**
   * Get a handler for a specific method.
   *
   * @param method - The method name to get the handler for.
   * @returns The handler for the specified method.
   * @throws If the method is not registered.
   */
  #getHandler<Method extends ExtractMethods<Handlers[keyof Handlers]>>(
    method: Method,
  ): Handlers[Method] {
    return this.#handlers[method];
  }
}
 
/**
 * @param params - The parameters to assert.
 * @param struct - The struct to assert the parameters against.
 * @throws If the parameters are invalid.
 */
function assertParams<Params extends JsonRpcParams>(
  params: unknown,
  struct: Struct<Params>,
): asserts params is Params {
  try {
    struct.assert(params);
  } catch (error) {
    throw new Error(`Invalid params: ${(error as Error).message}`);
  }
}
 
/**
 * Returns the subset of the specified `hooks` that are included in the
 * `hookNames` array. 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.
 */
function selectHooks<
  Hooks extends Record<string, unknown>,
  HookName extends keyof Hooks,
>(hooks: Hooks, hookNames: { [Key in HookName]: true }): Pick<Hooks, HookName> {
  return Object.keys(hookNames).reduce<Partial<Pick<Hooks, HookName>>>(
    (hookSubset, hookName) => {
      const key = hookName as HookName;
      hookSubset[key] = hooks[key];
      return hookSubset;
    },
    {},
  ) as Pick<Hooks, HookName>;
}