@chidchanun/bcp 0.1.16 → 0.1.18

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.
@@ -27,6 +27,10 @@ export interface MiddlewareRequest {
27
27
  readonly cookies: MiddlewareCookieStore;
28
28
  }
29
29
 
30
+ export interface MiddlewareContext {
31
+ readonly state: Record<string, unknown>;
32
+ }
33
+
30
34
  export interface MiddlewareHeaderOptions {
31
35
  request?: {
32
36
  headers?: HeadersInit;
@@ -66,21 +70,39 @@ export type MiddlewareResult =
66
70
  | MiddlewareRewriteResult
67
71
  | MiddlewareRedirectResult;
68
72
 
69
- export type MiddlewareHandler = (
70
- request: MiddlewareRequest
71
- ) =>
73
+ export type MiddlewareHandlerResult =
72
74
  | MiddlewareResult
73
75
  | Response
74
- | void
75
- | Promise<
76
- MiddlewareResult |
77
- Response |
78
- void
79
- >;
76
+ | void;
77
+
78
+ export type MiddlewareNext =
79
+ () => Promise<Response>;
80
+
81
+ export type MiddlewareLegacyHandler = (
82
+ request: MiddlewareRequest
83
+ ) =>
84
+ | MiddlewareHandlerResult
85
+ | Promise<MiddlewareHandlerResult>;
86
+
87
+ export type MiddlewarePipelineHandler = (
88
+ request: MiddlewareRequest,
89
+ next: MiddlewareNext,
90
+ context: MiddlewareContext
91
+ ) =>
92
+ | MiddlewareHandlerResult
93
+ | Promise<MiddlewareHandlerResult>;
94
+
95
+ export type MiddlewareHandler =
96
+ | MiddlewareLegacyHandler
97
+ | MiddlewarePipelineHandler;
80
98
 
81
99
  export interface MiddlewareModule {
82
- middleware?: MiddlewareHandler;
83
- default?: MiddlewareHandler;
100
+ middleware?:
101
+ | MiddlewareHandler
102
+ | readonly MiddlewareHandler[];
103
+ default?:
104
+ | MiddlewareHandler
105
+ | readonly MiddlewareHandler[];
84
106
  config?: MiddlewareConfig;
85
107
  }
86
108
 
@@ -114,6 +136,13 @@ export type MiddlewareExecutionResult =
114
136
  response: Response;
115
137
  };
116
138
 
139
+ export type MiddlewareDownstream = (
140
+ input: MiddlewareExecutionInput
141
+ ) => Promise<Response>;
142
+
143
+ const middlewareRedirectTargets =
144
+ new WeakMap<Response, URL>();
145
+
117
146
  export function next(
118
147
  options: MiddlewareHeaderOptions = {}
119
148
  ): MiddlewareNextResult {
@@ -166,6 +195,10 @@ export function redirect(
166
195
  };
167
196
  }
168
197
 
198
+ /**
199
+ * Legacy middleware evaluator retained for backward compatibility and tests.
200
+ * Middleware System v2 runtime traffic uses executeMiddlewarePipeline().
201
+ */
169
202
  export async function executeMiddleware(
170
203
  module: MiddlewareModule | null | undefined,
171
204
  input: MiddlewareExecutionInput
@@ -204,11 +237,14 @@ export async function executeMiddleware(
204
237
  return passthrough();
205
238
  }
206
239
 
207
- const handler =
208
- module.middleware ??
209
- module.default;
240
+ const handlers =
241
+ getMiddlewareHandlers(
242
+ module
243
+ );
210
244
 
211
- if (!handler) {
245
+ if (
246
+ handlers.length === 0
247
+ ) {
212
248
  return passthrough();
213
249
  }
214
250
 
@@ -221,121 +257,308 @@ export async function executeMiddleware(
221
257
  return passthrough();
222
258
  }
223
259
 
224
- const request:
225
- MiddlewareRequest = {
226
- url:
227
- originalUrl.href,
228
- method:
229
- input.method ??
230
- "GET",
231
- headers:
232
- baseHeaders,
233
- nextUrl:
260
+ const handler =
261
+ handlers[0] as MiddlewareLegacyHandler;
262
+
263
+ const request =
264
+ createMiddlewareRequest(
234
265
  originalUrl,
235
- cookies:
236
- createCookieStore(
237
- baseHeaders.get(
238
- "cookie"
239
- )
240
- ),
241
- };
266
+ input.method ?? "GET",
267
+ baseHeaders
268
+ );
242
269
 
243
270
  const result =
244
271
  await handler(
245
272
  request
246
273
  );
247
274
 
248
- if (
249
- result === undefined
250
- ) {
251
- return passthrough();
252
- }
275
+ return normalizeLegacyResult(
276
+ result,
277
+ originalUrl,
278
+ baseHeaders
279
+ );
280
+ }
281
+
282
+ /**
283
+ * Middleware System v2 onion pipeline.
284
+ *
285
+ * Handlers may call await next() to run the remaining middleware and the
286
+ * application, inspect or mutate the returned Response, then return it.
287
+ * Legacy one-argument handlers and next()/rewrite()/redirect() results remain
288
+ * supported inside the same chain.
289
+ */
290
+ export async function executeMiddlewarePipeline(
291
+ module: MiddlewareModule | null | undefined,
292
+ input: MiddlewareExecutionInput,
293
+ downstream: MiddlewareDownstream
294
+ ): Promise<Response> {
295
+ const initialUrl =
296
+ input.url instanceof URL
297
+ ? new URL(
298
+ input.url.href
299
+ )
300
+ : new URL(
301
+ input.url
302
+ );
303
+ const method =
304
+ input.method ??
305
+ "GET";
306
+ const initialHeaders =
307
+ new Headers(
308
+ input.headers
309
+ );
253
310
 
254
311
  if (
255
- result instanceof Response
312
+ !module ||
313
+ isFrameworkInternalPath(
314
+ initialUrl.pathname
315
+ ) ||
316
+ !matchesMiddleware(
317
+ initialUrl.pathname,
318
+ module.config?.matcher
319
+ )
256
320
  ) {
257
- return {
258
- type: "response",
259
- response:
260
- result,
261
- };
321
+ return downstream({
322
+ url:
323
+ initialUrl,
324
+ method,
325
+ headers:
326
+ initialHeaders,
327
+ });
262
328
  }
263
329
 
330
+ const handlers =
331
+ getMiddlewareHandlers(
332
+ module
333
+ );
334
+
264
335
  if (
265
- result.type === "next"
336
+ handlers.length === 0
266
337
  ) {
267
- return {
268
- type: "next",
338
+ return downstream({
269
339
  url:
270
- originalUrl,
271
- requestHeaders:
272
- mergeHeaders(
273
- baseHeaders,
274
- result.requestHeaders
275
- ),
276
- responseHeaders:
277
- result.responseHeaders,
278
- };
340
+ initialUrl,
341
+ method,
342
+ headers:
343
+ initialHeaders,
344
+ });
279
345
  }
280
346
 
281
- if (
282
- result.type === "rewrite"
283
- ) {
284
- const target =
285
- new URL(
286
- result.url,
287
- originalUrl
288
- );
347
+ const current = {
348
+ url:
349
+ initialUrl,
350
+ headers:
351
+ initialHeaders,
352
+ };
353
+ const context:
354
+ MiddlewareContext = {
355
+ state:
356
+ Object.create(
357
+ null
358
+ ) as Record<
359
+ string,
360
+ unknown
361
+ >,
362
+ };
289
363
 
290
- assertSafeMiddlewareTarget(
291
- target,
292
- originalUrl,
293
- "rewrite"
294
- );
364
+ const dispatch =
365
+ async (
366
+ index: number
367
+ ): Promise<Response> => {
368
+ if (
369
+ index >=
370
+ handlers.length
371
+ ) {
372
+ return downstream({
373
+ url:
374
+ new URL(
375
+ current.url.href
376
+ ),
377
+ method,
378
+ headers:
379
+ new Headers(
380
+ current.headers
381
+ ),
382
+ });
383
+ }
295
384
 
296
- return {
297
- type: "rewrite",
298
- url:
299
- target,
300
- requestHeaders:
385
+ const handler =
386
+ handlers[index];
387
+ const request =
388
+ createMiddlewareRequest(
389
+ current.url,
390
+ method,
391
+ current.headers
392
+ );
393
+ let nextPromise:
394
+ Promise<Response> | null =
395
+ null;
396
+
397
+ const runNext:
398
+ MiddlewareNext =
399
+ () => {
400
+ if (nextPromise) {
401
+ throw new Error(
402
+ "BCP Framework: middleware next() may only be called once per handler."
403
+ );
404
+ }
405
+
406
+ nextPromise =
407
+ dispatch(
408
+ index + 1
409
+ );
410
+ return nextPromise;
411
+ };
412
+
413
+ const invoke =
414
+ handler as (
415
+ request:
416
+ MiddlewareRequest,
417
+ next:
418
+ MiddlewareNext,
419
+ context:
420
+ MiddlewareContext
421
+ ) =>
422
+ | MiddlewareHandlerResult
423
+ | Promise<MiddlewareHandlerResult>;
424
+
425
+ const result =
426
+ await invoke(
427
+ request,
428
+ runNext,
429
+ context
430
+ );
431
+
432
+ if (
433
+ result === undefined
434
+ ) {
435
+ return nextPromise
436
+ ? await nextPromise
437
+ : runNext();
438
+ }
439
+
440
+ if (
441
+ result instanceof
442
+ Response
443
+ ) {
444
+ return result;
445
+ }
446
+
447
+ if (
448
+ result.type ===
449
+ "redirect"
450
+ ) {
451
+ const target =
452
+ new URL(
453
+ result.url,
454
+ current.url
455
+ );
456
+
457
+ assertSafeMiddlewareTarget(
458
+ target,
459
+ current.url,
460
+ "redirect"
461
+ );
462
+
463
+ const headers =
464
+ new Headers(
465
+ result.headers
466
+ );
467
+ headers.set(
468
+ "Location",
469
+ target.href
470
+ );
471
+
472
+ const response =
473
+ new Response(
474
+ null,
475
+ {
476
+ status:
477
+ result.status,
478
+ headers,
479
+ }
480
+ );
481
+
482
+ middlewareRedirectTargets.set(
483
+ response,
484
+ target
485
+ );
486
+
487
+ return response;
488
+ }
489
+
490
+ if (
491
+ result.type ===
492
+ "rewrite"
493
+ ) {
494
+ if (nextPromise) {
495
+ throw new Error(
496
+ "BCP Framework: rewrite() must be returned before middleware calls next()."
497
+ );
498
+ }
499
+
500
+ const target =
501
+ new URL(
502
+ result.url,
503
+ current.url
504
+ );
505
+
506
+ assertSafeMiddlewareTarget(
507
+ target,
508
+ current.url,
509
+ "rewrite"
510
+ );
511
+
512
+ current.url =
513
+ target;
514
+ current.headers =
515
+ mergeHeaders(
516
+ current.headers,
517
+ result.requestHeaders
518
+ );
519
+
520
+ const response =
521
+ await runNext();
522
+
523
+ return applyHeadersToResponse(
524
+ response,
525
+ result.responseHeaders
526
+ );
527
+ }
528
+
529
+ current.headers =
301
530
  mergeHeaders(
302
- baseHeaders,
531
+ current.headers,
303
532
  result.requestHeaders
304
- ),
305
- responseHeaders:
306
- result.responseHeaders,
307
- };
308
- }
533
+ );
309
534
 
310
- if (
311
- result.type === "redirect"
312
- ) {
313
- const target =
314
- new URL(
315
- result.url,
316
- originalUrl
535
+ const response =
536
+ nextPromise
537
+ ? await nextPromise
538
+ : await runNext();
539
+
540
+ return applyHeadersToResponse(
541
+ response,
542
+ result.responseHeaders
317
543
  );
544
+ };
318
545
 
319
- assertSafeMiddlewareTarget(
320
- target,
321
- originalUrl,
322
- "redirect"
323
- );
546
+ return dispatch(0);
547
+ }
324
548
 
325
- return {
326
- type: "redirect",
327
- url:
328
- target,
329
- status:
330
- result.status,
331
- headers:
332
- result.headers,
333
- };
334
- }
549
+ export function getMiddlewareRedirectTarget(
550
+ response: Response
551
+ ): URL | null {
552
+ const target =
553
+ middlewareRedirectTargets.get(
554
+ response
555
+ );
335
556
 
336
- throw new Error(
337
- "BCP Framework: middleware returned an unsupported result."
338
- );
557
+ return target
558
+ ? new URL(
559
+ target.href
560
+ )
561
+ : null;
339
562
  }
340
563
 
341
564
  export function matchesMiddleware(
@@ -526,6 +749,223 @@ export async function sendMiddlewareResponse(
526
749
  );
527
750
  }
528
751
 
752
+ function getMiddlewareHandlers(
753
+ module: MiddlewareModule
754
+ ): MiddlewareHandler[] {
755
+ const value =
756
+ module.middleware ??
757
+ module.default;
758
+
759
+ if (!value) {
760
+ return [];
761
+ }
762
+
763
+ return Array.isArray(
764
+ value
765
+ )
766
+ ? [
767
+ ...value,
768
+ ]
769
+ : [
770
+ value as MiddlewareHandler,
771
+ ];
772
+ }
773
+
774
+ function createMiddlewareRequest(
775
+ url: URL,
776
+ method: string,
777
+ headers: Headers
778
+ ): MiddlewareRequest {
779
+ const requestUrl =
780
+ new URL(
781
+ url.href
782
+ );
783
+ const requestHeaders =
784
+ new Headers(
785
+ headers
786
+ );
787
+
788
+ return {
789
+ url:
790
+ requestUrl.href,
791
+ method,
792
+ headers:
793
+ requestHeaders,
794
+ nextUrl:
795
+ requestUrl,
796
+ cookies:
797
+ createCookieStore(
798
+ requestHeaders.get(
799
+ "cookie"
800
+ )
801
+ ),
802
+ };
803
+ }
804
+
805
+ function normalizeLegacyResult(
806
+ result: MiddlewareHandlerResult,
807
+ originalUrl: URL,
808
+ baseHeaders: Headers
809
+ ): MiddlewareExecutionResult {
810
+ if (
811
+ result === undefined
812
+ ) {
813
+ return {
814
+ type: "next",
815
+ url:
816
+ originalUrl,
817
+ requestHeaders:
818
+ baseHeaders,
819
+ responseHeaders:
820
+ new Headers(),
821
+ };
822
+ }
823
+
824
+ if (
825
+ result instanceof Response
826
+ ) {
827
+ return {
828
+ type: "response",
829
+ response:
830
+ result,
831
+ };
832
+ }
833
+
834
+ if (
835
+ result.type === "next"
836
+ ) {
837
+ return {
838
+ type: "next",
839
+ url:
840
+ originalUrl,
841
+ requestHeaders:
842
+ mergeHeaders(
843
+ baseHeaders,
844
+ result.requestHeaders
845
+ ),
846
+ responseHeaders:
847
+ result.responseHeaders,
848
+ };
849
+ }
850
+
851
+ if (
852
+ result.type === "rewrite"
853
+ ) {
854
+ const target =
855
+ new URL(
856
+ result.url,
857
+ originalUrl
858
+ );
859
+
860
+ assertSafeMiddlewareTarget(
861
+ target,
862
+ originalUrl,
863
+ "rewrite"
864
+ );
865
+
866
+ return {
867
+ type: "rewrite",
868
+ url:
869
+ target,
870
+ requestHeaders:
871
+ mergeHeaders(
872
+ baseHeaders,
873
+ result.requestHeaders
874
+ ),
875
+ responseHeaders:
876
+ result.responseHeaders,
877
+ };
878
+ }
879
+
880
+ if (
881
+ result.type === "redirect"
882
+ ) {
883
+ const target =
884
+ new URL(
885
+ result.url,
886
+ originalUrl
887
+ );
888
+
889
+ assertSafeMiddlewareTarget(
890
+ target,
891
+ originalUrl,
892
+ "redirect"
893
+ );
894
+
895
+ return {
896
+ type: "redirect",
897
+ url:
898
+ target,
899
+ status:
900
+ result.status,
901
+ headers:
902
+ result.headers,
903
+ };
904
+ }
905
+
906
+ throw new Error(
907
+ "BCP Framework: middleware returned an unsupported result."
908
+ );
909
+ }
910
+
911
+ function applyHeadersToResponse(
912
+ response: Response,
913
+ additions: Headers
914
+ ): Response {
915
+ let hasAdditions =
916
+ false;
917
+
918
+ additions.forEach(
919
+ () => {
920
+ hasAdditions =
921
+ true;
922
+ }
923
+ );
924
+
925
+ if (!hasAdditions) {
926
+ return response;
927
+ }
928
+
929
+ const headers =
930
+ new Headers(
931
+ response.headers
932
+ );
933
+
934
+ additions.forEach(
935
+ (value, key) => {
936
+ headers.set(
937
+ key,
938
+ value
939
+ );
940
+ }
941
+ );
942
+
943
+ const wrapped =
944
+ new Response(
945
+ response.body,
946
+ {
947
+ status:
948
+ response.status,
949
+ statusText:
950
+ response.statusText,
951
+ headers,
952
+ }
953
+ );
954
+ const redirectTarget =
955
+ middlewareRedirectTargets.get(
956
+ response
957
+ );
958
+
959
+ if (redirectTarget) {
960
+ middlewareRedirectTargets.set(
961
+ wrapped,
962
+ redirectTarget
963
+ );
964
+ }
965
+
966
+ return wrapped;
967
+ }
968
+
529
969
  function matchPathPattern(
530
970
  pathname: string,
531
971
  pattern: string