@dwp/govuk-casa 8.0.3 → 8.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (54) hide show
  1. package/CHANGELOG.md +30 -0
  2. package/README.md +2 -0
  3. package/dist/assets/css/casa-ie8.css +1 -1
  4. package/dist/assets/css/casa.css +1 -1
  5. package/dist/casa.d.ts +210 -0
  6. package/dist/casa.js +107 -1
  7. package/dist/lib/CasaTemplateLoader.d.ts +7 -0
  8. package/dist/lib/JourneyContext.d.ts +15 -27
  9. package/dist/lib/JourneyContext.js +37 -16
  10. package/dist/lib/configuration-ingestor.d.ts +21 -139
  11. package/dist/lib/configuration-ingestor.js +39 -84
  12. package/dist/lib/configure.d.ts +6 -77
  13. package/dist/lib/configure.js +104 -40
  14. package/dist/lib/index.js +5 -1
  15. package/dist/lib/nunjucks.d.ts +1 -6
  16. package/dist/lib/nunjucks.js +1 -3
  17. package/dist/lib/utils.d.ts +21 -10
  18. package/dist/lib/utils.js +54 -8
  19. package/dist/lib/validators/dateObject.d.ts +1 -0
  20. package/dist/lib/validators/email.d.ts +2 -0
  21. package/dist/lib/validators/inArray.d.ts +2 -0
  22. package/dist/lib/validators/nino.d.ts +2 -0
  23. package/dist/lib/validators/postalAddressObject.d.ts +1 -0
  24. package/dist/lib/validators/regex.d.ts +2 -0
  25. package/dist/lib/validators/required.d.ts +4 -0
  26. package/dist/lib/validators/strlen.d.ts +2 -0
  27. package/dist/lib/validators/wordCount.d.ts +2 -0
  28. package/dist/lib/waypoint-url.js +8 -2
  29. package/dist/middleware/body-parser.js +1 -1
  30. package/dist/middleware/data.d.ts +1 -2
  31. package/dist/middleware/data.js +12 -2
  32. package/dist/middleware/post.d.ts +1 -3
  33. package/dist/middleware/post.js +2 -2
  34. package/dist/middleware/pre.d.ts +4 -2
  35. package/dist/middleware/pre.js +17 -6
  36. package/dist/middleware/progress-journey.d.ts +1 -2
  37. package/dist/middleware/progress-journey.js +2 -2
  38. package/dist/middleware/session.d.ts +1 -2
  39. package/dist/middleware/session.js +12 -6
  40. package/dist/middleware/skip-waypoint.d.ts +1 -2
  41. package/dist/middleware/skip-waypoint.js +2 -2
  42. package/dist/middleware/steer-journey.d.ts +1 -2
  43. package/dist/middleware/steer-journey.js +2 -2
  44. package/dist/middleware/validate-fields.d.ts +1 -2
  45. package/dist/middleware/validate-fields.js +2 -1
  46. package/dist/routes/journey.d.ts +1 -2
  47. package/dist/routes/journey.js +8 -8
  48. package/dist/routes/static.d.ts +1 -6
  49. package/dist/routes/static.js +6 -6
  50. package/package.json +30 -28
  51. package/views/casa/components/journey-form/README.md +3 -0
  52. package/views/casa/components/journey-form/template.njk +1 -1
  53. package/views/casa/partials/scripts.njk +1 -1
  54. package/views/casa/partials/styles.njk +2 -2
@@ -3,11 +3,14 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
3
3
  return (mod && mod.__esModule) ? mod : { "default": mod };
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
+ const express_1 = require("express");
6
7
  const express_session_1 = require("express-session");
7
8
  const path_1 = require("path");
8
9
  const module_1 = require("module");
9
10
  const cookie_parser_1 = __importDefault(require("cookie-parser"));
11
+ const path_to_regexp_1 = require("path-to-regexp");
10
12
  const dirname_cjs_1 = __importDefault(require("./dirname.cjs"));
13
+ const utils_js_1 = require("./utils.js");
11
14
  const configuration_ingestor_js_1 = __importDefault(require("./configuration-ingestor.js"));
12
15
  const nunjucks_js_1 = __importDefault(require("./nunjucks.js"));
13
16
  const static_js_1 = __importDefault(require("../routes/static.js"));
@@ -20,36 +23,20 @@ const i18n_js_1 = __importDefault(require("../middleware/i18n.js"));
20
23
  const data_js_1 = __importDefault(require("../middleware/data.js"));
21
24
  const body_parser_js_1 = __importDefault(require("../middleware/body-parser.js"));
22
25
  const csrf_js_1 = __importDefault(require("../middleware/csrf.js"));
