rpc-express-toolkit 2.1.0 → 2.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -91,6 +91,7 @@ app.listen(3000, () => {
91
91
  });
92
92
  ```
93
93
 
94
+
94
95
  ### Client Usage
95
96
 
96
97
  ```javascript
@@ -108,6 +109,21 @@ const client = new RpcClient('/api');
108
109
  const user = await client.call('getUser', { userId: 123 });
109
110
  ```
110
111
 
112
+ #### SSL e certificati self-signed in sviluppo (Node.js)
113
+
114
+ Per bypassare la validazione dei certificati SSL (ad esempio con certificati self-signed in sviluppo), imposta **prima** di avviare il client:
115
+
116
+ ```js
117
+ process.env.NODE_TLS_REJECT_UNAUTHORIZED = "0";
118
+ ```
119
+
120
+ **Attenzione:**
121
+ - Questa impostazione disabilita la validazione SSL per tutte le richieste nel processo Node.js.
122
+ - Usala solo in ambienti di sviluppo!
123
+ - Non committare mai questa impostazione in produzione.
124
+
125
+ Le opzioni avanzate `agent` e `ca` sono state rimosse per semplicità e compatibilità con Node.js moderno.
126
+
111
127
  ## Advanced Usage
112
128
 
113
129
  ### Configuration Options
@@ -516,30 +532,40 @@ logger.error('Database error', {
516
532
 
517
533
  ### Client Configuration
518
534
 
519
- The `RpcClient` supports various configuration options for different environments:
535
+ The `RpcClient` supports various configuration options for different environments, **inclusa la gestione di certificati self-signed**:
520
536
 
521
537
  ```javascript
538
+ const fs = require('fs');
522
539
  const { RpcClient } = require('rpc-express-toolkit');
523
540
 
524
541
  // Basic client
525
542
  const client = new RpcClient('https://api.example.com/rpc');
526
543
 
527
- // Client with custom headers
544
+ // Client con header personalizzati
528
545
  const clientWithHeaders = new RpcClient('https://api.example.com/rpc', {
529
546
  'Authorization': 'Bearer your-token',
530
547
  'X-API-Key': 'your-api-key',
531
548
  'User-Agent': 'MyApp/1.0'
532
549
  });
533
550
 
534
- // Client for development with self-signed certificates
551
+ // Client per sviluppo con certificato self-signed (accetta qualsiasi certificato, NON usare in produzione)
535
552
  const devClient = new RpcClient('https://localhost:3000/api', {
536
553
  'Authorization': 'Bearer dev-token'
537
554
  }, {
538
- rejectUnauthorized: false // Only for development!
555
+ rejectUnauthorized: false // Solo per sviluppo!
556
+ });
557
+
558
+ // Client per sviluppo con CA self-signed (modo sicuro: accetta solo la tua CA)
559
+ const ca = fs.readFileSync('path/to/your/selfsigned-ca.pem');
560
+ const devClientWithCA = new RpcClient('https://localhost:3000/api', {
561
+ 'Authorization': 'Bearer dev-token'
562
+ }, {
563
+ ca // Passa il certificato della tua CA self-signed
564
+ // oppure: agent: new require('https').Agent({ ca, rejectUnauthorized: true })
539
565
  });
