@travetto/web 7.1.4 → 8.0.0-alpha.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 +17 -15
- package/__index__.ts +2 -1
- package/package.json +11 -11
- package/src/context.ts +5 -4
- package/src/decorator/common.ts +3 -3
- package/src/decorator/controller.ts +1 -1
- package/src/interceptor/accept.ts +1 -1
- package/src/interceptor/body.ts +4 -3
- package/src/interceptor/compress.ts +28 -21
- package/src/interceptor/cookie.ts +10 -5
- package/src/interceptor/decompress.ts +17 -15
- package/src/interceptor/etag.ts +6 -12
- package/src/registry/registry-adapter.ts +10 -10
- package/src/registry/registry-index.ts +6 -6
- package/src/router/base.ts +3 -2
- package/src/router/standard.ts +2 -2
- package/src/types/error.ts +2 -2
- package/src/types/message.ts +0 -3
- package/src/util/body.ts +80 -93
- package/src/util/common.ts +3 -3
- package/src/util/cookie.ts +35 -26
- package/src/util/keygrip.ts +47 -27
- package/support/test/dispatch-util.ts +30 -28
- package/support/test/suite/base.ts +1 -1
- package/support/test/suite/controller.ts +4 -6
package/README.md
CHANGED
|
@@ -88,7 +88,7 @@ export class WebResponse<B = unknown> extends BaseWebMessage<B, WebResponseConte
|
|
|
88
88
|
}
|
|
89
89
|
```
|
|
90
90
|
|
|
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#
|
|
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#L51) 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.
|
|
92
92
|
|
|
93
93
|
## Defining a Controller
|
|
94
94
|
To start, we must define a [@Controller](https://github.com/travetto/travetto/tree/main/module/web/src/decorator/controller.ts#L12), which is only allowed on classes. Controllers can be configured with:
|
|
@@ -145,7 +145,7 @@ class SimpleController {
|
|
|
145
145
|
@Get('/')
|
|
146
146
|
async simpleGet() {
|
|
147
147
|
let data: Data | undefined;
|
|
148
|
-
//
|
|
148
|
+
// Do work
|
|
149
149
|
return data;
|
|
150
150
|
}
|
|
151
151
|
}
|
|
@@ -378,8 +378,8 @@ Out of the box, the web framework comes with a few interceptors, and more are co
|
|
|
378
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)
|
|
379
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)
|
|
380
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)
|
|
381
|
-
1. request - Handles inbound request, validation, and body preparation - [DecompressInterceptor](https://github.com/travetto/travetto/tree/main/module/web/src/interceptor/decompress.ts#
|
|
382
|
-
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#L51), [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#L57), [CookieInterceptor](https://github.com/travetto/travetto/tree/main/module/web/src/interceptor/cookie.ts#L61)
|
|
382
|
+
1. response - Prepares outbound response - [CompressInterceptor](https://github.com/travetto/travetto/tree/main/module/web/src/interceptor/compress.ts#L51), [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#L41), [CacheControlInterceptor](https://github.com/travetto/travetto/tree/main/module/web/src/interceptor/cache-control.ts#L23)
|
|
383
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.
|
|
384
384
|
|
|
385
385
|
### Packaged Interceptors
|
|
@@ -445,7 +445,7 @@ export class AcceptConfig {
|
|
|
445
445
|
```
|
|
446
446
|
|
|
447
447
|
#### DecompressInterceptor
|
|
448
|
-
[DecompressInterceptor](https://github.com/travetto/travetto/tree/main/module/web/src/interceptor/decompress.ts#
|
|
448
|
+
[DecompressInterceptor](https://github.com/travetto/travetto/tree/main/module/web/src/interceptor/decompress.ts#L51) handles decompressing the inbound request, if supported. This relies upon HTTP standards for content encoding, and negotiating the appropriate decompression scheme.
|
|
449
449
|
|
|
450
450
|
**Code: Decompress Config**
|
|
451
451
|
```typescript
|
|
@@ -462,7 +462,7 @@ export class DecompressConfig {
|
|
|
462
462
|
```
|
|
463
463
|
|
|
464
464
|
#### CookieInterceptor
|
|
465
|
-
[CookieInterceptor](https://github.com/travetto/travetto/tree/main/module/web/src/interceptor/cookie.ts#
|
|
465
|
+
[CookieInterceptor](https://github.com/travetto/travetto/tree/main/module/web/src/interceptor/cookie.ts#L61) is responsible for processing inbound cookie headers and populating the appropriate data on the request, as well as sending the appropriate response data
|
|
466
466
|
|
|
467
467
|
**Code: Cookies Config**
|
|
468
468
|
```typescript
|
|
@@ -532,7 +532,7 @@ export class WebBodyConfig {
|
|
|
532
532
|
```
|
|
533
533
|
|
|
534
534
|
#### CompressInterceptor
|
|
535
|
-
[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#L51) 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.
|
|
536
536
|
|
|
537
537
|
**Code: Compress Config**
|
|
538
538
|
```typescript
|
|
@@ -544,7 +544,7 @@ export class CompressConfig {
|
|
|
544
544
|
/**
|
|
545
545
|
* Raw encoding options
|
|
546
546
|
*/
|
|
547
|
-
raw?: (ZlibOptions & BrotliOptions) | undefined;
|
|
547
|
+
raw?: (zlib.ZlibOptions & zlib.BrotliOptions) | undefined;
|
|
548
548
|
/**
|
|
549
549
|
* Supported encodings
|
|
550
550
|
*/
|
|
@@ -553,7 +553,7 @@ export class CompressConfig {
|
|
|
553
553
|
```
|
|
554
554
|
|
|
555
555
|
#### EtagInterceptor
|
|
556
|
-
[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#L41) 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.
|
|
557
557
|
|
|
558
558
|
**Code: ETag Config**
|
|
559
559
|
```typescript
|
|
@@ -740,16 +740,18 @@ Cookies are a unique element, within the framework, as they sit on the request a
|
|
|
740
740
|
**Code: CookieJar contract**
|
|
741
741
|
```typescript
|
|
742
742
|
export class CookieJar {
|
|
743
|
-
constructor(
|
|
744
|
-
|
|
743
|
+
constructor(options: CookieJarOptions = {}, keys?: string[] | KeyGrip);
|
|
744
|
+
get shouldSign(): boolean;
|
|
745
|
+
async export(cookie: Cookie, response?: boolean): Promise<string[]>;
|
|
746
|
+
async import(cookies: Cookie[]): Promise<void>;
|
|
745
747
|
has(name: string, options: CookieGetOptions = {}): boolean;
|
|
746
748
|
get(name: string, options: CookieGetOptions = {}): string | undefined;
|
|
747
749
|
set(cookie: Cookie): void;
|
|
748
750
|
getAll(): Cookie[];
|
|
749
|
-
importCookieHeader(header: string | null | undefined):
|
|
750
|
-
importSetCookieHeader(headers: string[] | null | undefined):
|
|
751
|
-
exportCookieHeader(): string
|
|
752
|
-
exportSetCookieHeader(): string[]
|
|
751
|
+
importCookieHeader(header: string | null | undefined): Promise<void>;
|
|
752
|
+
importSetCookieHeader(headers: string[] | null | undefined): Promise<void>;
|
|
753
|
+
exportCookieHeader(): Promise<string>;
|
|
754
|
+
exportSetCookieHeader(): Promise<string[]>;
|
|
753
755
|
}
|
|
754
756
|
```
|
|
755
757
|
|
package/__index__.ts
CHANGED
|
@@ -7,6 +7,7 @@ export * from './src/types/error.ts';
|
|
|
7
7
|
export * from './src/types/cookie.ts';
|
|
8
8
|
export * from './src/types/interceptor.ts';
|
|
9
9
|
export * from './src/types/headers.ts';
|
|
10
|
+
export * from './src/types/message.ts';
|
|
10
11
|
|
|
11
12
|
export * from './src/context.ts';
|
|
12
13
|
export * from './src/config.ts';
|
|
@@ -43,4 +44,4 @@ export * from './src/util/header.ts';
|
|
|
43
44
|
export * from './src/util/cookie.ts';
|
|
44
45
|
export * from './src/util/common.ts';
|
|
45
46
|
export * from './src/util/keygrip.ts';
|
|
46
|
-
export * from './src/util/net.ts';
|
|
47
|
+
export * from './src/util/net.ts';
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@travetto/web",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "8.0.0-alpha.1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Declarative support for creating Web Applications",
|
|
6
6
|
"keywords": [
|
|
@@ -26,18 +26,18 @@
|
|
|
26
26
|
"directory": "module/web"
|
|
27
27
|
},
|
|
28
28
|
"dependencies": {
|
|
29
|
-
"@travetto/config": "^
|
|
30
|
-
"@travetto/context": "^
|
|
31
|
-
"@travetto/di": "^
|
|
32
|
-
"@travetto/registry": "^
|
|
33
|
-
"@travetto/runtime": "^
|
|
34
|
-
"@travetto/schema": "^
|
|
35
|
-
"find-my-way": "^9.
|
|
29
|
+
"@travetto/config": "^8.0.0-alpha.1",
|
|
30
|
+
"@travetto/context": "^8.0.0-alpha.1",
|
|
31
|
+
"@travetto/di": "^8.0.0-alpha.1",
|
|
32
|
+
"@travetto/registry": "^8.0.0-alpha.1",
|
|
33
|
+
"@travetto/runtime": "^8.0.0-alpha.1",
|
|
34
|
+
"@travetto/schema": "^8.0.0-alpha.1",
|
|
35
|
+
"find-my-way": "^9.5.0"
|
|
36
36
|
},
|
|
37
37
|
"peerDependencies": {
|
|
38
|
-
"@travetto/cli": "^
|
|
39
|
-
"@travetto/test": "^
|
|
40
|
-
"@travetto/transformer": "^
|
|
38
|
+
"@travetto/cli": "^8.0.0-alpha.1",
|
|
39
|
+
"@travetto/test": "^8.0.0-alpha.1",
|
|
40
|
+
"@travetto/transformer": "^8.0.0-alpha.1"
|
|
41
41
|
},
|
|
42
42
|
"peerDependenciesMeta": {
|
|
43
43
|
"@travetto/transformer": {
|
package/src/context.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { AsyncContextValue, type AsyncContext } from '@travetto/context';
|
|
2
|
-
import { Inject, Injectable } from '@travetto/di';
|
|
3
|
-
import {
|
|
2
|
+
import { Inject, Injectable, PostConstruct } from '@travetto/di';
|
|
3
|
+
import { RuntimeError, castTo, type Class } from '@travetto/runtime';
|
|
4
4
|
|
|
5
5
|
import { WebRequest } from './types/request.ts';
|
|
6
6
|
|
|
@@ -20,7 +20,8 @@ export class WebAsyncContext {
|
|
|
20
20
|
return this.#request.get()!;
|
|
21
21
|
}
|
|
22
22
|
|
|
23
|
-
|
|
23
|
+
@PostConstruct()
|
|
24
|
+
exposeContext(): void {
|
|
24
25
|
this.registerSource(WebRequest, () => this.#request.get());
|
|
25
26
|
}
|
|
26
27
|
|
|
@@ -38,7 +39,7 @@ export class WebAsyncContext {
|
|
|
38
39
|
getSource<T>(cls: Class<T>): () => T {
|
|
39
40
|
const item = this.#byType.get(cls.Ⲑid);
|
|
40
41
|
if (!item) {
|
|
41
|
-
throw new
|
|
42
|
+
throw new RuntimeError('Unknown type for web context');
|
|
42
43
|
}
|
|
43
44
|
return castTo(item);
|
|
44
45
|
}
|
package/src/decorator/common.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type Class, type ClassInstance, getClass, type
|
|
1
|
+
import { type Class, type ClassInstance, getClass, type RetainIntrinsicFields, type TimeSpan, TimeUtil } from '@travetto/runtime';
|
|
2
2
|
|
|
3
3
|
import { ControllerRegistryIndex } from '../registry/registry-index.ts';
|
|
4
4
|
import type { EndpointConfig, ControllerConfig, EndpointDecorator, EndpointFunctionDescriptor } from '../registry/types.ts';
|
|
@@ -49,7 +49,7 @@ export function CacheControl(input: TimeSpan | number | CacheControlInput, extra
|
|
|
49
49
|
const { cacheableAge, isPrivate } = input;
|
|
50
50
|
return register({
|
|
51
51
|
responseContext: {
|
|
52
|
-
...(cacheableAge !== undefined ? { cacheableAge: TimeUtil.
|
|
52
|
+
...(cacheableAge !== undefined ? { cacheableAge: TimeUtil.duration(cacheableAge, 's') } : {}),
|
|
53
53
|
...isPrivate !== undefined ? { isPrivate } : {}
|
|
54
54
|
}
|
|
55
55
|
});
|
|
@@ -74,7 +74,7 @@ export function Accepts(types: [string, ...string[]]): EndpointDecorator {
|
|
|
74
74
|
*/
|
|
75
75
|
export const ConfigureInterceptor = <T extends WebInterceptor>(
|
|
76
76
|
cls: Class<T>,
|
|
77
|
-
config: Partial<
|
|
77
|
+
config: Partial<RetainIntrinsicFields<T['config']>>,
|
|
78
78
|
extra?: Partial<EndpointConfig & ControllerConfig>
|
|
79
79
|
): EndpointDecorator =>
|
|
80
80
|
ControllerRegistryIndex.createInterceptorConfigDecorator(cls, config, extra);
|
|
@@ -12,6 +12,6 @@ import { ControllerRegistryIndex } from '../registry/registry-index.ts';
|
|
|
12
12
|
export function Controller(path: string) {
|
|
13
13
|
return function <T>(cls: Class<T>): void {
|
|
14
14
|
ControllerRegistryIndex.getForRegister(cls).register({ basePath: path, class: cls, });
|
|
15
|
-
DependencyRegistryIndex.
|
|
15
|
+
DependencyRegistryIndex.registerClass(cls);
|
|
16
16
|
};
|
|
17
17
|
}
|
|
@@ -61,7 +61,7 @@ export class AcceptInterceptor implements WebInterceptor<AcceptConfig> {
|
|
|
61
61
|
this.validate(request, config);
|
|
62
62
|
return response = await next();
|
|
63
63
|
} catch (error) {
|
|
64
|
-
throw response =
|
|
64
|
+
throw response = WebCommonUtil.catchResponse(error);
|
|
65
65
|
} finally {
|
|
66
66
|
response?.headers.setIfAbsent('Accept', config.types.join(','));
|
|
67
67
|
}
|
package/src/interceptor/body.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Injectable, Inject, DependencyRegistryIndex } from '@travetto/di';
|
|
1
|
+
import { Injectable, Inject, DependencyRegistryIndex, PostConstruct } from '@travetto/di';
|
|
2
2
|
import { Config } from '@travetto/config';
|
|
3
3
|
import { toConcrete } from '@travetto/runtime';
|
|
4
4
|
import { Ignore } from '@travetto/schema';
|
|
@@ -63,7 +63,8 @@ export class BodyInterceptor implements WebInterceptor<WebBodyConfig> {
|
|
|
63
63
|
@Inject()
|
|
64
64
|
config: WebBodyConfig;
|
|
65
65
|
|
|
66
|
-
|
|
66
|
+
@PostConstruct()
|
|
67
|
+
async loadParsers(): Promise<void> {
|
|
67
68
|
// Load all the parser types
|
|
68
69
|
const instances = await DependencyRegistryIndex.getInstances(toConcrete<BodyContentParser>());
|
|
69
70
|
for (const instance of instances) {
|
|
@@ -78,7 +79,7 @@ export class BodyInterceptor implements WebInterceptor<WebBodyConfig> {
|
|
|
78
79
|
async filter({ request, config, next }: WebChainedContext<WebBodyConfig>): Promise<WebResponse> {
|
|
79
80
|
const input = request.body;
|
|
80
81
|
|
|
81
|
-
if (!WebBodyUtil.
|
|
82
|
+
if (!WebBodyUtil.isRawBinary(input)) {
|
|
82
83
|
return next();
|
|
83
84
|
}
|
|
84
85
|
|
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
import
|
|
2
|
-
import
|
|
1
|
+
import zlib from 'node:zlib';
|
|
2
|
+
import util from 'node:util';
|
|
3
3
|
|
|
4
4
|
import { Injectable, Inject } from '@travetto/di';
|
|
5
5
|
import { Config } from '@travetto/config';
|
|
6
|
+
import { BinaryUtil } from '@travetto/runtime';
|
|
6
7
|
|
|
7
8
|
import type { WebInterceptor, WebInterceptorContext } from '../types/interceptor.ts';
|
|
8
9
|
import type { WebInterceptorCategory } from '../types/core.ts';
|
|
@@ -13,13 +14,19 @@ import { WebError } from '../types/error.ts';
|
|
|
13
14
|
import { WebBodyUtil } from '../util/body.ts';
|
|
14
15
|
import { WebHeaderUtil } from '../util/header.ts';
|
|
15
16
|
|
|
16
|
-
const
|
|
17
|
-
gzip: createGzip,
|
|
18
|
-
deflate: createDeflate,
|
|
19
|
-
br: createBrotliCompress,
|
|
17
|
+
const STREAM_COMPRESSORS = {
|
|
18
|
+
gzip: zlib.createGzip,
|
|
19
|
+
deflate: zlib.createDeflate,
|
|
20
|
+
br: zlib.createBrotliCompress,
|
|
20
21
|
};
|
|
21
22
|
|
|
22
|
-
|
|
23
|
+
const ARRAY_COMPRESSORS = {
|
|
24
|
+
gzip: util.promisify(zlib.gzip),
|
|
25
|
+
deflate: util.promisify(zlib.deflate),
|
|
26
|
+
br: util.promisify(zlib.brotliCompress),
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
type WebCompressEncoding = keyof typeof ARRAY_COMPRESSORS | 'identity';
|
|
23
30
|
|
|
24
31
|
@Config('web.compress')
|
|
25
32
|
export class CompressConfig {
|
|
@@ -30,7 +37,7 @@ export class CompressConfig {
|
|
|
30
37
|
/**
|
|
31
38
|
* Raw encoding options
|
|
32
39
|
*/
|
|
33
|
-
raw?: (ZlibOptions & BrotliOptions) | undefined;
|
|
40
|
+
raw?: (zlib.ZlibOptions & zlib.BrotliOptions) | undefined;
|
|
34
41
|
/**
|
|
35
42
|
* Supported encodings
|
|
36
43
|
*/
|
|
@@ -63,38 +70,38 @@ export class CompressInterceptor implements WebInterceptor {
|
|
|
63
70
|
}
|
|
64
71
|
|
|
65
72
|
const accepts = request.headers.get('Accept-Encoding');
|
|
66
|
-
const
|
|
73
|
+
const encoding = WebHeaderUtil.negotiateHeader(accepts ?? '*', supportedEncodings);
|
|
67
74
|
|
|
68
|
-
if (!
|
|
75
|
+
if (!encoding) {
|
|
69
76
|
throw WebError.for(`Please accept one of: ${supportedEncodings.join(', ')}. ${accepts} is not supported`, 406);
|
|
70
77
|
}
|
|
71
78
|
|
|
72
|
-
if (
|
|
79
|
+
if (encoding === 'identity') {
|
|
73
80
|
return response;
|
|
74
81
|
}
|
|
75
82
|
|
|
76
83
|
const binaryResponse = new WebResponse({ context: response.context, ...WebBodyUtil.toBinaryMessage(response) });
|
|
77
|
-
const chunkSize = raw.chunkSize ?? constants.Z_DEFAULT_CHUNK;
|
|
78
|
-
const len =
|
|
84
|
+
const chunkSize = raw.chunkSize ?? zlib.constants.Z_DEFAULT_CHUNK;
|
|
85
|
+
const len = BinaryUtil.isBinaryArray(binaryResponse.body) ? binaryResponse.body.byteLength : undefined;
|
|
79
86
|
|
|
80
87
|
if (len !== undefined && len >= 0 && len < chunkSize || !binaryResponse.body) {
|
|
81
88
|
return binaryResponse;
|
|
82
89
|
}
|
|
83
90
|
|
|
84
|
-
const options =
|
|
85
|
-
const stream = COMPRESSORS[type](options);
|
|
91
|
+
const options = encoding === 'br' ? { params: { [zlib.constants.BROTLI_PARAM_QUALITY]: 4, ...raw.params }, ...raw } : { ...raw };
|
|
86
92
|
|
|
87
93
|
// If we are compressing
|
|
88
|
-
binaryResponse.headers.set('Content-Encoding',
|
|
94
|
+
binaryResponse.headers.set('Content-Encoding', encoding);
|
|
89
95
|
|
|
90
|
-
if (
|
|
91
|
-
|
|
92
|
-
const out = await
|
|
96
|
+
if (BinaryUtil.isBinaryArray(binaryResponse.body)) {
|
|
97
|
+
const compressor = ARRAY_COMPRESSORS[encoding];
|
|
98
|
+
const out = await compressor(await BinaryUtil.toBuffer(binaryResponse.body), options);
|
|
93
99
|
binaryResponse.body = out;
|
|
94
100
|
binaryResponse.headers.set('Content-Length', `${out.byteLength}`);
|
|
95
101
|
} else {
|
|
96
|
-
|
|
97
|
-
binaryResponse.body
|
|
102
|
+
const compressedStream = STREAM_COMPRESSORS[encoding](options);
|
|
103
|
+
void BinaryUtil.pipeline(binaryResponse.body, compressedStream);
|
|
104
|
+
binaryResponse.body = compressedStream;
|
|
98
105
|
binaryResponse.headers.delete('Content-Length');
|
|
99
106
|
}
|
|
100
107
|
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Injectable, Inject } from '@travetto/di';
|
|
1
|
+
import { Injectable, Inject, PostConstruct } from '@travetto/di';
|
|
2
2
|
import { Config } from '@travetto/config';
|
|
3
3
|
import { Secret } from '@travetto/schema';
|
|
4
4
|
import { type AsyncContext, AsyncContextValue } from '@travetto/context';
|
|
@@ -10,6 +10,7 @@ import type { WebInterceptorCategory } from '../types/core.ts';
|
|
|
10
10
|
|
|
11
11
|
import type { WebConfig } from '../config.ts';
|
|
12
12
|
import type { Cookie, CookieSetOptions } from '../types/cookie.ts';
|
|
13
|
+
import { KeyGrip } from '../util/keygrip.ts';
|
|
13
14
|
import { CookieJar } from '../util/cookie.ts';
|
|
14
15
|
import type { WebAsyncContext } from '../context.ts';
|
|
15
16
|
|
|
@@ -61,6 +62,8 @@ export class CookieInterceptor implements WebInterceptor<CookieConfig> {
|
|
|
61
62
|
|
|
62
63
|
#cookieJar = new AsyncContextValue<CookieJar>(this);
|
|
63
64
|
|
|
65
|
+
keyGrip: KeyGrip;
|
|
66
|
+
|
|
64
67
|
category: WebInterceptorCategory = 'request';
|
|
65
68
|
|
|
66
69
|
@Inject()
|
|
@@ -75,15 +78,16 @@ export class CookieInterceptor implements WebInterceptor<CookieConfig> {
|
|
|
75
78
|
@Inject()
|
|
76
79
|
context: AsyncContext;
|
|
77
80
|
|
|
78
|
-
|
|
81
|
+
@PostConstruct()
|
|
82
|
+
exposeContext(): void {
|
|
79
83
|
this.webAsyncContext.registerSource(CookieJar, () => this.#cookieJar.get());
|
|
84
|
+
this.keyGrip ??= new KeyGrip(this.config.keys ?? []);
|
|
80
85
|
}
|
|
81
86
|
|
|
82
87
|
finalizeConfig({ config }: WebInterceptorContext<CookieConfig>): CookieConfig {
|
|
83
88
|
const url = new URL(this.webConfig.baseUrl ?? 'x://localhost');
|
|
84
89
|
config.secure ??= url.protocol === 'https:';
|
|
85
90
|
config.domain ??= url.hostname;
|
|
86
|
-
config.signed ??= !!config.keys?.length;
|
|
87
91
|
return config;
|
|
88
92
|
}
|
|
89
93
|
|
|
@@ -92,11 +96,12 @@ export class CookieInterceptor implements WebInterceptor<CookieConfig> {
|
|
|
92
96
|
}
|
|
93
97
|
|
|
94
98
|
async filter({ request, config, next }: WebChainedContext<CookieConfig>): Promise<WebResponse> {
|
|
95
|
-
const jar = new CookieJar(config
|
|
99
|
+
const jar = new CookieJar(config, this.keyGrip);
|
|
100
|
+
await jar.importCookieHeader(request.headers.get('Cookie'));
|
|
96
101
|
this.#cookieJar.set(jar);
|
|
97
102
|
|
|
98
103
|
const response = await next();
|
|
99
|
-
for (const cookie of jar.exportSetCookieHeader()) { response.headers.append('Set-Cookie', cookie); }
|
|
104
|
+
for (const cookie of await jar.exportSetCookieHeader()) { response.headers.append('Set-Cookie', cookie); }
|
|
100
105
|
return response;
|
|
101
106
|
}
|
|
102
107
|
}
|
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import type { Readable } from 'node:stream';
|
|
2
1
|
import zlib from 'node:zlib';
|
|
3
2
|
import util from 'node:util';
|
|
3
|
+
import { pipeline } from 'node:stream/promises';
|
|
4
4
|
|
|
5
5
|
import { Injectable, Inject } from '@travetto/di';
|
|
6
6
|
import { Config } from '@travetto/config';
|
|
7
|
-
import { castTo } from '@travetto/runtime';
|
|
7
|
+
import { BinaryUtil, castTo, type BinaryType } from '@travetto/runtime';
|
|
8
8
|
|
|
9
9
|
import type { WebChainedContext } from '../types/filter.ts';
|
|
10
10
|
import type { WebResponse } from '../types/response.ts';
|
|
@@ -18,18 +18,16 @@ import { WebError } from '../types/error.ts';
|
|
|
18
18
|
const STREAM_DECOMPRESSORS = {
|
|
19
19
|
gzip: zlib.createGunzip,
|
|
20
20
|
deflate: zlib.createInflate,
|
|
21
|
-
br: zlib.createBrotliDecompress
|
|
22
|
-
identity: (): Readable => null!
|
|
21
|
+
br: zlib.createBrotliDecompress
|
|
23
22
|
};
|
|
24
23
|
|
|
25
|
-
const
|
|
24
|
+
const ARRAY_DECOMPRESSORS = {
|
|
26
25
|
gzip: util.promisify(zlib.gunzip),
|
|
27
26
|
deflate: util.promisify(zlib.inflate),
|
|
28
|
-
br: util.promisify(zlib.brotliDecompress)
|
|
29
|
-
identity: (): Readable => null!
|
|
27
|
+
br: util.promisify(zlib.brotliDecompress)
|
|
30
28
|
};
|
|
31
29
|
|
|
32
|
-
type WebDecompressEncoding = keyof typeof
|
|
30
|
+
type WebDecompressEncoding = (keyof typeof ARRAY_DECOMPRESSORS) | 'identity';
|
|
33
31
|
|
|
34
32
|
/**
|
|
35
33
|
* Web body parse configuration
|
|
@@ -52,8 +50,8 @@ export class DecompressConfig {
|
|
|
52
50
|
@Injectable()
|
|
53
51
|
export class DecompressInterceptor implements WebInterceptor<DecompressConfig> {
|
|
54
52
|
|
|
55
|
-
static async decompress(headers: WebHeaders, input:
|
|
56
|
-
const encoding: WebDecompressEncoding
|
|
53
|
+
static async decompress(headers: WebHeaders, input: BinaryType, config: DecompressConfig): Promise<BinaryType> {
|
|
54
|
+
const encoding: WebDecompressEncoding = castTo(headers.getList('Content-Encoding')?.[0]) ?? 'identity';
|
|
57
55
|
|
|
58
56
|
if (!config.supportedEncodings.includes(encoding)) {
|
|
59
57
|
throw WebError.for(`Unsupported Content-Encoding: ${encoding}`, 415);
|
|
@@ -63,10 +61,14 @@ export class DecompressInterceptor implements WebInterceptor<DecompressConfig> {
|
|
|
63
61
|
return input;
|
|
64
62
|
}
|
|
65
63
|
|
|
66
|
-
if (
|
|
67
|
-
return
|
|
64
|
+
if (BinaryUtil.isBinaryArray(input)) {
|
|
65
|
+
return ARRAY_DECOMPRESSORS[encoding](await BinaryUtil.toBinaryArray(input));
|
|
66
|
+
} else if (BinaryUtil.isBinaryStream(input)) {
|
|
67
|
+
const output = STREAM_DECOMPRESSORS[encoding]();
|
|
68
|
+
void pipeline(input, output);
|
|
69
|
+
return output;
|
|
68
70
|
} else {
|
|
69
|
-
|
|
71
|
+
throw WebError.for('Unable to decompress body: unsupported type', 400);
|
|
70
72
|
}
|
|
71
73
|
}
|
|
72
74
|
|
|
@@ -81,9 +83,9 @@ export class DecompressInterceptor implements WebInterceptor<DecompressConfig> {
|
|
|
81
83
|
}
|
|
82
84
|
|
|
83
85
|
async filter({ request, config, next }: WebChainedContext<DecompressConfig>): Promise<WebResponse> {
|
|
84
|
-
if (WebBodyUtil.
|
|
86
|
+
if (WebBodyUtil.isRawBinary(request.body)) {
|
|
85
87
|
const updatedBody = await DecompressInterceptor.decompress(request.headers, request.body, config);
|
|
86
|
-
request.body = WebBodyUtil.
|
|
88
|
+
request.body = WebBodyUtil.markRawBinary(updatedBody);
|
|
87
89
|
}
|
|
88
90
|
return next();
|
|
89
91
|
}
|
package/src/interceptor/etag.ts
CHANGED
|
@@ -1,9 +1,7 @@
|
|
|
1
|
-
import crypto from 'node:crypto';
|
|
2
|
-
|
|
3
1
|
import { Injectable, Inject } from '@travetto/di';
|
|
4
2
|
import { Config } from '@travetto/config';
|
|
5
3
|
import { Ignore } from '@travetto/schema';
|
|
6
|
-
import { BinaryUtil } from '@travetto/runtime';
|
|
4
|
+
import { BinaryMetadataUtil, BinaryUtil, type BinaryArray } from '@travetto/runtime';
|
|
7
5
|
|
|
8
6
|
import type { WebChainedContext } from '../types/filter.ts';
|
|
9
7
|
import { WebResponse } from '../types/response.ts';
|
|
@@ -48,14 +46,10 @@ export class EtagInterceptor implements WebInterceptor {
|
|
|
48
46
|
@Inject()
|
|
49
47
|
config: EtagConfig;
|
|
50
48
|
|
|
51
|
-
computeTag(body:
|
|
49
|
+
computeTag(body: BinaryArray): string {
|
|
52
50
|
return body.byteLength === 0 ?
|
|
53
51
|
'2jmj7l5rSw0yVb/vlWAYkK/YBwk' :
|
|
54
|
-
|
|
55
|
-
.createHash('sha1')
|
|
56
|
-
.update(body.toString('utf8'), 'utf8')
|
|
57
|
-
.digest('base64')
|
|
58
|
-
.substring(0, 27);
|
|
52
|
+
BinaryMetadataUtil.hash(body, { length: 27, hashAlgorithm: 'sha1', outputEncoding: 'base64' });
|
|
59
53
|
}
|
|
60
54
|
|
|
61
55
|
addTag(ctx: WebChainedContext<EtagConfig>, response: WebResponse): WebResponse {
|
|
@@ -65,7 +59,7 @@ export class EtagInterceptor implements WebInterceptor {
|
|
|
65
59
|
|
|
66
60
|
if (
|
|
67
61
|
(statusCode >= 300 && statusCode !== 304) || // Ignore redirects
|
|
68
|
-
BinaryUtil.
|
|
62
|
+
BinaryUtil.isBinaryStream(response.body) // Ignore streams (unknown length)
|
|
69
63
|
) {
|
|
70
64
|
return response;
|
|
71
65
|
}
|
|
@@ -73,12 +67,12 @@ export class EtagInterceptor implements WebInterceptor {
|
|
|
73
67
|
const binaryResponse = new WebResponse({ ...response, ...WebBodyUtil.toBinaryMessage(response) });
|
|
74
68
|
|
|
75
69
|
const body = binaryResponse.body;
|
|
76
|
-
if (!
|
|
70
|
+
if (!BinaryUtil.isBinaryArray(body)) {
|
|
77
71
|
return binaryResponse;
|
|
78
72
|
}
|
|
79
73
|
|
|
80
74
|
const minSize = ctx.config._minimumSize ??= WebCommonUtil.parseByteSize(ctx.config.minimumSize);
|
|
81
|
-
if (body.
|
|
75
|
+
if (body.byteLength < minSize) {
|
|
82
76
|
return binaryResponse;
|
|
83
77
|
}
|
|
84
78
|
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { RegistryAdapter } from '@travetto/registry';
|
|
2
|
-
import {
|
|
2
|
+
import { RuntimeError, asFull, castTo, type Class, type RetainIntrinsicFields, safeAssign } from '@travetto/runtime';
|
|
3
3
|
import { WebHeaders } from '@travetto/web';
|
|
4
4
|
import { type SchemaParameterConfig, SchemaRegistryIndex } from '@travetto/schema';
|
|
5
5
|
|
|
@@ -60,7 +60,7 @@ function computeParameterLocation(endpoint: EndpointConfig, param: SchemaParamet
|
|
|
60
60
|
if (!SchemaRegistryIndex.has(param.type)) {
|
|
61
61
|
if ((param.type === String || param.type === Number) && name && endpoint.path.includes(`:${name}`)) {
|
|
62
62
|
return 'path';
|
|
63
|
-
} else if (param.
|
|
63
|
+
} else if (param.binary) {
|
|
64
64
|
return 'body';
|
|
65
65
|
}
|
|
66
66
|
return 'query';
|
|
@@ -100,7 +100,7 @@ export class ControllerRegistryAdapter implements RegistryAdapter<ControllerConf
|
|
|
100
100
|
registerEndpoint(method: string, ...data: Partial<EndpointConfig>[]): EndpointConfig {
|
|
101
101
|
this.register();
|
|
102
102
|
|
|
103
|
-
|
|
103
|
+
const resolved = this.#endpoints.getOrInsertComputed(method, () => {
|
|
104
104
|
const endpointConfig = asFull<EndpointConfig>({
|
|
105
105
|
path: '/',
|
|
106
106
|
fullPath: '/',
|
|
@@ -117,11 +117,11 @@ export class ControllerRegistryAdapter implements RegistryAdapter<ControllerConf
|
|
|
117
117
|
responseFinalizer: undefined
|
|
118
118
|
});
|
|
119
119
|
this.#config.endpoints.push(endpointConfig);
|
|
120
|
-
|
|
121
|
-
}
|
|
120
|
+
return endpointConfig;
|
|
121
|
+
});
|
|
122
122
|
|
|
123
|
-
combineEndpointConfigs(this.#config,
|
|
124
|
-
return
|
|
123
|
+
combineEndpointConfigs(this.#config, resolved, ...data);
|
|
124
|
+
return resolved;
|
|
125
125
|
}
|
|
126
126
|
|
|
127
127
|
registerEndpointParameter(method: string, idx: number, ...config: Partial<EndpointParameterConfig>[]): EndpointParameterConfig {
|
|
@@ -156,14 +156,14 @@ export class ControllerRegistryAdapter implements RegistryAdapter<ControllerConf
|
|
|
156
156
|
getEndpointConfig(method: string): EndpointConfig {
|
|
157
157
|
const endpoint = this.#endpoints.get(method);
|
|
158
158
|
if (!endpoint) {
|
|
159
|
-
throw new
|
|
159
|
+
throw new RuntimeError(`Endpoint not registered: ${String(method)} on ${this.#cls.name}`);
|
|
160
160
|
}
|
|
161
161
|
return endpoint;
|
|
162
162
|
}
|
|
163
163
|
|
|
164
164
|
registerInterceptorConfig<T extends WebInterceptor>(
|
|
165
165
|
cls: Class<T>,
|
|
166
|
-
config: Partial<
|
|
166
|
+
config: Partial<RetainIntrinsicFields<T['config']>>,
|
|
167
167
|
extra?: Partial<EndpointConfig & ControllerConfig>
|
|
168
168
|
): ControllerConfig {
|
|
169
169
|
return this.register({ interceptorConfigs: [[cls, castTo(config)]], ...extra });
|
|
@@ -172,7 +172,7 @@ export class ControllerRegistryAdapter implements RegistryAdapter<ControllerConf
|
|
|
172
172
|
registerEndpointInterceptorConfig<T extends WebInterceptor>(
|
|
173
173
|
property: string,
|
|
174
174
|
cls: Class<T>,
|
|
175
|
-
config: Partial<
|
|
175
|
+
config: Partial<RetainIntrinsicFields<T['config']>>,
|
|
176
176
|
extra?: Partial<EndpointConfig>
|
|
177
177
|
): EndpointConfig {
|
|
178
178
|
return this.registerEndpoint(property, { interceptorConfigs: [[cls, castTo(config)]], ...extra });
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { type RegistryIndex, RegistryIndexStore, Registry } from '@travetto/registry';
|
|
2
|
-
import { type Class, type ClassInstance, getClass, isClass, type
|
|
2
|
+
import { type Class, type ClassInstance, getClass, isClass, type RetainIntrinsicFields } from '@travetto/runtime';
|
|
3
3
|
import { DependencyRegistryIndex } from '@travetto/di';
|
|
4
4
|
import { SchemaRegistryIndex } from '@travetto/schema';
|
|
5
5
|
|
|
@@ -39,7 +39,7 @@ export class ControllerRegistryIndex implements RegistryIndex {
|
|
|
39
39
|
*/
|
|
40
40
|
static createInterceptorConfigDecorator<T extends WebInterceptor>(
|
|
41
41
|
cls: Class<T>,
|
|
42
|
-
config: Partial<
|
|
42
|
+
config: Partial<RetainIntrinsicFields<T['config']>>,
|
|
43
43
|
extra?: Partial<EndpointConfig & ControllerConfig>
|
|
44
44
|
): EndpointDecorator {
|
|
45
45
|
return (instanceOrCls: Class | ClassInstance, property?: string): void => {
|
|
@@ -74,10 +74,10 @@ export class ControllerRegistryIndex implements RegistryIndex {
|
|
|
74
74
|
* @param target Controller class
|
|
75
75
|
*/
|
|
76
76
|
bindContextParamsOnPostConstruct(cls: Class): void {
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
77
|
+
const bind = (instance: ClassInstance): Promise<void> => this.#bindContextParams(instance);
|
|
78
|
+
DependencyRegistryIndex.registerPostConstruct(cls, {
|
|
79
|
+
operation(this: ClassInstance): Promise<void> { return bind(this); },
|
|
80
|
+
priority: 100
|
|
81
81
|
});
|
|
82
82
|
}
|
|
83
83
|
|