@dwp/govuk-casa 8.1.0 → 8.2.0

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 (43) hide show
  1. package/CHANGELOG.md +11 -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 +102 -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 +13 -140
  10. package/dist/lib/configuration-ingestor.js +22 -88
  11. package/dist/lib/configure.d.ts +6 -77
  12. package/dist/lib/configure.js +80 -38
  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/validate-fields.d.ts +1 -2
  34. package/dist/middleware/validate-fields.js +2 -1
  35. package/dist/routes/journey.d.ts +1 -2
  36. package/dist/routes/journey.js +8 -8
  37. package/dist/routes/static.d.ts +1 -6
  38. package/dist/routes/static.js +6 -6
  39. package/package.json +25 -23
  40. package/views/casa/components/journey-form/README.md +3 -0
  41. package/views/casa/components/journey-form/template.njk +1 -1
  42. package/views/casa/partials/scripts.njk +1 -1
  43. package/views/casa/partials/styles.njk +2 -2
@@ -10,61 +10,10 @@ const Plan_js_1 = __importDefault(require("./Plan.js"));
10
10
  const logger_js_1 = __importDefault(require("./logger.js"));
11
11
  const utils_js_1 = require("./utils.js");
12
12
  /**
13
- * @typedef {import('./index').ContextEvent} ContextEvent
13
+ * @typedef {import('../casa').ConfigurationOptions} ConfigurationOptions
14
14
  */
15
15
  /**
16
- * @typedef {object} SessionOptions
17
- * @property {string} [name=casasession] Session name
18
- * @property {string} [secret=secret] Encryption secret
19
- * @property {number} [ttl=3600] Session ttl (seconds)
20
- * @property {boolean} [secure=false] Whether to use secure session cookies
21
- * @property {boolean|string} [cookieSameSite=true] SameSite (true = Strict)
22
- * @property {object} [store] Session store (default MemoryStore)
23
- */
24
- /**
25
- * @typedef {object} I18nOptions
26
- * @property {string[]} dirs Directories to search for locale dictionaries
27
- * @property {string[]} [locales=['en', 'cy']] Supported locales
28
- */
29
- /**
30
- * @typedef {object} GlobalHook Hook configuration
31
- * @property {string} hook Hook name in format `<router>.<hook>`
32
- * @property {Function} middleware Middleware function to insert at the hook point
33
- * @property {string|RegExp} [path=undefined] Only run if route path matches this string/regexp
34
- */
35
- /**
36
- * @typedef {object} PageHook (extends GlobalHook)
37
- * @property {string} hook Hook name (without a scope prefix)
38
- * @property {Function} middleware Middleware function to insert at the hook point
39
- */
40
- /**
41
- * @typedef {object} Page Page configuration. A Page is the interactive representation of a waypoint
42
- * @property {string} waypoint The waypoint with which this page is associated
43
- * @property {string} view Template path
44
- * @property {PageHook[]} [hooks=[]] Page-specific hooks (optional, default [])
45
- * @property {PageField[]} [fields=[]] Fields to be managed on this page (optional, default [])
46
- */
47
- /**
48
- * @callback HelmetConfigurator
49
- * @param {object} config A default Helmet configuration provided by CASA
50
- * @returns {object} The modified configuration object
51
- */
52
- /**
53
- * Configure some middleware for use in creating a new CASA app.
54
- *
55
- * `mountUrl` is used to ensure the CSS content uses the correct reference to
56
- * static assets in the `govuk-frontend` module.
57
- *
58
- * @typedef {object} ConfigurationOptions Configuration options
59
- * @property {string} [mountUrl=/] URL path to root of CASA app
60
- * @property {string[]} [views=[]] Template directories
61
- * @property {SessionOptions} [session] Session configuration
62
- * @property {Page[]} [pages=[]] Pages the represent waypoints
63
- * @property {GlobalHook[]} [hooks=[]] Hooks to apply
64
- * @property {object[]} [plugins=[]] Plugins
65
- * @property {I18nOptions[]} [i18n] I18n configuration
66
- * @property {Plan} plan CASA Plan
67
- * @property {ContextEvent[]} [events=[]] Handlers for JourneyContext events
16
+ * @typedef {import('../casa').HelmetConfigurator} HelmetConfigurator
68
17
  */
69
18
  const log = (0, logger_js_1.default)('lib:configuration-ingestor');
70
19
  const echo = (a) => (a);
@@ -76,7 +25,7 @@ const echo = (a) => (a);
76
25
  * @throws {TypeError} For invalid object.
