@chidchanun/bcp 0.1.17 → 0.1.19
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/docs/README.md +350 -0
- package/docs/middleware.md +486 -24
- package/docs/releases/0.1.18.md +119 -0
- package/docs/releases/0.1.19.md +106 -0
- package/docs/validation.md +342 -0
- package/package.json +5 -1
- package/packages/client/src/validation.ts +1118 -0
- package/packages/server/src/middleware-loader.ts +34 -7
- package/packages/server/src/middleware-proxy.ts +325 -159
- package/packages/server/src/middleware.ts +544 -104
|
@@ -5,6 +5,7 @@ import {
|
|
|
5
5
|
} from "node:url";
|
|
6
6
|
|
|
7
7
|
import type {
|
|
8
|
+
MiddlewareHandler,
|
|
8
9
|
MiddlewareModule,
|
|
9
10
|
} from "./middleware.js";
|
|
10
11
|
|
|
@@ -93,16 +94,17 @@ export function assertMiddlewareModule(
|
|
|
93
94
|
module: MiddlewareModule,
|
|
94
95
|
filePath = "middleware"
|
|
95
96
|
): void {
|
|
96
|
-
const
|
|
97
|
+
const value =
|
|
97
98
|
module.middleware ??
|
|
98
99
|
module.default;
|
|
99
100
|
|
|
100
101
|
if (
|
|
101
|
-
|
|
102
|
-
|
|
102
|
+
!isMiddlewareHandlerValue(
|
|
103
|
+
value
|
|
104
|
+
)
|
|
103
105
|
) {
|
|
104
106
|
throw new Error(
|
|
105
|
-
`BCP Framework: ${filePath} must export a middleware
|
|
107
|
+
`BCP Framework: ${filePath} must export a middleware function, a middleware function array, or a default equivalent.`
|
|
106
108
|
);
|
|
107
109
|
}
|
|
108
110
|
|
|
@@ -125,12 +127,12 @@ export function assertMiddlewareModule(
|
|
|
125
127
|
];
|
|
126
128
|
|
|
127
129
|
for (
|
|
128
|
-
const
|
|
130
|
+
const item
|
|
129
131
|
of values
|
|
130
132
|
) {
|
|
131
133
|
if (
|
|
132
|
-
typeof
|
|
133
|
-
!(
|
|
134
|
+
typeof item !== "string" &&
|
|
135
|
+
!(item instanceof RegExp)
|
|
134
136
|
) {
|
|
135
137
|
throw new Error(
|
|
136
138
|
`BCP Framework: middleware matcher in ${filePath} must be a string, RegExp, or an array of them.`
|
|
@@ -138,3 +140,28 @@ export function assertMiddlewareModule(
|
|
|
138
140
|
}
|
|
139
141
|
}
|
|
140
142
|
}
|
|
143
|
+
|
|
144
|
+
function isMiddlewareHandlerValue(
|
|
145
|
+
value: unknown
|
|
146
|
+
): value is
|
|
147
|
+
| MiddlewareHandler
|
|
148
|
+
| readonly MiddlewareHandler[] {
|
|
149
|
+
if (
|
|
150
|
+
typeof value ===
|
|
151
|
+
"function"
|
|
152
|
+
) {
|
|
153
|
+
return true;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
return (
|
|
157
|
+
Array.isArray(
|
|
158
|
+
value
|
|
159
|
+
) &&
|
|
160
|
+
value.length > 0 &&
|
|
161
|
+
value.every(
|
|
162
|
+
(handler) =>
|
|
163
|
+
typeof handler ===
|
|
164
|
+
"function"
|
|
165
|
+
)
|
|
166
|
+
);
|
|
167
|
+
}
|
|
@@ -1,11 +1,10 @@
|
|
|
1
1
|
import * as http from "node:http";
|
|
2
2
|
|
|
3
3
|
import {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
executeMiddleware,
|
|
7
|
-
sendMiddlewareRedirect,
|
|
4
|
+
executeMiddlewarePipeline,
|
|
5
|
+
getMiddlewareRedirectTarget,
|
|
8
6
|
sendMiddlewareResponse,
|
|
7
|
+
type MiddlewareExecutionInput,
|
|
9
8
|
type MiddlewareModule,
|
|
10
9
|
} from "./middleware.js";
|
|
11
10
|
|
|
@@ -81,8 +80,8 @@ export function createMiddlewareProxy(
|
|
|
81
80
|
await options
|
|
82
81
|
.getMiddleware();
|
|
83
82
|
|
|
84
|
-
const
|
|
85
|
-
await
|
|
83
|
+
const response =
|
|
84
|
+
await executeMiddlewarePipeline(
|
|
86
85
|
module,
|
|
87
86
|
{
|
|
88
87
|
url:
|
|
@@ -94,91 +93,53 @@ export function createMiddlewareProxy(
|
|
|
94
93
|
incomingHeadersToHeaders(
|
|
95
94
|
req.headers
|
|
96
95
|
),
|
|
97
|
-
}
|
|
96
|
+
},
|
|
97
|
+
(
|
|
98
|
+
pipelineInput
|
|
99
|
+
) =>
|
|
100
|
+
requestUpstreamAsResponse(
|
|
101
|
+
req,
|
|
102
|
+
pipelineInput,
|
|
103
|
+
upstreamHostname,
|
|
104
|
+
options.upstreamPort,
|
|
105
|
+
host,
|
|
106
|
+
incomingUrl,
|
|
107
|
+
navigationTarget,
|
|
108
|
+
targetUrl
|
|
109
|
+
)
|
|
98
110
|
);
|
|
99
111
|
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
) {
|
|
104
|
-
if (
|
|
105
|
-
navigationTarget
|
|
106
|
-
) {
|
|
107
|
-
applyMiddlewareResponseHeaders(
|
|
108
|
-
res,
|
|
109
|
-
result.headers
|
|
110
|
-
);
|
|
111
|
-
res.statusCode =
|
|
112
|
-
204;
|
|
113
|
-
res.setHeader(
|
|
114
|
-
NAVIGATION_REDIRECT_HEADER,
|
|
115
|
-
result.url.href
|
|
116
|
-
);
|
|
117
|
-
res.setHeader(
|
|
118
|
-
"Cache-Control",
|
|
119
|
-
"no-store"
|
|
120
|
-
);
|
|
121
|
-
res.end();
|
|
122
|
-
return;
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
sendMiddlewareRedirect(
|
|
126
|
-
req,
|
|
127
|
-
res,
|
|
128
|
-
result
|
|
112
|
+
const middlewareRedirect =
|
|
113
|
+
getMiddlewareRedirectTarget(
|
|
114
|
+
response
|
|
129
115
|
);
|
|
130
|
-
return;
|
|
131
|
-
}
|
|
132
116
|
|
|
133
117
|
if (
|
|
134
|
-
|
|
135
|
-
|
|
118
|
+
navigationTarget &&
|
|
119
|
+
middlewareRedirect
|
|
136
120
|
) {
|
|
137
|
-
|
|
138
|
-
req,
|
|
121
|
+
applyNavigationRedirectHeaders(
|
|
139
122
|
res,
|
|
140
|
-
|
|
123
|
+
response.headers
|
|
141
124
|
);
|
|
125
|
+
res.statusCode =
|
|
126
|
+
204;
|
|
127
|
+
res.setHeader(
|
|
128
|
+
NAVIGATION_REDIRECT_HEADER,
|
|
129
|
+
middlewareRedirect.href
|
|
130
|
+
);
|
|
131
|
+
res.setHeader(
|
|
132
|
+
"Cache-Control",
|
|
133
|
+
"no-store"
|
|
134
|
+
);
|
|
135
|
+
res.end();
|
|
142
136
|
return;
|
|
143
137
|
}
|
|
144
138
|
|
|
145
|
-
|
|
146
|
-
req,
|
|
147
|
-
result.requestHeaders
|
|
148
|
-
);
|
|
149
|
-
applyMiddlewareResponseHeaders(
|
|
150
|
-
res,
|
|
151
|
-
result.responseHeaders
|
|
152
|
-
);
|
|
153
|
-
|
|
154
|
-
if (
|
|
155
|
-
result.type ===
|
|
156
|
-
"rewrite"
|
|
157
|
-
) {
|
|
158
|
-
if (
|
|
159
|
-
navigationTarget
|
|
160
|
-
) {
|
|
161
|
-
incomingUrl
|
|
162
|
-
.searchParams
|
|
163
|
-
.set(
|
|
164
|
-
"url",
|
|
165
|
-
`${result.url.pathname}${result.url.search}${result.url.hash}`
|
|
166
|
-
);
|
|
167
|
-
|
|
168
|
-
req.url =
|
|
169
|
-
`${incomingUrl.pathname}${incomingUrl.search}`;
|
|
170
|
-
} else {
|
|
171
|
-
req.url =
|
|
172
|
-
`${result.url.pathname}${result.url.search}`;
|
|
173
|
-
}
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
proxyRequest(
|
|
139
|
+
await sendMiddlewareResponse(
|
|
177
140
|
req,
|
|
178
141
|
res,
|
|
179
|
-
|
|
180
|
-
options.upstreamPort,
|
|
181
|
-
host
|
|
142
|
+
response
|
|
182
143
|
);
|
|
183
144
|
} catch (error) {
|
|
184
145
|
console.error(
|
|
@@ -282,114 +243,319 @@ function resolveNavigationTarget(
|
|
|
282
243
|
}
|
|
283
244
|
}
|
|
284
245
|
|
|
285
|
-
function
|
|
246
|
+
function requestUpstreamAsResponse(
|
|
286
247
|
req: http.IncomingMessage,
|
|
287
|
-
|
|
248
|
+
input: MiddlewareExecutionInput,
|
|
288
249
|
upstreamHostname: string,
|
|
289
250
|
upstreamPort: number,
|
|
290
|
-
originalHost: string
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
originalHost,
|
|
309
|
-
"x-forwarded-host":
|
|
310
|
-
originalHost,
|
|
311
|
-
},
|
|
312
|
-
}
|
|
251
|
+
originalHost: string,
|
|
252
|
+
incomingUrl: URL,
|
|
253
|
+
navigationTarget: URL | null,
|
|
254
|
+
originalTargetUrl: URL
|
|
255
|
+
): Promise<Response> {
|
|
256
|
+
const target =
|
|
257
|
+
input.url instanceof URL
|
|
258
|
+
? new URL(
|
|
259
|
+
input.url.href
|
|
260
|
+
)
|
|
261
|
+
: new URL(
|
|
262
|
+
input.url
|
|
263
|
+
);
|
|
264
|
+
const requestHeaders =
|
|
265
|
+
headersToNodeHeaders(
|
|
266
|
+
new Headers(
|
|
267
|
+
input.headers
|
|
268
|
+
)
|
|
313
269
|
);
|
|
314
270
|
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
271
|
+
requestHeaders.host =
|
|
272
|
+
originalHost;
|
|
273
|
+
requestHeaders[
|
|
274
|
+
"x-forwarded-host"
|
|
275
|
+
] = originalHost;
|
|
276
|
+
|
|
277
|
+
const path =
|
|
278
|
+
resolveUpstreamPath(
|
|
279
|
+
target,
|
|
280
|
+
incomingUrl,
|
|
281
|
+
navigationTarget,
|
|
282
|
+
originalTargetUrl
|
|
283
|
+
);
|
|
321
284
|
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
285
|
+
return new Promise<Response>(
|
|
286
|
+
(
|
|
287
|
+
resolve,
|
|
288
|
+
reject
|
|
289
|
+
) => {
|
|
290
|
+
const upstream =
|
|
291
|
+
http.request(
|
|
292
|
+
{
|
|
293
|
+
hostname:
|
|
294
|
+
upstreamHostname,
|
|
295
|
+
port:
|
|
296
|
+
upstreamPort,
|
|
297
|
+
method:
|
|
298
|
+
input.method ??
|
|
299
|
+
req.method ??
|
|
300
|
+
"GET",
|
|
301
|
+
path,
|
|
302
|
+
headers:
|
|
303
|
+
requestHeaders,
|
|
304
|
+
}
|
|
305
|
+
);
|
|
306
|
+
|
|
307
|
+
upstream.once(
|
|
308
|
+
"response",
|
|
309
|
+
(
|
|
310
|
+
upstreamResponse
|
|
311
|
+
) => {
|
|
312
|
+
const chunks:
|
|
313
|
+
Buffer[] = [];
|
|
314
|
+
|
|
315
|
+
upstreamResponse.on(
|
|
316
|
+
"data",
|
|
317
|
+
(
|
|
318
|
+
chunk:
|
|
319
|
+
Buffer |
|
|
320
|
+
string
|
|
321
|
+
) => {
|
|
322
|
+
chunks.push(
|
|
323
|
+
Buffer.isBuffer(
|
|
324
|
+
chunk
|
|
325
|
+
)
|
|
326
|
+
? chunk
|
|
327
|
+
: Buffer.from(
|
|
328
|
+
chunk
|
|
329
|
+
)
|
|
330
|
+
);
|
|
331
|
+
}
|
|
332
|
+
);
|
|
333
|
+
upstreamResponse.once(
|
|
334
|
+
"error",
|
|
335
|
+
reject
|
|
336
|
+
);
|
|
337
|
+
upstreamResponse.once(
|
|
338
|
+
"end",
|
|
339
|
+
() => {
|
|
340
|
+
const headers =
|
|
341
|
+
responseHeadersFromNode(
|
|
342
|
+
upstreamResponse.headers
|
|
343
|
+
);
|
|
344
|
+
const status =
|
|
345
|
+
upstreamResponse.statusCode ??
|
|
346
|
+
502;
|
|
347
|
+
const body =
|
|
348
|
+
req.method ===
|
|
349
|
+
"HEAD" ||
|
|
350
|
+
status === 204 ||
|
|
351
|
+
status === 304
|
|
352
|
+
? null
|
|
353
|
+
: Buffer.concat(
|
|
354
|
+
chunks
|
|
355
|
+
);
|
|
356
|
+
|
|
357
|
+
resolve(
|
|
358
|
+
new Response(
|
|
359
|
+
body,
|
|
360
|
+
{
|
|
361
|
+
status,
|
|
362
|
+
statusText:
|
|
363
|
+
upstreamResponse.statusMessage ??
|
|
364
|
+
"",
|
|
365
|
+
headers,
|
|
366
|
+
}
|
|
367
|
+
)
|
|
368
|
+
);
|
|
369
|
+
}
|
|
337
370
|
);
|
|
338
371
|
}
|
|
372
|
+
);
|
|
373
|
+
|
|
374
|
+
upstream.once(
|
|
375
|
+
"error",
|
|
376
|
+
reject
|
|
377
|
+
);
|
|
378
|
+
|
|
379
|
+
req.pipe(
|
|
380
|
+
upstream
|
|
381
|
+
);
|
|
382
|
+
}
|
|
383
|
+
);
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
function resolveUpstreamPath(
|
|
387
|
+
target: URL,
|
|
388
|
+
incomingUrl: URL,
|
|
389
|
+
navigationTarget: URL | null,
|
|
390
|
+
originalTargetUrl: URL
|
|
391
|
+
): string {
|
|
392
|
+
if (!navigationTarget) {
|
|
393
|
+
return `${target.pathname}${target.search}`;
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
const envelope =
|
|
397
|
+
new URL(
|
|
398
|
+
incomingUrl.href
|
|
399
|
+
);
|
|
400
|
+
|
|
401
|
+
if (
|
|
402
|
+
target.href !==
|
|
403
|
+
originalTargetUrl.href
|
|
404
|
+
) {
|
|
405
|
+
envelope.searchParams.set(
|
|
406
|
+
"url",
|
|
407
|
+
`${target.pathname}${target.search}${target.hash}`
|
|
408
|
+
);
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
return `${envelope.pathname}${envelope.search}`;
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
function responseHeadersFromNode(
|
|
415
|
+
input: http.IncomingHttpHeaders
|
|
416
|
+
): Headers {
|
|
417
|
+
const headers =
|
|
418
|
+
new Headers();
|
|
419
|
+
|
|
420
|
+
for (
|
|
421
|
+
const [
|
|
422
|
+
key,
|
|
423
|
+
value,
|
|
424
|
+
]
|
|
425
|
+
of Object.entries(
|
|
426
|
+
input
|
|
427
|
+
)
|
|
428
|
+
) {
|
|
429
|
+
if (
|
|
430
|
+
value === undefined ||
|
|
431
|
+
isHopByHopHeader(
|
|
432
|
+
key
|
|
433
|
+
)
|
|
434
|
+
) {
|
|
435
|
+
continue;
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
if (
|
|
439
|
+
Array.isArray(
|
|
440
|
+
value
|
|
441
|
+
)
|
|
442
|
+
) {
|
|
443
|
+
for (
|
|
444
|
+
const item
|
|
445
|
+
of value
|
|
446
|
+
) {
|
|
447
|
+
headers.append(
|
|
448
|
+
key,
|
|
449
|
+
item
|
|
450
|
+
);
|
|
339
451
|
}
|
|
452
|
+
continue;
|
|
453
|
+
}
|
|
340
454
|
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
455
|
+
headers.set(
|
|
456
|
+
key,
|
|
457
|
+
value
|
|
458
|
+
);
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
return headers;
|
|
462
|
+
}
|
|
344
463
|
|
|
464
|
+
function headersToNodeHeaders(
|
|
465
|
+
input: Headers
|
|
466
|
+
): http.OutgoingHttpHeaders {
|
|
467
|
+
const headers:
|
|
468
|
+
http.OutgoingHttpHeaders = {};
|
|
469
|
+
|
|
470
|
+
input.forEach(
|
|
471
|
+
(value, key) => {
|
|
345
472
|
if (
|
|
346
|
-
|
|
473
|
+
!isHopByHopHeader(
|
|
474
|
+
key
|
|
475
|
+
)
|
|
347
476
|
) {
|
|
348
|
-
|
|
349
|
-
|
|
477
|
+
headers[key] =
|
|
478
|
+
value;
|
|
350
479
|
}
|
|
351
|
-
|
|
352
|
-
upstreamResponse.pipe(
|
|
353
|
-
res
|
|
354
|
-
);
|
|
355
480
|
}
|
|
356
481
|
);
|
|
357
482
|
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
483
|
+
return headers;
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
function applyNavigationRedirectHeaders(
|
|
487
|
+
res: http.ServerResponse,
|
|
488
|
+
headers: Headers
|
|
489
|
+
): void {
|
|
490
|
+
headers.forEach(
|
|
491
|
+
(value, key) => {
|
|
492
|
+
const normalized =
|
|
493
|
+
key.toLowerCase();
|
|
365
494
|
|
|
366
495
|
if (
|
|
367
|
-
|
|
496
|
+
normalized ===
|
|
497
|
+
"location" ||
|
|
498
|
+
normalized ===
|
|
499
|
+
"content-length" ||
|
|
500
|
+
isHopByHopHeader(
|
|
501
|
+
normalized
|
|
502
|
+
)
|
|
368
503
|
) {
|
|
369
|
-
res.destroy(
|
|
370
|
-
error
|
|
371
|
-
);
|
|
372
504
|
return;
|
|
373
505
|
}
|
|
374
506
|
|
|
375
|
-
res.
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
"Content-Type":
|
|
379
|
-
"text/plain; charset=utf-8",
|
|
380
|
-
"Cache-Control":
|
|
381
|
-
"no-store",
|
|
382
|
-
}
|
|
383
|
-
);
|
|
384
|
-
res.end(
|
|
385
|
-
"Bad Gateway"
|
|
507
|
+
res.setHeader(
|
|
508
|
+
key,
|
|
509
|
+
value
|
|
386
510
|
);
|
|
387
511
|
}
|
|
388
512
|
);
|
|
389
513
|
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
514
|
+
const getSetCookie =
|
|
515
|
+
(
|
|
516
|
+
headers as Headers & {
|
|
517
|
+
getSetCookie?: () => string[];
|
|
518
|
+
}
|
|
519
|
+
).getSetCookie;
|
|
520
|
+
|
|
521
|
+
if (
|
|
522
|
+
typeof getSetCookie ===
|
|
523
|
+
"function"
|
|
524
|
+
) {
|
|
525
|
+
const cookies =
|
|
526
|
+
getSetCookie.call(
|
|
527
|
+
headers
|
|
528
|
+
);
|
|
529
|
+
|
|
530
|
+
if (
|
|
531
|
+
cookies.length > 0
|
|
532
|
+
) {
|
|
533
|
+
res.setHeader(
|
|
534
|
+
"Set-Cookie",
|
|
535
|
+
cookies
|
|
536
|
+
);
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
function isHopByHopHeader(
|
|
542
|
+
name: string
|
|
543
|
+
): boolean {
|
|
544
|
+
switch (
|
|
545
|
+
name.toLowerCase()
|
|
546
|
+
) {
|
|
547
|
+
case "connection":
|
|
548
|
+
case "keep-alive":
|
|
549
|
+
case "proxy-authenticate":
|
|
550
|
+
case "proxy-authorization":
|
|
551
|
+
case "te":
|
|
552
|
+
case "trailer":
|
|
553
|
+
case "transfer-encoding":
|
|
554
|
+
case "upgrade":
|
|
555
|
+
return true;
|
|
556
|
+
default:
|
|
557
|
+
return false;
|
|
558
|
+
}
|
|
393
559
|
}
|
|
394
560
|
|
|
395
561
|
function incomingHeadersToHeaders(
|