540
566
  ```
541
567
 
542
- **⚠️ Security Warning**: Only use `rejectUnauthorized: false` in development environments with self-signed certificates. Never use this in production as it disables SSL certificate validation and makes your application vulnerable to man-in-the-middle attacks.
568
+ **⚠️ Security Warning**: Usa `rejectUnauthorized: false` solo in sviluppo. Per maggiore sicurezza, preferisci sempre la soluzione con `ca` per accettare solo la tua CA self-signed. In produzione usa sempre certificati validi e firmati da una CA attendibile.
543
569
 
544
570
  ### Built-in Middlewares
545
571
 
@@ -18,33 +18,32 @@ async function demonstrateSSLOptions() {
18
18
  });
19
19
  console.log(' ✓ Created client with strict SSL validation (default)\n');
20
20
 
21
- // Example 2: Development client for self-signed certificates
21
+
22
+
23
+ // Example 2: Development client per self-signed certificates (bypassa TUTTA la validazione SSL)
22
24
  console.log('2. Development Client (bypasses SSL validation):');
23
- const developmentClient = new RpcClient('https://localhost:3000/api', {
24
- 'Authorization': 'Bearer dev-token',
25
- 'X-Environment': 'development'
26
- }, {
27
- rejectUnauthorized: false // Only for development!
28
- });
29
- console.log(' ✓ Created client that bypasses SSL certificate validation');
30
- console.log(' ⚠️ WARNING: Only use this for development with self-signed certificates!\n');
25
+ console.log(' ⚠️ Per bypassare la validazione SSL in Node.js, imposta PRIMA di avviare il client:');
26
+ console.log(' process.env.NODE_TLS_REJECT_UNAUTHORIZED = "0";');
27
+ console.log(' ✓ Tutte le richieste accetteranno certificati self-signed (solo per sviluppo!)\n');
31
28
 
32
- // Example 3: Demonstrate how to choose configuration based on environment
33
- console.log('3. Environment-based Configuration:');
34
-
29
+ // Example 3: Best practice
30
+ console.log('3. Best Practices:');
31
+ console.log(' ✓ Usa sempre la validazione SSL in produzione');
32
+ console.log(' ✓ Disabilita la validazione solo per sviluppo con self-signed');
33
+ console.log(' ✓ Usa variabili d’ambiente per controllare il comportamento SSL');
34
+ console.log(' ✓ Non committare mai process.env.NODE_TLS_REJECT_UNAUTHORIZED = "0" in produzione');
35
+ console.log(' ✓ Considera l’uso di certificati di sviluppo validi invece di disabilitare la validazione\n');
36
+
37
+ // Example 4: Demonstrate how to choose configuration based on environment
38
+ console.log('4. Environment-based Configuration:');
35
39
  const isDevelopment = process.env.NODE_ENV === 'development';
36
40
  const baseUrl = isDevelopment ? 'https://localhost:3000/api' : 'https://api.production.com/rpc';
37
-
38
41
  const environmentClient = new RpcClient(baseUrl, {
39
42
  'Authorization': `Bearer ${process.env.API_TOKEN || 'default-token'}`,
40
43
  'X-Environment': process.env.NODE_ENV || 'development'
41
- }, {
42
- // Only bypass SSL validation in development
43
- ...(isDevelopment && { rejectUnauthorized: false })
44
- });
45
-
44
+ }, isDevelopment ? { ca } : {});
46
45
  console.log(` ✓ Created client for ${isDevelopment ? 'development' : 'production'} environment`);
47
- console.log(` ✓ SSL validation: ${isDevelopment ? 'disabled (dev only)' : 'enabled (secure)'}\n`);
46
+ console.log(` ✓ SSL validation: ${isDevelopment ? 'custom CA (dev only)' : 'enabled (secure)'}\n`);
48
47
 
49
48
  // Example usage (commented out since we don't have a real server)
50
49
  /*
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "rpc-express-toolkit",
3
- "version": "2.1.0",
4
- "description": "Enterprise-ready JSON-RPC 2.0 toolkit for Express.js with simplified APIs, structured logging, middleware system, schema validation, and comprehensive BigInt/Date support.",
3
+ "version": "2.3.0",
4
+ "description": "Enterprise-ready JSON-RPC 2.0 toolkit for Express.js with simplified APIs, structured logging, middleware system, schema validation, comprehensive BigInt/Date support. Semplificato: gestione SSL avanzata rimossa, vedi README.",
5
5
  "main": "src/index.js",
6
6
  "types": "src/index.d.ts",
7
7
  "files": [
@@ -39,7 +39,11 @@
39
39
  "validation",
40
40
  "bigint",
41
41
  "enterprise",
42
- "nodejs"
42
+ "nodejs",
43
+ "ssl",
44
+ "self-signed",
45
+ "secure",
46
+ "fetch"
43
47
  ],
44
48
  "author": "Nicola Carpanese <nicola.carpanese@gmail.com>",
45
49
  "license": "MIT",
@@ -52,21 +52,10 @@ class RpcClient {
52
52
 
53
53
  // Store fetch options for Node.js environments
54
54
  this.#fetchOptions = {};
55
-
56
- // Handle SSL certificate validation option for Node.js
57
- if (typeof process !== 'undefined' && process.env && options.rejectUnauthorized === false) {
58
- // Set up agent for Node.js fetch implementations
59
- if (typeof require !== 'undefined') {
60
- try {
61
- const https = require('https');
62
- this.#fetchOptions.agent = new https.Agent({
63
- rejectUnauthorized: false
64
- });
65
- } catch (e) {
66
- // Ignore if https module is not available
67
- }
68
- }
69
- }
55
+ // SSL validation: la gestione avanzata (agent/ca) è rimossa per semplicità e compatibilità.
56
+ // Per bypassare i certificati self-signed in sviluppo, imposta:
57
+ // process.env.NODE_TLS_REJECT_UNAUTHORIZED = "0";
58
+ // Vedi README per dettagli e best practice.
70
59
  }
71
60
 
72
61
  /**
@@ -1 +1 @@
1
- let fetchFn;if("undefined"==typeof BigInt||BigInt.prototype.toJSON||(BigInt.prototype.toJSON=function(){return this.toString()}),"undefined"!=typeof fetch)fetchFn=fetch;else try{fetchFn=require("node-fetch")}catch(t){if("undefined"==typeof globalThis||!globalThis.fetch)throw new Error("fetch is not available. Please install node-fetch or use Node.js 18+");fetchFn=globalThis.fetch}class RpcClient{#t;#e;#r;#i;constructor(t,e={},r={}){if(this.#t=t,this.#e={"Content-Type":"application/json",...e},this.#r=1e3*Date.now()+Math.floor(1e3*Math.random()),this.#i={},"undefined"!=typeof process&&process.env&&!1===r.rejectUnauthorized&&"undefined"!=typeof require)try{const t=require("https");this.#i.agent=new t.Agent({rejectUnauthorized:!1})}catch(t){}}#n(){return++this.#r}async call(t,e={},r=void 0,i={}){const n={jsonrpc:"2.0",method:t,params:e,id:void 0===r?this.#n():r};try{const t=await fetchFn(this.#t,{method:"POST",headers:{...this.#e,...i},body:JSON.stringify(n),...this.#i});if(!t.ok)throw new Error(`HTTP Error: ${t.status} ${t.statusText}`);const e=await t.json();if(e.error)throw e.error;return this.deserializeBigIntsAndDates(e.result)}catch(t){throw console.error("RPC call failed:",t),t}}async notify(t,e={},r={}){await this.call(t,e,null,r)}async batch(t,e={}){const r=t.map(t=>({jsonrpc:"2.0",method:t.method,params:t.params||{},id:void 0!==t.id?t.id:this.#n()}));try{const t=await fetchFn(this.#t,{method:"POST",headers:{...this.#e,...e},body:JSON.stringify(r),...this.#i});if(!t.ok)throw new Error(`HTTP Error: ${t.status} ${t.statusText}`);const i=await t.json();if(Array.isArray(i))return i.map(t=>{if(t.error)throw t.error;return this.deserializeBigIntsAndDates(t.result)});if(i.error)throw i.error;return[this.deserializeBigIntsAndDates(i.result)]}catch(t){throw console.error("Batch RPC call failed:",t),t}}serializeBigIntsAndDates(t){if("bigint"==typeof t)return t.toString();if(Array.isArray(t))return t.map(t=>this.serializeBigIntsAndDates(t));if(t instanceof Date)return t.toISOString();if(t&&"object"==typeof t){const e={};for(const[r,i]of Object.entries(t))e[r]=this.serializeBigIntsAndDates(i);return e}return t}deserializeBigIntsAndDates(t){const e=/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d+)?(Z|[+-]\d{2}:\d{2})?$/;if("string"==typeof t){if(/^-?\d+n?$/.test(t))return BigInt(t.replace(/n$/,""));if(e.test(t)){const e=new Date(t);if(!isNaN(e.getTime()))return e}}return Array.isArray(t)?t.map(t=>this.deserializeBigIntsAndDates(t)):t&&"object"==typeof t?Object.fromEntries(Object.entries(t).map(([t,e])=>[t,this.deserializeBigIntsAndDates(e)])):t}}module.exports=RpcClient,module.exports.RpcClient=RpcClient;
1
+ let fetchFn;if("undefined"==typeof BigInt||BigInt.prototype.toJSON||(BigInt.prototype.toJSON=function(){return this.toString()}),"undefined"!=typeof fetch)fetchFn=fetch;else try{fetchFn=require("node-fetch")}catch(t){if("undefined"==typeof globalThis||!globalThis.fetch)throw new Error("fetch is not available. Please install node-fetch or use Node.js 18+");fetchFn=globalThis.fetch}class RpcClient{#t;#e;#r;#i;constructor(t,e={},r={}){this.#t=t,this.#e={"Content-Type":"application/json",...e},this.#r=1e3*Date.now()+Math.floor(1e3*Math.random()),this.#i={}}#s(){return++this.#r}async call(t,e={},r=void 0,i={}){const s={jsonrpc:"2.0",method:t,params:e,id:void 0===r?this.#s():r};try{const t=await fetchFn(this.#t,{method:"POST",headers:{...this.#e,...i},body:JSON.stringify(s),...this.#i});if(!t.ok)throw new Error(`HTTP Error: ${t.status} ${t.statusText}`);const e=await t.json();if(e.error)throw e.error;return this.deserializeBigIntsAndDates(e.result)}catch(t){throw console.error("RPC call failed:",t),t}}async notify(t,e={},r={}){await this.call(t,e,null,r)}async batch(t,e={}){const r=t.map(t=>({jsonrpc:"2.0",method:t.method,params:t.params||{},id:void 0!==t.id?t.id:this.#s()}));try{const t=await fetchFn(this.#t,{method:"POST",headers:{...this.#e,...e},body:JSON.stringify(r),...this.#i});if(!t.ok)throw new Error(`HTTP Error: ${t.status} ${t.statusText}`);const i=await t.json();if(Array.isArray(i))return i.map(t=>{if(t.error)throw t.error;return this.deserializeBigIntsAndDates(t.result)});if(i.error)throw i.error;return[this.deserializeBigIntsAndDates(i.result)]}catch(t){throw console.error("Batch RPC call failed:",t),t}}serializeBigIntsAndDates(t){if("bigint"==typeof t)return t.toString();if(Array.isArray(t))return t.map(t=>this.serializeBigIntsAndDates(t));if(t instanceof Date)return t.toISOString();if(t&&"object"==typeof t){const e={};for(const[r,i]of Object.entries(t))e[r]=this.serializeBigIntsAndDates(i);return e}return t}deserializeBigIntsAndDates(t){const e=/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d+)?(Z|[+-]\d{2}:\d{2})?$/;if("string"==typeof t){if(/^-?\d+n?$/.test(t))return BigInt(t.replace(/n$/,""));if(e.test(t)){const e=new Date(t);if(!isNaN(e.getTime()))return e}}return Array.isArray(t)?t.map(t=>this.deserializeBigIntsAndDates(t)):t&&"object"==typeof t?Object.fromEntries(Object.entries(t).map(([t,e])=>[t,this.deserializeBigIntsAndDates(e)])):t}}module.exports=RpcClient,module.exports.RpcClient=RpcClient;
@@ -1 +1 @@
1
- let t;if("undefined"==typeof BigInt||BigInt.prototype.toJSON||(BigInt.prototype.toJSON=function(){return this.toString()}),"undefined"!=typeof fetch)t=fetch;else try{const{default:e}=await import("node-fetch");t=e}catch(e){if("undefined"==typeof globalThis||!globalThis.fetch)throw new Error("fetch is not available. Please install node-fetch or use Node.js 18+");t=globalThis.fetch}export class RpcClient{#t;#e;#r;#i;constructor(t,e={},r={}){if(this.#t=t,this.#e={"Content-Type":"application/json",...e},this.#r=1e3*Date.now()+Math.floor(1e3*Math.random()),this.#i={},"undefined"!=typeof process&&process.env&&!1===r.rejectUnauthorized&&"undefined"!=typeof require)try{const t=require("https");this.#i.agent=new t.Agent({rejectUnauthorized:!1})}catch(t){import("https").then(t=>{this.#i.agent=new t.Agent({rejectUnauthorized:!1})}).catch(()=>{})}}async call(e,r={},i=null,s={}){null===i&&(i=++this.#r);const n={jsonrpc:"2.0",method:e,params:r,id:i};try{const e=await t(this.#t,{method:"POST",headers:{...this.#e,...s},body:JSON.stringify(n),...this.#i});if(!e.ok)throw new Error(`HTTP Error: ${e.status} ${e.statusText}`);const r=await e.json();if(r.error)throw r.error;return this.deserializeBigIntsAndDates(r.result)}catch(t){throw console.error("RPC call failed:",t),t}}async notify(e,r={},i={}){const s={jsonrpc:"2.0",method:e,params:r};try{const e=await t(this.#t,{method:"POST",headers:{...this.#e,...i},body:JSON.stringify(s),...this.#i});if(!e.ok)throw new Error(`HTTP Error: ${e.status} ${e.statusText}`)}catch(t){throw console.error("RPC notification failed:",t),t}}async batch(e,r={}){const i=e.map(t=>{const e={jsonrpc:"2.0",method:t.method,params:t.params||{}};return t.notification||(e.id=void 0!==t.id?t.id:++this.#r),e});try{const e=await t(this.#t,{method:"POST",headers:{...this.#e,...r},body:JSON.stringify(i),...this.#i});if(!e.ok)throw new Error(`HTTP Error: ${e.status} ${e.statusText}`);const s=await e.json();if(Array.isArray(s))return s.map(t=>{if(t.error)throw t.error;return this.deserializeBigIntsAndDates(t.result)});if(s.error)throw s.error;return[this.deserializeBigIntsAndDates(s.result)]}catch(t){throw console.error("RPC batch call failed:",t),t}}serializeBigIntsAndDates(t){if("bigint"==typeof t)return t.toString();if(Array.isArray(t))return t.map(t=>this.serializeBigIntsAndDates(t));if(t instanceof Date)return t.toISOString();if(t&&"object"==typeof t){const e={};for(const[r,i]of Object.entries(t))e[r]=this.serializeBigIntsAndDates(i);return e}return t}deserializeBigIntsAndDates(t){const e=/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d+)?(Z|[+-]\d{2}:\d{2})?$/;if("string"==typeof t){if(/^-?\d+n?$/.test(t))return BigInt(t.replace(/n$/,""));if(e.test(t)){const e=new Date(t);if(!isNaN(e.getTime()))return e}}return Array.isArray(t)?t.map(t=>this.deserializeBigIntsAndDates(t)):t&&"object"==typeof t?Object.fromEntries(Object.entries(t).map(([t,e])=>[t,this.deserializeBigIntsAndDates(e)])):t}}
1
+ let t;if("undefined"==typeof BigInt||BigInt.prototype.toJSON||(BigInt.prototype.toJSON=function(){return this.toString()}),"undefined"!=typeof fetch)t=fetch;else try{const{default:e}=await import("node-fetch");t=e}catch(e){if("undefined"==typeof globalThis||!globalThis.fetch)throw new Error("fetch is not available. Please install node-fetch or use Node.js 18+");t=globalThis.fetch}export class RpcClient{#t;#e;#r;#s;constructor(t,e={},r={}){this.#t=t,this.#e={"Content-Type":"application/json",...e},this.#r=1e3*Date.now()+Math.floor(1e3*Math.random()),this.#s={}}async call(e,r={},s=null,i={}){null===s&&(s=++this.#r);const n={jsonrpc:"2.0",method:e,params:r,id:s};try{const e=await t(this.#t,{method:"POST",headers:{...this.#e,...i},body:JSON.stringify(n),...this.#s});if(!e.ok)throw new Error(`HTTP Error: ${e.status} ${e.statusText}`);const r=await e.json();if(r.error)throw r.error;return this.deserializeBigIntsAndDates(r.result)}catch(t){throw console.error("RPC call failed:",t),t}}async notify(e,r={},s={}){const i={jsonrpc:"2.0",method:e,params:r};try{const e=await t(this.#t,{method:"POST",headers:{...this.#e,...s},body:JSON.stringify(i),...this.#s});if(!e.ok)throw new Error(`HTTP Error: ${e.status} ${e.statusText}`)}catch(t){throw console.error("RPC notification failed:",t),t}}async batch(e,r={}){const s=e.map(t=>{const e={jsonrpc:"2.0",method:t.method,params:t.params||{}};return t.notification||(e.id=void 0!==t.id?t.id:++this.#r),e});try{const e=await t(this.#t,{method:"POST",headers:{...this.#e,...r},body:JSON.stringify(s),...this.#s});if(!e.ok)throw new Error(`HTTP Error: ${e.status} ${e.statusText}`);const i=await e.json();if(Array.isArray(i))return i.map(t=>{if(t.error)throw t.error;return this.deserializeBigIntsAndDates(t.result)});if(i.error)throw i.error;return[this.deserializeBigIntsAndDates(i.result)]}catch(t){throw console.error("RPC batch call failed:",t),t}}serializeBigIntsAndDates(t){if("bigint"==typeof t)return t.toString();if(Array.isArray(t))return t.map(t=>this.serializeBigIntsAndDates(t));if(t instanceof Date)return t.toISOString();if(t&&"object"==typeof t){const e={};for(const[r,s]of Object.entries(t))e[r]=this.serializeBigIntsAndDates(s);return e}return t}deserializeBigIntsAndDates(t){const e=/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d+)?(Z|[+-]\d{2}:\d{2})?$/;if("string"==typeof t){if(/^-?\d+n?$/.test(t))return BigInt(t.replace(/n$/,""));if(e.test(t)){const e=new Date(t);if(!isNaN(e.getTime()))return e}}return Array.isArray(t)?t.map(t=>this.deserializeBigIntsAndDates(t)):t&&"object"==typeof t?Object.fromEntries(Object.entries(t).map(([t,e])=>[t,this.deserializeBigIntsAndDates(e)])):t}}
@@ -53,28 +53,10 @@ export class RpcClient {
53
53
 
54
54
  // Store fetch options for Node.js environments
55
55
  this.#fetchOptions = {};
56
-
57
- // Handle SSL certificate validation option for Node.js
58
- if (typeof process !== 'undefined' && process.env && options.rejectUnauthorized === false) {
59
- // Set up agent for Node.js fetch implementations
60
- if (typeof require !== 'undefined') {
61
- try {
62
- const https = require('https');
63
- this.#fetchOptions.agent = new https.Agent({
64
- rejectUnauthorized: false
65
- });
66
- } catch (e) {
67
- // For ES modules, we'll try dynamic import
68
- import('https').then(httpsModule => {
69
- this.#fetchOptions.agent = new httpsModule.Agent({
70
- rejectUnauthorized: false
71
- });
72
- }).catch(() => {
73
- // Ignore if https module is not available
74
- });
75
- }
76
- }
77
- }
56
+ // SSL validation: la gestione avanzata (agent/ca) è rimossa per semplicità e compatibilità.
57
+ // Per bypassare i certificati self-signed in sviluppo, imposta:
58
+ // process.env.NODE_TLS_REJECT_UNAUTHORIZED = "0";
59
+ // Vedi README per dettagli e best practice.
78
60
  }
79
61
 
80
62
  /**
package/src/index.d.ts CHANGED
@@ -92,8 +92,16 @@ declare class RpcClient {
92
92
  * @param defaultHeaders - Default headers to include in requests
93
93
  * @param options - Configuration options
94
94
  * @param options.rejectUnauthorized - Whether to reject unauthorized SSL certificates (default: true). Set to false for development with self-signed certificates.
95
+ * @param options.ca - Buffer or string of a custom CA certificate to trust (for self-signed CA)
96
+ * @param options.agent - Custom https.Agent instance (advanced usage)
95
97
  */
96
- constructor(endpoint: string, defaultHeaders?: Record<string, string>, options?: { rejectUnauthorized?: boolean });
98
+ constructor(
99
+ endpoint: string,
100
+ defaultHeaders?: Record<string, string>,
101
+ options?: {
102
+ // Opzioni SSL avanzate rimosse: usa process.env.NODE_TLS_REJECT_UNAUTHORIZED per sviluppo
103
+ }
104
+ );
97
105
 
98
106
  /**
99
107
  * Make a JSON-RPC call to the server.