26
+ const logger_js_1 = __importDefault(require("./logger.js"));
27
+ const log = (0, logger_js_1.default)('lib:configure');
23
28
  /**
24
- * @typedef {import('express').RequestHandler} ExpressRequestHandler
29
+ * @typedef {import('../casa').ConfigurationOptions} ConfigurationOptions
25
30
  */
26
31
  /**
27
- * @typedef {import('./index').MutableRouter} MutableRouter
32
+ * @typedef {import('../casa').ConfigurationOptions} ConfigureResult
28
33
  */
29
34
  /**
30
- * @typedef {import('./configuration-ingestor').ConfigurationOptions} ConfigurationOptions
31
- */
32
- /**
33
- * @typedef {object} ConfigureResult Result of a call to configure() function
34
- * @property {nunjucks.Environment} nunjucksEnv Nunjucks environment
35
- * @property {MutableRouter} staticRouter Router handling all static assets
36
- * @property {MutableRouter} ancillaryRouter Router handling ancillary routes
37
- * @property {MutableRouter} journeyRouter Router handling all waypoint requests
38
- * @property {ExpressRequestHandler[]} preMiddleware Middleware mounted before anything else
39
- * @property {ExpressRequestHandler[]} postMiddleware Middleware mounted after everything else
40
- * @property {ExpressRequestHandler[]} csrfMiddleware CSRF get/set middleware (useful for forms)
41
- * @property {ExpressRequestHandler} sessionMiddleware Session middleware
42
- * @property {ExpressRequestHandler[]} cookieParserMiddleware Cookie-parsing middleware
43
- * @property {ExpressRequestHandler[]} i18nMiddleware I18n preparation middleware
44
- * @property {ExpressRequestHandler} bodyParserMiddleware Body parsing middleware
45
- * @property {Function} mount Function used to mount all CASA artifacts onto an ExpressJS app
35
+ * @typedef {import('../casa').Mounter} Mounter
46
36
  */
47
37
  /**
48
38
  * Configure some middleware for use in creating a new CASA app.
49
39
  *
50
- * `mountUrl` is used to ensure the CSS content uses the correct reference to
51
- * static assets in the `govuk-frontend` module.
52
- *
53
40
  * @param {ConfigurationOptions} config Configuration options
54
41
  * @returns {ConfigureResult} Result
55
42
  */
@@ -72,7 +59,7 @@ function configure(config = {}) {
72
59
  }, pages = [], plan = null, hooks = [], plugins = [], events = [], i18n = {
73
60
  dirs: [],
74
61
  locales: ['en', 'cy'],
75
- }, } = (0, configuration_ingestor_js_1.default)(config);
62
+ }, helmetConfigurator = undefined, } = (0, configuration_ingestor_js_1.default)(config);
76
63
  // Prepare all page hooks so they are prefixed with the `journey.` scope.
