rpc-express-toolkit 4.1.0 → 4.2.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.
package/src/safe.d.ts CHANGED
@@ -2,6 +2,38 @@ import type { Router } from 'express';
2
2
  import type { RpcEndpoint, RpcClient, RpcEndpointOptions, RpcClientOptions } from './index';
3
3
 
4
4
  /**
5
+ * RPC Client with safe serialization enabled by default.
6
+ * Extends RpcClient with safeEnabled: true preset.
7
+ */
8
+ export class RpcSafeClient extends RpcClient {
9
+ constructor(
10
+ endpoint: string,
11
+ defaultHeaders?: Record<string, string>,
12
+ options?: RpcClientOptions
13
+ );
14
+ }
15
+
16
+ /**
17
+ * RPC Endpoint with safe serialization enabled by default.
18
+ * Extends RpcEndpoint with safeEnabled: true and strictMode: true presets.
19
+ */
20
+ export class RpcSafeEndpoint<C = any> extends RpcEndpoint<C> {
21
+ constructor(
22
+ router: Router,
23
+ context: C,
24
+ endpoint?: string,
25
+ options?: RpcEndpointOptions
26
+ );
27
+
28
+ constructor(
29
+ router: Router,
30
+ context: C,
31
+ options?: RpcEndpointOptions
32
+ );
33
+ }
34
+
35
+ /**
36
+ * @deprecated Use `new RpcSafeEndpoint(...)` instead.
5
37
  * Factory: create a RpcEndpoint with safe defaults (safeEnabled=true, strictMode=true).
6
38
  */
