hof 23.0.0-frontend-v4-beta.14 → 23.0.0-vite-sourcemap-beta

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 (73) hide show
  1. package/CHANGELOG.md +45 -2
  2. package/README.md +28 -21
  3. package/build/tasks/images/index.js +2 -3
  4. package/build/tasks/index.js +1 -1
  5. package/build/tasks/vite/index.js +21 -0
  6. package/build/tasks/vite/vite.config.js +82 -0
  7. package/components/emailer/emailer.js +3 -3
  8. package/config/builder-defaults.js +7 -12
  9. package/config/hof-defaults.js +2 -1
  10. package/frontend/govuk-template/build/config.js +1 -0
  11. package/frontend/govuk-template/build/govuk_template.html +9 -9
  12. package/frontend/govuk-template/govuk_template_generated.html +9 -9
  13. package/frontend/template-mixins/mixins/template-mixins.js +1 -1
  14. package/frontend/template-mixins/partials/forms/checkbox.html +1 -1
  15. package/frontend/themes/gov-uk/styles/govuk.scss +1 -1
  16. package/frontend/toolkit/index.js +6 -6
  17. package/index.js +24 -18
  18. package/lib/encryption.js +43 -17
  19. package/lib/sessions.js +5 -2
  20. package/lib/settings.js +1 -1
  21. package/package.json +9 -13
  22. package/sandbox/assets/{rebrand/js → js}/index.js +1 -1
  23. package/sandbox/assets/{rebrand/scss → scss}/app.scss +1 -1
  24. package/sandbox/package.json +1 -1
  25. package/sandbox/public/css/app.css +53 -51
  26. package/sandbox/public/css/app.css.map +1 -0
  27. package/sandbox/public/js/bundle.js +44 -39242
  28. package/sandbox/server.js +4 -1
  29. package/build/tasks/browserify/compress.js +0 -15
  30. package/build/tasks/browserify/index.js +0 -48
  31. package/sandbox/public/images/passports/new-window-link-blue.png +0 -0
  32. package/sandbox/public/images/passports/new-window-link.png +0 -0
  33. package/sandbox/public/images/spinner.gif +0 -0
  34. /package/frontend/toolkit/assets/{rebrand/images → images}/passports/new-window-link-blue.png +0 -0
  35. /package/frontend/toolkit/assets/{rebrand/images → images}/passports/new-window-link.png +0 -0
  36. /package/frontend/toolkit/assets/{rebrand/images → images}/spinner.gif +0 -0
  37. /package/frontend/toolkit/assets/{rebrand/javascript → javascript}/character-count.js +0 -0
  38. /package/frontend/toolkit/assets/{rebrand/javascript → javascript}/form-focus.js +0 -0
  39. /package/frontend/toolkit/assets/{rebrand/javascript → javascript}/helpers.js +0 -0
  40. /package/frontend/toolkit/assets/{rebrand/javascript → javascript}/progressive-reveal.js +0 -0
  41. /package/frontend/toolkit/assets/{rebrand/javascript → javascript}/validation.js +0 -0
  42. /package/frontend/toolkit/assets/{rebrand/javascript → javascript}/vendor/details.polyfill.js +0 -0
  43. /package/frontend/toolkit/assets/{rebrand/javascript → javascript}/vendor/indexof.polyfill.js +0 -0
  44. /package/frontend/toolkit/assets/{rebrand/javascript → javascript}/vendor/safari-cachebuster.js +0 -0
  45. /package/frontend/toolkit/assets/{rebrand/stylesheets → stylesheets}/_base.scss +0 -0
  46. /package/frontend/toolkit/assets/{rebrand/stylesheets → stylesheets}/_helpers.scss +0 -0
  47. /package/frontend/toolkit/assets/{rebrand/stylesheets → stylesheets}/_layout.scss +0 -0
  48. /package/frontend/toolkit/assets/{rebrand/stylesheets → stylesheets}/_typography.scss +0 -0
  49. /package/frontend/toolkit/assets/{rebrand/stylesheets → stylesheets}/_variables.scss +0 -0
  50. /package/frontend/toolkit/assets/{rebrand/stylesheets → stylesheets}/app.scss +0 -0
  51. /package/frontend/toolkit/assets/{rebrand/stylesheets → stylesheets}/govuk-elements/_buttons.scss +0 -0
  52. /package/frontend/toolkit/assets/{rebrand/stylesheets → stylesheets}/govuk-elements/_details.scss +0 -0
  53. /package/frontend/toolkit/assets/{rebrand/stylesheets → stylesheets}/govuk-elements/_elements-typography.scss +0 -0
  54. /package/frontend/toolkit/assets/{rebrand/stylesheets → stylesheets}/govuk-elements/_forms.scss +0 -0
  55. /package/frontend/toolkit/assets/{rebrand/stylesheets → stylesheets}/govuk-elements/_helpers.scss +0 -0
  56. /package/frontend/toolkit/assets/{rebrand/stylesheets → stylesheets}/govuk-elements/_layout.scss +0 -0
  57. /package/frontend/toolkit/assets/{rebrand/stylesheets → stylesheets}/govuk-elements/_lists.scss +0 -0
  58. /package/frontend/toolkit/assets/{rebrand/stylesheets → stylesheets}/govuk-elements/_panels.scss +0 -0
  59. /package/frontend/toolkit/assets/{rebrand/stylesheets → stylesheets}/govuk-elements/_reset.scss +0 -0
  60. /package/frontend/toolkit/assets/{rebrand/stylesheets → stylesheets}/govuk-elements/_tables.scss +0 -0
  61. /package/frontend/toolkit/assets/{rebrand/stylesheets → stylesheets}/govuk-elements/forms/_form-block-labels.scss +0 -0
  62. /package/frontend/toolkit/assets/{rebrand/stylesheets → stylesheets}/govuk-elements/forms/_form-date.scss +0 -0
  63. /package/frontend/toolkit/assets/{rebrand/stylesheets → stylesheets}/govuk-elements/forms/_form-validation.scss +0 -0
  64. /package/frontend/toolkit/assets/{rebrand/stylesheets → stylesheets}/mixins.scss +0 -0
  65. /package/frontend/toolkit/assets/{rebrand/stylesheets → stylesheets}/modules/_alerts.scss +0 -0
  66. /package/frontend/toolkit/assets/{rebrand/stylesheets → stylesheets}/modules/_buttons.scss +0 -0
  67. /package/frontend/toolkit/assets/{rebrand/stylesheets → stylesheets}/modules/_confirm-page.scss +0 -0
  68. /package/frontend/toolkit/assets/{rebrand/stylesheets → stylesheets}/modules/_lists.scss +0 -0
  69. /package/frontend/toolkit/assets/{rebrand/stylesheets → stylesheets}/modules/_progressive-reveal.scss +0 -0
  70. /package/frontend/toolkit/assets/{rebrand/stylesheets → stylesheets}/modules/_validation.scss +0 -0
  71. /package/sandbox/assets/{rebrand/images → images}/icons/icon-caret-left.png +0 -0
  72. /package/sandbox/assets/{rebrand/images → images}/icons/icon-complete.png +0 -0
  73. /package/sandbox/assets/{rebrand/images → images}/icons/icon-cross-remove-sign.png +0 -0
