@salesforce/pwa-kit-runtime 4.0.0-extensibility-preview.2 → 4.0.0-extensibility-preview.4

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@salesforce/pwa-kit-runtime",
3
- "version": "4.0.0-extensibility-preview.2",
3
+ "version": "4.0.0-extensibility-preview.4",
4
4
  "description": "The PWAKit Runtime",
5
5
  "homepage": "https://github.com/SalesforceCommerceCloud/pwa-kit/tree/develop/packages/pwa-kit-runtime#readme",
6
6
  "bugs": {
@@ -46,13 +46,13 @@
46
46
  },
47
47
  "devDependencies": {
48
48
  "@loadable/component": "^5.15.3",
49
- "@salesforce/pwa-kit-dev": "4.0.0-extensibility-preview.2",
50
- "@salesforce/pwa-kit-extension-sdk": "4.0.0-extensibility-preview.2",
49
+ "@salesforce/pwa-kit-dev": "4.0.0-extensibility-preview.4",
50
+ "@salesforce/pwa-kit-extension-sdk": "4.0.0-extensibility-preview.4",
51
51
  "@serverless/event-mocks": "^1.1.1",
52
52
  "@types/express": "^4.17.21",
53
53
  "aws-lambda-mock-context": "^3.2.1",
54
54
  "fs-extra": "^11.1.1",
55
- "internal-lib-build": "4.0.0-extensibility-preview.2",
55
+ "internal-lib-build": "4.0.0-extensibility-preview.4",
56
56
  "nock": "^13.3.0",
57
57
  "nodemon": "^2.0.22",
58
58
  "sinon": "^13.0.2",
@@ -60,7 +60,7 @@
60
60
  "supertest": "^4.0.2"
61
61
  },
62
62
  "peerDependencies": {
63
- "@salesforce/pwa-kit-dev": "4.0.0-extensibility-preview.2"
63
+ "@salesforce/pwa-kit-dev": "4.0.0-extensibility-preview.4"
64
64
  },
65
65
  "peerDependenciesMeta": {
66
66
  "@salesforce/pwa-kit-dev": {
@@ -68,11 +68,11 @@
68
68
  }
69
69
  },
70
70
  "engines": {
71
- "node": "^16.11.0 || ^18.0.0 || ^20.0.0",
72
- "npm": "^8.0.0 || ^9.0.0 || ^10.0.0"
71
+ "node": "^16.11.0 || ^18.0.0 || ^20.0.0 || ^22.0.0",
72
+ "npm": "^8.0.0 || ^9.0.0 || ^10.0.0 || ^11.0.0"
73
73
  },
74
74
  "publishConfig": {
75
75
  "directory": "dist"
76
76
  },
77
- "gitHead": "6b5da20f750f39d89c490cf77f1bf160c7e3d8f7"
77
+ "gitHead": "5ad3b1f0e598cfe0fbdec5b1212b4dabc0a8733c"
78
78
  }
