zero-http 0.2.5 → 0.3.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.
Files changed (89) hide show
  1. package/README.md +1250 -283
  2. package/documentation/config/db.js +25 -0
  3. package/documentation/config/middleware.js +44 -0
  4. package/documentation/config/tls.js +12 -0
  5. package/documentation/controllers/cookies.js +34 -0
  6. package/documentation/controllers/tasks.js +108 -0
  7. package/documentation/full-server.js +25 -184
  8. package/documentation/models/Task.js +21 -0
  9. package/documentation/public/data/api.json +404 -24
  10. package/documentation/public/data/docs.json +1139 -0
  11. package/documentation/public/data/examples.json +80 -2
  12. package/documentation/public/data/options.json +23 -8
  13. package/documentation/public/index.html +138 -99
  14. package/documentation/public/scripts/app.js +1 -3
  15. package/documentation/public/scripts/custom-select.js +189 -0
  16. package/documentation/public/scripts/data-sections.js +233 -250
  17. package/documentation/public/scripts/playground.js +270 -0
  18. package/documentation/public/scripts/ui.js +4 -3
  19. package/documentation/public/styles.css +56 -5
  20. package/documentation/public/vendor/icons/compress.svg +17 -17
  21. package/documentation/public/vendor/icons/database.svg +21 -0
  22. package/documentation/public/vendor/icons/env.svg +21 -0
  23. package/documentation/public/vendor/icons/fetch.svg +11 -14
  24. package/documentation/public/vendor/icons/security.svg +15 -0
  25. package/documentation/public/vendor/icons/sse.svg +12 -13
  26. package/documentation/public/vendor/icons/static.svg +12 -26
  27. package/documentation/public/vendor/icons/stream.svg +7 -13
  28. package/documentation/public/vendor/icons/validate.svg +17 -0
  29. package/documentation/routes/api.js +41 -0
  30. package/documentation/routes/core.js +20 -0
  31. package/documentation/routes/playground.js +29 -0
  32. package/documentation/routes/realtime.js +49 -0
  33. package/documentation/routes/uploads.js +71 -0
  34. package/index.js +62 -1
  35. package/lib/app.js +200 -8
  36. package/lib/body/json.js +28 -5
  37. package/lib/body/multipart.js +29 -1
  38. package/lib/body/raw.js +1 -1
  39. package/lib/body/sendError.js +1 -0
  40. package/lib/body/text.js +1 -1
  41. package/lib/body/typeMatch.js +6 -2
  42. package/lib/body/urlencoded.js +5 -2
  43. package/lib/debug.js +345 -0
  44. package/lib/env/index.js +440 -0
  45. package/lib/errors.js +231 -0
  46. package/lib/http/request.js +219 -1
  47. package/lib/http/response.js +410 -6
  48. package/lib/middleware/compress.js +39 -6
  49. package/lib/middleware/cookieParser.js +237 -0
  50. package/lib/middleware/cors.js +13 -2
  51. package/lib/middleware/csrf.js +135 -0
  52. package/lib/middleware/errorHandler.js +90 -0
  53. package/lib/middleware/helmet.js +176 -0
  54. package/lib/middleware/index.js +7 -2
  55. package/lib/middleware/rateLimit.js +12 -1
  56. package/lib/middleware/requestId.js +54 -0
  57. package/lib/middleware/static.js +95 -11
  58. package/lib/middleware/timeout.js +72 -0
  59. package/lib/middleware/validator.js +257 -0
  60. package/lib/orm/adapters/json.js +215 -0
  61. package/lib/orm/adapters/memory.js +383 -0
  62. package/lib/orm/adapters/mongo.js +444 -0
  63. package/lib/orm/adapters/mysql.js +272 -0
  64. package/lib/orm/adapters/postgres.js +394 -0
  65. package/lib/orm/adapters/sql-base.js +142 -0
  66. package/lib/orm/adapters/sqlite.js +311 -0
  67. package/lib/orm/index.js +276 -0
  68. package/lib/orm/model.js +895 -0
  69. package/lib/orm/query.js +807 -0
  70. package/lib/orm/schema.js +172 -0
  71. package/lib/router/index.js +136 -47
  72. package/lib/sse/stream.js +15 -3
  73. package/lib/ws/connection.js +19 -3
  74. package/lib/ws/handshake.js +3 -0
  75. package/lib/ws/index.js +3 -1
  76. package/lib/ws/room.js +222 -0
  77. package/package.json +15 -5
  78. package/types/app.d.ts +120 -0
  79. package/types/env.d.ts +80 -0
  80. package/types/errors.d.ts +147 -0
  81. package/types/fetch.d.ts +43 -0
  82. package/types/index.d.ts +135 -0
  83. package/types/middleware.d.ts +292 -0
  84. package/types/orm.d.ts +610 -0
  85. package/types/request.d.ts +99 -0
  86. package/types/response.d.ts +142 -0
  87. package/types/router.d.ts +78 -0
  88. package/types/sse.d.ts +78 -0
  89. package/types/websocket.d.ts +119 -0
