From 8b08ff3691c1b3a4a3bb38434c5a6f769c813528 Mon Sep 17 00:00:00 2001 From: Mohammed Khalid Date: Wed, 13 May 2026 13:15:26 +0100 Subject: [PATCH 1/5] feat(DF-789): block save-and-exit and help routes when form is offline --- .../plugins/error-preview/error-preview.js | 9 +-- src/server/plugins/router.ts | 16 +++-- src/server/routes/index.test.ts | 3 + src/server/routes/save-and-exit.js | 69 +++++++++++++++---- src/server/services/formMetadataGuards.js | 60 ++++++++++++++++ 5 files changed, 131 insertions(+), 26 deletions(-) create mode 100644 src/server/services/formMetadataGuards.js diff --git a/src/server/plugins/error-preview/error-preview.js b/src/server/plugins/error-preview/error-preview.js index 3ca1652ea..9cdd8dbc4 100644 --- a/src/server/plugins/error-preview/error-preview.js +++ b/src/server/plugins/error-preview/error-preview.js @@ -2,10 +2,8 @@ import { FormStatus } from '@defra/forms-engine-plugin/types' import Boom from '@hapi/boom' import { createErrorPreviewModel } from '~/src/server/plugins/error-preview/error-preview-helper.js' -import { - getFormDefinition, - getFormMetadata -} from '~/src/server/services/formsService.js' +import { getFormMetadata } from '~/src/server/services/formMetadataGuards.js' +import { getFormDefinition } from '~/src/server/services/formsService.js' /** * @param {FormRequest} request @@ -15,10 +13,7 @@ export async function getErrorPreviewHandler(request, h) { const { params } = request const { slug, path, itemId } = params - // Get the form metadata using the `slug` param const metadata = await getFormMetadata(slug) - - // Get the form definition using the `id` from the metadata const definition = await getFormDefinition(metadata.id, FormStatus.Draft) if (!definition) { throw Boom.notFound( diff --git a/src/server/plugins/router.ts b/src/server/plugins/router.ts index 357f7bd93..2809baca5 100644 --- a/src/server/plugins/router.ts +++ b/src/server/plugins/router.ts @@ -34,10 +34,8 @@ import { publicRoutes, saveAndExitRoutes } from '~/src/server/routes/index.js' -import { - getFormDefinition, - getFormMetadata -} from '~/src/server/services/formsService.js' +import { getFormMetadata } from '~/src/server/services/formMetadataGuards.js' +import { getFormDefinition } from '~/src/server/services/formsService.js' import { getFeedbackFormLink } from '~/src/server/utils/utils.js' const routes: ServerRoute[] = [...publicRoutes, healthRoute] @@ -170,10 +168,13 @@ export default { options }) - server.route({ + server.route<{ Params: { slug: string } }>({ method: 'get', path: '/help/cookies/{slug}', async handler(request, h) { + const { slug } = request.params + await getFormMetadata(slug) + const sessionTimeout = config.get('sessionTimeout') const sessionDurationPretty = humanizeDuration(sessionTimeout) @@ -307,7 +308,10 @@ export default { server.route<{ Params: { slug: string } }>({ method: 'get', path: '/help/accessibility-statement/{slug}', - handler(_request, h) { + async handler(request, h) { + const { slug } = request.params + await getFormMetadata(slug) + return h.view('help/accessibility-statement') }, options diff --git a/src/server/routes/index.test.ts b/src/server/routes/index.test.ts index 85ad24790..235be0b01 100644 --- a/src/server/routes/index.test.ts +++ b/src/server/routes/index.test.ts @@ -26,6 +26,7 @@ describe('Routes', () => { }) test('cookies page is served with 24 hour duration and GA info', async () => { + jest.mocked(getFormMetadata).mockResolvedValue(fixtures.form.metadata) config.set('sessionTimeout', 86400000) config.set('googleTagManagerContainerId', 'GTM-XXXXXXXX') config.set('googleAnalyticsContainerId', 'YYYYYYYYYY') @@ -62,6 +63,7 @@ describe('Routes', () => { }) test('cookies page is served without GA info', async () => { + jest.mocked(getFormMetadata).mockResolvedValue(fixtures.form.metadata) config.set('sessionTimeout', 86400000) config.set('googleTagManagerContainerId', '') config.set('googleAnalyticsContainerId', '') @@ -97,6 +99,7 @@ describe('Routes', () => { }) test('accessibility statement page is served', async () => { + jest.mocked(getFormMetadata).mockResolvedValue(fixtures.form.metadata) const options = { method: 'GET', url: '/help/accessibility-statement/slug' diff --git a/src/server/routes/save-and-exit.js b/src/server/routes/save-and-exit.js index 6ac1ddbab..ad3a1e308 100644 --- a/src/server/routes/save-and-exit.js +++ b/src/server/routes/save-and-exit.js @@ -35,6 +35,9 @@ import { import { getFormMetadata, getFormMetadataById, + isOfflineBoom +} from '~/src/server/services/formMetadataGuards.js' +import { getSaveAndExitDetails, validateSaveAndExitCredentials } from '~/src/server/services/formsService.js' @@ -148,7 +151,10 @@ export default [ const { params, payload } = request const { slug, state: status } = params const { email, securityQuestion, securityAnswer } = payload + // Throws the offline marker BEFORE publishSaveAndExitEvent so we never + // emit a magic-link email for a form the user can no longer reach. const metadata = await getFormMetadata(slug) + const cacheService = getCacheService(request.server) // Publish topic message @@ -201,6 +207,7 @@ export default [ const { params, payload } = request const { slug, state: status } = params const metadata = await getFormMetadata(slug) + const model = detailsViewModel( metadata, status, @@ -255,11 +262,15 @@ export default [ const { params } = request const { formId, magicLinkId } = params - // Check form id + // Asserts the form is online BEFORE looking up the magic link, so we + // don't reveal link validity timing for offline forms. let form try { form = await getFormMetadataById(formId) } catch (err) { + if (isOfflineBoom(err)) { + throw err + } logger.error( err, `Invalid formId ${formId} in magic link id ${magicLinkId}` @@ -338,17 +349,16 @@ export default [ async handler(request, h) { const { params } = request const { formId, magicLinkId } = params - const resumeDetails = await getSaveAndExitDetails(magicLinkId) - if (!resumeDetails) { - return h.redirect(ERROR_BASE_URL) - } - - // Check form id + // Assert the form is online BEFORE looking up save-and-exit details so + // we don't leak magic-link validity timing for offline forms. let form try { - form = await getFormMetadataById(resumeDetails.form.id) + form = await getFormMetadataById(formId) } catch (err) { + if (isOfflineBoom(err)) { + throw err + } logger.error( err, `Invalid formId ${formId} in magic link id ${magicLinkId}` @@ -356,6 +366,12 @@ export default [ return h.redirect(ERROR_BASE_URL) } + const resumeDetails = await getSaveAndExitDetails(magicLinkId) + + if (!resumeDetails) { + return h.redirect(ERROR_BASE_URL) + } + const model = passwordViewModel( form, resumeDetails.question, @@ -376,9 +392,25 @@ export default [ ({ method: 'GET', path: '/resume-form-error/{slug?}', - handler(request, h) { + async handler(request, h) { const { params } = request const { slug } = params + + if (slug) { + try { + await getFormMetadata(slug) + } catch (err) { + if (isOfflineBoom(err)) { + throw err + } + // Fall through to the existing error view if metadata can't be fetched. + logger.info( + { err }, + `Could not load metadata for resume-form-error slug ${slug}; rendering generic error view` + ) + } + } + const model = resumeErrorViewModel({ slug }) return h.view(RESUME_ERROR, model) @@ -404,15 +436,25 @@ export default [ const { formId, magicLinkId } = params const { securityAnswer } = payload - // Validate the security answer + let form + try { + form = await getFormMetadataById(formId) + } catch (err) { + if (isOfflineBoom(err)) { + throw err + } + logger.error( + err, + `Invalid formId ${formId} in magic link id ${magicLinkId}` + ) + return h.redirect(ERROR_BASE_URL) + } + const validatedLink = await validateSaveAndExitCredentials( magicLinkId, securityAnswer ) - // Reload form title in case it has changed - const form = await getFormMetadataById(formId) - if (validatedLink.validPassword) { // Restore state const cacheService = getCacheService(request.server) @@ -499,6 +541,7 @@ export default [ const { params } = request const { slug, state } = params const form = await getFormMetadata(slug) + const model = resumeSuccessViewModel( form, /** @type {FormStatus | undefined} */ (state) diff --git a/src/server/services/formMetadataGuards.js b/src/server/services/formMetadataGuards.js new file mode 100644 index 000000000..254d13c55 --- /dev/null +++ b/src/server/services/formMetadataGuards.js @@ -0,0 +1,60 @@ +import Boom from '@hapi/boom' + +import * as rawFormsService from '~/src/server/services/formsService.js' + +/** + * Throws a Boom 503 stamped with `data.offline = true` when the form is + * offline. The engine plugin's onPreResponse extension catches the marker + * and renders the unavailable view at HTTP 200. + * @param {FormMetadata} metadata + */ +function assertFormAvailable(metadata) { + if (metadata.offline === true) { + throw Boom.boomify(new Error(`Form ${metadata.slug} is offline`), { + statusCode: 503, + data: { offline: true, metadata } + }) + } +} + +/** + * Returns true when the error is the offline marker thrown by the wrappers. + * Re-throw from `catch` blocks so the engine plugin's onPreResponse can + * render the unavailable view. + * @param {unknown} err + * @returns {boolean} + */ +export function isOfflineBoom(err) { + return ( + Boom.isBoom(err) && + !!err.data && + typeof err.data === 'object' && + /** @type {{ offline?: boolean }} */ (err.data).offline === true + ) +} + +/** + * Fetch form metadata by slug. Throws the offline marker when the form has + * been taken offline so route handlers don't have to check. + * @param {string} slug + */ +export async function getFormMetadata(slug) { + const metadata = await rawFormsService.getFormMetadata(slug) + assertFormAvailable(metadata) + return metadata +} + +/** + * Fetch form metadata by id. Throws the offline marker when the form has + * been taken offline. + * @param {string} formId + */ +export async function getFormMetadataById(formId) { + const metadata = await rawFormsService.getFormMetadataById(formId) + assertFormAvailable(metadata) + return metadata +} + +/** + * @import { FormMetadata } from '@defra/forms-model' + */ From e5286805c05a27a4a2cc037af0810f479e8e7ef5 Mon Sep 17 00:00:00 2001 From: Mohammed Khalid Date: Wed, 13 May 2026 13:23:53 +0100 Subject: [PATCH 2/5] test(DF-789): cover formMetadataGuards (offline marker + wrappers) --- .../services/formMetadataGuards.test.js | 71 +++++++++++++++++++ 1 file changed, 71 insertions(+) create mode 100644 src/server/services/formMetadataGuards.test.js diff --git a/src/server/services/formMetadataGuards.test.js b/src/server/services/formMetadataGuards.test.js new file mode 100644 index 000000000..1a4204008 --- /dev/null +++ b/src/server/services/formMetadataGuards.test.js @@ -0,0 +1,71 @@ +import Boom from '@hapi/boom' + +import { + getFormMetadata, + getFormMetadataById, + isOfflineBoom +} from '~/src/server/services/formMetadataGuards.js' +import { + getFormMetadata as rawGetFormMetadata, + getFormMetadataById as rawGetFormMetadataById +} from '~/src/server/services/formsService.js' + +jest.mock('~/src/server/services/formsService.js') + +const onlineForm = { id: 'form-1', slug: 'my-form', offline: false } +const offlineForm = { id: 'form-1', slug: 'my-form', offline: true } + +describe('formMetadataGuards', () => { + beforeEach(() => { + jest.clearAllMocks() + }) + + describe('getFormMetadata', () => { + it('returns metadata when the form is not offline', async () => { + jest.mocked(rawGetFormMetadata).mockResolvedValue(onlineForm) + await expect(getFormMetadata('my-form')).resolves.toBe(onlineForm) + }) + + it('throws an offline-marker Boom when the form is offline', async () => { + jest.mocked(rawGetFormMetadata).mockResolvedValue(offlineForm) + await expect(getFormMetadata('my-form')).rejects.toMatchObject({ + isBoom: true, + output: { statusCode: 503 }, + data: { offline: true, metadata: offlineForm } + }) + }) + }) + + describe('getFormMetadataById', () => { + it('returns metadata when the form is not offline', async () => { + jest.mocked(rawGetFormMetadataById).mockResolvedValue(onlineForm) + await expect(getFormMetadataById('form-1')).resolves.toBe(onlineForm) + }) + + it('throws an offline-marker Boom when the form is offline', async () => { + jest.mocked(rawGetFormMetadataById).mockResolvedValue(offlineForm) + await expect(getFormMetadataById('form-1')).rejects.toMatchObject({ + isBoom: true, + data: { offline: true } + }) + }) + }) + + describe('isOfflineBoom', () => { + it('returns true for the offline-marker Boom', () => { + const err = Boom.boomify(new Error('offline'), { + statusCode: 503, + data: { offline: true, metadata: offlineForm } + }) + expect(isOfflineBoom(err)).toBe(true) + }) + + it('returns false for a non-Boom error', () => { + expect(isOfflineBoom(new Error('boom'))).toBe(false) + }) + + it('returns false for a Boom without the offline marker', () => { + expect(isOfflineBoom(Boom.notFound('nope'))).toBe(false) + }) + }) +}) From 035b03d76c41786540e9bceebe1f6b16e2653c4f Mon Sep 17 00:00:00 2001 From: Mohammed Khalid Date: Wed, 13 May 2026 22:08:58 +0100 Subject: [PATCH 3/5] test: increase coverage --- .../services/formMetadataGuards.test.js | 20 +++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/src/server/services/formMetadataGuards.test.js b/src/server/services/formMetadataGuards.test.js index 1a4204008..b1e16d659 100644 --- a/src/server/services/formMetadataGuards.test.js +++ b/src/server/services/formMetadataGuards.test.js @@ -22,12 +22,18 @@ describe('formMetadataGuards', () => { describe('getFormMetadata', () => { it('returns metadata when the form is not offline', async () => { - jest.mocked(rawGetFormMetadata).mockResolvedValue(onlineForm) + jest + .mocked(rawGetFormMetadata) + // @ts-expect-error - allow partial objects for tests + .mockResolvedValue(onlineForm) await expect(getFormMetadata('my-form')).resolves.toBe(onlineForm) }) it('throws an offline-marker Boom when the form is offline', async () => { - jest.mocked(rawGetFormMetadata).mockResolvedValue(offlineForm) + jest + .mocked(rawGetFormMetadata) + // @ts-expect-error - allow partial objects for tests + .mockResolvedValue(offlineForm) await expect(getFormMetadata('my-form')).rejects.toMatchObject({ isBoom: true, output: { statusCode: 503 }, @@ -38,12 +44,18 @@ describe('formMetadataGuards', () => { describe('getFormMetadataById', () => { it('returns metadata when the form is not offline', async () => { - jest.mocked(rawGetFormMetadataById).mockResolvedValue(onlineForm) + jest + .mocked(rawGetFormMetadataById) + // @ts-expect-error - allow partial objects for tests + .mockResolvedValue(onlineForm) await expect(getFormMetadataById('form-1')).resolves.toBe(onlineForm) }) it('throws an offline-marker Boom when the form is offline', async () => { - jest.mocked(rawGetFormMetadataById).mockResolvedValue(offlineForm) + jest + .mocked(rawGetFormMetadataById) + // @ts-expect-error - allow partial objects for tests + .mockResolvedValue(offlineForm) await expect(getFormMetadataById('form-1')).rejects.toMatchObject({ isBoom: true, data: { offline: true } From e409166a08928387f383251440f3003c33a39e03 Mon Sep 17 00:00:00 2001 From: Mohammed Khalid Date: Thu, 14 May 2026 00:13:09 +0100 Subject: [PATCH 4/5] test: increase coverage for form offline feature in forms-runner --- src/server/routes/save-and-exit.test.js | 160 ++++++++++++++++++++++++ 1 file changed, 160 insertions(+) diff --git a/src/server/routes/save-and-exit.test.js b/src/server/routes/save-and-exit.test.js index 345d63ec7..83a78bb07 100644 --- a/src/server/routes/save-and-exit.test.js +++ b/src/server/routes/save-and-exit.test.js @@ -2,17 +2,22 @@ import { FormStatus } from '@defra/forms-model' import Boom from '@hapi/boom' import { StatusCodes } from 'http-status-codes' +import { logger } from '~/src/server/common/helpers/logging/logger.js' import { createJoiError } from '~/src/server/helpers/error-helper.js' import { createServer } from '~/src/server/index.js' import { addError } from '~/src/server/routes/save-and-exit.js' import { getFormMetadata, getFormMetadataById, + isOfflineBoom +} from '~/src/server/services/formMetadataGuards.js' +import { getSaveAndExitDetails, validateSaveAndExitCredentials } from '~/src/server/services/formsService.js' import { renderResponse } from '~/test/helpers/component-helpers.js' +jest.mock('~/src/server/services/formMetadataGuards.js') jest.mock('~/src/server/services/formsService.js') jest.mock('~/src/server/helpers/error-helper.js') @@ -29,6 +34,12 @@ describe('Save-and-exit check routes', () => { beforeEach(() => { jest.clearAllMocks() + jest.spyOn(logger, 'error').mockImplementation(() => { + /* mock */ + }) + jest.spyOn(logger, 'info').mockImplementation(() => { + /* mock */ + }) }) const FORM_ID = 'eab6ac6c-79b6-439f-bd94-d93eb121b3f1' @@ -202,6 +213,43 @@ describe('Save-and-exit check routes', () => { '/resume-form-error/my-form-to-resume' ) }) + + test('throws if form is offline', async () => { + const offlineErr = Boom.boomify(new Error('offline'), { + statusCode: 503, + data: { offline: true } + }) + jest.mocked(getFormMetadataById).mockRejectedValueOnce(offlineErr) + jest.mocked(isOfflineBoom).mockReturnValueOnce(true) + + const options = { + method: 'GET', + url: `/resume-form/${FORM_ID}/${MAGIC_LINK_ID}` + } + + const response = await server.inject(options) + expect(response.statusCode).toBe(StatusCodes.SERVICE_UNAVAILABLE) + }) + + test('logs error and redirects on other metadata fetch error', async () => { + const otherErr = new Error('fetch failed') + jest.mocked(getFormMetadataById).mockRejectedValueOnce(otherErr) + jest.mocked(isOfflineBoom).mockReturnValueOnce(false) + + const options = { + method: 'GET', + url: `/resume-form/${FORM_ID}/${MAGIC_LINK_ID}` + } + + const { response } = await renderResponse(server, options) + + expect(response.statusCode).toBe(StatusCodes.SEE_OTHER) + expect(response.headers.location).toBe('/resume-form-error') + expect(logger.error).toHaveBeenCalledWith( + otherErr, + `Invalid formId ${FORM_ID} in magic link id ${MAGIC_LINK_ID}` + ) + }) }) describe('GET /resume-form-verify/{formId}/{magicLinkId}/{slug}/state?}', () => { @@ -275,6 +323,43 @@ describe('Save-and-exit check routes', () => { expect(response.statusCode).toBe(StatusCodes.MOVED_TEMPORARILY) expect(response.headers.location).toBe('/resume-form-error') }) + + test('throws if form is offline', async () => { + const offlineErr = Boom.boomify(new Error('offline'), { + statusCode: 503, + data: { offline: true } + }) + jest.mocked(getFormMetadataById).mockRejectedValueOnce(offlineErr) + jest.mocked(isOfflineBoom).mockReturnValueOnce(true) + + const options = { + method: 'GET', + url: `/resume-form-verify/${FORM_ID}/${MAGIC_LINK_ID}/my-form-to-resume/draft` + } + + const response = await server.inject(options) + expect(response.statusCode).toBe(StatusCodes.SERVICE_UNAVAILABLE) + }) + + test('logs error and redirects on other metadata fetch error', async () => { + const otherErr = new Error('fetch failed') + jest.mocked(getFormMetadataById).mockRejectedValueOnce(otherErr) + jest.mocked(isOfflineBoom).mockReturnValueOnce(false) + + const options = { + method: 'GET', + url: `/resume-form-verify/${FORM_ID}/${MAGIC_LINK_ID}/my-form-to-resume/draft` + } + + const { response } = await renderResponse(server, options) + + expect(response.statusCode).toBe(StatusCodes.MOVED_TEMPORARILY) + expect(response.headers.location).toBe('/resume-form-error') + expect(logger.error).toHaveBeenCalledWith( + otherErr, + `Invalid formId ${FORM_ID} in magic link id ${MAGIC_LINK_ID}` + ) + }) }) describe('GET /resume-form-error', () => { @@ -322,6 +407,42 @@ describe('Save-and-exit check routes', () => { expect($button).toBeInTheDocument() expect($button).toHaveAttribute('href', '/form/my-slug') }) + + test('throws if form is offline', async () => { + const offlineErr = Boom.boomify(new Error('offline'), { + statusCode: 503, + data: { offline: true } + }) + jest.mocked(getFormMetadata).mockRejectedValueOnce(offlineErr) + jest.mocked(isOfflineBoom).mockReturnValueOnce(true) + + const options = { + method: 'GET', + url: '/resume-form-error/my-slug' + } + + const response = await server.inject(options) + expect(response.statusCode).toBe(StatusCodes.SERVICE_UNAVAILABLE) + }) + + test('logs info on other metadata fetch error', async () => { + const otherErr = new Error('fetch failed') + jest.mocked(getFormMetadata).mockRejectedValueOnce(otherErr) + jest.mocked(isOfflineBoom).mockReturnValueOnce(false) + + const options = { + method: 'GET', + url: '/resume-form-error/my-slug' + } + + const { response } = await renderResponse(server, options) + + expect(response.statusCode).toBe(StatusCodes.OK) + expect(logger.info).toHaveBeenCalledWith( + { err: otherErr }, + 'Could not load metadata for resume-form-error slug my-slug; rendering generic error view' + ) + }) }) describe('GET /resume-form-success', () => { @@ -536,6 +657,45 @@ describe('Save-and-exit check routes', () => { expect(response.statusCode).toBe(StatusCodes.MOVED_TEMPORARILY) expect(response.headers.location).toBe('/resume-form-error') }) + + test('throws if form is offline', async () => { + const offlineErr = Boom.boomify(new Error('offline'), { + statusCode: 503, + data: { offline: true } + }) + jest.mocked(getFormMetadataById).mockRejectedValueOnce(offlineErr) + jest.mocked(isOfflineBoom).mockReturnValueOnce(true) + + const options = { + method: 'POST', + url: `/resume-form-verify/${FORM_ID}/${MAGIC_LINK_ID}/my-form-to-resume`, + payload: { securityAnswer: 'any' } + } + + const response = await server.inject(options) + expect(response.statusCode).toBe(StatusCodes.SERVICE_UNAVAILABLE) + }) + + test('logs error and redirects on other metadata fetch error', async () => { + const otherErr = new Error('fetch failed') + jest.mocked(getFormMetadataById).mockRejectedValueOnce(otherErr) + jest.mocked(isOfflineBoom).mockReturnValueOnce(false) + + const options = { + method: 'POST', + url: `/resume-form-verify/${FORM_ID}/${MAGIC_LINK_ID}/my-form-to-resume`, + payload: { securityAnswer: 'any' } + } + + const { response } = await renderResponse(server, options) + + expect(response.statusCode).toBe(StatusCodes.MOVED_TEMPORARILY) + expect(response.headers.location).toBe('/resume-form-error') + expect(logger.error).toHaveBeenCalledWith( + otherErr, + `Invalid formId ${FORM_ID} in magic link id ${MAGIC_LINK_ID}` + ) + }) }) describe('addError', () => { From 117d584871f112397412f7798aa52ef0aa0977a7 Mon Sep 17 00:00:00 2001 From: Mohammed Khalid Date: Thu, 14 May 2026 15:11:33 +0100 Subject: [PATCH 5/5] chore(deps): update @defra/forms-engine-plugin to 4.12.0 and @defra/forms-model to 3.0.665 in package.json and package-lock.json --- package-lock.json | 10 +++++----- package.json | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/package-lock.json b/package-lock.json index 19a06bcb0..ecd7bf825 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,7 +11,7 @@ "license": "SEE LICENSE IN LICENSE", "dependencies": { "@aws-sdk/client-sns": "^3.997.0", - "@defra/forms-engine-plugin": "^4.11.3", + "@defra/forms-engine-plugin": "^4.12.0", "@defra/forms-model": "^3.0.665", "@defra/hapi-tracing": "^1.30.0", "@elastic/ecs-pino-format": "^1.5.0", @@ -4043,13 +4043,13 @@ } }, "node_modules/@defra/forms-engine-plugin": { - "version": "4.11.3", - "resolved": "https://registry.npmjs.org/@defra/forms-engine-plugin/-/forms-engine-plugin-4.11.3.tgz", - "integrity": "sha512-+ywwUqRHH9iRHKbU4BFzLyFMakVs7/0e5tlu+vBSWtflO/EWTefHD4nAfkoEDgU2/y4fWBX4+Dub/rx7iHE9AA==", + "version": "4.12.0", + "resolved": "https://registry.npmjs.org/@defra/forms-engine-plugin/-/forms-engine-plugin-4.12.0.tgz", + "integrity": "sha512-aVgFfrLCoO+PxU/64KF/zi82YzXIi/P0p6UYzYVtiebzuwt2ZpwDb4yICreiNqbwRJ4lZmh3MbgNUVEjH85eKw==", "hasInstallScript": true, "license": "SEE LICENSE IN LICENSE", "dependencies": { - "@defra/forms-model": "^3.0.655", + "@defra/forms-model": "^3.0.663", "@defra/hapi-tracing": "^1.29.0", "@defra/interactive-map": "^0.0.22-alpha", "@elastic/ecs-pino-format": "^1.5.0", diff --git a/package.json b/package.json index 91f530738..4e1088970 100644 --- a/package.json +++ b/package.json @@ -46,7 +46,7 @@ "license": "SEE LICENSE IN LICENSE", "dependencies": { "@aws-sdk/client-sns": "^3.997.0", - "@defra/forms-engine-plugin": "^4.11.3", + "@defra/forms-engine-plugin": "^4.12.0", "@defra/forms-model": "^3.0.665", "@defra/hapi-tracing": "^1.30.0", "@elastic/ecs-pino-format": "^1.5.0",