@sitecore-jss/sitecore-jss-proxy 22.3.0 → 22.3.1-canary.2

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.
Files changed (49) hide show
  1. package/README.md +1 -1
  2. package/dist/cjs/index.js +19 -447
  3. package/dist/cjs/middleware/editing/config.js +27 -0
  4. package/dist/cjs/middleware/editing/index.js +89 -0
  5. package/dist/cjs/middleware/editing/render.js +103 -0
  6. package/dist/cjs/middleware/headless-ssr-proxy/index.js +450 -0
  7. package/dist/cjs/middleware/healthcheck/index.js +16 -0
  8. package/dist/cjs/middleware/index.js +32 -0
  9. package/dist/cjs/personalize/PersonalizeHelper.js +243 -0
  10. package/dist/cjs/personalize/index.js +5 -0
  11. package/dist/cjs/personalize/test-data/personalizeData.js +86 -0
  12. package/dist/cjs/types/personalize.js +2 -0
  13. package/dist/esm/index.js +4 -441
  14. package/dist/esm/middleware/editing/config.js +23 -0
  15. package/dist/esm/middleware/editing/index.js +84 -0
  16. package/dist/esm/middleware/editing/render.js +98 -0
  17. package/dist/esm/middleware/headless-ssr-proxy/index.js +441 -0
  18. package/dist/esm/middleware/healthcheck/index.js +12 -0
  19. package/dist/esm/middleware/index.js +4 -0
  20. package/dist/esm/personalize/PersonalizeHelper.js +236 -0
  21. package/dist/esm/personalize/index.js +1 -0
  22. package/dist/esm/personalize/test-data/personalizeData.js +82 -0
  23. package/dist/esm/types/personalize.js +1 -0
  24. package/package.json +22 -9
  25. package/types/index.d.ts +4 -21
  26. package/types/middleware/editing/config.d.ts +27 -0
  27. package/types/middleware/editing/index.d.ts +32 -0
  28. package/types/middleware/editing/render.d.ts +42 -0
  29. package/types/{ProxyConfig.d.ts → middleware/headless-ssr-proxy/ProxyConfig.d.ts} +2 -5
  30. package/types/middleware/headless-ssr-proxy/index.d.ts +20 -0
  31. package/types/middleware/healthcheck/index.d.ts +6 -0
  32. package/types/middleware/index.d.ts +3 -0
  33. package/types/personalize/PersonalizeHelper.d.ts +43 -0
  34. package/types/personalize/index.d.ts +2 -0
  35. package/types/personalize/test-data/personalizeData.d.ts +59 -0
  36. package/types/types/AppRenderer.d.ts +35 -0
  37. package/types/types/index.d.ts +3 -0
  38. package/types/types/personalize.d.ts +85 -0
  39. package/types/AppRenderer.d.ts +0 -10
  40. package/types/RenderResponse.d.ts +0 -15
  41. /package/dist/cjs/{ProxyConfig.js → middleware/headless-ssr-proxy/ProxyConfig.js} +0 -0
  42. /package/dist/cjs/{AppRenderer.js → types/AppRenderer.js} +0 -0
  43. /package/dist/cjs/{RouteUrlParser.js → types/RouteUrlParser.js} +0 -0
  44. /package/dist/cjs/{RenderResponse.js → types/index.js} +0 -0
  45. /package/dist/esm/{ProxyConfig.js → middleware/headless-ssr-proxy/ProxyConfig.js} +0 -0
  46. /package/dist/esm/{AppRenderer.js → types/AppRenderer.js} +0 -0
  47. /package/dist/esm/{RouteUrlParser.js → types/RouteUrlParser.js} +0 -0
  48. /package/dist/esm/{RenderResponse.js → types/index.js} +0 -0
  49. /package/types/{RouteUrlParser.d.ts → types/RouteUrlParser.d.ts} +0 -0
