From 5ce6ef1f6a3bd006a0300aeae82882eb7300017f Mon Sep 17 00:00:00 2001 From: simonlabarere Date: Fri, 1 May 2026 10:26:12 +0100 Subject: [PATCH 1/8] CCM-17440: Simple dependency updates --- docs/Gemfile.lock | 4 +- package-lock.json | 78 ++++++++++++++++++++++------------- tests/playwright/package.json | 2 +- 3 files changed, 52 insertions(+), 32 deletions(-) diff --git a/docs/Gemfile.lock b/docs/Gemfile.lock index a0b0e6048..3c1c92ec2 100644 --- a/docs/Gemfile.lock +++ b/docs/Gemfile.lock @@ -19,7 +19,7 @@ GEM ast (2.4.3) base64 (0.3.0) bigdecimal (4.1.2) - cgi (0.5.0) + cgi (0.5.1) colorator (1.1.0) concurrent-ruby (1.3.6) connection_pool (3.0.2) @@ -28,7 +28,7 @@ GEM em-websocket (0.5.3) eventmachine (>= 0.12.9) http_parser.rb (~> 0) - erb (4.0.4) + erb (4.0.4.1) cgi (>= 0.3.3) eventmachine (1.2.7) ffi (1.17.2-aarch64-linux-gnu) diff --git a/package-lock.json b/package-lock.json index e08f8d92d..596c4cde9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -6985,13 +6985,14 @@ } }, "node_modules/@aws-sdk/xml-builder": { - "version": "3.972.15", - "resolved": "https://registry.npmjs.org/@aws-sdk/xml-builder/-/xml-builder-3.972.15.tgz", - "integrity": "sha512-PxMRlCFNiQnke9YR29vjFQwz4jq+6Q04rOVFeTDR2K7Qpv9h9FOWOxG+zJjageimYbWqE3bTuLjmryWHAWbvaA==", + "version": "3.972.22", + "resolved": "https://registry.npmjs.org/@aws-sdk/xml-builder/-/xml-builder-3.972.22.tgz", + "integrity": "sha512-PMYKKtJd70IsSG0yHrdAbxBr+ZWBKLvzFZfD3/urxgf6hXVMzuU5M+3MJ5G67RpOmLBu1fAUN65SbWuKUCOlAA==", "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^4.13.1", - "fast-xml-parser": "5.5.8", + "@nodable/entities": "2.1.0", + "@smithy/types": "^4.14.1", + "fast-xml-parser": "5.7.2", "tslib": "^2.6.2" }, "engines": { @@ -9857,6 +9858,18 @@ "zod": "^4.1.11" } }, + "node_modules/@nodable/entities": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@nodable/entities/-/entities-2.1.0.tgz", + "integrity": "sha512-nyT7T3nbMyBI/lvr6L5TyWbFJAI9FTgVRakNoBqCD+PmID8DzFrrNdLLtHMwMszOtqZa8PAOV24ZqDnQrhQINA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/nodable" + } + ], + "license": "MIT" + }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "license": "MIT", @@ -10994,9 +11007,9 @@ } }, "node_modules/@smithy/types": { - "version": "4.13.1", - "resolved": "https://registry.npmjs.org/@smithy/types/-/types-4.13.1.tgz", - "integrity": "sha512-787F3yzE2UiJIQ+wYW1CVg2odHjmaWLGksnKQHUrK/lYZSEcy1msuLVvxaR/sI2/aDe9U+TBuLsXnr3vod1g0g==", + "version": "4.14.1", + "resolved": "https://registry.npmjs.org/@smithy/types/-/types-4.14.1.tgz", + "integrity": "sha512-59b5HtSVrVR/eYNei3BUj3DCPKD/G7EtDDe7OEJE7i7FtQFugYo6MxbotS8mVJkLNVf8gYaAlEBwwtJ9HzhWSg==", "license": "Apache-2.0", "dependencies": { "tslib": "^2.6.2" @@ -16241,9 +16254,9 @@ "license": "BSD-3-Clause" }, "node_modules/fast-xml-builder": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/fast-xml-builder/-/fast-xml-builder-1.1.4.tgz", - "integrity": "sha512-f2jhpN4Eccy0/Uz9csxh3Nu6q4ErKxf0XIsasomfOihuSUa3/xw6w8dnOtCDgEItQFJG8KyXPzQXzcODDrrbOg==", + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/fast-xml-builder/-/fast-xml-builder-1.1.5.tgz", + "integrity": "sha512-4TJn/8FKLeslLAH3dnohXqE3QSoxkhvaMzepOIZytwJXZO69Bfz0HBdDHzOTOon6G59Zrk6VQ2bEiv1t61rfkA==", "funding": [ { "type": "github", @@ -16256,9 +16269,9 @@ } }, "node_modules/fast-xml-parser": { - "version": "5.5.8", - "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-5.5.8.tgz", - "integrity": "sha512-Z7Fh2nVQSb2d+poDViM063ix2ZGt9jmY1nWhPfHBOK2Hgnb/OW3P4Et3P/81SEej0J7QbWtJqxO05h8QYfK7LQ==", + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-5.7.2.tgz", + "integrity": "sha512-P7oW7tLbYnhOLQk/Gv7cZgzgMPP/XN03K02/Jy6Y/NHzyIAIpxuZIM/YqAkfiXFPxA2CTm7NtCijK9EDu09u2w==", "funding": [ { "type": "github", @@ -16267,9 +16280,10 @@ ], "license": "MIT", "dependencies": { - "fast-xml-builder": "^1.1.4", - "path-expression-matcher": "^1.2.0", - "strnum": "^2.2.0" + "@nodable/entities": "^2.1.0", + "fast-xml-builder": "^1.1.5", + "path-expression-matcher": "^1.5.0", + "strnum": "^2.2.3" }, "bin": { "fxparser": "src/cli/cli.js" @@ -21630,9 +21644,9 @@ } }, "node_modules/mermaid/node_modules/uuid": { - "version": "11.1.0", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-11.1.0.tgz", - "integrity": "sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==", + "version": "11.1.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-11.1.1.tgz", + "integrity": "sha512-vIYxrBCC/N/K+Js3qSN88go7kIfNPssr/hHCesKCQNAjmgvYS2oqr69kIufEG+O4+PfezOH4EbIeHCfFov8ZgQ==", "funding": [ "https://github.com/sponsors/broofa", "https://github.com/sponsors/ctavan" @@ -22464,9 +22478,9 @@ } }, "node_modules/path-expression-matcher": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/path-expression-matcher/-/path-expression-matcher-1.2.0.tgz", - "integrity": "sha512-DwmPWeFn+tq7TiyJ2CxezCAirXjFxvaiD03npak3cRjlP9+OjTmSy1EpIrEbh+l6JgUundniloMLDQ/6VTdhLQ==", + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/path-expression-matcher/-/path-expression-matcher-1.5.0.tgz", + "integrity": "sha512-cbrerZV+6rvdQrrD+iGMcZFEiiSrbv9Tfdkvnusy6y0x0GKBXREFg/Y65GhIfm0tnLntThhzCnfKwp1WRjeCyQ==", "funding": [ { "type": "github", @@ -24447,9 +24461,9 @@ } }, "node_modules/strnum": { - "version": "2.2.2", - "resolved": "https://registry.npmjs.org/strnum/-/strnum-2.2.2.tgz", - "integrity": "sha512-DnR90I+jtXNSTXWdwrEy9FakW7UX+qUZg28gj5fk2vxxl7uS/3bpI4fjFYVmdK9etptYBPNkpahuQnEwhwECqA==", + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/strnum/-/strnum-2.2.3.tgz", + "integrity": "sha512-oKx6RUCuHfT3oyVjtnrmn19H1SiCqgJSg+54XqURKp5aCMbrXrhLjRN9TjuwMjiYstZ0MzDrHqkGZ5dFTKd+zg==", "funding": [ { "type": "github", @@ -25807,10 +25821,16 @@ "link": true }, "node_modules/uuid": { - "version": "8.3.2", + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-14.0.0.tgz", + "integrity": "sha512-Qo+uWgilfSmAhXCMav1uYFynlQO7fMFiMVZsQqZRMIXp0O7rR7qjkj+cPvBHLgBqi960QCoo/PH2/6ZtVqKvrg==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], "license": "MIT", "bin": { - "uuid": "dist/bin/uuid" + "uuid": "dist-node/bin/uuid" } }, "node_modules/v8-compile-cache-lib": { @@ -27915,7 +27935,7 @@ "digital-letters-events": "^0.0.1", "sender-management": "^0.0.1", "utils": "^0.0.1", - "uuid": "^8.3.2" + "uuid": "^14.0.0" }, "devDependencies": { "@types/uuid": "^10.0.0" diff --git a/tests/playwright/package.json b/tests/playwright/package.json index 7d34a9b8f..d8838f818 100644 --- a/tests/playwright/package.json +++ b/tests/playwright/package.json @@ -16,7 +16,7 @@ "digital-letters-events": "^0.0.1", "sender-management": "^0.0.1", "utils": "^0.0.1", - "uuid": "^8.3.2" + "uuid": "^14.0.0" }, "devDependencies": { "@types/uuid": "^10.0.0" From 77d8accebb53c57475bf8db1915e678216735e75 Mon Sep 17 00:00:00 2001 From: simonlabarere Date: Fri, 1 May 2026 10:35:15 +0100 Subject: [PATCH 2/8] CCM-17440: Update mermaid dependencies + override uuid to 14.0.0 --- package-lock.json | 53 ++++++++++++++++++----------------------------- package.json | 3 +++ 2 files changed, 23 insertions(+), 33 deletions(-) diff --git a/package-lock.json b/package-lock.json index 596c4cde9..b51afda59 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8304,14 +8304,14 @@ "license": "MIT" }, "node_modules/@iconify/utils": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/@iconify/utils/-/utils-3.1.0.tgz", - "integrity": "sha512-Zlzem1ZXhI1iHeeERabLNzBHdOa4VhQbqAcOQaMKuTuyZCpwKbC2R4Dd0Zo3g9EAc+Y4fiarO8HIHRAth7+skw==", + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@iconify/utils/-/utils-3.1.1.tgz", + "integrity": "sha512-MwzoDtw9rO1x+qfgLTV/IVXsHDBqeYZoMIQC8SfxfYSlaSUG+oWiAcoiB1yajAda6mqblm4/1/w2E8tRu7a7Tw==", "license": "MIT", "dependencies": { "@antfu/install-pkg": "^1.1.0", "@iconify/types": "^2.0.0", - "mlly": "^1.8.0" + "mlly": "^1.8.2" } }, "node_modules/@isaacs/cliui": { @@ -13418,12 +13418,12 @@ } }, "node_modules/chevrotain-allstar": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/chevrotain-allstar/-/chevrotain-allstar-0.4.1.tgz", - "integrity": "sha512-PvVJm3oGqrveUVW2Vt/eZGeiAIsJszYweUcYwcskg9e+IubNYKKD+rHHem7A6XVO22eDAL+inxNIGAzZ/VIWlA==", + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/chevrotain-allstar/-/chevrotain-allstar-0.4.3.tgz", + "integrity": "sha512-2X4mkroolSMKqW+H22pyPMUVDqYZzPhephTmg/NODKb1IGYPHfxfhcW0EjS7wcPJNbze2i4vBWT7zT5FKF2lrQ==", "license": "MIT", "dependencies": { - "lodash-es": "^4.17.21" + "lodash-es": "^4.18.1" }, "peerDependencies": { "chevrotain": "^12.0.0" @@ -13934,9 +13934,9 @@ "license": "MIT" }, "node_modules/cytoscape": { - "version": "3.33.2", - "resolved": "https://registry.npmjs.org/cytoscape/-/cytoscape-3.33.2.tgz", - "integrity": "sha512-sj4HXd3DokGhzZAdjDejGvTPLqlt84vNFN8m7bGsOzDY5DyVcxIb2ejIXat2Iy7HxWhdT/N1oKyheJ5YdpsGuw==", + "version": "3.33.3", + "resolved": "https://registry.npmjs.org/cytoscape/-/cytoscape-3.33.3.tgz", + "integrity": "sha512-Gej7U+OKR+LZ8kvX7rb2HhCYJ0IhvEFsnkud4SB1PR+BUY/TsSO0dmOW59WEVLu51b1Rm+gQRKoz4bLYxGSZ2g==", "license": "MIT", "peer": true, "engines": { @@ -14729,9 +14729,9 @@ } }, "node_modules/dompurify": { - "version": "3.4.1", - "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.4.1.tgz", - "integrity": "sha512-JahakDAIg1gyOm7dlgWSDjV4n7Ip2PKR55NIT6jrMfIgLFgWo81vdr1/QGqWtFNRqXP9UV71oVePtjqS2ebnPw==", + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.4.2.tgz", + "integrity": "sha512-lHeS9SA/IKeIFFyYciHBr2n0v1VMPlSj843HdLOwjb2OxNwdq9Xykxqhk+FE42MzAdHvInbAolSE4mhahPpjXA==", "license": "(MPL-2.0 OR Apache-2.0)", "optionalDependencies": { "@types/trusted-types": "^2.0.7" @@ -21643,19 +21643,6 @@ "uuid": "^11.1.0" } }, - "node_modules/mermaid/node_modules/uuid": { - "version": "11.1.1", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-11.1.1.tgz", - "integrity": "sha512-vIYxrBCC/N/K+Js3qSN88go7kIfNPssr/hHCesKCQNAjmgvYS2oqr69kIufEG+O4+PfezOH4EbIeHCfFov8ZgQ==", - "funding": [ - "https://github.com/sponsors/broofa", - "https://github.com/sponsors/ctavan" - ], - "license": "MIT", - "bin": { - "uuid": "dist/esm/bin/uuid" - } - }, "node_modules/micromatch": { "version": "4.0.8", "license": "MIT", @@ -24739,9 +24726,9 @@ } }, "node_modules/tinyexec": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.1.1.tgz", - "integrity": "sha512-VKS/ZaQhhkKFMANmAOhhXVoIfBXblQxGX1myCQ2faQrfmobMftXeJPcZGp0gS07ocvGJWDLZGyOZDadDBqYIJg==", + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.1.2.tgz", + "integrity": "sha512-dAqSqE/RabpBKI8+h26GfLq6Vb3JVXs30XYQjdMjaj/c2tS8IYYMbIzP599KtRj7c57/wYApb3QjgRgXmrCukA==", "license": "MIT", "engines": { "node": ">=18" @@ -25686,9 +25673,9 @@ "link": true }, "node_modules/ufo": { - "version": "1.6.3", - "resolved": "https://registry.npmjs.org/ufo/-/ufo-1.6.3.tgz", - "integrity": "sha512-yDJTmhydvl5lJzBmy/hyOAA0d+aqCBuwl818haVdYCRrWV84o7YyeVm4QlVHStqNrrJSTb6jKuFAVqAFsr+K3Q==", + "version": "1.6.4", + "resolved": "https://registry.npmjs.org/ufo/-/ufo-1.6.4.tgz", + "integrity": "sha512-JFNbkD1Svwe0KvGi8GOeLcP4kAWQ609twvCdcHxq1oSL8svv39ZuSvajcD8B+5D0eL4+s1Is2D/O6KN3qcTeRA==", "license": "MIT" }, "node_modules/uglify-js": { diff --git a/package.json b/package.json index bc63990bd..9a2b25239 100644 --- a/package.json +++ b/package.json @@ -39,6 +39,9 @@ "overrides": { "brace-expansion": "^5.0.2", "fast-xml-parser": "^5.3.7", + "mermaid": { + "uuid": "^14.0.0" + }, "minimatch": "^10.2.4", "pretty-format": { "react-is": "^19.0.0" From 72c1f9356ce8773949204e60b6f787d667b1bd2b Mon Sep 17 00:00:00 2001 From: simonlabarere Date: Tue, 5 May 2026 16:17:11 +0100 Subject: [PATCH 3/8] CCM-17440: node-jose ridance attempt --- lambdas/key-generation/package.json | 3 +- .../src/__tests__/refresh-keystores.test.ts | 35 ++--- .../key-generation/src/refresh-keystores.ts | 6 +- .../__tests__/apis/sqs-trigger-lambda.test.ts | 1 + package-lock.json | 122 ++++-------------- utils/utils/package.json | 2 +- .../delete-key.test.ts | 0 .../generate-new-key.test.ts | 6 +- .../get-private-key.test.ts | 0 .../key-generation-utils/jwk-key.test.ts | 73 +++++++++++ .../upload-public-keystore-to-s3.test.ts | 4 +- .../validate-private-key.test.ts | 5 +- .../key-generation-utils/generate-new-key.ts | 6 +- .../key-generation-utils/get-private-key.ts | 10 +- utils/utils/src/key-generation-utils/index.ts | 1 + .../src/key-generation-utils/jwk-key-store.ts | 58 +++++++++ .../utils/src/key-generation-utils/jwk-key.ts | 70 ++++++++++ utils/utils/src/key-generation-utils/jwk.ts | 34 +++++ .../upload-public-keystore-to-s3.ts | 4 +- .../validate-private-key.ts | 6 +- 20 files changed, 304 insertions(+), 142 deletions(-) rename utils/utils/src/__tests__/{key-generation => key-generation-utils}/delete-key.test.ts (100%) rename utils/utils/src/__tests__/{key-generation => key-generation-utils}/generate-new-key.test.ts (89%) rename utils/utils/src/__tests__/{key-generation => key-generation-utils}/get-private-key.test.ts (100%) create mode 100644 utils/utils/src/__tests__/key-generation-utils/jwk-key.test.ts rename utils/utils/src/__tests__/{key-generation => key-generation-utils}/upload-public-keystore-to-s3.test.ts (93%) rename utils/utils/src/__tests__/{key-generation => key-generation-utils}/validate-private-key.test.ts (95%) create mode 100644 utils/utils/src/key-generation-utils/jwk-key-store.ts create mode 100644 utils/utils/src/key-generation-utils/jwk-key.ts create mode 100644 utils/utils/src/key-generation-utils/jwk.ts diff --git a/lambdas/key-generation/package.json b/lambdas/key-generation/package.json index 6ec26b7cf..8600dee1d 100644 --- a/lambdas/key-generation/package.json +++ b/lambdas/key-generation/package.json @@ -2,7 +2,7 @@ "dependencies": { "date-fns": "^4.1.0", "esbuild": "^0.25.9", - "node-jose": "^2.2.0", + "jose": "^5.10.0", "utils": "*" }, "devDependencies": { @@ -10,7 +10,6 @@ "@types/aws-lambda": "^8.10.148", "@types/jest": "^29.5.14", "@types/node": "^24.0.10", - "@types/node-jose": "^1.1.13", "jest": "^29.7.0", "jest-mock-extended": "^3.0.7", "typescript": "^5.8.2" diff --git a/lambdas/key-generation/src/__tests__/refresh-keystores.test.ts b/lambdas/key-generation/src/__tests__/refresh-keystores.test.ts index 2ce938e9f..5a75dce55 100644 --- a/lambdas/key-generation/src/__tests__/refresh-keystores.test.ts +++ b/lambdas/key-generation/src/__tests__/refresh-keystores.test.ts @@ -1,5 +1,6 @@ -import { JWK } from 'node-jose'; import { + Key, + createKeyStore, deleteKey, generateNewKey, logger, @@ -10,7 +11,6 @@ import { import { cleanAndRefreshKeystores } from 'refresh-keystores'; import { loadConfig } from 'config'; -jest.mock('node-jose'); jest.mock('utils', () => { const originalModule = jest.requireActual('utils'); @@ -19,6 +19,7 @@ jest.mock('utils', () => { parameterStore: { getAllParameters: jest.fn(), }, + createKeyStore: jest.fn(), deleteKey: jest.fn(), generateNewKey: jest.fn(), uploadPublicKeystoreToS3: jest.fn(), @@ -30,10 +31,10 @@ jest.mock('config'); const setupMocks = (preExistingKeys?: string[]) => { const mockKeyStore = { add: jest.fn(), - all: jest.fn().mockReturnValue(['']), + all: jest.fn().mockReturnValue([{ toJSON: () => ({ kid: 'mock-kid' }) }]), }; - (JWK.createKeyStore as jest.Mock).mockImplementation(() => mockKeyStore); + (createKeyStore as jest.Mock).mockImplementation(() => mockKeyStore); (loadConfig as jest.Mock).mockReturnValue({ environment: 'env', @@ -94,7 +95,7 @@ describe('cleanAndRefreshKeystores', () => { mockValidatePrivateKey.mockResolvedValue({ valid: true, - keyJwk: {} as JWK.Key, + keyJwk: {} as unknown as Key, keyDate: new Date('2021-02-24'), }); @@ -127,7 +128,7 @@ describe('cleanAndRefreshKeystores', () => { mockValidatePrivateKey.mockResolvedValue({ valid: false, - keyJwk: {} as JWK.Key, + keyJwk: {} as unknown as Key, keyDate: new Date('2021-02-24'), }); @@ -160,7 +161,7 @@ describe('cleanAndRefreshKeystores', () => { mockValidatePrivateKey.mockResolvedValue({ valid: true, - keyJwk: {} as JWK.Key, + keyJwk: {} as unknown as Key, keyDate: new Date('2021-02-23'), }); @@ -195,7 +196,7 @@ describe('cleanAndRefreshKeystores', () => { mockValidatePrivateKey.mockResolvedValue({ valid: true, - keyJwk: {} as JWK.Key, + keyJwk: {} as unknown as Key, keyDate: new Date('2024-07-27'), }); @@ -236,7 +237,7 @@ describe('cleanAndRefreshKeystores', () => { mockValidatePrivateKey.mockResolvedValue({ valid: true, - keyJwk: {} as JWK.Key, + keyJwk: {} as unknown as Key, keyDate: new Date('2024-06-30'), }); @@ -279,12 +280,12 @@ describe('cleanAndRefreshKeystores', () => { mockValidatePrivateKey .mockResolvedValueOnce({ valid: false, - keyJwk: {} as JWK.Key, + keyJwk: {} as unknown as Key, keyDate: new Date('2024-07-30'), }) .mockResolvedValueOnce({ valid: true, - keyJwk: {} as JWK.Key, + keyJwk: {} as unknown as Key, keyDate: new Date('2024-08-27'), }); @@ -332,7 +333,7 @@ describe('cleanAndRefreshKeystores', () => { mockValidatePrivateKey.mockResolvedValue({ valid: true, - keyJwk: { kid: 'key-1' } as JWK.Key, + keyJwk: { kid: 'key-1' } as unknown as Key, keyDate: new Date('2024-09-01'), // 24 days old < 28 days threshold }); @@ -361,12 +362,12 @@ describe('cleanAndRefreshKeystores', () => { mockValidatePrivateKey .mockResolvedValueOnce({ valid: true, - keyJwk: { kid: 'key1' } as JWK.Key, + keyJwk: { kid: 'key1' } as unknown as Key, keyDate: new Date('2024-07-15'), // newer key first }) .mockResolvedValueOnce({ valid: true, - keyJwk: { kid: 'key2' } as JWK.Key, + keyJwk: { kid: 'key2' } as unknown as Key, keyDate: new Date('2024-06-01'), // older key second — should not update youngestKeyDate }); @@ -390,12 +391,12 @@ describe('cleanAndRefreshKeystores', () => { mockValidatePrivateKey .mockResolvedValueOnce({ valid: true, - keyJwk: { kid: 'key1' } as JWK.Key, + keyJwk: { kid: 'key1' } as unknown as Key, keyDate: new Date('2024-06-01'), }) .mockResolvedValueOnce({ valid: true, - keyJwk: { kid: 'key2' } as JWK.Key, + keyJwk: { kid: 'key2' } as unknown as Key, keyDate: new Date('2024-07-15'), // later }); @@ -415,7 +416,7 @@ describe('cleanAndRefreshKeystores', () => { mockValidatePrivateKey.mockResolvedValue({ valid: false, - keyJwk: { kid: 'ignored' } as JWK.Key, + keyJwk: { kid: 'ignored' } as unknown as Key, keyDate: new Date('2000-01-01'), }); diff --git a/lambdas/key-generation/src/refresh-keystores.ts b/lambdas/key-generation/src/refresh-keystores.ts index 55d63804e..f04876bf9 100644 --- a/lambdas/key-generation/src/refresh-keystores.ts +++ b/lambdas/key-generation/src/refresh-keystores.ts @@ -1,7 +1,7 @@ -import { JWK } from 'node-jose'; import { isBefore, subDays } from 'date-fns'; import { NonNullSSMParam, + createKeyStore, deleteKey, generateNewKey, logger, @@ -23,7 +23,7 @@ const deleteInvalidKeysAndCreateKeystore = async ({ now, ssmPath, }: DeleteInvalidKeysAndCreateKeystoreParams) => { - const keystore = JWK.createKeyStore(); + const keystore = createKeyStore(); let youngestKeyDate: Date | null = null; const allParams = await parameterStore.getAllParameters(ssmPath); @@ -104,6 +104,6 @@ export const cleanAndRefreshKeystores = async ({ logger.info({ description: `Email auth keygen refresh complete: current key IDs: ${keystore .all() - .map((key) => key.kid)}`, + .map((key) => key.toJSON().kid)}`, }); }; diff --git a/lambdas/nhsapp-status-handler/src/__tests__/apis/sqs-trigger-lambda.test.ts b/lambdas/nhsapp-status-handler/src/__tests__/apis/sqs-trigger-lambda.test.ts index 01c208e22..f5d3584e4 100644 --- a/lambdas/nhsapp-status-handler/src/__tests__/apis/sqs-trigger-lambda.test.ts +++ b/lambdas/nhsapp-status-handler/src/__tests__/apis/sqs-trigger-lambda.test.ts @@ -8,6 +8,7 @@ import { import { randomUUID } from 'node:crypto'; jest.mock('node:crypto', () => ({ + ...jest.requireActual('node:crypto'), randomUUID: jest.fn(), })); diff --git a/package-lock.json b/package-lock.json index b51afda59..d75c79f0a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -700,7 +700,7 @@ "dependencies": { "date-fns": "^4.1.0", "esbuild": "^0.25.9", - "node-jose": "^2.2.0", + "jose": "^5.10.0", "utils": "*" }, "devDependencies": { @@ -708,7 +708,6 @@ "@types/aws-lambda": "^8.10.148", "@types/jest": "^29.5.14", "@types/node": "^24.0.10", - "@types/node-jose": "^1.1.13", "jest": "^29.7.0", "jest-mock-extended": "^3.0.7", "typescript": "^5.8.2" @@ -971,6 +970,15 @@ "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, + "lambdas/key-generation/node_modules/jose": { + "version": "5.10.0", + "resolved": "https://registry.npmjs.org/jose/-/jose-5.10.0.tgz", + "integrity": "sha512-s+3Al/p9g32Iq+oqXxkW//7jk2Vig6FF1CFqzVXoTUXt2qz89YWbL+OwS17NFYEvxC35n0FKeGO2LGYSxeM2Gg==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/panva" + } + }, "lambdas/key-generation/node_modules/picomatch": { "version": "2.3.2", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", @@ -11874,14 +11882,6 @@ "undici-types": "~7.18.0" } }, - "node_modules/@types/node-jose": { - "version": "1.1.13", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/node": "*" - } - }, "node_modules/@types/node/node_modules/undici-types": { "version": "7.18.2", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.18.2.tgz", @@ -12916,12 +12916,12 @@ } }, "node_modules/axios": { - "version": "1.15.0", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.15.0.tgz", - "integrity": "sha512-wWyJDlAatxk30ZJer+GeCWS209sA42X+N5jU2jy6oHTp7ufw8uzUTVFBX9+wTfAlhiJXGS0Bq7X6efruWjuK9Q==", + "version": "1.16.0", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.16.0.tgz", + "integrity": "sha512-6hp5CwvTPlN2A31g5dxnwAX0orzM7pmCRDLnZSX772mv8WDqICwFjowHuPs04Mc8deIld1+ejhtaMn5vp6b+1w==", "license": "MIT", "dependencies": { - "follow-redirects": "^1.15.11", + "follow-redirects": "^1.16.0", "form-data": "^4.0.5", "proxy-from-env": "^2.1.0" } @@ -13074,13 +13074,6 @@ ], "license": "MIT" }, - "node_modules/base64url": { - "version": "3.0.1", - "license": "MIT", - "engines": { - "node": ">=6.0.0" - } - }, "node_modules/baseline-browser-mapping": { "version": "2.9.19", "dev": true, @@ -15046,10 +15039,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/es6-promise": { - "version": "4.2.8", - "license": "MIT" - }, "node_modules/esbuild": { "version": "0.25.12", "hasInstallScript": true, @@ -21510,10 +21499,6 @@ "node": ">= 12.0.0" } }, - "node_modules/long": { - "version": "5.3.2", - "license": "Apache-2.0" - }, "node_modules/loose-envify": { "version": "1.4.0", "dev": true, @@ -22000,15 +21985,6 @@ "semver": "bin/semver.js" } }, - "node_modules/node-forge": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.4.0.tgz", - "integrity": "sha512-LarFH0+6VfriEhqMMcLX2F7SwSXeWwnEAJEsYm5QKWchiVYVvJyV9v7UDvUv+w5HO23ZpQTXDv/GxdDdMyOuoQ==", - "license": "(BSD-3-Clause OR GPL-2.0)", - "engines": { - "node": ">= 6.13.0" - } - }, "node_modules/node-gyp-build": { "version": "4.8.4", "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.8.4.tgz", @@ -22026,54 +22002,6 @@ "dev": true, "license": "MIT" }, - "node_modules/node-jose": { - "version": "2.2.0", - "license": "Apache-2.0", - "dependencies": { - "base64url": "^3.0.1", - "buffer": "^6.0.3", - "es6-promise": "^4.2.8", - "lodash": "^4.17.21", - "long": "^5.2.0", - "node-forge": "^1.2.1", - "pako": "^2.0.4", - "process": "^0.11.10", - "uuid": "^9.0.0" - } - }, - "node_modules/node-jose/node_modules/buffer": { - "version": "6.0.3", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT", - "dependencies": { - "base64-js": "^1.3.1", - "ieee754": "^1.2.1" - } - }, - "node_modules/node-jose/node_modules/uuid": { - "version": "9.0.1", - "funding": [ - "https://github.com/sponsors/broofa", - "https://github.com/sponsors/ctavan" - ], - "license": "MIT", - "bin": { - "uuid": "dist/bin/uuid" - } - }, "node_modules/node-releases": { "version": "2.0.27", "dev": true, @@ -22386,10 +22314,6 @@ "integrity": "sha512-61A5ThoTiDG/C8s8UMZwSorAGwMJ0ERVGj2OjoW5pAalsNOg15+iQiPzrLJ4jhZ1HJzmC2PIHT2oEiH3R5fzNA==", "license": "MIT" }, - "node_modules/pako": { - "version": "2.1.0", - "license": "(MIT AND Zlib)" - }, "node_modules/parent-module": { "version": "1.0.1", "dev": true, @@ -22928,13 +22852,6 @@ "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, - "node_modules/process": { - "version": "0.11.10", - "license": "MIT", - "engines": { - "node": ">= 0.6.0" - } - }, "node_modules/process-warning": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/process-warning/-/process-warning-5.0.0.tgz", @@ -28242,7 +28159,7 @@ "async-mutex": "^0.4.0", "axios": "^1.15.0", "date-fns": "^4.1.0", - "node-jose": "^2.2.0", + "jose": "^5.10.0", "winston": "^3.17.0", "zod": "^4.1.12" }, @@ -28727,6 +28644,15 @@ "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, + "utils/utils/node_modules/jose": { + "version": "5.10.0", + "resolved": "https://registry.npmjs.org/jose/-/jose-5.10.0.tgz", + "integrity": "sha512-s+3Al/p9g32Iq+oqXxkW//7jk2Vig6FF1CFqzVXoTUXt2qz89YWbL+OwS17NFYEvxC35n0FKeGO2LGYSxeM2Gg==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/panva" + } + }, "utils/utils/node_modules/picomatch": { "version": "2.3.2", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", diff --git a/utils/utils/package.json b/utils/utils/package.json index b9414a801..744ceb168 100644 --- a/utils/utils/package.json +++ b/utils/utils/package.json @@ -12,7 +12,7 @@ "async-mutex": "^0.4.0", "axios": "^1.15.0", "date-fns": "^4.1.0", - "node-jose": "^2.2.0", + "jose": "^5.10.0", "winston": "^3.17.0", "zod": "^4.1.12" }, diff --git a/utils/utils/src/__tests__/key-generation/delete-key.test.ts b/utils/utils/src/__tests__/key-generation-utils/delete-key.test.ts similarity index 100% rename from utils/utils/src/__tests__/key-generation/delete-key.test.ts rename to utils/utils/src/__tests__/key-generation-utils/delete-key.test.ts diff --git a/utils/utils/src/__tests__/key-generation/generate-new-key.test.ts b/utils/utils/src/__tests__/key-generation-utils/generate-new-key.test.ts similarity index 89% rename from utils/utils/src/__tests__/key-generation/generate-new-key.test.ts rename to utils/utils/src/__tests__/key-generation-utils/generate-new-key.test.ts index 5f654ecfb..c52ac0eb9 100644 --- a/utils/utils/src/__tests__/key-generation/generate-new-key.test.ts +++ b/utils/utils/src/__tests__/key-generation-utils/generate-new-key.test.ts @@ -1,4 +1,4 @@ -import { JWK } from 'node-jose'; +import { KeyStore } from '../../key-generation-utils/jwk'; import { logger } from '../../logger'; import { parameterStore } from '../../ssm-utils'; import { generateNewKey } from '../../key-generation-utils'; @@ -19,7 +19,7 @@ describe('generateNewKey', () => { }); it('generate key on SSM', async () => { - const keystore = JWK.createKeyStore(); + const keystore = new KeyStore(); await generateNewKey({ keystore, @@ -38,7 +38,7 @@ describe('generateNewKey', () => { }); it('generate key without specifying kid', async () => { - const keystore = JWK.createKeyStore(); + const keystore = new KeyStore(); await generateNewKey({ keystore, diff --git a/utils/utils/src/__tests__/key-generation/get-private-key.test.ts b/utils/utils/src/__tests__/key-generation-utils/get-private-key.test.ts similarity index 100% rename from utils/utils/src/__tests__/key-generation/get-private-key.test.ts rename to utils/utils/src/__tests__/key-generation-utils/get-private-key.test.ts diff --git a/utils/utils/src/__tests__/key-generation-utils/jwk-key.test.ts b/utils/utils/src/__tests__/key-generation-utils/jwk-key.test.ts new file mode 100644 index 000000000..ef90c2c60 --- /dev/null +++ b/utils/utils/src/__tests__/key-generation-utils/jwk-key.test.ts @@ -0,0 +1,73 @@ +import { Key } from '../../key-generation-utils/jwk-key'; + +const testPrivateKeyPem = `-----BEGIN EC PRIVATE KEY----- +MHcCAQEEIEpVnqrylY4xEsQdQgJhGFUFKGTGtl5cnKsIq2uNWa56oAoGCCqGSM49 +AwEHoUQDQgAEqoc8zybajz/NEoUzP5G7lchuuD7dej7vKlWConh1mvI9gvmyRheT +0vrkuPszvyLXTYusKKgiLZkqz3SHOjVhDw== +-----END EC PRIVATE KEY----- +`; + +describe('Key', () => { + describe('fromPEM', () => { + it('creates a Key from a valid PEM string', async () => { + const key = await Key.fromPEM(testPrivateKeyPem); + expect(key).toBeInstanceOf(Key); + }); + + it('throws an error for an invalid PEM string', async () => { + await expect(Key.fromPEM('not-a-valid-pem')).rejects.toThrow( + 'Invalid PEM formatted message.', + ); + }); + }); + + describe('fromJWK', () => { + it('creates a Key from a public JWK object', () => { + const jwk = { kty: 'EC', crv: 'P-256', x: 'abc', y: 'def' }; + const key = Key.fromJWK(jwk); + expect(key).toBeInstanceOf(Key); + }); + }); + + describe('toJSON', () => { + it('returns only public JWK fields (strips private key material)', async () => { + const key = await Key.fromPEM(testPrivateKeyPem); + const jwk = key.toJSON(); + + // Private fields must not be present + expect(jwk).not.toHaveProperty('d'); + expect(jwk).not.toHaveProperty('p'); + expect(jwk).not.toHaveProperty('q'); + expect(jwk).not.toHaveProperty('dp'); + expect(jwk).not.toHaveProperty('dq'); + expect(jwk).not.toHaveProperty('qi'); + expect(jwk).not.toHaveProperty('k'); + + // Public fields should be present + expect(jwk).toHaveProperty('kty'); + expect(jwk).toHaveProperty('x'); + expect(jwk).toHaveProperty('y'); + }); + + it('returns all fields when constructed from a public-only JWK', () => { + const jwk = { kty: 'EC', crv: 'P-256', x: 'abc', y: 'def' }; + const key = Key.fromJWK(jwk); + expect(key.toJSON()).toEqual(jwk); + }); + }); + + describe('toPEM', () => { + it('returns the original PEM string when one was provided', async () => { + const key = await Key.fromPEM(testPrivateKeyPem); + expect(key.toPEM()).toBe(testPrivateKeyPem); + }); + + it('throws when no private PEM is available (key created from public JWK)', () => { + const jwk = { kty: 'EC', crv: 'P-256', x: 'abc', y: 'def' }; + const key = Key.fromJWK(jwk); + expect(() => key.toPEM()).toThrow( + 'No private key PEM available on this Key instance.', + ); + }); + }); +}); diff --git a/utils/utils/src/__tests__/key-generation/upload-public-keystore-to-s3.test.ts b/utils/utils/src/__tests__/key-generation-utils/upload-public-keystore-to-s3.test.ts similarity index 93% rename from utils/utils/src/__tests__/key-generation/upload-public-keystore-to-s3.test.ts rename to utils/utils/src/__tests__/key-generation-utils/upload-public-keystore-to-s3.test.ts index ed6b9c8ed..6b89ecd80 100644 --- a/utils/utils/src/__tests__/key-generation/upload-public-keystore-to-s3.test.ts +++ b/utils/utils/src/__tests__/key-generation-utils/upload-public-keystore-to-s3.test.ts @@ -1,4 +1,4 @@ -import { JWK } from 'node-jose'; +import { asKeyStore } from '../../key-generation-utils/jwk'; import { logger } from '../../logger'; import { uploadPublicKeystoreToS3 } from '../../key-generation-utils'; import { putDataS3 } from '../../s3-utils'; @@ -30,7 +30,7 @@ const setup = async () => { const mockPutDataS3 = jest.fn(); (putDataS3 as jest.Mock).mockImplementation(mockPutDataS3); - const keystore = await JWK.asKeyStore(mockKeystore); + const keystore = await asKeyStore(mockKeystore); return { mockPutDataS3, keystore }; }; diff --git a/utils/utils/src/__tests__/key-generation/validate-private-key.test.ts b/utils/utils/src/__tests__/key-generation-utils/validate-private-key.test.ts similarity index 95% rename from utils/utils/src/__tests__/key-generation/validate-private-key.test.ts rename to utils/utils/src/__tests__/key-generation-utils/validate-private-key.test.ts index b5bc84e30..5961113cf 100644 --- a/utils/utils/src/__tests__/key-generation/validate-private-key.test.ts +++ b/utils/utils/src/__tests__/key-generation-utils/validate-private-key.test.ts @@ -1,5 +1,4 @@ -/* eslint-disable sonarjs/hardcoded-secret-signatures */ -import { JWK } from 'node-jose'; +import { asKey } from '../../key-generation-utils/jwk'; import { ValidateKeyResult, validatePrivateKey, @@ -105,7 +104,7 @@ describe('validatePrivateKey', () => { }); it('accepts valid key', async () => { - const testPrivateKeyJwk = await JWK.asKey(testPrivateKey, 'pem'); + const testPrivateKeyJwk = await asKey(testPrivateKey, 'pem'); const testOutput = await validatePrivateKey({ Name: 'privatekey_20201120_123.pem', diff --git a/utils/utils/src/key-generation-utils/generate-new-key.ts b/utils/utils/src/key-generation-utils/generate-new-key.ts index f9248aa54..7e9e63ddc 100644 --- a/utils/utils/src/key-generation-utils/generate-new-key.ts +++ b/utils/utils/src/key-generation-utils/generate-new-key.ts @@ -1,11 +1,11 @@ -import { JWK } from 'node-jose'; import { format } from 'date-fns'; import { logger } from '../logger'; import { parameterStore } from '../ssm-utils'; +import { KeyStore } from './jwk'; import { KeyJson } from './types'; type GenerateNewKeyParams = { - keystore: JWK.KeyStore; + keystore: KeyStore; ssmPath: string; now: Date; keyGenerationOptions?: Record; @@ -21,7 +21,7 @@ export const generateNewKey = async ({ logger.info({ description: 'Generating new key' }); const key = await keystore.generate('RSA', 4096, keyGenerationOptions); const { kid } = key.toJSON() as KeyJson; - const keyPem = key.toPEM(true); + const keyPem = key.toPEM(); const Name = `${ssmPath}/privatekey_${format(now, 'yyyyMMdd')}_${kid}.pem`; await parameterStore.addParameter(Name, keyPem); diff --git a/utils/utils/src/key-generation-utils/get-private-key.ts b/utils/utils/src/key-generation-utils/get-private-key.ts index 11d920203..dcb623a73 100644 --- a/utils/utils/src/key-generation-utils/get-private-key.ts +++ b/utils/utils/src/key-generation-utils/get-private-key.ts @@ -1,5 +1,5 @@ -import { JWK } from 'node-jose'; import { format, isValid, parse } from 'date-fns'; +import { asKey } from './jwk'; import { newCache } from '../cache'; import { logger } from '../logger'; import { @@ -8,8 +8,6 @@ import { parameterStore, } from '../ssm-utils'; -import { KeyJson } from './types'; - const validateParamName = (name: string) => { // private key params are /privatekey__.pem const privateKeyRegex = /privatekey_(\d{8})_(.+)\.pem/; @@ -82,8 +80,10 @@ export const privateKeyFetcher = (pemSSMPath: string) => { const param = await getValidPrivateKey(pemSSMPath); const keyPem = param.Value; - const key = await JWK.asKey(keyPem, 'pem'); - const { kid } = key.toJSON() as KeyJson; + await asKey(keyPem, 'pem'); + const privateKeyRegex = /privatekey_(\d{8})_(.+)\.pem/; + // eslint-disable-next-line sonarjs/prefer-regexp-exec + const kid = (param.Name.match(privateKeyRegex) ?? [])[2]; return { key: keyPem, kid, diff --git a/utils/utils/src/key-generation-utils/index.ts b/utils/utils/src/key-generation-utils/index.ts index 0474c0f45..a4a76e7af 100644 --- a/utils/utils/src/key-generation-utils/index.ts +++ b/utils/utils/src/key-generation-utils/index.ts @@ -1,6 +1,7 @@ export * from './get-private-key'; export * from './delete-key'; export * from './generate-new-key'; +export * from './jwk'; export * from './types'; export * from './upload-public-keystore-to-s3'; export * from './validate-private-key'; diff --git a/utils/utils/src/key-generation-utils/jwk-key-store.ts b/utils/utils/src/key-generation-utils/jwk-key-store.ts new file mode 100644 index 000000000..6c7767290 --- /dev/null +++ b/utils/utils/src/key-generation-utils/jwk-key-store.ts @@ -0,0 +1,58 @@ +import { calculateJwkThumbprint, exportJWK } from 'jose'; +import { createPrivateKey, generateKeyPairSync } from 'node:crypto'; +import { type JWKJson, Key } from './jwk-key'; + +/** + * Lightweight replacement for node-jose's `JWK.KeyStore`. + * + * Provides `add`, `all`, and `generate` with the same signatures used in this + * codebase. Keys are stored in an in-memory array. + */ +export class KeyStore { + private readonly _keys: Key[] = []; + + /** Add an existing Key to the store. */ + add(key: Key): void { + this._keys.push(key); + } + + /** Return a shallow copy of all keys in the store. */ + all(): Key[] { + return [...this._keys]; + } + + /** + * Generate a new RSA key, add it to the store, and return it. + * + * @param _type Key type – only `'RSA'` is used in this codebase. + * @param bits Key size in bits (e.g. 4096). + * @param options Optional JWK metadata (`kid`, `use`, `alg`, …). When `kid` is + * omitted a SHA-256 JWK thumbprint is calculated automatically. + */ + async generate( + _type: string, + bits: number, + options: Record = {}, + ): Promise { + const { privateKey: nodePrivateKey } = generateKeyPairSync('rsa', { + modulusLength: bits, + }); + + const pem = nodePrivateKey.export({ + type: 'pkcs8', + format: 'pem', + }) as string; + + const reImportedKey = createPrivateKey(pem); + const jwk = await exportJWK(reImportedKey); + + const { kid: optionsKid, ...restOptions } = options; + const kid = optionsKid ?? (await calculateJwkThumbprint(jwk)); + + const finalJwk: JWKJson = { ...jwk, ...restOptions, kid }; + + const key = new Key(finalJwk, pem); + this._keys.push(key); + return key; + } +} diff --git a/utils/utils/src/key-generation-utils/jwk-key.ts b/utils/utils/src/key-generation-utils/jwk-key.ts new file mode 100644 index 000000000..6698712c7 --- /dev/null +++ b/utils/utils/src/key-generation-utils/jwk-key.ts @@ -0,0 +1,70 @@ +import { exportJWK } from 'jose'; +import { createPrivateKey } from 'node:crypto'; + +export type JWKJson = Record; + +/** Private fields that must be stripped when producing a public JWK. */ +const PRIVATE_JWK_FIELDS = new Set(['d', 'dp', 'dq', 'k', 'p', 'q', 'qi']); + +/** + * Lightweight replacement for node-jose's `JWK.Key`. + * + * Stores the raw private PEM (when available) alongside the exported JWK so that + * `toJSON()` (public JWK) and `toPEM()` (private PEM) can be served cheaply without + * keeping a live crypto object in memory on the long path. + */ +export class Key { + /** The full JWK representation of this key (may contain private key material). */ + private readonly _jwk: JWKJson; + + /** Original PEM string used to import this key, if available. */ + private readonly _privatePem: string | null; + + constructor(jwk: JWKJson, privatePem: string | null) { + this._jwk = jwk; + this._privatePem = privatePem; + } + + /** + * Create a Key from a PEM-encoded private key string. + * Throws an error with a stable message when the PEM cannot be parsed, + * matching the behaviour callers expect. + */ + static async fromPEM(pem: string): Promise { + try { + const nodeKey = createPrivateKey(pem); + const jwk = await exportJWK(nodeKey); + return new Key(jwk as unknown as JWKJson, pem); + } catch { + throw new Error('Invalid PEM formatted message.'); + } + } + + /** Create a Key from an already-parsed public JWK object (no private material). */ + static fromJWK(jwk: JWKJson): Key { + return new Key(jwk, null); + } + + /** + * Returns the *public* JWK representation of this key (private key fields are + * stripped), matching the default behaviour of node-jose's `key.toJSON()`. + */ + toJSON(): JWKJson { + return Object.fromEntries( + Object.entries(this._jwk).filter( + ([field]) => !PRIVATE_JWK_FIELDS.has(field), + ), + ); + } + + /** + * Returns the PEM encoding of the key. + * `key.toPEM()` usage. + */ + toPEM(): string { + if (!this._privatePem) { + throw new Error('No private key PEM available on this Key instance.'); + } + return this._privatePem; + } +} diff --git a/utils/utils/src/key-generation-utils/jwk.ts b/utils/utils/src/key-generation-utils/jwk.ts new file mode 100644 index 000000000..00f033478 --- /dev/null +++ b/utils/utils/src/key-generation-utils/jwk.ts @@ -0,0 +1,34 @@ +import { type JWKJson, Key } from './jwk-key'; +import { KeyStore } from './jwk-key-store'; + +export { type JWKJson, Key } from './jwk-key'; +export { KeyStore } from './jwk-key-store'; + +// --------------------------------------------------------------------------- +// Factory helpers mirroring node-jose's JWK namespace +// --------------------------------------------------------------------------- + +/** Create a new empty KeyStore. */ +export const createKeyStore = (): KeyStore => new KeyStore(); + +/** + * Import a single PEM-encoded private key. + * The second argument (`format`) is accepted for API compatibility but ignored – + * the input is always treated as a PEM string. + */ +export const asKey = async (pem: string, _format: string): Promise => + Key.fromPEM(pem); + +/** + * Import a JWKS JSON object into a KeyStore. + * Useful in tests where a static set of public JWK objects is provided. + */ +export const asKeyStore = async (json: { + keys: JWKJson[]; +}): Promise => { + const store = new KeyStore(); + for (const keyJson of json.keys) { + store.add(Key.fromJWK(keyJson)); + } + return store; +}; diff --git a/utils/utils/src/key-generation-utils/upload-public-keystore-to-s3.ts b/utils/utils/src/key-generation-utils/upload-public-keystore-to-s3.ts index b4e45e552..b044feaed 100644 --- a/utils/utils/src/key-generation-utils/upload-public-keystore-to-s3.ts +++ b/utils/utils/src/key-generation-utils/upload-public-keystore-to-s3.ts @@ -1,10 +1,10 @@ -import { JWK } from 'node-jose'; import { logger } from '../logger'; import { putDataS3 } from '../s3-utils'; +import { KeyStore } from './jwk'; import { KeyStoreJson } from './types'; type UploadPublicKeystoreToS3Params = { - keystore: JWK.KeyStore; + keystore: KeyStore; staticAssetBucket: string; jwksFileName: string; }; diff --git a/utils/utils/src/key-generation-utils/validate-private-key.ts b/utils/utils/src/key-generation-utils/validate-private-key.ts index 30249f8a7..e9cdab30a 100644 --- a/utils/utils/src/key-generation-utils/validate-private-key.ts +++ b/utils/utils/src/key-generation-utils/validate-private-key.ts @@ -1,5 +1,5 @@ -import { JWK } from 'node-jose'; import { isBefore, parse } from 'date-fns'; +import { Key, asKey } from './jwk'; type ValidatePrivateKeyParams = { Name: string; @@ -14,7 +14,7 @@ export type ValidateKeyResult = deleteReason: string; warn?: boolean; } - | { valid: true; keyJwk: JWK.Key; keyDate: Date }; + | { valid: true; keyJwk: Key; keyDate: Date }; // private key param names are /riskstrat//emailauth/privatekey__.pem const privateKeyRegex = /privatekey_(\d{8})_(.+)\.pem/; @@ -55,7 +55,7 @@ export const validatePrivateKey = async ({ } try { - const keyJwk = await JWK.asKey(Value, 'pem'); + const keyJwk = await asKey(Value, 'pem'); return { valid: true, keyJwk, keyDate }; } catch (error) { return { From f86ddf5f5fa9119bbfea0dc812d009489c76a529 Mon Sep 17 00:00:00 2001 From: simonlabarere Date: Wed, 6 May 2026 15:22:11 +0100 Subject: [PATCH 4/8] CCM-17440: Update gitleaks ignore --- .gitleaksignore | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitleaksignore b/.gitleaksignore index ecc2ff3c3..400b9a8fe 100644 --- a/.gitleaksignore +++ b/.gitleaksignore @@ -18,4 +18,4 @@ d1c0a37078cbed4fbedae044e5cbafac71717af0:utils/utils/src/__tests__/key-generatio d1c0a37078cbed4fbedae044e5cbafac71717af0:utils/utils/src/__tests__/key-generation/get-private-key.test.ts:private-key:46 f0eebf1356a699213340a45f64c6b990afcbb869:infrastructure/terraform/components/dl/ssm_parameter_mesh.tf:hashicorp-tf-password:11 e75d9e202c1fad2c9591c4fe0e411194bf19c8f6:infrastructure/terraform/components/dl/ssm_parameter_mesh_config.tf:hashicorp-tf-password:11 -d0906d30e70813e7f1ae73cad88579d98a689a81:utils/utils/src/__tests__/key-generation-utils/jwk-key.test.ts:private-key:3 +72c1f9356ce8773949204e60b6f787d667b1bd2b:utils/utils/src/__tests__/key-generation-utils/jwk-key.test.ts:private-key:3 From eea0f88a574de8515a625997c3cd0e20a686879e Mon Sep 17 00:00:00 2001 From: simonlabarere Date: Thu, 7 May 2026 09:08:17 +0100 Subject: [PATCH 5/8] CCM-17440: Fix failing unit test following merge --- .../print-status-handler/src/__tests__/apis/sqs-handler.test.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/lambdas/print-status-handler/src/__tests__/apis/sqs-handler.test.ts b/lambdas/print-status-handler/src/__tests__/apis/sqs-handler.test.ts index 56f2d6ca2..490850dae 100644 --- a/lambdas/print-status-handler/src/__tests__/apis/sqs-handler.test.ts +++ b/lambdas/print-status-handler/src/__tests__/apis/sqs-handler.test.ts @@ -13,6 +13,7 @@ const logger = mock(); const eventPublisher = mock(); jest.mock('node:crypto', () => ({ + ...jest.requireActual('node:crypto'), randomUUID: jest.fn(), })); From 6247c135bfc36ec61472956f6a4c55e9596c30bb Mon Sep 17 00:00:00 2001 From: lapenna-bjss Date: Thu, 7 May 2026 12:20:12 +0100 Subject: [PATCH 6/8] CCM-17440: remove --with-deps flag from playwright install --- scripts/tests/integration.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/tests/integration.sh b/scripts/tests/integration.sh index 03994c53d..2952a5dc1 100755 --- a/scripts/tests/integration.sh +++ b/scripts/tests/integration.sh @@ -5,7 +5,7 @@ set -euo pipefail cd "$(git rev-parse --show-toplevel)" npm install -npx playwright install --with-deps > /dev/null +npx playwright install > /dev/null cd tests/playwright From ab7707a7135e1b52ba975d3485b68ec8a1190d41 Mon Sep 17 00:00:00 2001 From: lapenna-bjss Date: Thu, 7 May 2026 14:42:42 +0100 Subject: [PATCH 7/8] CCM-17440: Possible build docs fix --- src/cloudevents/Makefile | 1 + 1 file changed, 1 insertion(+) diff --git a/src/cloudevents/Makefile b/src/cloudevents/Makefile index 638cc6c44..9f9f18a1d 100644 --- a/src/cloudevents/Makefile +++ b/src/cloudevents/Makefile @@ -59,6 +59,7 @@ test-domains: deploy: @echo "=== Deploying all schemas (version: $(PUBLISH_VERSION)) ===" + npm ci $(MAKE) clean $(MAKE) -C $(CLOUDEVENTS_BASE_PATH) deploy PUBLISH_VERSION=$(PUBLISH_VERSION) $(MAKE) build-docs From 6e22d2a8af3caef5d6c677f6a6d07a6d2769926c Mon Sep 17 00:00:00 2001 From: lapenna-bjss Date: Thu, 7 May 2026 15:27:53 +0100 Subject: [PATCH 8/8] CCM-17440: Bump build-docs action to the latest version --- .github/workflows/stage-3-build.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/stage-3-build.yaml b/.github/workflows/stage-3-build.yaml index c83d9c4e7..ac4f13a53 100644 --- a/.github/workflows/stage-3-build.yaml +++ b/.github/workflows/stage-3-build.yaml @@ -75,6 +75,6 @@ jobs: - name: "Checkout code" uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5 - name: "Build docs" - uses: NHSDigital/nhs-notify-shared-modules/.github/actions/build-docs@3.0.0 + uses: NHSDigital/nhs-notify-shared-modules/.github/actions/build-docs@3.1.3 with: version: "${{ inputs.version }}"