@@ -0,0 +1,172 @@
1
+ /**
2
+ * @module orm/schema
3
+ * @description Schema definition and validation for ORM models.
4
+ * Validates data against column definitions, coerces types,
5
+ * and enforces constraints (required, unique, min, max, enum, match).
6
+ */
7
+
8
+ /**
9
+ * Supported column types.
10
+ * @enum {string}
11
+ */
12
+ const TYPES = {
13
+ STRING: 'string',
14
+ INTEGER: 'integer',
15
+ FLOAT: 'float',
16
+ BOOLEAN: 'boolean',
17
+ DATE: 'date',
18
+ DATETIME: 'datetime',
19
+ JSON: 'json',
20
+ TEXT: 'text',
21
+ BLOB: 'blob',
22
+ UUID: 'uuid',
23
+ };
24
+
25
+ /**
26
+ * Validate and sanitise a single value against a column definition.
27
+ *
28
+ * @param {*} value - Raw input value.
29
+ * @param {object} colDef - Column definition.
30
+ * @param {string} colName - Column name (for error messages).
31
+ * @returns {*} Coerced value.
32
+ * @throws {Error} On validation failure.
33
+ */
34
+ function validateValue(value, colDef, colName)
35
+ {
36
+ const type = colDef.type || 'string';
37
+
38
+ // Handle null/undefined
39
+ if (value === undefined || value === null)
40
+ {
41
+ if (colDef.required && colDef.default === undefined)
42
+ throw new Error(`"${colName}" is required`);
43
+ if (colDef.default !== undefined)
44
+ return typeof colDef.default === 'function' ? colDef.default() : colDef.default;
45
+ return colDef.nullable !== false ? null : undefined;
46
+ }
47
+
48
+ switch (type)
49
+ {
50
+ case 'string':
51
+ case 'text':
52
+ {
53
+ const val = String(value);
54
+ if (colDef.minLength !== undefined && val.length < colDef.minLength)
55
+ throw new Error(`"${colName}" must be at least ${colDef.minLength} characters`);
56
+ if (colDef.maxLength !== undefined && val.length > colDef.maxLength)
57
+ throw new Error(`"${colName}" must be at most ${colDef.maxLength} characters`);
58
+ if (colDef.match && !colDef.match.test(val))
59
+ throw new Error(`"${colName}" does not match pattern ${colDef.match}`);
60
+ if (colDef.enum && !colDef.enum.includes(val))
61
+ throw new Error(`"${colName}" must be one of [${colDef.enum.join(', ')}]`);
62
+ // Sanitise: prevent SQL-like injection patterns in string values
63
+ return val;
64
+ }
65
+ case 'integer':
66
+ {
67
+ const val = typeof value === 'string' ? parseInt(value, 10) : Math.floor(Number(value));
68
+ if (isNaN(val)) throw new Error(`"${colName}" must be an integer`);
69
+ if (colDef.min !== undefined && val < colDef.min)
70
+ throw new Error(`"${colName}" must be >= ${colDef.min}`);
71
+ if (colDef.max !== undefined && val > colDef.max)
72
+ throw new Error(`"${colName}" must be <= ${colDef.max}`);
73
+ return val;
74
+ }
75
+ case 'float':
76
+ {
77
+ const val = Number(value);
78
+ if (isNaN(val)) throw new Error(`"${colName}" must be a number`);
79
+ if (colDef.min !== undefined && val < colDef.min)
80
+ throw new Error(`"${colName}" must be >= ${colDef.min}`);
81
+ if (colDef.max !== undefined && val > colDef.max)
82
+ throw new Error(`"${colName}" must be <= ${colDef.max}`);
83
+ return val;
84
+ }
85
+ case 'boolean':
86
+ {
87
+ if (typeof value === 'boolean') return value;
88
+ if (typeof value === 'string')
89
+ {
90
+ const lower = value.toLowerCase();
91
+ if (['true', '1', 'yes'].includes(lower)) return true;
92
+ if (['false', '0', 'no'].includes(lower)) return false;
93
+ }
94
+ if (typeof value === 'number') return value !== 0;
95
+ throw new Error(`"${colName}" must be a boolean`);
96
+ }
97
+ case 'date':
98
+ case 'datetime':
99
+ {
100
+ if (value instanceof Date) return value;
101
+ const d = new Date(value);
102
+ if (isNaN(d.getTime())) throw new Error(`"${colName}" must be a valid date`);
103
+ return d;
104
+ }
105
+ case 'json':
106
+ {
107
+ if (typeof value === 'string')
108
+ {
109
+ try { return JSON.parse(value); }
110
+ catch (e) { throw new Error(`"${colName}" must be valid JSON`); }
111
+ }
112
+ // Already an object/array — return as-is for storage
113
+ return value;
114
+ }
115
+ case 'uuid':
116
+ {
117
+ const val = String(value);
118
+ if (!/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(val))
119
+ throw new Error(`"${colName}" must be a valid UUID`);
120
+ return val;
121
+ }
122
+ case 'blob':
123
+ return Buffer.isBuffer(value) ? value : Buffer.from(value);
124
+ default:
125
+ return value;
126
+ }
127
+ }
128
+
129
+ /**
130
+ * Validate all columns of a data object against the schema.
131
+ *
132
+ * @param {object} data - Input data object.
133
+ * @param {object} columns - Schema column definitions.
134
+ * @param {object} [options]
135
+ * @param {boolean} [options.partial=false] - When true, only validates provided fields (for updates).
136
+ * @returns {{ valid: boolean, errors: string[], sanitized: object }}
137
+ */
138
+ function validate(data, columns, options = {})
139
+ {
140
+ const errors = [];
141
+ const sanitized = {};
142
+
143
+ for (const [colName, colDef] of Object.entries(columns))
144
+ {
145
+ // Skip auto fields on create
146
+ if (colDef.primaryKey && colDef.autoIncrement && data[colName] === undefined) continue;
147
+
148
+ if (options.partial && data[colName] === undefined) continue;
149
+
150
+ try
151
+ {
152
+ sanitized[colName] = validateValue(data[colName], colDef, colName);
153
+ }
154
+ catch (e)
155
+ {
156
+ errors.push(e.message);
157
+ }
158
+ }
159
+
160
+ // Reject unknown keys (prevent mass-assignment)
161
+ for (const key of Object.keys(data))
162
+ {
163
+ if (!columns[key])
164
+ {
165
+ errors.push(`Unknown column "${key}"`);
166
+ }
167
+ }
168
+
169
+ return { valid: errors.length === 0, errors, sanitized };
170
+ }
171
+
172
+ module.exports = { TYPES, validateValue, validate };
@@ -5,6 +5,8 @@
5
5
  * mounting, and route introspection.
