@travetto/web 6.0.0-rc.4 → 6.0.0-rc.5

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 CHANGED
@@ -57,6 +57,7 @@ export interface WebRequestContext {
57
57
 
58
58
  /**
59
59
  * Web Request object
60
+ * @web_contextual
60
61
  */
61
62
  export class WebRequest<B = unknown> extends BaseWebMessage<B, Readonly<WebRequestContext>> {
62
63
 
@@ -73,6 +74,7 @@ export interface WebResponseContext {
73
74
 
74
75
  /**
75
76
  * Web Response as a simple object
77
+ * @web_invalid_parameter
76
78
  */
77
79
  export class WebResponse<B = unknown> extends BaseWebMessage<B, WebResponseContext> {
78
80
 
@@ -377,7 +379,7 @@ Out of the box, the web framework comes with a few interceptors, and more are co
377
379
  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)
378
380
  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)
379
381
  1. pre-request - Prepares the request for running - [TrustProxyInterceptor](https://github.com/travetto/travetto/tree/main/module/web/src/interceptor/trust-proxy.ts#L23)
380
- 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#L34), [BodyParseInterceptor](https://github.com/travetto/travetto/tree/main/module/web/src/interceptor/body-parse.ts#L61), [CookiesInterceptor](https://github.com/travetto/travetto/tree/main/module/web/src/interceptor/cookies.ts#L56)
382
+ 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#L34), [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#L60)
381
383
  1. response - Prepares outbound response - [CompressInterceptor](https://github.com/travetto/travetto/tree/main/module/web/src/interceptor/compress.ts#L50), [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#L34), [ResponseCacheInterceptor](https://github.com/travetto/travetto/tree/main/module/web/src/interceptor/response-cache.ts#L30)
382
384
  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.
383
385
 
@@ -460,8 +462,8 @@ export class DecompressConfig {
460
462
  }
461
463
  ```
462
464
 
463
- #### CookiesInterceptor
464
- [CookiesInterceptor](https://github.com/travetto/travetto/tree/main/module/web/src/interceptor/cookies.ts#L56) is responsible for processing inbound cookie headers and populating the appropriate data on the request, as well as sending the appropriate response data
465
+ #### CookieInterceptor
466
+ [CookieInterceptor](https://github.com/travetto/travetto/tree/main/module/web/src/interceptor/cookie.ts#L60) is responsible for processing inbound cookie headers and populating the appropriate data on the request, as well as sending the appropriate response data
465
467
 
466
468
  **Code: Cookies Config**
467
469
  ```typescript
@@ -473,7 +475,7 @@ export class CookieConfig implements CookieSetOptions {
473
475
  /**
474
476
  * Are they signed
475
477
  */
476
- signed = true;
478
+ signed?: boolean;
477
479
  /**
478
480
  * Supported only via http (not in JS)
479
481
  */
@@ -490,20 +492,24 @@ export class CookieConfig implements CookieSetOptions {
490
492
  /**
491
493
  * Is the cookie only valid for https
492
494
  */
493
- secure?: boolean = false;
495
+ secure?: boolean;
494
496
  /**
495
497
  * The domain of the cookie
496
498
  */
497
499
  domain?: string;
500
+ /**
501
+ * The default path of the cookie
502
+ */
503
+ path: string = '/';
498
504
  }
499
505
  ```
500
506
 
501
- #### BodyParseInterceptor
502
- [BodyParseInterceptor](https://github.com/travetto/travetto/tree/main/module/web/src/interceptor/body-parse.ts#L61) handles the inbound request, and converting the body payload into an appropriate format.
507
+ #### BodyInterceptor
508
+ [BodyInterceptor](https://github.com/travetto/travetto/tree/main/module/web/src/interceptor/body.ts#L57) handles the inbound request, and converting the body payload into an appropriate format.
503
509
 
504
- **Code: Body Parse Config**
510
+ **Code: Body Config**
505
511
  ```typescript
506
- export class BodyParseConfig {
512
+ export class WebBodyConfig {
507
513
  /**
508
514
  * Parse request body
509
515
  */
@@ -523,10 +529,6 @@ export class BodyParseConfig {
523
529
 
524
530
  @Ignore()
525
531
  _limit: number | undefined;
526
-
527
- postConstruct(): void {
528
- this._limit = WebCommonUtil.parseByteSize(this.limit);
529
- }
530
532
  }
531
533
  ```
532
534
 
package/__index__.ts CHANGED
@@ -24,7 +24,7 @@ export * from './src/registry/visitor.ts';
24
24
  export * from './src/registry/types.ts';
25
25
 
26
26
  export * from './src/interceptor/accept.ts';
27
- export * from './src/interceptor/body-parse.ts';
27
+ export * from './src/interceptor/body.ts';
28
28
  export * from './src/interceptor/cors.ts';
29
29
  export * from './src/interceptor/cookie.ts';
30
30
  export * from './src/interceptor/compress.ts';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@travetto/web",
3
- "version": "6.0.0-rc.4",
3
+ "version": "6.0.0-rc.5",
4
4
  "description": "Declarative api for Web Applications with support for the dependency injection.",
5
5
  "keywords": [
6
6
  "web",
@@ -37,13 +37,12 @@
37
37
  "@types/negotiator": "^0.6.3",
38
38
  "find-my-way": "^9.3.0",
39
39
  "fresh": "^0.5.2",
40
- "iconv-lite": "^0.6.3",
41
40
  "keygrip": "^1.1.0",
42
41
  "negotiator": "^1.0.0"
43
42
  },
44
43
  "peerDependencies": {
45
44
  "@travetto/cli": "^6.0.0-rc.3",
46
- "@travetto/test": "^6.0.0-rc.2",
45
+ "@travetto/test": "^6.0.0-rc.3",
47
46
  "@travetto/transformer": "^6.0.0-rc.3"
48
47
  },
49
48
  "peerDependenciesMeta": {
package/src/context.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import { AsyncContextValue, AsyncContext } from '@travetto/context';
2
2
  import { Inject, Injectable } from '@travetto/di';
3
- import { AppError, castTo, Class, toConcrete } from '@travetto/runtime';
3
+ import { AppError, castTo, Class } from '@travetto/runtime';
4
4
 
5
5
  import { WebRequest } from './types/request.ts';
6
6
 
@@ -21,7 +21,7 @@ export class WebAsyncContext {
21
21
  }
22
22
 
23
23
  postConstruct(): void {
24
- this.registerSource(toConcrete<WebRequest>(), () => this.#request.get());
24
+ this.registerSource(WebRequest, () => this.#request.get());
25
25
  }
26
26
 
27
27
  withContext<T>(request: WebRequest, next: () => Promise<T>): Promise<T> {
@@ -24,10 +24,10 @@ export interface BodyContentParser {
24
24
  };
25
25
 
26
26
  /**
27
- * Web body parse configuration
27
+ * Web body configuration
28
28
  */
29
- @Config('web.bodyParse')
30
- export class BodyParseConfig {
29
+ @Config('web.body')
30
+ export class WebBodyConfig {
31
31
  /**
32
32
  * Parse request body
33
33
  */
@@ -47,25 +47,21 @@ export class BodyParseConfig {
47
47
 
48
48
  @Ignore()
49
49
  _limit: number | undefined;
50
-
51
- postConstruct(): void {
52
- this._limit = WebCommonUtil.parseByteSize(this.limit);
53
- }
54
50
  }
55
51
 
56
52
 
57
53
  /**
58
- * Parses the body input content
54
+ * Verifies content length, decodes character encodings, and parses body input string via the content type
59
55
  */
60
56
  @Injectable()
61
- export class BodyParseInterceptor implements WebInterceptor<BodyParseConfig> {
57
+ export class BodyInterceptor implements WebInterceptor<WebBodyConfig> {
62
58
 
63
59
  dependsOn = [AcceptInterceptor, DecompressInterceptor];
64
60
  category: WebInterceptorCategory = 'request';
65
61
  parsers: Record<string, BodyContentParser> = {};
66
62
 
67
63
  @Inject()
68
- config: BodyParseConfig;
64
+ config: WebBodyConfig;
69
65
 
70
66
  async postConstruct(): Promise<void> {
71
67
  // Load all the parser types
@@ -75,11 +71,11 @@ export class BodyParseInterceptor implements WebInterceptor<BodyParseConfig> {
75
71
  }
76
72
  }
77
73
 
78
- applies({ endpoint, config }: WebInterceptorContext<BodyParseConfig>): boolean {
74
+ applies({ endpoint, config }: WebInterceptorContext<WebBodyConfig>): boolean {
79
75
  return config.applies && endpoint.allowsBody;
80
76
  }
81
77
 
82
- async filter({ request, config, next }: WebChainedContext<BodyParseConfig>): Promise<WebResponse> {
78
+ async filter({ request, config, next }: WebChainedContext<WebBodyConfig>): Promise<WebResponse> {
83
79
  const input = request.body;
84
80
 
85
81
  if (!WebBodyUtil.isRaw(input)) {
@@ -88,8 +84,8 @@ export class BodyParseInterceptor implements WebInterceptor<BodyParseConfig> {
88
84
 
89
85
  const lengthRead = +(request.headers.get('Content-Length') || '');
90
86
  const length = Number.isNaN(lengthRead) ? undefined : lengthRead;
87
+ const limit = config._limit ??= WebCommonUtil.parseByteSize(config.limit);
91
88
 
92
- const limit = config._limit ?? Number.MAX_SAFE_INTEGER;
93
89
  if (length && length > limit) {
94
90
  throw WebError.for('Request entity too large', 413, { length, limit });
95
91
  }
@@ -18,6 +18,7 @@ export interface WebRequestContext {
18
18
 
19
19
  /**
20
20
  * Web Request object
21
+ * @web_contextual
21
22
  */
22
23
  export class WebRequest<B = unknown> extends BaseWebMessage<B, Readonly<WebRequestContext>> {
23
24
 
@@ -6,6 +6,7 @@ export interface WebResponseContext {
6
6
 
7
7
  /**
8
8
  * Web Response as a simple object
9
+ * @web_invalid_parameter
9
10
  */
10
11
  export class WebResponse<B = unknown> extends BaseWebMessage<B, WebResponseContext> {
11
12
 
package/src/util/body.ts CHANGED
@@ -1,5 +1,4 @@
1
- import iconv from 'iconv-lite';
2
-
1
+ import { TextDecoder } from 'node:util';
3
2
  import { Readable } from 'node:stream';
4
3
  import { buffer as toBuffer } from 'node:stream/consumers';
5
4
 
@@ -187,27 +186,33 @@ export class WebBodyUtil {
187
186
  static async readText(input: Readable | Buffer, limit: number, encoding?: string): Promise<{ text: string, read: number }> {
188
187
  encoding ??= (Buffer.isBuffer(input) ? undefined : input.readableEncoding) ?? 'utf-8';
189
188
 
190
- if (!iconv.encodingExists(encoding)) {
189
+ let decoder: TextDecoder;
190
+ try {
191
+ decoder = new TextDecoder(encoding);
192
+ } catch {
191
193
  throw WebError.for('Specified Encoding Not Supported', 415, { encoding });
192
194
  }
193
195
 
194
196
  if (Buffer.isBuffer(input)) {
195
- return { text: iconv.decode(input, encoding), read: input.byteLength };
197
+ if (input.byteLength > limit) {
198
+ throw WebError.for('Request Entity Too Large', 413, { received: input.byteLength, limit });
199
+ }
200
+ return { text: decoder.decode(input), read: input.byteLength };
196
201
  }
197
202
 
198
203
  let received = Buffer.isBuffer(input) ? input.byteOffset : 0;
199
- const decoder = iconv.getDecoder(encoding);
200
204
  const all: string[] = [];
201
205
 
202
206
  try {
203
- for await (const chunk of input.iterator({ destroyOnReturn: false })) {
204
- received += Buffer.isBuffer(chunk) ? chunk.byteLength : (typeof chunk === 'string' ? chunk.length : chunk.length);
207
+ for await (const chunk of castTo<AsyncIterable<string | Buffer>>(input.iterator({ destroyOnReturn: false }))) {
208
+ const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk, 'utf8');
209
+ received += buffer.byteLength;
205
210
  if (received > limit) {
206
211
  throw WebError.for('Request Entity Too Large', 413, { received, limit });
207
212
  }
208
- all.push(decoder.write(chunk));
213
+ all.push(decoder.decode(buffer, { stream: true }));
209
214
  }
210
- all.push(decoder.end() ?? '');
215
+ all.push(decoder.decode(Buffer.alloc(0), { stream: false }));
211
216
  return { text: all.join(''), read: received };
212
217
  } catch (err) {
213
218
  if (err instanceof Error && err.name === 'AbortError') {
@@ -44,21 +44,26 @@ export class WebTransformer {
44
44
  // If non-regex
45
45
  if (arg && ts.isStringLiteral(arg)) {
46
46
  const literal = LiteralUtil.toLiteral(arg);
47
- if (typeof literal !== 'string') {
48
- throw new Error(`Unexpected literal type: ${literal}`);
49
- }
50
- // If param name matches path param, default to @Path
47
+ // If param name matches path param, default to @PathParam
51
48
  detectedParamType = new RegExp(`:${name}\\b`).test(literal) ? 'PathParam' : 'QueryParam';
52
49
  } else {
53
50
  // Default to query for empty or regex endpoints
54
51
  detectedParamType = 'QueryParam';
55
52
  }
56
- } else if (epDec.ident.getText() !== 'All') { // Treat all separate
53
+ } else {
57
54
  // Treat as schema, and see if endpoint supports a body for default behavior on untyped
58
55
  detectedParamType = epDec.targets?.includes('@travetto/web:HttpRequestBody') ? 'Body' : 'QueryParam';
59
56
  config.name = '';
60
57
  }
61
58
 
59
+ if (paramType.key === 'managed' && paramType.original) {
60
+ if (DocUtil.hasDocTag(paramType.original, 'web_contextual')) {
61
+ throw new Error(`${paramType.name} must be registered using @ContextParam`);
62
+ } else if (DocUtil.hasDocTag(paramType.original, 'web_invalid_parameter')) {
63
+ throw new Error(`${paramType.name} is an invalid endpoint parameter`);
64
+ }
65
+ }
66
+
62
67
  node = SchemaTransformUtil.computeField(state, node, config);
63
68
 
64
69
  const modifiers = (node.modifiers ?? []).filter(x => x !== pDec);