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