Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
78 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
55efe4e
WIP - US1 tasks
mjewildnhs Feb 17, 2026
294de36
WIP - US1 tasks - mock webhook
mjewildnhs Feb 24, 2026
3a71da3
WIP - US1 tasks - mock webhook - infrastructure
mjewildnhs Feb 17, 2026
6d193da
DROP - temporarily lower coverage
mjewildnhs Feb 18, 2026
f1075ed
Event schema changes
mjewildnhs Feb 18, 2026
bae6dcd
DROP - Update fast-xml-parser
mjewildnhs Feb 18, 2026
f22a849
Sonar fixes
mjewildnhs Feb 18, 2026
4234f19
fixup! DROP - temporarily lower coverage
mjewildnhs Feb 18, 2026
174aaf0
Update agent file to run correct test command
mjewildnhs Feb 18, 2026
7806385
fixup! Event schema changes
mjewildnhs Feb 18, 2026
9a7eca8
fixup! Sonar fixes
mjewildnhs Feb 18, 2026
66a64c7
Exclude jest config from coverage
mjewildnhs Feb 18, 2026
bf93ca2
Metric test coverage
mjewildnhs Feb 18, 2026
466ac35
Logger and error handler coverage
mjewildnhs Feb 18, 2026
93e0f1f
fixup! Logger and error handler coverage
mjewildnhs Feb 18, 2026
13c44af
fixup! WIP - US1 tasks
mjewildnhs Feb 18, 2026
1753b77
Handle SQS event correctly in lambda
mjewildnhs Feb 18, 2026
9f5bc7a
Permit lambda to put cloudwatch metrics
mjewildnhs Feb 18, 2026
fe0fffa
fixup! WIP - US1 tasks - mock webhook - infrastructure
mjewildnhs Feb 19, 2026
984e341
DROP - temp test client
mjewildnhs Feb 19, 2026
57b71f5
explicitly set pull-request read permission
cgitim Feb 19, 2026
edd0f32
DROP? - trivyignore for minimatch
mjewildnhs Feb 19, 2026
91a8aa3
WIP - type fixes
mjewildnhs Feb 19, 2026
823c33d
Tidy up transform lambda code
mjewildnhs Feb 19, 2026
16bdfe2
AGENTS.md add section on comment policy
mjewildnhs Feb 19, 2026
6f16332
Simplify validation using zod
mjewildnhs Feb 19, 2026
39b1cd1
Remove superflous comments, simplify code
mjewildnhs Feb 20, 2026
cf0b2c6
WIP - re-write metrics
mjewildnhs Feb 20, 2026
aec4edb
Re-write metrics to use aws-embedded-metrics
mjewildnhs Feb 20, 2026
b988f60
WIP - concurrent event processing
mjewildnhs Feb 20, 2026
47eac8c
More cleanup
mjewildnhs Feb 20, 2026
f03dd2f
Refactor error handling and callback logging
mjewildnhs Feb 23, 2026
d4583fc
fixup! Refactor error handling and callback logging
mjewildnhs Feb 23, 2026
f964104
fixup! WIP - US1 tasks - mock webhook
mjewildnhs Feb 23, 2026
304d473
Transform lambda root handler test coverage
mjewildnhs Feb 23, 2026
c0e7653
Introduce base test config
mjewildnhs Feb 23, 2026
011b247
Sonar and jest vscode settings
mjewildnhs Feb 23, 2026
1eb1594
Remove extraneous int test comments
mjewildnhs Feb 23, 2026
548abcb
Refactor some of the int test to use await util
mjewildnhs Feb 23, 2026
30988d1
Tidy up unncessary arg in unit test script and remove unncessary tsco…
mjewildnhs Feb 23, 2026
b1fa7d8
Scripts for running int test
mjewildnhs Feb 23, 2026
e9794b0
Remove dependencies not needed now int tests own workspace
mjewildnhs Feb 23, 2026
c835710
Refactor lambda handler code out
mjewildnhs Feb 24, 2026
9c4a844
Refactor lambda to tidy up observablity
mjewildnhs Feb 24, 2026
79919f8
Revert "explicitly set pull-request read permission"
mjewildnhs Feb 24, 2026
fd433af
Swap todo for comment for snar
mjewildnhs Feb 24, 2026
6287115
Turn off client creation
mjewildnhs Feb 24, 2026
bed3834
Revert "Turn off client creation"
mjewildnhs Feb 25, 2026
432a80c
Update zod/pino and update validation test assertions
mjewildnhs Feb 25, 2026
276e475
DI for handler and more test coverage
mjewildnhs Feb 25, 2026
1af1b86
Use mock pino in mock lambda test
mjewildnhs Feb 25, 2026
a011e6d
Delete test which is no longer required as coverage provided elsewhere
mjewildnhs Feb 25, 2026
72132ef
Fix cloudwatch events - single dimension with env, other fields as pr…
mjewildnhs Feb 26, 2026
aff9961
Fix event pipe template to align with lambda output
mjewildnhs Feb 26, 2026
372f8fe
Remove dataschemaversion from event pipe input template and bus rule
mjewildnhs Feb 26, 2026
92f6f4e
Var for pipe log level
mjewildnhs Feb 26, 2026
f27a558
REVIEW: Event bus logging - theres a new native approach we may want …
mjewildnhs Feb 26, 2026
fd457d5
Revert "REVIEW: Event bus logging - theres a new native approach we m…
mjewildnhs Feb 26, 2026
96d857c
Remove event parameters which don't work in batch scenario
mjewildnhs Feb 27, 2026
a5d0cfe
Fix correlation ID on delivery initiated event/logging
mjewildnhs Feb 27, 2026
b50026e
KEEP? - attempt API destination permission fix
mjewildnhs Feb 27, 2026
3048fe4
DROP - Enable INFO logging on event pipe
mjewildnhs Feb 27, 2026
30730ac
Revert "KEEP? - attempt API destination permission fix"
mjewildnhs Feb 27, 2026
781dc47
Permissions on lambda to allow it to be invoked without IAM
mjewildnhs Feb 27, 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 - 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.
6 changes: 5 additions & 1 deletion .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
{
"autoOpenWorkspace.enableAutoOpenIfSingleWorkspace": true,
"files.exclude": {
"**/.DS_Store": true,
"**/.git": true,
Expand All @@ -11,5 +10,10 @@
".devcontainer": true,
".github": false,
".vscode": false
},
"jest.jestCommandLine": "npm run test:unit --workspaces --",
"sonarlint.connectedMode.project": {
"connectionId": "nhsdigital",
"projectKey": "NHSDigital_nhs-notify-client-callbacks"
}
}
10 changes: 9 additions & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ When proposing a change, agents should:

to catch formatting and basic lint issues. Domain specific checks will be defined in appropriate nested AGENTS.md files.

- Suggest at least one extra validation step (for example `npm test` in a lambda, or triggering a specific workflow).
- Suggest at least one extra validation step (for example `npm test:unit` in a lambda, or triggering a specific workflow).
- Any required follow up activites which fall outside of the current task's scope should be clearly marked with a 'TODO: CCM-12345' comment. The human user should be prompted to create and provide a JIRA ticket ID to be added to the comment.

## Security & Safety
Expand All @@ -93,3 +93,11 @@ When proposing a change, agents should:
## Escalation / Blockers

If you are blocked by an unavailable secret, unclear architectural constraint, missing upstream module, or failing tooling you cannot safely fix, stop and ask a single clear clarifying question rather than guessing.

## Comment Policy

- No JSDoc unless it's a public API with non-obvious behavior
- No inline comments that just describe what the next line does
- Only comment when explaining WHY, not WHAT
- Prefer better naming over comments
- Trust developers can read TypeScript
7 changes: 7 additions & 0 deletions eslint.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -229,6 +229,13 @@ export default defineConfig([
"no-relative-import-paths/no-relative-import-paths": 2,
},
},
{
files: ["**/jest.config.ts"],
rules: {
"no-relative-import-paths/no-relative-import-paths": 0,
"import-x/no-relative-packages": 0,
},
},
{
files: ["scripts/**"],
rules: {
Expand Down
9 changes: 8 additions & 1 deletion infrastructure/terraform/components/callbacks/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
| <a name="input_clients"></a> [clients](#input\_clients) | n/a | <pre>list(object({<br/> connection_name = string<br/> destination_name = string<br/> invocation_endpoint = string<br/> invocation_rate_limit_per_second = optional(number, 10)<br/> http_method = optional(string, "POST")<br/> header_name = optional(string, "x-api-key")<br/> header_value = string<br/> client_detail = list(string)<br/> }))</pre> | `[]` | no |
| <a name="input_component"></a> [component](#input\_component) | The variable encapsulating the name of this component | `string` | `"callbacks"` | no |
| <a name="input_default_tags"></a> [default\_tags](#input\_default\_tags) | A map of default tags to apply to all taggable resources within the component | `map(string)` | `{}` | no |
| <a name="input_deploy_mock_webhook"></a> [deploy\_mock\_webhook](#input\_deploy\_mock\_webhook) | Flag to deploy mock webhook lambda for integration testing (test/dev environments only) | `bool` | `true` | no |
| <a name="input_environment"></a> [environment](#input\_environment) | The name of the tfscaffold environment | `string` | n/a | yes |
| <a name="input_force_lambda_code_deploy"></a> [force\_lambda\_code\_deploy](#input\_force\_lambda\_code\_deploy) | If the lambda package in s3 has the same commit id tag as the terraform build branch, the lambda will not update automatically. Set to True if making changes to Lambda code from on the same commit for example during development | `bool` | `false` | no |
| <a name="input_group"></a> [group](#input\_group) | The group variables are being inherited from (often synonmous with account short-name) | `string` | n/a | yes |
Expand All @@ -24,6 +25,7 @@
| <a name="input_log_retention_in_days"></a> [log\_retention\_in\_days](#input\_log\_retention\_in\_days) | The retention period in days for the Cloudwatch Logs events to be retained, default of 0 is indefinite | `number` | `0` | no |
| <a name="input_parent_acct_environment"></a> [parent\_acct\_environment](#input\_parent\_acct\_environment) | Name of the environment responsible for the acct resources used, affects things like DNS zone. Useful for named dev environments | `string` | `"main"` | no |
| <a name="input_pipe_event_patterns"></a> [pipe\_event\_patterns](#input\_pipe\_event\_patterns) | value | `list(string)` | `[]` | no |
| <a name="input_pipe_log_level"></a> [pipe\_log\_level](#input\_pipe\_log\_level) | Log level for the EventBridge Pipe. | `string` | `"INFO"` | no |
| <a name="input_pipe_sqs_input_batch_size"></a> [pipe\_sqs\_input\_batch\_size](#input\_pipe\_sqs\_input\_batch\_size) | n/a | `number` | `1` | no |
| <a name="input_pipe_sqs_max_batch_window"></a> [pipe\_sqs\_max\_batch\_window](#input\_pipe\_sqs\_max\_batch\_window) | n/a | `number` | `2` | no |
| <a name="input_project"></a> [project](#input\_project) | The name of the tfscaffold project | `string` | n/a | yes |
Expand All @@ -32,13 +34,18 @@

| 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 |
| <a name="module_mock_webhook_lambda"></a> [mock\_webhook\_lambda](#module\_mock\_webhook\_lambda) | git::https://github.com/NHSDigital/nhs-notify-shared-modules.git//infrastructure/modules/lambda | v2.0.29 |
| <a name="module_sqs_inbound_event"></a> [sqs\_inbound\_event](#module\_sqs\_inbound\_event) | https://github.com/NHSDigital/nhs-notify-shared-modules/releases/download/v2.0.29/terraform-sqs.zip | n/a |
## Outputs

No outputs.
| Name | Description |
|------|-------------|
| <a name="output_mock_webhook_lambda_log_group_name"></a> [mock\_webhook\_lambda\_log\_group\_name](#output\_mock\_webhook\_lambda\_log\_group\_name) | CloudWatch log group name for mock webhook lambda (for integration test queries) |
| <a name="output_mock_webhook_url"></a> [mock\_webhook\_url](#output\_mock\_webhook\_url) | URL endpoint for mock webhook (for TEST\_WEBHOOK\_URL environment variable) |
<!-- vale on -->
<!-- markdownlint-enable -->
<!-- END_TF_DOCS -->
21 changes: 21 additions & 0 deletions infrastructure/terraform/components/callbacks/locals.tf

Choose a reason for hiding this comment

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

The comments added to this file seem fairly redundant, maybe we can strip these out?

Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,29 @@ locals {
root_domain_name = "${var.environment}.${local.acct.route53_zone_names["client-callbacks"]}" # e.g. [main|dev|abxy0].smsnudge.[dev|nonprod|prod].nhsnotify.national.nhs.uk
root_domain_id = local.acct.route53_zone_ids["client-callbacks"]

# Clients from variable
clients_by_name = {
for client in var.clients :
client.connection_name => client
}

# Automatic test client when mock webhook is deployed
test_client = var.deploy_mock_webhook ? {
"test-client" = {
connection_name = "test-client"
destination_name = "test-destination"
invocation_endpoint = aws_lambda_function_url.mock_webhook[0].function_url
invocation_rate_limit_per_second = 10
http_method = "POST"
header_name = "x-api-key"
header_value = "test-api-key-placeholder"
client_detail = [
"uk.nhs.notify.client-callbacks.message.status.transitioned.v1",
"uk.nhs.notify.client-callbacks.channel.status.transitioned.v1"
]
}
} : {}

# Merge configured clients with test client
all_clients = merge(local.clients_by_name, local.test_client)
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
module "client_destination" {
source = "../../modules/client-destination"
for_each = local.clients_by_name
for_each = local.all_clients

project = var.project
aws_account_id = var.aws_account_id
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
module "mock_webhook_lambda" {
count = var.deploy_mock_webhook ? 1 : 0
source = "git::https://github.com/NHSDigital/nhs-notify-shared-modules.git//infrastructure/modules/lambda?ref=v2.0.29"

function_name = "mock-webhook"
description = "Mock webhook endpoint for integration testing - logs received callbacks to CloudWatch"

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

log_retention_in_days = var.log_retention_in_days
kms_key_arn = module.kms.key_arn

iam_policy_document = {
body = data.aws_iam_policy_document.mock_webhook_lambda[0].json
}

function_s3_bucket = local.acct.s3_buckets["lambda_function_artefacts"]["id"]
function_code_base_path = local.aws_lambda_functions_dir_path
function_code_dir = "mock-webhook-lambda/dist"
function_include_common = true
handler_function_name = "handler"
runtime = "nodejs22.x"
memory = 256
timeout = 10
log_level = var.log_level

force_lambda_code_deploy = var.force_lambda_code_deploy
enable_lambda_insights = false

log_destination_arn = local.log_destination_arn
log_subscription_role_arn = local.acct.log_subscription_role_arn

lambda_env_vars = {
LOG_LEVEL = var.log_level
}
}

data "aws_iam_policy_document" "mock_webhook_lambda" {
count = var.deploy_mock_webhook ? 1 : 0

statement {
sid = "KMSPermissions"
effect = "Allow"

actions = [
"kms:Decrypt",
"kms:GenerateDataKey",
]

resources = [
module.kms.key_arn,
]
}

# Mock webhook only needs CloudWatch Logs permissions (already granted by shared lambda module)
# No additional permissions required beyond base Lambda execution role
}

# Lambda Function URL for mock webhook (test/dev only)
resource "aws_lambda_function_url" "mock_webhook" {
count = var.deploy_mock_webhook ? 1 : 0
function_name = module.mock_webhook_lambda[0].function_name
authorization_type = "NONE" # Public endpoint for testing

cors {
allow_origins = ["*"]
allow_methods = ["POST"]
allow_headers = ["*"]
max_age = 86400
}
}

resource "aws_lambda_permission" "mock_webhook_function_url" {
count = var.deploy_mock_webhook ? 1 : 0
statement_id = "FunctionURLAllowPublicAccess"
action = "lambda:InvokeFunctionUrl"
function_name = module.mock_webhook_lambda[0].function_name
principal = "*"
function_url_auth_type = "NONE"
}
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 @@ -35,10 +35,12 @@ module "client_transform_filter_lambda" {
log_subscription_role_arn = local.acct.log_subscription_role_arn

lambda_env_vars = {
ENVIRONMENT = var.environment
METRICS_NAMESPACE = "nhs-notify-client-callbacks-metrics"
}
}

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 +54,30 @@ 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}/*",
]
}

statement {
sid = "CloudWatchMetrics"
effect = "Allow"

actions = [
"cloudwatch:PutMetricData",
]

resources = [
"*",
]
}
}
14 changes: 14 additions & 0 deletions infrastructure/terraform/components/callbacks/outputs.tf
Original file line number Diff line number Diff line change
@@ -1 +1,15 @@
# Define the outputs for the component. The outputs may well be referenced by other component in the same or different environments using terraform_remote_state data sources...

##
# Mock Webhook Lambda Outputs (test/dev environments only)
##

output "mock_webhook_lambda_log_group_name" {
description = "CloudWatch log group name for mock webhook lambda (for integration test queries)"
value = var.deploy_mock_webhook ? module.mock_webhook_lambda[0].cloudwatch_log_group_name : null
}

output "mock_webhook_url" {
description = "URL endpoint for mock webhook (for TEST_WEBHOOK_URL environment variable)"
value = var.deploy_mock_webhook ? aws_lambda_function_url.mock_webhook[0].function_url : null
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ resource "aws_pipes_pipe" "main" {
enrichment = module.client_transform_filter_lambda.function_arn
kms_key_identifier = module.kms.key_arn
log_configuration {
level = "ERROR"
level = var.pipe_log_level
cloudwatch_logs_log_destination {
log_group_arn = aws_cloudwatch_log_group.main_pipe.arn
}
Expand All @@ -25,8 +25,8 @@ resource "aws_pipes_pipe" "main" {

input_template = <<EOF
{
"dataschemaversion": <$.body.dataschemaversion>,
"type": <$.body.type>
"type": <$.type>,
"transformedPayload": <$.transformedPayload>
}
EOF
}
Expand Down
Loading
Loading