From 8d7ba10ba8d310a736a38d2b76b7986db4ff2d89 Mon Sep 17 00:00:00 2001 From: Landon Shumway Date: Wed, 24 Sep 2025 16:32:30 -0500 Subject: [PATCH 01/55] Set up skeleton for feature flag directory --- .../feature-flag/feature_flag_client.py | 41 +++++++++++ .../python/feature-flag/handlers/__init__.py | 0 .../handlers/check_feature_flag.py | 11 +++ .../python/feature-flag/requirements-dev.in | 1 + .../python/feature-flag/requirements-dev.txt | 70 +++++++++++++++++++ .../python/feature-flag/requirements.in | 3 + .../python/feature-flag/requirements.txt | 6 ++ .../python/feature-flag/tests/__init__.py | 19 +++++ .../feature-flag/tests/function/__init__.py | 49 +++++++++++++ .../tests/function/test_check_feature_flag.py | 24 +++++++ 10 files changed, 224 insertions(+) create mode 100644 backend/compact-connect/lambdas/python/feature-flag/feature_flag_client.py create mode 100644 backend/compact-connect/lambdas/python/feature-flag/handlers/__init__.py create mode 100644 backend/compact-connect/lambdas/python/feature-flag/handlers/check_feature_flag.py create mode 100644 backend/compact-connect/lambdas/python/feature-flag/requirements-dev.in create mode 100644 backend/compact-connect/lambdas/python/feature-flag/requirements-dev.txt create mode 100644 backend/compact-connect/lambdas/python/feature-flag/requirements.in create mode 100644 backend/compact-connect/lambdas/python/feature-flag/requirements.txt create mode 100644 backend/compact-connect/lambdas/python/feature-flag/tests/__init__.py create mode 100644 backend/compact-connect/lambdas/python/feature-flag/tests/function/__init__.py create mode 100644 backend/compact-connect/lambdas/python/feature-flag/tests/function/test_check_feature_flag.py diff --git a/backend/compact-connect/lambdas/python/feature-flag/feature_flag_client.py b/backend/compact-connect/lambdas/python/feature-flag/feature_flag_client.py new file mode 100644 index 000000000..0b9ca9707 --- /dev/null +++ b/backend/compact-connect/lambdas/python/feature-flag/feature_flag_client.py @@ -0,0 +1,41 @@ +from abc import ABC, abstractmethod +from typing import Any, Dict, Optional +from dataclasses import dataclass + + +@dataclass +class FeatureFlagContext: + """Context information for feature flag evaluation""" + user_id: Optional[str] = None + environment: str = 'prod' + custom_attributes: Optional[Dict[str, Any]] = None + + +@dataclass +class FeatureFlagResult: + """Result of a feature flag check""" + enabled: bool + flag_name: str + metadata: Optional[Dict[str, Any]] = None + + +class FeatureFlagClient(ABC): + """ + Abstract base class for feature flag clients. + + This interface provides a consistent way to interact with different + feature flag providers (StatSig, LaunchDarkly, etc.) while hiding + the underlying implementation details. + """ + + @abstractmethod + def check_flag(self, flag_name: str, context: FeatureFlagContext) -> FeatureFlagResult: + """ + Check if a feature flag is enabled for the given context. + + :param flag_name: Name of the feature flag to check + :param context: Context for flag evaluation (environment, user, etc.) + :return: FeatureFlagResult indicating if flag is enabled + :raises FeatureFlagException: If flag check fails + """ + pass \ No newline at end of file diff --git a/backend/compact-connect/lambdas/python/feature-flag/handlers/__init__.py b/backend/compact-connect/lambdas/python/feature-flag/handlers/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/backend/compact-connect/lambdas/python/feature-flag/handlers/check_feature_flag.py b/backend/compact-connect/lambdas/python/feature-flag/handlers/check_feature_flag.py new file mode 100644 index 000000000..38f3aa51c --- /dev/null +++ b/backend/compact-connect/lambdas/python/feature-flag/handlers/check_feature_flag.py @@ -0,0 +1,11 @@ + +from aws_lambda_powertools.utilities.typing import LambdaContext +from cc_common.config import logger +from cc_common.utils import api_handler + +# TODO - initialize feature flag client here outside of the handler + +@api_handler +def check_feature_flag(event: dict, context: LambdaContext): # noqa: ARG001 unused-argument + # TODO - validate body and add implementation of client to check flag based on provided values + pass diff --git a/backend/compact-connect/lambdas/python/feature-flag/requirements-dev.in b/backend/compact-connect/lambdas/python/feature-flag/requirements-dev.in new file mode 100644 index 000000000..e0c3124af --- /dev/null +++ b/backend/compact-connect/lambdas/python/feature-flag/requirements-dev.in @@ -0,0 +1 @@ +moto[dynamodb]>=5.0.12, <6 diff --git a/backend/compact-connect/lambdas/python/feature-flag/requirements-dev.txt b/backend/compact-connect/lambdas/python/feature-flag/requirements-dev.txt new file mode 100644 index 000000000..1c844d20c --- /dev/null +++ b/backend/compact-connect/lambdas/python/feature-flag/requirements-dev.txt @@ -0,0 +1,70 @@ +# +# This file is autogenerated by pip-compile with Python 3.12 +# by the following command: +# +# pip-compile --no-emit-index-url compact-connect/lambdas/python/disaster-recovery/requirements-dev.in +# +boto3==1.40.19 + # via moto +botocore==1.40.19 + # via + # boto3 + # moto + # s3transfer +certifi==2025.8.3 + # via requests +cffi==1.17.1 + # via cryptography +charset-normalizer==3.4.3 + # via requests +cryptography==45.0.6 + # via moto +docker==7.1.0 + # via moto +idna==3.10 + # via requests +jinja2==3.1.6 + # via moto +jmespath==1.0.1 + # via + # boto3 + # botocore +markupsafe==3.0.2 + # via + # jinja2 + # werkzeug +moto[dynamodb,s3]==5.1.11 + # via -r compact-connect/lambdas/python/disaster-recovery/requirements-dev.in +py-partiql-parser==0.6.1 + # via moto +pycparser==2.22 + # via cffi +python-dateutil==2.9.0.post0 + # via + # botocore + # moto +pyyaml==6.0.2 + # via + # moto + # responses +requests==2.32.5 + # via + # docker + # moto + # responses +responses==0.25.8 + # via moto +s3transfer==0.13.1 + # via boto3 +six==1.17.0 + # via python-dateutil +urllib3==2.5.0 + # via + # botocore + # docker + # requests + # responses +werkzeug==3.1.3 + # via moto +xmltodict==0.14.2 + # via moto diff --git a/backend/compact-connect/lambdas/python/feature-flag/requirements.in b/backend/compact-connect/lambdas/python/feature-flag/requirements.in new file mode 100644 index 000000000..58d329b47 --- /dev/null +++ b/backend/compact-connect/lambdas/python/feature-flag/requirements.in @@ -0,0 +1,3 @@ +# common requirements are managed in the common requirements.in file +statsig==1.45.0 +requests==2.31.0 diff --git a/backend/compact-connect/lambdas/python/feature-flag/requirements.txt b/backend/compact-connect/lambdas/python/feature-flag/requirements.txt new file mode 100644 index 000000000..24e02f5ca --- /dev/null +++ b/backend/compact-connect/lambdas/python/feature-flag/requirements.txt @@ -0,0 +1,6 @@ +# +# This file is autogenerated by pip-compile with Python 3.12 +# by the following command: +# +# pip-compile --no-emit-index-url compact-connect/lambdas/python/disaster-recovery/requirements.in +# diff --git a/backend/compact-connect/lambdas/python/feature-flag/tests/__init__.py b/backend/compact-connect/lambdas/python/feature-flag/tests/__init__.py new file mode 100644 index 000000000..5462ba841 --- /dev/null +++ b/backend/compact-connect/lambdas/python/feature-flag/tests/__init__.py @@ -0,0 +1,19 @@ +import os +from unittest import TestCase +from unittest.mock import MagicMock + +from aws_lambda_powertools.utilities.typing import LambdaContext + + +class TstLambdas(TestCase): + @classmethod + def setUpClass(cls): + os.environ.update( + { + # Set to 'true' to enable debug logging + 'DEBUG': 'true', + 'ALLOWED_ORIGINS': '["https://example.org"]', + 'AWS_DEFAULT_REGION': 'us-east-1', + }, + ) + cls.mock_context = MagicMock(name='MockLambdaContext', spec=LambdaContext) diff --git a/backend/compact-connect/lambdas/python/feature-flag/tests/function/__init__.py b/backend/compact-connect/lambdas/python/feature-flag/tests/function/__init__.py new file mode 100644 index 000000000..4d0c0808d --- /dev/null +++ b/backend/compact-connect/lambdas/python/feature-flag/tests/function/__init__.py @@ -0,0 +1,49 @@ +import logging +import os + +import boto3 +from moto import mock_aws + +from tests import TstLambdas + +logger = logging.getLogger(__name__) +logging.basicConfig() +logger.setLevel(logging.DEBUG if os.environ.get('DEBUG', 'false') == 'true' else logging.INFO) + + +@mock_aws +class TstFunction(TstLambdas): + """Base class to set up Moto mocking and create mock AWS resources for functional testing""" + + def setUp(self): # noqa: N801 invalid-name + super().setUp() + self.mock_destination_table_name = 'Test-PersistentStack-ProviderTableEC5D0597-TQ2RIO6VVBRE' + self.mock_destination_table_arn = ( + f'arn:aws:dynamodb:us-east-1:767398110685:table/{self.mock_destination_table_name}' + ) + self.mock_source_table_name = 'Recovered-ProviderTableEC5D0597-TQ2RIO6VVBRE' + self.mock_source_table_arn = f'arn:aws:dynamodb:us-east-1:767398110685:table/{self.mock_source_table_name}' + self.build_resources() + + self.addCleanup(self.delete_resources) + + def build_resources(self): + # in the case of DR, the lambda sync solution should be table agnostic, since we are performing the same + # cleanup and restoration process regardless of the table that is being recovered + self.mock_source_table = self.create_mock_table(table_name=self.mock_source_table_name) + self.mock_destination_table = self.create_mock_table(table_name=self.mock_destination_table_name) + + def create_mock_table(self, table_name: str): + return boto3.resource('dynamodb').create_table( + AttributeDefinitions=[ + {'AttributeName': 'pk', 'AttributeType': 'S'}, + {'AttributeName': 'sk', 'AttributeType': 'S'}, + ], + TableName=table_name, + KeySchema=[{'AttributeName': 'pk', 'KeyType': 'HASH'}, {'AttributeName': 'sk', 'KeyType': 'RANGE'}], + BillingMode='PAY_PER_REQUEST', + ) + + def delete_resources(self): + self.mock_source_table.delete() + self.mock_destination_table.delete() diff --git a/backend/compact-connect/lambdas/python/feature-flag/tests/function/test_check_feature_flag.py b/backend/compact-connect/lambdas/python/feature-flag/tests/function/test_check_feature_flag.py new file mode 100644 index 000000000..6209e100d --- /dev/null +++ b/backend/compact-connect/lambdas/python/feature-flag/tests/function/test_check_feature_flag.py @@ -0,0 +1,24 @@ +from unittest.mock import patch + +from moto import mock_aws + +from . import TstFunction + + +@mock_aws +class TestCheckFeatureFlag(TstFunction): + """Test suite for feature flag endpoint.""" + + def _generate_test_event(self) -> dict: + return { + 'destinationTableArn': self.mock_destination_table_arn, + 'sourceTableArn': self.mock_source_table_arn, + 'tableNameRecoveryConfirmation': self.mock_destination_table_name, + } + + def test_lambda_returns_expected_response_body(self): + from handlers.check_feature_flag import check_feature_flag + + # TODO - mock feature flag client and call handler to check if it returns expected response + pass + From cba792ec95e28e1196aa3cdb5d215215ba6688f3 Mon Sep 17 00:00:00 2001 From: Landon Shumway Date: Thu, 25 Sep 2025 10:51:42 -0500 Subject: [PATCH 02/55] Add implementation of feature flag handler --- .../feature-flag/feature_flag_client.py | 282 +++++++++++++++++- .../handlers/check_feature_flag.py | 44 ++- .../python/feature-flag/tests/__init__.py | 59 ++++ .../feature-flag/tests/function/__init__.py | 34 +-- .../tests/function/test_check_feature_flag.py | 101 ++++++- .../tests/function/test_statsig_client.py | 239 +++++++++++++++ 6 files changed, 700 insertions(+), 59 deletions(-) create mode 100644 backend/compact-connect/lambdas/python/feature-flag/tests/function/test_statsig_client.py diff --git a/backend/compact-connect/lambdas/python/feature-flag/feature_flag_client.py b/backend/compact-connect/lambdas/python/feature-flag/feature_flag_client.py index 0b9ca9707..94a354f6a 100644 --- a/backend/compact-connect/lambdas/python/feature-flag/feature_flag_client.py +++ b/backend/compact-connect/lambdas/python/feature-flag/feature_flag_client.py @@ -1,41 +1,291 @@ +# ruff: noqa: N801, N815 invalid-name + +import json +import os from abc import ABC, abstractmethod -from typing import Any, Dict, Optional from dataclasses import dataclass +from typing import Any + +import boto3 +from botocore.exceptions import ClientError +from marshmallow import Schema, ValidationError +from marshmallow.fields import Dict as DictField +from marshmallow.fields import Nested, String +from marshmallow.validate import Length +from statsig import StatsigEnvironmentTier, StatsigOptions, statsig +from statsig.statsig_user import StatsigUser @dataclass -class FeatureFlagContext: - """Context information for feature flag evaluation""" - user_id: Optional[str] = None - environment: str = 'prod' - custom_attributes: Optional[Dict[str, Any]] = None +class FeatureFlagRequest: + """Request object for feature flag evaluation""" + flagName: str # noqa: N815 + context: dict[str, Any] -@dataclass + +@dataclass class FeatureFlagResult: """Result of a feature flag check""" + enabled: bool flag_name: str - metadata: Optional[Dict[str, Any]] = None + metadata: dict[str, Any] | None = None + + +class BaseFeatureFlagCheckRequestSchema(Schema): + """ + Base schema for feature flag check requests. + + All provider-specific schemas should inherit from this base schema. + """ + + flagName = String(required=True, allow_none=False, validate=Length(1, 100)) # noqa: N815 class FeatureFlagClient(ABC): """ Abstract base class for feature flag clients. - + This interface provides a consistent way to interact with different feature flag providers (StatSig, LaunchDarkly, etc.) while hiding the underlying implementation details. """ - + + def __init__(self, request_schema: Schema): + """ + Initialize the feature flag client with a provider-specific schema. + + :param request_schema: Schema instance for validating requests + """ + self._request_schema = request_schema + + def validate_request(self, request_body: dict[str, Any]) -> dict[str, Any]: + """ + Validate the feature flag check request using the provider-specific schema. + + :param request_body: Raw request body dictionary + :return: Validated request data + :raises FeatureFlagValidationException: If validation fails + """ + try: + return self._request_schema.load(request_body) + except ValidationError as e: + raise FeatureFlagValidationException(f'Invalid request: {e.messages}') from e + @abstractmethod - def check_flag(self, flag_name: str, context: FeatureFlagContext) -> FeatureFlagResult: + def check_flag(self, request: FeatureFlagRequest) -> FeatureFlagResult: """ - Check if a feature flag is enabled for the given context. - - :param flag_name: Name of the feature flag to check - :param context: Context for flag evaluation (environment, user, etc.) + Check if a feature flag is enabled for the given request. + + :param request: FeatureFlagRequest containing flag name and context :return: FeatureFlagResult indicating if flag is enabled :raises FeatureFlagException: If flag check fails """ - pass \ No newline at end of file + pass + + def _get_secret(self, secret_name: str) -> dict[str, Any]: + """ + Retrieve a secret from AWS Secrets Manager and return it as a JSON object. + + :param secret_name: Name of the secret in AWS Secrets Manager + :return: Dictionary containing the secret data + :raises FeatureFlagException: If secret retrieval fails + """ + try: + # Create a Secrets Manager client + session = boto3.session.Session() + client = session.client( + service_name='secretsmanager', region_name=os.environ.get('AWS_DEFAULT_REGION', 'us-east-1') + ) + + # Retrieve the secret value + response = client.get_secret_value(SecretId=secret_name) + + # Parse the secret string as JSON + secret_data = json.loads(response['SecretString']) + + return secret_data + + except ClientError as e: + error_code = e.response['Error']['Code'] + raise FeatureFlagException(f"Failed to retrieve secret '{secret_name}': {error_code}") from e + except json.JSONDecodeError as e: + raise FeatureFlagException(f"Secret '{secret_name}' does not contain valid JSON") from e + except Exception as e: + raise FeatureFlagException(f"Unexpected error retrieving secret '{secret_name}': {e}") from e + + +# Custom exceptions +class FeatureFlagException(Exception): + """Base exception for feature flag operations""" + + pass + + +class FeatureFlagValidationException(FeatureFlagException): + """Exception raised when feature flag validation fails""" + + pass + + +# Implementing Classes + + +class StatSigContextSchema(Schema): + """ + StatSig-specific schema for feature flag context validation. + + Includes optional userId and customAttributes. + """ + + userId = String(required=False, allow_none=False, validate=Length(1, 100)) + customAttributes = DictField(required=False, allow_none=False, load_default=dict) + + +class StatSigFeatureFlagCheckRequestSchema(BaseFeatureFlagCheckRequestSchema): + """ + StatSig-specific schema for feature flag check requests. + + Includes optional context with userId and customAttributes. + """ + + context = Nested(StatSigContextSchema, required=False, allow_none=False, load_default=dict) + + +class StatSigFeatureFlagClient(FeatureFlagClient): + """ + StatSig implementation of the FeatureFlagClient interface. + + This client uses StatSig's Python SDK to check feature flags. + Configuration is handled through environment variables. + """ + + def __init__(self, environment: str): + """ + Initialize the StatSig client. + + :param environment: The CompactConnect environment the system is running in ('test', 'beta', 'prod') + """ + # Initialize parent class with StatSig-specific schema + super().__init__(StatSigFeatureFlagCheckRequestSchema()) + + self.environment = environment + self._is_initialized = False + + # Retrieve StatSig configuration from AWS Secrets Manager + try: + secret_name = f'compact-connect/env/{environment}/statsig/credentials' + secret_data = self._get_secret(secret_name) + self._server_secret_key = secret_data.get('serverKey') + + if not self._server_secret_key: + raise FeatureFlagException(f"Secret '{secret_name}' does not contain required 'serverKey' field") + + except Exception as e: + if isinstance(e, FeatureFlagException): + raise + raise FeatureFlagException( + f"Failed to retrieve StatSig configuration from secret '{secret_name}': {e}" + ) from e + + self._initialize_statsig() + + def _initialize_statsig(self): + """Initialize the StatSig SDK if not already initialized""" + if self._is_initialized: + return + + try: + # Map environment tier string to StatsigEnvironmentTier enum + tier_mapping = { + 'prod': StatsigEnvironmentTier.production, + 'beta': StatsigEnvironmentTier.staging, + 'test': StatsigEnvironmentTier.development, + } + + # default to development for all other environments (ie sandbox environments) + tier = tier_mapping.get(self.environment.lower(), StatsigEnvironmentTier.development) + options = StatsigOptions(tier=tier) + + statsig.initialize(self._server_secret_key, options=options).wait() + self._is_initialized = True + + except Exception as e: + raise FeatureFlagException(f'Failed to initialize StatSig client: {e}') from e + + def _create_statsig_user(self, context: dict[str, Any]) -> StatsigUser: + """Convert context dictionary to StatsigUser""" + user_data = { + 'user_id': context.get('userId') or 'default_cc_user', + } + + # Add custom attributes if provided + custom_attributes = context.get('customAttributes', {}) + if custom_attributes: + user_data.update({'custom': custom_attributes}) + + return StatsigUser(**user_data) + + def check_flag(self, request: FeatureFlagRequest) -> FeatureFlagResult: + """ + Check if a feature flag is enabled using StatSig. + + :param request: FeatureFlagRequest containing flag name and context + :return: FeatureFlagResult indicating if flag is enabled + :raises FeatureFlagException: If flag check fails + """ + if not request.flagName: + raise FeatureFlagValidationException('Flag name cannot be empty') + + try: + self._initialize_statsig() + + # Create StatSig user from context + statsig_user = self._create_statsig_user(request.context) + + # Check the gate (feature flag) using StatSig + enabled = statsig.check_gate(statsig_user, request.flagName) + + return FeatureFlagResult( + enabled=enabled, + flag_name=request.flagName, + ) + + except Exception as e: + # If it's already a FeatureFlagException, re-raise it + if isinstance(e, (FeatureFlagException, FeatureFlagValidationException)): + raise + + # Otherwise, wrap it in a FeatureFlagException + raise FeatureFlagException(f"Failed to check feature flag '{request.flagName}': {e}") from e + + def _shutdown(self): + """ + Shutdown the StatSig client to flush event logs to statsig. + """ + if self._is_initialized: + statsig.shutdown().wait() + self._is_initialized = False + + def __del__(self): + """ + Shutdown the StatSig client when the object is destroyed. + + This should be called to flush event logs to statsig when the lambda container shuts down. + """ + self._shutdown() + + +def create_feature_flag_client(environment: str) -> FeatureFlagClient: + """ + Factory function to create a FeatureFlagClient instance. + + This allows easy swapping of implementations based on configuration. + Currently only supports StatSig, but can be extended for other providers. + + :param environment: The CompactConnect environment the system is running in ('test', 'beta', 'prod') + :return: FeatureFlagClient instance + :raises FeatureFlagException: If client creation fails + """ + return StatSigFeatureFlagClient(environment=environment) diff --git a/backend/compact-connect/lambdas/python/feature-flag/handlers/check_feature_flag.py b/backend/compact-connect/lambdas/python/feature-flag/handlers/check_feature_flag.py index 38f3aa51c..ae5976790 100644 --- a/backend/compact-connect/lambdas/python/feature-flag/handlers/check_feature_flag.py +++ b/backend/compact-connect/lambdas/python/feature-flag/handlers/check_feature_flag.py @@ -1,11 +1,47 @@ +import json from aws_lambda_powertools.utilities.typing import LambdaContext -from cc_common.config import logger +from cc_common.config import config, logger +from cc_common.exceptions import CCInvalidRequestException from cc_common.utils import api_handler +from feature_flag_client import FeatureFlagRequest, FeatureFlagValidationException, create_feature_flag_client + +# Initialize feature flag client outside of handler for caching +feature_flag_client = create_feature_flag_client(environment=config.environment_name) -# TODO - initialize feature flag client here outside of the handler @api_handler def check_feature_flag(event: dict, context: LambdaContext): # noqa: ARG001 unused-argument - # TODO - validate body and add implementation of client to check flag based on provided values - pass + """ + Public endpoint for checking feature flags. + + This endpoint is designed to be called by other parts of the system for feature flag evaluation. + It abstracts away the underlying feature flag provider and provides a consistent interface for + checking feature flags. + """ + try: + # Parse and validate request body using client's validation + try: + body = json.loads(event['body']) + validated_body = feature_flag_client.validate_request(body) + except FeatureFlagValidationException as e: + logger.warning('Feature flag validation failed', error=str(e)) + raise CCInvalidRequestException(str(e)) from e + + # Create request object for flag evaluation + flag_request = FeatureFlagRequest(**validated_body) + + # Check the feature flag + result = feature_flag_client.check_flag(flag_request) + + logger.info('Feature flag checked', flag_name=validated_body['flagName'], enabled=result.enabled) + + # Return simple response with just the enabled status + return {'enabled': result.enabled} + + except CCInvalidRequestException: + # Re-raise CC exceptions as-is + raise + except Exception as e: + logger.error(f'Unexpected error checking feature flag: {e}') + raise CCInvalidRequestException('Feature flag check failed') from e diff --git a/backend/compact-connect/lambdas/python/feature-flag/tests/__init__.py b/backend/compact-connect/lambdas/python/feature-flag/tests/__init__.py index 5462ba841..b20364b37 100644 --- a/backend/compact-connect/lambdas/python/feature-flag/tests/__init__.py +++ b/backend/compact-connect/lambdas/python/feature-flag/tests/__init__.py @@ -1,3 +1,4 @@ +import json import os from unittest import TestCase from unittest.mock import MagicMock @@ -14,6 +15,64 @@ def setUpClass(cls): 'DEBUG': 'true', 'ALLOWED_ORIGINS': '["https://example.org"]', 'AWS_DEFAULT_REGION': 'us-east-1', + 'COMPACTS': '["aslp", "octp", "coun"]', + 'JURISDICTIONS': json.dumps( + [ + 'al', + 'ak', + 'az', + 'ar', + 'ca', + 'co', + 'ct', + 'de', + 'dc', + 'fl', + 'ga', + 'hi', + 'id', + 'il', + 'in', + 'ia', + 'ks', + 'ky', + 'la', + 'me', + 'md', + 'ma', + 'mi', + 'mn', + 'ms', + 'mo', + 'mt', + 'ne', + 'nv', + 'nh', + 'nj', + 'nm', + 'ny', + 'nc', + 'nd', + 'oh', + 'ok', + 'or', + 'pa', + 'pr', + 'ri', + 'sc', + 'sd', + 'tn', + 'tx', + 'ut', + 'vt', + 'va', + 'vi', + 'wa', + 'wv', + 'wi', + 'wy', + ] + ), }, ) cls.mock_context = MagicMock(name='MockLambdaContext', spec=LambdaContext) diff --git a/backend/compact-connect/lambdas/python/feature-flag/tests/function/__init__.py b/backend/compact-connect/lambdas/python/feature-flag/tests/function/__init__.py index 4d0c0808d..5ca33773f 100644 --- a/backend/compact-connect/lambdas/python/feature-flag/tests/function/__init__.py +++ b/backend/compact-connect/lambdas/python/feature-flag/tests/function/__init__.py @@ -1,7 +1,6 @@ import logging import os -import boto3 from moto import mock_aws from tests import TstLambdas @@ -17,33 +16,8 @@ class TstFunction(TstLambdas): def setUp(self): # noqa: N801 invalid-name super().setUp() - self.mock_destination_table_name = 'Test-PersistentStack-ProviderTableEC5D0597-TQ2RIO6VVBRE' - self.mock_destination_table_arn = ( - f'arn:aws:dynamodb:us-east-1:767398110685:table/{self.mock_destination_table_name}' - ) - self.mock_source_table_name = 'Recovered-ProviderTableEC5D0597-TQ2RIO6VVBRE' - self.mock_source_table_arn = f'arn:aws:dynamodb:us-east-1:767398110685:table/{self.mock_source_table_name}' - self.build_resources() + # This must be imported within the tests, since they import modules which require + # environment variables that are not set until the TstLambdas class is initialized + from common_test.test_data_generator import TestDataGenerator - self.addCleanup(self.delete_resources) - - def build_resources(self): - # in the case of DR, the lambda sync solution should be table agnostic, since we are performing the same - # cleanup and restoration process regardless of the table that is being recovered - self.mock_source_table = self.create_mock_table(table_name=self.mock_source_table_name) - self.mock_destination_table = self.create_mock_table(table_name=self.mock_destination_table_name) - - def create_mock_table(self, table_name: str): - return boto3.resource('dynamodb').create_table( - AttributeDefinitions=[ - {'AttributeName': 'pk', 'AttributeType': 'S'}, - {'AttributeName': 'sk', 'AttributeType': 'S'}, - ], - TableName=table_name, - KeySchema=[{'AttributeName': 'pk', 'KeyType': 'HASH'}, {'AttributeName': 'sk', 'KeyType': 'RANGE'}], - BillingMode='PAY_PER_REQUEST', - ) - - def delete_resources(self): - self.mock_source_table.delete() - self.mock_destination_table.delete() + self.test_data_generator = TestDataGenerator diff --git a/backend/compact-connect/lambdas/python/feature-flag/tests/function/test_check_feature_flag.py b/backend/compact-connect/lambdas/python/feature-flag/tests/function/test_check_feature_flag.py index 6209e100d..126fe899f 100644 --- a/backend/compact-connect/lambdas/python/feature-flag/tests/function/test_check_feature_flag.py +++ b/backend/compact-connect/lambdas/python/feature-flag/tests/function/test_check_feature_flag.py @@ -1,4 +1,6 @@ -from unittest.mock import patch +import json +from unittest.mock import MagicMock, patch +import boto3 from moto import mock_aws @@ -9,16 +11,97 @@ class TestCheckFeatureFlag(TstFunction): """Test suite for feature flag endpoint.""" - def _generate_test_event(self) -> dict: - return { - 'destinationTableArn': self.mock_destination_table_arn, - 'sourceTableArn': self.mock_source_table_arn, - 'tableNameRecoveryConfirmation': self.mock_destination_table_name, + def setUp(self): + super().setUp() + + # Set up environment variables for testing + import os + + os.environ['ENVIRONMENT_NAME'] = 'test' + + # Set up mock secrets manager with StatSig credentials + secrets_client = boto3.client('secretsmanager', region_name='us-east-1') + secrets_client.create_secret( + Name='compact-connect/env/test/statsig/credentials', + SecretString=json.dumps({'serverKey': 'test-server-key-123'}), + ) + + def _generate_test_api_gateway_event(self, body: dict) -> dict: + """Generate a test API Gateway event""" + event = self.test_data_generator.generate_test_api_event() + event['body'] = json.dumps(body) + + return event + + def _setup_mock_statsig(self, mock_statsig, mock_flag_enabled_return: bool = True): + # Mock StatSig to return True for flag check + mock_statsig.initialize.return_value = MagicMock() + mock_statsig.check_gate.return_value = mock_flag_enabled_return + mock_statsig.shutdown.return_value = MagicMock() + + @patch('feature_flag_client.statsig') + def test_feature_flag_enabled_returns_true(self, mock_statsig): + """Test that when StatSig returns True, our handler returns enabled: true""" + self._setup_mock_statsig(mock_statsig, mock_flag_enabled_return=True) + from handlers.check_feature_flag import check_feature_flag + + # Create test event + test_body = { + 'flagName': 'test-feature-flag', + 'context': {'userId': 'test-user-123', 'customAttributes': {'region': 'us-east-1'}}, } + event = self._generate_test_api_gateway_event(test_body) + + # Call the handler + result = check_feature_flag(event, self.mock_context) + + # Verify the API Gateway response format + self.assertEqual(result['statusCode'], 200) + + # Parse and verify the JSON body + response_body = json.loads(result['body']) + self.assertEqual(response_body, {'enabled': True}) + + @patch('feature_flag_client.statsig') + def test_feature_flag_disabled_returns_false(self, mock_statsig): + """Test that when StatSig returns False, our handler returns enabled: false""" + # Mock StatSig to return False for flag check + self._setup_mock_statsig(mock_statsig, mock_flag_enabled_return=False) - def test_lambda_returns_expected_response_body(self): from handlers.check_feature_flag import check_feature_flag - # TODO - mock feature flag client and call handler to check if it returns expected response - pass + # Create test event + test_body = {'flagName': 'disabled-feature-flag', 'context': {'userId': 'test-user-456'}} + event = self._generate_test_api_gateway_event(test_body) + + # Call the handler + result = check_feature_flag(event, self.mock_context) + + # Verify the API Gateway response format + self.assertEqual(result['statusCode'], 200) + + # Parse and verify the JSON body + response_body = json.loads(result['body']) + self.assertEqual(response_body, {'enabled': False}) + + @patch('feature_flag_client.statsig') + def test_feature_flag_with_minimal_context(self, mock_statsig): + """Test feature flag check with minimal context (no userId or customAttributes)""" + # Mock StatSig to return True for flag check + self._setup_mock_statsig(mock_statsig, mock_flag_enabled_return=True) + + from handlers.check_feature_flag import check_feature_flag + + # Create test event with minimal context + test_body = {'flagName': 'minimal-test-flag', 'context': {}} + event = self._generate_test_api_gateway_event(test_body) + + # Call the handler + result = check_feature_flag(event, self.mock_context) + + # Verify the API Gateway response format + self.assertEqual(result['statusCode'], 200) + # Parse and verify the JSON body + response_body = json.loads(result['body']) + self.assertEqual(response_body, {'enabled': True}) diff --git a/backend/compact-connect/lambdas/python/feature-flag/tests/function/test_statsig_client.py b/backend/compact-connect/lambdas/python/feature-flag/tests/function/test_statsig_client.py new file mode 100644 index 000000000..5f3e05191 --- /dev/null +++ b/backend/compact-connect/lambdas/python/feature-flag/tests/function/test_statsig_client.py @@ -0,0 +1,239 @@ +import json +from unittest.mock import MagicMock, patch + +from feature_flag_client import ( + FeatureFlagException, + FeatureFlagRequest, + FeatureFlagValidationException, + StatSigFeatureFlagClient, +) +from moto import mock_aws +from statsig import StatsigOptions + +from . import TstFunction + +MOCK_SERVER_KEY = 'test-server-key-123' + + +@mock_aws +class TestStatSigClient(TstFunction): + """Test suite for StatSig feature flag client.""" + + def setUp(self): + super().setUp() + + # Set up mock secrets manager with StatSig credentials + secrets_client = self.create_mock_secrets_manager() + secrets_client.create_secret( + Name='compact-connect/env/test/statsig/credentials', SecretString=json.dumps({'serverKey': MOCK_SERVER_KEY}) + ) + + def create_mock_secrets_manager(self): + """Create a mock secrets manager client""" + import boto3 + + return boto3.client('secretsmanager', region_name='us-east-1') + + def test_client_initialization_missing_secret(self): + """Test that client initialization fails when secret is missing""" + with self.assertRaises(FeatureFlagException) as context: + StatSigFeatureFlagClient(environment='nonexistent') + + self.assertIn( + "Failed to retrieve secret 'compact-connect/env/nonexistent/statsig/credentials'", str(context.exception) + ) + + @patch('feature_flag_client.statsig') + def test_validate_request_success(self, mock_statsig): + """Test request validation with valid data""" + mock_statsig.initialize.return_value = MagicMock() + + client = StatSigFeatureFlagClient(environment='test') + + # Valid request data + request_data = { + 'flagName': 'test-flag', + 'context': {'userId': 'user123', 'customAttributes': {'region': 'us-east-1'}}, + } + + # Should validate successfully + client.validate_request(request_data) + + @patch('feature_flag_client.statsig') + def test_validate_request_minimal_data(self, mock_statsig): + """Test request validation with minimal valid data""" + mock_statsig.initialize.return_value = MagicMock() + + client = StatSigFeatureFlagClient(environment='test') + + # Minimal valid request data + request_data = {'flagName': 'test-flag'} + + # Should validate successfully with defaults + validated = client.validate_request(request_data) + + self.assertEqual(validated['flagName'], 'test-flag') + self.assertEqual(validated['context'], {}) # Default empty context + + @patch('feature_flag_client.statsig') + def test_validate_request_missing_flag_name(self, mock_statsig): + """Test request validation fails when flagName is missing""" + mock_statsig.initialize.return_value = MagicMock() + + client = StatSigFeatureFlagClient(environment='test') + + # Invalid request data - missing flagName + request_data = {'context': {'userId': 'user123'}} + + with self.assertRaises(FeatureFlagValidationException): + client.validate_request(request_data) + + @patch('feature_flag_client.statsig') + def test_validate_request_invalid_flag_name(self, mock_statsig): + """Test request validation fails when flagName is empty""" + mock_statsig.initialize.return_value = MagicMock() + + client = StatSigFeatureFlagClient(environment='test') + + # Invalid request data - empty flagName + request_data = {'flagName': '', 'context': {}} + + with self.assertRaises(FeatureFlagValidationException): + client.validate_request(request_data) + + @patch('feature_flag_client.statsig') + def test_check_flag_enabled(self, mock_statsig): + """Test check_flag returns enabled=True when StatSig returns True""" + mock_statsig.initialize.return_value = MagicMock() + mock_statsig.check_gate.return_value = True + + client = StatSigFeatureFlagClient(environment='test') + + # Create request + request = FeatureFlagRequest(flagName='enabled-flag', context={'userId': 'user123'}) + + # Check flag + result = client.check_flag(request) + + # Verify result + self.assertTrue(result.enabled) + self.assertEqual(result.flag_name, 'enabled-flag') + + # Verify StatSig was called correctly + mock_statsig.check_gate.assert_called_once() + call_args = mock_statsig.check_gate.call_args + statsig_user = call_args[0][0] + flag_name = call_args[0][1] + + self.assertEqual(statsig_user.user_id, 'user123') + self.assertEqual(flag_name, 'enabled-flag') + + @patch('feature_flag_client.statsig') + def test_check_flag_disabled(self, mock_statsig): + """Test check_flag returns enabled=False when StatSig returns False""" + mock_statsig.initialize.return_value = MagicMock() + mock_statsig.check_gate.return_value = False + + client = StatSigFeatureFlagClient(environment='test') + + # Create request + request = FeatureFlagRequest(flagName='disabled-flag', context={'userId': 'user456'}) + + # Check flag + result = client.check_flag(request) + + # Verify result + self.assertFalse(result.enabled) + self.assertEqual(result.flag_name, 'disabled-flag') + + @patch('feature_flag_client.statsig') + def test_check_flag_with_custom_attributes(self, mock_statsig): + """Test check_flag properly handles custom attributes""" + mock_statsig.initialize.return_value = MagicMock() + mock_statsig.check_gate.return_value = True + + client = StatSigFeatureFlagClient(environment='test') + + # Create request with custom attributes + request = FeatureFlagRequest( + flagName='custom-flag', + context={ + 'userId': 'user789', + 'customAttributes': {'foo': 'bar'}, + }, + ) + + # Check flag + result = client.check_flag(request) + + # Verify result + self.assertTrue(result.enabled) + + # Verify StatSig user was created with custom attributes + call_args = mock_statsig.check_gate.call_args + statsig_user = call_args[0][0] + flag_name = call_args[0][1] + + self.assertEqual('user789', statsig_user.user_id) + self.assertEqual({'foo': 'bar'}, statsig_user.custom) + self.assertEqual('custom-flag', flag_name) + + @patch('feature_flag_client.statsig') + def test_check_flag_default_user(self, mock_statsig): + """Test check_flag uses default user when no userId provided""" + mock_statsig.initialize.return_value = MagicMock() + mock_statsig.check_gate.return_value = True + + client = StatSigFeatureFlagClient(environment='test') + + # Create request without userId + request = FeatureFlagRequest(flagName='default-user-flag', context={}) + + # Check flag + result = client.check_flag(request) + + # Verify result + self.assertTrue(result.enabled) + + # Verify default user was used + call_args = mock_statsig.check_gate.call_args + statsig_user = call_args[0][0] + + self.assertEqual(statsig_user.user_id, 'default_cc_user') + + @patch('feature_flag_client.statsig') + def test_environment_tier_mapping(self, mock_statsig): + """Test that different environments map to correct StatSig tiers""" + mock_statsig.initialize.return_value = MagicMock() + + # Test different environments + test_cases = [ + ('test', 'development'), + ('beta', 'staging'), + ('prod', 'production'), + ('sandbox', 'development'), # Unknown environments default to development + ] + + for cc_env, expected_tier in test_cases: + # Set up secret for this environment + secrets_client = self.create_mock_secrets_manager() + # note that the test environment secret is created as part of setup, so we don't add that here + if cc_env != 'test': + secrets_client.create_secret( + Name=f'compact-connect/env/{cc_env}/statsig/credentials', + SecretString=json.dumps({'serverKey': MOCK_SERVER_KEY}), + ) + + # Create client + StatSigFeatureFlagClient(environment=cc_env) + + # Verify StatSig was called correctly + mock_statsig.initialize.assert_called_once() + call_args = mock_statsig.initialize.call_args + server_key = call_args[0][0] + options: StatsigOptions = call_args.kwargs['options'] + + self.assertEqual(MOCK_SERVER_KEY, server_key) + self.assertEqual(expected_tier, options.get_sdk_environment_tier()) + + mock_statsig.reset_mock() From aa7ef8ae42703eab0ce3eb5b5b67fbfd3eb469a8 Mon Sep 17 00:00:00 2001 From: Landon Shumway Date: Thu, 25 Sep 2025 14:50:21 -0500 Subject: [PATCH 03/55] linter/formatting --- .../cc_common/data_model/data_client.py | 7 ++--- .../data_model/schema/privilege/record.py | 2 ++ .../data_model/schema/provider/api.py | 1 + .../common/common_test/test_data_generator.py | 2 +- .../tests/unit/test_provider_record_util.py | 31 ++++++++----------- .../handlers/encumbrance_events.py | 2 +- .../tests/function/test_encumbrance_events.py | 13 +++++--- .../feature-flag/feature_flag_client.py | 18 +++-------- .../handlers/check_feature_flag.py | 4 +-- .../tests/function/test_check_feature_flag.py | 2 +- .../test_handlers/test_encumbrance.py | 4 +-- 11 files changed, 39 insertions(+), 47 deletions(-) diff --git a/backend/compact-connect/lambdas/python/common/cc_common/data_model/data_client.py b/backend/compact-connect/lambdas/python/common/cc_common/data_model/data_client.py index 22f23af7b..28a251632 100644 --- a/backend/compact-connect/lambdas/python/common/cc_common/data_model/data_client.py +++ b/backend/compact-connect/lambdas/python/common/cc_common/data_model/data_client.py @@ -2726,7 +2726,6 @@ def encumber_home_jurisdiction_license_privileges( # Add PUT transaction for privilege update record transaction_items.append(self._generate_put_transaction_item(privilege_update_record)) - # Execute transactions in batches of 100 (DynamoDB limit) batch_size = 100 while transaction_items: @@ -2742,9 +2741,9 @@ def encumber_home_jurisdiction_license_privileges( logger.info('Successfully encumbered associated privileges for license') - return (unencumbered_privileges_associated_with_license - + previously_encumbered_privileges_associated_with_license) - + return ( + unencumbered_privileges_associated_with_license + previously_encumbered_privileges_associated_with_license + ) @logger_inject_kwargs(logger, 'compact', 'provider_id', 'jurisdiction', 'license_type_abbreviation') def lift_home_jurisdiction_license_privilege_encumbrances( diff --git a/backend/compact-connect/lambdas/python/common/cc_common/data_model/schema/privilege/record.py b/backend/compact-connect/lambdas/python/common/cc_common/data_model/schema/privilege/record.py index 926ecaa3d..389d055f2 100644 --- a/backend/compact-connect/lambdas/python/common/cc_common/data_model/schema/privilege/record.py +++ b/backend/compact-connect/lambdas/python/common/cc_common/data_model/schema/privilege/record.py @@ -51,6 +51,7 @@ class DeactivationDetailsSchema(Schema): deactivatedByStaffUserId = UUID(required=True, allow_none=False) deactivatedByStaffUserName = String(required=True, allow_none=False) + class EncumbranceDetailsSchema(Schema): """ Schema for tracking details about an encumbrance. @@ -61,6 +62,7 @@ class EncumbranceDetailsSchema(Schema): # present if update is created by upstream license encumbrance licenseJurisdiction = Jurisdiction(required=False, allow_none=False) + @BaseRecordSchema.register_schema('privilege') class PrivilegeRecordSchema(BaseRecordSchema, ValidatesLicenseTypeMixin): """ diff --git a/backend/compact-connect/lambdas/python/common/cc_common/data_model/schema/provider/api.py b/backend/compact-connect/lambdas/python/common/cc_common/data_model/schema/provider/api.py index f08e893e4..40deefa06 100644 --- a/backend/compact-connect/lambdas/python/common/cc_common/data_model/schema/provider/api.py +++ b/backend/compact-connect/lambdas/python/common/cc_common/data_model/schema/provider/api.py @@ -100,6 +100,7 @@ class ProviderReadPrivateResponseSchema(ForgivingSchema): dateOfBirth = Raw(required=True, allow_none=False) ssnLastFour = String(required=False, allow_none=False, validate=Length(equal=4)) + class ProviderGeneralResponseSchema(ForgivingSchema): """ Provider object fields that are sanitized for users with the 'readGeneral' permission. diff --git a/backend/compact-connect/lambdas/python/common/common_test/test_data_generator.py b/backend/compact-connect/lambdas/python/common/common_test/test_data_generator.py index fa0aeb43c..bd2c5f6a9 100644 --- a/backend/compact-connect/lambdas/python/common/common_test/test_data_generator.py +++ b/backend/compact-connect/lambdas/python/common/common_test/test_data_generator.py @@ -357,7 +357,7 @@ def generate_default_privilege_update( @staticmethod def put_default_privilege_update_record_in_provider_table( - value_overrides: dict | None = None + value_overrides: dict | None = None, ) -> PrivilegeUpdateData: """ Creates a default privilege update and stores it in the provider table. diff --git a/backend/compact-connect/lambdas/python/common/tests/unit/test_provider_record_util.py b/backend/compact-connect/lambdas/python/common/tests/unit/test_provider_record_util.py index 1773aec03..37b588128 100644 --- a/backend/compact-connect/lambdas/python/common/tests/unit/test_provider_record_util.py +++ b/backend/compact-connect/lambdas/python/common/tests/unit/test_provider_record_util.py @@ -879,9 +879,7 @@ def test_construct_simplified_privilege_history_object_returns_deactivation_note ] # Enrich the privilege history - history = ProviderRecordUtility.construct_simplified_privilege_history_object( - privilege_data - ) + history = ProviderRecordUtility.construct_simplified_privilege_history_object(privilege_data) # Define the expected issuance update expected_history = { @@ -896,7 +894,7 @@ def test_construct_simplified_privilege_history_object_returns_deactivation_note 'dateOfUpdate': datetime.fromisoformat('2024-01-01T00:00:00+00:00'), 'effectiveDate': datetime.fromisoformat('2024-01-01T00:00:00+00:00'), 'type': 'privilegeUpdate', - 'updateType': 'issuance' + 'updateType': 'issuance', }, { 'createDate': datetime.fromisoformat('2025-06-16T00:00:00+04:00'), @@ -905,8 +903,8 @@ def test_construct_simplified_privilege_history_object_returns_deactivation_note 'type': 'privilegeUpdate', 'updateType': 'deactivation', 'note': 'test deactivation note', - } - ] + }, + ], } # Check that the history contains exactly one update with the expected values @@ -959,9 +957,7 @@ def test_construct_simplified_privilege_history_object_returns_encumbrance_notes ] # Enrich the privilege history - history = ProviderRecordUtility.construct_simplified_privilege_history_object( - privilege_data - ) + history = ProviderRecordUtility.construct_simplified_privilege_history_object(privilege_data) # Define the expected issuance update expected_history = { @@ -976,7 +972,7 @@ def test_construct_simplified_privilege_history_object_returns_encumbrance_notes 'dateOfUpdate': datetime.fromisoformat('2024-01-01T00:00:00+00:00'), 'effectiveDate': datetime.fromisoformat('2024-01-01T00:00:00+00:00'), 'type': 'privilegeUpdate', - 'updateType': 'issuance' + 'updateType': 'issuance', }, { 'createDate': datetime.fromisoformat('2025-06-16T00:00:00+04:00'), @@ -985,8 +981,8 @@ def test_construct_simplified_privilege_history_object_returns_encumbrance_notes 'type': 'privilegeUpdate', 'updateType': 'encumbrance', 'note': 'Non-compliance With Requirements', - } - ] + }, + ], } # Check that the history contains exactly one update with the expected values @@ -1040,9 +1036,7 @@ def test_construct_simplified_privilege_history_object_does_not_return_encumbran ] # Enrich the privilege history - history = ProviderRecordUtility.construct_simplified_privilege_history_object( - privilege_data, False - ) + history = ProviderRecordUtility.construct_simplified_privilege_history_object(privilege_data, False) # Define the expected issuance update expected_history = { @@ -1057,7 +1051,7 @@ def test_construct_simplified_privilege_history_object_does_not_return_encumbran 'dateOfUpdate': datetime.fromisoformat('2024-01-01T00:00:00+00:00'), 'effectiveDate': datetime.fromisoformat('2024-01-01T00:00:00+00:00'), 'type': 'privilegeUpdate', - 'updateType': 'issuance' + 'updateType': 'issuance', }, { 'createDate': datetime.fromisoformat('2025-06-16T00:00:00+04:00'), @@ -1065,14 +1059,15 @@ def test_construct_simplified_privilege_history_object_does_not_return_encumbran 'effectiveDate': datetime.fromisoformat('2025-06-16T00:00:00+04:00'), 'type': 'privilegeUpdate', 'updateType': 'encumbrance', - } - ] + }, + ], } # Check that the history contains exactly one update with the expected values self.maxDiff = None self.assertEqual(expected_history, history) + class TestProviderRecordUtilityActiveSinceCalculation(TstLambdas): def setUp(self): from cc_common.data_model.provider_record_util import ProviderRecordUtility diff --git a/backend/compact-connect/lambdas/python/data-events/handlers/encumbrance_events.py b/backend/compact-connect/lambdas/python/data-events/handlers/encumbrance_events.py index 11b75d9ea..e31663658 100644 --- a/backend/compact-connect/lambdas/python/data-events/handlers/encumbrance_events.py +++ b/backend/compact-connect/lambdas/python/data-events/handlers/encumbrance_events.py @@ -209,7 +209,7 @@ def license_encumbrance_listener(message: dict): license_type_abbreviation=license_type_abbreviation, effective_date=effective_date, adverse_action_category=adverse_action_category, - adverse_action_id=adverse_action_id + adverse_action_id=adverse_action_id, ): logger.info('Processing license encumbrance event') diff --git a/backend/compact-connect/lambdas/python/data-events/tests/function/test_encumbrance_events.py b/backend/compact-connect/lambdas/python/data-events/tests/function/test_encumbrance_events.py index 46e169fa8..c462421e1 100644 --- a/backend/compact-connect/lambdas/python/data-events/tests/function/test_encumbrance_events.py +++ b/backend/compact-connect/lambdas/python/data-events/tests/function/test_encumbrance_events.py @@ -235,11 +235,14 @@ def test_license_encumbrance_listener_handles_all_privileges_already_encumbered( self.assertEqual(1, len(privilege_update_records['Items'])) update_record = privilege_update_records['Items'][0] update_encumbrance_details = update_record['encumbranceDetails'] - self.assertEqual({ - 'adverseActionId': DEFAULT_ADVERSE_ACTION_ID, - 'licenseJurisdiction': 'oh', - 'clinicalPrivilegeActionCategory': 'Unsafe Practice or Substandard Care' - }, update_encumbrance_details) + self.assertEqual( + { + 'adverseActionId': DEFAULT_ADVERSE_ACTION_ID, + 'licenseJurisdiction': 'oh', + 'clinicalPrivilegeActionCategory': 'Unsafe Practice or Substandard Care', + }, + update_encumbrance_details, + ) # Verify one event was published for the privilege update mock_publish_event.assert_called_once() diff --git a/backend/compact-connect/lambdas/python/feature-flag/feature_flag_client.py b/backend/compact-connect/lambdas/python/feature-flag/feature_flag_client.py index 94a354f6a..1ee5cac37 100644 --- a/backend/compact-connect/lambdas/python/feature-flag/feature_flag_client.py +++ b/backend/compact-connect/lambdas/python/feature-flag/feature_flag_client.py @@ -82,7 +82,6 @@ def check_flag(self, request: FeatureFlagRequest) -> FeatureFlagResult: :return: FeatureFlagResult indicating if flag is enabled :raises FeatureFlagException: If flag check fails """ - pass def _get_secret(self, secret_name: str) -> dict[str, Any]: """ @@ -103,9 +102,7 @@ def _get_secret(self, secret_name: str) -> dict[str, Any]: response = client.get_secret_value(SecretId=secret_name) # Parse the secret string as JSON - secret_data = json.loads(response['SecretString']) - - return secret_data + return json.loads(response['SecretString']) except ClientError as e: error_code = e.response['Error']['Code'] @@ -120,14 +117,10 @@ def _get_secret(self, secret_name: str) -> dict[str, Any]: class FeatureFlagException(Exception): """Base exception for feature flag operations""" - pass - class FeatureFlagValidationException(FeatureFlagException): """Exception raised when feature flag validation fails""" - pass - # Implementing Classes @@ -174,8 +167,8 @@ def __init__(self, environment: str): self._is_initialized = False # Retrieve StatSig configuration from AWS Secrets Manager + secret_name = f'compact-connect/env/{environment}/statsig/credentials' try: - secret_name = f'compact-connect/env/{environment}/statsig/credentials' secret_data = self._get_secret(secret_name) self._server_secret_key = secret_data.get('serverKey') @@ -252,11 +245,10 @@ def check_flag(self, request: FeatureFlagRequest) -> FeatureFlagResult: flag_name=request.flagName, ) - except Exception as e: + except (FeatureFlagException, FeatureFlagValidationException) as e: # If it's already a FeatureFlagException, re-raise it - if isinstance(e, (FeatureFlagException, FeatureFlagValidationException)): - raise - + raise e + except Exception as e: # Otherwise, wrap it in a FeatureFlagException raise FeatureFlagException(f"Failed to check feature flag '{request.flagName}': {e}") from e diff --git a/backend/compact-connect/lambdas/python/feature-flag/handlers/check_feature_flag.py b/backend/compact-connect/lambdas/python/feature-flag/handlers/check_feature_flag.py index ae5976790..c75559907 100644 --- a/backend/compact-connect/lambdas/python/feature-flag/handlers/check_feature_flag.py +++ b/backend/compact-connect/lambdas/python/feature-flag/handlers/check_feature_flag.py @@ -2,7 +2,7 @@ from aws_lambda_powertools.utilities.typing import LambdaContext from cc_common.config import config, logger -from cc_common.exceptions import CCInvalidRequestException +from cc_common.exceptions import CCInternalException, CCInvalidRequestException from cc_common.utils import api_handler from feature_flag_client import FeatureFlagRequest, FeatureFlagValidationException, create_feature_flag_client @@ -44,4 +44,4 @@ def check_feature_flag(event: dict, context: LambdaContext): # noqa: ARG001 unu raise except Exception as e: logger.error(f'Unexpected error checking feature flag: {e}') - raise CCInvalidRequestException('Feature flag check failed') from e + raise CCInternalException('Feature flag check failed') from e diff --git a/backend/compact-connect/lambdas/python/feature-flag/tests/function/test_check_feature_flag.py b/backend/compact-connect/lambdas/python/feature-flag/tests/function/test_check_feature_flag.py index 126fe899f..162b3bb24 100644 --- a/backend/compact-connect/lambdas/python/feature-flag/tests/function/test_check_feature_flag.py +++ b/backend/compact-connect/lambdas/python/feature-flag/tests/function/test_check_feature_flag.py @@ -1,7 +1,7 @@ import json from unittest.mock import MagicMock, patch -import boto3 +import boto3 from moto import mock_aws from . import TstFunction diff --git a/backend/compact-connect/lambdas/python/provider-data-v1/tests/function/test_handlers/test_encumbrance.py b/backend/compact-connect/lambdas/python/provider-data-v1/tests/function/test_handlers/test_encumbrance.py index 747cc467e..2e3395b58 100644 --- a/backend/compact-connect/lambdas/python/provider-data-v1/tests/function/test_handlers/test_encumbrance.py +++ b/backend/compact-connect/lambdas/python/provider-data-v1/tests/function/test_handlers/test_encumbrance.py @@ -165,11 +165,11 @@ def test_privilege_encumbrance_handler_adds_privilege_update_record_in_provider_ 'encumbranceDetails': { 'clinicalPrivilegeActionCategory': 'Unsafe Practice or Substandard Care', 'adverseActionId': DEFAULT_ADVERSE_ACTION_ID, - } + }, } ) loaded_privilege_update_data = PrivilegeUpdateData.from_database_record(item) - loaded_privilege_update_data.encumbranceDetails['adverseActionId']= uuid.UUID(DEFAULT_ADVERSE_ACTION_ID) + loaded_privilege_update_data.encumbranceDetails['adverseActionId'] = uuid.UUID(DEFAULT_ADVERSE_ACTION_ID) self.assertEqual( expected_privilege_update_data.to_dict(), From c18b933f528f306a0db436f4fc1ed6928095332d Mon Sep 17 00:00:00 2001 From: Landon Shumway Date: Tue, 30 Sep 2025 09:20:38 -0500 Subject: [PATCH 04/55] fix statsig sdk module name to match latest --- .../lambdas/python/feature-flag/requirements.in | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/backend/compact-connect/lambdas/python/feature-flag/requirements.in b/backend/compact-connect/lambdas/python/feature-flag/requirements.in index 58d329b47..4aee6f235 100644 --- a/backend/compact-connect/lambdas/python/feature-flag/requirements.in +++ b/backend/compact-connect/lambdas/python/feature-flag/requirements.in @@ -1,3 +1,2 @@ # common requirements are managed in the common requirements.in file -statsig==1.45.0 -requests==2.31.0 +statsig-python-core>=0.9.3, <1.0.0 From 1ce1963438130462bc85d58ef7e83eff8d1b1950 Mon Sep 17 00:00:00 2001 From: Landon Shumway Date: Tue, 30 Sep 2025 10:02:27 -0500 Subject: [PATCH 05/55] Add feature flag API endpoint infrastructure --- .../handlers/check_feature_flag.py | 2 +- .../stacks/api_lambda_stack/__init__.py | 7 ++ .../stacks/api_lambda_stack/feature_flags.py | 64 ++++++++++++++++++ .../stacks/api_stack/v1_api/api.py | 9 +++ .../stacks/api_stack/v1_api/api_model.py | 65 +++++++++++++++++++ .../stacks/api_stack/v1_api/feature_flags.py | 35 ++++++++++ 6 files changed, 181 insertions(+), 1 deletion(-) create mode 100644 backend/compact-connect/stacks/api_lambda_stack/feature_flags.py create mode 100644 backend/compact-connect/stacks/api_stack/v1_api/feature_flags.py diff --git a/backend/compact-connect/lambdas/python/feature-flag/handlers/check_feature_flag.py b/backend/compact-connect/lambdas/python/feature-flag/handlers/check_feature_flag.py index c75559907..699e95adf 100644 --- a/backend/compact-connect/lambdas/python/feature-flag/handlers/check_feature_flag.py +++ b/backend/compact-connect/lambdas/python/feature-flag/handlers/check_feature_flag.py @@ -34,7 +34,7 @@ def check_feature_flag(event: dict, context: LambdaContext): # noqa: ARG001 unu # Check the feature flag result = feature_flag_client.check_flag(flag_request) - logger.info('Feature flag checked', flag_name=validated_body['flagName'], enabled=result.enabled) + logger.debug('Feature flag checked', flag_name=validated_body['flagName'], enabled=result.enabled) # Return simple response with just the enabled status return {'enabled': result.enabled} diff --git a/backend/compact-connect/stacks/api_lambda_stack/__init__.py b/backend/compact-connect/stacks/api_lambda_stack/__init__.py index bf68bad61..5acfecf2a 100644 --- a/backend/compact-connect/stacks/api_lambda_stack/__init__.py +++ b/backend/compact-connect/stacks/api_lambda_stack/__init__.py @@ -6,6 +6,7 @@ from stacks import persistent_stack as ps from stacks.provider_users import ProviderUsersStack +from .feature_flags import FeatureFlagsLambdas from .provider_users import ProviderUsersLambdas @@ -29,6 +30,12 @@ def __init__( **kwargs, ) + # Feature Flags related API lambdas + self.feature_flags_lambdas = FeatureFlagsLambdas( + scope=self, + persistent_stack=persistent_stack, + ) + # Provider Users related API lambdas self.provider_users_lambdas = ProviderUsersLambdas( scope=self, diff --git a/backend/compact-connect/stacks/api_lambda_stack/feature_flags.py b/backend/compact-connect/stacks/api_lambda_stack/feature_flags.py new file mode 100644 index 000000000..2acb4d096 --- /dev/null +++ b/backend/compact-connect/stacks/api_lambda_stack/feature_flags.py @@ -0,0 +1,64 @@ +from __future__ import annotations + +import os + +from aws_cdk.aws_secretsmanager import Secret +from cdk_nag import NagSuppressions +from common_constructs.python_function import PythonFunction +from common_constructs.stack import Stack + +from stacks import persistent_stack as ps + + +class FeatureFlagsLambdas: + def __init__( + self, + *, + scope: Stack, + persistent_stack: ps.PersistentStack, + ) -> None: + self.scope = scope + self.persistent_stack = persistent_stack + + self.stack: Stack = Stack.of(scope) + lambda_environment = { + **self.stack.common_env_vars, + } + + # Get the StatsIg secret for each environment + environment_name = self.stack.common_env_vars['ENVIRONMENT_NAME'] + self.statsig_secret = Secret.from_secret_name_v2( + self.scope, + 'StatsigSecret', + f'compact-connect/env/{environment_name}/statsig/credentials', + ) + + self.check_feature_flag_function = self._create_check_feature_flag_function(lambda_environment) + + def _create_check_feature_flag_function(self, lambda_environment: dict) -> PythonFunction: + check_feature_flag_function = PythonFunction( + self.scope, + 'CheckFeatureFlagHandler', + description='Check feature flag handler', + lambda_dir='feature-flag', + index=os.path.join('handlers', 'check_feature_flag.py'), + handler='check_feature_flag', + environment=lambda_environment, + alarm_topic=self.persistent_stack.alarm_topic, + ) + + # Grant permission to read the StatsIg secret + self.statsig_secret.grant_read(check_feature_flag_function) + + NagSuppressions.add_resource_suppressions_by_path( + self.stack, + path=f'{check_feature_flag_function.node.path}/ServiceRole/DefaultPolicy/Resource', + suppressions=[ + { + 'id': 'AwsSolutions-IAM5', + 'reason': 'The actions in this policy are scoped to the StatsIg secret it needs to access.', + }, + ], + ) + + return check_feature_flag_function diff --git a/backend/compact-connect/stacks/api_stack/v1_api/api.py b/backend/compact-connect/stacks/api_stack/v1_api/api.py index 262d7369e..d9e33c7e4 100644 --- a/backend/compact-connect/stacks/api_stack/v1_api/api.py +++ b/backend/compact-connect/stacks/api_stack/v1_api/api.py @@ -16,6 +16,7 @@ from .bulk_upload_url import BulkUploadUrl from .compact_configuration_api import CompactConfigurationApi from .credentials import Credentials +from .feature_flags import FeatureFlagsApi from .post_licenses import PostLicenses from .provider_management import ProviderManagement from .provider_users import ProviderUsers @@ -122,6 +123,14 @@ def __init__( ], ) + # /v1/flags + self.flags_resource = self.resource.add_resource('flags') + self.feature_flags = FeatureFlagsApi( + resource=self.flags_resource, + api_model=self.api_model, + api_lambda_stack=api_lambda_stack, + ) + # /v1/public self.public_resource = self.resource.add_resource('public') # POST /v1/public/compacts/{compact}/providers/query diff --git a/backend/compact-connect/stacks/api_stack/v1_api/api_model.py b/backend/compact-connect/stacks/api_stack/v1_api/api_model.py index f96c66390..54d3ead35 100644 --- a/backend/compact-connect/stacks/api_stack/v1_api/api_model.py +++ b/backend/compact-connect/stacks/api_stack/v1_api/api_model.py @@ -2703,3 +2703,68 @@ def post_provider_email_verify_request_model(self) -> Model: ), ) return self.api._v1_post_provider_email_verify_request_model + + @property + def check_feature_flag_request_model(self) -> Model: + """Request model for POST /v1/flags/check""" + if hasattr(self.api, '_v1_check_feature_flag_request_model'): + return self.api._v1_check_feature_flag_request_model + + self.api._v1_check_feature_flag_request_model = self.api.add_model( + 'V1CheckFeatureFlagRequestModel', + description='Check feature flag request model', + schema=JsonSchema( + type=JsonSchemaType.OBJECT, + additional_properties=False, + required=['flagName'], + properties={ + 'flagName': JsonSchema( + type=JsonSchemaType.STRING, + description='The name of the feature flag to check', + min_length=1, + max_length=100, + ), + 'context': JsonSchema( + type=JsonSchemaType.OBJECT, + description='Optional context for feature flag evaluation', + additional_properties=False, + properties={ + 'userId': JsonSchema( + type=JsonSchemaType.STRING, + description='Optional user ID for feature flag evaluation', + min_length=1, + max_length=100, + ), + 'customAttributes': JsonSchema( + type=JsonSchemaType.OBJECT, + description='Optional custom attributes for feature flag evaluation', + additional_properties=JsonSchema(type=JsonSchemaType.STRING), + ), + }, + ), + }, + ), + ) + return self.api._v1_check_feature_flag_request_model + + @property + def check_feature_flag_response_model(self) -> Model: + """Response model for POST /v1/flags/check""" + if hasattr(self.api, '_v1_check_feature_flag_response_model'): + return self.api._v1_check_feature_flag_response_model + + self.api._v1_check_feature_flag_response_model = self.api.add_model( + 'V1CheckFeatureFlagResponseModel', + description='Check feature flag response model', + schema=JsonSchema( + type=JsonSchemaType.OBJECT, + required=['enabled'], + properties={ + 'enabled': JsonSchema( + type=JsonSchemaType.BOOLEAN, + description='Whether the feature flag is enabled', + ), + }, + ), + ) + return self.api._v1_check_feature_flag_response_model diff --git a/backend/compact-connect/stacks/api_stack/v1_api/feature_flags.py b/backend/compact-connect/stacks/api_stack/v1_api/feature_flags.py new file mode 100644 index 000000000..db058903b --- /dev/null +++ b/backend/compact-connect/stacks/api_stack/v1_api/feature_flags.py @@ -0,0 +1,35 @@ +from __future__ import annotations + +from api_lambda_stack import ApiLambdaStack +from aws_cdk.aws_apigateway import LambdaIntegration, Resource + +from .api_model import ApiModel + + +class FeatureFlagsApi: + """Feature flags API endpoints""" + + def __init__( + self, + *, + resource: Resource, + api_model: ApiModel, + api_lambda_stack: ApiLambdaStack, + ): + super().__init__() + self.resource = resource + self.api_model = api_model + + # POST /v1/flags/check + check_resource = resource.add_resource('check') + check_resource.add_method( + 'POST', + integration=LambdaIntegration(api_lambda_stack.feature_flags_lambdas.check_feature_flag_function), + request_models={'application/json': api_model.check_feature_flag_request_model}, + method_responses=[ + { + 'statusCode': '200', + 'responseModels': {'application/json': api_model.check_feature_flag_response_model}, + }, + ], + ) From 84c080aa86743e9bbd5d2fdad509bc7fe8ca5b31 Mon Sep 17 00:00:00 2001 From: Landon Shumway Date: Tue, 30 Sep 2025 12:17:53 -0500 Subject: [PATCH 06/55] Update API spec/postman collection to latest --- .../stacks/api_stack/v1_api/feature_flags.py | 22 +++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/backend/compact-connect/stacks/api_stack/v1_api/feature_flags.py b/backend/compact-connect/stacks/api_stack/v1_api/feature_flags.py index db058903b..8e658352a 100644 --- a/backend/compact-connect/stacks/api_stack/v1_api/feature_flags.py +++ b/backend/compact-connect/stacks/api_stack/v1_api/feature_flags.py @@ -1,7 +1,9 @@ from __future__ import annotations -from api_lambda_stack import ApiLambdaStack from aws_cdk.aws_apigateway import LambdaIntegration, Resource +from cdk_nag import NagSuppressions + +from stacks.api_lambda_stack import ApiLambdaStack from .api_model import ApiModel @@ -22,7 +24,7 @@ def __init__( # POST /v1/flags/check check_resource = resource.add_resource('check') - check_resource.add_method( + self.check_flag_method = check_resource.add_method( 'POST', integration=LambdaIntegration(api_lambda_stack.feature_flags_lambdas.check_feature_flag_function), request_models={'application/json': api_model.check_feature_flag_request_model}, @@ -33,3 +35,19 @@ def __init__( }, ], ) + + # Add suppressions for the public GET endpoint + NagSuppressions.add_resource_suppressions( + self.check_flag_method, + suppressions=[ + { + 'id': 'AwsSolutions-APIG4', + 'reason': 'This is a public endpoint that intentionally does not require authorization', + }, + { + 'id': 'AwsSolutions-COG4', + 'reason': 'This is a public endpoint that intentionally ' + 'does not use a Cognito user pool authorizer', + }, + ], + ) From 1071695c33e83ac47bdb956df1379ffb7bf82128 Mon Sep 17 00:00:00 2001 From: Landon Shumway Date: Tue, 30 Sep 2025 13:06:33 -0500 Subject: [PATCH 07/55] Move from legacy statsig SDK to current SDK --- .../feature-flag/feature_flag_client.py | 25 ++++--- .../tests/function/test_check_feature_flag.py | 36 +++++++--- .../tests/function/test_statsig_client.py | 67 +++++++++++-------- 3 files changed, 79 insertions(+), 49 deletions(-) diff --git a/backend/compact-connect/lambdas/python/feature-flag/feature_flag_client.py b/backend/compact-connect/lambdas/python/feature-flag/feature_flag_client.py index 1ee5cac37..3ced54195 100644 --- a/backend/compact-connect/lambdas/python/feature-flag/feature_flag_client.py +++ b/backend/compact-connect/lambdas/python/feature-flag/feature_flag_client.py @@ -12,8 +12,7 @@ from marshmallow.fields import Dict as DictField from marshmallow.fields import Nested, String from marshmallow.validate import Length -from statsig import StatsigEnvironmentTier, StatsigOptions, statsig -from statsig.statsig_user import StatsigUser +from statsig_python_core import StatsigOptions, Statsig, StatsigUser @dataclass @@ -124,6 +123,9 @@ class FeatureFlagValidationException(FeatureFlagException): # Implementing Classes +STATSIG_DEVELOPMENT_TIER = 'development' +STATSIG_STAGING_TIER = 'staging' +STATSIG_PRODUCTION_TIER = 'production' class StatSigContextSchema(Schema): """ @@ -164,6 +166,7 @@ def __init__(self, environment: str): super().__init__(StatSigFeatureFlagCheckRequestSchema()) self.environment = environment + self.statsig_client = None self._is_initialized = False # Retrieve StatSig configuration from AWS Secrets Manager @@ -192,16 +195,18 @@ def _initialize_statsig(self): try: # Map environment tier string to StatsigEnvironmentTier enum tier_mapping = { - 'prod': StatsigEnvironmentTier.production, - 'beta': StatsigEnvironmentTier.staging, - 'test': StatsigEnvironmentTier.development, + 'prod': STATSIG_PRODUCTION_TIER, + 'beta': STATSIG_STAGING_TIER, + 'test': STATSIG_DEVELOPMENT_TIER, } # default to development for all other environments (ie sandbox environments) - tier = tier_mapping.get(self.environment.lower(), StatsigEnvironmentTier.development) - options = StatsigOptions(tier=tier) + tier = tier_mapping.get(self.environment.lower(), STATSIG_DEVELOPMENT_TIER) + options = StatsigOptions() + options.environment = tier - statsig.initialize(self._server_secret_key, options=options).wait() + self.statsig_client = Statsig(self._server_secret_key, options=options) + self.statsig_client.initialize().wait() self._is_initialized = True except Exception as e: @@ -238,7 +243,7 @@ def check_flag(self, request: FeatureFlagRequest) -> FeatureFlagResult: statsig_user = self._create_statsig_user(request.context) # Check the gate (feature flag) using StatSig - enabled = statsig.check_gate(statsig_user, request.flagName) + enabled = self.statsig_client.check_gate(statsig_user, request.flagName) return FeatureFlagResult( enabled=enabled, @@ -257,7 +262,7 @@ def _shutdown(self): Shutdown the StatSig client to flush event logs to statsig. """ if self._is_initialized: - statsig.shutdown().wait() + self.statsig_client.shutdown().wait() self._is_initialized = False def __del__(self): diff --git a/backend/compact-connect/lambdas/python/feature-flag/tests/function/test_check_feature_flag.py b/backend/compact-connect/lambdas/python/feature-flag/tests/function/test_check_feature_flag.py index 162b3bb24..40212f9a6 100644 --- a/backend/compact-connect/lambdas/python/feature-flag/tests/function/test_check_feature_flag.py +++ b/backend/compact-connect/lambdas/python/feature-flag/tests/function/test_check_feature_flag.py @@ -26,6 +26,16 @@ def setUp(self): SecretString=json.dumps({'serverKey': 'test-server-key-123'}), ) + def tearDown(self): + """Clean up between tests to ensure test isolation""" + super().tearDown() + + # Reset the module-level feature_flag_client to force recreation in next test + # without this the client gets cached and cannot be modified + import sys + if 'handlers.check_feature_flag' in sys.modules: + del sys.modules['handlers.check_feature_flag'] + def _generate_test_api_gateway_event(self, body: dict) -> dict: """Generate a test API Gateway event""" event = self.test_data_generator.generate_test_api_event() @@ -34,12 +44,18 @@ def _generate_test_api_gateway_event(self, body: dict) -> dict: return event def _setup_mock_statsig(self, mock_statsig, mock_flag_enabled_return: bool = True): - # Mock StatSig to return True for flag check - mock_statsig.initialize.return_value = MagicMock() - mock_statsig.check_gate.return_value = mock_flag_enabled_return - mock_statsig.shutdown.return_value = MagicMock() + # Create a mock client instance + mock_client = MagicMock() + mock_client.initialize.return_value = MagicMock() + mock_client.check_gate.return_value = mock_flag_enabled_return + mock_client.shutdown.return_value = MagicMock() + + # Make the Statsig constructor return our mock client + mock_statsig.return_value = mock_client + + return mock_client - @patch('feature_flag_client.statsig') + @patch('feature_flag_client.Statsig') def test_feature_flag_enabled_returns_true(self, mock_statsig): """Test that when StatSig returns True, our handler returns enabled: true""" self._setup_mock_statsig(mock_statsig, mock_flag_enabled_return=True) @@ -60,9 +76,9 @@ def test_feature_flag_enabled_returns_true(self, mock_statsig): # Parse and verify the JSON body response_body = json.loads(result['body']) - self.assertEqual(response_body, {'enabled': True}) + self.assertEqual({'enabled': True}, response_body) - @patch('feature_flag_client.statsig') + @patch('feature_flag_client.Statsig') def test_feature_flag_disabled_returns_false(self, mock_statsig): """Test that when StatSig returns False, our handler returns enabled: false""" # Mock StatSig to return False for flag check @@ -82,9 +98,9 @@ def test_feature_flag_disabled_returns_false(self, mock_statsig): # Parse and verify the JSON body response_body = json.loads(result['body']) - self.assertEqual(response_body, {'enabled': False}) + self.assertEqual({'enabled': False}, response_body) - @patch('feature_flag_client.statsig') + @patch('feature_flag_client.Statsig') def test_feature_flag_with_minimal_context(self, mock_statsig): """Test feature flag check with minimal context (no userId or customAttributes)""" # Mock StatSig to return True for flag check @@ -104,4 +120,4 @@ def test_feature_flag_with_minimal_context(self, mock_statsig): # Parse and verify the JSON body response_body = json.loads(result['body']) - self.assertEqual(response_body, {'enabled': True}) + self.assertEqual({'enabled': True}, response_body) diff --git a/backend/compact-connect/lambdas/python/feature-flag/tests/function/test_statsig_client.py b/backend/compact-connect/lambdas/python/feature-flag/tests/function/test_statsig_client.py index 5f3e05191..aab131fa6 100644 --- a/backend/compact-connect/lambdas/python/feature-flag/tests/function/test_statsig_client.py +++ b/backend/compact-connect/lambdas/python/feature-flag/tests/function/test_statsig_client.py @@ -8,7 +8,7 @@ StatSigFeatureFlagClient, ) from moto import mock_aws -from statsig import StatsigOptions +from statsig_python_core import StatsigOptions from . import TstFunction @@ -34,6 +34,18 @@ def create_mock_secrets_manager(self): return boto3.client('secretsmanager', region_name='us-east-1') + def _setup_mock_statsig(self, mock_statsig, mock_flag_enabled_return: bool = True): + # Create a mock client instance + mock_client = MagicMock() + mock_client.initialize.return_value = MagicMock() + mock_client.check_gate.return_value = mock_flag_enabled_return + mock_client.shutdown.return_value = MagicMock() + + # Make the Statsig constructor return our mock client + mock_statsig.return_value = mock_client + + return mock_client + def test_client_initialization_missing_secret(self): """Test that client initialization fails when secret is missing""" with self.assertRaises(FeatureFlagException) as context: @@ -43,10 +55,10 @@ def test_client_initialization_missing_secret(self): "Failed to retrieve secret 'compact-connect/env/nonexistent/statsig/credentials'", str(context.exception) ) - @patch('feature_flag_client.statsig') + @patch('feature_flag_client.Statsig') def test_validate_request_success(self, mock_statsig): """Test request validation with valid data""" - mock_statsig.initialize.return_value = MagicMock() + self._setup_mock_statsig(mock_statsig) client = StatSigFeatureFlagClient(environment='test') @@ -59,9 +71,10 @@ def test_validate_request_success(self, mock_statsig): # Should validate successfully client.validate_request(request_data) - @patch('feature_flag_client.statsig') + @patch('feature_flag_client.Statsig') def test_validate_request_minimal_data(self, mock_statsig): """Test request validation with minimal valid data""" + self._setup_mock_statsig(mock_statsig) mock_statsig.initialize.return_value = MagicMock() client = StatSigFeatureFlagClient(environment='test') @@ -75,10 +88,10 @@ def test_validate_request_minimal_data(self, mock_statsig): self.assertEqual(validated['flagName'], 'test-flag') self.assertEqual(validated['context'], {}) # Default empty context - @patch('feature_flag_client.statsig') + @patch('feature_flag_client.Statsig') def test_validate_request_missing_flag_name(self, mock_statsig): """Test request validation fails when flagName is missing""" - mock_statsig.initialize.return_value = MagicMock() + self._setup_mock_statsig(mock_statsig) client = StatSigFeatureFlagClient(environment='test') @@ -88,10 +101,10 @@ def test_validate_request_missing_flag_name(self, mock_statsig): with self.assertRaises(FeatureFlagValidationException): client.validate_request(request_data) - @patch('feature_flag_client.statsig') + @patch('feature_flag_client.Statsig') def test_validate_request_invalid_flag_name(self, mock_statsig): """Test request validation fails when flagName is empty""" - mock_statsig.initialize.return_value = MagicMock() + self._setup_mock_statsig(mock_statsig) client = StatSigFeatureFlagClient(environment='test') @@ -101,11 +114,10 @@ def test_validate_request_invalid_flag_name(self, mock_statsig): with self.assertRaises(FeatureFlagValidationException): client.validate_request(request_data) - @patch('feature_flag_client.statsig') + @patch('feature_flag_client.Statsig') def test_check_flag_enabled(self, mock_statsig): """Test check_flag returns enabled=True when StatSig returns True""" - mock_statsig.initialize.return_value = MagicMock() - mock_statsig.check_gate.return_value = True + mock_statsig_client = self._setup_mock_statsig(mock_statsig) client = StatSigFeatureFlagClient(environment='test') @@ -120,19 +132,18 @@ def test_check_flag_enabled(self, mock_statsig): self.assertEqual(result.flag_name, 'enabled-flag') # Verify StatSig was called correctly - mock_statsig.check_gate.assert_called_once() - call_args = mock_statsig.check_gate.call_args + mock_statsig_client.check_gate.assert_called_once() + call_args = mock_statsig_client.check_gate.call_args statsig_user = call_args[0][0] flag_name = call_args[0][1] self.assertEqual(statsig_user.user_id, 'user123') self.assertEqual(flag_name, 'enabled-flag') - @patch('feature_flag_client.statsig') + @patch('feature_flag_client.Statsig') def test_check_flag_disabled(self, mock_statsig): """Test check_flag returns enabled=False when StatSig returns False""" - mock_statsig.initialize.return_value = MagicMock() - mock_statsig.check_gate.return_value = False + self._setup_mock_statsig(mock_statsig, mock_flag_enabled_return=False) client = StatSigFeatureFlagClient(environment='test') @@ -146,11 +157,10 @@ def test_check_flag_disabled(self, mock_statsig): self.assertFalse(result.enabled) self.assertEqual(result.flag_name, 'disabled-flag') - @patch('feature_flag_client.statsig') + @patch('feature_flag_client.Statsig') def test_check_flag_with_custom_attributes(self, mock_statsig): """Test check_flag properly handles custom attributes""" - mock_statsig.initialize.return_value = MagicMock() - mock_statsig.check_gate.return_value = True + mock_statsig_client = self._setup_mock_statsig(mock_statsig) client = StatSigFeatureFlagClient(environment='test') @@ -170,7 +180,7 @@ def test_check_flag_with_custom_attributes(self, mock_statsig): self.assertTrue(result.enabled) # Verify StatSig user was created with custom attributes - call_args = mock_statsig.check_gate.call_args + call_args = mock_statsig_client.check_gate.call_args statsig_user = call_args[0][0] flag_name = call_args[0][1] @@ -178,11 +188,10 @@ def test_check_flag_with_custom_attributes(self, mock_statsig): self.assertEqual({'foo': 'bar'}, statsig_user.custom) self.assertEqual('custom-flag', flag_name) - @patch('feature_flag_client.statsig') + @patch('feature_flag_client.Statsig') def test_check_flag_default_user(self, mock_statsig): """Test check_flag uses default user when no userId provided""" - mock_statsig.initialize.return_value = MagicMock() - mock_statsig.check_gate.return_value = True + mock_statsig_client = self._setup_mock_statsig(mock_statsig) client = StatSigFeatureFlagClient(environment='test') @@ -196,15 +205,15 @@ def test_check_flag_default_user(self, mock_statsig): self.assertTrue(result.enabled) # Verify default user was used - call_args = mock_statsig.check_gate.call_args + call_args = mock_statsig_client.check_gate.call_args statsig_user = call_args[0][0] self.assertEqual(statsig_user.user_id, 'default_cc_user') - @patch('feature_flag_client.statsig') + @patch('feature_flag_client.Statsig') def test_environment_tier_mapping(self, mock_statsig): """Test that different environments map to correct StatSig tiers""" - mock_statsig.initialize.return_value = MagicMock() + self._setup_mock_statsig(mock_statsig) # Test different environments test_cases = [ @@ -228,12 +237,12 @@ def test_environment_tier_mapping(self, mock_statsig): StatSigFeatureFlagClient(environment=cc_env) # Verify StatSig was called correctly - mock_statsig.initialize.assert_called_once() - call_args = mock_statsig.initialize.call_args + mock_statsig.assert_called_once() + call_args = mock_statsig.call_args server_key = call_args[0][0] options: StatsigOptions = call_args.kwargs['options'] self.assertEqual(MOCK_SERVER_KEY, server_key) - self.assertEqual(expected_tier, options.get_sdk_environment_tier()) + self.assertEqual(expected_tier, options.environment) mock_statsig.reset_mock() From 29b55a38c8a30278d613e92439cc8d1584d951d0 Mon Sep 17 00:00:00 2001 From: Landon Shumway Date: Tue, 30 Sep 2025 13:12:56 -0500 Subject: [PATCH 08/55] formatting/linter --- .../lambdas/python/feature-flag/feature_flag_client.py | 3 ++- .../feature-flag/tests/function/test_check_feature_flag.py | 3 ++- .../compact-connect/stacks/api_stack/v1_api/feature_flags.py | 2 +- 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/backend/compact-connect/lambdas/python/feature-flag/feature_flag_client.py b/backend/compact-connect/lambdas/python/feature-flag/feature_flag_client.py index 3ced54195..c7c1e5bde 100644 --- a/backend/compact-connect/lambdas/python/feature-flag/feature_flag_client.py +++ b/backend/compact-connect/lambdas/python/feature-flag/feature_flag_client.py @@ -12,7 +12,7 @@ from marshmallow.fields import Dict as DictField from marshmallow.fields import Nested, String from marshmallow.validate import Length -from statsig_python_core import StatsigOptions, Statsig, StatsigUser +from statsig_python_core import Statsig, StatsigOptions, StatsigUser @dataclass @@ -127,6 +127,7 @@ class FeatureFlagValidationException(FeatureFlagException): STATSIG_STAGING_TIER = 'staging' STATSIG_PRODUCTION_TIER = 'production' + class StatSigContextSchema(Schema): """ StatSig-specific schema for feature flag context validation. diff --git a/backend/compact-connect/lambdas/python/feature-flag/tests/function/test_check_feature_flag.py b/backend/compact-connect/lambdas/python/feature-flag/tests/function/test_check_feature_flag.py index 40212f9a6..572e41ec7 100644 --- a/backend/compact-connect/lambdas/python/feature-flag/tests/function/test_check_feature_flag.py +++ b/backend/compact-connect/lambdas/python/feature-flag/tests/function/test_check_feature_flag.py @@ -29,10 +29,11 @@ def setUp(self): def tearDown(self): """Clean up between tests to ensure test isolation""" super().tearDown() - + # Reset the module-level feature_flag_client to force recreation in next test # without this the client gets cached and cannot be modified import sys + if 'handlers.check_feature_flag' in sys.modules: del sys.modules['handlers.check_feature_flag'] diff --git a/backend/compact-connect/stacks/api_stack/v1_api/feature_flags.py b/backend/compact-connect/stacks/api_stack/v1_api/feature_flags.py index 8e658352a..f4695f10f 100644 --- a/backend/compact-connect/stacks/api_stack/v1_api/feature_flags.py +++ b/backend/compact-connect/stacks/api_stack/v1_api/feature_flags.py @@ -47,7 +47,7 @@ def __init__( { 'id': 'AwsSolutions-COG4', 'reason': 'This is a public endpoint that intentionally ' - 'does not use a Cognito user pool authorizer', + 'does not use a Cognito user pool authorizer', }, ], ) From 5b32da92b8ce954ee55df94553a96105b3159afd Mon Sep 17 00:00:00 2001 From: Landon Shumway Date: Tue, 30 Sep 2025 14:18:51 -0500 Subject: [PATCH 09/55] Add requirements files for feature flag directory --- .../python/feature-flag/requirements-dev.txt | 33 ++++++++++--------- .../python/feature-flag/requirements.txt | 16 ++++++++- 2 files changed, 33 insertions(+), 16 deletions(-) diff --git a/backend/compact-connect/lambdas/python/feature-flag/requirements-dev.txt b/backend/compact-connect/lambdas/python/feature-flag/requirements-dev.txt index 1c844d20c..240185eca 100644 --- a/backend/compact-connect/lambdas/python/feature-flag/requirements-dev.txt +++ b/backend/compact-connect/lambdas/python/feature-flag/requirements-dev.txt @@ -2,22 +2,22 @@ # This file is autogenerated by pip-compile with Python 3.12 # by the following command: # -# pip-compile --no-emit-index-url compact-connect/lambdas/python/disaster-recovery/requirements-dev.in +# pip-compile --no-emit-index-url compact-connect/lambdas/python/feature-flag/requirements-dev.in # -boto3==1.40.19 +boto3==1.40.41 # via moto -botocore==1.40.19 +botocore==1.40.41 # via # boto3 # moto # s3transfer certifi==2025.8.3 # via requests -cffi==1.17.1 +cffi==2.0.0 # via cryptography charset-normalizer==3.4.3 # via requests -cryptography==45.0.6 +cryptography==46.0.1 # via moto docker==7.1.0 # via moto @@ -29,35 +29,38 @@ jmespath==1.0.1 # via # boto3 # botocore -markupsafe==3.0.2 +markupsafe==3.0.3 # via # jinja2 # werkzeug -moto[dynamodb,s3]==5.1.11 - # via -r compact-connect/lambdas/python/disaster-recovery/requirements-dev.in +moto[dynamodb]==5.1.13 + # via -r compact-connect/lambdas/python/feature-flag/requirements-dev.in py-partiql-parser==0.6.1 # via moto -pycparser==2.22 +pycparser==2.23 # via cffi python-dateutil==2.9.0.post0 # via # botocore # moto -pyyaml==6.0.2 - # via - # moto - # responses +pyyaml==6.0.3 + # via responses requests==2.32.5 # via # docker # moto # responses + # statsig-python-core responses==0.25.8 # via moto -s3transfer==0.13.1 +s3transfer==0.14.0 # via boto3 six==1.17.0 # via python-dateutil +statsig-python-core==0.9.3 + # via -r compact-connect/lambdas/python/feature-flag/requirements-dev.in +typing-extensions==4.15.0 + # via statsig-python-core urllib3==2.5.0 # via # botocore @@ -66,5 +69,5 @@ urllib3==2.5.0 # responses werkzeug==3.1.3 # via moto -xmltodict==0.14.2 +xmltodict==1.0.2 # via moto diff --git a/backend/compact-connect/lambdas/python/feature-flag/requirements.txt b/backend/compact-connect/lambdas/python/feature-flag/requirements.txt index 24e02f5ca..ec599d6c7 100644 --- a/backend/compact-connect/lambdas/python/feature-flag/requirements.txt +++ b/backend/compact-connect/lambdas/python/feature-flag/requirements.txt @@ -2,5 +2,19 @@ # This file is autogenerated by pip-compile with Python 3.12 # by the following command: # -# pip-compile --no-emit-index-url compact-connect/lambdas/python/disaster-recovery/requirements.in +# pip-compile --no-emit-index-url compact-connect/lambdas/python/feature-flag/requirements.in # +certifi==2025.8.3 + # via requests +charset-normalizer==3.4.3 + # via requests +idna==3.10 + # via requests +requests==2.32.5 + # via statsig-python-core +statsig-python-core==0.9.3 + # via -r compact-connect/lambdas/python/feature-flag/requirements.in +typing-extensions==4.15.0 + # via statsig-python-core +urllib3==2.5.0 + # via requests From 84b14b98db5f5608ba9184375d1dc6dd14c89d62 Mon Sep 17 00:00:00 2001 From: Landon Shumway Date: Wed, 1 Oct 2025 12:23:38 -0500 Subject: [PATCH 10/55] add missing registration metric report --- .../lambdas/python/provider-data-v1/handlers/registration.py | 1 + 1 file changed, 1 insertion(+) diff --git a/backend/compact-connect/lambdas/python/provider-data-v1/handlers/registration.py b/backend/compact-connect/lambdas/python/provider-data-v1/handlers/registration.py index ac6f335a8..e25d8874e 100644 --- a/backend/compact-connect/lambdas/python/provider-data-v1/handlers/registration.py +++ b/backend/compact-connect/lambdas/python/provider-data-v1/handlers/registration.py @@ -215,6 +215,7 @@ def register_provider(event: dict, context: LambdaContext): # noqa: ARG001 unus jurisdiction=body['jurisdiction'], environment=config.environment_name, ) + metrics.add_metric(name=REGISTRATION_ATTEMPT_METRIC_NAME, unit=MetricUnit.NoUnit, value=0) raise CCInvalidRequestException('Registration is not currently available for the specified state.') from e # Check if the jurisdiction is configured and live in the compact's configuredStates From 66fa6a45beaba2a957a1ba8901186f74eaea1ac7 Mon Sep 17 00:00:00 2001 From: Landon Shumway Date: Wed, 1 Oct 2025 13:53:44 -0500 Subject: [PATCH 11/55] Update requirements to latest --- .../lambdas/python/common/requirements-dev.in | 1 + .../lambdas/python/feature-flag/requirements-dev.txt | 11 +++-------- 2 files changed, 4 insertions(+), 8 deletions(-) diff --git a/backend/compact-connect/lambdas/python/common/requirements-dev.in b/backend/compact-connect/lambdas/python/common/requirements-dev.in index b737d5527..2dc3299cf 100644 --- a/backend/compact-connect/lambdas/python/common/requirements-dev.in +++ b/backend/compact-connect/lambdas/python/common/requirements-dev.in @@ -1,3 +1,4 @@ moto[dynamodb, s3]>=5.0.12, <6 boto3-stubs[full] Faker>=28,<29 +cryptography>=45.0.5, <46 diff --git a/backend/compact-connect/lambdas/python/feature-flag/requirements-dev.txt b/backend/compact-connect/lambdas/python/feature-flag/requirements-dev.txt index 240185eca..211b62be8 100644 --- a/backend/compact-connect/lambdas/python/feature-flag/requirements-dev.txt +++ b/backend/compact-connect/lambdas/python/feature-flag/requirements-dev.txt @@ -4,9 +4,9 @@ # # pip-compile --no-emit-index-url compact-connect/lambdas/python/feature-flag/requirements-dev.in # -boto3==1.40.41 +boto3==1.40.42 # via moto -botocore==1.40.41 +botocore==1.40.42 # via # boto3 # moto @@ -17,7 +17,7 @@ cffi==2.0.0 # via cryptography charset-normalizer==3.4.3 # via requests -cryptography==46.0.1 +cryptography==46.0.2 # via moto docker==7.1.0 # via moto @@ -50,17 +50,12 @@ requests==2.32.5 # docker # moto # responses - # statsig-python-core responses==0.25.8 # via moto s3transfer==0.14.0 # via boto3 six==1.17.0 # via python-dateutil -statsig-python-core==0.9.3 - # via -r compact-connect/lambdas/python/feature-flag/requirements-dev.in -typing-extensions==4.15.0 - # via statsig-python-core urllib3==2.5.0 # via # botocore From fc9e28a2536553fb88e6d9f9584696cc406c2d98 Mon Sep 17 00:00:00 2001 From: Landon Shumway Date: Wed, 1 Oct 2025 16:32:32 -0500 Subject: [PATCH 12/55] Add python client for checking if feature flag is enabled --- .../common/cc_common/feature_flag_client.py | 134 ++++++++++ .../lambdas/python/common/tests/__init__.py | 1 + .../tests/unit/test_feature_flag_client.py | 236 ++++++++++++++++++ .../stacks/api_stack/v1_api/api.py | 4 + 4 files changed, 375 insertions(+) create mode 100644 backend/compact-connect/lambdas/python/common/cc_common/feature_flag_client.py create mode 100644 backend/compact-connect/lambdas/python/common/tests/unit/test_feature_flag_client.py diff --git a/backend/compact-connect/lambdas/python/common/cc_common/feature_flag_client.py b/backend/compact-connect/lambdas/python/common/cc_common/feature_flag_client.py new file mode 100644 index 000000000..b55ce72e4 --- /dev/null +++ b/backend/compact-connect/lambdas/python/common/cc_common/feature_flag_client.py @@ -0,0 +1,134 @@ +""" +Feature flag client for checking feature flags via the internal API. + +This module provides a simple, stateless interface for checking feature flags +from other Lambda functions without direct dependency on the feature flag provider. +""" + +from dataclasses import dataclass +from typing import Any + +import requests + +from cc_common.config import config, logger + + +@dataclass +class FeatureFlagContext: + """ + Context information for feature flag evaluation. + + This context is used by the feature flag provider to determine whether a flag + should be enabled for a specific user or scenario. + + :param user_id: Optional user identifier for user-specific flag evaluation + :param custom_attributes: Optional dictionary of custom attributes for advanced targeting + (e.g., {'licenseType': 'physician', 'jurisdiction': 'oh'}) + """ + + user_id: str | None = None + custom_attributes: dict[str, Any] | None = None + + def to_dict(self) -> dict[str, Any]: + """ + Convert the context to a dictionary for API serialization. + + :return: Dictionary representation of the context, excluding None values + """ + result = {} + if self.user_id is not None: + result['userId'] = self.user_id + if self.custom_attributes: + result['customAttributes'] = self.custom_attributes + return result + + +def is_feature_enabled( + flag_name: str, context: FeatureFlagContext | None = None, fail_open: bool = False +) -> bool: + """ + Check if a feature flag is enabled. + + This function calls the internal feature flag API endpoint to determine + if a feature flag is enabled for the given context. + + :param flag_name: The name of the feature flag to check + :param context: Optional FeatureFlagContext for feature flag evaluation + :param fail_open: If True, return True on errors; if False, return False on errors (default: False) + :return: True if the feature flag is enabled, False otherwise (or fail_open value on error) + + Example: + # Simple check without context + is_feature_enabled('test-feature') + True + + # Check with user ID + is_feature_enabled( + 'test-feature', + context=FeatureFlagContext(user_id='user123') + ) + False + + # Check with user ID and custom attributes + is_feature_enabled( + 'test-feature', + context=FeatureFlagContext( + user_id='user456', + custom_attributes={'licenseType': 'lpc', 'jurisdiction': 'oh'} + ) + ) + True + + # Fail open - if API fails, allow access + is_feature_enabled('critical-feature', fail_open=True) + + # Fail closed - if API fails, deny access (default) + is_feature_enabled('new-feature', fail_open=False) + """ + try: + api_base_url = _get_api_base_url() + endpoint_url = f'{api_base_url}/v1/flags/check' + + # Build request payload + payload = {'flagName': flag_name} + if context: + payload['context'] = context.to_dict() + + response = requests.post( + endpoint_url, + json=payload, + timeout=5, + headers={'Content-Type': 'application/json'}, + ) + + # Raise exception for HTTP errors (4xx, 5xx) + response.raise_for_status() + + # Parse response + response_data = response.json() + + # Extract and return the 'enabled' field + if 'enabled' not in response_data: + logger.info('Invalid response format - return fail_open value', response_data=response_data) + # Invalid response format - return fail_open value + return fail_open + + return bool(response_data['enabled']) + + except Exception as e: + # Any error (timeout, network, parsing, etc.) - return fail_open value + logger.info('Error checking feature flag - return fail_open value', exc_info=e) + return fail_open + + +def _get_api_base_url() -> str: + """ + Get the API base URL from environment variables. + + :return: The base URL for the API + :raises ValueError: If API_BASE_URL is not set + """ + api_base_url = config.api_base_url + # Remove trailing slash if present + return api_base_url.rstrip('/') + diff --git a/backend/compact-connect/lambdas/python/common/tests/__init__.py b/backend/compact-connect/lambdas/python/common/tests/__init__.py index 35625493a..4c47769cf 100644 --- a/backend/compact-connect/lambdas/python/common/tests/__init__.py +++ b/backend/compact-connect/lambdas/python/common/tests/__init__.py @@ -13,6 +13,7 @@ def setUpClass(cls): { # Set to 'true' to enable debug logging 'DEBUG': 'false', + 'API_BASE_URL': 'https://api.example.com', 'ALLOWED_ORIGINS': '["https://example.org", "http://localhost:1234"]', 'AWS_DEFAULT_REGION': 'us-east-1', 'BULK_BUCKET_NAME': 'cc-license-data-bulk-bucket', diff --git a/backend/compact-connect/lambdas/python/common/tests/unit/test_feature_flag_client.py b/backend/compact-connect/lambdas/python/common/tests/unit/test_feature_flag_client.py new file mode 100644 index 000000000..6440f8412 --- /dev/null +++ b/backend/compact-connect/lambdas/python/common/tests/unit/test_feature_flag_client.py @@ -0,0 +1,236 @@ +from unittest.mock import MagicMock, patch + +from tests import TstLambdas + + +class TestFeatureFlagClient(TstLambdas): + + def test_is_feature_enabled_returns_true_when_flag_enabled(self): + """Test that is_feature_enabled returns True when the API returns enabled=true.""" + from cc_common.feature_flag_client import is_feature_enabled + + # Mock successful API response with enabled=True + mock_response = MagicMock() + mock_response.json.return_value = {'enabled': True} + + with patch('cc_common.feature_flag_client.requests.post', return_value=mock_response) as mock_post: + result = is_feature_enabled('test-flag') + + # Verify the result + self.assertTrue(result) + + # Verify the API was called correctly + mock_post.assert_called_once_with( + 'https://api.example.com/v1/flags/check', + json={'flagName': 'test-flag'}, + timeout=5, + headers={'Content-Type': 'application/json'}, + ) + + def test_is_feature_enabled_returns_false_when_flag_disabled(self): + """Test that is_feature_enabled returns False when the API returns enabled=false.""" + from cc_common.feature_flag_client import is_feature_enabled + + # Mock successful API response with enabled=False + mock_response = MagicMock() + mock_response.json.return_value = {'enabled': False} + + with patch('cc_common.feature_flag_client.requests.post', return_value=mock_response): + result = is_feature_enabled('test-flag') + + # Verify the result + self.assertFalse(result) + + def test_is_feature_enabled_with_context(self): + """Test that is_feature_enabled correctly passes context to the API.""" + from cc_common.feature_flag_client import FeatureFlagContext, is_feature_enabled + + # Mock successful API response + mock_response = MagicMock() + mock_response.json.return_value = {'enabled': True} + + context = FeatureFlagContext(user_id='user123', custom_attributes={'licenseType': 'lpc'}) + + with patch('cc_common.feature_flag_client.requests.post', return_value=mock_response) as mock_post: + result = is_feature_enabled('test-flag', context=context) + + # Verify the result + self.assertTrue(result) + + # Verify the API was called with the context + mock_post.assert_called_once_with( + 'https://api.example.com/v1/flags/check', + json={'flagName': 'test-flag', 'context': {'userId': 'user123', 'customAttributes': {'licenseType': 'lpc'}}}, + timeout=5, + headers={'Content-Type': 'application/json'}, + ) + + def test_is_feature_enabled_fail_closed_on_timeout(self): + """Test that is_feature_enabled returns False (fail closed) on timeout.""" + from cc_common.feature_flag_client import is_feature_enabled + + with patch('cc_common.feature_flag_client.requests.post', side_effect=Exception('Timeout')): + result = is_feature_enabled('test-flag', fail_open=False) + + # Verify it fails closed (returns False) + self.assertFalse(result) + + def test_is_feature_enabled_fail_open_on_timeout(self): + """Test that is_feature_enabled returns True (fail open) on timeout.""" + from cc_common.feature_flag_client import is_feature_enabled + + with patch('cc_common.feature_flag_client.requests.post', side_effect=Exception('Timeout')): + result = is_feature_enabled('test-flag', fail_open=True) + + # Verify it fails open (returns True) + self.assertTrue(result) + + def test_is_feature_enabled_fail_closed_on_http_error(self): + """Test that is_feature_enabled returns False (fail closed) on HTTP error.""" + from cc_common.feature_flag_client import is_feature_enabled + + # Mock HTTP error response + mock_response = MagicMock() + mock_response.raise_for_status.side_effect = Exception('500 Server Error') + + with patch('cc_common.feature_flag_client.requests.post', return_value=mock_response): + result = is_feature_enabled('test-flag', fail_open=False) + + # Verify it fails closed (returns False) + self.assertFalse(result) + + def test_is_feature_enabled_fail_open_on_http_error(self): + """Test that is_feature_enabled returns True (fail open) on HTTP error.""" + from cc_common.feature_flag_client import is_feature_enabled + + # Mock HTTP error response + mock_response = MagicMock() + mock_response.raise_for_status.side_effect = Exception('500 Server Error') + + with patch('cc_common.feature_flag_client.requests.post', return_value=mock_response): + result = is_feature_enabled('test-flag', fail_open=True) + + # Verify it fails open (returns True) + self.assertTrue(result) + + def test_is_feature_enabled_fail_closed_on_invalid_response(self): + """Test that is_feature_enabled returns False (fail closed) when response missing 'enabled' field.""" + from cc_common.feature_flag_client import is_feature_enabled + + # Mock response with missing 'enabled' field + mock_response = MagicMock() + mock_response.json.return_value = {'some_other_field': 'value'} + mock_response.raise_for_status = MagicMock() + + with patch('cc_common.feature_flag_client.requests.post', return_value=mock_response): + result = is_feature_enabled('test-flag', fail_open=False) + + # Verify it fails closed (returns False) + self.assertFalse(result) + + def test_is_feature_enabled_fail_open_on_invalid_response(self): + """Test that is_feature_enabled returns True (fail open) when response missing 'enabled' field.""" + from cc_common.feature_flag_client import is_feature_enabled + + # Mock response with missing 'enabled' field + mock_response = MagicMock() + mock_response.json.return_value = {'some_other_field': 'value'} + mock_response.raise_for_status = MagicMock() + + with patch('cc_common.feature_flag_client.requests.post', return_value=mock_response): + result = is_feature_enabled('test-flag', fail_open=True) + + # Verify it fails open (returns True) + self.assertTrue(result) + + def test_is_feature_enabled_fail_closed_on_json_parse_error(self): + """Test that is_feature_enabled returns False (fail closed) when JSON parsing fails.""" + from cc_common.feature_flag_client import is_feature_enabled + + # Mock response with invalid JSON + mock_response = MagicMock() + mock_response.json.side_effect = ValueError('Invalid JSON') + mock_response.raise_for_status = MagicMock() + + with patch('cc_common.feature_flag_client.requests.post', return_value=mock_response): + result = is_feature_enabled('test-flag', fail_open=False) + + # Verify it fails closed (returns False) + self.assertFalse(result) + + def test_is_feature_enabled_fail_open_on_json_parse_error(self): + """Test that is_feature_enabled returns True (fail open) when JSON parsing fails.""" + from cc_common.feature_flag_client import is_feature_enabled + + # Mock response with invalid JSON + mock_response = MagicMock() + mock_response.json.side_effect = ValueError('Invalid JSON') + + with patch('cc_common.feature_flag_client.requests.post', return_value=mock_response): + result = is_feature_enabled('test-flag', fail_open=True) + + # Verify it fails open (returns True) + self.assertTrue(result) + + def test_feature_flag_context_with_user_id_only(self): + """Test FeatureFlagContext to_dict with only user_id.""" + from cc_common.feature_flag_client import FeatureFlagContext + + context = FeatureFlagContext(user_id='user123') + result = context.to_dict() + + self.assertEqual(result, {'userId': 'user123'}) + + def test_feature_flag_context_with_custom_attributes_only(self): + """Test FeatureFlagContext to_dict with only custom_attributes.""" + from cc_common.feature_flag_client import FeatureFlagContext + + context = FeatureFlagContext(custom_attributes={'licenseType': 'lpc', 'jurisdiction': 'oh'}) + result = context.to_dict() + + self.assertEqual(result, {'customAttributes': {'licenseType': 'lpc', 'jurisdiction': 'oh'}}) + + def test_feature_flag_context_with_both_fields(self): + """Test FeatureFlagContext to_dict with both user_id and custom_attributes.""" + from cc_common.feature_flag_client import FeatureFlagContext + + context = FeatureFlagContext(user_id='user456', custom_attributes={'licenseType': 'physician'}) + result = context.to_dict() + + self.assertEqual( + result, {'userId': 'user456', 'customAttributes': {'licenseType': 'physician'}} + ) + + def test_feature_flag_context_empty(self): + """Test FeatureFlagContext to_dict with no fields set.""" + from cc_common.feature_flag_client import FeatureFlagContext + + context = FeatureFlagContext() + result = context.to_dict() + + self.assertEqual(result, {}) + + def test_is_feature_enabled_with_context_user_id_only(self): + """Test that is_feature_enabled works with context containing only user_id.""" + from cc_common.feature_flag_client import FeatureFlagContext, is_feature_enabled + + # Mock successful API response + mock_response = MagicMock() + mock_response.json.return_value = {'enabled': True} + + context = FeatureFlagContext(user_id='user789') + + with patch('cc_common.feature_flag_client.requests.post', return_value=mock_response) as mock_post: + result = is_feature_enabled('test-flag', context=context) + + # Verify the result + self.assertTrue(result) + + # Verify the API was called with only userId in context + mock_post.assert_called_once_with( + 'https://api.example.com/v1/flags/check', + json={'flagName': 'test-flag', 'context': {'userId': 'user789'}}, + timeout=5, + headers={'Content-Type': 'application/json'}, + ) + diff --git a/backend/compact-connect/stacks/api_stack/v1_api/api.py b/backend/compact-connect/stacks/api_stack/v1_api/api.py index d9e33c7e4..75b6f870e 100644 --- a/backend/compact-connect/stacks/api_stack/v1_api/api.py +++ b/backend/compact-connect/stacks/api_stack/v1_api/api.py @@ -45,6 +45,10 @@ def __init__( data_event_bus = SSMParameterUtility.load_data_event_bus_from_ssm_parameter(stack) _active_compacts = persistent_stack.get_list_of_compact_abbreviations() + stack.common_env_vars.update({ + 'API_BASE_URL': f'https://{persistent_stack.api_domain_name}' + }) + read_scopes = [] write_scopes = [] admin_scopes = [] From a03d4d97926863a2d853a8da5bc6b6f32c43abde Mon Sep 17 00:00:00 2001 From: Landon Shumway Date: Wed, 1 Oct 2025 16:59:12 -0500 Subject: [PATCH 13/55] update requirements to latests --- .../compact-connect/lambdas/python/common/requirements-dev.in | 2 +- .../lambdas/python/feature-flag/requirements-dev.txt | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/backend/compact-connect/lambdas/python/common/requirements-dev.in b/backend/compact-connect/lambdas/python/common/requirements-dev.in index 2dc3299cf..6f51419dd 100644 --- a/backend/compact-connect/lambdas/python/common/requirements-dev.in +++ b/backend/compact-connect/lambdas/python/common/requirements-dev.in @@ -1,4 +1,4 @@ moto[dynamodb, s3]>=5.0.12, <6 boto3-stubs[full] Faker>=28,<29 -cryptography>=45.0.5, <46 +cryptography>=46, <47 diff --git a/backend/compact-connect/lambdas/python/feature-flag/requirements-dev.txt b/backend/compact-connect/lambdas/python/feature-flag/requirements-dev.txt index 211b62be8..0329b6161 100644 --- a/backend/compact-connect/lambdas/python/feature-flag/requirements-dev.txt +++ b/backend/compact-connect/lambdas/python/feature-flag/requirements-dev.txt @@ -4,9 +4,9 @@ # # pip-compile --no-emit-index-url compact-connect/lambdas/python/feature-flag/requirements-dev.in # -boto3==1.40.42 +boto3==1.40.43 # via moto -botocore==1.40.42 +botocore==1.40.43 # via # boto3 # moto From 536dffdfff5c0797c8c4e379c2693a9bfa00287c Mon Sep 17 00:00:00 2001 From: Landon Shumway Date: Thu, 2 Oct 2025 00:46:24 -0500 Subject: [PATCH 14/55] Add implementations to upsert feature gates in statsig --- .../feature-flag/feature_flag_client.py | 381 +++++++++- .../python/feature-flag/tests/__init__.py | 7 + .../tests/function/test_statsig_client.py | 715 +++++++++++++++++- 3 files changed, 1091 insertions(+), 12 deletions(-) diff --git a/backend/compact-connect/lambdas/python/feature-flag/feature_flag_client.py b/backend/compact-connect/lambdas/python/feature-flag/feature_flag_client.py index c7c1e5bde..2c55c2898 100644 --- a/backend/compact-connect/lambdas/python/feature-flag/feature_flag_client.py +++ b/backend/compact-connect/lambdas/python/feature-flag/feature_flag_client.py @@ -7,6 +7,7 @@ from typing import Any import boto3 +import requests from botocore.exceptions import ClientError from marshmallow import Schema, ValidationError from marshmallow.fields import Dict as DictField @@ -82,6 +83,66 @@ def check_flag(self, request: FeatureFlagRequest) -> FeatureFlagResult: :raises FeatureFlagException: If flag check fails """ + @abstractmethod + def upsert_flag(self, flag_name: str, auto_enable: bool = False, custom_attributes: dict[str, Any] | None = None) -> dict[str, Any]: + """ + Create or update a feature flag in the provider. + + In test environment: Creates a new flag if it doesn't exist. + In beta/prod: Updates existing flag to add current environment if auto_enable is True. + + :param flag_name: Name of the feature flag to create + :param auto_enable: If True, enable the flag in the current environment + :param custom_attributes: Optional custom attributes for targeting rules + :return: Dictionary containing flag data (including 'id' field) + :raises FeatureFlagException: If operation fails + """ + + @abstractmethod + def get_flag(self, flag_name: str) -> dict[str, Any] | None: + """ + Retrieve a feature flag by name. + + :param flag_name: Name of the feature flag to retrieve + :return: Flag data dictionary, or None if not found + :raises FeatureFlagException: If retrieval fails + """ + + @abstractmethod + def add_current_environment_to_flag(self, flag_id: str, flag_data: dict[str, Any]) -> bool: + """ + Add the current environment to an existing feature flag. + + :param flag_id: ID of the feature flag to update + :param flag_data: Current flag configuration + :return: True if successful + :raises FeatureFlagException: If update fails + """ + + @abstractmethod + def delete_flag(self, flag_name: str) -> bool: + """ + Delete a feature flag or remove current environment from it. + + If the flag has multiple environments, only the current environment is removed. + If the flag has only the current environment, the entire flag is deleted. + + :param flag_name: Name of the feature flag to delete + :return: True if flag was fully deleted, False if only environment was removed, None if flag doesn't exist + :raises FeatureFlagException: If operation fails + """ + + @abstractmethod + def remove_current_environment_from_flag(self, flag_id: str, flag_data: dict[str, Any]) -> bool: + """ + Remove the current environment from a feature flag. + + :param flag_id: ID of the feature flag + :param flag_data: Current flag configuration + :return: True if environment was removed, False if environment wasn't present + :raises FeatureFlagException: If operation fails + """ + def _get_secret(self, secret_name: str) -> dict[str, Any]: """ Retrieve a secret from AWS Secrets Manager and return it as a JSON object. @@ -127,6 +188,16 @@ class FeatureFlagValidationException(FeatureFlagException): STATSIG_STAGING_TIER = 'staging' STATSIG_PRODUCTION_TIER = 'production' +STATSIG_ENVIRONMENT_MAPPING = { + 'prod': STATSIG_PRODUCTION_TIER, + 'beta': STATSIG_STAGING_TIER, + 'test': STATSIG_DEVELOPMENT_TIER, +} + +# StatSig Console API configuration +STATSIG_API_BASE_URL = 'https://statsigapi.net/console/v1' +STATSIG_API_VERSION = '20240601' + class StatSigContextSchema(Schema): """ @@ -175,10 +246,15 @@ def __init__(self, environment: str): try: secret_data = self._get_secret(secret_name) self._server_secret_key = secret_data.get('serverKey') + self._console_api_key = secret_data.get('consoleKey') if not self._server_secret_key: raise FeatureFlagException(f"Secret '{secret_name}' does not contain required 'serverKey' field") + # If console API key not provided, try to get it from secret + if not self._console_api_key: + raise FeatureFlagException(f"Secret '{secret_name}' does not contain required 'consoleKey' field") + except Exception as e: if isinstance(e, FeatureFlagException): raise @@ -194,15 +270,8 @@ def _initialize_statsig(self): return try: - # Map environment tier string to StatsigEnvironmentTier enum - tier_mapping = { - 'prod': STATSIG_PRODUCTION_TIER, - 'beta': STATSIG_STAGING_TIER, - 'test': STATSIG_DEVELOPMENT_TIER, - } - # default to development for all other environments (ie sandbox environments) - tier = tier_mapping.get(self.environment.lower(), STATSIG_DEVELOPMENT_TIER) + tier = STATSIG_ENVIRONMENT_MAPPING.get(self.environment.lower(), STATSIG_DEVELOPMENT_TIER) options = StatsigOptions() options.environment = tier @@ -258,6 +327,302 @@ def check_flag(self, request: FeatureFlagRequest) -> FeatureFlagResult: # Otherwise, wrap it in a FeatureFlagException raise FeatureFlagException(f"Failed to check feature flag '{request.flagName}': {e}") from e + def _make_console_api_request( + self, method: str, endpoint: str, data: dict[str, Any] | None = None + ) -> requests.Response: + """ + Make a request to the StatSig Console API. + + :param method: HTTP method (GET, POST, PATCH, DELETE) + :param endpoint: API endpoint (e.g., '/gates') + :param data: Optional request payload + :return: Response object + :raises FeatureFlagException: If API key not configured or request fails + """ + if not self._console_api_key: + raise FeatureFlagException( + 'Console API key not configured. Required for management operations (create, update, delete).' + ) + + url = f'{STATSIG_API_BASE_URL}{endpoint}' + headers = { + 'STATSIG-API-KEY': self._console_api_key, + 'STATSIG-API-VERSION': STATSIG_API_VERSION, + 'Content-Type': 'application/json', + } + + try: + if method == 'GET': + response = requests.get(url, headers=headers, timeout=30) + elif method == 'POST': + response = requests.post(url, headers=headers, data=json.dumps(data), timeout=30) + elif method == 'PATCH': + response = requests.patch(url, headers=headers, data=json.dumps(data), timeout=30) + elif method == 'DELETE': + response = requests.delete(url, headers=headers, timeout=30) + else: + raise ValueError(f'Unsupported HTTP method: {method}') + + return response + + except requests.exceptions.RequestException as e: + raise FeatureFlagException(f'StatSig Console API request failed: {e}') from e + + def upsert_flag(self, flag_name: str, auto_enable: bool = False, custom_attributes: dict[str, Any] | None = None) -> dict[str, Any]: + """ + Create or update a feature gate in StatSig. + + In test environment: Creates a new gate if it doesn't exist. + In beta/prod: Updates existing gate to add current environment if auto_enable is True. + + :param flag_name: Name of the feature gate + :param auto_enable: If True, enable the flag in the current environment (beta/prod only) + :param custom_attributes: Optional custom attributes for targeting + :return: Flag data (with 'id' field) + :raises FeatureFlagException: If operation fails + """ + # Check if gate already exists + existing_gate = self.get_flag(flag_name) + + if self.environment.lower() == 'test': + # In test environment, create the gate if it doesn't exist + if existing_gate: + # Gate exists - update custom attributes if provided + gate_id = existing_gate.get('id') + if custom_attributes: + updated_gate = self._prepare_gate_update(existing_gate, custom_attributes, False) + self._update_gate(gate_id, updated_gate) + return existing_gate # Return existing gate data + else: + # Create new gate with development environment + return self._create_new_gate(flag_name, custom_attributes) + else: + # In beta/prod environment + if not existing_gate and not auto_enable: + # Gate doesn't exist and auto_enable is False - return empty dict to signal no action + return {} + elif not existing_gate and auto_enable: + # Gate doesn't exist but auto_enable is True - create it + return self._create_new_gate(flag_name, custom_attributes) + else: + # Gate exists - update it + gate_id = existing_gate.get('id') + + # Update the gate with new attributes and/or environment + updated_gate = self._prepare_gate_update(existing_gate, custom_attributes, auto_enable) + self._update_gate(gate_id, updated_gate) + + # Return updated gate data + return self.get_flag(flag_name) or existing_gate + + def _create_new_gate(self, flag_name: str, custom_attributes: dict[str, Any] | None = None) -> dict[str, Any]: + """ + Create a new feature gate in StatSig with the current environment enabled. + + :param flag_name: Name of the feature gate + :param custom_attributes: Optional custom attributes for targeting + :return: Created gate data (with 'id' field) + :raises FeatureFlagException: If creation fails + """ + # Get the StatSig environment tier for the current environment + statsig_tier = STATSIG_ENVIRONMENT_MAPPING.get(self.environment.lower(), STATSIG_DEVELOPMENT_TIER) + + # Build conditions for custom attributes if provided + conditions = [] + if custom_attributes: + for key, value in custom_attributes.items(): + conditions.append({'type': 'custom_field', 'targetValue': [value], 'field': key, 'operator': 'any'}) + + # Build the feature gate payload + gate_payload = { + 'name': flag_name, + 'description': f'Feature gate managed by CDK for {flag_name} feature', + 'isEnabled': True, + 'rules': [ + { + 'name': 'environment_toggle', + 'conditions': conditions, + 'environments': [statsig_tier], + 'passPercentage': 100, + } + ], + } + + response = self._make_console_api_request('POST', '/gates', gate_payload) + + if response.status_code in [200, 201]: + return response.json() + else: + raise FeatureFlagException( + f'Failed to create feature gate: {response.status_code} - {response.text[:200]}' + ) + + def _prepare_gate_update(self, gate_data: dict[str, Any], custom_attributes: dict[str, Any] | None = None, add_current_env: bool = False) -> dict[str, Any]: + """ + Prepare an updated gate configuration with new custom attributes and/or environment. + + :param gate_data: Original gate configuration + :param custom_attributes: New custom attributes to set (None = no change) + :param add_current_env: Whether to add the current environment + :return: Updated gate configuration + """ + updated_gate = gate_data.copy() + + # Find the environment_toggle rule + for rule in updated_gate.get('rules', []): + if rule.get('name') == 'environment_toggle': + # Update custom attributes if provided + if custom_attributes is not None: + new_conditions = [] + for key, value in custom_attributes.items(): + new_conditions.append({'type': 'custom_field', 'targetValue': [value], 'field': key, 'operator': 'any'}) + rule['conditions'] = new_conditions + + # Add current environment if requested + if add_current_env: + statsig_tier = STATSIG_ENVIRONMENT_MAPPING.get(self.environment.lower(), STATSIG_DEVELOPMENT_TIER) + current_environments = rule.get('environments', []) + if statsig_tier not in current_environments: + rule['environments'] = current_environments + [statsig_tier] + break + + return updated_gate + + def _update_gate(self, gate_id: str, gate_data: dict[str, Any]) -> bool: + """ + Update a feature gate using the PATCH endpoint. + + :param gate_id: ID of the feature gate to update + :param gate_data: Updated gate configuration + :return: True if successful + :raises FeatureFlagException: If update fails + """ + response = self._make_console_api_request('PATCH', f'/gates/{gate_id}', gate_data) + + if response.status_code in [200, 204]: + return True + else: + raise FeatureFlagException( + f'Failed to update feature gate: {response.status_code} - {response.text[:200]}' + ) + + def get_flag(self, flag_name: str) -> dict[str, Any] | None: + """ + Retrieve a feature gate by name. + + :param flag_name: Name of the feature gate to retrieve + :return: Gate data dictionary, or None if not found + :raises FeatureFlagException: If retrieval fails + """ + response = self._make_console_api_request('GET', '/gates') + + if response.status_code == 200: + gates_data = response.json() + + for gate in gates_data.get('data', []): + if gate.get('name') == flag_name: + return gate + + return None + else: + raise FeatureFlagException(f'Failed to fetch gates: {response.status_code} - {response.text[:200]}') + + def add_current_environment_to_flag(self, flag_id: str, flag_data: dict[str, Any]) -> bool: + """ + Add the current environment to an existing feature gate. + + :param flag_id: ID of the feature gate to update + :param flag_data: Current gate configuration + :return: True if successful + :raises FeatureFlagException: If update fails + """ + updated_gate = self._prepare_gate_update(flag_data, None, add_current_env=True) + return self._update_gate(flag_id, updated_gate) + + def delete_flag(self, flag_name: str) -> bool | None: + """ + Delete a feature gate or remove current environment from it. + + If the gate has multiple environments, only the current environment is removed. + If the gate has only the current environment, the entire gate is deleted. + + :param flag_name: Name of the feature flag to delete + :return: True if flag was fully deleted, False if only environment was removed, None if flag doesn't exist + :raises FeatureFlagException: If operation fails + """ + # Get the flag data first + flag_data = self.get_flag(flag_name) + if not flag_data: + return None # Flag doesn't exist + + flag_id = flag_data.get('id') + if not flag_id: + raise FeatureFlagException(f'Flag data missing ID field: {flag_name}') + + # Get the StatSig environment tier for the current environment + statsig_tier = STATSIG_ENVIRONMENT_MAPPING[self.environment.lower()] + + # Find the environment_toggle rule and check environment count + environments_in_flag = [] + for rule in flag_data.get('rules', []): + if rule.get('name') == 'environment_toggle': + environments_in_flag = rule.get('environments', []) + break + + # Check if current environment is the only one (or one of only one) + if len(environments_in_flag) <= 1 and statsig_tier in environments_in_flag: + # Delete the entire gate + response = self._make_console_api_request('DELETE', f'/gates/{flag_id}') + + if response.status_code in [200, 204]: + return True # Flag fully deleted + else: + raise FeatureFlagException( + f'Failed to delete feature gate: {response.status_code} - {response.text[:200]}' + ) + else: + # Remove only the current environment + removed = self.remove_current_environment_from_flag(flag_id, flag_data) + return False if removed else False # Environment removed, not full deletion + + def remove_current_environment_from_flag(self, flag_id: str, flag_data: dict[str, Any]) -> bool: + """ + Remove the current environment from a feature gate. + + :param flag_id: ID of the feature gate + :param flag_data: Current flag configuration + :return: True if environment was removed, False if it wasn't present + :raises FeatureFlagException: If operation fails + """ + # Get the StatSig environment tier for the current environment + statsig_tier = STATSIG_ENVIRONMENT_MAPPING.get(self.environment.lower(), STATSIG_DEVELOPMENT_TIER) + + # Check if current environment is present + environment_present = False + for rule in flag_data.get('rules', []): + if rule.get('name') == 'environment_toggle': + current_environments = rule.get('environments', []) + if statsig_tier in current_environments: + environment_present = True + break + + if not environment_present: + return False + + # Prepare updated gate with environment removed + updated_gate = flag_data.copy() + for rule in updated_gate.get('rules', []): + if rule.get('name') == 'environment_toggle': + current_environments = rule.get('environments', []) + if statsig_tier in current_environments: + current_environments.remove(statsig_tier) + rule['environments'] = current_environments + break + + # Update the gate + self._update_gate(flag_id, updated_gate) + return True + def _shutdown(self): """ Shutdown the StatSig client to flush event logs to statsig. diff --git a/backend/compact-connect/lambdas/python/feature-flag/tests/__init__.py b/backend/compact-connect/lambdas/python/feature-flag/tests/__init__.py index b20364b37..4f97b27ab 100644 --- a/backend/compact-connect/lambdas/python/feature-flag/tests/__init__.py +++ b/backend/compact-connect/lambdas/python/feature-flag/tests/__init__.py @@ -15,6 +15,7 @@ def setUpClass(cls): 'DEBUG': 'true', 'ALLOWED_ORIGINS': '["https://example.org"]', 'AWS_DEFAULT_REGION': 'us-east-1', + 'ENVIRONMENT_NAME': 'test', 'COMPACTS': '["aslp", "octp", "coun"]', 'JURISDICTIONS': json.dumps( [ @@ -75,4 +76,10 @@ def setUpClass(cls): ), }, ) + # Monkey-patch config object to be sure we have it based + # on the env vars we set above + import cc_common.config + + cls.config = cc_common.config._Config() # noqa: SLF001 protected-access + cc_common.config.config = cls.config cls.mock_context = MagicMock(name='MockLambdaContext', spec=LambdaContext) diff --git a/backend/compact-connect/lambdas/python/feature-flag/tests/function/test_statsig_client.py b/backend/compact-connect/lambdas/python/feature-flag/tests/function/test_statsig_client.py index aab131fa6..303cc615d 100644 --- a/backend/compact-connect/lambdas/python/feature-flag/tests/function/test_statsig_client.py +++ b/backend/compact-connect/lambdas/python/feature-flag/tests/function/test_statsig_client.py @@ -13,6 +13,10 @@ from . import TstFunction MOCK_SERVER_KEY = 'test-server-key-123' +MOCK_CONSOLE_KEY = 'test-console-key-456' + +STATSIG_API_BASE_URL = 'https://statsigapi.net/console/v1' +STATSIG_API_VERSION = '20240601' @mock_aws @@ -24,9 +28,14 @@ def setUp(self): # Set up mock secrets manager with StatSig credentials secrets_client = self.create_mock_secrets_manager() - secrets_client.create_secret( - Name='compact-connect/env/test/statsig/credentials', SecretString=json.dumps({'serverKey': MOCK_SERVER_KEY}) - ) + for env in ['test', 'prod']: + secrets_client.create_secret( + Name=f'compact-connect/env/{env}/statsig/credentials', + SecretString=json.dumps({ + 'serverKey': MOCK_SERVER_KEY, + 'consoleKey': MOCK_CONSOLE_KEY + }) + ) def create_mock_secrets_manager(self): """Create a mock secrets manager client""" @@ -230,7 +239,7 @@ def test_environment_tier_mapping(self, mock_statsig): if cc_env != 'test': secrets_client.create_secret( Name=f'compact-connect/env/{cc_env}/statsig/credentials', - SecretString=json.dumps({'serverKey': MOCK_SERVER_KEY}), + SecretString=json.dumps({'serverKey': MOCK_SERVER_KEY, 'consoleKey': MOCK_CONSOLE_KEY}), ) # Create client @@ -246,3 +255,701 @@ def test_environment_tier_mapping(self, mock_statsig): self.assertEqual(expected_tier, options.environment) mock_statsig.reset_mock() + + def _create_mock_response(self, status_code: int, json_data: dict = None): + """Create a mock requests response""" + mock_response = MagicMock() + mock_response.status_code = status_code + mock_response.json.return_value = json_data or {} + mock_response.text = json.dumps(json_data) if json_data else '' + return mock_response + + @patch('feature_flag_client.Statsig') + @patch('feature_flag_client.requests') + def test_upsert_flag_create_new_in_test_environment(self, mock_requests, mock_statsig): + """Test creating a new flag in test environment""" + self._setup_mock_statsig(mock_statsig) + + # Mock GET request (flag doesn't exist) + mock_requests.get.return_value = self._create_mock_response(200, {'data': []}) + + # Mock POST request (create flag) + created_flag = { + 'id': 'gate-123', + 'name': 'new-test-flag', + 'data': {'id': 'gate-123'} + } + mock_requests.post.return_value = self._create_mock_response(201, created_flag) + + client = StatSigFeatureFlagClient(environment='test') + + result = client.upsert_flag('new-test-flag', auto_enable=False, custom_attributes={'region': 'us-east-1'}) + + # Verify result + self.assertEqual(result['id'], 'gate-123') + self.assertEqual(result['name'], 'new-test-flag') + + # Verify API calls + mock_requests.get.assert_called_once() + # Verify POST payload + mock_requests.post.assert_called_once_with( + f'{STATSIG_API_BASE_URL}/gates', + headers={'STATSIG-API-KEY': MOCK_CONSOLE_KEY, + 'STATSIG-API-VERSION': STATSIG_API_VERSION, + 'Content-Type': 'application/json'}, + data=json.dumps({ + "name": "new-test-flag", + "description": "Feature gate managed by CDK for new-test-flag feature", + "isEnabled": True, "rules": [{"name": "environment_toggle", + "conditions": [{"type": "custom_field", "targetValue": ["us-east-1"], + "field": "region", "operator": "any"}], "environments": ["development"], + "passPercentage": 100}]}), + timeout=30 + ) + + + + + @patch('feature_flag_client.Statsig') + @patch('feature_flag_client.requests') + def test_upsert_flag_create_new_in_test_environment_no_attributes(self, mock_requests, mock_statsig): + """Test creating a new flag in test environment without custom attributes""" + self._setup_mock_statsig(mock_statsig) + + # Mock GET request (flag doesn't exist) + mock_requests.get.return_value = self._create_mock_response(200, {'data': []}) + + # Mock POST request (create flag) + created_flag = { + 'id': 'gate-456', + 'name': 'simple-flag', + 'data': {'id': 'gate-456'} + } + mock_requests.post.return_value = self._create_mock_response(201, created_flag) + + client = StatSigFeatureFlagClient(environment='test') + + result = client.upsert_flag('simple-flag') + + # Verify result + self.assertEqual(result['id'], 'gate-456') + + # Verify API calls + mock_requests.get.assert_called_once() + mock_requests.post.assert_called_once_with( + f'{STATSIG_API_BASE_URL}/gates', + headers={'STATSIG-API-KEY': MOCK_CONSOLE_KEY, + 'STATSIG-API-VERSION': STATSIG_API_VERSION, + 'Content-Type': 'application/json'}, + data=json.dumps({ + "name": "simple-flag", + "description": "Feature gate managed by CDK for simple-flag feature", + "isEnabled": True, + "rules": [{"name": "environment_toggle", + "conditions": [], + "environments": ["development"], + "passPercentage": 100}] + }), + timeout=30 + ) + + @patch('feature_flag_client.Statsig') + @patch('feature_flag_client.requests') + def test_upsert_flag_update_existing_in_test_environment(self, mock_requests, mock_statsig): + """Test updating an existing flag in test environment""" + self._setup_mock_statsig(mock_statsig) + + existing_flag = { + 'id': 'gate-789', + 'name': 'existing-flag', + 'rules': [{ + 'name': 'environment_toggle', + 'conditions': [{'field': 'old_attr', 'targetValue': ['old_value']}], + 'environments': ['development'] + }] + } + + # Mock GET requests (flag exists, then return updated flag) + mock_requests.get.side_effect = [ + self._create_mock_response(200, {'data': [existing_flag]}), # First call to check existence + self._create_mock_response(200, {'data': [existing_flag]}) # Second call after update + ] + + # Mock PATCH request (update flag) + mock_requests.patch.return_value = self._create_mock_response(200) + + client = StatSigFeatureFlagClient(environment='test') + + result = client.upsert_flag('existing-flag', custom_attributes={'new_attr': 'new_value'}) + + # Verify result + self.assertEqual(result['id'], 'gate-789') + + # Verify API calls + self.assertEqual(1, mock_requests.get.call_count) + mock_requests.patch.assert_called_once_with( + f'{STATSIG_API_BASE_URL}/gates/gate-789', + headers={'STATSIG-API-KEY': MOCK_CONSOLE_KEY, + 'STATSIG-API-VERSION': STATSIG_API_VERSION, + 'Content-Type': 'application/json'}, + data=json.dumps({ + 'id': 'gate-789', + 'name': 'existing-flag', + 'rules': [{ + 'name': 'environment_toggle', + 'conditions': [{'type': 'custom_field', 'targetValue': ['new_value'], 'field': 'new_attr', 'operator': 'any'}], + 'environments': ['development'] + }] + }), + timeout=30 + ) + + @patch('feature_flag_client.Statsig') + @patch('feature_flag_client.requests') + def test_upsert_flag_existing_in_test_no_changes(self, mock_requests, mock_statsig): + """Test updating an existing flag in test environment with no custom attributes""" + self._setup_mock_statsig(mock_statsig) + + existing_flag = { + 'id': 'gate-unchanged', + 'name': 'unchanged-flag', + 'rules': [{ + 'name': 'environment_toggle', + 'conditions': [], + 'environments': ['development'] + }] + } + + # Mock GET request (flag exists) + mock_requests.get.return_value = self._create_mock_response(200, {'data': [existing_flag]}) + + client = StatSigFeatureFlagClient(environment='test') + + result = client.upsert_flag('unchanged-flag') + + # Verify result + self.assertEqual(result['id'], 'gate-unchanged') + + # Verify no PATCH was called since no changes + mock_requests.get.assert_called_once() + mock_requests.patch.assert_not_called() + mock_requests.post.assert_not_called() + + @patch('feature_flag_client.Statsig') + @patch('feature_flag_client.requests') + def test_upsert_flag_prod_environment_auto_enable_false_no_existing_flag(self, mock_requests, mock_statsig): + """Test upsert in prod environment with autoEnable=False and no existing flag""" + self._setup_mock_statsig(mock_statsig) + + # Mock GET request (flag doesn't exist) + mock_requests.get.return_value = self._create_mock_response(200, {'data': []}) + + client = StatSigFeatureFlagClient(environment='prod') + + result = client.upsert_flag('prod-flag', auto_enable=False) + + # Should return empty dict (no action taken) + self.assertEqual(result, {}) + + # Should only call GET, not POST + mock_requests.get.assert_called_once() + mock_requests.post.assert_not_called() + + @patch('feature_flag_client.Statsig') + @patch('feature_flag_client.requests') + def test_upsert_flag_prod_environment_auto_enable_true_no_existing_flag(self, mock_requests, mock_statsig): + """Test upsert in prod environment with autoEnable=True and no existing flag""" + self._setup_mock_statsig(mock_statsig) + + # Mock GET request (flag doesn't exist) + mock_requests.get.return_value = self._create_mock_response(200, {'data': []}) + + # Mock POST request (create flag) + created_flag = { + 'id': 'gate-prod', + 'name': 'prod-flag', + 'data': {'id': 'gate-prod'} + } + mock_requests.post.return_value = self._create_mock_response(201, created_flag) + + client = StatSigFeatureFlagClient(environment='prod') + + result = client.upsert_flag('prod-flag', auto_enable=True) + + # Verify result + self.assertEqual(result['id'], 'gate-prod') + + # Verify API calls + mock_requests.get.assert_called_once() + mock_requests.post.assert_called_once_with( + f'{STATSIG_API_BASE_URL}/gates', + headers={'STATSIG-API-KEY': MOCK_CONSOLE_KEY, + 'STATSIG-API-VERSION': STATSIG_API_VERSION, + 'Content-Type': 'application/json'}, + data=json.dumps({ + "name": "prod-flag", + "description": "Feature gate managed by CDK for prod-flag feature", + "isEnabled": True, + "rules": [{"name": "environment_toggle", + "conditions": [], + "environments": ["production"], + "passPercentage": 100}] + }), + timeout=30 + ) + + @patch('feature_flag_client.Statsig') + @patch('feature_flag_client.requests') + def test_upsert_flag_prod_environment_existing_flag_auto_enable_true(self, mock_requests, mock_statsig): + """Test upsert in prod environment with existing flag and autoEnable=True""" + self._setup_mock_statsig(mock_statsig) + + existing_flag = { + 'id': 'gate-existing-prod', + 'name': 'existing-prod-flag', + 'rules': [{ + 'name': 'environment_toggle', + 'conditions': [], + 'environments': ['development'] # Only has development + }] + } + + # Mock GET requests (flag exists, then return updated flag) + mock_requests.get.side_effect = [ + self._create_mock_response(200, {'data': [existing_flag]}), # First call to check existence + self._create_mock_response(200, {'data': [existing_flag]}) # Second call after update + ] + + # Mock PATCH request (update flag) + mock_requests.patch.return_value = self._create_mock_response(200) + + client = StatSigFeatureFlagClient(environment='prod') + + result = client.upsert_flag('existing-prod-flag', auto_enable=True, custom_attributes={'env': 'prod'}) + + # Verify result + self.assertEqual(result['id'], 'gate-existing-prod') + + # Verify API calls + self.assertEqual(mock_requests.get.call_count, 2) # Once to check, once to return updated + mock_requests.patch.assert_called_once_with( + f'{STATSIG_API_BASE_URL}/gates/gate-existing-prod', + headers={'STATSIG-API-KEY': MOCK_CONSOLE_KEY, + 'STATSIG-API-VERSION': STATSIG_API_VERSION, + 'Content-Type': 'application/json'}, + data=json.dumps({ + 'id': 'gate-existing-prod', + 'name': 'existing-prod-flag', + 'rules': [{ + 'name': 'environment_toggle', + 'conditions': [{'type': 'custom_field', 'targetValue': ['prod'], 'field': 'env', 'operator': 'any'}], + 'environments': ['development', 'production'] + }] + }), + timeout=30 + ) + + @patch('feature_flag_client.Statsig') + @patch('feature_flag_client.requests') + def test_upsert_flag_prod_environment_existing_flag_auto_enable_false(self, mock_requests, mock_statsig): + """Test upsert in prod environment with existing flag and autoEnable=False""" + self._setup_mock_statsig(mock_statsig) + + existing_flag = { + 'id': 'gate-existing-prod-2', + 'name': 'existing-prod-flag-2', + 'rules': [{ + 'name': 'environment_toggle', + 'conditions': [{'field': 'old', 'targetValue': ['value']}], + 'environments': ['development', 'production'] # Already has production + }] + } + + # Mock GET requests (flag exists, then return updated flag) + mock_requests.get.side_effect = [ + self._create_mock_response(200, {'data': [existing_flag]}), # First call to check existence + self._create_mock_response(200, {'data': [existing_flag]}) # Second call after update + ] + + # Mock PATCH request (update flag) + mock_requests.patch.return_value = self._create_mock_response(200) + + client = StatSigFeatureFlagClient(environment='prod') + + result = client.upsert_flag('existing-prod-flag-2', auto_enable=False, custom_attributes={'new': 'attr'}) + + # Verify result + self.assertEqual(result['id'], 'gate-existing-prod-2') + + # Verify API calls + self.assertEqual(mock_requests.get.call_count, 2) # Once to check, once to return updated + mock_requests.patch.assert_called_once_with( + f'{STATSIG_API_BASE_URL}/gates/gate-existing-prod-2', + headers={'STATSIG-API-KEY': MOCK_CONSOLE_KEY, + 'STATSIG-API-VERSION': STATSIG_API_VERSION, + 'Content-Type': 'application/json'}, + data=json.dumps({ + 'id': 'gate-existing-prod-2', + 'name': 'existing-prod-flag-2', + 'rules': [{ + 'name': 'environment_toggle', + 'conditions': [{'type': 'custom_field', 'targetValue': ['attr'], 'field': 'new', 'operator': 'any'}], + 'environments': ['development', 'production'] + }] + }), + timeout=30 + ) + + @patch('feature_flag_client.Statsig') + @patch('feature_flag_client.requests') + def test_upsert_flag_api_error_handling(self, mock_requests, mock_statsig): + """Test error handling when StatSig API returns errors""" + self._setup_mock_statsig(mock_statsig) + + # Mock GET request failure + mock_requests.get.return_value = self._create_mock_response(500, {'error': 'Internal server error'}) + + client = StatSigFeatureFlagClient(environment='test') + + with self.assertRaises(FeatureFlagException) as context: + client.upsert_flag('error-flag') + + self.assertIn('Failed to fetch gates', str(context.exception)) + + @patch('feature_flag_client.Statsig') + @patch('feature_flag_client.requests') + def test_upsert_flag_create_api_error_raises_exception(self, mock_requests, mock_statsig): + """Test error handling when flag creation fails""" + self._setup_mock_statsig(mock_statsig) + + # Mock GET request (flag doesn't exist) + mock_requests.get.return_value = self._create_mock_response(200, {'data': []}) + + # Mock POST request failure + mock_requests.post.return_value = self._create_mock_response(400, {'error': 'Bad request'}) + + client = StatSigFeatureFlagClient(environment='test') + + with self.assertRaises(FeatureFlagException) as context: + client.upsert_flag('create-error-flag') + + self.assertIn('Failed to create feature gate', str(context.exception)) + + @patch('feature_flag_client.Statsig') + @patch('feature_flag_client.requests') + def test_upsert_flag_update_api_error_raises_exception(self, mock_requests, mock_statsig): + """Test error handling when flag update fails""" + self._setup_mock_statsig(mock_statsig) + + existing_flag = { + 'id': 'gate-update-error', + 'name': 'update-error-flag', + 'rules': [{ + 'name': 'environment_toggle', + 'conditions': [], + 'environments': ['development'] + }] + } + + # Mock GET request (flag exists) + mock_requests.get.return_value = self._create_mock_response(200, {'data': [existing_flag]}) + + # Mock PATCH request failure + mock_requests.patch.return_value = self._create_mock_response(403, {'error': 'Forbidden'}) + + client = StatSigFeatureFlagClient(environment='test') + + with self.assertRaises(FeatureFlagException) as context: + client.upsert_flag('update-error-flag', custom_attributes={'test': 'value'}) + + self.assertIn('Failed to update feature gate', str(context.exception)) + + @patch('feature_flag_client.Statsig') + @patch('feature_flag_client.requests') + def test_delete_flag_not_found(self, mock_requests, mock_statsig): + """Test delete_flag when flag doesn't exist""" + self._setup_mock_statsig(mock_statsig) + + # Mock GET request (flag doesn't exist) + mock_requests.get.return_value = self._create_mock_response(200, {'data': []}) + + client = StatSigFeatureFlagClient(environment='test') + + result = client.delete_flag('nonexistent-flag') + + # Should return None (flag doesn't exist) + self.assertIsNone(result) + + # Should only call GET, not DELETE or PATCH + mock_requests.get.assert_called_once() + mock_requests.delete.assert_not_called() + mock_requests.patch.assert_not_called() + + @patch('feature_flag_client.Statsig') + @patch('feature_flag_client.requests') + def test_delete_flag_last_environment_deletes_entire_flag(self, mock_requests, mock_statsig): + """Test delete_flag when current environment is the only one - should delete entire flag""" + self._setup_mock_statsig(mock_statsig) + + existing_flag = { + 'id': 'gate-delete-last', + 'name': 'delete-last-flag', + 'rules': [{ + 'name': 'environment_toggle', + 'conditions': [], + 'environments': ['development'] # Only current environment (test -> development) + }] + } + + # Mock GET request (flag exists with only current environment) + mock_requests.get.return_value = self._create_mock_response(200, {'data': [existing_flag]}) + + # Mock DELETE request (delete entire flag) + mock_requests.delete.return_value = self._create_mock_response(200) + + client = StatSigFeatureFlagClient(environment='test') + + result = client.delete_flag('delete-last-flag') + + # Should return True (flag fully deleted) + self.assertTrue(result) + + # Verify API calls + mock_requests.get.assert_called_once() + mock_requests.delete.assert_called_once_with( + f'{STATSIG_API_BASE_URL}/gates/gate-delete-last', + headers={'STATSIG-API-KEY': MOCK_CONSOLE_KEY, + 'STATSIG-API-VERSION': STATSIG_API_VERSION, + 'Content-Type': 'application/json'}, + timeout=30 + ) + mock_requests.patch.assert_not_called() + + @patch('feature_flag_client.Statsig') + @patch('feature_flag_client.requests') + def test_delete_flag_multiple_environments_removes_current_only(self, mock_requests, mock_statsig): + """Test delete_flag when flag has multiple environments - should only remove current""" + self._setup_mock_statsig(mock_statsig) + + existing_flag = { + 'id': 'gate-delete-multi', + 'name': 'delete-multi-flag', + 'rules': [{ + 'name': 'environment_toggle', + 'conditions': [], + 'environments': ['development', 'staging', 'production'] # Multiple environments + }] + } + + # Mock GET request (flag exists with multiple environments) + mock_requests.get.return_value = self._create_mock_response(200, {'data': [existing_flag]}) + + # Mock PATCH request (remove environment) + mock_requests.patch.return_value = self._create_mock_response(200) + + client = StatSigFeatureFlagClient(environment='test') # test -> development + + result = client.delete_flag('delete-multi-flag') + + # Should return False (environment removed, not full deletion) + self.assertFalse(result) + + # Verify API calls + mock_requests.get.assert_called_once() + mock_requests.patch.assert_called_once_with( + f'{STATSIG_API_BASE_URL}/gates/gate-delete-multi', + headers={'STATSIG-API-KEY': MOCK_CONSOLE_KEY, + 'STATSIG-API-VERSION': STATSIG_API_VERSION, + 'Content-Type': 'application/json'}, + data=json.dumps({ + 'id': 'gate-delete-multi', + 'name': 'delete-multi-flag', + 'rules': [{ + 'name': 'environment_toggle', + 'conditions': [], + 'environments': ['staging', 'production'] # development removed + }] + }), + timeout=30 + ) + mock_requests.delete.assert_not_called() + + @patch('feature_flag_client.Statsig') + @patch('feature_flag_client.requests') + def test_delete_flag_prod_environment_last_environment(self, mock_requests, mock_statsig): + """Test delete_flag in prod environment when it's the last environment""" + self._setup_mock_statsig(mock_statsig) + + existing_flag = { + 'id': 'gate-delete-prod', + 'name': 'delete-prod-flag', + 'rules': [{ + 'name': 'environment_toggle', + 'conditions': [], + 'environments': ['production'] # Only production environment + }] + } + + # Mock GET request (flag exists with only production environment) + mock_requests.get.return_value = self._create_mock_response(200, {'data': [existing_flag]}) + + # Mock DELETE request (delete entire flag) + mock_requests.delete.return_value = self._create_mock_response(200) + + client = StatSigFeatureFlagClient(environment='prod') + + result = client.delete_flag('delete-prod-flag') + + # Should return True (flag fully deleted) + self.assertTrue(result) + + # Verify API calls + mock_requests.get.assert_called_once() + mock_requests.delete.assert_called_once_with( + f'{STATSIG_API_BASE_URL}/gates/gate-delete-prod', + headers={'STATSIG-API-KEY': MOCK_CONSOLE_KEY, + 'STATSIG-API-VERSION': STATSIG_API_VERSION, + 'Content-Type': 'application/json'}, + timeout=30 + ) + + @patch('feature_flag_client.Statsig') + @patch('feature_flag_client.requests') + def test_delete_flag_prod_environment_multiple_environments(self, mock_requests, mock_statsig): + """Test delete_flag in prod environment when multiple environments exist""" + self._setup_mock_statsig(mock_statsig) + + existing_flag = { + 'id': 'gate-delete-prod-multi', + 'name': 'delete-prod-multi-flag', + 'rules': [{ + 'name': 'environment_toggle', + 'conditions': [{'type': 'custom_field', 'targetValue': ['value'], 'field': 'attr', 'operator': 'any'}], + 'environments': ['development', 'production'] # Multiple environments + }] + } + + # Mock GET request (flag exists with multiple environments) + mock_requests.get.return_value = self._create_mock_response(200, {'data': [existing_flag]}) + + # Mock PATCH request (remove production environment) + mock_requests.patch.return_value = self._create_mock_response(200) + + client = StatSigFeatureFlagClient(environment='prod') + + result = client.delete_flag('delete-prod-multi-flag') + + # Should return False (environment removed, not full deletion) + self.assertFalse(result) + + # Verify API calls + mock_requests.get.assert_called_once() + mock_requests.patch.assert_called_once_with( + f'{STATSIG_API_BASE_URL}/gates/gate-delete-prod-multi', + headers={'STATSIG-API-KEY': MOCK_CONSOLE_KEY, + 'STATSIG-API-VERSION': STATSIG_API_VERSION, + 'Content-Type': 'application/json'}, + data=json.dumps({ + 'id': 'gate-delete-prod-multi', + 'name': 'delete-prod-multi-flag', + 'rules': [{ + 'name': 'environment_toggle', + 'conditions': [{'type': 'custom_field', 'targetValue': ['value'], 'field': 'attr', 'operator': 'any'}], + 'environments': ['development'] # production removed + }] + }), + timeout=30 + ) + + @patch('feature_flag_client.Statsig') + @patch('feature_flag_client.requests') + def test_delete_flag_current_environment_not_present(self, mock_requests, mock_statsig): + """Test delete_flag when current environment is not in the flag's environments""" + self._setup_mock_statsig(mock_statsig) + + existing_flag = { + 'id': 'gate-delete-not-present', + 'name': 'delete-not-present-flag', + 'rules': [{ + 'name': 'environment_toggle', + 'conditions': [], + 'environments': ['staging', 'production'] # Current environment (development) not present + }] + } + + # Mock GET request (flag exists but current environment not in it) + mock_requests.get.return_value = self._create_mock_response(200, {'data': [existing_flag]}) + + # Mock PATCH request (no change needed, but method still called) + mock_requests.patch.return_value = self._create_mock_response(200) + + client = StatSigFeatureFlagClient(environment='test') # test -> development + + result = client.delete_flag('delete-not-present-flag') + + # Should return False (no environment removed since it wasn't there) + self.assertFalse(result) + + # Should not call PATCH since environment wasn't present + mock_requests.get.assert_called_once() + mock_requests.patch.assert_not_called() + mock_requests.delete.assert_not_called() + + + @patch('feature_flag_client.Statsig') + @patch('feature_flag_client.requests') + def test_delete_flag_api_error_on_delete_raises_exception(self, mock_requests, mock_statsig): + """Test delete_flag error handling when DELETE request fails""" + self._setup_mock_statsig(mock_statsig) + + existing_flag = { + 'id': 'gate-delete-error', + 'name': 'delete-error-flag', + 'rules': [{ + 'name': 'environment_toggle', + 'conditions': [], + 'environments': ['development'] # Only current environment + }] + } + + # Mock GET request (flag exists) + mock_requests.get.return_value = self._create_mock_response(200, {'data': [existing_flag]}) + + # Mock DELETE request failure + mock_requests.delete.return_value = self._create_mock_response(403, {'error': 'Forbidden'}) + + client = StatSigFeatureFlagClient(environment='test') + + with self.assertRaises(FeatureFlagException) as context: + client.delete_flag('delete-error-flag') + + self.assertIn('Failed to delete feature gate', str(context.exception)) + + @patch('feature_flag_client.Statsig') + @patch('feature_flag_client.requests') + def test_delete_flag_api_error_on_patch_raises_exception(self, mock_requests, mock_statsig): + """Test delete_flag error handling when PATCH request fails""" + self._setup_mock_statsig(mock_statsig) + + existing_flag = { + 'id': 'gate-patch-error', + 'name': 'patch-error-flag', + 'rules': [{ + 'name': 'environment_toggle', + 'conditions': [], + 'environments': ['development', 'staging'] # Multiple environments + }] + } + + # Mock GET request (flag exists) + mock_requests.get.return_value = self._create_mock_response(200, {'data': [existing_flag]}) + + # Mock PATCH request failure + mock_requests.patch.return_value = self._create_mock_response(400, {'error': 'Bad request'}) + + client = StatSigFeatureFlagClient(environment='test') + + with self.assertRaises(FeatureFlagException) as context: + client.delete_flag('patch-error-flag') + + self.assertIn('Failed to update feature gate', str(context.exception)) From f93e34a3e7e0cb715e5fec591f0179205bffbd6d Mon Sep 17 00:00:00 2001 From: Landon Shumway Date: Thu, 2 Oct 2025 00:59:44 -0500 Subject: [PATCH 15/55] implement custom resource feature flag handler --- .../cc_common}/custom_resource_handler.py | 1 + .../handlers/manage_feature_flag.py | 131 +++++++++++++++ .../function/test_manage_feature_flag.py | 155 ++++++++++++++++++ 3 files changed, 287 insertions(+) rename backend/compact-connect/lambdas/python/{migration => common/cc_common}/custom_resource_handler.py (99%) create mode 100644 backend/compact-connect/lambdas/python/feature-flag/handlers/manage_feature_flag.py create mode 100644 backend/compact-connect/lambdas/python/feature-flag/tests/function/test_manage_feature_flag.py diff --git a/backend/compact-connect/lambdas/python/migration/custom_resource_handler.py b/backend/compact-connect/lambdas/python/common/cc_common/custom_resource_handler.py similarity index 99% rename from backend/compact-connect/lambdas/python/migration/custom_resource_handler.py rename to backend/compact-connect/lambdas/python/common/cc_common/custom_resource_handler.py index 29ee5a56d..ff6b55eda 100644 --- a/backend/compact-connect/lambdas/python/migration/custom_resource_handler.py +++ b/backend/compact-connect/lambdas/python/common/cc_common/custom_resource_handler.py @@ -4,6 +4,7 @@ from aws_lambda_powertools.logging.lambda_context import build_lambda_context_model from aws_lambda_powertools.utilities.typing import LambdaContext + from cc_common.config import logger diff --git a/backend/compact-connect/lambdas/python/feature-flag/handlers/manage_feature_flag.py b/backend/compact-connect/lambdas/python/feature-flag/handlers/manage_feature_flag.py new file mode 100644 index 000000000..d32fc903a --- /dev/null +++ b/backend/compact-connect/lambdas/python/feature-flag/handlers/manage_feature_flag.py @@ -0,0 +1,131 @@ +""" +Custom resource handler for managing StatSig feature flags. + +This handler manages feature flag lifecycle through CloudFormation custom resources, +automatically creating and configuring flags across different environments. +""" + +import os + +from cc_common.config import logger +from cc_common.custom_resource_handler import CustomResourceHandler, CustomResourceResponse +from feature_flag_client import StatSigFeatureFlagClient + + +class ManageFeatureFlagHandler(CustomResourceHandler): + """Handler for managing StatSig feature flags as custom resources""" + + def __init__(self): + super().__init__('ManageFeatureFlag') + self.environment = os.environ['ENVIRONMENT_NAME'] + # Create a StatSig client with console API access + self.client = StatSigFeatureFlagClient(environment=self.environment) + + def on_create(self, properties: dict) -> CustomResourceResponse | None: + """ + Handle Create events for feature flags. + + Creates or updates the feature flag based on environment and autoEnable setting. + + :param properties: ResourceProperties containing flagName, autoEnable, customAttributes + :return: CustomResourceResponse with PhysicalResourceId + """ + flag_name = properties.get('flagName') + auto_enable = properties.get('autoEnable', False) + custom_attributes = properties.get('customAttributes') + + if not flag_name: + raise ValueError('flagName is required in ResourceProperties') + + logger.info( + 'Creating feature flag resource', + flag_name=flag_name, + environment=self.environment, + auto_enable=auto_enable, + ) + + # Create or update the flag - client handles all environment-specific logic + flag_data = self.client.upsert_flag(flag_name, auto_enable, custom_attributes) + + # Handle the case where no action was taken (beta/prod with autoEnable=False and no existing flag) + if not flag_data: + logger.warning('Feature flag not created (autoEnable=False in beta/prod)', flag_name=flag_name) + return None + + # Extract gate ID from response + gate_id = flag_data.get('data', {}).get('id') or flag_data.get('id') + + logger.info('Feature flag resource created/updated successfully', flag_name=flag_name, gate_id=gate_id) + + # Return the gate ID as the PhysicalResourceId for tracking + return {'PhysicalResourceId': f'feature-flag-{flag_name}-{self.environment}', 'Data': {'gateId': gate_id}} + + def on_update(self, properties: dict) -> CustomResourceResponse | None: + """ + Handle Update events for feature flags. + + Updates the feature gate configuration based on changed properties. + + :param properties: ResourceProperties containing updated values + :return: Optional response data + """ + flag_name = properties.get('flagName') + auto_enable = properties.get('autoEnable', False) + custom_attributes = properties.get('customAttributes') + + if not flag_name: + raise ValueError('flagName is required in ResourceProperties') + + logger.info('Updating feature flag resource', flag_name=flag_name, environment=self.environment) + + # Update the flag - client handles all environment-specific logic + flag_data = self.client.upsert_flag(flag_name, auto_enable, custom_attributes) + + if not flag_data: + raise RuntimeError(f"Feature gate '{flag_name}' could not be updated") + + # Extract gate ID from response + gate_id = flag_data.get('data', {}).get('id') or flag_data.get('id') + + logger.info('Feature flag resource updated successfully', flag_name=flag_name, gate_id=gate_id) + + return {'PhysicalResourceId': f'feature-flag-{flag_name}-{self.environment}', 'Data': {'gateId': gate_id}} + + def on_delete(self, properties: dict) -> CustomResourceResponse | None: + """ + Handle Delete events for feature flags. + + Removes the environment from the feature gate. If it's the last environment, + deletes the gate entirely. + + :param properties: ResourceProperties containing flagName + :return: Optional response data + """ + flag_name = properties.get('flagName') + + if not flag_name: + raise ValueError('flagName is required in ResourceProperties') + + logger.info('Deleting feature flag resource', flag_name=flag_name, environment=self.environment) + + # Delete flag or remove current environment + # The delete_flag method handles all logic internally (fetching, checking environments, etc.) + result = self.client.delete_flag(flag_name) + + if result is None: + logger.info('Feature gate does not exist, nothing to delete', flag_name=flag_name) + elif result is True: + logger.info('Feature gate fully deleted (was last environment)', flag_name=flag_name) + else: + logger.info('Removed current environment from feature gate', flag_name=flag_name) + + return None + + +# Lambda handler +handler = ManageFeatureFlagHandler() + + +def on_event(event: dict, context) -> dict | None: + """Lambda handler function for CloudFormation custom resource events""" + return handler(event, context) diff --git a/backend/compact-connect/lambdas/python/feature-flag/tests/function/test_manage_feature_flag.py b/backend/compact-connect/lambdas/python/feature-flag/tests/function/test_manage_feature_flag.py new file mode 100644 index 000000000..026c01b74 --- /dev/null +++ b/backend/compact-connect/lambdas/python/feature-flag/tests/function/test_manage_feature_flag.py @@ -0,0 +1,155 @@ +import json +from unittest.mock import MagicMock, patch + +from moto import mock_aws + +from . import TstFunction + +MOCK_SERVER_KEY = 'test-server-key-123' +MOCK_CONSOLE_KEY = 'test-console-key-456' + + +@mock_aws +class TestManageFeatureFlagHandler(TstFunction): + """Test suite for ManageFeatureFlagHandler custom resource.""" + + def setUp(self): + super().setUp() + + # Set up mock secrets manager with StatSig credentials + secrets_client = self.create_mock_secrets_manager() + secrets_client.create_secret( + Name='compact-connect/env/test/statsig/credentials', + SecretString=json.dumps({ + 'serverKey': MOCK_SERVER_KEY, + 'consoleKey': MOCK_CONSOLE_KEY + }) + ) + + def create_mock_secrets_manager(self): + """Create a mock secrets manager client""" + import boto3 + return boto3.client('secretsmanager', region_name='us-east-1') + + @patch('handlers.manage_feature_flag.StatSigFeatureFlagClient') + def test_on_create_calls_upsert_flag_with_correct_params(self, mock_client_class): + """Test that on_create calls upsert_flag with the correct parameters""" + from handlers.manage_feature_flag import ManageFeatureFlagHandler + + # Set up mock client instance + mock_client = MagicMock() + mock_client.upsert_flag.return_value = {'id': 'gate-123', 'name': 'test-flag'} + mock_client_class.return_value = mock_client + + handler = ManageFeatureFlagHandler() + properties = { + 'flagName': 'test-flag', + 'autoEnable': True, + 'customAttributes': {'region': 'us-east-1', 'feature': 'new'} + } + + result = handler.on_create(properties) + + # Verify upsert_flag was called with correct parameters + mock_client.upsert_flag.assert_called_once_with( + 'test-flag', + True, + {'region': 'us-east-1', 'feature': 'new'} + ) + + # Verify response + self.assertEqual(result['PhysicalResourceId'], 'feature-flag-test-flag-test') + self.assertEqual(result['Data']['gateId'], 'gate-123') + + @patch('handlers.manage_feature_flag.StatSigFeatureFlagClient') + def test_on_create_with_minimal_properties(self, mock_client_class): + """Test on_create with minimal required properties""" + from handlers.manage_feature_flag import ManageFeatureFlagHandler + # Set up mock client instance + mock_client = MagicMock() + mock_client.upsert_flag.return_value = {'id': 'gate-456', 'name': 'minimal-flag'} + mock_client_class.return_value = mock_client + + handler = ManageFeatureFlagHandler() + properties = {'flagName': 'minimal-flag'} + + result = handler.on_create(properties) + + # Verify upsert_flag was called with defaults + mock_client.upsert_flag.assert_called_once_with('minimal-flag', False, None) + + # Verify response + self.assertEqual(result['PhysicalResourceId'], 'feature-flag-minimal-flag-test') + self.assertEqual(result['Data']['gateId'], 'gate-456') + + @patch('handlers.manage_feature_flag.StatSigFeatureFlagClient') + def test_on_update_calls_upsert_flag_with_correct_params(self, mock_client_class): + from handlers.manage_feature_flag import ManageFeatureFlagHandler + """Test that on_update calls upsert_flag with the correct parameters""" + # Set up mock client instance + mock_client = MagicMock() + mock_client.upsert_flag.return_value = {'id': 'gate-789', 'name': 'update-flag'} + mock_client_class.return_value = mock_client + + handler = ManageFeatureFlagHandler() + properties = { + 'flagName': 'update-flag', + 'autoEnable': False, + 'customAttributes': {'version': '2.0'} + } + + result = handler.on_update(properties) + + # Verify client was initialized with correct environment + mock_client_class.assert_called_once_with(environment='test') + + # Verify upsert_flag was called with correct parameters + mock_client.upsert_flag.assert_called_once_with( + 'update-flag', + False, + {'version': '2.0'} + ) + + # Verify response + self.assertEqual(result['PhysicalResourceId'], 'feature-flag-update-flag-test') + self.assertEqual(result['Data']['gateId'], 'gate-789') + + @patch('handlers.manage_feature_flag.StatSigFeatureFlagClient') + def test_on_update_raises_error_when_no_flag_returned(self, mock_client_class): + """Test on_update raises RuntimeError when upsert_flag returns empty dict""" + from handlers.manage_feature_flag import ManageFeatureFlagHandler + # Set up mock client instance + mock_client = MagicMock() + mock_client.upsert_flag.return_value = {} # Empty dict means no action taken + mock_client_class.return_value = mock_client + + handler = ManageFeatureFlagHandler() + properties = {'flagName': 'failed-update-flag'} + + with self.assertRaises(RuntimeError) as context: + handler.on_update(properties) + + self.assertIn("Feature gate 'failed-update-flag' could not be updated", str(context.exception)) + + @patch('handlers.manage_feature_flag.StatSigFeatureFlagClient') + def test_on_delete_calls_delete_flag_with_correct_params(self, mock_client_class): + """Test that on_delete calls delete_flag with the correct parameters""" + from handlers.manage_feature_flag import ManageFeatureFlagHandler + # Set up mock client instance + mock_client = MagicMock() + mock_client.delete_flag.return_value = True # Flag fully deleted + mock_client_class.return_value = mock_client + + handler = ManageFeatureFlagHandler() + properties = {'flagName': 'delete-flag'} + + result = handler.on_delete(properties) + + # Verify client was initialized with correct environment + mock_client_class.assert_called_once_with(environment='test') + + # Verify delete_flag was called with correct parameters + mock_client.delete_flag.assert_called_once_with('delete-flag') + + # Should return None (successful deletion) + self.assertIsNone(result) From bcb522507309d0ba4d2651c582f3789041a5235b Mon Sep 17 00:00:00 2001 From: Landon Shumway Date: Thu, 2 Oct 2025 01:00:27 -0500 Subject: [PATCH 16/55] Add CDK feature flag stack --- .../compact-connect/pipeline/backend_stage.py | 11 ++ .../stacks/api_stack/v1_api/api.py | 4 +- .../stacks/feature_flag_stack/__init__.py | 27 ++++ .../feature_flag_resource.py | 150 ++++++++++++++++++ 4 files changed, 189 insertions(+), 3 deletions(-) create mode 100644 backend/compact-connect/stacks/feature_flag_stack/__init__.py create mode 100644 backend/compact-connect/stacks/feature_flag_stack/feature_flag_resource.py diff --git a/backend/compact-connect/pipeline/backend_stage.py b/backend/compact-connect/pipeline/backend_stage.py index 3dd131188..58cf94212 100644 --- a/backend/compact-connect/pipeline/backend_stage.py +++ b/backend/compact-connect/pipeline/backend_stage.py @@ -6,6 +6,7 @@ from stacks.api_stack import ApiStack from stacks.disaster_recovery_stack import DisasterRecoveryStack from stacks.event_listener_stack import EventListenerStack +from stacks.feature_flag_stack import FeatureFlagStack from stacks.ingest_stack import IngestStack from stacks.managed_login_stack import ManagedLoginStack from stacks.notification_stack import NotificationStack @@ -180,3 +181,13 @@ def __init__( standard_tags=standard_tags, persistent_stack=self.persistent_stack, ) + + # Stack to create and manage feature flags + self.feature_flag_stack = FeatureFlagStack( + self, + 'FeatureFlagStack', + env=environment, + environment_name=environment_name, + environment_context=environment_context, + standard_tags=standard_tags, + ) diff --git a/backend/compact-connect/stacks/api_stack/v1_api/api.py b/backend/compact-connect/stacks/api_stack/v1_api/api.py index 75b6f870e..7775496d2 100644 --- a/backend/compact-connect/stacks/api_stack/v1_api/api.py +++ b/backend/compact-connect/stacks/api_stack/v1_api/api.py @@ -45,9 +45,7 @@ def __init__( data_event_bus = SSMParameterUtility.load_data_event_bus_from_ssm_parameter(stack) _active_compacts = persistent_stack.get_list_of_compact_abbreviations() - stack.common_env_vars.update({ - 'API_BASE_URL': f'https://{persistent_stack.api_domain_name}' - }) + stack.common_env_vars.update({'API_BASE_URL': f'https://{persistent_stack.api_domain_name}'}) read_scopes = [] write_scopes = [] diff --git a/backend/compact-connect/stacks/feature_flag_stack/__init__.py b/backend/compact-connect/stacks/feature_flag_stack/__init__.py new file mode 100644 index 000000000..0b29ede89 --- /dev/null +++ b/backend/compact-connect/stacks/feature_flag_stack/__init__.py @@ -0,0 +1,27 @@ +from __future__ import annotations + +from common_constructs.stack import AppStack +from constructs import Construct +from feature_flag_stack.feature_flag_resource import FeatureFlagResource + + +class FeatureFlagStack(AppStack): + def __init__( + self, + scope: Construct, + construct_id: str, + *, + environment_name: str, + **kwargs, + ): + super().__init__(scope, construct_id, environment_name=environment_name, **kwargs) + + # Feature Flags are deployed through a custom resource + # one per flag + self.test_flag = FeatureFlagResource( + self, + 'ExampleFlag', + flag_name='example-flag', + custom_attributes={'hello': 'world'}, + environment_name=environment_name, + ) diff --git a/backend/compact-connect/stacks/feature_flag_stack/feature_flag_resource.py b/backend/compact-connect/stacks/feature_flag_stack/feature_flag_resource.py new file mode 100644 index 000000000..4051e712b --- /dev/null +++ b/backend/compact-connect/stacks/feature_flag_stack/feature_flag_resource.py @@ -0,0 +1,150 @@ +""" +CDK construct for managing StatSig feature flags as custom resources. + +This construct creates a CloudFormation custom resource that manages the lifecycle +of StatSig feature flags across different environments. +""" + +import os + +import jsii +from aws_cdk import CustomResource, Duration, Stack +from aws_cdk.aws_iam import IGrantable, PolicyStatement +from aws_cdk.aws_logs import RetentionDays +from aws_cdk.custom_resources import Provider +from cdk_nag import NagSuppressions +from common_constructs.python_function import PythonFunction +from constructs import Construct + + +@jsii.implements(IGrantable) +class FeatureFlagResource(Construct): + """ + Custom resource for managing StatSig feature flags. + + This construct creates a Lambda-backed custom resource that handles + creation, updates, and deletion of feature flags in StatSig. + """ + + def __init__( + self, + scope: Construct, + construct_id: str, + *, + flag_name: str, + auto_enable: bool = False, + custom_attributes: dict[str, str] | None = None, + environment_name: str, + ): + """ + Initialize the FeatureFlagResource construct. + + :param flag_name: Name of the feature flag to manage + :param auto_enable: If True, automatically enable the flag in this environment (beta/prod only) + :param custom_attributes: Optional custom attributes for feature flag targeting + :param environment_name: The environment name (test, beta, prod) + """ + super().__init__(scope, construct_id) + + if not flag_name: + raise ValueError('flag_name is required') + + # Lambda function for managing feature flags + self.manage_function = PythonFunction( + self, + 'ManageFunction', + index=os.path.join('handlers', 'manage_feature_flag.py'), + lambda_dir='feature-flag', + handler='on_event', + log_retention=RetentionDays.ONE_MONTH, + environment={'ENVIRONMENT_NAME': environment_name}, + timeout=Duration.minutes(5), + memory_size=256, + ) + + # Grant permissions to read secrets + secret_name = f'compact-connect/env/{environment_name}/statsig/credentials' + self.manage_function.add_to_role_policy( + PolicyStatement( + actions=['secretsmanager:GetSecretValue'], + resources=[ + f'arn:aws:secretsmanager:{Stack.of(self).region}:{Stack.of(self).account}:secret:{secret_name}-*' + ], + ) + ) + + # Create the custom resource provider + self.provider = Provider( + self, 'Provider', on_event_handler=self.manage_function, log_retention=RetentionDays.ONE_DAY + ) + + # Add CDK Nag suppressions for the provider framework + NagSuppressions.add_resource_suppressions_by_path( + Stack.of(self), + f'{self.provider.node.path}/framework-onEvent/Resource', + [ + {'id': 'AwsSolutions-L1', 'reason': 'We do not control this runtime'}, + { + 'id': 'HIPAA.Security-LambdaConcurrency', + 'reason': 'This function is only run at deploy time, by CloudFormation and has no need for ' + 'concurrency limits.', + }, + { + 'id': 'HIPAA.Security-LambdaDLQ', + 'reason': 'This is a synchronous function run at deploy time. It does not need a DLQ', + }, + { + 'id': 'HIPAA.Security-LambdaInsideVPC', + 'reason': 'We may choose to move our lambdas into private VPC subnets in a future enhancement', + }, + ], + ) + + NagSuppressions.add_resource_suppressions_by_path( + Stack.of(self), + path=f'{self.provider.node.path}/framework-onEvent/ServiceRole/DefaultPolicy/Resource', + suppressions=[ + { + 'id': 'AwsSolutions-IAM5', + 'reason': 'The actions in this policy are specifically what this lambda needs ' + 'and is scoped appropriately.', + 'appliesTo': [ + f'Resource::arn:aws:secretsmanager:{Stack.of(self).region}:{Stack.of(self).account}:secret:{secret_name}-*', + ], + }, + ], + ) + + NagSuppressions.add_resource_suppressions_by_path( + Stack.of(self), + path=f'{self.provider.node.path}/framework-onEvent/ServiceRole/Resource', + suppressions=[ + { + 'id': 'AwsSolutions-IAM4', + 'appliesTo': [ + 'Policy::arn::iam::aws:policy/service-role/AWSLambdaBasicExecutionRole' + ], + 'reason': 'This policy is appropriate for the custom resource lambda', + }, + ], + ) + + # Build custom resource properties + properties = {'flagName': flag_name, 'autoEnable': auto_enable} + + if custom_attributes: + properties['customAttributes'] = custom_attributes + + # Create the custom resource + self.custom_resource = CustomResource( + self, + 'CustomResource', + resource_type='Custom::FeatureFlag', + service_token=self.provider.service_token, + properties=properties, + ) + + @property + def grant_principal(self): + """Return the grant principal for IAM permissions""" + return self.manage_function.grant_principal From 8f45130796eeba05616f9b056187cd27f2df5206 Mon Sep 17 00:00:00 2001 From: Landon Shumway Date: Thu, 2 Oct 2025 01:00:41 -0500 Subject: [PATCH 17/55] formatting --- .../common/cc_common/feature_flag_client.py | 5 +- .../tests/unit/test_feature_flag_client.py | 11 +- .../feature-flag/feature_flag_client.py | 28 +- .../function/test_manage_feature_flag.py | 70 +- .../tests/function/test_statsig_client.py | 701 ++++++++++-------- 5 files changed, 440 insertions(+), 375 deletions(-) diff --git a/backend/compact-connect/lambdas/python/common/cc_common/feature_flag_client.py b/backend/compact-connect/lambdas/python/common/cc_common/feature_flag_client.py index b55ce72e4..0dce747c6 100644 --- a/backend/compact-connect/lambdas/python/common/cc_common/feature_flag_client.py +++ b/backend/compact-connect/lambdas/python/common/cc_common/feature_flag_client.py @@ -43,9 +43,7 @@ def to_dict(self) -> dict[str, Any]: return result -def is_feature_enabled( - flag_name: str, context: FeatureFlagContext | None = None, fail_open: bool = False -) -> bool: +def is_feature_enabled(flag_name: str, context: FeatureFlagContext | None = None, fail_open: bool = False) -> bool: """ Check if a feature flag is enabled. @@ -131,4 +129,3 @@ def _get_api_base_url() -> str: api_base_url = config.api_base_url # Remove trailing slash if present return api_base_url.rstrip('/') - diff --git a/backend/compact-connect/lambdas/python/common/tests/unit/test_feature_flag_client.py b/backend/compact-connect/lambdas/python/common/tests/unit/test_feature_flag_client.py index 6440f8412..d6a9b3779 100644 --- a/backend/compact-connect/lambdas/python/common/tests/unit/test_feature_flag_client.py +++ b/backend/compact-connect/lambdas/python/common/tests/unit/test_feature_flag_client.py @@ -4,7 +4,6 @@ class TestFeatureFlagClient(TstLambdas): - def test_is_feature_enabled_returns_true_when_flag_enabled(self): """Test that is_feature_enabled returns True when the API returns enabled=true.""" from cc_common.feature_flag_client import is_feature_enabled @@ -60,7 +59,10 @@ def test_is_feature_enabled_with_context(self): # Verify the API was called with the context mock_post.assert_called_once_with( 'https://api.example.com/v1/flags/check', - json={'flagName': 'test-flag', 'context': {'userId': 'user123', 'customAttributes': {'licenseType': 'lpc'}}}, + json={ + 'flagName': 'test-flag', + 'context': {'userId': 'user123', 'customAttributes': {'licenseType': 'lpc'}}, + }, timeout=5, headers={'Content-Type': 'application/json'}, ) @@ -197,9 +199,7 @@ def test_feature_flag_context_with_both_fields(self): context = FeatureFlagContext(user_id='user456', custom_attributes={'licenseType': 'physician'}) result = context.to_dict() - self.assertEqual( - result, {'userId': 'user456', 'customAttributes': {'licenseType': 'physician'}} - ) + self.assertEqual(result, {'userId': 'user456', 'customAttributes': {'licenseType': 'physician'}}) def test_feature_flag_context_empty(self): """Test FeatureFlagContext to_dict with no fields set.""" @@ -233,4 +233,3 @@ def test_is_feature_enabled_with_context_user_id_only(self): timeout=5, headers={'Content-Type': 'application/json'}, ) - diff --git a/backend/compact-connect/lambdas/python/feature-flag/feature_flag_client.py b/backend/compact-connect/lambdas/python/feature-flag/feature_flag_client.py index 2c55c2898..117a7238d 100644 --- a/backend/compact-connect/lambdas/python/feature-flag/feature_flag_client.py +++ b/backend/compact-connect/lambdas/python/feature-flag/feature_flag_client.py @@ -84,7 +84,9 @@ def check_flag(self, request: FeatureFlagRequest) -> FeatureFlagResult: """ @abstractmethod - def upsert_flag(self, flag_name: str, auto_enable: bool = False, custom_attributes: dict[str, Any] | None = None) -> dict[str, Any]: + def upsert_flag( + self, flag_name: str, auto_enable: bool = False, custom_attributes: dict[str, Any] | None = None + ) -> dict[str, Any]: """ Create or update a feature flag in the provider. @@ -368,7 +370,9 @@ def _make_console_api_request( except requests.exceptions.RequestException as e: raise FeatureFlagException(f'StatSig Console API request failed: {e}') from e - def upsert_flag(self, flag_name: str, auto_enable: bool = False, custom_attributes: dict[str, Any] | None = None) -> dict[str, Any]: + def upsert_flag( + self, flag_name: str, auto_enable: bool = False, custom_attributes: dict[str, Any] | None = None + ) -> dict[str, Any]: """ Create or update a feature gate in StatSig. @@ -407,11 +411,11 @@ def upsert_flag(self, flag_name: str, auto_enable: bool = False, custom_attribut else: # Gate exists - update it gate_id = existing_gate.get('id') - + # Update the gate with new attributes and/or environment updated_gate = self._prepare_gate_update(existing_gate, custom_attributes, auto_enable) self._update_gate(gate_id, updated_gate) - + # Return updated gate data return self.get_flag(flag_name) or existing_gate @@ -453,11 +457,11 @@ def _create_new_gate(self, flag_name: str, custom_attributes: dict[str, Any] | N if response.status_code in [200, 201]: return response.json() else: - raise FeatureFlagException( - f'Failed to create feature gate: {response.status_code} - {response.text[:200]}' - ) + raise FeatureFlagException(f'Failed to create feature gate: {response.status_code} - {response.text[:200]}') - def _prepare_gate_update(self, gate_data: dict[str, Any], custom_attributes: dict[str, Any] | None = None, add_current_env: bool = False) -> dict[str, Any]: + def _prepare_gate_update( + self, gate_data: dict[str, Any], custom_attributes: dict[str, Any] | None = None, add_current_env: bool = False + ) -> dict[str, Any]: """ Prepare an updated gate configuration with new custom attributes and/or environment. @@ -475,7 +479,9 @@ def _prepare_gate_update(self, gate_data: dict[str, Any], custom_attributes: dic if custom_attributes is not None: new_conditions = [] for key, value in custom_attributes.items(): - new_conditions.append({'type': 'custom_field', 'targetValue': [value], 'field': key, 'operator': 'any'}) + new_conditions.append( + {'type': 'custom_field', 'targetValue': [value], 'field': key, 'operator': 'any'} + ) rule['conditions'] = new_conditions # Add current environment if requested @@ -502,9 +508,7 @@ def _update_gate(self, gate_id: str, gate_data: dict[str, Any]) -> bool: if response.status_code in [200, 204]: return True else: - raise FeatureFlagException( - f'Failed to update feature gate: {response.status_code} - {response.text[:200]}' - ) + raise FeatureFlagException(f'Failed to update feature gate: {response.status_code} - {response.text[:200]}') def get_flag(self, flag_name: str) -> dict[str, Any] | None: """ diff --git a/backend/compact-connect/lambdas/python/feature-flag/tests/function/test_manage_feature_flag.py b/backend/compact-connect/lambdas/python/feature-flag/tests/function/test_manage_feature_flag.py index 026c01b74..b4b736dcd 100644 --- a/backend/compact-connect/lambdas/python/feature-flag/tests/function/test_manage_feature_flag.py +++ b/backend/compact-connect/lambdas/python/feature-flag/tests/function/test_manage_feature_flag.py @@ -20,15 +20,13 @@ def setUp(self): secrets_client = self.create_mock_secrets_manager() secrets_client.create_secret( Name='compact-connect/env/test/statsig/credentials', - SecretString=json.dumps({ - 'serverKey': MOCK_SERVER_KEY, - 'consoleKey': MOCK_CONSOLE_KEY - }) + SecretString=json.dumps({'serverKey': MOCK_SERVER_KEY, 'consoleKey': MOCK_CONSOLE_KEY}), ) def create_mock_secrets_manager(self): """Create a mock secrets manager client""" import boto3 + return boto3.client('secretsmanager', region_name='us-east-1') @patch('handlers.manage_feature_flag.StatSigFeatureFlagClient') @@ -40,23 +38,19 @@ def test_on_create_calls_upsert_flag_with_correct_params(self, mock_client_class mock_client = MagicMock() mock_client.upsert_flag.return_value = {'id': 'gate-123', 'name': 'test-flag'} mock_client_class.return_value = mock_client - + handler = ManageFeatureFlagHandler() properties = { 'flagName': 'test-flag', 'autoEnable': True, - 'customAttributes': {'region': 'us-east-1', 'feature': 'new'} + 'customAttributes': {'region': 'us-east-1', 'feature': 'new'}, } - + result = handler.on_create(properties) - + # Verify upsert_flag was called with correct parameters - mock_client.upsert_flag.assert_called_once_with( - 'test-flag', - True, - {'region': 'us-east-1', 'feature': 'new'} - ) - + mock_client.upsert_flag.assert_called_once_with('test-flag', True, {'region': 'us-east-1', 'feature': 'new'}) + # Verify response self.assertEqual(result['PhysicalResourceId'], 'feature-flag-test-flag-test') self.assertEqual(result['Data']['gateId'], 'gate-123') @@ -65,19 +59,20 @@ def test_on_create_calls_upsert_flag_with_correct_params(self, mock_client_class def test_on_create_with_minimal_properties(self, mock_client_class): """Test on_create with minimal required properties""" from handlers.manage_feature_flag import ManageFeatureFlagHandler + # Set up mock client instance mock_client = MagicMock() mock_client.upsert_flag.return_value = {'id': 'gate-456', 'name': 'minimal-flag'} mock_client_class.return_value = mock_client - + handler = ManageFeatureFlagHandler() properties = {'flagName': 'minimal-flag'} - + result = handler.on_create(properties) - + # Verify upsert_flag was called with defaults mock_client.upsert_flag.assert_called_once_with('minimal-flag', False, None) - + # Verify response self.assertEqual(result['PhysicalResourceId'], 'feature-flag-minimal-flag-test') self.assertEqual(result['Data']['gateId'], 'gate-456') @@ -85,30 +80,23 @@ def test_on_create_with_minimal_properties(self, mock_client_class): @patch('handlers.manage_feature_flag.StatSigFeatureFlagClient') def test_on_update_calls_upsert_flag_with_correct_params(self, mock_client_class): from handlers.manage_feature_flag import ManageFeatureFlagHandler + """Test that on_update calls upsert_flag with the correct parameters""" # Set up mock client instance mock_client = MagicMock() mock_client.upsert_flag.return_value = {'id': 'gate-789', 'name': 'update-flag'} mock_client_class.return_value = mock_client - + handler = ManageFeatureFlagHandler() - properties = { - 'flagName': 'update-flag', - 'autoEnable': False, - 'customAttributes': {'version': '2.0'} - } - + properties = {'flagName': 'update-flag', 'autoEnable': False, 'customAttributes': {'version': '2.0'}} + result = handler.on_update(properties) - + # Verify client was initialized with correct environment mock_client_class.assert_called_once_with(environment='test') - + # Verify upsert_flag was called with correct parameters - mock_client.upsert_flag.assert_called_once_with( - 'update-flag', - False, - {'version': '2.0'} - ) + mock_client.upsert_flag.assert_called_once_with('update-flag', False, {'version': '2.0'}) # Verify response self.assertEqual(result['PhysicalResourceId'], 'feature-flag-update-flag-test') @@ -118,38 +106,40 @@ def test_on_update_calls_upsert_flag_with_correct_params(self, mock_client_class def test_on_update_raises_error_when_no_flag_returned(self, mock_client_class): """Test on_update raises RuntimeError when upsert_flag returns empty dict""" from handlers.manage_feature_flag import ManageFeatureFlagHandler + # Set up mock client instance mock_client = MagicMock() mock_client.upsert_flag.return_value = {} # Empty dict means no action taken mock_client_class.return_value = mock_client - + handler = ManageFeatureFlagHandler() properties = {'flagName': 'failed-update-flag'} - + with self.assertRaises(RuntimeError) as context: handler.on_update(properties) - + self.assertIn("Feature gate 'failed-update-flag' could not be updated", str(context.exception)) @patch('handlers.manage_feature_flag.StatSigFeatureFlagClient') def test_on_delete_calls_delete_flag_with_correct_params(self, mock_client_class): """Test that on_delete calls delete_flag with the correct parameters""" from handlers.manage_feature_flag import ManageFeatureFlagHandler + # Set up mock client instance mock_client = MagicMock() mock_client.delete_flag.return_value = True # Flag fully deleted mock_client_class.return_value = mock_client - + handler = ManageFeatureFlagHandler() properties = {'flagName': 'delete-flag'} - + result = handler.on_delete(properties) - + # Verify client was initialized with correct environment mock_client_class.assert_called_once_with(environment='test') - + # Verify delete_flag was called with correct parameters mock_client.delete_flag.assert_called_once_with('delete-flag') - + # Should return None (successful deletion) self.assertIsNone(result) diff --git a/backend/compact-connect/lambdas/python/feature-flag/tests/function/test_statsig_client.py b/backend/compact-connect/lambdas/python/feature-flag/tests/function/test_statsig_client.py index 303cc615d..2445dce55 100644 --- a/backend/compact-connect/lambdas/python/feature-flag/tests/function/test_statsig_client.py +++ b/backend/compact-connect/lambdas/python/feature-flag/tests/function/test_statsig_client.py @@ -31,10 +31,7 @@ def setUp(self): for env in ['test', 'prod']: secrets_client.create_secret( Name=f'compact-connect/env/{env}/statsig/credentials', - SecretString=json.dumps({ - 'serverKey': MOCK_SERVER_KEY, - 'consoleKey': MOCK_CONSOLE_KEY - }) + SecretString=json.dumps({'serverKey': MOCK_SERVER_KEY, 'consoleKey': MOCK_CONSOLE_KEY}), ) def create_mock_secrets_manager(self): @@ -269,88 +266,102 @@ def _create_mock_response(self, status_code: int, json_data: dict = None): def test_upsert_flag_create_new_in_test_environment(self, mock_requests, mock_statsig): """Test creating a new flag in test environment""" self._setup_mock_statsig(mock_statsig) - + # Mock GET request (flag doesn't exist) mock_requests.get.return_value = self._create_mock_response(200, {'data': []}) - + # Mock POST request (create flag) - created_flag = { - 'id': 'gate-123', - 'name': 'new-test-flag', - 'data': {'id': 'gate-123'} - } + created_flag = {'id': 'gate-123', 'name': 'new-test-flag', 'data': {'id': 'gate-123'}} mock_requests.post.return_value = self._create_mock_response(201, created_flag) - + client = StatSigFeatureFlagClient(environment='test') - + result = client.upsert_flag('new-test-flag', auto_enable=False, custom_attributes={'region': 'us-east-1'}) - + # Verify result self.assertEqual(result['id'], 'gate-123') self.assertEqual(result['name'], 'new-test-flag') - + # Verify API calls mock_requests.get.assert_called_once() # Verify POST payload mock_requests.post.assert_called_once_with( f'{STATSIG_API_BASE_URL}/gates', - headers={'STATSIG-API-KEY': MOCK_CONSOLE_KEY, - 'STATSIG-API-VERSION': STATSIG_API_VERSION, - 'Content-Type': 'application/json'}, - data=json.dumps({ - "name": "new-test-flag", - "description": "Feature gate managed by CDK for new-test-flag feature", - "isEnabled": True, "rules": [{"name": "environment_toggle", - "conditions": [{"type": "custom_field", "targetValue": ["us-east-1"], - "field": "region", "operator": "any"}], "environments": ["development"], - "passPercentage": 100}]}), - timeout=30 + headers={ + 'STATSIG-API-KEY': MOCK_CONSOLE_KEY, + 'STATSIG-API-VERSION': STATSIG_API_VERSION, + 'Content-Type': 'application/json', + }, + data=json.dumps( + { + 'name': 'new-test-flag', + 'description': 'Feature gate managed by CDK for new-test-flag feature', + 'isEnabled': True, + 'rules': [ + { + 'name': 'environment_toggle', + 'conditions': [ + { + 'type': 'custom_field', + 'targetValue': ['us-east-1'], + 'field': 'region', + 'operator': 'any', + } + ], + 'environments': ['development'], + 'passPercentage': 100, + } + ], + } + ), + timeout=30, ) - - - @patch('feature_flag_client.Statsig') @patch('feature_flag_client.requests') def test_upsert_flag_create_new_in_test_environment_no_attributes(self, mock_requests, mock_statsig): """Test creating a new flag in test environment without custom attributes""" self._setup_mock_statsig(mock_statsig) - + # Mock GET request (flag doesn't exist) mock_requests.get.return_value = self._create_mock_response(200, {'data': []}) - + # Mock POST request (create flag) - created_flag = { - 'id': 'gate-456', - 'name': 'simple-flag', - 'data': {'id': 'gate-456'} - } + created_flag = {'id': 'gate-456', 'name': 'simple-flag', 'data': {'id': 'gate-456'}} mock_requests.post.return_value = self._create_mock_response(201, created_flag) - + client = StatSigFeatureFlagClient(environment='test') - + result = client.upsert_flag('simple-flag') - + # Verify result self.assertEqual(result['id'], 'gate-456') - + # Verify API calls mock_requests.get.assert_called_once() mock_requests.post.assert_called_once_with( f'{STATSIG_API_BASE_URL}/gates', - headers={'STATSIG-API-KEY': MOCK_CONSOLE_KEY, - 'STATSIG-API-VERSION': STATSIG_API_VERSION, - 'Content-Type': 'application/json'}, - data=json.dumps({ - "name": "simple-flag", - "description": "Feature gate managed by CDK for simple-flag feature", - "isEnabled": True, - "rules": [{"name": "environment_toggle", - "conditions": [], - "environments": ["development"], - "passPercentage": 100}] - }), - timeout=30 + headers={ + 'STATSIG-API-KEY': MOCK_CONSOLE_KEY, + 'STATSIG-API-VERSION': STATSIG_API_VERSION, + 'Content-Type': 'application/json', + }, + data=json.dumps( + { + 'name': 'simple-flag', + 'description': 'Feature gate managed by CDK for simple-flag feature', + 'isEnabled': True, + 'rules': [ + { + 'name': 'environment_toggle', + 'conditions': [], + 'environments': ['development'], + 'passPercentage': 100, + } + ], + } + ), + timeout=30, ) @patch('feature_flag_client.Statsig') @@ -358,50 +369,65 @@ def test_upsert_flag_create_new_in_test_environment_no_attributes(self, mock_req def test_upsert_flag_update_existing_in_test_environment(self, mock_requests, mock_statsig): """Test updating an existing flag in test environment""" self._setup_mock_statsig(mock_statsig) - + existing_flag = { 'id': 'gate-789', 'name': 'existing-flag', - 'rules': [{ - 'name': 'environment_toggle', - 'conditions': [{'field': 'old_attr', 'targetValue': ['old_value']}], - 'environments': ['development'] - }] + 'rules': [ + { + 'name': 'environment_toggle', + 'conditions': [{'field': 'old_attr', 'targetValue': ['old_value']}], + 'environments': ['development'], + } + ], } - + # Mock GET requests (flag exists, then return updated flag) mock_requests.get.side_effect = [ self._create_mock_response(200, {'data': [existing_flag]}), # First call to check existence - self._create_mock_response(200, {'data': [existing_flag]}) # Second call after update + self._create_mock_response(200, {'data': [existing_flag]}), # Second call after update ] - + # Mock PATCH request (update flag) mock_requests.patch.return_value = self._create_mock_response(200) - + client = StatSigFeatureFlagClient(environment='test') - + result = client.upsert_flag('existing-flag', custom_attributes={'new_attr': 'new_value'}) - + # Verify result self.assertEqual(result['id'], 'gate-789') - + # Verify API calls self.assertEqual(1, mock_requests.get.call_count) mock_requests.patch.assert_called_once_with( f'{STATSIG_API_BASE_URL}/gates/gate-789', - headers={'STATSIG-API-KEY': MOCK_CONSOLE_KEY, - 'STATSIG-API-VERSION': STATSIG_API_VERSION, - 'Content-Type': 'application/json'}, - data=json.dumps({ - 'id': 'gate-789', - 'name': 'existing-flag', - 'rules': [{ - 'name': 'environment_toggle', - 'conditions': [{'type': 'custom_field', 'targetValue': ['new_value'], 'field': 'new_attr', 'operator': 'any'}], - 'environments': ['development'] - }] - }), - timeout=30 + headers={ + 'STATSIG-API-KEY': MOCK_CONSOLE_KEY, + 'STATSIG-API-VERSION': STATSIG_API_VERSION, + 'Content-Type': 'application/json', + }, + data=json.dumps( + { + 'id': 'gate-789', + 'name': 'existing-flag', + 'rules': [ + { + 'name': 'environment_toggle', + 'conditions': [ + { + 'type': 'custom_field', + 'targetValue': ['new_value'], + 'field': 'new_attr', + 'operator': 'any', + } + ], + 'environments': ['development'], + } + ], + } + ), + timeout=30, ) @patch('feature_flag_client.Statsig') @@ -409,27 +435,23 @@ def test_upsert_flag_update_existing_in_test_environment(self, mock_requests, mo def test_upsert_flag_existing_in_test_no_changes(self, mock_requests, mock_statsig): """Test updating an existing flag in test environment with no custom attributes""" self._setup_mock_statsig(mock_statsig) - + existing_flag = { 'id': 'gate-unchanged', 'name': 'unchanged-flag', - 'rules': [{ - 'name': 'environment_toggle', - 'conditions': [], - 'environments': ['development'] - }] + 'rules': [{'name': 'environment_toggle', 'conditions': [], 'environments': ['development']}], } - + # Mock GET request (flag exists) mock_requests.get.return_value = self._create_mock_response(200, {'data': [existing_flag]}) - + client = StatSigFeatureFlagClient(environment='test') - + result = client.upsert_flag('unchanged-flag') - + # Verify result self.assertEqual(result['id'], 'gate-unchanged') - + # Verify no PATCH was called since no changes mock_requests.get.assert_called_once() mock_requests.patch.assert_not_called() @@ -440,17 +462,17 @@ def test_upsert_flag_existing_in_test_no_changes(self, mock_requests, mock_stats def test_upsert_flag_prod_environment_auto_enable_false_no_existing_flag(self, mock_requests, mock_statsig): """Test upsert in prod environment with autoEnable=False and no existing flag""" self._setup_mock_statsig(mock_statsig) - + # Mock GET request (flag doesn't exist) mock_requests.get.return_value = self._create_mock_response(200, {'data': []}) - + client = StatSigFeatureFlagClient(environment='prod') - + result = client.upsert_flag('prod-flag', auto_enable=False) - + # Should return empty dict (no action taken) self.assertEqual(result, {}) - + # Should only call GET, not POST mock_requests.get.assert_called_once() mock_requests.post.assert_not_called() @@ -460,42 +482,46 @@ def test_upsert_flag_prod_environment_auto_enable_false_no_existing_flag(self, m def test_upsert_flag_prod_environment_auto_enable_true_no_existing_flag(self, mock_requests, mock_statsig): """Test upsert in prod environment with autoEnable=True and no existing flag""" self._setup_mock_statsig(mock_statsig) - + # Mock GET request (flag doesn't exist) mock_requests.get.return_value = self._create_mock_response(200, {'data': []}) - + # Mock POST request (create flag) - created_flag = { - 'id': 'gate-prod', - 'name': 'prod-flag', - 'data': {'id': 'gate-prod'} - } + created_flag = {'id': 'gate-prod', 'name': 'prod-flag', 'data': {'id': 'gate-prod'}} mock_requests.post.return_value = self._create_mock_response(201, created_flag) - + client = StatSigFeatureFlagClient(environment='prod') - + result = client.upsert_flag('prod-flag', auto_enable=True) - + # Verify result self.assertEqual(result['id'], 'gate-prod') - + # Verify API calls mock_requests.get.assert_called_once() mock_requests.post.assert_called_once_with( f'{STATSIG_API_BASE_URL}/gates', - headers={'STATSIG-API-KEY': MOCK_CONSOLE_KEY, - 'STATSIG-API-VERSION': STATSIG_API_VERSION, - 'Content-Type': 'application/json'}, - data=json.dumps({ - "name": "prod-flag", - "description": "Feature gate managed by CDK for prod-flag feature", - "isEnabled": True, - "rules": [{"name": "environment_toggle", - "conditions": [], - "environments": ["production"], - "passPercentage": 100}] - }), - timeout=30 + headers={ + 'STATSIG-API-KEY': MOCK_CONSOLE_KEY, + 'STATSIG-API-VERSION': STATSIG_API_VERSION, + 'Content-Type': 'application/json', + }, + data=json.dumps( + { + 'name': 'prod-flag', + 'description': 'Feature gate managed by CDK for prod-flag feature', + 'isEnabled': True, + 'rules': [ + { + 'name': 'environment_toggle', + 'conditions': [], + 'environments': ['production'], + 'passPercentage': 100, + } + ], + } + ), + timeout=30, ) @patch('feature_flag_client.Statsig') @@ -503,50 +529,60 @@ def test_upsert_flag_prod_environment_auto_enable_true_no_existing_flag(self, mo def test_upsert_flag_prod_environment_existing_flag_auto_enable_true(self, mock_requests, mock_statsig): """Test upsert in prod environment with existing flag and autoEnable=True""" self._setup_mock_statsig(mock_statsig) - + existing_flag = { 'id': 'gate-existing-prod', 'name': 'existing-prod-flag', - 'rules': [{ - 'name': 'environment_toggle', - 'conditions': [], - 'environments': ['development'] # Only has development - }] + 'rules': [ + { + 'name': 'environment_toggle', + 'conditions': [], + 'environments': ['development'], # Only has development + } + ], } - + # Mock GET requests (flag exists, then return updated flag) mock_requests.get.side_effect = [ self._create_mock_response(200, {'data': [existing_flag]}), # First call to check existence - self._create_mock_response(200, {'data': [existing_flag]}) # Second call after update + self._create_mock_response(200, {'data': [existing_flag]}), # Second call after update ] - + # Mock PATCH request (update flag) mock_requests.patch.return_value = self._create_mock_response(200) - + client = StatSigFeatureFlagClient(environment='prod') - + result = client.upsert_flag('existing-prod-flag', auto_enable=True, custom_attributes={'env': 'prod'}) - + # Verify result self.assertEqual(result['id'], 'gate-existing-prod') - + # Verify API calls self.assertEqual(mock_requests.get.call_count, 2) # Once to check, once to return updated mock_requests.patch.assert_called_once_with( f'{STATSIG_API_BASE_URL}/gates/gate-existing-prod', - headers={'STATSIG-API-KEY': MOCK_CONSOLE_KEY, - 'STATSIG-API-VERSION': STATSIG_API_VERSION, - 'Content-Type': 'application/json'}, - data=json.dumps({ - 'id': 'gate-existing-prod', - 'name': 'existing-prod-flag', - 'rules': [{ - 'name': 'environment_toggle', - 'conditions': [{'type': 'custom_field', 'targetValue': ['prod'], 'field': 'env', 'operator': 'any'}], - 'environments': ['development', 'production'] - }] - }), - timeout=30 + headers={ + 'STATSIG-API-KEY': MOCK_CONSOLE_KEY, + 'STATSIG-API-VERSION': STATSIG_API_VERSION, + 'Content-Type': 'application/json', + }, + data=json.dumps( + { + 'id': 'gate-existing-prod', + 'name': 'existing-prod-flag', + 'rules': [ + { + 'name': 'environment_toggle', + 'conditions': [ + {'type': 'custom_field', 'targetValue': ['prod'], 'field': 'env', 'operator': 'any'} + ], + 'environments': ['development', 'production'], + } + ], + } + ), + timeout=30, ) @patch('feature_flag_client.Statsig') @@ -554,50 +590,60 @@ def test_upsert_flag_prod_environment_existing_flag_auto_enable_true(self, mock_ def test_upsert_flag_prod_environment_existing_flag_auto_enable_false(self, mock_requests, mock_statsig): """Test upsert in prod environment with existing flag and autoEnable=False""" self._setup_mock_statsig(mock_statsig) - + existing_flag = { 'id': 'gate-existing-prod-2', 'name': 'existing-prod-flag-2', - 'rules': [{ - 'name': 'environment_toggle', - 'conditions': [{'field': 'old', 'targetValue': ['value']}], - 'environments': ['development', 'production'] # Already has production - }] + 'rules': [ + { + 'name': 'environment_toggle', + 'conditions': [{'field': 'old', 'targetValue': ['value']}], + 'environments': ['development', 'production'], # Already has production + } + ], } - + # Mock GET requests (flag exists, then return updated flag) mock_requests.get.side_effect = [ self._create_mock_response(200, {'data': [existing_flag]}), # First call to check existence - self._create_mock_response(200, {'data': [existing_flag]}) # Second call after update + self._create_mock_response(200, {'data': [existing_flag]}), # Second call after update ] - + # Mock PATCH request (update flag) mock_requests.patch.return_value = self._create_mock_response(200) - + client = StatSigFeatureFlagClient(environment='prod') - + result = client.upsert_flag('existing-prod-flag-2', auto_enable=False, custom_attributes={'new': 'attr'}) - + # Verify result self.assertEqual(result['id'], 'gate-existing-prod-2') - + # Verify API calls self.assertEqual(mock_requests.get.call_count, 2) # Once to check, once to return updated mock_requests.patch.assert_called_once_with( f'{STATSIG_API_BASE_URL}/gates/gate-existing-prod-2', - headers={'STATSIG-API-KEY': MOCK_CONSOLE_KEY, - 'STATSIG-API-VERSION': STATSIG_API_VERSION, - 'Content-Type': 'application/json'}, - data=json.dumps({ - 'id': 'gate-existing-prod-2', - 'name': 'existing-prod-flag-2', - 'rules': [{ - 'name': 'environment_toggle', - 'conditions': [{'type': 'custom_field', 'targetValue': ['attr'], 'field': 'new', 'operator': 'any'}], - 'environments': ['development', 'production'] - }] - }), - timeout=30 + headers={ + 'STATSIG-API-KEY': MOCK_CONSOLE_KEY, + 'STATSIG-API-VERSION': STATSIG_API_VERSION, + 'Content-Type': 'application/json', + }, + data=json.dumps( + { + 'id': 'gate-existing-prod-2', + 'name': 'existing-prod-flag-2', + 'rules': [ + { + 'name': 'environment_toggle', + 'conditions': [ + {'type': 'custom_field', 'targetValue': ['attr'], 'field': 'new', 'operator': 'any'} + ], + 'environments': ['development', 'production'], + } + ], + } + ), + timeout=30, ) @patch('feature_flag_client.Statsig') @@ -605,15 +651,15 @@ def test_upsert_flag_prod_environment_existing_flag_auto_enable_false(self, mock def test_upsert_flag_api_error_handling(self, mock_requests, mock_statsig): """Test error handling when StatSig API returns errors""" self._setup_mock_statsig(mock_statsig) - + # Mock GET request failure mock_requests.get.return_value = self._create_mock_response(500, {'error': 'Internal server error'}) - + client = StatSigFeatureFlagClient(environment='test') - + with self.assertRaises(FeatureFlagException) as context: client.upsert_flag('error-flag') - + self.assertIn('Failed to fetch gates', str(context.exception)) @patch('feature_flag_client.Statsig') @@ -621,18 +667,18 @@ def test_upsert_flag_api_error_handling(self, mock_requests, mock_statsig): def test_upsert_flag_create_api_error_raises_exception(self, mock_requests, mock_statsig): """Test error handling when flag creation fails""" self._setup_mock_statsig(mock_statsig) - + # Mock GET request (flag doesn't exist) mock_requests.get.return_value = self._create_mock_response(200, {'data': []}) - + # Mock POST request failure mock_requests.post.return_value = self._create_mock_response(400, {'error': 'Bad request'}) - + client = StatSigFeatureFlagClient(environment='test') - + with self.assertRaises(FeatureFlagException) as context: client.upsert_flag('create-error-flag') - + self.assertIn('Failed to create feature gate', str(context.exception)) @patch('feature_flag_client.Statsig') @@ -640,28 +686,24 @@ def test_upsert_flag_create_api_error_raises_exception(self, mock_requests, mock def test_upsert_flag_update_api_error_raises_exception(self, mock_requests, mock_statsig): """Test error handling when flag update fails""" self._setup_mock_statsig(mock_statsig) - + existing_flag = { 'id': 'gate-update-error', 'name': 'update-error-flag', - 'rules': [{ - 'name': 'environment_toggle', - 'conditions': [], - 'environments': ['development'] - }] + 'rules': [{'name': 'environment_toggle', 'conditions': [], 'environments': ['development']}], } - + # Mock GET request (flag exists) mock_requests.get.return_value = self._create_mock_response(200, {'data': [existing_flag]}) - + # Mock PATCH request failure mock_requests.patch.return_value = self._create_mock_response(403, {'error': 'Forbidden'}) - + client = StatSigFeatureFlagClient(environment='test') - + with self.assertRaises(FeatureFlagException) as context: client.upsert_flag('update-error-flag', custom_attributes={'test': 'value'}) - + self.assertIn('Failed to update feature gate', str(context.exception)) @patch('feature_flag_client.Statsig') @@ -669,17 +711,17 @@ def test_upsert_flag_update_api_error_raises_exception(self, mock_requests, mock def test_delete_flag_not_found(self, mock_requests, mock_statsig): """Test delete_flag when flag doesn't exist""" self._setup_mock_statsig(mock_statsig) - + # Mock GET request (flag doesn't exist) mock_requests.get.return_value = self._create_mock_response(200, {'data': []}) - + client = StatSigFeatureFlagClient(environment='test') - + result = client.delete_flag('nonexistent-flag') - + # Should return None (flag doesn't exist) self.assertIsNone(result) - + # Should only call GET, not DELETE or PATCH mock_requests.get.assert_called_once() mock_requests.delete.assert_not_called() @@ -690,38 +732,42 @@ def test_delete_flag_not_found(self, mock_requests, mock_statsig): def test_delete_flag_last_environment_deletes_entire_flag(self, mock_requests, mock_statsig): """Test delete_flag when current environment is the only one - should delete entire flag""" self._setup_mock_statsig(mock_statsig) - + existing_flag = { 'id': 'gate-delete-last', 'name': 'delete-last-flag', - 'rules': [{ - 'name': 'environment_toggle', - 'conditions': [], - 'environments': ['development'] # Only current environment (test -> development) - }] + 'rules': [ + { + 'name': 'environment_toggle', + 'conditions': [], + 'environments': ['development'], # Only current environment (test -> development) + } + ], } - + # Mock GET request (flag exists with only current environment) mock_requests.get.return_value = self._create_mock_response(200, {'data': [existing_flag]}) - + # Mock DELETE request (delete entire flag) mock_requests.delete.return_value = self._create_mock_response(200) - + client = StatSigFeatureFlagClient(environment='test') - + result = client.delete_flag('delete-last-flag') - + # Should return True (flag fully deleted) self.assertTrue(result) - + # Verify API calls mock_requests.get.assert_called_once() mock_requests.delete.assert_called_once_with( f'{STATSIG_API_BASE_URL}/gates/gate-delete-last', - headers={'STATSIG-API-KEY': MOCK_CONSOLE_KEY, - 'STATSIG-API-VERSION': STATSIG_API_VERSION, - 'Content-Type': 'application/json'}, - timeout=30 + headers={ + 'STATSIG-API-KEY': MOCK_CONSOLE_KEY, + 'STATSIG-API-VERSION': STATSIG_API_VERSION, + 'Content-Type': 'application/json', + }, + timeout=30, ) mock_requests.patch.assert_not_called() @@ -730,47 +776,55 @@ def test_delete_flag_last_environment_deletes_entire_flag(self, mock_requests, m def test_delete_flag_multiple_environments_removes_current_only(self, mock_requests, mock_statsig): """Test delete_flag when flag has multiple environments - should only remove current""" self._setup_mock_statsig(mock_statsig) - + existing_flag = { 'id': 'gate-delete-multi', 'name': 'delete-multi-flag', - 'rules': [{ - 'name': 'environment_toggle', - 'conditions': [], - 'environments': ['development', 'staging', 'production'] # Multiple environments - }] + 'rules': [ + { + 'name': 'environment_toggle', + 'conditions': [], + 'environments': ['development', 'staging', 'production'], # Multiple environments + } + ], } - + # Mock GET request (flag exists with multiple environments) mock_requests.get.return_value = self._create_mock_response(200, {'data': [existing_flag]}) - + # Mock PATCH request (remove environment) mock_requests.patch.return_value = self._create_mock_response(200) - + client = StatSigFeatureFlagClient(environment='test') # test -> development - + result = client.delete_flag('delete-multi-flag') - + # Should return False (environment removed, not full deletion) self.assertFalse(result) - + # Verify API calls mock_requests.get.assert_called_once() mock_requests.patch.assert_called_once_with( f'{STATSIG_API_BASE_URL}/gates/gate-delete-multi', - headers={'STATSIG-API-KEY': MOCK_CONSOLE_KEY, - 'STATSIG-API-VERSION': STATSIG_API_VERSION, - 'Content-Type': 'application/json'}, - data=json.dumps({ - 'id': 'gate-delete-multi', - 'name': 'delete-multi-flag', - 'rules': [{ - 'name': 'environment_toggle', - 'conditions': [], - 'environments': ['staging', 'production'] # development removed - }] - }), - timeout=30 + headers={ + 'STATSIG-API-KEY': MOCK_CONSOLE_KEY, + 'STATSIG-API-VERSION': STATSIG_API_VERSION, + 'Content-Type': 'application/json', + }, + data=json.dumps( + { + 'id': 'gate-delete-multi', + 'name': 'delete-multi-flag', + 'rules': [ + { + 'name': 'environment_toggle', + 'conditions': [], + 'environments': ['staging', 'production'], # development removed + } + ], + } + ), + timeout=30, ) mock_requests.delete.assert_not_called() @@ -779,38 +833,42 @@ def test_delete_flag_multiple_environments_removes_current_only(self, mock_reque def test_delete_flag_prod_environment_last_environment(self, mock_requests, mock_statsig): """Test delete_flag in prod environment when it's the last environment""" self._setup_mock_statsig(mock_statsig) - + existing_flag = { 'id': 'gate-delete-prod', 'name': 'delete-prod-flag', - 'rules': [{ - 'name': 'environment_toggle', - 'conditions': [], - 'environments': ['production'] # Only production environment - }] + 'rules': [ + { + 'name': 'environment_toggle', + 'conditions': [], + 'environments': ['production'], # Only production environment + } + ], } - + # Mock GET request (flag exists with only production environment) mock_requests.get.return_value = self._create_mock_response(200, {'data': [existing_flag]}) - + # Mock DELETE request (delete entire flag) mock_requests.delete.return_value = self._create_mock_response(200) - + client = StatSigFeatureFlagClient(environment='prod') - + result = client.delete_flag('delete-prod-flag') - + # Should return True (flag fully deleted) self.assertTrue(result) - + # Verify API calls mock_requests.get.assert_called_once() mock_requests.delete.assert_called_once_with( f'{STATSIG_API_BASE_URL}/gates/gate-delete-prod', - headers={'STATSIG-API-KEY': MOCK_CONSOLE_KEY, - 'STATSIG-API-VERSION': STATSIG_API_VERSION, - 'Content-Type': 'application/json'}, - timeout=30 + headers={ + 'STATSIG-API-KEY': MOCK_CONSOLE_KEY, + 'STATSIG-API-VERSION': STATSIG_API_VERSION, + 'Content-Type': 'application/json', + }, + timeout=30, ) @patch('feature_flag_client.Statsig') @@ -818,47 +876,59 @@ def test_delete_flag_prod_environment_last_environment(self, mock_requests, mock def test_delete_flag_prod_environment_multiple_environments(self, mock_requests, mock_statsig): """Test delete_flag in prod environment when multiple environments exist""" self._setup_mock_statsig(mock_statsig) - + existing_flag = { 'id': 'gate-delete-prod-multi', 'name': 'delete-prod-multi-flag', - 'rules': [{ - 'name': 'environment_toggle', - 'conditions': [{'type': 'custom_field', 'targetValue': ['value'], 'field': 'attr', 'operator': 'any'}], - 'environments': ['development', 'production'] # Multiple environments - }] + 'rules': [ + { + 'name': 'environment_toggle', + 'conditions': [ + {'type': 'custom_field', 'targetValue': ['value'], 'field': 'attr', 'operator': 'any'} + ], + 'environments': ['development', 'production'], # Multiple environments + } + ], } - + # Mock GET request (flag exists with multiple environments) mock_requests.get.return_value = self._create_mock_response(200, {'data': [existing_flag]}) - + # Mock PATCH request (remove production environment) mock_requests.patch.return_value = self._create_mock_response(200) - + client = StatSigFeatureFlagClient(environment='prod') - + result = client.delete_flag('delete-prod-multi-flag') - + # Should return False (environment removed, not full deletion) self.assertFalse(result) - + # Verify API calls mock_requests.get.assert_called_once() mock_requests.patch.assert_called_once_with( f'{STATSIG_API_BASE_URL}/gates/gate-delete-prod-multi', - headers={'STATSIG-API-KEY': MOCK_CONSOLE_KEY, - 'STATSIG-API-VERSION': STATSIG_API_VERSION, - 'Content-Type': 'application/json'}, - data=json.dumps({ - 'id': 'gate-delete-prod-multi', - 'name': 'delete-prod-multi-flag', - 'rules': [{ - 'name': 'environment_toggle', - 'conditions': [{'type': 'custom_field', 'targetValue': ['value'], 'field': 'attr', 'operator': 'any'}], - 'environments': ['development'] # production removed - }] - }), - timeout=30 + headers={ + 'STATSIG-API-KEY': MOCK_CONSOLE_KEY, + 'STATSIG-API-VERSION': STATSIG_API_VERSION, + 'Content-Type': 'application/json', + }, + data=json.dumps( + { + 'id': 'gate-delete-prod-multi', + 'name': 'delete-prod-multi-flag', + 'rules': [ + { + 'name': 'environment_toggle', + 'conditions': [ + {'type': 'custom_field', 'targetValue': ['value'], 'field': 'attr', 'operator': 'any'} + ], + 'environments': ['development'], # production removed + } + ], + } + ), + timeout=30, ) @patch('feature_flag_client.Statsig') @@ -866,63 +936,66 @@ def test_delete_flag_prod_environment_multiple_environments(self, mock_requests, def test_delete_flag_current_environment_not_present(self, mock_requests, mock_statsig): """Test delete_flag when current environment is not in the flag's environments""" self._setup_mock_statsig(mock_statsig) - + existing_flag = { 'id': 'gate-delete-not-present', 'name': 'delete-not-present-flag', - 'rules': [{ - 'name': 'environment_toggle', - 'conditions': [], - 'environments': ['staging', 'production'] # Current environment (development) not present - }] + 'rules': [ + { + 'name': 'environment_toggle', + 'conditions': [], + 'environments': ['staging', 'production'], # Current environment (development) not present + } + ], } - + # Mock GET request (flag exists but current environment not in it) mock_requests.get.return_value = self._create_mock_response(200, {'data': [existing_flag]}) - + # Mock PATCH request (no change needed, but method still called) mock_requests.patch.return_value = self._create_mock_response(200) - + client = StatSigFeatureFlagClient(environment='test') # test -> development - + result = client.delete_flag('delete-not-present-flag') - + # Should return False (no environment removed since it wasn't there) self.assertFalse(result) - + # Should not call PATCH since environment wasn't present mock_requests.get.assert_called_once() mock_requests.patch.assert_not_called() mock_requests.delete.assert_not_called() - @patch('feature_flag_client.Statsig') @patch('feature_flag_client.requests') def test_delete_flag_api_error_on_delete_raises_exception(self, mock_requests, mock_statsig): """Test delete_flag error handling when DELETE request fails""" self._setup_mock_statsig(mock_statsig) - + existing_flag = { 'id': 'gate-delete-error', 'name': 'delete-error-flag', - 'rules': [{ - 'name': 'environment_toggle', - 'conditions': [], - 'environments': ['development'] # Only current environment - }] + 'rules': [ + { + 'name': 'environment_toggle', + 'conditions': [], + 'environments': ['development'], # Only current environment + } + ], } - + # Mock GET request (flag exists) mock_requests.get.return_value = self._create_mock_response(200, {'data': [existing_flag]}) - + # Mock DELETE request failure mock_requests.delete.return_value = self._create_mock_response(403, {'error': 'Forbidden'}) - + client = StatSigFeatureFlagClient(environment='test') - + with self.assertRaises(FeatureFlagException) as context: client.delete_flag('delete-error-flag') - + self.assertIn('Failed to delete feature gate', str(context.exception)) @patch('feature_flag_client.Statsig') @@ -930,26 +1003,28 @@ def test_delete_flag_api_error_on_delete_raises_exception(self, mock_requests, m def test_delete_flag_api_error_on_patch_raises_exception(self, mock_requests, mock_statsig): """Test delete_flag error handling when PATCH request fails""" self._setup_mock_statsig(mock_statsig) - + existing_flag = { 'id': 'gate-patch-error', 'name': 'patch-error-flag', - 'rules': [{ - 'name': 'environment_toggle', - 'conditions': [], - 'environments': ['development', 'staging'] # Multiple environments - }] + 'rules': [ + { + 'name': 'environment_toggle', + 'conditions': [], + 'environments': ['development', 'staging'], # Multiple environments + } + ], } - + # Mock GET request (flag exists) mock_requests.get.return_value = self._create_mock_response(200, {'data': [existing_flag]}) - + # Mock PATCH request failure mock_requests.patch.return_value = self._create_mock_response(400, {'error': 'Bad request'}) - + client = StatSigFeatureFlagClient(environment='test') - + with self.assertRaises(FeatureFlagException) as context: client.delete_flag('patch-error-flag') - + self.assertIn('Failed to update feature gate', str(context.exception)) From 2ab44ff201df28ea061d238393e3267af531ab70 Mon Sep 17 00:00:00 2001 From: Landon Shumway Date: Thu, 2 Oct 2025 10:30:40 -0500 Subject: [PATCH 18/55] support list of custom attribute values --- .../feature-flag/custom_resource_handler.py | 124 +++++++++++++ .../feature-flag/feature_flag_client.py | 108 ++++------- .../handlers/manage_feature_flag.py | 2 +- .../tests/function/test_statsig_client.py | 173 ++++++++++++++---- 4 files changed, 300 insertions(+), 107 deletions(-) create mode 100644 backend/compact-connect/lambdas/python/feature-flag/custom_resource_handler.py diff --git a/backend/compact-connect/lambdas/python/feature-flag/custom_resource_handler.py b/backend/compact-connect/lambdas/python/feature-flag/custom_resource_handler.py new file mode 100644 index 000000000..fc8c0c14e --- /dev/null +++ b/backend/compact-connect/lambdas/python/feature-flag/custom_resource_handler.py @@ -0,0 +1,124 @@ +#!/usr/bin/env python3 +from abc import ABC, abstractmethod +from typing import TypedDict + +from aws_lambda_powertools.logging.lambda_context import build_lambda_context_model +from aws_lambda_powertools.utilities.typing import LambdaContext +from cc_common.config import logger + + +class CustomResourceResponse(TypedDict, total=False): + """Return body for the custom resource handler.""" + + PhysicalResourceId: str + Data: dict + NoEcho: bool + + +class CustomResourceHandler(ABC): + """Base class for custom resource migrations. + + This class provides a framework for implementing temporary data migrations as custom resources. + It handles the routing of CloudFormation events to appropriate methods and provides a consistent + logging pattern. + + Subclasses must implement the on_create, on_update, and on_delete methods. + + Instances of this class are callable and can be used directly as Lambda handlers. + """ + + def __init__(self, handler_name: str): + """Initialize the custom resource handler. + + :type handler_name: str + """ + self.handler_name = handler_name + + def __call__(self, event: dict, _context: LambdaContext) -> CustomResourceResponse | None: + return self._on_event(event, _context) + + def _on_event(self, event: dict, _context: LambdaContext) -> CustomResourceResponse | None: + """CloudFormation event handler using the CDK provider framework. + See: https://docs.aws.amazon.com/cdk/api/v2/python/aws_cdk.custom_resources/README.html + + This method routes the event to the appropriate handler method based on the request type. + + :param event: The lambda event with properties in ResourceProperties + :type event: dict + :param _context: Lambda context + :type _context: LambdaContext + :return: Optional result from the handler method + :rtype: Optional[CustomResourceResponse] + :raises ValueError: If the request type is not supported + """ + + # @logger.inject_lambda_context doesn't work on instance methods, so we'll build the context manually + lambda_context = build_lambda_context_model(_context) + logger.structure_logs(**lambda_context.__dict__) + + logger.info(f'{self.handler_name} handler started') + + properties = event.get('ResourceProperties', {}) + request_type = event['RequestType'] + + match request_type: + case 'Create': + try: + resp = self.on_create(properties) + except Exception as e: + logger.error(f'Error in {self.handler_name} creation', exc_info=e) + raise + case 'Update': + try: + resp = self.on_update(properties) + except Exception as e: + logger.error(f'Error in {self.handler_name} update', exc_info=e) + raise + case 'Delete': + try: + resp = self.on_delete(properties) + except Exception as e: + logger.error(f'Error in {self.handler_name} delete', exc_info=e) + raise + case _: + raise ValueError(f'Unexpected request type: {request_type}') + + logger.info(f'{self.handler_name} handler complete') + return resp + + @abstractmethod + def on_create(self, properties: dict) -> CustomResourceResponse | None: + """Handle Create events. + + This method should be implemented by subclasses to perform the migration when a resource is being created. + + :param properties: The ResourceProperties from the CloudFormation event + :type properties: dict + :return: Any result to be returned to CloudFormation + :rtype: Optional[CustomResourceResponse] + """ + + @abstractmethod + def on_update(self, properties: dict) -> CustomResourceResponse | None: + """Handle Update events. + + This method should be implemented by subclasses to perform the migration when a resource is being updated. + + :param properties: The ResourceProperties from the CloudFormation event + :type properties: dict + :return: Any result to be returned to CloudFormation + :rtype: Optional[CustomResourceResponse] + """ + + @abstractmethod + def on_delete(self, properties: dict) -> CustomResourceResponse | None: + """Handle Delete events. + + This method should be implemented by subclasses to handle deletion of the migration. In many cases, this can + be a no-op as the migration is temporary and deletion should have no effect. + + :param properties: The ResourceProperties from the CloudFormation event + :type properties: dict + :return: Any result to be returned to CloudFormation + :rtype: Optional[CustomResourceResponse] + """ diff --git a/backend/compact-connect/lambdas/python/feature-flag/feature_flag_client.py b/backend/compact-connect/lambdas/python/feature-flag/feature_flag_client.py index 117a7238d..058789519 100644 --- a/backend/compact-connect/lambdas/python/feature-flag/feature_flag_client.py +++ b/backend/compact-connect/lambdas/python/feature-flag/feature_flag_client.py @@ -110,17 +110,6 @@ def get_flag(self, flag_name: str) -> dict[str, Any] | None: :raises FeatureFlagException: If retrieval fails """ - @abstractmethod - def add_current_environment_to_flag(self, flag_id: str, flag_data: dict[str, Any]) -> bool: - """ - Add the current environment to an existing feature flag. - - :param flag_id: ID of the feature flag to update - :param flag_data: Current flag configuration - :return: True if successful - :raises FeatureFlagException: If update fails - """ - @abstractmethod def delete_flag(self, flag_name: str) -> bool: """ @@ -134,17 +123,6 @@ def delete_flag(self, flag_name: str) -> bool: :raises FeatureFlagException: If operation fails """ - @abstractmethod - def remove_current_environment_from_flag(self, flag_id: str, flag_data: dict[str, Any]) -> bool: - """ - Remove the current environment from a feature flag. - - :param flag_id: ID of the feature flag - :param flag_data: Current flag configuration - :return: True if environment was removed, False if environment wasn't present - :raises FeatureFlagException: If operation fails - """ - def _get_secret(self, secret_name: str) -> dict[str, Any]: """ Retrieve a secret from AWS Secrets Manager and return it as a JSON object. @@ -388,36 +366,23 @@ def upsert_flag( # Check if gate already exists existing_gate = self.get_flag(flag_name) - if self.environment.lower() == 'test': - # In test environment, create the gate if it doesn't exist - if existing_gate: - # Gate exists - update custom attributes if provided - gate_id = existing_gate.get('id') - if custom_attributes: - updated_gate = self._prepare_gate_update(existing_gate, custom_attributes, False) - self._update_gate(gate_id, updated_gate) - return existing_gate # Return existing gate data - else: - # Create new gate with development environment - return self._create_new_gate(flag_name, custom_attributes) - else: - # In beta/prod environment - if not existing_gate and not auto_enable: - # Gate doesn't exist and auto_enable is False - return empty dict to signal no action - return {} - elif not existing_gate and auto_enable: - # Gate doesn't exist but auto_enable is True - create it - return self._create_new_gate(flag_name, custom_attributes) - else: - # Gate exists - update it - gate_id = existing_gate.get('id') - - # Update the gate with new attributes and/or environment - updated_gate = self._prepare_gate_update(existing_gate, custom_attributes, auto_enable) + # According to our current policy, we always deploy the flag from our testing + # environment, and perform updates on any environment if the flag was set to auto enabled + if not existing_gate and (auto_enable or self.environment.lower() == 'test'): + # Create new gate with the environment associated with it + return self._create_new_gate(flag_name, custom_attributes) + + # according to our current policy, we always deploy + if existing_gate and (auto_enable or self.environment.lower() == 'test'): + # Gate exists - update custom attributes if provided + gate_id = existing_gate.get('id') + if custom_attributes: + updated_gate = self._prepare_gate_update(existing_gate, custom_attributes) self._update_gate(gate_id, updated_gate) + return existing_gate # Return existing gate data - # Return updated gate data - return self.get_flag(flag_name) or existing_gate + # Return empty dict (no action taken) + return {} def _create_new_gate(self, flag_name: str, custom_attributes: dict[str, Any] | None = None) -> dict[str, Any]: """ @@ -460,39 +425,44 @@ def _create_new_gate(self, flag_name: str, custom_attributes: dict[str, Any] | N raise FeatureFlagException(f'Failed to create feature gate: {response.status_code} - {response.text[:200]}') def _prepare_gate_update( - self, gate_data: dict[str, Any], custom_attributes: dict[str, Any] | None = None, add_current_env: bool = False + self, gate_data: dict[str, Any], custom_attributes: dict[str, Any] | None = None ) -> dict[str, Any]: """ Prepare an updated gate configuration with new custom attributes and/or environment. :param gate_data: Original gate configuration :param custom_attributes: New custom attributes to set (None = no change) - :param add_current_env: Whether to add the current environment :return: Updated gate configuration """ - updated_gate = gate_data.copy() + updated_gate_configuration = gate_data.copy() # Find the environment_toggle rule - for rule in updated_gate.get('rules', []): + for rule in updated_gate_configuration.get('rules', []): if rule.get('name') == 'environment_toggle': # Update custom attributes if provided if custom_attributes is not None: new_conditions = [] for key, value in custom_attributes.items(): + # if the value is a list, leave it as is + # else, convert to list + if isinstance(value, str): + value = [value] + elif not isinstance(value, list): + raise FeatureFlagException(f'Custom attribute value must be a string or list: {value}') + new_conditions.append( - {'type': 'custom_field', 'targetValue': [value], 'field': key, 'operator': 'any'} + {'type': 'custom_field', 'targetValue': value, 'field': key, 'operator': 'any'} ) rule['conditions'] = new_conditions # Add current environment if requested - if add_current_env: - statsig_tier = STATSIG_ENVIRONMENT_MAPPING.get(self.environment.lower(), STATSIG_DEVELOPMENT_TIER) - current_environments = rule.get('environments', []) - if statsig_tier not in current_environments: - rule['environments'] = current_environments + [statsig_tier] + statsig_tier = STATSIG_ENVIRONMENT_MAPPING.get(self.environment.lower(), STATSIG_DEVELOPMENT_TIER) + current_environments = rule.get('environments', []) + if statsig_tier not in current_environments: + rule['environments'] = current_environments + [statsig_tier] break - return updated_gate + return updated_gate_configuration def _update_gate(self, gate_id: str, gate_data: dict[str, Any]) -> bool: """ @@ -531,18 +501,6 @@ def get_flag(self, flag_name: str) -> dict[str, Any] | None: else: raise FeatureFlagException(f'Failed to fetch gates: {response.status_code} - {response.text[:200]}') - def add_current_environment_to_flag(self, flag_id: str, flag_data: dict[str, Any]) -> bool: - """ - Add the current environment to an existing feature gate. - - :param flag_id: ID of the feature gate to update - :param flag_data: Current gate configuration - :return: True if successful - :raises FeatureFlagException: If update fails - """ - updated_gate = self._prepare_gate_update(flag_data, None, add_current_env=True) - return self._update_gate(flag_id, updated_gate) - def delete_flag(self, flag_name: str) -> bool | None: """ Delete a feature gate or remove current environment from it. @@ -586,10 +544,10 @@ def delete_flag(self, flag_name: str) -> bool | None: ) else: # Remove only the current environment - removed = self.remove_current_environment_from_flag(flag_id, flag_data) + removed = self._remove_current_environment_from_flag(flag_id, flag_data) return False if removed else False # Environment removed, not full deletion - def remove_current_environment_from_flag(self, flag_id: str, flag_data: dict[str, Any]) -> bool: + def _remove_current_environment_from_flag(self, flag_id: str, flag_data: dict[str, Any]) -> bool: """ Remove the current environment from a feature gate. diff --git a/backend/compact-connect/lambdas/python/feature-flag/handlers/manage_feature_flag.py b/backend/compact-connect/lambdas/python/feature-flag/handlers/manage_feature_flag.py index d32fc903a..a96557fa9 100644 --- a/backend/compact-connect/lambdas/python/feature-flag/handlers/manage_feature_flag.py +++ b/backend/compact-connect/lambdas/python/feature-flag/handlers/manage_feature_flag.py @@ -8,7 +8,7 @@ import os from cc_common.config import logger -from cc_common.custom_resource_handler import CustomResourceHandler, CustomResourceResponse +from custom_resource_handler import CustomResourceHandler, CustomResourceResponse from feature_flag_client import StatSigFeatureFlagClient diff --git a/backend/compact-connect/lambdas/python/feature-flag/tests/function/test_statsig_client.py b/backend/compact-connect/lambdas/python/feature-flag/tests/function/test_statsig_client.py index 2445dce55..260ddbebb 100644 --- a/backend/compact-connect/lambdas/python/feature-flag/tests/function/test_statsig_client.py +++ b/backend/compact-connect/lambdas/python/feature-flag/tests/function/test_statsig_client.py @@ -559,7 +559,7 @@ def test_upsert_flag_prod_environment_existing_flag_auto_enable_true(self, mock_ self.assertEqual(result['id'], 'gate-existing-prod') # Verify API calls - self.assertEqual(mock_requests.get.call_count, 2) # Once to check, once to return updated + self.assertEqual(mock_requests.get.call_count, 1) # Once to check, once to return updated mock_requests.patch.assert_called_once_with( f'{STATSIG_API_BASE_URL}/gates/gate-existing-prod', headers={ @@ -587,7 +587,9 @@ def test_upsert_flag_prod_environment_existing_flag_auto_enable_true(self, mock_ @patch('feature_flag_client.Statsig') @patch('feature_flag_client.requests') - def test_upsert_flag_prod_environment_existing_flag_auto_enable_false(self, mock_requests, mock_statsig): + def test_upsert_flag_prod_environment_existing_flag_auto_enable_false_should_not_update_flag( + self, mock_requests, mock_statsig + ): """Test upsert in prod environment with existing flag and autoEnable=False""" self._setup_mock_statsig(mock_statsig) @@ -614,37 +616,11 @@ def test_upsert_flag_prod_environment_existing_flag_auto_enable_false(self, mock client = StatSigFeatureFlagClient(environment='prod') - result = client.upsert_flag('existing-prod-flag-2', auto_enable=False, custom_attributes={'new': 'attr'}) - - # Verify result - self.assertEqual(result['id'], 'gate-existing-prod-2') + client.upsert_flag('existing-prod-flag-2', auto_enable=False, custom_attributes={'new': 'attr'}) # Verify API calls - self.assertEqual(mock_requests.get.call_count, 2) # Once to check, once to return updated - mock_requests.patch.assert_called_once_with( - f'{STATSIG_API_BASE_URL}/gates/gate-existing-prod-2', - headers={ - 'STATSIG-API-KEY': MOCK_CONSOLE_KEY, - 'STATSIG-API-VERSION': STATSIG_API_VERSION, - 'Content-Type': 'application/json', - }, - data=json.dumps( - { - 'id': 'gate-existing-prod-2', - 'name': 'existing-prod-flag-2', - 'rules': [ - { - 'name': 'environment_toggle', - 'conditions': [ - {'type': 'custom_field', 'targetValue': ['attr'], 'field': 'new', 'operator': 'any'} - ], - 'environments': ['development', 'production'], - } - ], - } - ), - timeout=30, - ) + self.assertEqual(mock_requests.get.call_count, 1) + mock_requests.patch.assert_not_called() @patch('feature_flag_client.Statsig') @patch('feature_flag_client.requests') @@ -1028,3 +1004,138 @@ def test_delete_flag_api_error_on_patch_raises_exception(self, mock_requests, mo client.delete_flag('patch-error-flag') self.assertIn('Failed to update feature gate', str(context.exception)) + + @patch('feature_flag_client.Statsig') + @patch('feature_flag_client.requests') + def test_upsert_flag_custom_attributes_as_string(self, mock_requests, mock_statsig): + """Test upsert_flag with custom attributes as string values""" + self._setup_mock_statsig(mock_statsig) + + # Mock GET request (flag doesn't exist) + mock_requests.get.return_value = self._create_mock_response(200, {'data': []}) + + # Mock POST request (create flag) + created_flag = {'id': 'gate-string-attrs', 'name': 'string-attrs-flag', 'data': {'id': 'gate-string-attrs'}} + mock_requests.post.return_value = self._create_mock_response(201, created_flag) + + client = StatSigFeatureFlagClient(environment='test') + + result = client.upsert_flag('string-attrs-flag', custom_attributes={'region': 'us-east-1', 'feature': 'new'}) + + # Verify result + self.assertEqual(result['id'], 'gate-string-attrs') + + # Verify API calls - string values should be converted to lists + mock_requests.post.assert_called_once_with( + f'{STATSIG_API_BASE_URL}/gates', + headers={ + 'STATSIG-API-KEY': MOCK_CONSOLE_KEY, + 'STATSIG-API-VERSION': STATSIG_API_VERSION, + 'Content-Type': 'application/json', + }, + data=json.dumps( + { + 'name': 'string-attrs-flag', + 'description': 'Feature gate managed by CDK for string-attrs-flag feature', + 'isEnabled': True, + 'rules': [ + { + 'name': 'environment_toggle', + 'conditions': [ + { + 'type': 'custom_field', + 'targetValue': ['us-east-1'], + 'field': 'region', + 'operator': 'any', + }, + {'type': 'custom_field', 'targetValue': ['new'], 'field': 'feature', 'operator': 'any'}, + ], + 'environments': ['development'], + 'passPercentage': 100, + } + ], + } + ), + timeout=30, + ) + + @patch('feature_flag_client.Statsig') + @patch('feature_flag_client.requests') + def test_upsert_flag_custom_attributes_as_list(self, mock_requests, mock_statsig): + """Test upsert_flag with custom attributes as list values""" + self._setup_mock_statsig(mock_statsig) + + # Mock GET request (flag doesn't exist) + mock_requests.get.return_value = self._create_mock_response(200, {'data': []}) + + # Mock POST request (create flag) + created_flag = {'id': 'gate-list-attrs', 'name': 'list-attrs-flag', 'data': {'id': 'gate-list-attrs'}} + mock_requests.post.return_value = self._create_mock_response(201, created_flag) + + client = StatSigFeatureFlagClient(environment='test') + + result = client.upsert_flag('list-attrs-flag', custom_attributes={'licenseType': ['slp', 'audiologist']}) + + # Verify result + self.assertEqual(result['id'], 'gate-list-attrs') + + # Verify API calls - list values should be kept as lists + mock_requests.post.assert_called_once_with( + f'{STATSIG_API_BASE_URL}/gates', + headers={ + 'STATSIG-API-KEY': MOCK_CONSOLE_KEY, + 'STATSIG-API-VERSION': STATSIG_API_VERSION, + 'Content-Type': 'application/json', + }, + data=json.dumps( + { + 'name': 'list-attrs-flag', + 'description': 'Feature gate managed by CDK for list-attrs-flag feature', + 'isEnabled': True, + 'rules': [ + { + 'name': 'environment_toggle', + 'conditions': [ + { + 'type': 'custom_field', + 'targetValue': [['slp', 'audiologist']], + 'field': 'licenseType', + 'operator': 'any', + } + ], + 'environments': ['development'], + 'passPercentage': 100, + } + ], + } + ), + timeout=30, + ) + + @patch('feature_flag_client.Statsig') + @patch('feature_flag_client.requests') + def test_upsert_flag_custom_attributes_invalid_type_raises_exception(self, mock_requests, mock_statsig): + """Test upsert_flag with custom attributes as invalid type (dict) raises exception""" + self._setup_mock_statsig(mock_statsig) + + existing_flag = { + 'id': 'gate-invalid-attrs', + 'name': 'invalid-attrs-flag', + 'rules': [{'name': 'environment_toggle', 'conditions': [], 'environments': ['development']}], + } + + # Mock GET request (flag exists) + mock_requests.get.return_value = self._create_mock_response(200, {'data': [existing_flag]}) + + client = StatSigFeatureFlagClient(environment='test') + + # Try to update with invalid custom attribute type (dict) + with self.assertRaises(FeatureFlagException) as context: + client.upsert_flag( + 'invalid-attrs-flag', + custom_attributes={ + 'invalid_attr': {'nested': 'dict'} # This should raise an exception + }, + ) + + self.assertIn('Custom attribute value must be a string or list', str(context.exception)) From 4ce4afa74faab3bb63533394e3e8ce5ef07a1f27 Mon Sep 17 00:00:00 2001 From: Landon Shumway Date: Thu, 2 Oct 2025 14:23:33 -0500 Subject: [PATCH 19/55] Creating one rule per environment to separate rules --- .../feature-flag/feature_flag_client.py | 255 ++++++---- .../tests/function/test_statsig_client.py | 434 ++++++++++-------- 2 files changed, 393 insertions(+), 296 deletions(-) diff --git a/backend/compact-connect/lambdas/python/feature-flag/feature_flag_client.py b/backend/compact-connect/lambdas/python/feature-flag/feature_flag_client.py index 058789519..aebee0560 100644 --- a/backend/compact-connect/lambdas/python/feature-flag/feature_flag_client.py +++ b/backend/compact-connect/lambdas/python/feature-flag/feature_flag_client.py @@ -354,53 +354,65 @@ def upsert_flag( """ Create or update a feature gate in StatSig. - In test environment: Creates a new gate if it doesn't exist. - In beta/prod: Updates existing gate to add current environment if auto_enable is True. + Each environment has its own rule (e.g., 'test-rule', 'beta-rule', 'prod-rule'). + - If auto_enable is False: passPercentage is set to 0 (disabled) + - If auto_enable is True: passPercentage is set to 100 (enabled) and custom attributes are applied :param flag_name: Name of the feature gate - :param auto_enable: If True, enable the flag in the current environment (beta/prod only) - :param custom_attributes: Optional custom attributes for targeting + :param auto_enable: If True, enable the flag (passPercentage=100); if False, disable it (passPercentage=0) + :param custom_attributes: Optional custom attributes for targeting (only applied if auto_enable=True) :return: Flag data (with 'id' field) :raises FeatureFlagException: If operation fails """ # Check if gate already exists existing_gate = self.get_flag(flag_name) - # According to our current policy, we always deploy the flag from our testing - # environment, and perform updates on any environment if the flag was set to auto enabled - if not existing_gate and (auto_enable or self.environment.lower() == 'test'): - # Create new gate with the environment associated with it - return self._create_new_gate(flag_name, custom_attributes) - - # according to our current policy, we always deploy - if existing_gate and (auto_enable or self.environment.lower() == 'test'): - # Gate exists - update custom attributes if provided + if not existing_gate: + # Create new gate with environment-specific rule + return self._create_new_gate(flag_name, auto_enable, custom_attributes) + else: + # Gate exists - check if environment rule exists gate_id = existing_gate.get('id') - if custom_attributes: - updated_gate = self._prepare_gate_update(existing_gate, custom_attributes) + rule_name = f'{self.environment.lower()}-rule' + environment_rule = self._find_environment_rule(existing_gate, rule_name) + + if not environment_rule: + # Environment rule doesn't exist - add it + updated_gate = self._add_environment_rule(existing_gate, auto_enable, custom_attributes) self._update_gate(gate_id, updated_gate) - return existing_gate # Return existing gate data + else: + # Environment rule exists, only update the rule if auto enabled or development environment + if auto_enable or self._is_development_environment(): + # Update the rule with new settings + updated_gate = self._update_environment_rule(existing_gate, auto_enable, custom_attributes) + self._update_gate(gate_id, updated_gate) - # Return empty dict (no action taken) - return {} + return existing_gate - def _create_new_gate(self, flag_name: str, custom_attributes: dict[str, Any] | None = None) -> dict[str, Any]: + def _is_development_environment(self) -> bool: + """ + Check if the current environment is a development environment. """ - Create a new feature gate in StatSig with the current environment enabled. + return self.environment.lower() != 'prod' and self.environment.lower() != 'beta' + + def _create_new_gate(self, flag_name: str, auto_enable: bool, custom_attributes: dict[str, Any] | None = None) -> dict[str, Any]: + """ + Create a new feature gate in StatSig with an environment-specific rule. :param flag_name: Name of the feature gate - :param custom_attributes: Optional custom attributes for targeting + :param auto_enable: If True, passPercentage=100; if False, passPercentage=0 + :param custom_attributes: Optional custom attributes for targeting (only applied if auto_enable=True) :return: Created gate data (with 'id' field) :raises FeatureFlagException: If creation fails """ # Get the StatSig environment tier for the current environment statsig_tier = STATSIG_ENVIRONMENT_MAPPING.get(self.environment.lower(), STATSIG_DEVELOPMENT_TIER) + rule_name = f'{self.environment.lower()}-rule' - # Build conditions for custom attributes if provided + # Build conditions for custom attributes if auto_enable is True conditions = [] if custom_attributes: - for key, value in custom_attributes.items(): - conditions.append({'type': 'custom_field', 'targetValue': [value], 'field': key, 'operator': 'any'}) + conditions = self._build_conditions_from_attributes(custom_attributes) # Build the feature gate payload gate_payload = { @@ -409,10 +421,10 @@ def _create_new_gate(self, flag_name: str, custom_attributes: dict[str, Any] | N 'isEnabled': True, 'rules': [ { - 'name': 'environment_toggle', + 'name': rule_name, 'conditions': conditions, 'environments': [statsig_tier], - 'passPercentage': 100, + 'passPercentage': 100 if auto_enable or self._is_development_environment() else 0, } ], } @@ -424,45 +436,102 @@ def _create_new_gate(self, flag_name: str, custom_attributes: dict[str, Any] | N else: raise FeatureFlagException(f'Failed to create feature gate: {response.status_code} - {response.text[:200]}') - def _prepare_gate_update( - self, gate_data: dict[str, Any], custom_attributes: dict[str, Any] | None = None - ) -> dict[str, Any]: + def _find_environment_rule(self, gate_data: dict[str, Any], rule_name: str) -> dict[str, Any] | None: + """ + Find an environment-specific rule in the gate data. + + :param gate_data: Gate configuration + :param rule_name: Name of the rule to find (e.g., 'test-rule', 'beta-rule', 'prod-rule') + :return: Rule data if found, None otherwise + """ + for rule in gate_data.get('rules', []): + if rule.get('name') == rule_name: + return rule + return None + + def _build_conditions_from_attributes(self, custom_attributes: dict[str, Any]) -> list[dict[str, Any]]: + """ + Build StatSig conditions from custom attributes. + + :param custom_attributes: Dictionary of custom attributes + :return: List of condition dictionaries + :raises FeatureFlagException: If attribute value is not a string or list + """ + conditions = [] + for key, value in custom_attributes.items(): + # Convert strings to lists, keep lists as-is, reject other types + if isinstance(value, str): + value = [value] + elif not isinstance(value, list): + raise FeatureFlagException(f'Custom attribute value must be a string or list: {value}') + + conditions.append( + {'type': 'custom_field', 'targetValue': value, 'field': key, 'operator': 'any'} + ) + return conditions + + def _add_environment_rule(self, gate_data: dict[str, Any], auto_enable: bool, custom_attributes: dict[str, Any] | None = None) -> dict[str, Any]: + """ + Add an environment-specific rule to an existing gate. + + :param gate_data: Original gate configuration + :param auto_enable: If True, passPercentage=100; if False, passPercentage=0 + :param custom_attributes: Optional custom attributes for targeting (only applied if auto_enable=True) + :return: Updated gate configuration + """ + updated_gate = gate_data.copy() + statsig_tier = STATSIG_ENVIRONMENT_MAPPING.get(self.environment.lower(), STATSIG_DEVELOPMENT_TIER) + rule_name = f'{self.environment.lower()}-rule' + + # Build conditions if auto_enable is True + conditions = [] + if auto_enable and custom_attributes: + conditions = self._build_conditions_from_attributes(custom_attributes) + + # Add new environment rule + new_rule = { + 'name': rule_name, + 'conditions': conditions, + 'environments': [statsig_tier], + 'passPercentage': 100 if auto_enable or self._is_development_environment() else 0, + } + + # Ensure rules list exists and add the new rule + if 'rules' not in updated_gate: + updated_gate['rules'] = [] + updated_gate['rules'].append(new_rule) + + return updated_gate + + def _update_environment_rule(self, gate_data: dict[str, Any], auto_enable: bool, custom_attributes: dict[str, Any] | None = None) -> dict[str, Any]: """ - Prepare an updated gate configuration with new custom attributes and/or environment. + Update an existing environment-specific rule in the gate. :param gate_data: Original gate configuration - :param custom_attributes: New custom attributes to set (None = no change) + :param auto_enable: If True, passPercentage=100; if False, passPercentage=0 + :param custom_attributes: Optional custom attributes for targeting :return: Updated gate configuration """ - updated_gate_configuration = gate_data.copy() - - # Find the environment_toggle rule - for rule in updated_gate_configuration.get('rules', []): - if rule.get('name') == 'environment_toggle': - # Update custom attributes if provided - if custom_attributes is not None: - new_conditions = [] - for key, value in custom_attributes.items(): - # if the value is a list, leave it as is - # else, convert to list - if isinstance(value, str): - value = [value] - elif not isinstance(value, list): - raise FeatureFlagException(f'Custom attribute value must be a string or list: {value}') - - new_conditions.append( - {'type': 'custom_field', 'targetValue': value, 'field': key, 'operator': 'any'} - ) - rule['conditions'] = new_conditions - - # Add current environment if requested - statsig_tier = STATSIG_ENVIRONMENT_MAPPING.get(self.environment.lower(), STATSIG_DEVELOPMENT_TIER) - current_environments = rule.get('environments', []) - if statsig_tier not in current_environments: - rule['environments'] = current_environments + [statsig_tier] + updated_gate = gate_data.copy() + rule_name = f'{self.environment.lower()}-rule' + statsig_tier = STATSIG_ENVIRONMENT_MAPPING.get(self.environment.lower(), STATSIG_DEVELOPMENT_TIER) + + # Find and update the environment rule + for rule in updated_gate.get('rules', []): + if rule.get('name') == rule_name: + # Update passPercentage + rule['passPercentage'] = 100 if auto_enable or self._is_development_environment() else 0 + + # Update conditions if custom_attributes provided + if custom_attributes: + rule['conditions'] = self._build_conditions_from_attributes(custom_attributes) + + # Ensure environment tier is set + if statsig_tier not in rule.get('environments', []): + rule['environments'] = rule.get('environments', []) + [statsig_tier] break - return updated_gate_configuration + return updated_gate def _update_gate(self, gate_id: str, gate_data: dict[str, Any]) -> bool: """ @@ -503,13 +572,13 @@ def get_flag(self, flag_name: str) -> dict[str, Any] | None: def delete_flag(self, flag_name: str) -> bool | None: """ - Delete a feature gate or remove current environment from it. + Delete a feature gate or remove current environment rule from it. - If the gate has multiple environments, only the current environment is removed. - If the gate has only the current environment, the entire gate is deleted. + If the gate has only the current environment's rule, the entire gate is deleted. + If the gate has multiple environment rules, only the current environment's rule is removed. :param flag_name: Name of the feature flag to delete - :return: True if flag was fully deleted, False if only environment was removed, None if flag doesn't exist + :return: True if flag was fully deleted, False if only environment rule was removed, None if flag doesn't exist :raises FeatureFlagException: If operation fails """ # Get the flag data first @@ -521,19 +590,19 @@ def delete_flag(self, flag_name: str) -> bool | None: if not flag_id: raise FeatureFlagException(f'Flag data missing ID field: {flag_name}') - # Get the StatSig environment tier for the current environment - statsig_tier = STATSIG_ENVIRONMENT_MAPPING[self.environment.lower()] + rule_name = f'{self.environment.lower()}-rule' + + # Check if current environment rule exists + environment_rule = self._find_environment_rule(flag_data, rule_name) + if not environment_rule: + # Environment rule doesn't exist, nothing to delete + return False - # Find the environment_toggle rule and check environment count - environments_in_flag = [] - for rule in flag_data.get('rules', []): - if rule.get('name') == 'environment_toggle': - environments_in_flag = rule.get('environments', []) - break + # Count total number of rules in the gate + total_rules = len(flag_data.get('rules', [])) - # Check if current environment is the only one (or one of only one) - if len(environments_in_flag) <= 1 and statsig_tier in environments_in_flag: - # Delete the entire gate + # If this is the only rule, delete the entire gate + if total_rules == 1: response = self._make_console_api_request('DELETE', f'/gates/{flag_id}') if response.status_code in [200, 204]: @@ -543,43 +612,29 @@ def delete_flag(self, flag_name: str) -> bool | None: f'Failed to delete feature gate: {response.status_code} - {response.text[:200]}' ) else: - # Remove only the current environment - removed = self._remove_current_environment_from_flag(flag_id, flag_data) - return False if removed else False # Environment removed, not full deletion + # Remove only the current environment's rule + removed = self._remove_environment_rule_from_flag(flag_id, flag_data, rule_name) + return False if removed else False # Environment rule removed, not full deletion - def _remove_current_environment_from_flag(self, flag_id: str, flag_data: dict[str, Any]) -> bool: + def _remove_environment_rule_from_flag(self, flag_id: str, flag_data: dict[str, Any], rule_name: str) -> bool: """ - Remove the current environment from a feature gate. + Remove an environment-specific rule from a feature gate. :param flag_id: ID of the feature gate :param flag_data: Current flag configuration - :return: True if environment was removed, False if it wasn't present + :param rule_name: Name of the rule to remove (e.g., 'test-rule', 'beta-rule', 'prod-rule') + :return: True if rule was removed, False if it wasn't present :raises FeatureFlagException: If operation fails """ - # Get the StatSig environment tier for the current environment - statsig_tier = STATSIG_ENVIRONMENT_MAPPING.get(self.environment.lower(), STATSIG_DEVELOPMENT_TIER) - - # Check if current environment is present - environment_present = False - for rule in flag_data.get('rules', []): - if rule.get('name') == 'environment_toggle': - current_environments = rule.get('environments', []) - if statsig_tier in current_environments: - environment_present = True - break + # Prepare updated gate with the environment rule removed + updated_gate = flag_data.copy() + updated_rules = [rule for rule in updated_gate.get('rules', []) if rule.get('name') != rule_name] - if not environment_present: + # If no rules were removed, the rule wasn't present + if len(updated_rules) == len(updated_gate.get('rules', [])): return False - # Prepare updated gate with environment removed - updated_gate = flag_data.copy() - for rule in updated_gate.get('rules', []): - if rule.get('name') == 'environment_toggle': - current_environments = rule.get('environments', []) - if statsig_tier in current_environments: - current_environments.remove(statsig_tier) - rule['environments'] = current_environments - break + updated_gate['rules'] = updated_rules # Update the gate self._update_gate(flag_id, updated_gate) diff --git a/backend/compact-connect/lambdas/python/feature-flag/tests/function/test_statsig_client.py b/backend/compact-connect/lambdas/python/feature-flag/tests/function/test_statsig_client.py index 260ddbebb..37c868ced 100644 --- a/backend/compact-connect/lambdas/python/feature-flag/tests/function/test_statsig_client.py +++ b/backend/compact-connect/lambdas/python/feature-flag/tests/function/test_statsig_client.py @@ -28,7 +28,7 @@ def setUp(self): # Set up mock secrets manager with StatSig credentials secrets_client = self.create_mock_secrets_manager() - for env in ['test', 'prod']: + for env in ['test', 'beta', 'prod']: secrets_client.create_secret( Name=f'compact-connect/env/{env}/statsig/credentials', SecretString=json.dumps({'serverKey': MOCK_SERVER_KEY, 'consoleKey': MOCK_CONSOLE_KEY}), @@ -264,7 +264,7 @@ def _create_mock_response(self, status_code: int, json_data: dict = None): @patch('feature_flag_client.Statsig') @patch('feature_flag_client.requests') def test_upsert_flag_create_new_in_test_environment(self, mock_requests, mock_statsig): - """Test creating a new flag in test environment""" + """Test creating a new flag in test environment with auto_enable=False (passPercentage=100 for dev)""" self._setup_mock_statsig(mock_statsig) # Mock GET request (flag doesn't exist) @@ -284,7 +284,7 @@ def test_upsert_flag_create_new_in_test_environment(self, mock_requests, mock_st # Verify API calls mock_requests.get.assert_called_once() - # Verify POST payload + # Verify POST payload - test environment always gets passPercentage=100 regardless of auto_enable mock_requests.post.assert_called_once_with( f'{STATSIG_API_BASE_URL}/gates', headers={ @@ -299,7 +299,7 @@ def test_upsert_flag_create_new_in_test_environment(self, mock_requests, mock_st 'isEnabled': True, 'rules': [ { - 'name': 'environment_toggle', + 'name': 'test-rule', 'conditions': [ { 'type': 'custom_field', @@ -309,7 +309,7 @@ def test_upsert_flag_create_new_in_test_environment(self, mock_requests, mock_st } ], 'environments': ['development'], - 'passPercentage': 100, + 'passPercentage': 100, # Always 100 for test environment } ], } @@ -353,7 +353,7 @@ def test_upsert_flag_create_new_in_test_environment_no_attributes(self, mock_req 'isEnabled': True, 'rules': [ { - 'name': 'environment_toggle', + 'name': 'test-rule', 'conditions': [], 'environments': ['development'], 'passPercentage': 100, @@ -367,7 +367,7 @@ def test_upsert_flag_create_new_in_test_environment_no_attributes(self, mock_req @patch('feature_flag_client.Statsig') @patch('feature_flag_client.requests') def test_upsert_flag_update_existing_in_test_environment(self, mock_requests, mock_statsig): - """Test updating an existing flag in test environment""" + """Test updating an existing flag in test environment (test-rule already exists, no modifications in test)""" self._setup_mock_statsig(mock_statsig) existing_flag = { @@ -375,30 +375,27 @@ def test_upsert_flag_update_existing_in_test_environment(self, mock_requests, mo 'name': 'existing-flag', 'rules': [ { - 'name': 'environment_toggle', + 'name': 'test-rule', 'conditions': [{'field': 'old_attr', 'targetValue': ['old_value']}], 'environments': ['development'], + 'passPercentage': 100, } ], } - # Mock GET requests (flag exists, then return updated flag) - mock_requests.get.side_effect = [ - self._create_mock_response(200, {'data': [existing_flag]}), # First call to check existence - self._create_mock_response(200, {'data': [existing_flag]}), # Second call after update - ] - - # Mock PATCH request (update flag) + # Mock GET request (flag exists with test-rule) + mock_requests.get.return_value = self._create_mock_response(200, {'data': [existing_flag]}) + # Mock PATCH request (update test-rule) mock_requests.patch.return_value = self._create_mock_response(200) client = StatSigFeatureFlagClient(environment='test') - result = client.upsert_flag('existing-flag', custom_attributes={'new_attr': 'new_value'}) + result = client.upsert_flag('existing-flag', auto_enable=False, custom_attributes={'new_attr': 'new_value'}) - # Verify result + # Verify result - no modification happens in test when rule already exists self.assertEqual(result['id'], 'gate-789') - # Verify API calls + # Verify API calls - no PATCH since test environment doesn't modify existing rules self.assertEqual(1, mock_requests.get.call_count) mock_requests.patch.assert_called_once_with( f'{STATSIG_API_BASE_URL}/gates/gate-789', @@ -413,7 +410,7 @@ def test_upsert_flag_update_existing_in_test_environment(self, mock_requests, mo 'name': 'existing-flag', 'rules': [ { - 'name': 'environment_toggle', + 'name': 'test-rule', 'conditions': [ { 'type': 'custom_field', @@ -423,6 +420,7 @@ def test_upsert_flag_update_existing_in_test_environment(self, mock_requests, mo } ], 'environments': ['development'], + 'passPercentage': 100, } ], } @@ -430,52 +428,52 @@ def test_upsert_flag_update_existing_in_test_environment(self, mock_requests, mo timeout=30, ) - @patch('feature_flag_client.Statsig') - @patch('feature_flag_client.requests') - def test_upsert_flag_existing_in_test_no_changes(self, mock_requests, mock_statsig): - """Test updating an existing flag in test environment with no custom attributes""" - self._setup_mock_statsig(mock_statsig) - - existing_flag = { - 'id': 'gate-unchanged', - 'name': 'unchanged-flag', - 'rules': [{'name': 'environment_toggle', 'conditions': [], 'environments': ['development']}], - } - - # Mock GET request (flag exists) - mock_requests.get.return_value = self._create_mock_response(200, {'data': [existing_flag]}) - - client = StatSigFeatureFlagClient(environment='test') - - result = client.upsert_flag('unchanged-flag') - - # Verify result - self.assertEqual(result['id'], 'gate-unchanged') - - # Verify no PATCH was called since no changes - mock_requests.get.assert_called_once() - mock_requests.patch.assert_not_called() - mock_requests.post.assert_not_called() - @patch('feature_flag_client.Statsig') @patch('feature_flag_client.requests') def test_upsert_flag_prod_environment_auto_enable_false_no_existing_flag(self, mock_requests, mock_statsig): - """Test upsert in prod environment with autoEnable=False and no existing flag""" + """Test upsert in prod environment with autoEnable=False and no existing flag - creates with passPercentage=0""" self._setup_mock_statsig(mock_statsig) # Mock GET request (flag doesn't exist) mock_requests.get.return_value = self._create_mock_response(200, {'data': []}) + # Mock POST request (create flag with passPercentage=0) + created_flag = {'id': 'gate-prod-disabled', 'name': 'prod-flag', 'data': {'id': 'gate-prod-disabled'}} + mock_requests.post.return_value = self._create_mock_response(201, created_flag) + client = StatSigFeatureFlagClient(environment='prod') result = client.upsert_flag('prod-flag', auto_enable=False) - # Should return empty dict (no action taken) - self.assertEqual(result, {}) + # Verify result + self.assertEqual(result['id'], 'gate-prod-disabled') - # Should only call GET, not POST + # Verify API calls mock_requests.get.assert_called_once() - mock_requests.post.assert_not_called() + mock_requests.post.assert_called_once_with( + f'{STATSIG_API_BASE_URL}/gates', + headers={ + 'STATSIG-API-KEY': MOCK_CONSOLE_KEY, + 'STATSIG-API-VERSION': STATSIG_API_VERSION, + 'Content-Type': 'application/json', + }, + data=json.dumps( + { + 'name': 'prod-flag', + 'description': 'Feature gate managed by CDK for prod-flag feature', + 'isEnabled': True, + 'rules': [ + { + 'name': 'prod-rule', + 'conditions': [], + 'environments': ['production'], + 'passPercentage': 0, # Disabled in prod when auto_enable=False + } + ], + } + ), + timeout=30, + ) @patch('feature_flag_client.Statsig') @patch('feature_flag_client.requests') @@ -513,7 +511,7 @@ def test_upsert_flag_prod_environment_auto_enable_true_no_existing_flag(self, mo 'isEnabled': True, 'rules': [ { - 'name': 'environment_toggle', + 'name': 'prod-rule', 'conditions': [], 'environments': ['production'], 'passPercentage': 100, @@ -524,10 +522,73 @@ def test_upsert_flag_prod_environment_auto_enable_true_no_existing_flag(self, mo timeout=30, ) + @patch('feature_flag_client.Statsig') + @patch('feature_flag_client.requests') + def test_upsert_flag_beta_environment_auto_enable_false_no_existing_rule_create_rule(self, mock_requests, mock_statsig): + """Test upsert in prod environment with autoEnable=True and no existing flag""" + self._setup_mock_statsig(mock_statsig) + + existing_flag = { + 'id': 'existing-flag', + 'name': 'existing-flag', + 'rules': [ + { + 'name': 'test-rule', + 'conditions': [{'field': 'old_attr', 'targetValue': ['old_value']}], + 'environments': ['development'], + 'passPercentage': 100, + } + ], + } + + # Mock GET request (flag exists with test-rule) + mock_requests.get.return_value = self._create_mock_response(200, {'data': [existing_flag]}) + + mock_requests.patch.return_value = self._create_mock_response(200) + + client = StatSigFeatureFlagClient(environment='beta') + + result = client.upsert_flag('existing-flag', auto_enable=False) + + # Verify result + self.assertEqual(result['id'], 'existing-flag') + + # Verify API calls + mock_requests.get.assert_called_once() + mock_requests.patch.assert_called_once_with( + f'{STATSIG_API_BASE_URL}/gates/existing-flag', + headers={ + 'STATSIG-API-KEY': MOCK_CONSOLE_KEY, + 'STATSIG-API-VERSION': STATSIG_API_VERSION, + 'Content-Type': 'application/json', + }, + data=json.dumps( + { + 'id': 'existing-flag', + 'name': 'existing-flag', + 'rules': [ + { + 'name': 'test-rule', + 'conditions': [{'field': 'old_attr', 'targetValue': ['old_value']}], + 'environments': ['development'], + 'passPercentage': 100, + }, + { + 'name': 'beta-rule', + 'conditions': [], + 'environments': ['staging'], + 'passPercentage': 0, + } + ] + } + ), + timeout=30, + ) + @patch('feature_flag_client.Statsig') @patch('feature_flag_client.requests') def test_upsert_flag_prod_environment_existing_flag_auto_enable_true(self, mock_requests, mock_statsig): - """Test upsert in prod environment with existing flag and autoEnable=True""" + """Test upsert in prod environment with existing flag (no prod-rule yet) and autoEnable=True - adds prod-rule""" self._setup_mock_statsig(mock_statsig) existing_flag = { @@ -535,9 +596,10 @@ def test_upsert_flag_prod_environment_existing_flag_auto_enable_true(self, mock_ 'name': 'existing-prod-flag', 'rules': [ { - 'name': 'environment_toggle', + 'name': 'test-rule', 'conditions': [], - 'environments': ['development'], # Only has development + 'environments': ['development'], + 'passPercentage': 100, } ], } @@ -545,21 +607,20 @@ def test_upsert_flag_prod_environment_existing_flag_auto_enable_true(self, mock_ # Mock GET requests (flag exists, then return updated flag) mock_requests.get.side_effect = [ self._create_mock_response(200, {'data': [existing_flag]}), # First call to check existence - self._create_mock_response(200, {'data': [existing_flag]}), # Second call after update ] - # Mock PATCH request (update flag) + # Mock PATCH request (add prod-rule) mock_requests.patch.return_value = self._create_mock_response(200) client = StatSigFeatureFlagClient(environment='prod') - result = client.upsert_flag('existing-prod-flag', auto_enable=True, custom_attributes={'env': 'prod'}) + result = client.upsert_flag('existing-prod-flag', auto_enable=True, custom_attributes={'example': 'value'}) # Verify result self.assertEqual(result['id'], 'gate-existing-prod') - # Verify API calls - self.assertEqual(mock_requests.get.call_count, 1) # Once to check, once to return updated + # Verify API calls - adds prod-rule to existing flag + self.assertEqual(1, mock_requests.get.call_count) mock_requests.patch.assert_called_once_with( f'{STATSIG_API_BASE_URL}/gates/gate-existing-prod', headers={ @@ -573,12 +634,19 @@ def test_upsert_flag_prod_environment_existing_flag_auto_enable_true(self, mock_ 'name': 'existing-prod-flag', 'rules': [ { - 'name': 'environment_toggle', + 'name': 'test-rule', + 'conditions': [], + 'environments': ['development'], + 'passPercentage': 100, + }, + { + 'name': 'prod-rule', 'conditions': [ - {'type': 'custom_field', 'targetValue': ['prod'], 'field': 'env', 'operator': 'any'} + {'type': 'custom_field', 'targetValue': ['value'], 'field': 'example', 'operator': 'any'} ], - 'environments': ['development', 'production'], - } + 'environments': ['production'], + 'passPercentage': 100, + }, ], } ), @@ -590,7 +658,7 @@ def test_upsert_flag_prod_environment_existing_flag_auto_enable_true(self, mock_ def test_upsert_flag_prod_environment_existing_flag_auto_enable_false_should_not_update_flag( self, mock_requests, mock_statsig ): - """Test upsert in prod environment with existing flag and autoEnable=False""" + """Test upsert in prod environment with existing prod-rule and autoEnable=False - no modification""" self._setup_mock_statsig(mock_statsig) existing_flag = { @@ -598,27 +666,31 @@ def test_upsert_flag_prod_environment_existing_flag_auto_enable_false_should_not 'name': 'existing-prod-flag-2', 'rules': [ { - 'name': 'environment_toggle', + 'name': 'test-rule', + 'conditions': [], + 'environments': ['development'], + 'passPercentage': 100, + }, + { + 'name': 'prod-rule', 'conditions': [{'field': 'old', 'targetValue': ['value']}], - 'environments': ['development', 'production'], # Already has production - } + 'environments': ['production'], + 'passPercentage': 0, + }, ], } - # Mock GET requests (flag exists, then return updated flag) - mock_requests.get.side_effect = [ - self._create_mock_response(200, {'data': [existing_flag]}), # First call to check existence - self._create_mock_response(200, {'data': [existing_flag]}), # Second call after update - ] - - # Mock PATCH request (update flag) - mock_requests.patch.return_value = self._create_mock_response(200) + # Mock GET request (flag exists with prod-rule) + mock_requests.get.return_value = self._create_mock_response(200, {'data': [existing_flag]}) client = StatSigFeatureFlagClient(environment='prod') - client.upsert_flag('existing-prod-flag-2', auto_enable=False, custom_attributes={'new': 'attr'}) + result = client.upsert_flag('existing-prod-flag-2', auto_enable=False, custom_attributes={'new': 'attr'}) - # Verify API calls + # Verify result - no modification when auto_enable=False and rule exists + self.assertEqual(result['id'], 'gate-existing-prod-2') + + # Verify API calls - no PATCH since auto_enable=False self.assertEqual(mock_requests.get.call_count, 1) mock_requests.patch.assert_not_called() @@ -705,8 +777,8 @@ def test_delete_flag_not_found(self, mock_requests, mock_statsig): @patch('feature_flag_client.Statsig') @patch('feature_flag_client.requests') - def test_delete_flag_last_environment_deletes_entire_flag(self, mock_requests, mock_statsig): - """Test delete_flag when current environment is the only one - should delete entire flag""" + def test_delete_flag_last_rule_deletes_entire_flag(self, mock_requests, mock_statsig): + """Test delete_flag when test-rule is the only rule - should delete entire flag""" self._setup_mock_statsig(mock_statsig) existing_flag = { @@ -714,14 +786,15 @@ def test_delete_flag_last_environment_deletes_entire_flag(self, mock_requests, m 'name': 'delete-last-flag', 'rules': [ { - 'name': 'environment_toggle', + 'name': 'test-rule', 'conditions': [], - 'environments': ['development'], # Only current environment (test -> development) + 'environments': ['development'], + 'passPercentage': 100, } ], } - # Mock GET request (flag exists with only current environment) + # Mock GET request (flag exists with only test-rule) mock_requests.get.return_value = self._create_mock_response(200, {'data': [existing_flag]}) # Mock DELETE request (delete entire flag) @@ -749,8 +822,8 @@ def test_delete_flag_last_environment_deletes_entire_flag(self, mock_requests, m @patch('feature_flag_client.Statsig') @patch('feature_flag_client.requests') - def test_delete_flag_multiple_environments_removes_current_only(self, mock_requests, mock_statsig): - """Test delete_flag when flag has multiple environments - should only remove current""" + def test_delete_flag_multiple_rules_removes_current_rule_only(self, mock_requests, mock_statsig): + """Test delete_flag when flag has multiple rules - should only remove test-rule""" self._setup_mock_statsig(mock_statsig) existing_flag = { @@ -758,24 +831,37 @@ def test_delete_flag_multiple_environments_removes_current_only(self, mock_reque 'name': 'delete-multi-flag', 'rules': [ { - 'name': 'environment_toggle', + 'name': 'test-rule', 'conditions': [], - 'environments': ['development', 'staging', 'production'], # Multiple environments - } + 'environments': ['development'], + 'passPercentage': 100, + }, + { + 'name': 'beta-rule', + 'conditions': [], + 'environments': ['staging'], + 'passPercentage': 100, + }, + { + 'name': 'prod-rule', + 'conditions': [], + 'environments': ['production'], + 'passPercentage': 100, + }, ], } - # Mock GET request (flag exists with multiple environments) + # Mock GET request (flag exists with multiple rules) mock_requests.get.return_value = self._create_mock_response(200, {'data': [existing_flag]}) - # Mock PATCH request (remove environment) + # Mock PATCH request (remove test-rule) mock_requests.patch.return_value = self._create_mock_response(200) - client = StatSigFeatureFlagClient(environment='test') # test -> development + client = StatSigFeatureFlagClient(environment='test') result = client.delete_flag('delete-multi-flag') - # Should return False (environment removed, not full deletion) + # Should return False (rule removed, not full deletion) self.assertFalse(result) # Verify API calls @@ -793,10 +879,17 @@ def test_delete_flag_multiple_environments_removes_current_only(self, mock_reque 'name': 'delete-multi-flag', 'rules': [ { - 'name': 'environment_toggle', + 'name': 'beta-rule', 'conditions': [], - 'environments': ['staging', 'production'], # development removed - } + 'environments': ['staging'], + 'passPercentage': 100, + }, + { + 'name': 'prod-rule', + 'conditions': [], + 'environments': ['production'], + 'passPercentage': 100, + }, ], } ), @@ -806,8 +899,8 @@ def test_delete_flag_multiple_environments_removes_current_only(self, mock_reque @patch('feature_flag_client.Statsig') @patch('feature_flag_client.requests') - def test_delete_flag_prod_environment_last_environment(self, mock_requests, mock_statsig): - """Test delete_flag in prod environment when it's the last environment""" + def test_delete_flag_prod_environment_last_rule(self, mock_requests, mock_statsig): + """Test delete_flag in prod environment when prod-rule is the only rule""" self._setup_mock_statsig(mock_statsig) existing_flag = { @@ -815,14 +908,15 @@ def test_delete_flag_prod_environment_last_environment(self, mock_requests, mock 'name': 'delete-prod-flag', 'rules': [ { - 'name': 'environment_toggle', + 'name': 'prod-rule', 'conditions': [], - 'environments': ['production'], # Only production environment + 'environments': ['production'], + 'passPercentage': 100, } ], } - # Mock GET request (flag exists with only production environment) + # Mock GET request (flag exists with only prod-rule) mock_requests.get.return_value = self._create_mock_response(200, {'data': [existing_flag]}) # Mock DELETE request (delete entire flag) @@ -847,70 +941,11 @@ def test_delete_flag_prod_environment_last_environment(self, mock_requests, mock timeout=30, ) - @patch('feature_flag_client.Statsig') - @patch('feature_flag_client.requests') - def test_delete_flag_prod_environment_multiple_environments(self, mock_requests, mock_statsig): - """Test delete_flag in prod environment when multiple environments exist""" - self._setup_mock_statsig(mock_statsig) - - existing_flag = { - 'id': 'gate-delete-prod-multi', - 'name': 'delete-prod-multi-flag', - 'rules': [ - { - 'name': 'environment_toggle', - 'conditions': [ - {'type': 'custom_field', 'targetValue': ['value'], 'field': 'attr', 'operator': 'any'} - ], - 'environments': ['development', 'production'], # Multiple environments - } - ], - } - - # Mock GET request (flag exists with multiple environments) - mock_requests.get.return_value = self._create_mock_response(200, {'data': [existing_flag]}) - - # Mock PATCH request (remove production environment) - mock_requests.patch.return_value = self._create_mock_response(200) - - client = StatSigFeatureFlagClient(environment='prod') - - result = client.delete_flag('delete-prod-multi-flag') - - # Should return False (environment removed, not full deletion) - self.assertFalse(result) - - # Verify API calls - mock_requests.get.assert_called_once() - mock_requests.patch.assert_called_once_with( - f'{STATSIG_API_BASE_URL}/gates/gate-delete-prod-multi', - headers={ - 'STATSIG-API-KEY': MOCK_CONSOLE_KEY, - 'STATSIG-API-VERSION': STATSIG_API_VERSION, - 'Content-Type': 'application/json', - }, - data=json.dumps( - { - 'id': 'gate-delete-prod-multi', - 'name': 'delete-prod-multi-flag', - 'rules': [ - { - 'name': 'environment_toggle', - 'conditions': [ - {'type': 'custom_field', 'targetValue': ['value'], 'field': 'attr', 'operator': 'any'} - ], - 'environments': ['development'], # production removed - } - ], - } - ), - timeout=30, - ) @patch('feature_flag_client.Statsig') @patch('feature_flag_client.requests') - def test_delete_flag_current_environment_not_present(self, mock_requests, mock_statsig): - """Test delete_flag when current environment is not in the flag's environments""" + def test_delete_flag_current_rule_not_present(self, mock_requests, mock_statsig): + """Test delete_flag when current environment rule is not in the flag""" self._setup_mock_statsig(mock_statsig) existing_flag = { @@ -918,27 +953,31 @@ def test_delete_flag_current_environment_not_present(self, mock_requests, mock_s 'name': 'delete-not-present-flag', 'rules': [ { - 'name': 'environment_toggle', + 'name': 'beta-rule', 'conditions': [], - 'environments': ['staging', 'production'], # Current environment (development) not present - } + 'environments': ['staging'], + 'passPercentage': 100, + }, + { + 'name': 'prod-rule', + 'conditions': [], + 'environments': ['production'], + 'passPercentage': 100, + }, ], } - # Mock GET request (flag exists but current environment not in it) + # Mock GET request (flag exists but test-rule not present) mock_requests.get.return_value = self._create_mock_response(200, {'data': [existing_flag]}) - # Mock PATCH request (no change needed, but method still called) - mock_requests.patch.return_value = self._create_mock_response(200) - - client = StatSigFeatureFlagClient(environment='test') # test -> development + client = StatSigFeatureFlagClient(environment='test') result = client.delete_flag('delete-not-present-flag') - # Should return False (no environment removed since it wasn't there) + # Should return False (no rule removed since it wasn't there) self.assertFalse(result) - # Should not call PATCH since environment wasn't present + # Should not call PATCH or DELETE since rule wasn't present mock_requests.get.assert_called_once() mock_requests.patch.assert_not_called() mock_requests.delete.assert_not_called() @@ -954,9 +993,10 @@ def test_delete_flag_api_error_on_delete_raises_exception(self, mock_requests, m 'name': 'delete-error-flag', 'rules': [ { - 'name': 'environment_toggle', + 'name': 'test-rule', 'conditions': [], - 'environments': ['development'], # Only current environment + 'environments': ['development'], + 'passPercentage': 100, } ], } @@ -985,10 +1025,17 @@ def test_delete_flag_api_error_on_patch_raises_exception(self, mock_requests, mo 'name': 'patch-error-flag', 'rules': [ { - 'name': 'environment_toggle', + 'name': 'test-rule', 'conditions': [], - 'environments': ['development', 'staging'], # Multiple environments - } + 'environments': ['development'], + 'passPercentage': 100, + }, + { + 'name': 'beta-rule', + 'conditions': [], + 'environments': ['staging'], + 'passPercentage': 100, + }, ], } @@ -1008,7 +1055,7 @@ def test_delete_flag_api_error_on_patch_raises_exception(self, mock_requests, mo @patch('feature_flag_client.Statsig') @patch('feature_flag_client.requests') def test_upsert_flag_custom_attributes_as_string(self, mock_requests, mock_statsig): - """Test upsert_flag with custom attributes as string values""" + """Test upsert_flag with custom attributes as string values - development environment always enabled""" self._setup_mock_statsig(mock_statsig) # Mock GET request (flag doesn't exist) @@ -1020,12 +1067,12 @@ def test_upsert_flag_custom_attributes_as_string(self, mock_requests, mock_stats client = StatSigFeatureFlagClient(environment='test') - result = client.upsert_flag('string-attrs-flag', custom_attributes={'region': 'us-east-1', 'feature': 'new'}) + result = client.upsert_flag('string-attrs-flag', auto_enable=False, custom_attributes={'region': 'us-east-1', 'feature': 'new'}) # Verify result self.assertEqual(result['id'], 'gate-string-attrs') - # Verify API calls - string values should be converted to lists + # Verify API calls - string values should be converted to lists, no conditions when auto_enable=False in test mock_requests.post.assert_called_once_with( f'{STATSIG_API_BASE_URL}/gates', headers={ @@ -1040,7 +1087,7 @@ def test_upsert_flag_custom_attributes_as_string(self, mock_requests, mock_stats 'isEnabled': True, 'rules': [ { - 'name': 'environment_toggle', + 'name': 'test-rule', 'conditions': [ { 'type': 'custom_field', @@ -1051,7 +1098,7 @@ def test_upsert_flag_custom_attributes_as_string(self, mock_requests, mock_stats {'type': 'custom_field', 'targetValue': ['new'], 'field': 'feature', 'operator': 'any'}, ], 'environments': ['development'], - 'passPercentage': 100, + 'passPercentage': 100, # Always 100 for test environment } ], } @@ -1062,7 +1109,7 @@ def test_upsert_flag_custom_attributes_as_string(self, mock_requests, mock_stats @patch('feature_flag_client.Statsig') @patch('feature_flag_client.requests') def test_upsert_flag_custom_attributes_as_list(self, mock_requests, mock_statsig): - """Test upsert_flag with custom attributes as list values""" + """Test upsert_flag with custom attributes as list values - no conditions for test when auto_enable=False""" self._setup_mock_statsig(mock_statsig) # Mock GET request (flag doesn't exist) @@ -1074,12 +1121,12 @@ def test_upsert_flag_custom_attributes_as_list(self, mock_requests, mock_statsig client = StatSigFeatureFlagClient(environment='test') - result = client.upsert_flag('list-attrs-flag', custom_attributes={'licenseType': ['slp', 'audiologist']}) + result = client.upsert_flag('list-attrs-flag', auto_enable=False, custom_attributes={'licenseType': ['slp', 'audiologist']}) # Verify result self.assertEqual(result['id'], 'gate-list-attrs') - # Verify API calls - list values should be kept as lists + # Verify API calls - list values preserved but no conditions when auto_enable=False mock_requests.post.assert_called_once_with( f'{STATSIG_API_BASE_URL}/gates', headers={ @@ -1094,11 +1141,11 @@ def test_upsert_flag_custom_attributes_as_list(self, mock_requests, mock_statsig 'isEnabled': True, 'rules': [ { - 'name': 'environment_toggle', + 'name': 'test-rule', 'conditions': [ { 'type': 'custom_field', - 'targetValue': [['slp', 'audiologist']], + 'targetValue': ['slp', 'audiologist'], 'field': 'licenseType', 'operator': 'any', } @@ -1118,21 +1165,16 @@ def test_upsert_flag_custom_attributes_invalid_type_raises_exception(self, mock_ """Test upsert_flag with custom attributes as invalid type (dict) raises exception""" self._setup_mock_statsig(mock_statsig) - existing_flag = { - 'id': 'gate-invalid-attrs', - 'name': 'invalid-attrs-flag', - 'rules': [{'name': 'environment_toggle', 'conditions': [], 'environments': ['development']}], - } - - # Mock GET request (flag exists) - mock_requests.get.return_value = self._create_mock_response(200, {'data': [existing_flag]}) + # Mock GET request (flag doesn't exist) + mock_requests.get.return_value = self._create_mock_response(200, {'data': []}) - client = StatSigFeatureFlagClient(environment='test') + client = StatSigFeatureFlagClient(environment='prod') - # Try to update with invalid custom attribute type (dict) + # Try to create flag with invalid custom attribute type (dict) when auto_enable=True with self.assertRaises(FeatureFlagException) as context: client.upsert_flag( 'invalid-attrs-flag', + auto_enable=True, custom_attributes={ 'invalid_attr': {'nested': 'dict'} # This should raise an exception }, From dac99f510d71ee7c4f515a5bdad06d4dd0304767 Mon Sep 17 00:00:00 2001 From: Landon Shumway Date: Thu, 2 Oct 2025 14:24:29 -0500 Subject: [PATCH 20/55] rename fail parameter for clarity --- .../common/cc_common/feature_flag_client.py | 8 ++++---- .../tests/unit/test_feature_flag_client.py | 16 ++++++++-------- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/backend/compact-connect/lambdas/python/common/cc_common/feature_flag_client.py b/backend/compact-connect/lambdas/python/common/cc_common/feature_flag_client.py index 0dce747c6..3fe980c41 100644 --- a/backend/compact-connect/lambdas/python/common/cc_common/feature_flag_client.py +++ b/backend/compact-connect/lambdas/python/common/cc_common/feature_flag_client.py @@ -43,7 +43,7 @@ def to_dict(self) -> dict[str, Any]: return result -def is_feature_enabled(flag_name: str, context: FeatureFlagContext | None = None, fail_open: bool = False) -> bool: +def is_feature_enabled(flag_name: str, context: FeatureFlagContext | None = None, fail_default: bool = False) -> bool: """ Check if a feature flag is enabled. @@ -52,7 +52,7 @@ def is_feature_enabled(flag_name: str, context: FeatureFlagContext | None = None :param flag_name: The name of the feature flag to check :param context: Optional FeatureFlagContext for feature flag evaluation - :param fail_open: If True, return True on errors; if False, return False on errors (default: False) + :param fail_default: If True, return True on errors; if False, return False on errors (default: False) :return: True if the feature flag is enabled, False otherwise (or fail_open value on error) Example: @@ -109,14 +109,14 @@ def is_feature_enabled(flag_name: str, context: FeatureFlagContext | None = None if 'enabled' not in response_data: logger.info('Invalid response format - return fail_open value', response_data=response_data) # Invalid response format - return fail_open value - return fail_open + return fail_default return bool(response_data['enabled']) except Exception as e: # Any error (timeout, network, parsing, etc.) - return fail_open value logger.info('Error checking feature flag - return fail_open value', exc_info=e) - return fail_open + return fail_default def _get_api_base_url() -> str: diff --git a/backend/compact-connect/lambdas/python/common/tests/unit/test_feature_flag_client.py b/backend/compact-connect/lambdas/python/common/tests/unit/test_feature_flag_client.py index d6a9b3779..4089f64e0 100644 --- a/backend/compact-connect/lambdas/python/common/tests/unit/test_feature_flag_client.py +++ b/backend/compact-connect/lambdas/python/common/tests/unit/test_feature_flag_client.py @@ -72,7 +72,7 @@ def test_is_feature_enabled_fail_closed_on_timeout(self): from cc_common.feature_flag_client import is_feature_enabled with patch('cc_common.feature_flag_client.requests.post', side_effect=Exception('Timeout')): - result = is_feature_enabled('test-flag', fail_open=False) + result = is_feature_enabled('test-flag', fail_default=False) # Verify it fails closed (returns False) self.assertFalse(result) @@ -82,7 +82,7 @@ def test_is_feature_enabled_fail_open_on_timeout(self): from cc_common.feature_flag_client import is_feature_enabled with patch('cc_common.feature_flag_client.requests.post', side_effect=Exception('Timeout')): - result = is_feature_enabled('test-flag', fail_open=True) + result = is_feature_enabled('test-flag', fail_default=True) # Verify it fails open (returns True) self.assertTrue(result) @@ -96,7 +96,7 @@ def test_is_feature_enabled_fail_closed_on_http_error(self): mock_response.raise_for_status.side_effect = Exception('500 Server Error') with patch('cc_common.feature_flag_client.requests.post', return_value=mock_response): - result = is_feature_enabled('test-flag', fail_open=False) + result = is_feature_enabled('test-flag', fail_default=False) # Verify it fails closed (returns False) self.assertFalse(result) @@ -110,7 +110,7 @@ def test_is_feature_enabled_fail_open_on_http_error(self): mock_response.raise_for_status.side_effect = Exception('500 Server Error') with patch('cc_common.feature_flag_client.requests.post', return_value=mock_response): - result = is_feature_enabled('test-flag', fail_open=True) + result = is_feature_enabled('test-flag', fail_default=True) # Verify it fails open (returns True) self.assertTrue(result) @@ -125,7 +125,7 @@ def test_is_feature_enabled_fail_closed_on_invalid_response(self): mock_response.raise_for_status = MagicMock() with patch('cc_common.feature_flag_client.requests.post', return_value=mock_response): - result = is_feature_enabled('test-flag', fail_open=False) + result = is_feature_enabled('test-flag', fail_default=False) # Verify it fails closed (returns False) self.assertFalse(result) @@ -140,7 +140,7 @@ def test_is_feature_enabled_fail_open_on_invalid_response(self): mock_response.raise_for_status = MagicMock() with patch('cc_common.feature_flag_client.requests.post', return_value=mock_response): - result = is_feature_enabled('test-flag', fail_open=True) + result = is_feature_enabled('test-flag', fail_default=True) # Verify it fails open (returns True) self.assertTrue(result) @@ -155,7 +155,7 @@ def test_is_feature_enabled_fail_closed_on_json_parse_error(self): mock_response.raise_for_status = MagicMock() with patch('cc_common.feature_flag_client.requests.post', return_value=mock_response): - result = is_feature_enabled('test-flag', fail_open=False) + result = is_feature_enabled('test-flag', fail_default=False) # Verify it fails closed (returns False) self.assertFalse(result) @@ -169,7 +169,7 @@ def test_is_feature_enabled_fail_open_on_json_parse_error(self): mock_response.json.side_effect = ValueError('Invalid JSON') with patch('cc_common.feature_flag_client.requests.post', return_value=mock_response): - result = is_feature_enabled('test-flag', fail_open=True) + result = is_feature_enabled('test-flag', fail_default=True) # Verify it fails open (returns True) self.assertTrue(result) From 54b4a076e03e4e08fce2fb18f3ef6a1edb0f5262 Mon Sep 17 00:00:00 2001 From: Landon Shumway Date: Thu, 2 Oct 2025 14:25:23 -0500 Subject: [PATCH 21/55] clean up custom resource --- .../custom_resource_handler.py | 1 - .../stacks/feature_flag_stack/__init__.py | 6 ++++-- .../feature_flag_resource.py | 21 +++++++++++++------ 3 files changed, 19 insertions(+), 9 deletions(-) rename backend/compact-connect/lambdas/python/{common/cc_common => migration}/custom_resource_handler.py (99%) diff --git a/backend/compact-connect/lambdas/python/common/cc_common/custom_resource_handler.py b/backend/compact-connect/lambdas/python/migration/custom_resource_handler.py similarity index 99% rename from backend/compact-connect/lambdas/python/common/cc_common/custom_resource_handler.py rename to backend/compact-connect/lambdas/python/migration/custom_resource_handler.py index ff6b55eda..29ee5a56d 100644 --- a/backend/compact-connect/lambdas/python/common/cc_common/custom_resource_handler.py +++ b/backend/compact-connect/lambdas/python/migration/custom_resource_handler.py @@ -4,7 +4,6 @@ from aws_lambda_powertools.logging.lambda_context import build_lambda_context_model from aws_lambda_powertools.utilities.typing import LambdaContext - from cc_common.config import logger diff --git a/backend/compact-connect/stacks/feature_flag_stack/__init__.py b/backend/compact-connect/stacks/feature_flag_stack/__init__.py index 0b29ede89..65bb41a9c 100644 --- a/backend/compact-connect/stacks/feature_flag_stack/__init__.py +++ b/backend/compact-connect/stacks/feature_flag_stack/__init__.py @@ -2,7 +2,8 @@ from common_constructs.stack import AppStack from constructs import Construct -from feature_flag_stack.feature_flag_resource import FeatureFlagResource + +from stacks.feature_flag_stack.feature_flag_resource import FeatureFlagResource class FeatureFlagStack(AppStack): @@ -22,6 +23,7 @@ def __init__( self, 'ExampleFlag', flag_name='example-flag', - custom_attributes={'hello': 'world'}, + auto_enable=False, + custom_attributes={'compact': ['coun', 'aslp']}, environment_name=environment_name, ) diff --git a/backend/compact-connect/stacks/feature_flag_stack/feature_flag_resource.py b/backend/compact-connect/stacks/feature_flag_stack/feature_flag_resource.py index 4051e712b..fefd33a81 100644 --- a/backend/compact-connect/stacks/feature_flag_stack/feature_flag_resource.py +++ b/backend/compact-connect/stacks/feature_flag_stack/feature_flag_resource.py @@ -33,7 +33,7 @@ def __init__( *, flag_name: str, auto_enable: bool = False, - custom_attributes: dict[str, str] | None = None, + custom_attributes: dict[str, str] | dict[str, list] | None = None, environment_name: str, ): """ @@ -100,17 +100,26 @@ def __init__( ], ) + NagSuppressions.add_resource_suppressions_by_path( + Stack.of(self), + path=f'{self.manage_function.node.path}/ServiceRole/DefaultPolicy/Resource', + suppressions=[ + { + 'id': 'AwsSolutions-IAM5', + 'reason': 'The actions in this policy contain a wildcard specifically to access the feature flag ' + 'client credentials secret and all of its versions.', + }, + ], + ) + NagSuppressions.add_resource_suppressions_by_path( Stack.of(self), path=f'{self.provider.node.path}/framework-onEvent/ServiceRole/DefaultPolicy/Resource', suppressions=[ { 'id': 'AwsSolutions-IAM5', - 'reason': 'The actions in this policy are specifically what this lambda needs ' - 'and is scoped appropriately.', - 'appliesTo': [ - f'Resource::arn:aws:secretsmanager:{Stack.of(self).region}:{Stack.of(self).account}:secret:{secret_name}-*', - ], + 'reason': 'The actions in this policy contain a wildcard specifically to access the feature flag ' + 'client credentials secret and all of its versions.', }, ], ) From d21b8a4e00d77c589454a0a16c8a7265dbb44abf Mon Sep 17 00:00:00 2001 From: Landon Shumway Date: Thu, 2 Oct 2025 16:18:45 -0500 Subject: [PATCH 22/55] set auto enabled by env name --- .../compact-connect/stacks/feature_flag_stack/__init__.py | 6 ++++-- .../stacks/feature_flag_stack/feature_flag_resource.py | 6 +++--- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/backend/compact-connect/stacks/feature_flag_stack/__init__.py b/backend/compact-connect/stacks/feature_flag_stack/__init__.py index 65bb41a9c..0f7f10f38 100644 --- a/backend/compact-connect/stacks/feature_flag_stack/__init__.py +++ b/backend/compact-connect/stacks/feature_flag_stack/__init__.py @@ -23,7 +23,9 @@ def __init__( self, 'ExampleFlag', flag_name='example-flag', - auto_enable=False, - custom_attributes={'compact': ['coun', 'aslp']}, + # This causes the flag to automatically be set to enabled for every environment in the list + auto_enable_envs=['test', 'beta', 'prod'], + # Note that flags are not updated once set and must be manually updated through the console + custom_attributes={'compact': ['coun']}, environment_name=environment_name, ) diff --git a/backend/compact-connect/stacks/feature_flag_stack/feature_flag_resource.py b/backend/compact-connect/stacks/feature_flag_stack/feature_flag_resource.py index fefd33a81..47078ce55 100644 --- a/backend/compact-connect/stacks/feature_flag_stack/feature_flag_resource.py +++ b/backend/compact-connect/stacks/feature_flag_stack/feature_flag_resource.py @@ -32,7 +32,7 @@ def __init__( construct_id: str, *, flag_name: str, - auto_enable: bool = False, + auto_enable_envs: list[str], custom_attributes: dict[str, str] | dict[str, list] | None = None, environment_name: str, ): @@ -40,7 +40,7 @@ def __init__( Initialize the FeatureFlagResource construct. :param flag_name: Name of the feature flag to manage - :param auto_enable: If True, automatically enable the flag in this environment (beta/prod only) + :param auto_enable_envs: List of environments to automatically enable the flag for :param custom_attributes: Optional custom attributes for feature flag targeting :param environment_name: The environment name (test, beta, prod) """ @@ -139,7 +139,7 @@ def __init__( ) # Build custom resource properties - properties = {'flagName': flag_name, 'autoEnable': auto_enable} + properties = {'flagName': flag_name, 'autoEnable': environment_name in auto_enable_envs} if custom_attributes: properties['customAttributes'] = custom_attributes From 4de1cd66f88816d1c5c4b1e5940d00c38e56cd88 Mon Sep 17 00:00:00 2001 From: Landon Shumway Date: Thu, 2 Oct 2025 16:19:07 -0500 Subject: [PATCH 23/55] do not update flags after creation --- .../handlers/manage_feature_flag.py | 30 ++--------- .../function/test_manage_feature_flag.py | 51 ++----------------- 2 files changed, 8 insertions(+), 73 deletions(-) diff --git a/backend/compact-connect/lambdas/python/feature-flag/handlers/manage_feature_flag.py b/backend/compact-connect/lambdas/python/feature-flag/handlers/manage_feature_flag.py index a96557fa9..2f0114d1a 100644 --- a/backend/compact-connect/lambdas/python/feature-flag/handlers/manage_feature_flag.py +++ b/backend/compact-connect/lambdas/python/feature-flag/handlers/manage_feature_flag.py @@ -62,40 +62,18 @@ def on_create(self, properties: dict) -> CustomResourceResponse | None: def on_update(self, properties: dict) -> CustomResourceResponse | None: """ - Handle Update events for feature flags. - - Updates the feature gate configuration based on changed properties. + Flags are not updated once created in an environment. :param properties: ResourceProperties containing updated values - :return: Optional response data + :return: None (no-op) """ - flag_name = properties.get('flagName') - auto_enable = properties.get('autoEnable', False) - custom_attributes = properties.get('customAttributes') - - if not flag_name: - raise ValueError('flagName is required in ResourceProperties') - - logger.info('Updating feature flag resource', flag_name=flag_name, environment=self.environment) - - # Update the flag - client handles all environment-specific logic - flag_data = self.client.upsert_flag(flag_name, auto_enable, custom_attributes) - - if not flag_data: - raise RuntimeError(f"Feature gate '{flag_name}' could not be updated") - - # Extract gate ID from response - gate_id = flag_data.get('data', {}).get('id') or flag_data.get('id') - - logger.info('Feature flag resource updated successfully', flag_name=flag_name, gate_id=gate_id) - - return {'PhysicalResourceId': f'feature-flag-{flag_name}-{self.environment}', 'Data': {'gateId': gate_id}} + return None def on_delete(self, properties: dict) -> CustomResourceResponse | None: """ Handle Delete events for feature flags. - Removes the environment from the feature gate. If it's the last environment, + Removes the environment rule from the feature gate. If it's the last environment, deletes the gate entirely. :param properties: ResourceProperties containing flagName diff --git a/backend/compact-connect/lambdas/python/feature-flag/tests/function/test_manage_feature_flag.py b/backend/compact-connect/lambdas/python/feature-flag/tests/function/test_manage_feature_flag.py index b4b736dcd..a6bef554f 100644 --- a/backend/compact-connect/lambdas/python/feature-flag/tests/function/test_manage_feature_flag.py +++ b/backend/compact-connect/lambdas/python/feature-flag/tests/function/test_manage_feature_flag.py @@ -36,7 +36,7 @@ def test_on_create_calls_upsert_flag_with_correct_params(self, mock_client_class # Set up mock client instance mock_client = MagicMock() - mock_client.upsert_flag.return_value = {'id': 'gate-123', 'name': 'test-flag'} + mock_client.upsert_flag.return_value = {'id': 'test-flag', 'name': 'test-flag'} mock_client_class.return_value = mock_client handler = ManageFeatureFlagHandler() @@ -53,7 +53,7 @@ def test_on_create_calls_upsert_flag_with_correct_params(self, mock_client_class # Verify response self.assertEqual(result['PhysicalResourceId'], 'feature-flag-test-flag-test') - self.assertEqual(result['Data']['gateId'], 'gate-123') + self.assertEqual(result['Data']['gateId'], 'test-flag') @patch('handlers.manage_feature_flag.StatSigFeatureFlagClient') def test_on_create_with_minimal_properties(self, mock_client_class): @@ -62,7 +62,7 @@ def test_on_create_with_minimal_properties(self, mock_client_class): # Set up mock client instance mock_client = MagicMock() - mock_client.upsert_flag.return_value = {'id': 'gate-456', 'name': 'minimal-flag'} + mock_client.upsert_flag.return_value = {'id': 'minimal-flag', 'name': 'minimal-flag'} mock_client_class.return_value = mock_client handler = ManageFeatureFlagHandler() @@ -75,50 +75,7 @@ def test_on_create_with_minimal_properties(self, mock_client_class): # Verify response self.assertEqual(result['PhysicalResourceId'], 'feature-flag-minimal-flag-test') - self.assertEqual(result['Data']['gateId'], 'gate-456') - - @patch('handlers.manage_feature_flag.StatSigFeatureFlagClient') - def test_on_update_calls_upsert_flag_with_correct_params(self, mock_client_class): - from handlers.manage_feature_flag import ManageFeatureFlagHandler - - """Test that on_update calls upsert_flag with the correct parameters""" - # Set up mock client instance - mock_client = MagicMock() - mock_client.upsert_flag.return_value = {'id': 'gate-789', 'name': 'update-flag'} - mock_client_class.return_value = mock_client - - handler = ManageFeatureFlagHandler() - properties = {'flagName': 'update-flag', 'autoEnable': False, 'customAttributes': {'version': '2.0'}} - - result = handler.on_update(properties) - - # Verify client was initialized with correct environment - mock_client_class.assert_called_once_with(environment='test') - - # Verify upsert_flag was called with correct parameters - mock_client.upsert_flag.assert_called_once_with('update-flag', False, {'version': '2.0'}) - - # Verify response - self.assertEqual(result['PhysicalResourceId'], 'feature-flag-update-flag-test') - self.assertEqual(result['Data']['gateId'], 'gate-789') - - @patch('handlers.manage_feature_flag.StatSigFeatureFlagClient') - def test_on_update_raises_error_when_no_flag_returned(self, mock_client_class): - """Test on_update raises RuntimeError when upsert_flag returns empty dict""" - from handlers.manage_feature_flag import ManageFeatureFlagHandler - - # Set up mock client instance - mock_client = MagicMock() - mock_client.upsert_flag.return_value = {} # Empty dict means no action taken - mock_client_class.return_value = mock_client - - handler = ManageFeatureFlagHandler() - properties = {'flagName': 'failed-update-flag'} - - with self.assertRaises(RuntimeError) as context: - handler.on_update(properties) - - self.assertIn("Feature gate 'failed-update-flag' could not be updated", str(context.exception)) + self.assertEqual(result['Data']['gateId'], 'minimal-flag') @patch('handlers.manage_feature_flag.StatSigFeatureFlagClient') def test_on_delete_calls_delete_flag_with_correct_params(self, mock_client_class): From 198e49f2236657a90d89c0cb399c1575cc74e94f Mon Sep 17 00:00:00 2001 From: Landon Shumway Date: Thu, 2 Oct 2025 17:22:35 -0500 Subject: [PATCH 24/55] Add documentation for feature flag usage --- .../stacks/feature_flag_stack/__init__.py | 75 ++++++++++++++++++- .../feature_flag_resource.py | 11 ++- 2 files changed, 82 insertions(+), 4 deletions(-) diff --git a/backend/compact-connect/stacks/feature_flag_stack/__init__.py b/backend/compact-connect/stacks/feature_flag_stack/__init__.py index 0f7f10f38..385a7d0cd 100644 --- a/backend/compact-connect/stacks/feature_flag_stack/__init__.py +++ b/backend/compact-connect/stacks/feature_flag_stack/__init__.py @@ -1,9 +1,74 @@ +""" +Feature Flag Management Stack + +This stack manages feature flags through CloudFormation custom resources. +Feature flags enable/disable functionality dynamically across environments without code deployments. + +While we initially create the flag through CDK deployments, updates to the flag configuration are managed through +the respective StatSig account console. + +When a flag is no longer used, removing it from this stack should result in cleaning up all the environment based rules +for the flag and deleting it from StatSig once it has been removed from all environments. + +Feature Flag Lifecycle: +----------------------- +1. **Creation** (on_create): + - Creates a new StatSig feature gate if it doesn't exist + - Adds an environment-specific rule (e.g., '-rule') to the gate + - If auto_enable=True: passPercentage=100 (enabled) + - If auto_enable=False: passPercentage=0 (disabled) + +2. **Updates** (on_update): + - Feature flags are IMMUTABLE once created in an environment + - Updates are no-ops to prevent overwriting manual console changes + +3. **Deletion** (on_delete): + - Removes the environment-specific rule from the gate + - If it's the last rule, deletes the entire gate + - Other environments' rules remain untouched + +StatSig Environment Mapping: +------------------- +StatSig has three fixed environment names, so we must map our environments to one of the three environments +- test → development (StatSig tier) +- beta → staging (StatSig tier) +- prod → production (StatSig tier) +- sandbox/other → development (StatSig tier, default) + + +Checking Flags in Lambda: +------------------------- +There is a common feature flag client python module that can be used to check if a flag is enabled in StatSig + +```python +from cc_common.feature_flag_client import is_feature_enabled, FeatureFlagContext + +# Simple check +if is_feature_enabled('my-feature-flag-name'): + # run feature gated code if enabled + +# With targeting context +context = FeatureFlagContext( + user_id='user-123', + custom_attributes={'compact': 'aslp', 'licenseType': 'slp'} +) +if is_feature_enabled('my-feature-flag-name', context=context): + # run feature gated code if enabled +``` + +Custom Attributes: +----------------- +- Values can be strings (converted to lists) or lists +- Used for targeting specific subsets of users/requests +- Examples: compact, jurisdiction, licenseType, etc. +""" + from __future__ import annotations from common_constructs.stack import AppStack from constructs import Construct -from stacks.feature_flag_stack.feature_flag_resource import FeatureFlagResource +from stacks.feature_flag_stack.feature_flag_resource import FeatureFlagEnvironmentName, FeatureFlagResource class FeatureFlagStack(AppStack): @@ -24,8 +89,12 @@ def __init__( 'ExampleFlag', flag_name='example-flag', # This causes the flag to automatically be set to enabled for every environment in the list - auto_enable_envs=['test', 'beta', 'prod'], + auto_enable_envs=[ + FeatureFlagEnvironmentName.TEST, + FeatureFlagEnvironmentName.BETA, + FeatureFlagEnvironmentName.PROD, + ], # Note that flags are not updated once set and must be manually updated through the console - custom_attributes={'compact': ['coun']}, + custom_attributes={'compact': ['aslp']}, environment_name=environment_name, ) diff --git a/backend/compact-connect/stacks/feature_flag_stack/feature_flag_resource.py b/backend/compact-connect/stacks/feature_flag_stack/feature_flag_resource.py index 47078ce55..9e5b402ba 100644 --- a/backend/compact-connect/stacks/feature_flag_stack/feature_flag_resource.py +++ b/backend/compact-connect/stacks/feature_flag_stack/feature_flag_resource.py @@ -6,6 +6,7 @@ """ import os +from enum import Enum import jsii from aws_cdk import CustomResource, Duration, Stack @@ -17,6 +18,14 @@ from constructs import Construct +class FeatureFlagEnvironmentName(Enum): + TEST = 'test' + BETA = 'beta' + PROD = 'prod' + # add sandbox environment names here if needed + # SANDBOX = 'sandbox' + + @jsii.implements(IGrantable) class FeatureFlagResource(Construct): """ @@ -32,7 +41,7 @@ def __init__( construct_id: str, *, flag_name: str, - auto_enable_envs: list[str], + auto_enable_envs: list[FeatureFlagEnvironmentName], custom_attributes: dict[str, str] | dict[str, list] | None = None, environment_name: str, ): From ebf1ad8f4ff58570a8d22e79e533ab03a2f35c71 Mon Sep 17 00:00:00 2001 From: Landon Shumway Date: Thu, 2 Oct 2025 17:25:10 -0500 Subject: [PATCH 25/55] Never update existing feature gate rule --- .../feature-flag/feature_flag_client.py | 63 ++++------------- .../tests/function/test_statsig_client.py | 67 +++++++------------ 2 files changed, 36 insertions(+), 94 deletions(-) diff --git a/backend/compact-connect/lambdas/python/feature-flag/feature_flag_client.py b/backend/compact-connect/lambdas/python/feature-flag/feature_flag_client.py index aebee0560..8bb008b01 100644 --- a/backend/compact-connect/lambdas/python/feature-flag/feature_flag_client.py +++ b/backend/compact-connect/lambdas/python/feature-flag/feature_flag_client.py @@ -376,26 +376,17 @@ def upsert_flag( rule_name = f'{self.environment.lower()}-rule' environment_rule = self._find_environment_rule(existing_gate, rule_name) + # we only set the environment rule if it doesn't already exist + # else we leave it alone to avoid overwriting manual changes if not environment_rule: - # Environment rule doesn't exist - add it updated_gate = self._add_environment_rule(existing_gate, auto_enable, custom_attributes) self._update_gate(gate_id, updated_gate) - else: - # Environment rule exists, only update the rule if auto enabled or development environment - if auto_enable or self._is_development_environment(): - # Update the rule with new settings - updated_gate = self._update_environment_rule(existing_gate, auto_enable, custom_attributes) - self._update_gate(gate_id, updated_gate) return existing_gate - def _is_development_environment(self) -> bool: - """ - Check if the current environment is a development environment. - """ - return self.environment.lower() != 'prod' and self.environment.lower() != 'beta' - - def _create_new_gate(self, flag_name: str, auto_enable: bool, custom_attributes: dict[str, Any] | None = None) -> dict[str, Any]: + def _create_new_gate( + self, flag_name: str, auto_enable: bool, custom_attributes: dict[str, Any] | None = None + ) -> dict[str, Any]: """ Create a new feature gate in StatSig with an environment-specific rule. @@ -424,7 +415,7 @@ def _create_new_gate(self, flag_name: str, auto_enable: bool, custom_attributes: 'name': rule_name, 'conditions': conditions, 'environments': [statsig_tier], - 'passPercentage': 100 if auto_enable or self._is_development_environment() else 0, + 'passPercentage': 100 if auto_enable else 0, } ], } @@ -465,12 +456,12 @@ def _build_conditions_from_attributes(self, custom_attributes: dict[str, Any]) - elif not isinstance(value, list): raise FeatureFlagException(f'Custom attribute value must be a string or list: {value}') - conditions.append( - {'type': 'custom_field', 'targetValue': value, 'field': key, 'operator': 'any'} - ) + conditions.append({'type': 'custom_field', 'targetValue': value, 'field': key, 'operator': 'any'}) return conditions - def _add_environment_rule(self, gate_data: dict[str, Any], auto_enable: bool, custom_attributes: dict[str, Any] | None = None) -> dict[str, Any]: + def _add_environment_rule( + self, gate_data: dict[str, Any], auto_enable: bool, custom_attributes: dict[str, Any] | None = None + ) -> dict[str, Any]: """ Add an environment-specific rule to an existing gate. @@ -493,7 +484,7 @@ def _add_environment_rule(self, gate_data: dict[str, Any], auto_enable: bool, cu 'name': rule_name, 'conditions': conditions, 'environments': [statsig_tier], - 'passPercentage': 100 if auto_enable or self._is_development_environment() else 0, + 'passPercentage': 100 if auto_enable else 0, } # Ensure rules list exists and add the new rule @@ -503,36 +494,6 @@ def _add_environment_rule(self, gate_data: dict[str, Any], auto_enable: bool, cu return updated_gate - def _update_environment_rule(self, gate_data: dict[str, Any], auto_enable: bool, custom_attributes: dict[str, Any] | None = None) -> dict[str, Any]: - """ - Update an existing environment-specific rule in the gate. - - :param gate_data: Original gate configuration - :param auto_enable: If True, passPercentage=100; if False, passPercentage=0 - :param custom_attributes: Optional custom attributes for targeting - :return: Updated gate configuration - """ - updated_gate = gate_data.copy() - rule_name = f'{self.environment.lower()}-rule' - statsig_tier = STATSIG_ENVIRONMENT_MAPPING.get(self.environment.lower(), STATSIG_DEVELOPMENT_TIER) - - # Find and update the environment rule - for rule in updated_gate.get('rules', []): - if rule.get('name') == rule_name: - # Update passPercentage - rule['passPercentage'] = 100 if auto_enable or self._is_development_environment() else 0 - - # Update conditions if custom_attributes provided - if custom_attributes: - rule['conditions'] = self._build_conditions_from_attributes(custom_attributes) - - # Ensure environment tier is set - if statsig_tier not in rule.get('environments', []): - rule['environments'] = rule.get('environments', []) + [statsig_tier] - break - - return updated_gate - def _update_gate(self, gate_id: str, gate_data: dict[str, Any]) -> bool: """ Update a feature gate using the PATCH endpoint. @@ -591,7 +552,7 @@ def delete_flag(self, flag_name: str) -> bool | None: raise FeatureFlagException(f'Flag data missing ID field: {flag_name}') rule_name = f'{self.environment.lower()}-rule' - + # Check if current environment rule exists environment_rule = self._find_environment_rule(flag_data, rule_name) if not environment_rule: diff --git a/backend/compact-connect/lambdas/python/feature-flag/tests/function/test_statsig_client.py b/backend/compact-connect/lambdas/python/feature-flag/tests/function/test_statsig_client.py index 37c868ced..84bede3f3 100644 --- a/backend/compact-connect/lambdas/python/feature-flag/tests/function/test_statsig_client.py +++ b/backend/compact-connect/lambdas/python/feature-flag/tests/function/test_statsig_client.py @@ -264,7 +264,7 @@ def _create_mock_response(self, status_code: int, json_data: dict = None): @patch('feature_flag_client.Statsig') @patch('feature_flag_client.requests') def test_upsert_flag_create_new_in_test_environment(self, mock_requests, mock_statsig): - """Test creating a new flag in test environment with auto_enable=False (passPercentage=100 for dev)""" + """Test creating a new flag in test environment with auto_enable=true (passPercentage=100 for dev)""" self._setup_mock_statsig(mock_statsig) # Mock GET request (flag doesn't exist) @@ -276,7 +276,7 @@ def test_upsert_flag_create_new_in_test_environment(self, mock_requests, mock_st client = StatSigFeatureFlagClient(environment='test') - result = client.upsert_flag('new-test-flag', auto_enable=False, custom_attributes={'region': 'us-east-1'}) + result = client.upsert_flag('new-test-flag', auto_enable=True, custom_attributes={'region': 'us-east-1'}) # Verify result self.assertEqual(result['id'], 'gate-123') @@ -356,7 +356,7 @@ def test_upsert_flag_create_new_in_test_environment_no_attributes(self, mock_req 'name': 'test-rule', 'conditions': [], 'environments': ['development'], - 'passPercentage': 100, + 'passPercentage': 0, } ], } @@ -366,7 +366,7 @@ def test_upsert_flag_create_new_in_test_environment_no_attributes(self, mock_req @patch('feature_flag_client.Statsig') @patch('feature_flag_client.requests') - def test_upsert_flag_update_existing_in_test_environment(self, mock_requests, mock_statsig): + def test_upsert_flag_does_not_update_existing_rule(self, mock_requests, mock_statsig): """Test updating an existing flag in test environment (test-rule already exists, no modifications in test)""" self._setup_mock_statsig(mock_statsig) @@ -397,36 +397,7 @@ def test_upsert_flag_update_existing_in_test_environment(self, mock_requests, mo # Verify API calls - no PATCH since test environment doesn't modify existing rules self.assertEqual(1, mock_requests.get.call_count) - mock_requests.patch.assert_called_once_with( - f'{STATSIG_API_BASE_URL}/gates/gate-789', - headers={ - 'STATSIG-API-KEY': MOCK_CONSOLE_KEY, - 'STATSIG-API-VERSION': STATSIG_API_VERSION, - 'Content-Type': 'application/json', - }, - data=json.dumps( - { - 'id': 'gate-789', - 'name': 'existing-flag', - 'rules': [ - { - 'name': 'test-rule', - 'conditions': [ - { - 'type': 'custom_field', - 'targetValue': ['new_value'], - 'field': 'new_attr', - 'operator': 'any', - } - ], - 'environments': ['development'], - 'passPercentage': 100, - } - ], - } - ), - timeout=30, - ) + mock_requests.patch.assert_not_called() @patch('feature_flag_client.Statsig') @patch('feature_flag_client.requests') @@ -524,7 +495,9 @@ def test_upsert_flag_prod_environment_auto_enable_true_no_existing_flag(self, mo @patch('feature_flag_client.Statsig') @patch('feature_flag_client.requests') - def test_upsert_flag_beta_environment_auto_enable_false_no_existing_rule_create_rule(self, mock_requests, mock_statsig): + def test_upsert_flag_beta_environment_auto_enable_false_no_existing_rule_create_rule( + self, mock_requests, mock_statsig + ): """Test upsert in prod environment with autoEnable=True and no existing flag""" self._setup_mock_statsig(mock_statsig) @@ -578,8 +551,8 @@ def test_upsert_flag_beta_environment_auto_enable_false_no_existing_rule_create_ 'conditions': [], 'environments': ['staging'], 'passPercentage': 0, - } - ] + }, + ], } ), timeout=30, @@ -642,7 +615,12 @@ def test_upsert_flag_prod_environment_existing_flag_auto_enable_true(self, mock_ { 'name': 'prod-rule', 'conditions': [ - {'type': 'custom_field', 'targetValue': ['value'], 'field': 'example', 'operator': 'any'} + { + 'type': 'custom_field', + 'targetValue': ['value'], + 'field': 'example', + 'operator': 'any', + } ], 'environments': ['production'], 'passPercentage': 100, @@ -941,7 +919,6 @@ def test_delete_flag_prod_environment_last_rule(self, mock_requests, mock_statsi timeout=30, ) - @patch('feature_flag_client.Statsig') @patch('feature_flag_client.requests') def test_delete_flag_current_rule_not_present(self, mock_requests, mock_statsig): @@ -1067,7 +1044,9 @@ def test_upsert_flag_custom_attributes_as_string(self, mock_requests, mock_stats client = StatSigFeatureFlagClient(environment='test') - result = client.upsert_flag('string-attrs-flag', auto_enable=False, custom_attributes={'region': 'us-east-1', 'feature': 'new'}) + result = client.upsert_flag( + 'string-attrs-flag', auto_enable=False, custom_attributes={'region': 'us-east-1', 'feature': 'new'} + ) # Verify result self.assertEqual(result['id'], 'gate-string-attrs') @@ -1098,7 +1077,7 @@ def test_upsert_flag_custom_attributes_as_string(self, mock_requests, mock_stats {'type': 'custom_field', 'targetValue': ['new'], 'field': 'feature', 'operator': 'any'}, ], 'environments': ['development'], - 'passPercentage': 100, # Always 100 for test environment + 'passPercentage': 0, # Always 100 for test environment } ], } @@ -1121,7 +1100,9 @@ def test_upsert_flag_custom_attributes_as_list(self, mock_requests, mock_statsig client = StatSigFeatureFlagClient(environment='test') - result = client.upsert_flag('list-attrs-flag', auto_enable=False, custom_attributes={'licenseType': ['slp', 'audiologist']}) + result = client.upsert_flag( + 'list-attrs-flag', auto_enable=False, custom_attributes={'licenseType': ['slp', 'audiologist']} + ) # Verify result self.assertEqual(result['id'], 'gate-list-attrs') @@ -1151,7 +1132,7 @@ def test_upsert_flag_custom_attributes_as_list(self, mock_requests, mock_statsig } ], 'environments': ['development'], - 'passPercentage': 100, + 'passPercentage': 0, } ], } From 8ba194a60ef1534314de060251bb23be0ba596ab Mon Sep 17 00:00:00 2001 From: Landon Shumway Date: Thu, 2 Oct 2025 17:30:51 -0500 Subject: [PATCH 26/55] add sandbox example --- backend/compact-connect/stacks/feature_flag_stack/__init__.py | 3 ++- .../stacks/feature_flag_stack/feature_flag_resource.py | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/backend/compact-connect/stacks/feature_flag_stack/__init__.py b/backend/compact-connect/stacks/feature_flag_stack/__init__.py index 385a7d0cd..d641ebaf4 100644 --- a/backend/compact-connect/stacks/feature_flag_stack/__init__.py +++ b/backend/compact-connect/stacks/feature_flag_stack/__init__.py @@ -84,7 +84,7 @@ def __init__( # Feature Flags are deployed through a custom resource # one per flag - self.test_flag = FeatureFlagResource( + self.example_flag = FeatureFlagResource( self, 'ExampleFlag', flag_name='example-flag', @@ -93,6 +93,7 @@ def __init__( FeatureFlagEnvironmentName.TEST, FeatureFlagEnvironmentName.BETA, FeatureFlagEnvironmentName.PROD, + FeatureFlagEnvironmentName.SANDBOX ], # Note that flags are not updated once set and must be manually updated through the console custom_attributes={'compact': ['aslp']}, diff --git a/backend/compact-connect/stacks/feature_flag_stack/feature_flag_resource.py b/backend/compact-connect/stacks/feature_flag_stack/feature_flag_resource.py index 9e5b402ba..f241a4834 100644 --- a/backend/compact-connect/stacks/feature_flag_stack/feature_flag_resource.py +++ b/backend/compact-connect/stacks/feature_flag_stack/feature_flag_resource.py @@ -23,7 +23,7 @@ class FeatureFlagEnvironmentName(Enum): BETA = 'beta' PROD = 'prod' # add sandbox environment names here if needed - # SANDBOX = 'sandbox' + SANDBOX = 'sandbox' @jsii.implements(IGrantable) From 6e0699946172e5f2a6869011689ef0856491aa63 Mon Sep 17 00:00:00 2001 From: Landon Shumway Date: Thu, 2 Oct 2025 17:43:50 -0500 Subject: [PATCH 27/55] linter --- .../feature-flag/feature_flag_client.py | 51 +++++++++---------- .../handlers/manage_feature_flag.py | 15 ++++-- .../stacks/feature_flag_stack/__init__.py | 2 +- 3 files changed, 36 insertions(+), 32 deletions(-) diff --git a/backend/compact-connect/lambdas/python/feature-flag/feature_flag_client.py b/backend/compact-connect/lambdas/python/feature-flag/feature_flag_client.py index 8bb008b01..d6351f52e 100644 --- a/backend/compact-connect/lambdas/python/feature-flag/feature_flag_client.py +++ b/backend/compact-connect/lambdas/python/feature-flag/feature_flag_client.py @@ -370,19 +370,19 @@ def upsert_flag( if not existing_gate: # Create new gate with environment-specific rule return self._create_new_gate(flag_name, auto_enable, custom_attributes) - else: - # Gate exists - check if environment rule exists - gate_id = existing_gate.get('id') - rule_name = f'{self.environment.lower()}-rule' - environment_rule = self._find_environment_rule(existing_gate, rule_name) - # we only set the environment rule if it doesn't already exist - # else we leave it alone to avoid overwriting manual changes - if not environment_rule: - updated_gate = self._add_environment_rule(existing_gate, auto_enable, custom_attributes) - self._update_gate(gate_id, updated_gate) + # Gate exists - check if environment rule exists + gate_id = existing_gate.get('id') + rule_name = f'{self.environment.lower()}-rule' + environment_rule = self._find_environment_rule(existing_gate, rule_name) + + # we only set the environment rule if it doesn't already exist + # else we leave it alone to avoid overwriting manual changes + if not environment_rule: + updated_gate = self._add_environment_rule(existing_gate, auto_enable, custom_attributes) + self._update_gate(gate_id, updated_gate) - return existing_gate + return existing_gate def _create_new_gate( self, flag_name: str, auto_enable: bool, custom_attributes: dict[str, Any] | None = None @@ -424,8 +424,8 @@ def _create_new_gate( if response.status_code in [200, 201]: return response.json() - else: - raise FeatureFlagException(f'Failed to create feature gate: {response.status_code} - {response.text[:200]}') + + raise FeatureFlagException(f'Failed to create feature gate: {response.status_code} - {response.text[:200]}') def _find_environment_rule(self, gate_data: dict[str, Any], rule_name: str) -> dict[str, Any] | None: """ @@ -507,8 +507,8 @@ def _update_gate(self, gate_id: str, gate_data: dict[str, Any]) -> bool: if response.status_code in [200, 204]: return True - else: - raise FeatureFlagException(f'Failed to update feature gate: {response.status_code} - {response.text[:200]}') + + raise FeatureFlagException(f'Failed to update feature gate: {response.status_code} - {response.text[:200]}') def get_flag(self, flag_name: str) -> dict[str, Any] | None: """ @@ -526,10 +526,9 @@ def get_flag(self, flag_name: str) -> dict[str, Any] | None: for gate in gates_data.get('data', []): if gate.get('name') == flag_name: return gate - return None - else: - raise FeatureFlagException(f'Failed to fetch gates: {response.status_code} - {response.text[:200]}') + + raise FeatureFlagException(f'Failed to fetch gates: {response.status_code} - {response.text[:200]}') def delete_flag(self, flag_name: str) -> bool | None: """ @@ -568,14 +567,14 @@ def delete_flag(self, flag_name: str) -> bool | None: if response.status_code in [200, 204]: return True # Flag fully deleted - else: - raise FeatureFlagException( - f'Failed to delete feature gate: {response.status_code} - {response.text[:200]}' - ) - else: - # Remove only the current environment's rule - removed = self._remove_environment_rule_from_flag(flag_id, flag_data, rule_name) - return False if removed else False # Environment rule removed, not full deletion + + raise FeatureFlagException( + f'Failed to delete feature gate: {response.status_code} - {response.text[:200]}' + ) + + # Remove only the current environment's rule + removed = self._remove_environment_rule_from_flag(flag_id, flag_data, rule_name) + return False if removed else False # Environment rule removed, not full deletion def _remove_environment_rule_from_flag(self, flag_id: str, flag_data: dict[str, Any], rule_name: str) -> bool: """ diff --git a/backend/compact-connect/lambdas/python/feature-flag/handlers/manage_feature_flag.py b/backend/compact-connect/lambdas/python/feature-flag/handlers/manage_feature_flag.py index 2f0114d1a..0adbf3831 100644 --- a/backend/compact-connect/lambdas/python/feature-flag/handlers/manage_feature_flag.py +++ b/backend/compact-connect/lambdas/python/feature-flag/handlers/manage_feature_flag.py @@ -9,7 +9,7 @@ from cc_common.config import logger from custom_resource_handler import CustomResourceHandler, CustomResourceResponse -from feature_flag_client import StatSigFeatureFlagClient +from feature_flag_client import FeatureFlagException, StatSigFeatureFlagClient class ManageFeatureFlagHandler(CustomResourceHandler): @@ -60,7 +60,7 @@ def on_create(self, properties: dict) -> CustomResourceResponse | None: # Return the gate ID as the PhysicalResourceId for tracking return {'PhysicalResourceId': f'feature-flag-{flag_name}-{self.environment}', 'Data': {'gateId': gate_id}} - def on_update(self, properties: dict) -> CustomResourceResponse | None: + def on_update(self, properties: dict) -> CustomResourceResponse | None: # noqa: ARG002 """ Flags are not updated once created in an environment. @@ -86,9 +86,14 @@ def on_delete(self, properties: dict) -> CustomResourceResponse | None: logger.info('Deleting feature flag resource', flag_name=flag_name, environment=self.environment) - # Delete flag or remove current environment - # The delete_flag method handles all logic internally (fetching, checking environments, etc.) - result = self.client.delete_flag(flag_name) + try: + # Delete flag or remove current environment + # The delete_flag method handles all logic internally (fetching, checking environments, etc.) + result = self.client.delete_flag(flag_name) + except FeatureFlagException: + # log the error and return so we don't fail deployment + logger.error('Failed to delete feature flag', flag_name=flag_name) + return None if result is None: logger.info('Feature gate does not exist, nothing to delete', flag_name=flag_name) diff --git a/backend/compact-connect/stacks/feature_flag_stack/__init__.py b/backend/compact-connect/stacks/feature_flag_stack/__init__.py index d641ebaf4..fdfd2a6ab 100644 --- a/backend/compact-connect/stacks/feature_flag_stack/__init__.py +++ b/backend/compact-connect/stacks/feature_flag_stack/__init__.py @@ -93,7 +93,7 @@ def __init__( FeatureFlagEnvironmentName.TEST, FeatureFlagEnvironmentName.BETA, FeatureFlagEnvironmentName.PROD, - FeatureFlagEnvironmentName.SANDBOX + FeatureFlagEnvironmentName.SANDBOX, ], # Note that flags are not updated once set and must be manually updated through the console custom_attributes={'compact': ['aslp']}, From b9bb7521e85967d2c7fc06bf5e219eafbfdf18d2 Mon Sep 17 00:00:00 2001 From: Landon Shumway Date: Fri, 3 Oct 2025 09:11:26 -0500 Subject: [PATCH 28/55] formatter/linter --- .../common/cc_common/feature_flag_client.py | 27 +++++++------------ .../feature-flag/feature_flag_client.py | 4 +-- 2 files changed, 11 insertions(+), 20 deletions(-) diff --git a/backend/compact-connect/lambdas/python/common/cc_common/feature_flag_client.py b/backend/compact-connect/lambdas/python/common/cc_common/feature_flag_client.py index 3fe980c41..b364e8354 100644 --- a/backend/compact-connect/lambdas/python/common/cc_common/feature_flag_client.py +++ b/backend/compact-connect/lambdas/python/common/cc_common/feature_flag_client.py @@ -57,31 +57,23 @@ def is_feature_enabled(flag_name: str, context: FeatureFlagContext | None = None Example: # Simple check without context - is_feature_enabled('test-feature') - True + if is_feature_enabled('test-feature'): + # feature code here # Check with user ID - is_feature_enabled( + if is_feature_enabled( 'test-feature', context=FeatureFlagContext(user_id='user123') - ) - False + ): # Check with user ID and custom attributes - is_feature_enabled( + if is_feature_enabled( 'test-feature', context=FeatureFlagContext( user_id='user456', custom_attributes={'licenseType': 'lpc', 'jurisdiction': 'oh'} ) - ) - True - - # Fail open - if API fails, allow access - is_feature_enabled('critical-feature', fail_open=True) - - # Fail closed - if API fails, deny access (default) - is_feature_enabled('new-feature', fail_open=False) + ): """ try: api_base_url = _get_api_base_url() @@ -108,13 +100,14 @@ def is_feature_enabled(flag_name: str, context: FeatureFlagContext | None = None # Extract and return the 'enabled' field if 'enabled' not in response_data: logger.info('Invalid response format - return fail_open value', response_data=response_data) - # Invalid response format - return fail_open value + # Invalid response format - return fail_default value return fail_default return bool(response_data['enabled']) - except Exception as e: - # Any error (timeout, network, parsing, etc.) - return fail_open value + # We catch all exceptions to prevent a feature flag issue causing the system from operating + except Exception as e: # noqa: BLE001 + # Any error (timeout, network, parsing, etc.) - return fail_default value logger.info('Error checking feature flag - return fail_open value', exc_info=e) return fail_default diff --git a/backend/compact-connect/lambdas/python/feature-flag/feature_flag_client.py b/backend/compact-connect/lambdas/python/feature-flag/feature_flag_client.py index d6351f52e..5bc62fdba 100644 --- a/backend/compact-connect/lambdas/python/feature-flag/feature_flag_client.py +++ b/backend/compact-connect/lambdas/python/feature-flag/feature_flag_client.py @@ -568,9 +568,7 @@ def delete_flag(self, flag_name: str) -> bool | None: if response.status_code in [200, 204]: return True # Flag fully deleted - raise FeatureFlagException( - f'Failed to delete feature gate: {response.status_code} - {response.text[:200]}' - ) + raise FeatureFlagException(f'Failed to delete feature gate: {response.status_code} - {response.text[:200]}') # Remove only the current environment's rule removed = self._remove_environment_rule_from_flag(flag_id, flag_data, rule_name) From 40ddb0315a3253e0f5d50dab32078404aa3f9b94 Mon Sep 17 00:00:00 2001 From: Landon Shumway Date: Fri, 3 Oct 2025 09:25:07 -0500 Subject: [PATCH 29/55] fix tests --- .../feature-flag/tests/function/test_check_feature_flag.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/backend/compact-connect/lambdas/python/feature-flag/tests/function/test_check_feature_flag.py b/backend/compact-connect/lambdas/python/feature-flag/tests/function/test_check_feature_flag.py index 572e41ec7..6289891b6 100644 --- a/backend/compact-connect/lambdas/python/feature-flag/tests/function/test_check_feature_flag.py +++ b/backend/compact-connect/lambdas/python/feature-flag/tests/function/test_check_feature_flag.py @@ -23,7 +23,10 @@ def setUp(self): secrets_client = boto3.client('secretsmanager', region_name='us-east-1') secrets_client.create_secret( Name='compact-connect/env/test/statsig/credentials', - SecretString=json.dumps({'serverKey': 'test-server-key-123'}), + SecretString=json.dumps({ + 'serverKey': 'test-server-key-123', + 'consoleKey': 'test-console-key-456' + }), ) def tearDown(self): From 34149c45f998396817afa1be5a9c6dedddef56a8 Mon Sep 17 00:00:00 2001 From: Landon Shumway Date: Fri, 3 Oct 2025 09:52:15 -0500 Subject: [PATCH 30/55] PR feedback --- .../lambdas/python/feature-flag/feature_flag_client.py | 8 ++++---- .../tests/function/test_check_feature_flag.py | 5 +---- backend/compact-connect/stacks/api_stack/v1_api/api.py | 6 +++++- .../stacks/feature_flag_stack/feature_flag_resource.py | 4 ++-- 4 files changed, 12 insertions(+), 11 deletions(-) diff --git a/backend/compact-connect/lambdas/python/feature-flag/feature_flag_client.py b/backend/compact-connect/lambdas/python/feature-flag/feature_flag_client.py index 5bc62fdba..711e05590 100644 --- a/backend/compact-connect/lambdas/python/feature-flag/feature_flag_client.py +++ b/backend/compact-connect/lambdas/python/feature-flag/feature_flag_client.py @@ -474,9 +474,9 @@ def _add_environment_rule( statsig_tier = STATSIG_ENVIRONMENT_MAPPING.get(self.environment.lower(), STATSIG_DEVELOPMENT_TIER) rule_name = f'{self.environment.lower()}-rule' - # Build conditions if auto_enable is True conditions = [] - if auto_enable and custom_attributes: + # Build conditions if custom attributes were passed in + if custom_attributes: conditions = self._build_conditions_from_attributes(custom_attributes) # Add new environment rule @@ -571,8 +571,8 @@ def delete_flag(self, flag_name: str) -> bool | None: raise FeatureFlagException(f'Failed to delete feature gate: {response.status_code} - {response.text[:200]}') # Remove only the current environment's rule - removed = self._remove_environment_rule_from_flag(flag_id, flag_data, rule_name) - return False if removed else False # Environment rule removed, not full deletion + self._remove_environment_rule_from_flag(flag_id, flag_data, rule_name) + return False # Environment rule removed, not full deletion def _remove_environment_rule_from_flag(self, flag_id: str, flag_data: dict[str, Any], rule_name: str) -> bool: """ diff --git a/backend/compact-connect/lambdas/python/feature-flag/tests/function/test_check_feature_flag.py b/backend/compact-connect/lambdas/python/feature-flag/tests/function/test_check_feature_flag.py index 6289891b6..e14c3af0c 100644 --- a/backend/compact-connect/lambdas/python/feature-flag/tests/function/test_check_feature_flag.py +++ b/backend/compact-connect/lambdas/python/feature-flag/tests/function/test_check_feature_flag.py @@ -23,10 +23,7 @@ def setUp(self): secrets_client = boto3.client('secretsmanager', region_name='us-east-1') secrets_client.create_secret( Name='compact-connect/env/test/statsig/credentials', - SecretString=json.dumps({ - 'serverKey': 'test-server-key-123', - 'consoleKey': 'test-console-key-456' - }), + SecretString=json.dumps({'serverKey': 'test-server-key-123', 'consoleKey': 'test-console-key-456'}), ) def tearDown(self): diff --git a/backend/compact-connect/stacks/api_stack/v1_api/api.py b/backend/compact-connect/stacks/api_stack/v1_api/api.py index 7775496d2..cf8e07ad4 100644 --- a/backend/compact-connect/stacks/api_stack/v1_api/api.py +++ b/backend/compact-connect/stacks/api_stack/v1_api/api.py @@ -45,7 +45,11 @@ def __init__( data_event_bus = SSMParameterUtility.load_data_event_bus_from_ssm_parameter(stack) _active_compacts = persistent_stack.get_list_of_compact_abbreviations() - stack.common_env_vars.update({'API_BASE_URL': f'https://{persistent_stack.api_domain_name}'}) + api_base_url = ( + f'https://{persistent_stack.api_domain_name}' if persistent_stack.api_domain_name else self.api.url + ) + + stack.common_env_vars.update({'API_BASE_URL': api_base_url}) read_scopes = [] write_scopes = [] diff --git a/backend/compact-connect/stacks/feature_flag_stack/feature_flag_resource.py b/backend/compact-connect/stacks/feature_flag_stack/feature_flag_resource.py index f241a4834..178014941 100644 --- a/backend/compact-connect/stacks/feature_flag_stack/feature_flag_resource.py +++ b/backend/compact-connect/stacks/feature_flag_stack/feature_flag_resource.py @@ -6,7 +6,7 @@ """ import os -from enum import Enum +from enum import StrEnum import jsii from aws_cdk import CustomResource, Duration, Stack @@ -18,7 +18,7 @@ from constructs import Construct -class FeatureFlagEnvironmentName(Enum): +class FeatureFlagEnvironmentName(StrEnum): TEST = 'test' BETA = 'beta' PROD = 'prod' From e15d0c1b665311fecb1f9f9f382cdba327a3e02f Mon Sep 17 00:00:00 2001 From: Landon Shumway Date: Fri, 3 Oct 2025 10:13:39 -0500 Subject: [PATCH 31/55] more PR feedback --- .../tests/function/test_handlers/test_encumbrance.py | 6 +++--- .../stacks/feature_flag_stack/feature_flag_resource.py | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/backend/compact-connect/lambdas/python/provider-data-v1/tests/function/test_handlers/test_encumbrance.py b/backend/compact-connect/lambdas/python/provider-data-v1/tests/function/test_handlers/test_encumbrance.py index 2e3395b58..b99ebd7d3 100644 --- a/backend/compact-connect/lambdas/python/provider-data-v1/tests/function/test_handlers/test_encumbrance.py +++ b/backend/compact-connect/lambdas/python/provider-data-v1/tests/function/test_handlers/test_encumbrance.py @@ -156,6 +156,8 @@ def test_privilege_encumbrance_handler_adds_privilege_update_record_in_provider_ self.assertEqual(1, len(privilege_update_records['Items'])) item = privilege_update_records['Items'][0] + loaded_privilege_update_data = PrivilegeUpdateData.from_database_record(item) + expected_privilege_update_data = self.test_data_generator.generate_default_privilege_update( value_overrides={ 'updateType': 'encumbrance', @@ -164,12 +166,10 @@ def test_privilege_encumbrance_handler_adds_privilege_update_record_in_provider_ 'createDate': datetime.fromisoformat(DEFAULT_DATE_OF_UPDATE_TIMESTAMP), 'encumbranceDetails': { 'clinicalPrivilegeActionCategory': 'Unsafe Practice or Substandard Care', - 'adverseActionId': DEFAULT_ADVERSE_ACTION_ID, + 'adverseActionId': loaded_privilege_update_data.encumbranceDetails['adverseActionId'], }, } ) - loaded_privilege_update_data = PrivilegeUpdateData.from_database_record(item) - loaded_privilege_update_data.encumbranceDetails['adverseActionId'] = uuid.UUID(DEFAULT_ADVERSE_ACTION_ID) self.assertEqual( expected_privilege_update_data.to_dict(), diff --git a/backend/compact-connect/stacks/feature_flag_stack/feature_flag_resource.py b/backend/compact-connect/stacks/feature_flag_stack/feature_flag_resource.py index 178014941..db9a44160 100644 --- a/backend/compact-connect/stacks/feature_flag_stack/feature_flag_resource.py +++ b/backend/compact-connect/stacks/feature_flag_stack/feature_flag_resource.py @@ -55,8 +55,8 @@ def __init__( """ super().__init__(scope, construct_id) - if not flag_name: - raise ValueError('flag_name is required') + if not flag_name or not environment_name: + raise ValueError('flag_name and environment_name are required') # Lambda function for managing feature flags self.manage_function = PythonFunction( From a044dc4e6bd3413766c9e91aa657e4649fbccb2b Mon Sep 17 00:00:00 2001 From: Landon Shumway Date: Fri, 3 Oct 2025 10:38:01 -0500 Subject: [PATCH 32/55] update requirements to latest --- .../lambdas/python/feature-flag/requirements-dev.txt | 6 +++--- .../lambdas/python/feature-flag/requirements.txt | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/backend/compact-connect/lambdas/python/feature-flag/requirements-dev.txt b/backend/compact-connect/lambdas/python/feature-flag/requirements-dev.txt index 0329b6161..3ba5fa673 100644 --- a/backend/compact-connect/lambdas/python/feature-flag/requirements-dev.txt +++ b/backend/compact-connect/lambdas/python/feature-flag/requirements-dev.txt @@ -2,11 +2,11 @@ # This file is autogenerated by pip-compile with Python 3.12 # by the following command: # -# pip-compile --no-emit-index-url compact-connect/lambdas/python/feature-flag/requirements-dev.in +# pip-compile --cert=None --client-cert=None --index-url=None --no-emit-index-url --pip-args=None compact-connect/lambdas/python/feature-flag/requirements-dev.in # -boto3==1.40.43 +boto3==1.40.44 # via moto -botocore==1.40.43 +botocore==1.40.44 # via # boto3 # moto diff --git a/backend/compact-connect/lambdas/python/feature-flag/requirements.txt b/backend/compact-connect/lambdas/python/feature-flag/requirements.txt index ec599d6c7..202c85a0c 100644 --- a/backend/compact-connect/lambdas/python/feature-flag/requirements.txt +++ b/backend/compact-connect/lambdas/python/feature-flag/requirements.txt @@ -2,7 +2,7 @@ # This file is autogenerated by pip-compile with Python 3.12 # by the following command: # -# pip-compile --no-emit-index-url compact-connect/lambdas/python/feature-flag/requirements.in +# pip-compile --cert=None --client-cert=None --index-url=None --no-emit-index-url --pip-args=None compact-connect/lambdas/python/feature-flag/requirements.in # certifi==2025.8.3 # via requests From f5dd16ed98f17d2171347eedd3df7e1cd2b1af38 Mon Sep 17 00:00:00 2001 From: Landon Shumway Date: Fri, 3 Oct 2025 10:52:48 -0500 Subject: [PATCH 33/55] setting API_BASE_URL env var only if domain name is present --- .../tests/function/test_handlers/test_encumbrance.py | 2 -- .../compact-connect/stacks/api_lambda_stack/__init__.py | 7 ++++++- backend/compact-connect/stacks/api_stack/v1_api/api.py | 9 ++++----- .../stacks/feature_flag_stack/__init__.py | 2 ++ 4 files changed, 12 insertions(+), 8 deletions(-) diff --git a/backend/compact-connect/lambdas/python/provider-data-v1/tests/function/test_handlers/test_encumbrance.py b/backend/compact-connect/lambdas/python/provider-data-v1/tests/function/test_handlers/test_encumbrance.py index b99ebd7d3..b48549319 100644 --- a/backend/compact-connect/lambdas/python/provider-data-v1/tests/function/test_handlers/test_encumbrance.py +++ b/backend/compact-connect/lambdas/python/provider-data-v1/tests/function/test_handlers/test_encumbrance.py @@ -1,5 +1,4 @@ import json -import uuid from datetime import UTC, date, datetime, timedelta from unittest.mock import patch @@ -7,7 +6,6 @@ from cc_common.exceptions import CCInternalException from common_test.test_constants import ( DEFAULT_AA_SUBMITTING_USER_ID, - DEFAULT_ADVERSE_ACTION_ID, DEFAULT_DATE_OF_UPDATE_TIMESTAMP, DEFAULT_ENCUMBRANCE_TYPE, DEFAULT_LICENSE_JURISDICTION, diff --git a/backend/compact-connect/stacks/api_lambda_stack/__init__.py b/backend/compact-connect/stacks/api_lambda_stack/__init__.py index 5acfecf2a..cd336e89d 100644 --- a/backend/compact-connect/stacks/api_lambda_stack/__init__.py +++ b/backend/compact-connect/stacks/api_lambda_stack/__init__.py @@ -1,6 +1,6 @@ from __future__ import annotations -from common_constructs.stack import AppStack +from common_constructs.stack import AppStack, Stack from constructs import Construct from stacks import persistent_stack as ps @@ -30,6 +30,11 @@ def __init__( **kwargs, ) + # we only pass the API_BASE_URL env var if the API_DOMAIN_NAME is set + # this is because the API_BASE_URL is used by the feature flag client to call the flag check endpoint + if persistent_stack.api_domain_name: + self.common_env_vars.update({'API_BASE_URL': f'https://{persistent_stack.api_domain_name}'}) + # Feature Flags related API lambdas self.feature_flags_lambdas = FeatureFlagsLambdas( scope=self, diff --git a/backend/compact-connect/stacks/api_stack/v1_api/api.py b/backend/compact-connect/stacks/api_stack/v1_api/api.py index cf8e07ad4..3d40fdfd6 100644 --- a/backend/compact-connect/stacks/api_stack/v1_api/api.py +++ b/backend/compact-connect/stacks/api_stack/v1_api/api.py @@ -45,11 +45,10 @@ def __init__( data_event_bus = SSMParameterUtility.load_data_event_bus_from_ssm_parameter(stack) _active_compacts = persistent_stack.get_list_of_compact_abbreviations() - api_base_url = ( - f'https://{persistent_stack.api_domain_name}' if persistent_stack.api_domain_name else self.api.url - ) - - stack.common_env_vars.update({'API_BASE_URL': api_base_url}) + # we only pass the API_BASE_URL env var if the API_DOMAIN_NAME is set + # this is because the API_BASE_URL is used by the feature flag client to call the flag check endpoint + if persistent_stack.api_domain_name: + stack.common_env_vars.update({'API_BASE_URL': f'https://{persistent_stack.api_domain_name}'}) read_scopes = [] write_scopes = [] diff --git a/backend/compact-connect/stacks/feature_flag_stack/__init__.py b/backend/compact-connect/stacks/feature_flag_stack/__init__.py index fdfd2a6ab..bc4d0622e 100644 --- a/backend/compact-connect/stacks/feature_flag_stack/__init__.py +++ b/backend/compact-connect/stacks/feature_flag_stack/__init__.py @@ -10,6 +10,8 @@ When a flag is no longer used, removing it from this stack should result in cleaning up all the environment based rules for the flag and deleting it from StatSig once it has been removed from all environments. +NOTE: Flags are only currently supported if the environment has a domain name configured. + Feature Flag Lifecycle: ----------------------- 1. **Creation** (on_create): From 4b84f56ef4f1fa52ea8635fcfc8f28d594063df3 Mon Sep 17 00:00:00 2001 From: Landon Shumway Date: Fri, 3 Oct 2025 10:53:28 -0500 Subject: [PATCH 34/55] remove unused import --- backend/compact-connect/stacks/api_lambda_stack/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/compact-connect/stacks/api_lambda_stack/__init__.py b/backend/compact-connect/stacks/api_lambda_stack/__init__.py index cd336e89d..7220e8dcb 100644 --- a/backend/compact-connect/stacks/api_lambda_stack/__init__.py +++ b/backend/compact-connect/stacks/api_lambda_stack/__init__.py @@ -1,6 +1,6 @@ from __future__ import annotations -from common_constructs.stack import AppStack, Stack +from common_constructs.stack import AppStack from constructs import Construct from stacks import persistent_stack as ps From 5084039b4ca5f82ff4d84e950f8ceb20b08faddb Mon Sep 17 00:00:00 2001 From: Landon Shumway Date: Fri, 3 Oct 2025 16:54:14 -0500 Subject: [PATCH 35/55] Implement PR feedback --- .../common/cc_common/feature_flag_client.py | 6 +- .../feature-flag/feature_flag_client.py | 5 +- .../stacks/api_stack/v1_api/feature_flags.py | 4 + .../stacks/feature_flag_stack/__init__.py | 107 ++++++++++++++++- .../feature_flag_resource.py | 110 ++---------------- 5 files changed, 120 insertions(+), 112 deletions(-) diff --git a/backend/compact-connect/lambdas/python/common/cc_common/feature_flag_client.py b/backend/compact-connect/lambdas/python/common/cc_common/feature_flag_client.py index b364e8354..22629848c 100644 --- a/backend/compact-connect/lambdas/python/common/cc_common/feature_flag_client.py +++ b/backend/compact-connect/lambdas/python/common/cc_common/feature_flag_client.py @@ -53,7 +53,7 @@ def is_feature_enabled(flag_name: str, context: FeatureFlagContext | None = None :param flag_name: The name of the feature flag to check :param context: Optional FeatureFlagContext for feature flag evaluation :param fail_default: If True, return True on errors; if False, return False on errors (default: False) - :return: True if the feature flag is enabled, False otherwise (or fail_open value on error) + :return: True if the feature flag is enabled, False otherwise (or fail_default value on error) Example: # Simple check without context @@ -99,7 +99,7 @@ def is_feature_enabled(flag_name: str, context: FeatureFlagContext | None = None # Extract and return the 'enabled' field if 'enabled' not in response_data: - logger.info('Invalid response format - return fail_open value', response_data=response_data) + logger.info('Invalid response format - return fail_default value', response_data=response_data) # Invalid response format - return fail_default value return fail_default @@ -108,7 +108,7 @@ def is_feature_enabled(flag_name: str, context: FeatureFlagContext | None = None # We catch all exceptions to prevent a feature flag issue causing the system from operating except Exception as e: # noqa: BLE001 # Any error (timeout, network, parsing, etc.) - return fail_default value - logger.info('Error checking feature flag - return fail_open value', exc_info=e) + logger.info('Error checking feature flag - return fail_default value', exc_info=e) return fail_default diff --git a/backend/compact-connect/lambdas/python/feature-flag/feature_flag_client.py b/backend/compact-connect/lambdas/python/feature-flag/feature_flag_client.py index 711e05590..d2dc33dab 100644 --- a/backend/compact-connect/lambdas/python/feature-flag/feature_flag_client.py +++ b/backend/compact-connect/lambdas/python/feature-flag/feature_flag_client.py @@ -1,7 +1,6 @@ # ruff: noqa: N801, N815 invalid-name import json -import os from abc import ABC, abstractmethod from dataclasses import dataclass from typing import Any @@ -134,9 +133,7 @@ def _get_secret(self, secret_name: str) -> dict[str, Any]: try: # Create a Secrets Manager client session = boto3.session.Session() - client = session.client( - service_name='secretsmanager', region_name=os.environ.get('AWS_DEFAULT_REGION', 'us-east-1') - ) + client = session.client(service_name='secretsmanager') # Retrieve the secret value response = client.get_secret_value(SecretId=secret_name) diff --git a/backend/compact-connect/stacks/api_stack/v1_api/feature_flags.py b/backend/compact-connect/stacks/api_stack/v1_api/feature_flags.py index f4695f10f..b494134fe 100644 --- a/backend/compact-connect/stacks/api_stack/v1_api/feature_flags.py +++ b/backend/compact-connect/stacks/api_stack/v1_api/feature_flags.py @@ -2,6 +2,7 @@ from aws_cdk.aws_apigateway import LambdaIntegration, Resource from cdk_nag import NagSuppressions +from common_constructs.cc_api import CCApi from stacks.api_lambda_stack import ApiLambdaStack @@ -20,6 +21,7 @@ def __init__( ): super().__init__() self.resource = resource + self.api: CCApi = resource.api self.api_model = api_model # POST /v1/flags/check @@ -36,6 +38,8 @@ def __init__( ], ) + self.api.log_groups.append(api_lambda_stack.feature_flags_lambdas.check_feature_flag_function.log_group) + # Add suppressions for the public GET endpoint NagSuppressions.add_resource_suppressions( self.check_flag_method, diff --git a/backend/compact-connect/stacks/feature_flag_stack/__init__.py b/backend/compact-connect/stacks/feature_flag_stack/__init__.py index bc4d0622e..62afde120 100644 --- a/backend/compact-connect/stacks/feature_flag_stack/__init__.py +++ b/backend/compact-connect/stacks/feature_flag_stack/__init__.py @@ -67,6 +67,14 @@ from __future__ import annotations +import os + +from aws_cdk import Duration +from aws_cdk.aws_logs import RetentionDays +from aws_cdk.aws_secretsmanager import Secret +from aws_cdk.custom_resources import Provider +from cdk_nag import NagSuppressions +from common_constructs.python_function import PythonFunction from common_constructs.stack import AppStack from constructs import Construct @@ -84,11 +92,14 @@ def __init__( ): super().__init__(scope, construct_id, environment_name=environment_name, **kwargs) - # Feature Flags are deployed through a custom resource - # one per flag + self.provider = self._create_common_provider(environment_name) + + # Feature Flags are deployed through custom resources + # All flags share the same custom resource provider defined above self.example_flag = FeatureFlagResource( self, 'ExampleFlag', + provider=self.provider, # Shared provider flag_name='example-flag', # This causes the flag to automatically be set to enabled for every environment in the list auto_enable_envs=[ @@ -101,3 +112,95 @@ def __init__( custom_attributes={'compact': ['aslp']}, environment_name=environment_name, ) + + def _create_common_provider(self, environment_name: str) -> Provider: + # Create shared Lambda function for managing all feature flags + # This function is reused across all FeatureFlagResource instances + self.manage_function = PythonFunction( + self, + 'ManageFunction', + index=os.path.join('handlers', 'manage_feature_flag.py'), + lambda_dir='feature-flag', + handler='on_event', + log_retention=RetentionDays.ONE_MONTH, + environment={'ENVIRONMENT_NAME': environment_name}, + timeout=Duration.minutes(5), + memory_size=256, + ) + + # Grant permissions to read secrets + self.statsig_secret = Secret.from_secret_name_v2( + self, + 'StatsigSecret', + f'compact-connect/env/{environment_name}/statsig/credentials', + ) + self.statsig_secret.grant_read(self.manage_function) + + # Add CDK Nag suppressions for the Lambda function + NagSuppressions.add_resource_suppressions_by_path( + self, + path=f'{self.manage_function.node.path}/ServiceRole/DefaultPolicy/Resource', + suppressions=[ + { + 'id': 'AwsSolutions-IAM5', + 'reason': 'The actions in this policy contain a wildcard specifically to access the feature flag ' + 'client credentials secret and all of its versions.', + }, + ], + ) + + # Create shared custom resource provider + # This provider is reused across all FeatureFlagResource instances + provider = Provider( + self, 'Provider', on_event_handler=self.manage_function, log_retention=RetentionDays.ONE_DAY + ) + + # Add CDK Nag suppressions for the provider framework + NagSuppressions.add_resource_suppressions_by_path( + self, + f'{provider.node.path}/framework-onEvent/Resource', + [ + {'id': 'AwsSolutions-L1', 'reason': 'We do not control this runtime'}, + { + 'id': 'HIPAA.Security-LambdaConcurrency', + 'reason': 'This function is only run at deploy time, by CloudFormation and has no need for ' + 'concurrency limits.', + }, + { + 'id': 'HIPAA.Security-LambdaDLQ', + 'reason': 'This is a synchronous function run at deploy time. It does not need a DLQ', + }, + { + 'id': 'HIPAA.Security-LambdaInsideVPC', + 'reason': 'We may choose to move our lambdas into private VPC subnets in a future enhancement', + }, + ], + ) + + NagSuppressions.add_resource_suppressions_by_path( + self, + path=f'{provider.node.path}/framework-onEvent/ServiceRole/DefaultPolicy/Resource', + suppressions=[ + { + 'id': 'AwsSolutions-IAM5', + 'reason': 'The actions in this policy contain a wildcard specifically to access the feature flag ' + 'client credentials secret and all of its versions.', + }, + ], + ) + + NagSuppressions.add_resource_suppressions_by_path( + self, + path=f'{provider.node.path}/framework-onEvent/ServiceRole/Resource', + suppressions=[ + { + 'id': 'AwsSolutions-IAM4', + 'appliesTo': [ + 'Policy::arn::iam::aws:policy/service-role/AWSLambdaBasicExecutionRole' + ], + 'reason': 'This policy is appropriate for the custom resource lambda', + }, + ], + ) + + return provider diff --git a/backend/compact-connect/stacks/feature_flag_stack/feature_flag_resource.py b/backend/compact-connect/stacks/feature_flag_stack/feature_flag_resource.py index db9a44160..15b89b154 100644 --- a/backend/compact-connect/stacks/feature_flag_stack/feature_flag_resource.py +++ b/backend/compact-connect/stacks/feature_flag_stack/feature_flag_resource.py @@ -5,16 +5,10 @@ of StatSig feature flags across different environments. """ -import os from enum import StrEnum -import jsii -from aws_cdk import CustomResource, Duration, Stack -from aws_cdk.aws_iam import IGrantable, PolicyStatement -from aws_cdk.aws_logs import RetentionDays +from aws_cdk import CustomResource from aws_cdk.custom_resources import Provider -from cdk_nag import NagSuppressions -from common_constructs.python_function import PythonFunction from constructs import Construct @@ -26,13 +20,13 @@ class FeatureFlagEnvironmentName(StrEnum): SANDBOX = 'sandbox' -@jsii.implements(IGrantable) class FeatureFlagResource(Construct): """ Custom resource for managing StatSig feature flags. - This construct creates a Lambda-backed custom resource that handles - creation, updates, and deletion of feature flags in StatSig. + This construct creates a CloudFormation custom resource that manages + the lifecycle of a single feature flag in StatSig. The Lambda function + and provider are shared across all flags and passed in as parameters. """ def __init__( @@ -40,6 +34,7 @@ def __init__( scope: Construct, construct_id: str, *, + provider: Provider, flag_name: str, auto_enable_envs: list[FeatureFlagEnvironmentName], custom_attributes: dict[str, str] | dict[str, list] | None = None, @@ -48,6 +43,7 @@ def __init__( """ Initialize the FeatureFlagResource construct. + :param provider: Shared CloudFormation custom resource provider :param flag_name: Name of the feature flag to manage :param auto_enable_envs: List of environments to automatically enable the flag for :param custom_attributes: Optional custom attributes for feature flag targeting @@ -58,94 +54,7 @@ def __init__( if not flag_name or not environment_name: raise ValueError('flag_name and environment_name are required') - # Lambda function for managing feature flags - self.manage_function = PythonFunction( - self, - 'ManageFunction', - index=os.path.join('handlers', 'manage_feature_flag.py'), - lambda_dir='feature-flag', - handler='on_event', - log_retention=RetentionDays.ONE_MONTH, - environment={'ENVIRONMENT_NAME': environment_name}, - timeout=Duration.minutes(5), - memory_size=256, - ) - - # Grant permissions to read secrets - secret_name = f'compact-connect/env/{environment_name}/statsig/credentials' - self.manage_function.add_to_role_policy( - PolicyStatement( - actions=['secretsmanager:GetSecretValue'], - resources=[ - f'arn:aws:secretsmanager:{Stack.of(self).region}:{Stack.of(self).account}:secret:{secret_name}-*' - ], - ) - ) - - # Create the custom resource provider - self.provider = Provider( - self, 'Provider', on_event_handler=self.manage_function, log_retention=RetentionDays.ONE_DAY - ) - - # Add CDK Nag suppressions for the provider framework - NagSuppressions.add_resource_suppressions_by_path( - Stack.of(self), - f'{self.provider.node.path}/framework-onEvent/Resource', - [ - {'id': 'AwsSolutions-L1', 'reason': 'We do not control this runtime'}, - { - 'id': 'HIPAA.Security-LambdaConcurrency', - 'reason': 'This function is only run at deploy time, by CloudFormation and has no need for ' - 'concurrency limits.', - }, - { - 'id': 'HIPAA.Security-LambdaDLQ', - 'reason': 'This is a synchronous function run at deploy time. It does not need a DLQ', - }, - { - 'id': 'HIPAA.Security-LambdaInsideVPC', - 'reason': 'We may choose to move our lambdas into private VPC subnets in a future enhancement', - }, - ], - ) - - NagSuppressions.add_resource_suppressions_by_path( - Stack.of(self), - path=f'{self.manage_function.node.path}/ServiceRole/DefaultPolicy/Resource', - suppressions=[ - { - 'id': 'AwsSolutions-IAM5', - 'reason': 'The actions in this policy contain a wildcard specifically to access the feature flag ' - 'client credentials secret and all of its versions.', - }, - ], - ) - - NagSuppressions.add_resource_suppressions_by_path( - Stack.of(self), - path=f'{self.provider.node.path}/framework-onEvent/ServiceRole/DefaultPolicy/Resource', - suppressions=[ - { - 'id': 'AwsSolutions-IAM5', - 'reason': 'The actions in this policy contain a wildcard specifically to access the feature flag ' - 'client credentials secret and all of its versions.', - }, - ], - ) - - NagSuppressions.add_resource_suppressions_by_path( - Stack.of(self), - path=f'{self.provider.node.path}/framework-onEvent/ServiceRole/Resource', - suppressions=[ - { - 'id': 'AwsSolutions-IAM4', - 'appliesTo': [ - 'Policy::arn::iam::aws:policy/service-role/AWSLambdaBasicExecutionRole' - ], - 'reason': 'This policy is appropriate for the custom resource lambda', - }, - ], - ) + self.provider = provider # Build custom resource properties properties = {'flagName': flag_name, 'autoEnable': environment_name in auto_enable_envs} @@ -161,8 +70,3 @@ def __init__( service_token=self.provider.service_token, properties=properties, ) - - @property - def grant_principal(self): - """Return the grant principal for IAM permissions""" - return self.manage_function.grant_principal From 9976e9493e067aa85f202a8cc3e11e4bd1f6dba9 Mon Sep 17 00:00:00 2001 From: Landon Shumway Date: Mon, 6 Oct 2025 11:18:46 -0500 Subject: [PATCH 36/55] PR feedback --- .../feature-flag/feature_flag_client.py | 13 +++-- .../tests/function/test_statsig_client.py | 54 +++++++------------ 2 files changed, 24 insertions(+), 43 deletions(-) diff --git a/backend/compact-connect/lambdas/python/feature-flag/feature_flag_client.py b/backend/compact-connect/lambdas/python/feature-flag/feature_flag_client.py index d2dc33dab..8b3f8532d 100644 --- a/backend/compact-connect/lambdas/python/feature-flag/feature_flag_client.py +++ b/backend/compact-connect/lambdas/python/feature-flag/feature_flag_client.py @@ -332,9 +332,9 @@ def _make_console_api_request( if method == 'GET': response = requests.get(url, headers=headers, timeout=30) elif method == 'POST': - response = requests.post(url, headers=headers, data=json.dumps(data), timeout=30) + response = requests.post(url, headers=headers, json=data, timeout=30) elif method == 'PATCH': - response = requests.patch(url, headers=headers, data=json.dumps(data), timeout=30) + response = requests.patch(url, headers=headers, json=data, timeout=30) elif method == 'DELETE': response = requests.delete(url, headers=headers, timeout=30) else: @@ -376,8 +376,7 @@ def upsert_flag( # we only set the environment rule if it doesn't already exist # else we leave it alone to avoid overwriting manual changes if not environment_rule: - updated_gate = self._add_environment_rule(existing_gate, auto_enable, custom_attributes) - self._update_gate(gate_id, updated_gate) + self._add_environment_rule(gate_id, existing_gate, auto_enable, custom_attributes) return existing_gate @@ -457,8 +456,8 @@ def _build_conditions_from_attributes(self, custom_attributes: dict[str, Any]) - return conditions def _add_environment_rule( - self, gate_data: dict[str, Any], auto_enable: bool, custom_attributes: dict[str, Any] | None = None - ) -> dict[str, Any]: + self, gate_id: str, gate_data: dict[str, Any], auto_enable: bool, custom_attributes: dict[str, Any] | None = None + ) -> None: """ Add an environment-specific rule to an existing gate. @@ -489,7 +488,7 @@ def _add_environment_rule( updated_gate['rules'] = [] updated_gate['rules'].append(new_rule) - return updated_gate + self._update_gate(gate_id, updated_gate) def _update_gate(self, gate_id: str, gate_data: dict[str, Any]) -> bool: """ diff --git a/backend/compact-connect/lambdas/python/feature-flag/tests/function/test_statsig_client.py b/backend/compact-connect/lambdas/python/feature-flag/tests/function/test_statsig_client.py index 84bede3f3..fb49c34c9 100644 --- a/backend/compact-connect/lambdas/python/feature-flag/tests/function/test_statsig_client.py +++ b/backend/compact-connect/lambdas/python/feature-flag/tests/function/test_statsig_client.py @@ -292,8 +292,7 @@ def test_upsert_flag_create_new_in_test_environment(self, mock_requests, mock_st 'STATSIG-API-VERSION': STATSIG_API_VERSION, 'Content-Type': 'application/json', }, - data=json.dumps( - { + json={ 'name': 'new-test-flag', 'description': 'Feature gate managed by CDK for new-test-flag feature', 'isEnabled': True, @@ -312,8 +311,7 @@ def test_upsert_flag_create_new_in_test_environment(self, mock_requests, mock_st 'passPercentage': 100, # Always 100 for test environment } ], - } - ), + }, timeout=30, ) @@ -346,8 +344,7 @@ def test_upsert_flag_create_new_in_test_environment_no_attributes(self, mock_req 'STATSIG-API-VERSION': STATSIG_API_VERSION, 'Content-Type': 'application/json', }, - data=json.dumps( - { + json={ 'name': 'simple-flag', 'description': 'Feature gate managed by CDK for simple-flag feature', 'isEnabled': True, @@ -359,8 +356,7 @@ def test_upsert_flag_create_new_in_test_environment_no_attributes(self, mock_req 'passPercentage': 0, } ], - } - ), + }, timeout=30, ) @@ -428,8 +424,7 @@ def test_upsert_flag_prod_environment_auto_enable_false_no_existing_flag(self, m 'STATSIG-API-VERSION': STATSIG_API_VERSION, 'Content-Type': 'application/json', }, - data=json.dumps( - { + json={ 'name': 'prod-flag', 'description': 'Feature gate managed by CDK for prod-flag feature', 'isEnabled': True, @@ -441,8 +436,7 @@ def test_upsert_flag_prod_environment_auto_enable_false_no_existing_flag(self, m 'passPercentage': 0, # Disabled in prod when auto_enable=False } ], - } - ), + }, timeout=30, ) @@ -475,8 +469,7 @@ def test_upsert_flag_prod_environment_auto_enable_true_no_existing_flag(self, mo 'STATSIG-API-VERSION': STATSIG_API_VERSION, 'Content-Type': 'application/json', }, - data=json.dumps( - { + json={ 'name': 'prod-flag', 'description': 'Feature gate managed by CDK for prod-flag feature', 'isEnabled': True, @@ -488,8 +481,7 @@ def test_upsert_flag_prod_environment_auto_enable_true_no_existing_flag(self, mo 'passPercentage': 100, } ], - } - ), + }, timeout=30, ) @@ -535,8 +527,7 @@ def test_upsert_flag_beta_environment_auto_enable_false_no_existing_rule_create_ 'STATSIG-API-VERSION': STATSIG_API_VERSION, 'Content-Type': 'application/json', }, - data=json.dumps( - { + json={ 'id': 'existing-flag', 'name': 'existing-flag', 'rules': [ @@ -553,8 +544,7 @@ def test_upsert_flag_beta_environment_auto_enable_false_no_existing_rule_create_ 'passPercentage': 0, }, ], - } - ), + }, timeout=30, ) @@ -601,8 +591,7 @@ def test_upsert_flag_prod_environment_existing_flag_auto_enable_true(self, mock_ 'STATSIG-API-VERSION': STATSIG_API_VERSION, 'Content-Type': 'application/json', }, - data=json.dumps( - { + json={ 'id': 'gate-existing-prod', 'name': 'existing-prod-flag', 'rules': [ @@ -626,8 +615,7 @@ def test_upsert_flag_prod_environment_existing_flag_auto_enable_true(self, mock_ 'passPercentage': 100, }, ], - } - ), + }, timeout=30, ) @@ -851,8 +839,7 @@ def test_delete_flag_multiple_rules_removes_current_rule_only(self, mock_request 'STATSIG-API-VERSION': STATSIG_API_VERSION, 'Content-Type': 'application/json', }, - data=json.dumps( - { + json={ 'id': 'gate-delete-multi', 'name': 'delete-multi-flag', 'rules': [ @@ -869,8 +856,7 @@ def test_delete_flag_multiple_rules_removes_current_rule_only(self, mock_request 'passPercentage': 100, }, ], - } - ), + }, timeout=30, ) mock_requests.delete.assert_not_called() @@ -1059,8 +1045,7 @@ def test_upsert_flag_custom_attributes_as_string(self, mock_requests, mock_stats 'STATSIG-API-VERSION': STATSIG_API_VERSION, 'Content-Type': 'application/json', }, - data=json.dumps( - { + json={ 'name': 'string-attrs-flag', 'description': 'Feature gate managed by CDK for string-attrs-flag feature', 'isEnabled': True, @@ -1080,8 +1065,7 @@ def test_upsert_flag_custom_attributes_as_string(self, mock_requests, mock_stats 'passPercentage': 0, # Always 100 for test environment } ], - } - ), + }, timeout=30, ) @@ -1115,8 +1099,7 @@ def test_upsert_flag_custom_attributes_as_list(self, mock_requests, mock_statsig 'STATSIG-API-VERSION': STATSIG_API_VERSION, 'Content-Type': 'application/json', }, - data=json.dumps( - { + json={ 'name': 'list-attrs-flag', 'description': 'Feature gate managed by CDK for list-attrs-flag feature', 'isEnabled': True, @@ -1135,8 +1118,7 @@ def test_upsert_flag_custom_attributes_as_list(self, mock_requests, mock_statsig 'passPercentage': 0, } ], - } - ), + }, timeout=30, ) From ba3c7b72ff85decee33bbc8abe2ebd06df297a72 Mon Sep 17 00:00:00 2001 From: Landon Shumway Date: Mon, 6 Oct 2025 11:43:05 -0500 Subject: [PATCH 37/55] formatter/linter/pr feedback --- .../common/cc_common/feature_flag_client.py | 2 +- .../feature-flag/feature_flag_client.py | 6 +- .../tests/function/test_statsig_client.py | 286 +++++++++--------- .../feature_flag_resource.py | 2 +- 4 files changed, 150 insertions(+), 146 deletions(-) diff --git a/backend/compact-connect/lambdas/python/common/cc_common/feature_flag_client.py b/backend/compact-connect/lambdas/python/common/cc_common/feature_flag_client.py index 22629848c..11b239f12 100644 --- a/backend/compact-connect/lambdas/python/common/cc_common/feature_flag_client.py +++ b/backend/compact-connect/lambdas/python/common/cc_common/feature_flag_client.py @@ -117,7 +117,7 @@ def _get_api_base_url() -> str: Get the API base URL from environment variables. :return: The base URL for the API - :raises ValueError: If API_BASE_URL is not set + :raises KeyError: If API_BASE_URL is not set """ api_base_url = config.api_base_url # Remove trailing slash if present diff --git a/backend/compact-connect/lambdas/python/feature-flag/feature_flag_client.py b/backend/compact-connect/lambdas/python/feature-flag/feature_flag_client.py index 8b3f8532d..6c2b8eb36 100644 --- a/backend/compact-connect/lambdas/python/feature-flag/feature_flag_client.py +++ b/backend/compact-connect/lambdas/python/feature-flag/feature_flag_client.py @@ -456,7 +456,11 @@ def _build_conditions_from_attributes(self, custom_attributes: dict[str, Any]) - return conditions def _add_environment_rule( - self, gate_id: str, gate_data: dict[str, Any], auto_enable: bool, custom_attributes: dict[str, Any] | None = None + self, + gate_id: str, + gate_data: dict[str, Any], + auto_enable: bool, + custom_attributes: dict[str, Any] | None = None, ) -> None: """ Add an environment-specific rule to an existing gate. diff --git a/backend/compact-connect/lambdas/python/feature-flag/tests/function/test_statsig_client.py b/backend/compact-connect/lambdas/python/feature-flag/tests/function/test_statsig_client.py index fb49c34c9..2585e82fd 100644 --- a/backend/compact-connect/lambdas/python/feature-flag/tests/function/test_statsig_client.py +++ b/backend/compact-connect/lambdas/python/feature-flag/tests/function/test_statsig_client.py @@ -293,24 +293,24 @@ def test_upsert_flag_create_new_in_test_environment(self, mock_requests, mock_st 'Content-Type': 'application/json', }, json={ - 'name': 'new-test-flag', - 'description': 'Feature gate managed by CDK for new-test-flag feature', - 'isEnabled': True, - 'rules': [ - { - 'name': 'test-rule', - 'conditions': [ - { - 'type': 'custom_field', - 'targetValue': ['us-east-1'], - 'field': 'region', - 'operator': 'any', - } - ], - 'environments': ['development'], - 'passPercentage': 100, # Always 100 for test environment - } - ], + 'name': 'new-test-flag', + 'description': 'Feature gate managed by CDK for new-test-flag feature', + 'isEnabled': True, + 'rules': [ + { + 'name': 'test-rule', + 'conditions': [ + { + 'type': 'custom_field', + 'targetValue': ['us-east-1'], + 'field': 'region', + 'operator': 'any', + } + ], + 'environments': ['development'], + 'passPercentage': 100, # Always 100 for test environment + } + ], }, timeout=30, ) @@ -345,17 +345,17 @@ def test_upsert_flag_create_new_in_test_environment_no_attributes(self, mock_req 'Content-Type': 'application/json', }, json={ - 'name': 'simple-flag', - 'description': 'Feature gate managed by CDK for simple-flag feature', - 'isEnabled': True, - 'rules': [ - { - 'name': 'test-rule', - 'conditions': [], - 'environments': ['development'], - 'passPercentage': 0, - } - ], + 'name': 'simple-flag', + 'description': 'Feature gate managed by CDK for simple-flag feature', + 'isEnabled': True, + 'rules': [ + { + 'name': 'test-rule', + 'conditions': [], + 'environments': ['development'], + 'passPercentage': 0, + } + ], }, timeout=30, ) @@ -425,17 +425,17 @@ def test_upsert_flag_prod_environment_auto_enable_false_no_existing_flag(self, m 'Content-Type': 'application/json', }, json={ - 'name': 'prod-flag', - 'description': 'Feature gate managed by CDK for prod-flag feature', - 'isEnabled': True, - 'rules': [ - { - 'name': 'prod-rule', - 'conditions': [], - 'environments': ['production'], - 'passPercentage': 0, # Disabled in prod when auto_enable=False - } - ], + 'name': 'prod-flag', + 'description': 'Feature gate managed by CDK for prod-flag feature', + 'isEnabled': True, + 'rules': [ + { + 'name': 'prod-rule', + 'conditions': [], + 'environments': ['production'], + 'passPercentage': 0, # Disabled in prod when auto_enable=False + } + ], }, timeout=30, ) @@ -470,17 +470,17 @@ def test_upsert_flag_prod_environment_auto_enable_true_no_existing_flag(self, mo 'Content-Type': 'application/json', }, json={ - 'name': 'prod-flag', - 'description': 'Feature gate managed by CDK for prod-flag feature', - 'isEnabled': True, - 'rules': [ - { - 'name': 'prod-rule', - 'conditions': [], - 'environments': ['production'], - 'passPercentage': 100, - } - ], + 'name': 'prod-flag', + 'description': 'Feature gate managed by CDK for prod-flag feature', + 'isEnabled': True, + 'rules': [ + { + 'name': 'prod-rule', + 'conditions': [], + 'environments': ['production'], + 'passPercentage': 100, + } + ], }, timeout=30, ) @@ -528,22 +528,22 @@ def test_upsert_flag_beta_environment_auto_enable_false_no_existing_rule_create_ 'Content-Type': 'application/json', }, json={ - 'id': 'existing-flag', - 'name': 'existing-flag', - 'rules': [ - { - 'name': 'test-rule', - 'conditions': [{'field': 'old_attr', 'targetValue': ['old_value']}], - 'environments': ['development'], - 'passPercentage': 100, - }, - { - 'name': 'beta-rule', - 'conditions': [], - 'environments': ['staging'], - 'passPercentage': 0, - }, - ], + 'id': 'existing-flag', + 'name': 'existing-flag', + 'rules': [ + { + 'name': 'test-rule', + 'conditions': [{'field': 'old_attr', 'targetValue': ['old_value']}], + 'environments': ['development'], + 'passPercentage': 100, + }, + { + 'name': 'beta-rule', + 'conditions': [], + 'environments': ['staging'], + 'passPercentage': 0, + }, + ], }, timeout=30, ) @@ -592,29 +592,29 @@ def test_upsert_flag_prod_environment_existing_flag_auto_enable_true(self, mock_ 'Content-Type': 'application/json', }, json={ - 'id': 'gate-existing-prod', - 'name': 'existing-prod-flag', - 'rules': [ - { - 'name': 'test-rule', - 'conditions': [], - 'environments': ['development'], - 'passPercentage': 100, - }, - { - 'name': 'prod-rule', - 'conditions': [ - { - 'type': 'custom_field', - 'targetValue': ['value'], - 'field': 'example', - 'operator': 'any', - } - ], - 'environments': ['production'], - 'passPercentage': 100, - }, - ], + 'id': 'gate-existing-prod', + 'name': 'existing-prod-flag', + 'rules': [ + { + 'name': 'test-rule', + 'conditions': [], + 'environments': ['development'], + 'passPercentage': 100, + }, + { + 'name': 'prod-rule', + 'conditions': [ + { + 'type': 'custom_field', + 'targetValue': ['value'], + 'field': 'example', + 'operator': 'any', + } + ], + 'environments': ['production'], + 'passPercentage': 100, + }, + ], }, timeout=30, ) @@ -840,22 +840,22 @@ def test_delete_flag_multiple_rules_removes_current_rule_only(self, mock_request 'Content-Type': 'application/json', }, json={ - 'id': 'gate-delete-multi', - 'name': 'delete-multi-flag', - 'rules': [ - { - 'name': 'beta-rule', - 'conditions': [], - 'environments': ['staging'], - 'passPercentage': 100, - }, - { - 'name': 'prod-rule', - 'conditions': [], - 'environments': ['production'], - 'passPercentage': 100, - }, - ], + 'id': 'gate-delete-multi', + 'name': 'delete-multi-flag', + 'rules': [ + { + 'name': 'beta-rule', + 'conditions': [], + 'environments': ['staging'], + 'passPercentage': 100, + }, + { + 'name': 'prod-rule', + 'conditions': [], + 'environments': ['production'], + 'passPercentage': 100, + }, + ], }, timeout=30, ) @@ -1046,25 +1046,25 @@ def test_upsert_flag_custom_attributes_as_string(self, mock_requests, mock_stats 'Content-Type': 'application/json', }, json={ - 'name': 'string-attrs-flag', - 'description': 'Feature gate managed by CDK for string-attrs-flag feature', - 'isEnabled': True, - 'rules': [ - { - 'name': 'test-rule', - 'conditions': [ - { - 'type': 'custom_field', - 'targetValue': ['us-east-1'], - 'field': 'region', - 'operator': 'any', - }, - {'type': 'custom_field', 'targetValue': ['new'], 'field': 'feature', 'operator': 'any'}, - ], - 'environments': ['development'], - 'passPercentage': 0, # Always 100 for test environment - } - ], + 'name': 'string-attrs-flag', + 'description': 'Feature gate managed by CDK for string-attrs-flag feature', + 'isEnabled': True, + 'rules': [ + { + 'name': 'test-rule', + 'conditions': [ + { + 'type': 'custom_field', + 'targetValue': ['us-east-1'], + 'field': 'region', + 'operator': 'any', + }, + {'type': 'custom_field', 'targetValue': ['new'], 'field': 'feature', 'operator': 'any'}, + ], + 'environments': ['development'], + 'passPercentage': 0, # Always 100 for test environment + } + ], }, timeout=30, ) @@ -1100,24 +1100,24 @@ def test_upsert_flag_custom_attributes_as_list(self, mock_requests, mock_statsig 'Content-Type': 'application/json', }, json={ - 'name': 'list-attrs-flag', - 'description': 'Feature gate managed by CDK for list-attrs-flag feature', - 'isEnabled': True, - 'rules': [ - { - 'name': 'test-rule', - 'conditions': [ - { - 'type': 'custom_field', - 'targetValue': ['slp', 'audiologist'], - 'field': 'licenseType', - 'operator': 'any', - } - ], - 'environments': ['development'], - 'passPercentage': 0, - } - ], + 'name': 'list-attrs-flag', + 'description': 'Feature gate managed by CDK for list-attrs-flag feature', + 'isEnabled': True, + 'rules': [ + { + 'name': 'test-rule', + 'conditions': [ + { + 'type': 'custom_field', + 'targetValue': ['slp', 'audiologist'], + 'field': 'licenseType', + 'operator': 'any', + } + ], + 'environments': ['development'], + 'passPercentage': 0, + } + ], }, timeout=30, ) diff --git a/backend/compact-connect/stacks/feature_flag_stack/feature_flag_resource.py b/backend/compact-connect/stacks/feature_flag_stack/feature_flag_resource.py index 15b89b154..73840f7b3 100644 --- a/backend/compact-connect/stacks/feature_flag_stack/feature_flag_resource.py +++ b/backend/compact-connect/stacks/feature_flag_stack/feature_flag_resource.py @@ -37,7 +37,7 @@ def __init__( provider: Provider, flag_name: str, auto_enable_envs: list[FeatureFlagEnvironmentName], - custom_attributes: dict[str, str] | dict[str, list] | None = None, + custom_attributes: dict[str, str | list[str]] | None = None, environment_name: str, ): """ From fd47e6a78a1b171d3ad79204e25ba9abb76f9599 Mon Sep 17 00:00:00 2001 From: Landon Shumway Date: Mon, 6 Oct 2025 11:57:37 -0500 Subject: [PATCH 38/55] Add feature flag bootstrapping documentation --- backend/compact-connect/README.md | 45 ++++++++++++++++++++++++++++--- 1 file changed, 41 insertions(+), 4 deletions(-) diff --git a/backend/compact-connect/README.md b/backend/compact-connect/README.md index 6e6ac356c..57bfa7a0e 100644 --- a/backend/compact-connect/README.md +++ b/backend/compact-connect/README.md @@ -188,11 +188,12 @@ its environment: The key under `environments` must match the value you put under `environment_name`. 6) Configure your aws cli to authenticate against your own account. There are several ways to do this based on the type of authentication you use to login to your account. See the [AWS CLI Configuration Guide](https://docs.aws.amazon.com/cli/latest/userguide/getting-started-quickstart.html). -7) Complete the [Google reCAPTCHA Setup](#google-recaptcha-setup) steps for your sandbox environment. -8) Run `cdk bootstrap` to add some base CDK support infrastructure to your AWS account. See +7) Complete the [StatSig Feature Flag Setup](#statsig-feature-flag-setup) steps for your sandbox environment. +8) Complete the [Google reCAPTCHA Setup](#google-recaptcha-setup) steps for your sandbox environment. +9) Run `cdk bootstrap` to add some base CDK support infrastructure to your AWS account. See [Custom bootstrap stack](#custom-bootstrap-stack) below for optional custom stack deployment. -9) Run `cdk deploy 'Sandbox/*'` to get the initial backend stack resources deployed. -10) *Optional:* If you have a domain name configured for your sandbox environment, once the backend stacks have +10) Run `cdk deploy 'Sandbox/*'` to get the initial backend stack resources deployed. +11)*Optional:* If you have a domain name configured for your sandbox environment, once the backend stacks have successfully deployed, you can deploy the frontend UI app as well. See the [UI app for details](../compact-connect-ui-app/README.md). @@ -230,6 +231,7 @@ authentication is working as expected. The production environment requires a few steps to fully set up before deploys can be automated. Refer to the [README.md](../multi-account/README.md) for details on setting up a full multi-account architecture environment. Once that is done, perform the following steps to deploy the CI/CD pipelines into the appropriate AWS account: +- Complete the [StatSig Feature Flag Setup](#statsig-feature-flag-setup) steps for each environment you will be deploying to (test, beta, prod). - Complete the [Google reCAPTCHA Setup](#google-recaptcha-setup) steps for each environment you will be deploying to (test, beta, prod). Use the appropriate domain name for the environment (ie `app.test.compactconnect.org` for test environment, `app.beta.compactconnect.org` for beta environment, `app.compactconnect.org` for production). For the production @@ -286,6 +288,41 @@ Once the pipelines are established with the above steps, deployments will be aut - Pushes to the `main` branch will trigger both the beta and production backend pipelines, which will then trigger their respective frontend pipelines. +## StatSig Feature Flag Setup +[Back to top](#compact-connect---backend-developer-documentation) + +The feature flag system uses StatSig to manage feature flags across different environments. Follow these steps to set up StatSig for your environment: + +1. **Create a StatSig Account** + - Visit [StatSig](https://www.statsig.com/) and create an account + - Set up your project and organization + +2. **Generate API Keys** + - Navigate to the [API Keys section](https://docs.statsig.com/guides/first-feature/#step-4---create-a-new-client-api-key) of the StatSig console + - You'll need to create three types of API keys: + - **Server Secret Key**: Used for server-side feature flag evaluation + - **Client API Key**: Used for client-side feature flag evaluation (optional for this backend setup) + - **Console API Key**: Used for programmatic management of feature flags via the Console API + +3. **Store Credentials in AWS Secrets Manager** + - For each environment (test, beta, prod), create a secret in AWS Secrets Manager with the following naming pattern: + ``` + compact-connect/env/{environment_name}/statsig/credentials + ``` + - The secret value should be a JSON object with the following structure: + ```json + { + "serverKey": "", + "consoleKey": "" + } + ``` + - You can create the secret for each environment account by logging into the respective environment account and using the AWS CLI: + ```bash + aws secretsmanager create-secret \ + --name "compact-connect/env/{test | beta | prod}/statsig/credentials" \ + --secret-string '{"serverKey": "", "consoleKey": ""}' + ``` + ## Google reCAPTCHA Setup [Back to top](#compact-connect---backend-developer-documentation) From 430dd33d5e7a0f2db51b009b5a945b07eb44cd3d Mon Sep 17 00:00:00 2001 From: Landon Shumway Date: Mon, 6 Oct 2025 12:01:41 -0500 Subject: [PATCH 39/55] PR feedback --- .../lambdas/python/feature-flag/feature_flag_client.py | 2 +- .../python/feature-flag/tests/function/test_statsig_client.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/backend/compact-connect/lambdas/python/feature-flag/feature_flag_client.py b/backend/compact-connect/lambdas/python/feature-flag/feature_flag_client.py index 6c2b8eb36..c531c1b32 100644 --- a/backend/compact-connect/lambdas/python/feature-flag/feature_flag_client.py +++ b/backend/compact-connect/lambdas/python/feature-flag/feature_flag_client.py @@ -110,7 +110,7 @@ def get_flag(self, flag_name: str) -> dict[str, Any] | None: """ @abstractmethod - def delete_flag(self, flag_name: str) -> bool: + def delete_flag(self, flag_name: str) -> bool | None: """ Delete a feature flag or remove current environment from it. diff --git a/backend/compact-connect/lambdas/python/feature-flag/tests/function/test_statsig_client.py b/backend/compact-connect/lambdas/python/feature-flag/tests/function/test_statsig_client.py index 2585e82fd..7edc9c8b1 100644 --- a/backend/compact-connect/lambdas/python/feature-flag/tests/function/test_statsig_client.py +++ b/backend/compact-connect/lambdas/python/feature-flag/tests/function/test_statsig_client.py @@ -1062,7 +1062,7 @@ def test_upsert_flag_custom_attributes_as_string(self, mock_requests, mock_stats {'type': 'custom_field', 'targetValue': ['new'], 'field': 'feature', 'operator': 'any'}, ], 'environments': ['development'], - 'passPercentage': 0, # Always 100 for test environment + 'passPercentage': 0, # 0 since auto_enabled is false } ], }, From 66d654eb910fe56689e960ce78e3878f726ae3c6 Mon Sep 17 00:00:00 2001 From: Landon Shumway Date: Mon, 6 Oct 2025 12:27:13 -0500 Subject: [PATCH 40/55] more pr feedback --- .../python/feature-flag/feature_flag_client.py | 1 - .../tests/function/test_statsig_client.py | 14 +++++++------- .../feature_flag_stack/feature_flag_resource.py | 2 +- 3 files changed, 8 insertions(+), 9 deletions(-) diff --git a/backend/compact-connect/lambdas/python/feature-flag/feature_flag_client.py b/backend/compact-connect/lambdas/python/feature-flag/feature_flag_client.py index c531c1b32..db4d8ac07 100644 --- a/backend/compact-connect/lambdas/python/feature-flag/feature_flag_client.py +++ b/backend/compact-connect/lambdas/python/feature-flag/feature_flag_client.py @@ -202,7 +202,6 @@ class StatSigFeatureFlagClient(FeatureFlagClient): StatSig implementation of the FeatureFlagClient interface. This client uses StatSig's Python SDK to check feature flags. - Configuration is handled through environment variables. """ def __init__(self, environment: str): diff --git a/backend/compact-connect/lambdas/python/feature-flag/tests/function/test_statsig_client.py b/backend/compact-connect/lambdas/python/feature-flag/tests/function/test_statsig_client.py index 7edc9c8b1..8c269067f 100644 --- a/backend/compact-connect/lambdas/python/feature-flag/tests/function/test_statsig_client.py +++ b/backend/compact-connect/lambdas/python/feature-flag/tests/function/test_statsig_client.py @@ -284,7 +284,7 @@ def test_upsert_flag_create_new_in_test_environment(self, mock_requests, mock_st # Verify API calls mock_requests.get.assert_called_once() - # Verify POST payload - test environment always gets passPercentage=100 regardless of auto_enable + # Verify POST payload mock_requests.post.assert_called_once_with( f'{STATSIG_API_BASE_URL}/gates', headers={ @@ -308,7 +308,7 @@ def test_upsert_flag_create_new_in_test_environment(self, mock_requests, mock_st } ], 'environments': ['development'], - 'passPercentage': 100, # Always 100 for test environment + 'passPercentage': 100, # Always 100 if auto_enable is true } ], }, @@ -490,7 +490,7 @@ def test_upsert_flag_prod_environment_auto_enable_true_no_existing_flag(self, mo def test_upsert_flag_beta_environment_auto_enable_false_no_existing_rule_create_rule( self, mock_requests, mock_statsig ): - """Test upsert in prod environment with autoEnable=True and no existing flag""" + """Test upsert in prod environment with autoEnable=True and no existing flag creates beta rule""" self._setup_mock_statsig(mock_statsig) existing_flag = { @@ -1018,7 +1018,7 @@ def test_delete_flag_api_error_on_patch_raises_exception(self, mock_requests, mo @patch('feature_flag_client.Statsig') @patch('feature_flag_client.requests') def test_upsert_flag_custom_attributes_as_string(self, mock_requests, mock_statsig): - """Test upsert_flag with custom attributes as string values - development environment always enabled""" + """Test upsert_flag with custom attributes as string values""" self._setup_mock_statsig(mock_statsig) # Mock GET request (flag doesn't exist) @@ -1037,7 +1037,7 @@ def test_upsert_flag_custom_attributes_as_string(self, mock_requests, mock_stats # Verify result self.assertEqual(result['id'], 'gate-string-attrs') - # Verify API calls - string values should be converted to lists, no conditions when auto_enable=False in test + # Verify API calls - string values should be converted to lists; conditions included even when auto_enable=False mock_requests.post.assert_called_once_with( f'{STATSIG_API_BASE_URL}/gates', headers={ @@ -1062,7 +1062,7 @@ def test_upsert_flag_custom_attributes_as_string(self, mock_requests, mock_stats {'type': 'custom_field', 'targetValue': ['new'], 'field': 'feature', 'operator': 'any'}, ], 'environments': ['development'], - 'passPercentage': 0, # 0 since auto_enabled is false + 'passPercentage': 0, # 0 since auto_enable is false } ], }, @@ -1091,7 +1091,7 @@ def test_upsert_flag_custom_attributes_as_list(self, mock_requests, mock_statsig # Verify result self.assertEqual(result['id'], 'gate-list-attrs') - # Verify API calls - list values preserved but no conditions when auto_enable=False + # Verify API calls - list values preserved; conditions included even when auto_enable=False mock_requests.post.assert_called_once_with( f'{STATSIG_API_BASE_URL}/gates', headers={ diff --git a/backend/compact-connect/stacks/feature_flag_stack/feature_flag_resource.py b/backend/compact-connect/stacks/feature_flag_stack/feature_flag_resource.py index 73840f7b3..54594f888 100644 --- a/backend/compact-connect/stacks/feature_flag_stack/feature_flag_resource.py +++ b/backend/compact-connect/stacks/feature_flag_stack/feature_flag_resource.py @@ -17,7 +17,7 @@ class FeatureFlagEnvironmentName(StrEnum): BETA = 'beta' PROD = 'prod' # add sandbox environment names here if needed - SANDBOX = 'sandbox' + SANDBOX = 'landon' class FeatureFlagResource(Construct): From ff360141bed36635e140dc58426cde713a5913ea Mon Sep 17 00:00:00 2001 From: Landon Shumway Date: Mon, 6 Oct 2025 12:31:45 -0500 Subject: [PATCH 41/55] comment cleanup --- .../python/feature-flag/tests/function/test_statsig_client.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/backend/compact-connect/lambdas/python/feature-flag/tests/function/test_statsig_client.py b/backend/compact-connect/lambdas/python/feature-flag/tests/function/test_statsig_client.py index 8c269067f..434e8a099 100644 --- a/backend/compact-connect/lambdas/python/feature-flag/tests/function/test_statsig_client.py +++ b/backend/compact-connect/lambdas/python/feature-flag/tests/function/test_statsig_client.py @@ -490,7 +490,7 @@ def test_upsert_flag_prod_environment_auto_enable_true_no_existing_flag(self, mo def test_upsert_flag_beta_environment_auto_enable_false_no_existing_rule_create_rule( self, mock_requests, mock_statsig ): - """Test upsert in prod environment with autoEnable=True and no existing flag creates beta rule""" + """Test upsert in beta environment with autoEnable=True and no existing flag creates beta rule""" self._setup_mock_statsig(mock_statsig) existing_flag = { @@ -1072,7 +1072,7 @@ def test_upsert_flag_custom_attributes_as_string(self, mock_requests, mock_stats @patch('feature_flag_client.Statsig') @patch('feature_flag_client.requests') def test_upsert_flag_custom_attributes_as_list(self, mock_requests, mock_statsig): - """Test upsert_flag with custom attributes as list values - no conditions for test when auto_enable=False""" + """Test upsert_flag with custom attributes as list values""" self._setup_mock_statsig(mock_statsig) # Mock GET request (flag doesn't exist) From e0b6f85ad3494ed59d7125f06f5d56bc22d6daae Mon Sep 17 00:00:00 2001 From: Landon Shumway Date: Mon, 6 Oct 2025 12:51:07 -0500 Subject: [PATCH 42/55] fix sandbox name --- .../stacks/feature_flag_stack/feature_flag_resource.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/compact-connect/stacks/feature_flag_stack/feature_flag_resource.py b/backend/compact-connect/stacks/feature_flag_stack/feature_flag_resource.py index 54594f888..73840f7b3 100644 --- a/backend/compact-connect/stacks/feature_flag_stack/feature_flag_resource.py +++ b/backend/compact-connect/stacks/feature_flag_stack/feature_flag_resource.py @@ -17,7 +17,7 @@ class FeatureFlagEnvironmentName(StrEnum): BETA = 'beta' PROD = 'prod' # add sandbox environment names here if needed - SANDBOX = 'landon' + SANDBOX = 'sandbox' class FeatureFlagResource(Construct): From e24d68e2a28a3a3e2d49d49fa161ed9d2b49607d Mon Sep 17 00:00:00 2001 From: Landon Shumway Date: Mon, 6 Oct 2025 15:07:57 -0500 Subject: [PATCH 43/55] PR feedback - add path param for flack check url --- .../common/cc_common/feature_flag_client.py | 4 +- .../tests/unit/test_feature_flag_client.py | 11 +++--- .../feature-flag/feature_flag_client.py | 12 +----- .../handlers/check_feature_flag.py | 13 +++++-- .../tests/function/test_check_feature_flag.py | 38 +++++++++++++++---- .../tests/function/test_statsig_client.py | 17 +-------- .../stacks/api_stack/v1_api/api_model.py | 7 ---- .../stacks/api_stack/v1_api/feature_flags.py | 5 ++- 8 files changed, 52 insertions(+), 55 deletions(-) diff --git a/backend/compact-connect/lambdas/python/common/cc_common/feature_flag_client.py b/backend/compact-connect/lambdas/python/common/cc_common/feature_flag_client.py index 11b239f12..23e3565f2 100644 --- a/backend/compact-connect/lambdas/python/common/cc_common/feature_flag_client.py +++ b/backend/compact-connect/lambdas/python/common/cc_common/feature_flag_client.py @@ -77,10 +77,10 @@ def is_feature_enabled(flag_name: str, context: FeatureFlagContext | None = None """ try: api_base_url = _get_api_base_url() - endpoint_url = f'{api_base_url}/v1/flags/check' + endpoint_url = f'{api_base_url}/v1/flags/{flag_name}/check' # Build request payload - payload = {'flagName': flag_name} + payload = {} if context: payload['context'] = context.to_dict() diff --git a/backend/compact-connect/lambdas/python/common/tests/unit/test_feature_flag_client.py b/backend/compact-connect/lambdas/python/common/tests/unit/test_feature_flag_client.py index 4089f64e0..d244cea68 100644 --- a/backend/compact-connect/lambdas/python/common/tests/unit/test_feature_flag_client.py +++ b/backend/compact-connect/lambdas/python/common/tests/unit/test_feature_flag_client.py @@ -20,8 +20,8 @@ def test_is_feature_enabled_returns_true_when_flag_enabled(self): # Verify the API was called correctly mock_post.assert_called_once_with( - 'https://api.example.com/v1/flags/check', - json={'flagName': 'test-flag'}, + 'https://api.example.com/v1/flags/test-flag/check', + json={}, timeout=5, headers={'Content-Type': 'application/json'}, ) @@ -58,9 +58,8 @@ def test_is_feature_enabled_with_context(self): # Verify the API was called with the context mock_post.assert_called_once_with( - 'https://api.example.com/v1/flags/check', + 'https://api.example.com/v1/flags/test-flag/check', json={ - 'flagName': 'test-flag', 'context': {'userId': 'user123', 'customAttributes': {'licenseType': 'lpc'}}, }, timeout=5, @@ -228,8 +227,8 @@ def test_is_feature_enabled_with_context_user_id_only(self): # Verify the API was called with only userId in context mock_post.assert_called_once_with( - 'https://api.example.com/v1/flags/check', - json={'flagName': 'test-flag', 'context': {'userId': 'user789'}}, + 'https://api.example.com/v1/flags/test-flag/check', + json={'context': {'userId': 'user789'}}, timeout=5, headers={'Content-Type': 'application/json'}, ) diff --git a/backend/compact-connect/lambdas/python/feature-flag/feature_flag_client.py b/backend/compact-connect/lambdas/python/feature-flag/feature_flag_client.py index db4d8ac07..d8ee99555 100644 --- a/backend/compact-connect/lambdas/python/feature-flag/feature_flag_client.py +++ b/backend/compact-connect/lambdas/python/feature-flag/feature_flag_client.py @@ -32,16 +32,6 @@ class FeatureFlagResult: metadata: dict[str, Any] | None = None -class BaseFeatureFlagCheckRequestSchema(Schema): - """ - Base schema for feature flag check requests. - - All provider-specific schemas should inherit from this base schema. - """ - - flagName = String(required=True, allow_none=False, validate=Length(1, 100)) # noqa: N815 - - class FeatureFlagClient(ABC): """ Abstract base class for feature flag clients. @@ -187,7 +177,7 @@ class StatSigContextSchema(Schema): customAttributes = DictField(required=False, allow_none=False, load_default=dict) -class StatSigFeatureFlagCheckRequestSchema(BaseFeatureFlagCheckRequestSchema): +class StatSigFeatureFlagCheckRequestSchema(Schema): """ StatSig-specific schema for feature flag check requests. diff --git a/backend/compact-connect/lambdas/python/feature-flag/handlers/check_feature_flag.py b/backend/compact-connect/lambdas/python/feature-flag/handlers/check_feature_flag.py index 699e95adf..27088e84b 100644 --- a/backend/compact-connect/lambdas/python/feature-flag/handlers/check_feature_flag.py +++ b/backend/compact-connect/lambdas/python/feature-flag/handlers/check_feature_flag.py @@ -20,6 +20,13 @@ def check_feature_flag(event: dict, context: LambdaContext): # noqa: ARG001 unu checking feature flags. """ try: + # Extract flagId from path parameters + path_parameters = event.get('pathParameters') or {} + flag_id = path_parameters.get('flagId') + + if not flag_id: + raise CCInvalidRequestException('flagId is required in the URL path') + # Parse and validate request body using client's validation try: body = json.loads(event['body']) @@ -28,13 +35,13 @@ def check_feature_flag(event: dict, context: LambdaContext): # noqa: ARG001 unu logger.warning('Feature flag validation failed', error=str(e)) raise CCInvalidRequestException(str(e)) from e - # Create request object for flag evaluation - flag_request = FeatureFlagRequest(**validated_body) + # Create request object for flag evaluation with flagId from path + flag_request = FeatureFlagRequest(flagName=flag_id, context=validated_body.get('context', {})) # Check the feature flag result = feature_flag_client.check_flag(flag_request) - logger.debug('Feature flag checked', flag_name=validated_body['flagName'], enabled=result.enabled) + logger.debug('Feature flag checked', flag_name=flag_id, enabled=result.enabled) # Return simple response with just the enabled status return {'enabled': result.enabled} diff --git a/backend/compact-connect/lambdas/python/feature-flag/tests/function/test_check_feature_flag.py b/backend/compact-connect/lambdas/python/feature-flag/tests/function/test_check_feature_flag.py index e14c3af0c..9b109514e 100644 --- a/backend/compact-connect/lambdas/python/feature-flag/tests/function/test_check_feature_flag.py +++ b/backend/compact-connect/lambdas/python/feature-flag/tests/function/test_check_feature_flag.py @@ -37,10 +37,11 @@ def tearDown(self): if 'handlers.check_feature_flag' in sys.modules: del sys.modules['handlers.check_feature_flag'] - def _generate_test_api_gateway_event(self, body: dict) -> dict: - """Generate a test API Gateway event""" + def _generate_test_api_gateway_event(self, body: dict, flag_id: str = 'test-flag') -> dict: + """Generate a test API Gateway event with flagId in path parameters""" event = self.test_data_generator.generate_test_api_event() event['body'] = json.dumps(body) + event['pathParameters'] = {'flagId': flag_id} return event @@ -64,10 +65,9 @@ def test_feature_flag_enabled_returns_true(self, mock_statsig): # Create test event test_body = { - 'flagName': 'test-feature-flag', 'context': {'userId': 'test-user-123', 'customAttributes': {'region': 'us-east-1'}}, } - event = self._generate_test_api_gateway_event(test_body) + event = self._generate_test_api_gateway_event(test_body, flag_id='test-feature-flag') # Call the handler result = check_feature_flag(event, self.mock_context) @@ -88,8 +88,8 @@ def test_feature_flag_disabled_returns_false(self, mock_statsig): from handlers.check_feature_flag import check_feature_flag # Create test event - test_body = {'flagName': 'disabled-feature-flag', 'context': {'userId': 'test-user-456'}} - event = self._generate_test_api_gateway_event(test_body) + test_body = {'context': {'userId': 'test-user-456'}} + event = self._generate_test_api_gateway_event(test_body, flag_id='disabled-feature-flag') # Call the handler result = check_feature_flag(event, self.mock_context) @@ -110,8 +110,8 @@ def test_feature_flag_with_minimal_context(self, mock_statsig): from handlers.check_feature_flag import check_feature_flag # Create test event with minimal context - test_body = {'flagName': 'minimal-test-flag', 'context': {}} - event = self._generate_test_api_gateway_event(test_body) + test_body = {'context': {}} + event = self._generate_test_api_gateway_event(test_body, flag_id='minimal-test-flag') # Call the handler result = check_feature_flag(event, self.mock_context) @@ -122,3 +122,25 @@ def test_feature_flag_with_minimal_context(self, mock_statsig): # Parse and verify the JSON body response_body = json.loads(result['body']) self.assertEqual({'enabled': True}, response_body) + + @patch('feature_flag_client.Statsig') + def test_missing_flag_id_returns_400(self, mock_statsig): + """Test that missing flagId in path parameters returns 400 error""" + self._setup_mock_statsig(mock_statsig, mock_flag_enabled_return=True) + from handlers.check_feature_flag import check_feature_flag + + # Create test event without flagId in path parameters + test_body = {'context': {'userId': 'test-user-123'}} + event = self._generate_test_api_gateway_event(test_body) + # Remove pathParameters to simulate missing flagId + event['pathParameters'] = None + + # Call the handler + result = check_feature_flag(event, self.mock_context) + + # Verify the API Gateway response format + self.assertEqual(result['statusCode'], 400) + + # Parse and verify the JSON body contains error message + response_body = json.loads(result['body']) + self.assertIn('flagId is required in the URL path', response_body['message']) diff --git a/backend/compact-connect/lambdas/python/feature-flag/tests/function/test_statsig_client.py b/backend/compact-connect/lambdas/python/feature-flag/tests/function/test_statsig_client.py index 434e8a099..0e8ce189e 100644 --- a/backend/compact-connect/lambdas/python/feature-flag/tests/function/test_statsig_client.py +++ b/backend/compact-connect/lambdas/python/feature-flag/tests/function/test_statsig_client.py @@ -70,7 +70,6 @@ def test_validate_request_success(self, mock_statsig): # Valid request data request_data = { - 'flagName': 'test-flag', 'context': {'userId': 'user123', 'customAttributes': {'region': 'us-east-1'}}, } @@ -86,27 +85,13 @@ def test_validate_request_minimal_data(self, mock_statsig): client = StatSigFeatureFlagClient(environment='test') # Minimal valid request data - request_data = {'flagName': 'test-flag'} + request_data = {} # Should validate successfully with defaults validated = client.validate_request(request_data) - self.assertEqual(validated['flagName'], 'test-flag') self.assertEqual(validated['context'], {}) # Default empty context - @patch('feature_flag_client.Statsig') - def test_validate_request_missing_flag_name(self, mock_statsig): - """Test request validation fails when flagName is missing""" - self._setup_mock_statsig(mock_statsig) - - client = StatSigFeatureFlagClient(environment='test') - - # Invalid request data - missing flagName - request_data = {'context': {'userId': 'user123'}} - - with self.assertRaises(FeatureFlagValidationException): - client.validate_request(request_data) - @patch('feature_flag_client.Statsig') def test_validate_request_invalid_flag_name(self, mock_statsig): """Test request validation fails when flagName is empty""" diff --git a/backend/compact-connect/stacks/api_stack/v1_api/api_model.py b/backend/compact-connect/stacks/api_stack/v1_api/api_model.py index 54d3ead35..792b7b00d 100644 --- a/backend/compact-connect/stacks/api_stack/v1_api/api_model.py +++ b/backend/compact-connect/stacks/api_stack/v1_api/api_model.py @@ -2716,14 +2716,7 @@ def check_feature_flag_request_model(self) -> Model: schema=JsonSchema( type=JsonSchemaType.OBJECT, additional_properties=False, - required=['flagName'], properties={ - 'flagName': JsonSchema( - type=JsonSchemaType.STRING, - description='The name of the feature flag to check', - min_length=1, - max_length=100, - ), 'context': JsonSchema( type=JsonSchemaType.OBJECT, description='Optional context for feature flag evaluation', diff --git a/backend/compact-connect/stacks/api_stack/v1_api/feature_flags.py b/backend/compact-connect/stacks/api_stack/v1_api/feature_flags.py index b494134fe..4db5985ff 100644 --- a/backend/compact-connect/stacks/api_stack/v1_api/feature_flags.py +++ b/backend/compact-connect/stacks/api_stack/v1_api/feature_flags.py @@ -24,8 +24,9 @@ def __init__( self.api: CCApi = resource.api self.api_model = api_model - # POST /v1/flags/check - check_resource = resource.add_resource('check') + # POST /v1/flags/{flagId}/check + flag_id_resource = resource.add_resource('{flagId}') + check_resource = flag_id_resource.add_resource('check') self.check_flag_method = check_resource.add_method( 'POST', integration=LambdaIntegration(api_lambda_stack.feature_flags_lambdas.check_feature_flag_function), From 1574222b76b7d764a28301a85ebdcf2bb806241a Mon Sep 17 00:00:00 2001 From: Landon Shumway Date: Mon, 6 Oct 2025 18:25:48 -0500 Subject: [PATCH 44/55] linter/formatter --- .../pipeline/frontend_pipeline.py | 24 +++++++------------ .../tests/app/test_pipeline.py | 10 ++++---- .../pipeline/backend_pipeline.py | 24 +++++++------------ .../stacks/api_lambda_stack/feature_flags.py | 2 +- .../stacks/api_stack/v1_api/feature_flags.py | 2 +- .../stacks/feature_flag_stack/__init__.py | 2 +- .../tests/app/test_pipeline.py | 11 +++++---- 7 files changed, 33 insertions(+), 42 deletions(-) diff --git a/backend/compact-connect-ui-app/pipeline/frontend_pipeline.py b/backend/compact-connect-ui-app/pipeline/frontend_pipeline.py index e87917a0c..059c9d315 100644 --- a/backend/compact-connect-ui-app/pipeline/frontend_pipeline.py +++ b/backend/compact-connect-ui-app/pipeline/frontend_pipeline.py @@ -233,13 +233,8 @@ def _add_codebuild_pipeline_role_override(self): account=stack.account, resource='log-group', resource_name=Fn.join( - '', - [ - '/aws/codebuild/', - Fn.ref(stack.get_logical_id(file_asset_node)), - ':*' - ] - ) , + '', ['/aws/codebuild/', Fn.ref(stack.get_logical_id(file_asset_node)), ':*'] + ), arn_format=ArnFormat.COLON_RESOURCE_NAME, ), ], @@ -262,17 +257,16 @@ def _add_codebuild_pipeline_role_override(self): ), ], ) - ) pipeline_role.add_to_principal_policy( PolicyStatement( effect=Effect.ALLOW, actions=[ - "codebuild:BatchPutCodeCoverages", - "codebuild:BatchPutTestCases", - "codebuild:CreateReport", - "codebuild:CreateReportGroup", - "codebuild:UpdateReport" + 'codebuild:BatchPutCodeCoverages', + 'codebuild:BatchPutTestCases', + 'codebuild:CreateReport', + 'codebuild:CreateReportGroup', + 'codebuild:UpdateReport', ], resources=[ Fn.join( @@ -288,9 +282,9 @@ def _add_codebuild_pipeline_role_override(self): ), Fn.ref(stack.get_logical_id(file_asset_node)), '-*', - ] + ], ), - ] + ], ) ) diff --git a/backend/compact-connect-ui-app/tests/app/test_pipeline.py b/backend/compact-connect-ui-app/tests/app/test_pipeline.py index 824cd317f..c9c9f206a 100644 --- a/backend/compact-connect-ui-app/tests/app/test_pipeline.py +++ b/backend/compact-connect-ui-app/tests/app/test_pipeline.py @@ -89,10 +89,12 @@ def test_pipeline_role_trust_policies(self): [ { 'Effect': 'Allow', - 'Principal': {'Service': [ - 'codebuild.amazonaws.com', - 'codepipeline.amazonaws.com', - ]}, + 'Principal': { + 'Service': [ + 'codebuild.amazonaws.com', + 'codepipeline.amazonaws.com', + ] + }, 'Action': 'sts:AssumeRole', } ] diff --git a/backend/compact-connect/pipeline/backend_pipeline.py b/backend/compact-connect/pipeline/backend_pipeline.py index 229d8bcca..232095104 100644 --- a/backend/compact-connect/pipeline/backend_pipeline.py +++ b/backend/compact-connect/pipeline/backend_pipeline.py @@ -228,13 +228,8 @@ def _add_codebuild_pipeline_role_override(self): account=stack.account, resource='log-group', resource_name=Fn.join( - '', - [ - '/aws/codebuild/', - Fn.ref(stack.get_logical_id(file_asset_node)), - ':*' - ] - ) , + '', ['/aws/codebuild/', Fn.ref(stack.get_logical_id(file_asset_node)), ':*'] + ), arn_format=ArnFormat.COLON_RESOURCE_NAME, ), ], @@ -257,17 +252,16 @@ def _add_codebuild_pipeline_role_override(self): ), ], ) - ) pipeline_role.add_to_principal_policy( PolicyStatement( effect=Effect.ALLOW, actions=[ - "codebuild:BatchPutCodeCoverages", - "codebuild:BatchPutTestCases", - "codebuild:CreateReport", - "codebuild:CreateReportGroup", - "codebuild:UpdateReport" + 'codebuild:BatchPutCodeCoverages', + 'codebuild:BatchPutTestCases', + 'codebuild:CreateReport', + 'codebuild:CreateReportGroup', + 'codebuild:UpdateReport', ], resources=[ Fn.join( @@ -283,9 +277,9 @@ def _add_codebuild_pipeline_role_override(self): ), Fn.ref(stack.get_logical_id(file_asset_node)), '-*', - ] + ], ), - ] + ], ) ) diff --git a/backend/compact-connect/stacks/api_lambda_stack/feature_flags.py b/backend/compact-connect/stacks/api_lambda_stack/feature_flags.py index 2acb4d096..0d19630ca 100644 --- a/backend/compact-connect/stacks/api_lambda_stack/feature_flags.py +++ b/backend/compact-connect/stacks/api_lambda_stack/feature_flags.py @@ -4,9 +4,9 @@ from aws_cdk.aws_secretsmanager import Secret from cdk_nag import NagSuppressions -from common_constructs.python_function import PythonFunction from common_constructs.stack import Stack +from common_constructs.python_function import PythonFunction from stacks import persistent_stack as ps diff --git a/backend/compact-connect/stacks/api_stack/v1_api/feature_flags.py b/backend/compact-connect/stacks/api_stack/v1_api/feature_flags.py index 4db5985ff..b22937582 100644 --- a/backend/compact-connect/stacks/api_stack/v1_api/feature_flags.py +++ b/backend/compact-connect/stacks/api_stack/v1_api/feature_flags.py @@ -2,8 +2,8 @@ from aws_cdk.aws_apigateway import LambdaIntegration, Resource from cdk_nag import NagSuppressions -from common_constructs.cc_api import CCApi +from common_constructs.cc_api import CCApi from stacks.api_lambda_stack import ApiLambdaStack from .api_model import ApiModel diff --git a/backend/compact-connect/stacks/feature_flag_stack/__init__.py b/backend/compact-connect/stacks/feature_flag_stack/__init__.py index 62afde120..6b2dffe54 100644 --- a/backend/compact-connect/stacks/feature_flag_stack/__init__.py +++ b/backend/compact-connect/stacks/feature_flag_stack/__init__.py @@ -74,10 +74,10 @@ from aws_cdk.aws_secretsmanager import Secret from aws_cdk.custom_resources import Provider from cdk_nag import NagSuppressions -from common_constructs.python_function import PythonFunction from common_constructs.stack import AppStack from constructs import Construct +from common_constructs.python_function import PythonFunction from stacks.feature_flag_stack.feature_flag_resource import FeatureFlagEnvironmentName, FeatureFlagResource diff --git a/backend/compact-connect/tests/app/test_pipeline.py b/backend/compact-connect/tests/app/test_pipeline.py index 5a927272d..97f406721 100644 --- a/backend/compact-connect/tests/app/test_pipeline.py +++ b/backend/compact-connect/tests/app/test_pipeline.py @@ -363,7 +363,6 @@ def test_predictable_pipeline_role_names_created(self): f'Should have exactly one backend pipeline role with name {expected_role_name}', ) - def test_pipeline_role_trust_policies(self): """Test that pipeline roles have correct trust policies for CodePipeline service.""" # Test that all pipeline roles trust the CodePipeline service @@ -388,10 +387,12 @@ def test_pipeline_role_trust_policies(self): [ { 'Effect': 'Allow', - 'Principal': {'Service': [ - 'codebuild.amazonaws.com', - 'codepipeline.amazonaws.com', - ]}, + 'Principal': { + 'Service': [ + 'codebuild.amazonaws.com', + 'codepipeline.amazonaws.com', + ] + }, 'Action': 'sts:AssumeRole', } ] From 8d57bbdcaaa02b09c58097aea551aa3ad9c27e0b Mon Sep 17 00:00:00 2001 From: Landon Shumway Date: Mon, 6 Oct 2025 20:37:32 -0500 Subject: [PATCH 45/55] PR feedback --- .../lambdas/python/feature-flag/feature_flag_client.py | 4 ++-- .../python/feature-flag/handlers/check_feature_flag.py | 2 +- .../python/feature-flag/tests/function/test_statsig_client.py | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/backend/compact-connect/lambdas/python/feature-flag/feature_flag_client.py b/backend/compact-connect/lambdas/python/feature-flag/feature_flag_client.py index d8ee99555..88b786d61 100644 --- a/backend/compact-connect/lambdas/python/feature-flag/feature_flag_client.py +++ b/backend/compact-connect/lambdas/python/feature-flag/feature_flag_client.py @@ -342,11 +342,11 @@ def upsert_flag( Each environment has its own rule (e.g., 'test-rule', 'beta-rule', 'prod-rule'). - If auto_enable is False: passPercentage is set to 0 (disabled) - - If auto_enable is True: passPercentage is set to 100 (enabled) and custom attributes are applied + - If auto_enable is True: passPercentage is set to 100 (enabled) :param flag_name: Name of the feature gate :param auto_enable: If True, enable the flag (passPercentage=100); if False, disable it (passPercentage=0) - :param custom_attributes: Optional custom attributes for targeting (only applied if auto_enable=True) + :param custom_attributes: Optional custom attributes for targeting :return: Flag data (with 'id' field) :raises FeatureFlagException: If operation fails """ diff --git a/backend/compact-connect/lambdas/python/feature-flag/handlers/check_feature_flag.py b/backend/compact-connect/lambdas/python/feature-flag/handlers/check_feature_flag.py index 27088e84b..36dfacf0a 100644 --- a/backend/compact-connect/lambdas/python/feature-flag/handlers/check_feature_flag.py +++ b/backend/compact-connect/lambdas/python/feature-flag/handlers/check_feature_flag.py @@ -29,7 +29,7 @@ def check_feature_flag(event: dict, context: LambdaContext): # noqa: ARG001 unu # Parse and validate request body using client's validation try: - body = json.loads(event['body']) + body = json.loads(event['body'] or '{}') validated_body = feature_flag_client.validate_request(body) except FeatureFlagValidationException as e: logger.warning('Feature flag validation failed', error=str(e)) diff --git a/backend/compact-connect/lambdas/python/feature-flag/tests/function/test_statsig_client.py b/backend/compact-connect/lambdas/python/feature-flag/tests/function/test_statsig_client.py index 0e8ce189e..4e03cb2f3 100644 --- a/backend/compact-connect/lambdas/python/feature-flag/tests/function/test_statsig_client.py +++ b/backend/compact-connect/lambdas/python/feature-flag/tests/function/test_statsig_client.py @@ -475,7 +475,7 @@ def test_upsert_flag_prod_environment_auto_enable_true_no_existing_flag(self, mo def test_upsert_flag_beta_environment_auto_enable_false_no_existing_rule_create_rule( self, mock_requests, mock_statsig ): - """Test upsert in beta environment with autoEnable=True and no existing flag creates beta rule""" + """Test upsert in beta environment with autoEnable=false and no existing flag creates beta rule""" self._setup_mock_statsig(mock_statsig) existing_flag = { From 12dec5a4cd90f4b9222d51c1574b5adaed9fa9e1 Mon Sep 17 00:00:00 2001 From: Landon Shumway Date: Mon, 6 Oct 2025 21:10:06 -0500 Subject: [PATCH 46/55] Handle invalid JSON request bodies --- .../handlers/check_feature_flag.py | 5 ++++- .../tests/function/test_check_feature_flag.py | 20 +++++++++++++++++++ 2 files changed, 24 insertions(+), 1 deletion(-) diff --git a/backend/compact-connect/lambdas/python/feature-flag/handlers/check_feature_flag.py b/backend/compact-connect/lambdas/python/feature-flag/handlers/check_feature_flag.py index 36dfacf0a..8521443c9 100644 --- a/backend/compact-connect/lambdas/python/feature-flag/handlers/check_feature_flag.py +++ b/backend/compact-connect/lambdas/python/feature-flag/handlers/check_feature_flag.py @@ -31,8 +31,11 @@ def check_feature_flag(event: dict, context: LambdaContext): # noqa: ARG001 unu try: body = json.loads(event['body'] or '{}') validated_body = feature_flag_client.validate_request(body) + except json.JSONDecodeError as e: + logger.info('Request body is invalid json', error=str(e)) + raise CCInvalidRequestException(str(e)) from e except FeatureFlagValidationException as e: - logger.warning('Feature flag validation failed', error=str(e)) + logger.info('Feature flag validation failed', error=str(e)) raise CCInvalidRequestException(str(e)) from e # Create request object for flag evaluation with flagId from path diff --git a/backend/compact-connect/lambdas/python/feature-flag/tests/function/test_check_feature_flag.py b/backend/compact-connect/lambdas/python/feature-flag/tests/function/test_check_feature_flag.py index 9b109514e..eb6d235e5 100644 --- a/backend/compact-connect/lambdas/python/feature-flag/tests/function/test_check_feature_flag.py +++ b/backend/compact-connect/lambdas/python/feature-flag/tests/function/test_check_feature_flag.py @@ -144,3 +144,23 @@ def test_missing_flag_id_returns_400(self, mock_statsig): # Parse and verify the JSON body contains error message response_body = json.loads(result['body']) self.assertIn('flagId is required in the URL path', response_body['message']) + + @patch('feature_flag_client.Statsig') + def test_invalid_json_request_body_returns_400(self, mock_statsig): + """Test that missing flagId in path parameters returns 400 error""" + self._setup_mock_statsig(mock_statsig, mock_flag_enabled_return=True) + from handlers.check_feature_flag import check_feature_flag + + event = self._generate_test_api_gateway_event(body={}, flag_id='test-flag') + # Create test event with invalid json + event['body'] = 'invalid' + + # Call the handler + result = check_feature_flag(event, self.mock_context) + + # Verify the API Gateway response format + self.assertEqual(result['statusCode'], 400) + + # Parse and verify the JSON body contains error message + response_body = json.loads(result['body']) + self.assertIn('Expecting value: line 1 column 1 (char 0)', response_body['message']) From 861f9804ace8a18eeaa6a84e4345295871314cae Mon Sep 17 00:00:00 2001 From: Landon Shumway Date: Tue, 7 Oct 2025 09:10:00 -0500 Subject: [PATCH 47/55] Update API spec to latest --- .../api-specification/latest-oas30.json | 9312 +++++++++-------- .../internal/postman/postman-collection.json | 409 +- 2 files changed, 4963 insertions(+), 4758 deletions(-) diff --git a/backend/compact-connect/docs/internal/api-specification/latest-oas30.json b/backend/compact-connect/docs/internal/api-specification/latest-oas30.json index 72ba117c3..b7d2ad901 100644 --- a/backend/compact-connect/docs/internal/api-specification/latest-oas30.json +++ b/backend/compact-connect/docs/internal/api-specification/latest-oas30.json @@ -2,7 +2,7 @@ "openapi": "3.0.1", "info": { "title": "LicenseApi", - "version": "2025-10-03T05:04:19Z" + "version": "2025-10-07T14:07:06Z" }, "servers": [ { @@ -36,7 +36,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/SandboLicenBxfAXNgCl0f1" + "$ref": "#/components/schemas/SandboLicenebLpX7xwG9up" } } } @@ -75,7 +75,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/SandboLicenrzUhY3WW0Y7L" + "$ref": "#/components/schemas/SandboLicenr3AcSGo022kV" } } }, @@ -87,7 +87,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/SandboLicengeanQqZ1NjKt" + "$ref": "#/components/schemas/SandboLicenZye1dOR8ybFD" } } } @@ -167,7 +167,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/SandboLicenHcuQxywUvhOh" + "$ref": "#/components/schemas/SandboLiceniL5ZkCnLFgry" } } } @@ -204,7 +204,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/SandboLicenLPepHoMLi1aj" + "$ref": "#/components/schemas/SandboLicenW5N7izWLhh77" } } }, @@ -216,7 +216,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/SandboLicenou5OKotlJ6Ez" + "$ref": "#/components/schemas/SandboLicenZezoNHumKMgB" } } } @@ -288,7 +288,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/SandboLicenHX8YNrOpPZbZ" + "$ref": "#/components/schemas/SandboLicen1ocd5zzucVYN" } } } @@ -339,7 +339,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/SandboLicenbGNGFVLD0EDE" + "$ref": "#/components/schemas/SandboLicenNPjSTFoXDCnt" } } } @@ -386,7 +386,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/SandboLicenTi9BrhAERjPp" + "$ref": "#/components/schemas/SandboLicen8ANzOyv4fxr5" } } }, @@ -398,7 +398,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/SandboLicengeanQqZ1NjKt" + "$ref": "#/components/schemas/SandboLicenZye1dOR8ybFD" } } } @@ -478,7 +478,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/SandboLicenMiDI81d6EYvq" + "$ref": "#/components/schemas/SandboLicenZuV3mbhTgVda" } } } @@ -488,7 +488,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/SandboLicengeanQqZ1NjKt" + "$ref": "#/components/schemas/SandboLicenZye1dOR8ybFD" } } } @@ -568,7 +568,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/SandboLicenJIK60DVCsApb" + "$ref": "#/components/schemas/SandboLicenro43WLdR3Nyo" } } } @@ -638,7 +638,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/SandboLicenYwiFMNF2Vu7Z" + "$ref": "#/components/schemas/SandboLicenRhrEcCIiPUoL" } } }, @@ -650,7 +650,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/SandboLiceno12FSfDWBzow" + "$ref": "#/components/schemas/SandboLicenY4MrNy3Nf4X3" } } } @@ -701,7 +701,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/SandboLicenr5TVmpxKfPGq" + "$ref": "#/components/schemas/SandboLicen6uzysMvBXwPR" } } } @@ -766,7 +766,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/SandboLicenEAecRKtirVZk" + "$ref": "#/components/schemas/SandboLicenwnTHxhykfOKL" } } }, @@ -778,7 +778,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/SandboLicengeanQqZ1NjKt" + "$ref": "#/components/schemas/SandboLicenZye1dOR8ybFD" } } } @@ -880,7 +880,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/SandboLicen3zolh21hPpCk" + "$ref": "#/components/schemas/SandboLicenMIkA7Wdy0MRl" } } }, @@ -892,7 +892,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/SandboLicengeanQqZ1NjKt" + "$ref": "#/components/schemas/SandboLicenZye1dOR8ybFD" } } } @@ -986,7 +986,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/SandboLicenPAsEF1Ia4s7g" + "$ref": "#/components/schemas/SandboLicen1qrFaB1TEABz" } } }, @@ -998,7 +998,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/SandboLicengeanQqZ1NjKt" + "$ref": "#/components/schemas/SandboLicenZye1dOR8ybFD" } } } @@ -1092,7 +1092,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/SandboLicen6bEbVSG2KkBx" + "$ref": "#/components/schemas/SandboLicen2ygiScVgBeUY" } } }, @@ -1104,7 +1104,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/SandboLicengeanQqZ1NjKt" + "$ref": "#/components/schemas/SandboLicenZye1dOR8ybFD" } } } @@ -1206,7 +1206,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/SandboLicenVZHLGnXN7APB" + "$ref": "#/components/schemas/SandboLicentVLkw57r45OM" } } }, @@ -1218,7 +1218,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/SandboLicengeanQqZ1NjKt" + "$ref": "#/components/schemas/SandboLicenZye1dOR8ybFD" } } } @@ -1314,7 +1314,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/SandboLicenobivPmYh5SvH" + "$ref": "#/components/schemas/SandboLicen5mI9RN9KTIVI" } } } @@ -1365,7 +1365,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/SandboLicen2x5yTuN7Vce5" + "$ref": "#/components/schemas/SandboLicenou25qiDo2Rmk" } } } @@ -1436,7 +1436,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/SandboLicenLLx7v2Sq2Nku" + "$ref": "#/components/schemas/SandboLicenYhXrtmBwiiII" } } } @@ -1496,7 +1496,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/SandboLicenDb8Dl04rAoPD" + "$ref": "#/components/schemas/SandboLicenkqjfNej7BZf5" } } }, @@ -1515,7 +1515,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/SandboLicen2VPoPHwDEcqq" + "$ref": "#/components/schemas/SandboLicenPOYfvpDMctYs" } } } @@ -1587,7 +1587,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/SandboLicengeanQqZ1NjKt" + "$ref": "#/components/schemas/SandboLicenZye1dOR8ybFD" } } } @@ -1604,7 +1604,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/SandboLicen2VPoPHwDEcqq" + "$ref": "#/components/schemas/SandboLicenPOYfvpDMctYs" } } } @@ -1674,7 +1674,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/SandboLicengeanQqZ1NjKt" + "$ref": "#/components/schemas/SandboLicenZye1dOR8ybFD" } } } @@ -1684,7 +1684,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/SandboLicengeanQqZ1NjKt" + "$ref": "#/components/schemas/SandboLicenZye1dOR8ybFD" } } } @@ -1752,7 +1752,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/SandboLicenC1PiOZAh5Usx" + "$ref": "#/components/schemas/SandboLicenPCBbBkJDJqKF" } } }, @@ -1764,7 +1764,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/SandboLicengeanQqZ1NjKt" + "$ref": "#/components/schemas/SandboLicenZye1dOR8ybFD" } } } @@ -1781,7 +1781,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/SandboLicen2VPoPHwDEcqq" + "$ref": "#/components/schemas/SandboLicenPOYfvpDMctYs" } } } @@ -1853,7 +1853,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/SandboLicengeanQqZ1NjKt" + "$ref": "#/components/schemas/SandboLicenZye1dOR8ybFD" } } } @@ -1863,7 +1863,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/SandboLicengeanQqZ1NjKt" + "$ref": "#/components/schemas/SandboLicenZye1dOR8ybFD" } } } @@ -1909,13 +1909,49 @@ ] } }, + "/v1/flags/{flagId}/check": { + "post": { + "parameters": [ + { + "name": "flagId", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SandboLicen3YKZmrc7RgkB" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "200 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SandboLicenQaBA3tB41KFn" + } + } + } + } + } + } + }, "/v1/provider-users/initiateRecovery": { "post": { "requestBody": { "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/SandboLicenpPZOnBEPSaJL" + "$ref": "#/components/schemas/SandboLicenRjihAxFfSYRz" } } }, @@ -1927,7 +1963,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/SandboLicengeanQqZ1NjKt" + "$ref": "#/components/schemas/SandboLicenZye1dOR8ybFD" } } } @@ -1953,7 +1989,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/SandboLicenr5TVmpxKfPGq" + "$ref": "#/components/schemas/SandboLicen6uzysMvBXwPR" } } } @@ -1982,7 +2018,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/SandboLicenaSmGZue6afyn" + "$ref": "#/components/schemas/SandboLicendX95fU3pWru0" } } }, @@ -1994,7 +2030,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/SandboLicengeanQqZ1NjKt" + "$ref": "#/components/schemas/SandboLicenZye1dOR8ybFD" } } } @@ -2023,7 +2059,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/SandboLicenbcsmAhiZJZ0Q" + "$ref": "#/components/schemas/SandboLicen7b3YUfyh3lip" } } }, @@ -2035,7 +2071,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/SandboLicengeanQqZ1NjKt" + "$ref": "#/components/schemas/SandboLicenZye1dOR8ybFD" } } } @@ -2064,7 +2100,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/SandboLicen8QcYRI7AzDyX" + "$ref": "#/components/schemas/SandboLicenCZ3tEgVMOUDm" } } }, @@ -2076,7 +2112,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/SandboLicengeanQqZ1NjKt" + "$ref": "#/components/schemas/SandboLicenZye1dOR8ybFD" } } } @@ -2123,7 +2159,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/SandboLicenobivPmYh5SvH" + "$ref": "#/components/schemas/SandboLicen5mI9RN9KTIVI" } } } @@ -2152,7 +2188,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/SandboLicenJZNKekBSUCl5" + "$ref": "#/components/schemas/SandboLicenbiPPccVxAAsP" } } }, @@ -2164,7 +2200,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/SandboLicenUzZJQKKk5goL" + "$ref": "#/components/schemas/SandboLicenr4NorsIuc9Tp" } } } @@ -2191,7 +2227,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/SandboLicendERazg9NeEsA" + "$ref": "#/components/schemas/SandboLicenqiznHQW2EhfU" } } }, @@ -2203,7 +2239,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/SandboLicengeanQqZ1NjKt" + "$ref": "#/components/schemas/SandboLicenZye1dOR8ybFD" } } } @@ -2222,7 +2258,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/SandboLicenwoqrSnm0rsGM" + "$ref": "#/components/schemas/SandboLicenEuYgjYKaxgIq" } } }, @@ -2234,7 +2270,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/SandboLicengeanQqZ1NjKt" + "$ref": "#/components/schemas/SandboLicenZye1dOR8ybFD" } } } @@ -2248,7 +2284,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/SandboLicenirtyanNQfXka" + "$ref": "#/components/schemas/SandboLicenleNKK47YnPrM" } } }, @@ -2260,7 +2296,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/SandboLicengeanQqZ1NjKt" + "$ref": "#/components/schemas/SandboLicenZye1dOR8ybFD" } } } @@ -2286,7 +2322,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/SandboLicenHX8YNrOpPZbZ" + "$ref": "#/components/schemas/SandboLicen1ocd5zzucVYN" } } } @@ -2310,7 +2346,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/SandboLicenYwiFMNF2Vu7Z" + "$ref": "#/components/schemas/SandboLicenRhrEcCIiPUoL" } } }, @@ -2322,7 +2358,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/SandboLicenBxXRobaYJSR9" + "$ref": "#/components/schemas/SandboLicenJ5jPsuhNIklP" } } } @@ -2356,7 +2392,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/SandboLicendF8CsBCXAU1x" + "$ref": "#/components/schemas/SandboLicenkk88e02FBvPL" } } } @@ -2406,7 +2442,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/SandboLicenobivPmYh5SvH" + "$ref": "#/components/schemas/SandboLicen5mI9RN9KTIVI" } } } @@ -2430,7 +2466,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/SandboLicenBWZgN83iyevY" + "$ref": "#/components/schemas/SandboLicen6PcW7zZXxI2G" } } }, @@ -2442,7 +2478,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/SandboLiceng72nqBB23bmL" + "$ref": "#/components/schemas/SandboLicen6tAp6Z6DYEWf" } } } @@ -2473,7 +2509,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/SandboLicenI2vqm7fRdSBi" + "$ref": "#/components/schemas/SandboLicenHifSlm4QMyok" } } } @@ -2494,7 +2530,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/SandboLicengeanQqZ1NjKt" + "$ref": "#/components/schemas/SandboLicenZye1dOR8ybFD" } } } @@ -2511,7 +2547,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/SandboLicen2VPoPHwDEcqq" + "$ref": "#/components/schemas/SandboLicenPOYfvpDMctYs" } } } @@ -2530,7 +2566,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/SandboLicenI4CqYMff5SWl" + "$ref": "#/components/schemas/SandboLicenOzDeYT20T683" } } }, @@ -2542,7 +2578,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/SandboLicengeanQqZ1NjKt" + "$ref": "#/components/schemas/SandboLicenZye1dOR8ybFD" } } } @@ -2559,7 +2595,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/SandboLicen2VPoPHwDEcqq" + "$ref": "#/components/schemas/SandboLicenPOYfvpDMctYs" } } } @@ -2577,457 +2613,513 @@ }, "components": { "schemas": { - "SandboLicen2x5yTuN7Vce5": { + "SandboLicenW5N7izWLhh77": { "required": [ - "ssn" - ], - "type": "object", - "properties": { - "ssn": { - "pattern": "^[0-9]{3}-[0-9]{2}-[0-9]{4}$", - "type": "string", - "description": "The provider's social security number" - } - } - }, - "SandboLicenbcsmAhiZJZ0Q": { - "required": [ - "verificationCode" + "apiLoginId", + "processor", + "transactionKey" ], "type": "object", "properties": { - "verificationCode": { - "pattern": "^[0-9]{4}$", + "apiLoginId": { + "maxLength": 100, + "minLength": 1, "type": "string", - "description": "4-digit verification code" - } - }, - "additionalProperties": false - }, - "SandboLicenJIK60DVCsApb": { - "required": [ - "upload" - ], - "type": "object", - "properties": { - "upload": { - "required": [ - "fields", - "url" - ], - "type": "object", - "properties": { - "fields": { - "type": "object", - "additionalProperties": { - "type": "string" - } - }, - "url": { - "type": "string" - } - } - } - } - }, - "SandboLicen6bEbVSG2KkBx": { - "required": [ - "clinicalPrivilegeActionCategory", - "encumbranceEffectiveDate", - "encumbranceType" - ], - "type": "object", - "properties": { - "encumbranceEffectiveDate": { - "pattern": "^[12]{1}[0-9]{3}-(0[1-9]|1[0-2])-(0[1-9]|[12][0-9]|3[01])$", + "description": "The api login id for the payment processor" + }, + "transactionKey": { + "maxLength": 100, + "minLength": 1, "type": "string", - "description": "The effective date of the encumbrance", - "format": "date" + "description": "The transaction key for the payment processor" }, - "encumbranceType": { + "processor": { "type": "string", - "description": "The type of encumbrance", + "description": "The type of payment processor", "enum": [ - "fine", - "reprimand", - "required supervision", - "completion of continuing education", - "public reprimand", - "probation", - "injunctive action", - "suspension", - "revocation", - "denial", - "surrender of license", - "modification of previous action-extension", - "modification of previous action-reduction", - "other monitoring", - "other adjudicated action not listed" + "authorize.net" ] - }, - "clinicalPrivilegeActionCategory": { - "type": "string", - "description": "The category of clinical privilege action" - } - }, - "additionalProperties": false - }, - "SandboLicenVZHLGnXN7APB": { - "required": [ - "effectiveLiftDate" - ], - "type": "object", - "properties": { - "effectiveLiftDate": { - "pattern": "^[12]{1}[0-9]{3}-(0[1-9]|1[0-2])-(0[1-9]|[12][0-9]|3[01])$", - "type": "string", - "description": "The effective date when the encumbrance will be lifted", - "format": "date" } }, "additionalProperties": false }, - "SandboLicenirtyanNQfXka": { + "SandboLicen5mI9RN9KTIVI": { "required": [ "compact", - "providerId", - "recaptchaToken", - "recoveryToken" + "events", + "jurisdiction", + "licenseType", + "privilegeId", + "providerId" ], "type": "object", "properties": { + "licenseType": { + "type": "string", + "enum": [ + "audiologist", + "speech-language pathologist", + "occupational therapist", + "occupational therapy assistant", + "licensed professional counselor" + ] + }, "compact": { "type": "string", - "description": "Compact abbreviation", "enum": [ "aslp", "octp", "coun" ] }, + "privilegeId": { + "type": "string" + }, "providerId": { "pattern": "[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab]{1}[0-9a-f]{3}-[0-9a-f]{12}", - "type": "string", - "description": "Provider UUID" + "type": "string" }, - "recaptchaToken": { - "minLength": 1, + "jurisdiction": { "type": "string", - "description": "ReCAPTCHA token for verification" + "enum": [ + "al", + "ak", + "az", + "ar", + "ca", + "co", + "ct", + "de", + "dc", + "fl", + "ga", + "hi", + "id", + "il", + "in", + "ia", + "ks", + "ky", + "la", + "me", + "md", + "ma", + "mi", + "mn", + "ms", + "mo", + "mt", + "ne", + "nv", + "nh", + "nj", + "nm", + "ny", + "nc", + "nd", + "oh", + "ok", + "or", + "pa", + "pr", + "ri", + "sc", + "sd", + "tn", + "tx", + "ut", + "vt", + "va", + "vi", + "wa", + "wv", + "wi", + "wy" + ] }, - "recoveryToken": { - "maxLength": 256, - "minLength": 1, - "type": "string", - "description": "Recovery token from the email link" - } - }, - "additionalProperties": false - }, - "SandboLicen2VPoPHwDEcqq": { - "required": [ - "attributes", - "permissions", - "status", - "userId" - ], - "type": "object", - "properties": { - "permissions": { - "type": "object", - "additionalProperties": { + "events": { + "type": "array", + "items": { + "required": [ + "createDate", + "dateOfUpdate", + "effectiveDate", + "type", + "updateType" + ], "type": "object", "properties": { - "actions": { - "type": "object", - "properties": { - "readPrivate": { - "type": "boolean" - }, - "admin": { - "type": "boolean" - }, - "readSSN": { - "type": "boolean" - } - } + "note": { + "type": "string" }, - "jurisdictions": { - "type": "object", - "additionalProperties": { - "type": "object", - "properties": { - "actions": { - "type": "object", - "properties": { - "readPrivate": { - "type": "boolean" - }, - "admin": { - "type": "boolean" - }, - "write": { - "type": "boolean" - }, - "readSSN": { - "type": "boolean" - } - }, - "additionalProperties": false - } - } - } + "type": { + "type": "string", + "enum": [ + "privilegeUpdate" + ] + }, + "dateOfUpdate": { + "pattern": "^[12]{1}[0-9]{3}-(0[1-9]|1[0-2])-(0[1-9]|[12][0-9]|3[01])$", + "type": "string", + "format": "date" + }, + "effectiveDate": { + "pattern": "^[12]{1}[0-9]{3}-(0[1-9]|1[0-2])-(0[1-9]|[12][0-9]|3[01])$", + "type": "string", + "format": "date" + }, + "updateType": { + "type": "string", + "enum": [ + "deactivation", + "expiration", + "issuance", + "other", + "renewal", + "encumbrance", + "homeJurisdictionChange", + "registration", + "lifting_encumbrance", + "licenseDeactivation", + "emailChange" + ] + }, + "createDate": { + "pattern": "^[12]{1}[0-9]{3}-(0[1-9]|1[0-2])-(0[1-9]|[12][0-9]|3[01])$", + "type": "string", + "format": "date" } - }, - "additionalProperties": false + } + } + } + } + }, + "SandboLicenebLpX7xwG9up": { + "required": [ + "compactAbbr", + "compactAdverseActionsNotificationEmails", + "compactCommissionFee", + "compactName", + "compactOperationsTeamEmails", + "compactSummaryReportNotificationEmails", + "configuredStates", + "licenseeRegistrationEnabled" + ], + "type": "object", + "properties": { + "configuredStates": { + "type": "array", + "description": "List of states that have submitted configurations and their live status", + "items": { + "required": [ + "isLive", + "postalAbbreviation" + ], + "type": "object", + "properties": { + "postalAbbreviation": { + "type": "string", + "description": "The postal abbreviation of the jurisdiction", + "enum": [ + "al", + "ak", + "az", + "ar", + "ca", + "co", + "ct", + "de", + "dc", + "fl", + "ga", + "hi", + "id", + "il", + "in", + "ia", + "ks", + "ky", + "la", + "me", + "md", + "ma", + "mi", + "mn", + "ms", + "mo", + "mt", + "ne", + "nv", + "nh", + "nj", + "nm", + "ny", + "nc", + "nd", + "oh", + "ok", + "or", + "pa", + "pr", + "ri", + "sc", + "sd", + "tn", + "tx", + "ut", + "vt", + "va", + "vi", + "wa", + "wv", + "wi", + "wy" + ] + }, + "isLive": { + "type": "boolean", + "description": "Whether the state is live and available for registrations." + } + } } }, - "attributes": { + "compactCommissionFee": { "required": [ - "email", - "familyName", - "givenName" + "feeAmount", + "feeType" ], "type": "object", "properties": { - "givenName": { - "maxLength": 100, - "minLength": 1, - "type": "string" - }, - "familyName": { - "maxLength": 100, - "minLength": 1, - "type": "string" + "feeAmount": { + "type": "number" }, - "email": { - "maxLength": 100, - "minLength": 5, - "type": "string" + "feeType": { + "type": "string", + "enum": [ + "FLAT_RATE" + ] } - }, - "additionalProperties": false - }, - "userId": { - "type": "string" - }, - "status": { - "type": "string", - "enum": [ - "active", - "inactive" - ] - } - }, - "additionalProperties": false - }, - "SandboLicenUzZJQKKk5goL": { - "required": [ - "affiliationType", - "dateOfUpdate", - "dateOfUpload", - "documentUploadFields", - "status" - ], - "type": "object", - "properties": { - "dateOfUpload": { - "pattern": "^[12]{1}[0-9]{3}-(0[1-9]|1[0-2])-(0[1-9]|[12][0-9]|3[01])$", - "type": "string", - "description": "The date the document was uploaded", - "format": "date" + } }, - "affiliationType": { - "type": "string", - "description": "The type of military affiliation", - "enum": [ - "militaryMember", - "militaryMemberSpouse" - ] + "compactSummaryReportNotificationEmails": { + "type": "array", + "description": "List of email addresses for summary report notifications", + "items": { + "type": "string", + "format": "email" + } }, - "fileNames": { + "compactAdverseActionsNotificationEmails": { "type": "array", - "description": "List of military affiliation file names", + "description": "List of email addresses for adverse actions notifications", "items": { "type": "string", - "description": "The name of the file being uploaded" + "format": "email" } }, - "dateOfUpdate": { - "pattern": "^[12]{1}[0-9]{3}-(0[1-9]|1[0-2])-(0[1-9]|[12][0-9]|3[01])$", - "type": "string", - "description": "The date the document was last updated", - "format": "date" + "licenseeRegistrationEnabled": { + "type": "boolean", + "description": "Denotes whether licensee registration is enabled" }, - "status": { + "compactAbbr": { "type": "string", - "description": "The status of the military affiliation" + "description": "The abbreviation of the compact" }, - "documentUploadFields": { - "type": "array", - "description": "The fields used to upload documents", - "items": { - "type": "object", - "properties": { - "fields": { - "type": "object", - "additionalProperties": { - "type": "string" + "transactionFeeConfiguration": { + "type": "object", + "properties": { + "licenseeCharges": { + "required": [ + "active", + "chargeAmount", + "chargeType" + ], + "type": "object", + "properties": { + "chargeType": { + "type": "string", + "description": "The type of transaction fee charge", + "enum": [ + "FLAT_FEE_PER_PRIVILEGE" + ] }, - "description": "The form fields used to upload the document" - }, - "url": { - "type": "string", - "description": "The url to upload the document to" + "active": { + "type": "boolean", + "description": "Whether the compact is charging licensees transaction fees" + }, + "chargeAmount": { + "type": "number", + "description": "The amount to charge per privilege purchased" + } } - }, - "description": "The fields used to upload a specific document" + } + } + }, + "compactName": { + "type": "string", + "description": "The full name of the compact" + }, + "compactOperationsTeamEmails": { + "type": "array", + "description": "List of email addresses for operations team notifications", + "items": { + "type": "string", + "format": "email" + } + } + } + }, + "SandboLicen1ocd5zzucVYN": { + "type": "array", + "items": { + "required": [ + "compact", + "jurisdictionName", + "postalAbbreviation" + ], + "type": "object", + "properties": { + "postalAbbreviation": { + "type": "string", + "description": "The postal abbreviation of the jurisdiction" + }, + "compact": { + "type": "string" + }, + "jurisdictionName": { + "type": "string", + "description": "The name of the jurisdiction" } } } }, - "SandboLicenMiDI81d6EYvq": { + "SandboLicenkqjfNej7BZf5": { + "required": [ + "attributes", + "permissions" + ], "type": "object", "properties": { - "message": { - "type": "string", - "description": "Message indicating success or failure" - }, - "errors": { + "permissions": { "type": "object", "additionalProperties": { "type": "object", - "additionalProperties": { - "type": "array", - "description": "List of error messages for a field", - "items": { - "type": "string" + "properties": { + "actions": { + "type": "object", + "properties": { + "readPrivate": { + "type": "boolean" + }, + "admin": { + "type": "boolean" + }, + "readSSN": { + "type": "boolean" + } + } + }, + "jurisdictions": { + "type": "object", + "additionalProperties": { + "type": "object", + "properties": { + "actions": { + "type": "object", + "properties": { + "readPrivate": { + "type": "boolean" + }, + "admin": { + "type": "boolean" + }, + "write": { + "type": "boolean" + }, + "readSSN": { + "type": "boolean" + } + }, + "additionalProperties": false + } + } + } } }, - "description": "Errors for a specific record" + "additionalProperties": false + } + }, + "attributes": { + "required": [ + "email", + "familyName", + "givenName" + ], + "type": "object", + "properties": { + "givenName": { + "maxLength": 100, + "minLength": 1, + "type": "string" + }, + "familyName": { + "maxLength": 100, + "minLength": 1, + "type": "string" + }, + "email": { + "maxLength": 100, + "minLength": 5, + "type": "string" + } }, - "description": "Validation errors by record index" + "additionalProperties": false } - } + }, + "additionalProperties": false }, - "SandboLicenYwiFMNF2Vu7Z": { + "SandboLicenY4MrNy3Nf4X3": { "required": [ - "query" + "pagination", + "providers" ], "type": "object", "properties": { "pagination": { "type": "object", "properties": { + "prevLastKey": { + "maxLength": 1024, + "minLength": 1, + "type": "object" + }, "lastKey": { "maxLength": 1024, "minLength": 1, - "type": "string" + "type": "object" }, "pageSize": { "maximum": 100, "minimum": 5, "type": "integer" } - }, - "additionalProperties": false + } }, - "query": { + "sorting": { + "required": [ + "key" + ], "type": "object", "properties": { - "providerId": { - "pattern": "[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab]{1}[0-9a-f]{3}-[0-9a-f]{12}", - "type": "string", - "description": "Internal UUID for the provider" - }, - "jurisdiction": { + "key": { "type": "string", - "description": "Filter for providers with privilege/license in a jurisdiction", + "description": "The key to sort results by", "enum": [ - "al", - "ak", - "az", - "ar", - "ca", - "co", - "ct", - "de", - "dc", - "fl", - "ga", - "hi", - "id", - "il", - "in", - "ia", - "ks", - "ky", - "la", - "me", - "md", - "ma", - "mi", - "mn", - "ms", - "mo", - "mt", - "ne", - "nv", - "nh", - "nj", - "nm", - "ny", - "nc", - "nd", - "oh", - "ok", - "or", - "pa", - "pr", - "ri", - "sc", - "sd", - "tn", - "tx", - "ut", - "vt", - "va", - "vi", - "wa", - "wv", - "wi", - "wy" + "dateOfUpdate", + "familyName" ] }, - "givenName": { - "maxLength": 100, - "type": "string", - "description": "Filter for providers with a given name (familyName is required if givenName is provided)" - }, - "familyName": { - "maxLength": 100, - "type": "string", - "description": "Filter for providers with a family name" - } - }, - "additionalProperties": false, - "description": "The query parameters" - }, - "sorting": { - "required": [ - "key" - ], - "type": "object", - "properties": { - "key": { - "type": "string", - "description": "The key to sort results by", - "enum": [ - "dateOfUpdate", - "familyName" - ] - }, - "direction": { + "direction": { "type": "string", "description": "Direction to sort results by", "enum": [ @@ -3037,407 +3129,130 @@ } }, "description": "How to sort results" - } - }, - "additionalProperties": false - }, - "SandboLicenHX8YNrOpPZbZ": { - "type": "array", - "items": { - "required": [ - "compact", - "jurisdictionName", - "postalAbbreviation" - ], - "type": "object", - "properties": { - "postalAbbreviation": { - "type": "string", - "description": "The postal abbreviation of the jurisdiction" - }, - "compact": { - "type": "string" - }, - "jurisdictionName": { - "type": "string", - "description": "The name of the jurisdiction" - } - } - } - }, - "SandboLicengeanQqZ1NjKt": { - "required": [ - "message" - ], - "type": "object", - "properties": { - "message": { - "type": "string", - "description": "A message about the request" - } - } - }, - "SandboLicenTi9BrhAERjPp": { - "required": [ - "jurisdictionAdverseActionsNotificationEmails", - "jurisdictionOperationsTeamEmails", - "jurisdictionSummaryReportNotificationEmails", - "jurisprudenceRequirements", - "licenseeRegistrationEnabled", - "privilegeFees" - ], - "type": "object", - "properties": { - "privilegeFees": { + }, + "providers": { + "maxLength": 100, "type": "array", - "description": "The fees for the privileges by license type", "items": { "required": [ - "amount", - "licenseTypeAbbreviation" + "birthMonthDay", + "compact", + "compactEligibility", + "dateOfExpiration", + "dateOfUpdate", + "familyName", + "givenName", + "jurisdictionUploadedCompactEligibility", + "jurisdictionUploadedLicenseStatus", + "licenseJurisdiction", + "licenseStatus", + "privilegeJurisdictions", + "providerId", + "type" ], "type": "object", "properties": { - "amount": { - "minimum": 0, - "type": "number" + "licenseJurisdiction": { + "type": "string", + "enum": [ + "al", + "ak", + "az", + "ar", + "ca", + "co", + "ct", + "de", + "dc", + "fl", + "ga", + "hi", + "id", + "il", + "in", + "ia", + "ks", + "ky", + "la", + "me", + "md", + "ma", + "mi", + "mn", + "ms", + "mo", + "mt", + "ne", + "nv", + "nh", + "nj", + "nm", + "ny", + "nc", + "nd", + "oh", + "ok", + "or", + "pa", + "pr", + "ri", + "sc", + "sd", + "tn", + "tx", + "ut", + "vt", + "va", + "vi", + "wa", + "wv", + "wi", + "wy" + ] }, - "militaryRate": { - "description": "Optional military rate for the privilege fee.", - "oneOf": [ - { - "minimum": 0, - "type": "number" - }, - null + "compact": { + "type": "string", + "enum": [ + "aslp", + "octp", + "coun" ] }, - "licenseTypeAbbreviation": { + "npi": { + "pattern": "^[0-9]{10}$", + "type": "string" + }, + "givenName": { + "maxLength": 100, + "minLength": 1, + "type": "string" + }, + "compactEligibility": { "type": "string", "enum": [ - "aud", - "slp", - "ot", - "ota", - "lpc" + "eligible", + "ineligible" ] - } - }, - "additionalProperties": false - } - }, - "jurisdictionAdverseActionsNotificationEmails": { - "maxItems": 10, - "minItems": 1, - "uniqueItems": true, - "type": "array", - "description": "List of email addresses for adverse actions notifications", - "items": { - "type": "string", - "format": "email" - } - }, - "jurisdictionOperationsTeamEmails": { - "maxItems": 10, - "minItems": 1, - "uniqueItems": true, - "type": "array", - "description": "List of email addresses for operations team notifications", - "items": { - "type": "string", - "format": "email" - } - }, - "jurisprudenceRequirements": { - "required": [ - "required" - ], - "type": "object", - "properties": { - "linkToDocumentation": { - "description": "Optional link to jurisprudence documentation", - "oneOf": [ - { - "type": "string" - }, - null - ] - }, - "required": { - "type": "boolean", - "description": "Whether jurisprudence requirements exist" - } - }, - "additionalProperties": false - }, - "licenseeRegistrationEnabled": { - "type": "boolean", - "description": "Denotes whether licensee registration is enabled" - }, - "jurisdictionSummaryReportNotificationEmails": { - "maxItems": 10, - "minItems": 1, - "uniqueItems": true, - "type": "array", - "description": "List of email addresses for summary report notifications", - "items": { - "type": "string", - "format": "email" - } - } - }, - "additionalProperties": false - }, - "SandboLicen3zolh21hPpCk": { - "required": [ - "effectiveLiftDate" - ], - "type": "object", - "properties": { - "effectiveLiftDate": { - "pattern": "^[12]{1}[0-9]{3}-(0[1-9]|1[0-2])-(0[1-9]|[12][0-9]|3[01])$", - "type": "string", - "description": "The effective date when the encumbrance will be lifted", - "format": "date" - } - }, - "additionalProperties": false - }, - "SandboLicenBxXRobaYJSR9": { - "required": [ - "pagination", - "providers" - ], - "type": "object", - "properties": { - "pagination": { - "type": "object", - "properties": { - "prevLastKey": { - "maxLength": 1024, - "minLength": 1, - "type": "object" - }, - "lastKey": { - "maxLength": 1024, - "minLength": 1, - "type": "object" - }, - "pageSize": { - "maximum": 100, - "minimum": 5, - "type": "integer" - } - } - }, - "query": { - "type": "object", - "properties": { - "providerId": { - "pattern": "[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab]{1}[0-9a-f]{3}-[0-9a-f]{12}", - "type": "string", - "description": "Internal UUID for the provider" - }, - "jurisdiction": { - "type": "string", - "description": "Filter for providers with privilege/license in a jurisdiction", - "enum": [ - "al", - "ak", - "az", - "ar", - "ca", - "co", - "ct", - "de", - "dc", - "fl", - "ga", - "hi", - "id", - "il", - "in", - "ia", - "ks", - "ky", - "la", - "me", - "md", - "ma", - "mi", - "mn", - "ms", - "mo", - "mt", - "ne", - "nv", - "nh", - "nj", - "nm", - "ny", - "nc", - "nd", - "oh", - "ok", - "or", - "pa", - "pr", - "ri", - "sc", - "sd", - "tn", - "tx", - "ut", - "vt", - "va", - "vi", - "wa", - "wv", - "wi", - "wy" - ] - }, - "givenName": { - "maxLength": 100, - "type": "string", - "description": "Filter for providers with a given name" - }, - "familyName": { - "maxLength": 100, - "type": "string", - "description": "Filter for providers with a family name" - } - } - }, - "sorting": { - "required": [ - "key" - ], - "type": "object", - "properties": { - "key": { - "type": "string", - "description": "The key to sort results by", - "enum": [ - "dateOfUpdate", - "familyName" - ] - }, - "direction": { - "type": "string", - "description": "Direction to sort results by", - "enum": [ - "ascending", - "descending" - ] - } - }, - "description": "How to sort results" - }, - "providers": { - "maxLength": 100, - "type": "array", - "items": { - "required": [ - "compact", - "familyName", - "givenName", - "licenseJurisdiction", - "privilegeJurisdictions", - "providerId", - "type" - ], - "type": "object", - "properties": { - "licenseJurisdiction": { + }, + "jurisdictionUploadedCompactEligibility": { "type": "string", "enum": [ - "al", - "ak", - "az", - "ar", - "ca", - "co", - "ct", - "de", - "dc", - "fl", - "ga", - "hi", - "id", - "il", - "in", - "ia", - "ks", - "ky", - "la", - "me", - "md", - "ma", - "mi", - "mn", - "ms", - "mo", - "mt", - "ne", - "nv", - "nh", - "nj", - "nm", - "ny", - "nc", - "nd", - "oh", - "ok", - "or", - "pa", - "pr", - "ri", - "sc", - "sd", - "tn", - "tx", - "ut", - "vt", - "va", - "vi", - "wa", - "wv", - "wi", - "wy" + "eligible", + "ineligible" ] }, - "compact": { + "dateOfBirth": { + "pattern": "^[12]{1}[0-9]{3}-(0[1-9]|1[0-2])-(0[1-9]|[12][0-9]|3[01])$", + "type": "string", + "format": "date" + }, + "jurisdictionUploadedLicenseStatus": { "type": "string", "enum": [ - "aslp", - "octp", - "coun" + "active", + "inactive" ] }, - "providerId": { - "pattern": "[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab]{1}[0-9a-f]{3}-[0-9a-f]{12}", - "type": "string" - }, - "npi": { - "pattern": "^[0-9]{10}$", - "type": "string" - }, - "givenName": { - "maxLength": 100, - "minLength": 1, - "type": "string" - }, - "familyName": { - "maxLength": 100, - "minLength": 1, - "type": "string" - }, - "middleName": { - "maxLength": 100, - "minLength": 1, - "type": "string" - }, "privilegeJurisdictions": { "type": "array", "items": { @@ -3571,44 +3386,99 @@ "unknown" ] }, - "dateOfUpdate": { + "ssnLastFour": { + "pattern": "^[0-9]{4}$", + "type": "string" + }, + "dateOfExpiration": { "pattern": "^[12]{1}[0-9]{3}-(0[1-9]|1[0-2])-(0[1-9]|[12][0-9]|3[01])$", "type": "string", "format": "date" - } - } - } - } - } - }, - "SandboLicenou5OKotlJ6Ez": { + }, + "providerId": { + "pattern": "[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab]{1}[0-9a-f]{3}-[0-9a-f]{12}", + "type": "string" + }, + "licenseStatus": { + "type": "string", + "enum": [ + "active", + "inactive" + ] + }, + "familyName": { + "maxLength": 100, + "minLength": 1, + "type": "string" + }, + "middleName": { + "maxLength": 100, + "minLength": 1, + "type": "string" + }, + "birthMonthDay": { + "pattern": "^[01]{1}[0-9]{1}-[0-3]{1}[0-9]{1}$", + "type": "string", + "format": "date" + }, + "compactConnectRegisteredEmailAddress": { + "maxLength": 100, + "minLength": 5, + "type": "string", + "format": "email" + }, + "dateOfUpdate": { + "pattern": "^[12]{1}[0-9]{3}-(0[1-9]|1[0-2])-(0[1-9]|[12][0-9]|3[01])$", + "type": "string", + "format": "date" + } + } + } + } + } + }, + "SandboLicenro43WLdR3Nyo": { "required": [ - "message" + "upload" ], "type": "object", "properties": { - "message": { - "type": "string", - "description": "A message about the request" + "upload": { + "required": [ + "fields", + "url" + ], + "type": "object", + "properties": { + "fields": { + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + "url": { + "type": "string" + } + } } } }, - "SandboLicenpPZOnBEPSaJL": { + "SandboLicenEuYgjYKaxgIq": { "required": [ "compact", "dob", + "email", "familyName", "givenName", "jurisdiction", "licenseType", "partialSocial", - "password", - "recaptchaToken", - "username" + "token" ], "type": "object", "properties": { "licenseType": { + "maxLength": 500, "type": "string", "description": "Type of license", "enum": [ @@ -3619,28 +3489,29 @@ "licensed professional counselor" ] }, - "password": { - "maxLength": 256, - "minLength": 12, - "type": "string", - "description": "Provider's current password" - }, "compact": { + "maxLength": 100, "type": "string", - "description": "Compact abbreviation", - "enum": [ - "aslp", - "octp", - "coun" - ] + "description": "Compact name" }, "dob": { "pattern": "^[12]{1}[0-9]{3}-(0[1-9]|1[0-2])-(0[1-9]|[12][0-9]|3[01])$", "type": "string", - "description": "Date of birth in YYYY-MM-DD format", - "format": "date" + "description": "Date of birth in YYYY-MM-DD format" + }, + "givenName": { + "maxLength": 200, + "type": "string", + "description": "Provider's given name" + }, + "familyName": { + "maxLength": 200, + "type": "string", + "description": "Provider's family name" }, "jurisdiction": { + "maxLength": 2, + "minLength": 2, "type": "string", "description": "Two-letter jurisdiction code", "enum": [ @@ -3699,1244 +3570,330 @@ "wy" ] }, - "givenName": { - "maxLength": 200, - "minLength": 1, - "type": "string", - "description": "Provider's given name" - }, - "familyName": { - "maxLength": 200, - "minLength": 1, - "type": "string", - "description": "Provider's family name" - }, - "recaptchaToken": { - "minLength": 1, - "type": "string", - "description": "ReCAPTCHA token for verification" - }, "partialSocial": { - "pattern": "^[0-9]{4}$", + "maxLength": 4, + "minLength": 4, "type": "string", "description": "Last 4 digits of SSN" }, - "username": { + "email": { "maxLength": 100, "minLength": 5, "type": "string", - "description": "Provider's email address (username)", + "description": "Provider's email address", "format": "email" + }, + "token": { + "type": "string", + "description": "ReCAPTCHA token" } - }, - "additionalProperties": false + } }, - "SandboLicenEAecRKtirVZk": { + "SandboLicentVLkw57r45OM": { "required": [ - "clinicalPrivilegeActionCategory", - "encumbranceEffectiveDate", - "encumbranceType" + "effectiveLiftDate" ], "type": "object", "properties": { - "encumbranceEffectiveDate": { + "effectiveLiftDate": { "pattern": "^[12]{1}[0-9]{3}-(0[1-9]|1[0-2])-(0[1-9]|[12][0-9]|3[01])$", "type": "string", - "description": "The effective date of the encumbrance", + "description": "The effective date when the encumbrance will be lifted", "format": "date" - }, - "encumbranceType": { - "type": "string", - "description": "The type of encumbrance", - "enum": [ - "fine", - "reprimand", - "required supervision", - "completion of continuing education", - "public reprimand", - "probation", - "injunctive action", - "suspension", - "revocation", - "denial", - "surrender of license", - "modification of previous action-extension", - "modification of previous action-reduction", - "other monitoring", - "other adjudicated action not listed" - ] - }, - "clinicalPrivilegeActionCategory": { - "type": "string", - "description": "The category of clinical privilege action" } }, "additionalProperties": false }, - "SandboLicendF8CsBCXAU1x": { + "SandboLicenRhrEcCIiPUoL": { "required": [ - "compact", - "dateOfUpdate", - "familyName", - "givenName", - "licenseJurisdiction", - "privilegeJurisdictions", - "providerId", - "type" + "query" ], "type": "object", "properties": { - "privileges": { - "type": "array", - "items": { - "required": [ - "administratorSetStatus", - "compact", - "dateOfExpiration", - "dateOfIssuance", - "dateOfRenewal", - "dateOfUpdate", - "jurisdiction", - "licenseJurisdiction", - "licenseType", - "privilegeId", - "providerId", - "status", - "type" - ], - "type": "object", - "properties": { - "licenseJurisdiction": { - "type": "string", - "enum": [ - "al", - "ak", - "az", - "ar", - "ca", - "co", - "ct", - "de", - "dc", - "fl", - "ga", - "hi", - "id", - "il", - "in", - "ia", - "ks", - "ky", - "la", - "me", - "md", - "ma", - "mi", - "mn", - "ms", - "mo", - "mt", - "ne", - "nv", - "nh", - "nj", - "nm", - "ny", - "nc", - "nd", - "oh", - "ok", - "or", - "pa", - "pr", - "ri", - "sc", - "sd", - "tn", - "tx", - "ut", - "vt", - "va", - "vi", - "wa", - "wv", - "wi", - "wy" - ] - }, - "compact": { - "type": "string", - "enum": [ - "aslp", - "octp", - "coun" - ] - }, - "jurisdiction": { - "type": "string", - "enum": [ - "al", - "ak", - "az", - "ar", - "ca", - "co", - "ct", - "de", - "dc", - "fl", - "ga", - "hi", - "id", - "il", - "in", - "ia", - "ks", - "ky", - "la", - "me", - "md", - "ma", - "mi", - "mn", - "ms", - "mo", - "mt", - "ne", - "nv", - "nh", - "nj", - "nm", - "ny", - "nc", - "nd", - "oh", - "ok", - "or", - "pa", - "pr", - "ri", - "sc", - "sd", - "tn", - "tx", - "ut", - "vt", - "va", - "vi", - "wa", - "wv", - "wi", - "wy" - ] + "pagination": { + "type": "object", + "properties": { + "lastKey": { + "maxLength": 1024, + "minLength": 1, + "type": "string" + }, + "pageSize": { + "maximum": 100, + "minimum": 5, + "type": "integer" + } + }, + "additionalProperties": false + }, + "query": { + "type": "object", + "properties": { + "providerId": { + "pattern": "[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab]{1}[0-9a-f]{3}-[0-9a-f]{12}", + "type": "string", + "description": "Internal UUID for the provider" + }, + "jurisdiction": { + "type": "string", + "description": "Filter for providers with privilege/license in a jurisdiction", + "enum": [ + "al", + "ak", + "az", + "ar", + "ca", + "co", + "ct", + "de", + "dc", + "fl", + "ga", + "hi", + "id", + "il", + "in", + "ia", + "ks", + "ky", + "la", + "me", + "md", + "ma", + "mi", + "mn", + "ms", + "mo", + "mt", + "ne", + "nv", + "nh", + "nj", + "nm", + "ny", + "nc", + "nd", + "oh", + "ok", + "or", + "pa", + "pr", + "ri", + "sc", + "sd", + "tn", + "tx", + "ut", + "vt", + "va", + "vi", + "wa", + "wv", + "wi", + "wy" + ] + }, + "givenName": { + "maxLength": 100, + "type": "string", + "description": "Filter for providers with a given name (familyName is required if givenName is provided)" + }, + "familyName": { + "maxLength": 100, + "type": "string", + "description": "Filter for providers with a family name" + } + }, + "additionalProperties": false, + "description": "The query parameters" + }, + "sorting": { + "required": [ + "key" + ], + "type": "object", + "properties": { + "key": { + "type": "string", + "description": "The key to sort results by", + "enum": [ + "dateOfUpdate", + "familyName" + ] + }, + "direction": { + "type": "string", + "description": "Direction to sort results by", + "enum": [ + "ascending", + "descending" + ] + } + }, + "description": "How to sort results" + } + }, + "additionalProperties": false + }, + "SandboLicen3YKZmrc7RgkB": { + "type": "object", + "properties": { + "context": { + "type": "object", + "properties": { + "userId": { + "maxLength": 100, + "minLength": 1, + "type": "string", + "description": "Optional user ID for feature flag evaluation" + }, + "customAttributes": { + "type": "object", + "additionalProperties": { + "type": "string" }, - "history": { - "type": "array", - "items": { - "required": [ - "compact", - "dateOfUpdate", - "jurisdiction", - "licenseType", - "previous", - "providerId", - "type", - "updateType", - "updatedValues" - ], - "type": "object", - "properties": { - "licenseType": { - "type": "string", - "enum": [ - "audiologist", - "speech-language pathologist", - "occupational therapist", - "occupational therapy assistant", - "licensed professional counselor" - ] - }, - "compact": { - "type": "string", - "enum": [ - "aslp", - "octp", - "coun" - ] - }, - "previous": { - "required": [ - "administratorSetStatus", - "dateOfExpiration", - "dateOfIssuance", - "dateOfRenewal", - "dateOfUpdate", - "licenseJurisdiction", - "privilegeId" - ], - "type": "object", - "properties": { - "administratorSetStatus": { - "type": "string", - "enum": [ - "active", - "inactive" - ] - }, - "dateOfExpiration": { - "pattern": "^[12]{1}[0-9]{3}-(0[1-9]|1[0-2])-(0[1-9]|[12][0-9]|3[01])$", - "type": "string", - "format": "date" - }, - "licenseJurisdiction": { - "type": "string", - "enum": [ - "al", - "ak", - "az", - "ar", - "ca", - "co", - "ct", - "de", - "dc", - "fl", - "ga", - "hi", - "id", - "il", - "in", - "ia", - "ks", - "ky", - "la", - "me", - "md", - "ma", - "mi", - "mn", - "ms", - "mo", - "mt", - "ne", - "nv", - "nh", - "nj", - "nm", - "ny", - "nc", - "nd", - "oh", - "ok", - "or", - "pa", - "pr", - "ri", - "sc", - "sd", - "tn", - "tx", - "ut", - "vt", - "va", - "vi", - "wa", - "wv", - "wi", - "wy" - ] - }, - "privilegeId": { - "type": "string" - }, - "dateOfRenewal": { - "pattern": "^[12]{1}[0-9]{3}-(0[1-9]|1[0-2])-(0[1-9]|[12][0-9]|3[01])$", - "type": "string", - "format": "date" - }, - "dateOfIssuance": { - "pattern": "^[12]{1}[0-9]{3}-(0[1-9]|1[0-2])-(0[1-9]|[12][0-9]|3[01])$", - "type": "string", - "format": "date" - }, - "dateOfUpdate": { - "pattern": "^[12]{1}[0-9]{3}-(0[1-9]|1[0-2])-(0[1-9]|[12][0-9]|3[01])$", - "type": "string", - "format": "date" - } - } - }, - "providerId": { - "pattern": "[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab]{1}[0-9a-f]{3}-[0-9a-f]{12}", - "type": "string" - }, - "jurisdiction": { - "type": "string", - "enum": [ - "al", - "ak", - "az", - "ar", - "ca", - "co", - "ct", - "de", - "dc", - "fl", - "ga", - "hi", - "id", - "il", - "in", - "ia", - "ks", - "ky", - "la", - "me", - "md", - "ma", - "mi", - "mn", - "ms", - "mo", - "mt", - "ne", - "nv", - "nh", - "nj", - "nm", - "ny", - "nc", - "nd", - "oh", - "ok", - "or", - "pa", - "pr", - "ri", - "sc", - "sd", - "tn", - "tx", - "ut", - "vt", - "va", - "vi", - "wa", - "wv", - "wi", - "wy" - ] - }, - "updatedValues": { - "type": "object", - "properties": { - "administratorSetStatus": { - "type": "string", - "enum": [ - "active", - "inactive" - ] - }, - "dateOfExpiration": { - "pattern": "^[12]{1}[0-9]{3}-(0[1-9]|1[0-2])-(0[1-9]|[12][0-9]|3[01])$", - "type": "string", - "format": "date" - }, - "licenseJurisdiction": { - "type": "string", - "enum": [ - "al", - "ak", - "az", - "ar", - "ca", - "co", - "ct", - "de", - "dc", - "fl", - "ga", - "hi", - "id", - "il", - "in", - "ia", - "ks", - "ky", - "la", - "me", - "md", - "ma", - "mi", - "mn", - "ms", - "mo", - "mt", - "ne", - "nv", - "nh", - "nj", - "nm", - "ny", - "nc", - "nd", - "oh", - "ok", - "or", - "pa", - "pr", - "ri", - "sc", - "sd", - "tn", - "tx", - "ut", - "vt", - "va", - "vi", - "wa", - "wv", - "wi", - "wy" - ] - }, - "privilegeId": { - "type": "string" - }, - "dateOfRenewal": { - "pattern": "^[12]{1}[0-9]{3}-(0[1-9]|1[0-2])-(0[1-9]|[12][0-9]|3[01])$", - "type": "string", - "format": "date" - }, - "dateOfIssuance": { - "pattern": "^[12]{1}[0-9]{3}-(0[1-9]|1[0-2])-(0[1-9]|[12][0-9]|3[01])$", - "type": "string", - "format": "date" - }, - "dateOfUpdate": { - "pattern": "^[12]{1}[0-9]{3}-(0[1-9]|1[0-2])-(0[1-9]|[12][0-9]|3[01])$", - "type": "string", - "format": "date" - } - } - }, - "type": { - "type": "string", - "enum": [ - "privilegeUpdate" - ] - }, - "dateOfUpdate": { - "pattern": "^[12]{1}[0-9]{3}-(0[1-9]|1[0-2])-(0[1-9]|[12][0-9]|3[01])$", - "type": "string", - "format": "date" - }, - "updateType": { - "type": "string", - "enum": [ - "deactivation", - "expiration", - "issuance", - "other", - "renewal", - "encumbrance", - "homeJurisdictionChange", - "registration", - "lifting_encumbrance", - "licenseDeactivation", - "emailChange" - ] - } - } - } - }, - "type": { - "type": "string", - "enum": [ - "privilege" - ] - }, - "dateOfIssuance": { - "pattern": "^[12]{1}[0-9]{3}-(0[1-9]|1[0-2])-(0[1-9]|[12][0-9]|3[01])$", - "type": "string", - "format": "date" - }, - "licenseType": { - "type": "string", - "enum": [ - "audiologist", - "speech-language pathologist", - "occupational therapist", - "occupational therapy assistant", - "licensed professional counselor" - ] - }, - "administratorSetStatus": { - "type": "string", - "enum": [ - "active", - "inactive" - ] - }, - "dateOfExpiration": { - "pattern": "^[12]{1}[0-9]{3}-(0[1-9]|1[0-2])-(0[1-9]|[12][0-9]|3[01])$", - "type": "string", - "format": "date" - }, - "privilegeId": { - "type": "string" - }, - "providerId": { - "pattern": "[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab]{1}[0-9a-f]{3}-[0-9a-f]{12}", - "type": "string" - }, - "dateOfRenewal": { - "pattern": "^[12]{1}[0-9]{3}-(0[1-9]|1[0-2])-(0[1-9]|[12][0-9]|3[01])$", - "type": "string", - "format": "date" - }, - "adverseActions": { - "type": "array", - "items": { - "required": [ - "actionAgainst", - "adverseActionId", - "compact", - "creationDate", - "dateOfUpdate", - "effectiveStartDate", - "jurisdiction", - "licenseType", - "licenseTypeAbbreviation", - "providerId", - "type" - ], - "type": "object", - "properties": { - "licenseType": { - "type": "string" - }, - "compact": { - "type": "string", - "enum": [ - "aslp", - "octp", - "coun" - ] - }, - "providerId": { - "pattern": "[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab]{1}[0-9a-f]{3}-[0-9a-f]{12}", - "type": "string" - }, - "jurisdiction": { - "type": "string", - "enum": [ - "al", - "ak", - "az", - "ar", - "ca", - "co", - "ct", - "de", - "dc", - "fl", - "ga", - "hi", - "id", - "il", - "in", - "ia", - "ks", - "ky", - "la", - "me", - "md", - "ma", - "mi", - "mn", - "ms", - "mo", - "mt", - "ne", - "nv", - "nh", - "nj", - "nm", - "ny", - "nc", - "nd", - "oh", - "ok", - "or", - "pa", - "pr", - "ri", - "sc", - "sd", - "tn", - "tx", - "ut", - "vt", - "va", - "vi", - "wa", - "wv", - "wi", - "wy" - ] - }, - "effectiveStartDate": { - "pattern": "^[12]{1}[0-9]{3}-(0[1-9]|1[0-2])-(0[1-9]|[12][0-9]|3[01])$", - "type": "string", - "format": "date" - }, - "licenseTypeAbbreviation": { - "type": "string" - }, - "adverseActionId": { - "type": "string" - }, - "effectiveLiftDate": { - "pattern": "^[12]{1}[0-9]{3}-(0[1-9]|1[0-2])-(0[1-9]|[12][0-9]|3[01])$", - "type": "string", - "format": "date" - }, - "type": { - "type": "string", - "enum": [ - "adverseAction" - ] - }, - "creationDate": { - "pattern": "^[12]{1}[0-9]{3}-(0[1-9]|1[0-2])-(0[1-9]|[12][0-9]|3[01])$", - "type": "string", - "format": "date" - }, - "actionAgainst": { - "type": "string" - }, - "dateOfUpdate": { - "pattern": "^[12]{1}[0-9]{3}-(0[1-9]|1[0-2])-(0[1-9]|[12][0-9]|3[01])$", - "type": "string", - "format": "date" - } - } - } - }, - "dateOfUpdate": { - "pattern": "^[12]{1}[0-9]{3}-(0[1-9]|1[0-2])-(0[1-9]|[12][0-9]|3[01])$", - "type": "string", - "format": "date" - }, - "status": { - "type": "string", - "enum": [ - "active", - "inactive" - ] - } - } - } - }, - "licenseJurisdiction": { - "type": "string", - "enum": [ - "al", - "ak", - "az", - "ar", - "ca", - "co", - "ct", - "de", - "dc", - "fl", - "ga", - "hi", - "id", - "il", - "in", - "ia", - "ks", - "ky", - "la", - "me", - "md", - "ma", - "mi", - "mn", - "ms", - "mo", - "mt", - "ne", - "nv", - "nh", - "nj", - "nm", - "ny", - "nc", - "nd", - "oh", - "ok", - "or", - "pa", - "pr", - "ri", - "sc", - "sd", - "tn", - "tx", - "ut", - "vt", - "va", - "vi", - "wa", - "wv", - "wi", - "wy" - ] - }, - "compact": { - "type": "string", - "enum": [ - "aslp", - "octp", - "coun" - ] - }, - "npi": { - "pattern": "^[0-9]{10}$", - "type": "string" - }, - "givenName": { - "maxLength": 100, - "minLength": 1, - "type": "string" - }, - "privilegeJurisdictions": { - "type": "array", - "items": { - "type": "string", - "enum": [ - "al", - "ak", - "az", - "ar", - "ca", - "co", - "ct", - "de", - "dc", - "fl", - "ga", - "hi", - "id", - "il", - "in", - "ia", - "ks", - "ky", - "la", - "me", - "md", - "ma", - "mi", - "mn", - "ms", - "mo", - "mt", - "ne", - "nv", - "nh", - "nj", - "nm", - "ny", - "nc", - "nd", - "oh", - "ok", - "or", - "pa", - "pr", - "ri", - "sc", - "sd", - "tn", - "tx", - "ut", - "vt", - "va", - "vi", - "wa", - "wv", - "wi", - "wy" - ] - } - }, - "type": { - "type": "string", - "enum": [ - "provider" - ] - }, - "suffix": { - "maxLength": 100, - "minLength": 1, - "type": "string" - }, - "currentHomeJurisdiction": { - "type": "string", - "description": "The current jurisdiction postal abbreviation if known.", - "enum": [ - "al", - "ak", - "az", - "ar", - "ca", - "co", - "ct", - "de", - "dc", - "fl", - "ga", - "hi", - "id", - "il", - "in", - "ia", - "ks", - "ky", - "la", - "me", - "md", - "ma", - "mi", - "mn", - "ms", - "mo", - "mt", - "ne", - "nv", - "nh", - "nj", - "nm", - "ny", - "nc", - "nd", - "oh", - "ok", - "or", - "pa", - "pr", - "ri", - "sc", - "sd", - "tn", - "tx", - "ut", - "vt", - "va", - "vi", - "wa", - "wv", - "wi", - "wy", - "other", - "unknown" - ] - }, - "providerId": { - "pattern": "[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab]{1}[0-9a-f]{3}-[0-9a-f]{12}", - "type": "string" - }, - "familyName": { - "maxLength": 100, - "minLength": 1, - "type": "string" - }, - "middleName": { - "maxLength": 100, - "minLength": 1, - "type": "string" - }, - "dateOfUpdate": { - "pattern": "^[12]{1}[0-9]{3}-(0[1-9]|1[0-2])-(0[1-9]|[12][0-9]|3[01])$", - "type": "string", - "format": "date" - } - } - }, - "SandboLicenI2vqm7fRdSBi": { - "required": [ - "items", - "pagination" - ], - "type": "object", - "properties": { - "pagination": { - "type": "object", - "properties": { - "prevLastKey": { - "maxLength": 1024, - "minLength": 1, - "type": "object" - }, - "lastKey": { - "maxLength": 1024, - "minLength": 1, - "type": "object" - }, - "pageSize": { - "maximum": 100, - "minimum": 5, - "type": "integer" - } - } - }, - "items": { - "maxLength": 100, - "type": "array", - "items": { - "type": "object", - "oneOf": [ - { - "required": [ - "compactAbbr", - "compactCommissionFee", - "compactName", - "isSandbox", - "paymentProcessorPublicFields", - "transactionFeeConfiguration", - "type" - ], - "type": "object", - "properties": { - "compactCommissionFee": { - "required": [ - "feeAmount", - "feeType" - ], - "type": "object", - "properties": { - "feeAmount": { - "type": "number" - }, - "feeType": { - "type": "string", - "enum": [ - "FLAT_RATE" - ] - } - } - }, - "compactAbbr": { - "type": "string", - "description": "The abbreviation of the compact" - }, - "paymentProcessorPublicFields": { - "required": [ - "apiLoginId", - "publicClientKey" - ], - "type": "object", - "properties": { - "publicClientKey": { - "type": "string", - "description": "The public client key for the payment processor" - }, - "apiLoginId": { - "type": "string", - "description": "The API login ID for the payment processor" - } - } - }, - "type": { - "type": "string", - "enum": [ - "compact" - ] - }, - "transactionFeeConfiguration": { - "required": [ - "licenseeCharges" - ], - "type": "object", - "properties": { - "licenseeCharges": { - "required": [ - "active", - "chargeAmount", - "chargeType" - ], - "type": "object", - "properties": { - "chargeType": { - "type": "string", - "description": "The type of transaction fee charge", - "enum": [ - "FLAT_FEE_PER_PRIVILEGE" - ] - }, - "active": { - "type": "boolean", - "description": "Whether the compact is charging licensees transaction fees" - }, - "chargeAmount": { - "type": "number", - "description": "The amount to charge per privilege purchased" - } - } - } - } - }, - "isSandbox": { - "type": "boolean", - "description": "Whether the compact is in sandbox mode" - }, - "compactName": { - "type": "string", - "description": "The full name of the compact" - } - } - }, - { - "required": [ - "jurisdictionName", - "jurisprudenceRequirements", - "postalAbbreviation", - "privilegeFees", - "type" - ], - "type": "object", - "properties": { - "privilegeFees": { - "type": "array", - "description": "The fees for the privileges", - "items": { - "required": [ - "amount", - "licenseTypeAbbreviation" - ], - "type": "object", - "properties": { - "amount": { - "type": "number" - }, - "militaryRate": { - "description": "Optional military rate for the privilege fee.", - "oneOf": [ - { - "minimum": 0, - "type": "number" - }, - null - ] - }, - "licenseTypeAbbreviation": { - "type": "string" - } - } - } - }, - "postalAbbreviation": { - "type": "string", - "description": "The postal abbreviation of the jurisdiction" - }, - "jurisprudenceRequirements": { - "required": [ - "required" - ], - "type": "object", - "properties": { - "linkToDocumentation": { - "description": "Optional link to jurisprudence documentation", - "oneOf": [ - { - "type": "string" - }, - null - ] - }, - "required": { - "type": "boolean", - "description": "Whether jurisprudence requirements exist" - } - } - }, - "jurisdictionName": { - "type": "string", - "description": "The name of the jurisdiction" - }, - "type": { - "type": "string", - "enum": [ - "jurisdiction" - ] - } - } - } - ] - } + "description": "Optional custom attributes for feature flag evaluation" + } + }, + "additionalProperties": false, + "description": "Optional context for feature flag evaluation" } - } + }, + "additionalProperties": false }, - "SandboLicenLPepHoMLi1aj": { + "SandboLicenCZ3tEgVMOUDm": { "required": [ - "apiLoginId", - "processor", - "transactionKey" + "jurisdiction" ], "type": "object", "properties": { - "apiLoginId": { - "maxLength": 100, - "minLength": 1, - "type": "string", - "description": "The api login id for the payment processor" - }, - "transactionKey": { - "maxLength": 100, - "minLength": 1, - "type": "string", - "description": "The transaction key for the payment processor" - }, - "processor": { + "jurisdiction": { "type": "string", - "description": "The type of payment processor", + "description": "The jurisdiction postal abbreviation to set as home jurisdiction", "enum": [ - "authorize.net" + "al", + "ak", + "az", + "ar", + "ca", + "co", + "ct", + "de", + "dc", + "fl", + "ga", + "hi", + "id", + "il", + "in", + "ia", + "ks", + "ky", + "la", + "me", + "md", + "ma", + "mi", + "mn", + "ms", + "mo", + "mt", + "ne", + "nv", + "nh", + "nj", + "nm", + "ny", + "nc", + "nd", + "oh", + "ok", + "or", + "pa", + "pr", + "ri", + "sc", + "sd", + "tn", + "tx", + "ut", + "vt", + "va", + "vi", + "wa", + "wv", + "wi", + "wy", + "other" ] } }, "additionalProperties": false }, - "SandboLicenrzUhY3WW0Y7L": { + "SandboLicen7b3YUfyh3lip": { "required": [ - "compactAdverseActionsNotificationEmails", - "compactCommissionFee", - "compactOperationsTeamEmails", - "compactSummaryReportNotificationEmails", - "configuredStates", - "licenseeRegistrationEnabled" + "verificationCode" ], "type": "object", "properties": { - "configuredStates": { + "verificationCode": { + "pattern": "^[0-9]{4}$", + "type": "string", + "description": "4-digit verification code" + } + }, + "additionalProperties": false + }, + "SandboLicenMIkA7Wdy0MRl": { + "required": [ + "effectiveLiftDate" + ], + "type": "object", + "properties": { + "effectiveLiftDate": { + "pattern": "^[12]{1}[0-9]{3}-(0[1-9]|1[0-2])-(0[1-9]|[12][0-9]|3[01])$", + "type": "string", + "description": "The effective date when the encumbrance will be lifted", + "format": "date" + } + }, + "additionalProperties": false + }, + "SandboLicenkk88e02FBvPL": { + "required": [ + "compact", + "dateOfUpdate", + "familyName", + "givenName", + "licenseJurisdiction", + "privilegeJurisdictions", + "providerId", + "type" + ], + "type": "object", + "properties": { + "privileges": { "type": "array", - "description": "List of states that have submitted configurations and their live status", "items": { "required": [ - "isLive", - "postalAbbreviation" + "administratorSetStatus", + "compact", + "dateOfExpiration", + "dateOfIssuance", + "dateOfRenewal", + "dateOfUpdate", + "jurisdiction", + "licenseJurisdiction", + "licenseType", + "privilegeId", + "providerId", + "status", + "type" ], "type": "object", "properties": { - "postalAbbreviation": { + "licenseJurisdiction": { "type": "string", - "description": "The postal abbreviation of the jurisdiction", "enum": [ "al", "ak", @@ -4993,232 +3950,571 @@ "wy" ] }, - "isLive": { - "type": "boolean", - "description": "Whether the state is live and available for registrations." - } - }, - "additionalProperties": false - } - }, - "compactCommissionFee": { - "required": [ - "feeAmount", - "feeType" - ], - "type": "object", - "properties": { - "feeAmount": { - "minimum": 0, - "type": "number" - }, - "feeType": { - "type": "string", - "enum": [ - "FLAT_RATE" - ] - } - }, - "additionalProperties": false - }, - "compactSummaryReportNotificationEmails": { - "maxItems": 10, - "minItems": 1, - "uniqueItems": true, - "type": "array", - "description": "List of email addresses for summary report notifications", - "items": { - "type": "string", - "format": "email" - } - }, - "compactAdverseActionsNotificationEmails": { - "maxItems": 10, - "minItems": 1, - "uniqueItems": true, - "type": "array", - "description": "List of email addresses for adverse actions notifications", - "items": { - "type": "string", - "format": "email" - } - }, - "licenseeRegistrationEnabled": { - "type": "boolean", - "description": "Denotes whether licensee registration is enabled" - }, - "transactionFeeConfiguration": { - "type": "object", - "properties": { - "licenseeCharges": { - "required": [ - "active", - "chargeAmount", - "chargeType" - ], - "type": "object", - "properties": { - "chargeType": { - "type": "string", - "description": "The type of transaction fee charge", - "enum": [ - "FLAT_FEE_PER_PRIVILEGE" - ] - }, - "active": { - "type": "boolean", - "description": "Whether the compact is charging licensees transaction fees" - }, - "chargeAmount": { - "minimum": 0, - "type": "number", - "description": "The amount to charge per privilege purchased" - } + "compact": { + "type": "string", + "enum": [ + "aslp", + "octp", + "coun" + ] + }, + "jurisdiction": { + "type": "string", + "enum": [ + "al", + "ak", + "az", + "ar", + "ca", + "co", + "ct", + "de", + "dc", + "fl", + "ga", + "hi", + "id", + "il", + "in", + "ia", + "ks", + "ky", + "la", + "me", + "md", + "ma", + "mi", + "mn", + "ms", + "mo", + "mt", + "ne", + "nv", + "nh", + "nj", + "nm", + "ny", + "nc", + "nd", + "oh", + "ok", + "or", + "pa", + "pr", + "ri", + "sc", + "sd", + "tn", + "tx", + "ut", + "vt", + "va", + "vi", + "wa", + "wv", + "wi", + "wy" + ] }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - "compactOperationsTeamEmails": { - "maxItems": 10, - "minItems": 1, - "uniqueItems": true, - "type": "array", - "description": "List of email addresses for operations team notifications", - "items": { - "type": "string", - "format": "email" - } - } - }, - "additionalProperties": false - }, - "SandboLicenPAsEF1Ia4s7g": { - "required": [ - "deactivationNote" - ], - "type": "object", - "properties": { - "deactivationNote": { - "maxLength": 256, - "type": "string", - "description": "Note describing why the privilege is being deactivated" - } - }, - "additionalProperties": false - }, - "SandboLicenLLx7v2Sq2Nku": { - "type": "object", - "properties": { - "pagination": { - "type": "object", - "properties": { - "prevLastKey": { - "maxLength": 1024, - "minLength": 1, - "type": "object" - }, - "lastKey": { - "maxLength": 1024, - "minLength": 1, - "type": "object" - }, - "pageSize": { - "maximum": 100, - "minimum": 5, - "type": "integer" - } - } - }, - "users": { - "type": "array", - "items": { - "required": [ - "attributes", - "permissions", - "status", - "userId" - ], - "type": "object", - "properties": { - "permissions": { - "type": "object", - "additionalProperties": { + "history": { + "type": "array", + "items": { + "required": [ + "compact", + "dateOfUpdate", + "jurisdiction", + "licenseType", + "previous", + "providerId", + "type", + "updateType", + "updatedValues" + ], "type": "object", "properties": { - "actions": { + "licenseType": { + "type": "string", + "enum": [ + "audiologist", + "speech-language pathologist", + "occupational therapist", + "occupational therapy assistant", + "licensed professional counselor" + ] + }, + "compact": { + "type": "string", + "enum": [ + "aslp", + "octp", + "coun" + ] + }, + "previous": { + "required": [ + "administratorSetStatus", + "dateOfExpiration", + "dateOfIssuance", + "dateOfRenewal", + "dateOfUpdate", + "licenseJurisdiction", + "privilegeId" + ], + "type": "object", + "properties": { + "administratorSetStatus": { + "type": "string", + "enum": [ + "active", + "inactive" + ] + }, + "dateOfExpiration": { + "pattern": "^[12]{1}[0-9]{3}-(0[1-9]|1[0-2])-(0[1-9]|[12][0-9]|3[01])$", + "type": "string", + "format": "date" + }, + "licenseJurisdiction": { + "type": "string", + "enum": [ + "al", + "ak", + "az", + "ar", + "ca", + "co", + "ct", + "de", + "dc", + "fl", + "ga", + "hi", + "id", + "il", + "in", + "ia", + "ks", + "ky", + "la", + "me", + "md", + "ma", + "mi", + "mn", + "ms", + "mo", + "mt", + "ne", + "nv", + "nh", + "nj", + "nm", + "ny", + "nc", + "nd", + "oh", + "ok", + "or", + "pa", + "pr", + "ri", + "sc", + "sd", + "tn", + "tx", + "ut", + "vt", + "va", + "vi", + "wa", + "wv", + "wi", + "wy" + ] + }, + "privilegeId": { + "type": "string" + }, + "dateOfRenewal": { + "pattern": "^[12]{1}[0-9]{3}-(0[1-9]|1[0-2])-(0[1-9]|[12][0-9]|3[01])$", + "type": "string", + "format": "date" + }, + "dateOfIssuance": { + "pattern": "^[12]{1}[0-9]{3}-(0[1-9]|1[0-2])-(0[1-9]|[12][0-9]|3[01])$", + "type": "string", + "format": "date" + }, + "dateOfUpdate": { + "pattern": "^[12]{1}[0-9]{3}-(0[1-9]|1[0-2])-(0[1-9]|[12][0-9]|3[01])$", + "type": "string", + "format": "date" + } + } + }, + "providerId": { + "pattern": "[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab]{1}[0-9a-f]{3}-[0-9a-f]{12}", + "type": "string" + }, + "jurisdiction": { + "type": "string", + "enum": [ + "al", + "ak", + "az", + "ar", + "ca", + "co", + "ct", + "de", + "dc", + "fl", + "ga", + "hi", + "id", + "il", + "in", + "ia", + "ks", + "ky", + "la", + "me", + "md", + "ma", + "mi", + "mn", + "ms", + "mo", + "mt", + "ne", + "nv", + "nh", + "nj", + "nm", + "ny", + "nc", + "nd", + "oh", + "ok", + "or", + "pa", + "pr", + "ri", + "sc", + "sd", + "tn", + "tx", + "ut", + "vt", + "va", + "vi", + "wa", + "wv", + "wi", + "wy" + ] + }, + "updatedValues": { "type": "object", "properties": { - "readPrivate": { - "type": "boolean" + "administratorSetStatus": { + "type": "string", + "enum": [ + "active", + "inactive" + ] }, - "admin": { - "type": "boolean" + "dateOfExpiration": { + "pattern": "^[12]{1}[0-9]{3}-(0[1-9]|1[0-2])-(0[1-9]|[12][0-9]|3[01])$", + "type": "string", + "format": "date" }, - "readSSN": { - "type": "boolean" + "licenseJurisdiction": { + "type": "string", + "enum": [ + "al", + "ak", + "az", + "ar", + "ca", + "co", + "ct", + "de", + "dc", + "fl", + "ga", + "hi", + "id", + "il", + "in", + "ia", + "ks", + "ky", + "la", + "me", + "md", + "ma", + "mi", + "mn", + "ms", + "mo", + "mt", + "ne", + "nv", + "nh", + "nj", + "nm", + "ny", + "nc", + "nd", + "oh", + "ok", + "or", + "pa", + "pr", + "ri", + "sc", + "sd", + "tn", + "tx", + "ut", + "vt", + "va", + "vi", + "wa", + "wv", + "wi", + "wy" + ] + }, + "privilegeId": { + "type": "string" + }, + "dateOfRenewal": { + "pattern": "^[12]{1}[0-9]{3}-(0[1-9]|1[0-2])-(0[1-9]|[12][0-9]|3[01])$", + "type": "string", + "format": "date" + }, + "dateOfIssuance": { + "pattern": "^[12]{1}[0-9]{3}-(0[1-9]|1[0-2])-(0[1-9]|[12][0-9]|3[01])$", + "type": "string", + "format": "date" + }, + "dateOfUpdate": { + "pattern": "^[12]{1}[0-9]{3}-(0[1-9]|1[0-2])-(0[1-9]|[12][0-9]|3[01])$", + "type": "string", + "format": "date" } } }, - "jurisdictions": { - "type": "object", - "additionalProperties": { - "type": "object", - "properties": { - "actions": { - "type": "object", - "properties": { - "readPrivate": { - "type": "boolean" - }, - "admin": { - "type": "boolean" - }, - "write": { - "type": "boolean" - }, - "readSSN": { - "type": "boolean" - } - }, - "additionalProperties": false - } - } - } + "type": { + "type": "string", + "enum": [ + "privilegeUpdate" + ] + }, + "dateOfUpdate": { + "pattern": "^[12]{1}[0-9]{3}-(0[1-9]|1[0-2])-(0[1-9]|[12][0-9]|3[01])$", + "type": "string", + "format": "date" + }, + "updateType": { + "type": "string", + "enum": [ + "deactivation", + "expiration", + "issuance", + "other", + "renewal", + "encumbrance", + "homeJurisdictionChange", + "registration", + "lifting_encumbrance", + "licenseDeactivation", + "emailChange" + ] + } + } + } + }, + "type": { + "type": "string", + "enum": [ + "privilege" + ] + }, + "dateOfIssuance": { + "pattern": "^[12]{1}[0-9]{3}-(0[1-9]|1[0-2])-(0[1-9]|[12][0-9]|3[01])$", + "type": "string", + "format": "date" + }, + "licenseType": { + "type": "string", + "enum": [ + "audiologist", + "speech-language pathologist", + "occupational therapist", + "occupational therapy assistant", + "licensed professional counselor" + ] + }, + "administratorSetStatus": { + "type": "string", + "enum": [ + "active", + "inactive" + ] + }, + "dateOfExpiration": { + "pattern": "^[12]{1}[0-9]{3}-(0[1-9]|1[0-2])-(0[1-9]|[12][0-9]|3[01])$", + "type": "string", + "format": "date" + }, + "privilegeId": { + "type": "string" + }, + "providerId": { + "pattern": "[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab]{1}[0-9a-f]{3}-[0-9a-f]{12}", + "type": "string" + }, + "dateOfRenewal": { + "pattern": "^[12]{1}[0-9]{3}-(0[1-9]|1[0-2])-(0[1-9]|[12][0-9]|3[01])$", + "type": "string", + "format": "date" + }, + "adverseActions": { + "type": "array", + "items": { + "required": [ + "actionAgainst", + "adverseActionId", + "compact", + "creationDate", + "dateOfUpdate", + "effectiveStartDate", + "jurisdiction", + "licenseType", + "licenseTypeAbbreviation", + "providerId", + "type" + ], + "type": "object", + "properties": { + "licenseType": { + "type": "string" + }, + "compact": { + "type": "string", + "enum": [ + "aslp", + "octp", + "coun" + ] + }, + "providerId": { + "pattern": "[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab]{1}[0-9a-f]{3}-[0-9a-f]{12}", + "type": "string" + }, + "jurisdiction": { + "type": "string", + "enum": [ + "al", + "ak", + "az", + "ar", + "ca", + "co", + "ct", + "de", + "dc", + "fl", + "ga", + "hi", + "id", + "il", + "in", + "ia", + "ks", + "ky", + "la", + "me", + "md", + "ma", + "mi", + "mn", + "ms", + "mo", + "mt", + "ne", + "nv", + "nh", + "nj", + "nm", + "ny", + "nc", + "nd", + "oh", + "ok", + "or", + "pa", + "pr", + "ri", + "sc", + "sd", + "tn", + "tx", + "ut", + "vt", + "va", + "vi", + "wa", + "wv", + "wi", + "wy" + ] + }, + "effectiveStartDate": { + "pattern": "^[12]{1}[0-9]{3}-(0[1-9]|1[0-2])-(0[1-9]|[12][0-9]|3[01])$", + "type": "string", + "format": "date" + }, + "licenseTypeAbbreviation": { + "type": "string" + }, + "adverseActionId": { + "type": "string" + }, + "effectiveLiftDate": { + "pattern": "^[12]{1}[0-9]{3}-(0[1-9]|1[0-2])-(0[1-9]|[12][0-9]|3[01])$", + "type": "string", + "format": "date" + }, + "type": { + "type": "string", + "enum": [ + "adverseAction" + ] + }, + "creationDate": { + "pattern": "^[12]{1}[0-9]{3}-(0[1-9]|1[0-2])-(0[1-9]|[12][0-9]|3[01])$", + "type": "string", + "format": "date" + }, + "actionAgainst": { + "type": "string" + }, + "dateOfUpdate": { + "pattern": "^[12]{1}[0-9]{3}-(0[1-9]|1[0-2])-(0[1-9]|[12][0-9]|3[01])$", + "type": "string", + "format": "date" } - }, - "additionalProperties": false - } - }, - "attributes": { - "required": [ - "email", - "familyName", - "givenName" - ], - "type": "object", - "properties": { - "givenName": { - "maxLength": 100, - "minLength": 1, - "type": "string" - }, - "familyName": { - "maxLength": 100, - "minLength": 1, - "type": "string" - }, - "email": { - "maxLength": 100, - "minLength": 5, - "type": "string" } - }, - "additionalProperties": false + } }, - "userId": { - "type": "string" + "dateOfUpdate": { + "pattern": "^[12]{1}[0-9]{3}-(0[1-9]|1[0-2])-(0[1-9]|[12][0-9]|3[01])$", + "type": "string", + "format": "date" }, "status": { "type": "string", @@ -5227,26 +4523,256 @@ "inactive" ] } - }, - "additionalProperties": false + } + } + }, + "licenseJurisdiction": { + "type": "string", + "enum": [ + "al", + "ak", + "az", + "ar", + "ca", + "co", + "ct", + "de", + "dc", + "fl", + "ga", + "hi", + "id", + "il", + "in", + "ia", + "ks", + "ky", + "la", + "me", + "md", + "ma", + "mi", + "mn", + "ms", + "mo", + "mt", + "ne", + "nv", + "nh", + "nj", + "nm", + "ny", + "nc", + "nd", + "oh", + "ok", + "or", + "pa", + "pr", + "ri", + "sc", + "sd", + "tn", + "tx", + "ut", + "vt", + "va", + "vi", + "wa", + "wv", + "wi", + "wy" + ] + }, + "compact": { + "type": "string", + "enum": [ + "aslp", + "octp", + "coun" + ] + }, + "npi": { + "pattern": "^[0-9]{10}$", + "type": "string" + }, + "givenName": { + "maxLength": 100, + "minLength": 1, + "type": "string" + }, + "privilegeJurisdictions": { + "type": "array", + "items": { + "type": "string", + "enum": [ + "al", + "ak", + "az", + "ar", + "ca", + "co", + "ct", + "de", + "dc", + "fl", + "ga", + "hi", + "id", + "il", + "in", + "ia", + "ks", + "ky", + "la", + "me", + "md", + "ma", + "mi", + "mn", + "ms", + "mo", + "mt", + "ne", + "nv", + "nh", + "nj", + "nm", + "ny", + "nc", + "nd", + "oh", + "ok", + "or", + "pa", + "pr", + "ri", + "sc", + "sd", + "tn", + "tx", + "ut", + "vt", + "va", + "vi", + "wa", + "wv", + "wi", + "wy" + ] } + }, + "type": { + "type": "string", + "enum": [ + "provider" + ] + }, + "suffix": { + "maxLength": 100, + "minLength": 1, + "type": "string" + }, + "currentHomeJurisdiction": { + "type": "string", + "description": "The current jurisdiction postal abbreviation if known.", + "enum": [ + "al", + "ak", + "az", + "ar", + "ca", + "co", + "ct", + "de", + "dc", + "fl", + "ga", + "hi", + "id", + "il", + "in", + "ia", + "ks", + "ky", + "la", + "me", + "md", + "ma", + "mi", + "mn", + "ms", + "mo", + "mt", + "ne", + "nv", + "nh", + "nj", + "nm", + "ny", + "nc", + "nd", + "oh", + "ok", + "or", + "pa", + "pr", + "ri", + "sc", + "sd", + "tn", + "tx", + "ut", + "vt", + "va", + "vi", + "wa", + "wv", + "wi", + "wy", + "other", + "unknown" + ] + }, + "providerId": { + "pattern": "[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab]{1}[0-9a-f]{3}-[0-9a-f]{12}", + "type": "string" + }, + "familyName": { + "maxLength": 100, + "minLength": 1, + "type": "string" + }, + "middleName": { + "maxLength": 100, + "minLength": 1, + "type": "string" + }, + "dateOfUpdate": { + "pattern": "^[12]{1}[0-9]{3}-(0[1-9]|1[0-2])-(0[1-9]|[12][0-9]|3[01])$", + "type": "string", + "format": "date" } - }, - "additionalProperties": false + } }, - "SandboLicenobivPmYh5SvH": { + "SandboLicenRjihAxFfSYRz": { "required": [ "compact", - "events", + "dob", + "familyName", + "givenName", "jurisdiction", "licenseType", - "privilegeId", - "providerId" + "partialSocial", + "password", + "recaptchaToken", + "username" ], "type": "object", "properties": { "licenseType": { "type": "string", + "description": "Type of license", "enum": [ "audiologist", "speech-language pathologist", @@ -5255,23 +4781,30 @@ "licensed professional counselor" ] }, + "password": { + "maxLength": 256, + "minLength": 12, + "type": "string", + "description": "Provider's current password" + }, "compact": { "type": "string", + "description": "Compact abbreviation", "enum": [ "aslp", "octp", "coun" ] }, - "privilegeId": { - "type": "string" - }, - "providerId": { - "pattern": "[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab]{1}[0-9a-f]{3}-[0-9a-f]{12}", - "type": "string" + "dob": { + "pattern": "^[12]{1}[0-9]{3}-(0[1-9]|1[0-2])-(0[1-9]|[12][0-9]|3[01])$", + "type": "string", + "description": "Date of birth in YYYY-MM-DD format", + "format": "date" }, "jurisdiction": { "type": "string", + "description": "Two-letter jurisdiction code", "enum": [ "al", "ak", @@ -5328,269 +4861,101 @@ "wy" ] }, - "events": { - "type": "array", - "items": { - "required": [ - "createDate", - "dateOfUpdate", - "effectiveDate", - "type", - "updateType" - ], - "type": "object", - "properties": { - "note": { - "type": "string" - }, - "type": { - "type": "string", - "enum": [ - "privilegeUpdate" - ] - }, - "dateOfUpdate": { - "pattern": "^[12]{1}[0-9]{3}-(0[1-9]|1[0-2])-(0[1-9]|[12][0-9]|3[01])$", - "type": "string", - "format": "date" - }, - "effectiveDate": { - "pattern": "^[12]{1}[0-9]{3}-(0[1-9]|1[0-2])-(0[1-9]|[12][0-9]|3[01])$", - "type": "string", - "format": "date" - }, - "updateType": { - "type": "string", - "enum": [ - "deactivation", - "expiration", - "issuance", - "other", - "renewal", - "encumbrance", - "homeJurisdictionChange", - "registration", - "lifting_encumbrance", - "licenseDeactivation", - "emailChange" - ] - }, - "createDate": { - "pattern": "^[12]{1}[0-9]{3}-(0[1-9]|1[0-2])-(0[1-9]|[12][0-9]|3[01])$", - "type": "string", - "format": "date" - } - } - } - } - } - }, - "SandboLicenI4CqYMff5SWl": { - "type": "object", - "properties": { - "attributes": { - "type": "object", - "properties": { - "givenName": { - "maxLength": 100, - "minLength": 1, - "type": "string" - }, - "familyName": { - "maxLength": 100, - "minLength": 1, - "type": "string" - } - }, - "additionalProperties": false + "givenName": { + "maxLength": 200, + "minLength": 1, + "type": "string", + "description": "Provider's given name" + }, + "familyName": { + "maxLength": 200, + "minLength": 1, + "type": "string", + "description": "Provider's family name" + }, + "recaptchaToken": { + "minLength": 1, + "type": "string", + "description": "ReCAPTCHA token for verification" + }, + "partialSocial": { + "pattern": "^[0-9]{4}$", + "type": "string", + "description": "Last 4 digits of SSN" + }, + "username": { + "maxLength": 100, + "minLength": 5, + "type": "string", + "description": "Provider's email address (username)", + "format": "email" } }, "additionalProperties": false }, - "SandboLiceng72nqBB23bmL": { + "SandboLicenZye1dOR8ybFD": { "required": [ - "transactionId" + "message" ], "type": "object", "properties": { "message": { "type": "string", - "description": "A message about the transaction" - }, - "transactionId": { - "type": "string", - "description": "The transaction id for the purchase" + "description": "A message about the request" } } }, - "SandboLicenJZNKekBSUCl5": { + "SandboLicendX95fU3pWru0": { "required": [ - "affiliationType", - "fileNames" + "newEmailAddress" ], "type": "object", "properties": { - "affiliationType": { + "newEmailAddress": { + "maxLength": 100, + "minLength": 5, "type": "string", - "description": "The type of military affiliation", - "enum": [ - "militaryMember", - "militaryMemberSpouse" - ] - }, - "fileNames": { - "type": "array", - "description": "List of military affiliation file names", - "items": { - "maxLength": 150, - "type": "string", - "description": "The name of the file being uploaded" - } + "description": "The new email address to set for the provider", + "format": "email" } }, "additionalProperties": false }, - "SandboLicenbGNGFVLD0EDE": { - "required": [ - "compact", - "jurisdictionAdverseActionsNotificationEmails", - "jurisdictionName", - "jurisdictionOperationsTeamEmails", - "jurisdictionSummaryReportNotificationEmails", - "jurisprudenceRequirements", - "licenseeRegistrationEnabled", - "postalAbbreviation", - "privilegeFees" - ], + "SandboLicenYhXrtmBwiiII": { "type": "object", "properties": { - "privilegeFees": { - "type": "array", - "description": "The fees for the privileges by license type", - "items": { - "required": [ - "amount", - "licenseTypeAbbreviation" - ], - "type": "object", - "properties": { - "amount": { - "type": "number" - }, - "militaryRate": { - "description": "Optional military rate for the privilege fee.", - "oneOf": [ - { - "minimum": 0, - "type": "number" - }, - null - ] - }, - "licenseTypeAbbreviation": { - "type": "string", - "enum": [ - "aud", - "slp", - "ot", - "ota", - "lpc" - ] - } - } - } - }, - "postalAbbreviation": { - "type": "string", - "description": "The postal abbreviation of the jurisdiction" - }, - "jurisdictionAdverseActionsNotificationEmails": { - "type": "array", - "description": "List of email addresses for adverse actions notifications", - "items": { - "type": "string", - "format": "email" - } - }, - "jurisdictionOperationsTeamEmails": { - "type": "array", - "description": "List of email addresses for operations team notifications", - "items": { - "type": "string", - "format": "email" - } - }, - "compact": { - "type": "string", - "description": "The compact this jurisdiction configuration belongs to", - "enum": [ - "aslp", - "octp", - "coun" - ] - }, - "jurisprudenceRequirements": { - "required": [ - "required" - ], + "pagination": { "type": "object", "properties": { - "linkToDocumentation": { - "description": "Optional link to jurisprudence documentation", - "oneOf": [ - { - "type": "string" - }, - null - ] + "prevLastKey": { + "maxLength": 1024, + "minLength": 1, + "type": "object" }, - "required": { - "type": "boolean", - "description": "Whether jurisprudence requirements exist" + "lastKey": { + "maxLength": 1024, + "minLength": 1, + "type": "object" + }, + "pageSize": { + "maximum": 100, + "minimum": 5, + "type": "integer" } } }, - "licenseeRegistrationEnabled": { - "type": "boolean", - "description": "Denotes whether licensee registration is enabled" - }, - "jurisdictionName": { - "type": "string", - "description": "The name of the jurisdiction" - }, - "jurisdictionSummaryReportNotificationEmails": { + "users": { "type": "array", - "description": "List of email addresses for summary report notifications", "items": { - "type": "string", - "format": "email" - } - } - } - }, - "SandboLicenC1PiOZAh5Usx": { - "type": "object", - "properties": { - "permissions": { - "type": "object", - "additionalProperties": { + "required": [ + "attributes", + "permissions", + "status", + "userId" + ], "type": "object", "properties": { - "actions": { - "type": "object", - "properties": { - "readPrivate": { - "type": "boolean" - }, - "admin": { - "type": "boolean" - }, - "readSSN": { - "type": "boolean" - } - } - }, - "jurisdictions": { + "permissions": { "type": "object", "additionalProperties": { "type": "object", @@ -5604,17 +4969,76 @@ "admin": { "type": "boolean" }, - "write": { - "type": "boolean" - }, "readSSN": { "type": "boolean" } - }, - "additionalProperties": false + } + }, + "jurisdictions": { + "type": "object", + "additionalProperties": { + "type": "object", + "properties": { + "actions": { + "type": "object", + "properties": { + "readPrivate": { + "type": "boolean" + }, + "admin": { + "type": "boolean" + }, + "write": { + "type": "boolean" + }, + "readSSN": { + "type": "boolean" + } + }, + "additionalProperties": false + } + } + } } - } + }, + "additionalProperties": false } + }, + "attributes": { + "required": [ + "email", + "familyName", + "givenName" + ], + "type": "object", + "properties": { + "givenName": { + "maxLength": 100, + "minLength": 1, + "type": "string" + }, + "familyName": { + "maxLength": 100, + "minLength": 1, + "type": "string" + }, + "email": { + "maxLength": 100, + "minLength": 5, + "type": "string" + } + }, + "additionalProperties": false + }, + "userId": { + "type": "string" + }, + "status": { + "type": "string", + "enum": [ + "active", + "inactive" + ] } }, "additionalProperties": false @@ -5623,199 +5047,190 @@ }, "additionalProperties": false }, - "SandboLicenHcuQxywUvhOh": { + "SandboLicenr4NorsIuc9Tp": { + "required": [ + "affiliationType", + "dateOfUpdate", + "dateOfUpload", + "documentUploadFields", + "status" + ], "type": "object", "properties": { - "dateCreated": { + "dateOfUpload": { + "pattern": "^[12]{1}[0-9]{3}-(0[1-9]|1[0-2])-(0[1-9]|[12][0-9]|3[01])$", "type": "string", - "format": "date-time" - }, - "attestationId": { - "type": "string" + "description": "The date the document was uploaded", + "format": "date" }, - "compact": { + "affiliationType": { "type": "string", + "description": "The type of military affiliation", "enum": [ - "aslp", - "octp", - "coun" + "militaryMember", + "militaryMemberSpouse" ] }, - "text": { - "type": "string" + "fileNames": { + "type": "array", + "description": "List of military affiliation file names", + "items": { + "type": "string", + "description": "The name of the file being uploaded" + } }, - "type": { + "dateOfUpdate": { + "pattern": "^[12]{1}[0-9]{3}-(0[1-9]|1[0-2])-(0[1-9]|[12][0-9]|3[01])$", "type": "string", - "enum": [ - "attestation" - ] - }, - "locale": { - "type": "string" + "description": "The date the document was last updated", + "format": "date" }, - "version": { - "type": "string" + "status": { + "type": "string", + "description": "The status of the military affiliation" }, - "required": { - "type": "boolean" + "documentUploadFields": { + "type": "array", + "description": "The fields used to upload documents", + "items": { + "type": "object", + "properties": { + "fields": { + "type": "object", + "additionalProperties": { + "type": "string" + }, + "description": "The form fields used to upload the document" + }, + "url": { + "type": "string", + "description": "The url to upload the document to" + } + }, + "description": "The fields used to upload a specific document" + } } } }, - "SandboLicenDb8Dl04rAoPD": { + "SandboLicenNPjSTFoXDCnt": { "required": [ - "attributes", - "permissions" + "compact", + "jurisdictionAdverseActionsNotificationEmails", + "jurisdictionName", + "jurisdictionOperationsTeamEmails", + "jurisdictionSummaryReportNotificationEmails", + "jurisprudenceRequirements", + "licenseeRegistrationEnabled", + "postalAbbreviation", + "privilegeFees" ], "type": "object", "properties": { - "permissions": { - "type": "object", - "additionalProperties": { + "privilegeFees": { + "type": "array", + "description": "The fees for the privileges by license type", + "items": { + "required": [ + "amount", + "licenseTypeAbbreviation" + ], "type": "object", "properties": { - "actions": { - "type": "object", - "properties": { - "readPrivate": { - "type": "boolean" - }, - "admin": { - "type": "boolean" + "amount": { + "type": "number" + }, + "militaryRate": { + "description": "Optional military rate for the privilege fee.", + "oneOf": [ + { + "minimum": 0, + "type": "number" }, - "readSSN": { - "type": "boolean" - } - } + null + ] }, - "jurisdictions": { - "type": "object", - "additionalProperties": { - "type": "object", - "properties": { - "actions": { - "type": "object", - "properties": { - "readPrivate": { - "type": "boolean" - }, - "admin": { - "type": "boolean" - }, - "write": { - "type": "boolean" - }, - "readSSN": { - "type": "boolean" - } - }, - "additionalProperties": false - } - } - } + "licenseTypeAbbreviation": { + "type": "string", + "enum": [ + "aud", + "slp", + "ot", + "ota", + "lpc" + ] } - }, - "additionalProperties": false + } } }, - "attributes": { - "required": [ - "email", - "familyName", - "givenName" - ], - "type": "object", - "properties": { - "givenName": { - "maxLength": 100, - "minLength": 1, - "type": "string" - }, - "familyName": { - "maxLength": 100, - "minLength": 1, - "type": "string" - }, - "email": { - "maxLength": 100, - "minLength": 5, - "type": "string" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - "SandboLicen8QcYRI7AzDyX": { - "required": [ - "jurisdiction" - ], - "type": "object", - "properties": { - "jurisdiction": { + "postalAbbreviation": { "type": "string", - "description": "The jurisdiction postal abbreviation to set as home jurisdiction", - "enum": [ - "al", - "ak", - "az", - "ar", - "ca", - "co", - "ct", - "de", - "dc", - "fl", - "ga", - "hi", - "id", - "il", - "in", - "ia", - "ks", - "ky", - "la", - "me", - "md", - "ma", - "mi", - "mn", - "ms", - "mo", - "mt", - "ne", - "nv", - "nh", - "nj", - "nm", - "ny", - "nc", - "nd", - "oh", - "ok", - "or", - "pa", - "pr", - "ri", - "sc", - "sd", - "tn", - "tx", - "ut", - "vt", - "va", - "vi", - "wa", - "wv", - "wi", - "wy", - "other" + "description": "The postal abbreviation of the jurisdiction" + }, + "jurisdictionAdverseActionsNotificationEmails": { + "type": "array", + "description": "List of email addresses for adverse actions notifications", + "items": { + "type": "string", + "format": "email" + } + }, + "jurisdictionOperationsTeamEmails": { + "type": "array", + "description": "List of email addresses for operations team notifications", + "items": { + "type": "string", + "format": "email" + } + }, + "compact": { + "type": "string", + "description": "The compact this jurisdiction configuration belongs to", + "enum": [ + "aslp", + "octp", + "coun" ] + }, + "jurisprudenceRequirements": { + "required": [ + "required" + ], + "type": "object", + "properties": { + "linkToDocumentation": { + "description": "Optional link to jurisprudence documentation", + "oneOf": [ + { + "type": "string" + }, + null + ] + }, + "required": { + "type": "boolean", + "description": "Whether jurisprudence requirements exist" + } + } + }, + "licenseeRegistrationEnabled": { + "type": "boolean", + "description": "Denotes whether licensee registration is enabled" + }, + "jurisdictionName": { + "type": "string", + "description": "The name of the jurisdiction" + }, + "jurisdictionSummaryReportNotificationEmails": { + "type": "array", + "description": "List of email addresses for summary report notifications", + "items": { + "type": "string", + "format": "email" + } } - }, - "additionalProperties": false + } }, - "SandboLicenBWZgN83iyevY": { + "SandboLicen6PcW7zZXxI2G": { "required": [ "attestations", "licenseType", @@ -5913,289 +5328,81 @@ "ky", "la", "me", - "md", - "ma", - "mi", - "mn", - "ms", - "mo", - "mt", - "ne", - "nv", - "nh", - "nj", - "nm", - "ny", - "nc", - "nd", - "oh", - "ok", - "or", - "pa", - "pr", - "ri", - "sc", - "sd", - "tn", - "tx", - "ut", - "vt", - "va", - "vi", - "wa", - "wv", - "wi", - "wy" - ] - } - } - } - }, - "SandboLicenBxfAXNgCl0f1": { - "required": [ - "compactAbbr", - "compactAdverseActionsNotificationEmails", - "compactCommissionFee", - "compactName", - "compactOperationsTeamEmails", - "compactSummaryReportNotificationEmails", - "configuredStates", - "licenseeRegistrationEnabled" - ], - "type": "object", - "properties": { - "configuredStates": { - "type": "array", - "description": "List of states that have submitted configurations and their live status", - "items": { - "required": [ - "isLive", - "postalAbbreviation" - ], - "type": "object", - "properties": { - "postalAbbreviation": { - "type": "string", - "description": "The postal abbreviation of the jurisdiction", - "enum": [ - "al", - "ak", - "az", - "ar", - "ca", - "co", - "ct", - "de", - "dc", - "fl", - "ga", - "hi", - "id", - "il", - "in", - "ia", - "ks", - "ky", - "la", - "me", - "md", - "ma", - "mi", - "mn", - "ms", - "mo", - "mt", - "ne", - "nv", - "nh", - "nj", - "nm", - "ny", - "nc", - "nd", - "oh", - "ok", - "or", - "pa", - "pr", - "ri", - "sc", - "sd", - "tn", - "tx", - "ut", - "vt", - "va", - "vi", - "wa", - "wv", - "wi", - "wy" - ] - }, - "isLive": { - "type": "boolean", - "description": "Whether the state is live and available for registrations." - } - } - } - }, - "compactCommissionFee": { - "required": [ - "feeAmount", - "feeType" - ], - "type": "object", - "properties": { - "feeAmount": { - "type": "number" - }, - "feeType": { - "type": "string", - "enum": [ - "FLAT_RATE" - ] - } - } - }, - "compactSummaryReportNotificationEmails": { - "type": "array", - "description": "List of email addresses for summary report notifications", - "items": { - "type": "string", - "format": "email" - } - }, - "compactAdverseActionsNotificationEmails": { - "type": "array", - "description": "List of email addresses for adverse actions notifications", - "items": { - "type": "string", - "format": "email" - } - }, - "licenseeRegistrationEnabled": { - "type": "boolean", - "description": "Denotes whether licensee registration is enabled" - }, - "compactAbbr": { - "type": "string", - "description": "The abbreviation of the compact" - }, - "transactionFeeConfiguration": { - "type": "object", - "properties": { - "licenseeCharges": { - "required": [ - "active", - "chargeAmount", - "chargeType" - ], - "type": "object", - "properties": { - "chargeType": { - "type": "string", - "description": "The type of transaction fee charge", - "enum": [ - "FLAT_FEE_PER_PRIVILEGE" - ] - }, - "active": { - "type": "boolean", - "description": "Whether the compact is charging licensees transaction fees" - }, - "chargeAmount": { - "type": "number", - "description": "The amount to charge per privilege purchased" - } - } - } - } - }, - "compactName": { - "type": "string", - "description": "The full name of the compact" - }, - "compactOperationsTeamEmails": { - "type": "array", - "description": "List of email addresses for operations team notifications", - "items": { - "type": "string", - "format": "email" + "md", + "ma", + "mi", + "mn", + "ms", + "mo", + "mt", + "ne", + "nv", + "nh", + "nj", + "nm", + "ny", + "nc", + "nd", + "oh", + "ok", + "or", + "pa", + "pr", + "ri", + "sc", + "sd", + "tn", + "tx", + "ut", + "vt", + "va", + "vi", + "wa", + "wv", + "wi", + "wy" + ] } } } }, - "SandboLiceno12FSfDWBzow": { + "SandboLicen6uzysMvBXwPR": { "required": [ - "pagination", - "providers" + "birthMonthDay", + "compact", + "dateOfExpiration", + "dateOfUpdate", + "familyName", + "givenName", + "licenseJurisdiction", + "licenses", + "militaryAffiliations", + "privilegeJurisdictions", + "privileges", + "providerId", + "type" ], "type": "object", "properties": { - "pagination": { - "type": "object", - "properties": { - "prevLastKey": { - "maxLength": 1024, - "minLength": 1, - "type": "object" - }, - "lastKey": { - "maxLength": 1024, - "minLength": 1, - "type": "object" - }, - "pageSize": { - "maximum": 100, - "minimum": 5, - "type": "integer" - } - } - }, - "sorting": { - "required": [ - "key" - ], - "type": "object", - "properties": { - "key": { - "type": "string", - "description": "The key to sort results by", - "enum": [ - "dateOfUpdate", - "familyName" - ] - }, - "direction": { - "type": "string", - "description": "Direction to sort results by", - "enum": [ - "ascending", - "descending" - ] - } - }, - "description": "How to sort results" - }, - "providers": { - "maxLength": 100, + "privileges": { "type": "array", "items": { "required": [ - "birthMonthDay", + "administratorSetStatus", + "attestations", "compact", - "compactEligibility", + "compactTransactionId", "dateOfExpiration", + "dateOfIssuance", + "dateOfRenewal", "dateOfUpdate", - "familyName", - "givenName", - "jurisdictionUploadedCompactEligibility", - "jurisdictionUploadedLicenseStatus", + "history", + "jurisdiction", "licenseJurisdiction", - "licenseStatus", - "privilegeJurisdictions", + "licenseType", + "privilegeId", "providerId", + "status", "type" ], "type": "object", @@ -6266,292 +5473,998 @@ "coun" ] }, - "npi": { - "pattern": "^[0-9]{10}$", - "type": "string" - }, - "givenName": { - "maxLength": 100, - "minLength": 1, - "type": "string" - }, - "compactEligibility": { - "type": "string", - "enum": [ - "eligible", - "ineligible" - ] - }, - "jurisdictionUploadedCompactEligibility": { - "type": "string", - "enum": [ - "eligible", - "ineligible" - ] - }, - "dateOfBirth": { - "pattern": "^[12]{1}[0-9]{3}-(0[1-9]|1[0-2])-(0[1-9]|[12][0-9]|3[01])$", - "type": "string", - "format": "date" - }, - "jurisdictionUploadedLicenseStatus": { + "jurisdiction": { "type": "string", "enum": [ - "active", - "inactive" - ] - }, - "privilegeJurisdictions": { - "type": "array", - "items": { - "type": "string", - "enum": [ - "al", - "ak", - "az", - "ar", - "ca", - "co", - "ct", - "de", - "dc", - "fl", - "ga", - "hi", - "id", - "il", - "in", - "ia", - "ks", - "ky", - "la", - "me", - "md", - "ma", - "mi", - "mn", - "ms", - "mo", - "mt", - "ne", - "nv", - "nh", - "nj", - "nm", - "ny", - "nc", - "nd", - "oh", - "ok", - "or", - "pa", - "pr", - "ri", - "sc", - "sd", - "tn", - "tx", - "ut", - "vt", - "va", - "vi", - "wa", - "wv", - "wi", - "wy" - ] + "al", + "ak", + "az", + "ar", + "ca", + "co", + "ct", + "de", + "dc", + "fl", + "ga", + "hi", + "id", + "il", + "in", + "ia", + "ks", + "ky", + "la", + "me", + "md", + "ma", + "mi", + "mn", + "ms", + "mo", + "mt", + "ne", + "nv", + "nh", + "nj", + "nm", + "ny", + "nc", + "nd", + "oh", + "ok", + "or", + "pa", + "pr", + "ri", + "sc", + "sd", + "tn", + "tx", + "ut", + "vt", + "va", + "vi", + "wa", + "wv", + "wi", + "wy" + ] + }, + "attestations": { + "type": "array", + "items": { + "required": [ + "attestationId", + "version" + ], + "type": "object", + "properties": { + "attestationId": { + "maxLength": 100, + "type": "string" + }, + "version": { + "maxLength": 100, + "type": "string" + } + } + } + }, + "history": { + "type": "array", + "items": { + "required": [ + "compact", + "dateOfUpdate", + "jurisdiction", + "previous", + "type", + "updateType" + ], + "type": "object", + "properties": { + "removedValues": { + "type": "array", + "description": "List of field names that were present in the previous record but removed in the update", + "items": { + "type": "string" + } + }, + "licenseType": { + "type": "string", + "enum": [ + "audiologist", + "speech-language pathologist", + "occupational therapist", + "occupational therapy assistant", + "licensed professional counselor" + ] + }, + "compact": { + "type": "string", + "enum": [ + "aslp", + "octp", + "coun" + ] + }, + "previous": { + "required": [ + "administratorSetStatus", + "attestations", + "compactTransactionId", + "dateOfExpiration", + "dateOfIssuance", + "dateOfRenewal", + "dateOfUpdate", + "licenseJurisdiction", + "privilegeId" + ], + "type": "object", + "properties": { + "licenseJurisdiction": { + "type": "string", + "enum": [ + "al", + "ak", + "az", + "ar", + "ca", + "co", + "ct", + "de", + "dc", + "fl", + "ga", + "hi", + "id", + "il", + "in", + "ia", + "ks", + "ky", + "la", + "me", + "md", + "ma", + "mi", + "mn", + "ms", + "mo", + "mt", + "ne", + "nv", + "nh", + "nj", + "nm", + "ny", + "nc", + "nd", + "oh", + "ok", + "or", + "pa", + "pr", + "ri", + "sc", + "sd", + "tn", + "tx", + "ut", + "vt", + "va", + "vi", + "wa", + "wv", + "wi", + "wy" + ] + }, + "compact": { + "type": "string", + "enum": [ + "aslp", + "octp", + "coun" + ] + }, + "jurisdiction": { + "type": "string", + "enum": [ + "al", + "ak", + "az", + "ar", + "ca", + "co", + "ct", + "de", + "dc", + "fl", + "ga", + "hi", + "id", + "il", + "in", + "ia", + "ks", + "ky", + "la", + "me", + "md", + "ma", + "mi", + "mn", + "ms", + "mo", + "mt", + "ne", + "nv", + "nh", + "nj", + "nm", + "ny", + "nc", + "nd", + "oh", + "ok", + "or", + "pa", + "pr", + "ri", + "sc", + "sd", + "tn", + "tx", + "ut", + "vt", + "va", + "vi", + "wa", + "wv", + "wi", + "wy" + ] + }, + "attestations": { + "type": "array", + "items": { + "required": [ + "attestationId", + "version" + ], + "type": "object", + "properties": { + "attestationId": { + "maxLength": 100, + "type": "string" + }, + "version": { + "maxLength": 100, + "type": "string" + } + } + } + }, + "type": { + "type": "string", + "enum": [ + "privilege" + ] + }, + "compactTransactionId": { + "type": "string" + }, + "dateOfIssuance": { + "pattern": "^[12]{1}[0-9]{3}-(0[1-9]|1[0-2])-(0[1-9]|[12][0-9]|3[01])$", + "type": "string", + "format": "date" + }, + "administratorSetStatus": { + "type": "string", + "enum": [ + "active", + "inactive" + ] + }, + "dateOfExpiration": { + "pattern": "^[12]{1}[0-9]{3}-(0[1-9]|1[0-2])-(0[1-9]|[12][0-9]|3[01])$", + "type": "string", + "format": "date" + }, + "privilegeId": { + "type": "string" + }, + "providerId": { + "pattern": "[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab]{1}[0-9a-f]{3}-[0-9a-f]{12}", + "type": "string" + }, + "dateOfRenewal": { + "pattern": "^[12]{1}[0-9]{3}-(0[1-9]|1[0-2])-(0[1-9]|[12][0-9]|3[01])$", + "type": "string", + "format": "date" + }, + "dateOfUpdate": { + "pattern": "^[12]{1}[0-9]{3}-(0[1-9]|1[0-2])-(0[1-9]|[12][0-9]|3[01])$", + "type": "string", + "format": "date" + }, + "status": { + "type": "string", + "enum": [ + "active", + "inactive" + ] + } + } + }, + "jurisdiction": { + "type": "string", + "enum": [ + "al", + "ak", + "az", + "ar", + "ca", + "co", + "ct", + "de", + "dc", + "fl", + "ga", + "hi", + "id", + "il", + "in", + "ia", + "ks", + "ky", + "la", + "me", + "md", + "ma", + "mi", + "mn", + "ms", + "mo", + "mt", + "ne", + "nv", + "nh", + "nj", + "nm", + "ny", + "nc", + "nd", + "oh", + "ok", + "or", + "pa", + "pr", + "ri", + "sc", + "sd", + "tn", + "tx", + "ut", + "vt", + "va", + "vi", + "wa", + "wv", + "wi", + "wy" + ] + }, + "updatedValues": { + "type": "object", + "properties": { + "licenseJurisdiction": { + "type": "string", + "enum": [ + "al", + "ak", + "az", + "ar", + "ca", + "co", + "ct", + "de", + "dc", + "fl", + "ga", + "hi", + "id", + "il", + "in", + "ia", + "ks", + "ky", + "la", + "me", + "md", + "ma", + "mi", + "mn", + "ms", + "mo", + "mt", + "ne", + "nv", + "nh", + "nj", + "nm", + "ny", + "nc", + "nd", + "oh", + "ok", + "or", + "pa", + "pr", + "ri", + "sc", + "sd", + "tn", + "tx", + "ut", + "vt", + "va", + "vi", + "wa", + "wv", + "wi", + "wy" + ] + }, + "compact": { + "type": "string", + "enum": [ + "aslp", + "octp", + "coun" + ] + }, + "jurisdiction": { + "type": "string", + "enum": [ + "al", + "ak", + "az", + "ar", + "ca", + "co", + "ct", + "de", + "dc", + "fl", + "ga", + "hi", + "id", + "il", + "in", + "ia", + "ks", + "ky", + "la", + "me", + "md", + "ma", + "mi", + "mn", + "ms", + "mo", + "mt", + "ne", + "nv", + "nh", + "nj", + "nm", + "ny", + "nc", + "nd", + "oh", + "ok", + "or", + "pa", + "pr", + "ri", + "sc", + "sd", + "tn", + "tx", + "ut", + "vt", + "va", + "vi", + "wa", + "wv", + "wi", + "wy" + ] + }, + "attestations": { + "type": "array", + "items": { + "required": [ + "attestationId", + "version" + ], + "type": "object", + "properties": { + "attestationId": { + "maxLength": 100, + "type": "string" + }, + "version": { + "maxLength": 100, + "type": "string" + } + } + } + }, + "type": { + "type": "string", + "enum": [ + "privilege" + ] + }, + "compactTransactionId": { + "type": "string" + }, + "dateOfIssuance": { + "pattern": "^[12]{1}[0-9]{3}-(0[1-9]|1[0-2])-(0[1-9]|[12][0-9]|3[01])$", + "type": "string", + "format": "date" + }, + "administratorSetStatus": { + "type": "string", + "enum": [ + "active", + "inactive" + ] + }, + "dateOfExpiration": { + "pattern": "^[12]{1}[0-9]{3}-(0[1-9]|1[0-2])-(0[1-9]|[12][0-9]|3[01])$", + "type": "string", + "format": "date" + }, + "privilegeId": { + "type": "string" + }, + "providerId": { + "pattern": "[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab]{1}[0-9a-f]{3}-[0-9a-f]{12}", + "type": "string" + }, + "dateOfRenewal": { + "pattern": "^[12]{1}[0-9]{3}-(0[1-9]|1[0-2])-(0[1-9]|[12][0-9]|3[01])$", + "type": "string", + "format": "date" + }, + "dateOfUpdate": { + "pattern": "^[12]{1}[0-9]{3}-(0[1-9]|1[0-2])-(0[1-9]|[12][0-9]|3[01])$", + "type": "string", + "format": "date" + }, + "status": { + "type": "string", + "enum": [ + "active", + "inactive" + ] + } + } + }, + "type": { + "type": "string", + "enum": [ + "privilegeUpdate" + ] + }, + "dateOfUpdate": { + "pattern": "^[12]{1}[0-9]{3}-(0[1-9]|1[0-2])-(0[1-9]|[12][0-9]|3[01])$", + "type": "string", + "format": "date" + }, + "updateType": { + "type": "string", + "enum": [ + "deactivation", + "expiration", + "issuance", + "other", + "renewal", + "encumbrance", + "homeJurisdictionChange", + "registration", + "lifting_encumbrance", + "licenseDeactivation", + "emailChange" + ] + } + } } }, "type": { "type": "string", "enum": [ - "provider" + "privilege" ] }, - "suffix": { - "maxLength": 100, - "minLength": 1, + "compactTransactionId": { "type": "string" }, - "currentHomeJurisdiction": { + "dateOfIssuance": { + "pattern": "^[12]{1}[0-9]{3}-(0[1-9]|1[0-2])-(0[1-9]|[12][0-9]|3[01])$", + "type": "string", + "format": "date" + }, + "licenseType": { "type": "string", - "description": "The current jurisdiction postal abbreviation if known.", "enum": [ - "al", - "ak", - "az", - "ar", - "ca", - "co", - "ct", - "de", - "dc", - "fl", - "ga", - "hi", - "id", - "il", - "in", - "ia", - "ks", - "ky", - "la", - "me", - "md", - "ma", - "mi", - "mn", - "ms", - "mo", - "mt", - "ne", - "nv", - "nh", - "nj", - "nm", - "ny", - "nc", - "nd", - "oh", - "ok", - "or", - "pa", - "pr", - "ri", - "sc", - "sd", - "tn", - "tx", - "ut", - "vt", - "va", - "vi", - "wa", - "wv", - "wi", - "wy", - "other", - "unknown" + "audiologist", + "speech-language pathologist", + "occupational therapist", + "occupational therapy assistant", + "licensed professional counselor" ] }, - "ssnLastFour": { - "pattern": "^[0-9]{4}$", + "administratorSetStatus": { + "type": "string", + "enum": [ + "active", + "inactive" + ] + }, + "dateOfExpiration": { + "pattern": "^[12]{1}[0-9]{3}-(0[1-9]|1[0-2])-(0[1-9]|[12][0-9]|3[01])$", + "type": "string", + "format": "date" + }, + "privilegeId": { + "type": "string" + }, + "providerId": { + "pattern": "[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab]{1}[0-9a-f]{3}-[0-9a-f]{12}", "type": "string" }, - "dateOfExpiration": { + "dateOfRenewal": { + "pattern": "^[12]{1}[0-9]{3}-(0[1-9]|1[0-2])-(0[1-9]|[12][0-9]|3[01])$", + "type": "string", + "format": "date" + }, + "adverseActions": { + "type": "array", + "items": { + "required": [ + "actionAgainst", + "adverseActionId", + "clinicalPrivilegeActionCategory", + "compact", + "creationDate", + "dateOfUpdate", + "effectiveStartDate", + "encumbranceType", + "jurisdiction", + "licenseType", + "licenseTypeAbbreviation", + "providerId", + "type" + ], + "type": "object", + "properties": { + "compact": { + "type": "string", + "enum": [ + "aslp", + "octp", + "coun" + ] + }, + "jurisdiction": { + "type": "string", + "enum": [ + "al", + "ak", + "az", + "ar", + "ca", + "co", + "ct", + "de", + "dc", + "fl", + "ga", + "hi", + "id", + "il", + "in", + "ia", + "ks", + "ky", + "la", + "me", + "md", + "ma", + "mi", + "mn", + "ms", + "mo", + "mt", + "ne", + "nv", + "nh", + "nj", + "nm", + "ny", + "nc", + "nd", + "oh", + "ok", + "or", + "pa", + "pr", + "ri", + "sc", + "sd", + "tn", + "tx", + "ut", + "vt", + "va", + "vi", + "wa", + "wv", + "wi", + "wy" + ] + }, + "licenseTypeAbbreviation": { + "type": "string" + }, + "type": { + "type": "string", + "enum": [ + "adverseAction" + ] + }, + "creationDate": { + "pattern": "^[12]{1}[0-9]{3}-(0[1-9]|1[0-2])-(0[1-9]|[12][0-9]|3[01])$", + "type": "string", + "format": "date" + }, + "actionAgainst": { + "type": "string" + }, + "licenseType": { + "type": "string" + }, + "providerId": { + "pattern": "[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab]{1}[0-9a-f]{3}-[0-9a-f]{12}", + "type": "string" + }, + "effectiveStartDate": { + "pattern": "^[12]{1}[0-9]{3}-(0[1-9]|1[0-2])-(0[1-9]|[12][0-9]|3[01])$", + "type": "string", + "format": "date" + }, + "adverseActionId": { + "type": "string" + }, + "effectiveLiftDate": { + "pattern": "^[12]{1}[0-9]{3}-(0[1-9]|1[0-2])-(0[1-9]|[12][0-9]|3[01])$", + "type": "string", + "format": "date" + }, + "encumbranceType": { + "type": "string" + }, + "clinicalPrivilegeActionCategory": { + "type": "string" + }, + "liftingUser": { + "type": "string" + }, + "dateOfUpdate": { + "pattern": "^[12]{1}[0-9]{3}-(0[1-9]|1[0-2])-(0[1-9]|[12][0-9]|3[01])$", + "type": "string", + "format": "date" + } + } + } + }, + "dateOfUpdate": { "pattern": "^[12]{1}[0-9]{3}-(0[1-9]|1[0-2])-(0[1-9]|[12][0-9]|3[01])$", "type": "string", "format": "date" }, - "providerId": { - "pattern": "[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab]{1}[0-9a-f]{3}-[0-9a-f]{12}", - "type": "string" - }, - "licenseStatus": { + "status": { "type": "string", "enum": [ "active", "inactive" ] - }, - "familyName": { - "maxLength": 100, - "minLength": 1, - "type": "string" - }, - "middleName": { - "maxLength": 100, - "minLength": 1, - "type": "string" - }, - "birthMonthDay": { - "pattern": "^[01]{1}[0-9]{1}-[0-3]{1}[0-9]{1}$", - "type": "string", - "format": "date" - }, - "compactConnectRegisteredEmailAddress": { - "maxLength": 100, - "minLength": 5, - "type": "string", - "format": "email" - }, - "dateOfUpdate": { - "pattern": "^[12]{1}[0-9]{3}-(0[1-9]|1[0-2])-(0[1-9]|[12][0-9]|3[01])$", - "type": "string", - "format": "date" } } } - } - } - }, - "SandboLicendERazg9NeEsA": { - "required": [ - "status" - ], - "type": "object", - "properties": { - "status": { + }, + "licenseJurisdiction": { "type": "string", - "description": "The status to set the military affiliation to.", "enum": [ - "inactive" + "al", + "ak", + "az", + "ar", + "ca", + "co", + "ct", + "de", + "dc", + "fl", + "ga", + "hi", + "id", + "il", + "in", + "ia", + "ks", + "ky", + "la", + "me", + "md", + "ma", + "mi", + "mn", + "ms", + "mo", + "mt", + "ne", + "nv", + "nh", + "nj", + "nm", + "ny", + "nc", + "nd", + "oh", + "ok", + "or", + "pa", + "pr", + "ri", + "sc", + "sd", + "tn", + "tx", + "ut", + "vt", + "va", + "vi", + "wa", + "wv", + "wi", + "wy" ] - } - }, - "additionalProperties": false - }, - "SandboLicenwoqrSnm0rsGM": { - "required": [ - "compact", - "dob", - "email", - "familyName", - "givenName", - "jurisdiction", - "licenseType", - "partialSocial", - "token" - ], - "type": "object", - "properties": { - "licenseType": { - "maxLength": 500, + }, + "compact": { "type": "string", - "description": "Type of license", "enum": [ - "audiologist", - "speech-language pathologist", - "occupational therapist", - "occupational therapy assistant", - "licensed professional counselor" + "aslp", + "octp", + "coun" ] }, - "compact": { + "npi": { + "pattern": "^[0-9]{10}$", + "type": "string" + }, + "givenName": { "maxLength": 100, + "minLength": 1, + "type": "string" + }, + "compactEligibility": { "type": "string", - "description": "Compact name" + "enum": [ + "eligible", + "ineligible" + ] }, - "dob": { + "jurisdictionUploadedCompactEligibility": { + "type": "string", + "enum": [ + "eligible", + "ineligible" + ] + }, + "dateOfBirth": { "pattern": "^[12]{1}[0-9]{3}-(0[1-9]|1[0-2])-(0[1-9]|[12][0-9]|3[01])$", "type": "string", - "description": "Date of birth in YYYY-MM-DD format" + "format": "date" }, - "givenName": { - "maxLength": 200, + "jurisdictionUploadedLicenseStatus": { "type": "string", - "description": "Provider's given name" + "enum": [ + "active", + "inactive" + ] }, - "familyName": { - "maxLength": 200, + "privilegeJurisdictions": { + "type": "array", + "items": { + "type": "string", + "enum": [ + "al", + "ak", + "az", + "ar", + "ca", + "co", + "ct", + "de", + "dc", + "fl", + "ga", + "hi", + "id", + "il", + "in", + "ia", + "ks", + "ky", + "la", + "me", + "md", + "ma", + "mi", + "mn", + "ms", + "mo", + "mt", + "ne", + "nv", + "nh", + "nj", + "nm", + "ny", + "nc", + "nd", + "oh", + "ok", + "or", + "pa", + "pr", + "ri", + "sc", + "sd", + "tn", + "tx", + "ut", + "vt", + "va", + "vi", + "wa", + "wv", + "wi", + "wy" + ] + } + }, + "type": { "type": "string", - "description": "Provider's family name" + "enum": [ + "provider" + ] }, - "jurisdiction": { - "maxLength": 2, - "minLength": 2, + "suffix": { + "maxLength": 100, + "minLength": 1, + "type": "string" + }, + "currentHomeJurisdiction": { "type": "string", - "description": "Two-letter jurisdiction code", + "description": "The current jurisdiction postal abbreviation if known.", "enum": [ "al", "ak", @@ -6605,143 +6518,39 @@ "wa", "wv", "wi", - "wy" + "wy", + "other", + "unknown" ] }, - "partialSocial": { - "maxLength": 4, - "minLength": 4, - "type": "string", - "description": "Last 4 digits of SSN" - }, - "email": { - "maxLength": 100, - "minLength": 5, - "type": "string", - "description": "Provider's email address", - "format": "email" - }, - "token": { - "type": "string", - "description": "ReCAPTCHA token" - } - } - }, - "SandboLicenaSmGZue6afyn": { - "required": [ - "newEmailAddress" - ], - "type": "object", - "properties": { - "newEmailAddress": { - "maxLength": 100, - "minLength": 5, - "type": "string", - "description": "The new email address to set for the provider", - "format": "email" - } - }, - "additionalProperties": false - }, - "SandboLicenr5TVmpxKfPGq": { - "required": [ - "birthMonthDay", - "compact", - "dateOfExpiration", - "dateOfUpdate", - "familyName", - "givenName", - "licenseJurisdiction", - "licenses", - "militaryAffiliations", - "privilegeJurisdictions", - "privileges", - "providerId", - "type" - ], - "type": "object", - "properties": { - "privileges": { + "licenses": { "type": "array", "items": { "required": [ - "administratorSetStatus", - "attestations", "compact", - "compactTransactionId", + "compactEligibility", "dateOfExpiration", "dateOfIssuance", "dateOfRenewal", "dateOfUpdate", + "familyName", + "givenName", "history", + "homeAddressCity", + "homeAddressPostalCode", + "homeAddressState", + "homeAddressStreet1", "jurisdiction", - "licenseJurisdiction", + "jurisdictionUploadedCompactEligibility", + "jurisdictionUploadedLicenseStatus", + "licenseStatus", "licenseType", - "privilegeId", + "middleName", "providerId", - "status", "type" ], "type": "object", "properties": { - "licenseJurisdiction": { - "type": "string", - "enum": [ - "al", - "ak", - "az", - "ar", - "ca", - "co", - "ct", - "de", - "dc", - "fl", - "ga", - "hi", - "id", - "il", - "in", - "ia", - "ks", - "ky", - "la", - "me", - "md", - "ma", - "mi", - "mn", - "ms", - "mo", - "mt", - "ne", - "nv", - "nh", - "nj", - "nm", - "ny", - "nc", - "nd", - "oh", - "ok", - "or", - "pa", - "pr", - "ri", - "sc", - "sd", - "tn", - "tx", - "ut", - "vt", - "va", - "vi", - "wa", - "wv", - "wi", - "wy" - ] - }, "compact": { "type": "string", "enum": [ @@ -6750,6 +6559,11 @@ "coun" ] }, + "homeAddressStreet2": { + "maxLength": 100, + "minLength": 1, + "type": "string" + }, "jurisdiction": { "type": "string", "enum": [ @@ -6808,25 +6622,116 @@ "wy" ] }, - "attestations": { - "type": "array", - "items": { - "required": [ - "attestationId", - "version" - ], - "type": "object", - "properties": { - "attestationId": { - "maxLength": 100, - "type": "string" - }, - "version": { - "maxLength": 100, - "type": "string" - } - } - } + "homeAddressStreet1": { + "maxLength": 100, + "minLength": 2, + "type": "string" + }, + "type": { + "type": "string", + "enum": [ + "license-home" + ] + }, + "suffix": { + "maxLength": 100, + "minLength": 1, + "type": "string" + }, + "dateOfIssuance": { + "pattern": "^[12]{1}[0-9]{3}-(0[1-9]|1[0-2])-(0[1-9]|[12][0-9]|3[01])$", + "type": "string", + "format": "date" + }, + "licenseType": { + "type": "string", + "enum": [ + "audiologist", + "speech-language pathologist", + "occupational therapist", + "occupational therapy assistant", + "licensed professional counselor" + ] + }, + "emailAddress": { + "maxLength": 100, + "minLength": 5, + "type": "string", + "format": "email" + }, + "dateOfExpiration": { + "pattern": "^[12]{1}[0-9]{3}-(0[1-9]|1[0-2])-(0[1-9]|[12][0-9]|3[01])$", + "type": "string", + "format": "date" + }, + "homeAddressState": { + "maxLength": 100, + "minLength": 2, + "type": "string" + }, + "providerId": { + "pattern": "[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab]{1}[0-9a-f]{3}-[0-9a-f]{12}", + "type": "string" + }, + "dateOfRenewal": { + "pattern": "^[12]{1}[0-9]{3}-(0[1-9]|1[0-2])-(0[1-9]|[12][0-9]|3[01])$", + "type": "string", + "format": "date" + }, + "familyName": { + "maxLength": 100, + "minLength": 1, + "type": "string" + }, + "homeAddressCity": { + "maxLength": 100, + "minLength": 2, + "type": "string" + }, + "licenseNumber": { + "maxLength": 100, + "minLength": 1, + "type": "string" + }, + "npi": { + "pattern": "^[0-9]{10}$", + "type": "string" + }, + "homeAddressPostalCode": { + "maxLength": 7, + "minLength": 5, + "type": "string" + }, + "compactEligibility": { + "type": "string", + "enum": [ + "eligible", + "ineligible" + ] + }, + "givenName": { + "maxLength": 100, + "minLength": 1, + "type": "string" + }, + "jurisdictionUploadedCompactEligibility": { + "type": "string", + "enum": [ + "eligible", + "ineligible" + ] + }, + "dateOfBirth": { + "pattern": "^[12]{1}[0-9]{3}-(0[1-9]|1[0-2])-(0[1-9]|[12][0-9]|3[01])$", + "type": "string", + "format": "date" + }, + "jurisdictionUploadedLicenseStatus": { + "type": "string", + "enum": [ + "active", + "inactive" + ] }, "history": { "type": "array", @@ -6868,169 +6773,74 @@ }, "previous": { "required": [ - "administratorSetStatus", - "attestations", - "compactTransactionId", "dateOfExpiration", "dateOfIssuance", "dateOfRenewal", - "dateOfUpdate", - "licenseJurisdiction", - "privilegeId" + "familyName", + "givenName", + "homeAddressCity", + "homeAddressPostalCode", + "homeAddressState", + "homeAddressStreet1", + "jurisdictionUploadedCompactEligibility", + "jurisdictionUploadedLicenseStatus", + "middleName" ], "type": "object", "properties": { - "licenseJurisdiction": { - "type": "string", - "enum": [ - "al", - "ak", - "az", - "ar", - "ca", - "co", - "ct", - "de", - "dc", - "fl", - "ga", - "hi", - "id", - "il", - "in", - "ia", - "ks", - "ky", - "la", - "me", - "md", - "ma", - "mi", - "mn", - "ms", - "mo", - "mt", - "ne", - "nv", - "nh", - "nj", - "nm", - "ny", - "nc", - "nd", - "oh", - "ok", - "or", - "pa", - "pr", - "ri", - "sc", - "sd", - "tn", - "tx", - "ut", - "vt", - "va", - "vi", - "wa", - "wv", - "wi", - "wy" - ] + "homeAddressStreet2": { + "maxLength": 100, + "minLength": 1, + "type": "string" }, - "compact": { + "npi": { + "pattern": "^[0-9]{10}$", + "type": "string" + }, + "homeAddressPostalCode": { + "maxLength": 7, + "minLength": 5, + "type": "string" + }, + "givenName": { + "maxLength": 100, + "minLength": 1, + "type": "string" + }, + "homeAddressStreet1": { + "maxLength": 100, + "minLength": 2, + "type": "string" + }, + "compactEligibility": { "type": "string", "enum": [ - "aslp", - "octp", - "coun" + "eligible", + "ineligible" ] }, - "jurisdiction": { + "jurisdictionUploadedCompactEligibility": { "type": "string", "enum": [ - "al", - "ak", - "az", - "ar", - "ca", - "co", - "ct", - "de", - "dc", - "fl", - "ga", - "hi", - "id", - "il", - "in", - "ia", - "ks", - "ky", - "la", - "me", - "md", - "ma", - "mi", - "mn", - "ms", - "mo", - "mt", - "ne", - "nv", - "nh", - "nj", - "nm", - "ny", - "nc", - "nd", - "oh", - "ok", - "or", - "pa", - "pr", - "ri", - "sc", - "sd", - "tn", - "tx", - "ut", - "vt", - "va", - "vi", - "wa", - "wv", - "wi", - "wy" + "eligible", + "ineligible" ] }, - "attestations": { - "type": "array", - "items": { - "required": [ - "attestationId", - "version" - ], - "type": "object", - "properties": { - "attestationId": { - "maxLength": 100, - "type": "string" - }, - "version": { - "maxLength": 100, - "type": "string" - } - } - } + "dateOfBirth": { + "pattern": "^[12]{1}[0-9]{3}-(0[1-9]|1[0-2])-(0[1-9]|[12][0-9]|3[01])$", + "type": "string", + "format": "date" }, - "type": { + "jurisdictionUploadedLicenseStatus": { "type": "string", "enum": [ - "privilege" + "active", + "inactive" ] }, - "compactTransactionId": { + "suffix": { + "maxLength": 100, + "minLength": 1, "type": "string" }, "dateOfIssuance": { @@ -7038,23 +6848,24 @@ "type": "string", "format": "date" }, - "administratorSetStatus": { + "emailAddress": { + "maxLength": 100, + "minLength": 5, "type": "string", - "enum": [ - "active", - "inactive" - ] + "format": "email" }, "dateOfExpiration": { "pattern": "^[12]{1}[0-9]{3}-(0[1-9]|1[0-2])-(0[1-9]|[12][0-9]|3[01])$", "type": "string", "format": "date" }, - "privilegeId": { + "phoneNumber": { + "pattern": "^\\+[0-9]{8,15}$", "type": "string" }, - "providerId": { - "pattern": "[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab]{1}[0-9a-f]{3}-[0-9a-f]{12}", + "homeAddressState": { + "maxLength": 100, + "minLength": 2, "type": "string" }, "dateOfRenewal": { @@ -7062,17 +6873,37 @@ "type": "string", "format": "date" }, - "dateOfUpdate": { - "pattern": "^[12]{1}[0-9]{3}-(0[1-9]|1[0-2])-(0[1-9]|[12][0-9]|3[01])$", - "type": "string", - "format": "date" + "licenseStatus": { + "type": "string", + "enum": [ + "active", + "inactive" + ] + }, + "familyName": { + "maxLength": 100, + "minLength": 1, + "type": "string" + }, + "homeAddressCity": { + "maxLength": 100, + "minLength": 2, + "type": "string" + }, + "licenseNumber": { + "maxLength": 100, + "minLength": 1, + "type": "string" + }, + "middleName": { + "maxLength": 100, + "minLength": 1, + "type": "string" }, - "status": { - "type": "string", - "enum": [ - "active", - "inactive" - ] + "licenseStatusName": { + "maxLength": 100, + "minLength": 1, + "type": "string" } } }, @@ -7137,157 +6968,59 @@ "updatedValues": { "type": "object", "properties": { - "licenseJurisdiction": { - "type": "string", - "enum": [ - "al", - "ak", - "az", - "ar", - "ca", - "co", - "ct", - "de", - "dc", - "fl", - "ga", - "hi", - "id", - "il", - "in", - "ia", - "ks", - "ky", - "la", - "me", - "md", - "ma", - "mi", - "mn", - "ms", - "mo", - "mt", - "ne", - "nv", - "nh", - "nj", - "nm", - "ny", - "nc", - "nd", - "oh", - "ok", - "or", - "pa", - "pr", - "ri", - "sc", - "sd", - "tn", - "tx", - "ut", - "vt", - "va", - "vi", - "wa", - "wv", - "wi", - "wy" - ] + "homeAddressStreet2": { + "maxLength": 100, + "minLength": 1, + "type": "string" }, - "compact": { + "npi": { + "pattern": "^[0-9]{10}$", + "type": "string" + }, + "homeAddressPostalCode": { + "maxLength": 7, + "minLength": 5, + "type": "string" + }, + "givenName": { + "maxLength": 100, + "minLength": 1, + "type": "string" + }, + "homeAddressStreet1": { + "maxLength": 100, + "minLength": 2, + "type": "string" + }, + "compactEligibility": { "type": "string", "enum": [ - "aslp", - "octp", - "coun" + "eligible", + "ineligible" ] }, - "jurisdiction": { + "jurisdictionUploadedCompactEligibility": { "type": "string", "enum": [ - "al", - "ak", - "az", - "ar", - "ca", - "co", - "ct", - "de", - "dc", - "fl", - "ga", - "hi", - "id", - "il", - "in", - "ia", - "ks", - "ky", - "la", - "me", - "md", - "ma", - "mi", - "mn", - "ms", - "mo", - "mt", - "ne", - "nv", - "nh", - "nj", - "nm", - "ny", - "nc", - "nd", - "oh", - "ok", - "or", - "pa", - "pr", - "ri", - "sc", - "sd", - "tn", - "tx", - "ut", - "vt", - "va", - "vi", - "wa", - "wv", - "wi", - "wy" + "eligible", + "ineligible" ] }, - "attestations": { - "type": "array", - "items": { - "required": [ - "attestationId", - "version" - ], - "type": "object", - "properties": { - "attestationId": { - "maxLength": 100, - "type": "string" - }, - "version": { - "maxLength": 100, - "type": "string" - } - } - } + "dateOfBirth": { + "pattern": "^[12]{1}[0-9]{3}-(0[1-9]|1[0-2])-(0[1-9]|[12][0-9]|3[01])$", + "type": "string", + "format": "date" }, - "type": { + "jurisdictionUploadedLicenseStatus": { "type": "string", "enum": [ - "privilege" + "active", + "inactive" ] }, - "compactTransactionId": { + "suffix": { + "maxLength": 100, + "minLength": 1, "type": "string" }, "dateOfIssuance": { @@ -7295,23 +7028,24 @@ "type": "string", "format": "date" }, - "administratorSetStatus": { + "emailAddress": { + "maxLength": 100, + "minLength": 5, "type": "string", - "enum": [ - "active", - "inactive" - ] + "format": "email" }, "dateOfExpiration": { "pattern": "^[12]{1}[0-9]{3}-(0[1-9]|1[0-2])-(0[1-9]|[12][0-9]|3[01])$", "type": "string", "format": "date" }, - "privilegeId": { + "phoneNumber": { + "pattern": "^\\+[0-9]{8,15}$", "type": "string" }, - "providerId": { - "pattern": "[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab]{1}[0-9a-f]{3}-[0-9a-f]{12}", + "homeAddressState": { + "maxLength": 100, + "minLength": 2, "type": "string" }, "dateOfRenewal": { @@ -7319,24 +7053,44 @@ "type": "string", "format": "date" }, - "dateOfUpdate": { - "pattern": "^[12]{1}[0-9]{3}-(0[1-9]|1[0-2])-(0[1-9]|[12][0-9]|3[01])$", - "type": "string", - "format": "date" - }, - "status": { + "licenseStatus": { "type": "string", "enum": [ "active", "inactive" ] + }, + "familyName": { + "maxLength": 100, + "minLength": 1, + "type": "string" + }, + "homeAddressCity": { + "maxLength": 100, + "minLength": 2, + "type": "string" + }, + "licenseNumber": { + "maxLength": 100, + "minLength": 1, + "type": "string" + }, + "middleName": { + "maxLength": 100, + "minLength": 1, + "type": "string" + }, + "licenseStatusName": { + "maxLength": 100, + "minLength": 1, + "type": "string" } } }, "type": { "type": "string", "enum": [ - "privilegeUpdate" + "licenseUpdate" ] }, "dateOfUpdate": { @@ -7358,59 +7112,36 @@ "lifting_encumbrance", "licenseDeactivation", "emailChange" - ] - } - } - } - }, - "type": { - "type": "string", - "enum": [ - "privilege" - ] + ] + } + } + } }, - "compactTransactionId": { + "ssnLastFour": { + "pattern": "^[0-9]{4}$", "type": "string" }, - "dateOfIssuance": { - "pattern": "^[12]{1}[0-9]{3}-(0[1-9]|1[0-2])-(0[1-9]|[12][0-9]|3[01])$", - "type": "string", - "format": "date" - }, - "licenseType": { - "type": "string", - "enum": [ - "audiologist", - "speech-language pathologist", - "occupational therapist", - "occupational therapy assistant", - "licensed professional counselor" - ] + "phoneNumber": { + "pattern": "^\\+[0-9]{8,15}$", + "type": "string" }, - "administratorSetStatus": { + "licenseStatus": { "type": "string", "enum": [ "active", "inactive" ] }, - "dateOfExpiration": { - "pattern": "^[12]{1}[0-9]{3}-(0[1-9]|1[0-2])-(0[1-9]|[12][0-9]|3[01])$", - "type": "string", - "format": "date" - }, - "privilegeId": { + "middleName": { + "maxLength": 100, + "minLength": 1, "type": "string" }, - "providerId": { - "pattern": "[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab]{1}[0-9a-f]{3}-[0-9a-f]{12}", + "licenseStatusName": { + "maxLength": 100, + "minLength": 1, "type": "string" }, - "dateOfRenewal": { - "pattern": "^[12]{1}[0-9]{3}-(0[1-9]|1[0-2])-(0[1-9]|[12][0-9]|3[01])$", - "type": "string", - "format": "date" - }, "adverseActions": { "type": "array", "items": { @@ -7555,6 +7286,92 @@ "pattern": "^[12]{1}[0-9]{3}-(0[1-9]|1[0-2])-(0[1-9]|[12][0-9]|3[01])$", "type": "string", "format": "date" + } + } + } + }, + "ssnLastFour": { + "pattern": "^[0-9]{4}$", + "type": "string" + }, + "dateOfExpiration": { + "pattern": "^[12]{1}[0-9]{3}-(0[1-9]|1[0-2])-(0[1-9]|[12][0-9]|3[01])$", + "type": "string", + "format": "date" + }, + "militaryAffiliations": { + "type": "array", + "items": { + "required": [ + "affiliationType", + "compact", + "dateOfUpdate", + "dateOfUpload", + "fileNames", + "providerId", + "status", + "type" + ], + "type": "object", + "properties": { + "dateOfUpload": { + "pattern": "^[12]{1}[0-9]{3}-(0[1-9]|1[0-2])-(0[1-9]|[12][0-9]|3[01])$", + "type": "string", + "format": "date" + }, + "compact": { + "type": "string", + "enum": [ + "aslp", + "octp", + "coun" + ] + }, + "downloadLinks": { + "type": "array", + "items": { + "required": [ + "fileName", + "url" + ], + "type": "object", + "properties": { + "fileName": { + "type": "string" + }, + "url": { + "type": "string" + } + } + } + }, + "providerId": { + "pattern": "[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab]{1}[0-9a-f]{3}-[0-9a-f]{12}", + "type": "string" + }, + "affiliationType": { + "type": "string", + "enum": [ + "militaryMember", + "militaryMemberSpouse" + ] + }, + "type": { + "type": "string", + "enum": [ + "militaryAffiliation" + ] + }, + "dateOfUpdate": { + "pattern": "^[12]{1}[0-9]{3}-(0[1-9]|1[0-2])-(0[1-9]|[12][0-9]|3[01])$", + "type": "string", + "format": "date" + }, + "fileNames": { + "type": "array", + "items": { + "type": "string" + } }, "status": { "type": "string", @@ -7563,285 +7380,426 @@ "inactive" ] } - } + } + } + }, + "providerId": { + "pattern": "[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab]{1}[0-9a-f]{3}-[0-9a-f]{12}", + "type": "string" + }, + "licenseStatus": { + "type": "string", + "enum": [ + "active", + "inactive" + ] + }, + "familyName": { + "maxLength": 100, + "minLength": 1, + "type": "string" + }, + "middleName": { + "maxLength": 100, + "minLength": 1, + "type": "string" + }, + "birthMonthDay": { + "pattern": "^[01]{1}[0-9]{1}-[0-3]{1}[0-9]{1}$", + "type": "string", + "format": "date" + }, + "compactConnectRegisteredEmailAddress": { + "maxLength": 100, + "minLength": 5, + "type": "string", + "format": "email" + }, + "dateOfUpdate": { + "pattern": "^[12]{1}[0-9]{3}-(0[1-9]|1[0-2])-(0[1-9]|[12][0-9]|3[01])$", + "type": "string", + "format": "date" + } + } + }, + "SandboLicenou25qiDo2Rmk": { + "required": [ + "ssn" + ], + "type": "object", + "properties": { + "ssn": { + "pattern": "^[0-9]{3}-[0-9]{2}-[0-9]{4}$", + "type": "string", + "description": "The provider's social security number" + } + } + }, + "SandboLicenr3AcSGo022kV": { + "required": [ + "compactAdverseActionsNotificationEmails", + "compactCommissionFee", + "compactOperationsTeamEmails", + "compactSummaryReportNotificationEmails", + "configuredStates", + "licenseeRegistrationEnabled" + ], + "type": "object", + "properties": { + "configuredStates": { + "type": "array", + "description": "List of states that have submitted configurations and their live status", + "items": { + "required": [ + "isLive", + "postalAbbreviation" + ], + "type": "object", + "properties": { + "postalAbbreviation": { + "type": "string", + "description": "The postal abbreviation of the jurisdiction", + "enum": [ + "al", + "ak", + "az", + "ar", + "ca", + "co", + "ct", + "de", + "dc", + "fl", + "ga", + "hi", + "id", + "il", + "in", + "ia", + "ks", + "ky", + "la", + "me", + "md", + "ma", + "mi", + "mn", + "ms", + "mo", + "mt", + "ne", + "nv", + "nh", + "nj", + "nm", + "ny", + "nc", + "nd", + "oh", + "ok", + "or", + "pa", + "pr", + "ri", + "sc", + "sd", + "tn", + "tx", + "ut", + "vt", + "va", + "vi", + "wa", + "wv", + "wi", + "wy" + ] + }, + "isLive": { + "type": "boolean", + "description": "Whether the state is live and available for registrations." + } + }, + "additionalProperties": false } }, - "licenseJurisdiction": { - "type": "string", - "enum": [ - "al", - "ak", - "az", - "ar", - "ca", - "co", - "ct", - "de", - "dc", - "fl", - "ga", - "hi", - "id", - "il", - "in", - "ia", - "ks", - "ky", - "la", - "me", - "md", - "ma", - "mi", - "mn", - "ms", - "mo", - "mt", - "ne", - "nv", - "nh", - "nj", - "nm", - "ny", - "nc", - "nd", - "oh", - "ok", - "or", - "pa", - "pr", - "ri", - "sc", - "sd", - "tn", - "tx", - "ut", - "vt", - "va", - "vi", - "wa", - "wv", - "wi", - "wy" - ] - }, - "compact": { - "type": "string", - "enum": [ - "aslp", - "octp", - "coun" - ] - }, - "npi": { - "pattern": "^[0-9]{10}$", - "type": "string" - }, - "givenName": { - "maxLength": 100, - "minLength": 1, - "type": "string" + "compactCommissionFee": { + "required": [ + "feeAmount", + "feeType" + ], + "type": "object", + "properties": { + "feeAmount": { + "minimum": 0, + "type": "number" + }, + "feeType": { + "type": "string", + "enum": [ + "FLAT_RATE" + ] + } + }, + "additionalProperties": false }, - "compactEligibility": { - "type": "string", - "enum": [ - "eligible", - "ineligible" - ] + "compactSummaryReportNotificationEmails": { + "maxItems": 10, + "minItems": 1, + "uniqueItems": true, + "type": "array", + "description": "List of email addresses for summary report notifications", + "items": { + "type": "string", + "format": "email" + } }, - "jurisdictionUploadedCompactEligibility": { - "type": "string", - "enum": [ - "eligible", - "ineligible" - ] + "compactAdverseActionsNotificationEmails": { + "maxItems": 10, + "minItems": 1, + "uniqueItems": true, + "type": "array", + "description": "List of email addresses for adverse actions notifications", + "items": { + "type": "string", + "format": "email" + } }, - "dateOfBirth": { - "pattern": "^[12]{1}[0-9]{3}-(0[1-9]|1[0-2])-(0[1-9]|[12][0-9]|3[01])$", - "type": "string", - "format": "date" + "licenseeRegistrationEnabled": { + "type": "boolean", + "description": "Denotes whether licensee registration is enabled" }, - "jurisdictionUploadedLicenseStatus": { - "type": "string", - "enum": [ - "active", - "inactive" - ] + "transactionFeeConfiguration": { + "type": "object", + "properties": { + "licenseeCharges": { + "required": [ + "active", + "chargeAmount", + "chargeType" + ], + "type": "object", + "properties": { + "chargeType": { + "type": "string", + "description": "The type of transaction fee charge", + "enum": [ + "FLAT_FEE_PER_PRIVILEGE" + ] + }, + "active": { + "type": "boolean", + "description": "Whether the compact is charging licensees transaction fees" + }, + "chargeAmount": { + "minimum": 0, + "type": "number", + "description": "The amount to charge per privilege purchased" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false }, - "privilegeJurisdictions": { + "compactOperationsTeamEmails": { + "maxItems": 10, + "minItems": 1, + "uniqueItems": true, "type": "array", + "description": "List of email addresses for operations team notifications", "items": { "type": "string", - "enum": [ - "al", - "ak", - "az", - "ar", - "ca", - "co", - "ct", - "de", - "dc", - "fl", - "ga", - "hi", - "id", - "il", - "in", - "ia", - "ks", - "ky", - "la", - "me", - "md", - "ma", - "mi", - "mn", - "ms", - "mo", - "mt", - "ne", - "nv", - "nh", - "nj", - "nm", - "ny", - "nc", - "nd", - "oh", - "ok", - "or", - "pa", - "pr", - "ri", - "sc", - "sd", - "tn", - "tx", - "ut", - "vt", - "va", - "vi", - "wa", - "wv", - "wi", - "wy" - ] + "format": "email" } - }, - "type": { + } + }, + "additionalProperties": false + }, + "SandboLicenleNKK47YnPrM": { + "required": [ + "compact", + "providerId", + "recaptchaToken", + "recoveryToken" + ], + "type": "object", + "properties": { + "compact": { "type": "string", + "description": "Compact abbreviation", "enum": [ - "provider" + "aslp", + "octp", + "coun" ] }, - "suffix": { - "maxLength": 100, + "providerId": { + "pattern": "[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab]{1}[0-9a-f]{3}-[0-9a-f]{12}", + "type": "string", + "description": "Provider UUID" + }, + "recaptchaToken": { "minLength": 1, - "type": "string" + "type": "string", + "description": "ReCAPTCHA token for verification" }, - "currentHomeJurisdiction": { + "recoveryToken": { + "maxLength": 256, + "minLength": 1, "type": "string", - "description": "The current jurisdiction postal abbreviation if known.", - "enum": [ - "al", - "ak", - "az", - "ar", - "ca", - "co", - "ct", - "de", - "dc", - "fl", - "ga", - "hi", - "id", - "il", - "in", - "ia", - "ks", - "ky", - "la", - "me", - "md", - "ma", - "mi", - "mn", - "ms", - "mo", - "mt", - "ne", - "nv", - "nh", - "nj", - "nm", - "ny", - "nc", - "nd", - "oh", - "ok", - "or", - "pa", - "pr", - "ri", - "sc", - "sd", - "tn", - "tx", - "ut", - "vt", - "va", - "vi", - "wa", - "wv", - "wi", - "wy", - "other", - "unknown" - ] + "description": "Recovery token from the email link" + } + }, + "additionalProperties": false + }, + "SandboLicenJ5jPsuhNIklP": { + "required": [ + "pagination", + "providers" + ], + "type": "object", + "properties": { + "pagination": { + "type": "object", + "properties": { + "prevLastKey": { + "maxLength": 1024, + "minLength": 1, + "type": "object" + }, + "lastKey": { + "maxLength": 1024, + "minLength": 1, + "type": "object" + }, + "pageSize": { + "maximum": 100, + "minimum": 5, + "type": "integer" + } + } }, - "licenses": { + "query": { + "type": "object", + "properties": { + "providerId": { + "pattern": "[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab]{1}[0-9a-f]{3}-[0-9a-f]{12}", + "type": "string", + "description": "Internal UUID for the provider" + }, + "jurisdiction": { + "type": "string", + "description": "Filter for providers with privilege/license in a jurisdiction", + "enum": [ + "al", + "ak", + "az", + "ar", + "ca", + "co", + "ct", + "de", + "dc", + "fl", + "ga", + "hi", + "id", + "il", + "in", + "ia", + "ks", + "ky", + "la", + "me", + "md", + "ma", + "mi", + "mn", + "ms", + "mo", + "mt", + "ne", + "nv", + "nh", + "nj", + "nm", + "ny", + "nc", + "nd", + "oh", + "ok", + "or", + "pa", + "pr", + "ri", + "sc", + "sd", + "tn", + "tx", + "ut", + "vt", + "va", + "vi", + "wa", + "wv", + "wi", + "wy" + ] + }, + "givenName": { + "maxLength": 100, + "type": "string", + "description": "Filter for providers with a given name" + }, + "familyName": { + "maxLength": 100, + "type": "string", + "description": "Filter for providers with a family name" + } + } + }, + "sorting": { + "required": [ + "key" + ], + "type": "object", + "properties": { + "key": { + "type": "string", + "description": "The key to sort results by", + "enum": [ + "dateOfUpdate", + "familyName" + ] + }, + "direction": { + "type": "string", + "description": "Direction to sort results by", + "enum": [ + "ascending", + "descending" + ] + } + }, + "description": "How to sort results" + }, + "providers": { + "maxLength": 100, "type": "array", "items": { "required": [ "compact", - "compactEligibility", - "dateOfExpiration", - "dateOfIssuance", - "dateOfRenewal", - "dateOfUpdate", "familyName", "givenName", - "history", - "homeAddressCity", - "homeAddressPostalCode", - "homeAddressState", - "homeAddressStreet1", - "jurisdiction", - "jurisdictionUploadedCompactEligibility", - "jurisdictionUploadedLicenseStatus", - "licenseStatus", - "licenseType", - "middleName", + "licenseJurisdiction", + "privilegeJurisdictions", "providerId", "type" - ], - "type": "object", - "properties": { - "compact": { - "type": "string", - "enum": [ - "aslp", - "octp", - "coun" - ] - }, - "homeAddressStreet2": { - "maxLength": 100, - "minLength": 1, - "type": "string" - }, - "jurisdiction": { + ], + "type": "object", + "properties": { + "licenseJurisdiction": { "type": "string", "enum": [ "al", @@ -7899,805 +7857,921 @@ "wy" ] }, - "homeAddressStreet1": { - "maxLength": 100, - "minLength": 2, - "type": "string" - }, - "type": { - "type": "string", - "enum": [ - "license-home" - ] - }, - "suffix": { - "maxLength": 100, - "minLength": 1, - "type": "string" - }, - "dateOfIssuance": { - "pattern": "^[12]{1}[0-9]{3}-(0[1-9]|1[0-2])-(0[1-9]|[12][0-9]|3[01])$", - "type": "string", - "format": "date" - }, - "licenseType": { + "compact": { "type": "string", "enum": [ - "audiologist", - "speech-language pathologist", - "occupational therapist", - "occupational therapy assistant", - "licensed professional counselor" + "aslp", + "octp", + "coun" ] }, - "emailAddress": { - "maxLength": 100, - "minLength": 5, - "type": "string", - "format": "email" - }, - "dateOfExpiration": { - "pattern": "^[12]{1}[0-9]{3}-(0[1-9]|1[0-2])-(0[1-9]|[12][0-9]|3[01])$", - "type": "string", - "format": "date" - }, - "homeAddressState": { - "maxLength": 100, - "minLength": 2, - "type": "string" - }, "providerId": { "pattern": "[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab]{1}[0-9a-f]{3}-[0-9a-f]{12}", "type": "string" }, - "dateOfRenewal": { - "pattern": "^[12]{1}[0-9]{3}-(0[1-9]|1[0-2])-(0[1-9]|[12][0-9]|3[01])$", - "type": "string", - "format": "date" + "npi": { + "pattern": "^[0-9]{10}$", + "type": "string" }, - "familyName": { + "givenName": { "maxLength": 100, "minLength": 1, "type": "string" }, - "homeAddressCity": { + "familyName": { "maxLength": 100, - "minLength": 2, + "minLength": 1, "type": "string" }, - "licenseNumber": { + "middleName": { "maxLength": 100, "minLength": 1, "type": "string" }, - "npi": { - "pattern": "^[0-9]{10}$", - "type": "string" - }, - "homeAddressPostalCode": { - "maxLength": 7, - "minLength": 5, - "type": "string" + "privilegeJurisdictions": { + "type": "array", + "items": { + "type": "string", + "enum": [ + "al", + "ak", + "az", + "ar", + "ca", + "co", + "ct", + "de", + "dc", + "fl", + "ga", + "hi", + "id", + "il", + "in", + "ia", + "ks", + "ky", + "la", + "me", + "md", + "ma", + "mi", + "mn", + "ms", + "mo", + "mt", + "ne", + "nv", + "nh", + "nj", + "nm", + "ny", + "nc", + "nd", + "oh", + "ok", + "or", + "pa", + "pr", + "ri", + "sc", + "sd", + "tn", + "tx", + "ut", + "vt", + "va", + "vi", + "wa", + "wv", + "wi", + "wy" + ] + } }, - "compactEligibility": { + "type": { "type": "string", "enum": [ - "eligible", - "ineligible" + "provider" ] }, - "givenName": { + "suffix": { "maxLength": 100, "minLength": 1, "type": "string" }, - "jurisdictionUploadedCompactEligibility": { + "currentHomeJurisdiction": { "type": "string", + "description": "The current jurisdiction postal abbreviation if known.", "enum": [ - "eligible", - "ineligible" + "al", + "ak", + "az", + "ar", + "ca", + "co", + "ct", + "de", + "dc", + "fl", + "ga", + "hi", + "id", + "il", + "in", + "ia", + "ks", + "ky", + "la", + "me", + "md", + "ma", + "mi", + "mn", + "ms", + "mo", + "mt", + "ne", + "nv", + "nh", + "nj", + "nm", + "ny", + "nc", + "nd", + "oh", + "ok", + "or", + "pa", + "pr", + "ri", + "sc", + "sd", + "tn", + "tx", + "ut", + "vt", + "va", + "vi", + "wa", + "wv", + "wi", + "wy", + "other", + "unknown" ] }, - "dateOfBirth": { + "dateOfUpdate": { "pattern": "^[12]{1}[0-9]{3}-(0[1-9]|1[0-2])-(0[1-9]|[12][0-9]|3[01])$", "type": "string", "format": "date" + } + } + } + } + } + }, + "SandboLicenQaBA3tB41KFn": { + "required": [ + "enabled" + ], + "type": "object", + "properties": { + "enabled": { + "type": "boolean", + "description": "Whether the feature flag is enabled" + } + } + }, + "SandboLicen8ANzOyv4fxr5": { + "required": [ + "jurisdictionAdverseActionsNotificationEmails", + "jurisdictionOperationsTeamEmails", + "jurisdictionSummaryReportNotificationEmails", + "jurisprudenceRequirements", + "licenseeRegistrationEnabled", + "privilegeFees" + ], + "type": "object", + "properties": { + "privilegeFees": { + "type": "array", + "description": "The fees for the privileges by license type", + "items": { + "required": [ + "amount", + "licenseTypeAbbreviation" + ], + "type": "object", + "properties": { + "amount": { + "minimum": 0, + "type": "number" }, - "jurisdictionUploadedLicenseStatus": { + "militaryRate": { + "description": "Optional military rate for the privilege fee.", + "oneOf": [ + { + "minimum": 0, + "type": "number" + }, + null + ] + }, + "licenseTypeAbbreviation": { "type": "string", "enum": [ - "active", - "inactive" + "aud", + "slp", + "ot", + "ota", + "lpc" ] - }, - "history": { - "type": "array", - "items": { - "required": [ - "compact", - "dateOfUpdate", - "jurisdiction", - "previous", - "type", - "updateType" - ], - "type": "object", - "properties": { - "removedValues": { - "type": "array", - "description": "List of field names that were present in the previous record but removed in the update", - "items": { - "type": "string" - } - }, - "licenseType": { - "type": "string", - "enum": [ - "audiologist", - "speech-language pathologist", - "occupational therapist", - "occupational therapy assistant", - "licensed professional counselor" - ] - }, - "compact": { - "type": "string", - "enum": [ - "aslp", - "octp", - "coun" - ] - }, - "previous": { - "required": [ - "dateOfExpiration", - "dateOfIssuance", - "dateOfRenewal", - "familyName", - "givenName", - "homeAddressCity", - "homeAddressPostalCode", - "homeAddressState", - "homeAddressStreet1", - "jurisdictionUploadedCompactEligibility", - "jurisdictionUploadedLicenseStatus", - "middleName" - ], - "type": "object", - "properties": { - "homeAddressStreet2": { - "maxLength": 100, - "minLength": 1, - "type": "string" - }, - "npi": { - "pattern": "^[0-9]{10}$", - "type": "string" - }, - "homeAddressPostalCode": { - "maxLength": 7, - "minLength": 5, - "type": "string" - }, - "givenName": { - "maxLength": 100, - "minLength": 1, - "type": "string" - }, - "homeAddressStreet1": { - "maxLength": 100, - "minLength": 2, - "type": "string" - }, - "compactEligibility": { - "type": "string", - "enum": [ - "eligible", - "ineligible" - ] - }, - "jurisdictionUploadedCompactEligibility": { - "type": "string", - "enum": [ - "eligible", - "ineligible" - ] - }, - "dateOfBirth": { - "pattern": "^[12]{1}[0-9]{3}-(0[1-9]|1[0-2])-(0[1-9]|[12][0-9]|3[01])$", - "type": "string", - "format": "date" - }, - "jurisdictionUploadedLicenseStatus": { - "type": "string", - "enum": [ - "active", - "inactive" - ] - }, - "suffix": { - "maxLength": 100, - "minLength": 1, - "type": "string" - }, - "dateOfIssuance": { - "pattern": "^[12]{1}[0-9]{3}-(0[1-9]|1[0-2])-(0[1-9]|[12][0-9]|3[01])$", - "type": "string", - "format": "date" - }, - "emailAddress": { - "maxLength": 100, - "minLength": 5, - "type": "string", - "format": "email" - }, - "dateOfExpiration": { - "pattern": "^[12]{1}[0-9]{3}-(0[1-9]|1[0-2])-(0[1-9]|[12][0-9]|3[01])$", - "type": "string", - "format": "date" - }, - "phoneNumber": { - "pattern": "^\\+[0-9]{8,15}$", - "type": "string" - }, - "homeAddressState": { - "maxLength": 100, - "minLength": 2, - "type": "string" - }, - "dateOfRenewal": { - "pattern": "^[12]{1}[0-9]{3}-(0[1-9]|1[0-2])-(0[1-9]|[12][0-9]|3[01])$", - "type": "string", - "format": "date" - }, - "licenseStatus": { - "type": "string", - "enum": [ - "active", - "inactive" - ] - }, - "familyName": { - "maxLength": 100, - "minLength": 1, - "type": "string" - }, - "homeAddressCity": { - "maxLength": 100, - "minLength": 2, - "type": "string" - }, - "licenseNumber": { - "maxLength": 100, - "minLength": 1, - "type": "string" - }, - "middleName": { - "maxLength": 100, - "minLength": 1, - "type": "string" - }, - "licenseStatusName": { - "maxLength": 100, - "minLength": 1, - "type": "string" - } - } - }, - "jurisdiction": { - "type": "string", - "enum": [ - "al", - "ak", - "az", - "ar", - "ca", - "co", - "ct", - "de", - "dc", - "fl", - "ga", - "hi", - "id", - "il", - "in", - "ia", - "ks", - "ky", - "la", - "me", - "md", - "ma", - "mi", - "mn", - "ms", - "mo", - "mt", - "ne", - "nv", - "nh", - "nj", - "nm", - "ny", - "nc", - "nd", - "oh", - "ok", - "or", - "pa", - "pr", - "ri", - "sc", - "sd", - "tn", - "tx", - "ut", - "vt", - "va", - "vi", - "wa", - "wv", - "wi", - "wy" - ] - }, - "updatedValues": { + } + }, + "additionalProperties": false + } + }, + "jurisdictionAdverseActionsNotificationEmails": { + "maxItems": 10, + "minItems": 1, + "uniqueItems": true, + "type": "array", + "description": "List of email addresses for adverse actions notifications", + "items": { + "type": "string", + "format": "email" + } + }, + "jurisdictionOperationsTeamEmails": { + "maxItems": 10, + "minItems": 1, + "uniqueItems": true, + "type": "array", + "description": "List of email addresses for operations team notifications", + "items": { + "type": "string", + "format": "email" + } + }, + "jurisprudenceRequirements": { + "required": [ + "required" + ], + "type": "object", + "properties": { + "linkToDocumentation": { + "description": "Optional link to jurisprudence documentation", + "oneOf": [ + { + "type": "string" + }, + null + ] + }, + "required": { + "type": "boolean", + "description": "Whether jurisprudence requirements exist" + } + }, + "additionalProperties": false + }, + "licenseeRegistrationEnabled": { + "type": "boolean", + "description": "Denotes whether licensee registration is enabled" + }, + "jurisdictionSummaryReportNotificationEmails": { + "maxItems": 10, + "minItems": 1, + "uniqueItems": true, + "type": "array", + "description": "List of email addresses for summary report notifications", + "items": { + "type": "string", + "format": "email" + } + } + }, + "additionalProperties": false + }, + "SandboLicenPOYfvpDMctYs": { + "required": [ + "attributes", + "permissions", + "status", + "userId" + ], + "type": "object", + "properties": { + "permissions": { + "type": "object", + "additionalProperties": { + "type": "object", + "properties": { + "actions": { + "type": "object", + "properties": { + "readPrivate": { + "type": "boolean" + }, + "admin": { + "type": "boolean" + }, + "readSSN": { + "type": "boolean" + } + } + }, + "jurisdictions": { + "type": "object", + "additionalProperties": { + "type": "object", + "properties": { + "actions": { "type": "object", "properties": { - "homeAddressStreet2": { - "maxLength": 100, - "minLength": 1, - "type": "string" - }, - "npi": { - "pattern": "^[0-9]{10}$", - "type": "string" - }, - "homeAddressPostalCode": { - "maxLength": 7, - "minLength": 5, - "type": "string" - }, - "givenName": { - "maxLength": 100, - "minLength": 1, - "type": "string" - }, - "homeAddressStreet1": { - "maxLength": 100, - "minLength": 2, - "type": "string" - }, - "compactEligibility": { - "type": "string", - "enum": [ - "eligible", - "ineligible" - ] - }, - "jurisdictionUploadedCompactEligibility": { - "type": "string", - "enum": [ - "eligible", - "ineligible" - ] - }, - "dateOfBirth": { - "pattern": "^[12]{1}[0-9]{3}-(0[1-9]|1[0-2])-(0[1-9]|[12][0-9]|3[01])$", - "type": "string", - "format": "date" - }, - "jurisdictionUploadedLicenseStatus": { - "type": "string", - "enum": [ - "active", - "inactive" - ] - }, - "suffix": { - "maxLength": 100, - "minLength": 1, - "type": "string" + "readPrivate": { + "type": "boolean" }, - "dateOfIssuance": { - "pattern": "^[12]{1}[0-9]{3}-(0[1-9]|1[0-2])-(0[1-9]|[12][0-9]|3[01])$", - "type": "string", - "format": "date" + "admin": { + "type": "boolean" }, - "emailAddress": { - "maxLength": 100, - "minLength": 5, - "type": "string", - "format": "email" + "write": { + "type": "boolean" }, - "dateOfExpiration": { - "pattern": "^[12]{1}[0-9]{3}-(0[1-9]|1[0-2])-(0[1-9]|[12][0-9]|3[01])$", - "type": "string", - "format": "date" + "readSSN": { + "type": "boolean" + } + }, + "additionalProperties": false + } + } + } + } + }, + "additionalProperties": false + } + }, + "attributes": { + "required": [ + "email", + "familyName", + "givenName" + ], + "type": "object", + "properties": { + "givenName": { + "maxLength": 100, + "minLength": 1, + "type": "string" + }, + "familyName": { + "maxLength": 100, + "minLength": 1, + "type": "string" + }, + "email": { + "maxLength": 100, + "minLength": 5, + "type": "string" + } + }, + "additionalProperties": false + }, + "userId": { + "type": "string" + }, + "status": { + "type": "string", + "enum": [ + "active", + "inactive" + ] + } + }, + "additionalProperties": false + }, + "SandboLicenZuV3mbhTgVda": { + "type": "object", + "properties": { + "message": { + "type": "string", + "description": "Message indicating success or failure" + }, + "errors": { + "type": "object", + "additionalProperties": { + "type": "object", + "additionalProperties": { + "type": "array", + "description": "List of error messages for a field", + "items": { + "type": "string" + } + }, + "description": "Errors for a specific record" + }, + "description": "Validation errors by record index" + } + } + }, + "SandboLicenPCBbBkJDJqKF": { + "type": "object", + "properties": { + "permissions": { + "type": "object", + "additionalProperties": { + "type": "object", + "properties": { + "actions": { + "type": "object", + "properties": { + "readPrivate": { + "type": "boolean" + }, + "admin": { + "type": "boolean" + }, + "readSSN": { + "type": "boolean" + } + } + }, + "jurisdictions": { + "type": "object", + "additionalProperties": { + "type": "object", + "properties": { + "actions": { + "type": "object", + "properties": { + "readPrivate": { + "type": "boolean" }, - "phoneNumber": { - "pattern": "^\\+[0-9]{8,15}$", - "type": "string" + "admin": { + "type": "boolean" }, - "homeAddressState": { - "maxLength": 100, - "minLength": 2, - "type": "string" + "write": { + "type": "boolean" }, - "dateOfRenewal": { - "pattern": "^[12]{1}[0-9]{3}-(0[1-9]|1[0-2])-(0[1-9]|[12][0-9]|3[01])$", - "type": "string", - "format": "date" + "readSSN": { + "type": "boolean" + } + }, + "additionalProperties": false + } + } + } + } + }, + "additionalProperties": false + } + } + }, + "additionalProperties": false + }, + "SandboLicen1qrFaB1TEABz": { + "required": [ + "deactivationNote" + ], + "type": "object", + "properties": { + "deactivationNote": { + "maxLength": 256, + "type": "string", + "description": "Note describing why the privilege is being deactivated" + } + }, + "additionalProperties": false + }, + "SandboLicenbiPPccVxAAsP": { + "required": [ + "affiliationType", + "fileNames" + ], + "type": "object", + "properties": { + "affiliationType": { + "type": "string", + "description": "The type of military affiliation", + "enum": [ + "militaryMember", + "militaryMemberSpouse" + ] + }, + "fileNames": { + "type": "array", + "description": "List of military affiliation file names", + "items": { + "maxLength": 150, + "type": "string", + "description": "The name of the file being uploaded" + } + } + }, + "additionalProperties": false + }, + "SandboLicenOzDeYT20T683": { + "type": "object", + "properties": { + "attributes": { + "type": "object", + "properties": { + "givenName": { + "maxLength": 100, + "minLength": 1, + "type": "string" + }, + "familyName": { + "maxLength": 100, + "minLength": 1, + "type": "string" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + "SandboLicenqiznHQW2EhfU": { + "required": [ + "status" + ], + "type": "object", + "properties": { + "status": { + "type": "string", + "description": "The status to set the military affiliation to.", + "enum": [ + "inactive" + ] + } + }, + "additionalProperties": false + }, + "SandboLicenHifSlm4QMyok": { + "required": [ + "items", + "pagination" + ], + "type": "object", + "properties": { + "pagination": { + "type": "object", + "properties": { + "prevLastKey": { + "maxLength": 1024, + "minLength": 1, + "type": "object" + }, + "lastKey": { + "maxLength": 1024, + "minLength": 1, + "type": "object" + }, + "pageSize": { + "maximum": 100, + "minimum": 5, + "type": "integer" + } + } + }, + "items": { + "maxLength": 100, + "type": "array", + "items": { + "type": "object", + "oneOf": [ + { + "required": [ + "compactAbbr", + "compactCommissionFee", + "compactName", + "isSandbox", + "paymentProcessorPublicFields", + "transactionFeeConfiguration", + "type" + ], + "type": "object", + "properties": { + "compactCommissionFee": { + "required": [ + "feeAmount", + "feeType" + ], + "type": "object", + "properties": { + "feeAmount": { + "type": "number" + }, + "feeType": { + "type": "string", + "enum": [ + "FLAT_RATE" + ] + } + } + }, + "compactAbbr": { + "type": "string", + "description": "The abbreviation of the compact" + }, + "paymentProcessorPublicFields": { + "required": [ + "apiLoginId", + "publicClientKey" + ], + "type": "object", + "properties": { + "publicClientKey": { + "type": "string", + "description": "The public client key for the payment processor" + }, + "apiLoginId": { + "type": "string", + "description": "The API login ID for the payment processor" + } + } + }, + "type": { + "type": "string", + "enum": [ + "compact" + ] + }, + "transactionFeeConfiguration": { + "required": [ + "licenseeCharges" + ], + "type": "object", + "properties": { + "licenseeCharges": { + "required": [ + "active", + "chargeAmount", + "chargeType" + ], + "type": "object", + "properties": { + "chargeType": { + "type": "string", + "description": "The type of transaction fee charge", + "enum": [ + "FLAT_FEE_PER_PRIVILEGE" + ] + }, + "active": { + "type": "boolean", + "description": "Whether the compact is charging licensees transaction fees" + }, + "chargeAmount": { + "type": "number", + "description": "The amount to charge per privilege purchased" + } + } + } + } + }, + "isSandbox": { + "type": "boolean", + "description": "Whether the compact is in sandbox mode" + }, + "compactName": { + "type": "string", + "description": "The full name of the compact" + } + } + }, + { + "required": [ + "jurisdictionName", + "jurisprudenceRequirements", + "postalAbbreviation", + "privilegeFees", + "type" + ], + "type": "object", + "properties": { + "privilegeFees": { + "type": "array", + "description": "The fees for the privileges", + "items": { + "required": [ + "amount", + "licenseTypeAbbreviation" + ], + "type": "object", + "properties": { + "amount": { + "type": "number" }, - "licenseStatus": { - "type": "string", - "enum": [ - "active", - "inactive" + "militaryRate": { + "description": "Optional military rate for the privilege fee.", + "oneOf": [ + { + "minimum": 0, + "type": "number" + }, + null ] }, - "familyName": { - "maxLength": 100, - "minLength": 1, - "type": "string" - }, - "homeAddressCity": { - "maxLength": 100, - "minLength": 2, - "type": "string" - }, - "licenseNumber": { - "maxLength": 100, - "minLength": 1, - "type": "string" - }, - "middleName": { - "maxLength": 100, - "minLength": 1, - "type": "string" - }, - "licenseStatusName": { - "maxLength": 100, - "minLength": 1, + "licenseTypeAbbreviation": { "type": "string" } } - }, - "type": { - "type": "string", - "enum": [ - "licenseUpdate" - ] - }, - "dateOfUpdate": { - "pattern": "^[12]{1}[0-9]{3}-(0[1-9]|1[0-2])-(0[1-9]|[12][0-9]|3[01])$", - "type": "string", - "format": "date" - }, - "updateType": { - "type": "string", - "enum": [ - "deactivation", - "expiration", - "issuance", - "other", - "renewal", - "encumbrance", - "homeJurisdictionChange", - "registration", - "lifting_encumbrance", - "licenseDeactivation", - "emailChange" - ] } - } - } - }, - "ssnLastFour": { - "pattern": "^[0-9]{4}$", - "type": "string" - }, - "phoneNumber": { - "pattern": "^\\+[0-9]{8,15}$", - "type": "string" - }, - "licenseStatus": { - "type": "string", - "enum": [ - "active", - "inactive" - ] - }, - "middleName": { - "maxLength": 100, - "minLength": 1, - "type": "string" - }, - "licenseStatusName": { - "maxLength": 100, - "minLength": 1, - "type": "string" - }, - "adverseActions": { - "type": "array", - "items": { - "required": [ - "actionAgainst", - "adverseActionId", - "clinicalPrivilegeActionCategory", - "compact", - "creationDate", - "dateOfUpdate", - "effectiveStartDate", - "encumbranceType", - "jurisdiction", - "licenseType", - "licenseTypeAbbreviation", - "providerId", - "type" - ], - "type": "object", - "properties": { - "compact": { - "type": "string", - "enum": [ - "aslp", - "octp", - "coun" - ] - }, - "jurisdiction": { - "type": "string", - "enum": [ - "al", - "ak", - "az", - "ar", - "ca", - "co", - "ct", - "de", - "dc", - "fl", - "ga", - "hi", - "id", - "il", - "in", - "ia", - "ks", - "ky", - "la", - "me", - "md", - "ma", - "mi", - "mn", - "ms", - "mo", - "mt", - "ne", - "nv", - "nh", - "nj", - "nm", - "ny", - "nc", - "nd", - "oh", - "ok", - "or", - "pa", - "pr", - "ri", - "sc", - "sd", - "tn", - "tx", - "ut", - "vt", - "va", - "vi", - "wa", - "wv", - "wi", - "wy" - ] - }, - "licenseTypeAbbreviation": { - "type": "string" - }, - "type": { - "type": "string", - "enum": [ - "adverseAction" - ] - }, - "creationDate": { - "pattern": "^[12]{1}[0-9]{3}-(0[1-9]|1[0-2])-(0[1-9]|[12][0-9]|3[01])$", - "type": "string", - "format": "date" - }, - "actionAgainst": { - "type": "string" - }, - "licenseType": { - "type": "string" - }, - "providerId": { - "pattern": "[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab]{1}[0-9a-f]{3}-[0-9a-f]{12}", - "type": "string" - }, - "effectiveStartDate": { - "pattern": "^[12]{1}[0-9]{3}-(0[1-9]|1[0-2])-(0[1-9]|[12][0-9]|3[01])$", - "type": "string", - "format": "date" - }, - "adverseActionId": { - "type": "string" - }, - "effectiveLiftDate": { - "pattern": "^[12]{1}[0-9]{3}-(0[1-9]|1[0-2])-(0[1-9]|[12][0-9]|3[01])$", - "type": "string", - "format": "date" - }, - "encumbranceType": { - "type": "string" - }, - "clinicalPrivilegeActionCategory": { - "type": "string" - }, - "liftingUser": { - "type": "string" - }, - "dateOfUpdate": { - "pattern": "^[12]{1}[0-9]{3}-(0[1-9]|1[0-2])-(0[1-9]|[12][0-9]|3[01])$", - "type": "string", - "format": "date" + }, + "postalAbbreviation": { + "type": "string", + "description": "The postal abbreviation of the jurisdiction" + }, + "jurisprudenceRequirements": { + "required": [ + "required" + ], + "type": "object", + "properties": { + "linkToDocumentation": { + "description": "Optional link to jurisprudence documentation", + "oneOf": [ + { + "type": "string" + }, + null + ] + }, + "required": { + "type": "boolean", + "description": "Whether jurisprudence requirements exist" + } } + }, + "jurisdictionName": { + "type": "string", + "description": "The name of the jurisdiction" + }, + "type": { + "type": "string", + "enum": [ + "jurisdiction" + ] } } - }, - "dateOfUpdate": { - "pattern": "^[12]{1}[0-9]{3}-(0[1-9]|1[0-2])-(0[1-9]|[12][0-9]|3[01])$", - "type": "string", - "format": "date" } - } + ] } - }, - "ssnLastFour": { - "pattern": "^[0-9]{4}$", - "type": "string" - }, - "dateOfExpiration": { + } + } + }, + "SandboLicen2ygiScVgBeUY": { + "required": [ + "clinicalPrivilegeActionCategory", + "encumbranceEffectiveDate", + "encumbranceType" + ], + "type": "object", + "properties": { + "encumbranceEffectiveDate": { "pattern": "^[12]{1}[0-9]{3}-(0[1-9]|1[0-2])-(0[1-9]|[12][0-9]|3[01])$", "type": "string", + "description": "The effective date of the encumbrance", "format": "date" }, - "militaryAffiliations": { - "type": "array", - "items": { - "required": [ - "affiliationType", - "compact", - "dateOfUpdate", - "dateOfUpload", - "fileNames", - "providerId", - "status", - "type" - ], - "type": "object", - "properties": { - "dateOfUpload": { - "pattern": "^[12]{1}[0-9]{3}-(0[1-9]|1[0-2])-(0[1-9]|[12][0-9]|3[01])$", - "type": "string", - "format": "date" - }, - "compact": { - "type": "string", - "enum": [ - "aslp", - "octp", - "coun" - ] - }, - "downloadLinks": { - "type": "array", - "items": { - "required": [ - "fileName", - "url" - ], - "type": "object", - "properties": { - "fileName": { - "type": "string" - }, - "url": { - "type": "string" - } - } - } - }, - "providerId": { - "pattern": "[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab]{1}[0-9a-f]{3}-[0-9a-f]{12}", - "type": "string" - }, - "affiliationType": { - "type": "string", - "enum": [ - "militaryMember", - "militaryMemberSpouse" - ] - }, - "type": { - "type": "string", - "enum": [ - "militaryAffiliation" - ] - }, - "dateOfUpdate": { - "pattern": "^[12]{1}[0-9]{3}-(0[1-9]|1[0-2])-(0[1-9]|[12][0-9]|3[01])$", - "type": "string", - "format": "date" - }, - "fileNames": { - "type": "array", - "items": { - "type": "string" - } - }, - "status": { - "type": "string", - "enum": [ - "active", - "inactive" - ] - } - } - } + "encumbranceType": { + "type": "string", + "description": "The type of encumbrance", + "enum": [ + "fine", + "reprimand", + "required supervision", + "completion of continuing education", + "public reprimand", + "probation", + "injunctive action", + "suspension", + "revocation", + "denial", + "surrender of license", + "modification of previous action-extension", + "modification of previous action-reduction", + "other monitoring", + "other adjudicated action not listed" + ] }, - "providerId": { - "pattern": "[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab]{1}[0-9a-f]{3}-[0-9a-f]{12}", + "clinicalPrivilegeActionCategory": { + "type": "string", + "description": "The category of clinical privilege action" + } + }, + "additionalProperties": false + }, + "SandboLicen6tAp6Z6DYEWf": { + "required": [ + "transactionId" + ], + "type": "object", + "properties": { + "message": { + "type": "string", + "description": "A message about the transaction" + }, + "transactionId": { + "type": "string", + "description": "The transaction id for the purchase" + } + } + }, + "SandboLicenZezoNHumKMgB": { + "required": [ + "message" + ], + "type": "object", + "properties": { + "message": { + "type": "string", + "description": "A message about the request" + } + } + }, + "SandboLiceniL5ZkCnLFgry": { + "type": "object", + "properties": { + "dateCreated": { + "type": "string", + "format": "date-time" + }, + "attestationId": { "type": "string" }, - "licenseStatus": { + "compact": { "type": "string", "enum": [ - "active", - "inactive" + "aslp", + "octp", + "coun" ] }, - "familyName": { - "maxLength": 100, - "minLength": 1, + "text": { "type": "string" }, - "middleName": { - "maxLength": 100, - "minLength": 1, + "type": { + "type": "string", + "enum": [ + "attestation" + ] + }, + "locale": { "type": "string" }, - "birthMonthDay": { - "pattern": "^[01]{1}[0-9]{1}-[0-3]{1}[0-9]{1}$", + "version": { + "type": "string" + }, + "required": { + "type": "boolean" + } + } + }, + "SandboLicenwnTHxhykfOKL": { + "required": [ + "clinicalPrivilegeActionCategory", + "encumbranceEffectiveDate", + "encumbranceType" + ], + "type": "object", + "properties": { + "encumbranceEffectiveDate": { + "pattern": "^[12]{1}[0-9]{3}-(0[1-9]|1[0-2])-(0[1-9]|[12][0-9]|3[01])$", "type": "string", + "description": "The effective date of the encumbrance", "format": "date" }, - "compactConnectRegisteredEmailAddress": { - "maxLength": 100, - "minLength": 5, + "encumbranceType": { "type": "string", - "format": "email" + "description": "The type of encumbrance", + "enum": [ + "fine", + "reprimand", + "required supervision", + "completion of continuing education", + "public reprimand", + "probation", + "injunctive action", + "suspension", + "revocation", + "denial", + "surrender of license", + "modification of previous action-extension", + "modification of previous action-reduction", + "other monitoring", + "other adjudicated action not listed" + ] }, - "dateOfUpdate": { - "pattern": "^[12]{1}[0-9]{3}-(0[1-9]|1[0-2])-(0[1-9]|[12][0-9]|3[01])$", + "clinicalPrivilegeActionCategory": { "type": "string", - "format": "date" + "description": "The category of clinical privilege action" } - } + }, + "additionalProperties": false } }, "securitySchemes": { diff --git a/backend/compact-connect/docs/internal/postman/postman-collection.json b/backend/compact-connect/docs/internal/postman/postman-collection.json index 57b6a14df..e967494ad 100644 --- a/backend/compact-connect/docs/internal/postman/postman-collection.json +++ b/backend/compact-connect/docs/internal/postman/postman-collection.json @@ -10,7 +10,7 @@ "type": "bearer" }, "info": { - "_postman_id": "21c1f358-3608-44fc-ba3e-64188c27ccaa", + "_postman_id": "55267d58-5f5c-4b65-bac1-3e2746a438d5", "description": { "content": "", "type": "text/plain" @@ -401,7 +401,7 @@ "item": [ { "event": [], - "id": "5cfbad61-25f7-434d-9c05-bb8ae6ba0f7d", + "id": "c714ce3d-4b60-4870-a75e-78ddffed1488", "name": "/v1/compacts/:compact", "protocolProfileBehavior": { "disableBodyPruning": true @@ -444,7 +444,7 @@ "response": [ { "_postman_previewlanguage": "json", - "body": "{\n \"compactAbbr\": \"\",\n \"compactAdverseActionsNotificationEmails\": [\n \"\",\n \"\"\n ],\n \"compactCommissionFee\": {\n \"feeAmount\": \"\",\n \"feeType\": \"FLAT_RATE\"\n },\n \"compactName\": \"\",\n \"compactOperationsTeamEmails\": [\n \"\",\n \"\"\n ],\n \"compactSummaryReportNotificationEmails\": [\n \"\",\n \"\"\n ],\n \"configuredStates\": [\n {\n \"isLive\": \"\",\n \"postalAbbreviation\": \"ok\"\n },\n {\n \"isLive\": \"\",\n \"postalAbbreviation\": \"hi\"\n }\n ],\n \"licenseeRegistrationEnabled\": \"\",\n \"transactionFeeConfiguration\": {\n \"licenseeCharges\": {\n \"active\": \"\",\n \"chargeAmount\": \"\",\n \"chargeType\": \"FLAT_FEE_PER_PRIVILEGE\"\n }\n }\n}", + "body": "{\n \"compactAbbr\": \"\",\n \"compactAdverseActionsNotificationEmails\": [\n \"\",\n \"\"\n ],\n \"compactCommissionFee\": {\n \"feeAmount\": \"\",\n \"feeType\": \"FLAT_RATE\"\n },\n \"compactName\": \"\",\n \"compactOperationsTeamEmails\": [\n \"\",\n \"\"\n ],\n \"compactSummaryReportNotificationEmails\": [\n \"\",\n \"\"\n ],\n \"configuredStates\": [\n {\n \"isLive\": \"\",\n \"postalAbbreviation\": \"id\"\n },\n {\n \"isLive\": \"\",\n \"postalAbbreviation\": \"oh\"\n }\n ],\n \"licenseeRegistrationEnabled\": \"\",\n \"transactionFeeConfiguration\": {\n \"licenseeCharges\": {\n \"active\": \"\",\n \"chargeAmount\": \"\",\n \"chargeType\": \"FLAT_FEE_PER_PRIVILEGE\"\n }\n }\n}", "code": 200, "cookie": [], "header": [ @@ -453,7 +453,7 @@ "value": "application/json" } ], - "id": "61afa685-7bfd-4f97-b50d-46f77cb5d60b", + "id": "3bd65b31-a388-44f8-8779-61bb80f8e9cd", "name": "200 response", "originalRequest": { "body": {}, @@ -491,7 +491,7 @@ }, { "event": [], - "id": "8ff0e43a-80ff-4579-ac44-fdc8dfce4887", + "id": "1c98671f-e222-4de9-91c8-115387f30378", "name": "/v1/compacts/:compact", "protocolProfileBehavior": { "disableBodyPruning": true @@ -505,7 +505,7 @@ "language": "json" } }, - "raw": "{\n \"compactAdverseActionsNotificationEmails\": [\n \"\"\n ],\n \"compactCommissionFee\": {\n \"feeAmount\": \"\",\n \"feeType\": \"FLAT_RATE\"\n },\n \"compactOperationsTeamEmails\": [\n \"\"\n ],\n \"compactSummaryReportNotificationEmails\": [\n \"\"\n ],\n \"configuredStates\": [\n {\n \"isLive\": \"\",\n \"postalAbbreviation\": \"tn\"\n },\n {\n \"isLive\": \"\",\n \"postalAbbreviation\": \"mt\"\n }\n ],\n \"licenseeRegistrationEnabled\": \"\",\n \"transactionFeeConfiguration\": {\n \"licenseeCharges\": {\n \"active\": \"\",\n \"chargeAmount\": \"\",\n \"chargeType\": \"FLAT_FEE_PER_PRIVILEGE\"\n }\n }\n}" + "raw": "{\n \"compactAdverseActionsNotificationEmails\": [\n \"\"\n ],\n \"compactCommissionFee\": {\n \"feeAmount\": \"\",\n \"feeType\": \"FLAT_RATE\"\n },\n \"compactOperationsTeamEmails\": [\n \"\"\n ],\n \"compactSummaryReportNotificationEmails\": [\n \"\"\n ],\n \"configuredStates\": [\n {\n \"isLive\": \"\",\n \"postalAbbreviation\": \"nv\"\n },\n {\n \"isLive\": \"\",\n \"postalAbbreviation\": \"ut\"\n }\n ],\n \"licenseeRegistrationEnabled\": \"\",\n \"transactionFeeConfiguration\": {\n \"licenseeCharges\": {\n \"active\": \"\",\n \"chargeAmount\": \"\",\n \"chargeType\": \"FLAT_FEE_PER_PRIVILEGE\"\n }\n }\n}" }, "description": {}, "header": [ @@ -556,7 +556,7 @@ "value": "application/json" } ], - "id": "0b2cd131-7a13-4e36-987b-a10fd1b57f90", + "id": "a06ab605-51d9-4de1-b2ee-f21e58176312", "name": "200 response", "originalRequest": { "body": { @@ -567,7 +567,7 @@ "language": "json" } }, - "raw": "{\n \"compactAdverseActionsNotificationEmails\": [\n \"\"\n ],\n \"compactCommissionFee\": {\n \"feeAmount\": \"\",\n \"feeType\": \"FLAT_RATE\"\n },\n \"compactOperationsTeamEmails\": [\n \"\"\n ],\n \"compactSummaryReportNotificationEmails\": [\n \"\"\n ],\n \"configuredStates\": [\n {\n \"isLive\": \"\",\n \"postalAbbreviation\": \"tn\"\n },\n {\n \"isLive\": \"\",\n \"postalAbbreviation\": \"mt\"\n }\n ],\n \"licenseeRegistrationEnabled\": \"\",\n \"transactionFeeConfiguration\": {\n \"licenseeCharges\": {\n \"active\": \"\",\n \"chargeAmount\": \"\",\n \"chargeType\": \"FLAT_FEE_PER_PRIVILEGE\"\n }\n }\n}" + "raw": "{\n \"compactAdverseActionsNotificationEmails\": [\n \"\"\n ],\n \"compactCommissionFee\": {\n \"feeAmount\": \"\",\n \"feeType\": \"FLAT_RATE\"\n },\n \"compactOperationsTeamEmails\": [\n \"\"\n ],\n \"compactSummaryReportNotificationEmails\": [\n \"\"\n ],\n \"configuredStates\": [\n {\n \"isLive\": \"\",\n \"postalAbbreviation\": \"nv\"\n },\n {\n \"isLive\": \"\",\n \"postalAbbreviation\": \"ut\"\n }\n ],\n \"licenseeRegistrationEnabled\": \"\",\n \"transactionFeeConfiguration\": {\n \"licenseeCharges\": {\n \"active\": \"\",\n \"chargeAmount\": \"\",\n \"chargeType\": \"FLAT_FEE_PER_PRIVILEGE\"\n }\n }\n}" }, "header": [ { @@ -613,7 +613,7 @@ "item": [ { "event": [], - "id": "5a3fe9b0-b924-431f-b25f-8fa90d50921b", + "id": "cad967ca-ccd6-4cb1-99e3-596881eb3e02", "name": "/v1/compacts/:compact/attestations/:attestationId", "protocolProfileBehavior": { "disableBodyPruning": true @@ -668,7 +668,7 @@ "response": [ { "_postman_previewlanguage": "json", - "body": "{\n \"dateCreated\": \"\",\n \"attestationId\": \"\",\n \"compact\": \"aslp\",\n \"text\": \"\",\n \"type\": \"attestation\",\n \"locale\": \"\",\n \"version\": \"\",\n \"required\": \"\"\n}", + "body": "{\n \"dateCreated\": \"\",\n \"attestationId\": \"\",\n \"compact\": \"octp\",\n \"text\": \"\",\n \"type\": \"attestation\",\n \"locale\": \"\",\n \"version\": \"\",\n \"required\": \"\"\n}", "code": 200, "cookie": [], "header": [ @@ -677,7 +677,7 @@ "value": "application/json" } ], - "id": "172bb999-9203-4fea-9de3-6917c7261052", + "id": "e60d96bc-a3c2-4a14-a6f9-31c30709d49e", "name": "200 response", "originalRequest": { "body": {}, @@ -729,7 +729,7 @@ "item": [ { "event": [], - "id": "3e4694da-2c38-47bf-a1f7-6802d3718d83", + "id": "9b92110a-29a4-40de-acb2-1f4d1187e311", "name": "/v1/compacts/:compact/credentials/payment-processor", "protocolProfileBehavior": { "disableBodyPruning": true @@ -796,7 +796,7 @@ "value": "application/json" } ], - "id": "c4f67b10-919d-48ed-b4f6-4398818a5058", + "id": "98d52611-e522-450b-984f-17a06bf6dbdb", "name": "200 response", "originalRequest": { "body": { @@ -858,7 +858,7 @@ "item": [ { "event": [], - "id": "e2e47c10-f50e-424c-9f47-819172d6cf10", + "id": "acfc6ce9-79fb-4fac-891b-150590b46978", "name": "/v1/compacts/:compact/jurisdictions", "protocolProfileBehavior": { "disableBodyPruning": true @@ -911,7 +911,7 @@ "value": "application/json" } ], - "id": "fba97345-dc13-4712-a43c-5950d2a31256", + "id": "d07334cb-fbaf-4fa0-8f17-11ddd804c881", "name": "200 response", "originalRequest": { "body": {}, @@ -956,7 +956,7 @@ "item": [ { "event": [], - "id": "9c935f0b-534d-40a4-82b4-4d532b06a0cd", + "id": "8f8b9467-45e6-472d-90b9-7d1ca3b917a7", "name": "/v1/compacts/:compact/jurisdictions/:jurisdiction/licenses", "protocolProfileBehavior": { "disableBodyPruning": true @@ -1021,7 +1021,7 @@ "value": "application/json" } ], - "id": "a4d7b805-553c-4e8f-938f-ffc5b5cf6968", + "id": "dc3ab7e1-7be6-485c-844f-a2dd19963d74", "name": "200 response", "originalRequest": { "body": {}, @@ -1060,7 +1060,7 @@ }, { "_postman_previewlanguage": "json", - "body": "{\n \"message\": \"\",\n \"errors\": {\n \"key_0\": {\n \"key_0\": [\n \"\",\n \"\"\n ],\n \"key_1\": [\n \"\",\n \"\"\n ]\n }\n }\n}", + "body": "{\n \"message\": \"\",\n \"errors\": {\n \"Ut1\": {\n \"nona75\": [\n \"\",\n \"\"\n ]\n },\n \"ex_c_\": {\n \"amet_e\": [\n \"\",\n \"\"\n ]\n },\n \"dolore_9\": {\n \"incididunt9c\": [\n \"\",\n \"\"\n ],\n \"minim_e3\": [\n \"\",\n \"\"\n ]\n }\n }\n}", "code": 400, "cookie": [], "header": [ @@ -1069,7 +1069,7 @@ "value": "application/json" } ], - "id": "da8d969d-810d-4979-afad-3fa6e11eebb1", + "id": "09f2342d-7f8e-4c64-8296-d28e30066351", "name": "400 response", "originalRequest": { "body": {}, @@ -1138,7 +1138,7 @@ } } ], - "id": "7dc90250-ae38-49e6-809c-1092ff63f9a5", + "id": "10ff919f-8754-407c-b76b-2bab24f195cd", "name": "/v1/compacts/:compact/jurisdictions/:jurisdiction/licenses/bulk-upload", "protocolProfileBehavior": { "disableBodyPruning": true @@ -1195,7 +1195,7 @@ "response": [ { "_postman_previewlanguage": "json", - "body": "{\n \"upload\": {\n \"fields\": {\n \"key_0\": \"\",\n \"key_1\": \"\"\n },\n \"url\": \"\"\n }\n}", + "body": "{\n \"upload\": {\n \"fields\": {\n \"eta45\": \"\",\n \"anim_11\": \"\"\n },\n \"url\": \"\"\n }\n}", "code": 200, "cookie": [], "header": [ @@ -1204,7 +1204,7 @@ "value": "application/json" } ], - "id": "fb35ec88-c696-44d4-b5d6-8c46225f1aca", + "id": "ad3d5d8d-54ba-4d47-b488-a5d18c7c4744", "name": "200 response", "originalRequest": { "body": {}, @@ -1264,7 +1264,7 @@ "item": [ { "event": [], - "id": "836a433b-8a55-45b0-81e1-56b3f676e7d3", + "id": "c01b8a3d-9b1b-41eb-90b2-0df5ad64df7e", "name": "/v1/compacts/:compact/providers/query", "protocolProfileBehavior": { "disableBodyPruning": true @@ -1278,7 +1278,7 @@ "language": "json" } }, - "raw": "{\n \"query\": {\n \"providerId\": \"3c82fb57-70cb-4318-8a61-d9238c00282e\",\n \"jurisdiction\": \"de\",\n \"givenName\": \"\",\n \"familyName\": \"\"\n },\n \"pagination\": {\n \"lastKey\": \"\",\n \"pageSize\": \"\"\n },\n \"sorting\": {\n \"key\": \"dateOfUpdate\",\n \"direction\": \"ascending\"\n }\n}" + "raw": "{\n \"query\": {\n \"providerId\": \"49eccb09-5890-4e21-ade5-3eeecabc7941\",\n \"jurisdiction\": \"fl\",\n \"givenName\": \"\",\n \"familyName\": \"\"\n },\n \"pagination\": {\n \"lastKey\": \"\",\n \"pageSize\": \"\"\n },\n \"sorting\": {\n \"key\": \"familyName\",\n \"direction\": \"descending\"\n }\n}" }, "description": {}, "header": [ @@ -1322,7 +1322,7 @@ "response": [ { "_postman_previewlanguage": "json", - "body": "{\n \"pagination\": {\n \"prevLastKey\": {},\n \"lastKey\": {},\n \"pageSize\": \"\"\n },\n \"providers\": [\n {\n \"birthMonthDay\": \"03-33\",\n \"compact\": \"octp\",\n \"compactEligibility\": \"eligible\",\n \"dateOfExpiration\": \"1232-06-20\",\n \"dateOfUpdate\": \"1674-12-30\",\n \"familyName\": \"\",\n \"givenName\": \"\",\n \"jurisdictionUploadedCompactEligibility\": \"ineligible\",\n \"jurisdictionUploadedLicenseStatus\": \"active\",\n \"licenseJurisdiction\": \"nv\",\n \"licenseStatus\": \"inactive\",\n \"privilegeJurisdictions\": [\n \"sd\",\n \"nd\"\n ],\n \"providerId\": \"520a2fc8-1d61-4308-b917-eb292ef16aa9\",\n \"type\": \"provider\",\n \"npi\": \"5434105039\",\n \"dateOfBirth\": \"2995-10-30\",\n \"suffix\": \"\",\n \"currentHomeJurisdiction\": \"dc\",\n \"ssnLastFour\": \"9911\",\n \"middleName\": \"\",\n \"compactConnectRegisteredEmailAddress\": \"\"\n },\n {\n \"birthMonthDay\": \"00-28\",\n \"compact\": \"coun\",\n \"compactEligibility\": \"eligible\",\n \"dateOfExpiration\": \"1200-05-19\",\n \"dateOfUpdate\": \"1758-02-15\",\n \"familyName\": \"\",\n \"givenName\": \"\",\n \"jurisdictionUploadedCompactEligibility\": \"ineligible\",\n \"jurisdictionUploadedLicenseStatus\": \"inactive\",\n \"licenseJurisdiction\": \"ar\",\n \"licenseStatus\": \"inactive\",\n \"privilegeJurisdictions\": [\n \"me\",\n \"ms\"\n ],\n \"providerId\": \"8039a08b-bcee-48f1-9d20-ae0a70cc31d3\",\n \"type\": \"provider\",\n \"npi\": \"0825863830\",\n \"dateOfBirth\": \"1466-10-21\",\n \"suffix\": \"\",\n \"currentHomeJurisdiction\": \"in\",\n \"ssnLastFour\": \"4159\",\n \"middleName\": \"\",\n \"compactConnectRegisteredEmailAddress\": \"\"\n }\n ],\n \"sorting\": {\n \"key\": \"dateOfUpdate\",\n \"direction\": \"ascending\"\n }\n}", + "body": "{\n \"pagination\": {\n \"prevLastKey\": {},\n \"lastKey\": {},\n \"pageSize\": \"\"\n },\n \"providers\": [\n {\n \"birthMonthDay\": \"18-18\",\n \"compact\": \"octp\",\n \"compactEligibility\": \"eligible\",\n \"dateOfExpiration\": \"1838-12-31\",\n \"dateOfUpdate\": \"1692-09-06\",\n \"familyName\": \"\",\n \"givenName\": \"\",\n \"jurisdictionUploadedCompactEligibility\": \"eligible\",\n \"jurisdictionUploadedLicenseStatus\": \"active\",\n \"licenseJurisdiction\": \"wv\",\n \"licenseStatus\": \"inactive\",\n \"privilegeJurisdictions\": [\n \"or\",\n \"va\"\n ],\n \"providerId\": \"cab15fde-d245-4c6e-9345-1fcfdf4dd861\",\n \"type\": \"provider\",\n \"npi\": \"9800547545\",\n \"dateOfBirth\": \"2477-07-20\",\n \"suffix\": \"\",\n \"currentHomeJurisdiction\": \"ca\",\n \"ssnLastFour\": \"7864\",\n \"middleName\": \"\",\n \"compactConnectRegisteredEmailAddress\": \"\"\n },\n {\n \"birthMonthDay\": \"14-21\",\n \"compact\": \"octp\",\n \"compactEligibility\": \"eligible\",\n \"dateOfExpiration\": \"1938-02-31\",\n \"dateOfUpdate\": \"1568-04-31\",\n \"familyName\": \"\",\n \"givenName\": \"\",\n \"jurisdictionUploadedCompactEligibility\": \"eligible\",\n \"jurisdictionUploadedLicenseStatus\": \"active\",\n \"licenseJurisdiction\": \"ne\",\n \"licenseStatus\": \"active\",\n \"privilegeJurisdictions\": [\n \"co\",\n \"ny\"\n ],\n \"providerId\": \"fa52df74-bcb7-40a7-b96a-03647f539ef6\",\n \"type\": \"provider\",\n \"npi\": \"7418073206\",\n \"dateOfBirth\": \"2305-11-30\",\n \"suffix\": \"\",\n \"currentHomeJurisdiction\": \"ks\",\n \"ssnLastFour\": \"8876\",\n \"middleName\": \"\",\n \"compactConnectRegisteredEmailAddress\": \"\"\n }\n ],\n \"sorting\": {\n \"key\": \"dateOfUpdate\",\n \"direction\": \"descending\"\n }\n}", "code": 200, "cookie": [], "header": [ @@ -1331,7 +1331,7 @@ "value": "application/json" } ], - "id": "82d9863b-9a28-47a5-aa45-64a369876ecc", + "id": "b9450f55-665a-46dc-8f48-3e416d7643b2", "name": "200 response", "originalRequest": { "body": { @@ -1342,7 +1342,7 @@ "language": "json" } }, - "raw": "{\n \"query\": {\n \"providerId\": \"3c82fb57-70cb-4318-8a61-d9238c00282e\",\n \"jurisdiction\": \"de\",\n \"givenName\": \"\",\n \"familyName\": \"\"\n },\n \"pagination\": {\n \"lastKey\": \"\",\n \"pageSize\": \"\"\n },\n \"sorting\": {\n \"key\": \"dateOfUpdate\",\n \"direction\": \"ascending\"\n }\n}" + "raw": "{\n \"query\": {\n \"providerId\": \"49eccb09-5890-4e21-ade5-3eeecabc7941\",\n \"jurisdiction\": \"fl\",\n \"givenName\": \"\",\n \"familyName\": \"\"\n },\n \"pagination\": {\n \"lastKey\": \"\",\n \"pageSize\": \"\"\n },\n \"sorting\": {\n \"key\": \"familyName\",\n \"direction\": \"descending\"\n }\n}" }, "header": [ { @@ -1390,7 +1390,7 @@ "item": [ { "event": [], - "id": "bcb54213-28e3-4dee-8a35-d52874e1749b", + "id": "a0a538c7-c323-46aa-969d-fbdfe0397550", "name": "/v1/compacts/:compact/providers/:providerId", "protocolProfileBehavior": { "disableBodyPruning": true @@ -1445,7 +1445,7 @@ "response": [ { "_postman_previewlanguage": "json", - "body": "{\n \"birthMonthDay\": \"09-29\",\n \"compact\": \"aslp\",\n \"dateOfExpiration\": \"2551-08-07\",\n \"dateOfUpdate\": \"2473-11-31\",\n \"familyName\": \"\",\n \"givenName\": \"\",\n \"licenseJurisdiction\": \"oh\",\n \"licenses\": [\n {\n \"compact\": \"coun\",\n \"compactEligibility\": \"eligible\",\n \"dateOfExpiration\": \"2422-10-19\",\n \"dateOfIssuance\": \"1176-08-27\",\n \"dateOfRenewal\": \"2158-12-07\",\n \"dateOfUpdate\": \"2503-12-31\",\n \"familyName\": \"\",\n \"givenName\": \"\",\n \"history\": [\n {\n \"compact\": \"aslp\",\n \"dateOfUpdate\": \"2760-10-30\",\n \"jurisdiction\": \"nd\",\n \"previous\": {\n \"dateOfExpiration\": \"1222-05-31\",\n \"dateOfIssuance\": \"1311-02-17\",\n \"dateOfRenewal\": \"1203-02-08\",\n \"familyName\": \"\",\n \"givenName\": \"\",\n \"homeAddressCity\": \"\",\n \"homeAddressPostalCode\": \"\",\n \"homeAddressState\": \"\",\n \"homeAddressStreet1\": \"\",\n \"jurisdictionUploadedCompactEligibility\": \"eligible\",\n \"jurisdictionUploadedLicenseStatus\": \"inactive\",\n \"middleName\": \"\",\n \"homeAddressStreet2\": \"\",\n \"npi\": \"3849686342\",\n \"compactEligibility\": \"ineligible\",\n \"dateOfBirth\": \"2224-12-22\",\n \"suffix\": \"\",\n \"emailAddress\": \"\",\n \"phoneNumber\": \"+817339777344\",\n \"licenseStatus\": \"inactive\",\n \"licenseNumber\": \"\",\n \"licenseStatusName\": \"\"\n },\n \"type\": \"licenseUpdate\",\n \"updateType\": \"licenseDeactivation\",\n \"removedValues\": [\n \"\",\n \"\"\n ],\n \"licenseType\": \"licensed professional counselor\",\n \"updatedValues\": {\n \"homeAddressStreet2\": \"\",\n \"npi\": \"1253840341\",\n \"homeAddressPostalCode\": \"\",\n \"givenName\": \"\",\n \"homeAddressStreet1\": \"\",\n \"compactEligibility\": \"eligible\",\n \"jurisdictionUploadedCompactEligibility\": \"ineligible\",\n \"dateOfBirth\": \"1741-12-01\",\n \"jurisdictionUploadedLicenseStatus\": \"active\",\n \"suffix\": \"\",\n \"dateOfIssuance\": \"2369-06-30\",\n \"emailAddress\": \"\",\n \"dateOfExpiration\": \"1500-12-09\",\n \"phoneNumber\": \"+24819955084\",\n \"homeAddressState\": \"\",\n \"dateOfRenewal\": \"2569-06-30\",\n \"licenseStatus\": \"inactive\",\n \"familyName\": \"\",\n \"homeAddressCity\": \"\",\n \"licenseNumber\": \"\",\n \"middleName\": \"\",\n \"licenseStatusName\": \"\"\n }\n },\n {\n \"compact\": \"coun\",\n \"dateOfUpdate\": \"2718-10-03\",\n \"jurisdiction\": \"wy\",\n \"previous\": {\n \"dateOfExpiration\": \"2661-12-06\",\n \"dateOfIssuance\": \"1835-09-30\",\n \"dateOfRenewal\": \"2896-01-30\",\n \"familyName\": \"\",\n \"givenName\": \"\",\n \"homeAddressCity\": \"\",\n \"homeAddressPostalCode\": \"\",\n \"homeAddressState\": \"\",\n \"homeAddressStreet1\": \"\",\n \"jurisdictionUploadedCompactEligibility\": \"ineligible\",\n \"jurisdictionUploadedLicenseStatus\": \"active\",\n \"middleName\": \"\",\n \"homeAddressStreet2\": \"\",\n \"npi\": \"9915283898\",\n \"compactEligibility\": \"eligible\",\n \"dateOfBirth\": \"2313-12-20\",\n \"suffix\": \"\",\n \"emailAddress\": \"\",\n \"phoneNumber\": \"+197882673191259\",\n \"licenseStatus\": \"inactive\",\n \"licenseNumber\": \"\",\n \"licenseStatusName\": \"\"\n },\n \"type\": \"licenseUpdate\",\n \"updateType\": \"lifting_encumbrance\",\n \"removedValues\": [\n \"\",\n \"\"\n ],\n \"licenseType\": \"occupational therapist\",\n \"updatedValues\": {\n \"homeAddressStreet2\": \"\",\n \"npi\": \"7336401807\",\n \"homeAddressPostalCode\": \"\",\n \"givenName\": \"\",\n \"homeAddressStreet1\": \"\",\n \"compactEligibility\": \"eligible\",\n \"jurisdictionUploadedCompactEligibility\": \"eligible\",\n \"dateOfBirth\": \"1160-11-04\",\n \"jurisdictionUploadedLicenseStatus\": \"inactive\",\n \"suffix\": \"\",\n \"dateOfIssuance\": \"2827-12-06\",\n \"emailAddress\": \"\",\n \"dateOfExpiration\": \"2834-02-15\",\n \"phoneNumber\": \"+3902382233\",\n \"homeAddressState\": \"\",\n \"dateOfRenewal\": \"2998-01-10\",\n \"licenseStatus\": \"active\",\n \"familyName\": \"\",\n \"homeAddressCity\": \"\",\n \"licenseNumber\": \"\",\n \"middleName\": \"\",\n \"licenseStatusName\": \"\"\n }\n }\n ],\n \"homeAddressCity\": \"\",\n \"homeAddressPostalCode\": \"\",\n \"homeAddressState\": \"\",\n \"homeAddressStreet1\": \"\",\n \"jurisdiction\": \"ri\",\n \"jurisdictionUploadedCompactEligibility\": \"eligible\",\n \"jurisdictionUploadedLicenseStatus\": \"inactive\",\n \"licenseStatus\": \"inactive\",\n \"licenseType\": \"occupational therapy assistant\",\n \"middleName\": \"\",\n \"providerId\": \"349bdce7-941e-4ff7-87bd-dd5b61f4324a\",\n \"type\": \"license-home\",\n \"homeAddressStreet2\": \"\",\n \"suffix\": \"\",\n \"emailAddress\": \"\",\n \"licenseNumber\": \"\",\n \"npi\": \"5115904651\",\n \"dateOfBirth\": \"2994-06-04\",\n \"ssnLastFour\": \"8743\",\n \"phoneNumber\": \"+35782742956\",\n \"licenseStatusName\": \"\",\n \"adverseActions\": [\n {\n \"actionAgainst\": \"\",\n \"adverseActionId\": \"\",\n \"clinicalPrivilegeActionCategory\": \"\",\n \"compact\": \"aslp\",\n \"creationDate\": \"2755-12-30\",\n \"dateOfUpdate\": \"2401-04-31\",\n \"effectiveStartDate\": \"2833-11-07\",\n \"encumbranceType\": \"\",\n \"jurisdiction\": \"me\",\n \"licenseType\": \"\",\n \"licenseTypeAbbreviation\": \"\",\n \"providerId\": \"2219d489-f54d-474f-b4c9-78c762c288ac\",\n \"type\": \"adverseAction\",\n \"effectiveLiftDate\": \"2693-06-15\",\n \"liftingUser\": \"\"\n },\n {\n \"actionAgainst\": \"\",\n \"adverseActionId\": \"\",\n \"clinicalPrivilegeActionCategory\": \"\",\n \"compact\": \"aslp\",\n \"creationDate\": \"2573-07-02\",\n \"dateOfUpdate\": \"2676-03-23\",\n \"effectiveStartDate\": \"2871-10-31\",\n \"encumbranceType\": \"\",\n \"jurisdiction\": \"me\",\n \"licenseType\": \"\",\n \"licenseTypeAbbreviation\": \"\",\n \"providerId\": \"ac666c27-7ccb-4c53-842e-fb95ce22727f\",\n \"type\": \"adverseAction\",\n \"effectiveLiftDate\": \"2642-06-15\",\n \"liftingUser\": \"\"\n }\n ]\n },\n {\n \"compact\": \"coun\",\n \"compactEligibility\": \"ineligible\",\n \"dateOfExpiration\": \"2914-10-31\",\n \"dateOfIssuance\": \"2897-12-21\",\n \"dateOfRenewal\": \"1438-11-01\",\n \"dateOfUpdate\": \"1534-12-31\",\n \"familyName\": \"\",\n \"givenName\": \"\",\n \"history\": [\n {\n \"compact\": \"octp\",\n \"dateOfUpdate\": \"2982-10-30\",\n \"jurisdiction\": \"sd\",\n \"previous\": {\n \"dateOfExpiration\": \"1099-04-16\",\n \"dateOfIssuance\": \"1148-12-30\",\n \"dateOfRenewal\": \"1579-09-30\",\n \"familyName\": \"\",\n \"givenName\": \"\",\n \"homeAddressCity\": \"\",\n \"homeAddressPostalCode\": \"\",\n \"homeAddressState\": \"\",\n \"homeAddressStreet1\": \"\",\n \"jurisdictionUploadedCompactEligibility\": \"ineligible\",\n \"jurisdictionUploadedLicenseStatus\": \"active\",\n \"middleName\": \"\",\n \"homeAddressStreet2\": \"\",\n \"npi\": \"2133645263\",\n \"compactEligibility\": \"eligible\",\n \"dateOfBirth\": \"2653-12-30\",\n \"suffix\": \"\",\n \"emailAddress\": \"\",\n \"phoneNumber\": \"+9549304435346\",\n \"licenseStatus\": \"active\",\n \"licenseNumber\": \"\",\n \"licenseStatusName\": \"\"\n },\n \"type\": \"licenseUpdate\",\n \"updateType\": \"emailChange\",\n \"removedValues\": [\n \"\",\n \"\"\n ],\n \"licenseType\": \"occupational therapy assistant\",\n \"updatedValues\": {\n \"homeAddressStreet2\": \"\",\n \"npi\": \"9102395494\",\n \"homeAddressPostalCode\": \"\",\n \"givenName\": \"\",\n \"homeAddressStreet1\": \"\",\n \"compactEligibility\": \"eligible\",\n \"jurisdictionUploadedCompactEligibility\": \"eligible\",\n \"dateOfBirth\": \"2140-02-23\",\n \"jurisdictionUploadedLicenseStatus\": \"inactive\",\n \"suffix\": \"\",\n \"dateOfIssuance\": \"2556-10-30\",\n \"emailAddress\": \"\",\n \"dateOfExpiration\": \"1166-05-13\",\n \"phoneNumber\": \"+723613998\",\n \"homeAddressState\": \"\",\n \"dateOfRenewal\": \"1521-11-31\",\n \"licenseStatus\": \"inactive\",\n \"familyName\": \"\",\n \"homeAddressCity\": \"\",\n \"licenseNumber\": \"\",\n \"middleName\": \"\",\n \"licenseStatusName\": \"\"\n }\n },\n {\n \"compact\": \"coun\",\n \"dateOfUpdate\": \"2326-04-21\",\n \"jurisdiction\": \"va\",\n \"previous\": {\n \"dateOfExpiration\": \"1319-05-01\",\n \"dateOfIssuance\": \"1136-01-07\",\n \"dateOfRenewal\": \"1798-12-18\",\n \"familyName\": \"\",\n \"givenName\": \"\",\n \"homeAddressCity\": \"\",\n \"homeAddressPostalCode\": \"\",\n \"homeAddressState\": \"\",\n \"homeAddressStreet1\": \"\",\n \"jurisdictionUploadedCompactEligibility\": \"ineligible\",\n \"jurisdictionUploadedLicenseStatus\": \"active\",\n \"middleName\": \"\",\n \"homeAddressStreet2\": \"\",\n \"npi\": \"6131652411\",\n \"compactEligibility\": \"ineligible\",\n \"dateOfBirth\": \"2895-05-30\",\n \"suffix\": \"\",\n \"emailAddress\": \"\",\n \"phoneNumber\": \"+516316834803391\",\n \"licenseStatus\": \"active\",\n \"licenseNumber\": \"\",\n \"licenseStatusName\": \"\"\n },\n \"type\": \"licenseUpdate\",\n \"updateType\": \"emailChange\",\n \"removedValues\": [\n \"\",\n \"\"\n ],\n \"licenseType\": \"occupational therapist\",\n \"updatedValues\": {\n \"homeAddressStreet2\": \"\",\n \"npi\": \"7963314050\",\n \"homeAddressPostalCode\": \"\",\n \"givenName\": \"\",\n \"homeAddressStreet1\": \"\",\n \"compactEligibility\": \"ineligible\",\n \"jurisdictionUploadedCompactEligibility\": \"eligible\",\n \"dateOfBirth\": \"1002-12-06\",\n \"jurisdictionUploadedLicenseStatus\": \"inactive\",\n \"suffix\": \"\",\n \"dateOfIssuance\": \"1042-10-05\",\n \"emailAddress\": \"\",\n \"dateOfExpiration\": \"1291-12-30\",\n \"phoneNumber\": \"+983515171158248\",\n \"homeAddressState\": \"\",\n \"dateOfRenewal\": \"2351-03-10\",\n \"licenseStatus\": \"inactive\",\n \"familyName\": \"\",\n \"homeAddressCity\": \"\",\n \"licenseNumber\": \"\",\n \"middleName\": \"\",\n \"licenseStatusName\": \"\"\n }\n }\n ],\n \"homeAddressCity\": \"\",\n \"homeAddressPostalCode\": \"\",\n \"homeAddressState\": \"\",\n \"homeAddressStreet1\": \"\",\n \"jurisdiction\": \"la\",\n \"jurisdictionUploadedCompactEligibility\": \"eligible\",\n \"jurisdictionUploadedLicenseStatus\": \"active\",\n \"licenseStatus\": \"active\",\n \"licenseType\": \"audiologist\",\n \"middleName\": \"\",\n \"providerId\": \"eca478f3-343c-4fc1-83c9-97faf27b6990\",\n \"type\": \"license-home\",\n \"homeAddressStreet2\": \"\",\n \"suffix\": \"\",\n \"emailAddress\": \"\",\n \"licenseNumber\": \"\",\n \"npi\": \"5472385917\",\n \"dateOfBirth\": \"2389-11-29\",\n \"ssnLastFour\": \"5264\",\n \"phoneNumber\": \"+955712482678352\",\n \"licenseStatusName\": \"\",\n \"adverseActions\": [\n {\n \"actionAgainst\": \"\",\n \"adverseActionId\": \"\",\n \"clinicalPrivilegeActionCategory\": \"\",\n \"compact\": \"octp\",\n \"creationDate\": \"2442-11-27\",\n \"dateOfUpdate\": \"1692-12-30\",\n \"effectiveStartDate\": \"1653-11-07\",\n \"encumbranceType\": \"\",\n \"jurisdiction\": \"wi\",\n \"licenseType\": \"\",\n \"licenseTypeAbbreviation\": \"\",\n \"providerId\": \"504c0320-3a92-431e-81fb-692dbca7d66f\",\n \"type\": \"adverseAction\",\n \"effectiveLiftDate\": \"2740-01-30\",\n \"liftingUser\": \"\"\n },\n {\n \"actionAgainst\": \"\",\n \"adverseActionId\": \"\",\n \"clinicalPrivilegeActionCategory\": \"\",\n \"compact\": \"coun\",\n \"creationDate\": \"1491-11-06\",\n \"dateOfUpdate\": \"1660-07-30\",\n \"effectiveStartDate\": \"2658-12-30\",\n \"encumbranceType\": \"\",\n \"jurisdiction\": \"ks\",\n \"licenseType\": \"\",\n \"licenseTypeAbbreviation\": \"\",\n \"providerId\": \"b34ce427-aa42-49c5-ad6a-40b7b041c081\",\n \"type\": \"adverseAction\",\n \"effectiveLiftDate\": \"2291-12-03\",\n \"liftingUser\": \"\"\n }\n ]\n }\n ],\n \"militaryAffiliations\": [\n {\n \"affiliationType\": \"militaryMemberSpouse\",\n \"compact\": \"coun\",\n \"dateOfUpdate\": \"1051-02-01\",\n \"dateOfUpload\": \"1673-12-20\",\n \"fileNames\": [\n \"\",\n \"\"\n ],\n \"providerId\": \"40af7950-c543-4022-bb09-1b27c8b55ffb\",\n \"status\": \"inactive\",\n \"type\": \"militaryAffiliation\",\n \"downloadLinks\": [\n {\n \"fileName\": \"\",\n \"url\": \"\"\n },\n {\n \"fileName\": \"\",\n \"url\": \"\"\n }\n ]\n },\n {\n \"affiliationType\": \"militaryMemberSpouse\",\n \"compact\": \"coun\",\n \"dateOfUpdate\": \"1650-10-30\",\n \"dateOfUpload\": \"2598-12-16\",\n \"fileNames\": [\n \"\",\n \"\"\n ],\n \"providerId\": \"214b459e-8ce1-421f-bc29-5fee8820f97f\",\n \"status\": \"inactive\",\n \"type\": \"militaryAffiliation\",\n \"downloadLinks\": [\n {\n \"fileName\": \"\",\n \"url\": \"\"\n },\n {\n \"fileName\": \"\",\n \"url\": \"\"\n }\n ]\n }\n ],\n \"privilegeJurisdictions\": [\n \"nm\",\n \"sc\"\n ],\n \"privileges\": [\n {\n \"administratorSetStatus\": \"inactive\",\n \"attestations\": [\n {\n \"attestationId\": \"\",\n \"version\": \"\"\n },\n {\n \"attestationId\": \"\",\n \"version\": \"\"\n }\n ],\n \"compact\": \"octp\",\n \"compactTransactionId\": \"\",\n \"dateOfExpiration\": \"2496-10-10\",\n \"dateOfIssuance\": \"1545-04-21\",\n \"dateOfRenewal\": \"1364-09-05\",\n \"dateOfUpdate\": \"1208-12-02\",\n \"history\": [\n {\n \"compact\": \"coun\",\n \"dateOfUpdate\": \"2871-05-30\",\n \"jurisdiction\": \"de\",\n \"previous\": {\n \"administratorSetStatus\": \"active\",\n \"attestations\": [\n {\n \"attestationId\": \"\",\n \"version\": \"\"\n },\n {\n \"attestationId\": \"\",\n \"version\": \"\"\n }\n ],\n \"compactTransactionId\": \"\",\n \"dateOfExpiration\": \"2807-12-11\",\n \"dateOfIssuance\": \"2958-05-30\",\n \"dateOfRenewal\": \"2653-06-29\",\n \"dateOfUpdate\": \"2610-11-31\",\n \"licenseJurisdiction\": \"hi\",\n \"privilegeId\": \"\",\n \"compact\": \"octp\",\n \"jurisdiction\": \"nj\",\n \"type\": \"privilege\",\n \"providerId\": \"75252a5a-0e7f-43f7-af44-6315b72533af\",\n \"status\": \"inactive\"\n },\n \"type\": \"privilegeUpdate\",\n \"updateType\": \"other\",\n \"removedValues\": [\n \"\",\n \"\"\n ],\n \"licenseType\": \"occupational therapist\",\n \"updatedValues\": {\n \"licenseJurisdiction\": \"co\",\n \"compact\": \"aslp\",\n \"jurisdiction\": \"me\",\n \"attestations\": [\n {\n \"attestationId\": \"\",\n \"version\": \"\"\n },\n {\n \"attestationId\": \"\",\n \"version\": \"\"\n }\n ],\n \"type\": \"privilege\",\n \"compactTransactionId\": \"\",\n \"dateOfIssuance\": \"1884-10-31\",\n \"administratorSetStatus\": \"active\",\n \"dateOfExpiration\": \"1902-11-10\",\n \"privilegeId\": \"\",\n \"providerId\": \"6ab0ae00-a913-4420-aaa1-d145c6f750ee\",\n \"dateOfRenewal\": \"2116-03-30\",\n \"dateOfUpdate\": \"1305-01-30\",\n \"status\": \"active\"\n }\n },\n {\n \"compact\": \"octp\",\n \"dateOfUpdate\": \"2586-08-19\",\n \"jurisdiction\": \"fl\",\n \"previous\": {\n \"administratorSetStatus\": \"active\",\n \"attestations\": [\n {\n \"attestationId\": \"\",\n \"version\": \"\"\n },\n {\n \"attestationId\": \"\",\n \"version\": \"\"\n }\n ],\n \"compactTransactionId\": \"\",\n \"dateOfExpiration\": \"2133-11-03\",\n \"dateOfIssuance\": \"2079-11-06\",\n \"dateOfRenewal\": \"1912-12-31\",\n \"dateOfUpdate\": \"1305-03-06\",\n \"licenseJurisdiction\": \"vt\",\n \"privilegeId\": \"\",\n \"compact\": \"aslp\",\n \"jurisdiction\": \"nc\",\n \"type\": \"privilege\",\n \"providerId\": \"bf7ab190-6ed1-4692-b1ff-b4c762569494\",\n \"status\": \"active\"\n },\n \"type\": \"privilegeUpdate\",\n \"updateType\": \"emailChange\",\n \"removedValues\": [\n \"\",\n \"\"\n ],\n \"licenseType\": \"occupational therapy assistant\",\n \"updatedValues\": {\n \"licenseJurisdiction\": \"ks\",\n \"compact\": \"aslp\",\n \"jurisdiction\": \"hi\",\n \"attestations\": [\n {\n \"attestationId\": \"\",\n \"version\": \"\"\n },\n {\n \"attestationId\": \"\",\n \"version\": \"\"\n }\n ],\n \"type\": \"privilege\",\n \"compactTransactionId\": \"\",\n \"dateOfIssuance\": \"1230-10-02\",\n \"administratorSetStatus\": \"active\",\n \"dateOfExpiration\": \"1140-07-06\",\n \"privilegeId\": \"\",\n \"providerId\": \"e183902c-0bf7-48dd-8fc0-f3c7f35417d5\",\n \"dateOfRenewal\": \"1971-10-30\",\n \"dateOfUpdate\": \"1785-10-31\",\n \"status\": \"active\"\n }\n }\n ],\n \"jurisdiction\": \"dc\",\n \"licenseJurisdiction\": \"il\",\n \"licenseType\": \"licensed professional counselor\",\n \"privilegeId\": \"\",\n \"providerId\": \"839bdc13-0a83-452d-8418-a5142124772d\",\n \"status\": \"active\",\n \"type\": \"privilege\",\n \"adverseActions\": [\n {\n \"actionAgainst\": \"\",\n \"adverseActionId\": \"\",\n \"clinicalPrivilegeActionCategory\": \"\",\n \"compact\": \"octp\",\n \"creationDate\": \"1050-10-18\",\n \"dateOfUpdate\": \"1670-01-31\",\n \"effectiveStartDate\": \"1520-05-31\",\n \"encumbranceType\": \"\",\n \"jurisdiction\": \"ky\",\n \"licenseType\": \"\",\n \"licenseTypeAbbreviation\": \"\",\n \"providerId\": \"ebe6c8f1-70a9-468e-bc5e-a4bbd188893d\",\n \"type\": \"adverseAction\",\n \"effectiveLiftDate\": \"1290-01-26\",\n \"liftingUser\": \"\"\n },\n {\n \"actionAgainst\": \"\",\n \"adverseActionId\": \"\",\n \"clinicalPrivilegeActionCategory\": \"\",\n \"compact\": \"aslp\",\n \"creationDate\": \"2122-08-31\",\n \"dateOfUpdate\": \"1461-11-07\",\n \"effectiveStartDate\": \"1709-11-31\",\n \"encumbranceType\": \"\",\n \"jurisdiction\": \"mt\",\n \"licenseType\": \"\",\n \"licenseTypeAbbreviation\": \"\",\n \"providerId\": \"790dec56-9655-452c-a00a-225907b28e1f\",\n \"type\": \"adverseAction\",\n \"effectiveLiftDate\": \"1627-12-08\",\n \"liftingUser\": \"\"\n }\n ]\n },\n {\n \"administratorSetStatus\": \"active\",\n \"attestations\": [\n {\n \"attestationId\": \"\",\n \"version\": \"\"\n },\n {\n \"attestationId\": \"\",\n \"version\": \"\"\n }\n ],\n \"compact\": \"octp\",\n \"compactTransactionId\": \"\",\n \"dateOfExpiration\": \"1655-10-06\",\n \"dateOfIssuance\": \"1307-01-30\",\n \"dateOfRenewal\": \"1641-10-30\",\n \"dateOfUpdate\": \"2282-05-25\",\n \"history\": [\n {\n \"compact\": \"aslp\",\n \"dateOfUpdate\": \"2733-03-22\",\n \"jurisdiction\": \"tx\",\n \"previous\": {\n \"administratorSetStatus\": \"active\",\n \"attestations\": [\n {\n \"attestationId\": \"\",\n \"version\": \"\"\n },\n {\n \"attestationId\": \"\",\n \"version\": \"\"\n }\n ],\n \"compactTransactionId\": \"\",\n \"dateOfExpiration\": \"1621-09-11\",\n \"dateOfIssuance\": \"1359-10-30\",\n \"dateOfRenewal\": \"2863-09-28\",\n \"dateOfUpdate\": \"1325-10-23\",\n \"licenseJurisdiction\": \"me\",\n \"privilegeId\": \"\",\n \"compact\": \"aslp\",\n \"jurisdiction\": \"oh\",\n \"type\": \"privilege\",\n \"providerId\": \"d3643c67-ed32-4f74-a6cf-d6e7f824c632\",\n \"status\": \"active\"\n },\n \"type\": \"privilegeUpdate\",\n \"updateType\": \"emailChange\",\n \"removedValues\": [\n \"\",\n \"\"\n ],\n \"licenseType\": \"audiologist\",\n \"updatedValues\": {\n \"licenseJurisdiction\": \"ms\",\n \"compact\": \"octp\",\n \"jurisdiction\": \"la\",\n \"attestations\": [\n {\n \"attestationId\": \"\",\n \"version\": \"\"\n },\n {\n \"attestationId\": \"\",\n \"version\": \"\"\n }\n ],\n \"type\": \"privilege\",\n \"compactTransactionId\": \"\",\n \"dateOfIssuance\": \"2567-06-21\",\n \"administratorSetStatus\": \"active\",\n \"dateOfExpiration\": \"1620-10-31\",\n \"privilegeId\": \"\",\n \"providerId\": \"87b330a0-8529-4bba-8c3f-442866bd5ef0\",\n \"dateOfRenewal\": \"2324-08-30\",\n \"dateOfUpdate\": \"2492-10-30\",\n \"status\": \"active\"\n }\n },\n {\n \"compact\": \"octp\",\n \"dateOfUpdate\": \"2653-11-26\",\n \"jurisdiction\": \"pa\",\n \"previous\": {\n \"administratorSetStatus\": \"inactive\",\n \"attestations\": [\n {\n \"attestationId\": \"\",\n \"version\": \"\"\n },\n {\n \"attestationId\": \"\",\n \"version\": \"\"\n }\n ],\n \"compactTransactionId\": \"\",\n \"dateOfExpiration\": \"1382-06-07\",\n \"dateOfIssuance\": \"1460-12-30\",\n \"dateOfRenewal\": \"1927-11-30\",\n \"dateOfUpdate\": \"1215-11-20\",\n \"licenseJurisdiction\": \"ak\",\n \"privilegeId\": \"\",\n \"compact\": \"aslp\",\n \"jurisdiction\": \"pa\",\n \"type\": \"privilege\",\n \"providerId\": \"6fcb01a4-52eb-490f-a9bb-18e20dc4237f\",\n \"status\": \"inactive\"\n },\n \"type\": \"privilegeUpdate\",\n \"updateType\": \"lifting_encumbrance\",\n \"removedValues\": [\n \"\",\n \"\"\n ],\n \"licenseType\": \"speech-language pathologist\",\n \"updatedValues\": {\n \"licenseJurisdiction\": \"or\",\n \"compact\": \"coun\",\n \"jurisdiction\": \"ri\",\n \"attestations\": [\n {\n \"attestationId\": \"\",\n \"version\": \"\"\n },\n {\n \"attestationId\": \"\",\n \"version\": \"\"\n }\n ],\n \"type\": \"privilege\",\n \"compactTransactionId\": \"\",\n \"dateOfIssuance\": \"2785-10-30\",\n \"administratorSetStatus\": \"inactive\",\n \"dateOfExpiration\": \"2086-12-09\",\n \"privilegeId\": \"\",\n \"providerId\": \"f158113c-4fb0-4f87-812e-43d8bf431d0b\",\n \"dateOfRenewal\": \"1234-05-18\",\n \"dateOfUpdate\": \"2693-05-30\",\n \"status\": \"inactive\"\n }\n }\n ],\n \"jurisdiction\": \"sc\",\n \"licenseJurisdiction\": \"pa\",\n \"licenseType\": \"licensed professional counselor\",\n \"privilegeId\": \"\",\n \"providerId\": \"1bf3d98e-4e67-4055-8ef2-3cfdf4ce943a\",\n \"status\": \"inactive\",\n \"type\": \"privilege\",\n \"adverseActions\": [\n {\n \"actionAgainst\": \"\",\n \"adverseActionId\": \"\",\n \"clinicalPrivilegeActionCategory\": \"\",\n \"compact\": \"coun\",\n \"creationDate\": \"2620-01-05\",\n \"dateOfUpdate\": \"2263-12-16\",\n \"effectiveStartDate\": \"1392-08-27\",\n \"encumbranceType\": \"\",\n \"jurisdiction\": \"wi\",\n \"licenseType\": \"\",\n \"licenseTypeAbbreviation\": \"\",\n \"providerId\": \"81428441-fb07-4fe0-8dc4-4c4c7420988e\",\n \"type\": \"adverseAction\",\n \"effectiveLiftDate\": \"1047-06-02\",\n \"liftingUser\": \"\"\n },\n {\n \"actionAgainst\": \"\",\n \"adverseActionId\": \"\",\n \"clinicalPrivilegeActionCategory\": \"\",\n \"compact\": \"coun\",\n \"creationDate\": \"1676-04-30\",\n \"dateOfUpdate\": \"1110-04-31\",\n \"effectiveStartDate\": \"1045-03-22\",\n \"encumbranceType\": \"\",\n \"jurisdiction\": \"ia\",\n \"licenseType\": \"\",\n \"licenseTypeAbbreviation\": \"\",\n \"providerId\": \"436f032e-c3b1-41e2-afe8-ab26446f06cb\",\n \"type\": \"adverseAction\",\n \"effectiveLiftDate\": \"1101-11-06\",\n \"liftingUser\": \"\"\n }\n ]\n }\n ],\n \"providerId\": \"3c9de2be-9898-4ab6-85df-6d0ab3acc6e2\",\n \"type\": \"provider\",\n \"npi\": \"3401297518\",\n \"compactEligibility\": \"ineligible\",\n \"jurisdictionUploadedCompactEligibility\": \"ineligible\",\n \"dateOfBirth\": \"2440-09-05\",\n \"jurisdictionUploadedLicenseStatus\": \"active\",\n \"suffix\": \"\",\n \"currentHomeJurisdiction\": \"ky\",\n \"ssnLastFour\": \"3099\",\n \"licenseStatus\": \"active\",\n \"middleName\": \"\",\n \"compactConnectRegisteredEmailAddress\": \"\"\n}", + "body": "{\n \"birthMonthDay\": \"18-31\",\n \"compact\": \"octp\",\n \"dateOfExpiration\": \"2943-11-09\",\n \"dateOfUpdate\": \"1250-02-24\",\n \"familyName\": \"\",\n \"givenName\": \"\",\n \"licenseJurisdiction\": \"ma\",\n \"licenses\": [\n {\n \"compact\": \"coun\",\n \"compactEligibility\": \"ineligible\",\n \"dateOfExpiration\": \"1065-06-23\",\n \"dateOfIssuance\": \"2970-07-14\",\n \"dateOfRenewal\": \"2821-11-02\",\n \"dateOfUpdate\": \"1704-11-31\",\n \"familyName\": \"\",\n \"givenName\": \"\",\n \"history\": [\n {\n \"compact\": \"aslp\",\n \"dateOfUpdate\": \"2231-10-14\",\n \"jurisdiction\": \"hi\",\n \"previous\": {\n \"dateOfExpiration\": \"2644-03-03\",\n \"dateOfIssuance\": \"2575-03-07\",\n \"dateOfRenewal\": \"1739-10-21\",\n \"familyName\": \"\",\n \"givenName\": \"\",\n \"homeAddressCity\": \"\",\n \"homeAddressPostalCode\": \"\",\n \"homeAddressState\": \"\",\n \"homeAddressStreet1\": \"\",\n \"jurisdictionUploadedCompactEligibility\": \"ineligible\",\n \"jurisdictionUploadedLicenseStatus\": \"inactive\",\n \"middleName\": \"\",\n \"homeAddressStreet2\": \"\",\n \"npi\": \"9863917273\",\n \"compactEligibility\": \"ineligible\",\n \"dateOfBirth\": \"2395-05-31\",\n \"suffix\": \"\",\n \"emailAddress\": \"\",\n \"phoneNumber\": \"+98530144953\",\n \"licenseStatus\": \"active\",\n \"licenseNumber\": \"\",\n \"licenseStatusName\": \"\"\n },\n \"type\": \"licenseUpdate\",\n \"updateType\": \"encumbrance\",\n \"removedValues\": [\n \"\",\n \"\"\n ],\n \"licenseType\": \"occupational therapy assistant\",\n \"updatedValues\": {\n \"homeAddressStreet2\": \"\",\n \"npi\": \"5294519747\",\n \"homeAddressPostalCode\": \"\",\n \"givenName\": \"\",\n \"homeAddressStreet1\": \"\",\n \"compactEligibility\": \"ineligible\",\n \"jurisdictionUploadedCompactEligibility\": \"eligible\",\n \"dateOfBirth\": \"1738-04-02\",\n \"jurisdictionUploadedLicenseStatus\": \"inactive\",\n \"suffix\": \"\",\n \"dateOfIssuance\": \"1535-01-31\",\n \"emailAddress\": \"\",\n \"dateOfExpiration\": \"1345-12-01\",\n \"phoneNumber\": \"+88983379\",\n \"homeAddressState\": \"\",\n \"dateOfRenewal\": \"2163-06-30\",\n \"licenseStatus\": \"inactive\",\n \"familyName\": \"\",\n \"homeAddressCity\": \"\",\n \"licenseNumber\": \"\",\n \"middleName\": \"\",\n \"licenseStatusName\": \"\"\n }\n },\n {\n \"compact\": \"octp\",\n \"dateOfUpdate\": \"1258-12-01\",\n \"jurisdiction\": \"pa\",\n \"previous\": {\n \"dateOfExpiration\": \"2242-11-19\",\n \"dateOfIssuance\": \"1812-10-29\",\n \"dateOfRenewal\": \"2431-02-30\",\n \"familyName\": \"\",\n \"givenName\": \"\",\n \"homeAddressCity\": \"\",\n \"homeAddressPostalCode\": \"\",\n \"homeAddressState\": \"\",\n \"homeAddressStreet1\": \"\",\n \"jurisdictionUploadedCompactEligibility\": \"eligible\",\n \"jurisdictionUploadedLicenseStatus\": \"active\",\n \"middleName\": \"\",\n \"homeAddressStreet2\": \"\",\n \"npi\": \"9204408223\",\n \"compactEligibility\": \"ineligible\",\n \"dateOfBirth\": \"2105-06-05\",\n \"suffix\": \"\",\n \"emailAddress\": \"\",\n \"phoneNumber\": \"+12442530879818\",\n \"licenseStatus\": \"inactive\",\n \"licenseNumber\": \"\",\n \"licenseStatusName\": \"\"\n },\n \"type\": \"licenseUpdate\",\n \"updateType\": \"emailChange\",\n \"removedValues\": [\n \"\",\n \"\"\n ],\n \"licenseType\": \"audiologist\",\n \"updatedValues\": {\n \"homeAddressStreet2\": \"\",\n \"npi\": \"4923994654\",\n \"homeAddressPostalCode\": \"\",\n \"givenName\": \"\",\n \"homeAddressStreet1\": \"\",\n \"compactEligibility\": \"ineligible\",\n \"jurisdictionUploadedCompactEligibility\": \"eligible\",\n \"dateOfBirth\": \"1541-10-09\",\n \"jurisdictionUploadedLicenseStatus\": \"inactive\",\n \"suffix\": \"\",\n \"dateOfIssuance\": \"2607-07-13\",\n \"emailAddress\": \"\",\n \"dateOfExpiration\": \"1347-12-30\",\n \"phoneNumber\": \"+994685399812775\",\n \"homeAddressState\": \"\",\n \"dateOfRenewal\": \"1766-07-31\",\n \"licenseStatus\": \"active\",\n \"familyName\": \"\",\n \"homeAddressCity\": \"\",\n \"licenseNumber\": \"\",\n \"middleName\": \"\",\n \"licenseStatusName\": \"\"\n }\n }\n ],\n \"homeAddressCity\": \"\",\n \"homeAddressPostalCode\": \"\",\n \"homeAddressState\": \"\",\n \"homeAddressStreet1\": \"\",\n \"jurisdiction\": \"al\",\n \"jurisdictionUploadedCompactEligibility\": \"eligible\",\n \"jurisdictionUploadedLicenseStatus\": \"active\",\n \"licenseStatus\": \"inactive\",\n \"licenseType\": \"occupational therapist\",\n \"middleName\": \"\",\n \"providerId\": \"eaf8b40b-5198-4246-a5c2-85f8975cad8f\",\n \"type\": \"license-home\",\n \"homeAddressStreet2\": \"\",\n \"suffix\": \"\",\n \"emailAddress\": \"\",\n \"licenseNumber\": \"\",\n \"npi\": \"4815134028\",\n \"dateOfBirth\": \"1567-11-21\",\n \"ssnLastFour\": \"4241\",\n \"phoneNumber\": \"+899877959\",\n \"licenseStatusName\": \"\",\n \"adverseActions\": [\n {\n \"actionAgainst\": \"\",\n \"adverseActionId\": \"\",\n \"clinicalPrivilegeActionCategory\": \"\",\n \"compact\": \"octp\",\n \"creationDate\": \"1766-02-29\",\n \"dateOfUpdate\": \"2423-10-07\",\n \"effectiveStartDate\": \"1333-01-31\",\n \"encumbranceType\": \"\",\n \"jurisdiction\": \"id\",\n \"licenseType\": \"\",\n \"licenseTypeAbbreviation\": \"\",\n \"providerId\": \"b4b528b2-8076-4d22-8fdd-591a6de8de47\",\n \"type\": \"adverseAction\",\n \"effectiveLiftDate\": \"2144-12-15\",\n \"liftingUser\": \"\"\n },\n {\n \"actionAgainst\": \"\",\n \"adverseActionId\": \"\",\n \"clinicalPrivilegeActionCategory\": \"\",\n \"compact\": \"octp\",\n \"creationDate\": \"2486-08-03\",\n \"dateOfUpdate\": \"1648-10-15\",\n \"effectiveStartDate\": \"2584-12-31\",\n \"encumbranceType\": \"\",\n \"jurisdiction\": \"de\",\n \"licenseType\": \"\",\n \"licenseTypeAbbreviation\": \"\",\n \"providerId\": \"d03a4970-7cda-4c5d-8662-1a1464439782\",\n \"type\": \"adverseAction\",\n \"effectiveLiftDate\": \"1395-01-22\",\n \"liftingUser\": \"\"\n }\n ]\n },\n {\n \"compact\": \"octp\",\n \"compactEligibility\": \"ineligible\",\n \"dateOfExpiration\": \"2661-09-03\",\n \"dateOfIssuance\": \"1334-04-31\",\n \"dateOfRenewal\": \"2596-12-30\",\n \"dateOfUpdate\": \"2431-11-27\",\n \"familyName\": \"\",\n \"givenName\": \"\",\n \"history\": [\n {\n \"compact\": \"coun\",\n \"dateOfUpdate\": \"1927-10-31\",\n \"jurisdiction\": \"ut\",\n \"previous\": {\n \"dateOfExpiration\": \"2097-02-30\",\n \"dateOfIssuance\": \"2104-10-24\",\n \"dateOfRenewal\": \"2520-07-05\",\n \"familyName\": \"\",\n \"givenName\": \"\",\n \"homeAddressCity\": \"\",\n \"homeAddressPostalCode\": \"\",\n \"homeAddressState\": \"\",\n \"homeAddressStreet1\": \"\",\n \"jurisdictionUploadedCompactEligibility\": \"ineligible\",\n \"jurisdictionUploadedLicenseStatus\": \"inactive\",\n \"middleName\": \"\",\n \"homeAddressStreet2\": \"\",\n \"npi\": \"9123939912\",\n \"compactEligibility\": \"ineligible\",\n \"dateOfBirth\": \"2205-11-30\",\n \"suffix\": \"\",\n \"emailAddress\": \"\",\n \"phoneNumber\": \"+50133362\",\n \"licenseStatus\": \"active\",\n \"licenseNumber\": \"\",\n \"licenseStatusName\": \"\"\n },\n \"type\": \"licenseUpdate\",\n \"updateType\": \"expiration\",\n \"removedValues\": [\n \"\",\n \"\"\n ],\n \"licenseType\": \"licensed professional counselor\",\n \"updatedValues\": {\n \"homeAddressStreet2\": \"\",\n \"npi\": \"7668946187\",\n \"homeAddressPostalCode\": \"\",\n \"givenName\": \"\",\n \"homeAddressStreet1\": \"\",\n \"compactEligibility\": \"eligible\",\n \"jurisdictionUploadedCompactEligibility\": \"eligible\",\n \"dateOfBirth\": \"2129-11-30\",\n \"jurisdictionUploadedLicenseStatus\": \"active\",\n \"suffix\": \"\",\n \"dateOfIssuance\": \"1429-10-30\",\n \"emailAddress\": \"\",\n \"dateOfExpiration\": \"1751-10-14\",\n \"phoneNumber\": \"+36228193470590\",\n \"homeAddressState\": \"\",\n \"dateOfRenewal\": \"1193-11-31\",\n \"licenseStatus\": \"active\",\n \"familyName\": \"\",\n \"homeAddressCity\": \"\",\n \"licenseNumber\": \"\",\n \"middleName\": \"\",\n \"licenseStatusName\": \"\"\n }\n },\n {\n \"compact\": \"octp\",\n \"dateOfUpdate\": \"1769-09-09\",\n \"jurisdiction\": \"il\",\n \"previous\": {\n \"dateOfExpiration\": \"2156-12-30\",\n \"dateOfIssuance\": \"1510-12-23\",\n \"dateOfRenewal\": \"2845-10-31\",\n \"familyName\": \"\",\n \"givenName\": \"\",\n \"homeAddressCity\": \"\",\n \"homeAddressPostalCode\": \"\",\n \"homeAddressState\": \"\",\n \"homeAddressStreet1\": \"\",\n \"jurisdictionUploadedCompactEligibility\": \"eligible\",\n \"jurisdictionUploadedLicenseStatus\": \"inactive\",\n \"middleName\": \"\",\n \"homeAddressStreet2\": \"\",\n \"npi\": \"6167646658\",\n \"compactEligibility\": \"ineligible\",\n \"dateOfBirth\": \"1263-11-30\",\n \"suffix\": \"\",\n \"emailAddress\": \"\",\n \"phoneNumber\": \"+2374225608467\",\n \"licenseStatus\": \"active\",\n \"licenseNumber\": \"\",\n \"licenseStatusName\": \"\"\n },\n \"type\": \"licenseUpdate\",\n \"updateType\": \"issuance\",\n \"removedValues\": [\n \"\",\n \"\"\n ],\n \"licenseType\": \"speech-language pathologist\",\n \"updatedValues\": {\n \"homeAddressStreet2\": \"\",\n \"npi\": \"5287069345\",\n \"homeAddressPostalCode\": \"\",\n \"givenName\": \"\",\n \"homeAddressStreet1\": \"\",\n \"compactEligibility\": \"eligible\",\n \"jurisdictionUploadedCompactEligibility\": \"ineligible\",\n \"dateOfBirth\": \"1015-11-13\",\n \"jurisdictionUploadedLicenseStatus\": \"inactive\",\n \"suffix\": \"\",\n \"dateOfIssuance\": \"1237-03-31\",\n \"emailAddress\": \"\",\n \"dateOfExpiration\": \"2563-07-31\",\n \"phoneNumber\": \"+110806100\",\n \"homeAddressState\": \"\",\n \"dateOfRenewal\": \"2400-11-31\",\n \"licenseStatus\": \"active\",\n \"familyName\": \"\",\n \"homeAddressCity\": \"\",\n \"licenseNumber\": \"\",\n \"middleName\": \"\",\n \"licenseStatusName\": \"\"\n }\n }\n ],\n \"homeAddressCity\": \"\",\n \"homeAddressPostalCode\": \"\",\n \"homeAddressState\": \"\",\n \"homeAddressStreet1\": \"\",\n \"jurisdiction\": \"mn\",\n \"jurisdictionUploadedCompactEligibility\": \"ineligible\",\n \"jurisdictionUploadedLicenseStatus\": \"active\",\n \"licenseStatus\": \"inactive\",\n \"licenseType\": \"occupational therapy assistant\",\n \"middleName\": \"\",\n \"providerId\": \"06b7efa9-e2ff-4231-a21c-ac6f81025226\",\n \"type\": \"license-home\",\n \"homeAddressStreet2\": \"\",\n \"suffix\": \"\",\n \"emailAddress\": \"\",\n \"licenseNumber\": \"\",\n \"npi\": \"7730730531\",\n \"dateOfBirth\": \"1902-04-20\",\n \"ssnLastFour\": \"2779\",\n \"phoneNumber\": \"+5584183492\",\n \"licenseStatusName\": \"\",\n \"adverseActions\": [\n {\n \"actionAgainst\": \"\",\n \"adverseActionId\": \"\",\n \"clinicalPrivilegeActionCategory\": \"\",\n \"compact\": \"aslp\",\n \"creationDate\": \"2305-01-17\",\n \"dateOfUpdate\": \"2745-11-22\",\n \"effectiveStartDate\": \"2196-10-31\",\n \"encumbranceType\": \"\",\n \"jurisdiction\": \"ct\",\n \"licenseType\": \"\",\n \"licenseTypeAbbreviation\": \"\",\n \"providerId\": \"dd8cde9f-58aa-481d-848e-4ec200705451\",\n \"type\": \"adverseAction\",\n \"effectiveLiftDate\": \"1920-05-04\",\n \"liftingUser\": \"\"\n },\n {\n \"actionAgainst\": \"\",\n \"adverseActionId\": \"\",\n \"clinicalPrivilegeActionCategory\": \"\",\n \"compact\": \"aslp\",\n \"creationDate\": \"2012-09-31\",\n \"dateOfUpdate\": \"2250-08-18\",\n \"effectiveStartDate\": \"1575-12-31\",\n \"encumbranceType\": \"\",\n \"jurisdiction\": \"tn\",\n \"licenseType\": \"\",\n \"licenseTypeAbbreviation\": \"\",\n \"providerId\": \"e341914c-587b-4208-b23f-a1a55bfa01a7\",\n \"type\": \"adverseAction\",\n \"effectiveLiftDate\": \"2871-11-06\",\n \"liftingUser\": \"\"\n }\n ]\n }\n ],\n \"militaryAffiliations\": [\n {\n \"affiliationType\": \"militaryMember\",\n \"compact\": \"octp\",\n \"dateOfUpdate\": \"1991-11-30\",\n \"dateOfUpload\": \"2882-12-30\",\n \"fileNames\": [\n \"\",\n \"\"\n ],\n \"providerId\": \"fc8e686c-c65d-4c3a-8e49-71b2807f80b9\",\n \"status\": \"inactive\",\n \"type\": \"militaryAffiliation\",\n \"downloadLinks\": [\n {\n \"fileName\": \"\",\n \"url\": \"\"\n },\n {\n \"fileName\": \"\",\n \"url\": \"\"\n }\n ]\n },\n {\n \"affiliationType\": \"militaryMemberSpouse\",\n \"compact\": \"aslp\",\n \"dateOfUpdate\": \"2520-11-05\",\n \"dateOfUpload\": \"2131-01-26\",\n \"fileNames\": [\n \"\",\n \"\"\n ],\n \"providerId\": \"5961ad9b-f28d-40a7-ae4a-9acd7949999a\",\n \"status\": \"inactive\",\n \"type\": \"militaryAffiliation\",\n \"downloadLinks\": [\n {\n \"fileName\": \"\",\n \"url\": \"\"\n },\n {\n \"fileName\": \"\",\n \"url\": \"\"\n }\n ]\n }\n ],\n \"privilegeJurisdictions\": [\n \"id\",\n \"ms\"\n ],\n \"privileges\": [\n {\n \"administratorSetStatus\": \"inactive\",\n \"attestations\": [\n {\n \"attestationId\": \"\",\n \"version\": \"\"\n },\n {\n \"attestationId\": \"\",\n \"version\": \"\"\n }\n ],\n \"compact\": \"coun\",\n \"compactTransactionId\": \"\",\n \"dateOfExpiration\": \"1274-12-30\",\n \"dateOfIssuance\": \"1780-12-31\",\n \"dateOfRenewal\": \"2292-09-22\",\n \"dateOfUpdate\": \"2130-10-31\",\n \"history\": [\n {\n \"compact\": \"octp\",\n \"dateOfUpdate\": \"1421-07-22\",\n \"jurisdiction\": \"co\",\n \"previous\": {\n \"administratorSetStatus\": \"active\",\n \"attestations\": [\n {\n \"attestationId\": \"\",\n \"version\": \"\"\n },\n {\n \"attestationId\": \"\",\n \"version\": \"\"\n }\n ],\n \"compactTransactionId\": \"\",\n \"dateOfExpiration\": \"2061-11-29\",\n \"dateOfIssuance\": \"1610-02-07\",\n \"dateOfRenewal\": \"2537-03-05\",\n \"dateOfUpdate\": \"2703-09-02\",\n \"licenseJurisdiction\": \"pa\",\n \"privilegeId\": \"\",\n \"compact\": \"coun\",\n \"jurisdiction\": \"ca\",\n \"type\": \"privilege\",\n \"providerId\": \"2969addd-238f-443e-8c68-5d139471c1e3\",\n \"status\": \"inactive\"\n },\n \"type\": \"privilegeUpdate\",\n \"updateType\": \"lifting_encumbrance\",\n \"removedValues\": [\n \"\",\n \"\"\n ],\n \"licenseType\": \"licensed professional counselor\",\n \"updatedValues\": {\n \"licenseJurisdiction\": \"ok\",\n \"compact\": \"octp\",\n \"jurisdiction\": \"tx\",\n \"attestations\": [\n {\n \"attestationId\": \"\",\n \"version\": \"\"\n },\n {\n \"attestationId\": \"\",\n \"version\": \"\"\n }\n ],\n \"type\": \"privilege\",\n \"compactTransactionId\": \"\",\n \"dateOfIssuance\": \"1024-09-05\",\n \"administratorSetStatus\": \"inactive\",\n \"dateOfExpiration\": \"2718-10-31\",\n \"privilegeId\": \"\",\n \"providerId\": \"6d2eb9c3-4f99-40a1-84e5-a1b6761782d6\",\n \"dateOfRenewal\": \"2059-04-08\",\n \"dateOfUpdate\": \"2107-10-05\",\n \"status\": \"inactive\"\n }\n },\n {\n \"compact\": \"coun\",\n \"dateOfUpdate\": \"2077-06-30\",\n \"jurisdiction\": \"il\",\n \"previous\": {\n \"administratorSetStatus\": \"inactive\",\n \"attestations\": [\n {\n \"attestationId\": \"\",\n \"version\": \"\"\n },\n {\n \"attestationId\": \"\",\n \"version\": \"\"\n }\n ],\n \"compactTransactionId\": \"\",\n \"dateOfExpiration\": \"2676-08-03\",\n \"dateOfIssuance\": \"2083-09-30\",\n \"dateOfRenewal\": \"2848-07-07\",\n \"dateOfUpdate\": \"2488-11-03\",\n \"licenseJurisdiction\": \"ms\",\n \"privilegeId\": \"\",\n \"compact\": \"octp\",\n \"jurisdiction\": \"or\",\n \"type\": \"privilege\",\n \"providerId\": \"f7c090f1-b36d-407f-9a4d-e0857ab098d4\",\n \"status\": \"active\"\n },\n \"type\": \"privilegeUpdate\",\n \"updateType\": \"deactivation\",\n \"removedValues\": [\n \"\",\n \"\"\n ],\n \"licenseType\": \"occupational therapy assistant\",\n \"updatedValues\": {\n \"licenseJurisdiction\": \"pa\",\n \"compact\": \"aslp\",\n \"jurisdiction\": \"ut\",\n \"attestations\": [\n {\n \"attestationId\": \"\",\n \"version\": \"\"\n },\n {\n \"attestationId\": \"\",\n \"version\": \"\"\n }\n ],\n \"type\": \"privilege\",\n \"compactTransactionId\": \"\",\n \"dateOfIssuance\": \"2090-10-31\",\n \"administratorSetStatus\": \"inactive\",\n \"dateOfExpiration\": \"2307-09-28\",\n \"privilegeId\": \"\",\n \"providerId\": \"401c04bf-75cc-4653-b9f8-4e73dce99729\",\n \"dateOfRenewal\": \"1545-11-30\",\n \"dateOfUpdate\": \"1889-04-19\",\n \"status\": \"inactive\"\n }\n }\n ],\n \"jurisdiction\": \"vi\",\n \"licenseJurisdiction\": \"ma\",\n \"licenseType\": \"occupational therapist\",\n \"privilegeId\": \"\",\n \"providerId\": \"54df1999-73b0-471d-b6fb-3c44e95cabbf\",\n \"status\": \"active\",\n \"type\": \"privilege\",\n \"adverseActions\": [\n {\n \"actionAgainst\": \"\",\n \"adverseActionId\": \"\",\n \"clinicalPrivilegeActionCategory\": \"\",\n \"compact\": \"aslp\",\n \"creationDate\": \"2306-03-05\",\n \"dateOfUpdate\": \"2057-12-12\",\n \"effectiveStartDate\": \"1856-06-26\",\n \"encumbranceType\": \"\",\n \"jurisdiction\": \"mo\",\n \"licenseType\": \"\",\n \"licenseTypeAbbreviation\": \"\",\n \"providerId\": \"cf8ecf9c-781b-460b-afae-1384db52d08c\",\n \"type\": \"adverseAction\",\n \"effectiveLiftDate\": \"1417-05-31\",\n \"liftingUser\": \"\"\n },\n {\n \"actionAgainst\": \"\",\n \"adverseActionId\": \"\",\n \"clinicalPrivilegeActionCategory\": \"\",\n \"compact\": \"aslp\",\n \"creationDate\": \"2891-04-02\",\n \"dateOfUpdate\": \"1660-03-23\",\n \"effectiveStartDate\": \"1306-09-30\",\n \"encumbranceType\": \"\",\n \"jurisdiction\": \"wa\",\n \"licenseType\": \"\",\n \"licenseTypeAbbreviation\": \"\",\n \"providerId\": \"6e1e2851-7cc2-4f7c-8b1b-8bba307deba1\",\n \"type\": \"adverseAction\",\n \"effectiveLiftDate\": \"1254-03-30\",\n \"liftingUser\": \"\"\n }\n ]\n },\n {\n \"administratorSetStatus\": \"inactive\",\n \"attestations\": [\n {\n \"attestationId\": \"\",\n \"version\": \"\"\n },\n {\n \"attestationId\": \"\",\n \"version\": \"\"\n }\n ],\n \"compact\": \"aslp\",\n \"compactTransactionId\": \"\",\n \"dateOfExpiration\": \"1340-03-31\",\n \"dateOfIssuance\": \"1260-10-18\",\n \"dateOfRenewal\": \"2300-12-16\",\n \"dateOfUpdate\": \"2905-08-01\",\n \"history\": [\n {\n \"compact\": \"octp\",\n \"dateOfUpdate\": \"1316-10-31\",\n \"jurisdiction\": \"ca\",\n \"previous\": {\n \"administratorSetStatus\": \"active\",\n \"attestations\": [\n {\n \"attestationId\": \"\",\n \"version\": \"\"\n },\n {\n \"attestationId\": \"\",\n \"version\": \"\"\n }\n ],\n \"compactTransactionId\": \"\",\n \"dateOfExpiration\": \"2143-11-31\",\n \"dateOfIssuance\": \"1911-12-30\",\n \"dateOfRenewal\": \"2349-04-04\",\n \"dateOfUpdate\": \"2101-10-28\",\n \"licenseJurisdiction\": \"va\",\n \"privilegeId\": \"\",\n \"compact\": \"coun\",\n \"jurisdiction\": \"md\",\n \"type\": \"privilege\",\n \"providerId\": \"8893ab59-563e-42e8-96b7-ba058426d34e\",\n \"status\": \"inactive\"\n },\n \"type\": \"privilegeUpdate\",\n \"updateType\": \"expiration\",\n \"removedValues\": [\n \"\",\n \"\"\n ],\n \"licenseType\": \"occupational therapy assistant\",\n \"updatedValues\": {\n \"licenseJurisdiction\": \"wy\",\n \"compact\": \"octp\",\n \"jurisdiction\": \"vt\",\n \"attestations\": [\n {\n \"attestationId\": \"\",\n \"version\": \"\"\n },\n {\n \"attestationId\": \"\",\n \"version\": \"\"\n }\n ],\n \"type\": \"privilege\",\n \"compactTransactionId\": \"\",\n \"dateOfIssuance\": \"2543-08-13\",\n \"administratorSetStatus\": \"active\",\n \"dateOfExpiration\": \"1133-03-19\",\n \"privilegeId\": \"\",\n \"providerId\": \"a0a26f5a-c6ce-4db9-9a4d-52117f4f96c6\",\n \"dateOfRenewal\": \"1838-09-08\",\n \"dateOfUpdate\": \"1020-01-30\",\n \"status\": \"active\"\n }\n },\n {\n \"compact\": \"aslp\",\n \"dateOfUpdate\": \"2409-10-07\",\n \"jurisdiction\": \"nj\",\n \"previous\": {\n \"administratorSetStatus\": \"inactive\",\n \"attestations\": [\n {\n \"attestationId\": \"\",\n \"version\": \"\"\n },\n {\n \"attestationId\": \"\",\n \"version\": \"\"\n }\n ],\n \"compactTransactionId\": \"\",\n \"dateOfExpiration\": \"2909-11-03\",\n \"dateOfIssuance\": \"2808-12-18\",\n \"dateOfRenewal\": \"1631-12-19\",\n \"dateOfUpdate\": \"1914-05-30\",\n \"licenseJurisdiction\": \"az\",\n \"privilegeId\": \"\",\n \"compact\": \"octp\",\n \"jurisdiction\": \"mn\",\n \"type\": \"privilege\",\n \"providerId\": \"9c45fa08-650b-40db-adbf-46bea62dfc04\",\n \"status\": \"active\"\n },\n \"type\": \"privilegeUpdate\",\n \"updateType\": \"renewal\",\n \"removedValues\": [\n \"\",\n \"\"\n ],\n \"licenseType\": \"speech-language pathologist\",\n \"updatedValues\": {\n \"licenseJurisdiction\": \"ga\",\n \"compact\": \"aslp\",\n \"jurisdiction\": \"ut\",\n \"attestations\": [\n {\n \"attestationId\": \"\",\n \"version\": \"\"\n },\n {\n \"attestationId\": \"\",\n \"version\": \"\"\n }\n ],\n \"type\": \"privilege\",\n \"compactTransactionId\": \"\",\n \"dateOfIssuance\": \"1703-01-01\",\n \"administratorSetStatus\": \"inactive\",\n \"dateOfExpiration\": \"1193-12-29\",\n \"privilegeId\": \"\",\n \"providerId\": \"d357d4b0-f62e-4c51-a501-44f0888ef8fa\",\n \"dateOfRenewal\": \"1498-08-31\",\n \"dateOfUpdate\": \"1632-05-12\",\n \"status\": \"inactive\"\n }\n }\n ],\n \"jurisdiction\": \"ut\",\n \"licenseJurisdiction\": \"ak\",\n \"licenseType\": \"occupational therapist\",\n \"privilegeId\": \"\",\n \"providerId\": \"e312adc2-e993-4cb3-b582-680408aa25f5\",\n \"status\": \"active\",\n \"type\": \"privilege\",\n \"adverseActions\": [\n {\n \"actionAgainst\": \"\",\n \"adverseActionId\": \"\",\n \"clinicalPrivilegeActionCategory\": \"\",\n \"compact\": \"aslp\",\n \"creationDate\": \"1514-11-04\",\n \"dateOfUpdate\": \"1494-07-18\",\n \"effectiveStartDate\": \"1952-02-03\",\n \"encumbranceType\": \"\",\n \"jurisdiction\": \"al\",\n \"licenseType\": \"\",\n \"licenseTypeAbbreviation\": \"\",\n \"providerId\": \"4a189e27-52ad-444f-9bf1-3d24939ed8d0\",\n \"type\": \"adverseAction\",\n \"effectiveLiftDate\": \"2687-07-31\",\n \"liftingUser\": \"\"\n },\n {\n \"actionAgainst\": \"\",\n \"adverseActionId\": \"\",\n \"clinicalPrivilegeActionCategory\": \"\",\n \"compact\": \"aslp\",\n \"creationDate\": \"1663-10-14\",\n \"dateOfUpdate\": \"1561-03-28\",\n \"effectiveStartDate\": \"2346-05-07\",\n \"encumbranceType\": \"\",\n \"jurisdiction\": \"ar\",\n \"licenseType\": \"\",\n \"licenseTypeAbbreviation\": \"\",\n \"providerId\": \"dd94642b-bac9-4b74-b2f2-8d280f68dc70\",\n \"type\": \"adverseAction\",\n \"effectiveLiftDate\": \"2249-12-06\",\n \"liftingUser\": \"\"\n }\n ]\n }\n ],\n \"providerId\": \"5321022c-1ece-4d00-bc1a-627262417cfe\",\n \"type\": \"provider\",\n \"npi\": \"2844543517\",\n \"compactEligibility\": \"eligible\",\n \"jurisdictionUploadedCompactEligibility\": \"ineligible\",\n \"dateOfBirth\": \"1821-12-27\",\n \"jurisdictionUploadedLicenseStatus\": \"active\",\n \"suffix\": \"\",\n \"currentHomeJurisdiction\": \"hi\",\n \"ssnLastFour\": \"6689\",\n \"licenseStatus\": \"inactive\",\n \"middleName\": \"\",\n \"compactConnectRegisteredEmailAddress\": \"\"\n}", "code": 200, "cookie": [], "header": [ @@ -1454,7 +1454,7 @@ "value": "application/json" } ], - "id": "8e17f188-d608-4c43-93b9-721b2ec082cd", + "id": "3106f7ad-8d77-425c-b919-8f6a3ec69691", "name": "200 response", "originalRequest": { "body": {}, @@ -1512,7 +1512,7 @@ "item": [ { "event": [], - "id": "2a226c93-f38c-4b19-9b29-a36386d56d37", + "id": "d61e2495-f586-4dc1-85ee-2afb35fe9346", "name": "/v1/compacts/:compact/providers/:providerId/licenses/jurisdiction/:jurisdiction/licenseType/:licenseType/encumbrance", "protocolProfileBehavior": { "disableBodyPruning": true @@ -1526,7 +1526,7 @@ "language": "json" } }, - "raw": "{\n \"clinicalPrivilegeActionCategory\": \"\",\n \"encumbranceEffectiveDate\": \"2144-02-14\",\n \"encumbranceType\": \"injunctive action\"\n}" + "raw": "{\n \"clinicalPrivilegeActionCategory\": \"\",\n \"encumbranceEffectiveDate\": \"2929-07-02\",\n \"encumbranceType\": \"modification of previous action-extension\"\n}" }, "description": {}, "header": [ @@ -1615,7 +1615,7 @@ "value": "application/json" } ], - "id": "a765b4e4-8215-4761-a61e-7c8fd6bf4878", + "id": "0baeeebe-6312-426d-9d97-bfef4ba20b82", "name": "200 response", "originalRequest": { "body": { @@ -1626,7 +1626,7 @@ "language": "json" } }, - "raw": "{\n \"clinicalPrivilegeActionCategory\": \"\",\n \"encumbranceEffectiveDate\": \"2144-02-14\",\n \"encumbranceType\": \"injunctive action\"\n}" + "raw": "{\n \"clinicalPrivilegeActionCategory\": \"\",\n \"encumbranceEffectiveDate\": \"2929-07-02\",\n \"encumbranceType\": \"modification of previous action-extension\"\n}" }, "header": [ { @@ -1677,7 +1677,7 @@ "item": [ { "event": [], - "id": "ad61ad51-8a14-440e-bdc1-b4379226e92c", + "id": "97da7eec-12e5-450e-921e-bc9723b16a5e", "name": "/v1/compacts/:compact/providers/:providerId/licenses/jurisdiction/:jurisdiction/licenseType/:licenseType/encumbrance/:encumbranceId", "protocolProfileBehavior": { "disableBodyPruning": true @@ -1691,7 +1691,7 @@ "language": "json" } }, - "raw": "{\n \"effectiveLiftDate\": \"1854-02-30\"\n}" + "raw": "{\n \"effectiveLiftDate\": \"1164-02-05\"\n}" }, "description": {}, "header": [ @@ -1791,7 +1791,7 @@ "value": "application/json" } ], - "id": "52a90c2b-fed8-4281-a02f-ce0976fa5dd5", + "id": "823f4835-7767-4698-8cf9-0f2db44ea2e2", "name": "200 response", "originalRequest": { "body": { @@ -1802,7 +1802,7 @@ "language": "json" } }, - "raw": "{\n \"effectiveLiftDate\": \"1854-02-30\"\n}" + "raw": "{\n \"effectiveLiftDate\": \"1164-02-05\"\n}" }, "header": [ { @@ -1890,7 +1890,7 @@ "item": [ { "event": [], - "id": "d4ba6e41-673b-464b-a268-9eef057e72a4", + "id": "fa49b9c9-afb9-4f12-913e-54a95327a538", "name": "/v1/compacts/:compact/providers/:providerId/privileges/jurisdiction/:jurisdiction/licenseType/:licenseType/deactivate", "protocolProfileBehavior": { "disableBodyPruning": true @@ -1993,7 +1993,7 @@ "value": "application/json" } ], - "id": "6dfb8d0a-8201-46d0-aa65-a252d4ef8d28", + "id": "3426e558-29c8-495e-8ac0-e9917552e94a", "name": "200 response", "originalRequest": { "body": { @@ -2058,7 +2058,7 @@ "item": [ { "event": [], - "id": "6c567702-08ec-4a82-b472-512453011a66", + "id": "039f5408-6cf5-4081-92c1-a17a429dcab2", "name": "/v1/compacts/:compact/providers/:providerId/privileges/jurisdiction/:jurisdiction/licenseType/:licenseType/encumbrance", "protocolProfileBehavior": { "disableBodyPruning": true @@ -2072,7 +2072,7 @@ "language": "json" } }, - "raw": "{\n \"clinicalPrivilegeActionCategory\": \"\",\n \"encumbranceEffectiveDate\": \"2144-02-14\",\n \"encumbranceType\": \"injunctive action\"\n}" + "raw": "{\n \"clinicalPrivilegeActionCategory\": \"\",\n \"encumbranceEffectiveDate\": \"2929-07-02\",\n \"encumbranceType\": \"modification of previous action-extension\"\n}" }, "description": {}, "header": [ @@ -2161,7 +2161,7 @@ "value": "application/json" } ], - "id": "147c1cef-bc84-45de-bfc2-297d675f72fc", + "id": "28690c51-aa04-4e94-b57c-91d004bdd3a4", "name": "200 response", "originalRequest": { "body": { @@ -2172,7 +2172,7 @@ "language": "json" } }, - "raw": "{\n \"clinicalPrivilegeActionCategory\": \"\",\n \"encumbranceEffectiveDate\": \"2144-02-14\",\n \"encumbranceType\": \"injunctive action\"\n}" + "raw": "{\n \"clinicalPrivilegeActionCategory\": \"\",\n \"encumbranceEffectiveDate\": \"2929-07-02\",\n \"encumbranceType\": \"modification of previous action-extension\"\n}" }, "header": [ { @@ -2223,7 +2223,7 @@ "item": [ { "event": [], - "id": "b0ed027a-bd3a-441f-8181-f3bab5843edb", + "id": "7bc52730-29db-48ac-9a19-0c9d5f09c6c6", "name": "/v1/compacts/:compact/providers/:providerId/privileges/jurisdiction/:jurisdiction/licenseType/:licenseType/encumbrance/:encumbranceId", "protocolProfileBehavior": { "disableBodyPruning": true @@ -2237,7 +2237,7 @@ "language": "json" } }, - "raw": "{\n \"effectiveLiftDate\": \"1854-02-30\"\n}" + "raw": "{\n \"effectiveLiftDate\": \"1164-02-05\"\n}" }, "description": {}, "header": [ @@ -2337,7 +2337,7 @@ "value": "application/json" } ], - "id": "230c54cf-ec0b-413a-8edc-c416ea59e654", + "id": "f4d926ba-cfd8-4f78-b90f-aa652e0571e1", "name": "200 response", "originalRequest": { "body": { @@ -2348,7 +2348,7 @@ "language": "json" } }, - "raw": "{\n \"effectiveLiftDate\": \"1854-02-30\"\n}" + "raw": "{\n \"effectiveLiftDate\": \"1164-02-05\"\n}" }, "header": [ { @@ -2406,7 +2406,7 @@ "item": [ { "event": [], - "id": "6cdbb8b3-5cb3-4c19-9971-f71f648cae2d", + "id": "b90423fb-f0b9-4de7-bf7d-3190bd0b9001", "name": "/v1/compacts/:compact/providers/:providerId/privileges/jurisdiction/:jurisdiction/licenseType/:licenseType/history", "protocolProfileBehavior": { "disableBodyPruning": true @@ -2487,7 +2487,7 @@ "response": [ { "_postman_previewlanguage": "json", - "body": "{\n \"compact\": \"coun\",\n \"events\": [\n {\n \"createDate\": \"2197-08-13\",\n \"dateOfUpdate\": \"1289-11-31\",\n \"effectiveDate\": \"1858-12-16\",\n \"type\": \"privilegeUpdate\",\n \"updateType\": \"encumbrance\",\n \"note\": \"\"\n },\n {\n \"createDate\": \"1334-11-17\",\n \"dateOfUpdate\": \"2968-08-31\",\n \"effectiveDate\": \"1830-11-07\",\n \"type\": \"privilegeUpdate\",\n \"updateType\": \"expiration\",\n \"note\": \"\"\n }\n ],\n \"jurisdiction\": \"wi\",\n \"licenseType\": \"occupational therapist\",\n \"privilegeId\": \"\",\n \"providerId\": \"f59c6912-a86d-46ca-afdd-4e7c599f7b4e\"\n}", + "body": "{\n \"compact\": \"aslp\",\n \"events\": [\n {\n \"createDate\": \"2605-10-31\",\n \"dateOfUpdate\": \"2897-08-19\",\n \"effectiveDate\": \"1167-11-25\",\n \"type\": \"privilegeUpdate\",\n \"updateType\": \"licenseDeactivation\",\n \"note\": \"\"\n },\n {\n \"createDate\": \"1452-06-28\",\n \"dateOfUpdate\": \"1887-12-24\",\n \"effectiveDate\": \"2287-10-31\",\n \"type\": \"privilegeUpdate\",\n \"updateType\": \"deactivation\",\n \"note\": \"\"\n }\n ],\n \"jurisdiction\": \"hi\",\n \"licenseType\": \"audiologist\",\n \"privilegeId\": \"\",\n \"providerId\": \"16d14b2a-9521-4509-9a76-8913f9636b79\"\n}", "code": 200, "cookie": [], "header": [ @@ -2496,7 +2496,7 @@ "value": "application/json" } ], - "id": "7b0d0ae0-35a7-4cad-af42-6f44772de873", + "id": "24b41b36-0700-4d38-9cd0-f4a7ddac7520", "name": "200 response", "originalRequest": { "body": {}, @@ -2563,7 +2563,7 @@ "item": [ { "event": [], - "id": "243025da-ec58-4a1e-929b-97abca0ab9ed", + "id": "88c7760c-595c-4f46-b291-6ad45cd93901", "name": "/v1/compacts/:compact/providers/:providerId/ssn", "protocolProfileBehavior": { "disableBodyPruning": true @@ -2619,7 +2619,7 @@ "response": [ { "_postman_previewlanguage": "json", - "body": "{\n \"ssn\": \"820-25-2323\"\n}", + "body": "{\n \"ssn\": \"214-46-8123\"\n}", "code": 200, "cookie": [], "header": [ @@ -2628,7 +2628,7 @@ "value": "application/json" } ], - "id": "50ca7079-2d63-4fe3-896d-a223a1e9b018", + "id": "eeb9613d-17bb-47c7-aab9-49fb11857dc7", "name": "200 response", "originalRequest": { "body": {}, @@ -2681,7 +2681,7 @@ "item": [ { "event": [], - "id": "e222b355-43cb-4b04-8f9f-92ecc0b7cb42", + "id": "6ce407b1-999d-493e-bde2-dd11decb8158", "name": "/v1/compacts/:compact/staff-users", "protocolProfileBehavior": { "disableBodyPruning": true @@ -2725,7 +2725,7 @@ "response": [ { "_postman_previewlanguage": "json", - "body": "{\n \"pagination\": {\n \"prevLastKey\": {},\n \"lastKey\": {},\n \"pageSize\": \"\"\n },\n \"users\": [\n {\n \"attributes\": {\n \"email\": \"\",\n \"familyName\": \"\",\n \"givenName\": \"\"\n },\n \"permissions\": {\n \"key_0\": {\n \"actions\": {\n \"readPrivate\": \"\",\n \"admin\": \"\",\n \"readSSN\": \"\"\n },\n \"jurisdictions\": {\n \"key_0\": {\n \"actions\": {\n \"readPrivate\": \"\",\n \"admin\": \"\",\n \"write\": \"\",\n \"readSSN\": \"\"\n }\n },\n \"key_1\": {\n \"actions\": {\n \"readPrivate\": \"\",\n \"admin\": \"\",\n \"write\": \"\",\n \"readSSN\": \"\"\n }\n },\n \"key_2\": {\n \"actions\": {\n \"readPrivate\": \"\",\n \"admin\": \"\",\n \"write\": \"\",\n \"readSSN\": \"\"\n }\n }\n }\n },\n \"key_1\": {\n \"actions\": {\n \"readPrivate\": \"\",\n \"admin\": \"\",\n \"readSSN\": \"\"\n },\n \"jurisdictions\": {\n \"key_0\": {\n \"actions\": {\n \"readPrivate\": \"\",\n \"admin\": \"\",\n \"write\": \"\",\n \"readSSN\": \"\"\n }\n }\n }\n },\n \"key_2\": {\n \"actions\": {\n \"readPrivate\": \"\",\n \"admin\": \"\",\n \"readSSN\": \"\"\n },\n \"jurisdictions\": {\n \"key_0\": {\n \"actions\": {\n \"readPrivate\": \"\",\n \"admin\": \"\",\n \"write\": \"\",\n \"readSSN\": \"\"\n }\n }\n }\n }\n },\n \"status\": \"inactive\",\n \"userId\": \"\"\n },\n {\n \"attributes\": {\n \"email\": \"\",\n \"familyName\": \"\",\n \"givenName\": \"\"\n },\n \"permissions\": {\n \"key_0\": {\n \"actions\": {\n \"readPrivate\": \"\",\n \"admin\": \"\",\n \"readSSN\": \"\"\n },\n \"jurisdictions\": {\n \"key_0\": {\n \"actions\": {\n \"readPrivate\": \"\",\n \"admin\": \"\",\n \"write\": \"\",\n \"readSSN\": \"\"\n }\n }\n }\n }\n },\n \"status\": \"active\",\n \"userId\": \"\"\n }\n ]\n}", + "body": "{\n \"pagination\": {\n \"prevLastKey\": {},\n \"lastKey\": {},\n \"pageSize\": \"\"\n },\n \"users\": [\n {\n \"attributes\": {\n \"email\": \"\",\n \"familyName\": \"\",\n \"givenName\": \"\"\n },\n \"permissions\": {\n \"quis21\": {\n \"actions\": {\n \"readPrivate\": \"\",\n \"admin\": \"\",\n \"readSSN\": \"\"\n },\n \"jurisdictions\": {\n \"enim235\": {\n \"actions\": {\n \"readPrivate\": \"\",\n \"admin\": \"\",\n \"write\": \"\",\n \"readSSN\": \"\"\n }\n }\n }\n },\n \"consectetur_9\": {\n \"actions\": {\n \"readPrivate\": \"\",\n \"admin\": \"\",\n \"readSSN\": \"\"\n },\n \"jurisdictions\": {\n \"nulla27\": {\n \"actions\": {\n \"readPrivate\": \"\",\n \"admin\": \"\",\n \"write\": \"\",\n \"readSSN\": \"\"\n }\n },\n \"qui4\": {\n \"actions\": {\n \"readPrivate\": \"\",\n \"admin\": \"\",\n \"write\": \"\",\n \"readSSN\": \"\"\n }\n }\n }\n },\n \"nostrud567\": {\n \"actions\": {\n \"readPrivate\": \"\",\n \"admin\": \"\",\n \"readSSN\": \"\"\n },\n \"jurisdictions\": {\n \"sunt_4d\": {\n \"actions\": {\n \"readPrivate\": \"\",\n \"admin\": \"\",\n \"write\": \"\",\n \"readSSN\": \"\"\n }\n },\n \"amet_39\": {\n \"actions\": {\n \"readPrivate\": \"\",\n \"admin\": \"\",\n \"write\": \"\",\n \"readSSN\": \"\"\n }\n },\n \"consectetur5b6\": {\n \"actions\": {\n \"readPrivate\": \"\",\n \"admin\": \"\",\n \"write\": \"\",\n \"readSSN\": \"\"\n }\n }\n }\n },\n \"laboris2\": {\n \"actions\": {\n \"readPrivate\": \"\",\n \"admin\": \"\",\n \"readSSN\": \"\"\n },\n \"jurisdictions\": {\n \"nonb\": {\n \"actions\": {\n \"readPrivate\": \"\",\n \"admin\": \"\",\n \"write\": \"\",\n \"readSSN\": \"\"\n }\n }\n }\n }\n },\n \"status\": \"active\",\n \"userId\": \"\"\n },\n {\n \"attributes\": {\n \"email\": \"\",\n \"familyName\": \"\",\n \"givenName\": \"\"\n },\n \"permissions\": {\n \"adipisicing_06d\": {\n \"actions\": {\n \"readPrivate\": \"\",\n \"admin\": \"\",\n \"readSSN\": \"\"\n },\n \"jurisdictions\": {\n \"consequat2\": {\n \"actions\": {\n \"readPrivate\": \"\",\n \"admin\": \"\",\n \"write\": \"\",\n \"readSSN\": \"\"\n }\n },\n \"dolore_a8f\": {\n \"actions\": {\n \"readPrivate\": \"\",\n \"admin\": \"\",\n \"write\": \"\",\n \"readSSN\": \"\"\n }\n }\n }\n }\n },\n \"status\": \"inactive\",\n \"userId\": \"\"\n }\n ]\n}", "code": 200, "cookie": [], "header": [ @@ -2743,7 +2743,7 @@ "value": "" } ], - "id": "63a6b4ce-b0ff-44a8-9758-a7fecab573be", + "id": "2cbbafb7-f284-497b-b726-369dea50c42a", "name": "200 response", "originalRequest": { "body": {}, @@ -2782,7 +2782,7 @@ }, { "event": [], - "id": "85119d2f-30e6-4a93-bed2-f63db983f335", + "id": "81ad1ba2-5201-4418-9d94-89497937c230", "name": "/v1/compacts/:compact/staff-users", "protocolProfileBehavior": { "disableBodyPruning": true @@ -2796,7 +2796,7 @@ "language": "json" } }, - "raw": "{\n \"attributes\": {\n \"email\": \"\",\n \"familyName\": \"\",\n \"givenName\": \"\"\n },\n \"permissions\": {\n \"key_0\": {\n \"actions\": {\n \"readPrivate\": \"\",\n \"admin\": \"\",\n \"readSSN\": \"\"\n },\n \"jurisdictions\": {\n \"key_0\": {\n \"actions\": {\n \"readPrivate\": \"\",\n \"admin\": \"\",\n \"write\": \"\",\n \"readSSN\": \"\"\n }\n },\n \"key_1\": {\n \"actions\": {\n \"readPrivate\": \"\",\n \"admin\": \"\",\n \"write\": \"\",\n \"readSSN\": \"\"\n }\n }\n }\n },\n \"key_1\": {\n \"actions\": {\n \"readPrivate\": \"\",\n \"admin\": \"\",\n \"readSSN\": \"\"\n },\n \"jurisdictions\": {\n \"key_0\": {\n \"actions\": {\n \"readPrivate\": \"\",\n \"admin\": \"\",\n \"write\": \"\",\n \"readSSN\": \"\"\n }\n }\n }\n }\n }\n}" + "raw": "{\n \"attributes\": {\n \"email\": \"\",\n \"familyName\": \"\",\n \"givenName\": \"\"\n },\n \"permissions\": {\n \"in_3\": {\n \"actions\": {\n \"readPrivate\": \"\",\n \"admin\": \"\",\n \"readSSN\": \"\"\n },\n \"jurisdictions\": {\n \"culpa2\": {\n \"actions\": {\n \"readPrivate\": \"\",\n \"admin\": \"\",\n \"write\": \"\",\n \"readSSN\": \"\"\n }\n },\n \"velit_e\": {\n \"actions\": {\n \"readPrivate\": \"\",\n \"admin\": \"\",\n \"write\": \"\",\n \"readSSN\": \"\"\n }\n },\n \"elit_e6\": {\n \"actions\": {\n \"readPrivate\": \"\",\n \"admin\": \"\",\n \"write\": \"\",\n \"readSSN\": \"\"\n }\n }\n }\n }\n }\n}" }, "description": {}, "header": [ @@ -2839,7 +2839,7 @@ "response": [ { "_postman_previewlanguage": "json", - "body": "{\n \"attributes\": {\n \"email\": \"\",\n \"familyName\": \"\",\n \"givenName\": \"\"\n },\n \"permissions\": {\n \"key_0\": {\n \"actions\": {\n \"readPrivate\": \"\",\n \"admin\": \"\",\n \"readSSN\": \"\"\n },\n \"jurisdictions\": {\n \"key_0\": {\n \"actions\": {\n \"readPrivate\": \"\",\n \"admin\": \"\",\n \"write\": \"\",\n \"readSSN\": \"\"\n }\n }\n }\n },\n \"key_1\": {\n \"actions\": {\n \"readPrivate\": \"\",\n \"admin\": \"\",\n \"readSSN\": \"\"\n },\n \"jurisdictions\": {\n \"key_0\": {\n \"actions\": {\n \"readPrivate\": \"\",\n \"admin\": \"\",\n \"write\": \"\",\n \"readSSN\": \"\"\n }\n },\n \"key_1\": {\n \"actions\": {\n \"readPrivate\": \"\",\n \"admin\": \"\",\n \"write\": \"\",\n \"readSSN\": \"\"\n }\n },\n \"key_2\": {\n \"actions\": {\n \"readPrivate\": \"\",\n \"admin\": \"\",\n \"write\": \"\",\n \"readSSN\": \"\"\n }\n }\n }\n }\n },\n \"status\": \"inactive\",\n \"userId\": \"\"\n}", + "body": "{\n \"attributes\": {\n \"email\": \"\",\n \"familyName\": \"\",\n \"givenName\": \"\"\n },\n \"permissions\": {\n \"ex_be\": {\n \"actions\": {\n \"readPrivate\": \"\",\n \"admin\": \"\",\n \"readSSN\": \"\"\n },\n \"jurisdictions\": {\n \"veniam_016\": {\n \"actions\": {\n \"readPrivate\": \"\",\n \"admin\": \"\",\n \"write\": \"\",\n \"readSSN\": \"\"\n }\n }\n }\n },\n \"consectetur_4_a\": {\n \"actions\": {\n \"readPrivate\": \"\",\n \"admin\": \"\",\n \"readSSN\": \"\"\n },\n \"jurisdictions\": {\n \"laboris_63\": {\n \"actions\": {\n \"readPrivate\": \"\",\n \"admin\": \"\",\n \"write\": \"\",\n \"readSSN\": \"\"\n }\n },\n \"voluptate_139\": {\n \"actions\": {\n \"readPrivate\": \"\",\n \"admin\": \"\",\n \"write\": \"\",\n \"readSSN\": \"\"\n }\n },\n \"dolor_a\": {\n \"actions\": {\n \"readPrivate\": \"\",\n \"admin\": \"\",\n \"write\": \"\",\n \"readSSN\": \"\"\n }\n }\n }\n }\n },\n \"status\": \"inactive\",\n \"userId\": \"\"\n}", "code": 200, "cookie": [], "header": [ @@ -2857,7 +2857,7 @@ "value": "" } ], - "id": "07233e43-01cc-4396-a92b-20a5b1cf56d8", + "id": "e6a3c861-208c-4abd-9136-c0533d9eff71", "name": "200 response", "originalRequest": { "body": { @@ -2868,7 +2868,7 @@ "language": "json" } }, - "raw": "{\n \"attributes\": {\n \"email\": \"\",\n \"familyName\": \"\",\n \"givenName\": \"\"\n },\n \"permissions\": {\n \"key_0\": {\n \"actions\": {\n \"readPrivate\": \"\",\n \"admin\": \"\",\n \"readSSN\": \"\"\n },\n \"jurisdictions\": {\n \"key_0\": {\n \"actions\": {\n \"readPrivate\": \"\",\n \"admin\": \"\",\n \"write\": \"\",\n \"readSSN\": \"\"\n }\n },\n \"key_1\": {\n \"actions\": {\n \"readPrivate\": \"\",\n \"admin\": \"\",\n \"write\": \"\",\n \"readSSN\": \"\"\n }\n }\n }\n },\n \"key_1\": {\n \"actions\": {\n \"readPrivate\": \"\",\n \"admin\": \"\",\n \"readSSN\": \"\"\n },\n \"jurisdictions\": {\n \"key_0\": {\n \"actions\": {\n \"readPrivate\": \"\",\n \"admin\": \"\",\n \"write\": \"\",\n \"readSSN\": \"\"\n }\n }\n }\n }\n }\n}" + "raw": "{\n \"attributes\": {\n \"email\": \"\",\n \"familyName\": \"\",\n \"givenName\": \"\"\n },\n \"permissions\": {\n \"in_3\": {\n \"actions\": {\n \"readPrivate\": \"\",\n \"admin\": \"\",\n \"readSSN\": \"\"\n },\n \"jurisdictions\": {\n \"culpa2\": {\n \"actions\": {\n \"readPrivate\": \"\",\n \"admin\": \"\",\n \"write\": \"\",\n \"readSSN\": \"\"\n }\n },\n \"velit_e\": {\n \"actions\": {\n \"readPrivate\": \"\",\n \"admin\": \"\",\n \"write\": \"\",\n \"readSSN\": \"\"\n }\n },\n \"elit_e6\": {\n \"actions\": {\n \"readPrivate\": \"\",\n \"admin\": \"\",\n \"write\": \"\",\n \"readSSN\": \"\"\n }\n }\n }\n }\n }\n}" }, "header": [ { @@ -2912,7 +2912,7 @@ "item": [ { "event": [], - "id": "648b4bc2-33b0-4650-a745-dd4edd0f39bb", + "id": "2e81153a-7043-4b01-9b99-76d7b9498116", "name": "/v1/compacts/:compact/staff-users/:userId", "protocolProfileBehavior": { "disableBodyPruning": true @@ -2967,7 +2967,7 @@ "response": [ { "_postman_previewlanguage": "json", - "body": "{\n \"attributes\": {\n \"email\": \"\",\n \"familyName\": \"\",\n \"givenName\": \"\"\n },\n \"permissions\": {\n \"key_0\": {\n \"actions\": {\n \"readPrivate\": \"\",\n \"admin\": \"\",\n \"readSSN\": \"\"\n },\n \"jurisdictions\": {\n \"key_0\": {\n \"actions\": {\n \"readPrivate\": \"\",\n \"admin\": \"\",\n \"write\": \"\",\n \"readSSN\": \"\"\n }\n }\n }\n },\n \"key_1\": {\n \"actions\": {\n \"readPrivate\": \"\",\n \"admin\": \"\",\n \"readSSN\": \"\"\n },\n \"jurisdictions\": {\n \"key_0\": {\n \"actions\": {\n \"readPrivate\": \"\",\n \"admin\": \"\",\n \"write\": \"\",\n \"readSSN\": \"\"\n }\n },\n \"key_1\": {\n \"actions\": {\n \"readPrivate\": \"\",\n \"admin\": \"\",\n \"write\": \"\",\n \"readSSN\": \"\"\n }\n },\n \"key_2\": {\n \"actions\": {\n \"readPrivate\": \"\",\n \"admin\": \"\",\n \"write\": \"\",\n \"readSSN\": \"\"\n }\n }\n }\n }\n },\n \"status\": \"inactive\",\n \"userId\": \"\"\n}", + "body": "{\n \"attributes\": {\n \"email\": \"\",\n \"familyName\": \"\",\n \"givenName\": \"\"\n },\n \"permissions\": {\n \"ex_be\": {\n \"actions\": {\n \"readPrivate\": \"\",\n \"admin\": \"\",\n \"readSSN\": \"\"\n },\n \"jurisdictions\": {\n \"veniam_016\": {\n \"actions\": {\n \"readPrivate\": \"\",\n \"admin\": \"\",\n \"write\": \"\",\n \"readSSN\": \"\"\n }\n }\n }\n },\n \"consectetur_4_a\": {\n \"actions\": {\n \"readPrivate\": \"\",\n \"admin\": \"\",\n \"readSSN\": \"\"\n },\n \"jurisdictions\": {\n \"laboris_63\": {\n \"actions\": {\n \"readPrivate\": \"\",\n \"admin\": \"\",\n \"write\": \"\",\n \"readSSN\": \"\"\n }\n },\n \"voluptate_139\": {\n \"actions\": {\n \"readPrivate\": \"\",\n \"admin\": \"\",\n \"write\": \"\",\n \"readSSN\": \"\"\n }\n },\n \"dolor_a\": {\n \"actions\": {\n \"readPrivate\": \"\",\n \"admin\": \"\",\n \"write\": \"\",\n \"readSSN\": \"\"\n }\n }\n }\n }\n },\n \"status\": \"inactive\",\n \"userId\": \"\"\n}", "code": 200, "cookie": [], "header": [ @@ -2985,7 +2985,7 @@ "value": "" } ], - "id": "d6d72f73-0166-4425-bfa8-2bafc5f0222c", + "id": "f679add1-f98e-4f4c-ba19-d684e5770077", "name": "200 response", "originalRequest": { "body": {}, @@ -3032,7 +3032,7 @@ "value": "application/json" } ], - "id": "ed0b585d-7daf-4606-a919-e3540b58e6e3", + "id": "fff92877-94a0-4593-bab1-4fae7fc3dc27", "name": "404 response", "originalRequest": { "body": {}, @@ -3072,7 +3072,7 @@ }, { "event": [], - "id": "a093a52c-1227-465a-bfbc-48d7eee98cc4", + "id": "2d77e4dc-499e-46e5-a96b-df4fb7a63464", "name": "/v1/compacts/:compact/staff-users/:userId", "protocolProfileBehavior": { "disableBodyPruning": true @@ -3136,7 +3136,7 @@ "value": "application/json" } ], - "id": "f7182271-4921-48df-adf8-c8693f3959db", + "id": "637002f0-f96b-4f47-8bdd-03c44761e797", "name": "200 response", "originalRequest": { "body": {}, @@ -3183,7 +3183,7 @@ "value": "application/json" } ], - "id": "df891f36-b46e-4f4f-bad9-69d81c4cb26f", + "id": "43158115-12ae-4f8c-bfc7-4df9be2755a9", "name": "404 response", "originalRequest": { "body": {}, @@ -3223,7 +3223,7 @@ }, { "event": [], - "id": "29ed6a5b-e506-49d7-95ba-f0d61d308619", + "id": "143d6ed1-35d6-4c04-8476-ce44408b90d9", "name": "/v1/compacts/:compact/staff-users/:userId", "protocolProfileBehavior": { "disableBodyPruning": true @@ -3237,7 +3237,7 @@ "language": "json" } }, - "raw": "{\n \"permissions\": {\n \"key_0\": {\n \"actions\": {\n \"readPrivate\": \"\",\n \"admin\": \"\",\n \"readSSN\": \"\"\n },\n \"jurisdictions\": {\n \"key_0\": {\n \"actions\": {\n \"readPrivate\": \"\",\n \"admin\": \"\",\n \"write\": \"\",\n \"readSSN\": \"\"\n }\n },\n \"key_1\": {\n \"actions\": {\n \"readPrivate\": \"\",\n \"admin\": \"\",\n \"write\": \"\",\n \"readSSN\": \"\"\n }\n }\n }\n }\n }\n}" + "raw": "{\n \"permissions\": {\n \"laborumc\": {\n \"actions\": {\n \"readPrivate\": \"\",\n \"admin\": \"\",\n \"readSSN\": \"\"\n },\n \"jurisdictions\": {\n \"incididunt2d9\": {\n \"actions\": {\n \"readPrivate\": \"\",\n \"admin\": \"\",\n \"write\": \"\",\n \"readSSN\": \"\"\n }\n },\n \"sitc0\": {\n \"actions\": {\n \"readPrivate\": \"\",\n \"admin\": \"\",\n \"write\": \"\",\n \"readSSN\": \"\"\n }\n },\n \"fugiat__\": {\n \"actions\": {\n \"readPrivate\": \"\",\n \"admin\": \"\",\n \"write\": \"\",\n \"readSSN\": \"\"\n }\n },\n \"Duis_d\": {\n \"actions\": {\n \"readPrivate\": \"\",\n \"admin\": \"\",\n \"write\": \"\",\n \"readSSN\": \"\"\n }\n }\n }\n },\n \"nostrudfb5\": {\n \"actions\": {\n \"readPrivate\": \"\",\n \"admin\": \"\",\n \"readSSN\": \"\"\n },\n \"jurisdictions\": {\n \"ea390\": {\n \"actions\": {\n \"readPrivate\": \"\",\n \"admin\": \"\",\n \"write\": \"\",\n \"readSSN\": \"\"\n }\n }\n }\n }\n }\n}" }, "description": {}, "header": [ @@ -3291,7 +3291,7 @@ "response": [ { "_postman_previewlanguage": "json", - "body": "{\n \"attributes\": {\n \"email\": \"\",\n \"familyName\": \"\",\n \"givenName\": \"\"\n },\n \"permissions\": {\n \"key_0\": {\n \"actions\": {\n \"readPrivate\": \"\",\n \"admin\": \"\",\n \"readSSN\": \"\"\n },\n \"jurisdictions\": {\n \"key_0\": {\n \"actions\": {\n \"readPrivate\": \"\",\n \"admin\": \"\",\n \"write\": \"\",\n \"readSSN\": \"\"\n }\n }\n }\n },\n \"key_1\": {\n \"actions\": {\n \"readPrivate\": \"\",\n \"admin\": \"\",\n \"readSSN\": \"\"\n },\n \"jurisdictions\": {\n \"key_0\": {\n \"actions\": {\n \"readPrivate\": \"\",\n \"admin\": \"\",\n \"write\": \"\",\n \"readSSN\": \"\"\n }\n },\n \"key_1\": {\n \"actions\": {\n \"readPrivate\": \"\",\n \"admin\": \"\",\n \"write\": \"\",\n \"readSSN\": \"\"\n }\n },\n \"key_2\": {\n \"actions\": {\n \"readPrivate\": \"\",\n \"admin\": \"\",\n \"write\": \"\",\n \"readSSN\": \"\"\n }\n }\n }\n }\n },\n \"status\": \"inactive\",\n \"userId\": \"\"\n}", + "body": "{\n \"attributes\": {\n \"email\": \"\",\n \"familyName\": \"\",\n \"givenName\": \"\"\n },\n \"permissions\": {\n \"ex_be\": {\n \"actions\": {\n \"readPrivate\": \"\",\n \"admin\": \"\",\n \"readSSN\": \"\"\n },\n \"jurisdictions\": {\n \"veniam_016\": {\n \"actions\": {\n \"readPrivate\": \"\",\n \"admin\": \"\",\n \"write\": \"\",\n \"readSSN\": \"\"\n }\n }\n }\n },\n \"consectetur_4_a\": {\n \"actions\": {\n \"readPrivate\": \"\",\n \"admin\": \"\",\n \"readSSN\": \"\"\n },\n \"jurisdictions\": {\n \"laboris_63\": {\n \"actions\": {\n \"readPrivate\": \"\",\n \"admin\": \"\",\n \"write\": \"\",\n \"readSSN\": \"\"\n }\n },\n \"voluptate_139\": {\n \"actions\": {\n \"readPrivate\": \"\",\n \"admin\": \"\",\n \"write\": \"\",\n \"readSSN\": \"\"\n }\n },\n \"dolor_a\": {\n \"actions\": {\n \"readPrivate\": \"\",\n \"admin\": \"\",\n \"write\": \"\",\n \"readSSN\": \"\"\n }\n }\n }\n }\n },\n \"status\": \"inactive\",\n \"userId\": \"\"\n}", "code": 200, "cookie": [], "header": [ @@ -3309,7 +3309,7 @@ "value": "" } ], - "id": "27880447-9902-4734-8b69-406da1478b4f", + "id": "9c4fc9de-52c7-4bc8-84dc-0ef229aa8110", "name": "200 response", "originalRequest": { "body": { @@ -3320,7 +3320,7 @@ "language": "json" } }, - "raw": "{\n \"permissions\": {\n \"key_0\": {\n \"actions\": {\n \"readPrivate\": \"\",\n \"admin\": \"\",\n \"readSSN\": \"\"\n },\n \"jurisdictions\": {\n \"key_0\": {\n \"actions\": {\n \"readPrivate\": \"\",\n \"admin\": \"\",\n \"write\": \"\",\n \"readSSN\": \"\"\n }\n },\n \"key_1\": {\n \"actions\": {\n \"readPrivate\": \"\",\n \"admin\": \"\",\n \"write\": \"\",\n \"readSSN\": \"\"\n }\n }\n }\n }\n }\n}" + "raw": "{\n \"permissions\": {\n \"laborumc\": {\n \"actions\": {\n \"readPrivate\": \"\",\n \"admin\": \"\",\n \"readSSN\": \"\"\n },\n \"jurisdictions\": {\n \"incididunt2d9\": {\n \"actions\": {\n \"readPrivate\": \"\",\n \"admin\": \"\",\n \"write\": \"\",\n \"readSSN\": \"\"\n }\n },\n \"sitc0\": {\n \"actions\": {\n \"readPrivate\": \"\",\n \"admin\": \"\",\n \"write\": \"\",\n \"readSSN\": \"\"\n }\n },\n \"fugiat__\": {\n \"actions\": {\n \"readPrivate\": \"\",\n \"admin\": \"\",\n \"write\": \"\",\n \"readSSN\": \"\"\n }\n },\n \"Duis_d\": {\n \"actions\": {\n \"readPrivate\": \"\",\n \"admin\": \"\",\n \"write\": \"\",\n \"readSSN\": \"\"\n }\n }\n }\n },\n \"nostrudfb5\": {\n \"actions\": {\n \"readPrivate\": \"\",\n \"admin\": \"\",\n \"readSSN\": \"\"\n },\n \"jurisdictions\": {\n \"ea390\": {\n \"actions\": {\n \"readPrivate\": \"\",\n \"admin\": \"\",\n \"write\": \"\",\n \"readSSN\": \"\"\n }\n }\n }\n }\n }\n}" }, "header": [ { @@ -3369,7 +3369,7 @@ "value": "application/json" } ], - "id": "ac1bd524-48d2-42d5-bdb8-d062e7aab824", + "id": "c8ad3d2f-9ec3-4d72-b296-84f366697c5a", "name": "404 response", "originalRequest": { "body": { @@ -3380,7 +3380,7 @@ "language": "json" } }, - "raw": "{\n \"permissions\": {\n \"key_0\": {\n \"actions\": {\n \"readPrivate\": \"\",\n \"admin\": \"\",\n \"readSSN\": \"\"\n },\n \"jurisdictions\": {\n \"key_0\": {\n \"actions\": {\n \"readPrivate\": \"\",\n \"admin\": \"\",\n \"write\": \"\",\n \"readSSN\": \"\"\n }\n },\n \"key_1\": {\n \"actions\": {\n \"readPrivate\": \"\",\n \"admin\": \"\",\n \"write\": \"\",\n \"readSSN\": \"\"\n }\n }\n }\n }\n }\n}" + "raw": "{\n \"permissions\": {\n \"laborumc\": {\n \"actions\": {\n \"readPrivate\": \"\",\n \"admin\": \"\",\n \"readSSN\": \"\"\n },\n \"jurisdictions\": {\n \"incididunt2d9\": {\n \"actions\": {\n \"readPrivate\": \"\",\n \"admin\": \"\",\n \"write\": \"\",\n \"readSSN\": \"\"\n }\n },\n \"sitc0\": {\n \"actions\": {\n \"readPrivate\": \"\",\n \"admin\": \"\",\n \"write\": \"\",\n \"readSSN\": \"\"\n }\n },\n \"fugiat__\": {\n \"actions\": {\n \"readPrivate\": \"\",\n \"admin\": \"\",\n \"write\": \"\",\n \"readSSN\": \"\"\n }\n },\n \"Duis_d\": {\n \"actions\": {\n \"readPrivate\": \"\",\n \"admin\": \"\",\n \"write\": \"\",\n \"readSSN\": \"\"\n }\n }\n }\n },\n \"nostrudfb5\": {\n \"actions\": {\n \"readPrivate\": \"\",\n \"admin\": \"\",\n \"readSSN\": \"\"\n },\n \"jurisdictions\": {\n \"ea390\": {\n \"actions\": {\n \"readPrivate\": \"\",\n \"admin\": \"\",\n \"write\": \"\",\n \"readSSN\": \"\"\n }\n }\n }\n }\n }\n}" }, "header": [ { @@ -3425,7 +3425,7 @@ "item": [ { "event": [], - "id": "981ab810-876b-4214-b163-01a696db70db", + "id": "8684361b-360c-453f-be0a-b0df9d6f83b4", "name": "/v1/compacts/:compact/staff-users/:userId/reinvite", "protocolProfileBehavior": { "disableBodyPruning": true @@ -3490,7 +3490,7 @@ "value": "application/json" } ], - "id": "e3ab420c-c3c5-4b98-bd53-d489649d7a6e", + "id": "2d24aa37-985d-4bf0-b2d6-674fa3000818", "name": "200 response", "originalRequest": { "body": {}, @@ -3538,7 +3538,7 @@ "value": "application/json" } ], - "id": "98dd9007-b396-4051-898e-f2a1ed511e4e", + "id": "c753cb34-3a0b-490c-971e-a953e3caac2a", "name": "404 response", "originalRequest": { "body": {}, @@ -3592,6 +3592,137 @@ ], "name": "compacts" }, + { + "description": "", + "item": [ + { + "description": "", + "item": [ + { + "description": "", + "item": [ + { + "event": [], + "id": "63b56f5c-7bae-4f45-a617-a018edd73281", + "name": "/v1/flags/:flagId/check", + "protocolProfileBehavior": { + "disableBodyPruning": true + }, + "request": { + "auth": { + "type": "noauth" + }, + "body": { + "mode": "raw", + "options": { + "raw": { + "headerFamily": "json", + "language": "json" + } + }, + "raw": "{\n \"context\": {\n \"userId\": \"\",\n \"customAttributes\": {\n \"cupidatateb5\": \"\",\n \"doloref72\": \"\"\n }\n }\n}" + }, + "description": {}, + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "method": "POST", + "name": "/v1/flags/:flagId/check", + "url": { + "host": [ + "{{baseUrl}}" + ], + "path": [ + "v1", + "flags", + ":flagId", + "check" + ], + "query": [], + "variable": [ + { + "description": { + "content": "(Required) ", + "type": "text/plain" + }, + "disabled": false, + "key": "flagId", + "type": "any", + "value": "" + } + ] + } + }, + "response": [ + { + "_postman_previewlanguage": "json", + "body": "{\n \"enabled\": \"\"\n}", + "code": 200, + "cookie": [], + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "id": "2620158d-9990-4130-bcb6-3646fba42b10", + "name": "200 response", + "originalRequest": { + "body": { + "mode": "raw", + "options": { + "raw": { + "headerFamily": "json", + "language": "json" + } + }, + "raw": "{\n \"context\": {\n \"userId\": \"\",\n \"customAttributes\": {\n \"cupidatateb5\": \"\",\n \"doloref72\": \"\"\n }\n }\n}" + }, + "header": [ + { + "key": "Content-Type", + "value": "application/json" + }, + { + "key": "Accept", + "value": "application/json" + } + ], + "method": "POST", + "url": { + "host": [ + "{{baseUrl}}" + ], + "path": [ + "v1", + "flags", + ":flagId", + "check" + ], + "query": [], + "variable": [] + } + }, + "status": "OK" + } + ] + } + ], + "name": "check" + } + ], + "name": "{flagId}" + } + ], + "name": "flags" + }, { "auth": { "bearer": [ @@ -3610,7 +3741,7 @@ "item": [ { "event": [], - "id": "553a98b2-6401-4e78-a5c4-dee036c7df65", + "id": "d22cc441-ae2e-4336-8d92-107b141bc1be", "name": "/v1/provider-users/initiateRecovery", "protocolProfileBehavior": { "disableBodyPruning": true @@ -3627,7 +3758,7 @@ "language": "json" } }, - "raw": "{\n \"compact\": \"aslp\",\n \"dob\": \"2589-11-05\",\n \"familyName\": \"\",\n \"givenName\": \"\",\n \"jurisdiction\": \"ma\",\n \"licenseType\": \"occupational therapy assistant\",\n \"partialSocial\": \"4869\",\n \"password\": \"\",\n \"recaptchaToken\": \"\",\n \"username\": \"\"\n}" + "raw": "{\n \"compact\": \"coun\",\n \"dob\": \"2301-11-19\",\n \"familyName\": \"\",\n \"givenName\": \"\",\n \"jurisdiction\": \"az\",\n \"licenseType\": \"audiologist\",\n \"partialSocial\": \"5778\",\n \"password\": \"\",\n \"recaptchaToken\": \"\",\n \"username\": \"\"\n}" }, "description": {}, "header": [ @@ -3667,7 +3798,7 @@ "value": "application/json" } ], - "id": "10ed4781-1747-43dd-aad9-dc6cfc534296", + "id": "6ba813ef-0534-4f1b-a689-68383e36d814", "name": "200 response", "originalRequest": { "body": { @@ -3678,7 +3809,7 @@ "language": "json" } }, - "raw": "{\n \"compact\": \"aslp\",\n \"dob\": \"2589-11-05\",\n \"familyName\": \"\",\n \"givenName\": \"\",\n \"jurisdiction\": \"ma\",\n \"licenseType\": \"occupational therapy assistant\",\n \"partialSocial\": \"4869\",\n \"password\": \"\",\n \"recaptchaToken\": \"\",\n \"username\": \"\"\n}" + "raw": "{\n \"compact\": \"coun\",\n \"dob\": \"2301-11-19\",\n \"familyName\": \"\",\n \"givenName\": \"\",\n \"jurisdiction\": \"az\",\n \"licenseType\": \"audiologist\",\n \"partialSocial\": \"5778\",\n \"password\": \"\",\n \"recaptchaToken\": \"\",\n \"username\": \"\"\n}" }, "header": [ { @@ -3716,7 +3847,7 @@ "item": [ { "event": [], - "id": "414d3c84-1376-4da3-b077-10d4a055deeb", + "id": "708305fa-361c-4825-ab35-41af00b3dd2b", "name": "/v1/provider-users/me", "protocolProfileBehavior": { "disableBodyPruning": true @@ -3748,7 +3879,7 @@ "response": [ { "_postman_previewlanguage": "json", - "body": "{\n \"birthMonthDay\": \"09-29\",\n \"compact\": \"aslp\",\n \"dateOfExpiration\": \"2551-08-07\",\n \"dateOfUpdate\": \"2473-11-31\",\n \"familyName\": \"\",\n \"givenName\": \"\",\n \"licenseJurisdiction\": \"oh\",\n \"licenses\": [\n {\n \"compact\": \"coun\",\n \"compactEligibility\": \"eligible\",\n \"dateOfExpiration\": \"2422-10-19\",\n \"dateOfIssuance\": \"1176-08-27\",\n \"dateOfRenewal\": \"2158-12-07\",\n \"dateOfUpdate\": \"2503-12-31\",\n \"familyName\": \"\",\n \"givenName\": \"\",\n \"history\": [\n {\n \"compact\": \"aslp\",\n \"dateOfUpdate\": \"2760-10-30\",\n \"jurisdiction\": \"nd\",\n \"previous\": {\n \"dateOfExpiration\": \"1222-05-31\",\n \"dateOfIssuance\": \"1311-02-17\",\n \"dateOfRenewal\": \"1203-02-08\",\n \"familyName\": \"\",\n \"givenName\": \"\",\n \"homeAddressCity\": \"\",\n \"homeAddressPostalCode\": \"\",\n \"homeAddressState\": \"\",\n \"homeAddressStreet1\": \"\",\n \"jurisdictionUploadedCompactEligibility\": \"eligible\",\n \"jurisdictionUploadedLicenseStatus\": \"inactive\",\n \"middleName\": \"\",\n \"homeAddressStreet2\": \"\",\n \"npi\": \"3849686342\",\n \"compactEligibility\": \"ineligible\",\n \"dateOfBirth\": \"2224-12-22\",\n \"suffix\": \"\",\n \"emailAddress\": \"\",\n \"phoneNumber\": \"+817339777344\",\n \"licenseStatus\": \"inactive\",\n \"licenseNumber\": \"\",\n \"licenseStatusName\": \"\"\n },\n \"type\": \"licenseUpdate\",\n \"updateType\": \"licenseDeactivation\",\n \"removedValues\": [\n \"\",\n \"\"\n ],\n \"licenseType\": \"licensed professional counselor\",\n \"updatedValues\": {\n \"homeAddressStreet2\": \"\",\n \"npi\": \"1253840341\",\n \"homeAddressPostalCode\": \"\",\n \"givenName\": \"\",\n \"homeAddressStreet1\": \"\",\n \"compactEligibility\": \"eligible\",\n \"jurisdictionUploadedCompactEligibility\": \"ineligible\",\n \"dateOfBirth\": \"1741-12-01\",\n \"jurisdictionUploadedLicenseStatus\": \"active\",\n \"suffix\": \"\",\n \"dateOfIssuance\": \"2369-06-30\",\n \"emailAddress\": \"\",\n \"dateOfExpiration\": \"1500-12-09\",\n \"phoneNumber\": \"+24819955084\",\n \"homeAddressState\": \"\",\n \"dateOfRenewal\": \"2569-06-30\",\n \"licenseStatus\": \"inactive\",\n \"familyName\": \"\",\n \"homeAddressCity\": \"\",\n \"licenseNumber\": \"\",\n \"middleName\": \"\",\n \"licenseStatusName\": \"\"\n }\n },\n {\n \"compact\": \"coun\",\n \"dateOfUpdate\": \"2718-10-03\",\n \"jurisdiction\": \"wy\",\n \"previous\": {\n \"dateOfExpiration\": \"2661-12-06\",\n \"dateOfIssuance\": \"1835-09-30\",\n \"dateOfRenewal\": \"2896-01-30\",\n \"familyName\": \"\",\n \"givenName\": \"\",\n \"homeAddressCity\": \"\",\n \"homeAddressPostalCode\": \"\",\n \"homeAddressState\": \"\",\n \"homeAddressStreet1\": \"\",\n \"jurisdictionUploadedCompactEligibility\": \"ineligible\",\n \"jurisdictionUploadedLicenseStatus\": \"active\",\n \"middleName\": \"\",\n \"homeAddressStreet2\": \"\",\n \"npi\": \"9915283898\",\n \"compactEligibility\": \"eligible\",\n \"dateOfBirth\": \"2313-12-20\",\n \"suffix\": \"\",\n \"emailAddress\": \"\",\n \"phoneNumber\": \"+197882673191259\",\n \"licenseStatus\": \"inactive\",\n \"licenseNumber\": \"\",\n \"licenseStatusName\": \"\"\n },\n \"type\": \"licenseUpdate\",\n \"updateType\": \"lifting_encumbrance\",\n \"removedValues\": [\n \"\",\n \"\"\n ],\n \"licenseType\": \"occupational therapist\",\n \"updatedValues\": {\n \"homeAddressStreet2\": \"\",\n \"npi\": \"7336401807\",\n \"homeAddressPostalCode\": \"\",\n \"givenName\": \"\",\n \"homeAddressStreet1\": \"\",\n \"compactEligibility\": \"eligible\",\n \"jurisdictionUploadedCompactEligibility\": \"eligible\",\n \"dateOfBirth\": \"1160-11-04\",\n \"jurisdictionUploadedLicenseStatus\": \"inactive\",\n \"suffix\": \"\",\n \"dateOfIssuance\": \"2827-12-06\",\n \"emailAddress\": \"\",\n \"dateOfExpiration\": \"2834-02-15\",\n \"phoneNumber\": \"+3902382233\",\n \"homeAddressState\": \"\",\n \"dateOfRenewal\": \"2998-01-10\",\n \"licenseStatus\": \"active\",\n \"familyName\": \"\",\n \"homeAddressCity\": \"\",\n \"licenseNumber\": \"\",\n \"middleName\": \"\",\n \"licenseStatusName\": \"\"\n }\n }\n ],\n \"homeAddressCity\": \"\",\n \"homeAddressPostalCode\": \"\",\n \"homeAddressState\": \"\",\n \"homeAddressStreet1\": \"\",\n \"jurisdiction\": \"ri\",\n \"jurisdictionUploadedCompactEligibility\": \"eligible\",\n \"jurisdictionUploadedLicenseStatus\": \"inactive\",\n \"licenseStatus\": \"inactive\",\n \"licenseType\": \"occupational therapy assistant\",\n \"middleName\": \"\",\n \"providerId\": \"349bdce7-941e-4ff7-87bd-dd5b61f4324a\",\n \"type\": \"license-home\",\n \"homeAddressStreet2\": \"\",\n \"suffix\": \"\",\n \"emailAddress\": \"\",\n \"licenseNumber\": \"\",\n \"npi\": \"5115904651\",\n \"dateOfBirth\": \"2994-06-04\",\n \"ssnLastFour\": \"8743\",\n \"phoneNumber\": \"+35782742956\",\n \"licenseStatusName\": \"\",\n \"adverseActions\": [\n {\n \"actionAgainst\": \"\",\n \"adverseActionId\": \"\",\n \"clinicalPrivilegeActionCategory\": \"\",\n \"compact\": \"aslp\",\n \"creationDate\": \"2755-12-30\",\n \"dateOfUpdate\": \"2401-04-31\",\n \"effectiveStartDate\": \"2833-11-07\",\n \"encumbranceType\": \"\",\n \"jurisdiction\": \"me\",\n \"licenseType\": \"\",\n \"licenseTypeAbbreviation\": \"\",\n \"providerId\": \"2219d489-f54d-474f-b4c9-78c762c288ac\",\n \"type\": \"adverseAction\",\n \"effectiveLiftDate\": \"2693-06-15\",\n \"liftingUser\": \"\"\n },\n {\n \"actionAgainst\": \"\",\n \"adverseActionId\": \"\",\n \"clinicalPrivilegeActionCategory\": \"\",\n \"compact\": \"aslp\",\n \"creationDate\": \"2573-07-02\",\n \"dateOfUpdate\": \"2676-03-23\",\n \"effectiveStartDate\": \"2871-10-31\",\n \"encumbranceType\": \"\",\n \"jurisdiction\": \"me\",\n \"licenseType\": \"\",\n \"licenseTypeAbbreviation\": \"\",\n \"providerId\": \"ac666c27-7ccb-4c53-842e-fb95ce22727f\",\n \"type\": \"adverseAction\",\n \"effectiveLiftDate\": \"2642-06-15\",\n \"liftingUser\": \"\"\n }\n ]\n },\n {\n \"compact\": \"coun\",\n \"compactEligibility\": \"ineligible\",\n \"dateOfExpiration\": \"2914-10-31\",\n \"dateOfIssuance\": \"2897-12-21\",\n \"dateOfRenewal\": \"1438-11-01\",\n \"dateOfUpdate\": \"1534-12-31\",\n \"familyName\": \"\",\n \"givenName\": \"\",\n \"history\": [\n {\n \"compact\": \"octp\",\n \"dateOfUpdate\": \"2982-10-30\",\n \"jurisdiction\": \"sd\",\n \"previous\": {\n \"dateOfExpiration\": \"1099-04-16\",\n \"dateOfIssuance\": \"1148-12-30\",\n \"dateOfRenewal\": \"1579-09-30\",\n \"familyName\": \"\",\n \"givenName\": \"\",\n \"homeAddressCity\": \"\",\n \"homeAddressPostalCode\": \"\",\n \"homeAddressState\": \"\",\n \"homeAddressStreet1\": \"\",\n \"jurisdictionUploadedCompactEligibility\": \"ineligible\",\n \"jurisdictionUploadedLicenseStatus\": \"active\",\n \"middleName\": \"\",\n \"homeAddressStreet2\": \"\",\n \"npi\": \"2133645263\",\n \"compactEligibility\": \"eligible\",\n \"dateOfBirth\": \"2653-12-30\",\n \"suffix\": \"\",\n \"emailAddress\": \"\",\n \"phoneNumber\": \"+9549304435346\",\n \"licenseStatus\": \"active\",\n \"licenseNumber\": \"\",\n \"licenseStatusName\": \"\"\n },\n \"type\": \"licenseUpdate\",\n \"updateType\": \"emailChange\",\n \"removedValues\": [\n \"\",\n \"\"\n ],\n \"licenseType\": \"occupational therapy assistant\",\n \"updatedValues\": {\n \"homeAddressStreet2\": \"\",\n \"npi\": \"9102395494\",\n \"homeAddressPostalCode\": \"\",\n \"givenName\": \"\",\n \"homeAddressStreet1\": \"\",\n \"compactEligibility\": \"eligible\",\n \"jurisdictionUploadedCompactEligibility\": \"eligible\",\n \"dateOfBirth\": \"2140-02-23\",\n \"jurisdictionUploadedLicenseStatus\": \"inactive\",\n \"suffix\": \"\",\n \"dateOfIssuance\": \"2556-10-30\",\n \"emailAddress\": \"\",\n \"dateOfExpiration\": \"1166-05-13\",\n \"phoneNumber\": \"+723613998\",\n \"homeAddressState\": \"\",\n \"dateOfRenewal\": \"1521-11-31\",\n \"licenseStatus\": \"inactive\",\n \"familyName\": \"\",\n \"homeAddressCity\": \"\",\n \"licenseNumber\": \"\",\n \"middleName\": \"\",\n \"licenseStatusName\": \"\"\n }\n },\n {\n \"compact\": \"coun\",\n \"dateOfUpdate\": \"2326-04-21\",\n \"jurisdiction\": \"va\",\n \"previous\": {\n \"dateOfExpiration\": \"1319-05-01\",\n \"dateOfIssuance\": \"1136-01-07\",\n \"dateOfRenewal\": \"1798-12-18\",\n \"familyName\": \"\",\n \"givenName\": \"\",\n \"homeAddressCity\": \"\",\n \"homeAddressPostalCode\": \"\",\n \"homeAddressState\": \"\",\n \"homeAddressStreet1\": \"\",\n \"jurisdictionUploadedCompactEligibility\": \"ineligible\",\n \"jurisdictionUploadedLicenseStatus\": \"active\",\n \"middleName\": \"\",\n \"homeAddressStreet2\": \"\",\n \"npi\": \"6131652411\",\n \"compactEligibility\": \"ineligible\",\n \"dateOfBirth\": \"2895-05-30\",\n \"suffix\": \"\",\n \"emailAddress\": \"\",\n \"phoneNumber\": \"+516316834803391\",\n \"licenseStatus\": \"active\",\n \"licenseNumber\": \"\",\n \"licenseStatusName\": \"\"\n },\n \"type\": \"licenseUpdate\",\n \"updateType\": \"emailChange\",\n \"removedValues\": [\n \"\",\n \"\"\n ],\n \"licenseType\": \"occupational therapist\",\n \"updatedValues\": {\n \"homeAddressStreet2\": \"\",\n \"npi\": \"7963314050\",\n \"homeAddressPostalCode\": \"\",\n \"givenName\": \"\",\n \"homeAddressStreet1\": \"\",\n \"compactEligibility\": \"ineligible\",\n \"jurisdictionUploadedCompactEligibility\": \"eligible\",\n \"dateOfBirth\": \"1002-12-06\",\n \"jurisdictionUploadedLicenseStatus\": \"inactive\",\n \"suffix\": \"\",\n \"dateOfIssuance\": \"1042-10-05\",\n \"emailAddress\": \"\",\n \"dateOfExpiration\": \"1291-12-30\",\n \"phoneNumber\": \"+983515171158248\",\n \"homeAddressState\": \"\",\n \"dateOfRenewal\": \"2351-03-10\",\n \"licenseStatus\": \"inactive\",\n \"familyName\": \"\",\n \"homeAddressCity\": \"\",\n \"licenseNumber\": \"\",\n \"middleName\": \"\",\n \"licenseStatusName\": \"\"\n }\n }\n ],\n \"homeAddressCity\": \"\",\n \"homeAddressPostalCode\": \"\",\n \"homeAddressState\": \"\",\n \"homeAddressStreet1\": \"\",\n \"jurisdiction\": \"la\",\n \"jurisdictionUploadedCompactEligibility\": \"eligible\",\n \"jurisdictionUploadedLicenseStatus\": \"active\",\n \"licenseStatus\": \"active\",\n \"licenseType\": \"audiologist\",\n \"middleName\": \"\",\n \"providerId\": \"eca478f3-343c-4fc1-83c9-97faf27b6990\",\n \"type\": \"license-home\",\n \"homeAddressStreet2\": \"\",\n \"suffix\": \"\",\n \"emailAddress\": \"\",\n \"licenseNumber\": \"\",\n \"npi\": \"5472385917\",\n \"dateOfBirth\": \"2389-11-29\",\n \"ssnLastFour\": \"5264\",\n \"phoneNumber\": \"+955712482678352\",\n \"licenseStatusName\": \"\",\n \"adverseActions\": [\n {\n \"actionAgainst\": \"\",\n \"adverseActionId\": \"\",\n \"clinicalPrivilegeActionCategory\": \"\",\n \"compact\": \"octp\",\n \"creationDate\": \"2442-11-27\",\n \"dateOfUpdate\": \"1692-12-30\",\n \"effectiveStartDate\": \"1653-11-07\",\n \"encumbranceType\": \"\",\n \"jurisdiction\": \"wi\",\n \"licenseType\": \"\",\n \"licenseTypeAbbreviation\": \"\",\n \"providerId\": \"504c0320-3a92-431e-81fb-692dbca7d66f\",\n \"type\": \"adverseAction\",\n \"effectiveLiftDate\": \"2740-01-30\",\n \"liftingUser\": \"\"\n },\n {\n \"actionAgainst\": \"\",\n \"adverseActionId\": \"\",\n \"clinicalPrivilegeActionCategory\": \"\",\n \"compact\": \"coun\",\n \"creationDate\": \"1491-11-06\",\n \"dateOfUpdate\": \"1660-07-30\",\n \"effectiveStartDate\": \"2658-12-30\",\n \"encumbranceType\": \"\",\n \"jurisdiction\": \"ks\",\n \"licenseType\": \"\",\n \"licenseTypeAbbreviation\": \"\",\n \"providerId\": \"b34ce427-aa42-49c5-ad6a-40b7b041c081\",\n \"type\": \"adverseAction\",\n \"effectiveLiftDate\": \"2291-12-03\",\n \"liftingUser\": \"\"\n }\n ]\n }\n ],\n \"militaryAffiliations\": [\n {\n \"affiliationType\": \"militaryMemberSpouse\",\n \"compact\": \"coun\",\n \"dateOfUpdate\": \"1051-02-01\",\n \"dateOfUpload\": \"1673-12-20\",\n \"fileNames\": [\n \"\",\n \"\"\n ],\n \"providerId\": \"40af7950-c543-4022-bb09-1b27c8b55ffb\",\n \"status\": \"inactive\",\n \"type\": \"militaryAffiliation\",\n \"downloadLinks\": [\n {\n \"fileName\": \"\",\n \"url\": \"\"\n },\n {\n \"fileName\": \"\",\n \"url\": \"\"\n }\n ]\n },\n {\n \"affiliationType\": \"militaryMemberSpouse\",\n \"compact\": \"coun\",\n \"dateOfUpdate\": \"1650-10-30\",\n \"dateOfUpload\": \"2598-12-16\",\n \"fileNames\": [\n \"\",\n \"\"\n ],\n \"providerId\": \"214b459e-8ce1-421f-bc29-5fee8820f97f\",\n \"status\": \"inactive\",\n \"type\": \"militaryAffiliation\",\n \"downloadLinks\": [\n {\n \"fileName\": \"\",\n \"url\": \"\"\n },\n {\n \"fileName\": \"\",\n \"url\": \"\"\n }\n ]\n }\n ],\n \"privilegeJurisdictions\": [\n \"nm\",\n \"sc\"\n ],\n \"privileges\": [\n {\n \"administratorSetStatus\": \"inactive\",\n \"attestations\": [\n {\n \"attestationId\": \"\",\n \"version\": \"\"\n },\n {\n \"attestationId\": \"\",\n \"version\": \"\"\n }\n ],\n \"compact\": \"octp\",\n \"compactTransactionId\": \"\",\n \"dateOfExpiration\": \"2496-10-10\",\n \"dateOfIssuance\": \"1545-04-21\",\n \"dateOfRenewal\": \"1364-09-05\",\n \"dateOfUpdate\": \"1208-12-02\",\n \"history\": [\n {\n \"compact\": \"coun\",\n \"dateOfUpdate\": \"2871-05-30\",\n \"jurisdiction\": \"de\",\n \"previous\": {\n \"administratorSetStatus\": \"active\",\n \"attestations\": [\n {\n \"attestationId\": \"\",\n \"version\": \"\"\n },\n {\n \"attestationId\": \"\",\n \"version\": \"\"\n }\n ],\n \"compactTransactionId\": \"\",\n \"dateOfExpiration\": \"2807-12-11\",\n \"dateOfIssuance\": \"2958-05-30\",\n \"dateOfRenewal\": \"2653-06-29\",\n \"dateOfUpdate\": \"2610-11-31\",\n \"licenseJurisdiction\": \"hi\",\n \"privilegeId\": \"\",\n \"compact\": \"octp\",\n \"jurisdiction\": \"nj\",\n \"type\": \"privilege\",\n \"providerId\": \"75252a5a-0e7f-43f7-af44-6315b72533af\",\n \"status\": \"inactive\"\n },\n \"type\": \"privilegeUpdate\",\n \"updateType\": \"other\",\n \"removedValues\": [\n \"\",\n \"\"\n ],\n \"licenseType\": \"occupational therapist\",\n \"updatedValues\": {\n \"licenseJurisdiction\": \"co\",\n \"compact\": \"aslp\",\n \"jurisdiction\": \"me\",\n \"attestations\": [\n {\n \"attestationId\": \"\",\n \"version\": \"\"\n },\n {\n \"attestationId\": \"\",\n \"version\": \"\"\n }\n ],\n \"type\": \"privilege\",\n \"compactTransactionId\": \"\",\n \"dateOfIssuance\": \"1884-10-31\",\n \"administratorSetStatus\": \"active\",\n \"dateOfExpiration\": \"1902-11-10\",\n \"privilegeId\": \"\",\n \"providerId\": \"6ab0ae00-a913-4420-aaa1-d145c6f750ee\",\n \"dateOfRenewal\": \"2116-03-30\",\n \"dateOfUpdate\": \"1305-01-30\",\n \"status\": \"active\"\n }\n },\n {\n \"compact\": \"octp\",\n \"dateOfUpdate\": \"2586-08-19\",\n \"jurisdiction\": \"fl\",\n \"previous\": {\n \"administratorSetStatus\": \"active\",\n \"attestations\": [\n {\n \"attestationId\": \"\",\n \"version\": \"\"\n },\n {\n \"attestationId\": \"\",\n \"version\": \"\"\n }\n ],\n \"compactTransactionId\": \"\",\n \"dateOfExpiration\": \"2133-11-03\",\n \"dateOfIssuance\": \"2079-11-06\",\n \"dateOfRenewal\": \"1912-12-31\",\n \"dateOfUpdate\": \"1305-03-06\",\n \"licenseJurisdiction\": \"vt\",\n \"privilegeId\": \"\",\n \"compact\": \"aslp\",\n \"jurisdiction\": \"nc\",\n \"type\": \"privilege\",\n \"providerId\": \"bf7ab190-6ed1-4692-b1ff-b4c762569494\",\n \"status\": \"active\"\n },\n \"type\": \"privilegeUpdate\",\n \"updateType\": \"emailChange\",\n \"removedValues\": [\n \"\",\n \"\"\n ],\n \"licenseType\": \"occupational therapy assistant\",\n \"updatedValues\": {\n \"licenseJurisdiction\": \"ks\",\n \"compact\": \"aslp\",\n \"jurisdiction\": \"hi\",\n \"attestations\": [\n {\n \"attestationId\": \"\",\n \"version\": \"\"\n },\n {\n \"attestationId\": \"\",\n \"version\": \"\"\n }\n ],\n \"type\": \"privilege\",\n \"compactTransactionId\": \"\",\n \"dateOfIssuance\": \"1230-10-02\",\n \"administratorSetStatus\": \"active\",\n \"dateOfExpiration\": \"1140-07-06\",\n \"privilegeId\": \"\",\n \"providerId\": \"e183902c-0bf7-48dd-8fc0-f3c7f35417d5\",\n \"dateOfRenewal\": \"1971-10-30\",\n \"dateOfUpdate\": \"1785-10-31\",\n \"status\": \"active\"\n }\n }\n ],\n \"jurisdiction\": \"dc\",\n \"licenseJurisdiction\": \"il\",\n \"licenseType\": \"licensed professional counselor\",\n \"privilegeId\": \"\",\n \"providerId\": \"839bdc13-0a83-452d-8418-a5142124772d\",\n \"status\": \"active\",\n \"type\": \"privilege\",\n \"adverseActions\": [\n {\n \"actionAgainst\": \"\",\n \"adverseActionId\": \"\",\n \"clinicalPrivilegeActionCategory\": \"\",\n \"compact\": \"octp\",\n \"creationDate\": \"1050-10-18\",\n \"dateOfUpdate\": \"1670-01-31\",\n \"effectiveStartDate\": \"1520-05-31\",\n \"encumbranceType\": \"\",\n \"jurisdiction\": \"ky\",\n \"licenseType\": \"\",\n \"licenseTypeAbbreviation\": \"\",\n \"providerId\": \"ebe6c8f1-70a9-468e-bc5e-a4bbd188893d\",\n \"type\": \"adverseAction\",\n \"effectiveLiftDate\": \"1290-01-26\",\n \"liftingUser\": \"\"\n },\n {\n \"actionAgainst\": \"\",\n \"adverseActionId\": \"\",\n \"clinicalPrivilegeActionCategory\": \"\",\n \"compact\": \"aslp\",\n \"creationDate\": \"2122-08-31\",\n \"dateOfUpdate\": \"1461-11-07\",\n \"effectiveStartDate\": \"1709-11-31\",\n \"encumbranceType\": \"\",\n \"jurisdiction\": \"mt\",\n \"licenseType\": \"\",\n \"licenseTypeAbbreviation\": \"\",\n \"providerId\": \"790dec56-9655-452c-a00a-225907b28e1f\",\n \"type\": \"adverseAction\",\n \"effectiveLiftDate\": \"1627-12-08\",\n \"liftingUser\": \"\"\n }\n ]\n },\n {\n \"administratorSetStatus\": \"active\",\n \"attestations\": [\n {\n \"attestationId\": \"\",\n \"version\": \"\"\n },\n {\n \"attestationId\": \"\",\n \"version\": \"\"\n }\n ],\n \"compact\": \"octp\",\n \"compactTransactionId\": \"\",\n \"dateOfExpiration\": \"1655-10-06\",\n \"dateOfIssuance\": \"1307-01-30\",\n \"dateOfRenewal\": \"1641-10-30\",\n \"dateOfUpdate\": \"2282-05-25\",\n \"history\": [\n {\n \"compact\": \"aslp\",\n \"dateOfUpdate\": \"2733-03-22\",\n \"jurisdiction\": \"tx\",\n \"previous\": {\n \"administratorSetStatus\": \"active\",\n \"attestations\": [\n {\n \"attestationId\": \"\",\n \"version\": \"\"\n },\n {\n \"attestationId\": \"\",\n \"version\": \"\"\n }\n ],\n \"compactTransactionId\": \"\",\n \"dateOfExpiration\": \"1621-09-11\",\n \"dateOfIssuance\": \"1359-10-30\",\n \"dateOfRenewal\": \"2863-09-28\",\n \"dateOfUpdate\": \"1325-10-23\",\n \"licenseJurisdiction\": \"me\",\n \"privilegeId\": \"\",\n \"compact\": \"aslp\",\n \"jurisdiction\": \"oh\",\n \"type\": \"privilege\",\n \"providerId\": \"d3643c67-ed32-4f74-a6cf-d6e7f824c632\",\n \"status\": \"active\"\n },\n \"type\": \"privilegeUpdate\",\n \"updateType\": \"emailChange\",\n \"removedValues\": [\n \"\",\n \"\"\n ],\n \"licenseType\": \"audiologist\",\n \"updatedValues\": {\n \"licenseJurisdiction\": \"ms\",\n \"compact\": \"octp\",\n \"jurisdiction\": \"la\",\n \"attestations\": [\n {\n \"attestationId\": \"\",\n \"version\": \"\"\n },\n {\n \"attestationId\": \"\",\n \"version\": \"\"\n }\n ],\n \"type\": \"privilege\",\n \"compactTransactionId\": \"\",\n \"dateOfIssuance\": \"2567-06-21\",\n \"administratorSetStatus\": \"active\",\n \"dateOfExpiration\": \"1620-10-31\",\n \"privilegeId\": \"\",\n \"providerId\": \"87b330a0-8529-4bba-8c3f-442866bd5ef0\",\n \"dateOfRenewal\": \"2324-08-30\",\n \"dateOfUpdate\": \"2492-10-30\",\n \"status\": \"active\"\n }\n },\n {\n \"compact\": \"octp\",\n \"dateOfUpdate\": \"2653-11-26\",\n \"jurisdiction\": \"pa\",\n \"previous\": {\n \"administratorSetStatus\": \"inactive\",\n \"attestations\": [\n {\n \"attestationId\": \"\",\n \"version\": \"\"\n },\n {\n \"attestationId\": \"\",\n \"version\": \"\"\n }\n ],\n \"compactTransactionId\": \"\",\n \"dateOfExpiration\": \"1382-06-07\",\n \"dateOfIssuance\": \"1460-12-30\",\n \"dateOfRenewal\": \"1927-11-30\",\n \"dateOfUpdate\": \"1215-11-20\",\n \"licenseJurisdiction\": \"ak\",\n \"privilegeId\": \"\",\n \"compact\": \"aslp\",\n \"jurisdiction\": \"pa\",\n \"type\": \"privilege\",\n \"providerId\": \"6fcb01a4-52eb-490f-a9bb-18e20dc4237f\",\n \"status\": \"inactive\"\n },\n \"type\": \"privilegeUpdate\",\n \"updateType\": \"lifting_encumbrance\",\n \"removedValues\": [\n \"\",\n \"\"\n ],\n \"licenseType\": \"speech-language pathologist\",\n \"updatedValues\": {\n \"licenseJurisdiction\": \"or\",\n \"compact\": \"coun\",\n \"jurisdiction\": \"ri\",\n \"attestations\": [\n {\n \"attestationId\": \"\",\n \"version\": \"\"\n },\n {\n \"attestationId\": \"\",\n \"version\": \"\"\n }\n ],\n \"type\": \"privilege\",\n \"compactTransactionId\": \"\",\n \"dateOfIssuance\": \"2785-10-30\",\n \"administratorSetStatus\": \"inactive\",\n \"dateOfExpiration\": \"2086-12-09\",\n \"privilegeId\": \"\",\n \"providerId\": \"f158113c-4fb0-4f87-812e-43d8bf431d0b\",\n \"dateOfRenewal\": \"1234-05-18\",\n \"dateOfUpdate\": \"2693-05-30\",\n \"status\": \"inactive\"\n }\n }\n ],\n \"jurisdiction\": \"sc\",\n \"licenseJurisdiction\": \"pa\",\n \"licenseType\": \"licensed professional counselor\",\n \"privilegeId\": \"\",\n \"providerId\": \"1bf3d98e-4e67-4055-8ef2-3cfdf4ce943a\",\n \"status\": \"inactive\",\n \"type\": \"privilege\",\n \"adverseActions\": [\n {\n \"actionAgainst\": \"\",\n \"adverseActionId\": \"\",\n \"clinicalPrivilegeActionCategory\": \"\",\n \"compact\": \"coun\",\n \"creationDate\": \"2620-01-05\",\n \"dateOfUpdate\": \"2263-12-16\",\n \"effectiveStartDate\": \"1392-08-27\",\n \"encumbranceType\": \"\",\n \"jurisdiction\": \"wi\",\n \"licenseType\": \"\",\n \"licenseTypeAbbreviation\": \"\",\n \"providerId\": \"81428441-fb07-4fe0-8dc4-4c4c7420988e\",\n \"type\": \"adverseAction\",\n \"effectiveLiftDate\": \"1047-06-02\",\n \"liftingUser\": \"\"\n },\n {\n \"actionAgainst\": \"\",\n \"adverseActionId\": \"\",\n \"clinicalPrivilegeActionCategory\": \"\",\n \"compact\": \"coun\",\n \"creationDate\": \"1676-04-30\",\n \"dateOfUpdate\": \"1110-04-31\",\n \"effectiveStartDate\": \"1045-03-22\",\n \"encumbranceType\": \"\",\n \"jurisdiction\": \"ia\",\n \"licenseType\": \"\",\n \"licenseTypeAbbreviation\": \"\",\n \"providerId\": \"436f032e-c3b1-41e2-afe8-ab26446f06cb\",\n \"type\": \"adverseAction\",\n \"effectiveLiftDate\": \"1101-11-06\",\n \"liftingUser\": \"\"\n }\n ]\n }\n ],\n \"providerId\": \"3c9de2be-9898-4ab6-85df-6d0ab3acc6e2\",\n \"type\": \"provider\",\n \"npi\": \"3401297518\",\n \"compactEligibility\": \"ineligible\",\n \"jurisdictionUploadedCompactEligibility\": \"ineligible\",\n \"dateOfBirth\": \"2440-09-05\",\n \"jurisdictionUploadedLicenseStatus\": \"active\",\n \"suffix\": \"\",\n \"currentHomeJurisdiction\": \"ky\",\n \"ssnLastFour\": \"3099\",\n \"licenseStatus\": \"active\",\n \"middleName\": \"\",\n \"compactConnectRegisteredEmailAddress\": \"\"\n}", + "body": "{\n \"birthMonthDay\": \"18-31\",\n \"compact\": \"octp\",\n \"dateOfExpiration\": \"2943-11-09\",\n \"dateOfUpdate\": \"1250-02-24\",\n \"familyName\": \"\",\n \"givenName\": \"\",\n \"licenseJurisdiction\": \"ma\",\n \"licenses\": [\n {\n \"compact\": \"coun\",\n \"compactEligibility\": \"ineligible\",\n \"dateOfExpiration\": \"1065-06-23\",\n \"dateOfIssuance\": \"2970-07-14\",\n \"dateOfRenewal\": \"2821-11-02\",\n \"dateOfUpdate\": \"1704-11-31\",\n \"familyName\": \"\",\n \"givenName\": \"\",\n \"history\": [\n {\n \"compact\": \"aslp\",\n \"dateOfUpdate\": \"2231-10-14\",\n \"jurisdiction\": \"hi\",\n \"previous\": {\n \"dateOfExpiration\": \"2644-03-03\",\n \"dateOfIssuance\": \"2575-03-07\",\n \"dateOfRenewal\": \"1739-10-21\",\n \"familyName\": \"\",\n \"givenName\": \"\",\n \"homeAddressCity\": \"\",\n \"homeAddressPostalCode\": \"\",\n \"homeAddressState\": \"\",\n \"homeAddressStreet1\": \"\",\n \"jurisdictionUploadedCompactEligibility\": \"ineligible\",\n \"jurisdictionUploadedLicenseStatus\": \"inactive\",\n \"middleName\": \"\",\n \"homeAddressStreet2\": \"\",\n \"npi\": \"9863917273\",\n \"compactEligibility\": \"ineligible\",\n \"dateOfBirth\": \"2395-05-31\",\n \"suffix\": \"\",\n \"emailAddress\": \"\",\n \"phoneNumber\": \"+98530144953\",\n \"licenseStatus\": \"active\",\n \"licenseNumber\": \"\",\n \"licenseStatusName\": \"\"\n },\n \"type\": \"licenseUpdate\",\n \"updateType\": \"encumbrance\",\n \"removedValues\": [\n \"\",\n \"\"\n ],\n \"licenseType\": \"occupational therapy assistant\",\n \"updatedValues\": {\n \"homeAddressStreet2\": \"\",\n \"npi\": \"5294519747\",\n \"homeAddressPostalCode\": \"\",\n \"givenName\": \"\",\n \"homeAddressStreet1\": \"\",\n \"compactEligibility\": \"ineligible\",\n \"jurisdictionUploadedCompactEligibility\": \"eligible\",\n \"dateOfBirth\": \"1738-04-02\",\n \"jurisdictionUploadedLicenseStatus\": \"inactive\",\n \"suffix\": \"\",\n \"dateOfIssuance\": \"1535-01-31\",\n \"emailAddress\": \"\",\n \"dateOfExpiration\": \"1345-12-01\",\n \"phoneNumber\": \"+88983379\",\n \"homeAddressState\": \"\",\n \"dateOfRenewal\": \"2163-06-30\",\n \"licenseStatus\": \"inactive\",\n \"familyName\": \"\",\n \"homeAddressCity\": \"\",\n \"licenseNumber\": \"\",\n \"middleName\": \"\",\n \"licenseStatusName\": \"\"\n }\n },\n {\n \"compact\": \"octp\",\n \"dateOfUpdate\": \"1258-12-01\",\n \"jurisdiction\": \"pa\",\n \"previous\": {\n \"dateOfExpiration\": \"2242-11-19\",\n \"dateOfIssuance\": \"1812-10-29\",\n \"dateOfRenewal\": \"2431-02-30\",\n \"familyName\": \"\",\n \"givenName\": \"\",\n \"homeAddressCity\": \"\",\n \"homeAddressPostalCode\": \"\",\n \"homeAddressState\": \"\",\n \"homeAddressStreet1\": \"\",\n \"jurisdictionUploadedCompactEligibility\": \"eligible\",\n \"jurisdictionUploadedLicenseStatus\": \"active\",\n \"middleName\": \"\",\n \"homeAddressStreet2\": \"\",\n \"npi\": \"9204408223\",\n \"compactEligibility\": \"ineligible\",\n \"dateOfBirth\": \"2105-06-05\",\n \"suffix\": \"\",\n \"emailAddress\": \"\",\n \"phoneNumber\": \"+12442530879818\",\n \"licenseStatus\": \"inactive\",\n \"licenseNumber\": \"\",\n \"licenseStatusName\": \"\"\n },\n \"type\": \"licenseUpdate\",\n \"updateType\": \"emailChange\",\n \"removedValues\": [\n \"\",\n \"\"\n ],\n \"licenseType\": \"audiologist\",\n \"updatedValues\": {\n \"homeAddressStreet2\": \"\",\n \"npi\": \"4923994654\",\n \"homeAddressPostalCode\": \"\",\n \"givenName\": \"\",\n \"homeAddressStreet1\": \"\",\n \"compactEligibility\": \"ineligible\",\n \"jurisdictionUploadedCompactEligibility\": \"eligible\",\n \"dateOfBirth\": \"1541-10-09\",\n \"jurisdictionUploadedLicenseStatus\": \"inactive\",\n \"suffix\": \"\",\n \"dateOfIssuance\": \"2607-07-13\",\n \"emailAddress\": \"\",\n \"dateOfExpiration\": \"1347-12-30\",\n \"phoneNumber\": \"+994685399812775\",\n \"homeAddressState\": \"\",\n \"dateOfRenewal\": \"1766-07-31\",\n \"licenseStatus\": \"active\",\n \"familyName\": \"\",\n \"homeAddressCity\": \"\",\n \"licenseNumber\": \"\",\n \"middleName\": \"\",\n \"licenseStatusName\": \"\"\n }\n }\n ],\n \"homeAddressCity\": \"\",\n \"homeAddressPostalCode\": \"\",\n \"homeAddressState\": \"\",\n \"homeAddressStreet1\": \"\",\n \"jurisdiction\": \"al\",\n \"jurisdictionUploadedCompactEligibility\": \"eligible\",\n \"jurisdictionUploadedLicenseStatus\": \"active\",\n \"licenseStatus\": \"inactive\",\n \"licenseType\": \"occupational therapist\",\n \"middleName\": \"\",\n \"providerId\": \"eaf8b40b-5198-4246-a5c2-85f8975cad8f\",\n \"type\": \"license-home\",\n \"homeAddressStreet2\": \"\",\n \"suffix\": \"\",\n \"emailAddress\": \"\",\n \"licenseNumber\": \"\",\n \"npi\": \"4815134028\",\n \"dateOfBirth\": \"1567-11-21\",\n \"ssnLastFour\": \"4241\",\n \"phoneNumber\": \"+899877959\",\n \"licenseStatusName\": \"\",\n \"adverseActions\": [\n {\n \"actionAgainst\": \"\",\n \"adverseActionId\": \"\",\n \"clinicalPrivilegeActionCategory\": \"\",\n \"compact\": \"octp\",\n \"creationDate\": \"1766-02-29\",\n \"dateOfUpdate\": \"2423-10-07\",\n \"effectiveStartDate\": \"1333-01-31\",\n \"encumbranceType\": \"\",\n \"jurisdiction\": \"id\",\n \"licenseType\": \"\",\n \"licenseTypeAbbreviation\": \"\",\n \"providerId\": \"b4b528b2-8076-4d22-8fdd-591a6de8de47\",\n \"type\": \"adverseAction\",\n \"effectiveLiftDate\": \"2144-12-15\",\n \"liftingUser\": \"\"\n },\n {\n \"actionAgainst\": \"\",\n \"adverseActionId\": \"\",\n \"clinicalPrivilegeActionCategory\": \"\",\n \"compact\": \"octp\",\n \"creationDate\": \"2486-08-03\",\n \"dateOfUpdate\": \"1648-10-15\",\n \"effectiveStartDate\": \"2584-12-31\",\n \"encumbranceType\": \"\",\n \"jurisdiction\": \"de\",\n \"licenseType\": \"\",\n \"licenseTypeAbbreviation\": \"\",\n \"providerId\": \"d03a4970-7cda-4c5d-8662-1a1464439782\",\n \"type\": \"adverseAction\",\n \"effectiveLiftDate\": \"1395-01-22\",\n \"liftingUser\": \"\"\n }\n ]\n },\n {\n \"compact\": \"octp\",\n \"compactEligibility\": \"ineligible\",\n \"dateOfExpiration\": \"2661-09-03\",\n \"dateOfIssuance\": \"1334-04-31\",\n \"dateOfRenewal\": \"2596-12-30\",\n \"dateOfUpdate\": \"2431-11-27\",\n \"familyName\": \"\",\n \"givenName\": \"\",\n \"history\": [\n {\n \"compact\": \"coun\",\n \"dateOfUpdate\": \"1927-10-31\",\n \"jurisdiction\": \"ut\",\n \"previous\": {\n \"dateOfExpiration\": \"2097-02-30\",\n \"dateOfIssuance\": \"2104-10-24\",\n \"dateOfRenewal\": \"2520-07-05\",\n \"familyName\": \"\",\n \"givenName\": \"\",\n \"homeAddressCity\": \"\",\n \"homeAddressPostalCode\": \"\",\n \"homeAddressState\": \"\",\n \"homeAddressStreet1\": \"\",\n \"jurisdictionUploadedCompactEligibility\": \"ineligible\",\n \"jurisdictionUploadedLicenseStatus\": \"inactive\",\n \"middleName\": \"\",\n \"homeAddressStreet2\": \"\",\n \"npi\": \"9123939912\",\n \"compactEligibility\": \"ineligible\",\n \"dateOfBirth\": \"2205-11-30\",\n \"suffix\": \"\",\n \"emailAddress\": \"\",\n \"phoneNumber\": \"+50133362\",\n \"licenseStatus\": \"active\",\n \"licenseNumber\": \"\",\n \"licenseStatusName\": \"\"\n },\n \"type\": \"licenseUpdate\",\n \"updateType\": \"expiration\",\n \"removedValues\": [\n \"\",\n \"\"\n ],\n \"licenseType\": \"licensed professional counselor\",\n \"updatedValues\": {\n \"homeAddressStreet2\": \"\",\n \"npi\": \"7668946187\",\n \"homeAddressPostalCode\": \"\",\n \"givenName\": \"\",\n \"homeAddressStreet1\": \"\",\n \"compactEligibility\": \"eligible\",\n \"jurisdictionUploadedCompactEligibility\": \"eligible\",\n \"dateOfBirth\": \"2129-11-30\",\n \"jurisdictionUploadedLicenseStatus\": \"active\",\n \"suffix\": \"\",\n \"dateOfIssuance\": \"1429-10-30\",\n \"emailAddress\": \"\",\n \"dateOfExpiration\": \"1751-10-14\",\n \"phoneNumber\": \"+36228193470590\",\n \"homeAddressState\": \"\",\n \"dateOfRenewal\": \"1193-11-31\",\n \"licenseStatus\": \"active\",\n \"familyName\": \"\",\n \"homeAddressCity\": \"\",\n \"licenseNumber\": \"\",\n \"middleName\": \"\",\n \"licenseStatusName\": \"\"\n }\n },\n {\n \"compact\": \"octp\",\n \"dateOfUpdate\": \"1769-09-09\",\n \"jurisdiction\": \"il\",\n \"previous\": {\n \"dateOfExpiration\": \"2156-12-30\",\n \"dateOfIssuance\": \"1510-12-23\",\n \"dateOfRenewal\": \"2845-10-31\",\n \"familyName\": \"\",\n \"givenName\": \"\",\n \"homeAddressCity\": \"\",\n \"homeAddressPostalCode\": \"\",\n \"homeAddressState\": \"\",\n \"homeAddressStreet1\": \"\",\n \"jurisdictionUploadedCompactEligibility\": \"eligible\",\n \"jurisdictionUploadedLicenseStatus\": \"inactive\",\n \"middleName\": \"\",\n \"homeAddressStreet2\": \"\",\n \"npi\": \"6167646658\",\n \"compactEligibility\": \"ineligible\",\n \"dateOfBirth\": \"1263-11-30\",\n \"suffix\": \"\",\n \"emailAddress\": \"\",\n \"phoneNumber\": \"+2374225608467\",\n \"licenseStatus\": \"active\",\n \"licenseNumber\": \"\",\n \"licenseStatusName\": \"\"\n },\n \"type\": \"licenseUpdate\",\n \"updateType\": \"issuance\",\n \"removedValues\": [\n \"\",\n \"\"\n ],\n \"licenseType\": \"speech-language pathologist\",\n \"updatedValues\": {\n \"homeAddressStreet2\": \"\",\n \"npi\": \"5287069345\",\n \"homeAddressPostalCode\": \"\",\n \"givenName\": \"\",\n \"homeAddressStreet1\": \"\",\n \"compactEligibility\": \"eligible\",\n \"jurisdictionUploadedCompactEligibility\": \"ineligible\",\n \"dateOfBirth\": \"1015-11-13\",\n \"jurisdictionUploadedLicenseStatus\": \"inactive\",\n \"suffix\": \"\",\n \"dateOfIssuance\": \"1237-03-31\",\n \"emailAddress\": \"\",\n \"dateOfExpiration\": \"2563-07-31\",\n \"phoneNumber\": \"+110806100\",\n \"homeAddressState\": \"\",\n \"dateOfRenewal\": \"2400-11-31\",\n \"licenseStatus\": \"active\",\n \"familyName\": \"\",\n \"homeAddressCity\": \"\",\n \"licenseNumber\": \"\",\n \"middleName\": \"\",\n \"licenseStatusName\": \"\"\n }\n }\n ],\n \"homeAddressCity\": \"\",\n \"homeAddressPostalCode\": \"\",\n \"homeAddressState\": \"\",\n \"homeAddressStreet1\": \"\",\n \"jurisdiction\": \"mn\",\n \"jurisdictionUploadedCompactEligibility\": \"ineligible\",\n \"jurisdictionUploadedLicenseStatus\": \"active\",\n \"licenseStatus\": \"inactive\",\n \"licenseType\": \"occupational therapy assistant\",\n \"middleName\": \"\",\n \"providerId\": \"06b7efa9-e2ff-4231-a21c-ac6f81025226\",\n \"type\": \"license-home\",\n \"homeAddressStreet2\": \"\",\n \"suffix\": \"\",\n \"emailAddress\": \"\",\n \"licenseNumber\": \"\",\n \"npi\": \"7730730531\",\n \"dateOfBirth\": \"1902-04-20\",\n \"ssnLastFour\": \"2779\",\n \"phoneNumber\": \"+5584183492\",\n \"licenseStatusName\": \"\",\n \"adverseActions\": [\n {\n \"actionAgainst\": \"\",\n \"adverseActionId\": \"\",\n \"clinicalPrivilegeActionCategory\": \"\",\n \"compact\": \"aslp\",\n \"creationDate\": \"2305-01-17\",\n \"dateOfUpdate\": \"2745-11-22\",\n \"effectiveStartDate\": \"2196-10-31\",\n \"encumbranceType\": \"\",\n \"jurisdiction\": \"ct\",\n \"licenseType\": \"\",\n \"licenseTypeAbbreviation\": \"\",\n \"providerId\": \"dd8cde9f-58aa-481d-848e-4ec200705451\",\n \"type\": \"adverseAction\",\n \"effectiveLiftDate\": \"1920-05-04\",\n \"liftingUser\": \"\"\n },\n {\n \"actionAgainst\": \"\",\n \"adverseActionId\": \"\",\n \"clinicalPrivilegeActionCategory\": \"\",\n \"compact\": \"aslp\",\n \"creationDate\": \"2012-09-31\",\n \"dateOfUpdate\": \"2250-08-18\",\n \"effectiveStartDate\": \"1575-12-31\",\n \"encumbranceType\": \"\",\n \"jurisdiction\": \"tn\",\n \"licenseType\": \"\",\n \"licenseTypeAbbreviation\": \"\",\n \"providerId\": \"e341914c-587b-4208-b23f-a1a55bfa01a7\",\n \"type\": \"adverseAction\",\n \"effectiveLiftDate\": \"2871-11-06\",\n \"liftingUser\": \"\"\n }\n ]\n }\n ],\n \"militaryAffiliations\": [\n {\n \"affiliationType\": \"militaryMember\",\n \"compact\": \"octp\",\n \"dateOfUpdate\": \"1991-11-30\",\n \"dateOfUpload\": \"2882-12-30\",\n \"fileNames\": [\n \"\",\n \"\"\n ],\n \"providerId\": \"fc8e686c-c65d-4c3a-8e49-71b2807f80b9\",\n \"status\": \"inactive\",\n \"type\": \"militaryAffiliation\",\n \"downloadLinks\": [\n {\n \"fileName\": \"\",\n \"url\": \"\"\n },\n {\n \"fileName\": \"\",\n \"url\": \"\"\n }\n ]\n },\n {\n \"affiliationType\": \"militaryMemberSpouse\",\n \"compact\": \"aslp\",\n \"dateOfUpdate\": \"2520-11-05\",\n \"dateOfUpload\": \"2131-01-26\",\n \"fileNames\": [\n \"\",\n \"\"\n ],\n \"providerId\": \"5961ad9b-f28d-40a7-ae4a-9acd7949999a\",\n \"status\": \"inactive\",\n \"type\": \"militaryAffiliation\",\n \"downloadLinks\": [\n {\n \"fileName\": \"\",\n \"url\": \"\"\n },\n {\n \"fileName\": \"\",\n \"url\": \"\"\n }\n ]\n }\n ],\n \"privilegeJurisdictions\": [\n \"id\",\n \"ms\"\n ],\n \"privileges\": [\n {\n \"administratorSetStatus\": \"inactive\",\n \"attestations\": [\n {\n \"attestationId\": \"\",\n \"version\": \"\"\n },\n {\n \"attestationId\": \"\",\n \"version\": \"\"\n }\n ],\n \"compact\": \"coun\",\n \"compactTransactionId\": \"\",\n \"dateOfExpiration\": \"1274-12-30\",\n \"dateOfIssuance\": \"1780-12-31\",\n \"dateOfRenewal\": \"2292-09-22\",\n \"dateOfUpdate\": \"2130-10-31\",\n \"history\": [\n {\n \"compact\": \"octp\",\n \"dateOfUpdate\": \"1421-07-22\",\n \"jurisdiction\": \"co\",\n \"previous\": {\n \"administratorSetStatus\": \"active\",\n \"attestations\": [\n {\n \"attestationId\": \"\",\n \"version\": \"\"\n },\n {\n \"attestationId\": \"\",\n \"version\": \"\"\n }\n ],\n \"compactTransactionId\": \"\",\n \"dateOfExpiration\": \"2061-11-29\",\n \"dateOfIssuance\": \"1610-02-07\",\n \"dateOfRenewal\": \"2537-03-05\",\n \"dateOfUpdate\": \"2703-09-02\",\n \"licenseJurisdiction\": \"pa\",\n \"privilegeId\": \"\",\n \"compact\": \"coun\",\n \"jurisdiction\": \"ca\",\n \"type\": \"privilege\",\n \"providerId\": \"2969addd-238f-443e-8c68-5d139471c1e3\",\n \"status\": \"inactive\"\n },\n \"type\": \"privilegeUpdate\",\n \"updateType\": \"lifting_encumbrance\",\n \"removedValues\": [\n \"\",\n \"\"\n ],\n \"licenseType\": \"licensed professional counselor\",\n \"updatedValues\": {\n \"licenseJurisdiction\": \"ok\",\n \"compact\": \"octp\",\n \"jurisdiction\": \"tx\",\n \"attestations\": [\n {\n \"attestationId\": \"\",\n \"version\": \"\"\n },\n {\n \"attestationId\": \"\",\n \"version\": \"\"\n }\n ],\n \"type\": \"privilege\",\n \"compactTransactionId\": \"\",\n \"dateOfIssuance\": \"1024-09-05\",\n \"administratorSetStatus\": \"inactive\",\n \"dateOfExpiration\": \"2718-10-31\",\n \"privilegeId\": \"\",\n \"providerId\": \"6d2eb9c3-4f99-40a1-84e5-a1b6761782d6\",\n \"dateOfRenewal\": \"2059-04-08\",\n \"dateOfUpdate\": \"2107-10-05\",\n \"status\": \"inactive\"\n }\n },\n {\n \"compact\": \"coun\",\n \"dateOfUpdate\": \"2077-06-30\",\n \"jurisdiction\": \"il\",\n \"previous\": {\n \"administratorSetStatus\": \"inactive\",\n \"attestations\": [\n {\n \"attestationId\": \"\",\n \"version\": \"\"\n },\n {\n \"attestationId\": \"\",\n \"version\": \"\"\n }\n ],\n \"compactTransactionId\": \"\",\n \"dateOfExpiration\": \"2676-08-03\",\n \"dateOfIssuance\": \"2083-09-30\",\n \"dateOfRenewal\": \"2848-07-07\",\n \"dateOfUpdate\": \"2488-11-03\",\n \"licenseJurisdiction\": \"ms\",\n \"privilegeId\": \"\",\n \"compact\": \"octp\",\n \"jurisdiction\": \"or\",\n \"type\": \"privilege\",\n \"providerId\": \"f7c090f1-b36d-407f-9a4d-e0857ab098d4\",\n \"status\": \"active\"\n },\n \"type\": \"privilegeUpdate\",\n \"updateType\": \"deactivation\",\n \"removedValues\": [\n \"\",\n \"\"\n ],\n \"licenseType\": \"occupational therapy assistant\",\n \"updatedValues\": {\n \"licenseJurisdiction\": \"pa\",\n \"compact\": \"aslp\",\n \"jurisdiction\": \"ut\",\n \"attestations\": [\n {\n \"attestationId\": \"\",\n \"version\": \"\"\n },\n {\n \"attestationId\": \"\",\n \"version\": \"\"\n }\n ],\n \"type\": \"privilege\",\n \"compactTransactionId\": \"\",\n \"dateOfIssuance\": \"2090-10-31\",\n \"administratorSetStatus\": \"inactive\",\n \"dateOfExpiration\": \"2307-09-28\",\n \"privilegeId\": \"\",\n \"providerId\": \"401c04bf-75cc-4653-b9f8-4e73dce99729\",\n \"dateOfRenewal\": \"1545-11-30\",\n \"dateOfUpdate\": \"1889-04-19\",\n \"status\": \"inactive\"\n }\n }\n ],\n \"jurisdiction\": \"vi\",\n \"licenseJurisdiction\": \"ma\",\n \"licenseType\": \"occupational therapist\",\n \"privilegeId\": \"\",\n \"providerId\": \"54df1999-73b0-471d-b6fb-3c44e95cabbf\",\n \"status\": \"active\",\n \"type\": \"privilege\",\n \"adverseActions\": [\n {\n \"actionAgainst\": \"\",\n \"adverseActionId\": \"\",\n \"clinicalPrivilegeActionCategory\": \"\",\n \"compact\": \"aslp\",\n \"creationDate\": \"2306-03-05\",\n \"dateOfUpdate\": \"2057-12-12\",\n \"effectiveStartDate\": \"1856-06-26\",\n \"encumbranceType\": \"\",\n \"jurisdiction\": \"mo\",\n \"licenseType\": \"\",\n \"licenseTypeAbbreviation\": \"\",\n \"providerId\": \"cf8ecf9c-781b-460b-afae-1384db52d08c\",\n \"type\": \"adverseAction\",\n \"effectiveLiftDate\": \"1417-05-31\",\n \"liftingUser\": \"\"\n },\n {\n \"actionAgainst\": \"\",\n \"adverseActionId\": \"\",\n \"clinicalPrivilegeActionCategory\": \"\",\n \"compact\": \"aslp\",\n \"creationDate\": \"2891-04-02\",\n \"dateOfUpdate\": \"1660-03-23\",\n \"effectiveStartDate\": \"1306-09-30\",\n \"encumbranceType\": \"\",\n \"jurisdiction\": \"wa\",\n \"licenseType\": \"\",\n \"licenseTypeAbbreviation\": \"\",\n \"providerId\": \"6e1e2851-7cc2-4f7c-8b1b-8bba307deba1\",\n \"type\": \"adverseAction\",\n \"effectiveLiftDate\": \"1254-03-30\",\n \"liftingUser\": \"\"\n }\n ]\n },\n {\n \"administratorSetStatus\": \"inactive\",\n \"attestations\": [\n {\n \"attestationId\": \"\",\n \"version\": \"\"\n },\n {\n \"attestationId\": \"\",\n \"version\": \"\"\n }\n ],\n \"compact\": \"aslp\",\n \"compactTransactionId\": \"\",\n \"dateOfExpiration\": \"1340-03-31\",\n \"dateOfIssuance\": \"1260-10-18\",\n \"dateOfRenewal\": \"2300-12-16\",\n \"dateOfUpdate\": \"2905-08-01\",\n \"history\": [\n {\n \"compact\": \"octp\",\n \"dateOfUpdate\": \"1316-10-31\",\n \"jurisdiction\": \"ca\",\n \"previous\": {\n \"administratorSetStatus\": \"active\",\n \"attestations\": [\n {\n \"attestationId\": \"\",\n \"version\": \"\"\n },\n {\n \"attestationId\": \"\",\n \"version\": \"\"\n }\n ],\n \"compactTransactionId\": \"\",\n \"dateOfExpiration\": \"2143-11-31\",\n \"dateOfIssuance\": \"1911-12-30\",\n \"dateOfRenewal\": \"2349-04-04\",\n \"dateOfUpdate\": \"2101-10-28\",\n \"licenseJurisdiction\": \"va\",\n \"privilegeId\": \"\",\n \"compact\": \"coun\",\n \"jurisdiction\": \"md\",\n \"type\": \"privilege\",\n \"providerId\": \"8893ab59-563e-42e8-96b7-ba058426d34e\",\n \"status\": \"inactive\"\n },\n \"type\": \"privilegeUpdate\",\n \"updateType\": \"expiration\",\n \"removedValues\": [\n \"\",\n \"\"\n ],\n \"licenseType\": \"occupational therapy assistant\",\n \"updatedValues\": {\n \"licenseJurisdiction\": \"wy\",\n \"compact\": \"octp\",\n \"jurisdiction\": \"vt\",\n \"attestations\": [\n {\n \"attestationId\": \"\",\n \"version\": \"\"\n },\n {\n \"attestationId\": \"\",\n \"version\": \"\"\n }\n ],\n \"type\": \"privilege\",\n \"compactTransactionId\": \"\",\n \"dateOfIssuance\": \"2543-08-13\",\n \"administratorSetStatus\": \"active\",\n \"dateOfExpiration\": \"1133-03-19\",\n \"privilegeId\": \"\",\n \"providerId\": \"a0a26f5a-c6ce-4db9-9a4d-52117f4f96c6\",\n \"dateOfRenewal\": \"1838-09-08\",\n \"dateOfUpdate\": \"1020-01-30\",\n \"status\": \"active\"\n }\n },\n {\n \"compact\": \"aslp\",\n \"dateOfUpdate\": \"2409-10-07\",\n \"jurisdiction\": \"nj\",\n \"previous\": {\n \"administratorSetStatus\": \"inactive\",\n \"attestations\": [\n {\n \"attestationId\": \"\",\n \"version\": \"\"\n },\n {\n \"attestationId\": \"\",\n \"version\": \"\"\n }\n ],\n \"compactTransactionId\": \"\",\n \"dateOfExpiration\": \"2909-11-03\",\n \"dateOfIssuance\": \"2808-12-18\",\n \"dateOfRenewal\": \"1631-12-19\",\n \"dateOfUpdate\": \"1914-05-30\",\n \"licenseJurisdiction\": \"az\",\n \"privilegeId\": \"\",\n \"compact\": \"octp\",\n \"jurisdiction\": \"mn\",\n \"type\": \"privilege\",\n \"providerId\": \"9c45fa08-650b-40db-adbf-46bea62dfc04\",\n \"status\": \"active\"\n },\n \"type\": \"privilegeUpdate\",\n \"updateType\": \"renewal\",\n \"removedValues\": [\n \"\",\n \"\"\n ],\n \"licenseType\": \"speech-language pathologist\",\n \"updatedValues\": {\n \"licenseJurisdiction\": \"ga\",\n \"compact\": \"aslp\",\n \"jurisdiction\": \"ut\",\n \"attestations\": [\n {\n \"attestationId\": \"\",\n \"version\": \"\"\n },\n {\n \"attestationId\": \"\",\n \"version\": \"\"\n }\n ],\n \"type\": \"privilege\",\n \"compactTransactionId\": \"\",\n \"dateOfIssuance\": \"1703-01-01\",\n \"administratorSetStatus\": \"inactive\",\n \"dateOfExpiration\": \"1193-12-29\",\n \"privilegeId\": \"\",\n \"providerId\": \"d357d4b0-f62e-4c51-a501-44f0888ef8fa\",\n \"dateOfRenewal\": \"1498-08-31\",\n \"dateOfUpdate\": \"1632-05-12\",\n \"status\": \"inactive\"\n }\n }\n ],\n \"jurisdiction\": \"ut\",\n \"licenseJurisdiction\": \"ak\",\n \"licenseType\": \"occupational therapist\",\n \"privilegeId\": \"\",\n \"providerId\": \"e312adc2-e993-4cb3-b582-680408aa25f5\",\n \"status\": \"active\",\n \"type\": \"privilege\",\n \"adverseActions\": [\n {\n \"actionAgainst\": \"\",\n \"adverseActionId\": \"\",\n \"clinicalPrivilegeActionCategory\": \"\",\n \"compact\": \"aslp\",\n \"creationDate\": \"1514-11-04\",\n \"dateOfUpdate\": \"1494-07-18\",\n \"effectiveStartDate\": \"1952-02-03\",\n \"encumbranceType\": \"\",\n \"jurisdiction\": \"al\",\n \"licenseType\": \"\",\n \"licenseTypeAbbreviation\": \"\",\n \"providerId\": \"4a189e27-52ad-444f-9bf1-3d24939ed8d0\",\n \"type\": \"adverseAction\",\n \"effectiveLiftDate\": \"2687-07-31\",\n \"liftingUser\": \"\"\n },\n {\n \"actionAgainst\": \"\",\n \"adverseActionId\": \"\",\n \"clinicalPrivilegeActionCategory\": \"\",\n \"compact\": \"aslp\",\n \"creationDate\": \"1663-10-14\",\n \"dateOfUpdate\": \"1561-03-28\",\n \"effectiveStartDate\": \"2346-05-07\",\n \"encumbranceType\": \"\",\n \"jurisdiction\": \"ar\",\n \"licenseType\": \"\",\n \"licenseTypeAbbreviation\": \"\",\n \"providerId\": \"dd94642b-bac9-4b74-b2f2-8d280f68dc70\",\n \"type\": \"adverseAction\",\n \"effectiveLiftDate\": \"2249-12-06\",\n \"liftingUser\": \"\"\n }\n ]\n }\n ],\n \"providerId\": \"5321022c-1ece-4d00-bc1a-627262417cfe\",\n \"type\": \"provider\",\n \"npi\": \"2844543517\",\n \"compactEligibility\": \"eligible\",\n \"jurisdictionUploadedCompactEligibility\": \"ineligible\",\n \"dateOfBirth\": \"1821-12-27\",\n \"jurisdictionUploadedLicenseStatus\": \"active\",\n \"suffix\": \"\",\n \"currentHomeJurisdiction\": \"hi\",\n \"ssnLastFour\": \"6689\",\n \"licenseStatus\": \"inactive\",\n \"middleName\": \"\",\n \"compactConnectRegisteredEmailAddress\": \"\"\n}", "code": 200, "cookie": [], "header": [ @@ -3757,7 +3888,7 @@ "value": "application/json" } ], - "id": "1d2b0a60-6398-49ef-ae98-275f1c9aec9b", + "id": "cd5edcf2-cd5c-4c12-8735-d2cd6455a740", "name": "200 response", "originalRequest": { "body": {}, @@ -3798,7 +3929,7 @@ "item": [ { "event": [], - "id": "3ec14269-6350-4747-95d2-2b95f72f6d85", + "id": "576158b1-64d4-4610-9d4f-e628952504a7", "name": "/v1/provider-users/me/email", "protocolProfileBehavior": { "disableBodyPruning": true @@ -3853,7 +3984,7 @@ "value": "application/json" } ], - "id": "410f6b3c-19e7-4173-8466-04d63dfe8590", + "id": "9ebc7783-e226-402f-ba30-091ff83782c8", "name": "200 response", "originalRequest": { "body": { @@ -3908,7 +4039,7 @@ "item": [ { "event": [], - "id": "38c7a401-e2e2-4add-8c96-e177d0218a7d", + "id": "d976d16f-10b4-4a80-a68f-c572d74c9955", "name": "/v1/provider-users/me/email/verify", "protocolProfileBehavior": { "disableBodyPruning": true @@ -3922,7 +4053,7 @@ "language": "json" } }, - "raw": "{\n \"verificationCode\": \"6911\"\n}" + "raw": "{\n \"verificationCode\": \"0764\"\n}" }, "description": {}, "header": [ @@ -3964,7 +4095,7 @@ "value": "application/json" } ], - "id": "4c98d624-f8eb-47bc-b591-aedd861d1a5f", + "id": "373cfb10-a71f-4586-bdbf-9d42744c7011", "name": "200 response", "originalRequest": { "body": { @@ -3975,7 +4106,7 @@ "language": "json" } }, - "raw": "{\n \"verificationCode\": \"6911\"\n}" + "raw": "{\n \"verificationCode\": \"0764\"\n}" }, "header": [ { @@ -4026,7 +4157,7 @@ "item": [ { "event": [], - "id": "76e60050-b782-4a71-8eec-00a63f69325d", + "id": "d62f980f-bfa3-4cf0-ba4a-8f7066784990", "name": "/v1/provider-users/me/home-jurisdiction", "protocolProfileBehavior": { "disableBodyPruning": true @@ -4040,7 +4171,7 @@ "language": "json" } }, - "raw": "{\n \"jurisdiction\": \"in\"\n}" + "raw": "{\n \"jurisdiction\": \"wi\"\n}" }, "description": {}, "header": [ @@ -4081,7 +4212,7 @@ "value": "application/json" } ], - "id": "a324e7a2-7214-493e-82e2-d43a96f10886", + "id": "723d65a2-326d-4b61-90b7-5f98db540cac", "name": "200 response", "originalRequest": { "body": { @@ -4092,7 +4223,7 @@ "language": "json" } }, - "raw": "{\n \"jurisdiction\": \"in\"\n}" + "raw": "{\n \"jurisdiction\": \"wi\"\n}" }, "header": [ { @@ -4151,7 +4282,7 @@ "item": [ { "event": [], - "id": "0f6a5caa-be1d-4460-934e-481f8f39f7ed", + "id": "ed19ef16-56eb-459a-b878-0fb36d3b7ab2", "name": "/v1/provider-users/me/jurisdiction/:jurisdiction/licenseType/:licenseType/history", "protocolProfileBehavior": { "disableBodyPruning": true @@ -4209,7 +4340,7 @@ "response": [ { "_postman_previewlanguage": "json", - "body": "{\n \"compact\": \"coun\",\n \"events\": [\n {\n \"createDate\": \"2197-08-13\",\n \"dateOfUpdate\": \"1289-11-31\",\n \"effectiveDate\": \"1858-12-16\",\n \"type\": \"privilegeUpdate\",\n \"updateType\": \"encumbrance\",\n \"note\": \"\"\n },\n {\n \"createDate\": \"1334-11-17\",\n \"dateOfUpdate\": \"2968-08-31\",\n \"effectiveDate\": \"1830-11-07\",\n \"type\": \"privilegeUpdate\",\n \"updateType\": \"expiration\",\n \"note\": \"\"\n }\n ],\n \"jurisdiction\": \"wi\",\n \"licenseType\": \"occupational therapist\",\n \"privilegeId\": \"\",\n \"providerId\": \"f59c6912-a86d-46ca-afdd-4e7c599f7b4e\"\n}", + "body": "{\n \"compact\": \"aslp\",\n \"events\": [\n {\n \"createDate\": \"2605-10-31\",\n \"dateOfUpdate\": \"2897-08-19\",\n \"effectiveDate\": \"1167-11-25\",\n \"type\": \"privilegeUpdate\",\n \"updateType\": \"licenseDeactivation\",\n \"note\": \"\"\n },\n {\n \"createDate\": \"1452-06-28\",\n \"dateOfUpdate\": \"1887-12-24\",\n \"effectiveDate\": \"2287-10-31\",\n \"type\": \"privilegeUpdate\",\n \"updateType\": \"deactivation\",\n \"note\": \"\"\n }\n ],\n \"jurisdiction\": \"hi\",\n \"licenseType\": \"audiologist\",\n \"privilegeId\": \"\",\n \"providerId\": \"16d14b2a-9521-4509-9a76-8913f9636b79\"\n}", "code": 200, "cookie": [], "header": [ @@ -4218,7 +4349,7 @@ "value": "application/json" } ], - "id": "eb4496a4-88d8-43a7-b33e-58c31e9e2c3f", + "id": "143c6ab3-0ce9-4f91-b296-637902803b71", "name": "200 response", "originalRequest": { "body": {}, @@ -4279,7 +4410,7 @@ "item": [ { "event": [], - "id": "957663ce-9be2-4aa6-87f6-7e30e638f52d", + "id": "491453e1-3a7e-4557-a3c7-eabd787b03e1", "name": "/v1/provider-users/me/military-affiliation", "protocolProfileBehavior": { "disableBodyPruning": true @@ -4325,7 +4456,7 @@ "response": [ { "_postman_previewlanguage": "json", - "body": "{\n \"affiliationType\": \"militaryMember\",\n \"dateOfUpdate\": \"1380-02-30\",\n \"dateOfUpload\": \"2339-11-08\",\n \"documentUploadFields\": [\n {\n \"fields\": {\n \"key_0\": \"\"\n },\n \"url\": \"\"\n },\n {\n \"fields\": {\n \"key_0\": \"\",\n \"key_1\": \"\",\n \"key_2\": \"\"\n },\n \"url\": \"\"\n }\n ],\n \"status\": \"\",\n \"fileNames\": [\n \"\",\n \"\"\n ]\n}", + "body": "{\n \"affiliationType\": \"militaryMember\",\n \"dateOfUpdate\": \"1665-04-30\",\n \"dateOfUpload\": \"1650-11-09\",\n \"documentUploadFields\": [\n {\n \"fields\": {\n \"Duisad\": \"\",\n \"do2\": \"\",\n \"commodod\": \"\"\n },\n \"url\": \"\"\n },\n {\n \"fields\": {\n \"officia_3eb\": \"\"\n },\n \"url\": \"\"\n }\n ],\n \"status\": \"\",\n \"fileNames\": [\n \"\",\n \"\"\n ]\n}", "code": 200, "cookie": [], "header": [ @@ -4334,7 +4465,7 @@ "value": "application/json" } ], - "id": "0bee1355-26b3-4f5e-bd92-87b6df90d5e7", + "id": "c25aa10a-0ba3-4375-b434-829828b41828", "name": "200 response", "originalRequest": { "body": { @@ -4386,7 +4517,7 @@ }, { "event": [], - "id": "3997ad91-3fd6-4661-8e45-5bc96e4b5965", + "id": "87ed43fb-3a87-42c0-a701-2e8ee1281da7", "name": "/v1/provider-users/me/military-affiliation", "protocolProfileBehavior": { "disableBodyPruning": true @@ -4441,7 +4572,7 @@ "value": "application/json" } ], - "id": "10003146-2d81-4d6c-9c56-c80a213b29d7", + "id": "6f0ee887-e7f1-49a8-a8d4-48f0c32d9372", "name": "200 response", "originalRequest": { "body": { @@ -4502,7 +4633,7 @@ "item": [ { "event": [], - "id": "73979a29-0df8-4f06-b9ba-e9a8029fb72f", + "id": "025fda09-84d1-40e0-a418-4f5085a67cc9", "name": "/v1/provider-users/registration", "protocolProfileBehavior": { "disableBodyPruning": true @@ -4519,7 +4650,7 @@ "language": "json" } }, - "raw": "{\n \"compact\": \"\",\n \"dob\": \"1988-06-09\",\n \"email\": \"\",\n \"familyName\": \"\",\n \"givenName\": \"\",\n \"jurisdiction\": \"de\",\n \"licenseType\": \"speech-language pathologist\",\n \"partialSocial\": \"\",\n \"token\": \"\"\n}" + "raw": "{\n \"compact\": \"\",\n \"dob\": \"2311-04-01\",\n \"email\": \"\",\n \"familyName\": \"\",\n \"givenName\": \"\",\n \"jurisdiction\": \"ri\",\n \"licenseType\": \"occupational therapist\",\n \"partialSocial\": \"\",\n \"token\": \"\"\n}" }, "description": {}, "header": [ @@ -4559,7 +4690,7 @@ "value": "application/json" } ], - "id": "5a39b5a4-ac90-4f3e-8a51-955bc6a7b4c4", + "id": "0037218d-626f-4456-ab42-dde4ce52c947", "name": "200 response", "originalRequest": { "body": { @@ -4570,7 +4701,7 @@ "language": "json" } }, - "raw": "{\n \"compact\": \"\",\n \"dob\": \"1988-06-09\",\n \"email\": \"\",\n \"familyName\": \"\",\n \"givenName\": \"\",\n \"jurisdiction\": \"de\",\n \"licenseType\": \"speech-language pathologist\",\n \"partialSocial\": \"\",\n \"token\": \"\"\n}" + "raw": "{\n \"compact\": \"\",\n \"dob\": \"2311-04-01\",\n \"email\": \"\",\n \"familyName\": \"\",\n \"givenName\": \"\",\n \"jurisdiction\": \"ri\",\n \"licenseType\": \"occupational therapist\",\n \"partialSocial\": \"\",\n \"token\": \"\"\n}" }, "header": [ { @@ -4608,7 +4739,7 @@ "item": [ { "event": [], - "id": "8f1f2a30-9e7c-4b46-894d-c656adbd586b", + "id": "f2588bfa-37c0-42e1-900b-b8d0dbd814fd", "name": "/v1/provider-users/verifyRecovery", "protocolProfileBehavior": { "disableBodyPruning": true @@ -4625,7 +4756,7 @@ "language": "json" } }, - "raw": "{\n \"compact\": \"octp\",\n \"providerId\": \"561f11c1-4306-4ba1-a84f-e4c0822c675a\",\n \"recaptchaToken\": \"\",\n \"recoveryToken\": \"\"\n}" + "raw": "{\n \"compact\": \"octp\",\n \"providerId\": \"c1e668d2-0445-4a4f-a1ad-f0a8c00be12b\",\n \"recaptchaToken\": \"\",\n \"recoveryToken\": \"\"\n}" }, "description": {}, "header": [ @@ -4665,7 +4796,7 @@ "value": "application/json" } ], - "id": "62845732-15e5-4eeb-82f9-1ef049a5c450", + "id": "c61c63d3-ffd7-4ca6-a24f-64def632f92f", "name": "200 response", "originalRequest": { "body": { @@ -4676,7 +4807,7 @@ "language": "json" } }, - "raw": "{\n \"compact\": \"octp\",\n \"providerId\": \"561f11c1-4306-4ba1-a84f-e4c0822c675a\",\n \"recaptchaToken\": \"\",\n \"recoveryToken\": \"\"\n}" + "raw": "{\n \"compact\": \"octp\",\n \"providerId\": \"c1e668d2-0445-4a4f-a1ad-f0a8c00be12b\",\n \"recaptchaToken\": \"\",\n \"recoveryToken\": \"\"\n}" }, "header": [ { @@ -4726,7 +4857,7 @@ "item": [ { "event": [], - "id": "ecdec2c6-deaf-409d-a1e6-08dce7df3f53", + "id": "02a6f822-e88f-42c6-a87c-e85b33beb261", "name": "/v1/public/compacts/:compact/jurisdictions", "protocolProfileBehavior": { "disableBodyPruning": true @@ -4783,7 +4914,7 @@ "value": "application/json" } ], - "id": "87b7a62a-5013-462a-9bb7-63d55b42177a", + "id": "74c5f084-0222-4479-ac28-c5468dbb5aa8", "name": "200 response", "originalRequest": { "body": {}, @@ -4824,7 +4955,7 @@ "item": [ { "event": [], - "id": "addab8e0-ce99-4268-a622-c8aaf5e08b1d", + "id": "2b1b0598-6a71-45ea-a02d-7ada200f3350", "name": "/v1/public/compacts/:compact/providers/query", "protocolProfileBehavior": { "disableBodyPruning": true @@ -4841,7 +4972,7 @@ "language": "json" } }, - "raw": "{\n \"query\": {\n \"providerId\": \"3c82fb57-70cb-4318-8a61-d9238c00282e\",\n \"jurisdiction\": \"de\",\n \"givenName\": \"\",\n \"familyName\": \"\"\n },\n \"pagination\": {\n \"lastKey\": \"\",\n \"pageSize\": \"\"\n },\n \"sorting\": {\n \"key\": \"dateOfUpdate\",\n \"direction\": \"ascending\"\n }\n}" + "raw": "{\n \"query\": {\n \"providerId\": \"49eccb09-5890-4e21-ade5-3eeecabc7941\",\n \"jurisdiction\": \"fl\",\n \"givenName\": \"\",\n \"familyName\": \"\"\n },\n \"pagination\": {\n \"lastKey\": \"\",\n \"pageSize\": \"\"\n },\n \"sorting\": {\n \"key\": \"familyName\",\n \"direction\": \"descending\"\n }\n}" }, "description": {}, "header": [ @@ -4886,7 +5017,7 @@ "response": [ { "_postman_previewlanguage": "json", - "body": "{\n \"pagination\": {\n \"prevLastKey\": {},\n \"lastKey\": {},\n \"pageSize\": \"\"\n },\n \"providers\": [\n {\n \"compact\": \"coun\",\n \"familyName\": \"\",\n \"givenName\": \"\",\n \"licenseJurisdiction\": \"pr\",\n \"privilegeJurisdictions\": [\n \"nd\",\n \"wy\"\n ],\n \"providerId\": \"3e79bfdf-2841-492e-92f9-a3d269a1aa5f\",\n \"type\": \"provider\",\n \"npi\": \"4752068335\",\n \"middleName\": \"\",\n \"suffix\": \"\",\n \"currentHomeJurisdiction\": \"ky\",\n \"dateOfUpdate\": \"1855-12-30\"\n },\n {\n \"compact\": \"aslp\",\n \"familyName\": \"\",\n \"givenName\": \"\",\n \"licenseJurisdiction\": \"tx\",\n \"privilegeJurisdictions\": [\n \"az\",\n \"nj\"\n ],\n \"providerId\": \"f9244bdb-266c-4156-8872-b31b35e15960\",\n \"type\": \"provider\",\n \"npi\": \"0347756312\",\n \"middleName\": \"\",\n \"suffix\": \"\",\n \"currentHomeJurisdiction\": \"nc\",\n \"dateOfUpdate\": \"1197-10-08\"\n }\n ],\n \"query\": {\n \"providerId\": \"83944271-322f-469f-b20c-2f4bb3b44da0\",\n \"jurisdiction\": \"fl\",\n \"givenName\": \"\",\n \"familyName\": \"\"\n },\n \"sorting\": {\n \"key\": \"dateOfUpdate\",\n \"direction\": \"descending\"\n }\n}", + "body": "{\n \"pagination\": {\n \"prevLastKey\": {},\n \"lastKey\": {},\n \"pageSize\": \"\"\n },\n \"providers\": [\n {\n \"compact\": \"aslp\",\n \"familyName\": \"\",\n \"givenName\": \"\",\n \"licenseJurisdiction\": \"de\",\n \"privilegeJurisdictions\": [\n \"nj\",\n \"vt\"\n ],\n \"providerId\": \"59d57d0d-698f-46af-9b81-a22d01ee163d\",\n \"type\": \"provider\",\n \"npi\": \"3822342077\",\n \"middleName\": \"\",\n \"suffix\": \"\",\n \"currentHomeJurisdiction\": \"la\",\n \"dateOfUpdate\": \"1854-05-01\"\n },\n {\n \"compact\": \"aslp\",\n \"familyName\": \"\",\n \"givenName\": \"\",\n \"licenseJurisdiction\": \"nv\",\n \"privilegeJurisdictions\": [\n \"ca\",\n \"sc\"\n ],\n \"providerId\": \"370a21ce-04a6-4d71-9bda-4c760c330ea3\",\n \"type\": \"provider\",\n \"npi\": \"8754434820\",\n \"middleName\": \"\",\n \"suffix\": \"\",\n \"currentHomeJurisdiction\": \"wy\",\n \"dateOfUpdate\": \"2124-11-31\"\n }\n ],\n \"query\": {\n \"providerId\": \"af9fe78c-af99-41ed-892f-bc6a6dd702d1\",\n \"jurisdiction\": \"ne\",\n \"givenName\": \"\",\n \"familyName\": \"\"\n },\n \"sorting\": {\n \"key\": \"dateOfUpdate\",\n \"direction\": \"ascending\"\n }\n}", "code": 200, "cookie": [], "header": [ @@ -4895,7 +5026,7 @@ "value": "application/json" } ], - "id": "0f9d412a-448a-4fd3-a64a-ef3713d28a3b", + "id": "b53afa02-a20b-482c-b2e6-08a29be9ebae", "name": "200 response", "originalRequest": { "body": { @@ -4906,7 +5037,7 @@ "language": "json" } }, - "raw": "{\n \"query\": {\n \"providerId\": \"3c82fb57-70cb-4318-8a61-d9238c00282e\",\n \"jurisdiction\": \"de\",\n \"givenName\": \"\",\n \"familyName\": \"\"\n },\n \"pagination\": {\n \"lastKey\": \"\",\n \"pageSize\": \"\"\n },\n \"sorting\": {\n \"key\": \"dateOfUpdate\",\n \"direction\": \"ascending\"\n }\n}" + "raw": "{\n \"query\": {\n \"providerId\": \"49eccb09-5890-4e21-ade5-3eeecabc7941\",\n \"jurisdiction\": \"fl\",\n \"givenName\": \"\",\n \"familyName\": \"\"\n },\n \"pagination\": {\n \"lastKey\": \"\",\n \"pageSize\": \"\"\n },\n \"sorting\": {\n \"key\": \"familyName\",\n \"direction\": \"descending\"\n }\n}" }, "header": [ { @@ -4947,7 +5078,7 @@ "item": [ { "event": [], - "id": "01581550-c11c-40d6-a0e1-12635287cf06", + "id": "aa1970dc-536d-4512-9d40-ea897e355c2b", "name": "/v1/public/compacts/:compact/providers/:providerId", "protocolProfileBehavior": { "disableBodyPruning": true @@ -5006,7 +5137,7 @@ "response": [ { "_postman_previewlanguage": "json", - "body": "{\n \"compact\": \"coun\",\n \"dateOfUpdate\": \"1914-07-31\",\n \"familyName\": \"\",\n \"givenName\": \"\",\n \"licenseJurisdiction\": \"wv\",\n \"privilegeJurisdictions\": [\n \"tn\",\n \"ar\"\n ],\n \"providerId\": \"60983418-cc5c-4785-8ff2-e198d358ce6c\",\n \"type\": \"provider\",\n \"privileges\": [\n {\n \"administratorSetStatus\": \"inactive\",\n \"compact\": \"coun\",\n \"dateOfExpiration\": \"1920-06-06\",\n \"dateOfIssuance\": \"2110-10-10\",\n \"dateOfRenewal\": \"1663-03-30\",\n \"dateOfUpdate\": \"2189-11-31\",\n \"jurisdiction\": \"nv\",\n \"licenseJurisdiction\": \"wi\",\n \"licenseType\": \"licensed professional counselor\",\n \"privilegeId\": \"\",\n \"providerId\": \"516eedf8-3ac4-4873-b113-268fab57dc24\",\n \"status\": \"active\",\n \"type\": \"privilege\",\n \"history\": [\n {\n \"compact\": \"aslp\",\n \"dateOfUpdate\": \"2937-12-04\",\n \"jurisdiction\": \"ca\",\n \"licenseType\": \"occupational therapy assistant\",\n \"previous\": {\n \"administratorSetStatus\": \"active\",\n \"dateOfExpiration\": \"1642-12-07\",\n \"dateOfIssuance\": \"2902-04-09\",\n \"dateOfRenewal\": \"1438-12-09\",\n \"dateOfUpdate\": \"2328-09-25\",\n \"licenseJurisdiction\": \"mt\",\n \"privilegeId\": \"\"\n },\n \"providerId\": \"e68f72b1-d279-4b21-a030-b17312f564c8\",\n \"type\": \"privilegeUpdate\",\n \"updateType\": \"emailChange\",\n \"updatedValues\": {\n \"administratorSetStatus\": \"inactive\",\n \"dateOfExpiration\": \"2263-09-04\",\n \"licenseJurisdiction\": \"ri\",\n \"privilegeId\": \"\",\n \"dateOfRenewal\": \"1185-11-03\",\n \"dateOfIssuance\": \"2096-11-07\",\n \"dateOfUpdate\": \"2607-10-07\"\n }\n },\n {\n \"compact\": \"octp\",\n \"dateOfUpdate\": \"2583-11-10\",\n \"jurisdiction\": \"nd\",\n \"licenseType\": \"occupational therapist\",\n \"previous\": {\n \"administratorSetStatus\": \"active\",\n \"dateOfExpiration\": \"2006-10-30\",\n \"dateOfIssuance\": \"2408-02-24\",\n \"dateOfRenewal\": \"2297-12-31\",\n \"dateOfUpdate\": \"2736-12-05\",\n \"licenseJurisdiction\": \"ny\",\n \"privilegeId\": \"\"\n },\n \"providerId\": \"c89ebb3a-1326-4a96-aead-be7b5d37bc36\",\n \"type\": \"privilegeUpdate\",\n \"updateType\": \"registration\",\n \"updatedValues\": {\n \"administratorSetStatus\": \"active\",\n \"dateOfExpiration\": \"2786-01-15\",\n \"licenseJurisdiction\": \"fl\",\n \"privilegeId\": \"\",\n \"dateOfRenewal\": \"2070-12-30\",\n \"dateOfIssuance\": \"2711-12-30\",\n \"dateOfUpdate\": \"1400-09-30\"\n }\n }\n ],\n \"adverseActions\": [\n {\n \"actionAgainst\": \"\",\n \"adverseActionId\": \"\",\n \"compact\": \"octp\",\n \"creationDate\": \"1512-11-11\",\n \"dateOfUpdate\": \"1732-05-04\",\n \"effectiveStartDate\": \"1704-04-01\",\n \"jurisdiction\": \"ks\",\n \"licenseType\": \"\",\n \"licenseTypeAbbreviation\": \"\",\n \"providerId\": \"6605dfeb-848d-4f3b-8000-6ca8de172f90\",\n \"type\": \"adverseAction\",\n \"effectiveLiftDate\": \"2722-11-05\"\n },\n {\n \"actionAgainst\": \"\",\n \"adverseActionId\": \"\",\n \"compact\": \"octp\",\n \"creationDate\": \"1167-10-31\",\n \"dateOfUpdate\": \"1766-04-22\",\n \"effectiveStartDate\": \"2429-02-26\",\n \"jurisdiction\": \"nd\",\n \"licenseType\": \"\",\n \"licenseTypeAbbreviation\": \"\",\n \"providerId\": \"0b03bb5a-4b58-4ecb-bfe3-50b1880cac77\",\n \"type\": \"adverseAction\",\n \"effectiveLiftDate\": \"1668-04-31\"\n }\n ]\n },\n {\n \"administratorSetStatus\": \"inactive\",\n \"compact\": \"aslp\",\n \"dateOfExpiration\": \"1925-12-21\",\n \"dateOfIssuance\": \"1194-12-31\",\n \"dateOfRenewal\": \"2392-01-04\",\n \"dateOfUpdate\": \"2386-12-31\",\n \"jurisdiction\": \"hi\",\n \"licenseJurisdiction\": \"nj\",\n \"licenseType\": \"audiologist\",\n \"privilegeId\": \"\",\n \"providerId\": \"969140da-3fe2-4b3b-a672-c5c882ee4a45\",\n \"status\": \"active\",\n \"type\": \"privilege\",\n \"history\": [\n {\n \"compact\": \"octp\",\n \"dateOfUpdate\": \"2113-04-21\",\n \"jurisdiction\": \"oh\",\n \"licenseType\": \"licensed professional counselor\",\n \"previous\": {\n \"administratorSetStatus\": \"active\",\n \"dateOfExpiration\": \"2689-02-07\",\n \"dateOfIssuance\": \"1230-02-31\",\n \"dateOfRenewal\": \"2657-12-14\",\n \"dateOfUpdate\": \"1052-12-02\",\n \"licenseJurisdiction\": \"wv\",\n \"privilegeId\": \"\"\n },\n \"providerId\": \"efe411da-dc3f-4f5f-a759-5bbee0d9ea04\",\n \"type\": \"privilegeUpdate\",\n \"updateType\": \"expiration\",\n \"updatedValues\": {\n \"administratorSetStatus\": \"active\",\n \"dateOfExpiration\": \"2699-10-05\",\n \"licenseJurisdiction\": \"md\",\n \"privilegeId\": \"\",\n \"dateOfRenewal\": \"2207-10-28\",\n \"dateOfIssuance\": \"2784-04-31\",\n \"dateOfUpdate\": \"2510-11-02\"\n }\n },\n {\n \"compact\": \"aslp\",\n \"dateOfUpdate\": \"2593-11-26\",\n \"jurisdiction\": \"mn\",\n \"licenseType\": \"audiologist\",\n \"previous\": {\n \"administratorSetStatus\": \"inactive\",\n \"dateOfExpiration\": \"2695-03-25\",\n \"dateOfIssuance\": \"2740-01-04\",\n \"dateOfRenewal\": \"1382-12-01\",\n \"dateOfUpdate\": \"1712-08-04\",\n \"licenseJurisdiction\": \"hi\",\n \"privilegeId\": \"\"\n },\n \"providerId\": \"2c106d00-93a2-4000-b26b-02ad3552bfdf\",\n \"type\": \"privilegeUpdate\",\n \"updateType\": \"expiration\",\n \"updatedValues\": {\n \"administratorSetStatus\": \"inactive\",\n \"dateOfExpiration\": \"1216-12-18\",\n \"licenseJurisdiction\": \"wy\",\n \"privilegeId\": \"\",\n \"dateOfRenewal\": \"2624-10-10\",\n \"dateOfIssuance\": \"1075-11-13\",\n \"dateOfUpdate\": \"2372-07-22\"\n }\n }\n ],\n \"adverseActions\": [\n {\n \"actionAgainst\": \"\",\n \"adverseActionId\": \"\",\n \"compact\": \"octp\",\n \"creationDate\": \"2728-05-10\",\n \"dateOfUpdate\": \"1473-03-17\",\n \"effectiveStartDate\": \"2279-10-04\",\n \"jurisdiction\": \"tn\",\n \"licenseType\": \"\",\n \"licenseTypeAbbreviation\": \"\",\n \"providerId\": \"b563c4f9-b135-406c-ac30-aec88f9f3d96\",\n \"type\": \"adverseAction\",\n \"effectiveLiftDate\": \"2111-08-22\"\n },\n {\n \"actionAgainst\": \"\",\n \"adverseActionId\": \"\",\n \"compact\": \"aslp\",\n \"creationDate\": \"1192-04-07\",\n \"dateOfUpdate\": \"2637-08-02\",\n \"effectiveStartDate\": \"2570-10-30\",\n \"jurisdiction\": \"ok\",\n \"licenseType\": \"\",\n \"licenseTypeAbbreviation\": \"\",\n \"providerId\": \"7c60c02d-ac47-452e-bfa7-44429fd557ff\",\n \"type\": \"adverseAction\",\n \"effectiveLiftDate\": \"2845-09-14\"\n }\n ]\n }\n ],\n \"npi\": \"8439165402\",\n \"suffix\": \"\",\n \"currentHomeJurisdiction\": \"wa\",\n \"middleName\": \"\"\n}", + "body": "{\n \"compact\": \"coun\",\n \"dateOfUpdate\": \"2766-11-31\",\n \"familyName\": \"\",\n \"givenName\": \"\",\n \"licenseJurisdiction\": \"ak\",\n \"privilegeJurisdictions\": [\n \"ny\",\n \"de\"\n ],\n \"providerId\": \"f7a01bc8-0598-4556-a5c3-da2b4cd6a778\",\n \"type\": \"provider\",\n \"privileges\": [\n {\n \"administratorSetStatus\": \"active\",\n \"compact\": \"coun\",\n \"dateOfExpiration\": \"1274-10-22\",\n \"dateOfIssuance\": \"1404-10-05\",\n \"dateOfRenewal\": \"2245-05-02\",\n \"dateOfUpdate\": \"2476-06-31\",\n \"jurisdiction\": \"de\",\n \"licenseJurisdiction\": \"al\",\n \"licenseType\": \"occupational therapy assistant\",\n \"privilegeId\": \"\",\n \"providerId\": \"24dd6fc0-d9b4-4981-bce6-4fa60d0023ec\",\n \"status\": \"active\",\n \"type\": \"privilege\",\n \"history\": [\n {\n \"compact\": \"aslp\",\n \"dateOfUpdate\": \"1968-12-13\",\n \"jurisdiction\": \"mi\",\n \"licenseType\": \"occupational therapist\",\n \"previous\": {\n \"administratorSetStatus\": \"inactive\",\n \"dateOfExpiration\": \"2437-12-14\",\n \"dateOfIssuance\": \"1640-09-29\",\n \"dateOfRenewal\": \"1567-06-25\",\n \"dateOfUpdate\": \"1378-12-21\",\n \"licenseJurisdiction\": \"nv\",\n \"privilegeId\": \"\"\n },\n \"providerId\": \"c95d7de3-38e8-4315-8f35-7f67e8b169e6\",\n \"type\": \"privilegeUpdate\",\n \"updateType\": \"encumbrance\",\n \"updatedValues\": {\n \"administratorSetStatus\": \"active\",\n \"dateOfExpiration\": \"1185-08-07\",\n \"licenseJurisdiction\": \"co\",\n \"privilegeId\": \"\",\n \"dateOfRenewal\": \"2609-12-12\",\n \"dateOfIssuance\": \"1240-06-26\",\n \"dateOfUpdate\": \"1407-04-24\"\n }\n },\n {\n \"compact\": \"coun\",\n \"dateOfUpdate\": \"2115-10-30\",\n \"jurisdiction\": \"ky\",\n \"licenseType\": \"audiologist\",\n \"previous\": {\n \"administratorSetStatus\": \"active\",\n \"dateOfExpiration\": \"1823-03-30\",\n \"dateOfIssuance\": \"1296-12-24\",\n \"dateOfRenewal\": \"1542-06-31\",\n \"dateOfUpdate\": \"2367-07-31\",\n \"licenseJurisdiction\": \"co\",\n \"privilegeId\": \"\"\n },\n \"providerId\": \"0e74075e-687b-4b91-8105-0ed9e284cf1c\",\n \"type\": \"privilegeUpdate\",\n \"updateType\": \"issuance\",\n \"updatedValues\": {\n \"administratorSetStatus\": \"inactive\",\n \"dateOfExpiration\": \"2910-09-30\",\n \"licenseJurisdiction\": \"tx\",\n \"privilegeId\": \"\",\n \"dateOfRenewal\": \"2463-06-31\",\n \"dateOfIssuance\": \"2116-03-31\",\n \"dateOfUpdate\": \"2668-01-06\"\n }\n }\n ],\n \"adverseActions\": [\n {\n \"actionAgainst\": \"\",\n \"adverseActionId\": \"\",\n \"compact\": \"coun\",\n \"creationDate\": \"2002-09-31\",\n \"dateOfUpdate\": \"2033-07-31\",\n \"effectiveStartDate\": \"1680-11-16\",\n \"jurisdiction\": \"ny\",\n \"licenseType\": \"\",\n \"licenseTypeAbbreviation\": \"\",\n \"providerId\": \"281e1677-7848-49dd-b55b-1a4d7e8e9826\",\n \"type\": \"adverseAction\",\n \"effectiveLiftDate\": \"1069-12-31\"\n },\n {\n \"actionAgainst\": \"\",\n \"adverseActionId\": \"\",\n \"compact\": \"coun\",\n \"creationDate\": \"2078-12-31\",\n \"dateOfUpdate\": \"1157-11-30\",\n \"effectiveStartDate\": \"1937-11-03\",\n \"jurisdiction\": \"ut\",\n \"licenseType\": \"\",\n \"licenseTypeAbbreviation\": \"\",\n \"providerId\": \"9ac5374b-6699-4afc-b36d-830d1df2a753\",\n \"type\": \"adverseAction\",\n \"effectiveLiftDate\": \"1644-06-19\"\n }\n ]\n },\n {\n \"administratorSetStatus\": \"active\",\n \"compact\": \"coun\",\n \"dateOfExpiration\": \"1562-11-18\",\n \"dateOfIssuance\": \"1551-05-20\",\n \"dateOfRenewal\": \"1283-04-04\",\n \"dateOfUpdate\": \"1191-12-31\",\n \"jurisdiction\": \"az\",\n \"licenseJurisdiction\": \"hi\",\n \"licenseType\": \"speech-language pathologist\",\n \"privilegeId\": \"\",\n \"providerId\": \"15ede8a1-db4d-41c4-a69f-31c5fa265f47\",\n \"status\": \"active\",\n \"type\": \"privilege\",\n \"history\": [\n {\n \"compact\": \"octp\",\n \"dateOfUpdate\": \"2055-12-31\",\n \"jurisdiction\": \"de\",\n \"licenseType\": \"speech-language pathologist\",\n \"previous\": {\n \"administratorSetStatus\": \"inactive\",\n \"dateOfExpiration\": \"1595-10-30\",\n \"dateOfIssuance\": \"1807-12-29\",\n \"dateOfRenewal\": \"1152-12-30\",\n \"dateOfUpdate\": \"2555-10-30\",\n \"licenseJurisdiction\": \"ar\",\n \"privilegeId\": \"\"\n },\n \"providerId\": \"d19c274b-e29c-4b3a-adf6-bce692083a08\",\n \"type\": \"privilegeUpdate\",\n \"updateType\": \"emailChange\",\n \"updatedValues\": {\n \"administratorSetStatus\": \"inactive\",\n \"dateOfExpiration\": \"1998-10-28\",\n \"licenseJurisdiction\": \"id\",\n \"privilegeId\": \"\",\n \"dateOfRenewal\": \"2593-07-30\",\n \"dateOfIssuance\": \"2511-12-26\",\n \"dateOfUpdate\": \"1315-10-05\"\n }\n },\n {\n \"compact\": \"aslp\",\n \"dateOfUpdate\": \"2505-08-31\",\n \"jurisdiction\": \"md\",\n \"licenseType\": \"occupational therapy assistant\",\n \"previous\": {\n \"administratorSetStatus\": \"inactive\",\n \"dateOfExpiration\": \"2470-03-31\",\n \"dateOfIssuance\": \"2192-06-22\",\n \"dateOfRenewal\": \"1244-04-31\",\n \"dateOfUpdate\": \"1289-12-19\",\n \"licenseJurisdiction\": \"mi\",\n \"privilegeId\": \"\"\n },\n \"providerId\": \"a974455a-4c99-4ff7-8d4f-56df31a1a991\",\n \"type\": \"privilegeUpdate\",\n \"updateType\": \"emailChange\",\n \"updatedValues\": {\n \"administratorSetStatus\": \"active\",\n \"dateOfExpiration\": \"2609-10-31\",\n \"licenseJurisdiction\": \"mi\",\n \"privilegeId\": \"\",\n \"dateOfRenewal\": \"1127-12-08\",\n \"dateOfIssuance\": \"2241-11-05\",\n \"dateOfUpdate\": \"1608-12-08\"\n }\n }\n ],\n \"adverseActions\": [\n {\n \"actionAgainst\": \"\",\n \"adverseActionId\": \"\",\n \"compact\": \"octp\",\n \"creationDate\": \"1592-10-29\",\n \"dateOfUpdate\": \"2449-04-31\",\n \"effectiveStartDate\": \"1699-10-09\",\n \"jurisdiction\": \"ma\",\n \"licenseType\": \"\",\n \"licenseTypeAbbreviation\": \"\",\n \"providerId\": \"359d864f-d697-4c03-b247-e126dc702023\",\n \"type\": \"adverseAction\",\n \"effectiveLiftDate\": \"1501-02-04\"\n },\n {\n \"actionAgainst\": \"\",\n \"adverseActionId\": \"\",\n \"compact\": \"aslp\",\n \"creationDate\": \"1017-11-08\",\n \"dateOfUpdate\": \"2499-06-30\",\n \"effectiveStartDate\": \"2623-11-30\",\n \"jurisdiction\": \"mo\",\n \"licenseType\": \"\",\n \"licenseTypeAbbreviation\": \"\",\n \"providerId\": \"c7a69df1-59c2-4afd-801c-ec3886c839b8\",\n \"type\": \"adverseAction\",\n \"effectiveLiftDate\": \"2059-11-22\"\n }\n ]\n }\n ],\n \"npi\": \"9294206397\",\n \"suffix\": \"\",\n \"currentHomeJurisdiction\": \"nj\",\n \"middleName\": \"\"\n}", "code": 200, "cookie": [], "header": [ @@ -5015,7 +5146,7 @@ "value": "application/json" } ], - "id": "c94fd4a2-7463-46dd-9743-6c3c80d4311c", + "id": "e401281b-5592-448b-be2f-f895357eb174", "name": "200 response", "originalRequest": { "body": {}, @@ -5063,7 +5194,7 @@ "item": [ { "event": [], - "id": "1173c578-0784-457c-be65-35bfa8ddc4bc", + "id": "9f149bac-68f4-464e-82d0-46a1fc7b2a60", "name": "/v1/public/compacts/:compact/providers/:providerId/jurisdiction/:jurisdiction/licenseType/:licenseType/history", "protocolProfileBehavior": { "disableBodyPruning": true @@ -5147,7 +5278,7 @@ "response": [ { "_postman_previewlanguage": "json", - "body": "{\n \"compact\": \"coun\",\n \"events\": [\n {\n \"createDate\": \"2197-08-13\",\n \"dateOfUpdate\": \"1289-11-31\",\n \"effectiveDate\": \"1858-12-16\",\n \"type\": \"privilegeUpdate\",\n \"updateType\": \"encumbrance\",\n \"note\": \"\"\n },\n {\n \"createDate\": \"1334-11-17\",\n \"dateOfUpdate\": \"2968-08-31\",\n \"effectiveDate\": \"1830-11-07\",\n \"type\": \"privilegeUpdate\",\n \"updateType\": \"expiration\",\n \"note\": \"\"\n }\n ],\n \"jurisdiction\": \"wi\",\n \"licenseType\": \"occupational therapist\",\n \"privilegeId\": \"\",\n \"providerId\": \"f59c6912-a86d-46ca-afdd-4e7c599f7b4e\"\n}", + "body": "{\n \"compact\": \"aslp\",\n \"events\": [\n {\n \"createDate\": \"2605-10-31\",\n \"dateOfUpdate\": \"2897-08-19\",\n \"effectiveDate\": \"1167-11-25\",\n \"type\": \"privilegeUpdate\",\n \"updateType\": \"licenseDeactivation\",\n \"note\": \"\"\n },\n {\n \"createDate\": \"1452-06-28\",\n \"dateOfUpdate\": \"1887-12-24\",\n \"effectiveDate\": \"2287-10-31\",\n \"type\": \"privilegeUpdate\",\n \"updateType\": \"deactivation\",\n \"note\": \"\"\n }\n ],\n \"jurisdiction\": \"hi\",\n \"licenseType\": \"audiologist\",\n \"privilegeId\": \"\",\n \"providerId\": \"16d14b2a-9521-4509-9a76-8913f9636b79\"\n}", "code": 200, "cookie": [], "header": [ @@ -5156,7 +5287,7 @@ "value": "application/json" } ], - "id": "d6b931c9-299b-4b70-a89b-42ebfef3c643", + "id": "00a7015c-9688-45e0-9158-ea11906e6734", "name": "200 response", "originalRequest": { "body": {}, @@ -5230,7 +5361,7 @@ "item": [ { "event": [], - "id": "a1abdd71-3896-4353-ba3a-20f3ffc14be9", + "id": "840c1325-cc6f-443f-9663-b18172047901", "name": "/v1/purchases/privileges", "protocolProfileBehavior": { "disableBodyPruning": true @@ -5244,7 +5375,7 @@ "language": "json" } }, - "raw": "{\n \"attestations\": [\n {\n \"attestationId\": \"\",\n \"version\": \"705265\"\n },\n {\n \"attestationId\": \"\",\n \"version\": \"3447673691\"\n }\n ],\n \"licenseType\": \"occupational therapist\",\n \"orderInformation\": {\n \"opaqueData\": {\n \"dataDescriptor\": \"\",\n \"dataValue\": \"\"\n }\n },\n \"selectedJurisdictions\": [\n \"mn\",\n \"il\"\n ]\n}" + "raw": "{\n \"attestations\": [\n {\n \"attestationId\": \"\",\n \"version\": \"20533\"\n },\n {\n \"attestationId\": \"\",\n \"version\": \"2399432\"\n }\n ],\n \"licenseType\": \"licensed professional counselor\",\n \"orderInformation\": {\n \"opaqueData\": {\n \"dataDescriptor\": \"\",\n \"dataValue\": \"\"\n }\n },\n \"selectedJurisdictions\": [\n \"hi\",\n \"nj\"\n ]\n}" }, "description": {}, "header": [ @@ -5284,7 +5415,7 @@ "value": "application/json" } ], - "id": "429fb04b-9831-418d-979c-34a619b6e464", + "id": "6cb028d3-7150-419b-9826-fc361ed8628d", "name": "200 response", "originalRequest": { "body": { @@ -5295,7 +5426,7 @@ "language": "json" } }, - "raw": "{\n \"attestations\": [\n {\n \"attestationId\": \"\",\n \"version\": \"705265\"\n },\n {\n \"attestationId\": \"\",\n \"version\": \"3447673691\"\n }\n ],\n \"licenseType\": \"occupational therapist\",\n \"orderInformation\": {\n \"opaqueData\": {\n \"dataDescriptor\": \"\",\n \"dataValue\": \"\"\n }\n },\n \"selectedJurisdictions\": [\n \"mn\",\n \"il\"\n ]\n}" + "raw": "{\n \"attestations\": [\n {\n \"attestationId\": \"\",\n \"version\": \"20533\"\n },\n {\n \"attestationId\": \"\",\n \"version\": \"2399432\"\n }\n ],\n \"licenseType\": \"licensed professional counselor\",\n \"orderInformation\": {\n \"opaqueData\": {\n \"dataDescriptor\": \"\",\n \"dataValue\": \"\"\n }\n },\n \"selectedJurisdictions\": [\n \"hi\",\n \"nj\"\n ]\n}" }, "header": [ { @@ -5338,7 +5469,7 @@ "item": [ { "event": [], - "id": "6e8fea57-345a-4b3d-a524-3c7b33e23f5e", + "id": "101c0762-263f-468d-a6cc-3bda7ea51aad", "name": "/v1/purchases/privileges/options", "protocolProfileBehavior": { "disableBodyPruning": true @@ -5380,7 +5511,7 @@ "value": "application/json" } ], - "id": "9fb8ff6b-5e80-47e3-8363-2af57e50f2cf", + "id": "8299e679-73d2-4c25-846b-75a251f4bfc3", "name": "200 response", "originalRequest": { "body": {}, @@ -5434,7 +5565,7 @@ "item": [ { "event": [], - "id": "e051190e-2c39-4eda-bdb4-44b23876a1f6", + "id": "d295fa57-b8ba-4b6c-a635-124e70f7b9f3", "name": "/v1/staff-users/me", "protocolProfileBehavior": { "disableBodyPruning": true @@ -5466,7 +5597,7 @@ "response": [ { "_postman_previewlanguage": "json", - "body": "{\n \"attributes\": {\n \"email\": \"\",\n \"familyName\": \"\",\n \"givenName\": \"\"\n },\n \"permissions\": {\n \"key_0\": {\n \"actions\": {\n \"readPrivate\": \"\",\n \"admin\": \"\",\n \"readSSN\": \"\"\n },\n \"jurisdictions\": {\n \"key_0\": {\n \"actions\": {\n \"readPrivate\": \"\",\n \"admin\": \"\",\n \"write\": \"\",\n \"readSSN\": \"\"\n }\n }\n }\n },\n \"key_1\": {\n \"actions\": {\n \"readPrivate\": \"\",\n \"admin\": \"\",\n \"readSSN\": \"\"\n },\n \"jurisdictions\": {\n \"key_0\": {\n \"actions\": {\n \"readPrivate\": \"\",\n \"admin\": \"\",\n \"write\": \"\",\n \"readSSN\": \"\"\n }\n },\n \"key_1\": {\n \"actions\": {\n \"readPrivate\": \"\",\n \"admin\": \"\",\n \"write\": \"\",\n \"readSSN\": \"\"\n }\n },\n \"key_2\": {\n \"actions\": {\n \"readPrivate\": \"\",\n \"admin\": \"\",\n \"write\": \"\",\n \"readSSN\": \"\"\n }\n }\n }\n }\n },\n \"status\": \"inactive\",\n \"userId\": \"\"\n}", + "body": "{\n \"attributes\": {\n \"email\": \"\",\n \"familyName\": \"\",\n \"givenName\": \"\"\n },\n \"permissions\": {\n \"ex_be\": {\n \"actions\": {\n \"readPrivate\": \"\",\n \"admin\": \"\",\n \"readSSN\": \"\"\n },\n \"jurisdictions\": {\n \"veniam_016\": {\n \"actions\": {\n \"readPrivate\": \"\",\n \"admin\": \"\",\n \"write\": \"\",\n \"readSSN\": \"\"\n }\n }\n }\n },\n \"consectetur_4_a\": {\n \"actions\": {\n \"readPrivate\": \"\",\n \"admin\": \"\",\n \"readSSN\": \"\"\n },\n \"jurisdictions\": {\n \"laboris_63\": {\n \"actions\": {\n \"readPrivate\": \"\",\n \"admin\": \"\",\n \"write\": \"\",\n \"readSSN\": \"\"\n }\n },\n \"voluptate_139\": {\n \"actions\": {\n \"readPrivate\": \"\",\n \"admin\": \"\",\n \"write\": \"\",\n \"readSSN\": \"\"\n }\n },\n \"dolor_a\": {\n \"actions\": {\n \"readPrivate\": \"\",\n \"admin\": \"\",\n \"write\": \"\",\n \"readSSN\": \"\"\n }\n }\n }\n }\n },\n \"status\": \"inactive\",\n \"userId\": \"\"\n}", "code": 200, "cookie": [], "header": [ @@ -5484,7 +5615,7 @@ "value": "" } ], - "id": "763cedac-bc42-4e36-acac-ebf97868cb14", + "id": "9985c66c-c0a3-427f-9216-1720a0780102", "name": "200 response", "originalRequest": { "body": {}, @@ -5529,7 +5660,7 @@ "value": "application/json" } ], - "id": "9f4da85a-7798-4afc-b2a9-c3cff2f28dd7", + "id": "ebae5af4-d8b8-41af-9857-0a9aa22b76f0", "name": "404 response", "originalRequest": { "body": {}, @@ -5567,7 +5698,7 @@ }, { "event": [], - "id": "6e8bf8c4-70e1-459d-b4fb-001c9557b07c", + "id": "37982e2a-0a52-45ed-a45e-4c927de61597", "name": "/v1/staff-users/me", "protocolProfileBehavior": { "disableBodyPruning": true @@ -5612,7 +5743,7 @@ "response": [ { "_postman_previewlanguage": "json", - "body": "{\n \"attributes\": {\n \"email\": \"\",\n \"familyName\": \"\",\n \"givenName\": \"\"\n },\n \"permissions\": {\n \"key_0\": {\n \"actions\": {\n \"readPrivate\": \"\",\n \"admin\": \"\",\n \"readSSN\": \"\"\n },\n \"jurisdictions\": {\n \"key_0\": {\n \"actions\": {\n \"readPrivate\": \"\",\n \"admin\": \"\",\n \"write\": \"\",\n \"readSSN\": \"\"\n }\n }\n }\n },\n \"key_1\": {\n \"actions\": {\n \"readPrivate\": \"\",\n \"admin\": \"\",\n \"readSSN\": \"\"\n },\n \"jurisdictions\": {\n \"key_0\": {\n \"actions\": {\n \"readPrivate\": \"\",\n \"admin\": \"\",\n \"write\": \"\",\n \"readSSN\": \"\"\n }\n },\n \"key_1\": {\n \"actions\": {\n \"readPrivate\": \"\",\n \"admin\": \"\",\n \"write\": \"\",\n \"readSSN\": \"\"\n }\n },\n \"key_2\": {\n \"actions\": {\n \"readPrivate\": \"\",\n \"admin\": \"\",\n \"write\": \"\",\n \"readSSN\": \"\"\n }\n }\n }\n }\n },\n \"status\": \"inactive\",\n \"userId\": \"\"\n}", + "body": "{\n \"attributes\": {\n \"email\": \"\",\n \"familyName\": \"\",\n \"givenName\": \"\"\n },\n \"permissions\": {\n \"ex_be\": {\n \"actions\": {\n \"readPrivate\": \"\",\n \"admin\": \"\",\n \"readSSN\": \"\"\n },\n \"jurisdictions\": {\n \"veniam_016\": {\n \"actions\": {\n \"readPrivate\": \"\",\n \"admin\": \"\",\n \"write\": \"\",\n \"readSSN\": \"\"\n }\n }\n }\n },\n \"consectetur_4_a\": {\n \"actions\": {\n \"readPrivate\": \"\",\n \"admin\": \"\",\n \"readSSN\": \"\"\n },\n \"jurisdictions\": {\n \"laboris_63\": {\n \"actions\": {\n \"readPrivate\": \"\",\n \"admin\": \"\",\n \"write\": \"\",\n \"readSSN\": \"\"\n }\n },\n \"voluptate_139\": {\n \"actions\": {\n \"readPrivate\": \"\",\n \"admin\": \"\",\n \"write\": \"\",\n \"readSSN\": \"\"\n }\n },\n \"dolor_a\": {\n \"actions\": {\n \"readPrivate\": \"\",\n \"admin\": \"\",\n \"write\": \"\",\n \"readSSN\": \"\"\n }\n }\n }\n }\n },\n \"status\": \"inactive\",\n \"userId\": \"\"\n}", "code": 200, "cookie": [], "header": [ @@ -5630,7 +5761,7 @@ "value": "" } ], - "id": "767a1544-c202-4c8f-9df2-d09d46684e02", + "id": "4bfba877-9a3e-49ee-b678-def956187a20", "name": "200 response", "originalRequest": { "body": { @@ -5688,7 +5819,7 @@ "value": "application/json" } ], - "id": "9088f618-558a-42d6-92ad-adc728cd1db0", + "id": "0e518830-2e27-423f-af0c-b62f26d337ff", "name": "404 response", "originalRequest": { "body": { From ab8929cdac23e4254b1244a330cfcb669636c66b Mon Sep 17 00:00:00 2001 From: Landon Shumway Date: Tue, 7 Oct 2025 09:12:21 -0500 Subject: [PATCH 48/55] PR feedback - fix test comment --- .../feature-flag/tests/function/test_check_feature_flag.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/compact-connect/lambdas/python/feature-flag/tests/function/test_check_feature_flag.py b/backend/compact-connect/lambdas/python/feature-flag/tests/function/test_check_feature_flag.py index eb6d235e5..263a44373 100644 --- a/backend/compact-connect/lambdas/python/feature-flag/tests/function/test_check_feature_flag.py +++ b/backend/compact-connect/lambdas/python/feature-flag/tests/function/test_check_feature_flag.py @@ -147,7 +147,7 @@ def test_missing_flag_id_returns_400(self, mock_statsig): @patch('feature_flag_client.Statsig') def test_invalid_json_request_body_returns_400(self, mock_statsig): - """Test that missing flagId in path parameters returns 400 error""" + """Test that an invalid JSON request body returns a 400 error""" self._setup_mock_statsig(mock_statsig, mock_flag_enabled_return=True) from handlers.check_feature_flag import check_feature_flag From 935c1fb6350b6a2c5770807db8b9f26c49afb30d Mon Sep 17 00:00:00 2001 From: Landon Shumway Date: Tue, 7 Oct 2025 10:56:55 -0500 Subject: [PATCH 49/55] Remove unneeded email dependencies from UI lambda folder --- .../lambdas/nodejs/package.json | 3 - .../lambdas/nodejs/yarn.lock | 1683 +---------------- 2 files changed, 19 insertions(+), 1667 deletions(-) diff --git a/backend/compact-connect-ui-app/lambdas/nodejs/package.json b/backend/compact-connect-ui-app/lambdas/nodejs/package.json index a14a73afb..1c355f7b4 100644 --- a/backend/compact-connect-ui-app/lambdas/nodejs/package.json +++ b/backend/compact-connect-ui-app/lambdas/nodejs/package.json @@ -28,11 +28,8 @@ "@aws-lambda-powertools/logger": "^2.10.0", "@aws-sdk/client-dynamodb": "^3.682.0", "@aws-sdk/client-s3": "^3.682.0", - "@aws-sdk/client-ses": "^3.682.0", "@aws-sdk/util-dynamodb": "^3.682.0", - "@usewaypoint/email-builder": "^0.0.6", "aws-lambda": "1.0.7", - "nodemailer": "^6.9.12", "zod": "^3.23.8" } } diff --git a/backend/compact-connect-ui-app/lambdas/nodejs/yarn.lock b/backend/compact-connect-ui-app/lambdas/nodejs/yarn.lock index e72e6ada6..793afb090 100644 --- a/backend/compact-connect-ui-app/lambdas/nodejs/yarn.lock +++ b/backend/compact-connect-ui-app/lambdas/nodejs/yarn.lock @@ -2,14 +2,6 @@ # yarn lockfile v1 -"@ampproject/remapping@^2.2.0": - version "2.3.0" - resolved "https://registry.yarnpkg.com/@ampproject/remapping/-/remapping-2.3.0.tgz#ed441b6fa600072520ce18b43d2c8cc8caecc7f4" - integrity sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw== - dependencies: - "@jridgewell/gen-mapping" "^0.3.5" - "@jridgewell/trace-mapping" "^0.3.24" - "@aws-crypto/crc32@5.2.0": version "5.2.0" resolved "https://registry.yarnpkg.com/@aws-crypto/crc32/-/crc32-5.2.0.tgz#cfcc22570949c98c6689cfcbd2d693d36cdae2e1" @@ -206,54 +198,6 @@ "@smithy/util-waiter" "^3.1.9" tslib "^2.6.2" -"@aws-sdk/client-ses@^3.682.0": - version "3.699.0" - resolved "https://registry.yarnpkg.com/@aws-sdk/client-ses/-/client-ses-3.699.0.tgz#b48d9c8865877b7baa4b1a90cb3e4c2d59c0c5e0" - integrity sha512-prpkr2jnhD2KsinQMBdX2wvSpNxFm9d02EUR4L78yxjg2oppXmu/cBjWdlVrSkqqE2EYfcHo0JV2WmRZZC1VyQ== - dependencies: - "@aws-crypto/sha256-browser" "5.2.0" - "@aws-crypto/sha256-js" "5.2.0" - "@aws-sdk/client-sso-oidc" "3.699.0" - "@aws-sdk/client-sts" "3.699.0" - "@aws-sdk/core" "3.696.0" - "@aws-sdk/credential-provider-node" "3.699.0" - "@aws-sdk/middleware-host-header" "3.696.0" - "@aws-sdk/middleware-logger" "3.696.0" - "@aws-sdk/middleware-recursion-detection" "3.696.0" - "@aws-sdk/middleware-user-agent" "3.696.0" - "@aws-sdk/region-config-resolver" "3.696.0" - "@aws-sdk/types" "3.696.0" - "@aws-sdk/util-endpoints" "3.696.0" - "@aws-sdk/util-user-agent-browser" "3.696.0" - "@aws-sdk/util-user-agent-node" "3.696.0" - "@smithy/config-resolver" "^3.0.12" - "@smithy/core" "^2.5.3" - "@smithy/fetch-http-handler" "^4.1.1" - "@smithy/hash-node" "^3.0.10" - "@smithy/invalid-dependency" "^3.0.10" - "@smithy/middleware-content-length" "^3.0.12" - "@smithy/middleware-endpoint" "^3.2.3" - "@smithy/middleware-retry" "^3.0.27" - "@smithy/middleware-serde" "^3.0.10" - "@smithy/middleware-stack" "^3.0.10" - "@smithy/node-config-provider" "^3.1.11" - "@smithy/node-http-handler" "^3.3.1" - "@smithy/protocol-http" "^4.1.7" - "@smithy/smithy-client" "^3.4.4" - "@smithy/types" "^3.7.1" - "@smithy/url-parser" "^3.0.10" - "@smithy/util-base64" "^3.0.0" - "@smithy/util-body-length-browser" "^3.0.0" - "@smithy/util-body-length-node" "^3.0.0" - "@smithy/util-defaults-mode-browser" "^3.0.27" - "@smithy/util-defaults-mode-node" "^3.0.27" - "@smithy/util-endpoints" "^2.1.6" - "@smithy/util-middleware" "^3.0.10" - "@smithy/util-retry" "^3.0.10" - "@smithy/util-utf8" "^3.0.0" - "@smithy/util-waiter" "^3.1.9" - tslib "^2.6.2" - "@aws-sdk/client-sso-oidc@3.699.0": version "3.699.0" resolved "https://registry.yarnpkg.com/@aws-sdk/client-sso-oidc/-/client-sso-oidc-3.699.0.tgz#a35665e681abd518b56330bc7dab63041fbdaf83" @@ -751,7 +695,7 @@ "@smithy/types" "^3.7.1" tslib "^2.6.2" -"@babel/code-frame@^7.0.0", "@babel/code-frame@^7.12.13", "@babel/code-frame@^7.25.9", "@babel/code-frame@^7.26.0", "@babel/code-frame@^7.26.2": +"@babel/code-frame@^7.12.13": version "7.26.2" resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.26.2.tgz#4b5fab97d33338eff916235055f0ebc21e573a85" integrity sha512-RJlIHRueQgwWitWgF8OdFYGZX328Ax5BCemNGlqHfplnRT9ESi8JkFlvaVYbS+UubVY6dpv87Fs2u5M29iNFVQ== @@ -760,260 +704,11 @@ js-tokens "^4.0.0" picocolors "^1.0.0" -"@babel/compat-data@^7.25.9": - version "7.26.3" - resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.26.3.tgz#99488264a56b2aded63983abd6a417f03b92ed02" - integrity sha512-nHIxvKPniQXpmQLb0vhY3VaFb3S0YrTAwpOWJZh1wn3oJPjJk9Asva204PsBdmAE8vpzfHudT8DB0scYvy9q0g== - -"@babel/core@^7.11.6", "@babel/core@^7.12.3", "@babel/core@^7.23.9": - version "7.26.0" - resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.26.0.tgz#d78b6023cc8f3114ccf049eb219613f74a747b40" - integrity sha512-i1SLeK+DzNnQ3LL/CswPCa/E5u4lh1k6IAEphON8F+cXt0t9euTshDru0q7/IqMa1PMPz5RnHuHscF8/ZJsStg== - dependencies: - "@ampproject/remapping" "^2.2.0" - "@babel/code-frame" "^7.26.0" - "@babel/generator" "^7.26.0" - "@babel/helper-compilation-targets" "^7.25.9" - "@babel/helper-module-transforms" "^7.26.0" - "@babel/helpers" "^7.26.0" - "@babel/parser" "^7.26.0" - "@babel/template" "^7.25.9" - "@babel/traverse" "^7.25.9" - "@babel/types" "^7.26.0" - convert-source-map "^2.0.0" - debug "^4.1.0" - gensync "^1.0.0-beta.2" - json5 "^2.2.3" - semver "^6.3.1" - -"@babel/generator@^7.26.0", "@babel/generator@^7.26.3", "@babel/generator@^7.7.2": - version "7.26.3" - resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.26.3.tgz#ab8d4360544a425c90c248df7059881f4b2ce019" - integrity sha512-6FF/urZvD0sTeO7k6/B15pMLC4CHUv1426lzr3N01aHJTl046uCAh9LXW/fzeXXjPNCJ6iABW5XaWOsIZB93aQ== - dependencies: - "@babel/parser" "^7.26.3" - "@babel/types" "^7.26.3" - "@jridgewell/gen-mapping" "^0.3.5" - "@jridgewell/trace-mapping" "^0.3.25" - jsesc "^3.0.2" - -"@babel/helper-compilation-targets@^7.25.9": - version "7.25.9" - resolved "https://registry.yarnpkg.com/@babel/helper-compilation-targets/-/helper-compilation-targets-7.25.9.tgz#55af025ce365be3cdc0c1c1e56c6af617ce88875" - integrity sha512-j9Db8Suy6yV/VHa4qzrj9yZfZxhLWQdVnRlXxmKLYlhWUVB1sB2G5sxuWYXk/whHD9iW76PmNzxZ4UCnTQTVEQ== - dependencies: - "@babel/compat-data" "^7.25.9" - "@babel/helper-validator-option" "^7.25.9" - browserslist "^4.24.0" - lru-cache "^5.1.1" - semver "^6.3.1" - -"@babel/helper-module-imports@^7.25.9": - version "7.25.9" - resolved "https://registry.yarnpkg.com/@babel/helper-module-imports/-/helper-module-imports-7.25.9.tgz#e7f8d20602ebdbf9ebbea0a0751fb0f2a4141715" - integrity sha512-tnUA4RsrmflIM6W6RFTLFSXITtl0wKjgpnLgXyowocVPrbYrLUXSBXDgTs8BlbmIzIdlBySRQjINYs2BAkiLtw== - dependencies: - "@babel/traverse" "^7.25.9" - "@babel/types" "^7.25.9" - -"@babel/helper-module-transforms@^7.26.0": - version "7.26.0" - resolved "https://registry.yarnpkg.com/@babel/helper-module-transforms/-/helper-module-transforms-7.26.0.tgz#8ce54ec9d592695e58d84cd884b7b5c6a2fdeeae" - integrity sha512-xO+xu6B5K2czEnQye6BHA7DolFFmS3LB7stHZFaOLb1pAwO1HWLS8fXA+eh0A2yIvltPVmx3eNNDBJA2SLHXFw== - dependencies: - "@babel/helper-module-imports" "^7.25.9" - "@babel/helper-validator-identifier" "^7.25.9" - "@babel/traverse" "^7.25.9" - -"@babel/helper-plugin-utils@^7.0.0", "@babel/helper-plugin-utils@^7.10.4", "@babel/helper-plugin-utils@^7.12.13", "@babel/helper-plugin-utils@^7.14.5", "@babel/helper-plugin-utils@^7.25.9", "@babel/helper-plugin-utils@^7.8.0": - version "7.25.9" - resolved "https://registry.yarnpkg.com/@babel/helper-plugin-utils/-/helper-plugin-utils-7.25.9.tgz#9cbdd63a9443a2c92a725cca7ebca12cc8dd9f46" - integrity sha512-kSMlyUVdWe25rEsRGviIgOWnoT/nfABVWlqt9N19/dIPWViAOW2s9wznP5tURbs/IDuNk4gPy3YdYRgH3uxhBw== - -"@babel/helper-string-parser@^7.25.9": - version "7.25.9" - resolved "https://registry.yarnpkg.com/@babel/helper-string-parser/-/helper-string-parser-7.25.9.tgz#1aabb72ee72ed35789b4bbcad3ca2862ce614e8c" - integrity sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA== - "@babel/helper-validator-identifier@^7.25.9": version "7.25.9" resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.25.9.tgz#24b64e2c3ec7cd3b3c547729b8d16871f22cbdc7" integrity sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ== -"@babel/helper-validator-option@^7.25.9": - version "7.25.9" - resolved "https://registry.yarnpkg.com/@babel/helper-validator-option/-/helper-validator-option-7.25.9.tgz#86e45bd8a49ab7e03f276577f96179653d41da72" - integrity sha512-e/zv1co8pp55dNdEcCynfj9X7nyUKUXoUEwfXqaZt0omVOmDe9oOTdKStH4GmAw6zxMFs50ZayuMfHDKlO7Tfw== - -"@babel/helpers@^7.26.0": - version "7.26.0" - resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.26.0.tgz#30e621f1eba5aa45fe6f4868d2e9154d884119a4" - integrity sha512-tbhNuIxNcVb21pInl3ZSjksLCvgdZy9KwJ8brv993QtIVKJBBkYXz4q4ZbAv31GdnC+R90np23L5FbEBlthAEw== - dependencies: - "@babel/template" "^7.25.9" - "@babel/types" "^7.26.0" - -"@babel/parser@^7.1.0", "@babel/parser@^7.14.7", "@babel/parser@^7.20.7", "@babel/parser@^7.23.9", "@babel/parser@^7.25.9", "@babel/parser@^7.26.0", "@babel/parser@^7.26.3": - version "7.26.3" - resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.26.3.tgz#8c51c5db6ddf08134af1ddbacf16aaab48bac234" - integrity sha512-WJ/CvmY8Mea8iDXo6a7RK2wbmJITT5fN3BEkRuFlxVyNx8jOKIIhmC4fSkTcPcf8JyavbBwIe6OpiCOBXt/IcA== - dependencies: - "@babel/types" "^7.26.3" - -"@babel/plugin-syntax-async-generators@^7.8.4": - version "7.8.4" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz#a983fb1aeb2ec3f6ed042a210f640e90e786fe0d" - integrity sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw== - dependencies: - "@babel/helper-plugin-utils" "^7.8.0" - -"@babel/plugin-syntax-bigint@^7.8.3": - version "7.8.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-bigint/-/plugin-syntax-bigint-7.8.3.tgz#4c9a6f669f5d0cdf1b90a1671e9a146be5300cea" - integrity sha512-wnTnFlG+YxQm3vDxpGE57Pj0srRU4sHE/mDkt1qv2YJJSeUAec2ma4WLUnUPeKjyrfntVwe/N6dCXpU+zL3Npg== - dependencies: - "@babel/helper-plugin-utils" "^7.8.0" - -"@babel/plugin-syntax-class-properties@^7.12.13": - version "7.12.13" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.12.13.tgz#b5c987274c4a3a82b89714796931a6b53544ae10" - integrity sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA== - dependencies: - "@babel/helper-plugin-utils" "^7.12.13" - -"@babel/plugin-syntax-class-static-block@^7.14.5": - version "7.14.5" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-class-static-block/-/plugin-syntax-class-static-block-7.14.5.tgz#195df89b146b4b78b3bf897fd7a257c84659d406" - integrity sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw== - dependencies: - "@babel/helper-plugin-utils" "^7.14.5" - -"@babel/plugin-syntax-import-attributes@^7.24.7": - version "7.26.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.26.0.tgz#3b1412847699eea739b4f2602c74ce36f6b0b0f7" - integrity sha512-e2dttdsJ1ZTpi3B9UYGLw41hifAubg19AtCu/2I/F1QNVclOBr1dYpTdmdyZ84Xiz43BS/tCUkMAZNLv12Pi+A== - dependencies: - "@babel/helper-plugin-utils" "^7.25.9" - -"@babel/plugin-syntax-import-meta@^7.10.4": - version "7.10.4" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-import-meta/-/plugin-syntax-import-meta-7.10.4.tgz#ee601348c370fa334d2207be158777496521fd51" - integrity sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g== - dependencies: - "@babel/helper-plugin-utils" "^7.10.4" - -"@babel/plugin-syntax-json-strings@^7.8.3": - version "7.8.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz#01ca21b668cd8218c9e640cb6dd88c5412b2c96a" - integrity sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA== - dependencies: - "@babel/helper-plugin-utils" "^7.8.0" - -"@babel/plugin-syntax-jsx@^7.7.2": - version "7.25.9" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.25.9.tgz#a34313a178ea56f1951599b929c1ceacee719290" - integrity sha512-ld6oezHQMZsZfp6pWtbjaNDF2tiiCYYDqQszHt5VV437lewP9aSi2Of99CK0D0XB21k7FLgnLcmQKyKzynfeAA== - dependencies: - "@babel/helper-plugin-utils" "^7.25.9" - -"@babel/plugin-syntax-logical-assignment-operators@^7.10.4": - version "7.10.4" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz#ca91ef46303530448b906652bac2e9fe9941f699" - integrity sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig== - dependencies: - "@babel/helper-plugin-utils" "^7.10.4" - -"@babel/plugin-syntax-nullish-coalescing-operator@^7.8.3": - version "7.8.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz#167ed70368886081f74b5c36c65a88c03b66d1a9" - integrity sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ== - dependencies: - "@babel/helper-plugin-utils" "^7.8.0" - -"@babel/plugin-syntax-numeric-separator@^7.10.4": - version "7.10.4" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.4.tgz#b9b070b3e33570cd9fd07ba7fa91c0dd37b9af97" - integrity sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug== - dependencies: - "@babel/helper-plugin-utils" "^7.10.4" - -"@babel/plugin-syntax-object-rest-spread@^7.8.3": - version "7.8.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz#60e225edcbd98a640332a2e72dd3e66f1af55871" - integrity sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA== - dependencies: - "@babel/helper-plugin-utils" "^7.8.0" - -"@babel/plugin-syntax-optional-catch-binding@^7.8.3": - version "7.8.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.8.3.tgz#6111a265bcfb020eb9efd0fdfd7d26402b9ed6c1" - integrity sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q== - dependencies: - "@babel/helper-plugin-utils" "^7.8.0" - -"@babel/plugin-syntax-optional-chaining@^7.8.3": - version "7.8.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz#4f69c2ab95167e0180cd5336613f8c5788f7d48a" - integrity sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg== - dependencies: - "@babel/helper-plugin-utils" "^7.8.0" - -"@babel/plugin-syntax-private-property-in-object@^7.14.5": - version "7.14.5" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-private-property-in-object/-/plugin-syntax-private-property-in-object-7.14.5.tgz#0dc6671ec0ea22b6e94a1114f857970cd39de1ad" - integrity sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg== - dependencies: - "@babel/helper-plugin-utils" "^7.14.5" - -"@babel/plugin-syntax-top-level-await@^7.14.5": - version "7.14.5" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.14.5.tgz#c1cfdadc35a646240001f06138247b741c34d94c" - integrity sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw== - dependencies: - "@babel/helper-plugin-utils" "^7.14.5" - -"@babel/plugin-syntax-typescript@^7.7.2": - version "7.25.9" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.25.9.tgz#67dda2b74da43727cf21d46cf9afef23f4365399" - integrity sha512-hjMgRy5hb8uJJjUcdWunWVcoi9bGpJp8p5Ol1229PoN6aytsLwNMgmdftO23wnCLMfVmTwZDWMPNq/D1SY60JQ== - dependencies: - "@babel/helper-plugin-utils" "^7.25.9" - -"@babel/template@^7.25.9", "@babel/template@^7.3.3": - version "7.25.9" - resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.25.9.tgz#ecb62d81a8a6f5dc5fe8abfc3901fc52ddf15016" - integrity sha512-9DGttpmPvIxBb/2uwpVo3dqJ+O6RooAFOS+lB+xDqoE2PVCE8nfoHMdZLpfCQRLwvohzXISPZcgxt80xLfsuwg== - dependencies: - "@babel/code-frame" "^7.25.9" - "@babel/parser" "^7.25.9" - "@babel/types" "^7.25.9" - -"@babel/traverse@^7.25.9": - version "7.26.4" - resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.26.4.tgz#ac3a2a84b908dde6d463c3bfa2c5fdc1653574bd" - integrity sha512-fH+b7Y4p3yqvApJALCPJcwb0/XaOSgtK4pzV6WVjPR5GLFQBRI7pfoX2V2iM48NXvX07NUxxm1Vw98YjqTcU5w== - dependencies: - "@babel/code-frame" "^7.26.2" - "@babel/generator" "^7.26.3" - "@babel/parser" "^7.26.3" - "@babel/template" "^7.25.9" - "@babel/types" "^7.26.3" - debug "^4.3.1" - globals "^11.1.0" - -"@babel/types@^7.0.0", "@babel/types@^7.20.7", "@babel/types@^7.25.9", "@babel/types@^7.26.0", "@babel/types@^7.26.3", "@babel/types@^7.3.3": - version "7.26.3" - resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.26.3.tgz#37e79830f04c2b5687acc77db97fbc75fb81f3c0" - integrity sha512-vN5p+1kl59GVKMvTHt55NzzmYVxprfJD+ql7U9NFIfKCBkYE55LYtS+WtPlaYOyzydrKI8Nezd+aZextrd+FMA== - dependencies: - "@babel/helper-string-parser" "^7.25.9" - "@babel/helper-validator-identifier" "^7.25.9" - -"@bcoe/v8-coverage@^0.2.3": - version "0.2.3" - resolved "https://registry.yarnpkg.com/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz#75a2e8b51cb758a7553d6804a5932d7aace75c39" - integrity sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw== - "@colors/colors@1.6.0", "@colors/colors@^1.6.0": version "1.6.0" resolved "https://registry.yarnpkg.com/@colors/colors/-/colors-1.6.0.tgz#ec6cd237440700bc23ca23087f513c75508958b0" @@ -1248,78 +943,6 @@ wrap-ansi "^8.1.0" wrap-ansi-cjs "npm:wrap-ansi@^7.0.0" -"@istanbuljs/load-nyc-config@^1.0.0": - version "1.1.0" - resolved "https://registry.yarnpkg.com/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz#fd3db1d59ecf7cf121e80650bb86712f9b55eced" - integrity sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ== - dependencies: - camelcase "^5.3.1" - find-up "^4.1.0" - get-package-type "^0.1.0" - js-yaml "^3.13.1" - resolve-from "^5.0.0" - -"@istanbuljs/schema@^0.1.2", "@istanbuljs/schema@^0.1.3": - version "0.1.3" - resolved "https://registry.yarnpkg.com/@istanbuljs/schema/-/schema-0.1.3.tgz#e45e384e4b8ec16bce2fd903af78450f6bf7ec98" - integrity sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA== - -"@jest/console@^29.7.0": - version "29.7.0" - resolved "https://registry.yarnpkg.com/@jest/console/-/console-29.7.0.tgz#cd4822dbdb84529265c5a2bdb529a3c9cc950ffc" - integrity sha512-5Ni4CU7XHQi32IJ398EEP4RrB8eV09sXP2ROqD4bksHrnTree52PsxvX8tpL8LvTZ3pFzXyPbNQReSN41CAhOg== - dependencies: - "@jest/types" "^29.6.3" - "@types/node" "*" - chalk "^4.0.0" - jest-message-util "^29.7.0" - jest-util "^29.7.0" - slash "^3.0.0" - -"@jest/core@^29.7.0": - version "29.7.0" - resolved "https://registry.yarnpkg.com/@jest/core/-/core-29.7.0.tgz#b6cccc239f30ff36609658c5a5e2291757ce448f" - integrity sha512-n7aeXWKMnGtDA48y8TLWJPJmLmmZ642Ceo78cYWEpiD7FzDgmNDV/GCVRorPABdXLJZ/9wzzgZAlHjXjxDHGsg== - dependencies: - "@jest/console" "^29.7.0" - "@jest/reporters" "^29.7.0" - "@jest/test-result" "^29.7.0" - "@jest/transform" "^29.7.0" - "@jest/types" "^29.6.3" - "@types/node" "*" - ansi-escapes "^4.2.1" - chalk "^4.0.0" - ci-info "^3.2.0" - exit "^0.1.2" - graceful-fs "^4.2.9" - jest-changed-files "^29.7.0" - jest-config "^29.7.0" - jest-haste-map "^29.7.0" - jest-message-util "^29.7.0" - jest-regex-util "^29.6.3" - jest-resolve "^29.7.0" - jest-resolve-dependencies "^29.7.0" - jest-runner "^29.7.0" - jest-runtime "^29.7.0" - jest-snapshot "^29.7.0" - jest-util "^29.7.0" - jest-validate "^29.7.0" - jest-watcher "^29.7.0" - micromatch "^4.0.4" - pretty-format "^29.7.0" - slash "^3.0.0" - strip-ansi "^6.0.0" - -"@jest/environment@^29.7.0": - version "29.7.0" - resolved "https://registry.yarnpkg.com/@jest/environment/-/environment-29.7.0.tgz#24d61f54ff1f786f3cd4073b4b94416383baf2a7" - integrity sha512-aQIfHDq33ExsN4jP1NWGXhxgQ/wixs60gDiKO+XVMd8Mn0NWPWgc34ZQDTb2jKaUWQ7MuwoitXAsN2XVXNMpAw== - dependencies: - "@jest/fake-timers" "^29.7.0" - "@jest/types" "^29.6.3" - "@types/node" "*" - jest-mock "^29.7.0" - "@jest/expect-utils@^29.7.0": version "29.7.0" resolved "https://registry.yarnpkg.com/@jest/expect-utils/-/expect-utils-29.7.0.tgz#023efe5d26a8a70f21677d0a1afc0f0a44e3a1c6" @@ -1327,66 +950,6 @@ dependencies: jest-get-type "^29.6.3" -"@jest/expect@^29.7.0": - version "29.7.0" - resolved "https://registry.yarnpkg.com/@jest/expect/-/expect-29.7.0.tgz#76a3edb0cb753b70dfbfe23283510d3d45432bf2" - integrity sha512-8uMeAMycttpva3P1lBHB8VciS9V0XAr3GymPpipdyQXbBcuhkLQOSe8E/p92RyAdToS6ZD1tFkX+CkhoECE0dQ== - dependencies: - expect "^29.7.0" - jest-snapshot "^29.7.0" - -"@jest/fake-timers@^29.7.0": - version "29.7.0" - resolved "https://registry.yarnpkg.com/@jest/fake-timers/-/fake-timers-29.7.0.tgz#fd91bf1fffb16d7d0d24a426ab1a47a49881a565" - integrity sha512-q4DH1Ha4TTFPdxLsqDXK1d3+ioSL7yL5oCMJZgDYm6i+6CygW5E5xVr/D1HdsGxjt1ZWSfUAs9OxSB/BNelWrQ== - dependencies: - "@jest/types" "^29.6.3" - "@sinonjs/fake-timers" "^10.0.2" - "@types/node" "*" - jest-message-util "^29.7.0" - jest-mock "^29.7.0" - jest-util "^29.7.0" - -"@jest/globals@^29.7.0": - version "29.7.0" - resolved "https://registry.yarnpkg.com/@jest/globals/-/globals-29.7.0.tgz#8d9290f9ec47ff772607fa864ca1d5a2efae1d4d" - integrity sha512-mpiz3dutLbkW2MNFubUGUEVLkTGiqW6yLVTA+JbP6fI6J5iL9Y0Nlg8k95pcF8ctKwCS7WVxteBs29hhfAotzQ== - dependencies: - "@jest/environment" "^29.7.0" - "@jest/expect" "^29.7.0" - "@jest/types" "^29.6.3" - jest-mock "^29.7.0" - -"@jest/reporters@^29.7.0": - version "29.7.0" - resolved "https://registry.yarnpkg.com/@jest/reporters/-/reporters-29.7.0.tgz#04b262ecb3b8faa83b0b3d321623972393e8f4c7" - integrity sha512-DApq0KJbJOEzAFYjHADNNxAE3KbhxQB1y5Kplb5Waqw6zVbuWatSnMjE5gs8FUgEPmNsnZA3NCWl9NG0ia04Pg== - dependencies: - "@bcoe/v8-coverage" "^0.2.3" - "@jest/console" "^29.7.0" - "@jest/test-result" "^29.7.0" - "@jest/transform" "^29.7.0" - "@jest/types" "^29.6.3" - "@jridgewell/trace-mapping" "^0.3.18" - "@types/node" "*" - chalk "^4.0.0" - collect-v8-coverage "^1.0.0" - exit "^0.1.2" - glob "^7.1.3" - graceful-fs "^4.2.9" - istanbul-lib-coverage "^3.0.0" - istanbul-lib-instrument "^6.0.0" - istanbul-lib-report "^3.0.0" - istanbul-lib-source-maps "^4.0.0" - istanbul-reports "^3.1.3" - jest-message-util "^29.7.0" - jest-util "^29.7.0" - jest-worker "^29.7.0" - slash "^3.0.0" - string-length "^4.0.1" - strip-ansi "^6.0.0" - v8-to-istanbul "^9.0.1" - "@jest/schemas@^29.6.3": version "29.6.3" resolved "https://registry.yarnpkg.com/@jest/schemas/-/schemas-29.6.3.tgz#430b5ce8a4e0044a7e3819663305a7b3091c8e03" @@ -1394,56 +957,6 @@ dependencies: "@sinclair/typebox" "^0.27.8" -"@jest/source-map@^29.6.3": - version "29.6.3" - resolved "https://registry.yarnpkg.com/@jest/source-map/-/source-map-29.6.3.tgz#d90ba772095cf37a34a5eb9413f1b562a08554c4" - integrity sha512-MHjT95QuipcPrpLM+8JMSzFx6eHp5Bm+4XeFDJlwsvVBjmKNiIAvasGK2fxz2WbGRlnvqehFbh07MMa7n3YJnw== - dependencies: - "@jridgewell/trace-mapping" "^0.3.18" - callsites "^3.0.0" - graceful-fs "^4.2.9" - -"@jest/test-result@^29.7.0": - version "29.7.0" - resolved "https://registry.yarnpkg.com/@jest/test-result/-/test-result-29.7.0.tgz#8db9a80aa1a097bb2262572686734baed9b1657c" - integrity sha512-Fdx+tv6x1zlkJPcWXmMDAG2HBnaR9XPSd5aDWQVsfrZmLVT3lU1cwyxLgRmXR9yrq4NBoEm9BMsfgFzTQAbJYA== - dependencies: - "@jest/console" "^29.7.0" - "@jest/types" "^29.6.3" - "@types/istanbul-lib-coverage" "^2.0.0" - collect-v8-coverage "^1.0.0" - -"@jest/test-sequencer@^29.7.0": - version "29.7.0" - resolved "https://registry.yarnpkg.com/@jest/test-sequencer/-/test-sequencer-29.7.0.tgz#6cef977ce1d39834a3aea887a1726628a6f072ce" - integrity sha512-GQwJ5WZVrKnOJuiYiAF52UNUJXgTZx1NHjFSEB0qEMmSZKAkdMoIzw/Cj6x6NF4AvV23AUqDpFzQkN/eYCYTxw== - dependencies: - "@jest/test-result" "^29.7.0" - graceful-fs "^4.2.9" - jest-haste-map "^29.7.0" - slash "^3.0.0" - -"@jest/transform@^29.7.0": - version "29.7.0" - resolved "https://registry.yarnpkg.com/@jest/transform/-/transform-29.7.0.tgz#df2dd9c346c7d7768b8a06639994640c642e284c" - integrity sha512-ok/BTPFzFKVMwO5eOHRrvnBVHdRy9IrsrW1GpMaQ9MCnilNLXQKmAX8s1YXDFaai9xJpac2ySzV0YeRRECr2Vw== - dependencies: - "@babel/core" "^7.11.6" - "@jest/types" "^29.6.3" - "@jridgewell/trace-mapping" "^0.3.18" - babel-plugin-istanbul "^6.1.1" - chalk "^4.0.0" - convert-source-map "^2.0.0" - fast-json-stable-stringify "^2.1.0" - graceful-fs "^4.2.9" - jest-haste-map "^29.7.0" - jest-regex-util "^29.6.3" - jest-util "^29.7.0" - micromatch "^4.0.4" - pirates "^4.0.4" - slash "^3.0.0" - write-file-atomic "^4.0.2" - "@jest/types@^29.6.3": version "29.6.3" resolved "https://registry.yarnpkg.com/@jest/types/-/types-29.6.3.tgz#1131f8cf634e7e84c5e77bab12f052af585fba59" @@ -1456,38 +969,6 @@ "@types/yargs" "^17.0.8" chalk "^4.0.0" -"@jridgewell/gen-mapping@^0.3.5": - version "0.3.5" - resolved "https://registry.yarnpkg.com/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz#dcce6aff74bdf6dad1a95802b69b04a2fcb1fb36" - integrity sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg== - dependencies: - "@jridgewell/set-array" "^1.2.1" - "@jridgewell/sourcemap-codec" "^1.4.10" - "@jridgewell/trace-mapping" "^0.3.24" - -"@jridgewell/resolve-uri@^3.1.0": - version "3.1.2" - resolved "https://registry.yarnpkg.com/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz#7a0ee601f60f99a20c7c7c5ff0c80388c1189bd6" - integrity sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw== - -"@jridgewell/set-array@^1.2.1": - version "1.2.1" - resolved "https://registry.yarnpkg.com/@jridgewell/set-array/-/set-array-1.2.1.tgz#558fb6472ed16a4c850b889530e6b36438c49280" - integrity sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A== - -"@jridgewell/sourcemap-codec@^1.4.10", "@jridgewell/sourcemap-codec@^1.4.14": - version "1.5.0" - resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz#3188bcb273a414b0d215fd22a58540b989b9409a" - integrity sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ== - -"@jridgewell/trace-mapping@^0.3.12", "@jridgewell/trace-mapping@^0.3.18", "@jridgewell/trace-mapping@^0.3.24", "@jridgewell/trace-mapping@^0.3.25": - version "0.3.25" - resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz#15f190e98895f3fc23276ee14bc76b675c2e50f0" - integrity sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ== - dependencies: - "@jridgewell/resolve-uri" "^3.1.0" - "@jridgewell/sourcemap-codec" "^1.4.14" - "@pkgjs/parseargs@^0.11.0": version "0.11.0" resolved "https://registry.yarnpkg.com/@pkgjs/parseargs/-/parseargs-0.11.0.tgz#a77ea742fab25775145434eb1d2328cf5013ac33" @@ -1512,13 +993,6 @@ dependencies: "@sinonjs/commons" "^3.0.0" -"@sinonjs/fake-timers@^10.0.2": - version "10.3.0" - resolved "https://registry.yarnpkg.com/@sinonjs/fake-timers/-/fake-timers-10.3.0.tgz#55fdff1ecab9f354019129daf4df0dd4d923ea66" - integrity sha512-V4BG07kuYSUkTCSBHG8G8TNhM+F19jXFWnQtzj+we8DrkpSBCee9Z3Ms8yiGer/dlmhe35/Xdgyo3/0rQKg7YA== - dependencies: - "@sinonjs/commons" "^3.0.0" - "@sinonjs/fake-timers@^13.0.1": version "13.0.5" resolved "https://registry.yarnpkg.com/@sinonjs/fake-timers/-/fake-timers-13.0.5.tgz#36b9dbc21ad5546486ea9173d6bea063eb1717d5" @@ -2030,52 +1504,12 @@ "@smithy/types" "^3.7.2" tslib "^2.6.2" -"@types/babel__core@^7.1.14": - version "7.20.5" - resolved "https://registry.yarnpkg.com/@types/babel__core/-/babel__core-7.20.5.tgz#3df15f27ba85319caa07ba08d0721889bb39c017" - integrity sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA== - dependencies: - "@babel/parser" "^7.20.7" - "@babel/types" "^7.20.7" - "@types/babel__generator" "*" - "@types/babel__template" "*" - "@types/babel__traverse" "*" - -"@types/babel__generator@*": - version "7.6.8" - resolved "https://registry.yarnpkg.com/@types/babel__generator/-/babel__generator-7.6.8.tgz#f836c61f48b1346e7d2b0d93c6dacc5b9535d3ab" - integrity sha512-ASsj+tpEDsEiFr1arWrlN6V3mdfjRMZt6LtK/Vp/kreFLnr5QH5+DhvD5nINYZXzwJvXeGq+05iUXcAzVrqWtw== - dependencies: - "@babel/types" "^7.0.0" - -"@types/babel__template@*": - version "7.4.4" - resolved "https://registry.yarnpkg.com/@types/babel__template/-/babel__template-7.4.4.tgz#5672513701c1b2199bc6dad636a9d7491586766f" - integrity sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A== - dependencies: - "@babel/parser" "^7.1.0" - "@babel/types" "^7.0.0" - -"@types/babel__traverse@*", "@types/babel__traverse@^7.0.6": - version "7.20.6" - resolved "https://registry.yarnpkg.com/@types/babel__traverse/-/babel__traverse-7.20.6.tgz#8dc9f0ae0f202c08d8d4dab648912c8d6038e3f7" - integrity sha512-r1bzfrm0tomOI8g1SzvCaQHo6Lcv6zu0EA+W2kHrt8dyrHQxGzBBL4kdkzIS+jBMV+EYcMAEAqXqYaLJq5rOZg== - dependencies: - "@babel/types" "^7.20.7" - "@types/estree@^1.0.6": version "1.0.6" resolved "https://registry.yarnpkg.com/@types/estree/-/estree-1.0.6.tgz#628effeeae2064a1b4e79f78e81d87b7e5fc7b50" integrity sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw== -"@types/graceful-fs@^4.1.3": - version "4.1.9" - resolved "https://registry.yarnpkg.com/@types/graceful-fs/-/graceful-fs-4.1.9.tgz#2a06bc0f68a20ab37b3e36aa238be6abdf49e8b4" - integrity sha512-olP3sd1qOEe5dXTSaFvQG+02VdRXcdytWLAZsAq1PecU8uqQAhkrnbli7DagjtXKW/Bl7YJbUsa8MPcuc8LHEQ== - dependencies: - "@types/node" "*" - -"@types/istanbul-lib-coverage@*", "@types/istanbul-lib-coverage@^2.0.0", "@types/istanbul-lib-coverage@^2.0.1": +"@types/istanbul-lib-coverage@*", "@types/istanbul-lib-coverage@^2.0.0": version "2.0.6" resolved "https://registry.yarnpkg.com/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz#7739c232a1fee9b4d3ce8985f314c0c6d33549d7" integrity sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w== @@ -2145,80 +1579,6 @@ dependencies: "@types/yargs-parser" "*" -"@usewaypoint/block-avatar@^0.0.3": - version "0.0.3" - resolved "https://registry.yarnpkg.com/@usewaypoint/block-avatar/-/block-avatar-0.0.3.tgz#f63510ed82690a2b4b68ead62a191d80b20c2957" - integrity sha512-3BM6P4ztMmqDbSijtVQqI1canRkcENOEHZ2X9BYNv8BZGJbmitTrzANvwmmYXfFEuWPCAyABvujdZds15Zg8Qg== - -"@usewaypoint/block-button@^0.0.3": - version "0.0.3" - resolved "https://registry.yarnpkg.com/@usewaypoint/block-button/-/block-button-0.0.3.tgz#1e9257601b452ab5687ce806cd24efd88a1185f3" - integrity sha512-LXSI3FmCTv13voYX4wdHY7iJdsfyRfpDJZCFKSun5EF1j9FXrqMDGScpk/yokopkQWvWkYXQNAne7W0yWhRQlg== - -"@usewaypoint/block-columns-container@^0.0.3": - version "0.0.3" - resolved "https://registry.yarnpkg.com/@usewaypoint/block-columns-container/-/block-columns-container-0.0.3.tgz#e6d9148f06523aa964a41f937e0b295c165d59d2" - integrity sha512-r5jaojU1Fr6Svtl0a9dDlBHgslJQ04M+XaXaEO+GZ12+35fdAirpLkrEhuyBIA1FFXzRTG740wkbkr++iv1kuA== - -"@usewaypoint/block-container@^0.0.2": - version "0.0.2" - resolved "https://registry.yarnpkg.com/@usewaypoint/block-container/-/block-container-0.0.2.tgz#de06a31242c799c7d1caaf80ab91107ca4b25fc5" - integrity sha512-li9GVdiahVpJ+MNRdkoCkP6/hBTdcpaLRGpaFBSQRkVt+cYAeB7qPNIo+242hUvVTm5Qky8ceGLDVblGYSZb7A== - -"@usewaypoint/block-divider@^0.0.4": - version "0.0.4" - resolved "https://registry.yarnpkg.com/@usewaypoint/block-divider/-/block-divider-0.0.4.tgz#29a938293a76ad8a5e9738d4ac4370f9fa3efbd8" - integrity sha512-q54ydWvKdg7Zwc4hzIwE6i/mC8dFYxfPRACEEEyu2dvSNa9cbKFIsPD9ipVSntK+Ib3Ml84uT4aHQmOlzP6hZA== - -"@usewaypoint/block-heading@^0.0.3": - version "0.0.3" - resolved "https://registry.yarnpkg.com/@usewaypoint/block-heading/-/block-heading-0.0.3.tgz#a300894acfc39556d1577be17a2d7b3d58c7c95e" - integrity sha512-1dMrf1U34nq2FuwTUfsq+hBOdLQz1H+lVMEH9xvyCq5I7nSXCzpeo7QgumZ3zZEHtu3QgSEGafJaZyrj2paC0w== - -"@usewaypoint/block-html@^0.0.3": - version "0.0.3" - resolved "https://registry.yarnpkg.com/@usewaypoint/block-html/-/block-html-0.0.3.tgz#296550235b2974679afcad4772f7bc56cf8ba520" - integrity sha512-ZI9oYDibMzs5y/YzfvUwuUBzHDKHOIjiStiVCvlmIA+VtJTycqT8X/ECjn+KmwesLTg5DhG07CC4WY2SL3AnJw== - -"@usewaypoint/block-image@^0.0.5": - version "0.0.5" - resolved "https://registry.yarnpkg.com/@usewaypoint/block-image/-/block-image-0.0.5.tgz#e9e866673d2bbdb628fd57c0798ba70e3cdc3b12" - integrity sha512-b66jAXF79idsrIRc2QoBlZctIXdqg/qOAL7/QvKvENZH2KmuXoZhEUx+Z7sACvEQD/VI0u7TK5msDsA5S0/oVQ== - -"@usewaypoint/block-spacer@^0.0.3": - version "0.0.3" - resolved "https://registry.yarnpkg.com/@usewaypoint/block-spacer/-/block-spacer-0.0.3.tgz#6655628dd74085ffcf3fbcd0f0aa286e40e69a10" - integrity sha512-CCcMtwcpeC2rHvawQdh5f0Hez7o4xA/edWl/6I3RuA6Yb6STyyrGjmPFs2ZxHQsLOGUK+0OvBenuHlSTCZwuuA== - -"@usewaypoint/block-text@^0.0.4": - version "0.0.4" - resolved "https://registry.yarnpkg.com/@usewaypoint/block-text/-/block-text-0.0.4.tgz#22be615f81f91749eac78501b1de9dfb5bd95560" - integrity sha512-c+CiTkwFSrclPxRx9Gt+nE6KkAmY5tWDumBa3qQnVrxdCjCmGK0qOj9avm9vqf9hd5JxaX4tgWhG/oi2u/zMxA== - dependencies: - markdown-parser-react "^1.1.2" - -"@usewaypoint/document-core@^0.0.6": - version "0.0.6" - resolved "https://registry.yarnpkg.com/@usewaypoint/document-core/-/document-core-0.0.6.tgz#c97468c84c85ccac46a06f82ac332e20e415cfb7" - integrity sha512-Hg10gszVCZRJhA4nIWwAi2rTXuoxPL+ATMe0hU243PFBIUZOwDIQus4XZSeoHsenMCq1uBFCRiFW4hl2+tVwgA== - -"@usewaypoint/email-builder@^0.0.6": - version "0.0.6" - resolved "https://registry.yarnpkg.com/@usewaypoint/email-builder/-/email-builder-0.0.6.tgz#e6bb5ad39acb0710814df1d925ee366276bec49f" - integrity sha512-OullFjVUwlPXkq7b+HSGMPKJcSSEbH1gm6a2TQ5uIsPIHeFb/af6EmXd8IIctf0pz1cH41USg6rBR504vuuXZw== - dependencies: - "@usewaypoint/block-avatar" "^0.0.3" - "@usewaypoint/block-button" "^0.0.3" - "@usewaypoint/block-columns-container" "^0.0.3" - "@usewaypoint/block-container" "^0.0.2" - "@usewaypoint/block-divider" "^0.0.4" - "@usewaypoint/block-heading" "^0.0.3" - "@usewaypoint/block-html" "^0.0.3" - "@usewaypoint/block-image" "^0.0.5" - "@usewaypoint/block-spacer" "^0.0.3" - "@usewaypoint/block-text" "^0.0.4" - "@usewaypoint/document-core" "^0.0.6" - "@vitest/expect@>1.6.0": version "2.1.8" resolved "https://registry.yarnpkg.com/@vitest/expect/-/expect-2.1.8.tgz#13fad0e8d5a0bf0feb675dcf1d1f1a36a1773bc1" @@ -2277,13 +1637,6 @@ ansi-colors@^4.1.3: resolved "https://registry.yarnpkg.com/ansi-colors/-/ansi-colors-4.1.3.tgz#37611340eb2243e70cc604cad35d63270d48781b" integrity sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw== -ansi-escapes@^4.2.1: - version "4.3.2" - resolved "https://registry.yarnpkg.com/ansi-escapes/-/ansi-escapes-4.3.2.tgz#6b2291d1db7d98b6521d5f1efa42d0f3a9feb65e" - integrity sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ== - dependencies: - type-fest "^0.21.3" - ansi-regex@^5.0.1: version "5.0.1" resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-5.0.1.tgz#082cb2c89c9fe8659a311a53bd6a4dc5301db304" @@ -2311,7 +1664,7 @@ ansi-styles@^6.1.0: resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-6.2.1.tgz#0e62320cf99c21afff3b3012192546aacbfb05c5" integrity sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug== -anymatch@^3.0.3, anymatch@~3.1.2: +anymatch@~3.1.2: version "3.1.3" resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-3.1.3.tgz#790c58b19ba1720a84205b57c618d5ad8524973e" integrity sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw== @@ -2397,69 +1750,6 @@ aws-sdk@^2.814.0: uuid "8.0.0" xml2js "0.6.2" -babel-jest@^29.7.0: - version "29.7.0" - resolved "https://registry.yarnpkg.com/babel-jest/-/babel-jest-29.7.0.tgz#f4369919225b684c56085998ac63dbd05be020d5" - integrity sha512-BrvGY3xZSwEcCzKvKsCi2GgHqDqsYkOP4/by5xCgIwGXQxIEh+8ew3gmrE1y7XRR6LHZIj6yLYnUi/mm2KXKBg== - dependencies: - "@jest/transform" "^29.7.0" - "@types/babel__core" "^7.1.14" - babel-plugin-istanbul "^6.1.1" - babel-preset-jest "^29.6.3" - chalk "^4.0.0" - graceful-fs "^4.2.9" - slash "^3.0.0" - -babel-plugin-istanbul@^6.1.1: - version "6.1.1" - resolved "https://registry.yarnpkg.com/babel-plugin-istanbul/-/babel-plugin-istanbul-6.1.1.tgz#fa88ec59232fd9b4e36dbbc540a8ec9a9b47da73" - integrity sha512-Y1IQok9821cC9onCx5otgFfRm7Lm+I+wwxOx738M/WLPZ9Q42m4IG5W0FNX8WLL2gYMZo3JkuXIH2DOpWM+qwA== - dependencies: - "@babel/helper-plugin-utils" "^7.0.0" - "@istanbuljs/load-nyc-config" "^1.0.0" - "@istanbuljs/schema" "^0.1.2" - istanbul-lib-instrument "^5.0.4" - test-exclude "^6.0.0" - -babel-plugin-jest-hoist@^29.6.3: - version "29.6.3" - resolved "https://registry.yarnpkg.com/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-29.6.3.tgz#aadbe943464182a8922c3c927c3067ff40d24626" - integrity sha512-ESAc/RJvGTFEzRwOTT4+lNDk/GNHMkKbNzsvT0qKRfDyyYTskxB5rnU2njIDYVxXCBHHEI1c0YwHob3WaYujOg== - dependencies: - "@babel/template" "^7.3.3" - "@babel/types" "^7.3.3" - "@types/babel__core" "^7.1.14" - "@types/babel__traverse" "^7.0.6" - -babel-preset-current-node-syntax@^1.0.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/babel-preset-current-node-syntax/-/babel-preset-current-node-syntax-1.1.0.tgz#9a929eafece419612ef4ae4f60b1862ebad8ef30" - integrity sha512-ldYss8SbBlWva1bs28q78Ju5Zq1F+8BrqBZZ0VFhLBvhh6lCpC2o3gDJi/5DRLs9FgYZCnmPYIVFU4lRXCkyUw== - dependencies: - "@babel/plugin-syntax-async-generators" "^7.8.4" - "@babel/plugin-syntax-bigint" "^7.8.3" - "@babel/plugin-syntax-class-properties" "^7.12.13" - "@babel/plugin-syntax-class-static-block" "^7.14.5" - "@babel/plugin-syntax-import-attributes" "^7.24.7" - "@babel/plugin-syntax-import-meta" "^7.10.4" - "@babel/plugin-syntax-json-strings" "^7.8.3" - "@babel/plugin-syntax-logical-assignment-operators" "^7.10.4" - "@babel/plugin-syntax-nullish-coalescing-operator" "^7.8.3" - "@babel/plugin-syntax-numeric-separator" "^7.10.4" - "@babel/plugin-syntax-object-rest-spread" "^7.8.3" - "@babel/plugin-syntax-optional-catch-binding" "^7.8.3" - "@babel/plugin-syntax-optional-chaining" "^7.8.3" - "@babel/plugin-syntax-private-property-in-object" "^7.14.5" - "@babel/plugin-syntax-top-level-await" "^7.14.5" - -babel-preset-jest@^29.6.3: - version "29.6.3" - resolved "https://registry.yarnpkg.com/babel-preset-jest/-/babel-preset-jest-29.6.3.tgz#fa05fa510e7d493896d7b0dd2033601c840f171c" - integrity sha512-0B3bhxR6snWXJZtR/RliHTDPRgn1sNHOR0yVtq/IiQFyuOVjFS+wuio/R4gSNkyYmKmJB4wGZv2NZanmKmTnNA== - dependencies: - babel-plugin-jest-hoist "^29.6.3" - babel-preset-current-node-syntax "^1.0.0" - balanced-match@^1.0.0: version "1.0.2" resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee" @@ -2507,28 +1797,6 @@ browser-stdout@^1.3.1: resolved "https://registry.yarnpkg.com/browser-stdout/-/browser-stdout-1.3.1.tgz#baa559ee14ced73452229bad7326467c61fabd60" integrity sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw== -browserslist@^4.24.0: - version "4.24.2" - resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.24.2.tgz#f5845bc91069dbd55ee89faf9822e1d885d16580" - integrity sha512-ZIc+Q62revdMcqC6aChtW4jz3My3klmCO1fEmINZY/8J3EpBg5/A/D0AKmBveUh6pgoeycoMkVMko84tuYS+Gg== - dependencies: - caniuse-lite "^1.0.30001669" - electron-to-chromium "^1.5.41" - node-releases "^2.0.18" - update-browserslist-db "^1.1.1" - -bser@2.1.1: - version "2.1.1" - resolved "https://registry.yarnpkg.com/bser/-/bser-2.1.1.tgz#e6787da20ece9d07998533cfd9de6f5c38f4bc05" - integrity sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ== - dependencies: - node-int64 "^0.4.0" - -buffer-from@^1.0.0: - version "1.1.2" - resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.2.tgz#2b146a6fd72e80b4f55d255f35ed59a3a9a41bd5" - integrity sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ== - buffer@4.9.2: version "4.9.2" resolved "https://registry.yarnpkg.com/buffer/-/buffer-4.9.2.tgz#230ead344002988644841ab0244af8c44bbe3ef8" @@ -2561,21 +1829,11 @@ callsites@^3.0.0: resolved "https://registry.yarnpkg.com/callsites/-/callsites-3.1.0.tgz#b3630abd8943432f54b3f0519238e33cd7df2f73" integrity sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ== -camelcase@^5.3.1: - version "5.3.1" - resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-5.3.1.tgz#e3c9b31569e106811df242f715725a1f4c494320" - integrity sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg== - -camelcase@^6.0.0, camelcase@^6.2.0: +camelcase@^6.0.0: version "6.3.0" resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-6.3.0.tgz#5685b95eb209ac9c0c177467778c9c84df58ba9a" integrity sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA== -caniuse-lite@^1.0.30001669: - version "1.0.30001687" - resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001687.tgz#d0ac634d043648498eedf7a3932836beba90ebae" - integrity sha512-0S/FDhf4ZiqrTUiQ39dKeUjYRjkv7lOZU1Dgif2rIqrTzX/1wV2hfKu9TOm1IHkdSijfLswxTFzl/cvir+SLSQ== - chai-match-pattern@^1.1.0: version "1.3.0" resolved "https://registry.yarnpkg.com/chai-match-pattern/-/chai-match-pattern-1.3.0.tgz#cefd4437de465860f4f87922c31049eb9d979104" @@ -2615,11 +1873,6 @@ chalk@^4.0.0, chalk@^4.1.0, chalk@^4.1.2: ansi-styles "^4.1.0" supports-color "^7.1.0" -char-regex@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/char-regex/-/char-regex-1.0.2.tgz#d744358226217f981ed58f479b1d6bcc29545dcf" - integrity sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw== - check-error@^1.0.3: version "1.0.3" resolved "https://registry.yarnpkg.com/check-error/-/check-error-1.0.3.tgz#a6502e4312a7ee969f646e83bb3ddd56281bd694" @@ -2660,11 +1913,6 @@ ci-info@^3.2.0: resolved "https://registry.yarnpkg.com/ci-info/-/ci-info-3.9.0.tgz#4279a62028a7b1f262f3473fc9605f5e218c59b4" integrity sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ== -cjs-module-lexer@^1.0.0: - version "1.4.1" - resolved "https://registry.yarnpkg.com/cjs-module-lexer/-/cjs-module-lexer-1.4.1.tgz#707413784dbb3a72aa11c2f2b042a0bef4004170" - integrity sha512-cuSVIHi9/9E/+821Qjdvngor+xpnlwnuwIyZOaLmHBVdXL+gP+I6QQB9VkO7RI77YIcTV+S1W9AreJ5eN63JBA== - cliui@^7.0.2: version "7.0.4" resolved "https://registry.yarnpkg.com/cliui/-/cliui-7.0.4.tgz#a0265ee655476fc807aea9df3df8df7783808b4f" @@ -2674,25 +1922,6 @@ cliui@^7.0.2: strip-ansi "^6.0.0" wrap-ansi "^7.0.0" -cliui@^8.0.1: - version "8.0.1" - resolved "https://registry.yarnpkg.com/cliui/-/cliui-8.0.1.tgz#0c04b075db02cbfe60dc8e6cf2f5486b1a3608aa" - integrity sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ== - dependencies: - string-width "^4.2.0" - strip-ansi "^6.0.1" - wrap-ansi "^7.0.0" - -co@^4.6.0: - version "4.6.0" - resolved "https://registry.yarnpkg.com/co/-/co-4.6.0.tgz#6ea6bdf3d853ae54ccb8e47bfa0bf3f9031fb184" - integrity sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ== - -collect-v8-coverage@^1.0.0: - version "1.0.2" - resolved "https://registry.yarnpkg.com/collect-v8-coverage/-/collect-v8-coverage-1.0.2.tgz#c0b29bcd33bcd0779a1344c2136051e6afd3d9e9" - integrity sha512-lHl4d5/ONEbLlJvaJNtsF/Lz+WvB07u2ycqTYbdrq7UypDXailES4valYb2eWiJFxZlVmpGekfqoxQhzyFdT4Q== - color-convert@^1.9.3: version "1.9.3" resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.3.tgz#bb71850690e1f136567de629d2d5471deda4c1e8" @@ -2756,25 +1985,7 @@ concat-map@0.0.1: resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" integrity sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg== -convert-source-map@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-2.0.0.tgz#4b560f649fc4e918dd0ab75cf4961e8bc882d82a" - integrity sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg== - -create-jest@^29.7.0: - version "29.7.0" - resolved "https://registry.yarnpkg.com/create-jest/-/create-jest-29.7.0.tgz#a355c5b3cb1e1af02ba177fe7afd7feee49a5320" - integrity sha512-Adz2bdH0Vq3F53KEMJOoftQFutWCukm6J24wbPWRO4k1kMY7gS7ds/uoJkNuV8wDCtWWnuwGcJwpWcih+zEW1Q== - dependencies: - "@jest/types" "^29.6.3" - chalk "^4.0.0" - exit "^0.1.2" - graceful-fs "^4.2.9" - jest-config "^29.7.0" - jest-util "^29.7.0" - prompts "^2.0.1" - -cross-spawn@^7.0.0, cross-spawn@^7.0.3, cross-spawn@^7.0.5: +cross-spawn@^7.0.0, cross-spawn@^7.0.5: version "7.0.6" resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.6.tgz#8a58fe78f00dcd70c370451759dfbfaf03e8ee9f" integrity sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA== @@ -2783,7 +1994,7 @@ cross-spawn@^7.0.0, cross-spawn@^7.0.3, cross-spawn@^7.0.5: shebang-command "^2.0.0" which "^2.0.1" -debug@^4.1.0, debug@^4.1.1, debug@^4.3.1, debug@^4.3.2, debug@^4.3.5: +debug@^4.3.1, debug@^4.3.2, debug@^4.3.5: version "4.4.0" resolved "https://registry.yarnpkg.com/debug/-/debug-4.4.0.tgz#2b3f2aea2ffeb776477460267377dc8710faba8a" integrity sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA== @@ -2795,11 +2006,6 @@ decamelize@^4.0.0: resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-4.0.0.tgz#aa472d7bf660eb15f3494efd531cab7f2a709837" integrity sha512-9iE1PgSik9HeIIw2JO94IidnE3eBoQrFJ3w7sFuzSX4DpmZ3v5sZpUiV5Swcf6mQEF+Y0ru8Neo+p+nyh2J+hQ== -dedent@^1.0.0: - version "1.5.3" - resolved "https://registry.yarnpkg.com/dedent/-/dedent-1.5.3.tgz#99aee19eb9bae55a67327717b6e848d0bf777e5a" - integrity sha512-NHQtfOOW68WD8lgypbLA5oT+Bt0xXJhiYvoR6SmmNXZfpzOGXwdKWmcwG8N7PwVVWV3eF/68nmD9BaJSsTBhyQ== - deep-eql@^4.1.3: version "4.1.4" resolved "https://registry.yarnpkg.com/deep-eql/-/deep-eql-4.1.4.tgz#d0d3912865911bb8fac5afb4e3acfa6a28dc72b7" @@ -2817,11 +2023,6 @@ deep-is@^0.1.3: resolved "https://registry.yarnpkg.com/deep-is/-/deep-is-0.1.4.tgz#a6f2dce612fadd2ef1f519b73551f17e85199831" integrity sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ== -deepmerge@^4.2.2: - version "4.3.1" - resolved "https://registry.yarnpkg.com/deepmerge/-/deepmerge-4.3.1.tgz#44b5f2147cd3b00d4b56137685966f26fd25dd4a" - integrity sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A== - define-data-property@^1.1.4: version "1.1.4" resolved "https://registry.yarnpkg.com/define-data-property/-/define-data-property-1.1.4.tgz#894dc141bb7d3060ae4366f6a0107e68fbe48c5e" @@ -2831,11 +2032,6 @@ define-data-property@^1.1.4: es-errors "^1.3.0" gopd "^1.0.1" -detect-newline@^3.0.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/detect-newline/-/detect-newline-3.1.0.tgz#576f5dfc63ae1a192ff192d8ad3af6308991b651" - integrity sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA== - diff-sequences@^29.6.3: version "29.6.3" resolved "https://registry.yarnpkg.com/diff-sequences/-/diff-sequences-29.6.3.tgz#4deaf894d11407c51efc8418012f9e70b84ea921" @@ -2865,16 +2061,6 @@ eastasianwidth@^0.2.0: resolved "https://registry.yarnpkg.com/eastasianwidth/-/eastasianwidth-0.2.0.tgz#696ce2ec0aa0e6ea93a397ffcf24aa7840c827cb" integrity sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA== -electron-to-chromium@^1.5.41: - version "1.5.71" - resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.5.71.tgz#d8b5dba1e55b320f2f4e9b1ca80738f53fcfec2b" - integrity sha512-dB68l59BI75W1BUGVTAEJy45CEVuEGy9qPVVQ8pnHyHMn36PLPPoE1mjLH+lo9rKulO3HC2OhbACI/8tCqJBcA== - -emittery@^0.13.1: - version "0.13.1" - resolved "https://registry.yarnpkg.com/emittery/-/emittery-0.13.1.tgz#c04b8c3457490e0847ae51fced3af52d338e3dad" - integrity sha512-DeWwawk6r5yR9jFgnDKYt4sLS0LmHJJi3ZOnb5/JdbYwj3nW+FxQnHIjhBKz8YLC7oRNPVM9NQ47I3CVx34eqQ== - emoji-regex@^8.0.0: version "8.0.0" resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-8.0.0.tgz#e818fd69ce5ccfcb404594f842963bf53164cc37" @@ -2890,13 +2076,6 @@ enabled@2.0.x: resolved "https://registry.yarnpkg.com/enabled/-/enabled-2.0.0.tgz#f9dd92ec2d6f4bbc0d5d1e64e21d61cd4665e7c2" integrity sha512-AKrN98kuwOzMIdAizXGI86UFBoo26CL21UM763y1h/GMSJ4/OHU9k2YlsmBpyScFo/wbLzWQJBMCW4+IO3/+OQ== -error-ex@^1.3.1: - version "1.3.2" - resolved "https://registry.yarnpkg.com/error-ex/-/error-ex-1.3.2.tgz#b4ac40648107fdcdcfae242f428bea8a14d4f1bf" - integrity sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g== - dependencies: - is-arrayish "^0.2.1" - es-define-property@^1.0.0, es-define-property@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/es-define-property/-/es-define-property-1.0.1.tgz#983eb2f9a6724e9303f61addf011c72e09e0b0fa" @@ -2937,7 +2116,7 @@ esbuild@0.24.0: "@esbuild/win32-ia32" "0.24.0" "@esbuild/win32-x64" "0.24.0" -escalade@^3.1.1, escalade@^3.2.0: +escalade@^3.1.1: version "3.2.0" resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.2.0.tgz#011a3f69856ba189dffa7dc8fcce99d2a87903e5" integrity sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA== @@ -3053,27 +2232,7 @@ events@1.1.1: resolved "https://registry.yarnpkg.com/events/-/events-1.1.1.tgz#9ebdb7635ad099c70dcc4c2a1f5004288e8bd924" integrity sha512-kEcvvCBByWXGnZy6JUlgAp2gBIUjfCAV6P6TgT1/aaQKcmuAEC4OZTV1I4EWQLz2gxZw76atuVyvHhTxvi0Flw== -execa@^5.0.0: - version "5.1.1" - resolved "https://registry.yarnpkg.com/execa/-/execa-5.1.1.tgz#f80ad9cbf4298f7bd1d4c9555c21e93741c411dd" - integrity sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg== - dependencies: - cross-spawn "^7.0.3" - get-stream "^6.0.0" - human-signals "^2.1.0" - is-stream "^2.0.0" - merge-stream "^2.0.0" - npm-run-path "^4.0.1" - onetime "^5.1.2" - signal-exit "^3.0.3" - strip-final-newline "^2.0.0" - -exit@^0.1.2: - version "0.1.2" - resolved "https://registry.yarnpkg.com/exit/-/exit-0.1.2.tgz#0632638f8d877cc82107d30a0fff1a17cba1cd0c" - integrity sha512-Zk/eNKV2zbjpKzrsQ+n1G6poVbErQxJ0LBOJXaKZ1EViLzH+hrLu9cdXI4zw9dBQJslwBEpbQ2P1oS7nDxs6jQ== - -expect@>28.1.3, expect@^29.7.0: +expect@>28.1.3: version "29.7.0" resolved "https://registry.yarnpkg.com/expect/-/expect-29.7.0.tgz#578874590dcb3214514084c08115d8aee61e11bc" integrity sha512-2Zks0hf1VLFYI1kbh0I5jP3KHHyCHpkfyHBzsSXRFgl/Bg9mWYfMW8oD+PdMPlEwy5HNsR9JutYy6pMeOh61nw== @@ -3089,7 +2248,7 @@ fast-deep-equal@^3.1.1, fast-deep-equal@^3.1.3: resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525" integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q== -fast-json-stable-stringify@^2.0.0, fast-json-stable-stringify@^2.1.0: +fast-json-stable-stringify@^2.0.0: version "2.1.0" resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz#874bf69c6f404c2b5d99c481341399fd55892633" integrity sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw== @@ -3106,13 +2265,6 @@ fast-xml-parser@4.4.1: dependencies: strnum "^1.0.5" -fb-watchman@^2.0.0: - version "2.0.2" - resolved "https://registry.yarnpkg.com/fb-watchman/-/fb-watchman-2.0.2.tgz#e9524ee6b5c77e9e5001af0f85f3adbb8623255c" - integrity sha512-p5161BqbuCaSnB8jIbzQHOlpgsPmK5rJVDfDKO91Axs5NC1uu3HRQm6wt9cd9/+GtQQIO53JdGXXoyDpTAsgYA== - dependencies: - bser "2.1.1" - fecha@^4.2.0: version "4.2.3" resolved "https://registry.yarnpkg.com/fecha/-/fecha-4.2.3.tgz#4d9ccdbc61e8629b259fdca67e65891448d569fd" @@ -3132,14 +2284,6 @@ fill-range@^7.1.1: dependencies: to-regex-range "^5.0.1" -find-up@^4.0.0, find-up@^4.1.0: - version "4.1.0" - resolved "https://registry.yarnpkg.com/find-up/-/find-up-4.1.0.tgz#97afe7d6cdc0bc5928584b7c8d7b16e8a9aa5d19" - integrity sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw== - dependencies: - locate-path "^5.0.0" - path-exists "^4.0.0" - find-up@^5.0.0: version "5.0.0" resolved "https://registry.yarnpkg.com/find-up/-/find-up-5.0.0.tgz#4c92819ecb7083561e4f4a240a86be5198f536fc" @@ -3186,12 +2330,7 @@ foreground-child@^3.1.0: cross-spawn "^7.0.0" signal-exit "^4.0.1" -fs.realpath@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" - integrity sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw== - -fsevents@^2.3.2, fsevents@~2.3.2: +fsevents@~2.3.2: version "2.3.3" resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.3.tgz#cac6407785d03675a2a5e1a5305c697b347d90d6" integrity sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw== @@ -3201,11 +2340,6 @@ function-bind@^1.1.2: resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.2.tgz#2c02d864d97f3ea6c8830c464cbd11ab6eab7a1c" integrity sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA== -gensync@^1.0.0-beta.2: - version "1.0.0-beta.2" - resolved "https://registry.yarnpkg.com/gensync/-/gensync-1.0.0-beta.2.tgz#32a6ee76c3d7f52d46b2b1ae5d93fea8580a25e0" - integrity sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg== - get-caller-file@^2.0.5: version "2.0.5" resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-2.0.5.tgz#4f94412a82db32f36e3b0b9741f8a97feb031f7e" @@ -3230,16 +2364,6 @@ get-intrinsic@^1.2.4: has-symbols "^1.1.0" hasown "^2.0.2" -get-package-type@^0.1.0: - version "0.1.0" - resolved "https://registry.yarnpkg.com/get-package-type/-/get-package-type-0.1.0.tgz#8de2d803cff44df3bc6c456e6668b36c3926e11a" - integrity sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q== - -get-stream@^6.0.0: - version "6.0.1" - resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-6.0.1.tgz#a262d8eef67aced57c2852ad6167526a43cbf7b7" - integrity sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg== - glob-parent@^6.0.2: version "6.0.2" resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-6.0.2.tgz#6d237d99083950c79290f24c7642a3de9a28f9e3" @@ -3271,23 +2395,6 @@ glob@^10.4.5: package-json-from-dist "^1.0.0" path-scurry "^1.11.1" -glob@^7.1.3, glob@^7.1.4: - version "7.2.3" - resolved "https://registry.yarnpkg.com/glob/-/glob-7.2.3.tgz#b8df0fb802bbfa8e89bd1d938b4e16578ed44f2b" - integrity sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q== - dependencies: - fs.realpath "^1.0.0" - inflight "^1.0.4" - inherits "2" - minimatch "^3.1.1" - once "^1.3.0" - path-is-absolute "^1.0.0" - -globals@^11.1.0: - version "11.12.0" - resolved "https://registry.yarnpkg.com/globals/-/globals-11.12.0.tgz#ab8795338868a0babd8525758018c2a7eb95c42e" - integrity sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA== - globals@^14.0.0: version "14.0.0" resolved "https://registry.yarnpkg.com/globals/-/globals-14.0.0.tgz#898d7413c29babcf6bafe56fcadded858ada724e" @@ -3339,16 +2446,6 @@ he@^1.2.0: resolved "https://registry.yarnpkg.com/he/-/he-1.2.0.tgz#84ae65fa7eafb165fddb61566ae14baf05664f0f" integrity sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw== -html-escaper@^2.0.0: - version "2.0.2" - resolved "https://registry.yarnpkg.com/html-escaper/-/html-escaper-2.0.2.tgz#dfd60027da36a36dfcbe236262c00a5822681453" - integrity sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg== - -human-signals@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/human-signals/-/human-signals-2.1.0.tgz#dc91fcba42e4d06e4abaed33b3e7a3c02f514ea0" - integrity sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw== - ieee754@1.1.13: version "1.1.13" resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.1.13.tgz#ec168558e95aa181fd87d37f55c32bbcb6708b84" @@ -3372,28 +2469,12 @@ import-fresh@^3.2.1: parent-module "^1.0.0" resolve-from "^4.0.0" -import-local@^3.0.2: - version "3.2.0" - resolved "https://registry.yarnpkg.com/import-local/-/import-local-3.2.0.tgz#c3d5c745798c02a6f8b897726aba5100186ee260" - integrity sha512-2SPlun1JUPWoM6t3F0dw0FkCF/jWY8kttcY4f599GLTSjh2OCuuhdTkJQsEcZzBqbXZGKMK2OqW1oZsjtf/gQA== - dependencies: - pkg-dir "^4.2.0" - resolve-cwd "^3.0.0" - imurmurhash@^0.1.4: version "0.1.4" resolved "https://registry.yarnpkg.com/imurmurhash/-/imurmurhash-0.1.4.tgz#9218b9b2b928a238b13dc4fb6b6d576f231453ea" integrity sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA== -inflight@^1.0.4: - version "1.0.6" - resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9" - integrity sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA== - dependencies: - once "^1.3.0" - wrappy "1" - -inherits@2, inherits@^2.0.1, inherits@^2.0.3: +inherits@^2.0.1, inherits@^2.0.3: version "2.0.4" resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== @@ -3406,11 +2487,6 @@ is-arguments@^1.0.4: call-bind "^1.0.2" has-tostringtag "^1.0.0" -is-arrayish@^0.2.1: - version "0.2.1" - resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.2.1.tgz#77c99840527aa8ecb1a8ba697b80645a7a926a9d" - integrity sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg== - is-arrayish@^0.3.1: version "0.3.2" resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.3.2.tgz#4574a2ae56f7ab206896fb431eaeed066fdf8f03" @@ -3428,13 +2504,6 @@ is-callable@^1.1.3: resolved "https://registry.yarnpkg.com/is-callable/-/is-callable-1.2.7.tgz#3bc2a85ea742d9e36205dcacdd72ca1fdc51b055" integrity sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA== -is-core-module@^2.13.0: - version "2.15.1" - resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.15.1.tgz#a7363a25bee942fefab0de13bf6aa372c82dcc37" - integrity sha512-z0vtXSwucUJtANQWldhbtbt7BnL0vxiFjIdDLAatwhDYty2bad6s+rijD6Ri4YuYJubLzIJLUidCh09e1djEVQ== - dependencies: - hasown "^2.0.2" - is-extglob@^2.1.1: version "2.1.1" resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-2.1.1.tgz#a88c02535791f02ed37c76a1b9ea9773c833f8c2" @@ -3445,11 +2514,6 @@ is-fullwidth-code-point@^3.0.0: resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz#f116f8064fe90b3f7844a38997c0b75051269f1d" integrity sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg== -is-generator-fn@^2.0.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/is-generator-fn/-/is-generator-fn-2.1.0.tgz#7d140adc389aaf3011a8f2a2a4cfa6faadffb118" - integrity sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ== - is-generator-function@^1.0.7: version "1.0.10" resolved "https://registry.yarnpkg.com/is-generator-function/-/is-generator-function-1.0.10.tgz#f1558baf1ac17e0deea7c0415c438351ff2b3c72" @@ -3501,59 +2565,6 @@ isexe@^2.0.0: resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10" integrity sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw== -istanbul-lib-coverage@^3.0.0, istanbul-lib-coverage@^3.2.0: - version "3.2.2" - resolved "https://registry.yarnpkg.com/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz#2d166c4b0644d43a39f04bf6c2edd1e585f31756" - integrity sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg== - -istanbul-lib-instrument@^5.0.4: - version "5.2.1" - resolved "https://registry.yarnpkg.com/istanbul-lib-instrument/-/istanbul-lib-instrument-5.2.1.tgz#d10c8885c2125574e1c231cacadf955675e1ce3d" - integrity sha512-pzqtp31nLv/XFOzXGuvhCb8qhjmTVo5vjVk19XE4CRlSWz0KoeJ3bw9XsA7nOp9YBf4qHjwBxkDzKcME/J29Yg== - dependencies: - "@babel/core" "^7.12.3" - "@babel/parser" "^7.14.7" - "@istanbuljs/schema" "^0.1.2" - istanbul-lib-coverage "^3.2.0" - semver "^6.3.0" - -istanbul-lib-instrument@^6.0.0: - version "6.0.3" - resolved "https://registry.yarnpkg.com/istanbul-lib-instrument/-/istanbul-lib-instrument-6.0.3.tgz#fa15401df6c15874bcb2105f773325d78c666765" - integrity sha512-Vtgk7L/R2JHyyGW07spoFlB8/lpjiOLTjMdms6AFMraYt3BaJauod/NGrfnVG/y4Ix1JEuMRPDPEj2ua+zz1/Q== - dependencies: - "@babel/core" "^7.23.9" - "@babel/parser" "^7.23.9" - "@istanbuljs/schema" "^0.1.3" - istanbul-lib-coverage "^3.2.0" - semver "^7.5.4" - -istanbul-lib-report@^3.0.0: - version "3.0.1" - resolved "https://registry.yarnpkg.com/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz#908305bac9a5bd175ac6a74489eafd0fc2445a7d" - integrity sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw== - dependencies: - istanbul-lib-coverage "^3.0.0" - make-dir "^4.0.0" - supports-color "^7.1.0" - -istanbul-lib-source-maps@^4.0.0: - version "4.0.1" - resolved "https://registry.yarnpkg.com/istanbul-lib-source-maps/-/istanbul-lib-source-maps-4.0.1.tgz#895f3a709fcfba34c6de5a42939022f3e4358551" - integrity sha512-n3s8EwkdFIJCG3BPKBYvskgXGoy88ARzvegkitk60NxRdwltLOTaH7CUiMRXvwYorl0Q712iEjcWB+fK/MrWVw== - dependencies: - debug "^4.1.1" - istanbul-lib-coverage "^3.0.0" - source-map "^0.6.1" - -istanbul-reports@^3.1.3: - version "3.1.7" - resolved "https://registry.yarnpkg.com/istanbul-reports/-/istanbul-reports-3.1.7.tgz#daed12b9e1dca518e15c056e1e537e741280fa0b" - integrity sha512-BewmUXImeuRk2YY0PVbxgKAysvhRPUQE0h5QRM++nVWyubKGV0l8qQ5op8+B2DOmwSe63Jivj0BjkPQVf8fP5g== - dependencies: - html-escaper "^2.0.0" - istanbul-lib-report "^3.0.0" - jackspeak@^3.1.2: version "3.4.3" resolved "https://registry.yarnpkg.com/jackspeak/-/jackspeak-3.4.3.tgz#8833a9d89ab4acde6188942bd1c53b6390ed5a8a" @@ -3563,86 +2574,6 @@ jackspeak@^3.1.2: optionalDependencies: "@pkgjs/parseargs" "^0.11.0" -jest-changed-files@^29.7.0: - version "29.7.0" - resolved "https://registry.yarnpkg.com/jest-changed-files/-/jest-changed-files-29.7.0.tgz#1c06d07e77c78e1585d020424dedc10d6e17ac3a" - integrity sha512-fEArFiwf1BpQ+4bXSprcDc3/x4HSzL4al2tozwVpDFpsxALjLYdyiIK4e5Vz66GQJIbXJ82+35PtysofptNX2w== - dependencies: - execa "^5.0.0" - jest-util "^29.7.0" - p-limit "^3.1.0" - -jest-circus@^29.7.0: - version "29.7.0" - resolved "https://registry.yarnpkg.com/jest-circus/-/jest-circus-29.7.0.tgz#b6817a45fcc835d8b16d5962d0c026473ee3668a" - integrity sha512-3E1nCMgipcTkCocFwM90XXQab9bS+GMsjdpmPrlelaxwD93Ad8iVEjX/vvHPdLPnFf+L40u+5+iutRdA1N9myw== - dependencies: - "@jest/environment" "^29.7.0" - "@jest/expect" "^29.7.0" - "@jest/test-result" "^29.7.0" - "@jest/types" "^29.6.3" - "@types/node" "*" - chalk "^4.0.0" - co "^4.6.0" - dedent "^1.0.0" - is-generator-fn "^2.0.0" - jest-each "^29.7.0" - jest-matcher-utils "^29.7.0" - jest-message-util "^29.7.0" - jest-runtime "^29.7.0" - jest-snapshot "^29.7.0" - jest-util "^29.7.0" - p-limit "^3.1.0" - pretty-format "^29.7.0" - pure-rand "^6.0.0" - slash "^3.0.0" - stack-utils "^2.0.3" - -jest-cli@^29.7.0: - version "29.7.0" - resolved "https://registry.yarnpkg.com/jest-cli/-/jest-cli-29.7.0.tgz#5592c940798e0cae677eec169264f2d839a37995" - integrity sha512-OVVobw2IubN/GSYsxETi+gOe7Ka59EFMR/twOU3Jb2GnKKeMGJB5SGUUrEz3SFVmJASUdZUzy83sLNNQ2gZslg== - dependencies: - "@jest/core" "^29.7.0" - "@jest/test-result" "^29.7.0" - "@jest/types" "^29.6.3" - chalk "^4.0.0" - create-jest "^29.7.0" - exit "^0.1.2" - import-local "^3.0.2" - jest-config "^29.7.0" - jest-util "^29.7.0" - jest-validate "^29.7.0" - yargs "^17.3.1" - -jest-config@^29.7.0: - version "29.7.0" - resolved "https://registry.yarnpkg.com/jest-config/-/jest-config-29.7.0.tgz#bcbda8806dbcc01b1e316a46bb74085a84b0245f" - integrity sha512-uXbpfeQ7R6TZBqI3/TxCU4q4ttk3u0PJeC+E0zbfSoSjq6bJ7buBPxzQPL0ifrkY4DNu4JUdk0ImlBUYi840eQ== - dependencies: - "@babel/core" "^7.11.6" - "@jest/test-sequencer" "^29.7.0" - "@jest/types" "^29.6.3" - babel-jest "^29.7.0" - chalk "^4.0.0" - ci-info "^3.2.0" - deepmerge "^4.2.2" - glob "^7.1.3" - graceful-fs "^4.2.9" - jest-circus "^29.7.0" - jest-environment-node "^29.7.0" - jest-get-type "^29.6.3" - jest-regex-util "^29.6.3" - jest-resolve "^29.7.0" - jest-runner "^29.7.0" - jest-util "^29.7.0" - jest-validate "^29.7.0" - micromatch "^4.0.4" - parse-json "^5.2.0" - pretty-format "^29.7.0" - slash "^3.0.0" - strip-json-comments "^3.1.1" - jest-diff@^29.7.0: version "29.7.0" resolved "https://registry.yarnpkg.com/jest-diff/-/jest-diff-29.7.0.tgz#017934a66ebb7ecf6f205e84699be10afd70458a" @@ -3653,68 +2584,11 @@ jest-diff@^29.7.0: jest-get-type "^29.6.3" pretty-format "^29.7.0" -jest-docblock@^29.7.0: - version "29.7.0" - resolved "https://registry.yarnpkg.com/jest-docblock/-/jest-docblock-29.7.0.tgz#8fddb6adc3cdc955c93e2a87f61cfd350d5d119a" - integrity sha512-q617Auw3A612guyaFgsbFeYpNP5t2aoUNLwBUbc/0kD1R4t9ixDbyFTHd1nok4epoVFpr7PmeWHrhvuV3XaJ4g== - dependencies: - detect-newline "^3.0.0" - -jest-each@^29.7.0: - version "29.7.0" - resolved "https://registry.yarnpkg.com/jest-each/-/jest-each-29.7.0.tgz#162a9b3f2328bdd991beaabffbb74745e56577d1" - integrity sha512-gns+Er14+ZrEoC5fhOfYCY1LOHHr0TI+rQUHZS8Ttw2l7gl+80eHc/gFf2Ktkw0+SIACDTeWvpFcv3B04VembQ== - dependencies: - "@jest/types" "^29.6.3" - chalk "^4.0.0" - jest-get-type "^29.6.3" - jest-util "^29.7.0" - pretty-format "^29.7.0" - -jest-environment-node@^29.7.0: - version "29.7.0" - resolved "https://registry.yarnpkg.com/jest-environment-node/-/jest-environment-node-29.7.0.tgz#0b93e111dda8ec120bc8300e6d1fb9576e164376" - integrity sha512-DOSwCRqXirTOyheM+4d5YZOrWcdu0LNZ87ewUoywbcb2XR4wKgqiG8vNeYwhjFMbEkfju7wx2GYH0P2gevGvFw== - dependencies: - "@jest/environment" "^29.7.0" - "@jest/fake-timers" "^29.7.0" - "@jest/types" "^29.6.3" - "@types/node" "*" - jest-mock "^29.7.0" - jest-util "^29.7.0" - jest-get-type@^29.6.3: version "29.6.3" resolved "https://registry.yarnpkg.com/jest-get-type/-/jest-get-type-29.6.3.tgz#36f499fdcea197c1045a127319c0481723908fd1" integrity sha512-zrteXnqYxfQh7l5FHyL38jL39di8H8rHoecLH3JNxH3BwOrBsNeabdap5e0I23lD4HHI8W5VFBZqG4Eaq5LNcw== -jest-haste-map@^29.7.0: - version "29.7.0" - resolved "https://registry.yarnpkg.com/jest-haste-map/-/jest-haste-map-29.7.0.tgz#3c2396524482f5a0506376e6c858c3bbcc17b104" - integrity sha512-fP8u2pyfqx0K1rGn1R9pyE0/KTn+G7PxktWidOBTqFPLYX0b9ksaMFkhK5vrS3DVun09pckLdlx90QthlW7AmA== - dependencies: - "@jest/types" "^29.6.3" - "@types/graceful-fs" "^4.1.3" - "@types/node" "*" - anymatch "^3.0.3" - fb-watchman "^2.0.0" - graceful-fs "^4.2.9" - jest-regex-util "^29.6.3" - jest-util "^29.7.0" - jest-worker "^29.7.0" - micromatch "^4.0.4" - walker "^1.0.8" - optionalDependencies: - fsevents "^2.3.2" - -jest-leak-detector@^29.7.0: - version "29.7.0" - resolved "https://registry.yarnpkg.com/jest-leak-detector/-/jest-leak-detector-29.7.0.tgz#5b7ec0dadfdfec0ca383dc9aa016d36b5ea4c728" - integrity sha512-kYA8IJcSYtST2BY9I+SMC32nDpBT3J2NvWJx8+JCuCdl/CR1I4EKUJROiP8XtCcxqgTTBGJNdbB1A8XRKbTetw== - dependencies: - jest-get-type "^29.6.3" - pretty-format "^29.7.0" - jest-matcher-utils@^29.7.0: version "29.7.0" resolved "https://registry.yarnpkg.com/jest-matcher-utils/-/jest-matcher-utils-29.7.0.tgz#ae8fec79ff249fd592ce80e3ee474e83a6c44f12" @@ -3740,129 +2614,6 @@ jest-message-util@^29.7.0: slash "^3.0.0" stack-utils "^2.0.3" -jest-mock@^29.7.0: - version "29.7.0" - resolved "https://registry.yarnpkg.com/jest-mock/-/jest-mock-29.7.0.tgz#4e836cf60e99c6fcfabe9f99d017f3fdd50a6347" - integrity sha512-ITOMZn+UkYS4ZFh83xYAOzWStloNzJFO2s8DWrE4lhtGD+AorgnbkiKERe4wQVBydIGPx059g6riW5Btp6Llnw== - dependencies: - "@jest/types" "^29.6.3" - "@types/node" "*" - jest-util "^29.7.0" - -jest-pnp-resolver@^1.2.2: - version "1.2.3" - resolved "https://registry.yarnpkg.com/jest-pnp-resolver/-/jest-pnp-resolver-1.2.3.tgz#930b1546164d4ad5937d5540e711d4d38d4cad2e" - integrity sha512-+3NpwQEnRoIBtx4fyhblQDPgJI0H1IEIkX7ShLUjPGA7TtUTvI1oiKi3SR4oBR0hQhQR80l4WAe5RrXBwWMA8w== - -jest-regex-util@^29.6.3: - version "29.6.3" - resolved "https://registry.yarnpkg.com/jest-regex-util/-/jest-regex-util-29.6.3.tgz#4a556d9c776af68e1c5f48194f4d0327d24e8a52" - integrity sha512-KJJBsRCyyLNWCNBOvZyRDnAIfUiRJ8v+hOBQYGn8gDyF3UegwiP4gwRR3/SDa42g1YbVycTidUF3rKjyLFDWbg== - -jest-resolve-dependencies@^29.7.0: - version "29.7.0" - resolved "https://registry.yarnpkg.com/jest-resolve-dependencies/-/jest-resolve-dependencies-29.7.0.tgz#1b04f2c095f37fc776ff40803dc92921b1e88428" - integrity sha512-un0zD/6qxJ+S0et7WxeI3H5XSe9lTBBR7bOHCHXkKR6luG5mwDDlIzVQ0V5cZCuoTgEdcdwzTghYkTWfubi+nA== - dependencies: - jest-regex-util "^29.6.3" - jest-snapshot "^29.7.0" - -jest-resolve@^29.7.0: - version "29.7.0" - resolved "https://registry.yarnpkg.com/jest-resolve/-/jest-resolve-29.7.0.tgz#64d6a8992dd26f635ab0c01e5eef4399c6bcbc30" - integrity sha512-IOVhZSrg+UvVAshDSDtHyFCCBUl/Q3AAJv8iZ6ZjnZ74xzvwuzLXid9IIIPgTnY62SJjfuupMKZsZQRsCvxEgA== - dependencies: - chalk "^4.0.0" - graceful-fs "^4.2.9" - jest-haste-map "^29.7.0" - jest-pnp-resolver "^1.2.2" - jest-util "^29.7.0" - jest-validate "^29.7.0" - resolve "^1.20.0" - resolve.exports "^2.0.0" - slash "^3.0.0" - -jest-runner@^29.7.0: - version "29.7.0" - resolved "https://registry.yarnpkg.com/jest-runner/-/jest-runner-29.7.0.tgz#809af072d408a53dcfd2e849a4c976d3132f718e" - integrity sha512-fsc4N6cPCAahybGBfTRcq5wFR6fpLznMg47sY5aDpsoejOcVYFb07AHuSnR0liMcPTgBsA3ZJL6kFOjPdoNipQ== - dependencies: - "@jest/console" "^29.7.0" - "@jest/environment" "^29.7.0" - "@jest/test-result" "^29.7.0" - "@jest/transform" "^29.7.0" - "@jest/types" "^29.6.3" - "@types/node" "*" - chalk "^4.0.0" - emittery "^0.13.1" - graceful-fs "^4.2.9" - jest-docblock "^29.7.0" - jest-environment-node "^29.7.0" - jest-haste-map "^29.7.0" - jest-leak-detector "^29.7.0" - jest-message-util "^29.7.0" - jest-resolve "^29.7.0" - jest-runtime "^29.7.0" - jest-util "^29.7.0" - jest-watcher "^29.7.0" - jest-worker "^29.7.0" - p-limit "^3.1.0" - source-map-support "0.5.13" - -jest-runtime@^29.7.0: - version "29.7.0" - resolved "https://registry.yarnpkg.com/jest-runtime/-/jest-runtime-29.7.0.tgz#efecb3141cf7d3767a3a0cc8f7c9990587d3d817" - integrity sha512-gUnLjgwdGqW7B4LvOIkbKs9WGbn+QLqRQQ9juC6HndeDiezIwhDP+mhMwHWCEcfQ5RUXa6OPnFF8BJh5xegwwQ== - dependencies: - "@jest/environment" "^29.7.0" - "@jest/fake-timers" "^29.7.0" - "@jest/globals" "^29.7.0" - "@jest/source-map" "^29.6.3" - "@jest/test-result" "^29.7.0" - "@jest/transform" "^29.7.0" - "@jest/types" "^29.6.3" - "@types/node" "*" - chalk "^4.0.0" - cjs-module-lexer "^1.0.0" - collect-v8-coverage "^1.0.0" - glob "^7.1.3" - graceful-fs "^4.2.9" - jest-haste-map "^29.7.0" - jest-message-util "^29.7.0" - jest-mock "^29.7.0" - jest-regex-util "^29.6.3" - jest-resolve "^29.7.0" - jest-snapshot "^29.7.0" - jest-util "^29.7.0" - slash "^3.0.0" - strip-bom "^4.0.0" - -jest-snapshot@^29.7.0: - version "29.7.0" - resolved "https://registry.yarnpkg.com/jest-snapshot/-/jest-snapshot-29.7.0.tgz#c2c574c3f51865da1bb329036778a69bf88a6be5" - integrity sha512-Rm0BMWtxBcioHr1/OX5YCP8Uov4riHvKPknOGs804Zg9JGZgmIBkbtlxJC/7Z4msKYVbIJtfU+tKb8xlYNfdkw== - dependencies: - "@babel/core" "^7.11.6" - "@babel/generator" "^7.7.2" - "@babel/plugin-syntax-jsx" "^7.7.2" - "@babel/plugin-syntax-typescript" "^7.7.2" - "@babel/types" "^7.3.3" - "@jest/expect-utils" "^29.7.0" - "@jest/transform" "^29.7.0" - "@jest/types" "^29.6.3" - babel-preset-current-node-syntax "^1.0.0" - chalk "^4.0.0" - expect "^29.7.0" - graceful-fs "^4.2.9" - jest-diff "^29.7.0" - jest-get-type "^29.6.3" - jest-matcher-utils "^29.7.0" - jest-message-util "^29.7.0" - jest-util "^29.7.0" - natural-compare "^1.4.0" - pretty-format "^29.7.0" - semver "^7.5.3" - jest-util@^29.7.0: version "29.7.0" resolved "https://registry.yarnpkg.com/jest-util/-/jest-util-29.7.0.tgz#23c2b62bfb22be82b44de98055802ff3710fc0bc" @@ -3875,63 +2626,17 @@ jest-util@^29.7.0: graceful-fs "^4.2.9" picomatch "^2.2.3" -jest-validate@^29.7.0: - version "29.7.0" - resolved "https://registry.yarnpkg.com/jest-validate/-/jest-validate-29.7.0.tgz#7bf705511c64da591d46b15fce41400d52147d9c" - integrity sha512-ZB7wHqaRGVw/9hST/OuFUReG7M8vKeq0/J2egIGLdvjHCmYqGARhzXmtgi+gVeZ5uXFF219aOc3Ls2yLg27tkw== - dependencies: - "@jest/types" "^29.6.3" - camelcase "^6.2.0" - chalk "^4.0.0" - jest-get-type "^29.6.3" - leven "^3.1.0" - pretty-format "^29.7.0" - -jest-watcher@^29.7.0: - version "29.7.0" - resolved "https://registry.yarnpkg.com/jest-watcher/-/jest-watcher-29.7.0.tgz#7810d30d619c3a62093223ce6bb359ca1b28a2f2" - integrity sha512-49Fg7WXkU3Vl2h6LbLtMQ/HyB6rXSIX7SqvBLQmssRBGN9I0PNvPmAmCWSOY6SOvrjhI/F7/bGAv9RtnsPA03g== - dependencies: - "@jest/test-result" "^29.7.0" - "@jest/types" "^29.6.3" - "@types/node" "*" - ansi-escapes "^4.2.1" - chalk "^4.0.0" - emittery "^0.13.1" - jest-util "^29.7.0" - string-length "^4.0.1" - -jest-worker@^29.7.0: - version "29.7.0" - resolved "https://registry.yarnpkg.com/jest-worker/-/jest-worker-29.7.0.tgz#acad073acbbaeb7262bd5389e1bcf43e10058d4a" - integrity sha512-eIz2msL/EzL9UFTFFx7jBTkeZfku0yUAyZZZmJ93H2TYEiroIx2PQjEXcwYtYl8zXCxb+PAmA2hLIt/6ZEkPHw== - dependencies: - "@types/node" "*" - jest-util "^29.7.0" - merge-stream "^2.0.0" - supports-color "^8.0.0" - -jest@^29.7.0: - version "29.7.0" - resolved "https://registry.yarnpkg.com/jest/-/jest-29.7.0.tgz#994676fc24177f088f1c5e3737f5697204ff2613" - integrity sha512-NIy3oAFp9shda19hy4HK0HRTWKtPJmGdnvywu01nOqNC2vZg+Z+fvJDxpMQA88eb2I9EcafcdjYgsDthnYTvGw== - dependencies: - "@jest/core" "^29.7.0" - "@jest/types" "^29.6.3" - import-local "^3.0.2" - jest-cli "^29.7.0" - jmespath@0.16.0: version "0.16.0" resolved "https://registry.yarnpkg.com/jmespath/-/jmespath-0.16.0.tgz#b15b0a85dfd4d930d43e69ed605943c802785076" integrity sha512-9FzQjJ7MATs1tSpnco1K6ayiYE3figslrXA72G2HQ/n76RzvYlofyi5QM+iX4YRs/pu3yzxlVQSST23+dMDknw== -"js-tokens@^3.0.0 || ^4.0.0", js-tokens@^4.0.0: +js-tokens@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499" integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ== -js-yaml@^3.13.1, js-yaml@^3.14.1: +js-yaml@^3.14.1: version "3.14.1" resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.14.1.tgz#dae812fdb3825fa306609a8717383c50c36a0537" integrity sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g== @@ -3946,21 +2651,11 @@ js-yaml@^4.1.0: dependencies: argparse "^2.0.1" -jsesc@^3.0.2: - version "3.0.2" - resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-3.0.2.tgz#bb8b09a6597ba426425f2e4a07245c3d00b9343e" - integrity sha512-xKqzzWXDttJuOcawBt4KnKHHIf5oQ/Cxax+0PWFG+DFDgHNAdi+TXECADI+RYiFUMmx8792xsMbbgXj4CwnP4g== - json-buffer@3.0.1: version "3.0.1" resolved "https://registry.yarnpkg.com/json-buffer/-/json-buffer-3.0.1.tgz#9338802a30d3b6605fbe0613e094008ca8c05a13" integrity sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ== -json-parse-even-better-errors@^2.3.0: - version "2.3.1" - resolved "https://registry.yarnpkg.com/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz#7c47805a94319928e05777405dc12e1f7a4ee02d" - integrity sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w== - json-schema-traverse@^0.4.1: version "0.4.1" resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz#69f6a87d9513ab8bb8fe63bdb0979c448e684660" @@ -3971,11 +2666,6 @@ json-stable-stringify-without-jsonify@^1.0.1: resolved "https://registry.yarnpkg.com/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz#9db7b59496ad3f3cfef30a75142d2d930ad72651" integrity sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw== -json5@^2.2.3: - version "2.2.3" - resolved "https://registry.yarnpkg.com/json5/-/json5-2.2.3.tgz#78cd6f1a19bdc12b73db5ad0c61efd66c1e29283" - integrity sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg== - just-extend@^6.2.0: version "6.2.0" resolved "https://registry.yarnpkg.com/just-extend/-/just-extend-6.2.0.tgz#b816abfb3d67ee860482e7401564672558163947" @@ -3988,11 +2678,6 @@ keyv@^4.5.4: dependencies: json-buffer "3.0.1" -kleur@^3.0.3: - version "3.0.3" - resolved "https://registry.yarnpkg.com/kleur/-/kleur-3.0.3.tgz#a79c9ecc86ee1ce3fa6206d1216c501f147fc07e" - integrity sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w== - kuler@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/kuler/-/kuler-2.0.0.tgz#e2c570a3800388fb44407e851531c1d670b061b3" @@ -4007,11 +2692,6 @@ lambda-local@^2.2.0: dotenv "^16.3.1" winston "^3.10.0" -leven@^3.1.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/leven/-/leven-3.1.0.tgz#77891de834064cccba82ae7842bb6b14a13ed7f2" - integrity sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A== - levn@^0.4.1: version "0.4.1" resolved "https://registry.yarnpkg.com/levn/-/levn-0.4.1.tgz#ae4562c007473b932a6200d403268dd2fffc6ade" @@ -4020,18 +2700,6 @@ levn@^0.4.1: prelude-ls "^1.2.1" type-check "~0.4.0" -lines-and-columns@^1.1.6: - version "1.2.4" - resolved "https://registry.yarnpkg.com/lines-and-columns/-/lines-and-columns-1.2.4.tgz#eca284f75d2965079309dc0ad9255abb2ebc1632" - integrity sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg== - -locate-path@^5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-5.0.0.tgz#1afba396afd676a6d42504d0a67a3a7eb9f62aa0" - integrity sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g== - dependencies: - p-locate "^4.1.0" - locate-path@^6.0.0: version "6.0.0" resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-6.0.0.tgz#55321eb309febbc59c4801d931a72452a681d286" @@ -4091,13 +2759,6 @@ logform@^2.7.0: safe-stable-stringify "^2.3.1" triple-beam "^1.3.0" -loose-envify@^1.1.0: - version "1.4.0" - resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.4.0.tgz#71ee51fa7be4caec1a63839f7e682d8132d30caf" - integrity sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q== - dependencies: - js-tokens "^3.0.0 || ^4.0.0" - loupe@^2.3.6: version "2.3.7" resolved "https://registry.yarnpkg.com/loupe/-/loupe-2.3.7.tgz#6e69b7d4db7d3ab436328013d37d1c8c3540c697" @@ -4115,39 +2776,6 @@ lru-cache@^10.2.0: resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-10.4.3.tgz#410fc8a17b70e598013df257c2446b7f3383f119" integrity sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ== -lru-cache@^5.1.1: - version "5.1.1" - resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-5.1.1.tgz#1da27e6710271947695daf6848e847f01d84b920" - integrity sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w== - dependencies: - yallist "^3.0.2" - -make-dir@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-4.0.0.tgz#c3c2307a771277cd9638305f915c29ae741b614e" - integrity sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw== - dependencies: - semver "^7.5.3" - -makeerror@1.0.12: - version "1.0.12" - resolved "https://registry.yarnpkg.com/makeerror/-/makeerror-1.0.12.tgz#3e5dd2079a82e812e983cc6610c4a2cb0eaa801a" - integrity sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg== - dependencies: - tmpl "1.0.5" - -markdown-parser-react@^1.1.2: - version "1.1.2" - resolved "https://registry.yarnpkg.com/markdown-parser-react/-/markdown-parser-react-1.1.2.tgz#5817a708ea1edc33579f436346cba866d58d4792" - integrity sha512-MNLHekU1xOwKZLJK4NMWJDL9pNnJdKx2jdsHfAF4+Y5rF4tD/S/OuNehd4X46/KcJzBfas19pePVcwQoibpeNg== - dependencies: - react "^18.2.0" - -merge-stream@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/merge-stream/-/merge-stream-2.0.0.tgz#52823629a14dd00c9770fb6ad47dc6310f2c1f60" - integrity sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w== - micromatch@^4.0.4: version "4.0.8" resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.8.tgz#d66fa18f3a47076789320b9b1af32bd86d9fa202" @@ -4156,12 +2784,7 @@ micromatch@^4.0.4: braces "^3.0.3" picomatch "^2.3.1" -mimic-fn@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-2.1.0.tgz#7ed2c2ccccaf84d3ffcb7a69b57711fc2083401b" - integrity sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg== - -minimatch@^3.0.4, minimatch@^3.1.1, minimatch@^3.1.2: +minimatch@^3.1.2: version "3.1.2" resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.1.2.tgz#19cd194bfd3e428f049a70817c038d89ab4be35b" integrity sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw== @@ -4241,45 +2864,16 @@ nise@^6.0.0: just-extend "^6.2.0" path-to-regexp "^8.1.0" -node-int64@^0.4.0: - version "0.4.0" - resolved "https://registry.yarnpkg.com/node-int64/-/node-int64-0.4.0.tgz#87a9065cdb355d3182d8f94ce11188b825c68a3b" - integrity sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw== - -node-releases@^2.0.18: - version "2.0.18" - resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.18.tgz#f010e8d35e2fe8d6b2944f03f70213ecedc4ca3f" - integrity sha512-d9VeXT4SJ7ZeOqGX6R5EM022wpL+eWPooLI+5UpWn2jCT1aosUQEhQP214x33Wkwx3JQMvIm+tIoVOdodFS40g== - -nodemailer@^6.9.12: - version "6.9.16" - resolved "https://registry.yarnpkg.com/nodemailer/-/nodemailer-6.9.16.tgz#3ebdf6c6f477c571c0facb0727b33892635e0b8b" - integrity sha512-psAuZdTIRN08HKVd/E8ObdV6NO7NTBY3KsC30F7M4H1OnmLCUNaS56FpYxyb26zWLSyYF9Ozch9KYHhHegsiOQ== - normalize-path@^3.0.0, normalize-path@~3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-3.0.0.tgz#0dcd69ff23a1c9b11fd0978316644a0388216a65" integrity sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA== -npm-run-path@^4.0.1: - version "4.0.1" - resolved "https://registry.yarnpkg.com/npm-run-path/-/npm-run-path-4.0.1.tgz#b7ecd1e5ed53da8e37a55e1c2269e0b97ed748ea" - integrity sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw== - dependencies: - path-key "^3.0.0" - obliterator@^1.6.1: version "1.6.1" resolved "https://registry.yarnpkg.com/obliterator/-/obliterator-1.6.1.tgz#dea03e8ab821f6c4d96a299e17aef6a3af994ef3" integrity sha512-9WXswnqINnnhOG/5SLimUlzuU1hFJUc8zkwyD59Sd+dPOMf05PmnYG/d6Q7HZ+KmgkZJa1PxRso6QdM3sTNHig== -once@^1.3.0: - version "1.4.0" - resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" - integrity sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w== - dependencies: - wrappy "1" - one-time@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/one-time/-/one-time-1.0.0.tgz#e06bc174aed214ed58edede573b433bbf827cb45" @@ -4287,13 +2881,6 @@ one-time@^1.0.0: dependencies: fn.name "1.x.x" -onetime@^5.1.2: - version "5.1.2" - resolved "https://registry.yarnpkg.com/onetime/-/onetime-5.1.2.tgz#d0e96ebb56b07476df1dd9c4806e5237985ca45e" - integrity sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg== - dependencies: - mimic-fn "^2.1.0" - optionator@^0.9.3: version "0.9.4" resolved "https://registry.yarnpkg.com/optionator/-/optionator-0.9.4.tgz#7ea1c1a5d91d764fb282139c88fe11e182a3a734" @@ -4306,27 +2893,13 @@ optionator@^0.9.3: type-check "^0.4.0" word-wrap "^1.2.5" -p-limit@^2.2.0: - version "2.3.0" - resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-2.3.0.tgz#3dd33c647a214fdfffd835933eb086da0dc21db1" - integrity sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w== - dependencies: - p-try "^2.0.0" - -p-limit@^3.0.2, p-limit@^3.1.0: +p-limit@^3.0.2: version "3.1.0" resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-3.1.0.tgz#e1daccbe78d0d1388ca18c64fea38e3e57e3706b" integrity sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ== dependencies: yocto-queue "^0.1.0" -p-locate@^4.1.0: - version "4.1.0" - resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-4.1.0.tgz#a3428bb7088b3a60292f66919278b7c297ad4f07" - integrity sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A== - dependencies: - p-limit "^2.2.0" - p-locate@^5.0.0: version "5.0.0" resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-5.0.0.tgz#83c8315c6785005e3bd021839411c9e110e6d834" @@ -4334,11 +2907,6 @@ p-locate@^5.0.0: dependencies: p-limit "^3.0.2" -p-try@^2.0.0: - version "2.2.0" - resolved "https://registry.yarnpkg.com/p-try/-/p-try-2.2.0.tgz#cb2868540e313d61de58fafbe35ce9004d5540e6" - integrity sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ== - package-json-from-dist@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz#4f1471a010827a86f94cfd9b0727e36d267de505" @@ -4351,36 +2919,16 @@ parent-module@^1.0.0: dependencies: callsites "^3.0.0" -parse-json@^5.2.0: - version "5.2.0" - resolved "https://registry.yarnpkg.com/parse-json/-/parse-json-5.2.0.tgz#c76fc66dee54231c962b22bcc8a72cf2f99753cd" - integrity sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg== - dependencies: - "@babel/code-frame" "^7.0.0" - error-ex "^1.3.1" - json-parse-even-better-errors "^2.3.0" - lines-and-columns "^1.1.6" - path-exists@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-4.0.0.tgz#513bdbe2d3b95d7762e8c1137efa195c6c61b5b3" integrity sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w== -path-is-absolute@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f" - integrity sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg== - -path-key@^3.0.0, path-key@^3.1.0: +path-key@^3.1.0: version "3.1.1" resolved "https://registry.yarnpkg.com/path-key/-/path-key-3.1.1.tgz#581f6ade658cbba65a0d3380de7753295054f375" integrity sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q== -path-parse@^1.0.7: - version "1.0.7" - resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.7.tgz#fbc114b60ca42b30d9daf5858e4bd68bbedb6735" - integrity sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw== - path-scurry@^1.11.1: version "1.11.1" resolved "https://registry.yarnpkg.com/path-scurry/-/path-scurry-1.11.1.tgz#7960a668888594a0720b12a911d1a742ab9f11d2" @@ -4404,7 +2952,7 @@ pathval@^2.0.0: resolved "https://registry.yarnpkg.com/pathval/-/pathval-2.0.0.tgz#7e2550b422601d4f6b8e26f1301bc8f15a741a25" integrity sha512-vE7JKRyES09KiunauX7nd2Q9/L7lhok4smP9RZTDeD4MVs72Dp2qNFVz39Nz5a0FVEW0BJR6C0DYrq6unoziZA== -picocolors@^1.0.0, picocolors@^1.1.0: +picocolors@^1.0.0: version "1.1.1" resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.1.1.tgz#3d321af3eab939b083c8f929a1d12cda81c26b6b" integrity sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA== @@ -4414,18 +2962,6 @@ picomatch@^2.0.4, picomatch@^2.2.1, picomatch@^2.2.3, picomatch@^2.3.1: resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.1.tgz#3ba3833733646d9d3e4995946c1365a67fb07a42" integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA== -pirates@^4.0.4: - version "4.0.6" - resolved "https://registry.yarnpkg.com/pirates/-/pirates-4.0.6.tgz#3018ae32ecfcff6c29ba2267cbf21166ac1f36b9" - integrity sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg== - -pkg-dir@^4.2.0: - version "4.2.0" - resolved "https://registry.yarnpkg.com/pkg-dir/-/pkg-dir-4.2.0.tgz#f099133df7ede422e81d1d8448270eeb3e4261f3" - integrity sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ== - dependencies: - find-up "^4.0.0" - possible-typed-array-names@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/possible-typed-array-names/-/possible-typed-array-names-1.0.0.tgz#89bb63c6fada2c3e90adc4a647beeeb39cc7bf8f" @@ -4445,14 +2981,6 @@ pretty-format@^29.7.0: ansi-styles "^5.0.0" react-is "^18.0.0" -prompts@^2.0.1: - version "2.4.2" - resolved "https://registry.yarnpkg.com/prompts/-/prompts-2.4.2.tgz#7b57e73b3a48029ad10ebd44f74b01722a4cb069" - integrity sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q== - dependencies: - kleur "^3.0.3" - sisteransi "^1.0.5" - punycode@1.3.2: version "1.3.2" resolved "https://registry.yarnpkg.com/punycode/-/punycode-1.3.2.tgz#9653a036fb7c1ee42342f2325cceefea3926c48d" @@ -4463,11 +2991,6 @@ punycode@^2.1.0: resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.3.1.tgz#027422e2faec0b25e1549c3e1bd8309b9133b6e5" integrity sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg== -pure-rand@^6.0.0: - version "6.1.0" - resolved "https://registry.yarnpkg.com/pure-rand/-/pure-rand-6.1.0.tgz#d173cf23258231976ccbdb05247c9787957604f2" - integrity sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA== - querystring@0.2.0: version "0.2.0" resolved "https://registry.yarnpkg.com/querystring/-/querystring-0.2.0.tgz#b209849203bb25df820da756e747005878521620" @@ -4485,13 +3008,6 @@ react-is@^18.0.0: resolved "https://registry.yarnpkg.com/react-is/-/react-is-18.3.1.tgz#e83557dc12eae63a99e003a46388b1dcbb44db7e" integrity sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg== -react@^18.2.0: - version "18.3.1" - resolved "https://registry.yarnpkg.com/react/-/react-18.3.1.tgz#49ab892009c53933625bd16b2533fc754cab2891" - integrity sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ== - dependencies: - loose-envify "^1.1.0" - readable-stream@^3.4.0, readable-stream@^3.6.2: version "3.6.2" resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.6.2.tgz#56a9b36ea965c00c5a93ef31eb111a0f11056967" @@ -4513,37 +3029,11 @@ require-directory@^2.1.1: resolved "https://registry.yarnpkg.com/require-directory/-/require-directory-2.1.1.tgz#8c64ad5fd30dab1c976e2344ffe7f792a6a6df42" integrity sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q== -resolve-cwd@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/resolve-cwd/-/resolve-cwd-3.0.0.tgz#0f0075f1bb2544766cf73ba6a6e2adfebcb13f2d" - integrity sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg== - dependencies: - resolve-from "^5.0.0" - resolve-from@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-4.0.0.tgz#4abcd852ad32dd7baabfe9b40e00a36db5f392e6" integrity sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g== -resolve-from@^5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-5.0.0.tgz#c35225843df8f776df21c57557bc087e9dfdfc69" - integrity sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw== - -resolve.exports@^2.0.0: - version "2.0.3" - resolved "https://registry.yarnpkg.com/resolve.exports/-/resolve.exports-2.0.3.tgz#41955e6f1b4013b7586f873749a635dea07ebe3f" - integrity sha512-OcXjMsGdhL4XnbShKpAcSqPMzQoYkYyhbEaeSko47MjRP9NfEQMhZkXL1DoFlt9LWQn4YttrdnV6X2OiyzBi+A== - -resolve@^1.20.0: - version "1.22.8" - resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.22.8.tgz#b6c87a9f2aa06dfab52e3d70ac8cde321fa5a48d" - integrity sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw== - dependencies: - is-core-module "^2.13.0" - path-parse "^1.0.7" - supports-preserve-symlinks-flag "^1.0.0" - safe-buffer@^5.1.0, safe-buffer@~5.2.0: version "5.2.1" resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6" @@ -4564,16 +3054,6 @@ sax@>=0.6.0: resolved "https://registry.yarnpkg.com/sax/-/sax-1.4.1.tgz#44cc8988377f126304d3b3fc1010c733b929ef0f" integrity sha512-+aWOz7yVScEGoKNd4PA10LZ8sk0A/z5+nXQG5giUO5rprX9jgYsTdov9qCchZiPIZezbZH+jRut8nPodFAX4Jg== -semver@^6.3.0, semver@^6.3.1: - version "6.3.1" - resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.1.tgz#556d2ef8689146e46dcea4bfdd095f3434dffcb4" - integrity sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA== - -semver@^7.5.3, semver@^7.5.4: - version "7.6.3" - resolved "https://registry.yarnpkg.com/semver/-/semver-7.6.3.tgz#980f7b5550bc175fb4dc09403085627f9eb33143" - integrity sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A== - serialize-javascript@^6.0.2: version "6.0.2" resolved "https://registry.yarnpkg.com/serialize-javascript/-/serialize-javascript-6.0.2.tgz#defa1e055c83bf6d59ea805d8da862254eb6a6c2" @@ -4605,11 +3085,6 @@ shebang-regex@^3.0.0: resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-3.0.0.tgz#ae16f1644d873ecad843b0307b143362d4c42172" integrity sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A== -signal-exit@^3.0.3, signal-exit@^3.0.7: - version "3.0.7" - resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.7.tgz#a9a1767f8af84155114eaabd73f99273c8f59ad9" - integrity sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ== - signal-exit@^4.0.1: version "4.1.0" resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-4.1.0.tgz#952188c1cbd546070e2dd20d0f41c0ae0530cb04" @@ -4634,29 +3109,11 @@ sinon@^18.0.1: nise "^6.0.0" supports-color "^7" -sisteransi@^1.0.5: - version "1.0.5" - resolved "https://registry.yarnpkg.com/sisteransi/-/sisteransi-1.0.5.tgz#134d681297756437cc05ca01370d3a7a571075ed" - integrity sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg== - slash@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/slash/-/slash-3.0.0.tgz#6539be870c165adbd5240220dbe361f1bc4d4634" integrity sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q== -source-map-support@0.5.13: - version "0.5.13" - resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.13.tgz#31b24a9c2e73c2de85066c0feb7d44767ed52932" - integrity sha512-SHSKFHadjVA5oR4PPqhtAVdcBWwRYVd6g6cAXnIbRiIwc2EhPrTuKUBdSLvlEKyIP3GCf89fltvcZiP9MMFA1w== - dependencies: - buffer-from "^1.0.0" - source-map "^0.6.0" - -source-map@^0.6.0, source-map@^0.6.1: - version "0.6.1" - resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263" - integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g== - sprintf-js@~1.0.2: version "1.0.3" resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.0.3.tgz#04e6926f662895354f3dd015203633b857297e2c" @@ -4674,14 +3131,6 @@ stack-utils@^2.0.3: dependencies: escape-string-regexp "^2.0.0" -string-length@^4.0.1: - version "4.0.2" - resolved "https://registry.yarnpkg.com/string-length/-/string-length-4.0.2.tgz#a8a8dc7bd5c1a82b9b3c8b87e125f66871b6e57a" - integrity sha512-+l6rNN5fYHNhZZy41RXsYptCjA2Igmq4EG7kZAYFQI1E1VTXarr6ZPXBg6eq7Y6eK4FEhY6AJlyuFIb/v/S0VQ== - dependencies: - char-regex "^1.0.2" - strip-ansi "^6.0.0" - "string-width-cjs@npm:string-width@^4.2.0": version "4.2.3" resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" @@ -4691,7 +3140,7 @@ string-length@^4.0.1: is-fullwidth-code-point "^3.0.0" strip-ansi "^6.0.1" -string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: +string-width@^4.1.0, string-width@^4.2.0: version "4.2.3" resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== @@ -4737,16 +3186,6 @@ strip-ansi@^7.0.1: dependencies: ansi-regex "^6.0.1" -strip-bom@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/strip-bom/-/strip-bom-4.0.0.tgz#9c3505c1db45bcedca3d9cf7a16f5c5aa3901878" - integrity sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w== - -strip-final-newline@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/strip-final-newline/-/strip-final-newline-2.0.0.tgz#89b852fb2fcbe936f6f4b3187afb0a12c1ab58ad" - integrity sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA== - strip-json-comments@^3.1.1: version "3.1.1" resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-3.1.1.tgz#31f1281b3832630434831c310c01cccda8cbe006" @@ -4764,27 +3203,13 @@ supports-color@^7, supports-color@^7.1.0: dependencies: has-flag "^4.0.0" -supports-color@^8.0.0, supports-color@^8.1.1: +supports-color@^8.1.1: version "8.1.1" resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-8.1.1.tgz#cd6fc17e28500cff56c1b86c0a7fd4a54a73005c" integrity sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q== dependencies: has-flag "^4.0.0" -supports-preserve-symlinks-flag@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz#6eda4bd344a3c94aea376d4cc31bc77311039e09" - integrity sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w== - -test-exclude@^6.0.0: - version "6.0.0" - resolved "https://registry.yarnpkg.com/test-exclude/-/test-exclude-6.0.0.tgz#04a8698661d805ea6fa293b6cb9e63ac044ef15e" - integrity sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w== - dependencies: - "@istanbuljs/schema" "^0.1.2" - glob "^7.1.4" - minimatch "^3.0.4" - text-hex@1.0.x: version "1.0.0" resolved "https://registry.yarnpkg.com/text-hex/-/text-hex-1.0.0.tgz#69dc9c1b17446ee79a92bf5b884bb4b9127506f5" @@ -4800,11 +3225,6 @@ tinyspy@^3.0.2: resolved "https://registry.yarnpkg.com/tinyspy/-/tinyspy-3.0.2.tgz#86dd3cf3d737b15adcf17d7887c84a75201df20a" integrity sha512-n1cw8k1k0x4pgA2+9XrOkFydTerNcJ1zWCO5Nn9scWHTD+5tp8dghT2x1uduQePZTZgd3Tupf+x9BxJjeJi77Q== -tmpl@1.0.5: - version "1.0.5" - resolved "https://registry.yarnpkg.com/tmpl/-/tmpl-1.0.5.tgz#8683e0b902bb9c20c4f726e3c0b69f36518c07cc" - integrity sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw== - to-regex-range@^5.0.1: version "5.0.1" resolved "https://registry.yarnpkg.com/to-regex-range/-/to-regex-range-5.0.1.tgz#1648c44aae7c8d988a326018ed72f5b4dd0392e4" @@ -4839,24 +3259,11 @@ type-detect@^4.0.0, type-detect@^4.1.0: resolved "https://registry.yarnpkg.com/type-detect/-/type-detect-4.1.0.tgz#deb2453e8f08dcae7ae98c626b13dddb0155906c" integrity sha512-Acylog8/luQ8L7il+geoSxhEkazvkslg7PSNKOX59mbB9cOveP5aq9h74Y7YU8yDpJwetzQQrfIwtf4Wp4LKcw== -type-fest@^0.21.3: - version "0.21.3" - resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.21.3.tgz#d260a24b0198436e133fa26a524a6d65fa3b2e37" - integrity sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w== - undici-types@~6.20.0: version "6.20.0" resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-6.20.0.tgz#8171bf22c1f588d1554d55bf204bc624af388433" integrity sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg== -update-browserslist-db@^1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/update-browserslist-db/-/update-browserslist-db-1.1.1.tgz#80846fba1d79e82547fb661f8d141e0945755fe5" - integrity sha512-R8UzCaa9Az+38REPiJ1tXlImTJXlVfgHZsglwBD/k6nj76ctsH1E3q4doGrukiLQd3sGQYu56r5+lo5r94l29A== - dependencies: - escalade "^3.2.0" - picocolors "^1.1.0" - uri-js@^4.2.2: version "4.4.1" resolved "https://registry.yarnpkg.com/uri-js/-/uri-js-4.4.1.tgz#9b1a52595225859e55f669d928f88c6c57f2a77e" @@ -4898,22 +3305,6 @@ uuid@^9.0.1: resolved "https://registry.yarnpkg.com/uuid/-/uuid-9.0.1.tgz#e188d4c8853cc722220392c424cd637f32293f30" integrity sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA== -v8-to-istanbul@^9.0.1: - version "9.3.0" - resolved "https://registry.yarnpkg.com/v8-to-istanbul/-/v8-to-istanbul-9.3.0.tgz#b9572abfa62bd556c16d75fdebc1a411d5ff3175" - integrity sha512-kiGUalWN+rgBJ/1OHZsBtU4rXZOfj/7rKQxULKlIzwzQSvMJUUNgPwJEEh7gU6xEVxC0ahoOBvN2YI8GH6FNgA== - dependencies: - "@jridgewell/trace-mapping" "^0.3.12" - "@types/istanbul-lib-coverage" "^2.0.1" - convert-source-map "^2.0.0" - -walker@^1.0.8: - version "1.0.8" - resolved "https://registry.yarnpkg.com/walker/-/walker-1.0.8.tgz#bd498db477afe573dc04185f011d3ab8a8d7653f" - integrity sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ== - dependencies: - makeerror "1.0.12" - watchpack@^2.0.0-beta.10: version "2.4.2" resolved "https://registry.yarnpkg.com/watchpack/-/watchpack-2.4.2.tgz#2feeaed67412e7c33184e5a79ca738fbd38564da" @@ -5003,19 +3394,6 @@ wrap-ansi@^8.1.0: string-width "^5.0.1" strip-ansi "^7.0.1" -wrappy@1: - version "1.0.2" - resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" - integrity sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ== - -write-file-atomic@^4.0.2: - version "4.0.2" - resolved "https://registry.yarnpkg.com/write-file-atomic/-/write-file-atomic-4.0.2.tgz#a9df01ae5b77858a027fd2e80768ee433555fcfd" - integrity sha512-7KxauUdBmSdWnmpaGFg+ppNjKF8uNLry8LyzjauQDOVONfFLNKrKvQOxZ/VuTIcS/gge/YNahf5RIIQWTSarlg== - dependencies: - imurmurhash "^0.1.4" - signal-exit "^3.0.7" - xml2js@0.6.2: version "0.6.2" resolved "https://registry.yarnpkg.com/xml2js/-/xml2js-0.6.2.tgz#dd0b630083aa09c161e25a4d0901e2b2a929b499" @@ -5034,21 +3412,11 @@ y18n@^5.0.5: resolved "https://registry.yarnpkg.com/y18n/-/y18n-5.0.8.tgz#7f4934d0f7ca8c56f95314939ddcd2dd91ce1d55" integrity sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA== -yallist@^3.0.2: - version "3.1.1" - resolved "https://registry.yarnpkg.com/yallist/-/yallist-3.1.1.tgz#dbb7daf9bfd8bac9ab45ebf602b8cbad0d5d08fd" - integrity sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g== - yargs-parser@^20.2.2, yargs-parser@^20.2.9: version "20.2.9" resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-20.2.9.tgz#2eb7dc3b0289718fc295f362753845c41a0c94ee" integrity sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w== -yargs-parser@^21.1.1: - version "21.1.1" - resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-21.1.1.tgz#9096bceebf990d21bb31fa9516e0ede294a77d35" - integrity sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw== - yargs-unparser@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/yargs-unparser/-/yargs-unparser-2.0.0.tgz#f131f9226911ae5d9ad38c432fe809366c2325eb" @@ -5072,19 +3440,6 @@ yargs@^16.2.0: y18n "^5.0.5" yargs-parser "^20.2.2" -yargs@^17.3.1: - version "17.7.2" - resolved "https://registry.yarnpkg.com/yargs/-/yargs-17.7.2.tgz#991df39aca675a192b816e1e0363f9d75d2aa269" - integrity sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w== - dependencies: - cliui "^8.0.1" - escalade "^3.1.1" - get-caller-file "^2.0.5" - require-directory "^2.1.1" - string-width "^4.2.3" - y18n "^5.0.5" - yargs-parser "^21.1.1" - yocto-queue@^0.1.0: version "0.1.0" resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-0.1.0.tgz#0294eb3dee05028d31ee1a5fa2c556a6aaf10a1b" From cc2ff78d03a477cad4eb49db5b3c64d141c15e04 Mon Sep 17 00:00:00 2001 From: Landon Shumway Date: Tue, 7 Oct 2025 13:06:06 -0500 Subject: [PATCH 50/55] Update Email dependencies/client version to address audit check --- .../lambdas/nodejs/cognito-emails/handler.ts | 4 +- .../lambdas/nodejs/cognito-emails/lambda.ts | 4 +- .../email-notification-service/handler.ts | 4 +- .../email-notification-service/lambda.ts | 4 +- .../nodejs/ingest-event-reporter/handler.ts | 4 +- .../nodejs/ingest-event-reporter/lambda.ts | 4 +- .../nodejs/lib/email/base-email-service.ts | 28 +- .../lambdas/nodejs/package.json | 14 +- .../nodejs/tests/cognito-emails.test.ts | 6 +- .../tests/email-notification-service.test.ts | 376 ++-- .../tests/ingest-event-reporter.test.ts | 8 +- .../lib/email/base-email-service.test.ts | 6 +- .../lib/email/cognito-email-service.test.ts | 6 +- .../email/email-notification-service.test.ts | 268 +-- .../encumbrance-notification-service.test.ts | 172 +- .../email/ingest-event-email-service.test.ts | 66 +- .../compact-connect/lambdas/nodejs/yarn.lock | 1746 ++++++++--------- 17 files changed, 1387 insertions(+), 1333 deletions(-) diff --git a/backend/compact-connect/lambdas/nodejs/cognito-emails/handler.ts b/backend/compact-connect/lambdas/nodejs/cognito-emails/handler.ts index 4b9f0c115..b4ce83bef 100644 --- a/backend/compact-connect/lambdas/nodejs/cognito-emails/handler.ts +++ b/backend/compact-connect/lambdas/nodejs/cognito-emails/handler.ts @@ -1,12 +1,12 @@ import { DynamoDBClient } from '@aws-sdk/client-dynamodb'; -import { SESClient } from '@aws-sdk/client-ses'; +import { SESv2Client } from '@aws-sdk/client-sesv2'; import { S3Client } from '@aws-sdk/client-s3'; import { Lambda } from './lambda'; const lambda = new Lambda({ dynamoDBClient: new DynamoDBClient(), s3Client: new S3Client(), - sesClient: new SESClient(), + sesClient: new SESv2Client(), }); export const customMessage = lambda.handler.bind(lambda); diff --git a/backend/compact-connect/lambdas/nodejs/cognito-emails/lambda.ts b/backend/compact-connect/lambdas/nodejs/cognito-emails/lambda.ts index d87edcf8a..b64ad072f 100644 --- a/backend/compact-connect/lambdas/nodejs/cognito-emails/lambda.ts +++ b/backend/compact-connect/lambdas/nodejs/cognito-emails/lambda.ts @@ -1,6 +1,6 @@ import type { LambdaInterface } from '@aws-lambda-powertools/commons/lib/esm/types'; import { Logger } from '@aws-lambda-powertools/logger'; -import { SESClient } from '@aws-sdk/client-ses'; +import { SESv2Client } from '@aws-sdk/client-sesv2'; import { S3Client } from '@aws-sdk/client-s3'; import { DynamoDBClient } from '@aws-sdk/client-dynamodb'; import { Context } from 'aws-lambda'; @@ -42,7 +42,7 @@ interface CognitoCustomMessageEvent { interface LambdaProperties { dynamoDBClient: DynamoDBClient; - sesClient: SESClient; + sesClient: SESv2Client; s3Client: S3Client; } diff --git a/backend/compact-connect/lambdas/nodejs/email-notification-service/handler.ts b/backend/compact-connect/lambdas/nodejs/email-notification-service/handler.ts index 8e7bc88f2..329441aa5 100644 --- a/backend/compact-connect/lambdas/nodejs/email-notification-service/handler.ts +++ b/backend/compact-connect/lambdas/nodejs/email-notification-service/handler.ts @@ -1,5 +1,5 @@ import { DynamoDBClient } from '@aws-sdk/client-dynamodb'; -import { SESClient } from '@aws-sdk/client-ses'; +import { SESv2Client } from '@aws-sdk/client-sesv2'; import { S3Client } from '@aws-sdk/client-s3'; import { Lambda } from './lambda'; @@ -7,7 +7,7 @@ import { Lambda } from './lambda'; const lambda = new Lambda({ dynamoDBClient: new DynamoDBClient(), s3Client: new S3Client(), - sesClient: new SESClient(), + sesClient: new SESv2Client(), }); export const sendEmail = lambda.handler.bind(lambda); diff --git a/backend/compact-connect/lambdas/nodejs/email-notification-service/lambda.ts b/backend/compact-connect/lambdas/nodejs/email-notification-service/lambda.ts index fccb0fc27..91579cd30 100644 --- a/backend/compact-connect/lambdas/nodejs/email-notification-service/lambda.ts +++ b/backend/compact-connect/lambdas/nodejs/email-notification-service/lambda.ts @@ -1,7 +1,7 @@ import type { LambdaInterface } from '@aws-lambda-powertools/commons/lib/esm/types'; import { Logger } from '@aws-lambda-powertools/logger'; import { DynamoDBClient } from '@aws-sdk/client-dynamodb'; -import { SESClient } from '@aws-sdk/client-ses'; +import { SESv2Client } from '@aws-sdk/client-sesv2'; import { S3Client } from '@aws-sdk/client-s3'; import { Context } from 'aws-lambda'; @@ -16,7 +16,7 @@ const logger = new Logger({ logLevel: environmentVariables.getLogLevel() }); interface LambdaProperties { dynamoDBClient: DynamoDBClient; - sesClient: SESClient; + sesClient: SESv2Client; s3Client: S3Client; } diff --git a/backend/compact-connect/lambdas/nodejs/ingest-event-reporter/handler.ts b/backend/compact-connect/lambdas/nodejs/ingest-event-reporter/handler.ts index d3cda25c3..5485c9084 100644 --- a/backend/compact-connect/lambdas/nodejs/ingest-event-reporter/handler.ts +++ b/backend/compact-connect/lambdas/nodejs/ingest-event-reporter/handler.ts @@ -1,5 +1,5 @@ import { DynamoDBClient } from '@aws-sdk/client-dynamodb'; -import { SESClient } from '@aws-sdk/client-ses'; +import { SESv2Client } from '@aws-sdk/client-sesv2'; import { S3Client } from '@aws-sdk/client-s3'; import { Lambda } from './lambda'; @@ -7,7 +7,7 @@ import { Lambda } from './lambda'; const lambda = new Lambda({ dynamoDBClient: new DynamoDBClient(), s3Client: new S3Client(), - sesClient: new SESClient(), + sesClient: new SESv2Client(), }); export const collectEvents = lambda.handler.bind(lambda); diff --git a/backend/compact-connect/lambdas/nodejs/ingest-event-reporter/lambda.ts b/backend/compact-connect/lambdas/nodejs/ingest-event-reporter/lambda.ts index 29270d146..67a294d43 100644 --- a/backend/compact-connect/lambdas/nodejs/ingest-event-reporter/lambda.ts +++ b/backend/compact-connect/lambdas/nodejs/ingest-event-reporter/lambda.ts @@ -2,7 +2,7 @@ import type { LambdaInterface } from '@aws-lambda-powertools/commons/lib/esm/typ import { Logger } from '@aws-lambda-powertools/logger'; import { DynamoDBClient } from '@aws-sdk/client-dynamodb'; import { S3Client } from '@aws-sdk/client-s3'; -import { SESClient } from '@aws-sdk/client-ses'; +import { SESv2Client } from '@aws-sdk/client-sesv2'; import { Context } from 'aws-lambda'; import { EnvironmentVariablesService } from '../lib/environment-variables-service'; @@ -17,7 +17,7 @@ const logger = new Logger({ logLevel: environmentVariables.getLogLevel() }); interface LambdaProperties { dynamoDBClient: DynamoDBClient; - sesClient: SESClient; + sesClient: SESv2Client; s3Client: S3Client; } diff --git a/backend/compact-connect/lambdas/nodejs/lib/email/base-email-service.ts b/backend/compact-connect/lambdas/nodejs/lib/email/base-email-service.ts index 1b704fc30..e26f776b5 100644 --- a/backend/compact-connect/lambdas/nodejs/lib/email/base-email-service.ts +++ b/backend/compact-connect/lambdas/nodejs/lib/email/base-email-service.ts @@ -2,7 +2,7 @@ import * as crypto from 'crypto'; import * as nodemailer from 'nodemailer'; import { Logger } from '@aws-lambda-powertools/logger'; -import { SendEmailCommand, SendRawEmailCommand, SESClient } from '@aws-sdk/client-ses'; +import { SendEmailCommand, SESv2Client } from '@aws-sdk/client-sesv2'; import { S3Client } from '@aws-sdk/client-s3'; import { TReaderDocument, renderToStaticMarkup } from '@jusdino-ia/email-builder'; import { CompactConfigurationClient } from '../compact-configuration-client'; @@ -14,7 +14,7 @@ const environmentVariableService = new EnvironmentVariablesService(); interface EmailServiceProperties { logger: Logger; - sesClient: SESClient; + sesClient: SESv2Client; s3Client: S3Client; compactConfigurationClient: CompactConfigurationClient; jurisdictionClient: JurisdictionClient; @@ -31,7 +31,7 @@ interface StyledBlockOptions { */ export abstract class BaseEmailService { protected readonly logger: Logger; - protected readonly sesClient: SESClient; + protected readonly sesClient: SESv2Client; protected readonly s3Client: S3Client; protected readonly compactConfigurationClient: CompactConfigurationClient; protected readonly jurisdictionClient: JurisdictionClient; @@ -75,20 +75,22 @@ export abstract class BaseEmailService { Destination: { ToAddresses: recipients, }, - Message: { - Body: { - Html: { + Content: { + Simple: { + Body: { + Html: { + Charset: 'UTF-8', + Data: htmlContent + } + }, + Subject: { Charset: 'UTF-8', - Data: htmlContent + Data: subject } - }, - Subject: { - Charset: 'UTF-8', - Data: subject } }, // We're required by the IAM policy to use this display name - Source: `Compact Connect <${environmentVariableService.getFromAddress()}>`, + FromEmailAddress: `Compact Connect <${environmentVariableService.getFromAddress()}>`, }); return (await this.sesClient.send(command)).MessageId; @@ -114,7 +116,7 @@ export abstract class BaseEmailService { try { // Create a nodemailer transport that generates raw MIME messages const transport = nodemailer.createTransport({ - SES: { ses: this.sesClient, aws: { SendRawEmailCommand }} + SES: { sesClient: this.sesClient, SendEmailCommand }, }); // Create the email message diff --git a/backend/compact-connect/lambdas/nodejs/package.json b/backend/compact-connect/lambdas/nodejs/package.json index 178e328d6..747edcde7 100644 --- a/backend/compact-connect/lambdas/nodejs/package.json +++ b/backend/compact-connect/lambdas/nodejs/package.json @@ -13,6 +13,10 @@ }, "author": "Inspiring Apps", "license": "UNLICENSED", + "resolutions": { + "@smithy/types": "^4.6.0", + "@smithy/util-stream": "^4.0.0" + }, "devDependencies": { "@types/aws-lambda": "8.10.145", "@types/jest": "^29.5.12", @@ -41,13 +45,13 @@ }, "dependencies": { "@aws-lambda-powertools/logger": "^2.10.0", - "@aws-sdk/client-dynamodb": "^3.682.0", - "@aws-sdk/client-s3": "^3.682.0", - "@aws-sdk/client-ses": "^3.682.0", - "@aws-sdk/util-dynamodb": "^3.682.0", + "@aws-sdk/client-dynamodb": "^3.901.0", + "@aws-sdk/client-s3": "^3.901.0", + "@aws-sdk/client-sesv2": "^3.901.0", + "@aws-sdk/util-dynamodb": "^3.901.0", "@jusdino-ia/email-builder": "^0.0.9-alpha.3", "aws-lambda": "1.0.7", - "nodemailer": "^6.9.12", + "nodemailer": "^7.0.7", "zod": "^3.23.8" } } diff --git a/backend/compact-connect/lambdas/nodejs/tests/cognito-emails.test.ts b/backend/compact-connect/lambdas/nodejs/tests/cognito-emails.test.ts index 59cdfbdc8..769f2542d 100644 --- a/backend/compact-connect/lambdas/nodejs/tests/cognito-emails.test.ts +++ b/backend/compact-connect/lambdas/nodejs/tests/cognito-emails.test.ts @@ -1,7 +1,7 @@ import { mockClient } from 'aws-sdk-client-mock'; import 'aws-sdk-client-mock-jest'; import { DynamoDBClient } from '@aws-sdk/client-dynamodb'; -import { SESClient } from '@aws-sdk/client-ses'; +import { SESv2Client } from '@aws-sdk/client-sesv2'; import { S3Client } from '@aws-sdk/client-s3'; import { Lambda } from '../cognito-emails/lambda'; import { describe, it, expect, beforeAll, beforeEach, jest } from '@jest/globals'; @@ -52,7 +52,7 @@ const SAMPLE_CONTEXT = { const asDynamoDBClient = (mock: ReturnType) => mock as unknown as DynamoDBClient; const asSESClient = (mock: ReturnType) => - mock as unknown as SESClient; + mock as unknown as SESv2Client; const asS3Client = (mock: ReturnType) => mock as unknown as S3Client; @@ -71,7 +71,7 @@ describe('CognitoEmailsLambda', () => { beforeEach(() => { jest.clearAllMocks(); mockDynamoDBClient = mockClient(DynamoDBClient); - mockSESClient = mockClient(SESClient); + mockSESClient = mockClient(SESv2Client); mockS3Client = mockClient(S3Client); // Reset environment variables diff --git a/backend/compact-connect/lambdas/nodejs/tests/email-notification-service.test.ts b/backend/compact-connect/lambdas/nodejs/tests/email-notification-service.test.ts index f29f16260..ddd052e51 100644 --- a/backend/compact-connect/lambdas/nodejs/tests/email-notification-service.test.ts +++ b/backend/compact-connect/lambdas/nodejs/tests/email-notification-service.test.ts @@ -1,7 +1,7 @@ import { mockClient } from 'aws-sdk-client-mock'; import 'aws-sdk-client-mock-jest'; import { DynamoDBClient, GetItemCommand } from '@aws-sdk/client-dynamodb'; -import { SESClient, SendEmailCommand, SendRawEmailCommand } from '@aws-sdk/client-ses'; +import { SESv2Client, SendEmailCommand } from '@aws-sdk/client-sesv2'; import { S3Client, GetObjectCommand } from '@aws-sdk/client-s3'; import { Readable } from 'stream'; import { sdkStreamMixin } from '@smithy/util-stream'; @@ -49,7 +49,7 @@ const asDynamoDBClient = (mock: ReturnType) => mock as unknown as DynamoDBClient; const asSESClient = (mock: ReturnType) => - mock as unknown as SESClient; + mock as unknown as SESv2Client; const asS3Client = (mock: ReturnType) => mock as unknown as S3Client; @@ -69,7 +69,7 @@ describe('EmailNotificationServiceLambda', () => { beforeEach(() => { jest.clearAllMocks(); mockDynamoDBClient = mockClient(DynamoDBClient); - mockSESClient = mockClient(SESClient); + mockSESClient = mockClient(SESv2Client); mockS3Client = mockClient(S3Client); // Reset environment variables @@ -97,9 +97,7 @@ describe('EmailNotificationServiceLambda', () => { MessageId: 'message-id-123' }); - mockSESClient.on(SendRawEmailCommand).resolves({ - MessageId: 'message-id-raw' - }); + // Note: SESv2 with nodemailer 7.0.7 uses SendEmailCommand for all email sending // Create a mock stream that implements the required AWS SDK interfaces const mockStream = sdkStreamMixin( @@ -158,19 +156,21 @@ describe('EmailNotificationServiceLambda', () => { Destination: { ToAddresses: ['operations@example.com'] }, - Message: { - Body: { - Html: { + Content: { + Simple: { + Body: { + Html: { + Charset: 'UTF-8', + Data: expect.stringContaining('A transaction settlement error was detected') + } + }, + Subject: { Charset: 'UTF-8', - Data: expect.stringContaining('A transaction settlement error was detected') + Data: 'Transactions Failed to Settle for Audiology and Speech Language Pathology Payment Processor' } - }, - Subject: { - Charset: 'UTF-8', - Data: 'Transactions Failed to Settle for Audiology and Speech Language Pathology Payment Processor' } }, - Source: 'Compact Connect ' + FromEmailAddress: 'Compact Connect ' }); }); @@ -225,17 +225,19 @@ describe('EmailNotificationServiceLambda', () => { }); // Verify email was sent with correct parameters - expect(mockSESClient).toHaveReceivedCommandWith(SendRawEmailCommand, { - RawMessage: { - Data: expect.any(Buffer) + expect(mockSESClient).toHaveReceivedCommandWith(SendEmailCommand, { + Content: { + Raw: { + Data: expect.any(Uint8Array) + } } }); // Get the raw email data and verify it contains the attachments - const rawEmailData = mockSESClient.commandCalls(SendRawEmailCommand)[0].args[0].input.RawMessage?.Data; + const rawEmailData = mockSESClient.commandCalls(SendEmailCommand)[0].args[0].input.Content?.Raw?.Data; expect(rawEmailData).toBeDefined(); - const rawEmailString = rawEmailData?.toString(); + const rawEmailString = Buffer.from(rawEmailData || new Uint8Array()).toString(); expect(rawEmailString).toContain('Content-Type: application/zip;'); expect(rawEmailString).toContain('name=settled-transaction-report-2024-03-01--2024-03-07.zip'); @@ -322,17 +324,19 @@ describe('EmailNotificationServiceLambda', () => { }); // Verify email was sent with correct parameters - expect(mockSESClient).toHaveReceivedCommandWith(SendRawEmailCommand, { - RawMessage: { - Data: expect.any(Buffer) + expect(mockSESClient).toHaveReceivedCommandWith(SendEmailCommand, { + Content: { + Raw: { + Data: expect.any(Uint8Array) + } } }); // Get the raw email data and verify it contains the attachments - const rawEmailData = mockSESClient.commandCalls(SendRawEmailCommand)[0].args[0].input.RawMessage?.Data; + const rawEmailData = mockSESClient.commandCalls(SendEmailCommand)[0].args[0].input.Content?.Raw?.Data; expect(rawEmailData).toBeDefined(); - const rawEmailString = rawEmailData?.toString(); + const rawEmailString = Buffer.from(rawEmailData || new Uint8Array()).toString(); expect(rawEmailString).toContain('Content-Type: application/zip;'); expect(rawEmailString).toContain('name=oh-settled-transaction-report-2024-03-01--2024-03-07.zip'); @@ -428,20 +432,23 @@ describe('EmailNotificationServiceLambda', () => { Destination: { ToAddresses: ['ohio@example.com'] }, - Message: { - Body: { - Html: { + Content: { + Simple: { + Body: { + Html: { + Charset: 'UTF-8', + Data: expect.stringContaining('') + } + }, + Subject: { Charset: 'UTF-8', - Data: expect.stringContaining('') + Data: 'A Privilege was Deactivated in the Audiology and Speech Language Pathology Compact' } - }, - Subject: { - Charset: 'UTF-8', - Data: 'A Privilege was Deactivated in the Audiology and Speech Language Pathology Compact' } }, - Source: 'Compact Connect ' - }); + FromEmailAddress: 'Compact Connect ' + } + ); }); }); @@ -468,19 +475,22 @@ describe('EmailNotificationServiceLambda', () => { Destination: { ToAddresses: ['specific@example.com'] }, - Message: { - Body: { - Html: { + Content: { + Simple: { + Body: { + Html: { + Charset: 'UTF-8', + Data: expect.stringContaining('') + } + }, + Subject: { Charset: 'UTF-8', - Data: expect.stringContaining('') + Data: 'Your Privilege 123 is Deactivated' } - }, - Subject: { - Charset: 'UTF-8', - Data: 'Your Privilege 123 is Deactivated' } }, - Source: 'Compact Connect ' + FromEmailAddress: 'Compact Connect ' + }); }); @@ -535,19 +545,21 @@ describe('EmailNotificationServiceLambda', () => { Destination: { ToAddresses: ['provider@example.com'] }, - Message: { - Body: { - Html: { + Content: { + Simple: { + Body: { + Html: { + Charset: 'UTF-8', + Data: expect.stringContaining('') + } + }, + Subject: { Charset: 'UTF-8', - Data: expect.stringContaining('') + Data: 'Compact Connect Privilege Purchase Confirmation' } - }, - Subject: { - Charset: 'UTF-8', - Data: 'Compact Connect Privilege Purchase Confirmation' } }, - Source: 'Compact Connect ' + FromEmailAddress: 'Compact Connect ' }); }); @@ -582,19 +594,21 @@ describe('EmailNotificationServiceLambda', () => { Destination: { ToAddresses: ['user@example.com'] }, - Message: { - Body: { - Html: { + Content: { + Simple: { + Body: { + Html: { + Charset: 'UTF-8', + Data: expect.stringContaining('') + } + }, + Subject: { Charset: 'UTF-8', - Data: expect.stringContaining('') + Data: 'Registration Attempt Notification - Compact Connect' } - }, - Subject: { - Charset: 'UTF-8', - Data: 'Registration Attempt Notification - Compact Connect' } }, - Source: 'Compact Connect ' + FromEmailAddress: 'Compact Connect ' }); }); @@ -636,19 +650,21 @@ describe('EmailNotificationServiceLambda', () => { Destination: { ToAddresses: ['provider@example.com'] }, - Message: { - Body: { - Html: { + Content: { + Simple: { + Body: { + Html: { + Charset: 'UTF-8', + Data: expect.stringContaining('Your Audiologist license in Ohio is encumbered') + } + }, + Subject: { Charset: 'UTF-8', - Data: expect.stringContaining('Your Audiologist license in Ohio is encumbered') + Data: 'Your Audiologist license in Ohio is encumbered' } - }, - Subject: { - Charset: 'UTF-8', - Data: 'Your Audiologist license in Ohio is encumbered' } }, - Source: 'Compact Connect ' + FromEmailAddress: 'Compact Connect ' }); }); @@ -706,19 +722,21 @@ describe('EmailNotificationServiceLambda', () => { Destination: { ToAddresses: ['ca-adverse@example.com'] }, - Message: { - Body: { - Html: { + Content: { + Simple: { + Body: { + Html: { + Charset: 'UTF-8', + Data: expect.stringContaining('License Encumbrance Notification - John Doe') + } + }, + Subject: { Charset: 'UTF-8', - Data: expect.stringContaining('License Encumbrance Notification - John Doe') + Data: 'License Encumbrance Notification - John Doe' } - }, - Subject: { - Charset: 'UTF-8', - Data: 'License Encumbrance Notification - John Doe' } }, - Source: 'Compact Connect ' + FromEmailAddress: 'Compact Connect ' }); }); @@ -739,7 +757,8 @@ describe('EmailNotificationServiceLambda', () => { await lambda.handler(SAMPLE_LICENSE_ENCUMBRANCE_STATE_NOTIFICATION_EVENT, {} as any); - const emailData = mockSESClient.commandCalls(SendEmailCommand)[0].args[0].input.Message?.Body?.Html?.Data; + const emailData = mockSESClient.commandCalls( + SendEmailCommand)[0].args[0].input.Content?.Simple?.Body?.Html?.Data; expect(emailData).toContain('Provider Details: https://app.test.compactconnect.org/aslp/Licensing/provider-123'); }); @@ -795,19 +814,21 @@ describe('EmailNotificationServiceLambda', () => { Destination: { ToAddresses: ['provider@example.com'] }, - Message: { - Body: { - Html: { + Content: { + Simple: { + Body: { + Html: { + Charset: 'UTF-8', + Data: expect.stringContaining('Your Audiologist license in Ohio is no longer encumbered') + } + }, + Subject: { Charset: 'UTF-8', - Data: expect.stringContaining('Your Audiologist license in Ohio is no longer encumbered') + Data: 'Your Audiologist license in Ohio is no longer encumbered' } - }, - Subject: { - Charset: 'UTF-8', - Data: 'Your Audiologist license in Ohio is no longer encumbered' } }, - Source: 'Compact Connect ' + FromEmailAddress: 'Compact Connect ' }); }); @@ -866,19 +887,21 @@ describe('EmailNotificationServiceLambda', () => { Destination: { ToAddresses: ['ca-adverse@example.com'] }, - Message: { - Body: { - Html: { + Content: { + Simple: { + Body: { + Html: { + Charset: 'UTF-8', + Data: expect.stringContaining('License Encumbrance Lifted Notification - John Doe') + } + }, + Subject: { Charset: 'UTF-8', - Data: expect.stringContaining('License Encumbrance Lifted Notification - John Doe') + Data: 'License Encumbrance Lifted Notification - John Doe' } - }, - Subject: { - Charset: 'UTF-8', - Data: 'License Encumbrance Lifted Notification - John Doe' } }, - Source: 'Compact Connect ' + FromEmailAddress: 'Compact Connect ' }); }); @@ -931,19 +954,21 @@ describe('EmailNotificationServiceLambda', () => { Destination: { ToAddresses: ['provider@example.com'] }, - Message: { - Body: { - Html: { + Content: { + Simple: { + Body: { + Html: { + Charset: 'UTF-8', + Data: expect.stringContaining('Your Audiologist privilege in Ohio is encumbered') + } + }, + Subject: { Charset: 'UTF-8', - Data: expect.stringContaining('Your Audiologist privilege in Ohio is encumbered') + Data: 'Your Audiologist privilege in Ohio is encumbered' } - }, - Subject: { - Charset: 'UTF-8', - Data: 'Your Audiologist privilege in Ohio is encumbered' } }, - Source: 'Compact Connect ' + FromEmailAddress: 'Compact Connect ' }); }); @@ -1000,19 +1025,21 @@ describe('EmailNotificationServiceLambda', () => { Destination: { ToAddresses: ['ca-adverse@example.com'] }, - Message: { - Body: { - Html: { + Content: { + Simple: { + Body: { + Html: { + Charset: 'UTF-8', + Data: expect.stringContaining('Privilege Encumbrance Notification - John Doe') + } + }, + Subject: { Charset: 'UTF-8', - Data: expect.stringContaining('Privilege Encumbrance Notification - John Doe') + Data: 'Privilege Encumbrance Notification - John Doe' } - }, - Subject: { - Charset: 'UTF-8', - Data: 'Privilege Encumbrance Notification - John Doe' } }, - Source: 'Compact Connect ' + FromEmailAddress: 'Compact Connect ' }); }); @@ -1067,19 +1094,21 @@ describe('EmailNotificationServiceLambda', () => { Destination: { ToAddresses: ['provider@example.com'] }, - Message: { - Body: { - Html: { + Content: { + Simple: { + Body: { + Html: { + Charset: 'UTF-8', + Data: expect.stringContaining('Your Audiologist privilege in Ohio is no longer encumbered') + } + }, + Subject: { Charset: 'UTF-8', - Data: expect.stringContaining('Your Audiologist privilege in Ohio is no longer encumbered') + Data: 'Your Audiologist privilege in Ohio is no longer encumbered' } - }, - Subject: { - Charset: 'UTF-8', - Data: 'Your Audiologist privilege in Ohio is no longer encumbered' } }, - Source: 'Compact Connect ' + FromEmailAddress: 'Compact Connect ' }); }); @@ -1138,19 +1167,20 @@ describe('EmailNotificationServiceLambda', () => { Destination: { ToAddresses: ['ca-adverse@example.com'] }, - Message: { - Body: { - Html: { + Content: { + Simple: { + Body: { + Html: { + Charset: 'UTF-8', + Data: expect.stringContaining('Privilege Encumbrance Lifted Notification - John Doe') + } + }, + Subject: { Charset: 'UTF-8', - Data: expect.stringContaining('Privilege Encumbrance Lifted Notification - John Doe') + Data: 'Privilege Encumbrance Lifted Notification - John Doe' } - }, - Subject: { - Charset: 'UTF-8', - Data: 'Privilege Encumbrance Lifted Notification - John Doe' - } - }, - Source: 'Compact Connect ' + }}, + FromEmailAddress: 'Compact Connect ' }); }); @@ -1200,24 +1230,26 @@ describe('EmailNotificationServiceLambda', () => { Destination: { ToAddresses: ['newuser@example.com'] }, - Message: { - Body: { - Html: { + Content: { + Simple: { + Body: { + Html: { + Charset: 'UTF-8', + Data: expect.any(String) + } + }, + Subject: { Charset: 'UTF-8', - Data: expect.any(String) + Data: 'Verify Your New Email Address - Compact Connect' } - }, - Subject: { - Charset: 'UTF-8', - Data: 'Verify Your New Email Address - Compact Connect' } }, - Source: 'Compact Connect ' + FromEmailAddress: 'Compact Connect ' }); // Get the actual HTML content for detailed validation const emailCall = mockSESClient.commandCalls(SendEmailCommand)[0]; - const htmlContent = emailCall.args[0].input.Message?.Body?.Html?.Data; + const htmlContent = emailCall.args[0].input.Content?.Simple?.Body?.Html?.Data; expect(htmlContent).toBeDefined(); expect(htmlContent).toContain('Please use the following verification code to complete your email address change'); @@ -1273,24 +1305,25 @@ describe('EmailNotificationServiceLambda', () => { Destination: { ToAddresses: ['olduser@example.com'] }, - Message: { - Body: { - Html: { + Content: { + Simple: { + Body: { + Html: { + Charset: 'UTF-8', + Data: expect.any(String) + } + }, + Subject: { Charset: 'UTF-8', - Data: expect.any(String) + Data: 'Email Address Changed - Compact Connect' } - }, - Subject: { - Charset: 'UTF-8', - Data: 'Email Address Changed - Compact Connect' - } - }, - Source: 'Compact Connect ' + }}, + FromEmailAddress: 'Compact Connect ' }); // Get the actual HTML content for detailed validation const emailCall = mockSESClient.commandCalls(SendEmailCommand)[0]; - const htmlContent = emailCall.args[0].input.Message?.Body?.Html?.Data; + const htmlContent = emailCall.args[0].input.Content?.Simple?.Body?.Html?.Data; expect(htmlContent).toBeDefined(); expect(htmlContent).toContain('This is to notify you that your Compact Connect account email address has been changed to the following:'); @@ -1344,24 +1377,25 @@ describe('EmailNotificationServiceLambda', () => { Destination: { ToAddresses: ['user@example.com'] }, - Message: { - Body: { - Html: { + Content: { + Simple: { + Body: { + Html: { + Charset: 'UTF-8', + Data: expect.any(String) + } + }, + Subject: { Charset: 'UTF-8', - Data: expect.any(String) + Data: 'Confirm Account Recovery - Compact Connect' } - }, - Subject: { - Charset: 'UTF-8', - Data: 'Confirm Account Recovery - Compact Connect' - } - }, - Source: 'Compact Connect ' + }}, + FromEmailAddress: 'Compact Connect ' }); // Get the actual HTML content for detailed validation const emailCall = mockSESClient.commandCalls(SendEmailCommand)[0]; - const htmlContent = emailCall.args[0].input.Message?.Body?.Html?.Data; + const htmlContent = emailCall.args[0].input.Content?.Simple?.Body?.Html?.Data; expect(htmlContent).toBeDefined(); expect(htmlContent).toContain('A request was made to recover access to your Compact Connect user account.'); diff --git a/backend/compact-connect/lambdas/nodejs/tests/ingest-event-reporter.test.ts b/backend/compact-connect/lambdas/nodejs/tests/ingest-event-reporter.test.ts index 10358b69f..b5812b763 100644 --- a/backend/compact-connect/lambdas/nodejs/tests/ingest-event-reporter.test.ts +++ b/backend/compact-connect/lambdas/nodejs/tests/ingest-event-reporter.test.ts @@ -2,7 +2,7 @@ import { mockClient } from 'aws-sdk-client-mock'; import 'aws-sdk-client-mock-jest'; import { Context, EventBridgeEvent } from 'aws-lambda'; import { DynamoDBClient, QueryCommand, GetItemCommand } from '@aws-sdk/client-dynamodb'; -import { SendEmailCommand, SESClient } from '@aws-sdk/client-ses'; +import { SendEmailCommand, SESv2Client } from '@aws-sdk/client-sesv2'; import { S3Client } from '@aws-sdk/client-s3'; import { Lambda } from '../ingest-event-reporter/lambda'; @@ -49,7 +49,7 @@ const SAMPLE_CONTEXT: Context = { const asDynamoDBClient = (mock: ReturnType) => mock as unknown as DynamoDBClient; const asSESClient = (mock: ReturnType) => - mock as unknown as SESClient; + mock as unknown as SESv2Client; const asS3Client = (mock: ReturnType) => mock as unknown as S3Client; @@ -86,7 +86,7 @@ describe('Nightly runs', () => { process.env.AWS_REGION = 'us-east-1'; // Get the mocked client instances - mockSESClient = mockClient(SESClient); + mockSESClient = mockClient(SESv2Client); }); beforeEach(() => { @@ -310,7 +310,7 @@ describe('Weekly runs', () => { jest.clearAllMocks(); // Get the mocked client instances - mockSESClient = mockClient(SESClient); + mockSESClient = mockClient(SESv2Client); mockSESClient.on(SendEmailCommand).resolves({ MessageId: 'foo-123' }); diff --git a/backend/compact-connect/lambdas/nodejs/tests/lib/email/base-email-service.test.ts b/backend/compact-connect/lambdas/nodejs/tests/lib/email/base-email-service.test.ts index 8893f5594..ef90f28ab 100644 --- a/backend/compact-connect/lambdas/nodejs/tests/lib/email/base-email-service.test.ts +++ b/backend/compact-connect/lambdas/nodejs/tests/lib/email/base-email-service.test.ts @@ -1,13 +1,13 @@ import { mockClient } from 'aws-sdk-client-mock'; import 'aws-sdk-client-mock-jest'; import { Logger } from '@aws-lambda-powertools/logger'; -import { SESClient } from '@aws-sdk/client-ses'; +import { SESv2Client } from '@aws-sdk/client-sesv2'; import { S3Client } from '@aws-sdk/client-s3'; import { BaseEmailService } from '../../../lib/email/base-email-service'; import { describe, it, expect, beforeEach, jest } from '@jest/globals'; const asSESClient = (mock: ReturnType) => - mock as unknown as SESClient; + mock as unknown as SESv2Client; const asS3Client = (mock: ReturnType) => mock as unknown as S3Client; @@ -32,7 +32,7 @@ describe('BaseEmailService Environment Banner', () => { beforeEach(() => { jest.clearAllMocks(); - mockSESClient = mockClient(SESClient); + mockSESClient = mockClient(SESv2Client); mockS3Client = mockClient(S3Client); // Reset environment variables diff --git a/backend/compact-connect/lambdas/nodejs/tests/lib/email/cognito-email-service.test.ts b/backend/compact-connect/lambdas/nodejs/tests/lib/email/cognito-email-service.test.ts index ef8f8b111..d2357db0e 100644 --- a/backend/compact-connect/lambdas/nodejs/tests/lib/email/cognito-email-service.test.ts +++ b/backend/compact-connect/lambdas/nodejs/tests/lib/email/cognito-email-service.test.ts @@ -1,14 +1,14 @@ import { mockClient } from 'aws-sdk-client-mock'; import 'aws-sdk-client-mock-jest'; import { Logger } from '@aws-lambda-powertools/logger'; -import { SESClient } from '@aws-sdk/client-ses'; +import { SESv2Client } from '@aws-sdk/client-sesv2'; import { CognitoEmailService } from '../../../lib/email'; import { EmailTemplateCapture } from '../../utils/email-template-capture'; import { TReaderDocument } from '@jusdino-ia/email-builder'; import { describe, it, expect, beforeEach, beforeAll, afterAll, jest } from '@jest/globals'; const asSESClient = (mock: ReturnType) => - mock as unknown as SESClient; + mock as unknown as SESv2Client; describe('CognitoEmailService', () => { let emailService: CognitoEmailService; @@ -39,7 +39,7 @@ describe('CognitoEmailService', () => { beforeEach(() => { jest.clearAllMocks(); - mockSESClient = mockClient(SESClient); + mockSESClient = mockClient(SESv2Client); // Reset environment variables process.env.FROM_ADDRESS = 'noreply@example.org'; diff --git a/backend/compact-connect/lambdas/nodejs/tests/lib/email/email-notification-service.test.ts b/backend/compact-connect/lambdas/nodejs/tests/lib/email/email-notification-service.test.ts index b9f05976f..94f44d418 100644 --- a/backend/compact-connect/lambdas/nodejs/tests/lib/email/email-notification-service.test.ts +++ b/backend/compact-connect/lambdas/nodejs/tests/lib/email/email-notification-service.test.ts @@ -1,7 +1,7 @@ import { mockClient } from 'aws-sdk-client-mock'; import 'aws-sdk-client-mock-jest'; import { Logger } from '@aws-lambda-powertools/logger'; -import { SendEmailCommand, SendRawEmailCommand, SESClient } from '@aws-sdk/client-ses'; +import { SendEmailCommand, SESv2Client } from '@aws-sdk/client-sesv2'; import { S3Client, GetObjectCommand } from '@aws-sdk/client-s3'; import { Readable } from 'stream'; import { sdkStreamMixin } from '@smithy/util-stream'; @@ -43,7 +43,7 @@ const SAMPLE_JURISDICTION_CONFIG = { }; const asSESClient = (mock: ReturnType) => - mock as unknown as SESClient; + mock as unknown as SESv2Client; const asS3Client = (mock: ReturnType) => mock as unknown as S3Client; @@ -86,7 +86,7 @@ describe('EmailNotificationService', () => { beforeEach(() => { jest.clearAllMocks(); - mockSESClient = mockClient(SESClient); + mockSESClient = mockClient(SESv2Client); mockS3Client = mockClient(S3Client); mockCompactConfigurationClient = { getCompactConfiguration: jest.fn() @@ -106,9 +106,7 @@ describe('EmailNotificationService', () => { MessageId: 'message-id-123' }); - mockSESClient.on(SendRawEmailCommand).resolves({ - MessageId: 'message-id-raw' - }); + // Note: SESv2 with nodemailer 7.0.7 uses SendEmailCommand for all email sending (nodemailer.createTransport as jest.Mock).mockReturnValue(MOCK_TRANSPORT); @@ -136,19 +134,21 @@ describe('EmailNotificationService', () => { Destination: { ToAddresses: ['operations@example.com'] }, - Message: { - Body: { - Html: { + Content: { + Simple: { + Body: { + Html: { + Charset: 'UTF-8', + Data: expect.stringContaining('') + } + }, + Subject: { Charset: 'UTF-8', - Data: expect.stringContaining('') + Data: 'Transactions Failed to Settle for Audiology and Speech Language Pathology Payment Processor' } - }, - Subject: { - Charset: 'UTF-8', - Data: 'Transactions Failed to Settle for Audiology and Speech Language Pathology Payment Processor' } }, - Source: 'Compact Connect ' + FromEmailAddress: 'Compact Connect ' } ); }); @@ -168,19 +168,21 @@ describe('EmailNotificationService', () => { Destination: { ToAddresses: ['specific@example.com'] }, - Message: { - Body: { - Html: { + Content: { + Simple: { + Body: { + Html: { + Charset: 'UTF-8', + Data: expect.stringContaining('') + } + }, + Subject: { Charset: 'UTF-8', - Data: expect.stringContaining('') + Data: 'Transactions Failed to Settle for Audiology and Speech Language Pathology Payment Processor' } - }, - Subject: { - Charset: 'UTF-8', - Data: 'Transactions Failed to Settle for Audiology and Speech Language Pathology Payment Processor' } }, - Source: 'Compact Connect ' + FromEmailAddress: 'Compact Connect ' } ); }); @@ -225,19 +227,21 @@ describe('EmailNotificationService', () => { Destination: { ToAddresses: ['operations@example.com'] }, - Message: { - Body: { - Html: { + Content: { + Simple: { + Body: { + Html: { + Charset: 'UTF-8', + Data: expect.stringContaining('src=\"https://app.test.compactconnect.org/img/email/compact-connect-logo-final.png\"') + } + }, + Subject: { Charset: 'UTF-8', - Data: expect.stringContaining('src=\"https://app.test.compactconnect.org/img/email/compact-connect-logo-final.png\"') + Data: 'Transactions Failed to Settle for Audiology and Speech Language Pathology Payment Processor' } - }, - Subject: { - Charset: 'UTF-8', - Data: 'Transactions Failed to Settle for Audiology and Speech Language Pathology Payment Processor' } }, - Source: 'Compact Connect ' + FromEmailAddress: 'Compact Connect ' } ); }); @@ -259,19 +263,21 @@ describe('EmailNotificationService', () => { Destination: { ToAddresses: ['specific@example.com'] }, - Message: { - Body: { - Html: { + Content: { + Simple: { + Body: { + Html: { + Charset: 'UTF-8', + Data: expect.stringContaining('') + } + }, + Subject: { Charset: 'UTF-8', - Data: expect.stringContaining('') + Data: 'Your Privilege some-privilege-id is Deactivated' } - }, - Subject: { - Charset: 'UTF-8', - Data: 'Your Privilege some-privilege-id is Deactivated' } }, - Source: 'Compact Connect ' + FromEmailAddress: 'Compact Connect ' } ); }); @@ -305,19 +311,21 @@ describe('EmailNotificationService', () => { Destination: { ToAddresses: ['oh-summary@example.com'] }, - Message: { - Body: { - Html: { + Content: { + Simple: { + Body: { + Html: { + Charset: 'UTF-8', + Data: expect.stringContaining('') + } + }, + Subject: { Charset: 'UTF-8', - Data: expect.stringContaining('') + Data: `A Privilege was Deactivated in the Audiology and Speech Language Pathology Compact` } - }, - Subject: { - Charset: 'UTF-8', - Data: `A Privilege was Deactivated in the Audiology and Speech Language Pathology Compact` } }, - Source: 'Compact Connect ' + FromEmailAddress: 'Compact Connect ' } ); }); @@ -377,8 +385,8 @@ describe('EmailNotificationService', () => { // Verify nodemailer transport was created with correct SES config expect(nodemailer.createTransport).toHaveBeenCalledWith({ SES: { - ses: expect.any(Object), - aws: { SendRawEmailCommand } + sesClient: expect.any(Object), + SendEmailCommand } }); @@ -507,8 +515,8 @@ describe('EmailNotificationService', () => { // Verify nodemailer transport was created with correct SES config expect(nodemailer.createTransport).toHaveBeenCalledWith({ SES: { - ses: expect.any(Object), - aws: { SendRawEmailCommand } + sesClient: expect.any(Object), + SendEmailCommand } }); @@ -611,20 +619,22 @@ describe('EmailNotificationService', () => { Destination: { ToAddresses: ['user@example.com'] }, - Message: { - Body: { - Html: { + Content: { + Simple: { + Body: { + Html: { + Charset: 'UTF-8', + Data: expect.stringContaining( + 'A registration attempt was made in the Compact Connect system ') + } + }, + Subject: { Charset: 'UTF-8', - Data: expect.stringContaining( - 'A registration attempt was made in the Compact Connect system ') + Data: 'Registration Attempt Notification - Compact Connect' } - }, - Subject: { - Charset: 'UTF-8', - Data: 'Registration Attempt Notification - Compact Connect' } }, - Source: 'Compact Connect ' + FromEmailAddress: 'Compact Connect ' } ); }); @@ -641,16 +651,18 @@ describe('EmailNotificationService', () => { Destination: { ToAddresses: ['user@example.com'] }, - Message: { - Body: { - Html: { - Charset: 'UTF-8', - Data: expect.stringContaining('https://app.test.compactconnect.org/Dashboard') - } - }, - Subject: expect.any(Object) + Content: { + Simple: { + Body: { + Html: { + Charset: 'UTF-8', + Data: expect.stringContaining('https://app.test.compactconnect.org/Dashboard') + } + }, + Subject: expect.any(Object) + } }, - Source: 'Compact Connect ' + FromEmailAddress: 'Compact Connect ' } ); }); @@ -667,16 +679,18 @@ describe('EmailNotificationService', () => { Destination: { ToAddresses: ['user@example.com'] }, - Message: { - Body: { - Html: { - Charset: 'UTF-8', - Data: expect.stringContaining('If you originally registered within the past 24 hours, make sure to login with your temporary password sent to this same email address.') - } - }, - Subject: expect.any(Object) + Content: { + Simple: { + Body: { + Html: { + Charset: 'UTF-8', + Data: expect.stringContaining('If you originally registered within the past 24 hours, make sure to login with your temporary password sent to this same email address.') + } + }, + Subject: expect.any(Object) + } }, - Source: 'Compact Connect ' + FromEmailAddress: 'Compact Connect ' } ); }); @@ -725,19 +739,21 @@ describe('EmailNotificationService', () => { Destination: { ToAddresses: ['provider@example.com'] }, - Message: { - Body: { - Html: { + Content: { + Simple: { + Body: { + Html: { + Charset: 'UTF-8', + Data: expect.stringContaining('Privilege Purchase Confirmation') + } + }, + Subject: { Charset: 'UTF-8', - Data: expect.stringContaining('Privilege Purchase Confirmation') + Data: 'Compact Connect Privilege Purchase Confirmation' } - }, - Subject: { - Charset: 'UTF-8', - Data: 'Compact Connect Privilege Purchase Confirmation' } }, - Source: 'Compact Connect ' + FromEmailAddress: 'Compact Connect ' } ); }); @@ -782,24 +798,26 @@ describe('EmailNotificationService', () => { Destination: { ToAddresses: ['newuser@example.com'] }, - Message: { - Body: { - Html: { + Content: { + Simple: { + Body: { + Html: { + Charset: 'UTF-8', + Data: expect.any(String) + } + }, + Subject: { Charset: 'UTF-8', - Data: expect.any(String) + Data: 'Verify Your New Email Address - Compact Connect' } - }, - Subject: { - Charset: 'UTF-8', - Data: 'Verify Your New Email Address - Compact Connect' } }, - Source: 'Compact Connect ' + FromEmailAddress: 'Compact Connect ' }); // Get the actual HTML content for detailed validation const emailCall = mockSESClient.commandCalls(SendEmailCommand)[0]; - const htmlContent = emailCall.args[0].input.Message?.Body?.Html?.Data; + const htmlContent = emailCall.args[0].input.Content?.Simple?.Body?.Html?.Data; expect(htmlContent).toBeDefined(); expect(htmlContent).toContain('Please use the following verification code to complete your email address change'); @@ -823,24 +841,26 @@ describe('EmailNotificationService', () => { Destination: { ToAddresses: ['olduser@example.com'] }, - Message: { - Body: { - Html: { + Content: { + Simple: { + Body: { + Html: { + Charset: 'UTF-8', + Data: expect.any(String) + } + }, + Subject: { Charset: 'UTF-8', - Data: expect.any(String) + Data: 'Email Address Changed - Compact Connect' } - }, - Subject: { - Charset: 'UTF-8', - Data: 'Email Address Changed - Compact Connect' } }, - Source: 'Compact Connect ' + FromEmailAddress: 'Compact Connect ' }); // Get the actual HTML content for detailed validation const emailCall = mockSESClient.commandCalls(SendEmailCommand)[0]; - const htmlContent = emailCall.args[0].input.Message?.Body?.Html?.Data; + const htmlContent = emailCall.args[0].input.Content?.Simple?.Body?.Html?.Data; expect(htmlContent).toBeDefined(); expect(htmlContent).toContain('Please use the new email address to login to your account from now on.'); @@ -867,24 +887,26 @@ describe('EmailNotificationService', () => { Destination: { ToAddresses: ['user@example.com'] }, - Message: { - Body: { - Html: { + Content: { + Simple: { + Body: { + Html: { + Charset: 'UTF-8', + Data: expect.any(String) + } + }, + Subject: { Charset: 'UTF-8', - Data: expect.any(String) + Data: 'Confirm Account Recovery - Compact Connect' } - }, - Subject: { - Charset: 'UTF-8', - Data: 'Confirm Account Recovery - Compact Connect' } }, - Source: 'Compact Connect ' + FromEmailAddress: 'Compact Connect ' }); // Get the actual HTML content for detailed validation const emailCall = mockSESClient.commandCalls(SendEmailCommand)[0]; - const htmlContent = emailCall.args[0].input.Message?.Body?.Html?.Data; + const htmlContent = emailCall.args[0].input.Content?.Simple?.Body?.Html?.Data; expect(htmlContent).toBeDefined(); expect(htmlContent).toContain('A request was made to recover access to your Compact Connect user account.'); @@ -929,7 +951,7 @@ describe('EmailNotificationService', () => { ); const emailCall = mockSESClient.commandCalls(SendEmailCommand)[0]; - const htmlContent = emailCall.args[0].input.Message?.Body?.Html?.Data; + const htmlContent = emailCall.args[0].input.Content?.Simple?.Body?.Html?.Data; // Verify reset password URL is present const expectedResetUrl = 'https://app.test.compactconnect.org/Dashboard?bypass=login-practitioner'; diff --git a/backend/compact-connect/lambdas/nodejs/tests/lib/email/encumbrance-notification-service.test.ts b/backend/compact-connect/lambdas/nodejs/tests/lib/email/encumbrance-notification-service.test.ts index a606e4196..33bb23748 100644 --- a/backend/compact-connect/lambdas/nodejs/tests/lib/email/encumbrance-notification-service.test.ts +++ b/backend/compact-connect/lambdas/nodejs/tests/lib/email/encumbrance-notification-service.test.ts @@ -1,7 +1,7 @@ import { mockClient } from 'aws-sdk-client-mock'; import 'aws-sdk-client-mock-jest'; import { Logger } from '@aws-lambda-powertools/logger'; -import { SendEmailCommand, SESClient } from '@aws-sdk/client-ses'; +import { SendEmailCommand, SESv2Client } from '@aws-sdk/client-sesv2'; import { S3Client } from '@aws-sdk/client-s3'; import { EncumbranceNotificationService } from '../../../lib/email'; import { CompactConfigurationClient } from '../../../lib/compact-configuration-client'; @@ -40,7 +40,7 @@ const SAMPLE_JURISDICTION_CONFIG = { }; const asSESClient = (mock: ReturnType) => - mock as unknown as SESClient; + mock as unknown as SESv2Client; const asS3Client = (mock: ReturnType) => mock as unknown as S3Client; @@ -88,7 +88,7 @@ describe('EncumbranceNotificationService', () => { beforeEach(() => { jest.clearAllMocks(); - mockSESClient = mockClient(SESClient); + mockSESClient = mockClient(SESv2Client); mockS3Client = mockClient(S3Client); mockCompactConfigurationClient = new MockCompactConfigurationClient(); mockJurisdictionClient = { @@ -132,19 +132,21 @@ describe('EncumbranceNotificationService', () => { Destination: { ToAddresses: ['provider@example.com'] }, - Message: { - Body: { - Html: { + Content: { + Simple: { + Body: { + Html: { + Charset: 'UTF-8', + Data: expect.stringContaining('Your Audiologist license in Ohio is encumbered') + } + }, + Subject: { Charset: 'UTF-8', - Data: expect.stringContaining('Your Audiologist license in Ohio is encumbered') + Data: 'Your Audiologist license in Ohio is encumbered' } - }, - Subject: { - Charset: 'UTF-8', - Data: 'Your Audiologist license in Ohio is encumbered' } }, - Source: 'Compact Connect ' + FromEmailAddress: 'Compact Connect ' }); }); @@ -180,19 +182,21 @@ describe('EncumbranceNotificationService', () => { Destination: { ToAddresses: ['oh-adverse@example.com'] }, - Message: { - Body: { - Html: { + Content: { + Simple: { + Body: { + Html: { + Charset: 'UTF-8', + Data: expect.stringContaining('License Encumbrance Notification - John Doe') + } + }, + Subject: { Charset: 'UTF-8', - Data: expect.stringContaining('License Encumbrance Notification - John Doe') + Data: 'License Encumbrance Notification - John Doe' } - }, - Subject: { - Charset: 'UTF-8', - Data: 'License Encumbrance Notification - John Doe' } }, - Source: 'Compact Connect ' + FromEmailAddress: 'Compact Connect ' }); }); @@ -256,19 +260,21 @@ describe('EncumbranceNotificationService', () => { Destination: { ToAddresses: ['provider@example.com'] }, - Message: { - Body: { - Html: { + Content: { + Simple: { + Body: { + Html: { + Charset: 'UTF-8', + Data: expect.stringContaining('Your Audiologist license in Ohio is no longer encumbered') + } + }, + Subject: { Charset: 'UTF-8', - Data: expect.stringContaining('Your Audiologist license in Ohio is no longer encumbered') + Data: 'Your Audiologist license in Ohio is no longer encumbered' } - }, - Subject: { - Charset: 'UTF-8', - Data: 'Your Audiologist license in Ohio is no longer encumbered' } }, - Source: 'Compact Connect ' + FromEmailAddress: 'Compact Connect ' } ); }); @@ -310,19 +316,21 @@ describe('EncumbranceNotificationService', () => { Destination: { ToAddresses: ['state-adverse@example.com'] }, - Message: { - Body: { - Html: { + Content: { + Simple: { + Body: { + Html: { + Charset: 'UTF-8', + Data: expect.stringContaining('License Encumbrance Lifted Notification - John Doe') + } + }, + Subject: { Charset: 'UTF-8', - Data: expect.stringContaining('License Encumbrance Lifted Notification - John Doe') + Data: 'License Encumbrance Lifted Notification - John Doe' } - }, - Subject: { - Charset: 'UTF-8', - Data: 'License Encumbrance Lifted Notification - John Doe' } }, - Source: 'Compact Connect ' + FromEmailAddress: 'Compact Connect ' } ); }); @@ -387,19 +395,21 @@ describe('EncumbranceNotificationService', () => { Destination: { ToAddresses: ['provider@example.com'] }, - Message: { - Body: { - Html: { + Content: { + Simple: { + Body: { + Html: { + Charset: 'UTF-8', + Data: expect.stringContaining('Your Audiologist privilege in Ohio is encumbered') + } + }, + Subject: { Charset: 'UTF-8', - Data: expect.stringContaining('Your Audiologist privilege in Ohio is encumbered') + Data: 'Your Audiologist privilege in Ohio is encumbered' } - }, - Subject: { - Charset: 'UTF-8', - Data: 'Your Audiologist privilege in Ohio is encumbered' } }, - Source: 'Compact Connect ' + FromEmailAddress: 'Compact Connect ' } ); }); @@ -441,24 +451,26 @@ describe('EncumbranceNotificationService', () => { Destination: { ToAddresses: ['state-adverse@example.com'] }, - Message: { - Body: { - Html: { + Content: { + Simple: { + Body: { + Html: { + Charset: 'UTF-8', + Data: expect.stringContaining('Privilege Encumbrance Notification - John Doe') + } + }, + Subject: { Charset: 'UTF-8', - Data: expect.stringContaining('Privilege Encumbrance Notification - John Doe') + Data: 'Privilege Encumbrance Notification - John Doe' } - }, - Subject: { - Charset: 'UTF-8', - Data: 'Privilege Encumbrance Notification - John Doe' } }, - Source: 'Compact Connect ' + FromEmailAddress: 'Compact Connect ' } ); const emailContent = mockSESClient.commandCalls(SendEmailCommand)[0] - .args[0].input.Message?.Body?.Html?.Data; + .args[0].input.Content?.Simple?.Body?.Html?.Data; expect(emailContent).toContain('This encumbrance restricts the provider's ability to practice in Ohio under the Audiology and Speech Language Pathology compact'); }); @@ -476,7 +488,7 @@ describe('EncumbranceNotificationService', () => { ); const emailContent = mockSESClient.commandCalls(SendEmailCommand)[0].args[0] - .input.Message?.Body?.Html?.Data; + .input.Content?.Simple?.Body?.Html?.Data; expect(emailContent).toContain('Provider Details: https://app.test.compactconnect.org/aslp/Licensing/provider-123'); expect(emailContent).toContain('This encumbrance restricts the provider's ability to practice in Ohio under the Audiology and Speech Language Pathology compact'); @@ -542,19 +554,21 @@ describe('EncumbranceNotificationService', () => { Destination: { ToAddresses: ['provider@example.com'] }, - Message: { - Body: { - Html: { + Content: { + Simple: { + Body: { + Html: { + Charset: 'UTF-8', + Data: expect.stringContaining('Your Audiologist privilege in Ohio is no longer encumbered') + } + }, + Subject: { Charset: 'UTF-8', - Data: expect.stringContaining('Your Audiologist privilege in Ohio is no longer encumbered') + Data: 'Your Audiologist privilege in Ohio is no longer encumbered' } - }, - Subject: { - Charset: 'UTF-8', - Data: 'Your Audiologist privilege in Ohio is no longer encumbered' } }, - Source: 'Compact Connect ' + FromEmailAddress: 'Compact Connect ' } ); }); @@ -596,24 +610,26 @@ describe('EncumbranceNotificationService', () => { Destination: { ToAddresses: ['state-adverse@example.com'] }, - Message: { - Body: { - Html: { + Content: { + Simple: { + Body: { + Html: { + Charset: 'UTF-8', + Data: expect.stringContaining('Privilege Encumbrance Lifted Notification - John Doe') + } + }, + Subject: { Charset: 'UTF-8', - Data: expect.stringContaining('Privilege Encumbrance Lifted Notification - John Doe') + Data: 'Privilege Encumbrance Lifted Notification - John Doe' } - }, - Subject: { - Charset: 'UTF-8', - Data: 'Privilege Encumbrance Lifted Notification - John Doe' } }, - Source: 'Compact Connect ' + FromEmailAddress: 'Compact Connect ' } ); const emailContent = mockSESClient.commandCalls(SendEmailCommand)[0] - .args[0].input.Message?.Body?.Html?.Data; + .args[0].input.Content?.Simple?.Body?.Html?.Data; expect(emailContent).toContain('Provider Details: https://app.test.compactconnect.org/aslp/Licensing/provider-123'); expect(emailContent).toContain('The encumbrance no longer restricts the provider's ability to practice in Ohio under the Audiology and Speech Language Pathology compact'); diff --git a/backend/compact-connect/lambdas/nodejs/tests/lib/email/ingest-event-email-service.test.ts b/backend/compact-connect/lambdas/nodejs/tests/lib/email/ingest-event-email-service.test.ts index e2ed5a746..d3516234f 100644 --- a/backend/compact-connect/lambdas/nodejs/tests/lib/email/ingest-event-email-service.test.ts +++ b/backend/compact-connect/lambdas/nodejs/tests/lib/email/ingest-event-email-service.test.ts @@ -1,7 +1,7 @@ import { mockClient } from 'aws-sdk-client-mock'; import 'aws-sdk-client-mock-jest'; import { Logger } from '@aws-lambda-powertools/logger'; -import { SendEmailCommand, SESClient } from '@aws-sdk/client-ses'; +import { SendEmailCommand, SESv2Client } from '@aws-sdk/client-sesv2'; import { IngestEventEmailService } from '../../../lib/email'; import { EmailTemplateCapture } from '../../utils/email-template-capture'; import { TReaderDocument } from '@jusdino-ia/email-builder'; @@ -13,7 +13,7 @@ import { import { describe, it, expect, beforeEach, beforeAll, afterAll, jest } from '@jest/globals'; const asSESClient = (mock: ReturnType) => - mock as unknown as SESClient; + mock as unknown as SESv2Client; describe('IngestEventEmailService', () => { let emailService: IngestEventEmailService; @@ -42,7 +42,7 @@ describe('IngestEventEmailService', () => { beforeEach(() => { jest.clearAllMocks(); - mockSESClient = mockClient(SESClient); + mockSESClient = mockClient(SESv2Client); // Reset environment variables process.env.FROM_ADDRESS = 'noreply@example.org'; @@ -97,19 +97,21 @@ describe('IngestEventEmailService', () => { Destination: { ToAddresses: ['operations@example.com'] }, - Message: { - Body: { - Html: { + Content: { + Simple: { + Body: { + Html: { + Charset: 'UTF-8', + Data: expect.stringContaining('') + } + }, + Subject: { Charset: 'UTF-8', - Data: expect.stringContaining('') + Data: 'License Data Error Summary: aslp / Ohio' } - }, - Subject: { - Charset: 'UTF-8', - Data: 'License Data Error Summary: aslp / Ohio' } }, - Source: 'Compact Connect ' + FromEmailAddress: 'Compact Connect ' } ); }); @@ -142,19 +144,21 @@ describe('IngestEventEmailService', () => { Destination: { ToAddresses: ['operations@example.com'] }, - Message: { - Body: { - Html: { + Content: { + Simple: { + Body: { + Html: { + Charset: 'UTF-8', + Data: expect.stringContaining('') + } + }, + Subject: { Charset: 'UTF-8', - Data: expect.stringContaining('') + Data: 'License Data Summary: aslp / Ohio' } - }, - Subject: { - Charset: 'UTF-8', - Data: 'License Data Summary: aslp / Ohio' } }, - Source: 'Compact Connect ' + FromEmailAddress: 'Compact Connect ' } ); }); @@ -173,19 +177,21 @@ describe('IngestEventEmailService', () => { Destination: { ToAddresses: ['operations@example.com'] }, - Message: { - Body: { - Html: { + Content: { + Simple: { + Body: { + Html: { + Charset: 'UTF-8', + Data: expect.stringContaining('src=\"https://app.test.compactconnect.org/img/email/ico-noupdates@2x.png\"') + } + }, + Subject: { Charset: 'UTF-8', - Data: expect.stringContaining('src=\"https://app.test.compactconnect.org/img/email/ico-noupdates@2x.png\"') + Data: 'No License Updates for Last 7 Days: aslp / Ohio' } - }, - Subject: { - Charset: 'UTF-8', - Data: 'No License Updates for Last 7 Days: aslp / Ohio' } }, - Source: 'Compact Connect ' + FromEmailAddress: 'Compact Connect ' } ); }); diff --git a/backend/compact-connect/lambdas/nodejs/yarn.lock b/backend/compact-connect/lambdas/nodejs/yarn.lock index 1d84fea78..a63ad9fc4 100644 --- a/backend/compact-connect/lambdas/nodejs/yarn.lock +++ b/backend/compact-connect/lambdas/nodejs/yarn.lock @@ -91,599 +91,562 @@ "@aws-lambda-powertools/commons" "^2.11.0" lodash.merge "^4.6.2" -"@aws-sdk/client-dynamodb@^3.682.0": - version "3.705.0" - resolved "https://registry.npmjs.org/@aws-sdk/client-dynamodb/-/client-dynamodb-3.705.0.tgz" - integrity sha512-uHmjzK4/r6KiXSMofSRmLdGb0N+X42yoTZN9YrQK2PxPMdLjh7JCGv4thlLcZP1NBHPfFxsEh61kqf8+1SfzgQ== +"@aws-sdk/client-dynamodb@^3.901.0": + version "3.902.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/client-dynamodb/-/client-dynamodb-3.902.0.tgz#74e73074c79f967ae4e8d23a20d272eca217f6a9" + integrity sha512-WoBzn00MEvnhKFkrFPpqDjHxaqOriwJ2N00/GsoppHiMMPqinN53wB/phur7BsvhQCZZMbnIWrslcZcQxOzFLg== dependencies: "@aws-crypto/sha256-browser" "5.2.0" "@aws-crypto/sha256-js" "5.2.0" - "@aws-sdk/client-sso-oidc" "3.699.0" - "@aws-sdk/client-sts" "3.699.0" - "@aws-sdk/core" "3.696.0" - "@aws-sdk/credential-provider-node" "3.699.0" - "@aws-sdk/middleware-endpoint-discovery" "3.696.0" - "@aws-sdk/middleware-host-header" "3.696.0" - "@aws-sdk/middleware-logger" "3.696.0" - "@aws-sdk/middleware-recursion-detection" "3.696.0" - "@aws-sdk/middleware-user-agent" "3.696.0" - "@aws-sdk/region-config-resolver" "3.696.0" - "@aws-sdk/types" "3.696.0" - "@aws-sdk/util-endpoints" "3.696.0" - "@aws-sdk/util-user-agent-browser" "3.696.0" - "@aws-sdk/util-user-agent-node" "3.696.0" - "@smithy/config-resolver" "^3.0.12" - "@smithy/core" "^2.5.3" - "@smithy/fetch-http-handler" "^4.1.1" - "@smithy/hash-node" "^3.0.10" - "@smithy/invalid-dependency" "^3.0.10" - "@smithy/middleware-content-length" "^3.0.12" - "@smithy/middleware-endpoint" "^3.2.3" - "@smithy/middleware-retry" "^3.0.27" - "@smithy/middleware-serde" "^3.0.10" - "@smithy/middleware-stack" "^3.0.10" - "@smithy/node-config-provider" "^3.1.11" - "@smithy/node-http-handler" "^3.3.1" - "@smithy/protocol-http" "^4.1.7" - "@smithy/smithy-client" "^3.4.4" - "@smithy/types" "^3.7.1" - "@smithy/url-parser" "^3.0.10" - "@smithy/util-base64" "^3.0.0" - "@smithy/util-body-length-browser" "^3.0.0" - "@smithy/util-body-length-node" "^3.0.0" - "@smithy/util-defaults-mode-browser" "^3.0.27" - "@smithy/util-defaults-mode-node" "^3.0.27" - "@smithy/util-endpoints" "^2.1.6" - "@smithy/util-middleware" "^3.0.10" - "@smithy/util-retry" "^3.0.10" - "@smithy/util-utf8" "^3.0.0" - "@smithy/util-waiter" "^3.1.9" - "@types/uuid" "^9.0.1" + "@aws-sdk/core" "3.901.0" + "@aws-sdk/credential-provider-node" "3.901.0" + "@aws-sdk/middleware-endpoint-discovery" "3.901.0" + "@aws-sdk/middleware-host-header" "3.901.0" + "@aws-sdk/middleware-logger" "3.901.0" + "@aws-sdk/middleware-recursion-detection" "3.901.0" + "@aws-sdk/middleware-user-agent" "3.901.0" + "@aws-sdk/region-config-resolver" "3.901.0" + "@aws-sdk/types" "3.901.0" + "@aws-sdk/util-endpoints" "3.901.0" + "@aws-sdk/util-user-agent-browser" "3.901.0" + "@aws-sdk/util-user-agent-node" "3.901.0" + "@smithy/config-resolver" "^4.3.0" + "@smithy/core" "^3.14.0" + "@smithy/fetch-http-handler" "^5.3.0" + "@smithy/hash-node" "^4.2.0" + "@smithy/invalid-dependency" "^4.2.0" + "@smithy/middleware-content-length" "^4.2.0" + "@smithy/middleware-endpoint" "^4.3.0" + "@smithy/middleware-retry" "^4.4.0" + "@smithy/middleware-serde" "^4.2.0" + "@smithy/middleware-stack" "^4.2.0" + "@smithy/node-config-provider" "^4.3.0" + "@smithy/node-http-handler" "^4.3.0" + "@smithy/protocol-http" "^5.3.0" + "@smithy/smithy-client" "^4.7.0" + "@smithy/types" "^4.6.0" + "@smithy/url-parser" "^4.2.0" + "@smithy/util-base64" "^4.2.0" + "@smithy/util-body-length-browser" "^4.2.0" + "@smithy/util-body-length-node" "^4.2.0" + "@smithy/util-defaults-mode-browser" "^4.2.0" + "@smithy/util-defaults-mode-node" "^4.2.0" + "@smithy/util-endpoints" "^3.2.0" + "@smithy/util-middleware" "^4.2.0" + "@smithy/util-retry" "^4.2.0" + "@smithy/util-utf8" "^4.2.0" + "@smithy/util-waiter" "^4.2.0" + "@smithy/uuid" "^1.1.0" tslib "^2.6.2" - uuid "^9.0.1" -"@aws-sdk/client-s3@^3.682.0": - version "3.705.0" - resolved "https://registry.npmjs.org/@aws-sdk/client-s3/-/client-s3-3.705.0.tgz" - integrity sha512-Fm0Cbc4zr0yG0DnNycz7ywlL5tQFdLSb7xCIPfzrxJb3YQiTXWxH5eu61SSsP/Z6RBNRolmRPvst/iNgX0fWvA== +"@aws-sdk/client-s3@^3.901.0": + version "3.901.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/client-s3/-/client-s3-3.901.0.tgz#42e9faf3b9943c56e86ade41a36950dfb231d095" + integrity sha512-wyKhZ51ur1tFuguZ6PgrUsot9KopqD0Tmxw8O8P/N3suQDxFPr0Yo7Y77ezDRDZQ95Ml3C0jlvx79HCo8VxdWA== dependencies: "@aws-crypto/sha1-browser" "5.2.0" "@aws-crypto/sha256-browser" "5.2.0" "@aws-crypto/sha256-js" "5.2.0" - "@aws-sdk/client-sso-oidc" "3.699.0" - "@aws-sdk/client-sts" "3.699.0" - "@aws-sdk/core" "3.696.0" - "@aws-sdk/credential-provider-node" "3.699.0" - "@aws-sdk/middleware-bucket-endpoint" "3.696.0" - "@aws-sdk/middleware-expect-continue" "3.696.0" - "@aws-sdk/middleware-flexible-checksums" "3.701.0" - "@aws-sdk/middleware-host-header" "3.696.0" - "@aws-sdk/middleware-location-constraint" "3.696.0" - "@aws-sdk/middleware-logger" "3.696.0" - "@aws-sdk/middleware-recursion-detection" "3.696.0" - "@aws-sdk/middleware-sdk-s3" "3.696.0" - "@aws-sdk/middleware-ssec" "3.696.0" - "@aws-sdk/middleware-user-agent" "3.696.0" - "@aws-sdk/region-config-resolver" "3.696.0" - "@aws-sdk/signature-v4-multi-region" "3.696.0" - "@aws-sdk/types" "3.696.0" - "@aws-sdk/util-endpoints" "3.696.0" - "@aws-sdk/util-user-agent-browser" "3.696.0" - "@aws-sdk/util-user-agent-node" "3.696.0" - "@aws-sdk/xml-builder" "3.696.0" - "@smithy/config-resolver" "^3.0.12" - "@smithy/core" "^2.5.3" - "@smithy/eventstream-serde-browser" "^3.0.13" - "@smithy/eventstream-serde-config-resolver" "^3.0.10" - "@smithy/eventstream-serde-node" "^3.0.12" - "@smithy/fetch-http-handler" "^4.1.1" - "@smithy/hash-blob-browser" "^3.1.9" - "@smithy/hash-node" "^3.0.10" - "@smithy/hash-stream-node" "^3.1.9" - "@smithy/invalid-dependency" "^3.0.10" - "@smithy/md5-js" "^3.0.10" - "@smithy/middleware-content-length" "^3.0.12" - "@smithy/middleware-endpoint" "^3.2.3" - "@smithy/middleware-retry" "^3.0.27" - "@smithy/middleware-serde" "^3.0.10" - "@smithy/middleware-stack" "^3.0.10" - "@smithy/node-config-provider" "^3.1.11" - "@smithy/node-http-handler" "^3.3.1" - "@smithy/protocol-http" "^4.1.7" - "@smithy/smithy-client" "^3.4.4" - "@smithy/types" "^3.7.1" - "@smithy/url-parser" "^3.0.10" - "@smithy/util-base64" "^3.0.0" - "@smithy/util-body-length-browser" "^3.0.0" - "@smithy/util-body-length-node" "^3.0.0" - "@smithy/util-defaults-mode-browser" "^3.0.27" - "@smithy/util-defaults-mode-node" "^3.0.27" - "@smithy/util-endpoints" "^2.1.6" - "@smithy/util-middleware" "^3.0.10" - "@smithy/util-retry" "^3.0.10" - "@smithy/util-stream" "^3.3.1" - "@smithy/util-utf8" "^3.0.0" - "@smithy/util-waiter" "^3.1.9" + "@aws-sdk/core" "3.901.0" + "@aws-sdk/credential-provider-node" "3.901.0" + "@aws-sdk/middleware-bucket-endpoint" "3.901.0" + "@aws-sdk/middleware-expect-continue" "3.901.0" + "@aws-sdk/middleware-flexible-checksums" "3.901.0" + "@aws-sdk/middleware-host-header" "3.901.0" + "@aws-sdk/middleware-location-constraint" "3.901.0" + "@aws-sdk/middleware-logger" "3.901.0" + "@aws-sdk/middleware-recursion-detection" "3.901.0" + "@aws-sdk/middleware-sdk-s3" "3.901.0" + "@aws-sdk/middleware-ssec" "3.901.0" + "@aws-sdk/middleware-user-agent" "3.901.0" + "@aws-sdk/region-config-resolver" "3.901.0" + "@aws-sdk/signature-v4-multi-region" "3.901.0" + "@aws-sdk/types" "3.901.0" + "@aws-sdk/util-endpoints" "3.901.0" + "@aws-sdk/util-user-agent-browser" "3.901.0" + "@aws-sdk/util-user-agent-node" "3.901.0" + "@aws-sdk/xml-builder" "3.901.0" + "@smithy/config-resolver" "^4.3.0" + "@smithy/core" "^3.14.0" + "@smithy/eventstream-serde-browser" "^4.2.0" + "@smithy/eventstream-serde-config-resolver" "^4.3.0" + "@smithy/eventstream-serde-node" "^4.2.0" + "@smithy/fetch-http-handler" "^5.3.0" + "@smithy/hash-blob-browser" "^4.2.0" + "@smithy/hash-node" "^4.2.0" + "@smithy/hash-stream-node" "^4.2.0" + "@smithy/invalid-dependency" "^4.2.0" + "@smithy/md5-js" "^4.2.0" + "@smithy/middleware-content-length" "^4.2.0" + "@smithy/middleware-endpoint" "^4.3.0" + "@smithy/middleware-retry" "^4.4.0" + "@smithy/middleware-serde" "^4.2.0" + "@smithy/middleware-stack" "^4.2.0" + "@smithy/node-config-provider" "^4.3.0" + "@smithy/node-http-handler" "^4.3.0" + "@smithy/protocol-http" "^5.3.0" + "@smithy/smithy-client" "^4.7.0" + "@smithy/types" "^4.6.0" + "@smithy/url-parser" "^4.2.0" + "@smithy/util-base64" "^4.2.0" + "@smithy/util-body-length-browser" "^4.2.0" + "@smithy/util-body-length-node" "^4.2.0" + "@smithy/util-defaults-mode-browser" "^4.2.0" + "@smithy/util-defaults-mode-node" "^4.2.0" + "@smithy/util-endpoints" "^3.2.0" + "@smithy/util-middleware" "^4.2.0" + "@smithy/util-retry" "^4.2.0" + "@smithy/util-stream" "^4.4.0" + "@smithy/util-utf8" "^4.2.0" + "@smithy/util-waiter" "^4.2.0" + "@smithy/uuid" "^1.1.0" tslib "^2.6.2" -"@aws-sdk/client-ses@^3.682.0": - version "3.699.0" - resolved "https://registry.npmjs.org/@aws-sdk/client-ses/-/client-ses-3.699.0.tgz" - integrity sha512-prpkr2jnhD2KsinQMBdX2wvSpNxFm9d02EUR4L78yxjg2oppXmu/cBjWdlVrSkqqE2EYfcHo0JV2WmRZZC1VyQ== +"@aws-sdk/client-sesv2@^3.901.0": + version "3.901.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/client-sesv2/-/client-sesv2-3.901.0.tgz#0e36caff43ff8b4503174132140a91b128233afc" + integrity sha512-xCS2qZlvgbXKZbJW8XgU8OEAL7BJyVqJ5yODOQxa1TJFZ/+wEhik9XZtULjNnQqa29sJDpPltuSDG1aDG2OUxQ== dependencies: "@aws-crypto/sha256-browser" "5.2.0" "@aws-crypto/sha256-js" "5.2.0" - "@aws-sdk/client-sso-oidc" "3.699.0" - "@aws-sdk/client-sts" "3.699.0" - "@aws-sdk/core" "3.696.0" - "@aws-sdk/credential-provider-node" "3.699.0" - "@aws-sdk/middleware-host-header" "3.696.0" - "@aws-sdk/middleware-logger" "3.696.0" - "@aws-sdk/middleware-recursion-detection" "3.696.0" - "@aws-sdk/middleware-user-agent" "3.696.0" - "@aws-sdk/region-config-resolver" "3.696.0" - "@aws-sdk/types" "3.696.0" - "@aws-sdk/util-endpoints" "3.696.0" - "@aws-sdk/util-user-agent-browser" "3.696.0" - "@aws-sdk/util-user-agent-node" "3.696.0" - "@smithy/config-resolver" "^3.0.12" - "@smithy/core" "^2.5.3" - "@smithy/fetch-http-handler" "^4.1.1" - "@smithy/hash-node" "^3.0.10" - "@smithy/invalid-dependency" "^3.0.10" - "@smithy/middleware-content-length" "^3.0.12" - "@smithy/middleware-endpoint" "^3.2.3" - "@smithy/middleware-retry" "^3.0.27" - "@smithy/middleware-serde" "^3.0.10" - "@smithy/middleware-stack" "^3.0.10" - "@smithy/node-config-provider" "^3.1.11" - "@smithy/node-http-handler" "^3.3.1" - "@smithy/protocol-http" "^4.1.7" - "@smithy/smithy-client" "^3.4.4" - "@smithy/types" "^3.7.1" - "@smithy/url-parser" "^3.0.10" - "@smithy/util-base64" "^3.0.0" - "@smithy/util-body-length-browser" "^3.0.0" - "@smithy/util-body-length-node" "^3.0.0" - "@smithy/util-defaults-mode-browser" "^3.0.27" - "@smithy/util-defaults-mode-node" "^3.0.27" - "@smithy/util-endpoints" "^2.1.6" - "@smithy/util-middleware" "^3.0.10" - "@smithy/util-retry" "^3.0.10" - "@smithy/util-utf8" "^3.0.0" - "@smithy/util-waiter" "^3.1.9" + "@aws-sdk/core" "3.901.0" + "@aws-sdk/credential-provider-node" "3.901.0" + "@aws-sdk/middleware-host-header" "3.901.0" + "@aws-sdk/middleware-logger" "3.901.0" + "@aws-sdk/middleware-recursion-detection" "3.901.0" + "@aws-sdk/middleware-user-agent" "3.901.0" + "@aws-sdk/region-config-resolver" "3.901.0" + "@aws-sdk/signature-v4-multi-region" "3.901.0" + "@aws-sdk/types" "3.901.0" + "@aws-sdk/util-endpoints" "3.901.0" + "@aws-sdk/util-user-agent-browser" "3.901.0" + "@aws-sdk/util-user-agent-node" "3.901.0" + "@smithy/config-resolver" "^4.3.0" + "@smithy/core" "^3.14.0" + "@smithy/fetch-http-handler" "^5.3.0" + "@smithy/hash-node" "^4.2.0" + "@smithy/invalid-dependency" "^4.2.0" + "@smithy/middleware-content-length" "^4.2.0" + "@smithy/middleware-endpoint" "^4.3.0" + "@smithy/middleware-retry" "^4.4.0" + "@smithy/middleware-serde" "^4.2.0" + "@smithy/middleware-stack" "^4.2.0" + "@smithy/node-config-provider" "^4.3.0" + "@smithy/node-http-handler" "^4.3.0" + "@smithy/protocol-http" "^5.3.0" + "@smithy/smithy-client" "^4.7.0" + "@smithy/types" "^4.6.0" + "@smithy/url-parser" "^4.2.0" + "@smithy/util-base64" "^4.2.0" + "@smithy/util-body-length-browser" "^4.2.0" + "@smithy/util-body-length-node" "^4.2.0" + "@smithy/util-defaults-mode-browser" "^4.2.0" + "@smithy/util-defaults-mode-node" "^4.2.0" + "@smithy/util-endpoints" "^3.2.0" + "@smithy/util-middleware" "^4.2.0" + "@smithy/util-retry" "^4.2.0" + "@smithy/util-utf8" "^4.2.0" tslib "^2.6.2" -"@aws-sdk/client-sso-oidc@3.699.0": - version "3.699.0" - resolved "https://registry.npmjs.org/@aws-sdk/client-sso-oidc/-/client-sso-oidc-3.699.0.tgz" - integrity sha512-u8a1GorY5D1l+4FQAf4XBUC1T10/t7neuwT21r0ymrtMFSK2a9QqVHKMoLkvavAwyhJnARSBM9/UQC797PFOFw== +"@aws-sdk/client-sso@3.901.0": + version "3.901.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/client-sso/-/client-sso-3.901.0.tgz#bad08910097ffa0458c2fe662dd4f8439c6e7eeb" + integrity sha512-sGyDjjkJ7ppaE+bAKL/Q5IvVCxtoyBIzN+7+hWTS/mUxWJ9EOq9238IqmVIIK6sYNIzEf9yhobfMARasPYVTNg== dependencies: "@aws-crypto/sha256-browser" "5.2.0" "@aws-crypto/sha256-js" "5.2.0" - "@aws-sdk/core" "3.696.0" - "@aws-sdk/credential-provider-node" "3.699.0" - "@aws-sdk/middleware-host-header" "3.696.0" - "@aws-sdk/middleware-logger" "3.696.0" - "@aws-sdk/middleware-recursion-detection" "3.696.0" - "@aws-sdk/middleware-user-agent" "3.696.0" - "@aws-sdk/region-config-resolver" "3.696.0" - "@aws-sdk/types" "3.696.0" - "@aws-sdk/util-endpoints" "3.696.0" - "@aws-sdk/util-user-agent-browser" "3.696.0" - "@aws-sdk/util-user-agent-node" "3.696.0" - "@smithy/config-resolver" "^3.0.12" - "@smithy/core" "^2.5.3" - "@smithy/fetch-http-handler" "^4.1.1" - "@smithy/hash-node" "^3.0.10" - "@smithy/invalid-dependency" "^3.0.10" - "@smithy/middleware-content-length" "^3.0.12" - "@smithy/middleware-endpoint" "^3.2.3" - "@smithy/middleware-retry" "^3.0.27" - "@smithy/middleware-serde" "^3.0.10" - "@smithy/middleware-stack" "^3.0.10" - "@smithy/node-config-provider" "^3.1.11" - "@smithy/node-http-handler" "^3.3.1" - "@smithy/protocol-http" "^4.1.7" - "@smithy/smithy-client" "^3.4.4" - "@smithy/types" "^3.7.1" - "@smithy/url-parser" "^3.0.10" - "@smithy/util-base64" "^3.0.0" - "@smithy/util-body-length-browser" "^3.0.0" - "@smithy/util-body-length-node" "^3.0.0" - "@smithy/util-defaults-mode-browser" "^3.0.27" - "@smithy/util-defaults-mode-node" "^3.0.27" - "@smithy/util-endpoints" "^2.1.6" - "@smithy/util-middleware" "^3.0.10" - "@smithy/util-retry" "^3.0.10" - "@smithy/util-utf8" "^3.0.0" + "@aws-sdk/core" "3.901.0" + "@aws-sdk/middleware-host-header" "3.901.0" + "@aws-sdk/middleware-logger" "3.901.0" + "@aws-sdk/middleware-recursion-detection" "3.901.0" + "@aws-sdk/middleware-user-agent" "3.901.0" + "@aws-sdk/region-config-resolver" "3.901.0" + "@aws-sdk/types" "3.901.0" + "@aws-sdk/util-endpoints" "3.901.0" + "@aws-sdk/util-user-agent-browser" "3.901.0" + "@aws-sdk/util-user-agent-node" "3.901.0" + "@smithy/config-resolver" "^4.3.0" + "@smithy/core" "^3.14.0" + "@smithy/fetch-http-handler" "^5.3.0" + "@smithy/hash-node" "^4.2.0" + "@smithy/invalid-dependency" "^4.2.0" + "@smithy/middleware-content-length" "^4.2.0" + "@smithy/middleware-endpoint" "^4.3.0" + "@smithy/middleware-retry" "^4.4.0" + "@smithy/middleware-serde" "^4.2.0" + "@smithy/middleware-stack" "^4.2.0" + "@smithy/node-config-provider" "^4.3.0" + "@smithy/node-http-handler" "^4.3.0" + "@smithy/protocol-http" "^5.3.0" + "@smithy/smithy-client" "^4.7.0" + "@smithy/types" "^4.6.0" + "@smithy/url-parser" "^4.2.0" + "@smithy/util-base64" "^4.2.0" + "@smithy/util-body-length-browser" "^4.2.0" + "@smithy/util-body-length-node" "^4.2.0" + "@smithy/util-defaults-mode-browser" "^4.2.0" + "@smithy/util-defaults-mode-node" "^4.2.0" + "@smithy/util-endpoints" "^3.2.0" + "@smithy/util-middleware" "^4.2.0" + "@smithy/util-retry" "^4.2.0" + "@smithy/util-utf8" "^4.2.0" tslib "^2.6.2" -"@aws-sdk/client-sso@3.696.0": - version "3.696.0" - resolved "https://registry.npmjs.org/@aws-sdk/client-sso/-/client-sso-3.696.0.tgz" - integrity sha512-q5TTkd08JS0DOkHfUL853tuArf7NrPeqoS5UOvqJho8ibV9Ak/a/HO4kNvy9Nj3cib/toHYHsQIEtecUPSUUrQ== - dependencies: - "@aws-crypto/sha256-browser" "5.2.0" - "@aws-crypto/sha256-js" "5.2.0" - "@aws-sdk/core" "3.696.0" - "@aws-sdk/middleware-host-header" "3.696.0" - "@aws-sdk/middleware-logger" "3.696.0" - "@aws-sdk/middleware-recursion-detection" "3.696.0" - "@aws-sdk/middleware-user-agent" "3.696.0" - "@aws-sdk/region-config-resolver" "3.696.0" - "@aws-sdk/types" "3.696.0" - "@aws-sdk/util-endpoints" "3.696.0" - "@aws-sdk/util-user-agent-browser" "3.696.0" - "@aws-sdk/util-user-agent-node" "3.696.0" - "@smithy/config-resolver" "^3.0.12" - "@smithy/core" "^2.5.3" - "@smithy/fetch-http-handler" "^4.1.1" - "@smithy/hash-node" "^3.0.10" - "@smithy/invalid-dependency" "^3.0.10" - "@smithy/middleware-content-length" "^3.0.12" - "@smithy/middleware-endpoint" "^3.2.3" - "@smithy/middleware-retry" "^3.0.27" - "@smithy/middleware-serde" "^3.0.10" - "@smithy/middleware-stack" "^3.0.10" - "@smithy/node-config-provider" "^3.1.11" - "@smithy/node-http-handler" "^3.3.1" - "@smithy/protocol-http" "^4.1.7" - "@smithy/smithy-client" "^3.4.4" - "@smithy/types" "^3.7.1" - "@smithy/url-parser" "^3.0.10" - "@smithy/util-base64" "^3.0.0" - "@smithy/util-body-length-browser" "^3.0.0" - "@smithy/util-body-length-node" "^3.0.0" - "@smithy/util-defaults-mode-browser" "^3.0.27" - "@smithy/util-defaults-mode-node" "^3.0.27" - "@smithy/util-endpoints" "^2.1.6" - "@smithy/util-middleware" "^3.0.10" - "@smithy/util-retry" "^3.0.10" - "@smithy/util-utf8" "^3.0.0" - tslib "^2.6.2" - -"@aws-sdk/client-sts@3.699.0": - version "3.699.0" - resolved "https://registry.npmjs.org/@aws-sdk/client-sts/-/client-sts-3.699.0.tgz" - integrity sha512-++lsn4x2YXsZPIzFVwv3fSUVM55ZT0WRFmPeNilYIhZClxHLmVAWKH4I55cY9ry60/aTKYjzOXkWwyBKGsGvQg== - dependencies: - "@aws-crypto/sha256-browser" "5.2.0" - "@aws-crypto/sha256-js" "5.2.0" - "@aws-sdk/client-sso-oidc" "3.699.0" - "@aws-sdk/core" "3.696.0" - "@aws-sdk/credential-provider-node" "3.699.0" - "@aws-sdk/middleware-host-header" "3.696.0" - "@aws-sdk/middleware-logger" "3.696.0" - "@aws-sdk/middleware-recursion-detection" "3.696.0" - "@aws-sdk/middleware-user-agent" "3.696.0" - "@aws-sdk/region-config-resolver" "3.696.0" - "@aws-sdk/types" "3.696.0" - "@aws-sdk/util-endpoints" "3.696.0" - "@aws-sdk/util-user-agent-browser" "3.696.0" - "@aws-sdk/util-user-agent-node" "3.696.0" - "@smithy/config-resolver" "^3.0.12" - "@smithy/core" "^2.5.3" - "@smithy/fetch-http-handler" "^4.1.1" - "@smithy/hash-node" "^3.0.10" - "@smithy/invalid-dependency" "^3.0.10" - "@smithy/middleware-content-length" "^3.0.12" - "@smithy/middleware-endpoint" "^3.2.3" - "@smithy/middleware-retry" "^3.0.27" - "@smithy/middleware-serde" "^3.0.10" - "@smithy/middleware-stack" "^3.0.10" - "@smithy/node-config-provider" "^3.1.11" - "@smithy/node-http-handler" "^3.3.1" - "@smithy/protocol-http" "^4.1.7" - "@smithy/smithy-client" "^3.4.4" - "@smithy/types" "^3.7.1" - "@smithy/url-parser" "^3.0.10" - "@smithy/util-base64" "^3.0.0" - "@smithy/util-body-length-browser" "^3.0.0" - "@smithy/util-body-length-node" "^3.0.0" - "@smithy/util-defaults-mode-browser" "^3.0.27" - "@smithy/util-defaults-mode-node" "^3.0.27" - "@smithy/util-endpoints" "^2.1.6" - "@smithy/util-middleware" "^3.0.10" - "@smithy/util-retry" "^3.0.10" - "@smithy/util-utf8" "^3.0.0" - tslib "^2.6.2" - -"@aws-sdk/core@3.696.0": - version "3.696.0" - resolved "https://registry.npmjs.org/@aws-sdk/core/-/core-3.696.0.tgz" - integrity sha512-3c9III1k03DgvRZWg8vhVmfIXPG6hAciN9MzQTzqGngzWAELZF/WONRTRQuDFixVtarQatmLHYVw/atGeA2Byw== - dependencies: - "@aws-sdk/types" "3.696.0" - "@smithy/core" "^2.5.3" - "@smithy/node-config-provider" "^3.1.11" - "@smithy/property-provider" "^3.1.9" - "@smithy/protocol-http" "^4.1.7" - "@smithy/signature-v4" "^4.2.2" - "@smithy/smithy-client" "^3.4.4" - "@smithy/types" "^3.7.1" - "@smithy/util-middleware" "^3.0.10" - fast-xml-parser "4.4.1" +"@aws-sdk/core@3.901.0": + version "3.901.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/core/-/core-3.901.0.tgz#054341ff9ddede525a7bc3881872a97598fe757f" + integrity sha512-brKAc3y64tdhyuEf+OPIUln86bRTqkLgb9xkd6kUdIeA5+qmp/N6amItQz+RN4k4O3kqkCPYnAd3LonTKluobw== + dependencies: + "@aws-sdk/types" "3.901.0" + "@aws-sdk/xml-builder" "3.901.0" + "@smithy/core" "^3.14.0" + "@smithy/node-config-provider" "^4.3.0" + "@smithy/property-provider" "^4.2.0" + "@smithy/protocol-http" "^5.3.0" + "@smithy/signature-v4" "^5.3.0" + "@smithy/smithy-client" "^4.7.0" + "@smithy/types" "^4.6.0" + "@smithy/util-base64" "^4.2.0" + "@smithy/util-middleware" "^4.2.0" + "@smithy/util-utf8" "^4.2.0" tslib "^2.6.2" -"@aws-sdk/credential-provider-env@3.696.0": - version "3.696.0" - resolved "https://registry.npmjs.org/@aws-sdk/credential-provider-env/-/credential-provider-env-3.696.0.tgz" - integrity sha512-T9iMFnJL7YTlESLpVFT3fg1Lkb1lD+oiaIC8KMpepb01gDUBIpj9+Y+pA/cgRWW0yRxmkDXNazAE2qQTVFGJzA== +"@aws-sdk/credential-provider-env@3.901.0": + version "3.901.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/credential-provider-env/-/credential-provider-env-3.901.0.tgz#d3192a091a94931b2fbc2ef82a278d8daea06f43" + integrity sha512-5hAdVl3tBuARh3zX5MLJ1P/d+Kr5kXtDU3xm1pxUEF4xt2XkEEpwiX5fbkNkz2rbh3BCt2gOHsAbh6b3M7n+DA== dependencies: - "@aws-sdk/core" "3.696.0" - "@aws-sdk/types" "3.696.0" - "@smithy/property-provider" "^3.1.9" - "@smithy/types" "^3.7.1" + "@aws-sdk/core" "3.901.0" + "@aws-sdk/types" "3.901.0" + "@smithy/property-provider" "^4.2.0" + "@smithy/types" "^4.6.0" tslib "^2.6.2" -"@aws-sdk/credential-provider-http@3.696.0": - version "3.696.0" - resolved "https://registry.npmjs.org/@aws-sdk/credential-provider-http/-/credential-provider-http-3.696.0.tgz" - integrity sha512-GV6EbvPi2eq1+WgY/o2RFA3P7HGmnkIzCNmhwtALFlqMroLYWKE7PSeHw66Uh1dFQeVESn0/+hiUNhu1mB0emA== - dependencies: - "@aws-sdk/core" "3.696.0" - "@aws-sdk/types" "3.696.0" - "@smithy/fetch-http-handler" "^4.1.1" - "@smithy/node-http-handler" "^3.3.1" - "@smithy/property-provider" "^3.1.9" - "@smithy/protocol-http" "^4.1.7" - "@smithy/smithy-client" "^3.4.4" - "@smithy/types" "^3.7.1" - "@smithy/util-stream" "^3.3.1" +"@aws-sdk/credential-provider-http@3.901.0": + version "3.901.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/credential-provider-http/-/credential-provider-http-3.901.0.tgz#40bbaa9e62431741d8ea7ed31c8e10de75a9ecde" + integrity sha512-Ggr7+0M6QZEsrqRkK7iyJLf4LkIAacAxHz9c4dm9hnDdU7vqrlJm6g73IxMJXWN1bIV7IxfpzB11DsRrB/oNjQ== + dependencies: + "@aws-sdk/core" "3.901.0" + "@aws-sdk/types" "3.901.0" + "@smithy/fetch-http-handler" "^5.3.0" + "@smithy/node-http-handler" "^4.3.0" + "@smithy/property-provider" "^4.2.0" + "@smithy/protocol-http" "^5.3.0" + "@smithy/smithy-client" "^4.7.0" + "@smithy/types" "^4.6.0" + "@smithy/util-stream" "^4.4.0" tslib "^2.6.2" -"@aws-sdk/credential-provider-ini@3.699.0": - version "3.699.0" - resolved "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.699.0.tgz" - integrity sha512-dXmCqjJnKmG37Q+nLjPVu22mNkrGHY8hYoOt3Jo9R2zr5MYV7s/NHsCHr+7E+BZ+tfZYLRPeB1wkpTeHiEcdRw== - dependencies: - "@aws-sdk/core" "3.696.0" - "@aws-sdk/credential-provider-env" "3.696.0" - "@aws-sdk/credential-provider-http" "3.696.0" - "@aws-sdk/credential-provider-process" "3.696.0" - "@aws-sdk/credential-provider-sso" "3.699.0" - "@aws-sdk/credential-provider-web-identity" "3.696.0" - "@aws-sdk/types" "3.696.0" - "@smithy/credential-provider-imds" "^3.2.6" - "@smithy/property-provider" "^3.1.9" - "@smithy/shared-ini-file-loader" "^3.1.10" - "@smithy/types" "^3.7.1" +"@aws-sdk/credential-provider-ini@3.901.0": + version "3.901.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.901.0.tgz#83ada385ae94fed0a362f3be4689cf0a0284847d" + integrity sha512-zxadcDS0hNJgv8n4hFYJNOXyfjaNE1vvqIiF/JzZSQpSSYXzCd+WxXef5bQh+W3giDtRUmkvP5JLbamEFjZKyw== + dependencies: + "@aws-sdk/core" "3.901.0" + "@aws-sdk/credential-provider-env" "3.901.0" + "@aws-sdk/credential-provider-http" "3.901.0" + "@aws-sdk/credential-provider-process" "3.901.0" + "@aws-sdk/credential-provider-sso" "3.901.0" + "@aws-sdk/credential-provider-web-identity" "3.901.0" + "@aws-sdk/nested-clients" "3.901.0" + "@aws-sdk/types" "3.901.0" + "@smithy/credential-provider-imds" "^4.2.0" + "@smithy/property-provider" "^4.2.0" + "@smithy/shared-ini-file-loader" "^4.3.0" + "@smithy/types" "^4.6.0" tslib "^2.6.2" -"@aws-sdk/credential-provider-node@3.699.0": - version "3.699.0" - resolved "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.699.0.tgz" - integrity sha512-MmEmNDo1bBtTgRmdNfdQksXu4uXe66s0p1hi1YPrn1h59Q605eq/xiWbGL6/3KdkViH6eGUuABeV2ODld86ylg== - dependencies: - "@aws-sdk/credential-provider-env" "3.696.0" - "@aws-sdk/credential-provider-http" "3.696.0" - "@aws-sdk/credential-provider-ini" "3.699.0" - "@aws-sdk/credential-provider-process" "3.696.0" - "@aws-sdk/credential-provider-sso" "3.699.0" - "@aws-sdk/credential-provider-web-identity" "3.696.0" - "@aws-sdk/types" "3.696.0" - "@smithy/credential-provider-imds" "^3.2.6" - "@smithy/property-provider" "^3.1.9" - "@smithy/shared-ini-file-loader" "^3.1.10" - "@smithy/types" "^3.7.1" +"@aws-sdk/credential-provider-node@3.901.0": + version "3.901.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/credential-provider-node/-/credential-provider-node-3.901.0.tgz#b48ddc78998e6a96ad14ecec22d81714c59ff6d1" + integrity sha512-dPuFzMF7L1s/lQyT3wDxqLe82PyTH+5o1jdfseTEln64LJMl0ZMWaKX/C1UFNDxaTd35Cgt1bDbjjAWHMiKSFQ== + dependencies: + "@aws-sdk/credential-provider-env" "3.901.0" + "@aws-sdk/credential-provider-http" "3.901.0" + "@aws-sdk/credential-provider-ini" "3.901.0" + "@aws-sdk/credential-provider-process" "3.901.0" + "@aws-sdk/credential-provider-sso" "3.901.0" + "@aws-sdk/credential-provider-web-identity" "3.901.0" + "@aws-sdk/types" "3.901.0" + "@smithy/credential-provider-imds" "^4.2.0" + "@smithy/property-provider" "^4.2.0" + "@smithy/shared-ini-file-loader" "^4.3.0" + "@smithy/types" "^4.6.0" tslib "^2.6.2" -"@aws-sdk/credential-provider-process@3.696.0": - version "3.696.0" - resolved "https://registry.npmjs.org/@aws-sdk/credential-provider-process/-/credential-provider-process-3.696.0.tgz" - integrity sha512-mL1RcFDe9sfmyU5K1nuFkO8UiJXXxLX4JO1gVaDIOvPqwStpUAwi3A1BoeZhWZZNQsiKI810RnYGo0E0WB/hUA== +"@aws-sdk/credential-provider-process@3.901.0": + version "3.901.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/credential-provider-process/-/credential-provider-process-3.901.0.tgz#0e388fe22f357adb9c07b5f4a055eff6ba99dcff" + integrity sha512-/IWgmgM3Cl1wTdJA5HqKMAojxLkYchh5kDuphApxKhupLu6Pu0JBOHU8A5GGeFvOycyaVwosod6zDduINZxe+A== dependencies: - "@aws-sdk/core" "3.696.0" - "@aws-sdk/types" "3.696.0" - "@smithy/property-provider" "^3.1.9" - "@smithy/shared-ini-file-loader" "^3.1.10" - "@smithy/types" "^3.7.1" + "@aws-sdk/core" "3.901.0" + "@aws-sdk/types" "3.901.0" + "@smithy/property-provider" "^4.2.0" + "@smithy/shared-ini-file-loader" "^4.3.0" + "@smithy/types" "^4.6.0" tslib "^2.6.2" -"@aws-sdk/credential-provider-sso@3.699.0": - version "3.699.0" - resolved "https://registry.npmjs.org/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.699.0.tgz" - integrity sha512-Ekp2cZG4pl9D8+uKWm4qO1xcm8/MeiI8f+dnlZm8aQzizeC+aXYy9GyoclSf6daK8KfRPiRfM7ZHBBL5dAfdMA== - dependencies: - "@aws-sdk/client-sso" "3.696.0" - "@aws-sdk/core" "3.696.0" - "@aws-sdk/token-providers" "3.699.0" - "@aws-sdk/types" "3.696.0" - "@smithy/property-provider" "^3.1.9" - "@smithy/shared-ini-file-loader" "^3.1.10" - "@smithy/types" "^3.7.1" +"@aws-sdk/credential-provider-sso@3.901.0": + version "3.901.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.901.0.tgz#b60d8619edeb6b45c79a3f7cc0392a899de44886" + integrity sha512-SjmqZQHmqFSET7+6xcZgtH7yEyh5q53LN87GqwYlJZ6KJ5oNw11acUNEhUOL1xTSJEvaWqwTIkS2zqrzLcM9bw== + dependencies: + "@aws-sdk/client-sso" "3.901.0" + "@aws-sdk/core" "3.901.0" + "@aws-sdk/token-providers" "3.901.0" + "@aws-sdk/types" "3.901.0" + "@smithy/property-provider" "^4.2.0" + "@smithy/shared-ini-file-loader" "^4.3.0" + "@smithy/types" "^4.6.0" tslib "^2.6.2" -"@aws-sdk/credential-provider-web-identity@3.696.0": - version "3.696.0" - resolved "https://registry.npmjs.org/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.696.0.tgz" - integrity sha512-XJ/CVlWChM0VCoc259vWguFUjJDn/QwDqHwbx+K9cg3v6yrqXfK5ai+p/6lx0nQpnk4JzPVeYYxWRpaTsGC9rg== - dependencies: - "@aws-sdk/core" "3.696.0" - "@aws-sdk/types" "3.696.0" - "@smithy/property-provider" "^3.1.9" - "@smithy/types" "^3.7.1" +"@aws-sdk/credential-provider-web-identity@3.901.0": + version "3.901.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.901.0.tgz#512ad0d35e59bc669b41e18479e6b92d62a2d42a" + integrity sha512-NYjy/6NLxH9m01+pfpB4ql8QgAorJcu8tw69kzHwUd/ql6wUDTbC7HcXqtKlIwWjzjgj2BKL7j6SyFapgCuafA== + dependencies: + "@aws-sdk/core" "3.901.0" + "@aws-sdk/nested-clients" "3.901.0" + "@aws-sdk/types" "3.901.0" + "@smithy/property-provider" "^4.2.0" + "@smithy/shared-ini-file-loader" "^4.3.0" + "@smithy/types" "^4.6.0" tslib "^2.6.2" -"@aws-sdk/endpoint-cache@3.693.0": - version "3.693.0" - resolved "https://registry.npmjs.org/@aws-sdk/endpoint-cache/-/endpoint-cache-3.693.0.tgz" - integrity sha512-/zK0ZZncBf5FbTfo8rJMcQIXXk4Ibhe5zEMiwFNivVPR2uNC0+oqfwXz7vjxwY0t6BPE3Bs4h9uFEz4xuGCY6w== +"@aws-sdk/endpoint-cache@3.893.0": + version "3.893.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/endpoint-cache/-/endpoint-cache-3.893.0.tgz#9b50d61d12360bedc2c25f3e52dce5ef48794613" + integrity sha512-KSwTfyLZyNLszz5f/yoLC+LC+CRKpeJii/+zVAy7JUOQsKhSykiRUPYUx7o2Sdc4oJfqqUl26A/jSttKYnYtAA== dependencies: mnemonist "0.38.3" tslib "^2.6.2" -"@aws-sdk/middleware-bucket-endpoint@3.696.0": - version "3.696.0" - resolved "https://registry.npmjs.org/@aws-sdk/middleware-bucket-endpoint/-/middleware-bucket-endpoint-3.696.0.tgz" - integrity sha512-V07jishKHUS5heRNGFpCWCSTjRJyQLynS/ncUeE8ZYtG66StOOQWftTwDfFOSoXlIqrXgb4oT9atryzXq7Z4LQ== - dependencies: - "@aws-sdk/types" "3.696.0" - "@aws-sdk/util-arn-parser" "3.693.0" - "@smithy/node-config-provider" "^3.1.11" - "@smithy/protocol-http" "^4.1.7" - "@smithy/types" "^3.7.1" - "@smithy/util-config-provider" "^3.0.0" +"@aws-sdk/middleware-bucket-endpoint@3.901.0": + version "3.901.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/middleware-bucket-endpoint/-/middleware-bucket-endpoint-3.901.0.tgz#5b7f740cff9f91d21084b666be225876d72e634b" + integrity sha512-mPF3N6eZlVs9G8aBSzvtoxR1RZqMo1aIwR+X8BAZSkhfj55fVF2no4IfPXfdFO3I66N+zEQ8nKoB0uTATWrogQ== + dependencies: + "@aws-sdk/types" "3.901.0" + "@aws-sdk/util-arn-parser" "3.893.0" + "@smithy/node-config-provider" "^4.3.0" + "@smithy/protocol-http" "^5.3.0" + "@smithy/types" "^4.6.0" + "@smithy/util-config-provider" "^4.2.0" tslib "^2.6.2" -"@aws-sdk/middleware-endpoint-discovery@3.696.0": - version "3.696.0" - resolved "https://registry.npmjs.org/@aws-sdk/middleware-endpoint-discovery/-/middleware-endpoint-discovery-3.696.0.tgz" - integrity sha512-KZvgR3lB9zdLuuO+SxeQQVDn8R46Brlolsbv7JGyR6id0BNy6pqitHdcrZCyp9jaMjrSFcPROceeLy70Cu3pZg== +"@aws-sdk/middleware-endpoint-discovery@3.901.0": + version "3.901.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/middleware-endpoint-discovery/-/middleware-endpoint-discovery-3.901.0.tgz#ef1b10b4d187bf7292fab5ae5fbe920c588fb041" + integrity sha512-nbqELNamIhsWcDmqa3rB5unNuOEGiNh2pEz663ZEzsa9DTasKvBHqdhVQo6DuWDvnkhAjOMrkM2sB4P45uy1Qw== dependencies: - "@aws-sdk/endpoint-cache" "3.693.0" - "@aws-sdk/types" "3.696.0" - "@smithy/node-config-provider" "^3.1.11" - "@smithy/protocol-http" "^4.1.7" - "@smithy/types" "^3.7.1" + "@aws-sdk/endpoint-cache" "3.893.0" + "@aws-sdk/types" "3.901.0" + "@smithy/node-config-provider" "^4.3.0" + "@smithy/protocol-http" "^5.3.0" + "@smithy/types" "^4.6.0" tslib "^2.6.2" -"@aws-sdk/middleware-expect-continue@3.696.0": - version "3.696.0" - resolved "https://registry.npmjs.org/@aws-sdk/middleware-expect-continue/-/middleware-expect-continue-3.696.0.tgz" - integrity sha512-vpVukqY3U2pb+ULeX0shs6L0aadNep6kKzjme/MyulPjtUDJpD3AekHsXRrCCGLmOqSKqRgQn5zhV9pQhHsb6Q== +"@aws-sdk/middleware-expect-continue@3.901.0": + version "3.901.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/middleware-expect-continue/-/middleware-expect-continue-3.901.0.tgz#bd6c1fde979808418ce013c6f5f379e67ef2f4c4" + integrity sha512-bwq9nj6MH38hlJwOY9QXIDwa6lI48UsaZpaXbdD71BljEIRlxDzfB4JaYb+ZNNK7RIAdzsP/K05mJty6KJAQHw== dependencies: - "@aws-sdk/types" "3.696.0" - "@smithy/protocol-http" "^4.1.7" - "@smithy/types" "^3.7.1" + "@aws-sdk/types" "3.901.0" + "@smithy/protocol-http" "^5.3.0" + "@smithy/types" "^4.6.0" tslib "^2.6.2" -"@aws-sdk/middleware-flexible-checksums@3.701.0": - version "3.701.0" - resolved "https://registry.npmjs.org/@aws-sdk/middleware-flexible-checksums/-/middleware-flexible-checksums-3.701.0.tgz" - integrity sha512-adNaPCyTT+CiVM0ufDiO1Fe7nlRmJdI9Hcgj0M9S6zR7Dw70Ra5z8Lslkd7syAccYvZaqxLklGjPQH/7GNxwTA== +"@aws-sdk/middleware-flexible-checksums@3.901.0": + version "3.901.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/middleware-flexible-checksums/-/middleware-flexible-checksums-3.901.0.tgz#373449d1609c9af810a824b395633ce6d1fc03f1" + integrity sha512-63lcKfggVUFyXhE4SsFXShCTCyh7ZHEqXLyYEL4DwX+VWtxutf9t9m3fF0TNUYDE8eEGWiRXhegj8l4FjuW+wA== dependencies: "@aws-crypto/crc32" "5.2.0" "@aws-crypto/crc32c" "5.2.0" "@aws-crypto/util" "5.2.0" - "@aws-sdk/core" "3.696.0" - "@aws-sdk/types" "3.696.0" - "@smithy/is-array-buffer" "^3.0.0" - "@smithy/node-config-provider" "^3.1.11" - "@smithy/protocol-http" "^4.1.7" - "@smithy/types" "^3.7.1" - "@smithy/util-middleware" "^3.0.10" - "@smithy/util-stream" "^3.3.1" - "@smithy/util-utf8" "^3.0.0" + "@aws-sdk/core" "3.901.0" + "@aws-sdk/types" "3.901.0" + "@smithy/is-array-buffer" "^4.2.0" + "@smithy/node-config-provider" "^4.3.0" + "@smithy/protocol-http" "^5.3.0" + "@smithy/types" "^4.6.0" + "@smithy/util-middleware" "^4.2.0" + "@smithy/util-stream" "^4.4.0" + "@smithy/util-utf8" "^4.2.0" tslib "^2.6.2" -"@aws-sdk/middleware-host-header@3.696.0": - version "3.696.0" - resolved "https://registry.npmjs.org/@aws-sdk/middleware-host-header/-/middleware-host-header-3.696.0.tgz" - integrity sha512-zELJp9Ta2zkX7ELggMN9qMCgekqZhFC5V2rOr4hJDEb/Tte7gpfKSObAnw/3AYiVqt36sjHKfdkoTsuwGdEoDg== +"@aws-sdk/middleware-host-header@3.901.0": + version "3.901.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/middleware-host-header/-/middleware-host-header-3.901.0.tgz#e6b3a6706601d93949ca25167ecec50c40e3d9de" + integrity sha512-yWX7GvRmqBtbNnUW7qbre3GvZmyYwU0WHefpZzDTYDoNgatuYq6LgUIQ+z5C04/kCRoFkAFrHag8a3BXqFzq5A== dependencies: - "@aws-sdk/types" "3.696.0" - "@smithy/protocol-http" "^4.1.7" - "@smithy/types" "^3.7.1" + "@aws-sdk/types" "3.901.0" + "@smithy/protocol-http" "^5.3.0" + "@smithy/types" "^4.6.0" tslib "^2.6.2" -"@aws-sdk/middleware-location-constraint@3.696.0": - version "3.696.0" - resolved "https://registry.npmjs.org/@aws-sdk/middleware-location-constraint/-/middleware-location-constraint-3.696.0.tgz" - integrity sha512-FgH12OB0q+DtTrP2aiDBddDKwL4BPOrm7w3VV9BJrSdkqQCNBPz8S1lb0y5eVH4tBG+2j7gKPlOv1wde4jF/iw== +"@aws-sdk/middleware-location-constraint@3.901.0": + version "3.901.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/middleware-location-constraint/-/middleware-location-constraint-3.901.0.tgz#0a74fdd450cdec336f3ccdcb7b2fdbf4ce8b9e0b" + integrity sha512-MuCS5R2ngNoYifkVt05CTULvYVWX0dvRT0/Md4jE3a0u0yMygYy31C1zorwfE/SUgAQXyLmUx8ATmPp9PppImQ== dependencies: - "@aws-sdk/types" "3.696.0" - "@smithy/types" "^3.7.1" + "@aws-sdk/types" "3.901.0" + "@smithy/types" "^4.6.0" tslib "^2.6.2" -"@aws-sdk/middleware-logger@3.696.0": - version "3.696.0" - resolved "https://registry.npmjs.org/@aws-sdk/middleware-logger/-/middleware-logger-3.696.0.tgz" - integrity sha512-KhkHt+8AjCxcR/5Zp3++YPJPpFQzxpr+jmONiT/Jw2yqnSngZ0Yspm5wGoRx2hS1HJbyZNuaOWEGuJoxLeBKfA== +"@aws-sdk/middleware-logger@3.901.0": + version "3.901.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/middleware-logger/-/middleware-logger-3.901.0.tgz#30562184bd0b6a90d30f2d6d58ef5054300f2652" + integrity sha512-UoHebjE7el/tfRo8/CQTj91oNUm+5Heus5/a4ECdmWaSCHCS/hXTsU3PTTHAY67oAQR8wBLFPfp3mMvXjB+L2A== dependencies: - "@aws-sdk/types" "3.696.0" - "@smithy/types" "^3.7.1" + "@aws-sdk/types" "3.901.0" + "@smithy/types" "^4.6.0" tslib "^2.6.2" -"@aws-sdk/middleware-recursion-detection@3.696.0": - version "3.696.0" - resolved "https://registry.npmjs.org/@aws-sdk/middleware-recursion-detection/-/middleware-recursion-detection-3.696.0.tgz" - integrity sha512-si/maV3Z0hH7qa99f9ru2xpS5HlfSVcasRlNUXKSDm611i7jFMWwGNLUOXFAOLhXotPX5G3Z6BLwL34oDeBMug== +"@aws-sdk/middleware-recursion-detection@3.901.0": + version "3.901.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/middleware-recursion-detection/-/middleware-recursion-detection-3.901.0.tgz#8492bd83aeee52f4e1b4194a81d044f46acf8c5b" + integrity sha512-Wd2t8qa/4OL0v/oDpCHHYkgsXJr8/ttCxrvCKAt0H1zZe2LlRhY9gpDVKqdertfHrHDj786fOvEQA28G1L75Dg== dependencies: - "@aws-sdk/types" "3.696.0" - "@smithy/protocol-http" "^4.1.7" - "@smithy/types" "^3.7.1" + "@aws-sdk/types" "3.901.0" + "@aws/lambda-invoke-store" "^0.0.1" + "@smithy/protocol-http" "^5.3.0" + "@smithy/types" "^4.6.0" tslib "^2.6.2" -"@aws-sdk/middleware-sdk-s3@3.696.0": - version "3.696.0" - resolved "https://registry.npmjs.org/@aws-sdk/middleware-sdk-s3/-/middleware-sdk-s3-3.696.0.tgz" - integrity sha512-M7fEiAiN7DBMHflzOFzh1I2MNSlLpbiH2ubs87bdRc2wZsDPSbs4l3v6h3WLhxoQK0bq6vcfroudrLBgvCuX3Q== - dependencies: - "@aws-sdk/core" "3.696.0" - "@aws-sdk/types" "3.696.0" - "@aws-sdk/util-arn-parser" "3.693.0" - "@smithy/core" "^2.5.3" - "@smithy/node-config-provider" "^3.1.11" - "@smithy/protocol-http" "^4.1.7" - "@smithy/signature-v4" "^4.2.2" - "@smithy/smithy-client" "^3.4.4" - "@smithy/types" "^3.7.1" - "@smithy/util-config-provider" "^3.0.0" - "@smithy/util-middleware" "^3.0.10" - "@smithy/util-stream" "^3.3.1" - "@smithy/util-utf8" "^3.0.0" +"@aws-sdk/middleware-sdk-s3@3.901.0": + version "3.901.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/middleware-sdk-s3/-/middleware-sdk-s3-3.901.0.tgz#65ae0e84b020a1dd28278a1610cc4c8978edf853" + integrity sha512-prgjVC3fDT2VIlmQPiw/cLee8r4frTam9GILRUVQyDdNtshNwV3MiaSCLzzQJjKJlLgnBLNUHJCSmvUVtg+3iA== + dependencies: + "@aws-sdk/core" "3.901.0" + "@aws-sdk/types" "3.901.0" + "@aws-sdk/util-arn-parser" "3.893.0" + "@smithy/core" "^3.14.0" + "@smithy/node-config-provider" "^4.3.0" + "@smithy/protocol-http" "^5.3.0" + "@smithy/signature-v4" "^5.3.0" + "@smithy/smithy-client" "^4.7.0" + "@smithy/types" "^4.6.0" + "@smithy/util-config-provider" "^4.2.0" + "@smithy/util-middleware" "^4.2.0" + "@smithy/util-stream" "^4.4.0" + "@smithy/util-utf8" "^4.2.0" tslib "^2.6.2" -"@aws-sdk/middleware-ssec@3.696.0": - version "3.696.0" - resolved "https://registry.npmjs.org/@aws-sdk/middleware-ssec/-/middleware-ssec-3.696.0.tgz" - integrity sha512-w/d6O7AOZ7Pg3w2d3BxnX5RmGNWb5X4RNxF19rJqcgu/xqxxE/QwZTNd5a7eTsqLXAUIfbbR8hh0czVfC1pJLA== +"@aws-sdk/middleware-ssec@3.901.0": + version "3.901.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/middleware-ssec/-/middleware-ssec-3.901.0.tgz#9a08f8a90a12c5d3eccabd884d8dfdd2f76473a4" + integrity sha512-YiLLJmA3RvjL38mFLuu8fhTTGWtp2qT24VqpucgfoyziYcTgIQkJJmKi90Xp6R6/3VcArqilyRgM1+x8i/em+Q== dependencies: - "@aws-sdk/types" "3.696.0" - "@smithy/types" "^3.7.1" + "@aws-sdk/types" "3.901.0" + "@smithy/types" "^4.6.0" tslib "^2.6.2" -"@aws-sdk/middleware-user-agent@3.696.0": - version "3.696.0" - resolved "https://registry.npmjs.org/@aws-sdk/middleware-user-agent/-/middleware-user-agent-3.696.0.tgz" - integrity sha512-Lvyj8CTyxrHI6GHd2YVZKIRI5Fmnugt3cpJo0VrKKEgK5zMySwEZ1n4dqPK6czYRWKd5+WnYHYAuU+Wdk6Jsjw== - dependencies: - "@aws-sdk/core" "3.696.0" - "@aws-sdk/types" "3.696.0" - "@aws-sdk/util-endpoints" "3.696.0" - "@smithy/core" "^2.5.3" - "@smithy/protocol-http" "^4.1.7" - "@smithy/types" "^3.7.1" +"@aws-sdk/middleware-user-agent@3.901.0": + version "3.901.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/middleware-user-agent/-/middleware-user-agent-3.901.0.tgz#ff6ff86115e1c580f369d33a25213e336896c548" + integrity sha512-Zby4F03fvD9xAgXGPywyk4bC1jCbnyubMEYChLYohD+x20ULQCf+AimF/Btn7YL+hBpzh1+RmqmvZcx+RgwgNQ== + dependencies: + "@aws-sdk/core" "3.901.0" + "@aws-sdk/types" "3.901.0" + "@aws-sdk/util-endpoints" "3.901.0" + "@smithy/core" "^3.14.0" + "@smithy/protocol-http" "^5.3.0" + "@smithy/types" "^4.6.0" tslib "^2.6.2" -"@aws-sdk/region-config-resolver@3.696.0": - version "3.696.0" - resolved "https://registry.npmjs.org/@aws-sdk/region-config-resolver/-/region-config-resolver-3.696.0.tgz" - integrity sha512-7EuH142lBXjI8yH6dVS/CZeiK/WZsmb/8zP6bQbVYpMrppSTgB3MzZZdxVZGzL5r8zPQOU10wLC4kIMy0qdBVQ== +"@aws-sdk/nested-clients@3.901.0": + version "3.901.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/nested-clients/-/nested-clients-3.901.0.tgz#8fcd2c48a0132ef1623b243ec88b6aff3164e76a" + integrity sha512-feAAAMsVwctk2Tms40ONybvpfJPLCmSdI+G+OTrNpizkGLNl6ik2Ng2RzxY6UqOfN8abqKP/DOUj1qYDRDG8ag== dependencies: - "@aws-sdk/types" "3.696.0" - "@smithy/node-config-provider" "^3.1.11" - "@smithy/types" "^3.7.1" - "@smithy/util-config-provider" "^3.0.0" - "@smithy/util-middleware" "^3.0.10" + "@aws-crypto/sha256-browser" "5.2.0" + "@aws-crypto/sha256-js" "5.2.0" + "@aws-sdk/core" "3.901.0" + "@aws-sdk/middleware-host-header" "3.901.0" + "@aws-sdk/middleware-logger" "3.901.0" + "@aws-sdk/middleware-recursion-detection" "3.901.0" + "@aws-sdk/middleware-user-agent" "3.901.0" + "@aws-sdk/region-config-resolver" "3.901.0" + "@aws-sdk/types" "3.901.0" + "@aws-sdk/util-endpoints" "3.901.0" + "@aws-sdk/util-user-agent-browser" "3.901.0" + "@aws-sdk/util-user-agent-node" "3.901.0" + "@smithy/config-resolver" "^4.3.0" + "@smithy/core" "^3.14.0" + "@smithy/fetch-http-handler" "^5.3.0" + "@smithy/hash-node" "^4.2.0" + "@smithy/invalid-dependency" "^4.2.0" + "@smithy/middleware-content-length" "^4.2.0" + "@smithy/middleware-endpoint" "^4.3.0" + "@smithy/middleware-retry" "^4.4.0" + "@smithy/middleware-serde" "^4.2.0" + "@smithy/middleware-stack" "^4.2.0" + "@smithy/node-config-provider" "^4.3.0" + "@smithy/node-http-handler" "^4.3.0" + "@smithy/protocol-http" "^5.3.0" + "@smithy/smithy-client" "^4.7.0" + "@smithy/types" "^4.6.0" + "@smithy/url-parser" "^4.2.0" + "@smithy/util-base64" "^4.2.0" + "@smithy/util-body-length-browser" "^4.2.0" + "@smithy/util-body-length-node" "^4.2.0" + "@smithy/util-defaults-mode-browser" "^4.2.0" + "@smithy/util-defaults-mode-node" "^4.2.0" + "@smithy/util-endpoints" "^3.2.0" + "@smithy/util-middleware" "^4.2.0" + "@smithy/util-retry" "^4.2.0" + "@smithy/util-utf8" "^4.2.0" tslib "^2.6.2" -"@aws-sdk/signature-v4-multi-region@3.696.0": - version "3.696.0" - resolved "https://registry.npmjs.org/@aws-sdk/signature-v4-multi-region/-/signature-v4-multi-region-3.696.0.tgz" - integrity sha512-ijPkoLjXuPtgxAYlDoYls8UaG/VKigROn9ebbvPL/orEY5umedd3iZTcS9T+uAf4Ur3GELLxMQiERZpfDKaz3g== +"@aws-sdk/region-config-resolver@3.901.0": + version "3.901.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/region-config-resolver/-/region-config-resolver-3.901.0.tgz#6673eeda4ecc0747f93a084e876cab71431a97ca" + integrity sha512-7F0N888qVLHo4CSQOsnkZ4QAp8uHLKJ4v3u09Ly5k4AEStrSlFpckTPyUx6elwGL+fxGjNE2aakK8vEgzzCV0A== dependencies: - "@aws-sdk/middleware-sdk-s3" "3.696.0" - "@aws-sdk/types" "3.696.0" - "@smithy/protocol-http" "^4.1.7" - "@smithy/signature-v4" "^4.2.2" - "@smithy/types" "^3.7.1" + "@aws-sdk/types" "3.901.0" + "@smithy/node-config-provider" "^4.3.0" + "@smithy/types" "^4.6.0" + "@smithy/util-config-provider" "^4.2.0" + "@smithy/util-middleware" "^4.2.0" tslib "^2.6.2" -"@aws-sdk/token-providers@3.699.0": - version "3.699.0" - resolved "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.699.0.tgz" - integrity sha512-kuiEW9DWs7fNos/SM+y58HCPhcIzm1nEZLhe2/7/6+TvAYLuEWURYsbK48gzsxXlaJ2k/jGY3nIsA7RptbMOwA== +"@aws-sdk/signature-v4-multi-region@3.901.0": + version "3.901.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/signature-v4-multi-region/-/signature-v4-multi-region-3.901.0.tgz#773cd83ab38efe8bd5c1e563e5bd8b79391dfa12" + integrity sha512-2IWxbll/pRucp1WQkHi2W5E2SVPGBvk4Is923H7gpNksbVFws18ItjMM8ZpGm44cJEoy1zR5gjhLFklatpuoOw== dependencies: - "@aws-sdk/types" "3.696.0" - "@smithy/property-provider" "^3.1.9" - "@smithy/shared-ini-file-loader" "^3.1.10" - "@smithy/types" "^3.7.1" + "@aws-sdk/middleware-sdk-s3" "3.901.0" + "@aws-sdk/types" "3.901.0" + "@smithy/protocol-http" "^5.3.0" + "@smithy/signature-v4" "^5.3.0" + "@smithy/types" "^4.6.0" + tslib "^2.6.2" + +"@aws-sdk/token-providers@3.901.0": + version "3.901.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/token-providers/-/token-providers-3.901.0.tgz#1f506f169cde6342c8bad75c068a719453ebcf54" + integrity sha512-pJEr1Ggbc/uVTDqp9IbNu9hdr0eQf3yZix3s4Nnyvmg4xmJSGAlbPC9LrNr5u3CDZoc8Z9CuLrvbP4MwYquNpQ== + dependencies: + "@aws-sdk/core" "3.901.0" + "@aws-sdk/nested-clients" "3.901.0" + "@aws-sdk/types" "3.901.0" + "@smithy/property-provider" "^4.2.0" + "@smithy/shared-ini-file-loader" "^4.3.0" + "@smithy/types" "^4.6.0" tslib "^2.6.2" -"@aws-sdk/types@3.696.0", "@aws-sdk/types@^3.222.0": +"@aws-sdk/types@3.901.0": + version "3.901.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/types/-/types-3.901.0.tgz#b5a2e26c7b3fb3bbfe4c7fc24873646992a1c56c" + integrity sha512-FfEM25hLEs4LoXsLXQ/q6X6L4JmKkKkbVFpKD4mwfVHtRVQG6QxJiCPcrkcPISquiy6esbwK2eh64TWbiD60cg== + dependencies: + "@smithy/types" "^4.6.0" + tslib "^2.6.2" + +"@aws-sdk/types@^3.222.0": version "3.696.0" resolved "https://registry.npmjs.org/@aws-sdk/types/-/types-3.696.0.tgz" integrity sha512-9rTvUJIAj5d3//U5FDPWGJ1nFJLuWb30vugGOrWk7aNZ6y9tuA3PI7Cc9dP8WEXKVyK1vuuk8rSFP2iqXnlgrw== @@ -691,28 +654,29 @@ "@smithy/types" "^3.7.1" tslib "^2.6.2" -"@aws-sdk/util-arn-parser@3.693.0": - version "3.693.0" - resolved "https://registry.npmjs.org/@aws-sdk/util-arn-parser/-/util-arn-parser-3.693.0.tgz" - integrity sha512-WC8x6ca+NRrtpAH64rWu+ryDZI3HuLwlEr8EU6/dbC/pt+r/zC0PBoC15VEygUaBA+isppCikQpGyEDu0Yj7gQ== +"@aws-sdk/util-arn-parser@3.893.0": + version "3.893.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/util-arn-parser/-/util-arn-parser-3.893.0.tgz#fcc9b792744b9da597662891c2422dda83881d8d" + integrity sha512-u8H4f2Zsi19DGnwj5FSZzDMhytYF/bCh37vAtBsn3cNDL3YG578X5oc+wSX54pM3tOxS+NY7tvOAo52SW7koUA== dependencies: tslib "^2.6.2" -"@aws-sdk/util-dynamodb@^3.682.0": - version "3.705.0" - resolved "https://registry.npmjs.org/@aws-sdk/util-dynamodb/-/util-dynamodb-3.705.0.tgz" - integrity sha512-cETrLaH8HGnHy2ohse02OR1/ux6IeT9dA1kngfYZyv0RhC1nt0fm6xJCTt8KPucPTTgXgGWzLaCB/AYu9uxH7A== +"@aws-sdk/util-dynamodb@^3.901.0": + version "3.902.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/util-dynamodb/-/util-dynamodb-3.902.0.tgz#4c8246a3de4e75eedec27fbc62e07df158eacdb0" + integrity sha512-elZ9e671bda7Z1NzNSHiYKktnoXx4z2FJCJf1hpHrA/rMqVmnErNl/QzHZqF+0jH/0UoK7g4LCjJRO5A+reBoQ== dependencies: tslib "^2.6.2" -"@aws-sdk/util-endpoints@3.696.0": - version "3.696.0" - resolved "https://registry.npmjs.org/@aws-sdk/util-endpoints/-/util-endpoints-3.696.0.tgz" - integrity sha512-T5s0IlBVX+gkb9g/I6CLt4yAZVzMSiGnbUqWihWsHvQR1WOoIcndQy/Oz/IJXT9T2ipoy7a80gzV6a5mglrioA== +"@aws-sdk/util-endpoints@3.901.0": + version "3.901.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/util-endpoints/-/util-endpoints-3.901.0.tgz#be6296739d0f446b89a3f497c3a85afeb6cddd92" + integrity sha512-5nZP3hGA8FHEtKvEQf4Aww5QZOkjLW1Z+NixSd+0XKfHvA39Ah5sZboScjLx0C9kti/K3OGW1RCx5K9Zc3bZqg== dependencies: - "@aws-sdk/types" "3.696.0" - "@smithy/types" "^3.7.1" - "@smithy/util-endpoints" "^2.1.6" + "@aws-sdk/types" "3.901.0" + "@smithy/types" "^4.6.0" + "@smithy/url-parser" "^4.2.0" + "@smithy/util-endpoints" "^3.2.0" tslib "^2.6.2" "@aws-sdk/util-locate-window@^3.0.0": @@ -722,35 +686,41 @@ dependencies: tslib "^2.6.2" -"@aws-sdk/util-user-agent-browser@3.696.0": - version "3.696.0" - resolved "https://registry.npmjs.org/@aws-sdk/util-user-agent-browser/-/util-user-agent-browser-3.696.0.tgz" - integrity sha512-Z5rVNDdmPOe6ELoM5AhF/ja5tSjbe6ctSctDPb0JdDf4dT0v2MfwhJKzXju2RzX8Es/77Glh7MlaXLE0kCB9+Q== +"@aws-sdk/util-user-agent-browser@3.901.0": + version "3.901.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/util-user-agent-browser/-/util-user-agent-browser-3.901.0.tgz#2c0e71e9019f054fb6a6061f99f55c13fb92830f" + integrity sha512-Ntb6V/WFI21Ed4PDgL/8NSfoZQQf9xzrwNgiwvnxgAl/KvAvRBgQtqj5gHsDX8Nj2YmJuVoHfH9BGjL9VQ4WNg== dependencies: - "@aws-sdk/types" "3.696.0" - "@smithy/types" "^3.7.1" + "@aws-sdk/types" "3.901.0" + "@smithy/types" "^4.6.0" bowser "^2.11.0" tslib "^2.6.2" -"@aws-sdk/util-user-agent-node@3.696.0": - version "3.696.0" - resolved "https://registry.npmjs.org/@aws-sdk/util-user-agent-node/-/util-user-agent-node-3.696.0.tgz" - integrity sha512-KhKqcfyXIB0SCCt+qsu4eJjsfiOrNzK5dCV7RAW2YIpp+msxGUUX0NdRE9rkzjiv+3EMktgJm3eEIS+yxtlVdQ== +"@aws-sdk/util-user-agent-node@3.901.0": + version "3.901.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/util-user-agent-node/-/util-user-agent-node-3.901.0.tgz#3a0a59a93229016f011e7ee0533d36275e3063bd" + integrity sha512-l59KQP5TY7vPVUfEURc7P5BJKuNg1RSsAKBQW7LHLECXjLqDUbo2SMLrexLBEoArSt6E8QOrIN0C8z/0Xk0jYw== dependencies: - "@aws-sdk/middleware-user-agent" "3.696.0" - "@aws-sdk/types" "3.696.0" - "@smithy/node-config-provider" "^3.1.11" - "@smithy/types" "^3.7.1" + "@aws-sdk/middleware-user-agent" "3.901.0" + "@aws-sdk/types" "3.901.0" + "@smithy/node-config-provider" "^4.3.0" + "@smithy/types" "^4.6.0" tslib "^2.6.2" -"@aws-sdk/xml-builder@3.696.0": - version "3.696.0" - resolved "https://registry.npmjs.org/@aws-sdk/xml-builder/-/xml-builder-3.696.0.tgz" - integrity sha512-dn1mX+EeqivoLYnY7p2qLrir0waPnCgS/0YdRCAVU2x14FgfUYCH6Im3w3oi2dMwhxfKY5lYVB5NKvZu7uI9lQ== +"@aws-sdk/xml-builder@3.901.0": + version "3.901.0" + resolved "https://registry.yarnpkg.com/@aws-sdk/xml-builder/-/xml-builder-3.901.0.tgz#3cd2e3929cefafd771c8bd790ec6965faa1be49d" + integrity sha512-pxFCkuAP7Q94wMTNPAwi6hEtNrp/BdFf+HOrIEeFQsk4EoOmpKY3I6S+u6A9Wg295J80Kh74LqDWM22ux3z6Aw== dependencies: - "@smithy/types" "^3.7.1" + "@smithy/types" "^4.6.0" + fast-xml-parser "5.2.5" tslib "^2.6.2" +"@aws/lambda-invoke-store@^0.0.1": + version "0.0.1" + resolved "https://registry.yarnpkg.com/@aws/lambda-invoke-store/-/lambda-invoke-store-0.0.1.tgz#92d792a7dda250dfcb902e13228f37a81be57c8f" + integrity sha512-ORHRQ2tmvnBXc8t/X9Z8IcSbBA4xTLKuN873FopzklHMeqBst7YG0d+AX97inkvDX+NChYtSr+qGfcqGFaI8Zw== + "@babel/code-frame@^7.0.0", "@babel/code-frame@^7.12.13", "@babel/code-frame@^7.25.9", "@babel/code-frame@^7.26.0", "@babel/code-frame@^7.26.2": version "7.26.2" resolved "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.26.2.tgz" @@ -1771,156 +1741,158 @@ resolved "https://registry.npmjs.org/@sinonjs/text-encoding/-/text-encoding-0.7.3.tgz" integrity sha512-DE427ROAphMQzU4ENbliGYrBSYPXF+TtLg9S8vzeA+OF4ZKzoDdzfL8sxuMUGS/lgRhM6j1URSk9ghf7Xo1tyA== -"@smithy/abort-controller@^3.1.9": - version "3.1.9" - resolved "https://registry.npmjs.org/@smithy/abort-controller/-/abort-controller-3.1.9.tgz" - integrity sha512-yiW0WI30zj8ZKoSYNx90no7ugVn3khlyH/z5W8qtKBtVE6awRALbhSG+2SAHA1r6bO/6M9utxYKVZ3PCJ1rWxw== +"@smithy/abort-controller@^4.2.0": + version "4.2.0" + resolved "https://registry.yarnpkg.com/@smithy/abort-controller/-/abort-controller-4.2.0.tgz#ced549ad5e74232bdcb3eec990b02b1c6d81003d" + integrity sha512-PLUYa+SUKOEZtXFURBu/CNxlsxfaFGxSBPcStL13KpVeVWIfdezWyDqkz7iDLmwnxojXD0s5KzuB5HGHvt4Aeg== dependencies: - "@smithy/types" "^3.7.2" + "@smithy/types" "^4.6.0" tslib "^2.6.2" -"@smithy/chunked-blob-reader-native@^3.0.1": - version "3.0.1" - resolved "https://registry.npmjs.org/@smithy/chunked-blob-reader-native/-/chunked-blob-reader-native-3.0.1.tgz" - integrity sha512-VEYtPvh5rs/xlyqpm5NRnfYLZn+q0SRPELbvBV+C/G7IQ+ouTuo+NKKa3ShG5OaFR8NYVMXls9hPYLTvIKKDrQ== +"@smithy/chunked-blob-reader-native@^4.2.0": + version "4.2.0" + resolved "https://registry.yarnpkg.com/@smithy/chunked-blob-reader-native/-/chunked-blob-reader-native-4.2.0.tgz#3115cfb230f20da21d1011ee2b47165f4c2773e3" + integrity sha512-HNbGWdyTfSM1nfrZKQjYTvD8k086+M8s1EYkBUdGC++lhxegUp2HgNf5RIt6oOGVvsC26hBCW/11tv8KbwLn/Q== dependencies: - "@smithy/util-base64" "^3.0.0" + "@smithy/util-base64" "^4.2.0" tslib "^2.6.2" -"@smithy/chunked-blob-reader@^4.0.0": - version "4.0.0" - resolved "https://registry.npmjs.org/@smithy/chunked-blob-reader/-/chunked-blob-reader-4.0.0.tgz" - integrity sha512-jSqRnZvkT4egkq/7b6/QRCNXmmYVcHwnJldqJ3IhVpQE2atObVJ137xmGeuGFhjFUr8gCEVAOKwSY79OvpbDaQ== +"@smithy/chunked-blob-reader@^5.2.0": + version "5.2.0" + resolved "https://registry.yarnpkg.com/@smithy/chunked-blob-reader/-/chunked-blob-reader-5.2.0.tgz#776fec5eaa5ab5fa70d0d0174b7402420b24559c" + integrity sha512-WmU0TnhEAJLWvfSeMxBNe5xtbselEO8+4wG0NtZeL8oR21WgH1xiO37El+/Y+H/Ie4SCwBy3MxYWmOYaGgZueA== dependencies: tslib "^2.6.2" -"@smithy/config-resolver@^3.0.12", "@smithy/config-resolver@^3.0.13": - version "3.0.13" - resolved "https://registry.npmjs.org/@smithy/config-resolver/-/config-resolver-3.0.13.tgz" - integrity sha512-Gr/qwzyPaTL1tZcq8WQyHhTZREER5R1Wytmz4WnVGL4onA3dNk6Btll55c8Vr58pLdvWZmtG8oZxJTw3t3q7Jg== +"@smithy/config-resolver@^4.3.0": + version "4.3.0" + resolved "https://registry.yarnpkg.com/@smithy/config-resolver/-/config-resolver-4.3.0.tgz#a8bb72a21ff99ac91183a62fcae94f200762c256" + integrity sha512-9oH+n8AVNiLPK/iK/agOsoWfrKZ3FGP3502tkksd6SRsKMYiu7AFX0YXo6YBADdsAj7C+G/aLKdsafIJHxuCkQ== dependencies: - "@smithy/node-config-provider" "^3.1.12" - "@smithy/types" "^3.7.2" - "@smithy/util-config-provider" "^3.0.0" - "@smithy/util-middleware" "^3.0.11" + "@smithy/node-config-provider" "^4.3.0" + "@smithy/types" "^4.6.0" + "@smithy/util-config-provider" "^4.2.0" + "@smithy/util-middleware" "^4.2.0" tslib "^2.6.2" -"@smithy/core@^2.5.3", "@smithy/core@^2.5.5": - version "2.5.5" - resolved "https://registry.npmjs.org/@smithy/core/-/core-2.5.5.tgz" - integrity sha512-G8G/sDDhXA7o0bOvkc7bgai6POuSld/+XhNnWAbpQTpLv2OZPvyqQ58tLPPlz0bSNsXktldDDREIv1LczFeNEw== - dependencies: - "@smithy/middleware-serde" "^3.0.11" - "@smithy/protocol-http" "^4.1.8" - "@smithy/types" "^3.7.2" - "@smithy/util-body-length-browser" "^3.0.0" - "@smithy/util-middleware" "^3.0.11" - "@smithy/util-stream" "^3.3.2" - "@smithy/util-utf8" "^3.0.0" +"@smithy/core@^3.14.0": + version "3.14.0" + resolved "https://registry.yarnpkg.com/@smithy/core/-/core-3.14.0.tgz#22bdb346b171c76b629c4f59dc496c27e10f1c82" + integrity sha512-XJ4z5FxvY/t0Dibms/+gLJrI5niRoY0BCmE02fwmPcRYFPI4KI876xaE79YGWIKnEslMbuQPsIEsoU/DXa0DoA== + dependencies: + "@smithy/middleware-serde" "^4.2.0" + "@smithy/protocol-http" "^5.3.0" + "@smithy/types" "^4.6.0" + "@smithy/util-base64" "^4.2.0" + "@smithy/util-body-length-browser" "^4.2.0" + "@smithy/util-middleware" "^4.2.0" + "@smithy/util-stream" "^4.4.0" + "@smithy/util-utf8" "^4.2.0" + "@smithy/uuid" "^1.1.0" tslib "^2.6.2" -"@smithy/credential-provider-imds@^3.2.6", "@smithy/credential-provider-imds@^3.2.8": - version "3.2.8" - resolved "https://registry.npmjs.org/@smithy/credential-provider-imds/-/credential-provider-imds-3.2.8.tgz" - integrity sha512-ZCY2yD0BY+K9iMXkkbnjo+08T2h8/34oHd0Jmh6BZUSZwaaGlGCyBT/3wnS7u7Xl33/EEfN4B6nQr3Gx5bYxgw== +"@smithy/credential-provider-imds@^4.2.0": + version "4.2.0" + resolved "https://registry.yarnpkg.com/@smithy/credential-provider-imds/-/credential-provider-imds-4.2.0.tgz#21855ceb157afeea60d74c61fe7316e90d8ec545" + integrity sha512-SOhFVvFH4D5HJZytb0bLKxCrSnwcqPiNlrw+S4ZXjMnsC+o9JcUQzbZOEQcA8yv9wJFNhfsUiIUKiEnYL68Big== dependencies: - "@smithy/node-config-provider" "^3.1.12" - "@smithy/property-provider" "^3.1.11" - "@smithy/types" "^3.7.2" - "@smithy/url-parser" "^3.0.11" + "@smithy/node-config-provider" "^4.3.0" + "@smithy/property-provider" "^4.2.0" + "@smithy/types" "^4.6.0" + "@smithy/url-parser" "^4.2.0" tslib "^2.6.2" -"@smithy/eventstream-codec@^3.1.10": - version "3.1.10" - resolved "https://registry.npmjs.org/@smithy/eventstream-codec/-/eventstream-codec-3.1.10.tgz" - integrity sha512-323B8YckSbUH0nMIpXn7HZsAVKHYHFUODa8gG9cHo0ySvA1fr5iWaNT+iIL0UCqUzG6QPHA3BSsBtRQou4mMqQ== +"@smithy/eventstream-codec@^4.2.0": + version "4.2.0" + resolved "https://registry.yarnpkg.com/@smithy/eventstream-codec/-/eventstream-codec-4.2.0.tgz#ea8514363278d062b574859d663f131238a6920c" + integrity sha512-XE7CtKfyxYiNZ5vz7OvyTf1osrdbJfmUy+rbh+NLQmZumMGvY0mT0Cq1qKSfhrvLtRYzMsOBuRpi10dyI0EBPg== dependencies: "@aws-crypto/crc32" "5.2.0" - "@smithy/types" "^3.7.2" - "@smithy/util-hex-encoding" "^3.0.0" + "@smithy/types" "^4.6.0" + "@smithy/util-hex-encoding" "^4.2.0" tslib "^2.6.2" -"@smithy/eventstream-serde-browser@^3.0.13": - version "3.0.14" - resolved "https://registry.npmjs.org/@smithy/eventstream-serde-browser/-/eventstream-serde-browser-3.0.14.tgz" - integrity sha512-kbrt0vjOIihW3V7Cqj1SXQvAI5BR8SnyQYsandva0AOR307cXAc+IhPngxIPslxTLfxwDpNu0HzCAq6g42kCPg== +"@smithy/eventstream-serde-browser@^4.2.0": + version "4.2.0" + resolved "https://registry.yarnpkg.com/@smithy/eventstream-serde-browser/-/eventstream-serde-browser-4.2.0.tgz#d97c4a3f185459097c00e05a23007ffa074f972d" + integrity sha512-U53p7fcrk27k8irLhOwUu+UYnBqsXNLKl1XevOpsxK3y1Lndk8R7CSiZV6FN3fYFuTPuJy5pP6qa/bjDzEkRvA== dependencies: - "@smithy/eventstream-serde-universal" "^3.0.13" - "@smithy/types" "^3.7.2" + "@smithy/eventstream-serde-universal" "^4.2.0" + "@smithy/types" "^4.6.0" tslib "^2.6.2" -"@smithy/eventstream-serde-config-resolver@^3.0.10": - version "3.0.11" - resolved "https://registry.npmjs.org/@smithy/eventstream-serde-config-resolver/-/eventstream-serde-config-resolver-3.0.11.tgz" - integrity sha512-P2pnEp4n75O+QHjyO7cbw/vsw5l93K/8EWyjNCAAybYwUmj3M+hjSQZ9P5TVdUgEG08ueMAP5R4FkuSkElZ5tQ== +"@smithy/eventstream-serde-config-resolver@^4.3.0": + version "4.3.0" + resolved "https://registry.yarnpkg.com/@smithy/eventstream-serde-config-resolver/-/eventstream-serde-config-resolver-4.3.0.tgz#5ee07ed6808c3cac2e4b7ef5059fd9be6aff4a4a" + integrity sha512-uwx54t8W2Yo9Jr3nVF5cNnkAAnMCJ8Wrm+wDlQY6rY/IrEgZS3OqagtCu/9ceIcZFQ1zVW/zbN9dxb5esuojfA== dependencies: - "@smithy/types" "^3.7.2" + "@smithy/types" "^4.6.0" tslib "^2.6.2" -"@smithy/eventstream-serde-node@^3.0.12": - version "3.0.13" - resolved "https://registry.npmjs.org/@smithy/eventstream-serde-node/-/eventstream-serde-node-3.0.13.tgz" - integrity sha512-zqy/9iwbj8Wysmvi7Lq7XFLeDgjRpTbCfwBhJa8WbrylTAHiAu6oQTwdY7iu2lxigbc9YYr9vPv5SzYny5tCXQ== +"@smithy/eventstream-serde-node@^4.2.0": + version "4.2.0" + resolved "https://registry.yarnpkg.com/@smithy/eventstream-serde-node/-/eventstream-serde-node-4.2.0.tgz#397640826f72082e4d33e02525603dcf1baf756f" + integrity sha512-yjM2L6QGmWgJjVu/IgYd6hMzwm/tf4VFX0lm8/SvGbGBwc+aFl3hOzvO/e9IJ2XI+22Tx1Zg3vRpFRs04SWFcg== dependencies: - "@smithy/eventstream-serde-universal" "^3.0.13" - "@smithy/types" "^3.7.2" + "@smithy/eventstream-serde-universal" "^4.2.0" + "@smithy/types" "^4.6.0" tslib "^2.6.2" -"@smithy/eventstream-serde-universal@^3.0.13": - version "3.0.13" - resolved "https://registry.npmjs.org/@smithy/eventstream-serde-universal/-/eventstream-serde-universal-3.0.13.tgz" - integrity sha512-L1Ib66+gg9uTnqp/18Gz4MDpJPKRE44geOjOQ2SVc0eiaO5l255ADziATZgjQjqumC7yPtp1XnjHlF1srcwjKw== +"@smithy/eventstream-serde-universal@^4.2.0": + version "4.2.0" + resolved "https://registry.yarnpkg.com/@smithy/eventstream-serde-universal/-/eventstream-serde-universal-4.2.0.tgz#e556f85638c7037cbd17f72a1cbd2dcdd3185f7d" + integrity sha512-C3jxz6GeRzNyGKhU7oV656ZbuHY93mrfkT12rmjDdZch142ykjn8do+VOkeRNjSGKw01p4g+hdalPYPhmMwk1g== dependencies: - "@smithy/eventstream-codec" "^3.1.10" - "@smithy/types" "^3.7.2" + "@smithy/eventstream-codec" "^4.2.0" + "@smithy/types" "^4.6.0" tslib "^2.6.2" -"@smithy/fetch-http-handler@^4.1.1", "@smithy/fetch-http-handler@^4.1.2": - version "4.1.2" - resolved "https://registry.npmjs.org/@smithy/fetch-http-handler/-/fetch-http-handler-4.1.2.tgz" - integrity sha512-R7rU7Ae3ItU4rC0c5mB2sP5mJNbCfoDc8I5XlYjIZnquyUwec7fEo78F6DA3SmgJgkU1qTMcZJuGblxZsl10ZA== +"@smithy/fetch-http-handler@^5.3.0": + version "5.3.0" + resolved "https://registry.yarnpkg.com/@smithy/fetch-http-handler/-/fetch-http-handler-5.3.0.tgz#1c5205642a9295f44441d8763e7c3a51a747fc95" + integrity sha512-BG3KSmsx9A//KyIfw+sqNmWFr1YBUr+TwpxFT7yPqAk0yyDh7oSNgzfNH7pS6OC099EGx2ltOULvumCFe8bcgw== dependencies: - "@smithy/protocol-http" "^4.1.8" - "@smithy/querystring-builder" "^3.0.11" - "@smithy/types" "^3.7.2" - "@smithy/util-base64" "^3.0.0" + "@smithy/protocol-http" "^5.3.0" + "@smithy/querystring-builder" "^4.2.0" + "@smithy/types" "^4.6.0" + "@smithy/util-base64" "^4.2.0" tslib "^2.6.2" -"@smithy/hash-blob-browser@^3.1.9": - version "3.1.10" - resolved "https://registry.npmjs.org/@smithy/hash-blob-browser/-/hash-blob-browser-3.1.10.tgz" - integrity sha512-elwslXOoNunmfS0fh55jHggyhccobFkexLYC1ZeZ1xP2BTSrcIBaHV2b4xUQOdctrSNOpMqOZH1r2XzWTEhyfA== +"@smithy/hash-blob-browser@^4.2.0": + version "4.2.0" + resolved "https://registry.yarnpkg.com/@smithy/hash-blob-browser/-/hash-blob-browser-4.2.0.tgz#b7bd8c5b379ebfae5b8ce10312da1351d7ff5ff4" + integrity sha512-MWmrRTPqVKpN8NmxmJPTeQuhewTt8Chf+waB38LXHZoA02+BeWYVQ9ViAwHjug8m7lQb1UWuGqp3JoGDOWvvuA== dependencies: - "@smithy/chunked-blob-reader" "^4.0.0" - "@smithy/chunked-blob-reader-native" "^3.0.1" - "@smithy/types" "^3.7.2" + "@smithy/chunked-blob-reader" "^5.2.0" + "@smithy/chunked-blob-reader-native" "^4.2.0" + "@smithy/types" "^4.6.0" tslib "^2.6.2" -"@smithy/hash-node@^3.0.10": - version "3.0.11" - resolved "https://registry.npmjs.org/@smithy/hash-node/-/hash-node-3.0.11.tgz" - integrity sha512-emP23rwYyZhQBvklqTtwetkQlqbNYirDiEEwXl2v0GYWMnCzxst7ZaRAnWuy28njp5kAH54lvkdG37MblZzaHA== +"@smithy/hash-node@^4.2.0": + version "4.2.0" + resolved "https://registry.yarnpkg.com/@smithy/hash-node/-/hash-node-4.2.0.tgz#d2de380cb88a3665d5e3f5bbe901cfb46867c74f" + integrity sha512-ugv93gOhZGysTctZh9qdgng8B+xO0cj+zN0qAZ+Sgh7qTQGPOJbMdIuyP89KNfUyfAqFSNh5tMvC+h2uCpmTtA== dependencies: - "@smithy/types" "^3.7.2" - "@smithy/util-buffer-from" "^3.0.0" - "@smithy/util-utf8" "^3.0.0" + "@smithy/types" "^4.6.0" + "@smithy/util-buffer-from" "^4.2.0" + "@smithy/util-utf8" "^4.2.0" tslib "^2.6.2" -"@smithy/hash-stream-node@^3.1.9": - version "3.1.10" - resolved "https://registry.npmjs.org/@smithy/hash-stream-node/-/hash-stream-node-3.1.10.tgz" - integrity sha512-olomK/jZQ93OMayW1zfTHwcbwBdhcZOHsyWyiZ9h9IXvc1mCD/VuvzbLb3Gy/qNJwI4MANPLctTp2BucV2oU/Q== +"@smithy/hash-stream-node@^4.2.0": + version "4.2.0" + resolved "https://registry.yarnpkg.com/@smithy/hash-stream-node/-/hash-stream-node-4.2.0.tgz#7d3067d566e32167ebcb80f22260cc57de036ec9" + integrity sha512-8dELAuGv+UEjtzrpMeNBZc1sJhO8GxFVV/Yh21wE35oX4lOE697+lsMHBoUIFAUuYkTMIeu0EuJSEsH7/8Y+UQ== dependencies: - "@smithy/types" "^3.7.2" - "@smithy/util-utf8" "^3.0.0" + "@smithy/types" "^4.6.0" + "@smithy/util-utf8" "^4.2.0" tslib "^2.6.2" -"@smithy/invalid-dependency@^3.0.10": - version "3.0.11" - resolved "https://registry.npmjs.org/@smithy/invalid-dependency/-/invalid-dependency-3.0.11.tgz" - integrity sha512-NuQmVPEJjUX6c+UELyVz8kUx8Q539EDeNwbRyu4IIF8MeV7hUtq1FB3SHVyki2u++5XLMFqngeMKk7ccspnNyQ== +"@smithy/invalid-dependency@^4.2.0": + version "4.2.0" + resolved "https://registry.yarnpkg.com/@smithy/invalid-dependency/-/invalid-dependency-4.2.0.tgz#749c741c1b01bcdb12c0ec24701db655102f6ea7" + integrity sha512-ZmK5X5fUPAbtvRcUPtk28aqIClVhbfcmfoS4M7UQBTnDdrNxhsrxYVv0ZEl5NaPSyExsPWqL4GsPlRvtlwg+2A== dependencies: - "@smithy/types" "^3.7.2" + "@smithy/types" "^4.6.0" tslib "^2.6.2" "@smithy/is-array-buffer@^2.2.0": @@ -1930,208 +1902,209 @@ dependencies: tslib "^2.6.2" -"@smithy/is-array-buffer@^3.0.0": - version "3.0.0" - resolved "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-3.0.0.tgz" - integrity sha512-+Fsu6Q6C4RSJiy81Y8eApjEB5gVtM+oFKTffg+jSuwtvomJJrhUJBu2zS8wjXSgH/g1MKEWrzyChTBe6clb5FQ== +"@smithy/is-array-buffer@^4.2.0": + version "4.2.0" + resolved "https://registry.yarnpkg.com/@smithy/is-array-buffer/-/is-array-buffer-4.2.0.tgz#b0f874c43887d3ad44f472a0f3f961bcce0550c2" + integrity sha512-DZZZBvC7sjcYh4MazJSGiWMI2L7E0oCiRHREDzIxi/M2LY79/21iXt6aPLHge82wi5LsuRF5A06Ds3+0mlh6CQ== dependencies: tslib "^2.6.2" -"@smithy/md5-js@^3.0.10": - version "3.0.11" - resolved "https://registry.npmjs.org/@smithy/md5-js/-/md5-js-3.0.11.tgz" - integrity sha512-3NM0L3i2Zm4bbgG6Ymi9NBcxXhryi3uE8fIfHJZIOfZVxOkGdjdgjR9A06SFIZCfnEIWKXZdm6Yq5/aPXFFhsQ== +"@smithy/md5-js@^4.2.0": + version "4.2.0" + resolved "https://registry.yarnpkg.com/@smithy/md5-js/-/md5-js-4.2.0.tgz#46bb7b122d9de1aa306e767ae64230fc6c8d67c2" + integrity sha512-LFEPniXGKRQArFmDQ3MgArXlClFJMsXDteuQQY8WG1/zzv6gVSo96+qpkuu1oJp4MZsKrwchY0cuAoPKzEbaNA== dependencies: - "@smithy/types" "^3.7.2" - "@smithy/util-utf8" "^3.0.0" + "@smithy/types" "^4.6.0" + "@smithy/util-utf8" "^4.2.0" tslib "^2.6.2" -"@smithy/middleware-content-length@^3.0.12": - version "3.0.13" - resolved "https://registry.npmjs.org/@smithy/middleware-content-length/-/middleware-content-length-3.0.13.tgz" - integrity sha512-zfMhzojhFpIX3P5ug7jxTjfUcIPcGjcQYzB9t+rv0g1TX7B0QdwONW+ATouaLoD7h7LOw/ZlXfkq4xJ/g2TrIw== +"@smithy/middleware-content-length@^4.2.0": + version "4.2.0" + resolved "https://registry.yarnpkg.com/@smithy/middleware-content-length/-/middleware-content-length-4.2.0.tgz#bf1bea6e7c0e35e8c6d4825880e4cfa903cbd501" + integrity sha512-6ZAnwrXFecrA4kIDOcz6aLBhU5ih2is2NdcZtobBDSdSHtE9a+MThB5uqyK4XXesdOCvOcbCm2IGB95birTSOQ== dependencies: - "@smithy/protocol-http" "^4.1.8" - "@smithy/types" "^3.7.2" + "@smithy/protocol-http" "^5.3.0" + "@smithy/types" "^4.6.0" tslib "^2.6.2" -"@smithy/middleware-endpoint@^3.2.3", "@smithy/middleware-endpoint@^3.2.5": - version "3.2.5" - resolved "https://registry.npmjs.org/@smithy/middleware-endpoint/-/middleware-endpoint-3.2.5.tgz" - integrity sha512-VhJNs/s/lyx4weiZdXSloBgoLoS8osV0dKIain8nGmx7of3QFKu5BSdEuk1z/U8x9iwes1i+XCiNusEvuK1ijg== - dependencies: - "@smithy/core" "^2.5.5" - "@smithy/middleware-serde" "^3.0.11" - "@smithy/node-config-provider" "^3.1.12" - "@smithy/shared-ini-file-loader" "^3.1.12" - "@smithy/types" "^3.7.2" - "@smithy/url-parser" "^3.0.11" - "@smithy/util-middleware" "^3.0.11" +"@smithy/middleware-endpoint@^4.3.0": + version "4.3.0" + resolved "https://registry.yarnpkg.com/@smithy/middleware-endpoint/-/middleware-endpoint-4.3.0.tgz#407ce4051be2f1855259a02900a957e9b347fdfd" + integrity sha512-jFVjuQeV8TkxaRlcCNg0GFVgg98tscsmIrIwRFeC74TIUyLE3jmY9xgc1WXrPQYRjQNK3aRoaIk6fhFRGOIoGw== + dependencies: + "@smithy/core" "^3.14.0" + "@smithy/middleware-serde" "^4.2.0" + "@smithy/node-config-provider" "^4.3.0" + "@smithy/shared-ini-file-loader" "^4.3.0" + "@smithy/types" "^4.6.0" + "@smithy/url-parser" "^4.2.0" + "@smithy/util-middleware" "^4.2.0" tslib "^2.6.2" -"@smithy/middleware-retry@^3.0.27": - version "3.0.30" - resolved "https://registry.npmjs.org/@smithy/middleware-retry/-/middleware-retry-3.0.30.tgz" - integrity sha512-6323RL2BvAR3VQpTjHpa52kH/iSHyxd/G9ohb2MkBk2Ucu+oMtRXT8yi7KTSIS9nb58aupG6nO0OlXnQOAcvmQ== - dependencies: - "@smithy/node-config-provider" "^3.1.12" - "@smithy/protocol-http" "^4.1.8" - "@smithy/service-error-classification" "^3.0.11" - "@smithy/smithy-client" "^3.5.0" - "@smithy/types" "^3.7.2" - "@smithy/util-middleware" "^3.0.11" - "@smithy/util-retry" "^3.0.11" +"@smithy/middleware-retry@^4.4.0": + version "4.4.0" + resolved "https://registry.yarnpkg.com/@smithy/middleware-retry/-/middleware-retry-4.4.0.tgz#7f4b313a808aa8ac1a5922aff355e12c5a270de1" + integrity sha512-yaVBR0vQnOnzex45zZ8ZrPzUnX73eUC8kVFaAAbn04+6V7lPtxn56vZEBBAhgS/eqD6Zm86o6sJs6FuQVoX5qg== + dependencies: + "@smithy/node-config-provider" "^4.3.0" + "@smithy/protocol-http" "^5.3.0" + "@smithy/service-error-classification" "^4.2.0" + "@smithy/smithy-client" "^4.7.0" + "@smithy/types" "^4.6.0" + "@smithy/util-middleware" "^4.2.0" + "@smithy/util-retry" "^4.2.0" + "@smithy/uuid" "^1.1.0" tslib "^2.6.2" - uuid "^9.0.1" -"@smithy/middleware-serde@^3.0.10", "@smithy/middleware-serde@^3.0.11": - version "3.0.11" - resolved "https://registry.npmjs.org/@smithy/middleware-serde/-/middleware-serde-3.0.11.tgz" - integrity sha512-KzPAeySp/fOoQA82TpnwItvX8BBURecpx6ZMu75EZDkAcnPtO6vf7q4aH5QHs/F1s3/snQaSFbbUMcFFZ086Mw== +"@smithy/middleware-serde@^4.2.0": + version "4.2.0" + resolved "https://registry.yarnpkg.com/@smithy/middleware-serde/-/middleware-serde-4.2.0.tgz#1b7fcaa699d1c48f2c3cbbce325aa756895ddf0f" + integrity sha512-rpTQ7D65/EAbC6VydXlxjvbifTf4IH+sADKg6JmAvhkflJO2NvDeyU9qsWUNBelJiQFcXKejUHWRSdmpJmEmiw== dependencies: - "@smithy/types" "^3.7.2" + "@smithy/protocol-http" "^5.3.0" + "@smithy/types" "^4.6.0" tslib "^2.6.2" -"@smithy/middleware-stack@^3.0.10", "@smithy/middleware-stack@^3.0.11": - version "3.0.11" - resolved "https://registry.npmjs.org/@smithy/middleware-stack/-/middleware-stack-3.0.11.tgz" - integrity sha512-1HGo9a6/ikgOMrTrWL/WiN9N8GSVYpuRQO5kjstAq4CvV59bjqnh7TbdXGQ4vxLD3xlSjfBjq5t1SOELePsLnA== +"@smithy/middleware-stack@^4.2.0": + version "4.2.0" + resolved "https://registry.yarnpkg.com/@smithy/middleware-stack/-/middleware-stack-4.2.0.tgz#fa2f7dcdb0f3a1649d1d2ec3dc4841d9c2f70e67" + integrity sha512-G5CJ//eqRd9OARrQu9MK1H8fNm2sMtqFh6j8/rPozhEL+Dokpvi1Og+aCixTuwDAGZUkJPk6hJT5jchbk/WCyg== dependencies: - "@smithy/types" "^3.7.2" + "@smithy/types" "^4.6.0" tslib "^2.6.2" -"@smithy/node-config-provider@^3.1.11", "@smithy/node-config-provider@^3.1.12": - version "3.1.12" - resolved "https://registry.npmjs.org/@smithy/node-config-provider/-/node-config-provider-3.1.12.tgz" - integrity sha512-O9LVEu5J/u/FuNlZs+L7Ikn3lz7VB9hb0GtPT9MQeiBmtK8RSY3ULmsZgXhe6VAlgTw0YO+paQx4p8xdbs43vQ== +"@smithy/node-config-provider@^4.3.0": + version "4.3.0" + resolved "https://registry.yarnpkg.com/@smithy/node-config-provider/-/node-config-provider-4.3.0.tgz#619ba522d683081d06f112a581b9009988cb38eb" + integrity sha512-5QgHNuWdT9j9GwMPPJCKxy2KDxZ3E5l4M3/5TatSZrqYVoEiqQrDfAq8I6KWZw7RZOHtVtCzEPdYz7rHZixwcA== dependencies: - "@smithy/property-provider" "^3.1.11" - "@smithy/shared-ini-file-loader" "^3.1.12" - "@smithy/types" "^3.7.2" + "@smithy/property-provider" "^4.2.0" + "@smithy/shared-ini-file-loader" "^4.3.0" + "@smithy/types" "^4.6.0" tslib "^2.6.2" -"@smithy/node-http-handler@^3.3.1", "@smithy/node-http-handler@^3.3.2": - version "3.3.2" - resolved "https://registry.npmjs.org/@smithy/node-http-handler/-/node-http-handler-3.3.2.tgz" - integrity sha512-t4ng1DAd527vlxvOfKFYEe6/QFBcsj7WpNlWTyjorwXXcKw3XlltBGbyHfSJ24QT84nF+agDha9tNYpzmSRZPA== +"@smithy/node-http-handler@^4.3.0": + version "4.3.0" + resolved "https://registry.yarnpkg.com/@smithy/node-http-handler/-/node-http-handler-4.3.0.tgz#783d3dbdf5b90b9e0ca1e56070a3be38b3836b7d" + integrity sha512-RHZ/uWCmSNZ8cneoWEVsVwMZBKy/8123hEpm57vgGXA3Irf/Ja4v9TVshHK2ML5/IqzAZn0WhINHOP9xl+Qy6Q== dependencies: - "@smithy/abort-controller" "^3.1.9" - "@smithy/protocol-http" "^4.1.8" - "@smithy/querystring-builder" "^3.0.11" - "@smithy/types" "^3.7.2" + "@smithy/abort-controller" "^4.2.0" + "@smithy/protocol-http" "^5.3.0" + "@smithy/querystring-builder" "^4.2.0" + "@smithy/types" "^4.6.0" tslib "^2.6.2" -"@smithy/property-provider@^3.1.11", "@smithy/property-provider@^3.1.9": - version "3.1.11" - resolved "https://registry.npmjs.org/@smithy/property-provider/-/property-provider-3.1.11.tgz" - integrity sha512-I/+TMc4XTQ3QAjXfOcUWbSS073oOEAxgx4aZy8jHaf8JQnRkq2SZWw8+PfDtBvLUjcGMdxl+YwtzWe6i5uhL/A== +"@smithy/property-provider@^4.2.0": + version "4.2.0" + resolved "https://registry.yarnpkg.com/@smithy/property-provider/-/property-provider-4.2.0.tgz#431c573326f572ae9063d58c21690f28251f9dce" + integrity sha512-rV6wFre0BU6n/tx2Ztn5LdvEdNZ2FasQbPQmDOPfV9QQyDmsCkOAB0osQjotRCQg+nSKFmINhyda0D3AnjSBJw== dependencies: - "@smithy/types" "^3.7.2" + "@smithy/types" "^4.6.0" tslib "^2.6.2" -"@smithy/protocol-http@^4.1.7", "@smithy/protocol-http@^4.1.8": - version "4.1.8" - resolved "https://registry.npmjs.org/@smithy/protocol-http/-/protocol-http-4.1.8.tgz" - integrity sha512-hmgIAVyxw1LySOwkgMIUN0kjN8TG9Nc85LJeEmEE/cNEe2rkHDUWhnJf2gxcSRFLWsyqWsrZGw40ROjUogg+Iw== +"@smithy/protocol-http@^5.3.0": + version "5.3.0" + resolved "https://registry.yarnpkg.com/@smithy/protocol-http/-/protocol-http-5.3.0.tgz#2a2834386b706b959d20e7841099b1780ae62ace" + integrity sha512-6POSYlmDnsLKb7r1D3SVm7RaYW6H1vcNcTWGWrF7s9+2noNYvUsm7E4tz5ZQ9HXPmKn6Hb67pBDRIjrT4w/d7Q== dependencies: - "@smithy/types" "^3.7.2" + "@smithy/types" "^4.6.0" tslib "^2.6.2" -"@smithy/querystring-builder@^3.0.11": - version "3.0.11" - resolved "https://registry.npmjs.org/@smithy/querystring-builder/-/querystring-builder-3.0.11.tgz" - integrity sha512-u+5HV/9uJaeLj5XTb6+IEF/dokWWkEqJ0XiaRRogyREmKGUgZnNecLucADLdauWFKUNbQfulHFEZEdjwEBjXRg== +"@smithy/querystring-builder@^4.2.0": + version "4.2.0" + resolved "https://registry.yarnpkg.com/@smithy/querystring-builder/-/querystring-builder-4.2.0.tgz#a6191d2eccc14ffce821a559ec26c94c636a39c6" + integrity sha512-Q4oFD0ZmI8yJkiPPeGUITZj++4HHYCW3pYBYfIobUCkYpI6mbkzmG1MAQQ3lJYYWj3iNqfzOenUZu+jqdPQ16A== dependencies: - "@smithy/types" "^3.7.2" - "@smithy/util-uri-escape" "^3.0.0" + "@smithy/types" "^4.6.0" + "@smithy/util-uri-escape" "^4.2.0" tslib "^2.6.2" -"@smithy/querystring-parser@^3.0.11": - version "3.0.11" - resolved "https://registry.npmjs.org/@smithy/querystring-parser/-/querystring-parser-3.0.11.tgz" - integrity sha512-Je3kFvCsFMnso1ilPwA7GtlbPaTixa3WwC+K21kmMZHsBEOZYQaqxcMqeFFoU7/slFjKDIpiiPydvdJm8Q/MCw== +"@smithy/querystring-parser@^4.2.0": + version "4.2.0" + resolved "https://registry.yarnpkg.com/@smithy/querystring-parser/-/querystring-parser-4.2.0.tgz#4c4ebe257e951dff91f9db65f9558752641185e8" + integrity sha512-BjATSNNyvVbQxOOlKse0b0pSezTWGMvA87SvoFoFlkRsKXVsN3bEtjCxvsNXJXfnAzlWFPaT9DmhWy1vn0sNEA== dependencies: - "@smithy/types" "^3.7.2" + "@smithy/types" "^4.6.0" tslib "^2.6.2" -"@smithy/service-error-classification@^3.0.11": - version "3.0.11" - resolved "https://registry.npmjs.org/@smithy/service-error-classification/-/service-error-classification-3.0.11.tgz" - integrity sha512-QnYDPkyewrJzCyaeI2Rmp7pDwbUETe+hU8ADkXmgNusO1bgHBH7ovXJiYmba8t0fNfJx75fE8dlM6SEmZxheog== +"@smithy/service-error-classification@^4.2.0": + version "4.2.0" + resolved "https://registry.yarnpkg.com/@smithy/service-error-classification/-/service-error-classification-4.2.0.tgz#d98d9b351d05c21b83c5a012194480a8c2eae5b7" + integrity sha512-Ylv1ttUeKatpR0wEOMnHf1hXMktPUMObDClSWl2TpCVT4DwtJhCeighLzSLbgH3jr5pBNM0LDXT5yYxUvZ9WpA== dependencies: - "@smithy/types" "^3.7.2" + "@smithy/types" "^4.6.0" -"@smithy/shared-ini-file-loader@^3.1.10", "@smithy/shared-ini-file-loader@^3.1.12": - version "3.1.12" - resolved "https://registry.npmjs.org/@smithy/shared-ini-file-loader/-/shared-ini-file-loader-3.1.12.tgz" - integrity sha512-1xKSGI+U9KKdbG2qDvIR9dGrw3CNx+baqJfyr0igKEpjbHL5stsqAesYBzHChYHlelWtb87VnLWlhvfCz13H8Q== +"@smithy/shared-ini-file-loader@^4.3.0": + version "4.3.0" + resolved "https://registry.yarnpkg.com/@smithy/shared-ini-file-loader/-/shared-ini-file-loader-4.3.0.tgz#241a493ea7fa7faeaefccf6a5fa81af521d91cfa" + integrity sha512-VCUPPtNs+rKWlqqntX0CbVvWyjhmX30JCtzO+s5dlzzxrvSfRh5SY0yxnkirvc1c80vdKQttahL71a9EsdolSQ== dependencies: - "@smithy/types" "^3.7.2" + "@smithy/types" "^4.6.0" tslib "^2.6.2" -"@smithy/signature-v4@^4.2.2": - version "4.2.4" - resolved "https://registry.npmjs.org/@smithy/signature-v4/-/signature-v4-4.2.4.tgz" - integrity sha512-5JWeMQYg81TgU4cG+OexAWdvDTs5JDdbEZx+Qr1iPbvo91QFGzjy0IkXAKaXUHqmKUJgSHK0ZxnCkgZpzkeNTA== - dependencies: - "@smithy/is-array-buffer" "^3.0.0" - "@smithy/protocol-http" "^4.1.8" - "@smithy/types" "^3.7.2" - "@smithy/util-hex-encoding" "^3.0.0" - "@smithy/util-middleware" "^3.0.11" - "@smithy/util-uri-escape" "^3.0.0" - "@smithy/util-utf8" "^3.0.0" +"@smithy/signature-v4@^5.3.0": + version "5.3.0" + resolved "https://registry.yarnpkg.com/@smithy/signature-v4/-/signature-v4-5.3.0.tgz#05d459cc4ec8f9d7300bb6b488cccedf2b73b7fb" + integrity sha512-MKNyhXEs99xAZaFhm88h+3/V+tCRDQ+PrDzRqL0xdDpq4gjxcMmf5rBA3YXgqZqMZ/XwemZEurCBQMfxZOWq/g== + dependencies: + "@smithy/is-array-buffer" "^4.2.0" + "@smithy/protocol-http" "^5.3.0" + "@smithy/types" "^4.6.0" + "@smithy/util-hex-encoding" "^4.2.0" + "@smithy/util-middleware" "^4.2.0" + "@smithy/util-uri-escape" "^4.2.0" + "@smithy/util-utf8" "^4.2.0" tslib "^2.6.2" -"@smithy/smithy-client@^3.4.4", "@smithy/smithy-client@^3.5.0": - version "3.5.0" - resolved "https://registry.npmjs.org/@smithy/smithy-client/-/smithy-client-3.5.0.tgz" - integrity sha512-Y8FeOa7gbDfCWf7njrkoRATPa5eNLUEjlJS5z5rXatYuGkCb80LbHcu8AQR8qgAZZaNHCLyo2N+pxPsV7l+ivg== - dependencies: - "@smithy/core" "^2.5.5" - "@smithy/middleware-endpoint" "^3.2.5" - "@smithy/middleware-stack" "^3.0.11" - "@smithy/protocol-http" "^4.1.8" - "@smithy/types" "^3.7.2" - "@smithy/util-stream" "^3.3.2" +"@smithy/smithy-client@^4.7.0": + version "4.7.0" + resolved "https://registry.yarnpkg.com/@smithy/smithy-client/-/smithy-client-4.7.0.tgz#1b0b74a3f58bdf7a77024473b6fe6ec1aa9556c2" + integrity sha512-3BDx/aCCPf+kkinYf5QQhdQ9UAGihgOVqI3QO5xQfSaIWvUE4KYLtiGRWsNe1SR7ijXC0QEPqofVp5Sb0zC8xQ== + dependencies: + "@smithy/core" "^3.14.0" + "@smithy/middleware-endpoint" "^4.3.0" + "@smithy/middleware-stack" "^4.2.0" + "@smithy/protocol-http" "^5.3.0" + "@smithy/types" "^4.6.0" + "@smithy/util-stream" "^4.4.0" tslib "^2.6.2" -"@smithy/types@^3.7.1", "@smithy/types@^3.7.2": - version "3.7.2" - resolved "https://registry.npmjs.org/@smithy/types/-/types-3.7.2.tgz" - integrity sha512-bNwBYYmN8Eh9RyjS1p2gW6MIhSO2rl7X9QeLM8iTdcGRP+eDiIWDt66c9IysCc22gefKszZv+ubV9qZc7hdESg== +"@smithy/types@^3.7.1", "@smithy/types@^4.6.0": + version "4.6.0" + resolved "https://registry.yarnpkg.com/@smithy/types/-/types-4.6.0.tgz#8ea8b15fedee3cdc555e8f947ce35fb1e973bb7a" + integrity sha512-4lI9C8NzRPOv66FaY1LL1O/0v0aLVrq/mXP/keUa9mJOApEeae43LsLd2kZRUJw91gxOQfLIrV3OvqPgWz1YsA== dependencies: tslib "^2.6.2" -"@smithy/url-parser@^3.0.10", "@smithy/url-parser@^3.0.11": - version "3.0.11" - resolved "https://registry.npmjs.org/@smithy/url-parser/-/url-parser-3.0.11.tgz" - integrity sha512-TmlqXkSk8ZPhfc+SQutjmFr5FjC0av3GZP4B/10caK1SbRwe/v+Wzu/R6xEKxoNqL+8nY18s1byiy6HqPG37Aw== +"@smithy/url-parser@^4.2.0": + version "4.2.0" + resolved "https://registry.yarnpkg.com/@smithy/url-parser/-/url-parser-4.2.0.tgz#b6d6e739233ae120e4d6725b04375cb87791491f" + integrity sha512-AlBmD6Idav2ugmoAL6UtR6ItS7jU5h5RNqLMZC7QrLCoITA9NzIN3nx9GWi8g4z1pfWh2r9r96SX/jHiNwPJ9A== dependencies: - "@smithy/querystring-parser" "^3.0.11" - "@smithy/types" "^3.7.2" + "@smithy/querystring-parser" "^4.2.0" + "@smithy/types" "^4.6.0" tslib "^2.6.2" -"@smithy/util-base64@^3.0.0": - version "3.0.0" - resolved "https://registry.npmjs.org/@smithy/util-base64/-/util-base64-3.0.0.tgz" - integrity sha512-Kxvoh5Qtt0CDsfajiZOCpJxgtPHXOKwmM+Zy4waD43UoEMA+qPxxa98aE/7ZhdnBFZFXMOiBR5xbcaMhLtznQQ== +"@smithy/util-base64@^4.2.0": + version "4.2.0" + resolved "https://registry.yarnpkg.com/@smithy/util-base64/-/util-base64-4.2.0.tgz#677f616772389adbad278b05d84835abbfe63bbc" + integrity sha512-+erInz8WDv5KPe7xCsJCp+1WCjSbah9gWcmUXc9NqmhyPx59tf7jqFz+za1tRG1Y5KM1Cy1rWCcGypylFp4mvA== dependencies: - "@smithy/util-buffer-from" "^3.0.0" - "@smithy/util-utf8" "^3.0.0" + "@smithy/util-buffer-from" "^4.2.0" + "@smithy/util-utf8" "^4.2.0" tslib "^2.6.2" -"@smithy/util-body-length-browser@^3.0.0": - version "3.0.0" - resolved "https://registry.npmjs.org/@smithy/util-body-length-browser/-/util-body-length-browser-3.0.0.tgz" - integrity sha512-cbjJs2A1mLYmqmyVl80uoLTJhAcfzMOyPgjwAYusWKMdLeNtzmMz9YxNl3/jRLoxSS3wkqkf0jwNdtXWtyEBaQ== +"@smithy/util-body-length-browser@^4.2.0": + version "4.2.0" + resolved "https://registry.yarnpkg.com/@smithy/util-body-length-browser/-/util-body-length-browser-4.2.0.tgz#04e9fc51ee7a3e7f648a4b4bcdf96c350cfa4d61" + integrity sha512-Fkoh/I76szMKJnBXWPdFkQJl2r9SjPt3cMzLdOB6eJ4Pnpas8hVoWPYemX/peO0yrrvldgCUVJqOAjUrOLjbxg== dependencies: tslib "^2.6.2" -"@smithy/util-body-length-node@^3.0.0": - version "3.0.0" - resolved "https://registry.npmjs.org/@smithy/util-body-length-node/-/util-body-length-node-3.0.0.tgz" - integrity sha512-Tj7pZ4bUloNUP6PzwhN7K386tmSmEET9QtQg0TgdNOnxhZvCssHji+oZTUIuzxECRfG8rdm2PMw2WCFs6eIYkA== +"@smithy/util-body-length-node@^4.2.0": + version "4.2.0" + resolved "https://registry.yarnpkg.com/@smithy/util-body-length-node/-/util-body-length-node-4.2.0.tgz#ea6a0fdabb48dd0b212e17e42b1f07bb7373147b" + integrity sha512-U8q1WsSZFjXijlD7a4wsDQOvOwV+72iHSfq1q7VD+V75xP/pdtm0WIGuaFJ3gcADDOKj2MIBn4+zisi140HEnQ== dependencies: tslib "^2.6.2" @@ -2143,96 +2116,96 @@ "@smithy/is-array-buffer" "^2.2.0" tslib "^2.6.2" -"@smithy/util-buffer-from@^3.0.0": - version "3.0.0" - resolved "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-3.0.0.tgz" - integrity sha512-aEOHCgq5RWFbP+UDPvPot26EJHjOC+bRgse5A8V3FSShqd5E5UN4qc7zkwsvJPPAVsf73QwYcHN1/gt/rtLwQA== +"@smithy/util-buffer-from@^4.2.0": + version "4.2.0" + resolved "https://registry.yarnpkg.com/@smithy/util-buffer-from/-/util-buffer-from-4.2.0.tgz#7abd12c4991b546e7cee24d1e8b4bfaa35c68a9d" + integrity sha512-kAY9hTKulTNevM2nlRtxAG2FQ3B2OR6QIrPY3zE5LqJy1oxzmgBGsHLWTcNhWXKchgA0WHW+mZkQrng/pgcCew== dependencies: - "@smithy/is-array-buffer" "^3.0.0" + "@smithy/is-array-buffer" "^4.2.0" tslib "^2.6.2" -"@smithy/util-config-provider@^3.0.0": - version "3.0.0" - resolved "https://registry.npmjs.org/@smithy/util-config-provider/-/util-config-provider-3.0.0.tgz" - integrity sha512-pbjk4s0fwq3Di/ANL+rCvJMKM5bzAQdE5S/6RL5NXgMExFAi6UgQMPOm5yPaIWPpr+EOXKXRonJ3FoxKf4mCJQ== +"@smithy/util-config-provider@^4.2.0": + version "4.2.0" + resolved "https://registry.yarnpkg.com/@smithy/util-config-provider/-/util-config-provider-4.2.0.tgz#2e4722937f8feda4dcb09672c59925a4e6286cfc" + integrity sha512-YEjpl6XJ36FTKmD+kRJJWYvrHeUvm5ykaUS5xK+6oXffQPHeEM4/nXlZPe+Wu0lsgRUcNZiliYNh/y7q9c2y6Q== dependencies: tslib "^2.6.2" -"@smithy/util-defaults-mode-browser@^3.0.27": - version "3.0.30" - resolved "https://registry.npmjs.org/@smithy/util-defaults-mode-browser/-/util-defaults-mode-browser-3.0.30.tgz" - integrity sha512-nLuGmgfcr0gzm64pqF2UT4SGWVG8UGviAdayDlVzJPNa6Z4lqvpDzdRXmLxtOdEjVlTOEdpZ9dd3ZMMu488mzg== +"@smithy/util-defaults-mode-browser@^4.2.0": + version "4.2.0" + resolved "https://registry.yarnpkg.com/@smithy/util-defaults-mode-browser/-/util-defaults-mode-browser-4.2.0.tgz#7b9f0299203aaa48953c4997c1630bdeffd80ec0" + integrity sha512-qzHp7ZDk1Ba4LDwQVCNp90xPGqSu7kmL7y5toBpccuhi3AH7dcVBIT/pUxYcInK4jOy6FikrcTGq5wxcka8UaQ== dependencies: - "@smithy/property-provider" "^3.1.11" - "@smithy/smithy-client" "^3.5.0" - "@smithy/types" "^3.7.2" + "@smithy/property-provider" "^4.2.0" + "@smithy/smithy-client" "^4.7.0" + "@smithy/types" "^4.6.0" bowser "^2.11.0" tslib "^2.6.2" -"@smithy/util-defaults-mode-node@^3.0.27": - version "3.0.30" - resolved "https://registry.npmjs.org/@smithy/util-defaults-mode-node/-/util-defaults-mode-node-3.0.30.tgz" - integrity sha512-OD63eWoH68vp75mYcfYyuVH+p7Li/mY4sYOROnauDrtObo1cS4uWfsy/zhOTW8F8ZPxQC1ZXZKVxoxvMGUv2Ow== - dependencies: - "@smithy/config-resolver" "^3.0.13" - "@smithy/credential-provider-imds" "^3.2.8" - "@smithy/node-config-provider" "^3.1.12" - "@smithy/property-provider" "^3.1.11" - "@smithy/smithy-client" "^3.5.0" - "@smithy/types" "^3.7.2" +"@smithy/util-defaults-mode-node@^4.2.0": + version "4.2.0" + resolved "https://registry.yarnpkg.com/@smithy/util-defaults-mode-node/-/util-defaults-mode-node-4.2.0.tgz#efe5a6be134755317a0edf9595582bd6732e493a" + integrity sha512-FxUHS3WXgx3bTWR6yQHNHHkQHZm/XKIi/CchTnKvBulN6obWpcbzJ6lDToXn+Wp0QlVKd7uYAz2/CTw1j7m+Kg== + dependencies: + "@smithy/config-resolver" "^4.3.0" + "@smithy/credential-provider-imds" "^4.2.0" + "@smithy/node-config-provider" "^4.3.0" + "@smithy/property-provider" "^4.2.0" + "@smithy/smithy-client" "^4.7.0" + "@smithy/types" "^4.6.0" tslib "^2.6.2" -"@smithy/util-endpoints@^2.1.6": - version "2.1.7" - resolved "https://registry.npmjs.org/@smithy/util-endpoints/-/util-endpoints-2.1.7.tgz" - integrity sha512-tSfcqKcN/Oo2STEYCABVuKgJ76nyyr6skGl9t15hs+YaiU06sgMkN7QYjo0BbVw+KT26zok3IzbdSOksQ4YzVw== +"@smithy/util-endpoints@^3.2.0": + version "3.2.0" + resolved "https://registry.yarnpkg.com/@smithy/util-endpoints/-/util-endpoints-3.2.0.tgz#4bdc4820ceab5d66365ee72cfb14226e10bb0e24" + integrity sha512-TXeCn22D56vvWr/5xPqALc9oO+LN+QpFjrSM7peG/ckqEPoI3zaKZFp+bFwfmiHhn5MGWPaLCqDOJPPIixk9Wg== dependencies: - "@smithy/node-config-provider" "^3.1.12" - "@smithy/types" "^3.7.2" + "@smithy/node-config-provider" "^4.3.0" + "@smithy/types" "^4.6.0" tslib "^2.6.2" -"@smithy/util-hex-encoding@^3.0.0": - version "3.0.0" - resolved "https://registry.npmjs.org/@smithy/util-hex-encoding/-/util-hex-encoding-3.0.0.tgz" - integrity sha512-eFndh1WEK5YMUYvy3lPlVmYY/fZcQE1D8oSf41Id2vCeIkKJXPcYDCZD+4+xViI6b1XSd7tE+s5AmXzz5ilabQ== +"@smithy/util-hex-encoding@^4.2.0": + version "4.2.0" + resolved "https://registry.yarnpkg.com/@smithy/util-hex-encoding/-/util-hex-encoding-4.2.0.tgz#1c22ea3d1e2c3a81ff81c0a4f9c056a175068a7b" + integrity sha512-CCQBwJIvXMLKxVbO88IukazJD9a4kQ9ZN7/UMGBjBcJYvatpWk+9g870El4cB8/EJxfe+k+y0GmR9CAzkF+Nbw== dependencies: tslib "^2.6.2" -"@smithy/util-middleware@^3.0.10", "@smithy/util-middleware@^3.0.11": - version "3.0.11" - resolved "https://registry.npmjs.org/@smithy/util-middleware/-/util-middleware-3.0.11.tgz" - integrity sha512-dWpyc1e1R6VoXrwLoLDd57U1z6CwNSdkM69Ie4+6uYh2GC7Vg51Qtan7ITzczuVpqezdDTKJGJB95fFvvjU/ow== +"@smithy/util-middleware@^4.2.0": + version "4.2.0" + resolved "https://registry.yarnpkg.com/@smithy/util-middleware/-/util-middleware-4.2.0.tgz#85973ae0db65af4ab4bedf12f31487a4105d1158" + integrity sha512-u9OOfDa43MjagtJZ8AapJcmimP+K2Z7szXn8xbty4aza+7P1wjFmy2ewjSbhEiYQoW1unTlOAIV165weYAaowA== dependencies: - "@smithy/types" "^3.7.2" + "@smithy/types" "^4.6.0" tslib "^2.6.2" -"@smithy/util-retry@^3.0.10", "@smithy/util-retry@^3.0.11": - version "3.0.11" - resolved "https://registry.npmjs.org/@smithy/util-retry/-/util-retry-3.0.11.tgz" - integrity sha512-hJUC6W7A3DQgaee3Hp9ZFcOxVDZzmBIRBPlUAk8/fSOEl7pE/aX7Dci0JycNOnm9Mfr0KV2XjIlUOcGWXQUdVQ== +"@smithy/util-retry@^4.2.0": + version "4.2.0" + resolved "https://registry.yarnpkg.com/@smithy/util-retry/-/util-retry-4.2.0.tgz#1fa58e277b62df98d834e6c8b7d57f4c62ff1baf" + integrity sha512-BWSiuGbwRnEE2SFfaAZEX0TqaxtvtSYPM/J73PFVm+A29Fg1HTPiYFb8TmX1DXp4hgcdyJcNQmprfd5foeORsg== dependencies: - "@smithy/service-error-classification" "^3.0.11" - "@smithy/types" "^3.7.2" + "@smithy/service-error-classification" "^4.2.0" + "@smithy/types" "^4.6.0" tslib "^2.6.2" -"@smithy/util-stream@^3.3.1", "@smithy/util-stream@^3.3.2": - version "3.3.2" - resolved "https://registry.npmjs.org/@smithy/util-stream/-/util-stream-3.3.2.tgz" - integrity sha512-sInAqdiVeisUGYAv/FrXpmJ0b4WTFmciTRqzhb7wVuem9BHvhIG7tpiYHLDWrl2stOokNZpTTGqz3mzB2qFwXg== - dependencies: - "@smithy/fetch-http-handler" "^4.1.2" - "@smithy/node-http-handler" "^3.3.2" - "@smithy/types" "^3.7.2" - "@smithy/util-base64" "^3.0.0" - "@smithy/util-buffer-from" "^3.0.0" - "@smithy/util-hex-encoding" "^3.0.0" - "@smithy/util-utf8" "^3.0.0" +"@smithy/util-stream@^4.0.0", "@smithy/util-stream@^4.4.0": + version "4.4.0" + resolved "https://registry.yarnpkg.com/@smithy/util-stream/-/util-stream-4.4.0.tgz#e203c74b8664d0e3f537185de5da960655333a45" + integrity sha512-vtO7ktbixEcrVzMRmpQDnw/Ehr9UWjBvSJ9fyAbadKkC4w5Cm/4lMO8cHz8Ysb8uflvQUNRcuux/oNHKPXkffg== + dependencies: + "@smithy/fetch-http-handler" "^5.3.0" + "@smithy/node-http-handler" "^4.3.0" + "@smithy/types" "^4.6.0" + "@smithy/util-base64" "^4.2.0" + "@smithy/util-buffer-from" "^4.2.0" + "@smithy/util-hex-encoding" "^4.2.0" + "@smithy/util-utf8" "^4.2.0" tslib "^2.6.2" -"@smithy/util-uri-escape@^3.0.0": - version "3.0.0" - resolved "https://registry.npmjs.org/@smithy/util-uri-escape/-/util-uri-escape-3.0.0.tgz" - integrity sha512-LqR7qYLgZTD7nWLBecUi4aqolw8Mhza9ArpNEQ881MJJIU2sE5iHCK6TdyqqzcDLy0OPe10IY4T8ctVdtynubg== +"@smithy/util-uri-escape@^4.2.0": + version "4.2.0" + resolved "https://registry.yarnpkg.com/@smithy/util-uri-escape/-/util-uri-escape-4.2.0.tgz#096a4cec537d108ac24a68a9c60bee73fc7e3a9e" + integrity sha512-igZpCKV9+E/Mzrpq6YacdTQ0qTiLm85gD6N/IrmyDvQFA4UnU3d5g3m8tMT/6zG/vVkWSU+VxeUyGonL62DuxA== dependencies: tslib "^2.6.2" @@ -2244,21 +2217,28 @@ "@smithy/util-buffer-from" "^2.2.0" tslib "^2.6.2" -"@smithy/util-utf8@^3.0.0": - version "3.0.0" - resolved "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-3.0.0.tgz" - integrity sha512-rUeT12bxFnplYDe815GXbq/oixEGHfRFFtcTF3YdDi/JaENIM6aSYYLJydG83UNzLXeRI5K8abYd/8Sp/QM0kA== +"@smithy/util-utf8@^4.2.0": + version "4.2.0" + resolved "https://registry.yarnpkg.com/@smithy/util-utf8/-/util-utf8-4.2.0.tgz#8b19d1514f621c44a3a68151f3d43e51087fed9d" + integrity sha512-zBPfuzoI8xyBtR2P6WQj63Rz8i3AmfAaJLuNG8dWsfvPe8lO4aCPYLn879mEgHndZH1zQ2oXmG8O1GGzzaoZiw== dependencies: - "@smithy/util-buffer-from" "^3.0.0" + "@smithy/util-buffer-from" "^4.2.0" tslib "^2.6.2" -"@smithy/util-waiter@^3.1.9": - version "3.2.0" - resolved "https://registry.npmjs.org/@smithy/util-waiter/-/util-waiter-3.2.0.tgz" - integrity sha512-PpjSboaDUE6yl+1qlg3Si57++e84oXdWGbuFUSAciXsVfEZJJJupR2Nb0QuXHiunt2vGR+1PTizOMvnUPaG2Qg== +"@smithy/util-waiter@^4.2.0": + version "4.2.0" + resolved "https://registry.yarnpkg.com/@smithy/util-waiter/-/util-waiter-4.2.0.tgz#fcf5609143fa745d45424b0463560425b39c34eb" + integrity sha512-0Z+nxUU4/4T+SL8BCNN4ztKdQjToNvUYmkF1kXO5T7Yz3Gafzh0HeIG6mrkN8Fz3gn9hSyxuAT+6h4vM+iQSBQ== + dependencies: + "@smithy/abort-controller" "^4.2.0" + "@smithy/types" "^4.6.0" + tslib "^2.6.2" + +"@smithy/uuid@^1.1.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@smithy/uuid/-/uuid-1.1.0.tgz#9fd09d3f91375eab94f478858123387df1cda987" + integrity sha512-4aUIteuyxtBUhVdiQqcDhKFitwfd9hqoSDYY2KRXiWtgoWJ9Bmise+KfEPDiVHWeJepvF8xJO9/9+WDIciMFFw== dependencies: - "@smithy/abort-controller" "^3.1.9" - "@smithy/types" "^3.7.2" tslib "^2.6.2" "@tsconfig/node10@^1.0.7": @@ -2412,11 +2392,6 @@ resolved "https://registry.npmjs.org/@types/triple-beam/-/triple-beam-1.3.5.tgz" integrity sha512-6WaYesThRMCl19iryMYP7/x2OVgCtbIVflDGFpWnb9irXI3UjYE4AzmYuiUKY1AJstGijoY+MgUszMgRxIYTYw== -"@types/uuid@^9.0.1": - version "9.0.8" - resolved "https://registry.npmjs.org/@types/uuid/-/uuid-9.0.8.tgz" - integrity sha512-jg+97EGIcY9AGHJJRaaPVgetKDsrTgbRjQ5Msgjh/DQKEFl0DtyRr/VCOyD1T2R1MNeWPK/u7JoGhlDZnKBAfA== - "@types/yargs-parser@*": version "21.0.3" resolved "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-21.0.3.tgz" @@ -3507,12 +3482,12 @@ fast-levenshtein@^2.0.6: resolved "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz" integrity sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw== -fast-xml-parser@4.4.1: - version "4.4.1" - resolved "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-4.4.1.tgz" - integrity sha512-xkjOecfnKGkSsOwtZ5Pz7Us/T6mrbPQrq0nh+aCO5V9nk5NLWmasAHumTKjiPJPWANe+kAZ84Jc8ooJkzZ88Sw== +fast-xml-parser@5.2.5: + version "5.2.5" + resolved "https://registry.yarnpkg.com/fast-xml-parser/-/fast-xml-parser-5.2.5.tgz#4809fdfb1310494e341098c25cb1341a01a9144a" + integrity sha512-pfX9uG9Ki0yekDHx2SiuRIyFdyAr1kMIMitPvb0YBo8SUfKvia7w7FIyd/l6av85pFYRhZscS75MwMnbvY+hcQ== dependencies: - strnum "^1.0.5" + strnum "^2.1.0" fastq@^1.6.0: version "1.17.1" @@ -4728,10 +4703,10 @@ node-releases@^2.0.18: resolved "https://registry.npmjs.org/node-releases/-/node-releases-2.0.18.tgz" integrity sha512-d9VeXT4SJ7ZeOqGX6R5EM022wpL+eWPooLI+5UpWn2jCT1aosUQEhQP214x33Wkwx3JQMvIm+tIoVOdodFS40g== -nodemailer@^6.9.12: - version "6.9.16" - resolved "https://registry.npmjs.org/nodemailer/-/nodemailer-6.9.16.tgz" - integrity sha512-psAuZdTIRN08HKVd/E8ObdV6NO7NTBY3KsC30F7M4H1OnmLCUNaS56FpYxyb26zWLSyYF9Ozch9KYHhHegsiOQ== +nodemailer@^7.0.7: + version "7.0.9" + resolved "https://registry.yarnpkg.com/nodemailer/-/nodemailer-7.0.9.tgz#fe5abd4173e08e01aa243c7cddd612ad8c6ccc18" + integrity sha512-9/Qm0qXIByEP8lEV2qOqcAW7bRpL8CR9jcTwk3NBnHJNmP9fIJ86g2fgmIXqHY+nj55ZEMwWqYAT2QTDpRUYiQ== normalize-path@^3.0.0, normalize-path@~3.0.0: version "3.0.0" @@ -5297,10 +5272,10 @@ strip-json-comments@^3.1.1: resolved "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz" integrity sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig== -strnum@^1.0.5: - version "1.0.5" - resolved "https://registry.npmjs.org/strnum/-/strnum-1.0.5.tgz" - integrity sha512-J8bbNyKKXl5qYcR36TIO8W3mVGVHrmmxsd5PAItGkmyzwJvybiw2IVq5nqd0i4LSNSkB/sx9VHllbfFdr9k1JA== +strnum@^2.1.0: + version "2.1.1" + resolved "https://registry.yarnpkg.com/strnum/-/strnum-2.1.1.tgz#cf2a6e0cf903728b8b2c4b971b7e36b4e82d46ab" + integrity sha512-7ZvoFTiCnGxBtDqJ//Cu6fWtZtc7Y3x+QOirG15wztbdngGSkht27o2pyGWrVy0b4WAy3jbKmnoK6g5VlVNUUw== supports-color@^7, supports-color@^7.1.0: version "7.2.0" @@ -5492,11 +5467,6 @@ uuid@8.0.0: resolved "https://registry.npmjs.org/uuid/-/uuid-8.0.0.tgz" integrity sha512-jOXGuXZAWdsTH7eZLtyXMqUb9EcWMGZNbL9YcGBJl4MH4nrxHmZJhEHvyLFrkxo+28uLb/NYRcStH48fnD0Vzw== -uuid@^9.0.1: - version "9.0.1" - resolved "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz" - integrity sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA== - v8-compile-cache-lib@^3.0.1: version "3.0.1" resolved "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz" From 101f26c204eb95b4e74b029a759292b5aef2e4ce Mon Sep 17 00:00:00 2001 From: Landon Shumway Date: Tue, 7 Oct 2025 15:46:27 -0500 Subject: [PATCH 51/55] PR feedback --- .../compact-connect/lambdas/nodejs/package.json | 4 ---- .../lib/email/ingest-event-email-service.test.ts | 14 +++++++------- backend/compact-connect/lambdas/nodejs/yarn.lock | 11 +++++++++-- 3 files changed, 16 insertions(+), 13 deletions(-) diff --git a/backend/compact-connect/lambdas/nodejs/package.json b/backend/compact-connect/lambdas/nodejs/package.json index 747edcde7..7e77b6590 100644 --- a/backend/compact-connect/lambdas/nodejs/package.json +++ b/backend/compact-connect/lambdas/nodejs/package.json @@ -13,10 +13,6 @@ }, "author": "Inspiring Apps", "license": "UNLICENSED", - "resolutions": { - "@smithy/types": "^4.6.0", - "@smithy/util-stream": "^4.0.0" - }, "devDependencies": { "@types/aws-lambda": "8.10.145", "@types/jest": "^29.5.12", diff --git a/backend/compact-connect/lambdas/nodejs/tests/lib/email/ingest-event-email-service.test.ts b/backend/compact-connect/lambdas/nodejs/tests/lib/email/ingest-event-email-service.test.ts index d3516234f..bb707419c 100644 --- a/backend/compact-connect/lambdas/nodejs/tests/lib/email/ingest-event-email-service.test.ts +++ b/backend/compact-connect/lambdas/nodejs/tests/lib/email/ingest-event-email-service.test.ts @@ -68,7 +68,7 @@ describe('IngestEventEmailService', () => { ingestFailures: [ SAMPLE_UNMARSHALLED_INGEST_FAILURE_ERROR_RECORD ], validationErrors: [ SAMPLE_UNMARSHALLED_VALIDATION_ERROR_RECORD ] }, - 'aslp', + 'Audiology and Speech Language Pathology', 'Ohio' ); @@ -83,7 +83,7 @@ describe('IngestEventEmailService', () => { ingestFailures: [ SAMPLE_UNMARSHALLED_INGEST_FAILURE_ERROR_RECORD ], validationErrors: [ SAMPLE_UNMARSHALLED_VALIDATION_ERROR_RECORD ] }, - 'aslp', + 'Audiology and Speech Language Pathology', 'Ohio', [ 'operations@example.com' @@ -107,7 +107,7 @@ describe('IngestEventEmailService', () => { }, Subject: { Charset: 'UTF-8', - Data: 'License Data Error Summary: aslp / Ohio' + Data: 'License Data Error Summary: Audiology and Speech Language Pathology / Ohio' } } }, @@ -132,7 +132,7 @@ describe('IngestEventEmailService', () => { it('should send an alls well email', async () => { const messageId = await emailService.sendAllsWellEmail( - 'aslp', + 'Audiology and Speech Language Pathology', 'Ohio', [ 'operations@example.com' ] ); @@ -154,7 +154,7 @@ describe('IngestEventEmailService', () => { }, Subject: { Charset: 'UTF-8', - Data: 'License Data Summary: aslp / Ohio' + Data: 'License Data Summary: Audiology and Speech Language Pathology / Ohio' } } }, @@ -165,7 +165,7 @@ describe('IngestEventEmailService', () => { it('should send a "no license updates" email with expected image url', async () => { const messageId = await emailService.sendNoLicenseUpdatesEmail( - 'aslp', + 'Audiology and Speech Language Pathology', 'Ohio', [ 'operations@example.com' ] ); @@ -187,7 +187,7 @@ describe('IngestEventEmailService', () => { }, Subject: { Charset: 'UTF-8', - Data: 'No License Updates for Last 7 Days: aslp / Ohio' + Data: 'No License Updates for Last 7 Days: Audiology and Speech Language Pathology / Ohio' } } }, diff --git a/backend/compact-connect/lambdas/nodejs/yarn.lock b/backend/compact-connect/lambdas/nodejs/yarn.lock index a63ad9fc4..89f1110b3 100644 --- a/backend/compact-connect/lambdas/nodejs/yarn.lock +++ b/backend/compact-connect/lambdas/nodejs/yarn.lock @@ -2069,7 +2069,14 @@ "@smithy/util-stream" "^4.4.0" tslib "^2.6.2" -"@smithy/types@^3.7.1", "@smithy/types@^4.6.0": +"@smithy/types@^3.7.1": + version "3.7.2" + resolved "https://registry.yarnpkg.com/@smithy/types/-/types-3.7.2.tgz#05cb14840ada6f966de1bf9a9c7dd86027343e10" + integrity sha512-bNwBYYmN8Eh9RyjS1p2gW6MIhSO2rl7X9QeLM8iTdcGRP+eDiIWDt66c9IysCc22gefKszZv+ubV9qZc7hdESg== + dependencies: + tslib "^2.6.2" + +"@smithy/types@^4.6.0": version "4.6.0" resolved "https://registry.yarnpkg.com/@smithy/types/-/types-4.6.0.tgz#8ea8b15fedee3cdc555e8f947ce35fb1e973bb7a" integrity sha512-4lI9C8NzRPOv66FaY1LL1O/0v0aLVrq/mXP/keUa9mJOApEeae43LsLd2kZRUJw91gxOQfLIrV3OvqPgWz1YsA== @@ -2188,7 +2195,7 @@ "@smithy/types" "^4.6.0" tslib "^2.6.2" -"@smithy/util-stream@^4.0.0", "@smithy/util-stream@^4.4.0": +"@smithy/util-stream@^4.4.0": version "4.4.0" resolved "https://registry.yarnpkg.com/@smithy/util-stream/-/util-stream-4.4.0.tgz#e203c74b8664d0e3f537185de5da960655333a45" integrity sha512-vtO7ktbixEcrVzMRmpQDnw/Ehr9UWjBvSJ9fyAbadKkC4w5Cm/4lMO8cHz8Ysb8uflvQUNRcuux/oNHKPXkffg== From 06ada5e95e7fd4e6bcad44f1f654159eea65f2d9 Mon Sep 17 00:00:00 2001 From: Landon Shumway Date: Wed, 8 Oct 2025 10:55:14 -0500 Subject: [PATCH 52/55] PR feedback --- .../python/feature-flag/custom_resource_handler.py | 2 +- .../python/feature-flag/feature_flag_client.py | 3 ++- .../feature-flag/handlers/manage_feature_flag.py | 2 +- .../tests/function/test_manage_feature_flag.py | 6 ++++-- .../tests/function/test_statsig_client.py | 11 +---------- 5 files changed, 9 insertions(+), 15 deletions(-) diff --git a/backend/compact-connect/lambdas/python/feature-flag/custom_resource_handler.py b/backend/compact-connect/lambdas/python/feature-flag/custom_resource_handler.py index fc8c0c14e..cb46deb96 100644 --- a/backend/compact-connect/lambdas/python/feature-flag/custom_resource_handler.py +++ b/backend/compact-connect/lambdas/python/feature-flag/custom_resource_handler.py @@ -18,7 +18,7 @@ class CustomResourceResponse(TypedDict, total=False): class CustomResourceHandler(ABC): """Base class for custom resource migrations. - This class provides a framework for implementing temporary data migrations as custom resources. + This class provides a framework for implementing CloudFormation custom resources. It handles the routing of CloudFormation events to appropriate methods and provides a consistent logging pattern. diff --git a/backend/compact-connect/lambdas/python/feature-flag/feature_flag_client.py b/backend/compact-connect/lambdas/python/feature-flag/feature_flag_client.py index 88b786d61..897cb2227 100644 --- a/backend/compact-connect/lambdas/python/feature-flag/feature_flag_client.py +++ b/backend/compact-connect/lambdas/python/feature-flag/feature_flag_client.py @@ -404,7 +404,7 @@ def _create_new_gate( } ], } - + # API spec https://docs.statsig.com/console-api/all-endpoints-generated#post-/console/v1/gates response = self._make_console_api_request('POST', '/gates', gate_payload) if response.status_code in [200, 201]: @@ -512,6 +512,7 @@ def get_flag(self, flag_name: str) -> dict[str, Any] | None: if response.status_code == 200: gates_data = response.json() + # API spec https://docs.statsig.com/console-api/all-endpoints-generated#get-/console/v1/gates for gate in gates_data.get('data', []): if gate.get('name') == flag_name: return gate diff --git a/backend/compact-connect/lambdas/python/feature-flag/handlers/manage_feature_flag.py b/backend/compact-connect/lambdas/python/feature-flag/handlers/manage_feature_flag.py index 0adbf3831..f729d1f2e 100644 --- a/backend/compact-connect/lambdas/python/feature-flag/handlers/manage_feature_flag.py +++ b/backend/compact-connect/lambdas/python/feature-flag/handlers/manage_feature_flag.py @@ -53,7 +53,7 @@ def on_create(self, properties: dict) -> CustomResourceResponse | None: return None # Extract gate ID from response - gate_id = flag_data.get('data', {}).get('id') or flag_data.get('id') + gate_id = flag_data.get('data', {}).get('id') logger.info('Feature flag resource created/updated successfully', flag_name=flag_name, gate_id=gate_id) diff --git a/backend/compact-connect/lambdas/python/feature-flag/tests/function/test_manage_feature_flag.py b/backend/compact-connect/lambdas/python/feature-flag/tests/function/test_manage_feature_flag.py index a6bef554f..1d0700686 100644 --- a/backend/compact-connect/lambdas/python/feature-flag/tests/function/test_manage_feature_flag.py +++ b/backend/compact-connect/lambdas/python/feature-flag/tests/function/test_manage_feature_flag.py @@ -36,7 +36,8 @@ def test_on_create_calls_upsert_flag_with_correct_params(self, mock_client_class # Set up mock client instance mock_client = MagicMock() - mock_client.upsert_flag.return_value = {'id': 'test-flag', 'name': 'test-flag'} + # API spec https://docs.statsig.com/console-api/all-endpoints-generated#post-/console/v1/gates + mock_client.upsert_flag.return_value = {'data':{'id': 'test-flag', 'name': 'test-flag'}} mock_client_class.return_value = mock_client handler = ManageFeatureFlagHandler() @@ -62,7 +63,8 @@ def test_on_create_with_minimal_properties(self, mock_client_class): # Set up mock client instance mock_client = MagicMock() - mock_client.upsert_flag.return_value = {'id': 'minimal-flag', 'name': 'minimal-flag'} + # API spec https://docs.statsig.com/console-api/all-endpoints-generated#post-/console/v1/gates + mock_client.upsert_flag.return_value = {'data': {'id': 'minimal-flag', 'name': 'minimal-flag'}} mock_client_class.return_value = mock_client handler = ManageFeatureFlagHandler() diff --git a/backend/compact-connect/lambdas/python/feature-flag/tests/function/test_statsig_client.py b/backend/compact-connect/lambdas/python/feature-flag/tests/function/test_statsig_client.py index 4e03cb2f3..0a1fec0cf 100644 --- a/backend/compact-connect/lambdas/python/feature-flag/tests/function/test_statsig_client.py +++ b/backend/compact-connect/lambdas/python/feature-flag/tests/function/test_statsig_client.py @@ -28,7 +28,7 @@ def setUp(self): # Set up mock secrets manager with StatSig credentials secrets_client = self.create_mock_secrets_manager() - for env in ['test', 'beta', 'prod']: + for env in ['sandbox', 'test', 'beta', 'prod']: secrets_client.create_secret( Name=f'compact-connect/env/{env}/statsig/credentials', SecretString=json.dumps({'serverKey': MOCK_SERVER_KEY, 'consoleKey': MOCK_CONSOLE_KEY}), @@ -215,15 +215,6 @@ def test_environment_tier_mapping(self, mock_statsig): ] for cc_env, expected_tier in test_cases: - # Set up secret for this environment - secrets_client = self.create_mock_secrets_manager() - # note that the test environment secret is created as part of setup, so we don't add that here - if cc_env != 'test': - secrets_client.create_secret( - Name=f'compact-connect/env/{cc_env}/statsig/credentials', - SecretString=json.dumps({'serverKey': MOCK_SERVER_KEY, 'consoleKey': MOCK_CONSOLE_KEY}), - ) - # Create client StatSigFeatureFlagClient(environment=cc_env) From 6c85a084f3353a6bb416d6786fa817904d360079 Mon Sep 17 00:00:00 2001 From: Landon Shumway Date: Wed, 8 Oct 2025 10:57:11 -0500 Subject: [PATCH 53/55] formatting --- .../feature-flag/tests/function/test_manage_feature_flag.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/compact-connect/lambdas/python/feature-flag/tests/function/test_manage_feature_flag.py b/backend/compact-connect/lambdas/python/feature-flag/tests/function/test_manage_feature_flag.py index 1d0700686..f71b8e961 100644 --- a/backend/compact-connect/lambdas/python/feature-flag/tests/function/test_manage_feature_flag.py +++ b/backend/compact-connect/lambdas/python/feature-flag/tests/function/test_manage_feature_flag.py @@ -37,7 +37,7 @@ def test_on_create_calls_upsert_flag_with_correct_params(self, mock_client_class # Set up mock client instance mock_client = MagicMock() # API spec https://docs.statsig.com/console-api/all-endpoints-generated#post-/console/v1/gates - mock_client.upsert_flag.return_value = {'data':{'id': 'test-flag', 'name': 'test-flag'}} + mock_client.upsert_flag.return_value = {'data': {'id': 'test-flag', 'name': 'test-flag'}} mock_client_class.return_value = mock_client handler = ManageFeatureFlagHandler() From e8f95a3921d6ad118b76aa9b665c40bc7695328a Mon Sep 17 00:00:00 2001 From: Landon Shumway Date: Wed, 8 Oct 2025 14:39:39 -0500 Subject: [PATCH 54/55] Add comment about StatSig Users --- .../lambdas/python/feature-flag/feature_flag_client.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/backend/compact-connect/lambdas/python/feature-flag/feature_flag_client.py b/backend/compact-connect/lambdas/python/feature-flag/feature_flag_client.py index 897cb2227..cdc56b74f 100644 --- a/backend/compact-connect/lambdas/python/feature-flag/feature_flag_client.py +++ b/backend/compact-connect/lambdas/python/feature-flag/feature_flag_client.py @@ -249,7 +249,14 @@ def _initialize_statsig(self): raise FeatureFlagException(f'Failed to initialize StatSig client: {e}') from e def _create_statsig_user(self, context: dict[str, Any]) -> StatsigUser: - """Convert context dictionary to StatsigUser""" + """Convert context dictionary to StatsigUser + + The SDK requires a StatSig user object to be passed in whenever checking a feature gate. + See https://docs.statsig.com/concepts/user/#why-is-an-id-always-required-for-server-sdks + """ + # note that we hardcode the user id if it is not provided by the caller, which means percentage based + # rules will have no effect, since StatSig always returns the same result for the same user. If callers + # want to have percentage based rules take effect, they need to pass in user ids with their requests. user_data = { 'user_id': context.get('userId') or 'default_cc_user', } From b6ef5ec779698f01241893c2db89f12d52e041c0 Mon Sep 17 00:00:00 2001 From: Landon Shumway Date: Wed, 8 Oct 2025 15:11:04 -0500 Subject: [PATCH 55/55] remove query group export to avoid clashing with deprecated pattern --- .../compact-connect/stacks/api_stack/v1_api/feature_flags.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/backend/compact-connect/stacks/api_stack/v1_api/feature_flags.py b/backend/compact-connect/stacks/api_stack/v1_api/feature_flags.py index b22937582..3778b21c8 100644 --- a/backend/compact-connect/stacks/api_stack/v1_api/feature_flags.py +++ b/backend/compact-connect/stacks/api_stack/v1_api/feature_flags.py @@ -39,8 +39,6 @@ def __init__( ], ) - self.api.log_groups.append(api_lambda_stack.feature_flags_lambdas.check_feature_flag_function.log_group) - # Add suppressions for the public GET endpoint NagSuppressions.add_resource_suppressions( self.check_flag_method,