vantiv.io 1.0.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.
Files changed (48) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +864 -0
  3. package/index.js +13 -0
  4. package/package.json +28 -0
  5. package/src/classes/Actions/Awaiter.js +202 -0
  6. package/src/classes/Actions/Channel.js +73 -0
  7. package/src/classes/Actions/Direct.js +263 -0
  8. package/src/classes/Actions/Inventory.js +156 -0
  9. package/src/classes/Actions/Music.js +278 -0
  10. package/src/classes/Actions/Player.js +377 -0
  11. package/src/classes/Actions/Public.js +66 -0
  12. package/src/classes/Actions/Room.js +333 -0
  13. package/src/classes/Actions/Utils.js +29 -0
  14. package/src/classes/Actions/lib/AudioStreaming.js +447 -0
  15. package/src/classes/Caches/MovementCache.js +357 -0
  16. package/src/classes/Handlers/AxiosErrorHandler.js +68 -0
  17. package/src/classes/Handlers/ErrorHandler.js +65 -0
  18. package/src/classes/Handlers/EventHandlers.js +259 -0
  19. package/src/classes/Handlers/WebSocketHandlers.js +54 -0
  20. package/src/classes/Managers/ChannelManager.js +303 -0
  21. package/src/classes/Managers/DanceFloorManagers.js +509 -0
  22. package/src/classes/Managers/Helpers/CleanupManager.js +130 -0
  23. package/src/classes/Managers/Helpers/LoggerManager.js +171 -0
  24. package/src/classes/Managers/Helpers/MetricsManager.js +83 -0
  25. package/src/classes/Managers/Networking/ConnectionManager.js +259 -0
  26. package/src/classes/Managers/Networking/CooldownManager.js +516 -0
  27. package/src/classes/Managers/Networking/EventsManager.js +64 -0
  28. package/src/classes/Managers/Networking/KeepAliveManager.js +109 -0
  29. package/src/classes/Managers/Networking/MessageHandler.js +110 -0
  30. package/src/classes/Managers/Networking/Request.js +329 -0
  31. package/src/classes/Managers/PermissionManager.js +288 -0
  32. package/src/classes/WebApi/Category/Grab.js +98 -0
  33. package/src/classes/WebApi/Category/Item.js +347 -0
  34. package/src/classes/WebApi/Category/Post.js +154 -0
  35. package/src/classes/WebApi/Category/Room.js +137 -0
  36. package/src/classes/WebApi/Category/User.js +88 -0
  37. package/src/classes/WebApi/webapi.js +52 -0
  38. package/src/constants/TypesConstants.js +89 -0
  39. package/src/constants/WebSocketConstants.js +80 -0
  40. package/src/core/Highrise.js +123 -0
  41. package/src/core/HighriseWebsocket.js +228 -0
  42. package/src/utils/ConvertSvgToPng.js +51 -0
  43. package/src/utils/ModelPool.js +160 -0
  44. package/src/utils/Models.js +128 -0
  45. package/src/utils/versionCheck.js +27 -0
  46. package/src/validators/ConfigValidator.js +205 -0
  47. package/src/validators/ConnectionValidator.js +65 -0
  48. package/typings/index.d.ts +3820 -0