77
64
  pages.forEach((page) => {
78
65
  var _a;
@@ -82,7 +69,6 @@ function configure(config = {}) {
82
69
  // Prepare a Nunjucks environment for rendering all templates.
83
70
  // Resolve priority: userland templates > CASA templates > GOVUK templates > Plugin templates
84
71
  const nunjucksEnv = (0, nunjucks_js_1.default)({
85
- mountUrl,
86
72
  views: [
87
73
  ...views,
88
74
  (0, path_1.resolve)(dirname_cjs_1.default, '../../views'),
@@ -92,8 +78,8 @@ function configure(config = {}) {
92
78
  // Prepare mandatory middleware
93
79
  // These _must_ be added to the ExpressJS application at the start and end
94
80
  // of all other middleware respectively.
95
- const preMiddleware = (0, pre_js_1.default)();
96
- const postMiddleware = (0, post_js_1.default)({ mountUrl });
81
+ const preMiddleware = (0, pre_js_1.default)({ helmetConfigurator });
82
+ const postMiddleware = (0, post_js_1.default)();
97
83
  // Prepare common middleware mounted prior to the ancillaryRouter
98
84
  const cookieParserMiddleware = (0, cookie_parser_1.default)(session.secret);
99
85
  const sessionMiddleware = (0, session_js_1.default)({
@@ -104,7 +90,6 @@ function configure(config = {}) {
104
90
  ttl: session.ttl,
105
91
  cookieSameSite: session.cookieSameSite,
106
92
  cookiePath: session.cookiePath,
107
- mountUrl,
108
93
  store: (_b = session.store) !== null && _b !== void 0 ? _b : new express_session_1.MemoryStore(),
109
94
  });
110
95
  const i18nMiddleware = (0, i18n_js_1.default)({
@@ -117,7 +102,6 @@ function configure(config = {}) {
117
102
  });
118
103
  const dataMiddleware = (0, data_js_1.default)({
119
104
  plan,
120
- mountUrl,
121
105
  events,
122
106
  });
123
107
  // Prepare form middleware and its constiuent parts
@@ -125,9 +109,7 @@ function configure(config = {}) {
125
109
  const bodyParserMiddleware = (0, body_parser_js_1.default)();
126
110
  const csrfMiddleware = (0, csrf_js_1.default)();
127
111
  // Setup router to serve up bundled static assets
128
- const staticRouter = (0, static_js_1.default)({
129
- mountUrl,
130
- });
112
+ const staticRouter = (0, static_js_1.default)();
131
113
  // Setup ancillary router default stand-alone pages.
132
114
  const ancillaryRouter = (0, ancillary_js_1.default)({
133
115
  sessionTtl: session.ttl,
@@ -138,25 +120,107 @@ function configure(config = {}) {
138
120
  pages,
139
121
  plan,
140
122
  csrfMiddleware,
141
- mountUrl,
142
123
  });
143
124
  // Mount function
144
125
  // This will mount all of these routes and middleware in the correct order on
145
126
  // the given ExpressJS app.
146
127
  // Once this is called, you will not be able to modify any of the routers as
147
128
  // they will be "sealed".
148
- const mount = (app) => {
129
+ /**
130
+ * Mounting function.
131
+ *
132
+ * @type {Mounter} mount
133
+ */
134
+ const mount = (app, { route = '/' } = {}) => {
149
135
  nunjucksEnv.express(app);
150
136
  app.set('view engine', 'njk');
137
+ // !!! DEPRECATION NOTICE !!!
138
+ // This provides a non-breaking pathway to replacing `mountUrl` with
139
+ // `req.baseUrl` in all internal route handlers/middleware for services
140
+ // that use a proxy path in their mount point.
141
+ //
142
+ // In some cases, the URL on which `app` instance is mounted might include a
143
+ // proxy path so that it can handle incoming requests that have had a path
144
+ // prepended to it by an intermediary, such as nginx. This would be common
145
+ // in a hosting environment that serves several separate applications.
146
+ //
147
+ // This bit of middleware removes that proxy path segment from the request
148
+ // so that all subsequent middleware behave as if it was never present.
149
+ //
150
+ // e.g. Where the proxy path is `my-proxy`, then a request to
151
+ // `/my-proxy/app` will be seen as `/app` in all subsequent middleware, and
152
+ // all URLs generated for the browser will use `/app`.
153
+ //
154
+ // Using `config.mountUrl` rather than `mountUrl` here to test whether the
155
+ // consumer explicitly set a `mountUrl`, in which case we're dealing with
156
+ // backwards-compatibility mode.
157
+ //
158
+ // This intervention would not be needed for apps that omit `mountUrl` as if
159
+ // so, it's assumed they're following the guidance for setting up a proxy
160
+ // as described in `docs/guides/setup-behind-a-proxy.md`.
161
+ if (config.mountUrl) {
162
+ log.warn('[DEPRECATION WARNING] Using configuration attribute, mountUrl. This will be removed in an upcoming major version');
163
+ app.use((req, res, next) => {
164
+ // Mimic what the `docs/guides/setup-behind-a-proxy.md` guidance
165
+ // recommends for stripping off any proxy prefixes to leave just the
166
+ // "mountUrl" remaining.
167
+ const originalBaseUrl = req.baseUrl;
168
+ req.baseUrl = mountUrl.replace(/\/$/, '');
169
+ // If the app has been mounted directly on the specific `mountUrl`, then
170
+ // there's nothing we need to do and can let this request pass-through.
171
+ if (req.baseUrl === originalBaseUrl) {
172
+ next();
173
+ }
174
+ else if (req.__CASA_BASE_URL_REWRITTEN__) {
175
+ delete req.__CASA_BASE_URL_REWRITTEN__;
176
+ next();
177
+ }
178
+ else {
179
+ // Issuing this call will re-run this same middleware, so we use this
180
+ // `__CASA_BASE_URL_REWRITTEN__` flag to prevent recursion.
181
+ req.__CASA_BASE_URL_REWRITTEN__ = true;
182
+ req.app.handle(req, res, next);
183
+ }
184
+ });
185
+ }
186
+ // Attach a handler to redirect requests for `/` to the first waypoint in
187
+ // the plan
188
+ if (plan) {
189
+ const re = (0, path_to_regexp_1.pathToRegexp)(`${route}`.replace(/\/+/g, '/'));
190
+ app.use(re, (req, res) => {
191
+ const reqUrl = new URL(req.url, 'https://placeholder.test/');
192
+ const reqPath = (0, utils_js_1.validateUrlPath)(`${req.baseUrl}${reqUrl.pathname}${plan.getWaypoints()[0]}`);
193
+ let reqParams = reqUrl.searchParams.toString();
194
+ reqParams = reqParams ? `?${reqParams}` : '';
195
+ res.redirect(302, `${reqPath}${reqParams}`);
196
+ });
197
+ }
198
+ // Service static assets from the `app` rather than the `router`. The router
199
+ // may contain paramaterised path segments which would mean serving static
200
+ // assets over a dynamic URL each time, thus causing lots of cache misses on
201
+ // the browser.
202
+ const sealedStaticRouter = staticRouter.seal();
151
203
  app.use(preMiddleware);
152
- app.use(staticRouter.seal());
153
- app.use(sessionMiddleware); // A session is useful to all pages, so always mounted
154
- app.use(i18nMiddleware);
155
- app.use(bodyParserMiddleware);
156
- app.use(dataMiddleware);
157
- app.use(ancillaryRouter.seal());
158
- app.use(journeyRouter.seal());
159
- app.use(postMiddleware);
204
+ app.use(sealedStaticRouter);
205
+ const router = (0, express_1.Router)({
206
+ // Required so that any parameters in the URL are propagated to middleware
207
+ mergeParams: true,
208
+ });
209
+ router.use(preMiddleware);
210
+ // !!! DEPRECATE in v9 !!! For performance reasons, static assets will
211
+ // always be handled via the `app` middleware rather than `router`.
212
+ // Anywhere `mountUrl` is used in templates to service static assets must be
213
+ // changed to use `staticMountUrl`.
214
+ // TASK: remove this line below
215
+ router.use(sealedStaticRouter);
216
+ router.use(sessionMiddleware);
217
+ router.use(i18nMiddleware);
218
+ router.use(bodyParserMiddleware);
219
+ router.use(dataMiddleware);
220
+ router.use(ancillaryRouter.seal());
221
+ router.use(journeyRouter.seal());
222
+ router.use(postMiddleware);
223
+ app.use(route, router);
160
224
  return app;
161
225
  };
162
226
  // Prepare configuration result
package/dist/lib/index.js CHANGED
@@ -5,7 +5,11 @@
5
5
  */
6
6
  var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
7
7
  if (k2 === undefined) k2 = k;
8
- Object.defineProperty(o, k2, { enumerable: true, get: function() { return m[k]; } });
8
+ var desc = Object.getOwnPropertyDescriptor(m, k);
9
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
10
+ desc = { enumerable: true, get: function() { return m[k]; } };
11
+ }
12
+ Object.defineProperty(o, k2, desc);
9
13
  }) : (function(o, m, k, k2) {
10
14
  if (k2 === undefined) k2 = k;
11
15
  o[k2] = m[k];
@@ -1,6 +1,5 @@
1
1
  /**
2
2
  * @typedef {object} NunjucksOptions
3
- * @property {string} [mountUrl=/] Mount URL (optional, default /)
4
3
  * @property {string[]} [views=[]] Template file directories (optional, default [])
5
4
  */
6
5
  /**
@@ -9,12 +8,8 @@
9
8
  * @param {NunjucksOptions} options Nunjucks options
10
9
  * @returns {Environment} Nunjucks Environment instance
11
10
  */
12
- export default function nunjucksConfig({ mountUrl, views, }: NunjucksOptions): Environment;
11
+ export default function nunjucksConfig({ views, }: NunjucksOptions): Environment;
13
12
  export type NunjucksOptions = {
14
- /**
15
- * )
16
- */
17
- mountUrl?: string | undefined;
18
13
  /**
19
14
  * Template file directories (optional, default [])
20
15
  */
@@ -11,7 +11,6 @@ const CasaTemplateLoader_js_1 = __importDefault(require("./CasaTemplateLoader.js
11
11
  const nunjucks_filters_js_1 = require("./nunjucks-filters.js");
12
12
  /**
13
13
  * @typedef {object} NunjucksOptions
14
- * @property {string} [mountUrl=/] Mount URL (optional, default /)
15
14
  * @property {string[]} [views=[]] Template file directories (optional, default [])
16
15
  */
17
16
  /**
@@ -20,7 +19,7 @@ const nunjucks_filters_js_1 = require("./nunjucks-filters.js");
20
19
  * @param {NunjucksOptions} options Nunjucks options
21
20
  * @returns {Environment} Nunjucks Environment instance
22
21
  */
23
- function nunjucksConfig({ mountUrl = '/', views = [], }) {
22
+ function nunjucksConfig({ views = [], }) {
24
23
  // Prepare a single Nunjucks environment for all responses to use. Note that
25
24
  // we cannot prepare response-specific global functions/filters if we use a
26
25
  // single environment, but the performance gains of doing so are significant.
@@ -38,7 +37,6 @@ function nunjucksConfig({ mountUrl = '/', views = [], }) {
38
37
  env.modifyBlock = loader.modifyBlock.bind(loader);
39
38
  // Globals
40
39
  // These can't be modified once set. But they can be overridden by res.locals.
41
- env.addGlobal('assetPath', `${mountUrl}govuk/assets`); // Required by govuk-frontend layout template
42
40
  env.addGlobal('casaVersion', JSON.parse((0, fs_1.readFileSync)((0, path_1.resolve)(dirname_cjs_1.default, '../../package.json'))).version);
43
41
  env.addGlobal('mergeObjects', nunjucks_filters_js_1.mergeObjects);
44
42
  env.addGlobal('includes', nunjucks_filters_js_1.includes);
@@ -1,11 +1,5 @@
1
1
  /**
2
- * @typedef {import('./configuration-ingestor').GlobalHook} GlobalHook
3
- */
4
- /**
5
- * @typedef {import('./configuration-ingestor').PageHook} PageHook
6
- */
7
- /**
8
- * @typedef {GlobalHook | PageHook} Hook
2
+ * @typedef {import('../casa').GlobalHook | import('../casa').PageHook} Hook
9
3
  */
10
4
  /**
11
5
  * Test is a value can be stringifed (numbers or strings)
@@ -40,9 +34,26 @@ export function isEmpty(val: any): boolean;
40
34
  */
41
35
  export function resolveMiddlewareHooks(hookName: string, path: string, hooks?: Hook[]): Function[];
42
36
  export function validateWaypoint(waypoint: any): void;
37
+ export function validateUrlPath(path: any): string;
43
38
  export function validateView(view: any): void;
44
39
  export function validateHookName(hookName: any): void;
45
40
  export function validateHookPath(path: any): void;
46
- export type GlobalHook = import('./configuration-ingestor').GlobalHook;
47
- export type PageHook = import('./configuration-ingestor').PageHook;
48
- export type Hook = GlobalHook | PageHook;
41
+ /**
42
+ * Checks if the given string can be used as an object key.
43
+ *
44
+ * @param {string} key Proposed Object key
45
+ * @returns {string} Same key if it's valid
46
+ * @throws {Error} if proposed key is an invalid keyword
47
+ */
48
+ export function notProto(key: string): string;
49
+ /**
50
+ * Remove any path segments from the URL that are present in the `mountpath`,
51
+ * but not in the `baseUrl`. Those segments are considered to be part of an
52
+ * internal proxying arrangement, and should not be used by CASA.
53
+ *
54
+ * @param {import('express').Request} req Express request
55
+ * @throws {Error} When multiple mountpaths are present
56
+ * @returns {string} URL path with any proxy prefixes removed
57
+ */
58
+ export function stripProxyFromUrlPath(req: import('express').Request): string;
59
+ export type Hook = import('../casa').GlobalHook | import('../casa').PageHook;
package/dist/lib/utils.js CHANGED
@@ -1,15 +1,9 @@
1
1
  "use strict";
2
2
  /**
3
- * @typedef {import('./configuration-ingestor').GlobalHook} GlobalHook
3
+ * @typedef {import('../casa').GlobalHook | import('../casa').PageHook} Hook
4
4
  */
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
- exports.validateHookPath = exports.validateHookName = exports.validateView = exports.validateWaypoint = exports.resolveMiddlewareHooks = exports.isEmpty = exports.stringifyInput = exports.isStringable = void 0;
7
- /**
8
- * @typedef {import('./configuration-ingestor').PageHook} PageHook
9
- */
10
- /**
11
- * @typedef {GlobalHook | PageHook} Hook
12
- */
6
+ exports.stripProxyFromUrlPath = exports.notProto = exports.validateHookPath = exports.validateHookName = exports.validateView = exports.validateUrlPath = exports.validateWaypoint = exports.resolveMiddlewareHooks = exports.isEmpty = exports.stringifyInput = exports.isStringable = void 0;
13
7
  /**
14
8
  * Test is a value can be stringifed (numbers or strings)
15
9
  *
@@ -81,6 +75,19 @@ function validateWaypoint(waypoint) {
81
75
  }
82
76
  }
83
77
  exports.validateWaypoint = validateWaypoint;
78
+ function validateUrlPath(path) {
79
+ if (typeof path !== 'string') {
80
+ throw new TypeError('URL path must be a string');
81
+ }
82
+ if (path.match(/[^/a-z0-9_-]/)) {
83
+ throw new SyntaxError('URL path must contain only a-z, 0-9, -, _ and / characters');
84
+ }
85
+ if (path.match(/\/{2,}/)) {
86
+ throw new SyntaxError('URL path must not contain consecutive /');
87
+ }
88
+ return path;
89
+ }
90
+ exports.validateUrlPath = validateUrlPath;
84
91
  function validateView(view) {
85
92
  if (typeof view !== 'string') {
86
93
  throw new TypeError('View must be a string');
@@ -111,3 +118,42 @@ function validateHookPath(path) {
111
118
  }
112
119
  }
113
120
  exports.validateHookPath = validateHookPath;
121
+ /**
122
+ * Checks if the given string can be used as an object key.
123
+ *
124
+ * @param {string} key Proposed Object key
125
+ * @returns {string} Same key if it's valid
126
+ * @throws {Error} if proposed key is an invalid keyword
127
+ */
128
+ function notProto(key) {
129
+ if (['__proto__', 'constructor', 'prototype'].includes(String(key).toLowerCase())) {
130
+ throw new Error('Attempt to use prototype key disallowed');
131
+ }
132
+ return key;
133
+ }
134
+ exports.notProto = notProto;
135
+ /**
136
+ * Remove any path segments from the URL that are present in the `mountpath`,
137
+ * but not in the `baseUrl`. Those segments are considered to be part of an
138
+ * internal proxying arrangement, and should not be used by CASA.
139
+ *
140
+ * @param {import('express').Request} req Express request
141
+ * @throws {Error} When multiple mountpaths are present
142
+ * @returns {string} URL path with any proxy prefixes removed
143
+ */
144
+ function stripProxyFromUrlPath(req) {
145
+ if (typeof req.app.mountpath !== 'string') {
146
+ throw new Error('CASA does not currently support multiple mountpaths');
147
+ }
148
+ let stripped = '/';
149
+ const mountPathParts = req.app.mountpath.replace(/^\/+/, '').replace(/\/+$/, '').split('/');
150
+ const baseUrlParts = req.baseUrl.replace(/^\/+/, '').replace(/\/+$/, '').split('/');
151
+ for (let i = 0, l = mountPathParts.length; i < l; i++) {
152
+ /* eslint-disable-next-line security/detect-object-injection */
153
+ if (baseUrlParts.length && mountPathParts[i] === baseUrlParts[0]) {
154
+ stripped = `${stripped}${baseUrlParts.shift()}/`;
155
+ }
156
+ }
157
+ return stripped;
158
+ }
159
+ exports.stripProxyFromUrlPath = stripProxyFromUrlPath;
@@ -1,4 +1,5 @@
1
1
  export default class DateObject extends ValidatorFactory {
2
2
  name: string;
3
+ validate(value: any, dataContext?: {}): object[];
3
4
  }
4
5
  import ValidatorFactory from "../ValidatorFactory.js";
@@ -1,4 +1,6 @@
1
1
  export default class Email extends ValidatorFactory {
2
2
  name: string;
3
+ validate(value: any, dataContext?: {}): object[];
4
+ sanitise(value: any): string | undefined;
3
5
  }
4
6
  import ValidatorFactory from "../ValidatorFactory.js";
@@ -1,4 +1,6 @@
1
1
  export default class InArray extends ValidatorFactory {
2
2
  name: string;
3
+ validate(value: any, dataContext?: {}): object[];
4
+ sanitise(value: any): string | string[] | undefined;
3
5
  }
4
6
  import ValidatorFactory from "../ValidatorFactory.js";
@@ -1,4 +1,6 @@
1
1
  export default class Nino extends ValidatorFactory {
2
2
  name: string;
3
+ validate(value: any, dataContext?: {}): object[];
4
+ sanitise(value: any): string | undefined;
3
5
  }
4
6
  import ValidatorFactory from "../ValidatorFactory.js";
@@ -1,4 +1,5 @@
1
1
  export default class PostalAddressObject extends ValidatorFactory {
2
2
  name: string;
3
+ validate(value: any, dataContext?: {}): object[];
3
4
  }
4
5
  import ValidatorFactory from "../ValidatorFactory.js";
@@ -1,4 +1,6 @@
1
1
  export default class Regex extends ValidatorFactory {
2
2
  name: string;
3
+ validate(value?: string, dataContext?: {}): object[];
4
+ sanitise(value: any): string | undefined;
3
5
  }
4
6
  import ValidatorFactory from "../ValidatorFactory.js";
@@ -1,4 +1,8 @@
1
1
  export default class Required extends ValidatorFactory {
2
2
  name: string;
3
+ validate(value: any, dataContext?: {}): object[];
4
+ sanitise(value: any): string | (string | undefined)[] | {
5
+ [k: string]: string | undefined;
6
+ } | undefined;
3
7
  }
4
8
  import ValidatorFactory from "../ValidatorFactory.js";
@@ -1,4 +1,6 @@
1
1
  export default class Strlen extends ValidatorFactory {
2
2
  name: string;
3
+ validate(inputValue?: string, dataContext?: {}): object[];
4
+ sanitise(value: any): string | undefined;
3
5
  }
4
6
  import ValidatorFactory from "../ValidatorFactory.js";
@@ -1,5 +1,7 @@
1
1
  export default class WordCount extends ValidatorFactory {
2
2
  name: string;
3
3
  count(input: any): any;
4
+ validate(inputValue?: string, dataContext?: {}): object[];
5
+ sanitise(value: any): string | undefined;
4
6
  }
5
7
  import ValidatorFactory from "../ValidatorFactory.js";
@@ -32,8 +32,14 @@ function waypointUrl({ waypoint = '', mountUrl = '/', journeyContext, edit = fal
32
32
  else {
33
33
  url.pathname = `${mountUrl}${waypoint}`;
34
34
  }
35
- // Attach context id for non-default contexts
36
- if (journeyContext && !journeyContext.isDefault() && journeyContext.identity.id) {
35
+ // Attach context ID as query parameter for non-default contexts.
36
+ // To avoid messy URLs with duplicated content, this parameter will _not_ be
37
+ // added if the context ID already appears in the url path, i.e. to avoid
38
+ // `/path/1234-abcd/waypoint?contextid=1234-abcd` scenarios
39
+ if (journeyContext
40
+ && !journeyContext.isDefault()
41
+ && journeyContext.identity.id
42
+ && !mountUrl.includes(journeyContext.identity.id)) {
37
43
  url.searchParams.append('contextid', journeyContext.identity.id);
38
44
  }
39
45
  // Attach edit mode flag
@@ -6,7 +6,7 @@ const rProto = /__proto__/i;
6
6
  const rPrototype = /prototype[='"[\]]/i;
7
7
  const rConstructor = /constructor[='"[\]]/i;
8
8
  function verifyBody(req, res, buf, encoding) {
9
- const body = decodeURI(buf.toString(encoding));
9
+ const body = decodeURI(buf.toString(encoding)).replace(/[\s\u200B-\u200D\uFEFF]/g, '');
10
10
  if (rProto.test(body)) {
11
11
  throw new Error('Request body verification failed (__proto__)');
12
12
  }
@@ -1,5 +1,4 @@
1
- export default function dataMiddleware({ plan, mountUrl, events, }: {
1
+ export default function dataMiddleware({ plan, events, }: {
2
2
  plan: any;
3
- mountUrl: any;
4
3
  events: any;
5
4
  }): ((req: any, res: any, next: any) => void)[];
@@ -7,6 +7,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
7
7
  Object.defineProperty(exports, "__esModule", { value: true });
8
8
  const lodash_1 = __importDefault(require("lodash"));
9
9
  const JourneyContext_js_1 = __importDefault(require("../lib/JourneyContext.js"));
10
+ const utils_js_1 = require("../lib/utils.js");
10
11
  const waypoint_url_js_1 = __importDefault(require("../lib/waypoint-url.js"));
11
12
  const { has } = lodash_1.default;
12
13
  const editOrigin = (req) => {
@@ -18,7 +19,7 @@ const editOrigin = (req) => {
18
19
  }
19
20
  return '';
20
21
  };
21
- function dataMiddleware({ plan, mountUrl, events, }) {
22
+ function dataMiddleware({ plan, events, }) {
22
23
  return [
23
24
  (req, res, next) => {
24
25
  /* ------------------------------------------------ Request decorations */
@@ -34,16 +35,25 @@ function dataMiddleware({ plan, mountUrl, events, }) {
34
35
  // Grab chosen language from session
35
36
  req.casa.journeyContext.nav.language = req.session.language;
36
37
  /* ------------------------------------------------- Template variables */
38
+ // Figure out the mount URL of the current request
39
+ const mountUrl = (0, utils_js_1.validateUrlPath)(`${req.baseUrl}/`.replace(/\/+/g, '/'));
40
+ // For browser performance reasons, CASA's static assets are potentially
41
+ // delivered over a different route to the `mountUrl` if this CASA app has
42
+ // been mounted on a parameterised route.
43
+ const staticMountUrl = (0, utils_js_1.validateUrlPath)(`${(0, utils_js_1.stripProxyFromUrlPath)(req)}`.replace(/\/+/g, '/'));
37
44
  // CASA and userland templates
38
45
  res.locals.casa = {
39
46
  mountUrl,
47
+ staticMountUrl,
40
48
  editMode: req.casa.editMode,
41
49
  editOrigin: req.casa.editOrigin,
42
50
  };
43
51
  res.locals.locale = req.language;
44
52
  // Used by govuk-frontend template
45
- // - req.language is provided by i18n-http-middleware
53
+ // htmlLang = req.language is provided by i18n-http-middleware
54
+ // assetPath = used for linking to static assets in the govuk-frontend module
46
55
  res.locals.htmlLang = req.language;
56
+ res.locals.assetPath = `${staticMountUrl}govuk/assets`;
47
57
  // Function for building URLs. This will be curried with the `mountUrl`,
48
58
  // `journeyContext`, `edit` and `editOrigin` for convenience. This means
49
59
  // the template author does not have to be concerned about the current
@@ -1,3 +1 @@
1
- export default function postMiddleware({ mountUrl, }: {
2
- mountUrl: any;
3
- }): ((err: any, req: any, res: any, next: any) => any)[];
1
+ export default function postMiddleware(): ((err: any, req: any, res: any, next: any) => any)[];
@@ -6,7 +6,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
6
6
  // 2 middleware: one as a fallback 404 handler, one to handle thrown errors
7
7
  const logger_js_1 = __importDefault(require("../lib/logger.js"));
8
8
  const log = (0, logger_js_1.default)('middleware:post');
9
- function postMiddleware({ mountUrl, }) {
9
+ function postMiddleware() {
10
10
  return [
11
11
  (req, res) => {
12
12
  res.status(404).render('casa/errors/404.njk');
@@ -20,7 +20,7 @@ function postMiddleware({ mountUrl, }) {
20
20
  let TEMPLATE = 'casa/errors/500.njk';
21
21
  if (!res.locals.t) {
22
22
  res.locals.t = () => ('');
23
- res.locals.casa = Object.assign(Object.assign({}, (_a = res.locals) === null || _a === void 0 ? void 0 : _a.casa), { mountUrl });
23
+ res.locals.casa = Object.assign(Object.assign({}, (_a = res.locals) === null || _a === void 0 ? void 0 : _a.casa), { mountUrl: `${req.baseUrl}/` });
24
24
  TEMPLATE = 'casa/errors/static.njk';
25
25
  }
26
26
  // CSRF token is invalid in some way
@@ -1,3 +1,5 @@
1
- /// <reference types="node" />
2
- declare function _default(): ((req: import("http").IncomingMessage, res: import("http").ServerResponse, next: (err?: unknown) => void) => void)[];
1
+ declare function _default({ helmetConfigurator, }?: {
2
+ helmetConfigurator: HelmetConfigurator;
3
+ }): Function[];
3
4
  export default _default;
5
+ export type HelmetConfigurator = import('../casa').HelmetConfigurator;
@@ -5,9 +5,20 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
6
  const crypto_1 = require("crypto");
7
7
  const helmet_1 = __importDefault(require("helmet"));
8
- const GA_DOMAIN = 'www.google-analytics.com';
8
+ const GA_DOMAIN = '*.google-analytics.com';
9
+ const GA_ANALYTICS_DOMAIN = '*.analytics.google.com';
9
10
  const GTM_DOMAIN = 'www.googletagmanager.com';
10
- exports.default = () => [
11
+ /**
12
+ * @typedef {import('../casa').HelmetConfigurator} HelmetConfigurator
13
+ */
14
+ /**
15
+ * Pre middleware.
16
+ *
17
+ * @param {object} opts Options
18
+ * @param {HelmetConfigurator} opts.helmetConfigurator Function to customise Helmet configuration
19
+ * @returns {Function[]} List of middleware
20
+ */
21
+ exports.default = ({ helmetConfigurator = (config) => (config), } = {}) => [
11
22
  // Only allow certain request methods
12
23
  (req, res, next) => {
13
24
  if (req.method !== 'GET' && req.method !== 'POST') {
@@ -37,15 +48,15 @@ exports.default = () => [
37
48
  next();
38
49
  },
39
50
  // Helmet suite of headers
40
- (0, helmet_1.default)({
51
+ (0, helmet_1.default)(helmetConfigurator({
41
52
  // Allows GA which is typically used, and a known inline script nonce
42
53
  contentSecurityPolicy: {
43
54
  useDefaults: true,
44
55
  directives: {
45
56
  'default-src': ["'none'"],
46
57
  'script-src': ["'self'", GA_DOMAIN, GTM_DOMAIN, (req, res) => `'nonce-${res.locals.cspNonce}'`],
47
- 'img-src': ["'self'", GA_DOMAIN],
48
- 'connect-src': ["'self'", GA_DOMAIN],
58
+ 'img-src': ["'self'", GA_DOMAIN, GA_ANALYTICS_DOMAIN],
59
+ 'connect-src': ["'self'", GA_DOMAIN, GA_ANALYTICS_DOMAIN],
49
60
  'frame-src': ["'self'", GTM_DOMAIN],
50
61
  'frame-ancestors': ["'self'"],
51
62
  'form-action': ["'self'"],
@@ -55,5 +66,5 @@ exports.default = () => [
55
66
  },
56
67
  // // Require referrer to aid navigation
57
68
  // referrerPolicy: 'no-referrer, same-origin',
58
- }),
69
+ })),
59
70
  ];