hoa 0.3.2 → 0.3.4

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.
@@ -0,0 +1,516 @@
1
+ import { parseSearchParamsToQuery, stringifyQueryToString } from "./lib/utils.mjs";
2
+
3
+ //#region src/request.js
4
+ /**
5
+ * @typedef {Object} ReqJSON
6
+ * @property {string} method - Request method
7
+ * @property {string} url - Request url
8
+ * @property {Record<string, string|string[]>} headers - Request headers
9
+ */
10
+ /**
11
+ * @class HoaRequest
12
+ */
13
+ var HoaRequest = class {
14
+ /**
15
+ * Parsed URL object of the incoming request.
16
+ * This property provides access to all URL components and is lazily initialized.
17
+ *
18
+ * @returns {URL} The parsed URL object with all URL components
19
+ * @public
20
+ */
21
+ get url() {
22
+ return this._url || (this._url = new URL(this.ctx.request.url));
23
+ }
24
+ /**
25
+ * Overwrite the request URL using a string or URL instance.
26
+ * This allows for URL manipulation and rewriting within middleware.
27
+ *
28
+ * @param {string|URL} val - The new URL as a string or URL object
29
+ * @public
30
+ */
31
+ set url(val) {
32
+ if (typeof val === "string") val = new URL(val);
33
+ this._url = val;
34
+ }
35
+ /**
36
+ * Full request URL string including protocol, host, path, query, and hash.
37
+ *
38
+ * @returns {string} The complete URL as a string
39
+ * @public
40
+ */
41
+ get href() {
42
+ return this.url.href;
43
+ }
44
+ /**
45
+ * Replace the full href (origin + path + search + hash).
46
+ * This is a convenience method for completely changing the URL.
47
+ *
48
+ * @param {string} val - The new complete URL string
49
+ * @public
50
+ */
51
+ set href(val) {
52
+ this.url.href = val;
53
+ }
54
+ /**
55
+ * Origin portion of the URL (scheme + host + port).
56
+ *
57
+ * @returns {string} The origin (e.g., 'https://example.com:8443')
58
+ * @public
59
+ */
60
+ get origin() {
61
+ return this.url.origin;
62
+ }
63
+ /**
64
+ * Replace origin while keeping path, search, and hash components.
65
+ * Useful for proxying requests to different hosts.
66
+ *
67
+ * @param {string} val - The new origin (protocol + host + port)
68
+ * @public
69
+ */
70
+ set origin(val) {
71
+ const { pathname, search, hash } = this.url;
72
+ this.url = `${val}${pathname}${search}${hash}`;
73
+ }
74
+ /**
75
+ * URL protocol including the trailing colon (e.g., 'https:').
76
+ *
77
+ * @returns {string} The protocol with colon
78
+ * @public
79
+ */
80
+ get protocol() {
81
+ return this.url.protocol;
82
+ }
83
+ /**
84
+ * Set the URL protocol.
85
+ *
86
+ * @param {string} val - The new protocol (should include colon)
87
+ * @public
88
+ */
89
+ set protocol(val) {
90
+ this.url.protocol = val;
91
+ }
92
+ /**
93
+ * Host with port (if present), e.g., 'example.com:8443'.
94
+ *
95
+ * @returns {string} The host including port if non-standard
96
+ * @public
97
+ */
98
+ get host() {
99
+ return this.url.host;
100
+ }
101
+ /**
102
+ * Set host (may include port).
103
+ *
104
+ * @param {string} val - The new host, optionally with port
105
+ * @public
106
+ */
107
+ set host(val) {
108
+ this.url.host = val;
109
+ }
110
+ /**
111
+ * Hostname without port, e.g., 'example.com'.
112
+ *
113
+ * @returns {string} The hostname without port
114
+ * @public
115
+ */
116
+ get hostname() {
117
+ const hostname = this.url.hostname;
118
+ if (hostname && hostname.startsWith("[") && hostname.endsWith("]")) return hostname.slice(1, -1);
119
+ return hostname;
120
+ }
121
+ /**
122
+ * Set hostname (without port).
123
+ *
124
+ * @param {string} val - The new hostname
125
+ * @public
126
+ */
127
+ set hostname(val) {
128
+ this.url.hostname = val;
129
+ }
130
+ /**
131
+ * Port string, e.g., '443'. Empty string if using default port.
132
+ *
133
+ * @returns {string} The port number as a string, or empty string for default ports
134
+ * @public
135
+ */
136
+ get port() {
137
+ return this.url.port;
138
+ }
139
+ /**
140
+ * Set port string.
141
+ *
142
+ * @param {string} val - The new port number as a string
143
+ * @public
144
+ */
145
+ set port(val) {
146
+ this.url.port = val;
147
+ }
148
+ /**
149
+ * Pathname starting with '/', e.g., '/users/1'.
150
+ *
151
+ * @returns {string} The URL pathname
152
+ * @public
153
+ */
154
+ get pathname() {
155
+ return this.url.pathname;
156
+ }
157
+ /**
158
+ * Set pathname.
159
+ *
160
+ * @param {string} val - The new pathname (should start with '/')
161
+ * @public
162
+ */
163
+ set pathname(val) {
164
+ this.url.pathname = val;
165
+ }
166
+ /**
167
+ * Raw search string including '?', or empty string if no query parameters.
168
+ *
169
+ * @returns {string} The search string with leading '?' or empty string
170
+ * @public
171
+ */
172
+ get search() {
173
+ return this.url.search;
174
+ }
175
+ /**
176
+ * Set raw search string (should begin with '?').
177
+ * This invalidates the cached query object.
178
+ *
179
+ * @param {string} val - The new search string
180
+ * @public
181
+ */
182
+ set search(val) {
183
+ this.url.search = val.startsWith("?") ? val : val ? `?${val}` : "";
184
+ this._query = null;
185
+ }
186
+ /**
187
+ * Hash fragment including '#', or empty string if no hash.
188
+ *
189
+ * @returns {string} The hash fragment with leading '#' or empty string
190
+ * @public
191
+ */
192
+ get hash() {
193
+ return this.url.hash;
194
+ }
195
+ /**
196
+ * Set hash fragment including '#'.
197
+ *
198
+ * @param {string} val - The new hash fragment
199
+ * @public
200
+ */
201
+ set hash(val) {
202
+ this.url.hash = val || "";
203
+ }
204
+ /**
205
+ * HTTP method (typically upper-case in most runtimes).
206
+ *
207
+ * @returns {string} The HTTP method (GET, POST, PUT, DELETE, etc.)
208
+ * @public
209
+ */
210
+ get method() {
211
+ return this._method || (this._method = this.ctx.request.method);
212
+ }
213
+ /**
214
+ * Override request method (useful for method override middlewares).
215
+ * This allows changing the perceived HTTP method for routing purposes.
216
+ *
217
+ * @param {string} val - The new HTTP method
218
+ * @public
219
+ */
220
+ set method(val) {
221
+ this._method = val;
222
+ }
223
+ /**
224
+ * Parsed query object where duplicate keys become arrays.
225
+ * This provides a convenient way to access URL query parameters.
226
+ *
227
+ * @returns {Record<string, string|string[]>} Object with query parameters
228
+ * @public
229
+ */
230
+ get query() {
231
+ return this._query || (this._query = parseSearchParamsToQuery(this.url.searchParams));
232
+ }
233
+ /**
234
+ * Replace query parameters with a plain object.
235
+ * Arrays are serialized with repeated keys.
236
+ *
237
+ * @param {Record<string, string|string[]>} val - New query parameters
238
+ * @public
239
+ */
240
+ set query(val) {
241
+ this.search = stringifyQueryToString(val);
242
+ this._query = null;
243
+ }
244
+ /**
245
+ * Expose a plain object snapshot of request headers.
246
+ * Uses the Web Standard Headers API internally for proper header handling.
247
+ * Returns a plain object representation where header names are normalized.
248
+ *
249
+ * @returns {Record<string, string>} Object with all request headers
250
+ * @public
251
+ */
252
+ get headers() {
253
+ this._headers = this._headers || new Headers(this.ctx.request.headers);
254
+ return Object.fromEntries(this._headers.entries());
255
+ }
256
+ /**
257
+ * Set the request headers.
258
+ * Accepts either a Headers object or a plain object/array of header entries.
259
+ * This replaces all existing headers with the new ones.
260
+ *
261
+ * @param {Headers|Record<string, string>|Array<[string, string]>} val - Headers to set
262
+ * @public
263
+ */
264
+ set headers(val) {
265
+ if (val instanceof Headers) this._headers = val;
266
+ else this._headers = new Headers(val);
267
+ }
268
+ /**
269
+ * Get the request body stream.
270
+ * Returns the underlying ReadableStream from the Web Standard Request.
271
+ * This provides direct access to the request body for custom processing.
272
+ *
273
+ * @returns {ReadableStream|null} The request body stream, or null if not available
274
+ * @public
275
+ */
276
+ get body() {
277
+ return this._body || (this._body = this.ctx.request.body);
278
+ }
279
+ /**
280
+ * Set a custom request body stream.
281
+ * This allows middleware to replace the request body with a custom stream.
282
+ *
283
+ * @param {any} val - The new request body
284
+ * @public
285
+ */
286
+ set body(val) {
287
+ this._body = val;
288
+ }
289
+ /**
290
+ * Get a request header value by name (case-insensitive).
291
+ * Uses the Web Standard Headers API for proper header retrieval.
292
+ * Special-cases "referer/referrer" for compatibility.
293
+ *
294
+ * @param {string} field - The header name
295
+ * @returns {string|null} The header value or null if not found
296
+ * @public
297
+ */
298
+ get(field) {
299
+ if (!field) return null;
300
+ this._headers = this._headers || new Headers(this.ctx.request.headers);
301
+ switch (field = field.toLowerCase()) {
302
+ case "referer":
303
+ case "referrer": return this._headers.get("referrer") ?? this._headers.get("referer");
304
+ default: return this._headers.get(field);
305
+ }
306
+ }
307
+ /**
308
+ * Get all Set-Cookie header values as an array.
309
+ * Returns all Set-Cookie headers from the request (useful for proxying).
310
+ *
311
+ * @returns {string[]} Array of Set-Cookie header values
312
+ * @public
313
+ */
314
+ getSetCookie() {
315
+ this._headers = this._headers || new Headers(this.ctx.request.headers);
316
+ return this._headers.getSetCookie();
317
+ }
318
+ /**
319
+ * Check if a request header is present.
320
+ * Uses the Web Standard Headers API for case-insensitive header checking.
321
+ *
322
+ * @param {string} field - The header name to check
323
+ * @returns {boolean} True if the header exists
324
+ * @public
325
+ */
326
+ has(field) {
327
+ if (!field) return false;
328
+ this._headers = this._headers || new Headers(this.ctx.request.headers);
329
+ return this._headers.has(field);
330
+ }
331
+ /**
332
+ * Set request header(s) using the Web Standard Headers API.
333
+ * Accepts various input formats:
334
+ * - Single key/value pair
335
+ * - Plain object with multiple headers
336
+ * Replaces existing header values.
337
+ *
338
+ * @param {string|Record<string,string>} field - Header name or headers object
339
+ * @param {string} [val] - Header value when field is a string
340
+ * @public
341
+ */
342
+ set(field, val) {
343
+ if (!field) return;
344
+ this._headers = this._headers || new Headers(this.ctx.request.headers);
345
+ if (typeof field === "string") this._headers.set(field, val);
346
+ else Object.keys(field).forEach((header) => this._headers.set(header, field[header]));
347
+ }
348
+ /**
349
+ * Append a request header value using the Web Standard Headers API.
350
+ * Does not replace existing values, but appends to them.
351
+ * This is useful for headers that can have multiple values.
352
+ *
353
+ * @param {string|Record<string,string>} field - The header name or headers object
354
+ * @param {string} [val] - The value to append when field is a string
355
+ * @public
356
+ */
357
+ append(field, val) {
358
+ if (!field) return;
359
+ this._headers = this._headers || new Headers(this.ctx.request.headers);
360
+ if (typeof field === "string") this._headers.append(field, val);
361
+ else Object.keys(field).forEach((header) => this._headers.append(header, field[header]));
362
+ }
363
+ /**
364
+ * Delete a request header by name using the Web Standard Headers API.
365
+ * Header deletion is case-insensitive.
366
+ *
367
+ * @param {string} field - The header name to delete
368
+ * @public
369
+ */
370
+ delete(field) {
371
+ if (!field) return;
372
+ this._headers = this._headers || new Headers(this.ctx.request.headers);
373
+ this._headers.delete(field);
374
+ }
375
+ /**
376
+ * Get the client IP addresses from the X-Forwarded-For header.
377
+ * Returns an array of IP addresses in order from client to proxy.
378
+ * If X-Forwarded-For is not available, returns array with single IP from get ip().
379
+ *
380
+ * @returns {string[]} Array of IP addresses, or array with single IP if X-Forwarded-For not found
381
+ * @public
382
+ */
383
+ get ips() {
384
+ const xForwardedFor = this.get("x-forwarded-for");
385
+ if (!xForwardedFor) {
386
+ const singleIp = this.ip;
387
+ return singleIp ? [singleIp] : [];
388
+ }
389
+ return xForwardedFor.split(",").map((ip) => ip.trim()).filter((ip) => ip);
390
+ }
391
+ /**
392
+ * Get the client IP address by checking various headers in order of precedence.
393
+ * Based on the request-ip library implementation logic.
394
+ *
395
+ * @returns {string} The client IP address, or empty string if none found
396
+ * @public
397
+ */
398
+ get ip() {
399
+ for (const header of [
400
+ "x-client-ip",
401
+ "x-forwarded-for",
402
+ "cf-connecting-ip",
403
+ "do-connecting-ip",
404
+ "fastly-client-ip",
405
+ "true-client-ip",
406
+ "x-real-ip",
407
+ "x-cluster-client-ip",
408
+ "x-forwarded",
409
+ "forwarded-for",
410
+ "forwarded",
411
+ "x-appengine-user-ip",
412
+ "cf-pseudo-ipv4"
413
+ ]) {
414
+ const value = this.get(header);
415
+ if (value) if (header === "x-forwarded-for" || header === "forwarded-for") {
416
+ const firstIp = value.split(",")[0]?.trim();
417
+ if (firstIp) return firstIp;
418
+ } else return value;
419
+ }
420
+ return "";
421
+ }
422
+ /**
423
+ * Get the request content length from the Content-Length header.
424
+ * Returns undefined if the header is missing, empty, or not a valid number.
425
+ *
426
+ * @returns {number|null} The content length in bytes, or null if not available
427
+ * @public
428
+ */
429
+ get length() {
430
+ const len = this.get("Content-Length");
431
+ if (len == null) return null;
432
+ return ~~len;
433
+ }
434
+ /**
435
+ * Get the request content type from the Content-Type header.
436
+ * Returns only the media type without parameters (e.g., 'application/json').
437
+ *
438
+ * @returns {string|null} The content type, or null if not set
439
+ * @public
440
+ */
441
+ get type() {
442
+ const type = this.get("Content-Type");
443
+ if (!type) return null;
444
+ return type.split(";", 1)[0].trim().toLowerCase();
445
+ }
446
+ /**
447
+ * Read request body as a Blob.
448
+ * This method can only be called once per request.
449
+ *
450
+ * @returns {Promise<Blob>} The request body as a Blob
451
+ * @public
452
+ */
453
+ async blob() {
454
+ return this.ctx.request.blob();
455
+ }
456
+ /**
457
+ * Read request body as an ArrayBuffer.
458
+ * This method can only be called once per request.
459
+ *
460
+ * @returns {Promise<ArrayBuffer>} The request body as an ArrayBuffer
461
+ * @public
462
+ */
463
+ async arrayBuffer() {
464
+ return this.ctx.request.arrayBuffer();
465
+ }
466
+ /**
467
+ * Read request body as text.
468
+ * This reads the Web Standard Request body stream. Platforms typically allow
469
+ * reading the underlying stream only once; calling multiple different readers
470
+ * (e.g., text() then blob()) is invalid.
471
+ *
472
+ * @returns {Promise<string>} The request body as a string
473
+ * @public
474
+ */
475
+ async text() {
476
+ return this.ctx.request.text();
477
+ }
478
+ /**
479
+ * Read request body as JSON.
480
+ * This method can only be called once per request.
481
+ *
482
+ * @returns {Promise<any>} The parsed JSON data
483
+ * @throws {SyntaxError}
484
+ * @public
485
+ */
486
+ async json() {
487
+ const text = await this.text();
488
+ return JSON.parse(text);
489
+ }
490
+ /**
491
+ * Read request body as FormData.
492
+ * This method can only be called once per request.
493
+ *
494
+ * @returns {Promise<FormData>} The request body as FormData
495
+ * @public
496
+ */
497
+ async formData() {
498
+ return this.ctx.request.formData();
499
+ }
500
+ /**
501
+ * Return JSON representation of the request.
502
+ *
503
+ * @returns {ReqJSON} JSON representation of request
504
+ * @public
505
+ */
506
+ toJSON() {
507
+ return {
508
+ method: this.method,
509
+ url: this.href,
510
+ headers: this.headers
511
+ };
512
+ }
513
+ };
514
+
515
+ //#endregion
516
+ export { HoaRequest as default };