UNPKG

@alius/rpc

Version:

JSON-RPC 2.0 implementation configured with OpenRPC

1,494 lines (1,418 loc) 46.1 kB
"use strict"; import Ajv from "ajv"; import addFormats from "ajv-formats"; import RefParser from "@apidevtools/json-schema-ref-parser"; import { Exception } from "@alius/exception"; import { ObjectUtils } from "@alius/utils"; import openrpcSchema from "./openrpc.schema.json" assert { type: "json" }; import jsonrpcRequestSchema from "./jsonrpc.request.schema.json" assert { type: "json" }; import emptyConfig from "./empty.openrpc.json" assert { type: "json" }; import rpcDiscoverDef from "./rpc.discover.json" assert { type: "json" }; /** * JSON-RPC 2.0 provider. * OpenRPC is used to provide API configuration. */ class RPC { /** @type {Function} */ #initFunction; /** @type {Ajv} */ #ajv; /** @type {OpenRPC} */ #config; /** @type {Object<string, Function>} */ #handlers; /** @type {RPCOptionsType} */ #options; /** @type {JSONRPCRequest} */ #json; /** * Creates new RPC provider instance with specified config and handlers. * <b>{@link RPC#init await init()} has to be executed to finish asynchronous initalization * of this object.</b> * <p>Every method specified in config must have corresponding handler.</p> * * @param {OpenRPC} [config="./empty.openrpc.json"] - API config * @param {Object<string, Function>} [handlers={}] - handlers for methods * @param {RPCOptionsType} [options={}] - additional options */ constructor(config, handlers = {}, options = {}) { this.#ajv = new Ajv({ strict: false, allowUnionTypes: true, }); addFormats(this.#ajv); this.#ajv.addSchema(jsonrpcRequestSchema); this.#ajv.addSchema(openrpcSchema); if (!config) { config = emptyConfig; handlers = {}; } if (!handlers) { handlers = {}; } this.#json = null; // asynchronous part of initialization will be executed in init() this.#initFunction = async () => { await this.configure(config, handlers, options); }; } /** * true when object initialization is complete. * * @type {boolean} * @readonly */ get initialized() { return !this.#initFunction; } /** * JSON validator. * * @type {Ajv} * @readonly */ get ajv() { return this.#ajv; } /** * JSON request object being processed. * <strong>Accessing this property must be done in current task of JS event loop.</strong> * * @type {JSONRPCRequest} * @readonly */ get json() { return this.#json; } /** * Initialized asynchronous part of this object. */ async init() { if (!this.initialized) { await this.#initFunction(); this.#initFunction = null; } return this; } /** * Configures this RPC provider with new configuration. * * @param {OpenRPC} config - API config * @param {Object<string, Function>} [handlers={}] - handlers for methods * @param {RPCOptionsType} [options={}] - additional options */ async configure(config, handlers = {}, options = {}) { RPC.assert(config && typeof config === "object", "config must be object"); // methods array is provided before dereferencing if (Array.isArray(config.methods)) { // Configuration for 'rpc.discover' method has result schema same as whole configuration. // Trying to validate it results in circular schema. // Test here to avoid validation. RPC.assert( !config.methods.find( (method) => // @ts-ignore method && method.name === RPC.RPC_DISCOVER ), `${RPC.RPC_DISCOVER} method is reserved for service discovery` ); } // deep-clone config to avoid changes from outside config = ObjectUtils.deepClone(config); // @ts-ignore config = await RefParser.dereference(config); RPC.assert( this.ajv.validate(openrpcSchema.$id, config), "config must be valid OpenRPC object" ); RPC.assert( // @ts-ignore !config.methods.find((method) => method.name === RPC.RPC_DISCOVER), `${RPC.RPC_DISCOVER} method is reserved for service discovery` ); if (!handlers) { handlers = {}; } RPC.assert(typeof handlers === "object", "handlers must be object"); // deep-clone handlers to avoid changes from outside handlers = ObjectUtils.deepClone(handlers); for (const method of config.methods) { RPC.assert( // @ts-ignore typeof handlers[method.name] === "function", // @ts-ignore `handler for method ${method.name} must be function` ); } RPC.assert( options && typeof options === "object", "options must be object" ); // deep-clone options to avoid changes from outside options = ObjectUtils.deepClone(options); if (typeof options.preProcessors !== "undefined") { this.#assertPreProcessors(options.preProcessors); } else { options.preProcessors = []; } if (typeof options.postProcessors !== "undefined") { this.#assertPostProcessors(options.postProcessors); } else { options.postProcessors = []; } // Save configuration copy this.#config = config; this.#handlers = handlers; this.#options = options; this.#buildValidators(); } // adds validators for all methods' parameters and results #buildValidators() { // remove any schemas left from previous configuration this.ajv.removeSchema(new RegExp("^param/")); this.ajv.removeSchema(new RegExp("^result/")); // adding schemas for methods' parameters and results for (const method of this.#config.methods) { // @ts-ignore for (const param of method.params) { // @ts-ignore this.ajv.addSchema(param.schema, `param/${method.name}/${param.name}`); } // @ts-ignore this.ajv.addSchema(method.result.schema, `result/${method.name}`); } } /** * Handler for 'rpc.discover' method. * * @returns {OpenRPC} current API configuration */ serviceDiscover() { RPC.assert(this.initialized, `${this.constructor.name} is not initialized`); return ObjectUtils.deepClone(this.#config); } /** * Adds new method to API. * If method with such name already exists - replaces it with provided config and handler. * * @param {OpenRPCMethod} config - method configuration * @param {function} handler - method handler */ async putMethod(config, handler) { RPC.assert(this.initialized, `${this.constructor.name} is not initialized`); RPC.assert(config && typeof config === "object", "config must be object"); // method name is checked before dereferencing if (config.name) { // Configuration for 'rpc.discover' method has result schema same as whole configuration. // Trying to validate it results in circular schema. // Test here to avoid validation RPC.assert( config.name !== RPC.RPC_DISCOVER, `method '${RPC.RPC_DISCOVER}' can not be replaced` ); } config = ObjectUtils.deepClone(config); // @ts-ignore config = await RefParser.dereference(config); RPC.assert( this.ajv.validate( `${openrpcSchema.$id}#/definitions/methodObject`, config ), "config must be valid OpenRPC methodObject" ); RPC.assert(typeof handler === "function", "handler must be function"); RPC.assert( config.name !== RPC.RPC_DISCOVER, `method '${RPC.RPC_DISCOVER}' can not be replaced` ); this.removeMethod(config.name); this.#config.methods.push(config); this.#handlers[config.name] = handler; this.#buildValidators(); } /** * Removes named method from API. * If there is no method with provided name - silently returns. * * @param {string} name - method name to remove */ removeMethod(name) { RPC.assert(this.initialized, `${this.constructor.name} is not initialized`); RPC.assert( name && typeof name === "string", "name must be non-empty string" ); RPC.assert( name !== RPC.RPC_DISCOVER, `method '${RPC.RPC_DISCOVER}' can not be removed` ); const index = this.#config.methods.findIndex( // @ts-ignore (method) => method.name === name ); if (index >= 0) { this.#config.methods.splice(index, 1); delete this.#handlers[name]; this.#buildValidators(); } } /** * Returns method config. * Returns <code>undefined</code> if method with such name is not found. * * @param {string} name - method name * @returns {OpenRPCMethod|undefined} method config */ getMethodConfig(name) { RPC.assert(this.initialized, `${this.constructor.name} is not initialized`); // @ts-ignore return this.#config.methods.find((method) => method.name === name); } /** * Executes single JSON-RPC request or batch of requests. * Execution order:<ul> * <li>parses request if string passed</li> * <li>if single request is passed - executes it with {@link RPC#executeSingle executeSingle} and returns response</li> * <li>if empty batch is passed - returns invalid request error (-32600)</li> * <li>executes all requests with {@link RPC#executeSingle executeSingle()} and collects responses</li> * <li>if there are no responses (batch contains notifications only) - returns</li> * <li>returns responses</li> * </ul> * * @param {(Array<JSONRPCRequest>|JSONRPCRequest|string)} json - JSON-RPC request as object or encoded in JSON string * @param {object} [options] - processing options * @param {Array<PreProcessorFunction>} [options.preProcessors] - list of additional pre-processor functions * will be executed before configured pre-processors * @param {boolean} [options.skipPreProcessors=false] - skips configured pre-processors if true * @param {Array<PostProcessorFunction>} [options.postProcessors] - list of additional post-processor functions * will be executed after configured post-processors * @param {boolean} [options.skipPostProcessors=false] - skips configured post-processors if true * @returns {Promise<JSONRPCResult|JSONRPCError|Array<JSONRPCResult|JSONRPCError>|undefined>} response object or undefined (for notifications) */ async execute(json, options = {}) { RPC.assert(this.initialized, `${this.constructor.name} is not initialized`); // if string is passed - parse JSON if (typeof json === "string") { try { json = JSON.parse(json); } catch (err) { // Failure to parse JSON always results in parse error response const e = RPC.ERROR_32700; e[RPC.ERROR_DATA] = Exception.errSerializer(err); return RPC.errorResponse(e, null); } } if (!Array.isArray(json)) { return this.executeSingle(json, options); } if (json.length <= 0) { // Empty batch results in invalid request error (-32600) return RPC.errorResponse(RPC.ERROR_32600, null); } const requests = []; /** @type {Array<JSONRPCResult|JSONRPCError>} */ const responses = []; for (const request of json) { requests.push( (async () => { const response = await this.executeSingle(request, options); if (typeof response !== "undefined") { responses.push(response); } })() ); } await Promise.all(requests); if (responses.length > 0) { return responses; } return; } /** * Executes single JSON-RPC request. * Execution order:<ul> * <li>parses request if string passed</li> * <li>validates request</li> * <li>tests if API has handler for requested method</li> * <li>executes additional pre-processors</li> * <li>executes configured pre-processors</li> * <li>matches params by name if passed as object - excesive parameters are discarded</li> * <li>validates params - excesive parameters are discarded</li> * <li>tests if values are provided for all params marked required</li> * <li>executes handler with provided parameters</li> * <li>validates response (on validation failure - changes response to error (-32030) * with original response saved in 'data' property)</li> * <li>executes configured post-processors</li> * <li>executes additional post-processors</li> * <li>on successful execution<ul> * <li>returns if request was notification</li> * <li>tries to stringify result to JSON to make sure that valid JSON can be produced * (on failure to stringify - returns stringify error (-32000) with stringify error * saved in 'data' property)</li> * <li>returns result response</li> * </ul></li> * <li>on execution failure<ul> * <li>if error code id thrown - tries to find error object among defined errors by code; * returns error object if found</li> * <li>if error object thrown - tries to find error object among defined errors by err.code; * if found - replaces 'message' and 'data' default property values with provided (if provided) * and returns resulting error object</li> * <li>if predefined error can not be found - returns method execution error (-32020) object * with original error serialized in 'data' property.</li> * </ul></li> * </ul> * <p>If parameter <code>json</code> value is object (not string) - it can be modified * by pre/post-processors. General rule - do not reuse request objects. * If you decide to reuse request object - take necessary precautions to prevent unpredictable results.</p> * * @param {(JSONRPCRequest|string)} json - JSON-RPC request as object or encoded in JSON string * @param {object} [options] - processing options * @param {Array<PreProcessorFunction>} [options.preProcessors] - list of additional pre-processor functions * will be executed before configured pre-processors * @param {boolean} [options.skipPreProcessors=false] - skips configured pre-processors if true * @param {Array<PostProcessorFunction>} [options.postProcessors] - list of additional post-processor functions * will be executed after configured post-processors * @param {boolean} [options.skipPostProcessors=false] - skips configured post-processors if true * @returns {Promise<JSONRPCResult|JSONRPCError|undefined>} response object or undefined (for notifications) */ async executeSingle(json, options = {}) { RPC.assert(this.initialized, `${this.constructor.name} is not initialized`); RPC.assert( options && typeof options === "object", "options must be object" ); if (typeof options.preProcessors !== "undefined") { this.#assertPreProcessors(options.preProcessors); } else { options.preProcessors = []; } if (typeof options.postProcessors !== "undefined") { this.#assertPostProcessors(options.postProcessors); } else { options.postProcessors = []; } // if string is passed - parse JSON if (typeof json === "string") { try { json = JSON.parse(json); } catch (err) { // failure to parse JSON always results in parse error response const e = RPC.ERROR_32700; e[RPC.ERROR_DATA] = Exception.errSerializer(err); return RPC.errorResponse(e, null); } } if (!this.ajv.validate(jsonrpcRequestSchema.$id, json)) { // json parameter (after parsing) is not valid request object // always results in invalid request response return RPC.errorResponse(RPC.ERROR_32600, null); } const id = json[RPC.ID]; const methodName = json[RPC.METHOD]; // special handler for 'rpc.discover' method if (RPC.RPC_DISCOVER === methodName) { if (typeof id !== "undefined") { // @ts-ignore return { [RPC.JSONRPC]: RPC.VERSION, [RPC.ID]: id, [RPC.RESULT]: this.serviceDiscover(), }; } return; } const method = this.#config.methods.find( // @ts-ignore (method) => method.name === methodName ); const handler = this.#handlers[methodName]; if (!method || !handler) { return RPC.errorResponse(RPC.ERROR_32601, id); } // execute explicitly passed pre-processors for (const preProcessor of options.preProcessors) { try { // @ts-ignore this.#json = json; await preProcessor(json, this); } catch (err) { return RPC.errorResponse( // @ts-ignore RPC.findError(err, RPC.ERROR_32010, method.errors), id ); } } if (!options.skipPreProcessors) { // execute preconfigured pre-processors for (const preProcessor of this.#options.preProcessors) { try { // @ts-ignore this.#json = json; await preProcessor(json, this); } catch (err) { return RPC.errorResponse( // @ts-ignore RPC.findError(err, RPC.ERROR_32010, method.errors), id ); } } } let params = json[RPC.PARAMS]; if ( params && // @ts-ignore method.paramStructure === "by-name" && !(params instanceof Object) ) { const e = RPC.ERROR_32602; e[RPC.ERROR_DATA] = "Parameters must be object (specified parameter structure \"by-name\")"; return RPC.errorResponse(e, id); } if ( params && // @ts-ignore method.paramStructure === "by-position" && !Array.isArray(params) ) { const e = RPC.ERROR_32602; e[RPC.ERROR_DATA] = "Parameters must be array (specified parameter structure \"by-position\")"; return RPC.errorResponse(e, id); } // convert object parameters-by-name to array prameters-by-index if (params && !Array.isArray(params)) { const paramsByPosition = []; // @ts-ignore for (const param of method.params) { if (param.required && params[param.name] === undefined) { const e = RPC.ERROR_32602; e[ RPC.ERROR_DATA ] = `Value for parameter '${param.name}' is not specified`; return RPC.errorResponse(e, id); } paramsByPosition.push(params[param.name]); } params = paramsByPosition; } // if parameters are not passed - use empty array if (!Array.isArray(params)) { params = []; } // testing passed paremeter types let i = 0; // @ts-ignore while (i < method.params.length && i < params.length) { // @ts-ignore const param = method.params[i]; if (param.required || typeof params[i] !== "undefined") { if ( !this.ajv.validate(`param/${methodName}/${param.name}`, params[i]) ) { const e = RPC.ERROR_32602; e[RPC.ERROR_DATA] = { message: `Parameter at index ${i} ('${param.name}') has invalid value`, errors: this.ajv.errors, }; return RPC.errorResponse(e, id); } } i++; } // remove excess paramters if (i < params.length) { params.splice(i); } // test if there are required parameters with no passed values // and add undefined values for missing non-required parameters // @ts-ignore while (i < method.params.length) { // @ts-ignore const param = method.params[i]; if (param.required) { const e = RPC.ERROR_32602; e[ RPC.ERROR_DATA ] = `Value for parameter at index ${i} ('${param.name}') is not specified`; return RPC.errorResponse(e, id); } else { params.push(undefined); } i++; } // at this point params variable contains values for all parameters // and we can execute handler try { // @ts-ignore this.#json = json; let result = await handler(...params); /** @type {JSONRPCResult|JSONRPCError} */ let resp; // Validate result // Skip validation for notifications if (typeof id !== "undefined") { // For validation try to convert result to JSON and back. // This makes sure that all objects with toJSON() method // are correctly converted before validation process. let resultForValidation = result; try { resultForValidation = JSON.parse(JSON.stringify(result)); } catch (err) { // If result can not be converted to JSON - validate original object // Maybe there are post processors which will fix stringify issue later // Anyway testing for JSON stringify will be done before returning response resultForValidation = result; } if (this.ajv.validate(`result/${methodName}`, resultForValidation)) { // @ts-ignore resp = { [RPC.JSONRPC]: RPC.VERSION, [RPC.ID]: id, [RPC.RESULT]: result, }; } else { // result is not valid const e = RPC.ERROR_32030; e[RPC.ERROR_DATA] = { result, errors: this.ajv.errors, }; return RPC.errorResponse(e, id); } } if (!options.skipPostProcessors) { // execute preconfigured post-processors for (const postProcessor of this.#options.postProcessors) { try { // @ts-ignore this.#json = json; await postProcessor(resp, json, this); } catch (err) { return RPC.errorResponse( // @ts-ignore RPC.findError(err, RPC.ERROR_32050, method.errors), id ); } } } // execute explicitly passed post-processors for (const postProcessor of options.postProcessors) { try { // @ts-ignore this.#json = json; await postProcessor(resp, json, this); } catch (err) { return RPC.errorResponse( // @ts-ignore RPC.findError(err, RPC.ERROR_32050, method.errors), id ); } } if (typeof id !== "undefined") { // Testing if response can be stringified try { JSON.stringify(resp); return resp; } catch (err) { // failure to stringify result to JSON const e = RPC.ERROR_32000; e[RPC.ERROR_DATA] = Exception.errSerializer(err); return RPC.errorResponse(e, id); } } } catch (err) { return RPC.errorResponse( // @ts-ignore RPC.findError(err, RPC.ERROR_32020, method.errors), id ); } } #assertPreProcessors(preProcessors) { RPC.assert(Array.isArray(preProcessors), "preProcessors must be array"); for (const pp of preProcessors) { RPC.assert( typeof pp === "function", "all provided pre-processors must be function" ); } } #assertPostProcessors(postProcessors) { RPC.assert(Array.isArray(postProcessors), "postProcessors must be array"); for (const pp of postProcessors) { RPC.assert( typeof pp === "function", "all provided post-processors must be function" ); } } /** * Creates JSON-RPC request object. * * @param {(null|string|number)} id - request id * @param {string} method - method name to execute * @param {(Object<string, *>|Array<*>)} [params] - list of parameter values to pass to method * @returns {JSONRPCRequest} created JSON-RPC request object */ static request(id, method, params) { RPC.assert( id === null || typeof id === "string" || typeof id === "number", "id must be either null or string or number" ); RPC.assert(typeof method === "string", "method must be string"); const req = { [RPC.JSONRPC]: RPC.VERSION, [RPC.ID]: id, [RPC.METHOD]: method, }; if (typeof params !== "undefined") { RPC.assert( params && (Array.isArray(params) || typeof params === "object"), "params must be either array or object" ); // @ts-ignore req[RPC.PARAMS] = params; } // @ts-ignore return req; } /** * Creates JSON-RPC notification object. * * @param {string} method - method name to execute * @param {(Object<string, *>|Array<*>)} [params] - list of parameter values to pass to method * @returns {JSONRPCRequest} created JSON-RPC notification object */ static notification(method, params) { RPC.assert(typeof method === "string", "method must be string"); const req = { [RPC.JSONRPC]: RPC.VERSION, [RPC.METHOD]: method, }; if (typeof params !== "undefined") { RPC.assert( params && (Array.isArray(params) || typeof params === "object"), "params must be either array or object" ); // @ts-ignore req[RPC.PARAMS] = params; } // @ts-ignore return req; } /** * Creates JSON-RPC error response object. * Passing <code>id</code> value <code>undefined</code> results in returning <code>undefined</code> value. * According to specification if <code>id</code> is undefined - server MUST NOT respond. * * @param {JSONRPCErrorObject} error - execution error to report * @param {(null|string|number)} [id] - response id * @returns {(JSONRPCError|undefined)} JSON-RPC error response object or * undefined if id parameter value is undefined */ static errorResponse(error, id) { if (typeof id === "undefined") { return; } RPC.assert( id === null || typeof id === "string" || typeof id === "number", "id must be either null or string or number" ); RPC.assert(error && typeof error === "object", "error must be object"); RPC.assert(typeof error.code === "number", "error.code must be number"); RPC.assert( typeof error.message === "string", "error.message must be string" ); // @ts-ignore return { [RPC.JSONRPC]: RPC.VERSION, [RPC.ID]: id, [RPC.ERROR]: error, }; } /** * Throws TypeError if provided test is false. * * @param {boolean} test - test value * @param {string} [message] - error message * @throws {TypeError} when test is false */ static assert(test, message) { if (!test) { if (typeof message === "string") { throw new TypeError(message); } else { throw new TypeError(); } } } /** * JSON-RPC version. * * @type {string} * @readonly */ static get VERSION() { return "2.0"; } /** * Member name for specifying JSON-RPC version. * Used in request and response objects. * * @type {string} * @readonly */ static get JSONRPC() { return "jsonrpc"; } /** * Member name for specifying identifier established by client. * Used in request and response objects. * * @type {string} * @readonly */ static get ID() { return "id"; } /** * Member name for specifying method name to be invoked. * Used in request objects. * * @type {string} * @readonly */ static get METHOD() { return "method"; } /** * Member name for specifying parameter values to be used during the invocation of the method. * Used in request objects. * * @type {string} * @readonly */ static get PARAMS() { return "params"; } /** * Member name for specifying result of the successful method invocation. * Used in response objects. * * @type {string} * @readonly */ static get RESULT() { return "result"; } /** * Member name for specifying error of the method invocation. * Used in response objects. * * @type {string} * @readonly */ static get ERROR() { return "error"; } /** * Member name for specifying error code in error object. * Used in response error objects. * * @type {string} * @readonly */ static get ERROR_CODE() { return "code"; } /** * Member name for specifying error message in error object. * Used in response error objects. * * @type {string} * @readonly */ static get ERROR_MESSAGE() { return "message"; } /** * Member name for specifying error additional data in error object. * Used in response error objects. * * @type {string} * @readonly */ static get ERROR_DATA() { return "data"; } /** * Finds corresponding JSONRPCErrorObject among implementation defined and * among additional errors (if provided). * If 'err' is number - tests err against JSONRPCErrorObject.code. * If 'err' is object - tests err.code against JSONRPCErrorObject.code. * If error is found and 'err' is object - resulting error object is updated * with 'err.message' and 'err.data' values (if exists). * If error is not found and default is provided - returns default error * with 'data' property containing serialized original error. * If error is not found and there is no default provided - returns undefined. * * @param {number|object|Error} err - error to look for * @param {JSONRPCErrorObject|OpenRPCError} [defaultError] - error response to return if not found * @param {Array<JSONRPCErrorObject|OpenRPCError>} [errors] - additional list of errors to look * @returns {JSONRPCErrorObject|undefined} error response object or undefined if not found */ static findError(err, defaultError, errors) { let foundError; if (typeof err === "number") { if (Array.isArray(errors)) { // first look in provided errors list foundError = errors.find((e) => { if (e && typeof e === "object") { if (e[RPC.ERROR_CODE] === err) { if (typeof e[RPC.ERROR_MESSAGE] === "string") { return true; } } } return false; }); } // look in implementation errors list if not found if (!foundError) { foundError = RPC.IMPL_ERRORS.find((e) => e[RPC.ERROR_CODE] === err); } } if (!foundError && err && typeof err === "object") { if (Array.isArray(errors)) { // first look in provided errors list foundError = errors.find((e) => { if (e && typeof e === "object") { if (e[RPC.ERROR_CODE] === err[RPC.ERROR_CODE]) { if (typeof e[RPC.ERROR_MESSAGE] === "string") { return true; } } } return false; }); } // look in implementation errors list if not found if (!foundError) { foundError = RPC.IMPL_ERRORS.find( (e) => e[RPC.ERROR_CODE] === err[RPC.ERROR_CODE] ); } // Update 'message' and 'data' properties if (foundError) { foundError = ObjectUtils.deepClone(foundError); if ( err[RPC.ERROR_MESSAGE] && typeof err[RPC.ERROR_MESSAGE] === "string" ) { foundError[RPC.ERROR_MESSAGE] = err[RPC.ERROR_MESSAGE]; } if (typeof err[RPC.ERROR_DATA] !== "undefined") { foundError[RPC.ERROR_DATA] = err[RPC.ERROR_DATA]; } } } if ( !foundError && defaultError && typeof defaultError === "object" && typeof defaultError[RPC.ERROR_CODE] === "number" && typeof defaultError[RPC.ERROR_MESSAGE] === "string" ) { foundError = ObjectUtils.deepClone(defaultError); foundError[RPC.ERROR_DATA] = Exception.errSerializer(err); } return foundError; } /** * JSON stringify error. * Reported when response can not be stringified to JSON * (usualy due circular references). * Non-standard. * * @type {JSONRPCErrorObject} * @readonly */ static get ERROR_32000() { // @ts-ignore return { [RPC.ERROR_CODE]: -32000, [RPC.ERROR_MESSAGE]: "Stringify error", }; } /** * Pre-processor execution error. * Reported when pre-processor throws unknown error. * Non-standard. * * @type {JSONRPCErrorObject} * @readonly */ static get ERROR_32010() { // @ts-ignore return { [RPC.ERROR_CODE]: -32010, [RPC.ERROR_MESSAGE]: "Pre-processor execution error", }; } /** * Method execution error. * Reported when method execution throws error. * Non-standard. * * @type {JSONRPCErrorObject} * @readonly */ static get ERROR_32020() { // @ts-ignore return { [RPC.ERROR_CODE]: -32020, [RPC.ERROR_MESSAGE]: "Method execution error", }; } /** * Invalid result error. * Reported when method produces invalid result. * Non-standard. * * @type {JSONRPCErrorObject} * @readonly */ static get ERROR_32030() { // @ts-ignore return { [RPC.ERROR_CODE]: -32030, [RPC.ERROR_MESSAGE]: "Invalid result", }; } /** * General security error. * Non-standard. * * @type {JSONRPCErrorObject} * @readonly */ static get ERROR_32040() { // @ts-ignore return { [RPC.ERROR_CODE]: -32040, [RPC.ERROR_MESSAGE]: "Security error", }; } /** * Credentials expired error. * Non-standard. * * @type {JSONRPCErrorObject} * @readonly */ static get ERROR_32041() { // @ts-ignore return { [RPC.ERROR_CODE]: -32041, [RPC.ERROR_MESSAGE]: "Credentials expired", }; } /** * Post-processor execution error. * Reported when post-processor throws unknown error. * Non-standard. * * @type {JSONRPCErrorObject} * @readonly */ static get ERROR_32050() { // @ts-ignore return { [RPC.ERROR_CODE]: -32050, [RPC.ERROR_MESSAGE]: "Post-processor execution error", }; } /** * Invalid request error. * * @type {JSONRPCErrorObject} * @readonly */ static get ERROR_32600() { // @ts-ignore return { [RPC.ERROR_CODE]: -32600, [RPC.ERROR_MESSAGE]: "Invalid Request", }; } /** * Method not found error. * * @type {JSONRPCErrorObject} * @readonly */ static get ERROR_32601() { // @ts-ignore return { [RPC.ERROR_CODE]: -32601, [RPC.ERROR_MESSAGE]: "Method not found", }; } /** * Invalid params error. * * @type {JSONRPCErrorObject} * @readonly */ static get ERROR_32602() { // @ts-ignore return { [RPC.ERROR_CODE]: -32602, [RPC.ERROR_MESSAGE]: "Invalid params", }; } /** * Internal error. * * @type {JSONRPCErrorObject} * @readonly */ static get ERROR_32603() { // @ts-ignore return { [RPC.ERROR_CODE]: -32603, [RPC.ERROR_MESSAGE]: "Internal error", }; } /** * JSON parse error. * * @type {JSONRPCErrorObject} * @readonly */ static get ERROR_32700() { // @ts-ignore return { [RPC.ERROR_CODE]: -32700, [RPC.ERROR_MESSAGE]: "Parse error", }; } /** * List of error objects defined in specs. * * @type {Array<JSONRPCErrorObject>} * @readonly */ static get SPEC_ERRORS() { return [ RPC.ERROR_32600, RPC.ERROR_32601, RPC.ERROR_32602, RPC.ERROR_32603, RPC.ERROR_32700, ]; } /** * List of all error objects defined in this implementation * (including spec errors). * * @type {Array<JSONRPCErrorObject>} * @readonly */ static get IMPL_ERRORS() { return [ RPC.ERROR_32000, RPC.ERROR_32010, RPC.ERROR_32020, RPC.ERROR_32030, RPC.ERROR_32040, RPC.ERROR_32041, RPC.ERROR_32050, ...RPC.SPEC_ERRORS, ]; } /** * Service discovery method name as specified in OpenRPC. * * @type {string} * @readonly */ static get RPC_DISCOVER() { return rpcDiscoverDef.name; } /** * Service discovery method schema as specified in OpenRPC. * * @type {OpenRPC} * @readonly */ static get RPC_DISCOVER_SCHEMA() { return ObjectUtils.deepClone(rpcDiscoverDef); } } export { RPC }; /** * @typedef RPCOptionsType * @type {object} * @property {Array<PreProcessorFunction>} [preProcessors] - list of functions executed before handler execution * @property {Array<PostProcessorFunction>} [postProcessors] - list of functions executed before handler execution */ /** * Pre-processor funtion definition. * * @typedef PreProcessorFunction * @type {function} * @param {JSONRPCRequest} request * @param {RPC} rpc - reference to RPC handler * @throws {number|object|Error} stops request processing and * returns error response. */ /** * Post-processor funtion definition. * * @typedef PostProcessorFunction * @type {function} * @param {JSONRPCRequest} request * @param {JSONRPCResult} result * @param {RPC} rpc - reference to RPC handler * @throws {number|object|Error} returns specified error response instead. */ /** * JSON-RPC request object structure. * * @typedef JSONRPCRequest * @type {object} * @property {"2.0"} jsonrpc - JSON-RPC version always exact string "2.0" * @property {null|string|number} [id] - request id * @property {string} method - method to execute * @property {(Object<string, *>|Array)} [params] - parameters for method */ /** * JSON-RPC error object structure. * * @typedef JSONRPCErrorObject * @type {object} * @property {number} code - error code * @property {string} message - error message * @property {*} [data] - additional error data */ /** * JSON-RPC error response object structure. * * @typedef JSONRPCError * @type {object} * @property {"2.0"} jsonrpc - JSON-RPC version always exact string "2.0" * @property {null|string|number} id - request id * @property {JSONRPCErrorObject} error - error object */ /** * JSON-RPC result object response structure. * * @typedef JSONRPCResult * @type {object} * @property {"2.0"} jsonrpc - JSON-RPC version always exact string "2.0" * @property {null|string|number} id - request id * @property {*} result - result object */ /** * OpenRPC object structure. * This object MAY be extended with Specification Extensions. * * @typedef OpenRPC * @type {object} * @property {string} openrpc - OpenRPC semantic version number * @property {OpenRPCInfo} info - API metadata * @property {Array<OpenRPCServer>} [servers] - array of object providing information on connectivity to target server * @property {Array<(OpenRPCMethod|OpenRPCReference)>} methods - list of methods available in API * @property {Array<OpenRPCComponents>} [components] - holds various schemas for specification * @property {OpenRPCExternalDoc} [externalDocs] - additional external documentation */ /** * OpenRPC Info structure. * This object MAY be extended with Specification Extensions. * * @typedef OpenRPCInfo * @type {object} * @property {string} title - application title * @property {string} [description] - verbose application description * @property {string} [termsOfService] - URL to terms of service for API * @property {OpenRPCContact} [contact] - contact information for exposed API * @property {OpenRPCLicense} [license] - license information for exposed API * @property {string} version - API version */ /** * OpenRPC Contact structure. * This object MAY be extended with Specification Extensions. * * @typedef OpenRPCContact * @type {object} * @property {string} [name] - name of contact person/organization * @property {string} [url] - URL to contact information * @property {string} [email] - email of contact person/organization */ /** * OpenRPC License structure. * This object MAY be extended with Specification Extensions. * * @typedef OpenRPCLicense * @type {object} * @property {string} name - license name used for API * @property {string} [url] - URL to license */ /** * OpenRPC Server structure. * This object MAY be extended with Specification Extensions. * * @typedef OpenRPCServer * @type {object} * @property {string} name - cannonical name for the server * @property {string} url - URL to the target host * @property {string} [summary] - short summary for what server is * @property {string} [description] - description of host designated by url * @property {Object<string, OpenRPCServerVariable>} [variables] - map of variables */ /** * OpenRPC Server variable structure. * This object MAY be extended with Specification Extensions. * * @typedef OpenRPCServerVariable * @type {object} * @property {Array<string>} [enum] - enumeration of string values to be used if the substitution options are from a limited set * @property {string} default - default value to use for substitution, which SHALL be sent if an alternate value is not supplied * @property {string} [description] - Description of server variable */ /** * OpenRPC Method structure. * This object MAY be extended with Specification Extensions. * * @typedef OpenRPCMethod * @type {object} * @property {string} name - method name * @property {Array<OpenRPCTag|OpenRPCReference>} [tags] - list of tags * @property {string} [summary] - short summary for what method does * @property {string} [description] - verbose explanation of the method behavior * @property {OpenRPCExternalDoc} [externalDocs] - additional external documentation * @property {Array<(OpenRPCContentDescriptor|OpenRPCReference)>} params - list of parameters for method * @property {(OpenRPCContentDescriptor|OpenRPCReference)} result - result returned by method * @property {boolean} [deprecated] - declares this method deprecated * @property {Array<OpenRPCServer>} [servers] - alternative servers to service this method * @property {Array<(OpenRPCError|OpenRPCReference)>} [errors] - list of custom application defined errors * @property {Array<(OpenRPCLink|OpenRPCReference)>} [links] - list of possible links from this method call * @property {("by-name"|"by-position"|"either")} [paramStructure] - expected format of paramters. Defaults to "by-position". * @property {Array<OpenRPCExamplePairing>} [examples] - list of examples */ /** * OpenRPC Content Descriptor structure. * This object MAY be extended with Specification Extensions. * * @typedef OpenRPCContentDescriptor * @type {object} * @property {string} name - name of content being described * @property {string} [summary] - short summary of content being described * @property {string} [description] - verbose explanation of content descriptor behavior * @property {boolean} [required] - determines if content is required field. Defaults to false * @property {object} schema - schema that describes content * @property {boolean} [deprecated] - specifies that content is deprecated. Defaults to false */ /** * OpenRPC Example Pairing structure. * This object MAY be extended with Specification Extensions. * * @typedef OpenRPCExamplePairing * @type {object} * @property {string} [name] - name for example pairing * @property {string} [summary] - short description for example pairing * @property {string} [description] - verbose explanation of example pairing * @property {Array<(OpenRPCExample|OpenRPCReference)>} [params] - example parameters * @property {(OpenRPCExample|OpenRPCReference)} [result] - example result */ /** * OpenRPC Example structure. * This object MAY be extended with Specification Extensions. * * @typedef OpenRPCExample * @type {object} * @property {string} [name] - canonical example name * @property {string} [summary] - short description for example * @property {string} [description] - verbose explanation of example * @property {*} [value] - embeded literal example * @property {string} [externalValue] - URL to literal example which can not easily be included in JSON */ /** * OpenRPC Link structure. * This object MAY be extended with Specification Extensions. * * @typedef OpenRPCLink * @type {object} * @property {string} name - canonical link name * @property {string} [summary] - short description for link * @property {string} [description] - full description for link * @property {string} [method] - name of existing, resolvable OpenRPC method * @property {Object<string, *>} [params] - params to pass to method * @property {OpenRPCServer} [server] - server to be used by target method */ /** * OpenRPC Error structure. * * @typedef OpenRPCError * @type {object} * @property {number} code - error code * @property {string} message - short error description * @property {*} [data] - additional information about error */ /** * OpenRPC Components structure. * This object MAY be extended with Specification Extensions. * * @typedef OpenRPCComponents * @type {object} * @property {Object<string, OpenRPCContentDescriptor>} [contentDescriptors] - reusable content descriptors * @property {Object<string, object>} [schemas] - reusable schemas * @property {Object<string, OpenRPCExample>} [examples] - reusable examples * @property {Object<string, OpenRPCLink>} [links] - reusable links * @property {Object<string, OpenRPCError>} [errors] - reusable errors * @property {Object<string, OpenRPCExamplePairing>} [examplePairingObjects] - reusable example pairings * @property {Object<string, OpenRPCTag>} [tags] - reusable tags */ /** * OpenRPC Tag structure. * This object MAY be extended with Specification Extensions. * * @typedef OpenRPCTag * @type {object} * @property {string} name - tag name * @property {string} [summary] - short summary for tag * @property {string} [description] - verbose tag explanation * @property {OpenRPCExternalDoc} [externalDocs] - additional external documentation */ /** * OpenRPC External Document structure. * This object MAY be extended with Specification Extensions. * * @typedef OpenRPCExternalDoc * @type {object} * @property {string} [description] - document description * @property {string} url - target document url */ /** * OpenRPC Reference structure. * * @typedef OpenRPCReference * @type {object} * @property {string} $ref - reference string */ /** * OpenRPC Specification Extensions structure. * The extensions properties are implemented as patterned fields * that are always prefixed by "x-". * * @typedef OpenRPCSpecificationExtensions * @type {object} */