package/CHANGELOG.md CHANGED
@@ -1,4 +1,15 @@
1
- ## 2025-10-25, Version 23.0.0 (Stable), @Rhodine-orleans-lindsay
1
+ ## 2025-12-16, Version 23.0.0 (Stable), @PaolaDMadd-Pro @Rhodine-orleans-lindsay
2
+ ### Changed
3
+ - Replaced Browserify with Vite
4
+ - removed browserify dependencies including in tests
5
+ - added vite configuration and related tests
6
+ - Updated readme.md
7
+ - Potential **breaking changes**:
8
+ - Node version required for Vite is `20.19.0 || >=22.12.0`
9
+ - Updated engine requirements to use node to `>=20.19.0`
10
+ - Vite uses Rollup in production and requires optional rollup dependencies to be loaded. Setting `--ignore-optional flag` in Dockerfiles will result in "Module not found" errors in CI/CD or Docker.
11
+
12
+ ## 2025-12-09, Version 22.13.0 (Stable), @Rhodine-orleans-lindsay
2
13
  ### Changed
3
14
  - Updated cookie banner view to follow GOV.UK design system. Updated cookieSettings.js to allow for different confirmation text based on whether cookies were accepted or rejected
4
15
  - Added inputmode='numeric' to date component
@@ -10,7 +21,39 @@
10
21
  - Added a modifier for phone text input styles that accept sequences of digits
11
22
  - Added govuk-template--rebranded class to govuk template html to enable govuk rebrand styles:
12
23
  - Updated header and footer to follow GOV.UK design system as part of rebrand
13
- * ⛓️‍💥 **BREAKING CHANGE** : files and folders in a project's `assets` folder will need to be moved to a `assets/rebrand` folder. Add a `rebrand` folder to the `assets` folder. e.g. `assets/images` will need to be moved to `assets/rebrand/images`.
24
+
25
+ ## 2025-11-20, Version 22.12.0 (Stable), @dk4g @jamiecarterHO
26
+
27
+ ### Infrastructure
28
+ - Updated CI/CD pipeline to test against Node.js 20.x, 22.x, and 24.x
29
+ - Updated Redis testing versions to 7 and 8
30
+ - Added `NODE_VERSION` environment variable for consistent Node.js version across jobs
31
+ - Updated release process to use Node.js 24 for tagging and publishing operations
32
+
33
+ ### Security
34
+ - Replaced deprecated `crypto.createCipher`/`crypto.createDecipher` with `crypto.createCipheriv`/`crypto.createDecipheriv`
35
+ - Added proper initialisation vector (IV) handling for enhanced security
36
+ - Enforced 32-byte session secret requirement for AES-256 encryption compatibility
37
+ - Removed insecure default session secret ('changethis') - now requires explicit configuration
38
+
39
+ ### Migration Notes
40
+ - **Session Reset Required**: Due to enhanced encryption security, existing user sessions will be invalidated and users will need to re-authenticate after this update
41
+ - **Session Secret**: You must now set a unique `SESSION_SECRET` environment variable of exactly 32 bytes for encryption compatibility.
42
+ For testing purposes, you can use the following command to generate a random value. For production environments, consult a security expert or refer to official cryptographic guidelines to generate a secure secret
43
+ `node -e "console.log(require('crypto').randomBytes(16).toString('hex'))"`
44
+
45
+ ## 2025-11-15, Version 22.11.0 (Stable), @Rhodine-orleans-lindsay
46
+
47
+ ### Changed
48
+ - Updated custom session-timeout handling so that custom behaviours are not blocked by a `404` middleware error.
49
+
50
+ ### Added
51
+ - Added a `CUSTOM_SESSION_EXPIRY` environment variable so that a time other than the redis session ttl can be used for the session timeout. **IMPORTANT**: The `CUSTOM_SESSION_EXPIRY` variable must always be a time before the redis session ttl would expire so that behaviours can run before the `SESSION_TIMEOUT` middleware is triggered.
52
+ - Added a `USE_CUSTOM_SESSION_TIMEOUT` that is `false` by default. When set to `true` the '/session-timeout' page can run before the session expires without triggering a `404` middleware error.
53
+
54
+ - 🎬 Action:
55
+ - For custom session timeout handling that is not linked to the redis session ttl, The following variables must be set: `CUSTOM_SESSION_EXPIRY` to the relevant expiry time e.g.600 and `USE_CUSTOM_SESSION_TIMEOUT` to true.
56
+ - If a behaviour is required on the '/session-timeout` step, the '/session-timeout' step must be set in the project's index.js, along with any relevant behaviours.
14
57
 
