@jsenv/server 17.5.0 → 17.6.0

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.
@@ -8485,12 +8485,16 @@ const serverPluginCORS = ({
8485
8485
  return [];
8486
8486
  }
8487
8487
 
8488
+ const allowedOriginChecker = createAllowedOriginChecker(
8489
+ accessControlAllowedOrigins,
8490
+ );
8491
+
8488
8492
  return {
8489
8493
  name: "jsenv:cors",
8490
8494
  injectResponseProperties: (request) => {
8491
8495
  const accessControlHeaders = generateAccessControlHeaders({
8492
8496
  request,
8493
- accessControlAllowedOrigins,
8497
+ allowedOriginChecker,
8494
8498
  accessControlAllowRequestOrigin,
8495
8499
  accessControlAllowedMethods,
8496
8500
  accessControlAllowRequestMethod,
@@ -8507,11 +8511,53 @@ const serverPluginCORS = ({
8507
8511
  };
8508
8512
  };
8509
8513
 
8514
+ /**
8515
+ * An origin ("scheme://host:port") can never contain "*", so it is free to be
8516
+ * used as a wildcard standing for any run of characters except "/":
8517
+ * "https://pr-*-my-app.fly.dev" matches "https://pr-12-my-app.fly.dev".
8518
+ */
8519
+ const createAllowedOriginChecker = (allowedOrigins) => {
8520
+ const literalOrigins = [];
8521
+ const originRegExps = [];
8522
+ for (const allowedOrigin of allowedOrigins) {
8523
+ if (allowedOrigin.includes("*")) {
8524
+ originRegExps.push(originPatternToRegExp(allowedOrigin));
8525
+ } else {
8526
+ literalOrigins.push(allowedOrigin);
8527
+ }
8528
+ }
8529
+
8530
+ return {
8531
+ // when the request origin cannot be reflected back we must still send a
8532
+ // single valid origin, never a pattern
8533
+ defaultOrigin: literalOrigins[0] ?? "*",
8534
+ isAllowed: (origin) => {
8535
+ if (literalOrigins.includes(origin)) {
8536
+ return true;
8537
+ }
8538
+ for (const originRegExp of originRegExps) {
8539
+ if (originRegExp.test(origin)) {
8540
+ return true;
8541
+ }
8542
+ }
8543
+ return false;
8544
+ },
8545
+ };
8546
+ };
8547
+
8548
+ const originPatternToRegExp = (originPattern) => {
8549
+ const source = originPattern
8550
+ .split("*")
8551
+ .map((part) => part.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"))
8552
+ .join("[^/]*");
8553
+ return new RegExp(`^${source}$`);
8554
+ };
8555
+
8510
8556
  // https://www.w3.org/TR/cors/
8511
8557
  // https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS
8512
8558
  const generateAccessControlHeaders = ({
8513
8559
  request: { headers },
8514
- accessControlAllowedOrigins,
8560
+ allowedOriginChecker,
8515
8561
  accessControlAllowRequestOrigin,
8516
8562
  accessControlAllowedMethods,
8517
8563
  accessControlAllowRequestMethod,
@@ -8538,7 +8584,7 @@ const generateAccessControlHeaders = ({
8538
8584
  : null;
8539
8585
 
8540
8586
  if (requestOrigin) {
8541
- if (accessControlAllowedOrigins.includes(requestOrigin)) {
8587
+ if (allowedOriginChecker.isAllowed(requestOrigin)) {
8542
8588
  allowOrigin = requestOrigin;
8543
8589
  vary.push("origin");
8544
8590
  } else if (accessControlAllowRequestOrigin) {
@@ -8550,7 +8596,7 @@ const generateAccessControlHeaders = ({
8550
8596
  }
8551
8597
 
8552
8598
  if (allowOrigin === null) {
8553
- allowOrigin = accessControlAllowedOrigins[0] ?? "*";
8599
+ allowOrigin = allowedOriginChecker.defaultOrigin;
8554
8600
  }
8555
8601
 
8556
8602
  const allowedMethodArray = [...accessControlAllowedMethods];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jsenv/server",
3
- "version": "17.5.0",
3
+ "version": "17.6.0",
4
4
  "type": "module",
5
5
  "description": "A modern Node.js HTTP server with declarative routing, content negotiation, and WebSocket support",
6
6
  "repository": {
@@ -35,12 +35,16 @@ export const serverPluginCORS = ({
35
35
  return [];
36
36
  }
37
37
 
38
+ const allowedOriginChecker = createAllowedOriginChecker(
39
+ accessControlAllowedOrigins,
40
+ );
41
+
38
42
  return {
39
43
  name: "jsenv:cors",
40
44
  injectResponseProperties: (request) => {
41
45
  const accessControlHeaders = generateAccessControlHeaders({
42
46
  request,
43
- accessControlAllowedOrigins,
47
+ allowedOriginChecker,
44
48
  accessControlAllowRequestOrigin,
45
49
  accessControlAllowedMethods,
46
50
  accessControlAllowRequestMethod,
@@ -57,11 +61,53 @@ export const serverPluginCORS = ({
57
61
  };
58
62
  };
59
63
 
64
+ /**
65
+ * An origin ("scheme://host:port") can never contain "*", so it is free to be
66
+ * used as a wildcard standing for any run of characters except "/":
67
+ * "https://pr-*-my-app.fly.dev" matches "https://pr-12-my-app.fly.dev".
68
+ */
69
+ const createAllowedOriginChecker = (allowedOrigins) => {
70
+ const literalOrigins = [];
71
+ const originRegExps = [];
72
+ for (const allowedOrigin of allowedOrigins) {
73
+ if (allowedOrigin.includes("*")) {
74
+ originRegExps.push(originPatternToRegExp(allowedOrigin));
75
+ } else {
76
+ literalOrigins.push(allowedOrigin);
77
+ }
78
+ }
79
+
80
+ return {
81
+ // when the request origin cannot be reflected back we must still send a
82
+ // single valid origin, never a pattern
83
+ defaultOrigin: literalOrigins[0] ?? "*",
84
+ isAllowed: (origin) => {
85
+ if (literalOrigins.includes(origin)) {
86
+ return true;
87
+ }
88
+ for (const originRegExp of originRegExps) {
89
+ if (originRegExp.test(origin)) {
90
+ return true;
91
+ }
92
+ }
93
+ return false;
94
+ },
95
+ };
96
+ };
97
+
98
+ const originPatternToRegExp = (originPattern) => {
99
+ const source = originPattern
100
+ .split("*")
101
+ .map((part) => part.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"))
102
+ .join("[^/]*");
103
+ return new RegExp(`^${source}$`);
104
+ };
105
+
60
106
  // https://www.w3.org/TR/cors/
61
107
  // https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS
62
108
  const generateAccessControlHeaders = ({
63
109
  request: { headers },
64
- accessControlAllowedOrigins,
110
+ allowedOriginChecker,
65
111
  accessControlAllowRequestOrigin,
66
112
  accessControlAllowedMethods,
67
113
  accessControlAllowRequestMethod,
@@ -88,7 +134,7 @@ const generateAccessControlHeaders = ({
88
134
  : null;
89
135
 
90
136
  if (requestOrigin) {
91
- if (accessControlAllowedOrigins.includes(requestOrigin)) {
137
+ if (allowedOriginChecker.isAllowed(requestOrigin)) {
92
138
  allowOrigin = requestOrigin;
93
139
  vary.push("origin");
94
140
  } else if (accessControlAllowRequestOrigin) {
@@ -100,7 +146,7 @@ const generateAccessControlHeaders = ({
100
146
  }
101
147
 
102
148
  if (allowOrigin === null) {
103
- allowOrigin = accessControlAllowedOrigins[0] ?? "*";
149
+ allowOrigin = allowedOriginChecker.defaultOrigin;
104
150
  }
105
151
 
106
152
  const allowedMethodArray = [...accessControlAllowedMethods];