diff --git a/.github/actions/install_dependencies/action.yml b/.github/actions/install_dependencies/action.yml new file mode 100644 index 000000000..108096bb8 --- /dev/null +++ b/.github/actions/install_dependencies/action.yml @@ -0,0 +1,35 @@ +name: "Install dependencies" +description: "Install dependencies including caching of npm packages" + +inputs: + npm-required: + description: "Set to true if npm dependencies are already installed" + required: false + default: "true" + GITHUB_TOKEN: + description: "GitHub token to access private npm packages" + required: true + +runs: + using: "composite" + steps: + - name: Setting up .npmrc + if: ${{ inputs.npm-required == 'true' }} + env: + NODE_AUTH_TOKEN: ${{ inputs.GITHUB_TOKEN }} + shell: bash + run: | + echo "//npm.pkg.github.com/:_authToken=${NODE_AUTH_TOKEN}" >> ~/.npmrc + echo "@nhsdigital:registry=https://npm.pkg.github.com" >> ~/.npmrc + + - name: Cache npm dependencies + if: ${{ inputs.npm-required == 'true' }} + uses: actions/cache@8b402f58fbc84540c8b491a91e594a4576fec3d7 + with: + path: ./node_modules + key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }} + + - name: make install + if: ${{ inputs.npm-required == 'true' }} + shell: bash + run: make install diff --git a/.github/workflows/cdk_package_code.yml b/.github/workflows/cdk_package_code.yml new file mode 100644 index 000000000..1ce76afb0 --- /dev/null +++ b/.github/workflows/cdk_package_code.yml @@ -0,0 +1,66 @@ +name: cdk package code + +on: + workflow_call: + inputs: + pinned_image: + type: string + required: true + +jobs: + package_code: + runs-on: ubuntu-22.04 + container: + image: ${{ inputs.pinned_image }} + options: --user 1001:1001 --group-add 128 + defaults: + run: + shell: bash + permissions: + id-token: write + contents: read + packages: read + steps: + - name: copy .tool-versions + run: | + cp /home/vscode/.tool-versions "$HOME/.tool-versions" + cp /home/vscode/.tool-versions ./.tool-versions + + - name: Checkout code + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd + with: + ref: ${{ github.head_ref || github.ref_name }} + + - name: Setting up .npmrc + env: + NODE_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + echo "//npm.pkg.github.com/:_authToken=${NODE_AUTH_TOKEN}" >> ~/.npmrc + echo "@nhsdigital:registry=https://npm.pkg.github.com" >> ~/.npmrc + + - name: make install and compile + run: | + make install + make compile + + - name: download the get secrets lambda layer + run: | + make download-get-secrets-layer + + - name: Tar files + run: | + tar -cf artifact.tar \ + .github \ + packages \ + node_modules \ + package.json \ + package-lock.json \ + tsconfig.defaults.json \ + tsconfig.build.json \ + Makefile + + - uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f + name: upload build artifact + with: + name: build_artifact + path: artifact.tar diff --git a/.github/workflows/cdk_release_code.yml b/.github/workflows/cdk_release_code.yml new file mode 100644 index 000000000..e1a6f70d6 --- /dev/null +++ b/.github/workflows/cdk_release_code.yml @@ -0,0 +1,348 @@ +name: cdk release code + +on: + workflow_call: + inputs: + IS_CDK_DEPLOY: + type: boolean + default: false + IS_PULL_REQUEST: + type: boolean + default: false + STACK_NAME: + required: true + type: string + TARGET_ENVIRONMENT: + required: true + type: string + APIGEE_ENVIRONMENT: + required: true + type: string + ENABLE_MUTUAL_TLS: + required: true + type: boolean + BUILD_ARTIFACT: + required: true + type: string + TRUSTSTORE_FILE: + required: true + type: string + VERSION_NUMBER: + required: true + type: string + COMMIT_ID: + required: true + type: string + CDK_APP_NAME: + type: string + default: PfPApiApp + LOG_LEVEL: + required: true + type: string + LOG_RETENTION_DAYS: + required: true + type: string + CREATE_INT_RELEASE_NOTES: + type: boolean + default: false + CREATE_INT_RC_RELEASE_NOTES: + type: boolean + default: false + CREATE_PROD_RELEASE_NOTES: + type: boolean + default: false + MARK_JIRA_RELEASED: + type: boolean + default: false + TOGGLE_GET_STATUS_UPDATES: + type: boolean + default: false + RUN_REGRESSION_TESTS: + type: boolean + default: true + ENABLE_ALERTS: + type: boolean + default: true + STATE_MACHINE_LOG_LEVEL: + type: string + REGRESSION_TEST_PRODUCT: + type: string + FORWARD_CSOC_LOGS: + required: true + type: boolean + TC007_NHS_NUMBERS: + required: false + type: string + TC008_NHS_NUMBERS: + required: false + type: string + TC009_NHS_NUMBERS: + required: false + type: string + DEPLOY_APIGEE: + type: boolean + default: true + MTLS_KEY: + type: string + required: true + ALLOW_NHS_NUMBER_OVERRIDE: + required: true + type: boolean + REGRESSION_TEST_NON_PROXYGEN: + type: boolean + UPDATE_GH_PAGES_RELEASE_TAG: + type: boolean + default: false + pinned_image: + type: string + required: true + secrets: + CLOUD_FORMATION_DEPLOY_ROLE: + required: true + # TODO: copied from tracker, used for delete stacks, needed? + #APIM_STATUS_API_KEY: + # required: true + TARGET_SPINE_SERVER: + required: true + TARGET_SERVICE_SEARCH_SERVER: + required: true + DEV_CLOUD_FORMATION_CHECK_VERSION_ROLE: + required: false + INT_CLOUD_FORMATION_CHECK_VERSION_ROLE: + required: false + PROD_CLOUD_FORMATION_CHECK_VERSION_ROLE: + required: false + DEV_CLOUD_FORMATION_EXECUTE_LAMBDA_ROLE: + required: false + REGRESSION_TESTS_PEM: + required: false + PROXYGEN_ROLE: + required: false + +jobs: + release_code_and_api: + runs-on: ubuntu-22.04 + environment: ${{ inputs.TARGET_ENVIRONMENT }} + container: + image: ${{ inputs.pinned_image }} + options: --user 1001:1001 --group-add 128 + defaults: + run: + shell: bash + name: deploy cdk app ${{ inputs.CDK_APP_NAME }} + permissions: + id-token: write + contents: write + env: + AWS_MAX_RETRY: 20 + + steps: + - name: copy .tool-versions + run: | + cp /home/vscode/.tool-versions "$HOME/.tool-versions" + + - name: Checkout local github actions + uses: actions/checkout@v5 + with: + fetch-depth: 0 + sparse-checkout: | + .github + + - name: create_int_rc_release_notes + uses: ./.github/actions/update_confluence_jira + if: ${{ inputs.CREATE_INT_RC_RELEASE_NOTES == true }} + with: + TARGET_ENVIRONMENT: int + RELEASE_TAG: ${{ inputs.VERSION_NUMBER }} + CONFLUENCE_PAGE_ID: "768067994" + CREATE_RC_RELEASE_NOTES: true + DEV_CLOUD_FORMATION_CHECK_VERSION_ROLE: ${{ secrets.DEV_CLOUD_FORMATION_CHECK_VERSION_ROLE }} + TARGET_CLOUD_FORMATION_CHECK_VERSION_ROLE: ${{ secrets.INT_CLOUD_FORMATION_CHECK_VERSION_ROLE }} + DEV_CLOUD_FORMATION_EXECUTE_LAMBDA_ROLE: ${{ secrets.DEV_CLOUD_FORMATION_EXECUTE_LAMBDA_ROLE }} + + - name: Configure AWS Credentials + uses: aws-actions/configure-aws-credentials@8df5847569e6427dd6c4fb1cf565c83acfa8afa7 + with: + aws-region: eu-west-2 + role-to-assume: ${{ secrets.CLOUD_FORMATION_DEPLOY_ROLE }} + role-session-name: aws-pfp-release-code + + - name: download build artifact + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c + with: + name: ${{ inputs.BUILD_ARTIFACT }} + path: . + + - name: extract build artifact + run: tar -xf artifact.tar + + + - name: delete old CDK stacks + if: ${{ inputs.IS_PULL_REQUEST != true }} + run: npm run delete-main-stacks --workspace packages/cdk + shell: bash + env: + AWS_ENVIRONMENT: "${{ inputs.TARGET_ENVIRONMENT }}" + APIGEE_ENVIRONMENT: "${{ inputs.APIGEE_ENVIRONMENT }}" + # APIM_STATUS_API_KEY: "${{ secrets.APIM_STATUS_API_KEY }}" + + - name: install dependencies + uses: ./.github/actions/install_dependencies + with: + npm-required: false + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - name: Show diff + env: + CDK_APP_NAME: ${{ inputs.CDK_APP_NAME }} + REQUIRE_APPROVAL: never + CDK_CONFIG_stackName: ${{ inputs.STACK_NAME }} + CDK_CONFIG_versionNumber: ${{ inputs.VERSION_NUMBER }} + CDK_CONFIG_commitId: ${{ inputs.COMMIT_ID }} + CDK_CONFIG_isPullRequest: ${{ inputs.IS_PULL_REQUEST }} + CDK_CONFIG_environment: ${{ inputs.TARGET_ENVIRONMENT }} + CDK_CONFIG_logRetentionInDays: ${{ inputs.LOG_RETENTION_DAYS }} + CDK_CONFIG_logLevel: ${{ inputs.LOG_LEVEL }} + CDK_CONFIG_targetSpineServer: ${{ secrets.TARGET_SPINE_SERVER }} + CDK_CONFIG_targetServiceSearchServer: ${{ secrets.TARGET_SERVICE_SEARCH_SERVER }} + CDK_CONFIG_toggleGetStatusUpdates: ${{ inputs.TOGGLE_GET_STATUS_UPDATES }} + CDK_CONFIG_allowNhsNumberOverride: ${{ inputs.ALLOW_NHS_NUMBER_OVERRIDE }} + CDK_CONFIG_tc007NhsNumberValue: ${{ inputs.TC007_NHS_NUMBERS || '9992387920' }} + CDK_CONFIG_tc008NhsNumberValue: ${{ inputs.TC008_NHS_NUMBERS || '9992387920' }} + CDK_CONFIG_tc009NhsNumberValue: ${{ inputs.TC009_NHS_NUMBERS || '9992387920' }} + CDK_CONFIG_trustStoreFile: ${{ inputs.TRUSTSTORE_FILE }} + CDK_CONFIG_forwardCsocLogs: ${{ inputs.FORWARD_CSOC_LOGS }} + run: npm run cdk-diff --workspace packages/cdk + + - name: Deploy code + env: + CDK_APP_NAME: ${{ inputs.CDK_APP_NAME }} + REQUIRE_APPROVAL: never + CDK_CONFIG_stackName: ${{ inputs.STACK_NAME }} + CDK_CONFIG_versionNumber: ${{ inputs.VERSION_NUMBER }} + CDK_CONFIG_commitId: ${{ inputs.COMMIT_ID }} + CDK_CONFIG_isPullRequest: ${{ inputs.IS_PULL_REQUEST }} + CDK_CONFIG_environment: ${{ inputs.TARGET_ENVIRONMENT }} + CDK_CONFIG_logRetentionInDays: ${{ inputs.LOG_RETENTION_DAYS }} + CDK_CONFIG_logLevel: ${{ inputs.LOG_LEVEL }} + CDK_CONFIG_targetSpineServer: ${{ secrets.TARGET_SPINE_SERVER }} + CDK_CONFIG_targetServiceSearchServer: ${{ secrets.TARGET_SERVICE_SEARCH_SERVER }} + CDK_CONFIG_toggleGetStatusUpdates: ${{ inputs.TOGGLE_GET_STATUS_UPDATES }} + CDK_CONFIG_allowNhsNumberOverride: ${{ inputs.ALLOW_NHS_NUMBER_OVERRIDE }} + CDK_CONFIG_tc007NhsNumberValue: ${{ inputs.TC007_NHS_NUMBERS || '9992387920' }} + CDK_CONFIG_tc008NhsNumberValue: ${{ inputs.TC008_NHS_NUMBERS || '9992387920' }} + CDK_CONFIG_tc009NhsNumberValue: ${{ inputs.TC009_NHS_NUMBERS || '9992387920' }} + CDK_CONFIG_trustStoreFile: ${{ inputs.TRUSTSTORE_FILE }} + CDK_CONFIG_forwardCsocLogs: ${{ inputs.FORWARD_CSOC_LOGS }} + run: npm run cdk-deploy --workspace packages/cdk + + - name: get mtls secrets + shell: bash + run: | + mkdir -p ~/.proxygen/tmp + client_private_key_arn=$(aws cloudformation list-exports --query "Exports[?Name=='account-resources:PfpClientKeySecret'].Value" --output text) + client_cert_arn=$(aws cloudformation list-exports --query "Exports[?Name=='account-resources:PfpClientCertSecret'].Value" --output text) + aws secretsmanager get-secret-value --secret-id "${client_private_key_arn}" --query SecretString --output text > ~/.proxygen/tmp/client_private_key + aws secretsmanager get-secret-value --secret-id "${client_cert_arn}" --query SecretString --output text > ~/.proxygen/tmp/client_cert + + - name: Configure AWS Credentials for api release + if: ${{ inputs.DEPLOY_APIGEE == true && always() && !failure() && !cancelled() }} + uses: aws-actions/configure-aws-credentials@8df5847569e6427dd6c4fb1cf565c83acfa8afa7 + with: + aws-region: eu-west-2 + role-to-assume: ${{ secrets.PROXYGEN_ROLE }} + role-session-name: pfp-api-proxygen + + - name: Deploy PFP API to Apigee + shell: bash + if: ${{ inputs.DEPLOY_APIGEE == true && always() && !failure() && !cancelled() }} + env: + API_TYPE: standard + VERSION_NUMBER: ${{ inputs.VERSION_NUMBER }} + SPEC_PATH: ./packages/specification/dist/prescriptions-for-patients.resolved.json + STACK_NAME: ${{ inputs.STACK_NAME }} + AWS_ENVIRONMENT: ${{ inputs.TARGET_ENVIRONMENT }} + APIGEE_ENVIRONMENT: ${{ inputs.APIGEE_ENVIRONMENT }} + PROXYGEN_PRIVATE_KEY_NAME: PrescriptionsForPatientsProxygenPrivateKey + PROXYGEN_KID: "2026-01-22-PROD-prescriptions-for-patients-v2" + DRY_RUN: false + ENABLE_MUTUAL_TLS: ${{ inputs.ENABLE_MUTUAL_TLS }} + MTLS_KEY: ${{ inputs.MTLS_KEY }} + IS_PULL_REQUEST: ${{ inputs.IS_PULL_REQUEST }} + run: ./.github/scripts/deploy_api.sh + + - uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f + name: Upload specs + if: ${{ inputs.IS_CDK_DEPLOY == true && always() && !cancelled() }} + with: + name: ${{ inputs.APIGEE_ENVIRONMENT }}-specs + path: | + ./packages/specification/dist/prescriptions-for-patients.resolved.json + + - name: create_int_release_notes + uses: ./.github/actions/update_confluence_jira + if: ${{ inputs.IS_CDK_DEPLOY == true && inputs.CREATE_INT_RELEASE_NOTES == true && always() && !failure() && !cancelled() }} + with: + TARGET_ENVIRONMENT: int + CONFLUENCE_PAGE_ID: "768067990" + CREATE_RC_RELEASE_NOTES: false + DEV_CLOUD_FORMATION_CHECK_VERSION_ROLE: ${{ secrets.DEV_CLOUD_FORMATION_CHECK_VERSION_ROLE }} + TARGET_CLOUD_FORMATION_CHECK_VERSION_ROLE: ${{ secrets.INT_CLOUD_FORMATION_CHECK_VERSION_ROLE }} + DEV_CLOUD_FORMATION_EXECUTE_LAMBDA_ROLE: ${{ secrets.DEV_CLOUD_FORMATION_EXECUTE_LAMBDA_ROLE }} + + - name: create_prod_release_notes + uses: ./.github/actions/update_confluence_jira + if: ${{ inputs.IS_CDK_DEPLOY == true && inputs.CREATE_PROD_RELEASE_NOTES == true && always() && !failure() && !cancelled() }} + with: + TARGET_ENVIRONMENT: prod + CONFLUENCE_PAGE_ID: "768067992" + CREATE_RC_RELEASE_NOTES: false + DEV_CLOUD_FORMATION_CHECK_VERSION_ROLE: ${{ secrets.DEV_CLOUD_FORMATION_CHECK_VERSION_ROLE }} + TARGET_CLOUD_FORMATION_CHECK_VERSION_ROLE: ${{ secrets.PROD_CLOUD_FORMATION_CHECK_VERSION_ROLE }} + DEV_CLOUD_FORMATION_EXECUTE_LAMBDA_ROLE: ${{ secrets.DEV_CLOUD_FORMATION_EXECUTE_LAMBDA_ROLE }} + + - name: mark_released_in_jira + uses: ./.github/actions/mark_jira_released + if: ${{ inputs.IS_CDK_DEPLOY == true && inputs.MARK_JIRA_RELEASED == true && always() && !failure() && !cancelled() }} + with: + RELEASE_TAG: ${{ inputs.VERSION_NUMBER }} + DEV_CLOUD_FORMATION_EXECUTE_LAMBDA_ROLE: ${{ secrets.DEV_CLOUD_FORMATION_EXECUTE_LAMBDA_ROLE }} + + - name: Checkout gh-pages + if: ${{ inputs.IS_CDK_DEPLOY == true && inputs.IS_PULL_REQUEST == false }} + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd + with: + ref: gh-pages + path: gh-pages + + - name: Update release tag in github pages + # if: ${{ inputs.IS_CDK_DEPLOY == true && inputs.IS_PULL_REQUEST == false }} + # Disabled by default; can be explicitly enabled by setting UPDATE_GH_PAGES_RELEASE_TAG to true. + if: ${{ inputs.UPDATE_GH_PAGES_RELEASE_TAG == true && inputs.IS_PULL_REQUEST == false }} + run: | + cd gh-pages + NOW=$(date +'%Y-%m-%dT%H:%M:%S') + echo "tag,release_datetime" > _data/${{ inputs.APIGEE_ENVIRONMENT }}_latest.csv + echo "${{ inputs.VERSION_NUMBER }},${NOW}" >> _data/${{ inputs.APIGEE_ENVIRONMENT }}_latest.csv + echo "${{ inputs.VERSION_NUMBER }},${NOW}" >> _data/${{ inputs.APIGEE_ENVIRONMENT }}_deployments.csv + git config user.name github-actions + git config user.email github-actions@github.com + git add _data/${{ inputs.APIGEE_ENVIRONMENT }}_latest.csv + git add _data/${{ inputs.APIGEE_ENVIRONMENT }}_deployments.csv + git commit -m 'update releases for ${{ inputs.APIGEE_ENVIRONMENT }}' + parallel --retries 10 --delay 3 ::: "git pull --rebase && git push" + + regression_tests: + name: Regression Tests + uses: ./.github/workflows/run_regression_tests.yml + # TODO: are always, !fail, !cancelled needed? + if: ${{ always() && !failure() && !cancelled() && inputs.RUN_REGRESSION_TESTS == true }} + needs: [release_code_and_api] + with: + ENVIRONMENT: "${{ inputs.APIGEE_ENVIRONMENT }}" + VERSION_NUMBER: "${{ inputs.VERSION_NUMBER }}" + REGRESSION_TEST_PRODUCT: "${{ inputs.REGRESSION_TEST_PRODUCT }}" + REGRESSION_TEST_NON_PROXYGEN: "${{ inputs.REGRESSION_TEST_NON_PROXYGEN }}" + pinned_image: "${{ inputs.pinned_image }}" + secrets: + REGRESSION_TESTS_PEM: ${{ secrets.REGRESSION_TESTS_PEM }} diff --git a/.github/workflows/pull_request.yml b/.github/workflows/pull_request.yml index e49997869..d2e3a7e8b 100644 --- a/.github/workflows/pull_request.yml +++ b/.github/workflows/pull_request.yml @@ -19,7 +19,6 @@ jobs: with: verify_published_from_main_image: false - quality_checks: uses: NHSDigital/eps-common-workflows/.github/workflows/quality-checks-devcontainer.yml@5ac2707dd9cd60ad127275179495b9c890d74711 needs: [get_config_values] @@ -81,14 +80,33 @@ jobs: run: | echo "commit_id=${{ github.sha }}" >> "$GITHUB_OUTPUT" + set_deploy_mode: + runs-on: ubuntu-22.04 + outputs: + is_cdk_deploy: ${{ steps.deploy_mode.outputs.is_cdk_deploy }} + steps: + - name: Set deploy mode from PR title + id: deploy_mode + run: | + echo "is_cdk_deploy=${{ contains(github.event.pull_request.title, '[CDK]') }}" >> "$GITHUB_OUTPUT" + - name: Summarize deploy mode + env: + PR_TITLE: ${{ github.event.pull_request.title }} + IS_CDK_DEPLOY: ${{ steps.deploy_mode.outputs.is_cdk_deploy }} + run: | + echo "PR title: ${PR_TITLE}" >> "$GITHUB_STEP_SUMMARY" + echo "is_cdk_deploy: ${IS_CDK_DEPLOY}" >> "$GITHUB_STEP_SUMMARY" + package_code: - needs: [get_issue_number, tag_release, get_config_values] + if: ${{ needs.set_deploy_mode.outputs.is_cdk_deploy != 'true' }} + needs: [get_issue_number, tag_release, get_config_values, set_deploy_mode] uses: ./.github/workflows/sam_package_code.yml with: pinned_image: ${{ needs.get_config_values.outputs.pinned_image }} release_code: - needs: [get_issue_number, package_code, get_commit_id, get_config_values] + if: ${{ needs.set_deploy_mode.outputs.is_cdk_deploy != 'true' }} + needs: [get_issue_number, package_code, get_commit_id, get_config_values, set_deploy_mode] uses: ./.github/workflows/sam_release_code.yml with: IS_PULL_REQUEST: true @@ -119,8 +137,10 @@ jobs: TARGET_SPINE_SERVER: ${{ secrets.DEV_TARGET_SPINE_SERVER }} TARGET_SERVICE_SEARCH_SERVER: ${{ secrets.DEV_TARGET_SERVICE_SEARCH_SERVER }} PROXYGEN_ROLE: ${{ secrets.PROXYGEN_PTL_ROLE }} + release_sandbox_code: - needs: [get_issue_number, package_code, get_commit_id, get_config_values] + if: ${{ needs.set_deploy_mode.outputs.is_cdk_deploy != 'true' }} + needs: [get_issue_number, package_code, get_commit_id, get_config_values, set_deploy_mode] uses: ./.github/workflows/sam_release_code.yml with: IS_PULL_REQUEST: true @@ -148,3 +168,78 @@ jobs: TARGET_SPINE_SERVER: sandbox TARGET_SERVICE_SEARCH_SERVER: sandbox PROXYGEN_ROLE: ${{ secrets.PROXYGEN_PTL_ROLE }} + + package_cdk_code: + if: ${{ needs.set_deploy_mode.outputs.is_cdk_deploy == 'true' }} + needs: [get_issue_number, tag_release, get_config_values, set_deploy_mode] + uses: ./.github/workflows/cdk_package_code.yml + with: + pinned_image: ${{ needs.get_config_values.outputs.pinned_image }} + + release_cdk_code: + if: ${{ needs.set_deploy_mode.outputs.is_cdk_deploy == 'true' }} + needs: [get_issue_number, package_cdk_code, get_commit_id, get_config_values, set_deploy_mode] + uses: ./.github/workflows/cdk_release_code.yml + with: + IS_PULL_REQUEST: true + STACK_NAME: pfp-pr-cdk-${{needs.get_issue_number.outputs.issue_number}} + TARGET_ENVIRONMENT: dev + APIGEE_ENVIRONMENT: internal-dev + # TODO: needed? + # APIM_STATUS_API_KEY: ${{ secrets.APIM_STATUS_API_KEY }} + ENABLE_MUTUAL_TLS: false + MTLS_KEY: prescriptions-for-patients-mtls-1 + BUILD_ARTIFACT: build_artifact + TRUSTSTORE_FILE: pfp-truststore-pr.pem + VERSION_NUMBER: PR-${{ needs.get_issue_number.outputs.issue_number }} + COMMIT_ID: ${{ needs.get_commit_id.outputs.commit_id }} + LOG_LEVEL: DEBUG + LOG_RETENTION_DAYS: 30 + TOGGLE_GET_STATUS_UPDATES: true + ENABLE_ALERTS: false + STATE_MACHINE_LOG_LEVEL: ALL + RUN_REGRESSION_TESTS: false # Don't run regression tests on CDK yet + REGRESSION_TEST_PRODUCT: PFP-AWS + FORWARD_CSOC_LOGS: false + DEPLOY_APIGEE: true + ALLOW_NHS_NUMBER_OVERRIDE: true + pinned_image: ${{ needs.get_config_values.outputs.pinned_image }} + secrets: + REGRESSION_TESTS_PEM: ${{ secrets.REGRESSION_TESTS_PEM }} + CLOUD_FORMATION_DEPLOY_ROLE: ${{ secrets.DEV_CLOUD_FORMATION_DEPLOY_ROLE }} + TARGET_SPINE_SERVER: ${{ secrets.DEV_TARGET_SPINE_SERVER }} + TARGET_SERVICE_SEARCH_SERVER: ${{ secrets.DEV_TARGET_SERVICE_SEARCH_SERVER }} + PROXYGEN_ROLE: ${{ secrets.PROXYGEN_PTL_ROLE }} + + release_cdk_sandbox_code: + if: ${{ needs.set_deploy_mode.outputs.is_cdk_deploy == 'true' }} + needs: [get_issue_number, package_cdk_code, get_commit_id, get_config_values, set_deploy_mode] + uses: ./.github/workflows/cdk_release_code.yml + with: + IS_PULL_REQUEST: true + STACK_NAME: pfp-pr-cdk-${{needs.get_issue_number.outputs.issue_number}}-sandbox + TARGET_ENVIRONMENT: dev + APIGEE_ENVIRONMENT: internal-dev-sandbox + ENABLE_MUTUAL_TLS: false + MTLS_KEY: prescriptions-for-patients-mtls-1 + BUILD_ARTIFACT: build_artifact + TRUSTSTORE_FILE: pfp-sandbox-truststore.pem + VERSION_NUMBER: PR-${{ needs.get_issue_number.outputs.issue_number }} + COMMIT_ID: ${{ needs.get_commit_id.outputs.commit_id }} + LOG_LEVEL: DEBUG + LOG_RETENTION_DAYS: 30 + TOGGLE_GET_STATUS_UPDATES: true + ENABLE_ALERTS: false + STATE_MACHINE_LOG_LEVEL: ALL + RUN_REGRESSION_TESTS: false + REGRESSION_TEST_PRODUCT: PFP-AWS + FORWARD_CSOC_LOGS: false + DEPLOY_APIGEE: false + ALLOW_NHS_NUMBER_OVERRIDE: true + pinned_image: ${{ needs.get_config_values.outputs.pinned_image }} + secrets: + REGRESSION_TESTS_PEM: ${{ secrets.REGRESSION_TESTS_PEM }} + CLOUD_FORMATION_DEPLOY_ROLE: ${{ secrets.DEV_CLOUD_FORMATION_DEPLOY_ROLE }} + TARGET_SPINE_SERVER: sandbox + TARGET_SERVICE_SEARCH_SERVER: sandbox + PROXYGEN_ROLE: ${{ secrets.PROXYGEN_PTL_ROLE }} diff --git a/.gitignore b/.gitignore index 3bf963f19..bb7cf1944 100644 --- a/.gitignore +++ b/.gitignore @@ -5,8 +5,10 @@ **/public/ **/coverage/ **/node_modules/ +cdk.out/ .#* __pycache__/ +.env .envrc .idea .vscode/settings.json diff --git a/Makefile b/Makefile index a135cc27f..8b627b594 100644 --- a/Makefile +++ b/Makefile @@ -1,3 +1,20 @@ +SHELL = /bin/bash +.SHELLFLAGS = -o pipefail -c +stack_name ?= pfp-api +export CDK_APP_NAME=PfPApiApp +export CDK_CONFIG_stackName=${stack_name} +export CDK_CONFIG_versionNumber=undefined +export CDK_CONFIG_commitId=undefined +export CDK_CONFIG_isPullRequest=true # Turns off mTLS and drift detection when true +export CDK_CONFIG_environment=dev +export CDK_CONFIG_logRetentionInDays=30 +export CDK_CONFIG_logLevel=DEBUG +export CDK_CONFIG_targetSpineServer=msg.veit07.devspineservices.nhs.uk +export CDK_CONFIG_targetServiceSearchServer=int.api.service.nhs.uk +export CDK_CONFIG_toggleGetStatusUpdates=false +export CDK_CONFIG_allowNhsNumberOverride=false +export CDK_CONFIG_forwardCsocLogs=false + .PHONY: install build test publish release clean install-node install-python install-hooks sam-build sam-build-sandbox sam-run-local sam-sync sam-sync-sandbox sam-deploy sam-delete sam-list-endpoints sam-list-resources sam-list-outputs sam-validate sam-validate-sandbox sam-deploy-package compile-node compile compile-specification download-get-secrets-layer lint-node lint test clean deep-clean install: install-python install-hooks install-node @@ -104,6 +121,18 @@ sam-deploy-package: guard-artifact_bucket guard-artifact_bucket_prefix guard-sta EnableAlerts=$$ENABLE_ALERTS \ StateMachineLogLevel=$$STATE_MACHINE_LOG_LEVEL +cdk-deploy: download-get-secrets-layer + CDK_CONFIG_stackName=${stack_name} REQUIRE_APPROVAL="$${REQUIRE_APPROVAL:-any-change}" npm run cdk-deploy --workspace packages/cdk + +cdk-synth: download-get-secrets-layer + CDK_CONFIG_stackName=${stack_name} npm run cdk-synth --workspace packages/cdk + +cdk-diff: + CDK_CONFIG_stackName=${stack_name} npm run cdk-diff --workspace packages/cdk + +cdk-watch: download-get-secrets-layer + CDK_CONFIG_stackName=${stack_name} REQUIRE_APPROVAL="$${REQUIRE_APPROVAL:-any-change}" npm run cdk-watch --workspace packages/cdk + compile-node: npx tsc --build tsconfig.build.json @@ -128,6 +157,7 @@ download-get-secrets-layer: fi lint-node: compile-node + npm run lint --workspace packages/cdk npm run lint --workspace packages/capabilityStatement npm run lint --workspace packages/getMyPrescriptions npm run lint --workspace packages/enrichPrescriptions @@ -142,6 +172,7 @@ lint: lint-node actionlint shellcheck cfn-lint echo "Linting complete" test: compile + npm run test --workspace packages/cdk npm run test --workspace packages/capabilityStatement npm run test --workspace packages/getMyPrescriptions npm run test --workspace packages/enrichPrescriptions @@ -153,6 +184,7 @@ test: compile npm run test --workspace packages/common/testing clean: + rm -rf packages/cdk/coverage rm -rf packages/capabilityStatement/coverage rm -rf packages/getMyPrescriptions/coverage rm -rf packages/enrichPrescriptions/coverage diff --git a/nhsdigital-eps-cdk-constructs-1.6.0.tgz b/nhsdigital-eps-cdk-constructs-1.6.0.tgz new file mode 100644 index 000000000..b920493d8 Binary files /dev/null and b/nhsdigital-eps-cdk-constructs-1.6.0.tgz differ diff --git a/package-lock.json b/package-lock.json index f815c5448..4e29f25c5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,6 +9,7 @@ "version": "1.0.0", "license": "MIT", "workspaces": [ + "packages/cdk", "packages/capabilityStatement", "packages/getMyPrescriptions", "packages/enrichPrescriptions", @@ -21,6 +22,7 @@ "packages/specification" ], "dependencies": { + "@nhsdigital/eps-cdk-constructs": "file:nhsdigital-eps-cdk-constructs-1.6.0.tgz", "esbuild": "^0.27.4" }, "devDependencies": { @@ -38,6 +40,132 @@ "vitest": "^4.1.0" } }, + "node_modules/@aws-cdk/asset-awscli-v1": { + "version": "2.2.263", + "resolved": "https://registry.npmjs.org/@aws-cdk/asset-awscli-v1/-/asset-awscli-v1-2.2.263.tgz", + "integrity": "sha512-X9JvcJhYcb7PHs8R7m4zMablO5C9PGb/hYfLnxds9h/rKJu6l7MiXE/SabCibuehxPnuO/vk+sVVJiUWrccarQ==", + "license": "Apache-2.0" + }, + "node_modules/@aws-cdk/asset-node-proxy-agent-v6": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@aws-cdk/asset-node-proxy-agent-v6/-/asset-node-proxy-agent-v6-2.1.1.tgz", + "integrity": "sha512-We4bmHaowOPHr+IQR4/FyTGjRfjgBj4ICMjtqmJeBDWad3Q/6St12NT07leNtyuukv2qMhtSZJQorD8KpKTwRA==", + "license": "Apache-2.0" + }, + "node_modules/@aws-cdk/cloud-assembly-schema": { + "version": "53.9.0", + "resolved": "https://registry.npmjs.org/@aws-cdk/cloud-assembly-schema/-/cloud-assembly-schema-53.9.0.tgz", + "integrity": "sha512-Ss7Af943iyyTABqeJS30LylmELpdpGgHzQP87KxO+HGPFIFDsoZymSuU1H5eQAcuuOvcfIPSKA62/lf274UB2A==", + "bundleDependencies": [ + "jsonschema", + "semver" + ], + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "jsonschema": "~1.4.1", + "semver": "^7.7.4" + }, + "engines": { + "node": ">= 18.0.0" + } + }, + "node_modules/@aws-cdk/cloud-assembly-schema/node_modules/jsonschema": { + "version": "1.4.1", + "inBundle": true, + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/@aws-cdk/cloud-assembly-schema/node_modules/semver": { + "version": "7.7.4", + "inBundle": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@aws-crypto/crc32": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/crc32/-/crc32-5.2.0.tgz", + "integrity": "sha512-nLbCWqQNgUiwwtFsen1AdzAtvuLRsQS8rYgMuxCrdKf9kOssamGLuPwyTY9wyYblNr9+1XM8v6zoDTPPSIeANg==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/util": "^5.2.0", + "@aws-sdk/types": "^3.222.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-crypto/crc32c": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/crc32c/-/crc32c-5.2.0.tgz", + "integrity": "sha512-+iWb8qaHLYKrNvGRbiYRHSdKRWhto5XlZUEBwDjYNf+ly5SVYG6zEoYIdxvf5R3zyeP16w4PLBn3rH1xc74Rag==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/util": "^5.2.0", + "@aws-sdk/types": "^3.222.0", + "tslib": "^2.6.2" + } + }, + "node_modules/@aws-crypto/sha1-browser": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/sha1-browser/-/sha1-browser-5.2.0.tgz", + "integrity": "sha512-OH6lveCFfcDjX4dbAvCFSYUjJZjDr/3XJ3xHtjn3Oj5b9RjojQo8npoLeA/bNwkOkrSQ0wgrHzXk4tDRxGKJeg==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/supports-web-crypto": "^5.2.0", + "@aws-crypto/util": "^5.2.0", + "@aws-sdk/types": "^3.222.0", + "@aws-sdk/util-locate-window": "^3.0.0", + "@smithy/util-utf8": "^2.0.0", + "tslib": "^2.6.2" + } + }, + "node_modules/@aws-crypto/sha1-browser/node_modules/@smithy/is-array-buffer": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-2.2.0.tgz", + "integrity": "sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-crypto/sha1-browser/node_modules/@smithy/util-buffer-from": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-2.2.0.tgz", + "integrity": "sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/is-array-buffer": "^2.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-crypto/sha1-browser/node_modules/@smithy/util-utf8": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-2.3.0.tgz", + "integrity": "sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/util-buffer-from": "^2.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, "node_modules/@aws-crypto/sha256-browser": { "version": "5.2.0", "license": "Apache-2.0", @@ -211,6 +339,175 @@ } } }, + "node_modules/@aws-sdk/client-cloudformation": { + "version": "3.1018.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-cloudformation/-/client-cloudformation-3.1018.0.tgz", + "integrity": "sha512-wgxqZIWMcblUHnE7lAxHHQr9RGbG756M/7gBrRD2O9Iad2xWC8QhGAmtOO1eD01YeQInJp02ouAiq5BTqNipIg==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/core": "^3.973.25", + "@aws-sdk/credential-provider-node": "^3.972.26", + "@aws-sdk/middleware-host-header": "^3.972.8", + "@aws-sdk/middleware-logger": "^3.972.8", + "@aws-sdk/middleware-recursion-detection": "^3.972.9", + "@aws-sdk/middleware-user-agent": "^3.972.26", + "@aws-sdk/region-config-resolver": "^3.972.10", + "@aws-sdk/types": "^3.973.6", + "@aws-sdk/util-endpoints": "^3.996.5", + "@aws-sdk/util-user-agent-browser": "^3.972.8", + "@aws-sdk/util-user-agent-node": "^3.973.12", + "@smithy/config-resolver": "^4.4.13", + "@smithy/core": "^3.23.12", + "@smithy/fetch-http-handler": "^5.3.15", + "@smithy/hash-node": "^4.2.12", + "@smithy/invalid-dependency": "^4.2.12", + "@smithy/middleware-content-length": "^4.2.12", + "@smithy/middleware-endpoint": "^4.4.27", + "@smithy/middleware-retry": "^4.4.44", + "@smithy/middleware-serde": "^4.2.15", + "@smithy/middleware-stack": "^4.2.12", + "@smithy/node-config-provider": "^4.3.12", + "@smithy/node-http-handler": "^4.5.0", + "@smithy/protocol-http": "^5.3.12", + "@smithy/smithy-client": "^4.12.7", + "@smithy/types": "^4.13.1", + "@smithy/url-parser": "^4.2.12", + "@smithy/util-base64": "^4.3.2", + "@smithy/util-body-length-browser": "^4.2.2", + "@smithy/util-body-length-node": "^4.2.3", + "@smithy/util-defaults-mode-browser": "^4.3.43", + "@smithy/util-defaults-mode-node": "^4.2.47", + "@smithy/util-endpoints": "^3.3.3", + "@smithy/util-middleware": "^4.2.12", + "@smithy/util-retry": "^4.2.12", + "@smithy/util-utf8": "^4.2.2", + "@smithy/util-waiter": "^4.2.13", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/client-route-53": { + "version": "3.1018.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-route-53/-/client-route-53-3.1018.0.tgz", + "integrity": "sha512-ZVMWxgkfe50RqYtiPnPIRiKWDytcVft+LB+4a/N7nsd6WY8zhhM06h7ExfLuUtNioMIngaz0OaRuUTb+OcLpZw==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/core": "^3.973.25", + "@aws-sdk/credential-provider-node": "^3.972.26", + "@aws-sdk/middleware-host-header": "^3.972.8", + "@aws-sdk/middleware-logger": "^3.972.8", + "@aws-sdk/middleware-recursion-detection": "^3.972.9", + "@aws-sdk/middleware-sdk-route53": "^3.972.10", + "@aws-sdk/middleware-user-agent": "^3.972.26", + "@aws-sdk/region-config-resolver": "^3.972.10", + "@aws-sdk/types": "^3.973.6", + "@aws-sdk/util-endpoints": "^3.996.5", + "@aws-sdk/util-user-agent-browser": "^3.972.8", + "@aws-sdk/util-user-agent-node": "^3.973.12", + "@smithy/config-resolver": "^4.4.13", + "@smithy/core": "^3.23.12", + "@smithy/fetch-http-handler": "^5.3.15", + "@smithy/hash-node": "^4.2.12", + "@smithy/invalid-dependency": "^4.2.12", + "@smithy/middleware-content-length": "^4.2.12", + "@smithy/middleware-endpoint": "^4.4.27", + "@smithy/middleware-retry": "^4.4.44", + "@smithy/middleware-serde": "^4.2.15", + "@smithy/middleware-stack": "^4.2.12", + "@smithy/node-config-provider": "^4.3.12", + "@smithy/node-http-handler": "^4.5.0", + "@smithy/protocol-http": "^5.3.12", + "@smithy/smithy-client": "^4.12.7", + "@smithy/types": "^4.13.1", + "@smithy/url-parser": "^4.2.12", + "@smithy/util-base64": "^4.3.2", + "@smithy/util-body-length-browser": "^4.2.2", + "@smithy/util-body-length-node": "^4.2.3", + "@smithy/util-defaults-mode-browser": "^4.3.43", + "@smithy/util-defaults-mode-node": "^4.2.47", + "@smithy/util-endpoints": "^3.3.3", + "@smithy/util-middleware": "^4.2.12", + "@smithy/util-retry": "^4.2.12", + "@smithy/util-utf8": "^4.2.2", + "@smithy/util-waiter": "^4.2.13", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/client-s3": { + "version": "3.1018.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-s3/-/client-s3-3.1018.0.tgz", + "integrity": "sha512-BiGKMjrkAJkyse1ECpVyxVYugf82FB3cM9zgKpx3boFuWobyolG5ri6XjoMIY8fpHddaO8ZClXEedACyelSLWA==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/sha1-browser": "5.2.0", + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/core": "^3.973.25", + "@aws-sdk/credential-provider-node": "^3.972.26", + "@aws-sdk/middleware-bucket-endpoint": "^3.972.8", + "@aws-sdk/middleware-expect-continue": "^3.972.8", + "@aws-sdk/middleware-flexible-checksums": "^3.974.5", + "@aws-sdk/middleware-host-header": "^3.972.8", + "@aws-sdk/middleware-location-constraint": "^3.972.8", + "@aws-sdk/middleware-logger": "^3.972.8", + "@aws-sdk/middleware-recursion-detection": "^3.972.9", + "@aws-sdk/middleware-sdk-s3": "^3.972.26", + "@aws-sdk/middleware-ssec": "^3.972.8", + "@aws-sdk/middleware-user-agent": "^3.972.26", + "@aws-sdk/region-config-resolver": "^3.972.10", + "@aws-sdk/signature-v4-multi-region": "^3.996.14", + "@aws-sdk/types": "^3.973.6", + "@aws-sdk/util-endpoints": "^3.996.5", + "@aws-sdk/util-user-agent-browser": "^3.972.8", + "@aws-sdk/util-user-agent-node": "^3.973.12", + "@smithy/config-resolver": "^4.4.13", + "@smithy/core": "^3.23.12", + "@smithy/eventstream-serde-browser": "^4.2.12", + "@smithy/eventstream-serde-config-resolver": "^4.3.12", + "@smithy/eventstream-serde-node": "^4.2.12", + "@smithy/fetch-http-handler": "^5.3.15", + "@smithy/hash-blob-browser": "^4.2.13", + "@smithy/hash-node": "^4.2.12", + "@smithy/hash-stream-node": "^4.2.12", + "@smithy/invalid-dependency": "^4.2.12", + "@smithy/md5-js": "^4.2.12", + "@smithy/middleware-content-length": "^4.2.12", + "@smithy/middleware-endpoint": "^4.4.27", + "@smithy/middleware-retry": "^4.4.44", + "@smithy/middleware-serde": "^4.2.15", + "@smithy/middleware-stack": "^4.2.12", + "@smithy/node-config-provider": "^4.3.12", + "@smithy/node-http-handler": "^4.5.0", + "@smithy/protocol-http": "^5.3.12", + "@smithy/smithy-client": "^4.12.7", + "@smithy/types": "^4.13.1", + "@smithy/url-parser": "^4.2.12", + "@smithy/util-base64": "^4.3.2", + "@smithy/util-body-length-browser": "^4.2.2", + "@smithy/util-body-length-node": "^4.2.3", + "@smithy/util-defaults-mode-browser": "^4.3.43", + "@smithy/util-defaults-mode-node": "^4.2.47", + "@smithy/util-endpoints": "^3.3.3", + "@smithy/util-middleware": "^4.2.12", + "@smithy/util-retry": "^4.2.12", + "@smithy/util-stream": "^4.5.20", + "@smithy/util-utf8": "^4.2.2", + "@smithy/util-waiter": "^4.2.13", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, "node_modules/@aws-sdk/client-secrets-manager": { "version": "3.1016.0", "resolved": "https://registry.npmjs.org/@aws-sdk/client-secrets-manager/-/client-secrets-manager-3.1016.0.tgz", @@ -266,6 +563,7 @@ "resolved": "https://registry.npmjs.org/@aws-sdk/client-ssm/-/client-ssm-3.1016.0.tgz", "integrity": "sha512-9qb58Utss0lJGAcQxftSQ1OY5Dm2aKtn/aBdV2ProFRkyQuxOWJsKMmAAqOpMYnQBEgs6k/ArvBZjQIESrsxfg==", "license": "Apache-2.0", + "peer": true, "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", @@ -336,6 +634,19 @@ "node": ">=20.0.0" } }, + "node_modules/@aws-sdk/crc64-nvme": { + "version": "3.972.5", + "resolved": "https://registry.npmjs.org/@aws-sdk/crc64-nvme/-/crc64-nvme-3.972.5.tgz", + "integrity": "sha512-2VbTstbjKdT+yKi8m7b3a9CiVac+pL/IY2PHJwsaGkkHmuuqkJZIErPck1h6P3T9ghQMLSdMPyW6Qp7Di5swFg==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.13.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, "node_modules/@aws-sdk/credential-provider-env": { "version": "3.972.23", "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-env/-/credential-provider-env-3.972.23.tgz", @@ -494,28 +805,32 @@ "node": ">=20.0.0" } }, - "node_modules/@aws-sdk/middleware-host-header": { + "node_modules/@aws-sdk/middleware-bucket-endpoint": { "version": "3.972.8", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-host-header/-/middleware-host-header-3.972.8.tgz", - "integrity": "sha512-wAr2REfKsqoKQ+OkNqvOShnBoh+nkPurDKW7uAeVSu6kUECnWlSJiPvnoqxGlfousEY/v9LfS9sNc46hjSYDIQ==", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-bucket-endpoint/-/middleware-bucket-endpoint-3.972.8.tgz", + "integrity": "sha512-WR525Rr2QJSETa9a050isktyWi/4yIGcmY3BQ1kpHqb0LqUglQHCS8R27dTJxxWNZvQ0RVGtEZjTCbZJpyF3Aw==", "license": "Apache-2.0", "dependencies": { "@aws-sdk/types": "^3.973.6", + "@aws-sdk/util-arn-parser": "^3.972.3", + "@smithy/node-config-provider": "^4.3.12", "@smithy/protocol-http": "^5.3.12", "@smithy/types": "^4.13.1", + "@smithy/util-config-provider": "^4.2.2", "tslib": "^2.6.2" }, "engines": { "node": ">=20.0.0" } }, - "node_modules/@aws-sdk/middleware-logger": { + "node_modules/@aws-sdk/middleware-expect-continue": { "version": "3.972.8", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-logger/-/middleware-logger-3.972.8.tgz", - "integrity": "sha512-CWl5UCM57WUFaFi5kB7IBY1UmOeLvNZAZ2/OZ5l20ldiJ3TiIz1pC65gYj8X0BCPWkeR1E32mpsCk1L1I4n+lA==", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-expect-continue/-/middleware-expect-continue-3.972.8.tgz", + "integrity": "sha512-5DTBTiotEES1e2jOHAq//zyzCjeMB78lEHd35u15qnrid4Nxm7diqIf9fQQ3Ov0ChH1V3Vvt13thOnrACmfGVQ==", "license": "Apache-2.0", "dependencies": { "@aws-sdk/types": "^3.973.6", + "@smithy/protocol-http": "^5.3.12", "@smithy/types": "^4.13.1", "tslib": "^2.6.2" }, @@ -523,18 +838,139 @@ "node": ">=20.0.0" } }, - "node_modules/@aws-sdk/middleware-recursion-detection": { - "version": "3.972.9", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-recursion-detection/-/middleware-recursion-detection-3.972.9.tgz", - "integrity": "sha512-/Wt5+CT8dpTFQxEJ9iGy/UGrXr7p2wlIOEHvIr/YcHYByzoLjrqkYqXdJjd9UIgWjv7eqV2HnFJen93UTuwfTQ==", + "node_modules/@aws-sdk/middleware-flexible-checksums": { + "version": "3.974.5", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-flexible-checksums/-/middleware-flexible-checksums-3.974.5.tgz", + "integrity": "sha512-SPSvF0G1t8m8CcB0L+ClNFszzQOvXaxmRj25oRWDf6aU+TuN2PXPFAJ9A6lt1IvX4oGAqqbTdMPTYs/SSHUYYQ==", "license": "Apache-2.0", "dependencies": { + "@aws-crypto/crc32": "5.2.0", + "@aws-crypto/crc32c": "5.2.0", + "@aws-crypto/util": "5.2.0", + "@aws-sdk/core": "^3.973.25", + "@aws-sdk/crc64-nvme": "^3.972.5", "@aws-sdk/types": "^3.973.6", - "@aws/lambda-invoke-store": "^0.2.2", + "@smithy/is-array-buffer": "^4.2.2", + "@smithy/node-config-provider": "^4.3.12", "@smithy/protocol-http": "^5.3.12", "@smithy/types": "^4.13.1", - "tslib": "^2.6.2" - }, + "@smithy/util-middleware": "^4.2.12", + "@smithy/util-stream": "^4.5.20", + "@smithy/util-utf8": "^4.2.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/middleware-host-header": { + "version": "3.972.8", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-host-header/-/middleware-host-header-3.972.8.tgz", + "integrity": "sha512-wAr2REfKsqoKQ+OkNqvOShnBoh+nkPurDKW7uAeVSu6kUECnWlSJiPvnoqxGlfousEY/v9LfS9sNc46hjSYDIQ==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "^3.973.6", + "@smithy/protocol-http": "^5.3.12", + "@smithy/types": "^4.13.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/middleware-location-constraint": { + "version": "3.972.8", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-location-constraint/-/middleware-location-constraint-3.972.8.tgz", + "integrity": "sha512-KaUoFuoFPziIa98DSQsTPeke1gvGXlc5ZGMhy+b+nLxZ4A7jmJgLzjEF95l8aOQN2T/qlPP3MrAyELm8ExXucw==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "^3.973.6", + "@smithy/types": "^4.13.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/middleware-logger": { + "version": "3.972.8", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-logger/-/middleware-logger-3.972.8.tgz", + "integrity": "sha512-CWl5UCM57WUFaFi5kB7IBY1UmOeLvNZAZ2/OZ5l20ldiJ3TiIz1pC65gYj8X0BCPWkeR1E32mpsCk1L1I4n+lA==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "^3.973.6", + "@smithy/types": "^4.13.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/middleware-recursion-detection": { + "version": "3.972.9", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-recursion-detection/-/middleware-recursion-detection-3.972.9.tgz", + "integrity": "sha512-/Wt5+CT8dpTFQxEJ9iGy/UGrXr7p2wlIOEHvIr/YcHYByzoLjrqkYqXdJjd9UIgWjv7eqV2HnFJen93UTuwfTQ==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "^3.973.6", + "@aws/lambda-invoke-store": "^0.2.2", + "@smithy/protocol-http": "^5.3.12", + "@smithy/types": "^4.13.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/middleware-sdk-route53": { + "version": "3.972.10", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-sdk-route53/-/middleware-sdk-route53-3.972.10.tgz", + "integrity": "sha512-eVSTduHxtUd1M/KxKFiAHj1Q9djzx428iPe7Yd8yJdEsgnVH0wPFsCK/ExB0Rv/yIYlVXdBlFJmMj0FgRj4crQ==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "^3.973.6", + "@smithy/types": "^4.13.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/middleware-sdk-s3": { + "version": "3.972.26", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-sdk-s3/-/middleware-sdk-s3-3.972.26.tgz", + "integrity": "sha512-5q7UGSTtt7/KF0Os8wj2VZtlLxeWJVb0e2eDrDJlWot2EIxUNKDDMPFq/FowUqrwZ40rO2bu6BypxaKNvQhI+g==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.973.25", + "@aws-sdk/types": "^3.973.6", + "@aws-sdk/util-arn-parser": "^3.972.3", + "@smithy/core": "^3.23.12", + "@smithy/node-config-provider": "^4.3.12", + "@smithy/protocol-http": "^5.3.12", + "@smithy/signature-v4": "^5.3.12", + "@smithy/smithy-client": "^4.12.7", + "@smithy/types": "^4.13.1", + "@smithy/util-config-provider": "^4.2.2", + "@smithy/util-middleware": "^4.2.12", + "@smithy/util-stream": "^4.5.20", + "@smithy/util-utf8": "^4.2.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/middleware-ssec": { + "version": "3.972.8", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-ssec/-/middleware-ssec-3.972.8.tgz", + "integrity": "sha512-wqlK0yO/TxEC2UsY9wIlqeeutF6jjLe0f96Pbm40XscTo57nImUk9lBcw0dPgsm0sppFtAkSlDrfpK+pC30Wqw==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "^3.973.6", + "@smithy/types": "^4.13.1", + "tslib": "^2.6.2" + }, "engines": { "node": ">=20.0.0" } @@ -623,6 +1059,23 @@ "node": ">=20.0.0" } }, + "node_modules/@aws-sdk/signature-v4-multi-region": { + "version": "3.996.14", + "resolved": "https://registry.npmjs.org/@aws-sdk/signature-v4-multi-region/-/signature-v4-multi-region-3.996.14.tgz", + "integrity": "sha512-4nZSrBr1NO+48HCM/6BRU8mnRjuHZjcpziCvLXZk5QVftwWz5Mxqbhwdz4xf7WW88buaTB8uRO2MHklSX1m0vg==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/middleware-sdk-s3": "^3.972.26", + "@aws-sdk/types": "^3.973.6", + "@smithy/protocol-http": "^5.3.12", + "@smithy/signature-v4": "^5.3.12", + "@smithy/types": "^4.13.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, "node_modules/@aws-sdk/token-providers": { "version": "3.1018.0", "resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.1018.0.tgz", @@ -654,6 +1107,18 @@ "node": ">=20.0.0" } }, + "node_modules/@aws-sdk/util-arn-parser": { + "version": "3.972.3", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-arn-parser/-/util-arn-parser-3.972.3.tgz", + "integrity": "sha512-HzSD8PMFrvgi2Kserxuff5VitNq2sgf3w9qxmskKDiDTThWfVteJxuCS9JXiPIPtmCrp+7N9asfIaVhBFORllA==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, "node_modules/@aws-sdk/util-endpoints": { "version": "3.996.5", "resolved": "https://registry.npmjs.org/@aws-sdk/util-endpoints/-/util-endpoints-3.996.5.tgz", @@ -1603,6 +2068,21 @@ "uninstall": "^0.0.0" } }, + "node_modules/@nhsdigital/eps-cdk-constructs": { + "version": "1.6.0", + "resolved": "file:nhsdigital-eps-cdk-constructs-1.6.0.tgz", + "integrity": "sha512-fFbcRQ7cbbbcfpcliT+0zAkXsZaUjbY/9m/ST6waA/yc3wl7vSrMzYJEpkwUmOaX9U+MfWmKx5FPjOuTeB75BQ==", + "license": "MIT", + "dependencies": { + "@aws-sdk/client-cloudformation": "^3.1018.0", + "@aws-sdk/client-route-53": "^3.1018.0", + "@aws-sdk/client-s3": "^3.1018.0", + "aws-cdk": "^2.1114.1", + "aws-cdk-lib": "^2.244.0", + "cdk-nag": "^2.37.52", + "constructs": "^10.6.0" + } + }, "node_modules/@nhsdigital/eps-spine-client": { "version": "2.1.78", "license": "MIT", @@ -1631,6 +2111,7 @@ "version": "1.9.0", "dev": true, "license": "Apache-2.0", + "peer": true, "engines": { "node": ">=8.0.0" } @@ -2514,6 +2995,31 @@ "node": ">=18.0.0" } }, + "node_modules/@smithy/chunked-blob-reader": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/@smithy/chunked-blob-reader/-/chunked-blob-reader-5.2.2.tgz", + "integrity": "sha512-St+kVicSyayWQca+I1rGitaOEH6uKgE8IUWoYnnEX26SWdWQcL6LvMSD19Lg+vYHKdT9B2Zuu7rd3i6Wnyb/iw==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/chunked-blob-reader-native": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/@smithy/chunked-blob-reader-native/-/chunked-blob-reader-native-4.2.3.tgz", + "integrity": "sha512-jA5k5Udn7Y5717L86h4EIv06wIr3xn8GM1qHRi/Nf31annXcXHJjBKvgztnbn2TxH3xWrPBfgwHsOwZf0UmQWw==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/util-base64": "^4.3.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, "node_modules/@smithy/config-resolver": { "version": "4.4.13", "resolved": "https://registry.npmjs.org/@smithy/config-resolver/-/config-resolver-4.4.13.tgz", @@ -2568,6 +3074,76 @@ "node": ">=18.0.0" } }, + "node_modules/@smithy/eventstream-codec": { + "version": "4.2.12", + "resolved": "https://registry.npmjs.org/@smithy/eventstream-codec/-/eventstream-codec-4.2.12.tgz", + "integrity": "sha512-FE3bZdEl62ojmy8x4FHqxq2+BuOHlcxiH5vaZ6aqHJr3AIZzwF5jfx8dEiU/X0a8RboyNDjmXjlbr8AdEyLgiA==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/crc32": "5.2.0", + "@smithy/types": "^4.13.1", + "@smithy/util-hex-encoding": "^4.2.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/eventstream-serde-browser": { + "version": "4.2.12", + "resolved": "https://registry.npmjs.org/@smithy/eventstream-serde-browser/-/eventstream-serde-browser-4.2.12.tgz", + "integrity": "sha512-XUSuMxlTxV5pp4VpqZf6Sa3vT/Q75FVkLSpSSE3KkWBvAQWeuWt1msTv8fJfgA4/jcJhrbrbMzN1AC/hvPmm5A==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/eventstream-serde-universal": "^4.2.12", + "@smithy/types": "^4.13.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/eventstream-serde-config-resolver": { + "version": "4.3.12", + "resolved": "https://registry.npmjs.org/@smithy/eventstream-serde-config-resolver/-/eventstream-serde-config-resolver-4.3.12.tgz", + "integrity": "sha512-7epsAZ3QvfHkngz6RXQYseyZYHlmWXSTPOfPmXkiS+zA6TBNo1awUaMFL9vxyXlGdoELmCZyZe1nQE+imbmV+Q==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.13.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/eventstream-serde-node": { + "version": "4.2.12", + "resolved": "https://registry.npmjs.org/@smithy/eventstream-serde-node/-/eventstream-serde-node-4.2.12.tgz", + "integrity": "sha512-D1pFuExo31854eAvg89KMn9Oab/wEeJR6Buy32B49A9Ogdtx5fwZPqBHUlDzaCDpycTFk2+fSQgX689Qsk7UGA==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/eventstream-serde-universal": "^4.2.12", + "@smithy/types": "^4.13.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/eventstream-serde-universal": { + "version": "4.2.12", + "resolved": "https://registry.npmjs.org/@smithy/eventstream-serde-universal/-/eventstream-serde-universal-4.2.12.tgz", + "integrity": "sha512-+yNuTiyBACxOJUTvbsNsSOfH9G9oKbaJE1lNL3YHpGcuucl6rPZMi3nrpehpVOVR2E07YqFFmtwpImtpzlouHQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/eventstream-codec": "^4.2.12", + "@smithy/types": "^4.13.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, "node_modules/@smithy/fetch-http-handler": { "version": "5.3.15", "resolved": "https://registry.npmjs.org/@smithy/fetch-http-handler/-/fetch-http-handler-5.3.15.tgz", @@ -2584,6 +3160,21 @@ "node": ">=18.0.0" } }, + "node_modules/@smithy/hash-blob-browser": { + "version": "4.2.13", + "resolved": "https://registry.npmjs.org/@smithy/hash-blob-browser/-/hash-blob-browser-4.2.13.tgz", + "integrity": "sha512-YrF4zWKh+ghLuquldj6e/RzE3xZYL8wIPfkt0MqCRphVICjyyjH8OwKD7LLlKpVEbk4FLizFfC1+gwK6XQdR3g==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/chunked-blob-reader": "^5.2.2", + "@smithy/chunked-blob-reader-native": "^4.2.3", + "@smithy/types": "^4.13.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, "node_modules/@smithy/hash-node": { "version": "4.2.12", "resolved": "https://registry.npmjs.org/@smithy/hash-node/-/hash-node-4.2.12.tgz", @@ -2599,6 +3190,20 @@ "node": ">=18.0.0" } }, + "node_modules/@smithy/hash-stream-node": { + "version": "4.2.12", + "resolved": "https://registry.npmjs.org/@smithy/hash-stream-node/-/hash-stream-node-4.2.12.tgz", + "integrity": "sha512-O3YbmGExeafuM/kP7Y8r6+1y0hIh3/zn6GROx0uNlB54K9oihAL75Qtc+jFfLNliTi6pxOAYZrRKD9A7iA6UFw==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.13.1", + "@smithy/util-utf8": "^4.2.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, "node_modules/@smithy/invalid-dependency": { "version": "4.2.12", "resolved": "https://registry.npmjs.org/@smithy/invalid-dependency/-/invalid-dependency-4.2.12.tgz", @@ -2622,6 +3227,20 @@ "node": ">=18.0.0" } }, + "node_modules/@smithy/md5-js": { + "version": "4.2.12", + "resolved": "https://registry.npmjs.org/@smithy/md5-js/-/md5-js-4.2.12.tgz", + "integrity": "sha512-W/oIpHCpWU2+iAkfZYyGWE+qkpuf3vEXHLxQQDx9FPNZTTdnul0dZ2d/gUFrtQ5je1G2kp4cjG0/24YueG2LbQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.13.1", + "@smithy/util-utf8": "^4.2.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, "node_modules/@smithy/middleware-content-length": { "version": "4.2.12", "resolved": "https://registry.npmjs.org/@smithy/middleware-content-length/-/middleware-content-length-4.2.12.tgz", @@ -3172,6 +3791,7 @@ "integrity": "sha512-jp2P3tQMSxWugkCUKLRPVUpGaL5MVFwF8RDuSRztfwgN1wmqJeMSbKlnEtQqU8UrhTmzEmZdu2I6v2dpp7XIxw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "undici-types": "~7.18.0" } @@ -3222,6 +3842,7 @@ "integrity": "sha512-30ScMRHIAD33JJQkgfGW1t8CURZtjc2JpTrq5n2HFhOefbAhb7ucc7xJwdWcrEtqUIYJ73Nybpsggii6GtAHjA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.57.2", "@typescript-eslint/types": "8.57.2", @@ -3499,228 +4120,636 @@ "dev": true, "license": "MIT", "dependencies": { - "@vitest/utils": "4.1.1", - "pathe": "^2.0.3" + "@vitest/utils": "4.1.1", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.1.1.tgz", + "integrity": "sha512-kMVSgcegWV2FibXEx9p9WIKgje58lcTbXgnJixfcg15iK8nzCXhmalL0ZLtTWLW9PH1+1NEDShiFFedB3tEgWg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "4.1.1", + "@vitest/utils": "4.1.1", + "magic-string": "^0.30.21", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/spy": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.1.tgz", + "integrity": "sha512-6Ti/KT5OVaiupdIZEuZN7l3CZcR0cxnxt70Z0//3CtwgObwA6jZhmVBA3yrXSVN3gmwjgd7oDNLlsXz526gpRA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.1.tgz", + "integrity": "sha512-cNxAlaB3sHoCdL6pj6yyUXv9Gry1NHNg0kFTXdvSIZXLHsqKH7chiWOkwJ5s5+d/oMwcoG9T0bKU38JZWKusrQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "4.1.1", + "convert-source-map": "^2.0.0", + "tinyrainbow": "^3.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/abort-controller": { + "version": "3.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "event-target-shim": "^5.0.0" + }, + "engines": { + "node": ">=6.5" + } + }, + "node_modules/acorn": { + "version": "8.16.0", + "dev": true, + "license": "MIT", + "peer": true, + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/acorn-walk": { + "version": "8.3.4", + "dev": true, + "license": "MIT", + "dependencies": { + "acorn": "^8.11.0" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/agent-base": { + "version": "7.1.4", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, + "node_modules/ajv": { + "version": "6.14.0", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ajv-formats": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-3.0.1.tgz", + "integrity": "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, + "node_modules/ajv-formats/node_modules/ajv": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz", + "integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ajv-formats/node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true, + "license": "MIT" + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/apim-spec": { + "resolved": "packages/specification", + "link": true + }, + "node_modules/arg": { + "version": "4.1.3", + "dev": true, + "license": "MIT" + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true, + "license": "Python-2.0" + }, + "node_modules/assertion-error": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/ast-v8-to-istanbul": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/ast-v8-to-istanbul/-/ast-v8-to-istanbul-1.0.0.tgz", + "integrity": "sha512-1fSfIwuDICFA4LKkCzRPO7F0hzFf0B7+Xqrl27ynQaa+Rh0e1Es0v6kWHPott3lU10AyAr7oKHa65OppjLn3Rg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.31", + "estree-walker": "^3.0.3", + "js-tokens": "^10.0.0" + } + }, + "node_modules/ast-v8-to-istanbul/node_modules/js-tokens": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-10.0.0.tgz", + "integrity": "sha512-lM/UBzQmfJRo9ABXbPWemivdCW8V2G8FHaHdypQaIy523snUjog0W71ayWXTjiR+ixeMyVHN2XcpnTd/liPg/Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/asynckit": { + "version": "0.4.0", + "license": "MIT" + }, + "node_modules/aws-cdk": { + "version": "2.1114.1", + "resolved": "https://registry.npmjs.org/aws-cdk/-/aws-cdk-2.1114.1.tgz", + "integrity": "sha512-jMaKPWQQs1G6AbhfCQG2zGrgAhTxzP0jn4T2CuwONuvcV374dMPhfC/LBAv48ruSOgpCK9x6V1xeO/aH3EchAA==", + "license": "Apache-2.0", + "bin": { + "cdk": "bin/cdk" + }, + "engines": { + "node": ">= 18.0.0" + } + }, + "node_modules/aws-cdk-lib": { + "version": "2.245.0", + "resolved": "https://registry.npmjs.org/aws-cdk-lib/-/aws-cdk-lib-2.245.0.tgz", + "integrity": "sha512-Yfeb+wKC6s+Ttm/N93C6vY6ksyCh68WaG/j3N6dalJWTW/V4o6hUolHm+v2c2IofJEUS45c5AF/EEj24e9hfMA==", + "bundleDependencies": [ + "@balena/dockerignore", + "@aws-cdk/cloud-assembly-api", + "case", + "fs-extra", + "ignore", + "jsonschema", + "minimatch", + "punycode", + "semver", + "table", + "yaml", + "mime-types" + ], + "license": "Apache-2.0", + "dependencies": { + "@aws-cdk/asset-awscli-v1": "2.2.263", + "@aws-cdk/asset-node-proxy-agent-v6": "^2.1.1", + "@aws-cdk/cloud-assembly-api": "^2.2.0", + "@aws-cdk/cloud-assembly-schema": "^53.0.0", + "@balena/dockerignore": "^1.0.2", + "case": "1.6.3", + "fs-extra": "^11.3.3", + "ignore": "^5.3.2", + "jsonschema": "^1.5.0", + "mime-types": "^2.1.35", + "minimatch": "^10.2.3", + "punycode": "^2.3.1", + "semver": "^7.7.4", + "table": "^6.9.0", + "yaml": "1.10.3" + }, + "engines": { + "node": ">= 20.0.0" + }, + "peerDependencies": { + "constructs": "^10.5.0" + } + }, + "node_modules/aws-cdk-lib/node_modules/@aws-cdk/cloud-assembly-api": { + "version": "2.2.0", + "bundleDependencies": [ + "jsonschema", + "semver" + ], + "inBundle": true, + "license": "Apache-2.0", + "dependencies": { + "jsonschema": "~1.4.1", + "semver": "^7.7.4" + }, + "engines": { + "node": ">= 18.0.0" + }, + "peerDependencies": { + "@aws-cdk/cloud-assembly-schema": ">=53.0.0" + } + }, + "node_modules/aws-cdk-lib/node_modules/@aws-cdk/cloud-assembly-api/node_modules/jsonschema": { + "version": "1.4.1", + "inBundle": true, + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/aws-cdk-lib/node_modules/@aws-cdk/cloud-assembly-api/node_modules/semver": { + "version": "7.7.4", + "inBundle": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/aws-cdk-lib/node_modules/@balena/dockerignore": { + "version": "1.0.2", + "inBundle": true, + "license": "Apache-2.0" + }, + "node_modules/aws-cdk-lib/node_modules/ajv": { + "version": "8.18.0", + "inBundle": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/aws-cdk-lib/node_modules/ansi-regex": { + "version": "5.0.1", + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/aws-cdk-lib/node_modules/ansi-styles": { + "version": "4.3.0", + "inBundle": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/aws-cdk-lib/node_modules/astral-regex": { + "version": "2.0.0", + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/aws-cdk-lib/node_modules/balanced-match": { + "version": "4.0.4", + "inBundle": true, + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/aws-cdk-lib/node_modules/brace-expansion": { + "version": "5.0.3", + "inBundle": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/aws-cdk-lib/node_modules/case": { + "version": "1.6.3", + "inBundle": true, + "license": "(MIT OR GPL-3.0-or-later)", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/aws-cdk-lib/node_modules/color-convert": { + "version": "2.0.1", + "inBundle": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/aws-cdk-lib/node_modules/color-name": { + "version": "1.1.4", + "inBundle": true, + "license": "MIT" + }, + "node_modules/aws-cdk-lib/node_modules/emoji-regex": { + "version": "8.0.0", + "inBundle": true, + "license": "MIT" + }, + "node_modules/aws-cdk-lib/node_modules/fast-deep-equal": { + "version": "3.1.3", + "inBundle": true, + "license": "MIT" + }, + "node_modules/aws-cdk-lib/node_modules/fast-uri": { + "version": "3.1.0", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "inBundle": true, + "license": "BSD-3-Clause" + }, + "node_modules/aws-cdk-lib/node_modules/fs-extra": { + "version": "11.3.3", + "inBundle": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" }, - "funding": { - "url": "https://opencollective.com/vitest" + "engines": { + "node": ">=14.14" } }, - "node_modules/@vitest/snapshot": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.1.1.tgz", - "integrity": "sha512-kMVSgcegWV2FibXEx9p9WIKgje58lcTbXgnJixfcg15iK8nzCXhmalL0ZLtTWLW9PH1+1NEDShiFFedB3tEgWg==", - "dev": true, + "node_modules/aws-cdk-lib/node_modules/graceful-fs": { + "version": "4.2.11", + "inBundle": true, + "license": "ISC" + }, + "node_modules/aws-cdk-lib/node_modules/ignore": { + "version": "5.3.2", + "inBundle": true, "license": "MIT", - "dependencies": { - "@vitest/pretty-format": "4.1.1", - "@vitest/utils": "4.1.1", - "magic-string": "^0.30.21", - "pathe": "^2.0.3" - }, - "funding": { - "url": "https://opencollective.com/vitest" + "engines": { + "node": ">= 4" } }, - "node_modules/@vitest/spy": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.1.tgz", - "integrity": "sha512-6Ti/KT5OVaiupdIZEuZN7l3CZcR0cxnxt70Z0//3CtwgObwA6jZhmVBA3yrXSVN3gmwjgd7oDNLlsXz526gpRA==", - "dev": true, + "node_modules/aws-cdk-lib/node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "inBundle": true, "license": "MIT", - "funding": { - "url": "https://opencollective.com/vitest" + "engines": { + "node": ">=8" } }, - "node_modules/@vitest/utils": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.1.tgz", - "integrity": "sha512-cNxAlaB3sHoCdL6pj6yyUXv9Gry1NHNg0kFTXdvSIZXLHsqKH7chiWOkwJ5s5+d/oMwcoG9T0bKU38JZWKusrQ==", - "dev": true, + "node_modules/aws-cdk-lib/node_modules/json-schema-traverse": { + "version": "1.0.0", + "inBundle": true, + "license": "MIT" + }, + "node_modules/aws-cdk-lib/node_modules/jsonfile": { + "version": "6.2.0", + "inBundle": true, "license": "MIT", "dependencies": { - "@vitest/pretty-format": "4.1.1", - "convert-source-map": "^2.0.0", - "tinyrainbow": "^3.0.3" + "universalify": "^2.0.0" }, - "funding": { - "url": "https://opencollective.com/vitest" + "optionalDependencies": { + "graceful-fs": "^4.1.6" } }, - "node_modules/abort-controller": { - "version": "3.0.0", - "dev": true, + "node_modules/aws-cdk-lib/node_modules/jsonschema": { + "version": "1.5.0", + "inBundle": true, "license": "MIT", - "dependencies": { - "event-target-shim": "^5.0.0" - }, "engines": { - "node": ">=6.5" + "node": "*" } }, - "node_modules/acorn": { - "version": "8.16.0", - "dev": true, + "node_modules/aws-cdk-lib/node_modules/lodash.truncate": { + "version": "4.4.2", + "inBundle": true, + "license": "MIT" + }, + "node_modules/aws-cdk-lib/node_modules/mime-db": { + "version": "1.52.0", + "inBundle": true, "license": "MIT", - "bin": { - "acorn": "bin/acorn" - }, "engines": { - "node": ">=0.4.0" + "node": ">= 0.6" } }, - "node_modules/acorn-jsx": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", - "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", - "dev": true, + "node_modules/aws-cdk-lib/node_modules/mime-types": { + "version": "2.1.35", + "inBundle": true, "license": "MIT", - "peerDependencies": { - "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" } }, - "node_modules/acorn-walk": { - "version": "8.3.4", - "dev": true, - "license": "MIT", + "node_modules/aws-cdk-lib/node_modules/minimatch": { + "version": "10.2.4", + "inBundle": true, + "license": "BlueOak-1.0.0", "dependencies": { - "acorn": "^8.11.0" + "brace-expansion": "^5.0.2" }, "engines": { - "node": ">=0.4.0" + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/agent-base": { - "version": "7.1.4", - "dev": true, + "node_modules/aws-cdk-lib/node_modules/punycode": { + "version": "2.3.1", + "inBundle": true, "license": "MIT", "engines": { - "node": ">= 14" + "node": ">=6" } }, - "node_modules/ajv": { - "version": "6.14.0", - "dev": true, + "node_modules/aws-cdk-lib/node_modules/require-from-string": { + "version": "2.0.2", + "inBundle": true, "license": "MIT", - "dependencies": { - "fast-deep-equal": "^3.1.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/aws-cdk-lib/node_modules/semver": { + "version": "7.7.4", + "inBundle": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" + "engines": { + "node": ">=10" } }, - "node_modules/ajv-formats": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-3.0.1.tgz", - "integrity": "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==", - "dev": true, + "node_modules/aws-cdk-lib/node_modules/slice-ansi": { + "version": "4.0.0", + "inBundle": true, "license": "MIT", "dependencies": { - "ajv": "^8.0.0" + "ansi-styles": "^4.0.0", + "astral-regex": "^2.0.0", + "is-fullwidth-code-point": "^3.0.0" }, - "peerDependencies": { - "ajv": "^8.0.0" + "engines": { + "node": ">=10" }, - "peerDependenciesMeta": { - "ajv": { - "optional": true - } + "funding": { + "url": "https://github.com/chalk/slice-ansi?sponsor=1" } }, - "node_modules/ajv-formats/node_modules/ajv": { - "version": "8.18.0", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz", - "integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==", - "dev": true, + "node_modules/aws-cdk-lib/node_modules/string-width": { + "version": "4.2.3", + "inBundle": true, "license": "MIT", "dependencies": { - "fast-deep-equal": "^3.1.3", - "fast-uri": "^3.0.1", - "json-schema-traverse": "^1.0.0", - "require-from-string": "^2.0.2" + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" + "engines": { + "node": ">=8" } }, - "node_modules/ajv-formats/node_modules/json-schema-traverse": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", - "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", - "dev": true, - "license": "MIT" - }, - "node_modules/ansi-styles": { - "version": "4.3.0", - "dev": true, + "node_modules/aws-cdk-lib/node_modules/strip-ansi": { + "version": "6.0.1", + "inBundle": true, "license": "MIT", "dependencies": { - "color-convert": "^2.0.1" + "ansi-regex": "^5.0.1" }, "engines": { "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, - "node_modules/apim-spec": { - "resolved": "packages/specification", - "link": true - }, - "node_modules/arg": { - "version": "4.1.3", - "dev": true, - "license": "MIT" - }, - "node_modules/argparse": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", - "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", - "dev": true, - "license": "Python-2.0" + "node_modules/aws-cdk-lib/node_modules/table": { + "version": "6.9.0", + "inBundle": true, + "license": "BSD-3-Clause", + "dependencies": { + "ajv": "^8.0.1", + "lodash.truncate": "^4.4.2", + "slice-ansi": "^4.0.0", + "string-width": "^4.2.3", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=10.0.0" + } }, - "node_modules/assertion-error": { + "node_modules/aws-cdk-lib/node_modules/universalify": { "version": "2.0.1", - "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", - "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", - "dev": true, + "inBundle": true, "license": "MIT", "engines": { - "node": ">=12" + "node": ">= 10.0.0" } }, - "node_modules/ast-v8-to-istanbul": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/ast-v8-to-istanbul/-/ast-v8-to-istanbul-1.0.0.tgz", - "integrity": "sha512-1fSfIwuDICFA4LKkCzRPO7F0hzFf0B7+Xqrl27ynQaa+Rh0e1Es0v6kWHPott3lU10AyAr7oKHa65OppjLn3Rg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/trace-mapping": "^0.3.31", - "estree-walker": "^3.0.3", - "js-tokens": "^10.0.0" + "node_modules/aws-cdk-lib/node_modules/yaml": { + "version": "1.10.3", + "inBundle": true, + "license": "ISC", + "engines": { + "node": ">= 6" } }, - "node_modules/ast-v8-to-istanbul/node_modules/js-tokens": { - "version": "10.0.0", - "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-10.0.0.tgz", - "integrity": "sha512-lM/UBzQmfJRo9ABXbPWemivdCW8V2G8FHaHdypQaIy523snUjog0W71ayWXTjiR+ixeMyVHN2XcpnTd/liPg/Q==", - "dev": true, - "license": "MIT" - }, - "node_modules/asynckit": { - "version": "0.4.0", - "license": "MIT" - }, "node_modules/axios": { "version": "1.13.6", "license": "MIT", + "peer": true, "dependencies": { "follow-redirects": "^1.15.11", "form-data": "^4.0.5", @@ -3779,7 +4808,9 @@ "license": "MIT" }, "node_modules/brace-expansion": { - "version": "2.0.2", + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.3.tgz", + "integrity": "sha512-MCV/fYJEbqx68aE58kv2cA/kiky1G8vux3OR6/jbS+jIMe/6fJWa0DTzJU7dqijOWYwHi1t29FlfYI9uytqlpA==", "dev": true, "license": "MIT", "dependencies": { @@ -3814,6 +4845,20 @@ "resolved": "packages/capabilityStatement", "link": true }, + "node_modules/cdk": { + "resolved": "packages/cdk", + "link": true + }, + "node_modules/cdk-nag": { + "version": "2.37.55", + "resolved": "https://registry.npmjs.org/cdk-nag/-/cdk-nag-2.37.55.tgz", + "integrity": "sha512-xcAkygwbph3pp7N0UEzJBmXUH/MIsluV7DYJSeZ/V3yCr0Y0QaRGO298WyD6mi4K+Rmnpl+EJoWUxcOblOqLKA==", + "license": "Apache-2.0", + "peerDependencies": { + "aws-cdk-lib": "^2.176.0", + "constructs": "^10.0.5" + } + }, "node_modules/chai": { "version": "6.2.2", "resolved": "https://registry.npmjs.org/chai/-/chai-6.2.2.tgz", @@ -3951,6 +4996,13 @@ "node": ">= 0.8" } }, + "node_modules/constructs": { + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/constructs/-/constructs-10.6.0.tgz", + "integrity": "sha512-TxHOnBO5zMo/G76ykzGF/wMpEHu257TbWiIxP9K0Yv/+t70UzgBQiTqjkAsWOPC6jW91DzJI0+ehQV6xDRNBuQ==", + "license": "Apache-2.0", + "peer": true + }, "node_modules/convert-source-map": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", @@ -4061,7 +5113,9 @@ } }, "node_modules/diff": { - "version": "4.0.2", + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.4.tgz", + "integrity": "sha512-X07nttJQkwkfKfvTPG/KSnE2OMdcUCao6+eXF3wmnIQRn2aPAHH3VxDbDOdegkd6JbPsXqShpvEOHfAT+nCNwQ==", "dev": true, "license": "BSD-3-Clause", "engines": { @@ -4069,7 +5123,9 @@ } }, "node_modules/dompurify": { - "version": "3.3.1", + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.3.3.tgz", + "integrity": "sha512-Oj6pzI2+RqBfFG+qOaOLbFXLQ90ARpcGG6UePL82bJLtdsa6CYJD7nmiU8MW9nQNOtCHV3lZ/Bzq1X0QYbBZCA==", "dev": true, "license": "(MPL-2.0 OR Apache-2.0)", "optionalDependencies": { @@ -4218,6 +5274,7 @@ "integrity": "sha512-S9jlY/ELKEUwwQnqWDO+f+m6sercqOPSqXM5Go94l7DOmxHVDgmSFGWEzeE/gwgTAr0W103BWt0QLe/7mabIvA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.2", @@ -4591,7 +5648,6 @@ "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", - "dev": true, "hasInstallScript": true, "license": "MIT", "optional": true, @@ -4652,7 +5708,6 @@ }, "node_modules/get-tsconfig": { "version": "4.13.0", - "dev": true, "license": "MIT", "dependencies": { "resolve-pkg-maps": "^1.0.0" @@ -4698,7 +5753,9 @@ } }, "node_modules/handlebars": { - "version": "4.7.8", + "version": "4.7.9", + "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.9.tgz", + "integrity": "sha512-4E71E0rpOaQuJR2A3xDZ+GM1HyWYv1clR58tC8emQNeQe3RH7MAzSbat+V0wG78LQBo6m6bzSG/L4pBuCsgnUQ==", "dev": true, "license": "MIT", "dependencies": { @@ -5416,9 +6473,9 @@ } }, "node_modules/minimatch/node_modules/brace-expansion": { - "version": "5.0.4", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.4.tgz", - "integrity": "sha512-h+DEnpVvxmfVefa4jFbCf5HdH5YMDXRsmKflpf1pILZWRFlTbJpxeU55nJl4Smt5HQaGzg1o6RHFPJaOqnmBDg==", + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.5.tgz", + "integrity": "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==", "dev": true, "license": "MIT", "dependencies": { @@ -5448,6 +6505,7 @@ "version": "6.15.0", "dev": true, "license": "MIT", + "peer": true, "funding": { "type": "opencollective", "url": "https://opencollective.com/mobx" @@ -5948,6 +7006,7 @@ "version": "19.2.1", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=0.10.0" } @@ -5956,6 +7015,7 @@ "version": "19.2.1", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "scheduler": "^0.27.0" }, @@ -6089,7 +7149,6 @@ }, "node_modules/resolve-pkg-maps": { "version": "1.0.0", - "dev": true, "license": "MIT", "funding": { "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" @@ -6344,6 +7403,7 @@ "version": "6.3.9", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@emotion/is-prop-valid": "1.4.0", "@emotion/unitless": "0.10.0", @@ -6543,7 +7603,6 @@ }, "node_modules/tsx": { "version": "4.21.0", - "dev": true, "license": "MIT", "dependencies": { "esbuild": "~0.27.0", @@ -6572,8 +7631,11 @@ }, "node_modules/typescript": { "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "dev": true, "license": "Apache-2.0", + "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -6658,6 +7720,7 @@ "integrity": "sha512-yF+o4POL41rpAzj5KVILUxm1GCjKnELvaqmU9TLLUbMfDzuN0UpUR9uaDs+mCtjPe+uYPksXDRLQGGPvj1cTmA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@vitest/expect": "4.1.1", "@vitest/mocker": "4.1.1", @@ -6796,6 +7859,7 @@ "integrity": "sha512-B9ifbFudT1TFhfltfaIPgjo9Z3mDynBTJSUYxTjOQruf/zHH+ezCQKcoqO+h7a9Pw9Nm/OtlXAiGT1axBgwqrQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "lightningcss": "^1.32.0", "picomatch": "^4.0.4", @@ -7062,6 +8126,17 @@ "@pfp-common/testing": "^1.0.0" } }, + "packages/cdk": { + "version": "0.1.0", + "dependencies": { + "@nhsdigital/eps-cdk-constructs": "file:../../nhsdigital-eps-cdk-constructs-1.6.0.tgz", + "aws-cdk": "^2.1106.0", + "aws-cdk-lib": "^2.239.0", + "cdk-nag": "^2.37.55", + "constructs": "^10.4.5", + "tsx": "^4.21.0" + } + }, "packages/common/testing": { "name": "@pfp-common/testing", "version": "1.0.0", diff --git a/package.json b/package.json index 00e678561..6f4f472e8 100644 --- a/package.json +++ b/package.json @@ -13,6 +13,7 @@ "author": "NHS Digital", "license": "MIT", "workspaces": [ + "packages/cdk", "packages/capabilityStatement", "packages/getMyPrescriptions", "packages/enrichPrescriptions", @@ -39,6 +40,7 @@ "vitest": "^4.1.0" }, "dependencies": { + "@nhsdigital/eps-cdk-constructs": "file:nhsdigital-eps-cdk-constructs-1.6.0.tgz", "esbuild": "^0.27.4" } } diff --git a/packages/cdk/bin/PfPApiApp.ts b/packages/cdk/bin/PfPApiApp.ts new file mode 100644 index 000000000..38b84359d --- /dev/null +++ b/packages/cdk/bin/PfPApiApp.ts @@ -0,0 +1,48 @@ +import { + CDK_ENV_PREFIX, + calculateVersionedStackName, + createApp, + getBooleanConfigFromEnvVar, + getConfigFromEnvVar, + getNumberConfigFromEnvVar +} from "@nhsdigital/eps-cdk-constructs" +import {PfPApiStack} from "../stacks/PfPApiStack" + +function main() { + const {app, props} = createApp({ + productName: "Prescriptions for Patients API", + appName: "PfPApiApp", + repoName: "prescriptionsforpatients", + driftDetectionGroup: "pfp-api" + }) + + const pfpApiStack = new PfPApiStack(app, "PfPApiStack", { + ...props, + stackName: calculateVersionedStackName(getConfigFromEnvVar("stackName"), props), + logRetentionInDays: getNumberConfigFromEnvVar("logRetentionInDays"), + logLevel: getConfigFromEnvVar("logLevel"), + serviceSearchApiKeySecretName: getConfigFromEnvVar("serviceSearchApiKeySecretName", + CDK_ENV_PREFIX, "pfp-PfP-ServiceSearch-API-Key"), + targetSpineServer: getConfigFromEnvVar("targetSpineServer"), + targetServiceSearchServer: getConfigFromEnvVar("targetServiceSearchServer"), + toggleGetStatusUpdates: getConfigFromEnvVar("toggleGetStatusUpdates"), + allowNhsNumberOverride: getConfigFromEnvVar("allowNhsNumberOverride"), + tc007NhsNumberValue: getConfigFromEnvVar("tc007NhsNumberValue", CDK_ENV_PREFIX, "not_in_use"), + tc008NhsNumberValue: getConfigFromEnvVar("tc008NhsNumberValue", CDK_ENV_PREFIX, "not_in_use"), + tc009NhsNumberValue: getConfigFromEnvVar("tc009NhsNumberValue", CDK_ENV_PREFIX, "not_in_use"), + enableAlerts: getBooleanConfigFromEnvVar("enableAlerts", CDK_ENV_PREFIX, "true"), + mutualTlsTrustStoreKey: props.isPullRequest ? undefined : getConfigFromEnvVar("trustStoreFile"), + // CSOC API GW log destination - do not change + csocApiGatewayDestination: "arn:aws:logs:eu-west-2:693466633220:destination:api_gateway_log_destination", + forwardCsocLogs: getBooleanConfigFromEnvVar("forwardCsocLogs") + }) + + return pfpApiStack +} + +try { + main() +} catch (error) { + console.error(error) + process.exit(1) +} diff --git a/packages/cdk/bin/PfPApiSandboxApp.ts b/packages/cdk/bin/PfPApiSandboxApp.ts new file mode 100644 index 000000000..d348e040c --- /dev/null +++ b/packages/cdk/bin/PfPApiSandboxApp.ts @@ -0,0 +1,35 @@ +import { + calculateVersionedStackName, + CDK_ENV_PREFIX, + createApp, + getConfigFromEnvVar, + getNumberConfigFromEnvVar +} from "@nhsdigital/eps-cdk-constructs" +import {PfPApiSandboxStack} from "../stacks/PfPApiSandboxStack" + +function main() { + const {app, props} = createApp({ + productName: "Prescriptions for Patients API", + appName: "PfPApiSandboxApp", + repoName: "prescriptionsforpatients", + driftDetectionGroup: "pfp-api" + }) + + new PfPApiSandboxStack(app, "PfPApiSandboxStack", { + ...props, + stackName: calculateVersionedStackName(getConfigFromEnvVar("stackName", CDK_ENV_PREFIX, "pfp-sandbox"), props), + targetSpineServer: getConfigFromEnvVar("targetSpineServer", CDK_ENV_PREFIX, "none"), + targetServiceSearchServer: getConfigFromEnvVar("targetServiceSearchServer", CDK_ENV_PREFIX, "none"), + logRetentionInDays: getNumberConfigFromEnvVar("logRetentionInDays", CDK_ENV_PREFIX, "30"), + logLevel: getConfigFromEnvVar("logLevel", CDK_ENV_PREFIX, "INFO"), + mutualTlsTrustStoreKey: props.isPullRequest ? + undefined : getConfigFromEnvVar("trustStoreFile", CDK_ENV_PREFIX, "none") + }) +} + +try { + main() +} catch (error) { + console.error(error) + process.exit(1) +} diff --git a/packages/cdk/cdk.json b/packages/cdk/cdk.json new file mode 100644 index 000000000..4ff6fefe1 --- /dev/null +++ b/packages/cdk/cdk.json @@ -0,0 +1,76 @@ +{ + "watch": { + "include": [ + "packages/**" + ], + "exclude": [ + "**/README.md", + "**/cdk*.json", + "**/*.d.ts", + "**/*.js", + "**/tsconfig.json", + "**/package*.json", + "**/yarn.lock", + "**/node_modules", + "**/tests*", + "**/lib", + "**/coverage", + "**/jest.config.ts", + "**/jest.debug.config.ts" + ] + }, + "context": { + "@aws-cdk/aws-lambda:recognizeLayerVersion": true, + "@aws-cdk/core:checkSecretUsage": true, + "@aws-cdk/core:target-partitions": [ + "aws", + "aws-cn" + ], + "@aws-cdk-containers/ecs-service-extensions:enableDefaultLogDriver": true, + "@aws-cdk/aws-ec2:uniqueImdsv2TemplateName": true, + "@aws-cdk/aws-ecs:arnFormatIncludesClusterName": true, + "@aws-cdk/aws-iam:minimizePolicies": true, + "@aws-cdk/core:validateSnapshotRemovalPolicy": true, + "@aws-cdk/aws-codepipeline:crossAccountKeyAliasStackSafeResourceName": true, + "@aws-cdk/aws-s3:createDefaultLoggingPolicy": true, + "@aws-cdk/aws-sns-subscriptions:restrictSqsDescryption": true, + "@aws-cdk/aws-apigateway:disableCloudWatchRole": true, + "@aws-cdk/core:enablePartitionLiterals": true, + "@aws-cdk/aws-events:eventsTargetQueueSameAccount": true, + "@aws-cdk/aws-ecs:disableExplicitDeploymentControllerForCircuitBreaker": true, + "@aws-cdk/aws-iam:importedRoleStackSafeDefaultPolicyName": true, + "@aws-cdk/aws-s3:serverAccessLogsUseBucketPolicy": true, + "@aws-cdk/aws-route53-patterns:useCertificate": true, + "@aws-cdk/customresources:installLatestAwsSdkDefault": false, + "@aws-cdk/aws-rds:databaseProxyUniqueResourceName": true, + "@aws-cdk/aws-codedeploy:removeAlarmsFromDeploymentGroup": true, + "@aws-cdk/aws-apigateway:authorizerChangeDeploymentLogicalId": true, + "@aws-cdk/aws-ec2:launchTemplateDefaultUserData": true, + "@aws-cdk/aws-secretsmanager:useAttachedSecretResourcePolicyForSecretTargetAttachments": true, + "@aws-cdk/aws-redshift:columnId": true, + "@aws-cdk/aws-stepfunctions-tasks:enableEmrServicePolicyV2": true, + "@aws-cdk/aws-ec2:restrictDefaultSecurityGroup": true, + "@aws-cdk/aws-apigateway:requestValidatorUniqueId": true, + "@aws-cdk/aws-kms:aliasNameRef": true, + "@aws-cdk/aws-autoscaling:generateLaunchTemplateInsteadOfLaunchConfig": true, + "@aws-cdk/core:includePrefixInUniqueNameGeneration": true, + "@aws-cdk/aws-efs:denyAnonymousAccess": true, + "@aws-cdk/aws-opensearchservice:enableOpensearchMultiAzWithStandby": true, + "@aws-cdk/aws-lambda-nodejs:useLatestRuntimeVersion": true, + "@aws-cdk/aws-efs:mountTargetOrderInsensitiveLogicalId": true, + "@aws-cdk/aws-rds:auroraClusterChangeScopeOfInstanceParameterGroupWithEachParameters": true, + "@aws-cdk/aws-appsync:useArnForSourceApiAssociationIdentifier": true, + "@aws-cdk/aws-rds:preventRenderingDeprecatedCredentials": true, + "@aws-cdk/aws-codepipeline-actions:useNewDefaultBranchForCodeCommitSource": true, + "@aws-cdk/aws-cloudwatch-actions:changeLambdaPermissionLogicalIdForLambdaAction": true, + "@aws-cdk/aws-codepipeline:crossAccountKeysDefaultValueToFalse": true, + "@aws-cdk/aws-codepipeline:defaultPipelineTypeToV2": true, + "@aws-cdk/aws-kms:reduceCrossAccountRegionPolicyScope": true, + "@aws-cdk/aws-eks:nodegroupNameAttribute": true, + "@aws-cdk/aws-ec2:ebsDefaultGp3Volume": true, + "@aws-cdk/aws-ecs:removeDefaultDeploymentAlarm": true, + "@aws-cdk/custom-resources:logApiResponseDataPropertyTrueDefault": false, + "@aws-cdk/aws-s3:keepNotificationInImportedBucket": false, + "acknowledged-issue-numbers": [34892] + } +} diff --git a/packages/cdk/constructs/MetricAlarm.ts b/packages/cdk/constructs/MetricAlarm.ts new file mode 100644 index 000000000..dd44ddded --- /dev/null +++ b/packages/cdk/constructs/MetricAlarm.ts @@ -0,0 +1,70 @@ +import {Duration} from "aws-cdk-lib" +import {Construct} from "constructs" +import { + Alarm, + ComparisonOperator, + Metric, + TreatMissingData, + Unit +} from "aws-cdk-lib/aws-cloudwatch" +import {ITopic} from "aws-cdk-lib/aws-sns" + +type AlarmDefinition = { + name: string + metric: string + description: string + dimensions?: {[key: string]: string} + threshold?: number + comparisonOperator?: ComparisonOperator + unit?: Unit +} + +export interface MetricAlarmProps { + readonly stackName: string + readonly enableAlerts: boolean + readonly namespace: string + readonly alarmDefinition: AlarmDefinition + readonly slackAlertTopic: ITopic +} + +export class MetricAlarm extends Construct { + alarms: {[key: string]: Alarm} + + public constructor(scope: Construct, id: string, props: MetricAlarmProps){ + super(scope, id) + + const metricFunction = (metricName: string) => + new Metric({ + namespace: props.namespace, + metricName, + dimensionsMap: props.alarmDefinition.dimensions, + unit: props.alarmDefinition.unit ?? Unit.COUNT, + statistic: "Sum", + period: Duration.minutes(1) + }) + + const alarm = new Alarm(this, `${props.alarmDefinition.name}Alarm`, { + alarmName: `${props.stackName}-${props.alarmDefinition.name}`, + metric: metricFunction(props.alarmDefinition.metric), + threshold: props.alarmDefinition.threshold ?? 1, + evaluationPeriods: 1, + comparisonOperator: + props.alarmDefinition.comparisonOperator ?? ComparisonOperator.GREATER_THAN_OR_EQUAL_TO_THRESHOLD, + treatMissingData: TreatMissingData.NOT_BREACHING, + alarmDescription: props.alarmDefinition.description, + actionsEnabled: props.enableAlerts + }) + + alarm.addAlarmAction({ + bind: () => ({alarmActionArn: props.slackAlertTopic.topicArn}) + }) + alarm.addOkAction({ + bind: () => ({alarmActionArn: props.slackAlertTopic.topicArn}) + }) + alarm.addInsufficientDataAction({ + bind: () => ({alarmActionArn: props.slackAlertTopic.topicArn}) + }) + + this.alarms = {[props.alarmDefinition.name]: alarm} + } +} diff --git a/packages/cdk/nagSuppressions.ts b/packages/cdk/nagSuppressions.ts new file mode 100644 index 000000000..b49660edc --- /dev/null +++ b/packages/cdk/nagSuppressions.ts @@ -0,0 +1,51 @@ +/* eslint-disable max-len */ +import {Stack} from "aws-cdk-lib" +import {safeAddNagSuppressionGroup, safeAddNagSuppression} from "@nhsdigital/eps-cdk-constructs" + +export const nagSuppressions = (stack: Stack) => { + safeAddNagSuppressionGroup( + stack, + [ + "/PfPApiStack/Functions/GetMyPrescriptionsLambda/LambdaPutLogsManagedPolicy/Resource", + "/PfPApiStack/Functions/EnrichPrescriptionsLambda/LambdaPutLogsManagedPolicy/Resource", + "/PfPApiStack/Functions/StatusLambda/LambdaPutLogsManagedPolicy/Resource", + "/PfPApiStack/StateMachines/GetMyPrescriptionsStateMachine/StateMachinePutLogsManagedPolicy/Resource" + ], + [ + { + id: "AwsSolutions-IAM5", + reason: "Suppress error for not having wildcards in permissions. This is a fine as we need to have permissions on all log streams under path" + } + ] + ) + + safeAddNagSuppression( + stack, + "/PfPApiStack/Apis/ApiGateway/ApiGateway/Resource", + [ + { + id: "AwsSolutions-APIG2", + reason: "Suppress error for request validation not being enabled. Validation will be handled by the service logic." + } + ] + ) + + safeAddNagSuppressionGroup( + stack, + [ + "/PfPApiStack/Apis/ApiGateway/ApiGateway/Default/Bundle/GET/Resource", + "/PfPApiStack/Apis/ApiGateway/ApiGateway/Default/_status/GET/Resource" + ], + [ + { + id: "AwsSolutions-APIG4", + reason: "Suppress error for not implementing authorization. Token endpoint should not have an authorizer" + }, + { + id: "AwsSolutions-COG4", + reason: "Suppress error for not implementing a Cognito user pool authorizer. Token endpoint should not have an authorizer" + } + ] + ) + +} diff --git a/packages/cdk/package.json b/packages/cdk/package.json new file mode 100644 index 000000000..35fd4d867 --- /dev/null +++ b/packages/cdk/package.json @@ -0,0 +1,23 @@ +{ + "name": "cdk", + "version": "0.1.0", + "scripts": { + "cdk-synth": "cdk synth --output ../../cdk.out --quiet --app \"npm run tsx -- bin/${CDK_APP_NAME}.ts\"", + "cdk-diff": "cdk diff --app \"npm run tsx -- bin/${CDK_APP_NAME}.ts\"", + "cdk-deploy": "cdk deploy --app \"npm run tsx -- bin/${CDK_APP_NAME}.ts\" --all --ci true --require-approval ${REQUIRE_APPROVAL}", + "cdk-watch": "cdk deploy --app \"npm run tsx -- bin/${CDK_APP_NAME}.ts\" --watch --all --ci true --require-approval ${REQUIRE_APPROVAL}", + "delete-main-stacks": "npm run tsx -- scripts/deleteMainStacks.ts", + "delete-old-pr-stacks": "npm run tsx -- scripts/deletePrStacks.ts", + "lint": "eslint --max-warnings 0 --fix --config ../../eslint.config.mjs .", + "test": "vitest run --coverage", + "tsx": "tsx" + }, + "dependencies": { + "@nhsdigital/eps-cdk-constructs": "file:../../nhsdigital-eps-cdk-constructs-1.6.0.tgz", + "aws-cdk": "^2.1106.0", + "aws-cdk-lib": "^2.239.0", + "cdk-nag": "^2.37.55", + "constructs": "^10.4.5", + "tsx": "^4.21.0" + } +} diff --git a/packages/cdk/resources/Alarms.ts b/packages/cdk/resources/Alarms.ts new file mode 100644 index 000000000..5e08b6781 --- /dev/null +++ b/packages/cdk/resources/Alarms.ts @@ -0,0 +1,144 @@ +import {Fn} from "aws-cdk-lib" +import {Unit} from "aws-cdk-lib/aws-cloudwatch" +import { + MetricFilter, + FilterPattern, + IFilterPattern, + ILogGroup +} from "aws-cdk-lib/aws-logs" +import {Topic} from "aws-cdk-lib/aws-sns" +import {TypescriptLambdaFunction} from "@nhsdigital/eps-cdk-constructs" +import {Construct} from "constructs" +import {MetricAlarm} from "../constructs/MetricAlarm" + +export interface AlarmsProps { + readonly stackName: string + readonly enableAlerts: boolean + readonly functions: {[key: string]: TypescriptLambdaFunction} +} + +export class Alarms extends Construct { + public constructor(scope: Construct, id: string, props: AlarmsProps) { + super(scope, id) + + const createMetricFilter = ( + metricFilterId: string, + metricFilterProps: { + filterName: string + filterPattern: IFilterPattern + logGroup: ILogGroup + metricNamespace: string + metricName?: string + metricValue?: string + unit?: Unit + dimensions?: {[key: string]: string} + } + ) => new MetricFilter(this, metricFilterId, { + ...metricFilterProps, + metricName: metricFilterProps.metricName ?? "ErrorCount", + metricValue: metricFilterProps.metricValue ?? "1", + unit: metricFilterProps.unit ?? Unit.COUNT + }) + + const slackAlertTopic = Topic.fromTopicArn( + this, + "SlackAlertsTopic", + Fn.importValue("lambda-resources:SlackAlertsSnsTopicArn") + ) + + const getMyPrescriptionsFunction = props.functions.getMyPrescriptions.function + const enrichPrescriptionsFunction = props.functions.enrichPrescriptions.function + + createMetricFilter("ServiceSearchErrorsLogsMetricFilter", { + filterName: "ServiceSearchErrors", + filterPattern: FilterPattern.literal( + `{ ($.level = "ERROR") && ($.function_name = "${getMyPrescriptionsFunction.functionName}") ` + + "&& $.message = %error in request to serviceSearch% }" + ), + logGroup: getMyPrescriptionsFunction.logGroup, + metricNamespace: "LambdaLogFilterMetrics", + metricName: "ServiceSearchErrorCount", + dimensions: { + FunctionName: "$.function_name" + } + }) + + new MetricAlarm(this, "ServiceSearchErrors", { + stackName: props.stackName, + enableAlerts: props.enableAlerts, + namespace: "LambdaLogFilterMetrics", + alarmDefinition: { + name: "ServiceSearch_Errors", + metric: "ServiceSearchErrorCount", + description: "Count of Service Search errors", + dimensions: { + FunctionName: getMyPrescriptionsFunction.functionName + } + }, + slackAlertTopic + }) + + new MetricAlarm(this, "ServiceSearchUnhandledErrors", { + stackName: props.stackName, + enableAlerts: props.enableAlerts, + namespace: "Lambda", + alarmDefinition: { + name: "ServiceSearch_UnhandledErrors", + metric: "Errors", + description: "Count of Service Search unhandled errors", + dimensions: { + FunctionName: getMyPrescriptionsFunction.functionName + } + }, + slackAlertTopic + }) + + createMetricFilter("GetMyPrescriptionsErrorsLogsMetricFilter", { + filterName: `${props.stackName}_GetMyPrescriptionsErrors`, + filterPattern: FilterPattern.literal( + `{ ($.level = "ERROR") && ($.function_name = "${getMyPrescriptionsFunction.functionName}") ` + + "&& ($.message != %error in request to serviceSearch%) }" + ), + logGroup: getMyPrescriptionsFunction.logGroup, + metricNamespace: "LambdaLogFilterMetrics", + dimensions: { + FunctionName: "$.function_name" + } + }) + + new MetricAlarm(this, "GetMyPrescriptionsErrors", { + stackName: props.stackName, + enableAlerts: props.enableAlerts, + namespace: "LambdaLogFilterMetrics", + alarmDefinition: { + name: "GetMyPrescriptions_Errors", + metric: "ErrorCount", + description: "Count of GetMyPrescriptions errors", + dimensions: { + FunctionName: getMyPrescriptionsFunction.functionName + } + }, + slackAlertTopic + }) + + createMetricFilter("EnrichPrescriptionsErrorsLogsMetricFilter", { + filterName: `${props.stackName}_EnrichPrescriptionsErrors`, + filterPattern: FilterPattern.literal("ERROR"), + logGroup: enrichPrescriptionsFunction.logGroup, + metricNamespace: "LambdaLogFilterMetrics", + metricName: `${props.stackName}EnrichPrescriptionsErrorCount` + }) + + new MetricAlarm(this, "EnrichPrescriptionsErrors", { + stackName: props.stackName, + enableAlerts: props.enableAlerts, + namespace: "LambdaLogFilterMetrics", + alarmDefinition: { + name: "EnrichPrescriptions_Errors", + metric: `${props.stackName}EnrichPrescriptionsErrorCount`, + description: "Count of EnrichPrescriptions errors" + }, + slackAlertTopic + }) + } +} diff --git a/packages/cdk/resources/Apis.ts b/packages/cdk/resources/Apis.ts new file mode 100644 index 000000000..b1ac0caac --- /dev/null +++ b/packages/cdk/resources/Apis.ts @@ -0,0 +1,65 @@ +import {HttpMethod} from "aws-cdk-lib/aws-lambda" +import { + ExpressStateMachine, + LambdaEndpoint, + RestApiGateway, + StateMachineEndpoint, + TypescriptLambdaFunction +} from "@nhsdigital/eps-cdk-constructs" +import {Construct} from "constructs" + +export interface ApisProps { + readonly stackName: string + readonly logRetentionInDays: number + readonly mutualTlsTrustStoreKey: string | undefined + functions: {[key: string]: TypescriptLambdaFunction} + stateMachines: {[key: string]: ExpressStateMachine} + readonly forwardCsocLogs: boolean + readonly csocApiGatewayDestination: string +} + +export class Apis extends Construct { + apis: {[key: string]: RestApiGateway} + endpoints: {[key: string]: Construct} + + public constructor(scope: Construct, id: string, props: ApisProps){ + super(scope, id) + + const apiGateway = new RestApiGateway(this, "ApiGateway", { + stackName: props.stackName, + logRetentionInDays: props.logRetentionInDays, + mutualTlsTrustStoreKey: props.mutualTlsTrustStoreKey, + forwardCsocLogs: props.forwardCsocLogs, + csocApiGatewayDestination: props.csocApiGatewayDestination, + executionPolicies: [ + props.stateMachines.getMyPrescriptions.executionPolicy, + props.functions.status.executionPolicy + ] + }) + const rootResource = apiGateway.api.root + + const getMyPrescriptionsEndpoint = new StateMachineEndpoint(this, "GetMyPrescriptionsEndpoint", { + parentResource: rootResource, + resourceName: "Bundle", + method: HttpMethod.GET, + restApiGatewayRole: apiGateway.role, + stateMachine: props.stateMachines.getMyPrescriptions + }) + + const statusEndpoint = new LambdaEndpoint(this, "StatusEndpoint", { + parentResource: rootResource, + resourceName: "_status", + method: HttpMethod.GET, + restApiGatewayRole: apiGateway.role, + lambdaFunction: props.functions.status + }) + + this.apis = { + api: apiGateway + } + this.endpoints = { + getMyPrescriptions: getMyPrescriptionsEndpoint, + status: statusEndpoint + } + } +} diff --git a/packages/cdk/resources/Functions.ts b/packages/cdk/resources/Functions.ts new file mode 100644 index 000000000..c88bf0818 --- /dev/null +++ b/packages/cdk/resources/Functions.ts @@ -0,0 +1,124 @@ +import {RemovalPolicy} from "aws-cdk-lib" +import {IManagedPolicy, ManagedPolicy} from "aws-cdk-lib/aws-iam" +import {Construct} from "constructs" +import {TypescriptLambdaFunction} from "@nhsdigital/eps-cdk-constructs" +import {ACCOUNT_RESOURCES} from "@nhsdigital/eps-cdk-constructs/lib/src/constants.js" +import {Code, LayerVersion} from "aws-cdk-lib/aws-lambda" +import {join, resolve} from "node:path" + +export interface FunctionsProps { + readonly stackName: string + readonly version: string + readonly commitId: string + readonly deploymentEnvironment: string + readonly targetSpineServer: string + readonly targetServiceSearchServer: string + readonly serviceSearchApiKeySecretName: string + readonly toggleGetStatusUpdates: string + readonly allowNhsNumberOverride: string + readonly getPfPParametersPolicy: IManagedPolicy + readonly logRetentionInDays: number + readonly logLevel: string +} + +const baseDir = resolve(__dirname, "../../..") + +export class Functions extends Construct { + functions: {[key: string]: TypescriptLambdaFunction} + + public constructor(scope: Construct, id: string, props: FunctionsProps){ + super(scope, id) + + // Imports + const lambdaAccessSecretsPolicy = ManagedPolicy.fromManagedPolicyArn( + this, "lambdaAccessSecretsPolicy", ACCOUNT_RESOURCES.LambdaAccessSecretsPolicy) + + const lambdaDecryptSecretsKMSPolicy = ManagedPolicy.fromManagedPolicyArn( + this, "lambdaDecryptSecretsKMSPolicy", ACCOUNT_RESOURCES.LambdaDecryptSecretsKMSPolicy) + + const lambdaDefaultEnvironmentVariables: {[key: string]: string} = { + STACK_NAME: props.stackName, + TargetSpineServer: props.targetSpineServer, + TargetServiceSearchServer: props.targetServiceSearchServer, + SpinePrivateKeyARN: ACCOUNT_RESOURCES.SpinePrivateKeyARN, + SpinePublicCertificateARN: ACCOUNT_RESOURCES.SpinePublicCertificateARN, + SpineASIDARN: ACCOUNT_RESOURCES.SpineASIDARN, + SpinePartyKeyARN: ACCOUNT_RESOURCES.SpinePartyKeyARN, + SpineCAChainARN: ACCOUNT_RESOURCES.SpineCAChainARN, + ServiceSearch3ApiKeyName: props.serviceSearchApiKeySecretName + } + + const getSecretsLambdaLayer = new LayerVersion(this, "GetSecretsLambdaLayer", { + description: "get secrets layer", + code: Code.fromAsset(join(baseDir, "packages/getSecretLayer/lib/get-secrets-layer.zip")), + removalPolicy: RemovalPolicy.RETAIN + }) + + // Resources + const getMyPrescriptionsLambda = new TypescriptLambdaFunction(this, "GetMyPrescriptionsLambda", { + functionName: `${props.stackName}-GetMyPrescriptions`, + projectBaseDir: baseDir, + packageBasePath: "packages/getMyPrescriptions", + entryPoint: "src/getMyPrescriptions.ts", + environmentVariables: { + ...lambdaDefaultEnvironmentVariables, + AWS_LAMBDA_EXEC_WRAPPER: "/opt/get-secrets-layer", + DEPLOYMENT_ENVIRONMENT: props.deploymentEnvironment, + GET_STATUS_UPDATES: props.toggleGetStatusUpdates, + ALLOW_NHS_NUMBER_OVERRIDE: props.allowNhsNumberOverride + }, + layers: [getSecretsLambdaLayer], + additionalPolicies: [ + lambdaAccessSecretsPolicy, + lambdaDecryptSecretsKMSPolicy, + props.getPfPParametersPolicy + ], + logRetentionInDays: props.logRetentionInDays, + logLevel: props.logLevel, + version: props.version, + commitId: props.commitId + }) + + const enrichPrescriptionsLambda = new TypescriptLambdaFunction(this, "EnrichPrescriptionsLambda", { + functionName: `${props.stackName}-EnrichPrescriptions`, + projectBaseDir: baseDir, + packageBasePath: "packages/enrichPrescriptions", + entryPoint: "src/enrichPrescriptions.ts", + environmentVariables: { + ...lambdaDefaultEnvironmentVariables, + DEPLOYMENT_ENVIRONMENT: props.deploymentEnvironment, + EXPECT_STATUS_UPDATES: props.toggleGetStatusUpdates + }, + additionalPolicies: [ + props.getPfPParametersPolicy + ], + logRetentionInDays: props.logRetentionInDays, + logLevel: props.logLevel, + version: props.version, + commitId: props.commitId + }) + + const statusLambda = new TypescriptLambdaFunction(this, "StatusLambda", { + functionName: `${props.stackName}-status`, + projectBaseDir: baseDir, + packageBasePath: "packages/statusLambda", + entryPoint: "src/statusLambda.ts", + environmentVariables: { + ...lambdaDefaultEnvironmentVariables, + AWS_LAMBDA_EXEC_WRAPPER: "/opt/get-secrets-layer" + }, + layers: [getSecretsLambdaLayer], + additionalPolicies: [lambdaAccessSecretsPolicy, lambdaDecryptSecretsKMSPolicy], + logRetentionInDays: props.logRetentionInDays, + logLevel: props.logLevel, + version: props.version, + commitId: props.commitId + }) + + this.functions = { + getMyPrescriptions: getMyPrescriptionsLambda, + enrichPrescriptions: enrichPrescriptionsLambda, + status: statusLambda + } + } +} diff --git a/packages/cdk/resources/Parameters.ts b/packages/cdk/resources/Parameters.ts new file mode 100644 index 000000000..24d01271f --- /dev/null +++ b/packages/cdk/resources/Parameters.ts @@ -0,0 +1,53 @@ +import {ManagedPolicy} from "aws-cdk-lib/aws-iam" +import {Construct} from "constructs" +import { + SsmParameterDefinition, + SsmParametersConstruct, + SsmParametersConstructProps +} from "@nhsdigital/eps-cdk-constructs" + +export interface ParametersProps { + readonly stackName: string + readonly tc007NhsNumberValue: string + readonly tc008NhsNumberValue: string + readonly tc009NhsNumberValue: string +} + +export default class Parameters extends Construct { + public readonly readParametersPolicy: ManagedPolicy + + public constructor(scope: Construct, id: string, props: ParametersProps) { + super(scope, id) + + const parameterDefinitions: Array = [ + { + id: "TC007NHSNumber", + nameSuffix: "TC007NHSNumber", + description: "List of NHS numbers that will trigger 'temporarily unavailable' response for testing purposes.", + value: props.tc007NhsNumberValue + }, + { + id: "TC008NHSNumber", + nameSuffix: "TC008NHSNumber", + description: "List of NHS numbers that will trigger '500 system error' response for testing purposes.", + value: props.tc008NhsNumberValue + }, + { + id: "TC009NHSNumber", + nameSuffix: "TC009NHSNumber", + description: + "List of NHS numbers that will trigger 'one or more prescriptions missing' response for testing purposes.", + value: props.tc009NhsNumberValue + } + ] + + const ssmParametersProps: SsmParametersConstructProps = { + namePrefix: props.stackName, + parameters: parameterDefinitions, + readPolicyDescription: "Allows reading SSM parameters" + } + + const ssmParameters = new SsmParametersConstruct(this, "PfPApiSsmParameters", ssmParametersProps) + this.readParametersPolicy = ssmParameters.readParametersPolicy + } +} diff --git a/packages/cdk/resources/SandboxApis.ts b/packages/cdk/resources/SandboxApis.ts new file mode 100644 index 000000000..55331eba0 --- /dev/null +++ b/packages/cdk/resources/SandboxApis.ts @@ -0,0 +1,72 @@ +import {HttpMethod} from "aws-cdk-lib/aws-lambda" +import { + LambdaEndpoint, + RestApiGateway, + TypescriptLambdaFunction +} from "@nhsdigital/eps-cdk-constructs" +import {Construct} from "constructs" + +export interface SandboxApisProps { + readonly stackName: string + readonly logRetentionInDays: number + readonly mutualTlsTrustStoreKey: string | undefined + readonly csocApiGatewayDestination: string + readonly forwardCsocLogs: boolean + functions: {[key: string]: TypescriptLambdaFunction} +} + +export class SandboxApis extends Construct { + apis: {[key: string]: RestApiGateway} + endpoints: {[key: string]: Construct} + + public constructor(scope: Construct, id: string, props: SandboxApisProps){ + super(scope, id) + + const apiGateway = new RestApiGateway(this, "ApiGateway", { + stackName: props.stackName, + logRetentionInDays: props.logRetentionInDays, + mutualTlsTrustStoreKey: props.mutualTlsTrustStoreKey, + forwardCsocLogs: props.forwardCsocLogs, + csocApiGatewayDestination: props.csocApiGatewayDestination, + executionPolicies: [ + props.functions.sandbox.executionPolicy, + props.functions.capabilityStatement.executionPolicy, + props.functions.status.executionPolicy + ] + }) + const rootResource = apiGateway.api.root + + const bundleEndpoint = new LambdaEndpoint(this, "SandboxBundleEndpoint", { + parentResource: rootResource, + resourceName: "Bundle", + method: HttpMethod.GET, + restApiGatewayRole: apiGateway.role, + lambdaFunction: props.functions.sandbox + }) + + const capabilityStatementEndpoint = new LambdaEndpoint(this, "CapabilityStatementEndpoint", { + parentResource: rootResource, + resourceName: "metadata", + method: HttpMethod.GET, + restApiGatewayRole: apiGateway.role, + lambdaFunction: props.functions.capabilityStatement + }) + + const statusEndpoint = new LambdaEndpoint(this, "StatusEndpoint", { + parentResource: rootResource, + resourceName: "_status", + method: HttpMethod.GET, + restApiGatewayRole: apiGateway.role, + lambdaFunction: props.functions.status + }) + + this.apis = { + api: apiGateway + } + this.endpoints = { + sandbox: bundleEndpoint, + capabilityStatement: capabilityStatementEndpoint, + status: statusEndpoint + } + } +} diff --git a/packages/cdk/resources/SandboxFunctions.ts b/packages/cdk/resources/SandboxFunctions.ts new file mode 100644 index 000000000..26c5a955f --- /dev/null +++ b/packages/cdk/resources/SandboxFunctions.ts @@ -0,0 +1,80 @@ +import {TypescriptLambdaFunction} from "@nhsdigital/eps-cdk-constructs" +import {Construct} from "constructs" +import {resolve} from "node:path" + +export interface SandboxFunctionsProps { + readonly stackName: string + readonly version: string + readonly commitId: string + readonly targetSpineServer: string + readonly targetServiceSearchServer: string + readonly logRetentionInDays: number + readonly logLevel: string +} + +const baseDir = resolve(__dirname, "../../..") + +export class SandboxFunctions extends Construct { + functions: {[key: string]: TypescriptLambdaFunction} + + public constructor(scope: Construct, id: string, props: SandboxFunctionsProps) { + super(scope, id) + + const lambdaDefaultEnvironmentVariables: {[key: string]: string} = { + STACK_NAME: props.stackName, + TargetSpineServer: props.targetSpineServer, + TargetServiceSearchServer: props.targetServiceSearchServer + } + + const sandboxLambda = new TypescriptLambdaFunction(this, "SandboxLambda", { + functionName: `${props.stackName}-Sandbox`, + projectBaseDir: baseDir, + packageBasePath: "packages/nhsd-pfp-sandbox", + entryPoint: "src/sandbox.ts", + environmentVariables: { + ...lambdaDefaultEnvironmentVariables + }, + timeoutInSeconds: 30, + logRetentionInDays: props.logRetentionInDays, + logLevel: props.logLevel, + version: props.version, + commitId: props.commitId + }) + + const capabilityStatementLambda = new TypescriptLambdaFunction(this, "CapabilityStatementLambda", { + functionName: `${props.stackName}-CapabilityStatement`, + projectBaseDir: baseDir, + packageBasePath: "packages/capabilityStatement", + entryPoint: "src/capabilityStatement.ts", + environmentVariables: { + ...lambdaDefaultEnvironmentVariables + }, + timeoutInSeconds: 30, + logRetentionInDays: props.logRetentionInDays, + logLevel: props.logLevel, + version: props.version, + commitId: props.commitId + }) + + const statusLambda = new TypescriptLambdaFunction(this, "StatusLambda", { + functionName: `${props.stackName}-status`, + projectBaseDir: baseDir, + packageBasePath: "packages/statusLambda", + entryPoint: "src/statusLambda.ts", + environmentVariables: { + ...lambdaDefaultEnvironmentVariables + }, + timeoutInSeconds: 30, + logRetentionInDays: props.logRetentionInDays, + logLevel: props.logLevel, + version: props.version, + commitId: props.commitId + }) + + this.functions = { + sandbox: sandboxLambda, + capabilityStatement: capabilityStatementLambda, + status: statusLambda + } + } +} diff --git a/packages/cdk/resources/StateMachineDefinitions/GetMyPrescriptions.ts b/packages/cdk/resources/StateMachineDefinitions/GetMyPrescriptions.ts new file mode 100644 index 000000000..a05953239 --- /dev/null +++ b/packages/cdk/resources/StateMachineDefinitions/GetMyPrescriptions.ts @@ -0,0 +1,70 @@ +import {IFunction} from "aws-cdk-lib/aws-lambda" +import { + Chain, + Choice, + Condition, + IChainable, + Pass, + TaskInput +} from "aws-cdk-lib/aws-stepfunctions" +import {LambdaInvoke} from "aws-cdk-lib/aws-stepfunctions-tasks" +import {CatchAllErrorPass} from "@nhsdigital/eps-cdk-constructs" +import {Construct} from "constructs" + +export interface DefinitionProps { + readonly getMyPrescriptionsFunction: IFunction + readonly enrichPrescriptionsFunction: IFunction + readonly getStatusUpdatesFunction: IFunction +} + +export class GetMyPrescriptions extends Construct { + public readonly definition: IChainable + + public constructor(scope: Construct, id: string, props: DefinitionProps){ + super(scope, id) + + const catchAllError = new CatchAllErrorPass(this, "Catch All Error") + + const getMyPrescriptions = LambdaInvoke.jsonata(this, "Get My Prescriptions", { + lambdaFunction: props.getMyPrescriptionsFunction, + payload: TaskInput.fromText("{% $states.input %}") + }) + getMyPrescriptions.addCatch(catchAllError.state) + + const failedGetMyPrescriptions = new Pass(this, "Failed Get My Prescriptions") + + const parseGetMyPrescriptionsBody = Pass.jsonata(this, "Parse Get My Prescriptions Body", { + outputs: "{% $parse($states.input.Payload.body) %}" + }) + + const enrichPrescriptions = LambdaInvoke.jsonata(this, "Enrich Prescriptions", { + lambdaFunction: props.enrichPrescriptionsFunction, + payload: TaskInput.fromText("{% $states.input %}") + }) + enrichPrescriptions.addCatch(catchAllError.state) + + const getStatusUpdates = LambdaInvoke.jsonata(this, "Get Status Updates", { + lambdaFunction: props.getStatusUpdatesFunction, + payload: TaskInput.fromText("{% $states.input.statusUpdateData %}"), + outputs: "{% $merge([$states.input, {'StatusUpdates': {'Payload': $states.result.Payload}}]) %}" + }) + getStatusUpdates.addCatch(enrichPrescriptions, { + outputs: "{% $merge([$states.input, {'error': $states.errorOutput}]) %}" + }) + + const checkGetMyPrescriptionsResult = Choice.jsonata(this, "Get My Prescriptions Result") + const evaluateToggleGetStatusUpdates = Choice.jsonata(this, "Evaluate Toggle Get Status Updates Parameter") + + this.definition = Chain + .start(getMyPrescriptions) + .next(checkGetMyPrescriptionsResult + .when(Condition.jsonata("{% $states.input.Payload.statusCode != 200 %}"), failedGetMyPrescriptions) + .otherwise(parseGetMyPrescriptionsBody + .next(evaluateToggleGetStatusUpdates + .when( + Condition.jsonata("{% $states.input.getStatusUpdates = true %}"), + getStatusUpdates.next(enrichPrescriptions) + ) + .otherwise(enrichPrescriptions)))) + } +} diff --git a/packages/cdk/resources/StateMachines.ts b/packages/cdk/resources/StateMachines.ts new file mode 100644 index 000000000..d7a6a18e6 --- /dev/null +++ b/packages/cdk/resources/StateMachines.ts @@ -0,0 +1,58 @@ +import {Fn} from "aws-cdk-lib" +import {ManagedPolicy, PolicyStatement} from "aws-cdk-lib/aws-iam" +import {Function} from "aws-cdk-lib/aws-lambda" +import {ExpressStateMachine, TypescriptLambdaFunction} from "@nhsdigital/eps-cdk-constructs" +import {Construct} from "constructs" +import {GetMyPrescriptions} from "./StateMachineDefinitions/GetMyPrescriptions" + +export interface StateMachinesProps { + readonly stackName: string + readonly logRetentionInDays: number + functions: {[key: string]: TypescriptLambdaFunction} +} + +export class StateMachines extends Construct { + stateMachines: {[key: string]: ExpressStateMachine} + + public constructor(scope: Construct, id: string, props: StateMachinesProps){ + super(scope, id) + + // Imports + const getStatusUpdates = Function.fromFunctionArn( + this, "GetStatusUpdates", `${Fn.importValue("psu:functions:GetStatusUpdates:FunctionArn")}:$LATEST`) + const callGetStatusUpdatesManagedPolicy = new ManagedPolicy(this, "CallGetStatusUpdatesManagedPolicy", { + description: "call get status updates lambda from get my prescriptions state machine", + statements: [ + new PolicyStatement({ + actions: [ + "lambda:InvokeFunction" + ], + resources: [ + getStatusUpdates.functionArn + ] + }) + ] + }) + + const getMyPrescriptions = new GetMyPrescriptions(this, "GetMyPrescriptionsStateMachineDefinition", { + getMyPrescriptionsFunction: props.functions.getMyPrescriptions.function, + enrichPrescriptionsFunction: props.functions.enrichPrescriptions.function, + getStatusUpdatesFunction: getStatusUpdates + }) + const getMyPrescriptionsStateMachine = new ExpressStateMachine(this, "GetMyPrescriptionsStateMachine", { + stackName: props.stackName, + stateMachineName: `${props.stackName}-GetMyPrescriptions`, + definition: getMyPrescriptions.definition, + logRetentionInDays: props.logRetentionInDays, + additionalPolicies: [ + props.functions.getMyPrescriptions.executionPolicy, + props.functions.enrichPrescriptions.executionPolicy, + callGetStatusUpdatesManagedPolicy + ] + }) + + this.stateMachines = { + getMyPrescriptions: getMyPrescriptionsStateMachine + } + } +} diff --git a/packages/cdk/scripts/deleteMainStacks.ts b/packages/cdk/scripts/deleteMainStacks.ts new file mode 100644 index 000000000..dfb3b28a8 --- /dev/null +++ b/packages/cdk/scripts/deleteMainStacks.ts @@ -0,0 +1,11 @@ +import {deleteUnusedMainStacks, getActiveApiVersions, getConfigFromEnvVar} from "@nhsdigital/eps-cdk-constructs" + +const awsEnvironment = getConfigFromEnvVar("AWS_ENVIRONMENT", "") +deleteUnusedMainStacks( + "pfp-api", + () => getActiveApiVersions("prescriptions-for-patients"), + `${awsEnvironment}.eps.national.nhs.uk.` +).catch((error) => { + console.error(error) + process.exit(1) +}) diff --git a/packages/cdk/scripts/deletePrStacks.ts b/packages/cdk/scripts/deletePrStacks.ts new file mode 100644 index 000000000..39977cbda --- /dev/null +++ b/packages/cdk/scripts/deletePrStacks.ts @@ -0,0 +1,10 @@ +import {deleteUnusedPrStacks} from "@nhsdigital/eps-cdk-constructs" + +deleteUnusedPrStacks( + "pfp-api", + "prescriptionsforpatients", + "dev.eps.national.nhs.uk." +).catch((error) => { + console.error(error) + process.exit(1) +}) diff --git a/packages/cdk/stacks/PfPApiSandboxStack.ts b/packages/cdk/stacks/PfPApiSandboxStack.ts new file mode 100644 index 000000000..87135905f --- /dev/null +++ b/packages/cdk/stacks/PfPApiSandboxStack.ts @@ -0,0 +1,42 @@ +import {Stack, App} from "aws-cdk-lib" +import {StandardStackProps} from "@nhsdigital/eps-cdk-constructs" +import {SandboxFunctions} from "../resources/SandboxFunctions" +import {SandboxApis} from "../resources/SandboxApis" +import {nagSuppressions} from "../nagSuppressions" + +export interface PfPApiSandboxStackProps extends StandardStackProps { + readonly stackName: string + readonly targetSpineServer: string + readonly targetServiceSearchServer: string + readonly logRetentionInDays: number + readonly logLevel: string + readonly mutualTlsTrustStoreKey: string | undefined +} + +export class PfPApiSandboxStack extends Stack { + public constructor(scope: App, id: string, props: PfPApiSandboxStackProps){ + super(scope, id, props) + + const functions = new SandboxFunctions(this, "Functions", { + stackName: props.stackName, + version: props.version, + commitId: props.commitId, + targetSpineServer: props.targetSpineServer, + targetServiceSearchServer: props.targetServiceSearchServer, + logRetentionInDays: props.logRetentionInDays, + logLevel: props.logLevel + }) + + new SandboxApis(this, "Apis", { + stackName: props.stackName, + logRetentionInDays: props.logRetentionInDays, + mutualTlsTrustStoreKey: props.mutualTlsTrustStoreKey, + // CSOC API GW log destination - do not change + csocApiGatewayDestination: "arn:aws:logs:eu-west-2:693466633220:destination:api_gateway_log_destination", + forwardCsocLogs: false, + functions: functions.functions + }) + + nagSuppressions(this) + } +} diff --git a/packages/cdk/stacks/PfPApiStack.ts b/packages/cdk/stacks/PfPApiStack.ts new file mode 100644 index 000000000..f0da1651d --- /dev/null +++ b/packages/cdk/stacks/PfPApiStack.ts @@ -0,0 +1,79 @@ +import {App, Stack} from "aws-cdk-lib" +import {nagSuppressions} from "../nagSuppressions" +import {Functions} from "../resources/Functions" +import {StateMachines} from "../resources/StateMachines" +import {Apis} from "../resources/Apis" +import {Alarms} from "../resources/Alarms" +import {StandardStackProps} from "@nhsdigital/eps-cdk-constructs" +import Parameters from "../resources/Parameters" + +export interface PfPApiStackProps extends StandardStackProps { + readonly stackName: string + readonly logRetentionInDays: number + readonly logLevel: string + readonly targetSpineServer: string + readonly targetServiceSearchServer: string + readonly serviceSearchApiKeySecretName: string + readonly toggleGetStatusUpdates: string + readonly allowNhsNumberOverride: string + readonly tc007NhsNumberValue: string + readonly tc008NhsNumberValue: string + readonly tc009NhsNumberValue: string + readonly mutualTlsTrustStoreKey: string | undefined + readonly enableAlerts: boolean + readonly csocApiGatewayDestination: string + readonly forwardCsocLogs: boolean +} + +export class PfPApiStack extends Stack { + public constructor(scope: App, id: string, props: PfPApiStackProps){ + super(scope, id, props) + + // Resources + const parameters = new Parameters(this, "SsmParameters", { + stackName: props.stackName, + tc007NhsNumberValue: props.tc007NhsNumberValue, + tc008NhsNumberValue: props.tc008NhsNumberValue, + tc009NhsNumberValue: props.tc009NhsNumberValue + }) + + const functions = new Functions(this, "Functions", { + stackName: props.stackName, + version: props.version, + commitId: props.commitId, + deploymentEnvironment: props.environment, + targetSpineServer: props.targetSpineServer, + targetServiceSearchServer: props.targetServiceSearchServer, + toggleGetStatusUpdates: props.toggleGetStatusUpdates, + serviceSearchApiKeySecretName: props.serviceSearchApiKeySecretName, + allowNhsNumberOverride: props.allowNhsNumberOverride, + getPfPParametersPolicy: parameters.readParametersPolicy, + logRetentionInDays: props.logRetentionInDays, + logLevel: props.logLevel + }) + + const stateMachines = new StateMachines(this, "StateMachines", { + stackName: props.stackName, + logRetentionInDays: props.logRetentionInDays, + functions: functions.functions + }) + + new Alarms(this, "Alarms", { + stackName: props.stackName, + enableAlerts: props.enableAlerts, + functions: functions.functions + }) + + new Apis(this, "Apis", { + stackName: props.stackName, + logRetentionInDays: props.logRetentionInDays, + mutualTlsTrustStoreKey: props.mutualTlsTrustStoreKey, + functions: functions.functions, + stateMachines: stateMachines.stateMachines, + csocApiGatewayDestination: props.csocApiGatewayDestination, + forwardCsocLogs: props.forwardCsocLogs + }) + + nagSuppressions(this) + } +} diff --git a/packages/cdk/tests/MetricAlarm.test.ts b/packages/cdk/tests/MetricAlarm.test.ts new file mode 100644 index 000000000..4550ccd25 --- /dev/null +++ b/packages/cdk/tests/MetricAlarm.test.ts @@ -0,0 +1,88 @@ +import {App, Stack} from "aws-cdk-lib" +import {Template} from "aws-cdk-lib/assertions" +import {ComparisonOperator, Unit} from "aws-cdk-lib/aws-cloudwatch" +import {Topic} from "aws-cdk-lib/aws-sns" +import {describe, expect, it} from "vitest" +import {MetricAlarm} from "../constructs/MetricAlarm" + +describe("MetricAlarm construct", () => { + it("applies sane defaults for simple alarm definitions", () => { + const app = new App() + const stack = new Stack(app, "TestStack") + const slackAlertTopic = new Topic(stack, "SlackAlertsTopic") + + new MetricAlarm(stack, "SimpleMetricAlarm", { + stackName: "pfp-test-stack", + enableAlerts: true, + namespace: "LambdaLogFilterMetrics", + alarmDefinition: { + name: "MySimpleAlarm", + metric: "ErrorCount", + description: "Simple alarm" + }, + slackAlertTopic + }) + + const template = Template.fromStack(stack) + + template.hasResourceProperties("AWS::CloudWatch::Alarm", { + AlarmName: "pfp-test-stack-MySimpleAlarm", + Namespace: "LambdaLogFilterMetrics", + MetricName: "ErrorCount", + Threshold: 1, + ComparisonOperator: "GreaterThanOrEqualToThreshold", + Unit: "Count", + Statistic: "Sum", + Period: 60, + EvaluationPeriods: 1, + TreatMissingData: "notBreaching", + AlarmDescription: "Simple alarm", + ActionsEnabled: true + }) + }) + + it("allows overriding threshold, comparison operator, unit and dimensions", () => { + const app = new App() + const stack = new Stack(app, "OverrideStack") + const slackAlertTopic = new Topic(stack, "SlackAlertsTopic") + + const metricAlarm = new MetricAlarm(stack, "OverrideMetricAlarm", { + stackName: "pfp-test-stack", + enableAlerts: false, + namespace: "CustomNamespace", + alarmDefinition: { + name: "MyOverrideAlarm", + metric: "Latency", + description: "Override alarm", + dimensions: { + FunctionName: "my-function" + }, + threshold: 250, + comparisonOperator: ComparisonOperator.GREATER_THAN_THRESHOLD, + unit: Unit.MILLISECONDS + }, + slackAlertTopic + }) + + expect(metricAlarm.alarms.MyOverrideAlarm).toBeDefined() + + const template = Template.fromStack(stack) + + template.hasResourceProperties("AWS::CloudWatch::Alarm", { + AlarmName: "pfp-test-stack-MyOverrideAlarm", + Namespace: "CustomNamespace", + MetricName: "Latency", + Threshold: 250, + ComparisonOperator: "GreaterThanThreshold", + Unit: "Milliseconds", + Dimensions: [ + { + Name: "FunctionName", + Value: "my-function" + } + ], + AlarmDescription: "Override alarm", + ActionsEnabled: false + }) + }) +}) diff --git a/packages/cdk/tests/synth.test.ts b/packages/cdk/tests/synth.test.ts new file mode 100644 index 000000000..5585182bc --- /dev/null +++ b/packages/cdk/tests/synth.test.ts @@ -0,0 +1,62 @@ +import {execFileSync} from "node:child_process" +import {resolve} from "node:path" +import {describe, expect, it} from "vitest" + +const cdkPackageRoot = resolve(__dirname, "..") + +function createBaseEnv(): NodeJS.ProcessEnv { + return { + ...process.env, + CI: "true", + CDK_DEFAULT_REGION: "eu-west-2", + CDK_CONFIG_versionNumber: "0.0.0-test", + CDK_CONFIG_commitId: "test-commit", + CDK_CONFIG_isPullRequest: "true", + CDK_CONFIG_environment: "test" + } +} + +describe("CDK synth smoke tests", () => { + it("type-checks the cdk package", () => { + expect(() => { + execFileSync("npx", ["tsc", "-p", "tsconfig.json", "--noEmit"], { + cwd: cdkPackageRoot, + stdio: "pipe" + }) + }).not.toThrow() + }, 30000) + + it("synthesizes the sandbox app", () => { + expect(() => { + execFileSync("npx", ["tsx", "bin/PfPApiSandboxApp.ts"], { + cwd: cdkPackageRoot, + stdio: "pipe", + env: createBaseEnv() + }) + }).not.toThrow() + }, 30000) + + it("synthesizes the main app", () => { + expect(() => { + execFileSync("npx", ["tsx", "bin/PfPApiApp.ts"], { + cwd: cdkPackageRoot, + stdio: "pipe", + env: { + ...createBaseEnv(), + CDK_CONFIG_stackName: "pfp-test-stack", + CDK_CONFIG_logRetentionInDays: "7", + CDK_CONFIG_logLevel: "INFO", + CDK_CONFIG_targetSpineServer: "https://example-spine.test", + CDK_CONFIG_targetServiceSearchServer: "https://live/service-search-api/", + CDK_CONFIG_toggleGetStatusUpdates: "true", + CDK_CONFIG_allowNhsNumberOverride: "false", + CDK_CONFIG_tc007NhsNumberValue: "9000000009", + CDK_CONFIG_tc008NhsNumberValue: "9000000017", + CDK_CONFIG_tc009NhsNumberValue: "9000000025", + CDK_CONFIG_enableAlerts: "true", + CDK_CONFIG_forwardCsocLogs: "false" + } + }) + }).not.toThrow() + }, 30000) +}) diff --git a/packages/cdk/tsconfig.json b/packages/cdk/tsconfig.json new file mode 100644 index 000000000..2b73af77d --- /dev/null +++ b/packages/cdk/tsconfig.json @@ -0,0 +1,29 @@ +{ + "extends": "../../tsconfig.defaults.json", + "compilerOptions": { + "module": "commonjs", + "rootDir": ".", + "outDir": "lib", + "noEmit": true, + "strict": false, + "lib": [ + "es2020" + ], + "noImplicitAny": true, + "strictNullChecks": true, + "noImplicitThis": true, + "alwaysStrict": true, + "noUnusedLocals": false, + "noUnusedParameters": false, + "noImplicitReturns": true, + "noFallthroughCasesInSwitch": false, + "inlineSources": true, + "experimentalDecorators": true, + "strictPropertyInitialization": false, + "typeRoots": [ + "../../node_modules/@types" + ] + }, + "include": ["resources/**/*", "constructs/**/*", "policies/**/*", "stacks/**/*", "tests/**/*", "scripts/**/*", "nagSuppressions.ts"], + "exclude": ["node_modules", "cdk.out"] +} diff --git a/packages/cdk/vitest.config.ts b/packages/cdk/vitest.config.ts new file mode 100644 index 000000000..739bbad20 --- /dev/null +++ b/packages/cdk/vitest.config.ts @@ -0,0 +1,5 @@ +import {createVitestConfig} from "../../vitest.default.config" + +export default createVitestConfig({ + workspaceRoot: "../../" +}) diff --git a/packages/common/utilities/src/config.ts b/packages/common/utilities/src/config.ts index 68daa4c83..168479e79 100644 --- a/packages/common/utilities/src/config.ts +++ b/packages/common/utilities/src/config.ts @@ -1,4 +1,5 @@ import {SSMProvider} from "@aws-lambda-powertools/parameters/ssm" +import {Logger} from "@aws-lambda-powertools/logger" const defaultSsmProvider = new SSMProvider({ clientConfig: {region: process.env.AWS_REGION || "eu-west-2"} @@ -9,40 +10,41 @@ export class PfPConfig { static readonly TC008_NHS_NUMBERS_PARAM = "TC008NHSNumber" static readonly TC009_NHS_NUMBERS_PARAM = "TC009NHSNumber" - private ssmProvider: SSMProvider + private readonly ssmProvider: SSMProvider + private readonly logger: Logger constructor(ssmProvider?: SSMProvider) { + this.logger = new Logger() this.ssmProvider = ssmProvider || defaultSsmProvider } - async isTC007(nhsNumber: string) { - // TC007: test case functionality for supplier testing + async isTestCase(nhsNumber: string, param: string) { const env = process.env["DEPLOYMENT_ENVIRONMENT"] - if (env === "prod") return false - const stackName = process.env.STACK_NAME || "pfp" - const TC007_NHS_NUMBERS = await this.ssmProvider.get(`/${stackName}-${PfPConfig.TC007_NHS_NUMBERS_PARAM}`) - return TC007_NHS_NUMBERS ? TC007_NHS_NUMBERS.includes(nhsNumber) : false + + try { + const stackName = process.env.STACK_NAME || "pfp" + const paramValue = await this.ssmProvider.get(`/${stackName}-${param}`) + return paramValue ? paramValue.includes(nhsNumber) : false + } catch (error) { + this.logger.warn(`Cannot read parameter ${param}, continue with test case disabled:`, {error}) + return false + } + } + + async isTC007(nhsNumber: string) { + // TC007: test case functionality for supplier testing + return this.isTestCase(nhsNumber, PfPConfig.TC007_NHS_NUMBERS_PARAM) } async isTC008(nhsNumber: string) { // AEA-5653, AEA-5853 | TC008: force internal error response for supplier testing - const env = process.env["DEPLOYMENT_ENVIRONMENT"] - - if (env === "prod") return false - const stackName = process.env.STACK_NAME || "pfp" - const TC008_NHS_NUMBERS = await this.ssmProvider.get(`/${stackName}-${PfPConfig.TC008_NHS_NUMBERS_PARAM}`) - return TC008_NHS_NUMBERS ? TC008_NHS_NUMBERS.includes(nhsNumber) : false + return this.isTestCase(nhsNumber, PfPConfig.TC008_NHS_NUMBERS_PARAM) } async isTC009(nhsNumber: string) { // TC009: test case functionality for supplier testing - const env = process.env["DEPLOYMENT_ENVIRONMENT"] - - if (env === "prod") return false - const stackName = process.env.STACK_NAME || "pfp" - const TC009_NHS_NUMBERS = await this.ssmProvider.get(`/${stackName}-${PfPConfig.TC009_NHS_NUMBERS_PARAM}`) - return TC009_NHS_NUMBERS ? TC009_NHS_NUMBERS.includes(nhsNumber) : false + return this.isTestCase(nhsNumber, PfPConfig.TC009_NHS_NUMBERS_PARAM) } } diff --git a/packages/serviceSearchClient/src/live-serviceSearch-client.ts b/packages/serviceSearchClient/src/live-serviceSearch-client.ts index f3c2f7103..ffd4e63b2 100644 --- a/packages/serviceSearchClient/src/live-serviceSearch-client.ts +++ b/packages/serviceSearchClient/src/live-serviceSearch-client.ts @@ -133,14 +133,14 @@ export class LiveServiceSearchClient implements ServiceSearchClient { private async loadApiKeyFromSecretsManager(): Promise { try { - const secretArn = process.env.ServiceSearch3ApiKeyARN - if (!secretArn) { - this.logger.error("ServiceSearch3ApiKeyARN environment variable is not set") + const secretId = process.env.ServiceSearch3ApiKeyName ?? process.env.ServiceSearch3ApiKeyARN + if (!secretId) { + this.logger.error("ServiceSearch3ApiKeyName or ServiceSearch3ApiKeyARN environment variable is not set") return undefined } - this.logger.info("Loading ServiceSearch API key from Secrets Manager", {secretArn}) + this.logger.info("Loading ServiceSearch API key from Secrets Manager", {secretId}) - const secret = await getSecret(secretArn, { + const secret = await getSecret(secretId, { maxAge: 300 // Cache for 5 minutes }) diff --git a/packages/serviceSearchClient/tests/live-serviceSearch-client.test.ts b/packages/serviceSearchClient/tests/live-serviceSearch-client.test.ts index df47fc7a5..34a71c444 100644 --- a/packages/serviceSearchClient/tests/live-serviceSearch-client.test.ts +++ b/packages/serviceSearchClient/tests/live-serviceSearch-client.test.ts @@ -60,6 +60,7 @@ describe("live serviceSearch client", () => { beforeEach(() => { process.env.TargetServiceSearchServer = "live" process.env.ServiceSearch3ApiKey = "test-key" + delete process.env.ServiceSearch3ApiKeyName delete process.env.ServiceSearch3ApiKeyARN logger = new Logger({serviceName: "svcClientTest"}) @@ -68,8 +69,9 @@ describe("live serviceSearch client", () => { vi.restoreAllMocks() }) - test("logs error when ServiceSearch3ApiKeyARN is missing", async () => { + test("logs error when ServiceSearch3ApiKeyName and ServiceSearch3ApiKeyARN are missing", async () => { delete process.env.ServiceSearch3ApiKey + delete process.env.ServiceSearch3ApiKeyName delete process.env.ServiceSearch3ApiKeyARN client = new LiveServiceSearchClient(logger) @@ -90,7 +92,7 @@ describe("live serviceSearch client", () => { await client.searchService("ABC123", dummyCorrelationId) expect(errorSpy).toHaveBeenCalledWith( - "ServiceSearch3ApiKeyARN environment variable is not set" + "ServiceSearch3ApiKeyName or ServiceSearch3ApiKeyARN environment variable is not set" ) }) diff --git a/template.env b/template.env new file mode 100644 index 000000000..96b9c5c56 --- /dev/null +++ b/template.env @@ -0,0 +1,5 @@ +export AWS_DEFAULT_PROFILE=Admin-591291862413 +export PATH="$PWD/node_modules/.bin:$PATH" +export stack_name= +export TARGET_SPINE_SERVER=msg.veit07.devspineservices.nhs.uk +export TARGET_SERVICE_SEARCH_SERVER=int.api.service.nhs.uk