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