6
6
  */
7
7
 
8
+ const log = require('../debug')('zero:router');
9
+
8
10
  /**
9
11
  * Convert a route path pattern into a RegExp and extract named parameter keys.
10
12
  * Supports `:param` segments and trailing `*` wildcards.
@@ -56,6 +58,11 @@ class Router
56
58
  this.routes = [];
57
59
  /** @type {{ prefix: string, router: Router }[]} */
58
60
  this._children = [];
61
+ /**
62
+ * Parameter pre-processing handlers (set by parent App).
63
+ * @type {Object<string, Function[]>}
64
+ */
65
+ this._paramHandlers = {};
59
66
  }
60
67
 
61
68
  /**
@@ -74,6 +81,7 @@ class Router
74
81
  const entry = { method: method.toUpperCase(), path, regex, keys, handlers };
75
82
  if (options.secure !== undefined) entry.secure = !!options.secure;
76
83
  this.routes.push(entry);
84
+ log.debug('route added %s %s', method.toUpperCase(), path);
77
85
  }
78
86
 
79
87
  /**
@@ -90,6 +98,7 @@ class Router
90
98
  {
91
99
  const cleanPrefix = prefix.endsWith('/') ? prefix.slice(0, -1) : prefix;
92
100
  this._children.push({ prefix: cleanPrefix, router });
101
+ log.debug('mounted child router at %s', cleanPrefix);
93
102
  }
94
103
  }
95
104
 
@@ -103,46 +112,10 @@ class Router
103
112
  */
