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.
- package/README.md +1250 -283
- package/documentation/config/db.js +25 -0
- package/documentation/config/middleware.js +44 -0
- package/documentation/config/tls.js +12 -0
- package/documentation/controllers/cookies.js +34 -0
- package/documentation/controllers/tasks.js +108 -0
- package/documentation/full-server.js +28 -177
- package/documentation/models/Task.js +21 -0
- package/documentation/public/data/api.json +404 -24
- package/documentation/public/data/docs.json +1139 -0
- package/documentation/public/data/examples.json +80 -2
- package/documentation/public/data/options.json +23 -8
- package/documentation/public/index.html +138 -99
- package/documentation/public/scripts/app.js +1 -3
- package/documentation/public/scripts/custom-select.js +189 -0
- package/documentation/public/scripts/data-sections.js +233 -250
- package/documentation/public/scripts/playground.js +270 -0
- package/documentation/public/scripts/ui.js +4 -3
- package/documentation/public/styles.css +56 -5
- package/documentation/public/vendor/icons/compress.svg +17 -17
- package/documentation/public/vendor/icons/database.svg +21 -0
- package/documentation/public/vendor/icons/env.svg +21 -0
- package/documentation/public/vendor/icons/fetch.svg +11 -14
- package/documentation/public/vendor/icons/security.svg +15 -0
- package/documentation/public/vendor/icons/sse.svg +12 -13
- package/documentation/public/vendor/icons/static.svg +12 -26
- package/documentation/public/vendor/icons/stream.svg +7 -13
- package/documentation/public/vendor/icons/validate.svg +17 -0
- package/documentation/routes/api.js +41 -0
- package/documentation/routes/core.js +20 -0
- package/documentation/routes/playground.js +29 -0
- package/documentation/routes/realtime.js +49 -0
- package/documentation/routes/uploads.js +71 -0
- package/index.js +62 -1
- package/lib/app.js +200 -8
- package/lib/body/json.js +28 -5
- package/lib/body/multipart.js +29 -1
- package/lib/body/raw.js +1 -1
- package/lib/body/sendError.js +1 -0
- package/lib/body/text.js +1 -1
- package/lib/body/typeMatch.js +6 -2
- package/lib/body/urlencoded.js +5 -2
- package/lib/debug.js +345 -0
- package/lib/env/index.js +440 -0
- package/lib/errors.js +231 -0
- package/lib/http/request.js +219 -1
- package/lib/http/response.js +410 -6
- package/lib/middleware/compress.js +39 -6
- package/lib/middleware/cookieParser.js +237 -0
- package/lib/middleware/cors.js +13 -2
- package/lib/middleware/csrf.js +135 -0
- package/lib/middleware/errorHandler.js +90 -0
- package/lib/middleware/helmet.js +176 -0
- package/lib/middleware/index.js +7 -2
- package/lib/middleware/rateLimit.js +12 -1
- package/lib/middleware/requestId.js +54 -0
- package/lib/middleware/static.js +95 -11
- package/lib/middleware/timeout.js +72 -0
- package/lib/middleware/validator.js +257 -0
- package/lib/orm/adapters/json.js +215 -0
- package/lib/orm/adapters/memory.js +383 -0
- package/lib/orm/adapters/mongo.js +444 -0
- package/lib/orm/adapters/mysql.js +272 -0
- package/lib/orm/adapters/postgres.js +394 -0
- package/lib/orm/adapters/sql-base.js +142 -0
- package/lib/orm/adapters/sqlite.js +311 -0
- package/lib/orm/index.js +276 -0
- package/lib/orm/model.js +895 -0
- package/lib/orm/query.js +807 -0
- package/lib/orm/schema.js +172 -0
- package/lib/router/index.js +136 -47
- package/lib/sse/stream.js +15 -3
- package/lib/ws/connection.js +19 -3
- package/lib/ws/handshake.js +3 -0
- package/lib/ws/index.js +3 -1
- package/lib/ws/room.js +222 -0
- package/package.json +15 -5
- package/types/app.d.ts +120 -0
- package/types/env.d.ts +80 -0
- package/types/errors.d.ts +147 -0
- package/types/fetch.d.ts +43 -0
- package/types/index.d.ts +135 -0
- package/types/middleware.d.ts +292 -0
- package/types/orm.d.ts +610 -0
- package/types/request.d.ts +99 -0
- package/types/response.d.ts +142 -0
- package/types/router.d.ts +78 -0
- package/types/sse.d.ts +78 -0
- package/types/websocket.d.ts +119 -0
package/lib/http/request.js
CHANGED
|
@@ -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
|
-
|
|
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;
|