@travetto/web 6.0.0 → 6.0.2
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 +33 -27
- package/__index__.ts +3 -2
- package/package.json +3 -9
- package/src/decorator/common.ts +20 -16
- package/src/decorator/endpoint.ts +0 -1
- package/src/interceptor/accept.ts +1 -2
- package/src/interceptor/body.ts +7 -5
- package/src/interceptor/cache-control.ts +50 -0
- package/src/interceptor/compress.ts +8 -16
- package/src/interceptor/cookie.ts +1 -1
- package/src/interceptor/etag.ts +31 -16
- package/src/registry/controller.ts +9 -5
- package/src/registry/types.ts +9 -5
- package/src/types/cookie.ts +1 -1
- package/src/types/headers.ts +1 -42
- package/src/types/response.ts +2 -0
- package/src/util/body.ts +0 -15
- package/src/util/common.ts +27 -16
- package/src/util/cookie.ts +8 -49
- package/src/util/endpoint.ts +9 -4
- package/src/util/header.ts +161 -0
- package/src/util/keygrip.ts +43 -0
- package/support/test/dispatch-util.ts +7 -4
- package/support/test/suite/base.ts +3 -0
- package/src/interceptor/response-cache.ts +0 -47
- package/src/util/mime.ts +0 -36
package/README.md
CHANGED
|
@@ -68,6 +68,8 @@ import { BaseWebMessage } from './message.ts';
|
|
|
68
68
|
|
|
69
69
|
export interface WebResponseContext {
|
|
70
70
|
httpStatusCode?: number;
|
|
71
|
+
isPrivate?: boolean;
|
|
72
|
+
cacheableAge?: number;
|
|
71
73
|
}
|
|
72
74
|
|
|
73
75
|
/**
|
|
@@ -86,7 +88,7 @@ export class WebResponse<B = unknown> extends BaseWebMessage<B, WebResponseConte
|
|
|
86
88
|
}
|
|
87
89
|
```
|
|
88
90
|
|
|
89
|
-
These objects do not represent the underlying sockets provided by various http servers, but in fact are simple wrappers that track the flow through the call stack of the various [WebInterceptor](https://github.com/travetto/travetto/tree/main/module/web/src/types/interceptor.ts#L15)s and the [Endpoint](https://github.com/travetto/travetto/tree/main/module/web/src/decorator/endpoint.ts#L14) handler. One of the biggest departures here, is that the response is not an entity that is passed around from call-site to call-site, but is is solely a return-value. This doesn't mean the return value has to be static and pre-allocated, on the contrary streams are still supported. The difference here is that the streams/asynchronous values will be consumed until the response is sent back to the user. The [CompressInterceptor](https://github.com/travetto/travetto/tree/main/module/web/src/interceptor/compress.ts#
|
|
91
|
+
These objects do not represent the underlying sockets provided by various http servers, but in fact are simple wrappers that track the flow through the call stack of the various [WebInterceptor](https://github.com/travetto/travetto/tree/main/module/web/src/types/interceptor.ts#L15)s and the [Endpoint](https://github.com/travetto/travetto/tree/main/module/web/src/decorator/endpoint.ts#L14) handler. One of the biggest departures here, is that the response is not an entity that is passed around from call-site to call-site, but is is solely a return-value. This doesn't mean the return value has to be static and pre-allocated, on the contrary streams are still supported. The difference here is that the streams/asynchronous values will be consumed until the response is sent back to the user. The [CompressInterceptor](https://github.com/travetto/travetto/tree/main/module/web/src/interceptor/compress.ts#L44) is a good reference for transforming a [WebResponse](https://github.com/travetto/travetto/tree/main/module/web/src/types/response.ts#L3) that can either be a stream or a fixed value.
|
|
90
92
|
|
|
91
93
|
## Defining a Controller
|
|
92
94
|
To start, we must define a [@Controller](https://github.com/travetto/travetto/tree/main/module/web/src/decorator/controller.ts#L9), which is only allowed on classes. Controllers can be configured with:
|
|
@@ -112,13 +114,13 @@ class SimpleController {
|
|
|
112
114
|
Once the controller is declared, each method of the controller is a candidate for being an endpoint. By design, everything is asynchronous, and so async/await is natively supported.
|
|
113
115
|
|
|
114
116
|
The most common pattern is to register HTTP-driven endpoints. The HTTP methods that are currently supported:
|
|
115
|
-
* [@Get](https://github.com/travetto/travetto/tree/main/module/web/src/decorator/endpoint.ts#
|
|
116
|
-
* [@Post](https://github.com/travetto/travetto/tree/main/module/web/src/decorator/endpoint.ts#
|
|
117
|
-
* [@Put](https://github.com/travetto/travetto/tree/main/module/web/src/decorator/endpoint.ts#
|
|
118
|
-
* [@Delete](https://github.com/travetto/travetto/tree/main/module/web/src/decorator/endpoint.ts#
|
|
119
|
-
* [@Patch](https://github.com/travetto/travetto/tree/main/module/web/src/decorator/endpoint.ts#
|
|
120
|
-
* [@Head](https://github.com/travetto/travetto/tree/main/module/web/src/decorator/endpoint.ts#
|
|
121
|
-
* [@Options](https://github.com/travetto/travetto/tree/main/module/web/src/decorator/endpoint.ts#
|
|
117
|
+
* [@Get](https://github.com/travetto/travetto/tree/main/module/web/src/decorator/endpoint.ts#L42)
|
|
118
|
+
* [@Post](https://github.com/travetto/travetto/tree/main/module/web/src/decorator/endpoint.ts#L49)
|
|
119
|
+
* [@Put](https://github.com/travetto/travetto/tree/main/module/web/src/decorator/endpoint.ts#L56)
|
|
120
|
+
* [@Delete](https://github.com/travetto/travetto/tree/main/module/web/src/decorator/endpoint.ts#L69)
|
|
121
|
+
* [@Patch](https://github.com/travetto/travetto/tree/main/module/web/src/decorator/endpoint.ts#L63)
|
|
122
|
+
* [@Head](https://github.com/travetto/travetto/tree/main/module/web/src/decorator/endpoint.ts#L75)
|
|
123
|
+
* [@Options](https://github.com/travetto/travetto/tree/main/module/web/src/decorator/endpoint.ts#L81)
|
|
122
124
|
|
|
123
125
|
Similar to the Controller, each endpoint decorator handles the following config:
|
|
124
126
|
* `title` - The definition of the endpoint
|
|
@@ -249,7 +251,7 @@ class ContextController {
|
|
|
249
251
|
}
|
|
250
252
|
```
|
|
251
253
|
|
|
252
|
-
**Note**: When referencing the [@ContextParam](https://github.com/travetto/travetto/tree/main/module/web/src/decorator/param.ts#L61) values, the contract for idempotency needs to be carefully inspected, if expected. You can see in the example above that the [CacheControl](https://github.com/travetto/travetto/tree/main/module/web/src/decorator/common.ts#
|
|
254
|
+
**Note**: When referencing the [@ContextParam](https://github.com/travetto/travetto/tree/main/module/web/src/decorator/param.ts#L61) values, the contract for idempotency needs to be carefully inspected, if expected. You can see in the example above that the [CacheControl](https://github.com/travetto/travetto/tree/main/module/web/src/decorator/common.ts#L48) decorator is used to ensure that the response is not cached.
|
|
253
255
|
|
|
254
256
|
### Validating Inputs
|
|
255
257
|
The module provides high level access for [Schema](https://github.com/travetto/travetto/tree/main/module/schema#readme "Data type registry for runtime validation, reflection and binding.") support, via decorators, for validating and typing request inputs.
|
|
@@ -376,8 +378,8 @@ Out of the box, the web framework comes with a few interceptors, and more are co
|
|
|
376
378
|
1. global - Intended to run outside of the request flow - [AsyncContextInterceptor](https://github.com/travetto/travetto/tree/main/module/web/src/interceptor/context.ts#L13)
|
|
377
379
|
1. terminal - Handles once request and response are finished building - [LoggingInterceptor](https://github.com/travetto/travetto/tree/main/module/web/src/interceptor/logging.ts#L28), [RespondInterceptor](https://github.com/travetto/travetto/tree/main/module/web/src/interceptor/respond.ts#L12)
|
|
378
380
|
1. pre-request - Prepares the request for running - [TrustProxyInterceptor](https://github.com/travetto/travetto/tree/main/module/web/src/interceptor/trust-proxy.ts#L23)
|
|
379
|
-
1. request - Handles inbound request, validation, and body preparation - [DecompressInterceptor](https://github.com/travetto/travetto/tree/main/module/web/src/interceptor/decompress.ts#L53), [AcceptInterceptor](https://github.com/travetto/travetto/tree/main/module/web/src/interceptor/accept.ts#
|
|
380
|
-
1. response - Prepares outbound response - [CompressInterceptor](https://github.com/travetto/travetto/tree/main/module/web/src/interceptor/compress.ts#
|
|
381
|
+
1. request - Handles inbound request, validation, and body preparation - [DecompressInterceptor](https://github.com/travetto/travetto/tree/main/module/web/src/interceptor/decompress.ts#L53), [AcceptInterceptor](https://github.com/travetto/travetto/tree/main/module/web/src/interceptor/accept.ts#L33), [BodyInterceptor](https://github.com/travetto/travetto/tree/main/module/web/src/interceptor/body.ts#L58), [CookieInterceptor](https://github.com/travetto/travetto/tree/main/module/web/src/interceptor/cookie.ts#L60)
|
|
382
|
+
1. response - Prepares outbound response - [CompressInterceptor](https://github.com/travetto/travetto/tree/main/module/web/src/interceptor/compress.ts#L44), [CorsInterceptor](https://github.com/travetto/travetto/tree/main/module/web/src/interceptor/cors.ts#L51), [EtagInterceptor](https://github.com/travetto/travetto/tree/main/module/web/src/interceptor/etag.ts#L43), [CacheControlInterceptor](https://github.com/travetto/travetto/tree/main/module/web/src/interceptor/cache-control.ts#L23)
|
|
381
383
|
1. application - Lives outside of the general request/response behavior, [Web Auth](https://github.com/travetto/travetto/tree/main/module/auth-web#readme "Web authentication integration support for the Travetto framework") uses this for login and logout flows.
|
|
382
384
|
|
|
383
385
|
### Packaged Interceptors
|
|
@@ -423,7 +425,7 @@ export class TrustProxyConfig {
|
|
|
423
425
|
```
|
|
424
426
|
|
|
425
427
|
#### AcceptInterceptor
|
|
426
|
-
[AcceptInterceptor](https://github.com/travetto/travetto/tree/main/module/web/src/interceptor/accept.ts#
|
|
428
|
+
[AcceptInterceptor](https://github.com/travetto/travetto/tree/main/module/web/src/interceptor/accept.ts#L33) handles verifying the inbound request matches the allowed content-types. This acts as a standard gate-keeper for spurious input.
|
|
427
429
|
|
|
428
430
|
**Code: Accept Config**
|
|
429
431
|
```typescript
|
|
@@ -476,7 +478,7 @@ export class CookieConfig implements CookieSetOptions {
|
|
|
476
478
|
/**
|
|
477
479
|
* Supported only via http (not in JS)
|
|
478
480
|
*/
|
|
479
|
-
|
|
481
|
+
httponly = true;
|
|
480
482
|
/**
|
|
481
483
|
* Enforce same site policy
|
|
482
484
|
*/
|
|
@@ -502,7 +504,7 @@ export class CookieConfig implements CookieSetOptions {
|
|
|
502
504
|
```
|
|
503
505
|
|
|
504
506
|
#### BodyInterceptor
|
|
505
|
-
[BodyInterceptor](https://github.com/travetto/travetto/tree/main/module/web/src/interceptor/body.ts#
|
|
507
|
+
[BodyInterceptor](https://github.com/travetto/travetto/tree/main/module/web/src/interceptor/body.ts#L58) handles the inbound request, and converting the body payload into an appropriate format.
|
|
506
508
|
|
|
507
509
|
**Code: Body Config**
|
|
508
510
|
```typescript
|
|
@@ -514,7 +516,7 @@ export class WebBodyConfig {
|
|
|
514
516
|
/**
|
|
515
517
|
* Max body size limit
|
|
516
518
|
*/
|
|
517
|
-
limit:
|
|
519
|
+
limit: ByteSizeInput = '1mb';
|
|
518
520
|
/**
|
|
519
521
|
* How to interpret different content types
|
|
520
522
|
*/
|
|
@@ -530,7 +532,7 @@ export class WebBodyConfig {
|
|
|
530
532
|
```
|
|
531
533
|
|
|
532
534
|
#### CompressInterceptor
|
|
533
|
-
[CompressInterceptor](https://github.com/travetto/travetto/tree/main/module/web/src/interceptor/compress.ts#
|
|
535
|
+
[CompressInterceptor](https://github.com/travetto/travetto/tree/main/module/web/src/interceptor/compress.ts#L44) by default, will compress all valid outbound responses over a certain size, or for streams will cache every response. This relies on Node's [Buffer](https://nodejs.org/api/zlib.html) support for compression.
|
|
534
536
|
|
|
535
537
|
**Code: Compress Config**
|
|
536
538
|
```typescript
|
|
@@ -543,10 +545,6 @@ export class CompressConfig {
|
|
|
543
545
|
* Raw encoding options
|
|
544
546
|
*/
|
|
545
547
|
raw?: (ZlibOptions & BrotliOptions) | undefined;
|
|
546
|
-
/**
|
|
547
|
-
* Preferred encodings
|
|
548
|
-
*/
|
|
549
|
-
preferredEncodings?: WebCompressEncoding[] = ['br', 'gzip', 'identity'];
|
|
550
548
|
/**
|
|
551
549
|
* Supported encodings
|
|
552
550
|
*/
|
|
@@ -555,7 +553,7 @@ export class CompressConfig {
|
|
|
555
553
|
```
|
|
556
554
|
|
|
557
555
|
#### EtagInterceptor
|
|
558
|
-
[EtagInterceptor](https://github.com/travetto/travetto/tree/main/module/web/src/interceptor/etag.ts#
|
|
556
|
+
[EtagInterceptor](https://github.com/travetto/travetto/tree/main/module/web/src/interceptor/etag.ts#L43) by default, will tag all cacheable HTTP responses, when the response value/length is known. Streams, and other async data sources do not have a pre-defined length, and so are ineligible for etagging.
|
|
559
557
|
|
|
560
558
|
**Code: ETag Config**
|
|
561
559
|
```typescript
|
|
@@ -568,9 +566,16 @@ export class EtagConfig {
|
|
|
568
566
|
* Should we generate a weak etag
|
|
569
567
|
*/
|
|
570
568
|
weak?: boolean;
|
|
569
|
+
/**
|
|
570
|
+
* Threshold for tagging avoids tagging small responses
|
|
571
|
+
*/
|
|
572
|
+
minimumSize: ByteSizeInput = '10kb';
|
|
571
573
|
|
|
572
574
|
@Ignore()
|
|
573
575
|
cacheable?: boolean;
|
|
576
|
+
|
|
577
|
+
@Ignore()
|
|
578
|
+
_minimumSize: number | undefined;
|
|
574
579
|
}
|
|
575
580
|
```
|
|
576
581
|
|
|
@@ -611,8 +616,12 @@ export class CorsConfig {
|
|
|
611
616
|
}
|
|
612
617
|
```
|
|
613
618
|
|
|
614
|
-
####
|
|
615
|
-
[
|
|
619
|
+
#### CacheControlInterceptor
|
|
620
|
+
[CacheControlInterceptor](https://github.com/travetto/travetto/tree/main/module/web/src/interceptor/cache-control.ts#L23) by default, enforces whatever caching policy is established on a given endpoint using the [CacheControl](https://github.com/travetto/travetto/tree/main/module/web/src/decorator/common.ts#L48) decorator. Additionally, the interceptor retains knowledge if it is running on a private endpoint, or not. If the endpoint is deemed private it affects the caching header accordingly. If the endpoint directly returns a `Cache-Control` header, that takes precedence and all other logic is ignored.
|
|
621
|
+
|
|
622
|
+
This can be managed by setting `web.cache.applies: <boolean>` in your config.
|
|
623
|
+
|
|
624
|
+
**Note**: The [Web Auth](https://github.com/travetto/travetto/tree/main/module/auth-web#readme "Web authentication integration support for the Travetto framework") module will mark endpoints as private if they require authentication.
|
|
616
625
|
|
|
617
626
|
### Configuring Interceptors
|
|
618
627
|
All framework-provided interceptors, follow the same patterns for general configuration. This falls into three areas:
|
|
@@ -726,14 +735,11 @@ export class SimpleAuthInterceptor implements WebInterceptor {
|
|
|
726
735
|
```
|
|
727
736
|
|
|
728
737
|
## Cookie Support
|
|
729
|
-
Cookies are a unique element, within the framework, as they sit on the request and response flows. Ideally we would separate these out, but given the support for key rotation, there is a scenario in which reading a cookie on the request, will result in a cookie needing to be written on the response. Because of this, cookies are treated as being outside the normal [WebRequest](https://github.com/travetto/travetto/tree/main/module/web/src/types/request.ts#L11) activity, and is exposed as the [@ContextParam](https://github.com/travetto/travetto/tree/main/module/web/src/decorator/param.ts#L61) [CookieJar](https://github.com/travetto/travetto/tree/main/module/web/src/util/cookie.ts#
|
|
738
|
+
Cookies are a unique element, within the framework, as they sit on the request and response flows. Ideally we would separate these out, but given the support for key rotation, there is a scenario in which reading a cookie on the request, will result in a cookie needing to be written on the response. Because of this, cookies are treated as being outside the normal [WebRequest](https://github.com/travetto/travetto/tree/main/module/web/src/types/request.ts#L11) activity, and is exposed as the [@ContextParam](https://github.com/travetto/travetto/tree/main/module/web/src/decorator/param.ts#L61) [CookieJar](https://github.com/travetto/travetto/tree/main/module/web/src/util/cookie.ts#L12). The [CookieJar](https://github.com/travetto/travetto/tree/main/module/web/src/util/cookie.ts#L12) has a fairly basic contract:
|
|
730
739
|
|
|
731
740
|
**Code: CookieJar contract**
|
|
732
741
|
```typescript
|
|
733
742
|
export class CookieJar {
|
|
734
|
-
static parseCookieHeader(header: string): Cookie[];
|
|
735
|
-
static parseSetCookieHeader(header: string): Cookie;
|
|
736
|
-
static responseSuffix(c: Cookie): string[];
|
|
737
743
|
constructor({ keys, ...options }: CookieJarOptions = {});
|
|
738
744
|
import(cookies: Cookie[]): this;
|
|
739
745
|
has(name: string, opts: CookieGetOptions = {}): boolean;
|
package/__index__.ts
CHANGED
|
@@ -31,14 +31,15 @@ export * from './src/interceptor/compress.ts';
|
|
|
31
31
|
export * from './src/interceptor/context.ts';
|
|
32
32
|
export * from './src/interceptor/decompress.ts';
|
|
33
33
|
export * from './src/interceptor/etag.ts';
|
|
34
|
-
export * from './src/interceptor/
|
|
34
|
+
export * from './src/interceptor/cache-control.ts';
|
|
35
35
|
export * from './src/interceptor/logging.ts';
|
|
36
36
|
export * from './src/interceptor/respond.ts';
|
|
37
37
|
export * from './src/interceptor/trust-proxy.ts';
|
|
38
38
|
|
|
39
39
|
export * from './src/util/body.ts';
|
|
40
40
|
export * from './src/util/endpoint.ts';
|
|
41
|
-
export * from './src/util/
|
|
41
|
+
export * from './src/util/header.ts';
|
|
42
42
|
export * from './src/util/cookie.ts';
|
|
43
43
|
export * from './src/util/common.ts';
|
|
44
|
+
export * from './src/util/keygrip.ts';
|
|
44
45
|
export * from './src/util/net.ts';
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@travetto/web",
|
|
3
|
-
"version": "6.0.
|
|
3
|
+
"version": "6.0.2",
|
|
4
4
|
"description": "Declarative support creating for Web Applications",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"web",
|
|
@@ -31,17 +31,11 @@
|
|
|
31
31
|
"@travetto/registry": "^6.0.0",
|
|
32
32
|
"@travetto/runtime": "^6.0.0",
|
|
33
33
|
"@travetto/schema": "^6.0.0",
|
|
34
|
-
"
|
|
35
|
-
"@types/keygrip": "^1.0.6",
|
|
36
|
-
"@types/negotiator": "^0.6.3",
|
|
37
|
-
"find-my-way": "^9.3.0",
|
|
38
|
-
"fresh": "^0.5.2",
|
|
39
|
-
"keygrip": "^1.1.0",
|
|
40
|
-
"negotiator": "^1.0.0"
|
|
34
|
+
"find-my-way": "^9.3.0"
|
|
41
35
|
},
|
|
42
36
|
"peerDependencies": {
|
|
43
37
|
"@travetto/cli": "^6.0.0",
|
|
44
|
-
"@travetto/test": "^6.0.
|
|
38
|
+
"@travetto/test": "^6.0.1",
|
|
45
39
|
"@travetto/transformer": "^6.0.0"
|
|
46
40
|
},
|
|
47
41
|
"peerDependenciesMeta": {
|
package/src/decorator/common.ts
CHANGED
|
@@ -1,10 +1,9 @@
|
|
|
1
|
-
import { asConstructable, castTo, Class, TimeSpan } from '@travetto/runtime';
|
|
1
|
+
import { asConstructable, castTo, Class, TimeSpan, TimeUtil } from '@travetto/runtime';
|
|
2
2
|
|
|
3
3
|
import { ControllerRegistry } from '../registry/controller.ts';
|
|
4
4
|
import { EndpointConfig, ControllerConfig, DescribableConfig, EndpointDecorator, EndpointFunctionDescriptor } from '../registry/types.ts';
|
|
5
5
|
import { AcceptInterceptor } from '../interceptor/accept.ts';
|
|
6
6
|
import { WebInterceptor } from '../types/interceptor.ts';
|
|
7
|
-
import { WebCommonUtil, CacheControlFlag } from '../util/common.ts';
|
|
8
7
|
|
|
9
8
|
function register(config: Partial<EndpointConfig | ControllerConfig>): EndpointDecorator {
|
|
10
9
|
return function <T>(target: T | Class<T>, property?: string, descriptor?: EndpointFunctionDescriptor) {
|
|
@@ -40,27 +39,25 @@ export function SetHeaders(headers: EndpointConfig['responseHeaders']): Endpoint
|
|
|
40
39
|
*/
|
|
41
40
|
export function Produces(mime: string): EndpointDecorator { return SetHeaders({ 'Content-Type': mime }); }
|
|
42
41
|
|
|
43
|
-
|
|
44
|
-
* Specifies if endpoint should be conditional
|
|
45
|
-
*/
|
|
46
|
-
export function ConditionalRegister(handler: () => (boolean | Promise<boolean>)): EndpointDecorator {
|
|
47
|
-
return register({ conditional: handler });
|
|
48
|
-
}
|
|
42
|
+
type CacheControlInput = { cacheableAge?: number | TimeSpan, isPrivate?: boolean };
|
|
49
43
|
|
|
50
44
|
/**
|
|
51
45
|
* Set the max-age of a response based on the config
|
|
52
46
|
* @param value The value for the duration
|
|
53
|
-
* @param unit The unit of measurement
|
|
54
47
|
*/
|
|
55
|
-
export function CacheControl(
|
|
56
|
-
|
|
48
|
+
export function CacheControl(input: TimeSpan | number | CacheControlInput, extra?: Omit<CacheControlInput, 'cacheableAge'>): EndpointDecorator {
|
|
49
|
+
if (typeof input === 'string' || typeof input === 'number') {
|
|
50
|
+
input = { ...extra, cacheableAge: input };
|
|
51
|
+
}
|
|
52
|
+
const { cacheableAge, isPrivate } = input;
|
|
53
|
+
return register({
|
|
54
|
+
responseContext: {
|
|
55
|
+
...(cacheableAge !== undefined ? { cacheableAge: TimeUtil.asSeconds(cacheableAge) } : {}),
|
|
56
|
+
...isPrivate !== undefined ? { isPrivate } : {}
|
|
57
|
+
}
|
|
58
|
+
});
|
|
57
59
|
}
|
|
58
60
|
|
|
59
|
-
/**
|
|
60
|
-
* Disable cache control, ensuring endpoint will not cache
|
|
61
|
-
*/
|
|
62
|
-
export const DisableCacheControl = (): EndpointDecorator => CacheControl(0);
|
|
63
|
-
|
|
64
61
|
/**
|
|
65
62
|
* Define an endpoint to support specific input types
|
|
66
63
|
* @param types The list of mime types to allow/deny
|
|
@@ -79,6 +76,13 @@ export function Accepts(types: [string, ...string[]]): EndpointDecorator {
|
|
|
79
76
|
export const ConfigureInterceptor =
|
|
80
77
|
ControllerRegistry.createInterceptorConfigDecorator.bind(ControllerRegistry);
|
|
81
78
|
|
|
79
|
+
/**
|
|
80
|
+
* Specifies if endpoint should be conditional
|
|
81
|
+
*/
|
|
82
|
+
export function ConditionalRegister(handler: () => (boolean | Promise<boolean>)): EndpointDecorator {
|
|
83
|
+
return register({ conditional: handler });
|
|
84
|
+
}
|
|
85
|
+
|
|
82
86
|
/**
|
|
83
87
|
* Registers an interceptor exclusion filter
|
|
84
88
|
*/
|
|
@@ -20,7 +20,6 @@ export function Endpoint(config: EndpointDecConfig): EndpointFunctionDecorator {
|
|
|
20
20
|
};
|
|
21
21
|
}
|
|
22
22
|
|
|
23
|
-
|
|
24
23
|
function HttpEndpoint(method: HttpMethod, path: string): EndpointFunctionDecorator {
|
|
25
24
|
const { body: allowsBody, cacheable, emptyStatusCode } = HTTP_METHODS[method];
|
|
26
25
|
return Endpoint({
|
|
@@ -2,7 +2,6 @@ import { Injectable, Inject } from '@travetto/di';
|
|
|
2
2
|
import { Config } from '@travetto/config';
|
|
3
3
|
import { Ignore } from '@travetto/schema';
|
|
4
4
|
|
|
5
|
-
import { MimeUtil } from '../util/mime.ts';
|
|
6
5
|
import { WebCommonUtil } from '../util/common.ts';
|
|
7
6
|
|
|
8
7
|
import { WebChainedContext } from '../types/filter.ts';
|
|
@@ -39,7 +38,7 @@ export class AcceptInterceptor implements WebInterceptor<AcceptConfig> {
|
|
|
39
38
|
config: AcceptConfig;
|
|
40
39
|
|
|
41
40
|
finalizeConfig({ config }: WebInterceptorContext<AcceptConfig>): AcceptConfig {
|
|
42
|
-
config.matcher =
|
|
41
|
+
config.matcher = WebCommonUtil.mimeTypeMatcher(config.types ?? []);
|
|
43
42
|
return config;
|
|
44
43
|
}
|
|
45
44
|
|
package/src/interceptor/body.ts
CHANGED
|
@@ -9,11 +9,12 @@ import { WebInterceptorCategory } from '../types/core.ts';
|
|
|
9
9
|
import { WebInterceptor, WebInterceptorContext } from '../types/interceptor.ts';
|
|
10
10
|
|
|
11
11
|
import { WebBodyUtil } from '../util/body.ts';
|
|
12
|
-
import { WebCommonUtil } from '../util/common.ts';
|
|
12
|
+
import { ByteSizeInput, WebCommonUtil } from '../util/common.ts';
|
|
13
13
|
|
|
14
14
|
import { AcceptInterceptor } from './accept.ts';
|
|
15
15
|
import { DecompressInterceptor } from './decompress.ts';
|
|
16
16
|
import { WebError } from '../types/error.ts';
|
|
17
|
+
import { WebHeaderUtil } from '../util/header.ts';
|
|
17
18
|
|
|
18
19
|
/**
|
|
19
20
|
* @concrete
|
|
@@ -35,7 +36,7 @@ export class WebBodyConfig {
|
|
|
35
36
|
/**
|
|
36
37
|
* Max body size limit
|
|
37
38
|
*/
|
|
38
|
-
limit:
|
|
39
|
+
limit: ByteSizeInput = '1mb';
|
|
39
40
|
/**
|
|
40
41
|
* How to interpret different content types
|
|
41
42
|
*/
|
|
@@ -90,12 +91,13 @@ export class BodyInterceptor implements WebInterceptor<WebBodyConfig> {
|
|
|
90
91
|
throw WebError.for('Request entity too large', 413, { length, limit });
|
|
91
92
|
}
|
|
92
93
|
|
|
93
|
-
const contentType = request.headers.
|
|
94
|
-
if (!contentType) {
|
|
94
|
+
const contentType = WebHeaderUtil.parseHeaderSegment(request.headers.get('Content-Type'));
|
|
95
|
+
if (!contentType.value) {
|
|
95
96
|
return next();
|
|
96
97
|
}
|
|
97
98
|
|
|
98
|
-
const
|
|
99
|
+
const [baseType,] = contentType.value.split('/');
|
|
100
|
+
const parserType = config.parsingTypes[contentType.value] ?? config.parsingTypes[baseType];
|
|
99
101
|
if (!parserType) {
|
|
100
102
|
return next();
|
|
101
103
|
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { Injectable, Inject } from '@travetto/di';
|
|
2
|
+
import { Config } from '@travetto/config';
|
|
3
|
+
|
|
4
|
+
import { WebChainedContext } from '../types/filter.ts';
|
|
5
|
+
import { WebInterceptor, WebInterceptorContext } from '../types/interceptor.ts';
|
|
6
|
+
import { WebInterceptorCategory } from '../types/core.ts';
|
|
7
|
+
import { WebResponse } from '../types/response.ts';
|
|
8
|
+
|
|
9
|
+
import { EtagInterceptor } from './etag.ts';
|
|
10
|
+
|
|
11
|
+
@Config('web.cache')
|
|
12
|
+
export class CacheControlConfig {
|
|
13
|
+
/**
|
|
14
|
+
* Generate response cache headers
|
|
15
|
+
*/
|
|
16
|
+
applies = true;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Determines if we should cache all get requests
|
|
21
|
+
*/
|
|
22
|
+
@Injectable()
|
|
23
|
+
export class CacheControlInterceptor implements WebInterceptor {
|
|
24
|
+
|
|
25
|
+
category: WebInterceptorCategory = 'response';
|
|
26
|
+
dependsOn = [EtagInterceptor];
|
|
27
|
+
|
|
28
|
+
@Inject()
|
|
29
|
+
config: CacheControlConfig;
|
|
30
|
+
|
|
31
|
+
applies({ config, endpoint }: WebInterceptorContext<CacheControlConfig>): boolean {
|
|
32
|
+
return config.applies && endpoint.cacheable;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
async filter({ next }: WebChainedContext<CacheControlConfig>): Promise<WebResponse> {
|
|
36
|
+
const response = await next();
|
|
37
|
+
if (!response.headers.has('Cache-Control')) {
|
|
38
|
+
const parts: string[] = [response.context.isPrivate ? 'private' : 'public'];
|
|
39
|
+
const age = response.context.cacheableAge ?? (response.context.isPrivate ? 0 : undefined);
|
|
40
|
+
if (age !== undefined) {
|
|
41
|
+
if (age <= 0) {
|
|
42
|
+
parts.push('no-store');
|
|
43
|
+
}
|
|
44
|
+
parts.push(`max-age=${Math.max(age, 0)}`);
|
|
45
|
+
}
|
|
46
|
+
response.headers.set('Cache-Control', parts.join(','));
|
|
47
|
+
}
|
|
48
|
+
return response;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
@@ -1,20 +1,18 @@
|
|
|
1
1
|
import { buffer } from 'node:stream/consumers';
|
|
2
2
|
import { BrotliOptions, constants, createBrotliCompress, createDeflate, createGzip, ZlibOptions } from 'node:zlib';
|
|
3
3
|
|
|
4
|
-
// eslint-disable-next-line @typescript-eslint/naming-convention
|
|
5
|
-
import Negotiator from 'negotiator';
|
|
6
|
-
|
|
7
4
|
import { Injectable, Inject } from '@travetto/di';
|
|
8
5
|
import { Config } from '@travetto/config';
|
|
9
|
-
import { castTo } from '@travetto/runtime';
|
|
10
6
|
|
|
11
7
|
import { WebInterceptor, WebInterceptorContext } from '../types/interceptor.ts';
|
|
12
8
|
import { WebInterceptorCategory } from '../types/core.ts';
|
|
13
9
|
import { WebChainedContext } from '../types/filter.ts';
|
|
14
10
|
import { WebResponse } from '../types/response.ts';
|
|
15
|
-
import { WebBodyUtil } from '../util/body.ts';
|
|
16
11
|
import { WebError } from '../types/error.ts';
|
|
17
12
|
|
|
13
|
+
import { WebBodyUtil } from '../util/body.ts';
|
|
14
|
+
import { WebHeaderUtil } from '../util/header.ts';
|
|
15
|
+
|
|
18
16
|
const COMPRESSORS = {
|
|
19
17
|
gzip: createGzip,
|
|
20
18
|
deflate: createDeflate,
|
|
@@ -33,10 +31,6 @@ export class CompressConfig {
|
|
|
33
31
|
* Raw encoding options
|
|
34
32
|
*/
|
|
35
33
|
raw?: (ZlibOptions & BrotliOptions) | undefined;
|
|
36
|
-
/**
|
|
37
|
-
* Preferred encodings
|
|
38
|
-
*/
|
|
39
|
-
preferredEncodings?: WebCompressEncoding[] = ['br', 'gzip', 'identity'];
|
|
40
34
|
/**
|
|
41
35
|
* Supported encodings
|
|
42
36
|
*/
|
|
@@ -55,10 +49,10 @@ export class CompressInterceptor implements WebInterceptor {
|
|
|
55
49
|
config: CompressConfig;
|
|
56
50
|
|
|
57
51
|
async compress(ctx: WebChainedContext, response: WebResponse): Promise<WebResponse> {
|
|
58
|
-
const { raw = {},
|
|
52
|
+
const { raw = {}, supportedEncodings } = this.config;
|
|
59
53
|
const { request } = ctx;
|
|
60
54
|
|
|
61
|
-
response.headers.
|
|
55
|
+
response.headers.append('Vary', 'Accept-Encoding');
|
|
62
56
|
|
|
63
57
|
if (
|
|
64
58
|
!response.body ||
|
|
@@ -69,15 +63,13 @@ export class CompressInterceptor implements WebInterceptor {
|
|
|
69
63
|
}
|
|
70
64
|
|
|
71
65
|
const accepts = request.headers.get('Accept-Encoding');
|
|
72
|
-
const type
|
|
73
|
-
castTo(new Negotiator({ headers: { 'accept-encoding': accepts ?? '*' } })
|
|
74
|
-
.encoding([...supportedEncodings, ...preferredEncodings]));
|
|
66
|
+
const type = WebHeaderUtil.negotiateHeader(accepts ?? '*', supportedEncodings);
|
|
75
67
|
|
|
76
|
-
if (
|
|
68
|
+
if (!type) {
|
|
77
69
|
throw WebError.for(`Please accept one of: ${supportedEncodings.join(', ')}. ${accepts} is not supported`, 406);
|
|
78
70
|
}
|
|
79
71
|
|
|
80
|
-
if (type === 'identity'
|
|
72
|
+
if (type === 'identity') {
|
|
81
73
|
return response;
|
|
82
74
|
}
|
|
83
75
|
|
package/src/interceptor/etag.ts
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import crypto from 'node:crypto';
|
|
2
|
-
import fresh from 'fresh';
|
|
3
2
|
|
|
4
3
|
import { Injectable, Inject } from '@travetto/di';
|
|
5
4
|
import { Config } from '@travetto/config';
|
|
6
5
|
import { Ignore } from '@travetto/schema';
|
|
6
|
+
import { BinaryUtil } from '@travetto/runtime';
|
|
7
7
|
|
|
8
8
|
import { WebChainedContext } from '../types/filter.ts';
|
|
9
9
|
import { WebResponse } from '../types/response.ts';
|
|
@@ -11,6 +11,8 @@ import { WebInterceptor, WebInterceptorContext } from '../types/interceptor.ts';
|
|
|
11
11
|
import { WebInterceptorCategory } from '../types/core.ts';
|
|
12
12
|
import { CompressInterceptor } from './compress.ts';
|
|
13
13
|
import { WebBodyUtil } from '../util/body.ts';
|
|
14
|
+
import { ByteSizeInput, WebCommonUtil } from '../util/common.ts';
|
|
15
|
+
import { WebHeaderUtil } from '../util/header.ts';
|
|
14
16
|
|
|
15
17
|
@Config('web.etag')
|
|
16
18
|
export class EtagConfig {
|
|
@@ -22,9 +24,16 @@ export class EtagConfig {
|
|
|
22
24
|
* Should we generate a weak etag
|
|
23
25
|
*/
|
|
24
26
|
weak?: boolean;
|
|
27
|
+
/**
|
|
28
|
+
* Threshold for tagging avoids tagging small responses
|
|
29
|
+
*/
|
|
30
|
+
minimumSize: ByteSizeInput = '10kb';
|
|
25
31
|
|
|
26
32
|
@Ignore()
|
|
27
33
|
cacheable?: boolean;
|
|
34
|
+
|
|
35
|
+
@Ignore()
|
|
36
|
+
_minimumSize: number | undefined;
|
|
28
37
|
}
|
|
29
38
|
|
|
30
39
|
/**
|
|
@@ -52,31 +61,37 @@ export class EtagInterceptor implements WebInterceptor {
|
|
|
52
61
|
addTag(ctx: WebChainedContext<EtagConfig>, response: WebResponse): WebResponse {
|
|
53
62
|
const { request } = ctx;
|
|
54
63
|
|
|
55
|
-
const statusCode = response.context.httpStatusCode;
|
|
64
|
+
const statusCode = response.context.httpStatusCode ?? 200;
|
|
56
65
|
|
|
57
|
-
if (
|
|
66
|
+
if (
|
|
67
|
+
(statusCode >= 300 && statusCode !== 304) || // Ignore redirects
|
|
68
|
+
BinaryUtil.isReadableStream(response.body) // Ignore streams (unknown length)
|
|
69
|
+
) {
|
|
58
70
|
return response;
|
|
59
71
|
}
|
|
60
72
|
|
|
61
73
|
const binaryResponse = new WebResponse({ ...response, ...WebBodyUtil.toBinaryMessage(response) });
|
|
62
|
-
|
|
74
|
+
|
|
75
|
+
const body = binaryResponse.body;
|
|
76
|
+
if (!Buffer.isBuffer(body)) {
|
|
63
77
|
return binaryResponse;
|
|
64
78
|
}
|
|
65
79
|
|
|
66
|
-
const
|
|
80
|
+
const minSize = ctx.config._minimumSize ??= WebCommonUtil.parseByteSize(ctx.config.minimumSize);
|
|
81
|
+
if (body.length < minSize) {
|
|
82
|
+
return binaryResponse;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const tag = this.computeTag(body);
|
|
67
86
|
binaryResponse.headers.set('ETag', `${ctx.config.weak ? 'W/' : ''}"${tag}"`);
|
|
68
87
|
|
|
69
|
-
if (ctx.config.cacheable &&
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
'last-modified': binaryResponse.headers.get('Last-Modified')!
|
|
77
|
-
})
|
|
78
|
-
) {
|
|
79
|
-
return new WebResponse({ context: { httpStatusCode: 304 } });
|
|
88
|
+
if (ctx.config.cacheable && WebHeaderUtil.isFresh(request.headers, binaryResponse.headers)) {
|
|
89
|
+
// Remove length for the 304
|
|
90
|
+
binaryResponse.headers.delete('Content-Length');
|
|
91
|
+
return new WebResponse({
|
|
92
|
+
context: { ...response.context, httpStatusCode: 304 },
|
|
93
|
+
headers: binaryResponse.headers
|
|
94
|
+
});
|
|
80
95
|
}
|
|
81
96
|
|
|
82
97
|
return binaryResponse;
|
|
@@ -50,6 +50,7 @@ class $ControllerRegistry extends MetadataRegistry<ControllerConfig, EndpointCon
|
|
|
50
50
|
externalName: cls.name.replace(/(Controller|Web|Service)$/, ''),
|
|
51
51
|
endpoints: [],
|
|
52
52
|
contextParams: {},
|
|
53
|
+
responseHeaders: {}
|
|
53
54
|
};
|
|
54
55
|
}
|
|
55
56
|
|
|
@@ -68,7 +69,8 @@ class $ControllerRegistry extends MetadataRegistry<ControllerConfig, EndpointCon
|
|
|
68
69
|
interceptorConfigs: [],
|
|
69
70
|
name: endpoint.name,
|
|
70
71
|
endpoint,
|
|
71
|
-
|
|
72
|
+
responseHeaders: {},
|
|
73
|
+
finalizedResponseHeaders: new WebHeaders(),
|
|
72
74
|
responseFinalizer: undefined
|
|
73
75
|
};
|
|
74
76
|
|
|
@@ -199,7 +201,7 @@ class $ControllerRegistry extends MetadataRegistry<ControllerConfig, EndpointCon
|
|
|
199
201
|
* @param src Root describable (controller, endpoint)
|
|
200
202
|
* @param dest Target (controller, endpoint)
|
|
201
203
|
*/
|
|
202
|
-
|
|
204
|
+
mergeCommon(src: Partial<ControllerConfig | EndpointConfig>, dest: Partial<ControllerConfig | EndpointConfig>): void {
|
|
203
205
|
dest.filters = [...(dest.filters ?? []), ...(src.filters ?? [])];
|
|
204
206
|
dest.interceptorConfigs = [...(dest.interceptorConfigs ?? []), ...(src.interceptorConfigs ?? [])];
|
|
205
207
|
dest.interceptorExclude = dest.interceptorExclude ?? src.interceptorExclude;
|
|
@@ -207,6 +209,7 @@ class $ControllerRegistry extends MetadataRegistry<ControllerConfig, EndpointCon
|
|
|
207
209
|
dest.description = src.description || dest.description;
|
|
208
210
|
dest.documented = src.documented ?? dest.documented;
|
|
209
211
|
dest.responseHeaders = { ...src.responseHeaders, ...dest.responseHeaders };
|
|
212
|
+
dest.responseContext = { ...src.responseContext, ...dest.responseContext };
|
|
210
213
|
}
|
|
211
214
|
|
|
212
215
|
/**
|
|
@@ -231,7 +234,7 @@ class $ControllerRegistry extends MetadataRegistry<ControllerConfig, EndpointCon
|
|
|
231
234
|
srcConf.path = `/${srcConf.path}`;
|
|
232
235
|
}
|
|
233
236
|
|
|
234
|
-
this.
|
|
237
|
+
this.mergeCommon(config, srcConf);
|
|
235
238
|
|
|
236
239
|
return descriptor;
|
|
237
240
|
}
|
|
@@ -252,7 +255,7 @@ class $ControllerRegistry extends MetadataRegistry<ControllerConfig, EndpointCon
|
|
|
252
255
|
srcConf.contextParams = { ...srcConf.contextParams, ...config.contextParams };
|
|
253
256
|
|
|
254
257
|
|
|
255
|
-
this.
|
|
258
|
+
this.mergeCommon(config, srcConf);
|
|
256
259
|
}
|
|
257
260
|
|
|
258
261
|
/**
|
|
@@ -266,7 +269,8 @@ class $ControllerRegistry extends MetadataRegistry<ControllerConfig, EndpointCon
|
|
|
266
269
|
this.#endpointsById.set(ep.id, ep);
|
|
267
270
|
// Store full path from base for use in other contexts
|
|
268
271
|
ep.fullPath = `/${final.basePath}/${ep.path}`.replace(/[/]{1,4}/g, '/').replace(/(.)[/]$/, (_, a) => a);
|
|
269
|
-
ep.
|
|
272
|
+
ep.finalizedResponseHeaders = new WebHeaders({ ...final.responseHeaders, ...ep.responseHeaders });
|
|
273
|
+
ep.responseContext = { ...final.responseContext, ...ep.responseContext };
|
|
270
274
|
}
|
|
271
275
|
|
|
272
276
|
if (this.has(final.basePath)) {
|