@wooksjs/event-http 0.7.26 → 0.7.28

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/dist/index.d.ts CHANGED
@@ -80,7 +80,7 @@ declare const rawBodySlot: _wooksjs_event_core.Cached<Promise<Buffer<ArrayBuffer
80
80
  * ```
81
81
  */
82
82
  declare const useRequest: _wooksjs_event_core.WookComposable<{
83
- raw: http.IncomingMessage;
83
+ raw: IncomingMessage;
84
84
  url: string | undefined;
85
85
  method: string | undefined;
86
86
  headers: http.IncomingHttpHeaders;
@@ -347,6 +347,60 @@ interface TCacheControl {
347
347
  /** Renders a `TCacheControl` object into a `Cache-Control` header string. */
348
348
  declare function renderCacheControl(data: TCacheControl): string;
349
349
 
350
+ /** Content codings supported by response compression, in the order they are listed by default. */
351
+ type THttpCompressionEncoding = 'br' | 'gzip';
352
+ /**
353
+ * Response compression settings — the `compression` option of `createHttpApp()` and the
354
+ * argument of `HttpResponse.setCompression()`.
355
+ */
356
+ interface THttpCompressionOptions {
357
+ /** Minimum body size in bytes to compress. Smaller bodies are sent as is. @default 1024 */
358
+ threshold?: number;
359
+ /**
360
+ * Codings the server may use, in preference order. The client's `Accept-Encoding` q-values
361
+ * decide first; this order breaks ties. @default ['br', 'gzip']
362
+ */
363
+ encodings?: THttpCompressionEncoding[];
364
+ /** Brotli quality (0–11). Higher is smaller and much slower — keep 4–5 for dynamic bodies. @default 4 */
365
+ brotliQuality?: number;
366
+ /** Gzip level (1–9). @default 6 */
367
+ gzipLevel?: number;
368
+ /**
369
+ * Decides whether a response with this content type may be compressed.
370
+ * Replaces the default check — call `isCompressibleType(contentType)` inside to extend it.
371
+ * @default isCompressibleType
372
+ */
373
+ filter?: (contentType: string, response: HttpResponse) => boolean;
374
+ }
375
+ /** Fully resolved compression settings (what `HttpResponse.compression` returns). */
376
+ interface TResolvedHttpCompression {
377
+ threshold: number;
378
+ encodings: THttpCompressionEncoding[];
379
+ brotliQuality: number;
380
+ gzipLevel: number;
381
+ filter: (contentType: string, response: HttpResponse) => boolean;
382
+ }
383
+ /**
384
+ * Default compression filter: `text/*` (except `text/event-stream`), JSON and `+json`, XML and
385
+ * `+xml`, JavaScript, SVG, NDJSON, WASM and form-urlencoded content types.
386
+ */
387
+ declare function isCompressibleType(contentType: string): boolean;
388
+ /**
389
+ * Picks the content coding for a response from the request's `Accept-Encoding` header.
390
+ *
391
+ * The coding with the highest client q-value wins; `supported` order breaks ties. `q=0`
392
+ * excludes a coding, `*` matches every coding not listed explicitly. Returns `undefined` when
393
+ * the header is missing or none of `supported` is acceptable — send the body uncompressed then.
394
+ *
395
+ * @example
396
+ * ```ts
397
+ * negotiateEncoding('gzip, deflate, br', ['br', 'gzip']) // 'br'
398
+ * negotiateEncoding('br;q=0.5, gzip', ['br', 'gzip']) // 'gzip'
399
+ * negotiateEncoding('*;q=0, identity', ['br', 'gzip']) // undefined
400
+ * ```
401
+ */
402
+ declare function negotiateEncoding<T extends string>(acceptEncoding: string | string[] | undefined, supported: readonly T[]): T | undefined;
403
+
350
404
  /**
351
405
  * Manages response status, headers, cookies, cache control, and body for an HTTP request.
352
406
  *
@@ -370,15 +424,23 @@ declare class HttpResponse {
370
424
  * @param _req - The underlying Node.js `IncomingMessage`.
371
425
  * @param _logger - Logger instance for error reporting.
372
426
  * @param defaultHeaders - Optional headers to pre-populate on this response (e.g. from `securityHeaders()`).
427
+ * @param _captureMode - Finalize state on `send()` without writing to `_res` (programmatic fetch).
428
+ * @param compression - App-level response compression settings (`undefined` = off).
373
429
  */