7
39
  export function createSafeEndpoint<C = any>(
@@ -18,6 +50,7 @@ export function createSafeEndpoint<C = any>(
18
50
  ): RpcEndpoint<C>;
19
51
 
20
52
  /**
53
+ * @deprecated Use `new RpcSafeClient(...)` instead.
21
54
  * Factory: create a RpcClient with safe defaults (safeEnabled=true).
22
55
  */
23
56
  export function createSafeClient(
package/src/safe.js CHANGED
@@ -1,14 +1,72 @@
1
+ /* eslint-disable max-classes-per-file */
1
2
  const Main = require('./index');
2
3
 
3
- // Default safe options applied unless explicitly overridden
4
- const defaultSafeOptions = {
5
- safeEnabled: true,
6
- strictMode: true,
7
- };
4
+ /**
5
+ * RPC Client with safe serialization enabled by default.
6
+ * Extends RpcClient with safeEnabled: true preset.
7
+ *
8
+ * This class automatically enables safe prefixes for strings (S:) and dates (D:)
9
+ * to avoid ambiguity with BigInt and ISO date strings.
10
+ *
11
+ * @example
12
+ * const client = new RpcSafeClient('http://localhost:3000/api');
13
+ * const result = await client.call('add', { a: 10n, b: 20n });
14
+ */
15
+ class RpcSafeClient extends Main.RpcClient {
16
+ /**
17
+ * @param {string} endpoint The JSON-RPC endpoint URL.
18
+ * @param {Object} [defaultHeaders={}] Optional default headers to include in requests.
19
+ * @param {Object} [options={}] Optional configuration options (safeEnabled will be set to true).
20
+ */
21
+ constructor(endpoint, defaultHeaders = {}, options = {}) {
22
+ super(endpoint, defaultHeaders, { safeEnabled: true, ...options });
23
+ }
24
+ }
25
+
26
+ /**
27
+ * RPC Endpoint with safe serialization enabled by default.
28
+ * Extends RpcEndpoint with safeEnabled: true and strictMode: true presets.
29
+ *
30
+ * This class automatically enables safe prefixes for strings (S:) and dates (D:)
31
+ * and enforces strict compatibility checking with clients.
32
+ *
33
+ * @example
34
+ * const app = express();
35
+ * const rpc = new RpcSafeEndpoint(app, {});
36
+ * rpc.addMethod('add', {
37
+ * handler: (req, ctx, params) => params.a + params.b,
38
+ * });
39
+ *
40
+ * @template C
41
+ */
42
+ class RpcSafeEndpoint extends Main.RpcEndpoint {
43
+ /**
44
+ * @param {import('express').Router} router Express router or app
45
+ * @param {C} context Context object passed to handlers
46
+ * @param {string|object} [endpointOrOptions='/api'] Endpoint path or options object
47
+ * @param {object} [options] Options object (when endpointOrOptions is a string)
48
+ */
49
+ constructor(router, context, endpointOrOptions = '/api', options = {}) {
50
+ const defaultSafeOptions = {
51
+ safeEnabled: true,
52
+ strictMode: true,
53
+ };
54
+
55
+ if (typeof endpointOrOptions === 'string') {
56
+ // String endpoint with options object
57
+ const finalOptions = { ...defaultSafeOptions, ...options };
58
+ super(router, context, endpointOrOptions, finalOptions);
59
+ } else {
60
+ // Options object form
61
+ const finalOptions = { ...defaultSafeOptions, ...endpointOrOptions };
62
+ super(router, context, finalOptions);
63
+ }
64
+ }
65
+ }
8
66
 
9
67
  /**
68
+ * @deprecated Use `new RpcSafeEndpoint(...)` instead.
10
69
  * Create a RpcEndpoint with safe defaults (safeEnabled=true, strictMode=true).
11
- * Preserves both constructor signatures supported by RpcEndpoint.
12
70
  *
13
71
  * @param {import('express').Router} router
14
72
  * @param {any} context
@@ -21,34 +79,34 @@ function createSafeEndpoint(
21
79
  endpointOrOptions = '/api',
22
80
  options = {}
23
81
  ) {
24
- if (typeof endpointOrOptions === 'string') {
25
- const finalOptions = { ...defaultSafeOptions, ...options };
26
- return new Main.RpcEndpoint(
27
- router,
28
- context,
29
- endpointOrOptions,
30
- finalOptions
31
- );
32
- }
33
- // options object form
34
- const final = { ...defaultSafeOptions, ...endpointOrOptions };
35
- return new Main.RpcEndpoint(router, context, final);
82
+ console.warn(
83
+ 'createSafeEndpoint() is deprecated. Use `new RpcSafeEndpoint(...)` instead.'
84
+ );
85
+ return new RpcSafeEndpoint(router, context, endpointOrOptions, options);
36
86
  }
37
87
 
38
88
  /**
89
+ * @deprecated Use `new RpcSafeClient(...)` instead.
39
90
  * Create a RpcClient with safe defaults (safeEnabled=true).
91
+ *
40
92
  * @param {string} endpoint
41
93
  * @param {Record<string,string>} [defaultHeaders]
42
94
  * @param {object} [options]
43
95
  */
44
96
  function createSafeClient(endpoint, defaultHeaders = {}, options = {}) {
45
- const finalOptions = { safeEnabled: true, ...options };
46
- return new Main.RpcClient(endpoint, defaultHeaders, finalOptions);
97
+ console.warn(
98
+ 'createSafeClient() is deprecated. Use `new RpcSafeClient(...)` instead.'
99
+ );
100
+ return new RpcSafeClient(endpoint, defaultHeaders, options);
47
101
  }
48
102
 
49
103
  module.exports = {
50
104
  // Re-export everything for convenience
51
105
  ...Main,
106
+ // New class-based API
107
+ RpcSafeClient,
108
+ RpcSafeEndpoint,
109
+ // Deprecated factory functions (kept for backward compatibility)
52
110
  createSafeEndpoint,
53
111
  createSafeClient,
54
112
  };
package/src/validation.js CHANGED
@@ -1,221 +1,222 @@
1
- /* eslint max-classes-per-file: off */
2
- const Ajv = require('ajv');
3
- const addFormats = require('ajv-formats');
4
-
5
- /**
6
- * JSON-RPC 2.0 Schema Validation Error
7
- */
8
- class SchemaValidator {
9
- constructor(options = {}) {
10
- this.ajv = new Ajv({
11
- allErrors: true,
12
- removeAdditional: options.removeAdditional || false,
13
- useDefaults: options.useDefaults !== false,
14
- coerceTypes: options.coerceTypes || false,
15
- ...options.ajvOptions,
16
- });
17
-
18
- addFormats(this.ajv);
19
-
20
- // Add custom formats
21
- this.addCustomFormats();
22
- }
23
-
24
- /**
25
- * Add custom format validators
26
- */
27
- addCustomFormats() {
28
- // BigInt format
29
- this.ajv.addFormat('bigint', {
30
- type: 'string',
31
- validate: (data) => /^-?\d+n?$/.test(data),
32
- });
33
-
34
- // ObjectId format (MongoDB style)
35
- this.ajv.addFormat('objectid', {
36
- type: 'string',
37
- validate: (data) => /^[0-9a-fA-F]{24}$/.test(data),
38
- });
39
- }
40
-
41
- /**
42
- * Validate parameters against a schema
43
- * @param {any} params
44
- * @param {Object} schema
45
- * @returns {Object} validation result
46
- */
47
- validate(params, schema) {
48
- const validator = this.ajv.compile(schema);
49
- const valid = validator(params);
50
-
51
- return {
52
- valid,
53
- errors: valid ? null : validator.errors,
54
- data: params,
55
- };
56
- }
57
-
58
- /**
59
- * Create a validation middleware
60
- * @param {Object} schema
61
- * @returns {Function}
62
- */
63
- middleware(schema) {
64
- return async (context) => {
65
- const { params } = context;
66
- const result = this.validate(params, schema);
67
-
68
- if (!result.valid) {
69
- const error = new Error('Validation failed');
70
- error.code = -32602; // Invalid params
71
- error.data = {
72
- validationErrors: result.errors.map((err) => ({
73
- field: err.instancePath || err.schemaPath,
74
- message: err.message,
75
- value: err.data,
76
- })),
77
- };
78
- throw error;
79
- }
80
-
81
- // Update params with validated/coerced data
82
- context.params = result.data;
83
- return context;
84
- };
85
- }
86
-
87
- /**
88
- * Add a custom keyword
89
- * @param {string} name
90
- * @param {Object} definition
91
- */
92
- addKeyword(name, definition) {
93
- this.ajv.addKeyword(name, definition);
94
- }
95
- }
96
-
97
- /**
98
- * Common schema definitions
99
- */
100
- const commonSchemas = {
101
- // Pagination parameters
102
- pagination: {
103
- type: 'object',
104
- properties: {
105
- page: { type: 'integer', minimum: 1, default: 1 },
106
- limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 },
107
- sort: { type: 'string' },
108
- order: { type: 'string', enum: ['asc', 'desc'], default: 'asc' },
109
- },
110
- },
111
-
112
- // User identification
113
- userId: {
114
- oneOf: [
115
- { type: 'string', format: 'objectid' },
116
- { type: 'string', format: 'uuid' },
117
- { type: 'integer', minimum: 1 },
118
- ],
119
- },
120
-
121
- // Email validation
122
- email: {
123
- type: 'string',
124
- format: 'email',
125
- maxLength: 255,
126
- },
127
-
128
- // Password requirements
129
- password: {
130
- type: 'string',
131
- minLength: 8,
132
- maxLength: 128,
133
- pattern: '^(?=.*[a-z])(?=.*[A-Z])(?=.*\\d)',
134
- },
135
-
136
- // BigInt validation
137
- bigintString: {
138
- type: 'string',
139
- format: 'bigint',
140
- },
141
-
142
- // Date validation
143
- dateString: {
144
- type: 'string',
145
- format: 'date-time',
146
- },
147
- };
148
-
149
- /**
150
- * Schema builder helper
151
- */
152
- class SchemaBuilder {
153
- constructor() {
154
- this.schema = {
155
- type: 'object',
156
- properties: {},
157
- required: [],
158
- };
159
- }
160
-
161
- /**
162
- * Add a property to the schema
163
- * @param {string} name
164
- * @param {Object} definition
165
- * @param {boolean} required
166
- * @returns {SchemaBuilder}
167
- */
168
- property(name, definition, required = false) {
169
- this.schema.properties[name] = definition;
170
- if (required) {
171
- this.schema.required.push(name);
172
- }
173
- return this;
174
- }
175
-
176
- /**
177
- * Add multiple properties
178
- * @param {Object} properties
179
- * @returns {SchemaBuilder}
180
- */
181
- properties(properties) {
182
- Object.entries(properties).forEach(([name, definition]) => {
183
- this.property(name, definition);
184
- });
185
- return this;
186
- }
187
-
188
- /**
189
- * Set required fields
190
- * @param {string[]} fields
191
- * @returns {SchemaBuilder}
192
- */
193
- required(fields) {
194
- this.schema.required = [...new Set([...this.schema.required, ...fields])];
195
- return this;
196
- }
197
-
198
- /**
199
- * Add additional properties setting
200
- * @param {boolean} allowed
201
- * @returns {SchemaBuilder}
202
- */
203
- additionalProperties(allowed) {
204
- this.schema.additionalProperties = allowed;
205
- return this;
206
- }
207
-
208
- /**
209
- * Build and return the schema
210
- * @returns {Object}
211
- */
212
- build() {
213
- return this.schema;
214
- }
215
- }
216
-
217
- module.exports = {
218
- SchemaValidator,
219
- commonSchemas,
220
- SchemaBuilder,
221
- };
1
+ /* eslint max-classes-per-file: off */
2
+ const Ajv = require('ajv');
3
+ const addFormats = require('ajv-formats');
4
+
5
+ /**
6
+ * JSON-RPC 2.0 Schema Validation Error
7
+ */
8
+ class SchemaValidator {
9
+ constructor(options = {}) {
10
+ this.ajv = new Ajv({
11
+ allErrors: true,
12
+ removeAdditional: options.removeAdditional || false,
13
+ useDefaults: options.useDefaults !== false,
14
+ coerceTypes: options.coerceTypes || false,
15
+ strict: false, // Allow unknown keywords like exposeSchema in method options
16
+ ...options.ajvOptions,
17
+ });
18
+
19
+ addFormats(this.ajv);
20
+
21
+ // Add custom formats
22
+ this.addCustomFormats();
23
+ }
24
+
25
+ /**
26
+ * Add custom format validators
27
+ */
28
+ addCustomFormats() {
29
+ // BigInt format
30
+ this.ajv.addFormat('bigint', {
31
+ type: 'string',
32
+ validate: (data) => /^-?\d+n?$/.test(data),
33
+ });
34
+
35
+ // ObjectId format (MongoDB style)
36
+ this.ajv.addFormat('objectid', {
37
+ type: 'string',
38
+ validate: (data) => /^[0-9a-fA-F]{24}$/.test(data),
39
+ });
40
+ }
41
+
42
+ /**
43
+ * Validate parameters against a schema
44
+ * @param {any} params
45
+ * @param {Object} schema
46
+ * @returns {Object} validation result
47
+ */
48
+ validate(params, schema) {
49
+ const validator = this.ajv.compile(schema);
50
+ const valid = validator(params);
51
+
52
+ return {
53
+ valid,
54
+ errors: valid ? null : validator.errors,
55
+ data: params,
56
+ };
57
+ }
58
+
59
+ /**
60
+ * Create a validation middleware
61
+ * @param {Object} schema
62
+ * @returns {Function}
63
+ */
64
+ middleware(schema) {
65
+ return async (context) => {
66
+ const { params } = context;
67
+ const result = this.validate(params, schema);
68
+
69
+ if (!result.valid) {
70
+ const error = new Error('Validation failed');
71
+ error.code = -32602; // Invalid params
72
+ error.data = {
73
+ validationErrors: result.errors.map((err) => ({
74
+ field: err.instancePath || err.schemaPath,
75
+ message: err.message,
76
+ value: err.data,
77
+ })),
78
+ };
79
+ throw error;
80
+ }
81
+
82
+ // Update params with validated/coerced data
83
+ context.params = result.data;
84
+ return context;
85
+ };
86
+ }
87
+
88
+ /**
89
+ * Add a custom keyword
90
+ * @param {string} name
91
+ * @param {Object} definition
92
+ */
93
+ addKeyword(name, definition) {
94
+ this.ajv.addKeyword(name, definition);
95
+ }
96
+ }
97
+
98
+ /**
99
+ * Common schema definitions
100
+ */
101
+ const commonSchemas = {
102
+ // Pagination parameters
103
+ pagination: {
104
+ type: 'object',
105
+ properties: {
106
+ page: { type: 'integer', minimum: 1, default: 1 },
107
+ limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 },
108
+ sort: { type: 'string' },
109
+ order: { type: 'string', enum: ['asc', 'desc'], default: 'asc' },
110
+ },
111
+ },
112
+
113
+ // User identification
114
+ userId: {
115
+ oneOf: [
116
+ { type: 'string', format: 'objectid' },
117
+ { type: 'string', format: 'uuid' },
118
+ { type: 'integer', minimum: 1 },
119
+ ],
120
+ },
121
+
122
+ // Email validation
123
+ email: {
124
+ type: 'string',
125
+ format: 'email',
126
+ maxLength: 255,
127
+ },
128
+
129
+ // Password requirements
130
+ password: {
131
+ type: 'string',
132
+ minLength: 8,
133
+ maxLength: 128,
134
+ pattern: '^(?=.*[a-z])(?=.*[A-Z])(?=.*\\d)',
135
+ },
136
+
137
+ // BigInt validation
138
+ bigintString: {
139
+ type: 'string',
140
+ format: 'bigint',
141
+ },
142
+
143
+ // Date validation
144
+ dateString: {
145
+ type: 'string',
146
+ format: 'date-time',
147
+ },
148
+ };
149
+
150
+ /**
151
+ * Schema builder helper
152
+ */
153
+ class SchemaBuilder {
154
+ constructor() {
155
+ this.schema = {
156
+ type: 'object',
157
+ properties: {},
158
+ required: [],
159
+ };
160
+ }
161
+
162
+ /**
163
+ * Add a property to the schema
164
+ * @param {string} name
165
+ * @param {Object} definition
166
+ * @param {boolean} required
167
+ * @returns {SchemaBuilder}
168
+ */
169
+ property(name, definition, required = false) {
170
+ this.schema.properties[name] = definition;
171
+ if (required) {
172
+ this.schema.required.push(name);
173
+ }
174
+ return this;
175
+ }
176
+
177
+ /**
178
+ * Add multiple properties
179
+ * @param {Object} properties
180
+ * @returns {SchemaBuilder}
181
+ */
182
+ properties(properties) {
183
+ Object.entries(properties).forEach(([name, definition]) => {
184
+ this.property(name, definition);
185
+ });
186
+ return this;
187
+ }
188
+
189
+ /**
190
+ * Set required fields
191
+ * @param {string[]} fields
192
+ * @returns {SchemaBuilder}
193
+ */
194
+ required(fields) {
195
+ this.schema.required = [...new Set([...this.schema.required, ...fields])];
196
+ return this;
197
+ }
198
+
199
+ /**
200
+ * Add additional properties setting
201
+ * @param {boolean} allowed
202
+ * @returns {SchemaBuilder}
203
+ */
204
+ additionalProperties(allowed) {
205
+ this.schema.additionalProperties = allowed;
206
+ return this;
207
+ }
208
+
209
+ /**
210
+ * Build and return the schema
211
+ * @returns {Object}
212
+ */
213
+ build() {
214
+ return this.schema;
215
+ }
216
+ }
217
+
218
+ module.exports = {
219
+ SchemaValidator,
220
+ commonSchemas,
221
+ SchemaBuilder,
222
+ };