104
113
  handle(req, res)
105
114
  {
106
- const method = req.method.toUpperCase();
107
- const url = req.url.split('?')[0];
108
-
109
- // Try own routes first
110
- for (const r of this.routes)
115
+ if (!this._matchAndExecute(req, res))
111
116
  {
112
- if (r.method !== 'ALL' && r.method !== method) continue;
113
- // Protocol-aware matching: skip if secure flag doesn't match
114
- if (r.secure === true && !req.secure) continue;
115
- if (r.secure === false && req.secure) continue;
116
- const m = url.match(r.regex);
117
- if (!m) continue;
118
- req.params = {};
119
- r.keys.forEach((k, i) => req.params[k] = decodeURIComponent(m[i + 1] || ''));
120
- let idx = 0;
121
- const next = () =>
122
- {
123
- if (idx < r.handlers.length)
124
- {
125
- const h = r.handlers[idx++];
126
- return h(req, res, next);
127
- }
128
- };
129
- return next();
117
+ res.status(404).json({ error: 'Not Found' });
130
118
  }
131
-
132
- // Try child routers
133
- for (const child of this._children)
134
- {
135
- if (url === child.prefix || url.startsWith(child.prefix + '/'))
136
- {
137
- const origUrl = req.url;
138
- req.url = req.url.slice(child.prefix.length) || '/';
139
- const found = child.router._tryHandle(req, res);
140
- if (found) return;
141
- req.url = origUrl; // restore if child didn't match
142
- }
143
- }
144
-
145
- res.status(404).json({ error: 'Not Found' });
146
119
  }
147
120
 
148
121
  /**
@@ -155,42 +128,129 @@ class Router
155
128
  * @private
156
129
  */
157
130
  _tryHandle(req, res)