374
- constructor(_res: ServerResponse, _req: IncomingMessage, _logger: Logger, defaultHeaders?: Record<string, string | string[]>, _captureMode?: boolean);
430
+ constructor(_res: ServerResponse, _req: IncomingMessage, _logger: Logger, defaultHeaders?: Record<string, string | string[]>, _captureMode?: boolean, compression?: TResolvedHttpCompression);
375
431
  protected _status: EHttpStatusCode;
376
432
  protected _body: unknown;
377
433
  protected _headers: Record<string, string | string[]>;
378
- protected _cookies: Record<string, TSetCookieData>;
379
- protected _rawCookies: string[];
434
+ /** Outgoing named cookies — allocated on the first `setCookie()`. */
435
+ protected _cookies?: Record<string, TSetCookieData>;
436
+ /** Outgoing raw `Set-Cookie` strings — allocated on the first `setCookieRaw()`. */
437
+ protected _rawCookies?: string[];
380
438
  protected _hasCookies: boolean;
381
439
  protected _responded: boolean;
440
+ /** Registry entry of the prerendered body picked by the last `renderBody()` (see `prerenderJson`). */
441
+ private _prerendered?;
442
+ /** Effective compression settings for this response (`undefined` = off). */
443
+ protected _compression: TResolvedHttpCompression | undefined;
382
444
  /** The HTTP status code. If not set, it is inferred automatically when `send()` is called. */
383
445
  get status(): EHttpStatusCode;
384
446
  set status(value: EHttpStatusCode);
@@ -433,6 +495,19 @@ declare class HttpResponse {
433
495
  setExpires(value: Date | string | number): this;
434
496
  /** Sets or clears the `Pragma: no-cache` header (chainable). */
435
497
  setPragmaNoCache(value?: boolean): this;
498
+ /**
499
+ * Overrides response compression for this response (chainable).
500
+ *
501
+ * - `false` — never compress this response (e.g. a body that mixes secrets with reflected input).
502
+ * - `true` — compress with the app settings, or the defaults when the app has compression off.
503
+ * - an options object — compress with these settings over the app settings (or the defaults).
504
+ *
505
+ * Only regular bodies (strings, numbers, booleans, objects, `Uint8Array`) are compressed; streams
506
+ * and fetch `Response` bodies are sent as is.
507
+ */
508
+ setCompression(value: boolean | THttpCompressionOptions): this;
509
+ /** Effective compression settings for this response, or `false` when compression is off. */
510
+ get compression(): Readonly<TResolvedHttpCompression> | false;
436
511
  /**
437
512
  * Returns the underlying Node.js `ServerResponse`.
438
513
  * @param passthrough - If `true`, the framework still manages the response lifecycle. If `false` (default), the response is marked as "responded" and the framework will not touch it.
@@ -463,11 +538,23 @@ declare class HttpResponse {
463
538
  *
464
539
  * Flushes all accumulated headers (including cookies) in a single `writeHead()` call,
465
540
  * then writes the body. Supports `Readable` streams, `fetch` `Response` objects, and regular values.
541
+ * Returns a Promise (which never rejects) for streamed bodies and compressed regular bodies.
466
542
  *
467
543
  * @throws Error if the response was already sent.
468
544
  */
469
545
  send(): void | Promise<void>;
470
546
  private finalizeCookies;
547
+ /**
548
+ * For a body registered with `prerenderJson(obj, { etag: true })`: sets the `ETag` header on a
549
+ * GET/HEAD 2xx response (unless one was set explicitly) and turns a `200` whose `If-None-Match`
550
+ * matches into a bodiless `304`. Returns `true` when the response became `304`.
551
+ * Other methods get no ETag (a validator on e.g. a PUT response would describe the stored resource).
552
+ */
553
+ private applyPrerenderedEtag;
554
+ /** Returns the stored key of header `name` (lower-case) in whatever casing it was set with. */
555
+ private headerKeyIgnoreCase;
556
+ /** Returns and clears the prerender entry picked by the last `renderBody()`. */
557
+ private takePrerendered;
471
558
  private autoStatus;
472
559
  private sendStream;
473
560
  /**
@@ -481,6 +568,25 @@ declare class HttpResponse {
481
568
  private pipeToResponse;
482
569
  private sendFetchResponse;
483
570
  private sendRegular;
571
+ /**
572
+ * Whether a rendered regular body of `size` bytes may be compressed — independent of the
573
+ * request's `Accept-Encoding` (and of HEAD, which is answered uncompressed).
574
+ */
575
+ private isCompressible;
576
+ /** Adds `token` to the `Vary` header (case-insensitive merge, keeps existing entries). */
577
+ private appendVary;
578
+ /**
579
+ * Sends the body compressed with `encoding`. Prerendered bodies are compressed once per coding
580
+ * and level and the bytes reused (sent synchronously once ready). A compressor failure is
581
+ * logged and the body is sent uncompressed — headers are not written yet, so that is safe.
582
+ * Never rejects: a sync handler's `send()` is not awaited.
583
+ */
584
+ private sendCompressed;
585
+ /**
586
+ * Writes the body picked by `sendCompressed()` — the `encoding`-compressed bytes, or the identity
587
+ * body after a compressor failure — unless the client went away meanwhile.
588
+ */
589
+ private writeDeferred;
484
590
  }
