rpc-express-toolkit 3.0.2 → 4.0.1

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.
@@ -1,43 +1,28 @@
1
1
  /**
2
2
  * JSON-RPC 2.0 Client for browser and Node.js environments.
3
- * Handles BigInt and Date serialization/deserialization automatically.
4
- *
5
- * Note: We extend BigInt.prototype.toJSON to handle automatic serialization
6
- * when JSON.stringify() is called, as BigInt doesn't have native JSON support.
3
+ * Handles BigInt and Date serialization/deserialization automatically
4
+ * using explicit serializers (no global prototype extensions).
7
5
  */
8
6
 
9
- // Extend BigInt prototype to handle JSON serialization
10
- if (typeof BigInt !== 'undefined' && !BigInt.prototype.toJSON) {
11
- BigInt.prototype.toJSON = function () {
12
- return this.toString();
13
- };
14
- }
15
-
16
- // Polyfill for Node.js environments that don't have fetch
7
+ // Select fetch implementation (Node 18+ or browser)
17
8
  let fetchFn;
18
- if (typeof fetch !== 'undefined') {
19
- fetchFn = fetch;
9
+ if (typeof globalThis !== 'undefined' && globalThis.fetch) {
10
+ fetchFn = globalThis.fetch.bind(globalThis);
20
11
  } else {
21
- // Try to use node-fetch if available, otherwise use built-in fetch in Node.js 18+
22
- try {
23
- fetchFn = require('node-fetch');
24
- } catch (e) {
25
- // For Node.js 18+, use global fetch
26
- if (typeof globalThis !== 'undefined' && globalThis.fetch) {
27
- fetchFn = globalThis.fetch;
28
- } else {
29
- throw new Error(
30
- 'fetch is not available. Please install node-fetch or use Node.js 18+'
31
- );
32
- }
33
- }
12
+ throw new Error(
13
+ 'globalThis.fetch is not available. Node.js 18+ is required.'
14
+ );
34
15
  }
35
16
 
36
17
  class RpcClient {
37
18
  #endpoint;
19
+
38
20
  #defaultHeaders;
21
+
39
22
  #requestId;
23
+
40
24
  #fetchOptions;
25
+
41
26
  #options;
42
27
 
43
28
  /**
@@ -45,8 +30,8 @@ class RpcClient {
45
30
  * @param {Object} [defaultHeaders={}] Optional default headers to include in requests.
46
31
  * @param {Object} [options={}] Optional configuration options.
47
32
  * @param {boolean} [options.rejectUnauthorized=true] Whether to reject unauthorized SSL certificates. Set to false for development with self-signed certificates.
48
- * @param {boolean} [options.safeStringEnabled=true] Whether to prefix strings with 'S:' to avoid confusion with BigInt values.
49
- * @param {boolean} [options.safeDateEnabled=true] Whether to prefix dates with 'D:' to avoid confusion with ISO string values.
33
+ * @param {boolean} [options.safeEnabled=false] Whether to use safe serialization for both strings and dates to avoid confusion with BigInt/ISO values. Default false for JSON-RPC 2.0 compliance.
34
+ * @param {boolean} [options.warnOnUnsafe=true] Whether to show educational warnings when BigInt/Date objects are serialized without safe mode. Default true.
50
35
  */
51
36
  constructor(endpoint, defaultHeaders = {}, options = {}) {
52
37
  this.#endpoint = endpoint;
@@ -59,11 +44,9 @@ class RpcClient {
59
44
 
60
45
  // Store options
61
46
  this.#options = {
62
- safeStringEnabled: options.safeStringEnabled !== false, // Default true
63
- safeDateEnabled: options.safeDateEnabled !== false, // Default true
64
- warnOnUnsafeString: options.warnOnUnsafeString !== false, // Default true
65
- warnOnUnsafeDate: options.warnOnUnsafeDate !== false, // Default true
66
- ...options
47
+ safeEnabled: options.safeEnabled === true, // Default false
48
+ warnOnUnsafe: options.warnOnUnsafe !== false, // Default true
49
+ ...options,
67
50
  };
68
51
 
69
52
  // Store fetch options for Node.js environments
@@ -84,11 +67,12 @@ class RpcClient {
84
67
 
85
68
  /**
86
69
  * Make a JSON-RPC call to the server.
70
+ * @template T
87
71
  * @param {string} method The RPC method name.
88
72
  * @param {any} params The parameters to pass to the RPC method.
89
73
  * @param {string|number|null} [id] The request ID (auto-generated if not provided).
90
74
  * @param {Object} [overrideHeaders={}] Optional headers to override defaults for this request.
91
- * @returns {Promise<any>} The result of the RPC call.
75
+ * @returns {Promise<T>} The result of the RPC call.
92
76
  */
93
77
  async call(method, params, id = undefined, overrideHeaders = {}) {
94
78
  // Auto-generate ID if not provided (null means notification)
@@ -110,13 +94,11 @@ class RpcClient {
110
94
  method: 'POST',
111
95
  headers: {
112
96
  ...this.#defaultHeaders,
113
- // Add safe options headers so server knows what client expects
114
- 'X-RPC-SafeString-Enabled': this.#options.safeStringEnabled ? 'true' : 'false',
115
- 'X-RPC-SafeDate-Enabled': this.#options.safeDateEnabled ? 'true' : 'false',
97
+ // Add safe options header so server knows what client expects
98
+ 'X-RPC-Safe-Enabled': this.#options.safeEnabled ? 'true' : 'false',
116
99
  ...overrideHeaders,
117
100
  },
118
- // Since we have BigInt.prototype.toJSON, we don't need manual serialization
119
- // JSON.stringify will handle BigInt and Date automatically
101
+ // Params are pre-serialized (BigInt/Date) via serializeBigIntsAndDates
120
102
  body: JSON.stringify(requestBody),
121
103
  ...this.#fetchOptions, // Include any additional fetch options (e.g., SSL settings)
122
104
  });
@@ -133,18 +115,42 @@ class RpcClient {
133
115
  throw responseBody.error;
134
116
  }
135
117
 
136
- // Check server's safe options from response headers
137
- const serverSafeStringEnabled = response.headers.get('X-RPC-SafeString-Enabled') === 'true';
138
- const serverSafeDateEnabled = response.headers.get('X-RPC-SafeDate-Enabled') === 'true';
118
+ // Check server's safe options from response headers - strict compatibility checking
119
+ const serverSafeHeader = response.headers.get('X-RPC-Safe-Enabled');
120
+
121
+ // Errore rigoroso: se client ha safe enabled ma server non risponde con header
122
+ if (this.#options.safeEnabled && serverSafeHeader === null) {
123
+ throw new Error(
124
+ 'RPC Compatibility Error: Client has safe serialization enabled but server did not respond with compatibility header (X-RPC-Safe-Enabled). ' +
125
+ 'This may indicate a version mismatch or non-toolkit server. ' +
126
+ 'Solutions: (1) Update server to rpc-express-toolkit v4+, (2) Disable client safeEnabled option, or (3) Use a compatible JSON-RPC server.'
127
+ );
128
+ }
129
+
130
+ // Warning: se client ha safe disabled ma server offre compatibilità
131
+ if (
132
+ !this.#options.safeEnabled &&
133
+ serverSafeHeader === 'true' &&
134
+ this.#options.warnOnUnsafe
135
+ ) {
136
+ console.warn(
137
+ '⚠️ RPC Compatibility Notice: Server supports safe serialization but client has safeEnabled=false. ' +
138
+ 'Consider enabling safeEnabled option for better BigInt/Date handling and forward compatibility.'
139
+ );
140
+ }
141
+
142
+ const serverSafeEnabled = serverSafeHeader === 'true';
139
143
 
140
144
  // Create deserialization options based on server's configuration
141
145
  const deserializationOptions = {
142
- safeStringEnabled: serverSafeStringEnabled,
143
- safeDateEnabled: serverSafeDateEnabled
146
+ safeEnabled: serverSafeEnabled,
144
147
  };
145
148
 
146
149
  // Convert back BigInts and Dates in the result using server's options
147
- return this.deserializeBigIntsAndDates(responseBody.result, deserializationOptions);
150
+ return this.deserializeBigIntsAndDates(
151
+ responseBody.result,
152
+ deserializationOptions
153
+ );
148
154
  } catch (error) {
149
155
  console.error('RPC call failed:', error);
150
156
  throw error;
@@ -164,9 +170,10 @@ class RpcClient {
164
170
 
165
171
  /**
166
172
  * Make multiple JSON-RPC calls in a single batch request.
173
+ * @template T
167
174
  * @param {Array<{method: string, params?: any, id?: string|number}>} requests Array of request objects.
168
175
  * @param {Object} [overrideHeaders={}] Optional headers to override defaults for this request.
169
- * @returns {Promise<Array<any>>} Array of results in the same order as requests.
176
+ * @returns {Promise<Array<T>>} Array of results in the same order as requests.
170
177
  */
171
178
  async batch(requests, overrideHeaders = {}) {
172
179
  const batchRequests = requests.map((req) => {
@@ -187,11 +194,11 @@ class RpcClient {
187
194
  method: 'POST',
188
195
  headers: {
189
196
  ...this.#defaultHeaders,
190
- // Add safe options headers so server knows what client expects
191
- 'X-RPC-SafeString-Enabled': this.#options.safeStringEnabled ? 'true' : 'false',
192
- 'X-RPC-SafeDate-Enabled': this.#options.safeDateEnabled ? 'true' : 'false',
197
+ // Add safe options header so server knows what client expects
198
+ 'X-RPC-Safe-Enabled': this.#options.safeEnabled ? 'true' : 'false',
193
199
  ...overrideHeaders,
194
200
  },
201
+ // Params are pre-serialized per item
195
202
  body: JSON.stringify(batchRequests),
196
203
  ...this.#fetchOptions, // Include any additional fetch options (e.g., SSL settings)
197
204
  });
@@ -204,14 +211,35 @@ class RpcClient {
204
211
 
205
212
  const responseBody = await response.json();
206
213
 
207
- // Check server's safe options from response headers
208
- const serverSafeStringEnabled = response.headers.get('X-RPC-SafeString-Enabled') === 'true';
209
- const serverSafeDateEnabled = response.headers.get('X-RPC-SafeDate-Enabled') === 'true';
214
+ // Check server's safe options from response headers - strict compatibility checking
215
+ const serverSafeHeader = response.headers.get('X-RPC-Safe-Enabled');
216
+
217
+ // Errore rigoroso: se client ha safe enabled ma server non risponde con header
218
+ if (this.#options.safeEnabled && serverSafeHeader === null) {
219
+ throw new Error(
220
+ 'RPC Compatibility Error: Client has safe serialization enabled but server did not respond with compatibility header (X-RPC-Safe-Enabled). ' +
221
+ 'This may indicate a version mismatch or non-toolkit server. ' +
222
+ 'Solutions: (1) Update server to rpc-express-toolkit v4+, (2) Disable client safeEnabled option, or (3) Use a compatible JSON-RPC server.'
223
+ );
224
+ }
225
+
226
+ // Warning: se client ha safe disabled ma server offre compatibilità
227
+ if (
228
+ !this.#options.safeEnabled &&
229
+ serverSafeHeader === 'true' &&
230
+ this.#options.warnOnUnsafe
231
+ ) {
232
+ console.warn(
233
+ '⚠️ RPC Compatibility Notice: Server supports safe serialization but client has safeEnabled=false. ' +
234
+ 'Consider enabling safeEnabled option for better BigInt/Date handling and forward compatibility.'
235
+ );
236
+ }
237
+
238
+ const serverSafeEnabled = serverSafeHeader === 'true';
210
239
 
211
240
  // Create deserialization options based on server's configuration
212
241
  const deserializationOptions = {
213
- safeStringEnabled: serverSafeStringEnabled,
214
- safeDateEnabled: serverSafeDateEnabled
242
+ safeEnabled: serverSafeEnabled,
215
243
  };
216
244
 
217
245
  // Handle batch response
@@ -220,15 +248,22 @@ class RpcClient {
220
248
  if (res.error) {
221
249
  throw res.error;
222
250
  }
223
- return this.deserializeBigIntsAndDates(res.result, deserializationOptions);
251
+ return this.deserializeBigIntsAndDates(
252
+ res.result,
253
+ deserializationOptions
254
+ );
224
255
  });
225
- } else {
226
- // Single response in batch
227
- if (responseBody.error) {
228
- throw responseBody.error;
229
- }
230
- return [this.deserializeBigIntsAndDates(responseBody.result, deserializationOptions)];
231
256
  }
257
+ // Single response in batch
258
+ if (responseBody.error) {
259
+ throw responseBody.error;
260
+ }
261
+ return [
262
+ this.deserializeBigIntsAndDates(
263
+ responseBody.result,
264
+ deserializationOptions
265
+ ),
266
+ ];
232
267
  } catch (error) {
233
268
  console.error('Batch RPC call failed:', error);
234
269
  throw error;
@@ -244,21 +279,40 @@ class RpcClient {
244
279
  serializeBigIntsAndDates(value) {
245
280
  if (typeof value === 'bigint') {
246
281
  // Convert BigInt to string with 'n' suffix for proper deserialization
247
- return value.toString() + 'n';
248
- } else if (value instanceof Date) {
249
- // Convert Date to ISO string with D: prefix if safeDateEnabled
282
+ return `${value.toString()}n`;
283
+ }
284
+ if (value instanceof Date) {
285
+ // Convert Date to ISO string with D: prefix if safeEnabled
250
286
  const isoString = value.toISOString();
251
- return this.#options.safeDateEnabled ? `D:${isoString}` : isoString;
252
- } else if (typeof value === 'string') {
253
- // Add S: prefix if safeStringEnabled is true
254
- if (this.#options.safeStringEnabled) {
255
- return 'S:' + value;
287
+ if (this.#options.safeEnabled) {
288
+ return `D:${isoString}`;
289
+ }
290
+ // Show educational warning if enabled
291
+ if (this.#options.warnOnUnsafe) {
292
+ console.warn(
293
+ '⚠️ Date serialization: Using plain ISO string format for JSON-RPC 2.0 compliance. Date objects will be deserialized as strings on the receiving end. Consider enabling safeEnabled or using string timestamps for better type safety.'
294
+ );
295
+ }
296
+ return isoString;
297
+ }
298
+ if (typeof value === 'string') {
299
+ // Add S: prefix if safeEnabled is true
300
+ if (this.#options.safeEnabled) {
301
+ return `S:${value}`;
302
+ }
303
+ // Show educational warning if enabled and string could be confused with BigInt
304
+ if (this.#options.warnOnUnsafe && /^-?\d+n?$/.test(value)) {
305
+ console.warn(
306
+ `⚠️ String serialization: String "${value}" could be confused with BigInt. Consider enabling safeEnabled for disambiguation or use explicit typing.`
307
+ );
256
308
  }
257
309
  return value;
258
- } else if (Array.isArray(value)) {
310
+ }
311
+ if (Array.isArray(value)) {
259
312
  // Recurse into arrays
260
313
  return value.map((v) => this.serializeBigIntsAndDates(v));
261
- } else if (value && typeof value === 'object') {
314
+ }
315
+ if (value && typeof value === 'object') {
262
316
  // Recurse into plain objects
263
317
  const result = {};
264
318
  for (const [key, val] of Object.entries(value)) {
@@ -277,14 +331,14 @@ class RpcClient {
277
331
  *
278
332
  * @param {any} value The value to deserialize.
279
333
  * @param {Object} [options] Custom deserialization options (uses client options if not provided).
280
- * @param {boolean} [options.safeStringEnabled] Whether to expect S: prefixed strings.
281
- * @param {boolean} [options.safeDateEnabled] Whether to expect D: prefixed dates.
334
+ * @param {boolean} [options.safeEnabled] Whether to expect safe prefixes for strings and dates.
282
335
  * @returns {any}
283
336
  */
284
337
  deserializeBigIntsAndDates(value, options = null) {
285
338
  // Use provided options or fall back to client options
286
- const safeStringEnabled = options ? options.safeStringEnabled : this.#options.safeStringEnabled;
287
- const safeDateEnabled = options ? options.safeDateEnabled : this.#options.safeDateEnabled;
339
+ const safeEnabled = options
340
+ ? options.safeEnabled
341
+ : this.#options.safeEnabled;
288
342
 
289
343
  // More comprehensive ISO date regex that handles:
290
344
  // - UTC: 2023-01-01T12:00:00.000Z
@@ -295,17 +349,17 @@ class RpcClient {
295
349
 
296
350
  // 1. Check if it's a string that might be a BigInt, Date, or safe string
297
351
  if (typeof value === 'string') {
298
- // Safe string check: if safeStringEnabled and starts with S:
299
- if (safeStringEnabled && value.startsWith('S:')) {
352
+ // Safe string check: if safeEnabled and starts with S:
353
+ if (safeEnabled && value.startsWith('S:')) {
300
354
  return value.substring(2); // Remove 'S:' prefix
301
355
  }
302
356
 
303
- // Safe date check: if safeDateEnabled and starts with D:
304
- if (safeDateEnabled && value.startsWith('D:')) {
357
+ // Safe date check: if safeEnabled and starts with D:
358
+ if (safeEnabled && value.startsWith('D:')) {
305
359
  const isoString = value.substring(2); // Remove 'D:' prefix
306
360
  const date = new Date(isoString);
307
361
  // Double-check that we got a valid date
308
- if (!isNaN(date.getTime())) {
362
+ if (!Number.isNaN(date.getTime())) {
309
363
  return date;
310
364
  }
311
365
  }
@@ -315,12 +369,12 @@ class RpcClient {
315
369
  if (/^-?\d+n$/.test(value)) {
316
370
  return BigInt(value.slice(0, -1)); // Remove 'n' and convert
317
371
  }
318
-
319
- // Date check: matches an ISO 8601 string (only if safeDateEnabled is false)
320
- if (!safeDateEnabled && ISO_DATE_REGEX.test(value)) {
372
+
373
+ // Date check: matches an ISO 8601 string (only if safeEnabled is false)
374
+ if (!safeEnabled && ISO_DATE_REGEX.test(value)) {
321
375
  const date = new Date(value);
322
376
  // Ensure it's valid
323
- if (!isNaN(date.getTime())) {
377
+ if (!Number.isNaN(date.getTime())) {
324
378
  return date;
325
379
  }
326
380
  }
@@ -347,4 +401,3 @@ class RpcClient {
347
401
  }
348
402
 
349
403
  module.exports = RpcClient;
350
- module.exports.RpcClient = RpcClient;
@@ -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(e){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{#e;#t;#n;#s;#a;constructor(e,t={},n={}){this.#e=e,this.#t={"Content-Type":"application/json",...t},this.#n=1e3*Date.now()+Math.floor(1e3*Math.random()),this.#a={safeStringEnabled:!1!==n.safeStringEnabled,safeDateEnabled:!1!==n.safeDateEnabled,warnOnUnsafeString:!1!==n.warnOnUnsafeString,warnOnUnsafeDate:!1!==n.warnOnUnsafeDate,...n},this.#s={}}#r(){return++this.#n}async call(e,t,n=void 0,s={}){const a={jsonrpc:"2.0",method:e,id:void 0===n?this.#r():n};null!=t&&(a.params=this.serializeBigIntsAndDates(t));try{const e=await fetchFn(this.#e,{method:"POST",headers:{...this.#t,"X-RPC-SafeString-Enabled":this.#a.safeStringEnabled?"true":"false","X-RPC-SafeDate-Enabled":this.#a.safeDateEnabled?"true":"false",...s},body:JSON.stringify(a),...this.#s});if(!e.ok)throw new Error(`HTTP Error: ${e.status} ${e.statusText}`);const t=await e.json();if(t.error)throw t.error;const n="true"===e.headers.get("X-RPC-SafeString-Enabled"),r={safeStringEnabled:n,safeDateEnabled:"true"===e.headers.get("X-RPC-SafeDate-Enabled")};return this.deserializeBigIntsAndDates(t.result,r)}catch(e){throw console.error("RPC call failed:",e),e}}async notify(e,t={},n={}){await this.call(e,t,null,n)}async batch(e,t={}){const n=e.map(e=>{const t={jsonrpc:"2.0",method:e.method,id:void 0!==e.id?e.id:this.#r()};return void 0!==e.params&&null!==e.params&&(t.params=this.serializeBigIntsAndDates(e.params)),t});try{const e=await fetchFn(this.#e,{method:"POST",headers:{...this.#t,"X-RPC-SafeString-Enabled":this.#a.safeStringEnabled?"true":"false","X-RPC-SafeDate-Enabled":this.#a.safeDateEnabled?"true":"false",...t},body:JSON.stringify(n),...this.#s});if(!e.ok)throw new Error(`HTTP Error: ${e.status} ${e.statusText}`);const s=await e.json(),a="true"===e.headers.get("X-RPC-SafeString-Enabled"),r={safeStringEnabled:a,safeDateEnabled:"true"===e.headers.get("X-RPC-SafeDate-Enabled")};if(Array.isArray(s))return s.map(e=>{if(e.error)throw e.error;return this.deserializeBigIntsAndDates(e.result,r)});if(s.error)throw s.error;return[this.deserializeBigIntsAndDates(s.result,r)]}catch(e){throw console.error("Batch RPC call failed:",e),e}}serializeBigIntsAndDates(e){if("bigint"==typeof e)return e.toString()+"n";if(e instanceof Date){const t=e.toISOString();return this.#a.safeDateEnabled?`D:${t}`:t}if("string"==typeof e)return this.#a.safeStringEnabled?"S:"+e:e;if(Array.isArray(e))return e.map(e=>this.serializeBigIntsAndDates(e));if(e&&"object"==typeof e){const t={};for(const[n,s]of Object.entries(e))t[n]=this.serializeBigIntsAndDates(s);return t}return e}deserializeBigIntsAndDates(e,t=null){const n=t?t.safeStringEnabled:this.#a.safeStringEnabled,s=t?t.safeDateEnabled:this.#a.safeDateEnabled,a=/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d+)?(Z|[+-]\d{2}:\d{2})?$/;if("string"==typeof e){if(n&&e.startsWith("S:"))return e.substring(2);if(s&&e.startsWith("D:")){const t=e.substring(2),n=new Date(t);if(!isNaN(n.getTime()))return n}if(/^-?\d+n$/.test(e))return BigInt(e.slice(0,-1));if(!s&&a.test(e)){const t=new Date(e);if(!isNaN(t.getTime()))return t}}return Array.isArray(e)?e.map(e=>this.deserializeBigIntsAndDates(e,t)):e&&"object"==typeof e?Object.fromEntries(Object.entries(e).map(([e,n])=>[e,this.deserializeBigIntsAndDates(n,t)])):e}}module.exports=RpcClient,module.exports.RpcClient=RpcClient;
1
+ let fetchFn;if("undefined"==typeof globalThis||!globalThis.fetch)throw new Error("globalThis.fetch is not available. Node.js 18+ is required.");fetchFn=globalThis.fetch.bind(globalThis);class RpcClient{#e;#t;#i;#s;#n;constructor(e,t={},i={}){this.#e=e,this.#t={"Content-Type":"application/json",...t},this.#i=1e3*Date.now()+Math.floor(1e3*Math.random()),this.#n={safeEnabled:!0===i.safeEnabled,warnOnUnsafe:!1!==i.warnOnUnsafe,...i},this.#s={}}#a(){return++this.#i}async call(e,t,i=void 0,s={}){const n={jsonrpc:"2.0",method:e,id:void 0===i?this.#a():i};null!=t&&(n.params=this.serializeBigIntsAndDates(t));try{const e=await fetchFn(this.#e,{method:"POST",headers:{...this.#t,"X-RPC-Safe-Enabled":this.#n.safeEnabled?"true":"false",...s},body:JSON.stringify(n),...this.#s});if(!e.ok)throw new Error(`HTTP Error: ${e.status} ${e.statusText}`);const t=await e.json();if(t.error)throw t.error;const i=e.headers.get("X-RPC-Safe-Enabled");if(this.#n.safeEnabled&&null===i)throw new Error("RPC Compatibility Error: Client has safe serialization enabled but server did not respond with compatibility header (X-RPC-Safe-Enabled). This may indicate a version mismatch or non-toolkit server. Solutions: (1) Update server to rpc-express-toolkit v4+, (2) Disable client safeEnabled option, or (3) Use a compatible JSON-RPC server.");!this.#n.safeEnabled&&"true"===i&&this.#n.warnOnUnsafe&&console.warn("⚠️ RPC Compatibility Notice: Server supports safe serialization but client has safeEnabled=false. Consider enabling safeEnabled option for better BigInt/Date handling and forward compatibility.");const a={safeEnabled:"true"===i};return this.deserializeBigIntsAndDates(t.result,a)}catch(e){throw console.error("RPC call failed:",e),e}}async notify(e,t={},i={}){await this.call(e,t,null,i)}async batch(e,t={}){const i=e.map(e=>{const t={jsonrpc:"2.0",method:e.method,id:void 0!==e.id?e.id:this.#a()};return void 0!==e.params&&null!==e.params&&(t.params=this.serializeBigIntsAndDates(e.params)),t});try{const e=await fetchFn(this.#e,{method:"POST",headers:{...this.#t,"X-RPC-Safe-Enabled":this.#n.safeEnabled?"true":"false",...t},body:JSON.stringify(i),...this.#s});if(!e.ok)throw new Error(`HTTP Error: ${e.status} ${e.statusText}`);const s=await e.json(),n=e.headers.get("X-RPC-Safe-Enabled");if(this.#n.safeEnabled&&null===n)throw new Error("RPC Compatibility Error: Client has safe serialization enabled but server did not respond with compatibility header (X-RPC-Safe-Enabled). This may indicate a version mismatch or non-toolkit server. Solutions: (1) Update server to rpc-express-toolkit v4+, (2) Disable client safeEnabled option, or (3) Use a compatible JSON-RPC server.");!this.#n.safeEnabled&&"true"===n&&this.#n.warnOnUnsafe&&console.warn("⚠️ RPC Compatibility Notice: Server supports safe serialization but client has safeEnabled=false. Consider enabling safeEnabled option for better BigInt/Date handling and forward compatibility.");const a={safeEnabled:"true"===n};if(Array.isArray(s))return s.map(e=>{if(e.error)throw e.error;return this.deserializeBigIntsAndDates(e.result,a)});if(s.error)throw s.error;return[this.deserializeBigIntsAndDates(s.result,a)]}catch(e){throw console.error("Batch RPC call failed:",e),e}}serializeBigIntsAndDates(e){if("bigint"==typeof e)return`${e.toString()}n`;if(e instanceof Date){const t=e.toISOString();return this.#n.safeEnabled?`D:${t}`:(this.#n.warnOnUnsafe&&console.warn("⚠️ Date serialization: Using plain ISO string format for JSON-RPC 2.0 compliance. Date objects will be deserialized as strings on the receiving end. Consider enabling safeEnabled or using string timestamps for better type safety."),t)}if("string"==typeof e)return this.#n.safeEnabled?`S:${e}`:(this.#n.warnOnUnsafe&&/^-?\d+n?$/.test(e)&&console.warn(`⚠️ String serialization: String "${e}" could be confused with BigInt. Consider enabling safeEnabled for disambiguation or use explicit typing.`),e);if(Array.isArray(e))return e.map(e=>this.serializeBigIntsAndDates(e));if(e&&"object"==typeof e){const t={};for(const[i,s]of Object.entries(e))t[i]=this.serializeBigIntsAndDates(s);return t}return e}deserializeBigIntsAndDates(e,t=null){const i=t?t.safeEnabled:this.#n.safeEnabled,s=/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d+)?(Z|[+-]\d{2}:\d{2})?$/;if("string"==typeof e){if(i&&e.startsWith("S:"))return e.substring(2);if(i&&e.startsWith("D:")){const t=e.substring(2),i=new Date(t);if(!Number.isNaN(i.getTime()))return i}if(/^-?\d+n$/.test(e))return BigInt(e.slice(0,-1));if(!i&&s.test(e)){const t=new Date(e);if(!Number.isNaN(t.getTime()))return t}}return Array.isArray(e)?e.map(e=>this.deserializeBigIntsAndDates(e,t)):e&&"object"==typeof e?Object.fromEntries(Object.entries(e).map(([e,i])=>[e,this.deserializeBigIntsAndDates(i,t)])):e}}module.exports=RpcClient;
@@ -1 +1 @@
1
- let e;if("undefined"==typeof BigInt||BigInt.prototype.toJSON||(BigInt.prototype.toJSON=function(){return this.toString()}),"undefined"!=typeof fetch)e=fetch;else try{e=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+");e=globalThis.fetch}class t{#e;#t;#s;#r;#a;constructor(e,t={},s={}){this.#e=e,this.#t={"Content-Type":"application/json",...t},this.#s=1e3*Date.now()+Math.floor(1e3*Math.random()),this.#a={safeStringEnabled:!1!==s.safeStringEnabled,safeDateEnabled:!1!==s.safeDateEnabled,...s},this.#r={}}#i(){return++this.#s}async call(t,s,r=void 0,a={}){const i={jsonrpc:"2.0",method:t,id:void 0===r?this.#i():r};null!=s&&(i.params=this.serializeBigIntsAndDates(s));try{const t=await e(this.#e,{method:"POST",headers:{...this.#t,"X-RPC-SafeString-Enabled":this.#a.safeStringEnabled?"true":"false","X-RPC-SafeDate-Enabled":this.#a.safeDateEnabled?"true":"false",...a},body:JSON.stringify(i),...this.#r});if(!t.ok)throw new Error(`HTTP Error: ${t.status} ${t.statusText}`);const s=await t.json();if(s.error)throw s.error;const r="true"===t.headers.get("X-RPC-SafeString-Enabled"),n={safeStringEnabled:r,safeDateEnabled:"true"===t.headers.get("X-RPC-SafeDate-Enabled")};return this.deserializeBigIntsAndDates(s.result,n)}catch(e){throw console.error("RPC call failed:",e),e}}async notify(e,t={},s={}){await this.call(e,t,null,s)}async batch(t,s={}){const r=t.map(e=>{const t={jsonrpc:"2.0",method:e.method,id:void 0!==e.id?e.id:this.#i()};return void 0!==e.params&&null!==e.params&&(t.params=this.serializeBigIntsAndDates(e.params)),t});try{const t=await e(this.#e,{method:"POST",headers:{...this.#t,"X-RPC-SafeString-Enabled":this.#a.safeStringEnabled?"true":"false","X-RPC-SafeDate-Enabled":this.#a.safeDateEnabled?"true":"false",...s},body:JSON.stringify(r),...this.#r});if(!t.ok)throw new Error(`HTTP Error: ${t.status} ${t.statusText}`);const a=await t.json(),i="true"===t.headers.get("X-RPC-SafeString-Enabled"),n={safeStringEnabled:i,safeDateEnabled:"true"===t.headers.get("X-RPC-SafeDate-Enabled")};if(Array.isArray(a))return a.map(e=>{if(e.error)throw e.error;return this.deserializeBigIntsAndDates(e.result,n)});if(a.error)throw a.error;return[this.deserializeBigIntsAndDates(a.result,n)]}catch(e){throw console.error("Batch RPC call failed:",e),e}}serializeBigIntsAndDates(e){if("bigint"==typeof e)return e.toString()+"n";if(e instanceof Date){const t=e.toISOString();return this.#a.safeDateEnabled?`D:${t}`:t}if("string"==typeof e)return this.#a.safeStringEnabled?"S:"+e:e;if(Array.isArray(e))return e.map(e=>this.serializeBigIntsAndDates(e));if(e&&"object"==typeof e){const t={};for(const[s,r]of Object.entries(e))t[s]=this.serializeBigIntsAndDates(r);return t}return e}deserializeBigIntsAndDates(e,t=null){const s=t?t.safeStringEnabled:this.#a.safeStringEnabled,r=t?t.safeDateEnabled:this.#a.safeDateEnabled,a=/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d+)?(Z|[+-]\d{2}:\d{2})?$/;if("string"==typeof e){if(s&&e.startsWith("S:"))return e.substring(2);if(r&&e.startsWith("D:")){const t=e.substring(2),s=new Date(t);if(!isNaN(s.getTime()))return s}if(/^-?\d+n$/.test(e))return BigInt(e.slice(0,-1));if(!r&&a.test(e)){const t=new Date(e);if(!isNaN(t.getTime()))return t}}return Array.isArray(e)?e.map(e=>this.deserializeBigIntsAndDates(e,t)):e&&"object"==typeof e?Object.fromEntries(Object.entries(e).map(([e,s])=>[e,this.deserializeBigIntsAndDates(s,t)])):e}}export default t;export{t as RpcClient};
1
+ let e;if("undefined"==typeof BigInt||BigInt.prototype.toJSON||(BigInt.prototype.toJSON=function(){return this.toString()}),"undefined"!=typeof fetch)e=fetch;else try{e=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+");e=globalThis.fetch}class t{#e;#t;#i;#s;#n;constructor(e,t={},i={}){this.#e=e,this.#t={"Content-Type":"application/json",...t},this.#i=1e3*Date.now()+Math.floor(1e3*Math.random()),this.#n={safeEnabled:!0===i.safeEnabled,warnOnUnsafe:!1!==i.warnOnUnsafe,...i},this.#s={}}#r(){return++this.#i}async call(t,i,s=void 0,n={}){const r={jsonrpc:"2.0",method:t,id:void 0===s?this.#r():s};null!=i&&(r.params=this.serializeBigIntsAndDates(i));try{const t=await e(this.#e,{method:"POST",headers:{...this.#t,"X-RPC-Safe-Enabled":this.#n.safeEnabled?"true":"false",...n},body:JSON.stringify(r),...this.#s});if(!t.ok)throw new Error(`HTTP Error: ${t.status} ${t.statusText}`);const i=await t.json();if(i.error)throw i.error;const s=t.headers.get("X-RPC-Safe-Enabled");if(this.#n.safeEnabled&&null===s)throw new Error("RPC Compatibility Error: Client has safe serialization enabled but server did not respond with compatibility header (X-RPC-Safe-Enabled). This may indicate a version mismatch or non-toolkit server. Solutions: (1) Update server to rpc-express-toolkit v4+, (2) Disable client safeEnabled option, or (3) Use a compatible JSON-RPC server.");!this.#n.safeEnabled&&"true"===s&&this.#n.warnOnUnsafe&&console.warn("⚠️ RPC Compatibility Notice: Server supports safe serialization but client has safeEnabled=false. Consider enabling safeEnabled option for better BigInt/Date handling and forward compatibility.");const a={safeEnabled:"true"===s};return this.deserializeBigIntsAndDates(i.result,a)}catch(e){throw console.error("RPC call failed:",e),e}}async notify(e,t={},i={}){await this.call(e,t,null,i)}async batch(t,i={}){const s=t.map(e=>{const t={jsonrpc:"2.0",method:e.method,id:void 0!==e.id?e.id:this.#r()};return void 0!==e.params&&null!==e.params&&(t.params=this.serializeBigIntsAndDates(e.params)),t});try{const t=await e(this.#e,{method:"POST",headers:{...this.#t,"X-RPC-Safe-Enabled":this.#n.safeEnabled?"true":"false",...i},body:JSON.stringify(s),...this.#s});if(!t.ok)throw new Error(`HTTP Error: ${t.status} ${t.statusText}`);const n=await t.json(),r=t.headers.get("X-RPC-Safe-Enabled");if(this.#n.safeEnabled&&null===r)throw new Error("RPC Compatibility Error: Client has safe serialization enabled but server did not respond with compatibility header (X-RPC-Safe-Enabled). This may indicate a version mismatch or non-toolkit server. Solutions: (1) Update server to rpc-express-toolkit v4+, (2) Disable client safeEnabled option, or (3) Use a compatible JSON-RPC server.");!this.#n.safeEnabled&&"true"===r&&this.#n.warnOnUnsafe&&console.warn("⚠️ RPC Compatibility Notice: Server supports safe serialization but client has safeEnabled=false. Consider enabling safeEnabled option for better BigInt/Date handling and forward compatibility.");const a={safeEnabled:"true"===r};if(Array.isArray(n))return n.map(e=>{if(e.error)throw e.error;return this.deserializeBigIntsAndDates(e.result,a)});if(n.error)throw n.error;return[this.deserializeBigIntsAndDates(n.result,a)]}catch(e){throw console.error("Batch RPC call failed:",e),e}}serializeBigIntsAndDates(e){if("bigint"==typeof e)return e.toString()+"n";if(e instanceof Date){const t=e.toISOString();return this.#n.safeEnabled?`D:${t}`:(this.#n.warnOnUnsafe&&console.warn("⚠️ Date serialization: Using plain ISO string format for JSON-RPC 2.0 compliance. Date objects will be deserialized as strings on the receiving end. Consider enabling safeEnabled or using string timestamps for better type safety."),t)}if("string"==typeof e)return this.#n.safeEnabled?"S:"+e:(this.#n.warnOnUnsafe&&/^-?\d+n?$/.test(e)&&console.warn(`⚠️ String serialization: String "${e}" could be confused with BigInt. Consider enabling safeEnabled for disambiguation or use explicit typing.`),e);if(Array.isArray(e))return e.map(e=>this.serializeBigIntsAndDates(e));if(e&&"object"==typeof e){const t={};for(const[i,s]of Object.entries(e))t[i]=this.serializeBigIntsAndDates(s);return t}return e}deserializeBigIntsAndDates(e,t=null){const i=t?t.safeEnabled:this.#n.safeEnabled,s=/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d+)?(Z|[+-]\d{2}:\d{2})?$/;if("string"==typeof e){if(i&&e.startsWith("S:"))return e.substring(2);if(i&&e.startsWith("D:")){const t=e.substring(2),i=new Date(t);if(!isNaN(i.getTime()))return i}if(/^-?\d+n$/.test(e))return BigInt(e.slice(0,-1));if(!i&&s.test(e)){const t=new Date(e);if(!isNaN(t.getTime()))return t}}return Array.isArray(e)?e.map(e=>this.deserializeBigIntsAndDates(e,t)):e&&"object"==typeof e?Object.fromEntries(Object.entries(e).map(([e,i])=>[e,this.deserializeBigIntsAndDates(i,t)])):e}}export default t;export{t as RpcClient};
@@ -45,8 +45,8 @@ class RpcClient {
45
45
  * @param {Object} [defaultHeaders={}] Optional default headers to include in requests.
46
46
  * @param {Object} [options={}] Optional configuration options.
47
47
  * @param {boolean} [options.rejectUnauthorized=true] Whether to reject unauthorized SSL certificates. Set to false for development with self-signed certificates.
48
- * @param {boolean} [options.safeStringEnabled=true] Whether to prefix strings with 'S:' to avoid confusion with BigInt values.
49
- * @param {boolean} [options.safeDateEnabled=true] Whether to prefix dates with 'D:' to avoid confusion with ISO string values.
48
+ * @param {boolean} [options.safeEnabled=false] Whether to use safe serialization for both strings and dates to avoid confusion with BigInt/ISO values. Default false for JSON-RPC 2.0 compliance.
49
+ * @param {boolean} [options.warnOnUnsafe=true] Whether to show educational warnings when BigInt/Date objects are serialized without safe mode. Default true.
50
50
  */
51
51
  constructor(endpoint, defaultHeaders = {}, options = {}) {
52
52
  this.#endpoint = endpoint;
@@ -59,8 +59,8 @@ class RpcClient {
59
59
 
60
60
  // Store options
61
61
  this.#options = {
62
- safeStringEnabled: options.safeStringEnabled !== false, // Default true
63
- safeDateEnabled: options.safeDateEnabled !== false, // Default true
62
+ safeEnabled: options.safeEnabled === true, // Default false
63
+ warnOnUnsafe: options.warnOnUnsafe !== false, // Default true
64
64
  ...options
65
65
  };
66
66
 
@@ -82,11 +82,12 @@ class RpcClient {
82
82
 
83
83
  /**
84
84
  * Make a JSON-RPC call to the server.
85
+ * @template T
85
86
  * @param {string} method The RPC method name.
86
87
  * @param {any} params The parameters to pass to the RPC method.
87
88
  * @param {string|number|null} [id] The request ID (auto-generated if not provided).
88
89
  * @param {Object} [overrideHeaders={}] Optional headers to override defaults for this request.
89
- * @returns {Promise<any>} The result of the RPC call.
90
+ * @returns {Promise<T>} The result of the RPC call.
90
91
  */
91
92
  async call(method, params, id = undefined, overrideHeaders = {}) {
92
93
  // Auto-generate ID if not provided (null means notification)
@@ -108,9 +109,8 @@ class RpcClient {
108
109
  method: 'POST',
109
110
  headers: {
110
111
  ...this.#defaultHeaders,
111
- // Add safe options headers so server knows what client expects
112
- 'X-RPC-SafeString-Enabled': this.#options.safeStringEnabled ? 'true' : 'false',
113
- 'X-RPC-SafeDate-Enabled': this.#options.safeDateEnabled ? 'true' : 'false',
112
+ // Add safe options header so server knows what client expects
113
+ 'X-RPC-Safe-Enabled': this.#options.safeEnabled ? 'true' : 'false',
114
114
  ...overrideHeaders,
115
115
  },
116
116
  // Since we have BigInt.prototype.toJSON, we don't need manual serialization
@@ -131,14 +131,31 @@ class RpcClient {
131
131
  throw responseBody.error;
132
132
  }
133
133
 
134
- // Check server's safe options from response headers
135
- const serverSafeStringEnabled = response.headers.get('X-RPC-SafeString-Enabled') === 'true';
136
- const serverSafeDateEnabled = response.headers.get('X-RPC-SafeDate-Enabled') === 'true';
134
+ // Check server's safe options from response headers - strict compatibility checking
135
+ const serverSafeHeader = response.headers.get('X-RPC-Safe-Enabled');
136
+
137
+ // Errore rigoroso: se client ha safe enabled ma server non risponde con header
138
+ if (this.#options.safeEnabled && serverSafeHeader === null) {
139
+ throw new Error(
140
+ 'RPC Compatibility Error: Client has safe serialization enabled but server did not respond with compatibility header (X-RPC-Safe-Enabled). ' +
141
+ 'This may indicate a version mismatch or non-toolkit server. ' +
142
+ 'Solutions: (1) Update server to rpc-express-toolkit v4+, (2) Disable client safeEnabled option, or (3) Use a compatible JSON-RPC server.'
143
+ );
144
+ }
145
+
146
+ // Warning: se client ha safe disabled ma server offre compatibilità
147
+ if (!this.#options.safeEnabled && serverSafeHeader === 'true' && this.#options.warnOnUnsafe) {
148
+ console.warn(
149
+ '⚠️ RPC Compatibility Notice: Server supports safe serialization but client has safeEnabled=false. ' +
150
+ 'Consider enabling safeEnabled option for better BigInt/Date handling and forward compatibility.'
151
+ );
152
+ }
153
+
154
+ const serverSafeEnabled = serverSafeHeader === 'true';
137
155
 
138
156
  // Create deserialization options based on server's configuration
139
157
  const deserializationOptions = {
140
- safeStringEnabled: serverSafeStringEnabled,
141
- safeDateEnabled: serverSafeDateEnabled
158
+ safeEnabled: serverSafeEnabled
142
159
  };
143
160
 
144
161
  // Convert back BigInts and Dates in the result using server's options
@@ -162,9 +179,10 @@ class RpcClient {
162
179
 
163
180
  /**
164
181
  * Make multiple JSON-RPC calls in a single batch request.
182
+ * @template T
165
183
  * @param {Array<{method: string, params?: any, id?: string|number}>} requests Array of request objects.
166
184
  * @param {Object} [overrideHeaders={}] Optional headers to override defaults for this request.
167
- * @returns {Promise<Array<any>>} Array of results in the same order as requests.
185
+ * @returns {Promise<Array<T>>} Array of results in the same order as requests.
168
186
  */
169
187
  async batch(requests, overrideHeaders = {}) {
170
188
  const batchRequests = requests.map((req) => {
@@ -185,9 +203,8 @@ class RpcClient {
185
203
  method: 'POST',
186
204
  headers: {
187
205
  ...this.#defaultHeaders,
188
- // Add safe options headers so server knows what client expects
189
- 'X-RPC-SafeString-Enabled': this.#options.safeStringEnabled ? 'true' : 'false',
190
- 'X-RPC-SafeDate-Enabled': this.#options.safeDateEnabled ? 'true' : 'false',
206
+ // Add safe options header so server knows what client expects
207
+ 'X-RPC-Safe-Enabled': this.#options.safeEnabled ? 'true' : 'false',
191
208
  ...overrideHeaders,
192
209
  },
193
210
  body: JSON.stringify(batchRequests),
@@ -202,14 +219,31 @@ class RpcClient {
202
219
 
203
220
  const responseBody = await response.json();
204
221
 
205
- // Check server's safe options from response headers
206
- const serverSafeStringEnabled = response.headers.get('X-RPC-SafeString-Enabled') === 'true';
207
- const serverSafeDateEnabled = response.headers.get('X-RPC-SafeDate-Enabled') === 'true';
222
+ // Check server's safe options from response headers - strict compatibility checking
223
+ const serverSafeHeader = response.headers.get('X-RPC-Safe-Enabled');
224
+
225
+ // Errore rigoroso: se client ha safe enabled ma server non risponde con header
226
+ if (this.#options.safeEnabled && serverSafeHeader === null) {
227
+ throw new Error(
228
+ 'RPC Compatibility Error: Client has safe serialization enabled but server did not respond with compatibility header (X-RPC-Safe-Enabled). ' +
229
+ 'This may indicate a version mismatch or non-toolkit server. ' +
230
+ 'Solutions: (1) Update server to rpc-express-toolkit v4+, (2) Disable client safeEnabled option, or (3) Use a compatible JSON-RPC server.'
231
+ );
232
+ }
233
+
234
+ // Warning: se client ha safe disabled ma server offre compatibilità
235
+ if (!this.#options.safeEnabled && serverSafeHeader === 'true' && this.#options.warnOnUnsafe) {
236
+ console.warn(
237
+ '⚠️ RPC Compatibility Notice: Server supports safe serialization but client has safeEnabled=false. ' +
238
+ 'Consider enabling safeEnabled option for better BigInt/Date handling and forward compatibility.'
239
+ );
240
+ }
241
+
242
+ const serverSafeEnabled = serverSafeHeader === 'true';
208
243
 
209
244
  // Create deserialization options based on server's configuration
210
245
  const deserializationOptions = {
211
- safeStringEnabled: serverSafeStringEnabled,
212
- safeDateEnabled: serverSafeDateEnabled
246
+ safeEnabled: serverSafeEnabled
213
247
  };
214
248
 
215
249
  // Handle batch response
@@ -244,15 +278,28 @@ class RpcClient {
244
278
  // Convert BigInt to string with 'n' suffix for proper deserialization
245
279
  return value.toString() + 'n';
246
280
  } else if (value instanceof Date) {
247
- // Convert Date to ISO string with D: prefix if safeDateEnabled
281
+ // Convert Date to ISO string with D: prefix if safeEnabled
248
282
  const isoString = value.toISOString();
249
- return this.#options.safeDateEnabled ? `D:${isoString}` : isoString;
283
+ if (this.#options.safeEnabled) {
284
+ return `D:${isoString}`;
285
+ } else {
286
+ // Show educational warning if enabled
287
+ if (this.#options.warnOnUnsafe) {
288
+ console.warn('⚠️ Date serialization: Using plain ISO string format for JSON-RPC 2.0 compliance. Date objects will be deserialized as strings on the receiving end. Consider enabling safeEnabled or using string timestamps for better type safety.');
289
+ }
290
+ return isoString;
291
+ }
250
292
  } else if (typeof value === 'string') {
251
- // Add S: prefix if safeStringEnabled is true
252
- if (this.#options.safeStringEnabled) {
293
+ // Add S: prefix if safeEnabled is true
294
+ if (this.#options.safeEnabled) {
253
295
  return 'S:' + value;
296
+ } else {
297
+ // Show educational warning if enabled and string could be confused with BigInt
298
+ if (this.#options.warnOnUnsafe && /^-?\d+n?$/.test(value)) {
299
+ console.warn(`⚠️ String serialization: String "${value}" could be confused with BigInt. Consider enabling safeEnabled for disambiguation or use explicit typing.`);
300
+ }
301
+ return value;
254
302
  }
255
- return value;
256
303
  } else if (Array.isArray(value)) {
257
304
  // Recurse into arrays
258
305
  return value.map((v) => this.serializeBigIntsAndDates(v));
@@ -275,14 +322,12 @@ class RpcClient {
275
322
  *
276
323
  * @param {any} value The value to deserialize.
277
324
  * @param {Object} [options] Custom deserialization options (uses client options if not provided).
278
- * @param {boolean} [options.safeStringEnabled] Whether to expect S: prefixed strings.
279
- * @param {boolean} [options.safeDateEnabled] Whether to expect D: prefixed dates.
325
+ * @param {boolean} [options.safeEnabled] Whether to expect safe prefixes for strings and dates.
280
326
  * @returns {any}
281
327
  */
282
328
  deserializeBigIntsAndDates(value, options = null) {
283
329
  // Use provided options or fall back to client options
284
- const safeStringEnabled = options ? options.safeStringEnabled : this.#options.safeStringEnabled;
285
- const safeDateEnabled = options ? options.safeDateEnabled : this.#options.safeDateEnabled;
330
+ const safeEnabled = options ? options.safeEnabled : this.#options.safeEnabled;
286
331
 
287
332
  // More comprehensive ISO date regex that handles:
288
333
  // - UTC: 2023-01-01T12:00:00.000Z
@@ -293,13 +338,13 @@ class RpcClient {
293
338
 
294
339
  // 1. Check if it's a string that might be a BigInt, Date, or safe string
295
340
  if (typeof value === 'string') {
296
- // Safe string check: if safeStringEnabled and starts with S:
297
- if (safeStringEnabled && value.startsWith('S:')) {
341
+ // Safe string check: if safeEnabled and starts with S:
342
+ if (safeEnabled && value.startsWith('S:')) {
298
343
  return value.substring(2); // Remove 'S:' prefix
299
344
  }
300
345
 
301
- // Safe date check: if safeDateEnabled and starts with D:
302
- if (safeDateEnabled && value.startsWith('D:')) {
346
+ // Safe date check: if safeEnabled and starts with D:
347
+ if (safeEnabled && value.startsWith('D:')) {
303
348
  const isoString = value.substring(2); // Remove 'D:' prefix
304
349
  const date = new Date(isoString);
305
350
  // Double-check that we got a valid date
@@ -314,8 +359,8 @@ class RpcClient {
314
359
  return BigInt(value.slice(0, -1)); // Remove 'n' and convert
315
360
  }
316
361
 
317
- // Date check: matches an ISO 8601 string (only if safeDateEnabled is false)
318
- if (!safeDateEnabled && ISO_DATE_REGEX.test(value)) {
362
+ // Date check: matches an ISO 8601 string (only if safeEnabled is false)
363
+ if (!safeEnabled && ISO_DATE_REGEX.test(value)) {
319
364
  const date = new Date(value);
320
365
  // Ensure it's valid
321
366
  if (!isNaN(date.getTime())) {