diff --git a/.eslintrc.json b/.eslintrc.json index e1ed0fa6f..23778cec2 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -68,7 +68,8 @@ "jest.config.ts", "jest.setup.ts", "**/__tests__/**", - "**/*.dev.[jt]s?(x)" + "**/*.dev.[jt]s?(x)", + "**/*.test.[jt]s?(x)" ] } ], diff --git a/.github/workflows/stage-2-test.yaml b/.github/workflows/stage-2-test.yaml index 531f15cdd..5ff6865e0 100644 --- a/.github/workflows/stage-2-test.yaml +++ b/.github/workflows/stage-2-test.yaml @@ -153,3 +153,67 @@ jobs: sonar_organisation_key: "${{ vars.SONAR_ORGANISATION_KEY }}" sonar_project_key: "${{ vars.SONAR_PROJECT_KEY }}" sonar_token: "${{ secrets.SONAR_TOKEN }}" + + consumer-contracts: + name: Consumer Contract Tests + runs-on: ubuntu-latest + environment: dev + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Install dependencies + run: npm ci + + - name: Configure AWS credentials + uses: aws-actions/configure-aws-credentials@v4 + with: + role-to-assume: arn:aws:iam::${{ secrets.AWS_ACCOUNT_ID }}:role/${{ secrets.AWS_ASSUME_ROLE_NAME }} + role-session-name: templates-ci-contract-tests + aws-region: ${{ env.AWS_REGION }} + + - name: Download golden contracts from providers + run: | + export PACT_BUCKET="nhs-notify-${{ secrets.AWS_ACCOUNT_ID }}-${{ env.AWS_REGION }}-main-acct-artefacts" + npm run test:contracts:download:provider + + - name: Validate consumer examples against golden contracts + run: npm run test:contracts:consumer:golden + + - name: Run consumer contract tests + run: npm run test:contracts:consumer + + - name: Upload consumer Pact files + run: | + export PACT_BUCKET="nhs-notify-${{ secrets.AWS_ACCOUNT_ID }}-${{ env.AWS_REGION }}-main-acct-artefacts" + npm run test:contracts:upload:consumer + + - name: Upload Pact contracts as artifacts + uses: actions/upload-artifact@v4 + with: + name: consumer-pacts + path: tests/contracts/tests/*/consumer/pacts/*.json + if-no-files-found: warn + + provider-contracts: + name: Provider Contract Tests + runs-on: ubuntu-latest + environment: dev + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Install dependencies + run: npm ci + + - name: Configure AWS credentials + uses: aws-actions/configure-aws-credentials@v4 + with: + role-to-assume: arn:aws:iam::${{ secrets.AWS_ACCOUNT_ID }}:role/${{ secrets.AWS_ASSUME_ROLE_NAME }} + role-session-name: templates-ci-contract-tests + aws-region: ${{ env.AWS_REGION }} + + - name: Download consumer generated Pact files and run provider contract tests + run: | + export PACT_BUCKET="nhs-notify-${{ secrets.AWS_ACCOUNT_ID }}-${{ env.AWS_REGION }}-main-acct-artefacts" + npm run test:contracts:provider:ci diff --git a/package-lock.json b/package-lock.json index 5275f5a41..bc4b32616 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14,6 +14,7 @@ "lambdas/download-authorizer", "lambdas/sftp-letters", "tests/accessibility", + "tests/contracts", "tests/test-team", "utils/backend-config", "utils/entity-update-command-builder", @@ -2029,6 +2030,23 @@ "node": ">=6.0.0" } }, + "node_modules/@apidevtools/json-schema-ref-parser": { + "version": "14.0.3", + "resolved": "https://registry.npmjs.org/@apidevtools/json-schema-ref-parser/-/json-schema-ref-parser-14.0.3.tgz", + "integrity": "sha512-XtI3vr6mq5ySDV7j+/ya7m9UDkRYN91NeSM5CBjGE8EZHXTuu5duHMm5emG+X8tmjRCYpEkWpHfxHpVR91owVg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/json-schema": "^7.0.15", + "js-yaml": "^4.1.0" + }, + "engines": { + "node": ">= 20" + }, + "funding": { + "url": "https://github.com/sponsors/philsturgeon" + } + }, "node_modules/@aws-amplify/adapter-nextjs": { "version": "1.5.8", "resolved": "https://registry.npmjs.org/@aws-amplify/adapter-nextjs/-/adapter-nextjs-1.5.8.tgz", @@ -11474,6 +11492,224 @@ "node": ">=12.4.0" } }, + "node_modules/@pact-foundation/pact": { + "version": "15.0.1", + "resolved": "https://registry.npmjs.org/@pact-foundation/pact/-/pact-15.0.1.tgz", + "integrity": "sha512-9pv9mN/grXiXCPmyzQb9YYeyT8aHYO4uRNtfuR4IGLhNSHkHIhiS97ZUsegPruVkWiTcCv9tJahG+1OhL5BrTQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@pact-foundation/pact-core": "^16.0.0", + "axios": "^1.8.4", + "body-parser": "^1.20.3", + "chalk": "4.1.2", + "express": "^4.21.1", + "graphql": "^16.10.0", + "graphql-tag": "^2.9.1", + "http-proxy": "^1.18.1", + "https-proxy-agent": "^7.0.4", + "js-base64": "^3.6.1", + "lodash": "^4.17.21", + "ramda": "^0.30.0", + "randexp": "^0.5.3" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/@pact-foundation/pact-core": { + "version": "16.0.0", + "resolved": "https://registry.npmjs.org/@pact-foundation/pact-core/-/pact-core-16.0.0.tgz", + "integrity": "sha512-Zdo/JIsReDrJLg0tCN0IinTmMi4tU+gmKPNc70J0wY0j/zMuL4xdpqotKhIDChf9yK4sEr2K24lKEZ9yQN2eWw==", + "cpu": [ + "x64", + "ia32", + "arm64" + ], + "dev": true, + "license": "MIT", + "os": [ + "darwin", + "linux", + "win32" + ], + "dependencies": { + "check-types": "7.4.0", + "detect-libc": "^2.0.3", + "node-gyp-build": "^4.6.0", + "pino": "^8.7.0", + "pino-pretty": "^9.1.1", + "underscore": "1.13.7" + }, + "engines": { + "node": ">=16" + }, + "optionalDependencies": { + "@pact-foundation/pact-core-darwin-arm64": "16.0.0", + "@pact-foundation/pact-core-darwin-x64": "16.0.0", + "@pact-foundation/pact-core-linux-arm64-glibc": "16.0.0", + "@pact-foundation/pact-core-linux-arm64-musl": "16.0.0", + "@pact-foundation/pact-core-linux-x64-glibc": "16.0.0", + "@pact-foundation/pact-core-linux-x64-musl": "16.0.0", + "@pact-foundation/pact-core-windows-x64": "16.0.0" + } + }, + "node_modules/@pact-foundation/pact-core-darwin-arm64": { + "version": "16.0.0", + "resolved": "https://registry.npmjs.org/@pact-foundation/pact-core-darwin-arm64/-/pact-core-darwin-arm64-16.0.0.tgz", + "integrity": "sha512-cXMBT9o+1Vs/bXJRwa+UNpgBJZ6MvI35IPL1vtiRdd1eclsZEkilRznzKFokcB2fO+oOFsq7LDY4eFGgsfPiEg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@pact-foundation/pact-core-darwin-x64": { + "version": "16.0.0", + "resolved": "https://registry.npmjs.org/@pact-foundation/pact-core-darwin-x64/-/pact-core-darwin-x64-16.0.0.tgz", + "integrity": "sha512-in9VZsuvQnqHHD+hxcwERYPESPHM6ZapJx0ptZKXPIOsSIfAlNEeXWI9a6cqdHE87oEE+ypyMDV9HcARLbCxGA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@pact-foundation/pact-core-linux-arm64-glibc": { + "version": "16.0.0", + "resolved": "https://registry.npmjs.org/@pact-foundation/pact-core-linux-arm64-glibc/-/pact-core-linux-arm64-glibc-16.0.0.tgz", + "integrity": "sha512-EbfSfnveyx1Fo73Cyx8IAor8Af6j6hxspikJTKNbencpsrEeXgOCBGFvnwTHR+xuvn88oBmRo0MtGJwsIT1S1A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@pact-foundation/pact-core-linux-arm64-musl": { + "version": "16.0.0", + "resolved": "https://registry.npmjs.org/@pact-foundation/pact-core-linux-arm64-musl/-/pact-core-linux-arm64-musl-16.0.0.tgz", + "integrity": "sha512-hQW06EYz3leTTfNZx6126JsghC8Ilqg8FToY0mLUBZ/Y9RKM9msOR7bJi05u8nHA7g2JBFyIwNllkq5aqELeIg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@pact-foundation/pact-core-linux-x64-glibc": { + "version": "16.0.0", + "resolved": "https://registry.npmjs.org/@pact-foundation/pact-core-linux-x64-glibc/-/pact-core-linux-x64-glibc-16.0.0.tgz", + "integrity": "sha512-DqwIoM15YXol6Xc3YoCrWazEF9u/Z97zU2an83ceroRXc1VWEjf+ssd/LRT11J8OPhC2e11RDyRp+qgGWIES1Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@pact-foundation/pact-core-linux-x64-musl": { + "version": "16.0.0", + "resolved": "https://registry.npmjs.org/@pact-foundation/pact-core-linux-x64-musl/-/pact-core-linux-x64-musl-16.0.0.tgz", + "integrity": "sha512-C9b+lhYrwpn96USpeWNNZrbOoaMKo7/JqFoL81V9QQFmUawZOZNhp1i5HbznM35Apk2QdLM73P2DESUryVJ4ng==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@pact-foundation/pact-core-windows-x64": { + "version": "16.0.0", + "resolved": "https://registry.npmjs.org/@pact-foundation/pact-core-windows-x64/-/pact-core-windows-x64-16.0.0.tgz", + "integrity": "sha512-/6d0bjouofuSCWM2wauAf7+tD2AG+y5X0duchkj0vpjBdYG7tbgEB322QcyTWBi7na4plEdLSdIMnfZs6faEqA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@pact-foundation/pact-core/node_modules/check-types": { + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/check-types/-/check-types-7.4.0.tgz", + "integrity": "sha512-YbulWHdfP99UfZ73NcUDlNJhEIDgm9Doq9GhpyXbF+7Aegi3CVV7qqMCKTTqJxlvEvnQBp9IA+dxsGN6xK/nSg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@pact-foundation/pact-core/node_modules/detect-libc": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.4.tgz", + "integrity": "sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/@pact-foundation/pact-core/node_modules/underscore": { + "version": "1.13.7", + "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.13.7.tgz", + "integrity": "sha512-GMXzWtsc57XAtguZgaQViUOzs0KTkk8ojr3/xAxXLITqf/3EMwxC0inyETfDFjH/Krbhuep0HNbbjI9i/q3F3g==", + "dev": true, + "license": "MIT" + }, + "node_modules/@pact-foundation/pact/node_modules/agent-base": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.3.tgz", + "integrity": "sha512-jRR5wdylq8CkOe6hei19GGZnxM6rBGwFl3Bg0YItGDimvjGtAvdZk4Pu6Cl4u4Igsws4a1fd1Vq3ezrhn4KmFw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, + "node_modules/@pact-foundation/pact/node_modules/graphql": { + "version": "16.11.0", + "resolved": "https://registry.npmjs.org/graphql/-/graphql-16.11.0.tgz", + "integrity": "sha512-mS1lbMsxgQj6hge1XZ6p7GPhbrtFwUFYi3wRzXAC/FmYnyXMTvvI3td3rjmQ2u8ewXueaSvRPWaEcgVVOT9Jnw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.22.0 || ^14.16.0 || ^16.0.0 || >=17.0.0" + } + }, + "node_modules/@pact-foundation/pact/node_modules/https-proxy-agent": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, "node_modules/@parcel/watcher": { "version": "2.5.1", "resolved": "https://registry.npmjs.org/@parcel/watcher/-/watcher-2.5.1.tgz", @@ -13575,6 +13811,31 @@ "node": ">=18.0.0" } }, + "node_modules/@sourcemeta/jsonschema": { + "version": "9.6.0", + "resolved": "https://registry.npmjs.org/@sourcemeta/jsonschema/-/jsonschema-9.6.0.tgz", + "integrity": "sha512-tUFn57UpzmL9k2UtAHJu1ItD2Zgrm1XvCyy9sLGfd4+zq5jWYwyocYtmK9zwvLo9C+pGvNVC/q5zhgI3LCocnQ==", + "cpu": [ + "x64", + "arm64" + ], + "dev": true, + "license": "AGPL-3.0", + "os": [ + "darwin", + "linux", + "win32" + ], + "bin": { + "jsonschema": "cli.js" + }, + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sourcemeta" + } + }, "node_modules/@swc/core": { "version": "1.11.18", "resolved": "https://registry.npmjs.org/@swc/core/-/core-1.11.18.tgz", @@ -14955,6 +15216,33 @@ "dev": true, "license": "BSD-3-Clause" }, + "node_modules/abort-controller": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", + "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==", + "dev": true, + "license": "MIT", + "dependencies": { + "event-target-shim": "^5.0.0" + }, + "engines": { + "node": ">=6.5" + } + }, + "node_modules/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "dev": true, + "license": "MIT", + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, "node_modules/acorn": { "version": "8.14.1", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.1.tgz", @@ -15173,6 +15461,13 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", + "dev": true, + "license": "MIT" + }, "node_modules/array-includes": { "version": "3.1.8", "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.8.tgz", @@ -15401,6 +15696,16 @@ "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", "license": "MIT" }, + "node_modules/atomic-sleep": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/atomic-sleep/-/atomic-sleep-1.0.0.tgz", + "integrity": "sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.0.0" + } + }, "node_modules/available-typed-arrays": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", @@ -15875,6 +16180,61 @@ "dev": true, "license": "MIT" }, + "node_modules/body-parser": { + "version": "1.20.3", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz", + "integrity": "sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==", + "dev": true, + "license": "MIT", + "dependencies": { + "bytes": "3.1.2", + "content-type": "~1.0.5", + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "on-finished": "2.4.1", + "qs": "6.13.0", + "raw-body": "2.5.2", + "type-is": "~1.6.18", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/body-parser/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/body-parser/node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "dev": true, + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/body-parser/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true, + "license": "MIT" + }, "node_modules/boolbase": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", @@ -16524,6 +16884,13 @@ "simple-swizzle": "^0.2.2" } }, + "node_modules/colorette": { + "version": "2.0.20", + "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz", + "integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==", + "dev": true, + "license": "MIT" + }, "node_modules/colorspace": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/colorspace/-/colorspace-1.1.4.tgz", @@ -16629,6 +16996,33 @@ "dev": true, "license": "Apache-2.0" }, + "node_modules/content-disposition": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", + "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/contract-tests": { + "resolved": "tests/contracts", + "link": 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", @@ -16645,6 +17039,13 @@ "node": ">=18" } }, + "node_modules/cookie-signature": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", + "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==", + "dev": true, + "license": "MIT" + }, "node_modules/core-js-compat": { "version": "3.41.0", "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.41.0.tgz", @@ -17111,13 +17512,23 @@ "node": ">=0.4.0" } }, - "node_modules/dequal": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", - "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", "dev": true, "license": "MIT", - "peer": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/dequal": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", + "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", + "dev": true, + "license": "MIT", + "peer": true, "engines": { "node": ">=6" } @@ -17128,6 +17539,17 @@ "integrity": "sha512-ugFTXCtDZunbzasqBxrK93Ik/DRYsO6S/fedkWEMKqt04xZ4csmnmwGDBAb07QWNaGMAmnTIemsYZCksjATwsA==", "license": "MIT" }, + "node_modules/destroy": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, "node_modules/detect-libc": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz", @@ -17295,6 +17717,16 @@ "url": "https://dotenvx.com" } }, + "node_modules/drange": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/drange/-/drange-1.1.1.tgz", + "integrity": "sha512-pYxfDYpued//QpnLIm4Avk7rsNtAtQkUES2cwAYSvD/wd2pKD71gN2Ebj3e7klzXwjocvE8c5vx/1fxwpqmSxA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, "node_modules/dunder-proto": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", @@ -17324,6 +17756,13 @@ "safe-buffer": "^5.0.1" } }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "dev": true, + "license": "MIT" + }, "node_modules/ejs": { "version": "3.1.10", "resolved": "https://registry.npmjs.org/ejs/-/ejs-3.1.10.tgz", @@ -17378,6 +17817,16 @@ "integrity": "sha512-ucAnuBEhUK4boH2HjVYG5Q2mQyPorvv0u/ocS+zhdw0S8AlHYY+GOFhP1Gio5z4icpP2ivFSvhtFjQi8+T9ppw==", "license": "MIT" }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/encoding-sniffer": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/encoding-sniffer/-/encoding-sniffer-0.2.0.tgz", @@ -18507,6 +18956,26 @@ "node": ">=0.10.0" } }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/event-target-shim": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", + "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/eventemitter2": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/eventemitter2/-/eventemitter2-5.0.1.tgz", @@ -18514,6 +18983,23 @@ "dev": true, "license": "MIT" }, + "node_modules/eventemitter3": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz", + "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==", + "dev": true, + "license": "MIT" + }, + "node_modules/events": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", + "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.x" + } + }, "node_modules/execa": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", @@ -18564,6 +19050,87 @@ "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, + "node_modules/express": { + "version": "4.21.2", + "resolved": "https://registry.npmjs.org/express/-/express-4.21.2.tgz", + "integrity": "sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "accepts": "~1.3.8", + "array-flatten": "1.1.1", + "body-parser": "1.20.3", + "content-disposition": "0.5.4", + "content-type": "~1.0.4", + "cookie": "0.7.1", + "cookie-signature": "1.0.6", + "debug": "2.6.9", + "depd": "2.0.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "1.3.1", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "merge-descriptors": "1.0.3", + "methods": "~1.1.2", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "path-to-regexp": "0.1.12", + "proxy-addr": "~2.0.7", + "qs": "6.13.0", + "range-parser": "~1.2.1", + "safe-buffer": "5.2.1", + "send": "0.19.0", + "serve-static": "1.16.2", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/express/node_modules/cookie": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.1.tgz", + "integrity": "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/express/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/express/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true, + "license": "MIT" + }, + "node_modules/express/node_modules/path-to-regexp": { + "version": "0.1.12", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz", + "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==", + "dev": true, + "license": "MIT" + }, "node_modules/extract-zip": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-2.0.1.tgz", @@ -18609,6 +19176,13 @@ "follow-redirects": "^1.14.0" } }, + "node_modules/fast-copy": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/fast-copy/-/fast-copy-3.0.2.tgz", + "integrity": "sha512-dl0O9Vhju8IrcLndv2eU4ldt1ftXMqqfgN4H1cpmGV7P6jeB9FwpN9a2c8DPGE1Ys88rNUJVYDHq73CGAGOPfQ==", + "dev": true, + "license": "MIT" + }, "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", @@ -18677,6 +19251,23 @@ "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", "license": "MIT" }, + "node_modules/fast-redact": { + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/fast-redact/-/fast-redact-3.5.0.tgz", + "integrity": "sha512-dwsoQlS7h9hMeYUq1W++23NDcBLV4KqONnITDV9DjfS3q1SgDGVrBdvvTLUotWtPSD7asWDV9/CmsZPy8Hf70A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/fast-safe-stringify": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/fast-safe-stringify/-/fast-safe-stringify-2.1.1.tgz", + "integrity": "sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==", + "dev": true, + "license": "MIT" + }, "node_modules/fast-xml-parser": { "version": "4.4.1", "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-4.4.1.tgz", @@ -18798,6 +19389,42 @@ "node": ">=8" } }, + "node_modules/finalhandler": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.1.tgz", + "integrity": "sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "statuses": "2.0.1", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/finalhandler/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/finalhandler/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true, + "license": "MIT" + }, "node_modules/find-up": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", @@ -18929,6 +19556,26 @@ "node": ">= 6" } }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, "node_modules/fs-constants": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", @@ -19375,6 +20022,22 @@ "node": ">= 10.x" } }, + "node_modules/graphql-tag": { + "version": "2.12.6", + "resolved": "https://registry.npmjs.org/graphql-tag/-/graphql-tag-2.12.6.tgz", + "integrity": "sha512-FdSNcu2QQcWnM2VNvSCCDCVS5PpPqpzgFT8+GXzqJuoDd0CBncxCY278u4mhRO7tMgo2JjgJA5aZ+nWSQ/Z+xg==", + "dev": true, + "license": "MIT", + "dependencies": { + "tslib": "^2.1.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "graphql": "^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0" + } + }, "node_modules/handlebars": { "version": "4.7.8", "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.8.tgz", @@ -19494,6 +20157,51 @@ "he": "bin/he" } }, + "node_modules/help-me": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/help-me/-/help-me-4.2.0.tgz", + "integrity": "sha512-TAOnTB8Tz5Dw8penUuzHVrKNKlCIbwwbHnXraNJxPwf8LRtE2HlM84RYuezMFcwOJmoYOCWVDyJ8TQGxn9PgxA==", + "dev": true, + "license": "MIT", + "dependencies": { + "glob": "^8.0.0", + "readable-stream": "^3.6.0" + } + }, + "node_modules/help-me/node_modules/glob": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-8.1.0.tgz", + "integrity": "sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "dev": true, + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^5.0.1", + "once": "^1.3.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/help-me/node_modules/minimatch": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", + "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/hoopy": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/hoopy/-/hoopy-0.1.4.tgz", @@ -19575,6 +20283,38 @@ "entities": "^4.5.0" } }, + "node_modules/http-errors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", + "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "depd": "2.0.0", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "toidentifier": "1.0.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/http-proxy": { + "version": "1.18.1", + "resolved": "https://registry.npmjs.org/http-proxy/-/http-proxy-1.18.1.tgz", + "integrity": "sha512-7mz/721AbnJwIVbnaSv1Cz3Am0ZLT/UBwkC92VlxhXv/k/BBQfM2fXElQNC27BVGr0uwUpplYPQM9LnaBMR5NQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "eventemitter3": "^4.0.0", + "follow-redirects": "^1.0.0", + "requires-port": "^1.0.0" + }, + "engines": { + "node": ">=8.0.0" + } + }, "node_modules/http-proxy-agent": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-5.0.0.tgz", @@ -19791,6 +20531,16 @@ "integrity": "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==", "license": "BSD-3-Clause" }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, "node_modules/is": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/is/-/is-3.3.0.tgz", @@ -21184,6 +21934,16 @@ "url": "https://github.com/sponsors/panva" } }, + "node_modules/joycon": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/joycon/-/joycon-3.1.1.tgz", + "integrity": "sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, "node_modules/js-base64": { "version": "3.7.7", "resolved": "https://registry.npmjs.org/js-base64/-/js-base64-3.7.7.tgz", @@ -21936,6 +22696,26 @@ "integrity": "sha512-/sKlQJCBYVY9Ers9hqzKou4H6V5UWc/M59TH2dvkt+84itfnq7uFOMLpOiOS4ujvHP4etln18fmIxA5R5fll0g==", "license": "MIT" }, + "node_modules/media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/merge-descriptors": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", + "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/merge-stream": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", @@ -21953,6 +22733,16 @@ "node": ">= 8" } }, + "node_modules/methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, "node_modules/micromatch": { "version": "4.0.8", "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", @@ -21967,6 +22757,19 @@ "node": ">=8.6" } }, + "node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "dev": true, + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/mime-db": { "version": "1.52.0", "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", @@ -22533,6 +23336,16 @@ "node": ">=0.10.0" } }, + "node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, "node_modules/neo-async": { "version": "2.6.2", "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", @@ -22782,6 +23595,18 @@ "webidl-conversions": "^3.0.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", + "integrity": "sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ==", + "dev": true, + "license": "MIT", + "bin": { + "node-gyp-build": "bin.js", + "node-gyp-build-optional": "optional.js", + "node-gyp-build-test": "build-test.js" + } + }, "node_modules/node-int64": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", @@ -23049,15 +23874,38 @@ "integrity": "sha512-TBu7PtV8YkAZn0tSxobKY2n2aAQva936lhRrj6957aDaCf9IEtqsKbgMzXE/F/sjqYOwmrukeORHNLe5glk7Cg==", "license": "MIT" }, - "node_modules/once": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", - "license": "ISC", - "dependencies": { - "wrappy": "1" - } - }, + "node_modules/on-exit-leak-free": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/on-exit-leak-free/-/on-exit-leak-free-2.1.2.tgz", + "integrity": "sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "dev": true, + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, "node_modules/one-time": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/one-time/-/one-time-1.0.0.tgz", @@ -23672,6 +24520,16 @@ "url": "https://github.com/inikulin/parse5?sponsor=1" } }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/path": { "version": "0.12.7", "resolved": "https://registry.npmjs.org/path/-/path-0.12.7.tgz", @@ -23855,6 +24713,167 @@ "node": ">=0.10.0" } }, + "node_modules/pino": { + "version": "8.21.0", + "resolved": "https://registry.npmjs.org/pino/-/pino-8.21.0.tgz", + "integrity": "sha512-ip4qdzjkAyDDZklUaZkcRFb2iA118H9SgRh8yzTkSQK8HilsOJF7rSY8HoW5+I0M46AZgX/pxbprf2vvzQCE0Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "atomic-sleep": "^1.0.0", + "fast-redact": "^3.1.1", + "on-exit-leak-free": "^2.1.0", + "pino-abstract-transport": "^1.2.0", + "pino-std-serializers": "^6.0.0", + "process-warning": "^3.0.0", + "quick-format-unescaped": "^4.0.3", + "real-require": "^0.2.0", + "safe-stable-stringify": "^2.3.1", + "sonic-boom": "^3.7.0", + "thread-stream": "^2.6.0" + }, + "bin": { + "pino": "bin.js" + } + }, + "node_modules/pino-abstract-transport": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/pino-abstract-transport/-/pino-abstract-transport-1.2.0.tgz", + "integrity": "sha512-Guhh8EZfPCfH+PMXAb6rKOjGQEoy0xlAIn+irODG5kgfYV+BQ0rGYYWTIel3P5mmyXqkYkPmdIkywsn6QKUR1Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "readable-stream": "^4.0.0", + "split2": "^4.0.0" + } + }, + "node_modules/pino-abstract-transport/node_modules/buffer": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "dev": true, + "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/pino-abstract-transport/node_modules/readable-stream": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.7.0.tgz", + "integrity": "sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg==", + "dev": true, + "license": "MIT", + "dependencies": { + "abort-controller": "^3.0.0", + "buffer": "^6.0.3", + "events": "^3.3.0", + "process": "^0.11.10", + "string_decoder": "^1.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/pino-pretty": { + "version": "9.4.1", + "resolved": "https://registry.npmjs.org/pino-pretty/-/pino-pretty-9.4.1.tgz", + "integrity": "sha512-loWr5SNawVycvY//hamIzyz3Fh5OSpvkcO13MwdDW+eKIGylobPLqnVGTDwDXkdmpJd1BhEG+qhDw09h6SqJiQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "colorette": "^2.0.7", + "dateformat": "^4.6.3", + "fast-copy": "^3.0.0", + "fast-safe-stringify": "^2.1.1", + "help-me": "^4.0.1", + "joycon": "^3.1.1", + "minimist": "^1.2.6", + "on-exit-leak-free": "^2.1.0", + "pino-abstract-transport": "^1.0.0", + "pump": "^3.0.0", + "readable-stream": "^4.0.0", + "secure-json-parse": "^2.4.0", + "sonic-boom": "^3.0.0", + "strip-json-comments": "^3.1.1" + }, + "bin": { + "pino-pretty": "bin.js" + } + }, + "node_modules/pino-pretty/node_modules/buffer": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "dev": true, + "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/pino-pretty/node_modules/dateformat": { + "version": "4.6.3", + "resolved": "https://registry.npmjs.org/dateformat/-/dateformat-4.6.3.tgz", + "integrity": "sha512-2P0p0pFGzHS5EMnhdxQi7aJN+iMheud0UhG4dlE1DLAlvL8JHjJJTX/CSm4JXwV0Ka5nGk3zC5mcb5bUQUxxMA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/pino-pretty/node_modules/readable-stream": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.7.0.tgz", + "integrity": "sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg==", + "dev": true, + "license": "MIT", + "dependencies": { + "abort-controller": "^3.0.0", + "buffer": "^6.0.3", + "events": "^3.3.0", + "process": "^0.11.10", + "string_decoder": "^1.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/pino-std-serializers": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/pino-std-serializers/-/pino-std-serializers-6.2.2.tgz", + "integrity": "sha512-cHjPPsE+vhj/tnhCy/wiMh3M3z3h/j15zHQX+S9GkTBgqJuTuJzYJ4gUyACLhDaJ7kk9ba9iRDmbH2tJU03OiA==", + "dev": true, + "license": "MIT" + }, "node_modules/pirates": { "version": "4.0.7", "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz", @@ -24373,6 +25392,13 @@ "node": ">= 0.6.0" } }, + "node_modules/process-warning": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/process-warning/-/process-warning-3.0.0.tgz", + "integrity": "sha512-mqn0kFRl0EoqhnL0GQ0veqFHyIN1yig9RHh/InzORTUiZHFRAur+aMtRkELNwGs9aNwKS6tg/An4NYBPGwvtzQ==", + "dev": true, + "license": "MIT" + }, "node_modules/progress": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", @@ -24450,6 +25476,20 @@ "node": ">=8" } }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, "node_modules/proxy-agent": { "version": "6.3.1", "resolved": "https://registry.npmjs.org/proxy-agent/-/proxy-agent-6.3.1.tgz", @@ -24753,6 +25793,22 @@ "node": ">=6" } }, + "node_modules/qs": { + "version": "6.13.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz", + "integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.0.6" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/querystringify": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz", @@ -24780,6 +25836,38 @@ ], "license": "MIT" }, + "node_modules/quick-format-unescaped": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/quick-format-unescaped/-/quick-format-unescaped-4.0.4.tgz", + "integrity": "sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg==", + "dev": true, + "license": "MIT" + }, + "node_modules/ramda": { + "version": "0.30.1", + "resolved": "https://registry.npmjs.org/ramda/-/ramda-0.30.1.tgz", + "integrity": "sha512-tEF5I22zJnuclswcZMc8bDIrwRHRzf+NqVEmqg50ShAZMP7MWeR/RGDthfM/p+BlqvF2fXAzpn8i+SJcYD3alw==", + "dev": true, + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/ramda" + } + }, + "node_modules/randexp": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/randexp/-/randexp-0.5.3.tgz", + "integrity": "sha512-U+5l2KrcMNOUPYvazA3h5ekF80FHTUG+87SEAmHZmolh1M+i/WyTCxVzmi+tidIa1tM4BSe8g2Y/D3loWDjj+w==", + "dev": true, + "license": "MIT", + "dependencies": { + "drange": "^1.0.2", + "ret": "^0.2.0" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/randombytes": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", @@ -24791,6 +25879,45 @@ "safe-buffer": "^5.1.0" } }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz", + "integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==", + "dev": true, + "license": "MIT", + "dependencies": { + "bytes": "3.1.2", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/raw-body/node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "dev": true, + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/rc9": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/rc9/-/rc9-2.1.2.tgz", @@ -25074,6 +26201,16 @@ "url": "https://paulmillr.com/funding/" } }, + "node_modules/real-require": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/real-require/-/real-require-0.2.0.tgz", + "integrity": "sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 12.13.0" + } + }, "node_modules/redent": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/redent/-/redent-3.0.0.tgz", @@ -25303,6 +26440,16 @@ "node": ">=10" } }, + "node_modules/ret": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/ret/-/ret-0.2.2.tgz", + "integrity": "sha512-M0b3YWQs7R3Z917WRQy1HHA7Ba7D8hvZg6UE5mLykJxQVE2ju0IXbGlaHPPlkY+WN7wFP+wUMXmBFA0aV6vYGQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, "node_modules/retry": { "version": "0.12.0", "resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz", @@ -25562,6 +26709,13 @@ "node": "^14.0.0 || >=16.0.0" } }, + "node_modules/secure-json-parse": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/secure-json-parse/-/secure-json-parse-2.7.0.tgz", + "integrity": "sha512-6aU+Rwsezw7VR8/nyvKTx8QpWH9FrcYiXXlqC4z5d5XQBDRqtbfsRjnwGyqbi3gddNtWHuEk9OANUotL26qKUw==", + "dev": true, + "license": "BSD-3-Clause" + }, "node_modules/semver": { "version": "7.7.1", "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.1.tgz", @@ -25574,6 +26728,58 @@ "node": ">=10" } }, + "node_modules/send": { + "version": "0.19.0", + "resolved": "https://registry.npmjs.org/send/-/send-0.19.0.tgz", + "integrity": "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "mime": "1.6.0", + "ms": "2.1.3", + "on-finished": "2.4.1", + "range-parser": "~1.2.1", + "statuses": "2.0.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/send/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/send/node_modules/debug/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true, + "license": "MIT" + }, + "node_modules/send/node_modules/encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/serialize-javascript": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.2.tgz", @@ -25585,6 +26791,22 @@ "randombytes": "^2.1.0" } }, + "node_modules/serve-static": { + "version": "1.16.2", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.2.tgz", + "integrity": "sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==", + "dev": true, + "license": "MIT", + "dependencies": { + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "0.19.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, "node_modules/set-blocking": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", @@ -25637,6 +26859,13 @@ "node": ">= 0.4" } }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "dev": true, + "license": "ISC" + }, "node_modules/sharp": { "version": "0.33.5", "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.33.5.tgz", @@ -25912,6 +27141,16 @@ "node": ">= 14" } }, + "node_modules/sonic-boom": { + "version": "3.8.1", + "resolved": "https://registry.npmjs.org/sonic-boom/-/sonic-boom-3.8.1.tgz", + "integrity": "sha512-y4Z8LCDBuum+PBP3lSV7RHrXscqksve/bi0as7mhwVnBW+/wUqKT/2Kb7um8yqcFy0duYbbPxzt89Zy2nOCaxg==", + "dev": true, + "license": "MIT", + "dependencies": { + "atomic-sleep": "^1.0.0" + } + }, "node_modules/source-map": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", @@ -25977,9 +27216,19 @@ "dev": true, "license": "CC0-1.0" }, - "node_modules/sprintf-js": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "node_modules/split2": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", + "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">= 10.x" + } + }, + "node_modules/sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", "dev": true, "license": "BSD-3-Clause" @@ -26144,6 +27393,16 @@ "node": ">= 0.8.0" } }, + "node_modules/statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/streamsearch": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz", @@ -26651,6 +27910,16 @@ "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", "license": "MIT" }, + "node_modules/thread-stream": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/thread-stream/-/thread-stream-2.7.0.tgz", + "integrity": "sha512-qQiRWsU/wvNolI6tbbCKd9iKaTnCXsTwVxhhKM6nctPdujTyztjlbUkUTUymidWcMnZ5pWR0ej4a0tjsW021vw==", + "dev": true, + "license": "MIT", + "dependencies": { + "real-require": "^0.2.0" + } + }, "node_modules/through": { "version": "2.3.8", "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", @@ -26748,6 +28017,16 @@ "node": ">=8.0" } }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, "node_modules/tough-cookie": { "version": "4.1.4", "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.1.4.tgz", @@ -27495,6 +28774,20 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, "node_modules/typed-array-buffer": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.3.tgz", @@ -27706,6 +28999,16 @@ "node": ">= 4.0.0" } }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/unrs-resolver": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/unrs-resolver/-/unrs-resolver-1.4.1.tgz", @@ -27881,6 +29184,16 @@ "integrity": "sha512-x00IRNXNy63jwGkJmzPigoySHbaqpNuzKbBOmzK+g2OdZpQ9w+sxCN+VSB3ja7IAge2OP2qpfxTjeNcyjmW1uw==", "license": "ISC" }, + "node_modules/utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4.0" + } + }, "node_modules/uuid": { "version": "9.0.1", "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", @@ -27937,6 +29250,16 @@ "node": ">= 0.10" } }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/vizion": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/vizion/-/vizion-2.2.1.tgz", @@ -28455,6 +29778,16 @@ "url": "https://github.com/sponsors/colinhacks" } }, + "node_modules/zod-to-json-schema": { + "version": "3.24.6", + "resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.24.6.tgz", + "integrity": "sha512-h/z3PKvcTcTetyjl1fkj79MHNEjm+HpD6NXheWjzOekY7kV+lwDYnHw+ivHkijnCSMz1yJaWBD9vu/Fcmk+vEg==", + "dev": true, + "license": "ISC", + "peerDependencies": { + "zod": "^3.24.1" + } + }, "tests/accessibility": { "name": "nhs-notify-web-template-management-accessibility-test", "version": "0.0.1", @@ -28473,6 +29806,510 @@ "tsx": "^4.19.3" } }, + "tests/contracts": { + "name": "contract-tests", + "version": "1.0.0", + "dependencies": { + "zod": "^3.24.2" + }, + "devDependencies": { + "@apidevtools/json-schema-ref-parser": "^14.0.3", + "@pact-foundation/pact": "^15.0.1", + "@sourcemeta/jsonschema": "^9.6.0", + "@swc/core": "^1.11.13", + "@swc/jest": "^0.2.37", + "@tsconfig/node20": "^20.1.5", + "jest": "^29.7.0", + "tsx": "^4.20.3", + "zod-to-json-schema": "^3.24.6" + } + }, + "tests/contracts/node_modules/@esbuild/aix-ppc64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.5.tgz", + "integrity": "sha512-9o3TMmpmftaCMepOdA5k/yDw8SfInyzWWTjYTFCX3kPSDJMROQTb8jg+h9Cnwnmm1vOzvxN7gIfB5V2ewpjtGA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "tests/contracts/node_modules/@esbuild/android-arm": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.5.tgz", + "integrity": "sha512-AdJKSPeEHgi7/ZhuIPtcQKr5RQdo6OO2IL87JkianiMYMPbCtot9fxPbrMiBADOWWm3T2si9stAiVsGbTQFkbA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "tests/contracts/node_modules/@esbuild/android-arm64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.5.tgz", + "integrity": "sha512-VGzGhj4lJO+TVGV1v8ntCZWJktV7SGCs3Pn1GRWI1SBFtRALoomm8k5E9Pmwg3HOAal2VDc2F9+PM/rEY6oIDg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "tests/contracts/node_modules/@esbuild/android-x64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.5.tgz", + "integrity": "sha512-D2GyJT1kjvO//drbRT3Hib9XPwQeWd9vZoBJn+bu/lVsOZ13cqNdDeqIF/xQ5/VmWvMduP6AmXvylO/PIc2isw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "tests/contracts/node_modules/@esbuild/darwin-arm64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.5.tgz", + "integrity": "sha512-GtaBgammVvdF7aPIgH2jxMDdivezgFu6iKpmT+48+F8Hhg5J/sfnDieg0aeG/jfSvkYQU2/pceFPDKlqZzwnfQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "tests/contracts/node_modules/@esbuild/darwin-x64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.5.tgz", + "integrity": "sha512-1iT4FVL0dJ76/q1wd7XDsXrSW+oLoquptvh4CLR4kITDtqi2e/xwXwdCVH8hVHU43wgJdsq7Gxuzcs6Iq/7bxQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "tests/contracts/node_modules/@esbuild/freebsd-arm64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.5.tgz", + "integrity": "sha512-nk4tGP3JThz4La38Uy/gzyXtpkPW8zSAmoUhK9xKKXdBCzKODMc2adkB2+8om9BDYugz+uGV7sLmpTYzvmz6Sw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "tests/contracts/node_modules/@esbuild/freebsd-x64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.5.tgz", + "integrity": "sha512-PrikaNjiXdR2laW6OIjlbeuCPrPaAl0IwPIaRv+SMV8CiM8i2LqVUHFC1+8eORgWyY7yhQY+2U2fA55mBzReaw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "tests/contracts/node_modules/@esbuild/linux-arm": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.5.tgz", + "integrity": "sha512-cPzojwW2okgh7ZlRpcBEtsX7WBuqbLrNXqLU89GxWbNt6uIg78ET82qifUy3W6OVww6ZWobWub5oqZOVtwolfw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "tests/contracts/node_modules/@esbuild/linux-arm64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.5.tgz", + "integrity": "sha512-Z9kfb1v6ZlGbWj8EJk9T6czVEjjq2ntSYLY2cw6pAZl4oKtfgQuS4HOq41M/BcoLPzrUbNd+R4BXFyH//nHxVg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "tests/contracts/node_modules/@esbuild/linux-ia32": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.5.tgz", + "integrity": "sha512-sQ7l00M8bSv36GLV95BVAdhJ2QsIbCuCjh/uYrWiMQSUuV+LpXwIqhgJDcvMTj+VsQmqAHL2yYaasENvJ7CDKA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "tests/contracts/node_modules/@esbuild/linux-loong64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.5.tgz", + "integrity": "sha512-0ur7ae16hDUC4OL5iEnDb0tZHDxYmuQyhKhsPBV8f99f6Z9KQM02g33f93rNH5A30agMS46u2HP6qTdEt6Q1kg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "tests/contracts/node_modules/@esbuild/linux-mips64el": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.5.tgz", + "integrity": "sha512-kB/66P1OsHO5zLz0i6X0RxlQ+3cu0mkxS3TKFvkb5lin6uwZ/ttOkP3Z8lfR9mJOBk14ZwZ9182SIIWFGNmqmg==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "tests/contracts/node_modules/@esbuild/linux-ppc64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.5.tgz", + "integrity": "sha512-UZCmJ7r9X2fe2D6jBmkLBMQetXPXIsZjQJCjgwpVDz+YMcS6oFR27alkgGv3Oqkv07bxdvw7fyB71/olceJhkQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "tests/contracts/node_modules/@esbuild/linux-riscv64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.5.tgz", + "integrity": "sha512-kTxwu4mLyeOlsVIFPfQo+fQJAV9mh24xL+y+Bm6ej067sYANjyEw1dNHmvoqxJUCMnkBdKpvOn0Ahql6+4VyeA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "tests/contracts/node_modules/@esbuild/linux-s390x": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.5.tgz", + "integrity": "sha512-K2dSKTKfmdh78uJ3NcWFiqyRrimfdinS5ErLSn3vluHNeHVnBAFWC8a4X5N+7FgVE1EjXS1QDZbpqZBjfrqMTQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "tests/contracts/node_modules/@esbuild/linux-x64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.5.tgz", + "integrity": "sha512-uhj8N2obKTE6pSZ+aMUbqq+1nXxNjZIIjCjGLfsWvVpy7gKCOL6rsY1MhRh9zLtUtAI7vpgLMK6DxjO8Qm9lJw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "tests/contracts/node_modules/@esbuild/netbsd-arm64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.5.tgz", + "integrity": "sha512-pwHtMP9viAy1oHPvgxtOv+OkduK5ugofNTVDilIzBLpoWAM16r7b/mxBvfpuQDpRQFMfuVr5aLcn4yveGvBZvw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "tests/contracts/node_modules/@esbuild/netbsd-x64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.5.tgz", + "integrity": "sha512-WOb5fKrvVTRMfWFNCroYWWklbnXH0Q5rZppjq0vQIdlsQKuw6mdSihwSo4RV/YdQ5UCKKvBy7/0ZZYLBZKIbwQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "tests/contracts/node_modules/@esbuild/openbsd-arm64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.5.tgz", + "integrity": "sha512-7A208+uQKgTxHd0G0uqZO8UjK2R0DDb4fDmERtARjSHWxqMTye4Erz4zZafx7Di9Cv+lNHYuncAkiGFySoD+Mw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "tests/contracts/node_modules/@esbuild/openbsd-x64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.5.tgz", + "integrity": "sha512-G4hE405ErTWraiZ8UiSoesH8DaCsMm0Cay4fsFWOOUcz8b8rC6uCvnagr+gnioEjWn0wC+o1/TAHt+It+MpIMg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "tests/contracts/node_modules/@esbuild/sunos-x64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.5.tgz", + "integrity": "sha512-l+azKShMy7FxzY0Rj4RCt5VD/q8mG/e+mDivgspo+yL8zW7qEwctQ6YqKX34DTEleFAvCIUviCFX1SDZRSyMQA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "tests/contracts/node_modules/@esbuild/win32-arm64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.5.tgz", + "integrity": "sha512-O2S7SNZzdcFG7eFKgvwUEZ2VG9D/sn/eIiz8XRZ1Q/DO5a3s76Xv0mdBzVM5j5R639lXQmPmSo0iRpHqUUrsxw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "tests/contracts/node_modules/@esbuild/win32-ia32": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.5.tgz", + "integrity": "sha512-onOJ02pqs9h1iMJ1PQphR+VZv8qBMQ77Klcsqv9CNW2w6yLqoURLcgERAIurY6QE63bbLuqgP9ATqajFLK5AMQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "tests/contracts/node_modules/@esbuild/win32-x64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.5.tgz", + "integrity": "sha512-TXv6YnJ8ZMVdX+SXWVBo/0p8LTcrUYngpWjvm91TMjjBQii7Oz11Lw5lbDV5Y0TzuhSJHwiH4hEtC1I42mMS0g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "tests/contracts/node_modules/esbuild": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.5.tgz", + "integrity": "sha512-P8OtKZRv/5J5hhz0cUAdu/cLuPIKXpQl1R9pZtvmHWQvrAUVd0UNIPT4IB4W3rNOqVO0rlqHmCIbSwxh/c9yUQ==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.25.5", + "@esbuild/android-arm": "0.25.5", + "@esbuild/android-arm64": "0.25.5", + "@esbuild/android-x64": "0.25.5", + "@esbuild/darwin-arm64": "0.25.5", + "@esbuild/darwin-x64": "0.25.5", + "@esbuild/freebsd-arm64": "0.25.5", + "@esbuild/freebsd-x64": "0.25.5", + "@esbuild/linux-arm": "0.25.5", + "@esbuild/linux-arm64": "0.25.5", + "@esbuild/linux-ia32": "0.25.5", + "@esbuild/linux-loong64": "0.25.5", + "@esbuild/linux-mips64el": "0.25.5", + "@esbuild/linux-ppc64": "0.25.5", + "@esbuild/linux-riscv64": "0.25.5", + "@esbuild/linux-s390x": "0.25.5", + "@esbuild/linux-x64": "0.25.5", + "@esbuild/netbsd-arm64": "0.25.5", + "@esbuild/netbsd-x64": "0.25.5", + "@esbuild/openbsd-arm64": "0.25.5", + "@esbuild/openbsd-x64": "0.25.5", + "@esbuild/sunos-x64": "0.25.5", + "@esbuild/win32-arm64": "0.25.5", + "@esbuild/win32-ia32": "0.25.5", + "@esbuild/win32-x64": "0.25.5" + } + }, + "tests/contracts/node_modules/tsx": { + "version": "4.20.3", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.20.3.tgz", + "integrity": "sha512-qjbnuR9Tr+FJOMBqJCW5ehvIo/buZq7vH7qD7JziU98h6l3qGy0a/yPFjwO+y0/T7GFpNgNAvEcPPVfyT8rrPQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "~0.25.0", + "get-tsconfig": "^4.7.5" + }, + "bin": { + "tsx": "dist/cli.mjs" + }, + "engines": { + "node": ">=18.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + } + }, "tests/test-team": { "name": "nhs-notify-web-template-management-ui-tests", "version": "0.0.1", diff --git a/package.json b/package.json index 4a49de004..4894adc20 100644 --- a/package.json +++ b/package.json @@ -41,6 +41,16 @@ "lint": "npm run lint --workspaces", "lint:fix": "npm run lint:fix --workspaces", "start": "npm run start --workspace frontend", + "test:contracts:clean": "npm --workspace=tests/contracts run pact:clean", + "test:contracts:consumer": "npm --workspace=tests/contracts run test:consumer", + "test:contracts:consumer:golden": "npm --workspace=tests/contracts run test:consumer:golden", + "test:contracts:download:consumer": "npm --workspace=tests/contracts run pact:download:consumer", + "test:contracts:download:provider": "npm --workspace=tests/contracts run pact:download:provider", + "test:contracts:generate:provider": "npm --workspace=tests/contracts run pact:generate:provider", + "test:contracts:provider": "npm --workspace=tests/contracts run test:provider", + "test:contracts:provider:ci": "npm --workspace=tests/contracts run test:provider:ci", + "test:contracts:upload:consumer": "npm --workspace=tests/contracts run pact:upload:consumer", + "test:contracts:upload:provider": "npm --workspace=tests/contracts run pact:upload:provider", "test:unit": "npm run test:unit --workspaces", "typecheck": "npm run typecheck --workspaces" }, @@ -53,6 +63,7 @@ "lambdas/download-authorizer", "lambdas/sftp-letters", "tests/accessibility", + "tests/contracts", "tests/test-team", "utils/backend-config", "utils/entity-update-command-builder", diff --git a/scripts/config/vale/styles/config/vocabularies/words/accept.txt b/scripts/config/vale/styles/config/vocabularies/words/accept.txt index 96f75515c..15f99273a 100644 --- a/scripts/config/vale/styles/config/vocabularies/words/accept.txt +++ b/scripts/config/vale/styles/config/vocabularies/words/accept.txt @@ -4,6 +4,7 @@ bot Cognito Cyber Dependabot +dev draw.io drawio endcapture @@ -21,8 +22,10 @@ onboarding Podman Python rawContent +repo sed Syft Terraform toolchain Trufflehog +Zod diff --git a/tests/contracts/.gitignore b/tests/contracts/.gitignore new file mode 100644 index 000000000..3bacde095 --- /dev/null +++ b/tests/contracts/.gitignore @@ -0,0 +1,2 @@ +**/.pacts/**/*.json +**/.schemas/**/*.schema.json diff --git a/tests/contracts/README.md b/tests/contracts/README.md new file mode 100644 index 000000000..a5eb2546f --- /dev/null +++ b/tests/contracts/README.md @@ -0,0 +1,213 @@ +# Contract Testing Spike + +## Summary + +This spike demonstrates an event-driven contract testing framework with the following features: + +- Consumers defining their expectations with Pact +- Providers defining canonical schema contracts with JSON schema (golden contracts) +- Contract sharing with S3 +- CI enforcing both consumer-driven and provider-driven correctness + +--- + +## Terminology + +**Consumer** +The system (usually a service or Lambda) that **receives** or **reacts to** an event. In Pact contract testing, the consumer defines what kind of event payloads it can handle. + +**Provider** +The system that **produces** and emits an event. In this setup, providers define canonical JSON Schemas (golden contracts) for the events they emit. + +> N.B. A service can be both a provider and a consumer of events + +**Golden Contract** +A JSON Schema file generated and owned by the provider, representing the authoritative shape of an event. Consumers use this to validate they are handling messages correctly. + +**Pact** +A contract testing tool used for consumer-driven contracts. Consumers define what they expect from the provider, and providers verify that they meet those expectations. + +--- + +## Project Structure + +```txt +├── scripts # bash heaven +│   ├── ci-verify-provider.sh - mushes together steps required for provider-side validation in CI +│   ├── clean.sh # deletes all local generated/downloaded contract files +│   ├── download-consumer-pacts.sh # downloads pact files generated by consumers for use in provider-side pact tests +│   ├── download-golden-contracts.sh # downloads golden contracts generated by providers, for use in consumer-side validation +│   ├── generate-golden-contracts.ts # generates golden contracts for event providers +│   ├── upload-consumer-pacts.sh # uploads pact files generated by consumer-side pact tests +│   ├── upload-golden-contracts.sh # uploads generated golden contracts to s3 +│   └── verify-golden-contracts.sh # verifies client-provided example events against provider-generated golden contracts +| +├── src # pseudo source-code +│   ├── +│   │   ├── events # code related to publishing events +│   │   │   ├── .schemas # generated golden contracts - gitignored +│   │   │   │   └── .schema.json +│   │   │   | +│   │   │   └── .event.ts # code that generates an event payload +│   │   | +│   │   └── handlers # code related to consuming events +│   │   └── template-deleted.handler.ts # event parsing and handling code +| +├── tests # contract testing code +│   ├── +│   │   ├── consumer # tests for event consumption +│   │   │   ├── .pacts # generated pact files outlining consumers expectations - gitignored +│   │   │   │   └── -.json +│   │   │   | +│   │   │   ├── .schemas # downloaded golden contracts from providers - gitignored +│   │   │   │   └── +│   │   │   │   └── .schema.json +│   │   │   | +│   │   │   ├── config.json # Lists the provider(s) and event(s) that the consumer depends on. Used to fetch only the relevant golden contracts +│   │   │   | +│   │   │   ├── examples # sample json files representing what the consumer thinks it might receive. These are validated against golden contracts to catch schema mismatches +│   │   │   │   └── +│   │   │   │   └── +│   │   │   │   └── .json +│   │   │   │ +│   │   │   └── .consumer.pact.test.ts # pact test file, outlines consumers expectations and generates pact file +│   │   | +│   │   └── provider # tests for event production +│   │   ├── .pacts # downloaded pact files from consumers - gitignored +│   │   │   └── -.json +│   │   └── .provider.pact.test.ts # validates an emitted event against consumer expectations +``` + +--- + +## Scenario + +The POC defines 3 services - `auth`, `core` and `templates`, which act as event providers and consumers: + +### Service Event Responsibilities + +#### auth + +**Emits:** + +| Event | Consumed By | +|-------------|-------------| +| UserCreated | templates | + +**Consumes:** + +| Event | Emitted By | +|------------------|-------------| +| TemplateDeleted | templates | + +--- + +#### templates + +**Emits:** + +| Event | Consumed By | +|------------------|------------------| +| TemplateDeleted | auth, core | + +**Consumes:** + +| Event | Emitted By | +|-------------|-------------| +| UserCreated | auth | + +--- + +#### core + +**Emits:** _(none)_ + +**Consumes:** + +| Event | Emitted By | +|------------------|-------------| +| TemplateDeleted | templates | + +--- + +## Contract Types + +### Consumer-Driven (Pact) + +- Pact consumer tests generate `.json` contracts for expected messages. +- Stored under: `tests//consumer/.pacts/` +- Uploaded to: `s3:///pacts//` - indexed by provider for easy download by provider +- Downloaded and validated on provider-side, to ensure that the provider is meeting consumer expectations + +### Provider-Driven (Golden Contracts) + +- JSON Schemas are generated by event providers using Zod + `zod-to-json-schema`. +- Flattened at generation time to avoid `$ref` - this is a bit of a quirk +- Saved to: `src//events/.schemas/` +- Uploaded to: `s3:///golden//.schema.json` +- Downloaded and validated against example events on client-side + +## Running locally + +Ensure you are signed into AWS, and have `PACT_BUCKET` set in your environment variables. I've been using the artifact bucket in the templates dev account. + +Then from the repository root, let's test out some golden contracts: + +- Start by generating some golden contracts for the provider services - `npm run test:contracts:generate:provider` +- Upload the golden contracts to S3 - `npm run test:contracts:upload:provider` +- Download them from S3 into the consumer tests - `npm run test:contracts:download:provider` +- Validate that the consumers expectations are valid against the golden contracts - `npm run test:contracts:consumer:golden` + +Next, lets run some consumer-driven pact tests: + +- Run the consumer tests, and generate some Pact contract files - `npm run test:contracts:consumer` +- Upload those to S3 - `npm run test:contracts:upload:consumer` +- Now download those Pact contracts into the provider tests - `npm run test:contracts:download:consumer` +- And validate that the events emitted by providers match the expectations of the consumers - `npm run test:contracts:provider` + +At any point, clean up all of the locally stored contract files using `npm run test:contracts:clean` + +I apologize for the very confusing command names. + +## CI Flow + +The CI flow contains both consumer and provider test jobs - a service can be both a provider and consumer of events. + +Currently the CI bits have been plumbed into the existing CI job. Because this repo defines multiple services which emit and consume events, it loops over each service and runs the tests all at once. In reality a repo would only define a single service, and only one set of provider tests and one set of consumer tests would be executed. + +### Consumer job + +The consumer test job in CI performs the following steps: + +1. **Download golden contracts** + Based on the consumer's `config.json`, only the required event schemas are pulled from S3. + +2. **Validate consumer example payloads** + Local examples are validated against the downloaded golden contracts using `@sourcemeta/jsonschema`. If any validation fails, the job stops here - no Pact contracts are generated or uploaded. + +3. **Run Pact consumer tests** + If schema validation passes, Pact tests are run to verify that the consumer's expectations are correctly captured in the contract. + +4. **Upload Pact contracts to S3** + Validated contracts are published to S3 under a provider-specific path for use in provider-side verification. + +### Provider Job + +The provider test job in CI performs the following steps: + +1. **Download Pact contracts from S3** + All Pact contracts for the provider are downloaded from `s3:///pacts//`. These contracts are written by consumers and define their expected message shapes. (N.B. Different consumers can have different expectations!) + +2. **Run Pact provider tests** + The provider uses real message-producing code to generate event payloads, which are then validated against each downloaded Pact contract. + +3. **Skip verification gracefully if no contracts found** + If no contracts are present in S3 (i.e. if nobody is consuming any events) the job logs a warning and exits successfully without running tests. + +This job ensures that the provider is compatible with all currently published consumer expectations. + +## Next Steps + +- Try open-source, self-hosted Pact broker for sharing contracts (instead of S3) +- Think about versioning contracts (using git branch/tags/commit-sha?) +- Simplify scripts and CI - remove iteration, run steps for one service at a time diff --git a/tests/contracts/jest.config.ts b/tests/contracts/jest.config.ts new file mode 100644 index 000000000..ab426c613 --- /dev/null +++ b/tests/contracts/jest.config.ts @@ -0,0 +1,10 @@ +import type { Config } from 'jest'; + +const config: Config = { + preset: 'ts-jest', + testEnvironment: 'node', + testMatch: ['**/tests/**/*.pact.test.ts'], + transform: { '^.+\\.ts$': '@swc/jest' }, +}; + +export default config; diff --git a/tests/contracts/package.json b/tests/contracts/package.json new file mode 100644 index 000000000..81f219c0a --- /dev/null +++ b/tests/contracts/package.json @@ -0,0 +1,35 @@ +{ + "dependencies": { + "zod": "^3.24.2" + }, + "devDependencies": { + "@apidevtools/json-schema-ref-parser": "^14.0.3", + "@pact-foundation/pact": "^15.0.1", + "@sourcemeta/jsonschema": "^9.6.0", + "@swc/core": "^1.11.13", + "@swc/jest": "^0.2.37", + "@tsconfig/node20": "^20.1.5", + "jest": "^29.7.0", + "tsx": "^4.20.3", + "zod-to-json-schema": "^3.24.6" + }, + "name": "contract-tests", + "private": true, + "scripts": { + "lint": "eslint .", + "lint:fix": "eslint . --fix", + "pact:clean": "./scripts/clean.sh", + "pact:download:consumer": "./scripts/download-consumer-pacts.sh", + "pact:download:provider": "./scripts/download-golden-contracts.sh", + "pact:generate:provider": "tsx ./scripts/generate-golden-contracts.ts", + "pact:upload:consumer": "./scripts/upload-consumer-pacts.sh", + "pact:upload:provider": "./scripts/upload-golden-contracts.sh", + "test:consumer": "jest consumer.pact.test.ts", + "test:consumer:golden": "./scripts/verify-golden-contracts.sh", + "test:provider": "jest provider.pact.test.ts", + "test:provider:ci": "./scripts/ci-verify-provider.sh", + "test:unit": "echo 'No unit tests required'", + "typecheck": "tsc --noEmit" + }, + "version": "1.0.0" +} diff --git a/tests/contracts/scripts/ci-verify-provider.sh b/tests/contracts/scripts/ci-verify-provider.sh new file mode 100755 index 000000000..9f29915c1 --- /dev/null +++ b/tests/contracts/scripts/ci-verify-provider.sh @@ -0,0 +1,62 @@ +#!/bin/bash +set -euo pipefail + +script_path="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"; + +source "${script_path}/lib/consumer-pacts.sh" + +contract_tests_root_dir=$(realpath "${script_path}/..") + +SUMMARY_FILE="${GITHUB_STEP_SUMMARY:-/dev/null}" + +echo "### Pact Provider Contract Test Results" > "$SUMMARY_FILE" +echo "" > "$SUMMARY_FILE" +echo "| Provider | Consumer Pacts found? | Result |" >> "$SUMMARY_FILE" +echo "|----------|-----------------------|--------|" >> "$SUMMARY_FILE" + +providers=("auth" "templates") + +for provider in "${providers[@]}"; do + echo "Downloading Pact files for provider: ${provider}" + + count=$(download_consumer_pacts $provider) + + echo "Downloaded ${count} Pact files for provider: ${provider}" + + if [[ "$count" -gt 0 ]]; then + echo "Pact contracts found — running $provider provider contract tests..." + + if npx jest tests/$provider/provider; then + echo "| ${provider} | 🟢 ${count} contracts found | 🟢 Passed |" >> "$SUMMARY_FILE" + else + echo "| ${provider} | 🟢 ${count} contracts found | 🔴 Failed |" >> "$SUMMARY_FILE" + exit 1 + fi + else + echo "No Pact contracts found from consumers — skipping $provider provider contract tests..." + echo "| ${provider} | 🟡 0 contracts found | 🟡 Skipped |" >> "$SUMMARY_FILE" + fi +done + +echo "All provider contract tests passed" +echo "Generating golden contracts" + +echo "" >> "$SUMMARY_FILE" +echo "" >> "$SUMMARY_FILE" +echo "### Golden Contracts" >> "$SUMMARY_FILE" + +if npm run pact:generate:provider; then + echo "Generated golden contracts" + echo "🟢 Generated golden contracts" >> "$SUMMARY_FILE" +else + echo "Failed to generate golden contracts" + echo "🔴 Failed to generate golden contracts" >> "$SUMMARY_FILE" +fi + +if npm run pact:upload:provider; then + echo "Uploaded golden contracts" + echo "🟢 Uploaded golden contracts" >> "$SUMMARY_FILE" +else + echo "Failed to upload golden contracts" + echo "🔴 Failed to upload golden contracts" >> "$SUMMARY_FILE" +fi diff --git a/tests/contracts/scripts/clean.sh b/tests/contracts/scripts/clean.sh new file mode 100755 index 000000000..5f77f4a3c --- /dev/null +++ b/tests/contracts/scripts/clean.sh @@ -0,0 +1,17 @@ +#!/bin/bash +set -euo pipefail + +script_path="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" +root_path="$(realpath "$script_path/..")" + +for service_dir in "$root_path/src" "$root_path/tests"; do + if [[ -d "$service_dir" ]]; then + find "$service_dir" -type d \( -name '.pacts' -o -name '.schemas' \) | while read -r dir; do + abs_path="$(realpath "$dir")" + echo "Removing $abs_path" + rm -rf "$abs_path" + done + fi +done + +echo "All local contract files deleted successfully" diff --git a/tests/contracts/scripts/download-consumer-pacts.sh b/tests/contracts/scripts/download-consumer-pacts.sh new file mode 100755 index 000000000..8463e6729 --- /dev/null +++ b/tests/contracts/scripts/download-consumer-pacts.sh @@ -0,0 +1,17 @@ +#!/bin/bash +set -euo pipefail + +script_path="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"; + +source "${script_path}/lib/consumer-pacts.sh" + +# Downloads all consumer-generated Pact contract files for a provider, for use in producer-side tests +providers=("auth" "templates") + +for provider in "${providers[@]}"; do + echo "Downloading Pact files for provider: ${provider}" + + count=$(download_consumer_pacts $provider) + + echo "Downloaded ${count} Pact files for provider: ${provider}" +done diff --git a/tests/contracts/scripts/download-golden-contracts.sh b/tests/contracts/scripts/download-golden-contracts.sh new file mode 100755 index 000000000..c1ef2e4ae --- /dev/null +++ b/tests/contracts/scripts/download-golden-contracts.sh @@ -0,0 +1,43 @@ +#!/bin/bash +set -euo pipefail + +script_path="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"; + +test_root="$( realpath "${script_path}/../tests" )" + +for consumer_dir in "$test_root"/*/consumer; do + config="$consumer_dir/config.json" + + consumer_name=$(basename "$(dirname "$consumer_dir")") + + if [[ ! -f "$config" ]]; then + echo "No config.json in $consumer_dir — skipping" + continue + fi + + echo "Reading config for consumer: $consumer_name" + + providers=$(jq -r '.providers | keys[]' "$config") + + + for provider in $providers; do + events=$(jq -r ".providers[\"$provider\"][]" "$config") + + for event in $events; do + filename="${event}.schema.json" + s3_path="s3://$PACT_BUCKET/golden/$provider/$filename" + local_dir="$consumer_dir/.schemas/$provider" + + mkdir -p "$local_dir" + + echo "Consumer \"${consumer_name}\" needs \"${event}\" event from provider \"${provider}\" - downloading..." + if aws s3 cp "$s3_path" "$local_dir/$filename" --quiet; then + echo "Downloaded ${s3_path} to ${local_dir}/${filename}" + else + echo "Failed to download ${s3_path}" + fi + done + done +done + +echo "Downloaded golden contracts for all consumers" diff --git a/tests/contracts/scripts/generate-golden-contracts.ts b/tests/contracts/scripts/generate-golden-contracts.ts new file mode 100644 index 000000000..c2e223cc9 --- /dev/null +++ b/tests/contracts/scripts/generate-golden-contracts.ts @@ -0,0 +1,49 @@ +import fs from 'node:fs'; +import path from 'node:path'; +import $RefParser from '@apidevtools/json-schema-ref-parser'; // eslint-disable-line import/no-extraneous-dependencies +import { zodToJsonSchema } from 'zod-to-json-schema'; // eslint-disable-line import/no-extraneous-dependencies +import { $TemplateDeletedEvent } from '../src/templates/events/template-deleted.event'; +import { $UserCreatedEvent } from '../src/auth/events/user-created.event'; + +// eslint-disable-next-line unicorn/prefer-top-level-await +(async function main() { + // Create template service golden contracts + const templateSchemasPath = path.resolve( + __dirname, + '../src/templates/events/.schemas' + ); + fs.mkdirSync(templateSchemasPath, { recursive: true }); + + const TemplateDeletedSchema = zodToJsonSchema( + $TemplateDeletedEvent, + 'TemplateDeletedEvent' + ); + const templateDeletedPath = path.join( + templateSchemasPath, + 'TemplateDeleted.schema.json' + ); + fs.writeFileSync( + templateDeletedPath, + JSON.stringify(await $RefParser.dereference(TemplateDeletedSchema), null, 2) + ); + console.log('Created JSONSchema file at', templateDeletedPath); + + // Create auth service golden contracts + const authSchemasPath = path.resolve( + __dirname, + '../src/auth/events/.schemas' + ); + fs.mkdirSync(authSchemasPath, { recursive: true }); + + const UserCreatedSchema = zodToJsonSchema( + $UserCreatedEvent, + 'UserCreatedEvent' + ); + + const userCreatedPath = path.join(authSchemasPath, 'UserCreated.schema.json'); + fs.writeFileSync( + userCreatedPath, + JSON.stringify(await $RefParser.dereference(UserCreatedSchema), null, 2) + ); + console.log('Created JSONSchema file at', userCreatedPath); +})(); diff --git a/tests/contracts/scripts/lib/consumer-pacts.sh b/tests/contracts/scripts/lib/consumer-pacts.sh new file mode 100644 index 000000000..30b5c475b --- /dev/null +++ b/tests/contracts/scripts/lib/consumer-pacts.sh @@ -0,0 +1,16 @@ +function download_consumer_pacts { + local provider=$1 + + local script_path="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"; + local contract_tests_root_dir=$(realpath "${script_path}/../..") + local target_dir="${contract_tests_root_dir}/tests/${provider}/provider/.pacts" + + rm -rf $target_dir + mkdir -p "$target_dir" + + aws s3 sync "s3://$PACT_BUCKET/pacts/$provider/" "$target_dir/" \ + --exclude "*" \ + --include "*.json" 1>/dev/null + + find "$target_dir" -maxdepth 1 -name '*.json' -type f | wc -l | xargs +} diff --git a/tests/contracts/scripts/upload-consumer-pacts.sh b/tests/contracts/scripts/upload-consumer-pacts.sh new file mode 100755 index 000000000..d010eb961 --- /dev/null +++ b/tests/contracts/scripts/upload-consumer-pacts.sh @@ -0,0 +1,33 @@ +#!/bin/bash +set -euo pipefail + +# Uploads Pact contract files generated by running consumer Pact tests +# These files should be downloaded by the provider for use in provider tests + +script_path="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"; + +consumer_service_dirs=("auth" "core" "templates") +consumer_pact_dir="consumer/.pacts" + +for consumer in "${consumer_service_dirs[@]}"; do + source="${script_path}/../tests/${consumer}/${consumer_pact_dir}" + + if [ -d "${source}" ]; then + source_dir=$(realpath "${source}") + + echo "Looking for pact files in $source_dir..." + + for file in "$source_dir"/*.json; do + if [[ -f "$file" ]]; then + filename=$(basename "$file") + provider=$(cat $file | jq -r ".provider.name") + + target_s3_key="pacts/$provider/$filename" + + aws s3 cp "$file" "s3://$PACT_BUCKET/$target_s3_key" + fi + done + fi +done + +echo "All Pact files uploaded successfully." diff --git a/tests/contracts/scripts/upload-golden-contracts.sh b/tests/contracts/scripts/upload-golden-contracts.sh new file mode 100755 index 000000000..d4c6e53e2 --- /dev/null +++ b/tests/contracts/scripts/upload-golden-contracts.sh @@ -0,0 +1,17 @@ +#!/bin/bash +set -euo pipefail + +script_path="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"; +src_root="$( realpath "${script_path}/../src" )" + +find "${src_root}" -type f -path "*/events/.schemas/*.schema.json" | while read -r filepath; do + relative_path="${filepath#$src_root/}" + provider="${relative_path%%/*}" + + s3_path="s3://$PACT_BUCKET/golden/$provider/$(basename "$filepath")" + + echo "Uploading $filepath to $s3_path" + aws s3 cp "$filepath" "$s3_path" +done + +echo "Uploaded all golden contracts to s3" diff --git a/tests/contracts/scripts/verify-golden-contracts.sh b/tests/contracts/scripts/verify-golden-contracts.sh new file mode 100755 index 000000000..0fa430958 --- /dev/null +++ b/tests/contracts/scripts/verify-golden-contracts.sh @@ -0,0 +1,78 @@ +#!/bin/bash +set -euo pipefail + +script_path="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" +test_root="$(realpath "$script_path/../tests")" + +summary_file="${GITHUB_STEP_SUMMARY:-/dev/null}" + +total_failures=0 +total_skipped=0 +total_passed=0 + +echo "### Golden Contract Validation - Per-Event Results" >> "$summary_file" +echo "| Consumer | Provider | Event | Result |" >> "$summary_file" +echo "|----------|----------|--------|--------|" >> "$summary_file" + + +for consumer_dir in "$test_root"/*/consumer; do + consumer_name=$(basename "$(dirname "$consumer_dir")") + schemas_dir="$consumer_dir/.schemas" + examples_dir="$consumer_dir/examples" + + if [[ ! -d "$schemas_dir" ]]; then + echo "No golden schemas downloaded for $consumer_name — skipping" + total_skipped=$((total_skipped + 1)) + echo "| $consumer_name | $provider | $event | ⚠️ Skipped |" >> "$summary_file" + continue + fi + + echo "Validating consumer: $consumer_name" + + while IFS= read -r -d '' schema_path; do + provider=$(basename "$(dirname "$schema_path")") + schema_file=$(basename "$schema_path") + event="${schema_file%.schema.json}" + + example_dir="$examples_dir/$provider/$event" + + echo $example_dir + + if [[ ! -d "$example_dir" ]]; then + echo "No examples exist for $event from $provider — skipping" + echo "| $consumer_name | $provider | $event | ⚠️ Skipped |" >> "$summary_file" + + total_skipped=$((total_skipped + 1)) + continue + fi + + for example in "$example_dir"/*.json; do + echo " Validating $example against $schema_path" + cd $( dirname $schema_path ) + if npx jsonschema validate "$schema_path" "$example"; then + echo "✅ Valid" + echo "| $consumer_name | $provider | $event | ✅ Pass |" >> "$summary_file" + total_passed=$((total_passed + 1)) + else + echo "❌ Invalid" + echo "| $consumer_name | $provider | $event | ❌ Failed |" >> "$summary_file" + total_failures=$((total_failures + 1)) + fi + done + done < <(find "$schemas_dir" -type f -name '*.schema.json' -print0) +done + +echo "Schema validation results: $total_passed passed, $total_failures failed, $total_skipped skipped" + +echo "" >> "$summary_file" +echo "### Golden Contract Validation Summary" >> "$summary_file" +echo "| Status | Count |" >> "$summary_file" +echo "|--------|-------|" >> "$summary_file" +echo "| ✅ Passed | $total_passed |" >> "$summary_file" +echo "| ❌ Failed | $total_failures |" >> "$summary_file" +echo "| ⚠️ Skipped | $total_skipped |" >> "$summary_file" + + +if [[ "$total_failures" -gt 0 ]]; then + exit 1 +fi diff --git a/tests/contracts/src/auth/events/user-created.event.ts b/tests/contracts/src/auth/events/user-created.event.ts new file mode 100644 index 000000000..561254979 --- /dev/null +++ b/tests/contracts/src/auth/events/user-created.event.ts @@ -0,0 +1,28 @@ +import { randomUUID } from 'node:crypto'; +import { z } from 'zod'; + +export const $UserCreatedEvent = z.object({ + 'detail-type': z.literal('UserCreated'), + source: z.literal('uk.nhs.notify.auth'), + time: z.string().datetime(), + version: z.string().default('1.0'), + detail: z.object({ + clientId: z.string().uuid(), + userId: z.string().uuid(), + }), +}); + +export type UserCreatedEvent = z.infer; + +export function createUserCreatedEvent(): UserCreatedEvent { + return $UserCreatedEvent.parse({ + 'detail-type': 'UserCreated', + source: 'uk.nhs.notify.auth', + time: new Date().toISOString(), + version: '1.0', + detail: { + clientId: randomUUID(), + userId: randomUUID(), + }, + }); +} diff --git a/tests/contracts/src/auth/handlers/template-deleted.handler.ts b/tests/contracts/src/auth/handlers/template-deleted.handler.ts new file mode 100644 index 000000000..895055377 --- /dev/null +++ b/tests/contracts/src/auth/handlers/template-deleted.handler.ts @@ -0,0 +1,16 @@ +import { z } from 'zod'; + +export const $TemplateDeletedEvent = z.object({ + 'detail-type': z.literal('TemplateDeleted'), + version: z.literal('1.0'), + detail: z.object({ + id: z.string().uuid(), + owner: z.string().uuid(), + }), +}); + +export async function handleTemplateDeleted(event: unknown): Promise { + $TemplateDeletedEvent.parse(event); + + // Handler logic goes here +} diff --git a/tests/contracts/src/core/handlers/template-deleted.handler.ts b/tests/contracts/src/core/handlers/template-deleted.handler.ts new file mode 100644 index 000000000..54e88d670 --- /dev/null +++ b/tests/contracts/src/core/handlers/template-deleted.handler.ts @@ -0,0 +1,20 @@ +import { z } from 'zod'; + +// Handlers should define their own minimal input validators +// Decoupled from implementations in other services +// So this is slightly different to what's defined in the templates service which produces the event +// And different to the TemplateDeleted handler in the auth service + +export const $TemplateDeletedEvent = z.object({ + 'detail-type': z.literal('TemplateDeleted'), + detail: z.object({ + id: z.string().uuid(), + owner: z.string().uuid(), + }), +}); + +export async function handleTemplateDeleted(event: unknown): Promise { + $TemplateDeletedEvent.parse(event); + + // Handler logic goes here +} diff --git a/tests/contracts/src/templates/events/template-deleted.event.ts b/tests/contracts/src/templates/events/template-deleted.event.ts new file mode 100644 index 000000000..0ced4d810 --- /dev/null +++ b/tests/contracts/src/templates/events/template-deleted.event.ts @@ -0,0 +1,28 @@ +import { randomUUID } from 'node:crypto'; +import { z } from 'zod'; + +export const $TemplateDeletedEvent = z.object({ + 'detail-type': z.literal('TemplateDeleted'), + source: z.literal('uk.nhs.notify.templates'), + time: z.string().datetime(), + version: z.string().default('1.0'), + detail: z.object({ + id: z.string().uuid(), + owner: z.string().uuid(), + }), +}); + +export type TemplateDeletedEvent = z.infer; + +export function createTemplateDeletedEvent(): TemplateDeletedEvent { + return $TemplateDeletedEvent.parse({ + 'detail-type': 'TemplateDeleted', + source: 'uk.nhs.notify.templates', + time: new Date().toISOString(), + version: '1.0', + detail: { + owner: randomUUID(), + id: randomUUID(), + }, + }); +} diff --git a/tests/contracts/src/templates/handlers/user-created.handler.ts b/tests/contracts/src/templates/handlers/user-created.handler.ts new file mode 100644 index 000000000..25e433c5e --- /dev/null +++ b/tests/contracts/src/templates/handlers/user-created.handler.ts @@ -0,0 +1,17 @@ +import { z } from 'zod'; + +// I guess this would be defined in the handler source code in the event consumer and imported here +// Handlers should parse the incoming event before doing anything else +export const $UserCreatedEvent = z.object({ + 'detail-type': z.literal('UserCreated'), + detail: z.object({ + userId: z.string().uuid(), + clientId: z.string().uuid(), + }), +}); + +export async function handleUserCreatedEvent(event: unknown): Promise { + $UserCreatedEvent.parse(event); + + // Handler logic goes here +} diff --git a/tests/contracts/tests/auth/consumer/config.json b/tests/contracts/tests/auth/consumer/config.json new file mode 100644 index 000000000..55b55bef2 --- /dev/null +++ b/tests/contracts/tests/auth/consumer/config.json @@ -0,0 +1,7 @@ +{ + "providers": { + "templates": [ + "TemplateDeleted" + ] + } +} diff --git a/tests/contracts/tests/auth/consumer/examples/templates/TemplateDeleted/example.json b/tests/contracts/tests/auth/consumer/examples/templates/TemplateDeleted/example.json new file mode 100644 index 000000000..deb6a795a --- /dev/null +++ b/tests/contracts/tests/auth/consumer/examples/templates/TemplateDeleted/example.json @@ -0,0 +1,10 @@ +{ + "detail": { + "id": "a6f7d152-fd5d-4aef-a12b-9d071fe33b6c", + "owner": "de77c674-c063-40b1-ae91-32c3cba13b23" + }, + "detail-type": "TemplateDeleted", + "source": "uk.nhs.notify.templates", + "time": "2025-06-28T12:34:56Z", + "version": "1.0" +} diff --git a/tests/contracts/tests/auth/consumer/template-deleted.consumer.pact.test.ts b/tests/contracts/tests/auth/consumer/template-deleted.consumer.pact.test.ts new file mode 100644 index 000000000..1ff31df44 --- /dev/null +++ b/tests/contracts/tests/auth/consumer/template-deleted.consumer.pact.test.ts @@ -0,0 +1,38 @@ +import path from 'node:path'; +import { + MessageConsumerPact, + Matchers, + asynchronousBodyHandler, +} from '@pact-foundation/pact'; +import { $TemplateDeletedEvent } from '../../../src/auth/handlers/template-deleted.handler'; + +// Stub of handler that processes the incoming event +// Only check the validation - don't run actual handler logic +async function handleTemplateDeleted(event: unknown): Promise { + $TemplateDeletedEvent.parse(event); +} + +describe('Pact Message Consumer - TemplateDeleted Event', () => { + const messagePact = new MessageConsumerPact({ + consumer: 'auth', + provider: 'templates', + dir: path.resolve(__dirname, '.pacts'), + pactfileWriteMode: 'update', + logLevel: 'error', + }); + + it('should validate the template deleted event structure and handler logic', async () => { + await messagePact + .given('a template has been deleted') + .expectsToReceive('TemplateDeleted') + .withContent({ + 'detail-type': 'TemplateDeleted', + version: '1.0', + detail: { + owner: Matchers.uuid('c0574019-4629-4b3f-8987-aa34ca8bc5b9'), + id: Matchers.uuid('b18a9a49-72a8-4157-8b85-76d5ac5c7804'), + }, + }) + .verify(asynchronousBodyHandler(handleTemplateDeleted)); + }); +}); diff --git a/tests/contracts/tests/auth/provider/user-created.provider.pact.test.ts b/tests/contracts/tests/auth/provider/user-created.provider.pact.test.ts new file mode 100644 index 000000000..df7092a21 --- /dev/null +++ b/tests/contracts/tests/auth/provider/user-created.provider.pact.test.ts @@ -0,0 +1,24 @@ +import fs from 'node:fs'; +import path from 'node:path'; +import { MessageProviderPact } from '@pact-foundation/pact'; +import { createUserCreatedEvent } from '../../../src/auth/events/user-created.event'; + +describe('Pact Message Provider - UserCreated Event', () => { + const pactDir = path.resolve(__dirname, '.pacts'); + + const messagePact = new MessageProviderPact({ + provider: 'auth', + pactUrls: fs + .readdirSync(pactDir) + .filter((f) => f.endsWith('.json')) + .map((f) => path.join(pactDir, f)), + messageProviders: { + UserCreated: () => createUserCreatedEvent(), + }, + logLevel: 'error', + }); + + it('should produce a message that satisfies the consumer contracts for UserCreated', () => { + return messagePact.verify(); + }); +}); diff --git a/tests/contracts/tests/core/consumer/config.json b/tests/contracts/tests/core/consumer/config.json new file mode 100644 index 000000000..55b55bef2 --- /dev/null +++ b/tests/contracts/tests/core/consumer/config.json @@ -0,0 +1,7 @@ +{ + "providers": { + "templates": [ + "TemplateDeleted" + ] + } +} diff --git a/tests/contracts/tests/core/consumer/examples/templates/TemplateDeleted/example.json b/tests/contracts/tests/core/consumer/examples/templates/TemplateDeleted/example.json new file mode 100644 index 000000000..deb6a795a --- /dev/null +++ b/tests/contracts/tests/core/consumer/examples/templates/TemplateDeleted/example.json @@ -0,0 +1,10 @@ +{ + "detail": { + "id": "a6f7d152-fd5d-4aef-a12b-9d071fe33b6c", + "owner": "de77c674-c063-40b1-ae91-32c3cba13b23" + }, + "detail-type": "TemplateDeleted", + "source": "uk.nhs.notify.templates", + "time": "2025-06-28T12:34:56Z", + "version": "1.0" +} diff --git a/tests/contracts/tests/core/consumer/template-deleted.consumer.pact.test.ts b/tests/contracts/tests/core/consumer/template-deleted.consumer.pact.test.ts new file mode 100644 index 000000000..a61d93bf8 --- /dev/null +++ b/tests/contracts/tests/core/consumer/template-deleted.consumer.pact.test.ts @@ -0,0 +1,37 @@ +import path from 'node:path'; +import { + MessageConsumerPact, + Matchers, + asynchronousBodyHandler, +} from '@pact-foundation/pact'; +import { $TemplateDeletedEvent } from '../../../src/core/handlers/template-deleted.handler'; + +// Stub of handler that processes the incoming event +// Only check the validation - don't run actual handler logic +async function handleTemplateDeleted(event: unknown): Promise { + $TemplateDeletedEvent.parse(event); +} + +describe('Pact Message Consumer - TemplateDeleted Event', () => { + const messagePact = new MessageConsumerPact({ + consumer: 'core', + provider: 'templates', + dir: path.resolve(__dirname, '.pacts'), + pactfileWriteMode: 'update', + logLevel: 'error', + }); + + it('should validate the template deleted event structure and handler logic', async () => { + await messagePact + .given('a template has been deleted') + .expectsToReceive('TemplateDeleted') + .withContent({ + 'detail-type': 'TemplateDeleted', + detail: { + owner: Matchers.uuid('c0574019-4629-4b3f-8987-aa34ca8bc5b9'), + id: Matchers.uuid('b18a9a49-72a8-4157-8b85-76d5ac5c7804'), + }, + }) + .verify(asynchronousBodyHandler(handleTemplateDeleted)); + }); +}); diff --git a/tests/contracts/tests/templates/consumer/config.json b/tests/contracts/tests/templates/consumer/config.json new file mode 100644 index 000000000..f17d1e276 --- /dev/null +++ b/tests/contracts/tests/templates/consumer/config.json @@ -0,0 +1,7 @@ +{ + "providers": { + "auth": [ + "UserCreated" + ] + } +} diff --git a/tests/contracts/tests/templates/consumer/examples/auth/UserCreated/example.json b/tests/contracts/tests/templates/consumer/examples/auth/UserCreated/example.json new file mode 100644 index 000000000..7d2dd97ff --- /dev/null +++ b/tests/contracts/tests/templates/consumer/examples/auth/UserCreated/example.json @@ -0,0 +1,10 @@ +{ + "detail": { + "clientId": "de77c674-c063-40b1-ae91-32c3cba13b23", + "userId": "a6f7d152-fd5d-4aef-a12b-9d071fe33b6c" + }, + "detail-type": "UserCreated", + "source": "uk.nhs.notify.auth", + "time": "2025-06-28T12:34:56Z", + "version": "1.0" +} diff --git a/tests/contracts/tests/templates/consumer/user-created.consumer.pact.test.ts b/tests/contracts/tests/templates/consumer/user-created.consumer.pact.test.ts new file mode 100644 index 000000000..a8af9a304 --- /dev/null +++ b/tests/contracts/tests/templates/consumer/user-created.consumer.pact.test.ts @@ -0,0 +1,37 @@ +import path from 'node:path'; +import { + MessageConsumerPact, + Matchers, + asynchronousBodyHandler, +} from '@pact-foundation/pact'; +import { $UserCreatedEvent } from '../../../src/templates/handlers/user-created.handler'; + +// Stub of handler that processes the incoming event +// Only check the validation - don't run actual handler logic +async function handleUserCreated(event: unknown): Promise { + $UserCreatedEvent.parse(event); +} + +describe('Pact Message Consumer - UserCreated Event', () => { + const messagePact = new MessageConsumerPact({ + consumer: 'templates', + provider: 'auth', + dir: path.resolve(__dirname, '.pacts'), + pactfileWriteMode: 'update', + logLevel: 'error', + }); + + it('should validate the template deleted event structure and handler logic', async () => { + await messagePact + .given('a user has been created') + .expectsToReceive('UserCreated') + .withContent({ + 'detail-type': 'UserCreated', + detail: { + userId: Matchers.uuid('eec2e415-dbb2-4e4d-9afb-ab64e280a3c9'), + clientId: Matchers.uuid('f5a3daf2-8fa5-4582-ba2c-478eea955b6f'), + }, + }) + .verify(asynchronousBodyHandler(handleUserCreated)); + }); +}); diff --git a/tests/contracts/tests/templates/provider/template-deleted.provider.pact.test.ts b/tests/contracts/tests/templates/provider/template-deleted.provider.pact.test.ts new file mode 100644 index 000000000..861e51a98 --- /dev/null +++ b/tests/contracts/tests/templates/provider/template-deleted.provider.pact.test.ts @@ -0,0 +1,24 @@ +import fs from 'node:fs'; +import path from 'node:path'; +import { MessageProviderPact } from '@pact-foundation/pact'; +import { createTemplateDeletedEvent } from '../../../src/templates/events/template-deleted.event'; + +describe('Pact Message Provider - TemplateDeleted Event', () => { + const pactDir = path.resolve(__dirname, '.pacts'); + + const messagePact = new MessageProviderPact({ + provider: 'templates', + pactUrls: fs + .readdirSync(pactDir) + .filter((f) => f.endsWith('.json')) + .map((f) => path.join(pactDir, f)), + messageProviders: { + TemplateDeleted: () => createTemplateDeletedEvent(), + }, + logLevel: 'error', + }); + + it('should produce a message that satisfies the consumer contracts for TemplateDeleted', () => { + return messagePact.verify(); + }); +}); diff --git a/tests/contracts/tsconfig.json b/tests/contracts/tsconfig.json new file mode 100644 index 000000000..b99454516 --- /dev/null +++ b/tests/contracts/tsconfig.json @@ -0,0 +1,3 @@ +{ + "extends": "@tsconfig/node20/tsconfig.json" +}