131
+ {
132
+ return this._matchAndExecute(req, res);
133
+ }
134
+
135
+ /**
136
+ * Shared route matching and handler execution.
137
+ * Returns `true` if a route matched (handler invoked), `false` otherwise.
138
+ *
139
+ * @param {import('./request')} req
140
+ * @param {import('./response')} res
141
+ * @returns {boolean}
142
+ * @private
143
+ */
144
+ _matchAndExecute(req, res)
158
145
  {
159
146
  const method = req.method.toUpperCase();
160
147
  const url = req.url.split('?')[0];
148
+ log.debug('%s %s', method, url);
161
149
 
162
- for (const r of this.routes)
150
+ // Try own routes first
151
+ for (let ri = 0; ri < this.routes.length; ri++)
163
152
  {
153
+ const r = this.routes[ri];
164
154
  if (r.method !== 'ALL' && r.method !== method) continue;
165
- // Protocol-aware matching: skip if secure flag doesn't match
166
155
  if (r.secure === true && !req.secure) continue;
167
156
  if (r.secure === false && req.secure) continue;
168
157
  const m = url.match(r.regex);
169
158
  if (!m) continue;
170
159
  req.params = {};
171
- r.keys.forEach((k, i) => req.params[k] = decodeURIComponent(m[i + 1] || ''));
160
+ for (let i = 0; i < r.keys.length; i++)
161
+ {
162
+ req.params[r.keys[i]] = decodeURIComponent(m[i + 1] || '');
163
+ }
164
+
165
+ // Run param pre-processing handlers
166
+ const paramHandlers = this._paramHandlers || {};
167
+ let paramKeys;
168
+ let paramCount = 0;
169
+ for (let i = 0; i < r.keys.length; i++)
170
+ {
171
+ if (paramHandlers[r.keys[i]])
172
+ {
173
+ if (!paramKeys) paramKeys = [];
174
+ paramKeys.push(r.keys[i]);
175
+ paramCount++;
176
+ }
177
+ }
178
+
179
+ let pIdx = 0;
180
+ const runParams = () =>
181
+ {
182
+ if (pIdx < paramCount)
183
+ {
184
+ const pk = paramKeys[pIdx++];
185
+ const fns = paramHandlers[pk];
186
+ let fIdx = 0;
187
+ const nextParam = () =>
188
+ {
189
+ if (fIdx < fns.length)
190
+ {
191
+ const fn = fns[fIdx++];
192
+ try
193
+ {
194
+ const result = fn(req, res, nextParam, req.params[pk]);
195
+ if (result && typeof result.catch === 'function')
196
+ {
197
+ result.catch(e => this._handleRouteError(e, req, res));
198
+ }
199
+ }
200
+ catch (e) { this._handleRouteError(e, req, res); }
201
+ }
202
+ else { runParams(); }
203
+ };
204
+ nextParam();
205
+ }
206
+ else { runHandlers(); }
207
+ };
208
+
172
209
  let idx = 0;
173
- const next = () =>
210
+ const runHandlers = () =>
174
211
  {
175
212
  if (idx < r.handlers.length)
176
213
  {
177
214
  const h = r.handlers[idx++];
178
- return h(req, res, next);
215
+ try
216
+ {
217
+ const result = h(req, res, runHandlers);
218
+ if (result && typeof result.catch === 'function')
219
+ {
220
+ result.catch(e => this._handleRouteError(e, req, res));
221
+ }
222
+ }
223
+ catch (e)
224
+ {
225
+ this._handleRouteError(e, req, res);
226
+ }
179
227
  }
180
228
  };
181
- next();
229
+
230
+ if (paramCount > 0) runParams();
231
+ else runHandlers();
182
232
  return true;
183
233
  }
184
234
 
185
- for (const child of this._children)
235
+ // Try child routers
236
+ for (let ci = 0; ci < this._children.length; ci++)
186
237
  {
238
+ const child = this._children[ci];
187
239
  if (url === child.prefix || url.startsWith(child.prefix + '/'))
188
240
  {
189
241
  const origUrl = req.url;
242
+ const origBaseUrl = req.baseUrl || '';
243
+ req.baseUrl = origBaseUrl + child.prefix;
190
244
  req.url = req.url.slice(child.prefix.length) || '/';
191
- const found = child.router._tryHandle(req, res);
192
- if (found) return true;
245
+ child.router._paramHandlers = this._paramHandlers;
246
+ try
247
+ {
248
+ const found = child.router._matchAndExecute(req, res);
249
+ if (found) return true;
250
+ }
251
+ catch (e) { this._handleRouteError(e, req, res); return true; }
193
252
  req.url = origUrl;
253
+ req.baseUrl = origBaseUrl;
194
254
  }
195
255
  }
196
256
 
@@ -249,6 +309,35 @@ class Router
249
309
 
250
310
  // -- Introspection -----------------------------------
251
311
 
312
+ /**
313
+ * Handle an error thrown by a route handler.
314
+ * Delegates to the app-level error handler if available, otherwise
315
+ * sends a generic 500 JSON response.
316
+ *
317
+ * @param {Error} err
318
+ * @param {import('../http/request')} req
319
+ * @param {import('../http/response')} res
320
+ * @private
321
+ */
322
+ _handleRouteError(err, req, res)
323
+ {
324
+ log.error('route error: %s', err.message || err);
325
+ // Check if the app has an error handler (set via app.onError())
326
+ if (req.app && req.app._errorHandler)
327
+ {
328
+ return req.app._errorHandler(err, req, res, () => {});
329
+ }
330
+ const statusCode = err.statusCode || err.status || 500;
331
+ if (!res.headersSent && !(res.raw && res.raw.headersSent))
332
+ {
333
+ res.status(statusCode).json(
334
+ typeof err.toJSON === 'function'
335
+ ? err.toJSON()
336
+ : { error: err.message || 'Internal Server Error' }
337
+ );
338
+ }
339
+ }
340
+
252
341
  /**
253
342
  * Return a flat list of all registered routes, including those in
254
343
  * mounted child routers. Useful for debugging or auto-documentation.
package/lib/sse/stream.js CHANGED
@@ -8,6 +8,8 @@
8
8
  * - `lastEventId` {string|null} The `Last-Event-ID` header from the client reconnection.
9
9
  * - `eventCount` {number} Total events sent on this stream.
10
10
  * - `bytesSent` {number} Total bytes written to the stream.
11
+ *
12
+ * @requires ../debug
11
13
  * - `connectedAt` {number} Timestamp (ms) when the stream was opened.
12
14
  * - `uptime` {number} Milliseconds since the stream was opened (computed).
13
15
  * - `data` {object} Arbitrary user-data store.
@@ -26,6 +28,7 @@ class SSEStream
26
28
  {
27
29
  this._raw = raw;
28
30
  this._closed = false;
31
+ this._log = require('../debug')('zero:sse');
29
32
 
30
33
  /** `true` when the underlying connection is over TLS (HTTPS). */
31
34
  this.secure = !!opts.secure;
@@ -67,10 +70,11 @@ class SSEStream
67
70
  {
68
71
  this._closed = true;
69
72
  this._clearKeepAlive();
73
+ this._log.debug('stream closed, %d events sent', this.eventCount);
70
74
  this._emit('close');
71
75
  });