485
591
  /** Converts a Record of headers to a Web Standard `Headers` object. */
486
592
  declare function recordToWebHeaders(record: Record<string, string | string[]>): Headers;
@@ -583,6 +689,18 @@ interface TWooksHttpOptions {
583
689
  * @default DEFAULT_FORWARD_HEADERS — ['authorization', 'cookie', 'accept-language', 'x-forwarded-for', 'x-request-id']
584
690
  */
585
691
  forwardHeaders?: string[] | false;
692
+ /**
693
+ * Compresses regular response bodies (JSON, text, HTML, …) with brotli or gzip, negotiated
694
+ * from the request's `Accept-Encoding`. Off by default. `true` uses the defaults
695
+ * (`threshold: 1024`, `encodings: ['br', 'gzip']`, `brotliQuality: 4`, `gzipLevel: 6`);
696
+ * an object overrides them. Streams, fetch `Response` bodies and programmatic `fetch()`
697
+ * responses are never compressed. Opt a single response out with
698
+ * `useResponse().setCompression(false)`.
699
+ *
700
+ * Do not enable it for responses that mix secrets with attacker-controlled input (BREACH).
701
+ * @default false
702
+ */
703
+ compression?: boolean | THttpCompressionOptions;
586
704
  }
587
705
  /** HTTP adapter for Wooks that provides route registration, server lifecycle, and request handling. */
