diff --git a/.github/actions/acceptance-tests/action.yaml b/.github/actions/acceptance-tests/action.yaml
new file mode 100644
index 0000000..922232e
--- /dev/null
+++ b/.github/actions/acceptance-tests/action.yaml
@@ -0,0 +1,54 @@
+name: Acceptance tests
+description: "Run acceptance tests for this repo"
+
+inputs:
+ testType:
+ description: Type of test to run
+ required: true
+
+ targetEnvironment:
+ description: Name of the environment under test
+ required: true
+
+ targetAccountGroup:
+ description: Name of the account group under test
+ default: nhs-notify-client-callbacks-dev
+ required: true
+
+ targetComponent:
+ description: Name of the component under test
+ required: true
+
+runs:
+ using: "composite"
+
+ steps:
+ - name: Fetch terraform output
+ uses: actions/download-artifact@v4
+ with:
+ name: terraform-output-${{ inputs.targetComponent }}
+
+ - name: Get Node version
+ id: nodejs_version
+ shell: bash
+ run: |
+ echo "nodejs_version=$(grep "^nodejs\s" .tool-versions | cut -f2 -d' ')" >> $GITHUB_OUTPUT
+
+ - name: "Repo setup"
+ uses: ./.github/actions/node-install
+ with:
+ GITHUB_TOKEN: ${{ env.GITHUB_TOKEN }}
+
+ - name: "Set environment variables"
+ shell: bash
+ run: |
+ echo "PR_NUMBER=${{ inputs.targetEnvironment }}" >> $GITHUB_ENV
+ echo "ENVIRONMENT=${{ inputs.targetEnvironment }}" >> $GITHUB_ENV
+
+ - name: Run test - ${{ inputs.testType }}
+ shell: bash
+ env:
+ PROJECT: nhs
+ COMPONENT: ${{ inputs.targetComponent }}
+ run: |
+ make test-${{ inputs.testType }}
diff --git a/.github/actions/node-install/action.yaml b/.github/actions/node-install/action.yaml
new file mode 100644
index 0000000..b1ed2d0
--- /dev/null
+++ b/.github/actions/node-install/action.yaml
@@ -0,0 +1,24 @@
+name: 'npm install and setup'
+description: 'Setup node, authenticate github package repository and perform clean npm install'
+
+inputs:
+ GITHUB_TOKEN:
+ description: "Token for access to github package registry"
+ required: true
+
+runs:
+ using: 'composite'
+ steps:
+ - name: 'Use Node.js'
+ uses: actions/setup-node@v4
+ with:
+ node-version-file: '.tool-versions'
+ registry-url: 'https://npm.pkg.github.com'
+ scope: '@nhsdigital'
+
+ - name: 'Install dependencies'
+ shell: bash
+ env:
+ NODE_AUTH_TOKEN: ${{ inputs.GITHUB_TOKEN }}
+ run: |
+ npm ci
diff --git a/.github/actions/test-types.json b/.github/actions/test-types.json
new file mode 100644
index 0000000..4fe0a8a
--- /dev/null
+++ b/.github/actions/test-types.json
@@ -0,0 +1,3 @@
+[
+ "integration"
+]
diff --git a/.github/workflows/cicd-1-pull-request.yaml b/.github/workflows/cicd-1-pull-request.yaml
index bb88afb..cd4d89e 100644
--- a/.github/workflows/cicd-1-pull-request.yaml
+++ b/.github/workflows/cicd-1-pull-request.yaml
@@ -174,9 +174,11 @@ jobs:
--overrides "branch_name=${GITHUB_HEAD_REF:-${GITHUB_REF#refs/heads/}}"
acceptance-stage: # Recommended maximum execution time is 10 minutes
name: "Acceptance stage"
- needs: [metadata, build-stage]
+ needs: [metadata, build-stage, pr-create-dynamic-environment]
uses: ./.github/workflows/stage-4-acceptance.yaml
- if: needs.metadata.outputs.does_pull_request_exist == 'true' || (github.event_name == 'pull_request' && (github.event.action == 'opened' || github.event.action == 'reopened')) || (github.event_name == 'push' && github.ref == 'refs/heads/main')
+ if: >-
+ contains(fromJSON('["success", "skipped"]'), needs.pr-create-dynamic-environment.result) &&
+ (needs.metadata.outputs.does_pull_request_exist == 'true' || (github.event_name == 'pull_request' && (github.event.action == 'opened' || github.event.action == 'reopened')) || (github.event_name == 'push' && github.ref == 'refs/heads/main'))
with:
build_datetime: "${{ needs.metadata.outputs.build_datetime }}"
build_timestamp: "${{ needs.metadata.outputs.build_timestamp }}"
diff --git a/.github/workflows/stage-4-acceptance.yaml b/.github/workflows/stage-4-acceptance.yaml
index c6dc58e..4ae997e 100644
--- a/.github/workflows/stage-4-acceptance.yaml
+++ b/.github/workflows/stage-4-acceptance.yaml
@@ -36,139 +36,37 @@ on:
required: true
type: string
+permissions:
+ id-token: write
+ contents: read
+
jobs:
- environment-set-up:
- name: "Environment set up"
- runs-on: ubuntu-latest
- timeout-minutes: 5
- steps:
- - name: "Checkout code"
- uses: actions/checkout@v4
- - name: "Create infractructure"
- run: |
- echo "Creating infractructure..."
- - name: "Update database"
- run: |
- echo "Updating database..."
- - name: "Deploy application"
- run: |
- echo "Deploying application..."
- test-contract:
- name: "Contract test"
- runs-on: ubuntu-latest
- needs: environment-set-up
- timeout-minutes: 10
- steps:
- - name: "Checkout code"
- uses: actions/checkout@v4
- - name: "Run contract test"
- run: |
- make test-contract
- - name: "Save result"
- run: |
- echo "Nothing to save"
- test-security:
- name: "Security test"
+ run-acceptance-tests:
+ name: Run Acceptance Tests
runs-on: ubuntu-latest
- needs: environment-set-up
- timeout-minutes: 10
steps:
- - name: "Checkout code"
- uses: actions/checkout@v4
- - name: "Run security test"
- run: |
- make test-security
- - name: "Save result"
- run: |
- echo "Nothing to save"
- test-ui:
- name: "UI test"
- runs-on: ubuntu-latest
- needs: environment-set-up
- timeout-minutes: 10
- steps:
- - name: "Checkout code"
- uses: actions/checkout@v4
- - name: "Run UI test"
- run: |
- make test-ui
- - name: "Save result"
- run: |
- echo "Nothing to save"
- test-ui-performance:
- name: "UI performance test"
- runs-on: ubuntu-latest
- needs: environment-set-up
- timeout-minutes: 10
- steps:
- - name: "Checkout code"
- uses: actions/checkout@v4
- - name: "Run UI performance test"
- run: |
- make test-ui-performance
- - name: "Save result"
- run: |
- echo "Nothing to save"
- test-integration:
- name: "Integration test"
- runs-on: ubuntu-latest
- needs: environment-set-up
- timeout-minutes: 10
- steps:
- - name: "Checkout code"
- uses: actions/checkout@v4
- - name: "Run integration test"
- run: |
- make test-integration
- - name: "Save result"
- run: |
- echo "Nothing to save"
- test-accessibility:
- name: "Accessibility test"
- runs-on: ubuntu-latest
- needs: environment-set-up
- timeout-minutes: 10
- steps:
- - name: "Checkout code"
- uses: actions/checkout@v4
- - name: "Run accessibility test"
- run: |
- make test-accessibility
- - name: "Save result"
- run: |
- echo "Nothing to save"
- test-load:
- name: "Load test"
- runs-on: ubuntu-latest
- needs: environment-set-up
- timeout-minutes: 10
- steps:
- - name: "Checkout code"
- uses: actions/checkout@v4
- - name: "Run load tests"
- run: |
- make test-load
- - name: "Save result"
- run: |
- echo "Nothing to save"
- environment-tear-down:
- name: "Environment tear down"
- runs-on: ubuntu-latest
- needs:
- [
- test-accessibility,
- test-contract,
- test-integration,
- test-load,
- test-security,
- test-ui-performance,
- test-ui,
- ]
- if: always()
- timeout-minutes: 5
- steps:
- - name: "Checkout code"
- uses: actions/checkout@v4
- - name: "Tear down environment"
- run: |
- echo "Tearing down environment..."
+ - uses: actions/checkout@v4
+
+ - name: "Use Node.js"
+ uses: actions/setup-node@v4
+ with:
+ node-version: "${{ inputs.nodejs_version }}"
+ registry-url: "https://npm.pkg.github.com"
+ scope: "@nhsdigital"
+
+ - name: Trigger Acceptance Tests
+ shell: bash
+ env:
+ APP_PEM_FILE: ${{ secrets.APP_PEM_FILE }}
+ APP_CLIENT_ID: ${{ secrets.APP_CLIENT_ID }}
+ NODE_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ TARGET_ENVIRONMENT: ${{ inputs.target_environment }}
+ run: |
+ .github/scripts/dispatch_internal_repo_workflow.sh \
+ --targetWorkflow "dispatch-contextual-tests-dynamic-env.yaml" \
+ --infraRepoName "nhs-notify-client-callbacks" \
+ --releaseVersion "${GITHUB_HEAD_REF:-${GITHUB_REF#refs/heads/}}" \
+ --overrideProjectName "nhs" \
+ --targetEnvironment "$TARGET_ENVIRONMENT" \
+ --targetAccountGroup "nhs-notify-client-callbacks-dev" \
+ --targetComponent "callbacks"
diff --git a/eslint.config.mjs b/eslint.config.mjs
index 9d3ce40..61b5183 100644
--- a/eslint.config.mjs
+++ b/eslint.config.mjs
@@ -101,6 +101,7 @@ export default defineConfig([
},
],
"unicorn/no-null": 0,
+ "unicorn/no-useless-undefined": 0,
"unicorn/prefer-module": 0,
"unicorn/import-style": [
2,
@@ -239,6 +240,20 @@ export default defineConfig([
],
},
},
+ {
+ files: ["tools/client-subscriptions-management/**/*.ts"],
+ rules: {
+ "no-console": "off",
+ "import-x/first": "off",
+ },
+ },
+ {
+ files: ["lambdas/client-transform-filter-lambda/**/*.ts"],
+ rules: {
+ "no-console": "off",
+ "import-x/first": "off",
+ },
+ },
// misc rule overrides
{
diff --git a/infrastructure/terraform/components/callbacks/README.md b/infrastructure/terraform/components/callbacks/README.md
index baaa7d2..1c3f444 100644
--- a/infrastructure/terraform/components/callbacks/README.md
+++ b/infrastructure/terraform/components/callbacks/README.md
@@ -47,7 +47,11 @@
| [sqs\_inbound\_event](#module\_sqs\_inbound\_event) | https://github.com/NHSDigital/nhs-notify-shared-modules/releases/download/v2.0.29/terraform-sqs.zip | n/a |
## Outputs
-No outputs.
+| Name | Description |
+|------|-------------|
+| [deployment](#output\_deployment) | Deployment details used for post-deployment scripts |
+| [mock\_webhook\_lambda\_log\_group\_name](#output\_mock\_webhook\_lambda\_log\_group\_name) | CloudWatch log group name for mock webhook lambda (for integration test queries) |
+| [mock\_webhook\_url](#output\_mock\_webhook\_url) | URL endpoint for mock webhook (for TEST\_WEBHOOK\_URL environment variable) |
diff --git a/infrastructure/terraform/components/callbacks/module_transform_filter_lambda.tf b/infrastructure/terraform/components/callbacks/module_transform_filter_lambda.tf
index 2fff974..386e237 100644
--- a/infrastructure/terraform/components/callbacks/module_transform_filter_lambda.tf
+++ b/infrastructure/terraform/components/callbacks/module_transform_filter_lambda.tf
@@ -35,8 +35,11 @@ module "client_transform_filter_lambda" {
log_subscription_role_arn = local.acct.log_subscription_role_arn
lambda_env_vars = {
- ENVIRONMENT = var.environment
- METRICS_NAMESPACE = "nhs-notify-client-callbacks"
+ ENVIRONMENT = var.environment
+ METRICS_NAMESPACE = "nhs-notify-client-callbacks-metrics"
+ CLIENT_SUBSCRIPTION_CONFIG_BUCKET = module.client_config_bucket.id
+ CLIENT_SUBSCRIPTION_CONFIG_PREFIX = "client_subscriptions/"
+ CLIENT_SUBSCRIPTION_CACHE_TTL_SECONDS = "60"
}
}
diff --git a/infrastructure/terraform/components/callbacks/outputs.tf b/infrastructure/terraform/components/callbacks/outputs.tf
new file mode 100644
index 0000000..b042e36
--- /dev/null
+++ b/infrastructure/terraform/components/callbacks/outputs.tf
@@ -0,0 +1,29 @@
+##
+# Deployment details
+##
+
+output "deployment" {
+ description = "Deployment details used for post-deployment scripts"
+ value = {
+ aws_region = var.region
+ aws_account_id = var.aws_account_id
+ project = var.project
+ environment = var.environment
+ group = var.group
+ component = var.component
+ }
+}
+
+##
+# Mock Webhook Lambda Outputs (test/dev environments only)
+##
+
+output "mock_webhook_lambda_log_group_name" {
+ description = "CloudWatch log group name for mock webhook lambda (for integration test queries)"
+ value = var.deploy_mock_webhook ? module.mock_webhook_lambda[0].cloudwatch_log_group_name : null
+}
+
+output "mock_webhook_url" {
+ description = "URL endpoint for mock webhook (for TEST_WEBHOOK_URL environment variable)"
+ value = var.deploy_mock_webhook ? aws_lambda_function_url.mock_webhook[0].function_url : null
+}
diff --git a/lambdas/client-transform-filter-lambda/package.json b/lambdas/client-transform-filter-lambda/package.json
index 5cc0128..4a4be91 100644
--- a/lambdas/client-transform-filter-lambda/package.json
+++ b/lambdas/client-transform-filter-lambda/package.json
@@ -1,5 +1,6 @@
{
"dependencies": {
+ "@aws-sdk/client-s3": "^3.821.0",
"@nhs-notify-client-callbacks/models": "*",
"aws-embedded-metrics": "^4.2.1",
"cloudevents": "^8.0.2",
diff --git a/lambdas/client-transform-filter-lambda/src/__tests__/index.component.test.ts b/lambdas/client-transform-filter-lambda/src/__tests__/index.component.test.ts
new file mode 100644
index 0000000..b8851d5
--- /dev/null
+++ b/lambdas/client-transform-filter-lambda/src/__tests__/index.component.test.ts
@@ -0,0 +1,239 @@
+/**
+ * Component test for the complete handler flow including S3 config loading and
+ * subscription filtering. Uses the real ConfigLoader + ConfigCache + filter pipeline
+ * with a mocked S3Client.
+ */
+// Mock S3Client before importing the handler
+const mockSend = jest.fn();
+jest.mock("@aws-sdk/client-s3", () => {
+ const actual = jest.requireActual("@aws-sdk/client-s3");
+ return {
+ ...actual,
+ S3Client: jest.fn().mockImplementation(() => ({
+ send: mockSend,
+ })),
+ };
+});
+
+jest.mock("aws-embedded-metrics", () => ({
+ createMetricsLogger: jest.fn(() => ({
+ setNamespace: jest.fn(),
+ setDimensions: jest.fn(),
+ putMetric: jest.fn(),
+ flush: jest.fn().mockResolvedValue(undefined as unknown),
+ })),
+ Unit: {
+ Count: "Count",
+ Milliseconds: "Milliseconds",
+ },
+}));
+
+import { GetObjectCommand, NoSuchKey } from "@aws-sdk/client-s3";
+import type { SQSRecord } from "aws-lambda";
+import { EventTypes } from "@nhs-notify-client-callbacks/models";
+import { createS3Client } from "services/config-loader-service";
+import { configLoaderService, handler } from "..";
+
+const makeSqsRecord = (body: object): SQSRecord => ({
+ messageId: "sqs-id",
+ receiptHandle: "receipt",
+ body: JSON.stringify(body),
+ attributes: {
+ ApproximateReceiveCount: "1",
+ SentTimestamp: "1519211230",
+ SenderId: "ABCDEFGHIJ",
+ ApproximateFirstReceiveTimestamp: "1519211230",
+ },
+ messageAttributes: {},
+ md5OfBody: "md5",
+ eventSource: "aws:sqs",
+ eventSourceARN: "arn:aws:sqs:eu-west-2:123456789:queue",
+ awsRegion: "eu-west-2",
+});
+
+const createValidConfig = (clientId: string) => [
+ {
+ SubscriptionId: "00000000-0000-0000-0000-000000000001",
+ ClientId: clientId,
+ Targets: [
+ {
+ Type: "API",
+ TargetId: "00000000-0000-4000-8000-000000000001",
+ InvocationEndpoint: "https://example.com/webhook",
+ InvocationMethod: "POST",
+ InvocationRateLimit: 10,
+ APIKey: {
+ HeaderName: "x-api-key",
+ HeaderValue: "secret",
+ },
+ },
+ ],
+ SubscriptionType: "MessageStatus",
+ MessageStatuses: ["DELIVERED", "FAILED"],
+ },
+];
+
+const validMessageStatusEvent = (clientId: string, messageStatus: string) => ({
+ specversion: "1.0",
+ id: "event-id",
+ source: "/nhs/england/notify/development/primary/data-plane/client-callbacks",
+ subject: `customer/test/message/msg-123`,
+ type: EventTypes.MESSAGE_STATUS_PUBLISHED,
+ time: "2025-01-01T10:00:00.000Z",
+ datacontenttype: "application/json",
+ dataschema: "https://nhs.uk/schemas/notify/message-status-data.v1.json",
+ traceparent: "00-4d678967f96e353c07a0a31c1849b500-07f83ba58dd8df70-01",
+ data: {
+ messageId: "msg-123",
+ messageReference: "ref-123",
+ messageStatus,
+ channels: [{ type: "NHSAPP", channelStatus: "DELIVERED" }],
+ timestamp: "2025-01-01T10:00:00Z",
+ routingPlan: {
+ id: "plan-id",
+ name: "plan-name",
+ version: "1",
+ createdDate: "2025-01-01T10:00:00Z",
+ },
+ clientId,
+ },
+});
+
+describe("Lambda handler with S3 subscription filtering", () => {
+ beforeAll(() => {
+ process.env.CLIENT_SUBSCRIPTION_CONFIG_BUCKET = "test-bucket";
+ process.env.CLIENT_SUBSCRIPTION_CONFIG_PREFIX = "client_subscriptions/";
+ process.env.CLIENT_SUBSCRIPTION_CACHE_TTL_SECONDS = "60";
+ process.env.METRICS_NAMESPACE = "test-namespace";
+ process.env.ENVIRONMENT = "test";
+ });
+
+ beforeEach(() => {
+ mockSend.mockClear();
+ // Reset loader and clear cache for clean state between tests
+ configLoaderService.reset(
+ createS3Client({ AWS_ENDPOINT_URL: "http://localhost:4566" }),
+ );
+ });
+
+ afterAll(() => {
+ configLoaderService.reset();
+ delete process.env.CLIENT_SUBSCRIPTION_CONFIG_BUCKET;
+ delete process.env.CLIENT_SUBSCRIPTION_CONFIG_PREFIX;
+ delete process.env.CLIENT_SUBSCRIPTION_CACHE_TTL_SECONDS;
+ delete process.env.METRICS_NAMESPACE;
+ delete process.env.ENVIRONMENT;
+ });
+
+ it("passes event through when client config matches subscription", async () => {
+ mockSend.mockResolvedValue({
+ Body: {
+ transformToString: jest
+ .fn()
+ .mockResolvedValue(JSON.stringify(createValidConfig("client-1"))),
+ },
+ });
+
+ const result = await handler([
+ makeSqsRecord(validMessageStatusEvent("client-1", "DELIVERED")),
+ ]);
+
+ expect(result).toHaveLength(1);
+ expect(mockSend).toHaveBeenCalledTimes(1);
+ expect(mockSend.mock.calls[0][0]).toBeInstanceOf(GetObjectCommand);
+ });
+
+ it("filters out event when status is not in subscription", async () => {
+ mockSend.mockResolvedValue({
+ Body: {
+ transformToString: jest
+ .fn()
+ .mockResolvedValue(JSON.stringify(createValidConfig("client-1"))),
+ },
+ });
+
+ const result = await handler([
+ makeSqsRecord(validMessageStatusEvent("client-1", "CREATED")),
+ ]);
+
+ expect(result).toHaveLength(0);
+ });
+
+ it("filters out event when client has no configuration in S3", async () => {
+ mockSend.mockRejectedValue(
+ new NoSuchKey({ message: "Not found", $metadata: {} }),
+ );
+
+ const result = await handler([
+ makeSqsRecord(validMessageStatusEvent("client-no-config", "DELIVERED")),
+ ]);
+
+ expect(result).toHaveLength(0);
+ });
+
+ it("passes matching events and filters non-matching in the same batch", async () => {
+ // First call (client-1 DELIVERED) → match
+ // Second call (client-1 CREATED) → no match
+ // Both share the same client config (cached after first call)
+ mockSend.mockResolvedValue({
+ Body: {
+ transformToString: jest
+ .fn()
+ .mockResolvedValue(JSON.stringify(createValidConfig("client-1"))),
+ },
+ });
+
+ const result = await handler([
+ makeSqsRecord(validMessageStatusEvent("client-1", "DELIVERED")),
+ makeSqsRecord(validMessageStatusEvent("client-1", "CREATED")),
+ ]);
+
+ // Only the DELIVERED event passes the filter
+ expect(result).toHaveLength(1);
+ expect((result[0].data as { messageStatus: string }).messageStatus).toBe(
+ "DELIVERED",
+ );
+ });
+
+ it("throws when CLIENT_SUBSCRIPTION_CONFIG_BUCKET is not set", async () => {
+ configLoaderService.reset();
+ const originalBucket = process.env.CLIENT_SUBSCRIPTION_CONFIG_BUCKET;
+ delete process.env.CLIENT_SUBSCRIPTION_CONFIG_BUCKET;
+
+ await expect(
+ handler([
+ makeSqsRecord(validMessageStatusEvent("client-1", "DELIVERED")),
+ ]),
+ ).rejects.toThrow("CLIENT_SUBSCRIPTION_CONFIG_BUCKET is required");
+
+ process.env.CLIENT_SUBSCRIPTION_CONFIG_BUCKET =
+ originalBucket ?? "test-bucket";
+ });
+
+ it("loads configs for multiple distinct clients in parallel and deduplicates S3 fetches", async () => {
+ mockSend.mockImplementation((cmd: { input: { Key: string } }) => {
+ const clientId = cmd.input.Key.replace(
+ "client_subscriptions/",
+ "",
+ ).replace(".json", "");
+ return Promise.resolve({
+ Body: {
+ transformToString: jest
+ .fn()
+ .mockResolvedValue(JSON.stringify(createValidConfig(clientId))),
+ },
+ });
+ });
+
+ const result = await handler([
+ makeSqsRecord(validMessageStatusEvent("client-a", "DELIVERED")),
+ makeSqsRecord(validMessageStatusEvent("client-b", "DELIVERED")),
+ makeSqsRecord(validMessageStatusEvent("client-a", "DELIVERED")), // duplicate client
+ ]);
+
+ // All three events match their respective configs
+ expect(result).toHaveLength(3);
+ // S3 fetched once per distinct client (client-a and client-b), not once per event
+ expect(mockSend).toHaveBeenCalledTimes(2);
+ });
+});
diff --git a/lambdas/client-transform-filter-lambda/src/__tests__/index.test.ts b/lambdas/client-transform-filter-lambda/src/__tests__/index.test.ts
index 13de95b..bda75a6 100644
--- a/lambdas/client-transform-filter-lambda/src/__tests__/index.test.ts
+++ b/lambdas/client-transform-filter-lambda/src/__tests__/index.test.ts
@@ -9,9 +9,64 @@ import type {
} from "@nhs-notify-client-callbacks/models";
import type { Logger } from "services/logger";
import type { CallbackMetrics } from "services/metrics";
+import type { ConfigLoader } from "services/config-loader";
import { ObservabilityService } from "services/observability";
+import { ConfigLoaderService } from "services/config-loader-service";
import { createHandler } from "..";
+jest.mock("aws-embedded-metrics");
+
+const createPassthroughConfigLoader = (): ConfigLoader =>
+ ({
+ loadClientConfig: jest.fn().mockImplementation(async (clientId: string) => [
+ {
+ SubscriptionType: "MessageStatus",
+ SubscriptionId: "00000000-0000-0000-0000-000000000001",
+ ClientId: clientId,
+ Targets: [],
+ MessageStatuses: [
+ "DELIVERED",
+ "FAILED",
+ "PENDING",
+ "SENDING",
+ "TECHNICAL_FAILURE",
+ "PERMANENT_FAILURE",
+ ],
+ },
+ {
+ SubscriptionType: "ChannelStatus",
+ SubscriptionId: "00000000-0000-0000-0000-000000000002",
+ ClientId: clientId,
+ Targets: [],
+ ChannelType: "NHSAPP",
+ ChannelStatuses: ["DELIVERED", "FAILED", "TECHNICAL_FAILURE"],
+ SupplierStatuses: [
+ "delivered",
+ "permanent_failure",
+ "temporary_failure",
+ ],
+ },
+ {
+ SubscriptionType: "ChannelStatus",
+ SubscriptionId: "00000000-0000-0000-0000-000000000003",
+ ClientId: clientId,
+ Targets: [],
+ ChannelType: "SMS",
+ ChannelStatuses: ["DELIVERED", "FAILED", "TECHNICAL_FAILURE"],
+ SupplierStatuses: [
+ "delivered",
+ "permanent_failure",
+ "temporary_failure",
+ ],
+ },
+ ]),
+ }) as unknown as ConfigLoader;
+
+const makeStubConfigLoaderService = (): ConfigLoaderService => {
+ const loader = createPassthroughConfigLoader();
+ return { getLoader: () => loader } as unknown as ConfigLoaderService;
+};
+
describe("Lambda handler", () => {
const mockLogger = {
info: jest.fn(),
@@ -29,6 +84,8 @@ describe("Lambda handler", () => {
emitTransformationFailure: jest.fn(),
emitDeliveryInitiated: jest.fn(),
emitValidationError: jest.fn(),
+ emitFilteringStarted: jest.fn(),
+ emitFilteringMatched: jest.fn(),
} as unknown as CallbackMetrics;
const mockMetricsLogger = {
@@ -38,6 +95,7 @@ describe("Lambda handler", () => {
const handler = createHandler({
createObservabilityService: () =>
new ObservabilityService(mockLogger, mockMetrics, mockMetricsLogger),
+ createConfigLoaderService: makeStubConfigLoaderService,
});
beforeEach(() => {
@@ -174,7 +232,7 @@ describe("Lambda handler", () => {
};
await expect(handler([sqsMessage])).rejects.toThrow(
- 'Validation failed: type: Invalid option: expected one of "uk.nhs.notify.message.status.PUBLISHED.v1"|"uk.nhs.notify.channel.status.PUBLISHED.v1"',
+ "Validation failed: type: Invalid option",
);
});
@@ -260,6 +318,60 @@ describe("Lambda handler", () => {
);
});
+ it("should use 'Unknown error' message when a non-Error is thrown during SQS message parsing", async () => {
+ const faultyMetrics = {
+ emitEventReceived: jest.fn(),
+ emitValidationError: jest.fn(),
+ emitTransformationFailure: jest.fn(),
+ emitDeliveryInitiated: jest.fn(),
+ emitTransformationSuccess: jest.fn(),
+ };
+ const faultyLogger = {
+ info: jest.fn(),
+ warn: jest.fn(),
+ error: jest.fn(),
+ debug: jest.fn(),
+ child: jest.fn().mockReturnThis(),
+ addContext: jest.fn(),
+ clearContext: jest.fn(),
+ };
+ const faultyObservability = {
+ recordProcessingStarted: jest.fn(() => {
+ // eslint-disable-next-line @typescript-eslint/only-throw-error
+ throw "non-error-thrown";
+ }),
+ getLogger: jest.fn().mockReturnValue(faultyLogger),
+ getMetrics: jest.fn().mockReturnValue(faultyMetrics),
+ flush: jest.fn().mockResolvedValue(undefined),
+ } as unknown as ObservabilityService;
+
+ const faultyHandler = createHandler({
+ createObservabilityService: () => faultyObservability,
+ createConfigLoaderService: makeStubConfigLoaderService,
+ });
+
+ const sqsMessage: SQSRecord = {
+ messageId: "sqs-msg-id-non-error",
+ receiptHandle: "receipt-handle-non-error",
+ body: JSON.stringify(validMessageStatusEvent),
+ attributes: {
+ ApproximateReceiveCount: "1",
+ SentTimestamp: "1519211230",
+ SenderId: "ABCDEFGHIJ",
+ ApproximateFirstReceiveTimestamp: "1519211230",
+ },
+ messageAttributes: {},
+ md5OfBody: "mock-md5",
+ eventSource: "aws:sqs",
+ eventSourceARN: "arn:aws:sqs:eu-west-2:123456789:mock-queue",
+ awsRegion: "eu-west-2",
+ };
+
+ await expect(faultyHandler([sqsMessage])).rejects.toThrow(
+ "Failed to parse SQS message body as JSON: Unknown error",
+ );
+ });
+
it("should process empty batch successfully", async () => {
const result = await handler([]);
@@ -392,7 +504,14 @@ describe("createHandler default wiring", () => {
});
expect(state.testHandler).toBeDefined();
+ const originalBucket = process.env.CLIENT_SUBSCRIPTION_CONFIG_BUCKET;
+ process.env.CLIENT_SUBSCRIPTION_CONFIG_BUCKET = "test-bucket";
const result = await state.testHandler!([]);
+ if (originalBucket === undefined) {
+ delete process.env.CLIENT_SUBSCRIPTION_CONFIG_BUCKET;
+ } else {
+ process.env.CLIENT_SUBSCRIPTION_CONFIG_BUCKET = originalBucket;
+ }
expect(state.createMetricLogger).toHaveBeenCalledTimes(1);
expect(state.CallbackMetrics).toHaveBeenCalledWith(state.mockMetricsLogger);
@@ -405,6 +524,7 @@ describe("createHandler default wiring", () => {
expect(state.processEvents).toHaveBeenCalledWith(
[],
state.mockObservabilityInstance,
+ expect.any(Object),
);
expect(result).toEqual(["ok"]);
diff --git a/lambdas/client-transform-filter-lambda/src/__tests__/services/callback-logger.test.ts b/lambdas/client-transform-filter-lambda/src/__tests__/services/callback-logger.test.ts
index 03206bf..e8b9308 100644
--- a/lambdas/client-transform-filter-lambda/src/__tests__/services/callback-logger.test.ts
+++ b/lambdas/client-transform-filter-lambda/src/__tests__/services/callback-logger.test.ts
@@ -21,7 +21,7 @@ describe("callback-logger", () => {
});
describe("logCallbackGenerated", () => {
- describe("MESSAGE_STATUS_TRANSITIONED events", () => {
+ describe("MESSAGE_STATUS_PUBLISHED events", () => {
const messageStatusPayload: ClientCallbackPayload = {
data: [
{
diff --git a/lambdas/client-transform-filter-lambda/src/__tests__/services/config-cache.test.ts b/lambdas/client-transform-filter-lambda/src/__tests__/services/config-cache.test.ts
new file mode 100644
index 0000000..cab5893
--- /dev/null
+++ b/lambdas/client-transform-filter-lambda/src/__tests__/services/config-cache.test.ts
@@ -0,0 +1,77 @@
+import type { ClientSubscriptionConfiguration } from "@nhs-notify-client-callbacks/models";
+import { ConfigCache } from "services/config-cache";
+
+describe("ConfigCache", () => {
+ it("stores and retrieves configuration", () => {
+ const cache = new ConfigCache(60_000);
+ const config: ClientSubscriptionConfiguration = [
+ {
+ SubscriptionId: "00000000-0000-0000-0000-000000000001",
+ ClientId: "client-1",
+ Targets: [],
+ SubscriptionType: "MessageStatus" as const,
+ MessageStatuses: ["DELIVERED"],
+ },
+ ];
+
+ cache.set("client-1", config);
+ const result = cache.get("client-1");
+
+ expect(result).toEqual(config);
+ });
+
+ it("returns undefined for non-existent key", () => {
+ const cache = new ConfigCache(60_000);
+ const result = cache.get("non-existent");
+
+ expect(result).toBeUndefined();
+ });
+
+ it("returns undefined for expired entries", () => {
+ jest.useFakeTimers();
+ jest.setSystemTime(new Date("2025-01-01T10:00:00Z"));
+
+ const cache = new ConfigCache(1000); // 1 second TTL
+ const config: ClientSubscriptionConfiguration = [
+ {
+ SubscriptionId: "00000000-0000-0000-0000-000000000001",
+ ClientId: "client-1",
+ Targets: [],
+ SubscriptionType: "MessageStatus" as const,
+ MessageStatuses: ["DELIVERED"],
+ },
+ ];
+
+ cache.set("client-1", config);
+
+ // Advance time past expiry
+ jest.advanceTimersByTime(1500);
+
+ const result = cache.get("client-1");
+
+ expect(result).toBeUndefined();
+
+ jest.useRealTimers();
+ });
+
+ it("clears all entries", () => {
+ const cache = new ConfigCache(60_000);
+ const config: ClientSubscriptionConfiguration = [
+ {
+ SubscriptionId: "00000000-0000-0000-0000-000000000001",
+ ClientId: "client-1",
+ Targets: [],
+ SubscriptionType: "MessageStatus" as const,
+ MessageStatuses: ["DELIVERED"],
+ },
+ ];
+
+ cache.set("client-1", config);
+ cache.set("client-2", config);
+
+ cache.clear();
+
+ expect(cache.get("client-1")).toBeUndefined();
+ expect(cache.get("client-2")).toBeUndefined();
+ });
+});
diff --git a/lambdas/client-transform-filter-lambda/src/__tests__/services/config-loader-service.test.ts b/lambdas/client-transform-filter-lambda/src/__tests__/services/config-loader-service.test.ts
new file mode 100644
index 0000000..a5741d2
--- /dev/null
+++ b/lambdas/client-transform-filter-lambda/src/__tests__/services/config-loader-service.test.ts
@@ -0,0 +1,169 @@
+import { S3Client } from "@aws-sdk/client-s3";
+import { ConfigLoader } from "services/config-loader";
+import {
+ ConfigLoaderService,
+ createS3Client,
+ resolveCacheTtlMs,
+} from "services/config-loader-service";
+
+const mockS3Client = jest.mocked(S3Client);
+const mockConfigLoader = jest.mocked(ConfigLoader);
+
+jest.mock("@aws-sdk/client-s3", () => ({
+ S3Client: jest.fn(),
+}));
+
+jest.mock("services/config-loader", () => ({
+ ConfigLoader: jest.fn(),
+}));
+
+describe("ConfigLoaderService", () => {
+ const originalBucket = process.env.CLIENT_SUBSCRIPTION_CONFIG_BUCKET;
+ const originalPrefix = process.env.CLIENT_SUBSCRIPTION_CONFIG_PREFIX;
+
+ beforeEach(() => {
+ mockConfigLoader.mockClear();
+ process.env.CLIENT_SUBSCRIPTION_CONFIG_BUCKET = "test-bucket";
+ });
+
+ afterEach(() => {
+ if (originalBucket === undefined) {
+ delete process.env.CLIENT_SUBSCRIPTION_CONFIG_BUCKET;
+ } else {
+ process.env.CLIENT_SUBSCRIPTION_CONFIG_BUCKET = originalBucket;
+ }
+
+ if (originalPrefix === undefined) {
+ delete process.env.CLIENT_SUBSCRIPTION_CONFIG_PREFIX;
+ } else {
+ process.env.CLIENT_SUBSCRIPTION_CONFIG_PREFIX = originalPrefix;
+ }
+ });
+
+ describe("getLoader", () => {
+ it("returns the same loader instance on subsequent calls (lazy singleton)", () => {
+ const service = new ConfigLoaderService();
+ const first = service.getLoader();
+ const second = service.getLoader();
+ expect(first).toBe(second);
+ });
+
+ it("throws when CLIENT_SUBSCRIPTION_CONFIG_BUCKET is not set", () => {
+ delete process.env.CLIENT_SUBSCRIPTION_CONFIG_BUCKET;
+ const service = new ConfigLoaderService();
+ expect(() => service.getLoader()).toThrow(
+ "CLIENT_SUBSCRIPTION_CONFIG_BUCKET is required",
+ );
+ });
+
+ it("uses the default key prefix when CLIENT_SUBSCRIPTION_CONFIG_PREFIX is not set", () => {
+ delete process.env.CLIENT_SUBSCRIPTION_CONFIG_PREFIX;
+ const service = new ConfigLoaderService();
+ service.getLoader();
+ expect(mockConfigLoader).toHaveBeenCalledWith(
+ expect.objectContaining({ keyPrefix: "client_subscriptions/" }),
+ );
+ });
+
+ it("uses the configured key prefix when CLIENT_SUBSCRIPTION_CONFIG_PREFIX is set", () => {
+ process.env.CLIENT_SUBSCRIPTION_CONFIG_PREFIX = "custom_prefix/";
+ const service = new ConfigLoaderService();
+ service.getLoader();
+ expect(mockConfigLoader).toHaveBeenCalledWith(
+ expect.objectContaining({ keyPrefix: "custom_prefix/" }),
+ );
+ });
+ });
+
+ describe("reset", () => {
+ it("clears the cached loader so a new one is created on next getLoader call", () => {
+ const service = new ConfigLoaderService();
+ const before = service.getLoader();
+ service.reset();
+ const after = service.getLoader();
+ expect(after).not.toBe(before);
+ });
+
+ it("initialises a new loader with a custom S3Client when provided", () => {
+ const customClient = createS3Client({
+ AWS_ENDPOINT_URL: "http://localhost:4566",
+ });
+ const service = new ConfigLoaderService();
+ service.reset(customClient);
+ // Should not throw and the loader should be available immediately
+ expect(() => service.getLoader()).not.toThrow();
+ });
+
+ it("uses the configured key prefix when CLIENT_SUBSCRIPTION_CONFIG_PREFIX is set", () => {
+ process.env.CLIENT_SUBSCRIPTION_CONFIG_PREFIX = "custom_prefix/";
+ const customClient = createS3Client({
+ AWS_ENDPOINT_URL: "http://localhost:4566",
+ });
+ const service = new ConfigLoaderService();
+ service.reset(customClient);
+ expect(mockConfigLoader).toHaveBeenCalledWith(
+ expect.objectContaining({ keyPrefix: "custom_prefix/" }),
+ );
+ });
+
+ it("throws when S3Client is provided but CLIENT_SUBSCRIPTION_CONFIG_BUCKET is not set", () => {
+ delete process.env.CLIENT_SUBSCRIPTION_CONFIG_BUCKET;
+ const customClient = createS3Client();
+ const service = new ConfigLoaderService();
+ expect(() => service.reset(customClient)).toThrow(
+ "CLIENT_SUBSCRIPTION_CONFIG_BUCKET is required",
+ );
+ });
+ });
+});
+
+describe("createS3Client", () => {
+ beforeEach(() => {
+ mockS3Client.mockClear();
+ });
+
+ it("sets forcePathStyle=true when endpoint contains localhost", () => {
+ createS3Client({ AWS_ENDPOINT_URL: "http://localhost:4566" });
+
+ expect(mockS3Client).toHaveBeenCalledWith({
+ endpoint: "http://localhost:4566",
+ forcePathStyle: true,
+ });
+ });
+
+ it("does not set forcePathStyle when endpoint does not contain localhost", () => {
+ createS3Client({ AWS_ENDPOINT_URL: "https://custom-s3.example.com" });
+
+ expect(mockS3Client).toHaveBeenCalledWith({
+ endpoint: "https://custom-s3.example.com",
+ forcePathStyle: undefined,
+ });
+ });
+
+ it("does not set forcePathStyle when endpoint is not set", () => {
+ createS3Client({});
+
+ expect(mockS3Client).toHaveBeenCalledWith({
+ endpoint: undefined,
+ forcePathStyle: undefined,
+ });
+ });
+});
+
+describe("resolveCacheTtlMs", () => {
+ it("falls back to default TTL when value is not a number", () => {
+ const ttlMs = resolveCacheTtlMs({
+ CLIENT_SUBSCRIPTION_CACHE_TTL_SECONDS: "not-a-number",
+ } as NodeJS.ProcessEnv);
+
+ expect(ttlMs).toBe(60_000);
+ });
+
+ it("uses the configured TTL when valid", () => {
+ const ttlMs = resolveCacheTtlMs({
+ CLIENT_SUBSCRIPTION_CACHE_TTL_SECONDS: "120",
+ } as NodeJS.ProcessEnv);
+
+ expect(ttlMs).toBe(120_000);
+ });
+});
diff --git a/lambdas/client-transform-filter-lambda/src/__tests__/services/config-loader.test.ts b/lambdas/client-transform-filter-lambda/src/__tests__/services/config-loader.test.ts
new file mode 100644
index 0000000..044035d
--- /dev/null
+++ b/lambdas/client-transform-filter-lambda/src/__tests__/services/config-loader.test.ts
@@ -0,0 +1,135 @@
+import { GetObjectCommand, NoSuchKey, S3Client } from "@aws-sdk/client-s3";
+import { ConfigCache } from "services/config-cache";
+import { ConfigLoader } from "services/config-loader";
+import { ConfigValidationError } from "services/validators/config-validator";
+
+jest.mock("services/logger", () => ({
+ logger: {
+ debug: jest.fn(),
+ info: jest.fn(),
+ warn: jest.fn(),
+ error: jest.fn(),
+ },
+}));
+
+const mockBody = (json: string) => ({
+ transformToString: jest.fn().mockResolvedValue(json),
+});
+
+const createValidConfig = (clientId: string) => [
+ {
+ SubscriptionId: "00000000-0000-0000-0000-000000000001",
+ ClientId: clientId,
+ Targets: [
+ {
+ Type: "API",
+ TargetId: "00000000-0000-4000-8000-000000000001",
+ InvocationEndpoint: "https://example.com/webhook",
+ InvocationMethod: "POST",
+ InvocationRateLimit: 10,
+ APIKey: {
+ HeaderName: "x-api-key",
+ HeaderValue: "secret",
+ },
+ },
+ ],
+ SubscriptionType: "MessageStatus",
+ MessageStatuses: ["DELIVERED"],
+ },
+];
+
+const createLoader = (send: jest.Mock) =>
+ new ConfigLoader({
+ bucketName: "bucket",
+ keyPrefix: "client_subscriptions/",
+ s3Client: { send } as unknown as S3Client,
+ cache: new ConfigCache(60_000),
+ });
+
+describe("ConfigLoader", () => {
+ it("loads and validates client configuration from S3", async () => {
+ const send = jest.fn().mockResolvedValue({
+ Body: mockBody(JSON.stringify(createValidConfig("client-1"))),
+ });
+ const loader = createLoader(send);
+
+ const result = await loader.loadClientConfig("client-1");
+
+ expect(result).toEqual(createValidConfig("client-1"));
+ expect(send).toHaveBeenCalledTimes(1);
+ expect(send.mock.calls[0][0]).toBeInstanceOf(GetObjectCommand);
+ expect(send.mock.calls[0][0].input).toEqual({
+ Bucket: "bucket",
+ Key: "client_subscriptions/client-1.json",
+ });
+ });
+
+ it("returns cached configuration on subsequent calls", async () => {
+ const send = jest.fn().mockResolvedValue({
+ Body: mockBody(JSON.stringify(createValidConfig("client-1"))),
+ });
+ const loader = createLoader(send);
+
+ await loader.loadClientConfig("client-1");
+ await loader.loadClientConfig("client-1");
+
+ expect(send).toHaveBeenCalledTimes(1);
+ });
+
+ it("returns undefined when the configuration file is missing", async () => {
+ const send = jest
+ .fn()
+ .mockRejectedValue(
+ new NoSuchKey({ message: "Not found", $metadata: {} }),
+ );
+ const loader = createLoader(send);
+
+ await expect(loader.loadClientConfig("client-1")).resolves.toBeUndefined();
+ });
+
+ it("throws when configuration fails validation", async () => {
+ const send = jest.fn().mockResolvedValue({
+ Body: mockBody(JSON.stringify([{ SubscriptionType: "MessageStatus" }])),
+ });
+ const loader = createLoader(send);
+
+ await expect(loader.loadClientConfig("client-1")).rejects.toThrow(
+ ConfigValidationError,
+ );
+ });
+
+ it("throws when S3 response body is empty", async () => {
+ const send = jest.fn().mockResolvedValue({});
+ const loader = createLoader(send);
+
+ await expect(loader.loadClientConfig("client-1")).rejects.toThrow(
+ ConfigValidationError,
+ );
+ });
+
+ it("wraps S3 errors as ConfigValidationError", async () => {
+ const send = jest.fn().mockRejectedValue(new Error("S3 access denied"));
+ const loader = createLoader(send);
+
+ const error = await loader
+ .loadClientConfig("client-1")
+ .catch((error_) => error_);
+ expect(error).toBeInstanceOf(ConfigValidationError);
+ expect(error.issues).toEqual([
+ { path: "config", message: "S3 access denied" },
+ ]);
+ });
+
+ it("wraps non-Error values thrown by S3 as ConfigValidationError", async () => {
+ const send = jest.fn().mockRejectedValue("unexpected string error");
+ const loader = createLoader(send);
+
+ const error = await loader
+ .loadClientConfig("client-1")
+ .catch((error_) => error_);
+ expect(error).toBeInstanceOf(ConfigValidationError);
+ expect(error.issues).toEqual([
+ { path: "config", message: "unexpected string error" },
+ ]);
+ });
+});
diff --git a/lambdas/client-transform-filter-lambda/src/__tests__/services/config-update.component.test.ts b/lambdas/client-transform-filter-lambda/src/__tests__/services/config-update.component.test.ts
new file mode 100644
index 0000000..3cf95b9
--- /dev/null
+++ b/lambdas/client-transform-filter-lambda/src/__tests__/services/config-update.component.test.ts
@@ -0,0 +1,90 @@
+import { S3Client } from "@aws-sdk/client-s3";
+import { ConfigCache } from "services/config-cache";
+import { ConfigLoader } from "services/config-loader";
+
+describe("config update component", () => {
+ it("reloads configuration after cache expiry", async () => {
+ jest.useFakeTimers();
+ jest.setSystemTime(new Date("2025-01-01T10:00:00Z"));
+
+ const send = jest
+ .fn()
+ .mockResolvedValueOnce({
+ Body: {
+ transformToString: jest.fn().mockResolvedValue(
+ JSON.stringify([
+ {
+ SubscriptionId: "00000000-0000-0000-0000-000000000001",
+ ClientId: "client-1",
+ Targets: [
+ {
+ Type: "API",
+ TargetId: "target",
+ InvocationEndpoint: "https://example.com",
+ InvocationMethod: "POST",
+ InvocationRateLimit: 10,
+ APIKey: {
+ HeaderName: "x-api-key",
+ HeaderValue: "secret",
+ },
+ },
+ ],
+ SubscriptionType: "MessageStatus",
+ MessageStatuses: ["DELIVERED"],
+ },
+ ]),
+ ),
+ },
+ })
+ .mockResolvedValueOnce({
+ Body: {
+ transformToString: jest.fn().mockResolvedValue(
+ JSON.stringify([
+ {
+ SubscriptionId: "00000000-0000-0000-0000-000000000001",
+ ClientId: "client-1",
+ Targets: [
+ {
+ Type: "API",
+ TargetId: "target",
+ InvocationEndpoint: "https://example.com",
+ InvocationMethod: "POST",
+ InvocationRateLimit: 10,
+ APIKey: {
+ HeaderName: "x-api-key",
+ HeaderValue: "secret",
+ },
+ },
+ ],
+ SubscriptionType: "MessageStatus",
+ MessageStatuses: ["FAILED"],
+ },
+ ]),
+ ),
+ },
+ });
+
+ const loader = new ConfigLoader({
+ bucketName: "bucket",
+ keyPrefix: "client_subscriptions/",
+ s3Client: { send } as unknown as S3Client,
+ cache: new ConfigCache(1000),
+ });
+
+ const first = await loader.loadClientConfig("client-1");
+ const firstMessage = first?.find(
+ (subscription) => subscription.SubscriptionType === "MessageStatus",
+ );
+ expect(firstMessage?.MessageStatuses).toEqual(["DELIVERED"]);
+
+ jest.advanceTimersByTime(1500);
+
+ const second = await loader.loadClientConfig("client-1");
+ const secondMessage = second?.find(
+ (subscription) => subscription.SubscriptionType === "MessageStatus",
+ );
+ expect(secondMessage?.MessageStatuses).toEqual(["FAILED"]);
+
+ jest.useRealTimers();
+ });
+});
diff --git a/lambdas/client-transform-filter-lambda/src/__tests__/services/error-handler.test.ts b/lambdas/client-transform-filter-lambda/src/__tests__/services/error-handler.test.ts
index 57ef1fe..b7c1555 100644
--- a/lambdas/client-transform-filter-lambda/src/__tests__/services/error-handler.test.ts
+++ b/lambdas/client-transform-filter-lambda/src/__tests__/services/error-handler.test.ts
@@ -1,8 +1,10 @@
import {
+ ConfigValidationError,
ErrorType,
LambdaError,
TransformationError,
ValidationError,
+ formatValidationIssuePath,
getEventError,
wrapUnknownError,
} from "services/error-handler";
@@ -127,6 +129,60 @@ describe("TransformationError", () => {
});
});
+describe("formatValidationIssuePath", () => {
+ it("returns empty string for empty path", () => {
+ expect(formatValidationIssuePath([])).toBe("");
+ });
+
+ it("returns string segment directly at root", () => {
+ expect(formatValidationIssuePath(["traceparent"])).toBe("traceparent");
+ });
+
+ it("uses dot notation for nested string segments", () => {
+ expect(formatValidationIssuePath(["data", "clientId"])).toBe(
+ "data.clientId",
+ );
+ });
+
+ it("uses bracket notation for numeric segments", () => {
+ expect(formatValidationIssuePath([0])).toBe("[0]");
+ });
+
+ it("combines bracket and dot notation for mixed paths", () => {
+ expect(formatValidationIssuePath(["channels", 0, "type"])).toBe(
+ "channels[0].type",
+ );
+ });
+});
+
+describe("ConfigValidationError", () => {
+ it("should create error with issues array", () => {
+ const issues = [
+ {
+ path: "[0].SubscriptionId",
+ message: "Expected SubscriptionId to be unique",
+ },
+ ];
+ const error = new ConfigValidationError(issues);
+
+ expect(error.message).toBe(
+ "Client subscription configuration validation failed",
+ );
+ expect(error.issues).toBe(issues);
+ expect(error.errorType).toBe(ErrorType.VALIDATION_ERROR);
+ expect(error.retryable).toBe(false);
+ expect(error.correlationId).toBeUndefined();
+ expect(error.name).toBe("ConfigValidationError");
+ });
+
+ it("should be instanceof LambdaError and Error", () => {
+ const error = new ConfigValidationError([]);
+ expect(error).toBeInstanceOf(ConfigValidationError);
+ expect(error).toBeInstanceOf(LambdaError);
+ expect(error).toBeInstanceOf(Error);
+ });
+});
+
describe("wrapUnknownError", () => {
it("should return LambdaError as-is", () => {
const originalError = new ValidationError("Original", "corr-123");
@@ -298,4 +354,23 @@ describe("getEventError", () => {
expect(mockMetrics.emitTransformationFailure).toHaveBeenCalled();
expect(mockMetrics.emitValidationError).not.toHaveBeenCalled();
});
+
+ it("should return ConfigValidationError and emit validation metric", () => {
+ const error = new ConfigValidationError([
+ {
+ path: "[0].SubscriptionId",
+ message: "Expected SubscriptionId to be unique",
+ },
+ ]);
+
+ const result = getEventError(error, mockMetrics, mockEventLogger);
+
+ expect(result).toBe(error);
+ expect(mockEventLogger.error).toHaveBeenCalledWith(
+ "Client config validation failed",
+ { error },
+ );
+ expect(mockMetrics.emitValidationError).toHaveBeenCalled();
+ expect(mockMetrics.emitTransformationFailure).not.toHaveBeenCalled();
+ });
});
diff --git a/lambdas/client-transform-filter-lambda/src/__tests__/services/filters/channel-status-filter.test.ts b/lambdas/client-transform-filter-lambda/src/__tests__/services/filters/channel-status-filter.test.ts
new file mode 100644
index 0000000..8c6eefa
--- /dev/null
+++ b/lambdas/client-transform-filter-lambda/src/__tests__/services/filters/channel-status-filter.test.ts
@@ -0,0 +1,286 @@
+import type {
+ ChannelStatus,
+ ChannelStatusData,
+ ClientSubscriptionConfiguration,
+ StatusPublishEvent,
+ SupplierStatus,
+} from "@nhs-notify-client-callbacks/models";
+import { EventTypes } from "@nhs-notify-client-callbacks/models";
+import { matchesChannelStatusSubscription } from "services/filters/channel-status-filter";
+
+jest.mock("services/logger", () => ({
+ logger: {
+ debug: jest.fn(),
+ info: jest.fn(),
+ warn: jest.fn(),
+ error: jest.fn(),
+ },
+}));
+
+const createBaseEvent = (
+ type: string,
+ source: string,
+ notifyData: T,
+): StatusPublishEvent => ({
+ specversion: "1.0",
+ id: "event-id",
+ source,
+ subject: "subject",
+ type,
+ time: "2025-01-01T10:00:00Z",
+ datacontenttype: "application/json",
+ dataschema: "schema",
+ traceparent: "traceparent",
+ data: notifyData,
+});
+
+const createChannelStatusConfig = (
+ channelStatuses: ChannelStatus[],
+ supplierStatuses: SupplierStatus[],
+ clientId = "client-1",
+): ClientSubscriptionConfiguration => [
+ {
+ SubscriptionId: "00000000-0000-0000-0000-000000000001",
+ ClientId: clientId,
+ Targets: [
+ {
+ Type: "API",
+ TargetId: "target",
+ InvocationEndpoint: "https://example.com",
+ InvocationMethod: "POST",
+ InvocationRateLimit: 10,
+ APIKey: {
+ HeaderName: "x-api-key",
+ HeaderValue: "secret",
+ },
+ },
+ ],
+ SubscriptionType: "ChannelStatus",
+ ChannelType: "EMAIL",
+ ChannelStatuses: channelStatuses,
+ SupplierStatuses: supplierStatuses,
+ },
+];
+
+const createChannelStatusData = (
+ overrides: Partial = {},
+): ChannelStatusData => ({
+ messageId: "message-id",
+ messageReference: "reference",
+ channel: "EMAIL",
+ channelStatus: "DELIVERED",
+ supplierStatus: "read",
+ cascadeType: "primary",
+ cascadeOrder: 1,
+ timestamp: "2025-01-01T10:00:00Z",
+ retryCount: 0,
+ clientId: "client-1",
+ ...overrides,
+});
+
+describe("matchesChannelStatusSubscription", () => {
+ it("matches by channel and supplier status", () => {
+ const notifyData = createChannelStatusData();
+ const event = createBaseEvent(
+ EventTypes.CHANNEL_STATUS_PUBLISHED,
+ "source-a",
+ notifyData,
+ );
+ expect(
+ matchesChannelStatusSubscription(
+ createChannelStatusConfig(["DELIVERED"], ["read"]),
+ { event, notifyData },
+ ),
+ ).toBe(true);
+ });
+
+ it("rejects when channel does not match", () => {
+ const notifyData = createChannelStatusData({ channel: "SMS" });
+ const event = createBaseEvent(
+ EventTypes.CHANNEL_STATUS_PUBLISHED,
+ "source-a",
+ notifyData,
+ );
+ expect(
+ matchesChannelStatusSubscription(
+ createChannelStatusConfig(["DELIVERED"], ["read"]),
+ { event, notifyData },
+ ),
+ ).toBe(false);
+ });
+
+ it("rejects when clientId does not match", () => {
+ const notifyData = createChannelStatusData({ clientId: "client-2" });
+ const event = createBaseEvent(
+ EventTypes.CHANNEL_STATUS_PUBLISHED,
+ "source-a",
+ notifyData,
+ );
+ expect(
+ matchesChannelStatusSubscription(
+ createChannelStatusConfig(["DELIVERED"], ["read"]),
+ { event, notifyData },
+ ),
+ ).toBe(false);
+ });
+
+ it("rejects when channelStatus does not match", () => {
+ const notifyData = createChannelStatusData({
+ channelStatus: "FAILED",
+ previousChannelStatus: "SENDING",
+ supplierStatus: "read",
+ previousSupplierStatus: "read",
+ });
+ const event = createBaseEvent(
+ EventTypes.CHANNEL_STATUS_PUBLISHED,
+ "source-a",
+ notifyData,
+ );
+ expect(
+ matchesChannelStatusSubscription(
+ createChannelStatusConfig(["DELIVERED"], ["read"]),
+ { event, notifyData },
+ ),
+ ).toBe(false);
+ });
+
+ it("rejects when supplierStatus does not match", () => {
+ const notifyData = createChannelStatusData({
+ channelStatus: "DELIVERED",
+ previousChannelStatus: "DELIVERED",
+ supplierStatus: "rejected",
+ previousSupplierStatus: "notified",
+ });
+ const event = createBaseEvent(
+ EventTypes.CHANNEL_STATUS_PUBLISHED,
+ "source-a",
+ notifyData,
+ );
+ expect(
+ matchesChannelStatusSubscription(
+ createChannelStatusConfig(["DELIVERED"], ["read"]),
+ { event, notifyData },
+ ),
+ ).toBe(false);
+ });
+
+ it("rejects when neither status changed", () => {
+ const notifyData = createChannelStatusData({
+ channelStatus: "DELIVERED",
+ previousChannelStatus: "DELIVERED",
+ supplierStatus: "read",
+ previousSupplierStatus: "read",
+ });
+ const event = createBaseEvent(
+ EventTypes.CHANNEL_STATUS_PUBLISHED,
+ "source-a",
+ notifyData,
+ );
+ expect(
+ matchesChannelStatusSubscription(
+ createChannelStatusConfig(["DELIVERED"], ["read"]),
+ { event, notifyData },
+ ),
+ ).toBe(false);
+ });
+
+ it("matches when only channelStatus changed and is subscribed (OR logic)", () => {
+ const notifyData = createChannelStatusData({
+ channelStatus: "DELIVERED",
+ previousChannelStatus: "SENDING",
+ supplierStatus: "notified",
+ previousSupplierStatus: "notified",
+ });
+ const event = createBaseEvent(
+ EventTypes.CHANNEL_STATUS_PUBLISHED,
+ "source-a",
+ notifyData,
+ );
+ expect(
+ matchesChannelStatusSubscription(
+ createChannelStatusConfig(["DELIVERED"], ["read"]),
+ { event, notifyData },
+ ),
+ ).toBe(true);
+ });
+
+ it("matches when only supplierStatus changed and is subscribed (OR logic)", () => {
+ const notifyData = createChannelStatusData({
+ channelStatus: "SENDING",
+ previousChannelStatus: "SENDING",
+ supplierStatus: "read",
+ previousSupplierStatus: "notified",
+ });
+ const event = createBaseEvent(
+ EventTypes.CHANNEL_STATUS_PUBLISHED,
+ "source-a",
+ notifyData,
+ );
+ expect(
+ matchesChannelStatusSubscription(
+ createChannelStatusConfig(["DELIVERED"], ["read"]),
+ { event, notifyData },
+ ),
+ ).toBe(true);
+ });
+
+ it("matches with empty supplierStatuses when channelStatus changed", () => {
+ const notifyData = createChannelStatusData({
+ channelStatus: "DELIVERED",
+ previousChannelStatus: "SENDING",
+ supplierStatus: "read",
+ previousSupplierStatus: "notified",
+ });
+ const event = createBaseEvent(
+ EventTypes.CHANNEL_STATUS_PUBLISHED,
+ "source-a",
+ notifyData,
+ );
+ expect(
+ matchesChannelStatusSubscription(
+ createChannelStatusConfig(["DELIVERED"], []),
+ { event, notifyData },
+ ),
+ ).toBe(true);
+ });
+
+ it("matches with empty channelStatuses when supplierStatus changed", () => {
+ const notifyData = createChannelStatusData({
+ channelStatus: "DELIVERED",
+ previousChannelStatus: "SENDING",
+ supplierStatus: "read",
+ previousSupplierStatus: "notified",
+ });
+ const event = createBaseEvent(
+ EventTypes.CHANNEL_STATUS_PUBLISHED,
+ "source-a",
+ notifyData,
+ );
+ expect(
+ matchesChannelStatusSubscription(
+ createChannelStatusConfig([], ["read"]),
+ { event, notifyData },
+ ),
+ ).toBe(true);
+ });
+
+ it("rejects with both channelStatuses and supplierStatuses empty", () => {
+ const notifyData = createChannelStatusData({
+ channelStatus: "DELIVERED",
+ previousChannelStatus: "SENDING",
+ supplierStatus: "read",
+ previousSupplierStatus: "notified",
+ });
+ const event = createBaseEvent(
+ EventTypes.CHANNEL_STATUS_PUBLISHED,
+ "source-a",
+ notifyData,
+ );
+ expect(
+ matchesChannelStatusSubscription(createChannelStatusConfig([], []), {
+ event,
+ notifyData,
+ }),
+ ).toBe(false);
+ });
+});
diff --git a/lambdas/client-transform-filter-lambda/src/__tests__/services/filters/message-status-filter.test.ts b/lambdas/client-transform-filter-lambda/src/__tests__/services/filters/message-status-filter.test.ts
new file mode 100644
index 0000000..ee3a070
--- /dev/null
+++ b/lambdas/client-transform-filter-lambda/src/__tests__/services/filters/message-status-filter.test.ts
@@ -0,0 +1,175 @@
+import type {
+ ClientSubscriptionConfiguration,
+ MessageStatus,
+ MessageStatusData,
+ StatusPublishEvent,
+} from "@nhs-notify-client-callbacks/models";
+import { EventTypes } from "@nhs-notify-client-callbacks/models";
+import { matchesMessageStatusSubscription } from "services/filters/message-status-filter";
+
+jest.mock("services/logger", () => ({
+ logger: {
+ debug: jest.fn(),
+ info: jest.fn(),
+ warn: jest.fn(),
+ error: jest.fn(),
+ },
+}));
+
+const createBaseEvent = (
+ type: string,
+ source: string,
+ notifyData: T,
+): StatusPublishEvent => ({
+ specversion: "1.0",
+ id: "event-id",
+ source,
+ subject: "subject",
+ type,
+ time: "2025-01-01T10:00:00Z",
+ datacontenttype: "application/json",
+ dataschema: "schema",
+ traceparent: "traceparent",
+ data: notifyData,
+});
+
+const createMessageStatusConfig = (
+ statuses: MessageStatus[],
+ clientId = "client-1",
+): ClientSubscriptionConfiguration => [
+ {
+ SubscriptionId: "00000000-0000-0000-0000-000000000001",
+ ClientId: clientId,
+ Targets: [
+ {
+ Type: "API",
+ TargetId: "target",
+ InvocationEndpoint: "https://example.com",
+ InvocationMethod: "POST",
+ InvocationRateLimit: 10,
+ APIKey: {
+ HeaderName: "x-api-key",
+ HeaderValue: "secret",
+ },
+ },
+ ],
+ SubscriptionType: "MessageStatus",
+ MessageStatuses: statuses,
+ },
+];
+
+const createMessageStatusData = (
+ overrides: Partial = {},
+): MessageStatusData => ({
+ messageId: "message-id",
+ messageReference: "reference",
+ messageStatus: "DELIVERED",
+ channels: [],
+ timestamp: "2025-01-01T10:00:00Z",
+ routingPlan: {
+ id: "plan-id",
+ name: "plan-name",
+ version: "1",
+ createdDate: "2025-01-01T10:00:00Z",
+ },
+ clientId: "client-1",
+ ...overrides,
+});
+
+describe("matchesMessageStatusSubscription", () => {
+ it("matches by client, status, and event pattern", () => {
+ const notifyData = createMessageStatusData();
+ const event = createBaseEvent(
+ EventTypes.MESSAGE_STATUS_PUBLISHED,
+ "source-a",
+ notifyData,
+ );
+ expect(
+ matchesMessageStatusSubscription(
+ createMessageStatusConfig(["DELIVERED"]),
+ {
+ event,
+ notifyData,
+ },
+ ),
+ ).toBe(true);
+ });
+
+ it("rejects when clientId does not match", () => {
+ const notifyData = createMessageStatusData({ clientId: "client-2" });
+ const event = createBaseEvent(
+ EventTypes.MESSAGE_STATUS_PUBLISHED,
+ "source-a",
+ notifyData,
+ );
+ expect(
+ matchesMessageStatusSubscription(
+ createMessageStatusConfig(["DELIVERED"]),
+ {
+ event,
+ notifyData,
+ },
+ ),
+ ).toBe(false);
+ });
+
+ it("rejects when status does not match", () => {
+ const notifyData = createMessageStatusData({ messageStatus: "FAILED" });
+ const event = createBaseEvent(
+ EventTypes.MESSAGE_STATUS_PUBLISHED,
+ "source-a",
+ notifyData,
+ );
+ expect(
+ matchesMessageStatusSubscription(
+ createMessageStatusConfig(["DELIVERED"]),
+ {
+ event,
+ notifyData,
+ },
+ ),
+ ).toBe(false);
+ });
+
+ it("rejects when status has not changed", () => {
+ const notifyData = createMessageStatusData({
+ messageStatus: "DELIVERED",
+ previousMessageStatus: "DELIVERED",
+ });
+ const event = createBaseEvent(
+ EventTypes.MESSAGE_STATUS_PUBLISHED,
+ "source-a",
+ notifyData,
+ );
+ expect(
+ matchesMessageStatusSubscription(
+ createMessageStatusConfig(["DELIVERED"]),
+ {
+ event,
+ notifyData,
+ },
+ ),
+ ).toBe(false);
+ });
+
+ it("matches when status has changed", () => {
+ const notifyData = createMessageStatusData({
+ messageStatus: "DELIVERED",
+ previousMessageStatus: "PENDING_ENRICHMENT",
+ });
+ const event = createBaseEvent(
+ EventTypes.MESSAGE_STATUS_PUBLISHED,
+ "source-a",
+ notifyData,
+ );
+ expect(
+ matchesMessageStatusSubscription(
+ createMessageStatusConfig(["DELIVERED"]),
+ {
+ event,
+ notifyData,
+ },
+ ),
+ ).toBe(true);
+ });
+});
diff --git a/lambdas/client-transform-filter-lambda/src/__tests__/services/logger.test.ts b/lambdas/client-transform-filter-lambda/src/__tests__/services/logger.test.ts
index 6c94fbf..94f1f47 100644
--- a/lambdas/client-transform-filter-lambda/src/__tests__/services/logger.test.ts
+++ b/lambdas/client-transform-filter-lambda/src/__tests__/services/logger.test.ts
@@ -309,6 +309,16 @@ describe("extractCorrelationId", () => {
expect(correlationId).toBeUndefined();
});
+
+ it("should return undefined when id is present but not a string", () => {
+ const event = {
+ id: 42,
+ };
+
+ const correlationId = extractCorrelationId(event);
+
+ expect(correlationId).toBeUndefined();
+ });
});
describe("logLifecycleEvent", () => {
diff --git a/lambdas/client-transform-filter-lambda/src/__tests__/services/metrics.test.ts b/lambdas/client-transform-filter-lambda/src/__tests__/services/metrics.test.ts
index b0e4578..bdbcc3a 100644
--- a/lambdas/client-transform-filter-lambda/src/__tests__/services/metrics.test.ts
+++ b/lambdas/client-transform-filter-lambda/src/__tests__/services/metrics.test.ts
@@ -131,4 +131,28 @@ describe("CallbackMetrics", () => {
);
});
});
+
+ describe("emitFilteringStarted", () => {
+ it("should emit FilteringStarted metric", () => {
+ callbackMetrics.emitFilteringStarted();
+
+ expect(mockPutMetric).toHaveBeenCalledWith(
+ "FilteringStarted",
+ 1,
+ Unit.Count,
+ );
+ });
+ });
+
+ describe("emitFilteringMatched", () => {
+ it("should emit FilteringMatched metric", () => {
+ callbackMetrics.emitFilteringMatched();
+
+ expect(mockPutMetric).toHaveBeenCalledWith(
+ "FilteringMatched",
+ 1,
+ Unit.Count,
+ );
+ });
+ });
});
diff --git a/lambdas/client-transform-filter-lambda/src/__tests__/services/subscription-filter.test.ts b/lambdas/client-transform-filter-lambda/src/__tests__/services/subscription-filter.test.ts
new file mode 100644
index 0000000..2df22a2
--- /dev/null
+++ b/lambdas/client-transform-filter-lambda/src/__tests__/services/subscription-filter.test.ts
@@ -0,0 +1,242 @@
+import type {
+ Channel,
+ ChannelStatus,
+ ChannelStatusData,
+ ClientSubscriptionConfiguration,
+ MessageStatus,
+ MessageStatusData,
+ StatusPublishEvent,
+ SupplierStatus,
+} from "@nhs-notify-client-callbacks/models";
+import { EventTypes } from "@nhs-notify-client-callbacks/models";
+import { TransformationError } from "services/error-handler";
+import { evaluateSubscriptionFilters } from "services/subscription-filter";
+
+jest.mock("services/logger", () => ({
+ logger: {
+ debug: jest.fn(),
+ info: jest.fn(),
+ warn: jest.fn(),
+ error: jest.fn(),
+ },
+}));
+
+const createMessageStatusEvent = (
+ clientId: string,
+ status: MessageStatus,
+): StatusPublishEvent => ({
+ specversion: "1.0",
+ id: "event-id",
+ source: "source-a",
+ subject: "subject",
+ type: EventTypes.MESSAGE_STATUS_PUBLISHED,
+ time: "2025-01-01T10:00:00Z",
+ datacontenttype: "application/json",
+ dataschema: "schema",
+ traceparent: "traceparent",
+ data: {
+ messageId: "msg-123",
+ messageReference: "ref-123",
+ messageStatus: status,
+ channels: [],
+ timestamp: "2025-01-01T10:00:00Z",
+ routingPlan: {
+ id: "plan-id",
+ name: "plan-name",
+ version: "1",
+ createdDate: "2025-01-01T10:00:00Z",
+ },
+ clientId,
+ },
+});
+
+const createChannelStatusEvent = (
+ clientId: string,
+ channel: Channel,
+ channelStatus: ChannelStatus,
+ supplierStatus: SupplierStatus,
+ previousChannelStatus?: ChannelStatus,
+ previousSupplierStatus?: SupplierStatus,
+): StatusPublishEvent => ({
+ specversion: "1.0",
+ id: "event-id",
+ source: "source-a",
+ subject: "subject",
+ type: EventTypes.CHANNEL_STATUS_PUBLISHED,
+ time: "2025-01-01T10:00:00Z",
+ datacontenttype: "application/json",
+ dataschema: "schema",
+ traceparent: "traceparent",
+ data: {
+ messageId: "msg-123",
+ messageReference: "ref-123",
+ channel,
+ channelStatus,
+ previousChannelStatus,
+ supplierStatus,
+ previousSupplierStatus,
+ cascadeType: "primary" as const,
+ cascadeOrder: 1,
+ timestamp: "2025-01-01T10:00:00Z",
+ retryCount: 0,
+ clientId,
+ },
+});
+
+const createMessageStatusConfig = (
+ clientId: string,
+ statuses: MessageStatus[],
+): ClientSubscriptionConfiguration => [
+ {
+ SubscriptionId: "00000000-0000-0000-0000-000000000001",
+ ClientId: clientId,
+ Targets: [
+ {
+ Type: "API",
+ TargetId: "target",
+ InvocationEndpoint: "https://example.com",
+ InvocationMethod: "POST",
+ InvocationRateLimit: 10,
+ APIKey: {
+ HeaderName: "x-api-key",
+ HeaderValue: "secret",
+ },
+ },
+ ],
+ SubscriptionType: "MessageStatus",
+ MessageStatuses: statuses,
+ },
+];
+
+const createChannelStatusConfig = (
+ clientId: string,
+ channelType: Channel,
+ channelStatuses: ChannelStatus[],
+ supplierStatuses: SupplierStatus[],
+): ClientSubscriptionConfiguration => [
+ {
+ SubscriptionId: "00000000-0000-0000-0000-000000000002",
+ ClientId: clientId,
+ Targets: [
+ {
+ Type: "API",
+ TargetId: "target",
+ InvocationEndpoint: "https://example.com",
+ InvocationMethod: "POST",
+ InvocationRateLimit: 10,
+ APIKey: {
+ HeaderName: "x-api-key",
+ HeaderValue: "secret",
+ },
+ },
+ ],
+ SubscriptionType: "ChannelStatus",
+ ChannelType: channelType,
+ ChannelStatuses: channelStatuses,
+ SupplierStatuses: supplierStatuses,
+ },
+];
+
+describe("evaluateSubscriptionFilters", () => {
+ describe("when config is undefined", () => {
+ it("returns not matched with Unknown subscription type", () => {
+ const event = createMessageStatusEvent("client-1", "DELIVERED");
+ const result = evaluateSubscriptionFilters(event, undefined);
+
+ expect(result).toEqual({
+ matched: false,
+ subscriptionType: "Unknown",
+ });
+ });
+ });
+
+ describe("when event is MessageStatus", () => {
+ it("returns matched true when status matches subscription", () => {
+ const event = createMessageStatusEvent("client-1", "DELIVERED");
+ const config = createMessageStatusConfig("client-1", ["DELIVERED"]);
+
+ const result = evaluateSubscriptionFilters(event, config);
+
+ expect(result).toEqual({
+ matched: true,
+ subscriptionType: "MessageStatus",
+ });
+ });
+
+ it("returns matched false when status does not match subscription", () => {
+ const event = createMessageStatusEvent("client-1", "FAILED");
+ const config = createMessageStatusConfig("client-1", ["DELIVERED"]);
+
+ const result = evaluateSubscriptionFilters(event, config);
+
+ expect(result).toEqual({
+ matched: false,
+ subscriptionType: "MessageStatus",
+ });
+ });
+ });
+
+ describe("when event is ChannelStatus", () => {
+ it("returns matched true when channel and statuses match subscription", () => {
+ const event = createChannelStatusEvent(
+ "client-1",
+ "EMAIL",
+ "DELIVERED",
+ "delivered",
+ "SENDING",
+ "notified",
+ );
+ const config = createChannelStatusConfig(
+ "client-1",
+ "EMAIL",
+ ["DELIVERED"],
+ ["delivered"],
+ );
+
+ const result = evaluateSubscriptionFilters(event, config);
+
+ expect(result).toEqual({
+ matched: true,
+ subscriptionType: "ChannelStatus",
+ });
+ });
+
+ it("returns matched false when channel status does not match subscription", () => {
+ const event = createChannelStatusEvent(
+ "client-1",
+ "EMAIL",
+ "FAILED",
+ "delivered",
+ "FAILED", // previousChannelStatus (no change)
+ "delivered", // previousSupplierStatus (no change)
+ );
+ const config = createChannelStatusConfig(
+ "client-1",
+ "EMAIL",
+ ["DELIVERED"],
+ ["delivered"],
+ );
+
+ const result = evaluateSubscriptionFilters(event, config);
+
+ expect(result).toEqual({
+ matched: false,
+ subscriptionType: "ChannelStatus",
+ });
+ });
+ });
+
+ describe("when event type is unknown", () => {
+ it("throws a TransformationError", () => {
+ const event = {
+ ...createMessageStatusEvent("client-1", "DELIVERED"),
+ type: "unknown-event-type",
+ } as StatusPublishEvent;
+ const config = createMessageStatusConfig("client-1", ["DELIVERED"]);
+
+ expect(() => evaluateSubscriptionFilters(event, config)).toThrow(
+ new TransformationError("Unsupported event type: unknown-event-type"),
+ );
+ });
+ });
+});
diff --git a/lambdas/client-transform-filter-lambda/src/__tests__/transformers/event-transformer.test.ts b/lambdas/client-transform-filter-lambda/src/__tests__/transformers/event-transformer.test.ts
new file mode 100644
index 0000000..49e3158
--- /dev/null
+++ b/lambdas/client-transform-filter-lambda/src/__tests__/transformers/event-transformer.test.ts
@@ -0,0 +1,106 @@
+import {
+ type ChannelStatusData,
+ type MessageStatusData,
+ type StatusPublishEvent,
+} from "@nhs-notify-client-callbacks/models";
+import { TransformationError } from "services/error-handler";
+import { transformEvent } from "services/transformers/event-transformer";
+
+const baseEvent = {
+ specversion: "1.0",
+ source: "/nhs/england/notify/development/primary/data-plane/messaging",
+ subject: "customer/client-abc-123/message/msg-789-xyz",
+ time: "2026-02-05T14:30:00.000Z",
+ datacontenttype: "application/json",
+ traceparent: "00-4d678967f96e353c07a0a31c1849b500-07f83ba58dd8df70-01",
+};
+
+const messageStatusEvent: StatusPublishEvent = {
+ ...baseEvent,
+ id: "msg-event-id-001",
+ dataschema: "https://notify.nhs.uk/schemas/message-status-published-v1.json",
+ type: "uk.nhs.notify.message.status.PUBLISHED.v1",
+ data: {
+ clientId: "client-abc-123",
+ messageId: "msg-789-xyz",
+ messageReference: "client-ref-12345",
+ messageStatus: "DELIVERED",
+ channels: [{ type: "NHSAPP", channelStatus: "DELIVERED" }],
+ timestamp: "2026-02-05T14:29:55Z",
+ routingPlan: {
+ id: "routing-plan-123",
+ name: "NHS App",
+ version: "v1",
+ createdDate: "2023-11-17T14:27:51.413Z",
+ },
+ },
+};
+
+const channelStatusEvent: StatusPublishEvent = {
+ ...baseEvent,
+ id: "ch-event-id-001",
+ dataschema: "https://notify.nhs.uk/schemas/channel-status-published-v1.json",
+ type: "uk.nhs.notify.channel.status.PUBLISHED.v1",
+ data: {
+ clientId: "client-abc-123",
+ messageId: "msg-789-xyz",
+ messageReference: "client-ref-12345",
+ channel: "NHSAPP",
+ channelStatus: "DELIVERED",
+ supplierStatus: "delivered",
+ cascadeType: "primary",
+ cascadeOrder: 1,
+ retryCount: 0,
+ timestamp: "2026-02-05T14:29:55Z",
+ },
+};
+
+describe("event-transformer", () => {
+ describe("transformEvent", () => {
+ it("transforms a message status event", () => {
+ const result = transformEvent(messageStatusEvent, "corr-id-001");
+
+ expect(result.data[0].type).toBe("MessageStatus");
+ });
+
+ it("transforms a channel status event", () => {
+ const result = transformEvent(channelStatusEvent, "corr-id-002");
+
+ expect(result.data[0].type).toBe("ChannelStatus");
+ });
+
+ it("throws TransformationError for unsupported event type", () => {
+ const unsupportedEvent = {
+ ...messageStatusEvent,
+ type: "uk.nhs.notify.unsupported.event.v1",
+ } as unknown as StatusPublishEvent;
+
+ expect(() => transformEvent(unsupportedEvent, "corr-id-003")).toThrow(
+ TransformationError,
+ );
+
+ expect(() => transformEvent(unsupportedEvent, "corr-id-003")).toThrow(
+ "Unsupported event type: uk.nhs.notify.unsupported.event.v1",
+ );
+ });
+
+ it("includes correlationId in TransformationError when provided", () => {
+ const unsupportedEvent = {
+ ...messageStatusEvent,
+ type: "uk.nhs.notify.unknown.v1",
+ } as unknown as StatusPublishEvent;
+
+ let caughtError: unknown;
+ try {
+ transformEvent(unsupportedEvent, "test-correlation-id");
+ } catch (error) {
+ caughtError = error;
+ }
+
+ expect(caughtError).toBeInstanceOf(TransformationError);
+ expect((caughtError as TransformationError).message).toBe(
+ "Unsupported event type: uk.nhs.notify.unknown.v1",
+ );
+ });
+ });
+});
diff --git a/lambdas/client-transform-filter-lambda/src/__tests__/validators/config-validator.test.ts b/lambdas/client-transform-filter-lambda/src/__tests__/validators/config-validator.test.ts
new file mode 100644
index 0000000..ad4f680
--- /dev/null
+++ b/lambdas/client-transform-filter-lambda/src/__tests__/validators/config-validator.test.ts
@@ -0,0 +1,81 @@
+import type { ClientSubscriptionConfiguration } from "@nhs-notify-client-callbacks/models";
+import {
+ ConfigValidationError,
+ validateClientConfig,
+} from "services/validators/config-validator";
+
+const createValidConfig = (): ClientSubscriptionConfiguration => [
+ {
+ SubscriptionId: "00000000-0000-0000-0000-000000000001",
+ ClientId: "client-1",
+ Targets: [
+ {
+ Type: "API",
+ TargetId: "target",
+ InvocationEndpoint: "https://example.com",
+ InvocationMethod: "POST",
+ InvocationRateLimit: 10,
+ APIKey: {
+ HeaderName: "x-api-key",
+ HeaderValue: "secret",
+ },
+ },
+ ],
+ SubscriptionType: "MessageStatus",
+ MessageStatuses: ["DELIVERED"],
+ },
+ {
+ SubscriptionId: "00000000-0000-0000-0000-000000000002",
+ ClientId: "client-1",
+ Targets: [
+ {
+ Type: "API",
+ TargetId: "target",
+ InvocationEndpoint: "https://example.com",
+ InvocationMethod: "POST",
+ InvocationRateLimit: 10,
+ APIKey: {
+ HeaderName: "x-api-key",
+ HeaderValue: "secret",
+ },
+ },
+ ],
+ SubscriptionType: "ChannelStatus",
+ ChannelType: "EMAIL",
+ ChannelStatuses: ["DELIVERED"],
+ SupplierStatuses: ["read"],
+ },
+];
+
+describe("validateClientConfig", () => {
+ it("returns the config when valid", () => {
+ const config = createValidConfig();
+
+ expect(validateClientConfig(config)).toEqual(config);
+ });
+
+ it("throws when config is not an array", () => {
+ expect(() => validateClientConfig({})).toThrow(ConfigValidationError);
+ });
+
+ it("throws when invocation endpoint is not https", () => {
+ const config = createValidConfig();
+ config[0].Targets[0].InvocationEndpoint = "http://example.com";
+
+ expect(() => validateClientConfig(config)).toThrow(ConfigValidationError);
+ });
+
+ it("throws when subscription IDs are not unique", () => {
+ const config = createValidConfig();
+ config[1].SubscriptionId = config[0].SubscriptionId;
+
+ expect(() => validateClientConfig(config)).toThrow(ConfigValidationError);
+ });
+
+ it("throws when InvocationEndpoint is not a valid URL", () => {
+ const config = createValidConfig();
+ config[0].Targets[0].InvocationEndpoint = "not-a-url";
+
+ expect(() => validateClientConfig(config)).toThrow(ConfigValidationError);
+ });
+});
diff --git a/lambdas/client-transform-filter-lambda/src/__tests__/validators/event-validator.test.ts b/lambdas/client-transform-filter-lambda/src/__tests__/validators/event-validator.test.ts
index ec9d424..8792bc4 100644
--- a/lambdas/client-transform-filter-lambda/src/__tests__/validators/event-validator.test.ts
+++ b/lambdas/client-transform-filter-lambda/src/__tests__/validators/event-validator.test.ts
@@ -70,7 +70,7 @@ describe("event-validator", () => {
};
expect(() => validateStatusPublishEvent(invalidEvent)).toThrow(
- 'Validation failed: type: Invalid option: expected one of "uk.nhs.notify.message.status.PUBLISHED.v1"|"uk.nhs.notify.channel.status.PUBLISHED.v1"',
+ "Validation failed: type: Invalid option",
);
});
});
@@ -199,7 +199,7 @@ describe("event-validator", () => {
};
expect(() => validateStatusPublishEvent(invalidEvent)).toThrow(
- "Validation failed: channels.0.type: Invalid input: expected string, received undefined",
+ "Validation failed: channels[0].type: Invalid input: expected string, received undefined",
);
});
@@ -213,7 +213,7 @@ describe("event-validator", () => {
};
expect(() => validateStatusPublishEvent(invalidEvent)).toThrow(
- "Validation failed: channels.0.channelStatus: Invalid input: expected string, received undefined",
+ "Validation failed: channels[0].channelStatus: Invalid input: expected string, received undefined",
);
});
});
@@ -318,6 +318,34 @@ describe("event-validator", () => {
jest.unmock("cloudevents");
});
+
+ it("should format generic Error exceptions during validation", () => {
+ jest.resetModules();
+
+ jest.isolateModules(() => {
+ const { ValidationError: RealCloudEventsValidationError } =
+ jest.requireActual("cloudevents");
+
+ jest.doMock("cloudevents", () => ({
+ CloudEvent: jest.fn(() => {
+ throw new Error("generic processing error");
+ }),
+ ValidationError: RealCloudEventsValidationError,
+ }));
+
+ const moduleUnderTest = jest.requireActual(
+ "services/validators/event-validator",
+ );
+
+ expect(() =>
+ moduleUnderTest.validateStatusPublishEvent({
+ specversion: "1.0",
+ }),
+ ).toThrow("generic processing error");
+ });
+
+ jest.unmock("cloudevents");
+ });
});
});
});
diff --git a/lambdas/client-transform-filter-lambda/src/handler.ts b/lambdas/client-transform-filter-lambda/src/handler.ts
index 3f2dc6a..cb53fd5 100644
--- a/lambdas/client-transform-filter-lambda/src/handler.ts
+++ b/lambdas/client-transform-filter-lambda/src/handler.ts
@@ -9,6 +9,8 @@ import { transformEvent } from "services/transformers/event-transformer";
import { extractCorrelationId } from "services/logger";
import { ValidationError, getEventError } from "services/error-handler";
import type { ObservabilityService } from "services/observability";
+import type { ConfigLoader } from "services/config-loader";
+import { evaluateSubscriptionFilters } from "services/subscription-filter";
const BATCH_CONCURRENCY = Number(process.env.BATCH_CONCURRENCY) || 10;
@@ -21,6 +23,8 @@ class BatchStats {
failed = 0;
+ filtered = 0;
+
processed = 0;
recordSuccess(): void {
@@ -33,10 +37,15 @@ class BatchStats {
this.processed += 1;
}
+ recordFiltered(): void {
+ this.filtered += 1;
+ }
+
toObject() {
return {
successful: this.successful,
failed: this.failed,
+ filtered: this.filtered,
processed: this.processed,
};
}
@@ -117,6 +126,62 @@ function recordDeliveryInitiated(
}
}
+async function filterBatch(
+ transformedEvents: TransformedEvent[],
+ configLoader: ConfigLoader,
+ observability: ObservabilityService,
+ stats: BatchStats,
+): Promise {
+ observability.recordFilteringStarted({ batchSize: transformedEvents.length });
+
+ const uniqueClientIds = new Set(
+ transformedEvents.map((e) => e.data.clientId),
+ );
+
+ const configEntries = await pMap(
+ uniqueClientIds,
+ async (clientId) => {
+ const config = await configLoader.loadClientConfig(clientId);
+ return [clientId, config] as const;
+ },
+ { concurrency: BATCH_CONCURRENCY },
+ );
+
+ const configByClientId = new Map(configEntries);
+
+ const filtered: TransformedEvent[] = [];
+
+ for (const event of transformedEvents) {
+ const { clientId } = event.data;
+ const config = configByClientId.get(clientId);
+ const filterResult = evaluateSubscriptionFilters(event, config);
+
+ if (filterResult.matched) {
+ filtered.push(event);
+ const targetIds = config?.flatMap((s) =>
+ s.Targets.map((t) => t.TargetId),
+ );
+ observability.recordFilteringMatched({
+ clientId,
+ eventType: event.type,
+ subscriptionType: filterResult.subscriptionType,
+ targetIds,
+ });
+ } else {
+ stats.recordFiltered();
+ observability
+ .getLogger()
+ .info("Event filtered out - no matching subscription", {
+ clientId,
+ eventType: event.type,
+ subscriptionType: filterResult.subscriptionType,
+ });
+ }
+ }
+
+ return filtered;
+}
+
async function transformBatch(
sqsRecords: SQSRecord[],
observability: ObservabilityService,
@@ -146,6 +211,7 @@ async function transformBatch(
export async function processEvents(
event: SQSRecord[],
observability: ObservabilityService,
+ configLoader: ConfigLoader,
): Promise {
const startTime = Date.now();
const stats = new BatchStats();
@@ -153,6 +219,13 @@ export async function processEvents(
try {
const transformedEvents = await transformBatch(event, observability, stats);
+ const filteredEvents = await filterBatch(
+ transformedEvents,
+ configLoader,
+ observability,
+ stats,
+ );
+
const processingTime = Date.now() - startTime;
observability.logBatchProcessingCompleted({
...stats.toObject(),
@@ -160,10 +233,10 @@ export async function processEvents(
processingTimeMs: processingTime,
});
- recordDeliveryInitiated(transformedEvents, observability);
+ recordDeliveryInitiated(filteredEvents, observability);
await observability.flush();
- return transformedEvents;
+ return filteredEvents;
} catch (error) {
stats.recordFailure();
diff --git a/lambdas/client-transform-filter-lambda/src/index.ts b/lambdas/client-transform-filter-lambda/src/index.ts
index 757b83c..5ef8e19 100644
--- a/lambdas/client-transform-filter-lambda/src/index.ts
+++ b/lambdas/client-transform-filter-lambda/src/index.ts
@@ -2,10 +2,14 @@ import type { SQSRecord } from "aws-lambda";
import { Logger } from "services/logger";
import { CallbackMetrics, createMetricLogger } from "services/metrics";
import { ObservabilityService } from "services/observability";
+import { ConfigLoaderService } from "services/config-loader-service";
import { type TransformedEvent, processEvents } from "handler";
+export const configLoaderService = new ConfigLoaderService();
+
export interface HandlerDependencies {
- createObservabilityService: () => ObservabilityService;
+ createObservabilityService?: () => ObservabilityService;
+ createConfigLoaderService?: () => ConfigLoaderService;
}
function createDefaultObservabilityService(): ObservabilityService {
@@ -16,16 +20,23 @@ function createDefaultObservabilityService(): ObservabilityService {
return new ObservabilityService(logger, metrics, metricsLogger);
}
+function createDefaultConfigLoaderService(): ConfigLoaderService {
+ return configLoaderService;
+}
+
export function createHandler(
dependencies: Partial = {},
): (event: SQSRecord[]) => Promise {
const createObservabilityService =
dependencies.createObservabilityService ??
createDefaultObservabilityService;
+ const configLoader = (
+ dependencies.createConfigLoaderService ?? createDefaultConfigLoaderService
+ )();
return async (event: SQSRecord[]): Promise => {
const observability = createObservabilityService();
- return processEvents(event, observability);
+ return processEvents(event, observability, configLoader.getLoader());
};
}
diff --git a/lambdas/client-transform-filter-lambda/src/services/config-cache.ts b/lambdas/client-transform-filter-lambda/src/services/config-cache.ts
new file mode 100644
index 0000000..e371fdd
--- /dev/null
+++ b/lambdas/client-transform-filter-lambda/src/services/config-cache.ts
@@ -0,0 +1,37 @@
+import type { ClientSubscriptionConfiguration } from "@nhs-notify-client-callbacks/models";
+
+type CacheEntry = {
+ value: ClientSubscriptionConfiguration;
+ expiresAt: number;
+};
+
+export class ConfigCache {
+ private readonly cache = new Map();
+
+ constructor(private readonly ttlMs: number) {}
+
+ get(clientId: string): ClientSubscriptionConfiguration | undefined {
+ const entry = this.cache.get(clientId);
+ if (!entry) {
+ return undefined;
+ }
+
+ if (entry.expiresAt <= Date.now()) {
+ this.cache.delete(clientId);
+ return undefined;
+ }
+
+ return entry.value;
+ }
+
+ set(clientId: string, value: ClientSubscriptionConfiguration): void {
+ this.cache.set(clientId, {
+ value,
+ expiresAt: Date.now() + this.ttlMs,
+ });
+ }
+
+ clear(): void {
+ this.cache.clear();
+ }
+}
diff --git a/lambdas/client-transform-filter-lambda/src/services/config-loader-service.ts b/lambdas/client-transform-filter-lambda/src/services/config-loader-service.ts
new file mode 100644
index 0000000..b0af71b
--- /dev/null
+++ b/lambdas/client-transform-filter-lambda/src/services/config-loader-service.ts
@@ -0,0 +1,77 @@
+import { S3Client } from "@aws-sdk/client-s3";
+import { ConfigCache } from "services/config-cache";
+import { ConfigLoader } from "services/config-loader";
+
+const DEFAULT_CACHE_TTL_SECONDS = 60;
+
+export const resolveCacheTtlMs = (
+ env: NodeJS.ProcessEnv = process.env,
+): number => {
+ const configuredTtlSeconds = Number.parseInt(
+ env.CLIENT_SUBSCRIPTION_CACHE_TTL_SECONDS ?? `${DEFAULT_CACHE_TTL_SECONDS}`,
+ 10,
+ );
+ const cacheTtlSeconds = Number.isFinite(configuredTtlSeconds)
+ ? configuredTtlSeconds
+ : DEFAULT_CACHE_TTL_SECONDS;
+ return cacheTtlSeconds * 1000;
+};
+
+export const createS3Client = (
+ env: NodeJS.ProcessEnv = process.env,
+): S3Client => {
+ const endpoint = env.AWS_ENDPOINT_URL;
+ const forcePathStyle = endpoint?.includes("localhost") ? true : undefined;
+ return new S3Client({ endpoint, forcePathStyle });
+};
+
+export class ConfigLoaderService {
+ private readonly cache: ConfigCache;
+
+ private loader: ConfigLoader | undefined;
+
+ constructor(cacheTtlMs: number = resolveCacheTtlMs()) {
+ this.cache = new ConfigCache(cacheTtlMs);
+ }
+
+ getLoader(): ConfigLoader {
+ const bucketName = process.env.CLIENT_SUBSCRIPTION_CONFIG_BUCKET;
+ if (!bucketName) {
+ throw new Error("CLIENT_SUBSCRIPTION_CONFIG_BUCKET is required");
+ }
+
+ if (this.loader) {
+ return this.loader;
+ }
+
+ this.loader = new ConfigLoader({
+ bucketName,
+ keyPrefix:
+ process.env.CLIENT_SUBSCRIPTION_CONFIG_PREFIX ??
+ "client_subscriptions/",
+ s3Client: createS3Client(),
+ cache: this.cache,
+ });
+
+ return this.loader;
+ }
+
+ reset(s3Client?: S3Client): void {
+ this.loader = undefined;
+ this.cache.clear();
+ if (s3Client) {
+ const bucketName = process.env.CLIENT_SUBSCRIPTION_CONFIG_BUCKET;
+ if (!bucketName) {
+ throw new Error("CLIENT_SUBSCRIPTION_CONFIG_BUCKET is required");
+ }
+ this.loader = new ConfigLoader({
+ bucketName,
+ keyPrefix:
+ process.env.CLIENT_SUBSCRIPTION_CONFIG_PREFIX ??
+ "client_subscriptions/",
+ s3Client,
+ cache: this.cache,
+ });
+ }
+ }
+}
diff --git a/lambdas/client-transform-filter-lambda/src/services/config-loader.ts b/lambdas/client-transform-filter-lambda/src/services/config-loader.ts
new file mode 100644
index 0000000..d95e141
--- /dev/null
+++ b/lambdas/client-transform-filter-lambda/src/services/config-loader.ts
@@ -0,0 +1,81 @@
+import { GetObjectCommand, NoSuchKey, S3Client } from "@aws-sdk/client-s3";
+import type { ClientSubscriptionConfiguration } from "@nhs-notify-client-callbacks/models";
+import { ConfigCache } from "services/config-cache";
+import { logger } from "services/logger";
+import {
+ ConfigValidationError,
+ validateClientConfig,
+} from "services/validators/config-validator";
+
+type ConfigLoaderOptions = {
+ bucketName: string;
+ keyPrefix: string;
+ s3Client: S3Client;
+ cache: ConfigCache;
+};
+
+function throwAsConfigError(error: unknown, clientId: string): never {
+ if (error instanceof ConfigValidationError) {
+ logger.error("Config validation failed with schema violations", {
+ clientId,
+ validationErrors: error.issues,
+ });
+ throw error;
+ }
+
+ const message = error instanceof Error ? error.message : String(error);
+ logger.error("Failed to load config from S3", { clientId });
+ throw new ConfigValidationError([{ path: "config", message }]);
+}
+
+export class ConfigLoader {
+ constructor(private readonly options: ConfigLoaderOptions) {}
+
+ async loadClientConfig(
+ clientId: string,
+ ): Promise {
+ const cached = this.options.cache.get(clientId);
+ if (cached) {
+ logger.debug("Config loaded from cache", { clientId, cacheHit: true });
+ return cached;
+ }
+
+ logger.debug("Config not in cache, fetching from S3", {
+ clientId,
+ cacheHit: false,
+ });
+
+ try {
+ const response = await this.options.s3Client.send(
+ new GetObjectCommand({
+ Bucket: this.options.bucketName,
+ Key: `${this.options.keyPrefix}${clientId}.json`,
+ }),
+ );
+
+ if (!response.Body) {
+ throw new Error("S3 response body was empty");
+ }
+
+ const rawConfig = await response.Body.transformToString();
+ const parsedConfig = JSON.parse(rawConfig) as unknown;
+ const validated = validateClientConfig(parsedConfig);
+ this.options.cache.set(clientId, validated);
+ logger.info("Config loaded successfully from S3", {
+ clientId,
+ subscriptionCount: validated.length,
+ });
+ return validated;
+ } catch (error) {
+ if (error instanceof NoSuchKey) {
+ logger.info(
+ "No config found in S3 for client - events will be filtered out",
+ { clientId },
+ );
+ return undefined;
+ }
+ throwAsConfigError(error, clientId);
+ return undefined;
+ }
+ }
+}
diff --git a/lambdas/client-transform-filter-lambda/src/services/error-handler.ts b/lambdas/client-transform-filter-lambda/src/services/error-handler.ts
index 26e99d2..8eef206 100644
--- a/lambdas/client-transform-filter-lambda/src/services/error-handler.ts
+++ b/lambdas/client-transform-filter-lambda/src/services/error-handler.ts
@@ -44,6 +44,38 @@ export class TransformationError extends LambdaError {
}
}
+export type ValidationIssue = {
+ path: string;
+ message: string;
+};
+
+export function formatValidationIssuePath(path: (string | number)[]): string {
+ let formatted = "";
+
+ for (const segment of path) {
+ if (typeof segment === "number") {
+ formatted = `${formatted}[${segment}]`;
+ } else if (formatted) {
+ formatted = `${formatted}.${segment}`;
+ } else {
+ formatted = segment;
+ }
+ }
+
+ return formatted;
+}
+
+export class ConfigValidationError extends LambdaError {
+ constructor(public readonly issues: ValidationIssue[]) {
+ super(
+ ErrorType.VALIDATION_ERROR,
+ "Client subscription configuration validation failed",
+ undefined,
+ false,
+ );
+ }
+}
+
function serializeUnknownError(error: unknown): string {
if (typeof error === "string") {
return error;
@@ -104,6 +136,14 @@ export function getEventError(
? error.correlationId
: "unknown";
+ if (error instanceof ConfigValidationError) {
+ eventLogger.error("Client config validation failed", {
+ error,
+ });
+ metrics.emitValidationError();
+ return error;
+ }
+
if (error instanceof ValidationError) {
eventLogger.error("Event validation failed", {
correlationId,
diff --git a/lambdas/client-transform-filter-lambda/src/services/filters/channel-status-filter.ts b/lambdas/client-transform-filter-lambda/src/services/filters/channel-status-filter.ts
new file mode 100644
index 0000000..23a287f
--- /dev/null
+++ b/lambdas/client-transform-filter-lambda/src/services/filters/channel-status-filter.ts
@@ -0,0 +1,92 @@
+import type {
+ ChannelStatusData,
+ ChannelStatusSubscriptionConfiguration,
+ ClientSubscriptionConfiguration,
+ StatusPublishEvent,
+} from "@nhs-notify-client-callbacks/models";
+import { logger } from "services/logger";
+
+type FilterContext = {
+ event: StatusPublishEvent;
+ notifyData: ChannelStatusData;
+};
+
+const isChannelStatusSubscription = (
+ subscription: ClientSubscriptionConfiguration[number],
+): subscription is ChannelStatusSubscriptionConfiguration =>
+ subscription.SubscriptionType === "ChannelStatus";
+
+export const matchesChannelStatusSubscription = (
+ config: ClientSubscriptionConfiguration,
+ context: FilterContext,
+): boolean => {
+ const { notifyData } = context;
+
+ const matched = config
+ .filter((sub) => isChannelStatusSubscription(sub))
+ .some((subscription) => {
+ if (subscription.ClientId !== notifyData.clientId) {
+ return false;
+ }
+
+ if (subscription.ChannelType !== notifyData.channel) {
+ logger.debug("Channel status filter rejected: channel type mismatch", {
+ clientId: notifyData.clientId,
+ channel: notifyData.channel,
+ expectedChannel: subscription.ChannelType,
+ });
+ return false;
+ }
+
+ // Check if supplier status changed AND client is subscribed to it
+ const supplierStatusChanged =
+ notifyData.previousSupplierStatus !== notifyData.supplierStatus;
+ const clientSubscribedSupplierStatus =
+ subscription.SupplierStatuses.includes(notifyData.supplierStatus);
+
+ // Check if channel status changed AND client is subscribed to it
+ const channelStatusChanged =
+ notifyData.previousChannelStatus !== notifyData.channelStatus;
+ const clientSubscribedChannelStatus =
+ subscription.ChannelStatuses.includes(notifyData.channelStatus);
+
+ const statusMatch =
+ (supplierStatusChanged && clientSubscribedSupplierStatus) ||
+ (channelStatusChanged && clientSubscribedChannelStatus);
+
+ if (!statusMatch) {
+ logger.debug(
+ "Channel status filter rejected: no matching status change for subscription",
+ {
+ clientId: notifyData.clientId,
+ channelStatus: notifyData.channelStatus,
+ previousChannelStatus: notifyData.previousChannelStatus,
+ channelStatusChanged,
+ clientSubscribedChannelStatus,
+ supplierStatus: notifyData.supplierStatus,
+ previousSupplierStatus: notifyData.previousSupplierStatus,
+ supplierStatusChanged,
+ clientSubscribedSupplierStatus,
+ subscribedChannelStatuses: subscription.ChannelStatuses,
+ subscribedSupplierStatuses: subscription.SupplierStatuses,
+ },
+ );
+ return false;
+ }
+
+ return true;
+ });
+
+ if (matched) {
+ logger.debug("Channel status filter matched", {
+ clientId: notifyData.clientId,
+ channel: notifyData.channel,
+ channelStatus: notifyData.channelStatus,
+ previousChannelStatus: notifyData.previousChannelStatus,
+ supplierStatus: notifyData.supplierStatus,
+ previousSupplierStatus: notifyData.previousSupplierStatus,
+ });
+ }
+
+ return matched;
+};
diff --git a/lambdas/client-transform-filter-lambda/src/services/filters/message-status-filter.ts b/lambdas/client-transform-filter-lambda/src/services/filters/message-status-filter.ts
new file mode 100644
index 0000000..e79c33a
--- /dev/null
+++ b/lambdas/client-transform-filter-lambda/src/services/filters/message-status-filter.ts
@@ -0,0 +1,65 @@
+import type {
+ ClientSubscriptionConfiguration,
+ MessageStatusData,
+ MessageStatusSubscriptionConfiguration,
+ StatusPublishEvent,
+} from "@nhs-notify-client-callbacks/models";
+import { logger } from "services/logger";
+
+type FilterContext = {
+ event: StatusPublishEvent;
+ notifyData: MessageStatusData;
+};
+
+const isMessageStatusSubscription = (
+ subscription: ClientSubscriptionConfiguration[number],
+): subscription is MessageStatusSubscriptionConfiguration =>
+ subscription.SubscriptionType === "MessageStatus";
+
+export const matchesMessageStatusSubscription = (
+ config: ClientSubscriptionConfiguration,
+ context: FilterContext,
+): boolean => {
+ const { notifyData } = context;
+
+ const matched = config
+ .filter((sub) => isMessageStatusSubscription(sub))
+ .some((subscription) => {
+ if (subscription.ClientId !== notifyData.clientId) {
+ return false;
+ }
+
+ // Check if message status changed AND client is subscribed to it
+ const messageStatusChanged =
+ notifyData.previousMessageStatus !== notifyData.messageStatus;
+ const clientSubscribedStatus = subscription.MessageStatuses.includes(
+ notifyData.messageStatus,
+ );
+
+ if (!messageStatusChanged || !clientSubscribedStatus) {
+ logger.debug(
+ "Message status filter rejected: no matching status change for subscription",
+ {
+ clientId: notifyData.clientId,
+ messageStatus: notifyData.messageStatus,
+ previousMessageStatus: notifyData.previousMessageStatus,
+ messageStatusChanged,
+ clientSubscribedStatus,
+ expectedStatuses: subscription.MessageStatuses,
+ },
+ );
+ return false;
+ }
+
+ return true;
+ });
+
+ if (matched) {
+ logger.debug("Message status filter matched", {
+ clientId: notifyData.clientId,
+ messageStatus: notifyData.messageStatus,
+ });
+ }
+
+ return matched;
+};
diff --git a/lambdas/client-transform-filter-lambda/src/services/logger.ts b/lambdas/client-transform-filter-lambda/src/services/logger.ts
index d627641..bc6e389 100644
--- a/lambdas/client-transform-filter-lambda/src/services/logger.ts
+++ b/lambdas/client-transform-filter-lambda/src/services/logger.ts
@@ -82,6 +82,8 @@ export function logLifecycleEvent(
| "processing-started"
| "transformation-started"
| "transformation-completed"
+ | "filtering-started"
+ | "filtering-matched"
| "delivery-initiated"
| "batch-processing-completed",
context: LogContext,
diff --git a/lambdas/client-transform-filter-lambda/src/services/metrics.ts b/lambdas/client-transform-filter-lambda/src/services/metrics.ts
index f77b487..398c5ec 100644
--- a/lambdas/client-transform-filter-lambda/src/services/metrics.ts
+++ b/lambdas/client-transform-filter-lambda/src/services/metrics.ts
@@ -39,4 +39,12 @@ export class CallbackMetrics {
emitValidationError(): void {
this.metrics.putMetric("ValidationErrors", 1, Unit.Count);
}
+
+ emitFilteringStarted(): void {
+ this.metrics.putMetric("FilteringStarted", 1, Unit.Count);
+ }
+
+ emitFilteringMatched(): void {
+ this.metrics.putMetric("FilteringMatched", 1, Unit.Count);
+ }
}
diff --git a/lambdas/client-transform-filter-lambda/src/services/observability.ts b/lambdas/client-transform-filter-lambda/src/services/observability.ts
index bb6126d..e1ee13d 100644
--- a/lambdas/client-transform-filter-lambda/src/services/observability.ts
+++ b/lambdas/client-transform-filter-lambda/src/services/observability.ts
@@ -39,9 +39,25 @@ export class ObservabilityService {
logLifecycleEvent(this.logger, "transformation-started", context);
}
+ recordFilteringStarted(context: { batchSize: number }): void {
+ logLifecycleEvent(this.logger, "filtering-started", context);
+ this.metrics.emitFilteringStarted();
+ }
+
+ recordFilteringMatched(context: {
+ clientId: string;
+ eventType: string;
+ subscriptionType: string;
+ targetIds?: string[];
+ }): void {
+ logLifecycleEvent(this.logger, "filtering-matched", context);
+ this.metrics.emitFilteringMatched();
+ }
+
logBatchProcessingCompleted(context: {
successful: number;
failed: number;
+ filtered: number;
processed: number;
batchSize: number;
processingTimeMs: number;
diff --git a/lambdas/client-transform-filter-lambda/src/services/subscription-filter.ts b/lambdas/client-transform-filter-lambda/src/services/subscription-filter.ts
new file mode 100644
index 0000000..33eddf8
--- /dev/null
+++ b/lambdas/client-transform-filter-lambda/src/services/subscription-filter.ts
@@ -0,0 +1,47 @@
+import type {
+ ChannelStatusData,
+ ClientSubscriptionConfiguration,
+ MessageStatusData,
+ StatusPublishEvent,
+} from "@nhs-notify-client-callbacks/models";
+import { EventTypes } from "@nhs-notify-client-callbacks/models";
+import { matchesChannelStatusSubscription } from "services/filters/channel-status-filter";
+import { matchesMessageStatusSubscription } from "services/filters/message-status-filter";
+import { TransformationError } from "services/error-handler";
+import { logger } from "services/logger";
+
+type FilterResult = {
+ matched: boolean;
+ subscriptionType: "MessageStatus" | "ChannelStatus" | "Unknown";
+};
+
+export const evaluateSubscriptionFilters = (
+ event: StatusPublishEvent,
+ config: ClientSubscriptionConfiguration | undefined,
+): FilterResult => {
+ if (!config) {
+ logger.debug("No config available for filtering", {
+ eventType: event.type,
+ });
+ return { matched: false, subscriptionType: "Unknown" };
+ }
+
+ if (event.type === EventTypes.MESSAGE_STATUS_PUBLISHED) {
+ const notifyData = event.data as MessageStatusData;
+ return {
+ matched: matchesMessageStatusSubscription(config, { event, notifyData }),
+ subscriptionType: "MessageStatus",
+ };
+ }
+
+ if (event.type === EventTypes.CHANNEL_STATUS_PUBLISHED) {
+ const notifyData = event.data as ChannelStatusData;
+ return {
+ matched: matchesChannelStatusSubscription(config, { event, notifyData }),
+ subscriptionType: "ChannelStatus",
+ };
+ }
+
+ logger.warn("Unknown event type for filtering", { eventType: event.type });
+ throw new TransformationError(`Unsupported event type: ${event.type}`);
+};
diff --git a/lambdas/client-transform-filter-lambda/src/services/validators/config-validator.ts b/lambdas/client-transform-filter-lambda/src/services/validators/config-validator.ts
new file mode 100644
index 0000000..cf476d5
--- /dev/null
+++ b/lambdas/client-transform-filter-lambda/src/services/validators/config-validator.ts
@@ -0,0 +1,108 @@
+import { z } from "zod";
+import type { ClientSubscriptionConfiguration } from "@nhs-notify-client-callbacks/models";
+import {
+ CHANNEL_STATUSES,
+ CHANNEL_TYPES,
+ MESSAGE_STATUSES,
+ SUPPLIER_STATUSES,
+} from "@nhs-notify-client-callbacks/models";
+import {
+ ConfigValidationError,
+ type ValidationIssue,
+ formatValidationIssuePath,
+} from "services/error-handler";
+
+export { ConfigValidationError } from "services/error-handler";
+
+const httpsUrlSchema = z.string().refine(
+ (value) => {
+ try {
+ const parsed = new URL(value);
+ return parsed.protocol === "https:";
+ } catch {
+ return false;
+ }
+ },
+ {
+ message: "Expected HTTPS URL",
+ },
+);
+
+const targetSchema = z.object({
+ Type: z.literal("API"),
+ TargetId: z.string(),
+ InvocationEndpoint: httpsUrlSchema,
+ InvocationMethod: z.literal("POST"),
+ InvocationRateLimit: z.number(),
+ APIKey: z.object({
+ HeaderName: z.string(),
+ HeaderValue: z.string(),
+ }),
+});
+
+const baseSubscriptionSchema = z.object({
+ SubscriptionId: z.string().min(1),
+ ClientId: z.string(),
+ Targets: z.array(targetSchema).min(1),
+});
+
+const messageStatusSchema = baseSubscriptionSchema.extend({
+ SubscriptionType: z.literal("MessageStatus"),
+ MessageStatuses: z.array(z.enum(MESSAGE_STATUSES)),
+});
+
+const channelStatusSchema = baseSubscriptionSchema.extend({
+ SubscriptionType: z.literal("ChannelStatus"),
+ ChannelType: z.enum(CHANNEL_TYPES),
+ ChannelStatuses: z.array(z.enum(CHANNEL_STATUSES)),
+ SupplierStatuses: z.array(z.enum(SUPPLIER_STATUSES)),
+});
+
+const subscriptionSchema = z.discriminatedUnion("SubscriptionType", [
+ messageStatusSchema,
+ channelStatusSchema,
+]);
+
+const configSchema = z.array(subscriptionSchema).superRefine((config, ctx) => {
+ const seenSubscriptionIds = new Set();
+
+ for (const [index, subscription] of config.entries()) {
+ if (seenSubscriptionIds.has(subscription.SubscriptionId)) {
+ ctx.addIssue({
+ code: "custom",
+ message: "Expected SubscriptionId to be unique",
+ path: [index, "SubscriptionId"],
+ });
+ } else {
+ seenSubscriptionIds.add(subscription.SubscriptionId);
+ }
+ }
+});
+
+export const validateClientConfig = (
+ rawConfig: unknown,
+): ClientSubscriptionConfiguration => {
+ const result = configSchema.safeParse(rawConfig);
+
+ if (!result.success) {
+ const issues: ValidationIssue[] = result.error.issues.map((issue) => {
+ const pathSegments = issue.path.filter(
+ (segment): segment is string | number =>
+ typeof segment === "string" || typeof segment === "number",
+ );
+
+ return {
+ path: formatValidationIssuePath(pathSegments),
+ message: issue.message,
+ };
+ });
+ throw new ConfigValidationError(issues);
+ }
+
+ return result.data;
+};
+
+export {
+ type ChannelStatusSubscriptionConfiguration,
+ type MessageStatusSubscriptionConfiguration,
+} from "@nhs-notify-client-callbacks/models";
diff --git a/lambdas/client-transform-filter-lambda/src/services/validators/event-validator.ts b/lambdas/client-transform-filter-lambda/src/services/validators/event-validator.ts
index 82180eb..03e3780 100644
--- a/lambdas/client-transform-filter-lambda/src/services/validators/event-validator.ts
+++ b/lambdas/client-transform-filter-lambda/src/services/validators/event-validator.ts
@@ -7,7 +7,10 @@ import {
EventTypes,
type StatusPublishEvent,
} from "@nhs-notify-client-callbacks/models";
-import { ValidationError } from "services/error-handler";
+import {
+ ValidationError,
+ formatValidationIssuePath,
+} from "services/error-handler";
import { extractCorrelationId } from "services/logger";
const NHSNotifyExtensionsSchema = z.object({
@@ -66,7 +69,15 @@ function formatValidationError(error: unknown, event: unknown): never {
message = `CloudEvents validation failed: ${error.message}`;
} else if (error instanceof z.ZodError) {
const issues = error.issues
- .map((issue) => `${issue.path.join(".")}: ${issue.message}`)
+ .map((issue) => {
+ const path = formatValidationIssuePath(
+ issue.path.filter(
+ (segment): segment is string | number =>
+ typeof segment === "string" || typeof segment === "number",
+ ),
+ );
+ return path ? `${path}: ${issue.message}` : issue.message;
+ })
.join(", ");
message = `Validation failed: ${issues}`;
} else if (error instanceof Error) {
diff --git a/package-lock.json b/package-lock.json
index 6ba1c04..4bfd023 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -9,9 +9,11 @@
"lambdas/client-transform-filter-lambda",
"src/models",
"lambdas/mock-webhook-lambda",
- "tests/integration"
+ "tests/integration",
+ "tools/client-subscriptions-management"
],
"devDependencies": {
+ "@aws-sdk/client-s3": "^3.821.0",
"@stylistic/eslint-plugin": "^3.1.0",
"@tsconfig/node22": "^22.0.2",
"@types/jest": "^29.5.14",
@@ -47,6 +49,7 @@
"name": "nhs-notify-client-transform-filter-lambda",
"version": "0.0.1",
"dependencies": {
+ "@aws-sdk/client-s3": "^3.821.0",
"@nhs-notify-client-callbacks/models": "*",
"aws-embedded-metrics": "^4.2.1",
"cloudevents": "^8.0.2",
@@ -64,6 +67,19 @@
"typescript": "^5.8.2"
}
},
+ "lambdas/client-transform-filter-lambda/node_modules/p-map": {
+ "version": "4.0.0",
+ "license": "MIT",
+ "dependencies": {
+ "aggregate-error": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
"lambdas/mock-webhook-lambda": {
"name": "nhs-notify-mock-webhook-lambda",
"version": "0.0.1",
@@ -84,9 +100,7 @@
}
},
"lambdas/mock-webhook-lambda/node_modules/@types/node": {
- "version": "22.19.13",
- "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.13.tgz",
- "integrity": "sha512-akNQMv0wW5uyRpD2v2IEyRSZiR+BeGuoB6L310EgGObO44HSMNT8z1xzio28V8qOrgYaopIDNA18YgdXd+qTiw==",
+ "version": "22.19.11",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -95,15 +109,23 @@
},
"lambdas/mock-webhook-lambda/node_modules/undici-types": {
"version": "6.21.0",
- "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
- "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
"dev": true,
"license": "MIT"
},
+ "node_modules/@ampproject/remapping": {
+ "version": "2.3.0",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@jridgewell/gen-mapping": "^0.3.5",
+ "@jridgewell/trace-mapping": "^0.3.24"
+ },
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
"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",
@@ -114,10 +136,61 @@
"node": ">=16.0.0"
}
},
+ "node_modules/@aws-crypto/crc32c": {
+ "version": "5.2.0",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@aws-crypto/util": "^5.2.0",
+ "@aws-sdk/types": "^3.222.0",
+ "tslib": "^2.6.2"
+ }
+ },
+ "node_modules/@aws-crypto/sha1-browser": {
+ "version": "5.2.0",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@aws-crypto/supports-web-crypto": "^5.2.0",
+ "@aws-crypto/util": "^5.2.0",
+ "@aws-sdk/types": "^3.222.0",
+ "@aws-sdk/util-locate-window": "^3.0.0",
+ "@smithy/util-utf8": "^2.0.0",
+ "tslib": "^2.6.2"
+ }
+ },
+ "node_modules/@aws-crypto/sha1-browser/node_modules/@smithy/is-array-buffer": {
+ "version": "2.2.0",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=14.0.0"
+ }
+ },
+ "node_modules/@aws-crypto/sha1-browser/node_modules/@smithy/util-buffer-from": {
+ "version": "2.2.0",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@smithy/is-array-buffer": "^2.2.0",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=14.0.0"
+ }
+ },
+ "node_modules/@aws-crypto/sha1-browser/node_modules/@smithy/util-utf8": {
+ "version": "2.3.0",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@smithy/util-buffer-from": "^2.2.0",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=14.0.0"
+ }
+ },
"node_modules/@aws-crypto/sha256-browser": {
"version": "5.2.0",
- "resolved": "https://registry.npmjs.org/@aws-crypto/sha256-browser/-/sha256-browser-5.2.0.tgz",
- "integrity": "sha512-AXfN/lGotSQwu6HNcEsIASo7kWXZ5HYWvfOmSNKDsEqC4OashTp8alTmaz+F7TC2L083SFv5RdB+qU3Vs1kZqw==",
"license": "Apache-2.0",
"dependencies": {
"@aws-crypto/sha256-js": "^5.2.0",
@@ -131,8 +204,6 @@
},
"node_modules/@aws-crypto/sha256-browser/node_modules/@smithy/is-array-buffer": {
"version": "2.2.0",
- "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-2.2.0.tgz",
- "integrity": "sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA==",
"license": "Apache-2.0",
"dependencies": {
"tslib": "^2.6.2"
@@ -143,8 +214,6 @@
},
"node_modules/@aws-crypto/sha256-browser/node_modules/@smithy/util-buffer-from": {
"version": "2.2.0",
- "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-2.2.0.tgz",
- "integrity": "sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA==",
"license": "Apache-2.0",
"dependencies": {
"@smithy/is-array-buffer": "^2.2.0",
@@ -156,8 +225,6 @@
},
"node_modules/@aws-crypto/sha256-browser/node_modules/@smithy/util-utf8": {
"version": "2.3.0",
- "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-2.3.0.tgz",
- "integrity": "sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A==",
"license": "Apache-2.0",
"dependencies": {
"@smithy/util-buffer-from": "^2.2.0",
@@ -169,8 +236,6 @@
},
"node_modules/@aws-crypto/sha256-js": {
"version": "5.2.0",
- "resolved": "https://registry.npmjs.org/@aws-crypto/sha256-js/-/sha256-js-5.2.0.tgz",
- "integrity": "sha512-FFQQyu7edu4ufvIZ+OadFpHHOt+eSTBaYaki44c+akjg7qZg9oOQeLlk77F6tSYqjDAFClrHJk9tMf0HdVyOvA==",
"license": "Apache-2.0",
"dependencies": {
"@aws-crypto/util": "^5.2.0",
@@ -183,8 +248,6 @@
},
"node_modules/@aws-crypto/supports-web-crypto": {
"version": "5.2.0",
- "resolved": "https://registry.npmjs.org/@aws-crypto/supports-web-crypto/-/supports-web-crypto-5.2.0.tgz",
- "integrity": "sha512-iAvUotm021kM33eCdNfwIN//F77/IADDSs58i+MDaOqFrVjZo9bAal0NK7HurRuWLLpF1iLX7gbWrjHjeo+YFg==",
"license": "Apache-2.0",
"dependencies": {
"tslib": "^2.6.2"
@@ -192,8 +255,6 @@
},
"node_modules/@aws-crypto/util": {
"version": "5.2.0",
- "resolved": "https://registry.npmjs.org/@aws-crypto/util/-/util-5.2.0.tgz",
- "integrity": "sha512-4RkU9EsI6ZpBve5fseQlGNUWKMa1RLPQ1dnjnQoe07ldfIzcsGb5hC5W0Dm7u423KWzawlrpbjXBrXCEv9zazQ==",
"license": "Apache-2.0",
"dependencies": {
"@aws-sdk/types": "^3.222.0",
@@ -203,8 +264,6 @@
},
"node_modules/@aws-crypto/util/node_modules/@smithy/is-array-buffer": {
"version": "2.2.0",
- "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-2.2.0.tgz",
- "integrity": "sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA==",
"license": "Apache-2.0",
"dependencies": {
"tslib": "^2.6.2"
@@ -215,8 +274,6 @@
},
"node_modules/@aws-crypto/util/node_modules/@smithy/util-buffer-from": {
"version": "2.2.0",
- "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-2.2.0.tgz",
- "integrity": "sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA==",
"license": "Apache-2.0",
"dependencies": {
"@smithy/is-array-buffer": "^2.2.0",
@@ -228,8 +285,6 @@
},
"node_modules/@aws-crypto/util/node_modules/@smithy/util-utf8": {
"version": "2.3.0",
- "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-2.3.0.tgz",
- "integrity": "sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A==",
"license": "Apache-2.0",
"dependencies": {
"@smithy/util-buffer-from": "^2.2.0",
@@ -240,48 +295,177 @@
}
},
"node_modules/@aws-sdk/client-cloudwatch-logs": {
- "version": "3.1000.0",
- "resolved": "https://registry.npmjs.org/@aws-sdk/client-cloudwatch-logs/-/client-cloudwatch-logs-3.1000.0.tgz",
- "integrity": "sha512-8/YP++CiBIh5jADEmPfBCHYWErHNYlG5Ome5h82F/yB+x6i9ARF/Y/u95Z9IHwO25CDvxTPKH0U66h7HFL8tcg==",
+ "version": "3.991.0",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@aws-crypto/sha256-browser": "5.2.0",
+ "@aws-crypto/sha256-js": "5.2.0",
+ "@aws-sdk/core": "^3.973.10",
+ "@aws-sdk/credential-provider-node": "^3.972.9",
+ "@aws-sdk/middleware-host-header": "^3.972.3",
+ "@aws-sdk/middleware-logger": "^3.972.3",
+ "@aws-sdk/middleware-recursion-detection": "^3.972.3",
+ "@aws-sdk/middleware-user-agent": "^3.972.10",
+ "@aws-sdk/region-config-resolver": "^3.972.3",
+ "@aws-sdk/types": "^3.973.1",
+ "@aws-sdk/util-endpoints": "3.991.0",
+ "@aws-sdk/util-user-agent-browser": "^3.972.3",
+ "@aws-sdk/util-user-agent-node": "^3.972.8",
+ "@smithy/config-resolver": "^4.4.6",
+ "@smithy/core": "^3.23.0",
+ "@smithy/eventstream-serde-browser": "^4.2.8",
+ "@smithy/eventstream-serde-config-resolver": "^4.3.8",
+ "@smithy/eventstream-serde-node": "^4.2.8",
+ "@smithy/fetch-http-handler": "^5.3.9",
+ "@smithy/hash-node": "^4.2.8",
+ "@smithy/invalid-dependency": "^4.2.8",
+ "@smithy/middleware-content-length": "^4.2.8",
+ "@smithy/middleware-endpoint": "^4.4.14",
+ "@smithy/middleware-retry": "^4.4.31",
+ "@smithy/middleware-serde": "^4.2.9",
+ "@smithy/middleware-stack": "^4.2.8",
+ "@smithy/node-config-provider": "^4.3.8",
+ "@smithy/node-http-handler": "^4.4.10",
+ "@smithy/protocol-http": "^5.3.8",
+ "@smithy/smithy-client": "^4.11.3",
+ "@smithy/types": "^4.12.0",
+ "@smithy/url-parser": "^4.2.8",
+ "@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.30",
+ "@smithy/util-defaults-mode-node": "^4.2.33",
+ "@smithy/util-endpoints": "^3.2.8",
+ "@smithy/util-middleware": "^4.2.8",
+ "@smithy/util-retry": "^4.2.8",
+ "@smithy/util-utf8": "^4.2.0",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=20.0.0"
+ }
+ },
+ "node_modules/@aws-sdk/client-cloudwatch-logs/node_modules/@aws-sdk/util-endpoints": {
+ "version": "3.991.0",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@aws-sdk/types": "^3.973.1",
+ "@smithy/types": "^4.12.0",
+ "@smithy/url-parser": "^4.2.8",
+ "@smithy/util-endpoints": "^3.2.8",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=20.0.0"
+ }
+ },
+ "node_modules/@aws-sdk/client-cognito-identity": {
+ "version": "3.1004.0",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/client-cognito-identity/-/client-cognito-identity-3.1004.0.tgz",
+ "integrity": "sha512-iRFVMN0Rlh9tjEuz1c6eQnv9EiYH0uxIvobsn5IvOjsM0PdfsKpGdRKiQIA/OgmpTPfuYyySwaRRtDFH9TMlQw==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@aws-crypto/sha256-browser": "5.2.0",
+ "@aws-crypto/sha256-js": "5.2.0",
+ "@aws-sdk/core": "^3.973.18",
+ "@aws-sdk/credential-provider-node": "^3.972.18",
+ "@aws-sdk/middleware-host-header": "^3.972.7",
+ "@aws-sdk/middleware-logger": "^3.972.7",
+ "@aws-sdk/middleware-recursion-detection": "^3.972.7",
+ "@aws-sdk/middleware-user-agent": "^3.972.19",
+ "@aws-sdk/region-config-resolver": "^3.972.7",
+ "@aws-sdk/types": "^3.973.5",
+ "@aws-sdk/util-endpoints": "^3.996.4",
+ "@aws-sdk/util-user-agent-browser": "^3.972.7",
+ "@aws-sdk/util-user-agent-node": "^3.973.4",
+ "@smithy/config-resolver": "^4.4.10",
+ "@smithy/core": "^3.23.8",
+ "@smithy/fetch-http-handler": "^5.3.13",
+ "@smithy/hash-node": "^4.2.11",
+ "@smithy/invalid-dependency": "^4.2.11",
+ "@smithy/middleware-content-length": "^4.2.11",
+ "@smithy/middleware-endpoint": "^4.4.22",
+ "@smithy/middleware-retry": "^4.4.39",
+ "@smithy/middleware-serde": "^4.2.12",
+ "@smithy/middleware-stack": "^4.2.11",
+ "@smithy/node-config-provider": "^4.3.11",
+ "@smithy/node-http-handler": "^4.4.14",
+ "@smithy/protocol-http": "^5.3.11",
+ "@smithy/smithy-client": "^4.12.2",
+ "@smithy/types": "^4.13.0",
+ "@smithy/url-parser": "^4.2.11",
+ "@smithy/util-base64": "^4.3.2",
+ "@smithy/util-body-length-browser": "^4.2.2",
+ "@smithy/util-body-length-node": "^4.2.3",
+ "@smithy/util-defaults-mode-browser": "^4.3.38",
+ "@smithy/util-defaults-mode-node": "^4.2.41",
+ "@smithy/util-endpoints": "^3.3.2",
+ "@smithy/util-middleware": "^4.2.11",
+ "@smithy/util-retry": "^4.2.11",
+ "@smithy/util-utf8": "^4.2.2",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=20.0.0"
+ }
+ },
+ "node_modules/@aws-sdk/client-cognito-identity/node_modules/@aws-sdk/util-endpoints": {
+ "version": "3.996.4",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/util-endpoints/-/util-endpoints-3.996.4.tgz",
+ "integrity": "sha512-Hek90FBmd4joCFj+Vc98KLJh73Zqj3s2W56gjAcTkrNLMDI5nIFkG9YpfcJiVI1YlE2Ne1uOQNe+IgQ/Vz2XRA==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@aws-sdk/types": "^3.973.5",
+ "@smithy/types": "^4.13.0",
+ "@smithy/url-parser": "^4.2.11",
+ "@smithy/util-endpoints": "^3.3.2",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=20.0.0"
+ }
+ },
+ "node_modules/@aws-sdk/client-eventbridge": {
+ "version": "3.1001.0",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/client-eventbridge/-/client-eventbridge-3.1001.0.tgz",
+ "integrity": "sha512-asySfaKnDTxhMtxCX1dvjDPfJwrQ5xy/tzdmFHmRyURNhIhXG3dwishJ6ROXzOrY7hFCiz+OTWWjZ+IJbrfzkA==",
"license": "Apache-2.0",
"dependencies": {
"@aws-crypto/sha256-browser": "5.2.0",
"@aws-crypto/sha256-js": "5.2.0",
- "@aws-sdk/core": "^3.973.15",
- "@aws-sdk/credential-provider-node": "^3.972.14",
+ "@aws-sdk/core": "^3.973.16",
+ "@aws-sdk/credential-provider-node": "^3.972.15",
"@aws-sdk/middleware-host-header": "^3.972.6",
"@aws-sdk/middleware-logger": "^3.972.6",
"@aws-sdk/middleware-recursion-detection": "^3.972.6",
- "@aws-sdk/middleware-user-agent": "^3.972.15",
+ "@aws-sdk/middleware-user-agent": "^3.972.16",
"@aws-sdk/region-config-resolver": "^3.972.6",
+ "@aws-sdk/signature-v4-multi-region": "^3.996.4",
"@aws-sdk/types": "^3.973.4",
"@aws-sdk/util-endpoints": "^3.996.3",
"@aws-sdk/util-user-agent-browser": "^3.972.6",
- "@aws-sdk/util-user-agent-node": "^3.973.0",
+ "@aws-sdk/util-user-agent-node": "^3.973.1",
"@smithy/config-resolver": "^4.4.9",
- "@smithy/core": "^3.23.6",
- "@smithy/eventstream-serde-browser": "^4.2.10",
- "@smithy/eventstream-serde-config-resolver": "^4.3.10",
- "@smithy/eventstream-serde-node": "^4.2.10",
- "@smithy/fetch-http-handler": "^5.3.11",
+ "@smithy/core": "^3.23.7",
+ "@smithy/fetch-http-handler": "^5.3.12",
"@smithy/hash-node": "^4.2.10",
"@smithy/invalid-dependency": "^4.2.10",
"@smithy/middleware-content-length": "^4.2.10",
- "@smithy/middleware-endpoint": "^4.4.20",
- "@smithy/middleware-retry": "^4.4.37",
+ "@smithy/middleware-endpoint": "^4.4.21",
+ "@smithy/middleware-retry": "^4.4.38",
"@smithy/middleware-serde": "^4.2.11",
"@smithy/middleware-stack": "^4.2.10",
"@smithy/node-config-provider": "^4.3.10",
- "@smithy/node-http-handler": "^4.4.12",
+ "@smithy/node-http-handler": "^4.4.13",
"@smithy/protocol-http": "^5.3.10",
- "@smithy/smithy-client": "^4.12.0",
+ "@smithy/smithy-client": "^4.12.1",
"@smithy/types": "^4.13.0",
"@smithy/url-parser": "^4.2.10",
"@smithy/util-base64": "^4.3.1",
"@smithy/util-body-length-browser": "^4.2.1",
"@smithy/util-body-length-node": "^4.2.2",
- "@smithy/util-defaults-mode-browser": "^4.3.36",
- "@smithy/util-defaults-mode-node": "^4.2.39",
+ "@smithy/util-defaults-mode-browser": "^4.3.37",
+ "@smithy/util-defaults-mode-node": "^4.2.40",
"@smithy/util-endpoints": "^3.3.1",
"@smithy/util-middleware": "^4.2.10",
"@smithy/util-retry": "^4.2.10",
@@ -292,49 +476,206 @@
"node": ">=20.0.0"
}
},
+ "node_modules/@aws-sdk/client-eventbridge/node_modules/@aws-sdk/util-endpoints": {
+ "version": "3.996.3",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/util-endpoints/-/util-endpoints-3.996.3.tgz",
+ "integrity": "sha512-yWIQSNiCjykLL+ezN5A+DfBb1gfXTytBxm57e64lYmwxDHNmInYHRJYYRAGWG1o77vKEiWaw4ui28e3yb1k5aQ==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@aws-sdk/types": "^3.973.4",
+ "@smithy/types": "^4.13.0",
+ "@smithy/url-parser": "^4.2.10",
+ "@smithy/util-endpoints": "^3.3.1",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=20.0.0"
+ }
+ },
+ "node_modules/@aws-sdk/client-s3": {
+ "version": "3.996.0",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@aws-crypto/sha1-browser": "5.2.0",
+ "@aws-crypto/sha256-browser": "5.2.0",
+ "@aws-crypto/sha256-js": "5.2.0",
+ "@aws-sdk/core": "^3.973.12",
+ "@aws-sdk/credential-provider-node": "^3.972.11",
+ "@aws-sdk/middleware-bucket-endpoint": "^3.972.3",
+ "@aws-sdk/middleware-expect-continue": "^3.972.3",
+ "@aws-sdk/middleware-flexible-checksums": "^3.972.10",
+ "@aws-sdk/middleware-host-header": "^3.972.3",
+ "@aws-sdk/middleware-location-constraint": "^3.972.3",
+ "@aws-sdk/middleware-logger": "^3.972.3",
+ "@aws-sdk/middleware-recursion-detection": "^3.972.3",
+ "@aws-sdk/middleware-sdk-s3": "^3.972.12",
+ "@aws-sdk/middleware-ssec": "^3.972.3",
+ "@aws-sdk/middleware-user-agent": "^3.972.12",
+ "@aws-sdk/region-config-resolver": "^3.972.3",
+ "@aws-sdk/signature-v4-multi-region": "3.996.0",
+ "@aws-sdk/types": "^3.973.1",
+ "@aws-sdk/util-endpoints": "3.996.0",
+ "@aws-sdk/util-user-agent-browser": "^3.972.3",
+ "@aws-sdk/util-user-agent-node": "^3.972.11",
+ "@smithy/config-resolver": "^4.4.6",
+ "@smithy/core": "^3.23.2",
+ "@smithy/eventstream-serde-browser": "^4.2.8",
+ "@smithy/eventstream-serde-config-resolver": "^4.3.8",
+ "@smithy/eventstream-serde-node": "^4.2.8",
+ "@smithy/fetch-http-handler": "^5.3.9",
+ "@smithy/hash-blob-browser": "^4.2.9",
+ "@smithy/hash-node": "^4.2.8",
+ "@smithy/hash-stream-node": "^4.2.8",
+ "@smithy/invalid-dependency": "^4.2.8",
+ "@smithy/md5-js": "^4.2.8",
+ "@smithy/middleware-content-length": "^4.2.8",
+ "@smithy/middleware-endpoint": "^4.4.16",
+ "@smithy/middleware-retry": "^4.4.33",
+ "@smithy/middleware-serde": "^4.2.9",
+ "@smithy/middleware-stack": "^4.2.8",
+ "@smithy/node-config-provider": "^4.3.8",
+ "@smithy/node-http-handler": "^4.4.10",
+ "@smithy/protocol-http": "^5.3.8",
+ "@smithy/smithy-client": "^4.11.5",
+ "@smithy/types": "^4.12.0",
+ "@smithy/url-parser": "^4.2.8",
+ "@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.32",
+ "@smithy/util-defaults-mode-node": "^4.2.35",
+ "@smithy/util-endpoints": "^3.2.8",
+ "@smithy/util-middleware": "^4.2.8",
+ "@smithy/util-retry": "^4.2.8",
+ "@smithy/util-stream": "^4.5.12",
+ "@smithy/util-utf8": "^4.2.0",
+ "@smithy/util-waiter": "^4.2.8",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=20.0.0"
+ }
+ },
+ "node_modules/@aws-sdk/client-s3/node_modules/@aws-sdk/signature-v4-multi-region": {
+ "version": "3.996.0",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@aws-sdk/middleware-sdk-s3": "^3.972.12",
+ "@aws-sdk/types": "^3.973.1",
+ "@smithy/protocol-http": "^5.3.8",
+ "@smithy/signature-v4": "^5.3.8",
+ "@smithy/types": "^4.12.0",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=20.0.0"
+ }
+ },
+ "node_modules/@aws-sdk/client-s3/node_modules/@aws-sdk/util-endpoints": {
+ "version": "3.996.0",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@aws-sdk/types": "^3.973.1",
+ "@smithy/types": "^4.12.0",
+ "@smithy/url-parser": "^4.2.8",
+ "@smithy/util-endpoints": "^3.2.8",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=20.0.0"
+ }
+ },
"node_modules/@aws-sdk/client-sqs": {
- "version": "3.1000.0",
- "resolved": "https://registry.npmjs.org/@aws-sdk/client-sqs/-/client-sqs-3.1000.0.tgz",
- "integrity": "sha512-fGp197WE/wy05DNAKLokN21RwhH17go631U6GT/t3BwHv7DBd5oI4OLT5TLy0dc4freAd3ib3XET1OEc1TG/3Q==",
+ "version": "3.990.0",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"@aws-crypto/sha256-browser": "5.2.0",
"@aws-crypto/sha256-js": "5.2.0",
- "@aws-sdk/core": "^3.973.15",
- "@aws-sdk/credential-provider-node": "^3.972.14",
+ "@aws-sdk/core": "^3.973.10",
+ "@aws-sdk/credential-provider-node": "^3.972.9",
+ "@aws-sdk/middleware-host-header": "^3.972.3",
+ "@aws-sdk/middleware-logger": "^3.972.3",
+ "@aws-sdk/middleware-recursion-detection": "^3.972.3",
+ "@aws-sdk/middleware-sdk-sqs": "^3.972.7",
+ "@aws-sdk/middleware-user-agent": "^3.972.10",
+ "@aws-sdk/region-config-resolver": "^3.972.3",
+ "@aws-sdk/types": "^3.973.1",
+ "@aws-sdk/util-endpoints": "3.990.0",
+ "@aws-sdk/util-user-agent-browser": "^3.972.3",
+ "@aws-sdk/util-user-agent-node": "^3.972.8",
+ "@smithy/config-resolver": "^4.4.6",
+ "@smithy/core": "^3.23.0",
+ "@smithy/fetch-http-handler": "^5.3.9",
+ "@smithy/hash-node": "^4.2.8",
+ "@smithy/invalid-dependency": "^4.2.8",
+ "@smithy/md5-js": "^4.2.8",
+ "@smithy/middleware-content-length": "^4.2.8",
+ "@smithy/middleware-endpoint": "^4.4.14",
+ "@smithy/middleware-retry": "^4.4.31",
+ "@smithy/middleware-serde": "^4.2.9",
+ "@smithy/middleware-stack": "^4.2.8",
+ "@smithy/node-config-provider": "^4.3.8",
+ "@smithy/node-http-handler": "^4.4.10",
+ "@smithy/protocol-http": "^5.3.8",
+ "@smithy/smithy-client": "^4.11.3",
+ "@smithy/types": "^4.12.0",
+ "@smithy/url-parser": "^4.2.8",
+ "@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.30",
+ "@smithy/util-defaults-mode-node": "^4.2.33",
+ "@smithy/util-endpoints": "^3.2.8",
+ "@smithy/util-middleware": "^4.2.8",
+ "@smithy/util-retry": "^4.2.8",
+ "@smithy/util-utf8": "^4.2.0",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=20.0.0"
+ }
+ },
+ "node_modules/@aws-sdk/client-sts": {
+ "version": "3.1001.0",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/client-sts/-/client-sts-3.1001.0.tgz",
+ "integrity": "sha512-1HVxJcad+BTMVQ4lN2jw4SzyVqnIRZ7mb8YjwqMQ6p1MjuklSriVUXKtYFyxLVJnqaw61nFv9F8oHMOK69p6BQ==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@aws-crypto/sha256-browser": "5.2.0",
+ "@aws-crypto/sha256-js": "5.2.0",
+ "@aws-sdk/core": "^3.973.16",
+ "@aws-sdk/credential-provider-node": "^3.972.15",
"@aws-sdk/middleware-host-header": "^3.972.6",
"@aws-sdk/middleware-logger": "^3.972.6",
"@aws-sdk/middleware-recursion-detection": "^3.972.6",
- "@aws-sdk/middleware-sdk-sqs": "^3.972.11",
- "@aws-sdk/middleware-user-agent": "^3.972.15",
+ "@aws-sdk/middleware-user-agent": "^3.972.16",
"@aws-sdk/region-config-resolver": "^3.972.6",
"@aws-sdk/types": "^3.973.4",
"@aws-sdk/util-endpoints": "^3.996.3",
"@aws-sdk/util-user-agent-browser": "^3.972.6",
- "@aws-sdk/util-user-agent-node": "^3.973.0",
+ "@aws-sdk/util-user-agent-node": "^3.973.1",
"@smithy/config-resolver": "^4.4.9",
- "@smithy/core": "^3.23.6",
- "@smithy/fetch-http-handler": "^5.3.11",
+ "@smithy/core": "^3.23.7",
+ "@smithy/fetch-http-handler": "^5.3.12",
"@smithy/hash-node": "^4.2.10",
"@smithy/invalid-dependency": "^4.2.10",
- "@smithy/md5-js": "^4.2.10",
"@smithy/middleware-content-length": "^4.2.10",
- "@smithy/middleware-endpoint": "^4.4.20",
- "@smithy/middleware-retry": "^4.4.37",
+ "@smithy/middleware-endpoint": "^4.4.21",
+ "@smithy/middleware-retry": "^4.4.38",
"@smithy/middleware-serde": "^4.2.11",
"@smithy/middleware-stack": "^4.2.10",
"@smithy/node-config-provider": "^4.3.10",
- "@smithy/node-http-handler": "^4.4.12",
+ "@smithy/node-http-handler": "^4.4.13",
"@smithy/protocol-http": "^5.3.10",
- "@smithy/smithy-client": "^4.12.0",
+ "@smithy/smithy-client": "^4.12.1",
"@smithy/types": "^4.13.0",
"@smithy/url-parser": "^4.2.10",
"@smithy/util-base64": "^4.3.1",
"@smithy/util-body-length-browser": "^4.2.1",
"@smithy/util-body-length-node": "^4.2.2",
- "@smithy/util-defaults-mode-browser": "^4.3.36",
- "@smithy/util-defaults-mode-node": "^4.2.39",
+ "@smithy/util-defaults-mode-browser": "^4.3.37",
+ "@smithy/util-defaults-mode-node": "^4.2.40",
"@smithy/util-endpoints": "^3.3.1",
"@smithy/util-middleware": "^4.2.10",
"@smithy/util-retry": "^4.2.10",
@@ -345,24 +686,67 @@
"node": ">=20.0.0"
}
},
- "node_modules/@aws-sdk/core": {
- "version": "3.973.15",
- "resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.973.15.tgz",
- "integrity": "sha512-AlC0oQ1/mdJ8vCIqu524j5RB7M8i8E24bbkZmya1CuiQxkY7SdIZAyw7NDNMGaNINQFq/8oGRMX0HeOfCVsl/A==",
+ "node_modules/@aws-sdk/client-sts/node_modules/@aws-sdk/util-endpoints": {
+ "version": "3.996.3",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/util-endpoints/-/util-endpoints-3.996.3.tgz",
+ "integrity": "sha512-yWIQSNiCjykLL+ezN5A+DfBb1gfXTytBxm57e64lYmwxDHNmInYHRJYYRAGWG1o77vKEiWaw4ui28e3yb1k5aQ==",
"license": "Apache-2.0",
"dependencies": {
"@aws-sdk/types": "^3.973.4",
- "@aws-sdk/xml-builder": "^3.972.8",
- "@smithy/core": "^3.23.6",
- "@smithy/node-config-provider": "^4.3.10",
- "@smithy/property-provider": "^4.2.10",
- "@smithy/protocol-http": "^5.3.10",
- "@smithy/signature-v4": "^5.3.10",
- "@smithy/smithy-client": "^4.12.0",
"@smithy/types": "^4.13.0",
- "@smithy/util-base64": "^4.3.1",
- "@smithy/util-middleware": "^4.2.10",
- "@smithy/util-utf8": "^4.2.1",
+ "@smithy/url-parser": "^4.2.10",
+ "@smithy/util-endpoints": "^3.3.1",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=20.0.0"
+ }
+ },
+ "node_modules/@aws-sdk/core": {
+ "version": "3.973.18",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.973.18.tgz",
+ "integrity": "sha512-GUIlegfcK2LO1J2Y98sCJy63rQSiLiDOgVw7HiHPRqfI2vb3XozTVqemwO0VSGXp54ngCnAQz0Lf0YPCBINNxA==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@aws-sdk/types": "^3.973.5",
+ "@aws-sdk/xml-builder": "^3.972.10",
+ "@smithy/core": "^3.23.8",
+ "@smithy/node-config-provider": "^4.3.11",
+ "@smithy/property-provider": "^4.2.11",
+ "@smithy/protocol-http": "^5.3.11",
+ "@smithy/signature-v4": "^5.3.11",
+ "@smithy/smithy-client": "^4.12.2",
+ "@smithy/types": "^4.13.0",
+ "@smithy/util-base64": "^4.3.2",
+ "@smithy/util-middleware": "^4.2.11",
+ "@smithy/util-utf8": "^4.2.2",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=20.0.0"
+ }
+ },
+ "node_modules/@aws-sdk/crc64-nvme": {
+ "version": "3.972.0",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@smithy/types": "^4.12.0",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=20.0.0"
+ }
+ },
+ "node_modules/@aws-sdk/credential-provider-cognito-identity": {
+ "version": "3.972.10",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-cognito-identity/-/credential-provider-cognito-identity-3.972.10.tgz",
+ "integrity": "sha512-R7saD8TvU6En8tFstAgbM9w6wlFxTwXrvMEpheVdGyDMKSxK412aRy87VNb2Mc2By0vL58OIE487afpxOc/rVQ==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@aws-sdk/nested-clients": "^3.996.7",
+ "@aws-sdk/types": "^3.973.5",
+ "@smithy/property-provider": "^4.2.11",
+ "@smithy/types": "^4.13.0",
"tslib": "^2.6.2"
},
"engines": {
@@ -370,14 +754,14 @@
}
},
"node_modules/@aws-sdk/credential-provider-env": {
- "version": "3.972.13",
- "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-env/-/credential-provider-env-3.972.13.tgz",
- "integrity": "sha512-6ljXKIQ22WFKyIs1jbORIkGanySBHaPPTOI4OxACP5WXgbcR0nDYfqNJfXEGwCK7IzHdNbCSFsNKKs0qCexR8Q==",
+ "version": "3.972.16",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-env/-/credential-provider-env-3.972.16.tgz",
+ "integrity": "sha512-HrdtnadvTGAQUr18sPzGlE5El3ICphnH6SU7UQOMOWFgRKbTRNN8msTxM4emzguUso9CzaHU2xy5ctSrmK5YNA==",
"license": "Apache-2.0",
"dependencies": {
- "@aws-sdk/core": "^3.973.15",
- "@aws-sdk/types": "^3.973.4",
- "@smithy/property-provider": "^4.2.10",
+ "@aws-sdk/core": "^3.973.18",
+ "@aws-sdk/types": "^3.973.5",
+ "@smithy/property-provider": "^4.2.11",
"@smithy/types": "^4.13.0",
"tslib": "^2.6.2"
},
@@ -386,20 +770,20 @@
}
},
"node_modules/@aws-sdk/credential-provider-http": {
- "version": "3.972.15",
- "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-http/-/credential-provider-http-3.972.15.tgz",
- "integrity": "sha512-dJuSTreu/T8f24SHDNTjd7eQ4rabr0TzPh2UTCwYexQtzG3nTDKm1e5eIdhiroTMDkPEJeY+WPkA6F9wod/20A==",
+ "version": "3.972.18",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-http/-/credential-provider-http-3.972.18.tgz",
+ "integrity": "sha512-NyB6smuZAixND5jZumkpkunQ0voc4Mwgkd+SZ6cvAzIB7gK8HV8Zd4rS8Kn5MmoGgusyNfVGG+RLoYc4yFiw+A==",
"license": "Apache-2.0",
"dependencies": {
- "@aws-sdk/core": "^3.973.15",
- "@aws-sdk/types": "^3.973.4",
- "@smithy/fetch-http-handler": "^5.3.11",
- "@smithy/node-http-handler": "^4.4.12",
- "@smithy/property-provider": "^4.2.10",
- "@smithy/protocol-http": "^5.3.10",
- "@smithy/smithy-client": "^4.12.0",
+ "@aws-sdk/core": "^3.973.18",
+ "@aws-sdk/types": "^3.973.5",
+ "@smithy/fetch-http-handler": "^5.3.13",
+ "@smithy/node-http-handler": "^4.4.14",
+ "@smithy/property-provider": "^4.2.11",
+ "@smithy/protocol-http": "^5.3.11",
+ "@smithy/smithy-client": "^4.12.2",
"@smithy/types": "^4.13.0",
- "@smithy/util-stream": "^4.5.15",
+ "@smithy/util-stream": "^4.5.17",
"tslib": "^2.6.2"
},
"engines": {
@@ -407,23 +791,23 @@
}
},
"node_modules/@aws-sdk/credential-provider-ini": {
- "version": "3.972.13",
- "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.972.13.tgz",
- "integrity": "sha512-JKSoGb7XeabZLBJptpqoZIFbROUIS65NuQnEHGOpuT9GuuZwag2qciKANiDLFiYk4u8nSrJC9JIOnWKVvPVjeA==",
- "license": "Apache-2.0",
- "dependencies": {
- "@aws-sdk/core": "^3.973.15",
- "@aws-sdk/credential-provider-env": "^3.972.13",
- "@aws-sdk/credential-provider-http": "^3.972.15",
- "@aws-sdk/credential-provider-login": "^3.972.13",
- "@aws-sdk/credential-provider-process": "^3.972.13",
- "@aws-sdk/credential-provider-sso": "^3.972.13",
- "@aws-sdk/credential-provider-web-identity": "^3.972.13",
- "@aws-sdk/nested-clients": "^3.996.3",
- "@aws-sdk/types": "^3.973.4",
- "@smithy/credential-provider-imds": "^4.2.10",
- "@smithy/property-provider": "^4.2.10",
- "@smithy/shared-ini-file-loader": "^4.4.5",
+ "version": "3.972.17",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.972.17.tgz",
+ "integrity": "sha512-dFqh7nfX43B8dO1aPQHOcjC0SnCJ83H3F+1LoCh3X1P7E7N09I+0/taID0asU6GCddfDExqnEvQtDdkuMe5tKQ==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@aws-sdk/core": "^3.973.18",
+ "@aws-sdk/credential-provider-env": "^3.972.16",
+ "@aws-sdk/credential-provider-http": "^3.972.18",
+ "@aws-sdk/credential-provider-login": "^3.972.17",
+ "@aws-sdk/credential-provider-process": "^3.972.16",
+ "@aws-sdk/credential-provider-sso": "^3.972.17",
+ "@aws-sdk/credential-provider-web-identity": "^3.972.17",
+ "@aws-sdk/nested-clients": "^3.996.7",
+ "@aws-sdk/types": "^3.973.5",
+ "@smithy/credential-provider-imds": "^4.2.11",
+ "@smithy/property-provider": "^4.2.11",
+ "@smithy/shared-ini-file-loader": "^4.4.6",
"@smithy/types": "^4.13.0",
"tslib": "^2.6.2"
},
@@ -432,17 +816,17 @@
}
},
"node_modules/@aws-sdk/credential-provider-login": {
- "version": "3.972.13",
- "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-login/-/credential-provider-login-3.972.13.tgz",
- "integrity": "sha512-RtYcrxdnJHKY8MFQGLltCURcjuMjnaQpAxPE6+/QEdDHHItMKZgabRe/KScX737F9vJMQsmJy9EmMOkCnoC1JQ==",
+ "version": "3.972.17",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-login/-/credential-provider-login-3.972.17.tgz",
+ "integrity": "sha512-gf2E5b7LpKb+JX2oQsRIDxdRZjBFZt2olCGlWCdb3vBERbXIPgm2t1R5mEnwd4j0UEO/Tbg5zN2KJbHXttJqwA==",
"license": "Apache-2.0",
"dependencies": {
- "@aws-sdk/core": "^3.973.15",
- "@aws-sdk/nested-clients": "^3.996.3",
- "@aws-sdk/types": "^3.973.4",
- "@smithy/property-provider": "^4.2.10",
- "@smithy/protocol-http": "^5.3.10",
- "@smithy/shared-ini-file-loader": "^4.4.5",
+ "@aws-sdk/core": "^3.973.18",
+ "@aws-sdk/nested-clients": "^3.996.7",
+ "@aws-sdk/types": "^3.973.5",
+ "@smithy/property-provider": "^4.2.11",
+ "@smithy/protocol-http": "^5.3.11",
+ "@smithy/shared-ini-file-loader": "^4.4.6",
"@smithy/types": "^4.13.0",
"tslib": "^2.6.2"
},
@@ -451,21 +835,21 @@
}
},
"node_modules/@aws-sdk/credential-provider-node": {
- "version": "3.972.14",
- "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.972.14.tgz",
- "integrity": "sha512-WqoC2aliIjQM/L3oFf6j+op/enT2i9Cc4UTxxMEKrJNECkq4/PlKE5BOjSYFcq6G9mz65EFbXJh7zOU4CvjSKQ==",
+ "version": "3.972.18",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.972.18.tgz",
+ "integrity": "sha512-ZDJa2gd1xiPg/nBDGhUlat02O8obaDEnICBAVS8qieZ0+nDfaB0Z3ec6gjZj27OqFTjnB/Q5a0GwQwb7rMVViw==",
"license": "Apache-2.0",
"dependencies": {
- "@aws-sdk/credential-provider-env": "^3.972.13",
- "@aws-sdk/credential-provider-http": "^3.972.15",
- "@aws-sdk/credential-provider-ini": "^3.972.13",
- "@aws-sdk/credential-provider-process": "^3.972.13",
- "@aws-sdk/credential-provider-sso": "^3.972.13",
- "@aws-sdk/credential-provider-web-identity": "^3.972.13",
- "@aws-sdk/types": "^3.973.4",
- "@smithy/credential-provider-imds": "^4.2.10",
- "@smithy/property-provider": "^4.2.10",
- "@smithy/shared-ini-file-loader": "^4.4.5",
+ "@aws-sdk/credential-provider-env": "^3.972.16",
+ "@aws-sdk/credential-provider-http": "^3.972.18",
+ "@aws-sdk/credential-provider-ini": "^3.972.17",
+ "@aws-sdk/credential-provider-process": "^3.972.16",
+ "@aws-sdk/credential-provider-sso": "^3.972.17",
+ "@aws-sdk/credential-provider-web-identity": "^3.972.17",
+ "@aws-sdk/types": "^3.973.5",
+ "@smithy/credential-provider-imds": "^4.2.11",
+ "@smithy/property-provider": "^4.2.11",
+ "@smithy/shared-ini-file-loader": "^4.4.6",
"@smithy/types": "^4.13.0",
"tslib": "^2.6.2"
},
@@ -474,15 +858,15 @@
}
},
"node_modules/@aws-sdk/credential-provider-process": {
- "version": "3.972.13",
- "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-process/-/credential-provider-process-3.972.13.tgz",
- "integrity": "sha512-rsRG0LQA4VR+jnDyuqtXi2CePYSmfm5GNL9KxiW8DSe25YwJSr06W8TdUfONAC+rjsTI+aIH2rBGG5FjMeANrw==",
+ "version": "3.972.16",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-process/-/credential-provider-process-3.972.16.tgz",
+ "integrity": "sha512-n89ibATwnLEg0ZdZmUds5bq8AfBAdoYEDpqP3uzPLaRuGelsKlIvCYSNNvfgGLi8NaHPNNhs1HjJZYbqkW9b+g==",
"license": "Apache-2.0",
"dependencies": {
- "@aws-sdk/core": "^3.973.15",
- "@aws-sdk/types": "^3.973.4",
- "@smithy/property-provider": "^4.2.10",
- "@smithy/shared-ini-file-loader": "^4.4.5",
+ "@aws-sdk/core": "^3.973.18",
+ "@aws-sdk/types": "^3.973.5",
+ "@smithy/property-provider": "^4.2.11",
+ "@smithy/shared-ini-file-loader": "^4.4.6",
"@smithy/types": "^4.13.0",
"tslib": "^2.6.2"
},
@@ -491,17 +875,17 @@
}
},
"node_modules/@aws-sdk/credential-provider-sso": {
- "version": "3.972.13",
- "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.972.13.tgz",
- "integrity": "sha512-fr0UU1wx8kNHDhTQBXioc/YviSW8iXuAxHvnH7eQUtn8F8o/FU3uu6EUMvAQgyvn7Ne5QFnC0Cj0BFlwCk+RFw==",
+ "version": "3.972.17",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.972.17.tgz",
+ "integrity": "sha512-wGtte+48xnhnhHMl/MsxzacBPs5A+7JJedjiP452IkHY7vsbYKcvQBqFye8LwdTJVeHtBHv+JFeTscnwepoWGg==",
"license": "Apache-2.0",
"dependencies": {
- "@aws-sdk/core": "^3.973.15",
- "@aws-sdk/nested-clients": "^3.996.3",
- "@aws-sdk/token-providers": "3.999.0",
- "@aws-sdk/types": "^3.973.4",
- "@smithy/property-provider": "^4.2.10",
- "@smithy/shared-ini-file-loader": "^4.4.5",
+ "@aws-sdk/core": "^3.973.18",
+ "@aws-sdk/nested-clients": "^3.996.7",
+ "@aws-sdk/token-providers": "3.1004.0",
+ "@aws-sdk/types": "^3.973.5",
+ "@smithy/property-provider": "^4.2.11",
+ "@smithy/shared-ini-file-loader": "^4.4.6",
"@smithy/types": "^4.13.0",
"tslib": "^2.6.2"
},
@@ -510,16 +894,16 @@
}
},
"node_modules/@aws-sdk/credential-provider-web-identity": {
- "version": "3.972.13",
- "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.972.13.tgz",
- "integrity": "sha512-a6iFMh1pgUH0TdcouBppLJUfPM7Yd3R9S1xFodPtCRoLqCz2RQFA3qjA8x4112PVYXEd4/pHX2eihapq39w0rA==",
+ "version": "3.972.17",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.972.17.tgz",
+ "integrity": "sha512-8aiVJh6fTdl8gcyL+sVNcNwTtWpmoFa1Sh7xlj6Z7L/cZ/tYMEBHq44wTYG8Kt0z/PpGNopD89nbj3FHl9QmTA==",
"license": "Apache-2.0",
"dependencies": {
- "@aws-sdk/core": "^3.973.15",
- "@aws-sdk/nested-clients": "^3.996.3",
- "@aws-sdk/types": "^3.973.4",
- "@smithy/property-provider": "^4.2.10",
- "@smithy/shared-ini-file-loader": "^4.4.5",
+ "@aws-sdk/core": "^3.973.18",
+ "@aws-sdk/nested-clients": "^3.996.7",
+ "@aws-sdk/types": "^3.973.5",
+ "@smithy/property-provider": "^4.2.11",
+ "@smithy/shared-ini-file-loader": "^4.4.6",
"@smithy/types": "^4.13.0",
"tslib": "^2.6.2"
},
@@ -527,14 +911,97 @@
"node": ">=20.0.0"
}
},
+ "node_modules/@aws-sdk/credential-providers": {
+ "version": "3.1004.0",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/credential-providers/-/credential-providers-3.1004.0.tgz",
+ "integrity": "sha512-THsua88i7DrPoO8WCIWLPWb8706s2ytl2ej+WB9sv39VPCJNc7YwGtTA51reziyzlLnJUGHkI+krp0oTHEGaBw==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@aws-sdk/client-cognito-identity": "3.1004.0",
+ "@aws-sdk/core": "^3.973.18",
+ "@aws-sdk/credential-provider-cognito-identity": "^3.972.10",
+ "@aws-sdk/credential-provider-env": "^3.972.16",
+ "@aws-sdk/credential-provider-http": "^3.972.18",
+ "@aws-sdk/credential-provider-ini": "^3.972.17",
+ "@aws-sdk/credential-provider-login": "^3.972.17",
+ "@aws-sdk/credential-provider-node": "^3.972.18",
+ "@aws-sdk/credential-provider-process": "^3.972.16",
+ "@aws-sdk/credential-provider-sso": "^3.972.17",
+ "@aws-sdk/credential-provider-web-identity": "^3.972.17",
+ "@aws-sdk/nested-clients": "^3.996.7",
+ "@aws-sdk/types": "^3.973.5",
+ "@smithy/config-resolver": "^4.4.10",
+ "@smithy/core": "^3.23.8",
+ "@smithy/credential-provider-imds": "^4.2.11",
+ "@smithy/node-config-provider": "^4.3.11",
+ "@smithy/property-provider": "^4.2.11",
+ "@smithy/types": "^4.13.0",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=20.0.0"
+ }
+ },
+ "node_modules/@aws-sdk/middleware-bucket-endpoint": {
+ "version": "3.972.3",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@aws-sdk/types": "^3.973.1",
+ "@aws-sdk/util-arn-parser": "^3.972.2",
+ "@smithy/node-config-provider": "^4.3.8",
+ "@smithy/protocol-http": "^5.3.8",
+ "@smithy/types": "^4.12.0",
+ "@smithy/util-config-provider": "^4.2.0",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=20.0.0"
+ }
+ },
+ "node_modules/@aws-sdk/middleware-expect-continue": {
+ "version": "3.972.3",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@aws-sdk/types": "^3.973.1",
+ "@smithy/protocol-http": "^5.3.8",
+ "@smithy/types": "^4.12.0",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=20.0.0"
+ }
+ },
+ "node_modules/@aws-sdk/middleware-flexible-checksums": {
+ "version": "3.972.10",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@aws-crypto/crc32": "5.2.0",
+ "@aws-crypto/crc32c": "5.2.0",
+ "@aws-crypto/util": "5.2.0",
+ "@aws-sdk/core": "^3.973.12",
+ "@aws-sdk/crc64-nvme": "3.972.0",
+ "@aws-sdk/types": "^3.973.1",
+ "@smithy/is-array-buffer": "^4.2.0",
+ "@smithy/node-config-provider": "^4.3.8",
+ "@smithy/protocol-http": "^5.3.8",
+ "@smithy/types": "^4.12.0",
+ "@smithy/util-middleware": "^4.2.8",
+ "@smithy/util-stream": "^4.5.12",
+ "@smithy/util-utf8": "^4.2.0",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=20.0.0"
+ }
+ },
"node_modules/@aws-sdk/middleware-host-header": {
- "version": "3.972.6",
- "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-host-header/-/middleware-host-header-3.972.6.tgz",
- "integrity": "sha512-5XHwjPH1lHB+1q4bfC7T8Z5zZrZXfaLcjSMwTd1HPSPrCmPFMbg3UQ5vgNWcVj0xoX4HWqTGkSf2byrjlnRg5w==",
+ "version": "3.972.7",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-host-header/-/middleware-host-header-3.972.7.tgz",
+ "integrity": "sha512-aHQZgztBFEpDU1BB00VWCIIm85JjGjQW1OG9+98BdmaOpguJvzmXBGbnAiYcciCd+IS4e9BEq664lhzGnWJHgQ==",
"license": "Apache-2.0",
"dependencies": {
- "@aws-sdk/types": "^3.973.4",
- "@smithy/protocol-http": "^5.3.10",
+ "@aws-sdk/types": "^3.973.5",
+ "@smithy/protocol-http": "^5.3.11",
"@smithy/types": "^4.13.0",
"tslib": "^2.6.2"
},
@@ -542,13 +1009,25 @@
"node": ">=20.0.0"
}
},
+ "node_modules/@aws-sdk/middleware-location-constraint": {
+ "version": "3.972.3",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@aws-sdk/types": "^3.973.1",
+ "@smithy/types": "^4.12.0",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=20.0.0"
+ }
+ },
"node_modules/@aws-sdk/middleware-logger": {
- "version": "3.972.6",
- "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-logger/-/middleware-logger-3.972.6.tgz",
- "integrity": "sha512-iFnaMFMQdljAPrvsCVKYltPt2j40LQqukAbXvW7v0aL5I+1GO7bZ/W8m12WxW3gwyK5p5u1WlHg8TSAizC5cZw==",
+ "version": "3.972.7",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-logger/-/middleware-logger-3.972.7.tgz",
+ "integrity": "sha512-LXhiWlWb26txCU1vcI9PneESSeRp/RYY/McuM4SpdrimQR5NgwaPb4VJCadVeuGWgh6QmqZ6rAKSoL1ob16W6w==",
"license": "Apache-2.0",
"dependencies": {
- "@aws-sdk/types": "^3.973.4",
+ "@aws-sdk/types": "^3.973.5",
"@smithy/types": "^4.13.0",
"tslib": "^2.6.2"
},
@@ -557,14 +1036,14 @@
}
},
"node_modules/@aws-sdk/middleware-recursion-detection": {
- "version": "3.972.6",
- "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-recursion-detection/-/middleware-recursion-detection-3.972.6.tgz",
- "integrity": "sha512-dY4v3of5EEMvik6+UDwQ96KfUFDk8m1oZDdkSc5lwi4o7rFrjnv0A+yTV+gu230iybQZnKgDLg/rt2P3H+Vscw==",
+ "version": "3.972.7",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-recursion-detection/-/middleware-recursion-detection-3.972.7.tgz",
+ "integrity": "sha512-l2VQdcBcYLzIzykCHtXlbpiVCZ94/xniLIkAj0jpnpjY4xlgZx7f56Ypn+uV1y3gG0tNVytJqo3K9bfMFee7SQ==",
"license": "Apache-2.0",
"dependencies": {
- "@aws-sdk/types": "^3.973.4",
+ "@aws-sdk/types": "^3.973.5",
"@aws/lambda-invoke-store": "^0.2.2",
- "@smithy/protocol-http": "^5.3.10",
+ "@smithy/protocol-http": "^5.3.11",
"@smithy/types": "^4.13.0",
"tslib": "^2.6.2"
},
@@ -572,17 +1051,24 @@
"node": ">=20.0.0"
}
},
- "node_modules/@aws-sdk/middleware-sdk-sqs": {
- "version": "3.972.11",
- "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-sdk-sqs/-/middleware-sdk-sqs-3.972.11.tgz",
- "integrity": "sha512-Y4dryR0y7wN3hBayLOVSRuP3FeTs8KbNEL4orW/hKpf4jsrneDpI2RifUQVhiyb3QkC83bpeKaOSa0waHiPvcg==",
- "dev": true,
+ "node_modules/@aws-sdk/middleware-sdk-s3": {
+ "version": "3.972.16",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-sdk-s3/-/middleware-sdk-s3-3.972.16.tgz",
+ "integrity": "sha512-U4K1rqyJYvT/zgTI3+rN+MToa51dFnnq1VSsVJuJWPNEKcEnuZVqf7yTpkJJMkYixVW5TTi1dgupd+nmJ0JyWw==",
"license": "Apache-2.0",
"dependencies": {
+ "@aws-sdk/core": "^3.973.16",
"@aws-sdk/types": "^3.973.4",
- "@smithy/smithy-client": "^4.12.0",
+ "@aws-sdk/util-arn-parser": "^3.972.2",
+ "@smithy/core": "^3.23.7",
+ "@smithy/node-config-provider": "^4.3.10",
+ "@smithy/protocol-http": "^5.3.10",
+ "@smithy/signature-v4": "^5.3.10",
+ "@smithy/smithy-client": "^4.12.1",
"@smithy/types": "^4.13.0",
- "@smithy/util-hex-encoding": "^4.2.1",
+ "@smithy/util-config-provider": "^4.2.1",
+ "@smithy/util-middleware": "^4.2.10",
+ "@smithy/util-stream": "^4.5.16",
"@smithy/util-utf8": "^4.2.1",
"tslib": "^2.6.2"
},
@@ -590,18 +1076,63 @@
"node": ">=20.0.0"
}
},
+ "node_modules/@aws-sdk/middleware-sdk-sqs": {
+ "version": "3.972.7",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@aws-sdk/types": "^3.973.1",
+ "@smithy/smithy-client": "^4.11.3",
+ "@smithy/types": "^4.12.0",
+ "@smithy/util-hex-encoding": "^4.2.0",
+ "@smithy/util-utf8": "^4.2.0",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=20.0.0"
+ }
+ },
+ "node_modules/@aws-sdk/middleware-ssec": {
+ "version": "3.972.3",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@aws-sdk/types": "^3.973.1",
+ "@smithy/types": "^4.12.0",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=20.0.0"
+ }
+ },
"node_modules/@aws-sdk/middleware-user-agent": {
- "version": "3.972.15",
- "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-user-agent/-/middleware-user-agent-3.972.15.tgz",
- "integrity": "sha512-ABlFVcIMmuRAwBT+8q5abAxOr7WmaINirDJBnqGY5b5jSDo00UMlg/G4a0xoAgwm6oAECeJcwkvDlxDwKf58fQ==",
+ "version": "3.972.19",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-user-agent/-/middleware-user-agent-3.972.19.tgz",
+ "integrity": "sha512-Km90fcXt3W/iqujHzuM6IaDkYCj73gsYufcuWXApWdzoTy6KGk8fnchAjePMARU0xegIR3K4N3yIo1vy7OVe8A==",
"license": "Apache-2.0",
"dependencies": {
- "@aws-sdk/core": "^3.973.15",
- "@aws-sdk/types": "^3.973.4",
- "@aws-sdk/util-endpoints": "^3.996.3",
- "@smithy/core": "^3.23.6",
- "@smithy/protocol-http": "^5.3.10",
+ "@aws-sdk/core": "^3.973.18",
+ "@aws-sdk/types": "^3.973.5",
+ "@aws-sdk/util-endpoints": "^3.996.4",
+ "@smithy/core": "^3.23.8",
+ "@smithy/protocol-http": "^5.3.11",
+ "@smithy/types": "^4.13.0",
+ "@smithy/util-retry": "^4.2.11",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=20.0.0"
+ }
+ },
+ "node_modules/@aws-sdk/middleware-user-agent/node_modules/@aws-sdk/util-endpoints": {
+ "version": "3.996.4",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/util-endpoints/-/util-endpoints-3.996.4.tgz",
+ "integrity": "sha512-Hek90FBmd4joCFj+Vc98KLJh73Zqj3s2W56gjAcTkrNLMDI5nIFkG9YpfcJiVI1YlE2Ne1uOQNe+IgQ/Vz2XRA==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@aws-sdk/types": "^3.973.5",
"@smithy/types": "^4.13.0",
+ "@smithy/url-parser": "^4.2.11",
+ "@smithy/util-endpoints": "^3.3.2",
"tslib": "^2.6.2"
},
"engines": {
@@ -609,48 +1140,64 @@
}
},
"node_modules/@aws-sdk/nested-clients": {
- "version": "3.996.3",
- "resolved": "https://registry.npmjs.org/@aws-sdk/nested-clients/-/nested-clients-3.996.3.tgz",
- "integrity": "sha512-AU5TY1V29xqwg/MxmA2odwysTez+ccFAhmfRJk+QZT5HNv90UTA9qKd1J9THlsQkvmH7HWTEV1lDNxkQO5PzNw==",
+ "version": "3.996.7",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/nested-clients/-/nested-clients-3.996.7.tgz",
+ "integrity": "sha512-MlGWA8uPaOs5AiTZ5JLM4uuWDm9EEAnm9cqwvqQIc6kEgel/8s1BaOWm9QgUcfc9K8qd7KkC3n43yDbeXOA2tg==",
"license": "Apache-2.0",
"dependencies": {
"@aws-crypto/sha256-browser": "5.2.0",
"@aws-crypto/sha256-js": "5.2.0",
- "@aws-sdk/core": "^3.973.15",
- "@aws-sdk/middleware-host-header": "^3.972.6",
- "@aws-sdk/middleware-logger": "^3.972.6",
- "@aws-sdk/middleware-recursion-detection": "^3.972.6",
- "@aws-sdk/middleware-user-agent": "^3.972.15",
- "@aws-sdk/region-config-resolver": "^3.972.6",
- "@aws-sdk/types": "^3.973.4",
- "@aws-sdk/util-endpoints": "^3.996.3",
- "@aws-sdk/util-user-agent-browser": "^3.972.6",
- "@aws-sdk/util-user-agent-node": "^3.973.0",
- "@smithy/config-resolver": "^4.4.9",
- "@smithy/core": "^3.23.6",
- "@smithy/fetch-http-handler": "^5.3.11",
- "@smithy/hash-node": "^4.2.10",
- "@smithy/invalid-dependency": "^4.2.10",
- "@smithy/middleware-content-length": "^4.2.10",
- "@smithy/middleware-endpoint": "^4.4.20",
- "@smithy/middleware-retry": "^4.4.37",
- "@smithy/middleware-serde": "^4.2.11",
- "@smithy/middleware-stack": "^4.2.10",
- "@smithy/node-config-provider": "^4.3.10",
- "@smithy/node-http-handler": "^4.4.12",
- "@smithy/protocol-http": "^5.3.10",
- "@smithy/smithy-client": "^4.12.0",
+ "@aws-sdk/core": "^3.973.18",
+ "@aws-sdk/middleware-host-header": "^3.972.7",
+ "@aws-sdk/middleware-logger": "^3.972.7",
+ "@aws-sdk/middleware-recursion-detection": "^3.972.7",
+ "@aws-sdk/middleware-user-agent": "^3.972.19",
+ "@aws-sdk/region-config-resolver": "^3.972.7",
+ "@aws-sdk/types": "^3.973.5",
+ "@aws-sdk/util-endpoints": "^3.996.4",
+ "@aws-sdk/util-user-agent-browser": "^3.972.7",
+ "@aws-sdk/util-user-agent-node": "^3.973.4",
+ "@smithy/config-resolver": "^4.4.10",
+ "@smithy/core": "^3.23.8",
+ "@smithy/fetch-http-handler": "^5.3.13",
+ "@smithy/hash-node": "^4.2.11",
+ "@smithy/invalid-dependency": "^4.2.11",
+ "@smithy/middleware-content-length": "^4.2.11",
+ "@smithy/middleware-endpoint": "^4.4.22",
+ "@smithy/middleware-retry": "^4.4.39",
+ "@smithy/middleware-serde": "^4.2.12",
+ "@smithy/middleware-stack": "^4.2.11",
+ "@smithy/node-config-provider": "^4.3.11",
+ "@smithy/node-http-handler": "^4.4.14",
+ "@smithy/protocol-http": "^5.3.11",
+ "@smithy/smithy-client": "^4.12.2",
"@smithy/types": "^4.13.0",
- "@smithy/url-parser": "^4.2.10",
- "@smithy/util-base64": "^4.3.1",
- "@smithy/util-body-length-browser": "^4.2.1",
- "@smithy/util-body-length-node": "^4.2.2",
- "@smithy/util-defaults-mode-browser": "^4.3.36",
- "@smithy/util-defaults-mode-node": "^4.2.39",
- "@smithy/util-endpoints": "^3.3.1",
- "@smithy/util-middleware": "^4.2.10",
- "@smithy/util-retry": "^4.2.10",
- "@smithy/util-utf8": "^4.2.1",
+ "@smithy/url-parser": "^4.2.11",
+ "@smithy/util-base64": "^4.3.2",
+ "@smithy/util-body-length-browser": "^4.2.2",
+ "@smithy/util-body-length-node": "^4.2.3",
+ "@smithy/util-defaults-mode-browser": "^4.3.38",
+ "@smithy/util-defaults-mode-node": "^4.2.41",
+ "@smithy/util-endpoints": "^3.3.2",
+ "@smithy/util-middleware": "^4.2.11",
+ "@smithy/util-retry": "^4.2.11",
+ "@smithy/util-utf8": "^4.2.2",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=20.0.0"
+ }
+ },
+ "node_modules/@aws-sdk/nested-clients/node_modules/@aws-sdk/util-endpoints": {
+ "version": "3.996.4",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/util-endpoints/-/util-endpoints-3.996.4.tgz",
+ "integrity": "sha512-Hek90FBmd4joCFj+Vc98KLJh73Zqj3s2W56gjAcTkrNLMDI5nIFkG9YpfcJiVI1YlE2Ne1uOQNe+IgQ/Vz2XRA==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@aws-sdk/types": "^3.973.5",
+ "@smithy/types": "^4.13.0",
+ "@smithy/url-parser": "^4.2.11",
+ "@smithy/util-endpoints": "^3.3.2",
"tslib": "^2.6.2"
},
"engines": {
@@ -658,14 +1205,31 @@
}
},
"node_modules/@aws-sdk/region-config-resolver": {
- "version": "3.972.6",
- "resolved": "https://registry.npmjs.org/@aws-sdk/region-config-resolver/-/region-config-resolver-3.972.6.tgz",
- "integrity": "sha512-Aa5PusHLXAqLTX1UKDvI3pHQJtIsF7Q+3turCHqfz/1F61/zDMWfbTC8evjhrrYVAtz9Vsv3SJ/waSUeu7B6gw==",
+ "version": "3.972.7",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/region-config-resolver/-/region-config-resolver-3.972.7.tgz",
+ "integrity": "sha512-/Ev/6AI8bvt4HAAptzSjThGUMjcWaX3GX8oERkB0F0F9x2dLSBdgFDiyrRz3i0u0ZFZFQ1b28is4QhyqXTUsVA==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@aws-sdk/types": "^3.973.5",
+ "@smithy/config-resolver": "^4.4.10",
+ "@smithy/node-config-provider": "^4.3.11",
+ "@smithy/types": "^4.13.0",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=20.0.0"
+ }
+ },
+ "node_modules/@aws-sdk/signature-v4-multi-region": {
+ "version": "3.996.4",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/signature-v4-multi-region/-/signature-v4-multi-region-3.996.4.tgz",
+ "integrity": "sha512-MGa8ro0onekYIiesHX60LwKdkxK3Kd61p7TTbLwZemBqlnD9OLrk9sXZdFOIxXanJ+3AaJnV/jiX866eD/4PDg==",
"license": "Apache-2.0",
"dependencies": {
+ "@aws-sdk/middleware-sdk-s3": "^3.972.16",
"@aws-sdk/types": "^3.973.4",
- "@smithy/config-resolver": "^4.4.9",
- "@smithy/node-config-provider": "^4.3.10",
+ "@smithy/protocol-http": "^5.3.10",
+ "@smithy/signature-v4": "^5.3.10",
"@smithy/types": "^4.13.0",
"tslib": "^2.6.2"
},
@@ -674,16 +1238,16 @@
}
},
"node_modules/@aws-sdk/token-providers": {
- "version": "3.999.0",
- "resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.999.0.tgz",
- "integrity": "sha512-cx0hHUlgXULfykx4rdu/ciNAJaa3AL5xz3rieCz7NKJ68MJwlj3664Y8WR5MGgxfyYJBdamnkjNSx5Kekuc0cg==",
+ "version": "3.1004.0",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.1004.0.tgz",
+ "integrity": "sha512-j9BwZZId9sFp+4GPhf6KrwO8Tben2sXibZA8D1vv2I1zBdvkUHcBA2g4pkqIpTRalMTLC0NPkBPX0gERxfy/iA==",
"license": "Apache-2.0",
"dependencies": {
- "@aws-sdk/core": "^3.973.15",
- "@aws-sdk/nested-clients": "^3.996.3",
- "@aws-sdk/types": "^3.973.4",
- "@smithy/property-provider": "^4.2.10",
- "@smithy/shared-ini-file-loader": "^4.4.5",
+ "@aws-sdk/core": "^3.973.18",
+ "@aws-sdk/nested-clients": "^3.996.7",
+ "@aws-sdk/types": "^3.973.5",
+ "@smithy/property-provider": "^4.2.11",
+ "@smithy/shared-ini-file-loader": "^4.4.6",
"@smithy/types": "^4.13.0",
"tslib": "^2.6.2"
},
@@ -692,9 +1256,9 @@
}
},
"node_modules/@aws-sdk/types": {
- "version": "3.973.4",
- "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.973.4.tgz",
- "integrity": "sha512-RW60aH26Bsc016Y9B98hC0Plx6fK5P2v/iQYwMzrSjiDh1qRMUCP6KrXHYEHe3uFvKiOC93Z9zk4BJsUi6Tj1Q==",
+ "version": "3.973.5",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.973.5.tgz",
+ "integrity": "sha512-hl7BGwDCWsjH8NkZfx+HgS7H2LyM2lTMAI7ba9c8O0KqdBLTdNJivsHpqjg9rNlAlPyREb6DeDRXUl0s8uFdmQ==",
"license": "Apache-2.0",
"dependencies": {
"@smithy/types": "^4.13.0",
@@ -704,16 +1268,25 @@
"node": ">=20.0.0"
}
},
+ "node_modules/@aws-sdk/util-arn-parser": {
+ "version": "3.972.2",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=20.0.0"
+ }
+ },
"node_modules/@aws-sdk/util-endpoints": {
- "version": "3.996.3",
- "resolved": "https://registry.npmjs.org/@aws-sdk/util-endpoints/-/util-endpoints-3.996.3.tgz",
- "integrity": "sha512-yWIQSNiCjykLL+ezN5A+DfBb1gfXTytBxm57e64lYmwxDHNmInYHRJYYRAGWG1o77vKEiWaw4ui28e3yb1k5aQ==",
+ "version": "3.990.0",
+ "dev": true,
"license": "Apache-2.0",
"dependencies": {
- "@aws-sdk/types": "^3.973.4",
- "@smithy/types": "^4.13.0",
- "@smithy/url-parser": "^4.2.10",
- "@smithy/util-endpoints": "^3.3.1",
+ "@aws-sdk/types": "^3.973.1",
+ "@smithy/types": "^4.12.0",
+ "@smithy/url-parser": "^4.2.8",
+ "@smithy/util-endpoints": "^3.2.8",
"tslib": "^2.6.2"
},
"engines": {
@@ -722,8 +1295,6 @@
},
"node_modules/@aws-sdk/util-locate-window": {
"version": "3.965.4",
- "resolved": "https://registry.npmjs.org/@aws-sdk/util-locate-window/-/util-locate-window-3.965.4.tgz",
- "integrity": "sha512-H1onv5SkgPBK2P6JR2MjGgbOnttoNzSPIRoeZTNPZYyaplwGg50zS3amXvXqF0/qfXpWEC9rLWU564QTB9bSog==",
"license": "Apache-2.0",
"dependencies": {
"tslib": "^2.6.2"
@@ -733,26 +1304,26 @@
}
},
"node_modules/@aws-sdk/util-user-agent-browser": {
- "version": "3.972.6",
- "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-browser/-/util-user-agent-browser-3.972.6.tgz",
- "integrity": "sha512-Fwr/llD6GOrFgQnKaI2glhohdGuBDfHfora6iG9qsBBBR8xv1SdCSwbtf5CWlUdCw5X7g76G/9Hf0Inh0EmoxA==",
+ "version": "3.972.7",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-browser/-/util-user-agent-browser-3.972.7.tgz",
+ "integrity": "sha512-7SJVuvhKhMF/BkNS1n0QAJYgvEwYbK2QLKBrzDiwQGiTRU6Yf1f3nehTzm/l21xdAOtWSfp2uWSddPnP2ZtsVw==",
"license": "Apache-2.0",
"dependencies": {
- "@aws-sdk/types": "^3.973.4",
+ "@aws-sdk/types": "^3.973.5",
"@smithy/types": "^4.13.0",
"bowser": "^2.11.0",
"tslib": "^2.6.2"
}
},
"node_modules/@aws-sdk/util-user-agent-node": {
- "version": "3.973.0",
- "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-node/-/util-user-agent-node-3.973.0.tgz",
- "integrity": "sha512-A9J2G4Nf236e9GpaC1JnA8wRn6u6GjnOXiTwBLA6NUJhlBTIGfrTy+K1IazmF8y+4OFdW3O5TZlhyspJMqiqjA==",
+ "version": "3.973.4",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-node/-/util-user-agent-node-3.973.4.tgz",
+ "integrity": "sha512-uqKeLqZ9D3nQjH7HGIERNXK9qnSpUK08l4MlJ5/NZqSSdeJsVANYp437EM9sEzwU28c2xfj2V6qlkqzsgtKs6Q==",
"license": "Apache-2.0",
"dependencies": {
- "@aws-sdk/middleware-user-agent": "^3.972.15",
- "@aws-sdk/types": "^3.973.4",
- "@smithy/node-config-provider": "^4.3.10",
+ "@aws-sdk/middleware-user-agent": "^3.972.19",
+ "@aws-sdk/types": "^3.973.5",
+ "@smithy/node-config-provider": "^4.3.11",
"@smithy/types": "^4.13.0",
"tslib": "^2.6.2"
},
@@ -769,13 +1340,13 @@
}
},
"node_modules/@aws-sdk/xml-builder": {
- "version": "3.972.8",
- "resolved": "https://registry.npmjs.org/@aws-sdk/xml-builder/-/xml-builder-3.972.8.tgz",
- "integrity": "sha512-Ql8elcUdYCha83Ol7NznBsgN5GVZnv3vUd86fEc6waU6oUdY0T1O9NODkEEOS/Uaogr87avDrUC6DSeM4oXjZg==",
+ "version": "3.972.10",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/xml-builder/-/xml-builder-3.972.10.tgz",
+ "integrity": "sha512-OnejAIVD+CxzyAUrVic7lG+3QRltyja9LoNqCE/1YVs8ichoTbJlVSaZ9iSMcnHLyzrSNtvaOGjSDRP+d/ouFA==",
"license": "Apache-2.0",
"dependencies": {
"@smithy/types": "^4.13.0",
- "fast-xml-parser": "5.3.6",
+ "fast-xml-parser": "5.4.1",
"tslib": "^2.6.2"
},
"engines": {
@@ -784,19 +1355,17 @@
},
"node_modules/@aws/lambda-invoke-store": {
"version": "0.2.3",
- "resolved": "https://registry.npmjs.org/@aws/lambda-invoke-store/-/lambda-invoke-store-0.2.3.tgz",
- "integrity": "sha512-oLvsaPMTBejkkmHhjf09xTgk71mOqyr/409NKhRIL08If7AhVfUsJhVsx386uJaqNd42v9kWamQ9lFbkoC2dYw==",
"license": "Apache-2.0",
"engines": {
"node": ">=18.0.0"
}
},
"node_modules/@babel/code-frame": {
- "version": "7.29.0",
+ "version": "7.27.1",
"dev": true,
"license": "MIT",
"dependencies": {
- "@babel/helper-validator-identifier": "^7.28.5",
+ "@babel/helper-validator-identifier": "^7.27.1",
"js-tokens": "^4.0.0",
"picocolors": "^1.1.1"
},
@@ -805,7 +1374,7 @@
}
},
"node_modules/@babel/compat-data": {
- "version": "7.29.0",
+ "version": "7.27.5",
"dev": true,
"license": "MIT",
"engines": {
@@ -813,20 +1382,20 @@
}
},
"node_modules/@babel/core": {
- "version": "7.29.0",
+ "version": "7.27.4",
"dev": true,
"license": "MIT",
"dependencies": {
- "@babel/code-frame": "^7.29.0",
- "@babel/generator": "^7.29.0",
- "@babel/helper-compilation-targets": "^7.28.6",
- "@babel/helper-module-transforms": "^7.28.6",
- "@babel/helpers": "^7.28.6",
- "@babel/parser": "^7.29.0",
- "@babel/template": "^7.28.6",
- "@babel/traverse": "^7.29.0",
- "@babel/types": "^7.29.0",
- "@jridgewell/remapping": "^2.3.5",
+ "@ampproject/remapping": "^2.2.0",
+ "@babel/code-frame": "^7.27.1",
+ "@babel/generator": "^7.27.3",
+ "@babel/helper-compilation-targets": "^7.27.2",
+ "@babel/helper-module-transforms": "^7.27.3",
+ "@babel/helpers": "^7.27.4",
+ "@babel/parser": "^7.27.4",
+ "@babel/template": "^7.27.2",
+ "@babel/traverse": "^7.27.4",
+ "@babel/types": "^7.27.3",
"convert-source-map": "^2.0.0",
"debug": "^4.1.0",
"gensync": "^1.0.0-beta.2",
@@ -850,14 +1419,14 @@
}
},
"node_modules/@babel/generator": {
- "version": "7.29.1",
+ "version": "7.27.5",
"dev": true,
"license": "MIT",
"dependencies": {
- "@babel/parser": "^7.29.0",
- "@babel/types": "^7.29.0",
- "@jridgewell/gen-mapping": "^0.3.12",
- "@jridgewell/trace-mapping": "^0.3.28",
+ "@babel/parser": "^7.27.5",
+ "@babel/types": "^7.27.3",
+ "@jridgewell/gen-mapping": "^0.3.5",
+ "@jridgewell/trace-mapping": "^0.3.25",
"jsesc": "^3.0.2"
},
"engines": {
@@ -865,11 +1434,11 @@
}
},
"node_modules/@babel/helper-compilation-targets": {
- "version": "7.28.6",
+ "version": "7.27.2",
"dev": true,
"license": "MIT",
"dependencies": {
- "@babel/compat-data": "^7.28.6",
+ "@babel/compat-data": "^7.27.2",
"@babel/helper-validator-option": "^7.27.1",
"browserslist": "^4.24.0",
"lru-cache": "^5.1.1",
@@ -887,34 +1456,26 @@
"semver": "bin/semver.js"
}
},
- "node_modules/@babel/helper-globals": {
- "version": "7.28.0",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=6.9.0"
- }
- },
"node_modules/@babel/helper-module-imports": {
- "version": "7.28.6",
+ "version": "7.27.1",
"dev": true,
"license": "MIT",
"dependencies": {
- "@babel/traverse": "^7.28.6",
- "@babel/types": "^7.28.6"
+ "@babel/traverse": "^7.27.1",
+ "@babel/types": "^7.27.1"
},
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/helper-module-transforms": {
- "version": "7.28.6",
+ "version": "7.27.3",
"dev": true,
"license": "MIT",
"dependencies": {
- "@babel/helper-module-imports": "^7.28.6",
- "@babel/helper-validator-identifier": "^7.28.5",
- "@babel/traverse": "^7.28.6"
+ "@babel/helper-module-imports": "^7.27.1",
+ "@babel/helper-validator-identifier": "^7.27.1",
+ "@babel/traverse": "^7.27.3"
},
"engines": {
"node": ">=6.9.0"
@@ -924,7 +1485,7 @@
}
},
"node_modules/@babel/helper-plugin-utils": {
- "version": "7.28.6",
+ "version": "7.27.1",
"dev": true,
"license": "MIT",
"engines": {
@@ -940,7 +1501,7 @@
}
},
"node_modules/@babel/helper-validator-identifier": {
- "version": "7.28.5",
+ "version": "7.27.1",
"dev": true,
"license": "MIT",
"engines": {
@@ -956,23 +1517,23 @@
}
},
"node_modules/@babel/helpers": {
- "version": "7.28.6",
+ "version": "7.27.6",
"dev": true,
"license": "MIT",
"dependencies": {
- "@babel/template": "^7.28.6",
- "@babel/types": "^7.28.6"
+ "@babel/template": "^7.27.2",
+ "@babel/types": "^7.27.6"
},
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/parser": {
- "version": "7.29.0",
+ "version": "7.27.5",
"dev": true,
"license": "MIT",
"dependencies": {
- "@babel/types": "^7.29.0"
+ "@babel/types": "^7.27.3"
},
"bin": {
"parser": "bin/babel-parser.js"
@@ -1029,11 +1590,11 @@
}
},
"node_modules/@babel/plugin-syntax-import-attributes": {
- "version": "7.28.6",
+ "version": "7.27.1",
"dev": true,
"license": "MIT",
"dependencies": {
- "@babel/helper-plugin-utils": "^7.28.6"
+ "@babel/helper-plugin-utils": "^7.27.1"
},
"engines": {
"node": ">=6.9.0"
@@ -1065,11 +1626,11 @@
}
},
"node_modules/@babel/plugin-syntax-jsx": {
- "version": "7.28.6",
+ "version": "7.27.1",
"dev": true,
"license": "MIT",
"dependencies": {
- "@babel/helper-plugin-utils": "^7.28.6"
+ "@babel/helper-plugin-utils": "^7.27.1"
},
"engines": {
"node": ">=6.9.0"
@@ -1173,11 +1734,11 @@
}
},
"node_modules/@babel/plugin-syntax-typescript": {
- "version": "7.28.6",
+ "version": "7.27.1",
"dev": true,
"license": "MIT",
"dependencies": {
- "@babel/helper-plugin-utils": "^7.28.6"
+ "@babel/helper-plugin-utils": "^7.27.1"
},
"engines": {
"node": ">=6.9.0"
@@ -1187,42 +1748,50 @@
}
},
"node_modules/@babel/template": {
- "version": "7.28.6",
+ "version": "7.27.2",
"dev": true,
"license": "MIT",
"dependencies": {
- "@babel/code-frame": "^7.28.6",
- "@babel/parser": "^7.28.6",
- "@babel/types": "^7.28.6"
+ "@babel/code-frame": "^7.27.1",
+ "@babel/parser": "^7.27.2",
+ "@babel/types": "^7.27.1"
},
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/traverse": {
- "version": "7.29.0",
+ "version": "7.27.4",
"dev": true,
"license": "MIT",
"dependencies": {
- "@babel/code-frame": "^7.29.0",
- "@babel/generator": "^7.29.0",
- "@babel/helper-globals": "^7.28.0",
- "@babel/parser": "^7.29.0",
- "@babel/template": "^7.28.6",
- "@babel/types": "^7.29.0",
- "debug": "^4.3.1"
+ "@babel/code-frame": "^7.27.1",
+ "@babel/generator": "^7.27.3",
+ "@babel/parser": "^7.27.4",
+ "@babel/template": "^7.27.2",
+ "@babel/types": "^7.27.3",
+ "debug": "^4.3.1",
+ "globals": "^11.1.0"
},
"engines": {
"node": ">=6.9.0"
}
},
+ "node_modules/@babel/traverse/node_modules/globals": {
+ "version": "11.12.0",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=4"
+ }
+ },
"node_modules/@babel/types": {
- "version": "7.29.0",
+ "version": "7.27.6",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/helper-string-parser": "^7.27.1",
- "@babel/helper-validator-identifier": "^7.28.5"
+ "@babel/helper-validator-identifier": "^7.27.1"
},
"engines": {
"node": ">=6.9.0"
@@ -1255,19 +1824,17 @@
},
"node_modules/@datastructures-js/heap": {
"version": "4.3.7",
- "resolved": "https://registry.npmjs.org/@datastructures-js/heap/-/heap-4.3.7.tgz",
- "integrity": "sha512-Dx4un7Uj0dVxkfoq4RkpzsY2OrvNJgQYZ3n3UlGdl88RxxdHd7oTi21/l3zoxUUe0sXFuNUrfmWqlHzqnoN6Ug==",
"license": "MIT"
},
- "node_modules/@esbuild/linux-x64": {
- "version": "0.25.12",
+ "node_modules/@esbuild/darwin-arm64": {
+ "version": "0.25.0",
"cpu": [
- "x64"
+ "arm64"
],
"license": "MIT",
"optional": true,
"os": [
- "linux"
+ "darwin"
],
"engines": {
"node": ">=18"
@@ -1290,17 +1857,6 @@
"eslint": "^6.0.0 || ^7.0.0 || >=8.0.0"
}
},
- "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": {
- "version": "3.4.3",
- "dev": true,
- "license": "Apache-2.0",
- "engines": {
- "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
- },
- "funding": {
- "url": "https://opencollective.com/eslint"
- }
- },
"node_modules/@eslint-community/regexpp": {
"version": "4.12.2",
"dev": true,
@@ -1322,6 +1878,26 @@
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
}
},
+ "node_modules/@eslint/config-array/node_modules/brace-expansion": {
+ "version": "1.1.12",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "balanced-match": "^1.0.0",
+ "concat-map": "0.0.1"
+ }
+ },
+ "node_modules/@eslint/config-array/node_modules/minimatch": {
+ "version": "3.1.5",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "brace-expansion": "^1.1.7"
+ },
+ "engines": {
+ "node": "*"
+ }
+ },
"node_modules/@eslint/config-helpers": {
"version": "0.4.2",
"dev": true,
@@ -1345,18 +1921,18 @@
}
},
"node_modules/@eslint/eslintrc": {
- "version": "3.3.4",
+ "version": "3.3.1",
"dev": true,
"license": "MIT",
"dependencies": {
- "ajv": "^6.14.0",
+ "ajv": "^6.12.4",
"debug": "^4.3.2",
"espree": "^10.0.1",
"globals": "^14.0.0",
"ignore": "^5.2.0",
"import-fresh": "^3.2.1",
- "js-yaml": "^4.1.1",
- "minimatch": "^3.1.3",
+ "js-yaml": "^4.1.0",
+ "minimatch": "^3.1.2",
"strip-json-comments": "^3.1.1"
},
"engines": {
@@ -1366,6 +1942,34 @@
"url": "https://opencollective.com/eslint"
}
},
+ "node_modules/@eslint/eslintrc/node_modules/brace-expansion": {
+ "version": "1.1.12",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "balanced-match": "^1.0.0",
+ "concat-map": "0.0.1"
+ }
+ },
+ "node_modules/@eslint/eslintrc/node_modules/ignore": {
+ "version": "5.3.2",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 4"
+ }
+ },
+ "node_modules/@eslint/eslintrc/node_modules/minimatch": {
+ "version": "3.1.5",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "brace-expansion": "^1.1.7"
+ },
+ "engines": {
+ "node": "*"
+ }
+ },
"node_modules/@eslint/js": {
"version": "9.39.3",
"dev": true,
@@ -1406,17 +2010,29 @@
}
},
"node_modules/@humanfs/node": {
- "version": "0.16.7",
+ "version": "0.16.6",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"@humanfs/core": "^0.19.1",
- "@humanwhocodes/retry": "^0.4.0"
+ "@humanwhocodes/retry": "^0.3.0"
},
"engines": {
"node": ">=18.18.0"
}
},
+ "node_modules/@humanfs/node/node_modules/@humanwhocodes/retry": {
+ "version": "0.3.1",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=18.18"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/nzakas"
+ }
+ },
"node_modules/@humanwhocodes/module-importer": {
"version": "1.0.1",
"dev": true,
@@ -1477,7 +2093,7 @@
}
},
"node_modules/@istanbuljs/load-nyc-config/node_modules/js-yaml": {
- "version": "3.14.2",
+ "version": "3.14.1",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -1819,25 +2435,28 @@
}
},
"node_modules/@jridgewell/gen-mapping": {
- "version": "0.3.13",
+ "version": "0.3.8",
"dev": true,
"license": "MIT",
"dependencies": {
- "@jridgewell/sourcemap-codec": "^1.5.0",
+ "@jridgewell/set-array": "^1.2.1",
+ "@jridgewell/sourcemap-codec": "^1.4.10",
"@jridgewell/trace-mapping": "^0.3.24"
+ },
+ "engines": {
+ "node": ">=6.0.0"
}
},
- "node_modules/@jridgewell/remapping": {
- "version": "2.3.5",
+ "node_modules/@jridgewell/resolve-uri": {
+ "version": "3.1.2",
"dev": true,
- "license": "MIT",
- "dependencies": {
- "@jridgewell/gen-mapping": "^0.3.5",
- "@jridgewell/trace-mapping": "^0.3.24"
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.0.0"
}
},
- "node_modules/@jridgewell/resolve-uri": {
- "version": "3.1.2",
+ "node_modules/@jridgewell/set-array": {
+ "version": "1.2.1",
"dev": true,
"license": "MIT",
"engines": {
@@ -1845,12 +2464,12 @@
}
},
"node_modules/@jridgewell/sourcemap-codec": {
- "version": "1.5.5",
+ "version": "1.5.0",
"dev": true,
"license": "MIT"
},
"node_modules/@jridgewell/trace-mapping": {
- "version": "0.3.31",
+ "version": "0.3.25",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -1858,6 +2477,46 @@
"@jridgewell/sourcemap-codec": "^1.4.14"
}
},
+ "node_modules/@next/eslint-plugin-next": {
+ "version": "15.3.4",
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "peer": true,
+ "dependencies": {
+ "fast-glob": "3.3.1"
+ }
+ },
+ "node_modules/@next/eslint-plugin-next/node_modules/fast-glob": {
+ "version": "3.3.1",
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "peer": true,
+ "dependencies": {
+ "@nodelib/fs.stat": "^2.0.2",
+ "@nodelib/fs.walk": "^1.2.3",
+ "glob-parent": "^5.1.2",
+ "merge2": "^1.3.0",
+ "micromatch": "^4.0.4"
+ },
+ "engines": {
+ "node": ">=8.6.0"
+ }
+ },
+ "node_modules/@next/eslint-plugin-next/node_modules/glob-parent": {
+ "version": "5.1.2",
+ "dev": true,
+ "license": "ISC",
+ "optional": true,
+ "peer": true,
+ "dependencies": {
+ "is-glob": "^4.0.1"
+ },
+ "engines": {
+ "node": ">= 6"
+ }
+ },
"node_modules/@nhs-notify-client-callbacks/models": {
"resolved": "src/models",
"link": true
@@ -1896,8 +2555,6 @@
},
"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/@pkgr/core": {
@@ -1911,8 +2568,15 @@
"url": "https://opencollective.com/pkgr"
}
},
+ "node_modules/@rtsao/scc": {
+ "version": "1.1.0",
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "peer": true
+ },
"node_modules/@sinclair/typebox": {
- "version": "0.27.10",
+ "version": "0.27.8",
"dev": true,
"license": "MIT"
},
@@ -1933,9 +2597,9 @@
}
},
"node_modules/@smithy/abort-controller": {
- "version": "4.2.10",
- "resolved": "https://registry.npmjs.org/@smithy/abort-controller/-/abort-controller-4.2.10.tgz",
- "integrity": "sha512-qocxM/X4XGATqQtUkbE9SPUB6wekBi+FyJOMbPj0AhvyvFGYEmOlz6VB22iMePCQsFmMIvFSeViDvA7mZJG47g==",
+ "version": "4.2.11",
+ "resolved": "https://registry.npmjs.org/@smithy/abort-controller/-/abort-controller-4.2.11.tgz",
+ "integrity": "sha512-Hj4WoYWMJnSpM6/kchsm4bUNTL9XiSyhvoMb2KIq4VJzyDt7JpGHUZHkVNPZVC7YE1tf8tPeVauxpFBKGW4/KQ==",
"license": "Apache-2.0",
"dependencies": {
"@smithy/types": "^4.13.0",
@@ -1945,17 +2609,38 @@
"node": ">=18.0.0"
}
},
+ "node_modules/@smithy/chunked-blob-reader": {
+ "version": "5.2.1",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "node_modules/@smithy/chunked-blob-reader-native": {
+ "version": "4.2.2",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@smithy/util-base64": "^4.3.1",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
"node_modules/@smithy/config-resolver": {
- "version": "4.4.9",
- "resolved": "https://registry.npmjs.org/@smithy/config-resolver/-/config-resolver-4.4.9.tgz",
- "integrity": "sha512-ejQvXqlcU30h7liR9fXtj7PIAau1t/sFbJpgWPfiYDs7zd16jpH0IsSXKcba2jF6ChTXvIjACs27kNMc5xxE2Q==",
+ "version": "4.4.10",
+ "resolved": "https://registry.npmjs.org/@smithy/config-resolver/-/config-resolver-4.4.10.tgz",
+ "integrity": "sha512-IRTkd6ps0ru+lTWnfnsbXzW80A8Od8p3pYiZnW98K2Hb20rqfsX7VTlfUwhrcOeSSy68Gn9WBofwPuw3e5CCsg==",
"license": "Apache-2.0",
"dependencies": {
- "@smithy/node-config-provider": "^4.3.10",
+ "@smithy/node-config-provider": "^4.3.11",
"@smithy/types": "^4.13.0",
- "@smithy/util-config-provider": "^4.2.1",
- "@smithy/util-endpoints": "^3.3.1",
- "@smithy/util-middleware": "^4.2.10",
+ "@smithy/util-config-provider": "^4.2.2",
+ "@smithy/util-endpoints": "^3.3.2",
+ "@smithy/util-middleware": "^4.2.11",
"tslib": "^2.6.2"
},
"engines": {
@@ -1963,20 +2648,20 @@
}
},
"node_modules/@smithy/core": {
- "version": "3.23.6",
- "resolved": "https://registry.npmjs.org/@smithy/core/-/core-3.23.6.tgz",
- "integrity": "sha512-4xE+0L2NrsFKpEVFlFELkIHQddBvMbQ41LRIP74dGCXnY1zQ9DgksrBcRBDJT+iOzGy4VEJIeU3hkUK5mn06kg==",
+ "version": "3.23.9",
+ "resolved": "https://registry.npmjs.org/@smithy/core/-/core-3.23.9.tgz",
+ "integrity": "sha512-1Vcut4LEL9HZsdpI0vFiRYIsaoPwZLjAxnVQDUMQK8beMS+EYPLDQCXtbzfxmM5GzSgjfe2Q9M7WaXwIMQllyQ==",
"license": "Apache-2.0",
"dependencies": {
- "@smithy/middleware-serde": "^4.2.11",
- "@smithy/protocol-http": "^5.3.10",
+ "@smithy/middleware-serde": "^4.2.12",
+ "@smithy/protocol-http": "^5.3.11",
"@smithy/types": "^4.13.0",
- "@smithy/util-base64": "^4.3.1",
- "@smithy/util-body-length-browser": "^4.2.1",
- "@smithy/util-middleware": "^4.2.10",
- "@smithy/util-stream": "^4.5.15",
- "@smithy/util-utf8": "^4.2.1",
- "@smithy/uuid": "^1.1.1",
+ "@smithy/util-base64": "^4.3.2",
+ "@smithy/util-body-length-browser": "^4.2.2",
+ "@smithy/util-middleware": "^4.2.11",
+ "@smithy/util-stream": "^4.5.17",
+ "@smithy/util-utf8": "^4.2.2",
+ "@smithy/uuid": "^1.1.2",
"tslib": "^2.6.2"
},
"engines": {
@@ -1984,15 +2669,15 @@
}
},
"node_modules/@smithy/credential-provider-imds": {
- "version": "4.2.10",
- "resolved": "https://registry.npmjs.org/@smithy/credential-provider-imds/-/credential-provider-imds-4.2.10.tgz",
- "integrity": "sha512-3bsMLJJLTZGZqVGGeBVFfLzuRulVsGTj12BzRKODTHqUABpIr0jMN1vN3+u6r2OfyhAQ2pXaMZWX/swBK5I6PQ==",
+ "version": "4.2.11",
+ "resolved": "https://registry.npmjs.org/@smithy/credential-provider-imds/-/credential-provider-imds-4.2.11.tgz",
+ "integrity": "sha512-lBXrS6ku0kTj3xLmsJW0WwqWbGQ6ueooYyp/1L9lkyT0M02C+DWwYwc5aTyXFbRaK38ojALxNixg+LxKSHZc0g==",
"license": "Apache-2.0",
"dependencies": {
- "@smithy/node-config-provider": "^4.3.10",
- "@smithy/property-provider": "^4.2.10",
+ "@smithy/node-config-provider": "^4.3.11",
+ "@smithy/property-provider": "^4.2.11",
"@smithy/types": "^4.13.0",
- "@smithy/url-parser": "^4.2.10",
+ "@smithy/url-parser": "^4.2.11",
"tslib": "^2.6.2"
},
"engines": {
@@ -2000,14 +2685,12 @@
}
},
"node_modules/@smithy/eventstream-codec": {
- "version": "4.2.10",
- "resolved": "https://registry.npmjs.org/@smithy/eventstream-codec/-/eventstream-codec-4.2.10.tgz",
- "integrity": "sha512-A4ynrsFFfSXUHicfTcRehytppFBcY3HQxEGYiyGktPIOye3Ot7fxpiy4VR42WmtGI4Wfo6OXt/c1Ky1nUFxYYQ==",
+ "version": "4.2.8",
"license": "Apache-2.0",
"dependencies": {
"@aws-crypto/crc32": "5.2.0",
- "@smithy/types": "^4.13.0",
- "@smithy/util-hex-encoding": "^4.2.1",
+ "@smithy/types": "^4.12.0",
+ "@smithy/util-hex-encoding": "^4.2.0",
"tslib": "^2.6.2"
},
"engines": {
@@ -2015,13 +2698,11 @@
}
},
"node_modules/@smithy/eventstream-serde-browser": {
- "version": "4.2.10",
- "resolved": "https://registry.npmjs.org/@smithy/eventstream-serde-browser/-/eventstream-serde-browser-4.2.10.tgz",
- "integrity": "sha512-0xupsu9yj9oDVuQ50YCTS9nuSYhGlrwqdaKQel9y2Fz7LU9fNErVlw9N0o4pm4qqvWEGbSTI4HKc6XJfB30MVw==",
+ "version": "4.2.8",
"license": "Apache-2.0",
"dependencies": {
- "@smithy/eventstream-serde-universal": "^4.2.10",
- "@smithy/types": "^4.13.0",
+ "@smithy/eventstream-serde-universal": "^4.2.8",
+ "@smithy/types": "^4.12.0",
"tslib": "^2.6.2"
},
"engines": {
@@ -2029,12 +2710,10 @@
}
},
"node_modules/@smithy/eventstream-serde-config-resolver": {
- "version": "4.3.10",
- "resolved": "https://registry.npmjs.org/@smithy/eventstream-serde-config-resolver/-/eventstream-serde-config-resolver-4.3.10.tgz",
- "integrity": "sha512-8kn6sinrduk0yaYHMJDsNuiFpXwQwibR7n/4CDUqn4UgaG+SeBHu5jHGFdU9BLFAM7Q4/gvr9RYxBHz9/jKrhA==",
+ "version": "4.3.8",
"license": "Apache-2.0",
"dependencies": {
- "@smithy/types": "^4.13.0",
+ "@smithy/types": "^4.12.0",
"tslib": "^2.6.2"
},
"engines": {
@@ -2042,13 +2721,11 @@
}
},
"node_modules/@smithy/eventstream-serde-node": {
- "version": "4.2.10",
- "resolved": "https://registry.npmjs.org/@smithy/eventstream-serde-node/-/eventstream-serde-node-4.2.10.tgz",
- "integrity": "sha512-uUrxPGgIffnYfvIOUmBM5i+USdEBRTdh7mLPttjphgtooxQ8CtdO1p6K5+Q4BBAZvKlvtJ9jWyrWpBJYzBKsyQ==",
+ "version": "4.2.8",
"license": "Apache-2.0",
"dependencies": {
- "@smithy/eventstream-serde-universal": "^4.2.10",
- "@smithy/types": "^4.13.0",
+ "@smithy/eventstream-serde-universal": "^4.2.8",
+ "@smithy/types": "^4.12.0",
"tslib": "^2.6.2"
},
"engines": {
@@ -2056,13 +2733,11 @@
}
},
"node_modules/@smithy/eventstream-serde-universal": {
- "version": "4.2.10",
- "resolved": "https://registry.npmjs.org/@smithy/eventstream-serde-universal/-/eventstream-serde-universal-4.2.10.tgz",
- "integrity": "sha512-aArqzOEvcs2dK+xQVCgLbpJQGfZihw8SD4ymhkwNTtwKbnrzdhJsFDKuMQnam2kF69WzgJYOU5eJlCx+CA32bw==",
+ "version": "4.2.8",
"license": "Apache-2.0",
"dependencies": {
- "@smithy/eventstream-codec": "^4.2.10",
- "@smithy/types": "^4.13.0",
+ "@smithy/eventstream-codec": "^4.2.8",
+ "@smithy/types": "^4.12.0",
"tslib": "^2.6.2"
},
"engines": {
@@ -2070,29 +2745,54 @@
}
},
"node_modules/@smithy/fetch-http-handler": {
- "version": "5.3.11",
- "resolved": "https://registry.npmjs.org/@smithy/fetch-http-handler/-/fetch-http-handler-5.3.11.tgz",
- "integrity": "sha512-wbTRjOxdFuyEg0CpumjZO0hkUl+fetJFqxNROepuLIoijQh51aMBmzFLfoQdwRjxsuuS2jizzIUTjPWgd8pd7g==",
+ "version": "5.3.13",
+ "resolved": "https://registry.npmjs.org/@smithy/fetch-http-handler/-/fetch-http-handler-5.3.13.tgz",
+ "integrity": "sha512-U2Hcfl2s3XaYjikN9cT4mPu8ybDbImV3baXR0PkVlC0TTx808bRP3FaPGAzPtB8OByI+JqJ1kyS+7GEgae7+qQ==",
"license": "Apache-2.0",
"dependencies": {
- "@smithy/protocol-http": "^5.3.10",
- "@smithy/querystring-builder": "^4.2.10",
+ "@smithy/protocol-http": "^5.3.11",
+ "@smithy/querystring-builder": "^4.2.11",
"@smithy/types": "^4.13.0",
- "@smithy/util-base64": "^4.3.1",
+ "@smithy/util-base64": "^4.3.2",
"tslib": "^2.6.2"
},
"engines": {
"node": ">=18.0.0"
}
},
- "node_modules/@smithy/hash-node": {
+ "node_modules/@smithy/hash-blob-browser": {
"version": "4.2.10",
- "resolved": "https://registry.npmjs.org/@smithy/hash-node/-/hash-node-4.2.10.tgz",
- "integrity": "sha512-1VzIOI5CcsvMDvP3iv1vG/RfLJVVVc67dCRyLSB2Hn9SWCZrDO3zvcIzj3BfEtqRW5kcMg5KAeVf1K3dR6nD3w==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@smithy/chunked-blob-reader": "^5.2.1",
+ "@smithy/chunked-blob-reader-native": "^4.2.2",
+ "@smithy/types": "^4.12.1",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "node_modules/@smithy/hash-node": {
+ "version": "4.2.11",
+ "resolved": "https://registry.npmjs.org/@smithy/hash-node/-/hash-node-4.2.11.tgz",
+ "integrity": "sha512-T+p1pNynRkydpdL015ruIoyPSRw9e/SQOWmSAMmmprfswMrd5Ow5igOWNVlvyVFZlxXqGmyH3NQwfwy8r5Jx0A==",
"license": "Apache-2.0",
"dependencies": {
"@smithy/types": "^4.13.0",
- "@smithy/util-buffer-from": "^4.2.1",
+ "@smithy/util-buffer-from": "^4.2.2",
+ "@smithy/util-utf8": "^4.2.2",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "node_modules/@smithy/hash-stream-node": {
+ "version": "4.2.9",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@smithy/types": "^4.12.1",
"@smithy/util-utf8": "^4.2.1",
"tslib": "^2.6.2"
},
@@ -2101,9 +2801,9 @@
}
},
"node_modules/@smithy/invalid-dependency": {
- "version": "4.2.10",
- "resolved": "https://registry.npmjs.org/@smithy/invalid-dependency/-/invalid-dependency-4.2.10.tgz",
- "integrity": "sha512-vy9KPNSFUU0ajFYk0sDZIYiUlAWGEAhRfehIr5ZkdFrRFTAuXEPUd41USuqHU6vvLX4r6Q9X7MKBco5+Il0Org==",
+ "version": "4.2.11",
+ "resolved": "https://registry.npmjs.org/@smithy/invalid-dependency/-/invalid-dependency-4.2.11.tgz",
+ "integrity": "sha512-cGNMrgykRmddrNhYy1yBdrp5GwIgEkniS7k9O1VLB38yxQtlvrxpZtUVvo6T4cKpeZsriukBuuxfJcdZQc/f/g==",
"license": "Apache-2.0",
"dependencies": {
"@smithy/types": "^4.13.0",
@@ -2114,9 +2814,9 @@
}
},
"node_modules/@smithy/is-array-buffer": {
- "version": "4.2.1",
- "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-4.2.1.tgz",
- "integrity": "sha512-Yfu664Qbf1B4IYIsYgKoABt010daZjkaCRvdU/sPnZG6TtHOB0md0RjNdLGzxe5UIdn9js4ftPICzmkRa9RJ4Q==",
+ "version": "4.2.2",
+ "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-4.2.2.tgz",
+ "integrity": "sha512-n6rQ4N8Jj4YTQO3YFrlgZuwKodf4zUFs7EJIWH86pSCWBaAtAGBFfCM7Wx6D2bBJ2xqFNxGBSrUWswT3M0VJow==",
"license": "Apache-2.0",
"dependencies": {
"tslib": "^2.6.2"
@@ -2126,14 +2826,11 @@
}
},
"node_modules/@smithy/md5-js": {
- "version": "4.2.10",
- "resolved": "https://registry.npmjs.org/@smithy/md5-js/-/md5-js-4.2.10.tgz",
- "integrity": "sha512-Op+Dh6dPLWTjWITChFayDllIaCXRofOed8ecpggTC5fkh8yXes0vAEX7gRUfjGK+TlyxoCAA05gHbZW/zB9JwQ==",
- "dev": true,
+ "version": "4.2.8",
"license": "Apache-2.0",
"dependencies": {
- "@smithy/types": "^4.13.0",
- "@smithy/util-utf8": "^4.2.1",
+ "@smithy/types": "^4.12.0",
+ "@smithy/util-utf8": "^4.2.0",
"tslib": "^2.6.2"
},
"engines": {
@@ -2141,12 +2838,12 @@
}
},
"node_modules/@smithy/middleware-content-length": {
- "version": "4.2.10",
- "resolved": "https://registry.npmjs.org/@smithy/middleware-content-length/-/middleware-content-length-4.2.10.tgz",
- "integrity": "sha512-TQZ9kX5c6XbjhaEBpvhSvMEZ0klBs1CFtOdPFwATZSbC9UeQfKHPLPN9Y+I6wZGMOavlYTOlHEPDrt42PMSH9w==",
+ "version": "4.2.11",
+ "resolved": "https://registry.npmjs.org/@smithy/middleware-content-length/-/middleware-content-length-4.2.11.tgz",
+ "integrity": "sha512-UvIfKYAKhCzr4p6jFevPlKhQwyQwlJ6IeKLDhmV1PlYfcW3RL4ROjNEDtSik4NYMi9kDkH7eSwyTP3vNJ/u/Dw==",
"license": "Apache-2.0",
"dependencies": {
- "@smithy/protocol-http": "^5.3.10",
+ "@smithy/protocol-http": "^5.3.11",
"@smithy/types": "^4.13.0",
"tslib": "^2.6.2"
},
@@ -2155,18 +2852,18 @@
}
},
"node_modules/@smithy/middleware-endpoint": {
- "version": "4.4.20",
- "resolved": "https://registry.npmjs.org/@smithy/middleware-endpoint/-/middleware-endpoint-4.4.20.tgz",
- "integrity": "sha512-9W6Np4ceBP3XCYAGLoMCmn8t2RRVzuD1ndWPLBbv7H9CrwM9Bprf6Up6BM9ZA/3alodg0b7Kf6ftBK9R1N04vw==",
+ "version": "4.4.23",
+ "resolved": "https://registry.npmjs.org/@smithy/middleware-endpoint/-/middleware-endpoint-4.4.23.tgz",
+ "integrity": "sha512-UEFIejZy54T1EJn2aWJ45voB7RP2T+IRzUqocIdM6GFFa5ClZncakYJfcYnoXt3UsQrZZ9ZRauGm77l9UCbBLw==",
"license": "Apache-2.0",
"dependencies": {
- "@smithy/core": "^3.23.6",
- "@smithy/middleware-serde": "^4.2.11",
- "@smithy/node-config-provider": "^4.3.10",
- "@smithy/shared-ini-file-loader": "^4.4.5",
+ "@smithy/core": "^3.23.9",
+ "@smithy/middleware-serde": "^4.2.12",
+ "@smithy/node-config-provider": "^4.3.11",
+ "@smithy/shared-ini-file-loader": "^4.4.6",
"@smithy/types": "^4.13.0",
- "@smithy/url-parser": "^4.2.10",
- "@smithy/util-middleware": "^4.2.10",
+ "@smithy/url-parser": "^4.2.11",
+ "@smithy/util-middleware": "^4.2.11",
"tslib": "^2.6.2"
},
"engines": {
@@ -2174,19 +2871,19 @@
}
},
"node_modules/@smithy/middleware-retry": {
- "version": "4.4.37",
- "resolved": "https://registry.npmjs.org/@smithy/middleware-retry/-/middleware-retry-4.4.37.tgz",
- "integrity": "sha512-/1psZZllBBSQ7+qo5+hhLz7AEPGLx3Z0+e3ramMBEuPK2PfvLK4SrncDB9VegX5mBn+oP/UTDrM6IHrFjvX1ZA==",
+ "version": "4.4.40",
+ "resolved": "https://registry.npmjs.org/@smithy/middleware-retry/-/middleware-retry-4.4.40.tgz",
+ "integrity": "sha512-YhEMakG1Ae57FajERdHNZ4ShOPIY7DsgV+ZoAxo/5BT0KIe+f6DDU2rtIymNNFIj22NJfeeI6LWIifrwM0f+rA==",
"license": "Apache-2.0",
"dependencies": {
- "@smithy/node-config-provider": "^4.3.10",
- "@smithy/protocol-http": "^5.3.10",
- "@smithy/service-error-classification": "^4.2.10",
- "@smithy/smithy-client": "^4.12.0",
+ "@smithy/node-config-provider": "^4.3.11",
+ "@smithy/protocol-http": "^5.3.11",
+ "@smithy/service-error-classification": "^4.2.11",
+ "@smithy/smithy-client": "^4.12.3",
"@smithy/types": "^4.13.0",
- "@smithy/util-middleware": "^4.2.10",
- "@smithy/util-retry": "^4.2.10",
- "@smithy/uuid": "^1.1.1",
+ "@smithy/util-middleware": "^4.2.11",
+ "@smithy/util-retry": "^4.2.11",
+ "@smithy/uuid": "^1.1.2",
"tslib": "^2.6.2"
},
"engines": {
@@ -2194,12 +2891,12 @@
}
},
"node_modules/@smithy/middleware-serde": {
- "version": "4.2.11",
- "resolved": "https://registry.npmjs.org/@smithy/middleware-serde/-/middleware-serde-4.2.11.tgz",
- "integrity": "sha512-STQdONGPwbbC7cusL60s7vOa6He6A9w2jWhoapL0mgVjmR19pr26slV+yoSP76SIssMTX/95e5nOZ6UQv6jolg==",
+ "version": "4.2.12",
+ "resolved": "https://registry.npmjs.org/@smithy/middleware-serde/-/middleware-serde-4.2.12.tgz",
+ "integrity": "sha512-W9g1bOLui7Xn5FABRVS0o3rXL0gfN37d/8I/W7i0N7oxjx9QecUmXEMSUMADTODwdtka9cN43t5BI2CodLJpng==",
"license": "Apache-2.0",
"dependencies": {
- "@smithy/protocol-http": "^5.3.10",
+ "@smithy/protocol-http": "^5.3.11",
"@smithy/types": "^4.13.0",
"tslib": "^2.6.2"
},
@@ -2208,9 +2905,9 @@
}
},
"node_modules/@smithy/middleware-stack": {
- "version": "4.2.10",
- "resolved": "https://registry.npmjs.org/@smithy/middleware-stack/-/middleware-stack-4.2.10.tgz",
- "integrity": "sha512-pmts/WovNcE/tlyHa8z/groPeOtqtEpp61q3W0nW1nDJuMq/x+hWa/OVQBtgU0tBqupeXq0VBOLA4UZwE8I0YA==",
+ "version": "4.2.11",
+ "resolved": "https://registry.npmjs.org/@smithy/middleware-stack/-/middleware-stack-4.2.11.tgz",
+ "integrity": "sha512-s+eenEPW6RgliDk2IhjD2hWOxIx1NKrOHxEwNUaUXxYBxIyCcDfNULZ2Mu15E3kwcJWBedTET/kEASPV1A1Akg==",
"license": "Apache-2.0",
"dependencies": {
"@smithy/types": "^4.13.0",
@@ -2221,13 +2918,13 @@
}
},
"node_modules/@smithy/node-config-provider": {
- "version": "4.3.10",
- "resolved": "https://registry.npmjs.org/@smithy/node-config-provider/-/node-config-provider-4.3.10.tgz",
- "integrity": "sha512-UALRbJtVX34AdP2VECKVlnNgidLHA2A7YgcJzwSBg1hzmnO/bZBHl/LDQQyYifzUwp1UOODnl9JJ3KNawpUJ9w==",
+ "version": "4.3.11",
+ "resolved": "https://registry.npmjs.org/@smithy/node-config-provider/-/node-config-provider-4.3.11.tgz",
+ "integrity": "sha512-xD17eE7kaLgBBGf5CZQ58hh2YmwK1Z0O8YhffwB/De2jsL0U3JklmhVYJ9Uf37OtUDLF2gsW40Xwwag9U869Gg==",
"license": "Apache-2.0",
"dependencies": {
- "@smithy/property-provider": "^4.2.10",
- "@smithy/shared-ini-file-loader": "^4.4.5",
+ "@smithy/property-provider": "^4.2.11",
+ "@smithy/shared-ini-file-loader": "^4.4.6",
"@smithy/types": "^4.13.0",
"tslib": "^2.6.2"
},
@@ -2236,14 +2933,14 @@
}
},
"node_modules/@smithy/node-http-handler": {
- "version": "4.4.12",
- "resolved": "https://registry.npmjs.org/@smithy/node-http-handler/-/node-http-handler-4.4.12.tgz",
- "integrity": "sha512-zo1+WKJkR9x7ZtMeMDAAsq2PufwiLDmkhcjpWPRRkmeIuOm6nq1qjFICSZbnjBvD09ei8KMo26BWxsu2BUU+5w==",
+ "version": "4.4.14",
+ "resolved": "https://registry.npmjs.org/@smithy/node-http-handler/-/node-http-handler-4.4.14.tgz",
+ "integrity": "sha512-DamSqaU8nuk0xTJDrYnRzZndHwwRnyj/n/+RqGGCcBKB4qrQem0mSDiWdupaNWdwxzyMU91qxDmHOCazfhtO3A==",
"license": "Apache-2.0",
"dependencies": {
- "@smithy/abort-controller": "^4.2.10",
- "@smithy/protocol-http": "^5.3.10",
- "@smithy/querystring-builder": "^4.2.10",
+ "@smithy/abort-controller": "^4.2.11",
+ "@smithy/protocol-http": "^5.3.11",
+ "@smithy/querystring-builder": "^4.2.11",
"@smithy/types": "^4.13.0",
"tslib": "^2.6.2"
},
@@ -2252,9 +2949,9 @@
}
},
"node_modules/@smithy/property-provider": {
- "version": "4.2.10",
- "resolved": "https://registry.npmjs.org/@smithy/property-provider/-/property-provider-4.2.10.tgz",
- "integrity": "sha512-5jm60P0CU7tom0eNrZ7YrkgBaoLFXzmqB0wVS+4uK8PPGmosSrLNf6rRd50UBvukztawZ7zyA8TxlrKpF5z9jw==",
+ "version": "4.2.11",
+ "resolved": "https://registry.npmjs.org/@smithy/property-provider/-/property-provider-4.2.11.tgz",
+ "integrity": "sha512-14T1V64o6/ndyrnl1ze1ZhyLzIeYNN47oF/QU6P5m82AEtyOkMJTb0gO1dPubYjyyKuPD6OSVMPDKe+zioOnCg==",
"license": "Apache-2.0",
"dependencies": {
"@smithy/types": "^4.13.0",
@@ -2265,9 +2962,9 @@
}
},
"node_modules/@smithy/protocol-http": {
- "version": "5.3.10",
- "resolved": "https://registry.npmjs.org/@smithy/protocol-http/-/protocol-http-5.3.10.tgz",
- "integrity": "sha512-2NzVWpYY0tRdfeCJLsgrR89KE3NTWT2wGulhNUxYlRmtRmPwLQwKzhrfVaiNlA9ZpJvbW7cjTVChYKgnkqXj1A==",
+ "version": "5.3.11",
+ "resolved": "https://registry.npmjs.org/@smithy/protocol-http/-/protocol-http-5.3.11.tgz",
+ "integrity": "sha512-hI+barOVDJBkNt4y0L2mu3Ugc0w7+BpJ2CZuLwXtSltGAAwCb3IvnalGlbDV/UCS6a9ZuT3+exd1WxNdLb5IlQ==",
"license": "Apache-2.0",
"dependencies": {
"@smithy/types": "^4.13.0",
@@ -2278,13 +2975,13 @@
}
},
"node_modules/@smithy/querystring-builder": {
- "version": "4.2.10",
- "resolved": "https://registry.npmjs.org/@smithy/querystring-builder/-/querystring-builder-4.2.10.tgz",
- "integrity": "sha512-HeN7kEvuzO2DmAzLukE9UryiUvejD3tMp9a1D1NJETerIfKobBUCLfviP6QEk500166eD2IATaXM59qgUI+YDA==",
+ "version": "4.2.11",
+ "resolved": "https://registry.npmjs.org/@smithy/querystring-builder/-/querystring-builder-4.2.11.tgz",
+ "integrity": "sha512-7spdikrYiljpket6u0up2Ck2mxhy7dZ0+TDd+S53Dg2DHd6wg+YNJrTCHiLdgZmEXZKI7LJZcwL3721ZRDFiqA==",
"license": "Apache-2.0",
"dependencies": {
"@smithy/types": "^4.13.0",
- "@smithy/util-uri-escape": "^4.2.1",
+ "@smithy/util-uri-escape": "^4.2.2",
"tslib": "^2.6.2"
},
"engines": {
@@ -2292,9 +2989,9 @@
}
},
"node_modules/@smithy/querystring-parser": {
- "version": "4.2.10",
- "resolved": "https://registry.npmjs.org/@smithy/querystring-parser/-/querystring-parser-4.2.10.tgz",
- "integrity": "sha512-4Mh18J26+ao1oX5wXJfWlTT+Q1OpDR8ssiC9PDOuEgVBGloqg18Fw7h5Ct8DyT9NBYwJgtJ2nLjKKFU6RP1G1Q==",
+ "version": "4.2.11",
+ "resolved": "https://registry.npmjs.org/@smithy/querystring-parser/-/querystring-parser-4.2.11.tgz",
+ "integrity": "sha512-nE3IRNjDltvGcoThD2abTozI1dkSy8aX+a2N1Rs55en5UsdyyIXgGEmevUL3okZFoJC77JgRGe99xYohhsjivQ==",
"license": "Apache-2.0",
"dependencies": {
"@smithy/types": "^4.13.0",
@@ -2305,9 +3002,9 @@
}
},
"node_modules/@smithy/service-error-classification": {
- "version": "4.2.10",
- "resolved": "https://registry.npmjs.org/@smithy/service-error-classification/-/service-error-classification-4.2.10.tgz",
- "integrity": "sha512-0R/+/Il5y8nB/By90o8hy/bWVYptbIfvoTYad0igYQO5RefhNCDmNzqxaMx7K1t/QWo0d6UynqpqN5cCQt1MCg==",
+ "version": "4.2.11",
+ "resolved": "https://registry.npmjs.org/@smithy/service-error-classification/-/service-error-classification-4.2.11.tgz",
+ "integrity": "sha512-HkMFJZJUhzU3HvND1+Yw/kYWXp4RPDLBWLcK1n+Vqw8xn4y2YiBhdww8IxhkQjP/QlZun5bwm3vcHc8AqIU3zw==",
"license": "Apache-2.0",
"dependencies": {
"@smithy/types": "^4.13.0"
@@ -2317,9 +3014,9 @@
}
},
"node_modules/@smithy/shared-ini-file-loader": {
- "version": "4.4.5",
- "resolved": "https://registry.npmjs.org/@smithy/shared-ini-file-loader/-/shared-ini-file-loader-4.4.5.tgz",
- "integrity": "sha512-pHgASxl50rrtOztgQCPmOXFjRW+mCd7ALr/3uXNzRrRoGV5G2+78GOsQ3HlQuBVHCh9o6xqMNvlIKZjWn4Euug==",
+ "version": "4.4.6",
+ "resolved": "https://registry.npmjs.org/@smithy/shared-ini-file-loader/-/shared-ini-file-loader-4.4.6.tgz",
+ "integrity": "sha512-IB/M5I8G0EeXZTHsAxpx51tMQ5R719F3aq+fjEB6VtNcCHDc0ajFDIGDZw+FW9GxtEkgTduiPpjveJdA/CX7sw==",
"license": "Apache-2.0",
"dependencies": {
"@smithy/types": "^4.13.0",
@@ -2330,18 +3027,18 @@
}
},
"node_modules/@smithy/signature-v4": {
- "version": "5.3.10",
- "resolved": "https://registry.npmjs.org/@smithy/signature-v4/-/signature-v4-5.3.10.tgz",
- "integrity": "sha512-Wab3wW8468WqTKIxI+aZe3JYO52/RYT/8sDOdzkUhjnLakLe9qoQqIcfih/qxcF4qWEFoWBszY0mj5uxffaVXA==",
+ "version": "5.3.11",
+ "resolved": "https://registry.npmjs.org/@smithy/signature-v4/-/signature-v4-5.3.11.tgz",
+ "integrity": "sha512-V1L6N9aKOBAN4wEHLyqjLBnAz13mtILU0SeDrjOaIZEeN6IFa6DxwRt1NNpOdmSpQUfkBj0qeD3m6P77uzMhgQ==",
"license": "Apache-2.0",
"dependencies": {
- "@smithy/is-array-buffer": "^4.2.1",
- "@smithy/protocol-http": "^5.3.10",
+ "@smithy/is-array-buffer": "^4.2.2",
+ "@smithy/protocol-http": "^5.3.11",
"@smithy/types": "^4.13.0",
- "@smithy/util-hex-encoding": "^4.2.1",
- "@smithy/util-middleware": "^4.2.10",
- "@smithy/util-uri-escape": "^4.2.1",
- "@smithy/util-utf8": "^4.2.1",
+ "@smithy/util-hex-encoding": "^4.2.2",
+ "@smithy/util-middleware": "^4.2.11",
+ "@smithy/util-uri-escape": "^4.2.2",
+ "@smithy/util-utf8": "^4.2.2",
"tslib": "^2.6.2"
},
"engines": {
@@ -2349,17 +3046,17 @@
}
},
"node_modules/@smithy/smithy-client": {
- "version": "4.12.0",
- "resolved": "https://registry.npmjs.org/@smithy/smithy-client/-/smithy-client-4.12.0.tgz",
- "integrity": "sha512-R8bQ9K3lCcXyZmBnQqUZJF4ChZmtWT5NLi6x5kgWx5D+/j0KorXcA0YcFg/X5TOgnTCy1tbKc6z2g2y4amFupQ==",
+ "version": "4.12.3",
+ "resolved": "https://registry.npmjs.org/@smithy/smithy-client/-/smithy-client-4.12.3.tgz",
+ "integrity": "sha512-7k4UxjSpHmPN2AxVhvIazRSzFQjWnud3sOsXcFStzagww17j1cFQYqTSiQ8xuYK3vKLR1Ni8FzuT3VlKr3xCNw==",
"license": "Apache-2.0",
"dependencies": {
- "@smithy/core": "^3.23.6",
- "@smithy/middleware-endpoint": "^4.4.20",
- "@smithy/middleware-stack": "^4.2.10",
- "@smithy/protocol-http": "^5.3.10",
+ "@smithy/core": "^3.23.9",
+ "@smithy/middleware-endpoint": "^4.4.23",
+ "@smithy/middleware-stack": "^4.2.11",
+ "@smithy/protocol-http": "^5.3.11",
"@smithy/types": "^4.13.0",
- "@smithy/util-stream": "^4.5.15",
+ "@smithy/util-stream": "^4.5.17",
"tslib": "^2.6.2"
},
"engines": {
@@ -2368,8 +3065,6 @@
},
"node_modules/@smithy/types": {
"version": "4.13.0",
- "resolved": "https://registry.npmjs.org/@smithy/types/-/types-4.13.0.tgz",
- "integrity": "sha512-COuLsZILbbQsdrwKQpkkpyep7lCsByxwj7m0Mg5v66/ZTyenlfBc40/QFQ5chO0YN/PNEH1Bi3fGtfXPnYNeDw==",
"license": "Apache-2.0",
"dependencies": {
"tslib": "^2.6.2"
@@ -2379,12 +3074,12 @@
}
},
"node_modules/@smithy/url-parser": {
- "version": "4.2.10",
- "resolved": "https://registry.npmjs.org/@smithy/url-parser/-/url-parser-4.2.10.tgz",
- "integrity": "sha512-uypjF7fCDsRk26u3qHmFI/ePL7bxxB9vKkE+2WKEciHhz+4QtbzWiHRVNRJwU3cKhrYDYQE3b0MRFtqfLYdA4A==",
+ "version": "4.2.11",
+ "resolved": "https://registry.npmjs.org/@smithy/url-parser/-/url-parser-4.2.11.tgz",
+ "integrity": "sha512-oTAGGHo8ZYc5VZsBREzuf5lf2pAurJQsccMusVZ85wDkX66ojEc/XauiGjzCj50A61ObFTPe6d7Pyt6UBYaing==",
"license": "Apache-2.0",
"dependencies": {
- "@smithy/querystring-parser": "^4.2.10",
+ "@smithy/querystring-parser": "^4.2.11",
"@smithy/types": "^4.13.0",
"tslib": "^2.6.2"
},
@@ -2393,13 +3088,13 @@
}
},
"node_modules/@smithy/util-base64": {
- "version": "4.3.1",
- "resolved": "https://registry.npmjs.org/@smithy/util-base64/-/util-base64-4.3.1.tgz",
- "integrity": "sha512-BKGuawX4Doq/bI/uEmg+Zyc36rJKWuin3py89PquXBIBqmbnJwBBsmKhdHfNEp0+A4TDgLmT/3MSKZ1SxHcR6w==",
+ "version": "4.3.2",
+ "resolved": "https://registry.npmjs.org/@smithy/util-base64/-/util-base64-4.3.2.tgz",
+ "integrity": "sha512-XRH6b0H/5A3SgblmMa5ErXQ2XKhfbQB+Fm/oyLZ2O2kCUrwgg55bU0RekmzAhuwOjA9qdN5VU2BprOvGGUkOOQ==",
"license": "Apache-2.0",
"dependencies": {
- "@smithy/util-buffer-from": "^4.2.1",
- "@smithy/util-utf8": "^4.2.1",
+ "@smithy/util-buffer-from": "^4.2.2",
+ "@smithy/util-utf8": "^4.2.2",
"tslib": "^2.6.2"
},
"engines": {
@@ -2407,9 +3102,9 @@
}
},
"node_modules/@smithy/util-body-length-browser": {
- "version": "4.2.1",
- "resolved": "https://registry.npmjs.org/@smithy/util-body-length-browser/-/util-body-length-browser-4.2.1.tgz",
- "integrity": "sha512-SiJeLiozrAoCrgDBUgsVbmqHmMgg/2bA15AzcbcW+zan7SuyAVHN4xTSbq0GlebAIwlcaX32xacnrG488/J/6g==",
+ "version": "4.2.2",
+ "resolved": "https://registry.npmjs.org/@smithy/util-body-length-browser/-/util-body-length-browser-4.2.2.tgz",
+ "integrity": "sha512-JKCrLNOup3OOgmzeaKQwi4ZCTWlYR5H4Gm1r2uTMVBXoemo1UEghk5vtMi1xSu2ymgKVGW631e2fp9/R610ZjQ==",
"license": "Apache-2.0",
"dependencies": {
"tslib": "^2.6.2"
@@ -2419,9 +3114,9 @@
}
},
"node_modules/@smithy/util-body-length-node": {
- "version": "4.2.2",
- "resolved": "https://registry.npmjs.org/@smithy/util-body-length-node/-/util-body-length-node-4.2.2.tgz",
- "integrity": "sha512-4rHqBvxtJEBvsZcFQSPQqXP2b/yy/YlB66KlcEgcH2WNoOKCKB03DSLzXmOsXjbl8dJ4OEYTn31knhdznwk7zw==",
+ "version": "4.2.3",
+ "resolved": "https://registry.npmjs.org/@smithy/util-body-length-node/-/util-body-length-node-4.2.3.tgz",
+ "integrity": "sha512-ZkJGvqBzMHVHE7r/hcuCxlTY8pQr1kMtdsVPs7ex4mMU+EAbcXppfo5NmyxMYi2XU49eqaz56j2gsk4dHHPG/g==",
"license": "Apache-2.0",
"dependencies": {
"tslib": "^2.6.2"
@@ -2431,12 +3126,12 @@
}
},
"node_modules/@smithy/util-buffer-from": {
- "version": "4.2.1",
- "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-4.2.1.tgz",
- "integrity": "sha512-/swhmt1qTiVkaejlmMPPDgZhEaWb/HWMGRBheaxwuVkusp/z+ErJyQxO6kaXumOciZSWlmq6Z5mNylCd33X7Ig==",
+ "version": "4.2.2",
+ "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-4.2.2.tgz",
+ "integrity": "sha512-FDXD7cvUoFWwN6vtQfEta540Y/YBe5JneK3SoZg9bThSoOAC/eGeYEua6RkBgKjGa/sz6Y+DuBZj3+YEY21y4Q==",
"license": "Apache-2.0",
"dependencies": {
- "@smithy/is-array-buffer": "^4.2.1",
+ "@smithy/is-array-buffer": "^4.2.2",
"tslib": "^2.6.2"
},
"engines": {
@@ -2444,9 +3139,9 @@
}
},
"node_modules/@smithy/util-config-provider": {
- "version": "4.2.1",
- "resolved": "https://registry.npmjs.org/@smithy/util-config-provider/-/util-config-provider-4.2.1.tgz",
- "integrity": "sha512-462id/00U8JWFw6qBuTSWfN5TxOHvDu4WliI97qOIOnuC/g+NDAknTU8eoGXEPlLkRVgWEr03jJBLV4o2FL8+A==",
+ "version": "4.2.2",
+ "resolved": "https://registry.npmjs.org/@smithy/util-config-provider/-/util-config-provider-4.2.2.tgz",
+ "integrity": "sha512-dWU03V3XUprJwaUIFVv4iOnS1FC9HnMHDfUrlNDSh4315v0cWyaIErP8KiqGVbf5z+JupoVpNM7ZB3jFiTejvQ==",
"license": "Apache-2.0",
"dependencies": {
"tslib": "^2.6.2"
@@ -2456,13 +3151,13 @@
}
},
"node_modules/@smithy/util-defaults-mode-browser": {
- "version": "4.3.36",
- "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-browser/-/util-defaults-mode-browser-4.3.36.tgz",
- "integrity": "sha512-R0smq7EHQXRVMxkAxtH5akJ/FvgAmNF6bUy/GwY/N20T4GrwjT633NFm0VuRpC+8Bbv8R9A0DoJ9OiZL/M3xew==",
+ "version": "4.3.39",
+ "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-browser/-/util-defaults-mode-browser-4.3.39.tgz",
+ "integrity": "sha512-ui7/Ho/+VHqS7Km2wBw4/Ab4RktoiSshgcgpJzC4keFPs6tLJS4IQwbeahxQS3E/w98uq6E1mirCH/id9xIXeQ==",
"license": "Apache-2.0",
"dependencies": {
- "@smithy/property-provider": "^4.2.10",
- "@smithy/smithy-client": "^4.12.0",
+ "@smithy/property-provider": "^4.2.11",
+ "@smithy/smithy-client": "^4.12.3",
"@smithy/types": "^4.13.0",
"tslib": "^2.6.2"
},
@@ -2471,16 +3166,16 @@
}
},
"node_modules/@smithy/util-defaults-mode-node": {
- "version": "4.2.39",
- "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-node/-/util-defaults-mode-node-4.2.39.tgz",
- "integrity": "sha512-otWuoDm35btJV1L8MyHrPl462B07QCdMTktKc7/yM+Psv6KbED/ziXiHnmr7yPHUjfIwE9S8Max0LO24Mo3ZVg==",
+ "version": "4.2.42",
+ "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-node/-/util-defaults-mode-node-4.2.42.tgz",
+ "integrity": "sha512-QDA84CWNe8Akpj15ofLO+1N3Rfg8qa2K5uX0y6HnOp4AnRYRgWrKx/xzbYNbVF9ZsyJUYOfcoaN3y93wA/QJ2A==",
"license": "Apache-2.0",
"dependencies": {
- "@smithy/config-resolver": "^4.4.9",
- "@smithy/credential-provider-imds": "^4.2.10",
- "@smithy/node-config-provider": "^4.3.10",
- "@smithy/property-provider": "^4.2.10",
- "@smithy/smithy-client": "^4.12.0",
+ "@smithy/config-resolver": "^4.4.10",
+ "@smithy/credential-provider-imds": "^4.2.11",
+ "@smithy/node-config-provider": "^4.3.11",
+ "@smithy/property-provider": "^4.2.11",
+ "@smithy/smithy-client": "^4.12.3",
"@smithy/types": "^4.13.0",
"tslib": "^2.6.2"
},
@@ -2489,12 +3184,12 @@
}
},
"node_modules/@smithy/util-endpoints": {
- "version": "3.3.1",
- "resolved": "https://registry.npmjs.org/@smithy/util-endpoints/-/util-endpoints-3.3.1.tgz",
- "integrity": "sha512-xyctc4klmjmieQiF9I1wssBWleRV0RhJ2DpO8+8yzi2LO1Z+4IWOZNGZGNj4+hq9kdo+nyfrRLmQTzc16Op2Vg==",
+ "version": "3.3.2",
+ "resolved": "https://registry.npmjs.org/@smithy/util-endpoints/-/util-endpoints-3.3.2.tgz",
+ "integrity": "sha512-+4HFLpE5u29AbFlTdlKIT7jfOzZ8PDYZKTb3e+AgLz986OYwqTourQ5H+jg79/66DB69Un1+qKecLnkZdAsYcA==",
"license": "Apache-2.0",
"dependencies": {
- "@smithy/node-config-provider": "^4.3.10",
+ "@smithy/node-config-provider": "^4.3.11",
"@smithy/types": "^4.13.0",
"tslib": "^2.6.2"
},
@@ -2503,9 +3198,9 @@
}
},
"node_modules/@smithy/util-hex-encoding": {
- "version": "4.2.1",
- "resolved": "https://registry.npmjs.org/@smithy/util-hex-encoding/-/util-hex-encoding-4.2.1.tgz",
- "integrity": "sha512-c1hHtkgAWmE35/50gmdKajgGAKV3ePJ7t6UtEmpfCWJmQE9BQAQPz0URUVI89eSkcDqCtzqllxzG28IQoZPvwA==",
+ "version": "4.2.2",
+ "resolved": "https://registry.npmjs.org/@smithy/util-hex-encoding/-/util-hex-encoding-4.2.2.tgz",
+ "integrity": "sha512-Qcz3W5vuHK4sLQdyT93k/rfrUwdJ8/HZ+nMUOyGdpeGA1Wxt65zYwi3oEl9kOM+RswvYq90fzkNDahPS8K0OIg==",
"license": "Apache-2.0",
"dependencies": {
"tslib": "^2.6.2"
@@ -2515,9 +3210,9 @@
}
},
"node_modules/@smithy/util-middleware": {
- "version": "4.2.10",
- "resolved": "https://registry.npmjs.org/@smithy/util-middleware/-/util-middleware-4.2.10.tgz",
- "integrity": "sha512-LxaQIWLp4y0r72eA8mwPNQ9va4h5KeLM0I3M/HV9klmFaY2kN766wf5vsTzmaOpNNb7GgXAd9a25P3h8T49PSA==",
+ "version": "4.2.11",
+ "resolved": "https://registry.npmjs.org/@smithy/util-middleware/-/util-middleware-4.2.11.tgz",
+ "integrity": "sha512-r3dtF9F+TpSZUxpOVVtPfk09Rlo4lT6ORBqEvX3IBT6SkQAdDSVKR5GcfmZbtl7WKhKnmb3wbDTQ6ibR2XHClw==",
"license": "Apache-2.0",
"dependencies": {
"@smithy/types": "^4.13.0",
@@ -2528,12 +3223,12 @@
}
},
"node_modules/@smithy/util-retry": {
- "version": "4.2.10",
- "resolved": "https://registry.npmjs.org/@smithy/util-retry/-/util-retry-4.2.10.tgz",
- "integrity": "sha512-HrBzistfpyE5uqTwiyLsFHscgnwB0kgv8vySp7q5kZ0Eltn/tjosaSGGDj/jJ9ys7pWzIP/icE2d+7vMKXLv7A==",
+ "version": "4.2.11",
+ "resolved": "https://registry.npmjs.org/@smithy/util-retry/-/util-retry-4.2.11.tgz",
+ "integrity": "sha512-XSZULmL5x6aCTTii59wJqKsY1l3eMIAomRAccW7Tzh9r8s7T/7rdo03oektuH5jeYRlJMPcNP92EuRDvk9aXbw==",
"license": "Apache-2.0",
"dependencies": {
- "@smithy/service-error-classification": "^4.2.10",
+ "@smithy/service-error-classification": "^4.2.11",
"@smithy/types": "^4.13.0",
"tslib": "^2.6.2"
},
@@ -2542,18 +3237,18 @@
}
},
"node_modules/@smithy/util-stream": {
- "version": "4.5.15",
- "resolved": "https://registry.npmjs.org/@smithy/util-stream/-/util-stream-4.5.15.tgz",
- "integrity": "sha512-OlOKnaqnkU9X+6wEkd7mN+WB7orPbCVDauXOj22Q7VtiTkvy7ZdSsOg4QiNAZMgI4OkvNf+/VLUC3VXkxuWJZw==",
+ "version": "4.5.17",
+ "resolved": "https://registry.npmjs.org/@smithy/util-stream/-/util-stream-4.5.17.tgz",
+ "integrity": "sha512-793BYZ4h2JAQkNHcEnyFxDTcZbm9bVybD0UV/LEWmZ5bkTms7JqjfrLMi2Qy0E5WFcCzLwCAPgcvcvxoeALbAQ==",
"license": "Apache-2.0",
"dependencies": {
- "@smithy/fetch-http-handler": "^5.3.11",
- "@smithy/node-http-handler": "^4.4.12",
+ "@smithy/fetch-http-handler": "^5.3.13",
+ "@smithy/node-http-handler": "^4.4.14",
"@smithy/types": "^4.13.0",
- "@smithy/util-base64": "^4.3.1",
- "@smithy/util-buffer-from": "^4.2.1",
- "@smithy/util-hex-encoding": "^4.2.1",
- "@smithy/util-utf8": "^4.2.1",
+ "@smithy/util-base64": "^4.3.2",
+ "@smithy/util-buffer-from": "^4.2.2",
+ "@smithy/util-hex-encoding": "^4.2.2",
+ "@smithy/util-utf8": "^4.2.2",
"tslib": "^2.6.2"
},
"engines": {
@@ -2561,9 +3256,9 @@
}
},
"node_modules/@smithy/util-uri-escape": {
- "version": "4.2.1",
- "resolved": "https://registry.npmjs.org/@smithy/util-uri-escape/-/util-uri-escape-4.2.1.tgz",
- "integrity": "sha512-YmiUDn2eo2IOiWYYvGQkgX5ZkBSiTQu4FlDo5jNPpAxng2t6Sjb6WutnZV9l6VR4eJul1ABmCrnWBC9hKHQa6Q==",
+ "version": "4.2.2",
+ "resolved": "https://registry.npmjs.org/@smithy/util-uri-escape/-/util-uri-escape-4.2.2.tgz",
+ "integrity": "sha512-2kAStBlvq+lTXHyAZYfJRb/DfS3rsinLiwb+69SstC9Vb0s9vNWkRwpnj918Pfi85mzi42sOqdV72OLxWAISnw==",
"license": "Apache-2.0",
"dependencies": {
"tslib": "^2.6.2"
@@ -2573,12 +3268,24 @@
}
},
"node_modules/@smithy/util-utf8": {
- "version": "4.2.1",
- "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-4.2.1.tgz",
- "integrity": "sha512-DSIwNaWtmzrNQHv8g7DBGR9mulSit65KSj5ymGEIAknmIN8IpbZefEep10LaMG/P/xquwbmJ1h9ectz8z6mV6g==",
+ "version": "4.2.2",
+ "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-4.2.2.tgz",
+ "integrity": "sha512-75MeYpjdWRe8M5E3AW0O4Cx3UadweS+cwdXjwYGBW5h/gxxnbeZ877sLPX/ZJA9GVTlL/qG0dXP29JWFCD1Ayw==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@smithy/util-buffer-from": "^4.2.2",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "node_modules/@smithy/util-waiter": {
+ "version": "4.2.9",
"license": "Apache-2.0",
"dependencies": {
- "@smithy/util-buffer-from": "^4.2.1",
+ "@smithy/abort-controller": "^4.2.9",
+ "@smithy/types": "^4.12.1",
"tslib": "^2.6.2"
},
"engines": {
@@ -2586,9 +3293,9 @@
}
},
"node_modules/@smithy/uuid": {
- "version": "1.1.1",
- "resolved": "https://registry.npmjs.org/@smithy/uuid/-/uuid-1.1.1.tgz",
- "integrity": "sha512-dSfDCeihDmZlV2oyr0yWPTUfh07suS+R5OB+FZGiv/hHyK3hrFBW5rR1UYjfa57vBsrP9lciFkRPzebaV1Qujw==",
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/@smithy/uuid/-/uuid-1.1.2.tgz",
+ "integrity": "sha512-O/IEdcCUKkubz60tFbGA7ceITTAJsty+lBjNoorP4Z6XRqaFb/OjQjZODophEcuq68nKm6/0r+6/lLQ+XVpk8g==",
"license": "Apache-2.0",
"dependencies": {
"tslib": "^2.6.2"
@@ -2615,6 +3322,28 @@
"eslint": ">=8.40.0"
}
},
+ "node_modules/@stylistic/eslint-plugin/node_modules/eslint-visitor-keys": {
+ "version": "4.2.1",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
+ "node_modules/@stylistic/eslint-plugin/node_modules/picomatch": {
+ "version": "4.0.3",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/jonschlinkert"
+ }
+ },
"node_modules/@tootallnate/once": {
"version": "2.0.0",
"dev": true,
@@ -2624,7 +3353,7 @@
}
},
"node_modules/@tsconfig/node10": {
- "version": "1.0.12",
+ "version": "1.0.11",
"dev": true,
"license": "MIT"
},
@@ -2644,12 +3373,12 @@
"license": "MIT"
},
"node_modules/@tsconfig/node22": {
- "version": "22.0.5",
+ "version": "22.0.2",
"dev": true,
"license": "MIT"
},
"node_modules/@types/aws-lambda": {
- "version": "8.10.160",
+ "version": "8.10.150",
"dev": true,
"license": "MIT"
},
@@ -2683,11 +3412,11 @@
}
},
"node_modules/@types/babel__traverse": {
- "version": "7.28.0",
+ "version": "7.20.7",
"dev": true,
"license": "MIT",
"dependencies": {
- "@babel/types": "^7.28.2"
+ "@babel/types": "^7.20.7"
}
},
"node_modules/@types/estree": {
@@ -2748,12 +3477,19 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/@types/json5": {
+ "version": "0.0.29",
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "peer": true
+ },
"node_modules/@types/node": {
- "version": "25.3.0",
+ "version": "24.0.3",
"dev": true,
"license": "MIT",
"dependencies": {
- "undici-types": "~7.18.0"
+ "undici-types": "~7.8.0"
}
},
"node_modules/@types/stack-utils": {
@@ -2767,7 +3503,7 @@
"license": "MIT"
},
"node_modules/@types/yargs": {
- "version": "17.0.35",
+ "version": "17.0.33",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -2806,14 +3542,6 @@
"typescript": ">=4.8.4 <6.0.0"
}
},
- "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": {
- "version": "7.0.5",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">= 4"
- }
- },
"node_modules/@typescript-eslint/parser": {
"version": "8.56.1",
"dev": true,
@@ -2998,28 +3726,16 @@
"url": "https://opencollective.com/eslint"
}
},
- "node_modules/@unrs/resolver-binding-linux-x64-gnu": {
- "version": "1.11.1",
- "cpu": [
- "x64"
- ],
- "dev": true,
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ]
- },
- "node_modules/@unrs/resolver-binding-linux-x64-musl": {
- "version": "1.11.1",
+ "node_modules/@unrs/resolver-binding-darwin-arm64": {
+ "version": "1.9.2",
"cpu": [
- "x64"
+ "arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
- "linux"
+ "darwin"
]
},
"node_modules/abab": {
@@ -3028,7 +3744,7 @@
"license": "BSD-3-Clause"
},
"node_modules/acorn": {
- "version": "8.16.0",
+ "version": "8.15.0",
"dev": true,
"license": "MIT",
"bin": {
@@ -3056,7 +3772,7 @@
}
},
"node_modules/acorn-walk": {
- "version": "8.3.5",
+ "version": "8.3.4",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -3079,8 +3795,6 @@
},
"node_modules/aggregate-error": {
"version": "3.1.0",
- "resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.1.0.tgz",
- "integrity": "sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==",
"license": "MIT",
"dependencies": {
"clean-stack": "^2.0.0",
@@ -3092,15 +3806,13 @@
},
"node_modules/aggregate-error/node_modules/indent-string": {
"version": "4.0.0",
- "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz",
- "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==",
"license": "MIT",
"engines": {
"node": ">=8"
}
},
"node_modules/ajv": {
- "version": "6.14.0",
+ "version": "6.12.6",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -3116,8 +3828,6 @@
},
"node_modules/ajv-formats": {
"version": "2.1.1",
- "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.1.tgz",
- "integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==",
"license": "MIT",
"dependencies": {
"ajv": "^8.0.0"
@@ -3133,8 +3843,6 @@
},
"node_modules/ajv-formats/node_modules/ajv": {
"version": "8.18.0",
- "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz",
- "integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==",
"license": "MIT",
"dependencies": {
"fast-deep-equal": "^3.1.3",
@@ -3149,8 +3857,6 @@
},
"node_modules/ajv-formats/node_modules/json-schema-traverse": {
"version": "1.0.0",
- "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz",
- "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==",
"license": "MIT"
},
"node_modules/ansi-escapes": {
@@ -3167,9 +3873,19 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
+ "node_modules/ansi-escapes/node_modules/type-fest": {
+ "version": "0.21.3",
+ "dev": true,
+ "license": "(MIT OR CC0-1.0)",
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
"node_modules/ansi-regex": {
"version": "5.0.1",
- "dev": true,
"license": "MIT",
"engines": {
"node": ">=8"
@@ -3177,7 +3893,6 @@
},
"node_modules/ansi-styles": {
"version": "4.3.0",
- "dev": true,
"license": "MIT",
"dependencies": {
"color-convert": "^2.0.1"
@@ -3197,19 +3912,8 @@
"normalize-path": "^3.0.0",
"picomatch": "^2.0.4"
},
- "engines": {
- "node": ">= 8"
- }
- },
- "node_modules/anymatch/node_modules/picomatch": {
- "version": "2.3.1",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=8.6"
- },
- "funding": {
- "url": "https://github.com/sponsors/jonschlinkert"
+ "engines": {
+ "node": ">= 8"
}
},
"node_modules/arg": {
@@ -3266,6 +3970,49 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/array.prototype.findlast": {
+ "version": "1.2.5",
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "peer": true,
+ "dependencies": {
+ "call-bind": "^1.0.7",
+ "define-properties": "^1.2.1",
+ "es-abstract": "^1.23.2",
+ "es-errors": "^1.3.0",
+ "es-object-atoms": "^1.0.0",
+ "es-shim-unscopables": "^1.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/array.prototype.findlastindex": {
+ "version": "1.2.6",
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "peer": true,
+ "dependencies": {
+ "call-bind": "^1.0.8",
+ "call-bound": "^1.0.4",
+ "define-properties": "^1.2.1",
+ "es-abstract": "^1.23.9",
+ "es-errors": "^1.3.0",
+ "es-object-atoms": "^1.1.1",
+ "es-shim-unscopables": "^1.1.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
"node_modules/array.prototype.flat": {
"version": "1.3.3",
"dev": true,
@@ -3300,6 +4047,23 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/array.prototype.tosorted": {
+ "version": "1.1.4",
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "peer": true,
+ "dependencies": {
+ "call-bind": "^1.0.7",
+ "define-properties": "^1.2.1",
+ "es-abstract": "^1.23.3",
+ "es-errors": "^1.3.0",
+ "es-shim-unscopables": "^1.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
"node_modules/arraybuffer.prototype.slice": {
"version": "1.0.4",
"dev": true,
@@ -3325,6 +4089,15 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/astral-regex": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-2.0.0.tgz",
+ "integrity": "sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
"node_modules/async-function": {
"version": "1.0.0",
"dev": true,
@@ -3335,8 +4108,6 @@
},
"node_modules/async-wait-until": {
"version": "2.0.31",
- "resolved": "https://registry.npmjs.org/async-wait-until/-/async-wait-until-2.0.31.tgz",
- "integrity": "sha512-9VCfHvc4f36oT6sG5p16aKc9zojf3wF4FrjNDxU3Db51SJ1bQ5lWAWtQDDZPysTwSLKBDzNZ083qPkTIj6XnrA==",
"dev": true,
"license": "MIT",
"engines": {
@@ -3355,8 +4126,6 @@
},
"node_modules/atomic-sleep": {
"version": "1.0.0",
- "resolved": "https://registry.npmjs.org/atomic-sleep/-/atomic-sleep-1.0.0.tgz",
- "integrity": "sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ==",
"license": "MIT",
"engines": {
"node": ">=8.0.0"
@@ -3377,8 +4146,6 @@
},
"node_modules/aws-embedded-metrics": {
"version": "4.2.1",
- "resolved": "https://registry.npmjs.org/aws-embedded-metrics/-/aws-embedded-metrics-4.2.1.tgz",
- "integrity": "sha512-uzydBXlGQVTB2sZ9ACCQZM3y0u4wdvxxRKFL9LP6RdfI2GcOrCcAsz65UKQvX9iagxFhah322VvvatgP8E7MIg==",
"license": "Apache-2.0",
"dependencies": {
"@datastructures-js/heap": "^4.0.2"
@@ -3388,7 +4155,7 @@
}
},
"node_modules/axe-core": {
- "version": "4.11.1",
+ "version": "4.10.3",
"dev": true,
"license": "MPL-2.0",
"engines": {
@@ -3476,7 +4243,7 @@
}
},
"node_modules/babel-preset-current-node-syntax": {
- "version": "1.2.0",
+ "version": "1.1.0",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -3497,7 +4264,7 @@
"@babel/plugin-syntax-top-level-await": "^7.14.5"
},
"peerDependencies": {
- "@babel/core": "^7.0.0 || ^8.0.0-0"
+ "@babel/core": "^7.0.0"
}
},
"node_modules/babel-preset-jest": {
@@ -3516,28 +4283,12 @@
}
},
"node_modules/balanced-match": {
- "version": "4.0.4",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": "18 || 20 || >=22"
- }
- },
- "node_modules/baseline-browser-mapping": {
- "version": "2.10.0",
+ "version": "1.0.2",
"dev": true,
- "license": "Apache-2.0",
- "bin": {
- "baseline-browser-mapping": "dist/cli.cjs"
- },
- "engines": {
- "node": ">=6.0.0"
- }
+ "license": "MIT"
},
"node_modules/bignumber.js": {
"version": "9.3.1",
- "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.3.1.tgz",
- "integrity": "sha512-Ko0uX15oIUS7wJ3Rb30Fs6SkVbLmPBAKdlm7q9+ak9bbIeFf0MwuBsQV6z7+X768/cHsfg+WlysDWJcmthjsjQ==",
"license": "MIT",
"engines": {
"node": "*"
@@ -3545,12 +4296,10 @@
},
"node_modules/bowser": {
"version": "2.14.1",
- "resolved": "https://registry.npmjs.org/bowser/-/bowser-2.14.1.tgz",
- "integrity": "sha512-tzPjzCxygAKWFOJP011oxFHs57HzIhOEracIgAePE4pqB3LikALKnSzUyU4MGs9/iCEUuHlAJTjTc5M+u7YEGg==",
"license": "MIT"
},
"node_modules/brace-expansion": {
- "version": "5.0.3",
+ "version": "5.0.4",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -3560,6 +4309,14 @@
"node": "18 || 20 || >=22"
}
},
+ "node_modules/brace-expansion/node_modules/balanced-match": {
+ "version": "4.0.4",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "18 || 20 || >=22"
+ }
+ },
"node_modules/braces": {
"version": "3.0.3",
"dev": true,
@@ -3572,7 +4329,7 @@
}
},
"node_modules/browserslist": {
- "version": "4.28.1",
+ "version": "4.25.0",
"dev": true,
"funding": [
{
@@ -3590,11 +4347,10 @@
],
"license": "MIT",
"dependencies": {
- "baseline-browser-mapping": "^2.9.0",
- "caniuse-lite": "^1.0.30001759",
- "electron-to-chromium": "^1.5.263",
- "node-releases": "^2.0.27",
- "update-browserslist-db": "^1.2.0"
+ "caniuse-lite": "^1.0.30001718",
+ "electron-to-chromium": "^1.5.160",
+ "node-releases": "^2.0.19",
+ "update-browserslist-db": "^1.1.3"
},
"bin": {
"browserslist": "cli.js"
@@ -3704,7 +4460,7 @@
}
},
"node_modules/caniuse-lite": {
- "version": "1.0.30001774",
+ "version": "1.0.30001724",
"dev": true,
"funding": [
{
@@ -3746,7 +4502,7 @@
}
},
"node_modules/ci-info": {
- "version": "4.4.0",
+ "version": "4.2.0",
"dev": true,
"funding": [
{
@@ -3785,16 +4541,17 @@
},
"node_modules/clean-stack": {
"version": "2.2.0",
- "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz",
- "integrity": "sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==",
"license": "MIT",
"engines": {
"node": ">=6"
}
},
+ "node_modules/client-subscriptions-management": {
+ "resolved": "tools/client-subscriptions-management",
+ "link": true
+ },
"node_modules/cliui": {
"version": "8.0.1",
- "dev": true,
"license": "ISC",
"dependencies": {
"string-width": "^4.2.0",
@@ -3807,8 +4564,6 @@
},
"node_modules/cloudevents": {
"version": "8.0.3",
- "resolved": "https://registry.npmjs.org/cloudevents/-/cloudevents-8.0.3.tgz",
- "integrity": "sha512-wTixKNjfLeyj9HQpESvLVVO4xgdqdvX4dTeg1IZ2SCunu/fxVzCamcIZneEyj31V82YolFCKwVeSkr8zResB0Q==",
"license": "Apache-2.0",
"dependencies": {
"ajv": "^8.11.0",
@@ -3824,8 +4579,6 @@
},
"node_modules/cloudevents/node_modules/ajv": {
"version": "8.18.0",
- "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz",
- "integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==",
"license": "MIT",
"dependencies": {
"fast-deep-equal": "^3.1.3",
@@ -3840,8 +4593,6 @@
},
"node_modules/cloudevents/node_modules/json-schema-traverse": {
"version": "1.0.0",
- "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz",
- "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==",
"license": "MIT"
},
"node_modules/co": {
@@ -3855,12 +4606,13 @@
},
"node_modules/collect-v8-coverage": {
"version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/collect-v8-coverage/-/collect-v8-coverage-1.0.3.tgz",
+ "integrity": "sha512-1L5aqIkwPfiodaMgQunkF1zRhNqifHBmtbbbxcr6yVxxBnliw4TDOW6NxpO8DJLgJ16OT+Y4ztZqP6p/FtXnAw==",
"dev": true,
"license": "MIT"
},
"node_modules/color-convert": {
"version": "2.0.1",
- "dev": true,
"license": "MIT",
"dependencies": {
"color-name": "~1.1.4"
@@ -3871,7 +4623,6 @@
},
"node_modules/color-name": {
"version": "1.1.4",
- "dev": true,
"license": "MIT"
},
"node_modules/combined-stream": {
@@ -3886,13 +4637,18 @@
}
},
"node_modules/comment-parser": {
- "version": "1.4.5",
+ "version": "1.4.1",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 12.0.0"
}
},
+ "node_modules/concat-map": {
+ "version": "0.0.1",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/confusing-browser-globals": {
"version": "1.0.11",
"dev": true,
@@ -3904,11 +4660,11 @@
"license": "MIT"
},
"node_modules/core-js-compat": {
- "version": "3.48.0",
+ "version": "3.43.0",
"dev": true,
"license": "MIT",
"dependencies": {
- "browserslist": "^4.28.1"
+ "browserslist": "^4.25.0"
},
"funding": {
"type": "opencollective",
@@ -4065,12 +4821,12 @@
}
},
"node_modules/decimal.js": {
- "version": "10.6.0",
+ "version": "10.5.0",
"dev": true,
"license": "MIT"
},
"node_modules/dedent": {
- "version": "1.7.1",
+ "version": "1.6.0",
"dev": true,
"license": "MIT",
"peerDependencies": {
@@ -4143,7 +4899,7 @@
}
},
"node_modules/diff": {
- "version": "4.0.4",
+ "version": "4.0.2",
"dev": true,
"license": "BSD-3-Clause",
"engines": {
@@ -4158,6 +4914,19 @@
"node": "^14.15.0 || ^16.10.0 || >=18.0.0"
}
},
+ "node_modules/doctrine": {
+ "version": "2.1.0",
+ "dev": true,
+ "license": "Apache-2.0",
+ "optional": true,
+ "peer": true,
+ "dependencies": {
+ "esutils": "^2.0.2"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
"node_modules/dom-serializer": {
"version": "2.0.0",
"dev": true,
@@ -4244,7 +5013,7 @@
}
},
"node_modules/electron-to-chromium": {
- "version": "1.5.302",
+ "version": "1.5.173",
"dev": true,
"license": "ISC"
},
@@ -4260,12 +5029,11 @@
}
},
"node_modules/emoji-regex": {
- "version": "9.2.2",
- "dev": true,
+ "version": "8.0.0",
"license": "MIT"
},
"node_modules/entities": {
- "version": "7.0.1",
+ "version": "6.0.1",
"dev": true,
"license": "BSD-2-Clause",
"engines": {
@@ -4276,7 +5044,7 @@
}
},
"node_modules/error-ex": {
- "version": "1.3.4",
+ "version": "1.3.2",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -4284,7 +5052,7 @@
}
},
"node_modules/es-abstract": {
- "version": "1.24.1",
+ "version": "1.24.0",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -4364,6 +5132,34 @@
"node": ">= 0.4"
}
},
+ "node_modules/es-iterator-helpers": {
+ "version": "1.2.1",
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "peer": true,
+ "dependencies": {
+ "call-bind": "^1.0.8",
+ "call-bound": "^1.0.3",
+ "define-properties": "^1.2.1",
+ "es-abstract": "^1.23.6",
+ "es-errors": "^1.3.0",
+ "es-set-tostringtag": "^2.0.3",
+ "function-bind": "^1.1.2",
+ "get-intrinsic": "^1.2.6",
+ "globalthis": "^1.0.4",
+ "gopd": "^1.2.0",
+ "has-property-descriptors": "^1.0.2",
+ "has-proto": "^1.2.0",
+ "has-symbols": "^1.1.0",
+ "internal-slot": "^1.1.0",
+ "iterator.prototype": "^1.1.4",
+ "safe-array-concat": "^1.1.3"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
"node_modules/es-object-atoms": {
"version": "1.1.1",
"license": "MIT",
@@ -4416,7 +5212,7 @@
}
},
"node_modules/esbuild": {
- "version": "0.25.12",
+ "version": "0.25.0",
"hasInstallScript": true,
"license": "MIT",
"bin": {
@@ -4426,37 +5222,35 @@
"node": ">=18"
},
"optionalDependencies": {
- "@esbuild/aix-ppc64": "0.25.12",
- "@esbuild/android-arm": "0.25.12",
- "@esbuild/android-arm64": "0.25.12",
- "@esbuild/android-x64": "0.25.12",
- "@esbuild/darwin-arm64": "0.25.12",
- "@esbuild/darwin-x64": "0.25.12",
- "@esbuild/freebsd-arm64": "0.25.12",
- "@esbuild/freebsd-x64": "0.25.12",
- "@esbuild/linux-arm": "0.25.12",
- "@esbuild/linux-arm64": "0.25.12",
- "@esbuild/linux-ia32": "0.25.12",
- "@esbuild/linux-loong64": "0.25.12",
- "@esbuild/linux-mips64el": "0.25.12",
- "@esbuild/linux-ppc64": "0.25.12",
- "@esbuild/linux-riscv64": "0.25.12",
- "@esbuild/linux-s390x": "0.25.12",
- "@esbuild/linux-x64": "0.25.12",
- "@esbuild/netbsd-arm64": "0.25.12",
- "@esbuild/netbsd-x64": "0.25.12",
- "@esbuild/openbsd-arm64": "0.25.12",
- "@esbuild/openbsd-x64": "0.25.12",
- "@esbuild/openharmony-arm64": "0.25.12",
- "@esbuild/sunos-x64": "0.25.12",
- "@esbuild/win32-arm64": "0.25.12",
- "@esbuild/win32-ia32": "0.25.12",
- "@esbuild/win32-x64": "0.25.12"
+ "@esbuild/aix-ppc64": "0.25.0",
+ "@esbuild/android-arm": "0.25.0",
+ "@esbuild/android-arm64": "0.25.0",
+ "@esbuild/android-x64": "0.25.0",
+ "@esbuild/darwin-arm64": "0.25.0",
+ "@esbuild/darwin-x64": "0.25.0",
+ "@esbuild/freebsd-arm64": "0.25.0",
+ "@esbuild/freebsd-x64": "0.25.0",
+ "@esbuild/linux-arm": "0.25.0",
+ "@esbuild/linux-arm64": "0.25.0",
+ "@esbuild/linux-ia32": "0.25.0",
+ "@esbuild/linux-loong64": "0.25.0",
+ "@esbuild/linux-mips64el": "0.25.0",
+ "@esbuild/linux-ppc64": "0.25.0",
+ "@esbuild/linux-riscv64": "0.25.0",
+ "@esbuild/linux-s390x": "0.25.0",
+ "@esbuild/linux-x64": "0.25.0",
+ "@esbuild/netbsd-arm64": "0.25.0",
+ "@esbuild/netbsd-x64": "0.25.0",
+ "@esbuild/openbsd-arm64": "0.25.0",
+ "@esbuild/openbsd-x64": "0.25.0",
+ "@esbuild/sunos-x64": "0.25.0",
+ "@esbuild/win32-arm64": "0.25.0",
+ "@esbuild/win32-ia32": "0.25.0",
+ "@esbuild/win32-x64": "0.25.0"
}
},
"node_modules/escalade": {
"version": "3.2.0",
- "dev": true,
"license": "MIT",
"engines": {
"node": ">=6"
@@ -4606,7 +5400,7 @@
}
},
"node_modules/eslint-config-airbnb-extended/node_modules/globals": {
- "version": "16.5.0",
+ "version": "16.2.0",
"dev": true,
"license": "MIT",
"engines": {
@@ -4653,6 +5447,28 @@
}
}
},
+ "node_modules/eslint-import-resolver-node": {
+ "version": "0.3.9",
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "peer": true,
+ "dependencies": {
+ "debug": "^3.2.7",
+ "is-core-module": "^2.13.0",
+ "resolve": "^1.22.4"
+ }
+ },
+ "node_modules/eslint-import-resolver-node/node_modules/debug": {
+ "version": "3.2.7",
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "peer": true,
+ "dependencies": {
+ "ms": "^2.1.1"
+ }
+ },
"node_modules/eslint-import-resolver-typescript": {
"version": "4.4.4",
"dev": true,
@@ -4686,6 +5502,34 @@
}
}
},
+ "node_modules/eslint-module-utils": {
+ "version": "2.12.1",
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "peer": true,
+ "dependencies": {
+ "debug": "^3.2.7"
+ },
+ "engines": {
+ "node": ">=4"
+ },
+ "peerDependenciesMeta": {
+ "eslint": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/eslint-module-utils/node_modules/debug": {
+ "version": "3.2.7",
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "peer": true,
+ "dependencies": {
+ "ms": "^2.1.1"
+ }
+ },
"node_modules/eslint-plugin-html": {
"version": "8.1.4",
"dev": true,
@@ -4697,6 +5541,40 @@
"node": ">=16.0.0"
}
},
+ "node_modules/eslint-plugin-import": {
+ "version": "2.32.0",
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "peer": true,
+ "dependencies": {
+ "@rtsao/scc": "^1.1.0",
+ "array-includes": "^3.1.9",
+ "array.prototype.findlastindex": "^1.2.6",
+ "array.prototype.flat": "^1.3.3",
+ "array.prototype.flatmap": "^1.3.3",
+ "debug": "^3.2.7",
+ "doctrine": "^2.1.0",
+ "eslint-import-resolver-node": "^0.3.9",
+ "eslint-module-utils": "^2.12.1",
+ "hasown": "^2.0.2",
+ "is-core-module": "^2.16.1",
+ "is-glob": "^4.0.3",
+ "minimatch": "^3.1.2",
+ "object.fromentries": "^2.0.8",
+ "object.groupby": "^1.0.3",
+ "object.values": "^1.2.1",
+ "semver": "^6.3.1",
+ "string.prototype.trimend": "^1.0.9",
+ "tsconfig-paths": "^3.15.0"
+ },
+ "engines": {
+ "node": ">=4"
+ },
+ "peerDependencies": {
+ "eslint": "^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8 || ^9"
+ }
+ },
"node_modules/eslint-plugin-import-x": {
"version": "4.16.1",
"dev": true,
@@ -4732,6 +5610,50 @@
}
}
},
+ "node_modules/eslint-plugin-import/node_modules/brace-expansion": {
+ "version": "1.1.12",
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "peer": true,
+ "dependencies": {
+ "balanced-match": "^1.0.0",
+ "concat-map": "0.0.1"
+ }
+ },
+ "node_modules/eslint-plugin-import/node_modules/debug": {
+ "version": "3.2.7",
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "peer": true,
+ "dependencies": {
+ "ms": "^2.1.1"
+ }
+ },
+ "node_modules/eslint-plugin-import/node_modules/minimatch": {
+ "version": "3.1.5",
+ "dev": true,
+ "license": "ISC",
+ "optional": true,
+ "peer": true,
+ "dependencies": {
+ "brace-expansion": "^1.1.7"
+ },
+ "engines": {
+ "node": "*"
+ }
+ },
+ "node_modules/eslint-plugin-import/node_modules/semver": {
+ "version": "6.3.1",
+ "dev": true,
+ "license": "ISC",
+ "optional": true,
+ "peer": true,
+ "bin": {
+ "semver": "bin/semver.js"
+ }
+ },
"node_modules/eslint-plugin-jest": {
"version": "28.14.0",
"dev": true,
@@ -4790,10 +5712,35 @@
"string.prototype.includes": "^2.0.1"
},
"engines": {
- "node": ">=4.0"
- },
- "peerDependencies": {
- "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9"
+ "node": ">=4.0"
+ },
+ "peerDependencies": {
+ "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9"
+ }
+ },
+ "node_modules/eslint-plugin-jsx-a11y/node_modules/brace-expansion": {
+ "version": "1.1.12",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "balanced-match": "^1.0.0",
+ "concat-map": "0.0.1"
+ }
+ },
+ "node_modules/eslint-plugin-jsx-a11y/node_modules/emoji-regex": {
+ "version": "9.2.2",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/eslint-plugin-jsx-a11y/node_modules/minimatch": {
+ "version": "3.1.5",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "brace-expansion": "^1.1.7"
+ },
+ "engines": {
+ "node": "*"
}
},
"node_modules/eslint-plugin-no-relative-import-paths": {
@@ -4830,6 +5777,104 @@
}
}
},
+ "node_modules/eslint-plugin-react": {
+ "version": "7.37.5",
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "peer": true,
+ "dependencies": {
+ "array-includes": "^3.1.8",
+ "array.prototype.findlast": "^1.2.5",
+ "array.prototype.flatmap": "^1.3.3",
+ "array.prototype.tosorted": "^1.1.4",
+ "doctrine": "^2.1.0",
+ "es-iterator-helpers": "^1.2.1",
+ "estraverse": "^5.3.0",
+ "hasown": "^2.0.2",
+ "jsx-ast-utils": "^2.4.1 || ^3.0.0",
+ "minimatch": "^3.1.2",
+ "object.entries": "^1.1.9",
+ "object.fromentries": "^2.0.8",
+ "object.values": "^1.2.1",
+ "prop-types": "^15.8.1",
+ "resolve": "^2.0.0-next.5",
+ "semver": "^6.3.1",
+ "string.prototype.matchall": "^4.0.12",
+ "string.prototype.repeat": "^1.0.0"
+ },
+ "engines": {
+ "node": ">=4"
+ },
+ "peerDependencies": {
+ "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9.7"
+ }
+ },
+ "node_modules/eslint-plugin-react-hooks": {
+ "version": "5.2.0",
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "peer": true,
+ "engines": {
+ "node": ">=10"
+ },
+ "peerDependencies": {
+ "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0"
+ }
+ },
+ "node_modules/eslint-plugin-react/node_modules/brace-expansion": {
+ "version": "1.1.12",
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "peer": true,
+ "dependencies": {
+ "balanced-match": "^1.0.0",
+ "concat-map": "0.0.1"
+ }
+ },
+ "node_modules/eslint-plugin-react/node_modules/minimatch": {
+ "version": "3.1.5",
+ "dev": true,
+ "license": "ISC",
+ "optional": true,
+ "peer": true,
+ "dependencies": {
+ "brace-expansion": "^1.1.7"
+ },
+ "engines": {
+ "node": "*"
+ }
+ },
+ "node_modules/eslint-plugin-react/node_modules/resolve": {
+ "version": "2.0.0-next.5",
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "peer": true,
+ "dependencies": {
+ "is-core-module": "^2.13.0",
+ "path-parse": "^1.0.7",
+ "supports-preserve-symlinks-flag": "^1.0.0"
+ },
+ "bin": {
+ "resolve": "bin/resolve"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/eslint-plugin-react/node_modules/semver": {
+ "version": "6.3.1",
+ "dev": true,
+ "license": "ISC",
+ "optional": true,
+ "peer": true,
+ "bin": {
+ "semver": "bin/semver.js"
+ }
+ },
"node_modules/eslint-plugin-security": {
"version": "3.0.1",
"dev": true,
@@ -4948,7 +5993,7 @@
}
},
"node_modules/eslint-plugin-unicorn/node_modules/globals": {
- "version": "16.5.0",
+ "version": "16.2.0",
"dev": true,
"license": "MIT",
"engines": {
@@ -4974,6 +6019,26 @@
}
},
"node_modules/eslint-visitor-keys": {
+ "version": "3.4.3",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
+ "node_modules/eslint/node_modules/brace-expansion": {
+ "version": "1.1.12",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "balanced-match": "^1.0.0",
+ "concat-map": "0.0.1"
+ }
+ },
+ "node_modules/eslint/node_modules/eslint-visitor-keys": {
"version": "4.2.1",
"dev": true,
"license": "Apache-2.0",
@@ -4984,6 +6049,25 @@
"url": "https://opencollective.com/eslint"
}
},
+ "node_modules/eslint/node_modules/ignore": {
+ "version": "5.3.2",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 4"
+ }
+ },
+ "node_modules/eslint/node_modules/minimatch": {
+ "version": "3.1.5",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "brace-expansion": "^1.1.7"
+ },
+ "engines": {
+ "node": "*"
+ }
+ },
"node_modules/espree": {
"version": "10.4.0",
"dev": true,
@@ -5000,6 +6084,17 @@
"url": "https://opencollective.com/eslint"
}
},
+ "node_modules/espree/node_modules/eslint-visitor-keys": {
+ "version": "4.2.1",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
"node_modules/esprima": {
"version": "4.0.1",
"dev": true,
@@ -5013,7 +6108,7 @@
}
},
"node_modules/esquery": {
- "version": "1.7.0",
+ "version": "1.6.0",
"dev": true,
"license": "BSD-3-Clause",
"dependencies": {
@@ -5141,8 +6236,6 @@
},
"node_modules/fast-uri": {
"version": "3.1.0",
- "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.0.tgz",
- "integrity": "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==",
"funding": [
{
"type": "github",
@@ -5155,10 +6248,22 @@
],
"license": "BSD-3-Clause"
},
+ "node_modules/fast-xml-builder": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/fast-xml-builder/-/fast-xml-builder-1.0.0.tgz",
+ "integrity": "sha512-fpZuDogrAgnyt9oDDz+5DBz0zgPdPZz6D4IR7iESxRXElrlGTRkHJ9eEt+SACRJwT0FNFrt71DFQIUFBJfX/uQ==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/NaturalIntelligence"
+ }
+ ],
+ "license": "MIT"
+ },
"node_modules/fast-xml-parser": {
- "version": "5.3.6",
- "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-5.3.6.tgz",
- "integrity": "sha512-QNI3sAvSvaOiaMl8FYU4trnEzCwiRr8XMWgAHzlrWpTSj+QaCSvOf1h82OEP1s4hiAXhnbXSyFWCf4ldZzZRVA==",
+ "version": "5.4.1",
+ "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-5.4.1.tgz",
+ "integrity": "sha512-BQ30U1mKkvXQXXkAGcuyUA/GA26oEB7NzOtsxCDtyu62sjGw5QraKFhx2Em3WQNjPw9PG6MQ9yuIIgkSDfGu5A==",
"funding": [
{
"type": "github",
@@ -5167,6 +6272,7 @@
],
"license": "MIT",
"dependencies": {
+ "fast-xml-builder": "^1.0.0",
"strnum": "^2.1.2"
},
"bin": {
@@ -5174,7 +6280,7 @@
}
},
"node_modules/fastq": {
- "version": "1.20.1",
+ "version": "1.19.1",
"dev": true,
"license": "ISC",
"dependencies": {
@@ -5189,22 +6295,6 @@
"bser": "2.1.1"
}
},
- "node_modules/fdir": {
- "version": "6.5.0",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=12.0.0"
- },
- "peerDependencies": {
- "picomatch": "^3 || ^4"
- },
- "peerDependenciesMeta": {
- "picomatch": {
- "optional": true
- }
- }
- },
"node_modules/file-entry-cache": {
"version": "8.0.0",
"dev": true,
@@ -5284,7 +6374,7 @@
}
},
"node_modules/form-data": {
- "version": "4.0.5",
+ "version": "4.0.4",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -5303,6 +6393,18 @@
"dev": true,
"license": "ISC"
},
+ "node_modules/fsevents": {
+ "version": "2.3.3",
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
+ }
+ },
"node_modules/function-bind": {
"version": "1.1.2",
"license": "MIT",
@@ -5342,13 +6444,6 @@
"url": "https://github.com/sponsors/ljharb"
}
},
- "node_modules/generator-function": {
- "version": "2.0.1",
- "license": "MIT",
- "engines": {
- "node": ">= 0.4"
- }
- },
"node_modules/gensync": {
"version": "1.0.0-beta.2",
"dev": true,
@@ -5359,7 +6454,6 @@
},
"node_modules/get-caller-file": {
"version": "2.0.5",
- "dev": true,
"license": "ISC",
"engines": {
"node": "6.* || 8.* || >= 10.*"
@@ -5434,7 +6528,7 @@
}
},
"node_modules/get-tsconfig": {
- "version": "4.13.6",
+ "version": "4.10.1",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -5474,6 +6568,26 @@
"node": ">=10.13.0"
}
},
+ "node_modules/glob/node_modules/brace-expansion": {
+ "version": "1.1.12",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "balanced-match": "^1.0.0",
+ "concat-map": "0.0.1"
+ }
+ },
+ "node_modules/glob/node_modules/minimatch": {
+ "version": "3.1.5",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "brace-expansion": "^1.1.7"
+ },
+ "engines": {
+ "node": "*"
+ }
+ },
"node_modules/globals": {
"version": "14.0.0",
"dev": true,
@@ -5628,7 +6742,7 @@
"license": "MIT"
},
"node_modules/htmlparser2": {
- "version": "10.1.0",
+ "version": "10.0.0",
"dev": true,
"funding": [
"https://github.com/fb55/htmlparser2?sponsor=1",
@@ -5641,8 +6755,8 @@
"dependencies": {
"domelementtype": "^2.3.0",
"domhandler": "^5.0.3",
- "domutils": "^3.2.2",
- "entities": "^7.0.1"
+ "domutils": "^3.2.1",
+ "entities": "^6.0.0"
}
},
"node_modules/http-proxy-agent": {
@@ -5690,7 +6804,7 @@
}
},
"node_modules/ignore": {
- "version": "5.3.2",
+ "version": "7.0.5",
"dev": true,
"license": "MIT",
"engines": {
@@ -5777,8 +6891,6 @@
},
"node_modules/is-arguments": {
"version": "1.2.0",
- "resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.2.0.tgz",
- "integrity": "sha512-7bVbi0huj/wrIAOzb8U1aszg9kdi3KN/CyU19CTI7tAoZYEZoL9yCDXpbXN+uPsuWnP02cyug1gleqq+TU+YCA==",
"license": "MIT",
"dependencies": {
"call-bound": "^1.0.2",
@@ -5971,7 +7083,6 @@
},
"node_modules/is-fullwidth-code-point": {
"version": "3.0.0",
- "dev": true,
"license": "MIT",
"engines": {
"node": ">=8"
@@ -5986,12 +7097,11 @@
}
},
"node_modules/is-generator-function": {
- "version": "1.1.2",
+ "version": "1.1.0",
"license": "MIT",
"dependencies": {
- "call-bound": "^1.0.4",
- "generator-function": "^2.0.0",
- "get-proto": "^1.0.1",
+ "call-bound": "^1.0.3",
+ "get-proto": "^1.0.0",
"has-tostringtag": "^1.0.2",
"safe-regex-test": "^1.1.0"
},
@@ -6259,7 +7369,7 @@
}
},
"node_modules/istanbul-reports": {
- "version": "3.2.0",
+ "version": "3.1.7",
"dev": true,
"license": "BSD-3-Clause",
"dependencies": {
@@ -6270,6 +7380,24 @@
"node": ">=8"
}
},
+ "node_modules/iterator.prototype": {
+ "version": "1.1.5",
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "peer": true,
+ "dependencies": {
+ "define-data-property": "^1.1.4",
+ "es-object-atoms": "^1.0.0",
+ "get-intrinsic": "^1.2.6",
+ "get-proto": "^1.0.0",
+ "has-symbols": "^1.1.0",
+ "set-function-name": "^2.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
"node_modules/jest": {
"version": "29.7.0",
"dev": true,
@@ -6810,17 +7938,6 @@
"node": ">=8"
}
},
- "node_modules/jest-util/node_modules/picomatch": {
- "version": "2.3.1",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=8.6"
- },
- "funding": {
- "url": "https://github.com/sponsors/jonschlinkert"
- }
- },
"node_modules/jest-validate": {
"version": "29.7.0",
"dev": true,
@@ -6900,7 +8017,7 @@
"license": "MIT"
},
"node_modules/js-yaml": {
- "version": "4.1.1",
+ "version": "4.1.0",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -6967,8 +8084,6 @@
},
"node_modules/json-bigint": {
"version": "1.0.0",
- "resolved": "https://registry.npmjs.org/json-bigint/-/json-bigint-1.0.0.tgz",
- "integrity": "sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ==",
"license": "MIT",
"dependencies": {
"bignumber.js": "^9.0.0"
@@ -7154,7 +8269,7 @@
}
},
"node_modules/lodash": {
- "version": "4.17.23",
+ "version": "4.17.21",
"dev": true,
"license": "MIT"
},
@@ -7168,6 +8283,25 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/lodash.truncate": {
+ "version": "4.4.2",
+ "resolved": "https://registry.npmjs.org/lodash.truncate/-/lodash.truncate-4.4.2.tgz",
+ "integrity": "sha512-jttmRe7bRse52OsWIMDLaXxWqRAmtIUccAQ3garviCqJjafXOfNMO0yMfNpdD6zbGaTU0P5Nz7e7gAT6cKmJRw==",
+ "license": "MIT"
+ },
+ "node_modules/loose-envify": {
+ "version": "1.4.0",
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "peer": true,
+ "dependencies": {
+ "js-tokens": "^3.0.0 || ^4.0.0"
+ },
+ "bin": {
+ "loose-envify": "cli.js"
+ }
+ },
"node_modules/lru-cache": {
"version": "5.1.1",
"dev": true,
@@ -7235,17 +8369,6 @@
"node": ">=8.6"
}
},
- "node_modules/micromatch/node_modules/picomatch": {
- "version": "2.3.1",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=8.6"
- },
- "funding": {
- "url": "https://github.com/sponsors/jonschlinkert"
- }
- },
"node_modules/mime-db": {
"version": "1.52.0",
"dev": true,
@@ -7273,8 +8396,16 @@
"node": ">=6"
}
},
+ "node_modules/min-indent": {
+ "version": "1.0.1",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=4"
+ }
+ },
"node_modules/minimatch": {
- "version": "10.2.3",
+ "version": "10.2.4",
"dev": true,
"license": "BlueOak-1.0.0",
"dependencies": {
@@ -7312,7 +8443,7 @@
"license": "MIT"
},
"node_modules/napi-postinstall": {
- "version": "0.3.4",
+ "version": "0.2.4",
"dev": true,
"license": "MIT",
"bin": {
@@ -7358,7 +8489,7 @@
"license": "MIT"
},
"node_modules/node-releases": {
- "version": "2.0.27",
+ "version": "2.0.19",
"dev": true,
"license": "MIT"
},
@@ -7382,10 +8513,20 @@
}
},
"node_modules/nwsapi": {
- "version": "2.2.23",
+ "version": "2.2.20",
"dev": true,
"license": "MIT"
},
+ "node_modules/object-assign": {
+ "version": "4.1.1",
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "peer": true,
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
"node_modules/object-inspect": {
"version": "1.13.4",
"dev": true,
@@ -7424,6 +8565,22 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/object.entries": {
+ "version": "1.1.9",
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "peer": true,
+ "dependencies": {
+ "call-bind": "^1.0.8",
+ "call-bound": "^1.0.4",
+ "define-properties": "^1.2.1",
+ "es-object-atoms": "^1.1.1"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
"node_modules/object.fromentries": {
"version": "2.0.8",
"dev": true,
@@ -7441,6 +8598,21 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/object.groupby": {
+ "version": "1.0.3",
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "peer": true,
+ "dependencies": {
+ "call-bind": "^1.0.7",
+ "define-properties": "^1.2.1",
+ "es-abstract": "^1.23.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
"node_modules/object.values": {
"version": "1.2.1",
"dev": true,
@@ -7460,8 +8632,6 @@
},
"node_modules/on-exit-leak-free": {
"version": "2.1.2",
- "resolved": "https://registry.npmjs.org/on-exit-leak-free/-/on-exit-leak-free-2.1.2.tgz",
- "integrity": "sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA==",
"license": "MIT",
"engines": {
"node": ">=14.0.0"
@@ -7549,21 +8719,6 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
- "node_modules/p-map": {
- "version": "4.0.0",
- "resolved": "https://registry.npmjs.org/p-map/-/p-map-4.0.0.tgz",
- "integrity": "sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ==",
- "license": "MIT",
- "dependencies": {
- "aggregate-error": "^3.0.0"
- },
- "engines": {
- "node": ">=10"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
- }
- },
"node_modules/p-try": {
"version": "2.2.0",
"dev": true,
@@ -7611,17 +8766,6 @@
"url": "https://github.com/inikulin/parse5?sponsor=1"
}
},
- "node_modules/parse5/node_modules/entities": {
- "version": "6.0.1",
- "dev": true,
- "license": "BSD-2-Clause",
- "engines": {
- "node": ">=0.12"
- },
- "funding": {
- "url": "https://github.com/fb55/entities?sponsor=1"
- }
- },
"node_modules/path-exists": {
"version": "4.0.0",
"dev": true,
@@ -7657,11 +8801,11 @@
"license": "ISC"
},
"node_modules/picomatch": {
- "version": "4.0.3",
+ "version": "2.3.1",
"dev": true,
"license": "MIT",
"engines": {
- "node": ">=12"
+ "node": ">=8.6"
},
"funding": {
"url": "https://github.com/sponsors/jonschlinkert"
@@ -7669,8 +8813,6 @@
},
"node_modules/pino": {
"version": "9.14.0",
- "resolved": "https://registry.npmjs.org/pino/-/pino-9.14.0.tgz",
- "integrity": "sha512-8OEwKp5juEvb/MjpIc4hjqfgCNysrS94RIOMXYvpYCdm/jglrKEiAYmiumbmGhCvs+IcInsphYDFwqrjr7398w==",
"license": "MIT",
"dependencies": {
"@pinojs/redact": "^0.4.0",
@@ -7691,8 +8833,6 @@
},
"node_modules/pino-abstract-transport": {
"version": "2.0.0",
- "resolved": "https://registry.npmjs.org/pino-abstract-transport/-/pino-abstract-transport-2.0.0.tgz",
- "integrity": "sha512-F63x5tizV6WCh4R6RHyi2Ml+M70DNRXt/+HANowMflpgGFMAym/VKm6G7ZOQRjqN7XbGxK1Lg9t6ZrtzOaivMw==",
"license": "MIT",
"dependencies": {
"split2": "^4.0.0"
@@ -7700,8 +8840,6 @@
},
"node_modules/pino-std-serializers": {
"version": "7.1.0",
- "resolved": "https://registry.npmjs.org/pino-std-serializers/-/pino-std-serializers-7.1.0.tgz",
- "integrity": "sha512-BndPH67/JxGExRgiX1dX0w1FvZck5Wa4aal9198SrRhZjH3GxKQUKIBnYJTdj2HDN3UQAS06HlfcSbQj2OHmaw==",
"license": "MIT"
},
"node_modules/pirates": {
@@ -7795,7 +8933,7 @@
}
},
"node_modules/prettier": {
- "version": "3.8.1",
+ "version": "3.6.0",
"dev": true,
"license": "MIT",
"peer": true,
@@ -7846,8 +8984,6 @@
},
"node_modules/process": {
"version": "0.11.10",
- "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz",
- "integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==",
"license": "MIT",
"engines": {
"node": ">= 0.6.0"
@@ -7855,8 +8991,6 @@
},
"node_modules/process-warning": {
"version": "5.0.0",
- "resolved": "https://registry.npmjs.org/process-warning/-/process-warning-5.0.0.tgz",
- "integrity": "sha512-a39t9ApHNx2L4+HBnQKqxxHNs1r7KF+Intd8Q/g1bUh6q0WIp9voPXJ/x0j+ZL45KF1pJd9+q2jLIRMfvEshkA==",
"funding": [
{
"type": "github",
@@ -7881,6 +9015,25 @@
"node": ">= 6"
}
},
+ "node_modules/prop-types": {
+ "version": "15.8.1",
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "peer": true,
+ "dependencies": {
+ "loose-envify": "^1.4.0",
+ "object-assign": "^4.1.1",
+ "react-is": "^16.13.1"
+ }
+ },
+ "node_modules/prop-types/node_modules/react-is": {
+ "version": "16.13.1",
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "peer": true
+ },
"node_modules/psl": {
"version": "1.15.0",
"dev": true,
@@ -7941,8 +9094,6 @@
},
"node_modules/quick-format-unescaped": {
"version": "4.0.4",
- "resolved": "https://registry.npmjs.org/quick-format-unescaped/-/quick-format-unescaped-4.0.4.tgz",
- "integrity": "sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg==",
"license": "MIT"
},
"node_modules/react-is": {
@@ -7952,8 +9103,6 @@
},
"node_modules/real-require": {
"version": "0.2.0",
- "resolved": "https://registry.npmjs.org/real-require/-/real-require-0.2.0.tgz",
- "integrity": "sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg==",
"license": "MIT",
"engines": {
"node": ">= 12.13.0"
@@ -8054,7 +9203,6 @@
},
"node_modules/require-directory": {
"version": "2.1.1",
- "dev": true,
"license": "MIT",
"engines": {
"node": ">=0.10.0"
@@ -8062,8 +9210,6 @@
},
"node_modules/require-from-string": {
"version": "2.0.2",
- "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz",
- "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==",
"license": "MIT",
"engines": {
"node": ">=0.10.0"
@@ -8075,11 +9221,11 @@
"license": "MIT"
},
"node_modules/resolve": {
- "version": "1.22.11",
+ "version": "1.22.10",
"dev": true,
"license": "MIT",
"dependencies": {
- "is-core-module": "^2.16.1",
+ "is-core-module": "^2.16.0",
"path-parse": "^1.0.7",
"supports-preserve-symlinks-flag": "^1.0.0"
},
@@ -8225,8 +9371,6 @@
},
"node_modules/safe-stable-stringify": {
"version": "2.5.0",
- "resolved": "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-2.5.0.tgz",
- "integrity": "sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==",
"license": "MIT",
"engines": {
"node": ">=10"
@@ -8419,10 +9563,25 @@
"node": ">=8"
}
},
+ "node_modules/slice-ansi": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-4.0.0.tgz",
+ "integrity": "sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ==",
+ "license": "MIT",
+ "dependencies": {
+ "ansi-styles": "^4.0.0",
+ "astral-regex": "^2.0.0",
+ "is-fullwidth-code-point": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/slice-ansi?sponsor=1"
+ }
+ },
"node_modules/sonic-boom": {
"version": "4.2.1",
- "resolved": "https://registry.npmjs.org/sonic-boom/-/sonic-boom-4.2.1.tgz",
- "integrity": "sha512-w6AxtubXa2wTXAUsZMMWERrsIRAdrK0Sc+FUytWvYAhBJLyuI4llrMIC1DtlNSdI99EI86KZum2MMq3EAZlF9Q==",
"license": "MIT",
"dependencies": {
"atomic-sleep": "^1.0.0"
@@ -8447,8 +9606,6 @@
},
"node_modules/split2": {
"version": "4.2.0",
- "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz",
- "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==",
"license": "ISC",
"engines": {
"node": ">= 10.x"
@@ -8512,7 +9669,6 @@
},
"node_modules/string-width": {
"version": "4.2.3",
- "dev": true,
"license": "MIT",
"dependencies": {
"emoji-regex": "^8.0.0",
@@ -8523,11 +9679,6 @@
"node": ">=8"
}
},
- "node_modules/string-width/node_modules/emoji-regex": {
- "version": "8.0.0",
- "dev": true,
- "license": "MIT"
- },
"node_modules/string.prototype.includes": {
"version": "2.0.1",
"dev": true,
@@ -8541,6 +9692,45 @@
"node": ">= 0.4"
}
},
+ "node_modules/string.prototype.matchall": {
+ "version": "4.0.12",
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "peer": true,
+ "dependencies": {
+ "call-bind": "^1.0.8",
+ "call-bound": "^1.0.3",
+ "define-properties": "^1.2.1",
+ "es-abstract": "^1.23.6",
+ "es-errors": "^1.3.0",
+ "es-object-atoms": "^1.0.0",
+ "get-intrinsic": "^1.2.6",
+ "gopd": "^1.2.0",
+ "has-symbols": "^1.1.0",
+ "internal-slot": "^1.1.0",
+ "regexp.prototype.flags": "^1.5.3",
+ "set-function-name": "^2.0.2",
+ "side-channel": "^1.1.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/string.prototype.repeat": {
+ "version": "1.0.0",
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "peer": true,
+ "dependencies": {
+ "define-properties": "^1.1.3",
+ "es-abstract": "^1.17.5"
+ }
+ },
"node_modules/string.prototype.trim": {
"version": "1.2.10",
"dev": true,
@@ -8596,7 +9786,6 @@
},
"node_modules/strip-ansi": {
"version": "6.0.1",
- "dev": true,
"license": "MIT",
"dependencies": {
"ansi-regex": "^5.0.1"
@@ -8622,9 +9811,12 @@
}
},
"node_modules/strip-indent": {
- "version": "4.1.1",
+ "version": "4.0.0",
"dev": true,
"license": "MIT",
+ "dependencies": {
+ "min-indent": "^1.0.1"
+ },
"engines": {
"node": ">=12"
},
@@ -8696,6 +9888,44 @@
"url": "https://opencollective.com/synckit"
}
},
+ "node_modules/table": {
+ "version": "6.9.0",
+ "resolved": "https://registry.npmjs.org/table/-/table-6.9.0.tgz",
+ "integrity": "sha512-9kY+CygyYM6j02t5YFHbNz2FN5QmYGv9zAjVp4lCDjlCw7amdckXlEt/bjMhUIfj4ThGRE4gCUH5+yGnNuPo5A==",
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "ajv": "^8.0.1",
+ "lodash.truncate": "^4.4.2",
+ "slice-ansi": "^4.0.0",
+ "string-width": "^4.2.3",
+ "strip-ansi": "^6.0.1"
+ },
+ "engines": {
+ "node": ">=10.0.0"
+ }
+ },
+ "node_modules/table/node_modules/ajv": {
+ "version": "8.18.0",
+ "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz",
+ "integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==",
+ "license": "MIT",
+ "dependencies": {
+ "fast-deep-equal": "^3.1.3",
+ "fast-uri": "^3.0.1",
+ "json-schema-traverse": "^1.0.0",
+ "require-from-string": "^2.0.2"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/epoberezkin"
+ }
+ },
+ "node_modules/table/node_modules/json-schema-traverse": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz",
+ "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==",
+ "license": "MIT"
+ },
"node_modules/test-exclude": {
"version": "6.0.0",
"dev": true,
@@ -8709,10 +9939,28 @@
"node": ">=8"
}
},
+ "node_modules/test-exclude/node_modules/brace-expansion": {
+ "version": "1.1.12",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "balanced-match": "^1.0.0",
+ "concat-map": "0.0.1"
+ }
+ },
+ "node_modules/test-exclude/node_modules/minimatch": {
+ "version": "3.1.5",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "brace-expansion": "^1.1.7"
+ },
+ "engines": {
+ "node": "*"
+ }
+ },
"node_modules/thread-stream": {
"version": "3.1.0",
- "resolved": "https://registry.npmjs.org/thread-stream/-/thread-stream-3.1.0.tgz",
- "integrity": "sha512-OqyPZ9u96VohAyMfJykzmivOrY2wfMSf3C5TtFJVgN+Hm6aj+voFhlK+kZEIv2FBh1X6Xp3DlnCOfEQ3B2J86A==",
"license": "MIT",
"dependencies": {
"real-require": "^0.2.0"
@@ -8733,6 +9981,33 @@
"url": "https://github.com/sponsors/SuperchupuDev"
}
},
+ "node_modules/tinyglobby/node_modules/fdir": {
+ "version": "6.5.0",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12.0.0"
+ },
+ "peerDependencies": {
+ "picomatch": "^3 || ^4"
+ },
+ "peerDependenciesMeta": {
+ "picomatch": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/tinyglobby/node_modules/picomatch": {
+ "version": "4.0.3",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/jonschlinkert"
+ }
+ },
"node_modules/tmpl": {
"version": "1.0.5",
"dev": true,
@@ -8902,18 +10177,52 @@
}
}
},
+ "node_modules/tsconfig-paths": {
+ "version": "3.15.0",
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "peer": true,
+ "dependencies": {
+ "@types/json5": "^0.0.29",
+ "json5": "^1.0.2",
+ "minimist": "^1.2.6",
+ "strip-bom": "^3.0.0"
+ }
+ },
+ "node_modules/tsconfig-paths/node_modules/json5": {
+ "version": "1.0.2",
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "peer": true,
+ "dependencies": {
+ "minimist": "^1.2.0"
+ },
+ "bin": {
+ "json5": "lib/cli.js"
+ }
+ },
+ "node_modules/tsconfig-paths/node_modules/strip-bom": {
+ "version": "3.0.0",
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "peer": true,
+ "engines": {
+ "node": ">=4"
+ }
+ },
"node_modules/tslib": {
"version": "2.8.1",
- "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
- "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
"license": "0BSD"
},
"node_modules/tsx": {
- "version": "4.21.0",
+ "version": "4.20.3",
"dev": true,
"license": "MIT",
"dependencies": {
- "esbuild": "~0.27.0",
+ "esbuild": "~0.25.0",
"get-tsconfig": "^4.7.5"
},
"bin": {
@@ -8926,23 +10235,23 @@
"fsevents": "~2.3.3"
}
},
- "node_modules/tsx/node_modules/@esbuild/linux-x64": {
- "version": "0.27.3",
+ "node_modules/tsx/node_modules/@esbuild/darwin-arm64": {
+ "version": "0.25.5",
"cpu": [
- "x64"
+ "arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
- "linux"
+ "darwin"
],
"engines": {
"node": ">=18"
}
},
"node_modules/tsx/node_modules/esbuild": {
- "version": "0.27.3",
+ "version": "0.25.5",
"dev": true,
"hasInstallScript": true,
"license": "MIT",
@@ -8953,32 +10262,31 @@
"node": ">=18"
},
"optionalDependencies": {
- "@esbuild/aix-ppc64": "0.27.3",
- "@esbuild/android-arm": "0.27.3",
- "@esbuild/android-arm64": "0.27.3",
- "@esbuild/android-x64": "0.27.3",
- "@esbuild/darwin-arm64": "0.27.3",
- "@esbuild/darwin-x64": "0.27.3",
- "@esbuild/freebsd-arm64": "0.27.3",
- "@esbuild/freebsd-x64": "0.27.3",
- "@esbuild/linux-arm": "0.27.3",
- "@esbuild/linux-arm64": "0.27.3",
- "@esbuild/linux-ia32": "0.27.3",
- "@esbuild/linux-loong64": "0.27.3",
- "@esbuild/linux-mips64el": "0.27.3",
- "@esbuild/linux-ppc64": "0.27.3",
- "@esbuild/linux-riscv64": "0.27.3",
- "@esbuild/linux-s390x": "0.27.3",
- "@esbuild/linux-x64": "0.27.3",
- "@esbuild/netbsd-arm64": "0.27.3",
- "@esbuild/netbsd-x64": "0.27.3",
- "@esbuild/openbsd-arm64": "0.27.3",
- "@esbuild/openbsd-x64": "0.27.3",
- "@esbuild/openharmony-arm64": "0.27.3",
- "@esbuild/sunos-x64": "0.27.3",
- "@esbuild/win32-arm64": "0.27.3",
- "@esbuild/win32-ia32": "0.27.3",
- "@esbuild/win32-x64": "0.27.3"
+ "@esbuild/aix-ppc64": "0.25.5",
+ "@esbuild/android-arm": "0.25.5",
+ "@esbuild/android-arm64": "0.25.5",
+ "@esbuild/android-x64": "0.25.5",
+ "@esbuild/darwin-arm64": "0.25.5",
+ "@esbuild/darwin-x64": "0.25.5",
+ "@esbuild/freebsd-arm64": "0.25.5",
+ "@esbuild/freebsd-x64": "0.25.5",
+ "@esbuild/linux-arm": "0.25.5",
+ "@esbuild/linux-arm64": "0.25.5",
+ "@esbuild/linux-ia32": "0.25.5",
+ "@esbuild/linux-loong64": "0.25.5",
+ "@esbuild/linux-mips64el": "0.25.5",
+ "@esbuild/linux-ppc64": "0.25.5",
+ "@esbuild/linux-riscv64": "0.25.5",
+ "@esbuild/linux-s390x": "0.25.5",
+ "@esbuild/linux-x64": "0.25.5",
+ "@esbuild/netbsd-arm64": "0.25.5",
+ "@esbuild/netbsd-x64": "0.25.5",
+ "@esbuild/openbsd-arm64": "0.25.5",
+ "@esbuild/openbsd-x64": "0.25.5",
+ "@esbuild/sunos-x64": "0.25.5",
+ "@esbuild/win32-arm64": "0.25.5",
+ "@esbuild/win32-ia32": "0.25.5",
+ "@esbuild/win32-x64": "0.25.5"
}
},
"node_modules/type-check": {
@@ -9000,17 +10308,6 @@
"node": ">=4"
}
},
- "node_modules/type-fest": {
- "version": "0.21.3",
- "dev": true,
- "license": "(MIT OR CC0-1.0)",
- "engines": {
- "node": ">=10"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
- }
- },
"node_modules/typed-array-buffer": {
"version": "1.0.3",
"dev": true,
@@ -9082,7 +10379,7 @@
}
},
"node_modules/typescript": {
- "version": "5.9.3",
+ "version": "5.8.3",
"dev": true,
"license": "Apache-2.0",
"bin": {
@@ -9145,7 +10442,7 @@
}
},
"node_modules/undici-types": {
- "version": "7.18.2",
+ "version": "7.8.0",
"dev": true,
"license": "MIT"
},
@@ -9158,40 +10455,40 @@
}
},
"node_modules/unrs-resolver": {
- "version": "1.11.1",
+ "version": "1.9.2",
"dev": true,
"hasInstallScript": true,
"license": "MIT",
"dependencies": {
- "napi-postinstall": "^0.3.0"
+ "napi-postinstall": "^0.2.4"
},
"funding": {
"url": "https://opencollective.com/unrs-resolver"
},
"optionalDependencies": {
- "@unrs/resolver-binding-android-arm-eabi": "1.11.1",
- "@unrs/resolver-binding-android-arm64": "1.11.1",
- "@unrs/resolver-binding-darwin-arm64": "1.11.1",
- "@unrs/resolver-binding-darwin-x64": "1.11.1",
- "@unrs/resolver-binding-freebsd-x64": "1.11.1",
- "@unrs/resolver-binding-linux-arm-gnueabihf": "1.11.1",
- "@unrs/resolver-binding-linux-arm-musleabihf": "1.11.1",
- "@unrs/resolver-binding-linux-arm64-gnu": "1.11.1",
- "@unrs/resolver-binding-linux-arm64-musl": "1.11.1",
- "@unrs/resolver-binding-linux-ppc64-gnu": "1.11.1",
- "@unrs/resolver-binding-linux-riscv64-gnu": "1.11.1",
- "@unrs/resolver-binding-linux-riscv64-musl": "1.11.1",
- "@unrs/resolver-binding-linux-s390x-gnu": "1.11.1",
- "@unrs/resolver-binding-linux-x64-gnu": "1.11.1",
- "@unrs/resolver-binding-linux-x64-musl": "1.11.1",
- "@unrs/resolver-binding-wasm32-wasi": "1.11.1",
- "@unrs/resolver-binding-win32-arm64-msvc": "1.11.1",
- "@unrs/resolver-binding-win32-ia32-msvc": "1.11.1",
- "@unrs/resolver-binding-win32-x64-msvc": "1.11.1"
+ "@unrs/resolver-binding-android-arm-eabi": "1.9.2",
+ "@unrs/resolver-binding-android-arm64": "1.9.2",
+ "@unrs/resolver-binding-darwin-arm64": "1.9.2",
+ "@unrs/resolver-binding-darwin-x64": "1.9.2",
+ "@unrs/resolver-binding-freebsd-x64": "1.9.2",
+ "@unrs/resolver-binding-linux-arm-gnueabihf": "1.9.2",
+ "@unrs/resolver-binding-linux-arm-musleabihf": "1.9.2",
+ "@unrs/resolver-binding-linux-arm64-gnu": "1.9.2",
+ "@unrs/resolver-binding-linux-arm64-musl": "1.9.2",
+ "@unrs/resolver-binding-linux-ppc64-gnu": "1.9.2",
+ "@unrs/resolver-binding-linux-riscv64-gnu": "1.9.2",
+ "@unrs/resolver-binding-linux-riscv64-musl": "1.9.2",
+ "@unrs/resolver-binding-linux-s390x-gnu": "1.9.2",
+ "@unrs/resolver-binding-linux-x64-gnu": "1.9.2",
+ "@unrs/resolver-binding-linux-x64-musl": "1.9.2",
+ "@unrs/resolver-binding-wasm32-wasi": "1.9.2",
+ "@unrs/resolver-binding-win32-arm64-msvc": "1.9.2",
+ "@unrs/resolver-binding-win32-ia32-msvc": "1.9.2",
+ "@unrs/resolver-binding-win32-x64-msvc": "1.9.2"
}
},
"node_modules/update-browserslist-db": {
- "version": "1.2.3",
+ "version": "1.1.3",
"dev": true,
"funding": [
{
@@ -9238,8 +10535,6 @@
},
"node_modules/util": {
"version": "0.12.5",
- "resolved": "https://registry.npmjs.org/util/-/util-0.12.5.tgz",
- "integrity": "sha512-kZf/K6hEIrWHI6XqOFUiiMa+79wE/D8Q+NCNAWclkyg3b4d2k7s0QGepNjiABc+aR3N1PAyHL7p6UcLY6LmrnA==",
"license": "MIT",
"dependencies": {
"inherits": "^2.0.3",
@@ -9251,8 +10546,6 @@
},
"node_modules/uuid": {
"version": "8.3.2",
- "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz",
- "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==",
"license": "MIT",
"bin": {
"uuid": "dist/bin/uuid"
@@ -9442,7 +10735,7 @@
}
},
"node_modules/which-typed-array": {
- "version": "1.1.20",
+ "version": "1.1.19",
"license": "MIT",
"dependencies": {
"available-typed-arrays": "^1.0.7",
@@ -9475,7 +10768,6 @@
},
"node_modules/wrap-ansi": {
"version": "7.0.0",
- "dev": true,
"license": "MIT",
"dependencies": {
"ansi-styles": "^4.0.0",
@@ -9507,7 +10799,7 @@
}
},
"node_modules/ws": {
- "version": "8.19.0",
+ "version": "8.18.2",
"dev": true,
"license": "MIT",
"engines": {
@@ -9549,7 +10841,6 @@
},
"node_modules/y18n": {
"version": "5.0.8",
- "dev": true,
"license": "ISC",
"engines": {
"node": ">=10"
@@ -9562,7 +10853,6 @@
},
"node_modules/yargs": {
"version": "17.7.2",
- "dev": true,
"license": "MIT",
"dependencies": {
"cliui": "^8.0.1",
@@ -9579,7 +10869,6 @@
},
"node_modules/yargs-parser": {
"version": "21.1.1",
- "dev": true,
"license": "ISC",
"engines": {
"node": ">=12"
@@ -9625,7 +10914,12 @@
"name": "nhs-notify-client-callbacks-integration-tests",
"version": "0.0.1",
"dependencies": {
- "@aws-sdk/client-cloudwatch-logs": "^3.991.0"
+ "@aws-sdk/client-cloudwatch-logs": "^3.991.0",
+ "@aws-sdk/client-eventbridge": "^3.990.0",
+ "@aws-sdk/client-s3": "^3.821.0",
+ "@aws-sdk/client-sqs": "^3.990.0",
+ "@aws-sdk/client-sts": "^3.821.0",
+ "async-wait-until": "^2.0.12"
},
"devDependencies": {
"@aws-sdk/client-sqs": "^3.990.0",
@@ -9636,6 +10930,104 @@
"jest": "^29.7.0",
"typescript": "^5.8.2"
}
+ },
+ "tools/client-subscriptions-management": {
+ "version": "0.0.1",
+ "dependencies": {
+ "@aws-sdk/client-s3": "^3.821.0",
+ "@aws-sdk/client-sts": "^3.1004.0",
+ "@aws-sdk/credential-providers": "^3.1004.0",
+ "@nhs-notify-client-callbacks/models": "*",
+ "table": "^6.9.0",
+ "yargs": "^17.7.2",
+ "zod": "^4.3.6"
+ },
+ "devDependencies": {
+ "@types/node": "^22.10.10",
+ "@types/yargs": "^17.0.24",
+ "eslint": "^9.27.0",
+ "tsx": "^4.19.3",
+ "typescript": "^5.8.2"
+ }
+ },
+ "tools/client-subscriptions-management/node_modules/@aws-sdk/client-sts": {
+ "version": "3.1004.0",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/client-sts/-/client-sts-3.1004.0.tgz",
+ "integrity": "sha512-fxTiEmAwj91OtrmhafZtmxrUa4wfT1CmnnV45jZ3NCHSTJhZy0MrtNZShxSnuhbF0i/JfsZdst3oxQGzGcCCmw==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@aws-crypto/sha256-browser": "5.2.0",
+ "@aws-crypto/sha256-js": "5.2.0",
+ "@aws-sdk/core": "^3.973.18",
+ "@aws-sdk/credential-provider-node": "^3.972.18",
+ "@aws-sdk/middleware-host-header": "^3.972.7",
+ "@aws-sdk/middleware-logger": "^3.972.7",
+ "@aws-sdk/middleware-recursion-detection": "^3.972.7",
+ "@aws-sdk/middleware-user-agent": "^3.972.19",
+ "@aws-sdk/region-config-resolver": "^3.972.7",
+ "@aws-sdk/types": "^3.973.5",
+ "@aws-sdk/util-endpoints": "^3.996.4",
+ "@aws-sdk/util-user-agent-browser": "^3.972.7",
+ "@aws-sdk/util-user-agent-node": "^3.973.4",
+ "@smithy/config-resolver": "^4.4.10",
+ "@smithy/core": "^3.23.8",
+ "@smithy/fetch-http-handler": "^5.3.13",
+ "@smithy/hash-node": "^4.2.11",
+ "@smithy/invalid-dependency": "^4.2.11",
+ "@smithy/middleware-content-length": "^4.2.11",
+ "@smithy/middleware-endpoint": "^4.4.22",
+ "@smithy/middleware-retry": "^4.4.39",
+ "@smithy/middleware-serde": "^4.2.12",
+ "@smithy/middleware-stack": "^4.2.11",
+ "@smithy/node-config-provider": "^4.3.11",
+ "@smithy/node-http-handler": "^4.4.14",
+ "@smithy/protocol-http": "^5.3.11",
+ "@smithy/smithy-client": "^4.12.2",
+ "@smithy/types": "^4.13.0",
+ "@smithy/url-parser": "^4.2.11",
+ "@smithy/util-base64": "^4.3.2",
+ "@smithy/util-body-length-browser": "^4.2.2",
+ "@smithy/util-body-length-node": "^4.2.3",
+ "@smithy/util-defaults-mode-browser": "^4.3.38",
+ "@smithy/util-defaults-mode-node": "^4.2.41",
+ "@smithy/util-endpoints": "^3.3.2",
+ "@smithy/util-middleware": "^4.2.11",
+ "@smithy/util-retry": "^4.2.11",
+ "@smithy/util-utf8": "^4.2.2",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=20.0.0"
+ }
+ },
+ "tools/client-subscriptions-management/node_modules/@aws-sdk/util-endpoints": {
+ "version": "3.996.4",
+ "resolved": "https://registry.npmjs.org/@aws-sdk/util-endpoints/-/util-endpoints-3.996.4.tgz",
+ "integrity": "sha512-Hek90FBmd4joCFj+Vc98KLJh73Zqj3s2W56gjAcTkrNLMDI5nIFkG9YpfcJiVI1YlE2Ne1uOQNe+IgQ/Vz2XRA==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@aws-sdk/types": "^3.973.5",
+ "@smithy/types": "^4.13.0",
+ "@smithy/url-parser": "^4.2.11",
+ "@smithy/util-endpoints": "^3.3.2",
+ "tslib": "^2.6.2"
+ },
+ "engines": {
+ "node": ">=20.0.0"
+ }
+ },
+ "tools/client-subscriptions-management/node_modules/@types/node": {
+ "version": "22.19.11",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "undici-types": "~6.21.0"
+ }
+ },
+ "tools/client-subscriptions-management/node_modules/undici-types": {
+ "version": "6.21.0",
+ "dev": true,
+ "license": "MIT"
}
}
}
diff --git a/package.json b/package.json
index c031567..e254dc1 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,7 @@
{
"devDependencies": {
"@stylistic/eslint-plugin": "^3.1.0",
+ "@aws-sdk/client-s3": "^3.821.0",
"@tsconfig/node22": "^22.0.2",
"@types/jest": "^29.5.14",
"@typescript-eslint/parser": "^8.56.1",
@@ -35,7 +36,8 @@
"pretty-format": {
"react-is": "19.0.0"
},
- "minimatch": "^10.2.2"
+ "minimatch@^3.0.0": "^3.1.5",
+ "minimatch": "^10.2.4"
},
"scripts": {
"generate-dependencies": "npm run generate-dependencies --workspaces --if-present",
@@ -45,12 +47,16 @@
"test:integration": "npm run test:integration --workspace tests/integration",
"test:unit": "npm run test:unit --workspaces",
"typecheck": "npm run typecheck --workspaces",
- "verify": "npm run lint && npm run typecheck && npm run test:unit"
+ "verify": "npm run lint && npm run typecheck && npm run test:unit",
+ "subscriptions:get": "npm run get-by-client-id --workspace tools/client-subscriptions-management --",
+ "subscriptions:put-channel-status": "npm run put-channel-status --workspace tools/client-subscriptions-management --",
+ "subscriptions:put-message-status": "npm run put-message-status --workspace tools/client-subscriptions-management --"
},
"workspaces": [
"lambdas/client-transform-filter-lambda",
"src/models",
"lambdas/mock-webhook-lambda",
- "tests/integration"
+ "tests/integration",
+ "tools/client-subscriptions-management"
]
}
diff --git a/scripts/config/sonar-scanner.properties b/scripts/config/sonar-scanner.properties
index be946be..6a2f270 100644
--- a/scripts/config/sonar-scanner.properties
+++ b/scripts/config/sonar-scanner.properties
@@ -5,5 +5,5 @@ sonar.qualitygate.wait=true
sonar.sourceEncoding=UTF-8
sonar.terraform.provider.aws.version=5.54.1
sonar.cpd.exclusions=**.test.*, src/models/**
-sonar.coverage.exclusions=tests/**, lambdas/**/src/__tests__/**, src/models/**, **/jest.config.ts
+sonar.coverage.exclusions=tests/**, lambdas/**/src/__tests__/**, src/models/**, scripts/**/src/__tests__/**, tools/**/src/__tests__/**, **/jest.config.*
sonar.javascript.lcov.reportPaths=lcov.info
diff --git a/scripts/init.mk b/scripts/init.mk
index 885d2d3..89a0611 100644
--- a/scripts/init.mk
+++ b/scripts/init.mk
@@ -46,7 +46,7 @@ _install-dependency: # Install asdf dependency - mandatory: name=[listed in the
asdf install ${name} $(or ${version},)
_install-dependencies: # Install all the dependencies listed in .tool-versions
- for plugin in $$(grep ^[a-z] .tool-versions | sed 's/[[:space:]].*//'); do
+ for plugin in $$(grep ^[a-z] .tool-versions | sed 's/[[:space:]].*//'); do \
$(MAKE) _install-dependency name=$${plugin}; \
done
diff --git a/src/models/src/channel-types.ts b/src/models/src/channel-types.ts
index d4526fb..d50c7c7 100644
--- a/src/models/src/channel-types.ts
+++ b/src/models/src/channel-types.ts
@@ -1 +1,3 @@
-export type Channel = "NHSAPP" | "EMAIL" | "SMS" | "LETTER";
+export const CHANNEL_TYPES = ["NHSAPP", "EMAIL", "SMS", "LETTER"] as const;
+
+export type Channel = (typeof CHANNEL_TYPES)[number];
diff --git a/src/models/src/client-config.ts b/src/models/src/client-config.ts
index e3f5c56..1afcc2c 100644
--- a/src/models/src/client-config.ts
+++ b/src/models/src/client-config.ts
@@ -1,24 +1,21 @@
+import type { Channel } from "./channel-types";
+import type {
+ ChannelStatus,
+ MessageStatus,
+ SupplierStatus,
+} from "./status-types";
+
export type ClientSubscriptionConfiguration = (
| MessageStatusSubscriptionConfiguration
| ChannelStatusSubscriptionConfiguration
)[];
interface SubscriptionConfigurationBase {
- Name: string;
+ SubscriptionId: string;
ClientId: string;
- Description: string;
- EventSource: string;
- EventDetail: string;
Targets: {
Type: "API";
TargetId: string;
- Name: string;
- InputTransformer: {
- InputPaths: string;
- InputHeaders: {
- "x-hmac-sha256-signature": string;
- };
- };
InvocationEndpoint: string;
InvocationMethod: "POST";
InvocationRateLimit: number;
@@ -29,14 +26,16 @@ interface SubscriptionConfigurationBase {
}[];
}
-export interface MessageStatusSubscriptionConfiguration extends SubscriptionConfigurationBase {
+export interface MessageStatusSubscriptionConfiguration
+ extends SubscriptionConfigurationBase {
SubscriptionType: "MessageStatus";
- Statuses: string[];
+ MessageStatuses: MessageStatus[];
}
-export interface ChannelStatusSubscriptionConfiguration extends SubscriptionConfigurationBase {
+export interface ChannelStatusSubscriptionConfiguration
+ extends SubscriptionConfigurationBase {
SubscriptionType: "ChannelStatus";
- ChannelType: string;
- ChannelStatuses: string[];
- SupplierStatuses: string[];
+ ChannelType: Channel;
+ ChannelStatuses: ChannelStatus[];
+ SupplierStatuses: SupplierStatus[];
}
diff --git a/src/models/src/index.ts b/src/models/src/index.ts
index d26c42d..c01a868 100644
--- a/src/models/src/index.ts
+++ b/src/models/src/index.ts
@@ -1,4 +1,5 @@
export type { ChannelStatusData } from "./channel-status-data";
+export { CHANNEL_TYPES } from "./channel-types";
export type { Channel } from "./channel-types";
export type {
CallbackItem,
@@ -19,6 +20,11 @@ export type { MessageStatusData } from "./message-status-data";
export type { RoutingPlan } from "./routing-plan";
export { EventTypes } from "./status-publish-event";
export type { StatusPublishEvent } from "./status-publish-event";
+export {
+ CHANNEL_STATUSES,
+ MESSAGE_STATUSES,
+ SUPPLIER_STATUSES,
+} from "./status-types";
export type {
ChannelStatus,
MessageStatus,
diff --git a/src/models/src/status-types.ts b/src/models/src/status-types.ts
index 2d2bfef..b2e9e5c 100644
--- a/src/models/src/status-types.ts
+++ b/src/models/src/status-types.ts
@@ -1,41 +1,50 @@
-export type MessageStatus =
- | "FAILED"
- | "PENDING_ENRICHMENT"
- | "DELIVERED"
- | "ENRICHED"
- | "SENDING";
+export const MESSAGE_STATUSES = [
+ "FAILED",
+ "PENDING_ENRICHMENT",
+ "DELIVERED",
+ "ENRICHED",
+ "SENDING",
+] as const;
-export type ChannelStatus =
- | "ASSIGNING_BATCH"
- | "CREATED"
- | "SENDING"
- | "DELIVERED"
- | "FAILED"
- | "RETRY"
- | "SKIPPED"
- | "STALE_PDS";
+export type MessageStatus = (typeof MESSAGE_STATUSES)[number];
-export type SupplierStatus =
- | "accepted"
- | "cancelled"
- | "created"
- | "delivered"
- | "dispatched"
- | "enclosed"
- | "failed"
- | "forwarded"
- | "pending"
- | "printed"
- | "read"
- | "notification_attempted"
- | "notified"
- | "rejected"
- | "returned"
- | "sending"
- | "sent"
- | "received"
- | "permanent_failure"
- | "temporary_failure"
- | "technical_failure"
- | "unnotified"
- | "unknown";
+export const CHANNEL_STATUSES = [
+ "ASSIGNING_BATCH",
+ "CREATED",
+ "SENDING",
+ "DELIVERED",
+ "FAILED",
+ "RETRY",
+ "SKIPPED",
+ "STALE_PDS",
+] as const;
+
+export type ChannelStatus = (typeof CHANNEL_STATUSES)[number];
+
+export const SUPPLIER_STATUSES = [
+ "accepted",
+ "cancelled",
+ "created",
+ "delivered",
+ "dispatched",
+ "enclosed",
+ "failed",
+ "forwarded",
+ "pending",
+ "printed",
+ "read",
+ "notification_attempted",
+ "notified",
+ "rejected",
+ "returned",
+ "sending",
+ "sent",
+ "received",
+ "permanent_failure",
+ "temporary_failure",
+ "technical_failure",
+ "unnotified",
+ "unknown",
+] as const;
+
+export type SupplierStatus = (typeof SUPPLIER_STATUSES)[number];
diff --git a/tests/integration/helpers/aws-helpers.ts b/tests/integration/helpers/aws-helpers.ts
new file mode 100644
index 0000000..c4c2ebe
--- /dev/null
+++ b/tests/integration/helpers/aws-helpers.ts
@@ -0,0 +1,45 @@
+import { S3Client } from "@aws-sdk/client-s3";
+
+export type DeploymentDetails = {
+ region: string;
+ environment: string;
+ accountId: string;
+};
+
+/**
+ * Reads deployment context from environment variables
+ *
+ * Requires: AWS_REGION, ENVIRONMENT, AWS_ACCOUNT_ID
+ */
+export function getDeploymentDetails(): DeploymentDetails {
+ const region = process.env.AWS_REGION ?? "eu-west-2";
+ const environment = process.env.ENVIRONMENT;
+ const accountId = process.env.AWS_ACCOUNT_ID;
+
+ if (!environment) {
+ throw new Error("ENVIRONMENT environment variable must be set");
+ }
+ if (!accountId) {
+ throw new Error("AWS_ACCOUNT_ID environment variable must be set");
+ }
+
+ return { region, environment, accountId };
+}
+
+/**
+ * Builds an S3 bucket name from deployment details and a bucket-specific suffix.
+ */
+export function buildBucketName(
+ { accountId, environment, region }: DeploymentDetails,
+ suffix: string,
+): string {
+ return `nhs-${accountId}-${region}-${environment}-${suffix}`;
+}
+
+/**
+ * Creates an S3 client configured for the given region.
+ */
+export function createS3Client(): S3Client {
+ const region = process.env.AWS_REGION ?? "eu-west-2";
+ return new S3Client({ region });
+}
diff --git a/tests/integration/helpers/index.ts b/tests/integration/helpers/index.ts
index b0718c3..e2b912e 100644
--- a/tests/integration/helpers/index.ts
+++ b/tests/integration/helpers/index.ts
@@ -1 +1,2 @@
+export * from "./aws-helpers";
export * from "./cloudwatch-helpers";
diff --git a/tests/integration/infrastructure-exists.test.ts b/tests/integration/infrastructure-exists.test.ts
new file mode 100644
index 0000000..bf53f2c
--- /dev/null
+++ b/tests/integration/infrastructure-exists.test.ts
@@ -0,0 +1,29 @@
+import { HeadBucketCommand } from "@aws-sdk/client-s3";
+import type { S3Client } from "@aws-sdk/client-s3";
+import { buildBucketName, createS3Client, getDeploymentDetails } from "helpers";
+
+describe("Infrastructure exists", () => {
+ let s3Client: S3Client;
+ let bucketName: string;
+
+ beforeAll(async () => {
+ const deploymentDetails = getDeploymentDetails();
+ bucketName = buildBucketName(
+ deploymentDetails,
+ "callbacks-subscription-config",
+ );
+ s3Client = createS3Client();
+ });
+
+ afterAll(() => {
+ s3Client?.destroy();
+ });
+
+ it("should confirm the subscription config S3 bucket exists", async () => {
+ const response = await s3Client.send(
+ new HeadBucketCommand({ Bucket: bucketName }),
+ );
+
+ expect(response.$metadata.httpStatusCode).toBe(200);
+ });
+});
diff --git a/tests/integration/jest.config.ts b/tests/integration/jest.config.ts
index 065243c..e52fedf 100644
--- a/tests/integration/jest.config.ts
+++ b/tests/integration/jest.config.ts
@@ -7,4 +7,7 @@ export default {
...(nodeJestConfig.coveragePathIgnorePatterns ?? []),
"/helpers/",
],
+ moduleNameMapper: {
+ "^helpers$": "/helpers/index",
+ },
};
diff --git a/tests/integration/package.json b/tests/integration/package.json
index c9e2212..def62d6 100644
--- a/tests/integration/package.json
+++ b/tests/integration/package.json
@@ -10,7 +10,12 @@
"typecheck": "tsc --noEmit"
},
"dependencies": {
- "@aws-sdk/client-cloudwatch-logs": "^3.991.0"
+ "@aws-sdk/client-cloudwatch-logs": "^3.991.0",
+ "@aws-sdk/client-eventbridge": "^3.990.0",
+ "@aws-sdk/client-s3": "^3.821.0",
+ "@aws-sdk/client-sts": "^3.821.0",
+ "@aws-sdk/client-sqs": "^3.990.0",
+ "async-wait-until": "^2.0.12"
},
"devDependencies": {
"@aws-sdk/client-sqs": "^3.990.0",
diff --git a/tests/integration/tsconfig.json b/tests/integration/tsconfig.json
index c0ab68d..a5cc2b8 100644
--- a/tests/integration/tsconfig.json
+++ b/tests/integration/tsconfig.json
@@ -1,7 +1,12 @@
{
"compilerOptions": {
"baseUrl": ".",
- "isolatedModules": true
+ "isolatedModules": true,
+ "paths": {
+ "helpers": [
+ "./helpers/index"
+ ]
+ }
},
"extends": "../../tsconfig.base.json",
"include": [
diff --git a/tools/client-subscriptions-management/README.md b/tools/client-subscriptions-management/README.md
new file mode 100644
index 0000000..32a900f
--- /dev/null
+++ b/tools/client-subscriptions-management/README.md
@@ -0,0 +1,117 @@
+# client-subscriptions-management
+
+TypeScript CLI utility for managing NHS Notify client subscription configuration in S3.
+
+## Usage
+
+From the repository root run:
+
+```bash
+npm --workspace tools/client-subscriptions-management run -- [options]
+```
+
+## Example
+
+Deploy a message status subscription to the `dev` environment using a named AWS profile:
+
+```bash
+npm --workspace tools/client-subscriptions-management run deploy -- message \
+ --environment dev \
+ --profile my-profile \
+ --client-id my-client \
+ --message-statuses DELIVERED FAILED \
+ --api-endpoint https://webhook.example.invalid/callbacks \
+ --api-key 1234.4321 \
+ --rate-limit 20 \
+ --dry-run false \
+ --terraform-apply \
+ --group dev
+```
+
+## Commands
+
+### Deploy a Subscription (upload config + optionally apply Terraform)
+
+Use `deploy` to upload a subscription config to S3 and optionally trigger a Terraform apply in one step.
+
+#### Message status
+
+```bash
+npm --workspace tools/client-subscriptions-management run deploy -- message \
+ --environment dev \
+ --client-id client-123 \
+ --message-statuses DELIVERED FAILED \
+ --api-endpoint https://webhook.example.invalid \
+ --api-key-header-name x-api-key \
+ --api-key 1234.4321 \
+ --dry-run false \
+ --rate-limit 20 \
+ --terraform-apply \
+ --group dev
+```
+
+#### Channel status
+
+```bash
+npm --workspace tools/client-subscriptions-management run deploy -- channel \
+ --environment dev \
+ --client-id client-123 \
+ --channel-type EMAIL \
+ --channel-statuses DELIVERED FAILED \
+ --supplier-statuses READ REJECTED \
+ --api-endpoint https://webhook.example.invalid \
+ --api-key-header-name x-api-key \
+ --api-key 1234.4321 \
+ --dry-run false \
+ --rate-limit 20 \
+ --terraform-apply \
+ --group dev
+```
+
+Optional for both: `--client-name "Test Client"` (defaults to client-id if not provided), `--project ` (defaults to `nhs`), `--region ` (defaults to `eu-west-2`), `--profile `, `--tf-region `, `--bucket-name ` (override derived bucket name)
+
+**Note (channel)**: At least one of `--channel-statuses` or `--supplier-statuses` must be provided.
+
+### Get Client Subscriptions By Client ID
+
+```bash
+npm --workspace tools/client-subscriptions-management run get-by-client-id -- \
+ --environment dev \
+ --client-id client-123
+```
+
+### Put Message Status Subscription (S3 upload only)
+
+```bash
+npm --workspace tools/client-subscriptions-management run put-message-status -- \
+ --environment dev \
+ --client-id client-123 \
+ --message-statuses DELIVERED FAILED \
+ --api-endpoint https://webhook.example.invalid \
+ --api-key-header-name x-api-key \
+ --api-key 1234.4321 \
+ --dry-run false \
+ --rate-limit 20
+```
+
+Optional: `--client-name "Test Client"` (defaults to client-id if not provided), `--profile `, `--bucket-name `
+
+### Put Channel Status Subscription (S3 upload only)
+
+```bash
+npm --workspace tools/client-subscriptions-management run put-channel-status -- \
+ --environment dev \
+ --client-id client-123 \
+ --channel-type EMAIL \
+ --channel-statuses DELIVERED FAILED \
+ --supplier-statuses READ REJECTED \
+ --api-endpoint https://webhook.example.invalid \
+ --api-key-header-name x-api-key \
+ --api-key 1234.4321 \
+ --dry-run false \
+ --rate-limit 20
+```
+
+Optional: `--client-name "Test Client"` (defaults to client-id if not provided), `--profile `, `--bucket-name `
+
+**Note**: At least one of `--channel-statuses` or `--supplier-statuses` must be provided.
diff --git a/tools/client-subscriptions-management/jest.config.ts b/tools/client-subscriptions-management/jest.config.ts
new file mode 100644
index 0000000..679cd1c
--- /dev/null
+++ b/tools/client-subscriptions-management/jest.config.ts
@@ -0,0 +1,20 @@
+import type { Config } from "jest";
+
+const jestConfig: Config = {
+ preset: "ts-jest",
+ clearMocks: true,
+ collectCoverage: true,
+ coverageDirectory: "./.reports/unit/coverage",
+ coverageProvider: "babel",
+ coveragePathIgnorePatterns: ["/__tests__/"],
+ transform: { "^.+\\.ts$": "ts-jest" },
+ testPathIgnorePatterns: [String.raw`\.build`],
+ testMatch: ["**/?(*.)+(spec|test).[jt]s?(x)"],
+ testEnvironment: "node",
+ modulePaths: ["/src"],
+ moduleNameMapper: {
+ "^src/(.*)$": "/src/$1",
+ },
+};
+
+export default jestConfig;
diff --git a/tools/client-subscriptions-management/package.json b/tools/client-subscriptions-management/package.json
new file mode 100644
index 0000000..ef4857d
--- /dev/null
+++ b/tools/client-subscriptions-management/package.json
@@ -0,0 +1,32 @@
+{
+ "name": "client-subscriptions-management",
+ "version": "0.0.1",
+ "private": true,
+ "main": "src/index.ts",
+ "scripts": {
+ "get-by-client-id": "tsx ./src/entrypoint/cli/get-client-subscriptions.ts",
+ "put-channel-status": "tsx ./src/entrypoint/cli/put-channel-status.ts",
+ "put-message-status": "tsx ./src/entrypoint/cli/put-message-status.ts",
+ "deploy": "tsx ./src/entrypoint/cli/deploy.ts",
+ "lint": "eslint .",
+ "lint:fix": "eslint . --fix",
+ "test:unit": "jest",
+ "typecheck": "tsc --noEmit"
+ },
+ "dependencies": {
+ "@aws-sdk/client-s3": "^3.821.0",
+ "@aws-sdk/client-sts": "^3.1004.0",
+ "@aws-sdk/credential-providers": "^3.1004.0",
+ "@nhs-notify-client-callbacks/models": "*",
+ "table": "^6.9.0",
+ "yargs": "^17.7.2",
+ "zod": "^4.3.6"
+ },
+ "devDependencies": {
+ "@types/node": "^22.10.10",
+ "@types/yargs": "^17.0.24",
+ "eslint": "^9.27.0",
+ "tsx": "^4.19.3",
+ "typescript": "^5.8.2"
+ }
+}
diff --git a/tools/client-subscriptions-management/src/__tests__/container-s3-config.test.ts b/tools/client-subscriptions-management/src/__tests__/container-s3-config.test.ts
new file mode 100644
index 0000000..d8214e6
--- /dev/null
+++ b/tools/client-subscriptions-management/src/__tests__/container-s3-config.test.ts
@@ -0,0 +1,52 @@
+import { createS3Client } from "src/container";
+
+const mockFromIni = jest.fn().mockReturnValue({ accessKeyId: "from-ini" });
+jest.mock("@aws-sdk/credential-providers", () => ({
+ fromIni: (...args: unknown[]) => mockFromIni(...args),
+}));
+
+describe("createS3Client", () => {
+ it("sets forcePathStyle=true when endpoint contains localhost", () => {
+ const env = { AWS_ENDPOINT_URL: "http://localhost:4566" };
+ const client = createS3Client("eu-west-2", undefined, env);
+
+ // Access the config through the client's config property
+ const { config } = client as any;
+ expect(config.endpoint).toBeDefined();
+ expect(config.forcePathStyle).toBe(true);
+ });
+
+ it("does not set forcePathStyle=true when endpoint does not contain localhost", () => {
+ const env = { AWS_ENDPOINT_URL: "https://custom-s3.example.com" };
+ const client = createS3Client("eu-west-2", undefined, env);
+
+ const { config } = client as any;
+ expect(config.endpoint).toBeDefined();
+ // S3Client converts undefined to false, so we just check it's not true
+ expect(config.forcePathStyle).not.toBe(true);
+ });
+
+ it("does not set forcePathStyle=true when endpoint is not set", () => {
+ const env = {};
+ const client = createS3Client("eu-west-2", undefined, env);
+
+ const { config } = client as any;
+ // S3Client converts undefined to false, so we just check it's not true
+ expect(config.forcePathStyle).not.toBe(true);
+ });
+
+ it("uses fromIni credentials when a profile is provided", () => {
+ const client = createS3Client("eu-west-2", "my-profile", {});
+
+ const { config } = client as any;
+ expect(mockFromIni).toHaveBeenCalledWith({ profile: "my-profile" });
+ expect(config.credentials).toBeDefined();
+ });
+
+ it("does not use fromIni credentials when profile is undefined", () => {
+ mockFromIni.mockClear();
+ createS3Client("eu-west-2", undefined, {});
+
+ expect(mockFromIni).not.toHaveBeenCalled();
+ });
+});
diff --git a/tools/client-subscriptions-management/src/__tests__/container.test.ts b/tools/client-subscriptions-management/src/__tests__/container.test.ts
new file mode 100644
index 0000000..f264e1e
--- /dev/null
+++ b/tools/client-subscriptions-management/src/__tests__/container.test.ts
@@ -0,0 +1,44 @@
+import { S3Client } from "@aws-sdk/client-s3";
+
+const mockS3Repository = jest.fn();
+const mockBuilderObject = {
+ messageStatus: jest.fn(),
+ channelStatus: jest.fn(),
+};
+const mockRepository = jest.fn();
+
+jest.mock("src/repository/s3", () => ({
+ S3Repository: mockS3Repository,
+}));
+
+jest.mock("src/domain/client-subscription-builder", () => ({
+ clientSubscriptionBuilder: mockBuilderObject,
+}));
+
+jest.mock("src/repository/client-subscriptions", () => ({
+ ClientSubscriptionRepository: mockRepository,
+}));
+
+import { createClientSubscriptionRepository } from "src/container";
+
+describe("createClientSubscriptionRepository", () => {
+ it("creates repository with provided options", () => {
+ const repoInstance = { repo: true };
+ mockRepository.mockReturnValue(repoInstance);
+
+ const result = createClientSubscriptionRepository({
+ bucketName: "bucket-1",
+ region: "eu-west-2",
+ });
+
+ expect(mockS3Repository).toHaveBeenCalledWith(
+ "bucket-1",
+ expect.any(S3Client),
+ );
+ expect(mockRepository).toHaveBeenCalledWith(
+ mockS3Repository.mock.instances[0],
+ mockBuilderObject,
+ );
+ expect(result).toBe(repoInstance);
+ });
+});
diff --git a/tools/client-subscriptions-management/src/__tests__/domain/client-subscription-builder.test.ts b/tools/client-subscriptions-management/src/__tests__/domain/client-subscription-builder.test.ts
new file mode 100644
index 0000000..1ff192e
--- /dev/null
+++ b/tools/client-subscriptions-management/src/__tests__/domain/client-subscription-builder.test.ts
@@ -0,0 +1,72 @@
+import {
+ buildChannelStatusSubscription,
+ buildMessageStatusSubscription,
+} from "src/domain/client-subscription-builder";
+
+describe("buildMessageStatusSubscription", () => {
+ it("builds message status subscription", () => {
+ const result = buildMessageStatusSubscription({
+ apiEndpoint: "https://example.com/webhook",
+ apiKey: "secret",
+ apiKeyHeaderName: "x-api-key",
+ clientId: "client-1",
+ clientName: "Client One",
+ rateLimit: 10,
+ statuses: ["DELIVERED"],
+ dryRun: false,
+ });
+
+ expect(result).toMatchObject({
+ SubscriptionId: "client-one",
+ SubscriptionType: "MessageStatus",
+ ClientId: "client-1",
+ MessageStatuses: ["DELIVERED"],
+ });
+ expect(result.Targets[0].TargetId).toMatch(
+ /^[\da-f]{8}-[\da-f]{4}-4[\da-f]{3}-[89ab][\da-f]{3}-[\da-f]{12}$/i,
+ );
+ });
+});
+
+describe("buildChannelStatusSubscription", () => {
+ it("builds channel status subscription", () => {
+ const result = buildChannelStatusSubscription({
+ apiEndpoint: "https://example.com/webhook",
+ apiKey: "secret",
+ clientId: "client-1",
+ clientName: "Client One",
+ channelStatuses: ["DELIVERED"],
+ supplierStatuses: ["delivered"],
+ channelType: "SMS",
+ rateLimit: 20,
+ dryRun: false,
+ });
+
+ expect(result).toMatchObject({
+ SubscriptionId: "client-one-SMS",
+ SubscriptionType: "ChannelStatus",
+ ClientId: "client-1",
+ ChannelType: "SMS",
+ ChannelStatuses: ["DELIVERED"],
+ SupplierStatuses: ["delivered"],
+ });
+ expect(result.Targets[0].TargetId).toMatch(
+ /^[\da-f]{8}-[\da-f]{4}-4[\da-f]{3}-[89ab][\da-f]{3}-[\da-f]{12}$/i,
+ );
+ });
+
+ it("defaults channelStatuses and supplierStatuses to [] when not provided", () => {
+ const result = buildChannelStatusSubscription({
+ apiEndpoint: "https://example.com/webhook",
+ apiKey: "secret",
+ clientId: "client-1",
+ clientName: "Client One",
+ channelType: "SMS",
+ rateLimit: 10,
+ dryRun: false,
+ });
+
+ expect(result.ChannelStatuses).toEqual([]);
+ expect(result.SupplierStatuses).toEqual([]);
+ });
+});
diff --git a/tools/client-subscriptions-management/src/__tests__/entrypoint/cli/get-client-subscriptions.test.ts b/tools/client-subscriptions-management/src/__tests__/entrypoint/cli/get-client-subscriptions.test.ts
new file mode 100644
index 0000000..a0993ab
--- /dev/null
+++ b/tools/client-subscriptions-management/src/__tests__/entrypoint/cli/get-client-subscriptions.test.ts
@@ -0,0 +1,173 @@
+const mockGetClientSubscriptions = jest.fn();
+const mockCreateRepository = jest.fn().mockReturnValue({
+ getClientSubscriptions: mockGetClientSubscriptions,
+});
+const mockFormatSubscriptionFileResponse = jest.fn();
+const mockResolveBucketName = jest.fn().mockReturnValue("bucket");
+const mockResolveProfile = jest.fn().mockReturnValue(undefined);
+const mockResolveRegion = jest.fn().mockReturnValue("region");
+
+jest.mock("src/container", () => ({
+ createClientSubscriptionRepository: mockCreateRepository,
+}));
+
+jest.mock("src/entrypoint/cli/helper", () => ({
+ formatSubscriptionFileResponse: mockFormatSubscriptionFileResponse,
+ resolveBucketName: mockResolveBucketName,
+ resolveProfile: mockResolveProfile,
+ resolveRegion: mockResolveRegion,
+}));
+
+import * as cli from "src/entrypoint/cli/get-client-subscriptions";
+
+describe("get-client-subscriptions CLI", () => {
+ const originalLog = console.log;
+ const originalError = console.error;
+ const originalExitCode = process.exitCode;
+ const originalArgv = process.argv;
+
+ beforeEach(() => {
+ mockGetClientSubscriptions.mockReset();
+ mockFormatSubscriptionFileResponse.mockReset();
+ mockResolveBucketName.mockReset();
+ mockResolveBucketName.mockReturnValue("bucket");
+ mockResolveRegion.mockReset();
+ mockResolveRegion.mockReturnValue("region");
+ console.log = jest.fn();
+ console.error = jest.fn();
+ delete process.exitCode;
+ });
+
+ afterAll(() => {
+ console.log = originalLog;
+ console.error = originalError;
+ process.exitCode = originalExitCode;
+ process.argv = originalArgv;
+ });
+
+ it("prints formatted config when subscription exists", async () => {
+ mockGetClientSubscriptions.mockResolvedValue([
+ { SubscriptionType: "MessageStatus" },
+ ]);
+ mockFormatSubscriptionFileResponse.mockReturnValue(["formatted"]);
+
+ await cli.main([
+ "node",
+ "script",
+ "--client-id",
+ "client-1",
+ "--bucket-name",
+ "bucket-1",
+ ]);
+
+ expect(mockCreateRepository).toHaveBeenCalled();
+ expect(mockGetClientSubscriptions).toHaveBeenCalledWith("client-1");
+ expect(console.log).toHaveBeenCalledWith(["formatted"]);
+ });
+
+ it("prints message when no configuration exists", async () => {
+ mockGetClientSubscriptions.mockResolvedValue(undefined);
+
+ await cli.main([
+ "node",
+ "script",
+ "--client-id",
+ "client-1",
+ "--bucket-name",
+ "bucket-1",
+ ]);
+
+ expect(console.log).toHaveBeenCalledWith(
+ "No configuration exists for client: client-1",
+ );
+ });
+
+ it("handles errors in runCli", async () => {
+ mockResolveBucketName.mockImplementation(() => {
+ throw new Error("Boom");
+ });
+
+ await cli.runCli([
+ "node",
+ "script",
+ "--client-id",
+ "client-1",
+ "--bucket-name",
+ "bucket-1",
+ ]);
+
+ expect(console.error).toHaveBeenCalledWith(new Error("Boom"));
+ expect(process.exitCode).toBe(1);
+ });
+
+ it("executes when run as main module", async () => {
+ mockGetClientSubscriptions.mockResolvedValue(undefined);
+ const runCliSpy = jest.spyOn(cli, "runCli").mockResolvedValue();
+
+ await cli.runIfMain(
+ [
+ "node",
+ "script",
+ "--client-id",
+ "client-1",
+ "--bucket-name",
+ "bucket-1",
+ ],
+ true,
+ );
+
+ expect(runCliSpy).toHaveBeenCalled();
+ runCliSpy.mockRestore();
+ });
+
+ it("does not execute when not main module", async () => {
+ const runCliSpy = jest.spyOn(cli, "runCli").mockResolvedValue();
+
+ await cli.runIfMain(
+ [
+ "node",
+ "script",
+ "--client-id",
+ "client-1",
+ "--bucket-name",
+ "bucket-1",
+ ],
+ false,
+ );
+
+ expect(runCliSpy).not.toHaveBeenCalled();
+ runCliSpy.mockRestore();
+ });
+
+ it("uses process.argv when no args are provided", async () => {
+ process.argv = [
+ "node",
+ "script",
+ "--client-id",
+ "client-1",
+ "--bucket-name",
+ "bucket-1",
+ ];
+ mockGetClientSubscriptions.mockResolvedValue(undefined);
+
+ await cli.runCli();
+
+ expect(mockGetClientSubscriptions).toHaveBeenCalledWith("client-1");
+ });
+
+ it("uses default args in main when none are provided", async () => {
+ process.argv = [
+ "node",
+ "script",
+ "--client-id",
+ "client-2",
+ "--bucket-name",
+ "bucket-2",
+ ];
+ mockGetClientSubscriptions.mockResolvedValue(undefined);
+
+ await cli.main();
+
+ expect(mockGetClientSubscriptions).toHaveBeenCalledWith("client-2");
+ });
+});
diff --git a/tools/client-subscriptions-management/src/__tests__/entrypoint/cli/helper.test.ts b/tools/client-subscriptions-management/src/__tests__/entrypoint/cli/helper.test.ts
new file mode 100644
index 0000000..2c3c291
--- /dev/null
+++ b/tools/client-subscriptions-management/src/__tests__/entrypoint/cli/helper.test.ts
@@ -0,0 +1,166 @@
+import type {
+ ChannelStatusSubscriptionConfiguration,
+ ClientSubscriptionConfiguration,
+ MessageStatusSubscriptionConfiguration,
+} from "@nhs-notify-client-callbacks/models";
+import {
+ deriveBucketName,
+ formatSubscriptionFileResponse,
+ normalizeClientName,
+ resolveBucketName,
+ resolveProfile,
+ resolveRegion,
+} from "src/entrypoint/cli/helper";
+
+jest.mock("@aws-sdk/client-sts", () => ({
+ STSClient: jest.fn().mockImplementation(() => ({
+ send: jest.fn().mockResolvedValue({ Account: "123456789012" }),
+ })),
+ GetCallerIdentityCommand: jest.fn(),
+}));
+
+describe("cli helper", () => {
+ const messageSubscription: MessageStatusSubscriptionConfiguration = {
+ SubscriptionId: "client-a",
+ SubscriptionType: "MessageStatus",
+ ClientId: "client-a",
+ MessageStatuses: ["DELIVERED"],
+ Targets: [
+ {
+ Type: "API",
+ TargetId: "00000000-0000-4000-8000-000000000001",
+ InvocationEndpoint: "https://example.com/webhook",
+ InvocationMethod: "POST",
+ InvocationRateLimit: 10,
+ APIKey: {
+ HeaderName: "x-api-key",
+ HeaderValue: "secret",
+ },
+ },
+ ],
+ };
+
+ const channelSubscription: ChannelStatusSubscriptionConfiguration = {
+ SubscriptionId: "client-a-sms",
+ SubscriptionType: "ChannelStatus",
+ ClientId: "client-a",
+ ChannelType: "SMS",
+ ChannelStatuses: ["DELIVERED"],
+ SupplierStatuses: ["delivered"],
+ Targets: [
+ {
+ Type: "API",
+ TargetId: "00000000-0000-4000-8000-000000000002",
+ InvocationEndpoint: "https://example.com/webhook",
+ InvocationMethod: "POST",
+ InvocationRateLimit: 20,
+ APIKey: {
+ HeaderName: "x-api-key",
+ HeaderValue: "secret",
+ },
+ },
+ ],
+ };
+
+ it("formats subscription output as a table string", () => {
+ const config: ClientSubscriptionConfiguration = [
+ messageSubscription,
+ channelSubscription,
+ ];
+
+ const result = formatSubscriptionFileResponse(config);
+
+ expect(typeof result).toBe("string");
+ // message status row
+ expect(result).toContain("client-a");
+ expect(result).toContain("MessageStatus");
+ expect(result).toContain("DELIVERED");
+ expect(result).toContain("https://example.com/webhook");
+ expect(result).toContain("POST");
+ expect(result).toContain("x-api-key");
+ expect(result).toContain("secret");
+ // channel status row
+ expect(result).toContain("ChannelStatus");
+ expect(result).toContain("SMS");
+ });
+
+ it("normalizes client name", () => {
+ expect(normalizeClientName("My Client Name")).toBe("my-client-name");
+ });
+
+ it("resolves bucket name from explicit argument", async () => {
+ await expect(resolveBucketName("bucket-1")).resolves.toBe("bucket-1");
+ });
+
+ it("derives bucket name from environment using STS account ID", async () => {
+ await expect(
+ resolveBucketName(undefined, "dev", "eu-west-2"),
+ ).resolves.toBe(
+ "nhs-123456789012-eu-west-2-dev-callbacks-subscription-config",
+ );
+ });
+
+ it("uses default region eu-west-2 when region is not provided", async () => {
+ await expect(resolveBucketName(undefined, "dev")).resolves.toBe(
+ "nhs-123456789012-eu-west-2-dev-callbacks-subscription-config",
+ );
+ });
+
+ it("throws when neither bucket name nor environment provided", async () => {
+ await expect(resolveBucketName()).rejects.toThrow(
+ "Bucket name is required: use --bucket-name to specify directly, or --environment",
+ );
+ });
+
+ it("derives bucket name correctly", () => {
+ expect(deriveBucketName("123456789012", "dev", "eu-west-2")).toBe(
+ "nhs-123456789012-eu-west-2-dev-callbacks-subscription-config",
+ );
+ });
+
+ it("derives bucket name with custom project and component", () => {
+ expect(
+ deriveBucketName("123456789012", "prod", "eu-west-2", "myproj", "mycomp"),
+ ).toBe("myproj-123456789012-eu-west-2-prod-mycomp-subscription-config");
+ });
+
+ it("resolves profile from argument", () => {
+ expect(resolveProfile("my-profile")).toBe("my-profile");
+ });
+
+ it("resolves profile from AWS_PROFILE env", () => {
+ expect(
+ resolveProfile(undefined, {
+ AWS_PROFILE: "env-profile",
+ } as NodeJS.ProcessEnv),
+ ).toBe("env-profile");
+ });
+
+ it("returns undefined when profile is not set", () => {
+ expect(resolveProfile(undefined, {} as NodeJS.ProcessEnv)).toBeUndefined();
+ });
+
+ it("resolves region from argument", () => {
+ expect(resolveRegion("eu-west-2")).toBe("eu-west-2");
+ });
+
+ it("resolves region from AWS_REGION", () => {
+ expect(
+ resolveRegion(undefined, {
+ AWS_REGION: "eu-west-1",
+ } as NodeJS.ProcessEnv),
+ ).toBe("eu-west-1");
+ });
+
+ it("resolves region from AWS_DEFAULT_REGION", () => {
+ expect(
+ resolveRegion(undefined, {
+ AWS_DEFAULT_REGION: "eu-west-3",
+ } as NodeJS.ProcessEnv),
+ ).toBe("eu-west-3");
+ });
+
+ it("returns undefined when region is not set", () => {
+ expect(resolveRegion(undefined, {} as NodeJS.ProcessEnv)).toBeUndefined();
+ });
+});
diff --git a/tools/client-subscriptions-management/src/__tests__/entrypoint/cli/put-channel-status.test.ts b/tools/client-subscriptions-management/src/__tests__/entrypoint/cli/put-channel-status.test.ts
new file mode 100644
index 0000000..92b3a2b
--- /dev/null
+++ b/tools/client-subscriptions-management/src/__tests__/entrypoint/cli/put-channel-status.test.ts
@@ -0,0 +1,378 @@
+const mockPutChannelStatusSubscription = jest.fn();
+const mockCreateRepository = jest.fn().mockReturnValue({
+ putChannelStatusSubscription: mockPutChannelStatusSubscription,
+});
+const mockFormatSubscriptionFileResponse = jest.fn();
+const mockResolveBucketName = jest.fn().mockReturnValue("bucket");
+const mockResolveProfile = jest.fn().mockReturnValue(undefined);
+const mockResolveRegion = jest.fn().mockReturnValue("region");
+jest.mock("src/container", () => ({
+ createClientSubscriptionRepository: mockCreateRepository,
+}));
+
+jest.mock("src/entrypoint/cli/helper", () => ({
+ formatSubscriptionFileResponse: mockFormatSubscriptionFileResponse,
+ resolveBucketName: mockResolveBucketName,
+ resolveProfile: mockResolveProfile,
+ resolveRegion: mockResolveRegion,
+}));
+
+import * as cli from "src/entrypoint/cli/put-channel-status";
+
+describe("put-channel-status CLI", () => {
+ const originalLog = console.log;
+ const originalError = console.error;
+ const originalExitCode = process.exitCode;
+ const originalArgv = process.argv;
+
+ beforeEach(() => {
+ mockPutChannelStatusSubscription.mockReset();
+ mockFormatSubscriptionFileResponse.mockReset();
+ mockResolveBucketName.mockReset();
+ mockResolveBucketName.mockReturnValue("bucket");
+ mockResolveRegion.mockReset();
+ mockResolveRegion.mockReturnValue("region");
+ console.log = jest.fn();
+ console.error = jest.fn();
+ delete process.exitCode;
+ });
+
+ afterAll(() => {
+ console.log = originalLog;
+ console.error = originalError;
+ process.exitCode = originalExitCode;
+ process.argv = originalArgv;
+ });
+
+ it("rejects non-https endpoints", async () => {
+ await cli.main([
+ "node",
+ "script",
+ "--client-name",
+ "Client One",
+ "--client-id",
+ "client-1",
+ "--api-endpoint",
+ "http://example.com",
+ "--api-key",
+ "secret",
+ "--channel-statuses",
+ "DELIVERED",
+ "--supplier-statuses",
+ "delivered",
+ "--channel-type",
+ "SMS",
+ "--rate-limit",
+ "10",
+ "--dry-run",
+ "true",
+ "--bucket-name",
+ "bucket-1",
+ ]);
+
+ expect(console.error).toHaveBeenCalledWith(
+ "Error: api-endpoint must start with https://",
+ );
+ expect(process.exitCode).toBe(1);
+ expect(mockPutChannelStatusSubscription).not.toHaveBeenCalled();
+ });
+
+ it("rejects when neither channel-statuses nor supplier-statuses are provided", async () => {
+ await cli.main([
+ "node",
+ "script",
+ "--client-name",
+ "Client One",
+ "--client-id",
+ "client-1",
+ "--api-endpoint",
+ "https://example.com",
+ "--api-key",
+ "secret",
+ "--channel-type",
+ "SMS",
+ "--rate-limit",
+ "10",
+ "--dry-run",
+ "true",
+ "--bucket-name",
+ "bucket-1",
+ ]);
+
+ expect(console.error).toHaveBeenCalledWith(
+ "Error: at least one of --channel-statuses or --supplier-statuses must be provided",
+ );
+ expect(process.exitCode).toBe(1);
+ expect(mockPutChannelStatusSubscription).not.toHaveBeenCalled();
+ });
+
+ it("writes channel subscription and logs response", async () => {
+ mockPutChannelStatusSubscription.mockResolvedValue([
+ { SubscriptionType: "ChannelStatus" },
+ ]);
+ mockFormatSubscriptionFileResponse.mockReturnValue(["formatted"]);
+
+ await cli.main([
+ "node",
+ "script",
+ "--client-name",
+ "Client One",
+ "--client-id",
+ "client-1",
+ "--api-endpoint",
+ "https://example.com",
+ "--api-key",
+ "secret",
+ "--channel-statuses",
+ "DELIVERED",
+ "--supplier-statuses",
+ "delivered",
+ "--channel-type",
+ "SMS",
+ "--rate-limit",
+ "10",
+ "--dry-run",
+ "false",
+ "--bucket-name",
+ "bucket-1",
+ "--api-key-header-name",
+ "x-api-key",
+ ]);
+
+ expect(mockPutChannelStatusSubscription).toHaveBeenCalledWith({
+ clientName: "Client One",
+ clientId: "client-1",
+ apiEndpoint: "https://example.com",
+ apiKeyHeaderName: "x-api-key",
+ apiKey: "secret",
+ channelType: "SMS",
+ channelStatuses: ["DELIVERED"],
+ supplierStatuses: ["delivered"],
+ rateLimit: 10,
+ dryRun: false,
+ });
+ expect(console.log).toHaveBeenCalledWith(["formatted"]);
+ });
+
+ it("handles errors in runCli", async () => {
+ mockResolveBucketName.mockImplementation(() => {
+ throw new Error("Boom");
+ });
+
+ await cli.runCli([
+ "node",
+ "script",
+ "--client-name",
+ "Client One",
+ "--client-id",
+ "client-1",
+ "--api-endpoint",
+ "https://example.com",
+ "--api-key",
+ "secret",
+ "--channel-statuses",
+ "DELIVERED",
+ "--supplier-statuses",
+ "delivered",
+ "--channel-type",
+ "SMS",
+ "--rate-limit",
+ "10",
+ "--dry-run",
+ "false",
+ "--bucket-name",
+ "bucket-1",
+ ]);
+
+ expect(console.error).toHaveBeenCalledWith(new Error("Boom"));
+ expect(process.exitCode).toBe(1);
+ });
+
+ it("executes when run as main module", async () => {
+ mockPutChannelStatusSubscription.mockResolvedValue([
+ { SubscriptionType: "ChannelStatus" },
+ ]);
+ mockFormatSubscriptionFileResponse.mockReturnValue(["formatted"]);
+ const runCliSpy = jest.spyOn(cli, "runCli").mockResolvedValue();
+
+ await cli.runIfMain(
+ [
+ "node",
+ "script",
+ "--client-name",
+ "Client One",
+ "--client-id",
+ "client-1",
+ "--api-endpoint",
+ "https://example.com",
+ "--api-key",
+ "secret",
+ "--channel-statuses",
+ "DELIVERED",
+ "--supplier-statuses",
+ "delivered",
+ "--channel-type",
+ "SMS",
+ "--rate-limit",
+ "10",
+ "--dry-run",
+ "false",
+ "--bucket-name",
+ "bucket-1",
+ ],
+ true,
+ );
+
+ expect(runCliSpy).toHaveBeenCalled();
+ runCliSpy.mockRestore();
+ });
+
+ it("does not execute when not main module", async () => {
+ const runCliSpy = jest.spyOn(cli, "runCli").mockResolvedValue();
+
+ await cli.runIfMain(
+ [
+ "node",
+ "script",
+ "--client-name",
+ "Client One",
+ "--client-id",
+ "client-1",
+ "--api-endpoint",
+ "https://example.com",
+ "--api-key",
+ "secret",
+ "--channel-statuses",
+ "DELIVERED",
+ "--supplier-statuses",
+ "delivered",
+ "--channel-type",
+ "SMS",
+ "--rate-limit",
+ "10",
+ "--dry-run",
+ "false",
+ "--bucket-name",
+ "bucket-1",
+ ],
+ false,
+ );
+
+ expect(runCliSpy).not.toHaveBeenCalled();
+ runCliSpy.mockRestore();
+ });
+
+ it("uses process.argv when no args are provided", async () => {
+ process.argv = [
+ "node",
+ "script",
+ "--client-name",
+ "Client One",
+ "--client-id",
+ "client-1",
+ "--api-endpoint",
+ "https://example.com",
+ "--api-key",
+ "secret",
+ "--channel-statuses",
+ "DELIVERED",
+ "--supplier-statuses",
+ "delivered",
+ "--channel-type",
+ "SMS",
+ "--rate-limit",
+ "10",
+ "--dry-run",
+ "false",
+ "--bucket-name",
+ "bucket-1",
+ ];
+ mockPutChannelStatusSubscription.mockResolvedValue([
+ { SubscriptionType: "ChannelStatus" },
+ ]);
+ mockFormatSubscriptionFileResponse.mockReturnValue(["formatted"]);
+
+ await cli.runCli();
+
+ expect(mockPutChannelStatusSubscription).toHaveBeenCalled();
+ });
+
+ it("uses default args in main when none are provided", async () => {
+ process.argv = [
+ "node",
+ "script",
+ "--client-name",
+ "Client Two",
+ "--client-id",
+ "client-2",
+ "--api-endpoint",
+ "https://example.com",
+ "--api-key",
+ "secret",
+ "--channel-statuses",
+ "DELIVERED",
+ "--supplier-statuses",
+ "delivered",
+ "--channel-type",
+ "SMS",
+ "--rate-limit",
+ "10",
+ "--dry-run",
+ "false",
+ "--bucket-name",
+ "bucket-1",
+ ];
+ mockPutChannelStatusSubscription.mockResolvedValue([
+ { SubscriptionType: "ChannelStatus" },
+ ]);
+ mockFormatSubscriptionFileResponse.mockReturnValue(["formatted"]);
+
+ await cli.main();
+
+ expect(mockPutChannelStatusSubscription).toHaveBeenCalled();
+ });
+
+ it("defaults client-name to client-id when not provided", async () => {
+ mockPutChannelStatusSubscription.mockResolvedValue([
+ { SubscriptionType: "ChannelStatus" },
+ ]);
+ mockFormatSubscriptionFileResponse.mockReturnValue(["formatted"]);
+
+ await cli.main([
+ "node",
+ "script",
+ "--client-id",
+ "client-1",
+ "--api-endpoint",
+ "https://example.com",
+ "--api-key",
+ "secret",
+ "--channel-statuses",
+ "DELIVERED",
+ "--supplier-statuses",
+ "delivered",
+ "--channel-type",
+ "SMS",
+ "--rate-limit",
+ "10",
+ "--dry-run",
+ "false",
+ "--bucket-name",
+ "bucket-1",
+ ]);
+
+ expect(mockPutChannelStatusSubscription).toHaveBeenCalledWith({
+ clientName: "client-1",
+ clientId: "client-1",
+ apiEndpoint: "https://example.com",
+ apiKeyHeaderName: "x-api-key",
+ apiKey: "secret",
+ channelStatuses: ["DELIVERED"],
+ supplierStatuses: ["delivered"],
+ channelType: "SMS",
+ rateLimit: 10,
+ dryRun: false,
+ });
+ expect(console.log).toHaveBeenCalledWith(["formatted"]);
+ });
+});
diff --git a/tools/client-subscriptions-management/src/__tests__/entrypoint/cli/put-message-status.test.ts b/tools/client-subscriptions-management/src/__tests__/entrypoint/cli/put-message-status.test.ts
new file mode 100644
index 0000000..afdb8bf
--- /dev/null
+++ b/tools/client-subscriptions-management/src/__tests__/entrypoint/cli/put-message-status.test.ts
@@ -0,0 +1,313 @@
+const mockPutMessageStatusSubscription = jest.fn();
+const mockCreateRepository = jest.fn().mockReturnValue({
+ putMessageStatusSubscription: mockPutMessageStatusSubscription,
+});
+const mockFormatSubscriptionFileResponse = jest.fn();
+const mockResolveBucketName = jest.fn().mockReturnValue("bucket");
+const mockResolveProfile = jest.fn().mockReturnValue(undefined);
+const mockResolveRegion = jest.fn().mockReturnValue("region");
+jest.mock("src/container", () => ({
+ createClientSubscriptionRepository: mockCreateRepository,
+}));
+
+jest.mock("src/entrypoint/cli/helper", () => ({
+ formatSubscriptionFileResponse: mockFormatSubscriptionFileResponse,
+ resolveBucketName: mockResolveBucketName,
+ resolveProfile: mockResolveProfile,
+ resolveRegion: mockResolveRegion,
+}));
+
+import * as cli from "src/entrypoint/cli/put-message-status";
+
+describe("put-message-status CLI", () => {
+ const originalLog = console.log;
+ const originalError = console.error;
+ const originalExitCode = process.exitCode;
+ const originalArgv = process.argv;
+
+ beforeEach(() => {
+ mockPutMessageStatusSubscription.mockReset();
+ mockFormatSubscriptionFileResponse.mockReset();
+ mockResolveBucketName.mockReset();
+ mockResolveBucketName.mockReturnValue("bucket");
+ mockResolveRegion.mockReset();
+ mockResolveRegion.mockReturnValue("region");
+ console.log = jest.fn();
+ console.error = jest.fn();
+ delete process.exitCode;
+ });
+
+ afterAll(() => {
+ console.log = originalLog;
+ console.error = originalError;
+ process.exitCode = originalExitCode;
+ process.argv = originalArgv;
+ });
+
+ it("rejects non-https endpoints", async () => {
+ await cli.main([
+ "node",
+ "script",
+ "--client-name",
+ "Client One",
+ "--client-id",
+ "client-1",
+ "--api-endpoint",
+ "http://example.com",
+ "--api-key",
+ "secret",
+ "--message-statuses",
+ "DELIVERED",
+ "--rate-limit",
+ "10",
+ "--dry-run",
+ "true",
+ "--bucket-name",
+ "bucket-1",
+ ]);
+
+ expect(console.error).toHaveBeenCalledWith(
+ "Error: api-endpoint must start with https://",
+ );
+ expect(process.exitCode).toBe(1);
+ expect(mockPutMessageStatusSubscription).not.toHaveBeenCalled();
+ });
+
+ it("writes subscription and logs response", async () => {
+ mockPutMessageStatusSubscription.mockResolvedValue([
+ { SubscriptionType: "MessageStatus" },
+ ]);
+ mockFormatSubscriptionFileResponse.mockReturnValue(["formatted"]);
+
+ await cli.main([
+ "node",
+ "script",
+ "--client-name",
+ "Client One",
+ "--client-id",
+ "client-1",
+ "--api-endpoint",
+ "https://example.com",
+ "--api-key",
+ "secret",
+ "--message-statuses",
+ "DELIVERED",
+ "--rate-limit",
+ "10",
+ "--dry-run",
+ "false",
+ "--bucket-name",
+ "bucket-1",
+ "--api-key-header-name",
+ "x-api-key",
+ ]);
+
+ expect(mockPutMessageStatusSubscription).toHaveBeenCalledWith({
+ clientName: "Client One",
+ clientId: "client-1",
+ apiEndpoint: "https://example.com",
+ apiKeyHeaderName: "x-api-key",
+ apiKey: "secret",
+ statuses: ["DELIVERED"],
+ rateLimit: 10,
+ dryRun: false,
+ });
+ expect(console.log).toHaveBeenCalledWith(["formatted"]);
+ });
+
+ it("handles errors in runCli", async () => {
+ mockResolveBucketName.mockImplementation(() => {
+ throw new Error("Boom");
+ });
+
+ await cli.runCli([
+ "node",
+ "script",
+ "--client-name",
+ "Client One",
+ "--client-id",
+ "client-1",
+ "--api-endpoint",
+ "https://example.com",
+ "--api-key",
+ "secret",
+ "--message-statuses",
+ "DELIVERED",
+ "--rate-limit",
+ "10",
+ "--dry-run",
+ "false",
+ "--bucket-name",
+ "bucket-1",
+ ]);
+
+ expect(console.error).toHaveBeenCalledWith(new Error("Boom"));
+ expect(process.exitCode).toBe(1);
+ });
+
+ it("executes when run as main module", async () => {
+ mockPutMessageStatusSubscription.mockResolvedValue([
+ { SubscriptionType: "MessageStatus" },
+ ]);
+ mockFormatSubscriptionFileResponse.mockReturnValue(["formatted"]);
+ const runCliSpy = jest.spyOn(cli, "runCli").mockResolvedValue();
+
+ await cli.runIfMain(
+ [
+ "node",
+ "script",
+ "--client-name",
+ "Client One",
+ "--client-id",
+ "client-1",
+ "--api-endpoint",
+ "https://example.com",
+ "--api-key",
+ "secret",
+ "--message-statuses",
+ "DELIVERED",
+ "--rate-limit",
+ "10",
+ "--dry-run",
+ "false",
+ "--bucket-name",
+ "bucket-1",
+ ],
+ true,
+ );
+
+ expect(runCliSpy).toHaveBeenCalled();
+ runCliSpy.mockRestore();
+ });
+
+ it("does not execute when not main module", async () => {
+ const runCliSpy = jest.spyOn(cli, "runCli").mockResolvedValue();
+
+ await cli.runIfMain(
+ [
+ "node",
+ "script",
+ "--client-name",
+ "Client One",
+ "--client-id",
+ "client-1",
+ "--api-endpoint",
+ "https://example.com",
+ "--api-key",
+ "secret",
+ "--message-statuses",
+ "DELIVERED",
+ "--rate-limit",
+ "10",
+ "--dry-run",
+ "false",
+ "--bucket-name",
+ "bucket-1",
+ ],
+ false,
+ );
+
+ expect(runCliSpy).not.toHaveBeenCalled();
+ runCliSpy.mockRestore();
+ });
+
+ it("uses process.argv when no args are provided", async () => {
+ process.argv = [
+ "node",
+ "script",
+ "--client-name",
+ "Client One",
+ "--client-id",
+ "client-1",
+ "--api-endpoint",
+ "https://example.com",
+ "--api-key",
+ "secret",
+ "--message-statuses",
+ "DELIVERED",
+ "--rate-limit",
+ "10",
+ "--dry-run",
+ "false",
+ "--bucket-name",
+ "bucket-1",
+ ];
+ mockPutMessageStatusSubscription.mockResolvedValue([
+ { SubscriptionType: "MessageStatus" },
+ ]);
+ mockFormatSubscriptionFileResponse.mockReturnValue(["formatted"]);
+
+ await cli.runCli();
+
+ expect(mockPutMessageStatusSubscription).toHaveBeenCalled();
+ });
+
+ it("uses default args in main when none are provided", async () => {
+ process.argv = [
+ "node",
+ "script",
+ "--client-name",
+ "Client Two",
+ "--client-id",
+ "client-2",
+ "--api-endpoint",
+ "https://example.com",
+ "--api-key",
+ "secret",
+ "--message-statuses",
+ "DELIVERED",
+ "--rate-limit",
+ "10",
+ "--dry-run",
+ "false",
+ "--bucket-name",
+ "bucket-1",
+ ];
+ mockPutMessageStatusSubscription.mockResolvedValue([
+ { SubscriptionType: "MessageStatus" },
+ ]);
+ mockFormatSubscriptionFileResponse.mockReturnValue(["formatted"]);
+
+ await cli.main();
+
+ expect(mockPutMessageStatusSubscription).toHaveBeenCalled();
+ });
+
+ it("defaults client-name to client-id when not provided", async () => {
+ mockPutMessageStatusSubscription.mockResolvedValue([
+ { SubscriptionType: "MessageStatus" },
+ ]);
+ mockFormatSubscriptionFileResponse.mockReturnValue(["formatted"]);
+
+ await cli.main([
+ "node",
+ "script",
+ "--client-id",
+ "client-1",
+ "--api-endpoint",
+ "https://example.com",
+ "--api-key",
+ "secret",
+ "--message-statuses",
+ "DELIVERED",
+ "--rate-limit",
+ "10",
+ "--dry-run",
+ "false",
+ "--bucket-name",
+ "bucket-1",
+ ]);
+
+ expect(mockPutMessageStatusSubscription).toHaveBeenCalledWith({
+ clientName: "client-1",
+ clientId: "client-1",
+ apiEndpoint: "https://example.com",
+ apiKeyHeaderName: "x-api-key",
+ apiKey: "secret",
+ statuses: ["DELIVERED"],
+ rateLimit: 10,
+ dryRun: false,
+ });
+ expect(console.log).toHaveBeenCalledWith(["formatted"]);
+ });
+});
diff --git a/tools/client-subscriptions-management/src/__tests__/repository/client-subscriptions.test.ts b/tools/client-subscriptions-management/src/__tests__/repository/client-subscriptions.test.ts
new file mode 100644
index 0000000..93fa6f5
--- /dev/null
+++ b/tools/client-subscriptions-management/src/__tests__/repository/client-subscriptions.test.ts
@@ -0,0 +1,372 @@
+import { z } from "zod";
+import { ClientSubscriptionRepository } from "src/repository/client-subscriptions";
+import type {
+ ChannelStatusSubscriptionConfiguration,
+ ClientSubscriptionConfiguration,
+ MessageStatusSubscriptionConfiguration,
+} from "@nhs-notify-client-callbacks/models";
+import type { S3Repository } from "src/repository/s3";
+import type { SubscriptionBuilder } from "src/domain/client-subscription-builder";
+
+const createRepository = (
+ overrides?: Partial<{
+ getObject: jest.Mock;
+ putRawData: jest.Mock;
+ messageStatus: jest.Mock;
+ channelStatus: jest.Mock;
+ }>,
+) => {
+ const s3Repository = {
+ getObject: overrides?.getObject ?? jest.fn(),
+ putRawData: overrides?.putRawData ?? jest.fn(),
+ } as unknown as S3Repository;
+
+ const configurationBuilder = {
+ messageStatus: overrides?.messageStatus ?? jest.fn(),
+ channelStatus: overrides?.channelStatus ?? jest.fn(),
+ } as unknown as SubscriptionBuilder;
+
+ const repository = new ClientSubscriptionRepository(
+ s3Repository,
+ configurationBuilder,
+ );
+
+ return { repository, s3Repository, configurationBuilder };
+};
+
+describe("ClientSubscriptionRepository", () => {
+ const baseTarget: MessageStatusSubscriptionConfiguration["Targets"][number] =
+ {
+ Type: "API",
+ TargetId: "00000000-0000-4000-8000-000000000001",
+ InvocationEndpoint: "https://example.com/webhook",
+ InvocationMethod: "POST",
+ InvocationRateLimit: 10,
+ APIKey: {
+ HeaderName: "x-api-key",
+ HeaderValue: "secret",
+ },
+ };
+
+ const messageSubscription: MessageStatusSubscriptionConfiguration = {
+ SubscriptionId: "client-1",
+ SubscriptionType: "MessageStatus",
+ ClientId: "client-1",
+ MessageStatuses: ["DELIVERED"],
+ Targets: [baseTarget],
+ };
+
+ const channelSubscription: ChannelStatusSubscriptionConfiguration = {
+ SubscriptionId: "client-1-SMS",
+ SubscriptionType: "ChannelStatus",
+ ClientId: "client-1",
+ ChannelType: "SMS",
+ ChannelStatuses: ["DELIVERED"],
+ SupplierStatuses: ["delivered"],
+ Targets: [baseTarget],
+ };
+
+ it("returns parsed subscriptions when file exists", async () => {
+ const storedConfig: ClientSubscriptionConfiguration = [messageSubscription];
+ const getObject = jest.fn().mockResolvedValue(JSON.stringify(storedConfig));
+ const { repository } = createRepository({ getObject });
+
+ const result = await repository.getClientSubscriptions("client-1");
+
+ expect(result).toEqual(storedConfig);
+ });
+
+ it("returns undefined when config file is missing", async () => {
+ const getObject = jest.fn().mockResolvedValue(undefined);
+ const { repository } = createRepository({ getObject });
+
+ await expect(
+ repository.getClientSubscriptions("client-1"),
+ ).resolves.toBeUndefined();
+ });
+
+ it("replaces existing message subscription", async () => {
+ const storedConfig: ClientSubscriptionConfiguration = [
+ channelSubscription,
+ messageSubscription,
+ ];
+ const getObject = jest.fn().mockResolvedValue(JSON.stringify(storedConfig));
+ const putRawData = jest.fn();
+ const newMessage: MessageStatusSubscriptionConfiguration = {
+ ...messageSubscription,
+ MessageStatuses: ["FAILED"],
+ };
+ const messageStatus = jest.fn().mockReturnValue(newMessage);
+
+ const { repository } = createRepository({
+ getObject,
+ putRawData,
+ messageStatus,
+ });
+
+ const result = await repository.putMessageStatusSubscription({
+ clientName: "Client 1",
+ clientId: "client-1",
+ apiKey: "secret",
+ apiEndpoint: "https://example.com/webhook",
+ statuses: ["FAILED"],
+ rateLimit: 10,
+ dryRun: false,
+ });
+
+ expect(result).toEqual([channelSubscription, newMessage]);
+ expect(putRawData).toHaveBeenCalledWith(
+ JSON.stringify([channelSubscription, newMessage]),
+ "client_subscriptions/client-1.json",
+ );
+ });
+
+ it("skips S3 write when dry run is enabled", async () => {
+ const getObject = jest.fn().mockResolvedValue(undefined);
+ const putRawData = jest.fn();
+ const messageStatus = jest.fn().mockReturnValue(messageSubscription);
+
+ const { repository } = createRepository({
+ getObject,
+ putRawData,
+ messageStatus,
+ });
+
+ await repository.putMessageStatusSubscription({
+ clientName: "Client 1",
+ clientId: "client-1",
+ apiKey: "secret",
+ apiEndpoint: "https://example.com/webhook",
+ statuses: ["DELIVERED"],
+ rateLimit: 10,
+ dryRun: true,
+ });
+
+ expect(putRawData).not.toHaveBeenCalled();
+ });
+
+ it("replaces existing channel subscription for the channel type", async () => {
+ const storedConfig: ClientSubscriptionConfiguration = [
+ channelSubscription,
+ messageSubscription,
+ ];
+ const getObject = jest.fn().mockResolvedValue(JSON.stringify(storedConfig));
+ const putRawData = jest.fn();
+ const newChannel: ChannelStatusSubscriptionConfiguration = {
+ ...channelSubscription,
+ ChannelStatuses: ["FAILED"],
+ };
+ const channelStatus = jest.fn().mockReturnValue(newChannel);
+
+ const { repository } = createRepository({
+ getObject,
+ putRawData,
+ channelStatus,
+ });
+
+ const result = await repository.putChannelStatusSubscription({
+ clientName: "Client 1",
+ clientId: "client-1",
+ apiKey: "secret",
+ apiEndpoint: "https://example.com/webhook",
+ channelStatuses: ["FAILED"],
+ supplierStatuses: ["delivered"],
+ channelType: "SMS",
+ rateLimit: 10,
+ dryRun: false,
+ });
+
+ expect(result).toEqual([messageSubscription, newChannel]);
+ expect(putRawData).toHaveBeenCalledWith(
+ JSON.stringify([messageSubscription, newChannel]),
+ "client_subscriptions/client-1.json",
+ );
+ });
+
+ it("skips S3 write for channel status dry run", async () => {
+ const getObject = jest.fn().mockResolvedValue(undefined);
+ const putRawData = jest.fn();
+ const channelStatus = jest.fn().mockReturnValue(channelSubscription);
+
+ const { repository } = createRepository({
+ getObject,
+ putRawData,
+ channelStatus,
+ });
+
+ await repository.putChannelStatusSubscription({
+ clientName: "Client 1",
+ clientId: "client-1",
+ apiKey: "secret",
+ apiEndpoint: "https://example.com/webhook",
+ channelStatuses: ["DELIVERED"],
+ supplierStatuses: ["delivered"],
+ channelType: "SMS",
+ rateLimit: 10,
+ dryRun: true,
+ });
+
+ expect(putRawData).not.toHaveBeenCalled();
+ });
+
+ describe("validation", () => {
+ it("throws validation error for invalid message status", async () => {
+ const { repository } = createRepository();
+
+ await expect(
+ repository.putMessageStatusSubscription({
+ clientName: "Client 1",
+ clientId: "client-1",
+ apiKey: "secret",
+ apiEndpoint: "https://example.com/webhook",
+ statuses: ["INVALID_STATUS" as never],
+ rateLimit: 10,
+ dryRun: false,
+ }),
+ ).rejects.toThrow(z.ZodError);
+ });
+
+ it("throws validation error for missing required fields in message subscription", async () => {
+ const { repository } = createRepository();
+
+ await expect(
+ repository.putMessageStatusSubscription({
+ clientName: "Client 1",
+ clientId: "client-1",
+ apiKey: "secret",
+ apiEndpoint: "https://example.com/webhook",
+ // @ts-expect-error Testing missing field
+ statuses: undefined,
+ rateLimit: 10,
+ dryRun: false,
+ }),
+ ).rejects.toThrow(z.ZodError);
+ });
+
+ it("throws validation error for invalid channel type", async () => {
+ const { repository } = createRepository();
+
+ await expect(
+ repository.putChannelStatusSubscription({
+ clientName: "Client 1",
+ clientId: "client-1",
+ apiKey: "secret",
+ apiEndpoint: "https://example.com/webhook",
+ channelStatuses: ["DELIVERED"],
+ supplierStatuses: ["delivered"],
+ channelType: "INVALID_CHANNEL" as never,
+ rateLimit: 10,
+ dryRun: false,
+ }),
+ ).rejects.toThrow(z.ZodError);
+ });
+
+ it("throws validation error for invalid channel status", async () => {
+ const { repository } = createRepository();
+
+ await expect(
+ repository.putChannelStatusSubscription({
+ clientName: "Client 1",
+ clientId: "client-1",
+ apiKey: "secret",
+ apiEndpoint: "https://example.com/webhook",
+ channelStatuses: ["INVALID_STATUS" as never],
+ supplierStatuses: ["delivered"],
+ channelType: "SMS",
+ rateLimit: 10,
+ dryRun: false,
+ }),
+ ).rejects.toThrow(z.ZodError);
+ });
+
+ it("throws validation error for invalid supplier status", async () => {
+ const { repository } = createRepository();
+
+ await expect(
+ repository.putChannelStatusSubscription({
+ clientName: "Client 1",
+ clientId: "client-1",
+ apiKey: "secret",
+ apiEndpoint: "https://example.com/webhook",
+ channelStatuses: ["DELIVERED"],
+ supplierStatuses: ["INVALID_STATUS" as never],
+ channelType: "SMS",
+ rateLimit: 10,
+ dryRun: false,
+ }),
+ ).rejects.toThrow(z.ZodError);
+ });
+
+ it("throws validation error when neither channelStatuses nor supplierStatuses are provided", async () => {
+ const { repository } = createRepository();
+
+ await expect(
+ repository.putChannelStatusSubscription({
+ clientName: "Client 1",
+ clientId: "client-1",
+ apiKey: "secret",
+ apiEndpoint: "https://example.com/webhook",
+ channelType: "SMS",
+ rateLimit: 10,
+ dryRun: false,
+ }),
+ ).rejects.toThrow(
+ /at least one of channelStatuses or supplierStatuses must be provided/,
+ );
+ });
+
+ it("applies default value for apiKeyHeaderName on message subscription", async () => {
+ const getObject = jest.fn().mockResolvedValue(undefined as never);
+ const messageStatus = jest.fn().mockReturnValue(messageSubscription);
+
+ const { configurationBuilder, repository } = createRepository({
+ getObject,
+ messageStatus,
+ });
+
+ await repository.putMessageStatusSubscription({
+ clientName: "Client 1",
+ clientId: "client-1",
+ apiKey: "secret",
+ apiEndpoint: "https://example.com/webhook",
+ statuses: ["DELIVERED"],
+ rateLimit: 10,
+ dryRun: false,
+ });
+
+ expect(configurationBuilder.messageStatus).toHaveBeenCalledWith(
+ expect.objectContaining({
+ apiKeyHeaderName: "x-api-key",
+ }),
+ );
+ });
+
+ it("applies default value for apiKeyHeaderName on channel subscription", async () => {
+ const getObject = jest.fn().mockResolvedValue(undefined as never);
+ const channelStatus = jest.fn().mockReturnValue(channelSubscription);
+
+ const { configurationBuilder, repository } = createRepository({
+ getObject,
+ channelStatus,
+ });
+
+ await repository.putChannelStatusSubscription({
+ clientName: "Client 1",
+ clientId: "client-1",
+ apiKey: "secret",
+ apiEndpoint: "https://example.com/webhook",
+ channelStatuses: ["DELIVERED"],
+ supplierStatuses: ["delivered"],
+ channelType: "SMS",
+ rateLimit: 10,
+ dryRun: false,
+ });
+
+ expect(configurationBuilder.channelStatus).toHaveBeenCalledWith(
+ expect.objectContaining({
+ apiKeyHeaderName: "x-api-key",
+ }),
+ );
+ });
+ });
+});
diff --git a/tools/client-subscriptions-management/src/__tests__/repository/s3.test.ts b/tools/client-subscriptions-management/src/__tests__/repository/s3.test.ts
new file mode 100644
index 0000000..04a9037
--- /dev/null
+++ b/tools/client-subscriptions-management/src/__tests__/repository/s3.test.ts
@@ -0,0 +1,68 @@
+import {
+ GetObjectCommand,
+ NoSuchKey,
+ PutObjectCommand,
+ S3Client,
+} from "@aws-sdk/client-s3";
+import { S3Repository } from "src/repository/s3";
+
+describe("S3Repository", () => {
+ it("returns string content from S3", async () => {
+ const send = jest.fn().mockResolvedValue({
+ Body: { transformToString: jest.fn().mockResolvedValue("content") },
+ });
+ const repository = new S3Repository("bucket", {
+ send,
+ } as unknown as S3Client);
+
+ const result = await repository.getObject("key.json");
+
+ expect(result).toBe("content");
+ expect(send.mock.calls[0][0]).toBeInstanceOf(GetObjectCommand);
+ });
+
+ it("throws when body is missing", async () => {
+ const send = jest.fn().mockResolvedValue({});
+ const repository = new S3Repository("bucket", {
+ send,
+ } as unknown as S3Client);
+
+ await expect(repository.getObject("key.json")).rejects.toThrow(
+ "Response body is missing",
+ );
+ });
+
+ it("returns undefined when object is missing", async () => {
+ const send = jest
+ .fn()
+ .mockRejectedValue(
+ new NoSuchKey({ message: "Not found", $metadata: {} }),
+ );
+ const repository = new S3Repository("bucket", {
+ send,
+ } as unknown as S3Client);
+
+ await expect(repository.getObject("key.json")).resolves.toBeUndefined();
+ });
+
+ it("rethrows non-NoSuchKey errors", async () => {
+ const send = jest.fn().mockRejectedValue(new Error("Denied"));
+ const repository = new S3Repository("bucket", {
+ send,
+ } as unknown as S3Client);
+
+ await expect(repository.getObject("key.json")).rejects.toThrow("Denied");
+ });
+
+ it("writes object to S3", async () => {
+ const send = jest.fn().mockResolvedValue({});
+ const repository = new S3Repository("bucket", {
+ send,
+ } as unknown as S3Client);
+
+ await repository.putRawData("payload", "key.json");
+
+ expect(send).toHaveBeenCalledTimes(1);
+ expect(send.mock.calls[0][0]).toBeInstanceOf(PutObjectCommand);
+ });
+});
diff --git a/tools/client-subscriptions-management/src/container.ts b/tools/client-subscriptions-management/src/container.ts
new file mode 100644
index 0000000..ddac500
--- /dev/null
+++ b/tools/client-subscriptions-management/src/container.ts
@@ -0,0 +1,35 @@
+import { S3Client } from "@aws-sdk/client-s3";
+import { fromIni } from "@aws-sdk/credential-providers";
+import { ClientSubscriptionRepository } from "src/repository/client-subscriptions";
+import { S3Repository } from "src/repository/s3";
+import { clientSubscriptionBuilder } from "src/domain/client-subscription-builder";
+
+type RepositoryOptions = {
+ bucketName: string;
+ region?: string;
+ profile?: string;
+};
+
+export const createS3Client = (
+ region?: string,
+ profile?: string,
+ env: NodeJS.ProcessEnv = process.env,
+): S3Client => {
+ const endpoint = env.AWS_ENDPOINT_URL;
+ const forcePathStyle = endpoint?.includes("localhost") ? true : undefined;
+ const credentials = profile ? fromIni({ profile }) : undefined;
+ return new S3Client({ region, endpoint, forcePathStyle, credentials });
+};
+
+export const createClientSubscriptionRepository = (
+ options: RepositoryOptions,
+): ClientSubscriptionRepository => {
+ const s3Repository = new S3Repository(
+ options.bucketName,
+ createS3Client(options.region, options.profile),
+ );
+ return new ClientSubscriptionRepository(
+ s3Repository,
+ clientSubscriptionBuilder,
+ );
+};
diff --git a/tools/client-subscriptions-management/src/domain/client-subscription-builder.ts b/tools/client-subscriptions-management/src/domain/client-subscription-builder.ts
new file mode 100644
index 0000000..11602f9
--- /dev/null
+++ b/tools/client-subscriptions-management/src/domain/client-subscription-builder.ts
@@ -0,0 +1,97 @@
+import { normalizeClientName } from "src/entrypoint/cli/helper";
+import type {
+ ChannelStatusSubscriptionArgs,
+ MessageStatusSubscriptionArgs,
+} from "src/repository/client-subscriptions";
+import type {
+ ChannelStatusSubscriptionConfiguration,
+ MessageStatusSubscriptionConfiguration,
+} from "@nhs-notify-client-callbacks/models";
+
+export type SubscriptionBuilder = {
+ messageStatus(
+ args: MessageStatusSubscriptionArgs,
+ ): MessageStatusSubscriptionConfiguration;
+ channelStatus(
+ args: ChannelStatusSubscriptionArgs,
+ ): ChannelStatusSubscriptionConfiguration;
+};
+
+export function buildMessageStatusSubscription(
+ args: MessageStatusSubscriptionArgs,
+): MessageStatusSubscriptionConfiguration {
+ const {
+ apiEndpoint,
+ apiKey,
+ apiKeyHeaderName = "x-api-key",
+ clientId,
+ clientName,
+ rateLimit,
+ statuses,
+ } = args;
+ const normalizedClientName = normalizeClientName(clientName);
+ const subscriptionId = normalizedClientName;
+ return {
+ SubscriptionId: subscriptionId,
+ SubscriptionType: "MessageStatus",
+ ClientId: clientId,
+ MessageStatuses: statuses,
+ Targets: [
+ {
+ Type: "API",
+ TargetId: crypto.randomUUID(),
+ InvocationEndpoint: apiEndpoint,
+ InvocationMethod: "POST",
+ InvocationRateLimit: rateLimit,
+ APIKey: {
+ HeaderName: apiKeyHeaderName,
+ HeaderValue: apiKey,
+ },
+ },
+ ],
+ };
+}
+
+export function buildChannelStatusSubscription(
+ args: ChannelStatusSubscriptionArgs,
+): ChannelStatusSubscriptionConfiguration {
+ const {
+ apiEndpoint,
+ apiKey,
+ apiKeyHeaderName = "x-api-key",
+ channelStatuses,
+ channelType,
+ clientId,
+ clientName,
+ rateLimit,
+ supplierStatuses,
+ } = args;
+ const normalizedClientName = normalizeClientName(clientName);
+ const subscriptionId = `${normalizedClientName}-${channelType}`;
+ return {
+ SubscriptionId: subscriptionId,
+ SubscriptionType: "ChannelStatus",
+ ClientId: clientId,
+ ChannelType: channelType,
+ ChannelStatuses: channelStatuses ?? [],
+ SupplierStatuses: supplierStatuses ?? [],
+ Targets: [
+ {
+ Type: "API",
+ TargetId: crypto.randomUUID(),
+ InvocationEndpoint: apiEndpoint,
+ InvocationMethod: "POST",
+ InvocationRateLimit: rateLimit,
+ APIKey: {
+ HeaderName: apiKeyHeaderName,
+ HeaderValue: apiKey,
+ },
+ },
+ ],
+ };
+}
+
+export const clientSubscriptionBuilder: SubscriptionBuilder = {
+ messageStatus: buildMessageStatusSubscription,
+ channelStatus: buildChannelStatusSubscription,
+};
diff --git a/tools/client-subscriptions-management/src/entrypoint/cli/deploy.ts b/tools/client-subscriptions-management/src/entrypoint/cli/deploy.ts
new file mode 100644
index 0000000..c1f4665
--- /dev/null
+++ b/tools/client-subscriptions-management/src/entrypoint/cli/deploy.ts
@@ -0,0 +1,305 @@
+import { spawnSync } from "node:child_process";
+import yargs from "yargs/yargs";
+import { hideBin } from "yargs/helpers";
+import {
+ CHANNEL_STATUSES,
+ CHANNEL_TYPES,
+ MESSAGE_STATUSES,
+ SUPPLIER_STATUSES,
+} from "@nhs-notify-client-callbacks/models";
+import { createClientSubscriptionRepository } from "src/container";
+import {
+ formatSubscriptionFileResponse,
+ resolveBucketName,
+ resolveProfile,
+ resolveRegion,
+} from "src/entrypoint/cli/helper";
+
+const sharedOptions = {
+ "bucket-name": {
+ type: "string" as const,
+ demandOption: false,
+ description: "Explicit S3 bucket name (overrides derived name)",
+ },
+ "client-name": {
+ type: "string" as const,
+ demandOption: false,
+ description: "Display name for the client (defaults to client-id)",
+ },
+ "client-id": {
+ type: "string" as const,
+ demandOption: true,
+ description: "Client identifier",
+ },
+ "api-endpoint": {
+ type: "string" as const,
+ demandOption: true,
+ description: "Webhook endpoint URL (must start with https://)",
+ },
+ "api-key": {
+ type: "string" as const,
+ demandOption: true,
+ description: "API key value for authenticating webhook calls",
+ },
+ "api-key-header-name": {
+ type: "string" as const,
+ default: "x-api-key",
+ demandOption: false,
+ description: "HTTP header name for the API key",
+ },
+ "rate-limit": {
+ type: "number" as const,
+ demandOption: true,
+ description: "Maximum number of webhook calls per second",
+ },
+ "dry-run": {
+ type: "boolean" as const,
+ demandOption: true,
+ description: "Validate config without writing to S3",
+ },
+ region: {
+ type: "string" as const,
+ demandOption: false,
+ description: "AWS region (defaults to AWS_REGION or eu-west-2)",
+ },
+ "terraform-apply": {
+ type: "boolean" as const,
+ default: false,
+ demandOption: false,
+ description: "Run terraform apply after uploading config",
+ },
+ environment: {
+ type: "string" as const,
+ demandOption: false,
+ description:
+ "Environment name, used to derive infrastructure resource names when not explicitly provided",
+ },
+ group: {
+ type: "string" as const,
+ demandOption: false,
+ description: "Group name (required when --terraform-apply is set)",
+ },
+ project: {
+ type: "string" as const,
+ default: "nhs",
+ demandOption: false,
+ description: "Project name prefix for derived resource names",
+ },
+ "tf-region": {
+ type: "string" as const,
+ demandOption: false,
+ description: "AWS region override for terraform",
+ },
+ profile: {
+ type: "string" as const,
+ demandOption: false,
+ description: "AWS profile to use (overrides AWS_PROFILE)",
+ },
+} as const;
+
+function runTerraformApply(argv: {
+ environment?: string;
+ group?: string;
+ project?: string;
+ "tf-region"?: string;
+}) {
+ const { environment, group, project = "nhs", "tf-region": tfRegion } = argv;
+ if (!environment || !group) {
+ console.error(
+ "Error: --environment and --group are required when --terraform-apply is set",
+ );
+ process.exitCode = 1;
+ return false;
+ }
+
+ console.log(
+ "[deploy-client-subscriptions] Running terraform apply for callbacks component...",
+ );
+
+ const makeArgs = [
+ "terraform-apply",
+ `component=callbacks`,
+ `environment=${environment}`,
+ `group=${group}`,
+ `project=${project}`,
+ ];
+ if (tfRegion) {
+ makeArgs.push(`region=${tfRegion}`);
+ }
+
+ // eslint-disable-next-line sonarjs/no-os-command-from-path
+ const result = spawnSync("make", makeArgs, { stdio: "inherit" });
+ if (result.status !== 0) {
+ console.error(
+ `Error: terraform apply failed with exit code ${result.status}`,
+ );
+ process.exitCode = result.status ?? 1;
+ return false;
+ }
+ return true;
+}
+
+export async function main(args: string[] = process.argv) {
+ await yargs(hideBin(args))
+ .command(
+ "message",
+ "Deploy a message status subscription",
+ {
+ ...sharedOptions,
+ "message-statuses": {
+ string: true,
+ type: "array" as const,
+ demandOption: true,
+ choices: MESSAGE_STATUSES,
+ },
+ },
+ async (argv) => {
+ const apiEndpoint = argv["api-endpoint"];
+ if (!/^https:\/\//.test(apiEndpoint)) {
+ console.error("Error: api-endpoint must start with https://");
+ process.exitCode = 1;
+ return;
+ }
+
+ console.log(
+ "[deploy-client-subscriptions] Uploading message status subscription config...",
+ );
+
+ const region = resolveRegion(argv.region);
+ const profile = resolveProfile(argv.profile);
+ const bucketName = await resolveBucketName(
+ argv["bucket-name"],
+ argv.environment,
+ region,
+ profile,
+ argv.project,
+ );
+ const clientSubscriptionRepository = createClientSubscriptionRepository(
+ {
+ bucketName,
+ region,
+ profile,
+ },
+ );
+
+ const result =
+ await clientSubscriptionRepository.putMessageStatusSubscription({
+ clientName: argv["client-name"] ?? argv["client-id"],
+ clientId: argv["client-id"],
+ apiEndpoint,
+ apiKeyHeaderName: argv["api-key-header-name"],
+ apiKey: argv["api-key"],
+ statuses: argv["message-statuses"],
+ rateLimit: argv["rate-limit"],
+ dryRun: argv["dry-run"],
+ });
+
+ console.log(formatSubscriptionFileResponse(result));
+
+ if (argv["terraform-apply"]) {
+ runTerraformApply(argv);
+ }
+ },
+ )
+ .command(
+ "channel",
+ "Deploy a channel status subscription",
+ {
+ ...sharedOptions,
+ "channel-type": {
+ type: "string" as const,
+ demandOption: true,
+ choices: CHANNEL_TYPES,
+ },
+ "channel-statuses": {
+ string: true,
+ type: "array" as const,
+ demandOption: false,
+ choices: CHANNEL_STATUSES,
+ },
+ "supplier-statuses": {
+ string: true,
+ type: "array" as const,
+ demandOption: false,
+ choices: SUPPLIER_STATUSES,
+ },
+ },
+ async (argv) => {
+ const apiEndpoint = argv["api-endpoint"];
+ if (!/^https:\/\//.test(apiEndpoint)) {
+ console.error("Error: api-endpoint must start with https://");
+ process.exitCode = 1;
+ return;
+ }
+
+ const channelStatuses = argv["channel-statuses"];
+ const supplierStatuses = argv["supplier-statuses"];
+ if (!channelStatuses?.length && !supplierStatuses?.length) {
+ console.error(
+ "Error: at least one of --channel-statuses or --supplier-statuses must be provided",
+ );
+ process.exitCode = 1;
+ return;
+ }
+
+ console.log(
+ "[deploy-client-subscriptions] Uploading channel status subscription config...",
+ );
+
+ const region = resolveRegion(argv.region);
+ const profile = resolveProfile(argv.profile);
+ const bucketName = await resolveBucketName(
+ argv["bucket-name"],
+ argv.environment,
+ region,
+ profile,
+ argv.project,
+ );
+ const clientSubscriptionRepository = createClientSubscriptionRepository(
+ {
+ bucketName,
+ region,
+ profile,
+ },
+ );
+
+ const result =
+ await clientSubscriptionRepository.putChannelStatusSubscription({
+ clientName: argv["client-name"] ?? argv["client-id"],
+ clientId: argv["client-id"],
+ apiEndpoint,
+ apiKeyHeaderName: argv["api-key-header-name"],
+ apiKey: argv["api-key"],
+ channelType: argv["channel-type"],
+ channelStatuses,
+ supplierStatuses,
+ rateLimit: argv["rate-limit"],
+ dryRun: argv["dry-run"],
+ });
+
+ console.log(formatSubscriptionFileResponse(result));
+
+ if (argv["terraform-apply"]) {
+ runTerraformApply(argv);
+ }
+ },
+ )
+ .demandCommand(1, "Please specify a command: message or channel")
+ .strict()
+ .parseAsync();
+}
+
+export const runCli = async (args: string[] = process.argv) => {
+ try {
+ await main(args);
+ } catch (error) {
+ console.error(error);
+ process.exitCode = 1;
+ }
+};
+
+(async () => {
+ if (require.main === module) {
+ await runCli();
+ }
+})();
diff --git a/tools/client-subscriptions-management/src/entrypoint/cli/get-client-subscriptions.ts b/tools/client-subscriptions-management/src/entrypoint/cli/get-client-subscriptions.ts
new file mode 100644
index 0000000..f9ce855
--- /dev/null
+++ b/tools/client-subscriptions-management/src/entrypoint/cli/get-client-subscriptions.ts
@@ -0,0 +1,90 @@
+import yargs from "yargs/yargs";
+import { hideBin } from "yargs/helpers";
+import { createClientSubscriptionRepository } from "src/container";
+import {
+ formatSubscriptionFileResponse,
+ resolveBucketName,
+ resolveProfile,
+ resolveRegion,
+} from "src/entrypoint/cli/helper";
+
+export const parseArgs = (args: string[]) =>
+ yargs(hideBin(args))
+ .options({
+ "bucket-name": {
+ type: "string",
+ demandOption: false,
+ description: "Explicit S3 bucket name (overrides derived name)",
+ },
+ environment: {
+ type: "string",
+ demandOption: false,
+ description:
+ "Environment name, used to derive infrastructure resource names when not explicitly provided",
+ },
+ "client-id": {
+ type: "string",
+ demandOption: true,
+ description: "Client identifier",
+ },
+ region: {
+ type: "string",
+ demandOption: false,
+ description: "AWS region (defaults to AWS_REGION or eu-west-2)",
+ },
+ profile: {
+ type: "string",
+ demandOption: false,
+ description: "AWS profile to use (overrides AWS_PROFILE)",
+ },
+ })
+ .parseSync();
+
+export async function main(args: string[] = process.argv) {
+ const argv = parseArgs(args);
+ const region = resolveRegion(argv.region);
+ const profile = resolveProfile(argv.profile);
+ const bucketName = await resolveBucketName(
+ argv["bucket-name"],
+ argv.environment,
+ region,
+ profile,
+ );
+ const clientSubscriptionRepository = createClientSubscriptionRepository({
+ bucketName,
+ region,
+ profile,
+ });
+
+ const result = await clientSubscriptionRepository.getClientSubscriptions(
+ argv["client-id"],
+ );
+
+ if (result) {
+ console.log(formatSubscriptionFileResponse(result));
+ } else {
+ console.log(`No configuration exists for client: ${argv["client-id"]}`);
+ }
+}
+
+export const runCli = async (args: string[] = process.argv) => {
+ try {
+ await main(args);
+ } catch (error) {
+ console.error(error);
+ process.exitCode = 1;
+ }
+};
+
+export const runIfMain = async (
+ args: string[] = process.argv,
+ isMain: boolean = require.main === module,
+) => {
+ if (isMain) {
+ await runCli(args);
+ }
+};
+
+(async () => {
+ await runIfMain();
+})();
diff --git a/tools/client-subscriptions-management/src/entrypoint/cli/helper.ts b/tools/client-subscriptions-management/src/entrypoint/cli/helper.ts
new file mode 100644
index 0000000..d060417
--- /dev/null
+++ b/tools/client-subscriptions-management/src/entrypoint/cli/helper.ts
@@ -0,0 +1,103 @@
+import { GetCallerIdentityCommand, STSClient } from "@aws-sdk/client-sts";
+import { fromIni } from "@aws-sdk/credential-providers";
+import { table } from "table";
+import type { ClientSubscriptionConfiguration } from "@nhs-notify-client-callbacks/models";
+
+const SUBSCRIPTION_TABLE_HEADER = [
+ "Client ID",
+ "Subscription Type",
+ "Statuses",
+ "Target ID",
+ "Endpoint",
+ "Method",
+ "Rate Limit",
+ "API Key Header",
+ "API Key Value",
+];
+
+const subscriptionStatuses = (
+ subscription: ClientSubscriptionConfiguration[number],
+): string => {
+ if (subscription.SubscriptionType === "MessageStatus") {
+ return subscription.MessageStatuses.join(", ");
+ }
+ const statuses = [
+ ...subscription.ChannelStatuses,
+ ...subscription.SupplierStatuses,
+ ];
+ return `${subscription.ChannelType}: ${statuses.join(", ")}`;
+};
+
+export const formatSubscriptionFileResponse = (
+ subscriptions: ClientSubscriptionConfiguration,
+): string => {
+ const rows = subscriptions.flatMap((subscription) =>
+ subscription.Targets.map((target) => [
+ subscription.ClientId,
+ subscription.SubscriptionType,
+ subscriptionStatuses(subscription),
+ target.TargetId,
+ target.InvocationEndpoint,
+ target.InvocationMethod,
+ String(target.InvocationRateLimit),
+ target.APIKey.HeaderName,
+ target.APIKey.HeaderValue,
+ ]),
+ );
+ return table([SUBSCRIPTION_TABLE_HEADER, ...rows]);
+};
+
+export const normalizeClientName = (name: string): string =>
+ name.replaceAll(/\s+/g, "-").toLowerCase();
+
+export const resolveProfile = (
+ profileArg?: string,
+ env: NodeJS.ProcessEnv = process.env,
+): string | undefined => profileArg ?? env.AWS_PROFILE;
+
+export const resolveAccountId = async (
+ profile?: string,
+ region?: string,
+): Promise => {
+ const credentials = profile ? fromIni({ profile }) : undefined;
+ const client = new STSClient({ region, credentials });
+ const { Account } = await client.send(new GetCallerIdentityCommand({}));
+ if (!Account) {
+ throw new Error("Unable to determine AWS account ID from STS");
+ }
+ return Account;
+};
+
+export const deriveBucketName = (
+ accountId: string,
+ environment: string,
+ region: string,
+ project = "nhs",
+ component = "callbacks",
+): string =>
+ `${project}-${accountId}-${region}-${environment}-${component}-subscription-config`;
+
+export const resolveBucketName = async (
+ bucketArg?: string,
+ environment?: string,
+ region?: string,
+ profile?: string,
+ project?: string,
+): Promise => {
+ if (bucketArg) {
+ return bucketArg;
+ }
+ if (!environment) {
+ throw new Error(
+ "Bucket name is required: use --bucket-name to specify directly, or --environment (with --region and optionally --profile) to determine this automatically",
+ );
+ }
+ const resolvedRegion = region ?? "eu-west-2";
+ const accountId = await resolveAccountId(profile, resolvedRegion);
+ return deriveBucketName(accountId, environment, resolvedRegion, project);
+};
+
+export const resolveRegion = (
+ regionArg?: string,
+ env: NodeJS.ProcessEnv = process.env,
+): string | undefined => regionArg ?? env.AWS_REGION ?? env.AWS_DEFAULT_REGION;
diff --git a/tools/client-subscriptions-management/src/entrypoint/cli/put-channel-status.ts b/tools/client-subscriptions-management/src/entrypoint/cli/put-channel-status.ts
new file mode 100644
index 0000000..4097dd9
--- /dev/null
+++ b/tools/client-subscriptions-management/src/entrypoint/cli/put-channel-status.ts
@@ -0,0 +1,169 @@
+import yargs from "yargs/yargs";
+import { hideBin } from "yargs/helpers";
+import {
+ CHANNEL_STATUSES,
+ CHANNEL_TYPES,
+ SUPPLIER_STATUSES,
+} from "@nhs-notify-client-callbacks/models";
+import { createClientSubscriptionRepository } from "src/container";
+import {
+ formatSubscriptionFileResponse,
+ resolveBucketName,
+ resolveProfile,
+ resolveRegion,
+} from "src/entrypoint/cli/helper";
+
+export const parseArgs = (args: string[]) =>
+ yargs(hideBin(args))
+ .options({
+ "bucket-name": {
+ type: "string",
+ demandOption: false,
+ description: "Explicit S3 bucket name (overrides derived name)",
+ },
+ environment: {
+ type: "string",
+ demandOption: false,
+ description:
+ "Environment name, used to derive infrastructure resource names when not explicitly provided",
+ },
+ "client-name": {
+ type: "string",
+ demandOption: false,
+ description: "Display name for the client (defaults to client-id)",
+ },
+ "client-id": {
+ type: "string",
+ demandOption: true,
+ description: "Client identifier",
+ },
+ "api-endpoint": {
+ type: "string",
+ demandOption: true,
+ description: "Webhook endpoint URL (must start with https://)",
+ },
+ "api-key-header-name": {
+ type: "string",
+ default: "x-api-key",
+ demandOption: false,
+ description: "HTTP header name for the API key",
+ },
+ "api-key": {
+ type: "string",
+ demandOption: true,
+ description: "API key value for authenticating webhook calls",
+ },
+ "channel-statuses": {
+ string: true,
+ type: "array",
+ demandOption: false,
+ choices: CHANNEL_STATUSES,
+ description: "Channel statuses to subscribe to",
+ },
+ "supplier-statuses": {
+ string: true,
+ type: "array",
+ demandOption: false,
+ choices: SUPPLIER_STATUSES,
+ description: "Supplier statuses to subscribe to",
+ },
+ "channel-type": {
+ type: "string",
+ demandOption: true,
+ choices: CHANNEL_TYPES,
+ description: "Channel type",
+ },
+ "rate-limit": {
+ type: "number",
+ demandOption: true,
+ description: "Maximum number of webhook calls per second",
+ },
+ "dry-run": {
+ type: "boolean",
+ demandOption: true,
+ description: "Validate config without writing to S3",
+ },
+ region: {
+ type: "string",
+ demandOption: false,
+ description: "AWS region (defaults to AWS_REGION or eu-west-2)",
+ },
+ profile: {
+ type: "string",
+ demandOption: false,
+ description: "AWS profile to use (overrides AWS_PROFILE)",
+ },
+ })
+ .parseSync();
+
+export async function main(args: string[] = process.argv) {
+ const argv = parseArgs(args);
+ const apiEndpoint = argv["api-endpoint"];
+ if (!/^https:\/\//.test(apiEndpoint)) {
+ console.error("Error: api-endpoint must start with https://");
+ process.exitCode = 1;
+ return;
+ }
+
+ const channelStatuses = argv["channel-statuses"];
+ const supplierStatuses = argv["supplier-statuses"];
+ if (!channelStatuses?.length && !supplierStatuses?.length) {
+ console.error(
+ "Error: at least one of --channel-statuses or --supplier-statuses must be provided",
+ );
+ process.exitCode = 1;
+ return;
+ }
+
+ const region = resolveRegion(argv.region);
+ const profile = resolveProfile(argv.profile);
+ const bucketName = await resolveBucketName(
+ argv["bucket-name"],
+ argv.environment,
+ region,
+ profile,
+ );
+ const clientSubscriptionRepository = createClientSubscriptionRepository({
+ bucketName,
+ region,
+ profile,
+ });
+
+ const result =
+ await clientSubscriptionRepository.putChannelStatusSubscription({
+ clientName: argv["client-name"] ?? argv["client-id"],
+ clientId: argv["client-id"],
+ apiEndpoint,
+ apiKeyHeaderName: argv["api-key-header-name"],
+ apiKey: argv["api-key"],
+ channelType: argv["channel-type"],
+ channelStatuses,
+ supplierStatuses,
+ rateLimit: argv["rate-limit"],
+ dryRun: argv["dry-run"],
+ });
+
+ console.log(formatSubscriptionFileResponse(result));
+}
+
+export const runCli = async (args: string[] = process.argv) => {
+ try {
+ await main(args);
+ } catch (error) {
+ console.error(error);
+ process.exitCode = 1;
+ }
+};
+
+export const runIfMain = async (
+ args: string[] = process.argv,
+ isMain: boolean = require.main === module,
+) => {
+ if (isMain) {
+ await runCli(args);
+ }
+};
+
+(async () => {
+ await runIfMain();
+})();
diff --git a/tools/client-subscriptions-management/src/entrypoint/cli/put-message-status.ts b/tools/client-subscriptions-management/src/entrypoint/cli/put-message-status.ts
new file mode 100644
index 0000000..8dcdb35
--- /dev/null
+++ b/tools/client-subscriptions-management/src/entrypoint/cli/put-message-status.ts
@@ -0,0 +1,140 @@
+import yargs from "yargs/yargs";
+import { hideBin } from "yargs/helpers";
+import { MESSAGE_STATUSES } from "@nhs-notify-client-callbacks/models";
+import { createClientSubscriptionRepository } from "src/container";
+import {
+ formatSubscriptionFileResponse,
+ resolveBucketName,
+ resolveProfile,
+ resolveRegion,
+} from "src/entrypoint/cli/helper";
+
+export const parseArgs = (args: string[]) =>
+ yargs(hideBin(args))
+ .options({
+ "bucket-name": {
+ type: "string",
+ demandOption: false,
+ description: "Explicit S3 bucket name (overrides derived name)",
+ },
+ environment: {
+ type: "string",
+ demandOption: false,
+ description:
+ "Environment name, used to derive infrastructure resource names when not explicitly provided",
+ },
+ "client-name": {
+ type: "string",
+ demandOption: false,
+ description: "Display name for the client (defaults to client-id)",
+ },
+ "client-id": {
+ type: "string",
+ demandOption: true,
+ description: "Client identifier",
+ },
+ "api-endpoint": {
+ type: "string",
+ demandOption: true,
+ description: "Webhook endpoint URL (must start with https://)",
+ },
+ "api-key": {
+ type: "string",
+ demandOption: true,
+ description: "API key value for authenticating webhook calls",
+ },
+ "api-key-header-name": {
+ type: "string",
+ default: "x-api-key",
+ demandOption: false,
+ description: "HTTP header name for the API key",
+ },
+ "message-statuses": {
+ string: true,
+ type: "array",
+ demandOption: true,
+ choices: MESSAGE_STATUSES,
+ description: "Message statuses to subscribe to",
+ },
+ "rate-limit": {
+ type: "number",
+ demandOption: true,
+ description: "Maximum number of webhook calls per second",
+ },
+ "dry-run": {
+ type: "boolean",
+ demandOption: true,
+ description: "Validate config without writing to S3",
+ },
+ region: {
+ type: "string",
+ demandOption: false,
+ description: "AWS region (defaults to AWS_REGION or eu-west-2)",
+ },
+ profile: {
+ type: "string",
+ demandOption: false,
+ description: "AWS profile to use (overrides AWS_PROFILE)",
+ },
+ })
+ .parseSync();
+
+export async function main(args: string[] = process.argv) {
+ const argv = parseArgs(args);
+ const apiEndpoint = argv["api-endpoint"];
+ if (!/^https:\/\//.test(apiEndpoint)) {
+ console.error("Error: api-endpoint must start with https://");
+ process.exitCode = 1;
+ return;
+ }
+
+ const region = resolveRegion(argv.region);
+ const profile = resolveProfile(argv.profile);
+ const bucketName = await resolveBucketName(
+ argv["bucket-name"],
+ argv.environment,
+ region,
+ profile,
+ );
+ const clientSubscriptionRepository = createClientSubscriptionRepository({
+ bucketName,
+ region,
+ profile,
+ });
+
+ const result =
+ await clientSubscriptionRepository.putMessageStatusSubscription({
+ clientName: argv["client-name"] ?? argv["client-id"],
+ clientId: argv["client-id"],
+ apiEndpoint,
+ apiKeyHeaderName: argv["api-key-header-name"],
+ apiKey: argv["api-key"],
+ statuses: argv["message-statuses"],
+ rateLimit: argv["rate-limit"],
+ dryRun: argv["dry-run"],
+ });
+
+ console.log(formatSubscriptionFileResponse(result));
+}
+
+export const runCli = async (args: string[] = process.argv) => {
+ try {
+ await main(args);
+ } catch (error) {
+ console.error(error);
+ process.exitCode = 1;
+ }
+};
+
+export const runIfMain = async (
+ args: string[] = process.argv,
+ isMain: boolean = require.main === module,
+) => {
+ if (isMain) {
+ await runCli(args);
+ }
+};
+
+(async () => {
+ await runIfMain();
+})();
diff --git a/tools/client-subscriptions-management/src/index.ts b/tools/client-subscriptions-management/src/index.ts
new file mode 100644
index 0000000..bec05b8
--- /dev/null
+++ b/tools/client-subscriptions-management/src/index.ts
@@ -0,0 +1,2 @@
+// eslint-disable-next-line import-x/prefer-default-export
+export { createClientSubscriptionRepository } from "src/container";
diff --git a/tools/client-subscriptions-management/src/repository/client-subscriptions.ts b/tools/client-subscriptions-management/src/repository/client-subscriptions.ts
new file mode 100644
index 0000000..48c5629
--- /dev/null
+++ b/tools/client-subscriptions-management/src/repository/client-subscriptions.ts
@@ -0,0 +1,165 @@
+import { z } from "zod";
+import {
+ CHANNEL_STATUSES,
+ CHANNEL_TYPES,
+ type Channel,
+ type ChannelStatus,
+ type ClientSubscriptionConfiguration,
+ MESSAGE_STATUSES,
+ type MessageStatus,
+ SUPPLIER_STATUSES,
+ type SupplierStatus,
+} from "@nhs-notify-client-callbacks/models";
+import type { SubscriptionBuilder } from "src/domain/client-subscription-builder";
+import { S3Repository } from "src/repository/s3";
+
+export type MessageStatusSubscriptionArgs = {
+ clientName: string;
+ clientId: string;
+ apiKey: string;
+ apiEndpoint: string;
+ statuses: MessageStatus[];
+ rateLimit: number;
+ dryRun: boolean;
+ apiKeyHeaderName?: string;
+};
+
+const messageStatusSubscriptionArgsSchema = z.object({
+ clientName: z.string(),
+ clientId: z.string(),
+ apiKey: z.string(),
+ apiEndpoint: z.string(),
+ statuses: z.array(z.enum(MESSAGE_STATUSES)),
+ rateLimit: z.number(),
+ dryRun: z.boolean(),
+ apiKeyHeaderName: z.string().optional().default("x-api-key"),
+});
+
+export type ChannelStatusSubscriptionArgs = {
+ clientName: string;
+ clientId: string;
+ apiKey: string;
+ apiEndpoint: string;
+ channelStatuses?: ChannelStatus[];
+ supplierStatuses?: SupplierStatus[];
+ channelType: Channel;
+ rateLimit: number;
+ dryRun: boolean;
+ apiKeyHeaderName?: string;
+};
+
+const channelStatusSubscriptionArgsSchema = z.object({
+ clientName: z.string(),
+ clientId: z.string(),
+ apiKey: z.string(),
+ apiEndpoint: z.string(),
+ channelStatuses: z.array(z.enum(CHANNEL_STATUSES)).min(1).optional(),
+ supplierStatuses: z.array(z.enum(SUPPLIER_STATUSES)).min(1).optional(),
+ channelType: z.enum(CHANNEL_TYPES),
+ rateLimit: z.number(),
+ dryRun: z.boolean(),
+ apiKeyHeaderName: z.string().optional().default("x-api-key"),
+});
+
+export class ClientSubscriptionRepository {
+ constructor(
+ private readonly s3Repository: S3Repository,
+ private readonly configurationBuilder: SubscriptionBuilder,
+ ) {}
+
+ async getClientSubscriptions(
+ clientId: string,
+ ): Promise {
+ const rawFile = await this.s3Repository.getObject(
+ `client_subscriptions/${clientId}.json`,
+ );
+
+ if (rawFile !== undefined) {
+ return JSON.parse(rawFile) as unknown as ClientSubscriptionConfiguration;
+ }
+ return undefined;
+ }
+
+ async putMessageStatusSubscription(
+ subscriptionArgs: MessageStatusSubscriptionArgs,
+ ) {
+ const parsedSubscriptionArgs =
+ messageStatusSubscriptionArgsSchema.parse(subscriptionArgs);
+
+ const { clientId } = parsedSubscriptionArgs;
+ const subscriptions = (await this.getClientSubscriptions(clientId)) ?? [];
+
+ const indexOfMessageStatusSubscription = subscriptions.findIndex(
+ (subscription) => subscription.SubscriptionType === "MessageStatus",
+ );
+
+ if (indexOfMessageStatusSubscription !== -1) {
+ subscriptions.splice(indexOfMessageStatusSubscription, 1);
+ }
+
+ const messageStatusConfig = this.configurationBuilder.messageStatus(
+ parsedSubscriptionArgs,
+ );
+
+ const newConfigFile: ClientSubscriptionConfiguration = [
+ ...subscriptions,
+ messageStatusConfig,
+ ];
+
+ if (!parsedSubscriptionArgs.dryRun) {
+ await this.s3Repository.putRawData(
+ JSON.stringify(newConfigFile),
+ `client_subscriptions/${clientId}.json`,
+ );
+ }
+
+ return newConfigFile;
+ }
+
+ async putChannelStatusSubscription(
+ subscriptionArgs: ChannelStatusSubscriptionArgs,
+ ): Promise {
+ const parsedSubscriptionArgs =
+ channelStatusSubscriptionArgsSchema.parse(subscriptionArgs);
+
+ if (
+ !parsedSubscriptionArgs.channelStatuses?.length &&
+ !parsedSubscriptionArgs.supplierStatuses?.length
+ ) {
+ throw new Error(
+ "Validation failed: at least one of channelStatuses or supplierStatuses must be provided",
+ );
+ }
+
+ const { clientId } = parsedSubscriptionArgs;
+ const subscriptions = (await this.getClientSubscriptions(clientId)) ?? [];
+
+ const indexOfChannelStatusSubscription = subscriptions.findIndex(
+ (subscription) =>
+ subscription.SubscriptionType === "ChannelStatus" &&
+ subscription.ChannelType === parsedSubscriptionArgs.channelType,
+ );
+
+ if (indexOfChannelStatusSubscription !== -1) {
+ subscriptions.splice(indexOfChannelStatusSubscription, 1);
+ }
+
+ const channelStatusConfig = this.configurationBuilder.channelStatus(
+ parsedSubscriptionArgs,
+ );
+
+ const newConfigFile: ClientSubscriptionConfiguration = [
+ ...subscriptions,
+ channelStatusConfig,
+ ];
+
+ if (!parsedSubscriptionArgs.dryRun) {
+ await this.s3Repository.putRawData(
+ JSON.stringify(newConfigFile),
+ `client_subscriptions/${clientId}.json`,
+ );
+ }
+
+ return newConfigFile;
+ }
+}
diff --git a/tools/client-subscriptions-management/src/repository/s3.ts b/tools/client-subscriptions-management/src/repository/s3.ts
new file mode 100644
index 0000000..a306298
--- /dev/null
+++ b/tools/client-subscriptions-management/src/repository/s3.ts
@@ -0,0 +1,49 @@
+import {
+ GetObjectCommand,
+ NoSuchKey,
+ PutObjectCommand,
+ PutObjectCommandInput,
+ S3Client,
+} from "@aws-sdk/client-s3";
+
+// eslint-disable-next-line import-x/prefer-default-export
+export class S3Repository {
+ constructor(
+ private readonly bucketName: string,
+ private readonly s3Client: S3Client,
+ ) {}
+
+ async getObject(key: string): Promise {
+ const params = {
+ Bucket: this.bucketName,
+ Key: key,
+ };
+ try {
+ const { Body } = await this.s3Client.send(new GetObjectCommand(params));
+
+ if (!Body) {
+ throw new Error("Response body is missing");
+ }
+
+ return await Body.transformToString();
+ } catch (error) {
+ if (error instanceof NoSuchKey) {
+ return undefined;
+ }
+ throw error;
+ }
+ }
+
+ async putRawData(
+ fileData: PutObjectCommandInput["Body"],
+ key: string,
+ ): Promise {
+ const params = {
+ Bucket: this.bucketName,
+ Key: key,
+ Body: fileData,
+ };
+
+ await this.s3Client.send(new PutObjectCommand(params));
+ }
+}
diff --git a/tools/client-subscriptions-management/tsconfig.json b/tools/client-subscriptions-management/tsconfig.json
new file mode 100644
index 0000000..5b308bc
--- /dev/null
+++ b/tools/client-subscriptions-management/tsconfig.json
@@ -0,0 +1,17 @@
+{
+ "compilerOptions": {
+ "baseUrl": ".",
+ "outDir": "dist",
+ "paths": {
+ "src/*": [
+ "src/*"
+ ]
+ },
+ "rootDir": "."
+ },
+ "extends": "../../tsconfig.base.json",
+ "include": [
+ "src/**/*",
+ "jest.config.ts"
+ ]
+}
diff --git a/tsconfig.json b/tsconfig.json
new file mode 100644
index 0000000..42cdff9
--- /dev/null
+++ b/tsconfig.json
@@ -0,0 +1,14 @@
+{
+ "$schema": "https://json.schemastore.org/tsconfig",
+ "compilerOptions": {
+ "noEmit": true
+ },
+ "extends": "./tsconfig.base.json",
+ "include": [
+ "lambdas/*/src/**/*",
+ "scripts/*/src/**/*",
+ "tools/*/src/**/*",
+ "src/**/*",
+ "tests/**/*"
+ ]
+}