@travetto/web 7.0.0-rc.2 → 7.0.0-rc.4
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 +8 -8
- package/package.json +10 -10
- package/src/decorator/common.ts +4 -4
- package/src/decorator/endpoint.ts +5 -5
- package/src/decorator/param.ts +3 -3
- package/src/interceptor/accept.ts +5 -5
- package/src/interceptor/body.ts +2 -2
- package/src/interceptor/compress.ts +2 -2
- package/src/interceptor/cookie.ts +1 -1
- package/src/interceptor/cors.ts +4 -4
- package/src/interceptor/logging.ts +4 -4
- package/src/interceptor/respond.ts +5 -5
- package/src/registry/registry-adapter.ts +26 -26
- package/src/registry/registry-index.ts +13 -27
- package/src/registry/types.ts +9 -15
- package/src/registry/visitor.ts +5 -6
- package/src/router/base.ts +10 -27
- package/src/router/standard.ts +5 -11
- package/src/types/core.ts +2 -2
- package/src/types/dispatch.ts +1 -1
- package/src/types/headers.ts +16 -16
- package/src/types/message.ts +4 -4
- package/src/types/response.ts +4 -4
- package/src/util/body.ts +20 -20
- package/src/util/common.ts +20 -20
- package/src/util/cookie.ts +9 -9
- package/src/util/endpoint.ts +52 -58
- package/src/util/header.ts +40 -37
- package/src/util/keygrip.ts +1 -1
- package/src/util/net.ts +15 -15
- package/support/test/dispatch-util.ts +2 -2
- package/support/test/suite/base.ts +2 -2
- package/support/test/suite/standard.ts +2 -8
package/src/util/endpoint.ts
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
|
-
import { asConstructable, castKey, castTo, Class,
|
|
1
|
+
import { asConstructable, castKey, castTo, Class, TypedObject } from '@travetto/runtime';
|
|
2
2
|
import { BindUtil, SchemaParameterConfig, SchemaRegistryIndex, SchemaValidator, ValidationResultError } from '@travetto/schema';
|
|
3
3
|
import { DependencyRegistryIndex } from '@travetto/di';
|
|
4
|
-
import { RetargettingProxy } from '@travetto/registry';
|
|
5
4
|
|
|
6
5
|
import { WebChainedFilter, WebChainedContext, WebFilter } from '../types/filter.ts';
|
|
7
6
|
import { WebResponse } from '../types/response.ts';
|
|
@@ -65,12 +64,12 @@ export class EndpointUtil {
|
|
|
65
64
|
|
|
66
65
|
const inputByClass = Map.groupBy(
|
|
67
66
|
[...controller?.interceptorConfigs ?? [], ...endpoint.interceptorConfigs ?? []],
|
|
68
|
-
|
|
67
|
+
entry => entry[0]
|
|
69
68
|
);
|
|
70
69
|
|
|
71
70
|
const configs = new Map<Class, unknown>(interceptors.map(inst => {
|
|
72
71
|
const cls = asConstructable<WebInterceptor>(inst).constructor;
|
|
73
|
-
const inputs = (inputByClass.get(cls) ?? []).map(
|
|
72
|
+
const inputs = (inputByClass.get(cls) ?? []).map(entry => entry[1]);
|
|
74
73
|
const config = Object.assign({}, inst.config, ...inputs);
|
|
75
74
|
return [cls, inst.finalizeConfig?.({ config, endpoint }, castTo(inputs)) ?? config];
|
|
76
75
|
}));
|
|
@@ -96,8 +95,8 @@ export class EndpointUtil {
|
|
|
96
95
|
case 'header': return isArray ? request.headers.getList(name) : request.headers.get(name);
|
|
97
96
|
case 'query': {
|
|
98
97
|
const withQuery: typeof request & { [WebQueryExpandedSymbol]?: Record<string, unknown> } = request;
|
|
99
|
-
const
|
|
100
|
-
return
|
|
98
|
+
const query = withQuery[WebQueryExpandedSymbol] ??= BindUtil.expandPaths(request.context.httpQuery ?? {});
|
|
99
|
+
return query[name];
|
|
101
100
|
}
|
|
102
101
|
}
|
|
103
102
|
}
|
|
@@ -114,33 +113,32 @@ export class EndpointUtil {
|
|
|
114
113
|
} else if (param.location === 'query') {
|
|
115
114
|
// TODO: Revisit this logic?
|
|
116
115
|
const withQuery: typeof request & { [WebQueryExpandedSymbol]?: Record<string, unknown> } = request;
|
|
117
|
-
const
|
|
116
|
+
const query = withQuery[WebQueryExpandedSymbol] ??= BindUtil.expandPaths(request.context.httpQuery ?? {});
|
|
118
117
|
if (param.prefix) { // Has a prefix provided
|
|
119
|
-
return
|
|
118
|
+
return query[param.prefix];
|
|
120
119
|
} else if (input.type.Ⲑid) { // Is a full type
|
|
121
|
-
return
|
|
120
|
+
return query;
|
|
122
121
|
}
|
|
123
122
|
}
|
|
124
123
|
|
|
125
|
-
let
|
|
126
|
-
for (let i = 0;
|
|
127
|
-
|
|
124
|
+
let result = this.extractParameterValue(request, param, input.name!, input.array) ?? undefined;
|
|
125
|
+
for (let i = 0; result === undefined && input.aliases && i < input.aliases.length; i += 1) {
|
|
126
|
+
result = this.extractParameterValue(request, param, input.aliases[i], input.array) ?? undefined;
|
|
128
127
|
}
|
|
129
|
-
return
|
|
128
|
+
return result;
|
|
130
129
|
}
|
|
131
130
|
|
|
132
131
|
/**
|
|
133
132
|
* Extract all parameters for a given endpoint/request/response combo
|
|
134
133
|
* @param endpoint The endpoint to extract for
|
|
135
|
-
* @param
|
|
136
|
-
* @param res The response
|
|
134
|
+
* @param request The request
|
|
137
135
|
*/
|
|
138
136
|
static async extractParameters(endpoint: EndpointConfig, request: WebRequest): Promise<unknown[]> {
|
|
139
137
|
const cls = endpoint.class;
|
|
140
138
|
const vals = WebCommonUtil.getRequestParams(request);
|
|
141
139
|
const { parameters } = SchemaRegistryIndex.get(cls).getMethod(endpoint.methodName);
|
|
142
|
-
const combined = parameters.map((
|
|
143
|
-
({ schema:
|
|
140
|
+
const combined = parameters.map((config) =>
|
|
141
|
+
({ schema: config, param: endpoint.parameters[config.index], value: vals?.[config.index] }));
|
|
144
142
|
|
|
145
143
|
try {
|
|
146
144
|
const extracted = combined.map(({ param, schema, value }) =>
|
|
@@ -149,20 +147,20 @@ export class EndpointUtil {
|
|
|
149
147
|
this.extractParameter(request, param, schema)
|
|
150
148
|
);
|
|
151
149
|
const params = BindUtil.coerceMethodParams(cls, endpoint.methodName, extracted);
|
|
152
|
-
await SchemaValidator.validateMethod(cls, endpoint.methodName, params, endpoint.parameters.map(
|
|
150
|
+
await SchemaValidator.validateMethod(cls, endpoint.methodName, params, endpoint.parameters.map(paramConfig => paramConfig.prefix));
|
|
153
151
|
return params;
|
|
154
|
-
} catch (
|
|
155
|
-
if (
|
|
156
|
-
for (const
|
|
157
|
-
if (
|
|
158
|
-
const config = combined.find(
|
|
152
|
+
} catch (error) {
|
|
153
|
+
if (error instanceof ValidationResultError) {
|
|
154
|
+
for (const validationError of error.details?.errors ?? []) {
|
|
155
|
+
if (validationError.kind === 'required') {
|
|
156
|
+
const config = combined.find(paramConfig => paramConfig.schema.name === validationError.path);
|
|
159
157
|
if (config) {
|
|
160
|
-
|
|
158
|
+
validationError.message = `Missing ${config.param.location} value: ${config.schema.name}`;
|
|
161
159
|
}
|
|
162
160
|
}
|
|
163
161
|
}
|
|
164
162
|
}
|
|
165
|
-
throw
|
|
163
|
+
throw error;
|
|
166
164
|
}
|
|
167
165
|
}
|
|
168
166
|
|
|
@@ -176,7 +174,7 @@ export class EndpointUtil {
|
|
|
176
174
|
const headers = endpoint.finalizedResponseHeaders;
|
|
177
175
|
let response: WebResponse;
|
|
178
176
|
if (body instanceof WebResponse) {
|
|
179
|
-
for (const [
|
|
177
|
+
for (const [key, value] of headers) { body.headers.setIfAbsent(key, value); }
|
|
180
178
|
// Rewrite context
|
|
181
179
|
Object.assign(body.context, { ...endpoint.responseContext, ...body.context });
|
|
182
180
|
response = body;
|
|
@@ -184,8 +182,8 @@ export class EndpointUtil {
|
|
|
184
182
|
response = new WebResponse({ body, headers, context: { ...endpoint.responseContext } });
|
|
185
183
|
}
|
|
186
184
|
return endpoint.responseFinalizer?.(response) ?? response;
|
|
187
|
-
} catch (
|
|
188
|
-
throw WebCommonUtil.catchResponse(
|
|
185
|
+
} catch (error) {
|
|
186
|
+
throw WebCommonUtil.catchResponse(error);
|
|
189
187
|
}
|
|
190
188
|
}
|
|
191
189
|
|
|
@@ -203,7 +201,7 @@ export class EndpointUtil {
|
|
|
203
201
|
|
|
204
202
|
// Filter interceptors if needed
|
|
205
203
|
for (const filter of [controller?.interceptorExclude, endpoint.interceptorExclude]) {
|
|
206
|
-
interceptors = filter ? interceptors.filter(
|
|
204
|
+
interceptors = filter ? interceptors.filter(interceptor => !filter(interceptor)) : interceptors;
|
|
207
205
|
}
|
|
208
206
|
|
|
209
207
|
const interceptorFilters =
|
|
@@ -214,7 +212,7 @@ export class EndpointUtil {
|
|
|
214
212
|
const endpointFilters = [
|
|
215
213
|
...(controller?.filters ?? []).map(fn => fn.bind(controller?.instance)),
|
|
216
214
|
...(endpoint.filters ?? []).map(fn => fn.bind(endpoint.instance)),
|
|
217
|
-
...(endpoint.parameters.filter(
|
|
215
|
+
...(endpoint.parameters.filter(config => config.resolve).map(fn => fn.resolve!))
|
|
218
216
|
]
|
|
219
217
|
.map(fn => ({ filter: fn }));
|
|
220
218
|
|
|
@@ -231,8 +229,8 @@ export class EndpointUtil {
|
|
|
231
229
|
/**
|
|
232
230
|
* Get bound endpoints, honoring the conditional status
|
|
233
231
|
*/
|
|
234
|
-
static async getBoundEndpoints(
|
|
235
|
-
const config = ControllerRegistryIndex.getConfig(
|
|
232
|
+
static async getBoundEndpoints(cls: Class): Promise<EndpointConfig[]> {
|
|
233
|
+
const config = ControllerRegistryIndex.getConfig(cls);
|
|
236
234
|
|
|
237
235
|
// Skip registering conditional controllers
|
|
238
236
|
if (config.conditional && !await config.conditional()) {
|
|
@@ -241,21 +239,17 @@ export class EndpointUtil {
|
|
|
241
239
|
|
|
242
240
|
config.instance = await DependencyRegistryIndex.getInstance(config.class);
|
|
243
241
|
|
|
244
|
-
if (Runtime.dynamic) {
|
|
245
|
-
config.instance = RetargettingProxy.unwrap(config.instance);
|
|
246
|
-
}
|
|
247
|
-
|
|
248
242
|
// Filter out conditional endpoints
|
|
249
243
|
const endpoints = (await Promise.all(
|
|
250
|
-
config.endpoints.map(
|
|
251
|
-
)).filter(
|
|
244
|
+
config.endpoints.map(endpoint => Promise.resolve(endpoint.conditional?.() ?? true).then(value => value ? endpoint : undefined))
|
|
245
|
+
)).filter(endpoint => !!endpoint);
|
|
252
246
|
|
|
253
247
|
if (!endpoints.length) {
|
|
254
248
|
return [];
|
|
255
249
|
}
|
|
256
250
|
|
|
257
|
-
for (const
|
|
258
|
-
|
|
251
|
+
for (const endpoint of endpoints) {
|
|
252
|
+
endpoint.instance = config.instance;
|
|
259
253
|
}
|
|
260
254
|
|
|
261
255
|
return endpoints;
|
|
@@ -266,12 +260,12 @@ export class EndpointUtil {
|
|
|
266
260
|
*/
|
|
267
261
|
static orderEndpoints(endpoints: EndpointConfig[]): EndpointConfig[] {
|
|
268
262
|
return endpoints
|
|
269
|
-
.map(
|
|
270
|
-
const parts =
|
|
271
|
-
return [
|
|
263
|
+
.map(endpoint => {
|
|
264
|
+
const parts = endpoint.path.replace(/^[/]|[/]$/g, '').split('/');
|
|
265
|
+
return [endpoint, parts.map(part => /[*]/.test(part) ? 1 : /:/.test(part) ? 2 : 3)] as const;
|
|
272
266
|
})
|
|
273
267
|
.toSorted((a, b) => this.#compareEndpoints(a[1], b[1]) || a[0].path.localeCompare(b[0].path))
|
|
274
|
-
.map(([
|
|
268
|
+
.map(([endpoint,]) => endpoint);
|
|
275
269
|
}
|
|
276
270
|
|
|
277
271
|
|
|
@@ -279,25 +273,25 @@ export class EndpointUtil {
|
|
|
279
273
|
* Order interceptors
|
|
280
274
|
*/
|
|
281
275
|
static orderInterceptors(instances: WebInterceptor[]): WebInterceptor[] {
|
|
282
|
-
const
|
|
283
|
-
key:
|
|
284
|
-
start: castTo<Class<WebInterceptor>>({ name: `${
|
|
285
|
-
end: castTo<Class<WebInterceptor>>({ name: `${
|
|
276
|
+
const categoryList = WEB_INTERCEPTOR_CATEGORIES.map(category => ({
|
|
277
|
+
key: category,
|
|
278
|
+
start: castTo<Class<WebInterceptor>>({ name: `${category}Start` }),
|
|
279
|
+
end: castTo<Class<WebInterceptor>>({ name: `${category}End` }),
|
|
286
280
|
}));
|
|
287
281
|
|
|
288
|
-
const categoryMapping = TypedObject.fromEntries(
|
|
282
|
+
const categoryMapping = TypedObject.fromEntries(categoryList.map(category => [category.key, category]));
|
|
289
283
|
|
|
290
|
-
const ordered = instances.map(
|
|
291
|
-
const group = categoryMapping[
|
|
292
|
-
const after = [...
|
|
293
|
-
const before = [...
|
|
294
|
-
return ({ key:
|
|
284
|
+
const ordered = instances.map(category => {
|
|
285
|
+
const group = categoryMapping[category.category];
|
|
286
|
+
const after = [...category.dependsOn ?? [], group.start];
|
|
287
|
+
const before = [...category.runsBefore ?? [], group.end];
|
|
288
|
+
return ({ key: category.constructor, before, after, target: category, placeholder: false });
|
|
295
289
|
});
|
|
296
290
|
|
|
297
291
|
// Add category sets into the ordering
|
|
298
292
|
let i = 0;
|
|
299
|
-
for (const cat of
|
|
300
|
-
const prevEnd =
|
|
293
|
+
for (const cat of categoryList) {
|
|
294
|
+
const prevEnd = categoryList[i - 1]?.end ? [categoryList[i - 1].end] : [];
|
|
301
295
|
ordered.push(
|
|
302
296
|
{ key: cat.start, before: [cat.end], after: prevEnd, placeholder: true, target: undefined! },
|
|
303
297
|
{ key: cat.end, before: [], after: [cat.start], placeholder: true, target: undefined! }
|
|
@@ -306,7 +300,7 @@ export class EndpointUtil {
|
|
|
306
300
|
}
|
|
307
301
|
|
|
308
302
|
return WebCommonUtil.ordered(ordered)
|
|
309
|
-
.filter(
|
|
310
|
-
.map(
|
|
303
|
+
.filter(category => !category.placeholder) // Drop out the placeholders
|
|
304
|
+
.map(category => category.target);
|
|
311
305
|
}
|
|
312
306
|
}
|
package/src/util/header.ts
CHANGED
|
@@ -19,8 +19,8 @@ export class WebHeaderUtil {
|
|
|
19
19
|
* Parse cookie header
|
|
20
20
|
*/
|
|
21
21
|
static parseCookieHeader(header: string): Cookie[] {
|
|
22
|
-
const
|
|
23
|
-
return !
|
|
22
|
+
const text = header.trim();
|
|
23
|
+
return !text ? [] : text.split(SPLIT_SEMI).map(item => {
|
|
24
24
|
const [name, value] = item.split(SPLIT_EQ);
|
|
25
25
|
return { name, value };
|
|
26
26
|
});
|
|
@@ -32,17 +32,17 @@ export class WebHeaderUtil {
|
|
|
32
32
|
static parseSetCookieHeader(header: string): Cookie {
|
|
33
33
|
const parts = header.split(SPLIT_SEMI);
|
|
34
34
|
const [name, value] = parts[0].split(SPLIT_EQ);
|
|
35
|
-
const
|
|
36
|
-
for (const
|
|
37
|
-
const [
|
|
38
|
-
const
|
|
39
|
-
if (
|
|
40
|
-
|
|
35
|
+
const result: Cookie = { name, value };
|
|
36
|
+
for (const part of parts.slice(1)) {
|
|
37
|
+
const [key, partValue = ''] = part.toLowerCase().split(SPLIT_EQ);
|
|
38
|
+
const cleanedValue = partValue.charCodeAt(0) === QUOTE ? partValue.slice(1, -1) : partValue;
|
|
39
|
+
if (key === 'expires') {
|
|
40
|
+
result[key] = new Date(cleanedValue);
|
|
41
41
|
} else {
|
|
42
|
-
|
|
42
|
+
result[castKey(key)] = castTo(cleanedValue || true);
|
|
43
43
|
}
|
|
44
44
|
}
|
|
45
|
-
return
|
|
45
|
+
return result;
|
|
46
46
|
}
|
|
47
47
|
|
|
48
48
|
/**
|
|
@@ -53,20 +53,20 @@ export class WebHeaderUtil {
|
|
|
53
53
|
if (!input) {
|
|
54
54
|
return { value: '', parameters: {} };
|
|
55
55
|
}
|
|
56
|
-
const [
|
|
56
|
+
const [rawValue, ...parts] = input.split(SPLIT_SEMI);
|
|
57
57
|
const item: WebParsedHeader = { value: '', parameters: {} };
|
|
58
|
-
const value =
|
|
58
|
+
const value = rawValue.charCodeAt(0) === QUOTE ? rawValue.slice(1, -1) : rawValue;
|
|
59
59
|
if (value.includes('=')) {
|
|
60
60
|
parts.unshift(value);
|
|
61
61
|
} else {
|
|
62
62
|
item.value = value;
|
|
63
63
|
}
|
|
64
64
|
for (const part of parts) {
|
|
65
|
-
const [
|
|
66
|
-
const
|
|
67
|
-
item.parameters[
|
|
68
|
-
if (
|
|
69
|
-
item.q = parseFloat(
|
|
65
|
+
const [key, partValue = ''] = part.split(SPLIT_EQ);
|
|
66
|
+
const cleanedValue = (partValue.charCodeAt(0) === QUOTE) ? partValue.slice(1, -1) : partValue;
|
|
67
|
+
item.parameters[key] = cleanedValue;
|
|
68
|
+
if (key === 'q') {
|
|
69
|
+
item.q = parseFloat(cleanedValue);
|
|
70
70
|
}
|
|
71
71
|
}
|
|
72
72
|
return item;
|
|
@@ -76,24 +76,24 @@ export class WebHeaderUtil {
|
|
|
76
76
|
* Parse full header
|
|
77
77
|
*/
|
|
78
78
|
static parseHeader(input: string): WebParsedHeader[] {
|
|
79
|
-
const
|
|
79
|
+
const value = input.trim();
|
|
80
80
|
if (!input) { return []; }
|
|
81
|
-
return
|
|
81
|
+
return value.split(SPLIT_COMMA).map(part => this.parseHeaderSegment(part));
|
|
82
82
|
}
|
|
83
83
|
|
|
84
84
|
/**
|
|
85
85
|
* Build cookie suffix
|
|
86
86
|
*/
|
|
87
|
-
static buildCookieSuffix(
|
|
87
|
+
static buildCookieSuffix(cookie: Cookie): string[] {
|
|
88
88
|
const parts = [];
|
|
89
|
-
if (
|
|
90
|
-
if (
|
|
91
|
-
if (
|
|
92
|
-
if (
|
|
93
|
-
if (
|
|
94
|
-
if (
|
|
95
|
-
if (
|
|
96
|
-
if (
|
|
89
|
+
if (cookie.path) { parts.push(`path=${cookie.path}`); }
|
|
90
|
+
if (cookie.expires) { parts.push(`expires=${cookie.expires.toUTCString()}`); }
|
|
91
|
+
if (cookie.domain) { parts.push(`domain=${cookie.domain}`); }
|
|
92
|
+
if (cookie.priority) { parts.push(`priority=${cookie.priority.toLowerCase()}`); }
|
|
93
|
+
if (cookie.sameSite) { parts.push(`samesite=${cookie.sameSite.toLowerCase()}`); }
|
|
94
|
+
if (cookie.secure) { parts.push('secure'); }
|
|
95
|
+
if (cookie.httponly) { parts.push('httponly'); }
|
|
96
|
+
if (cookie.partitioned) { parts.push('partitioned'); }
|
|
97
97
|
return parts;
|
|
98
98
|
}
|
|
99
99
|
|
|
@@ -104,7 +104,10 @@ export class WebHeaderUtil {
|
|
|
104
104
|
if (header === '*' || header === '*/*') {
|
|
105
105
|
return values[0];
|
|
106
106
|
}
|
|
107
|
-
const sorted = this.parseHeader(header.toLowerCase())
|
|
107
|
+
const sorted = this.parseHeader(header.toLowerCase())
|
|
108
|
+
.filter(item => (item.q ?? 1) > 0)
|
|
109
|
+
.toSorted((a, b) => (b.q ?? 1) - (a.q ?? 1));
|
|
110
|
+
|
|
108
111
|
const set = new Set(values);
|
|
109
112
|
for (const { value } of sorted) {
|
|
110
113
|
const vk: K = castKey(value);
|
|
@@ -127,7 +130,7 @@ export class WebHeaderUtil {
|
|
|
127
130
|
const { parameters } = this.parseHeaderSegment(headers.get('Range'));
|
|
128
131
|
if ('bytes' in parameters) {
|
|
129
132
|
const [start, end] = parameters.bytes.split('-')
|
|
130
|
-
.map(
|
|
133
|
+
.map(value => value ? parseInt(value, 10) : undefined);
|
|
131
134
|
if (start !== undefined) {
|
|
132
135
|
return { start, end: end ?? (start + chunkSize) };
|
|
133
136
|
}
|
|
@@ -137,20 +140,20 @@ export class WebHeaderUtil {
|
|
|
137
140
|
/**
|
|
138
141
|
* Check freshness of the response using request and response headers.
|
|
139
142
|
*/
|
|
140
|
-
static isFresh(
|
|
141
|
-
const cacheControl =
|
|
143
|
+
static isFresh(request: WebHeaders, response: WebHeaders): boolean {
|
|
144
|
+
const cacheControl = request.get('Cache-Control');
|
|
142
145
|
if (cacheControl?.includes('no-cache')) {
|
|
143
146
|
return false;
|
|
144
147
|
}
|
|
145
148
|
|
|
146
|
-
const noneMatch =
|
|
149
|
+
const noneMatch = request.get('If-None-Match');
|
|
147
150
|
if (noneMatch) {
|
|
148
|
-
const etag =
|
|
149
|
-
const validTag = (
|
|
151
|
+
const etag = response.get('ETag');
|
|
152
|
+
const validTag = (value: string): boolean => value === etag || value === `W/${etag}` || `W/${value}` === etag;
|
|
150
153
|
return noneMatch === '*' || (!!etag && noneMatch.split(SPLIT_COMMA).some(validTag));
|
|
151
154
|
} else {
|
|
152
|
-
const modifiedSince =
|
|
153
|
-
const lastModified =
|
|
155
|
+
const modifiedSince = request.get('If-Modified-Since');
|
|
156
|
+
const lastModified = response.get('Last-Modified');
|
|
154
157
|
if (!modifiedSince || !lastModified) {
|
|
155
158
|
return false;
|
|
156
159
|
}
|
package/src/util/keygrip.ts
CHANGED
|
@@ -30,7 +30,7 @@ export class KeyGrip {
|
|
|
30
30
|
.createHmac(this.#algorithm, key ?? this.#keys[0])
|
|
31
31
|
.update(data)
|
|
32
32
|
.digest(this.#encoding)
|
|
33
|
-
.replace(/[/+=]/g,
|
|
33
|
+
.replace(/[/+=]/g, ch => CHAR_MAPPING[castKey(ch)]);
|
|
34
34
|
}
|
|
35
35
|
|
|
36
36
|
verify(data: string, digest: string): boolean {
|
package/src/util/net.ts
CHANGED
|
@@ -8,25 +8,25 @@ import { ExecUtil } from '@travetto/runtime';
|
|
|
8
8
|
export class NetUtil {
|
|
9
9
|
|
|
10
10
|
/** Is an error an address in use error */
|
|
11
|
-
static isPortUsedError(
|
|
12
|
-
return !!
|
|
11
|
+
static isPortUsedError(error: unknown): error is Error & { port: number } {
|
|
12
|
+
return !!error && error instanceof Error && error.message.includes('EADDRINUSE');
|
|
13
13
|
}
|
|
14
14
|
|
|
15
15
|
/** Get the port process id */
|
|
16
16
|
static async getPortProcessId(port: number): Promise<number | undefined> {
|
|
17
|
-
const
|
|
18
|
-
const result = await ExecUtil.getResult(
|
|
19
|
-
const [
|
|
20
|
-
if (
|
|
21
|
-
return +
|
|
17
|
+
const subProcess = spawn('lsof', ['-t', '-i', `tcp:${port}`]);
|
|
18
|
+
const result = await ExecUtil.getResult(subProcess, { catch: true });
|
|
19
|
+
const [processId] = result.stdout.trim().split(/\n/g);
|
|
20
|
+
if (processId && +processId > 0) {
|
|
21
|
+
return +processId;
|
|
22
22
|
}
|
|
23
23
|
}
|
|
24
24
|
|
|
25
25
|
/** Free port if in use */
|
|
26
26
|
static async freePort(port: number): Promise<void> {
|
|
27
|
-
const
|
|
28
|
-
if (
|
|
29
|
-
process.kill(
|
|
27
|
+
const processId = await this.getPortProcessId(port);
|
|
28
|
+
if (processId) {
|
|
29
|
+
process.kill(processId);
|
|
30
30
|
}
|
|
31
31
|
}
|
|
32
32
|
|
|
@@ -54,19 +54,19 @@ export class NetUtil {
|
|
|
54
54
|
*/
|
|
55
55
|
static getLocalAddress(): string {
|
|
56
56
|
const useIPv4 = !![...Object.values(os.networkInterfaces())]
|
|
57
|
-
.find(interfaces => interfaces?.find(
|
|
57
|
+
.find(interfaces => interfaces?.find(item => item.family === 'IPv4'));
|
|
58
58
|
|
|
59
59
|
return useIPv4 ? '0.0.0.0' : '::';
|
|
60
60
|
}
|
|
61
61
|
|
|
62
62
|
/**
|
|
63
63
|
* Free a port if it is in use, typically used to resolve port conflicts.
|
|
64
|
-
* @param
|
|
64
|
+
* @param error The error that may indicate a port conflict
|
|
65
65
|
* @returns Returns true if the port was freed, false if not handled
|
|
66
66
|
*/
|
|
67
|
-
static async freePortOnConflict(
|
|
68
|
-
if (NetUtil.isPortUsedError(
|
|
69
|
-
await NetUtil.freePort(
|
|
67
|
+
static async freePortOnConflict(error: unknown): Promise<boolean> {
|
|
68
|
+
if (NetUtil.isPortUsedError(error) && typeof error.port === 'number') {
|
|
69
|
+
await NetUtil.freePort(error.port);
|
|
70
70
|
return true;
|
|
71
71
|
} else {
|
|
72
72
|
return false;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { buffer } from 'node:stream/consumers';
|
|
2
2
|
import { Readable } from 'node:stream';
|
|
3
3
|
|
|
4
|
-
import { AppError, BinaryUtil, castTo } from '@travetto/runtime';
|
|
4
|
+
import { AppError, BinaryUtil, castTo, JSONUtil } from '@travetto/runtime';
|
|
5
5
|
import { BindUtil } from '@travetto/schema';
|
|
6
6
|
|
|
7
7
|
import { WebResponse } from '../../src/types/response.ts';
|
|
@@ -51,7 +51,7 @@ export class WebTestDispatchUtil {
|
|
|
51
51
|
|
|
52
52
|
if (text) {
|
|
53
53
|
switch (response.headers.get('Content-Type')) {
|
|
54
|
-
case 'application/json': result =
|
|
54
|
+
case 'application/json': result = JSONUtil.parseSafe(castTo(text)); break;
|
|
55
55
|
case 'text/plain': result = text; break;
|
|
56
56
|
}
|
|
57
57
|
}
|
|
@@ -2,7 +2,7 @@ import { Registry } from '@travetto/registry';
|
|
|
2
2
|
import { castTo, Class } from '@travetto/runtime';
|
|
3
3
|
import { AfterAll, BeforeAll } from '@travetto/test';
|
|
4
4
|
import { DependencyRegistryIndex, Injectable } from '@travetto/di';
|
|
5
|
-
import { ConfigSource,
|
|
5
|
+
import { ConfigSource, ConfigPayload } from '@travetto/config';
|
|
6
6
|
import { Schema } from '@travetto/schema';
|
|
7
7
|
|
|
8
8
|
import { WebDispatcher } from '../../../src/types/dispatch.ts';
|
|
@@ -12,7 +12,7 @@ import { WebMessageInit } from '../../../src/types/message.ts';
|
|
|
12
12
|
|
|
13
13
|
@Injectable()
|
|
14
14
|
export class WebTestConfig implements ConfigSource {
|
|
15
|
-
async get(): Promise<
|
|
15
|
+
async get(): Promise<ConfigPayload> {
|
|
16
16
|
return {
|
|
17
17
|
data: {
|
|
18
18
|
web: {
|
|
@@ -1,19 +1,13 @@
|
|
|
1
1
|
import assert from 'node:assert';
|
|
2
2
|
|
|
3
|
-
import { Test, Suite
|
|
4
|
-
import { Registry } from '@travetto/registry';
|
|
3
|
+
import { Test, Suite } from '@travetto/test';
|
|
5
4
|
|
|
6
5
|
import { BaseWebSuite } from './base.ts';
|
|
7
|
-
import
|
|
6
|
+
import './controller.ts'; // Ensure imported
|
|
8
7
|
|
|
9
8
|
@Suite()
|
|
10
9
|
export abstract class StandardWebServerSuite extends BaseWebSuite {
|
|
11
10
|
|
|
12
|
-
@BeforeAll()
|
|
13
|
-
async init() {
|
|
14
|
-
Registry.process([{ type: 'added', curr: TestController }]);
|
|
15
|
-
}
|
|
16
|
-
|
|
17
11
|
@Test()
|
|
18
12
|
async getJSON() {
|
|
19
13
|
const response = await this.request({ context: { httpMethod: 'GET', path: '/test/json' } });
|