@dwp/govuk-casa 8.1.0 → 8.2.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. package/CHANGELOG.md +25 -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 +214 -0
  6. package/dist/casa.js +103 -0
  7. package/dist/lib/JourneyContext.d.ts +15 -27
  8. package/dist/lib/JourneyContext.js +25 -15
  9. package/dist/lib/configuration-ingestor.d.ts +16 -144
  10. package/dist/lib/configuration-ingestor.js +25 -95
  11. package/dist/lib/configure.d.ts +6 -77
  12. package/dist/lib/configure.js +58 -39
  13. package/dist/lib/nunjucks.d.ts +1 -6
  14. package/dist/lib/nunjucks.js +1 -3
  15. package/dist/lib/utils.d.ts +13 -10
  16. package/dist/lib/utils.js +40 -8
  17. package/dist/lib/waypoint-url.js +8 -2
  18. package/dist/middleware/body-parser.js +1 -1
  19. package/dist/middleware/data.d.ts +1 -2
  20. package/dist/middleware/data.js +12 -2
  21. package/dist/middleware/post.d.ts +1 -3
  22. package/dist/middleware/post.js +2 -2
  23. package/dist/middleware/pre.d.ts +1 -1
  24. package/dist/middleware/pre.js +5 -4
  25. package/dist/middleware/progress-journey.d.ts +1 -2
  26. package/dist/middleware/progress-journey.js +2 -2
  27. package/dist/middleware/session.d.ts +1 -2
  28. package/dist/middleware/session.js +7 -5
  29. package/dist/middleware/skip-waypoint.d.ts +1 -2
  30. package/dist/middleware/skip-waypoint.js +2 -2
  31. package/dist/middleware/steer-journey.d.ts +1 -2
  32. package/dist/middleware/steer-journey.js +2 -2
  33. package/dist/middleware/strip-proxy-path.d.ts +4 -0
  34. package/dist/middleware/strip-proxy-path.js +52 -0
  35. package/dist/middleware/validate-fields.d.ts +1 -2
  36. package/dist/middleware/validate-fields.js +2 -1
  37. package/dist/routes/journey.d.ts +1 -2
  38. package/dist/routes/journey.js +8 -8
  39. package/dist/routes/static.d.ts +1 -6
  40. package/dist/routes/static.js +6 -6
  41. package/package.json +25 -23
  42. package/views/casa/components/journey-form/README.md +3 -0
  43. package/views/casa/components/journey-form/template.njk +1 -1
  44. package/views/casa/partials/scripts.njk +1 -1
  45. package/views/casa/partials/styles.njk +2 -2
@@ -3,16 +3,20 @@ 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"));
14
17
  const ancillary_js_1 = __importDefault(require("../routes/ancillary.js"));
15
18
  const journey_js_1 = __importDefault(require("../routes/journey.js"));
19
+ const strip_proxy_path_js_1 = __importDefault(require("../middleware/strip-proxy-path.js"));
16
20
  const pre_js_1 = __importDefault(require("../middleware/pre.js"));
17
21
  const post_js_1 = __importDefault(require("../middleware/post.js"));
18
22
  const session_js_1 = __importDefault(require("../middleware/session.js"));
@@ -21,35 +25,17 @@ const data_js_1 = __importDefault(require("../middleware/data.js"));
21
25
  const body_parser_js_1 = __importDefault(require("../middleware/body-parser.js"));
22
26
  const csrf_js_1 = __importDefault(require("../middleware/csrf.js"));
23
27
  /**
24
- * @typedef {import('express').RequestHandler} ExpressRequestHandler
28
+ * @typedef {import('../casa').ConfigurationOptions} ConfigurationOptions
25
29
  */
26
30
  /**
27
- * @typedef {import('./index').MutableRouter} MutableRouter
31
+ * @typedef {import('../casa').ConfigurationOptions} ConfigureResult
28
32
  */
29
33
  /**
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
34
+ * @typedef {import('../casa').Mounter} Mounter
46
35
  */
