@chidchanun/bcp 0.2.16 → 0.2.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.
@@ -0,0 +1,631 @@
1
+ // packages/server/src/security.ts
2
+ var DEFAULT_BODY_LIMIT = 1024 * 1024;
3
+ function assertSafeMiddlewareTarget(target, originalUrl, kind) {
4
+ if (target.protocol !== "http:" && target.protocol !== "https:") {
5
+ throw new Error(
6
+ `BCP Framework: middleware ${kind} only supports http: and https: URLs.`
7
+ );
8
+ }
9
+ if (target.username || target.password) {
10
+ throw new Error(
11
+ `BCP Framework: middleware ${kind} URLs must not contain credentials.`
12
+ );
13
+ }
14
+ if (kind === "rewrite" && target.origin !== originalUrl.origin) {
15
+ throw new Error(
16
+ "BCP Framework: middleware rewrites must stay on the same origin."
17
+ );
18
+ }
19
+ }
20
+
21
+ // packages/server/src/middleware.ts
22
+ var middlewareRedirectTargets = /* @__PURE__ */ new WeakMap();
23
+ function next(options = {}) {
24
+ return {
25
+ type: "next",
26
+ requestHeaders: new Headers(
27
+ options.request?.headers
28
+ ),
29
+ responseHeaders: new Headers(
30
+ options.response?.headers
31
+ )
32
+ };
33
+ }
34
+ function rewrite(url, options = {}) {
35
+ return {
36
+ type: "rewrite",
37
+ url: url.toString(),
38
+ requestHeaders: new Headers(
39
+ options.request?.headers
40
+ ),
41
+ responseHeaders: new Headers(
42
+ options.response?.headers
43
+ )
44
+ };
45
+ }
46
+ function redirect(url, options = {}) {
47
+ return {
48
+ type: "redirect",
49
+ url: url.toString(),
50
+ status: options.status ?? 307,
51
+ headers: new Headers(
52
+ options.headers
53
+ )
54
+ };
55
+ }
56
+ async function executeMiddleware(module, input) {
57
+ const originalUrl = input.url instanceof URL ? new URL(
58
+ input.url.href
59
+ ) : new URL(
60
+ input.url
61
+ );
62
+ const baseHeaders = new Headers(
63
+ input.headers
64
+ );
65
+ const passthrough = () => ({
66
+ type: "next",
67
+ url: originalUrl,
68
+ requestHeaders: baseHeaders,
69
+ responseHeaders: new Headers()
70
+ });
71
+ if (!module || isFrameworkInternalPath(
72
+ originalUrl.pathname
73
+ )) {
74
+ return passthrough();
75
+ }
76
+ const handlers = getMiddlewareHandlers(
77
+ module
78
+ );
79
+ if (handlers.length === 0) {
80
+ return passthrough();
81
+ }
82
+ if (!matchesMiddleware(
83
+ originalUrl.pathname,
84
+ module.config?.matcher
85
+ )) {
86
+ return passthrough();
87
+ }
88
+ const handler = handlers[0];
89
+ const request = createMiddlewareRequest(
90
+ originalUrl,
91
+ input.method ?? "GET",
92
+ baseHeaders
93
+ );
94
+ const result = await handler(
95
+ request
96
+ );
97
+ return normalizeLegacyResult(
98
+ result,
99
+ originalUrl,
100
+ baseHeaders
101
+ );
102
+ }
103
+ async function executeMiddlewarePipeline(module, input, downstream) {
104
+ const initialUrl = input.url instanceof URL ? new URL(
105
+ input.url.href
106
+ ) : new URL(
107
+ input.url
108
+ );
109
+ const method = input.method ?? "GET";
110
+ const initialHeaders = new Headers(
111
+ input.headers
112
+ );
113
+ if (!module || isFrameworkInternalPath(
114
+ initialUrl.pathname
115
+ ) || !matchesMiddleware(
116
+ initialUrl.pathname,
117
+ module.config?.matcher
118
+ )) {
119
+ return downstream({
120
+ url: initialUrl,
121
+ method,
122
+ headers: initialHeaders
123
+ });
124
+ }
125
+ const handlers = getMiddlewareHandlers(
126
+ module
127
+ );
128
+ if (handlers.length === 0) {
129
+ return downstream({
130
+ url: initialUrl,
131
+ method,
132
+ headers: initialHeaders
133
+ });
134
+ }
135
+ const current = {
136
+ url: initialUrl,
137
+ headers: initialHeaders
138
+ };
139
+ const context = {
140
+ state: /* @__PURE__ */ Object.create(
141
+ null
142
+ )
143
+ };
144
+ const dispatch = async (index) => {
145
+ if (index >= handlers.length) {
146
+ return downstream({
147
+ url: new URL(
148
+ current.url.href
149
+ ),
150
+ method,
151
+ headers: new Headers(
152
+ current.headers
153
+ )
154
+ });
155
+ }
156
+ const handler = handlers[index];
157
+ const request = createMiddlewareRequest(
158
+ current.url,
159
+ method,
160
+ current.headers
161
+ );
162
+ let nextPromise = null;
163
+ const runNext = () => {
164
+ if (nextPromise) {
165
+ throw new Error(
166
+ "BCP Framework: middleware next() may only be called once per handler."
167
+ );
168
+ }
169
+ nextPromise = dispatch(
170
+ index + 1
171
+ );
172
+ return nextPromise;
173
+ };
174
+ const invoke = handler;
175
+ const result = await invoke(
176
+ request,
177
+ runNext,
178
+ context
179
+ );
180
+ if (result === void 0) {
181
+ return nextPromise ? await nextPromise : runNext();
182
+ }
183
+ if (result instanceof Response) {
184
+ return result;
185
+ }
186
+ if (result.type === "redirect") {
187
+ const target = new URL(
188
+ result.url,
189
+ current.url
190
+ );
191
+ assertSafeMiddlewareTarget(
192
+ target,
193
+ current.url,
194
+ "redirect"
195
+ );
196
+ const headers = new Headers(
197
+ result.headers
198
+ );
199
+ headers.set(
200
+ "Location",
201
+ target.href
202
+ );
203
+ const response2 = new Response(
204
+ null,
205
+ {
206
+ status: result.status,
207
+ headers
208
+ }
209
+ );
210
+ middlewareRedirectTargets.set(
211
+ response2,
212
+ target
213
+ );
214
+ return response2;
215
+ }
216
+ if (result.type === "rewrite") {
217
+ if (nextPromise) {
218
+ throw new Error(
219
+ "BCP Framework: rewrite() must be returned before middleware calls next()."
220
+ );
221
+ }
222
+ const target = new URL(
223
+ result.url,
224
+ current.url
225
+ );
226
+ assertSafeMiddlewareTarget(
227
+ target,
228
+ current.url,
229
+ "rewrite"
230
+ );
231
+ current.url = target;
232
+ current.headers = mergeHeaders(
233
+ current.headers,
234
+ result.requestHeaders
235
+ );
236
+ const response2 = await runNext();
237
+ return applyHeadersToResponse(
238
+ response2,
239
+ result.responseHeaders
240
+ );
241
+ }
242
+ current.headers = mergeHeaders(
243
+ current.headers,
244
+ result.requestHeaders
245
+ );
246
+ const response = nextPromise ? await nextPromise : await runNext();
247
+ return applyHeadersToResponse(
248
+ response,
249
+ result.responseHeaders
250
+ );
251
+ };
252
+ return dispatch(0);
253
+ }
254
+ function getMiddlewareRedirectTarget(response) {
255
+ const target = middlewareRedirectTargets.get(
256
+ response
257
+ );
258
+ return target ? new URL(
259
+ target.href
260
+ ) : null;
261
+ }
262
+ function matchesMiddleware(pathname, matcher) {
263
+ if (matcher === void 0) {
264
+ return true;
265
+ }
266
+ const matchers = Array.isArray(
267
+ matcher
268
+ ) ? matcher : [
269
+ matcher
270
+ ];
271
+ if (matchers.length === 0) {
272
+ return false;
273
+ }
274
+ return matchers.some(
275
+ (value) => {
276
+ if (value instanceof RegExp) {
277
+ value.lastIndex = 0;
278
+ return value.test(
279
+ pathname
280
+ );
281
+ }
282
+ return matchPathPattern(
283
+ pathname,
284
+ value
285
+ );
286
+ }
287
+ );
288
+ }
289
+ function applyMiddlewareRequestHeaders(req, headers) {
290
+ const nextHeaders = {};
291
+ headers.forEach(
292
+ (value, key) => {
293
+ nextHeaders[key.toLowerCase()] = value;
294
+ }
295
+ );
296
+ req.headers = nextHeaders;
297
+ }
298
+ function applyMiddlewareResponseHeaders(res, headers) {
299
+ headers.forEach(
300
+ (value, key) => {
301
+ res.setHeader(
302
+ key,
303
+ value
304
+ );
305
+ }
306
+ );
307
+ }
308
+ function sendMiddlewareRedirect(req, res, result) {
309
+ applyMiddlewareResponseHeaders(
310
+ res,
311
+ result.headers
312
+ );
313
+ res.statusCode = result.status;
314
+ res.setHeader(
315
+ "Location",
316
+ result.url.href
317
+ );
318
+ res.setHeader(
319
+ "Cache-Control",
320
+ "no-store"
321
+ );
322
+ if (req.method === "HEAD") {
323
+ res.end();
324
+ return;
325
+ }
326
+ res.end();
327
+ }
328
+ async function sendMiddlewareResponse(req, res, response) {
329
+ res.statusCode = response.status;
330
+ response.headers.forEach(
331
+ (value, key) => {
332
+ res.setHeader(
333
+ key,
334
+ value
335
+ );
336
+ }
337
+ );
338
+ const getSetCookie = response.headers.getSetCookie;
339
+ if (typeof getSetCookie === "function") {
340
+ const cookies = getSetCookie.call(
341
+ response.headers
342
+ );
343
+ if (cookies.length > 0) {
344
+ res.setHeader(
345
+ "Set-Cookie",
346
+ cookies
347
+ );
348
+ }
349
+ }
350
+ if (req.method === "HEAD" || response.body === null) {
351
+ res.end();
352
+ return;
353
+ }
354
+ const body = Buffer.from(
355
+ await response.arrayBuffer()
356
+ );
357
+ if (!res.hasHeader(
358
+ "Content-Length"
359
+ )) {
360
+ res.setHeader(
361
+ "Content-Length",
362
+ body.length
363
+ );
364
+ }
365
+ res.end(
366
+ body
367
+ );
368
+ }
369
+ function getMiddlewareHandlers(module) {
370
+ const value = module.middleware ?? module.default;
371
+ if (!value) {
372
+ return [];
373
+ }
374
+ return Array.isArray(
375
+ value
376
+ ) ? [
377
+ ...value
378
+ ] : [
379
+ value
380
+ ];
381
+ }
382
+ function createMiddlewareRequest(url, method, headers) {
383
+ const requestUrl = new URL(
384
+ url.href
385
+ );
386
+ const requestHeaders = new Headers(
387
+ headers
388
+ );
389
+ return {
390
+ url: requestUrl.href,
391
+ method,
392
+ headers: requestHeaders,
393
+ nextUrl: requestUrl,
394
+ cookies: createCookieStore(
395
+ requestHeaders.get(
396
+ "cookie"
397
+ )
398
+ )
399
+ };
400
+ }
401
+ function normalizeLegacyResult(result, originalUrl, baseHeaders) {
402
+ if (result === void 0) {
403
+ return {
404
+ type: "next",
405
+ url: originalUrl,
406
+ requestHeaders: baseHeaders,
407
+ responseHeaders: new Headers()
408
+ };
409
+ }
410
+ if (result instanceof Response) {
411
+ return {
412
+ type: "response",
413
+ response: result
414
+ };
415
+ }
416
+ if (result.type === "next") {
417
+ return {
418
+ type: "next",
419
+ url: originalUrl,
420
+ requestHeaders: mergeHeaders(
421
+ baseHeaders,
422
+ result.requestHeaders
423
+ ),
424
+ responseHeaders: result.responseHeaders
425
+ };
426
+ }
427
+ if (result.type === "rewrite") {
428
+ const target = new URL(
429
+ result.url,
430
+ originalUrl
431
+ );
432
+ assertSafeMiddlewareTarget(
433
+ target,
434
+ originalUrl,
435
+ "rewrite"
436
+ );
437
+ return {
438
+ type: "rewrite",
439
+ url: target,
440
+ requestHeaders: mergeHeaders(
441
+ baseHeaders,
442
+ result.requestHeaders
443
+ ),
444
+ responseHeaders: result.responseHeaders
445
+ };
446
+ }
447
+ if (result.type === "redirect") {
448
+ const target = new URL(
449
+ result.url,
450
+ originalUrl
451
+ );
452
+ assertSafeMiddlewareTarget(
453
+ target,
454
+ originalUrl,
455
+ "redirect"
456
+ );
457
+ return {
458
+ type: "redirect",
459
+ url: target,
460
+ status: result.status,
461
+ headers: result.headers
462
+ };
463
+ }
464
+ throw new Error(
465
+ "BCP Framework: middleware returned an unsupported result."
466
+ );
467
+ }
468
+ function applyHeadersToResponse(response, additions) {
469
+ let hasAdditions = false;
470
+ additions.forEach(
471
+ () => {
472
+ hasAdditions = true;
473
+ }
474
+ );
475
+ if (!hasAdditions) {
476
+ return response;
477
+ }
478
+ const headers = new Headers(
479
+ response.headers
480
+ );
481
+ additions.forEach(
482
+ (value, key) => {
483
+ headers.set(
484
+ key,
485
+ value
486
+ );
487
+ }
488
+ );
489
+ const wrapped = new Response(
490
+ response.body,
491
+ {
492
+ status: response.status,
493
+ statusText: response.statusText,
494
+ headers
495
+ }
496
+ );
497
+ const redirectTarget = middlewareRedirectTargets.get(
498
+ response
499
+ );
500
+ if (redirectTarget) {
501
+ middlewareRedirectTargets.set(
502
+ wrapped,
503
+ redirectTarget
504
+ );
505
+ }
506
+ return wrapped;
507
+ }
508
+ function matchPathPattern(pathname, pattern) {
509
+ if (pattern === "*" || pattern === "/:path*") {
510
+ return true;
511
+ }
512
+ if (!pattern.startsWith(
513
+ "/"
514
+ )) {
515
+ throw new Error(
516
+ `BCP Framework: middleware matcher "${pattern}" must start with "/".`
517
+ );
518
+ }
519
+ const escaped = pattern.split("/").filter(
520
+ Boolean
521
+ ).map(
522
+ (segment) => {
523
+ if (segment === "*" || /^:[A-Za-z_][A-Za-z0-9_]*\*$/.test(
524
+ segment
525
+ )) {
526
+ return "(?:.*)?";
527
+ }
528
+ if (/^:[A-Za-z_][A-Za-z0-9_]*$/.test(
529
+ segment
530
+ )) {
531
+ return "[^/]+";
532
+ }
533
+ return escapeRegExp(
534
+ segment
535
+ );
536
+ }
537
+ );
538
+ if (escaped.length === 0) {
539
+ return pathname === "/";
540
+ }
541
+ const source = `^/${escaped.join("/")}/?$`;
542
+ return new RegExp(
543
+ source
544
+ ).test(
545
+ pathname
546
+ );
547
+ }
548
+ function createCookieStore(cookieHeader) {
549
+ const values = /* @__PURE__ */ new Map();
550
+ if (cookieHeader) {
551
+ for (const part of cookieHeader.split(";")) {
552
+ const separator = part.indexOf("=");
553
+ if (separator < 0) {
554
+ continue;
555
+ }
556
+ const name = part.slice(
557
+ 0,
558
+ separator
559
+ ).trim();
560
+ const rawValue = part.slice(
561
+ separator + 1
562
+ ).trim();
563
+ if (!name) {
564
+ continue;
565
+ }
566
+ try {
567
+ values.set(
568
+ name,
569
+ decodeURIComponent(
570
+ rawValue
571
+ )
572
+ );
573
+ } catch {
574
+ values.set(
575
+ name,
576
+ rawValue
577
+ );
578
+ }
579
+ }
580
+ }
581
+ return {
582
+ get(name) {
583
+ return values.get(
584
+ name
585
+ ) ?? null;
586
+ },
587
+ has(name) {
588
+ return values.has(
589
+ name
590
+ );
591
+ }
592
+ };
593
+ }
594
+ function mergeHeaders(base, additions) {
595
+ const result = new Headers(
596
+ base
597
+ );
598
+ additions.forEach(
599
+ (value, key) => {
600
+ result.set(
601
+ key,
602
+ value
603
+ );
604
+ }
605
+ );
606
+ return result;
607
+ }
608
+ function isFrameworkInternalPath(pathname) {
609
+ return pathname === "/_bcp" || pathname.startsWith(
610
+ "/_bcp/"
611
+ );
612
+ }
613
+ function escapeRegExp(value) {
614
+ return value.replace(
615
+ /[.*+?^${}()|[\]\\]/g,
616
+ "\\$&"
617
+ );
618
+ }
619
+ export {
620
+ applyMiddlewareRequestHeaders,
621
+ applyMiddlewareResponseHeaders,
622
+ executeMiddleware,
623
+ executeMiddlewarePipeline,
624
+ getMiddlewareRedirectTarget,
625
+ matchesMiddleware,
626
+ next,
627
+ redirect,
628
+ rewrite,
629
+ sendMiddlewareRedirect,
630
+ sendMiddlewareResponse
631
+ };