72
76
 
73
- raw.on('error', (err) => this._emit('error', err));
77
+ raw.on('error', (err) => { this._log.error('stream error: %s', err.message); this._emit('error', err); });
74
78
  }
75
79
 
76
80
  // -- Event Emitter ---------------------------------
@@ -171,7 +175,13 @@ class SSEStream
171
175
  */
172
176
  _formatData(data)
173
177
  {
174
- const payload = typeof data === 'object' ? JSON.stringify(data) : String(data);
178
+ let payload;
179
+ if (typeof data === 'object')
180
+ {
181
+ try { payload = JSON.stringify(data); }
182
+ catch (e) { payload = '[Serialization Error]'; }
183
+ }
184
+ else { payload = String(data); }
175
185
  return payload.split('\n').map(line => `data: ${line}\n`).join('');
176
186
  }
177
187
 
@@ -240,7 +250,9 @@ class SSEStream
240
250
  comment(text)
241
251
  {
242
252
  if (this._closed) return this;
243
- this._write(`: ${text}\n\n`);
253
+ // Escape newlines to prevent SSE frame injection
254
+ const safe = String(text).split('\n').join('\n: ');
255
+ this._write(`: ${safe}\n\n`);
244
256
  return this;
245
257
  }
246
258
 
@@ -4,6 +4,8 @@
4
4
  * Implements RFC 6455 framing for text, binary, ping, pong, and close.
5
5
  */
6
6
 
7
+ const log = require('../debug')('zero:ws');
8
+
7
9
  /** Auto-incrementing connection ID counter. */
8
10
  let _wsIdCounter = 0;
9
11
 
@@ -66,6 +68,7 @@ class WebSocketConnection
66
68
 
67
69
  /** Unique connection identifier. */
