Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
fd5a645
First pass implementation
mjewildnhs Feb 10, 2026
84ae5d5
Update lambda placeholder IAM policy name
mjewildnhs Feb 10, 2026
8f8f6af
Use shared module for s3 config
mjewildnhs Feb 10, 2026
924357d
Fix sonar scanner props
mjewildnhs Feb 11, 2026
11c6c81
Placeholder tests to test coverage
mjewildnhs Feb 12, 2026
4e27d7a
Fix lambda jest config
mjewildnhs Feb 12, 2026
a1fa079
Better name for config bucket
mjewildnhs Feb 12, 2026
ff16952
Update with data-model changes
mjewildnhs Feb 12, 2026
ded5a05
Update event names/terminology and remove nhsnumber, routingplan fiel…
mjewildnhs Feb 13, 2026
7f340cc
Refactor model type structure
mjewildnhs Feb 13, 2026
152ef89
Remove unncessary sonar exclusions
mjewildnhs Feb 13, 2026
7ca4971
update teamResponsible enum
mjewildnhs Feb 13, 2026
40f54e9
Update event schema based on guidance from meeting
mjewildnhs Feb 17, 2026
ac0273c
fixup! Update event schema based on guidance from meeting
mjewildnhs Feb 17, 2026
71ee52f
fixup! Update event schema based on guidance from meeting
mjewildnhs Feb 18, 2026
05f25eb
Apply suggestion from @mjewildnhs - model import consistency
mjewildnhs Feb 19, 2026
d37a8a2
added explicit pull-requeest read permission for pipeline
cgitim Feb 19, 2026
505aa40
CCM-14203 - Fix for transitive dependency CVE
rhyscoxnhs Feb 19, 2026
63925b5
Update supplier status enum values to match core rather than Open API…
mjewildnhs Feb 19, 2026
c551b6d
Update message status enum values to match core rather than Open API …
mjewildnhs Feb 19, 2026
2f8761b
Update channel status enum values to match core rather than Open API …
mjewildnhs Feb 19, 2026
a683053
Revert "added explicit pull-requeest read permission for pipeline"
mjewildnhs Feb 24, 2026
e3df8d4
Force minimatch to patched versions - remove react lint
mjewildnhs Feb 24, 2026
1f6e4f3
Revert "CCM-14203 - Fix for transitive dependency CVE"
mjewildnhs Feb 24, 2026
b6e377d
Update trivyignore to indicate minimatch CVE now false positive for 3…
mjewildnhs Feb 24, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions .trivyignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# Add CVE IDs to ignore specific vulnerabilities.
# Keep justification inline after the CVE for auditability.
# Syntax: one entry per line, comments allowed.

# Examples:
# CVE-2025-0001 # Unexploitable in AWS Lambda base per vendor advisory
# CVE-2024-12345 # False positive: not present in runtime layer
# CVE-2024-12345 # https://avd.aquasec.com/nvd/cve-2024-12345 - package-name - < 2.0.1 - justification

###########################
# Package Vulnerabilities #
###########################

# All CVEs below are tracked for remediation under the following Jira ticket:
# https://nhsd-jira.digital.nhs.uk/browse/CCM-14687
# EXAMPLE:
# CVE-2024-12345 # https://avd.aquasec.com/nvd/cve-2024-12345 - package-name - < 2.0.1 - justification
CVE-2026-26996 # https://avd.aquasec.com/nvd/cve-2026-26996 - minimatch - <10.2.1 - False positive as the fix has been backported to 3.1.3 (see https://github.com/isaacs/minimatch/security/advisories/GHSA-3ppc-4f35-3m26). This is a dev dependency used in the build process, not present in the runtime layer, and therefore not exploitable in production. We will update to a non-vulnerable versions for our transitive dependencies when available.
9 changes: 0 additions & 9 deletions eslint.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import prettierRecommended from "eslint-plugin-prettier/recommended";
import { importX } from "eslint-plugin-import-x";
import * as eslintImportResolverTypescript from "eslint-import-resolver-typescript";
import noRelativeImportPaths from "eslint-plugin-no-relative-import-paths";
import react from "eslint-plugin-react";
import security from "eslint-plugin-security";
import sonarjs from "eslint-plugin-sonarjs";
import json from "eslint-plugin-json";
Expand Down Expand Up @@ -127,14 +126,6 @@ export default defineConfig([
},
},

// react
react.configs.flat.recommended,
airbnbConfigs.react.recommended,
airbnbConfigs.react.typescript,
airbnbPlugins.react,
airbnbPlugins.reactHooks,
airbnbPlugins.reactA11y,