15
58
  ## 2025-10-10, Version 22.10.4 (Stable), @dk4g
16
59
  ### Security
package/README.md CHANGED
@@ -60,7 +60,7 @@ It is recommended to alias `hof-build` to an npm script in your package.json.
60
60
 
61
61
  ## Tasks
62
62
 
63
- - `browserify` - compiles client-side js with browserify
63
+ - `vite` - compiles client-side js with vite in development and rollup in production. Requires node version `^20.19.0 || >=22.12.0`.
64
64
  - `sass` - compiles sass
65
65
  - `images` - copies images from ./assets/images directory to ./public/images
66
66
  - `translate` - compiles translation files
@@ -69,21 +69,18 @@ Note: For SASS compilation it's possible to additionally configure the following
69
69
  - `outputStyle` - Controls whether the CSS output is compressed or not, expanded (default) = non compressed and compressed = compressed CSS output.
70
70
  - `quietDeps` - This controls whether you get deprecation warning shown in the console output, if set to false (default) SASS deprecation warnings will be shown in the console, if set to true then deprecation warnings will not be shown in the console output.
71
71
  - `sourceMaps` - This controls whether the build will output css sourcemaps to help with debugging. These will be output to the same directory as the css output as a .map file. This option is not currently available in production.
72
-
73
- For JavaScript compilation, browserify can be set to debug mode by setting the `debug` option to true. This will cause browserify to output JavaScript sourcemaps as a .js.map file to the same directory as the js bundle.
74
-
75
- Debugging example (in hof.settings or your build config)
72
+ For JavaScript compilation, Vite outputs JavaScript sourcemaps as a .js.map file to the same directory as the js bundle.
73
+ You can change sourcemaps' default settings in hof.settings or your build config as follows:
76
74
  ```
77
75
  "build": {
78
76
  "sass": {
79
77
  "sourceMaps": true
80
78
  },
81
- "browserify": {
82
- "debug": true
83
- }
79
+ "js": {
80
+ "sourceMaps": true
81
+ },
84
82
  }
85
83
  ```
86
-
87
84
  ## Watch
88
85
 
89
86
  You can additionally run a `watch` task to start a server instance, which will automatically restart based on changes to files. This will also re-perform the tasks above when relevant files change.
@@ -109,7 +106,7 @@ hof-build watch --env .envdev
109
106
 
110
107
  ## Configuration
111
108
 