68
70
  this.id = 'ws_' + (++_wsIdCounter) + '_' + Date.now().toString(36);
71
+ log.info('connection opened id=%s ip=%s', this.id, socket.remoteAddress);
69
72
 
70
73
  /** Current ready state. */
71
74
  this.readyState = WS_READY_STATE.OPEN;
@@ -121,10 +124,11 @@ class WebSocketConnection
121
124
  {
122
125
  this.readyState = WS_READY_STATE.CLOSED;
123
126
  this._clearPing();
127
+ log.info('connection closed id=%s', this.id);
124
128
  this._emit('close', 1006, '');
125
129
  }
126
130
  });
127
- socket.on('error', (err) => this._emit('error', err));
131
+ socket.on('error', (err) => { log.error('socket error id=%s: %s', this.id, err.message); this._emit('error', err); });
128
132
  socket.on('drain', () => this._emit('drain'));
129
133
  }
130
134
 
@@ -229,7 +233,14 @@ class WebSocketConnection
229
233
  */
230
234
  sendJSON(obj, cb)
231
235
  {
232
- return this.send(JSON.stringify(obj), { callback: cb });
236
+ let json;
237
+ try { json = JSON.stringify(obj); }
238
+ catch (e)
239
+ {
240
+ this._emit('error', new Error('Failed to serialize JSON: ' + e.message));
241
+ return false;
242
+ }
243
+ return this.send(json, { callback: cb });
233
244
  }
234
245
 
235
246
  /**
@@ -371,7 +382,12 @@ class WebSocketConnection
371
382
  else if (payloadLen === 127)
372
383
  {
373
384
  if (this._buffer.length < 10) return;
374
- payloadLen = this._buffer.readUInt32BE(6);
385
+ // RFC 6455: 64-bit unsigned integer; read upper and lower 32-bit halves
386
+ const upper = this._buffer.readUInt32BE(2);
387
+ const lower = this._buffer.readUInt32BE(6);
388
+ // Reject frames where upper 32 bits are set (>4GB not supported)
389
+ if (upper > 0) { this.close(1009, 'Message too big'); this._buffer = Buffer.alloc(0); return; }
390
+ payloadLen = lower;
375
391
  offset = 10;
376
392
  }
377
393
 
@@ -7,6 +7,7 @@
7
7
  */
8
8
  const crypto = require('crypto');
9
9
  const WebSocketConnection = require('./connection');
10
+ const log = require('../debug')('zero:ws');
10
11
 
11
12
  /** RFC 6455 magic GUID used in the Sec-WebSocket-Accept hash. */
12
13
  const WS_MAGIC = '258EAFA5-E914-47DA-95CA-C5AB0DC85B11';
@@ -29,6 +30,7 @@ function handleUpgrade(req, socket, head, wsHandlers)
29
30
 
30
31
  if (!entry)
31
32
  {
33
+ log.warn('no WS handler for %s', urlPath);
32
34
  socket.write('HTTP/1.1 404 Not Found\r\n\r\n');
33
35
  socket.destroy();
34
36
  return;
@@ -84,6 +86,7 @@ function handleUpgrade(req, socket, head, wsHandlers)
84
86
 
85
87
  handshake += '\r\n';
86
88
  socket.write(handshake);
89
+ log.info('upgrade complete for %s', urlPath);
87
90
 
88
91
  // -- Parse query string ----------------------------
89
92
  const qIdx = req.url.indexOf('?');
package/lib/ws/index.js CHANGED
@@ -1,12 +1,14 @@
1
1
  /**
2
2
  * @module ws
3
3
  * @description WebSocket support for zero-http.
4
- * Exports the connection class and upgrade handler.
4
+ * Exports the connection class, upgrade handler, and pool manager.
5
5
  */
6
6
  const WebSocketConnection = require('./connection');
7
7
  const handleUpgrade = require('./handshake');
8
+ const WebSocketPool = require('./room');
8
9
 
9
10
  module.exports = {
10
11
  WebSocketConnection,
11
12
  handleUpgrade,
13
+ WebSocketPool,
12
14
  };