588
706
  declare class WooksHttp extends WooksAdapterBase {
@@ -590,6 +708,7 @@ declare class WooksHttp extends WooksAdapterBase {
590
708
  protected logger: TConsoleBase;
591
709
  protected ResponseClass: typeof WooksHttpResponse;
592
710
  protected eventContextOptions: EventContextOptions;
711
+ protected compression: TResolvedHttpCompression | undefined;
593
712
  constructor(opts?: TWooksHttpOptions | undefined, wooks?: Wooks | WooksAdapterBase);
594
713
  /** Registers a handler for all HTTP methods on the given path. */
595
714
  all<ResType = unknown, ParamsType = Record<string, string | string[]>>(path: string, handler: TWooksHandler<ResType>): wooks.TProstoRouterPathHandle<ParamsType>;
@@ -729,6 +848,49 @@ declare class WooksHttp extends WooksAdapterBase {
729
848
  */
730
849
  declare function createHttpApp(opts?: TWooksHttpOptions, wooks?: Wooks | WooksAdapterBase): WooksHttp;
731
850
 
851
+ /** Options for {@link prerenderJson}. */
852
+ interface TPrerenderJsonOptions {
853
+ /**
854
+ * Also compute a weak `ETag` from the serialized bytes. Responses that send the object then
855
+ * carry the `ETag` header and answer a matching `If-None-Match` (GET/HEAD, status 200) with
856
+ * `304 Not Modified`.
857
+ */
858
+ etag?: boolean;
859
+ }
860
+ /**
861
+ * Serializes `obj` to JSON once and remembers the string by object identity. Whenever a handler
862
+ * (or an interceptor) responds with that same object, the response reuses the stored JSON instead
863
+ * of calling `JSON.stringify` again. Use it for large, long-lived response objects that many
864
+ * requests return as is — e.g. a schema or metadata envelope built once and cached.
865
+ *
866
+ * With `{ etag: true }` a weak `ETag` is derived from the serialized bytes; a `GET`/`HEAD`
867
+ * that responds `200` with the object sets the `ETag` header and answers a matching
868
+ * `If-None-Match` with `304 Not Modified` (no body; other headers such as `Cache-Control`
869
+ * and `Vary` are kept). Error responses never become `304`. An `ETag` header set explicitly
870
+ * on the response wins and disables the `304` handling for that response.
871
+ *
872
+ * **Contract: never mutate a registered object (or anything it references).** The stored JSON
873
+ * is not refreshed, so a mutation would keep serving the old bytes. Build a new object instead —
874
+ * freezing registered objects (`Object.freeze`, deeply) in development makes violations throw.
875
+ *
876
+ * Calling it again for the same object is a no-op (it adds the `ETag` if it was not requested
877
+ * the first time). Entries are held weakly and disappear with the object.
878
+ *
879
+ * @param obj - A JSON-serializable object or array
880
+ * @param options - `{ etag: true }` to also compute a weak ETag
881
+ * @returns The same `obj`, so it can wrap a return value
882
+ *
883
+ * @example
884
+ * ```ts
885
+ * const envelope = prerenderJson(Object.freeze(buildMeta()), { etag: true })
886
+ * app.get('/meta', () => {
887
+ * useResponse().setHeader('cache-control', 'private, no-cache')
888
+ * return envelope // sent without re-serializing; 304 on a matching If-None-Match
889
+ * })
890
+ * ```
891
+ */
892
+ declare function prerenderJson<T extends object>(obj: T, options?: TPrerenderJsonOptions): T;
893
+
732
894
  /**
733
895
  * Configuration for `securityHeaders()`. Each option accepts a `string` (override value),
734
896
  * `false` (disable), or `undefined` (use default). `strictTransportSecurity` has no default (opt-in only).
@@ -793,5 +955,5 @@ interface TTestHttpContext {
793
955
  */
794
956
  declare function prepareTestHttpContext(options: TTestHttpContext): <T>(cb: (...a: any[]) => T) => T;
795
957
 
796
- export { DEFAULT_FORWARD_HEADERS, DEFAULT_LIMITS, EHttpStatusCode, HttpError, HttpResponse, WooksHttp, WooksHttpResponse, WooksURLSearchParams, createHttpApp, createHttpContext, httpKind, httpStatusCodes, prepareTestHttpContext, rawBodySlot, recordToWebHeaders, renderCacheControl, securityHeaders, seedRawBody, useAccept, useAuthorization, useCookies, useHeaders, useHttpContext, useRequest, useResponse, useUrlParams };
797
- export type { KnownAcceptType, KnownAuthType, SecurityHeadersOptions, TCacheControl, TCookieAttributes, TCookieAttributesInput, THttpEventData, TRequestLimits, TSetCookieData, TTestHttpContext, TWooksErrorBody, TWooksErrorBodyExt, TWooksHttpOptions };
958
+ export { DEFAULT_FORWARD_HEADERS, DEFAULT_LIMITS, EHttpStatusCode, HttpError, HttpResponse, WooksHttp, WooksHttpResponse, WooksURLSearchParams, createHttpApp, createHttpContext, httpKind, httpStatusCodes, isCompressibleType, negotiateEncoding, prepareTestHttpContext, prerenderJson, rawBodySlot, recordToWebHeaders, renderCacheControl, securityHeaders, seedRawBody, useAccept, useAuthorization, useCookies, useHeaders, useHttpContext, useRequest, useResponse, useUrlParams };
959
+ export type { KnownAcceptType, KnownAuthType, SecurityHeadersOptions, TCacheControl, TCookieAttributes, TCookieAttributesInput, THttpCompressionEncoding, THttpCompressionOptions, THttpEventData, TPrerenderJsonOptions, TRequestLimits, TResolvedHttpCompression, TSetCookieData, TTestHttpContext, TWooksErrorBody, TWooksErrorBodyExt, TWooksHttpOptions };