@salesforce/pwa-kit-runtime 3.11.0 → 3.12.0-dev
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 +5 -5
- package/ssr/server/build-remote-server.js +194 -24
- package/ssr/server/build-remote-server.test.js +5 -0
- package/ssr/server/express.lambda.test.js +5 -0
- package/ssr/server/express.test.js +207 -60
- package/utils/middleware/security.js +4 -2
- package/utils/middleware/security.test.js +8 -3
- package/utils/ssr-namespace-paths.js +53 -22
- package/utils/ssr-namespace-paths.test.js +56 -0
- package/utils/ssr-server/configure-proxy.js +8 -4
- package/utils/ssr-server/convert-express-route.js +126 -0
- package/utils/ssr-server/utils.test.js +5 -0
- package/utils/ssr-server.test.js +5 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@salesforce/pwa-kit-runtime",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.12.0-dev",
|
|
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,11 +46,11 @@
|
|
|
46
46
|
},
|
|
47
47
|
"devDependencies": {
|
|
48
48
|
"@loadable/component": "^5.15.3",
|
|
49
|
-
"@salesforce/pwa-kit-dev": "3.
|
|
49
|
+
"@salesforce/pwa-kit-dev": "3.12.0-dev",
|
|
50
50
|
"@serverless/event-mocks": "^1.1.1",
|
|
51
51
|
"aws-lambda-mock-context": "^3.2.1",
|
|
52
52
|
"fs-extra": "^11.1.1",
|
|
53
|
-
"internal-lib-build": "3.
|
|
53
|
+
"internal-lib-build": "3.12.0-dev",
|
|
54
54
|
"nock": "^13.3.0",
|
|
55
55
|
"nodemon": "^2.0.22",
|
|
56
56
|
"sinon": "^13.0.2",
|
|
@@ -58,7 +58,7 @@
|
|
|
58
58
|
"supertest": "^4.0.2"
|
|
59
59
|
},
|
|
60
60
|
"peerDependencies": {
|
|
61
|
-
"@salesforce/pwa-kit-dev": "3.
|
|
61
|
+
"@salesforce/pwa-kit-dev": "3.12.0-dev"
|
|
62
62
|
},
|
|
63
63
|
"peerDependenciesMeta": {
|
|
64
64
|
"@salesforce/pwa-kit-dev": {
|
|
@@ -72,5 +72,5 @@
|
|
|
72
72
|
"publishConfig": {
|
|
73
73
|
"directory": "dist"
|
|
74
74
|
},
|
|
75
|
-
"gitHead": "
|
|
75
|
+
"gitHead": "60d9a47ad71824709be52bbe1110b092f7698244"
|
|
76
76
|
}
|
|
@@ -27,6 +27,7 @@ var _awsServerlessExpress = _interopRequireDefault(require("aws-serverless-expre
|
|
|
27
27
|
var _morgan = _interopRequireDefault(require("morgan"));
|
|
28
28
|
var _loggerInstance = _interopRequireDefault(require("../../utils/logger-instance"));
|
|
29
29
|
var _httpProxyMiddleware = require("http-proxy-middleware");
|
|
30
|
+
var _convertExpressRoute = require("../../utils/ssr-server/convert-express-route");
|
|
30
31
|
function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
|
|
31
32
|
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; }
|
|
32
33
|
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; }
|
|
@@ -296,6 +297,17 @@ const RemoteServerFactory = exports.RemoteServerFactory = {
|
|
|
296
297
|
this._setCompression(app);
|
|
297
298
|
this._setRequestId(app);
|
|
298
299
|
// this._addEventContext(app)
|
|
300
|
+
|
|
301
|
+
// We want to remove any base paths that have made it this far.
|
|
302
|
+
// Base paths are used to route requests to the correct server.
|
|
303
|
+
// If the request has reached the server, it is no longer needed.
|
|
304
|
+
// Note: We use envBasePath as the feature flag for this middleware
|
|
305
|
+
// If envBasePath is `/`, '', or undefined when the server starts, we don't need to
|
|
306
|
+
// initialize this middleware.
|
|
307
|
+
if ((0, _ssrNamespacePaths.getEnvBasePath)()) {
|
|
308
|
+
this._setupBasePathMiddleware(app);
|
|
309
|
+
}
|
|
310
|
+
|
|
299
311
|
// Ordering of the next two calls are vital - we don't
|
|
300
312
|
// want request-processors applied to development views.
|
|
301
313
|
this._addSDKInternalHandlers(app);
|
|
@@ -393,6 +405,122 @@ const RemoteServerFactory = exports.RemoteServerFactory = {
|
|
|
393
405
|
next();
|
|
394
406
|
});
|
|
395
407
|
},
|
|
408
|
+
/**
|
|
409
|
+
* @private
|
|
410
|
+
*/
|
|
411
|
+
_setupBasePathMiddleware(app) {
|
|
412
|
+
// Cache the express route regexes to avoid re-calculating them on every request.
|
|
413
|
+
let expressRouteRegexes;
|
|
414
|
+
|
|
415
|
+
/**
|
|
416
|
+
* Remove the base path from a path.
|
|
417
|
+
*
|
|
418
|
+
* If path is like '/basepath/something', this returns '/something'
|
|
419
|
+
* If path is exactly '/basepath', this returns '/'
|
|
420
|
+
* If path doesn't start with base path or if there is no base path defined,
|
|
421
|
+
* returns the unmodified path
|
|
422
|
+
*
|
|
423
|
+
* @param path {string} the path to remove the base path from
|
|
424
|
+
* @returns {string} the path with the base path removed
|
|
425
|
+
*/
|
|
426
|
+
const removeBasePathFromPath = path => {
|
|
427
|
+
const basePath = (0, _ssrNamespacePaths.getEnvBasePath)();
|
|
428
|
+
if (!basePath) {
|
|
429
|
+
return path;
|
|
430
|
+
}
|
|
431
|
+
if (path.startsWith(basePath + '/')) {
|
|
432
|
+
return path.substring(basePath.length);
|
|
433
|
+
} else if (path === basePath) {
|
|
434
|
+
return '/';
|
|
435
|
+
}
|
|
436
|
+
return path;
|
|
437
|
+
};
|
|
438
|
+
|
|
439
|
+
/**
|
|
440
|
+
* Initializes a cache of regexes that correspond to the registered express routes
|
|
441
|
+
*
|
|
442
|
+
* This specifically omits the generic wildcard from the express routes where we
|
|
443
|
+
* want to remove the base path from since it is mapped to the app render. This is
|
|
444
|
+
* because routes sent to the render are handled by React Router
|
|
445
|
+
*/
|
|
446
|
+
const initializeExpressRouteRegexes = () => {
|
|
447
|
+
const expressRoutes = app._router.stack.filter(layer => layer.route && layer.route.path && layer.route.path !== '*').map(layer => layer.route.path);
|
|
448
|
+
expressRouteRegexes = expressRoutes.map(route => (0, _convertExpressRoute.convertExpressRouteToRegex)(route));
|
|
449
|
+
};
|
|
450
|
+
|
|
451
|
+
/**
|
|
452
|
+
* Very early request processing.
|
|
453
|
+
*
|
|
454
|
+
* If the server receives a request containing the base path, remove it before allowing
|
|
455
|
+
* the request through to the other express endpoints
|
|
456
|
+
*
|
|
457
|
+
* We scope base path removal to /mobify routes and routes defined by the express app
|
|
458
|
+
* (For example /callback or /worker.js)
|
|
459
|
+
* This is to avoid affecting React Router routes where a site id or locale might be present
|
|
460
|
+
* that is equal to the base path.
|
|
461
|
+
*
|
|
462
|
+
* For example, if you have a base path of /us and a site id of /us we don't want
|
|
463
|
+
* to remove the /us from www.example.com/us/en-US/category/... as this route is handled by
|
|
464
|
+
* React Router and the PWA multisite implementation.
|
|
465
|
+
*
|
|
466
|
+
* @param req {express.req} the incoming request - modified in-place
|
|
467
|
+
* @private
|
|
468
|
+
*/
|
|
469
|
+
const removeBasePathMiddleware = (req, res, next) => {
|
|
470
|
+
const basePath = (0, _ssrNamespacePaths.getEnvBasePath)();
|
|
471
|
+
|
|
472
|
+
// Fast path: /mobify routes always get the base path removed
|
|
473
|
+
if (req.path.startsWith(`${basePath}/mobify`)) {
|
|
474
|
+
const cleanPath = removeBasePathFromPath(req.path);
|
|
475
|
+
const parsed = _url.default.parse(req.url);
|
|
476
|
+
parsed.pathname = cleanPath;
|
|
477
|
+
req.url = _url.default.format(parsed);
|
|
478
|
+
return next();
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
// For other routes, only proceed if path actually starts with base path
|
|
482
|
+
if (!req.path.startsWith(basePath)) {
|
|
483
|
+
return next();
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
// Now we know path starts with base path, so we can remove it
|
|
487
|
+
const cleanPath = removeBasePathFromPath(req.path);
|
|
488
|
+
|
|
489
|
+
// Initialize express route regexes if needed
|
|
490
|
+
// We do this here since we want to ensure that any express route defined
|
|
491
|
+
// after the app is created, such as routes defined in ssr.js, are included.
|
|
492
|
+
if (!expressRouteRegexes) {
|
|
493
|
+
initializeExpressRouteRegexes();
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
// Next we check if the clean path matches any existing express routes
|
|
497
|
+
// (like /callback or /worker.js)
|
|
498
|
+
const matchesExpressRoute = expressRouteRegexes.some(routeRegex => {
|
|
499
|
+
try {
|
|
500
|
+
return routeRegex.test(cleanPath);
|
|
501
|
+
} catch (error) {
|
|
502
|
+
_loggerInstance.default.warn(`Invalid express route pattern: ${routeRegex}`, /* istanbul ignore next */
|
|
503
|
+
{
|
|
504
|
+
namespace: 'removeBasePathMiddleware',
|
|
505
|
+
additionalProperties: {
|
|
506
|
+
error: error
|
|
507
|
+
}
|
|
508
|
+
});
|
|
509
|
+
return false;
|
|
510
|
+
}
|
|
511
|
+
});
|
|
512
|
+
|
|
513
|
+
// Only update URL if our clean path matches an Express route
|
|
514
|
+
// This leaves React Router paths (like /en-US/category) unchanged
|
|
515
|
+
if (matchesExpressRoute) {
|
|
516
|
+
const parsed = _url.default.parse(req.url);
|
|
517
|
+
parsed.pathname = cleanPath;
|
|
518
|
+
req.url = _url.default.format(parsed);
|
|
519
|
+
}
|
|
520
|
+
next();
|
|
521
|
+
};
|
|
522
|
+
app.use(removeBasePathMiddleware);
|
|
523
|
+
},
|
|
396
524
|
/**
|
|
397
525
|
* @private
|
|
398
526
|
*/
|
|
@@ -576,8 +704,8 @@ const RemoteServerFactory = exports.RemoteServerFactory = {
|
|
|
576
704
|
/**
|
|
577
705
|
* @private
|
|
578
706
|
*/
|
|
579
|
-
_handleMissingSlasPrivateEnvVar(app) {
|
|
580
|
-
app.use(
|
|
707
|
+
_handleMissingSlasPrivateEnvVar(app, slasPrivateProxyPath) {
|
|
708
|
+
app.use(slasPrivateProxyPath, (_, res) => {
|
|
581
709
|
return res.status(501).json({
|
|
582
710
|
message: 'Environment variable PWA_KIT_SLAS_CLIENT_SECRET not set: Please set this environment variable to proceed.'
|
|
583
711
|
});
|
|
@@ -591,6 +719,15 @@ const RemoteServerFactory = exports.RemoteServerFactory = {
|
|
|
591
719
|
if (!options.useSLASPrivateClient) {
|
|
592
720
|
return;
|
|
593
721
|
}
|
|
722
|
+
|
|
723
|
+
// This is the full path to the SLAS trusted-system endpoint
|
|
724
|
+
// We want to throw an error if the regex defined options.applySLASPrivateClientToEndpoints
|
|
725
|
+
// matches this path as an early warning to developers that they should update their regex
|
|
726
|
+
// in ssr.js to exclude this path.
|
|
727
|
+
const trustedSystemPath = '/shopper/auth/v1/oauth2/trusted-system/token';
|
|
728
|
+
if (trustedSystemPath.match(options.applySLASPrivateClientToEndpoints)) {
|
|
729
|
+
throw new Error('It is not allowed to include /oauth2/trusted-system endpoints in `applySLASPrivateClientToEndpoints`');
|
|
730
|
+
}
|
|
594
731
|
(0, _ssrServer.localDevLog)(`Proxying ${_ssrNamespacePaths.slasPrivateProxyPath} to ${options.slasTarget}`);
|
|
595
732
|
const clientId = (_options$mobify2 = options.mobify) === null || _options$mobify2 === void 0 ? void 0 : (_options$mobify2$app = _options$mobify2.app) === null || _options$mobify2$app === void 0 ? void 0 : (_options$mobify2$app$ = _options$mobify2$app.commerceAPI) === null || _options$mobify2$app$ === void 0 ? void 0 : (_options$mobify2$app$2 = _options$mobify2$app$.parameters) === null || _options$mobify2$app$2 === void 0 ? void 0 : _options$mobify2$app$2.clientId;
|
|
596
733
|
const clientSecret = process.env.PWA_KIT_SLAS_CLIENT_SECRET;
|
|
@@ -599,14 +736,34 @@ const RemoteServerFactory = exports.RemoteServerFactory = {
|
|
|
599
736
|
return;
|
|
600
737
|
}
|
|
601
738
|
const encodedSlasCredentials = Buffer.from(`${clientId}:${clientSecret}`).toString('base64');
|
|
602
|
-
app.use(_ssrNamespacePaths.slasPrivateProxyPath, (
|
|
739
|
+
app.use(_ssrNamespacePaths.slasPrivateProxyPath, (req, res, next) => {
|
|
740
|
+
var _req$path, _req$path2;
|
|
741
|
+
// Check if the request should be blocked before it reaches the proxy
|
|
742
|
+
// We run this outside of the proxy middleware because modifying the response
|
|
743
|
+
// to send a 403 in the proxy causes issues with the response interceptor.
|
|
744
|
+
if (!((_req$path = req.path) !== null && _req$path !== void 0 && _req$path.match(options.slasApiPath)) || (_req$path2 = req.path) !== null && _req$path2 !== void 0 && _req$path2.match(/\/oauth2\/trusted-system/)) {
|
|
745
|
+
const message = `Request to ${req.path} is not allowed through the SLAS Private Client Proxy`;
|
|
746
|
+
_loggerInstance.default.error(message);
|
|
747
|
+
return res.status(403).json({
|
|
748
|
+
message: message
|
|
749
|
+
});
|
|
750
|
+
}
|
|
751
|
+
next();
|
|
752
|
+
}, (0, _httpProxyMiddleware.createProxyMiddleware)({
|
|
603
753
|
target: options.slasTarget,
|
|
604
754
|
changeOrigin: true,
|
|
605
|
-
|
|
606
|
-
|
|
755
|
+
// http-proxy-middleware uses the original incoming request path to determine
|
|
756
|
+
// both proxyRequest and incomingRequest paths.
|
|
757
|
+
// This cannot be modified by any express middleware
|
|
758
|
+
// So we need to use the built in pathRewrite to remove the base path
|
|
759
|
+
pathRewrite: path => {
|
|
760
|
+
const basePathRegexEntry = (0, _ssrNamespacePaths.getEnvBasePath)() ? `${(0, _ssrNamespacePaths.getEnvBasePath)()}?` : '';
|
|
761
|
+
const regex = new RegExp(`^${basePathRegexEntry}${_ssrNamespacePaths.slasPrivateProxyPath}`);
|
|
762
|
+
return path.replace(regex, '');
|
|
607
763
|
},
|
|
764
|
+
selfHandleResponse: true,
|
|
608
765
|
onProxyReq: (proxyRequest, incomingRequest, res) => {
|
|
609
|
-
var _incomingRequest$path, _incomingRequest$path2
|
|
766
|
+
var _incomingRequest$path, _incomingRequest$path2;
|
|
610
767
|
(0, _configureProxy.applyProxyRequestHeaders)({
|
|
611
768
|
proxyRequest,
|
|
612
769
|
incomingRequest,
|
|
@@ -614,28 +771,41 @@ const RemoteServerFactory = exports.RemoteServerFactory = {
|
|
|
614
771
|
targetHost: options.slasHostName,
|
|
615
772
|
targetProtocol: 'https'
|
|
616
773
|
});
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
774
|
+
if ((_incomingRequest$path = incomingRequest.path) !== null && _incomingRequest$path !== void 0 && _incomingRequest$path.match(/\/oauth2\/trusted-agent\/token/)) {
|
|
775
|
+
// /oauth2/trusted-agent/token endpoint auth header comes from Account Manager
|
|
776
|
+
// so the SLAS private client is sent via this special header
|
|
777
|
+
proxyRequest.setHeader('_sfdc_client_auth', encodedSlasCredentials);
|
|
778
|
+
} else if ((_incomingRequest$path2 = incomingRequest.path) !== null && _incomingRequest$path2 !== void 0 && _incomingRequest$path2.match(options.applySLASPrivateClientToEndpoints)) {
|
|
779
|
+
// We pattern match and add client secrets only to endpoints that
|
|
780
|
+
// match the regex specified by options.applySLASPrivateClientToEndpoints.
|
|
781
|
+
//
|
|
782
|
+
// Other SLAS endpoints, ie. SLAS authenticate (/oauth2/login) and
|
|
783
|
+
// SLAS logout (/oauth2/logout), use the Authorization header for a different
|
|
784
|
+
// purpose so we don't want to overwrite the header for those calls.
|
|
625
785
|
proxyRequest.setHeader('Authorization', `Basic ${encodedSlasCredentials}`);
|
|
626
|
-
} else if (!((_incomingRequest$path2 = incomingRequest.path) !== null && _incomingRequest$path2 !== void 0 && _incomingRequest$path2.match(options.slasApiPath))) {
|
|
627
|
-
const message = `Request to ${incomingRequest.path} is not allowed through the SLAS Private Client Proxy`;
|
|
628
|
-
_loggerInstance.default.error(message);
|
|
629
|
-
return res.status(403).json({
|
|
630
|
-
message: message
|
|
631
|
-
});
|
|
632
786
|
}
|
|
787
|
+
},
|
|
788
|
+
onProxyRes: (0, _httpProxyMiddleware.responseInterceptor)((responseBuffer, proxyRes, req, res) => {
|
|
789
|
+
try {
|
|
790
|
+
var _req$path3;
|
|
791
|
+
// If the passwordless login endpoint returns a 404, which corresponds to a user
|
|
792
|
+
// email not being found, we mask it with a 200 OK response so that it is not
|
|
793
|
+
// obvious that the user does not exist.
|
|
794
|
+
// We do this to prevent user enumeration.
|
|
795
|
+
if ((_req$path3 = req.path) !== null && _req$path3 !== void 0 && _req$path3.match(/\/oauth2\/passwordless\/login/) && proxyRes.statusCode === 404) {
|
|
796
|
+
res.statusCode = 200;
|
|
797
|
+
res.statusMessage = 'OK';
|
|
633
798
|
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
799
|
+
// When a /passwordless/login endpoint response returns 200, it has no body
|
|
800
|
+
// so we return an empty body here to match an actual 200 response.
|
|
801
|
+
return Buffer.from('', 'utf8');
|
|
802
|
+
}
|
|
803
|
+
return responseBuffer;
|
|
804
|
+
} catch (error) {
|
|
805
|
+
console.error('There is an error processing the response from SLAS. Returning original response.', error);
|
|
806
|
+
return responseBuffer;
|
|
637
807
|
}
|
|
638
|
-
}
|
|
808
|
+
})
|
|
639
809
|
}));
|
|
640
810
|
},
|
|
641
811
|
/**
|
|
@@ -20,6 +20,11 @@ jest.mock('aws-serverless-express', () => {
|
|
|
20
20
|
proxy: jest.fn()
|
|
21
21
|
};
|
|
22
22
|
});
|
|
23
|
+
jest.mock('../../utils/ssr-config', () => {
|
|
24
|
+
return {
|
|
25
|
+
getConfig: () => {}
|
|
26
|
+
};
|
|
27
|
+
});
|
|
23
28
|
describe('the once function', () => {
|
|
24
29
|
test('should prevent a function being called more than once', () => {
|
|
25
30
|
const fn = jest.fn(() => ({
|
|
@@ -45,6 +45,11 @@ const testPackageMobify = {
|
|
|
45
45
|
proxyPath2: 'base2'
|
|
46
46
|
}
|
|
47
47
|
};
|
|
48
|
+
jest.mock('../../utils/ssr-config', () => {
|
|
49
|
+
return {
|
|
50
|
+
getConfig: () => testPackageMobify
|
|
51
|
+
};
|
|
52
|
+
});
|
|
48
53
|
const testFixtures = path.resolve(process.cwd(), 'src/ssr/server/test_fixtures');
|
|
49
54
|
|
|
50
55
|
/**
|
|
@@ -12,6 +12,7 @@ var _express = _interopRequireDefault(require("express"));
|
|
|
12
12
|
var _ssrCache = require("../../utils/ssr-cache");
|
|
13
13
|
var _ssrServer = require("../../utils/ssr-server");
|
|
14
14
|
var ssrServerUtils = _interopRequireWildcard(require("../../utils/ssr-server/utils"));
|
|
15
|
+
var ssrConfig = _interopRequireWildcard(require("../../utils/ssr-config"));
|
|
15
16
|
var _buildRemoteServer = require("./build-remote-server");
|
|
16
17
|
var _constants = require("./constants");
|
|
17
18
|
var _express2 = require("./express");
|
|
@@ -88,6 +89,8 @@ const opts = (overrides = {}) => {
|
|
|
88
89
|
};
|
|
89
90
|
const mkdtempSync = () => _fsExtra.default.mkdtempSync(_path.default.resolve(_os.default.tmpdir(), 'ssr-server-tests-'));
|
|
90
91
|
beforeAll(() => {
|
|
92
|
+
jest.spyOn(ssrConfig, 'getConfig').mockReturnValue({});
|
|
93
|
+
|
|
91
94
|
// The SSR app applies patches on creation. Those patches are specific to an
|
|
92
95
|
// environment (Lambda or not) and we need to ensure that the non-lambda patches
|
|
93
96
|
// are applied for testing. Creating and immediately discarding an app in
|
|
@@ -915,29 +918,70 @@ describe('DevServer middleware', () => {
|
|
|
915
918
|
describe('SLAS private client proxy', () => {
|
|
916
919
|
const savedEnvironment = _extends({}, process.env);
|
|
917
920
|
let proxyApp;
|
|
921
|
+
let proxyServer;
|
|
918
922
|
const proxyPort = 12345;
|
|
919
923
|
const proxyPath = '/shopper/auth/responseHeaders';
|
|
920
924
|
const slasTarget = `http://localhost:${proxyPort}${proxyPath}`;
|
|
925
|
+
const appConfig = {
|
|
926
|
+
mobify: {
|
|
927
|
+
app: {
|
|
928
|
+
commerceAPI: {
|
|
929
|
+
parameters: {
|
|
930
|
+
clientId: 'clientId',
|
|
931
|
+
shortCode: 'shortCode'
|
|
932
|
+
}
|
|
933
|
+
}
|
|
934
|
+
}
|
|
935
|
+
},
|
|
936
|
+
useSLASPrivateClient: true,
|
|
937
|
+
slasTarget: slasTarget
|
|
938
|
+
};
|
|
921
939
|
beforeAll(() => {
|
|
940
|
+
jest.spyOn(ssrConfig, 'getConfig').mockReturnValue({});
|
|
922
941
|
// by setting slasTarget, rather than forwarding the request to SLAS,
|
|
923
942
|
// we send the proxy request here so we can return the request headers
|
|
924
943
|
proxyApp = (0, _express.default)();
|
|
925
944
|
proxyApp.use(proxyPath, (req, res) => {
|
|
926
945
|
res.send(req.headers);
|
|
927
946
|
});
|
|
928
|
-
proxyApp.listen(proxyPort);
|
|
947
|
+
proxyServer = proxyApp.listen(proxyPort);
|
|
929
948
|
});
|
|
930
949
|
afterEach(() => {
|
|
931
950
|
process.env = savedEnvironment;
|
|
932
951
|
});
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
952
|
+
|
|
953
|
+
// There is a lot of cleanup done here to ensure the proxy server is closed
|
|
954
|
+
// after these tests.
|
|
955
|
+
afterAll(/*#__PURE__*/_asyncToGenerator(function* () {
|
|
956
|
+
if (proxyServer) {
|
|
957
|
+
// Close the server and wait for it to fully close
|
|
958
|
+
yield new Promise(resolve => {
|
|
959
|
+
proxyServer.close(() => {
|
|
960
|
+
resolve();
|
|
961
|
+
});
|
|
962
|
+
});
|
|
963
|
+
|
|
964
|
+
// Additional cleanup to ensure all connections are closed
|
|
965
|
+
proxyServer.unref();
|
|
966
|
+
|
|
967
|
+
// Force close any remaining connections
|
|
968
|
+
if (proxyServer._handle) {
|
|
969
|
+
proxyServer._handle.close();
|
|
970
|
+
}
|
|
971
|
+
|
|
972
|
+
// Clear any remaining event listeners
|
|
973
|
+
proxyServer.removeAllListeners();
|
|
974
|
+
}
|
|
975
|
+
|
|
976
|
+
// Clear any remaining timers or intervals
|
|
977
|
+
jest.clearAllTimers();
|
|
978
|
+
}));
|
|
936
979
|
test('should not create proxy by default', () => {
|
|
937
980
|
const app = _buildRemoteServer.RemoteServerFactory._createApp(opts());
|
|
938
981
|
return (0, _supertest.default)(app).get('/mobify/slas/private').expect(404);
|
|
939
982
|
});
|
|
940
983
|
test('should return HTTP 501 if PWA_KIT_SLAS_CLIENT_SECRET env var not set', () => {
|
|
984
|
+
delete process.env.PWA_KIT_SLAS_CLIENT_SECRET;
|
|
941
985
|
const app = _buildRemoteServer.RemoteServerFactory._createApp(opts({
|
|
942
986
|
useSLASPrivateClient: true
|
|
943
987
|
}));
|
|
@@ -945,20 +989,7 @@ describe('SLAS private client proxy', () => {
|
|
|
945
989
|
});
|
|
946
990
|
test('does not insert client secret if request not for /oauth2/token', /*#__PURE__*/_asyncToGenerator(function* () {
|
|
947
991
|
process.env.PWA_KIT_SLAS_CLIENT_SECRET = 'a secret';
|
|
948
|
-
const app = _buildRemoteServer.RemoteServerFactory._createApp(opts(
|
|
949
|
-
mobify: {
|
|
950
|
-
app: {
|
|
951
|
-
commerceAPI: {
|
|
952
|
-
parameters: {
|
|
953
|
-
clientId: 'clientId',
|
|
954
|
-
shortCode: 'shortCode'
|
|
955
|
-
}
|
|
956
|
-
}
|
|
957
|
-
}
|
|
958
|
-
},
|
|
959
|
-
useSLASPrivateClient: true,
|
|
960
|
-
slasTarget: slasTarget
|
|
961
|
-
}));
|
|
992
|
+
const app = _buildRemoteServer.RemoteServerFactory._createApp(opts(appConfig));
|
|
962
993
|
return yield (0, _supertest.default)(app).get('/mobify/slas/private/shopper/auth/v1/somePath').then(response => {
|
|
963
994
|
expect(response.body.authorization).toBeUndefined();
|
|
964
995
|
expect(response.body.host).toBe('shortCode.api.commercecloud.salesforce.com');
|
|
@@ -968,20 +999,7 @@ describe('SLAS private client proxy', () => {
|
|
|
968
999
|
test('inserts client secret if request is for /oauth2/token', /*#__PURE__*/_asyncToGenerator(function* () {
|
|
969
1000
|
process.env.PWA_KIT_SLAS_CLIENT_SECRET = 'a secret';
|
|
970
1001
|
const encodedCredentials = Buffer.from('clientId:a secret').toString('base64');
|
|
971
|
-
const app = _buildRemoteServer.RemoteServerFactory._createApp(opts(
|
|
972
|
-
mobify: {
|
|
973
|
-
app: {
|
|
974
|
-
commerceAPI: {
|
|
975
|
-
parameters: {
|
|
976
|
-
clientId: 'clientId',
|
|
977
|
-
shortCode: 'shortCode'
|
|
978
|
-
}
|
|
979
|
-
}
|
|
980
|
-
}
|
|
981
|
-
},
|
|
982
|
-
useSLASPrivateClient: true,
|
|
983
|
-
slasTarget: slasTarget
|
|
984
|
-
}));
|
|
1002
|
+
const app = _buildRemoteServer.RemoteServerFactory._createApp(opts(appConfig));
|
|
985
1003
|
return yield (0, _supertest.default)(app).get('/mobify/slas/private/shopper/auth/v1/oauth2/token').then(response => {
|
|
986
1004
|
expect(response.body.authorization).toBe(`Basic ${encodedCredentials}`);
|
|
987
1005
|
expect(response.body.host).toBe('shortCode.api.commercecloud.salesforce.com');
|
|
@@ -990,21 +1008,7 @@ describe('SLAS private client proxy', () => {
|
|
|
990
1008
|
}), 15000);
|
|
991
1009
|
test('does not add _sfdc_client_auth header if request not for /oauth2/trusted-agent/token', /*#__PURE__*/_asyncToGenerator(function* () {
|
|
992
1010
|
process.env.PWA_KIT_SLAS_CLIENT_SECRET = 'a secret';
|
|
993
|
-
const
|
|
994
|
-
const app = _buildRemoteServer.RemoteServerFactory._createApp(opts({
|
|
995
|
-
mobify: {
|
|
996
|
-
app: {
|
|
997
|
-
commerceAPI: {
|
|
998
|
-
parameters: {
|
|
999
|
-
clientId: 'clientId',
|
|
1000
|
-
shortCode: 'shortCode'
|
|
1001
|
-
}
|
|
1002
|
-
}
|
|
1003
|
-
}
|
|
1004
|
-
},
|
|
1005
|
-
useSLASPrivateClient: true,
|
|
1006
|
-
slasTarget: slasTarget
|
|
1007
|
-
}));
|
|
1011
|
+
const app = _buildRemoteServer.RemoteServerFactory._createApp(opts(appConfig));
|
|
1008
1012
|
return yield (0, _supertest.default)(app).get('/mobify/slas/private/shopper/auth/v1/oauth2/other-path').then(response => {
|
|
1009
1013
|
expect(response.body._sfdc_client_auth).toBeUndefined();
|
|
1010
1014
|
});
|
|
@@ -1029,26 +1033,169 @@ describe('SLAS private client proxy', () => {
|
|
|
1029
1033
|
}));
|
|
1030
1034
|
return yield (0, _supertest.default)(app).get('/mobify/slas/private/shopper/auth/v1/oauth2/trusted-agent/token').then(response => {
|
|
1031
1035
|
expect(response.body['_sfdc_client_auth']).toBe(encodedCredentials);
|
|
1036
|
+
expect(response.body.authorization).toBeUndefined();
|
|
1032
1037
|
expect(response.body.host).toBe('shortCode.api.commercecloud.salesforce.com');
|
|
1033
1038
|
expect(response.body['x-mobify']).toBe('true');
|
|
1034
1039
|
});
|
|
1035
1040
|
}), 15000);
|
|
1036
1041
|
test('returns 403 if request is not for /shopper/auth endpoints', /*#__PURE__*/_asyncToGenerator(function* () {
|
|
1037
1042
|
process.env.PWA_KIT_SLAS_CLIENT_SECRET = 'a secret';
|
|
1038
|
-
const app = _buildRemoteServer.RemoteServerFactory._createApp(opts(
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1043
|
+
const app = _buildRemoteServer.RemoteServerFactory._createApp(opts(appConfig));
|
|
1044
|
+
return yield (0, _supertest.default)(app).get('/mobify/slas/private/shopper/auth-admin/v1/other-path').expect(403);
|
|
1045
|
+
}), 15000);
|
|
1046
|
+
test('returns 403 if request is for /oauth2/trusted-system/* endpoint', /*#__PURE__*/_asyncToGenerator(function* () {
|
|
1047
|
+
process.env.PWA_KIT_SLAS_CLIENT_SECRET = 'a secret';
|
|
1048
|
+
const app = _buildRemoteServer.RemoteServerFactory._createApp(opts(appConfig));
|
|
1049
|
+
return yield (0, _supertest.default)(app).get('/mobify/slas/private/shopper/auth/v1/oauth2/trusted-system/token').expect(403);
|
|
1050
|
+
}), 15000);
|
|
1051
|
+
test('throws an error if /oauth2/trusted-system/* is included in applySLASPrivateClientToEndpoints', /*#__PURE__*/_asyncToGenerator(function* () {
|
|
1052
|
+
process.env.PWA_KIT_SLAS_CLIENT_SECRET = 'a secret';
|
|
1053
|
+
expect(() => {
|
|
1054
|
+
_buildRemoteServer.RemoteServerFactory._createApp(opts({
|
|
1055
|
+
mobify: {
|
|
1056
|
+
app: {
|
|
1057
|
+
commerceAPI: {
|
|
1058
|
+
parameters: {
|
|
1059
|
+
clientId: 'clientId',
|
|
1060
|
+
shortCode: 'shortCode'
|
|
1061
|
+
}
|
|
1045
1062
|
}
|
|
1046
1063
|
}
|
|
1047
|
-
}
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1064
|
+
},
|
|
1065
|
+
useSLASPrivateClient: true,
|
|
1066
|
+
slasTarget: slasTarget,
|
|
1067
|
+
applySLASPrivateClientToEndpoints: /\/oauth2\/trusted-system/
|
|
1068
|
+
}));
|
|
1069
|
+
}).toThrow('It is not allowed to include /oauth2/trusted-system endpoints in `applySLASPrivateClientToEndpoints`');
|
|
1070
|
+
}), 15000);
|
|
1071
|
+
test('proxy returns a 200 OK masking a user not found error', /*#__PURE__*/_asyncToGenerator(function* () {
|
|
1072
|
+
process.env.PWA_KIT_SLAS_CLIENT_SECRET = 'a secret';
|
|
1073
|
+
|
|
1074
|
+
// Create a new mock server specifically for this test so we can mock a response from SLAS
|
|
1075
|
+
const testProxyApp = (0, _express.default)();
|
|
1076
|
+
const testProxyPort = 12346;
|
|
1077
|
+
const testSlasTarget = `http://localhost:${testProxyPort}/shopper/auth/responseHeaders`;
|
|
1078
|
+
|
|
1079
|
+
// Set up the mock server to return a 404 for passwordless login
|
|
1080
|
+
testProxyApp.use('/shopper/auth/responseHeaders', (req, res) => {
|
|
1081
|
+
if (req.url.includes('/oauth2/passwordless/login')) {
|
|
1082
|
+
res.status(404).send();
|
|
1083
|
+
} else {
|
|
1084
|
+
res.send(req.headers);
|
|
1085
|
+
}
|
|
1086
|
+
});
|
|
1087
|
+
const testProxyServer = testProxyApp.listen(testProxyPort);
|
|
1088
|
+
try {
|
|
1089
|
+
const testAppConfig = _objectSpread(_objectSpread({}, appConfig), {}, {
|
|
1090
|
+
slasTarget: testSlasTarget
|
|
1091
|
+
});
|
|
1092
|
+
const app = _buildRemoteServer.RemoteServerFactory._createApp(opts(testAppConfig));
|
|
1093
|
+
return yield (0, _supertest.default)(app).get('/mobify/slas/private/shopper/auth/v1/oauth2/passwordless/login').expect(200).then(response => {
|
|
1094
|
+
expect(response.text).toBe('');
|
|
1095
|
+
});
|
|
1096
|
+
} finally {
|
|
1097
|
+
// Clean up the test server
|
|
1098
|
+
testProxyServer.close();
|
|
1099
|
+
}
|
|
1100
|
+
}));
|
|
1101
|
+
});
|
|
1102
|
+
describe('Base path tests', () => {
|
|
1103
|
+
test('Base path is removed from /mobify request path and still gets through to /mobify endpoint', /*#__PURE__*/_asyncToGenerator(function* () {
|
|
1104
|
+
jest.spyOn(ssrConfig, 'getConfig').mockReturnValue({
|
|
1105
|
+
envBasePath: '/basepath'
|
|
1106
|
+
});
|
|
1107
|
+
const app = _buildRemoteServer.RemoteServerFactory._createApp(opts());
|
|
1108
|
+
return (0, _supertest.default)(app).get('/basepath/mobify/ping').then(response => {
|
|
1109
|
+
expect(response.status).toBe(200);
|
|
1110
|
+
});
|
|
1111
|
+
}), 15000);
|
|
1112
|
+
test('should not remove base path from non /mobify non-express routes', /*#__PURE__*/_asyncToGenerator(function* () {
|
|
1113
|
+
// Set base path to something that might also be a site id used by react router routes
|
|
1114
|
+
jest.spyOn(ssrConfig, 'getConfig').mockReturnValue({
|
|
1115
|
+
envBasePath: '/us'
|
|
1116
|
+
});
|
|
1117
|
+
const app = _buildRemoteServer.RemoteServerFactory._createApp(opts());
|
|
1118
|
+
|
|
1119
|
+
// Add a middleware to capture the request path after base path processing
|
|
1120
|
+
let capturedPath = null;
|
|
1121
|
+
app.use((req, res, next) => {
|
|
1122
|
+
capturedPath = req.path;
|
|
1123
|
+
next();
|
|
1124
|
+
});
|
|
1125
|
+
return (0, _supertest.default)(app).get('/us/products/123').then(response => {
|
|
1126
|
+
expect(response.status).toBe(404); // 404 because the route doesn't exist in express
|
|
1127
|
+
|
|
1128
|
+
// Verify that the base path was not removed from the request path
|
|
1129
|
+
expect(capturedPath).toBe('/us/products/123');
|
|
1130
|
+
});
|
|
1131
|
+
}), 15000);
|
|
1132
|
+
test('should remove base path from routes with path parameters', /*#__PURE__*/_asyncToGenerator(function* () {
|
|
1133
|
+
jest.spyOn(ssrConfig, 'getConfig').mockReturnValue({
|
|
1134
|
+
envBasePath: '/basepath'
|
|
1135
|
+
});
|
|
1136
|
+
const app = _buildRemoteServer.RemoteServerFactory._createApp(opts());
|
|
1137
|
+
app.get('/api/users/:id', (req, res) => {
|
|
1138
|
+
res.status(200).json({
|
|
1139
|
+
userId: req.params.id
|
|
1140
|
+
});
|
|
1141
|
+
});
|
|
1142
|
+
return (0, _supertest.default)(app).get('/basepath/api/users/123').then(response => {
|
|
1143
|
+
expect(response.status).toBe(200);
|
|
1144
|
+
expect(response.body.userId).toBe('123');
|
|
1145
|
+
});
|
|
1146
|
+
}), 15000);
|
|
1147
|
+
test('should remove base path from routes defined with regex', /*#__PURE__*/_asyncToGenerator(function* () {
|
|
1148
|
+
jest.spyOn(ssrConfig, 'getConfig').mockReturnValue({
|
|
1149
|
+
envBasePath: '/basepath'
|
|
1150
|
+
});
|
|
1151
|
+
const app = _buildRemoteServer.RemoteServerFactory._createApp(opts());
|
|
1152
|
+
app.get(/\/api\/users\/\d+/, (req, res) => {
|
|
1153
|
+
// Extract the user ID from the URL path since regex routes don't create req.params automatically
|
|
1154
|
+
const match = req.path.match(/\/api\/users\/(\d+)/);
|
|
1155
|
+
const userId = match ? match[1] : 'unknown';
|
|
1156
|
+
res.status(200).json({
|
|
1157
|
+
userId: userId
|
|
1158
|
+
});
|
|
1159
|
+
});
|
|
1160
|
+
return (0, _supertest.default)(app).get('/basepath/api/users/123').then(response => {
|
|
1161
|
+
expect(response.status).toBe(200);
|
|
1162
|
+
expect(response.body.userId).toBe('123');
|
|
1163
|
+
});
|
|
1164
|
+
}), 15000);
|
|
1165
|
+
test('remove base path can handle multi-part base paths', /*#__PURE__*/_asyncToGenerator(function* () {
|
|
1166
|
+
jest.spyOn(ssrConfig, 'getConfig').mockReturnValue({
|
|
1167
|
+
envBasePath: '/my/base/path'
|
|
1168
|
+
});
|
|
1169
|
+
const app = _buildRemoteServer.RemoteServerFactory._createApp(opts());
|
|
1170
|
+
app.get('/api/test', (req, res) => {
|
|
1171
|
+
res.status(200).json({
|
|
1172
|
+
message: 'test'
|
|
1173
|
+
});
|
|
1174
|
+
});
|
|
1175
|
+
return (0, _supertest.default)(app).get('/my/base/path/api/test').then(response => {
|
|
1176
|
+
expect(response.status).toBe(200);
|
|
1177
|
+
expect(response.body.message).toBe('test');
|
|
1178
|
+
});
|
|
1179
|
+
}), 15000);
|
|
1180
|
+
test('should handle optional characters in route pattern', /*#__PURE__*/_asyncToGenerator(function* () {
|
|
1181
|
+
jest.spyOn(ssrConfig, 'getConfig').mockReturnValue({
|
|
1182
|
+
envBasePath: '/basepath'
|
|
1183
|
+
});
|
|
1184
|
+
const app = _buildRemoteServer.RemoteServerFactory._createApp(opts());
|
|
1185
|
+
|
|
1186
|
+
// This route is intentionally made complex to test the following:
|
|
1187
|
+
// 1. Optional characters in route pattern ie. 'k?'
|
|
1188
|
+
// 2. Optional characters in route pattern with groups ie. (c)?
|
|
1189
|
+
// 3. Optional characters in route pattern with path parameters ie. (:param?)
|
|
1190
|
+
// 4. Wildcards ie. '*'
|
|
1191
|
+
app.get('/callba(c)?k?*/:param?', (req, res) => {
|
|
1192
|
+
res.status(200).json({
|
|
1193
|
+
message: 'test'
|
|
1194
|
+
});
|
|
1195
|
+
});
|
|
1196
|
+
return (0, _supertest.default)(app).get('/basepath/callback').then(response => {
|
|
1197
|
+
expect(response.status).toBe(200);
|
|
1198
|
+
expect(response.body.message).toBe('test');
|
|
1199
|
+
});
|
|
1053
1200
|
}), 15000);
|
|
1054
1201
|
});
|
|
@@ -26,16 +26,18 @@ const defaultPwaKitSecurityHeaders = (req, res, next) => {
|
|
|
26
26
|
/** CSP-compatible origin for Runtime Admin. */
|
|
27
27
|
// localhost doesn't include a protocol because different browsers behave differently :\
|
|
28
28
|
const runtimeAdmin = (0, _ssrServer.isRemote)() ? 'https://runtime.commercecloud.com' : 'localhost:*';
|
|
29
|
+
const siteDotCom = '*.site.com';
|
|
29
30
|
/**
|
|
30
31
|
* Map of directive names/values that are required for PWA Kit to work. Array values will be
|
|
31
32
|
* merged with user-provided values; boolean values will replace user-provided values.
|
|
32
33
|
* @type Object.<string, string[] | boolean>
|
|
33
34
|
*/
|
|
34
35
|
const directives = {
|
|
35
|
-
'connect-src': ["'self'", runtimeAdmin],
|
|
36
|
+
'connect-src': ["'self'", runtimeAdmin, '*.salesforce-scrt.com'],
|
|
37
|
+
'frame-src': [siteDotCom],
|
|
36
38
|
'frame-ancestors': [runtimeAdmin],
|
|
37
39
|
'img-src': ["'self'", 'data:'],
|
|
38
|
-
'script-src': ["'self'", "'unsafe-eval'", runtimeAdmin],
|
|
40
|
+
'script-src': ["'self'", "'unsafe-eval'", runtimeAdmin, siteDotCom],
|
|
39
41
|
// Always upgrade insecure requests when deployed, never upgrade on local dev server
|
|
40
42
|
'upgrade-insecure-requests': (0, _ssrServer.isRemote)()
|
|
41
43
|
};
|
|
@@ -9,6 +9,11 @@ var _security = require("./security");
|
|
|
9
9
|
* For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/BSD-3-Clause
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
|
+
jest.mock('../ssr-config', () => {
|
|
13
|
+
return {
|
|
14
|
+
getConfig: () => {}
|
|
15
|
+
};
|
|
16
|
+
});
|
|
12
17
|
describe('Content-Security-Policy enforcement', () => {
|
|
13
18
|
let res;
|
|
14
19
|
|
|
@@ -38,18 +43,18 @@ describe('Content-Security-Policy enforcement', () => {
|
|
|
38
43
|
test('adds required directives for development', () => {
|
|
39
44
|
(0, _security.defaultPwaKitSecurityHeaders)({}, res, () => {});
|
|
40
45
|
res.setHeader(_constants.CONTENT_SECURITY_POLICY, '');
|
|
41
|
-
expectDirectives(["connect-src 'self' localhost:*", 'frame-ancestors localhost:*', "img-src 'self' data:", "script-src 'self' 'unsafe-eval' localhost:*"]);
|
|
46
|
+
expectDirectives(["connect-src 'self' localhost:* *.salesforce-scrt.com", 'frame-src *.site.com', 'frame-ancestors localhost:*', "img-src 'self' data:", "script-src 'self' 'unsafe-eval' localhost:* *.site.com"]);
|
|
42
47
|
});
|
|
43
48
|
test('adds required directives for production', () => {
|
|
44
49
|
mockProduction();
|
|
45
50
|
(0, _security.defaultPwaKitSecurityHeaders)({}, res, () => {});
|
|
46
51
|
res.setHeader(_constants.CONTENT_SECURITY_POLICY, '');
|
|
47
|
-
expectDirectives(["connect-src 'self' https://runtime.commercecloud.com", 'frame-ancestors https://runtime.commercecloud.com', "img-src 'self' data:", "script-src 'self' 'unsafe-eval' https://runtime.commercecloud.com", 'upgrade-insecure-requests']);
|
|
52
|
+
expectDirectives(["connect-src 'self' https://runtime.commercecloud.com *.salesforce-scrt.com", 'frame-ancestors https://runtime.commercecloud.com', "img-src 'self' data:", "script-src 'self' 'unsafe-eval' https://runtime.commercecloud.com *.site.com", 'upgrade-insecure-requests']);
|
|
48
53
|
});
|
|
49
54
|
test('merges with existing CSP directives', () => {
|
|
50
55
|
(0, _security.defaultPwaKitSecurityHeaders)({}, res, () => {});
|
|
51
56
|
res.setHeader(_constants.CONTENT_SECURITY_POLICY, "connect-src test:* ; script-src 'unsafe-eval' test:*");
|
|
52
|
-
expectDirectives(["connect-src test:* 'self' localhost:*", "script-src 'unsafe-eval' test:* 'self' localhost:*"]);
|
|
57
|
+
expectDirectives(["connect-src test:* 'self' localhost:* *.salesforce-scrt.com", "script-src 'unsafe-eval' test:* 'self' localhost:* *.site.com", 'frame-src *.site.com', 'frame-ancestors localhost:*', "img-src 'self' data:"]);
|
|
53
58
|
});
|
|
54
59
|
test('allows other CSP directives', () => {
|
|
55
60
|
(0, _security.defaultPwaKitSecurityHeaders)({}, res, () => {});
|
|
@@ -3,9 +3,12 @@
|
|
|
3
3
|
Object.defineProperty(exports, "__esModule", {
|
|
4
4
|
value: true
|
|
5
5
|
});
|
|
6
|
-
exports.ssrNamespace = exports.slasPrivateProxyPath = exports.proxyBasePath = exports.healthCheckPath = exports.cachingBasePath = exports.bundleBasePath = void 0;
|
|
6
|
+
exports.ssrNamespace = exports.slasPrivateProxyPath = exports.proxyBasePath = exports.healthCheckPath = exports.getEnvBasePath = exports.cachingBasePath = exports.bundleBasePath = void 0;
|
|
7
|
+
var _ssrConfig = require("./ssr-config");
|
|
8
|
+
var _loggerInstance = _interopRequireDefault(require("./logger-instance"));
|
|
9
|
+
function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
|
|
7
10
|
/*
|
|
8
|
-
* Copyright (c)
|
|
11
|
+
* Copyright (c) 2025, salesforce.com, inc.
|
|
9
12
|
* All rights reserved.
|
|
10
13
|
* SPDX-License-Identifier: BSD-3-Clause
|
|
11
14
|
* For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/BSD-3-Clause
|
|
@@ -14,10 +17,12 @@ exports.ssrNamespace = exports.slasPrivateProxyPath = exports.proxyBasePath = ex
|
|
|
14
17
|
/**
|
|
15
18
|
* This file defines the /mobify paths used to set up our Express endpoints.
|
|
16
19
|
*
|
|
17
|
-
* If a
|
|
18
|
-
*
|
|
20
|
+
* If a base path for the /mobify paths is defined, the methods in here will return the
|
|
21
|
+
* basepath. ie. /basepath/mobify/...
|
|
19
22
|
*/
|
|
20
23
|
|
|
24
|
+
// The MOBIFY_PATH is defined separately in preparation for the future eventual removal or
|
|
25
|
+
// replacement of the 'mobify' part of these paths
|
|
21
26
|
const MOBIFY_PATH = '/mobify';
|
|
22
27
|
const PROXY_PATH_BASE = `${MOBIFY_PATH}/proxy`;
|
|
23
28
|
const BUNDLE_PATH_BASE = `${MOBIFY_PATH}/bundle`;
|
|
@@ -25,23 +30,49 @@ const CACHING_PATH_BASE = `${MOBIFY_PATH}/caching`;
|
|
|
25
30
|
const HEALTHCHECK_PATH = `${MOBIFY_PATH}/ping`;
|
|
26
31
|
const SLAS_PRIVATE_CLIENT_PROXY_PATH = `${MOBIFY_PATH}/slas/private`;
|
|
27
32
|
|
|
28
|
-
|
|
29
|
-
*
|
|
33
|
+
/*
|
|
34
|
+
* Returns the base path. This is prepended to a /mobify path.
|
|
35
|
+
* Returns an empty string if the base path is not set or is '/'.
|
|
30
36
|
*/
|
|
31
|
-
const
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
37
|
+
const getEnvBasePath = () => {
|
|
38
|
+
const config = (0, _ssrConfig.getConfig)();
|
|
39
|
+
let basePath = (config === null || config === void 0 ? void 0 : config.envBasePath) || '';
|
|
40
|
+
if (typeof basePath !== 'string') {
|
|
41
|
+
_loggerInstance.default.warn('Invalid envBasePath configuration. No base path is applied.', {
|
|
42
|
+
namespace: 'ssr-namespace-paths.getEnvBasePath'
|
|
43
|
+
});
|
|
44
|
+
return '';
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Normalize the base path
|
|
48
|
+
basePath = basePath.trim().replace(/^\/?/, '/') // Ensure leading slash
|
|
49
|
+
.replace(/\/+/g, '/') // Normalize multiple slashes
|
|
50
|
+
.replace(/\/$/, ''); // Remove trailing slash
|
|
51
|
+
|
|
52
|
+
// Return empty string for root path or empty result
|
|
53
|
+
if (basePath === '/' || !basePath) {
|
|
54
|
+
return '';
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// only allow simple, safe characters
|
|
58
|
+
// eslint-disable-next-line no-useless-escape
|
|
59
|
+
if (!/^\/[a-zA-Z0-9\-_\/]*$/.test(basePath)) {
|
|
60
|
+
_loggerInstance.default.warn('Invalid envBasePath configuration. Only letters, numbers, hyphens, underscores, and slashes allowed. No base path is applied.', {
|
|
61
|
+
namespace: 'ssr-namespace-paths.getEnvBasePath'
|
|
62
|
+
});
|
|
63
|
+
return '';
|
|
64
|
+
}
|
|
65
|
+
return basePath;
|
|
41
66
|
};
|
|
42
|
-
|
|
43
|
-
const proxyBasePath = exports.proxyBasePath =
|
|
44
|
-
const bundleBasePath = exports.bundleBasePath =
|
|
45
|
-
const cachingBasePath = exports.cachingBasePath =
|
|
46
|
-
const healthCheckPath = exports.healthCheckPath =
|
|
47
|
-
const slasPrivateProxyPath = exports.slasPrivateProxyPath =
|
|
67
|
+
exports.getEnvBasePath = getEnvBasePath;
|
|
68
|
+
const proxyBasePath = exports.proxyBasePath = PROXY_PATH_BASE;
|
|
69
|
+
const bundleBasePath = exports.bundleBasePath = BUNDLE_PATH_BASE;
|
|
70
|
+
const cachingBasePath = exports.cachingBasePath = CACHING_PATH_BASE;
|
|
71
|
+
const healthCheckPath = exports.healthCheckPath = HEALTHCHECK_PATH;
|
|
72
|
+
const slasPrivateProxyPath = exports.slasPrivateProxyPath = SLAS_PRIVATE_CLIENT_PROXY_PATH;
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* @deprecated This variable is no longer used. This variable has always been an empty string.
|
|
76
|
+
* Use getEnvBasePath() instead. Import from @salesforce/pwa-kit-runtime/utils/ssr-namespace-paths
|
|
77
|
+
*/
|
|
78
|
+
const ssrNamespace = exports.ssrNamespace = '';
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
var _ssrNamespacePaths = require("./ssr-namespace-paths");
|
|
4
|
+
var ssrConfig = _interopRequireWildcard(require("./ssr-config"));
|
|
5
|
+
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); }
|
|
6
|
+
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; }
|
|
7
|
+
/*
|
|
8
|
+
* Copyright (c) 2024, Salesforce, Inc.
|
|
9
|
+
* All rights reserved.
|
|
10
|
+
* SPDX-License-Identifier: BSD-3-Clause
|
|
11
|
+
* For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/BSD-3-Clause
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
jest.mock('./ssr-config');
|
|
15
|
+
describe('ssr-namespace-paths tests', () => {
|
|
16
|
+
test('getEnvBasePath returns base path from config', () => {
|
|
17
|
+
jest.spyOn(ssrConfig, 'getConfig').mockReturnValue({
|
|
18
|
+
envBasePath: '/sample'
|
|
19
|
+
});
|
|
20
|
+
expect((0, _ssrNamespacePaths.getEnvBasePath)()).toBe('/sample');
|
|
21
|
+
});
|
|
22
|
+
test('getEnvBasePath returns empty string if no base path is set', () => {
|
|
23
|
+
jest.spyOn(ssrConfig, 'getConfig').mockReturnValue({});
|
|
24
|
+
expect((0, _ssrNamespacePaths.getEnvBasePath)()).toBe('');
|
|
25
|
+
});
|
|
26
|
+
test('getEnvBasePath returns empty string if envBasePath is not a string', () => {
|
|
27
|
+
jest.spyOn(ssrConfig, 'getConfig').mockReturnValue({
|
|
28
|
+
envBasePath: 123
|
|
29
|
+
});
|
|
30
|
+
expect((0, _ssrNamespacePaths.getEnvBasePath)()).toBe('');
|
|
31
|
+
});
|
|
32
|
+
test('getEnvBasePath removes trailing slash', () => {
|
|
33
|
+
jest.spyOn(ssrConfig, 'getConfig').mockReturnValue({
|
|
34
|
+
envBasePath: '/sample/'
|
|
35
|
+
});
|
|
36
|
+
expect((0, _ssrNamespacePaths.getEnvBasePath)()).toBe('/sample');
|
|
37
|
+
});
|
|
38
|
+
test('getEnvBasePath returns empty string if invalid cahracters are detected in envBasePath', () => {
|
|
39
|
+
jest.spyOn(ssrConfig, 'getConfig').mockReturnValue({
|
|
40
|
+
envBasePath: '/sample.*'
|
|
41
|
+
});
|
|
42
|
+
expect((0, _ssrNamespacePaths.getEnvBasePath)()).toBe('');
|
|
43
|
+
});
|
|
44
|
+
test('getEnvBasePath normalizes envBasePath', () => {
|
|
45
|
+
jest.spyOn(ssrConfig, 'getConfig').mockReturnValue({
|
|
46
|
+
envBasePath: ' //sample/ '
|
|
47
|
+
});
|
|
48
|
+
expect((0, _ssrNamespacePaths.getEnvBasePath)()).toBe('/sample');
|
|
49
|
+
});
|
|
50
|
+
test('getEnvBasePath works with multiple part base path', () => {
|
|
51
|
+
jest.spyOn(ssrConfig, 'getConfig').mockReturnValue({
|
|
52
|
+
envBasePath: '//test/sample/ '
|
|
53
|
+
});
|
|
54
|
+
expect((0, _ssrNamespacePaths.getEnvBasePath)()).toBe('/test/sample');
|
|
55
|
+
});
|
|
56
|
+
});
|
|
@@ -10,6 +10,7 @@ var _ssrShared = require("../ssr-shared");
|
|
|
10
10
|
var _processExpressResponse = require("./process-express-response");
|
|
11
11
|
var _utils = require("./utils");
|
|
12
12
|
var _loggerInstance = _interopRequireDefault(require("../logger-instance"));
|
|
13
|
+
var _ssrNamespacePaths = require("../ssr-namespace-paths");
|
|
13
14
|
function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
|
|
14
15
|
/*
|
|
15
16
|
* Copyright (c) 2022, Salesforce, Inc.
|
|
@@ -239,10 +240,13 @@ const configureProxy = ({
|
|
|
239
240
|
(0, _processExpressResponse.processExpressResponse)(proxyResponse);
|
|
240
241
|
}
|
|
241
242
|
},
|
|
242
|
-
// Rewrite the request's path to remove the /mobify/proxy/...
|
|
243
|
-
//
|
|
244
|
-
pathRewrite
|
|
245
|
-
|
|
243
|
+
// Rewrite the request's path to remove the /mobify/proxy/... prefix.
|
|
244
|
+
// This cannot be modified by any express middleware
|
|
245
|
+
// So we need to use the built in pathRewrite to remove the base path if present
|
|
246
|
+
pathRewrite: path => {
|
|
247
|
+
const basePathRegexEntry = (0, _ssrNamespacePaths.getEnvBasePath)() ? `${(0, _ssrNamespacePaths.getEnvBasePath)()}?` : '';
|
|
248
|
+
const regex = new RegExp(`^${basePathRegexEntry}${proxyPath}`);
|
|
249
|
+
return path.replace(regex, '');
|
|
246
250
|
},
|
|
247
251
|
// The origin (protocol + host) to which we proxy
|
|
248
252
|
target: targetOrigin
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
Object.defineProperty(exports, "__esModule", {
|
|
4
|
+
value: true
|
|
5
|
+
});
|
|
6
|
+
exports.convertExpressRouteToRegex = void 0;
|
|
7
|
+
/*
|
|
8
|
+
* Copyright (c) 2025, Salesforce, Inc.
|
|
9
|
+
* All rights reserved.
|
|
10
|
+
* SPDX-License-Identifier: BSD-3-Clause
|
|
11
|
+
* For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/BSD-3-Clause
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Utillty function that converts an express route pattern into a regex.
|
|
16
|
+
*
|
|
17
|
+
* This is used in build-remote-server's removeBasePathMiddleware to check
|
|
18
|
+
* whether an incoming request path will match a registered Express endpoint.
|
|
19
|
+
*
|
|
20
|
+
* @param {string} routePattern - The express route pattern to convert
|
|
21
|
+
* @returns {RegExp} - The regex that corresponds to the express route pattern
|
|
22
|
+
* @throws {Error} - If the route pattern is invalid
|
|
23
|
+
*/
|
|
24
|
+
const convertExpressRouteToRegex = routePattern => {
|
|
25
|
+
if (!routePattern) return null;
|
|
26
|
+
if (routePattern instanceof RegExp) return routePattern;
|
|
27
|
+
if (typeof routePattern !== 'string') return null;
|
|
28
|
+
try {
|
|
29
|
+
// If it's a string, it's an Express route pattern that needs conversion
|
|
30
|
+
// Express route patterns can contain:
|
|
31
|
+
// - Static paths: /users, /about
|
|
32
|
+
// - Route parameters: :id, :userId
|
|
33
|
+
// - Optional parameters: :id?
|
|
34
|
+
// - Regex constraints: :id(\d+)
|
|
35
|
+
// - Wildcards: *, /*
|
|
36
|
+
// - Optional characters: abc?
|
|
37
|
+
// - Optional groups: (abc)?
|
|
38
|
+
|
|
39
|
+
let regexPattern = routePattern;
|
|
40
|
+
|
|
41
|
+
// Step 1: Handle regex constraints in parameters like :param(regex)
|
|
42
|
+
// Example: /search/:query(\d+) -> /search/(\d+)
|
|
43
|
+
// Store the constraints to prevent them from being escaped later
|
|
44
|
+
const constraints = [];
|
|
45
|
+
regexPattern = regexPattern.replace(/:([^(/]+)\(([^)]+)\)/g, (match, paramName, constraint) => {
|
|
46
|
+
const constraintId = `__CONSTRAINT_${constraints.length}__`;
|
|
47
|
+
constraints.push(constraint);
|
|
48
|
+
return constraintId;
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
// Step 2: Handle complex optional parameter sequences first
|
|
52
|
+
// For patterns like /api/:version?/users/:id?/posts/:postId?
|
|
53
|
+
// We need to make literal segments optional when they're followed by optional parameters
|
|
54
|
+
regexPattern = regexPattern.replace(/\/([a-zA-Z0-9_-]+)\/:([^(/]+)\?/g, (match, segment, param) => `(?:/${segment}(?:/[^/]+)?)?`);
|
|
55
|
+
|
|
56
|
+
// Step 3: Handle remaining optional parameters :param?
|
|
57
|
+
// For /users/:id?, we want to match both /users and /users/123
|
|
58
|
+
// So we need to replace the entire pattern, not just the parameter
|
|
59
|
+
regexPattern = regexPattern.replace(/\/:([^(/]+)\?/g, '(?:/[^/]+)?');
|
|
60
|
+
|
|
61
|
+
// Step 4: Handle regular parameters :param
|
|
62
|
+
regexPattern = regexPattern.replace(/:([^(/]+)/g, '[^/]+');
|
|
63
|
+
|
|
64
|
+
// Step 5: Handle wildcards
|
|
65
|
+
// Express wildcards * should be converted to .* which matches everything including slashes
|
|
66
|
+
// Handle /* first, then handle standalone * (but not if it's already been converted)
|
|
67
|
+
regexPattern = regexPattern.replace(/\/\*/g, '/.*');
|
|
68
|
+
// Handle standalone * that hasn't been converted yet
|
|
69
|
+
regexPattern = regexPattern.replace(/(?<!\.)\*(?!\*)/g, '.*');
|
|
70
|
+
|
|
71
|
+
// Step 6: Handle wildcard + optional parameter combinations
|
|
72
|
+
// For example /users*/:id?
|
|
73
|
+
regexPattern = regexPattern.replace(
|
|
74
|
+
// eslint-disable-next-line no-useless-escape
|
|
75
|
+
/\.\*\/\(\?\:\/\[(\^\/)\]\+\)\?/g, '.*(?:/(?:[^/]+)?)?');
|
|
76
|
+
|
|
77
|
+
// Step 7: Handle optional groups (user|admin) -> (?:user|admin)
|
|
78
|
+
regexPattern = regexPattern.replace(/\(([^)]*\|[^)]*)\)/g, '(?:$1)');
|
|
79
|
+
|
|
80
|
+
// Step 8: Handle optional characters in literal strings
|
|
81
|
+
// For patterns like /favori?te, /colou?r, /analy?se
|
|
82
|
+
// The ? makes the preceding character optional
|
|
83
|
+
regexPattern = regexPattern.replace(/([a-zA-Z0-9])\?/g, (match, char) => `(?:${char})?`);
|
|
84
|
+
|
|
85
|
+
// Step 9: Fix double slashes that may have been created
|
|
86
|
+
regexPattern = regexPattern.replace(/\/\//g, '/');
|
|
87
|
+
|
|
88
|
+
// Step 10: Restore regex constraints without escaping
|
|
89
|
+
constraints.forEach((constraint, index) => {
|
|
90
|
+
const constraintId = `__CONSTRAINT_${index}__`;
|
|
91
|
+
regexPattern = regexPattern.replace(constraintId, `(${constraint})`);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
// Step 10.5: Handle optional parameters with regex constraints
|
|
95
|
+
// For patterns like /users/:id(\d+)?, we need to make the entire constraint group optional
|
|
96
|
+
// This step must be applied after the constraints are restored but before root path handling
|
|
97
|
+
// Only apply to actual constraint groups, not already processed optional groups
|
|
98
|
+
regexPattern = regexPattern.replace(/\(([^)]+)\)\?/g, (match, content) => {
|
|
99
|
+
// Only convert if this looks like a regex constraint (contains regex patterns like \d, \w, etc.)
|
|
100
|
+
// and is not already an optional group (doesn't start with ?:)
|
|
101
|
+
if ((/\\[dwDsS]/.test(content) || /[^a-zA-Z0-9\s]/.test(content)) && !content.startsWith('?:')) {
|
|
102
|
+
return `(?:${content})?`;
|
|
103
|
+
}
|
|
104
|
+
return match;
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
// Step 11: Only escape literal characters that need escaping, but not regex constraints
|
|
108
|
+
// Don't escape curly braces {} as they're used in regex quantifiers
|
|
109
|
+
// eslint-disable-next-line no-useless-escape
|
|
110
|
+
regexPattern = regexPattern.replace(/[\$]/g, '\\$&');
|
|
111
|
+
|
|
112
|
+
// Step 12: Handle root path optional parameters
|
|
113
|
+
// For patterns like /:id? or /*, we need to handle the root path correctly
|
|
114
|
+
if (regexPattern === '^(?:/[^/]+)?$') {
|
|
115
|
+
// This is a root optional parameter, should match both / and /something
|
|
116
|
+
regexPattern = '^(?:/|/[^/]+)$';
|
|
117
|
+
} else if (regexPattern === '^/.*$') {
|
|
118
|
+
// This is a root wildcard, should match everything including /
|
|
119
|
+
regexPattern = '^/.*$';
|
|
120
|
+
}
|
|
121
|
+
return new RegExp(`^${regexPattern}$`);
|
|
122
|
+
} catch (error) {
|
|
123
|
+
throw new Error(`Invalid route pattern: ${routePattern}`);
|
|
124
|
+
}
|
|
125
|
+
};
|
|
126
|
+
exports.convertExpressRouteToRegex = convertExpressRouteToRegex;
|
|
@@ -9,6 +9,11 @@ function _extends() { return _extends = Object.assign ? Object.assign.bind() : f
|
|
|
9
9
|
* SPDX-License-Identifier: BSD-3-Clause
|
|
10
10
|
* For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/BSD-3-Clause
|
|
11
11
|
*/
|
|
12
|
+
jest.mock('../ssr-config', () => {
|
|
13
|
+
return {
|
|
14
|
+
getConfig: () => {}
|
|
15
|
+
};
|
|
16
|
+
});
|
|
12
17
|
describe.each([[true], [false]])('Utils remote/local tests (isRemote: %p)', isRemote => {
|
|
13
18
|
let originalEnv;
|
|
14
19
|
const bundleId = 'test-bundle-id-12345';
|