@salesforce/pwa-kit-runtime 3.12.0-preview.1 → 3.12.0-preview.3
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": "3.12.0-preview.
|
|
3
|
+
"version": "3.12.0-preview.3",
|
|
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.12.0-preview.
|
|
49
|
+
"@salesforce/pwa-kit-dev": "3.12.0-preview.3",
|
|
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.12.0-preview.
|
|
53
|
+
"internal-lib-build": "3.12.0-preview.3",
|
|
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.12.0-preview.
|
|
61
|
+
"@salesforce/pwa-kit-dev": "3.12.0-preview.3"
|
|
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": "36ffe8203fe8661e07571b1e6035ee5a3c9d310f"
|
|
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; }
|
|
@@ -179,7 +180,7 @@ const RemoteServerFactory = exports.RemoteServerFactory = {
|
|
|
179
180
|
* @private
|
|
180
181
|
*/
|
|
181
182
|
_isBundleOrProxyPath(url) {
|
|
182
|
-
return url.
|
|
183
|
+
return url.startsWith(_ssrNamespacePaths.proxyBasePath) || url.startsWith(_ssrNamespacePaths.bundleBasePath);
|
|
183
184
|
},
|
|
184
185
|
/**
|
|
185
186
|
* @private
|
|
@@ -300,7 +301,12 @@ const RemoteServerFactory = exports.RemoteServerFactory = {
|
|
|
300
301
|
// We want to remove any base paths that have made it this far.
|
|
301
302
|
// Base paths are used to route requests to the correct server.
|
|
302
303
|
// If the request has reached the server, it is no longer needed.
|
|
303
|
-
this
|
|
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
|
+
}
|
|
304
310
|
|
|
305
311
|
// Ordering of the next two calls are vital - we don't
|
|
306
312
|
// want request-processors applied to development views.
|
|
@@ -402,85 +408,118 @@ const RemoteServerFactory = exports.RemoteServerFactory = {
|
|
|
402
408
|
/**
|
|
403
409
|
* @private
|
|
404
410
|
*/
|
|
405
|
-
|
|
406
|
-
//
|
|
407
|
-
|
|
408
|
-
// initialize this middleware.
|
|
409
|
-
if ((0, _ssrNamespacePaths.getEnvBasePath)()) {
|
|
410
|
-
const removeBasePathFromPath = path => {
|
|
411
|
-
const regex = new RegExp(`^${(0, _ssrNamespacePaths.getEnvBasePath)()}(/|$)`);
|
|
412
|
-
return path.replace(regex, '/');
|
|
413
|
-
};
|
|
414
|
-
const _convertExpressRouteToRegex = routePattern => {
|
|
415
|
-
if (!routePattern) return null;
|
|
416
|
-
if (routePattern instanceof RegExp) return routePattern;
|
|
417
|
-
if (typeof routePattern !== 'string') return null;
|
|
411
|
+
_setupBasePathMiddleware(app) {
|
|
412
|
+
// Cache the express route regexes to avoid re-calculating them on every request.
|
|
413
|
+
let expressRouteRegexes;
|
|
418
414
|
|
|
419
|
-
|
|
420
|
-
|
|
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
|
+
};
|
|
421
438
|
|
|
422
|
-
|
|
423
|
-
|
|
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
|
+
};
|
|
424
450
|
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
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
|
+
}
|
|
429
480
|
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
*
|
|
435
|
-
* @param req {express.req} the incoming request - modified in-place
|
|
436
|
-
* @private
|
|
437
|
-
*/
|
|
438
|
-
const removeBasePathFromPathMiddleware = (req, res, next) => {
|
|
439
|
-
// Scope base path removal to /mobify routes and routes defined by the express app (ie. worker.js)
|
|
440
|
-
// This is to avoid affecting other paths where a base path might be present if it happens to
|
|
441
|
-
// be equal to a site id.
|
|
442
|
-
// For example, if you have a base path of /us and a site id of /us we don't want
|
|
443
|
-
// to remove the /us from www.example.com/us/en-US/category/...
|
|
444
|
-
|
|
445
|
-
const basePath = (0, _ssrNamespacePaths.getEnvBasePath)();
|
|
446
|
-
let shouldRemoveBasePath = false;
|
|
447
|
-
if (basePath) {
|
|
448
|
-
if (req.path.startsWith(`${basePath}/mobify`)) {
|
|
449
|
-
shouldRemoveBasePath = true;
|
|
450
|
-
}
|
|
481
|
+
// For other routes, only proceed if path actually starts with base path
|
|
482
|
+
if (!req.path.startsWith(basePath)) {
|
|
483
|
+
return next();
|
|
484
|
+
}
|
|
451
485
|
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
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
|
|
471
507
|
}
|
|
472
|
-
}
|
|
473
|
-
|
|
474
|
-
if (shouldRemoveBasePath) {
|
|
475
|
-
const updatedPath = removeBasePathFromPath(req.path);
|
|
476
|
-
const parsed = _url.default.parse(req.url);
|
|
477
|
-
parsed.pathname = updatedPath;
|
|
478
|
-
req.url = _url.default.format(parsed);
|
|
508
|
+
});
|
|
509
|
+
return false;
|
|
479
510
|
}
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
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);
|
|
484
523
|
},
|
|
485
524
|
/**
|
|
486
525
|
* @private
|
|
@@ -697,7 +736,20 @@ const RemoteServerFactory = exports.RemoteServerFactory = {
|
|
|
697
736
|
return;
|
|
698
737
|
}
|
|
699
738
|
const encodedSlasCredentials = Buffer.from(`${clientId}:${clientSecret}`).toString('base64');
|
|
700
|
-
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)({
|
|
701
753
|
target: options.slasTarget,
|
|
702
754
|
changeOrigin: true,
|
|
703
755
|
// http-proxy-middleware uses the original incoming request path to determine
|
|
@@ -709,8 +761,9 @@ const RemoteServerFactory = exports.RemoteServerFactory = {
|
|
|
709
761
|
const regex = new RegExp(`^${basePathRegexEntry}${_ssrNamespacePaths.slasPrivateProxyPath}`);
|
|
710
762
|
return path.replace(regex, '');
|
|
711
763
|
},
|
|
764
|
+
selfHandleResponse: true,
|
|
712
765
|
onProxyReq: (proxyRequest, incomingRequest, res) => {
|
|
713
|
-
var _incomingRequest$path, _incomingRequest$path2
|
|
766
|
+
var _incomingRequest$path, _incomingRequest$path2;
|
|
714
767
|
(0, _configureProxy.applyProxyRequestHeaders)({
|
|
715
768
|
proxyRequest,
|
|
716
769
|
incomingRequest,
|
|
@@ -718,31 +771,41 @@ const RemoteServerFactory = exports.RemoteServerFactory = {
|
|
|
718
771
|
targetHost: options.slasHostName,
|
|
719
772
|
targetProtocol: 'https'
|
|
720
773
|
});
|
|
721
|
-
|
|
722
|
-
// We don't want the proxy to handle any non-SLAS requests
|
|
723
|
-
// or any trusted system requests
|
|
724
|
-
if (!((_incomingRequest$path = incomingRequest.path) !== null && _incomingRequest$path !== void 0 && _incomingRequest$path.match(options.slasApiPath)) || (_incomingRequest$path2 = incomingRequest.path) !== null && _incomingRequest$path2 !== void 0 && _incomingRequest$path2.match(/\/oauth2\/trusted-system/)) {
|
|
725
|
-
const message = `Request to ${incomingRequest.path} is not allowed through the SLAS Private Client Proxy`;
|
|
726
|
-
_loggerInstance.default.error(message);
|
|
727
|
-
return res.status(403).json({
|
|
728
|
-
message: message
|
|
729
|
-
});
|
|
730
|
-
}
|
|
731
|
-
|
|
732
|
-
// We pattern match and add client secrets only to endpoints that
|
|
733
|
-
// match the regex specified by options.applySLASPrivateClientToEndpoints.
|
|
734
|
-
//
|
|
735
|
-
// Other SLAS endpoints, ie. SLAS authenticate (/oauth2/login) and
|
|
736
|
-
// SLAS logout (/oauth2/logout), use the Authorization header for a different
|
|
737
|
-
// purpose so we don't want to overwrite the header for those calls.
|
|
738
|
-
if ((_incomingRequest$path3 = incomingRequest.path) !== null && _incomingRequest$path3 !== void 0 && _incomingRequest$path3.match(options.applySLASPrivateClientToEndpoints)) {
|
|
739
|
-
proxyRequest.setHeader('Authorization', `Basic ${encodedSlasCredentials}`);
|
|
740
|
-
} else if ((_incomingRequest$path4 = incomingRequest.path) !== null && _incomingRequest$path4 !== void 0 && _incomingRequest$path4.match(/\/oauth2\/trusted-agent\/token/)) {
|
|
774
|
+
if ((_incomingRequest$path = incomingRequest.path) !== null && _incomingRequest$path !== void 0 && _incomingRequest$path.match(/\/oauth2\/trusted-agent\/token/)) {
|
|
741
775
|
// /oauth2/trusted-agent/token endpoint auth header comes from Account Manager
|
|
742
776
|
// so the SLAS private client is sent via this special header
|
|
743
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.
|
|
785
|
+
proxyRequest.setHeader('Authorization', `Basic ${encodedSlasCredentials}`);
|
|
744
786
|
}
|
|
745
|
-
}
|
|
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';
|
|
798
|
+
|
|
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;
|
|
807
|
+
}
|
|
808
|
+
})
|
|
746
809
|
}));
|
|
747
810
|
},
|
|
748
811
|
/**
|
|
@@ -1033,6 +1033,7 @@ describe('SLAS private client proxy', () => {
|
|
|
1033
1033
|
}));
|
|
1034
1034
|
return yield (0, _supertest.default)(app).get('/mobify/slas/private/shopper/auth/v1/oauth2/trusted-agent/token').then(response => {
|
|
1035
1035
|
expect(response.body['_sfdc_client_auth']).toBe(encodedCredentials);
|
|
1036
|
+
expect(response.body.authorization).toBeUndefined();
|
|
1036
1037
|
expect(response.body.host).toBe('shortCode.api.commercecloud.salesforce.com');
|
|
1037
1038
|
expect(response.body['x-mobify']).toBe('true');
|
|
1038
1039
|
});
|
|
@@ -1067,6 +1068,36 @@ describe('SLAS private client proxy', () => {
|
|
|
1067
1068
|
}));
|
|
1068
1069
|
}).toThrow('It is not allowed to include /oauth2/trusted-system endpoints in `applySLASPrivateClientToEndpoints`');
|
|
1069
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
|
+
}));
|
|
1070
1101
|
});
|
|
1071
1102
|
describe('Base path tests', () => {
|
|
1072
1103
|
test('Base path is removed from /mobify request path and still gets through to /mobify endpoint', /*#__PURE__*/_asyncToGenerator(function* () {
|
|
@@ -1079,20 +1110,23 @@ describe('Base path tests', () => {
|
|
|
1079
1110
|
});
|
|
1080
1111
|
}), 15000);
|
|
1081
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
|
|
1082
1114
|
jest.spyOn(ssrConfig, 'getConfig').mockReturnValue({
|
|
1083
|
-
envBasePath: '/
|
|
1115
|
+
envBasePath: '/us'
|
|
1084
1116
|
});
|
|
1085
1117
|
const app = _buildRemoteServer.RemoteServerFactory._createApp(opts());
|
|
1086
1118
|
|
|
1087
|
-
// Add a
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
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();
|
|
1092
1124
|
});
|
|
1093
|
-
return (0, _supertest.default)(app).get('/
|
|
1094
|
-
//
|
|
1095
|
-
|
|
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');
|
|
1096
1130
|
});
|
|
1097
1131
|
}), 15000);
|
|
1098
1132
|
test('should remove base path from routes with path parameters', /*#__PURE__*/_asyncToGenerator(function* () {
|
|
@@ -1128,7 +1162,7 @@ describe('Base path tests', () => {
|
|
|
1128
1162
|
expect(response.body.userId).toBe('123');
|
|
1129
1163
|
});
|
|
1130
1164
|
}), 15000);
|
|
1131
|
-
test('remove base path can handle
|
|
1165
|
+
test('remove base path can handle multi-part base paths', /*#__PURE__*/_asyncToGenerator(function* () {
|
|
1132
1166
|
jest.spyOn(ssrConfig, 'getConfig').mockReturnValue({
|
|
1133
1167
|
envBasePath: '/my/base/path'
|
|
1134
1168
|
});
|
|
@@ -1143,4 +1177,25 @@ describe('Base path tests', () => {
|
|
|
1143
1177
|
expect(response.body.message).toBe('test');
|
|
1144
1178
|
});
|
|
1145
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
|
+
});
|
|
1200
|
+
}), 15000);
|
|
1146
1201
|
});
|
|
@@ -38,10 +38,31 @@ const getEnvBasePath = () => {
|
|
|
38
38
|
const config = (0, _ssrConfig.getConfig)();
|
|
39
39
|
let basePath = (config === null || config === void 0 ? void 0 : config.envBasePath) || '';
|
|
40
40
|
if (typeof basePath !== 'string') {
|
|
41
|
-
_loggerInstance.default.warn('Invalid envBasePath configuration. No base path is applied.'
|
|
41
|
+
_loggerInstance.default.warn('Invalid envBasePath configuration. No base path is applied.', {
|
|
42
|
+
namespace: 'ssr-namespace-paths.getEnvBasePath'
|
|
43
|
+
});
|
|
42
44
|
return '';
|
|
43
45
|
}
|
|
44
|
-
|
|
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;
|
|
45
66
|
};
|
|
46
67
|
exports.getEnvBasePath = getEnvBasePath;
|
|
47
68
|
const proxyBasePath = exports.proxyBasePath = PROXY_PATH_BASE;
|
|
@@ -29,4 +29,28 @@ describe('ssr-namespace-paths tests', () => {
|
|
|
29
29
|
});
|
|
30
30
|
expect((0, _ssrNamespacePaths.getEnvBasePath)()).toBe('');
|
|
31
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
|
+
});
|
|
32
56
|
});
|
|
@@ -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;
|