// jest
jest.configs["flat/recommended"],

Expand Down
1 change: 1 addition & 0 deletions infrastructure/terraform/components/callbacks/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@

| Name | Source | Version |
|------|--------|---------|
| <a name="module_client_config_bucket"></a> [client\_config\_bucket](#module\_client\_config\_bucket) | https://github.com/NHSDigital/nhs-notify-shared-modules/releases/download/v2.0.28/terraform-s3bucket.zip | n/a |
| <a name="module_client_destination"></a> [client\_destination](#module\_client\_destination) | ../../modules/client-destination | n/a |
| <a name="module_client_transform_filter_lambda"></a> [client\_transform\_filter\_lambda](#module\_client\_transform\_filter\_lambda) | git::https://github.com/NHSDigital/nhs-notify-shared-modules.git//infrastructure/modules/lambda | v2.0.29 |
| <a name="module_kms"></a> [kms](#module\_kms) | https://github.com/NHSDigital/nhs-notify-shared-modules/releases/download/v2.0.29/terraform-kms.zip | n/a |
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ module "client_transform_filter_lambda" {
kms_key_arn = module.kms.key_arn ## Requires shared kms module

iam_policy_document = {
body = data.aws_iam_policy_document.example_lambda.json
body = data.aws_iam_policy_document.client_transform_filter_lambda.json
}

function_s3_bucket = local.acct.s3_buckets["lambda_function_artefacts"]["id"]
Expand All @@ -38,7 +38,7 @@ module "client_transform_filter_lambda" {
}
}

data "aws_iam_policy_document" "example_lambda" {
data "aws_iam_policy_document" "client_transform_filter_lambda" {
statement {
sid = "KMSPermissions"
effect = "Allow"
Expand All @@ -52,4 +52,17 @@ data "aws_iam_policy_document" "example_lambda" {
module.kms.key_arn, ## Requires shared kms module
]
}

statement {
sid = "S3ClientConfigReadAccess"
effect = "Allow"

actions = [
"s3:GetObject",
]

resources = [
"${module.client_config_bucket.arn}/*",
]
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
##
# S3 Bucket for Client Subscription Configuration
#
# Storage location for client subscription configurations loaded by Transform & Filter Lambda.
# Files are named {clientId}.json and contain ClientSubscriptionConfiguration arrays.
##

module "client_config_bucket" {
source = "https://github.com/NHSDigital/nhs-notify-shared-modules/releases/download/v2.0.28/terraform-s3bucket.zip"

name = "subscription-config"

aws_account_id = var.aws_account_id
component = var.component
environment = var.environment
project = var.project
region = var.region

default_tags = merge(
local.default_tags,
{
Description = "Client subscription configuration storage"
}
)

kms_key_arn = module.kms.key_arn
force_destroy = false
versioning = true
object_ownership = "BucketOwnerPreferred"
bucket_key_enabled = true

policy_documents = [
data.aws_iam_policy_document.client_config_bucket.json
]
}

##
# S3 Bucket Policy
#
# Allows Transform & Filter Lambda to read configuration files
##

data "aws_iam_policy_document" "client_config_bucket" {
statement {
sid = "AllowLambdaReadAccess"
effect = "Allow"

principals {
type = "AWS"
identifiers = [module.client_transform_filter_lambda.iam_role_arn]
}

actions = [
"s3:GetObject",
]

resources = [
"${module.client_config_bucket.arn}/*",
]
}

statement {
sid = "DenyInsecureTransport"
effect = "Deny"

principals {
type = "*"
identifiers = ["*"]
}

actions = [
"s3:*",
]

resources = [
module.client_config_bucket.arn,
"${module.client_config_bucket.arn}/*"
]

condition {
test = "Bool"
variable = "aws:SecureTransport"
values = ["false"]
}
}
}
3 changes: 3 additions & 0 deletions lambdas/client-transform-filter-lambda/jest.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,9 @@ const utilsJestConfig = {
...(baseJestConfig.coveragePathIgnorePatterns ?? []),
"zod-validators.ts",
],

// Mirror tsconfig's baseUrl: "src" - automatically resolves non-relative imports
modulePaths: ["<rootDir>/src"],
};

export default utilsJestConfig;
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { EventTypes } from "models/status-transition-event";

// coverage purposes
describe("EventTypes", () => {
it("should match the expected event type values", () => {
expect(EventTypes).toEqual({
MESSAGE_STATUS_TRANSITIONED:
"uk.nhs.notify.client-callbacks.message.status.transitioned.v1",
CHANNEL_STATUS_TRANSITIONED:
"uk.nhs.notify.client-callbacks.channel.status.transitioned.v1",
});
});
});
Comment on lines +4 to +13
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this a useful test?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No hence the "// coverage purposes" but sonar will fail coverage otherwise and i don't want to start adding exclusions

Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
/**
* Channel-level status transition event data.
*/
import type { Channel } from "models/channel-types";
import type { ChannelStatus, SupplierStatus } from "models/status-types";

export interface ChannelStatusData {
messageId: string;
messageReference: string;
channel: Channel;
channelStatus: ChannelStatus;
channelStatusDescription?: string;
channelFailureReasonCode?: string;
supplierStatus: SupplierStatus;
cascadeType: "primary" | "secondary";
cascadeOrder: number;
timestamp: string;
retryCount: number;

clientId: string;
previousChannelStatus?: ChannelStatus;
previousSupplierStatus?: SupplierStatus;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
/**
* Communication channel types
*/
export type Channel = "NHSAPP" | "EMAIL" | "SMS" | "LETTER";
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
/**
* Message/Channel Status Callback payload delivered to client webhooks.
*/

import type { RoutingPlan } from "models/routing-plan";
import type { Channel } from "models/channel-types";
import type {
ChannelStatus,
MessageStatus,
SupplierStatus,
} from "models/status-types";

export type ClientChannel = Lowercase<Channel>;
export type ClientMessageStatus = Lowercase<MessageStatus>;
export type ClientChannelStatus = Lowercase<ChannelStatus>;
export type ClientSupplierStatus = SupplierStatus; // SupplierStatus values are already lowercase

export interface ClientCallbackPayload {
data: CallbackItem[];
}

export interface CallbackItem {
type: "MessageStatus" | "ChannelStatus";
attributes: MessageStatusAttributes | ChannelStatusAttributes;
links: {
message: string;
};
meta: {
idempotencyKey: string;
};
}

export interface MessageStatusAttributes {
messageId: string;
messageReference: string;
messageStatus: ClientMessageStatus;
messageStatusDescription?: string;
messageFailureReasonCode?: string;
channels: {
type: ClientChannel;
channelStatus: ClientChannelStatus;
}[];
timestamp: string;
routingPlan: RoutingPlan;
}

export interface ChannelStatusAttributes {
messageId: string;
messageReference: string;
cascadeType: "primary" | "secondary";
cascadeOrder: number;
channel: ClientChannel;
channelStatus: ClientChannelStatus;
channelStatusDescription?: string;
channelFailureReasonCode?: string;
supplierStatus: ClientSupplierStatus;
timestamp: string;
retryCount: number;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
/**
* Client callback subscription configuration.
* Array of subscription rules (one per event type/channel combination).
*/

export type ClientSubscriptionConfiguration = (
| MessageStatusSubscriptionConfiguration
| ChannelStatusSubscriptionConfiguration
)[];

interface SubscriptionConfigurationBase {
Name: string;
ClientId: string;
Description: string;
EventSource: string;
EventDetail: string;
Targets: {
Type: "API";
TargetId: string;
Name: string;
InputTransformer: {
InputPaths: string;
InputHeaders: {
"x-hmac-sha256-signature": string;
};
};
InvocationEndpoint: string;
InvocationMethod: "POST";
InvocationRateLimit: number;
APIKey: {
HeaderName: string;
HeaderValue: string;
};
}[];
}

export interface MessageStatusSubscriptionConfiguration
extends SubscriptionConfigurationBase {
SubscriptionType: "MessageStatus";
Statuses: string[];
}

export interface ChannelStatusSubscriptionConfiguration
extends SubscriptionConfigurationBase {
SubscriptionType: "ChannelStatus";
ChannelType: string;
ChannelStatuses: string[];
SupplierStatuses: string[];
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
/**
* Message-level status transition event data.
*/
import type { RoutingPlan } from "models/routing-plan";
import type { Channel } from "models/channel-types";
import type { MessageStatus } from "models/status-types";

export interface MessageStatusData {
messageId: string;
messageReference: string;
messageStatus: MessageStatus;
messageStatusDescription?: string;
messageFailureReasonCode?: string;
channels: {
type: Channel;
channelStatus: string;
}[];
timestamp: string;
routingPlan: RoutingPlan;

clientId: string;
previousMessageStatus?: MessageStatus;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
export interface RoutingPlan {
id: string;
name: string;
version: string;
createdDate: string;
}
Loading
Loading