zero-http 0.2.4 → 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 +28 -177
  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
@@ -37,6 +37,36 @@ class Request
37
37
 
38
38
  /** Protocol string — `'https'` or `'http'`. */
39
39
  this.protocol = this.secure ? 'https' : 'http';
40
+
41
+ /** URL path without query string. */
42
+ this.path = this.url.split('?')[0];
43
+
44
+ /** Cookies parsed by cookie-parser middleware (populated by middleware). */
45
+ this.cookies = {};
46
+
47
+ /** Request-scoped locals store, shared with response. */
48
+ this.locals = {};
49
+
50
+ /**
51
+ * The original URL as received — never rewritten by middleware.
52
+ * Set by `app.handle()`.
53
+ * @type {string}
54
+ */
55
+ this.originalUrl = req.url;
56
+
57
+ /**
58
+ * The URL path on which the current router was mounted.
59
+ * Empty string at the top level; set by nested routers.
60
+ * @type {string}
61
+ */
62
+ this.baseUrl = '';
63
+
64
+ /**
65
+ * Reference to the parent App instance.
66
+ * Set by `app.handle()`.
67
+ * @type {import('../app')|null}
68
+ */
69
+ this.app = null;
40
70
  }
41
71
 
42
72
  /**
@@ -49,7 +79,27 @@ class Request
49
79
  {
50
80
  const idx = this.url.indexOf('?');
51
81
  if (idx === -1) return {};
52
- return Object.fromEntries(new URLSearchParams(this.url.slice(idx + 1)));
82
+ const query = {};
83
+ const raw = this.url.substring(idx + 1);
84
+ const parts = raw.split('&');
85
+ const limit = Math.min(parts.length, 100); // Max 100 query params to prevent DoS
86
+ for (let i = 0; i < limit; i++)
87
+ {
88
+ const eqIdx = parts[i].indexOf('=');
89
+ if (eqIdx === -1)
90
+ {
91
+ try { query[decodeURIComponent(parts[i])] = ''; } catch (e) { }
92
+ }
93
+ else
94
+ {
95
+ try
96
+ {
97
+ query[decodeURIComponent(parts[i].substring(0, eqIdx))] =
98
+ decodeURIComponent(parts[i].substring(eqIdx + 1));
99
+ } catch (e) { }
100
+ }
101
+ }
102
+ return query;
53
103
  }
54
104
 
55
105
  /**
@@ -77,6 +127,174 @@ class Request
77
127
  }
78
128
  return ct.indexOf(type) !== -1;
79
129
  }
130
+
131
+ /**
132
+ * Get the hostname from the Host header (without port).
133
+ * Respects X-Forwarded-Host when behind a proxy.
134
+ * @returns {string|undefined}
135
+ */
136
+ get hostname()
137
+ {
138
+ const host = this.headers['x-forwarded-host'] || this.headers['host'] || '';
139
+ // Remove port if present
140
+ const idx = host.indexOf(':');
141
+ return idx !== -1 ? host.slice(0, idx) : host;
142
+ }
143
+
144
+ /**
145
+ * Get the subdomains as an array (e.g. ['api', 'v2'] for 'v2.api.example.com').
146
+ * @param {number} [offset=2] - Number of dot-separated parts to remove from the right as TLD.
147
+ * @returns {string[]}
148
+ */
149
+ subdomains(offset = 2)
150
+ {
151
+ const host = this.hostname || '';
152
+ const parts = host.split('.');
153
+ return parts.slice(0, Math.max(0, parts.length - offset)).reverse();
154
+ }
155
+
156
+ /**
157
+ * Content negotiation — check if the client accepts the given type(s).
158
+ * Returns the best match, or `false` if none match.
159
+ *
160
+ * @param {...string} types - MIME types to check (e.g. 'json', 'html', 'text/plain').
161
+ * @returns {string|false} Best matching type, or `false`.
162
+ *
163
+ * @example
164
+ * req.accepts('json', 'html') // => 'json' or 'html' or false
165
+ */
166
+ accepts(...types)
167
+ {
168
+ const accept = this.headers['accept'] || '*/*';
169
+ const mimeMap = {
170
+ json: 'application/json',
171
+ html: 'text/html',
172
+ text: 'text/plain',
173
+ xml: 'application/xml',
174
+ css: 'text/css',
175
+ js: 'application/javascript',
176
+ };
177
+
178
+ // Hoist wildcard check outside loop
179
+ if (accept === '*/*' || accept.indexOf('*/*') !== -1) return types[0] || false;
180
+
181
+ for (let i = 0; i < types.length; i++)
182
+ {
183
+ const t = types[i];
184
+ const mime = mimeMap[t] || t;
185
+ if (accept.indexOf(mime) !== -1) return t;
186
+ const slashIdx = mime.indexOf('/');
187
+ if (slashIdx !== -1 && accept.indexOf(mime.substring(0, slashIdx) + '/*') !== -1) return t;
188
+ }
189
+ return false;
190
+ }
191
+
192
+ /**
193
+ * Check if the request is "fresh" (client cache is still valid).
194
+ * Compares If-None-Match / If-Modified-Since with ETag / Last-Modified.
195
+ * @returns {boolean}
196
+ */
197
+ get fresh()
198
+ {
199
+ const method = this.method;
200
+ if (method !== 'GET' && method !== 'HEAD') return false;
201
+
202
+ const noneMatch = this.headers['if-none-match'];
203
+ const modifiedSince = this.headers['if-modified-since'];
204
+
205
+ if (!noneMatch && !modifiedSince) return false;
206
+
207
+ // Check against response ETag if available
208
+ if (noneMatch && this._res)
209
+ {
210
+ const etag = this._res.get('ETag');
211
+ if (etag && noneMatch === etag) return true;
212
+ }
213
+
214
+ // Check against response Last-Modified if available
215
+ if (modifiedSince && this._res)
216
+ {
217
+ const lastMod = this._res.get('Last-Modified');
218
+ if (lastMod)
219
+ {
220
+ const since = Date.parse(modifiedSince);
221
+ const mod = Date.parse(lastMod);
222
+ if (!isNaN(since) && !isNaN(mod) && mod <= since) return true;
223
+ }
224
+ }
225
+
226
+ return false;
227
+ }
228
+
229
+ /**
230
+ * Inverse of `fresh`.
231
+ * @returns {boolean}
232
+ */
233
+ get stale()
234
+ {
235
+ return !this.fresh;
236
+ }
237
+
238
+ /**
239
+ * Check whether this request was made with XMLHttpRequest.
240
+ * @returns {boolean}
241
+ */
242
+ get xhr()
243
+ {
244
+ const val = this.headers['x-requested-with'] || '';
245
+ return val.toLowerCase() === 'xmlhttprequest';
246
+ }
247
+
248
+ /**
249
+ * Parse the Range header.
250
+ * @param {number} size - Total size of the resource in bytes.
251
+ * @returns {{ type: string, ranges: { start: number, end: number }[] } | -1 | -2}
252
+ * Returns the parsed ranges, -1 for unsatisfiable ranges, or -2 for malformed header.
253
+ */
254
+ range(size)
255
+ {
256
+ const header = this.headers['range'];
257
+ if (!header) return -2;
258
+
259
+ const match = /^(\w+)=(.+)$/.exec(header);
260
+ if (!match) return -2;
261
+
262
+ const type = match[1];
263
+ const ranges = [];
264
+
265
+ for (const part of match[2].split(','))
266
+ {
267
+ const trimmed = part.trim();
268
+ const dashIdx = trimmed.indexOf('-');
269
+ if (dashIdx === -1) return -2;
270
+
271
+ const startStr = trimmed.slice(0, dashIdx).trim();
272
+ const endStr = trimmed.slice(dashIdx + 1).trim();
273
+
274
+ let start, end;
275
+ if (startStr === '')
276
+ {
277
+ // Suffix range: -500 means last 500 bytes
278
+ const suffix = parseInt(endStr, 10);
279
+ if (isNaN(suffix)) return -2;
280
+ start = Math.max(0, size - suffix);
281
+ end = size - 1;
282
+ }
283
+ else
284
+ {
285
+ start = parseInt(startStr, 10);
286
+ end = endStr === '' ? size - 1 : parseInt(endStr, 10);
287
+ if (isNaN(start) || isNaN(end)) return -2;
288
+ }
289
+
290
+ if (start > end || start >= size) return -1;
291
+ end = Math.min(end, size - 1);
292
+ ranges.push({ start, end });
293
+ }
294
+
295
+ if (ranges.length === 0) return -1;
296
+ return { type, ranges };
297
+ }
80
298
  }
81
299
 
82
300
  module.exports = Request;