From 5c6ded5add40bad4b4bf28a114c3495addb0d539 Mon Sep 17 00:00:00 2001 From: David Wass Date: Thu, 25 Sep 2025 10:01:37 +0000 Subject: [PATCH 01/14] add product id header --- .../api/components/x-nhsd-apim/target-attributes.yml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/specification/api/components/x-nhsd-apim/target-attributes.yml b/specification/api/components/x-nhsd-apim/target-attributes.yml index 74d901d1..b3864cc7 100644 --- a/specification/api/components/x-nhsd-apim/target-attributes.yml +++ b/specification/api/components/x-nhsd-apim/target-attributes.yml @@ -1,3 +1,9 @@ - name: NHSD-Supplier-ID header: NHSD-Supplier-ID required: false +- name: developer.app.id + header: NHSD-Application-ID + required: false +- name: NHSD-Product-ID + header: NHSD-Product-ID + required: false From 92991439d7219f93b54687d7d0ac07730a721189 Mon Sep 17 00:00:00 2001 From: Mark Slowey Date: Thu, 30 Oct 2025 15:08:05 +0000 Subject: [PATCH 02/14] CCM-11600: supplier repository and table --- .../components/api/ddb_table_suppliers.tf | 34 +++++ internal/datastore/src/__test__/db.ts | 29 ++++ .../src/__test__/supplier-repository.test.ts | 127 ++++++++++++++++++ internal/datastore/src/config.ts | 1 + internal/datastore/src/supplier-repository.ts | 73 ++++++++++ internal/datastore/src/types.ts | 5 +- 6 files changed, 267 insertions(+), 2 deletions(-) create mode 100644 infrastructure/terraform/components/api/ddb_table_suppliers.tf create mode 100644 internal/datastore/src/__test__/supplier-repository.test.ts create mode 100644 internal/datastore/src/supplier-repository.ts diff --git a/infrastructure/terraform/components/api/ddb_table_suppliers.tf b/infrastructure/terraform/components/api/ddb_table_suppliers.tf new file mode 100644 index 00000000..24a79fb6 --- /dev/null +++ b/infrastructure/terraform/components/api/ddb_table_suppliers.tf @@ -0,0 +1,34 @@ +resource "aws_dynamodb_table" "suppliers" { + name = "${local.csi}-suppliers" + billing_mode = "PAY_PER_REQUEST" + + hash_key = "id" + range_key = "apimId" + + ttl { + attribute_name = "ttl" + enabled = false + } + + global_secondary_index { + name = "supplier-apim-index" + hash_key = "apimId" + projection_type = "ALL" + } + + attribute { + name = "id" + type = "S" + } + + attribute { + name = "apimId" + type = "S" + } + + point_in_time_recovery { + enabled = true + } + + tags = var.default_tags +} diff --git a/internal/datastore/src/__test__/db.ts b/internal/datastore/src/__test__/db.ts index 85f89193..16951106 100644 --- a/internal/datastore/src/__test__/db.ts +++ b/internal/datastore/src/__test__/db.ts @@ -31,6 +31,7 @@ export async function setupDynamoDBContainer() { endpoint, lettersTableName: 'letters', miTableName: 'management-info', + suppliersTableName: 'suppliers', lettersTtlHours: 1, miTtlHours: 1 }; @@ -94,6 +95,29 @@ const createMITableCommand = new CreateTableCommand({ ] }); +const createSupplierTableCommand = new CreateTableCommand({ + TableName: 'suppliers', + BillingMode: 'PAY_PER_REQUEST', + KeySchema: [ + { AttributeName: 'id', KeyType: 'HASH' } // Partition key + ], + GlobalSecondaryIndexes: [ + { + IndexName: 'supplier-apim-index', + KeySchema: [ + { AttributeName: 'apimId', KeyType: 'HASH' } // Partition key for GSI + ], + Projection: { + ProjectionType: 'ALL' + } + } + ], + AttributeDefinitions: [ + { AttributeName: 'id', AttributeType: 'S' }, + { AttributeName: 'apimId', AttributeType: 'S' } + ] + }); + export async function createTables(context: DBContext) { const { ddbClient } = context; @@ -102,6 +126,7 @@ export async function createTables(context: DBContext) { await ddbClient.send(updateTimeToLiveCommand); await ddbClient.send(createMITableCommand); + await ddbClient.send(createSupplierTableCommand); } @@ -115,4 +140,8 @@ export async function deleteTables(context: DBContext) { await ddbClient.send(new DeleteTableCommand({ TableName: 'management-info' })); + + await ddbClient.send(new DeleteTableCommand({ + TableName: 'suppliers' + })); } diff --git a/internal/datastore/src/__test__/supplier-repository.test.ts b/internal/datastore/src/__test__/supplier-repository.test.ts new file mode 100644 index 00000000..c7456574 --- /dev/null +++ b/internal/datastore/src/__test__/supplier-repository.test.ts @@ -0,0 +1,127 @@ +import { Logger } from "pino"; +import { createTables, DBContext, deleteTables, setupDynamoDBContainer } from "./db"; +import { createTestLogger, LogStream } from "./logs"; +import { SupplierRepository } from "../supplier-repository"; +import { Supplier } from "../types"; +import { v4 as uuidv4 } from 'uuid'; + +function createSupplier(status: 'ENABLED' | 'DISABLED', apimId = uuidv4()): Omit { + return { + id: uuidv4(), + name: 'Supplier One', + apimId, + status + } +} + +// Database tests can take longer, especially with setup and teardown +jest.setTimeout(30000); + +describe('SupplierRepository', () => { + let db: DBContext; + let supplierRepository: SupplierRepository; + let logStream: LogStream; + let logger: Logger; + + beforeAll(async () => { + db = await setupDynamoDBContainer(); + }); + + beforeEach(async () => { + await createTables(db); + ( + { logStream, logger } = createTestLogger() + ); + + supplierRepository = new SupplierRepository(db.docClient, logger, db.config); + }); + + afterEach(async () => { + await deleteTables(db); + jest.useRealTimers(); + }); + + afterAll(async () => { + await db.container.stop(); + }); + + test('creates an enabled supplier with provided values and timestamps', async () => { + jest.useFakeTimers(); + // Month is zero-indexed in JS Date + jest.setSystemTime(new Date(2020, 1, 1)); + + const supplier = createSupplier('ENABLED'); + + const persistedSupplier = await supplierRepository.putSupplier(supplier); + + expect(persistedSupplier).toEqual(expect.objectContaining({ + ...supplier, + updatedAt: '2020-02-01T00:00:00.000Z', + })); + }); + + test('fetches a supplier by its ID', async () => { + const supplier = createSupplier('DISABLED') + await supplierRepository.putSupplier(supplier); + + const fetched = await supplierRepository.getSupplierById(supplier.id); + + expect(fetched).toEqual(expect.objectContaining({ + ...supplier + })); + }); + + test('throws an error fetching a supplier that does not exist', async () => { + await expect(supplierRepository.getSupplierById('non-existent-id')) + .rejects.toThrow('Supplier with id non-existent-id not found'); + }); + + test('overwrites an existing supplier entry', async () => { + const supplier = createSupplier('DISABLED'); + + const original = await supplierRepository.putSupplier(supplier); + expect(original.status).toBe('DISABLED'); + + supplier.status = 'ENABLED'; + const updated = await supplierRepository.putSupplier(supplier); + expect(updated.status).toBe('ENABLED'); + }); + + test('rethrows errors from DynamoDB when creating a letter', async () => { + const misconfiguredRepository = new SupplierRepository(db.docClient, logger, { + ...db.config, + suppliersTableName: 'nonexistent-table' + }); + await expect(misconfiguredRepository.putSupplier(createSupplier('ENABLED'))) + .rejects.toThrow('Cannot do operations on a non-existent table'); + }); + + test('fetches a supplier by apimId', async () => { + const supplier = createSupplier('ENABLED'); + + await supplierRepository.putSupplier(supplier); + + const fetched = await supplierRepository.getSupplierByApimId(supplier.apimId); + expect(fetched).toEqual(expect.objectContaining({ + ...supplier + })); + }); + + test('throws an error fetching a supplier by apimId that does not exist', async () => { + await expect(supplierRepository.getSupplierByApimId('non-existent-apim-id')) + .rejects.toThrow('Supplier with apimId non-existent-apim-id not found'); + }); + + test('throws an error fetching a supplier by apimId when multiple exist', async () => { + const apimId = 'duplicate-apim-id'; + const supplier1 = createSupplier('ENABLED', apimId); + const supplier2 = createSupplier('DISABLED', apimId); + + await supplierRepository.putSupplier(supplier1); + await supplierRepository.putSupplier(supplier2); + + await expect(supplierRepository.getSupplierByApimId(apimId)) + .rejects.toThrow(`Multiple suppliers found with apimId ${apimId}`); + }); + +}); diff --git a/internal/datastore/src/config.ts b/internal/datastore/src/config.ts index 92081caf..b9a4f6ec 100644 --- a/internal/datastore/src/config.ts +++ b/internal/datastore/src/config.ts @@ -3,6 +3,7 @@ export type DatastoreConfig = { endpoint?: string, lettersTableName: string, miTableName: string, + suppliersTableName: string, lettersTtlHours: number, miTtlHours: number } diff --git a/internal/datastore/src/supplier-repository.ts b/internal/datastore/src/supplier-repository.ts new file mode 100644 index 00000000..5256da9a --- /dev/null +++ b/internal/datastore/src/supplier-repository.ts @@ -0,0 +1,73 @@ +import { + DynamoDBDocumentClient, + GetCommand, + PutCommand, + QueryCommand +} from '@aws-sdk/lib-dynamodb'; +import { Supplier, SupplierSchema } from './types'; +import { Logger } from 'pino'; +import { v4 as uuidv4 } from 'uuid'; +import z from 'zod'; + +export type SupplierRepositoryConfig = { + suppliersTableName: string +}; + +export class SupplierRepository { + constructor(readonly ddbClient: DynamoDBDocumentClient, + readonly log: Logger, + readonly config: SupplierRepositoryConfig) { + } + + async putSupplier(supplier: Omit): Promise { + + const now = new Date().toISOString(); + const supplierDb = { + ...supplier, + updatedAt: now + }; + + await this.ddbClient.send(new PutCommand({ + TableName: this.config.suppliersTableName, + Item: supplierDb, + })); + + return SupplierSchema.parse(supplierDb); + } + + async getSupplierById(supplierId: string): Promise { + const result = await this.ddbClient.send(new GetCommand({ + TableName: this.config.suppliersTableName, + Key: { + id: supplierId + } + })); + + if (!result.Item) { + throw new Error(`Supplier with id ${supplierId} not found`); + } + + return SupplierSchema.parse(result.Item); + } + + async getSupplierByApimId(apimId: string): Promise { + const result = await this.ddbClient.send(new QueryCommand({ + TableName: this.config.suppliersTableName, + IndexName: 'supplier-apim-index', + KeyConditionExpression: 'apimId = :apimId', + ExpressionAttributeValues: { + ':apimId': apimId + }, + })); + + if(result.Count && result.Count > 1) { + throw new Error(`Multiple suppliers found with apimId ${apimId}`); + } + + if(result.Count === 0 || !result.Items) { + throw new Error(`Supplier with apimId ${apimId} not found`); + } + + return SupplierSchema.parse(result.Items[0]); + } +}; diff --git a/internal/datastore/src/types.ts b/internal/datastore/src/types.ts index 1eb1e815..5eec03a3 100644 --- a/internal/datastore/src/types.ts +++ b/internal/datastore/src/types.ts @@ -1,13 +1,14 @@ import { z } from 'zod'; import { idRef } from '@internal/helpers'; -export const SupplerStatus = z.enum(['ENABLED', 'DISABLED']); +export const SupplierStatus = z.enum(['ENABLED', 'DISABLED']); export const SupplierSchema = z.object({ id: z.string(), name: z.string(), apimId: z.string(), - status: SupplerStatus + status: SupplierStatus, + updatedAt: z.string(), }).describe('Supplier'); export type Supplier = z.infer; From d8587e139c9ba68a37f494588ae4ff96bc49fea7 Mon Sep 17 00:00:00 2001 From: Mark Slowey Date: Thu, 30 Oct 2025 15:48:17 +0000 Subject: [PATCH 03/14] CCM-11600: cli interface for supplier repo --- internal/datastore/src/index.ts | 1 + package.json | 2 +- .../letter-test-data}/.eslintignore | 0 .../letter-test-data}/.gitignore | 0 .../letter-test-data}/README.md | 0 .../letter-test-data}/jest.config.ts | 0 .../letter-test-data}/package.json | 2 +- .../helpers/create_letter_helpers.test.ts | 0 .../letter-test-data}/src/cli/index.ts | 2 +- .../src/helpers/create_letter_helpers.ts | 2 +- .../src/helpers/s3_helpers.ts | 0 .../src/infrastructure/letter-repo-factory.ts | 4 +- .../letter-test-data}/test_letter.pdf | Bin .../letter-test-data}/tsconfig.json | 2 +- scripts/utilities/supplier-data/.eslintignore | 1 + scripts/utilities/supplier-data/.gitignore | 4 + scripts/utilities/supplier-data/README.md | 30 +++++ .../utilities/supplier-data/jest.config.ts | 61 +++++++++ scripts/utilities/supplier-data/package.json | 25 ++++ .../utilities/supplier-data/src/cli/index.ts | 120 ++++++++++++++++++ .../infrastructure/suppliers-repo-factory.ts | 15 +++ scripts/utilities/supplier-data/tsconfig.json | 8 ++ 22 files changed, 272 insertions(+), 7 deletions(-) rename scripts/{test-data => utilities/letter-test-data}/.eslintignore (100%) rename scripts/{test-data => utilities/letter-test-data}/.gitignore (100%) rename scripts/{test-data => utilities/letter-test-data}/README.md (100%) rename scripts/{test-data => utilities/letter-test-data}/jest.config.ts (100%) rename scripts/{test-data => utilities/letter-test-data}/package.json (90%) rename scripts/{test-data => utilities/letter-test-data}/src/__test__/helpers/create_letter_helpers.test.ts (100%) rename scripts/{test-data => utilities/letter-test-data}/src/cli/index.ts (99%) rename scripts/{test-data => utilities/letter-test-data}/src/helpers/create_letter_helpers.ts (95%) rename scripts/{test-data => utilities/letter-test-data}/src/helpers/s3_helpers.ts (100%) rename scripts/{test-data => utilities/letter-test-data}/src/infrastructure/letter-repo-factory.ts (90%) rename scripts/{test-data => utilities/letter-test-data}/test_letter.pdf (100%) rename scripts/{test-data => utilities/letter-test-data}/tsconfig.json (65%) create mode 100644 scripts/utilities/supplier-data/.eslintignore create mode 100644 scripts/utilities/supplier-data/.gitignore create mode 100644 scripts/utilities/supplier-data/README.md create mode 100644 scripts/utilities/supplier-data/jest.config.ts create mode 100644 scripts/utilities/supplier-data/package.json create mode 100644 scripts/utilities/supplier-data/src/cli/index.ts create mode 100644 scripts/utilities/supplier-data/src/infrastructure/suppliers-repo-factory.ts create mode 100644 scripts/utilities/supplier-data/tsconfig.json diff --git a/internal/datastore/src/index.ts b/internal/datastore/src/index.ts index 20f80e92..31f2b754 100644 --- a/internal/datastore/src/index.ts +++ b/internal/datastore/src/index.ts @@ -1,4 +1,5 @@ export * from './types'; export * from './mi-repository'; export * from './letter-repository'; +export * from './supplier-repository'; export * from './types'; diff --git a/package.json b/package.json index 0453cc74..68f8abc5 100644 --- a/package.json +++ b/package.json @@ -79,7 +79,7 @@ "workspaces": [ "lambdas/*", "internal/*", - "scripts/test-data", + "scripts/utilities/*", "docs" ] } diff --git a/scripts/test-data/.eslintignore b/scripts/utilities/letter-test-data/.eslintignore similarity index 100% rename from scripts/test-data/.eslintignore rename to scripts/utilities/letter-test-data/.eslintignore diff --git a/scripts/test-data/.gitignore b/scripts/utilities/letter-test-data/.gitignore similarity index 100% rename from scripts/test-data/.gitignore rename to scripts/utilities/letter-test-data/.gitignore diff --git a/scripts/test-data/README.md b/scripts/utilities/letter-test-data/README.md similarity index 100% rename from scripts/test-data/README.md rename to scripts/utilities/letter-test-data/README.md diff --git a/scripts/test-data/jest.config.ts b/scripts/utilities/letter-test-data/jest.config.ts similarity index 100% rename from scripts/test-data/jest.config.ts rename to scripts/utilities/letter-test-data/jest.config.ts diff --git a/scripts/test-data/package.json b/scripts/utilities/letter-test-data/package.json similarity index 90% rename from scripts/test-data/package.json rename to scripts/utilities/letter-test-data/package.json index c181c2cb..a5286292 100644 --- a/scripts/test-data/package.json +++ b/scripts/utilities/letter-test-data/package.json @@ -13,7 +13,7 @@ "jest-mock-extended": "^4.0.0", "typescript": "^5.8.3" }, - "name": "nhs-notify-supplier-api-data-generator", + "name": "nhs-notify-supplier-api-letter-test-data-utility", "private": true, "scripts": { "cli": "tsx ./src/cli/index.ts", diff --git a/scripts/test-data/src/__test__/helpers/create_letter_helpers.test.ts b/scripts/utilities/letter-test-data/src/__test__/helpers/create_letter_helpers.test.ts similarity index 100% rename from scripts/test-data/src/__test__/helpers/create_letter_helpers.test.ts rename to scripts/utilities/letter-test-data/src/__test__/helpers/create_letter_helpers.test.ts diff --git a/scripts/test-data/src/cli/index.ts b/scripts/utilities/letter-test-data/src/cli/index.ts similarity index 99% rename from scripts/test-data/src/cli/index.ts rename to scripts/utilities/letter-test-data/src/cli/index.ts index 0e2a4517..ff16b875 100644 --- a/scripts/test-data/src/cli/index.ts +++ b/scripts/utilities/letter-test-data/src/cli/index.ts @@ -1,5 +1,5 @@ import { hideBin } from "yargs/helpers"; -import yargs from "yargs/yargs"; +import yargs from 'yargs'; import { LetterStatusType } from "@internal/datastore/src/types"; import { randomUUID } from "crypto"; import { createLetter, createLetterDto } from "../helpers/create_letter_helpers"; diff --git a/scripts/test-data/src/helpers/create_letter_helpers.ts b/scripts/utilities/letter-test-data/src/helpers/create_letter_helpers.ts similarity index 95% rename from scripts/test-data/src/helpers/create_letter_helpers.ts rename to scripts/utilities/letter-test-data/src/helpers/create_letter_helpers.ts index e1eb0719..caaeea70 100644 --- a/scripts/test-data/src/helpers/create_letter_helpers.ts +++ b/scripts/utilities/letter-test-data/src/helpers/create_letter_helpers.ts @@ -33,7 +33,7 @@ export async function createLetter(params: { targetFilename, ); - const letter: Omit = { + const letter: Omit = { id: letterId, supplierId, specificationId, diff --git a/scripts/test-data/src/helpers/s3_helpers.ts b/scripts/utilities/letter-test-data/src/helpers/s3_helpers.ts similarity index 100% rename from scripts/test-data/src/helpers/s3_helpers.ts rename to scripts/utilities/letter-test-data/src/helpers/s3_helpers.ts diff --git a/scripts/test-data/src/infrastructure/letter-repo-factory.ts b/scripts/utilities/letter-test-data/src/infrastructure/letter-repo-factory.ts similarity index 90% rename from scripts/test-data/src/infrastructure/letter-repo-factory.ts rename to scripts/utilities/letter-test-data/src/infrastructure/letter-repo-factory.ts index 269642c0..e0060337 100644 --- a/scripts/test-data/src/infrastructure/letter-repo-factory.ts +++ b/scripts/utilities/letter-test-data/src/infrastructure/letter-repo-factory.ts @@ -1,6 +1,6 @@ import { DynamoDBClient } from '@aws-sdk/client-dynamodb'; import { DynamoDBDocumentClient } from '@aws-sdk/lib-dynamodb'; -import pino from 'pino'; +import { pino } from 'pino'; import { LetterRepository } from '@internal/datastore'; export function createLetterRepository(environment: string, ttlHours:number): LetterRepository { @@ -9,7 +9,7 @@ export function createLetterRepository(environment: string, ttlHours:number): Le const log = pino(); const config = { lettersTableName: `nhs-${environment}-supapi-letters`, - ttlHours: ttlHours, + lettersTtlHours: ttlHours, }; return new LetterRepository(docClient, log, config); diff --git a/scripts/test-data/test_letter.pdf b/scripts/utilities/letter-test-data/test_letter.pdf similarity index 100% rename from scripts/test-data/test_letter.pdf rename to scripts/utilities/letter-test-data/test_letter.pdf diff --git a/scripts/test-data/tsconfig.json b/scripts/utilities/letter-test-data/tsconfig.json similarity index 65% rename from scripts/test-data/tsconfig.json rename to scripts/utilities/letter-test-data/tsconfig.json index 24902365..730d18dd 100644 --- a/scripts/test-data/tsconfig.json +++ b/scripts/utilities/letter-test-data/tsconfig.json @@ -1,6 +1,6 @@ { "compilerOptions": {}, - "extends": "../../tsconfig.base.json", + "extends": "../../../tsconfig.base.json", "include": [ "src/**/*", "jest.config.ts" diff --git a/scripts/utilities/supplier-data/.eslintignore b/scripts/utilities/supplier-data/.eslintignore new file mode 100644 index 00000000..1521c8b7 --- /dev/null +++ b/scripts/utilities/supplier-data/.eslintignore @@ -0,0 +1 @@ +dist diff --git a/scripts/utilities/supplier-data/.gitignore b/scripts/utilities/supplier-data/.gitignore new file mode 100644 index 00000000..80323f7c --- /dev/null +++ b/scripts/utilities/supplier-data/.gitignore @@ -0,0 +1,4 @@ +coverage +node_modules +dist +.reports diff --git a/scripts/utilities/supplier-data/README.md b/scripts/utilities/supplier-data/README.md new file mode 100644 index 00000000..79a18f98 --- /dev/null +++ b/scripts/utilities/supplier-data/README.md @@ -0,0 +1,30 @@ +# Test letter generator + +Simple scripts to manipulate supplier data within the Suppliers table via the internal datastore definitions. + +## Usage + +Log in the desired AWS account and then run the command below. You may need to set the AWS_REGION envar (eu-west-2) + +Note that the AWS account ID is required in order to resolve the bucket name. + +```bash +npm run cli -- put-supplier \ + --id supplier-id \ + --name supplier \ + --apimId supplier-apim-id \ + --status ENABLED + --environment pr147 \ +``` + +```bash +npm run cli -- get-supplier-by-id \ + --id supplier-id \ + --environment main \ +``` + +```bash +npm run cli -- get-supplier-by-apim-id \ + --apimId apim-supplier-id + --environment main \ +``` diff --git a/scripts/utilities/supplier-data/jest.config.ts b/scripts/utilities/supplier-data/jest.config.ts new file mode 100644 index 00000000..f2972c27 --- /dev/null +++ b/scripts/utilities/supplier-data/jest.config.ts @@ -0,0 +1,61 @@ +import type { Config } from 'jest'; + +export const baseJestConfig: Config = { + preset: 'ts-jest', + + // Automatically clear mock calls, instances, contexts and results before every test + clearMocks: true, + + // Indicates whether the coverage information should be collected while executing the test + collectCoverage: true, + + // The directory where Jest should output its coverage files + coverageDirectory: './.reports/unit/coverage', + + // Indicates which provider should be used to instrument code for coverage + coverageProvider: 'babel', + + coverageThreshold: { + global: { + branches: 100, + functions: 100, + lines: 100, + statements: -10, + }, + }, + + coveragePathIgnorePatterns: ['/__tests__/'], + transform: { '^.+\\.ts$': 'ts-jest' }, + testPathIgnorePatterns: ['.build'], + testMatch: ['**/?(*.)+(spec|test).[jt]s?(x)'], + + // Use this configuration option to add custom reporters to Jest + reporters: [ + 'default', + [ + 'jest-html-reporter', + { + pageTitle: 'Test Report', + outputPath: './.reports/unit/test-report.html', + includeFailureMsg: true, + }, + ], + ], + + // The test environment that will be used for testing + testEnvironment: 'jsdom', +}; + +const utilsJestConfig = { + ...baseJestConfig, + + testEnvironment: 'node', + + coveragePathIgnorePatterns: [ + ...(baseJestConfig.coveragePathIgnorePatterns ?? []), + 'cli/index.ts', + 'suppliers-repo-factory.ts', + ], +}; + +export default utilsJestConfig; diff --git a/scripts/utilities/supplier-data/package.json b/scripts/utilities/supplier-data/package.json new file mode 100644 index 00000000..a74ca076 --- /dev/null +++ b/scripts/utilities/supplier-data/package.json @@ -0,0 +1,25 @@ +{ + "dependencies": { + "@internal/datastore": "*", + "esbuild": "^0.25.11", + "pino": "^9.7.0", + "yargs": "^17.7.2" + }, + "devDependencies": { + "@tsconfig/node22": "^22.0.2", + "@types/jest": "^30.0.0", + "jest": "^30.2.0", + "jest-mock-extended": "^4.0.0", + "typescript": "^5.8.3" + }, + "name": "nhs-notify-supplier-api-suppliers-data-utility", + "private": true, + "scripts": { + "cli": "tsx ./src/cli/index.ts", + "lint": "eslint .", + "lint:fix": "eslint . --fix", + "test:unit": "echo \"Supplier data utility has no unit tests\"", + "typecheck": "tsc --noEmit" + }, + "version": "0.0.1" +} diff --git a/scripts/utilities/supplier-data/src/cli/index.ts b/scripts/utilities/supplier-data/src/cli/index.ts new file mode 100644 index 00000000..4c37156a --- /dev/null +++ b/scripts/utilities/supplier-data/src/cli/index.ts @@ -0,0 +1,120 @@ +import { hideBin } from "yargs/helpers"; +import yargs from 'yargs'; +import { LetterStatusType } from "@internal/datastore/src/types"; +import { randomUUID } from "crypto"; +import { createSupplierRepository } from "../infrastructure/suppliers-repo-factory"; + + + +async function main() { + await yargs(hideBin(process.argv)) + .command( + "put-supplier", + "Create or update a supplier", + { + environment: { + type: "string", + demandOption: true, + }, + "id": { + type: "string", + demandOption: true, + }, + "name": { + type: "string", + demandOption: false, + }, + "apimId": { + type: "string", + demandOption: false, + }, + status: { + type: "string", + demandOption: true, + choices: [ + "ENABLED", + "DISABLED" + ], + }, + }, + async (argv) => { + // parse args + const id = argv.id; + const name = argv.name; + const apimId = argv.apimId; + const status = argv.status as "ENABLED" | "DISABLED"; + + const environment = argv.environment; + + const supplierRepository = createSupplierRepository(environment); + + const putResult = await supplierRepository.putSupplier({ + id, + name, + apimId, + status, + }); + + console.log(`PUT successful ${JSON.stringify(putResult)}`); + } + ) + .command( + "get-supplier-by-id", + "Get a supplier by their Supplier ID", + { + "id": { + type: "string", + demandOption: true, + }, + environment: { + type: "string", + demandOption: true, + }, + }, + async (argv) => { + + const id = argv.id; + const environment = argv.environment; + + const supplierRepository = createSupplierRepository(environment); + + const getResult = await supplierRepository.getSupplierById(id); + + console.log(`GET successful: ${JSON.stringify(getResult)}`); + }, + ) + .command( + "get-supplier-by-apim-id", + "Get a supplier by their APIM ID", + { + "apimId": { + type: "string", + demandOption: true, + }, + environment: { + type: "string", + demandOption: true, + }, + }, + async (argv) => { + + const apimId = argv.apimId; + const environment = argv.environment; + + const supplierRepository = createSupplierRepository(environment); + + const getResult = await supplierRepository.getSupplierByApimId(apimId); + + console.log(`GET successful: ${JSON.stringify(getResult)}`); + }, + ) + .demandCommand(1) + .parse(); +} + +if (require.main === module) { + main().catch((err) => { + console.error(err); + process.exitCode = 1; + }); +} diff --git a/scripts/utilities/supplier-data/src/infrastructure/suppliers-repo-factory.ts b/scripts/utilities/supplier-data/src/infrastructure/suppliers-repo-factory.ts new file mode 100644 index 00000000..3bddc616 --- /dev/null +++ b/scripts/utilities/supplier-data/src/infrastructure/suppliers-repo-factory.ts @@ -0,0 +1,15 @@ +import { DynamoDBClient } from '@aws-sdk/client-dynamodb'; +import { DynamoDBDocumentClient } from '@aws-sdk/lib-dynamodb'; +import { pino } from 'pino'; +import { SupplierRepository } from '@internal/datastore'; + +export function createSupplierRepository(environment: string): SupplierRepository { + const ddbClient = new DynamoDBClient({}); + const docClient = DynamoDBDocumentClient.from(ddbClient); + const log = pino(); + const config = { + suppliersTableName: `nhs-${environment}-supapi-suppliers`, + }; + + return new SupplierRepository(docClient, log, config); +} diff --git a/scripts/utilities/supplier-data/tsconfig.json b/scripts/utilities/supplier-data/tsconfig.json new file mode 100644 index 00000000..730d18dd --- /dev/null +++ b/scripts/utilities/supplier-data/tsconfig.json @@ -0,0 +1,8 @@ +{ + "compilerOptions": {}, + "extends": "../../../tsconfig.base.json", + "include": [ + "src/**/*", + "jest.config.ts" + ] +} From fecb456c51277fdfc65a01c104d83d993815d283 Mon Sep 17 00:00:00 2001 From: Mark Slowey Date: Thu, 30 Oct 2025 16:17:25 +0000 Subject: [PATCH 04/14] CCM-11600: lock for new packages --- package-lock.json | 924 +++++----------------------------------------- 1 file changed, 88 insertions(+), 836 deletions(-) diff --git a/package-lock.json b/package-lock.json index fc7a73a8..be173a7a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,7 +8,7 @@ "workspaces": [ "lambdas/*", "internal/*", - "scripts/test-data", + "scripts/utilities/*", "docs" ], "dependencies": { @@ -18326,14 +18326,18 @@ "resolved": "docs", "link": true }, - "node_modules/nhs-notify-supplier-api-data-generator": { - "resolved": "scripts/test-data", - "link": true - }, "node_modules/nhs-notify-supplier-api-handler": { "resolved": "lambdas/api-handler", "link": true }, + "node_modules/nhs-notify-supplier-api-letter-test-data-utility": { + "resolved": "scripts/utilities/letter-test-data", + "link": true + }, + "node_modules/nhs-notify-supplier-api-suppliers-data-utility": { + "resolved": "scripts/utilities/supplier-data", + "link": true + }, "node_modules/nhs-notify-supplier-authorizer": { "resolved": "lambdas/authorizer", "link": true @@ -22205,6 +22209,7 @@ "scripts/test-data": { "name": "nhs-notify-supplier-api-data-generator", "version": "0.0.1", + "extraneous": true, "dependencies": { "@aws-sdk/client-s3": "^3.858.0", "@internal/datastore": "*", @@ -22220,878 +22225,125 @@ "typescript": "^5.8.3" } }, - "scripts/test-data/node_modules/@jest/core": { - "version": "29.7.0", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/console": "^29.7.0", - "@jest/reporters": "^29.7.0", - "@jest/test-result": "^29.7.0", - "@jest/transform": "^29.7.0", - "@jest/types": "^29.6.3", - "@types/node": "*", - "ansi-escapes": "^4.2.1", - "chalk": "^4.0.0", - "ci-info": "^3.2.0", - "exit": "^0.1.2", - "graceful-fs": "^4.2.9", - "jest-changed-files": "^29.7.0", - "jest-config": "^29.7.0", - "jest-haste-map": "^29.7.0", - "jest-message-util": "^29.7.0", - "jest-regex-util": "^29.6.3", - "jest-resolve": "^29.7.0", - "jest-resolve-dependencies": "^29.7.0", - "jest-runner": "^29.7.0", - "jest-runtime": "^29.7.0", - "jest-snapshot": "^29.7.0", - "jest-util": "^29.7.0", - "jest-validate": "^29.7.0", - "jest-watcher": "^29.7.0", - "micromatch": "^4.0.4", - "pretty-format": "^29.7.0", - "slash": "^3.0.0", - "strip-ansi": "^6.0.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - }, - "peerDependencies": { - "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" - }, - "peerDependenciesMeta": { - "node-notifier": { - "optional": true - } - } - }, - "scripts/test-data/node_modules/@jest/environment": { - "version": "29.7.0", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/fake-timers": "^29.7.0", - "@jest/types": "^29.6.3", - "@types/node": "*", - "jest-mock": "^29.7.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "scripts/test-data/node_modules/@jest/expect": { - "version": "29.7.0", - "dev": true, - "license": "MIT", - "dependencies": { - "expect": "^29.7.0", - "jest-snapshot": "^29.7.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "scripts/test-data/node_modules/@jest/fake-timers": { - "version": "29.7.0", - "dev": true, - "license": "MIT", + "scripts/utilities/letter-test-data": { + "name": "nhs-notify-supplier-api-letter-test-data-utility", + "version": "0.0.1", "dependencies": { - "@jest/types": "^29.6.3", - "@sinonjs/fake-timers": "^10.0.2", - "@types/node": "*", - "jest-message-util": "^29.7.0", - "jest-mock": "^29.7.0", - "jest-util": "^29.7.0" + "@aws-sdk/client-s3": "^3.858.0", + "@internal/datastore": "*", + "esbuild": "^0.25.11", + "pino": "^9.7.0", + "yargs": "^17.7.2" }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + "devDependencies": { + "@tsconfig/node22": "^22.0.2", + "@types/jest": "^30.0.0", + "jest": "^30.2.0", + "jest-mock-extended": "^4.0.0", + "typescript": "^5.8.3" } }, - "scripts/test-data/node_modules/@jest/globals": { - "version": "29.7.0", - "dev": true, - "license": "MIT", + "scripts/utilities/letter-test-data/node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "license": "ISC", "dependencies": { - "@jest/environment": "^29.7.0", - "@jest/expect": "^29.7.0", - "@jest/types": "^29.6.3", - "jest-mock": "^29.7.0" + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" }, "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + "node": ">=12" } }, - "scripts/test-data/node_modules/@jest/reporters": { - "version": "29.7.0", - "dev": true, + "scripts/utilities/letter-test-data/node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", "license": "MIT", "dependencies": { - "@bcoe/v8-coverage": "^0.2.3", - "@jest/console": "^29.7.0", - "@jest/test-result": "^29.7.0", - "@jest/transform": "^29.7.0", - "@jest/types": "^29.6.3", - "@jridgewell/trace-mapping": "^0.3.18", - "@types/node": "*", - "chalk": "^4.0.0", - "collect-v8-coverage": "^1.0.0", - "exit": "^0.1.2", - "glob": "^7.1.3", - "graceful-fs": "^4.2.9", - "istanbul-lib-coverage": "^3.0.0", - "istanbul-lib-instrument": "^6.0.0", - "istanbul-lib-report": "^3.0.0", - "istanbul-lib-source-maps": "^4.0.0", - "istanbul-reports": "^3.1.3", - "jest-message-util": "^29.7.0", - "jest-util": "^29.7.0", - "jest-worker": "^29.7.0", - "slash": "^3.0.0", - "string-length": "^4.0.1", - "strip-ansi": "^6.0.0", - "v8-to-istanbul": "^9.0.1" + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" }, "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - }, - "peerDependencies": { - "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + "node": ">=10" }, - "peerDependenciesMeta": { - "node-notifier": { - "optional": true - } + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, - "scripts/test-data/node_modules/@jest/source-map": { - "version": "29.6.3", - "dev": true, + "scripts/utilities/letter-test-data/node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", "license": "MIT", "dependencies": { - "@jridgewell/trace-mapping": "^0.3.18", - "callsites": "^3.0.0", - "graceful-fs": "^4.2.9" + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" }, "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + "node": ">=12" } }, - "scripts/test-data/node_modules/@jest/test-sequencer": { - "version": "29.7.0", - "dev": true, - "license": "MIT", + "scripts/utilities/supplier-data": { + "name": "nhs-notify-supplier-api-suppliers-data-utility", + "version": "0.0.1", "dependencies": { - "@jest/test-result": "^29.7.0", - "graceful-fs": "^4.2.9", - "jest-haste-map": "^29.7.0", - "slash": "^3.0.0" + "@internal/datastore": "*", + "esbuild": "^0.25.11", + "pino": "^9.7.0", + "yargs": "^17.7.2" }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + "devDependencies": { + "@tsconfig/node22": "^22.0.2", + "@types/jest": "^30.0.0", + "jest": "^30.2.0", + "jest-mock-extended": "^4.0.0", + "typescript": "^5.8.3" } }, - "scripts/test-data/node_modules/@jest/transform": { - "version": "29.7.0", - "dev": true, - "license": "MIT", + "scripts/utilities/supplier-data/node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "license": "ISC", "dependencies": { - "@babel/core": "^7.11.6", - "@jest/types": "^29.6.3", - "@jridgewell/trace-mapping": "^0.3.18", - "babel-plugin-istanbul": "^6.1.1", - "chalk": "^4.0.0", - "convert-source-map": "^2.0.0", - "fast-json-stable-stringify": "^2.1.0", - "graceful-fs": "^4.2.9", - "jest-haste-map": "^29.7.0", - "jest-regex-util": "^29.6.3", - "jest-util": "^29.7.0", - "micromatch": "^4.0.4", - "pirates": "^4.0.4", - "slash": "^3.0.0", - "write-file-atomic": "^4.0.2" + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" }, "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "scripts/test-data/node_modules/@sinonjs/fake-timers": { - "version": "10.3.0", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "@sinonjs/commons": "^3.0.0" - } - }, - "scripts/test-data/node_modules/@types/jest": { - "version": "29.5.14", - "dev": true, - "license": "MIT", - "dependencies": { - "expect": "^29.0.0", - "pretty-format": "^29.0.0" + "node": ">=12" } }, - "scripts/test-data/node_modules/babel-jest": { - "version": "29.7.0", - "dev": true, + "scripts/utilities/supplier-data/node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", "license": "MIT", "dependencies": { - "@jest/transform": "^29.7.0", - "@types/babel__core": "^7.1.14", - "babel-plugin-istanbul": "^6.1.1", - "babel-preset-jest": "^29.6.3", - "chalk": "^4.0.0", - "graceful-fs": "^4.2.9", - "slash": "^3.0.0" + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" }, "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - }, - "peerDependencies": { - "@babel/core": "^7.8.0" - } - }, - "scripts/test-data/node_modules/babel-plugin-istanbul": { - "version": "6.1.1", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "@babel/helper-plugin-utils": "^7.0.0", - "@istanbuljs/load-nyc-config": "^1.0.0", - "@istanbuljs/schema": "^0.1.2", - "istanbul-lib-instrument": "^5.0.4", - "test-exclude": "^6.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "scripts/test-data/node_modules/babel-plugin-istanbul/node_modules/istanbul-lib-instrument": { - "version": "5.2.1", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "@babel/core": "^7.12.3", - "@babel/parser": "^7.14.7", - "@istanbuljs/schema": "^0.1.2", - "istanbul-lib-coverage": "^3.2.0", - "semver": "^6.3.0" - }, - "engines": { - "node": ">=8" - } - }, - "scripts/test-data/node_modules/babel-plugin-istanbul/node_modules/semver": { - "version": "6.3.1", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - } - }, - "scripts/test-data/node_modules/babel-plugin-jest-hoist": { - "version": "29.6.3", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/template": "^7.3.3", - "@babel/types": "^7.3.3", - "@types/babel__core": "^7.1.14", - "@types/babel__traverse": "^7.0.6" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "scripts/test-data/node_modules/babel-preset-jest": { - "version": "29.6.3", - "dev": true, - "license": "MIT", - "dependencies": { - "babel-plugin-jest-hoist": "^29.6.3", - "babel-preset-current-node-syntax": "^1.0.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "scripts/test-data/node_modules/brace-expansion": { - "version": "1.1.12", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "scripts/test-data/node_modules/camelcase": { - "version": "6.3.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "scripts/test-data/node_modules/ci-info": { - "version": "3.9.0", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/sibiraj-s" - } - ], - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "scripts/test-data/node_modules/cjs-module-lexer": { - "version": "1.4.3", - "dev": true, - "license": "MIT" - }, - "scripts/test-data/node_modules/cliui": { - "version": "8.0.1", - "license": "ISC", - "dependencies": { - "string-width": "^4.2.0", - "strip-ansi": "^6.0.1", - "wrap-ansi": "^7.0.0" - }, - "engines": { - "node": ">=12" - } - }, - "scripts/test-data/node_modules/glob": { - "version": "7.2.3", - "dev": true, - "license": "ISC", - "dependencies": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.1.1", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - }, - "engines": { - "node": "*" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "scripts/test-data/node_modules/istanbul-lib-source-maps": { - "version": "4.0.1", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "debug": "^4.1.1", - "istanbul-lib-coverage": "^3.0.0", - "source-map": "^0.6.1" - }, - "engines": { - "node": ">=10" - } - }, - "scripts/test-data/node_modules/jest": { - "version": "29.7.0", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/core": "^29.7.0", - "@jest/types": "^29.6.3", - "import-local": "^3.0.2", - "jest-cli": "^29.7.0" - }, - "bin": { - "jest": "bin/jest.js" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - }, - "peerDependencies": { - "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" - }, - "peerDependenciesMeta": { - "node-notifier": { - "optional": true - } - } - }, - "scripts/test-data/node_modules/jest-changed-files": { - "version": "29.7.0", - "dev": true, - "license": "MIT", - "dependencies": { - "execa": "^5.0.0", - "jest-util": "^29.7.0", - "p-limit": "^3.1.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "scripts/test-data/node_modules/jest-circus": { - "version": "29.7.0", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/environment": "^29.7.0", - "@jest/expect": "^29.7.0", - "@jest/test-result": "^29.7.0", - "@jest/types": "^29.6.3", - "@types/node": "*", - "chalk": "^4.0.0", - "co": "^4.6.0", - "dedent": "^1.0.0", - "is-generator-fn": "^2.0.0", - "jest-each": "^29.7.0", - "jest-matcher-utils": "^29.7.0", - "jest-message-util": "^29.7.0", - "jest-runtime": "^29.7.0", - "jest-snapshot": "^29.7.0", - "jest-util": "^29.7.0", - "p-limit": "^3.1.0", - "pretty-format": "^29.7.0", - "pure-rand": "^6.0.0", - "slash": "^3.0.0", - "stack-utils": "^2.0.3" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "scripts/test-data/node_modules/jest-cli": { - "version": "29.7.0", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/core": "^29.7.0", - "@jest/test-result": "^29.7.0", - "@jest/types": "^29.6.3", - "chalk": "^4.0.0", - "create-jest": "^29.7.0", - "exit": "^0.1.2", - "import-local": "^3.0.2", - "jest-config": "^29.7.0", - "jest-util": "^29.7.0", - "jest-validate": "^29.7.0", - "yargs": "^17.3.1" - }, - "bin": { - "jest": "bin/jest.js" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - }, - "peerDependencies": { - "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" - }, - "peerDependenciesMeta": { - "node-notifier": { - "optional": true - } - } - }, - "scripts/test-data/node_modules/jest-config": { - "version": "29.7.0", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/core": "^7.11.6", - "@jest/test-sequencer": "^29.7.0", - "@jest/types": "^29.6.3", - "babel-jest": "^29.7.0", - "chalk": "^4.0.0", - "ci-info": "^3.2.0", - "deepmerge": "^4.2.2", - "glob": "^7.1.3", - "graceful-fs": "^4.2.9", - "jest-circus": "^29.7.0", - "jest-environment-node": "^29.7.0", - "jest-get-type": "^29.6.3", - "jest-regex-util": "^29.6.3", - "jest-resolve": "^29.7.0", - "jest-runner": "^29.7.0", - "jest-util": "^29.7.0", - "jest-validate": "^29.7.0", - "micromatch": "^4.0.4", - "parse-json": "^5.2.0", - "pretty-format": "^29.7.0", - "slash": "^3.0.0", - "strip-json-comments": "^3.1.1" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - }, - "peerDependencies": { - "@types/node": "*", - "ts-node": ">=9.0.0" - }, - "peerDependenciesMeta": { - "@types/node": { - "optional": true - }, - "ts-node": { - "optional": true - } - } - }, - "scripts/test-data/node_modules/jest-docblock": { - "version": "29.7.0", - "dev": true, - "license": "MIT", - "dependencies": { - "detect-newline": "^3.0.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "scripts/test-data/node_modules/jest-each": { - "version": "29.7.0", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/types": "^29.6.3", - "chalk": "^4.0.0", - "jest-get-type": "^29.6.3", - "jest-util": "^29.7.0", - "pretty-format": "^29.7.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "scripts/test-data/node_modules/jest-environment-node": { - "version": "29.7.0", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/environment": "^29.7.0", - "@jest/fake-timers": "^29.7.0", - "@jest/types": "^29.6.3", - "@types/node": "*", - "jest-mock": "^29.7.0", - "jest-util": "^29.7.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "scripts/test-data/node_modules/jest-haste-map": { - "version": "29.7.0", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/types": "^29.6.3", - "@types/graceful-fs": "^4.1.3", - "@types/node": "*", - "anymatch": "^3.0.3", - "fb-watchman": "^2.0.0", - "graceful-fs": "^4.2.9", - "jest-regex-util": "^29.6.3", - "jest-util": "^29.7.0", - "jest-worker": "^29.7.0", - "micromatch": "^4.0.4", - "walker": "^1.0.8" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - }, - "optionalDependencies": { - "fsevents": "^2.3.2" - } - }, - "scripts/test-data/node_modules/jest-leak-detector": { - "version": "29.7.0", - "dev": true, - "license": "MIT", - "dependencies": { - "jest-get-type": "^29.6.3", - "pretty-format": "^29.7.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "scripts/test-data/node_modules/jest-mock": { - "version": "29.7.0", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/types": "^29.6.3", - "@types/node": "*", - "jest-util": "^29.7.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "scripts/test-data/node_modules/jest-regex-util": { - "version": "29.6.3", - "dev": true, - "license": "MIT", - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "scripts/test-data/node_modules/jest-resolve": { - "version": "29.7.0", - "dev": true, - "license": "MIT", - "dependencies": { - "chalk": "^4.0.0", - "graceful-fs": "^4.2.9", - "jest-haste-map": "^29.7.0", - "jest-pnp-resolver": "^1.2.2", - "jest-util": "^29.7.0", - "jest-validate": "^29.7.0", - "resolve": "^1.20.0", - "resolve.exports": "^2.0.0", - "slash": "^3.0.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "scripts/test-data/node_modules/jest-resolve-dependencies": { - "version": "29.7.0", - "dev": true, - "license": "MIT", - "dependencies": { - "jest-regex-util": "^29.6.3", - "jest-snapshot": "^29.7.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "scripts/test-data/node_modules/jest-runner": { - "version": "29.7.0", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/console": "^29.7.0", - "@jest/environment": "^29.7.0", - "@jest/test-result": "^29.7.0", - "@jest/transform": "^29.7.0", - "@jest/types": "^29.6.3", - "@types/node": "*", - "chalk": "^4.0.0", - "emittery": "^0.13.1", - "graceful-fs": "^4.2.9", - "jest-docblock": "^29.7.0", - "jest-environment-node": "^29.7.0", - "jest-haste-map": "^29.7.0", - "jest-leak-detector": "^29.7.0", - "jest-message-util": "^29.7.0", - "jest-resolve": "^29.7.0", - "jest-runtime": "^29.7.0", - "jest-util": "^29.7.0", - "jest-watcher": "^29.7.0", - "jest-worker": "^29.7.0", - "p-limit": "^3.1.0", - "source-map-support": "0.5.13" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "scripts/test-data/node_modules/jest-runtime": { - "version": "29.7.0", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/environment": "^29.7.0", - "@jest/fake-timers": "^29.7.0", - "@jest/globals": "^29.7.0", - "@jest/source-map": "^29.6.3", - "@jest/test-result": "^29.7.0", - "@jest/transform": "^29.7.0", - "@jest/types": "^29.6.3", - "@types/node": "*", - "chalk": "^4.0.0", - "cjs-module-lexer": "^1.0.0", - "collect-v8-coverage": "^1.0.0", - "glob": "^7.1.3", - "graceful-fs": "^4.2.9", - "jest-haste-map": "^29.7.0", - "jest-message-util": "^29.7.0", - "jest-mock": "^29.7.0", - "jest-regex-util": "^29.6.3", - "jest-resolve": "^29.7.0", - "jest-snapshot": "^29.7.0", - "jest-util": "^29.7.0", - "slash": "^3.0.0", - "strip-bom": "^4.0.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "scripts/test-data/node_modules/jest-snapshot": { - "version": "29.7.0", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/core": "^7.11.6", - "@babel/generator": "^7.7.2", - "@babel/plugin-syntax-jsx": "^7.7.2", - "@babel/plugin-syntax-typescript": "^7.7.2", - "@babel/types": "^7.3.3", - "@jest/expect-utils": "^29.7.0", - "@jest/transform": "^29.7.0", - "@jest/types": "^29.6.3", - "babel-preset-current-node-syntax": "^1.0.0", - "chalk": "^4.0.0", - "expect": "^29.7.0", - "graceful-fs": "^4.2.9", - "jest-diff": "^29.7.0", - "jest-get-type": "^29.6.3", - "jest-matcher-utils": "^29.7.0", - "jest-message-util": "^29.7.0", - "jest-util": "^29.7.0", - "natural-compare": "^1.4.0", - "pretty-format": "^29.7.0", - "semver": "^7.5.3" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "scripts/test-data/node_modules/jest-validate": { - "version": "29.7.0", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/types": "^29.6.3", - "camelcase": "^6.2.0", - "chalk": "^4.0.0", - "jest-get-type": "^29.6.3", - "leven": "^3.1.0", - "pretty-format": "^29.7.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "scripts/test-data/node_modules/jest-watcher": { - "version": "29.7.0", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/test-result": "^29.7.0", - "@jest/types": "^29.6.3", - "@types/node": "*", - "ansi-escapes": "^4.2.1", - "chalk": "^4.0.0", - "emittery": "^0.13.1", - "jest-util": "^29.7.0", - "string-length": "^4.0.1" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "scripts/test-data/node_modules/jest-worker": { - "version": "29.7.0", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/node": "*", - "jest-util": "^29.7.0", - "merge-stream": "^2.0.0", - "supports-color": "^8.0.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "scripts/test-data/node_modules/minimatch": { - "version": "3.1.2", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, - "scripts/test-data/node_modules/pure-rand": { - "version": "6.1.0", - "dev": true, - "funding": [ - { - "type": "individual", - "url": "https://github.com/sponsors/dubzzz" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/fast-check" - } - ], - "license": "MIT" - }, - "scripts/test-data/node_modules/signal-exit": { - "version": "3.0.7", - "dev": true, - "license": "ISC" - }, - "scripts/test-data/node_modules/supports-color": { - "version": "8.1.1", - "dev": true, - "license": "MIT", - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/supports-color?sponsor=1" - } - }, - "scripts/test-data/node_modules/wrap-ansi": { - "version": "7.0.0", - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" - }, - "engines": { - "node": ">=10" + "node": ">=10" }, "funding": { "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, - "scripts/test-data/node_modules/write-file-atomic": { - "version": "4.0.2", - "dev": true, - "license": "ISC", - "dependencies": { - "imurmurhash": "^0.1.4", - "signal-exit": "^3.0.7" - }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || >=16.0.0" - } - }, - "scripts/test-data/node_modules/yargs": { + "scripts/utilities/supplier-data/node_modules/yargs": { "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", "license": "MIT", "dependencies": { "cliui": "^8.0.1", From b0d773dabc1eecdea5d6e7c7ca6b474951badbd6 Mon Sep 17 00:00:00 2001 From: Tim Ireland Date: Thu, 30 Oct 2025 12:03:29 +0000 Subject: [PATCH 05/14] Update .gitleaksignore added gitleaks ignore --- .gitleaksignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitleaksignore b/.gitleaksignore index 803deb3d..996b4388 100644 --- a/.gitleaksignore +++ b/.gitleaksignore @@ -15,3 +15,4 @@ b1f85a7faf54eaf66074d7a6daa093aefe6b3ebe:sdk/python/pyproject.toml:ipv4:25 93e54b6baa390529aab08d9fd956837f7bb3f30:src/src.sln:ipv4:3 493e54b6baa390529aab08d9fd956837f7bb3f30:src/src.sln:ipv4:3 d8aaf7e033bf78fff491caa148897be266b60f67:src/src.sln:ipv4:3 +e12407e09151898bfd8d049d57eee9db9977d56b:.github/copilot-instructions.md:generic-api-key:213 From 98fa540e1a0afbe7e5381162b225b0851786e96b Mon Sep 17 00:00:00 2001 From: Mark Slowey Date: Fri, 31 Oct 2025 09:54:05 +0000 Subject: [PATCH 06/14] CCM-11600: fix some imports --- internal/datastore/src/supplier-repository.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/internal/datastore/src/supplier-repository.ts b/internal/datastore/src/supplier-repository.ts index 5256da9a..14a82765 100644 --- a/internal/datastore/src/supplier-repository.ts +++ b/internal/datastore/src/supplier-repository.ts @@ -6,8 +6,6 @@ import { } from '@aws-sdk/lib-dynamodb'; import { Supplier, SupplierSchema } from './types'; import { Logger } from 'pino'; -import { v4 as uuidv4 } from 'uuid'; -import z from 'zod'; export type SupplierRepositoryConfig = { suppliersTableName: string From 00ea22755704fc16a13000a9d0df7143f6f069bb Mon Sep 17 00:00:00 2001 From: Steve Buxton Date: Mon, 3 Nov 2025 15:26:33 +0000 Subject: [PATCH 07/14] Add certificate expiry check --- .../api/module_authorizer_lambda.tf | 20 + lambdas/authorizer/package.json | 5 +- .../authorizer/src/__tests__/index.test.ts | 112 ++- lambdas/authorizer/src/authorizer.ts | 127 +++ lambdas/authorizer/src/deps.ts | 19 + lambdas/authorizer/src/env.ts | 10 + lambdas/authorizer/src/index.ts | 76 +- package-lock.json | 881 +++++++++++++++--- 8 files changed, 1057 insertions(+), 193 deletions(-) create mode 100644 lambdas/authorizer/src/authorizer.ts create mode 100644 lambdas/authorizer/src/deps.ts create mode 100644 lambdas/authorizer/src/env.ts diff --git a/infrastructure/terraform/components/api/module_authorizer_lambda.tf b/infrastructure/terraform/components/api/module_authorizer_lambda.tf index 9d56c950..76300262 100644 --- a/infrastructure/terraform/components/api/module_authorizer_lambda.tf +++ b/infrastructure/terraform/components/api/module_authorizer_lambda.tf @@ -30,4 +30,24 @@ module "authorizer_lambda" { send_to_firehose = true log_destination_arn = local.destination_arn log_subscription_role_arn = local.acct.log_subscription_role_arn + + lambda_env_vars = { + CLOUDWATCH_NAMESPACE = "/aws/api-gateway/supplier/alarms" + CLIENT_CERTIFICATE_EXPIRATION_ALERT_DAYS = 14 + } +} + +data "aws_iam_policy_document" "authorizer_lambda" { + statement { + sid = "AllowPutMetricData" + effect = "Allow" + + actions = [ + "cloudwatch:PutMetricData" + ] + + resources = [ + "*" + ] + } } diff --git a/lambdas/authorizer/package.json b/lambdas/authorizer/package.json index 026072f7..dd3ef0a3 100644 --- a/lambdas/authorizer/package.json +++ b/lambdas/authorizer/package.json @@ -1,6 +1,9 @@ { "dependencies": { - "esbuild": "^0.25.11" + "@aws-sdk/client-cloudwatch": "^3.922.0", + "esbuild": "^0.25.11", + "pino": "^10.1.0", + "zod": "^4.1.12" }, "devDependencies": { "@tsconfig/node22": "^22.0.2", diff --git a/lambdas/authorizer/src/__tests__/index.test.ts b/lambdas/authorizer/src/__tests__/index.test.ts index 71bde62d..d8599f93 100644 --- a/lambdas/authorizer/src/__tests__/index.test.ts +++ b/lambdas/authorizer/src/__tests__/index.test.ts @@ -1,5 +1,20 @@ -import { APIGatewayRequestAuthorizerEvent, Callback, Context } from 'aws-lambda'; -import { handler } from '../index'; +import { APIGatewayEventClientCertificate, APIGatewayRequestAuthorizerEvent, Callback, Context } from 'aws-lambda'; +import { Deps } from '../deps'; +import pino from 'pino'; +import { EnvVars } from '../env'; +import { createAuthorizerHandler } from '../authorizer'; + +const mockedDeps: jest.Mocked = { + logger: { info: jest.fn(), error: jest.fn() } as unknown as pino.Logger, + env: { + CLOUDWATCH_NAMESPACE: 'cloudwatch-namespace', + CLIENT_CERTIFICATE_EXPIRATION_ALERT_DAYS: 14 + } as unknown as EnvVars, + cloudWatchClient: { + send: jest.fn().mockResolvedValue({}), + } as any, + } as Deps; + describe('Authorizer Lambda Function', () => { let mockEvent: APIGatewayRequestAuthorizerEvent; @@ -11,17 +26,20 @@ describe('Authorizer Lambda Function', () => { type: 'REQUEST', methodArn: 'arn:aws:execute-api:region:account-id:api-id/stage/GET/resource', headers: {}, - pathParameters: {} + pathParameters: {}, + requestContext: {identity: {clientCert: null}}, } as APIGatewayRequestAuthorizerEvent; mockContext = {} as Context; mockCallback = jest.fn(); }); - it('Should allow access when headers match', () => { + it('Should allow access when headers match', async() => { mockEvent.headers = { headerauth1: 'headervalue1' }; + const handler = createAuthorizerHandler(mockedDeps); handler(mockEvent, mockContext, mockCallback); + await new Promise(process.nextTick); expect(mockCallback).toHaveBeenCalledWith(null, expect.objectContaining({ policyDocument: expect.objectContaining({ @@ -34,10 +52,12 @@ describe('Authorizer Lambda Function', () => { })); }); - it('Should deny access when headers do not match', () => { + it('Should deny access when headers do not match', async() => { mockEvent.headers = { headerauth1: 'wrongValue' }; + const handler = createAuthorizerHandler(mockedDeps); handler(mockEvent, mockContext, mockCallback); + await new Promise(process.nextTick); expect(mockCallback).toHaveBeenCalledWith(null, expect.objectContaining({ policyDocument: expect.objectContaining({ @@ -50,10 +70,12 @@ describe('Authorizer Lambda Function', () => { })); }); - it('Should handle null headers gracefully', () => { + it('Should handle null headers gracefully', async() => { mockEvent.headers = null; + const handler = createAuthorizerHandler(mockedDeps); handler(mockEvent, mockContext, mockCallback); + await new Promise(process.nextTick); expect(mockCallback).toHaveBeenCalledWith(null, expect.objectContaining({ policyDocument: expect.objectContaining({ @@ -66,10 +88,12 @@ describe('Authorizer Lambda Function', () => { })); }); - it('Should handle defined headers correctly', () => { + it('Should handle defined headers correctly', async() => { mockEvent.headers = { headerauth1: 'headervalue1' }; + const handler = createAuthorizerHandler(mockedDeps); handler(mockEvent, mockContext, mockCallback); + await new Promise(process.nextTick); expect(mockCallback).toHaveBeenCalledWith(null, expect.objectContaining({ policyDocument: expect.objectContaining({ @@ -82,14 +106,16 @@ describe('Authorizer Lambda Function', () => { })); }); - it('Should handle additional headers correctly', () => { + it('Should handle additional headers correctly', async() => { mockEvent.headers = { headerauth1: 'headervalue1' , otherheader1: 'headervalue2', otherheader2: 'headervalue3' }; + const handler = createAuthorizerHandler(mockedDeps); handler(mockEvent, mockContext, mockCallback); + await new Promise(process.nextTick); expect(mockCallback).toHaveBeenCalledWith(null, expect.objectContaining({ policyDocument: expect.objectContaining({ @@ -101,4 +127,74 @@ describe('Authorizer Lambda Function', () => { }), })); }); + + describe('Certificate expiry check', () => { + + beforeEach(() => { + jest.useFakeTimers({ doNotFake: ['nextTick'] }) + .setSystemTime(new Date('2025-11-03T14:19:00Z')); + }); + + afterEach(() => { + jest.useRealTimers(); + }) + + it('Should not send CloudWatch metric when certificate is null', async () => { + mockEvent.headers = { headerauth1: 'headervalue1' }; + mockEvent.requestContext.identity.clientCert = null; + + const handler = createAuthorizerHandler(mockedDeps); + handler(mockEvent, mockContext, mockCallback); + await new Promise(process.nextTick); + + expect(mockedDeps.cloudWatchClient.send).not.toHaveBeenCalled(); + }); + + it('Should send CloudWatch metric when the certificate expiry threshold is reached', async () => { + mockEvent.headers = { headerauth1: 'headervalue1' }; + mockEvent.requestContext.identity.clientCert = buildCertWithExpiry('2025-11-17T14:19:00Z'); + + const handler = createAuthorizerHandler(mockedDeps); + handler(mockEvent, mockContext, mockCallback); + await new Promise(process.nextTick); + + expect(mockedDeps.cloudWatchClient.send).toHaveBeenCalledWith( + expect.objectContaining({ + input: { + Namespace: 'cloudwatch-namespace', + MetricData: [ + { + MetricName: 'apim-client-certificate-near-expiry', + Dimensions: [ + { Name: 'SUBJECT_DN', Value: 'CN=test-subject' }, + { Name: 'NOT_AFTER', Value: '2025-11-17T14:19:00Z' }, + ], + }, + ], + }, + }) + ); + }); + + it('Should not send CloudWatch metric when the certificate expiry threshold is not yet reached', async () => { + mockEvent.headers = { headerauth1: 'headervalue1' }; + mockEvent.requestContext.identity.clientCert = buildCertWithExpiry('2025-11-18T14:19:00Z'); + + const handler = createAuthorizerHandler(mockedDeps); + handler(mockEvent, mockContext, mockCallback); + await new Promise(process.nextTick); + + expect(mockedDeps.cloudWatchClient.send).not.toHaveBeenCalled(); + }); + }); + + function buildCertWithExpiry(expiry: string): APIGatewayEventClientCertificate { + + return { + subjectDN: 'CN=test-subject', + validity: { + notAfter: expiry, + } as APIGatewayEventClientCertificate['validity'], + } as APIGatewayEventClientCertificate; + } }); diff --git a/lambdas/authorizer/src/authorizer.ts b/lambdas/authorizer/src/authorizer.ts new file mode 100644 index 00000000..ff8469a1 --- /dev/null +++ b/lambdas/authorizer/src/authorizer.ts @@ -0,0 +1,127 @@ +// A simple request-based authorizer example to demonstrate how to use request +// parameters to allow or deny a request. In this example, a request is +// authorized if the client-supplied HeaderAuth1 header and stage variable of StageVar1 +// both match specified values of 'headerValue1' and 'stageValue1', respectively. +// +// Example curl request (replace and as appropriate): +// +// curl -H "HeaderAuth1: headerValue1" \ +// "//your-resource" +// + +// See https://docs.aws.amazon.com/apigateway/latest/developerguide/apigateway-use-lambda-authorizer.html for the original JS documentation + +import {APIGatewayAuthorizerResult, APIGatewayEventClientCertificate, APIGatewayRequestAuthorizerEvent, APIGatewayRequestAuthorizerHandler, + Callback, Context } from 'aws-lambda'; +import { Deps } from './deps'; +import { PutMetricDataCommand } from '@aws-sdk/client-cloudwatch'; + +export function createAuthorizerHandler(deps: Deps): APIGatewayRequestAuthorizerHandler { + + return ( + event: APIGatewayRequestAuthorizerEvent, + context: Context, + callback: Callback + ): void => { + deps.logger.info(event, 'Received event'); + + const headers = event.headers || {}; + + checkCertificateExpiry(event.requestContext.identity.clientCert, deps).then(() => { + // Perform authorization to return the Allow policy for correct parameters and + // the 'Unauthorized' error, otherwise. + if ( + headers['headerauth1'] === 'headervalue1' + ) { + deps.logger.info('Allow event'); + callback(null, generateAllow('me', event.methodArn)); + } else { + deps.logger.info('Deny event'); + callback(null, generateDeny('me', event.methodArn)); + } + }); + }; +} + + + // Helper function to generate an IAM policy + function generatePolicy( + principalId: string, + effect: 'Allow' | 'Deny', + resource: string + ): APIGatewayAuthorizerResult { + // Required output: + const authResponse: APIGatewayAuthorizerResult = { + principalId, + policyDocument: { + Version: '2012-10-17', + Statement: [ + { + Action: 'execute-api:Invoke', + Effect: effect, + Resource: resource, + }, + ], + }, + context: { + stringKey: 'stringval', + numberKey: 123, + booleanKey: true, + }, + }; + return authResponse; + } + +function generateAllow(principalId: string, resource: string): APIGatewayAuthorizerResult { + return generatePolicy(principalId, 'Allow', resource); +} + +function generateDeny(principalId: string, resource: string): APIGatewayAuthorizerResult { + return generatePolicy(principalId, 'Deny', resource); +} + +function getCertificateExpiryInDays(certificate: APIGatewayEventClientCertificate): number { + const now = new Date().getTime(); + const expiry = new Date(certificate.validity.notAfter).getTime(); + return (expiry - now) / (1000 * 60 * 60 * 24); +} + +async function checkCertificateExpiry(certificate: APIGatewayEventClientCertificate | null, deps: Deps): Promise { + deps.logger.info({ + description: 'Client certificate details', + issuerDN: certificate?.issuerDN, + subjectDN: certificate?.subjectDN, + validity: certificate?.validity, + }); + + if (!certificate) { + // In a real production environment, we won't have got this far if there wasn't a cert + return; + } + + const expiry = getCertificateExpiryInDays(certificate); + + if (expiry <= deps.env.CLIENT_CERTIFICATE_EXPIRATION_ALERT_DAYS) { + const { subjectDN, validity } = certificate; + + deps.logger.info({ + description: 'Client certificate near expiry', + certificateExpiry: validity.notAfter, + subjectDN, + }); + await deps.cloudWatchClient.send(buildCloudWatchCommand(deps.env.CLOUDWATCH_NAMESPACE, certificate)); + } + + function buildCloudWatchCommand(namespace: string, certificate: APIGatewayEventClientCertificate): PutMetricDataCommand { + return new PutMetricDataCommand({ + MetricData: [{ + MetricName: 'apim-client-certificate-near-expiry', + Dimensions: [ + {Name: 'SUBJECT_DN', Value: certificate.subjectDN}, + {Name: 'NOT_AFTER', Value: certificate.validity.notAfter} + ] + }], + Namespace: namespace + }); + } +}; diff --git a/lambdas/authorizer/src/deps.ts b/lambdas/authorizer/src/deps.ts new file mode 100644 index 00000000..02889da1 --- /dev/null +++ b/lambdas/authorizer/src/deps.ts @@ -0,0 +1,19 @@ +import { CloudWatchClient } from "@aws-sdk/client-cloudwatch"; +import pino from 'pino'; +import { envVars, EnvVars } from "./env"; + +export type Deps = { + cloudWatchClient: CloudWatchClient; + logger: pino.Logger; + env: EnvVars; +}; + +export function createDependenciesContainer(): Deps { + const log = pino(); + + return { + cloudWatchClient: new CloudWatchClient({}), + logger: log, + env: envVars + }; +} diff --git a/lambdas/authorizer/src/env.ts b/lambdas/authorizer/src/env.ts new file mode 100644 index 00000000..95fc5c29 --- /dev/null +++ b/lambdas/authorizer/src/env.ts @@ -0,0 +1,10 @@ +import {z} from 'zod'; + +const EnvVarsSchema = z.object({ + CLOUDWATCH_NAMESPACE: z.string(), + CLIENT_CERTIFICATE_EXPIRATION_ALERT_DAYS: z.coerce.number().int() +}); + +export type EnvVars = z.infer; + +export const envVars = EnvVarsSchema.parse(process.env); diff --git a/lambdas/authorizer/src/index.ts b/lambdas/authorizer/src/index.ts index 9797d5d2..f65d0f3d 100644 --- a/lambdas/authorizer/src/index.ts +++ b/lambdas/authorizer/src/index.ts @@ -1,74 +1,6 @@ -// A simple request-based authorizer example to demonstrate how to use request -// parameters to allow or deny a request. In this example, a request is -// authorized if the client-supplied HeaderAuth1 header and stage variable of StageVar1 -// both match specified values of 'headerValue1' and 'stageValue1', respectively. -// -// Example curl request (replace and as appropriate): -// -// curl -H "HeaderAuth1: headerValue1" \ -// "//your-resource" -// +import { createAuthorizerHandler } from "./authorizer"; +import { createDependenciesContainer } from "./deps"; -// See https://docs.aws.amazon.com/apigateway/latest/developerguide/apigateway-use-lambda-authorizer.html for the original JS documentation +const container = createDependenciesContainer(); -import { APIGatewayAuthorizerResult, APIGatewayRequestAuthorizerEvent, Callback, Context } from 'aws-lambda'; -import pino from 'pino'; - -export const handler = ( - event: APIGatewayRequestAuthorizerEvent, - context: Context, - callback: Callback, - log = pino() -): void => { - log.info(event, 'Received event'); - - const headers = event.headers || {}; - - // Perform authorization to return the Allow policy for correct parameters and - // the 'Unauthorized' error, otherwise. - if ( - headers['headerauth1'] === 'headervalue1' - ) { - log.info('Allow event'); - callback(null, generateAllow('me', event.methodArn)); - } else { - log.info('Deny event'); - callback(null, generateDeny('me', event.methodArn)); - } -}; - -// Helper function to generate an IAM policy -function generatePolicy( - principalId: string, - effect: 'Allow' | 'Deny', - resource: string -): APIGatewayAuthorizerResult { - // Required output: - const authResponse: APIGatewayAuthorizerResult = { - principalId, - policyDocument: { - Version: '2012-10-17', - Statement: [ - { - Action: 'execute-api:Invoke', - Effect: effect, - Resource: resource, - }, - ], - }, - context: { - stringKey: 'stringval', - numberKey: 123, - booleanKey: true, - }, - }; - return authResponse; -} - -function generateAllow(principalId: string, resource: string): APIGatewayAuthorizerResult { - return generatePolicy(principalId, 'Allow', resource); -} - -function generateDeny(principalId: string, resource: string): APIGatewayAuthorizerResult { - return generatePolicy(principalId, 'Deny', resource); -} +export const handler = createAuthorizerHandler(container); diff --git a/package-lock.json b/package-lock.json index be173a7a..1c3c7750 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1951,7 +1951,10 @@ "name": "nhs-notify-supplier-authorizer", "version": "0.0.1", "dependencies": { - "esbuild": "^0.25.11" + "@aws-sdk/client-cloudwatch": "^3.922.0", + "esbuild": "^0.25.11", + "pino": "^10.1.0", + "zod": "^4.1.12" }, "devDependencies": { "@tsconfig/node22": "^22.0.2", @@ -1962,6 +1965,37 @@ "typescript": "^5.8.3" } }, + "lambdas/authorizer/node_modules/pino": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/pino/-/pino-10.1.0.tgz", + "integrity": "sha512-0zZC2ygfdqvqK8zJIr1e+wT1T/L+LF6qvqvbzEQ6tiMAoTqEVK9a1K3YRu8HEUvGEvNqZyPJTtb2sNIoTkB83w==", + "license": "MIT", + "dependencies": { + "@pinojs/redact": "^0.4.0", + "atomic-sleep": "^1.0.0", + "on-exit-leak-free": "^2.1.0", + "pino-abstract-transport": "^2.0.0", + "pino-std-serializers": "^7.0.0", + "process-warning": "^5.0.0", + "quick-format-unescaped": "^4.0.3", + "real-require": "^0.2.0", + "safe-stable-stringify": "^2.3.1", + "sonic-boom": "^4.0.1", + "thread-stream": "^3.0.0" + }, + "bin": { + "pino": "bin.js" + } + }, + "lambdas/authorizer/node_modules/zod": { + "version": "4.1.12", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.1.12.tgz", + "integrity": "sha512-JInaHOamG8pt5+Ey8kGmdcAcg3OL9reK8ltczgHTAwNhMys/6ThXHityHxVV2p3fkw/c+MAvBHFVYHFZDmjMCQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, "node_modules/@ampproject/remapping": { "version": "2.3.0", "dev": true, @@ -2632,6 +2666,533 @@ ], "license": "MIT" }, + "node_modules/@aws-sdk/client-cloudwatch": { + "version": "3.922.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-cloudwatch/-/client-cloudwatch-3.922.0.tgz", + "integrity": "sha512-Bmh2GSfw7zRhANK3X3Ygs7G2IjIPEopTlRObEAqJSYq083jPSFtt6FyzuDpv1FmYSZJQ8wuoIjMdsxiLP/lgAw==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/core": "3.922.0", + "@aws-sdk/credential-provider-node": "3.922.0", + "@aws-sdk/middleware-host-header": "3.922.0", + "@aws-sdk/middleware-logger": "3.922.0", + "@aws-sdk/middleware-recursion-detection": "3.922.0", + "@aws-sdk/middleware-user-agent": "3.922.0", + "@aws-sdk/region-config-resolver": "3.922.0", + "@aws-sdk/types": "3.922.0", + "@aws-sdk/util-endpoints": "3.922.0", + "@aws-sdk/util-user-agent-browser": "3.922.0", + "@aws-sdk/util-user-agent-node": "3.922.0", + "@smithy/config-resolver": "^4.4.1", + "@smithy/core": "^3.17.2", + "@smithy/fetch-http-handler": "^5.3.5", + "@smithy/hash-node": "^4.2.4", + "@smithy/invalid-dependency": "^4.2.4", + "@smithy/middleware-compression": "^4.3.6", + "@smithy/middleware-content-length": "^4.2.4", + "@smithy/middleware-endpoint": "^4.3.6", + "@smithy/middleware-retry": "^4.4.6", + "@smithy/middleware-serde": "^4.2.4", + "@smithy/middleware-stack": "^4.2.4", + "@smithy/node-config-provider": "^4.3.4", + "@smithy/node-http-handler": "^4.4.4", + "@smithy/protocol-http": "^5.3.4", + "@smithy/smithy-client": "^4.9.2", + "@smithy/types": "^4.8.1", + "@smithy/url-parser": "^4.2.4", + "@smithy/util-base64": "^4.3.0", + "@smithy/util-body-length-browser": "^4.2.0", + "@smithy/util-body-length-node": "^4.2.1", + "@smithy/util-defaults-mode-browser": "^4.3.5", + "@smithy/util-defaults-mode-node": "^4.2.7", + "@smithy/util-endpoints": "^3.2.4", + "@smithy/util-middleware": "^4.2.4", + "@smithy/util-retry": "^4.2.4", + "@smithy/util-utf8": "^4.2.0", + "@smithy/util-waiter": "^4.2.4", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-cloudwatch/node_modules/@aws-sdk/client-sso": { + "version": "3.922.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-sso/-/client-sso-3.922.0.tgz", + "integrity": "sha512-jdHs7uy7cSpiMvrxhYmqHyJxgK7hyqw4plG8OQ4YTBpq0SbfAxdoOuOkwJ1IVUUQho4otR1xYYjiX/8e8J8qwQ==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/core": "3.922.0", + "@aws-sdk/middleware-host-header": "3.922.0", + "@aws-sdk/middleware-logger": "3.922.0", + "@aws-sdk/middleware-recursion-detection": "3.922.0", + "@aws-sdk/middleware-user-agent": "3.922.0", + "@aws-sdk/region-config-resolver": "3.922.0", + "@aws-sdk/types": "3.922.0", + "@aws-sdk/util-endpoints": "3.922.0", + "@aws-sdk/util-user-agent-browser": "3.922.0", + "@aws-sdk/util-user-agent-node": "3.922.0", + "@smithy/config-resolver": "^4.4.1", + "@smithy/core": "^3.17.2", + "@smithy/fetch-http-handler": "^5.3.5", + "@smithy/hash-node": "^4.2.4", + "@smithy/invalid-dependency": "^4.2.4", + "@smithy/middleware-content-length": "^4.2.4", + "@smithy/middleware-endpoint": "^4.3.6", + "@smithy/middleware-retry": "^4.4.6", + "@smithy/middleware-serde": "^4.2.4", + "@smithy/middleware-stack": "^4.2.4", + "@smithy/node-config-provider": "^4.3.4", + "@smithy/node-http-handler": "^4.4.4", + "@smithy/protocol-http": "^5.3.4", + "@smithy/smithy-client": "^4.9.2", + "@smithy/types": "^4.8.1", + "@smithy/url-parser": "^4.2.4", + "@smithy/util-base64": "^4.3.0", + "@smithy/util-body-length-browser": "^4.2.0", + "@smithy/util-body-length-node": "^4.2.1", + "@smithy/util-defaults-mode-browser": "^4.3.5", + "@smithy/util-defaults-mode-node": "^4.2.7", + "@smithy/util-endpoints": "^3.2.4", + "@smithy/util-middleware": "^4.2.4", + "@smithy/util-retry": "^4.2.4", + "@smithy/util-utf8": "^4.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-cloudwatch/node_modules/@aws-sdk/core": { + "version": "3.922.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.922.0.tgz", + "integrity": "sha512-EvfP4cqJfpO3L2v5vkIlTkMesPtRwWlMfsaW6Tpfm7iYfBOuTi6jx60pMDMTyJNVfh6cGmXwh/kj1jQdR+w99Q==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.922.0", + "@aws-sdk/xml-builder": "3.921.0", + "@smithy/core": "^3.17.2", + "@smithy/node-config-provider": "^4.3.4", + "@smithy/property-provider": "^4.2.4", + "@smithy/protocol-http": "^5.3.4", + "@smithy/signature-v4": "^5.3.4", + "@smithy/smithy-client": "^4.9.2", + "@smithy/types": "^4.8.1", + "@smithy/util-base64": "^4.3.0", + "@smithy/util-middleware": "^4.2.4", + "@smithy/util-utf8": "^4.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-cloudwatch/node_modules/@aws-sdk/credential-provider-env": { + "version": "3.922.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-env/-/credential-provider-env-3.922.0.tgz", + "integrity": "sha512-WikGQpKkROJSK3D3E7odPjZ8tU7WJp5/TgGdRuZw3izsHUeH48xMv6IznafpRTmvHcjAbDQj4U3CJZNAzOK/OQ==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "3.922.0", + "@aws-sdk/types": "3.922.0", + "@smithy/property-provider": "^4.2.4", + "@smithy/types": "^4.8.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-cloudwatch/node_modules/@aws-sdk/credential-provider-http": { + "version": "3.922.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-http/-/credential-provider-http-3.922.0.tgz", + "integrity": "sha512-i72DgHMK7ydAEqdzU0Duqh60Q8W59EZmRJ73y0Y5oFmNOqnYsAI+UXyOoCsubp+Dkr6+yOwAn1gPt1XGE9Aowg==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "3.922.0", + "@aws-sdk/types": "3.922.0", + "@smithy/fetch-http-handler": "^5.3.5", + "@smithy/node-http-handler": "^4.4.4", + "@smithy/property-provider": "^4.2.4", + "@smithy/protocol-http": "^5.3.4", + "@smithy/smithy-client": "^4.9.2", + "@smithy/types": "^4.8.1", + "@smithy/util-stream": "^4.5.5", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-cloudwatch/node_modules/@aws-sdk/credential-provider-ini": { + "version": "3.922.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.922.0.tgz", + "integrity": "sha512-bVF+pI5UCLNkvbiZr/t2fgTtv84s8FCdOGAPxQiQcw5qOZywNuuCCY3wIIchmQr6GJr8YFkEp5LgDCac5EC5aQ==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "3.922.0", + "@aws-sdk/credential-provider-env": "3.922.0", + "@aws-sdk/credential-provider-http": "3.922.0", + "@aws-sdk/credential-provider-process": "3.922.0", + "@aws-sdk/credential-provider-sso": "3.922.0", + "@aws-sdk/credential-provider-web-identity": "3.922.0", + "@aws-sdk/nested-clients": "3.922.0", + "@aws-sdk/types": "3.922.0", + "@smithy/credential-provider-imds": "^4.2.4", + "@smithy/property-provider": "^4.2.4", + "@smithy/shared-ini-file-loader": "^4.3.4", + "@smithy/types": "^4.8.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-cloudwatch/node_modules/@aws-sdk/credential-provider-node": { + "version": "3.922.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.922.0.tgz", + "integrity": "sha512-agCwaD6mBihToHkjycL8ObIS2XOnWypWZZWhJSoWyHwFrhEKz1zGvgylK9Dc711oUfU+zU6J8e0JPKNJMNb3BQ==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/credential-provider-env": "3.922.0", + "@aws-sdk/credential-provider-http": "3.922.0", + "@aws-sdk/credential-provider-ini": "3.922.0", + "@aws-sdk/credential-provider-process": "3.922.0", + "@aws-sdk/credential-provider-sso": "3.922.0", + "@aws-sdk/credential-provider-web-identity": "3.922.0", + "@aws-sdk/types": "3.922.0", + "@smithy/credential-provider-imds": "^4.2.4", + "@smithy/property-provider": "^4.2.4", + "@smithy/shared-ini-file-loader": "^4.3.4", + "@smithy/types": "^4.8.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-cloudwatch/node_modules/@aws-sdk/credential-provider-process": { + "version": "3.922.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-process/-/credential-provider-process-3.922.0.tgz", + "integrity": "sha512-1DZOYezT6okslpvMW7oA2q+y17CJd4fxjNFH0jtThfswdh9CtG62+wxenqO+NExttq0UMaKisrkZiVrYQBTShw==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "3.922.0", + "@aws-sdk/types": "3.922.0", + "@smithy/property-provider": "^4.2.4", + "@smithy/shared-ini-file-loader": "^4.3.4", + "@smithy/types": "^4.8.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-cloudwatch/node_modules/@aws-sdk/credential-provider-sso": { + "version": "3.922.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.922.0.tgz", + "integrity": "sha512-nbD3G3hShTYxLCkKMqLkLPtKwAAfxdY/k9jHtZmVBFXek2T6tQrqZHKxlAu+fd23Ga4/Aik7DLQQx1RA1a5ipg==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/client-sso": "3.922.0", + "@aws-sdk/core": "3.922.0", + "@aws-sdk/token-providers": "3.922.0", + "@aws-sdk/types": "3.922.0", + "@smithy/property-provider": "^4.2.4", + "@smithy/shared-ini-file-loader": "^4.3.4", + "@smithy/types": "^4.8.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-cloudwatch/node_modules/@aws-sdk/credential-provider-web-identity": { + "version": "3.922.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.922.0.tgz", + "integrity": "sha512-wjGIhgMHGGQfQTdFaJphNOKyAL8wZs6znJdHADPVURmgR+EWLyN/0fDO1u7wx8xaLMZpbHIFWBEvf9TritR/cQ==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "3.922.0", + "@aws-sdk/nested-clients": "3.922.0", + "@aws-sdk/types": "3.922.0", + "@smithy/property-provider": "^4.2.4", + "@smithy/shared-ini-file-loader": "^4.3.4", + "@smithy/types": "^4.8.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-cloudwatch/node_modules/@aws-sdk/middleware-host-header": { + "version": "3.922.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-host-header/-/middleware-host-header-3.922.0.tgz", + "integrity": "sha512-HPquFgBnq/KqKRVkiuCt97PmWbKtxQ5iUNLEc6FIviqOoZTmaYG3EDsIbuFBz9C4RHJU4FKLmHL2bL3FEId6AA==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.922.0", + "@smithy/protocol-http": "^5.3.4", + "@smithy/types": "^4.8.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-cloudwatch/node_modules/@aws-sdk/middleware-logger": { + "version": "3.922.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-logger/-/middleware-logger-3.922.0.tgz", + "integrity": "sha512-AkvYO6b80FBm5/kk2E636zNNcNgjztNNUxpqVx+huyGn9ZqGTzS4kLqW2hO6CBe5APzVtPCtiQsXL24nzuOlAg==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.922.0", + "@smithy/types": "^4.8.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-cloudwatch/node_modules/@aws-sdk/middleware-recursion-detection": { + "version": "3.922.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-recursion-detection/-/middleware-recursion-detection-3.922.0.tgz", + "integrity": "sha512-TtSCEDonV/9R0VhVlCpxZbp/9sxQvTTRKzIf8LxW3uXpby6Wl8IxEciBJlxmSkoqxh542WRcko7NYODlvL/gDA==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.922.0", + "@aws/lambda-invoke-store": "^0.1.1", + "@smithy/protocol-http": "^5.3.4", + "@smithy/types": "^4.8.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-cloudwatch/node_modules/@aws-sdk/middleware-user-agent": { + "version": "3.922.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-user-agent/-/middleware-user-agent-3.922.0.tgz", + "integrity": "sha512-N4Qx/9KP3oVQBJOrSghhz8iZFtUC2NNeSZt88hpPhbqAEAtuX8aD8OzVcpnAtrwWqy82Yd2YTxlkqMGkgqnBsQ==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "3.922.0", + "@aws-sdk/types": "3.922.0", + "@aws-sdk/util-endpoints": "3.922.0", + "@smithy/core": "^3.17.2", + "@smithy/protocol-http": "^5.3.4", + "@smithy/types": "^4.8.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-cloudwatch/node_modules/@aws-sdk/nested-clients": { + "version": "3.922.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/nested-clients/-/nested-clients-3.922.0.tgz", + "integrity": "sha512-uYvKCF1TGh/MuJ4TMqmUM0Csuao02HawcseG4LUDyxdUsd/EFuxalWq1Cx4fKZQ2K8F504efZBjctMAMNY+l7A==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/core": "3.922.0", + "@aws-sdk/middleware-host-header": "3.922.0", + "@aws-sdk/middleware-logger": "3.922.0", + "@aws-sdk/middleware-recursion-detection": "3.922.0", + "@aws-sdk/middleware-user-agent": "3.922.0", + "@aws-sdk/region-config-resolver": "3.922.0", + "@aws-sdk/types": "3.922.0", + "@aws-sdk/util-endpoints": "3.922.0", + "@aws-sdk/util-user-agent-browser": "3.922.0", + "@aws-sdk/util-user-agent-node": "3.922.0", + "@smithy/config-resolver": "^4.4.1", + "@smithy/core": "^3.17.2", + "@smithy/fetch-http-handler": "^5.3.5", + "@smithy/hash-node": "^4.2.4", + "@smithy/invalid-dependency": "^4.2.4", + "@smithy/middleware-content-length": "^4.2.4", + "@smithy/middleware-endpoint": "^4.3.6", + "@smithy/middleware-retry": "^4.4.6", + "@smithy/middleware-serde": "^4.2.4", + "@smithy/middleware-stack": "^4.2.4", + "@smithy/node-config-provider": "^4.3.4", + "@smithy/node-http-handler": "^4.4.4", + "@smithy/protocol-http": "^5.3.4", + "@smithy/smithy-client": "^4.9.2", + "@smithy/types": "^4.8.1", + "@smithy/url-parser": "^4.2.4", + "@smithy/util-base64": "^4.3.0", + "@smithy/util-body-length-browser": "^4.2.0", + "@smithy/util-body-length-node": "^4.2.1", + "@smithy/util-defaults-mode-browser": "^4.3.5", + "@smithy/util-defaults-mode-node": "^4.2.7", + "@smithy/util-endpoints": "^3.2.4", + "@smithy/util-middleware": "^4.2.4", + "@smithy/util-retry": "^4.2.4", + "@smithy/util-utf8": "^4.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-cloudwatch/node_modules/@aws-sdk/region-config-resolver": { + "version": "3.922.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/region-config-resolver/-/region-config-resolver-3.922.0.tgz", + "integrity": "sha512-44Y/rNNwhngR2KHp6gkx//TOr56/hx6s4l+XLjOqH7EBCHL7XhnrT1y92L+DLiroVr1tCSmO8eHQwBv0Y2+mvw==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.922.0", + "@smithy/config-resolver": "^4.4.1", + "@smithy/node-config-provider": "^4.3.4", + "@smithy/types": "^4.8.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-cloudwatch/node_modules/@aws-sdk/token-providers": { + "version": "3.922.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.922.0.tgz", + "integrity": "sha512-/inmPnjZE0ZBE16zaCowAvouSx05FJ7p6BQYuzlJ8vxEU0sS0Hf8fvhuiRnN9V9eDUPIBY+/5EjbMWygXL4wlQ==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "3.922.0", + "@aws-sdk/nested-clients": "3.922.0", + "@aws-sdk/types": "3.922.0", + "@smithy/property-provider": "^4.2.4", + "@smithy/shared-ini-file-loader": "^4.3.4", + "@smithy/types": "^4.8.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-cloudwatch/node_modules/@aws-sdk/types": { + "version": "3.922.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.922.0.tgz", + "integrity": "sha512-eLA6XjVobAUAMivvM7DBL79mnHyrm+32TkXNWZua5mnxF+6kQCfblKKJvxMZLGosO53/Ex46ogim8IY5Nbqv2w==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.8.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-cloudwatch/node_modules/@aws-sdk/util-endpoints": { + "version": "3.922.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-endpoints/-/util-endpoints-3.922.0.tgz", + "integrity": "sha512-4ZdQCSuNMY8HMlR1YN4MRDdXuKd+uQTeKIr5/pIM+g3TjInZoj8imvXudjcrFGA63UF3t92YVTkBq88mg58RXQ==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.922.0", + "@smithy/types": "^4.8.1", + "@smithy/url-parser": "^4.2.4", + "@smithy/util-endpoints": "^3.2.4", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-cloudwatch/node_modules/@aws-sdk/util-user-agent-browser": { + "version": "3.922.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-browser/-/util-user-agent-browser-3.922.0.tgz", + "integrity": "sha512-qOJAERZ3Plj1st7M4Q5henl5FRpE30uLm6L9edZqZXGR6c7ry9jzexWamWVpQ4H4xVAVmiO9dIEBAfbq4mduOA==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.922.0", + "@smithy/types": "^4.8.1", + "bowser": "^2.11.0", + "tslib": "^2.6.2" + } + }, + "node_modules/@aws-sdk/client-cloudwatch/node_modules/@aws-sdk/util-user-agent-node": { + "version": "3.922.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-node/-/util-user-agent-node-3.922.0.tgz", + "integrity": "sha512-NrPe/Rsr5kcGunkog0eBV+bY0inkRELsD2SacC4lQZvZiXf8VJ2Y7j+Yq1tB+h+FPLsdt3v9wItIvDf/laAm0Q==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/middleware-user-agent": "3.922.0", + "@aws-sdk/types": "3.922.0", + "@smithy/node-config-provider": "^4.3.4", + "@smithy/types": "^4.8.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + }, + "peerDependencies": { + "aws-crt": ">=1.0.0" + }, + "peerDependenciesMeta": { + "aws-crt": { + "optional": true + } + } + }, + "node_modules/@aws-sdk/client-cloudwatch/node_modules/@aws-sdk/xml-builder": { + "version": "3.921.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/xml-builder/-/xml-builder-3.921.0.tgz", + "integrity": "sha512-LVHg0jgjyicKKvpNIEMXIMr1EBViESxcPkqfOlT+X1FkmUMTNZEEVF18tOJg4m4hV5vxtkWcqtr4IEeWa1C41Q==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.8.1", + "fast-xml-parser": "5.2.5", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-cloudwatch/node_modules/@aws/lambda-invoke-store": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/@aws/lambda-invoke-store/-/lambda-invoke-store-0.1.1.tgz", + "integrity": "sha512-RcLam17LdlbSOSp9VxmUu1eI6Mwxp+OwhD2QhiSNmNCzoDb0EeUXTD2n/WbcnrAYMGlmf05th6QYq23VqvJqpA==", + "license": "Apache-2.0", + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-cloudwatch/node_modules/fast-xml-parser": { + "version": "5.2.5", + "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-5.2.5.tgz", + "integrity": "sha512-pfX9uG9Ki0yekDHx2SiuRIyFdyAr1kMIMitPvb0YBo8SUfKvia7w7FIyd/l6av85pFYRhZscS75MwMnbvY+hcQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT", + "dependencies": { + "strnum": "^2.1.0" + }, + "bin": { + "fxparser": "src/cli/cli.js" + } + }, + "node_modules/@aws-sdk/client-cloudwatch/node_modules/strnum": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/strnum/-/strnum-2.1.1.tgz", + "integrity": "sha512-7ZvoFTiCnGxBtDqJ//Cu6fWtZtc7Y3x+QOirG15wztbdngGSkht27o2pyGWrVy0b4WAy3jbKmnoK6g5VlVNUUw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT" + }, "node_modules/@aws-sdk/client-dynamodb": { "version": "3.864.0", "license": "Apache-2.0", @@ -7618,6 +8179,12 @@ "node": ">=14" } }, + "node_modules/@pinojs/redact": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@pinojs/redact/-/redact-0.4.0.tgz", + "integrity": "sha512-k2ENnmBugE/rzQfEcdWHcCY+/FM3VLzH9cYEsbdsoqrvzAKRhUZeRNhAZvB8OitQJ1TBed3yqWtdjzS6wJKBwg==", + "license": "MIT" + }, "node_modules/@pkgjs/parseargs": { "version": "0.11.0", "dev": true, @@ -7907,10 +8474,12 @@ } }, "node_modules/@smithy/abort-controller": { - "version": "4.2.0", + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/@smithy/abort-controller/-/abort-controller-4.2.4.tgz", + "integrity": "sha512-Z4DUr/AkgyFf1bOThW2HwzREagee0sB5ycl+hDiSZOfRLW8ZgrOjDi6g8mHH19yyU5E2A/64W3z6SMIf5XiUSQ==", "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^4.6.0", + "@smithy/types": "^4.8.1", "tslib": "^2.6.2" }, "engines": { @@ -7939,13 +8508,16 @@ } }, "node_modules/@smithy/config-resolver": { - "version": "4.3.0", + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/@smithy/config-resolver/-/config-resolver-4.4.1.tgz", + "integrity": "sha512-BciDJ5hkyYEGBBKMbjGB1A/Zq8bYZ41Zo9BMnGdKF6QD1fY4zIkYx6zui/0CHaVGnv6h0iy8y4rnPX9CPCAPyQ==", "license": "Apache-2.0", "dependencies": { - "@smithy/node-config-provider": "^4.3.0", - "@smithy/types": "^4.6.0", + "@smithy/node-config-provider": "^4.3.4", + "@smithy/types": "^4.8.1", "@smithy/util-config-provider": "^4.2.0", - "@smithy/util-middleware": "^4.2.0", + "@smithy/util-endpoints": "^3.2.4", + "@smithy/util-middleware": "^4.2.4", "tslib": "^2.6.2" }, "engines": { @@ -7953,16 +8525,18 @@ } }, "node_modules/@smithy/core": { - "version": "3.15.0", + "version": "3.17.2", + "resolved": "https://registry.npmjs.org/@smithy/core/-/core-3.17.2.tgz", + "integrity": "sha512-n3g4Nl1Te+qGPDbNFAYf+smkRVB+JhFsGy9uJXXZQEufoP4u0r+WLh6KvTDolCswaagysDc/afS1yvb2jnj1gQ==", "license": "Apache-2.0", "dependencies": { - "@smithy/middleware-serde": "^4.2.0", - "@smithy/protocol-http": "^5.3.0", - "@smithy/types": "^4.6.0", + "@smithy/middleware-serde": "^4.2.4", + "@smithy/protocol-http": "^5.3.4", + "@smithy/types": "^4.8.1", "@smithy/util-base64": "^4.3.0", "@smithy/util-body-length-browser": "^4.2.0", - "@smithy/util-middleware": "^4.2.0", - "@smithy/util-stream": "^4.5.0", + "@smithy/util-middleware": "^4.2.4", + "@smithy/util-stream": "^4.5.5", "@smithy/util-utf8": "^4.2.0", "@smithy/uuid": "^1.1.0", "tslib": "^2.6.2" @@ -7972,13 +8546,15 @@ } }, "node_modules/@smithy/credential-provider-imds": { - "version": "4.2.0", + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/@smithy/credential-provider-imds/-/credential-provider-imds-4.2.4.tgz", + "integrity": "sha512-YVNMjhdz2pVto5bRdux7GMs0x1m0Afz3OcQy/4Yf9DH4fWOtroGH7uLvs7ZmDyoBJzLdegtIPpXrpJOZWvUXdw==", "license": "Apache-2.0", "dependencies": { - "@smithy/node-config-provider": "^4.3.0", - "@smithy/property-provider": "^4.2.0", - "@smithy/types": "^4.6.0", - "@smithy/url-parser": "^4.2.0", + "@smithy/node-config-provider": "^4.3.4", + "@smithy/property-provider": "^4.2.4", + "@smithy/types": "^4.8.1", + "@smithy/url-parser": "^4.2.4", "tslib": "^2.6.2" }, "engines": { @@ -8046,12 +8622,14 @@ } }, "node_modules/@smithy/fetch-http-handler": { - "version": "5.3.1", + "version": "5.3.5", + "resolved": "https://registry.npmjs.org/@smithy/fetch-http-handler/-/fetch-http-handler-5.3.5.tgz", + "integrity": "sha512-mg83SM3FLI8Sa2ooTJbsh5MFfyMTyNRwxqpKHmE0ICRIa66Aodv80DMsTQI02xBLVJ0hckwqTRr5IGAbbWuFLQ==", "license": "Apache-2.0", "dependencies": { - "@smithy/protocol-http": "^5.3.0", - "@smithy/querystring-builder": "^4.2.0", - "@smithy/types": "^4.6.0", + "@smithy/protocol-http": "^5.3.4", + "@smithy/querystring-builder": "^4.2.4", + "@smithy/types": "^4.8.1", "@smithy/util-base64": "^4.3.0", "tslib": "^2.6.2" }, @@ -8073,10 +8651,12 @@ } }, "node_modules/@smithy/hash-node": { - "version": "4.2.0", + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/@smithy/hash-node/-/hash-node-4.2.4.tgz", + "integrity": "sha512-kKU0gVhx/ppVMntvUOZE7WRMFW86HuaxLwvqileBEjL7PoILI8/djoILw3gPQloGVE6O0oOzqafxeNi2KbnUJw==", "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^4.6.0", + "@smithy/types": "^4.8.1", "@smithy/util-buffer-from": "^4.2.0", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" @@ -8098,10 +8678,12 @@ } }, "node_modules/@smithy/invalid-dependency": { - "version": "4.2.0", + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/@smithy/invalid-dependency/-/invalid-dependency-4.2.4.tgz", + "integrity": "sha512-z6aDLGiHzsMhbS2MjetlIWopWz//K+mCoPXjW6aLr0mypF+Y7qdEh5TyJ20Onf9FbWHiWl4eC+rITdizpnXqOw==", "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^4.6.0", + "@smithy/types": "^4.8.1", "tslib": "^2.6.2" }, "engines": { @@ -8130,12 +8712,41 @@ "node": ">=18.0.0" } }, + "node_modules/@smithy/middleware-compression": { + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/@smithy/middleware-compression/-/middleware-compression-4.3.6.tgz", + "integrity": "sha512-RnQGHRaXGL7olwEeiAEmnB6/PyhXQzy+udmtXCW6c0UvUzmBf7Rs48TxsZ6ID2fI9z/LWt62ITJZcul+CSdGLA==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/core": "^3.17.2", + "@smithy/is-array-buffer": "^4.2.0", + "@smithy/node-config-provider": "^4.3.4", + "@smithy/protocol-http": "^5.3.4", + "@smithy/types": "^4.8.1", + "@smithy/util-config-provider": "^4.2.0", + "@smithy/util-middleware": "^4.2.4", + "@smithy/util-utf8": "^4.2.0", + "fflate": "0.8.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/middleware-compression/node_modules/fflate": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.8.1.tgz", + "integrity": "sha512-/exOvEuc+/iaUm105QIiOt4LpBdMTWsXxqR0HDF35vx3fmaKzw7354gTilCh5rkzEt8WYyG//ku3h3nRmd7CHQ==", + "license": "MIT" + }, "node_modules/@smithy/middleware-content-length": { - "version": "4.2.0", + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/@smithy/middleware-content-length/-/middleware-content-length-4.2.4.tgz", + "integrity": "sha512-hJRZuFS9UsElX4DJSJfoX4M1qXRH+VFiLMUnhsWvtOOUWRNvvOfDaUSdlNbjwv1IkpVjj/Rd/O59Jl3nhAcxow==", "license": "Apache-2.0", "dependencies": { - "@smithy/protocol-http": "^5.3.0", - "@smithy/types": "^4.6.0", + "@smithy/protocol-http": "^5.3.4", + "@smithy/types": "^4.8.1", "tslib": "^2.6.2" }, "engines": { @@ -8143,16 +8754,18 @@ } }, "node_modules/@smithy/middleware-endpoint": { - "version": "4.3.1", + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/@smithy/middleware-endpoint/-/middleware-endpoint-4.3.6.tgz", + "integrity": "sha512-PXehXofGMFpDqr933rxD8RGOcZ0QBAWtuzTgYRAHAL2BnKawHDEdf/TnGpcmfPJGwonhginaaeJIKluEojiF/w==", "license": "Apache-2.0", "dependencies": { - "@smithy/core": "^3.15.0", - "@smithy/middleware-serde": "^4.2.0", - "@smithy/node-config-provider": "^4.3.0", - "@smithy/shared-ini-file-loader": "^4.3.0", - "@smithy/types": "^4.6.0", - "@smithy/url-parser": "^4.2.0", - "@smithy/util-middleware": "^4.2.0", + "@smithy/core": "^3.17.2", + "@smithy/middleware-serde": "^4.2.4", + "@smithy/node-config-provider": "^4.3.4", + "@smithy/shared-ini-file-loader": "^4.3.4", + "@smithy/types": "^4.8.1", + "@smithy/url-parser": "^4.2.4", + "@smithy/util-middleware": "^4.2.4", "tslib": "^2.6.2" }, "engines": { @@ -8160,16 +8773,18 @@ } }, "node_modules/@smithy/middleware-retry": { - "version": "4.4.1", + "version": "4.4.6", + "resolved": "https://registry.npmjs.org/@smithy/middleware-retry/-/middleware-retry-4.4.6.tgz", + "integrity": "sha512-OhLx131znrEDxZPAvH/OYufR9d1nB2CQADyYFN4C3V/NQS7Mg4V6uvxHC/Dr96ZQW8IlHJTJ+vAhKt6oxWRndA==", "license": "Apache-2.0", "dependencies": { - "@smithy/node-config-provider": "^4.3.0", - "@smithy/protocol-http": "^5.3.0", - "@smithy/service-error-classification": "^4.2.0", - "@smithy/smithy-client": "^4.7.1", - "@smithy/types": "^4.6.0", - "@smithy/util-middleware": "^4.2.0", - "@smithy/util-retry": "^4.2.0", + "@smithy/node-config-provider": "^4.3.4", + "@smithy/protocol-http": "^5.3.4", + "@smithy/service-error-classification": "^4.2.4", + "@smithy/smithy-client": "^4.9.2", + "@smithy/types": "^4.8.1", + "@smithy/util-middleware": "^4.2.4", + "@smithy/util-retry": "^4.2.4", "@smithy/uuid": "^1.1.0", "tslib": "^2.6.2" }, @@ -8178,11 +8793,13 @@ } }, "node_modules/@smithy/middleware-serde": { - "version": "4.2.0", + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/@smithy/middleware-serde/-/middleware-serde-4.2.4.tgz", + "integrity": "sha512-jUr3x2CDhV15TOX2/Uoz4gfgeqLrRoTQbYAuhLS7lcVKNev7FeYSJ1ebEfjk+l9kbb7k7LfzIR/irgxys5ZTOg==", "license": "Apache-2.0", "dependencies": { - "@smithy/protocol-http": "^5.3.0", - "@smithy/types": "^4.6.0", + "@smithy/protocol-http": "^5.3.4", + "@smithy/types": "^4.8.1", "tslib": "^2.6.2" }, "engines": { @@ -8190,10 +8807,12 @@ } }, "node_modules/@smithy/middleware-stack": { - "version": "4.2.0", + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/@smithy/middleware-stack/-/middleware-stack-4.2.4.tgz", + "integrity": "sha512-Gy3TKCOnm9JwpFooldwAboazw+EFYlC+Bb+1QBsSi5xI0W5lX81j/P5+CXvD/9ZjtYKRgxq+kkqd/KOHflzvgA==", "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^4.6.0", + "@smithy/types": "^4.8.1", "tslib": "^2.6.2" }, "engines": { @@ -8201,12 +8820,14 @@ } }, "node_modules/@smithy/node-config-provider": { - "version": "4.3.0", + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/@smithy/node-config-provider/-/node-config-provider-4.3.4.tgz", + "integrity": "sha512-3X3w7qzmo4XNNdPKNS4nbJcGSwiEMsNsRSunMA92S4DJLLIrH5g1AyuOA2XKM9PAPi8mIWfqC+fnfKNsI4KvHw==", "license": "Apache-2.0", "dependencies": { - "@smithy/property-provider": "^4.2.0", - "@smithy/shared-ini-file-loader": "^4.3.0", - "@smithy/types": "^4.6.0", + "@smithy/property-provider": "^4.2.4", + "@smithy/shared-ini-file-loader": "^4.3.4", + "@smithy/types": "^4.8.1", "tslib": "^2.6.2" }, "engines": { @@ -8214,13 +8835,15 @@ } }, "node_modules/@smithy/node-http-handler": { - "version": "4.3.0", + "version": "4.4.4", + "resolved": "https://registry.npmjs.org/@smithy/node-http-handler/-/node-http-handler-4.4.4.tgz", + "integrity": "sha512-VXHGfzCXLZeKnFp6QXjAdy+U8JF9etfpUXD1FAbzY1GzsFJiDQRQIt2CnMUvUdz3/YaHNqT3RphVWMUpXTIODA==", "license": "Apache-2.0", "dependencies": { - "@smithy/abort-controller": "^4.2.0", - "@smithy/protocol-http": "^5.3.0", - "@smithy/querystring-builder": "^4.2.0", - "@smithy/types": "^4.6.0", + "@smithy/abort-controller": "^4.2.4", + "@smithy/protocol-http": "^5.3.4", + "@smithy/querystring-builder": "^4.2.4", + "@smithy/types": "^4.8.1", "tslib": "^2.6.2" }, "engines": { @@ -8228,10 +8851,12 @@ } }, "node_modules/@smithy/property-provider": { - "version": "4.2.0", + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/@smithy/property-provider/-/property-provider-4.2.4.tgz", + "integrity": "sha512-g2DHo08IhxV5GdY3Cpt/jr0mkTlAD39EJKN27Jb5N8Fb5qt8KG39wVKTXiTRCmHHou7lbXR8nKVU14/aRUf86w==", "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^4.6.0", + "@smithy/types": "^4.8.1", "tslib": "^2.6.2" }, "engines": { @@ -8239,10 +8864,12 @@ } }, "node_modules/@smithy/protocol-http": { - "version": "5.3.0", + "version": "5.3.4", + "resolved": "https://registry.npmjs.org/@smithy/protocol-http/-/protocol-http-5.3.4.tgz", + "integrity": "sha512-3sfFd2MAzVt0Q/klOmjFi3oIkxczHs0avbwrfn1aBqtc23WqQSmjvk77MBw9WkEQcwbOYIX5/2z4ULj8DuxSsw==", "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^4.6.0", + "@smithy/types": "^4.8.1", "tslib": "^2.6.2" }, "engines": { @@ -8250,10 +8877,12 @@ } }, "node_modules/@smithy/querystring-builder": { - "version": "4.2.0", + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/@smithy/querystring-builder/-/querystring-builder-4.2.4.tgz", + "integrity": "sha512-KQ1gFXXC+WsbPFnk7pzskzOpn4s+KheWgO3dzkIEmnb6NskAIGp/dGdbKisTPJdtov28qNDohQrgDUKzXZBLig==", "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^4.6.0", + "@smithy/types": "^4.8.1", "@smithy/util-uri-escape": "^4.2.0", "tslib": "^2.6.2" }, @@ -8262,10 +8891,12 @@ } }, "node_modules/@smithy/querystring-parser": { - "version": "4.2.0", + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/@smithy/querystring-parser/-/querystring-parser-4.2.4.tgz", + "integrity": "sha512-aHb5cqXZocdzEkZ/CvhVjdw5l4r1aU/9iMEyoKzH4eXMowT6M0YjBpp7W/+XjkBnY8Xh0kVd55GKjnPKlCwinQ==", "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^4.6.0", + "@smithy/types": "^4.8.1", "tslib": "^2.6.2" }, "engines": { @@ -8273,20 +8904,24 @@ } }, "node_modules/@smithy/service-error-classification": { - "version": "4.2.0", + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/@smithy/service-error-classification/-/service-error-classification-4.2.4.tgz", + "integrity": "sha512-fdWuhEx4+jHLGeew9/IvqVU/fxT/ot70tpRGuOLxE3HzZOyKeTQfYeV1oaBXpzi93WOk668hjMuuagJ2/Qs7ng==", "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^4.6.0" + "@smithy/types": "^4.8.1" }, "engines": { "node": ">=18.0.0" } }, "node_modules/@smithy/shared-ini-file-loader": { - "version": "4.3.0", + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/@smithy/shared-ini-file-loader/-/shared-ini-file-loader-4.3.4.tgz", + "integrity": "sha512-y5ozxeQ9omVjbnJo9dtTsdXj9BEvGx2X8xvRgKnV+/7wLBuYJQL6dOa/qMY6omyHi7yjt1OA97jZLoVRYi8lxA==", "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^4.6.0", + "@smithy/types": "^4.8.1", "tslib": "^2.6.2" }, "engines": { @@ -8294,14 +8929,16 @@ } }, "node_modules/@smithy/signature-v4": { - "version": "5.3.0", + "version": "5.3.4", + "resolved": "https://registry.npmjs.org/@smithy/signature-v4/-/signature-v4-5.3.4.tgz", + "integrity": "sha512-ScDCpasxH7w1HXHYbtk3jcivjvdA1VICyAdgvVqKhKKwxi+MTwZEqFw0minE+oZ7F07oF25xh4FGJxgqgShz0A==", "license": "Apache-2.0", "dependencies": { "@smithy/is-array-buffer": "^4.2.0", - "@smithy/protocol-http": "^5.3.0", - "@smithy/types": "^4.6.0", + "@smithy/protocol-http": "^5.3.4", + "@smithy/types": "^4.8.1", "@smithy/util-hex-encoding": "^4.2.0", - "@smithy/util-middleware": "^4.2.0", + "@smithy/util-middleware": "^4.2.4", "@smithy/util-uri-escape": "^4.2.0", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" @@ -8311,15 +8948,17 @@ } }, "node_modules/@smithy/smithy-client": { - "version": "4.7.1", + "version": "4.9.2", + "resolved": "https://registry.npmjs.org/@smithy/smithy-client/-/smithy-client-4.9.2.tgz", + "integrity": "sha512-gZU4uAFcdrSi3io8U99Qs/FvVdRxPvIMToi+MFfsy/DN9UqtknJ1ais+2M9yR8e0ASQpNmFYEKeIKVcMjQg3rg==", "license": "Apache-2.0", "dependencies": { - "@smithy/core": "^3.15.0", - "@smithy/middleware-endpoint": "^4.3.1", - "@smithy/middleware-stack": "^4.2.0", - "@smithy/protocol-http": "^5.3.0", - "@smithy/types": "^4.6.0", - "@smithy/util-stream": "^4.5.0", + "@smithy/core": "^3.17.2", + "@smithy/middleware-endpoint": "^4.3.6", + "@smithy/middleware-stack": "^4.2.4", + "@smithy/protocol-http": "^5.3.4", + "@smithy/types": "^4.8.1", + "@smithy/util-stream": "^4.5.5", "tslib": "^2.6.2" }, "engines": { @@ -8327,7 +8966,9 @@ } }, "node_modules/@smithy/types": { - "version": "4.6.0", + "version": "4.8.1", + "resolved": "https://registry.npmjs.org/@smithy/types/-/types-4.8.1.tgz", + "integrity": "sha512-N0Zn0OT1zc+NA+UVfkYqQzviRh5ucWwO7mBV3TmHHprMnfcJNfhlPicDkBHi0ewbh+y3evR6cNAW0Raxvb01NA==", "license": "Apache-2.0", "dependencies": { "tslib": "^2.6.2" @@ -8337,11 +8978,13 @@ } }, "node_modules/@smithy/url-parser": { - "version": "4.2.0", + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/@smithy/url-parser/-/url-parser-4.2.4.tgz", + "integrity": "sha512-w/N/Iw0/PTwJ36PDqU9PzAwVElo4qXxCC0eCTlUtIz/Z5V/2j/cViMHi0hPukSBHp4DVwvUlUhLgCzqSJ6plrg==", "license": "Apache-2.0", "dependencies": { - "@smithy/querystring-parser": "^4.2.0", - "@smithy/types": "^4.6.0", + "@smithy/querystring-parser": "^4.2.4", + "@smithy/types": "^4.8.1", "tslib": "^2.6.2" }, "engines": { @@ -8402,12 +9045,14 @@ } }, "node_modules/@smithy/util-defaults-mode-browser": { - "version": "4.3.0", + "version": "4.3.5", + "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-browser/-/util-defaults-mode-browser-4.3.5.tgz", + "integrity": "sha512-GwaGjv/QLuL/QHQaqhf/maM7+MnRFQQs7Bsl6FlaeK6lm6U7mV5AAnVabw68cIoMl5FQFyKK62u7RWRzWL25OQ==", "license": "Apache-2.0", "dependencies": { - "@smithy/property-provider": "^4.2.0", - "@smithy/smithy-client": "^4.7.1", - "@smithy/types": "^4.6.0", + "@smithy/property-provider": "^4.2.4", + "@smithy/smithy-client": "^4.9.2", + "@smithy/types": "^4.8.1", "tslib": "^2.6.2" }, "engines": { @@ -8415,15 +9060,17 @@ } }, "node_modules/@smithy/util-defaults-mode-node": { - "version": "4.2.1", + "version": "4.2.7", + "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-node/-/util-defaults-mode-node-4.2.7.tgz", + "integrity": "sha512-6hinjVqec0WYGsqN7h9hL/ywfULmJJNXGXnNZW7jrIn/cFuC/aVlVaiDfBIJEvKcOrmN8/EgsW69eY0gXABeHw==", "license": "Apache-2.0", "dependencies": { - "@smithy/config-resolver": "^4.3.0", - "@smithy/credential-provider-imds": "^4.2.0", - "@smithy/node-config-provider": "^4.3.0", - "@smithy/property-provider": "^4.2.0", - "@smithy/smithy-client": "^4.7.1", - "@smithy/types": "^4.6.0", + "@smithy/config-resolver": "^4.4.1", + "@smithy/credential-provider-imds": "^4.2.4", + "@smithy/node-config-provider": "^4.3.4", + "@smithy/property-provider": "^4.2.4", + "@smithy/smithy-client": "^4.9.2", + "@smithy/types": "^4.8.1", "tslib": "^2.6.2" }, "engines": { @@ -8431,11 +9078,13 @@ } }, "node_modules/@smithy/util-endpoints": { - "version": "3.2.0", + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@smithy/util-endpoints/-/util-endpoints-3.2.4.tgz", + "integrity": "sha512-f+nBDhgYRCmUEDKEQb6q0aCcOTXRDqH5wWaFHJxt4anB4pKHlgGoYP3xtioKXH64e37ANUkzWf6p4Mnv1M5/Vg==", "license": "Apache-2.0", "dependencies": { - "@smithy/node-config-provider": "^4.3.0", - "@smithy/types": "^4.6.0", + "@smithy/node-config-provider": "^4.3.4", + "@smithy/types": "^4.8.1", "tslib": "^2.6.2" }, "engines": { @@ -8453,10 +9102,12 @@ } }, "node_modules/@smithy/util-middleware": { - "version": "4.2.0", + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/@smithy/util-middleware/-/util-middleware-4.2.4.tgz", + "integrity": "sha512-fKGQAPAn8sgV0plRikRVo6g6aR0KyKvgzNrPuM74RZKy/wWVzx3BMk+ZWEueyN3L5v5EDg+P582mKU+sH5OAsg==", "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^4.6.0", + "@smithy/types": "^4.8.1", "tslib": "^2.6.2" }, "engines": { @@ -8464,11 +9115,13 @@ } }, "node_modules/@smithy/util-retry": { - "version": "4.2.0", + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/@smithy/util-retry/-/util-retry-4.2.4.tgz", + "integrity": "sha512-yQncJmj4dtv/isTXxRb4AamZHy4QFr4ew8GxS6XLWt7sCIxkPxPzINWd7WLISEFPsIan14zrKgvyAF+/yzfwoA==", "license": "Apache-2.0", "dependencies": { - "@smithy/service-error-classification": "^4.2.0", - "@smithy/types": "^4.6.0", + "@smithy/service-error-classification": "^4.2.4", + "@smithy/types": "^4.8.1", "tslib": "^2.6.2" }, "engines": { @@ -8476,12 +9129,14 @@ } }, "node_modules/@smithy/util-stream": { - "version": "4.5.0", + "version": "4.5.5", + "resolved": "https://registry.npmjs.org/@smithy/util-stream/-/util-stream-4.5.5.tgz", + "integrity": "sha512-7M5aVFjT+HPilPOKbOmQfCIPchZe4DSBc1wf1+NvHvSoFTiFtauZzT+onZvCj70xhXd0AEmYnZYmdJIuwxOo4w==", "license": "Apache-2.0", "dependencies": { - "@smithy/fetch-http-handler": "^5.3.1", - "@smithy/node-http-handler": "^4.3.0", - "@smithy/types": "^4.6.0", + "@smithy/fetch-http-handler": "^5.3.5", + "@smithy/node-http-handler": "^4.4.4", + "@smithy/types": "^4.8.1", "@smithy/util-base64": "^4.3.0", "@smithy/util-buffer-from": "^4.2.0", "@smithy/util-hex-encoding": "^4.2.0", @@ -8514,11 +9169,13 @@ } }, "node_modules/@smithy/util-waiter": { - "version": "4.1.1", + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/@smithy/util-waiter/-/util-waiter-4.2.4.tgz", + "integrity": "sha512-roKXtXIC6fopFvVOju8VYHtguc/jAcMlK8IlDOHsrQn0ayMkHynjm/D2DCMRf7MJFXzjHhlzg2edr3QPEakchQ==", "license": "Apache-2.0", "dependencies": { - "@smithy/abort-controller": "^4.1.1", - "@smithy/types": "^4.5.0", + "@smithy/abort-controller": "^4.2.4", + "@smithy/types": "^4.8.1", "tslib": "^2.6.2" }, "engines": { From 04da14d23d84f4f794ea73ef970479a4fff60d4c Mon Sep 17 00:00:00 2001 From: Steve Buxton Date: Tue, 4 Nov 2025 11:35:41 +0000 Subject: [PATCH 08/14] Added supplier ID lookup --- .../api/module_authorizer_lambda.tf | 6 +- .../components/api/resources/spec.tmpl.json | 4 +- lambdas/api-handler/src/config/deps.ts | 8 +- .../authorizer/src/__tests__/index.test.ts | 187 +++++++++--------- lambdas/authorizer/src/authorizer.ts | 49 ++--- lambdas/authorizer/src/deps.ts | 18 ++ lambdas/authorizer/src/env.ts | 2 + 7 files changed, 144 insertions(+), 130 deletions(-) diff --git a/infrastructure/terraform/components/api/module_authorizer_lambda.tf b/infrastructure/terraform/components/api/module_authorizer_lambda.tf index 76300262..8ef4218e 100644 --- a/infrastructure/terraform/components/api/module_authorizer_lambda.tf +++ b/infrastructure/terraform/components/api/module_authorizer_lambda.tf @@ -32,8 +32,10 @@ module "authorizer_lambda" { log_subscription_role_arn = local.acct.log_subscription_role_arn lambda_env_vars = { - CLOUDWATCH_NAMESPACE = "/aws/api-gateway/supplier/alarms" - CLIENT_CERTIFICATE_EXPIRATION_ALERT_DAYS = 14 + CLOUDWATCH_NAMESPACE = "/aws/api-gateway/supplier/alarms", + CLIENT_CERTIFICATE_EXPIRATION_ALERT_DAYS = 14, + APIM_APPLICATION_ID_HEADER = "apim-application-id", + SUPPLIERS_TABLE_NAME = aws_dynamodb_table.suppliers.name } } diff --git a/infrastructure/terraform/components/api/resources/spec.tmpl.json b/infrastructure/terraform/components/api/resources/spec.tmpl.json index 4514619b..713d49a3 100644 --- a/infrastructure/terraform/components/api/resources/spec.tmpl.json +++ b/infrastructure/terraform/components/api/resources/spec.tmpl.json @@ -3,13 +3,13 @@ "securitySchemes": { "LambdaAuthorizer": { "in": "header", - "name": "NHSD-Supplier-ID", + "name": "apim-application-id", "type": "apiKey", "x-amazon-apigateway-authorizer": { "authorizerCredentials": "${APIG_EXECUTION_ROLE_ARN}", "authorizerResultTtlInSeconds": 0, "authorizerUri": "arn:aws:apigateway:${AWS_REGION}:lambda:path/2015-03-31/functions/${AUTHORIZER_LAMBDA_ARN}/invocations", - "identitySource": "method.request.header.NHSD-Supplier-ID", + "identitySource": "method.request.header.apim-application-id", "type": "request" }, "x-amazon-apigateway-authtype": "custom" diff --git a/lambdas/api-handler/src/config/deps.ts b/lambdas/api-handler/src/config/deps.ts index 1942bc6c..e704150f 100644 --- a/lambdas/api-handler/src/config/deps.ts +++ b/lambdas/api-handler/src/config/deps.ts @@ -19,25 +19,21 @@ function createDocumentClient(): DynamoDBDocumentClient { } function createLetterRepository(documentClient: DynamoDBDocumentClient, log: pino.Logger, envVars: EnvVars): LetterRepository { - const ddbClient = new DynamoDBClient({}); - const docClient = DynamoDBDocumentClient.from(ddbClient); const config = { lettersTableName: envVars.LETTERS_TABLE_NAME, lettersTtlHours: envVars.LETTER_TTL_HOURS }; - return new LetterRepository(docClient, log, config); + return new LetterRepository(documentClient, log, config); } function createMIRepository(documentClient: DynamoDBDocumentClient, log: pino.Logger, envVars: EnvVars): MIRepository { - const ddbClient = new DynamoDBClient({}); - const docClient = DynamoDBDocumentClient.from(ddbClient); const config = { miTableName: envVars.MI_TABLE_NAME, miTtlHours: envVars.MI_TTL_HOURS }; - return new MIRepository(docClient, log, config); + return new MIRepository(documentClient, log, config); } export function createDependenciesContainer(): Deps { diff --git a/lambdas/authorizer/src/__tests__/index.test.ts b/lambdas/authorizer/src/__tests__/index.test.ts index d8599f93..fe847aff 100644 --- a/lambdas/authorizer/src/__tests__/index.test.ts +++ b/lambdas/authorizer/src/__tests__/index.test.ts @@ -8,11 +8,15 @@ const mockedDeps: jest.Mocked = { logger: { info: jest.fn(), error: jest.fn() } as unknown as pino.Logger, env: { CLOUDWATCH_NAMESPACE: 'cloudwatch-namespace', - CLIENT_CERTIFICATE_EXPIRATION_ALERT_DAYS: 14 + CLIENT_CERTIFICATE_EXPIRATION_ALERT_DAYS: 14, + APIM_APPLICATION_ID_HEADER: 'apim-application-id' } as unknown as EnvVars, cloudWatchClient: { send: jest.fn().mockResolvedValue({}), } as any, + supplierRepo: { + getSupplierByApimId: jest.fn(), + } as any, } as Deps; @@ -34,100 +38,6 @@ describe('Authorizer Lambda Function', () => { mockCallback = jest.fn(); }); - it('Should allow access when headers match', async() => { - mockEvent.headers = { headerauth1: 'headervalue1' }; - - const handler = createAuthorizerHandler(mockedDeps); - handler(mockEvent, mockContext, mockCallback); - await new Promise(process.nextTick); - - expect(mockCallback).toHaveBeenCalledWith(null, expect.objectContaining({ - policyDocument: expect.objectContaining({ - Statement: expect.arrayContaining([ - expect.objectContaining({ - Effect: 'Allow', - }), - ]), - }), - })); - }); - - it('Should deny access when headers do not match', async() => { - mockEvent.headers = { headerauth1: 'wrongValue' }; - - const handler = createAuthorizerHandler(mockedDeps); - handler(mockEvent, mockContext, mockCallback); - await new Promise(process.nextTick); - - expect(mockCallback).toHaveBeenCalledWith(null, expect.objectContaining({ - policyDocument: expect.objectContaining({ - Statement: expect.arrayContaining([ - expect.objectContaining({ - Effect: 'Deny', - }), - ]), - }), - })); - }); - - it('Should handle null headers gracefully', async() => { - mockEvent.headers = null; - - const handler = createAuthorizerHandler(mockedDeps); - handler(mockEvent, mockContext, mockCallback); - await new Promise(process.nextTick); - - expect(mockCallback).toHaveBeenCalledWith(null, expect.objectContaining({ - policyDocument: expect.objectContaining({ - Statement: expect.arrayContaining([ - expect.objectContaining({ - Effect: 'Deny', - }), - ]), - }), - })); - }); - - it('Should handle defined headers correctly', async() => { - mockEvent.headers = { headerauth1: 'headervalue1' }; - - const handler = createAuthorizerHandler(mockedDeps); - handler(mockEvent, mockContext, mockCallback); - await new Promise(process.nextTick); - - expect(mockCallback).toHaveBeenCalledWith(null, expect.objectContaining({ - policyDocument: expect.objectContaining({ - Statement: expect.arrayContaining([ - expect.objectContaining({ - Effect: 'Allow', - }), - ]), - }), - })); - }); - - it('Should handle additional headers correctly', async() => { - mockEvent.headers = { - headerauth1: 'headervalue1' , - otherheader1: 'headervalue2', - otherheader2: 'headervalue3' - }; - - const handler = createAuthorizerHandler(mockedDeps); - handler(mockEvent, mockContext, mockCallback); - await new Promise(process.nextTick); - - expect(mockCallback).toHaveBeenCalledWith(null, expect.objectContaining({ - policyDocument: expect.objectContaining({ - Statement: expect.arrayContaining([ - expect.objectContaining({ - Effect: 'Allow', - }), - ]), - }), - })); - }); - describe('Certificate expiry check', () => { beforeEach(() => { @@ -140,7 +50,6 @@ describe('Authorizer Lambda Function', () => { }) it('Should not send CloudWatch metric when certificate is null', async () => { - mockEvent.headers = { headerauth1: 'headervalue1' }; mockEvent.requestContext.identity.clientCert = null; const handler = createAuthorizerHandler(mockedDeps); @@ -151,7 +60,6 @@ describe('Authorizer Lambda Function', () => { }); it('Should send CloudWatch metric when the certificate expiry threshold is reached', async () => { - mockEvent.headers = { headerauth1: 'headervalue1' }; mockEvent.requestContext.identity.clientCert = buildCertWithExpiry('2025-11-17T14:19:00Z'); const handler = createAuthorizerHandler(mockedDeps); @@ -177,7 +85,6 @@ describe('Authorizer Lambda Function', () => { }); it('Should not send CloudWatch metric when the certificate expiry threshold is not yet reached', async () => { - mockEvent.headers = { headerauth1: 'headervalue1' }; mockEvent.requestContext.identity.clientCert = buildCertWithExpiry('2025-11-18T14:19:00Z'); const handler = createAuthorizerHandler(mockedDeps); @@ -197,4 +104,88 @@ describe('Authorizer Lambda Function', () => { } as APIGatewayEventClientCertificate['validity'], } as APIGatewayEventClientCertificate; } + + describe('Supplier ID lookup', () => { + + it('Should deny the request when no headers are present', async () => { + mockEvent.headers = null; + + const handler = createAuthorizerHandler(mockedDeps); + handler(mockEvent, mockContext, mockCallback); + await new Promise(process.nextTick); + + expect(mockCallback).toHaveBeenCalledWith(null, expect.objectContaining({ + policyDocument: expect.objectContaining({ + Statement: [ + expect.objectContaining({ + Effect: 'Deny', + }), + ], + }), + })); + }); + + it('Should deny the request when the APIM application ID header is absent', async () => { + mockEvent.headers = {'x-apim-correlation-id': 'correlation-id'}; + + const handler = createAuthorizerHandler(mockedDeps); + handler(mockEvent, mockContext, mockCallback); + await new Promise(process.nextTick); + + expect(mockCallback).toHaveBeenCalledWith(null, expect.objectContaining({ + policyDocument: expect.objectContaining({ + Statement: [ + expect.objectContaining({ + Effect: 'Deny', + }), + ], + }), + })); + }); + + it('Should deny the request when no supplier ID is found', async () => { + mockEvent.headers = { 'apim-application-id': 'unknown-apim-id' }; + (mockedDeps.supplierRepo.getSupplierByApimId as jest.Mock).mockRejectedValue(new Error('Supplier not found')); + + const handler = createAuthorizerHandler(mockedDeps); + handler(mockEvent, mockContext, mockCallback); + await new Promise(process.nextTick); + + expect(mockCallback).toHaveBeenCalledWith(null, expect.objectContaining({ + policyDocument: expect.objectContaining({ + Statement: [ + expect.objectContaining({ + Effect: 'Deny', + }), + ], + }), + })); + }); + + it('Should allow the request when the supplier ID is found', async () => { + mockEvent.headers = { 'apim-application-id': 'valid-apim-id' }; + (mockedDeps.supplierRepo.getSupplierByApimId as jest.Mock).mockResolvedValue({ + id: 'supplier-123', + apimApplicationId: 'valid-apim-id', + name: 'Test Supplier', + }); + + const handler = createAuthorizerHandler(mockedDeps); + handler(mockEvent, mockContext, mockCallback); + await new Promise(process.nextTick); + + expect(mockCallback).toHaveBeenCalledWith(null, expect.objectContaining({ + policyDocument: expect.objectContaining({ + Statement: [ + expect.objectContaining({ + Effect: 'Allow', + }), + ], + }), + context: { + supplierId: 'supplier-123', + }, + })); + }); + }); }); diff --git a/lambdas/authorizer/src/authorizer.ts b/lambdas/authorizer/src/authorizer.ts index ff8469a1..897fbb69 100644 --- a/lambdas/authorizer/src/authorizer.ts +++ b/lambdas/authorizer/src/authorizer.ts @@ -11,10 +11,11 @@ // See https://docs.aws.amazon.com/apigateway/latest/developerguide/apigateway-use-lambda-authorizer.html for the original JS documentation -import {APIGatewayAuthorizerResult, APIGatewayEventClientCertificate, APIGatewayRequestAuthorizerEvent, APIGatewayRequestAuthorizerHandler, +import {APIGatewayAuthorizerResult, APIGatewayAuthorizerResultContext, APIGatewayEventClientCertificate, APIGatewayRequestAuthorizerEvent, APIGatewayRequestAuthorizerEventHeaders, APIGatewayRequestAuthorizerHandler, Callback, Context } from 'aws-lambda'; import { Deps } from './deps'; import { PutMetricDataCommand } from '@aws-sdk/client-cloudwatch'; +import { Supplier } from '@internal/datastore'; export function createAuthorizerHandler(deps: Deps): APIGatewayRequestAuthorizerHandler { @@ -25,30 +26,38 @@ export function createAuthorizerHandler(deps: Deps): APIGatewayRequestAuthorizer ): void => { deps.logger.info(event, 'Received event'); - const headers = event.headers || {}; - checkCertificateExpiry(event.requestContext.identity.clientCert, deps).then(() => { - // Perform authorization to return the Allow policy for correct parameters and - // the 'Unauthorized' error, otherwise. - if ( - headers['headerauth1'] === 'headervalue1' - ) { + checkCertificateExpiry(event.requestContext.identity.clientCert, deps) + .then(() => deps.supplierRepo.getSupplierByApimId(extractApimId(event.headers, deps))) + .then((supplier: Supplier) => { deps.logger.info('Allow event'); - callback(null, generateAllow('me', event.methodArn)); - } else { - deps.logger.info('Deny event'); + callback(null, generateAllow('me', event.methodArn, supplier.id)); + }) + .catch((error) => { + deps.logger.info('Deny event', {error}); callback(null, generateDeny('me', event.methodArn)); - } - }); + }); }; } +function extractApimId(headers: APIGatewayRequestAuthorizerEventHeaders | null, deps: Deps): string { + const apimId = Object.entries(headers || {}) + .find(([headerName, _]) => headerName.toLowerCase() === deps.env.APIM_APPLICATION_ID_HEADER)?.[1]; + + if(!apimId) { + throw new Error("No APIM application ID found in header"); + } + return apimId; +} + + // Helper function to generate an IAM policy function generatePolicy( principalId: string, effect: 'Allow' | 'Deny', - resource: string + resource: string, + context: APIGatewayAuthorizerResultContext ): APIGatewayAuthorizerResult { // Required output: const authResponse: APIGatewayAuthorizerResult = { @@ -63,21 +72,17 @@ export function createAuthorizerHandler(deps: Deps): APIGatewayRequestAuthorizer }, ], }, - context: { - stringKey: 'stringval', - numberKey: 123, - booleanKey: true, - }, + context: context, }; return authResponse; } -function generateAllow(principalId: string, resource: string): APIGatewayAuthorizerResult { - return generatePolicy(principalId, 'Allow', resource); +function generateAllow(principalId: string, resource: string, supplierId: string): APIGatewayAuthorizerResult { + return generatePolicy(principalId, 'Allow', resource, {supplierId: supplierId}); } function generateDeny(principalId: string, resource: string): APIGatewayAuthorizerResult { - return generatePolicy(principalId, 'Deny', resource); + return generatePolicy(principalId, 'Deny', resource, {}); } function getCertificateExpiryInDays(certificate: APIGatewayEventClientCertificate): number { diff --git a/lambdas/authorizer/src/deps.ts b/lambdas/authorizer/src/deps.ts index 02889da1..226b0530 100644 --- a/lambdas/authorizer/src/deps.ts +++ b/lambdas/authorizer/src/deps.ts @@ -1,17 +1,35 @@ import { CloudWatchClient } from "@aws-sdk/client-cloudwatch"; +import { DynamoDBClient } from "@aws-sdk/client-dynamodb"; +import { DynamoDBDocumentClient } from "@aws-sdk/lib-dynamodb"; import pino from 'pino'; import { envVars, EnvVars } from "./env"; +import { SupplierRepository } from '@internal/datastore'; export type Deps = { + supplierRepo: SupplierRepository; cloudWatchClient: CloudWatchClient; logger: pino.Logger; env: EnvVars; }; +function createDocumentClient(): DynamoDBDocumentClient { + const ddbClient = new DynamoDBClient({}); + return DynamoDBDocumentClient.from(ddbClient); +} + +function createSupplierRepository(documentClient: DynamoDBDocumentClient, log: pino.Logger, envVars: EnvVars): SupplierRepository { + const config = { + suppliersTableName: envVars.SUPPLIERS_TABLE_NAME + }; + + return new SupplierRepository(documentClient, log, config); +} + export function createDependenciesContainer(): Deps { const log = pino(); return { + supplierRepo: createSupplierRepository(createDocumentClient(), log, envVars), cloudWatchClient: new CloudWatchClient({}), logger: log, env: envVars diff --git a/lambdas/authorizer/src/env.ts b/lambdas/authorizer/src/env.ts index 95fc5c29..5e181324 100644 --- a/lambdas/authorizer/src/env.ts +++ b/lambdas/authorizer/src/env.ts @@ -1,7 +1,9 @@ import {z} from 'zod'; const EnvVarsSchema = z.object({ + SUPPLIERS_TABLE_NAME: z.string(), CLOUDWATCH_NAMESPACE: z.string(), + APIM_APPLICATION_ID_HEADER: z.string(), CLIENT_CERTIFICATE_EXPIRATION_ALERT_DAYS: z.coerce.number().int() }); From d1847916bb38794de211071ff654f1eedd8ad53b Mon Sep 17 00:00:00 2001 From: Steve Buxton Date: Tue, 4 Nov 2025 15:28:20 +0000 Subject: [PATCH 09/14] Test fix --- lambdas/authorizer/src/authorizer.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lambdas/authorizer/src/authorizer.ts b/lambdas/authorizer/src/authorizer.ts index 897fbb69..9d13d524 100644 --- a/lambdas/authorizer/src/authorizer.ts +++ b/lambdas/authorizer/src/authorizer.ts @@ -34,7 +34,7 @@ export function createAuthorizerHandler(deps: Deps): APIGatewayRequestAuthorizer callback(null, generateAllow('me', event.methodArn, supplier.id)); }) .catch((error) => { - deps.logger.info('Deny event', {error}); + deps.logger.info({error}, 'Deny event'); callback(null, generateDeny('me', event.methodArn)); }); }; @@ -43,7 +43,7 @@ export function createAuthorizerHandler(deps: Deps): APIGatewayRequestAuthorizer function extractApimId(headers: APIGatewayRequestAuthorizerEventHeaders | null, deps: Deps): string { const apimId = Object.entries(headers || {}) - .find(([headerName, _]) => headerName.toLowerCase() === deps.env.APIM_APPLICATION_ID_HEADER)?.[1]; + .find(([headerName, _]) => headerName.toLowerCase() === deps.env.APIM_APPLICATION_ID_HEADER)?.[1] as string; if(!apimId) { throw new Error("No APIM application ID found in header"); From 0121de0455c48dd0654dae16a3fc72c9ecb966a5 Mon Sep 17 00:00:00 2001 From: Steve Buxton Date: Wed, 5 Nov 2025 10:38:19 +0000 Subject: [PATCH 10/14] Further development --- .../api/module_authorizer_lambda.tf | 18 ++++ .../handlers/__tests__/utils/test-utils.ts | 2 +- .../src/handlers/get-letter-data.ts | 13 +-- .../api-handler/src/handlers/get-letter.ts | 15 +-- .../api-handler/src/handlers/get-letters.ts | 14 +-- .../api-handler/src/handlers/patch-letter.ts | 13 +-- lambdas/api-handler/src/handlers/post-mi.ts | 13 +-- .../src/utils/__tests__/commonIds.test.ts | 98 +++++++++++++++++++ lambdas/api-handler/src/utils/commonIds.ts | 45 +++++++++ lambdas/api-handler/src/utils/validation.ts | 37 ------- .../authorizer/src/__tests__/index.test.ts | 29 +++++- lambdas/authorizer/src/authorizer.ts | 33 ++++--- 12 files changed, 241 insertions(+), 89 deletions(-) create mode 100644 lambdas/api-handler/src/utils/__tests__/commonIds.test.ts create mode 100644 lambdas/api-handler/src/utils/commonIds.ts diff --git a/infrastructure/terraform/components/api/module_authorizer_lambda.tf b/infrastructure/terraform/components/api/module_authorizer_lambda.tf index 8ef4218e..6ff2c2c4 100644 --- a/infrastructure/terraform/components/api/module_authorizer_lambda.tf +++ b/infrastructure/terraform/components/api/module_authorizer_lambda.tf @@ -11,6 +11,10 @@ module "authorizer_lambda" { log_retention_in_days = var.log_retention_in_days kms_key_arn = module.kms.key_arn + iam_policy_document = { + body = data.aws_iam_policy_document.authorizer_lambda.json + } + function_name = "authorizer" description = "Authorizer for Suppliers API" @@ -52,4 +56,18 @@ data "aws_iam_policy_document" "authorizer_lambda" { "*" ] } + + statement { + sid = "AllowDynamoDBAccess" + effect = "Allow" + + actions = [ + "dynamodb:Query" + ] + + resources = [ + aws_dynamodb_table.suppliers.arn, + "${aws_dynamodb_table.suppliers.arn}/index/supplier-apim-index" + ] + } } diff --git a/lambdas/api-handler/src/handlers/__tests__/utils/test-utils.ts b/lambdas/api-handler/src/handlers/__tests__/utils/test-utils.ts index 4a3a9171..dd348694 100644 --- a/lambdas/api-handler/src/handlers/__tests__/utils/test-utils.ts +++ b/lambdas/api-handler/src/handlers/__tests__/utils/test-utils.ts @@ -18,7 +18,7 @@ export function makeApiGwEvent( requestContext: { accountId: '123456789012', apiId: 'api-id', - authorizer: {}, + authorizer: null, protocol: 'HTTP/1.1', httpMethod: 'GET', identity: {} as any, diff --git a/lambdas/api-handler/src/handlers/get-letter-data.ts b/lambdas/api-handler/src/handlers/get-letter-data.ts index 547c8e17..62b1b39f 100644 --- a/lambdas/api-handler/src/handlers/get-letter-data.ts +++ b/lambdas/api-handler/src/handlers/get-letter-data.ts @@ -1,5 +1,6 @@ import { APIGatewayProxyHandler } from "aws-lambda"; -import { assertNotEmpty, validateCommonHeaders } from "../utils/validation"; +import { assertNotEmpty } from "../utils/validation"; +import { extractCommonIds } from '../utils/commonIds'; import { ApiErrorDetail } from '../contracts/errors'; import { mapErrorToResponse } from "../mappers/error-mapper"; import { ValidationError } from "../errors"; @@ -11,10 +12,10 @@ export function createGetLetterDataHandler(deps: Deps): APIGatewayProxyHandler { return async (event) => { - const commonHeadersResult = validateCommonHeaders(event.headers, deps); + const commonIds = extractCommonIds(event.headers, event.requestContext, deps); - if (!commonHeadersResult.ok) { - return mapErrorToResponse(commonHeadersResult.error, commonHeadersResult.correlationId, deps.logger); + if (!commonIds.ok) { + return mapErrorToResponse(commonIds.error, commonIds.correlationId, deps.logger); } try { @@ -24,13 +25,13 @@ export function createGetLetterDataHandler(deps: Deps): APIGatewayProxyHandler { return { statusCode: 303, headers: { - 'Location': await getLetterDataUrl(commonHeadersResult.value.supplierId, letterId, deps) + 'Location': await getLetterDataUrl(commonIds.value.supplierId, letterId, deps) }, body: '' }; } catch (error) { - return mapErrorToResponse(error, commonHeadersResult.value.correlationId, deps.logger); + return mapErrorToResponse(error, commonIds.value.correlationId, deps.logger); } } }; diff --git a/lambdas/api-handler/src/handlers/get-letter.ts b/lambdas/api-handler/src/handlers/get-letter.ts index 5c428dce..15e5a884 100644 --- a/lambdas/api-handler/src/handlers/get-letter.ts +++ b/lambdas/api-handler/src/handlers/get-letter.ts @@ -1,5 +1,6 @@ import { APIGatewayProxyHandler } from "aws-lambda"; -import { assertNotEmpty, validateCommonHeaders } from "../utils/validation"; +import { assertNotEmpty } from "../utils/validation"; +import { extractCommonIds } from '../utils/commonIds'; import { ValidationError } from "../errors"; import { ApiErrorDetail } from "../contracts/errors"; import { getLetterById } from "../services/letter-operations"; @@ -12,22 +13,22 @@ export function createGetLetterHandler(deps: Deps): APIGatewayProxyHandler { return async (event) => { - const commonHeadersResult = validateCommonHeaders(event.headers, deps); + const commonIds = extractCommonIds(event.headers, event.requestContext, deps); - if (!commonHeadersResult.ok) { - return mapErrorToResponse(commonHeadersResult.error, commonHeadersResult.correlationId, deps.logger); + if (!commonIds.ok) { + return mapErrorToResponse(commonIds.error, commonIds.correlationId, deps.logger); } try { const letterId = assertNotEmpty(event.pathParameters?.id, new ValidationError(ApiErrorDetail.InvalidRequestMissingLetterIdPathParameter)); - const letter = await getLetterById(commonHeadersResult.value.supplierId, letterId, deps.letterRepo); + const letter = await getLetterById(commonIds.value.supplierId, letterId, deps.letterRepo); const response = mapToGetLetterResponse(letter); deps.logger.info({ description: 'Letter successfully fetched by id', - supplierId: commonHeadersResult.value.supplierId, + supplierId: commonIds.value.supplierId, letterId }); @@ -37,7 +38,7 @@ export function createGetLetterHandler(deps: Deps): APIGatewayProxyHandler { }; } catch (error) { - return mapErrorToResponse(error, commonHeadersResult.value.correlationId, deps.logger); + return mapErrorToResponse(error, commonIds.value.correlationId, deps.logger); } } } diff --git a/lambdas/api-handler/src/handlers/get-letters.ts b/lambdas/api-handler/src/handlers/get-letters.ts index 0d42ea20..25c1e3ce 100644 --- a/lambdas/api-handler/src/handlers/get-letters.ts +++ b/lambdas/api-handler/src/handlers/get-letters.ts @@ -1,6 +1,6 @@ import { APIGatewayProxyEventQueryStringParameters, APIGatewayProxyHandler } from 'aws-lambda'; import { getLettersForSupplier } from '../services/letter-operations'; -import { validateCommonHeaders } from '../utils/validation'; +import { extractCommonIds } from '../utils/commonIds'; import { ApiErrorDetail } from '../contracts/errors'; import { mapErrorToResponse } from '../mappers/error-mapper'; import { ValidationError } from '../errors'; @@ -16,10 +16,10 @@ export function createGetLettersHandler(deps: Deps): APIGatewayProxyHandler { return async (event) => { - const commonHeadersResult = validateCommonHeaders(event.headers, deps); + const commonIds = extractCommonIds(event.headers, event.requestContext, deps); - if (!commonHeadersResult.ok) { - return mapErrorToResponse(commonHeadersResult.error, commonHeadersResult.correlationId, deps.logger); + if (!commonIds.ok) { + return mapErrorToResponse(commonIds.error, commonIds.correlationId, deps.logger); } try { @@ -28,7 +28,7 @@ export function createGetLettersHandler(deps: Deps): APIGatewayProxyHandler { const limitNumber = getLimitOrDefault(event.queryStringParameters, maxLimit, deps.logger); const letters = await getLettersForSupplier( - commonHeadersResult.value.supplierId, + commonIds.value.supplierId, status, limitNumber, deps.letterRepo, @@ -38,7 +38,7 @@ export function createGetLettersHandler(deps: Deps): APIGatewayProxyHandler { deps.logger.info({ description: 'Pending letters successfully fetched', - supplierId: commonHeadersResult.value.supplierId, + supplierId: commonIds.value.supplierId, limitNumber, status, lettersCount: letters.length @@ -50,7 +50,7 @@ export function createGetLettersHandler(deps: Deps): APIGatewayProxyHandler { }; } catch (error) { - return mapErrorToResponse(error, commonHeadersResult.value.correlationId, deps.logger); + return mapErrorToResponse(error, commonIds.value.correlationId, deps.logger); } } }; diff --git a/lambdas/api-handler/src/handlers/patch-letter.ts b/lambdas/api-handler/src/handlers/patch-letter.ts index e8442fae..2ff1d911 100644 --- a/lambdas/api-handler/src/handlers/patch-letter.ts +++ b/lambdas/api-handler/src/handlers/patch-letter.ts @@ -4,7 +4,8 @@ import { PatchLetterRequest, PatchLetterRequestSchema } from '../contracts/lette import { ApiErrorDetail } from '../contracts/errors'; import { ValidationError } from '../errors'; import { mapErrorToResponse } from '../mappers/error-mapper'; -import { assertNotEmpty, validateCommonHeaders } from '../utils/validation'; +import { assertNotEmpty } from '../utils/validation'; +import { extractCommonIds } from '../utils/commonIds'; import { mapToLetterDto } from '../mappers/letter-mapper'; import type { Deps } from "../config/deps"; @@ -13,10 +14,10 @@ export function createPatchLetterHandler(deps: Deps): APIGatewayProxyHandler { return async (event) => { - const commonHeadersResult = validateCommonHeaders(event.headers, deps); + const commonIds = extractCommonIds(event.headers, event.requestContext, deps); - if (!commonHeadersResult.ok) { - return mapErrorToResponse(commonHeadersResult.error, commonHeadersResult.correlationId, deps.logger); + if (!commonIds.ok) { + return mapErrorToResponse(commonIds.error, commonIds.correlationId, deps.logger); } try { @@ -35,7 +36,7 @@ export function createPatchLetterHandler(deps: Deps): APIGatewayProxyHandler { else throw error; } - const updatedLetter = await patchLetterStatus(mapToLetterDto(patchLetterRequest, commonHeadersResult.value.supplierId), letterId, deps.letterRepo); + const updatedLetter = await patchLetterStatus(mapToLetterDto(patchLetterRequest, commonIds.value.supplierId), letterId, deps.letterRepo); return { statusCode: 200, @@ -43,7 +44,7 @@ export function createPatchLetterHandler(deps: Deps): APIGatewayProxyHandler { }; } catch (error) { - return mapErrorToResponse(error, commonHeadersResult.value.correlationId, deps.logger); + return mapErrorToResponse(error, commonIds.value.correlationId, deps.logger); } }; }; diff --git a/lambdas/api-handler/src/handlers/post-mi.ts b/lambdas/api-handler/src/handlers/post-mi.ts index 526f849b..03229a13 100644 --- a/lambdas/api-handler/src/handlers/post-mi.ts +++ b/lambdas/api-handler/src/handlers/post-mi.ts @@ -3,7 +3,8 @@ import { postMI as postMIOperation } from '../services/mi-operations'; import { ApiErrorDetail } from "../contracts/errors"; import { ValidationError } from "../errors"; import { mapErrorToResponse } from "../mappers/error-mapper"; -import { assertNotEmpty, validateCommonHeaders, validateIso8601Timestamp } from "../utils/validation"; +import { assertNotEmpty, validateIso8601Timestamp } from "../utils/validation"; +import { extractCommonIds } from '../utils/commonIds'; import { PostMIRequest, PostMIRequestSchema } from "../contracts/mi"; import { mapToMI } from "../mappers/mi-mapper"; import { Deps } from "../config/deps"; @@ -12,10 +13,10 @@ export function createPostMIHandler(deps: Deps): APIGatewayProxyHandler { return async (event) => { - const commonHeadersResult = validateCommonHeaders(event.headers, deps); + const commonIds = extractCommonIds(event.headers, event.requestContext, deps); - if (!commonHeadersResult.ok) { - return mapErrorToResponse(commonHeadersResult.error, commonHeadersResult.correlationId, deps.logger); + if (!commonIds.ok) { + return mapErrorToResponse(commonIds.error, commonIds.correlationId, deps.logger); } try { @@ -33,7 +34,7 @@ export function createPostMIHandler(deps: Deps): APIGatewayProxyHandler { } validateIso8601Timestamp(postMIRequest.data.attributes.timestamp); - const result = await postMIOperation(mapToMI(postMIRequest, commonHeadersResult.value.supplierId), deps.miRepo); + const result = await postMIOperation(mapToMI(postMIRequest, commonIds.value.supplierId), deps.miRepo); return { statusCode: 201, @@ -41,7 +42,7 @@ export function createPostMIHandler(deps: Deps): APIGatewayProxyHandler { }; } catch (error) { - return mapErrorToResponse(error, commonHeadersResult.value.correlationId, deps.logger); + return mapErrorToResponse(error, commonIds.value.correlationId, deps.logger); } } }; diff --git a/lambdas/api-handler/src/utils/__tests__/commonIds.test.ts b/lambdas/api-handler/src/utils/__tests__/commonIds.test.ts new file mode 100644 index 00000000..2aa10c5b --- /dev/null +++ b/lambdas/api-handler/src/utils/__tests__/commonIds.test.ts @@ -0,0 +1,98 @@ +import { APIGatewayProxyEvent } from 'aws-lambda'; +import { extractCommonIds } from '../commonIds'; + +const mockDeps = { + env: { + APIM_CORRELATION_HEADER: 'x-correlation-id', + SUPPLIER_ID_HEADER: 'x-supplier-id', + } +} as any; + +const mockContext = {} as APIGatewayProxyEvent['requestContext']; + +describe('extractCommonIds', () => { + it('returns error if headers are missing', () => { + expect(extractCommonIds({}, mockContext, mockDeps)).toEqual({ + ok: false, + error: expect.any(Error) + }); + }); + + it('returns error if correlation id is missing', () => { + const headers = { 'x-supplier-id': 'SUP123', 'x-request-id': 'REQ123' }; + expect(extractCommonIds(headers, mockContext, mockDeps)).toEqual({ + ok: false, + error: expect.any(Error) + }); + }); + + it('returns error if request id is missing', () => { + const headers = { 'x-correlation-id': 'CORR123', 'x-supplier-id': 'SUP123' }; + expect(extractCommonIds(headers, mockContext, mockDeps)).toEqual({ + ok: false, + error: expect.any(Error), + correlationId: 'CORR123' + }); + }); + + it('returns error if supplier id is missing', () => { + const headers = { 'x-correlation-id': 'CORR123', 'x-request-id': 'REQ123' }; + expect(extractCommonIds(headers, mockContext, mockDeps)).toEqual({ + ok: false, + error: expect.any(Error), + correlationId: 'CORR123' + }); + }); + + it('returns ok and ids if all present', () => { + const headers = { + 'x-correlation-id': 'CORR123', + 'x-request-id': 'REQ123', + 'x-supplier-id': 'SUP123' + }; + expect(extractCommonIds(headers, mockContext, mockDeps)).toEqual({ + ok: true, + value: { + correlationId: 'CORR123', + supplierId: 'SUP123' + } + }); + }); + + it('handles mixed case header names', () => { + const headers = { + 'X-Correlation-Id': 'CORR123', + 'X-Request-Id': 'REQ123', + 'X-Supplier-Id': 'SUP123' + }; + expect(extractCommonIds(headers, mockContext, mockDeps)).toEqual({ + ok: true, + value: { + correlationId: 'CORR123', + supplierId: 'SUP123' + } + }); + }); + + it('uses the supplier id from the authorizer if present', () => { + const headers = { 'x-correlation-id': 'CORR123', 'x-supplier-id': 'SUP123', 'x-request-id': 'REQ123' }; + const context = { 'authorizer': {'principalId': 'SUP456'}} as unknown as APIGatewayProxyEvent['requestContext']; + expect(extractCommonIds(headers, context, mockDeps)).toEqual({ + ok: true, + value: { + correlationId: 'CORR123', + supplierId: 'SUP456' + } + }); + }); + + it('refuses to use the supplier id from the header if authorizer is present', () => { + const headers = { 'x-correlation-id': 'CORR123', 'x-supplier-id': 'SUP123', 'x-request-id': 'REQ123' }; + const context = { 'authorizer': {}} as unknown as APIGatewayProxyEvent['requestContext']; + expect(extractCommonIds(headers, context, mockDeps)).toEqual({ + ok: false, + error: expect.any(Error), + correlationId: 'CORR123' + }); + }); +}); diff --git a/lambdas/api-handler/src/utils/commonIds.ts b/lambdas/api-handler/src/utils/commonIds.ts new file mode 100644 index 00000000..47293a1f --- /dev/null +++ b/lambdas/api-handler/src/utils/commonIds.ts @@ -0,0 +1,45 @@ +import { APIGatewayProxyEvent, APIGatewayProxyEventHeaders } from 'aws-lambda'; +import { Deps } from '../config/deps'; +import { lowerCaseKeys } from './validation'; + + +export function extractCommonIds(headers: APIGatewayProxyEventHeaders, context: APIGatewayProxyEvent['requestContext'], deps: Deps +): { ok: true; value: { correlationId: string; supplierId: string; }; } | { ok: false; error: Error; correlationId?: string; } { + + if (!headers || Object.keys(headers).length === 0) { + return { ok: false, error: new Error('The request headers are empty') }; + } + + const lowerCasedHeaders = lowerCaseKeys(headers); + + const correlationId = lowerCasedHeaders[deps.env.APIM_CORRELATION_HEADER]; + if (!correlationId) { + return { ok: false, error: new Error("The request headers don't contain the APIM correlation id") }; + } + + const requestId = lowerCasedHeaders['x-request-id']; + if (!requestId) { + return { + ok: false, + error: new Error("The request headers don't contain the x-request-id"), + correlationId + }; + } + + // In normal API usage, we expect the authorizer to provide the supplier ID. When the lambda is invoked directly, for instance + // in the AWS console, then fall back to using the header. + + const supplierId = context.authorizer? + context.authorizer.principalId: + lowerCasedHeaders[deps.env.SUPPLIER_ID_HEADER]; + + if (!supplierId) { + return { + ok: false, + error: new Error('The supplier ID is missing from the request'), + correlationId + }; + } + + return { ok: true, value: { correlationId, supplierId } }; +} diff --git a/lambdas/api-handler/src/utils/validation.ts b/lambdas/api-handler/src/utils/validation.ts index f86b4fff..c898760d 100644 --- a/lambdas/api-handler/src/utils/validation.ts +++ b/lambdas/api-handler/src/utils/validation.ts @@ -1,7 +1,5 @@ -import { APIGatewayProxyEventHeaders } from 'aws-lambda'; import { ValidationError } from '../errors'; import { ApiErrorDetail } from '../contracts/errors'; -import { Deps } from '../config/deps'; export function assertNotEmpty( value: T | null | undefined, @@ -26,41 +24,6 @@ export function lowerCaseKeys(obj: Record): Record { return Object.fromEntries(Object.entries(obj).map(([k, v]) => [k.toLowerCase(), v])); } -export function validateCommonHeaders(headers: APIGatewayProxyEventHeaders, deps: Deps -): { ok: true; value: {correlationId: string, supplierId: string } } | { ok: false; error: Error; correlationId?: string } { - - if (!headers || Object.keys(headers).length === 0) { - return { ok: false, error: new Error('The request headers are empty') }; - } - - const lowerCasedHeaders = lowerCaseKeys(headers); - - const correlationId = lowerCasedHeaders[deps.env.APIM_CORRELATION_HEADER]; - if (!correlationId) { - return { ok: false, error: new Error("The request headers don't contain the APIM correlation id") }; - } - - const requestId = lowerCasedHeaders['x-request-id']; - if (!requestId) { - return { - ok: false, - error: new Error("The request headers don't contain the x-request-id"), - correlationId - }; - } - - const supplierId = lowerCasedHeaders[deps.env.SUPPLIER_ID_HEADER]; - if (!supplierId) { - return { - ok: false, - error: new Error('The supplier ID is missing from the request'), - correlationId - }; - } - - return { ok: true, value: { correlationId, supplierId } }; -} - export function validateIso8601Timestamp(timestamp: string) { function normalisePrecision([_, mainPart, fractionalPart='.000']: string[]) : string { diff --git a/lambdas/authorizer/src/__tests__/index.test.ts b/lambdas/authorizer/src/__tests__/index.test.ts index fe847aff..9f0aa8e1 100644 --- a/lambdas/authorizer/src/__tests__/index.test.ts +++ b/lambdas/authorizer/src/__tests__/index.test.ts @@ -168,6 +168,7 @@ describe('Authorizer Lambda Function', () => { id: 'supplier-123', apimApplicationId: 'valid-apim-id', name: 'Test Supplier', + status: 'ENABLED' }); const handler = createAuthorizerHandler(mockedDeps); @@ -182,10 +183,32 @@ describe('Authorizer Lambda Function', () => { }), ], }), - context: { - supplierId: 'supplier-123', - }, + principalId: 'supplier-123', })); }); }); + + it('Should deny the request the supplier is disabled', async () => { + mockEvent.headers = { 'apim-application-id': 'unknown-apim-id' }; + (mockedDeps.supplierRepo.getSupplierByApimId as jest.Mock).mockResolvedValue({ + id: 'supplier-123', + apimApplicationId: 'valid-apim-id', + name: 'Test Supplier', + status: 'DISABLED' + }); + + const handler = createAuthorizerHandler(mockedDeps); + handler(mockEvent, mockContext, mockCallback); + await new Promise(process.nextTick); + + expect(mockCallback).toHaveBeenCalledWith(null, expect.objectContaining({ + policyDocument: expect.objectContaining({ + Statement: [ + expect.objectContaining({ + Effect: 'Deny', + }), + ], + }), + })); + }); }); diff --git a/lambdas/authorizer/src/authorizer.ts b/lambdas/authorizer/src/authorizer.ts index 9d13d524..443f0cd3 100644 --- a/lambdas/authorizer/src/authorizer.ts +++ b/lambdas/authorizer/src/authorizer.ts @@ -11,7 +11,7 @@ // See https://docs.aws.amazon.com/apigateway/latest/developerguide/apigateway-use-lambda-authorizer.html for the original JS documentation -import {APIGatewayAuthorizerResult, APIGatewayAuthorizerResultContext, APIGatewayEventClientCertificate, APIGatewayRequestAuthorizerEvent, APIGatewayRequestAuthorizerEventHeaders, APIGatewayRequestAuthorizerHandler, +import {APIGatewayAuthorizerResult, APIGatewayEventClientCertificate, APIGatewayRequestAuthorizerEvent, APIGatewayRequestAuthorizerEventHeaders, APIGatewayRequestAuthorizerHandler, Callback, Context } from 'aws-lambda'; import { Deps } from './deps'; import { PutMetricDataCommand } from '@aws-sdk/client-cloudwatch'; @@ -28,27 +28,30 @@ export function createAuthorizerHandler(deps: Deps): APIGatewayRequestAuthorizer checkCertificateExpiry(event.requestContext.identity.clientCert, deps) - .then(() => deps.supplierRepo.getSupplierByApimId(extractApimId(event.headers, deps))) + .then(() => getSupplier(event.headers, deps)) .then((supplier: Supplier) => { deps.logger.info('Allow event'); - callback(null, generateAllow('me', event.methodArn, supplier.id)); + callback(null, generateAllow(event.methodArn, supplier.id)); }) .catch((error) => { - deps.logger.info({error}, 'Deny event'); - callback(null, generateDeny('me', event.methodArn)); + deps.logger.info(error, 'Deny event'); + callback(null, generateDeny(event.methodArn)); }); }; } - -function extractApimId(headers: APIGatewayRequestAuthorizerEventHeaders | null, deps: Deps): string { +async function getSupplier(headers: APIGatewayRequestAuthorizerEventHeaders | null, deps: Deps): Promise { const apimId = Object.entries(headers || {}) .find(([headerName, _]) => headerName.toLowerCase() === deps.env.APIM_APPLICATION_ID_HEADER)?.[1] as string; if(!apimId) { - throw new Error("No APIM application ID found in header"); + throw new Error('No APIM application ID found in header'); + } + const supplier = await deps.supplierRepo.getSupplierByApimId(apimId); + if (supplier.status === 'DISABLED') { + throw new Error(`Supplier ${supplier.id} is disabled`); } - return apimId; + return supplier; } @@ -56,8 +59,7 @@ function extractApimId(headers: APIGatewayRequestAuthorizerEventHeaders | null, function generatePolicy( principalId: string, effect: 'Allow' | 'Deny', - resource: string, - context: APIGatewayAuthorizerResultContext + resource: string ): APIGatewayAuthorizerResult { // Required output: const authResponse: APIGatewayAuthorizerResult = { @@ -72,17 +74,16 @@ function extractApimId(headers: APIGatewayRequestAuthorizerEventHeaders | null, }, ], }, - context: context, }; return authResponse; } -function generateAllow(principalId: string, resource: string, supplierId: string): APIGatewayAuthorizerResult { - return generatePolicy(principalId, 'Allow', resource, {supplierId: supplierId}); +function generateAllow(resource: string, supplierId: string): APIGatewayAuthorizerResult { + return generatePolicy(supplierId, 'Allow', resource); } -function generateDeny(principalId: string, resource: string): APIGatewayAuthorizerResult { - return generatePolicy(principalId, 'Deny', resource, {}); +function generateDeny(resource: string): APIGatewayAuthorizerResult { + return generatePolicy('invalid-user', 'Deny', resource); } function getCertificateExpiryInDays(certificate: APIGatewayEventClientCertificate): number { From acc043a340a9e9d0d077fe5d2d85cdfe8d436736 Mon Sep 17 00:00:00 2001 From: Steve Buxton Date: Thu, 6 Nov 2025 14:29:48 +0000 Subject: [PATCH 11/14] Fix dependencies --- package-lock.json | 998 ++++++++++++++++++++++++---------------------- package.json | 3 +- 2 files changed, 518 insertions(+), 483 deletions(-) diff --git a/package-lock.json b/package-lock.json index c1636e4d..a896bdd2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14,6 +14,7 @@ ], "dependencies": { "@aws-sdk/client-api-gateway": "^3.906.0", + "@aws-sdk/client-s3": "^3.925.0", "@playwright/test": "^1.55.1", "ajv": "^8.17.1", "js-yaml": "^4.1.0", @@ -3094,6 +3095,8 @@ }, "node_modules/@aws-crypto/crc32": { "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/crc32/-/crc32-5.2.0.tgz", + "integrity": "sha512-nLbCWqQNgUiwwtFsen1AdzAtvuLRsQS8rYgMuxCrdKf9kOssamGLuPwyTY9wyYblNr9+1XM8v6zoDTPPSIeANg==", "license": "Apache-2.0", "dependencies": { "@aws-crypto/util": "^5.2.0", @@ -3106,6 +3109,8 @@ }, "node_modules/@aws-crypto/crc32c": { "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/crc32c/-/crc32c-5.2.0.tgz", + "integrity": "sha512-+iWb8qaHLYKrNvGRbiYRHSdKRWhto5XlZUEBwDjYNf+ly5SVYG6zEoYIdxvf5R3zyeP16w4PLBn3rH1xc74Rag==", "license": "Apache-2.0", "dependencies": { "@aws-crypto/util": "^5.2.0", @@ -4313,65 +4318,67 @@ } }, "node_modules/@aws-sdk/client-s3": { - "version": "3.896.0", + "version": "3.925.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-s3/-/client-s3-3.925.0.tgz", + "integrity": "sha512-imAul+6pyJYH4cbxPz1OiFXxrKKTRqVzlT2e0M6zbPHmUcJsF5E+b+4qvHQChU8wFGtIWJHH/JChF2ibfTnXdA==", "license": "Apache-2.0", "dependencies": { "@aws-crypto/sha1-browser": "5.2.0", "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", - "@aws-sdk/core": "3.896.0", - "@aws-sdk/credential-provider-node": "3.896.0", - "@aws-sdk/middleware-bucket-endpoint": "3.893.0", - "@aws-sdk/middleware-expect-continue": "3.893.0", - "@aws-sdk/middleware-flexible-checksums": "3.896.0", - "@aws-sdk/middleware-host-header": "3.893.0", - "@aws-sdk/middleware-location-constraint": "3.893.0", - "@aws-sdk/middleware-logger": "3.893.0", - "@aws-sdk/middleware-recursion-detection": "3.893.0", - "@aws-sdk/middleware-sdk-s3": "3.896.0", - "@aws-sdk/middleware-ssec": "3.893.0", - "@aws-sdk/middleware-user-agent": "3.896.0", - "@aws-sdk/region-config-resolver": "3.893.0", - "@aws-sdk/signature-v4-multi-region": "3.896.0", - "@aws-sdk/types": "3.893.0", - "@aws-sdk/util-endpoints": "3.895.0", - "@aws-sdk/util-user-agent-browser": "3.893.0", - "@aws-sdk/util-user-agent-node": "3.896.0", - "@aws-sdk/xml-builder": "3.894.0", - "@smithy/config-resolver": "^4.2.2", - "@smithy/core": "^3.12.0", - "@smithy/eventstream-serde-browser": "^4.1.1", - "@smithy/eventstream-serde-config-resolver": "^4.2.1", - "@smithy/eventstream-serde-node": "^4.1.1", - "@smithy/fetch-http-handler": "^5.2.1", - "@smithy/hash-blob-browser": "^4.1.1", - "@smithy/hash-node": "^4.1.1", - "@smithy/hash-stream-node": "^4.1.1", - "@smithy/invalid-dependency": "^4.1.1", - "@smithy/md5-js": "^4.1.1", - "@smithy/middleware-content-length": "^4.1.1", - "@smithy/middleware-endpoint": "^4.2.4", - "@smithy/middleware-retry": "^4.3.0", - "@smithy/middleware-serde": "^4.1.1", - "@smithy/middleware-stack": "^4.1.1", - "@smithy/node-config-provider": "^4.2.2", - "@smithy/node-http-handler": "^4.2.1", - "@smithy/protocol-http": "^5.2.1", - "@smithy/smithy-client": "^4.6.4", - "@smithy/types": "^4.5.0", - "@smithy/url-parser": "^4.1.1", - "@smithy/util-base64": "^4.1.0", - "@smithy/util-body-length-browser": "^4.1.0", - "@smithy/util-body-length-node": "^4.1.0", - "@smithy/util-defaults-mode-browser": "^4.1.4", - "@smithy/util-defaults-mode-node": "^4.1.4", - "@smithy/util-endpoints": "^3.1.2", - "@smithy/util-middleware": "^4.1.1", - "@smithy/util-retry": "^4.1.2", - "@smithy/util-stream": "^4.3.2", - "@smithy/util-utf8": "^4.1.0", - "@smithy/util-waiter": "^4.1.1", - "@smithy/uuid": "^1.0.0", + "@aws-sdk/core": "3.922.0", + "@aws-sdk/credential-provider-node": "3.925.0", + "@aws-sdk/middleware-bucket-endpoint": "3.922.0", + "@aws-sdk/middleware-expect-continue": "3.922.0", + "@aws-sdk/middleware-flexible-checksums": "3.922.0", + "@aws-sdk/middleware-host-header": "3.922.0", + "@aws-sdk/middleware-location-constraint": "3.922.0", + "@aws-sdk/middleware-logger": "3.922.0", + "@aws-sdk/middleware-recursion-detection": "3.922.0", + "@aws-sdk/middleware-sdk-s3": "3.922.0", + "@aws-sdk/middleware-ssec": "3.922.0", + "@aws-sdk/middleware-user-agent": "3.922.0", + "@aws-sdk/region-config-resolver": "3.925.0", + "@aws-sdk/signature-v4-multi-region": "3.922.0", + "@aws-sdk/types": "3.922.0", + "@aws-sdk/util-endpoints": "3.922.0", + "@aws-sdk/util-user-agent-browser": "3.922.0", + "@aws-sdk/util-user-agent-node": "3.922.0", + "@aws-sdk/xml-builder": "3.921.0", + "@smithy/config-resolver": "^4.4.2", + "@smithy/core": "^3.17.2", + "@smithy/eventstream-serde-browser": "^4.2.4", + "@smithy/eventstream-serde-config-resolver": "^4.3.4", + "@smithy/eventstream-serde-node": "^4.2.4", + "@smithy/fetch-http-handler": "^5.3.5", + "@smithy/hash-blob-browser": "^4.2.5", + "@smithy/hash-node": "^4.2.4", + "@smithy/hash-stream-node": "^4.2.4", + "@smithy/invalid-dependency": "^4.2.4", + "@smithy/md5-js": "^4.2.4", + "@smithy/middleware-content-length": "^4.2.4", + "@smithy/middleware-endpoint": "^4.3.6", + "@smithy/middleware-retry": "^4.4.6", + "@smithy/middleware-serde": "^4.2.4", + "@smithy/middleware-stack": "^4.2.4", + "@smithy/node-config-provider": "^4.3.4", + "@smithy/node-http-handler": "^4.4.4", + "@smithy/protocol-http": "^5.3.4", + "@smithy/smithy-client": "^4.9.2", + "@smithy/types": "^4.8.1", + "@smithy/url-parser": "^4.2.4", + "@smithy/util-base64": "^4.3.0", + "@smithy/util-body-length-browser": "^4.2.0", + "@smithy/util-body-length-node": "^4.2.1", + "@smithy/util-defaults-mode-browser": "^4.3.5", + "@smithy/util-defaults-mode-node": "^4.2.8", + "@smithy/util-endpoints": "^3.2.4", + "@smithy/util-middleware": "^4.2.4", + "@smithy/util-retry": "^4.2.4", + "@smithy/util-stream": "^4.5.5", + "@smithy/util-utf8": "^4.2.0", + "@smithy/util-waiter": "^4.2.4", + "@smithy/uuid": "^1.1.0", "tslib": "^2.6.2" }, "engines": { @@ -4379,46 +4386,48 @@ } }, "node_modules/@aws-sdk/client-s3/node_modules/@aws-sdk/client-sso": { - "version": "3.896.0", + "version": "3.925.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-sso/-/client-sso-3.925.0.tgz", + "integrity": "sha512-ixC9CyXe/mBo1X+bzOxIIzsdBYzM+klWoHUYzwnPMrXhpDrMjj8D24R/FPqrDnhoYYXiyS4BApRLpeymsFJq2Q==", "license": "Apache-2.0", "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", - "@aws-sdk/core": "3.896.0", - "@aws-sdk/middleware-host-header": "3.893.0", - "@aws-sdk/middleware-logger": "3.893.0", - "@aws-sdk/middleware-recursion-detection": "3.893.0", - "@aws-sdk/middleware-user-agent": "3.896.0", - "@aws-sdk/region-config-resolver": "3.893.0", - "@aws-sdk/types": "3.893.0", - "@aws-sdk/util-endpoints": "3.895.0", - "@aws-sdk/util-user-agent-browser": "3.893.0", - "@aws-sdk/util-user-agent-node": "3.896.0", - "@smithy/config-resolver": "^4.2.2", - "@smithy/core": "^3.12.0", - "@smithy/fetch-http-handler": "^5.2.1", - "@smithy/hash-node": "^4.1.1", - "@smithy/invalid-dependency": "^4.1.1", - "@smithy/middleware-content-length": "^4.1.1", - "@smithy/middleware-endpoint": "^4.2.4", - "@smithy/middleware-retry": "^4.3.0", - "@smithy/middleware-serde": "^4.1.1", - "@smithy/middleware-stack": "^4.1.1", - "@smithy/node-config-provider": "^4.2.2", - "@smithy/node-http-handler": "^4.2.1", - "@smithy/protocol-http": "^5.2.1", - "@smithy/smithy-client": "^4.6.4", - "@smithy/types": "^4.5.0", - "@smithy/url-parser": "^4.1.1", - "@smithy/util-base64": "^4.1.0", - "@smithy/util-body-length-browser": "^4.1.0", - "@smithy/util-body-length-node": "^4.1.0", - "@smithy/util-defaults-mode-browser": "^4.1.4", - "@smithy/util-defaults-mode-node": "^4.1.4", - "@smithy/util-endpoints": "^3.1.2", - "@smithy/util-middleware": "^4.1.1", - "@smithy/util-retry": "^4.1.2", - "@smithy/util-utf8": "^4.1.0", + "@aws-sdk/core": "3.922.0", + "@aws-sdk/middleware-host-header": "3.922.0", + "@aws-sdk/middleware-logger": "3.922.0", + "@aws-sdk/middleware-recursion-detection": "3.922.0", + "@aws-sdk/middleware-user-agent": "3.922.0", + "@aws-sdk/region-config-resolver": "3.925.0", + "@aws-sdk/types": "3.922.0", + "@aws-sdk/util-endpoints": "3.922.0", + "@aws-sdk/util-user-agent-browser": "3.922.0", + "@aws-sdk/util-user-agent-node": "3.922.0", + "@smithy/config-resolver": "^4.4.2", + "@smithy/core": "^3.17.2", + "@smithy/fetch-http-handler": "^5.3.5", + "@smithy/hash-node": "^4.2.4", + "@smithy/invalid-dependency": "^4.2.4", + "@smithy/middleware-content-length": "^4.2.4", + "@smithy/middleware-endpoint": "^4.3.6", + "@smithy/middleware-retry": "^4.4.6", + "@smithy/middleware-serde": "^4.2.4", + "@smithy/middleware-stack": "^4.2.4", + "@smithy/node-config-provider": "^4.3.4", + "@smithy/node-http-handler": "^4.4.4", + "@smithy/protocol-http": "^5.3.4", + "@smithy/smithy-client": "^4.9.2", + "@smithy/types": "^4.8.1", + "@smithy/url-parser": "^4.2.4", + "@smithy/util-base64": "^4.3.0", + "@smithy/util-body-length-browser": "^4.2.0", + "@smithy/util-body-length-node": "^4.2.1", + "@smithy/util-defaults-mode-browser": "^4.3.5", + "@smithy/util-defaults-mode-node": "^4.2.8", + "@smithy/util-endpoints": "^3.2.4", + "@smithy/util-middleware": "^4.2.4", + "@smithy/util-retry": "^4.2.4", + "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" }, "engines": { @@ -4426,21 +4435,23 @@ } }, "node_modules/@aws-sdk/client-s3/node_modules/@aws-sdk/core": { - "version": "3.896.0", - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/types": "3.893.0", - "@aws-sdk/xml-builder": "3.894.0", - "@smithy/core": "^3.12.0", - "@smithy/node-config-provider": "^4.2.2", - "@smithy/property-provider": "^4.1.1", - "@smithy/protocol-http": "^5.2.1", - "@smithy/signature-v4": "^5.2.1", - "@smithy/smithy-client": "^4.6.4", - "@smithy/types": "^4.5.0", - "@smithy/util-base64": "^4.1.0", - "@smithy/util-middleware": "^4.1.1", - "@smithy/util-utf8": "^4.1.0", + "version": "3.922.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.922.0.tgz", + "integrity": "sha512-EvfP4cqJfpO3L2v5vkIlTkMesPtRwWlMfsaW6Tpfm7iYfBOuTi6jx60pMDMTyJNVfh6cGmXwh/kj1jQdR+w99Q==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.922.0", + "@aws-sdk/xml-builder": "3.921.0", + "@smithy/core": "^3.17.2", + "@smithy/node-config-provider": "^4.3.4", + "@smithy/property-provider": "^4.2.4", + "@smithy/protocol-http": "^5.3.4", + "@smithy/signature-v4": "^5.3.4", + "@smithy/smithy-client": "^4.9.2", + "@smithy/types": "^4.8.1", + "@smithy/util-base64": "^4.3.0", + "@smithy/util-middleware": "^4.2.4", + "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" }, "engines": { @@ -4448,13 +4459,15 @@ } }, "node_modules/@aws-sdk/client-s3/node_modules/@aws-sdk/credential-provider-env": { - "version": "3.896.0", + "version": "3.922.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-env/-/credential-provider-env-3.922.0.tgz", + "integrity": "sha512-WikGQpKkROJSK3D3E7odPjZ8tU7WJp5/TgGdRuZw3izsHUeH48xMv6IznafpRTmvHcjAbDQj4U3CJZNAzOK/OQ==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "3.896.0", - "@aws-sdk/types": "3.893.0", - "@smithy/property-provider": "^4.1.1", - "@smithy/types": "^4.5.0", + "@aws-sdk/core": "3.922.0", + "@aws-sdk/types": "3.922.0", + "@smithy/property-provider": "^4.2.4", + "@smithy/types": "^4.8.1", "tslib": "^2.6.2" }, "engines": { @@ -4462,18 +4475,20 @@ } }, "node_modules/@aws-sdk/client-s3/node_modules/@aws-sdk/credential-provider-http": { - "version": "3.896.0", - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/core": "3.896.0", - "@aws-sdk/types": "3.893.0", - "@smithy/fetch-http-handler": "^5.2.1", - "@smithy/node-http-handler": "^4.2.1", - "@smithy/property-provider": "^4.1.1", - "@smithy/protocol-http": "^5.2.1", - "@smithy/smithy-client": "^4.6.4", - "@smithy/types": "^4.5.0", - "@smithy/util-stream": "^4.3.2", + "version": "3.922.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-http/-/credential-provider-http-3.922.0.tgz", + "integrity": "sha512-i72DgHMK7ydAEqdzU0Duqh60Q8W59EZmRJ73y0Y5oFmNOqnYsAI+UXyOoCsubp+Dkr6+yOwAn1gPt1XGE9Aowg==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "3.922.0", + "@aws-sdk/types": "3.922.0", + "@smithy/fetch-http-handler": "^5.3.5", + "@smithy/node-http-handler": "^4.4.4", + "@smithy/property-provider": "^4.2.4", + "@smithy/protocol-http": "^5.3.4", + "@smithy/smithy-client": "^4.9.2", + "@smithy/types": "^4.8.1", + "@smithy/util-stream": "^4.5.5", "tslib": "^2.6.2" }, "engines": { @@ -4481,21 +4496,23 @@ } }, "node_modules/@aws-sdk/client-s3/node_modules/@aws-sdk/credential-provider-ini": { - "version": "3.896.0", - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/core": "3.896.0", - "@aws-sdk/credential-provider-env": "3.896.0", - "@aws-sdk/credential-provider-http": "3.896.0", - "@aws-sdk/credential-provider-process": "3.896.0", - "@aws-sdk/credential-provider-sso": "3.896.0", - "@aws-sdk/credential-provider-web-identity": "3.896.0", - "@aws-sdk/nested-clients": "3.896.0", - "@aws-sdk/types": "3.893.0", - "@smithy/credential-provider-imds": "^4.1.2", - "@smithy/property-provider": "^4.1.1", - "@smithy/shared-ini-file-loader": "^4.2.0", - "@smithy/types": "^4.5.0", + "version": "3.925.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.925.0.tgz", + "integrity": "sha512-TOs/UkKWwXrSPolRTChpDUQjczw6KqbbanF0EzjUm3sp/AS1ThOQCKuTTdaOBZXkCIJdvRmZjF3adccE3rAoXg==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "3.922.0", + "@aws-sdk/credential-provider-env": "3.922.0", + "@aws-sdk/credential-provider-http": "3.922.0", + "@aws-sdk/credential-provider-process": "3.922.0", + "@aws-sdk/credential-provider-sso": "3.925.0", + "@aws-sdk/credential-provider-web-identity": "3.925.0", + "@aws-sdk/nested-clients": "3.925.0", + "@aws-sdk/types": "3.922.0", + "@smithy/credential-provider-imds": "^4.2.4", + "@smithy/property-provider": "^4.2.4", + "@smithy/shared-ini-file-loader": "^4.3.4", + "@smithy/types": "^4.8.1", "tslib": "^2.6.2" }, "engines": { @@ -4503,20 +4520,22 @@ } }, "node_modules/@aws-sdk/client-s3/node_modules/@aws-sdk/credential-provider-node": { - "version": "3.896.0", - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/credential-provider-env": "3.896.0", - "@aws-sdk/credential-provider-http": "3.896.0", - "@aws-sdk/credential-provider-ini": "3.896.0", - "@aws-sdk/credential-provider-process": "3.896.0", - "@aws-sdk/credential-provider-sso": "3.896.0", - "@aws-sdk/credential-provider-web-identity": "3.896.0", - "@aws-sdk/types": "3.893.0", - "@smithy/credential-provider-imds": "^4.1.2", - "@smithy/property-provider": "^4.1.1", - "@smithy/shared-ini-file-loader": "^4.2.0", - "@smithy/types": "^4.5.0", + "version": "3.925.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.925.0.tgz", + "integrity": "sha512-+T9mnnTY73MLkVxsk5RtzE4fv7GnMhR7iXhL/yTusf1zLfA09uxlA9VCz6tWxm5rHcO4ZN0x4hnqqDhM+DB5KQ==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/credential-provider-env": "3.922.0", + "@aws-sdk/credential-provider-http": "3.922.0", + "@aws-sdk/credential-provider-ini": "3.925.0", + "@aws-sdk/credential-provider-process": "3.922.0", + "@aws-sdk/credential-provider-sso": "3.925.0", + "@aws-sdk/credential-provider-web-identity": "3.925.0", + "@aws-sdk/types": "3.922.0", + "@smithy/credential-provider-imds": "^4.2.4", + "@smithy/property-provider": "^4.2.4", + "@smithy/shared-ini-file-loader": "^4.3.4", + "@smithy/types": "^4.8.1", "tslib": "^2.6.2" }, "engines": { @@ -4524,14 +4543,16 @@ } }, "node_modules/@aws-sdk/client-s3/node_modules/@aws-sdk/credential-provider-process": { - "version": "3.896.0", + "version": "3.922.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-process/-/credential-provider-process-3.922.0.tgz", + "integrity": "sha512-1DZOYezT6okslpvMW7oA2q+y17CJd4fxjNFH0jtThfswdh9CtG62+wxenqO+NExttq0UMaKisrkZiVrYQBTShw==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "3.896.0", - "@aws-sdk/types": "3.893.0", - "@smithy/property-provider": "^4.1.1", - "@smithy/shared-ini-file-loader": "^4.2.0", - "@smithy/types": "^4.5.0", + "@aws-sdk/core": "3.922.0", + "@aws-sdk/types": "3.922.0", + "@smithy/property-provider": "^4.2.4", + "@smithy/shared-ini-file-loader": "^4.3.4", + "@smithy/types": "^4.8.1", "tslib": "^2.6.2" }, "engines": { @@ -4539,16 +4560,18 @@ } }, "node_modules/@aws-sdk/client-s3/node_modules/@aws-sdk/credential-provider-sso": { - "version": "3.896.0", + "version": "3.925.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.925.0.tgz", + "integrity": "sha512-aZlUC6LRsOMDvIu0ifF62mTjL3KGzclWu5XBBN8eLDAYTdhqMxv3HyrqWoiHnGZnZGaVU+II+qsVoeBnGOwHow==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/client-sso": "3.896.0", - "@aws-sdk/core": "3.896.0", - "@aws-sdk/token-providers": "3.896.0", - "@aws-sdk/types": "3.893.0", - "@smithy/property-provider": "^4.1.1", - "@smithy/shared-ini-file-loader": "^4.2.0", - "@smithy/types": "^4.5.0", + "@aws-sdk/client-sso": "3.925.0", + "@aws-sdk/core": "3.922.0", + "@aws-sdk/token-providers": "3.925.0", + "@aws-sdk/types": "3.922.0", + "@smithy/property-provider": "^4.2.4", + "@smithy/shared-ini-file-loader": "^4.3.4", + "@smithy/types": "^4.8.1", "tslib": "^2.6.2" }, "engines": { @@ -4556,15 +4579,17 @@ } }, "node_modules/@aws-sdk/client-s3/node_modules/@aws-sdk/credential-provider-web-identity": { - "version": "3.896.0", + "version": "3.925.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.925.0.tgz", + "integrity": "sha512-dR34s8Sfd1wJBzIuvRFO2FCnLmYD8iwPWrdXWI2ZypFt1EQR8jeQ20mnS+UOCoR5Z0tY6wJqEgTXKl4KuZ+DUg==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "3.896.0", - "@aws-sdk/nested-clients": "3.896.0", - "@aws-sdk/types": "3.893.0", - "@smithy/property-provider": "^4.1.1", - "@smithy/shared-ini-file-loader": "^4.2.0", - "@smithy/types": "^4.5.0", + "@aws-sdk/core": "3.922.0", + "@aws-sdk/nested-clients": "3.925.0", + "@aws-sdk/types": "3.922.0", + "@smithy/property-provider": "^4.2.4", + "@smithy/shared-ini-file-loader": "^4.3.4", + "@smithy/types": "^4.8.1", "tslib": "^2.6.2" }, "engines": { @@ -4572,12 +4597,14 @@ } }, "node_modules/@aws-sdk/client-s3/node_modules/@aws-sdk/middleware-host-header": { - "version": "3.893.0", + "version": "3.922.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-host-header/-/middleware-host-header-3.922.0.tgz", + "integrity": "sha512-HPquFgBnq/KqKRVkiuCt97PmWbKtxQ5iUNLEc6FIviqOoZTmaYG3EDsIbuFBz9C4RHJU4FKLmHL2bL3FEId6AA==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/types": "3.893.0", - "@smithy/protocol-http": "^5.2.1", - "@smithy/types": "^4.5.0", + "@aws-sdk/types": "3.922.0", + "@smithy/protocol-http": "^5.3.4", + "@smithy/types": "^4.8.1", "tslib": "^2.6.2" }, "engines": { @@ -4585,11 +4612,13 @@ } }, "node_modules/@aws-sdk/client-s3/node_modules/@aws-sdk/middleware-logger": { - "version": "3.893.0", + "version": "3.922.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-logger/-/middleware-logger-3.922.0.tgz", + "integrity": "sha512-AkvYO6b80FBm5/kk2E636zNNcNgjztNNUxpqVx+huyGn9ZqGTzS4kLqW2hO6CBe5APzVtPCtiQsXL24nzuOlAg==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/types": "3.893.0", - "@smithy/types": "^4.5.0", + "@aws-sdk/types": "3.922.0", + "@smithy/types": "^4.8.1", "tslib": "^2.6.2" }, "engines": { @@ -4597,13 +4626,15 @@ } }, "node_modules/@aws-sdk/client-s3/node_modules/@aws-sdk/middleware-recursion-detection": { - "version": "3.893.0", + "version": "3.922.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-recursion-detection/-/middleware-recursion-detection-3.922.0.tgz", + "integrity": "sha512-TtSCEDonV/9R0VhVlCpxZbp/9sxQvTTRKzIf8LxW3uXpby6Wl8IxEciBJlxmSkoqxh542WRcko7NYODlvL/gDA==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/types": "3.893.0", - "@aws/lambda-invoke-store": "^0.0.1", - "@smithy/protocol-http": "^5.2.1", - "@smithy/types": "^4.5.0", + "@aws-sdk/types": "3.922.0", + "@aws/lambda-invoke-store": "^0.1.1", + "@smithy/protocol-http": "^5.3.4", + "@smithy/types": "^4.8.1", "tslib": "^2.6.2" }, "engines": { @@ -4611,15 +4642,17 @@ } }, "node_modules/@aws-sdk/client-s3/node_modules/@aws-sdk/middleware-user-agent": { - "version": "3.896.0", + "version": "3.922.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-user-agent/-/middleware-user-agent-3.922.0.tgz", + "integrity": "sha512-N4Qx/9KP3oVQBJOrSghhz8iZFtUC2NNeSZt88hpPhbqAEAtuX8aD8OzVcpnAtrwWqy82Yd2YTxlkqMGkgqnBsQ==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "3.896.0", - "@aws-sdk/types": "3.893.0", - "@aws-sdk/util-endpoints": "3.895.0", - "@smithy/core": "^3.12.0", - "@smithy/protocol-http": "^5.2.1", - "@smithy/types": "^4.5.0", + "@aws-sdk/core": "3.922.0", + "@aws-sdk/types": "3.922.0", + "@aws-sdk/util-endpoints": "3.922.0", + "@smithy/core": "^3.17.2", + "@smithy/protocol-http": "^5.3.4", + "@smithy/types": "^4.8.1", "tslib": "^2.6.2" }, "engines": { @@ -4627,46 +4660,48 @@ } }, "node_modules/@aws-sdk/client-s3/node_modules/@aws-sdk/nested-clients": { - "version": "3.896.0", + "version": "3.925.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/nested-clients/-/nested-clients-3.925.0.tgz", + "integrity": "sha512-Fc8QhH+1YzGQb5aWQUX6gRnKSzUZ9p3p/muqXIgYBL8RSd5O6hSPhDTyrOWE247zFlOjVlAlEnoTMJKarH0cIA==", "license": "Apache-2.0", "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", - "@aws-sdk/core": "3.896.0", - "@aws-sdk/middleware-host-header": "3.893.0", - "@aws-sdk/middleware-logger": "3.893.0", - "@aws-sdk/middleware-recursion-detection": "3.893.0", - "@aws-sdk/middleware-user-agent": "3.896.0", - "@aws-sdk/region-config-resolver": "3.893.0", - "@aws-sdk/types": "3.893.0", - "@aws-sdk/util-endpoints": "3.895.0", - "@aws-sdk/util-user-agent-browser": "3.893.0", - "@aws-sdk/util-user-agent-node": "3.896.0", - "@smithy/config-resolver": "^4.2.2", - "@smithy/core": "^3.12.0", - "@smithy/fetch-http-handler": "^5.2.1", - "@smithy/hash-node": "^4.1.1", - "@smithy/invalid-dependency": "^4.1.1", - "@smithy/middleware-content-length": "^4.1.1", - "@smithy/middleware-endpoint": "^4.2.4", - "@smithy/middleware-retry": "^4.3.0", - "@smithy/middleware-serde": "^4.1.1", - "@smithy/middleware-stack": "^4.1.1", - "@smithy/node-config-provider": "^4.2.2", - "@smithy/node-http-handler": "^4.2.1", - "@smithy/protocol-http": "^5.2.1", - "@smithy/smithy-client": "^4.6.4", - "@smithy/types": "^4.5.0", - "@smithy/url-parser": "^4.1.1", - "@smithy/util-base64": "^4.1.0", - "@smithy/util-body-length-browser": "^4.1.0", - "@smithy/util-body-length-node": "^4.1.0", - "@smithy/util-defaults-mode-browser": "^4.1.4", - "@smithy/util-defaults-mode-node": "^4.1.4", - "@smithy/util-endpoints": "^3.1.2", - "@smithy/util-middleware": "^4.1.1", - "@smithy/util-retry": "^4.1.2", - "@smithy/util-utf8": "^4.1.0", + "@aws-sdk/core": "3.922.0", + "@aws-sdk/middleware-host-header": "3.922.0", + "@aws-sdk/middleware-logger": "3.922.0", + "@aws-sdk/middleware-recursion-detection": "3.922.0", + "@aws-sdk/middleware-user-agent": "3.922.0", + "@aws-sdk/region-config-resolver": "3.925.0", + "@aws-sdk/types": "3.922.0", + "@aws-sdk/util-endpoints": "3.922.0", + "@aws-sdk/util-user-agent-browser": "3.922.0", + "@aws-sdk/util-user-agent-node": "3.922.0", + "@smithy/config-resolver": "^4.4.2", + "@smithy/core": "^3.17.2", + "@smithy/fetch-http-handler": "^5.3.5", + "@smithy/hash-node": "^4.2.4", + "@smithy/invalid-dependency": "^4.2.4", + "@smithy/middleware-content-length": "^4.2.4", + "@smithy/middleware-endpoint": "^4.3.6", + "@smithy/middleware-retry": "^4.4.6", + "@smithy/middleware-serde": "^4.2.4", + "@smithy/middleware-stack": "^4.2.4", + "@smithy/node-config-provider": "^4.3.4", + "@smithy/node-http-handler": "^4.4.4", + "@smithy/protocol-http": "^5.3.4", + "@smithy/smithy-client": "^4.9.2", + "@smithy/types": "^4.8.1", + "@smithy/url-parser": "^4.2.4", + "@smithy/util-base64": "^4.3.0", + "@smithy/util-body-length-browser": "^4.2.0", + "@smithy/util-body-length-node": "^4.2.1", + "@smithy/util-defaults-mode-browser": "^4.3.5", + "@smithy/util-defaults-mode-node": "^4.2.8", + "@smithy/util-endpoints": "^3.2.4", + "@smithy/util-middleware": "^4.2.4", + "@smithy/util-retry": "^4.2.4", + "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" }, "engines": { @@ -4674,14 +4709,15 @@ } }, "node_modules/@aws-sdk/client-s3/node_modules/@aws-sdk/region-config-resolver": { - "version": "3.893.0", + "version": "3.925.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/region-config-resolver/-/region-config-resolver-3.925.0.tgz", + "integrity": "sha512-FOthcdF9oDb1pfQBRCfWPZhJZT5wqpvdAS5aJzB1WDZ+6EuaAhLzLH/fW1slDunIqq1PSQGG3uSnVglVVOvPHQ==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/types": "3.893.0", - "@smithy/node-config-provider": "^4.2.2", - "@smithy/types": "^4.5.0", - "@smithy/util-config-provider": "^4.1.0", - "@smithy/util-middleware": "^4.1.1", + "@aws-sdk/types": "3.922.0", + "@smithy/config-resolver": "^4.4.2", + "@smithy/node-config-provider": "^4.3.4", + "@smithy/types": "^4.8.1", "tslib": "^2.6.2" }, "engines": { @@ -4689,15 +4725,17 @@ } }, "node_modules/@aws-sdk/client-s3/node_modules/@aws-sdk/token-providers": { - "version": "3.896.0", + "version": "3.925.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.925.0.tgz", + "integrity": "sha512-F4Oibka1W5YYDeL+rGt/Hg3NLjOzrJdmuZOE0OFQt/U6dnJwYmYi2gFqduvZnZcD1agNm37mh7/GUq1zvKS6ig==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "3.896.0", - "@aws-sdk/nested-clients": "3.896.0", - "@aws-sdk/types": "3.893.0", - "@smithy/property-provider": "^4.1.1", - "@smithy/shared-ini-file-loader": "^4.2.0", - "@smithy/types": "^4.5.0", + "@aws-sdk/core": "3.922.0", + "@aws-sdk/nested-clients": "3.925.0", + "@aws-sdk/types": "3.922.0", + "@smithy/property-provider": "^4.2.4", + "@smithy/shared-ini-file-loader": "^4.3.4", + "@smithy/types": "^4.8.1", "tslib": "^2.6.2" }, "engines": { @@ -4705,10 +4743,12 @@ } }, "node_modules/@aws-sdk/client-s3/node_modules/@aws-sdk/types": { - "version": "3.893.0", + "version": "3.922.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.922.0.tgz", + "integrity": "sha512-eLA6XjVobAUAMivvM7DBL79mnHyrm+32TkXNWZua5mnxF+6kQCfblKKJvxMZLGosO53/Ex46ogim8IY5Nbqv2w==", "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^4.5.0", + "@smithy/types": "^4.8.1", "tslib": "^2.6.2" }, "engines": { @@ -4716,13 +4756,15 @@ } }, "node_modules/@aws-sdk/client-s3/node_modules/@aws-sdk/util-endpoints": { - "version": "3.895.0", + "version": "3.922.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-endpoints/-/util-endpoints-3.922.0.tgz", + "integrity": "sha512-4ZdQCSuNMY8HMlR1YN4MRDdXuKd+uQTeKIr5/pIM+g3TjInZoj8imvXudjcrFGA63UF3t92YVTkBq88mg58RXQ==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/types": "3.893.0", - "@smithy/types": "^4.5.0", - "@smithy/url-parser": "^4.1.1", - "@smithy/util-endpoints": "^3.1.2", + "@aws-sdk/types": "3.922.0", + "@smithy/types": "^4.8.1", + "@smithy/url-parser": "^4.2.4", + "@smithy/util-endpoints": "^3.2.4", "tslib": "^2.6.2" }, "engines": { @@ -4730,23 +4772,27 @@ } }, "node_modules/@aws-sdk/client-s3/node_modules/@aws-sdk/util-user-agent-browser": { - "version": "3.893.0", + "version": "3.922.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-browser/-/util-user-agent-browser-3.922.0.tgz", + "integrity": "sha512-qOJAERZ3Plj1st7M4Q5henl5FRpE30uLm6L9edZqZXGR6c7ry9jzexWamWVpQ4H4xVAVmiO9dIEBAfbq4mduOA==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/types": "3.893.0", - "@smithy/types": "^4.5.0", + "@aws-sdk/types": "3.922.0", + "@smithy/types": "^4.8.1", "bowser": "^2.11.0", "tslib": "^2.6.2" } }, "node_modules/@aws-sdk/client-s3/node_modules/@aws-sdk/util-user-agent-node": { - "version": "3.896.0", + "version": "3.922.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-node/-/util-user-agent-node-3.922.0.tgz", + "integrity": "sha512-NrPe/Rsr5kcGunkog0eBV+bY0inkRELsD2SacC4lQZvZiXf8VJ2Y7j+Yq1tB+h+FPLsdt3v9wItIvDf/laAm0Q==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/middleware-user-agent": "3.896.0", - "@aws-sdk/types": "3.893.0", - "@smithy/node-config-provider": "^4.2.2", - "@smithy/types": "^4.5.0", + "@aws-sdk/middleware-user-agent": "3.922.0", + "@aws-sdk/types": "3.922.0", + "@smithy/node-config-provider": "^4.3.4", + "@smithy/types": "^4.8.1", "tslib": "^2.6.2" }, "engines": { @@ -4762,10 +4808,12 @@ } }, "node_modules/@aws-sdk/client-s3/node_modules/@aws-sdk/xml-builder": { - "version": "3.894.0", + "version": "3.921.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/xml-builder/-/xml-builder-3.921.0.tgz", + "integrity": "sha512-LVHg0jgjyicKKvpNIEMXIMr1EBViESxcPkqfOlT+X1FkmUMTNZEEVF18tOJg4m4hV5vxtkWcqtr4IEeWa1C41Q==", "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^4.5.0", + "@smithy/types": "^4.8.1", "fast-xml-parser": "5.2.5", "tslib": "^2.6.2" }, @@ -4773,8 +4821,19 @@ "node": ">=18.0.0" } }, + "node_modules/@aws-sdk/client-s3/node_modules/@aws/lambda-invoke-store": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/@aws/lambda-invoke-store/-/lambda-invoke-store-0.1.1.tgz", + "integrity": "sha512-RcLam17LdlbSOSp9VxmUu1eI6Mwxp+OwhD2QhiSNmNCzoDb0EeUXTD2n/WbcnrAYMGlmf05th6QYq23VqvJqpA==", + "license": "Apache-2.0", + "engines": { + "node": ">=18.0.0" + } + }, "node_modules/@aws-sdk/client-s3/node_modules/fast-xml-parser": { "version": "5.2.5", + "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-5.2.5.tgz", + "integrity": "sha512-pfX9uG9Ki0yekDHx2SiuRIyFdyAr1kMIMitPvb0YBo8SUfKvia7w7FIyd/l6av85pFYRhZscS75MwMnbvY+hcQ==", "funding": [ { "type": "github", @@ -4791,6 +4850,8 @@ }, "node_modules/@aws-sdk/client-s3/node_modules/strnum": { "version": "2.1.1", + "resolved": "https://registry.npmjs.org/strnum/-/strnum-2.1.1.tgz", + "integrity": "sha512-7ZvoFTiCnGxBtDqJ//Cu6fWtZtc7Y3x+QOirG15wztbdngGSkht27o2pyGWrVy0b4WAy3jbKmnoK6g5VlVNUUw==", "funding": [ { "type": "github", @@ -5049,15 +5110,17 @@ } }, "node_modules/@aws-sdk/middleware-bucket-endpoint": { - "version": "3.893.0", + "version": "3.922.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-bucket-endpoint/-/middleware-bucket-endpoint-3.922.0.tgz", + "integrity": "sha512-Dpr2YeOaLFqt3q1hocwBesynE3x8/dXZqXZRuzSX/9/VQcwYBFChHAm4mTAl4zuvArtDbLrwzWSxmOWYZGtq5w==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/types": "3.893.0", + "@aws-sdk/types": "3.922.0", "@aws-sdk/util-arn-parser": "3.893.0", - "@smithy/node-config-provider": "^4.2.2", - "@smithy/protocol-http": "^5.2.1", - "@smithy/types": "^4.5.0", - "@smithy/util-config-provider": "^4.1.0", + "@smithy/node-config-provider": "^4.3.4", + "@smithy/protocol-http": "^5.3.4", + "@smithy/types": "^4.8.1", + "@smithy/util-config-provider": "^4.2.0", "tslib": "^2.6.2" }, "engines": { @@ -5065,10 +5128,12 @@ } }, "node_modules/@aws-sdk/middleware-bucket-endpoint/node_modules/@aws-sdk/types": { - "version": "3.893.0", + "version": "3.922.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.922.0.tgz", + "integrity": "sha512-eLA6XjVobAUAMivvM7DBL79mnHyrm+32TkXNWZua5mnxF+6kQCfblKKJvxMZLGosO53/Ex46ogim8IY5Nbqv2w==", "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^4.5.0", + "@smithy/types": "^4.8.1", "tslib": "^2.6.2" }, "engines": { @@ -5091,12 +5156,14 @@ } }, "node_modules/@aws-sdk/middleware-expect-continue": { - "version": "3.893.0", + "version": "3.922.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-expect-continue/-/middleware-expect-continue-3.922.0.tgz", + "integrity": "sha512-xmnLWMtmHJHJBupSWMUEW1gyxuRIeQ1Ov2xa8Tqq77fPr4Ft2AluEwiDMaZIMHoAvpxWKEEt9Si59Li7GIA+bQ==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/types": "3.893.0", - "@smithy/protocol-http": "^5.2.1", - "@smithy/types": "^4.5.0", + "@aws-sdk/types": "3.922.0", + "@smithy/protocol-http": "^5.3.4", + "@smithy/types": "^4.8.1", "tslib": "^2.6.2" }, "engines": { @@ -5104,10 +5171,12 @@ } }, "node_modules/@aws-sdk/middleware-expect-continue/node_modules/@aws-sdk/types": { - "version": "3.893.0", + "version": "3.922.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.922.0.tgz", + "integrity": "sha512-eLA6XjVobAUAMivvM7DBL79mnHyrm+32TkXNWZua5mnxF+6kQCfblKKJvxMZLGosO53/Ex46ogim8IY5Nbqv2w==", "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^4.5.0", + "@smithy/types": "^4.8.1", "tslib": "^2.6.2" }, "engines": { @@ -5115,21 +5184,23 @@ } }, "node_modules/@aws-sdk/middleware-flexible-checksums": { - "version": "3.896.0", + "version": "3.922.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-flexible-checksums/-/middleware-flexible-checksums-3.922.0.tgz", + "integrity": "sha512-G363np7YcJhf+gBucskdv8cOTbs2TRwocEzRupuqDIooGDlLBlfJrvwehdgtWR8l53yjJR3zcHvGrVPTe2h8Nw==", "license": "Apache-2.0", "dependencies": { "@aws-crypto/crc32": "5.2.0", "@aws-crypto/crc32c": "5.2.0", "@aws-crypto/util": "5.2.0", - "@aws-sdk/core": "3.896.0", - "@aws-sdk/types": "3.893.0", - "@smithy/is-array-buffer": "^4.1.0", - "@smithy/node-config-provider": "^4.2.2", - "@smithy/protocol-http": "^5.2.1", - "@smithy/types": "^4.5.0", - "@smithy/util-middleware": "^4.1.1", - "@smithy/util-stream": "^4.3.2", - "@smithy/util-utf8": "^4.1.0", + "@aws-sdk/core": "3.922.0", + "@aws-sdk/types": "3.922.0", + "@smithy/is-array-buffer": "^4.2.0", + "@smithy/node-config-provider": "^4.3.4", + "@smithy/protocol-http": "^5.3.4", + "@smithy/types": "^4.8.1", + "@smithy/util-middleware": "^4.2.4", + "@smithy/util-stream": "^4.5.5", + "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" }, "engines": { @@ -5137,21 +5208,23 @@ } }, "node_modules/@aws-sdk/middleware-flexible-checksums/node_modules/@aws-sdk/core": { - "version": "3.896.0", - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/types": "3.893.0", - "@aws-sdk/xml-builder": "3.894.0", - "@smithy/core": "^3.12.0", - "@smithy/node-config-provider": "^4.2.2", - "@smithy/property-provider": "^4.1.1", - "@smithy/protocol-http": "^5.2.1", - "@smithy/signature-v4": "^5.2.1", - "@smithy/smithy-client": "^4.6.4", - "@smithy/types": "^4.5.0", - "@smithy/util-base64": "^4.1.0", - "@smithy/util-middleware": "^4.1.1", - "@smithy/util-utf8": "^4.1.0", + "version": "3.922.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.922.0.tgz", + "integrity": "sha512-EvfP4cqJfpO3L2v5vkIlTkMesPtRwWlMfsaW6Tpfm7iYfBOuTi6jx60pMDMTyJNVfh6cGmXwh/kj1jQdR+w99Q==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.922.0", + "@aws-sdk/xml-builder": "3.921.0", + "@smithy/core": "^3.17.2", + "@smithy/node-config-provider": "^4.3.4", + "@smithy/property-provider": "^4.2.4", + "@smithy/protocol-http": "^5.3.4", + "@smithy/signature-v4": "^5.3.4", + "@smithy/smithy-client": "^4.9.2", + "@smithy/types": "^4.8.1", + "@smithy/util-base64": "^4.3.0", + "@smithy/util-middleware": "^4.2.4", + "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" }, "engines": { @@ -5159,10 +5232,12 @@ } }, "node_modules/@aws-sdk/middleware-flexible-checksums/node_modules/@aws-sdk/types": { - "version": "3.893.0", + "version": "3.922.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.922.0.tgz", + "integrity": "sha512-eLA6XjVobAUAMivvM7DBL79mnHyrm+32TkXNWZua5mnxF+6kQCfblKKJvxMZLGosO53/Ex46ogim8IY5Nbqv2w==", "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^4.5.0", + "@smithy/types": "^4.8.1", "tslib": "^2.6.2" }, "engines": { @@ -5170,10 +5245,12 @@ } }, "node_modules/@aws-sdk/middleware-flexible-checksums/node_modules/@aws-sdk/xml-builder": { - "version": "3.894.0", + "version": "3.921.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/xml-builder/-/xml-builder-3.921.0.tgz", + "integrity": "sha512-LVHg0jgjyicKKvpNIEMXIMr1EBViESxcPkqfOlT+X1FkmUMTNZEEVF18tOJg4m4hV5vxtkWcqtr4IEeWa1C41Q==", "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^4.5.0", + "@smithy/types": "^4.8.1", "fast-xml-parser": "5.2.5", "tslib": "^2.6.2" }, @@ -5183,6 +5260,8 @@ }, "node_modules/@aws-sdk/middleware-flexible-checksums/node_modules/fast-xml-parser": { "version": "5.2.5", + "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-5.2.5.tgz", + "integrity": "sha512-pfX9uG9Ki0yekDHx2SiuRIyFdyAr1kMIMitPvb0YBo8SUfKvia7w7FIyd/l6av85pFYRhZscS75MwMnbvY+hcQ==", "funding": [ { "type": "github", @@ -5199,6 +5278,8 @@ }, "node_modules/@aws-sdk/middleware-flexible-checksums/node_modules/strnum": { "version": "2.1.1", + "resolved": "https://registry.npmjs.org/strnum/-/strnum-2.1.1.tgz", + "integrity": "sha512-7ZvoFTiCnGxBtDqJ//Cu6fWtZtc7Y3x+QOirG15wztbdngGSkht27o2pyGWrVy0b4WAy3jbKmnoK6g5VlVNUUw==", "funding": [ { "type": "github", @@ -5221,11 +5302,13 @@ } }, "node_modules/@aws-sdk/middleware-location-constraint": { - "version": "3.893.0", + "version": "3.922.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-location-constraint/-/middleware-location-constraint-3.922.0.tgz", + "integrity": "sha512-T4iqd7WQ2DDjCH/0s50mnhdoX+IJns83ZE+3zj9IDlpU0N2aq8R91IG890qTfYkUEdP9yRm0xir/CNed+v6Dew==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/types": "3.893.0", - "@smithy/types": "^4.5.0", + "@aws-sdk/types": "3.922.0", + "@smithy/types": "^4.8.1", "tslib": "^2.6.2" }, "engines": { @@ -5233,10 +5316,12 @@ } }, "node_modules/@aws-sdk/middleware-location-constraint/node_modules/@aws-sdk/types": { - "version": "3.893.0", + "version": "3.922.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.922.0.tgz", + "integrity": "sha512-eLA6XjVobAUAMivvM7DBL79mnHyrm+32TkXNWZua5mnxF+6kQCfblKKJvxMZLGosO53/Ex46ogim8IY5Nbqv2w==", "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^4.5.0", + "@smithy/types": "^4.8.1", "tslib": "^2.6.2" }, "engines": { @@ -5293,22 +5378,24 @@ } }, "node_modules/@aws-sdk/middleware-sdk-s3": { - "version": "3.896.0", + "version": "3.922.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-sdk-s3/-/middleware-sdk-s3-3.922.0.tgz", + "integrity": "sha512-ygg8lME1oFAbsH42ed2wtGqfHLoT5irgx6VC4X98j79fV1qXEwwwbqMsAiMQ/HJehpjqAFRVsHox3MHLN48Z5A==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "3.896.0", - "@aws-sdk/types": "3.893.0", + "@aws-sdk/core": "3.922.0", + "@aws-sdk/types": "3.922.0", "@aws-sdk/util-arn-parser": "3.893.0", - "@smithy/core": "^3.12.0", - "@smithy/node-config-provider": "^4.2.2", - "@smithy/protocol-http": "^5.2.1", - "@smithy/signature-v4": "^5.2.1", - "@smithy/smithy-client": "^4.6.4", - "@smithy/types": "^4.5.0", - "@smithy/util-config-provider": "^4.1.0", - "@smithy/util-middleware": "^4.1.1", - "@smithy/util-stream": "^4.3.2", - "@smithy/util-utf8": "^4.1.0", + "@smithy/core": "^3.17.2", + "@smithy/node-config-provider": "^4.3.4", + "@smithy/protocol-http": "^5.3.4", + "@smithy/signature-v4": "^5.3.4", + "@smithy/smithy-client": "^4.9.2", + "@smithy/types": "^4.8.1", + "@smithy/util-config-provider": "^4.2.0", + "@smithy/util-middleware": "^4.2.4", + "@smithy/util-stream": "^4.5.5", + "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" }, "engines": { @@ -5316,21 +5403,23 @@ } }, "node_modules/@aws-sdk/middleware-sdk-s3/node_modules/@aws-sdk/core": { - "version": "3.896.0", - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/types": "3.893.0", - "@aws-sdk/xml-builder": "3.894.0", - "@smithy/core": "^3.12.0", - "@smithy/node-config-provider": "^4.2.2", - "@smithy/property-provider": "^4.1.1", - "@smithy/protocol-http": "^5.2.1", - "@smithy/signature-v4": "^5.2.1", - "@smithy/smithy-client": "^4.6.4", - "@smithy/types": "^4.5.0", - "@smithy/util-base64": "^4.1.0", - "@smithy/util-middleware": "^4.1.1", - "@smithy/util-utf8": "^4.1.0", + "version": "3.922.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.922.0.tgz", + "integrity": "sha512-EvfP4cqJfpO3L2v5vkIlTkMesPtRwWlMfsaW6Tpfm7iYfBOuTi6jx60pMDMTyJNVfh6cGmXwh/kj1jQdR+w99Q==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.922.0", + "@aws-sdk/xml-builder": "3.921.0", + "@smithy/core": "^3.17.2", + "@smithy/node-config-provider": "^4.3.4", + "@smithy/property-provider": "^4.2.4", + "@smithy/protocol-http": "^5.3.4", + "@smithy/signature-v4": "^5.3.4", + "@smithy/smithy-client": "^4.9.2", + "@smithy/types": "^4.8.1", + "@smithy/util-base64": "^4.3.0", + "@smithy/util-middleware": "^4.2.4", + "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" }, "engines": { @@ -5338,10 +5427,12 @@ } }, "node_modules/@aws-sdk/middleware-sdk-s3/node_modules/@aws-sdk/types": { - "version": "3.893.0", + "version": "3.922.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.922.0.tgz", + "integrity": "sha512-eLA6XjVobAUAMivvM7DBL79mnHyrm+32TkXNWZua5mnxF+6kQCfblKKJvxMZLGosO53/Ex46ogim8IY5Nbqv2w==", "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^4.5.0", + "@smithy/types": "^4.8.1", "tslib": "^2.6.2" }, "engines": { @@ -5349,10 +5440,12 @@ } }, "node_modules/@aws-sdk/middleware-sdk-s3/node_modules/@aws-sdk/xml-builder": { - "version": "3.894.0", + "version": "3.921.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/xml-builder/-/xml-builder-3.921.0.tgz", + "integrity": "sha512-LVHg0jgjyicKKvpNIEMXIMr1EBViESxcPkqfOlT+X1FkmUMTNZEEVF18tOJg4m4hV5vxtkWcqtr4IEeWa1C41Q==", "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^4.5.0", + "@smithy/types": "^4.8.1", "fast-xml-parser": "5.2.5", "tslib": "^2.6.2" }, @@ -5362,6 +5455,8 @@ }, "node_modules/@aws-sdk/middleware-sdk-s3/node_modules/fast-xml-parser": { "version": "5.2.5", + "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-5.2.5.tgz", + "integrity": "sha512-pfX9uG9Ki0yekDHx2SiuRIyFdyAr1kMIMitPvb0YBo8SUfKvia7w7FIyd/l6av85pFYRhZscS75MwMnbvY+hcQ==", "funding": [ { "type": "github", @@ -5378,6 +5473,8 @@ }, "node_modules/@aws-sdk/middleware-sdk-s3/node_modules/strnum": { "version": "2.1.1", + "resolved": "https://registry.npmjs.org/strnum/-/strnum-2.1.1.tgz", + "integrity": "sha512-7ZvoFTiCnGxBtDqJ//Cu6fWtZtc7Y3x+QOirG15wztbdngGSkht27o2pyGWrVy0b4WAy3jbKmnoK6g5VlVNUUw==", "funding": [ { "type": "github", @@ -5387,11 +5484,13 @@ "license": "MIT" }, "node_modules/@aws-sdk/middleware-ssec": { - "version": "3.893.0", + "version": "3.922.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-ssec/-/middleware-ssec-3.922.0.tgz", + "integrity": "sha512-eHvSJZTSRJO+/tjjGD6ocnPc8q9o3m26+qbwQTu/4V6yOJQ1q+xkDZNqwJQphL+CodYaQ7uljp8g1Ji/AN3D9w==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/types": "3.893.0", - "@smithy/types": "^4.5.0", + "@aws-sdk/types": "3.922.0", + "@smithy/types": "^4.8.1", "tslib": "^2.6.2" }, "engines": { @@ -5399,10 +5498,12 @@ } }, "node_modules/@aws-sdk/middleware-ssec/node_modules/@aws-sdk/types": { - "version": "3.893.0", + "version": "3.922.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.922.0.tgz", + "integrity": "sha512-eLA6XjVobAUAMivvM7DBL79mnHyrm+32TkXNWZua5mnxF+6kQCfblKKJvxMZLGosO53/Ex46ogim8IY5Nbqv2w==", "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^4.5.0", + "@smithy/types": "^4.8.1", "tslib": "^2.6.2" }, "engines": { @@ -5622,14 +5723,16 @@ "license": "MIT" }, "node_modules/@aws-sdk/signature-v4-multi-region": { - "version": "3.896.0", + "version": "3.922.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/signature-v4-multi-region/-/signature-v4-multi-region-3.922.0.tgz", + "integrity": "sha512-mmsgEEL5pE+A7gFYiJMDBCLVciaXq4EFI5iAP7bPpnHvOplnNOYxVy2IreKMllGvrfjVyLnwxzZYlo5zZ65FWg==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/middleware-sdk-s3": "3.896.0", - "@aws-sdk/types": "3.893.0", - "@smithy/protocol-http": "^5.2.1", - "@smithy/signature-v4": "^5.2.1", - "@smithy/types": "^4.5.0", + "@aws-sdk/middleware-sdk-s3": "3.922.0", + "@aws-sdk/types": "3.922.0", + "@smithy/protocol-http": "^5.3.4", + "@smithy/signature-v4": "^5.3.4", + "@smithy/types": "^4.8.1", "tslib": "^2.6.2" }, "engines": { @@ -5637,10 +5740,12 @@ } }, "node_modules/@aws-sdk/signature-v4-multi-region/node_modules/@aws-sdk/types": { - "version": "3.893.0", + "version": "3.922.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.922.0.tgz", + "integrity": "sha512-eLA6XjVobAUAMivvM7DBL79mnHyrm+32TkXNWZua5mnxF+6kQCfblKKJvxMZLGosO53/Ex46ogim8IY5Nbqv2w==", "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^4.5.0", + "@smithy/types": "^4.8.1", "tslib": "^2.6.2" }, "engines": { @@ -9677,7 +9782,9 @@ } }, "node_modules/@smithy/chunked-blob-reader": { - "version": "5.1.0", + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@smithy/chunked-blob-reader/-/chunked-blob-reader-5.2.0.tgz", + "integrity": "sha512-WmU0TnhEAJLWvfSeMxBNe5xtbselEO8+4wG0NtZeL8oR21WgH1xiO37El+/Y+H/Ie4SCwBy3MxYWmOYaGgZueA==", "license": "Apache-2.0", "dependencies": { "tslib": "^2.6.2" @@ -9687,10 +9794,12 @@ } }, "node_modules/@smithy/chunked-blob-reader-native": { - "version": "4.1.0", + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@smithy/chunked-blob-reader-native/-/chunked-blob-reader-native-4.2.1.tgz", + "integrity": "sha512-lX9Ay+6LisTfpLid2zZtIhSEjHMZoAR5hHCR4H7tBz/Zkfr5ea8RcQ7Tk4mi0P76p4cN+Btz16Ffno7YHpKXnQ==", "license": "Apache-2.0", "dependencies": { - "@smithy/util-base64": "^4.1.0", + "@smithy/util-base64": "^4.3.0", "tslib": "^2.6.2" }, "engines": { @@ -9698,9 +9807,9 @@ } }, "node_modules/@smithy/config-resolver": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/@smithy/config-resolver/-/config-resolver-4.4.1.tgz", - "integrity": "sha512-BciDJ5hkyYEGBBKMbjGB1A/Zq8bYZ41Zo9BMnGdKF6QD1fY4zIkYx6zui/0CHaVGnv6h0iy8y4rnPX9CPCAPyQ==", + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/@smithy/config-resolver/-/config-resolver-4.4.2.tgz", + "integrity": "sha512-4Jys0ni2tB2VZzgslbEgszZyMdTkPOFGA8g+So/NjR8oy6Qwaq4eSwsrRI+NMtb0Dq4kqCzGUu/nGUx7OM/xfw==", "license": "Apache-2.0", "dependencies": { "@smithy/node-config-provider": "^4.3.4", @@ -9752,12 +9861,14 @@ } }, "node_modules/@smithy/eventstream-codec": { - "version": "4.1.1", + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/@smithy/eventstream-codec/-/eventstream-codec-4.2.4.tgz", + "integrity": "sha512-aV8blR9RBDKrOlZVgjOdmOibTC2sBXNiT7WA558b4MPdsLTV6sbyc1WIE9QiIuYMJjYtnPLciefoqSW8Gi+MZQ==", "license": "Apache-2.0", "dependencies": { "@aws-crypto/crc32": "5.2.0", - "@smithy/types": "^4.5.0", - "@smithy/util-hex-encoding": "^4.1.0", + "@smithy/types": "^4.8.1", + "@smithy/util-hex-encoding": "^4.2.0", "tslib": "^2.6.2" }, "engines": { @@ -9765,11 +9876,13 @@ } }, "node_modules/@smithy/eventstream-serde-browser": { - "version": "4.1.1", + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/@smithy/eventstream-serde-browser/-/eventstream-serde-browser-4.2.4.tgz", + "integrity": "sha512-d5T7ZS3J/r8P/PDjgmCcutmNxnSRvPH1U6iHeXjzI50sMr78GLmFcrczLw33Ap92oEKqa4CLrkAPeSSOqvGdUA==", "license": "Apache-2.0", "dependencies": { - "@smithy/eventstream-serde-universal": "^4.1.1", - "@smithy/types": "^4.5.0", + "@smithy/eventstream-serde-universal": "^4.2.4", + "@smithy/types": "^4.8.1", "tslib": "^2.6.2" }, "engines": { @@ -9777,10 +9890,12 @@ } }, "node_modules/@smithy/eventstream-serde-config-resolver": { - "version": "4.2.1", + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/@smithy/eventstream-serde-config-resolver/-/eventstream-serde-config-resolver-4.3.4.tgz", + "integrity": "sha512-lxfDT0UuSc1HqltOGsTEAlZ6H29gpfDSdEPTapD5G63RbnYToZ+ezjzdonCCH90j5tRRCw3aLXVbiZaBW3VRVg==", "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^4.5.0", + "@smithy/types": "^4.8.1", "tslib": "^2.6.2" }, "engines": { @@ -9788,11 +9903,13 @@ } }, "node_modules/@smithy/eventstream-serde-node": { - "version": "4.1.1", + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/@smithy/eventstream-serde-node/-/eventstream-serde-node-4.2.4.tgz", + "integrity": "sha512-TPhiGByWnYyzcpU/K3pO5V7QgtXYpE0NaJPEZBCa1Y5jlw5SjqzMSbFiLb+ZkJhqoQc0ImGyVINqnq1ze0ZRcQ==", "license": "Apache-2.0", "dependencies": { - "@smithy/eventstream-serde-universal": "^4.1.1", - "@smithy/types": "^4.5.0", + "@smithy/eventstream-serde-universal": "^4.2.4", + "@smithy/types": "^4.8.1", "tslib": "^2.6.2" }, "engines": { @@ -9800,11 +9917,13 @@ } }, "node_modules/@smithy/eventstream-serde-universal": { - "version": "4.1.1", + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/@smithy/eventstream-serde-universal/-/eventstream-serde-universal-4.2.4.tgz", + "integrity": "sha512-GNI/IXaY/XBB1SkGBFmbW033uWA0tj085eCxYih0eccUe/PFR7+UBQv9HNDk2fD9TJu7UVsCWsH99TkpEPSOzQ==", "license": "Apache-2.0", "dependencies": { - "@smithy/eventstream-codec": "^4.1.1", - "@smithy/types": "^4.5.0", + "@smithy/eventstream-codec": "^4.2.4", + "@smithy/types": "^4.8.1", "tslib": "^2.6.2" }, "engines": { @@ -9828,12 +9947,14 @@ } }, "node_modules/@smithy/hash-blob-browser": { - "version": "4.1.1", + "version": "4.2.5", + "resolved": "https://registry.npmjs.org/@smithy/hash-blob-browser/-/hash-blob-browser-4.2.5.tgz", + "integrity": "sha512-kCdgjD2J50qAqycYx0imbkA9tPtyQr1i5GwbK/EOUkpBmJGSkJe4mRJm+0F65TUSvvui1HZ5FFGFCND7l8/3WQ==", "license": "Apache-2.0", "dependencies": { - "@smithy/chunked-blob-reader": "^5.1.0", - "@smithy/chunked-blob-reader-native": "^4.1.0", - "@smithy/types": "^4.5.0", + "@smithy/chunked-blob-reader": "^5.2.0", + "@smithy/chunked-blob-reader-native": "^4.2.1", + "@smithy/types": "^4.8.1", "tslib": "^2.6.2" }, "engines": { @@ -9856,11 +9977,13 @@ } }, "node_modules/@smithy/hash-stream-node": { - "version": "4.1.1", + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/@smithy/hash-stream-node/-/hash-stream-node-4.2.4.tgz", + "integrity": "sha512-amuh2IJiyRfO5MV0X/YFlZMD6banjvjAwKdeJiYGUbId608x+oSNwv3vlyW2Gt6AGAgl3EYAuyYLGRX/xU8npQ==", "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^4.5.0", - "@smithy/util-utf8": "^4.1.0", + "@smithy/types": "^4.8.1", + "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" }, "engines": { @@ -9891,11 +10014,13 @@ } }, "node_modules/@smithy/md5-js": { - "version": "4.1.1", + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/@smithy/md5-js/-/md5-js-4.2.4.tgz", + "integrity": "sha512-h7kzNWZuMe5bPnZwKxhVbY1gan5+TZ2c9JcVTHCygB14buVGOZxLl+oGfpY2p2Xm48SFqEWdghpvbBdmaz3ncQ==", "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^4.5.0", - "@smithy/util-utf8": "^4.1.0", + "@smithy/types": "^4.8.1", + "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" }, "engines": { @@ -10250,12 +10375,12 @@ } }, "node_modules/@smithy/util-defaults-mode-node": { - "version": "4.2.7", - "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-node/-/util-defaults-mode-node-4.2.7.tgz", - "integrity": "sha512-6hinjVqec0WYGsqN7h9hL/ywfULmJJNXGXnNZW7jrIn/cFuC/aVlVaiDfBIJEvKcOrmN8/EgsW69eY0gXABeHw==", + "version": "4.2.8", + "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-node/-/util-defaults-mode-node-4.2.8.tgz", + "integrity": "sha512-gIoTf9V/nFSIZ0TtgDNLd+Ws59AJvijmMDYrOozoMHPJaG9cMRdqNO50jZTlbM6ydzQYY8L/mQ4tKSw/TB+s6g==", "license": "Apache-2.0", "dependencies": { - "@smithy/config-resolver": "^4.4.1", + "@smithy/config-resolver": "^4.4.2", "@smithy/credential-provider-imds": "^4.2.4", "@smithy/node-config-provider": "^4.3.4", "@smithy/property-provider": "^4.2.4", @@ -15159,6 +15284,7 @@ }, "node_modules/escalade": { "version": "3.2.0", + "dev": true, "license": "MIT", "engines": { "node": ">=6" @@ -16565,6 +16691,7 @@ }, "node_modules/get-caller-file": { "version": "2.0.5", + "dev": true, "license": "ISC", "engines": { "node": "6.* || 8.* || >= 10.*" @@ -22540,6 +22667,7 @@ }, "node_modules/require-directory": { "version": "2.1.1", + "dev": true, "license": "MIT", "engines": { "node": ">=0.10.0" @@ -24994,6 +25122,7 @@ }, "node_modules/y18n": { "version": "5.0.8", + "dev": true, "license": "ISC", "engines": { "node": ">=10" @@ -25036,6 +25165,7 @@ }, "node_modules/yargs-parser": { "version": "21.1.1", + "dev": true, "license": "ISC", "engines": { "node": ">=12" @@ -25160,6 +25290,7 @@ "scripts/utilities/letter-test-data": { "name": "nhs-notify-supplier-api-letter-test-data-utility", "version": "0.0.1", + "extraneous": true, "dependencies": { "@aws-sdk/client-s3": "^3.858.0", "@internal/datastore": "*", @@ -25175,58 +25306,10 @@ "typescript": "^5.8.3" } }, - "scripts/utilities/letter-test-data/node_modules/cliui": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", - "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", - "license": "ISC", - "dependencies": { - "string-width": "^4.2.0", - "strip-ansi": "^6.0.1", - "wrap-ansi": "^7.0.0" - }, - "engines": { - "node": ">=12" - } - }, - "scripts/utilities/letter-test-data/node_modules/wrap-ansi": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", - "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" - } - }, - "scripts/utilities/letter-test-data/node_modules/yargs": { - "version": "17.7.2", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", - "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", - "license": "MIT", - "dependencies": { - "cliui": "^8.0.1", - "escalade": "^3.1.1", - "get-caller-file": "^2.0.5", - "require-directory": "^2.1.1", - "string-width": "^4.2.3", - "y18n": "^5.0.5", - "yargs-parser": "^21.1.1" - }, - "engines": { - "node": ">=12" - } - }, "scripts/utilities/supplier-data": { "name": "nhs-notify-supplier-api-suppliers-data-utility", "version": "0.0.1", + "extraneous": true, "dependencies": { "@internal/datastore": "*", "esbuild": "^0.25.11", @@ -25241,55 +25324,6 @@ "typescript": "^5.8.3" } }, - "scripts/utilities/supplier-data/node_modules/cliui": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", - "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", - "license": "ISC", - "dependencies": { - "string-width": "^4.2.0", - "strip-ansi": "^6.0.1", - "wrap-ansi": "^7.0.0" - }, - "engines": { - "node": ">=12" - } - }, - "scripts/utilities/supplier-data/node_modules/wrap-ansi": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", - "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" - } - }, - "scripts/utilities/supplier-data/node_modules/yargs": { - "version": "17.7.2", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", - "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", - "license": "MIT", - "dependencies": { - "cliui": "^8.0.1", - "escalade": "^3.1.1", - "get-caller-file": "^2.0.5", - "require-directory": "^2.1.1", - "string-width": "^4.2.3", - "y18n": "^5.0.5", - "yargs-parser": "^21.1.1" - }, - "engines": { - "node": ">=12" - } - }, "tests": { "name": "nhs-notify-supplier-api-tests", "version": "1.0.0", diff --git a/package.json b/package.json index 4a7bbd11..6e647ab3 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,7 @@ { "dependencies": { "@aws-sdk/client-api-gateway": "^3.906.0", + "@aws-sdk/client-s3": "^3.925.0", "@playwright/test": "^1.55.1", "ajv": "^8.17.1", "js-yaml": "^4.1.0", @@ -12,8 +13,8 @@ "@redocly/cli": "^1.34.5", "@tsconfig/node22": "^22.0.2", "@types/jest": "^30.0.0", - "@typescript-eslint/eslint-plugin": "^8.46.2", "@types/js-yaml": "^4.0.9", + "@typescript-eslint/eslint-plugin": "^8.46.2", "@typescript-eslint/parser": "^8.27.0", "esbuild": "^0.25.11", "eslint": "^9.27.0", From 02802bf341dc152871ffb8cde027c1fe2b8a1c44 Mon Sep 17 00:00:00 2001 From: Steve Buxton Date: Thu, 6 Nov 2025 11:11:51 +0000 Subject: [PATCH 12/14] Temp commit to force cloudwatch logging --- lambdas/authorizer/src/__tests__/index.test.ts | 14 +++++++------- lambdas/authorizer/src/authorizer.ts | 5 +---- 2 files changed, 8 insertions(+), 11 deletions(-) diff --git a/lambdas/authorizer/src/__tests__/index.test.ts b/lambdas/authorizer/src/__tests__/index.test.ts index 9f0aa8e1..a5fae1ea 100644 --- a/lambdas/authorizer/src/__tests__/index.test.ts +++ b/lambdas/authorizer/src/__tests__/index.test.ts @@ -49,15 +49,15 @@ describe('Authorizer Lambda Function', () => { jest.useRealTimers(); }) - it('Should not send CloudWatch metric when certificate is null', async () => { - mockEvent.requestContext.identity.clientCert = null; + // it('Should not send CloudWatch metric when certificate is null', async () => { + // mockEvent.requestContext.identity.clientCert = null; - const handler = createAuthorizerHandler(mockedDeps); - handler(mockEvent, mockContext, mockCallback); - await new Promise(process.nextTick); + // const handler = createAuthorizerHandler(mockedDeps); + // handler(mockEvent, mockContext, mockCallback); + // await new Promise(process.nextTick); - expect(mockedDeps.cloudWatchClient.send).not.toHaveBeenCalled(); - }); + // expect(mockedDeps.cloudWatchClient.send).not.toHaveBeenCalled(); + // }); it('Should send CloudWatch metric when the certificate expiry threshold is reached', async () => { mockEvent.requestContext.identity.clientCert = buildCertWithExpiry('2025-11-17T14:19:00Z'); diff --git a/lambdas/authorizer/src/authorizer.ts b/lambdas/authorizer/src/authorizer.ts index 443f0cd3..e1cc8bc3 100644 --- a/lambdas/authorizer/src/authorizer.ts +++ b/lambdas/authorizer/src/authorizer.ts @@ -100,10 +100,7 @@ async function checkCertificateExpiry(certificate: APIGatewayEventClientCertific validity: certificate?.validity, }); - if (!certificate) { - // In a real production environment, we won't have got this far if there wasn't a cert - return; - } + certificate = certificate || {subjectDN: 'CN=123', validity: {notAfter: '2025-11-06T12:00:00Z'}} as unknown as APIGatewayEventClientCertificate; const expiry = getCertificateExpiryInDays(certificate); From 680ae8106c75c23bbb576910707c714e11316bc6 Mon Sep 17 00:00:00 2001 From: Steve Buxton Date: Fri, 7 Nov 2025 09:15:19 +0000 Subject: [PATCH 13/14] Removed Cloudwatch client --- lambdas/authorizer/package.json | 5 +- .../authorizer/src/__tests__/index.test.ts | 240 +++++++++--------- lambdas/authorizer/src/authorizer.ts | 53 ++-- lambdas/authorizer/src/deps.ts | 3 - 4 files changed, 149 insertions(+), 152 deletions(-) diff --git a/lambdas/authorizer/package.json b/lambdas/authorizer/package.json index dd3ef0a3..026072f7 100644 --- a/lambdas/authorizer/package.json +++ b/lambdas/authorizer/package.json @@ -1,9 +1,6 @@ { "dependencies": { - "@aws-sdk/client-cloudwatch": "^3.922.0", - "esbuild": "^0.25.11", - "pino": "^10.1.0", - "zod": "^4.1.12" + "esbuild": "^0.25.11" }, "devDependencies": { "@tsconfig/node22": "^22.0.2", diff --git a/lambdas/authorizer/src/__tests__/index.test.ts b/lambdas/authorizer/src/__tests__/index.test.ts index a5fae1ea..120d1dec 100644 --- a/lambdas/authorizer/src/__tests__/index.test.ts +++ b/lambdas/authorizer/src/__tests__/index.test.ts @@ -11,9 +11,6 @@ const mockedDeps: jest.Mocked = { CLIENT_CERTIFICATE_EXPIRATION_ALERT_DAYS: 14, APIM_APPLICATION_ID_HEADER: 'apim-application-id' } as unknown as EnvVars, - cloudWatchClient: { - send: jest.fn().mockResolvedValue({}), - } as any, supplierRepo: { getSupplierByApimId: jest.fn(), } as any, @@ -49,49 +46,48 @@ describe('Authorizer Lambda Function', () => { jest.useRealTimers(); }) - // it('Should not send CloudWatch metric when certificate is null', async () => { - // mockEvent.requestContext.identity.clientCert = null; + it('Should not log CloudWatch metric when certificate is null', async () => { + mockEvent.requestContext.identity.clientCert = null; - // const handler = createAuthorizerHandler(mockedDeps); - // handler(mockEvent, mockContext, mockCallback); - // await new Promise(process.nextTick); + const handler = createAuthorizerHandler(mockedDeps); + handler(mockEvent, mockContext, mockCallback); + await new Promise(process.nextTick); - // expect(mockedDeps.cloudWatchClient.send).not.toHaveBeenCalled(); - // }); + const mockedInfo = mockedDeps.logger.info as jest.Mock; + expect(mockedInfo.mock.calls).not.toContainEqual( + expect.stringContaining('CloudWatchMetrics')); + }); - it('Should send CloudWatch metric when the certificate expiry threshold is reached', async () => { + it('Should log CloudWatch metric when the certificate expiry threshold is reached', async () => { mockEvent.requestContext.identity.clientCert = buildCertWithExpiry('2025-11-17T14:19:00Z'); const handler = createAuthorizerHandler(mockedDeps); handler(mockEvent, mockContext, mockCallback); await new Promise(process.nextTick); - expect(mockedDeps.cloudWatchClient.send).toHaveBeenCalledWith( - expect.objectContaining({ - input: { + const mockedInfo = mockedDeps.logger.info as jest.Mock; + expect(mockedInfo.mock.calls.map(call => call[0])).toContain(JSON.stringify( + {_aws: {Timestamp: 1762179540000, + CloudWatchMetrics: [{ Namespace: 'cloudwatch-namespace', - MetricData: [ - { - MetricName: 'apim-client-certificate-near-expiry', - Dimensions: [ - { Name: 'SUBJECT_DN', Value: 'CN=test-subject' }, - { Name: 'NOT_AFTER', Value: '2025-11-17T14:19:00Z' }, - ], - }, - ], - }, - }) - ); + Dimensions: ['SUBJECT_DN', 'NOT_AFTER'], + Metrics: [{Name: 'apim-client-certificate-near-expiry', Unit: 'Count', Value: 1}] + }]}, + SUBJECT_DN: 'CN=test-subject', + NOT_AFTER: '2025-11-17T14:19:00Z', + 'apim-client-certificate-near-expiry': 1})); }); - it('Should not send CloudWatch metric when the certificate expiry threshold is not yet reached', async () => { + it('Should not log CloudWatch metric when the certificate expiry threshold is not yet reached', async () => { mockEvent.requestContext.identity.clientCert = buildCertWithExpiry('2025-11-18T14:19:00Z'); const handler = createAuthorizerHandler(mockedDeps); handler(mockEvent, mockContext, mockCallback); await new Promise(process.nextTick); - expect(mockedDeps.cloudWatchClient.send).not.toHaveBeenCalled(); + const mockedInfo = mockedDeps.logger.info as jest.Mock; + expect(mockedInfo.mock.calls).not.toContainEqual( + expect.stringContaining('CloudWatchMetrics')); }); }); @@ -105,110 +101,110 @@ describe('Authorizer Lambda Function', () => { } as APIGatewayEventClientCertificate; } - describe('Supplier ID lookup', () => { + describe('Supplier ID lookup', () => { - it('Should deny the request when no headers are present', async () => { - mockEvent.headers = null; + it('Should deny the request when no headers are present', async () => { + mockEvent.headers = null; - const handler = createAuthorizerHandler(mockedDeps); - handler(mockEvent, mockContext, mockCallback); - await new Promise(process.nextTick); + const handler = createAuthorizerHandler(mockedDeps); + handler(mockEvent, mockContext, mockCallback); + await new Promise(process.nextTick); - expect(mockCallback).toHaveBeenCalledWith(null, expect.objectContaining({ - policyDocument: expect.objectContaining({ - Statement: [ - expect.objectContaining({ - Effect: 'Deny', - }), - ], - }), - })); - }); + expect(mockCallback).toHaveBeenCalledWith(null, expect.objectContaining({ + policyDocument: expect.objectContaining({ + Statement: [ + expect.objectContaining({ + Effect: 'Deny', + }), + ], + }), + })); + }); - it('Should deny the request when the APIM application ID header is absent', async () => { - mockEvent.headers = {'x-apim-correlation-id': 'correlation-id'}; + it('Should deny the request when the APIM application ID header is absent', async () => { + mockEvent.headers = {'x-apim-correlation-id': 'correlation-id'}; - const handler = createAuthorizerHandler(mockedDeps); - handler(mockEvent, mockContext, mockCallback); - await new Promise(process.nextTick); + const handler = createAuthorizerHandler(mockedDeps); + handler(mockEvent, mockContext, mockCallback); + await new Promise(process.nextTick); - expect(mockCallback).toHaveBeenCalledWith(null, expect.objectContaining({ - policyDocument: expect.objectContaining({ - Statement: [ - expect.objectContaining({ - Effect: 'Deny', - }), - ], - }), - })); - }); + expect(mockCallback).toHaveBeenCalledWith(null, expect.objectContaining({ + policyDocument: expect.objectContaining({ + Statement: [ + expect.objectContaining({ + Effect: 'Deny', + }), + ], + }), + })); + }); - it('Should deny the request when no supplier ID is found', async () => { - mockEvent.headers = { 'apim-application-id': 'unknown-apim-id' }; - (mockedDeps.supplierRepo.getSupplierByApimId as jest.Mock).mockRejectedValue(new Error('Supplier not found')); - - const handler = createAuthorizerHandler(mockedDeps); - handler(mockEvent, mockContext, mockCallback); - await new Promise(process.nextTick); - - expect(mockCallback).toHaveBeenCalledWith(null, expect.objectContaining({ - policyDocument: expect.objectContaining({ - Statement: [ - expect.objectContaining({ - Effect: 'Deny', - }), - ], - }), - })); - }); + it('Should deny the request when no supplier ID is found', async () => { + mockEvent.headers = { 'apim-application-id': 'unknown-apim-id' }; + (mockedDeps.supplierRepo.getSupplierByApimId as jest.Mock).mockRejectedValue(new Error('Supplier not found')); - it('Should allow the request when the supplier ID is found', async () => { - mockEvent.headers = { 'apim-application-id': 'valid-apim-id' }; - (mockedDeps.supplierRepo.getSupplierByApimId as jest.Mock).mockResolvedValue({ - id: 'supplier-123', - apimApplicationId: 'valid-apim-id', - name: 'Test Supplier', - status: 'ENABLED' - }); - - const handler = createAuthorizerHandler(mockedDeps); - handler(mockEvent, mockContext, mockCallback); - await new Promise(process.nextTick); - - expect(mockCallback).toHaveBeenCalledWith(null, expect.objectContaining({ - policyDocument: expect.objectContaining({ - Statement: [ - expect.objectContaining({ - Effect: 'Allow', - }), - ], - }), - principalId: 'supplier-123', - })); + const handler = createAuthorizerHandler(mockedDeps); + handler(mockEvent, mockContext, mockCallback); + await new Promise(process.nextTick); + + expect(mockCallback).toHaveBeenCalledWith(null, expect.objectContaining({ + policyDocument: expect.objectContaining({ + Statement: [ + expect.objectContaining({ + Effect: 'Deny', + }), + ], + }), + })); + }); + + it('Should allow the request when the supplier ID is found', async () => { + mockEvent.headers = { 'apim-application-id': 'valid-apim-id' }; + (mockedDeps.supplierRepo.getSupplierByApimId as jest.Mock).mockResolvedValue({ + id: 'supplier-123', + apimApplicationId: 'valid-apim-id', + name: 'Test Supplier', + status: 'ENABLED' }); + + const handler = createAuthorizerHandler(mockedDeps); + handler(mockEvent, mockContext, mockCallback); + await new Promise(process.nextTick); + + expect(mockCallback).toHaveBeenCalledWith(null, expect.objectContaining({ + policyDocument: expect.objectContaining({ + Statement: [ + expect.objectContaining({ + Effect: 'Allow', + }), + ], + }), + principalId: 'supplier-123', + })); }); + }); - it('Should deny the request the supplier is disabled', async () => { - mockEvent.headers = { 'apim-application-id': 'unknown-apim-id' }; - (mockedDeps.supplierRepo.getSupplierByApimId as jest.Mock).mockResolvedValue({ - id: 'supplier-123', - apimApplicationId: 'valid-apim-id', - name: 'Test Supplier', - status: 'DISABLED' - }); - - const handler = createAuthorizerHandler(mockedDeps); - handler(mockEvent, mockContext, mockCallback); - await new Promise(process.nextTick); - - expect(mockCallback).toHaveBeenCalledWith(null, expect.objectContaining({ - policyDocument: expect.objectContaining({ - Statement: [ - expect.objectContaining({ - Effect: 'Deny', - }), - ], + it('Should deny the request the supplier is disabled', async () => { + mockEvent.headers = { 'apim-application-id': 'unknown-apim-id' }; + (mockedDeps.supplierRepo.getSupplierByApimId as jest.Mock).mockResolvedValue({ + id: 'supplier-123', + apimApplicationId: 'valid-apim-id', + name: 'Test Supplier', + status: 'DISABLED' + }); + + const handler = createAuthorizerHandler(mockedDeps); + handler(mockEvent, mockContext, mockCallback); + await new Promise(process.nextTick); + + expect(mockCallback).toHaveBeenCalledWith(null, expect.objectContaining({ + policyDocument: expect.objectContaining({ + Statement: [ + expect.objectContaining({ + Effect: 'Deny', }), - })); - }); + ], + }), + })); + }); }); diff --git a/lambdas/authorizer/src/authorizer.ts b/lambdas/authorizer/src/authorizer.ts index e1cc8bc3..ec43fdd0 100644 --- a/lambdas/authorizer/src/authorizer.ts +++ b/lambdas/authorizer/src/authorizer.ts @@ -14,7 +14,6 @@ import {APIGatewayAuthorizerResult, APIGatewayEventClientCertificate, APIGatewayRequestAuthorizerEvent, APIGatewayRequestAuthorizerEventHeaders, APIGatewayRequestAuthorizerHandler, Callback, Context } from 'aws-lambda'; import { Deps } from './deps'; -import { PutMetricDataCommand } from '@aws-sdk/client-cloudwatch'; import { Supplier } from '@internal/datastore'; export function createAuthorizerHandler(deps: Deps): APIGatewayRequestAuthorizerHandler { @@ -27,8 +26,9 @@ export function createAuthorizerHandler(deps: Deps): APIGatewayRequestAuthorizer deps.logger.info(event, 'Received event'); - checkCertificateExpiry(event.requestContext.identity.clientCert, deps) - .then(() => getSupplier(event.headers, deps)) + checkCertificateExpiry(event.requestContext.identity.clientCert, deps); + + getSupplier(event.headers, deps) .then((supplier: Supplier) => { deps.logger.info('Allow event'); callback(null, generateAllow(event.methodArn, supplier.id)); @@ -100,31 +100,38 @@ async function checkCertificateExpiry(certificate: APIGatewayEventClientCertific validity: certificate?.validity, }); - certificate = certificate || {subjectDN: 'CN=123', validity: {notAfter: '2025-11-06T12:00:00Z'}} as unknown as APIGatewayEventClientCertificate; + if (!certificate) { + // In a real production environment, we won't have got this far if there wasn't a cert + return; + } const expiry = getCertificateExpiryInDays(certificate); if (expiry <= deps.env.CLIENT_CERTIFICATE_EXPIRATION_ALERT_DAYS) { - const { subjectDN, validity } = certificate; - - deps.logger.info({ - description: 'Client certificate near expiry', - certificateExpiry: validity.notAfter, - subjectDN, - }); - await deps.cloudWatchClient.send(buildCloudWatchCommand(deps.env.CLOUDWATCH_NAMESPACE, certificate)); + deps.logger.info(JSON.stringify(buildCloudWatchMetric(deps.env.CLOUDWATCH_NAMESPACE, certificate))); } - function buildCloudWatchCommand(namespace: string, certificate: APIGatewayEventClientCertificate): PutMetricDataCommand { - return new PutMetricDataCommand({ - MetricData: [{ - MetricName: 'apim-client-certificate-near-expiry', - Dimensions: [ - {Name: 'SUBJECT_DN', Value: certificate.subjectDN}, - {Name: 'NOT_AFTER', Value: certificate.validity.notAfter} - ] - }], - Namespace: namespace - }); + function buildCloudWatchMetric(namespace: string, certificate: APIGatewayEventClientCertificate) { + return { + _aws: { + Timestamp: new Date().valueOf(), + CloudWatchMetrics: [ + { + Namespace: namespace, + Dimensions: ['SUBJECT_DN', 'NOT_AFTER'], + Metrics: [ + { + Name: 'apim-client-certificate-near-expiry', + Unit: 'Count', + Value: 1, + }, + ], + }, + ], + }, + 'SUBJECT_DN': certificate.subjectDN, + 'NOT_AFTER': certificate.validity.notAfter, + 'apim-client-certificate-near-expiry': 1, + }; } }; diff --git a/lambdas/authorizer/src/deps.ts b/lambdas/authorizer/src/deps.ts index 226b0530..218fa649 100644 --- a/lambdas/authorizer/src/deps.ts +++ b/lambdas/authorizer/src/deps.ts @@ -1,4 +1,3 @@ -import { CloudWatchClient } from "@aws-sdk/client-cloudwatch"; import { DynamoDBClient } from "@aws-sdk/client-dynamodb"; import { DynamoDBDocumentClient } from "@aws-sdk/lib-dynamodb"; import pino from 'pino'; @@ -7,7 +6,6 @@ import { SupplierRepository } from '@internal/datastore'; export type Deps = { supplierRepo: SupplierRepository; - cloudWatchClient: CloudWatchClient; logger: pino.Logger; env: EnvVars; }; @@ -30,7 +28,6 @@ export function createDependenciesContainer(): Deps { return { supplierRepo: createSupplierRepository(createDocumentClient(), log, envVars), - cloudWatchClient: new CloudWatchClient({}), logger: log, env: envVars }; From 58fc378d72302d32ad5bd4d9010f4f0203cd0a31 Mon Sep 17 00:00:00 2001 From: Steve Buxton Date: Fri, 7 Nov 2025 09:18:27 +0000 Subject: [PATCH 14/14] Tidy up packages --- package-lock.json | 596 +--------------------------------------------- 1 file changed, 1 insertion(+), 595 deletions(-) diff --git a/package-lock.json b/package-lock.json index a896bdd2..4e6cc26a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2979,10 +2979,7 @@ "name": "nhs-notify-supplier-authorizer", "version": "0.0.1", "dependencies": { - "@aws-sdk/client-cloudwatch": "^3.922.0", - "esbuild": "^0.25.11", - "pino": "^10.1.0", - "zod": "^4.1.12" + "esbuild": "^0.25.11" }, "devDependencies": { "@tsconfig/node22": "^22.0.2", @@ -2993,37 +2990,6 @@ "typescript": "^5.8.3" } }, - "lambdas/authorizer/node_modules/pino": { - "version": "10.1.0", - "resolved": "https://registry.npmjs.org/pino/-/pino-10.1.0.tgz", - "integrity": "sha512-0zZC2ygfdqvqK8zJIr1e+wT1T/L+LF6qvqvbzEQ6tiMAoTqEVK9a1K3YRu8HEUvGEvNqZyPJTtb2sNIoTkB83w==", - "license": "MIT", - "dependencies": { - "@pinojs/redact": "^0.4.0", - "atomic-sleep": "^1.0.0", - "on-exit-leak-free": "^2.1.0", - "pino-abstract-transport": "^2.0.0", - "pino-std-serializers": "^7.0.0", - "process-warning": "^5.0.0", - "quick-format-unescaped": "^4.0.3", - "real-require": "^0.2.0", - "safe-stable-stringify": "^2.3.1", - "sonic-boom": "^4.0.1", - "thread-stream": "^3.0.0" - }, - "bin": { - "pino": "bin.js" - } - }, - "lambdas/authorizer/node_modules/zod": { - "version": "4.1.12", - "resolved": "https://registry.npmjs.org/zod/-/zod-4.1.12.tgz", - "integrity": "sha512-JInaHOamG8pt5+Ey8kGmdcAcg3OL9reK8ltczgHTAwNhMys/6ThXHityHxVV2p3fkw/c+MAvBHFVYHFZDmjMCQ==", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/colinhacks" - } - }, "node_modules/@ampproject/remapping": { "version": "2.3.0", "dev": true, @@ -3738,533 +3704,6 @@ ], "license": "MIT" }, - "node_modules/@aws-sdk/client-cloudwatch": { - "version": "3.922.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/client-cloudwatch/-/client-cloudwatch-3.922.0.tgz", - "integrity": "sha512-Bmh2GSfw7zRhANK3X3Ygs7G2IjIPEopTlRObEAqJSYq083jPSFtt6FyzuDpv1FmYSZJQ8wuoIjMdsxiLP/lgAw==", - "license": "Apache-2.0", - "dependencies": { - "@aws-crypto/sha256-browser": "5.2.0", - "@aws-crypto/sha256-js": "5.2.0", - "@aws-sdk/core": "3.922.0", - "@aws-sdk/credential-provider-node": "3.922.0", - "@aws-sdk/middleware-host-header": "3.922.0", - "@aws-sdk/middleware-logger": "3.922.0", - "@aws-sdk/middleware-recursion-detection": "3.922.0", - "@aws-sdk/middleware-user-agent": "3.922.0", - "@aws-sdk/region-config-resolver": "3.922.0", - "@aws-sdk/types": "3.922.0", - "@aws-sdk/util-endpoints": "3.922.0", - "@aws-sdk/util-user-agent-browser": "3.922.0", - "@aws-sdk/util-user-agent-node": "3.922.0", - "@smithy/config-resolver": "^4.4.1", - "@smithy/core": "^3.17.2", - "@smithy/fetch-http-handler": "^5.3.5", - "@smithy/hash-node": "^4.2.4", - "@smithy/invalid-dependency": "^4.2.4", - "@smithy/middleware-compression": "^4.3.6", - "@smithy/middleware-content-length": "^4.2.4", - "@smithy/middleware-endpoint": "^4.3.6", - "@smithy/middleware-retry": "^4.4.6", - "@smithy/middleware-serde": "^4.2.4", - "@smithy/middleware-stack": "^4.2.4", - "@smithy/node-config-provider": "^4.3.4", - "@smithy/node-http-handler": "^4.4.4", - "@smithy/protocol-http": "^5.3.4", - "@smithy/smithy-client": "^4.9.2", - "@smithy/types": "^4.8.1", - "@smithy/url-parser": "^4.2.4", - "@smithy/util-base64": "^4.3.0", - "@smithy/util-body-length-browser": "^4.2.0", - "@smithy/util-body-length-node": "^4.2.1", - "@smithy/util-defaults-mode-browser": "^4.3.5", - "@smithy/util-defaults-mode-node": "^4.2.7", - "@smithy/util-endpoints": "^3.2.4", - "@smithy/util-middleware": "^4.2.4", - "@smithy/util-retry": "^4.2.4", - "@smithy/util-utf8": "^4.2.0", - "@smithy/util-waiter": "^4.2.4", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@aws-sdk/client-cloudwatch/node_modules/@aws-sdk/client-sso": { - "version": "3.922.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/client-sso/-/client-sso-3.922.0.tgz", - "integrity": "sha512-jdHs7uy7cSpiMvrxhYmqHyJxgK7hyqw4plG8OQ4YTBpq0SbfAxdoOuOkwJ1IVUUQho4otR1xYYjiX/8e8J8qwQ==", - "license": "Apache-2.0", - "dependencies": { - "@aws-crypto/sha256-browser": "5.2.0", - "@aws-crypto/sha256-js": "5.2.0", - "@aws-sdk/core": "3.922.0", - "@aws-sdk/middleware-host-header": "3.922.0", - "@aws-sdk/middleware-logger": "3.922.0", - "@aws-sdk/middleware-recursion-detection": "3.922.0", - "@aws-sdk/middleware-user-agent": "3.922.0", - "@aws-sdk/region-config-resolver": "3.922.0", - "@aws-sdk/types": "3.922.0", - "@aws-sdk/util-endpoints": "3.922.0", - "@aws-sdk/util-user-agent-browser": "3.922.0", - "@aws-sdk/util-user-agent-node": "3.922.0", - "@smithy/config-resolver": "^4.4.1", - "@smithy/core": "^3.17.2", - "@smithy/fetch-http-handler": "^5.3.5", - "@smithy/hash-node": "^4.2.4", - "@smithy/invalid-dependency": "^4.2.4", - "@smithy/middleware-content-length": "^4.2.4", - "@smithy/middleware-endpoint": "^4.3.6", - "@smithy/middleware-retry": "^4.4.6", - "@smithy/middleware-serde": "^4.2.4", - "@smithy/middleware-stack": "^4.2.4", - "@smithy/node-config-provider": "^4.3.4", - "@smithy/node-http-handler": "^4.4.4", - "@smithy/protocol-http": "^5.3.4", - "@smithy/smithy-client": "^4.9.2", - "@smithy/types": "^4.8.1", - "@smithy/url-parser": "^4.2.4", - "@smithy/util-base64": "^4.3.0", - "@smithy/util-body-length-browser": "^4.2.0", - "@smithy/util-body-length-node": "^4.2.1", - "@smithy/util-defaults-mode-browser": "^4.3.5", - "@smithy/util-defaults-mode-node": "^4.2.7", - "@smithy/util-endpoints": "^3.2.4", - "@smithy/util-middleware": "^4.2.4", - "@smithy/util-retry": "^4.2.4", - "@smithy/util-utf8": "^4.2.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@aws-sdk/client-cloudwatch/node_modules/@aws-sdk/core": { - "version": "3.922.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.922.0.tgz", - "integrity": "sha512-EvfP4cqJfpO3L2v5vkIlTkMesPtRwWlMfsaW6Tpfm7iYfBOuTi6jx60pMDMTyJNVfh6cGmXwh/kj1jQdR+w99Q==", - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/types": "3.922.0", - "@aws-sdk/xml-builder": "3.921.0", - "@smithy/core": "^3.17.2", - "@smithy/node-config-provider": "^4.3.4", - "@smithy/property-provider": "^4.2.4", - "@smithy/protocol-http": "^5.3.4", - "@smithy/signature-v4": "^5.3.4", - "@smithy/smithy-client": "^4.9.2", - "@smithy/types": "^4.8.1", - "@smithy/util-base64": "^4.3.0", - "@smithy/util-middleware": "^4.2.4", - "@smithy/util-utf8": "^4.2.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@aws-sdk/client-cloudwatch/node_modules/@aws-sdk/credential-provider-env": { - "version": "3.922.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-env/-/credential-provider-env-3.922.0.tgz", - "integrity": "sha512-WikGQpKkROJSK3D3E7odPjZ8tU7WJp5/TgGdRuZw3izsHUeH48xMv6IznafpRTmvHcjAbDQj4U3CJZNAzOK/OQ==", - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/core": "3.922.0", - "@aws-sdk/types": "3.922.0", - "@smithy/property-provider": "^4.2.4", - "@smithy/types": "^4.8.1", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@aws-sdk/client-cloudwatch/node_modules/@aws-sdk/credential-provider-http": { - "version": "3.922.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-http/-/credential-provider-http-3.922.0.tgz", - "integrity": "sha512-i72DgHMK7ydAEqdzU0Duqh60Q8W59EZmRJ73y0Y5oFmNOqnYsAI+UXyOoCsubp+Dkr6+yOwAn1gPt1XGE9Aowg==", - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/core": "3.922.0", - "@aws-sdk/types": "3.922.0", - "@smithy/fetch-http-handler": "^5.3.5", - "@smithy/node-http-handler": "^4.4.4", - "@smithy/property-provider": "^4.2.4", - "@smithy/protocol-http": "^5.3.4", - "@smithy/smithy-client": "^4.9.2", - "@smithy/types": "^4.8.1", - "@smithy/util-stream": "^4.5.5", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@aws-sdk/client-cloudwatch/node_modules/@aws-sdk/credential-provider-ini": { - "version": "3.922.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.922.0.tgz", - "integrity": "sha512-bVF+pI5UCLNkvbiZr/t2fgTtv84s8FCdOGAPxQiQcw5qOZywNuuCCY3wIIchmQr6GJr8YFkEp5LgDCac5EC5aQ==", - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/core": "3.922.0", - "@aws-sdk/credential-provider-env": "3.922.0", - "@aws-sdk/credential-provider-http": "3.922.0", - "@aws-sdk/credential-provider-process": "3.922.0", - "@aws-sdk/credential-provider-sso": "3.922.0", - "@aws-sdk/credential-provider-web-identity": "3.922.0", - "@aws-sdk/nested-clients": "3.922.0", - "@aws-sdk/types": "3.922.0", - "@smithy/credential-provider-imds": "^4.2.4", - "@smithy/property-provider": "^4.2.4", - "@smithy/shared-ini-file-loader": "^4.3.4", - "@smithy/types": "^4.8.1", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@aws-sdk/client-cloudwatch/node_modules/@aws-sdk/credential-provider-node": { - "version": "3.922.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.922.0.tgz", - "integrity": "sha512-agCwaD6mBihToHkjycL8ObIS2XOnWypWZZWhJSoWyHwFrhEKz1zGvgylK9Dc711oUfU+zU6J8e0JPKNJMNb3BQ==", - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/credential-provider-env": "3.922.0", - "@aws-sdk/credential-provider-http": "3.922.0", - "@aws-sdk/credential-provider-ini": "3.922.0", - "@aws-sdk/credential-provider-process": "3.922.0", - "@aws-sdk/credential-provider-sso": "3.922.0", - "@aws-sdk/credential-provider-web-identity": "3.922.0", - "@aws-sdk/types": "3.922.0", - "@smithy/credential-provider-imds": "^4.2.4", - "@smithy/property-provider": "^4.2.4", - "@smithy/shared-ini-file-loader": "^4.3.4", - "@smithy/types": "^4.8.1", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@aws-sdk/client-cloudwatch/node_modules/@aws-sdk/credential-provider-process": { - "version": "3.922.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-process/-/credential-provider-process-3.922.0.tgz", - "integrity": "sha512-1DZOYezT6okslpvMW7oA2q+y17CJd4fxjNFH0jtThfswdh9CtG62+wxenqO+NExttq0UMaKisrkZiVrYQBTShw==", - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/core": "3.922.0", - "@aws-sdk/types": "3.922.0", - "@smithy/property-provider": "^4.2.4", - "@smithy/shared-ini-file-loader": "^4.3.4", - "@smithy/types": "^4.8.1", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@aws-sdk/client-cloudwatch/node_modules/@aws-sdk/credential-provider-sso": { - "version": "3.922.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.922.0.tgz", - "integrity": "sha512-nbD3G3hShTYxLCkKMqLkLPtKwAAfxdY/k9jHtZmVBFXek2T6tQrqZHKxlAu+fd23Ga4/Aik7DLQQx1RA1a5ipg==", - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/client-sso": "3.922.0", - "@aws-sdk/core": "3.922.0", - "@aws-sdk/token-providers": "3.922.0", - "@aws-sdk/types": "3.922.0", - "@smithy/property-provider": "^4.2.4", - "@smithy/shared-ini-file-loader": "^4.3.4", - "@smithy/types": "^4.8.1", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@aws-sdk/client-cloudwatch/node_modules/@aws-sdk/credential-provider-web-identity": { - "version": "3.922.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.922.0.tgz", - "integrity": "sha512-wjGIhgMHGGQfQTdFaJphNOKyAL8wZs6znJdHADPVURmgR+EWLyN/0fDO1u7wx8xaLMZpbHIFWBEvf9TritR/cQ==", - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/core": "3.922.0", - "@aws-sdk/nested-clients": "3.922.0", - "@aws-sdk/types": "3.922.0", - "@smithy/property-provider": "^4.2.4", - "@smithy/shared-ini-file-loader": "^4.3.4", - "@smithy/types": "^4.8.1", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@aws-sdk/client-cloudwatch/node_modules/@aws-sdk/middleware-host-header": { - "version": "3.922.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-host-header/-/middleware-host-header-3.922.0.tgz", - "integrity": "sha512-HPquFgBnq/KqKRVkiuCt97PmWbKtxQ5iUNLEc6FIviqOoZTmaYG3EDsIbuFBz9C4RHJU4FKLmHL2bL3FEId6AA==", - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/types": "3.922.0", - "@smithy/protocol-http": "^5.3.4", - "@smithy/types": "^4.8.1", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@aws-sdk/client-cloudwatch/node_modules/@aws-sdk/middleware-logger": { - "version": "3.922.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-logger/-/middleware-logger-3.922.0.tgz", - "integrity": "sha512-AkvYO6b80FBm5/kk2E636zNNcNgjztNNUxpqVx+huyGn9ZqGTzS4kLqW2hO6CBe5APzVtPCtiQsXL24nzuOlAg==", - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/types": "3.922.0", - "@smithy/types": "^4.8.1", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@aws-sdk/client-cloudwatch/node_modules/@aws-sdk/middleware-recursion-detection": { - "version": "3.922.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-recursion-detection/-/middleware-recursion-detection-3.922.0.tgz", - "integrity": "sha512-TtSCEDonV/9R0VhVlCpxZbp/9sxQvTTRKzIf8LxW3uXpby6Wl8IxEciBJlxmSkoqxh542WRcko7NYODlvL/gDA==", - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/types": "3.922.0", - "@aws/lambda-invoke-store": "^0.1.1", - "@smithy/protocol-http": "^5.3.4", - "@smithy/types": "^4.8.1", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@aws-sdk/client-cloudwatch/node_modules/@aws-sdk/middleware-user-agent": { - "version": "3.922.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-user-agent/-/middleware-user-agent-3.922.0.tgz", - "integrity": "sha512-N4Qx/9KP3oVQBJOrSghhz8iZFtUC2NNeSZt88hpPhbqAEAtuX8aD8OzVcpnAtrwWqy82Yd2YTxlkqMGkgqnBsQ==", - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/core": "3.922.0", - "@aws-sdk/types": "3.922.0", - "@aws-sdk/util-endpoints": "3.922.0", - "@smithy/core": "^3.17.2", - "@smithy/protocol-http": "^5.3.4", - "@smithy/types": "^4.8.1", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@aws-sdk/client-cloudwatch/node_modules/@aws-sdk/nested-clients": { - "version": "3.922.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/nested-clients/-/nested-clients-3.922.0.tgz", - "integrity": "sha512-uYvKCF1TGh/MuJ4TMqmUM0Csuao02HawcseG4LUDyxdUsd/EFuxalWq1Cx4fKZQ2K8F504efZBjctMAMNY+l7A==", - "license": "Apache-2.0", - "dependencies": { - "@aws-crypto/sha256-browser": "5.2.0", - "@aws-crypto/sha256-js": "5.2.0", - "@aws-sdk/core": "3.922.0", - "@aws-sdk/middleware-host-header": "3.922.0", - "@aws-sdk/middleware-logger": "3.922.0", - "@aws-sdk/middleware-recursion-detection": "3.922.0", - "@aws-sdk/middleware-user-agent": "3.922.0", - "@aws-sdk/region-config-resolver": "3.922.0", - "@aws-sdk/types": "3.922.0", - "@aws-sdk/util-endpoints": "3.922.0", - "@aws-sdk/util-user-agent-browser": "3.922.0", - "@aws-sdk/util-user-agent-node": "3.922.0", - "@smithy/config-resolver": "^4.4.1", - "@smithy/core": "^3.17.2", - "@smithy/fetch-http-handler": "^5.3.5", - "@smithy/hash-node": "^4.2.4", - "@smithy/invalid-dependency": "^4.2.4", - "@smithy/middleware-content-length": "^4.2.4", - "@smithy/middleware-endpoint": "^4.3.6", - "@smithy/middleware-retry": "^4.4.6", - "@smithy/middleware-serde": "^4.2.4", - "@smithy/middleware-stack": "^4.2.4", - "@smithy/node-config-provider": "^4.3.4", - "@smithy/node-http-handler": "^4.4.4", - "@smithy/protocol-http": "^5.3.4", - "@smithy/smithy-client": "^4.9.2", - "@smithy/types": "^4.8.1", - "@smithy/url-parser": "^4.2.4", - "@smithy/util-base64": "^4.3.0", - "@smithy/util-body-length-browser": "^4.2.0", - "@smithy/util-body-length-node": "^4.2.1", - "@smithy/util-defaults-mode-browser": "^4.3.5", - "@smithy/util-defaults-mode-node": "^4.2.7", - "@smithy/util-endpoints": "^3.2.4", - "@smithy/util-middleware": "^4.2.4", - "@smithy/util-retry": "^4.2.4", - "@smithy/util-utf8": "^4.2.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@aws-sdk/client-cloudwatch/node_modules/@aws-sdk/region-config-resolver": { - "version": "3.922.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/region-config-resolver/-/region-config-resolver-3.922.0.tgz", - "integrity": "sha512-44Y/rNNwhngR2KHp6gkx//TOr56/hx6s4l+XLjOqH7EBCHL7XhnrT1y92L+DLiroVr1tCSmO8eHQwBv0Y2+mvw==", - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/types": "3.922.0", - "@smithy/config-resolver": "^4.4.1", - "@smithy/node-config-provider": "^4.3.4", - "@smithy/types": "^4.8.1", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@aws-sdk/client-cloudwatch/node_modules/@aws-sdk/token-providers": { - "version": "3.922.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.922.0.tgz", - "integrity": "sha512-/inmPnjZE0ZBE16zaCowAvouSx05FJ7p6BQYuzlJ8vxEU0sS0Hf8fvhuiRnN9V9eDUPIBY+/5EjbMWygXL4wlQ==", - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/core": "3.922.0", - "@aws-sdk/nested-clients": "3.922.0", - "@aws-sdk/types": "3.922.0", - "@smithy/property-provider": "^4.2.4", - "@smithy/shared-ini-file-loader": "^4.3.4", - "@smithy/types": "^4.8.1", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@aws-sdk/client-cloudwatch/node_modules/@aws-sdk/types": { - "version": "3.922.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.922.0.tgz", - "integrity": "sha512-eLA6XjVobAUAMivvM7DBL79mnHyrm+32TkXNWZua5mnxF+6kQCfblKKJvxMZLGosO53/Ex46ogim8IY5Nbqv2w==", - "license": "Apache-2.0", - "dependencies": { - "@smithy/types": "^4.8.1", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@aws-sdk/client-cloudwatch/node_modules/@aws-sdk/util-endpoints": { - "version": "3.922.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/util-endpoints/-/util-endpoints-3.922.0.tgz", - "integrity": "sha512-4ZdQCSuNMY8HMlR1YN4MRDdXuKd+uQTeKIr5/pIM+g3TjInZoj8imvXudjcrFGA63UF3t92YVTkBq88mg58RXQ==", - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/types": "3.922.0", - "@smithy/types": "^4.8.1", - "@smithy/url-parser": "^4.2.4", - "@smithy/util-endpoints": "^3.2.4", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@aws-sdk/client-cloudwatch/node_modules/@aws-sdk/util-user-agent-browser": { - "version": "3.922.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-browser/-/util-user-agent-browser-3.922.0.tgz", - "integrity": "sha512-qOJAERZ3Plj1st7M4Q5henl5FRpE30uLm6L9edZqZXGR6c7ry9jzexWamWVpQ4H4xVAVmiO9dIEBAfbq4mduOA==", - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/types": "3.922.0", - "@smithy/types": "^4.8.1", - "bowser": "^2.11.0", - "tslib": "^2.6.2" - } - }, - "node_modules/@aws-sdk/client-cloudwatch/node_modules/@aws-sdk/util-user-agent-node": { - "version": "3.922.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-node/-/util-user-agent-node-3.922.0.tgz", - "integrity": "sha512-NrPe/Rsr5kcGunkog0eBV+bY0inkRELsD2SacC4lQZvZiXf8VJ2Y7j+Yq1tB+h+FPLsdt3v9wItIvDf/laAm0Q==", - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/middleware-user-agent": "3.922.0", - "@aws-sdk/types": "3.922.0", - "@smithy/node-config-provider": "^4.3.4", - "@smithy/types": "^4.8.1", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - }, - "peerDependencies": { - "aws-crt": ">=1.0.0" - }, - "peerDependenciesMeta": { - "aws-crt": { - "optional": true - } - } - }, - "node_modules/@aws-sdk/client-cloudwatch/node_modules/@aws-sdk/xml-builder": { - "version": "3.921.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/xml-builder/-/xml-builder-3.921.0.tgz", - "integrity": "sha512-LVHg0jgjyicKKvpNIEMXIMr1EBViESxcPkqfOlT+X1FkmUMTNZEEVF18tOJg4m4hV5vxtkWcqtr4IEeWa1C41Q==", - "license": "Apache-2.0", - "dependencies": { - "@smithy/types": "^4.8.1", - "fast-xml-parser": "5.2.5", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@aws-sdk/client-cloudwatch/node_modules/@aws/lambda-invoke-store": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/@aws/lambda-invoke-store/-/lambda-invoke-store-0.1.1.tgz", - "integrity": "sha512-RcLam17LdlbSOSp9VxmUu1eI6Mwxp+OwhD2QhiSNmNCzoDb0EeUXTD2n/WbcnrAYMGlmf05th6QYq23VqvJqpA==", - "license": "Apache-2.0", - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@aws-sdk/client-cloudwatch/node_modules/fast-xml-parser": { - "version": "5.2.5", - "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-5.2.5.tgz", - "integrity": "sha512-pfX9uG9Ki0yekDHx2SiuRIyFdyAr1kMIMitPvb0YBo8SUfKvia7w7FIyd/l6av85pFYRhZscS75MwMnbvY+hcQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/NaturalIntelligence" - } - ], - "license": "MIT", - "dependencies": { - "strnum": "^2.1.0" - }, - "bin": { - "fxparser": "src/cli/cli.js" - } - }, - "node_modules/@aws-sdk/client-cloudwatch/node_modules/strnum": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/strnum/-/strnum-2.1.1.tgz", - "integrity": "sha512-7ZvoFTiCnGxBtDqJ//Cu6fWtZtc7Y3x+QOirG15wztbdngGSkht27o2pyGWrVy0b4WAy3jbKmnoK6g5VlVNUUw==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/NaturalIntelligence" - } - ], - "license": "MIT" - }, "node_modules/@aws-sdk/client-dynamodb": { "version": "3.864.0", "license": "Apache-2.0", @@ -9374,12 +8813,6 @@ "node": ">=14" } }, - "node_modules/@pinojs/redact": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/@pinojs/redact/-/redact-0.4.0.tgz", - "integrity": "sha512-k2ENnmBugE/rzQfEcdWHcCY+/FM3VLzH9cYEsbdsoqrvzAKRhUZeRNhAZvB8OitQJ1TBed3yqWtdjzS6wJKBwg==", - "license": "MIT" - }, "node_modules/@pkgjs/parseargs": { "version": "0.11.0", "dev": true, @@ -10027,33 +9460,6 @@ "node": ">=18.0.0" } }, - "node_modules/@smithy/middleware-compression": { - "version": "4.3.6", - "resolved": "https://registry.npmjs.org/@smithy/middleware-compression/-/middleware-compression-4.3.6.tgz", - "integrity": "sha512-RnQGHRaXGL7olwEeiAEmnB6/PyhXQzy+udmtXCW6c0UvUzmBf7Rs48TxsZ6ID2fI9z/LWt62ITJZcul+CSdGLA==", - "license": "Apache-2.0", - "dependencies": { - "@smithy/core": "^3.17.2", - "@smithy/is-array-buffer": "^4.2.0", - "@smithy/node-config-provider": "^4.3.4", - "@smithy/protocol-http": "^5.3.4", - "@smithy/types": "^4.8.1", - "@smithy/util-config-provider": "^4.2.0", - "@smithy/util-middleware": "^4.2.4", - "@smithy/util-utf8": "^4.2.0", - "fflate": "0.8.1", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@smithy/middleware-compression/node_modules/fflate": { - "version": "0.8.1", - "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.8.1.tgz", - "integrity": "sha512-/exOvEuc+/iaUm105QIiOt4LpBdMTWsXxqR0HDF35vx3fmaKzw7354gTilCh5rkzEt8WYyG//ku3h3nRmd7CHQ==", - "license": "MIT" - }, "node_modules/@smithy/middleware-content-length": { "version": "4.2.4", "resolved": "https://registry.npmjs.org/@smithy/middleware-content-length/-/middleware-content-length-4.2.4.tgz",