Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 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
391c70d
Update event schema based on guidance from meeting
mjewildnhs Feb 17, 2026
69b09b7
Apply suggestion from @mjewildnhs - model import consistency
mjewildnhs Feb 19, 2026
b673134
Update supplier status enum values to match core rather than Open API…
mjewildnhs Feb 19, 2026
9cc6008
Update message status enum values to match core rather than Open API …
mjewildnhs Feb 19, 2026
caf720e
Update channel status enum values to match core rather than Open API …
mjewildnhs Feb 19, 2026
8dc282c
Force minimatch to patched versions - remove react lint
mjewildnhs Feb 24, 2026
3ccba7c
Update trivyignore to indicate minimatch CVE now false positive for 3…
mjewildnhs Feb 24, 2026
e3dadaa
Lint fix
mjewildnhs Feb 25, 2026
8aa0560
Remove unnecessary terraform comments
mjewildnhs Feb 25, 2026
accd6ea
Resolve minmatch CVE and remove unncessary lint config which prevente…
mjewildnhs Feb 25, 2026
3d6619a
Remove trivyignore
mjewildnhs Feb 25, 2026
6e38075
Event schema updates
mjewildnhs Feb 26, 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
20 changes: 0 additions & 20 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 All @@ -20,17 +19,6 @@ import {
} from "eslint-config-airbnb-extended";
import { rules as prettierConfigRules } from "eslint-config-prettier";

import { dirname } from "node:path";
import { fileURLToPath } from "node:url";
import { FlatCompat } from "@eslint/eslintrc";

const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);

const compat = new FlatCompat({
baseDirectory: __dirname,
});

export default defineConfig([
globalIgnores([
"**/*/coverage/*",
Expand Down Expand Up @@ -127,14 +115,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,73 @@
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
]
}

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"]
}
}
}
5 changes: 4 additions & 1 deletion lambdas/client-transform-filter-lambda/jest.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ export const baseJestConfig: Config = {
coverageDirectory: "./.reports/unit/coverage",

// Indicates which provider should be used to instrument code for coverage
coverageProvider: "babel",
coverageProvider: "v8",

coverageThreshold: {
global: {
Expand Down 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,11 @@
import { EventTypes } from "models/status-publish-event";

// coverage purposes
describe("EventTypes", () => {
it("should match the expected event type values", () => {
expect(EventTypes).toEqual({
MESSAGE_STATUS_PUBLISHED: "uk.nhs.notify.message.status.PUBLISHED.v1",
CHANNEL_STATUS_PUBLISHED: "uk.nhs.notify.channel.status.PUBLISHED.v1",
});
});
});
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,47 @@
/**
* 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;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import type { MessageStatusData } from "models/message-status-data";
import type { ChannelStatusData } from "models/channel-status-data";

export interface StatusPublishEvent<T = MessageStatusData | ChannelStatusData> {
specversion: string;
id: string;
source: string;
subject: string;
type: string;
time: string;
sequence?: string;
datacontenttype: string;
dataschema: string;
traceparent: string;

data: T;
}

export const EventTypes = {
MESSAGE_STATUS_PUBLISHED: "uk.nhs.notify.message.status.PUBLISHED.v1",
CHANNEL_STATUS_PUBLISHED: "uk.nhs.notify.channel.status.PUBLISHED.v1",
} as const;

export { type MessageStatusData } from "models/message-status-data";

export { type ChannelStatusData } from "models/channel-status-data";
Loading
Loading