From 23382eae6987120af23deaada13724cc76d66a9e Mon Sep 17 00:00:00 2001 From: Jani Kraner Date: Thu, 7 May 2026 20:03:12 +0100 Subject: [PATCH 1/7] Allow optional `santiseContentFor` parameter notifications-api accepts `sanitise_content_for` payload - https://github.com/alphagov/notifications-api/blob/main/app/v2/notifications/post_notifications.py#L134 - an array of strings denoting personalisations keys that it sanitises. This PR extends the client to allow this payload to be sent to the API with `santiseContentFor`. Naming matches our other parameters. API also returns `sanitised_content` object - https://github.com/alphagov/notifications-api/blob/main/app/v2/notifications/post_notifications.py#L215 - which we list in the `@returns` object notation. --- client/notification.js | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/client/notification.js b/client/notification.js index f013ceeb..270014d0 100644 --- a/client/notification.js +++ b/client/notification.js @@ -82,10 +82,11 @@ function NotifyClient(apiKeyOrUrl, serviceIdOrApiKey, apiKeyId) { * @param {String} reference * @param {String} replyToId * @param {String} oneClickUnsubscribeURL + * @param {String[]} [sanitiseContentFor] * * @returns {Object} */ -function createNotificationPayload(type, templateId, to, personalisation, reference, replyToId, oneClickUnsubscribeURL) { +function createNotificationPayload(type, templateId, to, personalisation, reference, replyToId, oneClickUnsubscribeURL, sanitiseContentFor) { var payload = { template_id: templateId @@ -116,6 +117,10 @@ function createNotificationPayload(type, templateId, to, personalisation, refere payload.one_click_unsubscribe_url = oneClickUnsubscribeURL; } + if (sanitiseContentFor && type == 'email') { + payload.sanitise_content_for = sanitiseContentFor; + } + return payload; } @@ -187,22 +192,23 @@ function _check_and_encode_file(file, size_limit) { /** * @param {string} templateId * @param {string} emailAddress - * @param {{personalisation?: Object, reference?: string, emailReplyToId?: string, oneClickUnsubscribeURL?: string}} [options] - * @returns {Promise>} + * @param {{personalisation?: Object, reference?: string, emailReplyToId?: string, oneClickUnsubscribeURL?: string, sanitiseContentFor?: string[]}} [options] + * @returns {Promise>, uri: string, template: TemplateRef}>>} */ NotifyClient.prototype.sendEmail = function (templateId, emailAddress, options) { options = options || {}; - var err = checkOptionsKeys(['personalisation', 'reference', 'emailReplyToId', 'oneClickUnsubscribeURL'], options) + var err = checkOptionsKeys(['personalisation', 'reference', 'emailReplyToId', 'oneClickUnsubscribeURL', 'sanitiseContentFor'], options) if (err) { return Promise.reject(err); } var personalisation = options.personalisation || undefined, reference = options.reference || undefined, emailReplyToId = options.emailReplyToId || undefined, - oneClickUnsubscribeURL = options.oneClickUnsubscribeURL || undefined; + oneClickUnsubscribeURL = options.oneClickUnsubscribeURL || undefined, + sanitiseContentFor = options.sanitiseContentFor || undefined; return this.apiClient.post('/v2/notifications/email', - createNotificationPayload('email', templateId, emailAddress, personalisation, reference, emailReplyToId, oneClickUnsubscribeURL)); + createNotificationPayload('email', templateId, emailAddress, personalisation, reference, emailReplyToId, oneClickUnsubscribeURL, sanitiseContentFor)); }; /** From 9981a0dd50f0e23cbc002d7979cc05bd52748986 Mon Sep 17 00:00:00 2001 From: Jani Kraner Date: Thu, 7 May 2026 20:03:40 +0100 Subject: [PATCH 2/7] Add unit test for santiseContentFor parameter --- spec/notification.js | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/spec/notification.js b/spec/notification.js index 50d45943..24439e0e 100644 --- a/spec/notification.js +++ b/spec/notification.js @@ -117,6 +117,31 @@ describe('notification api', () => { }); }); + it('should send an email with sanitise content parameter', () => { + + let email = 'dom@example.com', + templateId = '123', + options = { + personalisation: {code: '12345', name: 'John'}, + sanitiseContentFor: ['code'] + }, + data = { + template_id: templateId, + email_address: email, + personalisation: options.personalisation, + sanitise_content_for: options.sanitiseContentFor + }; + + notifyAuthNock + .post('/v2/notifications/email', data) + .reply(200, {hooray: 'bkbbk'}); + + return notifyClient.sendEmail(templateId, email, options) + .then((response) => { + expect(response.status).to.equal(200); + }); + }); + it('should send an email with document upload', () => { let email = 'dom@example.com', templateId = '123', From 960229e9d906910bd68654dc378606206da2d9ea Mon Sep 17 00:00:00 2001 From: Jani Kraner Date: Thu, 7 May 2026 20:04:01 +0100 Subject: [PATCH 3/7] Add type definition for santiseContentFor parameter --- types/client/notification.d.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/types/client/notification.d.ts b/types/client/notification.d.ts index 43acccc7..baa511cd 100644 --- a/types/client/notification.d.ts +++ b/types/client/notification.d.ts @@ -193,14 +193,15 @@ declare class NotifyClient { /** * @param {string} templateId * @param {string} emailAddress - * @param {{personalisation?: Object, reference?: string, emailReplyToId?: string, oneClickUnsubscribeURL?: string}} [options] - * @returns {Promise>} + * @param {{personalisation?: Object, reference?: string, emailReplyToId?: string, oneClickUnsubscribeURL?: string, sanitiseContentFor?: string[]}} [options] + * @returns {Promise>, uri: string, template: TemplateRef}>>} */ sendEmail(templateId: string, emailAddress: string, options?: { personalisation?: any; reference?: string; emailReplyToId?: string; oneClickUnsubscribeURL?: string; + sanitiseContentFor?: string[]; }): Promise>; uri: string; template: TemplateRef; }>>; From 88c1ace02cfa87277d1c56488480064e01f33f9e Mon Sep 17 00:00:00 2001 From: Jani Kraner Date: Thu, 7 May 2026 14:02:28 +0100 Subject: [PATCH 4/7] Update integration definitions JSON spec Adds a defitnion for `sanitiseContentFor` structure and types are. --- spec/integration/schemas/v2/definitions.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/spec/integration/schemas/v2/definitions.json b/spec/integration/schemas/v2/definitions.json index 63d6686e..e92d4f93 100644 --- a/spec/integration/schemas/v2/definitions.json +++ b/spec/integration/schemas/v2/definitions.json @@ -26,7 +26,8 @@ "body": {"type": "string"}, "from_email": {"type": "string", "format": "email_address"}, "subject": {"type": "string"}, - "one_click_unsubscribe_url": {"type": ["string", "null"], "format": "uri"} + "one_click_unsubscribe_url": {"type": ["string", "null"], "format": "uri"}, + "sanitise_content_for": {"type": "array", "items": {"type": "string"}} }, "required": ["body", "from_email", "subject"] }, From fc6d235a478479a7007a840f0df03898013922d3 Mon Sep 17 00:00:00 2001 From: Jani Kraner Date: Tue, 5 May 2026 14:12:24 +0100 Subject: [PATCH 5/7] Update integration test for sanitiseContentFor parameter --- spec/integration/test.js | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/spec/integration/test.js b/spec/integration/test.js index 77dfcbd4..fab656cd 100644 --- a/spec/integration/test.js +++ b/spec/integration/test.js @@ -110,6 +110,31 @@ describer('notification api with a live service', function () { }) }); + it('email notification with sanitise_content_for', () => { + const unsafeInput = 'User, [click here](https://evil.link)'; + const expectedSanitised = 'User, \\[click here\\]\\(\\)'; + + let modified_personalisation = { ...personalisation }; + modified_personalisation['name'] = unsafeInput; + + var postEmailNotificationResponseJson = require('./schemas/v2/POST_notification_email_response.json'), + options = { + personalisation: modified_personalisation, + reference: clientRef, + sanitiseContentFor: ['name'] + }; + + return notifyClient.sendEmail(emailTemplateId, email, options).then((response) => { + response.status.should.equal(201); + expect(response.data).to.be.jsonSchema(postEmailNotificationResponseJson); + response.data.content.body.should.contain(expectedSanitised); + const field = response.data.sanitised_content.name; + field.sanitised.should.equal(expectedSanitised); + field.unsanitised.should.equal(unsafeInput); + response.data.should.have.property('sanitised_content'); + }); + }); + it('send email notification with document upload', () => { var postEmailNotificationResponseJson = require('./schemas/v2/POST_notification_email_response.json'), options = {personalisation: { name: 'Foo', documents: From ac2fb290e96641909adb18038befcb2c3420783b Mon Sep 17 00:00:00 2001 From: Jani Kraner Date: Thu, 7 May 2026 14:02:38 +0100 Subject: [PATCH 6/7] Update POST_notification_email_response JSON spec Adds structure and type definitions for `sanitised_content` object the API returns. `sanitised_content` is always returned, being an empty object if `sanitiseContentFor` is ommited, so it's listed in the `required`. This matches the python implementation - https://github.com/alphagov/notifications-python-client/blob/1902b7d9a062ab101a6e2a1554e5d1c35f7f2179/integration_test/schemas/v2/notification_schemas.py#L162 --- .../schemas/v2/POST_notification_email_response.json | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/spec/integration/schemas/v2/POST_notification_email_response.json b/spec/integration/schemas/v2/POST_notification_email_response.json index ad93a2ee..a3084cff 100644 --- a/spec/integration/schemas/v2/POST_notification_email_response.json +++ b/spec/integration/schemas/v2/POST_notification_email_response.json @@ -11,8 +11,15 @@ "scheduled_for": {"oneOf":[ {"$ref": "definitions.json#/datetime"}, {"type": "null"} - ]} + ]}, + "sanitised_content": { + "type": "object", + "additionalProperties": { + "type": "object", + "additionalProperties": { "type": "string" } + } + } }, "additionalProperties": true, - "required": ["id", "content", "uri", "template"] + "required": ["id", "content", "uri", "template", "sanitised_content"] } From 05d13fc01e7e1152dffc8afa5810fd19daff5b99 Mon Sep 17 00:00:00 2001 From: Jani Kraner Date: Mon, 11 May 2026 11:05:48 +0100 Subject: [PATCH 7/7] Update CHANGELOG Add unreleased entry for `sanitiseContentFor` optional parameter --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index adc1f795..c7edf6d0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +## Unreleased + +* The `sendEmail` function can now be passed `santiseContentFor` as an optional argument. + ## 8.3.2 - 2026-03-24 * Include type files in published package (previous attempt did not work)