@travetto/web 6.0.0-rc.3 → 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 +15 -13
- package/__index__.ts +2 -2
- package/package.json +2 -3
- package/src/context.ts +2 -2
- package/src/interceptor/{body-parse.ts → body.ts} +9 -13
- package/src/interceptor/{cookies.ts → cookie.ts} +11 -6
- package/src/types/cookie.ts +1 -1
- package/src/types/request.ts +1 -0
- package/src/types/response.ts +1 -0
- package/src/util/body.ts +14 -9
- package/src/util/cookie.ts +106 -88
- package/support/transformer.web.ts +10 -5
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), [
|
|
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
|
-
####
|
|
464
|
-
[
|
|
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
|
|
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
|
|
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
|
-
####
|
|
502
|
-
[
|
|
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
|
|
510
|
+
**Code: Body Config**
|
|
505
511
|
```typescript
|
|
506
|
-
export class
|
|
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,9 +24,9 @@ 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
|
|
27
|
+
export * from './src/interceptor/body.ts';
|
|
28
28
|
export * from './src/interceptor/cors.ts';
|
|
29
|
-
export * from './src/interceptor/
|
|
29
|
+
export * from './src/interceptor/cookie.ts';
|
|
30
30
|
export * from './src/interceptor/compress.ts';
|
|
31
31
|
export * from './src/interceptor/context.ts';
|
|
32
32
|
export * from './src/interceptor/decompress.ts';
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@travetto/web",
|
|
3
|
-
"version": "6.0.0-rc.
|
|
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.
|
|
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
|
|
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(
|
|
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
|
|
27
|
+
* Web body configuration
|
|
28
28
|
*/
|
|
29
|
-
@Config('web.
|
|
30
|
-
export class
|
|
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
|
-
*
|
|
54
|
+
* Verifies content length, decodes character encodings, and parses body input string via the content type
|
|
59
55
|
*/
|
|
60
56
|
@Injectable()
|
|
61
|
-
export class
|
|
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:
|
|
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<
|
|
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<
|
|
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
|
}
|
|
@@ -25,7 +25,7 @@ export class CookieConfig implements CookieSetOptions {
|
|
|
25
25
|
/**
|
|
26
26
|
* Are they signed
|
|
27
27
|
*/
|
|
28
|
-
signed
|
|
28
|
+
signed?: boolean;
|
|
29
29
|
/**
|
|
30
30
|
* Supported only via http (not in JS)
|
|
31
31
|
*/
|
|
@@ -42,18 +42,22 @@ export class CookieConfig implements CookieSetOptions {
|
|
|
42
42
|
/**
|
|
43
43
|
* Is the cookie only valid for https
|
|
44
44
|
*/
|
|
45
|
-
secure?: boolean
|
|
45
|
+
secure?: boolean;
|
|
46
46
|
/**
|
|
47
47
|
* The domain of the cookie
|
|
48
48
|
*/
|
|
49
49
|
domain?: string;
|
|
50
|
+
/**
|
|
51
|
+
* The default path of the cookie
|
|
52
|
+
*/
|
|
53
|
+
path: string = '/';
|
|
50
54
|
}
|
|
51
55
|
|
|
52
56
|
/**
|
|
53
57
|
* Loads cookies from the request, verifies, exposes, and then signs and sets
|
|
54
58
|
*/
|
|
55
59
|
@Injectable()
|
|
56
|
-
export class
|
|
60
|
+
export class CookieInterceptor implements WebInterceptor<CookieConfig> {
|
|
57
61
|
|
|
58
62
|
#cookieJar = new AsyncContextValue<CookieJar>(this);
|
|
59
63
|
|
|
@@ -77,8 +81,9 @@ export class CookiesInterceptor implements WebInterceptor<CookieConfig> {
|
|
|
77
81
|
|
|
78
82
|
finalizeConfig({ config }: WebInterceptorContext<CookieConfig>): CookieConfig {
|
|
79
83
|
const url = new URL(this.webConfig.baseUrl ?? 'x://localhost');
|
|
80
|
-
config.secure ??= url.protocol === 'https';
|
|
84
|
+
config.secure ??= url.protocol === 'https:';
|
|
81
85
|
config.domain ??= url.hostname;
|
|
86
|
+
config.signed ??= !!config.keys?.length;
|
|
82
87
|
return config;
|
|
83
88
|
}
|
|
84
89
|
|
|
@@ -87,11 +92,11 @@ export class CookiesInterceptor implements WebInterceptor<CookieConfig> {
|
|
|
87
92
|
}
|
|
88
93
|
|
|
89
94
|
async filter({ request, config, next }: WebChainedContext<CookieConfig>): Promise<WebResponse> {
|
|
90
|
-
const jar = new CookieJar(request.headers.get('Cookie')
|
|
95
|
+
const jar = new CookieJar(config).importCookieHeader(request.headers.get('Cookie'));
|
|
91
96
|
this.#cookieJar.set(jar);
|
|
92
97
|
|
|
93
98
|
const response = await next();
|
|
94
|
-
for (const c of jar.
|
|
99
|
+
for (const c of jar.exportSetCookieHeader()) { response.headers.append('Set-Cookie', c); }
|
|
95
100
|
return response;
|
|
96
101
|
}
|
|
97
102
|
}
|
package/src/types/cookie.ts
CHANGED
package/src/types/request.ts
CHANGED
package/src/types/response.ts
CHANGED
package/src/util/body.ts
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
import
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
213
|
+
all.push(decoder.decode(buffer, { stream: true }));
|
|
209
214
|
}
|
|
210
|
-
all.push(decoder.
|
|
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') {
|
package/src/util/cookie.ts
CHANGED
|
@@ -1,14 +1,26 @@
|
|
|
1
1
|
import keygrip from 'keygrip';
|
|
2
2
|
import { AppError, castKey, castTo } from '@travetto/runtime';
|
|
3
3
|
|
|
4
|
-
import { Cookie, CookieGetOptions } from '../types/cookie.ts';
|
|
4
|
+
import { Cookie, CookieGetOptions, CookieSetOptions } from '../types/cookie.ts';
|
|
5
5
|
|
|
6
6
|
const pairText = (c: Cookie): string => `${c.name}=${c.value}`;
|
|
7
7
|
const pair = (k: string, v: unknown): string => `${k}=${v}`;
|
|
8
8
|
|
|
9
|
+
type CookieJarOptions = { keys?: string[] } & CookieSetOptions;
|
|
10
|
+
|
|
9
11
|
export class CookieJar {
|
|
10
12
|
|
|
11
|
-
static
|
|
13
|
+
static parseCookieHeader(header: string): Cookie[] {
|
|
14
|
+
return header.split(/\s{0,4};\s{0,4}/g)
|
|
15
|
+
.map(x => x.trim())
|
|
16
|
+
.filter(x => !!x)
|
|
17
|
+
.map(item => {
|
|
18
|
+
const kv = item.split(/\s{0,4}=\s{0,4}/);
|
|
19
|
+
return { name: kv[0], value: kv[1] };
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
static parseSetCookieHeader(header: string): Cookie {
|
|
12
24
|
const parts = header.split(/\s{0,4};\s{0,4}/g);
|
|
13
25
|
const [name, value] = parts[0].split(/\s{0,4}=\s{0,4}/);
|
|
14
26
|
const c: Cookie = { name, value };
|
|
@@ -27,119 +39,125 @@ export class CookieJar {
|
|
|
27
39
|
return c;
|
|
28
40
|
}
|
|
29
41
|
|
|
30
|
-
static
|
|
31
|
-
const
|
|
32
|
-
if (
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
if (c.path) { header.push(pair('path', c.path)); }
|
|
42
|
-
if (c.expires) { header.push(pair('expires', c.expires.toUTCString())); }
|
|
43
|
-
if (c.domain) { header.push(pair('domain', c.domain)); }
|
|
44
|
-
if (c.priority) { header.push(pair('priority', c.priority.toLowerCase())); }
|
|
45
|
-
if (c.sameSite) { header.push(pair('samesite', c.sameSite.toLowerCase())); }
|
|
46
|
-
if (c.secure) { header.push('secure'); }
|
|
47
|
-
if (c.httpOnly) { header.push('httponly'); }
|
|
48
|
-
if (c.partitioned) { header.push('partitioned'); }
|
|
49
|
-
}
|
|
50
|
-
return header.join(';');
|
|
42
|
+
static responseSuffix(c: Cookie): string[] {
|
|
43
|
+
const parts = [];
|
|
44
|
+
if (c.path) { parts.push(pair('path', c.path)); }
|
|
45
|
+
if (c.expires) { parts.push(pair('expires', c.expires.toUTCString())); }
|
|
46
|
+
if (c.domain) { parts.push(pair('domain', c.domain)); }
|
|
47
|
+
if (c.priority) { parts.push(pair('priority', c.priority.toLowerCase())); }
|
|
48
|
+
if (c.sameSite) { parts.push(pair('samesite', c.sameSite.toLowerCase())); }
|
|
49
|
+
if (c.secure) { parts.push('secure'); }
|
|
50
|
+
if (c.httpOnly) { parts.push('httponly'); }
|
|
51
|
+
if (c.partitioned) { parts.push('partitioned'); }
|
|
52
|
+
return parts;
|
|
51
53
|
}
|
|
52
54
|
|
|
53
|
-
#secure?: boolean;
|
|
54
55
|
#grip?: keygrip;
|
|
55
56
|
#cookies: Record<string, Cookie> = {};
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
}
|
|
57
|
+
#setOptions: CookieSetOptions = {};
|
|
58
|
+
#deleteOptions: CookieSetOptions = { maxAge: 0, expires: undefined };
|
|
59
|
+
|
|
60
|
+
constructor({ keys, ...options }: CookieJarOptions = {}) {
|
|
61
|
+
this.#grip = keys?.length ? new keygrip(keys) : undefined;
|
|
62
|
+
this.#setOptions = {
|
|
63
|
+
secure: false,
|
|
64
|
+
path: '/',
|
|
65
|
+
signed: !!keys?.length,
|
|
66
|
+
...options,
|
|
67
|
+
};
|
|
67
68
|
}
|
|
68
69
|
|
|
69
|
-
#
|
|
70
|
-
|
|
71
|
-
const
|
|
72
|
-
const
|
|
73
|
-
if (
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
c.signed = index >= 0;
|
|
77
|
-
sc.signed = false;
|
|
78
|
-
sc.secure = c.secure;
|
|
79
|
-
|
|
80
|
-
if (index >= 1) {
|
|
81
|
-
sc.value = this.#grip.sign(key);
|
|
82
|
-
sc.response = true;
|
|
83
|
-
return sc;
|
|
70
|
+
#exportCookie(cookie: Cookie, response?: boolean): string[] {
|
|
71
|
+
const suffix = response ? CookieJar.responseSuffix(cookie) : null;
|
|
72
|
+
const payload = pairText(cookie);
|
|
73
|
+
const out = suffix ? [[payload, ...suffix].join(';')] : [payload];
|
|
74
|
+
if (cookie.signed) {
|
|
75
|
+
const sigPair = pair(`${cookie.name}.sig`, this.#grip!.sign(payload));
|
|
76
|
+
out.push(suffix ? [sigPair, ...suffix].join(';') : sigPair);
|
|
84
77
|
}
|
|
78
|
+
return out;
|
|
85
79
|
}
|
|
86
80
|
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
81
|
+
import(cookies: Cookie[]): this {
|
|
82
|
+
const signatures: Record<string, string> = {};
|
|
83
|
+
for (const cookie of cookies) {
|
|
84
|
+
if (this.#setOptions.signed && cookie.name.endsWith('.sig')) {
|
|
85
|
+
signatures[cookie.name.replace(/[.]sig$/, '')] = cookie.value!;
|
|
86
|
+
} else {
|
|
87
|
+
this.#cookies[cookie.name] = { signed: false, ...cookie };
|
|
88
|
+
}
|
|
92
89
|
}
|
|
93
|
-
return { ...c, name: `${c.name}.sig`, value: this.#grip.sign(pairText(c)) };
|
|
94
|
-
}
|
|
95
90
|
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
this.#cookies[c.name] = c;
|
|
101
|
-
if (this.#grip && !c.name.endsWith('.sig')) {
|
|
102
|
-
toCheck.push(c);
|
|
91
|
+
for (const [name, value] of Object.entries(signatures)) {
|
|
92
|
+
const cookie = this.#cookies[name];
|
|
93
|
+
if (!cookie) {
|
|
94
|
+
continue;
|
|
103
95
|
}
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
const
|
|
107
|
-
|
|
108
|
-
|
|
96
|
+
cookie.signed = true;
|
|
97
|
+
|
|
98
|
+
const computed = pairText(cookie);
|
|
99
|
+
const index = this.#grip!.index(computed, value);
|
|
100
|
+
|
|
101
|
+
if (index < 0) {
|
|
102
|
+
delete this.#cookies[name];
|
|
103
|
+
} else if (index >= 1) {
|
|
104
|
+
cookie.response = true;
|
|
109
105
|
}
|
|
110
106
|
}
|
|
107
|
+
return this;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
has(name: string, opts: CookieGetOptions = {}): boolean {
|
|
111
|
+
const needSigned = opts.signed ?? this.#setOptions.signed;
|
|
112
|
+
return name in this.#cookies && this.#cookies[name].signed === needSigned;
|
|
111
113
|
}
|
|
112
114
|
|
|
113
115
|
get(name: string, opts: CookieGetOptions = {}): string | undefined {
|
|
114
|
-
|
|
115
|
-
|
|
116
|
+
if (this.has(name, opts)) {
|
|
117
|
+
return this.#cookies[name]?.value;
|
|
118
|
+
}
|
|
116
119
|
}
|
|
117
120
|
|
|
118
|
-
set(
|
|
119
|
-
this.#cookies[
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
121
|
+
set(cookie: Cookie): void {
|
|
122
|
+
const alias = this.#cookies[cookie.name] = {
|
|
123
|
+
...this.#setOptions,
|
|
124
|
+
...cookie,
|
|
125
|
+
response: true,
|
|
126
|
+
...(cookie.value === null || cookie.value === undefined) ? this.#deleteOptions : {},
|
|
127
|
+
};
|
|
123
128
|
|
|
124
|
-
if (
|
|
125
|
-
|
|
126
|
-
c.expires = undefined;
|
|
129
|
+
if (!this.#setOptions.secure && alias.secure) {
|
|
130
|
+
throw new AppError('Cannot send secure cookie over unencrypted connection');
|
|
127
131
|
}
|
|
128
132
|
|
|
129
|
-
if (
|
|
130
|
-
|
|
131
|
-
this.#cookies[sc.name] = sc;
|
|
132
|
-
sc.response = true;
|
|
133
|
+
if (alias.signed && !this.#grip) {
|
|
134
|
+
throw new AppError('Signing keys required for signed cookies');
|
|
133
135
|
}
|
|
134
|
-
}
|
|
135
136
|
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
.map(c => CookieJar.toHeaderValue(c, response));
|
|
137
|
+
if (alias.maxAge !== undefined && !alias.expires) {
|
|
138
|
+
alias.expires = new Date(Date.now() + alias.maxAge);
|
|
139
|
+
}
|
|
140
140
|
}
|
|
141
141
|
|
|
142
142
|
getAll(): Cookie[] {
|
|
143
143
|
return Object.values(this.#cookies);
|
|
144
144
|
}
|
|
145
|
+
|
|
146
|
+
importCookieHeader(header: string | null | undefined): this {
|
|
147
|
+
return this.import(CookieJar.parseCookieHeader(header ?? ''));
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
importSetCookieHeader(headers: string[] | null | undefined): this {
|
|
151
|
+
return this.import(headers?.map(CookieJar.parseSetCookieHeader) ?? []);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
exportCookieHeader(): string {
|
|
155
|
+
return this.getAll().flatMap(c => this.#exportCookie(c)).join('; ');
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
exportSetCookieHeader(): string[] {
|
|
159
|
+
return this.getAll()
|
|
160
|
+
.filter(x => x.response)
|
|
161
|
+
.flatMap(c => this.#exportCookie(c, true));
|
|
162
|
+
}
|
|
145
163
|
}
|
|
@@ -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
|
-
|
|
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
|
|
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);
|