@@ -74,6 +74,11 @@ export namespace RemoteServerFactory {
74
74
  * @private
75
75
  */
76
76
  function _addSDKInternalHandlers(app: any): void;
77
+ /**
78
+ * Set x-forward-* headers into locals, this is primarily used to facilitate react sdk hook `useOrigin`
79
+ * @private
80
+ */
81
+ function _setForwardedHeaders(app: any, options: any): void;
77
82
  /**
78
83
  * @private
79
84
  */
@@ -156,7 +161,7 @@ export namespace RemoteServerFactory {
156
161
  * @param app {Express} - an Express App
157
162
  * @private
158
163
  */
159
- function _createHandler(app: Express): {
164
+ function _createHandler(app: Express, options: any): {
160
165
  handler: (event: any, context: any, callback: any) => void;
161
166
  server: any;
162
167
  app: Express;
@@ -1 +1 @@
1
- {"version":3,"file":"build-remote-server.d.ts","sourceRoot":"","sources":["../../../src/ssr/server/build-remote-server.js"],"names":[],"mappings":"AAoEA;;;;GAIG;AACH,gDAKC;;IAWG;;OAEG;IACH,uCAgFC;IAED;;OAEG;IAEH,gDAEC;IAED;;OAEG;IAEH,iDAEC;IAED;;OAEG;IAEH,4CAEC;IAED;;OAEG;IACH,uDAEC;IAED;;OAEG;IAEH,2CAEC;IAED;;OAEG;IACH,6CAEC;IAED;;OAEG;IACH,4DAIC;IAED;;OAEG;IAEH,yCAEC;IAED;;OAEG;IAEH,uCA6CC;IAED;;;OAGG;IACH,uCAcC;IAED;;OAEG;IAEH,+CAEC;IAED;;OAEG;IACH,kDAEC;IAED;;OAEG;IACH,oDAEC;IAED;;OAEG;IACH,+EA0CC;IAED;;OAEG;IACH,sFAyDC;IAED;;OAEG;IAEH,iDAEC;IAED;;OAEG;IACH,6DAwLC;IAED;;OAEG;IAEH,sDAOC;IAED;;OAEG;IACH,yDAOC;IAED;;OAEG;IACH,oEAuEC;IAED;;OAEG;IACH,2CAIC;IAED;;OAEG;IACH,8DAaC;IAED;;OAEG;IACH,oDA0EC;IAED;;OAEG;IACH,wCAEC;IAED;;OAEG;IACH,gDAGC;IAED;;;;;;;;;;;OAWG;IACH,sDAwBC;IAED;;;;;;;OAOG;IACH,wFAKC;IAED;;OAEG;IACH,4FASC;IAED;;;;;;;;;;OAUG;IACH,6DAWC;IAED;;;;;;;OAOG;IACH;;;;MA8EC;IAED;;;;;;;;;;;;;;;;;;;;;;;;;;;OA2BG;IACH;;;;;;;;;;;;MAKC;IAED;;OAEG;IAEH,8CAEC;;AAyFE,uDASN"}
1
+ {"version":3,"file":"build-remote-server.d.ts","sourceRoot":"","sources":["../../../src/ssr/server/build-remote-server.js"],"names":[],"mappings":"AAsEA;;;;GAIG;AACH,gDAKC;;IAWG;;OAEG;IACH,uCAiFC;IAED;;OAEG;IAEH,gDAEC;IAED;;OAEG;IAEH,iDAEC;IAED;;OAEG;IAEH,4CAEC;IAED;;OAEG;IACH,uDAEC;IAED;;OAEG;IAEH,2CAEC;IAED;;OAEG;IACH,6CAEC;IAED;;OAEG;IACH,4DAIC;IAED;;OAEG;IAEH,yCAEC;IAED;;OAEG;IAEH,uCA6CC;IAED;;;OAGG;IACH,uCAcC;IAED;;OAEG;IAEH,+CAEC;IAED;;OAEG;IACH,kDAEC;IAED;;OAEG;IACH,oDAEC;IAED;;OAEG;IACH,+EA2CC;IAED;;OAEG;IACH,sFA2DC;IAED;;OAEG;IAEH,iDAEC;IAED;;;OAGG;IACH,4DAWC;IAED;;OAEG;IACH,6DAwLC;IAED;;OAEG;IAEH,sDAOC;IAED;;OAEG;IACH,yDAOC;IAED;;OAEG;IACH,oEA2EC;IAED;;OAEG;IACH,2CAIC;IAED;;OAEG;IACH,8DAiBC;IAED;;OAEG;IACH,oDA0EC;IAED;;OAEG;IACH,wCAEC;IAED;;OAEG;IACH,gDAGC;IAED;;;;;;;;;;;OAWG;IACH,sDA2CC;IAED;;;;;;;OAOG;IACH,wFAKC;IAED;;OAEG;IACH,4FASC;IAED;;;;;;;;;;OAUG;IACH,6DAWC;IAED;;;;;;;OAOG;IACH;;;;MAmGC;IAED;;;;;;;;;;;;;;;;;;;;;;;;;;;OA2BG;IACH;;;;;;;;;;;;MAKC;IAED;;OAEG;IAEH,8CAEC;;AA2HE,uDASN"}
@@ -103,9 +103,9 @@ const RemoteServerFactory = exports.RemoteServerFactory = {
103
103
  useSLASPrivateClient: false,
104
104
  // A regex for identifying which SLAS endpoints the custom SLAS private
105
105
  // client secret handler will inject an Authorization header.
106
- // Do not modify unless a project wants to customize additional SLAS
107
- // endpoints that we currently do not support (ie. /oauth2/passwordless/token)
108
- applySLASPrivateClientToEndpoints: /\/oauth2\/token/
106
+ // To allow additional SLAS endpoints, users can override this value in
107
+ // their project's ssr.js.
108
+ applySLASPrivateClientToEndpoints: /\/oauth2\/(token|passwordless\/(login|token)|password\/(reset|action))/
109
109
  };
110
110
  options = _extends({}, defaults, options);
111
111
  (0, _ssrServer.setQuiet)(options.quiet || process.env.SSR_QUIET);
@@ -297,6 +297,7 @@ const RemoteServerFactory = exports.RemoteServerFactory = {
297
297
  // want request-processors applied to development views.
298
298
  this._addSDKInternalHandlers(app);
299
299
  this._setupSSRRequestProcessorMiddleware(app);
300
+ this._setForwardedHeaders(app, options);
300
301
  this._setupLogging(app);
301
302
  this._setupMetricsFlushing(app);
302
303
  this._setupHealthcheck(app);
@@ -319,6 +320,8 @@ const RemoteServerFactory = exports.RemoteServerFactory = {
319
320
  app.disable('x-powered-by');
320
321
  const mixin = {
321
322
  options,
323
+ // Forcing a GC is no longer necessary, and will be
324
+ // skipped by default (unless FORCE_GC env-var is set).
322
325
  _collectGarbage() {
323
326
  // Do global.gc in a separate 'then' handler so
324
327
  // that all major variables are out of scope and
@@ -371,6 +374,22 @@ const RemoteServerFactory = exports.RemoteServerFactory = {
371
374
  _addSDKInternalHandlers(app) {
372
375
  // This method is used by the dev server, but is not needed here.
373
376
  },
377
+ /**
378
+ * Set x-forward-* headers into locals, this is primarily used to facilitate react sdk hook `useOrigin`
379
+ * @private
380
+ */
381
+ _setForwardedHeaders(app, options) {
382
+ app.use((req, res, next) => {
383
+ var _req$headers, _req$headers2;
384
+ const xForwardedHost = (_req$headers = req.headers) === null || _req$headers === void 0 ? void 0 : _req$headers['x-forwarded-host'];
385
+ const xForwardedProto = (_req$headers2 = req.headers) === null || _req$headers2 === void 0 ? void 0 : _req$headers2['x-forwarded-proto'];
386
+ if (xForwardedHost) {
387
+ // prettier-ignore
388
+ res.locals.xForwardedOrigin = `${xForwardedProto || options.protocol}://${xForwardedHost}`;
389
+ }
390
+ next();
391
+ });
392
+ },
374
393
  /**
375
394
  * @private
376
395
  */
@@ -584,7 +603,7 @@ const RemoteServerFactory = exports.RemoteServerFactory = {
584
603
  [_ssrNamespacePaths.slasPrivateProxyPath]: ''
585
604
  },
586
605
  onProxyReq: (proxyRequest, incomingRequest) => {
587
- var _incomingRequest$path;
606
+ var _incomingRequest$path, _incomingRequest$path2;
588
607
  (0, _configureProxy.applyProxyRequestHeaders)({
589
608
  proxyRequest,
590
609
  incomingRequest,
@@ -594,8 +613,7 @@ const RemoteServerFactory = exports.RemoteServerFactory = {
594
613
  });
595
614
 
596
615
  // We pattern match and add client secrets only to endpoints that
597
- // match the regex specified by options.applySLASPrivateClientToEndpoints.
598
- // By default, this regex matches only calls to SLAS /oauth2/token
616
+ // match the regex specified by options.applySLASPrivateClientToEndpoints
599
617
  // (see option defaults at the top of this file).
600
618
  // Other SLAS endpoints, ie. SLAS authenticate (/oauth2/login) and
601
619
  // SLAS logout (/oauth2/logout), use the Authorization header for a different
@@ -603,6 +621,11 @@ const RemoteServerFactory = exports.RemoteServerFactory = {
603
621
  if ((_incomingRequest$path = incomingRequest.path) !== null && _incomingRequest$path !== void 0 && _incomingRequest$path.match(options.applySLASPrivateClientToEndpoints)) {
604
622
  proxyRequest.setHeader('Authorization', `Basic ${encodedSlasCredentials}`);
605
623
  }
624
+
625
+ // /oauth2/trusted-agent/token endpoint requires a different auth header
626
+ if ((_incomingRequest$path2 = incomingRequest.path) !== null && _incomingRequest$path2 !== void 0 && _incomingRequest$path2.match(/\/oauth2\/trusted-agent\/token/)) {
627
+ proxyRequest.setHeader('_sfdc_client_auth', encodedSlasCredentials);
628
+ }
606
629
  },
607
630
  onProxyRes: (proxyRes, req) => {
608
631
  if (proxyRes.statusCode && proxyRes.statusCode >= 400) {
@@ -646,6 +669,9 @@ const RemoteServerFactory = exports.RemoteServerFactory = {
646
669
  // NOTE: Think about changing the name of this function to `applyApplicationExtensions`. First look into
647
670
  // what a common pattern is for application enhancement.
648
671
  (0, _express3.applyApplicationExtensions)(app);
672
+ if (options !== null && options !== void 0 && options.encodeNonAsciiHttpHeaders) {
673
+ app.use(encodeNonAsciiMiddleware);
674
+ }
649
675
  applyPatches(options);
650
676
  },
651
677
  /**
@@ -740,8 +766,27 @@ const RemoteServerFactory = exports.RemoteServerFactory = {
740
766
  encoding: 'utf8'
741
767
  });
742
768
 
769
+ // If the service worker is not updated when content security policy headers inside
770
+ // ssr.js are changed, then service worker initiated requests will continue to use
771
+ // the old CSP headers.
772
+ //
773
+ // This is problematic in stacked CDN setups where an old service worker with
774
+ // old CSPs can remain cached if the content of the service worker itself is not changed.
775
+ //
776
+ // To ensure the service worker is refetched when CSPs are changed, we factor in
777
+ // the CSP headers when generating the Etag.
778
+ //
779
+ // See https://gus.lightning.force.com/lightning/r/ADM_Work__c/a07EE000025yeu9YAA/view
780
+ // and https://salesforce-internal.slack.com/archives/C01GLHLBPT5/p1730739370922629
781
+ // for more details.
782
+
783
+ const contentSecurityPolicyHeader = res.getHeaders()[_constants.CONTENT_SECURITY_POLICY] || '';
784
+
743
785
  // Serve the file, with a strong ETag
744
- res.set('etag', (0, _ssrServer.getHashForString)(content));
786
+ // For this to be a valid ETag, the string must be placed between ""
787
+ // See https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/ETag#etag_value for
788
+ // more details
789
+ res.set('etag', `"${(0, _ssrServer.getHashForString)(content + contentSecurityPolicyHeader)}"`);
745
790
  res.set(_constants.CONTENT_TYPE, 'application/javascript');
746
791
  res.send(content);
747
792
  },
@@ -804,13 +849,29 @@ const RemoteServerFactory = exports.RemoteServerFactory = {
804
849
  * @param app {Express} - an Express App
805
850
  * @private
806
851
  */
807
- _createHandler(app) {
852
+ _createHandler(app, options) {
808
853
  // This flag is initially false, and is set true on the first request
809
854
  // handled by a Lambda. If it is true on entry to the handler function,
810
855
  // it indicates that the Lambda container has been reused.
811
856
  let lambdaContainerReused = false;
812
857
  const server = _awsServerlessExpress.default.createServer(app, null, binaryMimeTypes);
813
858
  const handler = (event, context, callback) => {
859
+ // encode non ASCII request headers
860
+ if (options !== null && options !== void 0 && options.encodeNonAsciiHttpHeaders) {
861
+ Object.keys(event.headers).forEach(key => {
862
+ if (!isASCII(event.headers[key])) {
863
+ event.headers[key] = encodeURIComponent(event.headers[key]);
864
+ // x-encoded-headers keeps track of which headers have been modified and encoded
865
+ if (event.headers[_constants.X_ENCODED_HEADERS]) {
866
+ // append header key
867
+ event.headers[_constants.X_ENCODED_HEADERS] = `${event.headers[_constants.X_ENCODED_HEADERS]},${key}`;
868
+ } else {
869
+ event.headers[_constants.X_ENCODED_HEADERS] = key;
870
+ }
871
+ }
872
+ });
873
+ }
874
+
814
875
  // We don't want to wait for an empty event loop once the response
815
876
  // has been sent. Setting this to false will "send the response
816
877
  // right away when the callback executes", but any pending events
@@ -829,12 +890,15 @@ const RemoteServerFactory = exports.RemoteServerFactory = {
829
890
  // the response to the browser.
830
891
  context.callbackWaitsForEmptyEventLoop = false;
831
892
  if (lambdaContainerReused) {
832
- // DESKTOP-434 If this Lambda container is being reused,
833
- // clean up memory now, so that we start with low usage.
834
- // These regular GC calls take about 80-100 mS each, as opposed
835
- // to forced GC calls, which occur randomly and can take several
836
- // hundred mS.
837
- app._collectGarbage();
893
+ const forceGarbageCollection = process.env.FORCE_GC;
894
+ if (forceGarbageCollection && forceGarbageCollection.toLowerCase() === 'true') {
895
+ // DESKTOP-434 If this Lambda container is being reused,
896
+ // clean up memory now, so that we start with low usage.
897
+ // These regular GC calls take about 80-100 mS each, as opposed
898
+ // to forced GC calls, which occur randomly and can take several
899
+ // hundred mS.
900
+ app._collectGarbage();
901
+ }
838
902
  app.sendMetric('LambdaReused');
839
903
  } else {
840
904
  // This is the first use of this container, so set the
@@ -912,8 +976,8 @@ const RemoteServerFactory = exports.RemoteServerFactory = {
912
976
  createHandler(options, customizeApp) {
913
977
  process.on('unhandledRejection', _ssrServer.catchAndLog);
914
978
  const app = this._createApp(options);
915
- customizeApp(app);
916
- return this._createHandler(app);
979
+ customizeApp(app, options);
980
+ return this._createHandler(app, options);
917
981
  },
918
982
  /**
919
983
  * @private
@@ -996,6 +1060,37 @@ const errorHandlerMiddleware = (err, req, res, next) => {
996
1060
  res.sendStatus(500);
997
1061
  };
998
1062
 
1063
+ /**
1064
+ * Helper function that checks if a string is composed of ASCII characters
1065
+ * We only check printable ASCII characters and not special ASCII characters
1066
+ * such as NULL
1067
+ *
1068
+ * @private
1069
+ */
1070
+ const isASCII = str => {
1071
+ return /^[\x20-\x7E]*$/.test(str);
1072
+ };
1073
+
1074
+ /**
1075
+ * Express Middleware applied to responses that encode any non ASCII headers
1076
+ *
1077
+ * @private
1078
+ */
1079
+ const encodeNonAsciiMiddleware = (req, res, next) => {
1080
+ const originalSetHeader = res.setHeader;
1081
+ res.setHeader = function (key, value) {
1082
+ if (!isASCII(value)) {
1083
+ originalSetHeader.call(this, key, encodeURIComponent(value));
1084
+ let encodedHeaders = res.getHeader(_constants.X_ENCODED_HEADERS);
1085
+ encodedHeaders = encodedHeaders ? `${encodedHeaders},${key}` : key;
1086
+ originalSetHeader.call(this, _constants.X_ENCODED_HEADERS, encodedHeaders);
1087
+ } else {
1088
+ originalSetHeader.call(this, key, value);
1089
+ }
1090
+ };
1091
+ next();
1092
+ };
1093
+
999
1094
  /**
1000
1095
  * Wrap the function fn in such a way that it will be called at most once. Subsequent
1001
1096
  * calls will always return the same value.
@@ -2,6 +2,8 @@
2
2
 
3
3
  var _supertest = _interopRequireDefault(require("supertest"));
4
4
  var _buildRemoteServer = require("./build-remote-server");
5
+ var _constants = require("./constants");
6
+ var _awsServerlessExpress = _interopRequireDefault(require("aws-serverless-express"));
5
7
  function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
6
8
  function ownKeys(e, r) { var t = Object.keys(e); if (Object.getOwnPropertySymbols) { var o = Object.getOwnPropertySymbols(e); r && (o = o.filter(function (r) { return Object.getOwnPropertyDescriptor(e, r).enumerable; })), t.push.apply(t, o); } return t; }
7
9
  function _objectSpread(e) { for (var r = 1; r < arguments.length; r++) { var t = null != arguments[r] ? arguments[r] : {}; r % 2 ? ownKeys(Object(t), !0).forEach(function (r) { _defineProperty(e, r, t[r]); }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(e, Object.getOwnPropertyDescriptors(t)) : ownKeys(Object(t)).forEach(function (r) { Object.defineProperty(e, r, Object.getOwnPropertyDescriptor(t, r)); }); } return e; }
@@ -13,6 +15,12 @@ function _toPrimitive(t, r) { if ("object" != typeof t || !t) return t; var e =
13
15
  * SPDX-License-Identifier: BSD-3-Clause
14
16
  * For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/BSD-3-Clause
15
17
  */
18
+ jest.mock('aws-serverless-express', () => {
19
+ return {
20
+ createServer: jest.fn(),
21
+ proxy: jest.fn()
22
+ };
23
+ });
16
24
  const opts = (overrides = {}) => {
17
25
  const defaults = {
18
26
  buildDir: './src/ssr/server/test_fixtures',
@@ -178,4 +186,70 @@ describe('remote server factory test coverage', () => {
178
186
  // ]
179
187
  // ]).toF
180
188
  // })
181
- // })
189
+ // })
190
+ describe('encodeNonAsciiHttpHeaders flag in options to createHandler', () => {
191
+ test('encodes request headers', () => {
192
+ const mockApp = {
193
+ sendMetric: jest.fn()
194
+ };
195
+ const mockOptions = {
196
+ encodeNonAsciiHttpHeaders: true
197
+ };
198
+ const originalHeaders = {
199
+ 'x-non-ascii-header-one': 'テスト',
200
+ 'x-non-ascii-header-two': '测试',
201
+ 'x-regular-header': 'ascii-str'
202
+ };
203
+ const event = {
204
+ headers: _objectSpread({}, originalHeaders)
205
+ };
206
+ const expectedHeaders = {
207
+ 'x-non-ascii-header-one': '%E3%83%86%E3%82%B9%E3%83%88',
208
+ 'x-non-ascii-header-two': '%E6%B5%8B%E8%AF%95',
209
+ 'x-encoded-headers': 'x-non-ascii-header-one,x-non-ascii-header-two',
210
+ 'x-regular-header': 'ascii-str'
211
+ };
212
+ const {
213
+ handler
214
+ } = _buildRemoteServer.RemoteServerFactory._createHandler(mockApp, mockOptions);
215
+ expect(event.headers).toEqual(originalHeaders);
216
+ handler(event, {}, {});
217
+ expect(event.headers).toEqual(expectedHeaders);
218
+ expect(decodeURIComponent(event.headers['x-non-ascii-header-one'])).toEqual(originalHeaders['x-non-ascii-header-one']);
219
+ });
220
+ test('encodes response headers', () => {
221
+ const mockApp = {
222
+ use: jest.fn()
223
+ };
224
+ const mockOptions = {
225
+ encodeNonAsciiHttpHeaders: true
226
+ };
227
+ const res = {
228
+ headers: {},
229
+ setHeader: (key, value) => {
230
+ res.headers[key] = value;
231
+ },
232
+ getHeader: key => {
233
+ return res.headers[key];
234
+ }
235
+ };
236
+ const nonASCIIheader = 'x-non-ascii-header';
237
+ const nonASCIIstr = 'テスト';
238
+ const expectedEncoding = '%E3%83%86%E3%82%B9%E3%83%88';
239
+ const regularHeaderKey = 'x-regular-header';
240
+ const regularHeaderValue = 'ascii-str';
241
+ _buildRemoteServer.RemoteServerFactory._setupCommonMiddleware(mockApp, mockOptions);
242
+ const encodeNonAsciiMiddleware = mockApp.use.mock.calls[3][0];
243
+ res.setHeader(nonASCIIheader, nonASCIIstr);
244
+ expect(res.getHeader(nonASCIIheader)).toEqual(nonASCIIstr);
245
+ encodeNonAsciiMiddleware({}, res, () => {});
246
+ res.setHeader(nonASCIIheader, nonASCIIstr);
247
+ expect(res.getHeader(nonASCIIheader)).toEqual(expectedEncoding);
248
+ expect(decodeURI(expectedEncoding)).toEqual(nonASCIIstr);
249
+ expect(res.getHeader(_constants.X_ENCODED_HEADERS)).toEqual(nonASCIIheader);
250
+
251
+ // confirm ASCII headers are not modified
252
+ res.setHeader(regularHeaderKey, regularHeaderValue);
253
+ expect(res.getHeader(regularHeaderKey)).toEqual(regularHeaderValue);
254
+ });
255
+ });
@@ -8,6 +8,7 @@ export const CONTENT_ENCODING: "content-encoding";
8
8
  export const X_ORIGINAL_CONTENT_TYPE: "x-original-content-type";
9
9
  export const X_MOBIFY_QUERYSTRING: "x-mobify-querystring";
10
10
  export const X_MOBIFY_FROM_CACHE: "x-mobify-from-cache";
11
+ export const X_ENCODED_HEADERS: "x-encoded-headers";
11
12
  export const SET_COOKIE: "set-cookie";
12
13
  export const CACHE_CONTROL: "cache-control";
13
14
  export const NO_CACHE: "max-age=0, nocache, nostore, must-revalidate";
@@ -1 +1 @@
1
- {"version":3,"file":"constants.d.ts","sourceRoot":"","sources":["../../../src/ssr/server/constants.js"],"names":[],"mappings":"AAMA,kEAAkE;AAClE,4BAA4B;AAC5B,4CAA4C;AAE5C,oEAAoE;AACpE,gDAAgD;AAGhD,0CAA0C;AAC1C,kDAAkD;AAClD,gEAAgE;AAChE,0DAA0D;AAC1D,wDAAwD;AACxD,sCAAsC;AACtC,4CAA4C;AAC5C,sEAAsE;AACtE,gEAAgE;AAChE,oEAAoE;AAEpE,2EAA2E;AAC3E,4DAA4D"}
1
+ {"version":3,"file":"constants.d.ts","sourceRoot":"","sources":["../../../src/ssr/server/constants.js"],"names":[],"mappings":"AAMA,kEAAkE;AAClE,4BAA4B;AAC5B,4CAA4C;AAE5C,oEAAoE;AACpE,gDAAgD;AAGhD,0CAA0C;AAC1C,kDAAkD;AAClD,gEAAgE;AAChE,0DAA0D;AAC1D,wDAAwD;AACxD,oDAAoD;AACpD,sCAAsC;AACtC,4CAA4C;AAC5C,sEAAsE;AACtE,gEAAgE;AAChE,oEAAoE;AAEpE,2EAA2E;AAC3E,4DAA4D"}
@@ -3,7 +3,7 @@
3
3
  Object.defineProperty(exports, "__esModule", {
4
4
  value: true
5
5
  });
6
- exports.X_ORIGINAL_CONTENT_TYPE = exports.X_MOBIFY_QUERYSTRING = exports.X_MOBIFY_FROM_CACHE = exports.STRICT_TRANSPORT_SECURITY = exports.STATIC_ASSETS = exports.SLAS_CUSTOM_PROXY_PATH = exports.SET_COOKIE = exports.PROXY_PATH_PREFIX = exports.NO_CACHE = exports.CONTENT_TYPE = exports.CONTENT_SECURITY_POLICY = exports.CONTENT_ENCODING = exports.CACHE_CONTROL = exports.BUILD = exports.APPLICATION_OCTET_STREAM = void 0;
6
+ exports.X_ORIGINAL_CONTENT_TYPE = exports.X_MOBIFY_QUERYSTRING = exports.X_MOBIFY_FROM_CACHE = exports.X_ENCODED_HEADERS = exports.STRICT_TRANSPORT_SECURITY = exports.STATIC_ASSETS = exports.SLAS_CUSTOM_PROXY_PATH = exports.SET_COOKIE = exports.PROXY_PATH_PREFIX = exports.NO_CACHE = exports.CONTENT_TYPE = exports.CONTENT_SECURITY_POLICY = exports.CONTENT_ENCODING = exports.CACHE_CONTROL = exports.BUILD = exports.APPLICATION_OCTET_STREAM = void 0;
7
7
  /*
8
8
  * Copyright (c) 2021, salesforce.com, inc.
9
9
  * All rights reserved.
@@ -23,6 +23,7 @@ const CONTENT_ENCODING = exports.CONTENT_ENCODING = 'content-encoding';
23
23
  const X_ORIGINAL_CONTENT_TYPE = exports.X_ORIGINAL_CONTENT_TYPE = 'x-original-content-type';
24
24
  const X_MOBIFY_QUERYSTRING = exports.X_MOBIFY_QUERYSTRING = 'x-mobify-querystring';
25
25
  const X_MOBIFY_FROM_CACHE = exports.X_MOBIFY_FROM_CACHE = 'x-mobify-from-cache';
26
+ const X_ENCODED_HEADERS = exports.X_ENCODED_HEADERS = 'x-encoded-headers';
26
27
  const SET_COOKIE = exports.SET_COOKIE = 'set-cookie';
27
28
  const CACHE_CONTROL = exports.CACHE_CONTROL = 'cache-control';
28
29
  const NO_CACHE = exports.NO_CACHE = 'max-age=0, nocache, nostore, must-revalidate';
@@ -54,6 +54,54 @@ const testFixtures = path.resolve(process.cwd(), 'src/ssr/server/test_fixtures')
54
54
  const httpsAgent = new https.Agent({
55
55
  rejectUnauthorized: false
56
56
  });
57
+ function createServerWithGCSpy() {
58
+ const route = jest.fn((req, res) => {
59
+ res.send('<html/>');
60
+ });
61
+ const options = {
62
+ buildDir: testFixtures,
63
+ mobify: testPackageMobify,
64
+ sslFilePath: path.join(testFixtures, 'localhost.pem'),
65
+ quiet: true,
66
+ port: TEST_PORT,
67
+ fetchAgents: {
68
+ https: httpsAgent
69
+ }
70
+ };
71
+ const {
72
+ handler,
73
+ server,
74
+ app
75
+ } = RemoteServerFactory.createHandler(options, app => {
76
+ app.get('/*', route);
77
+ });
78
+ const collectGarbage = jest.spyOn(app, '_collectGarbage');
79
+ const sendMetric = jest.spyOn(app, 'sendMetric');
80
+ return {
81
+ route,
82
+ handler,
83
+ collectGarbage,
84
+ sendMetric,
85
+ server
86
+ };
87
+ }
88
+ function createApiGatewayEvent() {
89
+ // Set up a fake event and a fake context for the Lambda call
90
+ const event = createEvent('aws:apiGateway', {
91
+ path: '/',
92
+ body: undefined
93
+ });
94
+ if (event.queryStringParameters) {
95
+ delete event.queryStringParameters;
96
+ }
97
+ const context = AWSMockContext({
98
+ functionName: 'SSRTest'
99
+ });
100
+ return {
101
+ event,
102
+ context
103
+ };
104
+ }
57
105
  describe('SSRServer Lambda integration', () => {
58
106
  let savedEnvironment;
59
107
  let server;
@@ -329,42 +377,50 @@ describe('SSRServer Lambda integration', () => {
329
377
  X_HEADERS_TO_REMOVE_ORIGIN.forEach(key => expect(reqHeaders[key]).toBeUndefined());
330
378
  });
331
379
  });
332
- test('Lambda reuse behaviour', () => {
333
- const route = jest.fn((req, res) => {
334
- res.send('<html/>');
335
- });
336
- const options = {
337
- buildDir: testFixtures,
338
- mobify: testPackageMobify,
339
- sslFilePath: path.join(testFixtures, 'localhost.pem'),
340
- quiet: true,
341
- port: TEST_PORT,
342
- fetchAgents: {
343
- https: httpsAgent
344
- }
345
- };
380
+ test('Lambda reuse -- Default Behavior', () => {
346
381
  const {
347
- app,
382
+ route,
348
383
  handler,
349
- server: srv
350
- } = RemoteServerFactory.createHandler(options, app => {
351
- app.get('/*', route);
352
- });
353
- const collectGarbage = jest.spyOn(app, '_collectGarbage');
354
- const sendMetric = jest.spyOn(app, 'sendMetric');
355
- server = srv;
356
-
357
- // Set up a fake event and a fake context for the Lambda call
358
- const event = createEvent('aws:apiGateway', {
359
- path: '/',
360
- body: undefined
361
- });
362
- if (event.queryStringParameters) {
363
- delete event.queryStringParameters;
364
- }
365
- const context = AWSMockContext({
366
- functionName: 'SSRTest'
384
+ collectGarbage,
385
+ sendMetric,
386
+ new_server
387
+ } = createServerWithGCSpy();
388
+ const {
389
+ event,
390
+ context
391
+ } = createApiGatewayEvent();
392
+ server = new_server;
393
+ const call = event => new Promise(resolve => handler(event, context, (err, response) => resolve(response)));
394
+ return Promise.resolve().then(() => call(event)).then(response => {
395
+ // First request - Lambda container created
396
+ expect(response.statusCode).toBe(200);
397
+ expect(collectGarbage.mock.calls).toHaveLength(0);
398
+ expect(route.mock.calls).toHaveLength(1);
399
+ expect(sendMetric).toHaveBeenCalledWith('LambdaCreated');
400
+ expect(sendMetric).not.toHaveBeenCalledWith('LambdaReused');
401
+ }).then(() => call(event)).then(response => {
402
+ // Second call - Lambda container reused
403
+ expect(response.statusCode).toBe(200);
404
+ expect(collectGarbage.mock.calls).toHaveLength(0);
405
+ expect(route.mock.calls).toHaveLength(2);
406
+ expect(sendMetric).toHaveBeenCalledWith('LambdaCreated');
407
+ expect(sendMetric).toHaveBeenCalledWith('LambdaReused');
367
408
  });
409
+ });
410
+ test('Lambda reuse -- with Forced Garbage Collection Enabled', () => {
411
+ process.env.FORCE_GC = 'true';
412
+ const {
413
+ event,
414
+ context
415
+ } = createApiGatewayEvent();
416
+ const {
417
+ route,
418
+ handler,
419
+ collectGarbage,
420
+ sendMetric,
421
+ new_server
422
+ } = createServerWithGCSpy();
423
+ server = new_server;
368
424
  const call = event => new Promise(resolve => handler(event, context, (err, response) => resolve(response)));
369
425
  return Promise.resolve().then(() => call(event)).then(response => {
370
426
  // First request - Lambda container created
@@ -19,6 +19,8 @@ var _crypto = require("crypto");
19
19
  function _getRequireWildcardCache(e) { if ("function" != typeof WeakMap) return null; var r = new WeakMap(), t = new WeakMap(); return (_getRequireWildcardCache = function (e) { return e ? t : r; })(e); }
20
20
  function _interopRequireWildcard(e, r) { if (!r && e && e.__esModule) return e; if (null === e || "object" != typeof e && "function" != typeof e) return { default: e }; var t = _getRequireWildcardCache(r); if (t && t.has(e)) return t.get(e); var n = { __proto__: null }, a = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var u in e) if ("default" !== u && {}.hasOwnProperty.call(e, u)) { var i = a ? Object.getOwnPropertyDescriptor(e, u) : null; i && (i.get || i.set) ? Object.defineProperty(n, u, i) : n[u] = e[u]; } return n.default = e, t && t.set(e, n), n; }
21
21
  function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
22
+ function asyncGeneratorStep(n, t, e, r, o, a, c) { try { var i = n[a](c), u = i.value; } catch (n) { return void e(n); } i.done ? t(u) : Promise.resolve(u).then(r, o); }
23
+ function _asyncToGenerator(n) { return function () { var t = this, e = arguments; return new Promise(function (r, o) { var a = n.apply(t, e); function _next(n) { asyncGeneratorStep(a, r, o, _next, _throw, "next", n); } function _throw(n) { asyncGeneratorStep(a, r, o, _next, _throw, "throw", n); } _next(void 0); }); }; }
22
24
  function _extends() { return _extends = Object.assign ? Object.assign.bind() : function (n) { for (var e = 1; e < arguments.length; e++) { var t = arguments[e]; for (var r in t) ({}).hasOwnProperty.call(t, r) && (n[r] = t[r]); } return n; }, _extends.apply(null, arguments); }
23
25
  function ownKeys(e, r) { var t = Object.keys(e); if (Object.getOwnPropertySymbols) { var o = Object.getOwnPropertySymbols(e); r && (o = o.filter(function (r) { return Object.getOwnPropertyDescriptor(e, r).enumerable; })), t.push.apply(t, o); } return t; }
24
26
  function _objectSpread(e) { for (var r = 1; r < arguments.length; r++) { var t = null != arguments[r] ? arguments[r] : {}; r % 2 ? ownKeys(Object(t), !0).forEach(function (r) { _defineProperty(e, r, t[r]); }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(e, Object.getOwnPropertyDescriptors(t)) : ownKeys(Object(t)).forEach(function (r) { Object.defineProperty(e, r, Object.getOwnPropertyDescriptor(t, r)); }); } return e; }
@@ -477,6 +479,40 @@ describe('SSRServer operation', () => {
477
479
  expect(response.headers['set-cookie']).toBeUndefined();
478
480
  });
479
481
  });
482
+ test('should set xForwardedOrigin based on defined x-forwarded-host and x-forwarded-proto headers', () => {
483
+ process.env = {
484
+ MRT_ALLOW_COOKIES: 'true'
485
+ };
486
+ const forwardedHost = 'www.example.com';
487
+ const forwardedProto = 'https';
488
+ const app = _buildRemoteServer.RemoteServerFactory._createApp(opts());
489
+ const route = (req, res) => {
490
+ expect(req.headers['x-forwarded-host']).toBe(forwardedHost);
491
+ expect(res.locals.xForwardedOrigin).toBe(`${forwardedProto}://${forwardedHost}`);
492
+ res.sendStatus(200);
493
+ };
494
+ app.get('/*', route);
495
+ return (0, _supertest.default)(app).get('/').set('x-forwarded-host', forwardedHost).set('x-forwarded-proto', 'https').then(response => {
496
+ expect(response.status).toBe(200);
497
+ });
498
+ });
499
+ test('should set xForwardedOrigin based on defined x-forwarded-host and undefined x-forwarded-proto headers', () => {
500
+ process.env = {
501
+ MRT_ALLOW_COOKIES: 'true'
502
+ };
503
+ const options = opts();
504
+ const forwardedHost = 'www.example.com';
505
+ const app = _buildRemoteServer.RemoteServerFactory._createApp(options);
506
+ const route = (req, res) => {
507
+ expect(req.headers['x-forwarded-host']).toBe(forwardedHost);
508
+ expect(res.locals.xForwardedOrigin).toBe(`${options.protocol}://${forwardedHost}`);
509
+ res.sendStatus(200);
510
+ };
511
+ app.get('/*', route);
512
+ return (0, _supertest.default)(app).get('/').set('x-forwarded-host', forwardedHost).then(response => {
513
+ expect(response.status).toBe(200);
514
+ });
515
+ });
480
516
  test(`should reject POST requests to /`, () => {
481
517
  const app = _buildRemoteServer.RemoteServerFactory._createApp(opts());
482
518
  const route = (req, res) => {
@@ -948,5 +984,50 @@ describe('SLAS private client proxy', () => {
948
984
  expect(response.body.host).toBe('shortCode.api.commercecloud.salesforce.com');
949
985
  expect(response.body['x-mobify']).toBe('true');
950
986
  });
951
- });
987
+ }, 15000);
988
+ test('does not add _sfdc_client_auth header if request not for /oauth2/trusted-agent/token', /*#__PURE__*/_asyncToGenerator(function* () {
989
+ process.env.PWA_KIT_SLAS_CLIENT_SECRET = 'a secret';
990
+ const encodedCredentials = Buffer.from('clientId:a secret').toString('base64');
991
+ const app = _buildRemoteServer.RemoteServerFactory._createApp(opts({
992
+ mobify: {
993
+ app: {
994
+ commerceAPI: {
995
+ parameters: {
996
+ clientId: 'clientId',
997
+ shortCode: 'shortCode'
998
+ }
999
+ }
1000
+ }
1001
+ },
1002
+ useSLASPrivateClient: true,
1003
+ slasTarget: slasTarget
1004
+ }));
1005
+ return yield (0, _supertest.default)(app).get('/mobify/slas/oauth2/other-path').then(response => {
1006
+ expect(response.body._sfdc_client_auth).toBeUndefined();
1007
+ });
1008
+ }), 15000);
1009
+ test('adds _sfdc_client_auth header if request is for /oauth2/trusted-agent/token', /*#__PURE__*/_asyncToGenerator(function* () {
1010
+ process.env.PWA_KIT_SLAS_CLIENT_SECRET = 'a secret';
1011
+ const encodedCredentials = Buffer.from('clientId:a secret').toString('base64');
1012
+ const app = _buildRemoteServer.RemoteServerFactory._createApp(opts({
1013
+ mobify: {
1014
+ app: {
1015
+ commerceAPI: {
1016
+ parameters: {
1017
+ clientId: 'clientId',
1018
+ shortCode: 'shortCode'
1019
+ }
1020
+ }
1021
+ }
1022
+ },
1023
+ useSLASPrivateClient: true,
1024
+ slasTarget: slasTarget,
1025
+ trustedAgentAuthPathMatch: /\/oauth2\/trusted-agent\/token/
1026
+ }));
1027
+ return yield (0, _supertest.default)(app).get('/mobify/slas/private/oauth2/trusted-agent/token').then(response => {
1028
+ expect(response.body['_sfdc_client_auth']).toBe(encodedCredentials);
1029
+ expect(response.body.host).toBe('shortCode.api.commercecloud.salesforce.com');
1030
+ expect(response.body['x-mobify']).toBe('true');
1031
+ });
1032
+ }), 15000);
952
1033
  });
@@ -1 +1 @@
1
- {"version":3,"file":"metrics-sender.d.ts","sourceRoot":"","sources":["../../../src/utils/ssr-server/metrics-sender.js"],"names":[],"mappings":"AASA;;;;;;;GAOG;AACH;IAKQ,iDAAe;IAKf,cAAgB;IAGpB;;;OAGG;IACH,0BAEC;IAED;;;;;;OAMG;IACH,eAaC;IAED;;;;;;;;OAQG;IACH,uBA0BC;IAED;;;;OAIG;IACH,+BAyBC;IAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;OA6BG;IACH,aA4BC;CACJ;;;IAOD;;;;;OAKG;IACH,oCAKC"}
1
+ {"version":3,"file":"metrics-sender.d.ts","sourceRoot":"","sources":["../../../src/utils/ssr-server/metrics-sender.js"],"names":[],"mappings":"AASA;;;;;;;GAOG;AACH;IAKQ,iDAAe;IAKf,cAAgB;IAGpB;;;OAGG;IACH,0BAEC;IAED;;;;;;OAMG;IACH,eAiBC;IAED;;;;;;;;OAQG;IACH,uBA0BC;IAED;;;;OAIG;IACH,+BAyBC;IAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;OA6BG;IACH,aA4BC;CACJ;;;IAOD;;;;;OAKG;IACH,oCAKC"}
@@ -59,7 +59,11 @@ class MetricsSender {
59
59
  apiVersion: '2010-08-01',
60
60
  // The AWS_REGION variable is defined by the Lambda
61
61
  // environment.
62
- region: process.env.AWS_REGION || 'us-east-1'
62
+ region: process.env.AWS_REGION || 'us-east-1',
63
+ // Setting maxRetries to 0 will prevent the SDK from retrying.
64
+ // This is necessary because under high load, there will be backpressure
65
+ // on the Lambda function, and causing severe performance issues (400-500ms latency)
66
+ maxRetries: 0
63
67
  });
64
68
  }
65
69
  return this._CW;