77
26
  * @returns {object} Sanitised i18n object.
78
27
  */
79
- function validateI18nObject(i18n, cb = echo) {
28
+ function validateI18nObject(i18n = Object.create(null), cb = echo) {
80
29
  if (Object.prototype.toString.call(i18n) !== '[object Object]') {
81
30
  throw new TypeError('I18n must be an object');
82
31
  }
@@ -91,11 +40,8 @@ exports.validateI18nObject = validateI18nObject;
91
40
  * @throws {TypeError} For invalid type.
92
41
  * @returns {Array} Array of directories.
93
42
  */
94
- function validateI18nDirs(dirs) {
95
- if (typeof dirs === 'undefined') {
96
- throw ReferenceError('I18n directories are missing (i18n.dirs)');
97
- }
98
- else if (!Array.isArray(dirs)) {
43
+ function validateI18nDirs(dirs = []) {
44
+ if (!Array.isArray(dirs)) {
99
45
  throw new TypeError('I18n directories must be an array (i18n.dirs)');
100
46
  }
101
47
  dirs.forEach((dir, i) => {
@@ -114,11 +60,8 @@ exports.validateI18nDirs = validateI18nDirs;
114
60
  * @throws {TypeError} For invalid type.
115
61
  * @returns {Array} Array of locales.
116
62
  */
117
- function validateI18nLocales(locales) {
118
- if (typeof locales === 'undefined') {
119
- throw ReferenceError('I18n locales are missing (i18n.locales)');
120
- }
121
- else if (!Array.isArray(locales)) {
63
+ function validateI18nLocales(locales = ['en', 'cy']) {
64
+ if (!Array.isArray(locales)) {
122
65
  throw new TypeError('I18n locales must be an array (i18n.locales)');
123
66
  }
124
67
  locales.forEach((locale, i) => {
@@ -155,7 +98,7 @@ exports.validateMountUrl = validateMountUrl;
155
98
  * @throws {TypeError} For invalid object.
156
99
  * @returns {object} Sanitised sessions object.
157
100
  */
158
- function validateSessionObject(session, cb = echo) {
101
+ function validateSessionObject(session = Object.create(null), cb = echo) {
159
102
  if (session === undefined) {
160
103
  return cb(session);
161
104
  }
@@ -173,11 +116,8 @@ exports.validateSessionObject = validateSessionObject;
173
116
  * @throws {TypeError} For invalid type.
174
117
  * @returns {Array} Array of directories.
175
118
  */
176
- function validateViews(dirs) {
177
- if (typeof dirs === 'undefined') {
178
- throw ReferenceError('View directories are missing (views)');
179
- }
180
- else if (!Array.isArray(dirs)) {
119
+ function validateViews(dirs = []) {
120
+ if (!Array.isArray(dirs)) {
181
121
  throw new TypeError('View directories must be an array (views)');
182
122
  }
183
123
  dirs.forEach((dir, i) => {
@@ -214,11 +154,8 @@ exports.validateSessionSecret = validateSessionSecret;
214
154
  * @throws {TypeError} For invalid value.
215
155
  * @returns {number} Ttl.
216
156
  */
217
- function validateSessionTtl(ttl) {
218
- if (typeof ttl === 'undefined') {
219
- throw ReferenceError('Session ttl is missing (session.ttl)');
220
- }
221
- else if (typeof ttl !== 'number') {
157
+ function validateSessionTtl(ttl = 3600) {
158
+ if (typeof ttl !== 'number') {
222
159
  throw new TypeError('Session ttl must be an integer (session.ttl)');
223
160
  }
224
161
  return ttl;
@@ -227,16 +164,13 @@ exports.validateSessionTtl = validateSessionTtl;
227
164
  /**
228
165
  * Validates and sanitises sessions name.
229
166
  *
230
- * @param {string} name Session name.
167
+ * @param {string} [name=casa-session] Session name.
231
168
  * @throws {ReferenceError} For missing value type.
232
169
  * @throws {TypeError} For invalid value.
233
170
  * @returns {string} Name.
234
171
  */
235
- function validateSessionName(name) {
236
- if (typeof name === 'undefined') {
237
- throw ReferenceError('Session name is missing (session.name)');
238
- }
239
- else if (typeof name !== 'string') {
172
+ function validateSessionName(name = 'casa-session') {
173
+ if (typeof name !== 'string') {
240
174
  throw new TypeError('Session name must be a string (session.name)');
241
175
  }
242
176
  return name;
@@ -245,16 +179,13 @@ exports.validateSessionName = validateSessionName;
245
179
  /**
246
180
  * Validates and sanitises sessions secure flag.
247
181
  *
248
- * @param {boolean} secure Session secure flag.
182
+ * @param {boolean} [secure=false] Session secure flag.
249
183
  * @throws {ReferenceError} For missing value type.
250
184
  * @throws {TypeError} For invalid value.
251
185
  * @returns {string} Name.
252
186
  */
253
- function validateSessionSecure(secure) {
254
- if (typeof secure === 'undefined') {
255
- throw ReferenceError('Session secure flag is missing (session.secure)');
256
- }
257
- else if (typeof secure !== 'boolean') {
187
+ function validateSessionSecure(secure = false) {
188
+ if (typeof secure !== 'boolean') {
258
189
  throw new TypeError('Session secure flag must be boolean (session.secure)');
259
190
  }
260
191
  return secure;
@@ -371,7 +302,7 @@ const validatePage = (page, index) => {
371
302
  throw err;
372
303
  }
373
304
  };
374
- function validatePages(pages) {
305
+ function validatePages(pages = []) {
375
306
  if (!Array.isArray(pages)) {
376
307
  throw new TypeError('Pages must be an array (pages)');
377
308
  }
@@ -451,6 +382,9 @@ function ingest(config = {}) {
451
382
  dirs: validateI18nDirs(i18n.dirs),
452
383
  locales: validateI18nLocales(i18n.locales),
453
384
  })),
385
+ // !!! DEPRECATION NOTICE !!!
386
+ // This will be removed in next major version
387
+ //
454
388
  // Public URL from which the app will be served
455
389
  mountUrl: validateMountUrl(config.mountUrl),
456
390
  // Session
@@ -1,90 +1,19 @@
1
1
  /**
2
- * @typedef {import('express').RequestHandler} ExpressRequestHandler
2
+ * @typedef {import('../casa').ConfigurationOptions} ConfigurationOptions
3
3
  */
4
4
  /**
5
- * @typedef {import('./index').MutableRouter} MutableRouter
5
+ * @typedef {import('../casa').ConfigurationOptions} ConfigureResult
6
6
  */
7
7
  /**
8
- * @typedef {import('./configuration-ingestor').ConfigurationOptions} ConfigurationOptions
9
- */
10
- /**
11
- * @typedef {object} ConfigureResult Result of a call to configure() function
12
- * @property {nunjucks.Environment} nunjucksEnv Nunjucks environment
13
- * @property {MutableRouter} staticRouter Router handling all static assets
14
- * @property {MutableRouter} ancillaryRouter Router handling ancillary routes
15
- * @property {MutableRouter} journeyRouter Router handling all waypoint requests
16
- * @property {ExpressRequestHandler[]} preMiddleware Middleware mounted before anything else
17
- * @property {ExpressRequestHandler[]} postMiddleware Middleware mounted after everything else
18
- * @property {ExpressRequestHandler[]} csrfMiddleware CSRF get/set middleware (useful for forms)
19
- * @property {ExpressRequestHandler} sessionMiddleware Session middleware
20
- * @property {ExpressRequestHandler[]} cookieParserMiddleware Cookie-parsing middleware
21
- * @property {ExpressRequestHandler[]} i18nMiddleware I18n preparation middleware
22
- * @property {ExpressRequestHandler} bodyParserMiddleware Body parsing middleware
23
- * @property {Function} mount Function used to mount all CASA artifacts onto an ExpressJS app
8
+ * @typedef {import('../casa').Mounter} Mounter
24
9
  */
25
10
  /**
26
11
  * Configure some middleware for use in creating a new CASA app.
27
12
  *
28
- * `mountUrl` is used to ensure the CSS content uses the correct reference to
29
- * static assets in the `govuk-frontend` module.
30
- *
31
13
  * @param {ConfigurationOptions} config Configuration options
32
14
  * @returns {ConfigureResult} Result
33
15
  */
34
16
  export default function configure(config?: ConfigurationOptions): ConfigureResult;
35
- export type ExpressRequestHandler = import('express').RequestHandler;
36
- export type MutableRouter = import('./index').MutableRouter;
37
- export type ConfigurationOptions = import('./configuration-ingestor').ConfigurationOptions;
38
- /**
39
- * Result of a call to configure() function
40
- */
41
- export type ConfigureResult = {
42
- /**
43
- * Nunjucks environment
44
- */
45
- nunjucksEnv: nunjucks.Environment;
46
- /**
47
- * Router handling all static assets
48
- */
49
- staticRouter: MutableRouter;
50
- /**
51
- * Router handling ancillary routes
52
- */
53
- ancillaryRouter: MutableRouter;
54
- /**
55
- * Router handling all waypoint requests
56
- */
57
- journeyRouter: MutableRouter;
58
- /**
59
- * Middleware mounted before anything else
60
- */
61
- preMiddleware: ExpressRequestHandler[];
62
- /**
63
- * Middleware mounted after everything else
64
- */
65
- postMiddleware: ExpressRequestHandler[];
66
- /**
67
- * CSRF get/set middleware (useful for forms)
68
- */
69
- csrfMiddleware: ExpressRequestHandler[];
70
- /**
71
- * Session middleware
72
- */
73
- sessionMiddleware: ExpressRequestHandler;
74
- /**
75
- * Cookie-parsing middleware
76
- */
77
- cookieParserMiddleware: ExpressRequestHandler[];
78
- /**
79
- * I18n preparation middleware
80
- */
81
- i18nMiddleware: ExpressRequestHandler[];
82
- /**
83
- * Body parsing middleware
84
- */
85
- bodyParserMiddleware: ExpressRequestHandler;
86
- /**
87
- * Function used to mount all CASA artifacts onto an ExpressJS app
88
- */
89
- mount: Function;
90
- };
17
+ export type ConfigurationOptions = import('../casa').ConfigurationOptions;
18
+ export type ConfigureResult = import('../casa').ConfigurationOptions;
19
+ export type Mounter = import('../casa').Mounter;
@@ -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
  */
@@ -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'),
@@ -93,7 +79,7 @@ function configure(config = {}) {
93
79
  // These _must_ be added to the ExpressJS application at the start and end
94
80
  // of all other middleware respectively.
95
81
  const preMiddleware = (0, pre_js_1.default)({ helmetConfigurator });
96
- const postMiddleware = (0, post_js_1.default)({ mountUrl });
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,85 @@ 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
+ if (config.mountUrl) {
158
+ log.warn('[DEPRECATION WARNING] Using configuration attribute, mountUrl. This will be removed in an upcoming major version');
159
+ app.use((req, res, next) => {
160
+ req.baseUrl = mountUrl.replace(/\/$/, '');
161
+ next();
162
+ });
163
+ }
164
+ // Attach a handler to redirect requests for `/` to the first waypoint in
165
+ // the plan
166
+ if (plan) {
167
+ const re = (0, path_to_regexp_1.pathToRegexp)(`${route}`.replace(/\/+/g, '/'));
168
+ app.use(re, (req, res) => {
169
+ const reqUrl = new URL(req.url, 'https://placeholder.test/');
170
+ const reqPath = (0, utils_js_1.validateUrlPath)(`${req.baseUrl}${reqUrl.pathname}${plan.getWaypoints()[0]}`);
171
+ let reqParams = reqUrl.searchParams.toString();
172
+ reqParams = reqParams ? `?${reqParams}` : '';
173
+ res.redirect(302, `${reqPath}${reqParams}`);
174
+ });
175
+ }
176
+ // Service static assets from the `app` rather than the `router`. The router
177
+ // may contain paramaterised path segments which would mean serving static
178
+ // assets over a dynamic URL each time, thus causing lots of cache misses on
179
+ // the browser.
180
+ const sealedStaticRouter = staticRouter.seal();
151
181
  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);
182
+ app.use(sealedStaticRouter);
183
+ const router = (0, express_1.Router)({
184
+ // Required so that any parameters in the URL are propagated to middleware
185
+ mergeParams: true,
186
+ });
187
+ router.use(preMiddleware);
188
+ // !!! DEPRECATE in v9 !!! For performance reasons, static assets will
189
+ // always be handled via the `app` middleware rather than `router`.
190
+ // Anywhere `mountUrl` is used in templates to service static assets must be
191
+ // changed to use `staticMountUrl`.
192
+ // TASK: remove this line below
193
+ router.use(sealedStaticRouter);
194
+ router.use(sessionMiddleware);
195
+ router.use(i18nMiddleware);
196
+ router.use(bodyParserMiddleware);
197
+ router.use(dataMiddleware);
198
+ router.use(ancillaryRouter.seal());
199
+ router.use(journeyRouter.seal());
200
+ router.use(postMiddleware);
201
+ app.use(route, router);
160
202
  return app;
161
203
  };
162
204
  // 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
  }