47
36
  /**
48
37
  * Configure some middleware for use in creating a new CASA app.
49
38
  *
50
- * `mountUrl` is used to ensure the CSS content uses the correct reference to
51
- * static assets in the `govuk-frontend` module.
52
- *
53
39
  * @param {ConfigurationOptions} config Configuration options
54
40
  * @returns {ConfigureResult} Result
55
41
  */
@@ -61,7 +47,7 @@ function configure(config = {}) {
61
47
  plugin.configure(config);
62
48
  });
63
49
  // Extract config
64
- const { mountUrl = '/', views = [], session = {
50
+ const { mountUrl, views = [], session = {
65
51
  secret: 'secret',
66
52
  name: 'casasession',
67
53
  secure: false,
@@ -82,7 +68,6 @@ function configure(config = {}) {
82
68
  // Prepare a Nunjucks environment for rendering all templates.
83
69
  // Resolve priority: userland templates > CASA templates > GOVUK templates > Plugin templates
84
70
  const nunjucksEnv = (0, nunjucks_js_1.default)({
85
- mountUrl,
86
71
  views: [
87
72
  ...views,
88
73
  (0, path_1.resolve)(dirname_cjs_1.default, '../../views'),
@@ -93,7 +78,7 @@ function configure(config = {}) {
93
78
  // These _must_ be added to the ExpressJS application at the start and end
94
79
  // of all other middleware respectively.
95
80
  const preMiddleware = (0, pre_js_1.default)({ helmetConfigurator });
96
- const postMiddleware = (0, post_js_1.default)({ mountUrl });
81
+ const postMiddleware = (0, post_js_1.default)();
97
82
  // Prepare common middleware mounted prior to the ancillaryRouter
98
83
  const cookieParserMiddleware = (0, cookie_parser_1.default)(session.secret);
99
84
  const sessionMiddleware = (0, session_js_1.default)({
@@ -104,7 +89,6 @@ function configure(config = {}) {
104
89
  ttl: session.ttl,
105
90
  cookieSameSite: session.cookieSameSite,
106
91
  cookiePath: session.cookiePath,
107
- mountUrl,
108
92
  store: (_b = session.store) !== null && _b !== void 0 ? _b : new express_session_1.MemoryStore(),
109
93
  });
110
94
  const i18nMiddleware = (0, i18n_js_1.default)({
@@ -117,7 +101,6 @@ function configure(config = {}) {
117
101
  });
118
102
  const dataMiddleware = (0, data_js_1.default)({
119
103
  plan,
120
- mountUrl,
121
104
  events,
122
105
  });
123
106
  // Prepare form middleware and its constiuent parts
@@ -125,9 +108,7 @@ function configure(config = {}) {
125
108
  const bodyParserMiddleware = (0, body_parser_js_1.default)();
126
109
  const csrfMiddleware = (0, csrf_js_1.default)();
127
110
  // Setup router to serve up bundled static assets
128
- const staticRouter = (0, static_js_1.default)({
129
- mountUrl,
130
- });
111
+ const staticRouter = (0, static_js_1.default)();
131
112
  // Setup ancillary router default stand-alone pages.
132
113
  const ancillaryRouter = (0, ancillary_js_1.default)({
133
114
  sessionTtl: session.ttl,
@@ -138,25 +119,63 @@ function configure(config = {}) {
138
119
  pages,
139
120
  plan,
140
121
  csrfMiddleware,
141
- mountUrl,
142
122
  });
143
123
  // Mount function
144
124
  // This will mount all of these routes and middleware in the correct order on
145
125
  // the given ExpressJS app.
146
126
  // Once this is called, you will not be able to modify any of the routers as
147
127
  // they will be "sealed".
148
- const mount = (app) => {
128
+ /**
129
+ * Mounting function.
130
+ *
131
+ * @type {Mounter} mount
132
+ */
133
+ const mount = (app, { route = '/' } = {}) => {
149
134
  nunjucksEnv.express(app);
150
135
  app.set('view engine', 'njk');
136
+ // If a `mountUrl` has been defined, then we're potentially in "proxy mode",
137
+ // in which we strip the proxy path prefix from the incoming request URLs.
138
+ if (mountUrl) {
139
+ app.use((0, strip_proxy_path_js_1.default)({ mountUrl }));
140
+ }
141
+ // Attach a handler to redirect requests for `/` to the first waypoint in
142
+ // the plan
143
+ if (plan) {
144
+ const re = (0, path_to_regexp_1.pathToRegexp)(`${route}`.replace(/\/+/g, '/'));
145
+ app.use(re, (req, res) => {
146
+ const reqUrl = new URL(req.url, 'https://placeholder.test/');
147
+ const reqPath = (0, utils_js_1.validateUrlPath)(`${req.baseUrl}${reqUrl.pathname}${plan.getWaypoints()[0]}`);
148
+ let reqParams = reqUrl.searchParams.toString();
149
+ reqParams = reqParams ? `?${reqParams}` : '';
150
+ res.redirect(302, `${reqPath}${reqParams}`);
151
+ });
152
+ }
153
+ // Service static assets from the `app` rather than the `router`. The router
154
+ // may contain paramaterised path segments which would mean serving static
155
+ // assets over a dynamic URL each time, thus causing lots of cache misses on
156
+ // the browser.
157
+ const sealedStaticRouter = staticRouter.seal();
151
158
  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);
159
+ app.use(sealedStaticRouter);
160
+ const router = (0, express_1.Router)({
161
+ // Required so that any parameters in the URL are propagated to middleware
162
+ mergeParams: true,
163
+ });
164
+ router.use(preMiddleware);
165
+ // !!! DEPRECATE in v9 !!! For performance reasons, static assets will
166
+ // always be handled via the `app` middleware rather than `router`.
167
+ // Anywhere `mountUrl` is used in templates to service static assets must be
168
+ // changed to use `staticMountUrl`.
169
+ // TASK: remove this line below
170
+ router.use(sealedStaticRouter);
171
+ router.use(sessionMiddleware);
172
+ router.use(i18nMiddleware);
173
+ router.use(bodyParserMiddleware);
174
+ router.use(dataMiddleware);
175
+ router.use(ancillaryRouter.seal());
176
+ router.use(journeyRouter.seal());
177
+ router.use(postMiddleware);
178
+ app.use(route, router);
160
179
  return app;
161
180
  };
162
181
  // Prepare configuration result
@@ -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,6 +34,7 @@ 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;
@@ -51,6 +46,14 @@ export function validateHookPath(path: any): void;
51
46
  * @throws {Error} if proposed key is an invalid keyword
52
47
  */
53
48
  export function notProto(key: string): string;
54
- export type GlobalHook = import('./configuration-ingestor').GlobalHook;
55
- export type PageHook = import('./configuration-ingestor').PageHook;
56
- export type Hook = GlobalHook | PageHook;
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.notProto = 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');
@@ -125,3 +132,28 @@ function notProto(key) {
125
132
  return key;
126
133
  }
127
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;
@@ -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
@@ -2,4 +2,4 @@ declare function _default({ helmetConfigurator, }?: {
2
2
  helmetConfigurator: HelmetConfigurator;
3
3
  }): Function[];
4
4
  export default _default;
5
- export type HelmetConfigurator = import('../lib/configuration-ingestor').HelmetConfigurator;
5
+ export type HelmetConfigurator = import('../casa').HelmetConfigurator;
@@ -5,10 +5,11 @@ 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
11
  /**
11
- * @typedef {import('../lib/configuration-ingestor').HelmetConfigurator} HelmetConfigurator
12
+ * @typedef {import('../casa').HelmetConfigurator} HelmetConfigurator
12
13
  */
13
14
  /**
14
15
  * Pre middleware.
@@ -54,8 +55,8 @@ exports.default = ({ helmetConfigurator = (config) => (config), } = {}) => [
54
55
  directives: {
55
56
  'default-src': ["'none'"],
56
57
  'script-src': ["'self'", GA_DOMAIN, GTM_DOMAIN, (req, res) => `'nonce-${res.locals.cspNonce}'`],
57
- 'img-src': ["'self'", GA_DOMAIN],
58
- 'connect-src': ["'self'", GA_DOMAIN],
58
+ 'img-src': ["'self'", GA_DOMAIN, GA_ANALYTICS_DOMAIN],
59
+ 'connect-src': ["'self'", GA_DOMAIN, GA_ANALYTICS_DOMAIN],
59
60
  'frame-src': ["'self'", GTM_DOMAIN],
60
61
  'frame-ancestors': ["'self'"],
61
62
  'form-action': ["'self'"],
@@ -1,6 +1,5 @@
1
- declare function _default({ waypoint, plan, mountUrl, }: {
1
+ declare function _default({ waypoint, plan, }: {
2
2
  waypoint: any;
3
3
  plan: any;
4
- mountUrl: any;
5
4
  }): ((req: any, res: any, next: any) => void)[];
6
5
  export default _default;
@@ -20,7 +20,7 @@ const saveAndRedirect = (session, journeyContext, url, res, next) => {
20
20
  res.redirect(302, url);
21
21
  });
22
22
  };
23
- exports.default = ({ waypoint, plan, mountUrl, }) => [
23
+ exports.default = ({ waypoint, plan, }) => [
24
24
  (req, res, next) => {
25
25
  // Determine the next available waypoint after the current one
26
26
  const traversed = plan.traverse(req.casa.journeyContext);
@@ -68,7 +68,7 @@ exports.default = ({ waypoint, plan, mountUrl, }) => [
68
68
  // Construct the next url
69
69
  const nextUrl = (0, waypoint_url_js_1.default)({
70
70
  waypoint: nextWaypoint,
71
- mountUrl,
71
+ mountUrl: `${req.baseUrl}/`,
72
72
  journeyContext: req.casa.journeyContext,
73
73
  edit: req.casa.editMode,
74
74
  editOrigin: req.casa.editOrigin,
@@ -1,10 +1,9 @@
1
- export default function sessionMiddleware({ cookieParserMiddleware, secret, name, secure, ttl, mountUrl, cookieSameSite, cookiePath, store, }: {
1
+ export default function sessionMiddleware({ cookieParserMiddleware, secret, name, secure, ttl, cookieSameSite, cookiePath, store, }: {
2
2
  cookieParserMiddleware: any;
3
3
  secret: any;
4
4
  name: any;
5
5
  secure: any;
6
6
  ttl: any;
7
- mountUrl?: string | undefined;
8
7
  cookieSameSite?: boolean | undefined;
9
8
  cookiePath?: string | undefined;
10
9
  store?: any;
@@ -31,8 +31,9 @@ Object.defineProperty(exports, "__esModule", { value: true });
31
31
  // interrupting their journey.
32
32
  const express_session_1 = __importStar(require("express-session"));
33
33
  const logger_js_1 = __importDefault(require("../lib/logger.js"));
34
+ const utils_js_1 = require("../lib/utils.js");
34
35
  const log = (0, logger_js_1.default)('middleware:session');
35
- const sessionExpiryMiddleware = (mountUrl, ttl, getCookie, touchCookie, removeCookie) => (req, res, next) => {
36
+ const sessionExpiryMiddleware = (ttl, getCookie, touchCookie, removeCookie) => (req, res, next) => {
36
37
  var _a;
37
38
  const lastModified = getCookie(req);
38
39
  const age = Math.floor(Date.now() * 0.001) - lastModified;
@@ -50,7 +51,7 @@ const sessionExpiryMiddleware = (mountUrl, ttl, getCookie, touchCookie, removeCo
50
51
  touchCookie(res);
51
52
  if (req.method === 'POST') {
52
53
  log.info('The CSRF token for this POST request will now be invalid for this regenerated session. Redirecting to app mount point.');
53
- res.redirect(302, mountUrl);
54
+ res.redirect(302, (0, utils_js_1.validateUrlPath)(`${req.baseUrl}/`));
54
55
  }
55
56
  else {
56
57
  next();
@@ -73,7 +74,8 @@ const sessionExpiryMiddleware = (mountUrl, ttl, getCookie, touchCookie, removeCo
73
74
  referrer: req.originalUrl,
74
75
  lang: language,
75
76
  });
76
- res.redirect(302, `${mountUrl}session-timeout?${params.toString()}`);
77
+ /* eslint-disable-next-line prefer-template */
78
+ res.redirect(302, (0, utils_js_1.validateUrlPath)(`${req.baseUrl}/session-timeout`) + `?${params.toString()}`);
77
79
  }
78
80
  });
79
81
  }
@@ -87,7 +89,7 @@ const sessionExpiryMiddleware = (mountUrl, ttl, getCookie, touchCookie, removeCo
87
89
  // - set the session cookie
88
90
  // - parse request cookies
89
91
  // - handle expiry of server-side session
90
- function sessionMiddleware({ cookieParserMiddleware, secret, name, secure, ttl, mountUrl = '/', cookieSameSite = true, cookiePath = '/', store = new express_session_1.MemoryStore(), }) {
92
+ function sessionMiddleware({ cookieParserMiddleware, secret, name, secure, ttl, cookieSameSite = true, cookiePath = '/', store = new express_session_1.MemoryStore(), }) {
91
93
  const commonCookieOptions = {
92
94
  httpOnly: true,
93
95
  path: cookiePath,
@@ -126,7 +128,7 @@ function sessionMiddleware({ cookieParserMiddleware, secret, name, secure, ttl,
126
128
  store,
127
129
  }),
128
130
  cookieParserMiddleware,
129
- sessionExpiryMiddleware(mountUrl, ttl, getCookie, touchCookie, removeCookie),
131
+ sessionExpiryMiddleware(ttl, getCookie, touchCookie, removeCookie),
130
132
  ];
131
133
  }
132
134
  exports.default = sessionMiddleware;
@@ -1,5 +1,4 @@
1
- declare function _default({ waypoint, mountUrl, }: {
1
+ declare function _default({ waypoint, }: {
2
2
  waypoint: any;
3
- mountUrl: any;
4
3
  }): ((req: any, res: any, next: any) => any)[];
5
4
  export default _default;
@@ -10,7 +10,7 @@ const waypoint_url_js_1 = __importDefault(require("../lib/waypoint-url.js"));
10
10
  const logger_js_1 = __importDefault(require("../lib/logger.js"));
11
11
  const { has } = lodash_1.default;
12
12
  const log = (0, logger_js_1.default)('middleware:skip-waypoint');
13
- exports.default = ({ waypoint, mountUrl, }) => [
13
+ exports.default = ({ waypoint, }) => [
14
14
  (req, res, next) => {
15
15
  if (!has(req.query, 'skipto')) {
16
16
  return next();
@@ -24,7 +24,7 @@ exports.default = ({ waypoint, mountUrl, }) => [
24
24
  });
25
25
  JourneyContext_js_1.default.putContext(req.session, req.casa.journeyContext);
26
26
  const redirectUrl = (0, waypoint_url_js_1.default)({
27
- mountUrl,
27
+ mountUrl: `${req.baseUrl}/`,
28
28
  waypoint: skipTo,
29
29
  edit: req.casa.editMode,
30
30
  editOrigin: req.casa.editOrigin,
@@ -1,7 +1,6 @@
1
- declare function _default({ waypoint, plan, mountUrl, }: {
1
+ declare function _default({ waypoint, plan, }: {
2
2
  waypoint: string;
3
3
  plan: Plan;
4
- mountUrl: string;
5
4
  }): void;
6
5
  export default _default;
7
6
  export type Plan = typeof import("../lib/Plan");
@@ -18,11 +18,11 @@ const log = (0, logger_js_1.default)('middleware:steer-journey');
18
18
  * @param {object} obj Options
19
19
  * @param {string} obj.waypoint Current waypoint
20
20
  * @param {Plan} obj.plan CASA Plan
21
- * @param {string} obj.mountUrl Mount URL
22
21
  * @returns {void}
23
22
  */
24
- exports.default = ({ waypoint, plan, mountUrl, }) => [
23
+ exports.default = ({ waypoint, plan, }) => [
25
24
  (req, res, next) => {
25
+ const mountUrl = `${req.baseUrl}/`;
26
26
  // If the requested waypoint doesn't exist in the traversed journey, send
27
27
  // the user back to the last good waypoint.
28
28
  const traversed = plan.traverse(req.casa.journeyContext);
@@ -0,0 +1,4 @@
1
+ declare function _default({ mountUrl, }: {
2
+ mountUrl?: string | undefined;
3
+ }): ((req: any, res: any, next: any) => void)[];
4
+ export default _default;