@jsenv/server 17.0.3 → 17.1.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.
@@ -3863,7 +3863,7 @@ const serverPluginOpenFile = () => {
3863
3863
  const routeInspectorHtmlFileUrl = import.meta
3864
3864
  .resolve("./client/route_inspector/route_inspector.html");
3865
3865
 
3866
- const serverPluginRouteInspector = () => {
3866
+ const serverPluginRouteInspector = ({ canExposeSensitiveData }) => {
3867
3867
  return {
3868
3868
  name: "jsenv:route_inspector",
3869
3869
  routes: [
@@ -3889,7 +3889,9 @@ const serverPluginRouteInspector = () => {
3889
3889
  description: "Get the routes available on this server in JSON.",
3890
3890
  declarationSource: import.meta.url,
3891
3891
  fetch: (request, helpers) => {
3892
- const routeJSON = helpers.router.inspect(request, helpers);
3892
+ const routeJSON = helpers.router.inspect({
3893
+ canExposeSensitiveData,
3894
+ });
3893
3895
  return Response.json(routeJSON);
3894
3896
  },
3895
3897
  },
@@ -5378,13 +5380,18 @@ const createResourcePattern = (pattern) => {
5378
5380
  let decodedPathname = decodeURIComponent(pathname);
5379
5381
  let result;
5380
5382
  if (patternEndsWithSlash && !searchPattern && !hashPattern) {
5381
- if (!decodedPathname.startsWith(pathnamePatternString)) {
5383
+ // also match the path without trailing slash (e.g. /foo matches /foo/)
5384
+ const pathnamePatternWithoutSlash = pathnamePatternString.slice(0, -1);
5385
+ if (decodedPathname === pathnamePatternWithoutSlash) {
5386
+ result = { named: {}, stars: [""] };
5387
+ } else if (!decodedPathname.startsWith(pathnamePatternString)) {
5382
5388
  return null;
5389
+ } else {
5390
+ result = {
5391
+ named: {},
5392
+ stars: [decodedPathname.slice(pathnamePatternString.length)],
5393
+ };
5383
5394
  }
5384
- result = {
5385
- named: {},
5386
- stars: [decodedPathname.slice(pathnamePatternString.length)],
5387
- };
5388
5395
  } else {
5389
5396
  result = pathnamePattern.match(decodedPathname);
5390
5397
  }
@@ -5664,7 +5671,7 @@ const HTTP_METHODS = [
5664
5671
 
5665
5672
  const createRouter = (
5666
5673
  routeDescriptionArray,
5667
- { optionsFallback, logLevel } = {},
5674
+ { optionsFallback, logLevel, redirect = "manual" } = {},
5668
5675
  ) => {
5669
5676
  const logger = createLogger({ logLevel });
5670
5677
  const routeSet = new Set();
@@ -5813,16 +5820,6 @@ const createRouter = (
5813
5820
 
5814
5821
  const match = async (request, fetchSecondArg) => {
5815
5822
  fetchSecondArg.router = router;
5816
- const wouldHaveMatched = {
5817
- // in case nothing matches we can produce a response with Allow: GET, POST, PUT for example
5818
- methodSet: new Set(),
5819
- requestMediaTypeSet: new Set(),
5820
- responseMediaTypeSet: new Set(),
5821
- responseLanguageSet: new Set(),
5822
- responseVersionSet: new Set(),
5823
- responseEncodingSet: new Set(),
5824
- upgrade: false,
5825
- };
5826
5823
 
5827
5824
  let currentServerPlugin;
5828
5825
  let currentRoutingTiming;
@@ -5897,7 +5894,6 @@ It should be set to one of route.${routePropertyName}: ${availableValues.join(",
5897
5894
  break;
5898
5895
  }
5899
5896
  }
5900
-
5901
5897
  if (!matchesAnAvailableValue) {
5902
5898
  request.logger.warn(
5903
5899
  `The value "${responseHeaderValue}" found in response header ${responseHeaderName} is strange.
@@ -5913,217 +5909,267 @@ It should be should be one of route.${routePropertyName}: ${availableValues.join
5913
5909
  checkResponseContentHeader(route, responseHeaders, "encoding");
5914
5910
  };
5915
5911
 
5916
- for (const route of routeSet) {
5917
- onRouteMatchStart(route);
5918
- const resourceMatchResult = route.matchResource(request.resource);
5919
- if (!resourceMatchResult) {
5920
- continue;
5921
- }
5922
- if (!route.matchMethod(request.method)) {
5923
- if (!route.isFallback) {
5924
- wouldHaveMatched.methodSet.add(route.method);
5925
- }
5926
- continue;
5912
+ const resolveResponse = async (fetchReturnValue, { request, route }) => {
5913
+ let status;
5914
+ let statusText;
5915
+ let statusMessage;
5916
+ let headers;
5917
+ let body;
5918
+
5919
+ if (fetchReturnValue instanceof Response) {
5920
+ status = fetchReturnValue.status;
5921
+ statusText = fetchReturnValue.statusText;
5922
+ headers = Object.fromEntries(fetchReturnValue.headers);
5923
+ body = fetchReturnValue.body;
5924
+ } else if (
5925
+ fetchReturnValue !== null &&
5926
+ typeof fetchReturnValue === "object"
5927
+ ) {
5928
+ status = fetchReturnValue.status || 404;
5929
+ statusText = fetchReturnValue.statusText;
5930
+ statusMessage = fetchReturnValue.statusMessage;
5931
+ headers = fetchReturnValue.headers || {};
5932
+ body = fetchReturnValue.body;
5933
+ } else {
5934
+ throw new TypeError(
5935
+ `response must be a Response, or an Object, received ${fetchReturnValue}`,
5936
+ );
5927
5937
  }
5938
+
5928
5939
  if (
5929
- request.method === "POST" ||
5930
- request.method === "PATCH" ||
5931
- request.method === "PUT"
5940
+ redirect === "follow" &&
5941
+ isRedirectStatus(status) &&
5942
+ headers["location"]
5932
5943
  ) {
5933
- const { acceptedMediaTypes } = route;
5944
+ const redirectUrl = new URL(headers["location"], request.url);
5945
+ if (redirectUrl.origin === new URL(request.url).origin) {
5946
+ const redirectRequest = {
5947
+ ...request,
5948
+ url: redirectUrl.href,
5949
+ resource:
5950
+ redirectUrl.pathname + redirectUrl.search + redirectUrl.hash,
5951
+ // GET for 301/302/303, preserve method for 307/308
5952
+ method: status === 307 || status === 308 ? request.method : "GET",
5953
+ params: {},
5954
+ };
5955
+ const redirectedResponse = await matchRoutes(redirectRequest);
5956
+ onRouteMatch(route);
5957
+ onResponseHeaders(request, route, redirectedResponse.headers || {});
5958
+ return redirectedResponse;
5959
+ }
5960
+ // redirect to other origins are left to the client to handle
5961
+ }
5962
+
5963
+ onRouteMatch(route);
5964
+ onResponseHeaders(request, route, headers);
5965
+ return { status, statusText, statusMessage, headers, body };
5966
+ };
5967
+
5968
+ const matchRoutes = async (request) => {
5969
+ // in case nothing matches we can produce a response with Allow: GET, POST, PUT for example
5970
+ const wouldHaveMatched = {
5971
+ methodSet: new Set(),
5972
+ requestMediaTypeSet: new Set(),
5973
+ responseMediaTypeSet: new Set(),
5974
+ responseLanguageSet: new Set(),
5975
+ responseVersionSet: new Set(),
5976
+ responseEncodingSet: new Set(),
5977
+ upgrade: false,
5978
+ };
5979
+
5980
+ for (const route of routeSet) {
5981
+ onRouteMatchStart(route);
5982
+ const resourceMatchResult = route.matchResource(request.resource);
5983
+ if (!resourceMatchResult) {
5984
+ continue;
5985
+ }
5986
+ if (!route.matchMethod(request.method)) {
5987
+ if (!route.isFallback) {
5988
+ wouldHaveMatched.methodSet.add(route.method);
5989
+ }
5990
+ continue;
5991
+ }
5934
5992
  if (
5935
- acceptedMediaTypes.length &&
5936
- !isRequestBodyMediaTypeSupported(request, { acceptedMediaTypes })
5993
+ request.method === "POST" ||
5994
+ request.method === "PATCH" ||
5995
+ request.method === "PUT"
5937
5996
  ) {
5938
- for (const acceptedMediaType of acceptedMediaTypes) {
5939
- wouldHaveMatched.requestMediaTypeSet.add(acceptedMediaType);
5997
+ const { acceptedMediaTypes } = route;
5998
+ if (
5999
+ acceptedMediaTypes.length &&
6000
+ !isRequestBodyMediaTypeSupported(request, { acceptedMediaTypes })
6001
+ ) {
6002
+ for (const acceptedMediaType of acceptedMediaTypes) {
6003
+ wouldHaveMatched.requestMediaTypeSet.add(acceptedMediaType);
6004
+ }
6005
+ continue;
5940
6006
  }
6007
+ }
6008
+ const headersMatchResult = route.matchHeaders(request.headers);
6009
+ if (!headersMatchResult) {
5941
6010
  continue;
5942
6011
  }
5943
- }
5944
- const headersMatchResult = route.matchHeaders(request.headers);
5945
- if (!headersMatchResult) {
5946
- continue;
5947
- }
5948
- if (route.isForWebSocket && request.headers["upgrade"] !== "websocket") {
5949
- wouldHaveMatched.upgrade = true;
5950
- continue;
5951
- }
5952
- // now we are "good", let's try to generate a response
5953
- const contentNegotiationResult = {};
5954
- {
5955
- // when content nego fails
5956
- // we will check the remaining accept headers to properly inform client of all the things are failing
5957
- // Example:
5958
- // client says "I want text in french"
5959
- // but server only provide json in english
5960
- // we want to tell client both text and french are not available
5961
- let hasFailed = false;
5962
- const { availableMediaTypes } = route;
5963
- if (availableMediaTypes.length) {
5964
- fetchSecondArg.injectResponseHeader("vary", "accept");
5965
- if (request.headers["accept"]) {
5966
- const mediaTypeNegotiated = pickContentType(
5967
- request,
5968
- availableMediaTypes,
5969
- );
5970
- if (!mediaTypeNegotiated) {
5971
- for (const availableMediaType of availableMediaTypes) {
5972
- wouldHaveMatched.responseMediaTypeSet.add(availableMediaType);
6012
+ if (
6013
+ route.isForWebSocket &&
6014
+ request.headers["upgrade"] !== "websocket"
6015
+ ) {
6016
+ wouldHaveMatched.upgrade = true;
6017
+ continue;
6018
+ }
6019
+ // now we are "good", let's try to generate a response
6020
+ const contentNegotiationResult = {};
6021
+ {
6022
+ // when content nego fails
6023
+ // we will check the remaining accept headers to properly inform client of all the things are failing
6024
+ // Example:
6025
+ // client says "I want text in french"
6026
+ // but server only provide json in english
6027
+ // we want to tell client both text and french are not available
6028
+ let hasFailed = false;
6029
+ const { availableMediaTypes } = route;
6030
+ if (availableMediaTypes.length) {
6031
+ fetchSecondArg.injectResponseHeader("vary", "accept");
6032
+ if (request.headers["accept"]) {
6033
+ const mediaTypeNegotiated = pickContentType(
6034
+ request,
6035
+ availableMediaTypes,
6036
+ );
6037
+ if (!mediaTypeNegotiated) {
6038
+ for (const availableMediaType of availableMediaTypes) {
6039
+ wouldHaveMatched.responseMediaTypeSet.add(availableMediaType);
6040
+ }
6041
+ hasFailed = true;
5973
6042
  }
5974
- hasFailed = true;
6043
+ contentNegotiationResult.mediaType = mediaTypeNegotiated;
6044
+ } else {
6045
+ contentNegotiationResult.mediaType = availableMediaTypes[0];
5975
6046
  }
5976
- contentNegotiationResult.mediaType = mediaTypeNegotiated;
5977
- } else {
5978
- contentNegotiationResult.mediaType = availableMediaTypes[0];
5979
6047
  }
5980
- }
5981
- const { availableLanguages } = route;
5982
- if (availableLanguages.length) {
5983
- fetchSecondArg.injectResponseHeader("vary", "accept-language");
5984
- if (request.headers["accept-language"]) {
5985
- const languageNegotiated = pickContentLanguage(
5986
- request,
5987
- availableLanguages,
5988
- );
5989
- if (!languageNegotiated) {
5990
- for (const availableLanguage of availableLanguages) {
5991
- wouldHaveMatched.responseLanguageSet.add(availableLanguage);
6048
+ const { availableLanguages } = route;
6049
+ if (availableLanguages.length) {
6050
+ fetchSecondArg.injectResponseHeader("vary", "accept-language");
6051
+ if (request.headers["accept-language"]) {
6052
+ const languageNegotiated = pickContentLanguage(
6053
+ request,
6054
+ availableLanguages,
6055
+ );
6056
+ if (!languageNegotiated) {
6057
+ for (const availableLanguage of availableLanguages) {
6058
+ wouldHaveMatched.responseLanguageSet.add(availableLanguage);
6059
+ }
6060
+ hasFailed = true;
5992
6061
  }
5993
- hasFailed = true;
6062
+ contentNegotiationResult.language = languageNegotiated;
6063
+ } else {
6064
+ contentNegotiationResult.language = availableLanguages[0];
5994
6065
  }
5995
- contentNegotiationResult.language = languageNegotiated;
5996
- } else {
5997
- contentNegotiationResult.language = availableLanguages[0];
5998
6066
  }
5999
- }
6000
- const { availableVersions } = route;
6001
- if (availableVersions.length) {
6002
- fetchSecondArg.injectResponseHeader("vary", "accept-version");
6003
- if (request.headers["accept-version"]) {
6004
- const versionNegotiated = pickContentVersion(
6005
- request,
6006
- availableVersions,
6007
- );
6008
- if (!versionNegotiated) {
6009
- for (const availableVersion of availableVersions) {
6010
- wouldHaveMatched.responseVersionSet.add(availableVersion);
6067
+ const { availableVersions } = route;
6068
+ if (availableVersions.length) {
6069
+ fetchSecondArg.injectResponseHeader("vary", "accept-version");
6070
+ if (request.headers["accept-version"]) {
6071
+ const versionNegotiated = pickContentVersion(
6072
+ request,
6073
+ availableVersions,
6074
+ );
6075
+ if (!versionNegotiated) {
6076
+ for (const availableVersion of availableVersions) {
6077
+ wouldHaveMatched.responseVersionSet.add(availableVersion);
6078
+ }
6079
+ hasFailed = true;
6011
6080
  }
6012
- hasFailed = true;
6081
+ contentNegotiationResult.version = versionNegotiated;
6082
+ } else {
6083
+ contentNegotiationResult.version = availableVersions[0];
6013
6084
  }
6014
- contentNegotiationResult.version = versionNegotiated;
6015
- } else {
6016
- contentNegotiationResult.version = availableVersions[0];
6017
6085
  }
6018
- }
6019
- const { availableEncodings } = route;
6020
- if (availableEncodings.length) {
6021
- fetchSecondArg.injectResponseHeader("vary", "accept-encoding");
6022
- if (request.headers["accept-encoding"]) {
6023
- const encodingNegotiated = pickContentEncoding(
6024
- request,
6025
- availableEncodings,
6026
- );
6027
- if (!encodingNegotiated) {
6028
- for (const availableEncoding of availableEncodings) {
6029
- wouldHaveMatched.responseEncodingSet.add(availableEncoding);
6086
+ const { availableEncodings } = route;
6087
+ if (availableEncodings.length) {
6088
+ fetchSecondArg.injectResponseHeader("vary", "accept-encoding");
6089
+ if (request.headers["accept-encoding"]) {
6090
+ const encodingNegotiated = pickContentEncoding(
6091
+ request,
6092
+ availableEncodings,
6093
+ );
6094
+ if (!encodingNegotiated) {
6095
+ for (const availableEncoding of availableEncodings) {
6096
+ wouldHaveMatched.responseEncodingSet.add(availableEncoding);
6097
+ }
6098
+ hasFailed = true;
6030
6099
  }
6031
- hasFailed = true;
6100
+ contentNegotiationResult.encoding = encodingNegotiated;
6101
+ } else {
6102
+ contentNegotiationResult.encoding = availableEncodings[0];
6032
6103
  }
6033
- contentNegotiationResult.encoding = encodingNegotiated;
6034
- } else {
6035
- contentNegotiationResult.encoding = availableEncodings[0];
6104
+ }
6105
+ if (hasFailed) {
6106
+ continue;
6036
6107
  }
6037
6108
  }
6038
- if (hasFailed) {
6109
+ const { named, stars = [] } = PATTERN.composeTwoMatchResults(
6110
+ resourceMatchResult,
6111
+ headersMatchResult,
6112
+ );
6113
+ Object.assign(request.params, named, stars);
6114
+ fetchSecondArg.contentNegotiation = contentNegotiationResult;
6115
+ let fetchReturnValue = route.fetch(request, fetchSecondArg);
6116
+ if (
6117
+ fetchReturnValue !== null &&
6118
+ typeof fetchReturnValue === "object" &&
6119
+ typeof fetchReturnValue.then === "function"
6120
+ ) {
6121
+ fetchReturnValue = await fetchReturnValue;
6122
+ }
6123
+ // route decided not to handle in the end
6124
+ if (fetchReturnValue === null || fetchReturnValue === undefined) {
6039
6125
  continue;
6040
6126
  }
6127
+ return resolveResponse(fetchReturnValue, { request, route });
6128
+ }
6129
+ // nothing has matched fully
6130
+ // if nothing matches at all we'll send 404
6131
+ // but if url matched but METHOD was not supported we send 405
6132
+ if (wouldHaveMatched.methodSet.size) {
6133
+ return createMethodNotAllowedResponse(request, {
6134
+ allowedMethods: [...wouldHaveMatched.methodSet],
6135
+ });
6136
+ }
6137
+ if (wouldHaveMatched.requestMediaTypeSet.size) {
6138
+ return createUnsupportedMediaTypeResponse(request, {
6139
+ acceptedMediaTypes: [...wouldHaveMatched.requestMediaTypeSet],
6140
+ });
6041
6141
  }
6042
- const { named, stars = [] } = PATTERN.composeTwoMatchResults(
6043
- resourceMatchResult,
6044
- headersMatchResult,
6045
- );
6046
- Object.assign(request.params, named, stars);
6047
- fetchSecondArg.contentNegotiation = contentNegotiationResult;
6048
- let fetchReturnValue = route.fetch(request, fetchSecondArg);
6049
6142
  if (
6050
- fetchReturnValue !== null &&
6051
- typeof fetchReturnValue === "object" &&
6052
- typeof fetchReturnValue.then === "function"
6143
+ wouldHaveMatched.responseMediaTypeSet.size ||
6144
+ wouldHaveMatched.responseLanguageSet.size ||
6145
+ wouldHaveMatched.responseVersionSet.size ||
6146
+ wouldHaveMatched.responseEncodingSet.size
6053
6147
  ) {
6054
- fetchReturnValue = await fetchReturnValue;
6055
- }
6056
- // route decided not to handle in the end
6057
- if (fetchReturnValue === null || fetchReturnValue === undefined) {
6058
- continue;
6059
- }
6060
- onRouteMatch(route);
6061
- if (fetchReturnValue instanceof Response) {
6062
- const headers = Object.fromEntries(fetchReturnValue.headers);
6063
- onResponseHeaders(request, route, headers);
6064
- return {
6065
- status: fetchReturnValue.status,
6066
- statusText: fetchReturnValue.statusText,
6067
- headers,
6068
- body: fetchReturnValue.body,
6069
- };
6148
+ return createNotAcceptableResponse(request, {
6149
+ availableMediaTypes: [...wouldHaveMatched.responseMediaTypeSet],
6150
+ availableLanguages: [...wouldHaveMatched.responseLanguageSet],
6151
+ availableVersions: [...wouldHaveMatched.responseVersionSet],
6152
+ availableEncodings: [...wouldHaveMatched.responseEncodingSet],
6153
+ });
6070
6154
  }
6071
- if (fetchReturnValue !== null && typeof fetchReturnValue === "object") {
6072
- const headers = fetchReturnValue.headers || {};
6073
- onResponseHeaders(request, route, headers);
6155
+ if (wouldHaveMatched.upgrade) {
6074
6156
  return {
6075
- status: fetchReturnValue.status || 404,
6076
- statusText: fetchReturnValue.statusText,
6077
- statusMessage: fetchReturnValue.statusMessage,
6078
- headers,
6079
- body: fetchReturnValue.body,
6157
+ status: 426,
6158
+ statusText: "Upgrade Required",
6159
+ statusMessage: `The request requires the upgrade to a webSocket connection`,
6080
6160
  };
6081
6161
  }
6082
- throw new TypeError(
6083
- `response must be a Response, or an Object, received ${fetchReturnValue}`,
6084
- );
6085
- }
6086
- // nothing has matched fully
6087
- // if nothing matches at all we'll send 404
6088
- // but if url matched but METHOD was not supported we send 405
6089
- if (wouldHaveMatched.methodSet.size) {
6090
- return createMethodNotAllowedResponse(request, {
6091
- allowedMethods: [...wouldHaveMatched.methodSet],
6092
- });
6093
- }
6094
- if (wouldHaveMatched.requestMediaTypeSet.size) {
6095
- return createUnsupportedMediaTypeResponse(request, {
6096
- acceptedMediaTypes: [...wouldHaveMatched.requestMediaTypeSet],
6097
- });
6098
- }
6099
- if (
6100
- wouldHaveMatched.responseMediaTypeSet.size ||
6101
- wouldHaveMatched.responseLanguageSet.size ||
6102
- wouldHaveMatched.responseVersionSet.size ||
6103
- wouldHaveMatched.responseEncodingSet.size
6104
- ) {
6105
- return createNotAcceptableResponse(request, {
6106
- availableMediaTypes: [...wouldHaveMatched.responseMediaTypeSet],
6107
- availableLanguages: [...wouldHaveMatched.responseLanguageSet],
6108
- availableVersions: [...wouldHaveMatched.responseVersionSet],
6109
- availableEncodings: [...wouldHaveMatched.responseEncodingSet],
6110
- });
6111
- }
6112
- if (wouldHaveMatched.upgrade) {
6113
- return {
6114
- status: 426,
6115
- statusText: "Upgrade Required",
6116
- statusMessage: `The request requires the upgrade to a webSocket connection`,
6117
- };
6118
- }
6119
- constructAvailableEndpoints();
6120
- return createRouteNotFoundResponse(request);
6162
+ constructAvailableEndpoints();
6163
+ return createRouteNotFoundResponse(request);
6164
+ }; // end matchRoutes
6165
+
6166
+ return matchRoutes(request);
6121
6167
  };
6122
- const inspect = () => {
6168
+ const inspect = ({ canExposeSensitiveData }) => {
6123
6169
  // I want all the info I can gather about the routes
6124
6170
  const data = [];
6125
6171
  for (const route of routeSet) {
6126
- data.push(route.toJSON());
6172
+ data.push(route.toJSON({ canExposeSensitiveData }));
6127
6173
  }
6128
6174
  return data;
6129
6175
  };
@@ -6248,10 +6294,10 @@ const createRoute = ({
6248
6294
  toString: () => {
6249
6295
  return `${method} ${resource}`;
6250
6296
  },
6251
- toJSON: () => {
6297
+ toJSON: ({ canExposeSensitiveData }) => {
6252
6298
  const meta = {};
6253
6299
 
6254
- if (declarationSource) {
6300
+ if (declarationSource && canExposeSensitiveData) {
6255
6301
  meta.declarationLink = {
6256
6302
  url: `javascript:window.fetch("/.internal/open_file/${encodeURIComponent(declarationSource)}")`,
6257
6303
  text: declarationSource,
@@ -6279,7 +6325,6 @@ const createRoute = ({
6279
6325
  } catch {}
6280
6326
  }
6281
6327
  }
6282
-
6283
6328
  return {
6284
6329
  method,
6285
6330
  resource,
@@ -6296,7 +6341,9 @@ const createRoute = ({
6296
6341
  : typeof clientCodeExample === "string"
6297
6342
  ? clientCodeExample
6298
6343
  : undefined,
6299
- declarationSource,
6344
+ declarationSource: canExposeSensitiveData
6345
+ ? declarationSource
6346
+ : undefined,
6300
6347
  meta,
6301
6348
  };
6302
6349
  },
@@ -6308,6 +6355,9 @@ const createRoute = ({
6308
6355
  return route;
6309
6356
  };
6310
6357
 
6358
+ const REDIRECT_STATUSES = new Set([301, 302, 303, 307, 308]);
6359
+ const isRedirectStatus = (status) => REDIRECT_STATUSES.has(status);
6360
+
6311
6361
  const isRequestBodyMediaTypeSupported = (request, { acceptedMediaTypes }) => {
6312
6362
  const requestBodyContentType = request.headers["content-type"];
6313
6363
  if (!requestBodyContentType) {
@@ -6749,6 +6799,12 @@ const startServer = async ({
6749
6799
  keepProcessAlive = true,
6750
6800
  routes = [],
6751
6801
  plugins = [],
6802
+ // When enabled, the server gives more power:
6803
+ // - each route show the source where it was declared
6804
+ // - server can be requested to open a file on the machine
6805
+ // - client can subscribe to server to detect when it restarts
6806
+ // This param should be enabled ONLY during development on your machine
6807
+ canExposeSensitiveData = false,
6752
6808
  nagle = true,
6753
6809
  serverTiming = false,
6754
6810
  requestWaitingMs = 0,
@@ -6823,11 +6879,11 @@ const startServer = async ({
6823
6879
  }
6824
6880
 
6825
6881
  plugins = [
6826
- serverPluginOpenFile(),
6882
+ ...(canExposeSensitiveData ? [serverPluginOpenFile()] : []),
6827
6883
  serverPluginDefaultBody4xx5xx(),
6828
- serverPluginRouteInspector(),
6829
- serverPluginInternalClientFiles(),
6830
- serverPluginAutoreloadOnRestart(),
6884
+ serverPluginRouteInspector({ canExposeSensitiveData }),
6885
+ ...(canExposeSensitiveData ? [serverPluginInternalClientFiles()] : []),
6886
+ ...(canExposeSensitiveData ? [serverPluginAutoreloadOnRestart()] : []),
6831
6887
  ...plugins,
6832
6888
  ];
6833
6889
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jsenv/server",
3
- "version": "17.0.3",
3
+ "version": "17.1.0",
4
4
  "description": "A modern Node.js HTTP server with declarative routing, content negotiation, and WebSocket support",
5
5
  "repository": {
6
6
  "type": "git",
@@ -3,7 +3,7 @@ import { readFileSync } from "node:fs";
3
3
  const routeInspectorHtmlFileUrl = import.meta
4
4
  .resolve("./client/route_inspector.html");
5
5
 
6
- export const serverPluginRouteInspector = () => {
6
+ export const serverPluginRouteInspector = ({ canExposeSensitiveData }) => {
7
7
  return {
8
8
  name: "jsenv:route_inspector",
9
9
  routes: [
@@ -29,7 +29,9 @@ export const serverPluginRouteInspector = () => {
29
29
  description: "Get the routes available on this server in JSON.",
30
30
  declarationSource: import.meta.url,
31
31
  fetch: (request, helpers) => {
32
- const routeJSON = helpers.router.inspect(request, helpers);
32
+ const routeJSON = helpers.router.inspect({
33
+ canExposeSensitiveData,
34
+ });
33
35
  return Response.json(routeJSON);
34
36
  },
35
37
  },
@@ -28,7 +28,7 @@ const HTTP_METHODS = [
28
28
 
29
29
  export const createRouter = (
30
30
  routeDescriptionArray,
31
- { optionsFallback, logLevel } = {},
31
+ { optionsFallback, logLevel, redirect = "manual" } = {},
32
32
  ) => {
33
33
  const logger = createLogger({ logLevel });
34
34
  const routeSet = new Set();
@@ -177,16 +177,6 @@ export const createRouter = (
177
177
 
178
178
  const match = async (request, fetchSecondArg) => {
179
179
  fetchSecondArg.router = router;
180
- const wouldHaveMatched = {
181
- // in case nothing matches we can produce a response with Allow: GET, POST, PUT for example
182
- methodSet: new Set(),
183
- requestMediaTypeSet: new Set(),
184
- responseMediaTypeSet: new Set(),
185
- responseLanguageSet: new Set(),
186
- responseVersionSet: new Set(),
187
- responseEncodingSet: new Set(),
188
- upgrade: false,
189
- };
190
180
 
191
181
  let currentServerPlugin;
192
182
  let currentRoutingTiming;
@@ -261,7 +251,6 @@ It should be set to one of route.${routePropertyName}: ${availableValues.join(",
261
251
  break;
262
252
  }
263
253
  }
264
-
265
254
  if (!matchesAnAvailableValue) {
266
255
  request.logger.warn(
267
256
  `The value "${responseHeaderValue}" found in response header ${responseHeaderName} is strange.
@@ -277,220 +266,270 @@ It should be should be one of route.${routePropertyName}: ${availableValues.join
277
266
  checkResponseContentHeader(route, responseHeaders, "encoding");
278
267
  };
279
268
 
280
- for (const route of routeSet) {
281
- onRouteMatchStart(route);
282
- const resourceMatchResult = route.matchResource(request.resource);
283
- if (!resourceMatchResult) {
284
- continue;
285
- }
286
- if (!route.matchMethod(request.method)) {
287
- if (!route.isFallback) {
288
- wouldHaveMatched.methodSet.add(route.method);
289
- }
290
- continue;
269
+ const resolveResponse = async (fetchReturnValue, { request, route }) => {
270
+ let status;
271
+ let statusText;
272
+ let statusMessage;
273
+ let headers;
274
+ let body;
275
+
276
+ if (fetchReturnValue instanceof Response) {
277
+ status = fetchReturnValue.status;
278
+ statusText = fetchReturnValue.statusText;
279
+ headers = Object.fromEntries(fetchReturnValue.headers);
280
+ body = fetchReturnValue.body;
281
+ } else if (
282
+ fetchReturnValue !== null &&
283
+ typeof fetchReturnValue === "object"
284
+ ) {
285
+ status = fetchReturnValue.status || 404;
286
+ statusText = fetchReturnValue.statusText;
287
+ statusMessage = fetchReturnValue.statusMessage;
288
+ headers = fetchReturnValue.headers || {};
289
+ body = fetchReturnValue.body;
290
+ } else {
291
+ throw new TypeError(
292
+ `response must be a Response, or an Object, received ${fetchReturnValue}`,
293
+ );
291
294
  }
295
+
292
296
  if (
293
- request.method === "POST" ||
294
- request.method === "PATCH" ||
295
- request.method === "PUT"
297
+ redirect === "follow" &&
298
+ isRedirectStatus(status) &&
299
+ headers["location"]
296
300
  ) {
297
- const { acceptedMediaTypes } = route;
301
+ const redirectUrl = new URL(headers["location"], request.url);
302
+ if (redirectUrl.origin === new URL(request.url).origin) {
303
+ const redirectRequest = {
304
+ ...request,
305
+ url: redirectUrl.href,
306
+ resource:
307
+ redirectUrl.pathname + redirectUrl.search + redirectUrl.hash,
308
+ // GET for 301/302/303, preserve method for 307/308
309
+ method: status === 307 || status === 308 ? request.method : "GET",
310
+ params: {},
311
+ };
312
+ const redirectedResponse = await matchRoutes(redirectRequest);
313
+ onRouteMatch(route);
314
+ onResponseHeaders(request, route, redirectedResponse.headers || {});
315
+ return redirectedResponse;
316
+ }
317
+ // redirect to other origins are left to the client to handle
318
+ }
319
+
320
+ onRouteMatch(route);
321
+ onResponseHeaders(request, route, headers);
322
+ return { status, statusText, statusMessage, headers, body };
323
+ };
324
+
325
+ const matchRoutes = async (request) => {
326
+ // in case nothing matches we can produce a response with Allow: GET, POST, PUT for example
327
+ const wouldHaveMatched = {
328
+ methodSet: new Set(),
329
+ requestMediaTypeSet: new Set(),
330
+ responseMediaTypeSet: new Set(),
331
+ responseLanguageSet: new Set(),
332
+ responseVersionSet: new Set(),
333
+ responseEncodingSet: new Set(),
334
+ upgrade: false,
335
+ };
336
+
337
+ for (const route of routeSet) {
338
+ onRouteMatchStart(route);
339
+ const resourceMatchResult = route.matchResource(request.resource);
340
+ if (!resourceMatchResult) {
341
+ continue;
342
+ }
343
+ if (!route.matchMethod(request.method)) {
344
+ if (!route.isFallback) {
345
+ wouldHaveMatched.methodSet.add(route.method);
346
+ }
347
+ continue;
348
+ }
298
349
  if (
299
- acceptedMediaTypes.length &&
300
- !isRequestBodyMediaTypeSupported(request, { acceptedMediaTypes })
350
+ request.method === "POST" ||
351
+ request.method === "PATCH" ||
352
+ request.method === "PUT"
301
353
  ) {
302
- for (const acceptedMediaType of acceptedMediaTypes) {
303
- wouldHaveMatched.requestMediaTypeSet.add(acceptedMediaType);
354
+ const { acceptedMediaTypes } = route;
355
+ if (
356
+ acceptedMediaTypes.length &&
357
+ !isRequestBodyMediaTypeSupported(request, { acceptedMediaTypes })
358
+ ) {
359
+ for (const acceptedMediaType of acceptedMediaTypes) {
360
+ wouldHaveMatched.requestMediaTypeSet.add(acceptedMediaType);
361
+ }
362
+ continue;
304
363
  }
364
+ }
365
+ const headersMatchResult = route.matchHeaders(request.headers);
366
+ if (!headersMatchResult) {
305
367
  continue;
306
368
  }
307
- }
308
- const headersMatchResult = route.matchHeaders(request.headers);
309
- if (!headersMatchResult) {
310
- continue;
311
- }
312
- if (route.isForWebSocket && request.headers["upgrade"] !== "websocket") {
313
- wouldHaveMatched.upgrade = true;
314
- continue;
315
- }
316
- // now we are "good", let's try to generate a response
317
- const contentNegotiationResult = {};
318
- content_negotiation: {
319
- // when content nego fails
320
- // we will check the remaining accept headers to properly inform client of all the things are failing
321
- // Example:
322
- // client says "I want text in french"
323
- // but server only provide json in english
324
- // we want to tell client both text and french are not available
325
- let hasFailed = false;
326
- const { availableMediaTypes } = route;
327
- if (availableMediaTypes.length) {
328
- fetchSecondArg.injectResponseHeader("vary", "accept");
329
- if (request.headers["accept"]) {
330
- const mediaTypeNegotiated = pickContentType(
331
- request,
332
- availableMediaTypes,
333
- );
334
- if (!mediaTypeNegotiated) {
335
- for (const availableMediaType of availableMediaTypes) {
336
- wouldHaveMatched.responseMediaTypeSet.add(availableMediaType);
369
+ if (
370
+ route.isForWebSocket &&
371
+ request.headers["upgrade"] !== "websocket"
372
+ ) {
373
+ wouldHaveMatched.upgrade = true;
374
+ continue;
375
+ }
376
+ // now we are "good", let's try to generate a response
377
+ const contentNegotiationResult = {};
378
+ content_negotiation: {
379
+ // when content nego fails
380
+ // we will check the remaining accept headers to properly inform client of all the things are failing
381
+ // Example:
382
+ // client says "I want text in french"
383
+ // but server only provide json in english
384
+ // we want to tell client both text and french are not available
385
+ let hasFailed = false;
386
+ const { availableMediaTypes } = route;
387
+ if (availableMediaTypes.length) {
388
+ fetchSecondArg.injectResponseHeader("vary", "accept");
389
+ if (request.headers["accept"]) {
390
+ const mediaTypeNegotiated = pickContentType(
391
+ request,
392
+ availableMediaTypes,
393
+ );
394
+ if (!mediaTypeNegotiated) {
395
+ for (const availableMediaType of availableMediaTypes) {
396
+ wouldHaveMatched.responseMediaTypeSet.add(availableMediaType);
397
+ }
398
+ hasFailed = true;
337
399
  }
338
- hasFailed = true;
400
+ contentNegotiationResult.mediaType = mediaTypeNegotiated;
401
+ } else {
402
+ contentNegotiationResult.mediaType = availableMediaTypes[0];
339
403
  }
340
- contentNegotiationResult.mediaType = mediaTypeNegotiated;
341
- } else {
342
- contentNegotiationResult.mediaType = availableMediaTypes[0];
343
404
  }
344
- }
345
- const { availableLanguages } = route;
346
- if (availableLanguages.length) {
347
- fetchSecondArg.injectResponseHeader("vary", "accept-language");
348
- if (request.headers["accept-language"]) {
349
- const languageNegotiated = pickContentLanguage(
350
- request,
351
- availableLanguages,
352
- );
353
- if (!languageNegotiated) {
354
- for (const availableLanguage of availableLanguages) {
355
- wouldHaveMatched.responseLanguageSet.add(availableLanguage);
405
+ const { availableLanguages } = route;
406
+ if (availableLanguages.length) {
407
+ fetchSecondArg.injectResponseHeader("vary", "accept-language");
408
+ if (request.headers["accept-language"]) {
409
+ const languageNegotiated = pickContentLanguage(
410
+ request,
411
+ availableLanguages,
412
+ );
413
+ if (!languageNegotiated) {
414
+ for (const availableLanguage of availableLanguages) {
415
+ wouldHaveMatched.responseLanguageSet.add(availableLanguage);
416
+ }
417
+ hasFailed = true;
356
418
  }
357
- hasFailed = true;
419
+ contentNegotiationResult.language = languageNegotiated;
420
+ } else {
421
+ contentNegotiationResult.language = availableLanguages[0];
358
422
  }
359
- contentNegotiationResult.language = languageNegotiated;
360
- } else {
361
- contentNegotiationResult.language = availableLanguages[0];
362
423
  }
363
- }
364
- const { availableVersions } = route;
365
- if (availableVersions.length) {
366
- fetchSecondArg.injectResponseHeader("vary", "accept-version");
367
- if (request.headers["accept-version"]) {
368
- const versionNegotiated = pickContentVersion(
369
- request,
370
- availableVersions,
371
- );
372
- if (!versionNegotiated) {
373
- for (const availableVersion of availableVersions) {
374
- wouldHaveMatched.responseVersionSet.add(availableVersion);
424
+ const { availableVersions } = route;
425
+ if (availableVersions.length) {
426
+ fetchSecondArg.injectResponseHeader("vary", "accept-version");
427
+ if (request.headers["accept-version"]) {
428
+ const versionNegotiated = pickContentVersion(
429
+ request,
430
+ availableVersions,
431
+ );
432
+ if (!versionNegotiated) {
433
+ for (const availableVersion of availableVersions) {
434
+ wouldHaveMatched.responseVersionSet.add(availableVersion);
435
+ }
436
+ hasFailed = true;
375
437
  }
376
- hasFailed = true;
438
+ contentNegotiationResult.version = versionNegotiated;
439
+ } else {
440
+ contentNegotiationResult.version = availableVersions[0];
377
441
  }
378
- contentNegotiationResult.version = versionNegotiated;
379
- } else {
380
- contentNegotiationResult.version = availableVersions[0];
381
442
  }
382
- }
383
- const { availableEncodings } = route;
384
- if (availableEncodings.length) {
385
- fetchSecondArg.injectResponseHeader("vary", "accept-encoding");
386
- if (request.headers["accept-encoding"]) {
387
- const encodingNegotiated = pickContentEncoding(
388
- request,
389
- availableEncodings,
390
- );
391
- if (!encodingNegotiated) {
392
- for (const availableEncoding of availableEncodings) {
393
- wouldHaveMatched.responseEncodingSet.add(availableEncoding);
443
+ const { availableEncodings } = route;
444
+ if (availableEncodings.length) {
445
+ fetchSecondArg.injectResponseHeader("vary", "accept-encoding");
446
+ if (request.headers["accept-encoding"]) {
447
+ const encodingNegotiated = pickContentEncoding(
448
+ request,
449
+ availableEncodings,
450
+ );
451
+ if (!encodingNegotiated) {
452
+ for (const availableEncoding of availableEncodings) {
453
+ wouldHaveMatched.responseEncodingSet.add(availableEncoding);
454
+ }
455
+ hasFailed = true;
394
456
  }
395
- hasFailed = true;
457
+ contentNegotiationResult.encoding = encodingNegotiated;
458
+ } else {
459
+ contentNegotiationResult.encoding = availableEncodings[0];
396
460
  }
397
- contentNegotiationResult.encoding = encodingNegotiated;
398
- } else {
399
- contentNegotiationResult.encoding = availableEncodings[0];
400
461
  }
462
+ if (hasFailed) {
463
+ continue;
464
+ }
465
+ }
466
+ const { named, stars = [] } = PATTERN.composeTwoMatchResults(
467
+ resourceMatchResult,
468
+ headersMatchResult,
469
+ );
470
+ Object.assign(request.params, named, stars);
471
+ fetchSecondArg.contentNegotiation = contentNegotiationResult;
472
+ let fetchReturnValue = route.fetch(request, fetchSecondArg);
473
+ if (
474
+ fetchReturnValue !== null &&
475
+ typeof fetchReturnValue === "object" &&
476
+ typeof fetchReturnValue.then === "function"
477
+ ) {
478
+ fetchReturnValue = await fetchReturnValue;
401
479
  }
402
- if (hasFailed) {
480
+ // route decided not to handle in the end
481
+ if (fetchReturnValue === null || fetchReturnValue === undefined) {
403
482
  continue;
404
483
  }
484
+ return resolveResponse(fetchReturnValue, { request, route });
405
485
  }
406
- const { named, stars = [] } = PATTERN.composeTwoMatchResults(
407
- resourceMatchResult,
408
- headersMatchResult,
409
- );
410
- Object.assign(request.params, named, stars);
411
- fetchSecondArg.contentNegotiation = contentNegotiationResult;
412
- let fetchReturnValue = route.fetch(request, fetchSecondArg);
413
- if (
414
- fetchReturnValue !== null &&
415
- typeof fetchReturnValue === "object" &&
416
- typeof fetchReturnValue.then === "function"
417
- ) {
418
- fetchReturnValue = await fetchReturnValue;
486
+ // nothing has matched fully
487
+ // if nothing matches at all we'll send 404
488
+ // but if url matched but METHOD was not supported we send 405
489
+ if (wouldHaveMatched.methodSet.size) {
490
+ return createMethodNotAllowedResponse(request, {
491
+ allowedMethods: [...wouldHaveMatched.methodSet],
492
+ });
419
493
  }
420
- // route decided not to handle in the end
421
- if (fetchReturnValue === null || fetchReturnValue === undefined) {
422
- continue;
494
+ if (wouldHaveMatched.requestMediaTypeSet.size) {
495
+ return createUnsupportedMediaTypeResponse(request, {
496
+ acceptedMediaTypes: [...wouldHaveMatched.requestMediaTypeSet],
497
+ });
423
498
  }
424
- onRouteMatch(route);
425
- if (fetchReturnValue instanceof Response) {
426
- const headers = Object.fromEntries(fetchReturnValue.headers);
427
- onResponseHeaders(request, route, headers);
428
- return {
429
- status: fetchReturnValue.status,
430
- statusText: fetchReturnValue.statusText,
431
- headers,
432
- body: fetchReturnValue.body,
433
- };
499
+ if (
500
+ wouldHaveMatched.responseMediaTypeSet.size ||
501
+ wouldHaveMatched.responseLanguageSet.size ||
502
+ wouldHaveMatched.responseVersionSet.size ||
503
+ wouldHaveMatched.responseEncodingSet.size
504
+ ) {
505
+ return createNotAcceptableResponse(request, {
506
+ availableMediaTypes: [...wouldHaveMatched.responseMediaTypeSet],
507
+ availableLanguages: [...wouldHaveMatched.responseLanguageSet],
508
+ availableVersions: [...wouldHaveMatched.responseVersionSet],
509
+ availableEncodings: [...wouldHaveMatched.responseEncodingSet],
510
+ });
434
511
  }
435
- if (fetchReturnValue !== null && typeof fetchReturnValue === "object") {
436
- const headers = fetchReturnValue.headers || {};
437
- onResponseHeaders(request, route, headers);
512
+ if (wouldHaveMatched.upgrade) {
438
513
  return {
439
- status: fetchReturnValue.status || 404,
440
- statusText: fetchReturnValue.statusText,
441
- statusMessage: fetchReturnValue.statusMessage,
442
- headers,
443
- body: fetchReturnValue.body,
514
+ status: 426,
515
+ statusText: "Upgrade Required",
516
+ statusMessage: `The request requires the upgrade to a webSocket connection`,
444
517
  };
445
518
  }
446
- throw new TypeError(
447
- `response must be a Response, or an Object, received ${fetchReturnValue}`,
519
+ const availableEndpoints = constructAvailableEndpoints(
520
+ request,
521
+ fetchSecondArg,
448
522
  );
449
- }
450
- // nothing has matched fully
451
- // if nothing matches at all we'll send 404
452
- // but if url matched but METHOD was not supported we send 405
453
- if (wouldHaveMatched.methodSet.size) {
454
- return createMethodNotAllowedResponse(request, {
455
- allowedMethods: [...wouldHaveMatched.methodSet],
456
- });
457
- }
458
- if (wouldHaveMatched.requestMediaTypeSet.size) {
459
- return createUnsupportedMediaTypeResponse(request, {
460
- acceptedMediaTypes: [...wouldHaveMatched.requestMediaTypeSet],
461
- });
462
- }
463
- if (
464
- wouldHaveMatched.responseMediaTypeSet.size ||
465
- wouldHaveMatched.responseLanguageSet.size ||
466
- wouldHaveMatched.responseVersionSet.size ||
467
- wouldHaveMatched.responseEncodingSet.size
468
- ) {
469
- return createNotAcceptableResponse(request, {
470
- availableMediaTypes: [...wouldHaveMatched.responseMediaTypeSet],
471
- availableLanguages: [...wouldHaveMatched.responseLanguageSet],
472
- availableVersions: [...wouldHaveMatched.responseVersionSet],
473
- availableEncodings: [...wouldHaveMatched.responseEncodingSet],
474
- });
475
- }
476
- if (wouldHaveMatched.upgrade) {
477
- return {
478
- status: 426,
479
- statusText: "Upgrade Required",
480
- statusMessage: `The request requires the upgrade to a webSocket connection`,
481
- };
482
- }
483
- const availableEndpoints = constructAvailableEndpoints(
484
- request,
485
- fetchSecondArg,
486
- );
487
- return createRouteNotFoundResponse(request, { availableEndpoints });
523
+ return createRouteNotFoundResponse(request, { availableEndpoints });
524
+ }; // end matchRoutes
525
+
526
+ return matchRoutes(request);
488
527
  };
489
- const inspect = () => {
528
+ const inspect = ({ canExposeSensitiveData }) => {
490
529
  // I want all the info I can gather about the routes
491
530
  const data = [];
492
531
  for (const route of routeSet) {
493
- data.push(route.toJSON());
532
+ data.push(route.toJSON({ canExposeSensitiveData }));
494
533
  }
495
534
  return data;
496
535
  };
@@ -615,10 +654,10 @@ const createRoute = ({
615
654
  toString: () => {
616
655
  return `${method} ${resource}`;
617
656
  },
618
- toJSON: () => {
657
+ toJSON: ({ canExposeSensitiveData }) => {
619
658
  const meta = {};
620
659
 
621
- if (declarationSource) {
660
+ if (declarationSource && canExposeSensitiveData) {
622
661
  meta.declarationLink = {
623
662
  url: `javascript:window.fetch("/.internal/open_file/${encodeURIComponent(declarationSource)}")`,
624
663
  text: declarationSource,
@@ -646,7 +685,6 @@ const createRoute = ({
646
685
  } catch {}
647
686
  }
648
687
  }
649
-
650
688
  return {
651
689
  method,
652
690
  resource,
@@ -663,7 +701,9 @@ const createRoute = ({
663
701
  : typeof clientCodeExample === "string"
664
702
  ? clientCodeExample
665
703
  : undefined,
666
- declarationSource,
704
+ declarationSource: canExposeSensitiveData
705
+ ? declarationSource
706
+ : undefined,
667
707
  meta,
668
708
  };
669
709
  },
@@ -675,6 +715,9 @@ const createRoute = ({
675
715
  return route;
676
716
  };
677
717
 
718
+ const REDIRECT_STATUSES = new Set([301, 302, 303, 307, 308]);
719
+ const isRedirectStatus = (status) => REDIRECT_STATUSES.has(status);
720
+
678
721
  const isRequestBodyMediaTypeSupported = (request, { acceptedMediaTypes }) => {
679
722
  const requestBodyContentType = request.headers["content-type"];
680
723
  if (!requestBodyContentType) {
@@ -79,6 +79,12 @@ export const startServer = async ({
79
79
  keepProcessAlive = true,
80
80
  routes = [],
81
81
  plugins = [],
82
+ // When enabled, the server gives more power:
83
+ // - each route show the source where it was declared
84
+ // - server can be requested to open a file on the machine
85
+ // - client can subscribe to server to detect when it restarts
86
+ // This param should be enabled ONLY during development on your machine
87
+ canExposeSensitiveData = false,
82
88
  nagle = true,
83
89
  serverTiming = false,
84
90
  requestWaitingMs = 0,
@@ -153,11 +159,11 @@ export const startServer = async ({
153
159
  }
154
160
 
155
161
  plugins = [
156
- serverPluginOpenFile(),
162
+ ...(canExposeSensitiveData ? [serverPluginOpenFile()] : []),
157
163
  serverPluginDefaultBody4xx5xx(),
158
- serverPluginRouteInspector(),
159
- serverPluginInternalClientFiles(),
160
- serverPluginAutoreloadOnRestart(),
164
+ serverPluginRouteInspector({ canExposeSensitiveData }),
165
+ ...(canExposeSensitiveData ? [serverPluginInternalClientFiles()] : []),
166
+ ...(canExposeSensitiveData ? [serverPluginAutoreloadOnRestart()] : []),
161
167
  ...plugins,
162
168
  ];
163
169