@@ -0,0 +1,51 @@
1
+ const sharp = require('sharp');
2
+ const fs = require('fs').promises;
3
+
4
+ async function convertAvatarToPNG(encodedSVG, outputPath = 'avatar.png', options = {}) {
5
+ try {
6
+ const {
7
+ density = 1800,
8
+ quality = 100,
9
+ compressionLevel = 0
10
+ } = options;
11
+
12
+ function decodeAvatarSVG(encodedSVG) {
13
+ return encodedSVG
14
+ .replace(/\\u003C/g, '<')
15
+ .replace(/\\u003E/g, '>')
16
+ .replace(/\\u0026/g, '&')
17
+ .replace(/\\u0027/g, "'")
18
+ .replace(/\\u0022/g, '"');
19
+ }
20
+
21
+ const decodedSVG = decodeAvatarSVG(encodedSVG);
22
+
23
+ const pngBuffer = await sharp(Buffer.from(decodedSVG), {
24
+ density: density
25
+ })
26
+ .png({
27
+ quality: quality,
28
+ compressionLevel: compressionLevel,
29
+ adaptiveFiltering: false,
30
+ force: true
31
+ })
32
+ .toBuffer();
33
+
34
+ await fs.writeFile(outputPath, pngBuffer);
35
+
36
+ const metadata = await sharp(pngBuffer).metadata();
37
+ console.log(`✅ Lossless PNG: ${metadata.width}x${metadata.height}px @ ${density}DPI`);
38
+
39
+ return {
40
+ success: true,
41
+ width: metadata.width,
42
+ height: metadata.height,
43
+ size: pngBuffer.length / 1024 / 1024
44
+ };
45
+
46
+ } catch (error) {
47
+ throw error;
48
+ }
49
+ }
50
+
51
+ module.exports = convertAvatarToPNG;
@@ -0,0 +1,160 @@
1
+ class ModelPool {
2
+ constructor() {
3
+ this._pools = new Map();
4
+ this._parsers = new Map();
5
+ this._stats = {
6
+ created: 0,
7
+ reused: 0,
8
+ peakSize: 0,
9
+ batchReleased: 0
10
+ };
11
+ }
12
+
13
+ registerModel(type, parser) {
14
+ if (typeof parser !== 'function') {
15
+ throw new Error(`Parser for type '${type}' must be a function`);
16
+ }
17
+ this._parsers.set(type, parser);
18
+ if (!this._pools.has(type)) {
19
+ this._pools.set(type, []);
20
+ }
21
+ }
22
+
23
+ get(type, ...args) {
24
+ if (!this._parsers.has(type)) {
25
+ throw new Error(`Model type '${type}' not registered. Call registerModel() first.`);
26
+ }
27
+
28
+ const pool = this._getPool(type);
29
+ const parser = this._parsers.get(type);
30
+
31
+ let model;
32
+ if (pool.length > 0) {
33
+ model = pool.pop();
34
+ this._stats.reused++;
35
+ Object.assign(model, parser(...args));
36
+ } else {
37
+ model = parser(...args);
38
+ this._stats.created++;
39
+ }
40
+
41
+ return model;
42
+ }
43
+
44
+ release(type, model) {
45
+ if (!model) return;
46
+
47
+ const pool = this._getPool(type);
48
+ pool.push(model);
49
+ this._stats.peakSize = Math.max(this._stats.peakSize, pool.length);
50
+ }
51
+
52
+ releaseBatch(type, models) {
53
+ if (!models || !Array.isArray(models) || models.length === 0) return;
54
+
55
+ const pool = this._getPool(type);
56
+ const validModels = models.filter(model => model != null);
57
+
58
+ if (validModels.length > 0) {
59
+ pool.push(...validModels);
60
+ this._stats.batchReleased += validModels.length;
61
+ this._stats.peakSize = Math.max(this._stats.peakSize, pool.length);
62
+ }
63
+ }
64
+
65
+ releaseMultiple(modelMap) {
66
+ if (!modelMap || typeof modelMap !== 'object') return;
67
+
68
+ let totalReleased = 0;
69
+
70
+ for (const [type, models] of Object.entries(modelMap)) {
71
+ if (Array.isArray(models)) {
72
+ const pool = this._getPool(type);
73
+ const validModels = models.filter(model => model != null);
74
+
75
+ if (validModels.length > 0) {
76
+ pool.push(...validModels);
77
+ totalReleased += validModels.length;
78
+ this._stats.peakSize = Math.max(this._stats.peakSize, pool.length);
79
+ }
80
+ }
81
+ }
82
+
83
+ this._stats.batchReleased += totalReleased;
84
+ }
85
+
86
+ getBatch(type, count, ...args) {
87
+ if (count <= 0) return [];
88
+
89
+ const models = new Array(count);
90
+ for (let i = 0; i < count; i++) {
91
+ models[i] = this.get(type, ...args);
92
+ }
93
+ return models;
94
+ }
95
+
96
+ hasType(type) {
97
+ return this._parsers.has(type);
98
+ }
99
+
100
+ getRegisteredTypes() {
101
+ return Array.from(this._parsers.keys());
102
+ }
103
+
104
+ getStats() {
105
+ const poolSizes = {};
106
+ for (const [type, pool] of this._pools) {
107
+ poolSizes[type] = pool.length;
108
+ }
109
+
110
+ return {
111
+ ...this._stats,
112
+ poolSizes,
113
+ registeredTypes: this.getRegisteredTypes(),
114
+ totalOperations: this._stats.created + this._stats.reused + this._stats.batchReleased
115
+ };
116
+ }
117
+
118
+ clear(type = null) {
119
+ if (type) {
120
+ this._pools.get(type)?.splice(0);
121
+ } else {
122
+ for (const pool of this._pools.values()) {
123
+ pool.splice(0);
124
+ }
125
+
126
+ this._stats.created = 0;
127
+ this._stats.reused = 0;
128
+ this._stats.peakSize = 0;
129
+ this._stats.batchReleased = 0;
130
+ }
131
+ }
132
+
133
+ getPoolSize(type) {
134
+ return this._pools.get(type)?.length || 0;
135
+ }
136
+
137
+ preWarm(type, count) {
138
+ if (count <= 0) return;
139
+
140
+ const pool = this._getPool(type);
141
+ const parser = this._parsers.get(type);
142
+
143
+ for (let i = 0; i < count; i++) {
144
+ const model = parser();
145
+ pool.push(model);
146
+ this._stats.created++;
147
+ }
148
+
149
+ this._stats.peakSize = Math.max(this._stats.peakSize, pool.length);
150
+ }
151
+
152
+ _getPool(type) {
153
+ if (!this._pools.has(type)) {
154
+ this._pools.set(type, []);
155
+ }
156
+ return this._pools.get(type);
157
+ }
158
+ }
159
+
160
+ module.exports = ModelPool;
@@ -0,0 +1,128 @@
1
+ class User {
2
+ constructor(data = {}) {
3
+ this.username = data.user.username || ''
4
+ this.id = data.user.id || ''
5
+ }
6
+ }
7
+
8
+ class Position {
9
+ constructor(data = {}) {
10
+ this.x = data.position.x || 0
11
+ this.y = data.position.y || 0
12
+ this.z = data.position.z || 0
13
+ this.facing = data.position.facing || ''
14
+ }
15
+ }
16
+
17
+ class Tip {
18
+ constructor(data = {}) {
19
+ this.sender = {
20
+ id: data.sender.id,
21
+ username: data.sender.username
22
+ }
23
+
24
+ this.receiver = {
25
+ id: data.receiver.id,
26
+ username: data.receiver.username
27
+ }
28
+
29
+ this.item = {
30
+ type: data.item.type || '',
31
+ amount: data.item.amount || 0
32
+ }
33
+ }
34
+ }
35
+
36
+ class AnchorPosition {
37
+ constructor(data = {}) {
38
+ this.entity_id = data.position.entity_id || ''
39
+ this.anchor_ix = data.position.anchor_ix || 0
40
+ }
41
+ }
42
+
43
+ class Message {
44
+ constructor(data = {}) {
45
+ this.user = {
46
+ id: data.user?.id || '',
47
+ username: data.user?.username || ''
48
+ };
49
+ this.message = data.message || ''
50
+ }
51
+ }
52
+
53
+ class Direct {
54
+ constructor(data = {}) {
55
+ this.user = {
56
+ id: data.user_id || '',
57
+ username: null
58
+ },
59
+
60
+ this.message = data.message
61
+ this.conversation = {
62
+ id: data.conversation_id || "",
63
+ is_new_conversation: data.is_new_conversation
64
+ }
65
+ }
66
+ }
67
+
68
+ class SessionMetadata {
69
+ constructor(data = {}) {
70
+ this.bot_id = data.user_id || ''
71
+ this.room = {
72
+ owner_id: data.room_info.owner_id || '',
73
+ room_name: data.room_info.room_name || ''
74
+ };
75
+ this.metadata = {
76
+ connection_id: data.connection_id || '',
77
+ rate_limits: data.rate_limits || {}
78
+ };
79
+ }
80
+ }
81
+
82
+ class RoomModerate {
83
+ constructor(data = {}) {
84
+ this.moderator = {
85
+ id: data.moderatorId || '',
86
+ username: data.moderator_username || null
87
+ }
88
+
89
+ this.target = {
90
+ id: data.targetUserId || '',
91
+ username: data.target_username || null
92
+ }
93
+
94
+ this.action = {
95
+ type: data.moderationType || '',
96
+ duration: data.duration || null
97
+ }
98
+ }
99
+ }
100
+
101
+ class HiddenChannel {
102
+ constructor(data = {}) {
103
+ this.sender_id = data.sender_id
104
+ this.message = data.msg
105
+ this.tags = data.tags
106
+ }
107
+ }
108
+
109
+ class Voice {
110
+ constructor(data = {}) {
111
+ this.users = data.users.map(([user, status]) => ({ user, status }));
112
+ this.seconds_left = data.seconds_left
113
+ this.ended = false
114
+ }
115
+ }
116
+
117
+ module.exports = {
118
+ Tip,
119
+ User,
120
+ Voice,
121
+ Direct,
122
+ Message,
123
+ Position,
124
+ RoomModerate,
125
+ HiddenChannel,
126
+ AnchorPosition,
127
+ SessionMetadata,
128
+ }
@@ -0,0 +1,27 @@
1
+ const colors = require('colors');
2
+
3
+ async function checkVersion(logger) {
4
+ try {
5
+ const packageJson = require('../../package.json');
6
+ const currentVersion = packageJson.version;
7
+ const packageName = packageJson.name;
8
+
9
+ const response = await fetch(`https://registry.npmjs.org/${packageName}/latest`);
10
+ const data = await response.json();
11
+ const latestVersion = data.version;
12
+
13
+ if (latestVersion === currentVersion) {
14
+ logger.success('versionCheck', `Using latest version: ${colors.green(`v${currentVersion}`)}`);
15
+ return;
16
+ }
17
+
18
+ logger.warn('versionCheck',
19
+ `${colors.yellow('Update available:')} ${colors.red(`v${currentVersion}`)} → ${colors.green(`v${latestVersion}`)} | ${colors.cyan(`npm install ${packageName}@latest`)}`
20
+ );
21
+
22
+ } catch (error) {
23
+ logger.debug('versionCheck', 'Version check failed', { error: error.message });
24
+ }
25
+ }
26
+
27
+ module.exports = { checkVersion };
@@ -0,0 +1,205 @@
1
+ class ConfigValidator {
2
+ static validateHighriseOptions(options = {}) {
3
+ const errors = [];
4
+ const warnings = [];
5
+ const validated = { ...options };
6
+
7
+ if (validated.Events !== undefined) {
8
+ if (!Array.isArray(validated.Events)) {
9
+ errors.push('Events must be an array');
10
+ } else if (validated.Events.length === 0) {
11
+ errors.push('At least one event type must be specified');
12
+ }
13
+ }
14
+
15
+ if (validated.LoggerOptions !== undefined) {
16
+ if (typeof validated.LoggerOptions !== 'object' || validated.LoggerOptions === null) {
17
+ errors.push('LoggerOptions must be an object');
18
+ } else {
19
+ const loggerResult = this.validateLoggerOptions(validated.LoggerOptions);
20
+ if (!loggerResult.success) {
21
+ errors.push(...loggerResult.errors);
22
+ } else {
23
+ validated.LoggerOptions = loggerResult.validatedConfig;
24
+ }
25
+ }
26
+ }
27
+
28
+ if (validated.autoReconnect !== undefined) {
29
+ if (typeof validated.autoReconnect !== 'boolean') {
30
+ errors.push('autoReconnect must be a boolean');
31
+ }
32
+ }
33
+
34
+ if (validated.reconnectDelay !== undefined) {
35
+ if (typeof validated.reconnectDelay !== 'number') {
36
+ errors.push('reconnectDelay must be a number');
37
+ } else if (validated.reconnectDelay < 1000) {
38
+ warnings.push('reconnectDelay below 1000ms may cause rate limiting');
39
+ validated.reconnectDelay = Math.max(500, validated.reconnectDelay);
40
+ } else if (validated.reconnectDelay > 300000) {
41
+ warnings.push('reconnectDelay above 300000ms (5 minutes) may impact user experience');
42
+ validated.reconnectDelay = Math.min(validated.reconnectDelay, 600000);
43
+ }
44
+
45
+ validated.reconnectDelay = Math.round(validated.reconnectDelay);
46
+ }
47
+
48
+ return {
49
+ success: errors.length === 0,
50
+ errors,
51
+ warnings,
52
+ validatedConfig: validated
53
+ };
54
+ }
55
+
56
+ static validateLoggerOptions(loggerOptions = {}) {
57
+ const errors = [];
58
+ const warnings = [];
59
+ const validated = { ...loggerOptions };
60
+
61
+ const booleanFields = ['showTimestamp', 'showMethodName', 'colors'];
62
+ booleanFields.forEach(field => {
63
+ if (validated[field] !== undefined) {
64
+ if (typeof validated[field] !== 'boolean') {
65
+ // Try to convert string booleans
66
+ if (typeof validated[field] === 'string') {
67
+ if (validated[field].toLowerCase() === 'true') {
68
+ validated[field] = true;
69
+ warnings.push(`LoggerOptions.${field} was converted from string to boolean`);
70
+ } else if (validated[field].toLowerCase() === 'false') {
71
+ validated[field] = false;
72
+ warnings.push(`LoggerOptions.${field} was converted from string to boolean`);
73
+ } else {
74
+ errors.push(`LoggerOptions.${field} must be a boolean`);
75
+ }
76
+ } else {
77
+ errors.push(`LoggerOptions.${field} must be a boolean`);
78
+ }
79
+ }
80
+ }
81
+ });
82
+
83
+ return {
84
+ success: errors.length === 0,
85
+ errors,
86
+ warnings,
87
+ validatedConfig: validated
88
+ };
89
+ }
90
+
91
+ static validateSenderConfig(config = {}) {
92
+ const errors = [];
93
+ const warnings = [];
94
+ const validated = { ...config };
95
+
96
+ if (validated.defaultTimeout !== undefined) {
97
+ const timeout = parseInt(validated.defaultTimeout);
98
+ if (isNaN(timeout)) {
99
+ errors.push('defaultTimeout must be a number');
100
+ } else if (timeout < 100) {
101
+ errors.push('defaultTimeout must be at least 100ms');
102
+ } else if (timeout > 60000) {
103
+ warnings.push('defaultTimeout above 60000ms may cause slow response detection');
104
+ validated.defaultTimeout = Math.min(timeout, 120000);
105
+ } else {
106
+ validated.defaultTimeout = timeout;
107
+ }
108
+ }
109
+
110
+ if (validated.maxRetries !== undefined) {
111
+ const retries = parseInt(validated.maxRetries);
112
+ if (isNaN(retries)) {
113
+ errors.push('maxRetries must be a number');
114
+ } else if (retries < 0) {
115
+ errors.push('maxRetries cannot be negative');
116
+ } else if (retries > 10) {
117
+ warnings.push('maxRetries above 10 may cause excessive network traffic');
118
+ validated.maxRetries = Math.min(retries, 15);
119
+ } else {
120
+ validated.maxRetries = retries;
121
+ }
122
+ }
123
+
124
+ if (validated.retryDelay !== undefined) {
125
+ const delay = parseInt(validated.retryDelay);
126
+ if (isNaN(delay)) {
127
+ errors.push('retryDelay must be a number');
128
+ } else if (delay < 10) {
129
+ warnings.push('retryDelay below 10ms may not allow proper error recovery');
130
+ validated.retryDelay = Math.max(delay, 5);
131
+ } else if (delay > 30000) {
132
+ warnings.push('retryDelay above 30000ms may impact user experience');
133
+ validated.retryDelay = Math.min(delay, 60000);
134
+ } else {
135
+ validated.retryDelay = delay;
136
+ }
137
+ }
138
+
139
+ return {
140
+ success: errors.length === 0,
141
+ errors,
142
+ warnings,
143
+ validatedConfig: validated
144
+ };
145
+ }
146
+
147
+ static validateEvents(events) {
148
+ const errors = [];
149
+ const validEvents = [
150
+ 'SessionMetadata', 'ChatEvent', 'WhisperEvent', 'UserMovedEvent',
151
+ 'UserJoinedEvent', 'UserLeftEvent', 'MessageEvent', 'TipReactionEvent',
152
+ 'RoomModeratedEvent', 'ChannelEvent'
153
+ ];
154
+
155
+ if (!Array.isArray(events)) {
156
+ errors.push('Events must be an array');
157
+ return { success: false, errors, validEvents };
158
+ }
159
+
160
+ if (events.length === 0) {
161
+ errors.push('At least one event type must be specified');
162
+ }
163
+
164
+ const invalidEvents = events.filter(event => !validEvents.includes(event));
165
+ if (invalidEvents.length > 0) {
166
+ errors.push(`Invalid event types: ${invalidEvents.join(', ')}`);
167
+ }
168
+
169
+ const duplicateEvents = events.filter((event, index) => events.indexOf(event) !== index);
170
+ if (duplicateEvents.length > 0) {
171
+ warnings.push(`Duplicate event types: ${duplicateEvents.join(', ')}`);
172
+ }
173
+
174
+ return {
175
+ success: errors.length === 0,
176
+ errors,
177
+ warnings: duplicateEvents.length > 0 ? [`Duplicate event types: ${duplicateEvents.join(', ')}`] : [],
178
+ validEvents,
179
+ validatedEvents: [...new Set(events)]
180
+ };
181
+ }
182
+
183
+ static validateCredentials(token, roomId) {
184
+ const errors = [];
185
+
186
+ if (!token || typeof token !== 'string') {
187
+ errors.push('Token must be a non-empty string');
188
+ } else if (token.length !== 64) {
189
+ errors.push('Token must be exactly 64 characters long');
190
+ }
191
+
192
+ if (!roomId || typeof roomId !== 'string') {
193
+ errors.push('Room ID must be a non-empty string');
194
+ } else if (roomId.length !== 24) {
195
+ errors.push('Room ID must be exactly 24 characters long');
196
+ }
197
+
198
+ return {
199
+ success: errors.length === 0,
200
+ errors
201
+ };
202
+ }
203
+ }
204
+
205
+ module.exports = { ConfigValidator };
@@ -0,0 +1,65 @@
1
+ class ConnectionValidator {
2
+ static validateCredentials(token, roomId) {
3
+ const errors = [];
4
+
5
+ if (!token || typeof token !== 'string') {
6
+ errors.push('Token must be a non-empty string');
7
+ } else if (token.length !== 64) {
8
+ errors.push(`Token must be exactly 64 characters (got ${token.length})`);
9
+ }
10
+
11
+ if (!roomId || typeof roomId !== 'string') {
12
+ errors.push('Room ID must be a non-empty string');
13
+ } else if (roomId.length !== 24) {
14
+ errors.push(`Room ID must be exactly 24 characters (got ${roomId.length})`);
15
+ }
16
+
17
+ if (errors.length > 0) {
18
+ throw new Error(`Connection validation failed: ${errors.join('; ')}`);
19
+ }
20
+ }
21
+
22
+ static validateEvents(events) {
23
+ if (!Array.isArray(events)) {
24
+ throw new Error('Events must be an array');
25
+ }
26
+
27
+ if (events.length === 0) {
28
+ throw new Error('At least one event type must be specified');
29
+ }
30
+
31
+ const validEvents = [
32
+ 'SessionMetadata', 'ChatEvent', 'WhisperEvent',
33
+ 'UserMovedEvent', 'UserJoinedEvent', 'UserLeftEvent',
34
+ 'MessageEvent', 'TipReactionEvent', 'RoomModeratedEvent',
35
+ 'ChannelEvent'
36
+ ];
37
+
38
+ const invalidEvents = events.filter(event => !validEvents.includes(event));
39
+ if (invalidEvents.length > 0) {
40
+ throw new Error(`Invalid event types: ${invalidEvents.join(', ')}`);
41
+ }
42
+
43
+ return [...new Set(events)];
44
+ }
45
+
46
+ static validateOptions(options) {
47
+ const validated = { ...options };
48
+
49
+ if (validated.autoReconnect !== undefined && typeof validated.autoReconnect !== 'boolean') {
50
+ throw new TypeError('autoReconnect must be a boolean');
51
+ }
52
+
53
+ if (validated.reconnectDelay !== undefined) {
54
+ const delay = parseInt(validated.reconnectDelay);
55
+ if (isNaN(delay) || delay < 1000) {
56
+ throw new TypeError('reconnectDelay must be a number >= 1000');
57
+ }
58
+ validated.reconnectDelay = Math.min(delay, 300000);
59
+ }
60
+
61
+ return validated;
62
+ }
63
+ }
64
+
65
+ module.exports = ConnectionValidator;