112
- The default settings will match those for an app generated using [`hof-generator`](https://npmjs.com/hof-generator).
109
+ The default settings will match those for an app generated using [`hof-generator`](https://npmjs.com/hof-generator) (⚠️ This package has been deprecated).
113
110
 
114
111
  If a `hof.settings.json` file is found in the application root, then the `build` section of the settings file will be used to override [the default configuration](./defaults.js).
115
112
 
@@ -119,17 +116,9 @@ Alternatively you can define a path to a local config file by passing a `--confi
119
116
  hof-build --config /path/to/my/config.js
120
117
  ```
121
118
 
122
- Any task can be disabled by setting its configuration to `false` (or any falsy value).
123
-
124
- ```js
125
- module.exports = {
126
- browserify: false,
127
- };
128
- ```
129
-
130
119
  ### Configuration options
131
120
 
132
- Each task has a common configuration format with the following options:
121
+ Each task (except Vite) has a common configuration format with the following options:
133
122
 
134
123
  - `src` - defines the input file or files for the build task
135
124
  - `out` - defines the output location of the built code where relevant
@@ -867,7 +856,7 @@ Kubernetes healthcheck URLs are provided as defaults if no overrides are supplie
867
856
  |----------|-------------|---------|---------|
868
857
  | `SHOW_COOKIES_BANNER` | Controls whether the cookies banner is displayed | Auto-detected based on GA tags | `true`, `false` |
869
858
 
870
- **Behavior:**
859
+ **Behaviour:**
871
860
  - If `SHOW_COOKIES_BANNER` is explicitly set, that value is used
872
861
  - If not set, banner is automatically shown when `GA_TAG` or `GA_4_TAG` is present
873
862
  - If no GA tags are configured, banner is hidden by default
@@ -1328,7 +1317,11 @@ This feature allows you to customise the content related to the session timeout
1328
1317
 
1329
1318
  ### Usage
1330
1319
 
1331
- To enable and customise the session timeout behavior, you need to set the component and translations in your project's `hof.settings.json` file:
1320
+ By default, the session timeout is set to the redis session ttl. To bypass this and display the session timeout message before the redis session ttl the following evironment variables must be set:
1321
+ `CUSTOM_SESSION_EXPIRY` - e.g. `600`. Configure to expire before thte project's redis session ttl.
1322
+ `USE_CUSTOM_SESSION_TIMEOUT` - `false` by default. When set to `true` the '/session-timeout' page can run before the session expires without triggering a `404` middleware error.
1323
+
1324
+ To enable and customise the session timeout behaviour, you need to set the component and translations in your project's `hof.settings.json` file:
1332
1325
  ```json
1333
1326
  "behaviours": [
1334
1327
  "hof/components/session-timeout-warning"
@@ -1348,6 +1341,20 @@ By default, the framework uses the standard content provided by HOF. If you wish
1348
1341
  "saveExitFormContent": true // allows you to customise the content on the save-and-exit page
1349
1342
  ```
1350
1343
 
1344
+ To override the default session-timeout page completely, the path to the session-timeout.html should be set in the views property in the hof.settings.json file e.g. 
1345
+ ```json
1346
+ "behaviours": [
1347
+ "hof/components/session-timeout-warning"
1348
+ ],
1349
+ "translations": "./apps/common/translations",
1350
+ "views": ["./apps/common/views"], // allows you to overide the HOF default session-timeout page and use a custom one from the specified views
1351
+ ...
1352
+ ```
1353
+ or in the project's `server.js` e.g.
1354
+ ```js
1355
+ settings.views = path.resolve(__dirname, './apps/common/views');
1356
+ ```
1357
+
1351
1358
  ### Customising content in `pages.json`
1352
1359
  Once the variables are set, you can customise the session timeout warning and exit messages in your project's pages.json:
1353
1360
 
@@ -4,7 +4,6 @@
4
4
  const fs = require('fs');
5
5
  const chalk = require('chalk');
6
6
  const spawn = require('../../lib/spawn');
7
- const mkdir = require('../../lib/mkdir');
8
7
 
9
8
  module.exports = config => {
10
9
  if (!config.images) {
@@ -28,11 +27,11 @@ module.exports = config => {
28
27
  console.log(`${chalk.yellow('warning')}: Skipping missing images folder: ${src}`);
29
28
  return Promise.resolve();
30
29
  }
31
- return mkdir(imagesOutput)
32
- .then(() => spawn('cp', ['-r', `${src}/.`, imagesOutput]));
30
+ return spawn('cp', ['-r', `${src}/.`, imagesOutput]);
33
31
  }))
34
32
  .catch(e => {
35
33
  if (e.code !== 'ENOENT') {
34
+ console.error(`${chalk.red('error')}: Failed to copy images - ${e.message}`);
36
35
  throw e;
37
36
  } else {
38
37
  console.log(`${chalk.yellow('warning')}: no images directory found at ${config.images.src}`);
@@ -1,7 +1,7 @@
1
1
  'use strict';
2
2
 
3
3
  module.exports = {
4
- browserify: require('./browserify'),
4
+ vite: require('./vite'),
5
5
  sass: require('./sass'),
6
6
  translate: require('./translate'),
7
7
  images: require('./images')
@@ -0,0 +1,21 @@
1
+ 'use strict';
2
+
3
+ const vite = require('vite');
4
+ const path = require('path');
5
+ const viteConfig = path.resolve(__dirname, './vite.config.js');
6
+ const hofDefaults = require('../../../config/hof-defaults');
7
+
8
+ module.exports = config => {
9
+ process.env.NODE_ENV = hofDefaults.env;
10
+
11
+ if(!config.production) {
12
+ return vite.build({
13
+ configFile: viteConfig,
14
+ mode: 'development'
15
+ });
16
+ }
17
+ return vite.build({
18
+ configFile: viteConfig
19
+ });
20
+ };
21
+ module.exports.task = 'vite';
@@ -0,0 +1,82 @@
1
+ /* eslint-disable no-console */
2
+ 'use strict';
3
+
4
+ import { defineConfig } from 'vite';
5
+ import { resolve } from 'path';
6
+ import fs from 'fs';
7
+ import { nodeResolve } from '@rollup/plugin-node-resolve';
8
+ import commonjs from '@rollup/plugin-commonjs';
9
+ import config from '../../../config/builder-defaults';
10
+
11
+ const publicDirectory = resolve(process.cwd(), 'public');
12
+ const entryFile = (() => {
13
+ const src = resolve(process.cwd(), 'assets/js/index.js');
14
+ if (fs.existsSync(src)) return src;
15
+
16
+ throw new Error(`vite: entry file not found. Checked: ${src}`);
17
+ })();
18
+
19
+
20
+ export default defineConfig({
21
+ plugins: [
22
+ commonjs({
23
+ include: [/node_modules/, /assets\/js/, /frontend/]
24
+ }),
25
+ nodeResolve({ browser: true, preferBuiltins: false }),
26
+ // Intercept all console warnings printed during build
27
+ // Custom plugin to suppress specific resolve warnings.
28
+ // Vite does not process and optimise static images and assets files, it uses a src/ folder.
29
+ // the images and assets folders are therefore loaded at runtime from the public/ folder.
30
+ // This should be resolved with the v5/nunjucks work so we can ignore these warnings for now.
31
+ // TODO: Remove this when v5/nunjucks is implemented.
32
+ {
33
+ name: 'suppress-resolve-warnings',
34
+ apply: 'build',
35
+ configResolved() {
36
+ const originalWarning = console.warn;
37
+ console.warn = (...args) => {
38
+ if (
39
+ args.some(arg =>
40
+ typeof arg === 'string' &&
41
+ arg.includes("didn't resolve at build time")
42
+ )
43
+ ) {
44
+ return; // skip this specific warning
45
+ }
46
+ originalWarning(...args);
47
+ };
48
+ }
49
+ }
50
+ ],
51
+ base: '/assets/',
52
+ publicDir: 'static', // static files copied as-is
53
+ build: {
54
+ outDir: publicDirectory,
55
+ emptyOutDir: false,
56
+ sourcemap: config.js.sourceMaps, // Enable JS/TS sourcemaps in dev
57
+ rollupOptions: {
58
+ input: {
59
+ index: entryFile
60
+ },
61
+ output: {
62
+ entryFileNames: 'js/bundle.js',
63
+ chunkFileNames: 'js/[name]-[hash].js',
64
+ assetFileNames: assetInfo => {
65
+ const ext = assetInfo.name && assetInfo.name.split('.').pop();
66
+ if (/css/i.test(ext)) return 'css/[name]-[hash][extname]';
67
+ if (/svg|png|jpg|jpeg|gif|webp|ico/i.test(ext)) return 'images/[name]-[hash][extname]';
68
+ return 'assets/[name]-[hash][extname]';
69
+ },
70
+ format: 'iife'
71
+ }
72
+ },
73
+ css: {
74
+ // devSourcemap: isDev,
75
+ preprocessorOptions: {
76
+ scss: {
77
+ includes: ['node_modules']
78
+ }
79
+ }
80
+ }
81
+ }
82
+ });
@@ -34,17 +34,17 @@ module.exports = class Emailer {
34
34
  html: body,
35
35
  attachments: [{
36
36
  filename: 'govuk_logotype_email.png',
37
- path: path.resolve(__dirname, './assets/rebrand/images/govuk_logotype_email.png'),
37
+ path: path.resolve(__dirname, './assets/images/govuk_logotype_email.png'),
38
38
  cid: 'govuk_logotype_email'
39
39
  },
40
40
  {
41
41
  filename: 'ho_crest_27px.png',
42
- path: path.resolve(__dirname, './assets/rebrand/images/ho_crest_27px.png'),
42
+ path: path.resolve(__dirname, './assets/images/ho_crest_27px.png'),
43
43
  cid: 'ho_crest_27px'
44
44
  },
45
45
  {
46
46
  filename: 'spacer.gif',
47
- path: path.resolve(__dirname, './assets/rebrand/images/spacer.gif'),
47
+ path: path.resolve(__dirname, './assets/images/spacer.gif'),
48
48
  cid: 'spacer_image'
49
49
  }]
50
50
  }, (err, result) => err ? reject(err) : resolve(result));
@@ -5,32 +5,27 @@ const toolkitImages =
5
5
  : 'node_modules/hof/frontend/toolkit/assets/rebrand/images';
6
6
 
7
7
  module.exports = {
8
- browserify: {
9
- src: 'assets/rebrand/js/index.js',
10
- out: 'public/js/bundle.js',
11
- match: 'assets/rebrand/js/**/*.js',
12
- restart: false,
13
- compress: false,
14
- debug: false
15
- },
16
8
  sass: {
17
- src: 'assets/rebrand/scss/app.scss',
9
+ src: 'assets/scss/app.scss',
18
10
  out: 'public/css/app.css',
19
- match: 'assets/rebrand/scss/**/*.scss',
11
+ match: 'assets/scss/**/*.scss',
20
12
  restart: false,
21
13
  quietDeps: false,
22
14
  outputStyle: 'expanded',
23
15
  sourceMaps: false
24
16
  },
17
+ js: {
18
+ sourceMaps: false
19
+ },
25
20
  translate: {
26
21
  src: 'apps/**/translations/src',
27
22
  match: 'apps/**/translations/src/**/*.json',
28
23
  shared: 'apps/common/translations/src'
29
24
  },
30
25
  images: {
31
- src: ['assets/rebrand/images', toolkitImages],
26
+ src: ['assets/rebrand/images', 'assets/images', toolkitImages],
32
27
  out: 'public/images',
33
- match: ['assets/rebrand/images/**/*', `${toolkitImages}/**/*`],
28
+ match: 'assets/images/**/*',
34
29
  restart: false
35
30
  },
36
31
  server: {
@@ -51,7 +51,7 @@ const defaults = {
51
51
  },
52
52
  session: {
53
53
  ttl: process.env.SESSION_TTL || 1800,
54
- secret: process.env.SESSION_SECRET || 'changethis',
54
+ secret: process.env.SESSION_SECRET,
55
55
  name: process.env.SESSION_NAME || 'hod.sid',
56
56
  sanitiseInputs: false
57
57
  },
@@ -59,6 +59,7 @@ const defaults = {
59
59
  pdfConverter: process.env.PDF_CONVERTER_URL
60
60
  },
61
61
  serveStatic: process.env.SERVE_STATIC_FILES !== 'false',
62
+ useCustomSessionTimeout: parseBoolean(process.env.USE_CUSTOM_SESSION_TIMEOUT, false, 'USE_CUSTOM_SESSION_TIMEOUT'),
62
63
  sessionTimeOutWarning: process.env.SESSION_TIMEOUT_WARNING || 300,
63
64
  serviceUnavailable: parseBoolean(process.env.SERVICE_UNAVAILABLE, false, 'SERVICE_UNAVAILABLE')
64
65
  };
@@ -4,6 +4,7 @@ module.exports = {
4
4
  htmlLang: '{{htmlLang}}',
5
5
  assetPath: '{{govukAssetPath}}',
6
6
  assetRebrandPath: '{{govukAssetPath}}rebrand/',
7
+ themeColour: '#1d70b8',
7
8
  afterHeader: '{{$afterHeader}}{{/afterHeader}}',
8
9
  bodyClasses: '{{$bodyClasses}}{{/bodyClasses}}',
9
10
  bodyStart: '{{$bodyStart}}{{/bodyStart}}',
@@ -6,15 +6,15 @@
6
6
  <meta charset="utf-8" />
7
7
  <title>{{ pageTitle }}</title>
8
8
  {{{ head }}}
9
-
10
- <meta name="theme-color" content="#1d70b8">
11
- <meta http-equiv="X-UA-Compatible" content="IE=edge">
12
- <link rel="icon" sizes="48x48" href="{{assetRebrandPath}}images/favicon.ico">
13
- <link rel="icon" sizes="any" href="{{assetRebrandPath}}images/favicon.svg" type="image/svg+xml">
14
- <link rel="mask-icon" href="{{assetRebrandPath}}images/govuk-icon-mask.svg" color="#1d70b8">
15
- <link rel="apple-touch-icon" href="{{assetRebrandPath}}images/govuk-icon-180.png">
16
- <link rel="manifest" href="{{assetRebrandPath}}manifest.json">
17
- <meta property="og:image" content="{{assetRebrandPath}}images/govuk-opengraph-image.png">
9
+ <meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
10
+ <meta name="theme-color" content="{{themeColour}}">
11
+ <meta http-equiv="X-UA-Compatible" content="IE=edge">
12
+ <link rel="icon" sizes="48x48" href="{{assetRebrandPath}}images/favicon.ico">
13
+ <link rel="icon" sizes="any" href="{{assetRebrandPath}}images/favicon.svg" type="image/svg+xml">
14
+ <link rel="mask-icon" href="{{assetRebrandPath}}images/govuk-icon-mask.svg" color="{{themeColour}}">
15
+ <link rel="apple-touch-icon" href="{{assetRebrandPath}}images/govuk-icon-180.png">
16
+ <link rel="manifest" href="{{assetRebrandPath}}manifest.json">
17
+ <meta property="og:image" content="{{assetRebrandPath}}images/govuk-opengraph-image.png">
18
18
  </head>
19
19
 
20
20
  <body class="{{ bodyClasses }} govuk-template__body js-enabled" >
@@ -6,15 +6,15 @@
6
6
  <meta charset="utf-8" />
7
7
  <title>{{$pageTitle}}{{/pageTitle}}</title>
8
8
  {{$head}}{{/head}}
9
-
10
- <meta name="theme-color" content="#1d70b8">
11
- <meta http-equiv="X-UA-Compatible" content="IE=edge">
12
- <link rel="icon" sizes="48x48" href="{{govukAssetPath}}rebrand/images/favicon.ico">
13
- <link rel="icon" sizes="any" href="{{govukAssetPath}}rebrand/images/favicon.svg" type="image/svg+xml">
14
- <link rel="mask-icon" href="{{govukAssetPath}}rebrand/images/govuk-icon-mask.svg" color="#1d70b8">
15
- <link rel="apple-touch-icon" href="{{govukAssetPath}}rebrand/images/govuk-icon-180.png">
16
- <link rel="manifest" href="{{govukAssetPath}}rebrand/manifest.json">
17
- <meta property="og:image" content="{{govukAssetPath}}rebrand/images/govuk-opengraph-image.png">
9
+ <meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
10
+ <meta name="theme-color" content="#1d70b8">
11
+ <meta http-equiv="X-UA-Compatible" content="IE=edge">
12
+ <link rel="icon" sizes="48x48" href="{{govukAssetPath}}rebrand/images/favicon.ico">
13
+ <link rel="icon" sizes="any" href="{{govukAssetPath}}rebrand/images/favicon.svg" type="image/svg+xml">
14
+ <link rel="mask-icon" href="{{govukAssetPath}}rebrand/images/govuk-icon-mask.svg" color="#1d70b8">
15
+ <link rel="apple-touch-icon" href="{{govukAssetPath}}rebrand/images/govuk-icon-180.png">
16
+ <link rel="manifest" href="{{govukAssetPath}}rebrand/manifest.json">
17
+ <meta property="og:image" content="{{govukAssetPath}}rebrand/images/govuk-opengraph-image.png">
18
18
  </head>
19
19
 
20
20
  <body class="{{$bodyClasses}}{{/bodyClasses}} govuk-template__body js-enabled" >
@@ -359,7 +359,7 @@ module.exports = function (options) {
359
359
  renderWith: inputText,
360
360
  options: {
361
361
  maxlength: 18,
362
- className: 'govuk-input govuk-input--extra-letter-spacing'
362
+ className: 'govuk-input'
363
363
  }
364
364
  },
365
365
  'input-file': {
@@ -1,6 +1,6 @@
1
1
  <div id="{{key}}-group" class="govuk-form-group {{#compound}} form-group-compound{{/compound}}{{#formGroupClassName}} {{formGroupClassName}}{{/formGroupClassName}}{{#error}} govuk-form-group--error{{/error}}">
2
2
  {{#error}}
3
- <p class="govuk-error-message" aria-hidden="true">
3
+ <p id="{{key}}-error" class="govuk-error-message" aria-hidden="true">
4
4
  <span class="govuk-visually-hidden">Error:</span> {{error.message}}
5
5
  </p>
6
6
  {{/error}}
@@ -1,7 +1,7 @@
1
1
  // App styles
2
2
 
3
3
  // set the default image directory
4
- $path: "/public/images/" !default;
4
+ $path: "/images/" !default;
5
5
 
6
6
  // GOV.UK front end toolkit
7
7
  // Sass variables, mixins and functions
@@ -1,10 +1,10 @@
1
1
  module.exports = {
2
- helpers: require('./assets/rebrand/javascript/helpers'),
3
- formFocus: require('./assets/rebrand/javascript/form-focus'),
4
- progressiveReveal: require('./assets/rebrand/javascript/progressive-reveal'),
5
- validation: require('./assets/rebrand/javascript/validation'),
2
+ helpers: require('./assets/javascript/helpers'),
3
+ formFocus: require('./assets/javascript/form-focus'),
4
+ progressiveReveal: require('./assets/javascript/progressive-reveal'),
5
+ validation: require('./assets/javascript/validation'),
6
6
  detailsSummary: function () {
7
- require('./assets/rebrand/javascript/vendor/details.polyfill');
7
+ require('./assets/javascript/vendor/details.polyfill');
8
8
  },
9
- characterCount: require('./assets/rebrand/javascript/character-count')
9
+ characterCount: require('./assets/javascript/character-count')
10
10
  };
package/index.js CHANGED
@@ -158,7 +158,7 @@ function bootstrap(options) {
158
158
  res.locals.htmlLang = config.htmlLang;
159
159
  res.locals.cookieName = config.session.name;
160
160
  // session timeout warning configs
161
- res.locals.sessionTimeOut = config.session.ttl;
161
+ res.locals.sessionTimeOut = process.env.CUSTOM_SESSION_EXPIRY || config.session.ttl;
162
162
  res.locals.sessionTimeOutWarning = config.sessionTimeOutWarning;
163
163
  res.locals.sessionTimeoutWarningContent = config.sessionTimeoutWarningContent;
164
164
  res.locals.exitFormContent = config.exitFormContent;
@@ -238,8 +238,8 @@ function bootstrap(options) {
238
238
  app.use(hofMiddleware.rateLimiter(config, 'requests'));
239
239
  }
240
240
 
241
- // Set up routing so <YOUR-SITE-URL>/assets are served from /node_modules/govuk-frontend/govuk/assets/rebrand
242
- app.use('/assets', express.static(path.join(__dirname, '/node_modules/govuk-frontend/govuk/assets/rebrand')));
241
+ // Set up routing so <YOUR-SITE-URL>/assets are served from /node_modules/govuk-frontend/govuk/assets
242
+ app.use('/assets', express.static(path.join(__dirname, '/node_modules/govuk-frontend/govuk/assets')));
243
243
  // Check if service has been paused and redirect accordingly
244
244
  const bypassPaths = ['/assets', '/healthcheck', '/service-unavailable'];
245
245
 
@@ -254,26 +254,32 @@ function bootstrap(options) {
254
254
 
255
255
  /**
256
256
  * Handles requests to the session timeout page.
257
+ * For custom session timeout handling that is not linked to the redis session ttl,
258
+ * set `CUSTOM_SESSION_EXPIRY` variables to the relevant time and `USE_CUSTOM_SESSION_TIMEOUT`variables to true.
259
+ * If `CUSTOM_SESSION_EXPIRY` and `USE_CUSTOM_SESSION_TIMEOUT` envs are set,
260
+ * include '/session-timeout' step in the project's index.js.
257
261
  * - If the user has a session cookie but their session is missing or inactive,
258
262
  * this triggers a session timeout error handled by error middleware.
259
263
  * - Otherwise, responds with a 404 "Page Not Found" error.
260
264
  * This route ensures the timeout page only appears after an actual session expiry.
261
265
  */
262
- app.get('/session-timeout', (req, res, next) => {
263
- if ((req.cookies['hof-wizard-sc']) && (!req.session || req.session.exists !== true)) {
264
- const err = new Error('Session expired');
265
- err.code = 'SESSION_TIMEOUT';
266
- return next(err);
267
- }
268
- const err = new Error('Not Found');
269
- err.status = 404;
270
- const locals = Object.assign({}, req.translate('errors'));
271
- if (locals && locals['404']) {
272
- return res.status(404).render('404', locals['404']);
273
- }
274
- // Fallback: render a basic 404 page if translation is missing
275
- return res.status(404).send('Page Not Found');
276
- });
266
+ if (!config.useCustomSessionTimeout) {
267
+ app.get('/session-timeout', (req, res, next) => {
268
+ if ((req.cookies['hof-wizard-sc']) && (!req.session || req.session.exists !== true)) {
269
+ const err = new Error('Session expired');
270
+ err.code = 'SESSION_TIMEOUT';
271
+ return next(err);
272
+ }
273
+ const err = new Error('Not Found');
274
+ err.status = 404;
275
+ const locals = Object.assign({}, req.translate('errors'));
276
+ if (locals && locals['404']) {
277
+ return res.status(404).render('404', locals['404']);
278
+ }
279
+ // Fallback: render a basic 404 page if translation is missing
280
+ return res.status(404).send('Page Not Found');
281
+ });
282
+ }
277
283
 
278
284
  if (config.getAccessibility === true) {
279
285
  deprecate(
package/lib/encryption.js CHANGED
@@ -1,23 +1,49 @@
1
- /* eslint-disable */
2
1
  'use strict';
3
2
 
4
- const crypto = require('crypto');
3
+ const crypto = require('node:crypto');
5
4
  const algorithm = 'aes-256-cbc';
5
+ const ivLength = 16;
6
6
 
7
- module.exports = password => ({
8
-
9
- encrypt: text => {
10
- const cipher = crypto.createCipher(algorithm, password);
11
- let crypted = cipher.update(text, 'utf8', 'hex');
12
- crypted += cipher.final('hex');
13
- return crypted;
14
- },
15
-
16
- decrypt: text => {
17
- const decipher = crypto.createDecipher(algorithm, password);
18
- let dec = decipher.update(text, 'hex', 'utf8');
19
- dec += decipher.final('utf8');
20
- return dec;
7
+ /**
8
+ * Creates an encryption utility with AES-256-CBC algorithm.
9
+ * Provides encrypt and decrypt methods that use a random IV for each encryption operation.
10
+ *
11
+ * @module encryption
12
+ * @param {string|Buffer} secret - Must be exactly 32 bytes
13
+ * @returns {Object} Encryption utility object
14
+ * @throws {Error} If secret is not exactly 32 bytes
15
+ */
16
+ module.exports = secret => {
17
+ const encryptionKey = Buffer.from(secret, 'utf8');
18
+ if (encryptionKey.byteLength !== 32) {
19
+ throw new Error(`Encryption secret must be exactly 32 bytes. Provided: ${encryptionKey.byteLength} bytes.`);
21
20
  }
22
21
 
23
- });
22
+ return {
23
+ encrypt: text => {
24
+ try {
25
+ const iv = crypto.randomBytes(ivLength);
26
+ const cipher = crypto.createCipheriv(algorithm, encryptionKey, iv);
27
+ let encrypted = cipher.update(text, 'utf8');
28
+ encrypted = Buffer.concat([encrypted, cipher.final()]);
29
+ return iv.toString('hex') + ':' + encrypted.toString('hex');
30
+ } catch (error) {
31
+ throw new Error(`Encryption failed: ${error.message}`);
32
+ }
33
+ },
34
+
35
+ decrypt: text => {
36
+ try {
37
+ const textParts = text.split(':');
38
+ const iv = Buffer.from(textParts.shift(), 'hex');
39
+ const encryptedText = Buffer.from(textParts.join(':'), 'hex');
40
+ const decipher = crypto.createDecipheriv(algorithm, encryptionKey, iv);
41
+ let decrypted = decipher.update(encryptedText);
42
+ decrypted = Buffer.concat([decrypted, decipher.final()]);
43
+ return decrypted.toString('utf8');
44
+ } catch (error) {
45
+ throw new Error(`Decryption failed: ${error.message}`);
46
+ }
47
+ }
48
+ };
49
+ };
package/lib/sessions.js CHANGED
@@ -10,8 +10,11 @@ const secureHttps = config => config.protocol === 'https' || config.env === 'pro
10
10
  module.exports = (app, config) => {
11
11
  const logger = config.logger || console;
12
12
 
13
- if (config.env === 'production' && config.session.secret === 'changethis') {
14
- throw new Error('Session secret must be set to a unique random string for production environments');
13
+ const secretBuffer = Buffer.from(config.session.secret, 'utf8');
14
+ if (secretBuffer.byteLength !== 32) {
15
+ throw new Error(
16
+ `Session secret must be exactly 32 bytes. Current: ${secretBuffer.byteLength} bytes.`
17
+ );
15
18
  }
16
19
 
17
20
  app.use(cookieParser(config.session.secret, {