@@ -0,0 +1,103 @@
1
+ "use strict";
2
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
3
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
4
+ return new (P || (P = Promise))(function (resolve, reject) {
5
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
6
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
7
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
8
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
9
+ });
10
+ };
11
+ Object.defineProperty(exports, "__esModule", { value: true });
12
+ exports.getSCPHeader = exports.editingRenderMiddleware = void 0;
13
+ const sitecore_jss_1 = require("@sitecore-jss/sitecore-jss");
14
+ const utils_1 = require("@sitecore-jss/sitecore-jss/utils");
15
+ const editing_1 = require("@sitecore-jss/sitecore-jss/editing");
16
+ const personalize_1 = require("@sitecore-jss/sitecore-jss/personalize");
17
+ /**
18
+ * Middleware to handle editing render requests
19
+ * @param {EditingRenderEndpointOptions} config for the endpoint
20
+ */
21
+ const editingRenderMiddleware = (config) => (req, res) => __awaiter(void 0, void 0, void 0, function* () {
22
+ var _a;
23
+ try {
24
+ sitecore_jss_1.debug.editing('editing render middleware start');
25
+ const startTimestamp = Date.now();
26
+ const query = req.query;
27
+ const requiredQueryParams = [
28
+ 'sc_site',
29
+ 'sc_itemid',
30
+ 'sc_lang',
31
+ 'route',
32
+ 'mode',
33
+ ];
34
+ const missingQueryParams = requiredQueryParams.filter((param) => !query[param]);
35
+ // Validate query parameters
36
+ if (missingQueryParams.length) {
37
+ sitecore_jss_1.debug.editing('missing required query parameters: %o', missingQueryParams);
38
+ res.status(400).send(`Missing required query parameters: ${missingQueryParams.join(', ')}`);
39
+ return;
40
+ }
41
+ const graphQLEditingService = new editing_1.GraphQLEditingService({
42
+ clientFactory: config.clientFactory,
43
+ });
44
+ const data = yield graphQLEditingService.fetchEditingData({
45
+ siteName: query.sc_site,
46
+ itemId: query.sc_itemid,
47
+ language: query.sc_lang,
48
+ version: query.sc_version,
49
+ layoutKind: query.sc_layoutKind,
50
+ });
51
+ if (!data || !data.layoutData || !data.dictionary) {
52
+ throw new Error(`Unable to fetch editing data for ${JSON.stringify(query)}`);
53
+ }
54
+ const variantIds = ((_a = query.sc_variant) === null || _a === void 0 ? void 0 : _a.split(',')) || [personalize_1.DEFAULT_VARIANT];
55
+ const personalizeData = (0, personalize_1.getGroomedVariantIds)(variantIds);
56
+ (0, personalize_1.personalizeLayout)(data.layoutData, personalizeData.variantId, personalizeData.componentVariantIds);
57
+ const viewBag = { dictionary: data.dictionary };
58
+ config.renderView((err, result) => {
59
+ if (err) {
60
+ handleError(res, err);
61
+ return;
62
+ }
63
+ if (!result) {
64
+ sitecore_jss_1.debug.editing('editing render middleware end in %dms: %o', Date.now() - startTimestamp, {
65
+ status: 204,
66
+ route: query.route,
67
+ });
68
+ res.status(204).send();
69
+ return;
70
+ }
71
+ const statusCode = data.layoutData.sitecore.route ? 200 : 404;
72
+ // Restrict the page to be rendered only within the allowed origins
73
+ res.setHeader('Content-Security-Policy', (0, exports.getSCPHeader)());
74
+ sitecore_jss_1.debug.editing('editing render middleware end in %dms: %o', Date.now() - startTimestamp, {
75
+ status: statusCode,
76
+ route: query.route,
77
+ });
78
+ res.status(statusCode).send(result.html);
79
+ }, query.route, data.layoutData, viewBag);
80
+ }
81
+ catch (err) {
82
+ handleError(res, err);
83
+ return;
84
+ }
85
+ });
86
+ exports.editingRenderMiddleware = editingRenderMiddleware;
87
+ /**
88
+ * Gets the Content-Security-Policy header value
89
+ * @returns {string} Content-Security-Policy header value
90
+ */
91
+ const getSCPHeader = () => {
92
+ return `frame-ancestors 'self' ${[...(0, utils_1.getAllowedOriginsFromEnv)(), ...editing_1.EDITING_ALLOWED_ORIGINS].join(' ')}`;
93
+ };
94
+ exports.getSCPHeader = getSCPHeader;
95
+ /**
96
+ * Handle unexpected error
97
+ * @param {Response} res server response
98
+ * @param {Error} err error
99
+ */
100
+ const handleError = (res, err) => {
101
+ sitecore_jss_1.debug.editing('response error %o', err);
102
+ res.status(500).send('Internal Server Error');
103
+ };
@@ -0,0 +1,450 @@
1
+ "use strict";
2
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
3
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
4
+ return new (P || (P = Promise))(function (resolve, reject) {
5
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
6
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
7
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
8
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
9
+ });
10
+ };
11
+ var __importDefault = (this && this.__importDefault) || function (mod) {
12
+ return (mod && mod.__esModule) ? mod : { "default": mod };
13
+ };
14
+ Object.defineProperty(exports, "__esModule", { value: true });
15
+ exports.removeEmptyAnalyticsCookie = void 0;
16
+ exports.rewriteRequestPath = rewriteRequestPath;
17
+ exports.middleware = middleware;
18
+ const http_proxy_middleware_1 = require("http-proxy-middleware");
19
+ const http_status_codes_1 = __importDefault(require("http-status-codes"));
20
+ const set_cookie_parser_1 = __importDefault(require("set-cookie-parser"));
21
+ const zlib_1 = __importDefault(require("zlib")); // node.js standard lib
22
+ const util_1 = require("../../util");
23
+ // For some reason, every other response returned by Sitecore contains the 'set-cookie' header with the SC_ANALYTICS_GLOBAL_COOKIE value as an empty string.
24
+ // This effectively sets the cookie to empty on the client as well, so if a user were to close their browser
25
+ // after one of these 'empty value' responses, they would not be tracked as a returning visitor after re-opening their browser.
26
+ // To address this, we simply parse the response cookies and if the analytics cookie is present but has an empty value, then we
27
+ // remove it from the response header. This means the existing cookie in the browser remains intact.
28
+ const removeEmptyAnalyticsCookie = (proxyResponse) => {
29
+ const cookies = set_cookie_parser_1.default.parse(proxyResponse.headers['set-cookie']);
30
+ if (cookies) {
31
+ const analyticsCookieIndex = cookies.findIndex((c) => c.name === 'SC_ANALYTICS_GLOBAL_COOKIE');
32
+ if (analyticsCookieIndex !== -1) {
33
+ const analyticsCookie = cookies[analyticsCookieIndex];
34
+ if (analyticsCookie && analyticsCookie.value === '') {
35
+ cookies.splice(analyticsCookieIndex, 1);
36
+ /* eslint-disable no-param-reassign */
37
+ proxyResponse.headers['set-cookie'] = cookies;
38
+ /* eslint-enable no-param-reassign */
39
+ }
40
+ }
41
+ }
42
+ };
43
+ exports.removeEmptyAnalyticsCookie = removeEmptyAnalyticsCookie;
44
+ // inspired by: http://stackoverflow.com/a/22487927/9324
45
+ /**
46
+ * @param {IncomingMessage} proxyResponse
47
+ * @param {IncomingMessage} request
48
+ * @param {ServerResponse} serverResponse
49
+ * @param {AppRenderer} renderer
50
+ * @param {ProxyConfig} config
51
+ */
52
+ function renderAppToResponse(proxyResponse, request, serverResponse, renderer, config) {
53
+ return __awaiter(this, void 0, void 0, function* () {
54
+ // monkey-patch FTW?
55
+ const originalWriteHead = serverResponse.writeHead;
56
+ const originalWrite = serverResponse.write;
57
+ const originalEnd = serverResponse.end;
58
+ // these lines are necessary and must happen before we do any writing to the response
59
+ // otherwise the headers will have already been sent
60
+ delete proxyResponse.headers['content-length'];
61
+ proxyResponse.headers['content-type'] = 'text/html; charset=utf-8';
62
+ // remove IIS server header for security
63
+ delete proxyResponse.headers.server;
64
+ if (config.setHeaders) {
65
+ config.setHeaders(request, serverResponse, proxyResponse);
66
+ }
67
+ const contentEncoding = proxyResponse.headers['content-encoding'];
68
+ if (contentEncoding &&
69
+ (contentEncoding.indexOf('gzip') !== -1 || contentEncoding.indexOf('deflate') !== -1)) {
70
+ delete proxyResponse.headers['content-encoding'];
71
+ }
72
+ // we are going to set our own status code if rendering fails
73
+ serverResponse.writeHead = () => serverResponse;
74
+ // buffer the response body as it is written for later processing
75
+ let buf = Buffer.from('');
76
+ serverResponse.write = (data, encoding) => {
77
+ if (Buffer.isBuffer(data)) {
78
+ buf = Buffer.concat([buf, data]); // append raw buffer
79
+ }
80
+ else {
81
+ buf = Buffer.concat([buf, Buffer.from(data, encoding)]); // append string with optional character encoding (default utf8)
82
+ }
83
+ // sanity check: if the response is huge, bail.
84
+ // ...we don't want to let someone bring down the server by filling up all our RAM.
85
+ if (buf.length > config.maxResponseSizeBytes) {
86
+ throw new Error('Document too large');
87
+ }
88
+ return true;
89
+ };
90
+ /**
91
+ * Extract layout service data from proxy response
92
+ */
93
+ function extractLayoutServiceDataFromProxyResponse() {
94
+ return __awaiter(this, void 0, void 0, function* () {
95
+ if (proxyResponse.statusCode === http_status_codes_1.default.OK ||
96
+ proxyResponse.statusCode === http_status_codes_1.default.NOT_FOUND) {
97
+ let responseString;
98
+ if (contentEncoding &&
99
+ (contentEncoding.indexOf('gzip') !== -1 || contentEncoding.indexOf('deflate') !== -1)) {
100
+ responseString = new Promise((resolve, reject) => {
101
+ if (config.debug) {
102
+ console.log('Layout service response is compressed; decompressing.');
103
+ }
104
+ zlib_1.default.unzip(buf, (error, result) => {
105
+ if (error) {
106
+ reject(error);
107
+ }
108
+ if (result) {
109
+ resolve(result.toString('utf-8'));
110
+ }
111
+ });
112
+ });
113
+ }
114
+ else {
115
+ responseString = Promise.resolve(buf.toString('utf-8'));
116
+ }
117
+ return responseString.then(util_1.tryParseJson);
118
+ }
119
+ return Promise.resolve(null);
120
+ });
121
+ }
122
+ /**
123
+ * function replies with HTTP 500 when an error occurs
124
+ * @param {Error} error
125
+ */
126
+ function replyWithError(error) {
127
+ return __awaiter(this, void 0, void 0, function* () {
128
+ console.error(error);
129
+ let errorResponse = {
130
+ statusCode: proxyResponse.statusCode || http_status_codes_1.default.INTERNAL_SERVER_ERROR,
131
+ content: proxyResponse.statusMessage || 'Internal Server Error',
132
+ headers: {},
133
+ };
134
+ if (config.onError) {
135
+ const onError = yield config.onError(error, proxyResponse);
136
+ errorResponse = Object.assign(Object.assign({}, errorResponse), onError);
137
+ }
138
+ completeProxyResponse(Buffer.from(errorResponse.content), errorResponse.statusCode, errorResponse.headers);
139
+ });
140
+ }
141
+ // callback handles the result of server-side rendering
142
+ /**
143
+ * @param {Error | null} error
144
+ * @param {RenderResponse} result
145
+ */
146
+ function handleRenderingResult(error, result) {
147
+ return __awaiter(this, void 0, void 0, function* () {
148
+ if (!error && !result) {
149
+ return replyWithError(new Error('Render function did not return a result or an error!'));
150
+ }
151
+ if (error) {
152
+ return replyWithError(error);
153
+ }
154
+ if (!result) {
155
+ // should not occur, but makes TS happy
156
+ return replyWithError(new Error('Render function result did not return a result.'));
157
+ }
158
+ if (!result.html) {
159
+ return replyWithError(new Error('Render function result was returned but html property was falsy.'));
160
+ }
161
+ if (config.transformSSRContent) {
162
+ result.html = yield config.transformSSRContent(result, request, serverResponse);
163
+ }
164
+ // we have to convert back to a buffer so that we can get the *byte count* (rather than character count) of the body
165
+ let content = Buffer.from(result.html);
166
+ // setting the content-length header is not absolutely necessary, but is recommended
167
+ proxyResponse.headers['content-length'] = content.length.toString(10);
168
+ // if original request was a HEAD, we should not return a response body
169
+ if (request.method === 'HEAD') {
170
+ if (config.debug) {
171
+ console.log('DEBUG: Original request method was HEAD, clearing response body');
172
+ }
173
+ content = Buffer.from([]);
174
+ }
175
+ if (result.redirect) {
176
+ if (!result.status) {
177
+ result.status = 302;
178
+ }
179
+ proxyResponse.headers.location = result.redirect;
180
+ }
181
+ const finalStatusCode = result.status || proxyResponse.statusCode || http_status_codes_1.default.OK;
182
+ if (config.debug) {
183
+ console.log('DEBUG: FINAL response headers for client', JSON.stringify(proxyResponse.headers, null, 2));
184
+ console.log('DEBUG: FINAL status code for client', finalStatusCode);
185
+ }
186
+ completeProxyResponse(content, finalStatusCode);
187
+ });
188
+ }
189
+ /**
190
+ * @param {Buffer | null} content
191
+ * @param {number} statusCode
192
+ * @param {IncomingHttpHeaders} [headers]
193
+ */
194
+ function completeProxyResponse(content, statusCode, headers) {
195
+ if (!headers) {
196
+ headers = proxyResponse.headers;
197
+ }
198
+ originalWriteHead.apply(serverResponse, [statusCode, headers]);
199
+ if (content)
200
+ originalWrite.call(serverResponse, content);
201
+ originalEnd.call(serverResponse);
202
+ }
203
+ /**
204
+ * @param {object} layoutServiceData
205
+ */
206
+ function createViewBag(layoutServiceData) {
207
+ return __awaiter(this, void 0, void 0, function* () {
208
+ let viewBag = {
209
+ statusCode: proxyResponse.statusCode,
210
+ dictionary: {},
211
+ };
212
+ if (config.createViewBag) {
213
+ const customViewBag = yield config.createViewBag(request, serverResponse, proxyResponse, layoutServiceData);
214
+ viewBag = Object.assign(Object.assign({}, viewBag), customViewBag);
215
+ }
216
+ return viewBag;
217
+ });
218
+ }
219
+ // as the response is ending, we parse the current response body which is JSON, then
220
+ // render the app using that JSON, but return HTML to the final response.
221
+ // eslint-disable-next-line @typescript-eslint/ban-ts-comment
222
+ // @ts-ignore
223
+ serverResponse.end = () => __awaiter(this, void 0, void 0, function* () {
224
+ try {
225
+ const layoutServiceData = yield extractLayoutServiceDataFromProxyResponse();
226
+ const viewBag = yield createViewBag(layoutServiceData);
227
+ if (!layoutServiceData) {
228
+ throw new Error(`Received invalid response ${proxyResponse.statusCode} ${proxyResponse.statusMessage}`);
229
+ }
230
+ return renderer(handleRenderingResult,
231
+ // originalUrl not defined in `http-proxy-middleware` types but it exists
232
+ request.originalUrl, layoutServiceData, viewBag);
233
+ }
234
+ catch (error) {
235
+ return replyWithError(error);
236
+ }
237
+ });
238
+ });
239
+ }
240
+ /**
241
+ * @param {IncomingMessage} proxyResponse
242
+ * @param {Request} request
243
+ * @param {Response} serverResponse
244
+ * @param {AppRenderer} renderer
245
+ * @param {ProxyConfig} config
246
+ */
247
+ function handleProxyResponse(proxyResponse, request, serverResponse, renderer, config) {
248
+ (0, exports.removeEmptyAnalyticsCookie)(proxyResponse);
249
+ if (config.debug) {
250
+ console.log('DEBUG: request url', request.url);
251
+ console.log('DEBUG: request query', request.query);
252
+ console.log('DEBUG: request original url', request.originalUrl);
253
+ console.log('DEBUG: proxied request response code', proxyResponse.statusCode);
254
+ console.log('DEBUG: RAW request headers', JSON.stringify(request.headers, null, 2));
255
+ console.log('DEBUG: RAW headers from the proxied response', JSON.stringify(proxyResponse.headers, null, 2));
256
+ }
257
+ // if the request URL contains any of the excluded rewrite routes, we assume the response does not need to be server rendered.
258
+ // instead, the response should just be relayed as usual.
259
+ if (isUrlIgnored(request.originalUrl, config, true)) {
260
+ return Promise.resolve(undefined);
261
+ }
262
+ // your first thought might be: why do we need to render the app here? why not just pass the JSON response to another piece of middleware that will render the app?
263
+ // the answer: the proxy middleware ends the response and does not "chain"
264
+ return renderAppToResponse(proxyResponse, request, serverResponse, renderer, config);
265
+ }
266
+ /**
267
+ * @param {string} reqPath
268
+ * @param {Request} req
269
+ * @param {ProxyConfig} config
270
+ * @param {RouteUrlParser} parseRouteUrl
271
+ */
272
+ function rewriteRequestPath(reqPath, req, config, parseRouteUrl) {
273
+ // the path comes in URL-encoded by default,
274
+ // but we don't want that because...
275
+ // 1. We need to URL-encode it before we send it out to the Layout Service, if it matches a route
276
+ // 2. We don't want to force people to URL-encode ignored routes, etc (just use spaces instead of %20, etc)
277
+ const decodedReqPath = decodeURIComponent(reqPath);
278
+ // if the request URL contains a path/route that should not be re-written, then just pass it along as-is
279
+ if (isUrlIgnored(reqPath, config)) {
280
+ // we do not return the decoded URL because we're using it verbatim - should be encoded.
281
+ return reqPath;
282
+ }
283
+ // if the request URL doesn't contain the layout service controller path, assume we need to rewrite the request URL so that it does
284
+ // if this seems redundant, it is. the config.pathRewriteExcludeRoutes should contain the layout service path, but can't always assume that it will...
285
+ if (decodedReqPath.indexOf(config.layoutServiceRoute) !== -1) {
286
+ return reqPath;
287
+ }
288
+ let finalReqPath = decodedReqPath;
289
+ const qsIndex = finalReqPath.indexOf('?');
290
+ let qs = '';
291
+ if (qsIndex > -1 || Object.keys(req.query).length) {
292
+ qs = (0, util_1.buildQueryString)(req.query);
293
+ // Splice qs part when url contains that
294
+ if (qsIndex > -1)
295
+ finalReqPath = finalReqPath.slice(0, qsIndex);
296
+ }
297
+ if (config.qsParams) {
298
+ if (qs) {
299
+ qs += '&';
300
+ }
301
+ qs += `${config.qsParams}`;
302
+ }
303
+ let lang;
304
+ if (parseRouteUrl) {
305
+ if (config.debug) {
306
+ console.log(`DEBUG: Parsing route URL using ${decodedReqPath} URL...`);
307
+ }
308
+ const routeParams = parseRouteUrl(finalReqPath);
309
+ if (routeParams) {
310
+ if (routeParams.sitecoreRoute) {
311
+ finalReqPath = routeParams.sitecoreRoute;
312
+ }
313
+ else {
314
+ finalReqPath = '/';
315
+ }
316
+ if (!finalReqPath.startsWith('/')) {
317
+ finalReqPath = `/${finalReqPath}`;
318
+ }
319
+ lang = routeParams.lang;
320
+ if (routeParams.qsParams) {
321
+ qs += `&${routeParams.qsParams}`;
322
+ }
323
+ if (config.debug) {
324
+ console.log('DEBUG: parseRouteUrl() result', routeParams);
325
+ }
326
+ }
327
+ }
328
+ let path = `${config.layoutServiceRoute}?item=${encodeURIComponent(finalReqPath)}&sc_apikey=${config.apiKey}`;
329
+ if (lang) {
330
+ path = `${path}&sc_lang=${lang}`;
331
+ }
332
+ if (qs) {
333
+ path = `${path}&${qs}`;
334
+ }
335
+ return path;
336
+ }
337
+ /**
338
+ * @param {string} originalUrl
339
+ * @param {ProxyConfig} config
340
+ * @param {boolean} noDebug
341
+ */
342
+ function isUrlIgnored(originalUrl, config, noDebug = false) {
343
+ if (config.pathRewriteExcludePredicate && config.pathRewriteExcludeRoutes) {
344
+ console.error('ERROR: pathRewriteExcludePredicate and pathRewriteExcludeRoutes were both provided in config. Provide only one.');
345
+ process.exit(1);
346
+ }
347
+ let result = null;
348
+ if (config.pathRewriteExcludeRoutes) {
349
+ const matchRoute = decodeURIComponent(originalUrl).toUpperCase();
350
+ result = config.pathRewriteExcludeRoutes.find((excludedRoute) => excludedRoute.length > 0 && matchRoute.startsWith(excludedRoute));
351
+ if (!noDebug && config.debug) {
352
+ if (!result) {
353
+ console.log(`DEBUG: URL ${originalUrl} did not match the proxy exclude list, and will be treated as a layout service route to render. Excludes:`, config.pathRewriteExcludeRoutes);
354
+ }
355
+ else {
356
+ console.log(`DEBUG: URL ${originalUrl} matched the proxy exclude list and will be served verbatim as received. Excludes: `, config.pathRewriteExcludeRoutes);
357
+ }
358
+ }
359
+ return result ? true : false;
360
+ }
361
+ if (config.pathRewriteExcludePredicate) {
362
+ result = config.pathRewriteExcludePredicate(originalUrl);
363
+ if (!noDebug && config.debug) {
364
+ if (!result) {
365
+ console.log(`DEBUG: URL ${originalUrl} did not match the proxy exclude function, and will be treated as a layout service route to render.`);
366
+ }
367
+ else {
368
+ console.log(`DEBUG: URL ${originalUrl} matched the proxy exclude function and will be served verbatim as received.`);
369
+ }
370
+ }
371
+ return result;
372
+ }
373
+ return false;
374
+ }
375
+ /**
376
+ * @param {ClientRequest} proxyReq
377
+ * @param {Request} req
378
+ * @param {Response} res
379
+ * @param {ServerOptions} options
380
+ * @param {ProxyConfig} config
381
+ * @param {Function} customOnProxyReq
382
+ */
383
+ function handleProxyRequest(proxyReq, req, res, options, config, customOnProxyReq) {
384
+ if (!isUrlIgnored(req.originalUrl, config, true)) {
385
+ // In case 'followRedirects' is enabled, and before the proxy was initialized we had set 'originalMethod'
386
+ // now we need to set req.method back to original one, since proxyReq is already initialized.
387
+ // See more info in 'preProxyHandler'
388
+ if (options.followRedirects && req.originalMethod === 'HEAD') {
389
+ req.method = req.originalMethod;
390
+ delete req.originalMethod;
391
+ if (config.debug) {
392
+ console.log('DEBUG: Rewriting HEAD request to GET to create accurate headers');
393
+ }
394
+ }
395
+ else if (proxyReq.method === 'HEAD') {
396
+ if (config.debug) {
397
+ console.log('DEBUG: Rewriting HEAD request to GET to create accurate headers');
398
+ }
399
+ // if a HEAD request, we still need to issue a GET so we can return accurate headers
400
+ proxyReq.method = 'GET';
401
+ }
402
+ }
403
+ // invoke custom onProxyReq
404
+ if (customOnProxyReq) {
405
+ customOnProxyReq(proxyReq, req, res, options);
406
+ }
407
+ }
408
+ /**
409
+ * @param {AppRenderer} renderer
410
+ * @param {ProxyConfig} config
411
+ * @param {RouteUrlParser} parseRouteUrl
412
+ */
413
+ function createOptions(renderer, config, parseRouteUrl) {
414
+ var _a;
415
+ if (!config.maxResponseSizeBytes) {
416
+ config.maxResponseSizeBytes = 10 * 1024 * 1024;
417
+ }
418
+ // ensure all excludes are case insensitive
419
+ if (config.pathRewriteExcludeRoutes && Array.isArray(config.pathRewriteExcludeRoutes)) {
420
+ config.pathRewriteExcludeRoutes = config.pathRewriteExcludeRoutes.map((exclude) => exclude.toUpperCase());
421
+ }
422
+ if (config.debug) {
423
+ console.log('DEBUG: Final proxy config', config);
424
+ }
425
+ const customOnProxyReq = (_a = config.proxyOptions) === null || _a === void 0 ? void 0 : _a.onProxyReq;
426
+ return Object.assign(Object.assign({}, config.proxyOptions), { target: config.apiHost, changeOrigin: true, ws: config.ws || false, pathRewrite: (reqPath, req) => rewriteRequestPath(reqPath, req, config, parseRouteUrl), logLevel: config.debug ? 'debug' : 'info', onProxyReq: (proxyReq, req, res, options) => handleProxyRequest(proxyReq, req, res, options, config, customOnProxyReq), onProxyRes: (proxyRes, req, res) => handleProxyResponse(proxyRes, req, res, renderer, config) });
427
+ }
428
+ /**
429
+ * @param {AppRenderer} renderer
430
+ * @param {ProxyConfig} config
431
+ * @param {RouteUrlParser} parseRouteUrl
432
+ */
433
+ function middleware(renderer, config, parseRouteUrl) {
434
+ const options = createOptions(renderer, config, parseRouteUrl);
435
+ const preProxyHandler = (req, _res, next) => {
436
+ // When 'followRedirects' is enabled, 'onProxyReq' is executed after 'proxyReq' is initialized based on original 'req'
437
+ // and there are no public properties/methods to modify Redirectable 'proxyReq'.
438
+ // so, we need to set 'HEAD' req as 'GET' before the proxy is initialized.
439
+ // During the 'onProxyReq' event we will set 'req.method' back as 'HEAD'.
440
+ // if a HEAD request, we need to issue a GET so we can return accurate headers
441
+ if (req.method === 'HEAD' &&
442
+ options.followRedirects &&
443
+ !isUrlIgnored(req.originalUrl, config, true)) {
444
+ req.method = 'GET';
445
+ req.originalMethod = 'HEAD';
446
+ }
447
+ next();
448
+ };
449
+ return [preProxyHandler, (0, http_proxy_middleware_1.createProxyMiddleware)(options)];
450
+ }
@@ -0,0 +1,16 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.healthCheck = void 0;
4
+ const express_1 = require("express");
5
+ /**
6
+ * Creates a router for health check requests.
7
+ * @returns {Router} Editing router
8
+ */
9
+ const healthCheck = () => {
10
+ const router = (0, express_1.Router)();
11
+ router.get('/api/healthz', (_req, res) => {
12
+ res.status(200).send('Healthy');
13
+ });
14
+ return router;
15
+ };
16
+ exports.healthCheck = healthCheck;
@@ -0,0 +1,32 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || function (mod) {
19
+ if (mod && mod.__esModule) return mod;
20
+ var result = {};
21
+ if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
22
+ __setModuleDefault(result, mod);
23
+ return result;
24
+ };
25
+ Object.defineProperty(exports, "__esModule", { value: true });
26
+ exports.healthCheck = exports.editingRouter = exports.headlessProxy = void 0;
27
+ // eslint-disable-next-line prettier/prettier
28
+ exports.headlessProxy = __importStar(require("./headless-ssr-proxy"));
29
+ var editing_1 = require("./editing");
30
+ Object.defineProperty(exports, "editingRouter", { enumerable: true, get: function () { return editing_1.editingRouter; } });
31
+ var healthcheck_1 = require("./healthcheck");
32
+ Object.defineProperty(exports, "healthCheck", { enumerable: true, get: function () { return healthcheck_1.healthCheck; } });